From 39e97c3041ab7ddc4308b367c1699457922ec136 Mon Sep 17 00:00:00 2001 From: dharaneesh-r Date: Wed, 1 Jul 2026 16:19:49 +0530 Subject: [PATCH] updates on the login --- src/App.js | 3 + src/hooks/useInactivityLogout.js | 96 +++++++++++++++++++ .../HeaderContent/Profile/ProfileTab.js | 17 +--- .../Header/HeaderContent/Profile/index.js | 39 ++------ src/pages/nearle/login.js | 3 + src/utils/session.js | 38 ++++++++ 6 files changed, 147 insertions(+), 49 deletions(-) create mode 100644 src/hooks/useInactivityLogout.js create mode 100644 src/utils/session.js diff --git a/src/App.js b/src/App.js index 62f9497..ba4e4e8 100644 --- a/src/App.js +++ b/src/App.js @@ -10,6 +10,7 @@ import { useNavigate } from 'react-router'; import { useEffect } from 'react'; import { generateToken, initFirebaseNotificationListener } from 'firebase_notification/notification'; import InternetStatus from 'components/updateNetworkStatus'; +import useInactivityLogout from 'hooks/useInactivityLogout'; // auth-provider // import { JWTProvider as AuthProvider } from 'contexts/JWTContext'; @@ -25,6 +26,8 @@ const App = () => { }, [navigate]); const AppContent = () => { + useInactivityLogout(); + useEffect(() => { generateToken(); initFirebaseNotificationListener(); diff --git a/src/hooks/useInactivityLogout.js b/src/hooks/useInactivityLogout.js new file mode 100644 index 0000000..69b4a24 --- /dev/null +++ b/src/hooks/useInactivityLogout.js @@ -0,0 +1,96 @@ +import { useEffect, useRef } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import { useDispatch } from 'react-redux'; +import { clearFcmToken } from 'store/reducers/fcmSlice'; +import { logoutUser } from 'store/reducers/loginUserSlice'; +import { + ABSOLUTE_SESSION_TIMEOUT_MS, + ACTIVITY_STORAGE_KEY, + AUTH_PRESENCE_KEY, + INACTIVITY_TIMEOUT_MS, + SESSION_START_STORAGE_KEY, + isSessionActive, + markActivity, + markSessionStart, + performSessionLogout +} from 'utils/session'; + +const ACTIVITY_EVENTS = ['mousemove', 'mousedown', 'keydown', 'scroll', 'touchstart', 'wheel']; +const ACTIVITY_WRITE_THROTTLE_MS = 5000; +const IDLE_CHECK_INTERVAL_MS = 15000; + +// Two independent timers, both enforced from localStorage so every tab on the +// origin agrees: +// - INACTIVITY_TIMEOUT_MS: logs out after 15 minutes with no interaction, so +// a laptop left unlocked doesn't leave the console open indefinitely. +// - ABSOLUTE_SESSION_TIMEOUT_MS: a hard 30-minute cap since login, even if +// the user has been continuously active, so localStorage (auth keys, FCM +// token, cached zone list) never lingers on disk longer than that. +// A logout in one tab (auth key cleared) is picked up by the others via the +// 'storage' event. +const useInactivityLogout = () => { + const queryClient = useQueryClient(); + const dispatch = useDispatch(); + const lastWriteRef = useRef(0); + + useEffect(() => { + const doLogout = () => performSessionLogout({ queryClient, dispatch, clearFcmToken, logoutUser }); + + const handleActivity = () => { + const now = Date.now(); + if (now - lastWriteRef.current < ACTIVITY_WRITE_THROTTLE_MS) return; + lastWriteRef.current = now; + markActivity(); + }; + + const handleStorage = (event) => { + if (event.key === AUTH_PRESENCE_KEY && !event.newValue) { + doLogout(); + } + }; + + // Guards against the browser restoring a cached (bfcache) copy of a + // protected page via the back/forward button after logout happened. + const handlePageShow = (event) => { + if (event.persisted && !isSessionActive()) { + window.location.replace('/login'); + } + }; + + if (isSessionActive()) { + if (!localStorage.getItem(ACTIVITY_STORAGE_KEY)) markActivity(); + // Sessions that were already open before this feature shipped won't have + // a start time yet — give them a fresh 30-minute window instead of + // treating them as already expired. + if (!localStorage.getItem(SESSION_START_STORAGE_KEY)) markSessionStart(); + } + + ACTIVITY_EVENTS.forEach((eventName) => window.addEventListener(eventName, handleActivity, { passive: true })); + window.addEventListener('storage', handleStorage); + window.addEventListener('pageshow', handlePageShow); + + const intervalId = setInterval(() => { + if (!isSessionActive()) return; + + const lastActivity = Number(localStorage.getItem(ACTIVITY_STORAGE_KEY)) || Date.now(); + if (Date.now() - lastActivity >= INACTIVITY_TIMEOUT_MS) { + doLogout(); + return; + } + + const sessionStart = Number(localStorage.getItem(SESSION_START_STORAGE_KEY)) || Date.now(); + if (Date.now() - sessionStart >= ABSOLUTE_SESSION_TIMEOUT_MS) { + doLogout(); + } + }, IDLE_CHECK_INTERVAL_MS); + + return () => { + ACTIVITY_EVENTS.forEach((eventName) => window.removeEventListener(eventName, handleActivity)); + window.removeEventListener('storage', handleStorage); + window.removeEventListener('pageshow', handlePageShow); + clearInterval(intervalId); + }; + }, [queryClient, dispatch]); +}; + +export default useInactivityLogout; diff --git a/src/layout/MainLayout/Header/HeaderContent/Profile/ProfileTab.js b/src/layout/MainLayout/Header/HeaderContent/Profile/ProfileTab.js index b95cf02..92d8b1d 100644 --- a/src/layout/MainLayout/Header/HeaderContent/Profile/ProfileTab.js +++ b/src/layout/MainLayout/Header/HeaderContent/Profile/ProfileTab.js @@ -7,14 +7,10 @@ import { List, ListItemButton, ListItemIcon, ListItemText } from '@mui/material' // assets import { EditOutlined, LogoutOutlined, CommentOutlined } from '@ant-design/icons'; import { useNavigate } from 'react-router'; -import { useDispatch } from 'react-redux'; -import { clearFcmToken } from 'store/reducers/fcmSlice'; -import { logoutUser } from 'store/reducers/loginUserSlice'; // ==============================|| HEADER PROFILE - PROFILE TAB ||============================== // const ProfileTab = ({ handleLogout }) => { - const dispatch = useDispatch(); const [selectedIndex, setSelectedIndex] = useState(0); const navigate = useNavigate(); const handleListItemClick = (event, index) => { @@ -48,18 +44,7 @@ const ProfileTab = ({ handleLogout }) => { */} - { - handleLogout(); - dispatch(clearFcmToken()); // ✅ dispatch the action - dispatch(logoutUser()); // ✅ dispatch logout user as initial state - }} - // onClick={()=>{ - // navigate('/login') - // }} - > + diff --git a/src/layout/MainLayout/Header/HeaderContent/Profile/index.js b/src/layout/MainLayout/Header/HeaderContent/Profile/index.js index 532d4d8..f02bec4 100644 --- a/src/layout/MainLayout/Header/HeaderContent/Profile/index.js +++ b/src/layout/MainLayout/Header/HeaderContent/Profile/index.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import { useRef, useState } from 'react'; -import { useNavigate } from 'react-router'; +import { useQueryClient } from '@tanstack/react-query'; // material-ui import { useTheme } from '@mui/material/styles'; @@ -23,6 +23,7 @@ import { LogoutOutlined, UserOutlined } from '@ant-design/icons'; import { clearFcmToken } from 'store/reducers/fcmSlice'; import { useDispatch } from 'react-redux'; import { logoutUser } from 'store/reducers/loginUserSlice'; +import { performSessionLogout } from 'utils/session'; // tab panel wrapper function TabPanel({ children, value, index, ...other }) { @@ -50,30 +51,11 @@ function a11yProps(index) { const Profile = () => { const theme = useTheme(); - const navigate = useNavigate(); const dispatch = useDispatch(); + const queryClient = useQueryClient(); - // const { logout, user } = useAuth(); - const handleLogout = async () => { - try { - // await logout(); - - // navigate(`/login`, { - // state: { - // from: '' - // } - // }); - localStorage.removeItem('firstname'); - localStorage.removeItem('appuserid'); - localStorage.removeItem('authname'); - localStorage.removeItem('roleid'); - localStorage.removeItem('tenantid'); - localStorage.clear(); - - navigate('/login'); - } catch (err) { - console.error(err); - } + const handleLogout = () => { + performSessionLogout({ queryClient, dispatch, clearFcmToken, logoutUser }); }; const anchorRef = useRef(null); @@ -173,16 +155,7 @@ const Profile = () => { - - onClick={() => { - handleLogout(); - dispatch(clearFcmToken()); // ✅ dispatch the action dispatch(logoutUser()); // ✅ dispatch logout user as initial state dispatch(logoutUser()); // ✅ dispatch logout user as initial state - dispatch(logoutUser()); // ✅ dispatch logout user as initial state - }} - > + diff --git a/src/pages/nearle/login.js b/src/pages/nearle/login.js index 9cdbc7a..eea09dc 100644 --- a/src/pages/nearle/login.js +++ b/src/pages/nearle/login.js @@ -15,6 +15,7 @@ import { closeGlobalToast, GlobalToast } from 'components/nearle_components/Glob import Visibility from '@mui/icons-material/Visibility'; import VisibilityOff from '@mui/icons-material/VisibilityOff'; import { setLoginUser } from 'store/reducers/loginUserSlice'; +import { markSessionStart } from 'utils/session'; const Login = () => { const dispatch = useDispatch(); @@ -96,6 +97,7 @@ const Login = () => { localStorage.setItem('applocationid', userinfo.applocationid); localStorage.setItem('userid', userinfo.userid); localStorage.setItem('userfcmtoken', userinfo.userfcmtoken); + markSessionStart(); fetchAppLocations(userinfo.userid); navigate('/nearle/dispatch'); } else { @@ -117,6 +119,7 @@ const Login = () => { localStorage.setItem('applocationid', userinfo.applocationid); localStorage.setItem('userid', userinfo.userid); localStorage.setItem('userfcmtoken', userinfo.userfcmtoken); + markSessionStart(); closeGlobalToast(); // to close the pin snackbar navigate('/nearle/dispatch'); diff --git a/src/utils/session.js b/src/utils/session.js new file mode 100644 index 0000000..878a1b7 --- /dev/null +++ b/src/utils/session.js @@ -0,0 +1,38 @@ +// Shared session/logout contract used by both the manual "Logout" buttons +// and the automatic inactivity logout (see hooks/useInactivityLogout.js). +// Keeping this in one place means every logout path clears the same things +// the same way, instead of each caller hand-rolling its own localStorage cleanup. + +export const ACTIVITY_STORAGE_KEY = 'lastActivityTime'; +export const SESSION_START_STORAGE_KEY = 'sessionStartTime'; +export const AUTH_PRESENCE_KEY = 'authname'; +export const INACTIVITY_TIMEOUT_MS = 15 * 60 * 1000; // 15 minutes, resets on user activity +export const ABSOLUTE_SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes, hard cap since login regardless of activity + +export const markActivity = () => { + localStorage.setItem(ACTIVITY_STORAGE_KEY, String(Date.now())); +}; + +// Called once at login to start the absolute-lifetime clock for the session. +export const markSessionStart = () => { + localStorage.setItem(SESSION_START_STORAGE_KEY, String(Date.now())); +}; + +export const isSessionActive = () => Boolean(localStorage.getItem(AUTH_PRESENCE_KEY)); + +// Wipes local auth state + the in-memory query cache, then hard-navigates to +// /login. A real navigation (not react-router) is deliberate: it throws away +// the entire JS heap (Redux store, component state, any variable holding +// fetched data) so nothing sensitive survives in memory after logout, and it +// prevents the back button from resurrecting a stale authenticated page from +// the render tree. +export const performSessionLogout = ({ queryClient, dispatch, clearFcmToken, logoutUser } = {}) => { + try { + queryClient?.clear(); + if (dispatch && clearFcmToken) dispatch(clearFcmToken()); + if (dispatch && logoutUser) dispatch(logoutUser()); + } finally { + localStorage.clear(); + window.location.replace('/login'); + } +};