diff --git a/Dockerfile b/Dockerfile
index fdbc62940..272f7f971 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -6,6 +6,9 @@ WORKDIR /app
# Copy the entire sim directory
COPY sim/ ./
+# Create the .env file if it doesn't exist
+RUN touch .env
+
# Install dependencies
RUN npm install
diff --git a/docker-compose.yml b/docker-compose.yml
index d803306a3..9561f2b08 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -17,6 +17,14 @@ services:
- POSTGRES_URL=postgresql://postgres:postgres@db:5432/simstudio
- BETTER_AUTH_URL=http://localhost:3000
- NEXT_PUBLIC_APP_URL=http://localhost:3000
+ - BETTER_AUTH_SECRET=your_auth_secret_here
+ - ENCRYPTION_KEY=your_encryption_key_here
+ - GOOGLE_CLIENT_ID=placeholder
+ - GOOGLE_CLIENT_SECRET=placeholder
+ - GITHUB_CLIENT_ID=placeholder
+ - GITHUB_CLIENT_SECRET=placeholder
+ - RESEND_API_KEY=placeholder
+ - WEBCONTAINER_CLIENT_ID=placeholder
depends_on:
db:
condition: service_healthy
diff --git a/sim/app/(auth)/components/oauth-provider-checker.tsx b/sim/app/(auth)/components/oauth-provider-checker.tsx
new file mode 100644
index 000000000..9abcb46a2
--- /dev/null
+++ b/sim/app/(auth)/components/oauth-provider-checker.tsx
@@ -0,0 +1,21 @@
+'use server'
+
+export async function getOAuthProviderStatus() {
+ const githubAvailable = !!(
+ process.env.GITHUB_CLIENT_ID &&
+ process.env.GITHUB_CLIENT_SECRET &&
+ process.env.GITHUB_CLIENT_ID !== 'placeholder' &&
+ process.env.GITHUB_CLIENT_SECRET !== 'placeholder'
+ )
+
+ const googleAvailable = !!(
+ process.env.GOOGLE_CLIENT_ID &&
+ process.env.GOOGLE_CLIENT_SECRET &&
+ process.env.GOOGLE_CLIENT_ID !== 'placeholder' &&
+ process.env.GOOGLE_CLIENT_SECRET !== 'placeholder'
+ )
+
+ const isProduction = process.env.NODE_ENV === 'production'
+
+ return { githubAvailable, googleAvailable, isProduction }
+}
diff --git a/sim/app/(auth)/components/social-login-buttons.tsx b/sim/app/(auth)/components/social-login-buttons.tsx
new file mode 100644
index 000000000..358902b24
--- /dev/null
+++ b/sim/app/(auth)/components/social-login-buttons.tsx
@@ -0,0 +1,158 @@
+'use client'
+
+import { useState } from 'react'
+import { GithubIcon, GoogleIcon } from '@/components/icons'
+import { Button } from '@/components/ui/button'
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
+import { client } from '@/lib/auth-client'
+import { useNotificationStore } from '@/stores/notifications/store'
+
+interface SocialLoginButtonsProps {
+ githubAvailable: boolean
+ googleAvailable: boolean
+ callbackURL?: string
+ isProduction: boolean
+}
+
+export function SocialLoginButtons({
+ githubAvailable,
+ googleAvailable,
+ callbackURL = '/w',
+ isProduction,
+}: SocialLoginButtonsProps) {
+ const [isGithubLoading, setIsGithubLoading] = useState(false)
+ const [isGoogleLoading, setIsGoogleLoading] = useState(false)
+ const { addNotification } = useNotificationStore()
+
+ async function signInWithGithub() {
+ if (!githubAvailable) return
+
+ setIsGithubLoading(true)
+ try {
+ await client.signIn.social({ provider: 'github', callbackURL })
+ } catch (err: any) {
+ let errorMessage = 'Failed to sign in with GitHub'
+
+ if (err.message?.includes('account exists')) {
+ errorMessage = 'An account with this email already exists. Please sign in instead.'
+ } else if (err.message?.includes('cancelled')) {
+ errorMessage = 'GitHub sign in was cancelled. Please try again.'
+ } else if (err.message?.includes('network')) {
+ errorMessage = 'Network error. Please check your connection and try again.'
+ } else if (err.message?.includes('rate limit')) {
+ errorMessage = 'Too many attempts. Please try again later.'
+ }
+
+ addNotification('error', errorMessage, null)
+ } finally {
+ setIsGithubLoading(false)
+ }
+ }
+
+ async function signInWithGoogle() {
+ if (!googleAvailable) return
+
+ setIsGoogleLoading(true)
+ try {
+ await client.signIn.social({ provider: 'google', callbackURL })
+ } catch (err: any) {
+ let errorMessage = 'Failed to sign in with Google'
+
+ if (err.message?.includes('account exists')) {
+ errorMessage = 'An account with this email already exists. Please sign in instead.'
+ } else if (err.message?.includes('cancelled')) {
+ errorMessage = 'Google sign in was cancelled. Please try again.'
+ } else if (err.message?.includes('network')) {
+ errorMessage = 'Network error. Please check your connection and try again.'
+ } else if (err.message?.includes('rate limit')) {
+ errorMessage = 'Too many attempts. Please try again later.'
+ }
+
+ addNotification('error', errorMessage, null)
+ } finally {
+ setIsGoogleLoading(false)
+ }
+ }
+
+ const githubButton = (
+
+ )
+
+ const googleButton = (
+
+ )
+
+ // Early return for production mode
+ if (isProduction) return null
+
+ const renderGithubButton = () => {
+ if (githubAvailable) return githubButton
+
+ return (
+
+
+
+ {githubButton}
+
+
+
+ GitHub login requires OAuth credentials to be configured. Add the following
+ environment variables:
+
+
+ - • GITHUB_CLIENT_ID
+ - • GITHUB_CLIENT_SECRET
+
+
+
+
+ )
+ }
+
+ const renderGoogleButton = () => {
+ if (googleAvailable) return googleButton
+
+ return (
+
+
+
+ {googleButton}
+
+
+
+ Google login requires OAuth credentials to be configured. Add the following
+ environment variables:
+
+
+ - • GOOGLE_CLIENT_ID
+ - • GOOGLE_CLIENT_SECRET
+
+
+
+
+ )
+ }
+
+ return (
+
+ {renderGithubButton()}
+ {renderGoogleButton()}
+
+ )
+}
diff --git a/sim/app/(auth)/login/login-form.tsx b/sim/app/(auth)/login/login-form.tsx
new file mode 100644
index 000000000..2b9374cb4
--- /dev/null
+++ b/sim/app/(auth)/login/login-form.tsx
@@ -0,0 +1,165 @@
+'use client'
+
+import { useEffect, useState } from 'react'
+import Link from 'next/link'
+import { useRouter } from 'next/navigation'
+import { Button } from '@/components/ui/button'
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from '@/components/ui/card'
+import { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
+import { client } from '@/lib/auth-client'
+import { useNotificationStore } from '@/stores/notifications/store'
+import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
+import { NotificationList } from '@/app/w/[id]/components/notifications/notifications'
+
+export default function LoginPage({
+ githubAvailable,
+ googleAvailable,
+ isProduction,
+}: {
+ githubAvailable: boolean
+ googleAvailable: boolean
+ isProduction: boolean
+}) {
+ const router = useRouter()
+ const [isLoading, setIsLoading] = useState(false)
+ const [mounted, setMounted] = useState(false)
+ const { addNotification } = useNotificationStore()
+
+ useEffect(() => {
+ setMounted(true)
+ }, [])
+
+ async function onSubmit(e: React.FormEvent) {
+ e.preventDefault()
+ setIsLoading(true)
+
+ const formData = new FormData(e.currentTarget)
+ const email = formData.get('email') as string
+ const password = formData.get('password') as string
+
+ try {
+ const result = await client.signIn.email({
+ email,
+ password,
+ callbackURL: '/w',
+ })
+
+ if (!result || result.error) {
+ throw new Error(result?.error?.message || 'Authentication failed')
+ }
+ } catch (err: any) {
+ let errorMessage = 'Invalid email or password'
+
+ if (err.message?.includes('not verified')) {
+ // Redirect to verification page directly without asking for confirmation
+ try {
+ // Send a new verification OTP
+ await client.emailOtp.sendVerificationOtp({
+ email,
+ type: 'email-verification',
+ })
+
+ // Redirect to the verify page
+ router.push(`/verify?email=${encodeURIComponent(email)}`)
+ return
+ } catch (verifyErr) {
+ errorMessage = 'Failed to send verification code. Please try again later.'
+ }
+ } else if (err.message?.includes('not found')) {
+ errorMessage = 'No account found with this email. Please sign up first.'
+ } else if (err.message?.includes('invalid password')) {
+ errorMessage = 'Invalid password. Please try again or use the forgot password link.'
+ } else if (err.message?.includes('too many attempts')) {
+ errorMessage = 'Too many login attempts. Please try again later or reset your password.'
+ } else if (err.message?.includes('account locked')) {
+ errorMessage = 'Your account has been locked for security. Please reset your password.'
+ } else if (err.message?.includes('network')) {
+ errorMessage = 'Network error. Please check your connection and try again.'
+ } else if (err.message?.includes('rate limit')) {
+ errorMessage = 'Too many requests. Please wait a moment before trying again.'
+ }
+
+ addNotification('error', errorMessage, null)
+ // Prevent navigation on error
+ return
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+
+ {mounted && }
+
+
Sim Studio
+
+
+ Welcome back
+ Enter your credentials to access your account
+
+
+
+
+
+
+
+
+
+ Or continue with
+
+
+
+
+
+
+
+ Don't have an account?{' '}
+
+ Sign up
+
+
+
+
+
+
+ )
+}
diff --git a/sim/app/(auth)/login/page.tsx b/sim/app/(auth)/login/page.tsx
index 6d3d267fa..8933a6985 100644
--- a/sim/app/(auth)/login/page.tsx
+++ b/sim/app/(auth)/login/page.tsx
@@ -1,194 +1,14 @@
-'use client'
+import { getOAuthProviderStatus } from '../components/oauth-provider-checker'
+import LoginForm from './login-form'
-import { useState } from 'react'
-import Link from 'next/link'
-import { useRouter } from 'next/navigation'
-import { GithubIcon, GoogleIcon } from '@/components/icons'
-import { Button } from '@/components/ui/button'
-import {
- Card,
- CardContent,
- CardDescription,
- CardFooter,
- CardHeader,
- CardTitle,
-} from '@/components/ui/card'
-import { Input } from '@/components/ui/input'
-import { Label } from '@/components/ui/label'
-import { client } from '@/lib/auth-client'
-import { useNotificationStore } from '@/stores/notifications/store'
-import { NotificationList } from '@/app/w/[id]/components/notifications/notifications'
-
-export default function LoginPage() {
- const router = useRouter()
- const [isLoading, setIsLoading] = useState(false)
- const { addNotification } = useNotificationStore()
-
- async function onSubmit(e: React.FormEvent) {
- e.preventDefault()
- setIsLoading(true)
-
- const formData = new FormData(e.currentTarget)
- const email = formData.get('email') as string
- const password = formData.get('password') as string
-
- try {
- const result = await client.signIn.email({
- email,
- password,
- callbackURL: '/w',
- })
-
- if (!result || result.error) {
- throw new Error(result?.error?.message || 'Authentication failed')
- }
- } catch (err: any) {
- let errorMessage = 'Invalid email or password'
-
- if (err.message?.includes('not verified')) {
- // Redirect to verification page directly without asking for confirmation
- try {
- // Send a new verification OTP
- await client.emailOtp.sendVerificationOtp({
- email,
- type: 'email-verification',
- })
-
- // Redirect to the verify page
- router.push(`/verify?email=${encodeURIComponent(email)}`)
- return
- } catch (verifyErr) {
- errorMessage = 'Failed to send verification code. Please try again later.'
- }
- } else if (err.message?.includes('not found')) {
- errorMessage = 'No account found with this email. Please sign up first.'
- } else if (err.message?.includes('invalid password')) {
- errorMessage = 'Invalid password. Please try again or use the forgot password link.'
- } else if (err.message?.includes('too many attempts')) {
- errorMessage = 'Too many login attempts. Please try again later or reset your password.'
- } else if (err.message?.includes('account locked')) {
- errorMessage = 'Your account has been locked for security. Please reset your password.'
- } else if (err.message?.includes('network')) {
- errorMessage = 'Network error. Please check your connection and try again.'
- } else if (err.message?.includes('rate limit')) {
- errorMessage = 'Too many requests. Please wait a moment before trying again.'
- }
-
- addNotification('error', errorMessage, null)
- // Prevent navigation on error
- return
- } finally {
- setIsLoading(false)
- }
- }
-
- async function signInWithGithub() {
- try {
- await client.signIn.social({ provider: 'github', callbackURL: '/w' })
- } catch (err: any) {
- let errorMessage = 'Failed to sign in with GitHub'
-
- if (err.message?.includes('account exists')) {
- errorMessage =
- 'An account with this email already exists. Please sign in with email instead.'
- } else if (err.message?.includes('cancelled')) {
- errorMessage = 'GitHub sign in was cancelled. Please try again.'
- } else if (err.message?.includes('network')) {
- errorMessage = 'Network error. Please check your connection and try again.'
- }
-
- addNotification('error', errorMessage, null)
- }
- }
-
- async function signInWithGoogle() {
- try {
- await client.signIn.social({ provider: 'google', callbackURL: '/w' })
- } catch (err: any) {
- let errorMessage = 'Failed to sign in with Google'
-
- if (err.message?.includes('account exists')) {
- errorMessage =
- 'An account with this email already exists. Please sign in with email instead.'
- } else if (err.message?.includes('cancelled')) {
- errorMessage = 'Google sign in was cancelled. Please try again.'
- } else if (err.message?.includes('network')) {
- errorMessage = 'Network error. Please check your connection and try again.'
- }
-
- addNotification('error', errorMessage, null)
- }
- }
+export default async function LoginPage() {
+ const { githubAvailable, googleAvailable, isProduction } = await getOAuthProviderStatus()
return (
-
-
-
-
Sim Studio
-
-
- Welcome back
- Enter your credentials to access your account
-
-
-
-
-
-
-
-
-
-
-
-
- Or continue with
-
-
-
-
-
-
-
- Don't have an account?{' '}
-
- Sign up
-
-
-
-
-
-
+
)
}
diff --git a/sim/app/(auth)/signup/page.tsx b/sim/app/(auth)/signup/page.tsx
index e9c7f3b47..cbd46bcac 100644
--- a/sim/app/(auth)/signup/page.tsx
+++ b/sim/app/(auth)/signup/page.tsx
@@ -1,175 +1,14 @@
-'use client'
+import { getOAuthProviderStatus } from '../components/oauth-provider-checker'
+import SignupForm from './signup-form'
-import { useState } from 'react'
-import Link from 'next/link'
-import { useRouter } from 'next/navigation'
-import { GithubIcon, GoogleIcon } from '@/components/icons'
-import { Button } from '@/components/ui/button'
-import {
- Card,
- CardContent,
- CardDescription,
- CardFooter,
- CardHeader,
- CardTitle,
-} from '@/components/ui/card'
-import { Input } from '@/components/ui/input'
-import { Label } from '@/components/ui/label'
-import { client } from '@/lib/auth-client'
-import { useNotificationStore } from '@/stores/notifications/store'
-import { NotificationList } from '@/app/w/[id]/components/notifications/notifications'
-
-export default function SignupPage() {
- const router = useRouter()
- const [isLoading, setIsLoading] = useState(false)
- const { addNotification } = useNotificationStore()
-
- async function onSubmit(e: React.FormEvent) {
- e.preventDefault()
- setIsLoading(true)
-
- const formData = new FormData(e.currentTarget)
- const email = formData.get('email') as string
- const password = formData.get('password') as string
- const name = formData.get('name') as string
-
- try {
- await client.signUp.email({ email, password, name })
-
- // Pass fromSignup=true to indicate we're coming from signup
- router.push(`/verify?email=${encodeURIComponent(email)}&fromSignup=true`)
- } catch (err: any) {
- let errorMessage = 'Failed to create account'
-
- if (err.message?.includes('Password is too short')) {
- errorMessage = 'Password must be at least 8 characters long'
- } else if (err.message?.includes('existing email')) {
- errorMessage = 'An account with this email already exists. Please sign in instead.'
- } else if (err.message?.includes('invalid email')) {
- errorMessage = 'Please enter a valid email address'
- } else if (err.message?.includes('password too long')) {
- errorMessage = 'Password must be less than 128 characters'
- } else if (err.message?.includes('rate limit')) {
- errorMessage = 'Too many signup attempts. Please try again later.'
- } else if (err.message?.includes('network')) {
- errorMessage = 'Network error. Please check your connection and try again.'
- } else if (err.message?.includes('invalid name')) {
- errorMessage = 'Please enter a valid name'
- }
-
- addNotification('error', errorMessage, null)
- } finally {
- setIsLoading(false)
- }
- }
-
- async function signUpWithGithub() {
- try {
- await client.signIn.social({ provider: 'github', callbackURL: '/w' })
- } catch (err: any) {
- let errorMessage = 'Failed to sign up with GitHub'
-
- if (err.message?.includes('account exists')) {
- errorMessage = 'An account with this email already exists. Please sign in instead.'
- } else if (err.message?.includes('cancelled')) {
- errorMessage = 'GitHub sign up was cancelled. Please try again.'
- } else if (err.message?.includes('network')) {
- errorMessage = 'Network error. Please check your connection and try again.'
- } else if (err.message?.includes('rate limit')) {
- errorMessage = 'Too many attempts. Please try again later.'
- }
-
- addNotification('error', errorMessage, null)
- }
- }
-
- async function signUpWithGoogle() {
- try {
- await client.signIn.social({ provider: 'google', callbackURL: '/w' })
- } catch (err: any) {
- let errorMessage = 'Failed to sign up with Google'
-
- if (err.message?.includes('account exists')) {
- errorMessage = 'An account with this email already exists. Please sign in instead.'
- } else if (err.message?.includes('cancelled')) {
- errorMessage = 'Google sign up was cancelled. Please try again.'
- } else if (err.message?.includes('network')) {
- errorMessage = 'Network error. Please check your connection and try again.'
- } else if (err.message?.includes('rate limit')) {
- errorMessage = 'Too many attempts. Please try again later.'
- }
-
- addNotification('error', errorMessage, null)
- }
- }
+export default async function SignupPage() {
+ const { githubAvailable, googleAvailable, isProduction } = await getOAuthProviderStatus()
return (
-
-
-
-
Sim Studio
-
-
- Create an account
- Enter your details to get started
-
-
-
-
-
-
-
-
-
-
-
-
- Or continue with
-
-
-
-
-
-
-
- Already have an account?{' '}
-
- Sign in
-
-
-
-
-
-
+
)
}
diff --git a/sim/app/(auth)/signup/signup-form.tsx b/sim/app/(auth)/signup/signup-form.tsx
new file mode 100644
index 000000000..208d861ef
--- /dev/null
+++ b/sim/app/(auth)/signup/signup-form.tsx
@@ -0,0 +1,151 @@
+'use client'
+
+import { useEffect, useState } from 'react'
+import Link from 'next/link'
+import { useRouter } from 'next/navigation'
+import { Button } from '@/components/ui/button'
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from '@/components/ui/card'
+import { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
+import { client } from '@/lib/auth-client'
+import { useNotificationStore } from '@/stores/notifications/store'
+import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
+import { NotificationList } from '@/app/w/[id]/components/notifications/notifications'
+
+export default function SignupPage({
+ githubAvailable,
+ googleAvailable,
+ isProduction,
+}: {
+ githubAvailable: boolean
+ googleAvailable: boolean
+ isProduction: boolean
+}) {
+ const router = useRouter()
+ const [isLoading, setIsLoading] = useState(false)
+ const [mounted, setMounted] = useState(false)
+ const { addNotification } = useNotificationStore()
+
+ useEffect(() => {
+ setMounted(true)
+ }, [])
+
+ async function onSubmit(e: React.FormEvent) {
+ e.preventDefault()
+ setIsLoading(true)
+
+ const formData = new FormData(e.currentTarget)
+ const email = formData.get('email') as string
+ const password = formData.get('password') as string
+ const name = formData.get('name') as string
+
+ try {
+ // Validate password length before attempting signup
+ if (password.length < 8) {
+ addNotification('error', 'Password must be at least 8 characters long', null)
+ setIsLoading(false)
+ return
+ }
+
+ await client.signUp.email({ email, password, name })
+
+ // Pass fromSignup=true to indicate we're coming from signup
+ router.push(`/verify?email=${encodeURIComponent(email)}&fromSignup=true`)
+ } catch (err: any) {
+ let errorMessage = 'Failed to create account'
+
+ if (err.message?.includes('Password is too short')) {
+ errorMessage = 'Password must be at least 8 characters long'
+ } else if (err.message?.includes('existing email')) {
+ errorMessage = 'An account with this email already exists. Please sign in instead.'
+ } else if (err.message?.includes('invalid email')) {
+ errorMessage = 'Please enter a valid email address'
+ } else if (err.message?.includes('password too long')) {
+ errorMessage = 'Password must be less than 128 characters'
+ } else if (err.message?.includes('rate limit')) {
+ errorMessage = 'Too many signup attempts. Please try again later.'
+ } else if (err.message?.includes('network')) {
+ errorMessage = 'Network error. Please check your connection and try again.'
+ } else if (err.message?.includes('invalid name')) {
+ errorMessage = 'Please enter a valid name'
+ }
+
+ addNotification('error', errorMessage, null)
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+
+ {mounted && }
+
+
Sim Studio
+
+
+ Create an account
+ Enter your details to get started
+
+
+
+
+
+
+
+
+
+ Or continue with
+
+
+
+
+
+
+
+ Already have an account?{' '}
+
+ Sign in
+
+
+
+
+
+
+ )
+}
diff --git a/sim/app/(auth)/verify/page.tsx b/sim/app/(auth)/verify/page.tsx
index 80a103fbe..6c71c10ef 100644
--- a/sim/app/(auth)/verify/page.tsx
+++ b/sim/app/(auth)/verify/page.tsx
@@ -1,200 +1,25 @@
-'use client'
-
-import { Suspense, useEffect, useState } from 'react'
-import { useRouter, useSearchParams } from 'next/navigation'
-import { Button } from '@/components/ui/button'
-import {
- Card,
- CardContent,
- CardDescription,
- CardFooter,
- CardHeader,
- CardTitle,
-} from '@/components/ui/card'
-import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/ui/input-otp'
-import { client } from '@/lib/auth-client'
-import { createLogger } from '@/lib/logs/console-logger'
-import { useNotificationStore } from '@/stores/notifications/store'
-
-const logger = createLogger('VerifyPage')
-
-// Extract the content into a separate component
-function VerifyContent() {
- const router = useRouter()
- const searchParams = useSearchParams()
- const { addNotification } = useNotificationStore()
- const [otp, setOtp] = useState('')
- const [email, setEmail] = useState('')
- const [isLoading, setIsLoading] = useState(false)
- const [isVerified, setIsVerified] = useState(false)
- const [isSendingInitialOtp, setIsSendingInitialOtp] = useState(false)
-
- // Get email from URL query param
- useEffect(() => {
- const emailParam = searchParams.get('email')
- if (emailParam) {
- setEmail(decodeURIComponent(emailParam))
- }
- }, [searchParams])
-
- // Send initial OTP code if this is the first load
- useEffect(() => {
- if (email && !isSendingInitialOtp) {
- setIsSendingInitialOtp(true)
-
- // Only send verification OTP if we're coming from login page
- // Skip this if coming from signup since the OTP is already sent
- if (!searchParams.get('fromSignup')) {
- client.emailOtp
- .sendVerificationOtp({
- email,
- type: 'email-verification',
- })
- .then(() => {})
- .catch((error) => {
- logger.error('Failed to send initial verification code:', error)
- addNotification?.(
- 'error',
- 'Failed to send verification code. Please use the resend button.',
- null
- )
- })
- }
- }
- }, [email, isSendingInitialOtp, addNotification, searchParams])
-
- // Enable the verify button when all 6 digits are entered
- const isOtpComplete = otp.length === 6
-
- async function verifyCode() {
- if (!isOtpComplete || !email) return
-
- setIsLoading(true)
-
- try {
- // Call the verification API with the OTP code
- await client.emailOtp.verifyEmail({
- email,
- otp,
- })
-
- setIsVerified(true)
-
- // Redirect to dashboard after a short delay
- setTimeout(() => router.push('/w'), 2000)
- } catch (error: any) {
- let errorMessage = 'Verification failed. Please check your code and try again.'
-
- if (error.message?.includes('expired')) {
- errorMessage = 'The verification code has expired. Please request a new one.'
- } else if (error.message?.includes('invalid')) {
- errorMessage = 'Invalid verification code. Please check and try again.'
- } else if (error.message?.includes('attempts')) {
- errorMessage = 'Too many failed attempts. Please request a new code.'
- }
-
- addNotification?.('error', errorMessage, null)
- } finally {
- setIsLoading(false)
- }
- }
-
- function resendCode() {
- if (!email) return
-
- setIsLoading(true)
-
- client.emailOtp
- .sendVerificationOtp({
- email,
- type: 'email-verification',
- })
- .then(() => {})
- .catch(() => {
- addNotification?.(
- 'error',
- 'Failed to resend verification code. Please try again later.',
- null
- )
- })
- .finally(() => {
- setIsLoading(false)
- })
- }
-
- return (
-
-
- {isVerified ? 'Email Verified!' : 'Verify your email'}
-
- {isVerified
- ? 'Your email has been verified. Redirecting to dashboard...'
- : `A verification code has been sent to ${email || 'your email'}`}
-
-
-
- {!isVerified && (
-
-
- Enter the 6-digit code from your email to verify your account. If you don't see it,
- check your spam folder.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )}
-
- {!isVerified && (
-
-
- Didn't receive a code?{' '}
-
-
-
- )}
-
- )
-}
+import { VerifyContent } from './verify-content'
export default function VerifyPage() {
+ const protocol = process.env.NODE_ENV === 'development' ? 'http' : 'https'
+ const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'localhost:3000'
+ const baseUrl = `${protocol}://${appUrl}`
+
+ const hasResendKey = Boolean(
+ process.env.RESEND_API_KEY &&
+ process.env.RESEND_API_KEY !== 'placeholder'
+ )
+ const isProduction = process.env.NODE_ENV === 'production'
+
return (
Sim Studio
-
-
- Verify your email
- Loading verification page...
-
-
-
-
-
- }
- >
-
-
+
)
diff --git a/sim/app/(auth)/verify/use-verification.ts b/sim/app/(auth)/verify/use-verification.ts
new file mode 100644
index 000000000..7bd6f4753
--- /dev/null
+++ b/sim/app/(auth)/verify/use-verification.ts
@@ -0,0 +1,177 @@
+'use client'
+
+import { useEffect, useState } from 'react'
+import { useRouter, useSearchParams } from 'next/navigation'
+import { client } from '@/lib/auth-client'
+import { createLogger } from '@/lib/logs/console-logger'
+import { useNotificationStore } from '@/stores/notifications/store'
+
+const logger = createLogger('useVerification')
+
+interface UseVerificationParams {
+ hasResendKey: boolean
+ isProduction: boolean
+}
+
+interface UseVerificationReturn {
+ otp: string
+ email: string
+ isLoading: boolean
+ isVerified: boolean
+ isInvalidOtp: boolean
+ errorMessage: string
+ isOtpComplete: boolean
+ hasResendKey: boolean
+ isProduction: boolean
+ verifyCode: () => Promise
+ resendCode: () => void
+ handleOtpChange: (value: string) => void
+}
+
+export function useVerification({ hasResendKey, isProduction }: UseVerificationParams): UseVerificationReturn {
+ const router = useRouter()
+ const searchParams = useSearchParams()
+ const { addNotification } = useNotificationStore()
+ const [otp, setOtp] = useState('')
+ const [email, setEmail] = useState('')
+ const [isLoading, setIsLoading] = useState(false)
+ const [isVerified, setIsVerified] = useState(false)
+ const [isSendingInitialOtp, setIsSendingInitialOtp] = useState(false)
+ const [isInvalidOtp, setIsInvalidOtp] = useState(false)
+ const [errorMessage, setErrorMessage] = useState('')
+
+ // Debug notification store
+ useEffect(() => {
+ logger.info('Notification store state:', { addNotification: !!addNotification })
+ }, [addNotification])
+
+ // Get email from URL query param
+ useEffect(() => {
+ const emailParam = searchParams.get('email')
+ if (emailParam) {
+ setEmail(decodeURIComponent(emailParam))
+ }
+ }, [searchParams])
+
+ // Send initial OTP code if this is the first load
+ useEffect(() => {
+ if (email && !isSendingInitialOtp && hasResendKey) {
+ setIsSendingInitialOtp(true)
+
+ // Only send verification OTP if we're coming from login page
+ // Skip this if coming from signup since the OTP is already sent
+ if (!searchParams.get('fromSignup')) {
+ client.emailOtp
+ .sendVerificationOtp({
+ email,
+ type: 'email-verification',
+ })
+ .then(() => {})
+ .catch((error) => {
+ logger.error('Failed to send initial verification code:', error)
+ setErrorMessage('Failed to send verification code. Please use the resend button.')
+ })
+ }
+ }
+ }, [email, isSendingInitialOtp, searchParams, hasResendKey])
+
+ // Enable the verify button when all 6 digits are entered
+ const isOtpComplete = otp.length === 6
+
+ async function verifyCode() {
+ if (!isOtpComplete || !email) return
+
+ setIsLoading(true)
+ setIsInvalidOtp(false)
+ setErrorMessage('')
+
+ try {
+ // Call the verification API with the OTP code
+ const response = await client.emailOtp.verifyEmail({
+ email,
+ otp,
+ })
+
+ // Check if verification was successful
+ if (response && !response.error) {
+ setIsVerified(true)
+ // Redirect to dashboard after a short delay
+ setTimeout(() => router.push('/w'), 2000)
+ } else {
+ logger.info('Setting invalid OTP state - API error response')
+ const message = 'Invalid verification code. Please check and try again.'
+ // Set both state variables to ensure the error shows
+ setIsInvalidOtp(true)
+ setErrorMessage(message)
+ logger.info('Error state after API error:', { isInvalidOtp: true, errorMessage: message })
+ // Clear the OTP input on invalid code
+ setOtp('')
+ }
+ } catch (error: any) {
+ let message = 'Verification failed. Please check your code and try again.'
+
+ if (error.message?.includes('expired')) {
+ message = 'The verification code has expired. Please request a new one.'
+ } else if (error.message?.includes('invalid')) {
+ logger.info('Setting invalid OTP state - caught error')
+ message = 'Invalid verification code. Please check and try again.'
+ } else if (error.message?.includes('attempts')) {
+ message = 'Too many failed attempts. Please request a new code.'
+ }
+
+ // Set both state variables to ensure the error shows
+ setIsInvalidOtp(true)
+ setErrorMessage(message)
+ logger.info('Error state after caught error:', { isInvalidOtp: true, errorMessage: message })
+
+ // Clear the OTP input on error
+ setOtp('')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ function resendCode() {
+ if (!email || !hasResendKey) return
+
+ setIsLoading(true)
+ setErrorMessage('')
+
+ client.emailOtp
+ .sendVerificationOtp({
+ email,
+ type: 'email-verification',
+ })
+ .then(() => {})
+ .catch(() => {
+ setErrorMessage('Failed to resend verification code. Please try again later.')
+ })
+ .finally(() => {
+ setIsLoading(false)
+ })
+ }
+
+ function handleOtpChange(value: string) {
+ // Only clear error when user is actively typing a new code
+ if (value.length === 6) {
+ setIsInvalidOtp(false)
+ setErrorMessage('')
+ }
+ setOtp(value)
+ }
+
+ return {
+ otp,
+ email,
+ isLoading,
+ isVerified,
+ isInvalidOtp,
+ errorMessage,
+ isOtpComplete,
+ hasResendKey,
+ isProduction,
+ verifyCode,
+ resendCode,
+ handleOtpChange
+ }
+}
\ No newline at end of file
diff --git a/sim/app/(auth)/verify/verify-content.tsx b/sim/app/(auth)/verify/verify-content.tsx
new file mode 100644
index 000000000..ea0175e25
--- /dev/null
+++ b/sim/app/(auth)/verify/verify-content.tsx
@@ -0,0 +1,142 @@
+'use client'
+
+import { Suspense } from 'react'
+import { Button } from '@/components/ui/button'
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from '@/components/ui/card'
+import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/ui/input-otp'
+import { cn } from '@/lib/utils'
+import { useVerification } from './use-verification'
+
+interface VerifyContentProps {
+ hasResendKey: boolean
+ baseUrl: string
+ isProduction: boolean
+}
+
+function VerificationForm({ hasResendKey, isProduction }: { hasResendKey: boolean, isProduction: boolean }) {
+ const {
+ otp,
+ email,
+ isLoading,
+ isVerified,
+ isInvalidOtp,
+ errorMessage,
+ isOtpComplete,
+ verifyCode,
+ resendCode,
+ handleOtpChange
+ } = useVerification({ hasResendKey, isProduction })
+
+ return (
+ <>
+
+ {isVerified ? 'Email Verified!' : 'Verify your email'}
+
+ {isVerified ? (
+ 'Your email has been verified. Redirecting to dashboard...'
+ ) : hasResendKey ? (
+ A verification code has been sent to {email || 'your email'}
+ ) : !isProduction ? (
+
+
Development mode: No Resend API key configured
+
+ Check your console logs for the verification code
+
+
+ ) : (
+ Error: Invalid API key configuration
+ )}
+
+
+
+ {/* Add debug output for error state */}
+
+ Debug - isInvalidOtp: {String(isInvalidOtp)}, errorMessage: {errorMessage || 'none'}
+
+
+ {!isVerified && (
+
+
+ Enter the 6-digit code to verify your account.
+ {hasResendKey ? " If you don't see it in your email, check your spam folder." : ''}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Error message - moved above the button for better visibility */}
+ {errorMessage && (
+
+ )}
+
+
+
+ )}
+
+ {!isVerified && hasResendKey && (
+
+
+ Didn't receive a code?{' '}
+
+
+
+ )}
+ >
+ )
+}
+
+// Fallback component while the verification form is loading
+function VerificationFormFallback() {
+ return (
+
+ Loading verification...
+
+ Please wait while we load your verification details...
+
+
+ )
+}
+
+export function VerifyContent({ hasResendKey, baseUrl, isProduction }: VerifyContentProps) {
+ return (
+
+ }>
+
+
+
+ )
+}
diff --git a/sim/app/api/help/route.ts b/sim/app/api/help/route.ts
index ad0ede01b..b6b9f3b35 100644
--- a/sim/app/api/help/route.ts
+++ b/sim/app/api/help/route.ts
@@ -3,7 +3,7 @@ import { Resend } from 'resend'
import { z } from 'zod'
import { createLogger } from '@/lib/logs/console-logger'
-const resend = new Resend(process.env.RESEND_API_KEY)
+const resend = process.env.RESEND_API_KEY ? new Resend(process.env.RESEND_API_KEY) : null
const logger = createLogger('HelpAPI')
// Define schema for validation
@@ -18,6 +18,18 @@ export async function POST(req: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
try {
+ // Check if Resend API key is configured
+ if (!resend) {
+ logger.error(`[${requestId}] RESEND_API_KEY not configured`)
+ return NextResponse.json(
+ {
+ error:
+ 'Email service not configured. Please set RESEND_API_KEY in environment variables.',
+ },
+ { status: 500 }
+ )
+ }
+
// Handle multipart form data
const formData = await req.formData()
@@ -132,6 +144,15 @@ The Sim Studio Team
{ status: 200 }
)
} catch (error) {
+ // Check if error is related to missing API key
+ if (error instanceof Error && error.message.includes('API key')) {
+ logger.error(`[${requestId}] API key configuration error`, error)
+ return NextResponse.json(
+ { error: 'Email service configuration error. Please check your RESEND_API_KEY.' },
+ { status: 500 }
+ )
+ }
+
logger.error(`[${requestId}] Error processing help request`, error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
diff --git a/sim/app/lib/auth.ts b/sim/app/lib/auth.ts
index c1a8ed595..f2f4bf02b 100644
--- a/sim/app/lib/auth.ts
+++ b/sim/app/lib/auth.ts
@@ -12,17 +12,22 @@ const logger = createLogger('Auth')
// If there is no resend key, it might be a local dev environment
// In that case, we don't want to send emails and just log them
-const resend =
- process.env.RESEND_API_KEY && process.env.RESEND_API_KEY.trim() !== ''
- ? new Resend(process.env.RESEND_API_KEY)
- : {
- emails: {
- send: async (...args: any[]) => {
- logger.info('Email would have been sent in production. Details:', args)
- return { id: 'local-dev-mode' }
- },
+
+const validResendAPIKEY =
+ process.env.RESEND_API_KEY &&
+ process.env.RESEND_API_KEY.trim() !== '' &&
+ process.env.RESEND_API_KEY !== 'placeholder'
+
+const resend = validResendAPIKEY
+ ? new Resend(process.env.RESEND_API_KEY)
+ : {
+ emails: {
+ send: async (...args: any[]) => {
+ logger.info('Email would have been sent in production. Details:', args)
+ return { id: 'local-dev-mode' }
},
- }
+ },
+ }
export const auth = betterAuth({
database: drizzleAdapter(db, {
@@ -95,7 +100,7 @@ export const auth = betterAuth({
}
// In development with no RESEND_API_KEY, log verification code
- if (!process.env.RESEND_API_KEY || process.env.RESEND_API_KEY.trim() === '') {
+ if (!validResendAPIKEY) {
logger.info('🔑 VERIFICATION CODE FOR LOGIN/SIGNUP', {
email: data.email,
otp: data.otp,
diff --git a/sim/package-lock.json b/sim/package-lock.json
index 018e3eb09..88539dcf3 100644
--- a/sim/package-lock.json
+++ b/sim/package-lock.json
@@ -5759,6 +5759,8 @@
},
"node_modules/dotenv": {
"version": "16.4.7",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
+ "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {