mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 12:31:12 -05:00
Refactor: Secure email confirmation flow
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -21,6 +21,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [profile, setProfile] = useState<Profile | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [pendingEmail, setPendingEmail] = useState<string | null>(null);
|
||||
const [previousEmail, setPreviousEmail] = useState<string | null>(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
|
||||
|
||||
Reference in New Issue
Block a user