diff --git a/backend/workflows/static/workflows/js/session_warning.js b/backend/workflows/static/workflows/js/session_warning.js index 6ea9ca5..9b460d5 100644 --- a/backend/workflows/static/workflows/js/session_warning.js +++ b/backend/workflows/static/workflows/js/session_warning.js @@ -18,6 +18,8 @@ let lastConfirmedAt = Date.now(); let warningVisible = false; let keepaliveInFlight = false; + let timeoutCheckInFlight = false; + let redirectInFlight = false; function getCsrfToken() { const cookie = document.cookie @@ -44,11 +46,22 @@ status.textContent = ""; } + function redirectToLogin() { + if (redirectInFlight) return; + redirectInFlight = true; + window.location.href = config.loginUrl; + } + function readStoredConfirmedAt() { try { const raw = window.localStorage.getItem(storageKey); const parsed = raw ? Number.parseInt(raw, 10) : NaN; - return Number.isFinite(parsed) ? parsed : null; + if (!Number.isFinite(parsed)) return null; + const maxAgeMs = config.idleTimeoutSeconds * 1000; + if (Date.now() - parsed >= maxAgeMs) { + return null; + } + return parsed; } catch (_error) { return null; } @@ -95,8 +108,9 @@ credentials: "same-origin", body: JSON.stringify({ keepalive: true }), }); - if (!response.ok) { - window.location.href = config.loginUrl; + const contentType = (response.headers.get("content-type") || "").toLowerCase(); + if (!response.ok || response.redirected || !contentType.includes("application/json")) { + redirectToLogin(); return; } syncConfirmedAt(Date.now(), "self"); @@ -108,12 +122,46 @@ hideStatus(); }, 1200); } catch (_error) { - window.location.href = config.loginUrl; + redirectToLogin(); } finally { keepaliveInFlight = false; } } + async function confirmSessionOrRedirect() { + if (timeoutCheckInFlight || keepaliveInFlight || redirectInFlight) return; + timeoutCheckInFlight = true; + try { + const response = await fetch(config.keepaliveUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-CSRFToken": getCsrfToken(), + "X-Requested-With": "XMLHttpRequest", + }, + credentials: "same-origin", + body: JSON.stringify({ keepalive: true, timeout_check: true }), + }); + const contentType = (response.headers.get("content-type") || "").toLowerCase(); + if (!response.ok || response.redirected || !contentType.includes("application/json")) { + try { + window.localStorage.removeItem(storageKey); + } catch (_error) { + // Ignore storage cleanup failures. + } + redirectToLogin(); + return; + } + syncConfirmedAt(Date.now(), "self"); + hideWarning(); + hideStatus(); + } catch (_error) { + redirectToLogin(); + } finally { + timeoutCheckInFlight = false; + } + } + const storedConfirmedAt = readStoredConfirmedAt(); if (storedConfirmedAt) { lastConfirmedAt = storedConfirmedAt; @@ -149,7 +197,7 @@ const elapsedSeconds = Math.floor((Date.now() - lastConfirmedAt) / 1000); const secondsLeft = config.idleTimeoutSeconds - elapsedSeconds; if (secondsLeft <= 0) { - window.location.href = config.loginUrl; + confirmSessionOrRedirect(); return; } if (secondsLeft <= warningLeadSeconds) {