fix: expose staging admin entry
Deploy staging to test server / deploy (push) Successful in 3m11s

Use persisted self-hosted roles and the isolated admin origin so authorized staging accounts can discover the protected admin surface.
This commit is contained in:
linmeng
2026-07-29 10:14:39 +08:00
parent f7fd3e6a60
commit 2dac8bc47b
9 changed files with 100 additions and 19 deletions
+15 -15
View File
@@ -4,25 +4,27 @@ import test from "node:test";
const root = new URL("../", import.meta.url);
const adminSource = readFileSync(new URL("src/lib/supabase/admin.ts", root), "utf8");
const layoutSource = readFileSync(new URL("src/app/admin/layout.tsx", root), "utf8");
const adminCoreSource = readFileSync(new URL("src/lib/supabase/admin-client-core.ts", root), "utf8");
const codesSource = readFileSync(new URL("src/app/api/admin/codes/route.ts", root), "utf8");
const sessionSource = readFileSync(new URL("src/app/api/admin/session/route.ts", root), "utf8");
const accountSource = readFileSync(new URL("src/app/api/account/route.ts", root), "utf8");
const usersSource = readFileSync(new URL("src/app/api/admin/users/route.ts", root), "utf8");
const migration = readFileSync(new URL("supabase/migrations/20260727010000_admin_users.sql", root), "utf8");
test("ADMIN_EMAILS remains a case-insensitive comma-separated allowlist", () => {
assert.match(adminSource, /configured/);
assert.match(adminSource, /split\(\",\"\)/);
assert.match(adminSource, /toLowerCase/);
assert.match(adminSource, /export function isAdminEmail/);
assert.match(adminCoreSource, /configured/);
assert.match(adminCoreSource, /split\(","\)/);
assert.match(adminCoreSource, /toLowerCase/);
assert.match(adminCoreSource, /export function isAdminEmail/);
});
test("admin surfaces await database-backed administrator checks", () => {
assert.match(adminSource, /export async function isAdminUser/);
assert.match(adminSource, /from\("admin_users"\)/);
assert.match(layoutSource, /await isAdminUser\(user\)/);
assert.match(codesSource, /await isAdminUser\(user\)/);
assert.match(accountSource, /isAdmin: await isAdminUser\(user\)/);
assert.match(sessionSource, /await requireAdminSession\(\)/);
assert.match(codesSource, /await requireAdminSession\("write"\)/);
assert.match(accountSource, /const isAdmin = await isAdminUser\(user\)/);
assert.match(accountSource, /isAdmin,/);
});
test("admin_users migration is service-role-only and auditable", () => {
@@ -36,12 +38,10 @@ test("admin_users migration is service-role-only and auditable", () => {
assert.match(migration, /grant select, insert, update on table public\.admin_users to service_role/);
});
test("admin users route exposes guarded list, add, and soft revoke contracts", () => {
test("admin users route exposes a guarded list and rejects mutations", () => {
assert.match(usersSource, /export async function GET/);
assert.match(usersSource, /export async function POST/);
assert.match(usersSource, /export async function DELETE/);
assert.match(usersSource, /auth\.admin\.listUsers/);
assert.match(usersSource, /upsert\(\{ user_id: target\.id, created_by: auth\.user\.id/);
assert.match(usersSource, /revoked_at: new Date\(\)\.toISOString\(\)/);
assert.match(usersSource, /环境配置管理员不可撤销/);
assert.match(usersSource, /await requireAdminSession\(\)/);
assert.match(usersSource, /export const POST = readonlyAdminMutation/);
assert.match(usersSource, /export const PATCH = readonlyAdminMutation/);
assert.match(usersSource, /export const DELETE = readonlyAdminMutation/);
});