# Einkaufsapp – Phase 0 & 1 Backend-Grundgerüst mit Registrierung, Mailverifikation, Login und Administrationsschalter, dazu nginx als einziger Eintrittspunkt. ## Ports Nach außen ist genau **ein** Port offen: `HTTP_PORT` aus der `.env`, voreingestellt `46600`. Dahinter liegt nginx und verteilt: | Pfad | Ziel | |---|---| | `/` | statische Dateien (ab Phase 3 die gebaute PWA) | | `/api/…` | FastAPI | | `/docs`, `/openapi.json` | FastAPI-Dokumentation | | `/healthz`, `/readyz` | Statusprüfungen | Der `api`-Container hat keinen veröffentlichten Port. Wenn du ihn zum Debuggen doch direkt erreichen willst, leg eine `docker-compose.override.yml` an: ```yaml services: api: ports: - "127.0.0.1:8000:8000" ``` Die Bindung an `127.0.0.1` ist wichtig – sonst hängt eine ungeschützte API am öffentlichen Interface. ## Start ```bash cp .env.example .env # DB_PASSWORD, DB_ROOT_PASSWORD und ADMIN_EMAIL setzen docker compose --profile dev up -d --build docker compose ps docker compose logs -f api web ``` Erwartete Log-Ausgabe: ``` [entrypoint] Warte auf Datenbank db:3306 ... [wait_for_db] Datenbank erreichbar. [entrypoint] Migrationen einspielen ... INFO [alembic.runtime.migration] Running upgrade -> 0001 [entrypoint] Starte API. INFO: Application startup complete. ``` ### Ersteinrichtung im Browser 1. `ADMIN_INITIAL_PASSWORD` in der `.env` setzen 2. `docker compose up -d` – **nicht** `restart`: ein Neustart übernimmt keine geänderten Werte aus der `.env`, der Container muss neu angelegt werden 3. `http://:46600/` aufrufen, mit `ADMIN_EMAIL` und dem Startpasswort anmelden 4. Die Oberfläche verlangt sofort ein neues Passwort – erst danach ist alles andere erreichbar 5. `ADMIN_INITIAL_PASSWORD` wieder aus der `.env` entfernen Ohne Startpasswort führt derselbe Weg über „Konto anlegen"; dann brauchst du für die Bestätigungsmail allerdings einen funktionierenden SMTP-Relay oder Mailpit. Oberfläche: `http://:46600/` Interaktive API-Dokumentation: `http://:46600/docs` Abgefangene Mails (nur mit `--profile dev`): `http://localhost:8025` ## Durchklicken ```bash curl -s localhost:46600/readyz # 1. Registrieren curl -s -X POST localhost:46600/api/auth/register \ -H 'Content-Type: application/json' \ -d '{"email":"admin@example.com","password":"ein-langes-testpasswort"}' # 2. Link aus Mailpit (http://localhost:8025) im Browser öffnen # oder: curl -s "localhost:46600/api/auth/verify?token=" # 3. Anmelden – Cookies in eine Datei schreiben curl -s -c cookies.txt -X POST localhost:46600/api/auth/login \ -H 'Content-Type: application/json' \ -d '{"email":"admin@example.com","password":"ein-langes-testpasswort"}' # 4. Eigenes Profil lesen (nur Session-Cookie nötig) curl -s -b cookies.txt localhost:46600/api/auth/me # 5. Schreibender Request braucht zusätzlich den CSRF-Header CSRF=$(grep ea_csrf cookies.txt | awk '{print $7}') curl -s -b cookies.txt -H "X-CSRF-Token: $CSRF" \ -X PUT localhost:46600/api/admin/settings \ -H 'Content-Type: application/json' \ -d '{"allow_self_registration": false}' ``` Schritt 5 ohne den Header muss mit `403 CSRF-Token fehlt oder ungültig` abgewiesen werden – das ist der Test, ob der Schutz greift. ### Mailversand prüfen ```bash # Verbindung zum Relay, ohne etwas zu senden curl -s -b cookies.txt localhost:46600/api/admin/mail/check # Testnachricht curl -s -b cookies.txt -H "X-CSRF-Token: $CSRF" \ -X POST localhost:46600/api/admin/mail/test \ -H 'Content-Type: application/json' \ -d '{"to":"dein-postfach@example.de"}' docker compose logs --tail=20 api ``` ## Was hier absichtlich so ist **Erstes Administratorkonto – zwei Wege.** *Ohne Startpasswort* (`ADMIN_INITIAL_PASSWORD` leer): Du registrierst dich regulär über `POST /api/auth/register`. Stimmt die Adresse mit `ADMIN_EMAIL` überein, wird das Konto zum Administrator. Kein Passwort liegt jemals in der Umgebung – dafür brauchst du für die Bestätigung entweder Mailpit oder ein `UPDATE` in der Datenbank. *Mit Startpasswort*: Setz `ADMIN_INITIAL_PASSWORD` in der `.env`. Beim ersten Start legt die Anwendung das Konto an, bestätigt es sofort und markiert es mit `must_change_password`. Bis das Passwort geändert ist, antworten alle Routen außer `GET /api/auth/me` und `POST /api/auth/password/change` mit `403`. Die Absicherungen sind bewusst eng: Das Konto entsteht nur, wenn die Datenbank noch **gar keinen** Nutzer enthält. Ein vergessener Eintrag in der `.env` kann damit weder ein bestehendes Konto überschreiben noch ein zweites Admin-Konto nachschieben – die Anwendung protokolliert stattdessen eine Warnung. Trotzdem: Nach der ersten Anmeldung gehört der Wert aus der `.env` entfernt. Er steht dort im Klartext, liegt in jedem Backup und ist über `docker inspect einkaufsapp_api` für jeden lesbar, der Zugriff auf den Host hat. Für Docker Secrets gibt es `ADMIN_INITIAL_PASSWORD_FILE`. **Registrierung antwortet immer gleich.** Ob eine Adresse schon existiert, verrät die API nicht – weder über den Statuscode noch über die Laufzeit. Beim Login läuft die Passwortprüfung auch bei unbekanntem Konto gegen einen Dummy-Hash, damit die Antwortzeit konstant bleibt. **Rate Limiting sowohl pro IP als auch pro Konto.** Nur pro IP zu begrenzen hilft nicht gegen verteilte Angriffe auf ein einzelnes Konto; nur pro Konto zu begrenzen macht das Aussperren fremder Nutzer trivial. **Produktivbetrieb ist die Voreinstellung.** Der `api`-Container läuft mit `restart: unless-stopped`, ohne Bind-Mount und ohne `--reload` – es läuft der Code aus dem gebauten Abbild. Zum Entwickeln: ```bash cp docker-compose.override.yml.example docker-compose.override.yml docker compose up -d ``` Compose zieht eine vorhandene `docker-compose.override.yml` automatisch mit heran. Sie bindet `./backend` ein, schaltet `--reload` an und setzt `restart: "no"`, damit ein Startfehler zu einem stehenden Container führt statt zu einer Endlosschleife im Log. Die Datei steht in `.gitignore` und gehört nicht auf den Produktivserver. **Ohne Override wirken Änderungen am Quelltext erst nach einem Neubau:** ```bash docker compose up -d --build ``` **Selbstregistrierung: drei Zustände statt zwei.** `ALLOW_SELF_REGISTRATION` kennt `true`, `false` und `admin`. Bei `true`/`false` ist der Wert fest verdrahtet, die Admin-API antwortet auf Änderungsversuche mit `409` und nennt den Grund. Bei `admin` entscheidet die Datenbankeinstellung und der Administrator darf zur Laufzeit umschalten. `SELF_REGISTRATION_DEFAULT` ist dann der Startwert beim allerersten Start. Die Alternative wäre gewesen, die Umgebungsvariable bei jedem Neustart über die Admin-Einstellung schreiben zu lassen – dann wäre eine Abschaltung nach dem nächsten `docker compose up` still wieder verschwunden. **SMTP-Relay.** `SMTP_SECURITY` ersetzt die vorherigen Flags `SMTP_STARTTLS` und `SMTP_SSL`; erlaubt sind `none`, `starttls` und `ssl`. Beim Start prüft die Anwendung, dass keine Zugangsdaten über eine unverschlüsselte Verbindung gehen, und bricht sonst mit einer klaren Meldung ab. Details zu SPF, DKIM, DMARC und PTR stehen in `docs/mail-zustellbarkeit.md`. **Ein Port statt zwei.** Oberfläche und API teilen sich dieselbe Origin. Damit entfällt CORS vollständig, Cookies verhalten sich vorhersagbar, und der Service Worker in Phase 4 kann API-Antworten ohne Sonderregeln cachen. Zwei Ports hätten drei zusätzliche Fehlerquellen eingeführt, ohne etwas zu gewinnen. **`COOKIE_SECURE` muss zum Zugriffsweg passen.** Bei direktem HTTP-Zugriff auf Port 46600 gehört `false` in die `.env`. Steht ein Reverse Proxy mit TLS davor – bei dir vermutlich OPNsense –, dann `true` und `PUBLIC_BASE_URL` auf `https://…`. Ein `Secure`-Cookie über reines HTTP wird vom Browser verworfen, und die Anmeldung scheitert dann ohne verständliche Fehlermeldung. **CSP ohne `unsafe-inline`.** Die Platzhalterseite lagert deshalb CSS und JavaScript in eigene Dateien aus. Wenn du dort etwas ergänzt: Inline-`