97 lines
3.6 KiB
JavaScript
97 lines
3.6 KiB
JavaScript
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;
|