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:
+4
-25
@@ -14,6 +14,7 @@
|
|||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { apiJson, getAuthIframeUrl } from '@/utils/api'
|
import { apiJson, getAuthIframeUrl } from '@/utils/api'
|
||||||
|
import { useSessionValidation } from '@/utils/session'
|
||||||
import StatusMessage from '@/components/StatusMessage.vue'
|
import StatusMessage from '@/components/StatusMessage.vue'
|
||||||
import ProfileView from '@/components/ProfileView.vue'
|
import ProfileView from '@/components/ProfileView.vue'
|
||||||
import HostProfileView from '@/components/HostProfileView.vue'
|
import HostProfileView from '@/components/HostProfileView.vue'
|
||||||
@@ -46,7 +47,7 @@ const isHostMode = computed(() => {
|
|||||||
const configuredHost = normalizeHost(authHost)
|
const configuredHost = normalizeHost(authHost)
|
||||||
return currentHost !== configuredHost
|
return currentHost !== configuredHost
|
||||||
})
|
})
|
||||||
let validationTimer = null
|
const userUuid = computed(() => store.userInfo?.ctx.user.uuid)
|
||||||
let authIframe = null
|
let authIframe = null
|
||||||
|
|
||||||
function terminateSession() {
|
function terminateSession() {
|
||||||
@@ -54,11 +55,12 @@ function terminateSession() {
|
|||||||
viewState.value = 'terminal'
|
viewState.value = 'terminal'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useSessionValidation(userUuid, terminateSession)
|
||||||
|
|
||||||
async function loadUserInfo() {
|
async function loadUserInfo() {
|
||||||
try {
|
try {
|
||||||
store.userInfo = await apiJson('/auth/api/user-info', { method: 'POST' })
|
store.userInfo = await apiJson('/auth/api/user-info', { method: 'POST' })
|
||||||
viewState.value = 'profile'
|
viewState.value = 'profile'
|
||||||
startSessionValidation()
|
|
||||||
return true
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
store.userInfo = null
|
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 () => {
|
onMounted(async () => {
|
||||||
// Listen for postMessage from auth iframe
|
// Listen for postMessage from auth iframe
|
||||||
window.addEventListener('message', handleAuthMessage)
|
window.addEventListener('message', handleAuthMessage)
|
||||||
@@ -178,7 +158,6 @@ onMounted(async () => {
|
|||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('message', handleAuthMessage)
|
window.removeEventListener('message', handleAuthMessage)
|
||||||
stopSessionValidation()
|
|
||||||
hideAuthIframe()
|
hideAuthIframe()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import AdminDialogs from '@/admin/AdminDialogs.vue'
|
|||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { adminUiPath, makeUiHref } from '@/utils/settings'
|
import { adminUiPath, makeUiHref } from '@/utils/settings'
|
||||||
import { apiJson } from '@/utils/api'
|
import { apiJson } from '@/utils/api'
|
||||||
|
import { useSessionValidation } from '@/utils/session'
|
||||||
import { getDirection } from '@/utils/keynav'
|
import { getDirection } from '@/utils/keynav'
|
||||||
import { goBack } from '@/utils/helpers'
|
import { goBack } from '@/utils/helpers'
|
||||||
|
|
||||||
@@ -157,6 +158,18 @@ function clearSensitiveState() {
|
|||||||
authenticated.value = false
|
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() {
|
async function load() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
loadingMessage.value = 'Loading...'
|
loadingMessage.value = 'Loading...'
|
||||||
@@ -177,12 +190,7 @@ async function load() {
|
|||||||
}
|
}
|
||||||
} else parseHash()
|
} else parseHash()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
clearSensitiveState()
|
onSessionLost(e)
|
||||||
if (e.name === 'AuthCancelledError') {
|
|
||||||
showBackMessage.value = true
|
|
||||||
} else {
|
|
||||||
error.value = e.message
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import QRCodeDisplay from '@/components/QRCodeDisplay.vue'
|
|||||||
import { apiJson } from '@/utils/api'
|
import { apiJson } from '@/utils/api'
|
||||||
import { formatDate } from '@/utils/helpers'
|
import { formatDate } from '@/utils/helpers'
|
||||||
import { getDirection } from '@/utils/keynav'
|
import { getDirection } from '@/utils/keynav'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
endpoint: { type: String, required: true },
|
endpoint: { type: String, required: true },
|
||||||
@@ -46,6 +47,7 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['close', 'copied'])
|
const emit = defineEmits(['close', 'copied'])
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
const dialog = ref(null)
|
const dialog = ref(null)
|
||||||
const linkUrl = ref(null)
|
const linkUrl = ref(null)
|
||||||
const expiresAt = ref(null)
|
const expiresAt = ref(null)
|
||||||
@@ -73,7 +75,8 @@ async function generateLink() {
|
|||||||
} else {
|
} else {
|
||||||
emit('close')
|
emit('close')
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (e) {
|
||||||
|
authStore.showMessage(e.message || 'Failed to generate link', 'error')
|
||||||
emit('close')
|
emit('close')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user