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:
53
src/lib/audit.ts
Normal file
53
src/lib/audit.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user