class SessionTimeoutManager { constructor() { this.config = { timeout: 1800, warning_time: 30, check_interval: 30000, ping_interval: 60000, logout_url: '/logout', keep_alive_url: '/session/ping-activity', check_url: '/session/check-timeout' }; this.timers = { checkTimer: null, logoutTimer: null, warningTimer: null, pingTimer: null }; this.modal = null; this.warningShown = false; this.pingInProgress = false; this.sessionExpired = false; this.loggedOut = false; this.consecutiveFailures = 0; this.nextPingAllowedAt = 0; this.lastSuccessfulPingAt = 0; this.activityPending = false; this.backoffMs = [120000, 300000, 600000, 900000]; } async init() { try { await this.fetchConfig(); this.setupEventListeners(); this.startPeriodicChecks(); console.log('Session timeout manager initialized with config:', this.config); } catch (error) { console.warn('Session timeout using default config due to error:', error.message); this.setupEventListeners(); this.startPeriodicChecks(); } } async fetchConfig() { try { const response = await fetch('/session/get-timeout-config', { method: 'GET', headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, credentials: 'same-origin' }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json(); if (data.success) { this.config = { ...this.config, ...data }; } } catch (error) { console.warn('Failed to load timeout config, using defaults:', error.message); } } shouldPausePings() { return document.hidden || this.sessionExpired || this.loggedOut; } currentBackoffMs() { if (this.consecutiveFailures <= 0) { return this.config.ping_interval || 60000; } const index = Math.min(this.consecutiveFailures - 1, this.backoffMs.length - 1); return this.backoffMs[index]; } setupEventListeners() { const events = ['mousedown', 'keydown', 'scroll', 'touchstart', 'click', 'input']; events.forEach((event) => { document.addEventListener(event, () => this.onUserActivity(), { passive: true }); }); document.addEventListener('visibilitychange', () => { if (document.hidden) { return; } if (!this.shouldPausePings()) { this.checkSessionStatus(); } }); } onUserActivity() { if (this.shouldPausePings()) { return; } this.activityPending = true; this.schedulePing(); } schedulePing() { if (this.shouldPausePings() || this.pingInProgress) { return; } const now = Date.now(); const waitMs = Math.max(0, this.nextPingAllowedAt - now); clearTimeout(this.timers.pingTimer); this.timers.pingTimer = setTimeout(() => { this.resetActivity(); }, waitMs); } async resetActivity() { if (this.pingInProgress || this.shouldPausePings()) { return; } const now = Date.now(); if (now < this.nextPingAllowedAt) { this.schedulePing(); return; } if ( this.consecutiveFailures === 0 && this.lastSuccessfulPingAt > 0 && (now - this.lastSuccessfulPingAt) < (this.config.ping_interval || 60000) && !this.activityPending ) { return; } this.pingInProgress = true; try { const response = await fetch(this.config.keep_alive_url, { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, credentials: 'same-origin' }); if (response.status === 401 || response.status === 403) { this.sessionExpired = true; this.handleSessionExpired({ redirect: this.config.logout_url, message: 'Your session has expired. Please log in again.' }); return; } if (response.status === 429) { this.consecutiveFailures += 1; this.nextPingAllowedAt = Date.now() + this.currentBackoffMs(); return; } if (!response.ok) { throw new Error(`HTTP ${response.status}`); } let data = null; try { data = await response.json(); } catch (parseError) { data = { status: 'active' }; } if (data && data.status === 'expired') { this.handleSessionExpired(data); return; } this.consecutiveFailures = 0; this.activityPending = false; this.lastSuccessfulPingAt = Date.now(); this.nextPingAllowedAt = this.lastSuccessfulPingAt + (this.config.ping_interval || 60000); this.clearWarning(); } catch (error) { this.consecutiveFailures += 1; this.nextPingAllowedAt = Date.now() + this.currentBackoffMs(); console.warn('Activity reset failed; backing off:', error); } finally { this.pingInProgress = false; } } startPeriodicChecks() { clearInterval(this.timers.checkTimer); this.timers.checkTimer = setInterval(() => { if (this.shouldPausePings()) { return; } this.checkSessionStatus(); }, this.config.check_interval); } async checkSessionStatus() { if (this.shouldPausePings()) { return; } try { const response = await fetch(this.config.check_url, { method: 'GET', headers: { 'X-Requested-With': 'XMLHttpRequest' }, credentials: 'same-origin' }); if (response.status === 404) { console.log('Session check endpoint not available, skipping'); return; } if (response.status === 401 || response.status === 403) { this.sessionExpired = true; this.handleSessionExpired({ redirect: this.config.logout_url, message: 'Your session has expired. Please log in again.' }); return; } if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json(); switch (data.status) { case 'expired': this.handleSessionExpired(data); break; case 'warning': this.handleSessionWarning(data); break; case 'active': this.handleSessionActive(data); break; } } catch (error) { console.warn('Session status check failed:', error); } } handleSessionExpired(data) { this.sessionExpired = true; this.clearWarning(); clearInterval(this.timers.checkTimer); clearTimeout(this.timers.pingTimer); if (data.redirect) { alert(data.message || 'Your session has expired. Please log in again.'); window.location.href = data.redirect; } else { window.location.href = this.config.logout_url; } } handleSessionWarning(data) { if (!this.warningShown) { this.showWarning(data.time_remaining); } else { this.updateWarning(data.time_remaining); } this.scheduleLogout(data.time_remaining); this.startCountdown(data.time_remaining); } handleSessionActive(data) { this.clearWarning(); clearTimeout(this.timers.logoutTimer); this.timers.logoutTimer = null; } showWarning(timeRemaining) { this.warningShown = true; this.createWarningModal(timeRemaining); } updateWarning(timeRemaining) { if (this.modal) { const countdownElement = this.modal.querySelector('.countdown'); if (countdownElement) { countdownElement.textContent = Math.max(0, timeRemaining); } } } scheduleLogout(timeRemaining) { clearTimeout(this.timers.logoutTimer); const delay = Math.max(0, Number(timeRemaining) || 0) * 1000; this.timers.logoutTimer = setTimeout(() => { this.handleSessionExpired({ redirect: this.config.logout_url, message: 'Your session has expired due to inactivity. Please log in again.' }); }, delay); } startCountdown(timeRemaining) { clearInterval(this.timers.warningTimer); let secondsRemaining = Math.max(0, Number(timeRemaining) || 0); this.updateWarning(secondsRemaining); this.timers.warningTimer = setInterval(() => { secondsRemaining = Math.max(0, secondsRemaining - 1); this.updateWarning(secondsRemaining); if (secondsRemaining <= 0) { clearInterval(this.timers.warningTimer); this.timers.warningTimer = null; } }, 1000); } createWarningModal(timeRemaining) { this.hideWarning(); this.modal = document.createElement('div'); this.modal.className = 'session-timeout-modal'; this.modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 99999; display: flex; justify-content: center; align-items: center; font-family: Arial, sans-serif; `; this.modal.innerHTML = `

Session About to Expire

Your session will expire in ${timeRemaining} seconds due to inactivity.

Click Continue Session if you want to keep using this session.

`; document.body.appendChild(this.modal); document.body.style.overflow = 'hidden'; } hideWarning() { if (this.modal) { this.modal.remove(); this.modal = null; } this.warningShown = false; document.body.style.overflow = ''; } clearWarning() { this.hideWarning(); clearTimeout(this.timers.logoutTimer); this.timers.logoutTimer = null; clearInterval(this.timers.warningTimer); this.timers.warningTimer = null; } continueSession() { this.activityPending = true; this.nextPingAllowedAt = 0; this.resetActivity(); this.hideWarning(); } logout() { this.loggedOut = true; clearInterval(this.timers.checkTimer); clearTimeout(this.timers.pingTimer); window.location.href = this.config.logout_url; } destroy() { clearInterval(this.timers.checkTimer); clearTimeout(this.timers.logoutTimer); clearInterval(this.timers.warningTimer); clearTimeout(this.timers.pingTimer); this.hideWarning(); } } const sessionTimeout = new SessionTimeoutManager(); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => sessionTimeout.init()); } else { sessionTimeout.init(); } window.sessionTimeout = sessionTimeout;