| @@ -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) | |||
| 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 | |||
| export const handleLogin = data => { | |||
| return dispatch => { | |||
| @@ -152,6 +162,12 @@ export const SetupAxiosInterceptors = () => { | |||
| // const { config, response: { status } } = error | |||
| const status = error.response?.status | |||
| 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) | |||
| if ( | |||
| status === 401 && | |||
| @@ -160,11 +176,15 @@ export const SetupAxiosInterceptors = () => { | |||
| ) { | |||
| 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 | |||
| const refreshToken = localStorage.getItem('refreshToken'); | |||
| if (isRefreshToken) { | |||
| return; | |||
| return Promise.reject(error); | |||
| } | |||
| isRefreshToken = true; | |||
| return axios | |||
| @@ -217,7 +237,7 @@ export const SetupAxiosInterceptors = () => { | |||
| // } | |||
| // } | |||
| if (localStorage.getItem("expiredAlertShown")) { | |||
| if (localStorage.getItem("expiredAlertShown") && !isPublicAuthRequest(reqUrl)) { | |||
| await dispatch(handleLogoutFunction()); | |||
| await navigate('/login'); | |||
| await window.location.reload(); | |||
| @@ -40,29 +40,46 @@ export default class JwtService { | |||
| // ** const { config, response: { status } } = error | |||
| const {config, response} = error | |||
| const originalRequest = config | |||
| const reqUrl = config?.url || '' | |||
| // ** if (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) { | |||
| 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 | |||
| } | |||
| @@ -72,11 +89,17 @@ export default class JwtService { | |||
| } | |||
| 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() { | |||
| @@ -40,7 +40,7 @@ import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons'; | |||
| // import axios from "axios"; | |||
| import { useDispatch } from "react-redux"; | |||
| import { handleLogin } from "auth/index"; | |||
| import { handleLogin, clearExpiredSessionAlert } from "auth/index"; | |||
| import useJwt from "auth/jwt/useJwt"; | |||
| import { FormattedMessage, useIntl } from "react-intl"; | |||
| import { SysContext } from "components/SysSettingProvider" | |||
| @@ -74,6 +74,10 @@ const AuthLoginCustom = () => { | |||
| /** Blocks a second login before React re-renders (double-click / rapid taps). */ | |||
| const loginInFlightRef = useRef(false); | |||
| useEffect(() => { | |||
| clearExpiredSessionAlert(); | |||
| }, []); | |||
| const handleMouseDownPassword = (event) => { | |||
| event.preventDefault(); | |||
| }; | |||
| @@ -84,6 +88,7 @@ const AuthLoginCustom = () => { | |||
| return; | |||
| } | |||
| loginInFlightRef.current = true; | |||
| clearExpiredSessionAlert(); | |||
| setOnLogin(true) | |||
| useJwt | |||
| .login({ username: values.username, password: values.password }) | |||