import { useState } from 'react'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Separator } from '@/components/ui/separator'; import { Zap, Mail, Lock, User, Eye, EyeOff } from 'lucide-react'; import { supabase } from '@/lib/supabaseClient'; import { useToast } from '@/hooks/use-toast'; import { handleError, handleNonCriticalError } from '@/lib/errorHandler'; import { TurnstileCaptcha } from './TurnstileCaptcha'; import { notificationService } from '@/lib/notificationService'; import { useCaptchaBypass } from '@/hooks/useCaptchaBypass'; import { MFAChallenge } from './MFAChallenge'; import { verifyMfaUpgrade } from '@/lib/authService'; import { setAuthMethod } from '@/lib/sessionFlags'; import { validateEmailNotDisposable } from '@/lib/emailValidation'; import type { SignInOptions } from '@/types/supabase-auth'; interface AuthModalProps { open: boolean; onOpenChange: (open: boolean) => void; defaultTab?: 'signin' | 'signup'; } export function AuthModal({ open, onOpenChange, defaultTab = 'signin' }: AuthModalProps) { 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 [formData, setFormData] = useState({ email: '', password: '', confirmPassword: '', username: '', displayName: '' }); const { requireCaptcha } = useCaptchaBypass(); const handleInputChange = (e: React.ChangeEvent) => { setFormData(prev => ({ ...prev, [e.target.name]: e.target.value })); }; const handleSignIn = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); if (requireCaptcha && !signInCaptchaToken) { toast({ variant: "destructive", title: "CAPTCHA required", description: "Please complete the CAPTCHA verification." }); setLoading(false); return; } const tokenToUse = signInCaptchaToken; setSignInCaptchaToken(null); try { const signInOptions: SignInOptions = { email: formData.email, password: formData.password, }; if (tokenToUse) { signInOptions.options = { captchaToken: tokenToUse }; } const { data, error } = await supabase.auth.signInWithPassword(signInOptions); 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) { // Get the TOTP factor ID const { data: factors } = await supabase.auth.mfa.listFactors(); const totpFactor = factors?.totp?.find(f => f.status === 'verified'); if (totpFactor) { setMfaFactorId(totpFactor.id); setLoading(false); return; // Stay in modal, show MFA challenge } } toast({ title: "Welcome back!", description: "You've been signed in successfully." }); // Wait for auth state to propagate before closing await new Promise(resolve => setTimeout(resolve, 100)); onOpenChange(false); } catch (error: unknown) { setSignInCaptchaKey(prev => prev + 1); handleError(error, { action: 'Sign In', metadata: { method: 'password', hasCaptcha: !!tokenToUse // ⚠️ NEVER log: email, password, tokens } }); } finally { setLoading(false); } }; const handleMfaSuccess = async () => { // Verify AAL upgrade was successful const { data: { session } } = await supabase.auth.getSession(); const verification = await verifyMfaUpgrade(session); if (!verification.success) { toast({ variant: "destructive", title: "MFA Verification Failed", description: verification.error || "Failed to upgrade session. Please try again." }); // Force sign out on verification failure await supabase.auth.signOut(); setMfaFactorId(null); return; } setMfaFactorId(null); onOpenChange(false); }; const handleMfaCancel = () => { setMfaFactorId(null); setSignInCaptchaKey(prev => prev + 1); }; const handleSignUp = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); if (formData.password !== formData.confirmPassword) { toast({ variant: "destructive", title: "Passwords don't match", description: "Please make sure your passwords match." }); setLoading(false); return; } if (formData.password.length < 6) { toast({ variant: "destructive", title: "Password too short", description: "Password must be at least 6 characters long." }); setLoading(false); return; } if (requireCaptcha && !captchaToken) { toast({ variant: "destructive", title: "CAPTCHA required", description: "Please complete the CAPTCHA verification." }); setLoading(false); return; } const tokenToUse = captchaToken; setCaptchaToken(null); try { // Validate email is not disposable const emailValidation = await validateEmailNotDisposable(formData.email); if (!emailValidation.valid) { toast({ variant: "destructive", title: "Invalid Email", description: emailValidation.reason || "Please use a permanent email address" }); setCaptchaKey(prev => prev + 1); setLoading(false); return; } interface SignUpOptions { email: string; password: string; options?: { captchaToken?: string; data?: { username: string; display_name: string; }; }; } const signUpOptions: SignUpOptions = { email: formData.email, password: formData.password, options: { data: { username: formData.username, display_name: formData.displayName } } }; if (tokenToUse) { signUpOptions.options = { ...signUpOptions.options, captchaToken: tokenToUse }; } const { data, error } = await supabase.auth.signUp(signUpOptions); if (error) throw error; if (data.user) { const userId = data.user.id; notificationService.createSubscriber({ subscriberId: userId, email: formData.email, firstName: formData.username, data: { username: formData.username, } }).catch(err => { handleNonCriticalError(err, { action: 'Register Novu subscriber', userId, metadata: { email: formData.email, context: 'post_signup' } }); }); } toast({ title: "Welcome to ThrillWiki!", description: "Please check your email to verify your account." }); onOpenChange(false); } catch (error: unknown) { setCaptchaKey(prev => prev + 1); handleError(error, { action: 'Sign Up', metadata: { hasCaptcha: !!tokenToUse, hasUsername: !!formData.username // ⚠️ NEVER log: email, password, username } }); } 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." }); onOpenChange(false); } catch (error: unknown) { handleError(error, { action: 'Send Magic Link', metadata: { method: 'magic_link' // ⚠️ NEVER log: email, link } }); } 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: unknown) { handleError(error, { action: 'Social Sign In', metadata: { provider, method: 'oauth' } }); } }; return ( ThrillWiki Join the ultimate theme park community Sign In Sign Up {mfaFactorId ? ( ) : ( <>
{requireCaptcha && (
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
)}
{requireCaptcha && (
setCaptchaToken(null)} onExpire={() => setCaptchaToken(null)} siteKey={import.meta.env.VITE_TURNSTILE_SITE_KEY} theme="auto" />
)}
Or continue with

By signing up, you agree to our{' '} Terms {' '}and{' '} Privacy Policy

); }