532 lines
11 KiB
JavaScript
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();
|
|
}
|
|
})();
|