feat: scaffold TASK-WP6-01 admin operations
Dada P0-A isolated Windows CI / validate-and-package (push) Successful in 10m45s

This commit is contained in:
suyx
2026-08-03 19:15:03 +08:00
parent f1bebab611
commit 19212cc1b6
17 changed files with 1760 additions and 17 deletions
+140
View File
@@ -0,0 +1,140 @@
import { randomUUID } from "node:crypto";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { createApp } from "../../apps/api/src/app.js";
import { RegistrationService } from "../../apps/api/src/registration.js";
import { MockResendAdapter } from "../../apps/api/src/resend-adapter.js";
import { adminOverviewFixture } from "../fixtures/wp6-01-admin-overview.js";
const roots: string[] = [];
const services: RegistrationService[] = [];
const now = Date.parse("2026-08-03T09:30:00.000Z");
const requestHeaders = { host: "127.0.0.1:43121", origin: "http://127.0.0.1:43121" };
function createRegistration() {
const root = mkdtempSync(join(tmpdir(), "dada-wp6-01-api-"));
roots.push(root);
const registration = new RegistrationService({
adminAllowlistPepper: Buffer.alloc(32, 0xd1),
challengePepper: Buffer.alloc(32, 0xd2),
clock: () => now,
currentPrivacyNoticeVersion: "p0a-registration-notice-v1",
databasePath: join(root, "dada.sqlite3"),
invitePepper: Buffer.alloc(32, 0xd3),
resend: new MockResendAdapter(),
sessionPepper: Buffer.alloc(32, 0xd4),
});
services.push(registration);
return registration;
}
function seedSubject(registration: RegistrationService, role: "super_admin" | "user") {
const userId = randomUUID();
registration.database.prepare(`
INSERT INTO users (
user_id, normalized_email, role, status, counts_toward_stage_limit,
registration_id, created_at
) VALUES (?, ?, ?, 'active', ?, ?, ?)
`).run(userId, `${role}-${userId}@example.invalid`, role, role === "user" ? 1 : 0, randomUUID(), now);
if (role === "super_admin") {
registration.database.prepare("INSERT INTO admin_access (user_id, allowed) VALUES (?, 1)").run(userId);
}
return userId;
}
function tableCounts(registration: RegistrationService) {
return {
admin: (registration.database.prepare("SELECT COUNT(*) AS count FROM admin_operation_logs").get() as { count: number }).count,
private: (registration.database.prepare("SELECT COUNT(*) AS count FROM private_content_access_logs").get() as { count: number }).count,
};
}
afterEach(() => {
for (const service of services.splice(0)) service.close();
for (const root of roots.splice(0)) rmSync(root, { force: true, recursive: true });
});
describe("TDD-WP6-ADM-001-role-and-summary", () => {
it("authorizes only an active admin audience and returns a schema-redacted summary", async () => {
const registration = createRegistration();
const adminId = seedSubject(registration, "super_admin");
const ordinaryId = seedSubject(registration, "user");
const previewId = seedSubject(registration, "user");
registration.database.exec("CREATE TABLE asset_preview_grants_fixture (user_id TEXT PRIMARY KEY, status TEXT NOT NULL)");
registration.database.prepare("INSERT INTO asset_preview_grants_fixture (user_id, status) VALUES (?, 'active')").run(previewId);
const adminSession = registration.issueAuthenticatedSession(adminId, "admin");
const ordinarySession = registration.issueAuthenticatedSession(ordinaryId, "user");
const previewSession = registration.issueAuthenticatedSession(previewId, "user");
let providerCalls = 0;
const app = await createApp({
adminOverview: async () => {
providerCalls += 1;
return {
...adminOverviewFixture,
absolute_path: "forbidden-path-trap",
["api" + "_key"]: "forbidden-key-trap",
private_prompt: "forbidden-prompt-trap",
recent_operations: adminOverviewFixture.recent_operations.map((operation) => ({
...operation,
actor_email: "forbidden@example.invalid",
})),
};
},
browserGate: false,
networkBoundary: { allowTestPort: true },
registration,
});
for (const token of [undefined, ordinarySession.sessionToken, previewSession.sessionToken]) {
const response = await app.inject({
headers: token ? { ...requestHeaders, cookie: `dada_admin_session=${token}` } : requestHeaders,
method: "GET",
url: "/api/v1/admin/overview",
});
expect(response.statusCode).toBe(401);
}
expect(providerCalls).toBe(0);
const before = tableCounts(registration);
const allowed = await app.inject({
headers: { ...requestHeaders, cookie: `dada_admin_session=${adminSession.sessionToken}` },
method: "GET",
url: "/api/v1/admin/overview",
});
expect(allowed.statusCode).toBe(200);
expect(allowed.json()).toEqual(adminOverviewFixture);
expect(JSON.stringify(allowed.json())).not.toMatch(/absolute_path|api_key|private_prompt|actor_email|forbidden/i);
expect(providerCalls).toBe(1);
expect(tableCounts(registration)).toEqual(before);
registration.revokeAdminSessions(adminId, "disabled");
const afterDisable = tableCounts(registration);
const revoked = await app.inject({
headers: { ...requestHeaders, cookie: `dada_admin_session=${adminSession.sessionToken}` },
method: "GET",
url: "/api/v1/admin/overview",
});
expect(revoked.statusCode).toBe(401);
expect(providerCalls).toBe(1);
expect(tableCounts(registration)).toEqual(afterDisable);
const evidenceRoot = process.env.DADA_WP6_01_EVIDENCE_DIR;
if (evidenceRoot) {
mkdirSync(evidenceRoot, { recursive: true });
writeFileSync(resolve(evidenceRoot, "response.json"), `${JSON.stringify({
active_admin: allowed.json(),
denied_statuses: { anonymous: 401, ordinary: 401, preview: 401, suspended_admin: revoked.statusCode },
}, null, 2)}\n`);
writeFileSync(resolve(evidenceRoot, "db-access.json"), `${JSON.stringify({
active_read_delta: { admin_operation_logs: 0, private_content_access_logs: 0 },
denied_read_delta: { admin_operation_logs: 0, private_content_access_logs: 0 },
provider_calls: providerCalls,
}, null, 2)}\n`);
}
await app.close();
});
});
+137
View File
@@ -0,0 +1,137 @@
import { mkdirSync } from "node:fs";
import { resolve } from "node:path";
import { expect, test, type Page } from "@playwright/test";
import { createServer, type ViteDevServer } from "vite";
import { adminOverviewFixture } from "../fixtures/wp6-01-admin-overview.js";
let vite: ViteDevServer;
let webUrl: string;
const adminSession = {
acknowledged_private_content_notice_version: null,
admin: { role: "super_admin", status: "active", user_id: "00000000-0000-4000-8000-000000000601" },
audience: "admin",
authenticated: true,
csrf_token: "csrf-admin-shell-fixture-000000000000000000000000000000000",
current_private_content_notice_version: null,
expires_at: "2026-09-02T09:30:00.000Z",
notice_acknowledged: false,
};
const navigation = [
["总览", "/admin"],
["用户与点数", "/admin/users"],
["邀请码", "/admin/invites"],
["模型", "/admin/models"],
["素材", "/admin/assets"],
["内部预览", "/admin/preview"],
["生成记录", "/admin/generations"],
["服务与存储", "/admin/services-storage"],
["审计", "/admin/audit"],
] as const;
test.beforeAll(async () => {
vite = await createServer({
configFile: resolve("apps/web/vite.config.ts"),
root: resolve("apps/web"),
server: { host: "127.0.0.1", port: 0 },
});
await vite.listen();
const address = vite.httpServer?.address();
if (!address || typeof address === "string") throw new Error("Vite did not expose a test port.");
webUrl = `http://127.0.0.1:${address.port}`;
});
test.afterAll(async () => vite.close());
async function routeActiveAdmin(page: Page) {
await page.route("**/api/v1/admin-auth/session", (route) => route.fulfill({
body: JSON.stringify(adminSession),
contentType: "application/json",
status: 200,
}));
await page.route("**/api/v1/admin/overview", (route) => route.fulfill({
body: JSON.stringify(adminOverviewFixture),
contentType: "application/json",
status: 200,
}));
}
test("TDD-WP6-ADM-001-role-and-summary renders the protected nine-entry admin shell", async ({ page }) => {
const requests: string[] = [];
page.on("request", (request) => requests.push(request.url()));
await routeActiveAdmin(page);
await page.goto(`${webUrl}/admin`);
await expect(page.getByRole("heading", { level: 2, name: "运营总览" })).toBeVisible();
const sidebar = page.getByRole("navigation", { name: "后台主导航" });
await expect(sidebar).toBeVisible();
for (const [name, href] of navigation) {
await expect(sidebar.getByRole("link", { name, exact: true })).toHaveAttribute("href", href);
}
expect(Math.round((await sidebar.boundingBox())?.width ?? 0)).toBe(216);
await expect(page.getByText("4 / 10", { exact: true })).toBeVisible();
await expect(page.getByText("待人工核对 1", { exact: true })).toBeVisible();
await expect(page.getByText("85.0%", { exact: true })).toBeVisible();
await expect(page.getByRole("link", { name: "有异常", exact: true })).toBeVisible();
await expect(page.locator("body")).not.toContainText(/forbidden|example\.invalid|api[_ -]?key|完整提示词/i);
expect(requests.some((url) => /prompt|private-content|image-content/i.test(url))).toBe(false);
const evidenceRoot = process.env.DADA_EVIDENCE_DIR_ADMIN;
if (evidenceRoot) {
const screenshotDirectory = resolve(evidenceRoot, "screenshots");
mkdirSync(screenshotDirectory, { recursive: true });
await page.screenshot({ fullPage: true, path: resolve(screenshotDirectory, "admin-overview.png") });
}
});
test("TDD-WP6-ADM-001-role-and-summary keeps ordinary and preview sessions outside admin", async ({ page }) => {
let overviewCalls = 0;
await page.route("**/api/v1/admin-auth/session", (route) => route.fulfill({
body: JSON.stringify({ error: { code: "AUTH_SESSION_INVALID", subject: "preview_user" } }),
contentType: "application/json",
status: 401,
}));
await page.route("**/api/v1/admin/overview", (route) => {
overviewCalls += 1;
return route.fulfill({ body: "null", contentType: "application/json", status: 401 });
});
await page.goto(`${webUrl}/admin`);
await expect(page).toHaveURL(`${webUrl}/admin/login`);
await expect(page.getByRole("heading", { name: "管理员邮箱验证码登录" })).toBeVisible();
expect(overviewCalls).toBe(0);
await expect(page.getByText("DADA ADMIN", { exact: true })).toHaveCount(0);
const evidenceRoot = process.env.DADA_EVIDENCE_DIR_ADMIN;
if (evidenceRoot) {
const screenshotDirectory = resolve(evidenceRoot, "screenshots");
mkdirSync(screenshotDirectory, { recursive: true });
await page.screenshot({ fullPage: true, path: resolve(screenshotDirectory, "admin-denied.png") });
}
});
test("TDD-WP6-ADM-001-role-and-summary ejects a disabled admin when the session is rechecked", async ({ page }) => {
let active = true;
await page.route("**/api/v1/admin-auth/session", (route) => route.fulfill(active ? {
body: JSON.stringify(adminSession),
contentType: "application/json",
status: 200,
} : {
body: JSON.stringify({ error: { code: "AUTH_SESSION_INVALID" } }),
contentType: "application/json",
status: 401,
}));
await page.route("**/api/v1/admin/overview", (route) => route.fulfill({
body: JSON.stringify(adminOverviewFixture),
contentType: "application/json",
status: 200,
}));
await page.goto(`${webUrl}/admin`);
await expect(page.getByRole("heading", { level: 2, name: "运营总览" })).toBeVisible();
active = false;
await page.evaluate(() => window.dispatchEvent(new Event("dada:session-invalid")));
await expect(page).toHaveURL(`${webUrl}/admin/login`);
});
+44
View File
@@ -0,0 +1,44 @@
export const adminOverviewFixture = {
generated_at: "2026-08-03T09:30:00.000Z",
user_slots: {
active_and_suspended: 4,
limit: 10,
},
generation_jobs: {
pending_manual_review: 1,
pending_manual_review_oldest_at: "2026-08-03T09:12:00.000Z",
queued: 2,
running: 1,
},
models: {
configured_default_model_id: "gemini-3.1-flash-image-preview",
configured_model_count: 3,
recommended_model_id: "gemini-3-pro-image-preview",
runtime_available_count: 2,
},
storage: {
last_measured_at: "2026-08-03T09:29:00.000Z",
limit_bytes: 5_368_709_120,
managed_content_bytes: 4_563_402_752,
status: "critical" as const,
},
services: [
{ checked_at: "2026-08-03T09:29:00.000Z", service_id: "resend", status: "available" as const },
{ checked_at: "2026-08-03T09:29:00.000Z", service_id: "amap", status: "available" as const },
{ checked_at: "2026-08-03T09:29:00.000Z", service_id: "ai_gateway", status: "degraded" as const },
{ checked_at: "2026-08-03T09:29:00.000Z", service_id: "worker", status: "available" as const },
{ checked_at: "2026-08-03T09:29:00.000Z", service_id: "asset_root", status: "degraded" as const },
],
recent_operations: [
{
created_at: "2026-08-03T09:20:00.000Z",
operation_id: "00000000-0000-4000-8000-000000000621",
operation_type: "model_configuration_update",
result: "succeeded" as const,
target_ref: "model-config-set:7",
},
],
asset_cleanup: {
pending_jobs: 0,
},
};