"use client"; import { useState, useEffect } from "react"; import QRCode from "qrcode"; import { trpc } from "~/lib/trpc/client.js"; type SetupStep = "idle" | "show-secret" | "verify" | "show-backup-codes" | "done"; export function MfaSetup() { const [step, setStep] = useState("idle"); const [secret, setSecret] = useState(""); const [uri, setUri] = useState(""); const [qrDataUrl, setQrDataUrl] = useState(""); const [token, setToken] = useState(""); const [backupCodes, setBackupCodes] = useState(null); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); useEffect(() => { if (!uri) return; let cancelled = false; QRCode.toDataURL(uri, { width: 200, margin: 2 }) .then((dataUrl) => { if (!cancelled) setQrDataUrl(dataUrl); }) .catch(() => { /* ignore — manual key is shown as fallback */ }); return () => { cancelled = true; }; }, [uri]); const { data: mfaStatus, refetch } = trpc.user.getMfaStatus.useQuery(); const generateMutation = trpc.user.generateTotpSecret.useMutation(); const verifyMutation = trpc.user.verifyAndEnableTotp.useMutation(); const regenerateBackupCodesMutation = trpc.user.regenerateBackupCodes.useMutation(); async function handleGenerate() { setError(null); try { const result = await generateMutation.mutateAsync(); setSecret(result.secret); setUri(result.uri); setStep("show-secret"); } catch (err) { setError(err instanceof Error ? err.message : "Failed to generate TOTP secret"); } } async function handleVerify() { setError(null); try { const result = await verifyMutation.mutateAsync({ token }); setBackupCodes(result.backupCodes ?? null); setStep("show-backup-codes"); setSecret(""); setUri(""); setToken(""); await refetch(); } catch (err) { setError(err instanceof Error ? err.message : "Verification failed"); } } async function handleRegenerateBackupCodes() { setError(null); try { const result = await regenerateBackupCodesMutation.mutateAsync(); setBackupCodes(result.codes); setStep("show-backup-codes"); await refetch(); } catch (err) { setError(err instanceof Error ? err.message : "Could not regenerate backup codes"); } } function handleFinishBackupCodes() { setBackupCodes(null); setStep("done"); setSuccess("MFA is active. Keep your backup codes in a safe place."); } function copyBackupCodes() { if (!backupCodes) return; void navigator.clipboard.writeText(backupCodes.join("\n")); } function downloadBackupCodes() { if (!backupCodes) return; const blob = new Blob( [ `Nexus MFA Backup Codes\nGenerated: ${new Date().toISOString()}\n\nEach code works exactly once. Keep this file somewhere safe.\n\n${backupCodes.join("\n")}\n`, ], { type: "text/plain" }, ); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "nexus-backup-codes.txt"; a.click(); URL.revokeObjectURL(url); } if (mfaStatus?.totpEnabled && step !== "done" && step !== "show-backup-codes") { const remaining = mfaStatus.backupCodesRemaining ?? 0; const lowCodes = remaining <= 3; return (

MFA Enabled

Two-factor authentication is active on your account.

Backup codes

{remaining === 0 ? "You have no backup codes left. Generate a new set to avoid being locked out if you lose your device." : `You have ${remaining} backup code${remaining === 1 ? "" : "s"} remaining.`}{" "} {lowCodes && remaining > 0 && Regenerate soon.}

{error && (
{error}
)}
); } return (
{error && (
{error}
)} {success && (
{success}
)} {step === "idle" && (

Two-Factor Authentication (TOTP)

Add an extra layer of security by requiring a code from your authenticator app when signing in.

)} {step === "show-secret" && (

Step 1: Scan the QR code

Scan this QR code with your authenticator app (Google Authenticator, Authy, 1Password, etc.).

{/* QR Code — rendered locally, no external service */}
{qrDataUrl ? ( TOTP QR Code ) : (
Generating…
)}

Or enter this key manually:

{secret}
)} {step === "verify" && (

Step 2: Verify your code

Enter the 6-digit code from your authenticator app to confirm setup.

setToken(e.target.value.replace(/\D/g, "").slice(0, 6))} className="w-48 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-4 py-2 text-center text-xl font-mono tracking-[0.3em] text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-brand-400" placeholder="000000" autoFocus />
)} {step === "show-backup-codes" && backupCodes && (

Save your backup codes

Each code works exactly once. Store them in a password manager or print them. You will not see them again — regenerating invalidates the whole set.

{backupCodes.map((code) => ( {code} ))}
)}
); }