Commit Graph
6 Commits
Author SHA1 Message Date
Nick SzaunigandClaude Opus 5 70ede77dde feat: Rechnungen werden abgelegt, gesichert und mitversendet
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 2m12s
Die Rechnung entstand bisher nur als Datenbankzeile; das PDF wurde bei jedem
Abruf neu gerendert und tauchte in keiner E-Mail auf. Das hatte drei
Nachteile: Gastbestellungen haben kein Konto, in dem sie den Beleg finden
koennten; eine Sicherung der Datenbank sicherte den Beleg nicht mit; und ein
spaeter erneut gerendertes PDF konnte nach einer Aenderung an Firmendaten
oder Layout anders aussehen als das, was die Kundschaft in Haenden hielt.

Ablage jetzt unter INVOICE_DIR, nach Jahr und Kunde sortiert:

  invoices/2026/mustermann-erika/RE-2026-A3F9C1B2.pdf

Das Jahr zuerst, weil sich die Aufbewahrungsfrist danach richtet — ein
abgelaufener Jahrgang laesst sich als ganzes Verzeichnis archivieren. Der
relative Pfad steht als Invoice.pdfPath in der Datenbank, damit eine spaetere
Aenderung an der Pfadbildung keine bereits abgelegten Belege unauffindbar
macht. Dafuer wurde die nie gelesene Spalte "pdfUrl" umbenannt; eine URL war
ohnehin das falsche Konzept.

INVOICE_DIR ist bewusst ein ANDERES Verzeichnis als UPLOAD_DIR:
app/uploads/[...path] liefert jede Datei unterhalb von UPLOAD_DIR ohne
Anmeldung aus. Laegen Rechnungen dort, koennte jede Person, die den Pfad
erraet, fremde Rechnungen samt Anschrift und Bestellinhalt herunterladen.
Auf das Rechnungsverzeichnis zeigt keine oeffentliche Route; der Abruf laeuft
weiter ueber /rechnung/<nummer>/pdf mit Besitzpruefung.

Der Abruf liefert jetzt die abgelegte Datei aus und rendert nur neu, wenn
keine da ist — also bei Rechnungen aus der Zeit davor.

Versand: das PDF haengt an der Bestellbestaetigung. sendMail nimmt dafuer
optionale Anhaenge entgegen, alle zehn bestehenden Aufrufe bleiben
unveraendert. Schlaegt das Erzeugen oder Ablegen fehl, geht die Bestaetigung
trotzdem raus und die Rechnung bleibt abrufbar: ein 500er an Stripe waere
die falsche Antwort auf eine volle Platte, denn die Zahlung ist erfolgt und
ein Wiederholungslauf aenderte daran nichts.

Sicherung: der taegliche CronJob packt das Verzeichnis als
rechnungen-<Zeitstempel>.tar.gz neben den Datenbank-Dump und haengt das
Volume nur lesend ein. Vermerkt ist dort auch die Einschraenkung, dass
wabenhain-invoices als ReadWriteOnce nur von Pods auf demselben Knoten
einhaengbar ist — bei mehreren Knoten braucht es ReadWriteMany, sonst bleibt
der Sicherungs-Pod lautlos in "Pending".

Weiterhin offen und im README vermerkt: die Rechnungsnummer ist eindeutig,
aber nicht fortlaufend (§ 14 Abs. 4 Nr. 4 UStG), und das PDF weist keine
Umsatzsteuer aus.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-17 04:12:44 +02:00
Nick SzaunigandClaude Opus 5 479ace1b88 docs: Konfigurationsvorlagen vollstaendig und uebernahmefaehig machen
01-configmap.yaml und 02-secret.example.yaml sind jetzt so geschrieben, dass
sie sich als Produktivkonfiguration uebernehmen lassen: in drei Abschnitte
gegliedert (ANPASSEN / Vorgaben / optional), jeder Wert mit der Angabe, was
ohne ihn nicht funktioniert. Anzupassen sind genau zwei Werte in der
ConfigMap und die mit "CHANGE-ME" markierten im Secret; alles Uebrige traegt
eine brauchbare Vorgabe oder darf leer bleiben.

Beide Dateien decken zusammen jede Umgebungsvariable ab, die die Anwendung
liest, und jeder Schluessel steht in genau einer der beiden. Bisher war das
Handarbeit und driftete: GOOGLE_PLACES_API_KEY lag in der unverschluesselten
ConfigMap, APICORE_API_URL unnoetig im Secret.

Neu ist deshalb src/lib/k8s-manifest-env.test.ts. Der Test liest die
process.env-Zugriffe unter src/ und die Schluessel der beiden Manifeste und
prueft vier Zusagen: keine gelesene Variable fehlt, kein Schluessel steht
doppelt (sonst entschiede allein die Reihenfolge der envFrom-Eintraege,
welcher Wert gewinnt), kein Schluessel ist tot, und die committete
Secret-Vorlage enthaelt keine ausgefuellten Werte.

Beim ersten Lauf hat der Test zwei Dinge gefunden. ADMIN_EMAIL und
ADMIN_PASSWORD liest nicht die App, sondern prisma/seed.ts — das ist
legitim und steht jetzt begruendet in einer Ausnahmeliste, zusammen mit
DB_PASSWORD fuer das Datenbank-StatefulSet. STRIPE_PUBLISHABLE_KEY dagegen
wird von keiner einzigen Code-Stelle gelesen: der Checkout laeuft
vollstaendig serverseitig ueber eine Stripe-Checkout-Session. Der Schluessel
ist aus Secret-Vorlage, .env.example, docker-compose.yml und README
entfernt, mit Hinweis, wofuer man ihn braeuchte.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-17 03:06:18 +02:00
Nick SzaunigandClaude Opus 5 990a16e654 chore: Manifeste an den laufenden Cluster angleichen
Die Manifeste in k8s/ trugen noch Platzhalter, waehrend im Cluster laengst
die echten Werte stehen. Wer sie unveraendert anwendet, ueberschreibt damit
funktionierende Einstellungen.

- Domain: shop.example.com -> wabenhain.com, in 06-ingress.yaml (host und
  tls.hosts) und in 01-configmap.yaml (NEXT_PUBLIC_SITE_URL). Beide Stellen
  muessen uebereinstimmen; aus NEXT_PUBLIC_SITE_URL entstehen Sitemap,
  Canonical-Tags und jeder Link in jeder Bestellbestaetigung.

- GOOGLE_PLACES_API_KEY von der ConfigMap ins Secret. Das ist ein
  abrechenbarer Zugangsschluessel, und eine ConfigMap ist unverschluesselt
  und in der Cluster-Oberflaeche im Klartext lesbar. Die zugehoerige
  GOOGLE_PLACE_ID ist oeffentlich und bleibt in der ConfigMap. Der Cluster
  handhabt das bereits so.

- APICORE_API_URL vom Secret in die ConfigMap: eine Adresse ist kein
  Geheimnis. Auch das entspricht dem Cluster.

- VIDEO_TRANSCODE_THREADS in die ConfigMap aufgenommen, damit die
  Threadbegrenzung des Video-Transcodes (siehe vorheriger Commit) auch ueber
  das Deployment einstellbar ist.

Gegenprobe: die Vereinigung aller Schluessel aus ConfigMap und Secret deckt
jetzt genau die Umgebungsvariablen ab, die der Code ueber process.env liest
- keine fehlt. Die beiden Ueberhaenge sind beabsichtigt: DB_PASSWORD liest
der Postgres-StatefulSet, STRIPE_PUBLISHABLE_KEY haelt die Konfiguration
vollstaendig fuer eine kuenftige clientseitige Stripe-Einbindung.

Der Hinweistext der Go-Live-Pruefung nannte shop.example.com als
ausgelieferte Vorgabe; das stimmt nicht mehr. Die Pruefung selbst ist
unveraendert.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-17 02:20:34 +02:00
Nick SzaunigandClaude Sonnet 5 54919d82ed feat: Bild-Upload fuer Inhalte, Produkte und Ernten
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]>
2026-09-04 19:59:42 +02:00
Nick Szaunig 4d3100e9ba feat: Security CSRF Fix, Logout optimierung, Footer Anpassung, Rechtstexteanpassung
Docker Image bauen und veroeffentlichen / build-and-push (push) Failing after 1m33s
2026-09-03 13:51:50 +02:00
Nick Szaunig 5889f58da7 feat: Docker-, Kubernetes- und Gitea-Actions-Deployment-Pipeline
Docker Image bauen und veroeffentlichen / build-and-push (push) Canceled after 0s
Dockerfile (Multi-Stage, "output: standalone"), docker-compose.yml mit
Caddy als einzigem nach aussen offenem Reverse-Proxy (verhindert
X-Forwarded-For-Faelschung fuer das App-eigene Rate-Limiting), Gitea
Actions Workflow der bei jedem Push zwei Image-Tags in die Gitea-
Container-Registry baut (schlankes Laufzeit-Image + ein Migrations-
Image mit Prisma-CLI), sowie rohe Kubernetes-Manifeste (Namespace,
ConfigMap, Secret-Vorlage, Postgres-/MySQL-StatefulSet, Migrations-Job,
App-Deployment, Ingress) fuer den Cluster-Betrieb. Siehe k8s/README.md
fuer den vollstaendigen Einrichtungs-/Deployment-Ablauf.
2026-08-17 14:30:46 +02:00