101 lines
3.6 KiB
TypeScript
101 lines
3.6 KiB
TypeScript
// Duenner Client der AUD-03-Export-API (internal/audit.ExportHandler) —
|
|
// keine eigene Aggregations-/Filterlogik im Frontend (Ticket-Vorgabe:
|
|
// "reiner Konsument der Export-API"). Die JSON-Lines-Antwort wird nur
|
|
// dekodiert, nicht neu berechnet oder gefiltert.
|
|
export type AuditRecord = {
|
|
occurred_at: string;
|
|
tenant_slug: string;
|
|
actor: string;
|
|
action: string;
|
|
target: string;
|
|
metadata: unknown;
|
|
};
|
|
|
|
export type AuditFilter = {
|
|
tenant: string;
|
|
actor: string;
|
|
action: string;
|
|
from: string; // RFC3339, leer = kein Filter
|
|
to: string;
|
|
};
|
|
|
|
// QA-15 F-09: das Admin-Token geht seither als Header, nicht mehr als
|
|
// Query-Parameter (Zugriffsprotokolle, Browserverlauf, Proxy-Logs) — muss
|
|
// mit internal/audit.ExportTokenHeader (= internal/svcauth.Header)
|
|
// uebereinstimmen.
|
|
const TOKEN_HEADER = "X-Service-Token";
|
|
|
|
function apiBase(): string {
|
|
const base = process.env.NEXT_PUBLIC_AUDITLOG_API_URL;
|
|
if (!base) {
|
|
throw new Error(
|
|
"NEXT_PUBLIC_AUDITLOG_API_URL ist nicht gesetzt (Umgebungsvariable erforderlich)"
|
|
);
|
|
}
|
|
return base;
|
|
}
|
|
|
|
function buildParams(filter: AuditFilter, format: "json" | "csv"): URLSearchParams {
|
|
const params = new URLSearchParams({ format });
|
|
if (filter.tenant) params.set("tenant", filter.tenant);
|
|
if (filter.actor) params.set("actor", filter.actor);
|
|
if (filter.action) params.set("action", filter.action);
|
|
if (filter.from) params.set("from", filter.from);
|
|
if (filter.to) params.set("to", filter.to);
|
|
return params;
|
|
}
|
|
|
|
// exportURL liefert die Adresse desselben Endpunkts, den auch die
|
|
// Export-Datei-Funktion (Akzeptanzkriterium 2) verwendet — Liste und Export
|
|
// sind bewusst DERSELBE API-Aufruf mit unterschiedlichem "format". Enthaelt
|
|
// (seit QA-15 F-09) KEIN Token mehr — das geht ausschliesslich per Header.
|
|
function exportURL(filter: AuditFilter, format: "json" | "csv"): string {
|
|
return `${apiBase()}/audit/export?${buildParams(filter, format)}`;
|
|
}
|
|
|
|
async function fetchExport(caller: string, filter: AuditFilter, format: "json" | "csv"): Promise<Response> {
|
|
const res = await fetch(exportURL(filter, format), {
|
|
cache: "no-store",
|
|
headers: { [TOKEN_HEADER]: caller },
|
|
});
|
|
if (!res.ok) {
|
|
const text = await res.text().catch(() => "");
|
|
throw new Error(text || `Anfrage fehlgeschlagen (${res.status})`);
|
|
}
|
|
return res;
|
|
}
|
|
|
|
// fetchRecords laedt die Liste als JSON-Lines und dekodiert Zeile fuer
|
|
// Zeile — dieselben Filterparameter wie ein direkter API-Aufruf
|
|
// (Akzeptanzkriterium 1 / Pruefung 1).
|
|
export async function fetchRecords(caller: string, filter: AuditFilter): Promise<AuditRecord[]> {
|
|
const res = await fetchExport(caller, filter, "json");
|
|
const text = await res.text();
|
|
return text
|
|
.split("\n")
|
|
.filter((line) => line.trim() !== "")
|
|
.map((line) => JSON.parse(line) as AuditRecord);
|
|
}
|
|
|
|
// downloadExport laedt die Exportdatei per Header-authentifiziertem Abruf
|
|
// und loest ueber einen temporaeren Objekt-URL denselben Datei-Download aus
|
|
// wie zuvor eine direkte Browser-Navigation — die brauchte QA-15 F-09 nicht
|
|
// mehr moeglich zu sein, weil ein <a href>/window.open()-Aufruf keinen
|
|
// eigenen Header setzen kann (Akzeptanzkriterium 2: Export weiterhin direkt
|
|
// als Datei ausloesbar).
|
|
export async function downloadExport(caller: string, filter: AuditFilter, format: "csv" | "json"): Promise<void> {
|
|
const res = await fetchExport(caller, filter, format);
|
|
const blob = await res.blob();
|
|
const url = URL.createObjectURL(blob);
|
|
try {
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = `audit-export.${format}`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
a.remove();
|
|
} finally {
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
}
|