import type { EffectivePermissions } from '@/lib/auth/permissions' /** * Wer darf einen Verlaufseintrag nachträglich ändern, und ist die Änderung * sichtbar zu markieren? Siehe docs/05-akten-und-vorlagen.md. * * Bewusst eine reine Funktion ohne Datenbank: das ist die Regel, an der die * Nachvollziehbarkeit der ganzen Akte hängt, und sie gehört einzeln prüfbar. * Die Karenzzeit wird **serverseitig** aus `AppSetting` gelesen, nie aus dem * Request. */ export type EditPolicyName = 'APPEND_ONLY' | 'GRACE_WINDOW' | 'OPEN' export type EntrySnapshot = { authorId: string type: string version: number createdAt: Date retractedAt: Date | null minClearance: number } export type EditSettings = { entryEditPolicy: EditPolicyName entryEditGraceMinutes: number entryEditMarkGrace: boolean } export type EditDecision = | { ok: true /** Begründung Pflicht (mindestens 10 Zeichen)? */ reasonRequired: boolean /** Bekommt der Eintrag ein sichtbares „bearbeitet · vN"? */ markVisible: boolean withinGrace: boolean } | { ok: false; reason: EditDenial } export type EditDenial = | 'system-entry' | 'retracted' | 'append-only' | 'grace-expired' | 'not-author' /** Systemeinträge sind von jeder Änderung ausgenommen. */ const IMMUTABLE_TYPES = new Set(['SYSTEM', 'STATUS_CHANGE']) export function evaluateEntryEdit( p: EffectivePermissions, entry: EntrySnapshot, settings: EditSettings, now: Date = new Date(), ): EditDecision { if (IMMUTABLE_TYPES.has(entry.type)) return { ok: false, reason: 'system-entry' } if (entry.retractedAt) return { ok: false, reason: 'retracted' } if (settings.entryEditPolicy === 'APPEND_ONLY') return { ok: false, reason: 'append-only' } const isOwn = entry.authorId === p.userId const ageMs = now.getTime() - entry.createdAt.getTime() const withinGrace = settings.entryEditPolicy === 'GRACE_WINDOW' && isOwn && ageMs <= settings.entryEditGraceMinutes * 60_000 // Ab der zweiten Änderung oder bei einem fremden Eintrag ist die Begründung // Pflicht. Sie steht danach in der Historie, die jeder sieht, der den // Eintrag sehen darf. const reasonRequired = entry.version >= 2 || !isOwn if (withinGrace) { // Ein Tippfehler, der zwei Minuten nach dem Absenden korrigiert wird, soll // die Akte nicht mit „bearbeitet"-Marken zupflastern. Versioniert und // protokolliert wird trotzdem. return { ok: true, reasonRequired, markVisible: settings.entryEditMarkGrace, withinGrace } } const needed = isOwn ? 'entry.edit_own' : 'entry.edit_any' if (!p.isAdmin && !p.permissions.has(needed)) { return { ok: false, reason: isOwn ? 'grace-expired' : 'not-author' } } return { ok: true, reasonRequired, markVisible: true, withinGrace: false } } /** Zurückziehen und Wiederherstellen hängen beide am Recht `entry.retract`. */ export function canRetract(p: EffectivePermissions, entry: EntrySnapshot): boolean { if (IMMUTABLE_TYPES.has(entry.type)) return false return p.isAdmin || p.permissions.has('entry.retract') }