From 4a60782f89b45dba0b9f3269ffbc828d125b9ed7 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 9 Jul 2025 12:21:41 -0400 Subject: [PATCH] Implement admin functionality for merging duplicate records with error handling and logging --- composables/useAuthorization.ts | 61 +++++-- pages/dashboard.vue | 252 +++++++++++++++++---------- server/api/admin/duplicates/merge.ts | 84 +++++++++ 3 files changed, 285 insertions(+), 112 deletions(-) create mode 100644 server/api/admin/duplicates/merge.ts diff --git a/composables/useAuthorization.ts b/composables/useAuthorization.ts index 33897e1..87b267d 100644 --- a/composables/useAuthorization.ts +++ b/composables/useAuthorization.ts @@ -27,19 +27,28 @@ export const useAuthorization = () => { groups: [] }); + // Create a loading state + const isLoading = ref(true); + // Function to sync auth state from nuxtApp payload const syncAuthState = () => { - const payloadAuthState = nuxtApp.payload.data?.authState as AuthState; - if (payloadAuthState) { - authState.value = payloadAuthState; - console.log('[useAuthorization] Auth state synced:', { - authenticated: payloadAuthState.authenticated, - groups: payloadAuthState.groups, - user: payloadAuthState.user?.email - }); - } else { - console.log('[useAuthorization] No auth state found in payload'); + try { + // Safely check if payload data exists + if (nuxtApp.payload && nuxtApp.payload.data && nuxtApp.payload.data.authState) { + const payloadAuthState = nuxtApp.payload.data.authState as AuthState; + authState.value = payloadAuthState; + isLoading.value = false; + console.log('[useAuthorization] Auth state synced from payload:', { + authenticated: payloadAuthState.authenticated, + groups: payloadAuthState.groups, + user: payloadAuthState.user?.email + }); + return true; + } + } catch (error) { + console.error('[useAuthorization] Error syncing auth state:', error); } + return false; }; // Try to get auth state from API if not in payload @@ -47,6 +56,7 @@ export const useAuthorization = () => { try { const sessionData = await $fetch('/api/auth/session') as AuthState; authState.value = sessionData; + isLoading.value = false; console.log('[useAuthorization] Auth state loaded from API:', { authenticated: sessionData.authenticated, groups: sessionData.groups, @@ -57,31 +67,46 @@ export const useAuthorization = () => { updateAuthState(sessionData); } catch (error) { console.error('[useAuthorization] Failed to load auth state:', error); + isLoading.value = false; } }; - // Initialize auth state - onMounted(() => { - syncAuthState(); + // Initialize auth state immediately (not just onMounted) + if (process.client) { + // Try to sync from payload first + const synced = syncAuthState(); - // If no auth state in payload, try to load from API - if (!authState.value.authenticated) { + // If not synced from payload, load from API + if (!synced) { loadAuthState(); } - }); + } else { + // On server, try to get from payload + syncAuthState(); + } /** * Get current user groups from session */ const getUserGroups = (): string[] => { - return authState.value.groups || []; + try { + return authState.value?.groups || []; + } catch (error) { + console.error('[useAuthorization] Error getting user groups:', error); + return []; + } }; /** * Get current authenticated user */ const getCurrentUser = (): UserWithGroups | null => { - return authState.value.user || null; + try { + return authState.value?.user || null; + } catch (error) { + console.error('[useAuthorization] Error getting current user:', error); + return null; + } }; /** diff --git a/pages/dashboard.vue b/pages/dashboard.vue index 7d8624e..5f2e733 100644 --- a/pages/dashboard.vue +++ b/pages/dashboard.vue @@ -72,117 +72,181 @@ definePageMeta({ const { mdAndDown } = useDisplay(); const { user, logout, authSource } = useUnifiedAuth(); -const { isAdmin, getUserGroups, getCurrentUser } = useAuthorization(); +const authUtils = useAuthorization(); const tags = usePortalTags(); const drawer = ref(false); +// Safe wrapper for auth functions +const safeIsAdmin = () => { + try { + return authUtils.isAdmin(); + } catch (error) { + console.error('[Dashboard] Error checking admin status:', error); + return false; + } +}; + +const safeGetUserGroups = () => { + try { + return authUtils.getUserGroups(); + } catch (error) { + console.error('[Dashboard] Error getting user groups:', error); + return []; + } +}; + +const safeGetCurrentUser = () => { + try { + return authUtils.getCurrentUser(); + } catch (error) { + console.error('[Dashboard] Error getting current user:', error); + return null; + } +}; + // Debug auth state onMounted(() => { - console.log('[Dashboard] Auth state on mount:', { - isAdmin: isAdmin(), - userGroups: getUserGroups(), - currentUser: getCurrentUser() + nextTick(() => { + console.log('[Dashboard] Auth state on mount:', { + isAdmin: safeIsAdmin(), + userGroups: safeGetUserGroups(), + currentUser: safeGetCurrentUser() + }); }); }); const interestMenu = computed(() => { - const userIsAdmin = isAdmin(); - const userGroups = getUserGroups(); - - console.log('[Dashboard] Computing interest menu - isAdmin:', userIsAdmin, 'groups:', userGroups); - - const baseMenu = [ - //{ - // to: "/dashboard/interest-eoi-queue", - // icon: "mdi-tray-full", - // title: "EOI Queue", - //}, - { - to: "/dashboard/interest-analytics", - icon: "mdi-view-dashboard", - title: "Analytics", - }, - { - to: "/dashboard/interest-berth-list", - icon: "mdi-table", - title: "Berth List", - }, - { - to: "/dashboard/interest-berth-status", - icon: "mdi-sail-boat", - title: "Berth Status", - }, - { - to: "/dashboard/interest-list", - icon: "mdi-view-list", - title: "Interest List", - }, - { - to: "/dashboard/interest-status", - icon: "mdi-account-check", - title: "Interest Status", - }, - { - to: "/dashboard/expenses", - icon: "mdi-receipt", - title: "Expenses", - }, - { - to: "/dashboard/file-browser", - icon: "mdi-folder", - title: "File Browser", - }, - ]; + try { + const userIsAdmin = safeIsAdmin(); + const userGroups = safeGetUserGroups(); + + console.log('[Dashboard] Computing interest menu - isAdmin:', userIsAdmin, 'groups:', userGroups); + + const baseMenu = [ + //{ + // to: "/dashboard/interest-eoi-queue", + // icon: "mdi-tray-full", + // title: "EOI Queue", + //}, + { + to: "/dashboard/interest-analytics", + icon: "mdi-view-dashboard", + title: "Analytics", + }, + { + to: "/dashboard/interest-berth-list", + icon: "mdi-table", + title: "Berth List", + }, + { + to: "/dashboard/interest-berth-status", + icon: "mdi-sail-boat", + title: "Berth Status", + }, + { + to: "/dashboard/interest-list", + icon: "mdi-view-list", + title: "Interest List", + }, + { + to: "/dashboard/interest-status", + icon: "mdi-account-check", + title: "Interest Status", + }, + { + to: "/dashboard/expenses", + icon: "mdi-receipt", + title: "Expenses", + }, + { + to: "/dashboard/file-browser", + icon: "mdi-folder", + title: "File Browser", + }, + ]; - // Add admin menu items if user is admin - if (userIsAdmin) { - console.log('[Dashboard] Adding admin console to interest menu'); - baseMenu.push({ - to: "/dashboard/admin", - icon: "mdi-shield-crown", - title: "Admin Console", - }); + // Add admin menu items if user is admin + if (userIsAdmin) { + console.log('[Dashboard] Adding admin console to interest menu'); + baseMenu.push({ + to: "/dashboard/admin", + icon: "mdi-shield-crown", + title: "Admin Console", + }); + } + + return baseMenu; + } catch (error) { + console.error('[Dashboard] Error computing interest menu:', error); + // Return basic menu without admin items on error + return [ + { + to: "/dashboard/interest-analytics", + icon: "mdi-view-dashboard", + title: "Analytics", + }, + { + to: "/dashboard/interest-list", + icon: "mdi-view-list", + title: "Interest List", + }, + ]; } - - return baseMenu; }); const defaultMenu = computed(() => { - const userIsAdmin = isAdmin(); - const userGroups = getUserGroups(); - - console.log('[Dashboard] Computing default menu - isAdmin:', userIsAdmin, 'groups:', userGroups); - - const baseMenu = [ - { - to: "/dashboard/site", - icon: "mdi-view-dashboard", - title: "Site Analytics", - }, - { - to: "/dashboard/data", - icon: "mdi-finance", - title: "Data Analytics", - }, - { - to: "/dashboard/file-browser", - icon: "mdi-folder", - title: "File Browser", - }, - ]; + try { + const userIsAdmin = safeIsAdmin(); + const userGroups = safeGetUserGroups(); + + console.log('[Dashboard] Computing default menu - isAdmin:', userIsAdmin, 'groups:', userGroups); + + const baseMenu = [ + { + to: "/dashboard/site", + icon: "mdi-view-dashboard", + title: "Site Analytics", + }, + { + to: "/dashboard/data", + icon: "mdi-finance", + title: "Data Analytics", + }, + { + to: "/dashboard/file-browser", + icon: "mdi-folder", + title: "File Browser", + }, + ]; - // Add admin menu items if user is admin - if (userIsAdmin) { - console.log('[Dashboard] Adding admin console to default menu'); - baseMenu.push({ - to: "/dashboard/admin", - icon: "mdi-shield-crown", - title: "Admin Console", - }); + // Add admin menu items if user is admin + if (userIsAdmin) { + console.log('[Dashboard] Adding admin console to default menu'); + baseMenu.push({ + to: "/dashboard/admin", + icon: "mdi-shield-crown", + title: "Admin Console", + }); + } + + return baseMenu; + } catch (error) { + console.error('[Dashboard] Error computing default menu:', error); + // Return basic menu without admin items on error + return [ + { + to: "/dashboard/site", + icon: "mdi-view-dashboard", + title: "Site Analytics", + }, + { + to: "/dashboard/data", + icon: "mdi-finance", + title: "Data Analytics", + }, + ]; } - - return baseMenu; }); const menu = computed(() => diff --git a/server/api/admin/duplicates/merge.ts b/server/api/admin/duplicates/merge.ts new file mode 100644 index 0000000..6e0d884 --- /dev/null +++ b/server/api/admin/duplicates/merge.ts @@ -0,0 +1,84 @@ +import { requireAdmin } from '~/server/utils/auth'; +import { getNocoDbConfiguration, updateInterest, deleteInterest } from '~/server/utils/nocodb'; + +export default defineEventHandler(async (event) => { + console.log('[ADMIN] Merge duplicates request'); + + try { + // Require admin authentication + await requireAdmin(event); + + const body = await readBody(event); + const { masterId, duplicateIds, mergeData } = body; + + if (!masterId || !duplicateIds || !Array.isArray(duplicateIds)) { + throw createError({ + statusCode: 400, + statusMessage: 'Invalid merge request: masterId and duplicateIds array required' + }); + } + + console.log('[ADMIN] Merging duplicates:', { + masterId, + duplicateIds, + fieldsToMerge: Object.keys(mergeData || {}) + }); + + // Update the master record with merged data + if (mergeData && Object.keys(mergeData).length > 0) { + console.log('[ADMIN] Updating master record with merged data'); + await updateInterest(masterId.toString(), mergeData); + } + + // Delete the duplicate records + const deletionResults = []; + for (const duplicateId of duplicateIds) { + try { + console.log('[ADMIN] Deleting duplicate record:', duplicateId); + await deleteInterest(duplicateId.toString()); + deletionResults.push({ id: duplicateId, success: true }); + } catch (error: any) { + console.error('[ADMIN] Failed to delete duplicate:', duplicateId, error); + deletionResults.push({ + id: duplicateId, + success: false, + error: error.message + }); + } + } + + console.log('[ADMIN] Merge operation completed'); + + return { + success: true, + data: { + masterId, + mergedCount: duplicateIds.length, + deletionResults, + updatedFields: Object.keys(mergeData || {}) + } + }; + + } catch (error: any) { + console.error('[ADMIN] Failed to merge duplicates:', error); + + if (error.statusCode === 403) { + return { + success: false, + error: 'Insufficient permissions. Admin access required.' + }; + } + + if (error.statusCode === 400) { + return { + success: false, + error: error.statusMessage + }; + } + + return { + success: false, + error: 'Failed to merge duplicate records' + }; + } +});