gallery_server_client_s190js/public/js/logout.js
2026-10-26 11:00:14 +01:00

532 lines
11 KiB
JavaScript

// ===============================
// LOGOUT — pulizia token + revoca + auto-logout JWT
// ===============================
(() => {
const AUTH_LOGOUT_ENDPOINT = "/auth/logout";
const TOKEN_KEYS = ["token", "access_token", "refresh_token"];
const LONG_PRESS_MS = 650;
const LONG_PRESS_MOVE_PX = 12;
let contextMenu = null;
let suppressNextLogoutClick = false;
// -------------------------------
// Helpers token
// -------------------------------
function getToken() {
try {
return (
localStorage.getItem("token") ||
localStorage.getItem("access_token") ||
""
);
} catch {
return "";
}
}
function clearTokens() {
try {
TOKEN_KEYS.forEach(k => localStorage.removeItem(k));
} catch {}
}
// -------------------------------
// Logout lato server
// -------------------------------
async function serverLogout(token) {
try {
const headers = token ? { Authorization: `Bearer ${token}` } : {};
await fetch(AUTH_LOGOUT_ENDPOINT, { method: "POST", headers });
} catch (err) {
console.warn("Logout server fallito (ignoro):", err);
}
}
// -------------------------------
// Redirect
// -------------------------------
function getRedirectUrl() {
const btn = document.querySelector("[data-logout]");
return btn?.getAttribute("data-redirect") || "/";
}
function redirect(url) {
window.location.assign(url || "/");
}
// -------------------------------
// Auto-logout alla scadenza JWT
// -------------------------------
let autoLogoutTimer = null;
function scheduleAutoLogout() {
clearTimeout(autoLogoutTimer);
const token = getToken();
if (!token) return;
const payload = parseJwt(token); // usa la funzione globale già esistente
const expSec = payload?.exp;
if (!expSec) return;
const msToExp = expSec * 1000 - Date.now();
if (msToExp <= 0) {
doLogout({ doRedirect: true });
return;
}
autoLogoutTimer = setTimeout(() => {
doLogout({ doRedirect: true });
}, msToExp);
}
// -------------------------------
// UI helpers
// -------------------------------
function setButtonsDisabled(disabled) {
document.querySelectorAll("[data-logout]").forEach(btn => {
btn.disabled = disabled;
btn.setAttribute("aria-busy", disabled ? "true" : "false");
});
}
// -------------------------------
// Azione principale
// -------------------------------
async function doLogout({ doRedirect = true } = {}) {
const token = getToken();
setButtonsDisabled(true);
await serverLogout(token);
clearTokens();
try {
window.dispatchEvent(new CustomEvent("logout:success"));
} catch {}
if (doRedirect) redirect(getRedirectUrl());
setButtonsDisabled(false);
}
// -------------------------------
// Menu contestuale Logout
// -------------------------------
function currentGalleryMode() {
return window.state?.mode === "activities"
? "activities"
: "photos";
}
function closeLogoutContextMenu() {
if (contextMenu) {
contextMenu.remove();
contextMenu = null;
}
document.removeEventListener(
"pointerdown",
onContextOutsidePointer,
true
);
document.removeEventListener(
"keydown",
onContextKeyDown,
true
);
}
function onContextOutsidePointer(event) {
if (!contextMenu) return;
if (contextMenu.contains(event.target)) {
return;
}
if (event.target.closest?.("[data-logout]")) {
return;
}
closeLogoutContextMenu();
}
function onContextKeyDown(event) {
if (event.key === "Escape") {
event.preventDefault();
closeLogoutContextMenu();
}
}
async function authFetch(url, options = {}) {
const token = getToken();
const headers = {
...(options.headers || {})
};
if (token) {
headers.Authorization = `Bearer ${token}`;
}
const response = await fetch(url, {
...options,
headers
});
if (!response.ok) {
let detail = "";
try {
const data = await response.json();
detail = data?.error || data?.message || "";
} catch {}
throw new Error(
detail ||
`HTTP ${response.status}`
);
}
return response;
}
async function startContextScan(mode) {
const endpoint =
mode === "activities"
? "/scanActivities"
: "/scan";
const label =
mode === "activities"
? "attivita"
: "foto";
closeLogoutContextMenu();
console.log(
`[LogoutMenu] Avvio scansione ${label}: ${endpoint}`
);
try {
await authFetch(endpoint);
console.log(
`[LogoutMenu] Scansione ${label} avviata`
);
} catch (error) {
console.error(
`[LogoutMenu] Errore scansione ${label}:`,
error
);
}
}
function createContextMenuButton(label, action, danger = false) {
const button = document.createElement("button");
button.type = "button";
button.textContent = label;
Object.assign(button.style, {
display: "block",
width: "100%",
border: "0",
margin: "0",
padding: "12px 16px",
textAlign: "left",
background: "transparent",
color: danger ? "#ff8d8d" : "inherit",
font: "inherit",
cursor: "pointer",
whiteSpace: "nowrap",
borderRadius: "8px"
});
button.addEventListener("pointerenter", () => {
button.style.background =
"rgba(255,255,255,0.10)";
});
button.addEventListener("pointerleave", () => {
button.style.background = "transparent";
});
button.addEventListener("click", async event => {
event.preventDefault();
event.stopPropagation();
await action();
});
return button;
}
function showLogoutContextMenu(btn) {
closeLogoutContextMenu();
const mode = currentGalleryMode();
const menu = document.createElement("div");
contextMenu = menu;
menu.id = "logoutContextMenu";
menu.setAttribute("role", "menu");
Object.assign(menu.style, {
position: "fixed",
zIndex: "100000",
minWidth: "170px",
padding: "6px",
borderRadius: "12px",
background: "rgba(28,28,30,0.98)",
color: "#fff",
boxShadow: "0 10px 30px rgba(0,0,0,0.38)",
border: "1px solid rgba(255,255,255,0.12)",
backdropFilter: "blur(12px)"
});
const scanLabel =
mode === "activities"
? "Scan attivita"
: "Scan foto";
menu.appendChild(
createContextMenuButton(
scanLabel,
() => startContextScan(mode)
)
);
const separator = document.createElement("div");
Object.assign(separator.style, {
height: "1px",
margin: "5px 4px",
background: "rgba(255,255,255,0.12)"
});
menu.appendChild(separator);
menu.appendChild(
createContextMenuButton(
"Logout",
async () => {
closeLogoutContextMenu();
await doLogout({
doRedirect: true
});
},
true
)
);
document.body.appendChild(menu);
const rect = btn.getBoundingClientRect();
const menuRect = menu.getBoundingClientRect();
let left =
rect.right - menuRect.width;
let top =
rect.bottom + 8;
if (
top + menuRect.height >
window.innerHeight - 8
) {
top =
rect.top -
menuRect.height -
8;
}
left = Math.max(
8,
Math.min(
left,
window.innerWidth -
menuRect.width -
8
)
);
top = Math.max(
8,
Math.min(
top,
window.innerHeight -
menuRect.height -
8
)
);
menu.style.left = `${left}px`;
menu.style.top = `${top}px`;
setTimeout(() => {
document.addEventListener(
"pointerdown",
onContextOutsidePointer,
true
);
document.addEventListener(
"keydown",
onContextKeyDown,
true
);
}, 0);
}
function bindLogoutLongPress(btn) {
let timer = null;
let startX = 0;
let startY = 0;
let activePointerId = null;
let longPressTriggered = false;
function clearLongPressTimer() {
if (timer) {
clearTimeout(timer);
timer = null;
}
}
btn.addEventListener("pointerdown", event => {
if (btn.disabled) return;
if (
event.pointerType === "mouse" &&
event.button !== 0
) {
return;
}
closeLogoutContextMenu();
activePointerId = event.pointerId;
startX = event.clientX;
startY = event.clientY;
longPressTriggered = false;
timer = setTimeout(() => {
timer = null;
longPressTriggered = true;
suppressNextLogoutClick = true;
if (navigator.vibrate) {
try {
navigator.vibrate(25);
} catch {}
}
showLogoutContextMenu(btn);
}, LONG_PRESS_MS);
});
btn.addEventListener("pointermove", event => {
if (
activePointerId !== event.pointerId ||
!timer
) {
return;
}
const distance = Math.hypot(
event.clientX - startX,
event.clientY - startY
);
if (distance > LONG_PRESS_MOVE_PX) {
clearLongPressTimer();
}
});
btn.addEventListener("pointerup", event => {
if (
activePointerId !== null &&
activePointerId !== event.pointerId
) {
return;
}
clearLongPressTimer();
activePointerId = null;
if (longPressTriggered) {
event.preventDefault();
event.stopPropagation();
}
});
btn.addEventListener("pointercancel", () => {
clearLongPressTimer();
activePointerId = null;
longPressTriggered = false;
});
btn.addEventListener("contextmenu", event => {
event.preventDefault();
suppressNextLogoutClick = true;
showLogoutContextMenu(btn);
});
}
// -------------------------------
// Bind pulsanti
// -------------------------------
function bindLogoutButtons() {
document.querySelectorAll("[data-logout]").forEach(btn => {
btn.addEventListener("click", async (e) => {
e.preventDefault();
if (suppressNextLogoutClick) {
suppressNextLogoutClick = false;
return;
}
if (btn.disabled) return;
closeLogoutContextMenu();
await doLogout({ doRedirect: true });
});
bindLogoutLongPress(btn);
});
}
// -------------------------------
// Public API
// -------------------------------
window.AppAuth = Object.freeze({
logout: (opts) => doLogout(opts),
isLoggedIn: () => !!getToken()
});
// -------------------------------
// Init
// -------------------------------
function init() {
bindLogoutButtons();
scheduleAutoLogout();
document.querySelectorAll("[data-logout]").forEach(btn => {
if (!window.AppAuth.isLoggedIn()) btn.style.display = "none";
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();