/** * Fociz Session Guard (Release v1.6.1) * 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 * - Beschermd tegen oneindige herlaad- en redirect-loops */ (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 now = Date.now(); var lastActivity = now; var lastPingTime = now; var isWarningShowing = false; var keepaliveInFlight = false; var hasRedirected = false; // Synchronisatie met andere tabbladen: // Alleen een timestamp overnemen als deze GELDIG is en NIET al verlopen is! // Aangezien DEZE pagina zojuist succesvol geladen is, is de sessie NU actief. var storedActivity = localStorage.getItem('fociz_session_last_activity'); if (storedActivity) { var parsed = parseInt(storedActivity, 10); if (!isNaN(parsed) && parsed > 0 && parsed <= now && (now - parsed) < (idleTimeout * 1000)) { lastActivity = Math.max(now, parsed); } } // Direct opslaan zodat alle tabbladen weten dat er zojuist activiteit was try { localStorage.setItem('fociz_session_last_activity', lastActivity.toString()); localStorage.removeItem('fociz_session_logged_out'); } catch (e) {} // Veilige eenmalige redirect helper om redirect loops te voorkomen function redirectToExpired() { if (hasRedirected) return; hasRedirected = true; try { localStorage.setItem('fociz_session_logged_out', Date.now().toString()); } catch (e) {} window.location.href = '/auth/login.php?expired=1'; } // 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') { if (!hasRedirected) { var loggedOutTime = parseInt(e.newValue, 10); if (!isNaN(loggedOutTime) && Math.abs(Date.now() - loggedOutTime) < 10000) { hasRedirected = true; 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 (hasRedirected || 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 op de server redirectToExpired(); return null; } return res.json(); }) .then(function (data) { keepaliveInFlight = false; if (manualClick && keepaliveBtn) { keepaliveBtn.disabled = false; keepaliveBtn.textContent = 'Blijf Ingelogd'; } if (data && data.success) { var currentNow = Date.now(); lastActivity = currentNow; lastPingTime = currentNow; try { localStorage.setItem('fociz_session_last_activity', currentNow.toString()); } catch (e) {} 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() { if (hasRedirected) return; hasRedirected = true; try { localStorage.setItem('fociz_session_logged_out', Date.now().toString()); } catch (e) {} 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() { if (hasRedirected) return; var currentNow = Date.now(); // Controleer of de sessie al verlopen is (bijv. laptop uit slaapstand) var elapsedSec = Math.floor((currentNow - lastActivity) / 1000); if (elapsedSec >= idleTimeout) { redirectToExpired(); return; } // Als de waarschuwingsmodal actief is, vereisen we expliciete klik op 'Blijf Ingelogd' if (isWarningShowing) return; // Lokale activiteit bijwerken (maximaal eens per 3 seconden wegschrijven) if (currentNow - lastActivity > 3000) { lastActivity = currentNow; try { localStorage.setItem('fociz_session_last_activity', currentNow.toString()); } catch (e) {} } // Server keepalive ping (maximaal eens per 60 seconden) var minPingInterval = Math.min(60000, Math.max(15000, (idleTimeout * 1000) / 4)); if (currentNow - lastPingTime > minPingInterval) { lastPingTime = currentNow; sendKeepalive(false); } } // Direct controleren bij terugkeer naar tabblad of ontwaken uit slaapstand function checkIdleOnWakeup() { if (hasRedirected) return; var currentNow = Date.now(); var stored = localStorage.getItem('fociz_session_last_activity'); if (stored) { var parsed = parseInt(stored, 10); if (!isNaN(parsed) && parsed > 0 && parsed <= currentNow) { lastActivity = Math.max(lastActivity, parsed); } } var elapsedSec = Math.floor((currentNow - lastActivity) / 1000); if (elapsedSec >= idleTimeout) { redirectToExpired(); } } window.addEventListener('focus', checkIdleOnWakeup); document.addEventListener('visibilitychange', function () { if (document.visibilityState === 'visible') { checkIdleOnWakeup(); } }); // 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() { if (hasRedirected) return; var currentNow = Date.now(); var elapsedSec = Math.floor((currentNow - lastActivity) / 1000); var remainingSec = idleTimeout - elapsedSec; if (remainingSec <= 0) { redirectToExpired(); 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); } })();