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 { handleError, handleSuccess } from '@/lib/errorHandler'; import { useAuth } from '@/hooks/useAuth'; import { Shield, Key, Smartphone, Globe, Loader2, Monitor, Tablet, Trash2 } from 'lucide-react'; import { format } from 'date-fns'; import { Skeleton } from '@/components/ui/skeleton'; 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 type { AuthSession } from '@/types/auth'; import { supabase } from '@/integrations/supabase/client'; import { logger } from '@/lib/logger'; import { SessionRevokeConfirmDialog } from './SessionRevokeConfirmDialog'; export function SecurityTab() { const { user } = useAuth(); 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); const [sessionToRevoke, setSessionToRevoke] = useState<{ id: string; isCurrent: boolean } | null>(null); // 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) { logger.error('Failed to load identities', { userId: user?.id, action: 'load_identities', error: error instanceof Error ? error.message : String(error) }); handleError(error, { action: 'Load connected accounts', userId: user?.id }); } finally { setLoadingIdentities(false); } }; const handleSocialLogin = async (provider: OAuthProvider) => { try { const result = await connectIdentity(provider, '/settings?tab=security'); if (!result.success) { handleError( new Error(result.error || 'Connection failed'), { action: `Connect ${provider} account` } ); } else { handleSuccess('Redirecting...', `Connecting your ${provider} account...`); } } catch (error) { handleError(error, { action: `Connect ${provider} account` }); } }; 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(); handleSuccess( "Password Required First", "Check your email for a password reset link. Once you've set a password, you can disconnect your social login." ); return; } if (safetyCheck.reason === 'last_identity') { handleError( new Error("You cannot disconnect your only login method"), { action: "Disconnect social login" } ); return; } } // Proceed with disconnect setDisconnectingProvider(provider); const result = await disconnectIdentity(provider); setDisconnectingProvider(null); if (result.success) { await loadIdentities(); // Refresh identities list handleSuccess("Disconnected", `Your ${provider} account has been successfully disconnected.`); } else { handleError( new Error(result.error || 'Disconnect failed'), { action: `Disconnect ${provider} account` } ); } }; const handleAddPassword = async () => { setAddingPassword(true); const result = await addPasswordToAccount(); if (result.success) { handleSuccess( "Password Setup Email Sent!", `Check ${result.email} for a password reset link. Click it to set your password.` ); } else { handleError( new Error(result.error || 'Failed to send email'), { action: 'Send password setup email' } ); } setAddingPassword(false); }; const fetchSessions = async () => { if (!user) return; const { data, error } = await supabase.rpc('get_my_sessions'); if (error) { logger.error('Failed to fetch sessions', { userId: user.id, action: 'fetch_sessions', error: error.message }); handleError(error, { action: 'Load sessions', userId: user.id }); } else { setSessions((data as AuthSession[]) || []); } setLoadingSessions(false); }; const initiateSessionRevoke = async (sessionId: string) => { // Get current session to check if revoking self const { data: { session: currentSession } } = await supabase.auth.getSession(); const isCurrentSession = currentSession && sessions.some(s => s.id === sessionId && s.refreshed_at === currentSession.access_token ); setSessionToRevoke({ id: sessionId, isCurrent: !!isCurrentSession }); }; const confirmRevokeSession = async () => { if (!sessionToRevoke) return; const { error } = await supabase.rpc('revoke_my_session', { session_id: sessionToRevoke.id }); if (error) { logger.error('Failed to revoke session', { userId: user?.id, action: 'revoke_session', sessionId: sessionToRevoke.id, error: error.message }); handleError(error, { action: 'Revoke session', userId: user?.id }); } else { handleSuccess('Success', 'Session revoked successfully'); if (sessionToRevoke.isCurrent) { // Redirect to login after revoking current session setTimeout(() => { window.location.href = '/auth'; }, 1000); } else { fetchSessions(); } } setSessionToRevoke(null); }; 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 ( <> { handleSuccess('Password updated', '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 ? (
{[1, 2, 3].map(i => (
))}
) : 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

)}
!open && setSessionToRevoke(null)} onConfirm={confirmRevokeSession} isCurrentSession={sessionToRevoke?.isCurrent ?? false} />
); }