feat: Implement all authentication compliance phases

This commit is contained in:
gpt-engineer-app[bot]
2025-10-31 14:01:45 +00:00
parent 4bc749a843
commit dade374c2a
8 changed files with 757 additions and 73 deletions

View File

@@ -0,0 +1,225 @@
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Link, Unlink, Shield, AlertCircle } from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
import {
getUserIdentities,
disconnectIdentity,
linkOAuthIdentity,
checkDisconnectSafety,
addPasswordToAccount
} from '@/lib/identityService';
import type { UserIdentity, OAuthProvider } from '@/types/identity';
export function IdentityManagement() {
const { toast } = useToast();
const [identities, setIdentities] = useState<UserIdentity[]>([]);
const [loading, setLoading] = useState(true);
const [actionLoading, setActionLoading] = useState<string | null>(null);
useEffect(() => {
loadIdentities();
}, []);
const loadIdentities = async () => {
setLoading(true);
const data = await getUserIdentities();
setIdentities(data);
setLoading(false);
};
const handleDisconnect = async (provider: OAuthProvider) => {
// Safety check
const safety = await checkDisconnectSafety(provider);
if (!safety.canDisconnect) {
toast({
variant: 'destructive',
title: 'Cannot Disconnect',
description: safety.reason === 'last_identity'
? 'This is your only sign-in method. Add a password or another provider first.'
: 'Please add a password before disconnecting your last social login.',
});
return;
}
setActionLoading(provider);
const result = await disconnectIdentity(provider);
if (result.success) {
toast({
title: 'Provider Disconnected',
description: `${provider} has been removed from your account.`,
});
await loadIdentities();
} else if (result.requiresAAL2) {
toast({
variant: 'destructive',
title: 'MFA Required',
description: result.error || 'Please verify your identity with MFA.',
});
} else {
toast({
variant: 'destructive',
title: 'Failed to Disconnect',
description: result.error,
});
}
setActionLoading(null);
};
const handleLink = async (provider: OAuthProvider) => {
setActionLoading(provider);
const result = await linkOAuthIdentity(provider);
if (result.success) {
// OAuth redirect will happen automatically
toast({
title: 'Redirecting...',
description: `Opening ${provider} sign-in window...`,
});
} else {
toast({
variant: 'destructive',
title: 'Failed to Link',
description: result.error,
});
setActionLoading(null);
}
};
const handleAddPassword = async () => {
setActionLoading('password');
const result = await addPasswordToAccount();
if (result.success) {
toast({
title: 'Check Your Email',
description: `We've sent a password setup link to ${result.email}`,
});
} else {
toast({
variant: 'destructive',
title: 'Failed to Add Password',
description: result.error,
});
}
setActionLoading(null);
};
const hasProvider = (provider: string) =>
identities.some(i => i.provider === provider);
const hasPassword = hasProvider('email');
const providers: { id: OAuthProvider; label: string; icon: string }[] = [
{ id: 'google', label: 'Google', icon: 'G' },
{ id: 'discord', label: 'Discord', icon: 'D' },
];
if (loading) {
return (
<Card>
<CardHeader>
<CardTitle>Connected Accounts</CardTitle>
<CardDescription>Loading...</CardDescription>
</CardHeader>
</Card>
);
}
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Link className="w-5 h-5" />
Connected Accounts
</CardTitle>
<CardDescription>
Link multiple sign-in methods to your account for easy access
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{identities.length === 1 && !hasPassword && (
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
Add a password as a backup sign-in method
</AlertDescription>
</Alert>
)}
{/* Password Authentication */}
<div className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
<Shield className="w-5 h-5 text-primary" />
</div>
<div>
<div className="font-medium">Email & Password</div>
<div className="text-sm text-muted-foreground">
{hasPassword ? 'Connected' : 'Not set up'}
</div>
</div>
</div>
{!hasPassword && (
<Button
variant="outline"
size="sm"
onClick={handleAddPassword}
disabled={actionLoading === 'password'}
>
<Link className="w-4 h-4 mr-2" />
{actionLoading === 'password' ? 'Setting up...' : 'Add Password'}
</Button>
)}
</div>
{/* OAuth Providers */}
{providers.map((provider) => {
const isConnected = hasProvider(provider.id);
return (
<div key={provider.id} className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center font-bold">
{provider.icon}
</div>
<div>
<div className="font-medium">{provider.label}</div>
<div className="text-sm text-muted-foreground">
{isConnected ? 'Connected' : 'Not connected'}
</div>
</div>
</div>
<Button
variant={isConnected ? 'destructive' : 'outline'}
size="sm"
onClick={() => isConnected
? handleDisconnect(provider.id)
: handleLink(provider.id)
}
disabled={actionLoading === provider.id}
>
{isConnected ? (
<>
<Unlink className="w-4 h-4 mr-2" />
{actionLoading === provider.id ? 'Disconnecting...' : 'Disconnect'}
</>
) : (
<>
<Link className="w-4 h-4 mr-2" />
{actionLoading === provider.id ? 'Connecting...' : 'Connect'}
</>
)}
</Button>
</div>
);
})}
</CardContent>
</Card>
);
}