import { useState, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Badge } from '@/components/ui/badge'; import { handleError, handleSuccess, handleInfo, handleNonCriticalError, AppError, getErrorMessage } from '@/lib/errorHandler'; import { logger } from '@/lib/logger'; import { useAuth } from '@/hooks/useAuth'; import { useRequireMFA } from '@/hooks/useRequireMFA'; import { supabase } from '@/lib/supabaseClient'; import { Smartphone, Shield, Copy, Eye, EyeOff, Trash2, AlertTriangle } from 'lucide-react'; import { MFARemovalDialog } from './MFARemovalDialog'; import { setStepUpRequired, getAuthMethod } from '@/lib/sessionFlags'; import { useNavigate } from 'react-router-dom'; import type { MFAFactor } from '@/types/auth'; export function TOTPSetup() { const { user } = useAuth(); const { requiresMFA } = useRequireMFA(); const navigate = useNavigate(); const [factors, setFactors] = useState([]); const [loading, setLoading] = useState(false); const [enrolling, setEnrolling] = useState(false); const [qrCode, setQrCode] = useState(''); const [secret, setSecret] = useState(''); const [factorId, setFactorId] = useState(''); const [verificationCode, setVerificationCode] = useState(''); const [showSecret, setShowSecret] = useState(false); const [showRemovalDialog, setShowRemovalDialog] = useState(false); useEffect(() => { fetchTOTPFactors(); }, [user]); const fetchTOTPFactors = async () => { if (!user) return; try { const { data, error } = await supabase.auth.mfa.listFactors(); if (error) throw error; const totpFactors = (data.totp || []).map(factor => ({ id: factor.id, friendly_name: factor.friendly_name || 'Authenticator App', factor_type: 'totp' as const, status: factor.status as 'verified' | 'unverified', created_at: factor.created_at, updated_at: factor.updated_at })); setFactors(totpFactors); } catch (error: unknown) { handleNonCriticalError(error, { action: 'Fetch TOTP factors', userId: user?.id, metadata: { context: 'initial_load' } }); } }; const startEnrollment = async () => { if (!user) return; setLoading(true); try { const { data, error } = await supabase.auth.mfa.enroll({ factorType: 'totp', friendlyName: 'Authenticator App' }); if (error) throw error; setQrCode(data.totp.qr_code); setSecret(data.totp.secret); setFactorId(data.id); setEnrolling(true); } catch (error: unknown) { handleError( new AppError( getErrorMessage(error) || 'Failed to start TOTP enrollment', 'TOTP_ENROLL_FAILED' ), { action: 'Start TOTP enrollment', userId: user?.id } ); } finally { setLoading(false); } }; const verifyAndEnable = async () => { if (!factorId || !verificationCode.trim()) { handleError( new AppError('Please enter the verification code', 'INVALID_INPUT'), { action: 'Verify TOTP', userId: user?.id, metadata: { step: 'code_entry' } } ); return; } setLoading(true); try { // Step 1: Create a challenge first const { data: challengeData, error: challengeError } = await supabase.auth.mfa.challenge({ factorId }); if (challengeError) throw challengeError; // Step 2: Verify using the challengeId from the challenge response const { error: verifyError } = await supabase.auth.mfa.verify({ factorId, challengeId: challengeData.id, code: verificationCode.trim() }); if (verifyError) throw verifyError; // Check if user signed in via OAuth and trigger step-up flow const authMethod = getAuthMethod(); const isOAuthUser = authMethod === 'oauth'; if (isOAuthUser) { setStepUpRequired(true, window.location.pathname); navigate('/auth/mfa-step-up'); return; } handleSuccess( 'Multi-Factor Authentication Enabled', isOAuthUser ? 'Please verify with your authenticator app to continue.' : 'Please sign in again to activate Multi-Factor Authentication protection.' ); if (isOAuthUser) { // Already handled above with navigate return; } else { // For email/password users, force sign out to require MFA on next login setTimeout(async () => { await supabase.auth.signOut(); window.location.href = '/auth'; }, 2000); } } catch (error: unknown) { handleError( new AppError( getErrorMessage(error) || 'Invalid verification code. Please try again.', 'TOTP_VERIFY_FAILED' ), { action: 'Verify TOTP code', userId: user?.id, metadata: { factorId } } ); } finally { setLoading(false); } }; const handleRemovalSuccess = async () => { await fetchTOTPFactors(); }; const copySecret = () => { navigator.clipboard.writeText(secret); handleInfo('Copied', 'Secret key copied to clipboard'); }; const cancelEnrollment = () => { setEnrolling(false); setQrCode(''); setSecret(''); setFactorId(''); setVerificationCode(''); }; const activeFactor = factors.find(f => f.status === 'verified'); if (enrolling) { return ( Set Up Authenticator App Scan the QR code with your authenticator app, then enter the verification code below. {/* QR Code */}
TOTP QR Code
{/* Manual Entry */}
{/* Verification */}
setVerificationCode(e.target.value)} onPaste={(e) => e.preventDefault()} placeholder="000000" maxLength={6} className="text-center text-lg tracking-widest font-mono" />
); } return ( Add an extra layer of security to your account with Multi-Factor Authentication. {activeFactor ? (
Multi-Factor Authentication is enabled for your account. You'll be prompted for a code from your authenticator app when signing in. {/* Phase 2: Warning for role-required users */} {requiresMFA && ( Your role requires Multi-Factor Authentication. You cannot disable it. )}

{activeFactor.friendly_name || 'Authenticator App'}

Enabled {new Date(activeFactor.created_at).toLocaleDateString()}

Active
) : (

Authenticator App

Use an authenticator app to generate verification codes

)}
); }