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,71 @@
|
||||
"use server";
|
||||
|
||||
import { join } from "node:path";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
import { requireSession } from "@/server/auth";
|
||||
import { prisma } from "@/server/db";
|
||||
import { provisionTenant } from "@/server/provision";
|
||||
|
||||
/** Nur Plattform-Admins (Superadmin) — mandantenübergreifend. */
|
||||
async function requirePlatformAdmin() {
|
||||
const session = await requireSession();
|
||||
if (!session.user.isPlatformAdmin) throw new Error("Nur für Plattform-Admins.");
|
||||
return session;
|
||||
}
|
||||
|
||||
const str = (v: FormDataEntryValue | null) => (v ? String(v).trim() : "");
|
||||
const slugify = (s: string) =>
|
||||
s.toLowerCase().normalize("NFKD").replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "kunde";
|
||||
|
||||
export async function createTenant(formData: FormData) {
|
||||
const session = await requirePlatformAdmin();
|
||||
const name = z.string().trim().min(2).parse(formData.get("name"));
|
||||
const slug = str(formData.get("slug")) || slugify(name);
|
||||
const adminEmail = z.string().trim().email().parse(formData.get("adminEmail"));
|
||||
const adminName = str(formData.get("adminName")) || "Administrator";
|
||||
const adminPassword = z.string().min(8).parse(formData.get("adminPassword"));
|
||||
const tisaxLevel = str(formData.get("tisaxLevel")) === "AL3" ? "AL3" : "AL2";
|
||||
const seedPolicies = formData.get("seedPolicies") === "on";
|
||||
|
||||
const existing = await prisma.tenant.findUnique({ where: { slug } });
|
||||
if (existing) throw new Error(`Kürzel „${slug}" ist bereits vergeben.`);
|
||||
|
||||
const tenant = await provisionTenant(prisma, {
|
||||
name,
|
||||
slug,
|
||||
short: str(formData.get("short")) || undefined,
|
||||
sector: str(formData.get("sector")) || undefined,
|
||||
admin: { email: adminEmail, name: adminName, password: adminPassword },
|
||||
tisaxLevel,
|
||||
seedPoliciesDir: seedPolicies ? join(process.cwd(), "seed", "isms-vorlagenpaket-v2") : undefined,
|
||||
actorId: session.user.id,
|
||||
});
|
||||
|
||||
revalidatePath("/admin");
|
||||
redirect(`/admin?created=${tenant.id}`);
|
||||
}
|
||||
|
||||
export async function setTenantStatus(tenantId: string, status: "ACTIVE" | "SUSPENDED" | "ARCHIVED") {
|
||||
const session = await requirePlatformAdmin();
|
||||
await prisma.tenant.update({ where: { id: tenantId }, data: { status } });
|
||||
await prisma.auditLog.create({
|
||||
data: { tenantId, scope: "platform", actorId: session.user.id, action: "update", entity: "tenant", entityId: tenantId, after: { status } },
|
||||
});
|
||||
revalidatePath("/admin");
|
||||
revalidatePath(`/admin/${tenantId}`);
|
||||
}
|
||||
|
||||
export async function toggleTenantModule(tenantId: string, moduleKey: string, enabled: boolean) {
|
||||
const session = await requirePlatformAdmin();
|
||||
await prisma.tenantModule.upsert({
|
||||
where: { tenantId_moduleKey: { tenantId, moduleKey } },
|
||||
update: { enabled },
|
||||
create: { tenantId, moduleKey, enabled },
|
||||
});
|
||||
await prisma.auditLog.create({
|
||||
data: { tenantId, scope: "platform", actorId: session.user.id, action: "update", entity: "tenant_module", entityId: moduleKey, after: { enabled } },
|
||||
});
|
||||
revalidatePath(`/admin/${tenantId}`);
|
||||
}
|
||||
Reference in New Issue
Block a user