| @@ -14,6 +14,7 @@ import {useNavigate} from "react-router-dom"; | |||||
| import {useDispatch} from "react-redux"; | import {useDispatch} from "react-redux"; | ||||
| import { LOGOUT, REFRESH_TOKEN } from 'utils/ApiPathConst'; | import { LOGOUT, REFRESH_TOKEN } from 'utils/ApiPathConst'; | ||||
| import { getMessage } from 'utils/getI18nMessage'; | import { getMessage } from 'utils/getI18nMessage'; | ||||
| import { beginRefresh } from 'auth/refreshGate'; | |||||
| // Guard so we only register interceptors once (ThemeRoutes re-renders add duplicate handlers otherwise) | // Guard so we only register interceptors once (ThemeRoutes re-renders add duplicate handlers otherwise) | ||||
| let axiosInterceptorsSetup = false; | let axiosInterceptorsSetup = false; | ||||
| @@ -192,10 +193,10 @@ export const SetupAxiosInterceptors = () => { | |||||
| return Promise.reject(error); | return Promise.reject(error); | ||||
| } | } | ||||
| isRefreshToken = true; | isRefreshToken = true; | ||||
| return axios | |||||
| return beginRefresh(() => axios | |||||
| .post(`${apiPath}${REFRESH_TOKEN}`, { | .post(`${apiPath}${REFRESH_TOKEN}`, { | ||||
| refreshToken: refreshToken // Replace with your refresh token | |||||
| }) | |||||
| refreshToken: refreshToken | |||||
| })) | |||||
| .then((response) => { | .then((response) => { | ||||
| if (response.status === 200) { | if (response.status === 200) { | ||||
| const newAccessToken = response.data.accessToken; | const newAccessToken = response.data.accessToken; | ||||
| @@ -261,36 +262,17 @@ export const SetupAxiosInterceptors = () => { | |||||
| } | } | ||||
| export const handleRefreshTokenFunction = () => { | export const handleRefreshTokenFunction = () => { | ||||
| // const token = localStorage.getItem('accessToken'); | |||||
| let isRefresh = false; | |||||
| if (!isRefresh) { | |||||
| const refreshToken = localStorage.getItem('refreshToken'); | |||||
| isRefresh = true; | |||||
| axios.post(`${apiPath}${REFRESH_TOKEN}`, { | |||||
| refreshToken: refreshToken | |||||
| }).then((response) => { | |||||
| if (response.status === 200) { | |||||
| const newAccessToken = response.data.accessToken; | |||||
| const newRefreshToken = response.data.refreshToken; | |||||
| localStorage.setItem('accessToken', newAccessToken); | |||||
| localStorage.setItem('refreshToken', newRefreshToken); | |||||
| // token = newAccessToken; | |||||
| isRefresh = false; | |||||
| } else { | |||||
| // token = null; | |||||
| isRefresh = false; | |||||
| } | |||||
| }) | |||||
| .catch((refreshError) => { | |||||
| console.log('Failed to refresh token'); | |||||
| if (refreshError != undefined){ | |||||
| console.log(refreshError) | |||||
| } | |||||
| // token = null | |||||
| isRefresh = false; | |||||
| }); | |||||
| const refreshToken = localStorage.getItem('refreshToken'); | |||||
| if (!refreshToken) { | |||||
| return false; | |||||
| } | } | ||||
| return isRefresh; | |||||
| beginRefresh(() => axios.post(`${apiPath}${REFRESH_TOKEN}`, { | |||||
| refreshToken: refreshToken | |||||
| })).catch((refreshError) => { | |||||
| console.log('Failed to refresh token'); | |||||
| if (refreshError != undefined) { | |||||
| console.log(refreshError) | |||||
| } | |||||
| }); | |||||
| return true; | |||||
| } | } | ||||
| @@ -1,5 +1,6 @@ | |||||
| import axios from 'axios' | import axios from 'axios' | ||||
| import jwtDefaultConfig from './jwtDefaultConfig' | import jwtDefaultConfig from './jwtDefaultConfig' | ||||
| import { beginRefresh } from '../refreshGate' | |||||
| export default class JwtService { | export default class JwtService { | ||||
| // ** jwtConfig <= Will be used by this service | // ** jwtConfig <= Will be used by this service | ||||
| @@ -126,8 +127,8 @@ export default class JwtService { | |||||
| } | } | ||||
| refreshToken() { | refreshToken() { | ||||
| return axios.post(this.jwtConfig.refreshEndpoint, { | |||||
| return beginRefresh(() => axios.post(this.jwtConfig.refreshEndpoint, { | |||||
| refreshToken: this.getRefreshToken() | refreshToken: this.getRefreshToken() | ||||
| }) | |||||
| })) | |||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,71 @@ | |||||
| const LOCK_KEY = 'pnspsRefreshLock'; | |||||
| const LOCK_MS = 15000; | |||||
| let inFlight = null; | |||||
| function waitForStoredToken(previousRefreshToken) { | |||||
| return new Promise((resolve, reject) => { | |||||
| const started = Date.now(); | |||||
| const timer = setInterval(() => { | |||||
| const refreshToken = localStorage.getItem('refreshToken'); | |||||
| const accessToken = localStorage.getItem('accessToken'); | |||||
| if (refreshToken && accessToken && refreshToken !== previousRefreshToken) { | |||||
| clearInterval(timer); | |||||
| resolve({ | |||||
| status: 200, | |||||
| data: { accessToken, refreshToken } | |||||
| }); | |||||
| return; | |||||
| } | |||||
| const lockGone = !localStorage.getItem(LOCK_KEY); | |||||
| if ((lockGone && Date.now() - started > 500) || Date.now() - started > LOCK_MS) { | |||||
| clearInterval(timer); | |||||
| reject(new Error('refresh-lock-released')); | |||||
| } | |||||
| }, 200); | |||||
| }); | |||||
| } | |||||
| export function storeRefreshedTokens(accessToken, refreshToken) { | |||||
| if (accessToken) { | |||||
| localStorage.setItem('accessToken', accessToken); | |||||
| localStorage.setItem('axiosToken', 'Bearer ' + accessToken); | |||||
| } | |||||
| if (refreshToken) { | |||||
| localStorage.setItem('refreshToken', refreshToken); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * One refresh at a time in this tab, and a short lock so a second tab waits | |||||
| * for the new token instead of posting the same refresh token again. | |||||
| */ | |||||
| export function beginRefresh(run) { | |||||
| if (inFlight) { | |||||
| return inFlight; | |||||
| } | |||||
| const previousRefreshToken = localStorage.getItem('refreshToken'); | |||||
| inFlight = (async () => { | |||||
| const lockedAt = Number(localStorage.getItem(LOCK_KEY) || 0); | |||||
| if (lockedAt && Date.now() - lockedAt < LOCK_MS) { | |||||
| try { | |||||
| return await waitForStoredToken(previousRefreshToken); | |||||
| } catch (e) { | |||||
| // The other tab did not store a new token. Refresh from here. | |||||
| } | |||||
| } | |||||
| localStorage.setItem(LOCK_KEY, String(Date.now())); | |||||
| try { | |||||
| const response = await run(); | |||||
| if (response?.status === 200 && response.data) { | |||||
| storeRefreshedTokens(response.data.accessToken, response.data.refreshToken); | |||||
| } | |||||
| return response; | |||||
| } finally { | |||||
| localStorage.removeItem(LOCK_KEY); | |||||
| } | |||||
| })().finally(() => { | |||||
| inFlight = null; | |||||
| }); | |||||
| return inFlight; | |||||
| } | |||||
| @@ -2,6 +2,7 @@ import { createContext, useEffect, useRef, useCallback } from 'react'; | |||||
| import { apiPath } from 'auth/utils'; | import { apiPath } from 'auth/utils'; | ||||
| import { REFRESH_TOKEN } from 'utils/ApiPathConst'; | import { REFRESH_TOKEN } from 'utils/ApiPathConst'; | ||||
| import axios from 'axios'; | import axios from 'axios'; | ||||
| import { beginRefresh } from 'auth/refreshGate'; | |||||
| import { | import { | ||||
| isUserLoggedIn, | isUserLoggedIn, | ||||
| } from "utils/Utils"; | } from "utils/Utils"; | ||||
| @@ -16,10 +17,10 @@ const RefreshTokenProvider = ({ children }) => { | |||||
| if (!isRefresh.current) { | if (!isRefresh.current) { | ||||
| const refreshToken = localStorage.getItem('refreshToken'); | const refreshToken = localStorage.getItem('refreshToken'); | ||||
| isRefresh.current = true; | isRefresh.current = true; | ||||
| axios | |||||
| beginRefresh(() => axios | |||||
| .post(`${apiPath}${REFRESH_TOKEN}`, { | .post(`${apiPath}${REFRESH_TOKEN}`, { | ||||
| refreshToken: refreshToken | refreshToken: refreshToken | ||||
| }) | |||||
| })) | |||||
| .then((response) => { | .then((response) => { | ||||
| if (response.status === 200) { | if (response.status === 200) { | ||||
| const newAccessToken = response.data.accessToken; | const newAccessToken = response.data.accessToken; | ||||