From 70f94b8a3057c24889c7b50dd73fc75501ed10a7 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 17:14:34 +0000 Subject: [PATCH] feat: Implement password reset flow --- src/components/settings/SecurityTab.tsx | 36 ++++---- src/hooks/useAuth.tsx | 6 +- src/lib/identityService.ts | 105 ++++++++++++------------ src/pages/AuthCallback.tsx | 92 +++++++++++++++++++++ 4 files changed, 166 insertions(+), 73 deletions(-) diff --git a/src/components/settings/SecurityTab.tsx b/src/components/settings/SecurityTab.tsx index 122a28c0..df731081 100644 --- a/src/components/settings/SecurityTab.tsx +++ b/src/components/settings/SecurityTab.tsx @@ -140,24 +140,22 @@ export function SecurityTab() { }; const handlePasswordSetupSuccess = (email?: string, needsConfirmation?: boolean) => { - if (email) { - // Password was set, user was logged out, needs to confirm email - if (needsConfirmation) { - toast({ - title: 'Check Your Email', - description: 'A confirmation link has been sent to your email. Click it to activate password authentication, then sign in.', - duration: 10000, - }); - } else { - toast({ - title: 'Password Set Successfully', - description: 'Please sign in with your email and password to complete setup.', - duration: 6000, - }); - } + if (email && needsConfirmation) { + // Password setup initiated via reset flow + toast({ + 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.", + duration: 15000, + }); - // Redirect to auth page with email pre-filled - navigate(`/auth?email=${encodeURIComponent(email)}&message=complete-password-setup`); + // Stay on settings page - user will complete setup via email link + } 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 { // Normal password change flow (user already had email identity) setAddingPassword(true); @@ -185,8 +183,8 @@ export function SecurityTab() { const result = await triggerOrphanedPasswordConfirmation('security_settings'); if (result.success) { - sonnerToast.success("Confirmation Email Sent!", { - description: "Check your email for a confirmation link to activate your password authentication.", + sonnerToast.success("Reset Email Sent!", { + 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, }); } else { diff --git a/src/hooks/useAuth.tsx b/src/hooks/useAuth.tsx index 64981871..01020e8b 100644 --- a/src/hooks/useAuth.tsx +++ b/src/hooks/useAuth.tsx @@ -147,7 +147,7 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) { const { toast: sonnerToast } = await import('sonner'); 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, action: { label: "Resend Email", @@ -155,8 +155,8 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) { const result = await triggerOrphanedPasswordConfirmation('signin_toast'); if (result.success) { - sonnerToast.success("Confirmation Email Sent!", { - description: `Check ${result.email} for the confirmation link.`, + sonnerToast.success("Reset Email Sent!", { + description: `Check ${result.email} for the password reset link from Supabase.`, duration: 10000, }); } else { diff --git a/src/lib/identityService.ts b/src/lib/identityService.ts index f4cfb263..a11dddd2 100644 --- a/src/lib/identityService.ts +++ b/src/lib/identityService.ts @@ -213,35 +213,38 @@ export async function addPasswordToAccount( }; } - // Step 1: Set the password (does NOT create email identity yet) - console.log('[IdentityService] Setting password'); - const { error: updateError } = await supabase.auth.updateUser({ password }); - if (updateError) throw updateError; - - // Step 2: Trigger signup confirmation email (this creates the email identity) - console.log('[IdentityService] Sending signup confirmation email'); - const { error: resendError } = await supabase.auth.resend({ - type: 'signup', - email: userEmail, - options: { - emailRedirectTo: `${window.location.origin}/auth?confirmed=password-setup` + console.log('[IdentityService] Initiating password setup via reset flow'); + + // Step 1: Store the desired password temporarily in session storage + // This will be used after clicking the reset link + sessionStorage.setItem('pending_password_setup', password); + console.log('[IdentityService] Stored pending password in session storage'); + + // Step 2: Trigger Supabase password reset email + // This creates the email identity when user clicks link and sets password + const { error: resetError } = await supabase.auth.resetPasswordForEmail( + userEmail, + { + redirectTo: `${window.location.origin}/auth/callback?action=password-setup` } - }); - - if (resendError) { - console.error('[IdentityService] Failed to send confirmation email:', resendError); - throw resendError; + ); + + if (resetError) { + console.error('[IdentityService] Failed to send password reset email:', resetError); + 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 - console.log('[IdentityService] Fetching user profile'); const { data: profile } = await supabase .from('profiles') .select('display_name, username') .eq('user_id', user!.id) .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'); try { await supabase.functions.invoke('send-password-added-email', { @@ -251,33 +254,32 @@ export async function addPasswordToAccount( username: profile?.username, }, }); + console.log('[IdentityService] Custom notification email sent'); } catch (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 { success: true, - needsRelogin: true, needsEmailConfirmation: true, email: userEmail }; - } 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 { 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 - const { error: resendError } = await supabase.auth.resend({ - type: 'signup', - email: user.email, - options: { - emailRedirectTo: `${window.location.origin}/auth?confirmed=password-confirmation` + // Send Supabase password reset email + // The user's password is already set in auth.users, they just need to click + // the reset link to create the email identity + const { error: resetError } = await supabase.auth.resetPasswordForEmail( + user.email, + { + redirectTo: `${window.location.origin}/auth/callback?action=confirm-password` } - }); + ); - if (resendError) { - console.error('[IdentityService] Failed to resend confirmation:', resendError); - throw resendError; + if (resetError) { + console.error('[IdentityService] Failed to send password reset email:', resetError); + throw resetError; } - console.log('[IdentityService] Confirmation email resent successfully'); + console.log('[IdentityService] Password reset email sent successfully'); // Optional: Get profile for custom notification const { data: profile } = await supabase @@ -354,7 +357,7 @@ export async function triggerOrphanedPasswordConfirmation( await logIdentityChange(user.id, 'orphaned_password_confirmation_triggered', { method: source || 'manual_button_click', timestamp: new Date().toISOString(), - confirmation_email_sent: true + reset_email_sent: true }); return { @@ -363,10 +366,10 @@ export async function triggerOrphanedPasswordConfirmation( email: user.email }; } catch (error: any) { - console.error('[IdentityService] Failed to trigger confirmation:', error); + console.error('[IdentityService] Failed to trigger password reset:', error); return { success: false, - error: error.message || 'Failed to trigger email confirmation' + error: error.message || 'Failed to send password reset email' }; } } diff --git a/src/pages/AuthCallback.tsx b/src/pages/AuthCallback.tsx index c03e3aca..cfdd8686 100644 --- a/src/pages/AuthCallback.tsx +++ b/src/pages/AuthCallback.tsx @@ -32,6 +32,98 @@ export default function AuthCallback() { const user = session.user; 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) const createdAt = new Date(user.created_at); const now = new Date();