From a0ae63882155666c7107084c8136e1ec18958128 Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Fri, 29 May 2026 21:28:34 +0200 Subject: [PATCH] fix(storefront-auth): apiClient redirects to login on 401 from /account/* MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the customer's JWT (30-min TTL via JWT_EXPIRE_MINUTES) expires in localStorage, subsequent API calls from a customer-area page returned 401 → callers showed an unrelated error UI (loyalty dashboard rendered the "join now" CTA because card came back null on the catch path). Three changes in static/shared/js/api-client.js: 1. Path detection in getToken() + clearTokens() now recognises /account/* and /api/v1/storefront/* as customer-area routes (the only existing checks were for /shop/* which was never used in this codebase). Also clears customer_user alongside customer_token. 2. New redirectIfCustomerAreaUnauthorized() helper: on a /account/* page, sends the browser to /account/login?next= (with a guard to skip the redirect when already on the login page, avoiding loops). Called from all three 401 paths (request, requestFormData, getBlob). 3. login.html now honours the ?next= query param (in addition to the legacy ?return=), so the redirect lands the user back where their session expired. Other personas (admin/store/merchant) are unaffected — the helper is a no-op outside /account/*. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../templates/customers/storefront/login.html | 7 +++- static/shared/js/api-client.js | 42 +++++++++++++++++-- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/app/modules/customers/templates/customers/storefront/login.html b/app/modules/customers/templates/customers/storefront/login.html index c5011b1d..217d00d8 100644 --- a/app/modules/customers/templates/customers/storefront/login.html +++ b/app/modules/customers/templates/customers/storefront/login.html @@ -295,9 +295,12 @@ this.showAlert('Login successful! Redirecting...', 'success'); - // Redirect to account page or return URL + // Redirect to account page or return URL. + // Accepts `?next=` (apiClient's 401-handler convention) + // or `?return=` (legacy) — `next` wins. setTimeout(() => { - const returnUrl = new URLSearchParams(window.location.search).get('return') || '{{ base_url }}account'; + const params = new URLSearchParams(window.location.search); + const returnUrl = params.get('next') || params.get('return') || '{{ base_url }}account'; window.location.href = returnUrl; }, 1000); diff --git a/static/shared/js/api-client.js b/static/shared/js/api-client.js index a3b5fca4..31ed6703 100644 --- a/static/shared/js/api-client.js +++ b/static/shared/js/api-client.js @@ -56,7 +56,12 @@ class APIClient { } else if (currentPath.startsWith('/admin/') || currentPath.startsWith('/api/v1/admin/')) { token = adminToken; source = 'admin (path-based)'; - } else if (currentPath.includes('/shop/') || currentPath.startsWith('/api/v1/shop/')) { + } else if ( + currentPath.startsWith('/account/') || + currentPath.startsWith('/api/v1/storefront/') || + currentPath.includes('/shop/') || + currentPath.startsWith('/api/v1/shop/') + ) { token = customerToken; source = 'customer (path-based)'; } else if (currentPath.startsWith('/merchants/') || currentPath.startsWith('/api/v1/merchants/')) { @@ -145,12 +150,13 @@ class APIClient { } } - // Handle 401 Unauthorized - Just clear tokens, DON'T redirect + // Handle 401 Unauthorized if (response.status === 401) { apiLog.warn('401 Unauthorized - Authentication failed'); apiLog.debug('Error details:', data); apiLog.info('Clearing authentication tokens'); this.clearTokens(); + this.redirectIfCustomerAreaUnauthorized(); const errorMessage = data.message || data.detail || 'Unauthorized - please login again'; apiLog.error('Throwing authentication error:', errorMessage); @@ -299,6 +305,7 @@ class APIClient { if (response.status === 401) { apiLog.warn('401 Unauthorized - Authentication failed'); this.clearTokens(); + this.redirectIfCustomerAreaUnauthorized(); throw new Error(data.message || data.detail || 'Unauthorized'); } @@ -339,6 +346,7 @@ class APIClient { if (response.status === 401) { apiLog.warn('401 Unauthorized - Authentication failed'); this.clearTokens(); + this.redirectIfCustomerAreaUnauthorized(); throw new Error('Unauthorized'); } @@ -395,9 +403,15 @@ class APIClient { localStorage.removeItem('store_user'); localStorage.removeItem('currentUser'); localStorage.removeItem('storeCode'); - } else if (currentPath.includes('/shop/') || currentPath.startsWith('/api/v1/shop/')) { + } else if ( + currentPath.startsWith('/account/') || + currentPath.startsWith('/api/v1/storefront/') || + currentPath.includes('/shop/') || + currentPath.startsWith('/api/v1/shop/') + ) { apiLog.info('Clearing customer tokens only'); localStorage.removeItem('customer_token'); + localStorage.removeItem('customer_user'); } else if (currentPath.startsWith('/merchants/') || currentPath.startsWith('/api/v1/merchants/')) { apiLog.info('Clearing merchant tokens only'); localStorage.removeItem('merchant_token'); @@ -426,6 +440,28 @@ class APIClient { apiLog.info('Context-specific tokens cleared'); } + /** + * If the user is on a customer-area page (/account/*) and gets 401, + * send them to the login page with a return URL so they land back + * here after re-authenticating. + * + * No-op for admin/store/merchant areas — those callers handle 401 + * their own way. Also no-op if already on the login page (avoids + * a redirect loop). + * + * Returns true if a redirect was scheduled (caller should suppress + * its own error UI since the page is about to navigate away). + */ + redirectIfCustomerAreaUnauthorized() { + const path = window.location.pathname; + const onCustomerArea = path.startsWith('/account/') && path !== '/account/login'; + if (!onCustomerArea) return false; + const next = encodeURIComponent(path + window.location.search); + apiLog.info('Redirecting to /account/login (session expired), next=' + next); + window.location.href = `/account/login?next=${next}`; + return true; + } + /** * Handle unauthorized access * DEPRECATED - Now just clears tokens, doesn't redirect