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;