140 lines
5.3 KiB
TypeScript
140 lines
5.3 KiB
TypeScript
import { useState } from "react";
|
|
import { useMutation } from "@tanstack/react-query";
|
|
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
|
import { ShieldCheck, KeyRound, AlertTriangle } from "lucide-react";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { api, unwrap, ApiError } from "@/lib/api";
|
|
|
|
export const Route = createFileRoute("/login")({
|
|
component: LoginPage,
|
|
});
|
|
|
|
function LoginPage() {
|
|
const navigate = useNavigate();
|
|
const [devSubject, setDevSubject] = useState("CN=lab.operator,OU=PKI,O=SMGW");
|
|
const [err, setErr] = useState<string | null>(null);
|
|
|
|
const login = useMutation({
|
|
mutationFn: () =>
|
|
unwrap(
|
|
api.POST("/auth/session", {
|
|
body: { dev_subject: devSubject },
|
|
}),
|
|
),
|
|
onError: (e: ApiError) => setErr(e.message),
|
|
onSuccess: () => navigate({ to: "/" }),
|
|
});
|
|
|
|
return (
|
|
<div className="min-h-screen grid lg:grid-cols-[1.05fr_0.95fr]">
|
|
<aside className="relative hidden lg:flex flex-col justify-between p-12 border-r border-line overflow-hidden bg-paper">
|
|
<div className="absolute inset-0 grid-bg opacity-60 pointer-events-none" />
|
|
<div className="relative z-10 flex items-center gap-3">
|
|
<div className="size-10 rounded-[12px] bg-ink text-paper grid place-items-center font-display font-semibold text-[17px] tracking-[-0.02em]">
|
|
sm
|
|
</div>
|
|
<div className="leading-tight">
|
|
<div className="font-display text-[16px] font-semibold tracking-[-0.015em] text-ink">
|
|
SMGW PKI · Console
|
|
</div>
|
|
<div className="text-[11px] uppercase tracking-[0.1em] text-ink-faint">
|
|
Test / Labor — BSI TR-03129-4
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="relative z-10 max-w-md space-y-6">
|
|
<p className="font-serif italic text-[34px] leading-[1.1] text-ink tracking-[-0.01em]">
|
|
Eine ruhige Konsole für eine{" "}
|
|
<span className="not-italic font-display font-semibold">
|
|
vorsichtige PKI
|
|
</span>
|
|
.
|
|
</p>
|
|
<p className="text-[13.5px] leading-relaxed text-ink-mute">
|
|
Steuerfläche für den smgw-pki-automator. Erneuerungen, Sub-CA-Status,
|
|
iconfig-Generierung und SoftHSM-Diagnose — alle Operationen sind
|
|
auditiert und an mTLS-Identitäten gebunden.
|
|
</p>
|
|
<div className="flex flex-wrap gap-2">
|
|
<Badge tone="outline">TR-03129-4</Badge>
|
|
<Badge tone="outline">TR-03109-1</Badge>
|
|
<Badge tone="outline">SM-PKI CP</Badge>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="relative z-10 text-[11.5px] text-ink-faint mono">
|
|
v0.1.0 · build skeleton · {new Date().getFullYear()}
|
|
</div>
|
|
</aside>
|
|
|
|
<section className="flex items-center justify-center p-8">
|
|
<div className="w-full max-w-[420px] surface p-7">
|
|
<div className="flex items-center gap-2 text-[11px] uppercase tracking-[0.1em] text-ink-faint">
|
|
<ShieldCheck className="size-3.5 text-accent" />
|
|
Sitzung
|
|
</div>
|
|
<h2 className="mt-3 font-display text-[26px] font-semibold tracking-[-0.02em] text-ink">
|
|
Anmeldung über mTLS
|
|
</h2>
|
|
<p className="mt-1.5 text-[13px] leading-relaxed text-ink-mute">
|
|
Ihr Client-Zertifikat wird vom Reverse Proxy terminiert und im Header{" "}
|
|
<code className="mono text-[12px]">X-Forwarded-Cert-Subject</code>{" "}
|
|
übergeben. Im Lab-Modus ist ein Dev-Subject als Fallback erlaubt.
|
|
</p>
|
|
|
|
<form
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
setErr(null);
|
|
login.mutate();
|
|
}}
|
|
className="mt-7 space-y-4"
|
|
>
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="dev_subject">Dev-Subject (nur Lab)</Label>
|
|
<Input
|
|
id="dev_subject"
|
|
value={devSubject}
|
|
onChange={(e) => setDevSubject(e.target.value)}
|
|
mono
|
|
placeholder="CN=…"
|
|
/>
|
|
<p className="text-[11.5px] text-ink-faint">
|
|
Wird ignoriert, sobald der Proxy ein gültiges Cert-Subject mitschickt.
|
|
</p>
|
|
</div>
|
|
|
|
{err && (
|
|
<div className="flex gap-2 items-start surface-inset p-3 text-[12.5px] text-danger">
|
|
<AlertTriangle className="size-4 mt-0.5 shrink-0" />
|
|
<span>{err}</span>
|
|
</div>
|
|
)}
|
|
|
|
<Button
|
|
type="submit"
|
|
variant="primary"
|
|
size="lg"
|
|
className="w-full"
|
|
disabled={login.isPending}
|
|
>
|
|
<KeyRound className="size-4" />
|
|
Sitzung erstellen
|
|
</Button>
|
|
</form>
|
|
|
|
<div className="mt-6 pt-5 border-t border-line text-[11.5px] text-ink-faint leading-relaxed">
|
|
Cookies sind <span className="text-ink">HttpOnly</span> und{" "}
|
|
<span className="text-ink">SameSite=Strict</span>. Sitzungsdauer 8 h.
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|