//? Two-factor authentication management (ADR 0024). Talks to the FRAMEWORK //? routes /auth/api/2fa/setup|enable|disable|recovery-codes — adapter-based, //? so they work on every data layer. These are authenticated, state-changing //? POSTs: in cookie mode the CSRF double-submit header is required, hence the //? getCsrfToken() wiring below. //? //? Enrollment shows the base32 secret + otpauth:// URI for the authenticator //? app (Google/Microsoft Authenticator, Authy, … — manual entry works in all //? of them; render the URI as a QR code if you add a QR library). import { useState } from "react"; import { backendUrl, sessionBasedToken } from "config"; import tryCatch from "shared/tryCatch"; import { getCsrfToken, i18nNotify as notify, useSession, useTranslator } from "@luckystack/core/client"; type View = "status" | "enroll" | "confirm" | "recovery" | "disable" | "regenerate"; interface TwoFactorResponse { status: boolean | string; reason?: string; errorCode?: string; secret?: string; otpauthUri?: string; recoveryCodes?: string[]; } const fail = (response: TwoFactorResponse): void => { const key = [response.reason, response.errorCode].find((k): k is string => typeof k === "string" && k.length > 0) ?? "api.internalServerError"; notify.error({ key }); }; const copyText = async (value: string): Promise => { const [copyError] = await tryCatch(() => navigator.clipboard.writeText(value)); if (!copyError) notify.success({ key: "settings.twoFactorCopied" }); }; const postTwoFactor = async (path: string, body: Record): Promise => { const headers: Record = { "Content-Type": "application/json" }; //? Cookie mode: state-changing authed POSTs pass the CSRF double-submit check. const [, csrf] = await tryCatch(() => Promise.resolve(getCsrfToken())); if (typeof csrf === "string" && csrf) headers["x-csrf-token"] = csrf; //? Token mode: the session travels as a Bearer header instead of a cookie. if (sessionBasedToken) { const existingToken = sessionStorage.getItem("token"); if (existingToken) headers.Authorization = `Bearer ${existingToken}`; } const [error, response] = await tryCatch(async () => { const res = await fetch(`${backendUrl}${path}`, { method: "POST", headers, body: JSON.stringify(body), credentials: "include", }); return (await res.json()) as TwoFactorResponse; }); if (error || !response) { notify.error({ key: "common.404" }); return null; } return response; }; export default function TwoFactorSection() { const translate = useTranslator(); const { session } = useSession(); //? The session copy can be stale right after enable/disable (it is refreshed //? on the next login) — track the live status locally after each action. const sessionEnabled = Boolean((session as { twoFactorEnabled?: boolean } | null)?.twoFactorEnabled); const [enabledOverride, setEnabledOverride] = useState(null); const enabled = enabledOverride ?? sessionEnabled; const [view, setView] = useState("status"); const [loading, setLoading] = useState(false); const [secret, setSecret] = useState(""); const [otpauthUri, setOtpauthUri] = useState(""); const [code, setCode] = useState(""); const [recoveryCodes, setRecoveryCodes] = useState([]); const inputClass = "rounded-md w-full h-9 border border-container1-border bg-container1 px-3 text-sm focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/30 transition-colors"; const primaryButtonClass = "h-9 px-4 rounded-md bg-primary text-title-primary text-sm font-medium hover:bg-primary-hover transition-colors cursor-pointer disabled:opacity-60"; const subtleButtonClass = "h-9 px-4 rounded-md bg-container2 text-title text-sm border border-container2-border hover:bg-container2-hover transition-colors cursor-pointer disabled:opacity-60"; const handleSetup = async (): Promise => { if (loading) return; setLoading(true); const response = await postTwoFactor("/auth/api/2fa/setup", {}); setLoading(false); if (!response) return; if (response.status !== true || !response.secret || !response.otpauthUri) { fail(response); return; } setSecret(response.secret); setOtpauthUri(response.otpauthUri); setCode(""); setView("confirm"); }; const handleEnable = async (): Promise => { if (loading || !code.trim()) return; setLoading(true); const response = await postTwoFactor("/auth/api/2fa/enable", { code }); setLoading(false); if (!response) return; if (response.status !== true || !response.recoveryCodes) { fail(response); return; } setRecoveryCodes(response.recoveryCodes); setEnabledOverride(true); setCode(""); setView("recovery"); notify.success({ key: "settings.twoFactorEnabled" }); }; const handleDisable = async (): Promise => { if (loading || !code.trim()) return; setLoading(true); const response = await postTwoFactor("/auth/api/2fa/disable", { code }); setLoading(false); if (!response) return; if (response.status !== true) { fail(response); return; } setEnabledOverride(false); setCode(""); setView("status"); notify.success({ key: "settings.twoFactorDisabled" }); }; const handleRegenerate = async (): Promise => { if (loading || !code.trim()) return; setLoading(true); const response = await postTwoFactor("/auth/api/2fa/recovery-codes", { code }); setLoading(false); if (!response) return; if (response.status !== true || !response.recoveryCodes) { fail(response); return; } setRecoveryCodes(response.recoveryCodes); setCode(""); setView("recovery"); }; return (
{view === "status" && ( <>

{enabled ? translate({ key: "settings.twoFactorStatusOn" }) : translate({ key: "settings.twoFactorStatusOff" })}

{enabled ? ( <> ) : ( )}
)} {view === "confirm" && ( <>

{translate({ key: "settings.twoFactorScanIntro" })}

{translate({ key: "settings.twoFactorSecretLabel" })}
{ setCode(e.target.value); }} className={inputClass} />
)} {view === "recovery" && ( <>

{translate({ key: "settings.twoFactorRecoveryIntro" })}

{recoveryCodes.map((recoveryCode) => ( {recoveryCode} ))}
)} {(view === "disable" || view === "regenerate") && ( <>

{view === "disable" ? translate({ key: "settings.twoFactorDisableIntro" }) : translate({ key: "settings.twoFactorRegenerateIntro" })}

{ setCode(e.target.value); }} className={inputClass} />
)}
); }