Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 28 additions & 7 deletions frontend/src/context/AuthContext.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable react-refresh/only-export-components */
import { createContext, useContext, useState, useEffect } from "react";
import { getProfile } from "../services/userService";

Expand All @@ -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;

Expand All @@ -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);
Expand All @@ -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 = () => {
Expand All @@ -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 <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
5 changes: 3 additions & 2 deletions frontend/src/services/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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";
Expand Down
Loading