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]>
62 lines
2.4 KiB
TypeScript
62 lines
2.4 KiB
TypeScript
import type { Metadata } from "next";
|
|
import { getCurrentCustomer } from "@/lib/auth";
|
|
import { getAddressesForCustomer } from "@/lib/account-data";
|
|
import { getShippingTiers } from "@/lib/shipping";
|
|
import { getBulkDiscountTiers } from "@/lib/bulk-discount";
|
|
import { getLocale } from "@/i18n/locale";
|
|
import { getDictionary } from "@/i18n/get-dictionary";
|
|
import { CheckoutForm, type CheckoutInitialValues } from "./checkout-form";
|
|
import { isPackstationSearchConfigured } from "@/lib/dhl-locations";
|
|
|
|
// Vorausfuellen aus Kundendaten aendert sich pro Nutzer/Anmeldestatus, und
|
|
// Versandstaffeln koennen sich im Admin-Bereich jederzeit aendern —
|
|
// Rendering pro Request statt statischem Build entkoppelt zudem den
|
|
// Docker-Build-Schritt von einer zur Build-Zeit erreichbaren Datenbank.
|
|
export const dynamic = "force-dynamic";
|
|
|
|
export async function generateMetadata(): Promise<Metadata> {
|
|
const t = getDictionary(await getLocale());
|
|
return { title: t.checkoutPage.eyebrow };
|
|
}
|
|
|
|
export default async function CheckoutPage() {
|
|
const [tiers, bulkDiscountTiers] = await Promise.all([getShippingTiers(), getBulkDiscountTiers()]);
|
|
const tierValues = tiers.map((t) => ({
|
|
minQuantity: t.minQuantity,
|
|
maxQuantity: t.maxQuantity,
|
|
priceCents: t.priceCents,
|
|
}));
|
|
|
|
const customer = await getCurrentCustomer();
|
|
if (!customer) return <CheckoutForm
|
|
turnstileSiteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY}
|
|
tiers={tierValues}
|
|
bulkDiscountTiers={bulkDiscountTiers}
|
|
packstationSearchAvailable={isPackstationSearchConfigured()}
|
|
/>;
|
|
|
|
// getAddressesForCustomer sortiert Standardadresse (isDefault) zuerst, sonst
|
|
// die zuletzt angelegte — die erste Adresse in der Liste ist also immer die
|
|
// sinnvollste Vorbelegung fuer den Checkout.
|
|
const addresses = await getAddressesForCustomer(customer.id);
|
|
const defaultAddress = addresses[0];
|
|
|
|
const initialValues: CheckoutInitialValues = {
|
|
firstName: customer.firstName,
|
|
lastName: customer.lastName,
|
|
email: customer.email,
|
|
phone: customer.phone ?? "",
|
|
street: defaultAddress?.street ?? "",
|
|
zip: defaultAddress?.zip ?? "",
|
|
city: defaultAddress?.city ?? "",
|
|
};
|
|
|
|
return <CheckoutForm
|
|
turnstileSiteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY}
|
|
initialValues={initialValues}
|
|
tiers={tierValues}
|
|
bulkDiscountTiers={bulkDiscountTiers}
|
|
packstationSearchAvailable={isPackstationSearchConfigured()}
|
|
/>;
|
|
}
|