# Anmeldung und Benutzerverwaltung Die Leistungsbilanz verlangt eine Anmeldung. Es gibt kein Self-Signup: jeder Zugang wird von einem Administrator über **Config → Benutzer** angelegt. ## Rollen Es gibt genau zwei Rollen (`src/shared/constants/user-roles.ts`): - **admin** – wie `user`, zusätzlich Zugriff auf den Config-Bereich (Benutzerverwaltung, eigenes Konto) - **user** – volle Nutzung der Leistungsbilanz (Projekte, Stromkreislisten, Grobbilanz, …), kein Zugriff auf Config Es gibt (bewusst, siehe unten) keine konfigurierbare Rechte-Matrix und keine Verknüpfung mit einer Mitarbeiter-/Personen-Stammdatentabelle. ## Ablauf beim Anlegen eines Zugangs 1. Ein Administrator öffnet Config → Benutzer und legt einen neuen Zugang mit Anmeldename, Name, Rolle und einem **Startpasswort** an, das er selbst eintippt. 2. Der Zugang startet mit `mustChangePassword = true`. 3. Beim ersten Anmelden zeigt die Oberfläche statt der App ein Formular, das ein eigenes Passwort verlangt, bevor es weitergeht. 4. "Passwort zurücksetzen" (ebenfalls in Config → Benutzer) setzt ein neues Startpasswort und `mustChangePassword` wieder auf `true` – z. B. wenn jemand sein Passwort vergessen hat. ## Technischer Aufbau - **Datenmodell:** Tabelle `users` (`src/db/schema/users.ts`) – `username`, `passwordHash` (scrypt, `src/server/auth/password.ts`), `name`, `role`, `active`, `mustChangePassword`. - **Sessions:** In-Memory-Store (`src/server/auth/sessions.ts`), Cookie `lb_session`, sliding expiry (`AUTH_SESSION_HOURS`, Standard 12h) mit absoluter Obergrenze (`AUTH_SESSION_MAX_DAYS`, Standard 7). Ein Neustart der API beendet alle Sessions – für einen Single-Container-Dienst ein akzeptabler Kompromiss gegenüber einer persistierten Session-Tabelle. - **Middleware:** `src/server/auth/auth.middleware.ts` schützt `/api/*` (außer `/api/auth/login`, `/api/auth/status`, `/api/auth/logout`) und erzwingt den Passwortwechsel, solange `mustChangePassword` gesetzt ist. `/api/users/*` verlangt zusätzlich die Rolle `admin`. - **Frontend:** `AuthProvider` lädt `/api/auth/me`; `LoginGate` (`src/app/layout.tsx`) zeigt Login- bzw. Passwortformular, solange nicht angemeldet bzw. `mustChangePassword` gesetzt ist, sonst die App. ## Schutzmechanismen | Angriff | Schutz | |---|---| | Passwort-Raten (online) | `LoginThrottle`: 5 Fehlversuche je Anmeldename in 15 min → HTTP 429 bis das Fenster abläuft. Schlüssel ist der Name, nicht die IP (alle Anfragen kommen über den Next-Rewrite mit derselben Quell-IP). | | Passwort-Raten (offline, DB-Diebstahl) | scrypt (N=16384, r=8, p=1), eigenes Salt je Hash | | Nutzernamen ausspähen (Timing) | Unbekannte Namen werden gegen einen festen Dummy-Hash geprüft – immer genau ein scrypt-Lauf; einheitliche Fehlermeldung | | CSRF | SameSite=Lax **plus** `requireSameOriginForWrites`: schreibende Anfragen nur mit `Sec-Fetch-Site: same-origin`. Nötig, weil andere Dienste auf demselben Host (andere Ports) als „same-site“ gelten | | Session-Fixation | Neue Session-ID bei jedem Login und nach jedem Passwortwechsel | | Gestohlenes Cookie | httpOnly; gleitende Laufzeit (`AUTH_SESSION_HOURS`, 12 h) **und** harte Obergrenze (`AUTH_SESSION_MAX_DAYS`, 7 Tage) | | Kompromittiertes Konto | Passwort-Reset und Deaktivieren beenden sofort alle Sessions des Kontos; Passwortwechsel beendet alle anderen | | Aussperren | Der letzte aktive Admin kann weder herabgestuft noch deaktiviert werden; eigener Zugang ist in der Liste gesperrt | | Rechteausweitung / Mass Assignment | Rolle kommt aus der DB, nie aus der Anfrage; zod-Schemas mit `.strict()` | | DoS über riesige Passwörter | Längengrenzen (Name 100, Passwort 200 Zeichen) vor scrypt | | Look-alike-Namen | Anmeldename nur `a-z 0-9 . _ @ -`, klein geschrieben | | Clickjacking | `X-Frame-Options: SAMEORIGIN`, `frame-ancestors 'self'` (next.config.mjs) | | Speicherwachstum | Max. 20 Sessions je Nutzer, periodisches Aufräumen abgelaufener Sessions, Throttle-Tabelle begrenzt | **Nicht durch Code lösbar:** Ohne TLS gehen Passwort und Session-Cookie im Klartext durchs Netz. Für Zugriff außerhalb des LANs HTTPS über einen Reverse-Proxy vorschalten und `COOKIE_SECURE=true` setzen. ## Bootstrap-Admin `AUTH_BOOTSTRAP_USERNAME` / `AUTH_BOOTSTRAP_PASSWORT` (Env-Variablen, siehe `compose.yaml`) legen beim ersten Start einen Admin an – **nur falls die `users`-Tabelle noch komplett leer ist**. Danach werden diese Variablen ignoriert; jeder weitere Zugang entsteht über Config → Benutzer. Die Werte gehören in eine lokale, nicht versionierte `.env` auf dem Deploy-Host, nicht in `compose.yaml` selbst. ## Bewusst nicht umgesetzt (Stand jetzt) - Konfigurierbare Rechte-Matrix statt fixer `admin`/`user`-Rollen - Dauerhafte Konten-Sperre nach Fehlversuchen (es gibt nur die zeitlich begrenzte Drosselung, siehe oben) - E-Mail-/Einmal-Link-Passwortvergabe, OIDC/Entra-Anmeldung - Verknüpfung von Zugängen mit einer Mitarbeiter-Stammdatentabelle