feat(auth): Anmeldung mit Passwort und OIDC, Schutz aller Seiten (#013)

Signierte Sitzungs-Cookies mit jose, OIDC ueber openid-client (authentik),
Schutz in proxy.ts, Anmeldeseite, Audit-Log-Ansicht, Sicherheits-Kopfzeilen und
Rate-Limit. Kein Auth.js, weil Version 5 Beta ist. 147 Tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-17 22:40:03 +02:00
co-authored by Claude Opus 5
parent 6cc39591b7
commit ee487775ec
26 changed files with 945 additions and 27 deletions
+62
View File
@@ -0,0 +1,62 @@
import Link from "next/link";
import { AnmeldeFormular } from "@/components/auth/AnmeldeFormular";
import { Logo } from "@/components/Logo";
import { oidcEinstellungen } from "@/server/auth/oidc";
export const dynamic = "force-dynamic";
export const metadata = { title: "Anmelden" };
type SuchParameter = Promise<Record<string, string | string[] | undefined>>;
export default async function AnmeldeSeite({ searchParams }: { searchParams: SuchParameter }) {
const roh = await searchParams;
const einzeln = (wert: string | string[] | undefined) => (Array.isArray(wert) ? wert[0] : wert);
const ziel = einzeln(roh.ziel) ?? "/";
const fehler = einzeln(roh.fehler);
const oidc = oidcEinstellungen();
return (
<main className="d-flex align-items-center justify-content-center min-vh-100 p-3">
<div style={{ width: "min(24rem, 100%)" }}>
<div className="text-center mb-4">
<Logo groesse={64} titel="ProxLedger" />
<h1 className="h4 mt-3 mb-1" style={{ fontWeight: 300 }}>
ProxLedger
</h1>
<p className="text-body-secondary small mb-0">Proxmox Subscription Dashboard</p>
</div>
<div className="card shadow-sm">
<div className="card-body">
{fehler ? (
<div className="alert alert-danger py-2" role="alert">
{fehler === "oidc"
? "Die Anmeldung über den Anbieter hat nicht geklappt."
: "Anmeldung nicht möglich."}
</div>
) : null}
{oidc ? (
<>
<Link
href={`/api/auth/oidc/start?ziel=${encodeURIComponent(ziel)}`}
className="btn btn-primary w-100 pl-touch-target mb-3"
>
<i className="bi bi-box-arrow-in-right me-1" aria-hidden="true" />
Mit {oidc.bezeichnung} anmelden
</Link>
<div className="text-center text-body-secondary small mb-3">oder mit Passwort</div>
</>
) : null}
<AnmeldeFormular ziel={ziel} />
</div>
</div>
<p className="text-center text-body-secondary small mt-3 mb-0">
Der lokale Zugang bleibt auch dann nutzbar, wenn der Anmeldedienst ausfällt.
</p>
</div>
</main>
);
}