session timeout
This commit is contained in:
@@ -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 };
|
||||
})();
|
||||
Reference in New Issue
Block a user