Refactor identity management

This commit is contained in:
gpt-engineer-app[bot]
2025-10-14 17:38:18 +00:00
parent 5c075c363e
commit a255442616
5 changed files with 21 additions and 456 deletions

View File

@@ -160,49 +160,13 @@ export async function connectIdentity(
}
}
/**
* Wait for email provider to be created after password addition
* Supabase takes time to create the email identity, so we poll with retries
*/
async function waitForEmailProvider(maxRetries = 6): Promise<boolean> {
const delays = [500, 1000, 1500, 2000, 2500, 3000]; // ~10.5s total
for (let i = 0; i < maxRetries; i++) {
const identities = await getUserIdentities();
const hasEmail = identities.some(id => id.provider === 'email');
if (hasEmail) {
console.log(`[IdentityService] Email provider found after ${i + 1} attempts`);
return true;
}
console.log(`[IdentityService] Email provider not found, attempt ${i + 1}/${maxRetries}`);
if (i < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, delays[i]));
}
}
console.error('[IdentityService] Email provider not found after max retries');
return false;
}
/**
* Add password authentication to an OAuth-only account
* Automatically creates email identity by signing in immediately after setting password
* Triggers Supabase password reset flow - user sets password via email link
*/
export async function addPasswordToAccount(
password: string
): Promise<IdentityOperationResult> {
export async function addPasswordToAccount(): Promise<IdentityOperationResult> {
try {
// Validate password strength
if (password.length < 8) {
return {
success: false,
error: 'Password must be at least 8 characters long'
};
}
const { data: { user } } = await supabase.auth.getUser();
const userEmail = user?.email;
@@ -213,61 +177,30 @@ export async function addPasswordToAccount(
};
}
console.log('[IdentityService] Initiating password setup via reset flow');
console.log('[IdentityService] Sending password reset email');
// 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
// Trigger Supabase password reset email
// User clicks link and sets password, which automatically creates email identity
const { error: resetError } = await supabase.auth.resetPasswordForEmail(
userEmail,
{
redirectTo: `${window.location.origin}/auth/callback?action=password-setup`
redirectTo: `${window.location.origin}/auth/callback?type=recovery`
}
);
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
const { data: profile } = await supabase
.from('profiles')
.select('display_name, username')
.eq('user_id', user!.id)
.single();
// 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', {
body: {
email: userEmail,
displayName: profile?.display_name,
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
// 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,
needsEmailConfirmation: true,
@@ -275,98 +208,6 @@ export async function addPasswordToAccount(
};
} catch (error: any) {
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 initiate password setup'
};
}
}
/**
* Check if user has an orphaned password (password exists but no email identity)
*/
export async function hasOrphanedPassword(): Promise<boolean> {
const identities = await getUserIdentities();
const hasEmailIdentity = identities.some(i => i.provider === 'email');
if (hasEmailIdentity) return false;
// If user has OAuth identities but no email identity, they might have an orphaned password
return identities.length > 0 && !hasEmailIdentity;
}
/**
* Trigger email confirmation for orphaned password
* Direct trigger without requiring password re-entry
*/
export async function triggerOrphanedPasswordConfirmation(
source?: string
): Promise<IdentityOperationResult> {
try {
const { data: { user } } = await supabase.auth.getUser();
if (!user?.email) {
return {
success: false,
error: 'No email found for current user'
};
}
console.log('[IdentityService] Resending password reset email for orphaned password');
// 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 (resetError) {
console.error('[IdentityService] Failed to send password reset email:', resetError);
throw resetError;
}
console.log('[IdentityService] Password reset email sent successfully');
// Optional: Get profile for custom notification
const { data: profile } = await supabase
.from('profiles')
.select('display_name, username')
.eq('user_id', user.id)
.single();
// Optional: Send custom notification email (non-blocking)
try {
await supabase.functions.invoke('send-password-added-email', {
body: {
email: user.email,
displayName: profile?.display_name,
username: profile?.username,
},
});
} catch (emailError) {
console.error('[IdentityService] Custom email failed (non-blocking):', emailError);
}
// Log the action
await logIdentityChange(user.id, 'orphaned_password_confirmation_triggered', {
method: source || 'manual_button_click',
timestamp: new Date().toISOString(),
reset_email_sent: true
});
return {
success: true,
needsEmailConfirmation: true,
email: user.email
};
} catch (error: any) {
console.error('[IdentityService] Failed to trigger password reset:', error);
return {
success: false,
error: error.message || 'Failed to send password reset email'
@@ -374,6 +215,7 @@ export async function triggerOrphanedPasswordConfirmation(
}
}
/**
* Log identity changes to audit log
*/