feat(bereiche): Bereiche verwalten mit Rollenprüfung und Audit

Bereiche mit Farbe, Arbeitsfenster und Tageskapazität – die erste
schreibende Funktion und damit der Beleg, dass die Kette trägt: zod prüft
jede Eingabe, die Rolle wird in der Seite und noch einmal in der Server
Action geprüft, jede Änderung hinterlässt einen Audit-Eintrag.

Die Audit-Details werden vor dem Speichern gesäubert: Felder, deren Name
nach Passwort, Token, Betreff oder Mailinhalt aussieht, werden ersetzt.
Das gilt auch in verschachtelten Objekten und Listen.

Dazu die Protokollseite für die Verwaltung und die Startseite.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-10 13:02:49 +02:00
co-authored by Claude Opus 5
parent cc013fd6a1
commit e9a2b4b463
14 changed files with 1432 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
import 'server-only'
import type { Prisma } from '@/generated/prisma/client'
import { auditDetails } from '@/lib/audit'
import { db } from '@/server/db'
import { aktuelleInstanz } from '@/server/instanz'
/**
* Nachweis jeder schreibenden Aktion. Die Details werden vorher gesaeubert
* (siehe @/lib/audit), damit keine Geheimnisse in der Datenbank landen.
*/
export type AuditAkteur = {
id: string | null
email: string
}
export type AuditEingabe = {
akteur: AuditAkteur
/** Was geschah, in der Form "objekt.verb", z. B. "bereich.angelegt". */
aktion: string
objektTyp: string
objektId?: string | null
details?: Record<string, unknown> | undefined
}
/**
* Schreibt einen Audit-Eintrag. Schlaegt das fehl, darf die eigentliche
* Aktion nicht als geglueckt gelten – deshalb wird der Fehler weitergereicht
* und nicht verschluckt.
*/
export async function auditSchreiben(eingabe: AuditEingabe): Promise<void> {
const details = auditDetails(eingabe.details)
await db.auditEintrag.create({
data: {
benutzerId: eingabe.akteur.id,
benutzerEmail: eingabe.akteur.email,
aktion: eingabe.aktion,
objektTyp: eingabe.objektTyp,
objektId: eingabe.objektId ?? null,
...(details ? { details: details as Prisma.InputJsonObject } : {}),
instanz: aktuelleInstanz(),
},
})
}