# Auth-Redirect Dev/Prod Symmetry **Date:** 2026-05-31 **Status:** Proposed (deferred — low priority, dev-only impact) **Owner:** TBD **Effort:** ~1–2h including a local smoke pass ## Motivation Commit `4423f0a5` ("fix(api-client): generalize 401 redirect from `/account/*` to all 4 personas") made `redirectIfUnauthorized()` in `static/shared/js/api-client.js` dispatch a mid-session 401 to the right persona login page with a `?next=` return URL, killing the wrong-state UI flash on token expiry. It is correct and complete **for production**, where every persona is served from its bare path prefix: | Persona | Prod path matched | | --------- | ----------------------------------------- | | customer | `/account/*` (not `/account/login`) | | admin | `/admin/*` (not `/admin/login`) | | merchant | `/merchants/*` (not `/merchants/login`) | | store | `/store/{code}/*` (not `/store/{code}/login`) | A follow-up verification (2026-05-31) confirmed there is **no production gap**: the only authenticated storefront pages (`/account/loyalty`, `/account/loyalty/history`) fall under the `/account/` branch, and the one uncovered storefront page (`/loyalty/join`) makes exclusively public, no-auth API calls (`/storefront/loyalty/program`, `/storefront/loyalty/enroll`, `/storefront/cms/pages/{slug}`) that cannot return 401. The remaining gap is **dev/path-based mode only**. ## The gap In dev/localhost, personas are served behind a `/platforms/{...}/` prefix instead of a subdomain (see `app/core/frontend_detector.py` and `app/modules/core/utils/page_context.py`): - tenancy store → `/platforms/{platform}/store/{code}/...` - storefront → `/platforms/{code}/storefront/{slug}/...` (e.g. the dashboard at `/platforms/loyalty/storefront/FASHIONHUB/account/loyalty`) `redirectIfUnauthorized()` matches only the bare prefixes (`/store/`, `/account/`, …), so a path-based-mode URL falls through to `return false` → the caller throws → the same wrong-state flash the commit fixed still happens. Note the asymmetry the commit already introduced: the three `login.js` files **do** normalize the `/platforms/{...}/` basePath when honouring `?next=`, but the redirect helper that *sets* `?next=` does not — so in dev the two halves disagree. ### Impact Dev/localhost only. Cosmetic — a developer hitting an expired-session 401 on a path-based page sees the old generic error instead of a clean redirect. No production user is ever on a `/platforms/...` URL. This is why it is deferred, not shipped with the original fix. ## Proposed change Strip the dev prefix before the existing branch matching in `redirectIfUnauthorized()`, so the same four branches handle both modes: ```js redirectIfUnauthorized() { // Dev/path-based mode serves personas behind /platforms/{...}/. // Strip it so the prod-shaped branch matching below works for both. const path = window.location.pathname.replace(/^\/platforms\/[^/]+/, ''); // ...existing /account, /admin, /merchants, /store branch matching... } ``` Two details to settle during implementation: 1. **Storefront branch.** Today the helper has no storefront branch because in prod the storefront mounts at `/` and authenticated pages already match `/account/`. After stripping the dev prefix, a path-based storefront URL becomes `/storefront/{slug}/account/loyalty` — still not `/account/*`. Decide whether to add a `/storefront/` branch (mapping to the customer `/account/login`) or to additionally strip the `/storefront/{slug}` segment so it collapses to `/account/...`. The second is closer to how prod behaves. 2. **The `next` value.** `next` is built from `path + search`. Confirm it should carry the *original* prefixed path (so the user lands back on the dev URL) — the `login.js` basePath normalization already re-derives the prefix, so passing the stripped path is likely cleaner and avoids double-prefixing. Verify against `store/js/login.js`'s `storePathMatch` logic. ## Acceptance criteria - On localhost, an expired-session 401 from an apiClient call on each of: - `/platforms/{platform}/store/{code}/...` - `/platforms/{code}/storefront/{slug}/account/loyalty` redirects to the correct persona login with a working `?next=`, and re-auth lands the user back on the originating page. - No change to production behaviour (subdomain mode paths are unaffected by the prefix strip — the regex is a no-op when there is no `/platforms/` prefix). - No new architecture-validation findings. ## Out of scope - The original production fix (done in `4423f0a5`). - Any change to login.js basePath handling beyond what's needed to honour the stripped/un-stripped `next` decision above. ## Verification log - **2026-05-31** — Confirmed prod has no gap: storefront `/loyalty/join` uses only public endpoints; authenticated storefront pages are under `/account/`. Logged the dev-only gap; agreed to defer behind this proposal.