diff --git a/components/DuplicateNotificationBanner.vue b/components/DuplicateNotificationBanner.vue new file mode 100644 index 0000000..59b15e4 --- /dev/null +++ b/components/DuplicateNotificationBanner.vue @@ -0,0 +1,91 @@ + + + diff --git a/composables/useAuthorization.ts b/composables/useAuthorization.ts index ccbc770..c7ffcd7 100644 --- a/composables/useAuthorization.ts +++ b/composables/useAuthorization.ts @@ -17,6 +17,9 @@ export interface AuthState { * Authorization composable for role-based access control */ export const useAuthorization = () => { + // Get the cached auth state from middleware instead of making API calls + const nuxtApp = useNuxtApp(); + // Simple reactive state that starts with safe defaults const authState = reactive({ user: null, @@ -28,23 +31,23 @@ export const useAuthorization = () => { const isLoading = ref(true); const hasInitialized = ref(false); - // Initialize auth state with comprehensive error handling - const initializeAuth = async () => { + // Initialize auth state from middleware cache (no API calls!) + const initializeAuth = () => { if (hasInitialized.value) return; try { - console.log('[useAuthorization] Initializing auth state...'); + console.log('[useAuthorization] Initializing from middleware cache...'); - // Try to get auth from session API directly - const sessionData = await $fetch('/api/auth/session') as AuthState; + // Try to get auth from middleware's cached state first + const cachedAuthState = nuxtApp.payload?.data?.authState; - if (sessionData && typeof sessionData === 'object') { - // Update reactive state - authState.user = sessionData.user || null; - authState.authenticated = sessionData.authenticated || false; - authState.groups = Array.isArray(sessionData.groups) ? sessionData.groups : []; + if (cachedAuthState && typeof cachedAuthState === 'object') { + // Update reactive state from cache + authState.user = cachedAuthState.user || null; + authState.authenticated = cachedAuthState.authenticated || false; + authState.groups = Array.isArray(cachedAuthState.groups) ? cachedAuthState.groups : []; - console.log('[useAuthorization] Auth state initialized:', { + console.log('[useAuthorization] Auth state loaded from cache:', { authenticated: authState.authenticated, groups: authState.groups, user: authState.user?.email @@ -54,11 +57,31 @@ export const useAuthorization = () => { isLoading.value = false; return true; } + + // Fallback: try session cache + const sessionCache = nuxtApp.payload?.data?.['auth:session:cache']; + if (sessionCache && typeof sessionCache === 'object') { + authState.user = sessionCache.user || null; + authState.authenticated = sessionCache.authenticated || false; + authState.groups = Array.isArray(sessionCache.groups) ? sessionCache.groups : []; + + console.log('[useAuthorization] Auth state loaded from session cache:', { + authenticated: authState.authenticated, + groups: authState.groups, + user: authState.user?.email + }); + + hasInitialized.value = true; + isLoading.value = false; + return true; + } + } catch (error) { - console.error('[useAuthorization] Failed to initialize auth:', error); + console.error('[useAuthorization] Failed to load from cache:', error); } - // Set safe defaults on error + // Set safe defaults if no cache available + console.log('[useAuthorization] No cache available, using defaults'); authState.user = null; authState.authenticated = false; authState.groups = []; @@ -67,9 +90,34 @@ export const useAuthorization = () => { return false; }; - // Initialize immediately on client + // Initialize immediately (both client and server) + initializeAuth(); + + // Watch for changes in payload data and reinitialize if (process.client) { - initializeAuth(); + watch( + () => nuxtApp.payload?.data?.authState, + (newAuthState) => { + if (newAuthState && typeof newAuthState === 'object') { + console.log('[useAuthorization] Auth state updated, reinitializing...'); + hasInitialized.value = false; // Force re-initialization + initializeAuth(); + } + }, + { immediate: true, deep: true } + ); + + // Also watch for session cache updates + watch( + () => nuxtApp.payload?.data?.['auth:session:cache'], + (newSessionCache) => { + if (newSessionCache && typeof newSessionCache === 'object' && !hasInitialized.value) { + console.log('[useAuthorization] Session cache updated, initializing...'); + initializeAuth(); + } + }, + { immediate: true, deep: true } + ); } /** diff --git a/pages/dashboard.vue b/pages/dashboard.vue index d0c183d..c83c86a 100644 --- a/pages/dashboard.vue +++ b/pages/dashboard.vue @@ -8,7 +8,7 @@ toValue(tags).interest ? interestMenu : defaultMenu ); +// Safe menu wrapper to prevent crashes when menu is undefined +const safeMenu = computed(() => { + try { + const currentMenu = menu.value; + if (Array.isArray(currentMenu)) { + return currentMenu; + } + + console.warn('[Dashboard] Menu is not an array, returning fallback menu'); + + // Fallback menu with essential items (including admin for safety) + return [ + { + to: "/dashboard/interest-list", + icon: "mdi-view-list", + title: "Interest List", + }, + { + to: "/dashboard/expenses", + icon: "mdi-receipt", + title: "Expenses", + }, + { + to: "/dashboard/file-browser", + icon: "mdi-folder", + title: "File Browser", + }, + { + to: "/dashboard/admin", + icon: "mdi-shield-crown", + title: "Admin Console", + }, + ]; + } catch (error) { + console.error('[Dashboard] Error computing menu:', error); + + // Emergency fallback menu + return [ + { + to: "/dashboard/interest-list", + icon: "mdi-view-list", + title: "Interest List", + }, + ]; + } +}); + const logOut = async () => { await logout(); return navigateTo("/login"); diff --git a/pages/dashboard/interest-list.vue b/pages/dashboard/interest-list.vue index e6e0bca..e2fc7d7 100644 --- a/pages/dashboard/interest-list.vue +++ b/pages/dashboard/interest-list.vue @@ -1,6 +1,9 @@