feat(auth): Einrichtung, Anmeldeseite und Registrierung

- /setup fuehrt beim ersten Aufruf durch das Anlegen des Administratorkontos
  und die Wahl der Betriebsart. Der Endpunkt funktioniert nur, solange es
  keinen Benutzer gibt; danach kann sich niemand mehr nachtraeglich ein
  Adminkonto erschleichen. Vorhandene Projekte ohne Besitzer werden dabei
  uebernommen und im Formular vorher angekuendigt.
- /login mit Passwort oder Passkey. Der Passkey-Knopf entfaellt, wenn APP_URL
  keinen sicheren Kontext ergibt - mit Hinweis, woran es liegt. Das Ziel nach
  der Anmeldung wird auf relative Pfade begrenzt, sonst waere der
  weiter-Parameter ein offener Redirect.
- /register nur im Mehrbenutzerbetrieb und nur, wenn ein Administrator die
  Selbstregistrierung freigegeben hat.
- Kopfzeile mit Benutzermenue: eigenes Konto, Admin-Einstellungen, Abmelden.
  Ohne Anmeldung bleibt sie auf das Logo reduziert.
- Layout liest Titel und Namen ueber generateMetadata, damit APP_NAME zur
  Laufzeit greift statt beim Build eingefroren zu werden.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-25 21:30:33 +02:00
co-authored by Claude Opus 5
parent 13bc5aa0b8
commit 4f85e00016
19 changed files with 1415 additions and 40 deletions
@@ -0,0 +1,28 @@
import { NextResponse } from "next/server";
import { WebAuthnUnavailableError, buildAuthenticationOptions } from "@/lib/auth/webauthn";
import { LIMITS, rateLimit } from "@/lib/security/rateLimit";
import { clientIp, jsonError, tooManyRequests } from "@/lib/security/request";
export const dynamic = "force-dynamic";
/**
* Optionen fuer navigator.credentials.get(). Ohne allowCredentials – der
* Browser bietet alle passenden Passkeys an, ein Benutzername ist nicht
* noetig. Der Endpunkt verraet dadurch auch nichts ueber vorhandene Konten.
*/
export async function POST(request: Request) {
const limited = rateLimit(
`webauthn:auth:${clientIp(request)}`,
LIMITS.webauthn.limit,
LIMITS.webauthn.windowMs,
);
if (!limited.ok) return tooManyRequests(limited.retryAfter);
try {
const options = await buildAuthenticationOptions();
return NextResponse.json({ options });
} catch (error) {
if (error instanceof WebAuthnUnavailableError) return jsonError(error.message, 501);
return jsonError(error instanceof Error ? error.message : String(error), 500);
}
}