Die Anmeldung galt fest 7 Tage - und die Frist lief ab dem *Anmelden*, nicht ab der letzten Nutzung. Wer die App taeglich benutzte, flog trotzdem punktgenau nach einer Woche hinaus. Beides ist jetzt geregelt: - Einstellungen -> Anmeldung: 7 Tage bis 1 Jahr oder unbegrenzt. Gilt fuer Website und App gemeinsam, weil beide dasselbe Token bekommen - auch nach Single Sign-on. - POST /auth/refresh schiebt die Frist beim Start nach vorn. Damit zaehlt die eingestellte Dauer ab dem letzten Besuch, und eine geaenderte Einstellung greift ohne Neuanmeldung. "Unbegrenzt" heisst ein Token ganz ohne exp. Das ist bewusst folgenreich: Es gibt keine Sperrliste, eine erteilte Anmeldung laesst sich nicht einzeln zurueckziehen. Die Oberflaeche sagt das dazu und nennt den einzigen Ausweg (JWT_SECRET aendern). /auth/refresh nimmt bewusst nur JWTs an, keine langlebigen API-Token: aus einem jederzeit widerrufbaren Zugang (Home Assistant) waere sonst ein unwiderruflicher geworden. Der Rueckfallwert aus der Umgebung steigt von 7 auf 30 Tage - er gilt nur noch, solange nichts eingestellt ist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
260 lines
12 KiB
Markdown
260 lines
12 KiB
Markdown
# 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.
|
||
- **Einzelstücke verleihen** mit Name und Grund als Freitext und optionalem
|
||
Rückgabedatum – auch direkt nach dem Scannen des QR/NFC-Tags. Der Lagerort
|
||
bleibt stehen (dorthin gehört das Stück zurück), als verliehen zählt es aber
|
||
nicht mehr zum Bestand. Zurückgegebene Verleihe bleiben als Verlauf erhalten;
|
||
überfällige Rückgaben sind in Liste und Dashboard rot.
|
||
- **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).
|
||
- **Sitzungsdauer einstellbar** (Einstellungen → Anmeldung): 7 Tage bis 1 Jahr
|
||
oder unbegrenzt, für Website und App gemeinsam. Die Frist zählt ab dem letzten
|
||
Besuch, nicht ab dem Anmelden.
|
||
|
||
### 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**.
|
||
- **Verleihen direkt nach dem Scannen** – QR, eingenähtes NFC-Tag oder
|
||
angetipptes Tag landen alle im Einzelstück, wo Herausgeben und Zurücknehmen
|
||
gleich oben stehen.
|
||
- **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 Anwendungen anlegen** – Web-UI und App brauchen
|
||
unterschiedliche Client-Typen, und ein Provider kann nur einer davon sein:
|
||
|
||
| Für | Client-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` |
|
||
|
||
Drei Punkte, an denen es sonst klemmt:
|
||
|
||
- Das `/api` in der Web-Adresse ist kein Versehen: Es entsteht im mitgelieferten
|
||
nginx und gehört deshalb in die bei Authentik hinterlegte Adresse.
|
||
- **Signing Key setzen** (Provider ▸ *Advanced protocol settings*). Ohne ihn
|
||
signiert Authentik mit HS256, und Vorrania prüft gegen den öffentlichen
|
||
Schlüsselsatz – das Token würde abgelehnt.
|
||
- Jede Anwendung hat in Authentik **ihren eigenen Issuer** (aus dem
|
||
Application-Slug). Deshalb bekommt der App-Client eine eigene
|
||
Discovery-Adresse: `OIDC_AUTHENTIK_NATIVE_DISCOVERY_URL`.
|
||
|
||
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
|
||
OIDC_AUTHENTIK_NATIVE_DISCOVERY_URL=https://authentik.example.com/application/o/vorrania-ios/.well-known/openid-configuration
|
||
```
|
||
|
||
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.** Beim ersten SSO-Login zu einer noch
|
||
unbekannten Kennung fragt Vorrania nach, statt still ein zweites Konto
|
||
anzulegen:
|
||
|
||
> Die Anmeldung über Authentik hat geklappt – zu diesem Zugang gibt es hier aber
|
||
> noch kein Konto.
|
||
> **[ Bestehendes Konto verknüpfen ]** oder **[ Neues Konto anlegen ]**
|
||
|
||
Das Verknüpfen verlangt einmalig Benutzername und Passwort des bestehenden
|
||
Kontos. Dieser Nachweis ist der Grund, warum der Anmeldeweg selbst nie fragt
|
||
„welches Konto?" – ohne ihn könnte sich jeder mit einem beliebigen Konto beim
|
||
Anmeldedienst an ein fremdes Vorrania-Konto hängen. Rolle und Verlauf bleiben
|
||
dabei erhalten.
|
||
|
||
Zwei Wege führen daran vorbei, weil sie das Konto schon vorher finden:
|
||
|
||
- **E-Mail-Adresse** unter *Benutzer* eintragen – dieselbe wie in Authentik.
|
||
Verknüpft wird nur bei **bestätigter** Adresse.
|
||
- **Vorab verknüpfen**: angemeldet bleiben, unten in der Seitenleiste auf den
|
||
eigenen Namen → **Mein Konto** → *Verknüpfen*. Dort lässt sich eine
|
||
Verknüpfung auch wieder lösen – außer es wäre die letzte Anmeldemöglichkeit.
|
||
|
||
Mit `OIDC_AUTHENTIK_AUTO_CREATE=false` entfällt der Knopf „Neues Konto anlegen";
|
||
dann bleibt nur das Verknüpfen mit einem Konto, das der Admin angelegt hat.
|
||
|
||
Die App zeigt diesen Dialog nicht – dort gilt weiterhin: bekannte Kennung oder
|
||
passende Adresse, sonst je nach `AUTO_CREATE` ein neues Konto oder eine Absage.
|
||
Einmal im Browser verknüpft, funktioniert die App danach ohne Weiteres.
|
||
|
||
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.
|
||
- Der **Benutzername** wird einmal beim Anlegen übernommen und gehört danach
|
||
Vorrania – ein späterer Namenswechsel beim Anmeldedienst schlägt nicht durch.
|
||
Ändern lässt er sich jederzeit unter *Benutzer*: Sitzungen, Verlauf und
|
||
API-Tokens hängen an der Benutzer-ID, nicht am Namen.
|
||
- **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).
|