docs(auth): record 2026-05-31 401-redirect review + dev/prod symmetry proposal
Some checks failed
CI / ruff (push) Successful in 19s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled

Reviewed commit 4423f0a5 (generalize 401 redirect to all 4 personas) and
verified production has no coverage gap: the only uncovered storefront page
(/loyalty/join) hits exclusively public, no-auth endpoints that can't 401,
and authenticated storefront pages live under /account/ which is already
covered. Remaining gap is dev/path-based-mode only (/platforms/...) — captured
as a deferred enhancement in docs/proposals/auth-redirect-dev-prod-symmetry.md
with the proposed one-line prefix-strip fix and two open implementation
details. No code shipped this session. mkdocs --strict clean; architecture
validation passed (0 new findings).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-31 22:34:09 +02:00
parent 4423f0a5ed
commit ac7850b880
2 changed files with 110 additions and 0 deletions

View File

@@ -0,0 +1,109 @@
# Auth-Redirect Dev/Prod Symmetry
**Date:** 2026-05-31
**Status:** Proposed (deferred — low priority, dev-only impact)
**Owner:** TBD
**Effort:** ~12h 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.

View File

@@ -329,6 +329,7 @@ nav:
- Module Dependency Redesign: proposals/SESSION_NOTE_2026-02-03_module-dependency-redesign.md - Module Dependency Redesign: proposals/SESSION_NOTE_2026-02-03_module-dependency-redesign.md
- Import-002 Cross-Module Deps: proposals/SESSION_NOTE_2026-02-09_import002-cross-module-deps.md - Import-002 Cross-Module Deps: proposals/SESSION_NOTE_2026-02-09_import002-cross-module-deps.md
- Android Terminal Implementation: proposals/android-terminal-implementation.md - Android Terminal Implementation: proposals/android-terminal-implementation.md
- Auth-Redirect Dev/Prod Symmetry: proposals/auth-redirect-dev-prod-symmetry.md
- Backward Compatibility Cleanup: proposals/backward-compatibility-cleanup.md - Backward Compatibility Cleanup: proposals/backward-compatibility-cleanup.md
- CMS Redesign Alignment: proposals/cms-redesign-alignment.md - CMS Redesign Alignment: proposals/cms-redesign-alignment.md
- Decouple Modules: proposals/decouple-modules.md - Decouple Modules: proposals/decouple-modules.md