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:
@@ -4,6 +4,8 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
"check:guards": "tsx scripts/check-module-guards.ts",
|
||||||
|
"prebuild": "tsx scripts/check-module-guards.ts",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
* Vollständigkeitscheck der serverseitigen Modul-Durchsetzung (§3.4, Phase-1-Härtung).
|
||||||
|
*
|
||||||
|
* Jede mutierende Server-Action eines gegateten Moduls MUSS über einen
|
||||||
|
* `moduleGuard("<key>")`-Guard laufen (siehe src/server/action-guard.ts), damit ein
|
||||||
|
* für den Mandanten deaktiviertes Modul auch Writes serverseitig abweist.
|
||||||
|
*
|
||||||
|
* Dieses Script erzwingt das statisch: Es kennt die Zuordnung Action-Datei → Modul
|
||||||
|
* und schlägt fehl (Exit 1 → Build/Test rot), sobald
|
||||||
|
* - eine neue Action-Datei nicht zugeordnet ist ("vergessener Endpoint"),
|
||||||
|
* - eine gegatete Datei den erwarteten moduleGuard nicht verwendet, oder
|
||||||
|
* - eine exportierte Action nicht über `await guard(...)` läuft.
|
||||||
|
*
|
||||||
|
* Neue Action-Datei anlegen ⇒ hier eintragen (Modul-Key oder "EXEMPT").
|
||||||
|
*/
|
||||||
|
import { readdirSync, readFileSync } from "node:fs";
|
||||||
|
import { join, dirname } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { MODULE_KEYS } from "../src/lib/modules";
|
||||||
|
|
||||||
|
const ACTIONS_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "src", "server", "actions");
|
||||||
|
|
||||||
|
/** Zuordnung Action-Datei → Modul-Key. "EXEMPT" = kein gegatetes Fachmodul (eigene Auth). */
|
||||||
|
const ACTION_MODULE: Record<string, string> = {
|
||||||
|
"assets.ts": "assets",
|
||||||
|
"processes.ts": "bia",
|
||||||
|
"risks.ts": "risk",
|
||||||
|
"measures.ts": "measures",
|
||||||
|
"suppliers.ts": "suppliers",
|
||||||
|
"services.ts": "suppliers",
|
||||||
|
"policies.ts": "policies",
|
||||||
|
// Plattform-Admin (isPlatformAdmin) und Kunden-Einstellungen (tenant:manage) sind
|
||||||
|
// keine per TenantModule gegateten Fachmodule — eigene Autorisierung, kein moduleGuard.
|
||||||
|
"admin.ts": "EXEMPT",
|
||||||
|
"tenant-settings.ts": "EXEMPT",
|
||||||
|
};
|
||||||
|
|
||||||
|
const errors: string[] = [];
|
||||||
|
const files = readdirSync(ACTIONS_DIR).filter((f) => f.endsWith(".ts"));
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const mapped = ACTION_MODULE[file];
|
||||||
|
if (!mapped) {
|
||||||
|
errors.push(
|
||||||
|
`Nicht zugeordnete Action-Datei: ${file} — in scripts/check-module-guards.ts eintragen (Modul-Key oder "EXEMPT").`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const src = readFileSync(join(ACTIONS_DIR, file), "utf8");
|
||||||
|
|
||||||
|
if (mapped === "EXEMPT") {
|
||||||
|
if (!/require(Session|PlatformAdmin|Permission)/.test(src)) {
|
||||||
|
errors.push(`${file}: als EXEMPT markiert, aber keine erkennbare Auth-Prüfung.`);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!MODULE_KEYS.includes(mapped)) {
|
||||||
|
errors.push(`${file}: unbekannter Modul-Key "${mapped}" (nicht in src/lib/modules.ts).`);
|
||||||
|
}
|
||||||
|
if (!src.includes(`moduleGuard("${mapped}")`)) {
|
||||||
|
errors.push(`${file}: erwartet moduleGuard("${mapped}") — Modul-Gating fehlt oder falscher Key.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jede exportierte Server-Action muss über await guard(...) laufen.
|
||||||
|
const exportRe = /export async function (\w+)\s*\(/g;
|
||||||
|
const positions: { name: string; index: number }[] = [];
|
||||||
|
let m: RegExpExecArray | null;
|
||||||
|
while ((m = exportRe.exec(src))) positions.push({ name: m[1], index: m.index });
|
||||||
|
for (let i = 0; i < positions.length; i++) {
|
||||||
|
const start = positions[i].index;
|
||||||
|
const end = i + 1 < positions.length ? positions[i + 1].index : src.length;
|
||||||
|
if (!/await guard\(/.test(src.slice(start, end))) {
|
||||||
|
errors.push(
|
||||||
|
`${file}: Action "${positions[i].name}" läuft nicht über await guard(...) — Modul-/Rechte-Guard fehlt.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length) {
|
||||||
|
console.error("✗ Modul-Guard-Vollständigkeitscheck fehlgeschlagen:");
|
||||||
|
for (const e of errors) console.error(" - " + e);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
`✓ Modul-Guard-Vollständigkeitscheck: ${files.length} Action-Dateien geprüft — alle mutierenden Actions sind modul- und rechtegegated.`
|
||||||
|
);
|
||||||
@@ -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 { revalidatePath } from "next/cache";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { requireSession } from "@/server/auth";
|
import { moduleGuard } from "@/server/action-guard";
|
||||||
import { dbForTenant } from "@/server/db";
|
|
||||||
import { requirePermission } from "@/server/rbac";
|
|
||||||
import { writeAuditLog } from "@/server/audit";
|
import { writeAuditLog } from "@/server/audit";
|
||||||
|
|
||||||
|
const guard = moduleGuard("assets");
|
||||||
|
|
||||||
const level = z.coerce.number().int().min(1).max(4);
|
const level = z.coerce.number().int().min(1).max(4);
|
||||||
|
|
||||||
const assetSchema = z.object({
|
const assetSchema = z.object({
|
||||||
@@ -48,9 +48,7 @@ function parseAssetForm(formData: FormData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function createAsset(formData: FormData) {
|
export async function createAsset(formData: FormData) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("asset:write");
|
||||||
requirePermission(session, "asset:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
|
|
||||||
const data = parseAssetForm(formData);
|
const data = parseAssetForm(formData);
|
||||||
// tenantId doppelt gemoppelt: explizit für die Typen, der Guard injiziert ohnehin
|
// 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) {
|
export async function updateAsset(assetId: string, formData: FormData) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("asset:write");
|
||||||
requirePermission(session, "asset:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
|
|
||||||
const before = await db.asset.findUnique({ where: { id: assetId } });
|
const before = await db.asset.findUnique({ where: { id: assetId } });
|
||||||
if (!before) throw new Error("Asset nicht gefunden");
|
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) {
|
export async function deleteAsset(assetId: string) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("asset:write");
|
||||||
requirePermission(session, "asset:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
|
|
||||||
const before = await db.asset.findUnique({ where: { id: assetId } });
|
const before = await db.asset.findUnique({ where: { id: assetId } });
|
||||||
if (!before) throw new Error("Asset nicht gefunden");
|
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) {
|
export async function addAssetRelation(assetId: string, formData: FormData) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("asset:write");
|
||||||
requirePermission(session, "asset:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
|
|
||||||
const relatedAssetId = z.string().min(1).parse(formData.get("relatedAssetId"));
|
const relatedAssetId = z.string().min(1).parse(formData.get("relatedAssetId"));
|
||||||
if (relatedAssetId === assetId) return;
|
if (relatedAssetId === assetId) return;
|
||||||
@@ -153,9 +145,7 @@ export async function addAssetRelation(assetId: string, formData: FormData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function removeAssetRelation(assetId: string, relationId: string) {
|
export async function removeAssetRelation(assetId: string, relationId: string) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("asset:write");
|
||||||
requirePermission(session, "asset:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
|
|
||||||
const before = await db.assetRelation.findUnique({ where: { id: relationId } });
|
const before = await db.assetRelation.findUnique({ where: { id: relationId } });
|
||||||
if (!before) return;
|
if (!before) return;
|
||||||
|
|||||||
@@ -3,10 +3,11 @@
|
|||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { requireSession } from "@/server/auth";
|
import { prisma } from "@/server/db";
|
||||||
import { dbForTenant, prisma } from "@/server/db";
|
import { moduleGuard } from "@/server/action-guard";
|
||||||
import { requirePermission } from "@/server/rbac";
|
|
||||||
import { writeAuditLog } from "@/server/audit";
|
import { writeAuditLog } from "@/server/audit";
|
||||||
|
|
||||||
|
const guard = moduleGuard("measures");
|
||||||
import { recomputeResidualRisk } from "@/server/risk-calc";
|
import { recomputeResidualRisk } from "@/server/risk-calc";
|
||||||
|
|
||||||
const measureSchema = z.object({
|
const measureSchema = z.object({
|
||||||
@@ -54,9 +55,7 @@ async function nextRefNo(tenantId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function createMeasure(formData: FormData) {
|
export async function createMeasure(formData: FormData) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("measure:write");
|
||||||
requirePermission(session, "measure:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
|
|
||||||
const data = parseMeasureForm(formData);
|
const data = parseMeasureForm(formData);
|
||||||
const measure = await db.measure.create({
|
const measure = await db.measure.create({
|
||||||
@@ -81,9 +80,7 @@ export async function createMeasure(formData: FormData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function updateMeasure(measureId: string, formData: FormData) {
|
export async function updateMeasure(measureId: string, formData: FormData) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("measure:write");
|
||||||
requirePermission(session, "measure:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
|
|
||||||
const before = await db.measure.findUnique({ where: { id: measureId } });
|
const before = await db.measure.findUnique({ where: { id: measureId } });
|
||||||
if (!before) throw new Error("Maßnahme nicht gefunden");
|
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. */
|
/** Status-Wechsel per Drag-and-Drop auf dem Kanban-Board. */
|
||||||
export async function updateMeasureStatus(measureId: string, status: string) {
|
export async function updateMeasureStatus(measureId: string, status: string) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("measure:write");
|
||||||
requirePermission(session, "measure:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
|
|
||||||
const parsed = z.enum(["OPEN", "IN_PROGRESS", "DONE"]).parse(status);
|
const parsed = z.enum(["OPEN", "IN_PROGRESS", "DONE"]).parse(status);
|
||||||
const before = await db.measure.findUnique({ where: { id: measureId } });
|
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) {
|
export async function deleteMeasure(measureId: string) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("measure:write");
|
||||||
requirePermission(session, "measure:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
|
|
||||||
const before = await db.measure.findUnique({
|
const before = await db.measure.findUnique({
|
||||||
where: { id: measureId },
|
where: { id: measureId },
|
||||||
@@ -162,9 +155,7 @@ export async function deleteMeasure(measureId: string) {
|
|||||||
|
|
||||||
/** Bestehende Maßnahme mit einem Risiko verknüpfen (inkl. Minderung). */
|
/** Bestehende Maßnahme mit einem Risiko verknüpfen (inkl. Minderung). */
|
||||||
export async function linkMeasureToRisk(riskId: string, formData: FormData) {
|
export async function linkMeasureToRisk(riskId: string, formData: FormData) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("risk:write");
|
||||||
requirePermission(session, "risk:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
|
|
||||||
const measureId = z.string().min(1).parse(formData.get("measureId"));
|
const measureId = z.string().min(1).parse(formData.get("measureId"));
|
||||||
const reductionLikelihood = reduction.parse(formData.get("reductionLikelihood") ?? 0);
|
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). */
|
/** Neue Maßnahme direkt aus dem Risiko heraus anlegen und verknüpfen (SPEC §4.2). */
|
||||||
export async function createMeasureForRisk(riskId: string, formData: FormData) {
|
export async function createMeasureForRisk(riskId: string, formData: FormData) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("risk:write", "measure:write");
|
||||||
requirePermission(session, "risk:write");
|
|
||||||
requirePermission(session, "measure:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
|
|
||||||
const riskCount = await db.risk.count({ where: { id: riskId } });
|
const riskCount = await db.risk.count({ where: { id: riskId } });
|
||||||
if (riskCount !== 1) throw new Error("Risiko nicht gefunden");
|
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) {
|
export async function unlinkMeasureFromRisk(riskId: string, riskMeasureId: string) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("risk:write");
|
||||||
requirePermission(session, "risk:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
|
|
||||||
const before = await db.riskMeasure.findUnique({ where: { id: riskMeasureId } });
|
const before = await db.riskMeasure.findUnique({ where: { id: riskMeasureId } });
|
||||||
if (!before) return;
|
if (!before) return;
|
||||||
|
|||||||
@@ -3,20 +3,17 @@
|
|||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { requireSession } from "@/server/auth";
|
import { moduleGuard } from "@/server/action-guard";
|
||||||
import { dbForTenant } from "@/server/db";
|
import { type Permission } from "@/server/rbac";
|
||||||
import { requirePermission } from "@/server/rbac";
|
|
||||||
import { requireModule } from "@/server/modules";
|
|
||||||
import { writeAuditLog } from "@/server/audit";
|
import { writeAuditLog } from "@/server/audit";
|
||||||
|
|
||||||
const optDate = (v: FormDataEntryValue | null) => (v && String(v) ? new Date(String(v)) : null);
|
const optDate = (v: FormDataEntryValue | null) => (v && String(v) ? new Date(String(v)) : null);
|
||||||
const str = (v: FormDataEntryValue | null) => (v ? String(v).trim() : "");
|
const str = (v: FormDataEntryValue | null) => (v ? String(v).trim() : "");
|
||||||
|
|
||||||
async function guard() {
|
const guardModule = moduleGuard("policies");
|
||||||
const session = await requireSession();
|
/** Modul-Gating „policies" + RBAC. Default-Recht policy:write; Freigabe nutzt policy:approve. */
|
||||||
await requireModule("policies"); // API-Ebene: deaktiviertes Modul sperrt auch Actions
|
async function guard(permission: Permission = "policy:write") {
|
||||||
requirePermission(session, "policy:write");
|
return guardModule(permission);
|
||||||
return { session, db: dbForTenant(session.user.tenantId) };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Krypto-Register (VA-07) ── */
|
/* ── Krypto-Register (VA-07) ── */
|
||||||
@@ -244,9 +241,7 @@ export async function submitForApproval(code: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function approvePolicy(code: string) {
|
export async function approvePolicy(code: string) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("policy:approve");
|
||||||
requirePermission(session, "policy:approve");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
const doc = await db.policyDocument.findFirst({ where: { code } });
|
const doc = await db.policyDocument.findFirst({ where: { code } });
|
||||||
if (!doc) throw new Error("Dokument nicht gefunden");
|
if (!doc) throw new Error("Dokument nicht gefunden");
|
||||||
if (doc.status !== "IN_FREIGABE") throw new Error("Dokument ist nicht in Freigabe");
|
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) {
|
export async function rejectPolicy(code: string, formData: FormData) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("policy:approve");
|
||||||
requirePermission(session, "policy:approve");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
const doc = await db.policyDocument.findFirst({ where: { code } });
|
const doc = await db.policyDocument.findFirst({ where: { code } });
|
||||||
if (!doc) throw new Error("Dokument nicht gefunden");
|
if (!doc) throw new Error("Dokument nicht gefunden");
|
||||||
const reason = str(formData.get("reason"));
|
const reason = str(formData.get("reason"));
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { requireSession } from "@/server/auth";
|
import { moduleGuard } from "@/server/action-guard";
|
||||||
import { dbForTenant } from "@/server/db";
|
|
||||||
import { requirePermission } from "@/server/rbac";
|
|
||||||
import { writeAuditLog } from "@/server/audit";
|
import { writeAuditLog } from "@/server/audit";
|
||||||
|
|
||||||
|
const guard = moduleGuard("bia");
|
||||||
|
|
||||||
const processSchema = z.object({
|
const processSchema = z.object({
|
||||||
name: z.string().trim().min(1).max(200),
|
name: z.string().trim().min(1).max(200),
|
||||||
description: z.string().trim().max(5000).optional(),
|
description: z.string().trim().max(5000).optional(),
|
||||||
@@ -30,9 +30,7 @@ function parseProcessForm(formData: FormData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function createProcess(formData: FormData) {
|
export async function createProcess(formData: FormData) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("bia:write");
|
||||||
requirePermission(session, "bia:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
|
|
||||||
const data = parseProcessForm(formData);
|
const data = parseProcessForm(formData);
|
||||||
// tenantId explizit für die Typen, der Guard injiziert ohnehin
|
// 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) {
|
export async function updateProcess(processId: string, formData: FormData) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("bia:write");
|
||||||
requirePermission(session, "bia:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
|
|
||||||
const before = await db.process.findUnique({ where: { id: processId } });
|
const before = await db.process.findUnique({ where: { id: processId } });
|
||||||
if (!before) throw new Error("Prozess nicht gefunden");
|
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) {
|
export async function deleteProcess(processId: string) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("bia:write");
|
||||||
requirePermission(session, "bia:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
|
|
||||||
const before = await db.process.findUnique({ where: { id: processId } });
|
const before = await db.process.findUnique({ where: { id: processId } });
|
||||||
if (!before) throw new Error("Prozess nicht gefunden");
|
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) {
|
export async function assignAsset(processId: string, formData: FormData) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("bia:write");
|
||||||
requirePermission(session, "bia:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
|
|
||||||
const assetId = z.string().min(1).parse(formData.get("assetId"));
|
const assetId = z.string().min(1).parse(formData.get("assetId"));
|
||||||
const role = z.enum(["PRIMARY", "SECONDARY"]).parse(formData.get("role"));
|
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) {
|
export async function unassignAsset(processId: string, processAssetId: string) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("bia:write");
|
||||||
requirePermission(session, "bia:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
|
|
||||||
const before = await db.processAsset.findUnique({ where: { id: processAssetId } });
|
const before = await db.processAsset.findUnique({ where: { id: processAssetId } });
|
||||||
if (!before) return;
|
if (!before) return;
|
||||||
@@ -152,9 +142,7 @@ const hours = z
|
|||||||
.transform((v) => (v === "" ? null : v));
|
.transform((v) => (v === "" ? null : v));
|
||||||
|
|
||||||
export async function saveBia(processId: string, formData: FormData) {
|
export async function saveBia(processId: string, formData: FormData) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("bia:write");
|
||||||
requirePermission(session, "bia:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
|
|
||||||
const processCount = await db.process.count({ where: { id: processId } });
|
const processCount = await db.process.count({ where: { id: processId } });
|
||||||
if (processCount !== 1) throw new Error("Prozess nicht gefunden");
|
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.
|
* weiter über die inkrementellen Aktionen assignAsset/unassignAsset.
|
||||||
*/
|
*/
|
||||||
export async function saveProcessAll(processId: string, formData: FormData) {
|
export async function saveProcessAll(processId: string, formData: FormData) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("bia:write");
|
||||||
requirePermission(session, "bia:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
|
|
||||||
const before = await db.process.findUnique({
|
const before = await db.process.findUnique({
|
||||||
where: { id: processId },
|
where: { id: processId },
|
||||||
|
|||||||
@@ -3,10 +3,11 @@
|
|||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { requireSession } from "@/server/auth";
|
import { prisma } from "@/server/db";
|
||||||
import { dbForTenant, prisma } from "@/server/db";
|
import { moduleGuard } from "@/server/action-guard";
|
||||||
import { requirePermission } from "@/server/rbac";
|
|
||||||
import { writeAuditLog } from "@/server/audit";
|
import { writeAuditLog } from "@/server/audit";
|
||||||
|
|
||||||
|
const guard = moduleGuard("risk");
|
||||||
import { recomputeResidualRisk } from "@/server/risk-calc";
|
import { recomputeResidualRisk } from "@/server/risk-calc";
|
||||||
|
|
||||||
const scale = z.coerce.number().int().min(1).max(5);
|
const scale = z.coerce.number().int().min(1).max(5);
|
||||||
@@ -53,9 +54,7 @@ function parseRiskForm(formData: FormData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function createRisk(formData: FormData) {
|
export async function createRisk(formData: FormData) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("risk:write");
|
||||||
requirePermission(session, "risk:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
|
|
||||||
const data = parseRiskForm(formData);
|
const data = parseRiskForm(formData);
|
||||||
// Laufende Nummer je Mandant (MVP: max+1; bei Kollision greift der Unique-Index)
|
// 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) {
|
export async function updateRisk(riskId: string, formData: FormData) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("risk:write");
|
||||||
requirePermission(session, "risk:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
|
|
||||||
const before = await db.risk.findUnique({ where: { id: riskId } });
|
const before = await db.risk.findUnique({ where: { id: riskId } });
|
||||||
if (!before) throw new Error("Risiko nicht gefunden");
|
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) {
|
export async function deleteRisk(riskId: string) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("risk:write");
|
||||||
requirePermission(session, "risk:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
|
|
||||||
const before = await db.risk.findUnique({ where: { id: riskId } });
|
const before = await db.risk.findUnique({ where: { id: riskId } });
|
||||||
if (!before) throw new Error("Risiko nicht gefunden");
|
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) {
|
export async function addRiskAsset(riskId: string, formData: FormData) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("risk:write");
|
||||||
requirePermission(session, "risk:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
|
|
||||||
const assetId = z.string().min(1).parse(formData.get("assetId"));
|
const assetId = z.string().min(1).parse(formData.get("assetId"));
|
||||||
const assetCount = await db.asset.count({ where: { id: 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) {
|
export async function removeRiskAsset(riskId: string, riskAssetId: string) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("risk:write");
|
||||||
requirePermission(session, "risk:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
|
|
||||||
const before = await db.riskAsset.findUnique({ where: { id: riskAssetId } });
|
const before = await db.riskAsset.findUnique({ where: { id: riskAssetId } });
|
||||||
if (!before) return;
|
if (!before) return;
|
||||||
|
|||||||
@@ -3,10 +3,11 @@
|
|||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { requireSession } from "@/server/auth";
|
import { prisma } from "@/server/db";
|
||||||
import { dbForTenant, prisma } from "@/server/db";
|
import { moduleGuard } from "@/server/action-guard";
|
||||||
import { requirePermission } from "@/server/rbac";
|
|
||||||
import { writeAuditLog } from "@/server/audit";
|
import { writeAuditLog } from "@/server/audit";
|
||||||
|
|
||||||
|
const guard = moduleGuard("suppliers");
|
||||||
import { withQuery } from "@/lib/supplier";
|
import { withQuery } from "@/lib/supplier";
|
||||||
|
|
||||||
const level = z.coerce.number().int().min(1).max(4);
|
const level = z.coerce.number().int().min(1).max(4);
|
||||||
@@ -45,9 +46,7 @@ function parse(formData: FormData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function createService(formData: FormData) {
|
export async function createService(formData: FormData) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("supplier:write");
|
||||||
requirePermission(session, "supplier:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
const { asset, profile } = parse(formData);
|
const { asset, profile } = parse(formData);
|
||||||
const created = await db.asset.create({
|
const created = await db.asset.create({
|
||||||
data: { ...asset, type: "IT_SERVICE", tenantId: session.user.tenantId, createdBy: session.user.id },
|
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) {
|
export async function updateService(assetId: string, formData: FormData) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("supplier:write");
|
||||||
requirePermission(session, "supplier:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
const before = await db.asset.findUnique({ where: { id: assetId }, include: { serviceProfile: true } });
|
const before = await db.asset.findUnique({ where: { id: assetId }, include: { serviceProfile: true } });
|
||||||
if (!before) throw new Error("Service nicht gefunden");
|
if (!before) throw new Error("Service nicht gefunden");
|
||||||
const { asset, profile } = parse(formData);
|
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") {
|
export async function deleteService(assetId: string, returnTo: string = "/suppliers?tab=services") {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("supplier:write");
|
||||||
requirePermission(session, "supplier:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
const before = await db.asset.findUnique({ where: { id: assetId } });
|
const before = await db.asset.findUnique({ where: { id: assetId } });
|
||||||
if (!before) throw new Error("Service nicht gefunden");
|
if (!before) throw new Error("Service nicht gefunden");
|
||||||
await db.asset.delete({ where: { id: assetId } });
|
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) {
|
export async function addRaci(assetId: string, formData: FormData) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("supplier:write");
|
||||||
requirePermission(session, "supplier:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
if ((await db.asset.count({ where: { id: assetId } })) !== 1) throw new Error("Nicht gefunden");
|
if ((await db.asset.count({ where: { id: assetId } })) !== 1) throw new Error("Nicht gefunden");
|
||||||
await db.serviceControlResponsibility.create({
|
await db.serviceControlResponsibility.create({
|
||||||
data: {
|
data: {
|
||||||
@@ -114,9 +107,7 @@ export async function addRaci(assetId: string, formData: FormData) {
|
|||||||
|
|
||||||
/** Verantwortung einer RACI-Zeile per Klick ändern (PROVIDER/US/SHARED durchschalten). */
|
/** Verantwortung einer RACI-Zeile per Klick ändern (PROVIDER/US/SHARED durchschalten). */
|
||||||
export async function cycleRaci(assetId: string, raciId: string) {
|
export async function cycleRaci(assetId: string, raciId: string) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("supplier:write");
|
||||||
requirePermission(session, "supplier:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
const row = await db.serviceControlResponsibility.findUnique({ where: { id: raciId } });
|
const row = await db.serviceControlResponsibility.findUnique({ where: { id: raciId } });
|
||||||
if (!row) return;
|
if (!row) return;
|
||||||
const next = { PROVIDER: "US", US: "SHARED", SHARED: "PROVIDER" } as const;
|
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) {
|
export async function deleteRaci(assetId: string, raciId: string) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("supplier:write");
|
||||||
requirePermission(session, "supplier:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
await db.serviceControlResponsibility.delete({ where: { id: raciId } });
|
await db.serviceControlResponsibility.delete({ where: { id: raciId } });
|
||||||
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "delete", entity: "service_raci", entityId: assetId });
|
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "delete", entity: "service_raci", entityId: assetId });
|
||||||
revalidatePath("/suppliers");
|
revalidatePath("/suppliers");
|
||||||
|
|||||||
@@ -3,12 +3,13 @@
|
|||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { requireSession } from "@/server/auth";
|
import { prisma, type TenantDb } from "@/server/db";
|
||||||
import { dbForTenant, prisma } from "@/server/db";
|
import { moduleGuard } from "@/server/action-guard";
|
||||||
import { requirePermission } from "@/server/rbac";
|
|
||||||
import { writeAuditLog } from "@/server/audit";
|
import { writeAuditLog } from "@/server/audit";
|
||||||
import { withQuery } from "@/lib/supplier";
|
import { withQuery } from "@/lib/supplier";
|
||||||
|
|
||||||
|
const guard = moduleGuard("suppliers");
|
||||||
|
|
||||||
const level = z.coerce.number().int().min(1).max(4);
|
const level = z.coerce.number().int().min(1).max(4);
|
||||||
|
|
||||||
const supplierSchema = z.object({
|
const supplierSchema = z.object({
|
||||||
@@ -67,9 +68,7 @@ function parse(formData: FormData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function createSupplier(formData: FormData) {
|
export async function createSupplier(formData: FormData) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("supplier:write");
|
||||||
requirePermission(session, "supplier:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
const { asset, profile } = parse(formData);
|
const { asset, profile } = parse(formData);
|
||||||
|
|
||||||
const created = await db.asset.create({
|
const created = await db.asset.create({
|
||||||
@@ -100,9 +99,7 @@ export async function createSupplier(formData: FormData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function updateSupplier(assetId: string, formData: FormData) {
|
export async function updateSupplier(assetId: string, formData: FormData) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("supplier:write");
|
||||||
requirePermission(session, "supplier:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
const before = await db.asset.findUnique({ where: { id: assetId }, include: { supplierProfile: true } });
|
const before = await db.asset.findUnique({ where: { id: assetId }, include: { supplierProfile: true } });
|
||||||
if (!before) throw new Error("Lieferant nicht gefunden");
|
if (!before) throw new Error("Lieferant nicht gefunden");
|
||||||
const { asset, profile } = parse(formData);
|
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") {
|
export async function deleteSupplier(assetId: string, returnTo: string = "/suppliers") {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("supplier:write");
|
||||||
requirePermission(session, "supplier:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
const before = await db.asset.findUnique({ where: { id: assetId } });
|
const before = await db.asset.findUnique({ where: { id: assetId } });
|
||||||
if (!before) throw new Error("Lieferant nicht gefunden");
|
if (!before) throw new Error("Lieferant nicht gefunden");
|
||||||
await db.asset.delete({ where: { id: assetId } }); // Profil + Kinder kaskadieren
|
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);
|
redirect(returnTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensure(session: Awaited<ReturnType<typeof requireSession>>, assetId: string) {
|
/** Stellt sicher, dass das Asset zum Mandanten gehört (Guard hat Modul/Recht bereits geprüft). */
|
||||||
const db = dbForTenant(session.user.tenantId);
|
async function ensure(db: TenantDb, assetId: string) {
|
||||||
if ((await db.asset.count({ where: { id: assetId } })) !== 1) throw new Error("Nicht gefunden");
|
if ((await db.asset.count({ where: { id: assetId } })) !== 1) throw new Error("Nicht gefunden");
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
const optDate = (v: FormDataEntryValue | null) => (v && String(v) ? new Date(String(v)) : null);
|
const optDate = (v: FormDataEntryValue | null) => (v && String(v) ? new Date(String(v)) : null);
|
||||||
|
|
||||||
export async function addContract(assetId: string, formData: FormData) {
|
export async function addContract(assetId: string, formData: FormData) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("supplier:write");
|
||||||
requirePermission(session, "supplier:write");
|
await ensure(db, assetId);
|
||||||
const db = await ensure(session, assetId);
|
|
||||||
const bool = (n: string) => formData.get(n) === "on";
|
const bool = (n: string) => formData.get(n) === "on";
|
||||||
await db.contract.create({
|
await db.contract.create({
|
||||||
data: {
|
data: {
|
||||||
@@ -160,9 +154,8 @@ export async function addContract(assetId: string, formData: FormData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function addNda(assetId: string, formData: FormData) {
|
export async function addNda(assetId: string, formData: FormData) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("supplier:write");
|
||||||
requirePermission(session, "supplier:write");
|
await ensure(db, assetId);
|
||||||
const db = await ensure(session, assetId);
|
|
||||||
await db.nda.create({
|
await db.nda.create({
|
||||||
data: {
|
data: {
|
||||||
assetId,
|
assetId,
|
||||||
@@ -181,9 +174,8 @@ export async function addNda(assetId: string, formData: FormData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function addEvidence(assetId: string, formData: FormData) {
|
export async function addEvidence(assetId: string, formData: FormData) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("supplier:write");
|
||||||
requirePermission(session, "supplier:write");
|
await ensure(db, assetId);
|
||||||
const db = await ensure(session, assetId);
|
|
||||||
await db.supplierEvidence.create({
|
await db.supplierEvidence.create({
|
||||||
data: {
|
data: {
|
||||||
assetId,
|
assetId,
|
||||||
@@ -201,9 +193,8 @@ export async function addEvidence(assetId: string, formData: FormData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function addAssessment(assetId: string, formData: FormData) {
|
export async function addAssessment(assetId: string, formData: FormData) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("supplier:write");
|
||||||
requirePermission(session, "supplier:write");
|
await ensure(db, assetId);
|
||||||
const db = await ensure(session, assetId);
|
|
||||||
await db.supplierAssessment.create({
|
await db.supplierAssessment.create({
|
||||||
data: {
|
data: {
|
||||||
assetId,
|
assetId,
|
||||||
@@ -221,9 +212,8 @@ export async function addAssessment(assetId: string, formData: FormData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function addDecision(assetId: string, formData: FormData) {
|
export async function addDecision(assetId: string, formData: FormData) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("supplier:write");
|
||||||
requirePermission(session, "supplier:write");
|
await ensure(db, assetId);
|
||||||
const db = await ensure(session, assetId);
|
|
||||||
await db.managementDecision.create({
|
await db.managementDecision.create({
|
||||||
data: {
|
data: {
|
||||||
assetId,
|
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. */
|
/** Gate-Kompensation: echtes Risiko im Risikomodul anlegen und mit dem Lieferanten verknüpfen. */
|
||||||
export async function createGateRisk(assetId: string) {
|
export async function createGateRisk(assetId: string) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("supplier:write", "risk:write");
|
||||||
requirePermission(session, "supplier:write");
|
|
||||||
requirePermission(session, "risk:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
const asset = await db.asset.findUnique({ where: { id: assetId } });
|
const asset = await db.asset.findUnique({ where: { id: assetId } });
|
||||||
if (!asset) throw new Error("Lieferant nicht gefunden");
|
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). */
|
/** ISB-Freigabe des Reifegrads (Abweichung nur mit Begründung → Audit-Log). */
|
||||||
export async function approveMaturity(assetId: string, computed: number, formData: FormData) {
|
export async function approveMaturity(assetId: string, computed: number, formData: FormData) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("supplier:write");
|
||||||
requirePermission(session, "supplier:write");
|
await ensure(db, assetId);
|
||||||
const db = await ensure(session, assetId);
|
|
||||||
const isbValue = z.coerce.number().min(0).max(3).parse(formData.get("isbValue"));
|
const isbValue = z.coerce.number().min(0).max(3).parse(formData.get("isbValue"));
|
||||||
const justification = (formData.get("justification") as string)?.trim() || null;
|
const justification = (formData.get("justification") as string)?.trim() || null;
|
||||||
if (Math.abs(isbValue - computed) > 0.01 && !justification) {
|
if (Math.abs(isbValue - computed) > 0.01 && !justification) {
|
||||||
@@ -296,9 +282,7 @@ export async function deleteChild(
|
|||||||
kind: "contract" | "nda" | "evidence" | "assessment" | "decision" | "subcontractor",
|
kind: "contract" | "nda" | "evidence" | "assessment" | "decision" | "subcontractor",
|
||||||
id: string
|
id: string
|
||||||
) {
|
) {
|
||||||
const session = await requireSession();
|
const { session, db } = await guard("supplier:write");
|
||||||
requirePermission(session, "supplier:write");
|
|
||||||
const db = dbForTenant(session.user.tenantId);
|
|
||||||
const map = {
|
const map = {
|
||||||
contract: db.contract,
|
contract: db.contract,
|
||||||
nda: db.nda,
|
nda: db.nda,
|
||||||
|
|||||||
+3
-1
@@ -8,7 +8,8 @@ import { prisma } from "./db";
|
|||||||
export async function writeAuditLog(entry: {
|
export async function writeAuditLog(entry: {
|
||||||
tenantId: string;
|
tenantId: string;
|
||||||
actorId?: 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;
|
entity: string;
|
||||||
entityId?: string;
|
entityId?: string;
|
||||||
before?: unknown;
|
before?: unknown;
|
||||||
@@ -17,6 +18,7 @@ export async function writeAuditLog(entry: {
|
|||||||
await prisma.auditLog.create({
|
await prisma.auditLog.create({
|
||||||
data: {
|
data: {
|
||||||
tenantId: entry.tenantId,
|
tenantId: entry.tenantId,
|
||||||
|
scope: entry.scope ?? "tenant",
|
||||||
actorId: entry.actorId,
|
actorId: entry.actorId,
|
||||||
action: entry.action,
|
action: entry.action,
|
||||||
entity: entry.entity,
|
entity: entry.entity,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
import type { Session } from "next-auth";
|
||||||
import { requireSession } from "@/server/auth";
|
import { requireSession } from "@/server/auth";
|
||||||
import { dbForTenant } from "@/server/db";
|
import { dbForTenant } from "@/server/db";
|
||||||
|
import { writeAuditLog } from "@/server/audit";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serverseitige Modul-Durchsetzung (§3.4): blockiert den Zugriff auf ein für den
|
* 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");
|
if (row && !row.enabled) redirect("/dashboard?module=disabled");
|
||||||
return session;
|
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