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:
parent
3615e2fa9b
commit
36048dfed1
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
// 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 }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Reference in New Issue