From 570fd92d00c9335176e120cdee325925f1820165 Mon Sep 17 00:00:00 2001 From: Deco Vander Date: Thu, 3 Jul 2025 20:53:44 -0400 Subject: [PATCH] Add comprehensive session security to admin panel Security improvements: - Auto-logout after 30 minutes of inactivity - Session warning 5 minutes before expiry with option to extend - Activity-based session extension on user interaction - Session validation on page load and API calls - Periodic session validity checks every minute - Secure cleanup of tokens and timers on logout - Protection against expired session usage This prevents unauthorized access if admin leaves session open or if tokens are compromised. --- public/admin.js | 126 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 117 insertions(+), 9 deletions(-) diff --git a/public/admin.js b/public/admin.js index a51da74..66c8c19 100644 --- a/public/admin.js +++ b/public/admin.js @@ -2,6 +2,12 @@ document.addEventListener('DOMContentLoaded', () => { let authToken = localStorage.getItem('adminToken'); let allLocations = []; let editingRowId = null; + let sessionTimeout = null; + let warningTimeout = null; + + // Session timeout settings (in milliseconds) + const SESSION_DURATION = 30 * 60 * 1000; // 30 minutes + const WARNING_TIME = 5 * 60 * 1000; // Show warning 5 minutes before expiry const loginSection = document.getElementById('login-section'); const adminSection = document.getElementById('admin-section'); @@ -11,10 +17,19 @@ document.addEventListener('DOMContentLoaded', () => { const refreshBtn = document.getElementById('refresh-btn'); const locationsTableBody = document.getElementById('locations-tbody'); - // Check if already logged in + // Check if already logged in and session is still valid if (authToken) { - showAdminSection(); - loadLocations(); + const loginTime = localStorage.getItem('adminLoginTime'); + const now = Date.now(); + + if (loginTime && (now - parseInt(loginTime)) < SESSION_DURATION) { + showAdminSection(); + loadLocations(); + startSessionTimer(); + } else { + // Session expired + logout('Session expired. Please log in again.'); + } } // Login form handler @@ -34,8 +49,10 @@ document.addEventListener('DOMContentLoaded', () => { if (response.ok) { authToken = data.token; localStorage.setItem('adminToken', authToken); + localStorage.setItem('adminLoginTime', Date.now().toString()); showAdminSection(); loadLocations(); + startSessionTimer(); } else { showMessage(loginMessage, data.error, 'error'); } @@ -46,9 +63,7 @@ document.addEventListener('DOMContentLoaded', () => { // Logout handler logoutBtn.addEventListener('click', () => { - authToken = null; - localStorage.removeItem('adminToken'); - showLoginSection(); + logout('Logged out successfully.'); }); // Refresh button handler @@ -78,6 +93,101 @@ document.addEventListener('DOMContentLoaded', () => { function hideMessage(element) { element.style.display = 'none'; } + + // Session management functions + function startSessionTimer() { + clearSessionTimers(); + + // Set warning timer (5 minutes before expiry) + warningTimeout = setTimeout(() => { + showSessionWarning(); + }, SESSION_DURATION - WARNING_TIME); + + // Set logout timer (30 minutes) + sessionTimeout = setTimeout(() => { + logout('Session expired due to inactivity.'); + }, SESSION_DURATION); + } + + function clearSessionTimers() { + if (sessionTimeout) { + clearTimeout(sessionTimeout); + sessionTimeout = null; + } + if (warningTimeout) { + clearTimeout(warningTimeout); + warningTimeout = null; + } + } + + function showSessionWarning() { + const extend = confirm('Your admin session will expire in 5 minutes. Click OK to extend your session, or Cancel to log out now.'); + if (extend) { + extendSession(); + } else { + logout('Session ended by user.'); + } + } + + function extendSession() { + // Update login time and restart timers + localStorage.setItem('adminLoginTime', Date.now().toString()); + startSessionTimer(); + console.log('Admin session extended for 30 minutes'); + } + + function logout(message = 'Logged out.') { + clearSessionTimers(); + authToken = null; + localStorage.removeItem('adminToken'); + localStorage.removeItem('adminLoginTime'); + showLoginSection(); + if (message) { + showMessage(loginMessage, message, 'error'); + } + } + + // Reset session timer on user activity + function resetSessionTimer() { + if (authToken && adminSection.style.display !== 'none') { + const loginTime = localStorage.getItem('adminLoginTime'); + const now = Date.now(); + + // Only reset if we're within the session duration + if (loginTime && (now - parseInt(loginTime)) < SESSION_DURATION) { + localStorage.setItem('adminLoginTime', now.toString()); + startSessionTimer(); + } + } + } + + // Listen for user activity to reset session timer + const activityEvents = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart', 'click']; + let activityTimer = null; + + activityEvents.forEach(event => { + document.addEventListener(event, () => { + // Throttle activity detection to once per minute + if (!activityTimer) { + activityTimer = setTimeout(() => { + resetSessionTimer(); + activityTimer = null; + }, 60000); // 1 minute throttle + } + }, true); + }); + + // Check session validity periodically + setInterval(() => { + if (authToken) { + const loginTime = localStorage.getItem('adminLoginTime'); + const now = Date.now(); + + if (!loginTime || (now - parseInt(loginTime)) >= SESSION_DURATION) { + logout('Session expired.'); + } + } + }, 60000); // Check every minute async function loadLocations() { try { @@ -87,9 +197,7 @@ document.addEventListener('DOMContentLoaded', () => { if (response.status === 401) { // Token expired or invalid - authToken = null; - localStorage.removeItem('adminToken'); - showLoginSection(); + logout('Session expired. Please log in again.'); return; }