updates on the login
This commit is contained in:
@@ -10,6 +10,7 @@ import { useNavigate } from 'react-router';
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { generateToken, initFirebaseNotificationListener } from 'firebase_notification/notification';
|
import { generateToken, initFirebaseNotificationListener } from 'firebase_notification/notification';
|
||||||
import InternetStatus from 'components/updateNetworkStatus';
|
import InternetStatus from 'components/updateNetworkStatus';
|
||||||
|
import useInactivityLogout from 'hooks/useInactivityLogout';
|
||||||
|
|
||||||
// auth-provider
|
// auth-provider
|
||||||
// import { JWTProvider as AuthProvider } from 'contexts/JWTContext';
|
// import { JWTProvider as AuthProvider } from 'contexts/JWTContext';
|
||||||
@@ -25,6 +26,8 @@ const App = () => {
|
|||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
const AppContent = () => {
|
const AppContent = () => {
|
||||||
|
useInactivityLogout();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
generateToken();
|
generateToken();
|
||||||
initFirebaseNotificationListener();
|
initFirebaseNotificationListener();
|
||||||
|
|||||||
96
src/hooks/useInactivityLogout.js
Normal file
96
src/hooks/useInactivityLogout.js
Normal file
@@ -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;
|
||||||
@@ -7,14 +7,10 @@ import { List, ListItemButton, ListItemIcon, ListItemText } from '@mui/material'
|
|||||||
// assets
|
// assets
|
||||||
import { EditOutlined, LogoutOutlined, CommentOutlined } from '@ant-design/icons';
|
import { EditOutlined, LogoutOutlined, CommentOutlined } from '@ant-design/icons';
|
||||||
import { useNavigate } from 'react-router';
|
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 ||============================== //
|
// ==============================|| HEADER PROFILE - PROFILE TAB ||============================== //
|
||||||
|
|
||||||
const ProfileTab = ({ handleLogout }) => {
|
const ProfileTab = ({ handleLogout }) => {
|
||||||
const dispatch = useDispatch();
|
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const handleListItemClick = (event, index) => {
|
const handleListItemClick = (event, index) => {
|
||||||
@@ -48,18 +44,7 @@ const ProfileTab = ({ handleLogout }) => {
|
|||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary="Billing" />
|
<ListItemText primary="Billing" />
|
||||||
</ListItemButton> */}
|
</ListItemButton> */}
|
||||||
<ListItemButton
|
<ListItemButton selected={selectedIndex === 3} onClick={handleLogout}>
|
||||||
selected={selectedIndex === 3}
|
|
||||||
// onClick={handleLogout}
|
|
||||||
onClick={() => {
|
|
||||||
handleLogout();
|
|
||||||
dispatch(clearFcmToken()); // ✅ dispatch the action
|
|
||||||
dispatch(logoutUser()); // ✅ dispatch logout user as initial state
|
|
||||||
}}
|
|
||||||
// onClick={()=>{
|
|
||||||
// navigate('/login')
|
|
||||||
// }}
|
|
||||||
>
|
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<LogoutOutlined />
|
<LogoutOutlined />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
// material-ui
|
// material-ui
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
@@ -23,6 +23,7 @@ import { LogoutOutlined, UserOutlined } from '@ant-design/icons';
|
|||||||
import { clearFcmToken } from 'store/reducers/fcmSlice';
|
import { clearFcmToken } from 'store/reducers/fcmSlice';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { logoutUser } from 'store/reducers/loginUserSlice';
|
import { logoutUser } from 'store/reducers/loginUserSlice';
|
||||||
|
import { performSessionLogout } from 'utils/session';
|
||||||
|
|
||||||
// tab panel wrapper
|
// tab panel wrapper
|
||||||
function TabPanel({ children, value, index, ...other }) {
|
function TabPanel({ children, value, index, ...other }) {
|
||||||
@@ -50,30 +51,11 @@ function a11yProps(index) {
|
|||||||
|
|
||||||
const Profile = () => {
|
const Profile = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const navigate = useNavigate();
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
// const { logout, user } = useAuth();
|
const handleLogout = () => {
|
||||||
const handleLogout = async () => {
|
performSessionLogout({ queryClient, dispatch, clearFcmToken, logoutUser });
|
||||||
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 anchorRef = useRef(null);
|
const anchorRef = useRef(null);
|
||||||
@@ -173,16 +155,7 @@ const Profile = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Tooltip title="Logout">
|
<Tooltip title="Logout">
|
||||||
<IconButton
|
<IconButton size="large" sx={{ color: 'text.primary' }} onClick={handleLogout}>
|
||||||
size="large"
|
|
||||||
sx={{ color: 'text.primary' }}
|
|
||||||
// onClick={handleLogout}>
|
|
||||||
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
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LogoutOutlined />
|
<LogoutOutlined />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { closeGlobalToast, GlobalToast } from 'components/nearle_components/Glob
|
|||||||
import Visibility from '@mui/icons-material/Visibility';
|
import Visibility from '@mui/icons-material/Visibility';
|
||||||
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
||||||
import { setLoginUser } from 'store/reducers/loginUserSlice';
|
import { setLoginUser } from 'store/reducers/loginUserSlice';
|
||||||
|
import { markSessionStart } from 'utils/session';
|
||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@@ -96,6 +97,7 @@ const Login = () => {
|
|||||||
localStorage.setItem('applocationid', userinfo.applocationid);
|
localStorage.setItem('applocationid', userinfo.applocationid);
|
||||||
localStorage.setItem('userid', userinfo.userid);
|
localStorage.setItem('userid', userinfo.userid);
|
||||||
localStorage.setItem('userfcmtoken', userinfo.userfcmtoken);
|
localStorage.setItem('userfcmtoken', userinfo.userfcmtoken);
|
||||||
|
markSessionStart();
|
||||||
fetchAppLocations(userinfo.userid);
|
fetchAppLocations(userinfo.userid);
|
||||||
navigate('/nearle/dispatch');
|
navigate('/nearle/dispatch');
|
||||||
} else {
|
} else {
|
||||||
@@ -117,6 +119,7 @@ const Login = () => {
|
|||||||
localStorage.setItem('applocationid', userinfo.applocationid);
|
localStorage.setItem('applocationid', userinfo.applocationid);
|
||||||
localStorage.setItem('userid', userinfo.userid);
|
localStorage.setItem('userid', userinfo.userid);
|
||||||
localStorage.setItem('userfcmtoken', userinfo.userfcmtoken);
|
localStorage.setItem('userfcmtoken', userinfo.userfcmtoken);
|
||||||
|
markSessionStart();
|
||||||
closeGlobalToast(); // to close the pin snackbar
|
closeGlobalToast(); // to close the pin snackbar
|
||||||
|
|
||||||
navigate('/nearle/dispatch');
|
navigate('/nearle/dispatch');
|
||||||
|
|||||||
38
src/utils/session.js
Normal file
38
src/utils/session.js
Normal file
@@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user