diff --git a/apps/sim/app/(auth)/verify/page.tsx b/apps/sim/app/(auth)/verify/page.tsx index 7e169b8760..2c6176a020 100644 --- a/apps/sim/app/(auth)/verify/page.tsx +++ b/apps/sim/app/(auth)/verify/page.tsx @@ -1,11 +1,11 @@ -import { env } from '@/lib/env' +import { hasEmailService } from '@/lib/email/mailer' import { isProd } from '@/lib/environment' import { VerifyContent } from '@/app/(auth)/verify/verify-content' export const dynamic = 'force-dynamic' export default function VerifyPage() { - const hasResendKey = Boolean(env.RESEND_API_KEY) + const emailServiceConfigured = hasEmailService() - return + return } diff --git a/apps/sim/app/(auth)/verify/use-verification.ts b/apps/sim/app/(auth)/verify/use-verification.ts index 3d4b1d32a0..9a3db164f6 100644 --- a/apps/sim/app/(auth)/verify/use-verification.ts +++ b/apps/sim/app/(auth)/verify/use-verification.ts @@ -8,7 +8,7 @@ import { createLogger } from '@/lib/logs/console/logger' const logger = createLogger('useVerification') interface UseVerificationParams { - hasResendKey: boolean + hasEmailService: boolean isProduction: boolean } @@ -20,7 +20,7 @@ interface UseVerificationReturn { isInvalidOtp: boolean errorMessage: string isOtpComplete: boolean - hasResendKey: boolean + hasEmailService: boolean isProduction: boolean verifyCode: () => Promise resendCode: () => void @@ -28,7 +28,7 @@ interface UseVerificationReturn { } export function useVerification({ - hasResendKey, + hasEmailService, isProduction, }: UseVerificationParams): UseVerificationReturn { const router = useRouter() @@ -74,10 +74,10 @@ export function useVerification({ }, [searchParams]) useEffect(() => { - if (email && !isSendingInitialOtp && hasResendKey) { + if (email && !isSendingInitialOtp && hasEmailService) { setIsSendingInitialOtp(true) } - }, [email, isSendingInitialOtp, hasResendKey]) + }, [email, isSendingInitialOtp, hasEmailService]) const isOtpComplete = otp.length === 6 @@ -157,7 +157,7 @@ export function useVerification({ } function resendCode() { - if (!email || !hasResendKey) return + if (!email || !hasEmailService) return setIsLoading(true) setErrorMessage('') @@ -197,17 +197,27 @@ export function useVerification({ useEffect(() => { if (typeof window !== 'undefined') { - if (!isProduction || !hasResendKey) { + if (!isProduction && !hasEmailService) { setIsVerified(true) - const timeoutId = setTimeout(() => { - window.location.href = '/workspace' - }, 1000) + const handleRedirect = async () => { + try { + await refetchSession() + } catch (error) { + logger.warn('Failed to refetch session during dev verification skip:', error) + } + + if (isInviteFlow && redirectUrl) { + window.location.href = redirectUrl + } else { + router.push('/workspace') + } + } - return () => clearTimeout(timeoutId) + handleRedirect() } } - }, [isProduction, hasResendKey, router]) + }, [isProduction, hasEmailService, router, isInviteFlow, redirectUrl]) return { otp, @@ -217,7 +227,7 @@ export function useVerification({ isInvalidOtp, errorMessage, isOtpComplete, - hasResendKey, + hasEmailService, isProduction, verifyCode, resendCode, diff --git a/apps/sim/app/(auth)/verify/verify-content.tsx b/apps/sim/app/(auth)/verify/verify-content.tsx index c402bd9015..cb20fdd364 100644 --- a/apps/sim/app/(auth)/verify/verify-content.tsx +++ b/apps/sim/app/(auth)/verify/verify-content.tsx @@ -10,15 +10,15 @@ import { inter } from '@/app/fonts/inter' import { soehne } from '@/app/fonts/soehne/soehne' interface VerifyContentProps { - hasResendKey: boolean + hasEmailService: boolean isProduction: boolean } function VerificationForm({ - hasResendKey, + hasEmailService, isProduction, }: { - hasResendKey: boolean + hasEmailService: boolean isProduction: boolean }) { const { @@ -32,7 +32,7 @@ function VerificationForm({ verifyCode, resendCode, handleOtpChange, - } = useVerification({ hasResendKey, isProduction }) + } = useVerification({ hasEmailService, isProduction }) const [countdown, setCountdown] = useState(0) const [isResendDisabled, setIsResendDisabled] = useState(false) @@ -93,7 +93,7 @@ function VerificationForm({

{isVerified ? 'Your email has been verified. Redirecting to dashboard...' - : hasResendKey + : hasEmailService ? `A verification code has been sent to ${email || 'your email'}` : !isProduction ? 'Development mode: Check your console logs for the verification code' @@ -106,7 +106,7 @@ function VerificationForm({

Enter the 6-digit code to verify your account. - {hasResendKey ? " If you don't see it in your inbox, check your spam folder." : ''} + {hasEmailService ? " If you don't see it in your inbox, check your spam folder." : ''}

@@ -192,7 +192,7 @@ function VerificationForm({ {isLoading ? 'Verifying...' : 'Verify Email'} - {hasResendKey && ( + {hasEmailService && (

Didn't receive a code?{' '} @@ -245,10 +245,10 @@ function VerificationFormFallback() { ) } -export function VerifyContent({ hasResendKey, isProduction }: VerifyContentProps) { +export function VerifyContent({ hasEmailService, isProduction }: VerifyContentProps) { return ( }> - + ) } diff --git a/apps/sim/lib/auth.ts b/apps/sim/lib/auth.ts index e5183db575..3e0b1f0064 100644 --- a/apps/sim/lib/auth.ts +++ b/apps/sim/lib/auth.ts @@ -32,7 +32,7 @@ import { handleInvoicePaymentFailed, handleInvoicePaymentSucceeded, } from '@/lib/billing/webhooks/invoices' -import { sendEmail } from '@/lib/email/mailer' +import { hasEmailService, sendEmail } from '@/lib/email/mailer' import { getFromEmailAddress } from '@/lib/email/utils' import { quickValidateEmail } from '@/lib/email/validation' import { env, isTruthy } from '@/lib/env' @@ -147,7 +147,7 @@ export const auth = betterAuth({ }, emailAndPassword: { enabled: true, - requireEmailVerification: isProd, + requireEmailVerification: isProd && hasEmailService(), sendVerificationOnSignUp: false, throwOnMissingCredentials: true, throwOnInvalidCredentials: true, diff --git a/apps/sim/lib/email/mailer.ts b/apps/sim/lib/email/mailer.ts index 9b12f2d7b1..ab35acc349 100644 --- a/apps/sim/lib/email/mailer.ts +++ b/apps/sim/lib/email/mailer.ts @@ -69,6 +69,13 @@ const azureEmailClient = ? new EmailClient(azureConnectionString) : null +/** + * Check if any email service is configured and available + */ +export function hasEmailService(): boolean { + return !!(resend || azureEmailClient) +} + export async function sendEmail(options: EmailOptions): Promise { try { // Check if user has unsubscribed (skip for critical transactional emails) diff --git a/docker-compose.local.yml b/docker-compose.local.yml index ad42a55e67..768c0cc706 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -10,6 +10,7 @@ services: limits: memory: 8G environment: + - NODE_ENV=development - DATABASE_URL=postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-simstudio} - BETTER_AUTH_URL=${NEXT_PUBLIC_APP_URL:-http://localhost:3000} - NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL:-http://localhost:3000} diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 3c3d6f82e2..c6b79e6c1e 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -9,6 +9,7 @@ services: limits: memory: 8G environment: + - NODE_ENV=production - DATABASE_URL=postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-simstudio} - BETTER_AUTH_URL=${NEXT_PUBLIC_APP_URL:-http://localhost:3000} - NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL:-http://localhost:3000}