feat(auth): implement global logout callback for API interceptors

- Add global logout callback registration and retrieval functions in User context
- Integrate logout callback in response interceptor to handle 401 errors on token refresh
- Ensure fallback logout mechanism if callback is not registered, clearing cookies and redirecting to sign-in
This commit is contained in:
AmirReza Jamali
2026-01-25 14:52:04 +03:30
parent 3080a5fe6d
commit cafd7568e0
2 changed files with 70 additions and 9 deletions
+19
View File
@@ -29,6 +29,17 @@ interface UserContextType {
const UserContext = createContext<UserContextType | undefined>(undefined);
// Global logout callback for use in non-React contexts (e.g., API interceptors)
let globalLogoutCallback: (() => void) | null = null;
export const registerLogoutCallback = (callback: () => void) => {
globalLogoutCallback = callback;
};
export const getLogoutCallback = (): (() => void) | null => {
return globalLogoutCallback;
};
interface UserProviderProps {
children: ReactNode;
}
@@ -47,6 +58,14 @@ export const UserProvider: React.FC<UserProviderProps> = ({ children }) => {
Cookies.remove(COOKIE_KEYS.user);
window.location.href = "/auth/sign-in";
}, [router]);
// Register logout callback for use in API interceptors
useEffect(() => {
registerLogoutCallback(logout);
return () => {
globalLogoutCallback = null;
};
}, [logout]);
useEffect(() => {
try {
const tokenFromCookie = Cookies.get(COOKIE_KEYS.access_token);
+45 -3
View File
@@ -1,6 +1,7 @@
import { AxiosError, AxiosResponse } from "axios";
import Cookies from "js-cookie";
import { toast } from "react-toastify";
import { getLogoutCallback } from "../contexts/User.ctx";
import { API_ENDPOINTS, api } from "../lib/api";
import { COOKIE_KEYS } from "../lib/shared/cookies";
@@ -31,6 +32,27 @@ export const responseErrorHandler = async (error: AxiosError) => {
API_ENDPOINTS.USER.LOGIN,
);
const isRefreshTokenRequest = originalRequest?.url?.includes(
API_ENDPOINTS.USER.REFRESH_TOKEN,
);
// If we get a 401 on refresh token endpoint, logout immediately (token refresh failed)
if (error.response?.status === 401 && isRefreshTokenRequest) {
const logout = getLogoutCallback();
if (logout) {
logout();
} else {
// Fallback if logout callback is not registered
Cookies.remove(COOKIE_KEYS.access_token);
Cookies.remove(COOKIE_KEYS.refresh_token);
Cookies.remove(COOKIE_KEYS.user);
if (typeof window !== "undefined") {
window.location.href = "/auth/sign-in";
}
}
return Promise.reject(error);
}
if (
error.response?.status === 401 &&
!originalRequest._retry &&
@@ -55,7 +77,20 @@ export const responseErrorHandler = async (error: AxiosError) => {
try {
const refreshToken = Cookies.get(COOKIE_KEYS.refresh_token);
if (!refreshToken) {
throw new Error("Refresh token not found.");
// No refresh token available, logout immediately
const logout = getLogoutCallback();
if (logout) {
logout();
} else {
// Fallback if logout callback is not registered
Cookies.remove(COOKIE_KEYS.access_token);
Cookies.remove(COOKIE_KEYS.refresh_token);
Cookies.remove(COOKIE_KEYS.user);
if (typeof window !== "undefined") {
window.location.href = "/auth/sign-in";
}
}
return Promise.reject(new Error("Refresh token not found."));
}
const response = await api.post(API_ENDPOINTS.USER.REFRESH_TOKEN, {
@@ -81,14 +116,21 @@ export const responseErrorHandler = async (error: AxiosError) => {
return api(originalRequest);
} catch (refreshError) {
processQueue(refreshError, null);
// Use logout callback from User context
const logout = getLogoutCallback();
if (logout) {
logout();
} else {
// Fallback if logout callback is not registered
Cookies.remove(COOKIE_KEYS.access_token);
Cookies.remove(COOKIE_KEYS.refresh_token);
Cookies.remove(COOKIE_KEYS.user);
if (typeof window !== "undefined") {
localStorage.removeItem("auth-token");
localStorage.removeItem("user-data");
window.location.href = "/login";
window.location.href = "/auth/sign-in";
}
}
const errorMessage =