Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 2m34s
Echter Datei-Upload statt CSS-Platzhaltergrafiken, gespeichert unter UPLOAD_DIR (Laufzeit-Verzeichnis, weiss nichts von seiner tatsaechlichen Speicher-Anbindung — in Produktion ein von der Betreiberin selbst per Kubernetes eingebundenes Volume, z.B. SMB; lokal ein simples ./uploads). - lib/uploads.ts: Kernlogik. Bildformat wird ausschliesslich aus den tatsaechlichen Bilddaten erkannt (Magic-Bytes via sharp), nie aus Client-Dateiname/MIME-Type. Zufaellige, nicht erratbare Dateinamen (Path-Traversal-/Overwrite-Schutz). Re-Encoding validiert Pixel-Daten vollstaendig und entfernt eingebettete Metadaten (EXIF/ICC/XMP) als Nebeneffekt. Content-Type beim Ausliefern kommt ausschliesslich aus einer festen Endung->MIME-Zuordnung, nie aus Dateiinhalt/-namen. - app/api/admin/upload/route.ts: Admin-only Upload-Endpunkt (CSRF- + Auth- + Rate-Limit-Pruefung, max. 8 MB, JPEG/PNG/WebP/AVIF). - app/uploads/[...path]/route.ts: liefert hochgeladene Bilder aus (public/ wird zur Build-Zeit fest ins Image gebacken, kann also keine zur Laufzeit hinzugefuegten Dateien bedienen) — mit Path-Traversal-Schutz und nosniff-Header. - content-registry.ts: neuer "image"-Feldtyp fuer Hero-/Startseite- Grafiken, admin/inhalte hat dafuer ein Upload-Widget. - Produkt-/Ernten-Admin-Formulare: Mehrfach-Bild-Upload fuer Product.images/Harvest.images (beide Felder existierten im Schema, hatten aber bislang keine Admin-Oberflaeche). Oeffentliche Seiten (Shop, Ernten, Chargen-QR-Seite) zeigen jetzt echte Fotos statt Platzhalter, sobald vorhanden. - Kubernetes: nur ein einfacher volumeMount in 05-app-deployment.yaml (Platzhalter-emptyDir, klar als CHANGE-ME markiert) — die eigentliche Volume-Quelle (SMB o.ae.) bindet die Betreiberin selbst ein, wie gewuenscht keine SMB/CSI-spezifischen Manifeste hinzugefuegt. Verifiziert: tsc --noEmit clean, 139/139 Tests gruen (21 neu), pnpm build clean. Upload-/Serving-Pipeline live gegen einen laufenden Server getestet (Auth-/CSRF-Gates, Path-Traversal-Ablehnung, Format-Ablehnung) — ein vollstaendiger authentifizierter Upload end-to-end (braucht echten Admin-Login inkl. 2FA) wurde nicht live durchgespielt, nur per Code-Review verifiziert. Co-Authored-By: Claude Sonnet 5 <[email protected]>
179 lines
8.9 KiB
Bash
179 lines
8.9 KiB
Bash
# Datenbank: "postgresql" (Standard) oder "mysql". Nach einer Aenderung
|
|
# einmal `pnpm db:push` ausfuehren (schreibt prisma/schema.prisma neu und
|
|
# generiert den passenden Treiber-Adapter-Client) — siehe README, Abschnitt
|
|
# "Datenbank-Provider wechseln".
|
|
DATABASE_PROVIDER=postgresql
|
|
|
|
# PostgreSQL (Standard). Fuer docker compose siehe POSTGRES_* unten.
|
|
DATABASE_URL="postgresql://wabenhain:wabenhain@localhost:5432/wabenhain?schema=public"
|
|
# MySQL/MariaDB stattdessen:
|
|
# DATABASE_URL="mysql://wabenhain:wabenhain@localhost:3306/wabenhain"
|
|
|
|
# Postgres-Zugangsdaten fuer docker-compose.yml (Datenbank-Container)
|
|
POSTGRES_USER=wabenhain
|
|
POSTGRES_PASSWORD=change-me
|
|
POSTGRES_DB=wabenhain
|
|
|
|
# Nur noetig, wenn DATABASE_PROVIDER=mysql: Zugangsdaten fuer den
|
|
# db-mysql-Container ("docker compose --profile mysql up -d db-mysql")
|
|
MYSQL_USER=wabenhain
|
|
MYSQL_PASSWORD=change-me
|
|
MYSQL_DATABASE=wabenhain
|
|
|
|
# Stripe (Test-Keys aus dem Stripe-Dashboard, https://dashboard.stripe.com/test/apikeys)
|
|
STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxxxxx
|
|
STRIPE_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxxxxxxxxxxxxxx
|
|
# Webhook-Secret aus `stripe listen` bzw. dem Stripe-Dashboard-Webhook-Endpoint
|
|
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxx
|
|
|
|
# Admin-Zugang: Der interne /admin-Bereich wird NICHT per Basic-Auth
|
|
# geschuetzt, sondern ueber das normale Kundenkonto-Login (/konto) und die
|
|
# Rolle "ADMIN" am Konto (siehe README, Abschnitt "Admin-Zugang"). Diese
|
|
# beiden Werte legt `prisma db seed` als initiales Admin-Konto an, falls es
|
|
# noch nicht existiert. Eine UNVORHERSEHBARE E-Mail waehlen (nicht
|
|
# "admin@..." o.ae.) — sonst koennte sich vor dem ersten Seed-Lauf jemand
|
|
# genau diese Adresse selbst registrieren; prisma/seed.ts befoerdert ein
|
|
# bereits bestehendes Nicht-Admin-Konto zwar nicht mehr automatisch, ein
|
|
# unvorhersehbarer Wert ist trotzdem die robustere erste Verteidigungslinie.
|
|
ADMIN_EMAIL=[email protected]
|
|
ADMIN_PASSWORD=change-me
|
|
|
|
# Geheimer Schluessel zum Signieren der Kundenkonto-JWTs (HS256).
|
|
# In Produktion MINDESTENS 32 Zeichen (wird beim Start erzwungen).
|
|
# Erzeugen z.B. mit: openssl rand -base64 32
|
|
SESSION_SECRET=change-me-to-a-long-random-string
|
|
|
|
# Basis-URL der Seite (fuer absolute Links, z.B. in E-Mails/Sitemaps).
|
|
# Lokale Entwicklung (pnpm dev): http://localhost:3000 (Standard hier).
|
|
# Vollstaendiger Docker-Betrieb: http://localhost (Caddy laeuft auf Port 80,
|
|
# siehe SITE_ADDRESS/Caddyfile unten) bzw. die echte Domain in Produktion.
|
|
NEXT_PUBLIC_SITE_URL=http://localhost:3000
|
|
|
|
# Nur fuer den vollstaendigen Docker-Betrieb (docker-compose.yml, Caddyfile):
|
|
# die oeffentliche Domain, unter der die Seite erreichbar sein soll. Leer
|
|
# gelassen laeuft Caddy lokal per HTTP auf Port 80 ohne TLS — fuer eine
|
|
# echte Domain mit automatischem HTTPS z.B. "wabenhain.com" eintragen.
|
|
SITE_ADDRESS=
|
|
|
|
# Google Rezensionen (optional). Ohne diese beiden Werte zeigt die Startseite
|
|
# automatisch die statischen Beispiel-Stimmen als Fallback.
|
|
# API-Key: Google-Cloud-Projekt mit aktivierter "Places API".
|
|
# Place-ID: https://developers.google.com/maps/documentation/places/web-service/place-id
|
|
GOOGLE_PLACES_API_KEY=
|
|
GOOGLE_PLACE_ID=
|
|
|
|
# Cloudflare Turnstile (optional). Ohne diese beiden Werte wird auf
|
|
# Registrierung, Newsletter und Firmenanfrage kein CAPTCHA angezeigt und die
|
|
# serverseitige Pruefung uebersprungen. Site-Key ist oeffentlich (NEXT_PUBLIC_),
|
|
# Secret-Key bleibt privat. Einrichtung: https://dash.cloudflare.com/?to=/:account/turnstile
|
|
TURNSTILE_SECRET_KEY=
|
|
NEXT_PUBLIC_TURNSTILE_SITE_KEY=
|
|
|
|
# SMTP fuer Transaktions-E-Mails (Bestellbestaetigung, Newsletter-Opt-in,
|
|
# Passwort-Reset). Ohne diese drei Werte werden E-Mails nur geloggt statt
|
|
# versendet — praktisch fuer lokale Entwicklung ohne Mail-Setup.
|
|
# Funktioniert mit jedem SMTP-Anbieter (z.B. das E-Mail-Postfach eures
|
|
# Domain-Hosters wie IONOS/Strato, oder ein Dienst wie Resend/Postmark im
|
|
# SMTP-Modus).
|
|
SMTP_HOST=
|
|
SMTP_PORT=587
|
|
SMTP_USER=
|
|
SMTP_PASSWORD=
|
|
SMTP_FROM="Wabenhain <[email protected]>"
|
|
|
|
# ApiCore-Ernten-Synchronisierung (optional, admin/ernten -> "ApiCore-
|
|
# Synchronisierung"). Ohne diese drei Werte zeigt der Sync-Button einen
|
|
# Konfigurationsfehler statt eines Ergebnisses. ApiCore ist ein separates
|
|
# Projekt (NestJS-Backend) mit reinem E-Mail/Passwort-Login (kein API-Key-
|
|
# Mechanismus) — hier IDEALERWEISE die Zugangsdaten eines dedizierten,
|
|
# MFA-freien Service-Kontos mit ausschliesslich den Berechtigungen
|
|
# "harvests:read", "apiaries:read" (optional, fuer Standortnamen),
|
|
# "extractions:read" und "bottling:read" (beide fuer den Chargen-Sync)
|
|
# eintragen, nicht die eines echten Admin-Zugangs. Details siehe
|
|
# src/lib/apicore-client.ts.
|
|
APICORE_API_URL=
|
|
APICORE_SYNC_EMAIL=
|
|
APICORE_SYNC_PASSWORD=
|
|
|
|
# DHL Parcel DE Shipping API ("Post & Parcel Germany", developer.dhl.com) —
|
|
# ermoeglicht das Erstellen von DHL-Versandmarken per Knopfdruck im
|
|
# Admin-Bestelldetail (siehe src/lib/dhl.ts). Ohne diese fuenf Werte zeigt
|
|
# der Button dort nur den Hinweis "DHL ist noch nicht konfiguriert".
|
|
#
|
|
# So kommt ihr an die Werte:
|
|
# 1. Account anlegen auf https://developer.dhl.com und eine App registrieren,
|
|
# die die API "Parcel DE Shipping" (Post & Parcel Germany) einbindet.
|
|
# Die App liefert DHL_API_KEY (Consumer Key) und DHL_API_SECRET
|
|
# (Consumer Secret). Fuer den Sandbox-Test reichen diese beiden Werte
|
|
# plus die DHL-Testzugangsdaten unten.
|
|
# 2. Fuer den PRODUKTIV-Betrieb zusaetzlich einen DHL-Geschaeftskundenvertrag
|
|
# ("DHL Paket" Versandvertrag) abschliessen — Ansprechpartner ist der
|
|
# DHL-Vertrieb bzw. https://www.dhl.de/de/geschaeftskunden/paket.html.
|
|
# Darueber bekommt ihr:
|
|
# - Zugangsdaten zum Geschaeftskundenportal (GKP) -> DHL_GKP_USERNAME/
|
|
# DHL_GKP_PASSWORD (dieselben Zugangsdaten, mit denen ihr euch auf
|
|
# dhl.de/geschaeftskundenportal einloggt).
|
|
# - Die EKP (10-stellige Kundennummer) plus Verfahren/Teilnahme fuer
|
|
# das gebuchte Produkt (Standard-Inlandspaket V01PAK) -> zusammen
|
|
# ergibt das die 14-stellige DHL_ACCOUNT_NUMBER (billingNumber).
|
|
# 3. DHL_ENVIRONMENT auf "sandbox" lassen, bis alles funktioniert, dann auf
|
|
# "production" umstellen (unterschiedliche Basis-URLs, siehe lib/dhl.ts).
|
|
#
|
|
# DHL-Sandbox-Testzugangsdaten (Stand der Recherche, koennen sich aendern —
|
|
# im Zweifel im DHL-Developer-Portal unter der App nachschauen):
|
|
# DHL_GKP_USERNAME=user-valid
|
|
# DHL_GKP_PASSWORD=SandboxPasswort2023!
|
|
DHL_API_KEY=
|
|
DHL_API_SECRET=
|
|
DHL_GKP_USERNAME=
|
|
DHL_GKP_PASSWORD=
|
|
# 14-stellig: EKP (10-stellig) + Verfahren (2-stellig) + Teilnahme (2-stellig)
|
|
DHL_ACCOUNT_NUMBER=
|
|
DHL_ENVIRONMENT=sandbox
|
|
|
|
# Microsoft Entra ID SSO ("Mit Microsoft anmelden", siehe
|
|
# src/app/api/auth/entra/). Ohne diese vier Werte zeigt der Button auf
|
|
# /konto einen Hinweis, dass SSO noch nicht konfiguriert ist, statt
|
|
# abzustuerzen.
|
|
#
|
|
# So richtet ihr es ein (Owner-Aufgabe im Azure-Portal, https://entra.microsoft.com):
|
|
# 1. Entra ID -> App registrations -> "New registration".
|
|
# - Name frei waehlbar, z.B. "Wabenhain Kundenlogin".
|
|
# - "Supported account types": i.d.R. "Accounts in this organizational
|
|
# directory only" (Single Tenant) fuer eine reine Firmen-SSO-Anbindung.
|
|
# - Redirect URI: Plattform "Web", URL = <eure Domain>/api/auth/entra/callback
|
|
# (lokal: http://localhost:3000/api/auth/entra/callback).
|
|
# 2. Nach dem Erstellen: "Application (client) ID" -> ENTRA_CLIENT_ID,
|
|
# "Directory (tenant) ID" -> ENTRA_TENANT_ID (beide auf der Overview-Seite
|
|
# der App-Registrierung sichtbar).
|
|
# 3. "Certificates & secrets" -> "New client secret" erzeugen -> der
|
|
# angezeigte WERT (nicht die Secret-ID!) -> ENTRA_CLIENT_SECRET. Nur
|
|
# einmal sichtbar, bei Verlust ein neues Secret erzeugen.
|
|
# 4. ENTRA_REDIRECT_URI = exakt dieselbe URL wie die in Schritt 1 hinterlegte
|
|
# Redirect-URI (muss zeichengenau uebereinstimmen).
|
|
# 5. "Enterprise applications" in Entra ID zeigt die App danach automatisch
|
|
# als Unternehmensapplikation (wird beim App-Registration-Schritt
|
|
# automatisch mit angelegt) — dort laesst sich optional der Nutzerkreis
|
|
# (z.B. nur bestimmte Gruppen) einschraenken.
|
|
ENTRA_TENANT_ID=
|
|
ENTRA_CLIENT_ID=
|
|
ENTRA_CLIENT_SECRET=
|
|
ENTRA_REDIRECT_URI=http://localhost:3000/api/auth/entra/callback
|
|
|
|
# Verzeichnis fuer hochgeladene Bilder (Inhalte-Bild in admin/inhalte,
|
|
# Produkt-/Erntenfotos in admin/produkte bzw. admin/ernten — siehe
|
|
# app/api/admin/upload/route.ts und app/uploads/[...path]/route.ts). Die App
|
|
# liest/schreibt hier ganz normal per node:fs; sie weiss nichts davon, WAS
|
|
# dieses Verzeichnis in Produktion ist (siehe unten).
|
|
#
|
|
# Lokal reicht der Default "./uploads" (relativ zum Projektverzeichnis, per
|
|
# .gitignore ausgeschlossen — beim ersten Upload automatisch angelegt).
|
|
#
|
|
# In Produktion (Kubernetes, siehe k8s/05-app-deployment.yaml) zeigt
|
|
# UPLOAD_DIR stattdessen auf einen Volume-Mount, damit hochgeladene Bilder
|
|
# einen Pod-Neustart/-Redeploy ueberleben — welche Art Volume das konkret
|
|
# ist (SMB/NFS/hostPath/...), entscheidet die Betreiberin beim Anlegen des
|
|
# PersistentVolume; UPLOAD_DIR muss nur exakt auf den gewaehlten mountPath
|
|
# zeigen.
|
|
UPLOAD_DIR=./uploads
|