From 3670f02ccdec07dacac7b475ea328dd72dad670a Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Mon, 28 Sep 2026 22:42:11 +0200 Subject: [PATCH] Document authenticated deployment --- .env.example | 7 +++++++ .gitignore | 3 +++ AGENTS.md | 5 ++++- README.md | 21 ++++++++++++++++---- compose.dev.yaml | 2 ++ compose.yaml | 2 +- docs/authentication.md | 25 ++++++++++++++++++------ docs/current-architecture.md | 14 +++++++++++++ docs/deployment.md | 38 ++++++++++++++++++++++++++++-------- 9 files changed, 97 insertions(+), 20 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..46fa42a --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +# Nur beim ersten Start mit leerer Benutzertabelle erforderlich. +AUTH_BOOTSTRAP_USERNAME= +AUTH_BOOTSTRAP_PASSWORT= +AUTH_SESSION_HOURS=12 +AUTH_SESSION_MAX_DAYS=7 +# Hinter HTTPS true, bei direktem HTTP-Zugriff im LAN false. +COOKIE_SECURE=false diff --git a/.gitignore b/.gitignore index a6e1ff5..b237969 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ node_modules/ dist/ .next/ +.env +.env.* +!.env.example # Streuprodukt von tsc; drizzle-kit liest drizzle.config.ts. /drizzle.config.js data/*.db diff --git a/AGENTS.md b/AGENTS.md index 65b5fab..b01e6b5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,7 +42,10 @@ It must support circuits, device rows, project devices, drag-and-drop restructur `src/db/repositories/user.repository.ts`. Every `/api/*` route requires a signed-in user (`src/server/index.ts`); `/api/users/*` additionally requires `admin`. Frontend gate: `AuthProvider` + `LoginGate` - (`src/app/layout.tsx`). See `docs/authentication.md` for the full picture. + (`src/app/layout.tsx`). Every unsafe API request also requires the explicit + `X-Leistungsbilanz-Request: 1` application header, including over plain HTTP. + Account settings are available to every signed-in user; user administration + remains admin-only. See `docs/authentication.md` for the full picture. The supported runtime model is Circuit-First. The former Consumer UI, API, tables and upgrade tooling are removed; do not reintroduce them. diff --git a/README.md b/README.md index b01ae85..f64ae04 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,9 @@ Gerätezeilen und wiederverwendbare Projektgeräte fachlich getrennt behandelt. Das Projekt befindet sich in aktiver Entwicklung. Für SQLite stehen ein Docker-Server-Stack mit gebautem Anwendungscode und ein separater Entwicklungsstack bereit. Ein vollständig unterstütztes Produktionsdeployment -und Mehrbenutzerbetrieb fehlen weiterhin. Revit-CSV-Konfiguration, Vorschau +für mehrere gleichzeitig arbeitende Prozesse und ein vollständig unterstütztes +Produktionsdeployment fehlen weiterhin. Anmeldung und zwei feste Rollen sind +vorhanden. Revit-CSV-Konfiguration, Vorschau und bestätigter Erstimport sind implementiert; die Editoroberfläche zur Objektzuordnung, Folgeimporte und Rückexport stehen noch aus. Die weitere Revit-Arbeit ist zurückgestellt und erfolgt später über den CSV-Export/-Import @@ -60,6 +62,8 @@ Voraussetzungen: ```powershell git clone Set-Location leistungsbilanz-ts +$env:AUTH_BOOTSTRAP_USERNAME = "admin" +$env:AUTH_BOOTSTRAP_PASSWORT = "" docker compose up --build --detach ``` @@ -68,9 +72,15 @@ Danach: - Frontend: - API-Healthcheck: (über den Next.js-Rewrite) -Die Express-API ist nicht mehr auf dem Host veröffentlicht. Sie hat keine -Authentifizierung und ist nur innerhalb des Compose-Netzes erreichbar; der -Zugriff läuft über den Rewrite im Web-Dienst. +Beim ersten Start erzeugen diese beiden Variablen genau einen Administrator, +solange die Benutzertabelle noch leer ist. Danach werden sie ignoriert. Für +einen dauerhaften Serverbetrieb gehören die Werte in eine nicht versionierte +`.env` neben `compose.yaml`; Details stehen unter +[Anmeldung und Benutzerverwaltung](docs/authentication.md). + +Die Express-API ist nicht auf dem Host veröffentlicht. Sämtliche fachlichen +API-Routen verlangen eine gültige Anmeldung; der Zugriff läuft über den +Rewrite im Web-Dienst. Der API-Container führt ausstehende Migrationen und die Schemaprüfung beim Start automatisch aus. Die SQLite-Datei liegt auf dem Host unter @@ -116,6 +126,8 @@ Voraussetzungen: npm ci npm run db:migrate npm run db:verify:circuit-schema +$env:AUTH_BOOTSTRAP_USERNAME = "admin" +$env:AUTH_BOOTSTRAP_PASSWORT = "" npm run dev:api ``` @@ -162,6 +174,7 @@ vor der Baseline werden nicht unterstützt. - [Gebäudestruktur und Raumflächen](docs/building-structure.md) - [Grobe Leistungsbilanz](docs/estimate-balance.md) - [Leitungsberechnung](docs/cable-sizing-module.md) +- [Anmeldung und Benutzerverwaltung](docs/authentication.md) - [Entwicklungs- und Contributor-Workflow](docs/development-workflow.md) - [Bekannte Einschränkungen](docs/circuit-list-editor-known-limitations.md) - [Roadmap](docs/spec/07-implementation-phases-todo.md) diff --git a/compose.dev.yaml b/compose.dev.yaml index 2c362bc..f49e9e6 100644 --- a/compose.dev.yaml +++ b/compose.dev.yaml @@ -38,6 +38,8 @@ services: AUTH_BOOTSTRAP_USERNAME: "${AUTH_BOOTSTRAP_USERNAME:-}" AUTH_BOOTSTRAP_PASSWORT: "${AUTH_BOOTSTRAP_PASSWORT:-}" AUTH_SESSION_HOURS: "${AUTH_SESSION_HOURS:-12}" + AUTH_SESSION_MAX_DAYS: "${AUTH_SESSION_MAX_DAYS:-7}" + COOKIE_SECURE: "${COOKIE_SECURE:-false}" # Nur auf dem Loopback: die API darf nicht aus dem LAN erreichbar sein. ports: - "127.0.0.1:3000:3000" diff --git a/compose.yaml b/compose.yaml index 2a2eecd..1de9104 100644 --- a/compose.yaml +++ b/compose.yaml @@ -47,7 +47,7 @@ services: LOG_LEVEL: "${LOG_LEVEL:-info}" # AUTH_BOOTSTRAP_* legen nur den allerersten Admin an (solange die # users-Tabelle leer ist) und gehoeren in eine lokale, nicht versionierte - # .env - siehe docs/AUTH.md. + # .env - siehe docs/authentication.md. AUTH_BOOTSTRAP_USERNAME: "${AUTH_BOOTSTRAP_USERNAME:-}" AUTH_BOOTSTRAP_PASSWORT: "${AUTH_BOOTSTRAP_PASSWORT:-}" AUTH_SESSION_HOURS: "${AUTH_SESSION_HOURS:-12}" diff --git a/docs/authentication.md b/docs/authentication.md index 15729aa..32039cf 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -7,10 +7,9 @@ Zugang wird von einem Administrator über **Config → Benutzer** angelegt. Es gibt genau zwei Rollen (`src/shared/constants/user-roles.ts`): -- **admin** – wie `user`, zusätzlich Zugriff auf den Config-Bereich - (Benutzerverwaltung, eigenes Konto) +- **admin** – wie `user`, zusätzlich Zugriff auf **Config → Benutzer** - **user** – volle Nutzung der Leistungsbilanz (Projekte, Stromkreislisten, - Grobbilanz, …), kein Zugriff auf Config + Grobbilanz, …) und Zugriff auf das eigene Konto, keine Benutzerverwaltung Es gibt (bewusst, siehe unten) keine konfigurierbare Rechte-Matrix und keine Verknüpfung mit einer Mitarbeiter-/Personen-Stammdatentabelle. @@ -44,6 +43,8 @@ Verknüpfung mit einer Mitarbeiter-/Personen-Stammdatentabelle. - **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. + **Konto** ist für jede angemeldete Person erreichbar; **Benutzer** nur für + Administratoren. ## Schutzmechanismen @@ -52,12 +53,12 @@ Verknüpfung mit einer Mitarbeiter-/Personen-Stammdatentabelle. | 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 | -| CSRF über reines HTTP | Browser senden `Sec-Fetch-Site` nur an HTTPS-Adressen. Beim Zugriff per `http://…` im LAN greift deshalb nur SameSite=Lax (schützt gegen fremde Domains, nicht gegen andere Ports desselben Hosts). | +| CSRF | SameSite=Lax **plus** `requireSameOriginForWrites`: jeder Schreibzugriff braucht `X-Leistungsbilanz-Request: 1`; fremde HTML-Formulare können den Header nicht setzen und ein fremder Fetch scheitert an der notwendigen CORS-Preflight-Anfrage. Ein vorhandener `Sec-Fetch-Site`-Header wird zusätzlich geprüft. | +| CSRF über reines HTTP | Der eigene Anwendungsheader gilt auch dann, wenn Browser im HTTP-LAN keinen `Sec-Fetch-Site`-Header senden. Skripte und manuelle API-Aufrufe müssen ihn bei `POST`, `PUT`, `PATCH` und `DELETE` ausdrücklich mitsenden. | | 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 | +| Aussperren | Der letzte aktive Admin kann weder herabgestuft noch deaktiviert werden; Prüfung und Änderung laufen atomar in einer SQLite-Transaktion. Der eigene 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 | @@ -77,6 +78,18 @@ 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. +```dotenv +AUTH_BOOTSTRAP_USERNAME=admin +AUTH_BOOTSTRAP_PASSWORT= +AUTH_SESSION_HOURS=12 +AUTH_SESSION_MAX_DAYS=7 +COOKIE_SECURE=false +``` + +Bei Zugriff über HTTPS muss `COOKIE_SECURE=true` gesetzt werden. Bei direktem +HTTP-Zugriff im LAN bleibt es `false`, da der Browser das Session-Cookie sonst +nicht speichert. + ## Bewusst nicht umgesetzt (Stand jetzt) - Konfigurierbare Rechte-Matrix statt fixer `admin`/`user`-Rollen diff --git a/docs/current-architecture.md b/docs/current-architecture.md index 71b7feb..20c7ccd 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -33,6 +33,15 @@ mit Hot Reload. Das Frontend leitet `/api/*` über `API_INTERNAL_URL` an die API weiter; beim gebauten Stand wird das Ziel zur Build-Zeit festgelegt. Die Datenbank liegt über einen Host-Mount außerhalb des Containers. +Vor allen fachlichen `/api/*`-Routen liegt die Cookie-basierte Anmeldung aus +`src/server/auth`. Nur Login, Status und Logout sind ohne Session erreichbar. +Schreibzugriffe verlangen zusätzlich den Anwendungsheader +`X-Leistungsbilanz-Request: 1`, damit der Schutz auch beim HTTP-Zugriff im LAN +ohne Fetch-Metadata-Header greift. `/api/users/*` ist Administratoren +vorbehalten. Konten sind globale Betriebsdaten und gehören nicht zu +Projektkommandos, Snapshots oder Projekttransfers. Sessions liegen im Speicher +des einzelnen API-Prozesses und enden bei dessen Neustart. + Neue IDs für Stromkreise, Gerätezeilen, Gruppen und Verteilerkomponenten erzeugt der Editor über `src/frontend/utils/client-id.ts` (`createClientId`). Die Hilfsfunktion nutzt `crypto.randomUUID()`, sofern verfügbar, andernfalls @@ -57,6 +66,11 @@ vor dem Command und bleiben über Undo/Redo unverändert. - `src/frontend/components/circuit-grid-*.ts` – reine Grid-Projektion, Zellbesitz, Einfügen und Sicherheitsregeln - `src/frontend/utils/api.ts` – typisierte Frontend-API-Aufrufe +- `src/frontend/components/AuthProvider.tsx` und `LoginGate.tsx` – Anmeldung + und erzwungener Wechsel eines Startpassworts +- `src/server/auth/` – Passwort-Hashing, Sessions und Zugriffsschutz +- `src/domain/services/user.service.ts` und `src/domain/ports/user.store.ts` – + kontobezogene Regeln und schmale Persistenzgrenze - `src/server/index.ts` und `src/server/routes/` – API-Komposition - `src/domain/services/` – fachliche Command- und Synchronisierungsregeln - `src/server/composition/` – Verdrahtung fachlicher Services mit konkreten diff --git a/docs/deployment.md b/docs/deployment.md index 0b5baba..89c99a3 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -16,10 +16,10 @@ und macOS keine inotify-Events durchreichen; das kostet dauerhaft CPU, auch ohne Benutzeraktivität. Dieser Stack gehört deshalb nur auf einen Entwicklungsrechner. -Beide Stacks enthalten weder TLS, Authentifizierung, einen vorgeschalteten -Reverse Proxy noch ein zentral betriebenes Datenbanksystem. Die oben genannten -Container-Schutzmaßnahmen ersetzen diese Funktionen nicht. Der Stack darf -deshalb nicht öffentlich erreichbar gemacht werden. +Beide Stacks enthalten Anmeldung mit festen Rollen, aber weder TLS, einen +vorgeschalteten Reverse Proxy noch ein zentral betriebenes Datenbanksystem. +Passwörter und Session-Cookies werden bei reinem HTTP unverschlüsselt +übertragen. Der Stack darf deshalb nicht öffentlich erreichbar gemacht werden. ## Topologie @@ -29,9 +29,10 @@ deshalb nicht öffentlich erreichbar gemacht werden. | Express API | 3000 | nicht veröffentlicht (Server) / `127.0.0.1:3000` (Dev) | `GET /health` | `./data:/app/data` | | SQLite | Datei | – | Integritäts-/FK-Prüfung via Backup und Skript | `data/leistungsbilanz.db` | -Die Express-API wird im Server-Stack nicht mehr auf dem Host veröffentlicht -(`expose` statt `ports`). Sie hat keine Authentifizierung; jeder, der sie -erreicht, kann jedes Projekt lesen und ändern. Erreichbar ist sie nur aus dem +Die Express-API wird im Server-Stack nicht auf dem Host veröffentlicht +(`expose` statt `ports`). Alle fachlichen API-Routen verlangen eine aktive +Session; Schreibzugriffe zusätzlich den Anwendungsheader +`X-Leistungsbilanz-Request: 1`. Erreichbar ist die API nur aus dem Compose-Netz, praktisch also über den Next.js-Rewrite. Im Entwicklungs-Stack hängen beide Ports am Loopback und sind nicht aus dem LAN erreichbar. @@ -72,6 +73,26 @@ Verwendete Umgebungsvariablen: JSON-Loggers (`error`, `warn`, `info`, `verbose`, `debug`), Standard `info`. Setzbar über eine `.env`-Datei neben `compose.yaml` oder `LOG_LEVEL=verbose docker compose up`. +- `AUTH_BOOTSTRAP_USERNAME` und `AUTH_BOOTSTRAP_PASSWORT` – erzeugen nur bei + leerer Benutzertabelle den ersten Administrator; das Startpasswort hat + mindestens 12 Zeichen. +- `AUTH_SESSION_HOURS` – Inaktivitätsgrenze einer Session, Standard 12 Stunden. +- `AUTH_SESSION_MAX_DAYS` – absolute Session-Laufzeit, Standard 7 Tage. +- `COOKIE_SECURE` – hinter HTTPS `true`; beim direkten HTTP-Zugriff im LAN + `false`, da Browser das Cookie sonst nicht speichern. + +Vor dem ersten Start müssen mindestens die Bootstrap-Werte in einer lokalen, +nicht versionierten `.env` neben `compose.yaml` gesetzt werden: + +```dotenv +AUTH_BOOTSTRAP_USERNAME=admin +AUTH_BOOTSTRAP_PASSWORT= +``` + +Nach dem ersten erfolgreichen Start werden diese Werte ignoriert, solange +mindestens ein Benutzer existiert. Weitere Zugänge legt ein Administrator in +**Config → Benutzer** an. Details stehen in +[Anmeldung und Benutzerverwaltung](authentication.md). Beim API-Start laufen zuerst die Migrationen und die Schemaprüfung (`scripts/run-migrations.js` und `scripts/db-verify-circuit-schema.js`, im @@ -137,7 +158,8 @@ bleibt `sh` der Vordergrundprozess, und `SIGTERM` erreicht Node nie. Vor einer produktiven Installation werden mindestens benötigt: - TLS-Termination und Reverse Proxy -- Authentifizierung, Autorisierung und Benutzer-/Rollenmodell +- zentraler Identitätsanbieter oder eine feinere Rechte-Matrix, falls die zwei + festen Rollen später nicht ausreichen - definierte Secrets- und Konfigurationsverwaltung - persistenter, gesicherter Datenbankbetrieb - kontrollierter einmaliger Migrationsschritt vor dem API-Rollout