import type { DialogProps } from "../store"; import { t } from "@lingui/core/macro"; import { Trans } from "@lingui/react/macro"; import { ArrowDownIcon, CopyIcon, EyeIcon, EyeSlashIcon } from "@phosphor-icons/react"; import { useStore } from "@tanstack/react-form"; import { useRouter } from "@tanstack/react-router"; import { QRCodeSVG } from "qrcode.react"; import { useState } from "react"; import { match } from "ts-pattern"; import { useToggle } from "usehooks-ts"; import z from "zod"; import { Button } from "@reactive-resume/ui/components/button"; import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@reactive-resume/ui/components/dialog"; import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form"; import { Input } from "@reactive-resume/ui/components/input"; import { toast } from "@reactive-resume/ui/components/toast"; import { useFormBlocker } from "@/hooks/use-form-blocker"; import { authClient } from "@/libs/auth/client"; import { getReadableErrorMessage } from "@/libs/error-message"; import { useAppForm } from "@/libs/tanstack-form"; import { useDialogStore } from "../store"; const enableFormSchema = z.object({ password: z.string().min(6).max(64), }); const verifyFormSchema = z.object({ code: z.string().length(6, "Code must be 6 digits"), }); type TwoFactorSetupStep = "backup" | "enable" | "verify"; type TwoFactorStepProps = { step: TwoFactorSetupStep; }; type TwoFactorQRCodeProps = { totpUri: string; }; export function EnableTwoFactorDialog(_: DialogProps<"auth.two-factor.enable">) { const router = useRouter(); const [totpUri, setTotpUri] = useState(null); const [backupCodes, setBackupCodes] = useState(null); const [step, setStep] = useState("enable"); const [showPassword, toggleShowPassword] = useToggle(false); const closeDialog = useDialogStore((state) => state.closeDialog); const enableForm = useAppForm({ defaultValues: { password: "" }, validators: { onSubmit: enableFormSchema }, onSubmit: async ({ value }) => { const toastId = toast.add({ type: "loading", description: t`Enabling two-factor authentication…` }); const { data, error } = await authClient.twoFactor.enable({ password: value.password, issuer: "Reactive Resume", }); if (error) { toast.add({ type: "error", description: getReadableErrorMessage( error, t({ comment: "Fallback toast when enabling two-factor authentication fails", message: "Failed to enable two-factor authentication. Please try again.", }), ), id: toastId, }); return; } if (data.totpURI && data.backupCodes) { setTotpUri(data.totpURI); setBackupCodes(data.backupCodes); setStep("verify"); toast.close(toastId); } else { toast.add({ type: "error", description: t`Failed to setup two-factor authentication.`, id: toastId }); } }, }); const verifyForm = useAppForm({ defaultValues: { code: "" }, validators: { onSubmit: verifyFormSchema }, onSubmit: async ({ value }) => { const toastId = toast.add({ type: "loading", description: t`Verifying code…` }); const { error } = await authClient.twoFactor.verifyTotp({ code: value.code }); if (error) { toast.add({ type: "error", description: getReadableErrorMessage( error, t({ comment: "Fallback toast when verifying two-factor setup code fails", message: "Failed to verify your code. Please try again.", }), ), id: toastId, }); return; } toast.close(toastId); setStep("backup"); }, }); const enableIsDirty = useStore(enableForm.store, (s) => s.isDirty); const enableIsSubmitting = useStore(enableForm.store, (s) => s.isSubmitting); const verifyIsDirty = useStore(verifyForm.store, (s) => s.isDirty); const verifyIsSubmitting = useStore(verifyForm.store, (s) => s.isSubmitting); const { requestClose } = useFormBlocker(enableForm, { shouldBlock: () => { if (step === "enable") return enableIsDirty && !enableIsSubmitting; if (step === "verify") return verifyIsDirty && !verifyIsSubmitting; return false; }, }); const onConfirmBackup = () => { toast.add({ type: "success", description: t`Two-factor authentication has been setup successfully.` }); void router.invalidate(); closeDialog(); onReset(); }; const onReset = () => { enableForm.reset(); verifyForm.reset(); setStep("enable"); setTotpUri(null); setBackupCodes(null); }; const handleCopySecret = async () => { if (!totpUri) return; const secret = extractSecretFromTotpUri(totpUri); if (!secret) return; await navigator.clipboard.writeText(secret); toast.add({ type: "success", description: t`Secret copied to clipboard.` }); }; const handleCopyBackupCodes = async () => { if (!backupCodes) return; await navigator.clipboard.writeText(backupCodes.join("\n")); toast.add({ type: "success", description: t`Backup codes copied to clipboard.` }); }; const handleDownloadBackupCodes = () => { if (!backupCodes) return; const content = backupCodes.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 = "reactive-resume_backup-codes.txt"; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; return ( {match(step) .with("enable", () => (
{ event.preventDefault(); event.stopPropagation(); void enableForm.handleSubmit(); }} > {(field) => ( 0}> Password
field.handleChange(event.target.value)} /> } />
)}
)) .with("verify", () => { const secret = totpUri ? extractSecretFromTotpUri(totpUri) : null; return (
{totpUri && secret && ( <>
)}

Then, enter the 6 digit code that the app provides to continue.

{ event.preventDefault(); event.stopPropagation(); void verifyForm.handleSubmit(); }} > {(field) => ( 0}> field.handleChange(event.target.value)} /> } /> )}
); }) .with("backup", () => (
{backupCodes && (
{backupCodes.map((code) => (
{code}
))}
)}
)) .exhaustive()}
); } function extractSecretFromTotpUri(totpUri: string): string | null { try { const url = new URL(totpUri); return url.searchParams.get("secret"); } catch { return null; } } function TwoFactorDialogTitle({ step }: TwoFactorStepProps) { return match(step) .with("enable", () => Enable Two-Factor Authentication) .with("verify", () => Setup Authenticator App) .with("backup", () => Copy Backup Codes) .exhaustive(); } function TwoFactorDialogDescription({ step }: TwoFactorStepProps) { return match(step) .with("enable", () => ( Enter your password to confirm setting up two-factor authentication. When enabled, you'll need to enter a code from your authenticator app every time you log in. )) .with("verify", () => ( Scan the QR code below with your preferred authenticator app. You can also copy the secret below and paste it into your app. )) .with("backup", () => Copy and store these backup codes in case you lose your device.) .exhaustive(); } function TwoFactorQRCode({ totpUri }: TwoFactorQRCodeProps) { return ( ); }