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:
@@ -27,6 +27,15 @@ test("account API reads and returns the server-configured rectification price",
|
||||
assert.doesNotMatch(source, /RECTIFICATION_PRICE_CREDITS[^\n]*\?\?\s*["']1["']/);
|
||||
});
|
||||
|
||||
test("account API returns a server-resolved admin entry URL", () => {
|
||||
assert.match(source, /readIdentityConfig\(process\.env\)/);
|
||||
assert.match(source, /new URL\("\/admin\/codes", identityConfig\.adminOrigin\)\.toString\(\)/);
|
||||
assert.match(source, /identityConfig\.provider === "self-hosted"/);
|
||||
assert.match(source, /: "\/admin\/codes"/);
|
||||
assert.match(source, /adminUrl,/);
|
||||
assert.doesNotMatch(source, /NEXT_PUBLIC_ADMIN|process\.env\.AUTH_ADMIN_ORIGIN/);
|
||||
});
|
||||
|
||||
test("account API projects only the minimum case state needed by the homepage", () => {
|
||||
const caseSelect = source.match(/\.from\("birth_time_rectification_cases"\)[\s\S]*?\.limit\(\d+\)/)?.[0] ?? "";
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ const migration = readFileSync(
|
||||
);
|
||||
const auth = readFileSync(new URL("../src/lib/admin/auth.ts", import.meta.url), "utf8");
|
||||
const authPolicy = readFileSync(new URL("../src/lib/admin/auth-policy.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");
|
||||
@@ -27,6 +28,20 @@ test("admin APIs use persisted Better Auth roles with admin and viewer boundarie
|
||||
assert.match(codeRoute, /requireAdminSession\("write"\)/g);
|
||||
});
|
||||
|
||||
test("self-hosted account entry checks persisted admin or viewer roles", () => {
|
||||
const selfHostedBranch = adminUser.slice(
|
||||
adminUser.indexOf('process.env.AUTH_PROVIDER?.trim() === "self-hosted"'),
|
||||
adminUser.indexOf("if (isAdminEmail"),
|
||||
);
|
||||
|
||||
assert.match(selfHostedBranch, /queryAdminRows/);
|
||||
assert.match(selfHostedBranch, /select role from identity\.users where id = \$1 limit 1/);
|
||||
assert.match(selfHostedBranch, /role === "admin" \|\| role === "viewer"/);
|
||||
assert.doesNotMatch(selfHostedBranch, /isAdminEmail|ADMIN_EMAILS/);
|
||||
assert.match(auth, /getIdentityAuthServices\(\)\.admin\.api/);
|
||||
assert.match(auth, /authorizeAdminAccess\(user, access\)/);
|
||||
});
|
||||
|
||||
test("readonly resources cannot be mutated through Refine access control", () => {
|
||||
for (const resource of ["users", "credit-transactions", "consultations", "audit-logs"]) {
|
||||
assert.match(providers, new RegExp(`"${resource}"`));
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
|
||||
@@ -154,6 +154,18 @@ test("keeps app sidebar props as product data and callbacks", () => {
|
||||
assert.doesNotMatch(appSidebar, /supabase|fetch\(|\/api\//i);
|
||||
});
|
||||
|
||||
test("uses the server-provided independent admin entry URL", () => {
|
||||
const appSidebar = readProjectFile("src/components/app-sidebar.tsx");
|
||||
const page = readProjectFile("src/app/page.tsx");
|
||||
|
||||
assert.match(appSidebar, /adminUrl: string \| null/);
|
||||
assert.match(appSidebar, /account\.isAdmin && account\.adminUrl/);
|
||||
assert.match(appSidebar, /href=\{account\.adminUrl\}/);
|
||||
assert.match(appSidebar, />后台管理</);
|
||||
assert.doesNotMatch(appSidebar, /href="\/admin\/codes"/);
|
||||
assert.match(page, /adminUrl: account\.adminUrl/);
|
||||
});
|
||||
|
||||
test("composes the chat page with the app sidebar shell", () => {
|
||||
const page = readProjectFile("src/app/page.tsx");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user