Optimize auth initialization by using cached middleware state

- Replace API calls with cached auth state from middleware in useAuthorization
- Add fallback to session cache and watchers for auth state updates
- Change initialization from async to synchronous for better performance
- Add DuplicateNotificationBanner component
This commit is contained in:
Matt 2025-07-09 12:43:24 -04:00
parent 3615e2fa9b
commit 36048dfed1
4 changed files with 205 additions and 16 deletions

View File

@ -0,0 +1,91 @@
<template>
<v-alert
v-if="showBanner && duplicateCount > 0"
type="warning"
variant="tonal"
closable
@click:close="dismissBanner"
class="ma-4"
>
<template #prepend>
<v-icon>mdi-content-duplicate</v-icon>
</template>
<div class="d-flex align-center justify-space-between">
<div>
<div class="text-subtitle-1 font-weight-medium">
{{ duplicateCount }} duplicate interest record{{ duplicateCount > 1 ? 's' : '' }} detected
</div>
<div class="text-body-2">
Duplicate records can affect data integrity and reporting accuracy.
</div>
</div>
<v-btn
color="warning"
variant="elevated"
size="small"
:to="'/dashboard/admin/duplicates'"
prepend-icon="mdi-wrench"
>
Clean Up Duplicates
</v-btn>
</div>
</v-alert>
</template>
<script setup>
const { isAdmin } = useAuthorization();
const showBanner = ref(true);
const duplicateCount = ref(0);
const loading = ref(false);
// Check for duplicates on mount (admin only)
const checkForDuplicates = async () => {
if (!isAdmin() || loading.value) return;
try {
loading.value = true;
const response = await $fetch('/api/admin/duplicates/find', {
method: 'POST',
body: { threshold: 80 }
});
if (response.success && response.data?.duplicateGroups) {
duplicateCount.value = response.data.duplicateGroups.length;
}
} catch (error) {
console.error('[DuplicateNotification] Failed to check for duplicates:', error);
// Silently fail - this is just a notification banner
} finally {
loading.value = false;
}
};
// Dismiss the banner for this session
const dismissBanner = () => {
showBanner.value = false;
// Store dismissal in session storage
if (process.client) {
sessionStorage.setItem('duplicates-banner-dismissed', 'true');
}
};
// Check if banner was already dismissed this session
onMounted(() => {
if (process.client) {
const dismissed = sessionStorage.getItem('duplicates-banner-dismissed');
if (dismissed === 'true') {
showBanner.value = false;
return;
}
}
// Only check for duplicates if user is admin
if (isAdmin()) {
// Small delay to let other components load first
setTimeout(checkForDuplicates, 2000);
}
});
</script>

View File

@ -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<AuthState>({
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;
}
} catch (error) {
console.error('[useAuthorization] Failed to initialize auth:', error);
// 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;
}
// Set safe defaults on error
} catch (error) {
console.error('[useAuthorization] Failed to load from cache:', 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
if (process.client) {
// Initialize immediately (both client and server)
initializeAuth();
// Watch for changes in payload data and reinitialize
if (process.client) {
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 }
);
}
/**

View File

@ -8,7 +8,7 @@
<v-list color="primary" lines="two">
<v-list-item
v-for="(item, index) in menu"
v-for="(item, index) in safeMenu"
:key="index"
:to="item.to"
:title="item.title"
@ -191,6 +191,53 @@ const menu = computed(() =>
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");

View File

@ -1,6 +1,9 @@
<template>
<div>
<v-container fluid>
<!-- Duplicate notification banner for admins -->
<DuplicateNotificationBanner />
<!-- Header Section -->
<v-row class="mb-4 mb-md-6">
<v-col cols="12" md="8">