91f6fef6ea
Customers can now select which items and how many units to return instead of only the whole order. The Gutschrift reflects only the returned quantities, excludes shipping (already delivered), and leaves the original discount untouched — confirmed policy, not an engineering default. Stornorechnung (pre-shipping cancellation) is unaffected and stays a full reversal including shipping.
557 lines
21 KiB
TypeScript
557 lines
21 KiB
TypeScript
import { cookies } from "next/headers";
|
|
import { randomUUID } from "node:crypto";
|
|
import type { CartItem } from "./cart";
|
|
import { sendVerificationEmail } from "./alertAdmin";
|
|
|
|
// Server-only — imported by app/api/account/*/route.ts, app/api/checkout/
|
|
// route.ts, and the /checkout and /konto/* Server Components. Never touch
|
|
// Payload's own auth cookie directly: Payload (payload.mk360.de) and this
|
|
// app (einfach-produktiv.mk360.de) are different origins, so instead this
|
|
// app mints its OWN httpOnly cookie holding the JWT Payload issued, and
|
|
// simply forwards that token as an Authorization header on every
|
|
// subsequent Payload call — no shared-domain cookie config, no CORS setup
|
|
// needed on the Payload side.
|
|
const PAYLOAD_URL = process.env.PAYLOAD_URL || "https://payload.mk360.de";
|
|
const TENANT_SLUG = "einfach-produktiv";
|
|
const SESSION_COOKIE = "ep_customer_token";
|
|
// Reused from the order-creation service call (see orderServer.ts) for
|
|
// the handful of customer-collection operations that legitimately have no
|
|
// customer session of their own yet — the email-verification link click
|
|
// (cold, from an email client) being the main one. Same trust level
|
|
// ("this app's own backend acting on its own behalf"), so a third secret
|
|
// felt like unnecessary sprawl rather than added security.
|
|
const SERVICE_SECRET = process.env.ORDER_SERVICE_SECRET || "";
|
|
|
|
async function resolveTenantId(): Promise<number | null> {
|
|
const params = new URLSearchParams({ "where[slug][equals]": TENANT_SLUG, limit: "1" });
|
|
const res = await fetch(`${PAYLOAD_URL}/api/tenants?${params}`, { cache: "no-store" });
|
|
if (!res.ok) return null;
|
|
const data: { docs?: { id: number }[] } = await res.json();
|
|
return data.docs?.[0]?.id ?? null;
|
|
}
|
|
|
|
export type CustomerSummary = {
|
|
id: number;
|
|
customerNumber: string;
|
|
firstName: string;
|
|
lastName: string;
|
|
email: string;
|
|
emailVerified: boolean;
|
|
};
|
|
|
|
export type AuthResult =
|
|
| { ok: true; token: string; customer: CustomerSummary }
|
|
| { ok: false; reason: string; emailExists?: boolean };
|
|
|
|
// Called from app/api/account/check-email/route.ts — lets the checkout
|
|
// form detect an existing account *before* a submit attempt fails (see
|
|
// CheckoutContent.tsx's email field onBlur), not just after. Service-secret
|
|
// authenticated (same reasoning as the other service calls in this file) —
|
|
// Customers' read access isn't public, and there's no customer session yet
|
|
// at this point either way.
|
|
export async function checkEmailExists(email: string): Promise<boolean> {
|
|
const params = new URLSearchParams({ "where[email][equals]": email, limit: "1" });
|
|
const res = await fetch(`${PAYLOAD_URL}/api/customers?${params}`, {
|
|
headers: { "x-order-service-secret": SERVICE_SECRET },
|
|
cache: "no-store",
|
|
});
|
|
if (!res.ok) return false;
|
|
const data: { docs?: unknown[] } = await res.json();
|
|
return (data.docs?.length ?? 0) > 0;
|
|
}
|
|
|
|
export async function registerCustomer(input: {
|
|
firstName: string;
|
|
lastName: string;
|
|
email: string;
|
|
password: string;
|
|
}): Promise<AuthResult> {
|
|
const tenantId = await resolveTenantId();
|
|
if (tenantId == null) return { ok: false, reason: "Registrierung ist gerade nicht möglich." };
|
|
|
|
const res = await fetch(`${PAYLOAD_URL}/api/customers`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ ...input, tenant: tenantId }),
|
|
});
|
|
if (!res.ok) {
|
|
const data = await res.json().catch(() => null);
|
|
const message: string | undefined = data?.errors?.[0]?.message;
|
|
// Payload's own message for a duplicate unique field is a generic
|
|
// "The following field is invalid: email" — not distinguishable from
|
|
// any other email-field validation failure by content alone, but at
|
|
// this point the client's own type="email" + required already ruled
|
|
// out a malformed/missing address, so "email" being the flagged field
|
|
// here in practice only ever means one thing: this address is already
|
|
// registered. emailExists lets the checkout UI react to that
|
|
// specifically (switch to the login toggle) instead of just showing
|
|
// an error the customer has no clear next step for.
|
|
const emailExists = Boolean(message?.toLowerCase().includes("email"));
|
|
return {
|
|
ok: false,
|
|
reason: emailExists
|
|
? "Diese E-Mail-Adresse ist bereits registriert. Bitte logge dich stattdessen ein."
|
|
: (message ?? "Registrierung ist gerade nicht möglich."),
|
|
emailExists,
|
|
};
|
|
}
|
|
|
|
return loginCustomer(input);
|
|
}
|
|
|
|
export async function loginCustomer(input: { email: string; password: string }): Promise<AuthResult> {
|
|
const res = await fetch(`${PAYLOAD_URL}/api/customers/login`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(input),
|
|
});
|
|
if (!res.ok) return { ok: false, reason: "E-Mail-Adresse oder Passwort ist falsch." };
|
|
|
|
const data: {
|
|
token: string;
|
|
user: { id: number; customerNumber: string; firstName: string; lastName: string; email: string; emailVerified: boolean };
|
|
} = await res.json();
|
|
return {
|
|
ok: true,
|
|
token: data.token,
|
|
customer: {
|
|
id: data.user.id,
|
|
customerNumber: data.user.customerNumber,
|
|
firstName: data.user.firstName,
|
|
lastName: data.user.lastName,
|
|
email: data.user.email,
|
|
emailVerified: data.user.emailVerified,
|
|
},
|
|
};
|
|
}
|
|
|
|
// Always resolves — never throws or returns a distinguishable "email not
|
|
// found" shape. Mirrors Payload's own forgot-password operation, which
|
|
// fails silently on a non-existent email specifically to avoid leaking
|
|
// which addresses are registered (see auth/operations/forgotPassword.js);
|
|
// the caller (app/api/account/forgot-password/route.ts) must preserve that
|
|
// by always responding the same way regardless of this call's outcome.
|
|
export async function requestPasswordReset(email: string): Promise<void> {
|
|
await fetch(`${PAYLOAD_URL}/api/customers/forgot-password`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ email }),
|
|
}).catch(() => {
|
|
// Best-effort — same "never reveal anything" reasoning as above.
|
|
});
|
|
}
|
|
|
|
// Payload's reset-password operation logs the customer in on success (see
|
|
// auth/operations/resetPassword.js) and returns the same {token, user}
|
|
// shape as login — reused here so the frontend route can set the session
|
|
// cookie immediately, no separate login step needed after a reset.
|
|
export async function resetPassword(token: string, newPassword: string): Promise<AuthResult> {
|
|
const res = await fetch(`${PAYLOAD_URL}/api/customers/reset-password`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ token, password: newPassword }),
|
|
});
|
|
if (!res.ok) return { ok: false, reason: "Der Link ist ungültig oder abgelaufen." };
|
|
|
|
const data: {
|
|
token: string;
|
|
user: { id: number; customerNumber: string; firstName: string; lastName: string; email: string; emailVerified: boolean };
|
|
} = await res.json();
|
|
return {
|
|
ok: true,
|
|
token: data.token,
|
|
customer: {
|
|
id: data.user.id,
|
|
customerNumber: data.user.customerNumber,
|
|
firstName: data.user.firstName,
|
|
lastName: data.user.lastName,
|
|
email: data.user.email,
|
|
emailVerified: data.user.emailVerified,
|
|
},
|
|
};
|
|
}
|
|
|
|
export async function getCustomerFromToken(token: string): Promise<CustomerSummary | null> {
|
|
const res = await fetch(`${PAYLOAD_URL}/api/customers/me`, {
|
|
headers: { Authorization: `JWT ${token}` },
|
|
cache: "no-store",
|
|
});
|
|
if (!res.ok) return null;
|
|
const data: {
|
|
user: { id: number; customerNumber: string; firstName: string; lastName: string; email: string; emailVerified: boolean } | null;
|
|
} = await res.json();
|
|
if (!data.user) return null;
|
|
return {
|
|
id: data.user.id,
|
|
customerNumber: data.user.customerNumber,
|
|
firstName: data.user.firstName,
|
|
lastName: data.user.lastName,
|
|
email: data.user.email,
|
|
emailVerified: data.user.emailVerified,
|
|
};
|
|
}
|
|
|
|
export type CustomerAddress = {
|
|
deliveryMethod: "address" | "packstation" | null;
|
|
street: string | null;
|
|
packstationNumber: string | null;
|
|
postNumber: string | null;
|
|
zip: string | null;
|
|
city: string | null;
|
|
country: string | null;
|
|
};
|
|
|
|
export type CustomerProfile = CustomerSummary & CustomerAddress;
|
|
|
|
type PayloadCustomerMe = {
|
|
id: number;
|
|
customerNumber: string;
|
|
firstName: string;
|
|
lastName: string;
|
|
email: string;
|
|
emailVerified: boolean;
|
|
deliveryMethod: "address" | "packstation" | null;
|
|
street: string | null;
|
|
packstationNumber: string | null;
|
|
postNumber: string | null;
|
|
zip: string | null;
|
|
city: string | null;
|
|
country: string | null;
|
|
cart: { product: number; productSlug: string; quantity: number }[] | null;
|
|
};
|
|
|
|
export async function getCustomerProfile(token: string): Promise<CustomerProfile | null> {
|
|
const res = await fetch(`${PAYLOAD_URL}/api/customers/me`, {
|
|
headers: { Authorization: `JWT ${token}` },
|
|
cache: "no-store",
|
|
});
|
|
if (!res.ok) return null;
|
|
const data: { user: PayloadCustomerMe | null } = await res.json();
|
|
if (!data.user) return null;
|
|
const u = data.user;
|
|
return {
|
|
id: u.id,
|
|
customerNumber: u.customerNumber,
|
|
firstName: u.firstName,
|
|
lastName: u.lastName,
|
|
email: u.email,
|
|
emailVerified: u.emailVerified,
|
|
deliveryMethod: u.deliveryMethod,
|
|
street: u.street,
|
|
packstationNumber: u.packstationNumber,
|
|
postNumber: u.postNumber,
|
|
zip: u.zip,
|
|
city: u.city,
|
|
country: u.country,
|
|
};
|
|
}
|
|
|
|
export async function updateCustomerProfile(
|
|
token: string,
|
|
customerId: number,
|
|
data: {
|
|
firstName: string;
|
|
lastName: string;
|
|
deliveryMethod: "address" | "packstation";
|
|
street?: string;
|
|
packstationNumber?: string;
|
|
postNumber?: string;
|
|
zip: string;
|
|
city: string;
|
|
country: string;
|
|
},
|
|
): Promise<{ ok: true } | { ok: false; reason: string }> {
|
|
const res = await fetch(`${PAYLOAD_URL}/api/customers/${customerId}`, {
|
|
method: "PATCH",
|
|
headers: { Authorization: `JWT ${token}`, "Content-Type": "application/json" },
|
|
body: JSON.stringify(data),
|
|
});
|
|
if (!res.ok) return { ok: false, reason: "Profil konnte nicht gespeichert werden." };
|
|
return { ok: true };
|
|
}
|
|
|
|
// Verifies the current password by attempting a real login with it (rather
|
|
// than trusting the caller) before changing anything — self-update access
|
|
// alone (see Customers.ts) would let an already-authenticated request set
|
|
// any password without proving it knows the old one.
|
|
export async function changeCustomerPassword(
|
|
email: string,
|
|
currentPassword: string,
|
|
newPassword: string,
|
|
): Promise<{ ok: true } | { ok: false; reason: string }> {
|
|
const verify = await loginCustomer({ email, password: currentPassword });
|
|
if (!verify.ok) return { ok: false, reason: "Aktuelles Passwort ist falsch." };
|
|
|
|
const res = await fetch(`${PAYLOAD_URL}/api/customers/${verify.customer.id}`, {
|
|
method: "PATCH",
|
|
headers: { Authorization: `JWT ${verify.token}`, "Content-Type": "application/json" },
|
|
body: JSON.stringify({ password: newPassword }),
|
|
});
|
|
if (!res.ok) return { ok: false, reason: "Passwort konnte nicht geändert werden." };
|
|
return { ok: true };
|
|
}
|
|
|
|
// Called from app/api/account/verify-email/route.ts — no customer session
|
|
// exists at this point (cold click from an email client), so this
|
|
// authenticates as the service instead (see SERVICE_SECRET above).
|
|
export async function verifyEmailByToken(token: string): Promise<boolean> {
|
|
const params = new URLSearchParams({ "where[emailVerificationToken][equals]": token, limit: "1" });
|
|
const res = await fetch(`${PAYLOAD_URL}/api/customers?${params}`, {
|
|
headers: { "x-order-service-secret": SERVICE_SECRET },
|
|
cache: "no-store",
|
|
});
|
|
if (!res.ok) return false;
|
|
const data: { docs?: { id: number; emailVerificationExpires: string | null }[] } = await res.json();
|
|
const doc = data.docs?.[0];
|
|
if (!doc) return false;
|
|
if (doc.emailVerificationExpires && new Date(doc.emailVerificationExpires).getTime() < Date.now()) return false;
|
|
|
|
const patchRes = await fetch(`${PAYLOAD_URL}/api/customers/${doc.id}`, {
|
|
method: "PATCH",
|
|
headers: { "x-order-service-secret": SERVICE_SECRET, "Content-Type": "application/json" },
|
|
body: JSON.stringify({ emailVerified: true }),
|
|
});
|
|
return patchRes.ok;
|
|
}
|
|
|
|
// Called by an already-logged-in customer (app/api/account/resend-
|
|
// verification/route.ts) — updates the token via their own session (self-
|
|
// update access, see Customers.ts), then sends the mail directly (no
|
|
// Payload afterChange hook to piggyback on for a plain update — that hook
|
|
// only fires on create, see Customers.ts's own comment).
|
|
export async function resendVerificationEmail(session: { token: string; customer: CustomerSummary }): Promise<boolean> {
|
|
const newToken = randomUUID();
|
|
const expires = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
|
|
const res = await fetch(`${PAYLOAD_URL}/api/customers/${session.customer.id}`, {
|
|
method: "PATCH",
|
|
headers: { Authorization: `JWT ${session.token}`, "Content-Type": "application/json" },
|
|
body: JSON.stringify({ emailVerificationToken: newToken, emailVerificationExpires: expires }),
|
|
});
|
|
if (!res.ok) return false;
|
|
await sendVerificationEmail(session.customer.email, session.customer.firstName, newToken);
|
|
return true;
|
|
}
|
|
|
|
// Self-service GDPR deletion (app/api/account/delete/route.ts) — password
|
|
// re-verification happens there via loginCustomer() before this is ever
|
|
// called. orders.customer is ON DELETE SET NULL (see the Payload
|
|
// migration) — past orders keep their own name/address/items snapshot for
|
|
// tax-retention purposes (§147 AO / GDPR Art. 17(3)(b)), only the account
|
|
// itself disappears.
|
|
export async function deleteCustomerAccount(token: string, customerId: number): Promise<boolean> {
|
|
const res = await fetch(`${PAYLOAD_URL}/api/customers/${customerId}`, {
|
|
method: "DELETE",
|
|
headers: { Authorization: `JWT ${token}` },
|
|
});
|
|
return res.ok;
|
|
}
|
|
|
|
export async function getServerCart(token: string): Promise<CartItem[]> {
|
|
const res = await fetch(`${PAYLOAD_URL}/api/customers/me`, {
|
|
headers: { Authorization: `JWT ${token}` },
|
|
cache: "no-store",
|
|
});
|
|
if (!res.ok) return [];
|
|
const data: { user: PayloadCustomerMe | null } = await res.json();
|
|
return (data.user?.cart ?? []).map((line) => ({ id: line.productSlug, qty: line.quantity }));
|
|
}
|
|
|
|
export async function saveServerCart(
|
|
token: string,
|
|
customerId: number,
|
|
cart: { productId: number; productSlug: string; quantity: number }[],
|
|
): Promise<boolean> {
|
|
const res = await fetch(`${PAYLOAD_URL}/api/customers/${customerId}`, {
|
|
method: "PATCH",
|
|
headers: { Authorization: `JWT ${token}`, "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
cart: cart.map((line) => ({ product: line.productId, productSlug: line.productSlug, quantity: line.quantity })),
|
|
}),
|
|
});
|
|
return res.ok;
|
|
}
|
|
|
|
export const ORDER_STATUS_LABEL: Record<string, string> = {
|
|
received: "Eingegangen",
|
|
processing: "In Bearbeitung",
|
|
shipped: "Versandt",
|
|
delivered: "Zugestellt",
|
|
cancelled: "Storniert",
|
|
return_requested: "Rücksendung angefragt",
|
|
returned: "Zurückgesendet",
|
|
};
|
|
|
|
// Which self-service action is available given the order's current
|
|
// status — mirrors CUSTOMER_ALLOWED_TRANSITIONS in Orders.ts exactly
|
|
// (that hook is the real security boundary; this is just so the UI can
|
|
// decide which button, if any, to show).
|
|
export function customerOrderAction(status: string): "cancel" | "request-return" | null {
|
|
if (status === "received") return "cancel";
|
|
if (status === "shipped" || status === "delivered") return "request-return";
|
|
return null;
|
|
}
|
|
|
|
export type CustomerOrder = {
|
|
orderNumber: string;
|
|
createdAt: string;
|
|
total: number;
|
|
status: string;
|
|
itemCount: number;
|
|
};
|
|
|
|
export async function getCustomerOrders(token: string, customerId: number): Promise<CustomerOrder[]> {
|
|
const params = new URLSearchParams({
|
|
"where[customer][equals]": String(customerId),
|
|
sort: "-createdAt",
|
|
depth: "0",
|
|
limit: "50",
|
|
});
|
|
const res = await fetch(`${PAYLOAD_URL}/api/orders?${params}`, {
|
|
headers: { Authorization: `JWT ${token}` },
|
|
cache: "no-store",
|
|
});
|
|
if (!res.ok) return [];
|
|
const data: { docs?: { orderNumber: string; createdAt: string; total: number; status: string; items: unknown[] }[] } =
|
|
await res.json();
|
|
return (data.docs ?? []).map((doc) => ({
|
|
orderNumber: doc.orderNumber,
|
|
createdAt: doc.createdAt,
|
|
total: doc.total,
|
|
status: doc.status,
|
|
itemCount: doc.items.length,
|
|
}));
|
|
}
|
|
|
|
export type CustomerOrderDetail = CustomerOrder & {
|
|
id: number;
|
|
invoiceNumber: string | null;
|
|
invoiceIssuedAt: string | null;
|
|
correctionInvoiceNumber: string | null;
|
|
correctionInvoiceIssuedAt: string | null;
|
|
customerFirstName: string;
|
|
customerLastName: string;
|
|
customerEmail: string;
|
|
deliveryMethod: "address" | "packstation";
|
|
street: string | null;
|
|
packstationNumber: string | null;
|
|
postNumber: string | null;
|
|
zip: string;
|
|
city: string;
|
|
country: string;
|
|
subtotal: number;
|
|
shippingCost: number;
|
|
shippingMethodTitle: string;
|
|
paymentMethodTitle: string;
|
|
discountCode: string | null;
|
|
discountAmount: number;
|
|
returnReason: string | null;
|
|
items: CustomerOrderItem[];
|
|
};
|
|
|
|
export type CustomerOrderItem = {
|
|
product: number;
|
|
productName: string;
|
|
quantity: number;
|
|
unitPrice: number;
|
|
taxRatePercent: number;
|
|
bundleContents: string | null;
|
|
returnQuantity: number;
|
|
};
|
|
|
|
// Access control (Orders.ts) already scopes a customer's own JWT to only
|
|
// their own orders — the where[customer] filter here is redundant with
|
|
// that, kept only so a wrong/foreign orderNumber returns "not found"
|
|
// instead of leaking whether that order number exists for someone else.
|
|
// depth=0 — every field this type reads is already flat; keeping `product`
|
|
// as a plain id (not populated) is what lets requestOrderStatusChange's
|
|
// caller round-trip a full, valid items array back on a return request
|
|
// (Orders.ts's field-lock hook needs every required item field present,
|
|
// not just returnQuantity — see that hook's own comment).
|
|
export async function getCustomerOrderDetail(token: string, customerId: number, orderNumber: string): Promise<CustomerOrderDetail | null> {
|
|
const params = new URLSearchParams({
|
|
"where[orderNumber][equals]": orderNumber,
|
|
"where[customer][equals]": String(customerId),
|
|
depth: "0",
|
|
limit: "1",
|
|
});
|
|
const res = await fetch(`${PAYLOAD_URL}/api/orders?${params}`, {
|
|
headers: { Authorization: `JWT ${token}` },
|
|
cache: "no-store",
|
|
});
|
|
if (!res.ok) return null;
|
|
const data: { docs?: Omit<CustomerOrderDetail, "itemCount">[] } = await res.json();
|
|
const doc = data.docs?.[0];
|
|
if (!doc) return null;
|
|
return { ...doc, itemCount: doc.items.length };
|
|
}
|
|
|
|
// Called from app/api/account/orders/[orderNumber]/route.ts. Security
|
|
// lives in Orders.ts's beforeChange hook (only `status` can change, plus
|
|
// each item's `returnQuantity` alongside a return_requested transition —
|
|
// see that hook's own comment) — this is just the authenticated call; a
|
|
// request the hook rejects comes back as a non-ok response here.
|
|
//
|
|
// `items`, when provided, must be the order's FULL current items array
|
|
// with only `returnQuantity` adjusted on the returned lines — Payload's
|
|
// array field expects every required sub-field present on each row, not
|
|
// a sparse "just the changed key" patch (the field-lock hook's own diff
|
|
// also expects to see the untouched fields, not their absence). The
|
|
// caller (the API route, which already has the order loaded) builds this
|
|
// from getCustomerOrderDetail()'s own `items`.
|
|
export async function requestOrderStatusChange(
|
|
token: string,
|
|
orderId: number,
|
|
action: "cancel" | "request-return",
|
|
extra?: { returnReason?: string; items?: CustomerOrderItem[] },
|
|
): Promise<{ ok: true } | { ok: false; reason: string }> {
|
|
const status = action === "cancel" ? "cancelled" : "return_requested";
|
|
const body: { status: string; returnReason?: string; items?: CustomerOrderItem[] } = { status };
|
|
if (action === "request-return") {
|
|
if (extra?.returnReason) body.returnReason = extra.returnReason;
|
|
if (extra?.items) body.items = extra.items;
|
|
}
|
|
const res = await fetch(`${PAYLOAD_URL}/api/orders/${orderId}`, {
|
|
method: "PATCH",
|
|
headers: { Authorization: `JWT ${token}`, "Content-Type": "application/json" },
|
|
body: JSON.stringify(body),
|
|
});
|
|
if (!res.ok) {
|
|
const data = await res.json().catch(() => null);
|
|
return { ok: false, reason: data?.errors?.[0]?.message ?? "Aktion war nicht möglich." };
|
|
}
|
|
return { ok: true };
|
|
}
|
|
|
|
// Cookie helpers — Next.js's async cookies() API (Next 15+), usable in
|
|
// Route Handlers (read/write) and Server Components (read-only).
|
|
export async function setSessionCookie(token: string) {
|
|
const store = await cookies();
|
|
store.set(SESSION_COOKIE, token, {
|
|
httpOnly: true,
|
|
secure: true,
|
|
sameSite: "lax",
|
|
path: "/",
|
|
maxAge: 60 * 60 * 2, // matches Payload's default JWT lifetime — no refresh flow in this stage
|
|
});
|
|
}
|
|
|
|
export async function clearSessionCookie() {
|
|
const store = await cookies();
|
|
store.delete(SESSION_COOKIE);
|
|
}
|
|
|
|
export async function readSessionToken(): Promise<string | null> {
|
|
const store = await cookies();
|
|
return store.get(SESSION_COOKIE)?.value ?? null;
|
|
}
|
|
|
|
// Convenience for Server Components (checkout page, /konto/*) that just
|
|
// need "who's logged in, if anyone" without touching the cookie API twice.
|
|
export async function getSessionCustomer(): Promise<{ token: string; customer: CustomerSummary } | null> {
|
|
const token = await readSessionToken();
|
|
if (!token) return null;
|
|
const customer = await getCustomerFromToken(token);
|
|
if (!customer) return null;
|
|
return { token, customer };
|
|
}
|