288 lines
9.3 KiB
Markdown
288 lines
9.3 KiB
Markdown
# Einkaufsliste
|
||
|
||
Selbst gehostete Einkaufslisten-App für Haushalt, WG oder Verein. Läuft als
|
||
Progressive Web App auf dem Telefon, funktioniert ohne Verbindung weiter und
|
||
gleicht ab, sobald wieder Netz da ist.
|
||
|
||
Keine Werbung, kein Tracking, keine externen Schriften oder Skripte. Alle Daten
|
||
bleiben auf dem eigenen Server.
|
||
|
||
## Funktionen
|
||
|
||
- **Listen teilen** – per Einladung mit Rollen (lesen, bearbeiten, verwalten)
|
||
oder über einen zeitlich begrenzten öffentlichen Link
|
||
- **Offline nutzbar** – Änderungen werden lokal festgehalten und später
|
||
nachgereicht, ohne dass etwas verlorengeht
|
||
- **Nach Märkten und Warengruppen sortiert** – die Reihenfolge lässt sich an
|
||
den Weg durch den Laden anpassen
|
||
- **Strichcodes scannen** – eigener EAN-Decoder, Produktdaten von Open Food
|
||
Facts (Abfrage über den Server, nicht vom Gerät)
|
||
- **Preise erfassen und vergleichen** – je Markt, ohne Personenbezug
|
||
- **Push-Benachrichtigungen** bei Änderungen, gedrosselt auf höchstens eine
|
||
Meldung je Liste alle zwei Stunden
|
||
- **Druckansicht** auf A4, nach Märkten gegliedert
|
||
- **Benutzerverwaltung** mit einstellbaren Fristen für Bereinigung und Archiv
|
||
|
||
## Technik
|
||
|
||
| Schicht | Verwendet |
|
||
|---|---|
|
||
| Backend | Python 3.12, FastAPI, SQLAlchemy 2, Alembic |
|
||
| Datenbank | MariaDB 11.4 |
|
||
| Frontend | ES-Module, kein Framework, kein Bauschritt |
|
||
| Auslieferung | nginx |
|
||
| Betrieb | Docker Compose, drei Container |
|
||
|
||
## Voraussetzungen
|
||
|
||
- Docker mit Compose-Plugin
|
||
- Ein SMTP-Zugang für Bestätigungs- und Einladungsmails
|
||
- Für den öffentlichen Betrieb: ein Reverse Proxy mit TLS (siehe unten)
|
||
|
||
## Installation
|
||
|
||
```bash
|
||
git clone <repository-url> einkaufsapp
|
||
cd einkaufsapp
|
||
cp .env.example .env
|
||
```
|
||
|
||
In der `.env` mindestens setzen:
|
||
|
||
```ini
|
||
DB_PASSWORD=… # eigenes Passwort
|
||
DB_ROOT_PASSWORD=… # anderes Passwort
|
||
ADMIN_EMAIL=du@example.de # das erste Konto
|
||
ADMIN_INITIAL_PASSWORD=… # mind. 12 Zeichen, danach ändern
|
||
PUBLIC_BASE_URL=https://einkauf.example.de
|
||
COOKIE_SECURE=true # false nur beim Testen ohne TLS
|
||
|
||
SMTP_HOST=mail.example.de
|
||
SMTP_PORT=587
|
||
SMTP_SECURITY=starttls
|
||
SMTP_USER=…
|
||
SMTP_PASSWORD=…
|
||
SMTP_FROM=einkauf@example.de
|
||
```
|
||
|
||
Prüfen und starten:
|
||
|
||
```bash
|
||
bash tools/check-env.sh .env
|
||
docker compose up -d
|
||
docker compose logs -f api
|
||
```
|
||
|
||
Beim ersten Start legt die Anwendung das Administratorkonto an und führt die
|
||
Datenbankmigrationen aus. Danach ist sie unter `http://<host>:8080/` erreichbar.
|
||
|
||
**Nach der ersten Anmeldung:** Passwort ändern und `ADMIN_INITIAL_PASSWORD` in
|
||
der `.env` leeren – der Wert wird danach ignoriert, stünde aber weiter im
|
||
Klartext in der Umgebung.
|
||
|
||
### Push-Benachrichtigungen (freiwillig)
|
||
|
||
```bash
|
||
python3 tools/vapid-keys.py
|
||
```
|
||
|
||
Die drei ausgegebenen Zeilen in die `.env` übernehmen – **vorhandene Zeilen
|
||
ersetzen, nicht ergänzen.** Bei doppelten Einträgen nimmt Docker Compose den
|
||
letzten, und das ist oft die leere Vorlagenzeile.
|
||
|
||
```bash
|
||
docker compose up -d
|
||
```
|
||
|
||
## TLS und Reverse Proxy
|
||
|
||
> **Die Anwendung fordert keine Zertifikate an und verwaltet sie nicht.**
|
||
> Sie liefert ausschließlich HTTP auf dem konfigurierten Port aus. Für TLS ist
|
||
> ein vorgeschalteter Reverse Proxy zuständig.
|
||
|
||
Ohne HTTPS fehlen wesentliche Funktionen: Service Worker, Kamerazugriff zum
|
||
Scannen und Push-Benachrichtigungen sind an einen sicheren Kontext gebunden.
|
||
Für lokale Tests gilt `localhost` als sicher, jede andere Adresse nicht.
|
||
|
||
### Variante A: nginx davor
|
||
|
||
```nginx
|
||
server {
|
||
listen 443 ssl http2;
|
||
server_name einkauf.example.de;
|
||
|
||
ssl_certificate /etc/letsencrypt/live/einkauf.example.de/fullchain.pem;
|
||
ssl_certificate_key /etc/letsencrypt/live/einkauf.example.de/privkey.pem;
|
||
|
||
location / {
|
||
proxy_pass http://127.0.0.1:8080;
|
||
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
|
||
# Server-Sent Events: ohne diese beiden Zeilen bekommt die App
|
||
# keine Änderungen mit, die andere gemacht haben.
|
||
proxy_buffering off;
|
||
proxy_read_timeout 3600s;
|
||
}
|
||
}
|
||
```
|
||
|
||
**Wichtig:** Setze am vorgelagerten Proxy **keine eigene
|
||
Content-Security-Policy.** Der mitgelieferte nginx bringt bereits eine mit.
|
||
Zwei Richtlinien werden vom Browser als Schnittmenge angewandt – die Anwendung
|
||
hört dann ohne erkennbaren Fehler auf zu funktionieren.
|
||
|
||
Die übrigen Sicherheitskopfzeilen (HSTS, `X-Frame-Options`,
|
||
`Referrer-Policy`) doppelt zu setzen ist unschädlich.
|
||
|
||
### Variante B: Traefik
|
||
|
||
`docker-compose.override.yml` im Projektverzeichnis:
|
||
|
||
```yaml
|
||
services:
|
||
web:
|
||
ports: [] # kein direkter Port mehr nötig
|
||
networks: [einkaufsapp, traefik]
|
||
labels:
|
||
- "traefik.enable=true"
|
||
- "traefik.docker.network=traefik"
|
||
- "traefik.http.routers.einkauf.rule=Host(`einkauf.example.de`)"
|
||
- "traefik.http.routers.einkauf.entrypoints=websecure"
|
||
- "traefik.http.routers.einkauf.tls.certresolver=le"
|
||
- "traefik.http.services.einkauf.loadbalancer.server.port=80"
|
||
# SSE: Antworten nicht puffern
|
||
- "traefik.http.services.einkauf.loadbalancer.responseforwarding.flushinterval=100ms"
|
||
|
||
networks:
|
||
traefik:
|
||
external: true
|
||
```
|
||
|
||
Traefik setzt `X-Forwarded-*` von sich aus. Auch hier gilt: keine zusätzliche
|
||
CSP über eine Middleware.
|
||
|
||
### Nach dem Einrichten prüfen
|
||
|
||
```bash
|
||
curl -sI https://einkauf.example.de | grep -i content-security-policy
|
||
```
|
||
|
||
Es darf **genau eine** Zeile erscheinen.
|
||
|
||
## Betrieb
|
||
|
||
### Aktualisieren
|
||
|
||
```bash
|
||
cd einkaufsapp
|
||
git pull
|
||
bash tools/check-all.sh # alle Prüfungen
|
||
docker compose up -d --build
|
||
docker compose logs --tail=30 api
|
||
```
|
||
|
||
Migrationen laufen beim Start automatisch. **Vorher sichern** – Migrationen
|
||
lassen sich nicht immer zurücknehmen.
|
||
|
||
### Sichern
|
||
|
||
```bash
|
||
docker compose exec -T db mariadb-dump -u root -p"$DB_ROOT_PASSWORD" \
|
||
--single-transaction --quick --routines --events \
|
||
--default-character-set=utf8mb4 einkaufsapp | gzip > einkaufsapp-$(date +%F).sql.gz
|
||
```
|
||
|
||
Sichere den **Datenbankauszug**, nicht das Volume: Ein Dateisystemabbild einer
|
||
laufenden Datenbank ist nicht zuverlässig wiederherstellbar. `--single-transaction`
|
||
liefert einen konsistenten Stand, ohne die Anwendung anzuhalten.
|
||
|
||
Die `.env` gehört ebenfalls gesichert – aber getrennt und verschlüsselt. Sie
|
||
enthält Datenbankpasswort, SMTP-Zugangsdaten und den privaten VAPID-Schlüssel.
|
||
|
||
Ausführlich: [`docs/betrieb.md`](docs/betrieb.md)
|
||
|
||
### Prüfskripte
|
||
|
||
```bash
|
||
bash tools/check-all.sh
|
||
```
|
||
|
||
Prüft Konfiguration, Berechtigungen aller Endpunkte, Datenbankschema gegen die
|
||
Modelle, nginx-Kopfzeilen, JavaScript-Module, Fristen und den Strichcode-Decoder.
|
||
Rückgabewert 1 bei Problemen – als Git-Hook oder in einer CI verwendbar.
|
||
|
||
### Funktionstest gegen eine laufende Instanz
|
||
|
||
```bash
|
||
bash tools/smoke-test.sh http://localhost:8080 admin@example.de deinpasswort
|
||
```
|
||
|
||
Legt eine Liste an, fügt Einträge hinzu, hakt ab, räumt auf und löscht sich
|
||
selbst wieder. Nützlich nach einem Update.
|
||
|
||
### Entwicklungsmodus
|
||
|
||
```bash
|
||
cp docker-compose.override.yml.example docker-compose.override.yml
|
||
docker compose up -d
|
||
```
|
||
|
||
Bindet den Quelltext ein und startet uvicorn bei Änderungen neu. **Die Datei
|
||
gehört nicht auf den Produktivserver** – dort liefe sonst Code vom Host statt
|
||
aus dem gebauten Abbild.
|
||
|
||
## Rechtliches
|
||
|
||
`web/html/impressum.html` und `web/html/datenschutz.html` sind **Entwürfe** mit
|
||
Platzhaltern. Vor der Veröffentlichung ausfüllen und prüfen lassen – die
|
||
Hinweiskästen im Dokument nennen, worauf zu achten ist.
|
||
|
||
Beide sind aus jeder Ansicht über die Fußzeile erreichbar.
|
||
|
||
## Dokumentation
|
||
|
||
| Datei | Inhalt |
|
||
|---|---|
|
||
| [`doku/`](doku/) | Ausführliche Erklärung des Codes, 17 Notizen mit Diagrammen |
|
||
| [`docs/betrieb.md`](docs/betrieb.md) | Sicherung, Wiederherstellung, Fehlersuche |
|
||
| [`docs/sicherheit.md`](docs/sicherheit.md) | Prüfergebnisse und bekannte Grenzen |
|
||
| [`docs/reverse-proxy.md`](docs/reverse-proxy.md) | Zusammenspiel mit einem vorgelagerten Proxy |
|
||
| [`docs/entwicklung.md`](docs/entwicklung.md) | Entstehung der Anwendung, Phase für Phase |
|
||
| [`db/schema.sql`](db/schema.sql) | Vollständiges Datenbankschema |
|
||
|
||
## Struktur
|
||
|
||
```
|
||
backend/ FastAPI, SQLAlchemy, Alembic-Migrationen
|
||
web/ nginx-Konfiguration und das Frontend
|
||
db/schema.sql Schema als ein Stück SQL
|
||
tools/ Prüfskripte
|
||
docs/ Betriebs- und Sicherheitsdokumentation
|
||
doku/ Code-Dokumentation zum Nachlesen
|
||
```
|
||
|
||
## Entstehung
|
||
|
||
Diese Anwendung ist in Zusammenarbeit mit **Claude (Anthropic)** entstanden.
|
||
Konzept, Anforderungen, fachliche Entscheidungen und das Testen im echten
|
||
Betrieb kamen von mir; der Code wurde im Dialog erarbeitet – einschließlich der
|
||
Fehlersuche, wenn etwas nicht funktionierte.
|
||
|
||
Ich halte diese Angabe für angebracht: Wer den Code liest, soll wissen, wie er
|
||
entstanden ist. Die Verantwortung für das, was hier läuft, liegt trotzdem bei
|
||
mir – geprüft, angepasst und im Alltag erprobt habe ich es selbst. Die Nutzung
|
||
erfolgt ohne jegliche Garantie und auf eigene Verantwortung des jeweiligens
|
||
Anwenders.
|
||
Es wird keine Haftung für Datenverlust oder andere Schäden übernommen.
|
||
|
||
## Lizenz
|
||
|
||
GPL v3 (General Public Licence Version 3)
|
||
|
||
Produktdaten zu Strichcodes stammen von [Open Food Facts](https://openfoodfacts.org)
|
||
und stehen unter der Open Database License (ODbL).
|