Files
florian.wildeandClaude Opus 5.5 4219701b29 feat(anmeldung): lokale Benutzerkonten mit Pflicht-TOTP (#035)
Ohne authentik gab es nur den Notfall-Admin – ein geteiltes Konto ohne
Rollen und ohne erkennbare Akteure im Audit-Log. Admins legen jetzt
lokale Konten an; sshkv erzeugt ein Einmal-Passwort, das bei der ersten
Anmeldung ersetzt wird, danach ist TOTP Pflicht.

Rollen, Sperren (beendet Sitzungen sofort) und Zurücksetzen unter
Einstellungen → Benutzer, nur für Admins mit frischer Bestätigung.
SSHKV_LOKALE_KONTEN=0 schaltet lokale Konten ab; der Notfall-Admin
bleibt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:36:25 +02:00

47 lines
2.0 KiB
TypeScript
Raw Permalink 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 { 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/anmelden/passwort/page.tsx": "nur mit Cookie aus richtigem Einmal-Passwort erreichbar (#035)",
"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);
});
});