Add password reset, order confirmation email with editable templates, and fix missing account entry points

Password reset uses Payload's built-in forgot/reset-password flow,
customized to link to this app instead of the Payload admin. Order
confirmation email and the password-reset email's wording both come from
a new Payload email-templates collection, editable without a deploy and
previewable via Live Preview at /email-preview/[type] (same mechanism as
Posts/LegalPages/Testimonials, sample data instead of a real document).

Also: order numbers get a random suffix (prevents guessing, motivated by
a considered-and-deferred guest order-lookup feature); the discount code
field only shows in the cart when a code is actually active (codes now
apply via a ?code= link instead of manual entry); and three navigation
gaps found while testing — no reachable login link with an empty cart, no
logout link anywhere, no way back from profile to order history.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marco
2026-07-22 08:14:08 +00:00
parent adca6e0f64
commit f0df359db4
26 changed files with 865 additions and 81 deletions
+19
View File
@@ -0,0 +1,19 @@
import { NextResponse } from "next/server";
import { requestPasswordReset } from "../../../lib/customerAuth";
import { checkRateLimit, getClientIp } from "../../../lib/rateLimit";
// Always responds the same way regardless of whether the email exists —
// see requestPasswordReset()'s own comment. Rate-limited a bit tighter
// than login/register (5/15min) since there's no secondary defense here
// the way Payload's own per-account lockout backs up the login route.
export async function POST(request: Request) {
if (!checkRateLimit(`forgot-password:${getClientIp(request)}`, { limit: 5, windowMs: 15 * 60 * 1000 })) {
return NextResponse.json({ ok: true });
}
const body = await request.json().catch(() => null);
const email = typeof body?.email === "string" ? body.email : "";
if (email) await requestPasswordReset(email);
return NextResponse.json({ ok: true });
}
+22
View File
@@ -0,0 +1,22 @@
import { NextResponse } from "next/server";
import { resetPassword, setSessionCookie } from "../../../lib/customerAuth";
import { checkRateLimit, getClientIp } from "../../../lib/rateLimit";
export async function POST(request: Request) {
if (!checkRateLimit(`reset-password:${getClientIp(request)}`, { limit: 5, windowMs: 15 * 60 * 1000 })) {
return NextResponse.json({ ok: false, reason: "Zu viele Versuche. Bitte in ein paar Minuten erneut probieren." }, { status: 429 });
}
const body = await request.json().catch(() => null);
const token = typeof body?.token === "string" ? body.token : "";
const password = typeof body?.password === "string" ? body.password : "";
if (!token || !password || password.length < 8) {
return NextResponse.json({ ok: false, reason: "Bitte ein neues Passwort (mind. 8 Zeichen) angeben." }, { status: 400 });
}
const result = await resetPassword(token, password);
if (!result.ok) return NextResponse.json(result, { status: 400 });
await setSessionCookie(result.token);
return NextResponse.json({ ok: true, customer: result.customer });
}