Basis: Certvia dev@a48c5fb als Fundament für Craftvia
Unveränderter Stand von certvia/dev (a48c5fb) plus Craftvia-Spezifikation und Brandbook unter docs/craftvia/. ISMS-Module werden im Folgecommit entfernt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
import { Modal } from "@/components/modal";
|
||||
import { Pill } from "@/components/mockup-ui";
|
||||
|
||||
/**
|
||||
* Wiederverwendbarer Audit-Trail (Aktivitätsprotokoll) als server-gerendertes
|
||||
* Popup — gleiches Muster wie die übrigen Popups (searchParam öffnet/schließt,
|
||||
* kein Client-State). Wird an zwei Stellen eingebunden:
|
||||
* - Mandanten-Einstellungen (/settings?audit=1): eigener Mandant, RLS-gefiltert.
|
||||
* - Plattform-Konsole je Mandant (/admin/[id]?audit=1): Superadmin liest den
|
||||
* Trail des jeweiligen Mandanten cross-tenant über den Owner-Client.
|
||||
* Die Zeilen werden bereits gefiltert/aufbereitet übergeben — die Komponente
|
||||
* rendert nur; das hält den Datenpfad (Mandant vs. Plattform) in der Seite.
|
||||
*/
|
||||
|
||||
export type AuditRow = {
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
actorId: string | null;
|
||||
action: string;
|
||||
entity: string;
|
||||
entityId: string | null;
|
||||
scope: string;
|
||||
};
|
||||
|
||||
const ACTION_LABEL: Record<string, string> = {
|
||||
create: "Erstellt",
|
||||
update: "Geändert",
|
||||
delete: "Gelöscht",
|
||||
login: "Anmeldung",
|
||||
logout: "Abmeldung",
|
||||
denied: "Abgelehnt",
|
||||
export: "Export",
|
||||
import: "Import",
|
||||
approve: "Freigegeben",
|
||||
reject: "Zurückgewiesen",
|
||||
};
|
||||
|
||||
const ACTION_TONE: Record<string, "ok" | "warn" | "mut" | "info"> = {
|
||||
create: "ok",
|
||||
update: "info",
|
||||
approve: "ok",
|
||||
delete: "warn",
|
||||
denied: "warn",
|
||||
reject: "warn",
|
||||
login: "mut",
|
||||
logout: "mut",
|
||||
};
|
||||
|
||||
const ENTITY_LABEL: Record<string, string> = {
|
||||
asset: "Asset",
|
||||
process: "Prozess",
|
||||
risk: "Risiko",
|
||||
measure: "Maßnahme",
|
||||
supplier: "Lieferant",
|
||||
software: "Software",
|
||||
project: "Projekt",
|
||||
service: "IT-Dienst",
|
||||
policy: "Richtlinie",
|
||||
task: "Aufgabe",
|
||||
user: "Benutzer",
|
||||
role: "Rolle",
|
||||
tenant: "Mandant",
|
||||
module: "Modul",
|
||||
session: "Sitzung",
|
||||
bia: "BIA",
|
||||
platformAdmin: "Plattform-Admin",
|
||||
};
|
||||
|
||||
const fmt = new Intl.DateTimeFormat("de-DE", { dateStyle: "medium", timeStyle: "short" });
|
||||
|
||||
function actionLabel(a: string) {
|
||||
return ACTION_LABEL[a] ?? a.charAt(0).toUpperCase() + a.slice(1);
|
||||
}
|
||||
function entityLabel(e: string) {
|
||||
return ENTITY_LABEL[e] ?? e;
|
||||
}
|
||||
|
||||
export function AuditTrailModal({
|
||||
rows,
|
||||
actorNames,
|
||||
closeHref,
|
||||
sub,
|
||||
}: {
|
||||
rows: AuditRow[];
|
||||
actorNames: Record<string, string>;
|
||||
closeHref: string;
|
||||
sub?: string;
|
||||
}) {
|
||||
return (
|
||||
<Modal
|
||||
title="Audit-Trail"
|
||||
sub={sub ?? "Nachvollziehbare Aktivitäten (neueste zuerst)"}
|
||||
closeHref={closeHref}
|
||||
closeLabel="Schließen"
|
||||
>
|
||||
<div className="max-h-[65vh] overflow-y-auto p-5">
|
||||
{rows.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">Noch keine Einträge protokolliert.</p>
|
||||
) : (
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b text-left text-[12px] text-muted-foreground">
|
||||
<th className="py-2 pr-3 font-semibold">Zeitpunkt</th>
|
||||
<th className="py-2 pr-3 font-semibold">Akteur</th>
|
||||
<th className="py-2 pr-3 font-semibold">Aktion</th>
|
||||
<th className="py-2 pr-3 font-semibold">Objekt</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((r) => (
|
||||
<tr key={r.id} className="border-b last:border-0 align-top">
|
||||
<td className="whitespace-nowrap py-2 pr-3 text-xs text-muted-foreground">
|
||||
{fmt.format(r.createdAt)}
|
||||
</td>
|
||||
<td className="py-2 pr-3">
|
||||
{r.actorId ? actorNames[r.actorId] ?? "System" : "System"}
|
||||
</td>
|
||||
<td className="py-2 pr-3">
|
||||
<Pill tone={ACTION_TONE[r.action] ?? "mut"}>{actionLabel(r.action)}</Pill>
|
||||
</td>
|
||||
<td className="py-2 pr-3">
|
||||
<span className="font-medium">{entityLabel(r.entity)}</span>
|
||||
{r.entityId && (
|
||||
<span className="ml-1 text-[11px] text-muted-foreground">
|
||||
#{r.entityId.slice(0, 8)}
|
||||
</span>
|
||||
)}
|
||||
{r.scope === "platform" && (
|
||||
<span className="ml-1 text-[11px] text-muted-foreground">· Plattform</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user