Verleihen und Zuruecknehmen in der Einzelstueck-Tabelle eines Artikels und in der Liste aller Einzelstuecke, dort als Spalten "Verliehen an" und "Zurueck bis" (filter- und sortierbar). Ueberfaellig ist rot, demnaechst faellig gelb - dieselben Zustandsfarben wie beim MHD. Die Scan-Zielseite /i/<uid> leitete vorher nur zum Artikel weiter. Fuer "scannen und verleihen" braucht die Aktion aber genau dort ihren Platz: Die Seite zeigt jetzt, was man in der Hand haelt - Stueck, Artikel, Lagerort, Verleihzustand - und bietet Verleihen bzw. Zuruecknehmen an. Gegenstueck zur Lagerort-Seite /l/<code>, die es fuers Einlagern genauso macht. Anzeige und Aktionen liegen einmal in components/Verleih.jsx, weil alle drei Stellen dasselbe brauchen. Der Knopf haengt nicht an isAdmin: Verleihen ist eine Buchung wie Ein- und Auslagern. Dazu zwei Dashboard-Karten: alles Verliehene und, getrennt, nur das Ueberfaellige. Stuecke ohne Rueckgabedatum sind nie faellig und tauchen in der zweiten Karte nicht auf. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
254 lines
11 KiB
Markdown
254 lines
11 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).
|
||
|
||
### 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 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).
|