mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 22:11:11 -05:00
feat: Implement password reset flow
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
console.log('[IdentityService] Initiating password setup via reset flow');
|
||||
|
||||
// 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`
|
||||
// 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
|
||||
await logIdentityChange(user!.id, 'password_setup_initiated', {
|
||||
method: 'reset_password_flow',
|
||||
reset_email_sent: true,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// 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
|
||||
// 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'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user