fix(admin): remove redundant confirmations and repair code access
This commit is contained in:
@@ -25,7 +25,6 @@ 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 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");
|
||||
@@ -50,7 +49,7 @@ test("admin APIs use persisted Better Auth roles with admin-only boundaries", ()
|
||||
assert.doesNotMatch(auth, /ADMIN_EMAILS|isAdminEmail/);
|
||||
assert.match(authBoundary, /readAuthProvider\(\)\?\.trim\(\) !== "self-hosted"/);
|
||||
assert.match(authBoundary, /后台服务暂时不可用", 503/);
|
||||
assert.match(codesRoute, /requireAdminMutation\(\s*request,\s*"billing\.adjustments\.write",?\s*\)/);
|
||||
assert.match(codesRoute, /requireAdminMutation\(\s*request,\s*"admin\.access",?\s*\)/);
|
||||
assert.equal((codeRoute.match(/requireAdminMutation\(/g) ?? []).length, 2);
|
||||
assert.doesNotMatch(codesRoute, /requireHighRiskAdminMutation/);
|
||||
assert.doesNotMatch(codeRoute, /requireHighRiskAdminMutation/);
|
||||
@@ -237,9 +236,7 @@ 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.doesNotMatch(confirmActionModal, /操作原因|name="reason"|TextArea/);
|
||||
assert.match(confirmActionModal, /await onConfirm\(\)/);
|
||||
assert.doesNotMatch(confirmActionModal, /邮箱验证码|\/api\/admin\/reauth|reauthPermission/);
|
||||
assert.equal(existsSync(new URL("../src/components/admin/confirm-action-modal.tsx", import.meta.url)), false);
|
||||
|
||||
assert.match(authFactory, /twoFactor\(/);
|
||||
assert.match(authFactory, /schema: identityModelMapping\.twoFactor/);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
@@ -75,13 +75,10 @@ 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 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");
|
||||
test("business operation confirmation UI is removed while account MFA remains separate", () => {
|
||||
const http = readFileSync(new URL("../src/lib/admin/http.ts", import.meta.url), "utf8");
|
||||
|
||||
assert.doesNotMatch(modal, /操作原因|name="reason"|TextArea/);
|
||||
assert.match(modal, /await onConfirm\(\)/);
|
||||
assert.doesNotMatch(modal, /邮箱验证码|\/api\/admin\/reauth|reauthPermission/);
|
||||
assert.equal(existsSync(new URL("../src/components/admin/confirm-action-modal.tsx", import.meta.url)), false);
|
||||
assert.match(http, /requireAdminMutation/);
|
||||
assert.doesNotMatch(http, /requireHighRiskAdminMutation|verifyHighRiskAdminProof/);
|
||||
});
|
||||
|
||||
@@ -43,20 +43,19 @@ test("admin mutations retain session, permission, trusted-origin, server audit,
|
||||
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/confirm-action-modal.tsx");
|
||||
assert.equal(existsSync(new URL("src/components/admin/confirm-action-modal.tsx", root)), false);
|
||||
|
||||
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, /requireAdminMutation\([\s\S]*"admin\.access"/);
|
||||
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.doesNotMatch(modal, /操作原因|TextArea|name="reason"/);
|
||||
assert.match(modal, /await onConfirm\(\)/);
|
||||
assert.match(codesUi, /permissions\.includes\("admin\.access"\)/);
|
||||
assert.match(codesUi, /onFinish=\{\(values\) => void submitCreate\(values\)\}/);
|
||||
assert.doesNotMatch(codesUi, /ConfirmActionModal|pendingCreate|pendingEdit|revokeRecord/);
|
||||
});
|
||||
|
||||
test("account-level TOTP MFA and customer login email OTP remain available", () => {
|
||||
|
||||
@@ -33,13 +33,13 @@ const mutationMappings = [
|
||||
name: "兑换码创建",
|
||||
ui: "src/components/admin/codes-resource.tsx",
|
||||
api: "src/app/api/admin/codes/route.ts",
|
||||
permission: "billing.adjustments.write",
|
||||
permission: "admin.access",
|
||||
},
|
||||
{
|
||||
name: "兑换码编辑与撤销",
|
||||
ui: "src/components/admin/codes-resource.tsx",
|
||||
api: "src/app/api/admin/codes/[id]/route.ts",
|
||||
permission: "billing.adjustments.write",
|
||||
permission: "admin.access",
|
||||
},
|
||||
{
|
||||
name: "功能开关发布",
|
||||
@@ -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, /ConfirmActionModal/);
|
||||
assert.doesNotMatch(ui, /ConfirmActionModal|Popconfirm|Modal\.confirm/);
|
||||
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/);
|
||||
@@ -75,6 +75,17 @@ test("admin mutation UI permissions match ordinary API guards without operation
|
||||
}
|
||||
});
|
||||
|
||||
test("redemption-code list and mutations are available to every authenticated admin role", () => {
|
||||
const providers = source("src/lib/admin/providers.ts");
|
||||
const route = source("src/app/api/admin/codes/route.ts");
|
||||
|
||||
assert.match(
|
||||
providers,
|
||||
/codes:\s*\{\s*read:\s*"admin\.access",\s*write:\s*"admin\.access"\s*\}/,
|
||||
);
|
||||
assert.match(route, /await requirePermission\("admin\.access"\)/);
|
||||
});
|
||||
|
||||
test("product management localizes product, billing and entitlement enums", () => {
|
||||
const ui = source("src/components/admin/product-management.tsx");
|
||||
|
||||
@@ -115,18 +126,16 @@ test("model management uses ordinary admin mutation guards without reauth", () =
|
||||
assert.doesNotMatch(ui, /验证并(?:保存|获取)/);
|
||||
});
|
||||
|
||||
test("admin request failures are real Error instances and confirmation modal keeps failures visible", () => {
|
||||
test("admin request failures are real Error instances and operation confirmation UI is absent", () => {
|
||||
const providers = source("src/lib/admin/providers.ts");
|
||||
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.doesNotMatch(modal, /操作原因|name="reason"|TextArea/);
|
||||
assert.match(modal, /await onConfirm\(\)/);
|
||||
assert.doesNotMatch(modal, /reauth|邮箱验证码|发送验证码/);
|
||||
for (const path of mutationMappings.map((mapping) => mapping.ui)) {
|
||||
assert.doesNotMatch(source(path), /ConfirmActionModal|Popconfirm|Modal\.confirm/);
|
||||
}
|
||||
});
|
||||
|
||||
test("customer list is always masked and a single explicit reveal is audited each time", () => {
|
||||
@@ -154,7 +163,8 @@ test("administrator UI supports all six roles and exposes last-owner protection"
|
||||
for (const role of ["owner", "model_admin", "billing_admin", "operations", "support", "auditor"]) {
|
||||
assert.match(ui, new RegExp(`["']${role}["']`));
|
||||
}
|
||||
assert.match(ui, /method: pendingAction\.action === "assign" \? "POST" : "DELETE"/);
|
||||
assert.match(ui, /method: action\.action === "assign" \? "POST" : "DELETE"/);
|
||||
assert.doesNotMatch(ui, /pendingAction|ConfirmActionModal/);
|
||||
const mutationSection = route.slice(route.indexOf("async function mutate"));
|
||||
assert.match(mutationSection, /last_owner_protected/);
|
||||
assert.match(mutationSection, /不能撤销最后一位 Owner,请先分配另一位 Owner/);
|
||||
|
||||
@@ -22,7 +22,7 @@ test("admin surfaces await database-backed administrator checks", () => {
|
||||
assert.match(adminSource, /export async function isAdminUser/);
|
||||
assert.match(adminSource, /admin_has_permission\(\$1, 'admin\.access'\)/);
|
||||
assert.match(sessionSource, /await requirePermission\("admin\.access"\)/);
|
||||
assert.match(codesSource, /await requireAdminMutation\(\s*request,\s*"billing\.adjustments\.write",?\s*\)/);
|
||||
assert.match(codesSource, /await requireAdminMutation\(\s*request,\s*"admin\.access",?\s*\)/);
|
||||
assert.doesNotMatch(codesSource, /requireHighRiskAdminMutation/);
|
||||
assert.match(accountSource, /const isAdmin = await isAdminUser\(user\)/);
|
||||
assert.match(accountSource, /isAdmin,/);
|
||||
|
||||
@@ -20,6 +20,8 @@ const codeRoute = source("src/app/api/admin/codes/[id]/route.ts");
|
||||
const codesHelper = source("src/lib/admin/codes.ts");
|
||||
const billingOperationsUi = source("src/components/admin/billing-operations-resources.tsx");
|
||||
const codesUi = source("src/components/admin/codes-resource.tsx");
|
||||
const redemptionAccessMigration = source("supabase/migrations/20260816010000_admin_redemption_admin_access.sql");
|
||||
const baseRedemptionMigration = source("supabase/migrations/20260805010000_reconcile_admin_redemption_audit.sql");
|
||||
|
||||
test("billing.subscriptions only gates new trial and subscription purchases", () => {
|
||||
assert.match(packagesRoute, /loadRuntimeFeatureFlags\(\["billing\.subscriptions"\]\)/);
|
||||
@@ -94,7 +96,8 @@ test("order adjustments require permission, version, server audit reason, reques
|
||||
});
|
||||
|
||||
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.match(codesRoute, /await requirePermission\("admin\.access"\)/);
|
||||
assert.match(codesRoute, /requireAdminMutation\(\s*request,\s*"admin\.access",?\s*\)/);
|
||||
assert.equal((codeRoute.match(/requireAdminMutation\(/g) ?? []).length, 2);
|
||||
assert.doesNotMatch(codeRoute, /requireHighRiskAdminMutation/);
|
||||
assert.doesNotMatch(codesRoute, /reason:\s*z\.string/);
|
||||
@@ -108,9 +111,15 @@ test("redemption-code writes generate server audit reasons and encode JSONB for
|
||||
assert.match(codesHelper, /public\.admin_update_redemption_code\(/);
|
||||
assert.match(codesHelper, /public\.admin_revoke_redemption_code\(/);
|
||||
assert.doesNotMatch(codesHelper, /createAdminSupabaseClient|\.rpc\(|\$\{functionName\}/);
|
||||
assert.match(codesUi, /permissions\.includes\("billing\.adjustments\.write"\)/);
|
||||
assert.match(codesUi, /permissions\.includes\("admin\.access"\)/);
|
||||
assert.doesNotMatch(codesUi, /reauthPermission|邮箱验证码|\/api\/admin\/reauth/);
|
||||
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!\)\}/);
|
||||
assert.match(codesUi, /onFinish=\{\(values\) => void submitCreate\(values\)\}/);
|
||||
assert.match(codesUi, /onFinish=\{\(values\) => void submitEdit\(values\)\}/);
|
||||
assert.match(codesUi, /onClick=\{\(\) => void revoke\(record\)\}/);
|
||||
assert.doesNotMatch(codesUi, /ConfirmActionModal|pendingCreate|pendingEdit|revokeRecord/);
|
||||
assert.equal((redemptionAccessMigration.match(/admin_has_permission\(p_actor_user_id,'admin\.access'\)/g) ?? []).length, 3);
|
||||
assert.match(redemptionAccessMigration, /new\.permission_used := 'admin\.access'/);
|
||||
assert.match(baseRedemptionMigration, /admin_verified_actor_email/);
|
||||
assert.match(redemptionAccessMigration, /grant execute on function public\.admin_create_redemption_codes/);
|
||||
assert.doesNotMatch(redemptionAccessMigration, /billing\.adjustments\.write/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user