Fundament: ISMS-Module entfernt; Craftvia-Rollen, Module, Navigation, i18n-Split

- ISMS-Routen, Actions, Server-/Lib-Code, Komponenten, Prisma-Modelle, Seeds,
  Importer, Skripte und ISMS-Tests entfernt (Fundament bleibt: Auth, Identity,
  MFA/WebAuthn, RBAC, Audit, Mail, Storage, Backup/DSGVO, Plattform-Admin)
- Schema auf Fundament-Modelle reduziert; TenantSettings generisch (+phone/email)
- TENANT_MODELS (db.ts, backup/topology.ts) und PII-Felder ausgedünnt
- RBAC: Rollen tenant-admin/backoffice/team-lead/technician + Craftvia-Permissions
- Modul-Katalog (customers, sites, teams, work_orders, imports, field, reports,
  emergency, documents, notifications, lotse) + Navigation aus src/lib/nav.ts
- Modul-Routen mit requireModule-Layout und Platzhalterseite
- Message-Katalog je Namespace (messages/<locale>/<namespace>.json), fs-Loader
- check-module-guards: Modul-Key aus src/server/actions/<moduleKey>/
- Provisionierung, Admin-Konsole, Einstellungen, Files-Route, Mail entkoppelt
- Seed minimal (demo/demo2, Nutzer je Rolle); Fundament-Tests auf Role/
  NotificationPreference-Fixtures umgestellt

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 11:35:44 +02:00
co-authored by Claude Opus 5
parent c8e6f30a27
commit 8491c7f173
443 changed files with 1325 additions and 87773 deletions
+88 -97
View File
@@ -1,145 +1,136 @@
/**
* Vollständigkeitscheck der serverseitigen Modul-Durchsetzung (§3.4, Phase-1-Härtung).
* Vollständigkeitscheck der serverseitigen Modul-Durchsetzung.
*
* Jede mutierende Server-Action eines gegateten Moduls MUSS über einen
* `moduleGuard("<key>")`-Guard laufen (siehe src/server/action-guard.ts), damit ein
* `moduleGuard("<key>")`-Guard laufen (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"),
* ── Konvention (Craftvia) ────────────────────────────────────────────────────
*
* 1) Modul-Actions liegen in einem Unterordner je Modul:
* src/server/actions/<moduleKey>/*.ts (auch tiefer verschachtelt)
* Der Modul-Key wird aus dem ERSTEN Ordnernamen abgeleitet und muss in MODULE_KEYS
* (src/lib/modules.ts) stehen, z. B. src/server/actions/work_orders/assign.ts →
* Modul „work_orders". Keine Eintragung in diesem Skript nötig.
* Jede solche Datei muss
* - `moduleGuard("<moduleKey>")` verwenden und
* - jede `export async function` über `await guard(` laufen lassen.
* Reine Hilfsdateien ohne exportierte async functions (z. B. schemas.ts) sind erlaubt;
* Dateien, die mit `_` beginnen, werden als intern übersprungen.
*
* 2) Top-Level-Dateien src/server/actions/*.ts sind Fundament (Auth, Plattform,
* Einstellungen …) und stehen in der expliziten Map ACTION_MODULE — entweder mit
* Modul-Key (dann gelten die Regeln aus 1) oder "EXEMPT" (eigene Auth-Prüfung
* erforderlich: require…-Guard oder auth()).
*
* Schlägt fehl (Exit 1 → prebuild/Gate rot), sobald
* - eine Top-Level-Datei nicht zugeordnet ist ("vergessener Endpoint"),
* - ein Modulordner keinen gültigen Modul-Key trägt,
* - 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 { readdirSync, readFileSync, statSync } from "node:fs";
import { join, dirname, relative, sep } 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). */
/** Top-Level-Action-Datei → Modul-Key oder "EXEMPT" (Fundament mit eigener Auth). */
const ACTION_MODULE: Record<string, string> = {
"assets.ts": "assets",
// M2 Strukturanalyse: primäre Informations-Assets (Dedup/Autocomplete) → Asset-Modul.
"structure.ts": "assets",
"processes.ts": "bia",
"risks.ts": "risk",
"risk-catalog.ts": "risk",
"measures.ts": "measures",
"tasks.ts": "tasks",
"incidents.ts": "incidents",
// IM-D: mandantenseitige Pflege der E-Mail-Intake-Konfiguration (moduleGuard("incidents") + tenant:manage).
"incident-intake.ts": "incidents",
"suppliers.ts": "suppliers",
"services.ts": "suppliers",
"software.ts": "suppliers",
"projects.ts": "assets",
"onboarding.ts": "onboarding",
"onboarding-facts.ts": "onboarding",
"onboarding-steps.ts": "onboarding",
"onboarding-team.ts": "onboarding",
"soa.ts": "onboarding",
// AP3: ISO-Anwendbarkeitserklärung (eigenes Modul „soa").
"soa-entries.ts": "soa",
// AP4: Managementklauseln (Kennzahlen/Managementbewertung/CAPA) im Modul „review".
"review.ts": "review",
"gap.ts": "onboarding",
// Audit-Vorbereitung — Modul `audit`.
"audits.ts": "audit",
"audit-evidence.ts": "audit",
"control-descriptions.ts": "audit",
"policies.ts": "policies",
"policy-package.ts": "policies",
// AP5: Dokumentenlenkung (Prüfzyklus, Neuversion/Historie, Lesebestätigung).
"policy-control.ts": "policies",
"policy-upload.ts": "policies",
"hints.ts": "policies",
"register.ts": "policies",
// Plattform-Betrieb (eigene Auth) und Kunden-Einstellungen (tenant:manage) sind
// keine per TenantModule gegateten Fachmodule — eigene Autorisierung, kein moduleGuard.
// Plattform-Betrieb (Auth über die Plattform-Session)
"admin.ts": "EXEMPT",
// SEC1: Mail-Betriebsfunktionen der Plattform-Administration (Auth über die
// Plattform-Session), kein per TenantModule gegatetes Fachmodul.
"mail.ts": "EXEMPT",
// Backup-Portal: Enqueue-Actions für Portal-Restore/Export/DSGVO-Zustellung.
// Betreiber-/Plattform-Fähigkeit (Auth über requirePlatformFullAdmin + MFA-Step-up),
// kein per TenantModule gegatetes Fachmodul.
"backup-admin.ts": "EXEMPT",
"backup-settings.ts": "EXEMPT",
// IM-D: Betreiber-Provisionierung/Verifizierung der Intake-Konfiguration + Inbound-Review.
// Plattform-Fähigkeit (requirePlatformFullAdmin), kein per TenantModule gegatetes Fachmodul.
"incident-intake-admin.ts": "EXEMPT",
// SEC2: Passwort-Self-Service. Die Reset-Abläufe laufen bewusst OHNE Session
// (der Nutzer ist ausgesperrt); abgesichert über Rate-Limit, Enumeration-
// Neutralität und single-use-Tokens. Die angemeldeten Abläufe nutzen
// requireSession bzw. requirePlatformSession.
"auth-recovery.ts": "EXEMPT",
"platform.ts": "EXEMPT",
"platform-users.ts": "EXEMPT",
"platform-admins.ts": "EXEMPT",
// SEC2: Passwort-Self-Service. Reset-Abläufe laufen bewusst OHNE Session; abgesichert
// über Rate-Limit, Enumeration-Neutralität und single-use-Tokens.
"auth-recovery.ts": "EXEMPT",
// Mandanten-Fundament (requireSession/requirePermission)
"tenant-users.ts": "EXEMPT",
"tenant-settings.ts": "EXEMPT",
"account.ts": "EXEMPT",
"tenant-switch.ts": "EXEMPT",
"webauthn.ts": "EXEMPT",
"platform-admins.ts": "EXEMPT",
"policy-templates.ts": "EXEMPT",
"tenant-settings.ts": "EXEMPT",
};
const errors: string[] = [];
const files = readdirSync(ACTIONS_DIR).filter((f) => f.endsWith(".ts"));
let checked = 0;
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;
function checkGatedFile(label: string, src: string, moduleKey: string) {
if (!(MODULE_KEYS as readonly string[]).includes(moduleKey)) {
errors.push(`${label}: unbekannter Modul-Key "${moduleKey}" (nicht in src/lib/modules.ts).`);
}
const src = readFileSync(join(ACTIONS_DIR, file), "utf8");
if (mapped === "EXEMPT") {
// Auth-Nachweis: entweder ein require*-Guard ODER ein direkter auth()-Aufruf
// (z. B. tenant-switch.ts, das im No-Tenant-Zustand kein requireSession nutzen
// kann, aber die Identity + Mitgliedschaftszugehörigkeit selbst prüft).
if (!/require(Session|Platform\w*|Permission)|\bauth\(\)/.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 });
if (positions.length === 0) return; // Hilfsdatei ohne Actions
if (!src.includes(`moduleGuard("${moduleKey}")`)) {
errors.push(`${label}: erwartet moduleGuard("${moduleKey}") — Modul-Gating fehlt oder falscher Key.`);
}
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.`
);
errors.push(`${label}: Action "${positions[i].name}" läuft nicht über await guard(...) — Modul-/Rechte-Guard fehlt.`);
}
}
}
function walk(dir: string): string[] {
return readdirSync(dir).flatMap((name) => {
const full = join(dir, name);
return statSync(full).isDirectory() ? walk(full) : [full];
});
}
for (const entry of readdirSync(ACTIONS_DIR)) {
const full = join(ACTIONS_DIR, entry);
if (statSync(full).isDirectory()) {
// (1) Modulordner: Key = Ordnername.
const moduleKey = entry;
for (const file of walk(full).filter((f) => f.endsWith(".ts"))) {
const rel = relative(ACTIONS_DIR, file).split(sep).join("/");
if (rel.split("/").some((seg) => seg.startsWith("_"))) continue;
checked++;
checkGatedFile(rel, readFileSync(file, "utf8"), moduleKey);
}
continue;
}
if (!entry.endsWith(".ts")) continue;
// (2) Top-Level-Fundament-Datei.
checked++;
const mapped = ACTION_MODULE[entry];
if (!mapped) {
errors.push(
`Nicht zugeordnete Action-Datei: ${entry} — Modul-Actions gehören nach src/server/actions/<moduleKey>/; ` +
`Fundament-Dateien in scripts/check-module-guards.ts eintragen (Modul-Key oder "EXEMPT").`,
);
continue;
}
const src = readFileSync(full, "utf8");
if (mapped === "EXEMPT") {
// Auth-Nachweis: ein require*-Guard ODER ein direkter auth()-Aufruf.
if (!/require(Session|Platform\w*|Permission)|\bauth\(\)/.test(src)) {
errors.push(`${entry}: als EXEMPT markiert, aber keine erkennbare Auth-Prüfung.`);
}
continue;
}
checkGatedFile(entry, src, mapped);
}
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.`
`✓ Modul-Guard-Vollständigkeitscheck: ${checked} Action-Dateien geprüft — alle mutierenden Actions sind modul- und rechtegegated.`,
);