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, Monitor, Tablet, Trash2 } from 'lucide-react'; import { format } from 'date-fns'; import { TOTPSetup } from '@/components/auth/TOTPSetup'; import { GoogleIcon } from '@/components/icons/GoogleIcon'; import { DiscordIcon } from '@/components/icons/DiscordIcon'; import { PasswordUpdateDialog } from './PasswordUpdateDialog'; import { getUserIdentities, checkDisconnectSafety, disconnectIdentity, connectIdentity, addPasswordToAccount } from '@/lib/identityService'; import type { UserIdentity, OAuthProvider } from '@/types/identity'; import { toast as sonnerToast } from '@/components/ui/sonner'; import { supabase } from '@/integrations/supabase/client'; interface AuthSession { id: string; created_at: string; updated_at: string; refreshed_at: string | null; user_agent: string | null; ip: unknown; not_after: string | null; aal: 'aal1' | 'aal2' | 'aal3' | null; } export function SecurityTab() { const { user } = useAuth(); const { toast } = useToast(); const navigate = useNavigate(); const [passwordDialogOpen, setPasswordDialogOpen] = useState(false); const [identities, setIdentities] = useState([]); const [loadingIdentities, setLoadingIdentities] = useState(true); const [disconnectingProvider, setDisconnectingProvider] = useState(null); const [hasPassword, setHasPassword] = useState(false); const [addingPassword, setAddingPassword] = useState(false); const [sessions, setSessions] = useState([]); const [loadingSessions, setLoadingSessions] = useState(true); // Load user identities on mount useEffect(() => { loadIdentities(); fetchSessions(); }, []); const loadIdentities = async () => { try { setLoadingIdentities(true); const fetchedIdentities = await getUserIdentities(); setIdentities(fetchedIdentities); // Check if user has email/password auth const hasEmailProvider = fetchedIdentities.some(i => i.provider === 'email'); setHasPassword(hasEmailProvider); } catch (error) { console.error('Failed to load identities:', error); toast({ title: 'Error', description: 'Failed to load connected accounts', variant: 'destructive', }); } finally { setLoadingIdentities(false); } }; const handleSocialLogin = async (provider: OAuthProvider) => { try { const result = await connectIdentity(provider, '/settings?tab=security'); if (!result.success) { // Handle rate limiting if (result.error?.includes('rate limit')) { toast({ title: 'Too Many Attempts', description: 'Please wait a few minutes before trying again.', variant: 'destructive' }); } else { toast({ title: 'Connection Failed', description: result.error, variant: 'destructive' }); } } else { toast({ title: 'Redirecting...', description: `Connecting your ${provider} account...` }); } } catch (error: any) { toast({ title: 'Connection Error', description: error.message || 'Failed to connect account', variant: 'destructive' }); } }; const handleUnlinkSocial = async (provider: OAuthProvider) => { // Check if disconnect is safe const safetyCheck = await checkDisconnectSafety(provider); if (!safetyCheck.canDisconnect) { if (safetyCheck.reason === 'no_password_backup') { // Trigger password reset flow first await handleAddPassword(); toast({ title: "Password Required First", description: "Check your email for a password reset link. Once you've set a password, you can disconnect your social login.", duration: 10000 }); return; } if (safetyCheck.reason === 'last_identity') { toast({ title: "Cannot Disconnect", description: "You cannot disconnect your only login method. Please add another authentication method first.", variant: "destructive" }); return; } } // Proceed with disconnect setDisconnectingProvider(provider); const result = await disconnectIdentity(provider); setDisconnectingProvider(null); if (result.success) { await loadIdentities(); // Refresh identities list toast({ title: "Disconnected", description: `Your ${provider} account has been successfully disconnected.` }); } else { toast({ title: "Disconnect Failed", description: result.error, variant: "destructive" }); } }; const handleAddPassword = async () => { setAddingPassword(true); const result = await addPasswordToAccount(); if (result.success) { sonnerToast.success("Password Setup Email Sent!", { description: `Check ${result.email} for a password reset link. Click it to set your password.`, duration: 15000, }); } else { toast({ title: "Failed to Send Email", description: result.error, variant: "destructive" }); } setAddingPassword(false); }; const fetchSessions = async () => { if (!user) return; const { data, error } = await supabase.rpc('get_my_sessions'); if (error) { console.error('Error fetching sessions:', error); toast({ title: 'Error', description: 'Failed to load sessions', variant: 'destructive' }); } else { setSessions(data || []); } setLoadingSessions(false); }; const revokeSession = async (sessionId: string) => { const { error } = await supabase.rpc('revoke_my_session', { session_id: sessionId }); if (error) { toast({ title: 'Error', description: 'Failed to revoke session', variant: 'destructive' }); } else { toast({ title: 'Success', description: 'Session revoked successfully' }); fetchSessions(); } }; const getDeviceIcon = (userAgent: string | null) => { if (!userAgent) return ; const ua = userAgent.toLowerCase(); if (ua.includes('mobile') || ua.includes('android') || ua.includes('iphone')) { return ; } if (ua.includes('tablet') || ua.includes('ipad')) { return ; } return ; }; const getBrowserName = (userAgent: string | null) => { if (!userAgent) return 'Unknown Browser'; const ua = userAgent.toLowerCase(); if (ua.includes('firefox')) return 'Firefox'; if (ua.includes('chrome') && !ua.includes('edg')) return 'Chrome'; if (ua.includes('safari') && !ua.includes('chrome')) return 'Safari'; if (ua.includes('edg')) return 'Edge'; return 'Unknown Browser'; }; // Get connected accounts with identity data const connectedAccounts = [ { provider: 'google' as OAuthProvider, identity: identities.find(i => i.provider === 'google'), icon: }, { provider: 'discord' as OAuthProvider, identity: identities.find(i => i.provider === 'discord'), icon: } ]; return ( <> { toast({ title: 'Password updated', description: 'Your password has been successfully changed.' }); }} />
{/* Password Section - Conditional based on auth method */}

{hasPassword ? 'Change Password' : 'Add Password'}

{hasPassword ? ( <>Update your password to keep your account secure. ) : ( <>Add password authentication to your account for increased security and backup access. )} {hasPassword ? ( ) : ( )}
{/* Social Login Connections */}

Connected Accounts

Manage your social login connections for easier access to your account. To enable social providers, configure them in your Supabase Auth settings. {loadingIdentities ? (
) : ( connectedAccounts.map(account => { const isConnected = !!account.identity; const isDisconnecting = disconnectingProvider === account.provider; const email = account.identity?.identity_data?.email; return (
{account.icon}

{account.provider}

{isConnected && email && (

{email}

)}
{isConnected ? ( <> Connected ) : ( )}
); }) )}
{/* Two-Factor Authentication */}

Two-Factor Authentication

{/* Active Sessions & Login History */}

Active Sessions {sessions.length > 0 && `(${sessions.length})`}

Review and manage your active login sessions across all devices {loadingSessions ? (
) : sessions.length > 0 ? (
{sessions.map((session) => (
{getDeviceIcon(session.user_agent)}

{getBrowserName(session.user_agent)}

{session.ip && `${session.ip} • `} Last active: {format(new Date(session.refreshed_at || session.created_at), 'PPpp')} {session.aal === 'aal2' && ' • MFA'}

{session.not_after && (

Expires: {format(new Date(session.not_after), 'PPpp')}

)}
))}
) : (

No active sessions found

)}
); }