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 && ( +
+

{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": {