feat(auth): Anmeldung, Rollen und Benutzerverwaltung (#21)
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s

OIDC mit Discovery, PKCE, state und nonce; lokaler Login mit argon2 als
Notfallzugang; Rechte in einer Tabelle, Prüfung ausschließlich serverseitig,
Rolle und Aktiv-Status pro Anfrage aus der Datenbank.

Ein unabhängiger Review fand eine offene Weiterleitung: die Präfix-Prüfung des
Rücksprungziels ließ /<TAB>/fremd.example durch, woraus der Browser
//fremd.example macht. Das Ziel wird jetzt aufgelöst und die Herkunft
verglichen. Dazu acht weitere Befunde, alle behoben.

274 Tests.

Closes #7

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit was merged in pull request #21.
This commit is contained in:
2026-09-25 01:35:02 +00:00
co-authored by Claude Opus 5
parent 7a0f875f55
commit 40e95dbe5f
43 changed files with 3426 additions and 56 deletions
+28 -3
View File
@@ -5,6 +5,8 @@
* die Huelle von Anfang an die echte Struktur, ohne auf Seiten zu verlinken, die
* es noch nicht gibt. Die Beschriftungen haengen am Betriebsmodus (#008).
*/
import type { Recht } from "@/server/auth/rollen";
export type Betriebsmodus = "INTERN" | "DIENSTLEISTUNG";
export type NavEintrag = {
@@ -15,6 +17,8 @@ export type NavEintrag = {
/** Nur in diesem Modus zeigen. Fehlt der Wert, gilt der Eintrag in beiden. */
readonly nurModus?: Betriebsmodus;
readonly symbol: string;
/** Wer dieses Recht nicht hat, sieht den Eintrag gar nicht erst. */
readonly recht?: Recht;
readonly verfuegbar: boolean;
/** Issue, das die Seite liefert – nur fuer den Hinweis, solange sie fehlt. */
readonly ausIssue?: number;
@@ -41,11 +45,32 @@ export const NAVIGATION: readonly NavEintrag[] = [
ausIssue: 8,
},
{ pfad: "/archiv", beschriftung: "Archiv", symbol: "Archive", verfuegbar: false, ausIssue: 15 },
{ pfad: "/admin", beschriftung: "Admin", symbol: "Settings", verfuegbar: false, ausIssue: 8 },
{
// Zeigt vorerst direkt auf die Benutzerverwaltung; mit #008 kommen die
// Einstellungen dazu und daraus wird ein eigener Bereich.
pfad: "/admin/benutzer",
beschriftung: "Admin",
symbol: "Settings",
recht: "verwaltung",
verfuegbar: true,
},
];
export function navigationFuer(modus: Betriebsmodus): readonly NavEintrag[] {
return NAVIGATION.filter((e) => e.nurModus === undefined || e.nurModus === modus).map((e) =>
/**
* Die sichtbaren Eintraege fuer diesen Modus und diese Rolle.
*
* Das Ausblenden ist Bequemlichkeit, keine Absicherung – die Rechtepruefung
* sitzt in der Service-Schicht. Aber ein Leser soll nicht auf Knoepfe klicken,
* die ihm hinterher eine Fehlermeldung zeigen.
*/
export function navigationFuer(
modus: Betriebsmodus,
darfEintrag: (recht: Recht) => boolean = () => true,
): readonly NavEintrag[] {
return NAVIGATION.filter(
(e) =>
(e.nurModus === undefined || e.nurModus === modus) && (e.recht === undefined || darfEintrag(e.recht)),
).map((e) =>
modus === "DIENSTLEISTUNG" && e.beschriftungDienstleistung
? { ...e, beschriftung: e.beschriftungDienstleistung }
: e,