Repository navigation
feat(accounts): optional customer accounts with an auth email hook - #20
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t order Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… production runbook Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… fill a blank profile name Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…onfirmed Supabase sign up is public, so anyone could create the unconfirmed user with a password they know. Confirming made that password work, and a customer who left set password without choosing one let the attacker sign in and see their linked guest orders. confirmEmail now swaps in a throwaway password and ends other sessions as soon as a signup token is spent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… guard the admin password on confirm Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nticator deleteAccount checked only admin_users, while change password and the account AGENTS.md treat a verified TOTP factor as running the store too. Both now use runsTheStore; spec 0012 AC-20 amended to match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… error name on the throw Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ndingOrder The cart choice becomes checkoutCartId and the account lock, order name and profile fills move to checkout/account.ts, so createPendingOrder freezes the cart and nothing else, and its locals are const. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n src/lib/orders The admin badge, the confirmation page and the account pages each kept their own status labels, and four parsers repeated the Postgres int ceiling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… feature The route stays thin and now calls a feature, like every other route; the shared email pieces stay in src/lib/email. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n next path /account/../admin and /account/sign-in/ passed the prefix check and then landed outside /account or on an auth page (spec 0012, AC-4). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dd locks it addToCart skipped getCustomerId() whenever the cookie named a live cart, so a cart that expired before the lock fell back to a guest cart for a signed in customer (spec 0012, AC-7). The add now retries the create branch with the session read outside the transaction. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…bad_payload A signed payload missing user.email or the token hash logged auth_email.unexpected_type, hiding a contract break as a refused type. Spec 0012 AC-8 and the log list amended. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the AC-22 link's form Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A customer session gets a 404 on /admin, Back after sign out shows no account data, and a deleted account's session on another device writes nothing and recreates no row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… closes
The edit dialog's description repeats the address label, so getByText('Gdańsk')
matched twice while the dialog was closing and failed strict mode in CI.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Customers can now make an optional account: they enter an email, confirm it from a link, and then choose a password. Guest checkout stays the default. An account shows past orders (including earlier guest orders with the same email), keeps an address book that fills in checkout, and lets customers change their profile and password or delete the account. Every auth email, for customers and admins, is now sent by our own Send Email Hook route through the existing email transport.
Why
Implements spec 0012, customer accounts (feature 12, Slice 6 in the scope). It reuses the Supabase Auth user pool the admin panel already uses, so there is no new auth vendor and no migration. Sending auth emails from our code also fixes spec 0004's shared reset template problem: the app now picks the customer or admin wording. The options weighed are in rationale.md.
Changes
Sign up, sign in and passwords (
src/features/account/,app/(store)/account/(auth)/)after()with a random throwaway password, so neither the reply nor its timing reveals whether an account exists./account/confirmsendsReferrer-Policy: no-referrerandnoindex(next.config.ts).confirmEmailswaps in a fresh throwaway password and ends other sessions, so a password planted through the public Auth API never survives a confirm (fix from the review,6146a05).next, sign out, forgot password. Sign in errors never say which part was wrong.Send Email Hook (
app/api/auth/send-email/route.ts,src/lib/email/,emails/)standardwebhookspackage) before anything else, accepts onlysignupandrecovery, and picks the admin template from the database (anadmin_usersrow or an authenticator), never fromredirect_to.mailpittransport so local dev, e2e and CI can read every email.supabase/templates/recovery.htmlis gone.Account area (
app/(store)/account/(area)/)src/lib/customer/setup.ts) creates thecustomersrow and links earlier guest orders with the same email. One advisory lock per customer guards setup, merge, addresses and deletion.Cart and checkout
src/lib/cart/merge.ts). A signed in add with no cookie cart lands in the account cart.customer_nameis the profile name, else the delivery full name./checkout/completeoffers "Create an account to track this order" after a paid guest order.Admin and shared code
/admin/ordersand the order page show "Account" or "Guest"./account/*and/checkout/*to refresh the session; catalog pages still never touch Supabase. Signed out visitors on account pages get a 307 to sign in.src/features/admin-auth/tosrc/lib/so both features can use them.Config, CI and docs
supabase/config.toml: sign up and email confirmations on,/account/**redirect URLs, the Send Email Hook.SEND_EMAIL_HOOK_SECRET,MAILPIT_URL, andEMAIL_TRANSPORT=mailpit;SEND_EMAIL_HOOK_URIis read only by the Supabase CLI. CI and Playwright set them.How to test / verify
pnpm test: schemas, password checks and errors, paths andnextsafety, proxy decisions, the hook (signature, types, template choice), the actions and log redaction.pnpm test:db(needspnpm db:start): account setup and order linking, orders, addresses, settings and deletion, sign up from an order, cart merge and signed in checkout.pnpm test:e2e tests/e2e/account tests/e2e/admin/password-reset.spec.ts: every account flow plus the admin reset through the hook, with axe on every account page. Locally, setSEND_EMAIL_HOOK_URIto the port your dev server uses and restart the stack.signUpFromOrderrequest.Risk & rollout
/account/**redirect URLs, the hook URL and secret in Supabase and Vercel, and a Firewall rate limit on/account/*and/checkout/complete. Not deploying yet, so none of it is part of this PR./adminin that browser.hasLiveCartdoes not check ownership).deleteUsercall, capped at 15 seconds.Notes for reviewers
src/lib/email/auth-hook.ts(signature first, template choice),src/features/account/actions/confirm.ts(the planted password swap),src/lib/cart/merge.tsandsrc/lib/customer/setup.ts(lock order and linking), andsrc/features/checkout/actions/start-checkout.ts(the account email and address save).src/features/admin-auth/diffs are just imports, from the helpers moving tosrc/lib/.SEND_EMAIL_HOOK_SECRETinci.ymlis a fixed test value for the CI stack only, like the Stripe webhook secret there.🤖 Generated with Claude Code