Add real order persistence, customer accounts, and cart sync

Checkout now persists orders server-side (Payload orders collection,
re-priced from live product data, discount codes redeemed exactly once)
instead of writing a client-only sessionStorage snapshot. Buying requires
an account (registration inline in checkout, no separate step) — accounts
get order history with delivery status, profile/address editing, password
change, and a cart that syncs across devices while logged in.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marco
2026-07-22 06:45:42 +00:00
parent 516945fc8c
commit 7f37f111e8
27 changed files with 1697 additions and 89 deletions
+34
View File
@@ -0,0 +1,34 @@
import { NextResponse } from "next/server";
import type { CartItem } from "../../../lib/cart";
import { getServerCart, getSessionCustomer, saveServerCart } from "../../../lib/customerAuth";
import { fetchProductsBySlug } from "../../../lib/productsServer";
export async function GET() {
const session = await getSessionCustomer();
if (!session) return NextResponse.json({ cart: [] }, { status: 401 });
const cart = await getServerCart(session.token);
return NextResponse.json({ cart });
}
// Called by CartSync.tsx (debounced) on every local cart change while a
// session is active — keeps the server-side mirror current so the cart
// follows the customer across devices. Silently no-ops when logged out;
// the caller doesn't care either way.
export async function POST(request: Request) {
const session = await getSessionCustomer();
if (!session) return NextResponse.json({ ok: false }, { status: 401 });
const body = await request.json().catch(() => null);
const cart: CartItem[] = Array.isArray(body?.cart) ? body.cart : [];
const productsBySlug = await fetchProductsBySlug();
const lines = cart
.map((item) => {
const product = productsBySlug.get(item.id);
return product ? { productId: product.id, productSlug: product.slug, quantity: item.qty } : null;
})
.filter((line): line is { productId: number; productSlug: string; quantity: number } => line !== null);
const ok = await saveServerCart(session.token, session.customer.id, lines);
return NextResponse.json({ ok });
}
+17
View File
@@ -0,0 +1,17 @@
import { NextResponse } from "next/server";
import { loginCustomer, setSessionCookie } from "../../../lib/customerAuth";
export async function POST(request: Request) {
const body = await request.json().catch(() => null);
const email = typeof body?.email === "string" ? body.email : "";
const password = typeof body?.password === "string" ? body.password : "";
if (!email || !password) {
return NextResponse.json({ ok: false, reason: "Bitte E-Mail und Passwort angeben." }, { status: 400 });
}
const result = await loginCustomer({ email, password });
if (!result.ok) return NextResponse.json(result, { status: 401 });
await setSessionCookie(result.token);
return NextResponse.json({ ok: true, customer: result.customer });
}
+7
View File
@@ -0,0 +1,7 @@
import { NextResponse } from "next/server";
import { clearSessionCookie } from "../../../lib/customerAuth";
export async function POST() {
await clearSessionCookie();
return NextResponse.json({ ok: true });
}
+8
View File
@@ -0,0 +1,8 @@
import { NextResponse } from "next/server";
import { getSessionCustomer } from "../../../lib/customerAuth";
export async function GET() {
const session = await getSessionCustomer();
if (!session) return NextResponse.json({ customer: null }, { status: 401 });
return NextResponse.json({ customer: session.customer });
}
+10
View File
@@ -0,0 +1,10 @@
import { NextResponse } from "next/server";
import { getCustomerOrders, getSessionCustomer } from "../../../lib/customerAuth";
export async function GET() {
const session = await getSessionCustomer();
if (!session) return NextResponse.json({ orders: [] }, { status: 401 });
const orders = await getCustomerOrders(session.token, session.customer.id);
return NextResponse.json({ orders });
}
+17
View File
@@ -0,0 +1,17 @@
import { NextResponse } from "next/server";
import { changeCustomerPassword, getSessionCustomer } from "../../../lib/customerAuth";
export async function POST(request: Request) {
const session = await getSessionCustomer();
if (!session) return NextResponse.json({ ok: false, reason: "Bitte zuerst einloggen." }, { status: 401 });
const body = await request.json().catch(() => null);
const currentPassword = typeof body?.currentPassword === "string" ? body.currentPassword : "";
const newPassword = typeof body?.newPassword === "string" ? body.newPassword : "";
if (!currentPassword || !newPassword || newPassword.length < 8) {
return NextResponse.json({ ok: false, reason: "Bitte aktuelles und ein neues Passwort (mind. 8 Zeichen) angeben." }, { status: 400 });
}
const result = await changeCustomerPassword(session.customer.email, currentPassword, newPassword);
return NextResponse.json(result, { status: result.ok ? 200 : 400 });
}
+50
View File
@@ -0,0 +1,50 @@
import { NextResponse } from "next/server";
import { getSessionCustomer, updateCustomerProfile } from "../../../lib/customerAuth";
export async function GET() {
const session = await getSessionCustomer();
if (!session) return NextResponse.json({ profile: null }, { status: 401 });
return NextResponse.json({ profile: session.customer });
}
export async function PATCH(request: Request) {
const session = await getSessionCustomer();
if (!session) return NextResponse.json({ ok: false, reason: "Bitte zuerst einloggen." }, { status: 401 });
const body = await request.json().catch(() => null);
const { firstName, lastName, deliveryMethod, street, packstationNumber, postNumber, zip, city, country } = body ?? {};
if (
typeof firstName !== "string" ||
!firstName ||
typeof lastName !== "string" ||
!lastName ||
(deliveryMethod !== "address" && deliveryMethod !== "packstation") ||
typeof zip !== "string" ||
!zip ||
typeof city !== "string" ||
!city ||
typeof country !== "string" ||
!country
) {
return NextResponse.json({ ok: false, reason: "Bitte alle Pflichtfelder ausfüllen." }, { status: 400 });
}
if (deliveryMethod === "address" && !street) {
return NextResponse.json({ ok: false, reason: "Bitte Straße und Hausnummer angeben." }, { status: 400 });
}
if (deliveryMethod === "packstation" && (!packstationNumber || !postNumber)) {
return NextResponse.json({ ok: false, reason: "Bitte Packstation- und Postnummer angeben." }, { status: 400 });
}
const result = await updateCustomerProfile(session.token, session.customer.id, {
firstName,
lastName,
deliveryMethod,
street,
packstationNumber,
postNumber,
zip,
city,
country,
});
return NextResponse.json(result, { status: result.ok ? 200 : 400 });
}
+25
View File
@@ -0,0 +1,25 @@
import { NextResponse } from "next/server";
import { registerCustomer, setSessionCookie } from "../../../lib/customerAuth";
export async function POST(request: Request) {
const body = await request.json().catch(() => null);
const { firstName, lastName, email, password } = body ?? {};
if (
typeof firstName !== "string" ||
typeof lastName !== "string" ||
typeof email !== "string" ||
typeof password !== "string" ||
!firstName ||
!lastName ||
!email ||
!password
) {
return NextResponse.json({ ok: false, reason: "Bitte alle Felder ausfüllen." }, { status: 400 });
}
const result = await registerCustomer({ firstName, lastName, email, password });
if (!result.ok) return NextResponse.json(result, { status: 400 });
await setSessionCookie(result.token);
return NextResponse.json({ ok: true, customer: result.customer });
}