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:
@@ -0,0 +1,192 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
import { requireSession } from "@/server/auth";
|
||||
import { dbForTenant } from "@/server/db";
|
||||
import { requirePermission } from "@/server/rbac";
|
||||
import { writeAuditLog } from "@/server/audit";
|
||||
|
||||
const processSchema = z.object({
|
||||
name: z.string().trim().min(1).max(200),
|
||||
description: z.string().trim().max(5000).optional(),
|
||||
ownerId: z.string().optional(),
|
||||
});
|
||||
|
||||
function parseProcessForm(formData: FormData) {
|
||||
const parsed = processSchema.parse({
|
||||
name: formData.get("name"),
|
||||
description: formData.get("description") || undefined,
|
||||
ownerId: formData.get("ownerId") || undefined,
|
||||
});
|
||||
return {
|
||||
...parsed,
|
||||
description: parsed.description || null,
|
||||
ownerId: parsed.ownerId || null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function createProcess(formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "bia:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
|
||||
const data = parseProcessForm(formData);
|
||||
// tenantId explizit für die Typen, der Guard injiziert ohnehin
|
||||
const process = await db.process.create({
|
||||
data: { ...data, tenantId: session.user.tenantId, createdBy: session.user.id },
|
||||
});
|
||||
|
||||
await writeAuditLog({
|
||||
tenantId: session.user.tenantId,
|
||||
actorId: session.user.id,
|
||||
action: "create",
|
||||
entity: "process",
|
||||
entityId: process.id,
|
||||
after: data,
|
||||
});
|
||||
revalidatePath("/processes");
|
||||
redirect(`/processes/${process.id}`);
|
||||
}
|
||||
|
||||
export async function updateProcess(processId: string, formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "bia:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
|
||||
const before = await db.process.findUnique({ where: { id: processId } });
|
||||
if (!before) throw new Error("Prozess nicht gefunden");
|
||||
|
||||
const data = parseProcessForm(formData);
|
||||
await db.process.update({ where: { id: processId }, data });
|
||||
|
||||
await writeAuditLog({
|
||||
tenantId: session.user.tenantId,
|
||||
actorId: session.user.id,
|
||||
action: "update",
|
||||
entity: "process",
|
||||
entityId: processId,
|
||||
before,
|
||||
after: data,
|
||||
});
|
||||
revalidatePath("/processes");
|
||||
revalidatePath(`/processes/${processId}`);
|
||||
redirect(`/processes/${processId}`);
|
||||
}
|
||||
|
||||
export async function deleteProcess(processId: string) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "bia:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
|
||||
const before = await db.process.findUnique({ where: { id: processId } });
|
||||
if (!before) throw new Error("Prozess nicht gefunden");
|
||||
|
||||
await db.process.delete({ where: { id: processId } });
|
||||
|
||||
await writeAuditLog({
|
||||
tenantId: session.user.tenantId,
|
||||
actorId: session.user.id,
|
||||
action: "delete",
|
||||
entity: "process",
|
||||
entityId: processId,
|
||||
before,
|
||||
});
|
||||
revalidatePath("/processes");
|
||||
redirect("/processes");
|
||||
}
|
||||
|
||||
export async function assignAsset(processId: string, formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "bia:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
|
||||
const assetId = z.string().min(1).parse(formData.get("assetId"));
|
||||
const role = z.enum(["PRIMARY", "SECONDARY"]).parse(formData.get("role"));
|
||||
|
||||
const assetCount = await db.asset.count({ where: { id: assetId } });
|
||||
const processCount = await db.process.count({ where: { id: processId } });
|
||||
if (assetCount !== 1 || processCount !== 1) throw new Error("Nicht gefunden");
|
||||
|
||||
await db.processAsset.upsert({
|
||||
where: { processId_assetId: { processId, assetId } },
|
||||
update: { role },
|
||||
create: { processId, assetId, role, tenantId: session.user.tenantId },
|
||||
});
|
||||
|
||||
await writeAuditLog({
|
||||
tenantId: session.user.tenantId,
|
||||
actorId: session.user.id,
|
||||
action: "update",
|
||||
entity: "process_asset",
|
||||
entityId: processId,
|
||||
after: { processId, assetId, role },
|
||||
});
|
||||
revalidatePath(`/processes/${processId}`);
|
||||
}
|
||||
|
||||
export async function unassignAsset(processId: string, processAssetId: string) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "bia:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
|
||||
const before = await db.processAsset.findUnique({ where: { id: processAssetId } });
|
||||
if (!before) return;
|
||||
await db.processAsset.delete({ where: { id: processAssetId } });
|
||||
|
||||
await writeAuditLog({
|
||||
tenantId: session.user.tenantId,
|
||||
actorId: session.user.id,
|
||||
action: "delete",
|
||||
entity: "process_asset",
|
||||
entityId: processAssetId,
|
||||
before,
|
||||
});
|
||||
revalidatePath(`/processes/${processId}`);
|
||||
}
|
||||
|
||||
const hours = z
|
||||
.union([z.literal(""), z.coerce.number().int().min(0).max(100000)])
|
||||
.transform((v) => (v === "" ? null : v));
|
||||
|
||||
export async function saveBia(processId: string, formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "bia:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
|
||||
const processCount = await db.process.count({ where: { id: processId } });
|
||||
if (processCount !== 1) throw new Error("Prozess nicht gefunden");
|
||||
|
||||
const level = z.coerce.number().int().min(1).max(4);
|
||||
const data = {
|
||||
rtoHours: hours.parse(formData.get("rtoHours") ?? ""),
|
||||
rpoHours: hours.parse(formData.get("rpoHours") ?? ""),
|
||||
mtdHours: hours.parse(formData.get("mtdHours") ?? ""),
|
||||
impactC: level.parse(formData.get("impactC")),
|
||||
impactI: level.parse(formData.get("impactI")),
|
||||
impactA: level.parse(formData.get("impactA")),
|
||||
notes: (formData.get("notes") as string)?.trim() || null,
|
||||
};
|
||||
// Kritikalität nach Max-Prinzip aus den Schadenshöhen (SPEC §4.1.2)
|
||||
const criticality = Math.max(data.impactC, data.impactI, data.impactA);
|
||||
|
||||
const before = await db.biaEntry.findUnique({ where: { processId } });
|
||||
await db.biaEntry.upsert({
|
||||
where: { processId },
|
||||
update: { ...data, criticality },
|
||||
create: { ...data, criticality, processId, tenantId: session.user.tenantId },
|
||||
});
|
||||
|
||||
await writeAuditLog({
|
||||
tenantId: session.user.tenantId,
|
||||
actorId: session.user.id,
|
||||
action: before ? "update" : "create",
|
||||
entity: "bia_entry",
|
||||
entityId: processId,
|
||||
before: before ?? undefined,
|
||||
after: { ...data, criticality },
|
||||
});
|
||||
revalidatePath("/processes");
|
||||
revalidatePath(`/processes/${processId}`);
|
||||
}
|
||||
Reference in New Issue
Block a user