import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { ShieldCheck, ShieldOff, Mail, Smartphone, Key, RefreshCw, Trash2, X, Eye, EyeOff, Copy } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { api } from '../api/client'; import { Card, CardContent, CardHeader } from './Card'; import { Button } from './Button'; import { useToast } from '../contexts/ToastContext'; import { useAuth } from '../contexts/AuthContext'; // ─── Small reusable code input ──────────────────────────────────────────────── function CodeInput({ value, onChange, placeholder, maxLength = 6, }: { value: string; onChange: (v: string) => void; placeholder?: string; maxLength?: number; }) { return ( onChange(e.target.value.toUpperCase().replace(/\s/g, ''))} maxLength={maxLength} className="w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors font-mono tracking-widest text-center" placeholder={placeholder} autoComplete="one-time-code" /> ); } // ─── Backup codes display ───────────────────────────────────────────────────── function BackupCodesDisplay({ codes, onDone }: { codes: string[]; onDone: () => void }) { const { t } = useTranslation(); const [copied, setCopied] = useState(false); const handleCopy = () => { navigator.clipboard.writeText(codes.join('\n')); setCopied(true); setTimeout(() => setCopied(false), 2000); }; return (

{t('settings.twoFa.backupCodesWarning')}

{codes.map((code, index) => ( {code} ))}
); } // ─── TOTP setup wizard ──────────────────────────────────────────────────────── function TOTPSetupWizard({ onDone }: { onDone: () => void }) { const { t } = useTranslation(); const queryClient = useQueryClient(); const { showToast } = useToast(); const [step, setStep] = useState<'qr' | 'confirm' | 'backup'>('qr'); const [code, setCode] = useState(''); const [backupCodes, setBackupCodes] = useState([]); const [showSecret, setShowSecret] = useState(false); const { data: setupData, isLoading } = useQuery({ queryKey: ['totp-setup'], queryFn: () => api.setupTOTP(), staleTime: Infinity, }); const enableMutation = useMutation({ mutationFn: (c: string) => api.enableTOTP(c), onSuccess: (data) => { setBackupCodes(data.backup_codes); setStep('backup'); queryClient.invalidateQueries({ queryKey: ['2fa-status'] }); }, onError: () => showToast(t('settings.twoFa.invalidCode'), 'error'), }); if (isLoading || !setupData) { return (
); } if (step === 'qr') { return (

{t('settings.twoFa.setupInstructions')}

TOTP QR Code

{t('settings.twoFa.manualEntry')}

{showSecret ? setupData.secret : '••••••••••••••••'}
); } if (step === 'confirm') { return (

{t('settings.twoFa.enterCodeToConfirm')}

); } // step === 'backup' return (

{t('settings.twoFa.backupCodesTitle')}

); } // ─── Main component ─────────────────────────────────────────────────────────── export function TwoFactorSettings() { const { t } = useTranslation(); const queryClient = useQueryClient(); const { showToast } = useToast(); const { user } = useAuth(); const [showTOTPSetup, setShowTOTPSetup] = useState(false); const [showDisableTOTP, setShowDisableTOTP] = useState(false); const [showRegenBackup, setShowRegenBackup] = useState(false); const [disableCode, setDisableCode] = useState(''); const [regenCode, setRegenCode] = useState(''); const [newBackupCodes, setNewBackupCodes] = useState(null); // Email OTP enable: two-step proof-of-possession flow const [emailSetupToken, setEmailSetupToken] = useState(null); const [emailSetupCode, setEmailSetupCode] = useState(''); // Email OTP disable: requires account password const [showDisableEmail, setShowDisableEmail] = useState(false); const [emailDisablePassword, setEmailDisablePassword] = useState(''); const [showEmailDisablePassword, setShowEmailDisablePassword] = useState(false); const { data: status, isLoading } = useQuery({ queryKey: ['2fa-status'], queryFn: () => api.get2FAStatus(), }); const { data: oidcLinks } = useQuery({ queryKey: ['oidc-links'], queryFn: () => api.getOIDCLinks(), }); // Step 1: request verification code (proof of possession) const enableEmailRequestMutation = useMutation({ mutationFn: () => api.enableEmailOTP(), onSuccess: (data: { message: string; setup_token: string }) => { setEmailSetupToken(data.setup_token); showToast(data.message, 'success'); }, onError: (e: Error) => { const msg = e.message ?? ''; if (msg.toLowerCase().includes('smtp')) { showToast(t('settings.twoFa.smtpRequired'), 'error'); } else { showToast(msg, 'error'); } }, }); // Step 2: confirm with the code received by email const enableEmailConfirmMutation = useMutation({ mutationFn: () => api.confirmEnableEmailOTP(emailSetupToken!, emailSetupCode), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['2fa-status'] }); setEmailSetupToken(null); setEmailSetupCode(''); showToast(t('settings.twoFa.emailOtpEnabled'), 'success'); }, onError: (e: Error) => showToast(e.message, 'error'), }); const disableEmailMutation = useMutation({ mutationFn: (password: string) => api.disableEmailOTP(password), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['2fa-status'] }); setShowDisableEmail(false); setEmailDisablePassword(''); showToast(t('settings.twoFa.emailOtpDisabled'), 'success'); }, onError: (e: Error) => showToast(e.message, 'error'), }); const disableTOTPMutation = useMutation({ mutationFn: (code: string) => api.disableTOTP(code), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['2fa-status'] }); setShowDisableTOTP(false); setDisableCode(''); showToast(t('settings.twoFa.totpDisabled'), 'success'); }, onError: () => showToast(t('settings.twoFa.invalidCode'), 'error'), }); const regenMutation = useMutation({ mutationFn: (code: string) => api.regenerateBackupCodes(code), onSuccess: (data) => { queryClient.invalidateQueries({ queryKey: ['2fa-status'] }); setShowRegenBackup(false); setRegenCode(''); setNewBackupCodes(data.backup_codes); }, onError: () => showToast(t('settings.twoFa.invalidCode'), 'error'), }); const unlinkOIDCMutation = useMutation({ mutationFn: (providerId: number) => api.deleteOIDCLink(providerId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['oidc-links'] }); showToast(t('settings.twoFa.oidcUnlinked'), 'success'); }, onError: (e: Error) => showToast(e.message, 'error'), }); if (isLoading) { return (
); } const hasEmail = !!user?.email; return (
{/* ── TOTP ─────────────────────────────────────────────────────────── */}

{t('settings.twoFa.totpTitle')}

{t('settings.twoFa.totpDesc')}

{status?.totp_enabled ? ( {t('common.enabled')} ) : ( {t('common.disabled')} )}
{/* TOTP Setup wizard */} {showTOTPSetup ? (

{t('settings.twoFa.setupAuthApp')}

{ setShowTOTPSetup(false); queryClient.removeQueries({ queryKey: ['totp-setup'] }); }} />
) : showDisableTOTP ? (

{t('settings.twoFa.disableConfirmHint')}

) : showRegenBackup ? (

{t('settings.twoFa.regenBackupHint')}

) : newBackupCodes ? (

{t('settings.twoFa.newBackupCodes')}

setNewBackupCodes(null)} />
) : (
{!status?.totp_enabled ? ( ) : (
{t('settings.twoFa.backupCodesRemaining', { count: status.backup_codes_remaining })}
)}
)}
{/* ── Email OTP ─────────────────────────────────────────────────────── */}

{t('settings.twoFa.emailOtpTitle')}

{hasEmail ? t('settings.twoFa.emailOtpDesc', { email: user?.email }) : t('settings.twoFa.emailOtpNoEmail')}

{/* Show status badge; enable/disable handled in CardContent */}
{status?.email_otp_enabled ? ( {t('common.enabled')} ) : ( {t('common.disabled')} )}
{!hasEmail ? (

{t('settings.twoFa.addEmailFirst')}

) : emailSetupToken ? ( /* Step 2: enter the code that was sent to the email */

{t('settings.twoFa.emailSetupEnterCode')}

) : showDisableEmail ? ( /* Disable: require account password for re-auth */

{t('settings.twoFa.emailDisablePasswordHint')}

setEmailDisablePassword(e.target.value)} placeholder={t('settings.twoFa.passwordPlaceholder')} className="w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors" />
) : (
{!status?.email_otp_enabled ? ( ) : ( )}
)}
{/* ── Linked SSO accounts ───────────────────────────────────────────── */} {oidcLinks && oidcLinks.length > 0 && (

{t('settings.twoFa.linkedAccounts')}

{t('settings.twoFa.linkedAccountsDesc')}

{oidcLinks.map((link) => (

{link.provider_name}

{link.provider_email && (

{link.provider_email}

)}
))}
)}
); }