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
+77
View File
@@ -0,0 +1,77 @@
import type { Metadata } from 'next'
import { PasskeyVerwaltung } from '@/app/(app)/einstellungen/passkey-verwaltung'
import {
Card,
CardBeschreibung,
CardInhalt,
CardKopf,
CardTitel,
} from '@/components/ui/card'
import { ROLLEN_NAME } from '@/lib/rollen'
import { alsDatumUhrzeit } from '@/lib/zeit'
import { verlangeAnmeldung } from '@/server/berechtigung'
import { db } from '@/server/db'
export const metadata: Metadata = { title: 'Einstellungen' }
export default async function EinstellungenSeite() {
const benutzer = await verlangeAnmeldung('/einstellungen')
const passkeys = await db.passkey.findMany({
where: { benutzerId: benutzer.id },
orderBy: { erstelltAm: 'asc' },
select: {
id: true,
bezeichnung: true,
erstelltAm: true,
zuletztBenutzt: true,
gesichert: true,
},
})
return (
<div className="flex flex-col gap-8">
<header>
<h1 className="text-2xl font-semibold tracking-tight">Einstellungen</h1>
</header>
<Card>
<CardKopf>
<CardTitel className="text-base">Konto</CardTitel>
</CardKopf>
<CardInhalt>
<dl className="grid gap-2 text-sm sm:grid-cols-[10rem_1fr]">
<dt className="text-gedaempft-vordergrund">Name</dt>
<dd>{benutzer.name ?? '—'}</dd>
<dt className="text-gedaempft-vordergrund">E-Mail</dt>
<dd>{benutzer.email}</dd>
<dt className="text-gedaempft-vordergrund">Rolle</dt>
<dd>{ROLLEN_NAME[benutzer.rolle]}</dd>
</dl>
</CardInhalt>
</Card>
<Card>
<CardKopf>
<CardTitel className="text-base">Passkeys</CardTitel>
<CardBeschreibung>
Mit einem Passkey meldest du dich per Fingerabdruck, Gesicht oder Geräte-PIN
an – ohne Passwort. Hinterlege am besten zwei, damit du bei Verlust eines
Geräts nicht ausgesperrt bist.
</CardBeschreibung>
</CardKopf>
<CardInhalt>
<PasskeyVerwaltung
passkeys={passkeys.map((p) => ({
id: p.id,
bezeichnung: p.bezeichnung,
erstelltAm: alsDatumUhrzeit(p.erstelltAm),
zuletztBenutzt: p.zuletztBenutzt ? alsDatumUhrzeit(p.zuletztBenutzt) : null,
gesichert: p.gesichert,
}))}
/>
</CardInhalt>
</Card>
</div>
)
}