feat: Implement password reset flow

This commit is contained in:
gpt-engineer-app[bot]
2025-10-14 17:14:34 +00:00
parent e8a7c028e9
commit 70f94b8a30
4 changed files with 166 additions and 73 deletions

View File

@@ -140,24 +140,22 @@ export function SecurityTab() {
}; };
const handlePasswordSetupSuccess = (email?: string, needsConfirmation?: boolean) => { const handlePasswordSetupSuccess = (email?: string, needsConfirmation?: boolean) => {
if (email) { if (email && needsConfirmation) {
// Password was set, user was logged out, needs to confirm email // Password setup initiated via reset flow
if (needsConfirmation) { toast({
toast({ title: 'Check Your Email',
title: 'Check Your Email', description: "Click the password reset link from Supabase to complete setup. You'll receive two emails: one with the reset link, and one with instructions from ThrillWiki.",
description: 'A confirmation link has been sent to your email. Click it to activate password authentication, then sign in.', duration: 15000,
duration: 10000, });
});
} else {
toast({
title: 'Password Set Successfully',
description: 'Please sign in with your email and password to complete setup.',
duration: 6000,
});
}
// Redirect to auth page with email pre-filled // Stay on settings page - user will complete setup via email link
navigate(`/auth?email=${encodeURIComponent(email)}&message=complete-password-setup`); } else if (email) {
// Fallback: direct password set (shouldn't happen with new flow)
toast({
title: 'Password Set Successfully',
description: 'You can now sign in with your email and password.',
duration: 6000,
});
} else { } else {
// Normal password change flow (user already had email identity) // Normal password change flow (user already had email identity)
setAddingPassword(true); setAddingPassword(true);
@@ -185,8 +183,8 @@ export function SecurityTab() {
const result = await triggerOrphanedPasswordConfirmation('security_settings'); const result = await triggerOrphanedPasswordConfirmation('security_settings');
if (result.success) { if (result.success) {
sonnerToast.success("Confirmation Email Sent!", { sonnerToast.success("Reset Email Sent!", {
description: "Check your email for a confirmation link to activate your password authentication.", description: "Check your email for a password reset link from Supabase to activate your password authentication. You'll also receive a notification email from ThrillWiki.",
duration: 15000, duration: 15000,
}); });
} else { } else {

View File

@@ -147,7 +147,7 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
const { toast: sonnerToast } = await import('sonner'); const { toast: sonnerToast } = await import('sonner');
sonnerToast.warning("Password Activation Pending", { sonnerToast.warning("Password Activation Pending", {
description: "Your password needs email confirmation to be fully activated.", description: "Check your email for a password reset link to complete activation. You'll receive two emails: one from Supabase with the reset link, and one from ThrillWiki with instructions.",
duration: Infinity, duration: Infinity,
action: { action: {
label: "Resend Email", label: "Resend Email",
@@ -155,8 +155,8 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
const result = await triggerOrphanedPasswordConfirmation('signin_toast'); const result = await triggerOrphanedPasswordConfirmation('signin_toast');
if (result.success) { if (result.success) {
sonnerToast.success("Confirmation Email Sent!", { sonnerToast.success("Reset Email Sent!", {
description: `Check ${result.email} for the confirmation link.`, description: `Check ${result.email} for the password reset link from Supabase.`,
duration: 10000, duration: 10000,
}); });
} else { } else {

View File

@@ -213,35 +213,38 @@ export async function addPasswordToAccount(
}; };
} }
// Step 1: Set the password (does NOT create email identity yet) console.log('[IdentityService] Initiating password setup via reset flow');
console.log('[IdentityService] Setting password');
const { error: updateError } = await supabase.auth.updateUser({ password }); // Step 1: Store the desired password temporarily in session storage
if (updateError) throw updateError; // This will be used after clicking the reset link
sessionStorage.setItem('pending_password_setup', password);
// Step 2: Trigger signup confirmation email (this creates the email identity) console.log('[IdentityService] Stored pending password in session storage');
console.log('[IdentityService] Sending signup confirmation email');
const { error: resendError } = await supabase.auth.resend({ // Step 2: Trigger Supabase password reset email
type: 'signup', // This creates the email identity when user clicks link and sets password
email: userEmail, const { error: resetError } = await supabase.auth.resetPasswordForEmail(
options: { userEmail,
emailRedirectTo: `${window.location.origin}/auth?confirmed=password-setup` {
redirectTo: `${window.location.origin}/auth/callback?action=password-setup`
} }
}); );
if (resendError) { if (resetError) {
console.error('[IdentityService] Failed to send confirmation email:', resendError); console.error('[IdentityService] Failed to send password reset email:', resetError);
throw resendError; sessionStorage.removeItem('pending_password_setup'); // Cleanup on failure
throw resetError;
} }
console.log('[IdentityService] Password reset email sent successfully');
// Step 3: Get user profile for custom notification email // Step 3: Get user profile for custom notification email
console.log('[IdentityService] Fetching user profile');
const { data: profile } = await supabase const { data: profile } = await supabase
.from('profiles') .from('profiles')
.select('display_name, username') .select('display_name, username')
.eq('user_id', user!.id) .eq('user_id', user!.id)
.single(); .single();
// Step 4: Send our custom "Password Added" notification (informational) // Step 4: Send custom "Password Setup Instructions" email (informational)
console.log('[IdentityService] Sending custom notification email'); console.log('[IdentityService] Sending custom notification email');
try { try {
await supabase.functions.invoke('send-password-added-email', { await supabase.functions.invoke('send-password-added-email', {
@@ -251,33 +254,32 @@ export async function addPasswordToAccount(
username: profile?.username, username: profile?.username,
}, },
}); });
console.log('[IdentityService] Custom notification email sent');
} catch (emailError) { } catch (emailError) {
console.error('[IdentityService] Custom email failed (non-blocking):', emailError); console.error('[IdentityService] Custom email failed (non-blocking):', emailError);
// Don't fail the whole operation
} }
// Step 5: Log the action
await logIdentityChange(user!.id, 'password_added', {
method: 'oauth_password_addition_with_signup_confirmation',
confirmation_email_sent: true
});
// Step 6: Sign user out (they must confirm via email)
console.log('[IdentityService] Signing user out');
await supabase.auth.signOut();
// Return success with relogin and email confirmation flags // Step 5: Log the action
await logIdentityChange(user!.id, 'password_setup_initiated', {
method: 'reset_password_flow',
reset_email_sent: true,
timestamp: new Date().toISOString()
});
// Return success - user needs to check email and click reset link
return { return {
success: true, success: true,
needsRelogin: true,
needsEmailConfirmation: true, needsEmailConfirmation: true,
email: userEmail email: userEmail
}; };
} catch (error: any) { } catch (error: any) {
console.error('[IdentityService] Failed to add password:', error); console.error('[IdentityService] Failed to initiate password setup:', error);
// Cleanup on error
sessionStorage.removeItem('pending_password_setup');
return { return {
success: false, success: false,
error: error.message || 'Failed to set password' error: error.message || 'Failed to initiate password setup'
}; };
} }
} }
@@ -312,23 +314,24 @@ export async function triggerOrphanedPasswordConfirmation(
}; };
} }
console.log('[IdentityService] Resending signup confirmation email'); console.log('[IdentityService] Resending password reset email for orphaned password');
// Send Supabase signup confirmation email // Send Supabase password reset email
const { error: resendError } = await supabase.auth.resend({ // The user's password is already set in auth.users, they just need to click
type: 'signup', // the reset link to create the email identity
email: user.email, const { error: resetError } = await supabase.auth.resetPasswordForEmail(
options: { user.email,
emailRedirectTo: `${window.location.origin}/auth?confirmed=password-confirmation` {
redirectTo: `${window.location.origin}/auth/callback?action=confirm-password`
} }
}); );
if (resendError) { if (resetError) {
console.error('[IdentityService] Failed to resend confirmation:', resendError); console.error('[IdentityService] Failed to send password reset email:', resetError);
throw resendError; throw resetError;
} }
console.log('[IdentityService] Confirmation email resent successfully'); console.log('[IdentityService] Password reset email sent successfully');
// Optional: Get profile for custom notification // Optional: Get profile for custom notification
const { data: profile } = await supabase const { data: profile } = await supabase
@@ -354,7 +357,7 @@ export async function triggerOrphanedPasswordConfirmation(
await logIdentityChange(user.id, 'orphaned_password_confirmation_triggered', { await logIdentityChange(user.id, 'orphaned_password_confirmation_triggered', {
method: source || 'manual_button_click', method: source || 'manual_button_click',
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
confirmation_email_sent: true reset_email_sent: true
}); });
return { return {
@@ -363,10 +366,10 @@ export async function triggerOrphanedPasswordConfirmation(
email: user.email email: user.email
}; };
} catch (error: any) { } catch (error: any) {
console.error('[IdentityService] Failed to trigger confirmation:', error); console.error('[IdentityService] Failed to trigger password reset:', error);
return { return {
success: false, success: false,
error: error.message || 'Failed to trigger email confirmation' error: error.message || 'Failed to send password reset email'
}; };
} }
} }

View File

@@ -32,6 +32,98 @@ export default function AuthCallback() {
const user = session.user; const user = session.user;
console.log('[AuthCallback] User authenticated:', user.id); console.log('[AuthCallback] User authenticated:', user.id);
// Check for password setup actions from reset flow
const urlParams = new URLSearchParams(window.location.search);
const action = urlParams.get('action');
if (action === 'password-setup') {
console.log('[AuthCallback] Processing password-setup action');
// Retrieve the password from session storage
const pendingPassword = sessionStorage.getItem('pending_password_setup');
if (pendingPassword) {
try {
console.log('[AuthCallback] Setting password from pending setup');
// Set the password - this creates the email identity
const { error: passwordError } = await supabase.auth.updateUser({
password: pendingPassword
});
if (passwordError) {
console.error('[AuthCallback] Failed to set password:', passwordError);
throw passwordError;
}
// Clear session storage
sessionStorage.removeItem('pending_password_setup');
console.log('[AuthCallback] Password set successfully, email identity created');
// Show success message
toast({
title: "Password Set Successfully!",
description: "You can now sign in with your email and password.",
});
// Redirect to auth page for sign-in
setTimeout(() => {
navigate('/auth');
}, 1500);
return;
} catch (error: any) {
console.error('[AuthCallback] Password setup error:', error);
sessionStorage.removeItem('pending_password_setup'); // Cleanup
toast({
variant: 'destructive',
title: 'Password Setup Failed',
description: error.message || 'Failed to set password. Please try again.',
});
setTimeout(() => {
navigate('/settings?tab=security');
}, 2000);
return;
}
} else {
console.warn('[AuthCallback] No pending password found in session storage');
toast({
variant: 'destructive',
title: 'Password Setup Incomplete',
description: 'Please try setting your password again from Security Settings.',
});
setTimeout(() => {
navigate('/settings?tab=security');
}, 2000);
return;
}
}
if (action === 'confirm-password') {
console.log('[AuthCallback] Processing confirm-password action (orphaned password)');
// For orphaned password, the password is already set in auth.users
// The reset link just needed to be clicked to create the email identity
// Supabase handles this automatically when the reset link is clicked
toast({
title: "Password Activated!",
description: "Your password authentication is now fully active. You can sign in with email and password.",
});
// Redirect to auth page for sign-in
setTimeout(() => {
navigate('/auth');
}, 1500);
return;
}
// Check if this is a new OAuth user (created within last minute) // Check if this is a new OAuth user (created within last minute)
const createdAt = new Date(user.created_at); const createdAt = new Date(user.created_at);
const now = new Date(); const now = new Date();