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
This commit is contained in:
@@ -0,0 +1,58 @@
|
|||||||
|
# RET-06 – Prüfprotokoll: Aufbewahrungsfristen-Konfigurationsoberfläche
|
||||||
|
|
||||||
|
Voraussetzung RET-02, RET-06-API, RET-08 (RBAC-Migration) – alle
|
||||||
|
bereits Fertig. RET-08 hat das Header-Provisorium in RET-06-API bereits
|
||||||
|
durch einen echten Aufruf von Core RBAC-06 ersetzt – dieses Ticket
|
||||||
|
testet daher von Anfang an gegen echte RBAC-Autorisierung, nicht gegen
|
||||||
|
ein Provisorium (siehe RET-08-Prüfprotokoll).
|
||||||
|
|
||||||
|
## Umsetzung
|
||||||
|
|
||||||
|
- `web/retention-admin` – eigenständige Next.js/React/TypeScript-App
|
||||||
|
(kein `/admin`-Annex im Go-Backend), analog zu `web/notifications`
|
||||||
|
(CFG-04) und `web/lic-admin` (LIC-04), aufbauend auf `web/shl`
|
||||||
|
(SHL-01, gemeinsames Design-System).
|
||||||
|
- `lib/api.ts` – dünner Client für RET-06-API. Reicht die vom Nutzer
|
||||||
|
beanspruchte Rolle über `X-User-Role` durch (RET-08 prüft sie gegen
|
||||||
|
RBAC-06), trifft selbst keine Autorisierungsentscheidung. Wirft
|
||||||
|
`ForbiddenError` bei HTTP 403, getrennt von generischen `ApiError`s.
|
||||||
|
- `app/classes/page.tsx` – Akzeptanzkriterium 1: Aufbewahrungsklasse
|
||||||
|
anlegen/ändern (ein Formular, Backend-UPSERT) und deaktivieren.
|
||||||
|
Akzeptanzkriterium 3: `ForbiddenError` führt zu einer expliziten
|
||||||
|
"Zugriff verweigert"-Anzeige, nicht zu einer leeren Tabelle.
|
||||||
|
- `app/preview/page.tsx` – Akzeptanzkriterium 2: Vorschauliste über
|
||||||
|
`/retention-classes/preview`, dieselbe Funktion wie der periodische
|
||||||
|
Job (RET-02/RET-06-API), keine eigene Berechnung im Frontend.
|
||||||
|
|
||||||
|
## Prüfungen
|
||||||
|
|
||||||
|
| # | Prüfung | Ergebnis |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | Änderung einer Frist wirkt nur auf künftige Berechnungen, nicht rückwirkend | **bestanden (Backend-seitig bereits durch RET-02/RET-06-API bewiesen)** – Frontend ruft ausschließlich den bestehenden UPSERT-Endpunkt auf, keine eigene Berechnungslogik im Frontend, die dies unterlaufen könnte |
|
||||||
|
| 2 | Nicht berechtigte Rolle erhält keinen Zugriff auf die Konfiguration | **bestanden – expliziter 403-Nachweis, nicht nur der 200-Fall:** `lib/api.test.ts`, `describe("api client - 403-Nachweis (keine berechtigte Rolle)")` – zwei Tests (`fetchClassRules`, `configureClassRule`) mit real gemockter 403-HTTP-Antwort, beide werfen `ForbiddenError`; `ClassesPage`/`PreviewPage` fangen `ForbiddenError` ab und zeigen `role="alert"` "Zugriff verweigert" statt einer leeren/stillen Seite. Der 403-Vertrag selbst (RET-06-API antwortet real mit 403 bei fehlender RBAC-06-Berechtigung) ist bereits in RET-08 end-zu-ende gegen den laufenden Dienst auf 131 bewiesen (`curl` ohne Policy-Rule → 403) – dieses Ticket prüft, dass das Frontend diesen real existierenden Vertrag korrekt behandelt, nicht das Backend erneut |
|
||||||
|
| 3 | Vorschauliste stimmt mit dem Ergebnis des periodischen Jobs überein | **bestanden (Backend-seitig bereits durch RET-06-API bewiesen)** – `PreviewPage` ruft exakt denselben `/retention-classes/preview`-Endpunkt auf, der intern `retentionengine.ListExpiringObjects` verwendet (identische Funktion wie der periodische Job), keine zweite Implementierung im Frontend |
|
||||||
|
|
||||||
|
## Build/Test-Ergebnis (lokal, node v22.16.0/npm 10.9.2 – bereits installiert)
|
||||||
|
|
||||||
|
```
|
||||||
|
npx tsc --noEmit -> clean (eigener Code; siehe Hinweis)
|
||||||
|
npx next build -> Compiled successfully, 3 Routen (/, /classes, /preview)
|
||||||
|
npx vitest run -> 3/3 Tests bestanden
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hinweis:** `web/shl` (SHL-01, bereits Fertig) hatte kein eigenes
|
||||||
|
`node_modules` im Checkout – ohne `npm install` dort lieferte `tsc`
|
||||||
|
kaskadierende "Cannot find module 'react'"-Fehler in shl-eigenen
|
||||||
|
Dateien, nicht durch RET-06 verursacht. Für den lokalen Testlauf wurde
|
||||||
|
`npm install` in `web/shl` ausgeführt (kein Code-Umbau, nur Abhängigkeiten
|
||||||
|
installiert); `web/shl/package-lock.json` wurde dadurch neu erzeugt,
|
||||||
|
aber bewusst NICHT mitcommittet (gehört zu SHL-01, nicht zu diesem
|
||||||
|
Ticket – kein Umbau angrenzender Bereiche).
|
||||||
|
|
||||||
|
## Gesamtergebnis
|
||||||
|
|
||||||
|
**Bestanden.** Alle drei Akzeptanzkriterien real erfüllt, insbesondere
|
||||||
|
der explizit geforderte negative 403-Fall (nicht nur der Erfolgsfall)
|
||||||
|
sowie die eigenständige Next.js-App-Struktur (kein Backend-Annex).
|
||||||
|
Frontend testet von Anfang an gegen die durch RET-08 real hergestellte
|
||||||
|
RBAC-06-Autorisierung, nicht gegen ein Provisorium.
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState, type ChangeEvent, type FormEvent } from "react";
|
||||||
|
import { Table, TextField, useToast, type TableColumn } from "@nexarch/shl";
|
||||||
|
import {
|
||||||
|
ApiError,
|
||||||
|
ClassRule,
|
||||||
|
ForbiddenError,
|
||||||
|
configureClassRule,
|
||||||
|
deactivateClassRule,
|
||||||
|
fetchClassRules,
|
||||||
|
} from "../../lib/api";
|
||||||
|
|
||||||
|
export default function ClassesPage() {
|
||||||
|
const { push } = useToast();
|
||||||
|
const [rules, setRules] = useState<ClassRule[] | null>(null);
|
||||||
|
const [forbidden, setForbidden] = useState(false);
|
||||||
|
const [loadError, setLoadError] = useState<string | null>(null);
|
||||||
|
const [newClass, setNewClass] = useState("");
|
||||||
|
const [newDuration, setNewDuration] = useState("");
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
|
function load() {
|
||||||
|
fetchClassRules()
|
||||||
|
.then((r) => {
|
||||||
|
setRules(r);
|
||||||
|
setForbidden(false);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (err instanceof ForbiddenError) {
|
||||||
|
setForbidden(true);
|
||||||
|
} else {
|
||||||
|
setLoadError(err instanceof Error ? err.message : "Aufbewahrungsklassen konnten nicht geladen werden.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(load, []);
|
||||||
|
|
||||||
|
// Akzeptanzkriterium 3: nicht berechtigte Rolle sieht explizit den
|
||||||
|
// Zugriff-verweigert-Zustand, keine leere Tabelle ohne Erklaerung.
|
||||||
|
if (forbidden) {
|
||||||
|
return (
|
||||||
|
<main style={{ maxWidth: 720, margin: "40px auto", padding: "0 16px" }}>
|
||||||
|
<p role="alert">Zugriff verweigert: Ihre Rolle ist nicht zur Fristenverwaltung berechtigt.</p>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (loadError) {
|
||||||
|
return (
|
||||||
|
<main style={{ maxWidth: 720, margin: "40px auto", padding: "0 16px" }}>
|
||||||
|
<p role="alert">{loadError}</p>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onCreateOrChange(e: FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
// Akzeptanzkriterium 1: gleicher Endpunkt legt an ODER aendert
|
||||||
|
// (UPSERT im Backend, retentionengine.ConfigureClassRule).
|
||||||
|
await configureClassRule(newClass, newDuration);
|
||||||
|
push(`Aufbewahrungsklasse "${newClass}" gespeichert.`, "success");
|
||||||
|
setNewClass("");
|
||||||
|
setNewDuration("");
|
||||||
|
load();
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof ForbiddenError) {
|
||||||
|
setForbidden(true);
|
||||||
|
} else {
|
||||||
|
push(err instanceof ApiError ? err.message : "Speichern fehlgeschlagen.", "danger");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onDeactivate(retentionClass: string) {
|
||||||
|
try {
|
||||||
|
await deactivateClassRule(retentionClass);
|
||||||
|
push(`Aufbewahrungsklasse "${retentionClass}" deaktiviert.`, "success");
|
||||||
|
load();
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof ForbiddenError) {
|
||||||
|
setForbidden(true);
|
||||||
|
} else {
|
||||||
|
push(err instanceof ApiError ? err.message : "Deaktivieren fehlgeschlagen.", "danger");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns: TableColumn<ClassRule>[] = [
|
||||||
|
{ key: "class", header: "Aufbewahrungsklasse", render: (r) => r.RetentionClass },
|
||||||
|
{ key: "duration", header: "Frist", render: (r) => r.Duration },
|
||||||
|
{ key: "active", header: "Status", render: (r) => (r.Active ? "aktiv" : "deaktiviert") },
|
||||||
|
{
|
||||||
|
key: "actions",
|
||||||
|
header: "Aktion",
|
||||||
|
render: (r) =>
|
||||||
|
r.Active ? (
|
||||||
|
<button type="button" onClick={() => onDeactivate(r.RetentionClass)}>
|
||||||
|
Deaktivieren
|
||||||
|
</button>
|
||||||
|
) : null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main style={{ maxWidth: 720, margin: "40px auto", padding: "0 16px" }}>
|
||||||
|
<h1>Aufbewahrungsfristen-Konfiguration</h1>
|
||||||
|
|
||||||
|
<form onSubmit={onCreateOrChange} style={{ marginBottom: 24 }}>
|
||||||
|
<TextField
|
||||||
|
label="Aufbewahrungsklasse"
|
||||||
|
value={newClass}
|
||||||
|
onChange={(e: ChangeEvent<HTMLInputElement>) => setNewClass(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Frist (z. B. 5 years)"
|
||||||
|
value={newDuration}
|
||||||
|
onChange={(e: ChangeEvent<HTMLInputElement>) => setNewDuration(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<button type="submit" disabled={saving}>
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{rules && (
|
||||||
|
<Table columns={columns} rows={rules} rowKey={(r) => r.RetentionClass} caption="Konfigurierte Aufbewahrungsklassen" />
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { ThemeProvider, I18nProvider, ToastProvider, typography } from "@nexarch/shl";
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: "NEXARCH Aufbewahrungsfristen",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="de">
|
||||||
|
<body
|
||||||
|
style={{
|
||||||
|
fontFamily: typography.fontFamily,
|
||||||
|
margin: 0,
|
||||||
|
background: "var(--shl-color-background, #ffffff)",
|
||||||
|
color: "var(--shl-color-text-primary, #14181f)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ThemeProvider>
|
||||||
|
<I18nProvider initialLocale="de">
|
||||||
|
<ToastProvider>{children}</ToastProvider>
|
||||||
|
</I18nProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
export default function IndexPage() {
|
||||||
|
redirect("/classes");
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Table, type TableColumn } from "@nexarch/shl";
|
||||||
|
import { ExpiringObject, ForbiddenError, fetchExpiringPreview } from "../../lib/api";
|
||||||
|
|
||||||
|
// Akzeptanzkriterium 2: Vorschauliste bald ablaufender Objekte (naechste 30
|
||||||
|
// Tage) - ruft dieselbe RET-06-API /retention-classes/preview auf, die
|
||||||
|
// intern dieselbe Funktion wie der periodische Job nutzt (RET-02), keine
|
||||||
|
// eigene Berechnung im Frontend.
|
||||||
|
export default function PreviewPage() {
|
||||||
|
const [objects, setObjects] = useState<ExpiringObject[] | null>(null);
|
||||||
|
const [forbidden, setForbidden] = useState(false);
|
||||||
|
const [loadError, setLoadError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchExpiringPreview()
|
||||||
|
.then(setObjects)
|
||||||
|
.catch((err) => {
|
||||||
|
if (err instanceof ForbiddenError) {
|
||||||
|
setForbidden(true);
|
||||||
|
} else {
|
||||||
|
setLoadError(err instanceof Error ? err.message : "Vorschauliste konnte nicht geladen werden.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (forbidden) {
|
||||||
|
return (
|
||||||
|
<main style={{ maxWidth: 720, margin: "40px auto", padding: "0 16px" }}>
|
||||||
|
<p role="alert">Zugriff verweigert: Ihre Rolle ist nicht zur Fristenverwaltung berechtigt.</p>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (loadError) {
|
||||||
|
return (
|
||||||
|
<main style={{ maxWidth: 720, margin: "40px auto", padding: "0 16px" }}>
|
||||||
|
<p role="alert">{loadError}</p>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns: TableColumn<ExpiringObject>[] = [
|
||||||
|
{ key: "type", header: "Objekttyp", render: (o) => o.ObjectType },
|
||||||
|
{ key: "ref", header: "Referenz", render: (o) => o.ObjectReference },
|
||||||
|
{ key: "class", header: "Aufbewahrungsklasse", render: (o) => o.RetentionClass },
|
||||||
|
{ key: "due", header: "Fällig am", render: (o) => o.DueDate },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main style={{ maxWidth: 720, margin: "40px auto", padding: "0 16px" }}>
|
||||||
|
<h1>In den nächsten 30 Tagen ablaufende Objekte</h1>
|
||||||
|
{objects && (
|
||||||
|
<Table columns={columns} rows={objects} rowKey={(o) => o.RetentionObjectID} caption="Bald ablaufende Objekte" />
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { ForbiddenError, fetchClassRules, configureClassRule } from "./api";
|
||||||
|
|
||||||
|
const ORIGINAL_ENV = process.env;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
process.env = { ...ORIGINAL_ENV, NEXT_PUBLIC_RETENTION_API_URL: "http://backend.test" };
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.env = ORIGINAL_ENV;
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Akzeptanzkriterium 3 ("nur berechtigten Rollen zugaenglich"): der
|
||||||
|
// Negativfall MUSS explizit gepruft werden, nicht nur der Erfolgsfall -
|
||||||
|
// Frontend wirft ForbiddenError, wenn RET-06-API (real gegen RBAC-06,
|
||||||
|
// siehe RET-08) mit 403 antwortet.
|
||||||
|
describe("api client - 403-Nachweis (keine berechtigte Rolle)", () => {
|
||||||
|
it("fetchClassRules wirft ForbiddenError bei 403-Antwort", async () => {
|
||||||
|
vi.stubGlobal(
|
||||||
|
"fetch",
|
||||||
|
vi.fn().mockResolvedValue(new Response(null, { status: 403 }))
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(fetchClassRules()).rejects.toBeInstanceOf(ForbiddenError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("configureClassRule wirft ForbiddenError bei 403-Antwort", async () => {
|
||||||
|
vi.stubGlobal(
|
||||||
|
"fetch",
|
||||||
|
vi.fn().mockResolvedValue(new Response(null, { status: 403 }))
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(configureClassRule("klasse-x", "1 year")).rejects.toBeInstanceOf(ForbiddenError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("api client - erlaubte Rolle", () => {
|
||||||
|
it("fetchClassRules liefert die Liste bei 200-Antwort", async () => {
|
||||||
|
const rules = [{ RetentionClass: "klasse-a", Duration: "5 years", Active: true }];
|
||||||
|
vi.stubGlobal(
|
||||||
|
"fetch",
|
||||||
|
vi.fn().mockResolvedValue(
|
||||||
|
new Response(JSON.stringify(rules), { status: 200, headers: { "Content-Type": "application/json" } })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(fetchClassRules()).resolves.toEqual(rules);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
// 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");
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
transpilePackages: ["@nexarch/shl"],
|
||||||
|
};
|
||||||
|
export default nextConfig;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "nexarch-retention-admin",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint",
|
||||||
|
"test": "vitest run"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nexarch/shl": "file:../shl",
|
||||||
|
"next": "14.2.35",
|
||||||
|
"react": "18.3.1",
|
||||||
|
"react-dom": "18.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@testing-library/react": "16.0.0",
|
||||||
|
"@types/node": "20.14.9",
|
||||||
|
"@types/react": "18.3.3",
|
||||||
|
"@types/react-dom": "18.3.0",
|
||||||
|
"jsdom": "24.1.0",
|
||||||
|
"typescript": "5.5.3",
|
||||||
|
"vitest": "2.0.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [{ "name": "next" }],
|
||||||
|
"paths": { "@/*": ["./*"] }
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: "jsdom",
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user