Commit Graph
9 Commits
Author SHA1 Message Date
Nick SzaunigandClaude Opus 5 33580b3290 feat(security): Turnstile schuetzt jetzt alle oeffentlichen Eingabeformulare
Bisher waren nur Registrierung, Newsletter und Firmenanfrage mit einem
CAPTCHA versehen. Damit standen ausgerechnet die Formulare ungeschuetzt
offen, die am meisten hergeben:

- Login und Zwei-Faktor-Eingabe — Durchprobieren erbeuteter Zugangsdaten
  bzw. des sechsstelligen Codes.
- Passwort vergessen/zuruecksetzen — Mailversand an fremde Adressen und ein
  Weg herauszufinden, welche Adressen registriert sind.
- Checkout — automatisierte Bestellungen blockieren Lagerbestand und
  eroeffnen Stripe-Sitzungen, was die Zahlungsstrecke fuer Kartentests
  brauchbar macht.
- Verfuegbarkeits-Benachrichtigung — nimmt eine fremde E-Mail-Adresse
  entgegen und loest spaeter Versand an sie aus.

Die Pruefung sitzt jeweils VOR jeder weiteren Arbeit, damit automatisierte
Versuche Datenbank, Mailversand und Ratenlimit gar nicht erst belasten. Der
Checkout bekommt das Token ueber den JSON-Body statt ueber ein Formularfeld
— er schickt kein Formular ab.

Neu ist isTurnstileValid(formData, ip) als gemeinsame Stelle: haette eine
Seite den Feldnamen anders geschrieben, waere das Token leer angekommen —
und weil ein leeres Token bei nicht konfiguriertem Secret durchgeht, waere
der Schutz dort lautlos ausgefallen.

Genau deshalb ist die Turnstile-Pruefung in "pnpm preflight" jetzt ein
BLOCKER statt einer Warnung: fehlt der Schluessel, schlaegt die Pruefung
nicht fehl, sondern wird still uebersprungen (lib/turnstile.ts). Ohne diesen
Blocker koennte der Shop mit dem Eindruck live gehen, geschuetzt zu sein,
waehrend jedes Formular offensteht.

Bewusst NICHT geschuetzt ist die Sendungsverfolgung: sie ist ein
GET-Formular, dessen Ergebnis verlinkbar und neu ladbar bleiben soll — ein
Token in der Adresszeile waere nach dem ersten Aufruf verbraucht. Sie bleibt
rate-limitiert und verlangt Bestellnummer UND zugehoerige E-Mail-Adresse.
Angemeldete Bereiche (Adressen, Bewertungen, Wunschliste, Retouren) bleiben
ebenfalls ohne CAPTCHA: dort ist die Anmeldung die Huerde, ein zusaetzliches
Raetsel kostete nur Bedienbarkeit.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-17 19:07:12 +02:00
Nick SzaunigandClaude Opus 5 91aee0d609 fix: deaktivierte Produkte sind nicht mehr bestellbar
Ein Produkt, das im Admin-Bereich auf "Inaktiv" gesetzt wird, verschwand
zwar aus der Shop-Uebersicht und aus der Sitemap — seine Detailseite blieb
aber vollstaendig bestellbar. Erreichbar war sie weiterhin ueber
Direktlink, Lesezeichen, alte Newsletter, "Zuletzt angesehen" und den
Google-Index. getProductBySlug filtert isActive nicht, und die Seite
pruefte es nicht nach.

Auch der Checkout selbst pruefte es nicht: getProductsByIds filtert
bewusst nicht danach (es wird auch fuer bereits bestellte, inzwischen
deaktivierte Produkte genutzt, siehe lib/data.ts), und die Route verliess
sich allein auf den Abgleich der Anzahl gefundener Produkte. Die
Warenkorb-Oberflaeche zeigt ein deaktiviertes Produkt ueber
/api/shop/cart-stock zwar mit Bestand 0 an — aber das ist nur der Client.
Ein Warenkorb, der im Browser lag, bevor das Produkt deaktiviert wurde,
kam ungeprueft durch: die Bestellung wurde angelegt und tatsaechlich
bezahlt, obwohl die Imkerei den Artikel gerade aus dem Verkauf genommen
hatte.

Behoben an beiden Stellen:

- Produktseite: isPurchasable = isActive && Bestand > 0 steuert jetzt
  Bestellbutton, Sticky-Leiste und die schema.org-Verfuegbarkeit. Die
  Seite bleibt bewusst erreichbar statt 404 zu liefern — Bestellhistorie
  und Sendungsverfolgung verlinken hierher zum Bewerten, und wer eine
  ausgelaufene Saisonernte gekauft hat, soll sie weiter bewerten koennen.
  Stattdessen "robots: noindex", damit sie nicht ueber Google als
  vermeintlich bestellbares Produkt gefunden wird.
- Die "Benachrichtige mich"-Anmeldung erscheint nur noch bei einem aktiven,
  aber ausverkauften Produkt. Die Benachrichtigung haengt am Lagerbestand
  (lib/stock-notifications.ts) — bei einem dauerhaft eingestellten Produkt
  waere sie ein Versprechen, das nie eingeloest wird.
- Checkout-Route: lehnt Bestellungen mit deaktivierten Produkten
  serverseitig ab, ueber dieselbe Fehlermeldung wie ein nicht mehr
  vorhandenes Produkt.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-15 10:08:15 +02:00
Nick SzaunigandClaude Opus 5 d3020cd4da fix: Sitemap, Canonicals und JSON-LD folgen der echten Domain
Die Adresse der Seite war an zwei Stellen gepflegt: E-Mails nutzten
NEXT_PUBLIC_SITE_URL, alles SEO-Relevante dagegen ein fest verdrahtetes
"https://" + siteConfig.domain (16 Stellen, darunter eine roh eingetippte
"https://wabenhain.com" im Produkt-JSON-LD).

Laeuft der Shop unter einer anderen Domain als wabenhain.com — Staging,
Vorschau-Host, spaeterer Domainwechsel, der im k8s-Ingress hinterlegte Host
— zeigten damit sitemap.xml, robots.txt, saemtliche Canonical-Tags und alle
strukturierten Daten geschlossen auf eine fremde Adresse. Google verwirft
eine Sitemap mit fremden Hosts, und ein Canonical auf eine andere Domain
weist die Indexierung aktiv von der echten Seite weg.

Jetzt eine einzige Quelle: getSiteUrl() in lib/url.ts, mit
siteConfig.domain als Rueckfallebene, damit eine vergessene
Umgebungsvariable nicht "http://localhost:3000" in einer oeffentlichen
Sitemap landen laesst. getBaseUrl() bleibt als Alias fuer den in
E-Mail-Code eingebuergerten Namen und liefert denselben Wert.

robots.ts bekommt dabei "force-dynamic" (wie sitemap.ts es schon hatte):
ohne das rendert Next.js die Datei beim Build statisch vor und backt den
Wert ein, den NEXT_PUBLIC_SITE_URL genau dann hatte. Im Docker-Build ist
die Variable nicht gesetzt — die Sitemap-Zeile waere also dauerhaft auf dem
Rueckfallwert eingefroren. Gegen einen laufenden Produktions-Build
geprueft: robots.txt, sitemap.xml, Canonical-Tags und JSON-LD folgen jetzt
alle dem zur Laufzeit gesetzten Wert.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-15 10:00:59 +02:00
Nick SzaunigandClaude Sonnet 5 6814e9e2c8 feat: Hero-Hintergrundvideo im Admin, ScrollReveal + Bild-Perf auf Unterseiten
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 2m31s
Neues admin-editierbares Hintergrundvideo fuer den Startseiten-Hero
(home.hero.video, MP4/WebM) mit eigenem, magic-byte-validiertem
Upload-Endpunkt (app/api/admin/upload-video) analog zur bestehenden
Bild-Upload-Pipeline. Ausserdem: ScrollReveal-Animation von der
Startseite auf Imkerei, Herkunft, Unternehmen, Ernten, Wissen, Kontakt,
Shop sowie die Produkt-/Ernte-/Chargen-Detailseiten ausgerollt (bisher
nur Startseite), priority auf bislang lazy geladene Above-the-fold-Bilder
(Herkunft/Unternehmen) gesetzt, und passende sizes-Hints auf Grid-Bilder
(Produktkarten, Origin/Regional/Business-Teaser, Ernten-Liste) ergaenzt
statt voller Viewport-Breite.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-11 14:53:14 +02:00
Nick Szaunig a579ebabeb feat: Geschenk- und Honigwissen-Hinweise auf der Produktseite, Umlaut-Suche fixen
- Produktdetailseite: dezenter Hinweis direkt bei der Kaufentscheidung, dass
  beim Checkout eine persoenliche Geschenknachricht hinterlegt werden kann
  (Feature existierte bereits, war aber nur im Checkout-Formular selbst
  sichtbar — zu spaet, um Mengen-/Produktwahl noch zu beeinflussen)
- Produktdetailseite (Honig): kontextuelle Links zu passenden
  Honigwissen-Artikeln (Kristallisation immer, Fruehtracht/Sommertracht
  wenn aus Harvest.type erkennbar) direkt bei den Produktdetails statt
  einer isolierten, von der Kaufseite aus unverlinkten Wissenssektion
- Suche (Shop-Uebersicht + Live-Suchvorschlaege) gleicht jetzt deutsche
  Umlaute/scharfes S gegen ihre ASCII-Transliteration ab (z.B. "Bluete"
  findet "Bluete"-Produkte, "kraeftig" findet "kräftig") — vorher lieferte
  eine Suche in der jeweils anderen Schreibweise keine Treffer, obwohl der
  Katalog durchgaengig echte Umlaute verwendet; Logik in neuer, reiner
  lib/search-match.ts mit eigenen Tests ausgelagert
- Bewusst NICHT angefasst: Newsletter-Rabattanreiz (fehlt, aber Prozentsatz/
  Mechanik ist eine Owner-Entscheidung) und Trust-Badges am Kaufabschluss
  (bereits vorhanden auf Produktseite/Warenkorb/Checkout)

Verifikation: pnpm exec tsc --noEmit clean, pnpm test 151/151 (15 Dateien,
+9 neue Tests fuer search-match), pnpm build erfolgreich.
2026-09-06 03:07:24 +02:00
Nick Szaunig de0f6ee249 fix: Preis-/Datumsformat an Kunden-Locale anpassen, Produktgalerie-LCP verbessern
- format.ts: formatPrice/formatDate/formatDateTime/formatMonthYear nehmen
  jetzt ein optionales Locale-Argument (Default "de" fuer unveraenderte
  Aufrufer wie Rechnungen/Admin) und formatieren fuer "en" als en-GB statt
  hart codiert als de-DE -- englischsprachige Kundschaft sah bisher trotz
  gewaehlter Sprache "9,90 €" und "10. Mai 2026" statt "€9.90"/"10 May 2026".
  Alle kundenseitigen Aufrufer (Produktseite, Warenkorb, Checkout, Bestell-
  verfolgung, Konto-Bestellungen, Ernten-/Chargenseiten, Live-Suchvorschlaege,
  Bestell-/Versand-E-Mails) reichen jetzt die aktive Locale durch.
- en.ts: openCart/shippingWithCount/shippingLabel pluralisieren "item(s)"
  jetzt korrekt (vorher z.B. "Open cart, 1 items").
- product-gallery.tsx: Haupt- und Vorschaubilder nutzen jetzt next/image
  statt <img> (hochgeladene Produktfotos sind immer same-origin unter
  /uploads/..., brauchen also keine remotePatterns-Konfiguration wie der
  entfernte Kommentar behauptete) -- Hauptbild mit priority/responsivem
  sizes, da es auf der Produktseite das wahrscheinlichste LCP-Element ist.
- content-image.tsx: neue optionale priority-Prop; hero.tsx setzt sie fuer
  das Startseiten-Hero-Bild (ebenfalls LCP-Kandidat).
- Sticky-Mobile-CTA-Leiste und useCartStock-Polling gegengeprueft: bereits
  korrekt (pb-28-Platzreservierung bzw. Fetch nur bei Warenkorb-Aenderung
  ohne Intervall) -- keine Aenderung noetig.

Verifiziert: tsc --noEmit clean, pnpm test (142/142 gruen), pnpm build erfolgreich.
2026-09-06 02:57:57 +02:00
Nick Szaunig ac432e261b fix: SEO- und Barrierefreiheits-Luecken auf Kaufentscheidungsseiten schliessen
Konkrete, verifizierte Luecken auf den Seiten, die Kund:innen von Google
aus oder per Link erreichen:

- Product-JSON-LD auf der Produktdetailseite ergaenzt um "image" (absolute
  URLs der hochgeladenen Produktfotos) — fehlte bisher trotz vorhandener
  Bilder und ist fuer Google Rich-Results/Merchant-Listing mit Sternebewertung
  + Preis im Suchergebnis erforderlich.
- Alt-Text fuer echte (hochgeladene) Bilder aussagekraeftiger: ContentImage
  uebernimmt jetzt das bereits vorhandene, bisher nur auf Platzhalterkacheln
  sichtbare "label" (Ernte-Jahr/Typ, Kategorie, Ort) mit in den Alt-Text —
  wirkt projektweit auf Produktkarten, Ernte-Uebersicht/-Detail und
  Produkt-Teaser. Produktgalerie-Hauptbild zeigt zusaetzlich die
  Geschmacksnoten im Alt-Text statt nur des Produktnamens.
- Checkout: allgemeine Fehlermeldung nach fehlgeschlagenem Absenden bekommt
  role="alert" und Fokus wird aktiv dorthin verschoben — bisher rein farblich
  markiert, Screenreader-Nutzer:innen haetten den Fehler beim geldwerten
  Bestellabschluss sonst gar nicht mitbekommen.
- Cart-Drawer und Warenkorb-Seite: Lagerbestand-Hinweise (Ausverkauft/Nur
  noch N) treffen asynchron erst nach dem ersten Rendern ein — jetzt in
  einem persistenten aria-live-Wrapper, damit der Uebergang tatsaechlich
  angesagt wird statt stumm zu bleiben.

Geprueft und bereits solide befunden (keine Aenderung noetig): Shop-Kanonical
ignoriert bewusst Such-/Filter-Query-Parameter (kein Duplicate-Content-Risiko),
Sitemap deckt Produkt- und Ernte-Detailseiten mit echtem lastmod ab,
Produktgalerie-Tastaturbedienung ueber echte <button>-Kacheln mit
aria-label/aria-current, Cart-Drawer-Fokusfalle/Fokus-Rueckgabe, Mengen-Steller-
aria-live, Farbkontrast von text-honey-dark/text-red-700 auf dem Hintergrund
(~4.9:1 bzw. ~6.1:1, beide AA-konform).

tsc/vitest/build bleiben gruen (142 Tests).
2026-09-06 02:46:06 +02:00
Nick Szaunig 0b2196460a feat: Kaufjourney-Optimierungen fuer Kund:innen (Shop, Produktseite, Warenkorb, Checkout)
Konkrete Reibungspunkte entlang der Kauf-Journey behoben:
- Shop-Suche als sichtbares, editierbares Feld im Filter-Formular statt nur
  in der kleinen Kopfzeilen-Suche — Suche/Kategorie/Sortierung/Verfuegbarkeit
  lassen sich jetzt in beide Richtungen kombinieren, ohne sich gegenseitig
  zurueckzusetzen; plus "Suche zuruecksetzen"-Link.
- Versandkosten- und Lieferzeit-Hinweis direkt auf der Produktdetailseite
  (bisher erst im Warenkorb/Checkout sichtbar).
- Produktgalerie per Touch-Wisch bedienbar, nicht nur ueber die
  Vorschau-Kacheln (mobile Kaufentscheidung).
- Immer sichtbare mobile Kaufleiste auf der Produktseite, damit der
  Warenkorb-Button auf langen Seiten (Bewertungen, verwandte Produkte) nicht
  wieder erst erscrollt werden muss.
- Live-Lagerbestand (Ausverkauft/Nur noch N) jetzt auch im Cart-Drawer und
  auf der Warenkorb-Seite sichtbar, nicht nur auf der Produktkarte — neuer
  Endpunkt /api/shop/cart-stock + useCartStock-Hook.
- Checkout- und Adressformulare: autoComplete/inputMode/type auf allen
  Feldern (given-name, tel, postal-code, ...) fuer Mobil-Autofill und
  passende Bildschirmtastaturen.
- Bestellpositionen in "Meine Bestellungen" und der Bestellverfolgung
  verlinken jetzt zur Produktseite (samt Bewertungs-Anker) — bisher war die
  Bewertungsfunktion nach dem Kauf nur durch manuelles Zurueeknavigieren
  auffindbar.

Neue Tests fuer die Lagerbestand-Hinweise im Warenkorb; tsc/vitest/build
bleiben gruen (142 Tests).
2026-09-05 18:02:38 +02:00
Nick Szaunig c7927f2883 feat: Wabenhain Honig-Shop Anwendung
Vollstaendige Next.js/Prisma-E-Commerce-Anwendung fuer eine Imkerei:
Shop, Warenkorb, Stripe-Checkout, Kundenkonto mit 2FA, Admin-Bereich
(Produkte, Ernten, Chargen, Bestellungen, Gutscheine, admin-editierbare
Website-Inhalte), i18n (DE/EN), Rate-Limiting/CSP/Audit-Log-Haertung,
PostgreSQL- und MySQL-Unterstuetzung ueber austauschbare Prisma-
Treiber-Adapter, Unit-/Komponenten-/E2E-Tests.
2026-08-17 14:30:28 +02:00