From 830883c6b74c53203273a64909e45686e92dee53 Mon Sep 17 00:00:00 2001 From: "santasri.pachhal" Date: Mon, 13 Jul 2026 19:29:04 +0530 Subject: [PATCH] fix(auth): prevent logout on transient API failures --- src/providers/AppInitializer.tsx | 44 +++++++++++------ src/services/axios/axios.ts | 85 +++++++++++++++++++++----------- 2 files changed, 87 insertions(+), 42 deletions(-) diff --git a/src/providers/AppInitializer.tsx b/src/providers/AppInitializer.tsx index dbd37ac..b960966 100644 --- a/src/providers/AppInitializer.tsx +++ b/src/providers/AppInitializer.tsx @@ -1,19 +1,20 @@ 'use client'; +import { Button } from '@/components/ui/button'; import { initializeAuthenticatedApp } from '@/services/initializer.service'; import { useAppStore } from '@/store/app.store'; import { useAuthStore } from '@/store/auth.store'; import type { ReactNode } from 'react'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; export default function AppInitializer({ children }: { children: ReactNode }) { const accessToken = useAuthStore((state) => state.accessToken); - const logout = useAuthStore((state) => state.logout); const user = useAppStore((state) => state.user); + const loadStatus = useAppStore((state) => state.loadStatus); const setLoading = useAppStore((state) => state.setLoading); const setLoaded = useAppStore((state) => state.setLoaded); const setLoadError = useAppStore((state) => state.setLoadError); - const clearApp = useAppStore((state) => state.clear); + const [retryAttempt, setRetryAttempt] = useState(0); useEffect(() => { let isActive = true; @@ -30,8 +31,6 @@ export default function AppInitializer({ children }: { children: ReactNode }) { await initializeAuthenticatedApp(); } catch { if (isActive) { - logout(); - clearApp(); setLoadError(); } } @@ -42,15 +41,32 @@ export default function AppInitializer({ children }: { children: ReactNode }) { return () => { isActive = false; }; - }, [ - accessToken, - clearApp, - logout, - setLoaded, - setLoadError, - setLoading, - user, - ]); + }, [accessToken, retryAttempt, setLoaded, setLoadError, setLoading, user]); + + if (accessToken && !user && loadStatus === 'error') { + return ( +
+
+
+

Unable to load workspace

+

+ We could not load your account details. Your session has been + kept, so you can safely try again. +

+
+ +
+
+ ); + } return children; } diff --git a/src/services/axios/axios.ts b/src/services/axios/axios.ts index b5820ea..bb065e7 100644 --- a/src/services/axios/axios.ts +++ b/src/services/axios/axios.ts @@ -12,6 +12,62 @@ const axiosClient = axios.create({ }, }); +export const axiosAuth = axios.create({ + baseURL: BASE_URL, + headers: { + 'Content-Type': 'application/json', + }, +}); + +let refreshPromise: Promise | null = null; + +const isInvalidSessionResponse = (error: unknown) => { + if (!axios.isAxiosError(error)) return false; + + return error.response?.status === 401 || error.response?.status === 403; +}; + +const refreshAccessToken = () => { + if (!refreshPromise) { + refreshPromise = axiosAuth + .post( + 'api/auth/refresh', + {}, + { + withCredentials: true, + }, + ) + .then((response) => { + const accessToken = response.data?.access_token; + + if (!accessToken) { + throw new Error('Refresh failed: no access token'); + } + + useAuthStore.getState().setAccessToken(accessToken); + + return accessToken; + }) + .catch((error: unknown) => { + if (isInvalidSessionResponse(error)) { + useAuthStore.getState().logout(); + useAppStore.getState().clear(); + + if (typeof window !== 'undefined') { + window.location.href = '/login'; + } + } + + throw error; + }) + .finally(() => { + refreshPromise = null; + }); + } + + return refreshPromise; +}; + axiosClient.interceptors.request.use( (config) => { const token = useAuthStore.getState().accessToken; @@ -49,31 +105,11 @@ axiosClient.interceptors.response.use( originalRequest._retry = true; try { - const response = await axiosAuth.post( - 'api/auth/refresh', - {}, - { - withCredentials: true, - }, - ); - const accessToken = response.data?.access_token; - - if (!accessToken) { - throw new Error('Refresh failed: no access token'); - } - - useAuthStore.getState().setAccessToken(accessToken); + const accessToken = await refreshAccessToken(); originalRequest.headers.Authorization = `Bearer ${accessToken}`; return axiosClient(originalRequest); } catch (refreshError) { - useAuthStore.getState().logout(); - useAppStore.getState().clear(); - - if (typeof window !== 'undefined') { - window.location.href = '/login'; - } - return Promise.reject(refreshError); } } @@ -82,11 +118,4 @@ axiosClient.interceptors.response.use( }, ); -export const axiosAuth = axios.create({ - baseURL: BASE_URL, - headers: { - 'Content-Type': 'application/json', - }, -}); - export default axiosClient;