From 9e418c759ce508adf6025e0740468d8229938ffe Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Tue, 12 May 2026 00:07:50 +0200 Subject: [PATCH] fix(customer): don't log customer out on transient session-refresh errors --- frontend/src/contexts/CustomerAuthContext.tsx | 45 ++++++++++++------- frontend/src/services/customer.service.ts | 27 ++++++++++- 2 files changed, 55 insertions(+), 17 deletions(-) diff --git a/frontend/src/contexts/CustomerAuthContext.tsx b/frontend/src/contexts/CustomerAuthContext.tsx index a21e2043..8297a1b8 100644 --- a/frontend/src/contexts/CustomerAuthContext.tsx +++ b/frontend/src/contexts/CustomerAuthContext.tsx @@ -84,22 +84,37 @@ export const CustomerAuthProvider: React.FC = ({ children }) => { * mount-time sessionStorage cache and stays stale until a hard reload. */ const refreshSession = React.useCallback(async () => { + // Contract (see customerService.session()): + // - object → fresh data, store it. + // - null → server says we're explicitly logged out (401); + // clear local state. + // - throw → transient error (network blip, 5xx, timeout). + // KEEP whatever state we have — logging the user out + // on a flaky network call is the wrong default. The + // old code clobbered local state on any error, which + // caused mysterious "customer keeps getting kicked + // out" reports during unrelated admin saves and on + // brief connection drops. + let response: Awaited>; try { - const response = await customerService.session(); - if (response?.customer) { - setCustomerState(response.customer); - setFeatures(response.features); - setBranding(response.branding); - sessionStorage.setItem(STORAGE_KEY, JSON.stringify(response.customer)); - sessionStorage.setItem(FEATURES_KEY, JSON.stringify(response.features)); - sessionStorage.setItem(BRANDING_KEY, JSON.stringify(response.branding)); - } else { - setCustomerState(null); - sessionStorage.removeItem(STORAGE_KEY); - sessionStorage.removeItem(FEATURES_KEY); - sessionStorage.removeItem(BRANDING_KEY); - } - } catch { + response = await customerService.session(); + } catch (err) { + // Transient. Don't touch state. The next focus/visibility tick + // will retry; if the customer really is unauthenticated the + // retry will see the 401 and clear properly. + // eslint-disable-next-line no-console + console.warn('[CustomerAuth] session refresh failed transiently, keeping current state', err); + return; + } + if (response?.customer) { + setCustomerState(response.customer); + setFeatures(response.features); + setBranding(response.branding); + sessionStorage.setItem(STORAGE_KEY, JSON.stringify(response.customer)); + sessionStorage.setItem(FEATURES_KEY, JSON.stringify(response.features)); + sessionStorage.setItem(BRANDING_KEY, JSON.stringify(response.branding)); + } else { + // Explicit 401 — server says no. setCustomerState(null); sessionStorage.removeItem(STORAGE_KEY); sessionStorage.removeItem(FEATURES_KEY); diff --git a/frontend/src/services/customer.service.ts b/frontend/src/services/customer.service.ts index 61a49184..216b974c 100644 --- a/frontend/src/services/customer.service.ts +++ b/frontend/src/services/customer.service.ts @@ -126,6 +126,23 @@ export const customerService = { } }, + /** + * Resolve the current customer session. + * + * Return contract: + * - object → fresh customer + features + branding from the server. + * - null → backend says we are NOT authenticated (401). The + * caller should clear local state and bounce to login. + * - throws → any other error (network blip, 5xx, timeout, 410 + * from a feature-flag flip mid-flight). The caller + * should KEEP whatever state it has — punishing the + * user with a logout for a transient failure is the + * wrong default. Previously this catch swallowed + * everything and returned null, which logged the + * customer out on the slightest server hiccup + * (including the brief window while the admin saves + * an unrelated change like gallery assignments). + */ async session(): Promise<{ customer: CustomerProfile; features: { calendar: boolean; quotes: boolean; bills: boolean }; @@ -142,8 +159,14 @@ export const customerService = { features: response.data.features || { calendar: false, quotes: false, bills: false }, branding: response.data.branding || { showLogo: true, showCompanyName: true }, }; - } catch { - return null; + } catch (error: any) { + // Only treat an explicit 401 as "session is gone". Anything else + // (network failure, server 500, etc.) is a transient problem + // and should not log the customer out. + if (error?.response?.status === 401) { + return null; + } + throw error; } },