fix(storefront-auth): apiClient redirects to login on 401 from /account/*
Some checks failed
Some checks failed
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=<current path> (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) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user