| @@ -20,6 +20,16 @@ let axiosInterceptorsSetup = false; | |||||
| // In-memory guard so only one 401/logout flow shows the alert (avoids race when multiple 401s or interceptors run) | // In-memory guard so only one 401/logout flow shows the alert (avoids race when multiple 401s or interceptors run) | ||||
| let expiredAlertShownInMemory = false; | let expiredAlertShownInMemory = false; | ||||
| /** Login / public auth endpoints must not trigger token refresh or session-expiry reload. */ | |||||
| const isPublicAuthRequest = (reqUrl) => | |||||
| reqUrl.includes('/login') || reqUrl.includes('/ldap-login'); | |||||
| /** Clear stale session-expiry flag so a new login attempt can show its own error dialog. */ | |||||
| export const clearExpiredSessionAlert = () => { | |||||
| localStorage.removeItem('expiredAlertShown'); | |||||
| expiredAlertShownInMemory = false; | |||||
| }; | |||||
| // ** Handle User Login | // ** Handle User Login | ||||
| export const handleLogin = data => { | export const handleLogin = data => { | ||||
| return dispatch => { | return dispatch => { | ||||
| @@ -152,6 +162,12 @@ export const SetupAxiosInterceptors = () => { | |||||
| // const { config, response: { status } } = error | // const { config, response: { status } } = error | ||||
| const status = error.response?.status | const status = error.response?.status | ||||
| const reqUrl = error.config?.url || '' | const reqUrl = error.config?.url || '' | ||||
| // Let login forms handle their own errors (wrong password, locked account, etc.) | |||||
| if (isPublicAuthRequest(reqUrl)) { | |||||
| return Promise.reject(error) | |||||
| } | |||||
| // iAM Smart registration: HKID already linked — caller must show /iamsmart/pleaseLogin (not refresh/logout) | // iAM Smart registration: HKID already linked — caller must show /iamsmart/pleaseLogin (not refresh/logout) | ||||
| if ( | if ( | ||||
| status === 401 && | status === 401 && | ||||
| @@ -160,11 +176,15 @@ export const SetupAxiosInterceptors = () => { | |||||
| ) { | ) { | ||||
| return Promise.reject(error) | return Promise.reject(error) | ||||
| } | } | ||||
| if (status === 401 && error.config?.url !== apiPath + REFRESH_TOKEN) { | |||||
| if ( | |||||
| status === 401 && | |||||
| !isPublicAuthRequest(reqUrl) && | |||||
| error.config?.url !== apiPath + REFRESH_TOKEN | |||||
| ) { | |||||
| // Make a request to refresh the access token | // Make a request to refresh the access token | ||||
| const refreshToken = localStorage.getItem('refreshToken'); | const refreshToken = localStorage.getItem('refreshToken'); | ||||
| if (isRefreshToken) { | if (isRefreshToken) { | ||||
| return; | |||||
| return Promise.reject(error); | |||||
| } | } | ||||
| isRefreshToken = true; | isRefreshToken = true; | ||||
| return axios | return axios | ||||
| @@ -217,7 +237,7 @@ export const SetupAxiosInterceptors = () => { | |||||
| // } | // } | ||||
| // } | // } | ||||
| if (localStorage.getItem("expiredAlertShown")) { | |||||
| if (localStorage.getItem("expiredAlertShown") && !isPublicAuthRequest(reqUrl)) { | |||||
| await dispatch(handleLogoutFunction()); | await dispatch(handleLogoutFunction()); | ||||
| await navigate('/login'); | await navigate('/login'); | ||||
| await window.location.reload(); | await window.location.reload(); | ||||
| @@ -40,29 +40,46 @@ export default class JwtService { | |||||
| // ** const { config, response: { status } } = error | // ** const { config, response: { status } } = error | ||||
| const {config, response} = error | const {config, response} = error | ||||
| const originalRequest = config | const originalRequest = config | ||||
| const reqUrl = config?.url || '' | |||||
| // ** if (status === 401) { | // ** if (status === 401) { | ||||
| if (response && response.status === 401) { | if (response && response.status === 401) { | ||||
| const isPublicAuthRequest = | |||||
| reqUrl.includes('/login') || | |||||
| reqUrl.includes('/ldap-login') || | |||||
| reqUrl.includes('/refresh-token') | |||||
| if (isPublicAuthRequest) { | |||||
| return Promise.reject(error) | |||||
| } | |||||
| if (!this.isAlreadyFetchingAccessToken) { | if (!this.isAlreadyFetchingAccessToken) { | ||||
| this.isAlreadyFetchingAccessToken = true | this.isAlreadyFetchingAccessToken = true | ||||
| this.refreshToken().then(r => { | |||||
| this.isAlreadyFetchingAccessToken = false | |||||
| // ** Update accessToken in localStorage | |||||
| this.setToken(r.data.accessToken) | |||||
| this.setRefreshToken(r.data.refreshToken) | |||||
| this.onAccessTokenFetched(r.data.accessToken) | |||||
| }) | |||||
| this.refreshToken() | |||||
| .then(r => { | |||||
| this.isAlreadyFetchingAccessToken = false | |||||
| // ** Update accessToken in localStorage | |||||
| this.setToken(r.data.accessToken) | |||||
| this.setRefreshToken(r.data.refreshToken) | |||||
| this.onAccessTokenFetched(r.data.accessToken) | |||||
| }) | |||||
| .catch(refreshError => { | |||||
| this.isAlreadyFetchingAccessToken = false | |||||
| this.onAccessTokenFetchFailed(refreshError) | |||||
| }) | |||||
| } | } | ||||
| const retryOriginalRequest = new Promise(resolve => { | |||||
| this.addSubscriber(accessToken => { | |||||
| // ** Make sure to assign accessToken according to your response. | |||||
| // ** Check: https://pixinvent.ticksy.com/ticket/2413870 | |||||
| // ** Change Authorization header | |||||
| originalRequest.headers.Authorization = `${this.jwtConfig.tokenType} ${accessToken}` | |||||
| resolve(this.axios(originalRequest)) | |||||
| }) | |||||
| const retryOriginalRequest = new Promise((resolve, reject) => { | |||||
| this.addSubscriber( | |||||
| accessToken => { | |||||
| // ** Make sure to assign accessToken according to your response. | |||||
| // ** Check: https://pixinvent.ticksy.com/ticket/2413870 | |||||
| // ** Change Authorization header | |||||
| originalRequest.headers.Authorization = `${this.jwtConfig.tokenType} ${accessToken}` | |||||
| resolve(axios(originalRequest)) | |||||
| }, | |||||
| reject | |||||
| ) | |||||
| }) | }) | ||||
| return retryOriginalRequest | return retryOriginalRequest | ||||
| } | } | ||||
| @@ -72,11 +89,17 @@ export default class JwtService { | |||||
| } | } | ||||
| onAccessTokenFetched(accessToken) { | onAccessTokenFetched(accessToken) { | ||||
| this.subscribers = this.subscribers.filter(callback => callback(accessToken)) | |||||
| this.subscribers.forEach(({ onSuccess }) => onSuccess(accessToken)) | |||||
| this.subscribers = [] | |||||
| } | |||||
| onAccessTokenFetchFailed(refreshError) { | |||||
| this.subscribers.forEach(({ onFailure }) => onFailure(refreshError)) | |||||
| this.subscribers = [] | |||||
| } | } | ||||
| addSubscriber(callback) { | |||||
| this.subscribers.push(callback) | |||||
| addSubscriber(onSuccess, onFailure) { | |||||
| this.subscribers.push({ onSuccess, onFailure }) | |||||
| } | } | ||||
| getToken() { | getToken() { | ||||
| @@ -40,7 +40,7 @@ import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons'; | |||||
| // import axios from "axios"; | // import axios from "axios"; | ||||
| import { useDispatch } from "react-redux"; | import { useDispatch } from "react-redux"; | ||||
| import { handleLogin } from "auth/index"; | |||||
| import { handleLogin, clearExpiredSessionAlert } from "auth/index"; | |||||
| import useJwt from "auth/jwt/useJwt"; | import useJwt from "auth/jwt/useJwt"; | ||||
| import { FormattedMessage, useIntl } from "react-intl"; | import { FormattedMessage, useIntl } from "react-intl"; | ||||
| import { SysContext } from "components/SysSettingProvider" | import { SysContext } from "components/SysSettingProvider" | ||||
| @@ -74,6 +74,10 @@ const AuthLoginCustom = () => { | |||||
| /** Blocks a second login before React re-renders (double-click / rapid taps). */ | /** Blocks a second login before React re-renders (double-click / rapid taps). */ | ||||
| const loginInFlightRef = useRef(false); | const loginInFlightRef = useRef(false); | ||||
| useEffect(() => { | |||||
| clearExpiredSessionAlert(); | |||||
| }, []); | |||||
| const handleMouseDownPassword = (event) => { | const handleMouseDownPassword = (event) => { | ||||
| event.preventDefault(); | event.preventDefault(); | ||||
| }; | }; | ||||
| @@ -84,6 +88,7 @@ const AuthLoginCustom = () => { | |||||
| return; | return; | ||||
| } | } | ||||
| loginInFlightRef.current = true; | loginInFlightRef.current = true; | ||||
| clearExpiredSessionAlert(); | |||||
| setOnLogin(true) | setOnLogin(true) | ||||
| useJwt | useJwt | ||||
| .login({ username: values.username, password: values.password }) | .login({ username: values.username, password: values.password }) | ||||