diff --git a/.env-cmdrc b/.env-cmdrc
index b0fd1cc..dbb40a2 100644
--- a/.env-cmdrc
+++ b/.env-cmdrc
@@ -14,7 +14,7 @@
"REACT_APP_BACKEND_PROTOCOL": "https",
"REACT_APP_BACKEND_HOST": "forms.lioner.com",
"REACT_APP_BACKEND_PORT": "8090",
- "REACT_APP_ADOBE_API_KEY": "ee4433f258a74641ae6d502fd41cf703",
+ "REACT_APP_ADOBE_API_KEY": "7f498776ecdf47399d7e123d85b9fe1b",
"REACT_APP_BACKEND_API_PATH": "/api"
}
}
\ No newline at end of file
diff --git a/package.json b/package.json
index e742b02..5c21250 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,8 @@
"dependencies": {
"@ant-design/colors": "^6.0.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/react": "^3.1.0",
"@emotion/cache": "^11.10.3",
diff --git a/src/auth/axiosMsalInterceptor.js b/src/auth/axiosMsalInterceptor.js
new file mode 100644
index 0000000..aede2f9
--- /dev/null
+++ b/src/auth/axiosMsalInterceptor.js
@@ -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);
+ }
+ );
+};
\ No newline at end of file
diff --git a/src/auth/axiosMsalInterceptor/index.js b/src/auth/axiosMsalInterceptor/index.js
new file mode 100644
index 0000000..86ac6b1
--- /dev/null
+++ b/src/auth/axiosMsalInterceptor/index.js
@@ -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().
+};
\ No newline at end of file
diff --git a/src/auth/msalConfig.js b/src/auth/msalConfig.js
new file mode 100644
index 0000000..fe32239
--- /dev/null
+++ b/src/auth/msalConfig.js
@@ -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"]
+};
\ No newline at end of file
diff --git a/src/components/AutoLogoutProvider.js b/src/components/AutoLogoutProvider.js
index 2dbdc25..74d739d 100644
--- a/src/components/AutoLogoutProvider.js
+++ b/src/components/AutoLogoutProvider.js
@@ -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 { useDispatch } from 'react-redux';
import {dispatch} from "../store";
import {useNavigate} from "react-router-dom";
import axios from "axios";
@@ -12,136 +15,59 @@ import {ChangePasswordWindow} from "../layout/MainLayout/Header/HeaderContent/Pr
import {ThemeProvider} from "@emotion/react";
const TimerContext = createContext();
+const IDLE_TIMEOUT_SECONDS = 3600;
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(() => {
- 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 () => {
- 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 (
-
- {children}
-
-
-
-
- );
+ return children;
};
export { TimerContext, AutoLogoutProvider };
\ No newline at end of file
diff --git a/src/pages/pdf/PdfSearchPage/PdfSearchForm.js b/src/pages/pdf/PdfSearchPage/PdfSearchForm.js
index 1eda7a2..025a647 100644
--- a/src/pages/pdf/PdfSearchPage/PdfSearchForm.js
+++ b/src/pages/pdf/PdfSearchPage/PdfSearchForm.js
@@ -320,106 +320,6 @@ const PdfSearchForm = ({applySearch, setExpanded,expanded, clientId}) => {
}
-
- {/*
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- */}