import React, { useState } from "react"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card.tsx"; import { Button } from "@/components/ui/button.tsx"; import { Input } from "@/components/ui/input.tsx"; import { PasswordInput } from "@/components/ui/password-input.tsx"; import { Label } from "@/components/ui/label.tsx"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx"; import { Tabs, TabsContent, TabsList, TabsTrigger, } from "@/components/ui/tabs.tsx"; import { Shield, Copy, Download, AlertCircle, CheckCircle2, } from "lucide-react"; import { setupTOTP, enableTOTP, disableTOTP, generateBackupCodes, } from "@/ui/main-axios.ts"; import { toast } from "sonner"; import { useTranslation } from "react-i18next"; interface TOTPSetupProps { isEnabled: boolean; onStatusChange?: (enabled: boolean) => void; } export function TOTPSetup({ isEnabled: initialEnabled, onStatusChange, }: TOTPSetupProps) { const { t } = useTranslation(); const [isEnabled, setIsEnabled] = useState(initialEnabled); const [isSettingUp, setIsSettingUp] = useState(false); const [setupStep, setSetupStep] = useState< "init" | "qr" | "verify" | "backup" >("init"); const [qrCode, setQrCode] = useState(""); const [secret, setSecret] = useState(""); const [verificationCode, setVerificationCode] = useState(""); const [backupCodes, setBackupCodes] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [password, setPassword] = useState(""); const [disableCode, setDisableCode] = useState(""); const handleSetupStart = async () => { setError(null); setLoading(true); try { const response = await setupTOTP(); setQrCode(response.qr_code); setSecret(response.secret); setSetupStep("qr"); setIsSettingUp(true); } catch (err: unknown) { const error = err as { response?: { data?: { error?: string } } }; setError(error?.response?.data?.error || "Failed to start TOTP setup"); } finally { setLoading(false); } }; const handleVerifyCode = async () => { if (verificationCode.length !== 6) { setError("Please enter a 6-digit code"); return; } setError(null); setLoading(true); try { const response = await enableTOTP(verificationCode); setBackupCodes(response.backup_codes); setSetupStep("backup"); toast.success(t("auth.twoFactorEnabledSuccess")); } catch (err: unknown) { const error = err as { response?: { data?: { error?: string } } }; setError(error?.response?.data?.error || "Invalid verification code"); } finally { setLoading(false); } }; const handleDisable = async () => { setError(null); setLoading(true); try { await disableTOTP(password || undefined, disableCode || undefined); setIsEnabled(false); setIsSettingUp(false); setSetupStep("init"); setPassword(""); setDisableCode(""); onStatusChange?.(false); toast.success(t("auth.twoFactorDisabled")); } catch (err: unknown) { const error = err as { response?: { data?: { error?: string } } }; setError(error?.response?.data?.error || "Failed to disable TOTP"); } finally { setLoading(false); } }; const handleGenerateNewBackupCodes = async () => { setError(null); setLoading(true); try { const response = await generateBackupCodes( password || undefined, disableCode || undefined, ); setBackupCodes(response.backup_codes); toast.success(t("auth.newBackupCodesGenerated")); } catch (err: unknown) { const error = err as { response?: { data?: { error?: string } } }; setError( error?.response?.data?.error || "Failed to generate backup codes", ); } finally { setLoading(false); } }; const copyToClipboard = (text: string, label: string) => { navigator.clipboard.writeText(text); toast.success(t("messages.copiedToClipboard", { item: label })); }; const downloadBackupCodes = () => { const content = `Termix Two-Factor Authentication Backup Codes\n` + `Generated: ${new Date().toISOString()}\n\n` + `Keep these codes in a safe place. Each code can only be used once.\n\n` + backupCodes.map((code, i) => `${i + 1}. ${code}`).join("\n"); const blob = new Blob([content], { type: "text/plain" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "termix-backup-codes.txt"; a.click(); URL.revokeObjectURL(url); toast.success(t("auth.backupCodesDownloaded")); }; const handleComplete = () => { setIsEnabled(true); setIsSettingUp(false); setSetupStep("init"); setVerificationCode(""); onStatusChange?.(true); }; if (isEnabled && !isSettingUp) { return ( {t("auth.twoFactorTitle")} {t("auth.twoFactorProtected")} {t("common.enabled")} {t("auth.twoFactorActive")} {t("auth.disable2FA")} {t("auth.backupCodes")} {t("common.warning")} {t("auth.disableTwoFactorWarning")}
setPassword(e.target.value)} />

{t("auth.or")}

setDisableCode(e.target.value.replace(/\D/g, "")) } />

{t("auth.generateNewBackupCodesText")}

setPassword(e.target.value)} />

{t("auth.or")}

setDisableCode(e.target.value.replace(/\D/g, "")) } />
{backupCodes.length > 0 && (
{backupCodes.map((code, i) => (
{code}
))}
)}
{error && ( {t("common.error")} {error} )}
); } if (setupStep === "qr") { return ( {t("auth.setupTwoFactorTitle")} {t("auth.step1ScanQR")}
TOTP QR Code

{t("auth.cannotScanQRText")}

); } if (setupStep === "verify") { return ( {t("auth.verifyAuthenticator")} {t("auth.step2EnterCode")}
setVerificationCode(e.target.value.replace(/\D/g, "")) } className="text-center text-2xl tracking-widest font-mono" />
{error && ( {t("common.error")} {error} )}
); } if (setupStep === "backup") { return ( {t("auth.saveBackupCodesTitle")} {t("auth.step3StoreCodesSecurely")} {t("common.important")} {t("auth.importantBackupCodesText")}
{backupCodes.map((code, i) => (
{i + 1}. {code}
))}
); } return ( {t("auth.twoFactorTitle")}

{t("auth.addExtraSecurityLayer")}.

{t("common.notEnabled")} {t("auth.notEnabledText")} {error && ( Error {error} )}
); }