Iteration 2: Assets & BIA als gemeinsames Modul

- App-Shell mit Seitennavigation (alle 12 Module, kommende ausgegraut),
  shadcn/ui-Setup (Base-UI-Variante) mit hellem Theme
- Asset-Inventar: filterbare Liste (Suche/Typ/Status), Anlegen/Bearbeiten/
  Löschen, Detailansicht mit Schutzbedarf (C/I/A 1–4), Abhängigkeiten
  (beide Richtungen, hinzufügen/entfernen), zugeordneten Prozessen mit
  Primär-/Sekundär-Rolle und Platzhalter für zugeordnete Risiken
- Prozesse & BIA: Liste mit Kritikalität/RTO/MTD, Prozess-Detail mit
  Asset-Zuordnung nach Rolle (primär/sekundär), BIA-Formular
  (RTO/RPO/MTD, Schadenshöhe je Schutzziel, Kritikalität nach Max-Prinzip)
- Server-Actions mit Zod-Validierung, requirePermission und Audit-Log
  für jede schreibende Aktion; Tenant-Guard um upsert-Injektion erweitert
- Prisma: Asset, AssetRelation, Process, ProcessAsset, BiaEntry
  inkl. RLS-Policies; Seed mit Beispiel-Assets, Prozessen und BIA
- Im Browser verifiziert: CRUD, Relationen, BIA-Speichern, Audit-Einträge

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Martin
2026-07-02 12:35:19 +02:00
co-authored by Claude Fable 5
parent 880afed391
commit 392bb246cf
41 changed files with 6085 additions and 149 deletions
+117
View File
@@ -0,0 +1,117 @@
import Link from "next/link";
import { getTranslations } from "next-intl/server";
import { Plus } from "lucide-react";
import { requireSession } from "@/server/auth";
import { dbForTenant } from "@/server/db";
import { hasPermission, requirePermission } from "@/server/rbac";
import { Button } from "@/components/ui/button";
import { LevelBadge } from "@/components/level-badge";
import { ModuleTabs } from "@/components/module-tabs";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
export default async function ProcessesPage() {
const session = await requireSession();
requirePermission(session, "bia:read");
const t = await getTranslations("processes");
const tAssets = await getTranslations("assets");
const tCrit = await getTranslations("criticality");
const tc = await getTranslations("common");
const db = dbForTenant(session.user.tenantId);
const processes = await db.process.findMany({
include: {
owner: { select: { name: true } },
bia: { select: { criticality: true, rtoHours: true, mtdHours: true } },
_count: { select: { processAssets: true } },
},
orderBy: { name: "asc" },
take: 200,
});
const canWrite = hasPermission(session, "bia:write");
return (
<main className="flex-1 p-6">
<div className="flex items-center justify-between">
<h1 className="text-xl font-semibold">{tAssets("title")}</h1>
{canWrite && (
<Button nativeButton={false} render={<Link href="/processes/new" />}>
<Plus className="size-4" /> {t("newProcess")}
</Button>
)}
</div>
<div className="mt-4">
<ModuleTabs
active="/processes"
tabs={[
{ href: "/assets", label: tAssets("tabAssets") },
{ href: "/processes", label: tAssets("tabProcesses") },
]}
/>
</div>
<div className="mt-4 rounded-lg border bg-card">
<Table>
<TableHeader>
<TableRow>
<TableHead>{t("name")}</TableHead>
<TableHead>{t("owner")}</TableHead>
<TableHead>{t("criticality")}</TableHead>
<TableHead>RTO</TableHead>
<TableHead>MTD</TableHead>
<TableHead>{t("assets")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{processes.length === 0 && (
<TableRow>
<TableCell colSpan={6} className="py-8 text-center text-muted-foreground">
{t("empty")}
</TableCell>
</TableRow>
)}
{processes.map((process) => (
<TableRow key={process.id}>
<TableCell>
<Link href={`/processes/${process.id}`} className="font-medium hover:underline">
{process.name}
</Link>
</TableCell>
<TableCell className="text-muted-foreground">
{process.owner?.name ?? tc("none")}
</TableCell>
<TableCell>
{process.bia ? (
<LevelBadge
level={process.bia.criticality}
label={tCrit(String(process.bia.criticality))}
/>
) : (
<span className="text-muted-foreground">{t("noBia")}</span>
)}
</TableCell>
<TableCell className="text-muted-foreground">
{process.bia?.rtoHours != null ? `${process.bia.rtoHours} h` : tc("none")}
</TableCell>
<TableCell className="text-muted-foreground">
{process.bia?.mtdHours != null ? `${process.bia.mtdHours} h` : tc("none")}
</TableCell>
<TableCell className="text-muted-foreground">
{process._count.processAssets}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</main>
);
}