Files
nexarch/web/retention-admin/lib/api.ts
T
sysops 36d1cf5e7f RET-06: aufbewahrungsfristen-konfigurationsoberfläche
- web/retention-admin: eigenstaendige Next.js/React/TypeScript-App
  (kein Backend-Annex), auf web/shl (SHL-01) aufbauend
- classes: Aufbewahrungsklasse anlegen/aendern/deaktivieren (AC1)
- preview: Vorschauliste 30 Tage, nutzt denselben Endpunkt wie der
  periodische Job (AC2)
- lib/api.ts: ForbiddenError bei 403, getrennt behandelt, keine
  eigene Autorisierungslogik im Frontend (RBAC-Entscheidung liegt
  bei RET-08/RBAC-06)
- expliziter 403-Nachweis (nicht nur der 200-Fall): lib/api.test.ts,
  ClassesPage/PreviewPage zeigen 'Zugriff verweigert' statt leerer Seite
- lokal getestet: tsc clean, next build clean, vitest 3/3 gruen

Pruefungen siehe archive/docs/RET-06-PRUEFPROTOKOLL.md
2026-08-30 08:49:24 +02:00

82 lines
2.6 KiB
TypeScript

// Duenner Client fuer RET-06-API (archive/internal/retentionapi) — enthaelt
// keine eigene Fristen-/RBAC-Logik, nur Datenabruf und -weitergabe. Der
// RBAC-Check selbst passiert serverseitig ueber RET-08 (echter RBAC-06-Aufruf),
// dieser Client meldet nur, ob der Zugriff erlaubt war (403) oder nicht.
export class ForbiddenError extends Error {}
export class ApiError extends Error {}
function apiBase(): string {
const base = process.env.NEXT_PUBLIC_RETENTION_API_URL;
if (!base) {
throw new Error("NEXT_PUBLIC_RETENTION_API_URL ist nicht gesetzt (Umgebungsvariable erforderlich)");
}
return base;
}
// userRole wird bis zu einer zentralen Session-/Identitaets-Loesung im
// Frontend als Umgebungsvariable/Query mitgegeben und als X-User-Role-Header
// an RET-06-API durchgereicht, das ihn seinerseits gegen RBAC-06 prueft
// (RET-08) - das Frontend trifft selbst KEINE Autorisierungsentscheidung.
function userRoleHeader(): Record<string, string> {
const role = process.env.NEXT_PUBLIC_USER_ROLE;
return role ? { "X-User-Role": role } : {};
}
async function req<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${apiBase()}${path}`, {
...init,
headers: {
...userRoleHeader(),
...(init?.body ? { "Content-Type": "application/json" } : {}),
...init?.headers,
},
});
if (res.status === 403) {
throw new ForbiddenError("Zugriff verweigert: Ihre Rolle ist nicht zur Fristenverwaltung berechtigt.");
}
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new ApiError(body.error ?? `Anfrage fehlgeschlagen (${res.status})`);
}
if (res.status === 200 && res.headers.get("content-length") === "0") {
return undefined as T;
}
return res.json().catch(() => undefined as T);
}
export interface ClassRule {
RetentionClass: string;
Duration: string;
Active: boolean;
}
export interface ExpiringObject {
RetentionObjectID: string;
ObjectType: string;
ObjectReference: string;
RetentionClass: string;
DueDate: string;
}
export function fetchClassRules(): Promise<ClassRule[]> {
return req("/retention-classes");
}
export function configureClassRule(retentionClass: string, duration: string): Promise<void> {
return req("/retention-classes", {
method: "POST",
body: JSON.stringify({ retention_class: retentionClass, duration }),
});
}
export function deactivateClassRule(retentionClass: string): Promise<void> {
return req(`/retention-classes/${encodeURIComponent(retentionClass)}/deactivate`, {
method: "POST",
});
}
export function fetchExpiringPreview(): Promise<ExpiringObject[]> {
return req("/retention-classes/preview");
}