浏览代码

no message

master
父节点
当前提交
03de849e35
共有 7 个文件被更改,包括 184 次插入220 次删除
  1. +1
    -1
      .env-cmdrc
  2. +2
    -0
      package.json
  3. +58
    -0
      src/auth/axiosMsalInterceptor.js
  4. +44
    -0
      src/auth/axiosMsalInterceptor/index.js
  5. +34
    -0
      src/auth/msalConfig.js
  6. +45
    -119
      src/components/AutoLogoutProvider.js
  7. +0
    -100
      src/pages/pdf/PdfSearchPage/PdfSearchForm.js

+ 1
- 1
.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"
}
}

+ 2
- 0
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",


+ 58
- 0
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);
}
);
};

+ 44
- 0
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().
};

+ 34
- 0
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"]
};

+ 45
- 119
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 (
<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 };

+ 0
- 100
src/pages/pdf/PdfSearchPage/PdfSearchForm.js 查看文件

@@ -320,106 +320,6 @@ const PdfSearchForm = ({applySearch, setExpanded,expanded, clientId}) => {
<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>
</ThemeProvider>
</Grid>


正在加载...
取消
保存