import React, { useState } from "react"; import { Modal, Stack, Text, Button, Group, Stepper, Center, Image, PinInput, Alert, List, CopyButton, ActionIcon, Tooltip, Paper, Code, Loader, Collapse, UnstyledButton, } from "@mantine/core"; import { IconQrcode, IconShieldCheck, IconKey, IconCopy, IconCheck, IconAlertCircle, IconChevronDown, IconChevronRight, IconPrinter, } from "@tabler/icons-react"; import { useForm } from "@mantine/form"; import { useMutation } from "@tanstack/react-query"; import { notifications } from "@mantine/notifications"; import { useTranslation } from "react-i18next"; import { setupMfa, enableMfa } from "@/ee/mfa"; import { zodResolver } from "mantine-form-zod-resolver"; import { z } from "zod"; interface MfaSetupModalProps { opened: boolean; onClose?: () => void; onComplete: () => void; isRequired?: boolean; } interface SetupData { secret: string; qrCode: string; manualKey: string; } const formSchema = z.object({ verificationCode: z .string() .length(6, { message: "Please enter a 6-digit code" }), }); export function MfaSetupModal({ opened, onClose, onComplete, isRequired = false, }: MfaSetupModalProps) { const { t } = useTranslation(); const [active, setActive] = useState(0); const [setupData, setSetupData] = useState(null); const [backupCodes, setBackupCodes] = useState([]); const [manualEntryOpen, setManualEntryOpen] = useState(false); const form = useForm({ validate: zodResolver(formSchema), initialValues: { verificationCode: "", }, }); const setupMutation = useMutation({ mutationFn: () => setupMfa({ method: "totp" }), onSuccess: (data) => { setSetupData(data); }, onError: (error: any) => { notifications.show({ title: t("Error"), message: error.response?.data?.message || t("Failed to setup MFA"), color: "red", }); }, }); // Generate QR code when modal opens React.useEffect(() => { if (opened && !setupData && !setupMutation.isPending) { setupMutation.mutate(); } }, [opened]); const enableMutation = useMutation({ mutationFn: (verificationCode: string) => enableMfa({ secret: setupData!.secret, verificationCode, }), onSuccess: (data) => { setBackupCodes(data.backupCodes); setActive(1); // Move to backup codes step }, onError: (error: any) => { notifications.show({ title: t("Error"), message: error.response?.data?.message || t("Invalid verification code"), color: "red", }); form.setFieldValue("verificationCode", ""); }, }); const handleClose = () => { if (active === 1 && backupCodes.length > 0) { onComplete(); } onClose(); // Reset state setTimeout(() => { setActive(0); setSetupData(null); setBackupCodes([]); setManualEntryOpen(false); form.reset(); }, 200); }; const handleVerify = async (values: { verificationCode: string }) => { await enableMutation.mutateAsync(values.verificationCode); }; const handlePrintBackupCodes = () => { window.print(); }; return ( } >
{setupMutation.isPending ? (
) : setupData ? ( <> {t("1. Scan this QR code with your authenticator app")}
MFA QR Code
setManualEntryOpen(!manualEntryOpen)} > {manualEntryOpen ? ( ) : ( )} {t("Can't scan the code?")} } color="gray" variant="light" > {t( "Enter this code manually in your authenticator app:", )} {setupData.manualKey} {({ copied, copy }) => ( {copied ? ( ) : ( )} )} {t("2. Enter the 6-digit code from your authenticator")} {form.errors.verificationCode && ( {form.errors.verificationCode} )} ) : (
{t("Failed to generate QR code. Please try again.")}
)}
} > } title={t("Save your backup codes")} color="yellow" > {t( "These codes can be used to access your account if you lose access to your authenticator app. Each code can only be used once.", )} {t("Backup codes")} {({ copied, copy }) => ( )} {backupCodes.map((code, index) => ( {code} ))}
); }