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
+13 -88
View File
@@ -2,9 +2,9 @@ import { PrismaClient } from "@prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
/**
* Central database access for the ISMS tool.
* Central database access for Craftvia.
*
* Multi-tenant rule (see docs/SPEC.md §2): no query on tenant-scoped models
* Multi-tenant rule (see docs/craftvia/SPEC-CRAFTVIA.md §5): no query on tenant-scoped models
* without a tenant context. Application code MUST use `dbForTenant(tenantId)`
* for all tenant-scoped models — the raw client is only for global data
* (permissions, control catalogs) and platform administration.
@@ -18,7 +18,7 @@ import { PrismaPg } from "@prisma/adapter-pg";
* ohne FORCE nicht, die alleinige Isolation ist der Tenant-Guard unten.
*
* Ist `RLS_ENFORCED=true`, verbindet sich die App zusätzlich als eingeschränkte
* Rolle `isms_app` über `RLS_DATABASE_URL` (NOBYPASSRLS). Für diese Rolle sind
* Rolle `craftvia_app` über `RLS_DATABASE_URL` (NOBYPASSRLS). Für diese Rolle sind
* die Policies unter `FORCE ROW LEVEL SECURITY` scharf. `dbForTenant` setzt dann
* pro Operation den Mandantenkontext (`app.tenant_id`) TRANSAKTIONSLOKAL und
* führt die Operation auf DERSELBEN Transaktions-Connection aus — nur so greift
@@ -51,7 +51,7 @@ if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
export const RLS_ENFORCED = process.env.RLS_ENFORCED === "true";
/**
* Basis-Client der eingeschränkten App-Rolle `isms_app` (nur bei aktivem Flag).
* Basis-Client der eingeschränkten App-Rolle `craftvia_app` (nur bei aktivem Flag).
* Uneextendiert — wird in `dbForTenant` gezielt für die transaktionslokale
* Kontextsetzung genutzt, damit die Guard-Extension nicht rekursiv greift.
*
@@ -64,8 +64,8 @@ if (RLS_ENFORCED) {
if (!rlsUrl) {
throw new Error(
"RLS_ENFORCED=true, aber RLS_DATABASE_URL fehlt. Setze eine Verbindung als " +
"Rolle isms_app (LOGIN, NOBYPASSRLS), z. B. " +
"postgresql://isms_app:<pw>@<host>:5432/isms?schema=public — sonst würde die " +
"Rolle craftvia_app (LOGIN, NOBYPASSRLS), z. B. " +
"postgresql://craftvia_app:<pw>@<host>:5432/craftvia?schema=public — sonst würde die " +
"App als Owner ohne scharfe RLS laufen (fail secure).",
);
}
@@ -92,87 +92,12 @@ const TENANT_MODELS = new Set<string>([
// davor, dass ein Token versehentlich ohne Mandantenfilter über dbForTenant
// gelesen wird.
"AuthToken",
"Task",
"TaskComment",
"TaskParticipant",
"Evidence",
"Audit",
"AuditEvidenceItem",
"ControlDescription",
"ManagedRegister",
"RegisterRow",
"OnboardingProgress",
"ProjectFunctionAssignment",
"WizardFact",
"WizardScope",
"PolicyPackageState",
"TenantSettings",
"TenantModule",
"Asset",
"AssetRelation",
"Process",
"ProcessAsset",
"ProcessDependency",
"BiaEntry",
"Risk",
"RiskAsset",
"Measure",
"RiskMeasure",
"SupplierProfile",
"ITServiceProfile",
"SoftwareProfile",
"ProjectProfile",
"SupplierAssessment",
"Contract",
"Nda",
"SupplierEvidence",
"ServiceControlResponsibility",
"Subcontractor",
"ManagementDecision",
"MaturityAssessment",
"ControlAssessment",
"ControlImplementation",
// Incident-Management (IM-A)
"Incident",
"IncidentComment",
"IncidentAsset",
"IncidentProcess",
"IncidentRisk",
"IncidentControl",
"IncidentMeasure",
"IncidentEvidence",
"IncidentAttachment",
// IM-D — mandantengebundene Intake-Konfiguration (RLS). Die Review-Queue
// (IncidentInboundReview) ist bewusst plattformweit → NICHT hier.
"IncidentIntakeConfig",
// WS4b: WebAuthnCredential ist jetzt identitäts-global (kein tenant_id) → NICHT hier.
"PolicyDocument",
"PolicyRequirement",
"PolicyVariable",
// AP1: Framework-Zugehörigkeit je Mandant (RLS-Policy in der Migration).
"TenantFramework",
// AP3: Anwendbarkeitserklärung (SoA) je Mandant/Framework (RLS-Policy in der Migration).
"SoaEntry",
// AP4: Managementklauseln (Kennzahlen 9.1, Managementbewertung 9.3, CAPA 10.2).
"Kpi",
"KpiValue",
"ManagementReview",
"ManagementReviewDecision",
"Nonconformity",
"CorrectiveAction",
// AP5: Dokumentenlenkung (Lesebestätigung + Versionshistorie).
"PolicyAcknowledgement",
"PolicyDocumentVersion",
"PolicyBaselineParam",
"PolicyEvidence",
"CryptoEntry",
"ClassificationClass",
"HandlingAspect",
"HandlingRule",
"RiskMatrixClass",
"RiskEwLevel",
"RiskDamageDimension",
"HandbookTopic",
// WebAuthnCredential/Identity sind identitäts-global (kein tenant_id) → NICHT hier.
// Craftvia-Fachmodelle hier ergänzen — UND in src/server/backup/topology.ts
// (TENANT_MODELS) sowie per `SELECT enable_tenant_rls('<table>')` in der Migration
// (docs/craftvia/MIGRATIONS.md).
]);
/**
@@ -196,7 +121,7 @@ const TENANT_MODELS = new Set<string>([
* Isolations-Exception wirft der Aufrufer UNABHÄNGIG davon weiter.
*
* Achtung Kontext: Die Funktion läuft mitten in der laufenden DB-Operation
* (im RLS-Pfad ggf. innerhalb der F-04-Transaktion des `isms_app`-Basisclients).
* (im RLS-Pfad ggf. innerhalb der F-04-Transaktion des `craftvia_app`-Basisclients).
* Der Audit-Insert läuft über den separaten Owner-`prisma` auf EIGENER Connection
* und ist damit von der Tenant-Transaktion entkoppelt: Er verklemmt sie nicht und
* bleibt bestehen, auch wenn die Tenant-Operation durch den anschließenden Throw
@@ -243,7 +168,7 @@ type DelegateSource = Record<string, GuardDelegate>;
/**
* F-02-Tenant-Guard, herausgelöst, damit er über zwei Ausführungspfade
* (Owner ohne RLS / isms_app mit RLS-Transaktion) identisch wiederverwendet wird.
* (Owner ohne RLS / craftvia_app mit RLS-Transaktion) identisch wiederverwendet wird.
*
* `source` liefert die uneextendierten Modell-Delegates für die Direktaufrufe
* (findUnique-Hybrid, Ownership-Vorprüfung) — bei RLS-off der Owner-`prisma`,
@@ -371,7 +296,7 @@ async function applyTenantGuard(
* reads are filtered by tenantId, creates get the tenantId injected.
*
* RLS-off (Default): Owner-`prisma` + Guard-Extension, `query(args)` als Ausführung.
* RLS-on (`RLS_ENFORCED=true`): `isms_app`-Basisclient; jede Operation eines
* RLS-on (`RLS_ENFORCED=true`): `craftvia_app`-Basisclient; jede Operation eines
* Tenant-Modells läuft in einer eigenen Transaktion des Basisclients, in der
* `app.tenant_id` transaktionslokal gesetzt wird (verworfen bei Commit/Rollback
* → kein Leak über den Pool). Die Guard-Logik ist in beiden Pfaden identisch.