Separater Superadmin-Store + eigener Login + MFA-Pflicht (Phase-1-Härtung Paket 2)
Plattform-Administratoren sind nicht länger isPlatformAdmin-Nutzer innerhalb eines Mandanten, sondern ein getrennter Store mit eigener Auth-Domäne — Voraussetzung für den sicheren Betrieb beim ersten echten Kunden. Store & Migration - Neues Modell PlatformAdmin (kein tenant_id): Argon2id-Hash, TOTP-Secret, Recovery-Codes (nur SHA-256-Hashes), Fehlversuchszähler + Sperre, lastLogin. - AuditLog.tenant_id nullable → mandantenlose Plattform-Ereignisse (scope=platform). - Datenmigration: bestehende isPlatformAdmin-Nutzer in den neuen Store übernommen (gleicher Hash → Login sofort möglich), Flag mandantenweit auf false gesetzt. Getrennter Login + MFA - Zweite NextAuth-Instanz (server/platform-auth.ts) mit eigenem Cookie und eigenem basePath /api/platform-auth; Session trägt bewusst KEINEN tenantId. - TOTP-MFA (otplib): Enrollment beim ersten Login (/platform/enroll-mfa, QR + Klartext-Secret), danach bei jedem Login erzwungen; 10 einmalige Recovery-Codes. - Härtung: Konto-Sperre nach 5 Fehlversuchen (15 min), Audit aller Anmeldungen, Fehlversuche und Sperren (scope=platform). Autorisierung / Trennung - Admin-Konsole nach (platform)/admin verschoben; (platform)/layout.tsx erzwingt Plattform-Session + aktivierte MFA. Mandanten-Session hat KEINEN Zugriff auf /admin. - Mandanten-Shell zeigt keinen /admin-Link mehr; admin-Actions prüfen die Plattform-Session statt des abgelösten Flags. - provision/seed setzen isPlatformAdmin nicht mehr; Seed legt den Demo-Plattform- Admin (admin@demo.example) im getrennten Store an. Browser-verifiziert: Plattform-Login → erzwungenes MFA-Enrollment → Recovery-Codes → /admin; Login ohne Code scheitert (?error=1); Mandanten-Session auf /admin wird auf /platform/login umgeleitet. tsc + lint + build + Guard-Check grün. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -18,7 +18,6 @@ import {
|
||||
LineChart,
|
||||
Search,
|
||||
Settings,
|
||||
ShieldCheck,
|
||||
} from "lucide-react";
|
||||
import { auth, signOut } from "@/server/auth";
|
||||
import { dbForTenant } from "@/server/db";
|
||||
@@ -61,7 +60,6 @@ export default async function AppLayout({
|
||||
};
|
||||
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 ?? "?")
|
||||
@@ -111,18 +109,11 @@ export default async function AppLayout({
|
||||
)
|
||||
)}
|
||||
|
||||
{(canManageTenant || isPlatformAdmin) && (
|
||||
{canManageTenant && (
|
||||
<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>
|
||||
)}
|
||||
<NavLink href="/settings">
|
||||
<Settings className="size-[18px] opacity-85" /> {t("settings")}
|
||||
</NavLink>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
Reference in New Issue
Block a user