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.
162 lines
8.3 KiB
Markdown
162 lines
8.3 KiB
Markdown
# Canteen Menu – Speisekarte für die Betriebskantine
|
||
|
||
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) · 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`).
|
||
|
||
## Funktionen
|
||
|
||
| Bereich | Funktionen |
|
||
| --- | --- |
|
||
| Ö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 (serverseitig erzwungen).
|
||
|
||
Die gesamte Oberfläche ist auf Deutsch, Englisch und Polnisch verfügbar
|
||
(Sprachumschalter, Cookie-basiert). Gerichte, Essenszeiten, Allergene und
|
||
Zusatzstoffe lassen sich zusätzlich auf Englisch/Polnisch benennen –
|
||
mit optionaler automatischer Übersetzung über DeepL (siehe
|
||
`DEEPL_API_KEY` unten) und jederzeit manuell korrigierbar.
|
||
|
||
## Projektstruktur
|
||
|
||
```
|
||
app/ Next.js-Routen
|
||
page.tsx Öffentliche Speisekarte
|
||
login/ Login (Passwort + Passkey)
|
||
vorschlag/ Öffentliches Rezeptvorschlag-Formular
|
||
intern/ Kantinen-Bereich (Rolle: staff oder admin)
|
||
vorschlaege/ Rezeptvorschläge sichten/übernehmen
|
||
admin/ Verwaltung (Rolle: admin)
|
||
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/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-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
|
||
|
||
Voraussetzung in beiden Varianten: `.env.local` mit den Werten aus
|
||
`.env.example` (liegt lokal bereits vor und ist per `.gitignore` vom
|
||
Repo ausgeschlossen).
|
||
|
||
**Variante A – Docker (nichts vorinstallieren außer Docker):**
|
||
|
||
```bash
|
||
docker compose --env-file .env.local --profile dev up dev
|
||
# http://localhost:3000 (Hot-Reload, Quellcode wird gemountet)
|
||
```
|
||
|
||
**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)
|
||
```
|
||
|
||
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)).
|
||
|
||
## Umgebungsvariablen (Live-Betrieb)
|
||
|
||
| Variable | Bedeutung | Geheim? |
|
||
| --- | --- | --- |
|
||
| `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
|
||
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)
|
||
# PostgreSQL: Port 5433
|
||
```
|
||
|
||
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)
|
||
```
|
||
|
||
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
|
||
|
||
Reines PostgreSQL (kein Supabase, keine Row-Level-Security-Policies) –
|
||
alle Zugriffsrechte werden in den Route-Handlern geprüft (`lib/auth/session.ts`,
|
||
`requireRole`).
|
||
|
||
- 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`
|
||
|
||
Rechte-Modell (in Anwendungscode statt Datenbank-RLS):
|
||
|
||
- Ö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
|
||
|
||
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.
|