feat(frontend): validate persisted session on app boot
A stale or revoked token previously rendered the full panel chrome and only collapsed on the first API call. App boot now calls /auth/me through useApi (401 -> refresh -> logout already handled there); user profile refreshes on success, and non-auth failures (network, 5xx) never log the user out. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { onMounted } from 'vue'
|
||||||
import { RouterView } from 'vue-router'
|
import { RouterView } from 'vue-router'
|
||||||
import ToastNotification from '@/components/ToastNotification.vue'
|
import ToastNotification from '@/components/ToastNotification.vue'
|
||||||
import ErrorBoundary from '@/components/ErrorBoundary.vue'
|
import ErrorBoundary from '@/components/ErrorBoundary.vue'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
|
// Validate any persisted session against the API on boot — a stale token
|
||||||
|
// should bounce to login immediately, not after the first failed call.
|
||||||
|
const auth = useAuthStore()
|
||||||
|
onMounted(() => { void auth.validateSession() })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -58,6 +58,27 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
permissions.value = {}
|
permissions.value = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the persisted session against the API on app boot. Without this,
|
||||||
|
* a stale/revoked token renders the full panel chrome and only collapses on
|
||||||
|
* the first real API call. useApi's 401 path (refresh → retry → logout)
|
||||||
|
* does the heavy lifting; any non-auth failure (network, 5xx) keeps the
|
||||||
|
* session — never log users out because the API blipped.
|
||||||
|
* Dynamic import avoids a static auth-store ↔ useApi module cycle.
|
||||||
|
*/
|
||||||
|
async function validateSession(): Promise<void> {
|
||||||
|
if (!accessToken.value) return
|
||||||
|
try {
|
||||||
|
const { useApi } = await import('@/composables/useApi')
|
||||||
|
const me = await useApi().get<Partial<User>>('/auth/me')
|
||||||
|
if (user.value && me && typeof me === 'object') {
|
||||||
|
user.value = { ...user.value, ...me }
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 401 → refresh → logout/redirect already handled inside useApi.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function hasModule(moduleSlug: string): boolean {
|
function hasModule(moduleSlug: string): boolean {
|
||||||
return license.value?.modules_enabled?.includes(moduleSlug) ?? false
|
return license.value?.modules_enabled?.includes(moduleSlug) ?? false
|
||||||
}
|
}
|
||||||
@@ -92,6 +113,7 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
setAuth,
|
setAuth,
|
||||||
setLicense,
|
setLicense,
|
||||||
logout,
|
logout,
|
||||||
|
validateSession,
|
||||||
hasModule,
|
hasModule,
|
||||||
hasPermission,
|
hasPermission,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user