feat(protokoll): Audit-Log mit Filtern, Kettenpruefung und CSV (#027)
Das Protokoll war bisher nur in der Datenbank lesbar. Die Ansicht zeigt es jeder Rolle mit lesbaren Aktionstexten, aufklappbarem Diff, Details und Hashes; ein Test haelt fest, dass jede Aktion im Code einen Text hat. Geblaettert wird ueber die ID statt OFFSET, weil das Protokoll waehrend des Lesens waechst. Die Kettenpruefung liest jede Zeile und laeuft deshalb nur auf Knopfdruck, mit 5 Minuten Zwischenspeicher; ein Bruch wird selbst protokolliert. Der CSV-Export streamt in Stapeln, entschaerft Formeln und steht selbst im Protokoll. protokollieren() reicht jeden Eintrag nach dem Schreiben an Syslog weiter, ohne Details und Diff. Kein Weg aendert oder loescht Eintraege; ein struktureller Test sichert das ab, zusaetzlich zum Trigger. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
"use server";
|
||||
|
||||
import { cursorAus, protokollFilterAus } from "@/lib/protokoll-filter";
|
||||
import { RechteFehler, rechtErforderlich } from "@/server/auth/sitzungspflicht";
|
||||
import { protokollSeite, type ProtokollSeite } from "@/server/queries/protokoll";
|
||||
import { kettePruefenZwischengespeichert } from "@/server/services/protokoll";
|
||||
|
||||
/**
|
||||
* Protokollansicht (#027): nur lesende Aktionen. Bewusst gibt es hier nichts, was einen
|
||||
* Eintrag aendert oder loescht – das verbietet ohnehin der Trigger, und
|
||||
* `tests/protokoll-ansicht.test.ts` haelt fest, dass auch kein Code es versucht.
|
||||
*/
|
||||
|
||||
export type KettenAntwort = {
|
||||
readonly fehlerhafteId?: string | null;
|
||||
readonly eintraege?: number;
|
||||
readonly geprueftAm?: string;
|
||||
readonly fehler?: string;
|
||||
};
|
||||
|
||||
export async function kettePruefenAktion(): Promise<KettenAntwort> {
|
||||
try {
|
||||
const { benutzer } = await rechtErforderlich("lesen");
|
||||
const ergebnis = await kettePruefenZwischengespeichert({ id: benutzer.id, name: benutzer.name });
|
||||
return {
|
||||
fehlerhafteId: ergebnis.fehlerhafteId,
|
||||
eintraege: ergebnis.eintraege,
|
||||
geprueftAm: ergebnis.geprueftAm.toISOString(),
|
||||
};
|
||||
} catch (fehler) {
|
||||
if (fehler instanceof RechteFehler) return { fehler: fehler.message };
|
||||
throw fehler;
|
||||
}
|
||||
}
|
||||
|
||||
/** „Ältere laden“: dieselben GET-Filter wie die Seite, Cursor ist die kleinste gezeigte ID. */
|
||||
export async function aeltereLadenAktion(
|
||||
query: string,
|
||||
vor: string,
|
||||
): Promise<ProtokollSeite | { fehler: string }> {
|
||||
try {
|
||||
await rechtErforderlich("lesen");
|
||||
} catch (fehler) {
|
||||
if (fehler instanceof RechteFehler) return { fehler: fehler.message };
|
||||
throw fehler;
|
||||
}
|
||||
const cursor = cursorAus(String(vor));
|
||||
if (cursor === undefined) return { zeilen: [], weiterVor: null };
|
||||
const filter = protokollFilterAus(Object.fromEntries(new URLSearchParams(String(query).slice(0, 2000))));
|
||||
return protokollSeite(filter, cursor);
|
||||
}
|
||||
Reference in New Issue
Block a user