Refactor: Implement sign-out and re-login flow

This commit is contained in:
gpt-engineer-app[bot]
2025-10-14 15:16:49 +00:00
parent 92943f2692
commit 52cad6c4dc
5 changed files with 84 additions and 119 deletions

View File

@@ -18,7 +18,7 @@ import type { OAuthProvider } from '@/types/identity';
interface PasswordSetupDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess: () => void;
onSuccess: (email?: string) => void;
provider?: OAuthProvider;
mode?: 'standalone' | 'disconnect';
}
@@ -59,7 +59,13 @@ export function PasswordSetupDialog({
if (result.success) {
setPassword('');
setConfirmPassword('');
onSuccess();
if (result.needsRelogin && result.email) {
// Pass email to parent for redirect handling
onSuccess(result.email);
} else {
onSuccess();
}
onOpenChange(false);
} else {
setError(result.error || 'Failed to set password');

View File

@@ -1,11 +1,12 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import { Badge } from '@/components/ui/badge';
import { useToast } from '@/hooks/use-toast';
import { useAuth } from '@/hooks/useAuth';
import { Shield, Key, Smartphone, Globe, Loader2, RefreshCw } from 'lucide-react';
import { Shield, Key, Smartphone, Globe, Loader2 } from 'lucide-react';
import { TOTPSetup } from '@/components/auth/TOTPSetup';
import { GoogleIcon } from '@/components/icons/GoogleIcon';
import { DiscordIcon } from '@/components/icons/DiscordIcon';
@@ -21,6 +22,7 @@ import type { UserIdentity, OAuthProvider } from '@/types/identity';
export function SecurityTab() {
const { user } = useAuth();
const { toast } = useToast();
const navigate = useNavigate();
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
const [identities, setIdentities] = useState<UserIdentity[]>([]);
const [loadingIdentities, setLoadingIdentities] = useState(true);
@@ -120,56 +122,34 @@ export function SecurityTab() {
}
};
const handlePasswordSetupSuccess = async () => {
setAddingPassword(true);
const handlePasswordSetupSuccess = async (email?: string) => {
if (email) {
// Password was set, user was logged out, needs to sign in with email/password
toast({
title: 'Password Set Successfully',
description: 'Please sign in with your email and password to complete setup.',
duration: 6000,
});
try {
// Refresh identities - should now include email provider after waiting in addPasswordToAccount
await loadIdentities();
// Redirect to auth page with email pre-filled
navigate(`/auth?email=${encodeURIComponent(email)}&message=complete-password-setup`);
} else {
// Normal password change flow (user already had email identity)
setAddingPassword(true);
// Check if password was actually verified
if (!hasPassword && addPasswordMode === 'standalone') {
// Password addition failed verification
try {
await loadIdentities();
toast({
title: 'Verification Failed',
description: 'Password was set but identity verification failed. Please refresh the page and check your Security settings.',
variant: 'destructive'
});
return; // Don't close dialog or clear provider
}
if (addPasswordMode === 'disconnect' && passwordSetupProvider) {
toast({
title: "Password Set",
description: "You can now disconnect your social login."
});
await handleUnlinkSocial(passwordSetupProvider);
setPasswordSetupProvider(null);
} else {
toast({
title: 'Password Added',
description: 'You can now sign in using your email and password.',
title: 'Password Updated',
description: 'Your password has been successfully updated.',
});
setPasswordSetupProvider(null);
} finally {
setAddingPassword(false);
}
} finally {
setAddingPassword(false);
}
};
const handleRefreshIdentities = async () => {
setLoadingIdentities(true);
await loadIdentities();
setLoadingIdentities(false);
toast({
title: 'Identities Refreshed',
description: hasPassword
? 'Your password authentication is now active.'
: 'Identity status updated.',
});
};
const handleAddPassword = () => {
setAddPasswordMode('standalone');
setPasswordSetupProvider('google' as OAuthProvider);
@@ -230,33 +210,22 @@ export function SecurityTab() {
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center gap-2">
{hasPassword ? (
<Button onClick={() => setPasswordDialogOpen(true)}>
Change Password
</Button>
) : (
<Button onClick={handleAddPassword} disabled={addingPassword}>
{addingPassword ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Adding Password...
</>
) : (
'Add Password'
)}
</Button>
)}
<Button
variant="ghost"
size="icon"
onClick={handleRefreshIdentities}
disabled={loadingIdentities}
title="Refresh identity status"
>
<RefreshCw className={`h-4 w-4 ${loadingIdentities ? 'animate-spin' : ''}`} />
{hasPassword ? (
<Button onClick={() => setPasswordDialogOpen(true)}>
Change Password
</Button>
</div>
) : (
<Button onClick={handleAddPassword} disabled={addingPassword}>
{addingPassword ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Adding Password...
</>
) : (
'Add Password'
)}
</Button>
)}
</CardContent>
</Card>
</div>

View File

@@ -204,7 +204,9 @@ export async function addPasswordToAccount(
}
const { data: { user } } = await supabase.auth.getUser();
if (!user?.email) {
const userEmail = user?.email;
if (!userEmail) {
return {
success: false,
error: 'No email address found on your account'
@@ -216,56 +218,21 @@ export async function addPasswordToAccount(
const { error: updateError } = await supabase.auth.updateUser({ password });
if (updateError) throw updateError;
// Step 2: IMMEDIATELY attempt sign-in to force identity creation
// This is the ONLY reliable way to create the email identity
console.log('[IdentityService] Attempting sign-in to create email identity');
const { error: signInError } = await supabase.auth.signInWithPassword({
email: user.email,
password: password
// Step 2: Log the password addition
await logIdentityChange(user!.id, 'password_added', {
method: 'oauth_with_relogin_required'
});
if (signInError) {
// Sign-in failed, but password was set
console.error('[IdentityService] Sign-in failed:', signInError);
// Step 3: Sign the user out so they can sign back in with email/password
console.log('[IdentityService] Signing user out to force re-login');
await supabase.auth.signOut();
// Check if it's just an email confirmation issue
if (signInError.message?.includes('Email not confirmed')) {
// Password is set, identity might be created, just needs confirmation
console.log('[IdentityService] Email confirmation required, checking identity');
const emailCreated = await waitForEmailProvider(3);
if (emailCreated) {
await logIdentityChange(user.id, 'password_added', {
method: 'oauth_fallback_unconfirmed'
});
return { success: true };
}
}
return {
success: false,
error: `Password was set but authentication failed: ${signInError.message}. Please try signing out and signing back in with your email and password.`
};
}
// Step 3: Verify identity was created
console.log('[IdentityService] Sign-in successful, verifying identity creation');
const emailCreated = await waitForEmailProvider(4);
if (!emailCreated) {
console.error('[IdentityService] Identity not found after successful sign-in');
return {
success: false,
error: 'Password authentication was successful but identity verification failed. Please refresh the page.'
};
}
// Step 4: Log success
console.log('[IdentityService] Email identity successfully created');
await logIdentityChange(user.id, 'password_added', {
method: 'oauth_fallback_signin'
});
return { success: true };
// Return success with relogin flag
return {
success: true,
needsRelogin: true,
email: userEmail
};
} catch (error: any) {
console.error('[IdentityService] Failed to add password:', error);

View File

@@ -33,8 +33,13 @@ export default function Auth() {
const [signInCaptchaToken, setSignInCaptchaToken] = useState<string | null>(null);
const [signInCaptchaKey, setSignInCaptchaKey] = useState(0);
const [mfaFactorId, setMfaFactorId] = useState<string | null>(null);
const emailParam = searchParams.get('email');
const messageParam = searchParams.get('message');
const showPasswordSetupMessage = messageParam === 'complete-password-setup';
const [formData, setFormData] = useState({
email: '',
email: emailParam || '',
password: '',
confirmPassword: '',
username: '',
@@ -43,6 +48,13 @@ export default function Auth() {
const defaultTab = searchParams.get('tab') || 'signin';
const { user } = useAuth();
// Pre-fill email from query param
useEffect(() => {
if (emailParam) {
setFormData(prev => ({ ...prev, email: emailParam }));
}
}, [emailParam]);
// Auto-redirect when user is authenticated
useEffect(() => {
if (user) {
@@ -374,6 +386,15 @@ export default function Auth() {
</CardDescription>
</CardHeader>
<CardContent>
{showPasswordSetupMessage && (
<Alert className="mb-4">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
Your password has been set. Please sign in with your email and password to complete the setup.
</AlertDescription>
</Alert>
)}
{mfaFactorId ? (
<MFAChallenge
factorId={mfaFactorId}

View File

@@ -30,4 +30,6 @@ export interface IdentitySafetyCheck {
export interface IdentityOperationResult {
success: boolean;
error?: string;
needsRelogin?: boolean;
email?: string;
}