fix: update auth provider
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user