fix(admin): remove operation-level email reauthentication
Independent Staging Quality Gate / validate (push) Successful in 12m37s
Independent Staging Quality Gate / publish (push) Successful in 10m0s

This commit is contained in:
Jesse_Chen
2026-08-16 14:07:57 +08:00
parent 9d8570640d
commit 4f6cf5782d
30 changed files with 230 additions and 582 deletions
@@ -100,7 +100,7 @@ export default function AdministratorsResource() {
extra={canManage ? <Button type="primary" icon={<PlusOutlined />} onClick={() => openAssignment()}></Button> : <Typography.Text type="secondary"></Typography.Text>}
>
<Typography.Paragraph type="secondary">
Owner
Owner
</Typography.Paragraph>
<Form {...searchFormProps} layout="inline" style={{ marginBottom: 16 }}>
<Form.Item name="q" label="搜索">
@@ -154,10 +154,9 @@ export default function AdministratorsResource() {
<ReasonActionModal
open={Boolean(pendingAction)}
title={pendingAction?.action === "revoke" ? `撤销角色:${pendingAction.label}` : `分配角色:${pendingAction?.label ?? ""}`}
okText={pendingAction?.action === "revoke" ? "验证并撤销" : "验证并分配"}
okText={pendingAction?.action === "revoke" ? "确认撤销" : "确认分配"}
danger={pendingAction?.action === "revoke"}
confirmLoading={saving}
reauthPermission="admin.users.manage_roles"
onCancel={() => setPendingAction(null)}
onSubmit={submitRoleAction}
/>
@@ -206,9 +206,8 @@ export function SubscriptionsResource() {
<ReasonActionModal
open={Boolean(selected && extendDays !== null)}
title="延长订阅权益"
okText="验证并延长"
okText="确认延长"
confirmLoading={saving}
reauthPermission="billing.adjustments.write"
onCancel={() => setExtendDays(null)}
onSubmit={extend}
/>
@@ -218,7 +217,6 @@ export function SubscriptionsResource() {
okText="确认撤销"
danger
confirmLoading={Boolean(revokingId)}
reauthPermission="billing.adjustments.write"
onCancel={() => setRevokeTarget(null)}
onSubmit={(reason) => revoke(revokeTarget!, reason)}
/>
@@ -430,7 +428,6 @@ export function OrdersResource() {
okText="确认执行"
danger={target?.action === "record_refund"}
confirmLoading={saving}
reauthPermission="billing.adjustments.write"
onCancel={() => setTarget(null)}
onSubmit={adjust}
/>
@@ -319,18 +319,16 @@ export default function CodesPage() {
<ReasonActionModal
open={Boolean(pendingCreate)}
title="生成兑换码"
okText="验证并生成"
okText="确认生成"
confirmLoading={createMutation.isPending}
reauthPermission="billing.adjustments.write"
onCancel={() => setPendingCreate(null)}
onSubmit={submitCreate}
/>
<ReasonActionModal
open={Boolean(pendingEdit)}
title="编辑未兑换码"
okText="验证并保存"
okText="确认保存"
confirmLoading={updateMutation.isPending}
reauthPermission="billing.adjustments.write"
onCancel={() => setPendingEdit(null)}
onSubmit={submitEdit}
/>
@@ -340,7 +338,6 @@ export default function CodesPage() {
okText="确认撤销"
danger
confirmLoading={revoking}
reauthPermission="billing.adjustments.write"
onCancel={() => setRevokeRecord(null)}
onSubmit={(reason) => revoke(revokeRecord!, reason)}
/>
@@ -162,7 +162,6 @@ export default function FeatureFlagsManagement() {
title={`发布功能开关${publishTarget ? `${publishTarget.flagKey}` : ""}`}
okText="确认发布"
confirmLoading={Boolean(publishingId)}
reauthPermission="ops.flags.write"
onCancel={() => setPublishTarget(null)}
onSubmit={(reason) => publish(publishTarget!, reason)}
/>
+6 -12
View File
@@ -22,7 +22,6 @@ type MfaStatus = {
required: boolean;
enrolled: boolean;
verified: boolean;
highRiskWritesEnabled: boolean;
expiresIn?: number;
};
@@ -104,7 +103,7 @@ export default function MfaSecurity() {
if (!data) return;
setStatus(data);
setEnrollment(undefined);
setNotice("当前管理员会话已完成真实第二因素验证。高风险操作仍需随后完成权限范围内的邮箱验证。");
setNotice("当前管理员会话已完成 MFA 验证。");
}
async function recover({ code }: { code: string }) {
@@ -131,8 +130,8 @@ export default function MfaSecurity() {
setEnrollment(undefined);
setBackupCodes(undefined);
setNotice(data.required
? "MFA 已禁用。角色要求 MFA因此高风险写入现已关闭,重新绑定后才能恢复。"
: "MFA 已禁用,当前 MFA 与邮箱重认证证明已撤销。");
? "MFA 已禁用。当前角色要求启用 MFA可随时重新绑定。"
: "MFA 已禁用,当前会话的 MFA 证明已撤销。");
}
if (loading && !status) {
@@ -153,7 +152,7 @@ export default function MfaSecurity() {
type="warning"
showIcon
message="当前角色强制要求 MFA"
description="在 MFA 绑定完成前,所有高风险写入均默认拒绝。"
description="这是账户级安全要求,不会再触发具体管理操作的邮箱验证码复核。"
/> : null}
<Card title="当前状态">
@@ -167,11 +166,6 @@ export default function MfaSecurity() {
<Descriptions.Item label="当前会话">
<Text type={status?.verified ? undefined : "secondary"}>{status?.verified ? "已验证" : "未验证"}</Text>
</Descriptions.Item>
<Descriptions.Item label="高风险写入前置能力">
<Text type={status?.highRiskWritesEnabled ? undefined : "danger"}>
{status?.highRiskWritesEnabled ? "可发起邮箱重认证" : "已关闭"}
</Text>
</Descriptions.Item>
</Descriptions>
</Card>
@@ -208,7 +202,7 @@ export default function MfaSecurity() {
</Card> : null}
{status?.enrolled && !status.verified ? <Card title="验证当前会话">
<Paragraph></Paragraph>
<Paragraph>使</Paragraph>
<Radio.Group value={factor} onChange={(event) => setFactor(event.target.value as Factor)}>
<Radio.Button value="totp"></Radio.Button>
<Radio.Button value="backup"></Radio.Button>
@@ -254,7 +248,7 @@ export default function MfaSecurity() {
<Divider />
<div>
<Title level={4}> MFA</Title>
<Paragraph type="secondary"> Better Auth MFA </Paragraph>
<Paragraph type="secondary"> Better Auth MFA </Paragraph>
<Form layout="inline" onFinish={disable}>
<Form.Item name="password" rules={[{ required: true, message: "请输入当前密码" }]}>
<Input.Password placeholder="当前密码" autoComplete="current-password" maxLength={128} />
@@ -280,9 +280,8 @@ export default function PaymentManagement() {
<ReasonActionModal
open={Boolean(pendingEpaySettings)}
title="保存易支付设置"
okText="验证并保存"
okText="确认保存"
confirmLoading={epaySaving}
reauthPermission="billing.adjustments.write"
onCancel={() => setPendingEpaySettings(null)}
onSubmit={saveEpaySettings}
/>
@@ -280,9 +280,8 @@ export default function ProductManagement() {
<ReasonActionModal
open={Boolean(pendingSave)}
title="保存商品草稿"
okText="验证并保存"
okText="确认保存"
confirmLoading={saving}
reauthPermission="billing.products.write"
onCancel={() => setPendingSave(null)}
onSubmit={save}
/>
@@ -291,7 +290,6 @@ export default function ProductManagement() {
title={`发布商品${publishTarget ? `${publishTarget.name}` : ""}`}
okText="确认发布"
confirmLoading={Boolean(publishingId)}
reauthPermission="billing.products.publish"
onCancel={() => setPublishTarget(null)}
onSubmit={(reason) => publish(publishTarget!, reason)}
/>
@@ -1,136 +1,76 @@
"use client";
import { Alert, Button, Form, Input, Modal, Space } from "antd";
import { Alert, Form, Input, Modal } from "antd";
import { useState } from "react";
import type { AdminPermission } from "@/lib/admin/auth-policy";
type ReasonActionModalProps = {
open: boolean;
title: string;
okText: string;
confirmLoading?: boolean;
danger?: boolean;
reauthPermission?: AdminPermission;
confirmLoading?: boolean;
onCancel: () => void;
onSubmit: (reason: string) => void | Promise<void>;
onSubmit: (reason: string) => Promise<void> | void;
};
type FormValues = { reason: string; otp?: string };
async function adminSecurityRequest<T>(url: string, body: object): Promise<T> {
const response = await fetch(url, {
method: "POST",
cache: "no-store",
credentials: "same-origin",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
const value = await response.json().catch(() => null) as {
data?: T;
error?: unknown;
} | null;
if (response.ok && value?.data) return value.data;
throw new Error(
typeof value?.error === "string" ? value.error : "安全验证失败,请稍后再试",
);
}
type ReasonFormValues = {
reason: string;
};
export function ReasonActionModal({
open,
title,
okText,
confirmLoading = false,
danger = false,
reauthPermission,
confirmLoading = false,
onCancel,
onSubmit,
}: ReasonActionModalProps) {
const [form] = Form.useForm<FormValues>();
const [otpSent, setOtpSent] = useState(false);
const [reauthLoading, setReauthLoading] = useState(false);
const [reauthError, setReauthError] = useState<string>();
const [form] = Form.useForm<ReasonFormValues>();
const [actionLoading, setActionLoading] = useState(false);
const [actionError, setActionError] = useState<string>();
async function requestOtp() {
if (!reauthPermission) return;
setReauthLoading(true);
setReauthError(undefined);
async function submit(values: ReasonFormValues) {
setActionError(undefined);
setActionLoading(true);
try {
await adminSecurityRequest("/api/admin/reauth", {
action: "request",
permission: reauthPermission,
});
setOtpSent(true);
await onSubmit(values.reason.trim());
} catch (error) {
setReauthError(error instanceof Error ? error.message : "验证码发送失败");
setActionError(error instanceof Error ? error.message : "操作失败,请稍后再试");
} finally {
setReauthLoading(false);
setActionLoading(false);
}
}
async function submit({ reason, otp }: FormValues) {
setReauthError(undefined);
setReauthLoading(true);
try {
if (reauthPermission) {
await adminSecurityRequest("/api/admin/reauth", {
action: "verify",
permission: reauthPermission,
otp,
});
}
await onSubmit(reason.trim());
} catch (error) {
setReauthError(error instanceof Error ? error.message : "操作失败,请稍后再试");
} finally {
setReauthLoading(false);
}
}
return <Modal
title={title}
open={open}
okText={okText}
cancelText="取消"
confirmLoading={confirmLoading || reauthLoading}
okButtonProps={{
danger,
disabled: Boolean(reauthPermission && !otpSent),
}}
onOk={() => form.submit()}
onCancel={onCancel}
afterOpenChange={(visible) => {
if (visible) form.resetFields();
setOtpSent(false);
setReauthError(undefined);
}}
destroyOnHidden
>
<Form form={form} layout="vertical" onFinish={submit}>
<Form.Item name="reason" label="操作原因" rules={[{ required: true, whitespace: true }, { max: 500 }]}>
<Input.TextArea rows={3} showCount maxLength={500} autoFocus />
</Form.Item>
{reauthPermission ? <Form.Item
label="邮箱验证码"
extra="验证码发送到当前登录管理员邮箱,5 分钟内有效,且只授权本次操作权限。"
required
>
<Space.Compact style={{ width: "100%" }}>
<Form.Item
name="otp"
noStyle
rules={[{ required: true }, { pattern: /^\d{6}$/, message: "请输入 6 位验证码" }]}
>
<Input inputMode="numeric" autoComplete="one-time-code" maxLength={6} placeholder="请输入 6 位验证码" />
</Form.Item>
<Button type="default" loading={reauthLoading} onClick={() => void requestOtp()}>
{otpSent ? "重新发送" : "发送验证码"}
</Button>
</Space.Compact>
</Form.Item> : null}
{reauthError ? <Alert type="error" showIcon message={reauthError} /> : null}
</Form>
</Modal>;
return (
<Modal
open={open}
title={title}
okText={okText}
cancelText="取消"
okButtonProps={{ danger }}
confirmLoading={confirmLoading || actionLoading}
onCancel={onCancel}
onOk={() => form.submit()}
afterOpenChange={(visible) => {
if (visible) form.resetFields();
setActionError(undefined);
}}
destroyOnHidden
>
<Form form={form} layout="vertical" preserve={false} onFinish={submit}>
<Form.Item
label="操作原因"
name="reason"
rules={[
{ required: true, whitespace: true, message: "请输入操作原因" },
{ max: 500, message: "操作原因最多 500 字" },
]}
>
<Input.TextArea rows={3} maxLength={500} showCount autoFocus />
</Form.Item>
{actionError ? <Alert type="error" showIcon message={actionError} /> : null}
</Form>
</Modal>
);
}