fix: update auth provider
This commit is contained in:
@@ -1,8 +1,13 @@
|
|||||||
// useAuth.tsx
|
|
||||||
import { AuthContext, type UserInfo } from '@/contexts/AuthContext';
|
import { AuthContext, type UserInfo } from '@/contexts/AuthContext';
|
||||||
import type { GenerateTokenResponse } from '@/features/authentication/api/identityAPI';
|
import type { GenerateTokenResponse } from '@/features/authentication/api/identityAPI';
|
||||||
import axios from 'axios';
|
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 { jwtDecode } from 'jwt-decode';
|
||||||
import type { GUID } from '@/types/commonTypes';
|
import type { GUID } from '@/types/commonTypes';
|
||||||
|
|
||||||
@@ -27,157 +32,149 @@ export interface AccessTokenJwtPayload {
|
|||||||
jti: string;
|
jti: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ACCESS_TOKEN_KEY: 'access_token' = 'access_token' as const;
|
export const ACCESS_TOKEN_KEY = 'access_token';
|
||||||
export const REFRESH_TOKEN_KEY: 'refresh_token' = 'refresh_token' as const;
|
export const REFRESH_TOKEN_KEY = 'refresh_token';
|
||||||
export const EXPIRES_IN_KEY: 'expires_in' = 'expires_in' as const;
|
export const EXPIRES_IN_KEY = 'expires_in';
|
||||||
|
|
||||||
let inMemoryToken: string | null = null;
|
|
||||||
let expiresAt = 0;
|
|
||||||
|
|
||||||
export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||||||
const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
|
const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
|
||||||
const [authFinished, setAuthFinished] = useState<boolean>(false);
|
const [authFinished, setAuthFinished] = useState<boolean>(false);
|
||||||
const [accessToken, setAccessToken] = useState<string | null>(
|
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||||
sessionStorage.getItem(ACCESS_TOKEN_KEY),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Initialize from sessionStorage (page reload)
|
const refreshPromiseRef = useRef<Promise<string | null> | null>(null);
|
||||||
useEffect(() => {
|
const expiresAtRef = useRef<number>(0);
|
||||||
const handleAndSetToken = async () => {
|
|
||||||
const token = sessionStorage.getItem(ACCESS_TOKEN_KEY);
|
|
||||||
|
|
||||||
if (token) {
|
const extractUserFromToken = useCallback((token: string) => {
|
||||||
setUserInfoFromToken(token);
|
try {
|
||||||
await refreshAccessToken();
|
const decoded = jwtDecode<AccessTokenJwtPayload>(token);
|
||||||
inMemoryToken = token;
|
setUserInfo({
|
||||||
expiresAt = Number(sessionStorage.getItem(EXPIRES_IN_KEY) || 0);
|
picture: decoded.picture,
|
||||||
setAccessToken(token);
|
firstName: decoded.given_name,
|
||||||
|
lastName: decoded.family_name,
|
||||||
setAuthFinished(true);
|
fullName: decoded.name,
|
||||||
|
email: decoded.email,
|
||||||
|
phoneNumber: decoded.phone_number,
|
||||||
|
userID: decoded.sub,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to decode token', e);
|
||||||
|
setUserInfo(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
setAuthFinished(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleAndSetToken();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Background refresh
|
const logout = useCallback(() => {
|
||||||
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();
|
|
||||||
setAccessToken(null);
|
setAccessToken(null);
|
||||||
}
|
setUserInfo(null);
|
||||||
|
expiresAtRef.current = 0;
|
||||||
|
sessionStorage.removeItem(ACCESS_TOKEN_KEY);
|
||||||
|
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
||||||
|
}, []);
|
||||||
|
|
||||||
function getToken() {
|
const performRefresh = async (): Promise<string | null> => {
|
||||||
return inMemoryToken;
|
const refreshToken = localStorage.getItem(REFRESH_TOKEN_KEY);
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshAccessToken() {
|
|
||||||
const refreshToken = sessionStorage.getItem(REFRESH_TOKEN_KEY);
|
|
||||||
if (!refreshToken) {
|
if (!refreshToken) {
|
||||||
logout();
|
logout();
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const activeRefreshSession: string | null =
|
|
||||||
sessionStorage.getItem('active-refresh');
|
|
||||||
|
|
||||||
if (
|
|
||||||
!activeRefreshSession ||
|
|
||||||
(activeRefreshSession && JSON.parse(activeRefreshSession) === false)
|
|
||||||
) {
|
|
||||||
sessionStorage.setItem('active-refresh', JSON.stringify(true));
|
|
||||||
|
|
||||||
const result = await axios.post<GenerateTokenResponse>(
|
const result = await axios.post<GenerateTokenResponse>(
|
||||||
import.meta.env.VITE_IDENTITY_URL,
|
import.meta.env.VITE_IDENTITY_URL,
|
||||||
new URLSearchParams({
|
new URLSearchParams({
|
||||||
grant_type: 'refresh_token',
|
grant_type: 'refresh_token',
|
||||||
refresh_token: refreshToken,
|
refresh_token: refreshToken,
|
||||||
client_id: import.meta.env.VITE_IDENTITY_CLIENT_ID, // from your token payload
|
client_id: import.meta.env.VITE_IDENTITY_CLIENT_ID,
|
||||||
}),
|
}),
|
||||||
{
|
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } },
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.data.access_token) {
|
const newAccessToken = result.data.access_token;
|
||||||
inMemoryToken = result.data.access_token;
|
const newRefreshToken = result.data.refresh_token;
|
||||||
expiresAt = Date.now() + result.data.expires_in * 1000;
|
|
||||||
|
|
||||||
sessionStorage.setItem(ACCESS_TOKEN_KEY, inMemoryToken as string);
|
localStorage.setItem(REFRESH_TOKEN_KEY, newRefreshToken);
|
||||||
sessionStorage.setItem(REFRESH_TOKEN_KEY, result.data.refresh_token);
|
sessionStorage.setItem(ACCESS_TOKEN_KEY, newAccessToken);
|
||||||
sessionStorage.setItem(EXPIRES_IN_KEY, String(expiresAt));
|
|
||||||
|
|
||||||
setAccessToken(inMemoryToken);
|
expiresAtRef.current = Date.now() + result.data.expires_in * 1000;
|
||||||
} else {
|
setAccessToken(newAccessToken);
|
||||||
|
extractUserFromToken(newAccessToken);
|
||||||
|
|
||||||
|
return newAccessToken;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Refresh failed', error);
|
||||||
logout();
|
logout();
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionStorage.setItem('active-refresh', JSON.stringify(false));
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
logout();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setUserInfoFromToken(token: string) {
|
|
||||||
const accessTokenPayload = jwtDecode<AccessTokenJwtPayload>(token);
|
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
setUserInfo(userInfo);
|
const getOrRefreshAccessToken = async () => {
|
||||||
|
// If we have a valid token in memory, return it
|
||||||
|
if (accessToken && Date.now() < expiresAtRef.current - 60000) {
|
||||||
|
return accessToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
};
|
||||||
|
|
||||||
|
initAuth();
|
||||||
|
}, [extractUserFromToken]);
|
||||||
|
|
||||||
|
// INTERVAL CHECK
|
||||||
|
useEffect(() => {
|
||||||
|
if (!accessToken) return;
|
||||||
|
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
getOrRefreshAccessToken();
|
||||||
|
}, 30_000);
|
||||||
|
|
||||||
|
return () => clearInterval(intervalId);
|
||||||
|
}, [accessToken]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider
|
<AuthContext.Provider
|
||||||
value={{
|
value={{
|
||||||
accessToken,
|
accessToken,
|
||||||
getToken,
|
getToken: () => accessToken,
|
||||||
login,
|
login,
|
||||||
logout,
|
logout,
|
||||||
authFinished,
|
authFinished,
|
||||||
|
|||||||
Reference in New Issue
Block a user