Files
Holzleitner---Backend--aktu…/README.md
Dennis Nemec c65e13485d keycloak_bootstrap: parametrisierbar für baremetal-Prod
Das Bootstrap-Skript funktioniert jetzt gegen jede Keycloak-Instanz (Dev-Docker
wie baremetal-Prod) und nimmt Realm + Server-Adresse als Parameter:
- --url <basis-inkl-port>, --realm <name> (überschreibt den Namen in der
  JSON-Payload), --admin/--admin-password.
- --provisioner-secret <s>: ersetzt das Dev-Secret in der Payload (Prod).
- --no-test-user: entfernt "testfahrer" aus der Payload (Prod).
- Docker wird per Default NICHT mehr angefasst (Prod=baremetal); nur via --up.
- Alle Optionen auch via Env; CLI hat Vorrang.

README: parametrisierte Nutzung (Dev + Prod-Beispiel) + Abschnitt "Wie die
Authentifizierung funktioniert" (Skript→Keycloak Admin-Grant; Laufzeit
App↔Backend↔Keycloak: PKCE-Login, JWKS-JWT-Validierung mit aud/iss, Provisioner
client-credentials, X-Admin-Api-Key für /admin).

Verifiziert gegen Dev-Keycloak: custom Realm + überschriebenes Secret +
--no-test-user erzeugt korrekten Realm; Test-Realm danach wieder entfernt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 18:24:55 +02:00

209 lines
8.5 KiB
Markdown
Raw 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.

# Holzleitner-Backend
Rust-Backend für die Holzleitner-Lieferservice-App. Workspace mit Clean
Architecture: `domain``application``infrastructure``api`.
## Schnellstart (lokale Entwicklung)
```bash
# 1) Postgres + Keycloak hochfahren
docker compose up -d
# Keycloak braucht ~30s bis "Listening on http://0.0.0.0:8080" im Log steht.
# 2) Konfiguration vorbereiten
cp config.example.toml config.toml
# Werte in config.toml anpassen (DB-URL, Keycloak-Issuer, ERP-Zugang, …).
# 3) Backend starten
cargo run -p holzleitner-api
```
Migrations laufen beim Start automatisch über `sqlx::migrate!`.
Smoke-Test danach:
```bash
curl http://127.0.0.1:3000/health
# → ok
curl http://127.0.0.1:3000/accounts/1001
# → {"personalnummer":1001,"name":"Müller Logistik GmbH","active":true}
```
## Keycloak (Dev)
| Wo | URL / Credentials |
|---|---|
| Admin-Console | http://localhost:8080/admin/ (admin / admin) |
| Realm | `holzleitner` |
| Client | `holzleitner-app` (public, PKCE) |
| Test-User | `testfahrer` / `test` · Personalnummer 1001 · Rolle `driver` |
| Audience im Access-Token | `holzleitner-api` |
Der Realm liegt in `keycloak/import/realm-holzleitner.json` und ist bereits
passend zum Backend konfiguriert (Client `holzleitner-app` mit PKCE +
Audience-Mapper `holzleitner-api` + `personalnummer`-Claim, Rolle `driver`,
Service-Account-Client `holzleitner-provisioner`, Test-User). Wer in der
Admin-UI Änderungen macht, sollte sie **in die JSON zurückspielen**.
**Wichtig:** Das compose-`--import-realm` greift nur beim **ersten** Start
(solange das `keycloak-data`-Volume leer ist) — spätere JSON-Änderungen landen
NICHT automatisch in Keycloak. Realm anwenden/bootstrappen geht daher über
`tool/keycloak_bootstrap.sh` (spricht nur die Admin-REST-API an, funktioniert
gegen **Dev-Docker wie baremetal-Prod**):
```bash
# Dev (lokales Docker mit hochfahren, Default-Realm "holzleitner")
./tool/keycloak_bootstrap.sh --up
# Production (baremetal Keycloak, eigener Realm, frisches Secret, kein Test-User)
./tool/keycloak_bootstrap.sh \
--url https://auth.holzleitner.de \
--realm holzleitner-prod \
--admin kc-admin --admin-password "$KC_PW" \
--provisioner-secret "$(openssl rand -hex 24)" \
--no-test-user
# Sauberer Neu-Import (LÖSCHT den Realm inkl. provisionierter Fahrer-Konten):
./tool/keycloak_bootstrap.sh --url ... --realm ... --reset
```
Wichtigste Optionen: `--url` (Keycloak-Basis inkl. Port), `--realm` (Realm-Name),
`--admin`/`--admin-password`, `--provisioner-secret`, `--no-test-user`, `--reset`,
`--up`. Alles auch via Env (`KC_URL`, `REALM`, `KC_ADMIN`, `KC_ADMIN_PASSWORD`,
`PROVISIONER_SECRET`, `REALM_FILE`). Default ohne `--up` wird **kein** Docker
angefasst. Das Skript gibt am Ende die passenden `[keycloak]`-Werte für die
`config.toml` aus.
### Wie die Authentifizierung funktioniert
Es gibt zwei getrennte Auth-Kontexte:
**a) Bootstrap-Skript → Keycloak (einmalig/selten).** Das Skript meldet sich am
`master`-Realm über den eingebauten Client `admin-cli` per Passwort-Grant an
(`--admin`/`--admin-password`) und nutzt das Admin-Token nur, um den Realm
anzulegen. In Prod ein echtes Admin-Konto + HTTPS verwenden; das Passwort besser
über die Env-Variable `KC_ADMIN_PASSWORD` setzen (nicht in der History).
**b) Laufzeit — App ↔ Backend ↔ Keycloak.**
1. Die Flutter-App loggt sich per **OIDC Authorization Code + PKCE** gegen
`{KC_URL}/realms/{realm}` ein und erhält ein **Access-Token (JWT, RS256)** mit
`aud=holzleitner-api`, Claim `personalnummer` und Realm-Rolle `driver`.
2. Das Backend **validiert das JWT public-key-basiert**: es holt die Realm-JWKS
von `{issuer}/protocol/openid-connect/certs`, prüft Signatur, `iss` (==
`config.toml [keycloak] issuer_url`), `aud` (enthält `holzleitner-api`) und
Ablauf. Kein gemeinsames Secret nötig — nur der öffentliche Schlüssel.
3. Die `/admin`-Maschinen-Endpunkte des Backends laufen NICHT über Keycloak,
sondern über den statischen `X-Admin-Api-Key` (siehe `[admin] api_key`).
4. Der **Provisioner** (`holzleitner-provisioner`, confidential, Client-
Credentials-Grant mit `manage-users`) wird vom ERP-Sync genutzt, um neue
Fahrer-Konten im Realm anzulegen — dafür gilt das `provisioner_client_secret`.
Damit b) klappt, müssen `issuer_url`/`realm`/`admin_url` in der `config.toml`
**exakt** auf denselben Server + Realm zeigen, den das Skript angelegt hat — und
`issuer_url` muss dem von Keycloak ausgestellten `iss`-Claim entsprechen (in
Prod also Keycloaks Frontend-/Hostname-URL korrekt setzen, sonst `invalid issuer`).
### Token für Dev-Tests holen
```bash
curl -s -X POST \
http://localhost:8080/realms/holzleitner/protocol/openid-connect/token \
-d 'grant_type=password' \
-d 'client_id=holzleitner-app' \
-d 'username=testfahrer' \
-d 'password=test' | jq -r .access_token
```
Den Token in den `Authorization`-Header packen, sobald die JWT-Middleware
in der API-Schicht aktiv ist:
```bash
TOKEN=$(curl -s -X POST .../token -d ... | jq -r .access_token)
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:3000/accounts/1001
```
## Crate-Layout
| Crate | Inhalt | Abhängigkeiten |
|---|---|---|
| `holzleitner-domain` | Reines Domänenmodell (serde + UUID + chrono) | — |
| `holzleitner-application` | Use Cases und Ports (Trait-Definitionen für Repository, AuthService) | `domain` |
| `holzleitner-infrastructure` | Konkrete Adapter (sqlx-Postgres, später Keycloak) | `domain`, `application` |
| `holzleitner-api` | Axum HTTP-Layer + Composition Root | alle |
## Konfiguration
Werte werden aus `config.toml` gelesen (Vorlage: `config.example.toml`),
gruppiert in TOML-Sections. Der Dateipfad ist über die Env-Variable
`HOLZLEITNER_CONFIG` überschreibbar (z. B. im Deployment); Default ist
`config.toml` im Arbeitsverzeichnis. Die echte `config.toml` enthält
Secrets und ist `.gitignore`-t.
| Section | Bereich | Pflicht? |
|---|---|---|
| `[server]` | Bind-Host/Port | ja |
| `[database]` | Postgres-URL, Pool-Größe | ja |
| `[keycloak]` | OIDC-Issuer, Audience, JWKS-Cache, Provisioning | ja |
| `[gsd]` | DOCUframe-REST (Datei-Upload) | ja |
| `[erp]` | ERPframe-MSSQL (Touren-Pull) | optional |
| `[import]` | Import-Scheduler (Cron, Offset) | optional |
| `[report]` / `[signature]` / `[attachment]` | Lokale Speicher-Pfade | optional |
| `[dev]` | `today_override`, `sync_enabled` (DEV-ONLY) | optional |
| `[admin]` | `api_key` für das `/admin`-Gate | optional |
| `[logging]` | Log-Filter (Default; `RUST_LOG`-Env hat Vorrang) | optional |
Unbekannte Schlüssel werden beim Laden abgewiesen (`deny_unknown_fields`),
sodass Tippfehler sofort als Startfehler auffallen.
## Migrations
Migrations liegen im Workspace-Root `migrations/`. Eingebettet via
`sqlx::migrate!()` — kein zusätzlicher Laufzeit-Dateizugriff nötig.
Neue Migration anlegen:
```bash
# Format: <epoch>_<beschreibung>.sql, z.B.:
touch migrations/0002_tour.sql
```
## Logging
`tracing` + `tracing-subscriber` mit `EnvFilter`. Der Default-Filter steht
in `config.toml` unter `[logging] filter`; die Env-Variable `RUST_LOG` hat
Vorrang (Ad-hoc-Debugging ohne Datei-Edit, z. B. `RUST_LOG=debug cargo run`).
Im **Konsolenmodus** geht alles auf `stderr`. Im **Windows-Dienst-Modus**
(keine Konsole) wird stattdessen in rollende Tages-Logdateien unter
`[logging] dir` (Default `logs/`, relativ zur EXE) geschrieben.
## Windows-Dienst
Das Backend kann als Windows-Dienst laufen (gleiches Muster wie der
Mail-Client). Beim Start versucht die EXE zuerst, sich beim Service Control
Manager zu registrieren; gelingt das nicht (interaktiver Start), fällt sie in
den Konsolenmodus zurück. `--console` (bzw. `-c`) erzwingt den Konsolenmodus.
Der Dienst setzt sein Arbeitsverzeichnis auf das EXE-Verzeichnis — `config.toml`,
`data/` und `logs/` müssen also **neben der EXE** liegen.
```powershell
# 1) Release-Build (auf dem Zielsystem oder per Cross-Build)
cargo build --release -p holzleitner-api
# 2) target\release\holzleitner-server.exe + config.toml + die *.ps1-Skripte
# in ein Installationsverzeichnis kopieren, z. B. C:\HolzleitnerBackend\
# 3) Als Administrator registrieren (Dienst "Holzleitner Backend")
.\install-service.ps1
# Optional mit Dienstkonto (DB-/Netzwerkzugriff):
.\install-service.ps1 -Credential (Get-Credential)
# Entfernen
.\uninstall-service.ps1
```
Der Dienst startet verzögert-automatisch (nach den System-/DB-Diensten) und
wird bei Absturz 3× im Abstand von 60 s neu gestartet. Interner Dienst-Name:
`HolzleitnerBackend`. Boot-Fehler **vor** der Logger-Initialisierung (z. B.
fehlende `config.toml`) landen in `holzleitner-backend-fatal.log` neben der EXE.