import { useState } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { toast } from 'sonner'; import { supabase } from '@/integrations/supabase/client'; import { Loader2, Mail, CheckCircle2, AlertCircle } from 'lucide-react'; import { TurnstileCaptcha } from '@/components/auth/TurnstileCaptcha'; import { useTheme } from '@/components/theme/ThemeProvider'; import { notificationService } from '@/lib/notificationService'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { validateEmailNotDisposable } from '@/lib/emailValidation'; const emailSchema = z.object({ currentPassword: z.string().min(1, 'Current password is required'), newEmail: z.string().email('Please enter a valid email address'), confirmEmail: z.string().email('Please enter a valid email address'), }).refine((data) => data.newEmail === data.confirmEmail, { message: "Email addresses don't match", path: ["confirmEmail"], }); type EmailFormData = z.infer; type Step = 'verification' | 'success'; interface EmailChangeDialogProps { open: boolean; onOpenChange: (open: boolean) => void; currentEmail: string; userId: string; } export function EmailChangeDialog({ open, onOpenChange, currentEmail, userId }: EmailChangeDialogProps) { const { theme } = useTheme(); const [step, setStep] = useState('verification'); const [loading, setLoading] = useState(false); const [captchaToken, setCaptchaToken] = useState(''); const [captchaKey, setCaptchaKey] = useState(0); const form = useForm({ resolver: zodResolver(emailSchema), defaultValues: { currentPassword: '', newEmail: '', confirmEmail: '', }, }); const handleClose = () => { if (loading) return; onOpenChange(false); setTimeout(() => { setStep('verification'); form.reset(); setCaptchaToken(''); setCaptchaKey(prev => prev + 1); }, 300); }; const onSubmit = async (data: EmailFormData) => { if (!captchaToken) { toast.error('CAPTCHA Required', { description: 'Please complete the CAPTCHA verification.', }); return; } if (data.newEmail.toLowerCase() === currentEmail.toLowerCase()) { toast.error('Same Email', { description: 'The new email is the same as your current email.', }); return; } setLoading(true); try { // Step 1: Validate email is not disposable const emailValidation = await validateEmailNotDisposable(data.newEmail); if (!emailValidation.valid) { toast.error("Invalid Email", { description: emailValidation.reason || "Please use a permanent email address" }); setLoading(false); return; } // Step 2: Reauthenticate with current password const { error: signInError } = await supabase.auth.signInWithPassword({ email: currentEmail, password: data.currentPassword, options: { captchaToken } }); if (signInError) { // Reset CAPTCHA on authentication failure setCaptchaToken(''); setCaptchaKey(prev => prev + 1); throw signInError; } // Step 3: Update email address // Supabase will send verification emails to both old and new addresses const { error: updateError } = await supabase.auth.updateUser({ email: data.newEmail }); if (updateError) throw updateError; // Step 4: Novu subscriber will be updated automatically after both emails are confirmed // This happens in the useAuth hook when the email change is fully verified // Step 5: Log the email change attempt supabase.from('admin_audit_log').insert({ admin_user_id: userId, target_user_id: userId, action: 'email_change_initiated', details: { old_email: currentEmail, new_email: data.newEmail, timestamp: new Date().toISOString(), } }).then(({ error }) => { if (error) console.error('Failed to log email change:', error); }); // Step 6: Send security notifications (non-blocking) if (notificationService.isEnabled()) { notificationService.trigger({ workflowId: 'security-alert', subscriberId: userId, payload: { alert_type: 'email_change_initiated', old_email: currentEmail, new_email: data.newEmail, timestamp: new Date().toISOString(), } }).catch(error => { console.error('Failed to send security notification:', error); }); } toast.success('Email change initiated', { description: 'Check both email addresses for confirmation links.', }); setStep('success'); } catch (error: any) { console.error('Email change error:', error); // Handle rate limiting specifically if (error.message?.includes('rate limit') || error.status === 429) { toast.error('Too Many Attempts', { description: 'Please wait a few minutes before trying again.', }); } else { toast.error('Failed to change email', { description: error.message || 'Please try again.', }); } } finally { setLoading(false); } }; return ( Change Email Address {step === 'verification' ? ( 'Enter your current password and new email address to proceed.' ) : ( 'Verification emails have been sent.' )} {step === 'verification' ? (
Current email: {currentEmail} ( Current Password * )} /> ( New Email Address * )} /> ( Confirm New Email * )} />
setCaptchaToken('')} onExpire={() => setCaptchaToken('')} siteKey={import.meta.env.VITE_TURNSTILE_SITE_KEY} theme={theme === 'dark' ? 'dark' : 'light'} size="normal" />
) : (

Verification Required

We've sent verification emails to both your current email address ({currentEmail}) and your new email address ({form.getValues('newEmail')}).

Important: You must click the confirmation link in BOTH emails to complete the email change. Your email address will not change until both verifications are confirmed.
)}
); }