From 572fe7fdced24b6f0a7d394cdf4efedbb5c2dae6 Mon Sep 17 00:00:00 2001 From: Rex Lorenzo Date: Fri, 7 Aug 2026 18:42:40 -0700 Subject: [PATCH] feat(session): move the SPA session poll off the legacy CFM - Point the poll at the VIPER 2 endpoint via VITE_API_URL, dropping the CFM URL that passed the login id as an unauthenticated query parameter - Keep plain fetch rather than useFetch, which reports every failure through the global error store: that would banner a silent five minute poll and fire the auth handler while our dialog offers a log in - Carry over the Razor fixes, since this file had the same defects: reject non-OK responses, report a failed extend in a StatusBanner while offering both Refresh Session and Log in, reschedule after a failed poll rather than stopping for the life of the page, stand the warning down when the session is extended elsewhere, and render midnight as 12 AM rather than 0 AM --- VueApp/src/components/SessionTimeout.vue | 111 ++++++++++++----------- 1 file changed, 58 insertions(+), 53 deletions(-) diff --git a/VueApp/src/components/SessionTimeout.vue b/VueApp/src/components/SessionTimeout.vue index ddfa77c92..ec4957903 100644 --- a/VueApp/src/components/SessionTimeout.vue +++ b/VueApp/src/components/SessionTimeout.vue @@ -2,82 +2,81 @@ import { ref } from "vue" import { useUserStore } from "@/store/UserStore" import LoginButton from "@/components/LoginButton.vue" +import StatusBanner from "@/components/StatusBanner.vue" const userStore = useUserStore() -//https://" + HttpHelper.HttpContext?.Request.Host.Value -const onDev = import.meta.env.VITE_ENVIRONMENT === "DEVELOPMENT" const viperHome = import.meta.env.VITE_VIPER_HOME -const sessionRefreshUrl = - (onDev ? "http://localhost/" : "/") + - "public/timeout/seconds_until_timeout_v2.cfm?id=" + - userStore.userInfo.loginId + - "&service=" + - (onDev ? "Viper2-dev" : "Viper2") +const sessionTimeoutUrl = `${import.meta.env.VITE_API_URL}sessionTimeout` const showSessionTimeoutWarning = ref(false) const sessionExpireTime = ref("") const sessionExpired = ref(false) let sessionTimeoutCheckEventId = 0 const sessionReloaded = ref(false) +const sessionExtendFailed = ref(false) +function formatExpireTime(sessionTimeoutDateTime: string) { + return new Date(sessionTimeoutDateTime).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" }) +} + +// Plain fetch, not useFetch: that reports every failure to the global error store and fires the +// auth handler, which would spam a silent poll and fight our own dialog's Log in. async function checkSessionTimeout() { - if ( - userStore.userInfo.loginId === undefined || - userStore.userInfo.loginId === null || - userStore.userInfo.loginId.length === 0 - ) { + // One live timer only: a refresh that overlaps an in-flight poll must not leave two chains running. + clearTimeout(sessionTimeoutCheckEventId) + if (!userStore.userInfo.loginId) { return //don't check the session if the user is not logged in } - //try to get the session timeout from an external application - try { - fetch(sessionRefreshUrl) - .then((r) => (r.status === 200 ? r.json() : r)) - .then((r) => { - let nextCheck = 300 - //show timeout warning if the session will time out in 5 minutes or less - if (r.secondsUntilTimeout !== undefined && r.secondsUntilTimeout < 300) { - showSessionTimeoutWarning.value = true - sessionExpired.value = r.secondsUntilTimeout < 15 //consider session timing out in 15 seconds to be timed out already - var d = new Date(r.sessionTimeoutDateTime) - sessionExpireTime.value = - (d.getHours() > 12 ? d.getHours() - 12 : d.getHours()) + - ":" + - ("0" + d.getMinutes()).slice(-2) + - (d.getHours() >= 12 ? " PM" : " AM") - nextCheck = sessionExpired.value ? 0 : Math.max(r.secondsUntilTimeout - 15, 5) - } - if (nextCheck > 0) { - sessionTimeoutCheckEventId = window.setTimeout(checkSessionTimeout, nextCheck * 1000) - } - }) - } catch (e) { - void e - } -} -async function extendSession() { - fetch(viperHome + "RefreshSession") - .then((r) => (r.status === 200 ? r.json() : r)) + // Timeout so a request that hangs rather than fails still reaches the catch below. + fetch(sessionTimeoutUrl, { signal: AbortSignal.timeout(10000) }) + .then((r) => (r.ok ? r.json() : Promise.reject(new Error("Session check returned " + r.status)))) .then((r) => { - try { - clearTimeout(sessionTimeoutCheckEventId) - } catch (e) { - void e + let nextCheck = 300 + //show timeout warning if the session will time out in 5 minutes or less + // "<=" so an exact 300 still warns: otherwise the next poll lands at expiry. + if (r.secondsUntilTimeout !== undefined && r.secondsUntilTimeout <= 300) { + showSessionTimeoutWarning.value = true + sessionExpired.value = r.secondsUntilTimeout < 15 //consider session timing out in 15 seconds to be timed out already + sessionExpireTime.value = formatExpireTime(r.sessionTimeoutDateTime) + nextCheck = sessionExpired.value ? 0 : Math.max(r.secondsUntilTimeout - 15, 5) + } else if (r.secondsUntilTimeout !== undefined) { + // Extended elsewhere, in another tab or by an API call, so stand the warning down. + hideSessionTimeoutWarning() + } + if (nextCheck > 0) { + sessionTimeoutCheckEventId = window.setTimeout(checkSessionTimeout, nextCheck * 1000) } + }) + // Silent, but reschedule: one failed poll must not stop the checks for the life of the page. + // Retry quickly once the warning is up, since expiry is minutes away. + .catch(() => { + const retry = showSessionTimeoutWarning.value ? 15000 : 300000 + sessionTimeoutCheckEventId = window.setTimeout(checkSessionTimeout, retry) + }) +} - var d = new Date(r.sessionTimeoutDateTime) - sessionExpireTime.value = - (d.getHours() > 12 ? d.getHours() - 12 : d.getHours()) + - ":" + - ("0" + d.getMinutes()).slice(-2) + - (d.getHours() >= 12 ? " PM" : " AM") +async function extendSession() { + sessionExtendFailed.value = false + fetch(viperHome + "RefreshSession", { signal: AbortSignal.timeout(10000) }) + .then((r) => (r.ok ? r.json() : Promise.reject(new Error("RefreshSession returned " + r.status)))) + .then((r) => { + clearTimeout(sessionTimeoutCheckEventId) + sessionExpireTime.value = formatExpireTime(r.sessionTimeoutDateTime) sessionReloaded.value = true sessionTimeoutCheckEventId = window.setTimeout(checkSessionTimeout, 5000) window.setTimeout(hideSessionTimeoutWarning, 1000) }) + // Leave the dialog up so the user can retry or log in, and say so: the session was not extended. + .catch(() => { + sessionExtendFailed.value = true + }) } + function hideSessionTimeoutWarning() { showSessionTimeoutWarning.value = false + sessionExpired.value = false sessionReloaded.value = false + sessionExtendFailed.value = false } sessionTimeoutCheckEventId = window.setTimeout(checkSessionTimeout, 60000) @@ -106,7 +105,7 @@ sessionTimeoutCheckEventId = window.setTimeout(checkSessionTimeout, 60000)
Your session has been extended to {{ sessionExpireTime }}.
@@ -129,6 +128,12 @@ sessionTimeoutCheckEventId = window.setTimeout(checkSessionTimeout, 60000) @click="hideSessionTimeoutWarning" > + + Could not extend your session. Please try again. +