From 94d10d56a9801df797fc9aa68374318b43609ab7 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Tue, 27 Jan 2026 12:39:58 +0330 Subject: [PATCH] base refresh token --- src/App.tsx | 116 +++++++++++++++++++++++++- src/config/axios.ts | 4 +- src/pages/auth/service/AuthService.ts | 2 +- src/shared/types/Types.ts | 20 +++++ 4 files changed, 135 insertions(+), 7 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 5ad0655..a7f3430 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,16 +3,126 @@ import '@/assets/fonts/irancell/style.css' import 'swiper/swiper-bundle.css'; import 'react-loading-skeleton/dist/skeleton.css' import { BrowserRouter } from 'react-router-dom' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query' // import { getToken } from './config/func' import MainRouter from './router/MainRouter' // import AuthRouter from './router/AuthRouter' import ToastContainer from './components/Toast' import { HelmetProvider } from 'react-helmet-async'; -import { getToken } from './config/func'; +import { getRefreshToken, getToken, removeRefreshToken, removeToken, setRefreshToken, setToken } from './config/func'; import AuthRouter from './router/AuthRouter'; +import type { IApiErrorRepsonse } from './shared/types/Types'; +import { Paths } from './config/Paths'; +import { refreshToken } from './pages/auth/service/AuthService'; -const queryClient = new QueryClient() +declare global { + interface Window { + isRefreshingToken?: boolean; + isRefreshTokenExpired?: boolean; + } +} + +const queryClient = new QueryClient({ + queryCache: new QueryCache({ + onError: async (error: IApiErrorRepsonse) => { + if (error?.response?.statusCode === 401) { + // اگر refresh token منقضی شده باشد، دیگر تلاش نکن + if (window.isRefreshTokenExpired) { + return; + } + + // بررسی کن که آیا خطا از refresh endpoint است + const isRefreshEndpoint = error?.config?.url?.includes('/public/auth/refresh'); + + // اگر خطا از refresh endpoint است، refresh token منقضی شده + if (isRefreshEndpoint) { + window.isRefreshTokenExpired = true; + await removeToken(); + await removeRefreshToken(); + window.location.href = Paths.auth.login; + return; + } + + try { + // Use a flag to track if refresh is in progress + if (window.isRefreshingToken) { + // Wait for the refresh to complete + await new Promise(resolve => { + const checkComplete = setInterval(() => { + if (!window.isRefreshingToken) { + clearInterval(checkComplete); + resolve(true); + } + }, 100); + }); + return; + } + + // Set the flag to indicate refresh is in progress + window.isRefreshingToken = true; + + try { + const refreshTokenValue = await getRefreshToken(); + + if (!refreshTokenValue) { + window.isRefreshTokenExpired = true; + await removeToken(); + await removeRefreshToken(); + window.location.href = Paths.auth.login; + return; + } + + const { data } = await refreshToken({ refreshToken: refreshTokenValue || '' }); + + if (data?.accessToken?.token) { + // Save the new token + await setToken(data?.accessToken?.token); + + // If refresh token also returned, update it + if (data.refreshToken?.token) { + await setRefreshToken(data.refreshToken?.token); + } + + // Reset expired flag if refresh was successful + window.isRefreshTokenExpired = false; + + // Retry the failed request + queryClient.invalidateQueries(); + return; + } else { + throw new Error('Invalid token response'); + } + } finally { + window.isRefreshingToken = false; + } + } catch (refreshError: unknown) { + console.error('Token refresh failed:', refreshError); + + // بررسی کن که آیا خطا از refresh endpoint است + const errorWithConfig = refreshError as IApiErrorRepsonse; + const isRefreshError = errorWithConfig?.config?.url?.includes('/admin/auth/refresh'); + + // اگر refresh token خودش 401 خورده، آن را منقضی شده علامت بزن + if (isRefreshError || errorWithConfig?.response?.statusCode === 401) { + window.isRefreshTokenExpired = true; + } + + // Clear tokens and redirect to login + await removeToken(); + await removeRefreshToken(); + window.location.href = Paths.auth.login; + } + } + } + }), + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + retry: false + // staleTime: 86400000 // 1 day in milliseconds + } + } +}); const App: FC = () => { diff --git a/src/config/axios.ts b/src/config/axios.ts index db6c869..6bcdf09 100644 --- a/src/config/axios.ts +++ b/src/config/axios.ts @@ -8,9 +8,7 @@ const instance = axios.create({ instance.interceptors.request.use( (config) => { - const token = - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIwMUtGWjREWDhTNVJNOFMzMDc1NVZTWUNQUSIsImlhdCI6MTc2OTUwMTE0OCwiZXhwIjoxNzY5NTAyMzQ4fQ.IOYPKCBPCUS9BtWdIDJWG1uG3nnVmLoInDirEZfsl6k" || - getToken(); + const token = getToken(); if (token) { config.headers.Authorization = `Bearer ${token}`; } diff --git a/src/pages/auth/service/AuthService.ts b/src/pages/auth/service/AuthService.ts index 819aaa2..834620b 100644 --- a/src/pages/auth/service/AuthService.ts +++ b/src/pages/auth/service/AuthService.ts @@ -40,7 +40,7 @@ export const register = async (params: RegisterType) => { }; export const refreshToken = async (params: RefreshTokenType) => { - const { data } = await axios.post(`/auth/refresh`, params); + const { data } = await axios.post(`/public/auth/refresh`, params); return data; }; diff --git a/src/shared/types/Types.ts b/src/shared/types/Types.ts index c8cf3eb..68ebde0 100644 --- a/src/shared/types/Types.ts +++ b/src/shared/types/Types.ts @@ -1,3 +1,5 @@ +import type { DefaultError } from "@tanstack/react-query"; + export type SharedStoreType = { openSidebar: boolean; setOpenSidebar: (open: boolean) => void; @@ -14,3 +16,21 @@ export interface BaseResponse { totalPages: number; }; } + +interface IError { + message: string[]; +} + +type MyErrorRespone = { + error: IError; +}; + +export type IErrorResponse = BaseResponse; + +export interface IApiErrorRepsonse extends DefaultError { + response?: IErrorResponse; + config?: { + url?: string; + [key: string]: unknown; + }; +}