// =============================== // 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(); } })();