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:
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useActionState } from "react";
|
||||
import { confirmMfaEnrollment, type EnrollState } from "@/server/actions/platform";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
const initial: EnrollState = { status: "idle" };
|
||||
|
||||
export function PlatformEnrollForm() {
|
||||
const [state, action, pending] = useActionState(confirmMfaEnrollment, initial);
|
||||
|
||||
if (state.status === "done") {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg bg-[rgba(46,204,113,0.14)] px-3 py-2 text-sm text-[var(--ok)]">
|
||||
MFA ist aktiv. Bewahren Sie die folgenden Recovery-Codes sicher auf — sie werden
|
||||
<strong> nur jetzt</strong> angezeigt und ermöglichen den Zugang, falls der Authenticator verloren geht.
|
||||
</div>
|
||||
<ul className="grid grid-cols-2 gap-2 font-mono text-sm">
|
||||
{state.recoveryCodes.map((c) => (
|
||||
<li key={c} className="rounded border bg-card px-3 py-1.5 text-center tracking-wider">{c}</li>
|
||||
))}
|
||||
</ul>
|
||||
<Button nativeButton={false} render={<Link href="/admin" />} className="w-full">
|
||||
Codes gesichert — zur Admin-Konsole
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={action} className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="token">6-stelliger Code aus der Authenticator-App</Label>
|
||||
<Input
|
||||
id="token"
|
||||
name="token"
|
||||
inputMode="numeric"
|
||||
autoComplete="one-time-code"
|
||||
placeholder="123456"
|
||||
required
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
{state.status === "error" && (
|
||||
<p role="alert" className="rounded-lg bg-[rgba(255,107,107,0.16)] px-3 py-2 text-sm text-[var(--risk)]">
|
||||
{state.message}
|
||||
</p>
|
||||
)}
|
||||
<Button type="submit" disabled={pending} className="w-full">
|
||||
{pending ? "Prüfe…" : "MFA aktivieren"}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user