Consistent and stronger session revalidation checks in Auth profile and Admin App. Fix missing handling of link generation network errors.

This commit is contained in:
2026-01-29 16:02:29 +00:00
parent 58f46c6abf
commit c9ea1c8948
4 changed files with 81 additions and 32 deletions
+4 -25
View File
@@ -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()
})
</script>
+14 -6
View File
@@ -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
}
@@ -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')
}
}
+59
View File
@@ -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()
})
}