From c9ea1c8948ec281d0fff0398de1aba7f640a87e5 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Thu, 29 Jan 2026 16:02:29 +0000 Subject: [PATCH] Consistent and stronger session revalidation checks in Auth profile and Admin App. Fix missing handling of link generation network errors. --- frontend/auth/App.vue | 29 ++------- frontend/auth/admin/AdminApp.vue | 20 +++++-- .../src/components/RegistrationLinkModal.vue | 5 +- frontend/src/utils/session.js | 59 +++++++++++++++++++ 4 files changed, 81 insertions(+), 32 deletions(-) create mode 100644 frontend/src/utils/session.js diff --git a/frontend/auth/App.vue b/frontend/auth/App.vue index a4a54d1..96949f3 100644 --- a/frontend/auth/App.vue +++ b/frontend/auth/App.vue @@ -14,6 +14,7 @@ import { computed, onMounted, onUnmounted, ref } from 'vue' import { useAuthStore } from '@/stores/auth' import { apiJson, getAuthIframeUrl } from '@/utils/api' +import { useSessionValidation } from '@/utils/session' import StatusMessage from '@/components/StatusMessage.vue' import ProfileView from '@/components/ProfileView.vue' import HostProfileView from '@/components/HostProfileView.vue' @@ -46,7 +47,7 @@ const isHostMode = computed(() => { const configuredHost = normalizeHost(authHost) return currentHost !== configuredHost }) -let validationTimer = null +const userUuid = computed(() => store.userInfo?.ctx.user.uuid) let authIframe = null function terminateSession() { @@ -54,11 +55,12 @@ function terminateSession() { viewState.value = 'terminal' } +useSessionValidation(userUuid, terminateSession) + async function loadUserInfo() { try { store.userInfo = await apiJson('/auth/api/user-info', { method: 'POST' }) viewState.value = 'profile' - startSessionValidation() return true } catch { store.userInfo = null @@ -128,28 +130,6 @@ function handleAuthMessage(event) { } } -async function validateSession() { - try { - await apiJson('/auth/api/validate', { method: 'POST' }) - } catch { - stopSessionValidation() - terminateSession() - } -} - -function startSessionValidation() { - // Validate session every 2 minutes - stopSessionValidation() - validationTimer = setInterval(validateSession, 2 * 60 * 1000) -} - -function stopSessionValidation() { - if (validationTimer) { - clearInterval(validationTimer) - validationTimer = null - } -} - onMounted(async () => { // Listen for postMessage from auth iframe window.addEventListener('message', handleAuthMessage) @@ -178,7 +158,6 @@ onMounted(async () => { onUnmounted(() => { window.removeEventListener('message', handleAuthMessage) - stopSessionValidation() hideAuthIframe() }) diff --git a/frontend/auth/admin/AdminApp.vue b/frontend/auth/admin/AdminApp.vue index fa3551c..81a029c 100644 --- a/frontend/auth/admin/AdminApp.vue +++ b/frontend/auth/admin/AdminApp.vue @@ -13,6 +13,7 @@ import AdminDialogs from '@/admin/AdminDialogs.vue' import { useAuthStore } from '@/stores/auth' import { adminUiPath, makeUiHref } from '@/utils/settings' import { apiJson } from '@/utils/api' +import { useSessionValidation } from '@/utils/session' import { getDirection } from '@/utils/keynav' import { goBack } from '@/utils/helpers' @@ -157,6 +158,18 @@ function clearSensitiveState() { authenticated.value = false } +function onSessionLost(e) { + clearSensitiveState() + if (e.name === 'AuthCancelledError') { + showBackMessage.value = true + } else { + error.value = e.message + } +} + +const userUuid = computed(() => info.value?.ctx.user.uuid) +useSessionValidation(userUuid, onSessionLost) + async function load() { loading.value = true loadingMessage.value = 'Loading...' @@ -177,12 +190,7 @@ async function load() { } } else parseHash() } catch (e) { - clearSensitiveState() - if (e.name === 'AuthCancelledError') { - showBackMessage.value = true - } else { - error.value = e.message - } + onSessionLost(e) } finally { loading.value = false } diff --git a/frontend/src/components/RegistrationLinkModal.vue b/frontend/src/components/RegistrationLinkModal.vue index 4d58158..e154f13 100644 --- a/frontend/src/components/RegistrationLinkModal.vue +++ b/frontend/src/components/RegistrationLinkModal.vue @@ -38,6 +38,7 @@ import QRCodeDisplay from '@/components/QRCodeDisplay.vue' import { apiJson } from '@/utils/api' import { formatDate } from '@/utils/helpers' import { getDirection } from '@/utils/keynav' +import { useAuthStore } from '@/stores/auth' const props = defineProps({ endpoint: { type: String, required: true }, @@ -46,6 +47,7 @@ const props = defineProps({ const emit = defineEmits(['close', 'copied']) +const authStore = useAuthStore() const dialog = ref(null) const linkUrl = ref(null) const expiresAt = ref(null) @@ -73,7 +75,8 @@ async function generateLink() { } else { emit('close') } - } catch { + } catch (e) { + authStore.showMessage(e.message || 'Failed to generate link', 'error') emit('close') } } diff --git a/frontend/src/utils/session.js b/frontend/src/utils/session.js new file mode 100644 index 0000000..63e068a --- /dev/null +++ b/frontend/src/utils/session.js @@ -0,0 +1,59 @@ +import { onMounted, onUnmounted } from 'vue' +import { apiJson } from './api' + +const POLL_INTERVAL = 60 * 1000 +const IDLE_TIMEOUT = 5 * 60 * 1000 + +export function useSessionValidation(userUuid, onSessionLost) { + let pollTimer = null + let idleTimer = null + let active = false + + function resetIdleTimer() { + if (idleTimer) clearTimeout(idleTimer) + if (!active) startPolling() + idleTimer = setTimeout(stopPolling, IDLE_TIMEOUT) + } + + async function validate() { + try { + const data = await apiJson('/auth/api/validate', { method: 'POST' }) + const newUuid = data.ctx?.user?.uuid + if (newUuid !== userUuid.value) { + window.location.reload() + } + } catch (error) { + if (error.name !== 'NetworkError') { + stopPolling() + onSessionLost(error) + } + } + } + + function startPolling() { + if (active) return + active = true + pollTimer = setInterval(validate, POLL_INTERVAL) + } + + function stopPolling() { + active = false + if (pollTimer) { + clearInterval(pollTimer) + pollTimer = null + } + } + + onMounted(() => { + window.addEventListener('pointermove', resetIdleTimer) + window.addEventListener('pointerdown', resetIdleTimer) + resetIdleTimer() + }) + + onUnmounted(() => { + window.removeEventListener('pointermove', resetIdleTimer) + window.removeEventListener('pointerdown', resetIdleTimer) + if (idleTimer) clearTimeout(idleTimer) + stopPolling() + }) +}