mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 02:51:12 -05:00
226 lines
7.0 KiB
TypeScript
226 lines
7.0 KiB
TypeScript
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>
|
|
);
|
|
}
|