mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 06:58:07 -05:00
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:
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user