Files
nebula/src/lib/auth/guard.ts
T
florian.wildeandClaude Opus 5 13bc5aa0b8 feat(auth): Anmeldung mit Passwort und Passkey
Kern der Benutzerverwaltung.

Passwoerter:
- scrypt aus der Node-Standardbibliothek (N=32768, r=8, p=1), speicherhart
  und ohne zusaetzliches natives Modul
- Format scrypt$N$r$p$salt$hash, damit sich die Parameter spaeter anheben
  lassen
- fakeVerifyDelay verbrennt bei unbekanntem Benutzernamen dieselbe
  Rechenzeit, sonst liesse sich an der Antwortzeit ablesen, welche Konten
  existieren

Passkeys ueber SimpleWebAuthn 13:
- Anmeldung ohne Benutzernamen ueber auffindbare Credentials
- Challenges liegen in der Datenbank und werden beim Einloesen geloescht,
  koennen also nicht erneut eingespielt werden
- rpID und Origin kommen aus APP_URL

Sitzungen:
- Zufallstoken im HttpOnly-Cookie, in der Datenbank nur dessen Hash
- Abmelden und Sperren wirken sofort, weil bei jeder Anfrage nachgeschlagen
  wird - anders als bei einem selbsttragenden Token

Middleware: CSRF-Pruefung ueber Sec-Fetch-Site bzw. Origin,
Sicherheitskopfzeilen inklusive CSP mit Per-Request-Nonce und
strict-dynamic, dazu eine grobe Zugangsschranke. Letztere ist ausdruecklich
keine Sicherheitsgrenze - im Edge-Runtime gibt es keine Datenbank.
Verbindlich entscheiden requireUserPage und requireUserApi, serverseitig,
in jeder Seite und jedem Endpunkt.

config.ts liest APP_NAME, APP_TAGLINE und APP_URL zur Laufzeit statt ueber
NEXT_PUBLIC_-Variablen. Die waeren beim Build fest ins Bundle geschrieben;
so laesst sich dasselbe Docker-Image ohne Neubau umbenennen und unter einer
anderen Adresse betreiben.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 21:30:12 +02:00

63 lines
1.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { redirect } from "next/navigation";
import { NextResponse } from "next/server";
import { getSessionUser, type SessionUser } from "@/lib/auth/session";
import { getInstanceState } from "@/lib/auth/settings";
/**
* Zugriffsschutz.
*
* Die Middleware prueft nur, ob ueberhaupt ein Sitzungscookie mitkommt – im
* Edge-Runtime steht keine Datenbank zur Verfuegung. Die eigentliche
* Entscheidung faellt hier, serverseitig, und zwar in jeder Seite und jedem
* API-Endpunkt. Das ist die Sicherheitsgrenze.
*/
export type { SessionUser };
/** Fuer Server Components: leitet auf /login bzw. /setup um. */
export async function requireUserPage(): Promise<SessionUser> {
const user = await getSessionUser();
if (user) return user;
const state = await getInstanceState();
redirect(state.needsSetup ? "/setup" : "/login");
}
/** Wie requireUserPage, verlangt zusaetzlich die Administratorrolle. */
export async function requireAdminPage(): Promise<SessionUser> {
const user = await requireUserPage();
if (user.role !== "ADMIN") redirect("/");
return user;
}
export type ApiAuth =
| { ok: true; user: SessionUser }
| { ok: false; response: NextResponse };
/** Fuer Route Handler: liefert entweder den Benutzer oder eine 401/403-Antwort. */
export async function requireUserApi(): Promise<ApiAuth> {
const user = await getSessionUser();
if (!user) {
return {
ok: false,
response: NextResponse.json({ error: "Nicht angemeldet." }, { status: 401 }),
};
}
return { ok: true, user };
}
export async function requireAdminApi(): Promise<ApiAuth> {
const auth = await requireUserApi();
if (!auth.ok) return auth;
if (auth.user.role !== "ADMIN") {
return {
ok: false,
response: NextResponse.json(
{ error: "Diese Aktion ist Administratoren vorbehalten." },
{ status: 403 },
),
};
}
return auth;
}