Workstream 6: Admin-Panel — Taxonomie & Bereitstellung (Phase 4)

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>
This commit is contained in:
Matthias Hochmeister
2026-06-09 10:30:52 +02:00
parent 0a7173ef38
commit e97e16d254
49 changed files with 3676 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
import { test, expect } from "@playwright/test";
/**
* Admin-Gating (Workstream 6, Querschnittsstandard 13, 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();
});
});