From 78baa4cc3123a3c50ae425a933960c88485b5b3e Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Bajpai Date: Wed, 20 May 2026 02:15:57 +0530 Subject: [PATCH 1/2] fix: preserve session on transient auth checks --- frontend/src/context/AuthContext.jsx | 35 ++++++++++++++++++++++------ frontend/src/services/api.js | 5 ++-- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/frontend/src/context/AuthContext.jsx b/frontend/src/context/AuthContext.jsx index dcdc4ed..2607e25 100644 --- a/frontend/src/context/AuthContext.jsx +++ b/frontend/src/context/AuthContext.jsx @@ -1,3 +1,4 @@ +/* eslint-disable react-refresh/only-export-components */ import { createContext, useContext, useState, useEffect } from "react"; import { getProfile } from "../services/userService"; @@ -10,9 +11,20 @@ export const useAuth = () => { }; export const AuthProvider = ({ children }) => { - const [user, setUser] = useState(null); + const readCachedUser = () => { + try { + const cachedUser = localStorage.getItem("user"); + return cachedUser ? JSON.parse(cachedUser) : null; + } catch { + localStorage.removeItem("user"); + return null; + } + }; + + const [user, setUser] = useState(readCachedUser); const [token, setToken] = useState(localStorage.getItem("token")); const [loading, setLoading] = useState(true); + const [authError, setAuthError] = useState(""); const isAuthenticated = !!token && !!user; @@ -24,12 +36,19 @@ export const AuthProvider = ({ children }) => { setToken(storedToken); const response = await getProfile(); setUser(response.data); + localStorage.setItem("user", JSON.stringify(response.data)); + setAuthError(""); } catch (error) { - // Token expired or invalid - localStorage.removeItem("token"); - localStorage.removeItem("user"); - setToken(null); - setUser(null); + if ([401, 403].includes(error?.response?.status)) { + localStorage.removeItem("token"); + localStorage.removeItem("user"); + setToken(null); + setUser(null); + setAuthError(""); + } else { + setUser((currentUser) => currentUser || readCachedUser()); + setAuthError("We could not refresh your profile. Your session is preserved and will retry when the connection is stable."); + } } } setLoading(false); @@ -39,8 +58,10 @@ export const AuthProvider = ({ children }) => { const login = (newToken, userData) => { localStorage.setItem("token", newToken); + localStorage.setItem("user", JSON.stringify(userData)); setToken(newToken); setUser(userData); + setAuthError(""); }; const logout = () => { @@ -50,7 +71,7 @@ export const AuthProvider = ({ children }) => { setUser(null); }; - const value = { user, setUser, token, isAuthenticated, loading, login, logout }; + const value = { user, setUser, token, isAuthenticated, loading, authError, login, logout }; return {children}; }; diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 4eb4d6f..62d1b06 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -4,6 +4,7 @@ const api = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, headers: { "Content-Type": "application/json" }, }); +const AUTH_FAILURE_STATUSES = new Set([401, 403]); // Request interceptor - inject auth token api.interceptors.request.use((config) => { @@ -14,11 +15,11 @@ api.interceptors.request.use((config) => { return config; }); -// Response interceptor - handle 401 +// Response interceptor - clear stored sessions only for real auth failures api.interceptors.response.use( (response) => response, (error) => { - if (error.response?.status === 401) { + if (AUTH_FAILURE_STATUSES.has(error.response?.status)) { localStorage.removeItem("token"); localStorage.removeItem("user"); window.location.href = "/login"; From 6ab632e2cf54ee37f6a64aae8ec8d4a200b6877b Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Bajpai Date: Mon, 1 Jun 2026 00:25:10 +0530 Subject: [PATCH 2/2] fix: address transient auth review --- frontend/src/context/AuthContext.jsx | 42 +++++++++++++++++++++++++--- frontend/src/services/api.js | 15 ++++++++-- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/frontend/src/context/AuthContext.jsx b/frontend/src/context/AuthContext.jsx index fd412dd..2f5a653 100644 --- a/frontend/src/context/AuthContext.jsx +++ b/frontend/src/context/AuthContext.jsx @@ -3,6 +3,24 @@ import { createContext, useContext, useState, useEffect, useCallback } from "rea import { getMe, logoutApi } from "../services/authService"; const AuthContext = createContext(null); +const AUTH_SESSION_HINT_KEY = "codelens.auth.sessionHint"; + +const readSessionHint = () => { + if (typeof window === "undefined") return false; + return window.sessionStorage.getItem(AUTH_SESSION_HINT_KEY) === "true"; +}; + +const writeSessionHint = () => { + if (typeof window !== "undefined") { + window.sessionStorage.setItem(AUTH_SESSION_HINT_KEY, "true"); + } +}; + +const clearSessionHint = () => { + if (typeof window !== "undefined") { + window.sessionStorage.removeItem(AUTH_SESSION_HINT_KEY); + } +}; export const useAuth = () => { const context = useContext(AuthContext); @@ -14,13 +32,15 @@ export const AuthProvider = ({ children }) => { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [authError, setAuthError] = useState(""); + const [authUnknown, setAuthUnknown] = useState(readSessionHint); /** - * isAuthenticated: true only when we have a confirmed user object from the server. - * We do NOT rely on localStorage or any client-side token — the server's HttpOnly - * cookie is the single source of truth. + * isAuthenticated remains true for a previously confirmed session when the + * bootstrap profile call hits a transient failure. The server's HttpOnly + * cookie is still the source of truth; authUnknown only prevents route guards + * from treating temporary API downtime as a logout. */ - const isAuthenticated = !!user; + const isAuthenticated = !!user || authUnknown; /** * On mount: ask the server "who am I?" using the HttpOnly cookie. @@ -32,12 +52,17 @@ export const AuthProvider = ({ children }) => { try { const response = await getMe(); setUser(response.data); + setAuthUnknown(false); setAuthError(""); + writeSessionHint(); } catch (err) { if ([401, 403].includes(err?.response?.status)) { setUser(null); + setAuthUnknown(false); setAuthError(""); + clearSessionHint(); } else { + setAuthUnknown(readSessionHint()); setAuthError("We could not refresh your session. Please retry when the connection is stable."); } } finally { @@ -54,7 +79,9 @@ export const AuthProvider = ({ children }) => { */ const login = useCallback((userData) => { setUser(userData); + setAuthUnknown(false); setAuthError(""); + writeSessionHint(); }, []); /** @@ -70,7 +97,9 @@ export const AuthProvider = ({ children }) => { // Even if the API call fails, clear local state } finally { setUser(null); + setAuthUnknown(false); setAuthError(""); + clearSessionHint(); } }, []); @@ -83,12 +112,16 @@ export const AuthProvider = ({ children }) => { try { const response = await getMe(); setUser(response.data); + setAuthUnknown(false); setAuthError(""); + writeSessionHint(); return response.data; } catch (err) { if ([401, 403].includes(err?.response?.status)) { setUser(null); + setAuthUnknown(false); setAuthError(""); + clearSessionHint(); } else { setAuthError("We could not refresh your session. Please retry when the connection is stable."); } @@ -102,6 +135,7 @@ export const AuthProvider = ({ children }) => { isAuthenticated, loading, authError, + authUnknown, login, logout, refreshUser diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 410c7bb..790733a 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -7,6 +7,17 @@ const api = axios.create({ }); const AUTH_FAILURE_STATUSES = new Set([401, 403]); +const AUTH_ONLY_PATHS = new Set(["/api/auth/me", "/api/user/profile"]); + +const getRequestPathname = (requestUrl) => { + try { + const origin = typeof window !== "undefined" ? window.location.origin : "http://localhost"; + const { pathname } = new URL(requestUrl, origin); + return pathname.replace(/\/+$/, "") || "/"; + } catch { + return ""; + } +}; // No token injection needed — cookies are sent automatically by the browser. api.interceptors.request.use((config) => config); @@ -14,8 +25,6 @@ api.interceptors.request.use((config) => config); // Redirect only when the application's own session endpoints confirm auth loss. // Other 401/403 responses, such as GitHub proxy failures, should be handled by // the feature page that made the request. -const AUTH_ONLY_PATHS = ["/api/auth/me", "/api/user/profile"]; - api.interceptors.response.use( (response) => response, (error) => { @@ -23,7 +32,7 @@ api.interceptors.response.use( const requestUrl = error.config?.url || ""; if (AUTH_FAILURE_STATUSES.has(status)) { - const isAuthPath = AUTH_ONLY_PATHS.some((path) => requestUrl.includes(path)); + const isAuthPath = AUTH_ONLY_PATHS.has(getRequestPathname(requestUrl)); if (isAuthPath) { window.location.replace("/login");