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:
+15
-103
@@ -1,38 +1,7 @@
|
||||
import type { PrismaClient, Framework } from "@prisma/client";
|
||||
import type { PrismaClient } from "@prisma/client";
|
||||
import { hashPassword } from "@/server/password";
|
||||
import { PERMISSIONS, ROLE_DEFS } from "@/server/rbac";
|
||||
import { MODULES } from "@/lib/modules";
|
||||
import { reconcilePackage, stampPackageVersion } from "../../prisma/import-policies";
|
||||
import { resolvePackageForTenant } from "../../prisma/template-store";
|
||||
import { importManaged } from "../../prisma/import-managed";
|
||||
|
||||
/**
|
||||
* Mappt die Mandanten-Stammdaten (TenantSettings) auf die ISMS-Template-Variablen
|
||||
* des Richtlinienmoduls (§4.1) — eine Pflegestelle, keine Doppeleingabe.
|
||||
*/
|
||||
export async function syncPolicyVariablesFromSettings(
|
||||
prisma: PrismaClient,
|
||||
tenantId: string,
|
||||
s: {
|
||||
orgName: string; orgShort?: string | null; ismsScope?: string | null; ismsScopeDescription?: string | null;
|
||||
roleManagement?: string | null; roleIsb?: string | null; roleItLead?: string | null; roleDpo?: string | null;
|
||||
}
|
||||
) {
|
||||
const map: Record<string, string | null | undefined> = {
|
||||
ORG_NAME: s.orgName,
|
||||
ORG_SHORT: s.orgShort,
|
||||
ISMS_SCOPE: s.ismsScope,
|
||||
ISMS_SCOPE_DESCRIPTION: s.ismsScopeDescription,
|
||||
ROLE_MANAGEMENT: s.roleManagement,
|
||||
ROLE_ISB: s.roleIsb,
|
||||
ROLE_IT_LEAD: s.roleItLead,
|
||||
ROLE_DPO: s.roleDpo,
|
||||
};
|
||||
for (const [key, value] of Object.entries(map)) {
|
||||
if (value == null || value === "") continue;
|
||||
await prisma.policyVariable.updateMany({ where: { tenantId, key }, data: { value } });
|
||||
}
|
||||
}
|
||||
|
||||
export interface ProvisionOpts {
|
||||
name: string;
|
||||
@@ -40,25 +9,18 @@ export interface ProvisionOpts {
|
||||
short?: string;
|
||||
sector?: string;
|
||||
admin: { email: string; name: string; password: string };
|
||||
tisaxLevel?: "AL2" | "AL3";
|
||||
seedPoliciesDir?: string;
|
||||
actorId?: string | null;
|
||||
/**
|
||||
* AP2: Rahmenwerke, die der Mandant führt (Reihenfolge = Primär zuerst). Default
|
||||
* `["TISAX"]` (rückwärtskompatibel). Steuert die TenantFramework-Zeilen, welche
|
||||
* Mappings importiert und welche Sichtbarkeits-Flags (FLAG_FW_*) gesetzt werden.
|
||||
*/
|
||||
frameworks?: Framework[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Legt einen Mandanten idempotent an bzw. aktualisiert ihn: Standard-Rollen +
|
||||
* Permissions, erster Admin-User, Mandanten-Einstellungen, alle Module aktiv,
|
||||
* optional Richtlinienpaket-Seed inkl. TISAX-Default (§3.7 Auto-Provisioning).
|
||||
* Legt einen Mandanten idempotent an bzw. aktualisiert ihn: globaler Permission-Katalog,
|
||||
* Standard-Rollen (ROLE_DEFS) inkl. Rechte, erster Mandantenadministrator (Identity +
|
||||
* Mitgliedschaft), Mandanten-Einstellungen, alle Module aktiv, Audit-Eintrag.
|
||||
*
|
||||
* Andockpunkt Fachmodule: mandantenspezifische Stammdaten-Defaults (z. B. Auftragsarten,
|
||||
* Checklisten-Vorlagen) werden künftig hier nach Schritt 6 angelegt.
|
||||
*/
|
||||
export async function provisionTenant(prisma: PrismaClient, opts: ProvisionOpts) {
|
||||
const tisaxLevel = opts.tisaxLevel ?? "AL2";
|
||||
|
||||
// 1. Globaler Permission-Katalog
|
||||
for (const key of PERMISSIONS) {
|
||||
await prisma.permission.upsert({ where: { key }, update: {}, create: { key } });
|
||||
@@ -88,11 +50,8 @@ export async function provisionTenant(prisma: PrismaClient, opts: ProvisionOpts)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Erster Admin-User (Mandanten-Admin + ISB)
|
||||
// Option C: globale Identity (Anmeldung) + Mitgliedschaft (User) im Mandanten.
|
||||
// Expand/Contract: passwordHash bleibt vorerst auch auf der Membership (Legacy),
|
||||
// bis WS1 den Login gegen Identity umstellt. Idempotent: bestehende Identity/
|
||||
// Passwort wird NICHT überschrieben (analog Bootstrap-Semantik).
|
||||
// 4. Erster Mandantenadministrator: globale Identity (Anmeldung) + Mitgliedschaft.
|
||||
// Idempotent: eine bestehende Identity/ihr Passwort wird NICHT überschrieben.
|
||||
const passwordHash = await hashPassword(opts.admin.password);
|
||||
const identity = await prisma.identity.upsert({
|
||||
where: { email: opts.admin.email },
|
||||
@@ -104,16 +63,16 @@ export async function provisionTenant(prisma: PrismaClient, opts: ProvisionOpts)
|
||||
update: { name: opts.admin.name, identityId: identity.id },
|
||||
create: { tenantId: tenant.id, identityId: identity.id, email: opts.admin.email, name: opts.admin.name },
|
||||
});
|
||||
const adminRoles = await prisma.role.findMany({ where: { tenantId: tenant.id, key: { in: ["tenant-admin", "isb"] } } });
|
||||
for (const r of adminRoles) {
|
||||
const adminRole = await prisma.role.findUnique({ where: { tenantId_key: { tenantId: tenant.id, key: "tenant-admin" } } });
|
||||
if (adminRole) {
|
||||
await prisma.userRole.upsert({
|
||||
where: { userId_roleId: { userId: admin.id, roleId: r.id } },
|
||||
where: { userId_roleId: { userId: admin.id, roleId: adminRole.id } },
|
||||
update: {},
|
||||
create: { userId: admin.id, roleId: r.id },
|
||||
create: { userId: admin.id, roleId: adminRole.id },
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Mandanten-Einstellungen (Quelle der ISMS-Variablen)
|
||||
// 5. Mandanten-Einstellungen (Unternehmensdaten)
|
||||
await prisma.tenantSettings.upsert({
|
||||
where: { tenantId: tenant.id },
|
||||
update: {},
|
||||
@@ -122,11 +81,6 @@ export async function provisionTenant(prisma: PrismaClient, opts: ProvisionOpts)
|
||||
orgName: opts.name,
|
||||
orgShort: opts.short ?? null,
|
||||
sector: opts.sector ?? null,
|
||||
roleManagement: "Geschäftsführung",
|
||||
roleIsb: "Informationssicherheitsbeauftragte(r) (ISB)",
|
||||
roleItLead: "IT-Leitung",
|
||||
roleDpo: "Datenschutzbeauftragte(r) (DSB)",
|
||||
tisaxLevel,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -139,50 +93,8 @@ export async function provisionTenant(prisma: PrismaClient, opts: ProvisionOpts)
|
||||
});
|
||||
}
|
||||
|
||||
// 6b. Framework-Zugehörigkeit (AP2): der Mandant führt die in `frameworks` genannten
|
||||
// Rahmenwerke (Default TISAX). Erstes = Primär. Idempotent je (tenant, framework).
|
||||
const frameworks: Framework[] = opts.frameworks?.length ? [...new Set(opts.frameworks)] : ["TISAX"];
|
||||
const runsTisax = frameworks.includes("TISAX");
|
||||
for (const [i, framework] of frameworks.entries()) {
|
||||
await prisma.tenantFramework.upsert({
|
||||
where: { tenantId_framework: { tenantId: tenant.id, framework } },
|
||||
update: { isPrimary: i === 0 },
|
||||
create: { tenantId: tenant.id, framework, isPrimary: i === 0 },
|
||||
});
|
||||
}
|
||||
|
||||
// 7. Richtlinienpaket JE FRAMEWORK importieren (falls Seed-Verzeichnis übergeben)
|
||||
if (opts.seedPoliciesDir) {
|
||||
// Neue Mandanten erhalten die neueste veröffentlichte DB-Vorlage (Datei-Fallback).
|
||||
// Anforderungen sind framework-scoped, die geteilten Inhalte werden nur beim ersten
|
||||
// Framework abgeglichen (reconcileShared, Falle 1.1).
|
||||
for (const [i, framework] of frameworks.entries()) {
|
||||
const { pkg } = await resolvePackageForTenant(prisma, tenant.id, opts.seedPoliciesDir, framework);
|
||||
await reconcilePackage(prisma, tenant.id, pkg, { framework, reconcileShared: i === 0 });
|
||||
await stampPackageVersion(prisma, tenant.id, pkg.version, framework);
|
||||
}
|
||||
await importManaged(prisma, tenant.id);
|
||||
|
||||
// AL-/Schutzbedarf-Flags sind TISAX-Konzepte → nur bei TISAX-Mandanten setzen
|
||||
// (reiner ISO-Mandant: keine AL-Flags, Übergabe D2/§1.3).
|
||||
if (runsTisax) {
|
||||
await prisma.policyVariable.updateMany({ where: { tenantId: tenant.id, key: "FLAG_HIGH_PROTECTION" }, data: { value: "true" } });
|
||||
await prisma.policyVariable.updateMany({ where: { tenantId: tenant.id, key: "FLAG_VERY_HIGH_PROTECTION" }, data: { value: tisaxLevel === "AL3" ? "true" : "false" } });
|
||||
}
|
||||
|
||||
// WICHTIG (Reihenfolge, Übergabe AP2): die Framework-Sichtbarkeits-Flags NACH dem
|
||||
// Import setzen. reconcilePackage legt sie beim ersten Import mit dem Schema-Default
|
||||
// an (TISAX=true, ISO=false) und überschreibt bestehende Werte NIE — würden wir vorher
|
||||
// setzen, bliebe der Default stehen und ein ISO-Mandant sähe die VDA-ISA-Sicht.
|
||||
await prisma.policyVariable.updateMany({ where: { tenantId: tenant.id, key: "FLAG_FW_TISAX" }, data: { value: runsTisax ? "true" : "false" } });
|
||||
await prisma.policyVariable.updateMany({ where: { tenantId: tenant.id, key: "FLAG_FW_ISO27001" }, data: { value: frameworks.includes("ISO_27001") ? "true" : "false" } });
|
||||
|
||||
const settings = await prisma.tenantSettings.findUnique({ where: { tenantId: tenant.id } });
|
||||
if (settings) await syncPolicyVariablesFromSettings(prisma, tenant.id, settings);
|
||||
}
|
||||
|
||||
await prisma.auditLog.create({
|
||||
data: { tenantId: tenant.id, scope: "platform", actorId: opts.actorId ?? null, action: "provision", entity: "tenant", entityId: tenant.id, after: { name: opts.name, tisaxLevel, frameworks } },
|
||||
data: { tenantId: tenant.id, scope: "platform", actorId: opts.actorId ?? null, action: "provision", entity: "tenant", entityId: tenant.id, after: { name: opts.name } },
|
||||
});
|
||||
|
||||
return tenant;
|
||||
|
||||
Reference in New Issue
Block a user