Implement admin functionality for merging duplicate records with error handling and logging

This commit is contained in:
Matt 2025-07-09 12:21:41 -04:00
parent b3e7d04b86
commit 4a60782f89
3 changed files with 285 additions and 112 deletions

View File

@ -27,19 +27,28 @@ export const useAuthorization = () => {
groups: [] groups: []
}); });
// Create a loading state
const isLoading = ref(true);
// Function to sync auth state from nuxtApp payload // Function to sync auth state from nuxtApp payload
const syncAuthState = () => { const syncAuthState = () => {
const payloadAuthState = nuxtApp.payload.data?.authState as AuthState; try {
if (payloadAuthState) { // Safely check if payload data exists
authState.value = payloadAuthState; if (nuxtApp.payload && nuxtApp.payload.data && nuxtApp.payload.data.authState) {
console.log('[useAuthorization] Auth state synced:', { const payloadAuthState = nuxtApp.payload.data.authState as AuthState;
authenticated: payloadAuthState.authenticated, authState.value = payloadAuthState;
groups: payloadAuthState.groups, isLoading.value = false;
user: payloadAuthState.user?.email console.log('[useAuthorization] Auth state synced from payload:', {
}); authenticated: payloadAuthState.authenticated,
} else { groups: payloadAuthState.groups,
console.log('[useAuthorization] No auth state found in payload'); 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 // Try to get auth state from API if not in payload
@ -47,6 +56,7 @@ export const useAuthorization = () => {
try { try {
const sessionData = await $fetch('/api/auth/session') as AuthState; const sessionData = await $fetch('/api/auth/session') as AuthState;
authState.value = sessionData; authState.value = sessionData;
isLoading.value = false;
console.log('[useAuthorization] Auth state loaded from API:', { console.log('[useAuthorization] Auth state loaded from API:', {
authenticated: sessionData.authenticated, authenticated: sessionData.authenticated,
groups: sessionData.groups, groups: sessionData.groups,
@ -57,31 +67,46 @@ export const useAuthorization = () => {
updateAuthState(sessionData); updateAuthState(sessionData);
} catch (error) { } catch (error) {
console.error('[useAuthorization] Failed to load auth state:', error); console.error('[useAuthorization] Failed to load auth state:', error);
isLoading.value = false;
} }
}; };
// Initialize auth state // Initialize auth state immediately (not just onMounted)
onMounted(() => { if (process.client) {
syncAuthState(); // Try to sync from payload first
const synced = syncAuthState();
// If no auth state in payload, try to load from API // If not synced from payload, load from API
if (!authState.value.authenticated) { if (!synced) {
loadAuthState(); loadAuthState();
} }
}); } else {
// On server, try to get from payload
syncAuthState();
}
/** /**
* Get current user groups from session * Get current user groups from session
*/ */
const getUserGroups = (): string[] => { 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 * Get current authenticated user
*/ */
const getCurrentUser = (): UserWithGroups | null => { 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;
}
}; };
/** /**

View File

@ -72,117 +72,181 @@ definePageMeta({
const { mdAndDown } = useDisplay(); const { mdAndDown } = useDisplay();
const { user, logout, authSource } = useUnifiedAuth(); const { user, logout, authSource } = useUnifiedAuth();
const { isAdmin, getUserGroups, getCurrentUser } = useAuthorization(); const authUtils = useAuthorization();
const tags = usePortalTags(); const tags = usePortalTags();
const drawer = ref(false); 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 // Debug auth state
onMounted(() => { onMounted(() => {
console.log('[Dashboard] Auth state on mount:', { nextTick(() => {
isAdmin: isAdmin(), console.log('[Dashboard] Auth state on mount:', {
userGroups: getUserGroups(), isAdmin: safeIsAdmin(),
currentUser: getCurrentUser() userGroups: safeGetUserGroups(),
currentUser: safeGetCurrentUser()
});
}); });
}); });
const interestMenu = computed(() => { const interestMenu = computed(() => {
const userIsAdmin = isAdmin(); try {
const userGroups = getUserGroups(); const userIsAdmin = safeIsAdmin();
const userGroups = safeGetUserGroups();
console.log('[Dashboard] Computing interest menu - isAdmin:', userIsAdmin, 'groups:', userGroups);
console.log('[Dashboard] Computing interest menu - isAdmin:', userIsAdmin, 'groups:', userGroups);
const baseMenu = [
//{ const baseMenu = [
// to: "/dashboard/interest-eoi-queue", //{
// icon: "mdi-tray-full", // to: "/dashboard/interest-eoi-queue",
// title: "EOI Queue", // icon: "mdi-tray-full",
//}, // title: "EOI Queue",
{ //},
to: "/dashboard/interest-analytics", {
icon: "mdi-view-dashboard", to: "/dashboard/interest-analytics",
title: "Analytics", icon: "mdi-view-dashboard",
}, title: "Analytics",
{ },
to: "/dashboard/interest-berth-list", {
icon: "mdi-table", to: "/dashboard/interest-berth-list",
title: "Berth List", icon: "mdi-table",
}, title: "Berth List",
{ },
to: "/dashboard/interest-berth-status", {
icon: "mdi-sail-boat", to: "/dashboard/interest-berth-status",
title: "Berth Status", icon: "mdi-sail-boat",
}, title: "Berth Status",
{ },
to: "/dashboard/interest-list", {
icon: "mdi-view-list", to: "/dashboard/interest-list",
title: "Interest List", icon: "mdi-view-list",
}, title: "Interest List",
{ },
to: "/dashboard/interest-status", {
icon: "mdi-account-check", to: "/dashboard/interest-status",
title: "Interest Status", icon: "mdi-account-check",
}, title: "Interest Status",
{ },
to: "/dashboard/expenses", {
icon: "mdi-receipt", to: "/dashboard/expenses",
title: "Expenses", icon: "mdi-receipt",
}, title: "Expenses",
{ },
to: "/dashboard/file-browser", {
icon: "mdi-folder", to: "/dashboard/file-browser",
title: "File Browser", icon: "mdi-folder",
}, title: "File Browser",
]; },
];
// Add admin menu items if user is admin // Add admin menu items if user is admin
if (userIsAdmin) { if (userIsAdmin) {
console.log('[Dashboard] Adding admin console to interest menu'); console.log('[Dashboard] Adding admin console to interest menu');
baseMenu.push({ baseMenu.push({
to: "/dashboard/admin", to: "/dashboard/admin",
icon: "mdi-shield-crown", icon: "mdi-shield-crown",
title: "Admin Console", 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 defaultMenu = computed(() => {
const userIsAdmin = isAdmin(); try {
const userGroups = getUserGroups(); const userIsAdmin = safeIsAdmin();
const userGroups = safeGetUserGroups();
console.log('[Dashboard] Computing default menu - isAdmin:', userIsAdmin, 'groups:', userGroups);
console.log('[Dashboard] Computing default menu - isAdmin:', userIsAdmin, 'groups:', userGroups);
const baseMenu = [
{ const baseMenu = [
to: "/dashboard/site", {
icon: "mdi-view-dashboard", to: "/dashboard/site",
title: "Site Analytics", icon: "mdi-view-dashboard",
}, title: "Site Analytics",
{ },
to: "/dashboard/data", {
icon: "mdi-finance", to: "/dashboard/data",
title: "Data Analytics", icon: "mdi-finance",
}, title: "Data Analytics",
{ },
to: "/dashboard/file-browser", {
icon: "mdi-folder", to: "/dashboard/file-browser",
title: "File Browser", icon: "mdi-folder",
}, title: "File Browser",
]; },
];
// Add admin menu items if user is admin // Add admin menu items if user is admin
if (userIsAdmin) { if (userIsAdmin) {
console.log('[Dashboard] Adding admin console to default menu'); console.log('[Dashboard] Adding admin console to default menu');
baseMenu.push({ baseMenu.push({
to: "/dashboard/admin", to: "/dashboard/admin",
icon: "mdi-shield-crown", icon: "mdi-shield-crown",
title: "Admin Console", 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(() => const menu = computed(() =>

View File

@ -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'
};
}
});