Basis: Certvia dev@a48c5fb als Fundament für Craftvia
Unveränderter Stand von certvia/dev (a48c5fb) plus Craftvia-Spezifikation und Brandbook unter docs/craftvia/. ISMS-Module werden im Folgecommit entfernt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import QRCode from "qrcode";
|
||||
import { platformAuth } from "@/server/platform-auth";
|
||||
import { prisma } from "@/server/db";
|
||||
import { newTotpSecret, totpUri } from "@/server/mfa";
|
||||
import { encryptSecret, decryptSecret } from "@/server/secret-crypto";
|
||||
import { PlatformEnrollForm } from "@/components/platform-enroll-form";
|
||||
import { CertviaLogo } from "@/components/brand/certvia-logo";
|
||||
|
||||
/**
|
||||
* MFA-Einrichtung für Plattform-Administratoren (Pflicht beim ersten Login).
|
||||
* Erzeugt/persistiert ein Einrichtungs-Secret, zeigt QR + Klartext-Secret und
|
||||
* bestätigt per TOTP-Code (Server-Action confirmMfaEnrollment).
|
||||
*/
|
||||
export default async function EnrollMfaPage() {
|
||||
const session = await platformAuth();
|
||||
if (!session?.user?.id) redirect("/platform/login");
|
||||
|
||||
const admin = await prisma.platformAdmin.findUnique({ where: { id: session.user.id } });
|
||||
if (!admin) redirect("/platform/login");
|
||||
if (admin.mfaEnrolledAt) redirect("/admin");
|
||||
|
||||
let secret = admin.mfaSecret ? decryptSecret(admin.mfaSecret) : null;
|
||||
if (!secret) {
|
||||
secret = newTotpSecret();
|
||||
await prisma.platformAdmin.update({ where: { id: admin.id }, data: { mfaSecret: encryptSecret(secret) } });
|
||||
}
|
||||
const qr = await QRCode.toDataURL(totpUri(admin.email, secret), { margin: 1, width: 208 });
|
||||
|
||||
return (
|
||||
<main className="flex flex-1 items-center justify-center p-6">
|
||||
<div className="shadow-card w-full max-w-md rounded-2xl border bg-card p-8">
|
||||
<CertviaLogo variant="lockup" theme="dark" height={34} />
|
||||
<p className="mt-5 font-heading text-lg font-semibold">Zwei-Faktor-Authentifizierung einrichten</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Für Plattform-Administratoren verpflichtend. Scannen Sie den QR-Code mit einer
|
||||
Authenticator-App (z. B. Google Authenticator, Aegis, 1Password) und bestätigen Sie mit dem angezeigten Code.
|
||||
</p>
|
||||
|
||||
<div className="mt-5 flex flex-col items-center gap-3">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={qr} alt="QR-Code für die Authenticator-App" width={208} height={208} className="rounded-lg border bg-white p-2" />
|
||||
<div className="text-center">
|
||||
<p className="text-xs text-muted-foreground">Manuell eingeben:</p>
|
||||
<code className="select-all break-all font-mono text-xs">{secret}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<PlatformEnrollForm />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { AuthError } from "next-auth";
|
||||
import { platformAuth, platformSignIn } from "@/server/platform-auth";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { CertviaLogo } from "@/components/brand/certvia-logo";
|
||||
import { PoweredByGefim } from "@/components/brand/powered-by-gefim";
|
||||
|
||||
/**
|
||||
* Getrennter Login für Plattform-Administratoren (Phase-1-Härtung Paket 2).
|
||||
* Eigene Auth-Domäne, kein Mandantenkontext. TOTP-Code ist ab eingerichteter MFA
|
||||
* erforderlich (Feld optional, damit der erste Login zur Einrichtung durchläuft).
|
||||
*/
|
||||
export default async function PlatformLoginPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ error?: string }>;
|
||||
}) {
|
||||
const { error } = await searchParams;
|
||||
|
||||
const session = await platformAuth();
|
||||
if (session?.user?.id) redirect("/admin");
|
||||
|
||||
async function login(formData: FormData) {
|
||||
"use server";
|
||||
try {
|
||||
await platformSignIn("credentials", {
|
||||
email: formData.get("email"),
|
||||
password: formData.get("password"),
|
||||
token: formData.get("token"),
|
||||
redirectTo: "/admin",
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof AuthError) redirect("/platform/login?error=1");
|
||||
throw err; // NEXT_REDIRECT eines erfolgreichen signIn muss durchlaufen
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="flex flex-1 items-center justify-center p-6">
|
||||
<div className="shadow-card w-full max-w-sm rounded-2xl border bg-card p-8">
|
||||
<CertviaLogo variant="lockup" theme="dark" height={38} />
|
||||
<p className="mt-4 font-heading text-lg font-semibold">Plattform-Administration</p>
|
||||
<h2 className="mt-1 text-sm font-normal text-muted-foreground">
|
||||
Betreiberzugang — getrennt vom Mandanten-Login
|
||||
</h2>
|
||||
|
||||
{error && (
|
||||
<p
|
||||
role="alert"
|
||||
className="mt-4 rounded-lg bg-[rgba(255,107,107,0.16)] px-3 py-2 text-sm text-[var(--risk)]"
|
||||
>
|
||||
Anmeldung fehlgeschlagen. Bitte E-Mail, Passwort und — falls MFA eingerichtet — den 6-stelligen Code prüfen.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<form action={login} className="mt-6 space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="email">E-Mail</Label>
|
||||
<Input id="email" name="email" type="email" required autoComplete="email" className="mt-1" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="password">Passwort</Label>
|
||||
<Input id="password" name="password" type="password" required autoComplete="current-password" className="mt-1" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="token">MFA-Code (falls eingerichtet)</Label>
|
||||
<Input
|
||||
id="token"
|
||||
name="token"
|
||||
inputMode="numeric"
|
||||
autoComplete="one-time-code"
|
||||
placeholder="6-stelliger Code oder Recovery-Code"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" className="w-full">Anmelden</Button>
|
||||
</form>
|
||||
|
||||
<p className="mt-4 text-center text-[12.5px]">
|
||||
<Link href="/forgot-password?domain=platform" className="text-muted-foreground hover:text-foreground">
|
||||
Passwort vergessen?
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<PoweredByGefim className="mt-7 border-t border-[var(--panel-brd)] pt-4" />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user