From 139baeb94c7e9dbf60c894f600f83176894d7b27 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Tue, 11 Aug 2026 21:09:30 +0800 Subject: [PATCH] fix(admin): remove email reauth from account reset --- docs/BUG_HISTORY.md | 15 +++++++++++++++ .../src/app/api/admin/customers/reset/route.ts | 4 ++-- frontend/src/components/admin/users-resource.tsx | 1 - .../tests/admin-account-reset-contract.test.ts | 9 ++++++--- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 0eaea1af..5c823634 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -2869,3 +2869,18 @@ - 防复发:任何测试创建本地数据客户端必须在 `fixture.stop()` 之前按自身 service URL 关闭连接池;测试不得用 `identity_runtime` 向 `public.*` 播种 fixture 行(一律走 postgres admin 的 `fixture.psql`);禁止为测试放宽运行时 grant 或改迁移。 - 相关记录:BUG-163、BUG-164、BUG-165 - 修复版本:本地 staging 候选(未 push / deploy) + +## BUG-167 | 管理端账号重置重复要求邮箱验证码 + +- 状态:resolved(本地验证通过,待 staging 部署验收) +- 首次发现:2026-08-11 +- 最近更新:2026-08-11 +- 影响面:管理端用户列表“重置资料与会话”弹窗与 `POST /api/admin/customers/reset` +- 用户现象:已登录且具备账号管理权限的管理员执行账号重置时,仍需额外发送并输入邮箱验证码,增加不必要的操作步骤。 +- 触发条件:在用户列表打开重置弹窗并提交账号重置。 +- 根因:账号重置复用了面向角色变更、账务调整等操作的 `requireHighRiskAdminMutation` 与 `reauthPermission`,把一次性邮箱复核错误扩展到了已有登录、权限、原因、可信来源及数据库审计保护的重置路径。 +- 修复:该路由改用既有 `requireAdminMutation(request, "admin.users.manage_roles")`,前端仅移除该弹窗的 `reauthPermission`;保留管理员会话、权限、可信 Origin、确认字面量、操作原因、数据库权限检查与审计日志。其他高风险管理员操作继续使用邮箱复核。 +- 验证:账号重置合同测试确认前后端均不再要求邮箱复核,同时管理员角色变更仍使用 `requireHighRiskAdminMutation`;管理端 Origin 与授权边界的既有测试继续覆盖未登录 `401`、缺少权限/不可信来源 `403`。 +- 防复发:账号重置只允许使用普通管理员 mutation guard;共享高风险 guard 不做全局放松,并由合同测试锁定角色变更仍需邮箱复核。 +- 相关记录:无 +- 修复版本:本次 staging 候选 diff --git a/frontend/src/app/api/admin/customers/reset/route.ts b/frontend/src/app/api/admin/customers/reset/route.ts index da448706..23af1a06 100644 --- a/frontend/src/app/api/admin/customers/reset/route.ts +++ b/frontend/src/app/api/admin/customers/reset/route.ts @@ -6,7 +6,7 @@ import { adminErrorResponse, invalidQueryResponse, requestId, - requireHighRiskAdminMutation, + requireAdminMutation, } from "@/lib/admin/http"; export const runtime = "nodejs"; @@ -28,7 +28,7 @@ type ResetRow = { export async function POST(request: Request) { try { - const session = await requireHighRiskAdminMutation( + const session = await requireAdminMutation( request, "admin.users.manage_roles", ); diff --git a/frontend/src/components/admin/users-resource.tsx b/frontend/src/components/admin/users-resource.tsx index e82b262b..5ca5ab46 100644 --- a/frontend/src/components/admin/users-resource.tsx +++ b/frontend/src/components/admin/users-resource.tsx @@ -133,7 +133,6 @@ export default function UsersPage() { okText="确认重置" danger confirmLoading={resetting} - reauthPermission="admin.users.manage_roles" onCancel={() => setResetTarget(null)} onSubmit={resetAccount} /> diff --git a/frontend/tests/admin-account-reset-contract.test.ts b/frontend/tests/admin-account-reset-contract.test.ts index 74c965a2..03436afe 100644 --- a/frontend/tests/admin-account-reset-contract.test.ts +++ b/frontend/tests/admin-account-reset-contract.test.ts @@ -6,14 +6,17 @@ const root = new URL("../", import.meta.url); const route = readFileSync(new URL("src/app/api/admin/customers/reset/route.ts", root), "utf8"); const users = readFileSync(new URL("src/components/admin/users-resource.tsx", root), "utf8"); const migration = readFileSync(new URL("supabase/migrations/20260811020000_admin_customer_account_reset.sql", root), "utf8"); +const administratorsRoute = readFileSync(new URL("src/app/api/admin/administrators/route.ts", root), "utf8"); -test("admin account reset stays owner-only, explicit, and high risk", () => { - assert.match(route, /requireHighRiskAdminMutation\(\s*request,\s*"admin\.users\.manage_roles",?\s*\)/); +test("admin account reset stays owner-only and explicit without email reauth", () => { + assert.match(route, /requireAdminMutation\(\s*request,\s*"admin\.users\.manage_roles",?\s*\)/); + assert.doesNotMatch(route, /requireHighRiskAdminMutation/); assert.match(route, /confirmation:\s*z\.literal\("RESET"\)/); assert.match(route, /admin_reset_customer_account\(\$1, \$2, \$3, \$4\)/); assert.match(users, /permissions\.includes\("admin\.users\.manage_roles"\)/); - assert.match(users, /reauthPermission="admin\.users\.manage_roles"/); + assert.doesNotMatch(users, /reauthPermission=/); assert.match(users, /登录身份、管理员角色、积分及账务审计记录会保留/); + assert.match(administratorsRoute, /requireHighRiskAdminMutation\(\s*request,\s*"admin\.users\.manage_roles",?\s*\)/); }); test("database reset mirrors the existing staging reset boundary and audits it", () => {