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:
@@ -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 });
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { createOrder } from "../../lib/orderServer";
|
||||
import { getSessionCustomer, registerCustomer, setSessionCookie, type CustomerSummary } from "../../lib/customerAuth";
|
||||
import { fetchProductsBySlug } from "../../lib/productsServer";
|
||||
import { sendCriticalAlert } from "../../lib/alertAdmin";
|
||||
import { sendOrderConfirmationEmail } from "../../lib/orderEmail";
|
||||
|
||||
type CheckoutBody = {
|
||||
cart: CartItem[];
|
||||
@@ -154,6 +155,31 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ ok: false, reason: "Bestellung konnte nicht gespeichert werden." }, { status: 500 });
|
||||
}
|
||||
|
||||
// Fire-and-forget — a failed confirmation email must never undo an
|
||||
// already-successful order or block the response the customer is
|
||||
// waiting on. Lower severity than the "order lost" alert above (the
|
||||
// order itself is safe either way), but still worth knowing about, since
|
||||
// it's the one thing that would otherwise fail completely silently.
|
||||
sendOrderConfirmationEmail(
|
||||
{
|
||||
orderNumber: order.orderNumber,
|
||||
createdAt: order.createdAt,
|
||||
items: items.map((i) => ({ productName: i.productName, quantity: i.quantity, unitPrice: i.unitPrice })),
|
||||
subtotal,
|
||||
shippingCost,
|
||||
discountAmount,
|
||||
discountCode: body.discountCode || null,
|
||||
total,
|
||||
},
|
||||
body.email,
|
||||
).catch((err) => {
|
||||
sendCriticalAlert("Bestätigungs-Mail konnte nicht gesendet werden", {
|
||||
orderNumber: order.orderNumber,
|
||||
customerEmail: body.email,
|
||||
error: String(err),
|
||||
});
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
orderNumber: order.orderNumber,
|
||||
|
||||
Reference in New Issue
Block a user