v1.5: Docker/Compose/.env.example auf selbst gehostetes Postgres umgestellt

Dockerfile: keine NEXT_PUBLIC_*-Build-Args mehr (Branding-URLs sind jetzt
relativ, kein Anon-Key-Konzept mehr – das Image wird dadurch
umgebungsunabhängig), db/migrate.mjs + scripts/ werden zusätzlich in die
Runner-Stage kopiert (nicht Teil von Next's Standalone-Trace), CMD wendet
Migrationen an und legt bei Bedarf den Admin an, bevor der Server startet.

docker-compose.yml: db-Dienst ist jetzt die primäre Datenbank (kein
Supabase-Dump-Mount mehr), neues Volume branding_data für Logo/Favicon,
Bind-Mount für ZUGANGSDATEN.md.

.env.example: Supabase-Variablen durch DATABASE_URL + optionales
ADMIN_EMAIL ersetzt. package.json: @supabase/ssr und @supabase/supabase-js
entfernt. README.md komplett auf die neue Architektur aktualisiert.
This commit is contained in:
2026-07-18 11:02:16 +02:00
parent a9a9cf7ba4
commit b512093a7e
7 changed files with 105 additions and 243 deletions
+69 -82
View File
@@ -4,8 +4,11 @@ Digitale Speisekarte für eine Firmenkantine (ca. 250 Mitarbeiter) mit
öffentlicher Wochenansicht, Kantinen-Bereich zur Pflege der Gerichte und
Admin-Bereich für Benutzerverwaltung und Branding.
**Stack:** Next.js 16 (App Router, TypeScript, Tailwind CSS) · Supabase
(PostgreSQL, Auth inkl. Passkeys, Storage, Row Level Security)
**Stack:** Next.js 16 (App Router, TypeScript, Tailwind CSS) · selbst
gehostetes PostgreSQL (via `pg`, eigene Migrationen) · eigene Session-Auth
(Passwort via `node:crypto` scrypt, Passkeys via `@simplewebauthn`) ·
DeepL als einzige externe API (optional). Läuft vollständig selbst
gehostet, keine Cloud-Abhängigkeit außer optional DeepL.
Änderungen je Version stehen in [CHANGELOG.md](CHANGELOG.md), Git-Tags
markieren die jeweiligen Versionsstände (`git tag`).
@@ -14,14 +17,14 @@ markieren die jeweiligen Versionsstände (`git tag`).
| Bereich | Funktionen |
| --- | --- |
| Öffentlich (ohne Login) | Wochenplan Mo–Fr mit den Essenszeiten Brotzeit (09:00–10:00), Mittagessen, Mittagessen vegetarisch und Suppe (je 11:30–13:30); Navigation vorherige/nächste Woche/Heute; auf schmalen Bildschirmen umschaltbare Tages-/Wochenansicht (Tag ist Standard); Logo zentriert über der Karte; Allergen-/Zusatzstoff-Kürzel je Gericht, Legende unterhalb der Karte; Sprachumschalter Deutsch/Englisch/Polnisch; Rezeptvorschläge einreichen |
| Kantinen-Team (`/intern`) | Gerichte anlegen/bearbeiten/löschen (inkl. Live-Suche, Mehrfachauswahl zum Batch-Löschen, englischem/polnischem Namen mit automatischer DeepL-Übersetzung) mit Allergenen/Zusatzstoffen je Gericht; Gerichte Tagen/Essenszeiten zuweisen (mehrere pro Zelle) und wieder entfernen; globale Allergen-/Zusatzstofflisten pflegen (ebenfalls mehrsprachig); eingereichte Rezeptvorschläge sichten, übernehmen oder ablehnen; eigenes Konto verwalten (Name, E-Mail, Passwort, Passkeys, Passwort-Anmeldung an/aus) |
| Öffentlich (ohne Login) | Wochenplan Mo–Fr mit frei konfigurierbaren Essenszeiten; Navigation vorherige/nächste Woche/Heute; auf schmalen Bildschirmen umschaltbare Tages-/Wochenansicht (Tag ist Standard); Logo zentriert über der Karte; Allergen-/Zusatzstoff-Kürzel je Gericht, Legende unterhalb der Karte; Sprachumschalter Deutsch/Englisch/Polnisch; Rezeptvorschläge einreichen |
| Kantinen-Team (`/intern`) | Essenszeiten anlegen/bearbeiten/löschen/neu anordnen; Gerichte anlegen/bearbeiten/löschen (inkl. Live-Suche, Mehrfachauswahl zum Batch-Löschen, englischem/polnischem Namen mit automatischer DeepL-Übersetzung) mit Allergenen/Zusatzstoffen je Gericht; Gerichte Tagen/Essenszeiten zuweisen (mehrere pro Zelle) und wieder entfernen; globale Allergen-/Zusatzstofflisten pflegen (ebenfalls mehrsprachig); eingereichte Rezeptvorschläge sichten, übernehmen oder ablehnen; eigenes Konto verwalten (Name, E-Mail, Passwort, Passkeys, Passwort-Anmeldung an/aus) |
| Admin (`/admin`) | Kantinenmitarbeiter anlegen/bearbeiten/löschen (inkl. Passwort-Reset mit Wiederholung und Rollenvergabe); Farbgebung der Speisekarte anpassen; Logo und Favicon hochladen/entfernen |
Anmeldung mit E-Mail + Passwort oder alternativ per **Passkey**
(Registrierung im eigenen Konto nach dem ersten Login). Die
Passwort-Anmeldung kann pro Konto deaktiviert werden, sobald mindestens
ein Passkey registriert ist.
ein Passkey registriert ist (serverseitig erzwungen).
Die gesamte Oberfläche ist auf Deutsch, Englisch und Polnisch verfügbar
(Sprachumschalter, Cookie-basiert). Gerichte, Essenszeiten, Allergene und
@@ -39,18 +42,29 @@ app/ Next.js-Routen
intern/ Kantinen-Bereich (Rolle: staff oder admin)
vorschlaege/ Rezeptvorschläge sichten/übernehmen
admin/ Verwaltung (Rolle: admin)
api/admin/users/ Benutzerverwaltung (serverseitig, Service-Role-Key)
api/translate/ DeepL-Anbindung (serverseitig)
branding/[filename]/ Logo/Favicon ausliefern (öffentlich)
api/auth/ Login, Logout, Passkeys
api/account/ Eigenes Konto (Name, E-Mail, Passwort, Toggle)
api/admin/users/ Benutzerverwaltung (nur Admin)
api/admin/branding/ Logo-/Favicon-Upload (nur Admin)
api/dishes/, api/sections/, api/allergens/, api/additives/,
api/menu-entries/, api/dish-suggestions/, api/settings/
CRUD-Routen für den Kantinen-/Admin-Bereich
api/translate/ DeepL-Anbindung (serverseitig)
api/locale/ Sprach-Cookie setzen
components/ Client-Komponenten (WeekEditor, DishManager, …)
components/i18n/ Sprachkontext, Sprachumschalter
lib/i18n/ Wörterbücher (de/en/pl), Locale-Helfer
lib/supabase/ Supabase-Clients (Browser / Server / Admin)
lib/auth/ Sessions, Passwort-Hashing, WebAuthn-Konfiguration
lib/db/ pg-Pool, Query-Helfer, gemeinsame Ladefunktionen
lib/branding.ts Lokale Datei-Ablage für Logo/Favicon
lib/ Typen, Datums-/Formatierungshelfer
proxy.ts Session-Refresh + Schutz von /intern und /admin
supabase/migrations/ Schema & Seed-Daten (Referenz/Doku)
supabase/local/ Skripte + Dumps für die lokale PostgreSQL-Kopie
scripts/ Hilfsskripte (DB-Migration, lokaler Sync)
proxy.ts Session-Prüfung + Schutz von /intern, /admin, /konto
db/migrations/ Datenbankschema (reines SQL, nummeriert)
db/migrate.mjs Wendet neue Migrationen an (idempotent, bei jedem Start)
scripts/bootstrap-admin.mjs
Legt bei leerer Datenbank automatisch einen
Admin mit Zufallspasswort an (siehe ZUGANGSDATEN.md)
```
## Entwicklung
@@ -66,25 +80,25 @@ docker compose --env-file .env.local --profile dev up dev
# http://localhost:3000 (Hot-Reload, Quellcode wird gemountet)
```
**Variante B – lokal installiertes Node.js:**
**Variante B – lokal installiertes Node.js + PostgreSQL:**
```bash
npm install
npm run dev # http://localhost:3000 (bindet an 0.0.0.0, auch im LAN erreichbar)
```
## Umgebungsvariablen (Live-Betrieb)
In beiden Varianten wird die Datenbank beim Start automatisch auf den
neuesten Stand migriert und – falls noch kein Benutzer existiert – ein
Admin-Konto mit Zufallspasswort angelegt (siehe [Zugangsdaten](#zugangsdaten)).
Beim Webserver/Hoster müssen genau diese drei Variablen hinterlegt werden
(Werte: Supabase-Dashboard → Project Settings → API Keys):
## Umgebungsvariablen (Live-Betrieb)
| Variable | Bedeutung | Geheim? |
| --- | --- | --- |
| `NEXT_PUBLIC_SUPABASE_URL` | URL des Supabase-Projekts, z. B. `https://hvzikwjoyhoakedlywhj.supabase.co` | nein |
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Öffentlicher anon-Key (RLS schützt die Daten) | nein |
| `SUPABASE_SERVICE_ROLE_KEY` | Service-Role-Key für die Benutzerverwaltung | **ja – niemals im Browser/Repo!** |
| `APP_URL` | Öffentliche URL der App, z. B. `https://menu.example.de` (Origin-Check hinter Reverse-Proxy; zugleich Passkey-Domain) | nein |
| `DATABASE_URL` | Verbindung zur PostgreSQL-Datenbank, z. B. `postgres://postgres:<passwort>@db:5432/canteen_menu` | **ja** |
| `APP_URL` | Öffentliche URL der App, z. B. `https://menu.example.de` (Origin-Check hinter Reverse-Proxy; zugleich Passkey-RP-Domain – wird direkt daraus abgeleitet, kein separater Konfigurationsschritt nötig) | nein |
| `SITE_TITLE` | Titel im Browser-Tab und Überschrift ohne Logo, Default `Canteen Menu` | nein |
| `ADMIN_EMAIL` | Optional: E-Mail des automatisch angelegten Admin-Erstnutzers (nur relevant, solange die Datenbank leer ist). Ohne Angabe: `admin@<Hostname aus APP_URL>` | nein |
| `DEEPL_API_KEY` | Optional: aktiviert „Automatisch übersetzen“ für Gerichte/Allergene/Zusatzstoffe (DeepL, kostenloser Free-Tier). Ohne Key funktioniert alles weiter, nur automatisch übersetzen ist deaktiviert | **ja – niemals im Browser/Repo!** |
Build & Start auf dem Server – **mit Docker** (empfohlen, Node.js und
@@ -93,82 +107,55 @@ PostgreSQL müssen nicht installiert sein):
```bash
docker compose --env-file .env.local up -d --build
# App: Port 3000 (hinter Reverse-Proxy mit HTTPS betreiben)
# Lokale DB-Kopie: Port 5433 (Dienst "db", optional)
# PostgreSQL: Port 5433
```
Oder klassisch ohne Docker:
Vor dem ersten Start: eine leere `ZUGANGSDATEN.md` im Projektverzeichnis
anlegen (`touch ZUGANGSDATEN.md`), da diese Datei per Bind-Mount in den
Container eingebunden wird und die automatisch generierten Admin-
Zugangsdaten dort landen.
Oder klassisch ohne Docker (mit lokal erreichbarem PostgreSQL):
```bash
npm ci
npm run build
npm run start # Port 3000, bindet an 0.0.0.0 (hinter Reverse-Proxy betreiben)
npm run start # Port 3000, bindet an 0.0.0.0 (hinter Reverse-Proxy betreiben)
```
### Passkeys für die Produktions-Domain freischalten
Passkeys sind an die Domain gebunden. Aktuell ist `localhost` konfiguriert.
Vor dem Livegang: `APP_URL` auf die öffentliche URL setzen und einmalig
```bash
SUPABASE_ACCESS_TOKEN=<personal-access-token> node scripts/configure-passkeys.mjs
```
ausführen (liest `APP_URL` aus der Umgebung; die URL kann alternativ als
Argument übergeben werden). Ohne lokales Node.js geht es auch über Docker:
```bash
docker run --rm -v .:/app -w /app \
-e SUPABASE_ACCESS_TOKEN=<token> -e APP_URL=https://menu.example.de \
node:24-alpine node scripts/configure-passkeys.mjs
```
Achtung: Bereits registrierte Passkeys der alten Domain verlieren dabei
ihre Gültigkeit und müssen neu registriert werden.
Passkeys sind an die Domain gebunden (RP-ID = Hostname aus `APP_URL`).
Vor dem Livegang `APP_URL` auf die öffentliche URL setzen – ein
zusätzlicher Konfigurationsschritt ist nicht nötig. Bereits registrierte
Passkeys einer vorherigen Domain verlieren dabei ihre Gültigkeit und
müssen neu registriert werden.
## Datenbank
### Supabase (führende Datenbank)
Reines PostgreSQL (kein Supabase, keine Row-Level-Security-Policies) –
alle Zugriffsrechte werden in den Route-Handlern geprüft (`lib/auth/session.ts`,
`requireRole`).
- Projekt-Ref: `hvzikwjoyhoakedlywhj` (Region `eu-central-1`, Frankfurt)
- Schema und Seed-Daten: `supabase/migrations/0001_init.sql` und `0002_seed.sql`
- Ausführen weiterer SQL-Dateien:
`SUPABASE_ACCESS_TOKEN=<token> node scripts/db-apply.mjs <datei.sql>`
- Schema: `db/migrations/*.sql`, nummeriert, wird von `db/migrate.mjs`
automatisch angewendet (idempotent, bei jedem Start des Servers bzw.
Docker-Containers).
- Neue Migration hinzufügen: `db/migrations/000N_beschreibung.sql`
anlegen, wird beim nächsten Start automatisch eingespielt.
- Manuell migrieren: `npm run db:migrate`
Sicherheitsmodell (Row Level Security):
Rechte-Modell (in Anwendungscode statt Datenbank-RLS):
- `anon` darf ausschließlich lesen (Speisekarte, Essenszeiten, Allergene,
Zusatzstoffe, Einstellungen).
- `staff`/`admin` (Rolle im `profiles`-Eintrag) pflegen Gerichte,
Wochenplan und Kennzeichnungen.
- Nur `admin` ändert Einstellungen (Farben/Logo) und verwaltet Benutzer;
das Anlegen/Löschen von Benutzern läuft serverseitig über den
Service-Role-Key (`app/api/admin/users`).
- Storage-Bucket `branding` (Logo): öffentlich lesbar, Schreibzugriff nur Admin.
### Lokale PostgreSQL-Kopie
Die Datenbank `canteen_menu` (Kopie von Schema + Daten aus Supabase, inkl.
Minimal-Stubs für `auth`/`storage`) gibt es in zwei Varianten:
**Per Docker:** Der Dienst `db` aus `docker-compose.yml` spielt die
Skripte aus `supabase/local/` beim ersten Start automatisch ein
(PostgreSQL 17, Host-Port 5433 – 5432 ist meist durch eine native
PostgreSQL-Installation belegt –, Passwort über `LOCAL_PG_PASSWORD`).
**Nativ:** Auf dem Entwicklungsrechner läuft PostgreSQL 17 mit derselben
Datenbank. Neu aufbauen bzw. aktualisieren:
```powershell
$env:SUPABASE_DB_PASSWORD = "<Supabase-DB-Passwort>"
$env:LOCAL_PG_PASSWORD = "<lokales postgres-Passwort>"
powershell -File scripts\sync-local-db.ps1
```
Hinweis: Die lokale Kopie dient als Ablage/Backup und für Auswertungen.
Logins (Passwort-Hashes, Passkeys) verbleiben ausschließlich in Supabase.
- Öffentlich lesbar (kein Login nötig): Speisekarte, Essenszeiten,
Allergene, Zusatzstoffe, Logo/Favicon.
- `staff`/`admin` pflegen Gerichte, Essenszeiten, Wochenplan und
Kennzeichnungen.
- Nur `admin` ändert Einstellungen (Farben/Logo) und verwaltet Benutzer.
- Logo/Favicon liegen als Dateien in `BRANDING_DIR` (Docker-Volume
`branding_data`), öffentlich ausgeliefert über `/branding/[filename]`.
## Zugangsdaten
Die Anmeldedaten (Admin, Demo-Mitarbeiter, Datenbank-Passwörter) stehen in
`ZUGANGSDATEN.md` – diese Datei ist bewusst **nicht** im Repository
(`.gitignore`). Nach dem ersten Login bitte die Startpasswörter ändern.
Beim allerersten Start mit leerer Datenbank legt `scripts/bootstrap-admin.mjs`
automatisch einen Admin-Benutzer mit zufällig generiertem Passwort an und
schreibt die Zugangsdaten nach `ZUGANGSDATEN.md` – diese Datei ist bewusst
**nicht** im Repository (`.gitignore`). Nach dem ersten Login bitte das
Startpasswort ändern.