Refactor: Secure email confirmation flow

This commit is contained in:
gpt-engineer-app[bot]
2025-10-01 15:50:27 +00:00
parent ba04dbc966
commit 5634dc4920
2 changed files with 65 additions and 12 deletions

View File

@@ -117,15 +117,8 @@ export function EmailChangeDialog({ open, onOpenChange, currentEmail, userId }:
if (updateError) throw updateError; if (updateError) throw updateError;
// Step 3: Update Novu subscriber (non-blocking) // Step 3: Novu subscriber will be updated automatically after both emails are confirmed
if (notificationService.isEnabled()) { // This happens in the useAuth hook when the email change is fully verified
notificationService.updateSubscriber({
subscriberId: userId,
email: data.newEmail,
}).catch(error => {
console.error('Failed to update Novu subscriber:', error);
});
}
// Step 4: Log the email change attempt // Step 4: Log the email change attempt
supabase.from('admin_audit_log').insert({ supabase.from('admin_audit_log').insert({

View File

@@ -21,6 +21,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const [profile, setProfile] = useState<Profile | null>(null); const [profile, setProfile] = useState<Profile | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [pendingEmail, setPendingEmail] = useState<string | null>(null); const [pendingEmail, setPendingEmail] = useState<string | null>(null);
const [previousEmail, setPreviousEmail] = useState<string | null>(null);
const fetchProfile = async (userId: string) => { const fetchProfile = async (userId: string) => {
try { try {
@@ -61,13 +62,72 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
// Listen for auth changes // Listen for auth changes
const { const {
data: { subscription }, 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); setSession(session);
setUser(session?.user ?? null); setUser(session?.user ?? null);
// Track pending email changes // Track pending email changes
const newEmail = session?.user?.new_email; setPendingEmail(newEmailPending ?? null);
setPendingEmail(newEmail ?? 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) { if (session?.user) {
// Defer profile fetch to avoid deadlock // Defer profile fetch to avoid deadlock