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,122 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { requireSession } from "@/server/auth";
|
||||
import { dbForTenant } from "@/server/db";
|
||||
import { hasPermission } from "@/server/rbac";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { PageHead, Pill } from "@/components/mockup-ui";
|
||||
import { MODULES } from "@/lib/modules";
|
||||
import { updateTenantSettings } from "@/server/actions/tenant-settings";
|
||||
|
||||
const inputCls = "h-9 w-full rounded-md border border-input bg-transparent px-3 text-sm";
|
||||
|
||||
export default async function SettingsPage() {
|
||||
const session = await requireSession();
|
||||
if (!hasPermission(session, "tenant:manage")) redirect("/dashboard");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
|
||||
const [s, moduleRows] = await Promise.all([
|
||||
db.tenantSettings.findUnique({ where: { tenantId: session.user.tenantId } }),
|
||||
db.tenantModule.findMany(),
|
||||
]);
|
||||
const enabled = new Set(moduleRows.filter((m) => m.enabled).map((m) => m.moduleKey));
|
||||
const v = (x?: string | null) => x ?? "";
|
||||
|
||||
return (
|
||||
<main className="flex-1 p-6">
|
||||
<PageHead crumb="Organisation" title="Einstellungen" sub="Unternehmensdaten, Branding, Sicherheits-Policy — Stammdaten speisen die ISMS-Variablen" />
|
||||
|
||||
<form action={updateTenantSettings} className="mt-4 grid gap-5 lg:grid-cols-[1fr_320px]">
|
||||
<div className="space-y-5">
|
||||
{/* Unternehmensdaten */}
|
||||
<div className="shadow-card rounded-xl border bg-card p-5">
|
||||
<p className="mb-1 font-heading text-sm font-semibold">Unternehmensdaten</p>
|
||||
<p className="mb-3 text-[12px] text-muted-foreground">Diese Werte füttern automatisch die Template-Variablen des Richtlinienmoduls (eine Pflegestelle).</p>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="md:col-span-2">
|
||||
<Label htmlFor="orgName">Unternehmensname *</Label>
|
||||
<Input id="orgName" name="orgName" required defaultValue={v(s?.orgName)} className="mt-1" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="orgShort">Kurzname</Label>
|
||||
<Input id="orgShort" name="orgShort" defaultValue={v(s?.orgShort)} className="mt-1" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="sector">Sektor</Label>
|
||||
<Input id="sector" name="sector" defaultValue={v(s?.sector)} className="mt-1" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="address">Adresse</Label>
|
||||
<Input id="address" name="address" defaultValue={v(s?.address)} className="mt-1" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="duns">D-U-N-S</Label>
|
||||
<Input id="duns" name="duns" defaultValue={v(s?.duns)} className="mt-1" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="ismsScope">ISMS-Geltungsbereich (Kurz)</Label>
|
||||
<Input id="ismsScope" name="ismsScope" defaultValue={v(s?.ismsScope)} className="mt-1" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="ismsScopeDescription">Geltungsbereich (Beschreibung)</Label>
|
||||
<Textarea id="ismsScopeDescription" name="ismsScopeDescription" rows={2} defaultValue={v(s?.ismsScopeDescription)} className="mt-1" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Verantwortliche Rollen */}
|
||||
<div className="shadow-card rounded-xl border bg-card p-5">
|
||||
<p className="mb-3 font-heading text-sm font-semibold">Verantwortliche Rollen (→ ISMS-Variablen)</p>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div><Label htmlFor="roleManagement">Oberste Leitung (ROLE_MANAGEMENT)</Label><Input id="roleManagement" name="roleManagement" defaultValue={v(s?.roleManagement)} className="mt-1" /></div>
|
||||
<div><Label htmlFor="roleIsb">ISB / CISO (ROLE_ISB)</Label><Input id="roleIsb" name="roleIsb" defaultValue={v(s?.roleIsb)} className="mt-1" /></div>
|
||||
<div><Label htmlFor="roleItLead">IT-Leitung (ROLE_IT_LEAD)</Label><Input id="roleItLead" name="roleItLead" defaultValue={v(s?.roleItLead)} className="mt-1" /></div>
|
||||
<div><Label htmlFor="roleDpo">Datenschutz (ROLE_DPO)</Label><Input id="roleDpo" name="roleDpo" defaultValue={v(s?.roleDpo)} className="mt-1" /></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5">
|
||||
{/* Branding & Regionales */}
|
||||
<div className="shadow-card rounded-xl border bg-card p-5">
|
||||
<p className="mb-3 font-heading text-sm font-semibold">Branding & Regionales</p>
|
||||
<div className="space-y-3">
|
||||
<div><Label htmlFor="accent">Akzentfarbe (Hex)</Label><Input id="accent" name="accent" defaultValue={v(s?.accent)} placeholder="#7d6fd6" className="mt-1" /></div>
|
||||
<div>
|
||||
<Label htmlFor="locale">Sprache</Label>
|
||||
<select id="locale" name="locale" defaultValue={s?.locale ?? "de"} className={`${inputCls} mt-1`}><option value="de">Deutsch</option><option value="en">English</option></select>
|
||||
</div>
|
||||
<div><Label htmlFor="timezone">Zeitzone</Label><Input id="timezone" name="timezone" defaultValue={s?.timezone ?? "Europe/Berlin"} className="mt-1" /></div>
|
||||
<p className="text-[11px] text-muted-foreground">Logo-Upload (PNG/SVG) folgt in Phase 2 (Objektspeicher).</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Schutzbedarf / TISAX */}
|
||||
<div className="shadow-card rounded-xl border bg-card p-5">
|
||||
<p className="mb-3 font-heading text-sm font-semibold">Schutzbedarf / TISAX-Level</p>
|
||||
<select name="tisaxLevel" defaultValue={s?.tisaxLevel ?? "AL2"} className={inputCls}>
|
||||
<option value="AL2">AL2 — MUSS · SOLL · HOCH</option>
|
||||
<option value="AL3">AL3 — zusätzlich SEHR HOCH</option>
|
||||
</select>
|
||||
<p className="mt-2 text-[11px] text-muted-foreground">Steuert die Schutzbedarf-Flags des Richtlinienmoduls (global; Override je Richtlinie im Editor).</p>
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full justify-center">Speichern</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Module (Übersicht, Freischaltung durch Plattform-Admin) */}
|
||||
<div className="shadow-card mt-5 rounded-xl border bg-card p-5">
|
||||
<p className="mb-3 font-heading text-sm font-semibold">Freigeschaltete Module</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{MODULES.map((m) => (
|
||||
<Pill key={m.key} tone={enabled.has(m.key) ? "ok" : "mut"}>{m.name}</Pill>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-3 text-[11px] text-muted-foreground">Die Modul-Freischaltung erfolgt durch den Plattform-Betreiber.</p>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user