Skip to content

Customer accounts

Orders were guest-only until this existed: retrieved by an unguessable Stripe session id, with no way back for a buyer who lost the confirmation email. customers and customer_addresses add a second login under /account on the storefront and /api/account/* on the API: sign in, register, order history, an address book, password reset.

A customer session sets req.session.customerId, never adminId, so requireAdmin refuses it exactly like an anonymous request. server/security.test.ts asserts a signed-in customer gets 401 on every admin route.

Registration, login and a password-reset request all answer identically for a known and an unknown email. createCustomer hashes the password before it discovers the email is taken, so the two branches cost about the same, not just look the same.

Orders are only ever linked to an account after the email is verified. Registering creates the account immediately, so a new customer can sign in right away, but claiming past guest orders under that address, and the address book that comes with it, waits for the emailed verification link.

Skipping that gate would let anyone register with a stranger’s email and read their order history and shipping address. It is the sharpest edge in this feature. The same gate applies when a guest checkout completes under an email that already belongs to a verified account: the webhook links it there, never to an unverified one.

Nothing in the cart forces an account. The cart page only offers signing in, and prefills the shipping country from a signed-in buyer’s default address.

Reset and verification tokens follow the staff-invite pattern: only a hash is stored, single-use, and short-lived. An hour for a reset link, since it is a live credential; a day for verification, since it is an onboarding step.

POST /api/account/register, /verify, /session, DELETE /session, /password/forgot, /password/reset, and under /api/account/me the profile, orders, and addresses. Every write carries the session’s CSRF token.