From 33c72569c71d26e3746b5da78b4d5f9d22c23271 Mon Sep 17 00:00:00 2001 From: Sajad Mirjalili Date: Sat, 29 Nov 2025 11:08:49 +0330 Subject: [PATCH] fix: update auth provider --- src/providers/AuthProvider.tsx | 241 ++++++++++++++++----------------- 1 file changed, 119 insertions(+), 122 deletions(-) diff --git a/src/providers/AuthProvider.tsx b/src/providers/AuthProvider.tsx index 24bf1e2..aa790d5 100644 --- a/src/providers/AuthProvider.tsx +++ b/src/providers/AuthProvider.tsx @@ -1,8 +1,13 @@ -// useAuth.tsx import { AuthContext, type UserInfo } from '@/contexts/AuthContext'; import type { GenerateTokenResponse } from '@/features/authentication/api/identityAPI'; import axios from 'axios'; -import { useEffect, useState, type ReactNode } from 'react'; +import { + useCallback, + useEffect, + useRef, + useState, + type ReactNode, +} from 'react'; import { jwtDecode } from 'jwt-decode'; import type { GUID } from '@/types/commonTypes'; @@ -27,157 +32,149 @@ export interface AccessTokenJwtPayload { jti: string; } -export const ACCESS_TOKEN_KEY: 'access_token' = 'access_token' as const; -export const REFRESH_TOKEN_KEY: 'refresh_token' = 'refresh_token' as const; -export const EXPIRES_IN_KEY: 'expires_in' = 'expires_in' as const; - -let inMemoryToken: string | null = null; -let expiresAt = 0; +export const ACCESS_TOKEN_KEY = 'access_token'; +export const REFRESH_TOKEN_KEY = 'refresh_token'; +export const EXPIRES_IN_KEY = 'expires_in'; export const AuthProvider = ({ children }: { children: ReactNode }) => { const [userInfo, setUserInfo] = useState(null); const [authFinished, setAuthFinished] = useState(false); - const [accessToken, setAccessToken] = useState( - sessionStorage.getItem(ACCESS_TOKEN_KEY), - ); + const [accessToken, setAccessToken] = useState(null); - // Initialize from sessionStorage (page reload) - useEffect(() => { - const handleAndSetToken = async () => { - const token = sessionStorage.getItem(ACCESS_TOKEN_KEY); + const refreshPromiseRef = useRef | null>(null); + const expiresAtRef = useRef(0); - if (token) { - setUserInfoFromToken(token); - await refreshAccessToken(); - inMemoryToken = token; - expiresAt = Number(sessionStorage.getItem(EXPIRES_IN_KEY) || 0); - setAccessToken(token); - - setAuthFinished(true); - } - - setAuthFinished(true); - }; - - handleAndSetToken(); + const extractUserFromToken = useCallback((token: string) => { + try { + const decoded = jwtDecode(token); + setUserInfo({ + picture: decoded.picture, + firstName: decoded.given_name, + lastName: decoded.family_name, + fullName: decoded.name, + email: decoded.email, + phoneNumber: decoded.phone_number, + userID: decoded.sub, + }); + } catch (e) { + console.error('Failed to decode token', e); + setUserInfo(null); + } }, []); - // Background refresh - useEffect(() => { - const handleRefreshTokenIfNeeded = async () => { - if (!inMemoryToken) return; - if (Date.now() < expiresAt - 60_000) return; // still valid (buffer) - - await refreshAccessToken(); - setAccessToken(inMemoryToken); - }; - - const interval = setInterval(async () => { - await handleRefreshTokenIfNeeded(); - }, 30_000); - - return () => clearInterval(interval); - }, []); - - function login(tokens: { - access_token: string; - refresh_token: string; - expires_in: number; - }) { - setUserInfoFromToken(tokens.access_token); - inMemoryToken = tokens.access_token; - expiresAt = Date.now() + tokens.expires_in * 1000; - - sessionStorage.setItem(ACCESS_TOKEN_KEY, tokens.access_token); - sessionStorage.setItem(REFRESH_TOKEN_KEY, tokens.refresh_token); - sessionStorage.setItem(EXPIRES_IN_KEY, String(expiresAt)); - - setAccessToken(tokens.access_token); - } - - function logout() { - inMemoryToken = null; - expiresAt = 0; - sessionStorage.clear(); + const logout = useCallback(() => { setAccessToken(null); - } + setUserInfo(null); + expiresAtRef.current = 0; + sessionStorage.removeItem(ACCESS_TOKEN_KEY); + localStorage.removeItem(REFRESH_TOKEN_KEY); + }, []); - function getToken() { - return inMemoryToken; - } + const performRefresh = async (): Promise => { + const refreshToken = localStorage.getItem(REFRESH_TOKEN_KEY); - async function refreshAccessToken() { - const refreshToken = sessionStorage.getItem(REFRESH_TOKEN_KEY); if (!refreshToken) { logout(); - return; + return null; } try { - const activeRefreshSession: string | null = - sessionStorage.getItem('active-refresh'); + const result = await axios.post( + import.meta.env.VITE_IDENTITY_URL, + new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: refreshToken, + client_id: import.meta.env.VITE_IDENTITY_CLIENT_ID, + }), + { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }, + ); - if ( - !activeRefreshSession || - (activeRefreshSession && JSON.parse(activeRefreshSession) === false) - ) { - sessionStorage.setItem('active-refresh', JSON.stringify(true)); + const newAccessToken = result.data.access_token; + const newRefreshToken = result.data.refresh_token; - const result = await axios.post( - import.meta.env.VITE_IDENTITY_URL, - new URLSearchParams({ - grant_type: 'refresh_token', - refresh_token: refreshToken, - client_id: import.meta.env.VITE_IDENTITY_CLIENT_ID, // from your token payload - }), - { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - }, - ); + localStorage.setItem(REFRESH_TOKEN_KEY, newRefreshToken); + sessionStorage.setItem(ACCESS_TOKEN_KEY, newAccessToken); - if (result.data.access_token) { - inMemoryToken = result.data.access_token; - expiresAt = Date.now() + result.data.expires_in * 1000; + expiresAtRef.current = Date.now() + result.data.expires_in * 1000; + setAccessToken(newAccessToken); + extractUserFromToken(newAccessToken); - sessionStorage.setItem(ACCESS_TOKEN_KEY, inMemoryToken as string); - sessionStorage.setItem(REFRESH_TOKEN_KEY, result.data.refresh_token); - sessionStorage.setItem(EXPIRES_IN_KEY, String(expiresAt)); - - setAccessToken(inMemoryToken); - } else { - logout(); - } - - sessionStorage.setItem('active-refresh', JSON.stringify(false)); - } - } catch { + return newAccessToken; + } catch (error) { + console.error('Refresh failed', error); logout(); + return null; } - } + }; - function setUserInfoFromToken(token: string) { - const accessTokenPayload = jwtDecode(token); + const getOrRefreshAccessToken = async () => { + // If we have a valid token in memory, return it + if (accessToken && Date.now() < expiresAtRef.current - 60000) { + return accessToken; + } - const userInfo: UserInfo = { - picture: accessTokenPayload.picture, - firstName: accessTokenPayload.given_name, - lastName: accessTokenPayload.family_name, - fullName: accessTokenPayload.name, - email: accessTokenPayload.email, - phoneNumber: accessTokenPayload.phone_number, - userID: accessTokenPayload.sub, + // If a refresh is already happening, return that existing promise + if (refreshPromiseRef.current) { + return refreshPromiseRef.current; + } + + // Otherwise, start a new refresh + refreshPromiseRef.current = performRefresh().finally(() => { + refreshPromiseRef.current = null; + }); + + return refreshPromiseRef.current; + }; + + const login = (tokens: { + access_token: string; + refresh_token: string; + expires_in: number; + }) => { + setAccessToken(tokens.access_token); + extractUserFromToken(tokens.access_token); + + expiresAtRef.current = Date.now() + tokens.expires_in * 1000; + + localStorage.setItem(REFRESH_TOKEN_KEY, tokens.refresh_token); + sessionStorage.setItem(ACCESS_TOKEN_KEY, tokens.access_token); + }; + + // INITIALIZATION + useEffect(() => { + const initAuth = async () => { + const persistedRefreshToken = localStorage.getItem(REFRESH_TOKEN_KEY); + const persistedAccessToken = sessionStorage.getItem(ACCESS_TOKEN_KEY); + + if (persistedRefreshToken) { + if (persistedAccessToken) { + setAccessToken(persistedAccessToken); + extractUserFromToken(persistedAccessToken); + } + await getOrRefreshAccessToken(); + } + setAuthFinished(true); }; - setUserInfo(userInfo); - } + initAuth(); + }, [extractUserFromToken]); + + // INTERVAL CHECK + useEffect(() => { + if (!accessToken) return; + + const intervalId = setInterval(() => { + getOrRefreshAccessToken(); + }, 30_000); + + return () => clearInterval(intervalId); + }, [accessToken]); return ( accessToken, login, logout, authFinished,