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