/** * Fociz Session Guard (Release v1.6) * Realtime beveiligingsbeheer voor sessie-inactiviteit conform NIST SP 800-63B / ISO 27001 * - Waarschuwt de gebruiker 2 minuten voor het verlopen met aftelklok * - Throttled keepalive pings tijdens actieve interactie * - Synchronisatie over meerdere open browsertabbladen via localStorage */ (function () { 'use strict'; // Script configuratie uitlezen var scriptTag = document.currentScript || document.querySelector('script[src*="session-guard.js"]'); var rawTimeout = scriptTag ? parseInt(scriptTag.getAttribute('data-timeout'), 10) : 1800; var idleTimeout = (!isNaN(rawTimeout) && rawTimeout > 0) ? rawTimeout : 1800; // Waarschuwing 120 seconden voor afloop (of de helft indien timeout kort is) var warningSeconds = idleTimeout > 240 ? 120 : Math.max(10, Math.floor(idleTimeout / 2)); var lastActivity = Date.now(); var lastPingTime = Date.now(); var isWarningShowing = false; var keepaliveInFlight = false; // Synchronisatie met andere tabbladen var storedActivity = localStorage.getItem('fociz_session_last_activity'); if (storedActivity) { var parsed = parseInt(storedActivity, 10); if (!isNaN(parsed) && parsed > 0 && parsed <= Date.now()) { lastActivity = parsed; } } // Modal DOM elementen var modalEl = null; var countdownEl = null; var keepaliveBtn = null; var logoutBtn = null; function initDom() { modalEl = document.getElementById('focizSessionModal'); countdownEl = document.getElementById('focizSessionCountdown'); keepaliveBtn = document.getElementById('focizSessionKeepaliveBtn'); logoutBtn = document.getElementById('focizSessionLogoutBtn'); if (keepaliveBtn) { keepaliveBtn.addEventListener('click', function (e) { e.preventDefault(); sendKeepalive(true); }); } if (logoutBtn) { logoutBtn.addEventListener('click', function (e) { e.preventDefault(); performLogout(); }); } } // Luister naar tabblad synchronisatie window.addEventListener('storage', function (e) { if (e.key === 'fociz_session_last_activity') { var updated = parseInt(e.newValue, 10); if (!isNaN(updated) && updated > lastActivity) { lastActivity = updated; if (isWarningShowing) { hideModal(); } } } else if (e.key === 'fociz_session_logged_out') { window.location.href = '/auth/login.php?expired=1'; } }); // Formatteer seconden naar mm:ss function formatTime(seconds) { var m = Math.floor(seconds / 60); var s = seconds % 60; return (m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s; } // Toon modal function showModal(remainingSeconds) { if (!modalEl) initDom(); if (!modalEl) return; isWarningShowing = true; modalEl.classList.remove('hidden'); modalEl.classList.add('flex'); if (countdownEl) { countdownEl.textContent = formatTime(remainingSeconds); } } // Verberg modal function hideModal() { if (!modalEl) return; isWarningShowing = false; modalEl.classList.add('hidden'); modalEl.classList.remove('flex'); } // Verzend keepalive naar de backend function sendKeepalive(manualClick) { if (keepaliveInFlight) return; keepaliveInFlight = true; if (manualClick && keepaliveBtn) { keepaliveBtn.disabled = true; keepaliveBtn.textContent = 'Verlengen...'; } fetch('/api/session.php?action=keepalive', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' } }) .then(function (res) { if (res.status === 401) { // Reeds verlopen localStorage.setItem('fociz_session_logged_out', Date.now().toString()); window.location.href = '/auth/login.php?expired=1'; return null; } return res.json(); }) .then(function (data) { keepaliveInFlight = false; if (manualClick && keepaliveBtn) { keepaliveBtn.disabled = false; keepaliveBtn.textContent = 'Blijf Ingelogd'; } if (data && data.success) { lastActivity = Date.now(); lastPingTime = Date.now(); localStorage.setItem('fociz_session_last_activity', lastActivity.toString()); hideModal(); } }) .catch(function (err) { keepaliveInFlight = false; if (manualClick && keepaliveBtn) { keepaliveBtn.disabled = false; keepaliveBtn.textContent = 'Blijf Ingelogd'; } console.error('Session keepalive error:', err); }); } // Direct uitloggen function performLogout() { localStorage.setItem('fociz_session_logged_out', Date.now().toString()); fetch('/api/session.php?action=logout', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' } }).finally(function () { window.location.href = '/auth/logout.php'; }); } // Registreer gebruikersactiviteit (throttled) function registerUserActivity() { var now = Date.now(); // Als de waarschuwingsmodal actief is, negeren we vage muisbewegingen; // de gebruiker moet expliciet op 'Blijf Ingelogd' klikken conform beveiligingsnormen. if (isWarningShowing) return; // Lokale activiteit bijwerken (maximaal eens per 3 seconden wegschrijven) if (now - lastActivity > 3000) { lastActivity = now; localStorage.setItem('fociz_session_last_activity', now.toString()); } // Server keepalive: stuur op de achtergrond een ping als er activiteit is // én er minimaal 60 seconden (of idleTimeout / 4) sinds de vorige serverping is verstreken. var minPingInterval = Math.min(60000, Math.max(15000, (idleTimeout * 1000) / 4)); if (now - lastPingTime > minPingInterval) { lastPingTime = now; sendKeepalive(false); } } // Event listeners voor menselijke interactie var events = ['mousemove', 'keydown', 'click', 'scroll', 'touchstart']; events.forEach(function (evt) { window.addEventListener(evt, registerUserActivity, { passive: true }); }); // Hoofdtimer interval (elke seconde) function tick() { var now = Date.now(); var elapsedSec = Math.floor((now - lastActivity) / 1000); var remainingSec = idleTimeout - elapsedSec; if (remainingSec <= 0) { // Sessie officieel verlopen localStorage.setItem('fociz_session_logged_out', now.toString()); window.location.href = '/auth/login.php?expired=1'; return; } if (remainingSec <= warningSeconds) { // Binnen waarschuwingsvenster showModal(remainingSec); } else if (isWarningShowing) { // Waarschuwing niet meer van toepassing (bijv. verlengd vanuit ander tabblad) hideModal(); } } // Starten zodra DOM gereed is if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function () { initDom(); setInterval(tick, 1000); }); } else { initDom(); setInterval(tick, 1000); } })();