fix: update auth provider

This commit is contained in:
Sajad Mirjalili
2025-11-29 11:08:49 +03:30
parent 0f07e8436d
commit 33c72569c7

View File

@@ -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<UserInfo | null>(null);
const [authFinished, setAuthFinished] = useState<boolean>(false);
const [accessToken, setAccessToken] = useState<string | null>(
sessionStorage.getItem(ACCESS_TOKEN_KEY),
);
const [accessToken, setAccessToken] = useState<string | null>(null);
// Initialize from sessionStorage (page reload)
useEffect(() => {
const handleAndSetToken = async () => {
const token = sessionStorage.getItem(ACCESS_TOKEN_KEY);
const refreshPromiseRef = useRef<Promise<string | null> | null>(null);
const expiresAtRef = useRef<number>(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<AccessTokenJwtPayload>(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<string | null> => {
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<GenerateTokenResponse>(
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<GenerateTokenResponse>(
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<AccessTokenJwtPayload>(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 (
<AuthContext.Provider
value={{
accessToken,
getToken,
getToken: () => accessToken,
login,
logout,
authFinished,