feat(otp): added environemnt variable to control enforcement of verified accounts (#1411)

* feat(otp): added environemnt variable to control enforcement of verified accounts

* added to helm
This commit is contained in:
Waleed
2025-09-22 11:04:47 -07:00
committed by GitHub
parent 16f5819941
commit e640102797
8 changed files with 53 additions and 20 deletions

View File

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

View File

@@ -10,6 +10,7 @@ const logger = createLogger('useVerification')
interface UseVerificationParams {
hasEmailService: boolean
isProduction: boolean
isEmailVerificationEnabled: boolean
}
interface UseVerificationReturn {
@@ -22,6 +23,7 @@ interface UseVerificationReturn {
isOtpComplete: boolean
hasEmailService: boolean
isProduction: boolean
isEmailVerificationEnabled: boolean
verifyCode: () => Promise<void>
resendCode: () => void
handleOtpChange: (value: string) => void
@@ -30,6 +32,7 @@ interface UseVerificationReturn {
export function useVerification({
hasEmailService,
isProduction,
isEmailVerificationEnabled,
}: UseVerificationParams): UseVerificationReturn {
const router = useRouter()
const searchParams = useSearchParams()
@@ -157,7 +160,7 @@ export function useVerification({
}
function resendCode() {
if (!email || !hasEmailService) return
if (!email || !hasEmailService || !isEmailVerificationEnabled) return
setIsLoading(true)
setErrorMessage('')
@@ -197,14 +200,14 @@ export function useVerification({
useEffect(() => {
if (typeof window !== 'undefined') {
if (!isProduction && !hasEmailService) {
if (!isEmailVerificationEnabled) {
setIsVerified(true)
const handleRedirect = async () => {
try {
await refetchSession()
} catch (error) {
logger.warn('Failed to refetch session during dev verification skip:', error)
logger.warn('Failed to refetch session during verification skip:', error)
}
if (isInviteFlow && redirectUrl) {
@@ -217,7 +220,7 @@ export function useVerification({
handleRedirect()
}
}
}, [isProduction, hasEmailService, router, isInviteFlow, redirectUrl])
}, [isEmailVerificationEnabled, router, isInviteFlow, redirectUrl])
return {
otp,
@@ -229,6 +232,7 @@ export function useVerification({
isOtpComplete,
hasEmailService,
isProduction,
isEmailVerificationEnabled,
verifyCode,
resendCode,
handleOtpChange,

View File

@@ -12,14 +12,17 @@ import { soehne } from '@/app/fonts/soehne/soehne'
interface VerifyContentProps {
hasEmailService: boolean
isProduction: boolean
isEmailVerificationEnabled: boolean
}
function VerificationForm({
hasEmailService,
isProduction,
isEmailVerificationEnabled,
}: {
hasEmailService: boolean
isProduction: boolean
isEmailVerificationEnabled: boolean
}) {
const {
otp,
@@ -32,7 +35,7 @@ function VerificationForm({
verifyCode,
resendCode,
handleOtpChange,
} = useVerification({ hasEmailService, isProduction })
} = useVerification({ hasEmailService, isProduction, isEmailVerificationEnabled })
const [countdown, setCountdown] = useState(0)
const [isResendDisabled, setIsResendDisabled] = useState(false)
@@ -93,15 +96,17 @@ function VerificationForm({
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
{isVerified
? 'Your email has been verified. Redirecting to dashboard...'
: hasEmailService
? `A verification code has been sent to ${email || 'your email'}`
: !isProduction
? 'Development mode: Check your console logs for the verification code'
: 'Error: Invalid API key configuration'}
: !isEmailVerificationEnabled
? 'Email verification is disabled. Redirecting to dashboard...'
: hasEmailService
? `A verification code has been sent to ${email || 'your email'}`
: !isProduction
? 'Development mode: Check your console logs for the verification code'
: 'Error: Email verification is enabled but no email service is configured'}
</p>
</div>
{!isVerified && (
{!isVerified && isEmailVerificationEnabled && (
<div className={`${inter.className} mt-8 space-y-8`}>
<div className='space-y-6'>
<p className='text-center text-muted-foreground text-sm'>
@@ -245,10 +250,18 @@ function VerificationFormFallback() {
)
}
export function VerifyContent({ hasEmailService, isProduction }: VerifyContentProps) {
export function VerifyContent({
hasEmailService,
isProduction,
isEmailVerificationEnabled,
}: VerifyContentProps) {
return (
<Suspense fallback={<VerificationFormFallback />}>
<VerificationForm hasEmailService={hasEmailService} isProduction={isProduction} />
<VerificationForm
hasEmailService={hasEmailService}
isProduction={isProduction}
isEmailVerificationEnabled={isEmailVerificationEnabled}
/>
</Suspense>
)
}

View File

@@ -32,11 +32,11 @@ import {
handleInvoicePaymentFailed,
handleInvoicePaymentSucceeded,
} from '@/lib/billing/webhooks/invoices'
import { hasEmailService, sendEmail } from '@/lib/email/mailer'
import { sendEmail } from '@/lib/email/mailer'
import { getFromEmailAddress } from '@/lib/email/utils'
import { quickValidateEmail } from '@/lib/email/validation'
import { env, isTruthy } from '@/lib/env'
import { isBillingEnabled, isProd } from '@/lib/environment'
import { isBillingEnabled, isEmailVerificationEnabled } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console/logger'
const logger = createLogger('Auth')
@@ -165,7 +165,7 @@ export const auth = betterAuth({
},
emailAndPassword: {
enabled: true,
requireEmailVerification: isProd && hasEmailService(),
requireEmailVerification: isEmailVerificationEnabled,
sendVerificationOnSignUp: false,
throwOnMissingCredentials: true,
throwOnInvalidCredentials: true,
@@ -240,8 +240,8 @@ export const auth = betterAuth({
otp: string
type: 'sign-in' | 'email-verification' | 'forget-password'
}) => {
if (!isProd) {
logger.info('Skipping email verification in dev/docker')
if (!isEmailVerificationEnabled) {
logger.info('Skipping email verification')
return
}
try {

View File

@@ -51,6 +51,7 @@ export const env = createEnv({
BILLING_ENABLED: z.boolean().optional(), // Enable billing enforcement and usage tracking
// Email & Communication
EMAIL_VERIFICATION_ENABLED: z.boolean().optional(), // Enable email verification for user registration and login (defaults to false)
RESEND_API_KEY: z.string().min(1).optional(), // Resend API key for transactional emails
FROM_EMAIL_ADDRESS: z.string().min(1).optional(), // Complete from address (e.g., "Sim <noreply@domain.com>" or "noreply@domain.com")
EMAIL_DOMAIN: z.string().min(1).optional(), // Domain for sending emails (fallback when FROM_EMAIL_ADDRESS not set)

View File

@@ -30,6 +30,11 @@ export const isHosted =
*/
export const isBillingEnabled = isTruthy(env.BILLING_ENABLED)
/**
* Is email verification enabled
*/
export const isEmailVerificationEnabled = isTruthy(env.EMAIL_VERIFICATION_ENABLED)
/**
* Get cost multiplier based on environment
*/

View File

@@ -30,6 +30,9 @@ app:
BETTER_AUTH_SECRET: "your-production-auth-secret-here"
ENCRYPTION_KEY: "your-production-encryption-key-here"
# Email verification (set to true if you want to require email verification)
EMAIL_VERIFICATION_ENABLED: "false"
# Optional third-party service integrations (configure as needed)
RESEND_API_KEY: "your-resend-api-key"
GOOGLE_CLIENT_ID: "your-google-client-id"

View File

@@ -65,6 +65,7 @@ app:
ENCRYPTION_KEY: "" # REQUIRED - set via --set flag or external secret manager
# Email & Communication
EMAIL_VERIFICATION_ENABLED: "false" # Enable email verification for user registration and login (defaults to false)
RESEND_API_KEY: "" # Resend API key for transactional emails
FROM_EMAIL_ADDRESS: "" # Complete from address (e.g., "Sim <noreply@domain.com>" or "DoNotReply@domain.com")
EMAIL_DOMAIN: "" # Domain for sending emails (fallback when FROM_EMAIL_ADDRESS not set)