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
+44
View File
@@ -0,0 +1,44 @@
import type { Metadata } from "next";
import { redirect } from "next/navigation";
import { AuthShell } from "@/components/auth/AuthShell";
import { LoginForm } from "@/components/auth/LoginForm";
import { getSessionUser } from "@/lib/auth/session";
import { getInstanceState } from "@/lib/auth/settings";
import { APP_NAME, WEBAUTHN_AVAILABLE } from "@/lib/config";
export const dynamic = "force-dynamic";
export async function generateMetadata(): Promise<Metadata> {
return { title: "Anmelden" };
}
type PageProps = { searchParams: Promise<{ weiter?: string }> };
export default async function LoginPage({ searchParams }: PageProps) {
const [user, state, params] = await Promise.all([
getSessionUser(),
getInstanceState(),
searchParams,
]);
if (state.needsSetup) redirect("/setup");
if (user) redirect("/");
// Nur relative Pfade zulassen – sonst waere das ein offener Redirect.
const raw = params.weiter ?? "/";
const next = raw.startsWith("/") && !raw.startsWith("//") ? raw : "/";
return (
<AuthShell
appName={APP_NAME}
title="Willkommen zurueck"
subtitle="Melde dich an, um deine Projekte zu sehen."
>
<LoginForm
passkeysAvailable={WEBAUTHN_AVAILABLE}
registrationOpen={state.registrationOpen}
next={next}
/>
</AuthShell>
);
}