session timeout

This commit is contained in:
2026-10-29 09:19:20 +01:00
parent 2d4ee95ab1
commit b8a8e5ad11
5 changed files with 281 additions and 4 deletions
+2 -1
View File
@@ -1,3 +1,4 @@
<footer class="page-footer"> <footer class="page-footer">
<p class="mb-0">C.E. Soft srl © 2025. All right reserved.</p> <p class="mb-0">C.E. Soft srl © 2025. All right reserved.</p>
</footer> </footer>
<?php include __DIR__ . '/session_timeout.php'; ?>
+20 -3
View File
@@ -62,10 +62,27 @@ require_once(__DIR__ . '/../../languages/en/general.php');
require_once __DIR__ . '/permissions_helper.php'; require_once __DIR__ . '/permissions_helper.php';
require_once(__DIR__ . '/popup-moved.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 { 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 = DBHandlerSelect::getInstance()->getConnection();
$pdoSess->prepare("UPDATE auth_sessions SET last_activity = ? WHERE user_id = ? ORDER BY last_activity DESC LIMIT 1") $pdoSess->prepare("UPDATE `$table` SET last_activity = ? WHERE user_id = ? AND last_activity > ?")
->execute([time(), $iduserlogin]); ->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 */ } catch (\Throwable $e) { /* non bloccare la pagina */
} }
@@ -0,0 +1,32 @@
<?php
/**
* include/session_timeout.php - avviso "stai per essere scollegato" su tutte le pagine userarea.
* Da includere una volta in fondo a include/footer.php.
*
* Per escludere una pagina (es. postazioni tablet sempre accese) mettere PRIMA dell'include del footer:
* $sessionTimeoutDisabled = true;
*/
if (!empty($sessionTimeoutDisabled)) return;
// durata della sessione di Vanguard (minuti), come nel .env (SESSION_LIFETIME)
$__stLifetime = 120;
try {
if (function_exists('config')) $__stLifetime = (int)config('session.lifetime', 120);
} catch (\Throwable $e) {
}
// indirizzo di uscita: la parte prima di /userarea/ + /logout (come il redirect al login di headscript.php)
$__stScript = $_SERVER['SCRIPT_NAME'] ?? '';
$__stPos = strpos($__stScript, '/userarea/');
$__stBase = $__stPos !== false ? substr($__stScript, 0, $__stPos) : '';
?>
<script>
window.SESSION_TIMEOUT = {
lifetimeMin: <?= max(5, (int)$__stLifetime) ?>,
logoutUrl: <?= json_encode($__stBase . '/logout') ?>,
loginUrl: <?= json_encode($__stBase . '/login') ?>,
pingUrl: 'session_ping.php'
};
</script>
<script src="session_timeout.js?v=1"></script>
+13
View File
@@ -0,0 +1,13 @@
<?php
/**
* session_ping.php - tiene viva la sessione mentre l'utente è attivo (chiamato da session_timeout.js).
* headscript.php controlla il login e timbra la sessione: se l'utente non è più collegato fa il redirect al login
* e la pagina se ne accorge perché non riceve JSON.
*/
ob_start();
include __DIR__ . '/include/headscript.php';
while (ob_get_level()) ob_end_clean();
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
echo json_encode(['ok' => true, 'user' => (int)$iduserlogin, 'time' => time()]);
+214
View File
@@ -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 =
'<div class="st-box">' +
'<div class="st-ico">⏱</div>' +
'<div class="st-t">Stai per essere scollegato</div>' +
'<div class="st-p">Non ci sono attività da un po\'. Per sicurezza verrai scollegato tra</div>' +
'<div class="st-c" id="stCount">2:00</div>' +
'<div class="st-b"><button type="button" class="st-stay" id="stStay">Resta collegato</button>' +
'<button type="button" class="st-out" id="stOut">Esci ora</button></div>' +
"</div>";
document.body.appendChild(box);
document.getElementById("stStay").addEventListener("click", stay);
document.getElementById("stOut").addEventListener("click", logout);
setTimeout(function () {
const b = document.getElementById("stStay");
if (b) b.focus();
}, 50);
tick();
}
function hideBox() {
if (box) box.remove();
box = null;
}
function stay() {
setLast(Date.now());
hideBox();
ping(true);
}
function tick() {
const el = document.getElementById("stCount");
if (!el) return;
const left = Math.max(
0,
Math.ceil((getLast() + LIMIT - Date.now()) / 1000),
);
el.textContent =
Math.floor(left / 60) + ":" + String(left % 60).padStart(2, "0");
}
/* ---------------- controllo periodico ---------------- */
function check() {
if (leaving) return;
const idle = Date.now() - getLast();
if (idle >= LIMIT) {
logout();
return;
}
if (idle >= LIMIT - WARN) {
showBox();
tick();
} else if (box) {
hideBox(); // attività in un'altra scheda
}
}
setLast(Math.max(getLast(), Date.now())); // aprire una pagina conta come attività
setInterval(check, 1000);
window.SessionTimeout = { stay: stay, logout: logout, check: check };
})();