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:
2026-07-19 20:38:10 +02:00
co-authored by Claude Opus 4.8
parent eab16c861d
commit 0e3ed82b21
14 changed files with 864 additions and 3 deletions
+33 -1
View File
@@ -17,8 +17,13 @@ import {
Truck,
LineChart,
Search,
Settings,
ShieldCheck,
} from "lucide-react";
import { auth, signOut } from "@/server/auth";
import { dbForTenant } from "@/server/db";
import { hasPermission } from "@/server/rbac";
import { HREF_TO_MODULE } from "@/lib/modules";
import { Button } from "@/components/ui/button";
import { NavLink } from "@/components/nav-link";
@@ -47,6 +52,18 @@ export default async function AppLayout({
{ href: "/review", label: t("review"), icon: LineChart, enabled: false },
];
// Modul-Gating: deaktivierte Module werden ausgeblendet (§3.4)
const moduleRows = await dbForTenant(session.user.tenantId).tenantModule.findMany();
const disabledModules = new Set(moduleRows.filter((m) => !m.enabled).map((m) => m.moduleKey));
const moduleEnabled = (href: string) => {
const key = HREF_TO_MODULE[href];
return !key || !disabledModules.has(key);
};
const visibleNav = nav.filter((item) => !item.enabled || moduleEnabled(item.href));
const isPlatformAdmin = session.user.isPlatformAdmin;
const canManageTenant = hasPermission(session, "tenant:manage");
const initials = (session.user.name ?? "?")
.split(/\s+/)
.map((p) => p[0])
@@ -76,7 +93,7 @@ export default async function AppLayout({
</Link>
</div>
<nav className="flex-1 space-y-0.5 overflow-y-auto px-2.5 py-3">
{nav.map((item) =>
{visibleNav.map((item) =>
item.enabled ? (
<NavLink key={item.href} href={item.href}>
<item.icon className="size-[18px] opacity-85" />
@@ -93,6 +110,21 @@ export default async function AppLayout({
</span>
)
)}
{(canManageTenant || isPlatformAdmin) && (
<div className="mt-2 space-y-0.5 border-t border-sidebar-border pt-2">
{canManageTenant && (
<NavLink href="/settings">
<Settings className="size-[18px] opacity-85" /> {t("settings")}
</NavLink>
)}
{isPlatformAdmin && (
<NavLink href="/admin">
<ShieldCheck className="size-[18px] opacity-85" /> {t("admin")}
</NavLink>
)}
</div>
)}
</nav>
</aside>