"use client"; import { Alert, Button, Card, Descriptions, Divider, Form, Input, List, Radio, Space, Spin, Tag, Typography, } from "antd"; import { useEffect, useState } from "react"; const { Paragraph, Text, Title } = Typography; type MfaStatus = { required: boolean; enrolled: boolean; verified: boolean; highRiskWritesEnabled: boolean; expiresIn?: number; }; type Enrollment = { totpUri: string; backupCodes: string[]; verificationRequired: true; }; type Factor = "totp" | "backup"; async function mfaRequest(body?: object): Promise { const response = await fetch("/api/admin/mfa", { method: body ? "POST" : "GET", cache: "no-store", credentials: "same-origin", headers: body ? { "content-type": "application/json" } : undefined, body: body ? JSON.stringify(body) : undefined, }); const payload = await response.json().catch(() => null) as { data?: T; error?: unknown } | null; if (!response.ok || !payload?.data) { throw new Error(typeof payload?.error === "string" ? payload.error : "MFA 请求失败,请稍后再试"); } return payload.data; } export default function MfaSecurity() { const [status, setStatus] = useState(); const [enrollment, setEnrollment] = useState(); const [backupCodes, setBackupCodes] = useState(); const [factor, setFactor] = useState("totp"); const [loading, setLoading] = useState(true); const [action, setAction] = useState(); const [error, setError] = useState(); const [notice, setNotice] = useState(); useEffect(() => { let cancelled = false; void mfaRequest() .then((value) => { if (!cancelled) setStatus(value); }) .catch((cause: unknown) => { if (!cancelled) { setError(cause instanceof Error ? cause.message : "无法读取 MFA 状态"); } }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, []); async function run(name: string, operation: () => Promise): Promise { setAction(name); setError(undefined); setNotice(undefined); try { return await operation(); } catch (cause) { setError(cause instanceof Error ? cause.message : "操作失败,请稍后再试"); } finally { setAction(undefined); } } async function enroll({ password }: { password: string }) { const data = await run("enroll", () => mfaRequest({ action: "enroll", password })); if (!data) return; setEnrollment(data); setBackupCodes(data.backupCodes); setNotice("请先保存恢复码,再使用认证器生成的 6 位验证码完成 enrollment。未验证前 MFA 不会启用。"); } async function verify({ code }: { code: string }) { const data = await run("verify", () => mfaRequest({ action: "verify", code })); if (!data) return; setStatus(data); setEnrollment(undefined); setNotice("当前管理员 session 已完成真实第二因素验证。高风险操作仍需随后完成权限范围内的邮箱验证码。"); } async function recover({ code }: { code: string }) { const data = await run("recover", () => mfaRequest({ action: "recover", code })); if (!data) return; setStatus(data); setNotice("恢复码已消费,当前 session 已完成 MFA 验证。请在恢复访问后重新生成恢复码。"); } async function regenerate({ password }: { password: string }) { const data = await run("regenerate", () => mfaRequest<{ backupCodes: string[] }>({ action: "regenerate", password, })); if (!data) return; setBackupCodes(data.backupCodes); setNotice("新的恢复码已生成,旧恢复码已全部失效。请立即离线保存。"); } async function disable({ password }: { password: string }) { const data = await run("disable", () => mfaRequest({ action: "disable", password })); if (!data) return; setStatus(data); setEnrollment(undefined); setBackupCodes(undefined); setNotice(data.required ? "MFA 已禁用。该角色要求 MFA,因此高风险写入现已关闭,重新 enrollment 后才能恢复。" : "MFA 已禁用,当前 MFA 与邮箱重认证证明均已撤销。"); } if (loading && !status) { return 正在读取 MFA 状态; } return
安全验证 管理员 MFA 使用 Better Auth 的真实 TOTP 与一次性恢复码。MFA 证明只绑定当前 server session,短时有效且可随 session 撤销。
{error ? : null} {notice ? : null} {status?.required && !status.enrolled ? : null} {status?.required ? "必须 MFA" : "可选 MFA"} {status?.enrolled ? "已启用" : "未启用"} {status?.verified ? "已验证" : "未验证"} {status?.highRiskWritesEnabled ? "可发起邮箱重认证" : "已关闭"} {!status?.enrolled && !enrollment ? 输入当前账户密码后生成认证器 URI 与一次性恢复码。服务器只保存加密后的 seed 与恢复码。
: null} {enrollment ? 认证器 URI {enrollment.totpUri} 一次性恢复码
: null} {status?.enrolled && !status.verified ? 高权限操作前,先用认证器验证码或一次性恢复码完成真实第二因素。 setFactor(event.target.value as Factor)}> 认证器验证码 恢复码
: null} {status?.enrolled && status.verified ?
重新生成恢复码 生成后旧恢复码立即失效,明文只在本次响应显示。
{backupCodes?.length ? : null}
禁用 MFA 禁用会轮换 Better Auth session,并撤销当前 MFA 与邮箱重认证证明。
: null}
; } function BackupCodeList({ codes }: { codes: string[] }) { return {code}} />; }