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:
2026-07-20 20:55:12 +02:00
co-authored by Claude Opus 4.8
parent 8348764c99
commit 8e4040d1da
22 changed files with 1009 additions and 54 deletions
+25 -3
View File
@@ -166,6 +166,27 @@ model RolePermission {
@@map("role_permissions")
}
// Plattform-Administratoren (Betreiber). Getrennter Store OHNE tenant_id, eigener
// Login (/platform/login), TOTP-MFA-Pflicht + Recovery-Codes. Kein Zugriff auf
// Mandanten-Fachdaten (nur Betrieb: Mandanten, Module, Lebenszyklus). §Phase-1-Härtung.
model PlatformAdmin {
id String @id @default(cuid())
email String @unique
passwordHash String @map("password_hash")
name String
status String @default("ACTIVE") // ACTIVE | LOCKED | DISABLED
mfaSecret String? @map("mfa_secret")
mfaEnrolledAt DateTime? @map("mfa_enrolled_at")
recoveryCodes Json @default("[]") @map("recovery_codes") // SHA-256-Hashes der Recovery-Codes
failedLogins Int @default(0) @map("failed_logins")
lockedUntil DateTime? @map("locked_until")
lastLoginAt DateTime? @map("last_login_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("platform_admins")
}
// ── Assets & BIA (ein Modul, SPEC §4.1) ─────────────────────────────────────
enum AssetType {
@@ -692,17 +713,18 @@ model MaturityAssessment {
model AuditLog {
id String @id @default(cuid())
tenantId String @map("tenant_id")
// Nullable: Plattform-Ereignisse (scope=platform, z. B. Superadmin-Login) haben keinen Mandantenbezug.
tenantId String? @map("tenant_id")
scope String @default("tenant") // tenant | platform
actorId String? @map("actor_id")
action String // create | update | delete | login | export | …
action String // create | update | delete | login | denied | export | …
entity String // z. B. asset, risk, user
entityId String? @map("entity_id")
before Json?
after Json?
createdAt DateTime @default(now()) @map("created_at")
tenant Tenant @relation(fields: [tenantId], references: [id])
tenant Tenant? @relation(fields: [tenantId], references: [id])
@@index([tenantId, entity, entityId])
@@index([tenantId, createdAt])