Add rate limiting, sliding sessions, email verification, GDPR self-service, order cancellation/returns, and critical-error alerting

Complements Payload's per-account login lockout with per-IP rate limiting
on auth routes; proxy.ts silently refreshes an active customer's session
via Payload's built-in refresh-token endpoint instead of a long-lived
token. Registration now sends a non-blocking email-verification link
(doesn't gate login, since checkout registers and immediately logs in
mid-purchase). /konto/profil gets GDPR export/delete; order detail pages
get self-service cancel/return-request, backed by a Payload hook that
closes a real gap (a customer's JWT could previously PATCH any field of
their own order, not just status). Checkout failures now email an alert
independent of Payload's own health, since Kuma's uptime checks can't see
an order silently failing to persist.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marco
2026-07-22 07:28:01 +00:00
parent 7f37f111e8
commit df05ea5358
22 changed files with 822 additions and 20 deletions
@@ -0,0 +1,53 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
const LABEL = { cancel: "Bestellung stornieren", "request-return": "Rücksendung anfragen" } as const;
const CONFIRM = {
cancel: "Bestellung wirklich stornieren?",
"request-return": "Rücksendung wirklich anfragen? Wir melden uns mit den nächsten Schritten.",
} as const;
export function OrderActionButton({ orderNumber, action }: { orderNumber: string; action: "cancel" | "request-return" }) {
const router = useRouter();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
async function handleClick() {
if (!window.confirm(CONFIRM[action])) return;
setLoading(true);
setError(null);
try {
const res = await fetch(`/api/account/orders/${encodeURIComponent(orderNumber)}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action }),
});
const data = await res.json();
if (!data.ok) {
setError(data.reason || "Aktion war nicht möglich.");
setLoading(false);
return;
}
router.refresh();
} catch {
setError("Aktion war gerade nicht möglich.");
setLoading(false);
}
}
return (
<div className="flex flex-col gap-2 items-start">
<button
type="button"
onClick={handleClick}
disabled={loading}
className={`px-5 py-3 rounded-sm border border-border hover:border-brand font-bold text-body-sm text-text-primary transition-colors ${loading ? "opacity-70 pointer-events-none" : ""}`}
>
{loading ? "…" : LABEL[action]}
</button>
{error && <p className="text-label text-red-600">{error}</p>}
</div>
);
}
@@ -4,7 +4,8 @@ import Link from "next/link";
import { Reveal } from "../../../components/Reveal";
import { Footer } from "../../../components/Footer";
import { formatPrice, formatDate } from "../../../lib/format";
import { getSessionCustomer, getCustomerOrderDetail, ORDER_STATUS_LABEL } from "../../../lib/customerAuth";
import { getSessionCustomer, getCustomerOrderDetail, ORDER_STATUS_LABEL, customerOrderAction } from "../../../lib/customerAuth";
import { OrderActionButton } from "./components/OrderActionButton";
export const metadata: Metadata = {
title: "Bestelldetails",
@@ -23,6 +24,7 @@ export default async function KontoBestellungDetailPage({ params }: { params: Pr
order.deliveryMethod === "address"
? order.street
: `Packstation ${order.packstationNumber} · Postnummer ${order.postNumber}`;
const action = customerOrderAction(order.status);
return (
<>
@@ -104,6 +106,8 @@ export default async function KontoBestellungDetailPage({ params }: { params: Pr
<span className="font-bold text-h-small text-text-primary">{formatPrice(order.total)}</span>
</div>
</div>
{action && <OrderActionButton orderNumber={order.orderNumber} action={action} />}
</Reveal>
</main>
<Footer />