import { useState, useEffect } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { useAuth } from '@/hooks/useAuth'; import { Header } from '@/components/layout/Header'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Separator } from '@/components/ui/separator'; import { Zap, Mail, Lock, User, AlertCircle, Eye, EyeOff, Shield } from 'lucide-react'; import { supabase } from '@/integrations/supabase/client'; import { useToast } from '@/hooks/use-toast'; import { getErrorMessage } from '@/lib/errorHandler'; import { TurnstileCaptcha } from '@/components/auth/TurnstileCaptcha'; import { notificationService } from '@/lib/notificationService'; import { StorageWarning } from '@/components/auth/StorageWarning'; import { MFAChallenge } from '@/components/auth/MFAChallenge'; import { verifyMfaUpgrade } from '@/lib/authService'; import { setAuthMethod } from '@/lib/sessionFlags'; import { useDocumentTitle } from '@/hooks/useDocumentTitle'; export default function Auth() { useDocumentTitle('Sign In'); const [searchParams] = useSearchParams(); const navigate = useNavigate(); const { toast } = useToast(); const [loading, setLoading] = useState(false); const [magicLinkLoading, setMagicLinkLoading] = useState(false); const [showPassword, setShowPassword] = useState(false); const [captchaToken, setCaptchaToken] = useState(null); const [captchaKey, setCaptchaKey] = useState(0); const [signInCaptchaToken, setSignInCaptchaToken] = useState(null); const [signInCaptchaKey, setSignInCaptchaKey] = useState(0); const [mfaFactorId, setMfaFactorId] = useState(null); const [mfaPendingEmail, setMfaPendingEmail] = useState(null); const emailParam = searchParams.get('email'); const messageParam = searchParams.get('message'); const showPasswordSetupMessage = messageParam === 'complete-password-setup'; const [formData, setFormData] = useState({ email: emailParam || '', password: '', confirmPassword: '', username: '', displayName: '' }); const defaultTab = searchParams.get('tab') || 'signin'; const { user } = useAuth(); // Pre-fill email from query param useEffect(() => { if (emailParam) { setFormData(prev => ({ ...prev, email: emailParam })); } }, [emailParam]); // Auto-redirect when user is authenticated useEffect(() => { if (user) { const redirectTo = searchParams.get('redirect') || '/'; navigate(redirectTo); } }, [user, navigate, searchParams]); const handleInputChange = (e: React.ChangeEvent) => { setFormData(prev => ({ ...prev, [e.target.name]: e.target.value })); }; const handleSignIn = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); // Validate CAPTCHA if (!signInCaptchaToken) { toast({ variant: "destructive", title: "CAPTCHA required", description: "Please complete the CAPTCHA verification." }); setLoading(false); return; } // Consume token immediately to prevent reuse const tokenToUse = signInCaptchaToken; setSignInCaptchaToken(null); try { const { data, error } = await supabase.auth.signInWithPassword({ email: formData.email, password: formData.password, options: { captchaToken: tokenToUse } }); if (error) throw error; // CRITICAL: Check ban status immediately after successful authentication const { data: profile } = await supabase .from('profiles') .select('banned, ban_reason') .eq('user_id', data.user.id) .single(); if (profile?.banned) { // Sign out immediately await supabase.auth.signOut(); const reason = profile.ban_reason ? `Reason: ${profile.ban_reason}` : 'Contact support for assistance.'; toast({ variant: "destructive", title: "Account Suspended", description: `Your account has been suspended. ${reason}`, duration: 10000 }); setLoading(false); return; // Stop authentication flow } // Check if MFA is required (user exists but no session) if (data.user && !data.session) { const totpFactor = data.user.factors?.find(f => f.factor_type === 'totp' && f.status === 'verified'); if (totpFactor) { setMfaFactorId(totpFactor.id); setLoading(false); return; } } // Track auth method for audit logging setAuthMethod('password'); // Check if MFA step-up is required const { handlePostAuthFlow } = await import('@/lib/authService'); const postAuthResult = await handlePostAuthFlow(data.session, 'password'); if (postAuthResult.success && postAuthResult.data.shouldRedirect) { // MFA IS REQUIRED - we must show the challenge or sign out const { data: factors } = await supabase.auth.mfa.listFactors(); const totpFactor = factors?.totp?.find(f => f.status === 'verified'); if (totpFactor) { // DESTROY the AAL1 session - user should NOT be logged in before MFA console.log('[Auth] MFA required - destroying AAL1 session and storing credentials'); await supabase.auth.signOut(); // Store credentials in memory for re-authentication after TOTP sessionStorage.setItem('mfa_pending_email', formData.email); sessionStorage.setItem('mfa_pending_password', formData.password); sessionStorage.setItem('mfa_factor_id', totpFactor.id); setMfaPendingEmail(formData.email); setMfaFactorId(totpFactor.id); setLoading(false); return; // User has NO session - MFA modal will show } else { // MFA is required but no factor found - FORCE SIGN OUT for security console.error('[Auth] SECURITY: MFA required but no verified factor found'); await supabase.auth.signOut(); toast({ variant: "destructive", title: "Authentication Error", description: "Multi-factor authentication is required but not properly configured. Please contact support." }); setLoading(false); return; } } // ONLY show success toast if MFA was NOT required if (postAuthResult.success && !postAuthResult.data.shouldRedirect) { // Verify session was stored setTimeout(async () => { const { data: { session } } = await supabase.auth.getSession(); if (!session) { toast({ variant: "destructive", title: "Session Error", description: "Login succeeded but session was not stored. Please check your browser settings and enable cookies/storage." }); } else { toast({ title: "Welcome back!", description: "You've been signed in successfully." }); } }, 500); } } catch (error) { // Reset CAPTCHA widget to force fresh token generation setSignInCaptchaKey(prev => prev + 1); console.error('[Auth] Sign in error:', error); // Enhanced error messages const errorMsg = getErrorMessage(error); let errorMessage = errorMsg; if (errorMsg.includes('Invalid login credentials')) { errorMessage = 'Invalid email or password. Please try again.'; } else if (errorMsg.includes('Email not confirmed')) { errorMessage = 'Please confirm your email address before signing in.'; } else if (error.message.includes('Too many requests')) { errorMessage = 'Too many login attempts. Please wait a few minutes and try again.'; } toast({ variant: "destructive", title: "Sign in failed", description: errorMessage }); } finally { setLoading(false); } }; const handleMfaSuccess = async () => { console.log('[Auth] MFA verification succeeded'); // Retrieve stored credentials const email = sessionStorage.getItem('mfa_pending_email'); const password = sessionStorage.getItem('mfa_pending_password'); if (!email || !password) { console.error('[Auth] Missing stored credentials for re-authentication'); toast({ title: "Authentication error", description: "Please sign in again.", variant: "destructive", }); setMfaFactorId(null); setMfaPendingEmail(null); return; } // Clear stored credentials sessionStorage.removeItem('mfa_pending_email'); sessionStorage.removeItem('mfa_pending_password'); sessionStorage.removeItem('mfa_factor_id'); // Re-authenticate with stored credentials - this should create AAL2 session console.log('[Auth] Re-authenticating with verified credentials'); const { error: reAuthError } = await supabase.auth.signInWithPassword({ email, password, }); if (reAuthError) { console.error('[Auth] Re-authentication failed:', reAuthError); toast({ title: "Authentication error", description: "Please sign in again.", variant: "destructive", }); setMfaFactorId(null); setMfaPendingEmail(null); return; } setMfaFactorId(null); setMfaPendingEmail(null); toast({ title: "Authentication complete", description: "You've been signed in successfully.", }); setTimeout(() => { navigate('/'); }, 500); }; const handleMfaCancel = async () => { console.log('[Auth] User cancelled MFA verification'); // Clear stored credentials sessionStorage.removeItem('mfa_pending_email'); sessionStorage.removeItem('mfa_pending_password'); sessionStorage.removeItem('mfa_factor_id'); setMfaFactorId(null); setMfaPendingEmail(null); setSignInCaptchaKey(prev => prev + 1); toast({ title: "Authentication cancelled", description: "Please sign in again when you're ready to complete two-factor authentication.", }); }; const handleSignUp = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); // Validate passwords match if (formData.password !== formData.confirmPassword) { toast({ variant: "destructive", title: "Passwords don't match", description: "Please make sure your passwords match." }); setLoading(false); return; } // Validate password length if (formData.password.length < 6) { toast({ variant: "destructive", title: "Password too short", description: "Password must be at least 6 characters long." }); setLoading(false); return; } // Validate CAPTCHA if (!captchaToken) { toast({ variant: "destructive", title: "CAPTCHA required", description: "Please complete the CAPTCHA verification." }); setLoading(false); return; } // Consume token immediately to prevent reuse const tokenToUse = captchaToken; setCaptchaToken(null); try { const { data, error } = await supabase.auth.signUp({ email: formData.email, password: formData.password, options: { emailRedirectTo: `${window.location.origin}/auth/callback`, captchaToken: tokenToUse, data: { username: formData.username, display_name: formData.displayName } } }); if (error) throw error; // Register user with Novu (non-blocking) if (data.user) { notificationService.createSubscriber({ subscriberId: data.user.id, email: formData.email, firstName: formData.username, // Send username as firstName to Novu data: { username: formData.username, } }).catch(err => { console.error('Failed to register Novu subscriber:', err); // Don't block signup if Novu registration fails }); } toast({ title: "Welcome to ThrillWiki!", description: "Please check your email to verify your account." }); } catch (error) { // Reset CAPTCHA widget to force fresh token generation setCaptchaKey(prev => prev + 1); toast({ variant: "destructive", title: "Sign up failed", description: getErrorMessage(error) }); } finally { setLoading(false); } }; const handleMagicLinkSignIn = async (email: string) => { if (!email) { toast({ variant: "destructive", title: "Email required", description: "Please enter your email address to receive a magic link." }); return; } setMagicLinkLoading(true); try { const { error } = await supabase.auth.signInWithOtp({ email, options: { emailRedirectTo: `${window.location.origin}/auth/callback` } }); if (error) throw error; toast({ title: "Magic link sent!", description: "Check your email for a sign-in link." }); } catch (error) { toast({ variant: "destructive", title: "Failed to send magic link", description: getErrorMessage(error) }); } finally { setMagicLinkLoading(false); } }; const handleSocialSignIn = async (provider: 'google' | 'discord') => { try { const { error } = await supabase.auth.signInWithOAuth({ provider, options: { redirectTo: `${window.location.origin}/auth/callback`, // Request additional scopes for avatar access scopes: provider === 'google' ? 'email profile' : 'identify email' } }); if (error) throw error; } catch (error) { toast({ variant: "destructive", title: "Social sign in failed", description: getErrorMessage(error) }); } }; return

ThrillWiki

Join the ultimate theme park community

Sign In Sign Up Welcome back Sign in to your ThrillWiki account {showPasswordSetupMessage && ( Password setup in progress. Check your email for a confirmation link. After confirming your email, sign in below with your email and password. )} {mfaFactorId ? ( { if (!isOpen) { handleMfaCancel(); } }}> e.preventDefault()} onEscapeKeyDown={(e) => e.preventDefault()} >
Two-Factor Authentication Required
Your account security settings require MFA verification to continue.
) : ( <>
setSignInCaptchaToken(null)} onExpire={() => setSignInCaptchaToken(null)} siteKey={import.meta.env.VITE_TURNSTILE_SITE_KEY} theme="auto" />

Enter your email above and click to receive a sign-in link

Or continue with
)}
Create account Join the ThrillWiki community today
setCaptchaToken(null)} onExpire={() => setCaptchaToken(null)} siteKey={import.meta.env.VITE_TURNSTILE_SITE_KEY} theme="auto" className="flex justify-center" />

Skip the password - just enter your email above

Or continue with
By signing up, you agree to our Terms of Service and Privacy Policy.
; }