import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { GET as getAdminUnavailable } from "../src/app/admin-unavailable/route.ts"; import { resolveAdminPageAccessFailure } from "../src/lib/admin/page-access.ts"; const migration = readFileSync( new URL("../supabase/migrations/20260805010000_reconcile_admin_redemption_audit.sql", import.meta.url), "utf8", ); const auth = readFileSync(new URL("../src/lib/admin/auth.ts", import.meta.url), "utf8"); const authBoundary = readFileSync(new URL("../src/lib/admin/auth-boundary.ts", import.meta.url), "utf8"); const authPolicy = readFileSync(new URL("../src/lib/admin/auth-policy.ts", import.meta.url), "utf8"); const authFactory = readFileSync(new URL("../src/modules/identity/auth-factory.ts", import.meta.url), "utf8"); const adminHttp = readFileSync(new URL("../src/lib/admin/http.ts", import.meta.url), "utf8"); const rbacMigration = readFileSync(new URL("../supabase/migrations/20260806010000_admin_rbac.sql", import.meta.url), "utf8"); const ownerRecoveryMigration = readFileSync(new URL("../db/migrations/20260807010000_recover_initial_admin_owner.sql", import.meta.url), "utf8"); const bootstrapRoles = readFileSync(new URL("../../deploy/postgres/001-bootstrap-roles.sh", import.meta.url), "utf8"); const compatibilityRoles = readFileSync(new URL("../../deploy/postgres/002-ensure-business-compatibility-roles.sql", import.meta.url), "utf8"); const administratorsRoute = readFileSync(new URL("../src/app/api/admin/administrators/route.ts", import.meta.url), "utf8"); const reauthRoute = readFileSync(new URL("../src/app/api/admin/reauth/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 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"); const codeRoute = readFileSync(new URL("../src/app/api/admin/codes/[id]/route.ts", import.meta.url), "utf8"); const providers = readFileSync(new URL("../src/lib/admin/providers.ts", import.meta.url), "utf8"); const adminLayout = readFileSync(new URL("../src/app/admin/layout.tsx", import.meta.url), "utf8"); const adminApp = readFileSync(new URL("../src/components/admin/admin-app.tsx", import.meta.url), "utf8"); const adminRootRoute = readFileSync(new URL("../src/app/admin/route.ts", import.meta.url), "utf8"); const forbiddenPage = readFileSync(new URL("../src/app/forbidden.tsx", import.meta.url), "utf8"); const nextConfig = readFileSync(new URL("../next.config.ts", import.meta.url), "utf8"); const stagingCaddy = readFileSync(new URL("../../deploy/Caddyfile.staging", import.meta.url), "utf8"); const readonlyRoutes = ["customers", "credit-transactions", "consultations", "audit-logs"].map((resource) => readFileSync(new URL(`../src/app/api/admin/${resource}/route.ts`, import.meta.url), "utf8"), ); const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")); test("admin APIs use persisted Better Auth roles with admin-only boundaries", () => { assert.match(auth, /requireIdentityServerSession/); assert.match(auth, /getIdentityAuthServices\(\)\.user\.api/); assert.match(authPolicy, /permissions\.includes\(required\)/); assert.match(auth, /admin_permission_keys\(\$1\)/); assert.doesNotMatch(auth, /ADMIN_EMAILS|isAdminEmail/); assert.match(authBoundary, /readAuthProvider\(\)\?\.trim\(\) !== "self-hosted"/); assert.match(authBoundary, /后台服务暂时不可用", 503/); assert.match(codesRoute, /requireHighRiskAdminMutation\(\s*request,\s*"billing\.adjustments\.write",?\s*\)/); assert.match(codeRoute, /requireHighRiskAdminMutation\(\s*request,\s*"billing\.adjustments\.write",?\s*\)/g); }); test("self-hosted account entry uses the database permission graph", () => { const selfHostedBranch = adminUser.slice( adminUser.indexOf('process.env.AUTH_PROVIDER?.trim() === "self-hosted"'), adminUser.indexOf("if (isAdminEmail"), ); assert.match(selfHostedBranch, /queryAdminRows/); assert.match(selfHostedBranch, /admin_has_permission\(\$1, 'admin\.access'\)/); assert.doesNotMatch(selfHostedBranch, /role === "admin"|viewer|isAdminEmail|ADMIN_EMAILS/); assert.match(authBoundary, /authorizeAdminAccess\([\s\S]*identitySession\.user,[\s\S]*session\.permissions,[\s\S]*permission/); }); test("admin navigation exposes separated RBAC and billing resources", () => { assert.match(adminApp, /name: "administrators", list: "\/admin\/administrators"/); assert.match(adminApp, /name: "customers", list: "\/admin\/customers"/); assert.match(adminApp, /name: "products", list: "\/admin\/products"/); assert.match(adminApp, /name: "subscriptions", list: "\/admin\/subscriptions"/); assert.match(adminApp, /name: "orders", list: "\/admin\/orders"/); assert.match(adminApp, /name: "security", list: "\/admin\/security"/); assert.match(adminApp, /CreditCardOutlined/); assert.match(adminApp, /ShoppingOutlined/); }); test("admin shell uses large controls without the return-to-chat sider override", () => { assert.match(adminApp, /\{children\}<\/ThemedLayout>/); assert.doesNotMatch(adminApp, /AdminSider|ThemedSider|ArrowLeftOutlined|返回对话|next\/link|Menu\.Item/); }); test("admin pages and root route are server-gated before rendering or redirecting", () => { assert.match(adminLayout, /await requireAdminSession\("read"\)/); assert.match(adminLayout, /redirect\(/); assert.match(adminRootRoute, /await requireAdminSession\("read"\)/); assert.match(adminRootRoute, /headers: \{ location: "\/admin\/codes" \}/); }); test("admin authorization denial cannot enter the staging Caddy root redirect loop", () => { assert.match(stagingCaddy, /@root path \/\n\s+redir @root \/admin 308/); assert.deepEqual(resolveAdminPageAccessFailure(401), { kind: "login", location: "/login" }); assert.deepEqual(resolveAdminPageAccessFailure(403), { kind: "forbidden", status: 403, message: "无权访问后台", }); assert.deepEqual(resolveAdminPageAccessFailure(503), { kind: "unavailable", status: 503, message: "后台服务暂时不可用", location: "/admin-unavailable", }); assert.match(adminLayout, /failure\.kind === "login"[\s\S]*redirect\(failure\.location\)/); assert.match(adminLayout, /failure\.kind === "forbidden"[\s\S]*forbidden\(\)/); assert.match(adminLayout, /failure\.kind === "unavailable"[\s\S]*redirect\(failure\.location\)/); assert.match(adminLayout, /throw error/); assert.match(adminRootRoute, /status: failure\.status/); assert.match(adminRootRoute, /"cache-control": "no-store"/); assert.doesNotMatch(adminLayout, /redirect\([^)]*"\/"/); assert.doesNotMatch(adminRootRoute, /location:[^\n]*"\/"/); assert.match(nextConfig, /authInterrupts: true/); assert.match(forbiddenPage, /403/); assert.match(forbiddenPage, /没有后台访问权限/); }); test("admin unavailable route terminates layout redirects with a no-store 503", async () => { const response = getAdminUnavailable(); assert.equal(response.status, 503); assert.equal(response.headers.get("cache-control"), "no-store"); assert.equal(response.headers.get("location"), null); assert.equal(response.headers.get("content-type"), "text/plain; charset=utf-8"); assert.equal(await response.text(), "后台服务暂时不可用"); }); test("initial Owner recovery safely no-ops until every RBAC prerequisite exists", () => { for (const prerequisite of [ "public.admin_users", "public.admin_roles", "public.admin_user_roles", "identity.users", "auth.users", ]) { const escaped = prerequisite.replaceAll(".", "\\."); assert.match(ownerRecoveryMigration, new RegExp(`to_regclass\\('${escaped}'\\) is null`)); } assert.match(ownerRecoveryMigration, /to_regprocedure\('public\.admin_permission_keys\(uuid\)'\) is null/); assert.match(ownerRecoveryMigration, /to_regprocedure\('public\.assert_active_admin_owner_exists\(\)'\) is null/); assert.match(ownerRecoveryMigration, /then[\s\S]*return;[\s\S]*end if;[\s\S]*pg_advisory_xact_lock/); }); test("initial Owner recovery is single-candidate, fail-closed, and independent of ADMIN_EMAILS", () => { assert.match(ownerRecoveryMigration, /v_active_owner_count > 0[\s\S]*return/); assert.match( ownerRecoveryMigration, /not u\.banned or \(u\.ban_expires is not null and u\.ban_expires <= clock_timestamp\(\)\)/, ); assert.match(ownerRecoveryMigration, /lock table identity\.users, auth\.users in share mode/); assert.match( ownerRecoveryMigration, /not exists \(select 1 from identity\.users\)[\s\S]*not exists \(select 1 from auth\.users\)/, ); assert.match(ownerRecoveryMigration, /join auth\.users a on a\.id = u\.id/); assert.match(ownerRecoveryMigration, /left join public\.admin_users existing_admin/); assert.match( ownerRecoveryMigration, /existing_admin\.user_id is null or existing_admin\.revoked_at is null/, ); assert.match( ownerRecoveryMigration, /unnest\(string_to_array\(u\.role, ','\)\)[\s\S]*btrim\(role_part\.value\) = 'admin'/, ); assert.match(ownerRecoveryMigration, /v_candidate_count <> 1/); assert.match(ownerRecoveryMigration, /admin_owner_recovery_requires_exactly_one_active_identity_admin/); assert.match(ownerRecoveryMigration, /insert into public\.admin_users/); assert.match( ownerRecoveryMigration, /on conflict on constraint admin_users_pkey do nothing/, ); assert.doesNotMatch( ownerRecoveryMigration, /on conflict on constraint admin_users_pkey do update[\s\S]*revoked_at\s*=\s*null/, ); assert.match(ownerRecoveryMigration, /insert into public\.admin_user_roles/); assert.doesNotMatch(ownerRecoveryMigration, /ADMIN_EMAILS|email\s*=|ilike|lower\(.*email/); }); test("readonly resources cannot be mutated through Refine access control", () => { for (const resource of ["customers", "credit-transactions", "consultations", "audit-logs"]) { assert.match(providers, new RegExp(resource.includes("-") ? `"${resource}"` : `${resource}:`)); } assert.match(providers, /const resourcePermissions/); assert.match(providers, /permission && identity\.permissions\.includes\(permission\)/); for (const route of readonlyRoutes) { assert.match(route, /export const POST = readonlyAdminMutation/); assert.match(route, /export const PATCH = readonlyAdminMutation/); assert.match(route, /export const DELETE = readonlyAdminMutation/); } }); test("redemption code writes are atomic with append-only redacted audit", () => { assert.match(migration, /create table if not exists audit\.admin_audit_logs/); assert.match(migration, /admin_audit_logs_append_only/); assert.match(migration, /redemption_code\.create/); assert.match(migration, /redemption_code\.update/); assert.match(migration, /redemption_code\.revoke/); assert.match(migration, /before_value is null or not \(before_value \?\| array\['code', 'code_hash', 'token', 'secret', 'key'\]\)/); assert.match(migration, /insert into audit\.admin_audit_logs/); assert.match(migration, /redeemed codes are immutable/); assert.match(migration, /revoked codes are immutable/); assert.match(migration, /v_code\.revoked_at is not null/); assert.match(migration, /'revoked_code'/); assert.match(migration, /set local role service_role|profiles_admin_read/); assert.match(migration, /p_codes is null or jsonb_typeof\(p_codes\) is distinct from 'array'/); assert.match(migration, /admin_verified_actor_email/); }); test("plaintext code is returned only by create and never enters audit snapshots", () => { assert.match(codesRoute, /plainCodes\.map/); assert.match(codesRoute, /code,/); assert.doesNotMatch(codeRoute, /codeHash|code_hash|plainCodes/); const snapshot = migration.match(/create or replace function public\.admin_redemption_code_snapshot[\s\S]*?revoke all on function/); assert.ok(snapshot); assert.doesNotMatch(snapshot[0], /code_hash|'code'/); assert.match(snapshot[0], /'mask'/); }); test("Refine dependencies and same-origin admin data provider are present", () => { for (const dependency of ["@refinedev/core", "@refinedev/antd", "@refinedev/nextjs-router", "antd"]) { assert.ok(packageJson.dependencies[dependency], `${dependency} missing`); } assert.match(providers, /const apiBase = "\/api\/admin"/); assert.doesNotMatch(providers, /https?:\/\//); }); test("administrator writes require scoped email OTP reauthentication without mandatory MFA", () => { assert.match(adminHttp, /isSameOriginAdminMutation/); assert.match(administratorsRoute, /requireHighRiskAdminMutation\(request, "admin\.users\.manage_roles"\)/); assert.match(adminHttp, /requireAdminMutation\(request, permission\)[\s\S]*verifyHighRiskAdminProof/); assert.doesNotMatch(adminHttp, /requireAdminMfaIfRequired/); assert.match(reauthRoute, /requireAdminMutation\(request, parsed\.data\.permission\)[\s\S]*sendVerificationOTP/); assert.doesNotMatch(reauthRoute, /requireAdminMfaIfRequired/); assert.match(reauthRoute, /sendVerificationOTP/); assert.match(reauthRoute, /verifyEmailOTP/); assert.match(reauthRoute, /httpOnly: true/); assert.match(reasonActionModal, /action: "request"/); assert.match(reasonActionModal, /action: "verify"/); assert.match(reasonActionModal, /邮箱验证码/); assert.doesNotMatch(reasonActionModal, /\/api\/admin\/mfa|\/admin\/security|MfaStatus|mfaFactor|mfaReady/); assert.match(authFactory, /twoFactor\(/); assert.match(authFactory, /schema: identityModelMapping\.twoFactor/); assert.doesNotMatch(authFactory, /skipVerificationOnEnable\s*:\s*true/); assert.match(mfaRoute, /enableTwoFactor/); assert.match(mfaRoute, /verifyTOTP/); assert.match(mfaRoute, /verifyBackupCode/); assert.match(mfaRoute, /generateBackupCodes/); assert.match(mfaRoute, /disableTwoFactor/); assert.match(mfaSecurity, /\/api\/admin\/mfa/); assert.doesNotMatch(administratorsRoute, /ADMIN_MFA_CAPABLE|process\.env/); assert.doesNotMatch(reauthRoute, /ADMIN_MFA_CAPABLE|process\.env/); }); test("customer birth data uses a narrow permission, is masked by default, and audits sensitive reads", () => { assert.match(authPolicy, /"admin\.customers\.read"/); assert.match(authPolicy, /"admin\.customers\.birth_data\.read"/); assert.match(customersRoute, /requirePermission\("admin\.customers\.read"\)/); assert.doesNotMatch(customersRoute, /requirePermission\("billing\.orders\.read"\)/); assert.match(customersRoute, /admin_read_customer_birth_data/); assert.match(customersRoute, /revealCustomerBirthData/); assert.match(customersRoute, /birthDataMasked: true/); assert.match(rbacMigration, /'owner', 'admin\.customers\.birth_data\.read'/); for (const role of ["support", "operations", "auditor"]) { assert.doesNotMatch(rbacMigration, new RegExp(`'${role}', 'admin\\.customers\\.birth_data\\.read'`)); } assert.match(rbacMigration, /admin\.customer\.birth_data\.read/); assert.match(rbacMigration, /permission_used[\s\S]*admin\.customers\.birth_data\.read/); assert.match(rbacMigration, /revoke select \(birth_date, birth_time_status, birth_place_label\)/); }); test("admin runtime cannot assume service_role and keeps explicit RBAC grants", () => { assert.match(bootstrapRoles, /GRANT service_role TO service_runtime/); assert.match(bootstrapRoles, /REVOKE service_role FROM admin_runtime/); assert.doesNotMatch(bootstrapRoles, /GRANT service_role TO admin_runtime/); assert.match(compatibilityRoles, /grant service_role to service_runtime/); assert.match(compatibilityRoles, /revoke service_role from admin_runtime/); assert.doesNotMatch(compatibilityRoles, /grant service_role to admin_runtime/); assert.match(rbacMigration, /admin_runtime_service_role_membership_must_be_revoked_by_bootstrap/); assert.match(rbacMigration, /grant execute on function public\.admin_has_permission/); assert.match(rbacMigration, /public\.admin_read_customer_birth_data\(uuid, uuid\[\], text\)[\s\S]*to admin_runtime/); }); test("last Owner revocations are serialized by one transaction advisory lock", () => { assert.match(rbacMigration, /pg_advisory_xact_lock\(1096040772, 1\)[\s\S]*v_owner_count/); });