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

53
src/lib/audit.ts Normal file
View File

@@ -0,0 +1,53 @@
import type { PgTransaction } from "drizzle-orm/pg-core";
import type { ExtractTablesWithRelations } from "drizzle-orm";
import type { NodePgQueryResultHKT } from "drizzle-orm/node-postgres";
import { db } from "@/db";
import * as schema from "@/db/schema";
import { auditLog } from "@/db/schema";
/**
* Transaktionstyp des Drizzle-pg-Clients (kein `any`). Server Actions, die in
* einer Transaktion schreiben, übergeben ihre `tx` an `writeAudit`, damit das
* Audit-Insert Teil derselben atomaren Operation ist.
*/
export type Tx = PgTransaction<
NodePgQueryResultHKT,
typeof schema,
ExtractTablesWithRelations<typeof schema>
>;
/** Audit-fähiges Ziel-Objekt. `zielId` ist eine UUID (Schema-Spalte). */
export type AuditZielTyp =
| "brigade"
| "user"
| "merkmal"
| "vehicle"
| "equipment"
| "vehicle_template"
| "equipment_category";
/**
* EINE Signatur (Querschnittsstandard 6) für alle Schreib-Actions. Mit
* optionalem `tx`: ohne `tx` läuft das Insert auf dem Pool-`db`.
*
* `details` ist serialisierbares JSON (kein PII über das Nötige hinaus). Der
* `actorUserId` referenziert `users.id` (FK set-null), sodass die Historie auch
* nach Benutzerlöschung erhalten bleibt.
*/
export async function writeAudit(
actorUserId: string | null,
aktion: string,
zielTyp: AuditZielTyp | null,
zielId: string | null,
details?: Record<string, unknown>,
tx?: Tx,
): Promise<void> {
const exec = tx ?? db;
await exec.insert(auditLog).values({
actorUserId: actorUserId ?? null,
aktion,
zielTyp,
zielId,
details: details ?? null,
});
}