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
|
* Authorization composable for role-based access control
|
||||||
*/
|
*/
|
||||||
export const useAuthorization = () => {
|
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
|
// Simple reactive state that starts with safe defaults
|
||||||
const authState = reactive<AuthState>({
|
const authState = reactive<AuthState>({
|
||||||
user: null,
|
user: null,
|
||||||
|
|
@ -28,23 +31,23 @@ export const useAuthorization = () => {
|
||||||
const isLoading = ref(true);
|
const isLoading = ref(true);
|
||||||
const hasInitialized = ref(false);
|
const hasInitialized = ref(false);
|
||||||
|
|
||||||
// Initialize auth state with comprehensive error handling
|
// Initialize auth state from middleware cache (no API calls!)
|
||||||
const initializeAuth = async () => {
|
const initializeAuth = () => {
|
||||||
if (hasInitialized.value) return;
|
if (hasInitialized.value) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('[useAuthorization] Initializing auth state...');
|
console.log('[useAuthorization] Initializing from middleware cache...');
|
||||||
|
|
||||||
// Try to get auth from session API directly
|
// Try to get auth from middleware's cached state first
|
||||||
const sessionData = await $fetch('/api/auth/session') as AuthState;
|
const cachedAuthState = nuxtApp.payload?.data?.authState;
|
||||||
|
|
||||||
if (sessionData && typeof sessionData === 'object') {
|
if (cachedAuthState && typeof cachedAuthState === 'object') {
|
||||||
// Update reactive state
|
// Update reactive state from cache
|
||||||
authState.user = sessionData.user || null;
|
authState.user = cachedAuthState.user || null;
|
||||||
authState.authenticated = sessionData.authenticated || false;
|
authState.authenticated = cachedAuthState.authenticated || false;
|
||||||
authState.groups = Array.isArray(sessionData.groups) ? sessionData.groups : [];
|
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,
|
authenticated: authState.authenticated,
|
||||||
groups: authState.groups,
|
groups: authState.groups,
|
||||||
user: authState.user?.email
|
user: authState.user?.email
|
||||||
|
|
@ -54,11 +57,31 @@ export const useAuthorization = () => {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
return true;
|
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) {
|
} 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.user = null;
|
||||||
authState.authenticated = false;
|
authState.authenticated = false;
|
||||||
authState.groups = [];
|
authState.groups = [];
|
||||||
|
|
@ -67,9 +90,34 @@ export const useAuthorization = () => {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize immediately on client
|
// Initialize immediately (both client and server)
|
||||||
|
initializeAuth();
|
||||||
|
|
||||||
|
// Watch for changes in payload data and reinitialize
|
||||||
if (process.client) {
|
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 color="primary" lines="two">
|
||||||
<v-list-item
|
<v-list-item
|
||||||
v-for="(item, index) in menu"
|
v-for="(item, index) in safeMenu"
|
||||||
:key="index"
|
:key="index"
|
||||||
:to="item.to"
|
:to="item.to"
|
||||||
:title="item.title"
|
:title="item.title"
|
||||||
|
|
@ -191,6 +191,53 @@ const menu = computed(() =>
|
||||||
toValue(tags).interest ? interestMenu : defaultMenu
|
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 () => {
|
const logOut = async () => {
|
||||||
await logout();
|
await logout();
|
||||||
return navigateTo("/login");
|
return navigateTo("/login");
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<v-container fluid>
|
<v-container fluid>
|
||||||
|
<!-- Duplicate notification banner for admins -->
|
||||||
|
<DuplicateNotificationBanner />
|
||||||
|
|
||||||
<!-- Header Section -->
|
<!-- Header Section -->
|
||||||
<v-row class="mb-4 mb-md-6">
|
<v-row class="mb-4 mb-md-6">
|
||||||
<v-col cols="12" md="8">
|
<v-col cols="12" md="8">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue