fix(admin): remove manual reasons and repair code creation
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled

This commit is contained in:
Jesse_Chen
2026-08-16 16:49:05 +08:00
parent 9df43e8112
commit c712787ec2
24 changed files with 180 additions and 206 deletions
+4 -4
View File
@@ -25,7 +25,7 @@ const compatibilityRoles = readFileSync(new URL("../../deploy/postgres/002-ensur
const administratorsRoute = readFileSync(new URL("../src/app/api/admin/administrators/route.ts", import.meta.url), "utf8");
const mfaRoute = readFileSync(new URL("../src/app/api/admin/mfa/route.ts", import.meta.url), "utf8");
const mfaSecurity = readFileSync(new URL("../src/components/admin/mfa-security.tsx", import.meta.url), "utf8");
const reasonActionModal = readFileSync(new URL("../src/components/admin/reason-action-modal.tsx", import.meta.url), "utf8");
const confirmActionModal = readFileSync(new URL("../src/components/admin/confirm-action-modal.tsx", import.meta.url), "utf8");
const customersRoute = readFileSync(new URL("../src/app/api/admin/customers/route.ts", import.meta.url), "utf8");
const adminUser = readFileSync(new URL("../src/lib/supabase/admin.ts", import.meta.url), "utf8");
const codesRoute = readFileSync(new URL("../src/app/api/admin/codes/route.ts", import.meta.url), "utf8");
@@ -237,9 +237,9 @@ test("administrator writes keep permission and origin checks without operation-l
assert.doesNotMatch(adminHttp, /requireHighRiskAdminMutation|verifyHighRiskAdminProof/);
assert.doesNotMatch(administratorsRoute, /requireHighRiskAdminMutation/);
assert.equal(existsSync(new URL("../src/app/api/admin/reauth/route.ts", import.meta.url)), false);
assert.match(reasonActionModal, /label="操作原因"/);
assert.match(reasonActionModal, /await onSubmit\(values\.reason\.trim\(\)\)/);
assert.doesNotMatch(reasonActionModal, /邮箱验证码|\/api\/admin\/reauth|reauthPermission/);
assert.doesNotMatch(confirmActionModal, /操作原因|name="reason"|TextArea/);
assert.match(confirmActionModal, /await onConfirm\(\)/);
assert.doesNotMatch(confirmActionModal, /邮箱验证码|\/api\/admin\/reauth|reauthPermission/);
assert.match(authFactory, /twoFactor\(/);
assert.match(authFactory, /schema: identityModelMapping\.twoFactor/);
+4 -4
View File
@@ -75,12 +75,12 @@ test("MFA API keeps native Better Auth enrollment, recovery, rotation, and proof
assert.match(migration, /grant select, insert, update, delete on table identity\.two_factors[\s\S]*identity_runtime/);
});
test("business confirmation UI requires only an audit reason while account MFA remains separate", () => {
const modal = readFileSync(new URL("../src/components/admin/reason-action-modal.tsx", import.meta.url), "utf8");
test("business confirmation UI has no per-operation input while account MFA remains separate", () => {
const modal = readFileSync(new URL("../src/components/admin/confirm-action-modal.tsx", import.meta.url), "utf8");
const http = readFileSync(new URL("../src/lib/admin/http.ts", import.meta.url), "utf8");
assert.match(modal, /label="操作原因"/);
assert.match(modal, /await onSubmit\(values\.reason\.trim\(\)\)/);
assert.doesNotMatch(modal, /操作原因|name="reason"|TextArea/);
assert.match(modal, /await onConfirm\(\)/);
assert.doesNotMatch(modal, /邮箱验证码|\/api\/admin\/reauth|reauthPermission/);
assert.match(http, /requireAdminMutation/);
assert.doesNotMatch(http, /requireHighRiskAdminMutation|verifyHighRiskAdminProof/);
@@ -38,22 +38,25 @@ test("operation-level admin email reauthentication is removed from routes and UI
}
});
test("admin mutations retain session, permission, trusted-origin, reason, request-id, and audit boundaries", () => {
test("admin mutations retain session, permission, trusted-origin, server audit, request-id, and audit boundaries", () => {
const http = source("src/lib/admin/http.ts");
const codesRoute = source("src/app/api/admin/codes/route.ts");
const codesHelper = source("src/lib/admin/codes.ts");
const codesUi = source("src/components/admin/codes-resource.tsx");
const modal = source("src/components/admin/reason-action-modal.tsx");
const modal = source("src/components/admin/confirm-action-modal.tsx");
assert.match(http, /requirePermission\(permission, request\.headers\)/);
assert.match(http, /isTrustedAdminMutationRequest\(request, process\.env\.ADMIN_USER_ORIGIN\)/);
assert.match(codesRoute, /requireAdminMutation\([\s\S]*"billing\.adjustments\.write"/);
assert.match(codesRoute, /reason:\s*z\.string\(\)\.trim\(\)\.min\(1\)\.max\(500\)/);
assert.doesNotMatch(codesRoute, /reason:\s*z\.string/);
assert.match(codesRoute, /p_reason:\s*"admin_console_create_redemption_codes"/);
assert.match(codesHelper, /JSON\.stringify\(input\.p_codes\)/);
assert.match(codesRoute, /requestId\(request\)/);
assert.match(codesRoute, /admin_create_redemption_codes/);
assert.match(codesUi, /permissions\.includes\("billing\.adjustments\.write"\)/);
assert.match(codesUi, /okText="确认生成"/);
assert.match(modal, /label="操作原因"/);
assert.match(modal, /await onSubmit\(values\.reason\.trim\(\)\)/);
assert.doesNotMatch(modal, /操作原因|TextArea|name="reason"/);
assert.match(modal, /await onConfirm\(\)/);
});
test("account-level TOTP MFA and customer login email OTP remain available", () => {
@@ -66,7 +66,7 @@ test("admin mutation UI permissions match ordinary API guards without operation
await t.test(mapping.name, () => {
const ui = source(mapping.ui);
const api = source(mapping.api);
assert.match(ui, /ReasonActionModal/);
assert.match(ui, /ConfirmActionModal/);
assert.doesNotMatch(ui, /reauthPermission|邮箱验证码|\/api\/admin\/reauth/);
assert.match(api, new RegExp(String.raw`requireAdminMutation\(\s*request,\s*(?:permission|["']${mapping.permission.replaceAll(".", "\\.")}["']),?\s*\)`));
assert.doesNotMatch(api, /requireHighRiskAdminMutation/);
@@ -115,17 +115,17 @@ test("model management uses ordinary admin mutation guards without reauth", () =
assert.doesNotMatch(ui, /验证并(?:保存|获取)/);
});
test("admin request failures are real Error instances and reason modal keeps failures visible", () => {
test("admin request failures are real Error instances and confirmation modal keeps failures visible", () => {
const providers = source("src/lib/admin/providers.ts");
const modal = source("src/components/admin/reason-action-modal.tsx");
const modal = source("src/components/admin/confirm-action-modal.tsx");
assert.match(
providers,
/Object\.assign\(new Error\(message\),\s*\{\s*statusCode: response\.status,/,
);
assert.doesNotMatch(providers, /throw \{ message, statusCode/);
assert.match(modal, /catch \(error\)[\s\S]*setActionError\(error instanceof Error \? error\.message/);
assert.match(modal, /name="reason"[\s\S]*required: true, whitespace: true/);
assert.match(modal, /await onSubmit\(values\.reason\.trim\(\)\)/);
assert.doesNotMatch(modal, /操作原因|name="reason"|TextArea/);
assert.match(modal, /await onConfirm\(\)/);
assert.doesNotMatch(modal, /reauth|邮箱验证码|发送验证码/);
});
@@ -40,13 +40,13 @@ test("existing payment orders remain queryable and settleable when subscriptions
test("billing and operations writes use the shared ordinary mutation guard", () => {
assert.match(productsRoute, /requireAdminMutation\(request, permission\)/);
assert.match(productsRoute, /body\.data\.reason[\s\S]*requestId/);
assert.match(productsRoute, /admin_console_(?:save|publish)_product/);
assert.match(epaySettingsRoute, /requireAdminMutation\(request, "billing\.adjustments\.write"\)/);
assert.match(epaySettingsRoute, /admin_save_epay_settings/);
assert.match(subscriptionsRoute, /requireAdminMutation\(request,"billing\.adjustments\.write"\)/);
assert.match(subscriptionsRoute, /body\.data\.reason,rid/);
assert.match(subscriptionsRoute, /admin_console_adjust_subscription/);
assert.match(featureFlagsRoute, /requireAdminMutation\(request,"ops\.flags\.write"\)/);
assert.match(featureFlagsRoute, /admin_publish_feature_flag[\s\S]*b\.data\.reason,rid/);
assert.match(featureFlagsRoute, /admin_console_publish_feature_flag/);
for (const route of [productsRoute, epaySettingsRoute, subscriptionsRoute, featureFlagsRoute]) {
assert.doesNotMatch(route, /requireHighRiskAdminMutation/);
}
@@ -74,13 +74,14 @@ test("self-hosted payment catalog uses simple queries and immutable product snap
assert.match(createRoute, /product_snapshot:\s*productSnapshot/);
});
test("order adjustments require permission, reason, version, idempotency request id, and the domain RPC", () => {
test("order adjustments require permission, version, server audit reason, request id, and the domain RPC", () => {
assert.match(
ordersRoute,
/requireAdminMutation\(\s*request,\s*"billing\.adjustments\.write",?\s*\)/,
);
assert.match(ordersRoute, /expectedVersion:\s*z\.number\(\)\.int\(\)\.min\(0\)/);
assert.match(ordersRoute, /reason:\s*z\.string\(\)\.trim\(\)\.min\(1\)\.max\(500\)/);
assert.doesNotMatch(ordersRoute, /reason:\s*z\.string/);
assert.match(ordersRoute, /admin_console_adjust_order/);
assert.match(ordersRoute, /queryAdminRows<AdjustmentRow>/);
assert.match(ordersRoute, /public\.admin_adjust_order\(/);
assert.match(ordersRoute, /requestId\(request\)/);
@@ -92,16 +93,16 @@ test("order adjustments require permission, reason, version, idempotency request
assert.match(billingOperationsUi, /不调用支付网关|仅记录账务/);
});
test("every redemption-code write requires a reason and forwards it to the audited RPC", () => {
test("redemption-code writes generate server audit reasons and encode JSONB for self-hosted pg", () => {
assert.match(codesRoute, /requireAdminMutation\(\s*request,\s*"billing\.adjustments\.write",?\s*\)/);
assert.equal((codeRoute.match(/requireAdminMutation\(/g) ?? []).length, 2);
assert.doesNotMatch(codeRoute, /requireHighRiskAdminMutation/);
assert.match(codesRoute, /reason:\s*z\.string\(\)\.trim\(\)\.min\(1\)\.max\(500\)/);
assert.match(codesRoute, /p_reason:\s*parsed\.data\.reason/);
assert.match(codeRoute, /const revokeCodeSchema[\s\S]*reason:\s*z\.string\(\)\.trim\(\)\.min\(1\)\.max\(500\)/);
assert.match(codeRoute, /const updateCodeSchema[\s\S]*reason:\s*z\.string\(\)\.trim\(\)\.min\(1\)\.max\(500\)/);
assert.match(codeRoute, /p_reason:\s*body\.reason/);
assert.match(codeRoute, /p_reason:\s*parsedBody\.data\.reason/);
assert.doesNotMatch(codesRoute, /reason:\s*z\.string/);
assert.doesNotMatch(codeRoute, /reason:\s*z\.string/);
assert.match(codesRoute, /p_reason:\s*"admin_console_create_redemption_codes"/);
assert.match(codeRoute, /p_reason:\s*"admin_console_update_redemption_code"/);
assert.match(codeRoute, /p_reason:\s*"admin_console_revoke_redemption_code"/);
assert.match(codesHelper, /JSON\.stringify\(input\.p_codes\)/);
assert.match(codesHelper, /queryAdminRows<RpcCodeRow>/);
assert.match(codesHelper, /public\.admin_create_redemption_codes\(/);
assert.match(codesHelper, /public\.admin_update_redemption_code\(/);
@@ -109,7 +110,7 @@ test("every redemption-code write requires a reason and forwards it to the audit
assert.doesNotMatch(codesHelper, /createAdminSupabaseClient|\.rpc\(|\$\{functionName\}/);
assert.match(codesUi, /permissions\.includes\("billing\.adjustments\.write"\)/);
assert.doesNotMatch(codesUi, /reauthPermission|邮箱验证码|\/api\/admin\/reauth/);
assert.match(codesUi, /open=\{Boolean\(pendingCreate\)\}[\s\S]*onSubmit=\{submitCreate\}/);
assert.match(codesUi, /open=\{Boolean\(pendingEdit\)\}[\s\S]*onSubmit=\{submitEdit\}/);
assert.match(codesUi, /<ReasonActionModal[\s\S]*onSubmit=\{\(reason\) => revoke\(revokeRecord!, reason\)\}/);
assert.match(codesUi, /open=\{Boolean\(pendingCreate\)\}[\s\S]*onConfirm=\{submitCreate\}/);
assert.match(codesUi, /open=\{Boolean\(pendingEdit\)\}[\s\S]*onConfirm=\{submitEdit\}/);
assert.match(codesUi, /<ConfirmActionModal[\s\S]*onConfirm=\{\(\) => revoke\(revokeRecord!\)\}/);
});