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:
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user