diff --git a/src/components/settings/EmailChangeDialog.tsx b/src/components/settings/EmailChangeDialog.tsx index 161c8ecf..88c4097d 100644 --- a/src/components/settings/EmailChangeDialog.tsx +++ b/src/components/settings/EmailChangeDialog.tsx @@ -117,15 +117,8 @@ export function EmailChangeDialog({ open, onOpenChange, currentEmail, userId }: if (updateError) throw updateError; - // Step 3: Update Novu subscriber (non-blocking) - if (notificationService.isEnabled()) { - notificationService.updateSubscriber({ - subscriberId: userId, - email: data.newEmail, - }).catch(error => { - console.error('Failed to update Novu subscriber:', error); - }); - } + // Step 3: 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 4: Log the email change attempt supabase.from('admin_audit_log').insert({ diff --git a/src/hooks/useAuth.tsx b/src/hooks/useAuth.tsx index 6279718c..a8638010 100644 --- a/src/hooks/useAuth.tsx +++ b/src/hooks/useAuth.tsx @@ -21,6 +21,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const [profile, setProfile] = useState(null); const [loading, setLoading] = useState(true); const [pendingEmail, setPendingEmail] = useState(null); + const [previousEmail, setPreviousEmail] = useState(null); const fetchProfile = async (userId: string) => { try { @@ -61,13 +62,72 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { // Listen for auth changes const { data: { subscription }, - } = supabase.auth.onAuthStateChange((event, session) => { + } = supabase.auth.onAuthStateChange(async (event, session) => { + const currentEmail = session?.user?.email; + const newEmailPending = session?.user?.new_email; + setSession(session); setUser(session?.user ?? null); // Track pending email changes - const newEmail = session?.user?.new_email; - setPendingEmail(newEmail ?? null); + setPendingEmail(newEmailPending ?? null); + + // Detect confirmed email change: email changed AND no longer pending + if ( + session?.user && + previousEmail && + currentEmail && + currentEmail !== previousEmail && + !newEmailPending + ) { + console.log('Email change confirmed:', { from: previousEmail, to: currentEmail }); + + // Defer Novu update and notifications to avoid blocking auth + setTimeout(async () => { + try { + // Update Novu subscriber with confirmed email + const { notificationService } = await import('@/lib/notificationService'); + if (notificationService.isEnabled()) { + await notificationService.updateSubscriber({ + subscriberId: session.user.id, + email: currentEmail, + }); + } + + // Log the confirmed email change + await supabase.from('admin_audit_log').insert({ + admin_user_id: session.user.id, + target_user_id: session.user.id, + action: 'email_change_completed', + details: { + old_email: previousEmail, + new_email: currentEmail, + timestamp: new Date().toISOString(), + }, + }); + + // Send final security notification + if (notificationService.isEnabled()) { + await notificationService.trigger({ + workflowId: 'email-changed', + subscriberId: session.user.id, + payload: { + oldEmail: previousEmail, + newEmail: currentEmail, + timestamp: new Date().toISOString(), + }, + }); + } + } catch (error) { + console.error('Error updating Novu after email confirmation:', error); + } + }, 0); + } + + // Update tracked email + if (currentEmail) { + setPreviousEmail(currentEmail); + } if (session?.user) { // Defer profile fetch to avoid deadlock