mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 09:11:13 -05:00
Refactor: Implement sign-out and re-login flow
This commit is contained in:
@@ -18,7 +18,7 @@ import type { OAuthProvider } from '@/types/identity';
|
|||||||
interface PasswordSetupDialogProps {
|
interface PasswordSetupDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
onSuccess: () => void;
|
onSuccess: (email?: string) => void;
|
||||||
provider?: OAuthProvider;
|
provider?: OAuthProvider;
|
||||||
mode?: 'standalone' | 'disconnect';
|
mode?: 'standalone' | 'disconnect';
|
||||||
}
|
}
|
||||||
@@ -59,7 +59,13 @@ export function PasswordSetupDialog({
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
setPassword('');
|
setPassword('');
|
||||||
setConfirmPassword('');
|
setConfirmPassword('');
|
||||||
|
|
||||||
|
if (result.needsRelogin && result.email) {
|
||||||
|
// Pass email to parent for redirect handling
|
||||||
|
onSuccess(result.email);
|
||||||
|
} else {
|
||||||
onSuccess();
|
onSuccess();
|
||||||
|
}
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
} else {
|
} else {
|
||||||
setError(result.error || 'Failed to set password');
|
setError(result.error || 'Failed to set password');
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
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 { TOTPSetup } from '@/components/auth/TOTPSetup';
|
||||||
import { GoogleIcon } from '@/components/icons/GoogleIcon';
|
import { GoogleIcon } from '@/components/icons/GoogleIcon';
|
||||||
import { DiscordIcon } from '@/components/icons/DiscordIcon';
|
import { DiscordIcon } from '@/components/icons/DiscordIcon';
|
||||||
@@ -21,6 +22,7 @@ import type { UserIdentity, OAuthProvider } from '@/types/identity';
|
|||||||
export function SecurityTab() {
|
export function SecurityTab() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const navigate = useNavigate();
|
||||||
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
|
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
|
||||||
const [identities, setIdentities] = useState<UserIdentity[]>([]);
|
const [identities, setIdentities] = useState<UserIdentity[]>([]);
|
||||||
const [loadingIdentities, setLoadingIdentities] = useState(true);
|
const [loadingIdentities, setLoadingIdentities] = useState(true);
|
||||||
@@ -120,54 +122,32 @@ export function SecurityTab() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePasswordSetupSuccess = async () => {
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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);
|
setAddingPassword(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Refresh identities - should now include email provider after waiting in addPasswordToAccount
|
|
||||||
await loadIdentities();
|
await loadIdentities();
|
||||||
|
|
||||||
// Check if password was actually verified
|
|
||||||
if (!hasPassword && addPasswordMode === 'standalone') {
|
|
||||||
// Password addition failed verification
|
|
||||||
toast({
|
toast({
|
||||||
title: 'Verification Failed',
|
title: 'Password Updated',
|
||||||
description: 'Password was set but identity verification failed. Please refresh the page and check your Security settings.',
|
description: 'Your password has been successfully updated.',
|
||||||
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.',
|
|
||||||
});
|
});
|
||||||
setPasswordSetupProvider(null);
|
setPasswordSetupProvider(null);
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
setAddingPassword(false);
|
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 = () => {
|
const handleAddPassword = () => {
|
||||||
@@ -230,7 +210,6 @@ export function SecurityTab() {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{hasPassword ? (
|
{hasPassword ? (
|
||||||
<Button onClick={() => setPasswordDialogOpen(true)}>
|
<Button onClick={() => setPasswordDialogOpen(true)}>
|
||||||
Change Password
|
Change Password
|
||||||
@@ -247,16 +226,6 @@ export function SecurityTab() {
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={handleRefreshIdentities}
|
|
||||||
disabled={loadingIdentities}
|
|
||||||
title="Refresh identity status"
|
|
||||||
>
|
|
||||||
<RefreshCw className={`h-4 w-4 ${loadingIdentities ? 'animate-spin' : ''}`} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -204,7 +204,9 @@ export async function addPasswordToAccount(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { data: { user } } = await supabase.auth.getUser();
|
const { data: { user } } = await supabase.auth.getUser();
|
||||||
if (!user?.email) {
|
const userEmail = user?.email;
|
||||||
|
|
||||||
|
if (!userEmail) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'No email address found on your account'
|
error: 'No email address found on your account'
|
||||||
@@ -216,56 +218,21 @@ export async function addPasswordToAccount(
|
|||||||
const { error: updateError } = await supabase.auth.updateUser({ password });
|
const { error: updateError } = await supabase.auth.updateUser({ password });
|
||||||
if (updateError) throw updateError;
|
if (updateError) throw updateError;
|
||||||
|
|
||||||
// Step 2: IMMEDIATELY attempt sign-in to force identity creation
|
// Step 2: Log the password addition
|
||||||
// This is the ONLY reliable way to create the email identity
|
await logIdentityChange(user!.id, 'password_added', {
|
||||||
console.log('[IdentityService] Attempting sign-in to create email identity');
|
method: 'oauth_with_relogin_required'
|
||||||
const { error: signInError } = await supabase.auth.signInWithPassword({
|
|
||||||
email: user.email,
|
|
||||||
password: password
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (signInError) {
|
// Step 3: Sign the user out so they can sign back in with email/password
|
||||||
// Sign-in failed, but password was set
|
console.log('[IdentityService] Signing user out to force re-login');
|
||||||
console.error('[IdentityService] Sign-in failed:', signInError);
|
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 with relogin flag
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: true,
|
||||||
error: `Password was set but authentication failed: ${signInError.message}. Please try signing out and signing back in with your email and password.`
|
needsRelogin: true,
|
||||||
|
email: userEmail
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
// 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 };
|
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[IdentityService] Failed to add password:', error);
|
console.error('[IdentityService] Failed to add password:', error);
|
||||||
|
|||||||
@@ -33,8 +33,13 @@ export default function Auth() {
|
|||||||
const [signInCaptchaToken, setSignInCaptchaToken] = useState<string | null>(null);
|
const [signInCaptchaToken, setSignInCaptchaToken] = useState<string | null>(null);
|
||||||
const [signInCaptchaKey, setSignInCaptchaKey] = useState(0);
|
const [signInCaptchaKey, setSignInCaptchaKey] = useState(0);
|
||||||
const [mfaFactorId, setMfaFactorId] = useState<string | null>(null);
|
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({
|
const [formData, setFormData] = useState({
|
||||||
email: '',
|
email: emailParam || '',
|
||||||
password: '',
|
password: '',
|
||||||
confirmPassword: '',
|
confirmPassword: '',
|
||||||
username: '',
|
username: '',
|
||||||
@@ -43,6 +48,13 @@ export default function Auth() {
|
|||||||
const defaultTab = searchParams.get('tab') || 'signin';
|
const defaultTab = searchParams.get('tab') || 'signin';
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
// Pre-fill email from query param
|
||||||
|
useEffect(() => {
|
||||||
|
if (emailParam) {
|
||||||
|
setFormData(prev => ({ ...prev, email: emailParam }));
|
||||||
|
}
|
||||||
|
}, [emailParam]);
|
||||||
|
|
||||||
// Auto-redirect when user is authenticated
|
// Auto-redirect when user is authenticated
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
@@ -374,6 +386,15 @@ export default function Auth() {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<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 ? (
|
{mfaFactorId ? (
|
||||||
<MFAChallenge
|
<MFAChallenge
|
||||||
factorId={mfaFactorId}
|
factorId={mfaFactorId}
|
||||||
|
|||||||
@@ -30,4 +30,6 @@ export interface IdentitySafetyCheck {
|
|||||||
export interface IdentityOperationResult {
|
export interface IdentityOperationResult {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
needsRelogin?: boolean;
|
||||||
|
email?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user