Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 60 additions & 53 deletions VueApp/src/components/SessionTimeout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})
}

function hideSessionTimeoutWarning() {
showSessionTimeoutWarning.value = false
sessionExpired.value = false
sessionReloaded.value = false
sessionExtendFailed.value = false
}

sessionTimeoutCheckEventId = window.setTimeout(checkSessionTimeout, 60000)
Expand Down Expand Up @@ -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"
></q-btn>
<q-btn
Expand All @@ -123,6 +124,12 @@ sessionTimeoutCheckEventId = window.setTimeout(checkSessionTimeout, 60000)
@click="extendSession"
></q-btn>
</q-card-section>
<q-card-section
v-if="sessionExtendFailed && !sessionExpired"
class="q-pt-none"
>
<StatusBanner type="error"> Could not extend your session. Please try again. </StatusBanner>
</q-card-section>
</q-card>
</q-dialog>
</template>
Loading