import type { Prisma } from '@afis/db' import { db } from '@/lib/db' /** * Zugriffsprotokoll. Zweite, unabhängige Spur neben der Versionstabelle. * * Zwei Wege, aus einem Grund: der Eintrag darf die Antwort nicht blockieren * (docs/09-performance-und-ux.md), aber bei schreibenden Aktionen darf er auch * nicht verloren gehen. * * - `writeAudit` läuft **in derselben Transaktion** wie die Änderung. Schlägt * das Protokoll fehl, schlägt die Änderung fehl. * - `recordRead` sammelt Lesezugriffe im Prozess und schreibt sie alle zwei * Sekunden gebündelt weg. Ein verlorener Lesevermerk nach einem Absturz ist * hinnehmbar, eine um 40 ms langsamere Aktenansicht nicht. */ export type AuditAction = | 'case.read' | 'case.create' | 'case.update' | 'case.reclassify' | 'case.status' | 'entry.create' | 'entry.edit' | 'entry.retract' | 'entry.restore' | 'case.participant.add' | 'case.participant.remove' | 'case.link' | 'case.unlink' | 'personnel.read' | 'personnel.create' | 'personnel.update' | 'personnel.reclassify' | 'personnel.assign' | 'personnel.unassign' | 'document.read' | 'document.create' | 'document.update' | 'document.finalize' | 'document.retract' | 'document.reclassify' | 'attachment.upload' | 'attachment.read' | 'attachment.release' | 'chat.adopt' | 'channel.create' | 'roles.update' | 'export.pdf' | 'export.approve' | 'export.reject' | 'export.download' | 'export.database' export type AuditInput = { actorId: string action: AuditAction targetType: string targetId: string clearanceAtTime: number meta?: Prisma.InputJsonValue } /** Ab dieser Stufe wird auch das blosse Lesen protokolliert (harte Regel 3). */ export const READ_AUDIT_FROM_LEVEL = 3 type Client = Pick /** Schreibende Aktionen: synchron, in der Transaktion des Aufrufers. */ export async function writeAudit(client: Client, input: AuditInput): Promise { await client.auditLog.create({ data: { actorId: input.actorId, action: input.action, targetType: input.targetType, targetId: input.targetId, clearanceAtTime: input.clearanceAtTime, meta: input.meta, }, }) } const buffer: AuditInput[] = [] let flushTimer: NodeJS.Timeout | null = null async function flush(): Promise { flushTimer = null if (buffer.length === 0) return const batch = buffer.splice(0, buffer.length) try { await db.auditLog.createMany({ data: batch.map((entry) => ({ actorId: entry.actorId, action: entry.action, targetType: entry.targetType, targetId: entry.targetId, clearanceAtTime: entry.clearanceAtTime, meta: entry.meta, })), }) } catch (error) { console.error('[audit] Bündel nicht geschrieben', error) } } /** * Lesezugriff vormerken. Nur ab Stufe 3 aufrufen, sonst füllt sich das * Protokoll mit Belanglosigkeiten und die interessanten Zeilen gehen unter. */ export function recordRead(input: AuditInput): void { buffer.push(input) flushTimer ??= setTimeout(() => void flush(), 2000) // Der Prozess soll nicht wegen des Protokolls offen bleiben. flushTimer.unref?.() } /** Für Tests und für das geordnete Herunterfahren. */ export async function flushAuditBuffer(): Promise { if (flushTimer) { clearTimeout(flushTimer) flushTimer = null } await flush() }