"use server"; import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; import { z } from "zod"; import { moduleGuard } from "@/server/action-guard"; import { writeAuditLog } from "@/server/audit"; const guard = moduleGuard("bia"); const processSchema = z.object({ name: z.string().trim().min(1).max(200), description: z.string().trim().max(5000).optional(), category: z.enum(["CORE", "MANAGEMENT", "SUPPORT"]), ownerId: z.string().optional(), }); function parseProcessForm(formData: FormData) { const parsed = processSchema.parse({ name: formData.get("name"), description: formData.get("description") || undefined, category: formData.get("category"), ownerId: formData.get("ownerId") || undefined, }); return { ...parsed, description: parsed.description || null, ownerId: parsed.ownerId || null, }; } export async function createProcess(formData: FormData) { const { session, db } = await guard("bia:write"); 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?edit=${process.id}`); } export async function updateProcess(processId: string, formData: FormData) { const { session, db } = await guard("bia:write"); 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"); redirect(`/processes?detail=${processId}`); } export async function deleteProcess(processId: string) { const { session, db } = await guard("bia:write"); 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, db } = await guard("bia:write"); 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"); } export async function unassignAsset(processId: string, processAssetId: string) { const { session, db } = await guard("bia:write"); 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"); } 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, db } = await guard("bia:write"); 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"); } /** * Kombiniertes Speichern für den aufgeräumten Bearbeiten-Dialog: Stammdaten * und BIA in einem Vorgang (ein Speichern-Button). Asset-Zuordnung läuft * weiter über die inkrementellen Aktionen assignAsset/unassignAsset. */ export async function saveProcessAll(processId: string, formData: FormData) { const { session, db } = await guard("bia:write"); const before = await db.process.findUnique({ where: { id: processId }, include: { bia: true }, }); if (!before) throw new Error("Prozess nicht gefunden"); const master = parseProcessForm(formData); await db.process.update({ where: { id: processId }, data: master }); const level = z.coerce.number().int().min(1).max(4); const biaData = { 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, }; const criticality = Math.max(biaData.impactC, biaData.impactI, biaData.impactA); await db.biaEntry.upsert({ where: { processId }, update: { ...biaData, criticality }, create: { ...biaData, criticality, processId, tenantId: session.user.tenantId }, }); await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "update", entity: "process", entityId: processId, before, after: { ...master, bia: { ...biaData, criticality } }, }); revalidatePath("/processes"); redirect(`/processes?detail=${processId}`); }