Files
Vorrania/README.md
T
ScarriffleandClaude Opus 5 3d0ff54bf5 Anmeldung wahlweise ueber OpenID Connect (Authentik)
Neben dem Passwort-Login gibt es jetzt einen zweiten, gleichberechtigten Weg:
OpenID Connect gegen einen selbst gehosteten Anmeldedienst. Ohne Konfiguration
aendert sich nichts - keine SSO-Knoepfe, Passwort-Login wie bisher.

Der Anmeldedienst ist NUR fuer die Anmeldung zustaendig. Danach stellt Vorrania
sein eigenes Token aus wie beim Passwort-Login, deshalb bleiben deps.py, die
Rollenpruefung und die langlebigen API-Tokens unveraendert.

Backend
- Anbieter kommen aus der Umgebung (OIDC_PROVIDERS + OIDC_<KUERZEL>_*). Ein
  zweiter Anbieter ist damit reine Konfiguration, ohne Codeaenderung.
- Browser-Weg (vertraulicher Client) und App-Weg (oeffentlicher Client, PKCE)
  enden beide im selben Token-Schema.
- state/nonce/PKCE-Verifier liegen in einer kurzlebigen Tabelle statt in einem
  Cookie: Starlettes CORS-Schicht spiegelt bei allow_origins=["*"] zusammen mit
  allow_credentials die Herkunft zurueck, sobald ein Request ein Cookie traegt.
  Ohne Cookie entsteht diese Wechselwirkung erst gar nicht - und der Ablauf
  bleibt mit der db-Fixture pruefbar.
- Zuordnung ueber die sub des Anbieters; die Erstverknuepfung laeuft ueber die
  BESTAETIGTE E-Mail-Adresse. Rollen kommen nie aus dem Anbieter, und ein
  gleichlautender preferred_username uebernimmt nie ein bestehendes Konto.
- users.password_hash darf jetzt leer sein (reine SSO-Konten). verify_password
  faengt das ab, sonst antwortete der Login mit 500 statt 401.

Verknuepfen bestehender Konten
- Neue Seite "Mein Konto" (fuer alle Benutzer, nicht nur Admins): verknuepfen
  und wieder loesen. Die Berechtigung stammt aus der laufenden Sitzung, es
  braucht also weder eine hinterlegte Adresse noch Admin-Rechte.
- Der Start laeuft ueber POST, weil eine Seitennavigation das Bearer-Token
  nicht mitschicken kann.
- Die letzte Anmeldemoeglichkeit eines Kontos laesst sich nicht loesen.

iOS
- AppAuth (erste Paketabhaengigkeit) mit PKCE im System-Browser, nie in einem
  eingebetteten WebView, und ohne Client-Secret.
- offline_access nur im App-Weg: der Browser-Weg braucht kein Refresh-Token und
  bekaeme sonst einen ueberfluessigen Zustimmungsdialog.
- Anmeldezustand je Serverprofil im Keychain; abgelaufene Token erneuert die App
  still im Hintergrund. Gleichzeitige 401er teilen sich eine Erneuerung.

Deployment
- docker-compose reicht die ganze .env durch. Einzeln aufzuzaehlen ginge nur mit
  fest verdrahteten Kuerzeln - genau das soll ein weiterer Anbieter nicht
  erfordern.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 21:55:38 +02:00

223 lines
9.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.
# Vorrania – Selfhostbare Vorrats- und Gegenstandsverwaltung
Ein selbst gehosteter Dienst, um sowohl deinen **Lebensmittelvorrat** als auch
allgemeine **Gegenstände** (Haushalt, Elektronik, Kleidung, Verbrauchsmaterial …)
zu verwalten: Ein-/Auslagern per Barcode, Chargen mit MHD, Einzelstücke mit
eigener UID/QR, Mindestbestände, automatische Einkaufsliste, verschachtelte
Lagerorte, Gruppen, Kategorien und Dashboards – mit **Web-UI** und **nativer
iOS-App**, die beide dieselbe REST-API nutzen.
## Was Vorrania verwaltet
**Lebensmittel** laufen als **Chargen** mit Mindesthaltbarkeitsdatum. Beim
Auslagern wird automatisch die zuerst ablaufende Charge entnommen (**FEFO**).
**Gegenstände** kennen drei Arten:
- **Menge je Lagerort** – reines Zählen (z. B. Unterhosen, Batterien).
- **Einzelstücke** – jedes Exemplar mit eigener **UID/QR**, Kaufdatum, Garantie,
Bezugsquelle und Beleg (z. B. Powerbank, Kamera).
- **Verbrauchsgegenstand** – wie ein Lebensmittel als Charge mit Menge und
Einheit geführt (z. B. Sonnencreme in ml), nur ohne eindeutigen Code.
## Features
- **Barcode-Lookup** über [Open Food Facts](https://world.openfoodfacts.org) und
Open Products Facts mit lokalem Fallback; unbekannte Produkte selbst anlegen,
Bild wird automatisch geholt.
- **Chargen mit MHD & FEFO**, Mengen in Packungen **oder** Teilmengen
(Basiseinheit Stück / Gramm / Milliliter plus Packungsgröße/Gebinde).
- **Lagerorte** beliebig verschachtelt (Keller → Regal 2 → Fach A), mit
**QR-Etiketten**, **Inhaltsansicht** („was liegt hier?“), Umlagern und
Entfernen mit Grund. Chargen und Einzelstücke sind einem Lagerort zugeordnet.
- **Kategorien** verschachtelt und pro Typ (Lebensmittel/Gegenstand); für
Gegenstände mit **eigenen Feldern** (z. B. Kapazität, Größe, Farbe).
- **Gruppen** rechnen Bestände mehrerer Marken/Artikel zusammen (z. B. „5 kg
Mehl“) mit eigenem Mindestbestand und Einheit.
- **Mindestbestände** je Produkt und Gruppe – gesamt **und je Lagerort** – mit
zentraler Übersichtsseite, daraus die automatische **Einkaufsliste** und
**Ablaufwarnung** für bald ablaufende Chargen.
- **Übergreifende Listen** für Produkte (getrennt Lebensmittel/Gegenstände),
Einzelstücke und Chargen – filter-, sortier- und spaltenkonfigurierbar; bei
Chargen lässt sich der Lagerort mehrerer auf einmal setzen.
- **Dashboards** mit konfigurierbaren Karten und Diagrammen, **Verlauf** (wer hat
wann was ein-/ausgelagert), **Stammdaten** (Einheiten, Gebinde, Shops) und
**Import/Export**.
- **Mehrbenutzer mit Rollen:** *Admin* verwaltet Stammdaten und Benutzer,
*Nutzer* lagert nur ein/aus und sieht Bestände; alle Bewegungen werden
protokolliert.
- **Single Sign-on (optional)** über OpenID Connect, z. B. gegen ein selbst
gehostetes [Authentik](https://goauthentik.io). Der Passwort-Login bleibt
daneben bestehen – siehe [Single Sign-on](#single-sign-on-optional).
### In der iOS-App zusätzlich
- **Scannen** von Barcode, **MHD/Datum per Texterkennung** (auch nur Tag+Monat
ohne Jahr bei Frischware), **Lagerort-QR** und **Name/Marke per Text-Scan**.
- **Foto direkt beim Anlegen** (Kamera/Galerie), **Push-Benachrichtigungen** bei
ablaufenden Produkten, lokale Dashboards und Home-Screen-Schnellaktionen.
Details zur App: [ios/README.md](ios/README.md).
## Architektur
| Teil | Technik |
|---------|----------------------------------------|
| Backend | Python · FastAPI · SQLAlchemy |
| Anmeldung | Passwort (JWT) und optional OpenID Connect |
| DB | PostgreSQL (SQLite für Tests/lokal) |
| Web-UI | React · Vite (via nginx ausgeliefert) |
| iOS | Native SwiftUI-App (VisionKit-Scanner) |
| Deploy | Docker Compose · `install.sh` |
```
backend/ FastAPI-App + Tests
web/ React/Vite SPA
ios/ Native SwiftUI-App (XcodeGen-Projekt)
deploy/ docker-compose.yml, install.sh, update.sh, .env.example
docs/ Roadmap
```
Das Schema wird beim Start per idempotenter `ALTER TABLE`-Migration in
`backend/app/main.py` nachgezogen (kein Alembic).
## Installation (selfhosted, Linux)
Voraussetzung: eine Linux-Maschine (Server, NAS, Raspberry Pi …). Docker wird bei
Bedarf automatisch installiert.
```bash
git clone https://git.scarriffle.com/Scarriffle/vorrania.git
cd vorrania/deploy
chmod +x install.sh
./install.sh # interaktiv – fragt Port & Admin-Passwort
# oder vollautomatisch mit generiertem Admin-Passwort:
# ./install.sh --yes
```
> Ist das Gitea-Repo privat, fragt `git clone` nach Benutzername + Access-Token.
> Falls `git` fehlt: `apt update && apt install -y git`.
Nach dem Start:
- Web-UI: `http://<server-ip>:8080` (Port konfigurierbar)
- Anmeldung mit dem im Installer angezeigten Admin-Benutzer/Passwort.
Die iOS-App wird mit derselben Adresse verbunden (`http://<server-ip>:8080`).
## Single Sign-on (optional)
Neben dem Passwort-Login kann sich Vorrania an einen OpenID-Connect-Dienst
hängen (getestet mit Authentik). Ohne Konfiguration ändert sich nichts: Es gibt
keinen SSO-Knopf, und der Passwort-Login bleibt der einzige Weg.
**1. In Authentik zwei Provider anlegen** – die Web-Oberfläche und die App
brauchen unterschiedliche Client-Typen:
| Für | Typ | Redirect-URI |
|-----|-----|--------------|
| Web-UI | *Confidential* (mit Secret) | `https://<deine-adresse>/api/auth/oidc/authentik/callback` |
| iOS-App | *Public* (ohne Secret, PKCE) | `vorrania://oauth/callback` |
Das `/api` in der Web-Adresse ist kein Versehen: Es entsteht im mitgelieferten
nginx und gehört deshalb in die bei Authentik hinterlegte Adresse.
Damit die App ihre Anmeldung im Hintergrund erneuern kann, muss der
öffentliche Client `offline_access` ausliefern dürfen (in Authentik seit 2024.2
ein eigenes Scope-Mapping) – sonst kommt kein Refresh-Token zurück.
**2. `deploy/.env` ergänzen** (vollständige Vorlage in `deploy/.env.example`):
```bash
PUBLIC_BASE_URL=https://vorrania.example.com
OIDC_PROVIDERS=authentik
OIDC_AUTHENTIK_DISPLAY_NAME=Authentik
OIDC_AUTHENTIK_DISCOVERY_URL=https://authentik.example.com/application/o/vorrania/.well-known/openid-configuration
OIDC_AUTHENTIK_CLIENT_ID=...
OIDC_AUTHENTIK_CLIENT_SECRET=...
OIDC_AUTHENTIK_NATIVE_CLIENT_IDS=vorrania-ios
```
Danach `./update.sh` (oder `docker compose --env-file .env up -d`).
Ob es geklappt hat, verrät `curl http://<server>:8080/api/auth/oidc/providers`.
**3. Bestehende Konten verknüpfen.** Dafür gibt es zwei Wege:
*Selbst verknüpfen (empfohlen).* Angemeldet bleiben, in der Seitenleiste unten
auf den eigenen Namen klicken → **Mein Konto** → *Verknüpfen*. Das funktioniert
für jeden Benutzer, braucht keine hinterlegte E-Mail-Adresse und keine
Admin-Rechte: Die Berechtigung stammt aus der laufenden Sitzung. Dort lässt sich
eine Verknüpfung auch wieder lösen – außer es wäre die letzte
Anmeldemöglichkeit des Kontos.
*Über die E-Mail-Adresse.* Trage sie unter *Benutzer* ein – dieselbe wie in
Authentik. Beim ersten SSO-Login wird das Konto dann automatisch gefunden.
Verknüpft wird nur bei **bestätigter** Adresse.
Ohne eines von beidem entsteht beim ersten Anmelden ein **zweites** Konto mit
der Rolle *Benutzer*, statt dass das bestehende übernommen wird.
Weitere Anbieter sind reine Konfiguration: Kürzel in `OIDC_PROVIDERS` ergänzen
und einen zweiten Block mit dessen Präfix anlegen (`OIDC_<KÜRZEL>_…`) – am Code
ändert sich dafür nichts.
### Was dabei bewusst so gebaut ist
- Der Anmeldedienst ist **nur für die Anmeldung** zuständig. Danach stellt
Vorrania sein eigenes Token aus, wie beim Passwort-Login – Rollen, API-Tokens
und alle Endpunkte bleiben unverändert.
- **Rollen kommen nie aus dem Anbieter.** Neu angelegte SSO-Konten sind immer
*Benutzer*; Admin vergibt ein Admin von Hand.
- **Automatisch** verknüpft wird nur bei bestätigter E-Mail-Adresse. Sonst
könnte ein Konto im Anmeldedienst mit frei gewählter Adresse ein fremdes
Vorrania-Konto übernehmen. Beim Verknüpfen über *Mein Konto* entfällt das,
weil dort die eigene Sitzung bürgt – eine bereits anderweitig vergebene
Kennung wird aber auch dort abgelehnt.
- Die App meldet sich ausschließlich im **System-Browser** an (AppAuth mit
PKCE), nie in einem eingebetteten WebView, und sendet nie ein Client-Secret.
## Aktualisieren
```bash
cd vorrania/deploy
./update.sh # holt den neuesten Stand aus Git und baut neu
./update.sh --prune # zusätzlich alte Images aufräumen
./update.sh --no-pull # nur neu bauen, ohne git pull
```
Das Skript ist von überall aufrufbar, nutzt die vorhandene `deploy/.env` und
prüft am Ende, ob das Backend wieder erreichbar ist.
Verwaltung:
```bash
cd deploy
docker compose logs -f # Logs ansehen
docker compose down # stoppen
docker compose up -d # starten
```
Die Konfiguration liegt in `deploy/.env` (Passwörter, Port, JWT-Secret). Für den
Produktivbetrieb bitte hinter einen HTTPS-Reverse-Proxy (z. B. Caddy/Traefik) stellen.
## Entwicklung (ohne Docker)
**Backend:**
```bash
cd backend
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
# lokale SQLite-DB nutzen:
export DATABASE_URL="sqlite:///./vorrania.db"
uvicorn app.main:app --reload
```
API-Doku dann unter `http://localhost:8000/docs`.
**Web:**
```bash
cd web
npm install
npm run dev # http://localhost:5173, proxyt /api → localhost:8000
```
**iOS:** siehe [ios/README.md](ios/README.md) (XcodeGen-Projekt, `xcodegen generate`).
**Tests:**
```bash
cd backend
pip install -r requirements.txt
DATABASE_URL="sqlite://" pytest
```
## Weiteres
Fahrplan und Ideen: [docs/ROADMAP.md](docs/ROADMAP.md).