mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 18:31:12 -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) => {
|
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: 'A confirmation link has been sent to your email. Click it to activate password authentication, then sign in.',
|
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: 10000,
|
duration: 15000,
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
|
// 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({
|
toast({
|
||||||
title: 'Password Set Successfully',
|
title: 'Password Set Successfully',
|
||||||
description: 'Please sign in with your email and password to complete setup.',
|
description: 'You can now sign in with your email and password.',
|
||||||
duration: 6000,
|
duration: 6000,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect to auth page with email pre-filled
|
|
||||||
navigate(`/auth?email=${encodeURIComponent(email)}&message=complete-password-setup`);
|
|
||||||
} 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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 });
|
|
||||||
if (updateError) throw updateError;
|
|
||||||
|
|
||||||
// Step 2: Trigger signup confirmation email (this creates the email identity)
|
// Step 1: Store the desired password temporarily in session storage
|
||||||
console.log('[IdentityService] Sending signup confirmation email');
|
// This will be used after clicking the reset link
|
||||||
const { error: resendError } = await supabase.auth.resend({
|
sessionStorage.setItem('pending_password_setup', password);
|
||||||
type: 'signup',
|
console.log('[IdentityService] Stored pending password in session storage');
|
||||||
email: userEmail,
|
|
||||||
options: {
|
|
||||||
emailRedirectTo: `${window.location.origin}/auth?confirmed=password-setup`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (resendError) {
|
// Step 2: Trigger Supabase password reset email
|
||||||
console.error('[IdentityService] Failed to send confirmation email:', resendError);
|
// This creates the email identity when user clicks link and sets password
|
||||||
throw resendError;
|
const { error: resetError } = await supabase.auth.resetPasswordForEmail(
|
||||||
|
userEmail,
|
||||||
|
{
|
||||||
|
redirectTo: `${window.location.origin}/auth/callback?action=password-setup`
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
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
|
// 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
|
// Step 5: Log the action
|
||||||
await logIdentityChange(user!.id, 'password_added', {
|
await logIdentityChange(user!.id, 'password_setup_initiated', {
|
||||||
method: 'oauth_password_addition_with_signup_confirmation',
|
method: 'reset_password_flow',
|
||||||
confirmation_email_sent: true
|
reset_email_sent: true,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Step 6: Sign user out (they must confirm via email)
|
// Return success - user needs to check email and click reset link
|
||||||
console.log('[IdentityService] Signing user out');
|
|
||||||
await supabase.auth.signOut();
|
|
||||||
|
|
||||||
// Return success with relogin and email confirmation flags
|
|
||||||
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'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user