API-seitige Modul-Durchsetzung vervollständigt (§3.4, Phase-1-Härtung Paket 1)
Der requireModule-Guard war bislang nur exemplarisch in den Policy-Actions
gesetzt. Jetzt läuft jede mutierende Server-Action eines gegateten Moduls über
einen zentralen, modul- und rechteprüfenden Guard — ein für den Mandanten
deaktiviertes Modul weist damit auch Writes serverseitig ab (nicht nur Reads
über die Route-Layouts).
- action-guard.ts: moduleGuard("<key>") erzeugt je Modul einen guard(...perms),
der Session → Modul-Aktivierung → RBAC prüft und {session, db} zurückgibt.
- modules.ts: assertModuleEnabled(session, key) wirft (statt Redirect) bei
Deaktivierung und protokolliert die Verweigerung (Audit-Aktion "denied").
- audit.ts: Audit-Aktion "denied" + optionaler scope-Parameter.
- Alle gegateten Action-Dateien umgestellt: assets→assets, processes→bia,
risks→risk, measures→measures, suppliers/services→suppliers, policies→policies.
Freigabe/Ablehnung der Richtlinien laufen jetzt ebenfalls über den Modul-Guard.
- Vollständigkeitscheck scripts/check-module-guards.ts (Registry Action→Modul):
schlägt fehl bei nicht zugeordneter Datei, fehlendem moduleGuard oder einer
Action ohne guard(...). Als prebuild verdrahtet ⇒ Build failt bei vergessenem
Endpoint. admin/tenant-settings sind als EXEMPT (eigene Auth) markiert.
Verifiziert: tsc sauber, lint sauber, Guard-Check grün (9 Dateien), Negativ-Probe
(ungemappte Action-Datei) failt den Check wie erwartet.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
import { requireSession } from "@/server/auth";
|
||||
import { dbForTenant } from "@/server/db";
|
||||
import { requirePermission, type Permission } from "@/server/rbac";
|
||||
import { assertModuleEnabled } from "@/server/modules";
|
||||
|
||||
/**
|
||||
* Einheitlicher Einstieg für mutierende Server-Actions eines gegateten Moduls (§3.4).
|
||||
* Reihenfolge: Session → Modul-Aktivierung (wirft bei Deaktivierung + Audit) → RBAC.
|
||||
* Gibt Session und den mandantengebundenen Prisma-Client zurück.
|
||||
*
|
||||
* `moduleGuard("assets")` erzeugt den Guard eines Moduls; jede Action ruft ihn mit
|
||||
* den benötigten Rechten auf, z. B. `const { session, db } = await guard("asset:write")`.
|
||||
* Mehrere Rechte werden alle geprüft (z. B. `guard("risk:write", "measure:write")`).
|
||||
*
|
||||
* Der Vollständigkeitscheck (`scripts/check-module-guards.ts`) verlässt sich darauf,
|
||||
* dass jede Action eines gegateten Moduls über einen so erzeugten `guard(...)` läuft.
|
||||
*/
|
||||
export function moduleGuard(moduleKey: string) {
|
||||
return async function guard(...permissions: Permission[]) {
|
||||
const session = await requireSession();
|
||||
await assertModuleEnabled(session, moduleKey);
|
||||
for (const permission of permissions) requirePermission(session, permission);
|
||||
return { session, db: dbForTenant(session.user.tenantId) };
|
||||
};
|
||||
}
|
||||
@@ -3,11 +3,11 @@
|
||||
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 { moduleGuard } from "@/server/action-guard";
|
||||
import { writeAuditLog } from "@/server/audit";
|
||||
|
||||
const guard = moduleGuard("assets");
|
||||
|
||||
const level = z.coerce.number().int().min(1).max(4);
|
||||
|
||||
const assetSchema = z.object({
|
||||
@@ -48,9 +48,7 @@ function parseAssetForm(formData: FormData) {
|
||||
}
|
||||
|
||||
export async function createAsset(formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "asset:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("asset:write");
|
||||
|
||||
const data = parseAssetForm(formData);
|
||||
// tenantId doppelt gemoppelt: explizit für die Typen, der Guard injiziert ohnehin
|
||||
@@ -71,9 +69,7 @@ export async function createAsset(formData: FormData) {
|
||||
}
|
||||
|
||||
export async function updateAsset(assetId: string, formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "asset:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("asset:write");
|
||||
|
||||
const before = await db.asset.findUnique({ where: { id: assetId } });
|
||||
if (!before) throw new Error("Asset nicht gefunden");
|
||||
@@ -95,9 +91,7 @@ export async function updateAsset(assetId: string, formData: FormData) {
|
||||
}
|
||||
|
||||
export async function deleteAsset(assetId: string) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "asset:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("asset:write");
|
||||
|
||||
const before = await db.asset.findUnique({ where: { id: assetId } });
|
||||
if (!before) throw new Error("Asset nicht gefunden");
|
||||
@@ -117,9 +111,7 @@ export async function deleteAsset(assetId: string) {
|
||||
}
|
||||
|
||||
export async function addAssetRelation(assetId: string, formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "asset:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("asset:write");
|
||||
|
||||
const relatedAssetId = z.string().min(1).parse(formData.get("relatedAssetId"));
|
||||
if (relatedAssetId === assetId) return;
|
||||
@@ -153,9 +145,7 @@ export async function addAssetRelation(assetId: string, formData: FormData) {
|
||||
}
|
||||
|
||||
export async function removeAssetRelation(assetId: string, relationId: string) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "asset:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("asset:write");
|
||||
|
||||
const before = await db.assetRelation.findUnique({ where: { id: relationId } });
|
||||
if (!before) return;
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
import { requireSession } from "@/server/auth";
|
||||
import { dbForTenant, prisma } from "@/server/db";
|
||||
import { requirePermission } from "@/server/rbac";
|
||||
import { prisma } from "@/server/db";
|
||||
import { moduleGuard } from "@/server/action-guard";
|
||||
import { writeAuditLog } from "@/server/audit";
|
||||
|
||||
const guard = moduleGuard("measures");
|
||||
import { recomputeResidualRisk } from "@/server/risk-calc";
|
||||
|
||||
const measureSchema = z.object({
|
||||
@@ -54,9 +55,7 @@ async function nextRefNo(tenantId: string) {
|
||||
}
|
||||
|
||||
export async function createMeasure(formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "measure:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("measure:write");
|
||||
|
||||
const data = parseMeasureForm(formData);
|
||||
const measure = await db.measure.create({
|
||||
@@ -81,9 +80,7 @@ export async function createMeasure(formData: FormData) {
|
||||
}
|
||||
|
||||
export async function updateMeasure(measureId: string, formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "measure:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("measure:write");
|
||||
|
||||
const before = await db.measure.findUnique({ where: { id: measureId } });
|
||||
if (!before) throw new Error("Maßnahme nicht gefunden");
|
||||
@@ -107,9 +104,7 @@ export async function updateMeasure(measureId: string, formData: FormData) {
|
||||
|
||||
/** Status-Wechsel per Drag-and-Drop auf dem Kanban-Board. */
|
||||
export async function updateMeasureStatus(measureId: string, status: string) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "measure:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("measure:write");
|
||||
|
||||
const parsed = z.enum(["OPEN", "IN_PROGRESS", "DONE"]).parse(status);
|
||||
const before = await db.measure.findUnique({ where: { id: measureId } });
|
||||
@@ -131,9 +126,7 @@ export async function updateMeasureStatus(measureId: string, status: string) {
|
||||
}
|
||||
|
||||
export async function deleteMeasure(measureId: string) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "measure:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("measure:write");
|
||||
|
||||
const before = await db.measure.findUnique({
|
||||
where: { id: measureId },
|
||||
@@ -162,9 +155,7 @@ export async function deleteMeasure(measureId: string) {
|
||||
|
||||
/** Bestehende Maßnahme mit einem Risiko verknüpfen (inkl. Minderung). */
|
||||
export async function linkMeasureToRisk(riskId: string, formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "risk:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("risk:write");
|
||||
|
||||
const measureId = z.string().min(1).parse(formData.get("measureId"));
|
||||
const reductionLikelihood = reduction.parse(formData.get("reductionLikelihood") ?? 0);
|
||||
@@ -201,10 +192,7 @@ export async function linkMeasureToRisk(riskId: string, formData: FormData) {
|
||||
|
||||
/** Neue Maßnahme direkt aus dem Risiko heraus anlegen und verknüpfen (SPEC §4.2). */
|
||||
export async function createMeasureForRisk(riskId: string, formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "risk:write");
|
||||
requirePermission(session, "measure:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("risk:write", "measure:write");
|
||||
|
||||
const riskCount = await db.risk.count({ where: { id: riskId } });
|
||||
if (riskCount !== 1) throw new Error("Risiko nicht gefunden");
|
||||
@@ -245,9 +233,7 @@ export async function createMeasureForRisk(riskId: string, formData: FormData) {
|
||||
}
|
||||
|
||||
export async function unlinkMeasureFromRisk(riskId: string, riskMeasureId: string) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "risk:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("risk:write");
|
||||
|
||||
const before = await db.riskMeasure.findUnique({ where: { id: riskMeasureId } });
|
||||
if (!before) return;
|
||||
|
||||
@@ -3,20 +3,17 @@
|
||||
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 { requireModule } from "@/server/modules";
|
||||
import { moduleGuard } from "@/server/action-guard";
|
||||
import { type Permission } from "@/server/rbac";
|
||||
import { writeAuditLog } from "@/server/audit";
|
||||
|
||||
const optDate = (v: FormDataEntryValue | null) => (v && String(v) ? new Date(String(v)) : null);
|
||||
const str = (v: FormDataEntryValue | null) => (v ? String(v).trim() : "");
|
||||
|
||||
async function guard() {
|
||||
const session = await requireSession();
|
||||
await requireModule("policies"); // API-Ebene: deaktiviertes Modul sperrt auch Actions
|
||||
requirePermission(session, "policy:write");
|
||||
return { session, db: dbForTenant(session.user.tenantId) };
|
||||
const guardModule = moduleGuard("policies");
|
||||
/** Modul-Gating „policies" + RBAC. Default-Recht policy:write; Freigabe nutzt policy:approve. */
|
||||
async function guard(permission: Permission = "policy:write") {
|
||||
return guardModule(permission);
|
||||
}
|
||||
|
||||
/* ── Krypto-Register (VA-07) ── */
|
||||
@@ -244,9 +241,7 @@ export async function submitForApproval(code: string) {
|
||||
}
|
||||
|
||||
export async function approvePolicy(code: string) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "policy:approve");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("policy:approve");
|
||||
const doc = await db.policyDocument.findFirst({ where: { code } });
|
||||
if (!doc) throw new Error("Dokument nicht gefunden");
|
||||
if (doc.status !== "IN_FREIGABE") throw new Error("Dokument ist nicht in Freigabe");
|
||||
@@ -261,9 +256,7 @@ export async function approvePolicy(code: string) {
|
||||
}
|
||||
|
||||
export async function rejectPolicy(code: string, formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "policy:approve");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("policy:approve");
|
||||
const doc = await db.policyDocument.findFirst({ where: { code } });
|
||||
if (!doc) throw new Error("Dokument nicht gefunden");
|
||||
const reason = str(formData.get("reason"));
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
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 { 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(),
|
||||
@@ -30,9 +30,7 @@ function parseProcessForm(formData: FormData) {
|
||||
}
|
||||
|
||||
export async function createProcess(formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "bia:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("bia:write");
|
||||
|
||||
const data = parseProcessForm(formData);
|
||||
// tenantId explizit für die Typen, der Guard injiziert ohnehin
|
||||
@@ -53,9 +51,7 @@ export async function createProcess(formData: FormData) {
|
||||
}
|
||||
|
||||
export async function updateProcess(processId: string, formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "bia:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("bia:write");
|
||||
|
||||
const before = await db.process.findUnique({ where: { id: processId } });
|
||||
if (!before) throw new Error("Prozess nicht gefunden");
|
||||
@@ -77,9 +73,7 @@ export async function updateProcess(processId: string, formData: FormData) {
|
||||
}
|
||||
|
||||
export async function deleteProcess(processId: string) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "bia:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("bia:write");
|
||||
|
||||
const before = await db.process.findUnique({ where: { id: processId } });
|
||||
if (!before) throw new Error("Prozess nicht gefunden");
|
||||
@@ -99,9 +93,7 @@ export async function deleteProcess(processId: string) {
|
||||
}
|
||||
|
||||
export async function assignAsset(processId: string, formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "bia:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
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"));
|
||||
@@ -128,9 +120,7 @@ export async function assignAsset(processId: string, formData: FormData) {
|
||||
}
|
||||
|
||||
export async function unassignAsset(processId: string, processAssetId: string) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "bia:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("bia:write");
|
||||
|
||||
const before = await db.processAsset.findUnique({ where: { id: processAssetId } });
|
||||
if (!before) return;
|
||||
@@ -152,9 +142,7 @@ const hours = z
|
||||
.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 { session, db } = await guard("bia:write");
|
||||
|
||||
const processCount = await db.process.count({ where: { id: processId } });
|
||||
if (processCount !== 1) throw new Error("Prozess nicht gefunden");
|
||||
@@ -197,9 +185,7 @@ export async function saveBia(processId: string, formData: FormData) {
|
||||
* weiter über die inkrementellen Aktionen assignAsset/unassignAsset.
|
||||
*/
|
||||
export async function saveProcessAll(processId: string, formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "bia:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("bia:write");
|
||||
|
||||
const before = await db.process.findUnique({
|
||||
where: { id: processId },
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
import { requireSession } from "@/server/auth";
|
||||
import { dbForTenant, prisma } from "@/server/db";
|
||||
import { requirePermission } from "@/server/rbac";
|
||||
import { prisma } from "@/server/db";
|
||||
import { moduleGuard } from "@/server/action-guard";
|
||||
import { writeAuditLog } from "@/server/audit";
|
||||
|
||||
const guard = moduleGuard("risk");
|
||||
import { recomputeResidualRisk } from "@/server/risk-calc";
|
||||
|
||||
const scale = z.coerce.number().int().min(1).max(5);
|
||||
@@ -53,9 +54,7 @@ function parseRiskForm(formData: FormData) {
|
||||
}
|
||||
|
||||
export async function createRisk(formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "risk:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("risk:write");
|
||||
|
||||
const data = parseRiskForm(formData);
|
||||
// Laufende Nummer je Mandant (MVP: max+1; bei Kollision greift der Unique-Index)
|
||||
@@ -97,9 +96,7 @@ export async function createRisk(formData: FormData) {
|
||||
}
|
||||
|
||||
export async function updateRisk(riskId: string, formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "risk:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("risk:write");
|
||||
|
||||
const before = await db.risk.findUnique({ where: { id: riskId } });
|
||||
if (!before) throw new Error("Risiko nicht gefunden");
|
||||
@@ -123,9 +120,7 @@ export async function updateRisk(riskId: string, formData: FormData) {
|
||||
}
|
||||
|
||||
export async function deleteRisk(riskId: string) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "risk:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("risk:write");
|
||||
|
||||
const before = await db.risk.findUnique({ where: { id: riskId } });
|
||||
if (!before) throw new Error("Risiko nicht gefunden");
|
||||
@@ -145,9 +140,7 @@ export async function deleteRisk(riskId: string) {
|
||||
}
|
||||
|
||||
export async function addRiskAsset(riskId: string, formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "risk:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("risk:write");
|
||||
|
||||
const assetId = z.string().min(1).parse(formData.get("assetId"));
|
||||
const assetCount = await db.asset.count({ where: { id: assetId } });
|
||||
@@ -173,9 +166,7 @@ export async function addRiskAsset(riskId: string, formData: FormData) {
|
||||
}
|
||||
|
||||
export async function removeRiskAsset(riskId: string, riskAssetId: string) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "risk:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("risk:write");
|
||||
|
||||
const before = await db.riskAsset.findUnique({ where: { id: riskAssetId } });
|
||||
if (!before) return;
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
import { requireSession } from "@/server/auth";
|
||||
import { dbForTenant, prisma } from "@/server/db";
|
||||
import { requirePermission } from "@/server/rbac";
|
||||
import { prisma } from "@/server/db";
|
||||
import { moduleGuard } from "@/server/action-guard";
|
||||
import { writeAuditLog } from "@/server/audit";
|
||||
|
||||
const guard = moduleGuard("suppliers");
|
||||
import { withQuery } from "@/lib/supplier";
|
||||
|
||||
const level = z.coerce.number().int().min(1).max(4);
|
||||
@@ -45,9 +46,7 @@ function parse(formData: FormData) {
|
||||
}
|
||||
|
||||
export async function createService(formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "supplier:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("supplier:write");
|
||||
const { asset, profile } = parse(formData);
|
||||
const created = await db.asset.create({
|
||||
data: { ...asset, type: "IT_SERVICE", tenantId: session.user.tenantId, createdBy: session.user.id },
|
||||
@@ -63,9 +62,7 @@ export async function createService(formData: FormData) {
|
||||
}
|
||||
|
||||
export async function updateService(assetId: string, formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "supplier:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("supplier:write");
|
||||
const before = await db.asset.findUnique({ where: { id: assetId }, include: { serviceProfile: true } });
|
||||
if (!before) throw new Error("Service nicht gefunden");
|
||||
const { asset, profile } = parse(formData);
|
||||
@@ -79,9 +76,7 @@ export async function updateService(assetId: string, formData: FormData) {
|
||||
}
|
||||
|
||||
export async function deleteService(assetId: string, returnTo: string = "/suppliers?tab=services") {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "supplier:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("supplier:write");
|
||||
const before = await db.asset.findUnique({ where: { id: assetId } });
|
||||
if (!before) throw new Error("Service nicht gefunden");
|
||||
await db.asset.delete({ where: { id: assetId } });
|
||||
@@ -92,9 +87,7 @@ export async function deleteService(assetId: string, returnTo: string = "/suppli
|
||||
}
|
||||
|
||||
export async function addRaci(assetId: string, formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "supplier:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("supplier:write");
|
||||
if ((await db.asset.count({ where: { id: assetId } })) !== 1) throw new Error("Nicht gefunden");
|
||||
await db.serviceControlResponsibility.create({
|
||||
data: {
|
||||
@@ -114,9 +107,7 @@ export async function addRaci(assetId: string, formData: FormData) {
|
||||
|
||||
/** Verantwortung einer RACI-Zeile per Klick ändern (PROVIDER/US/SHARED durchschalten). */
|
||||
export async function cycleRaci(assetId: string, raciId: string) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "supplier:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("supplier:write");
|
||||
const row = await db.serviceControlResponsibility.findUnique({ where: { id: raciId } });
|
||||
if (!row) return;
|
||||
const next = { PROVIDER: "US", US: "SHARED", SHARED: "PROVIDER" } as const;
|
||||
@@ -127,9 +118,7 @@ export async function cycleRaci(assetId: string, raciId: string) {
|
||||
}
|
||||
|
||||
export async function deleteRaci(assetId: string, raciId: string) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "supplier:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("supplier:write");
|
||||
await db.serviceControlResponsibility.delete({ where: { id: raciId } });
|
||||
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "delete", entity: "service_raci", entityId: assetId });
|
||||
revalidatePath("/suppliers");
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
import { requireSession } from "@/server/auth";
|
||||
import { dbForTenant, prisma } from "@/server/db";
|
||||
import { requirePermission } from "@/server/rbac";
|
||||
import { prisma, type TenantDb } from "@/server/db";
|
||||
import { moduleGuard } from "@/server/action-guard";
|
||||
import { writeAuditLog } from "@/server/audit";
|
||||
import { withQuery } from "@/lib/supplier";
|
||||
|
||||
const guard = moduleGuard("suppliers");
|
||||
|
||||
const level = z.coerce.number().int().min(1).max(4);
|
||||
|
||||
const supplierSchema = z.object({
|
||||
@@ -67,9 +68,7 @@ function parse(formData: FormData) {
|
||||
}
|
||||
|
||||
export async function createSupplier(formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "supplier:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("supplier:write");
|
||||
const { asset, profile } = parse(formData);
|
||||
|
||||
const created = await db.asset.create({
|
||||
@@ -100,9 +99,7 @@ export async function createSupplier(formData: FormData) {
|
||||
}
|
||||
|
||||
export async function updateSupplier(assetId: string, formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "supplier:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("supplier:write");
|
||||
const before = await db.asset.findUnique({ where: { id: assetId }, include: { supplierProfile: true } });
|
||||
if (!before) throw new Error("Lieferant nicht gefunden");
|
||||
const { asset, profile } = parse(formData);
|
||||
@@ -116,9 +113,7 @@ export async function updateSupplier(assetId: string, formData: FormData) {
|
||||
}
|
||||
|
||||
export async function deleteSupplier(assetId: string, returnTo: string = "/suppliers") {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "supplier:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("supplier:write");
|
||||
const before = await db.asset.findUnique({ where: { id: assetId } });
|
||||
if (!before) throw new Error("Lieferant nicht gefunden");
|
||||
await db.asset.delete({ where: { id: assetId } }); // Profil + Kinder kaskadieren
|
||||
@@ -128,17 +123,16 @@ export async function deleteSupplier(assetId: string, returnTo: string = "/suppl
|
||||
redirect(returnTo);
|
||||
}
|
||||
|
||||
async function ensure(session: Awaited<ReturnType<typeof requireSession>>, assetId: string) {
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
/** Stellt sicher, dass das Asset zum Mandanten gehört (Guard hat Modul/Recht bereits geprüft). */
|
||||
async function ensure(db: TenantDb, assetId: string) {
|
||||
if ((await db.asset.count({ where: { id: assetId } })) !== 1) throw new Error("Nicht gefunden");
|
||||
return db;
|
||||
}
|
||||
const optDate = (v: FormDataEntryValue | null) => (v && String(v) ? new Date(String(v)) : null);
|
||||
|
||||
export async function addContract(assetId: string, formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "supplier:write");
|
||||
const db = await ensure(session, assetId);
|
||||
const { session, db } = await guard("supplier:write");
|
||||
await ensure(db, assetId);
|
||||
const bool = (n: string) => formData.get(n) === "on";
|
||||
await db.contract.create({
|
||||
data: {
|
||||
@@ -160,9 +154,8 @@ export async function addContract(assetId: string, formData: FormData) {
|
||||
}
|
||||
|
||||
export async function addNda(assetId: string, formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "supplier:write");
|
||||
const db = await ensure(session, assetId);
|
||||
const { session, db } = await guard("supplier:write");
|
||||
await ensure(db, assetId);
|
||||
await db.nda.create({
|
||||
data: {
|
||||
assetId,
|
||||
@@ -181,9 +174,8 @@ export async function addNda(assetId: string, formData: FormData) {
|
||||
}
|
||||
|
||||
export async function addEvidence(assetId: string, formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "supplier:write");
|
||||
const db = await ensure(session, assetId);
|
||||
const { session, db } = await guard("supplier:write");
|
||||
await ensure(db, assetId);
|
||||
await db.supplierEvidence.create({
|
||||
data: {
|
||||
assetId,
|
||||
@@ -201,9 +193,8 @@ export async function addEvidence(assetId: string, formData: FormData) {
|
||||
}
|
||||
|
||||
export async function addAssessment(assetId: string, formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "supplier:write");
|
||||
const db = await ensure(session, assetId);
|
||||
const { session, db } = await guard("supplier:write");
|
||||
await ensure(db, assetId);
|
||||
await db.supplierAssessment.create({
|
||||
data: {
|
||||
assetId,
|
||||
@@ -221,9 +212,8 @@ export async function addAssessment(assetId: string, formData: FormData) {
|
||||
}
|
||||
|
||||
export async function addDecision(assetId: string, formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "supplier:write");
|
||||
const db = await ensure(session, assetId);
|
||||
const { session, db } = await guard("supplier:write");
|
||||
await ensure(db, assetId);
|
||||
await db.managementDecision.create({
|
||||
data: {
|
||||
assetId,
|
||||
@@ -241,10 +231,7 @@ export async function addDecision(assetId: string, formData: FormData) {
|
||||
|
||||
/** Gate-Kompensation: echtes Risiko im Risikomodul anlegen und mit dem Lieferanten verknüpfen. */
|
||||
export async function createGateRisk(assetId: string) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "supplier:write");
|
||||
requirePermission(session, "risk:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("supplier:write", "risk:write");
|
||||
const asset = await db.asset.findUnique({ where: { id: assetId } });
|
||||
if (!asset) throw new Error("Lieferant nicht gefunden");
|
||||
|
||||
@@ -274,9 +261,8 @@ export async function createGateRisk(assetId: string) {
|
||||
|
||||
/** ISB-Freigabe des Reifegrads (Abweichung nur mit Begründung → Audit-Log). */
|
||||
export async function approveMaturity(assetId: string, computed: number, formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "supplier:write");
|
||||
const db = await ensure(session, assetId);
|
||||
const { session, db } = await guard("supplier:write");
|
||||
await ensure(db, assetId);
|
||||
const isbValue = z.coerce.number().min(0).max(3).parse(formData.get("isbValue"));
|
||||
const justification = (formData.get("justification") as string)?.trim() || null;
|
||||
if (Math.abs(isbValue - computed) > 0.01 && !justification) {
|
||||
@@ -296,9 +282,7 @@ export async function deleteChild(
|
||||
kind: "contract" | "nda" | "evidence" | "assessment" | "decision" | "subcontractor",
|
||||
id: string
|
||||
) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "supplier:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { session, db } = await guard("supplier:write");
|
||||
const map = {
|
||||
contract: db.contract,
|
||||
nda: db.nda,
|
||||
|
||||
+3
-1
@@ -8,7 +8,8 @@ import { prisma } from "./db";
|
||||
export async function writeAuditLog(entry: {
|
||||
tenantId: string;
|
||||
actorId?: string;
|
||||
action: "create" | "update" | "delete" | "login" | "logout" | "export" | "import";
|
||||
action: "create" | "update" | "delete" | "login" | "logout" | "export" | "import" | "denied";
|
||||
scope?: "tenant" | "platform";
|
||||
entity: string;
|
||||
entityId?: string;
|
||||
before?: unknown;
|
||||
@@ -17,6 +18,7 @@ export async function writeAuditLog(entry: {
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
tenantId: entry.tenantId,
|
||||
scope: entry.scope ?? "tenant",
|
||||
actorId: entry.actorId,
|
||||
action: entry.action,
|
||||
entity: entry.entity,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import type { Session } from "next-auth";
|
||||
import { requireSession } from "@/server/auth";
|
||||
import { dbForTenant } from "@/server/db";
|
||||
import { writeAuditLog } from "@/server/audit";
|
||||
|
||||
/**
|
||||
* Serverseitige Modul-Durchsetzung (§3.4): blockiert den Zugriff auf ein für den
|
||||
@@ -16,3 +18,36 @@ export async function requireModule(moduleKey: string) {
|
||||
if (row && !row.enabled) redirect("/dashboard?module=disabled");
|
||||
return session;
|
||||
}
|
||||
|
||||
/** Wird geworfen, wenn eine Server-Action ein für den Mandanten deaktiviertes Modul betrifft. */
|
||||
export class ModuleDisabledError extends Error {
|
||||
constructor(public readonly moduleKey: string) {
|
||||
super(`Modul „${moduleKey}" ist für diesen Mandanten deaktiviert.`);
|
||||
this.name = "ModuleDisabledError";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action-Ebene der Modul-Durchsetzung (§3.4): prüft für eine bereits ermittelte
|
||||
* Session, ob das Modul aktiv ist. Anders als `requireModule` (Redirect, für
|
||||
* Layouts/Reads) **wirft** diese Variante bei Deaktivierung — der Layout-Guard
|
||||
* greift bei Server-Actions erst nach der Mutation, daher hier vor dem Schreiben.
|
||||
* Ein deaktiviertes Modul wird zusätzlich als Zugriffsverweigerung protokolliert.
|
||||
* Fehlende TenantModule-Zeile ⇒ Modul gilt als aktiv (Default an).
|
||||
*/
|
||||
export async function assertModuleEnabled(session: Session, moduleKey: string) {
|
||||
const tenantId = session.user.tenantId;
|
||||
const row = await dbForTenant(tenantId).tenantModule.findUnique({
|
||||
where: { tenantId_moduleKey: { tenantId, moduleKey } },
|
||||
});
|
||||
if (row && !row.enabled) {
|
||||
await writeAuditLog({
|
||||
tenantId,
|
||||
actorId: session.user.id,
|
||||
action: "denied",
|
||||
entity: "module",
|
||||
entityId: moduleKey,
|
||||
});
|
||||
throw new ModuleDisabledError(moduleKey);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user