Dennis Nemec 819005eaa5 feat(dev): /dev/reset-delivery — einzelne Lieferung per Belegnummer zurücksetzen
Setzt EINE Lieferung zurück, ohne die Tour zu löschen (Alternative zum
destruktiven /dev/resync): Abschluss (delivery_completions), Scan-/
Gutschrift-Audit + offener Report-Job weg, Positionen zurück (scanned/
credited = 0, Status in_progress), Lieferung wieder active (state_reason/
assigned_car/review_* geleert). Notizen/Dienstleistungen/Anhänge bleiben.

Vervollständigt den bisher nur als Port-Stub vorhandenen
reset_delivery_by_belegnummer (Build war dadurch kaputt) + Use Case +
DEV-Route (nur bei dev.sync_enabled gemountet, unauthentifiziert).

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

Holzleitner-Backend

Rust-Backend für die Holzleitner-Lieferservice-App. Workspace mit Clean Architecture: domainapplicationinfrastructureapi.

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.

  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

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.

Description
No description provided
Readme 533 KiB
Languages
Rust 92.7%
Shell 6.7%
PowerShell 0.6%