fix(verification): add OTP dev skip (#1395)

This commit is contained in:
Waleed
2025-09-20 11:31:41 -07:00
committed by GitHub
parent b5570c1c0e
commit 93f9293f2c
7 changed files with 46 additions and 27 deletions

View File

@@ -1,11 +1,11 @@
import { env } from '@/lib/env' import { hasEmailService } from '@/lib/email/mailer'
import { isProd } from '@/lib/environment' import { isProd } from '@/lib/environment'
import { VerifyContent } from '@/app/(auth)/verify/verify-content' import { VerifyContent } from '@/app/(auth)/verify/verify-content'
export const dynamic = 'force-dynamic' export const dynamic = 'force-dynamic'
export default function VerifyPage() { export default function VerifyPage() {
const hasResendKey = Boolean(env.RESEND_API_KEY) const emailServiceConfigured = hasEmailService()
return <VerifyContent hasResendKey={hasResendKey} isProduction={isProd} /> return <VerifyContent hasEmailService={emailServiceConfigured} isProduction={isProd} />
} }

View File

@@ -8,7 +8,7 @@ import { createLogger } from '@/lib/logs/console/logger'
const logger = createLogger('useVerification') const logger = createLogger('useVerification')
interface UseVerificationParams { interface UseVerificationParams {
hasResendKey: boolean hasEmailService: boolean
isProduction: boolean isProduction: boolean
} }
@@ -20,7 +20,7 @@ interface UseVerificationReturn {
isInvalidOtp: boolean isInvalidOtp: boolean
errorMessage: string errorMessage: string
isOtpComplete: boolean isOtpComplete: boolean
hasResendKey: boolean hasEmailService: boolean
isProduction: boolean isProduction: boolean
verifyCode: () => Promise<void> verifyCode: () => Promise<void>
resendCode: () => void resendCode: () => void
@@ -28,7 +28,7 @@ interface UseVerificationReturn {
} }
export function useVerification({ export function useVerification({
hasResendKey, hasEmailService,
isProduction, isProduction,
}: UseVerificationParams): UseVerificationReturn { }: UseVerificationParams): UseVerificationReturn {
const router = useRouter() const router = useRouter()
@@ -74,10 +74,10 @@ export function useVerification({
}, [searchParams]) }, [searchParams])
useEffect(() => { useEffect(() => {
if (email && !isSendingInitialOtp && hasResendKey) { if (email && !isSendingInitialOtp && hasEmailService) {
setIsSendingInitialOtp(true) setIsSendingInitialOtp(true)
} }
}, [email, isSendingInitialOtp, hasResendKey]) }, [email, isSendingInitialOtp, hasEmailService])
const isOtpComplete = otp.length === 6 const isOtpComplete = otp.length === 6
@@ -157,7 +157,7 @@ export function useVerification({
} }
function resendCode() { function resendCode() {
if (!email || !hasResendKey) return if (!email || !hasEmailService) return
setIsLoading(true) setIsLoading(true)
setErrorMessage('') setErrorMessage('')
@@ -197,17 +197,27 @@ export function useVerification({
useEffect(() => { useEffect(() => {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
if (!isProduction || !hasResendKey) { if (!isProduction && !hasEmailService) {
setIsVerified(true) setIsVerified(true)
const timeoutId = setTimeout(() => { const handleRedirect = async () => {
window.location.href = '/workspace' try {
}, 1000) await refetchSession()
} catch (error) {
logger.warn('Failed to refetch session during dev verification skip:', error)
}
return () => clearTimeout(timeoutId) if (isInviteFlow && redirectUrl) {
window.location.href = redirectUrl
} else {
router.push('/workspace')
}
}
handleRedirect()
} }
} }
}, [isProduction, hasResendKey, router]) }, [isProduction, hasEmailService, router, isInviteFlow, redirectUrl])
return { return {
otp, otp,
@@ -217,7 +227,7 @@ export function useVerification({
isInvalidOtp, isInvalidOtp,
errorMessage, errorMessage,
isOtpComplete, isOtpComplete,
hasResendKey, hasEmailService,
isProduction, isProduction,
verifyCode, verifyCode,
resendCode, resendCode,

View File

@@ -10,15 +10,15 @@ import { inter } from '@/app/fonts/inter'
import { soehne } from '@/app/fonts/soehne/soehne' import { soehne } from '@/app/fonts/soehne/soehne'
interface VerifyContentProps { interface VerifyContentProps {
hasResendKey: boolean hasEmailService: boolean
isProduction: boolean isProduction: boolean
} }
function VerificationForm({ function VerificationForm({
hasResendKey, hasEmailService,
isProduction, isProduction,
}: { }: {
hasResendKey: boolean hasEmailService: boolean
isProduction: boolean isProduction: boolean
}) { }) {
const { const {
@@ -32,7 +32,7 @@ function VerificationForm({
verifyCode, verifyCode,
resendCode, resendCode,
handleOtpChange, handleOtpChange,
} = useVerification({ hasResendKey, isProduction }) } = useVerification({ hasEmailService, isProduction })
const [countdown, setCountdown] = useState(0) const [countdown, setCountdown] = useState(0)
const [isResendDisabled, setIsResendDisabled] = useState(false) const [isResendDisabled, setIsResendDisabled] = useState(false)
@@ -93,7 +93,7 @@ function VerificationForm({
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}> <p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
{isVerified {isVerified
? 'Your email has been verified. Redirecting to dashboard...' ? 'Your email has been verified. Redirecting to dashboard...'
: hasResendKey : hasEmailService
? `A verification code has been sent to ${email || 'your email'}` ? `A verification code has been sent to ${email || 'your email'}`
: !isProduction : !isProduction
? 'Development mode: Check your console logs for the verification code' ? 'Development mode: Check your console logs for the verification code'
@@ -106,7 +106,7 @@ function VerificationForm({
<div className='space-y-6'> <div className='space-y-6'>
<p className='text-center text-muted-foreground text-sm'> <p className='text-center text-muted-foreground text-sm'>
Enter the 6-digit code to verify your account. 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." : ''}
</p> </p>
<div className='flex justify-center'> <div className='flex justify-center'>
@@ -192,7 +192,7 @@ function VerificationForm({
{isLoading ? 'Verifying...' : 'Verify Email'} {isLoading ? 'Verifying...' : 'Verify Email'}
</Button> </Button>
{hasResendKey && ( {hasEmailService && (
<div className='text-center'> <div className='text-center'>
<p className='text-muted-foreground text-sm'> <p className='text-muted-foreground text-sm'>
Didn't receive a code?{' '} Didn't receive a code?{' '}
@@ -245,10 +245,10 @@ function VerificationFormFallback() {
) )
} }
export function VerifyContent({ hasResendKey, isProduction }: VerifyContentProps) { export function VerifyContent({ hasEmailService, isProduction }: VerifyContentProps) {
return ( return (
<Suspense fallback={<VerificationFormFallback />}> <Suspense fallback={<VerificationFormFallback />}>
<VerificationForm hasResendKey={hasResendKey} isProduction={isProduction} /> <VerificationForm hasEmailService={hasEmailService} isProduction={isProduction} />
</Suspense> </Suspense>
) )
} }

View File

@@ -32,7 +32,7 @@ import {
handleInvoicePaymentFailed, handleInvoicePaymentFailed,
handleInvoicePaymentSucceeded, handleInvoicePaymentSucceeded,
} from '@/lib/billing/webhooks/invoices' } 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 { getFromEmailAddress } from '@/lib/email/utils'
import { quickValidateEmail } from '@/lib/email/validation' import { quickValidateEmail } from '@/lib/email/validation'
import { env, isTruthy } from '@/lib/env' import { env, isTruthy } from '@/lib/env'
@@ -147,7 +147,7 @@ export const auth = betterAuth({
}, },
emailAndPassword: { emailAndPassword: {
enabled: true, enabled: true,
requireEmailVerification: isProd, requireEmailVerification: isProd && hasEmailService(),
sendVerificationOnSignUp: false, sendVerificationOnSignUp: false,
throwOnMissingCredentials: true, throwOnMissingCredentials: true,
throwOnInvalidCredentials: true, throwOnInvalidCredentials: true,

View File

@@ -69,6 +69,13 @@ const azureEmailClient =
? new EmailClient(azureConnectionString) ? new EmailClient(azureConnectionString)
: null : null
/**
* Check if any email service is configured and available
*/
export function hasEmailService(): boolean {
return !!(resend || azureEmailClient)
}
export async function sendEmail(options: EmailOptions): Promise<SendEmailResult> { export async function sendEmail(options: EmailOptions): Promise<SendEmailResult> {
try { try {
// Check if user has unsubscribed (skip for critical transactional emails) // Check if user has unsubscribed (skip for critical transactional emails)

View File

@@ -10,6 +10,7 @@ services:
limits: limits:
memory: 8G memory: 8G
environment: environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-simstudio} - DATABASE_URL=postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-simstudio}
- BETTER_AUTH_URL=${NEXT_PUBLIC_APP_URL:-http://localhost:3000} - BETTER_AUTH_URL=${NEXT_PUBLIC_APP_URL:-http://localhost:3000}
- NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL:-http://localhost:3000} - NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL:-http://localhost:3000}

View File

@@ -9,6 +9,7 @@ services:
limits: limits:
memory: 8G memory: 8G
environment: environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-simstudio} - DATABASE_URL=postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-simstudio}
- BETTER_AUTH_URL=${NEXT_PUBLIC_APP_URL:-http://localhost:3000} - BETTER_AUTH_URL=${NEXT_PUBLIC_APP_URL:-http://localhost:3000}
- NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL:-http://localhost:3000} - NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL:-http://localhost:3000}