Platform-Admin-only Oberflächen und Domänenlogik: - codes.ts erweitert um allradCode/normalizeCode/codesMatch (Allrad-Infix kanonisch; Suche importiert weiterhin expandNameQuery). Pure-Unit-Tests. - slug.ts (Idempotenz-Key-Erzeugung) + Tests. - audit.ts: writeAudit mit EINER Signatur und optionalem typisierten tx. - provisioning.ts: createBrigadeWithFirstAdmin (Geocoding inline, argon2id, Audit brigade.create/user.create) + resetUserPassword (Audit user.reset). - Zod-Validierung: merkmal/template/equipment-category/brigade (+ Tests). - Server Actions (jede mit Guard als erster Anweisung, default-deny): merkmale (CRUD, Delete blockiert bei Referenz), proposals (promote/merge mit Typ-Kompatibilität), templates (Merkmale/Vorgabewerte/Aliasse), equipment- categories, brigades (Bereitstellung/Reset). Audit in allen Schreib-Actions. - (admin)-Route-Group: Layout mit requirePlatformAdmin als erster Zeile, AdminNav, DataTable, loading/error; Seiten für Merkmale (+Editor), Vorschläge (Merge), Vorlagen (+Detail mit Merkmal-/Alias-Editor und Allrad-Hinweis), Geräte-Kategorien (+Detail), Wehren (Liste/neu/Detail mit Passwort-Reset), paginierter Audit-Viewer mit Filter. Jede Seite ruft zusätzlich den Guard. - i18n: admin-Strings in zentraler de.ts. - Playwright-Specs (deferred, nicht ausgeführt): admin-gating, admin-merkmal-proposal, admin-brigade-provision. Schema NICHT neu definiert — nur importiert. codes.ts ist hier Eigentümer. Offline-Verifikation: tsc --noEmit grün; eslint grün; vitest run grün (119 passed, 7 DB-roundtrip skipped); next build Exit 0; drizzle-kit check ok. DB-/Server-/Browser-abhängige Schritte deferred (kein Postgres/Server im Sandbox). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
74 lines
2.3 KiB
TypeScript
74 lines
2.3 KiB
TypeScript
import { test, expect } from "@playwright/test";
|
||
|
||
/**
|
||
* Admin-Gating (Workstream 6, Querschnittsstandard 1–3, default-deny dreifach).
|
||
*
|
||
* NICHT in der Sandbox ausführbar (kein Server/DB) — deferred. Wird über einen
|
||
* laufenden Server ausgeführt. Erwartet:
|
||
* - anonym: jede /admin/*-Seite -> Redirect auf /login.
|
||
* - wehr_admin / wehr_read: jede /admin/*-Seite -> 403 (forbidden()).
|
||
* - platform_admin: /admin erreichbar.
|
||
*
|
||
* Negativ-Probe: Entfernen von `requirePlatformAdmin()` aus (admin)/layout.tsx
|
||
* muss diese Suite rot machen.
|
||
*/
|
||
|
||
const ADMIN_PAGES = [
|
||
"/admin",
|
||
"/admin/merkmale",
|
||
"/admin/merkmale/proposals",
|
||
"/admin/vorlagen",
|
||
"/admin/geraete-kategorien",
|
||
"/admin/wehren",
|
||
"/admin/wehren/neu",
|
||
"/admin/audit",
|
||
];
|
||
|
||
test.describe("Admin: anonym -> Redirect auf /login", () => {
|
||
test.use({ storageState: { cookies: [], origins: [] } });
|
||
for (const path of ADMIN_PAGES) {
|
||
test(`anonymer Aufruf von ${path} leitet auf /login um`, async ({
|
||
page,
|
||
}) => {
|
||
await page.goto(path);
|
||
await expect(page).toHaveURL(/\/login/);
|
||
});
|
||
}
|
||
});
|
||
|
||
// Diese Projekte setzen einen storageState eines wehr_admin/wehr_read-Kontos
|
||
// voraus (Test-Workstream stellt die Fixtures bereit). Hier dokumentiert als
|
||
// erwartetes Verhalten; das tatsächliche Konto wird über --project gewählt.
|
||
test.describe("Admin: falsche Rolle -> 403", () => {
|
||
test.skip(
|
||
!process.env.E2E_WEHR_ADMIN_STATE,
|
||
"benötigt wehr_admin-Fixture (Test-Workstream)",
|
||
);
|
||
test.use({
|
||
storageState: process.env.E2E_WEHR_ADMIN_STATE ?? { cookies: [], origins: [] },
|
||
});
|
||
for (const path of ADMIN_PAGES) {
|
||
test(`wehr_admin-Aufruf von ${path} -> 403`, async ({ page }) => {
|
||
const res = await page.goto(path);
|
||
expect(res?.status()).toBe(403);
|
||
});
|
||
}
|
||
});
|
||
|
||
test.describe("Admin: platform_admin -> erreichbar", () => {
|
||
test.skip(
|
||
!process.env.E2E_PLATFORM_ADMIN_STATE,
|
||
"benötigt platform_admin-Fixture (Test-Workstream)",
|
||
);
|
||
test.use({
|
||
storageState:
|
||
process.env.E2E_PLATFORM_ADMIN_STATE ?? { cookies: [], origins: [] },
|
||
});
|
||
test("/admin ist als platform_admin erreichbar", async ({ page }) => {
|
||
await page.goto("/admin");
|
||
await expect(
|
||
page.getByRole("heading", { name: "Administration" }),
|
||
).toBeVisible();
|
||
});
|
||
});
|