Admin-Konsole & Mandantenverwaltung (Phase 1)
Plattform-Admin-Konsole (Superadmin) + Kunden-Einstellungsbereich: - Datenmodell: TenantSettings (Quelle der ISMS-Template-Variablen), TenantModule (Feature-Toggles je Mandant), Tenant += short/sector, TenantStatus += ARCHIVED, AuditLog += scope (tenant|platform); RLS für die neuen Tenant-Tabellen. - Wiederverwendbare Provisionierung (server/provision.ts): Rollen+Permissions, erster Admin-User, Einstellungen, alle Module, optional Richtlinienpaket-Seed + TISAX-Default; idempotent + Audit (§3.7). syncPolicyVariablesFromSettings mappt Stammdaten → ISMS-Variablen (ORG_NAME, ISMS_SCOPE, ROLE_* …). - Admin-Konsole /admin (Guard isPlatformAdmin): Mandantenliste, „Neuer Kunde" (anlegen + provisionieren), Detailseite mit Modul-Toggles, Lebenszyklus (aktiv/gesperrt/archiviert), Nutzerliste. Aktionen im Plattform-Audit-Log. - Kunden-Einstellungen /settings (Guard tenant:manage): Unternehmensdaten, verantwortliche Rollen, Branding/Regionales, TISAX-Level — Stammdaten speisen die ISMS-Variablen (eine Pflegestelle). Modul-Übersicht. - Modul-Gating der Navigation (deaktivierte Module ausgeblendet); Login bereits für gesperrte/archivierte Mandanten blockiert (auth.authorize). - Seed: Demo-Mandant mit Einstellungen, 12 aktiven Modulen; admin@demo.example = Superadmin. Verifiziert: tsc/lint/build grün; Seed rollt Einstellungen/Module/Superadmin aus. (Browser-Verifikation diese Sitzung nicht möglich — Preview-Tools getrennt.) Offen (Phase 2): separater Superadmin-Store + eigener Login + MFA-Pflicht; Impersonation (zeitlich begrenzt, protokolliert); Plan/Limits; Logo-Upload/ Objektspeicher; SMTP/Benachrichtigungen; per-Route-Modul-Enforcement (serverseitig); Datenexport/Löschung/Retention (DSGVO); SSO. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
import { hash } from "@node-rs/argon2";
|
||||
import type { PrismaClient } from "@prisma/client";
|
||||
import { PERMISSIONS, ROLE_DEFS } from "@/server/rbac";
|
||||
import { MODULES } from "@/lib/modules";
|
||||
import { importPolicies } from "../../prisma/import-policies";
|
||||
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;
|
||||
slug: string;
|
||||
short?: string;
|
||||
sector?: string;
|
||||
admin: { email: string; name: string; password: string; isPlatformAdmin?: boolean };
|
||||
tisaxLevel?: "AL2" | "AL3";
|
||||
seedPoliciesDir?: string;
|
||||
actorId?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
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 } });
|
||||
}
|
||||
|
||||
// 2. Mandant
|
||||
const tenant = await prisma.tenant.upsert({
|
||||
where: { slug: opts.slug },
|
||||
update: { name: opts.name, short: opts.short ?? null, sector: opts.sector ?? null },
|
||||
create: { name: opts.name, slug: opts.slug, short: opts.short ?? null, sector: opts.sector ?? null },
|
||||
});
|
||||
|
||||
// 3. Rollen + Permissions
|
||||
for (const [key, def] of Object.entries(ROLE_DEFS)) {
|
||||
const role = await prisma.role.upsert({
|
||||
where: { tenantId_key: { tenantId: tenant.id, key } },
|
||||
update: { name: def.name },
|
||||
create: { tenantId: tenant.id, key, name: def.name },
|
||||
});
|
||||
const perms = await prisma.permission.findMany({ where: { key: { in: [...def.permissions] } } });
|
||||
for (const p of perms) {
|
||||
await prisma.rolePermission.upsert({
|
||||
where: { roleId_permissionId: { roleId: role.id, permissionId: p.id } },
|
||||
update: {},
|
||||
create: { roleId: role.id, permissionId: p.id },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Erster Admin-User (Mandanten-Admin + ISB)
|
||||
const passwordHash = await hash(opts.admin.password);
|
||||
const admin = await prisma.user.upsert({
|
||||
where: { tenantId_email: { tenantId: tenant.id, email: opts.admin.email } },
|
||||
update: { name: opts.admin.name, isPlatformAdmin: opts.admin.isPlatformAdmin ?? false },
|
||||
create: { tenantId: tenant.id, email: opts.admin.email, name: opts.admin.name, passwordHash, isPlatformAdmin: opts.admin.isPlatformAdmin ?? false },
|
||||
});
|
||||
const adminRoles = await prisma.role.findMany({ where: { tenantId: tenant.id, key: { in: ["tenant-admin", "isb"] } } });
|
||||
for (const r of adminRoles) {
|
||||
await prisma.userRole.upsert({
|
||||
where: { userId_roleId: { userId: admin.id, roleId: r.id } },
|
||||
update: {},
|
||||
create: { userId: admin.id, roleId: r.id },
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Mandanten-Einstellungen (Quelle der ISMS-Variablen)
|
||||
await prisma.tenantSettings.upsert({
|
||||
where: { tenantId: tenant.id },
|
||||
update: {},
|
||||
create: {
|
||||
tenantId: tenant.id,
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
// 6. Alle Module aktivieren
|
||||
for (const m of MODULES) {
|
||||
await prisma.tenantModule.upsert({
|
||||
where: { tenantId_moduleKey: { tenantId: tenant.id, moduleKey: m.key } },
|
||||
update: {},
|
||||
create: { tenantId: tenant.id, moduleKey: m.key, enabled: true },
|
||||
});
|
||||
}
|
||||
|
||||
// 7. Richtlinienpaket + TISAX-Default (falls Seed-Verzeichnis übergeben)
|
||||
if (opts.seedPoliciesDir) {
|
||||
await importPolicies(prisma, tenant.id, opts.seedPoliciesDir);
|
||||
await importManaged(prisma, tenant.id);
|
||||
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" } });
|
||||
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 } },
|
||||
});
|
||||
|
||||
return tenant;
|
||||
}
|
||||
Reference in New Issue
Block a user