From 65679c1b2a6aa01fb8f6d0012dd9738f7ec3544d Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Mon, 1 Dec 2025 09:31:04 +0330 Subject: [PATCH] base refresh token bug fix --- src/lib/api/auth/refresh-token.ts | 18 ++-- src/lib/api/axiosInstance.ts | 161 ++++++++++++++++-------------- src/lib/api/func.ts | 42 ++++++++ 3 files changed, 140 insertions(+), 81 deletions(-) create mode 100644 src/lib/api/func.ts diff --git a/src/lib/api/auth/refresh-token.ts b/src/lib/api/auth/refresh-token.ts index 572abd3..a136fc1 100644 --- a/src/lib/api/auth/refresh-token.ts +++ b/src/lib/api/auth/refresh-token.ts @@ -1,10 +1,16 @@ -import { api } from "../axiosInstance"; +import axios from "axios"; export type RefreshTokenRequestModel = { - token: string, -} + token: string; +}; export const refreshToken = async (model: RefreshTokenRequestModel) => { - const res = await api.post('/token', model) - return res.data -} + const res = await axios.post( + `${process.env.NEXT_PUBLIC_API_BASE_URL}/public/auth/refresh`, + { refreshToken: model.token }, + { + headers: { "Content-Type": "application/json" }, + } + ); + return res.data; +}; diff --git a/src/lib/api/axiosInstance.ts b/src/lib/api/axiosInstance.ts index 42319f2..a25be56 100644 --- a/src/lib/api/axiosInstance.ts +++ b/src/lib/api/axiosInstance.ts @@ -1,99 +1,101 @@ -import { useAuthStore } from "@/zustand/authStore"; import axios from "axios"; +import { + getToken, + getRefreshToken, + setToken, + setRefreshToken, + removeToken, + removeRefreshToken, +} from "./func"; import { refreshToken } from "./auth/refresh-token"; -// Create axios instance -const api = axios.create({ +declare global { + interface Window { + isRefreshingToken?: boolean; + } +} + +const axiosInstance = axios.create({ baseURL: process.env.NEXT_PUBLIC_API_BASE_URL, headers: { "Content-Type": "application/json" }, }); -let isRefreshing = false; -let failedQueue: { - resolve: (value?: unknown) => void; - reject: (error: unknown) => void; -}[] = []; - -const processQueue = (error: unknown, token: string | null = null) => { - failedQueue.forEach(({ resolve, reject }) => { - if (error) { - reject(error); - } else { - resolve(token); - } - }); - failedQueue = []; -}; - -// Attach access token to requests -api.interceptors.request.use((config) => { - const token = localStorage.getItem( - process.env.NEXT_PUBLIC_TOKEN_NAME as string - ); - - if (token) { - config.headers = config.headers || {}; - config.headers.Authorization = `Bearer ${token}`; - } - return config; -}); - -// Response interceptor to handle 401 and refresh token logic -api.interceptors.response.use( +axiosInstance.interceptors.response.use( (response) => response, async (error) => { const originalRequest = error.config; - if ( - error.response?.status === 401 && - !originalRequest._retry // custom flag to prevent infinite loops - ) { - if (isRefreshing) { - // Queue the requests while refreshing - return new Promise((resolve, reject) => { - failedQueue.push({ resolve, reject }); - }) - .then((token) => { - originalRequest.headers.Authorization = `Bearer ${token}`; - return api(originalRequest); - }) - .catch((err) => { - return Promise.reject(err); - }); + if (error?.response?.status === 401 && !originalRequest._retry) { + if (typeof window !== "undefined" && window.isRefreshingToken) { + // Wait for the refresh to complete + await new Promise((resolve) => { + const checkComplete = setInterval(() => { + if (typeof window !== "undefined" && !window.isRefreshingToken) { + clearInterval(checkComplete); + resolve(true); + } + }, 100); + }); + + return axiosInstance(originalRequest); } originalRequest._retry = true; - isRefreshing = true; - - const token = useAuthStore.getState().user?.refreshToken; - console.log(useAuthStore.getState()); - - if (!token) { - useAuthStore.getState().logout(); - return Promise.reject(error); + if (typeof window !== "undefined") { + window.isRefreshingToken = true; } try { - const data = await refreshToken({ token }); - // Update tokens in Zustand store - useAuthStore.getState().login({ - ...useAuthStore.getState().user!, - accessToken: data.accessToken, - refreshToken: data.refreshToken || token, + const refreshTokenValue = await getRefreshToken(); + + if (!refreshTokenValue) { + await removeRefreshToken(); + await removeToken(); + return; + } + + const data = await refreshToken({ + token: refreshTokenValue, }); - api.defaults.headers.common.Authorization = `Bearer ${data.accessToken}`; - originalRequest.headers.Authorization = `Bearer ${data.accessToken}`; + // Support both response structures + const accessToken = + data?.results?.data?.accessToken?.token || + data?.data?.tokens?.accessToken?.token || + data?.accessToken; + const refreshTokenValueNew = + data?.results?.data?.refreshToken?.token || + data?.data?.tokens?.refreshToken?.token || + data?.refreshToken; - processQueue(null, data.accessToken); + if (accessToken) { + await setToken(accessToken); - return api(originalRequest); - } catch (err) { - processQueue(err, null); - useAuthStore.getState().logout(); - return Promise.reject(err); + if (refreshTokenValueNew) { + await setRefreshToken(refreshTokenValueNew); + } + + originalRequest.headers.Authorization = `Bearer ${accessToken}`; + return axiosInstance(originalRequest); + } else { + await removeRefreshToken(); + await removeToken(); + return; + } + } catch (refreshError) { + console.error("Token refresh failed:", refreshError); + await removeToken(); + await removeRefreshToken(); + if (typeof window !== "undefined") { + const pathname = window.location.pathname; + const slug = pathname.split("/")[1]; + window.location.href = slug ? `/${slug}` : `/`; + } + return Promise.reject(refreshError); } finally { - isRefreshing = false; + if (typeof window !== "undefined") { + window.isRefreshingToken = false; + } } } @@ -101,4 +103,13 @@ api.interceptors.response.use( } ); -export { api }; +axiosInstance.interceptors.request.use(async function (config) { + const tokenValue = await getToken(); + const token = tokenValue ? tokenValue : ""; + + config.headers.Authorization = "Bearer " + token; + + return config; +}); + +export { axiosInstance as api }; diff --git a/src/lib/api/func.ts b/src/lib/api/func.ts new file mode 100644 index 0000000..0786196 --- /dev/null +++ b/src/lib/api/func.ts @@ -0,0 +1,42 @@ +export const getToken = async (): Promise => { + if (typeof window === "undefined") return null; + return localStorage.getItem( + process.env.NEXT_PUBLIC_TOKEN_NAME as string + ); +}; + +export const getRefreshToken = async (): Promise => { + if (typeof window === "undefined") return null; + return localStorage.getItem( + process.env.NEXT_PUBLIC_REFRESH_TOKEN_NAME as string + ); +}; + +export const setToken = async (token: string): Promise => { + if (typeof window === "undefined") return; + localStorage.setItem( + process.env.NEXT_PUBLIC_TOKEN_NAME as string, + token + ); +}; + +export const setRefreshToken = async (token: string): Promise => { + if (typeof window === "undefined") return; + localStorage.setItem( + process.env.NEXT_PUBLIC_REFRESH_TOKEN_NAME as string, + token + ); +}; + +export const removeToken = async (): Promise => { + if (typeof window === "undefined") return; + localStorage.removeItem(process.env.NEXT_PUBLIC_TOKEN_NAME as string); +}; + +export const removeRefreshToken = async (): Promise => { + if (typeof window === "undefined") return; + localStorage.removeItem( + process.env.NEXT_PUBLIC_REFRESH_TOKEN_NAME as string + ); +}; +