Jeder ERPframe-Import (Scheduler, Startup, manuell) wird in der neuen Tabelle erp_sync_runs protokolliert (target_date, trigger, success, Zaehler, Fehler). Beim Serverstart synct das Backend das Zieldatum (heute + offset, i.d.R. morgen) nach, falls dafuer noch kein erfolgreicher Lauf dokumentiert ist — deckt Erststart UND laengere Unterbrechungen ab, bei denen der Cron-Zeitpunkt verpasst wurde. Gated ueber [import] enabled. - Migration 0030_erp_sync_runs - Port SyncRunRepository (+ SyncRunRecord, SyncTrigger) - Adapter PgSyncRunRepository - ImportErpToursUseCase dokumentiert jeden Lauf; neues execute_with(date, trigger) - main.rs: Repo verdrahtet, Scheduler-Trigger, Startup-Catch-up (tokio::spawn) Co-Authored-By: Claude Opus 4.8 (1M context) <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.