diff --git a/public/userarea/include/footer.php b/public/userarea/include/footer.php index b33e5b54..16ba3364 100644 --- a/public/userarea/include/footer.php +++ b/public/userarea/include/footer.php @@ -1,3 +1,4 @@ \ No newline at end of file + + \ No newline at end of file diff --git a/public/userarea/include/headscript.php b/public/userarea/include/headscript.php index d44b3b75..eb6a8dbd 100644 --- a/public/userarea/include/headscript.php +++ b/public/userarea/include/headscript.php @@ -62,10 +62,27 @@ require_once(__DIR__ . '/../../languages/en/general.php'); require_once __DIR__ . '/permissions_helper.php'; require_once(__DIR__ . '/popup-moved.php'); -// Timbra la sessione dell'utente a ogni pagina (rolling manuale) +// Timbra la sessione: tiene vive le sessioni ancora valide dell'utente e rinnova il cookie di Laravel try { + $now = time(); + $lifetime = (int)(function_exists('config') ? config('session.lifetime', 120) : 120); // minuti + $table = function_exists('config') ? config('session.table', 'auth_sessions') : 'auth_sessions'; + $pdoSess = DBHandlerSelect::getInstance()->getConnection(); - $pdoSess->prepare("UPDATE auth_sessions SET last_activity = ? WHERE user_id = ? ORDER BY last_activity DESC LIMIT 1") - ->execute([time(), $iduserlogin]); + $pdoSess->prepare("UPDATE `$table` SET last_activity = ? WHERE user_id = ? AND last_activity > ?") + ->execute([$now, $iduserlogin, $now - $lifetime * 60]); + + // le pagine userarea non passano da Laravel: rinnovo io la scadenza del cookie di sessione + $cookie = function_exists('config') ? config('session.cookie') : null; + if ($cookie && !empty($_COOKIE[$cookie]) && !headers_sent() && !(function_exists('config') && config('session.expire_on_close'))) { + setcookie($cookie, $_COOKIE[$cookie], [ + 'expires' => $now + $lifetime * 60, + 'path' => function_exists('config') ? (config('session.path') ?: '/') : '/', + 'domain' => function_exists('config') ? (config('session.domain') ?: '') : '', + 'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off', + 'httponly' => true, + 'samesite' => function_exists('config') ? (config('session.same_site') ?: 'Lax') : 'Lax', + ]); + } } catch (\Throwable $e) { /* non bloccare la pagina */ } diff --git a/public/userarea/include/session_timeout.php b/public/userarea/include/session_timeout.php new file mode 100644 index 00000000..53ddf611 --- /dev/null +++ b/public/userarea/include/session_timeout.php @@ -0,0 +1,32 @@ + + + \ No newline at end of file diff --git a/public/userarea/session_ping.php b/public/userarea/session_ping.php new file mode 100644 index 00000000..8193852f --- /dev/null +++ b/public/userarea/session_ping.php @@ -0,0 +1,13 @@ + true, 'user' => (int)$iduserlogin, 'time' => time()]); diff --git a/public/userarea/session_timeout.js b/public/userarea/session_timeout.js new file mode 100644 index 00000000..1a196e7a --- /dev/null +++ b/public/userarea/session_timeout.js @@ -0,0 +1,214 @@ +/** + * session_timeout.js - scollega l'utente dopo un periodo di INATTIVITÀ vera (mouse, tastiera, tocco, scorrimento), + * con un avviso e il conto alla rovescia prima della scadenza. + * + * - L'ultima attività è condivisa tra le schede dello stesso browser (localStorage): se lavori in una scheda + * le altre non ti scollegano. + * - Finché l'utente è attivo, al massimo una volta al minuto la sessione viene timbrata sul server (session_ping.php). + * - Le chiamate automatiche delle pagine (aggiornamenti periodici) NON contano come attività. + * + * Configurazione da window.SESSION_TIMEOUT (scritta da include/session_timeout.php). + */ +(function () { + "use strict"; + const CFG = Object.assign( + { + lifetimeMin: 120, + warnSec: 120, // avviso N secondi prima + marginSec: 90, // scollego un po' prima della scadenza vera del server + pingEverySec: 60, + logoutUrl: "/logout", + loginUrl: "/login", + pingUrl: "session_ping.php", + }, + window.SESSION_TIMEOUT || {}, + ); + + const KEY = "zibo_last_activity"; + const LIMIT = Math.max(120, CFG.lifetimeMin * 60 - CFG.marginSec) * 1000; // inattività massima (ms) + const WARN = Math.min(CFG.warnSec * 1000, LIMIT - 30000); + let lastPing = 0; + let box = null; + let leaving = false; + + /* ---------------- ultima attività, condivisa tra schede ---------------- */ + let memLast = Date.now(); + + function getLast() { + try { + const v = parseInt(localStorage.getItem(KEY), 10); + if (v && v > memLast) memLast = v; + } catch (e) {} + return memLast; + } + + function setLast(t) { + memLast = t; + try { + localStorage.setItem(KEY, String(t)); + } catch (e) {} + } + + /* ---------------- ping al server ---------------- */ + function ping(force) { + const now = Date.now(); + if (!force && now - lastPing < CFG.pingEverySec * 1000) + return Promise.resolve(true); + lastPing = now; + return fetch(CFG.pingUrl + "?t=" + now, { + credentials: "same-origin", + cache: "no-store", + headers: { "X-Requested-With": "XMLHttpRequest" }, + }) + .then(function (r) { + const ct = r.headers.get("content-type") || ""; + if (!r.ok || ct.indexOf("application/json") === -1) + throw new Error("sessione scaduta"); + return r.json(); + }) + .then(function (d) { + if (!d || !d.ok) throw new Error("sessione scaduta"); + return true; + }) + .catch(function () { + // il server non ci riconosce più: al login + goLogin(); + return false; + }); + } + + function goLogin() { + if (leaving) return; + leaving = true; + location.href = CFG.loginUrl; + } + + function logout() { + if (leaving) return; + leaving = true; + try { + localStorage.removeItem(KEY); + } catch (e) {} + location.href = CFG.logoutUrl; + } + + /* ---------------- attività dell'utente ---------------- */ + let throttle = 0; + + function activity() { + if (leaving) return; + const now = Date.now(); + if (now - throttle < 2000) return; // basta una registrazione ogni 2 secondi + throttle = now; + if (box) return; // con l'avviso aperto conta solo il pulsante + setLast(now); + ping(false); + } + [ + "mousedown", + "mousemove", + "keydown", + "touchstart", + "scroll", + "wheel", + ].forEach(function (ev) { + window.addEventListener(ev, activity, { passive: true, capture: true }); + }); + document.addEventListener("visibilitychange", function () { + if (!document.hidden) check(); + }); + window.addEventListener("storage", function (e) { + if (e.key === KEY && box) { + // l'utente è tornato attivo in un'altra scheda + const idle = Date.now() - getLast(); + if (idle < LIMIT - WARN) hideBox(); + } + }); + + /* ---------------- avviso ---------------- */ + function css() { + if (document.getElementById("st-css")) return; + const s = document.createElement("style"); + s.id = "st-css"; + s.textContent = + ".st-ov{position:fixed;inset:0;z-index:20000;background:rgba(15,23,42,.55);display:flex;align-items:center;justify-content:center;padding:16px}" + + ".st-box{background:#fff;border-radius:18px;max-width:440px;width:100%;padding:26px 24px 22px;text-align:center;box-shadow:0 24px 60px rgba(0,0,0,.3);font-family:inherit;color:#1f2d3d}" + + ".st-ico{width:64px;height:64px;border-radius:50%;background:#fef3c7;color:#b45309;display:flex;align-items:center;justify-content:center;font-size:30px;margin:0 auto 12px}" + + ".st-t{font-size:1.25rem;font-weight:800;margin-bottom:6px}" + + ".st-p{color:#475569;margin-bottom:10px}" + + ".st-c{font-size:2.6rem;font-weight:900;color:#b45309;font-variant-numeric:tabular-nums;margin:6px 0 16px}" + + ".st-b{display:flex;gap:10px;justify-content:center;flex-wrap:wrap}" + + ".st-b button{border:0;border-radius:12px;padding:13px 22px;font-weight:800;font-size:1rem;min-height:50px;cursor:pointer}" + + ".st-stay{background:#198754;color:#fff;flex:1 1 200px}" + + ".st-out{background:#f1f5f9;color:#334155}"; + document.head.appendChild(s); + } + + function showBox() { + if (box || leaving) return; + css(); + box = document.createElement("div"); + box.className = "st-ov"; + box.setAttribute("role", "alertdialog"); + box.setAttribute("aria-modal", "true"); + box.innerHTML = + '