diff --git a/VueApp/src/components/SessionTimeout.vue b/VueApp/src/components/SessionTimeout.vue
index 27c1be08..f4f4c45e 100644
--- a/VueApp/src/components/SessionTimeout.vue
+++ b/VueApp/src/components/SessionTimeout.vue
@@ -2,83 +2,84 @@
import { ref } from "vue"
import { useUserStore } from "@/store/UserStore"
import { getLoginUrl } from "@/composables/RequireLogin"
+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 sessionTimeoutUrl = `${import.meta.env.VITE_API_URL}sessionTimeout`
const loginHref = getLoginUrl()
-const sessionRefreshUrl =
- (onDev ? "http://localhost/" : "/") +
- "public/timeout/seconds_until_timeout_v2.cfm?id=" +
- userStore.userInfo.loginId +
- "&service=" +
- (onDev ? "Viper2-dev" : "Viper2")
const showSessionTimeoutWarning = ref(false)
const sessionExpireTime = ref("")
const sessionExpired = ref(false)
let sessionTimeoutCheckEventId = 0
const sessionReloaded = ref(false)
+const sessionExtendFailed = ref(false)
+// Hour 0 is 12 AM, not 0 AM.
+function formatExpireTime(sessionTimeoutDateTime: string) {
+ const d = new Date(sessionTimeoutDateTime)
+ return (d.getHours() % 12 || 12) + ":" + ("0" + d.getMinutes()).slice(-2) + (d.getHours() >= 12 ? " PM" : " AM")
+}
+
+// Plain fetch rather than useFetch, deliberately: these are session lifecycle calls. useFetch
+// pushes every failure into the global error store, which would raise a banner on each blip of a
+// silent five minute poll, and would fire the auth handler at exactly the moment our own dialog
+// is offering the user a log in.
async function checkSessionTimeout() {
- if (
- userStore.userInfo.loginId === undefined ||
- userStore.userInfo.loginId === null ||
- userStore.userInfo.loginId.length === 0
- ) {
+ 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)
@@ -111,7 +112,7 @@ sessionTimeoutCheckEventId = window.setTimeout(checkSessionTimeout, 60000)
color="secondary"
class="q-px-md"
label="Log in"
- v-if="sessionExpired"
+ v-if="sessionExpired || sessionExtendFailed"
:href="loginHref"
>
+
+ Could not extend your session. Please try again.
+