| @@ -0,0 +1,52 @@ | |||||
| // src/components/Require2FA.jsx | |||||
| import React, { useState, useEffect } from 'react'; | |||||
| import { Navigate } from 'react-router-dom'; | |||||
| import { Box, CircularProgress, Typography } from '@mui/material'; | |||||
| import axios from 'axios'; | |||||
| import { apiPath } from '../auth/utils'; | |||||
| const Require2FA = ({ children }) => { | |||||
| const [is2FAEnabled, setIs2FAEnabled] = useState(null); // null = loading | |||||
| const [error, setError] = useState(false); | |||||
| useEffect(() => { | |||||
| const check2FA = async () => { | |||||
| try { | |||||
| const response = await axios.get(`${apiPath}/2fa/status`, { | |||||
| headers: { Authorization: `Bearer ${localStorage.getItem('accessToken')}` } | |||||
| }); | |||||
| setIs2FAEnabled(response.data.enabled); | |||||
| } catch (err) { | |||||
| // Fallback to localStorage (same logic as in Profile page) | |||||
| try { | |||||
| const userData = JSON.parse(localStorage.getItem('userData') || '{}'); | |||||
| setIs2FAEnabled(!!userData.twoFactorEnabled); | |||||
| } catch { | |||||
| setError(true); | |||||
| } | |||||
| } | |||||
| }; | |||||
| check2FA(); | |||||
| }, []); | |||||
| if (is2FAEnabled === null) { | |||||
| // Still loading | |||||
| return ( | |||||
| <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}> | |||||
| <CircularProgress /> | |||||
| <Typography sx={{ ml: 2 }}>Checking security settings...</Typography> | |||||
| </Box> | |||||
| ); | |||||
| } | |||||
| if (!is2FAEnabled || error) { | |||||
| // Redirect to profile page with a clear message | |||||
| return <Navigate to="/profile" replace state={{ require2FA: true }} />; | |||||
| } | |||||
| // 2FA is enabled → allow access | |||||
| return children; | |||||
| }; | |||||
| export default Require2FA; | |||||
| @@ -1,6 +1,6 @@ | |||||
| import {lazy, useContext} from 'react'; | import {lazy, useContext} from 'react'; | ||||
| // project import | |||||
| import Require2FA from '../components/Require2FA'; | |||||
| import Loadable from 'components/Loadable'; | import Loadable from 'components/Loadable'; | ||||
| import MainLayout from "../layout/MainLayout"; | import MainLayout from "../layout/MainLayout"; | ||||
| import {handleRouteAbility} from "../utils/CommonFunction"; | import {handleRouteAbility} from "../utils/CommonFunction"; | ||||
| @@ -26,64 +26,78 @@ const ClientRoutes =() => { | |||||
| path: '/', | path: '/', | ||||
| element: <MainLayout />, | element: <MainLayout />, | ||||
| children: [ | children: [ | ||||
| { | { | ||||
| path: 'client', | path: 'client', | ||||
| element: ( | element: ( | ||||
| handleRouteAbility( | |||||
| <Require2FA> | |||||
| {handleRouteAbility( | |||||
| ability.can('VIEW', 'CLIENT'), | ability.can('VIEW', 'CLIENT'), | ||||
| <ClientSearchPage />, | <ClientSearchPage />, | ||||
| <Navigate to="/" /> | <Navigate to="/" /> | ||||
| ) | |||||
| )} | |||||
| </Require2FA> | |||||
| ), | ), | ||||
| }, | }, | ||||
| { | { | ||||
| path: 'client/maintain/:id', | path: 'client/maintain/:id', | ||||
| element: ( | element: ( | ||||
| <Require2FA> | |||||
| { | |||||
| handleRouteAbility( | handleRouteAbility( | ||||
| ability.can('VIEW', 'CLIENT'), | ability.can('VIEW', 'CLIENT'), | ||||
| <ClientMaintainPage />, | <ClientMaintainPage />, | ||||
| <Navigate to="/" /> | <Navigate to="/" /> | ||||
| ) | |||||
| )} | |||||
| </Require2FA> | |||||
| ), | ), | ||||
| }, | }, | ||||
| { | { | ||||
| path: '/pdf/:id', | path: '/pdf/:id', | ||||
| element: ( | element: ( | ||||
| handleRouteAbility( | |||||
| ability.can('VIEW', 'CLIENT'), | |||||
| <PdfSearchPage />, | |||||
| <Navigate to="/" /> | |||||
| ) | |||||
| <Require2FA> | |||||
| {handleRouteAbility( | |||||
| ability.can('VIEW', 'CLIENT'), | |||||
| <PdfSearchPage />, | |||||
| <Navigate to="/" /> | |||||
| )} | |||||
| </Require2FA> | |||||
| ), | ), | ||||
| }, | }, | ||||
| { | { | ||||
| path: '/pdf/maintain/:id', | path: '/pdf/maintain/:id', | ||||
| element: ( | element: ( | ||||
| handleRouteAbility( | |||||
| <Require2FA> | |||||
| {handleRouteAbility( | |||||
| ability.can('VIEW', 'CLIENT'), | ability.can('VIEW', 'CLIENT'), | ||||
| <PdfMaintainPage />, | <PdfMaintainPage />, | ||||
| <Navigate to="/" /> | <Navigate to="/" /> | ||||
| ) | |||||
| )} | |||||
| </Require2FA> | |||||
| ), | ), | ||||
| }, | }, | ||||
| { | { | ||||
| path: '/template/', | path: '/template/', | ||||
| element: ( | element: ( | ||||
| handleRouteAbility( | |||||
| <Require2FA> | |||||
| {handleRouteAbility( | |||||
| ability.can('VIEW', 'TEMPLATE'), | ability.can('VIEW', 'TEMPLATE'), | ||||
| <TemplateSearchPage />, | <TemplateSearchPage />, | ||||
| <Navigate to="/" /> | <Navigate to="/" /> | ||||
| ) | |||||
| )} | |||||
| </Require2FA> | |||||
| ), | ), | ||||
| }, | }, | ||||
| { | { | ||||
| path: '/pdf/form-up-down/:id', | path: '/pdf/form-up-down/:id', | ||||
| element: ( | element: ( | ||||
| handleRouteAbility( | |||||
| <Require2FA> | |||||
| {handleRouteAbility( | |||||
| ability.can('VIEW', 'CLIENT'), | ability.can('VIEW', 'CLIENT'), | ||||
| <PdfFormUpAndDown />, | <PdfFormUpAndDown />, | ||||
| <Navigate to="/" /> | <Navigate to="/" /> | ||||
| ) | |||||
| )} | |||||
| </Require2FA> | |||||
| ), | ), | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -1,6 +1,6 @@ | |||||
| import {lazy, useContext} from 'react'; | import {lazy, useContext} from 'react'; | ||||
| // project import | |||||
| import Require2FA from '../components/Require2FA'; | |||||
| import Loadable from 'components/Loadable'; | import Loadable from 'components/Loadable'; | ||||
| import MainLayout from "../layout/MainLayout"; | import MainLayout from "../layout/MainLayout"; | ||||
| import AbilityContext from "../components/AbilityProvider"; | import AbilityContext from "../components/AbilityProvider"; | ||||
| @@ -102,102 +102,122 @@ const SettingRoutes = () => { | |||||
| { | { | ||||
| path: 'usergroupSearchview', | path: 'usergroupSearchview', | ||||
| element: ( | element: ( | ||||
| handleRouteAbility( | |||||
| <Require2FA> | |||||
| {handleRouteAbility( | |||||
| ability.can('MAINTAIN', 'USER_GROUP'), | ability.can('MAINTAIN', 'USER_GROUP'), | ||||
| <UserGroupSearchPage />, | <UserGroupSearchPage />, | ||||
| <Navigate to="/" /> | <Navigate to="/" /> | ||||
| ) | |||||
| )} | |||||
| </Require2FA> | |||||
| ), | ), | ||||
| }, | }, | ||||
| { | { | ||||
| path: 'userGroup/:id', | path: 'userGroup/:id', | ||||
| element:( | element:( | ||||
| handleRouteAbility( | |||||
| <Require2FA> | |||||
| {handleRouteAbility( | |||||
| ability.can('MAINTAIN', 'USER_GROUP'), | ability.can('MAINTAIN', 'USER_GROUP'), | ||||
| <UserGroupDetailPage />, | <UserGroupDetailPage />, | ||||
| <Navigate to="/" /> | <Navigate to="/" /> | ||||
| ) | |||||
| )} | |||||
| </Require2FA> | |||||
| ), | ), | ||||
| }, | }, | ||||
| { | { | ||||
| path: 'user/:id', | path: 'user/:id', | ||||
| element: ( | element: ( | ||||
| handleRouteAbility( | |||||
| <Require2FA> | |||||
| {handleRouteAbility( | |||||
| ability.can('VIEW', 'USER'), | ability.can('VIEW', 'USER'), | ||||
| <UserMaintainPage />, | <UserMaintainPage />, | ||||
| <Navigate to="/" /> | <Navigate to="/" /> | ||||
| ) | |||||
| )} | |||||
| </Require2FA> | |||||
| ), | ), | ||||
| }, | }, | ||||
| { | { | ||||
| path: 'userSearchview', | path: 'userSearchview', | ||||
| element:( | element:( | ||||
| handleRouteAbility( | |||||
| <Require2FA> | |||||
| {handleRouteAbility( | |||||
| ability.can('VIEW', 'USER'), | ability.can('VIEW', 'USER'), | ||||
| <UserSearchPage />, | <UserSearchPage />, | ||||
| <Navigate to="/" /> | <Navigate to="/" /> | ||||
| ) | |||||
| )} | |||||
| </Require2FA> | |||||
| ), | ), | ||||
| }, | }, | ||||
| { | { | ||||
| path: 'consultant', | path: 'consultant', | ||||
| element: ( | element: ( | ||||
| handleRouteAbility( | |||||
| <Require2FA> | |||||
| {handleRouteAbility( | |||||
| ability.can('VIEW', 'USER'), | ability.can('VIEW', 'USER'), | ||||
| <ConsultantSearchPage />, | <ConsultantSearchPage />, | ||||
| <Navigate to="/" /> | <Navigate to="/" /> | ||||
| ) | |||||
| )} | |||||
| </Require2FA> | |||||
| ), | ), | ||||
| }, | }, | ||||
| { | { | ||||
| path: 'consultant/:id', | path: 'consultant/:id', | ||||
| element: ( | element: ( | ||||
| handleRouteAbility( | |||||
| <Require2FA> | |||||
| {handleRouteAbility( | |||||
| ability.can('VIEW', 'USER'), | ability.can('VIEW', 'USER'), | ||||
| <ConsultantMaintainPage />, | <ConsultantMaintainPage />, | ||||
| <Navigate to="/" /> | <Navigate to="/" /> | ||||
| ) | |||||
| )} | |||||
| </Require2FA> | |||||
| ), | ), | ||||
| }, | }, | ||||
| { | { | ||||
| path: 'setting', | path: 'setting', | ||||
| element: ( | element: ( | ||||
| handleRouteAbility( | |||||
| <Require2FA> | |||||
| {handleRouteAbility( | |||||
| ability.can('MANAGE', 'SYSTEM_CONFIGURATION'), | ability.can('MANAGE', 'SYSTEM_CONFIGURATION'), | ||||
| <SettingPage />, | <SettingPage />, | ||||
| <Navigate to="/" /> | <Navigate to="/" /> | ||||
| ) | |||||
| )} | |||||
| </Require2FA> | |||||
| ), | ), | ||||
| }, | }, | ||||
| { | { | ||||
| path: 'auditLog', | path: 'auditLog', | ||||
| element:( | element:( | ||||
| handleRouteAbility( | |||||
| <Require2FA> | |||||
| {handleRouteAbility( | |||||
| ability.can('VIEW', 'AUDIT_LOG'), | ability.can('VIEW', 'AUDIT_LOG'), | ||||
| <AuditLogPage />, | <AuditLogPage />, | ||||
| <Navigate to="/" /> | <Navigate to="/" /> | ||||
| ) | |||||
| )} | |||||
| </Require2FA> | |||||
| ), | ), | ||||
| }, | }, | ||||
| { | { | ||||
| path: 'userActionLog', | path: 'userActionLog', | ||||
| element: ( | element: ( | ||||
| handleRouteAbility( | |||||
| <Require2FA> | |||||
| {handleRouteAbility( | |||||
| ability.can('VIEW', 'USER'), | ability.can('VIEW', 'USER'), | ||||
| <UserActionLogPage />, | <UserActionLogPage />, | ||||
| <Navigate to="/" /> | <Navigate to="/" /> | ||||
| ) | |||||
| )} | |||||
| </Require2FA> | |||||
| ), | ), | ||||
| }, | }, | ||||
| { | { | ||||
| path: 'loginLog', | path: 'loginLog', | ||||
| element: ( | element: ( | ||||
| handleRouteAbility( | |||||
| <Require2FA> | |||||
| {handleRouteAbility( | |||||
| ability.can('VIEW', 'LOGIN_LOG'), | ability.can('VIEW', 'LOGIN_LOG'), | ||||
| <LoginLogPage />, | <LoginLogPage />, | ||||
| <Navigate to="/" /> | <Navigate to="/" /> | ||||
| ) | |||||
| )} | |||||
| </Require2FA> | |||||
| ), | ), | ||||
| }, | }, | ||||
| { | { | ||||