feat: add verifiable audit evidence bundles
This commit is contained in:
@@ -45,6 +45,30 @@ export type AuditAdminDeltaResponse = AuditAdminListResponse & {
|
||||
full: boolean;
|
||||
};
|
||||
|
||||
export type EvidenceBundleExportRequest = {
|
||||
scope: "tenant" | "system" | "all";
|
||||
tenant_id?: string | null;
|
||||
record_ids?: string[];
|
||||
max_records?: number;
|
||||
sign?: boolean;
|
||||
};
|
||||
|
||||
export type EvidenceBundleResponse = {
|
||||
id: string;
|
||||
scope: "tenant" | "system" | "all";
|
||||
tenant_id?: string | null;
|
||||
status: "pending" | "ready" | "failed";
|
||||
bundle_sha256?: string | null;
|
||||
record_count: number;
|
||||
reference_count: number;
|
||||
generated_at?: string | null;
|
||||
download_url?: string | null;
|
||||
};
|
||||
|
||||
export type EvidenceBundleDownloadResponse = {
|
||||
bundle: Record<string, unknown>;
|
||||
};
|
||||
|
||||
function auditQuery(options: AuditQueryOptions & { since?: string | null } = {}): string {
|
||||
const params = new URLSearchParams();
|
||||
if (options.tenantId) params.set("tenant_id", options.tenantId);
|
||||
@@ -72,3 +96,20 @@ export function fetchAdminAudit(settings: ApiSettings, options: AuditQueryOption
|
||||
export function fetchAdminAuditDelta(settings: ApiSettings, options: AuditQueryOptions & { since?: string | null } = {}): Promise<AuditAdminDeltaResponse> {
|
||||
return apiFetch(settings, `/api/v1/admin/audit/delta${auditQuery(options)}`);
|
||||
}
|
||||
|
||||
export function exportAuditEvidenceBundle(
|
||||
settings: ApiSettings,
|
||||
payload: EvidenceBundleExportRequest
|
||||
): Promise<EvidenceBundleResponse> {
|
||||
return apiFetch(settings, "/api/v1/admin/audit/evidence-bundles", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
|
||||
export function downloadAuditEvidenceBundle(
|
||||
settings: ApiSettings,
|
||||
bundleId: string
|
||||
): Promise<EvidenceBundleDownloadResponse> {
|
||||
return apiFetch(settings, `/api/v1/admin/audit/evidence-bundles/${bundleId}/download`);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Dialog,
|
||||
DocumentationHelpLink,
|
||||
formatAdminDateTime as formatDateTime,
|
||||
hasScope,
|
||||
i18nMessage,
|
||||
mergeDeltaRows,
|
||||
TableActionGroup,
|
||||
@@ -18,7 +19,14 @@ import {
|
||||
type DataGridColumn,
|
||||
type DataGridQueryState
|
||||
} from "@govoplan/core-webui";
|
||||
import { fetchAdminAudit, fetchAdminAuditDelta, type AuditAdminItem, type AuditSortBy } from "../../api/audit";
|
||||
import {
|
||||
downloadAuditEvidenceBundle,
|
||||
exportAuditEvidenceBundle,
|
||||
fetchAdminAudit,
|
||||
fetchAdminAuditDelta,
|
||||
type AuditAdminItem,
|
||||
type AuditSortBy
|
||||
} from "../../api/audit";
|
||||
|
||||
type Props = {
|
||||
settings: ApiSettings;
|
||||
@@ -38,6 +46,8 @@ const I18N = {
|
||||
close: "i18n:govoplan-audit.close.f1a20804",
|
||||
details: "i18n:govoplan-audit.details.f1a20805",
|
||||
eventDetails: "i18n:govoplan-audit.audit_event_details.f1a20806",
|
||||
exportEvidence: "i18n:govoplan-audit.export_page_evidence.f1a20822",
|
||||
exportingEvidence: "i18n:govoplan-audit.audit_evidence_export_is_in_progress.f1a20823",
|
||||
inspect: "i18n:govoplan-audit.inspect_audit_event.f1a20807",
|
||||
loading: "i18n:govoplan-audit.audit_evidence_is_loading.f1a20808",
|
||||
noDetails: "i18n:govoplan-audit.no_additional_details_were_recorded.f1a20809",
|
||||
@@ -74,7 +84,12 @@ export default function AdminAuditPanel({ settings, auth, systemMode = false }:
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
const [reloadToken, setReloadToken] = useState(0);
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const tenantId = (auth.active_tenant ?? auth.tenant).id;
|
||||
const canExport = hasScope(
|
||||
auth,
|
||||
systemMode ? "audit:system_evidence:export" : "audit:evidence:export"
|
||||
);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -144,6 +159,29 @@ export default function AdminAuditPanel({ settings, auth, systemMode = false }:
|
||||
});
|
||||
}, []);
|
||||
|
||||
const exportPageEvidence = useCallback(async () => {
|
||||
setExporting(true);
|
||||
setError("");
|
||||
try {
|
||||
const created = await exportAuditEvidenceBundle(settings, {
|
||||
scope: systemMode ? "system" : "tenant",
|
||||
tenant_id: systemMode ? null : tenantId,
|
||||
record_ids: items.map((item) => item.id),
|
||||
max_records: Math.max(1, items.length),
|
||||
sign: false
|
||||
});
|
||||
const downloaded = await downloadAuditEvidenceBundle(settings, created.id);
|
||||
downloadJson(
|
||||
downloaded.bundle,
|
||||
`govoplan-audit-evidence-${created.id}.json`
|
||||
);
|
||||
} catch (err) {
|
||||
setError(adminErrorMessage(err));
|
||||
} finally {
|
||||
setExporting(false);
|
||||
}
|
||||
}, [items, settings, systemMode, tenantId]);
|
||||
|
||||
const columns = useMemo<DataGridColumn<AuditAdminItem>[]>(() => [
|
||||
{ id: "time", header: I18N.time, width: 190, minWidth: 150, maxWidth: 260, resizable: true, sticky: "start", sortable: true, filterable: true, filterType: "date", value: (row) => row.created_at, render: (row) => formatDateTime(row.created_at) },
|
||||
{ id: "actor", header: I18N.actor, width: 220, minWidth: 170, maxWidth: 360, resizable: true, sortable: true, filterable: true, value: (row) => row.actor_email || "System", render: (row) => row.actor_email || I18N.system },
|
||||
@@ -175,6 +213,14 @@ export default function AdminAuditPanel({ settings, auth, systemMode = false }:
|
||||
error={error}
|
||||
actions={(
|
||||
<>
|
||||
{canExport && (
|
||||
<Button
|
||||
onClick={() => { void exportPageEvidence(); }}
|
||||
disabled={loading || exporting || items.length === 0}
|
||||
disabledReason={exporting ? I18N.exportingEvidence : undefined}>
|
||||
{I18N.exportEvidence}
|
||||
</Button>
|
||||
)}
|
||||
<DocumentationHelpLink
|
||||
reference={{
|
||||
topicId: "audit.read-authorized-evidence",
|
||||
@@ -243,6 +289,16 @@ export default function AdminAuditPanel({ settings, auth, systemMode = false }:
|
||||
);
|
||||
}
|
||||
|
||||
function downloadJson(value: unknown, filename: string): void {
|
||||
const blob = new Blob([`${JSON.stringify(value, null, 2)}\n`], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = url;
|
||||
anchor.download = filename;
|
||||
anchor.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function auditDetailRows(details: Record<string, unknown>): AuditDetailRow[] {
|
||||
return Object.entries(details)
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
|
||||
@@ -22,7 +22,9 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-audit.tenant_context.f1a20818": "Tenant context",
|
||||
"i18n:govoplan-audit.tenant_level_administrative_history_showing_value0_value1_of_value2.f1a20819": "Tenant-level administrative history for the active tenant, showing {value0}-{value1} of {value2}.",
|
||||
"i18n:govoplan-audit.time.f1a20820": "Time",
|
||||
"i18n:govoplan-audit.value.f1a20821": "Value"
|
||||
"i18n:govoplan-audit.value.f1a20821": "Value",
|
||||
"i18n:govoplan-audit.export_page_evidence.f1a20822": "Export page evidence",
|
||||
"i18n:govoplan-audit.audit_evidence_export_is_in_progress.f1a20823": "Audit evidence export is in progress."
|
||||
},
|
||||
de: {
|
||||
"i18n:govoplan-audit.action.f1a20801": "Aktion",
|
||||
@@ -45,6 +47,8 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-audit.tenant_context.f1a20818": "Mandantenkontext",
|
||||
"i18n:govoplan-audit.tenant_level_administrative_history_showing_value0_value1_of_value2.f1a20819": "Administrative Historie des aktiven Mandanten, angezeigt werden {value0}-{value1} von {value2}.",
|
||||
"i18n:govoplan-audit.time.f1a20820": "Zeit",
|
||||
"i18n:govoplan-audit.value.f1a20821": "Wert"
|
||||
"i18n:govoplan-audit.value.f1a20821": "Wert",
|
||||
"i18n:govoplan-audit.export_page_evidence.f1a20822": "Seitennachweise exportieren",
|
||||
"i18n:govoplan-audit.audit_evidence_export_is_in_progress.f1a20823": "Der Export der Auditnachweise läuft."
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user