florian.wilde 94a2c98252 bootstrap-admin.mjs: Zugangsdaten nicht mehr verlieren, wenn ZUGANGSDATEN.md nicht beschreibbar ist
Der Admin-Benutzer wird immer in der Datenbank angelegt, bevor
versucht wird, die generierten Zugangsdaten in ZUGANGSDATEN.md zu
schreiben. Schlug dieser Schreibvorgang fehl (z. B. EACCES, weil die
per Bind-Mount eingebundene Host-Datei root gehört und der
App-Container als nicht-root läuft), flog bisher eine ungefangene
Exception – das Skript brach ab, BEVOR das Passwort irgendwo
protokolliert wurde. Der Admin existierte dann in der DB, aber niemand
kannte sein Passwort mehr.

appendFileSync() läuft jetzt in einem try/catch; schlägt der
Dateizugriff fehl, werden E-Mail und Passwort stattdessen direkt ins
Log geschrieben, und das Skript beendet sich weiterhin mit Exitcode 0
(die eigentlich kritische Operation – der DB-Insert – ist ja bereits
erfolgreich). Lokal verifiziert: Skript bricht bei einer nicht
beschreibbaren ZUGANGSDATEN.md nicht mehr ab, Zugangsdaten landen im
Log, zweiter Lauf bleibt idempotent.

DEPLOYMENT.md: Schritt 2 ergänzt um "chmod 666 ZUGANGSDATEN.md" (der
eigentliche Auslöser), neue Fehlerbehebung-Einträge für
ECONNREFUSED-Startreihenfolge und diesen EACCES-Fall inkl.
Recovery-Anleitung für einen bereits verwaisten Admin-Account.
2026-07-18 14:44:38 +02:00
2026-07-17 17:13:05 +02:00
2026-07-18 14:29:13 +02:00
2026-07-17 17:13:05 +02:00
2026-07-18 14:29:13 +02:00
2026-07-18 14:29:13 +02:00

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, Git-Tags markieren die jeweiligen Versionsstände (git tag). Schritt-für-Schritt- Anleitung für die Inbetriebnahme auf einem neuen Server: DEPLOYMENT.md.

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; RSS-Feed (/feed.xml, Icon im Kopfbereich) mit einem Eintrag pro Tag zum Abonnieren
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 per Suche Tagen/Essenszeiten zuweisen (mehrere pro Zelle) und wieder entfernen; globale Allergen-/Zusatzstofflisten pflegen (ebenfalls mehrsprachig); eingereichte Rezeptvorschläge sichten, übernehmen oder ablehnen (Anzahl offener Vorschläge als Badge in der Navigation); 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
  feed.xml/              RSS-Feed des Speiseplans (öffentlich)
  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):

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:

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).

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):

docker compose --env-file .env.local up -d --build
# App: Port 3000 (hinter Reverse-Proxy mit HTTPS betreiben)
# PostgreSQL: kein Host-Port, nur intern im Compose-Netzwerk erreichbar

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):

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, selbst gehostetes PostgreSQL (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.

S
Description
No description provided
Readme
465 KiB
Languages
TypeScript 94.3%
JavaScript 2.1%
PLpgSQL 1.6%
CSS 1.1%
Shell 0.5%
Other 0.4%