Beleg-, Liefer-, Rechnungsadresse, Ansprechpartner und Kundenstamm tragen jetzt neben Namensblock und Kanälen auch ihre Anschrift (Straße, Nr., PLZ, Ort, Land aus dem Länderstamm, Adresszusatz), sofern im ERP gepflegt. Migration 0035 ergänzt die Spalten an delivery_contact_sources; ContactSource.address in der API. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Holzleitner-Backend
Rust-Backend für die Holzleitner-Lieferservice-App. Workspace mit Clean
Architecture: domain → application → infrastructure → api.
Schnellstart (lokale Entwicklung)
# 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:
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):
# 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.
- Die Flutter-App loggt sich per OIDC Authorization Code + PKCE gegen
{KC_URL}/realms/{realm}ein und erhält ein Access-Token (JWT, RS256) mitaud=holzleitner-api, Claimpersonalnummerund Realm-Rolledriver. - 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ältholzleitner-api) und Ablauf. Kein gemeinsames Secret nötig — nur der öffentliche Schlüssel. - Die
/admin-Maschinen-Endpunkte des Backends laufen NICHT über Keycloak, sondern über den statischenX-Admin-Api-Key(siehe[admin] api_key). - Der Provisioner (
holzleitner-provisioner, confidential, Client- Credentials-Grant mitmanage-users) wird vom ERP-Sync genutzt, um neue Fahrer-Konten im Realm anzulegen — dafür gilt dasprovisioner_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
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:
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:
# 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.
# 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.