From 6ddeedf0b6279d6a309c9a78d10fe3db57a63977 Mon Sep 17 00:00:00 2001 From: Martin Date: Sun, 19 Jul 2026 20:46:14 +0200 Subject: [PATCH] =?UTF-8?q?Serverseitige=20Modul-Durchsetzung=20(=C2=A73.4?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deaktivierte Module werden nicht nur in der Navigation ausgeblendet, sondern serverseitig gesperrt: - requireModule-Guard (server/modules.ts): prüft TenantModule und leitet bei deaktiviertem Modul auf /dashboard?module=disabled um (fehlende Zeile = aktiv). - Modul-Layout je Routenbereich (assets, processes, risks, measures, policies, suppliers, dependencies) — deckt alle Unterrouten mit ab (GET-Zugriff). - API-Ebene: requireModule zusätzlich im gemeinsamen guard() der Policy-Actions (Layout-Guard läuft bei Server-Actions erst nach der Mutation). Verifiziert im Browser: bei deaktiviertem Policies-Modul werden /policies und /policies/R08 auf /dashboard?module=disabled umgeleitet; Nav blendet den Punkt aus. Hinweis: Das gleiche guard()-Einzeiler-Muster kann auf die Actions der übrigen Module übertragen werden (bislang exemplarisch für Policies); die Route-Layouts sichern bereits alle Modul-Seiten. Co-Authored-By: Claude Opus 4.8 --- src/app/(app)/assets/layout.tsx | 7 +++++++ src/app/(app)/dependencies/layout.tsx | 7 +++++++ src/app/(app)/measures/layout.tsx | 7 +++++++ src/app/(app)/policies/layout.tsx | 7 +++++++ src/app/(app)/processes/layout.tsx | 7 +++++++ src/app/(app)/risks/layout.tsx | 7 +++++++ src/app/(app)/suppliers/layout.tsx | 7 +++++++ src/server/actions/policies.ts | 2 ++ src/server/modules.ts | 18 ++++++++++++++++++ 9 files changed, 69 insertions(+) create mode 100644 src/app/(app)/assets/layout.tsx create mode 100644 src/app/(app)/dependencies/layout.tsx create mode 100644 src/app/(app)/measures/layout.tsx create mode 100644 src/app/(app)/policies/layout.tsx create mode 100644 src/app/(app)/processes/layout.tsx create mode 100644 src/app/(app)/risks/layout.tsx create mode 100644 src/app/(app)/suppliers/layout.tsx create mode 100644 src/server/modules.ts diff --git a/src/app/(app)/assets/layout.tsx b/src/app/(app)/assets/layout.tsx new file mode 100644 index 0000000..683b298 --- /dev/null +++ b/src/app/(app)/assets/layout.tsx @@ -0,0 +1,7 @@ +import { requireModule } from "@/server/modules"; + +/** Serverseitige Modul-Durchsetzung für alle Routen dieses Bereichs (§3.4). */ +export default async function ModuleLayout({ children }: { children: React.ReactNode }) { + await requireModule("assets"); + return <>{children}; +} diff --git a/src/app/(app)/dependencies/layout.tsx b/src/app/(app)/dependencies/layout.tsx new file mode 100644 index 0000000..669b89c --- /dev/null +++ b/src/app/(app)/dependencies/layout.tsx @@ -0,0 +1,7 @@ +import { requireModule } from "@/server/modules"; + +/** Serverseitige Modul-Durchsetzung für alle Routen dieses Bereichs (§3.4). */ +export default async function ModuleLayout({ children }: { children: React.ReactNode }) { + await requireModule("dependencies"); + return <>{children}; +} diff --git a/src/app/(app)/measures/layout.tsx b/src/app/(app)/measures/layout.tsx new file mode 100644 index 0000000..3e89260 --- /dev/null +++ b/src/app/(app)/measures/layout.tsx @@ -0,0 +1,7 @@ +import { requireModule } from "@/server/modules"; + +/** Serverseitige Modul-Durchsetzung für alle Routen dieses Bereichs (§3.4). */ +export default async function ModuleLayout({ children }: { children: React.ReactNode }) { + await requireModule("measures"); + return <>{children}; +} diff --git a/src/app/(app)/policies/layout.tsx b/src/app/(app)/policies/layout.tsx new file mode 100644 index 0000000..0bb7129 --- /dev/null +++ b/src/app/(app)/policies/layout.tsx @@ -0,0 +1,7 @@ +import { requireModule } from "@/server/modules"; + +/** Serverseitige Modul-Durchsetzung für alle Routen dieses Bereichs (§3.4). */ +export default async function ModuleLayout({ children }: { children: React.ReactNode }) { + await requireModule("policies"); + return <>{children}; +} diff --git a/src/app/(app)/processes/layout.tsx b/src/app/(app)/processes/layout.tsx new file mode 100644 index 0000000..4599170 --- /dev/null +++ b/src/app/(app)/processes/layout.tsx @@ -0,0 +1,7 @@ +import { requireModule } from "@/server/modules"; + +/** Serverseitige Modul-Durchsetzung für alle Routen dieses Bereichs (§3.4). */ +export default async function ModuleLayout({ children }: { children: React.ReactNode }) { + await requireModule("bia"); + return <>{children}; +} diff --git a/src/app/(app)/risks/layout.tsx b/src/app/(app)/risks/layout.tsx new file mode 100644 index 0000000..d08bde1 --- /dev/null +++ b/src/app/(app)/risks/layout.tsx @@ -0,0 +1,7 @@ +import { requireModule } from "@/server/modules"; + +/** Serverseitige Modul-Durchsetzung für alle Routen dieses Bereichs (§3.4). */ +export default async function ModuleLayout({ children }: { children: React.ReactNode }) { + await requireModule("risk"); + return <>{children}; +} diff --git a/src/app/(app)/suppliers/layout.tsx b/src/app/(app)/suppliers/layout.tsx new file mode 100644 index 0000000..990186c --- /dev/null +++ b/src/app/(app)/suppliers/layout.tsx @@ -0,0 +1,7 @@ +import { requireModule } from "@/server/modules"; + +/** Serverseitige Modul-Durchsetzung für alle Routen dieses Bereichs (§3.4). */ +export default async function ModuleLayout({ children }: { children: React.ReactNode }) { + await requireModule("suppliers"); + return <>{children}; +} diff --git a/src/server/actions/policies.ts b/src/server/actions/policies.ts index f6ad141..826d01a 100644 --- a/src/server/actions/policies.ts +++ b/src/server/actions/policies.ts @@ -6,6 +6,7 @@ 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 { writeAuditLog } from "@/server/audit"; const optDate = (v: FormDataEntryValue | null) => (v && String(v) ? new Date(String(v)) : null); @@ -13,6 +14,7 @@ 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) }; } diff --git a/src/server/modules.ts b/src/server/modules.ts new file mode 100644 index 0000000..c3d6e2c --- /dev/null +++ b/src/server/modules.ts @@ -0,0 +1,18 @@ +import { redirect } from "next/navigation"; +import { requireSession } from "@/server/auth"; +import { dbForTenant } from "@/server/db"; + +/** + * Serverseitige Modul-Durchsetzung (§3.4): blockiert den Zugriff auf ein für den + * Mandanten deaktiviertes Modul. Wird als Modul-Layout je Routenbereich angewandt + * und deckt damit alle Unterrouten ab. Fehlt eine TenantModule-Zeile, gilt das + * Modul als aktiv (Default an, Rückwärtskompatibilität). + */ +export async function requireModule(moduleKey: string) { + const session = await requireSession(); + const row = await dbForTenant(session.user.tenantId).tenantModule.findUnique({ + where: { tenantId_moduleKey: { tenantId: session.user.tenantId, moduleKey } }, + }); + if (row && !row.enabled) redirect("/dashboard?module=disabled"); + return session; +}