| @@ -14,7 +14,7 @@ | |||||
| "REACT_APP_BACKEND_PROTOCOL": "https", | "REACT_APP_BACKEND_PROTOCOL": "https", | ||||
| "REACT_APP_BACKEND_HOST": "forms.lioner.com", | "REACT_APP_BACKEND_HOST": "forms.lioner.com", | ||||
| "REACT_APP_BACKEND_PORT": "8090", | "REACT_APP_BACKEND_PORT": "8090", | ||||
| "REACT_APP_ADOBE_API_KEY": "ee4433f258a74641ae6d502fd41cf703", | |||||
| "REACT_APP_ADOBE_API_KEY": "7f498776ecdf47399d7e123d85b9fe1b", | |||||
| "REACT_APP_BACKEND_API_PATH": "/api" | "REACT_APP_BACKEND_API_PATH": "/api" | ||||
| } | } | ||||
| } | } | ||||
| @@ -6,6 +6,8 @@ | |||||
| "dependencies": { | "dependencies": { | ||||
| "@ant-design/colors": "^6.0.0", | "@ant-design/colors": "^6.0.0", | ||||
| "@ant-design/icons": "^4.7.0", | "@ant-design/icons": "^4.7.0", | ||||
| "@azure/msal-browser": "^4.26.1", | |||||
| "@azure/msal-react": "^3.0.21", | |||||
| "@casl/ability": "^6.5.0", | "@casl/ability": "^6.5.0", | ||||
| "@casl/react": "^3.1.0", | "@casl/react": "^3.1.0", | ||||
| "@emotion/cache": "^11.10.3", | "@emotion/cache": "^11.10.3", | ||||
| @@ -0,0 +1,58 @@ | |||||
| import axios from 'axios'; | |||||
| import { InteractionRequiredAuthError } from "@azure/msal-browser"; | |||||
| // ⚠️ UPDATE THIS SCOPE if needed | |||||
| const apiRequest = { | |||||
| scopes: ["api://d82560a8-8fac-401d-9173-10668acb7dfa/access_as_user"] | |||||
| }; | |||||
| /** | |||||
| * Sets up an Axios interceptor to acquire an MSAL token and handle 401 errors. | |||||
| */ | |||||
| export const setupMsalAxiosInterceptor = (msalInstance, dispatch, handleLogoutFunction) => { | |||||
| // REQUEST Interceptor: Attach the token | |||||
| axios.interceptors.request.use(async (config) => { | |||||
| const accounts = msalInstance.getAllAccounts(); | |||||
| if (accounts.length > 0) { | |||||
| apiRequest.account = accounts[0]; | |||||
| try { | |||||
| // Attempt to acquire the token silently | |||||
| const response = await msalInstance.acquireTokenSilent(apiRequest); | |||||
| config.headers.Authorization = `Bearer ${response.accessToken}`; | |||||
| } catch (error) { | |||||
| if (error instanceof InteractionRequiredAuthError) { | |||||
| console.warn("MSAL: Silent token acquisition failed. Request sent without token."); | |||||
| } else { | |||||
| console.error("MSAL: Token acquisition failed.", error); | |||||
| } | |||||
| } | |||||
| } | |||||
| // Keep your custom header | |||||
| config.headers['X-Authorization'] = process.env.REACT_APP_API_KEY; | |||||
| return config; | |||||
| }, (error) => { | |||||
| return Promise.reject(error); | |||||
| }); | |||||
| // RESPONSE Interceptor: Handle 401 Unauthorized | |||||
| axios.interceptors.response.use( | |||||
| (response) => response, | |||||
| async (error) => { | |||||
| if (error.response && error.response.status === 401) { | |||||
| console.error("401 Unauthorized received. Forcing MSAL logout."); | |||||
| // 1. Clear Redux/local storage state | |||||
| await dispatch(handleLogoutFunction()); | |||||
| // 2. Clear MSAL session and redirect to login | |||||
| msalInstance.logoutRedirect(); | |||||
| } | |||||
| return Promise.reject(error); | |||||
| } | |||||
| ); | |||||
| }; | |||||
| @@ -0,0 +1,44 @@ | |||||
| import axios from 'axios'; | |||||
| // NOTE: This scope MUST match the one used in your AutoLogoutProvider | |||||
| const apiRequest = { | |||||
| scopes: ["api://d82560a8-8fac-401d-9173-10668acb7dfa/access_as_user"] | |||||
| }; | |||||
| /** | |||||
| * Sets up an Axios request interceptor to automatically acquire an MSAL token | |||||
| * and attach it to the Authorization header before sending the request. | |||||
| * @param {object} msalInstance - The PublicClientApplication instance from MSAL. | |||||
| */ | |||||
| export const setupMsalAxiosInterceptor = (msalInstance) => { | |||||
| axios.interceptors.request.use(async (config) => { | |||||
| // 1. Check if user is logged in | |||||
| const accounts = msalInstance.getAllAccounts(); | |||||
| if (accounts.length > 0) { | |||||
| // Set the account for the token request | |||||
| apiRequest.account = accounts[0]; | |||||
| try { | |||||
| // 2. Attempt to acquire the token silently (fastest way) | |||||
| const response = await msalInstance.acquireTokenSilent(apiRequest); | |||||
| // 3. Attach the Access Token to the Authorization header | |||||
| config.headers.Authorization = `Bearer ${response.accessToken}`; | |||||
| } catch (error) { | |||||
| // If silent acquisition fails (e.g., token expired, or user needs interaction), | |||||
| // we allow the request to proceed without a token. The response interceptor | |||||
| // or the AutoLogoutProvider will handle the eventual 401/logout. | |||||
| console.warn("MSAL: Silent token acquisition failed. Request sent without token.", error); | |||||
| } | |||||
| } | |||||
| // 4. Return the (potentially modified) config | |||||
| return config; | |||||
| }, (error) => { | |||||
| return Promise.reject(error); | |||||
| }); | |||||
| // You should use the existing response interceptor in auth/index.js to handle 401s | |||||
| // for non-idle requests, which should call dispatch(handleLogoutFunction()) and instance.logoutRedirect(). | |||||
| }; | |||||
| @@ -0,0 +1,34 @@ | |||||
| import { LogLevel } from '@azure/msal-browser'; | |||||
| // 1. MSAL Configuration (msalConfig) | |||||
| export const msalConfig = { | |||||
| auth: { | |||||
| // Application (client) ID of your React SPA from Azure Portal | |||||
| clientId: "d82560a8-8fac-401d-9173-10668acb7dfa", | |||||
| // Authority URL (e.g., https://login.microsoftonline.com/YOUR_TENANT_ID) | |||||
| authority: "https://login.microsoftonline.com/cb3fc669-059d-4f8c-85eb-5c6564032c53", | |||||
| // Must match the Redirect URI set in the Azure Portal | |||||
| redirectUri: "http://localhost:3000/client", | |||||
| }, | |||||
| cache: { | |||||
| cacheLocation: "sessionStorage", // Recommended location | |||||
| storeAuthStateInCookie: false, | |||||
| }, | |||||
| system: { | |||||
| loggerOptions: { | |||||
| loggerCallback: (level, message, containsPii) => { | |||||
| if (containsPii) { return; } | |||||
| if (level === LogLevel.Error) { console.error(message); } | |||||
| }, | |||||
| piiLoggingEnabled: false, | |||||
| logLevel: LogLevel.Warning, | |||||
| } | |||||
| } | |||||
| }; | |||||
| // 2. Scopes for your Spring Boot API (loginRequest) | |||||
| export const loginRequest = { | |||||
| // This scope tells Azure AD that you want permission to call your API | |||||
| // The scope is typically your API's App ID URI + '/.default' | |||||
| scopes: ["api://d82560a8-8fac-401d-9173-10668acb7dfa/.default"] | |||||
| }; | |||||
| @@ -1,5 +1,8 @@ | |||||
| import React, { createContext, useState, useEffect } from 'react'; | |||||
| import React, { createContext, useState, useEffect, useRef, useCallback} from 'react'; | |||||
| import { useMsal, useIsAuthenticated } from "@azure/msal-react"; | |||||
| import { InteractionRequiredAuthError } from "@azure/msal-browser"; | |||||
| import {handleLogoutFunction} from "../auth"; | import {handleLogoutFunction} from "../auth"; | ||||
| import { useDispatch } from 'react-redux'; | |||||
| import {dispatch} from "../store"; | import {dispatch} from "../store"; | ||||
| import {useNavigate} from "react-router-dom"; | import {useNavigate} from "react-router-dom"; | ||||
| import axios from "axios"; | import axios from "axios"; | ||||
| @@ -12,136 +15,59 @@ import {ChangePasswordWindow} from "../layout/MainLayout/Header/HeaderContent/Pr | |||||
| import {ThemeProvider} from "@emotion/react"; | import {ThemeProvider} from "@emotion/react"; | ||||
| const TimerContext = createContext(); | const TimerContext = createContext(); | ||||
| const IDLE_TIMEOUT_SECONDS = 3600; | |||||
| const AutoLogoutProvider = ({ children }) => { | const AutoLogoutProvider = ({ children }) => { | ||||
| const [lastRequestTime, setLastRequestTime] = useState(Date.now()); | |||||
| const navigate = useNavigate(); | |||||
| const [logoutInterval, setLogoutInterval] = useState(30); | |||||
| const [state, setState] = useState('Active'); | |||||
| const [isTempWindowOpen, setIsTempWindowOpen] = useState(false); | |||||
| const [forceChangePassword, setForceChangePassword] = useState(false); | |||||
| useEffect(() => { | |||||
| const userData = getUserData(); | |||||
| const checked = localStorage.getItem("checkPasswordExpired"); | |||||
| if(userData !== null){ | |||||
| //system user | |||||
| if(checked === "false"){ | |||||
| axios.get(`${apiPath}${GET_USER_PASSWORD_DURATION}`,{ | |||||
| params:{ | |||||
| id: userData.id | |||||
| } | |||||
| }) | |||||
| .then((response) => { | |||||
| if (response.status === 200) { | |||||
| setForceChangePassword(response.data.expired); | |||||
| if(!response.data.expired){ | |||||
| localStorage.setItem("checkPasswordExpired",true); | |||||
| } | |||||
| } | |||||
| }) | |||||
| .catch(error => { | |||||
| console.log(error); | |||||
| return false; | |||||
| }); | |||||
| } | |||||
| const dispatch = useDispatch(); | |||||
| // ⚡️ Use the inProgress property from useMsal to guard against premature logout | |||||
| const { instance, accounts, inProgress } = useMsal(); | |||||
| const isAuthenticated = accounts.length > 0; | |||||
| const idleTimerRef = useRef(null); | |||||
| // The core logout logic | |||||
| const forceLogout = useCallback(() => { | |||||
| // 🛑 CRITICAL GUARD CLAUSE: | |||||
| // Do NOT log out if: | |||||
| // 1. MSAL is currently busy (acquiring token, handling redirect, etc.) | |||||
| // 2. No accounts are logged in (to prevent unnecessary calls) | |||||
| if (inProgress !== "none" || !isAuthenticated) { | |||||
| console.log("MSAL is busy or user is not authenticated. Aborting idle logout attempt."); | |||||
| return; | |||||
| } | } | ||||
| }, []); | |||||
| const onIdle = () => { | |||||
| setLastRequestTime(Date.now()); | |||||
| setState('Idle') | |||||
| } | |||||
| const onActive = () => { | |||||
| setLastRequestTime(Date.now()); | |||||
| setState('Active') | |||||
| } | |||||
| console.warn('Idle timeout reached. Forcing logout.'); | |||||
| const { | |||||
| getRemainingTime, | |||||
| //getTabId, | |||||
| isLastActiveTab, | |||||
| } = useIdleTimer({ | |||||
| onIdle, | |||||
| onActive, | |||||
| timeout: 60_000, | |||||
| throttle: 500, | |||||
| crossTab: true, | |||||
| syncTimers: 200, | |||||
| }) | |||||
| const lastActiveTab = isLastActiveTab() === null ? 'loading' : isLastActiveTab() | |||||
| //const tabId = getTabId() === null ? 'loading' : getTabId().toString() | |||||
| // 1. Clear Redux/local storage state | |||||
| dispatch(handleLogoutFunction()); | |||||
| // 2. Perform MSAL logout (This is what triggers the Azure AD sign out page) | |||||
| instance.logoutRedirect(); | |||||
| }, [dispatch, instance, isAuthenticated, inProgress]); // Dependencies updated | |||||
| // useEffect for monitoring activity and setting up the timer | |||||
| useEffect(() => { | useEffect(() => { | ||||
| const userData = getUserData(); | |||||
| if(!isObjEmpty(userData)){ | |||||
| axios.get(`${apiPath}${GET_IDLE_LOGOUT_TIME}`, | |||||
| ) | |||||
| .then((response) => { | |||||
| if (response.status === 200) { | |||||
| setLastRequestTime(Date.now()); | |||||
| setLogoutInterval(parseInt(response.data.data)); | |||||
| } | |||||
| }) | |||||
| .catch(error => { | |||||
| console.log(error); | |||||
| return false; | |||||
| }); | |||||
| } | |||||
| else{ | |||||
| //navigate('/login'); | |||||
| } | |||||
| }, []); | |||||
| console.log('AutoLogoutProvider Render/Effect:', { | |||||
| isAuthenticated, | |||||
| inProgress, | |||||
| hasAccessToken: !!localStorage.getItem('accessToken') // Check legacy token | |||||
| }); | |||||
| // ... (Your existing logic to set up the idle timer and reset it on activity) | |||||
| // Ensure that your timer starts ONLY if isAuthenticated is true AND inProgress is "none" | |||||
| useEffect(() => { | |||||
| const interval = setInterval(async () => { | |||||
| const currentTime = Date.now(); | |||||
| getRemainingTime(); | |||||
| if(state !== "Active" && lastActiveTab){ | |||||
| const timeElapsed = currentTime - lastRequestTime; | |||||
| // if (timeElapsed >= logoutInterval * 60 * 1000) { | |||||
| // await dispatch(handleLogoutFunction()); | |||||
| // await navigate('/login'); | |||||
| // await window.location.reload(); | |||||
| // } | |||||
| } | |||||
| }, 1000); // Check every second | |||||
| if (isAuthenticated && inProgress === "none") { | |||||
| // Start the timer | |||||
| idleTimerRef.current = setTimeout(forceLogout, IDLE_TIMEOUT_SECONDS * 1000); | |||||
| } | |||||
| return () => { | return () => { | ||||
| clearInterval(interval); | |||||
| clearTimeout(idleTimerRef.current); | |||||
| }; | }; | ||||
| }, [lastRequestTime,logoutInterval]); | |||||
| useEffect(() => { | |||||
| //if user data from parent are not null | |||||
| if(forceChangePassword){ | |||||
| setIsTempWindowOpen(true); | |||||
| } | |||||
| }, [forceChangePassword]); | |||||
| const handleTempClose = (event, reason) => { | |||||
| if (reason && reason === "backdropClick") | |||||
| return; | |||||
| setIsTempWindowOpen(false); | |||||
| }; | |||||
| }, [isAuthenticated, inProgress, forceLogout]); // Depend on MSAL state | |||||
| return ( | |||||
| <TimerContext.Provider value={{lastRequestTime,setLastRequestTime}}> | |||||
| {children} | |||||
| <ThemeProvider theme={LIONER_FORM_THEME}> | |||||
| <ChangePasswordWindow | |||||
| isWindowOpen={isTempWindowOpen} | |||||
| title={"Change Password"} | |||||
| onNormalClose={handleTempClose} | |||||
| onConfirmClose={handleTempClose} | |||||
| isForce={true} | |||||
| /> | |||||
| </ThemeProvider> | |||||
| </TimerContext.Provider> | |||||
| ); | |||||
| return children; | |||||
| }; | }; | ||||
| export { TimerContext, AutoLogoutProvider }; | export { TimerContext, AutoLogoutProvider }; | ||||
| @@ -320,106 +320,6 @@ const PdfSearchForm = ({applySearch, setExpanded,expanded, clientId}) => { | |||||
| <Grid/> | <Grid/> | ||||
| } | } | ||||
| </Grid> | </Grid> | ||||
| {/* <Grid item> | |||||
| <Button | |||||
| variant="contained" | |||||
| color="create" | |||||
| onClick={createFormIDA} | |||||
| > | |||||
| New Lioner IDA | |||||
| </Button> | |||||
| </Grid> | |||||
| <Grid item> | |||||
| <Button | |||||
| variant="contained" | |||||
| color="create" | |||||
| onClick={createFormFNA} | |||||
| > | |||||
| New Lioner FNA | |||||
| </Button> | |||||
| </Grid> | |||||
| <Grid item> | |||||
| <Button | |||||
| variant="contained" | |||||
| color="create" | |||||
| onClick={createFormHSBCFIN} | |||||
| > | |||||
| New HSBCFIN | |||||
| </Button> | |||||
| </Grid> | |||||
| <Grid item> | |||||
| <Button | |||||
| variant="contained" | |||||
| color="create" | |||||
| onClick={() => {}} | |||||
| > | |||||
| New HSBCA31 | |||||
| </Button> | |||||
| </Grid> | |||||
| <Grid item> | |||||
| <Button | |||||
| variant="contained" | |||||
| color="create" | |||||
| onClick={() => {}} | |||||
| > | |||||
| New MLB035 | |||||
| </Button> | |||||
| </Grid> | |||||
| <Grid item> | |||||
| <Button | |||||
| variant="contained" | |||||
| color="create" | |||||
| onClick={() => {}} | |||||
| > | |||||
| New MLFNA ENG | |||||
| </Button> | |||||
| </Grid> | |||||
| <Grid item> | |||||
| <Button | |||||
| variant="contained" | |||||
| color="create" | |||||
| onClick={() => {}} | |||||
| > | |||||
| New MLFNA CHI | |||||
| </Button> | |||||
| </Grid> | |||||
| <Grid item> | |||||
| <Button | |||||
| variant="contained" | |||||
| color="create" | |||||
| onClick={() => {}} | |||||
| > | |||||
| New SLFNA ENG | |||||
| </Button> | |||||
| </Grid> | |||||
| <Grid item> | |||||
| <Button | |||||
| variant="contained" | |||||
| color="create" | |||||
| onClick={() => {}} | |||||
| > | |||||
| New SLFNA CHI | |||||
| </Button> | |||||
| </Grid> | |||||
| <Grid item> | |||||
| <Button | |||||
| variant="contained" | |||||
| color="create" | |||||
| onClick={() => {}} | |||||
| > | |||||
| New SLGII | |||||
| </Button> | |||||
| </Grid> | |||||
| <Grid item> | |||||
| <Button | |||||
| variant="contained" | |||||
| color="create" | |||||
| onClick={() => {}} | |||||
| > | |||||
| New SL Saving | |||||
| </Button> | |||||
| </Grid> */} | |||||
| </Grid> | </Grid> | ||||
| </ThemeProvider> | </ThemeProvider> | ||||
| </Grid> | </Grid> | ||||