feat(auth): Anmeldeseiten, TOTP-Einrichtung und Bestaetigung (#004)

Anmeldung, Einrichtung des zweiten Faktors fuer den Notfall-Admin,
erneute Bestaetigung vor kritischen Aktionen und die Seite fuer
fehlende Rechte. Die Huelle zeigt jetzt den echten Benutzer, und jede
Seite prueft die Sitzung selbst; ein struktureller Test haelt das fest.

Die Ablaeufe sind im gebauten Stand gegen den Mock-OIDC geprueft
(pnpm test:e2e): Admin, Konto ohne Gruppe, Notfall-Admin mit TOTP.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 09:28:51 +02:00
co-authored by Claude Opus 5.5
parent 4d0044b380
commit d1ba822a4a
37 changed files with 763 additions and 38 deletions
+45
View File
@@ -0,0 +1,45 @@
import { readFileSync, readdirSync, statSync } from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
/**
* Struktureller Test (#004, Muster aus Laufzettel): jede Seite und jeder Route Handler
* prueft selbst, wer zugreift. Die Ausnahmen stehen hier namentlich mit Begruendung – ein
* neuer ungepruefter Handler soll jemanden zwingen, sie aufzuschreiben.
*/
const AUSNAHMEN: Record<string, string> = {
"src/app/anmelden/page.tsx": "Anmeldeseite selbst",
"src/app/anmelden/totp/page.tsx": "nur mit Einrichtungs-Cookie aus richtigem Passwort erreichbar",
"src/app/api/auth/abmelden/route.ts": "Abmelden braucht keine Pruefung",
"src/app/api/auth/oidc/start/route.ts": "Beginn der Anmeldung",
"src/app/api/auth/oidc/rueckruf/route.ts": "Ende der Anmeldung; prueft state, nonce und PKCE",
"src/app/api/gesundheit/route.ts": "Healthcheck, verraet nur die Version",
};
const PRUEFUNGEN = /sitzungErforderlich\(|rechtErforderlich\(|totpEinrichtungLesen\(/;
function dateien(verzeichnis: string): string[] {
return readdirSync(verzeichnis).flatMap((name) => {
const pfad = path.join(verzeichnis, name);
if (statSync(pfad).isDirectory()) return dateien(pfad);
return /^(page\.tsx|route\.ts)$/.test(name) ? [pfad] : [];
});
}
describe("Zugangspruefung", () => {
const alle = dateien("src/app").map((p) => p.split(path.sep).join("/"));
it("findet Seiten und Handler", () => {
expect(alle.length).toBeGreaterThan(10);
});
it.each(alle)("%s prueft selbst oder steht in den Ausnahmen", (datei) => {
if (AUSNAHMEN[datei]) return;
const inhalt = readFileSync(datei, "utf8");
expect(PRUEFUNGEN.test(inhalt), `${datei} prueft keine Sitzung`).toBe(true);
});
it("listet keine Ausnahme fuer eine Datei, die es nicht mehr gibt", () => {
for (const datei of Object.keys(AUSNAHMEN)) expect(alle).toContain(datei);
});
});