feat: Implement dashboard layout with navigation and role-based access, enhance authentication middleware to clear cache only on actual auth errors, and update expenses page metadata for authorization checks

This commit is contained in:
Matt 2025-07-11 14:57:15 -04:00
parent c6f81a6686
commit 7ee2cb3368
5 changed files with 296 additions and 6 deletions

View File

@ -1,5 +1,7 @@
<template>
<NuxtPwaManifest />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
<GlobalToast />
</template>

View File

@ -0,0 +1,100 @@
# 404 and Session Expiration Fixes
## Issues Addressed
1. **404 Error on Expenses Page** - The expenses page was returning a 404 error
2. **Session Expiration After 404** - Users were getting logged out after encountering the 404 error
## Root Cause Analysis
### 404 Error Cause
- The expenses page was missing the authorization middleware configuration
- The dashboard layout referenced in the page metadata didn't exist
- Nuxt wasn't properly configured to use layouts
### Session Expiration Cause
- The authentication middleware was incorrectly clearing the session cache on ALL errors (including 404s)
- This caused a valid session to be invalidated when encountering any page error
## Fixes Implemented
### 1. Fixed Expenses Page Metadata
**File**: `pages/dashboard/expenses.vue`
Added proper middleware configuration:
```javascript
definePageMeta({
middleware: ['authentication', 'authorization'],
layout: 'dashboard',
roles: ['sales', 'admin']
});
```
This ensures:
- Authentication is checked first
- Authorization checks for sales/admin roles
- Proper layout is applied
### 2. Fixed Authentication Middleware
**File**: `middleware/authentication.ts`
Updated error handling to only clear cache on actual auth errors:
```javascript
onResponseError({ response }) {
// Clear cache only on actual auth errors, not 404s or other errors
if (response.status === 401) {
console.log('[MIDDLEWARE] Unauthorized error detected, clearing cache')
sessionManager.clearCache();
delete nuxtApp.payload.data?.authState;
} else if (response.status === 403) {
console.log('[MIDDLEWARE] Forbidden error detected, partial cache clear')
// Don't clear cache on 403 as user is authenticated but lacks permissions
}
// Ignore 404s and other errors - they're not authentication issues
}
```
### 3. Enabled Layout Support
**File**: `app.vue`
Updated to support layouts:
```vue
<template>
<NuxtPwaManifest />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
<GlobalToast />
</template>
```
### 4. Created Dashboard Layout
**File**: `layouts/dashboard.vue`
Created a full dashboard layout with:
- Navigation drawer with role-based menu items
- App bar showing user info and role badges
- Proper logout functionality
- Responsive design with rail mode
## Expected Results
1. **Expenses page should now load properly** for users with sales or admin roles
2. **404 errors won't cause session expiration** - only actual authentication failures (401) will clear the session
3. **Better error handling** - 403 errors (insufficient permissions) will redirect to dashboard with a message instead of logging out
4. **Consistent layout** across all dashboard pages
## Testing Steps
1. Log in with a user that has sales or admin role
2. Navigate to `/dashboard/expenses`
3. Verify the page loads without 404
4. If you don't have the required role, you should be redirected to dashboard with an error message (not logged out)
5. Try navigating to a non-existent page - you should get a 404 but remain logged in
## Additional Improvements
- The authorization middleware now stores error messages that are displayed via toast
- The dashboard layout shows the current user and their role
- Navigation menu dynamically shows/hides items based on user roles
- Session validation continues to work with the 3-minute cache + jitter to prevent race conditions

183
layouts/dashboard.vue Normal file
View File

@ -0,0 +1,183 @@
<template>
<v-app>
<!-- Navigation Drawer -->
<v-navigation-drawer
v-model="drawer"
:rail="rail"
permanent
color="grey-darken-4"
>
<v-list>
<v-list-item
prepend-avatar="/Port Nimara New Logo-Circular Frame.png"
:title="rail ? '' : 'Port Nimara'"
:subtitle="rail ? '' : 'Client Portal'"
nav
>
<template v-slot:append>
<v-btn
v-if="!rail"
icon="mdi-chevron-left"
variant="text"
@click.stop="rail = !rail"
size="small"
/>
</template>
</v-list-item>
</v-list>
<v-divider />
<v-list density="compact" nav>
<v-list-item
v-for="item in navigationItems"
:key="item.title"
:to="item.to"
:prepend-icon="item.icon"
:title="item.title"
:value="item.title"
color="primary"
/>
</v-list>
<template v-slot:append>
<v-list density="compact" nav>
<v-list-item
@click="handleLogout"
prepend-icon="mdi-logout"
title="Logout"
value="logout"
color="primary"
/>
</v-list>
</template>
</v-navigation-drawer>
<!-- App Bar -->
<v-app-bar
color="primary"
density="compact"
elevation="0"
>
<v-app-bar-nav-icon
@click="rail = !rail"
v-if="rail"
/>
<v-toolbar-title>{{ pageTitle }}</v-toolbar-title>
<v-spacer />
<v-chip
v-if="authState?.user"
class="mr-2"
color="white"
variant="tonal"
>
<v-icon start>mdi-account</v-icon>
{{ authState.user.name || authState.user.email }}
</v-chip>
<v-chip
v-if="authState?.groups?.includes('admin')"
color="orange"
variant="tonal"
class="mr-2"
>
Admin
</v-chip>
<v-chip
v-else-if="authState?.groups?.includes('sales')"
color="green"
variant="tonal"
class="mr-2"
>
Sales
</v-chip>
</v-app-bar>
<!-- Main Content -->
<v-main>
<slot />
</v-main>
</v-app>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
const route = useRoute();
const router = useRouter();
const nuxtApp = useNuxtApp();
// Navigation state
const drawer = ref(true);
const rail = ref(false);
// Get auth state
const authState = computed(() => nuxtApp.payload.data?.authState);
// Page title based on current route
const pageTitle = computed(() => {
const routeName = route.name as string;
if (routeName === 'dashboard') return 'Dashboard';
if (routeName === 'dashboard-expenses') return 'Expense Tracking';
if (routeName === 'dashboard-interest-list') return 'Interest List';
if (routeName === 'dashboard-berth-list') return 'Berth List';
if (routeName === 'dashboard-admin-audit-logs') return 'Audit Logs';
if (routeName === 'dashboard-admin-system-logs') return 'System Logs';
if (routeName === 'dashboard-admin-duplicates') return 'Duplicate Management';
// Default: capitalize route name
return routeName?.split('-').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ') || 'Dashboard';
});
// Navigation items based on user role
const navigationItems = computed(() => {
const items = [
{ title: 'Dashboard', icon: 'mdi-view-dashboard', to: '/dashboard' },
{ title: 'Interest List', icon: 'mdi-account-group', to: '/dashboard/interest-list' },
{ title: 'Berth List', icon: 'mdi-sailboat', to: '/dashboard/berth-list' },
];
// Add sales/admin specific items
if (authState.value?.groups?.includes('sales') || authState.value?.groups?.includes('admin')) {
items.push(
{ title: 'Expenses', icon: 'mdi-receipt', to: '/dashboard/expenses' },
{ title: 'Interest Status', icon: 'mdi-chart-timeline', to: '/dashboard/interest-status' },
{ title: 'Interest Emails', icon: 'mdi-email-multiple', to: '/dashboard/interest-emails' },
);
}
// Add admin-only items
if (authState.value?.groups?.includes('admin')) {
items.push(
{ title: 'Admin', icon: 'mdi-shield-crown', to: '/dashboard/admin' },
{ title: 'File Browser', icon: 'mdi-folder-open', to: '/dashboard/file-browser' },
);
}
return items;
});
// Logout handler
const handleLogout = async () => {
try {
await $fetch('/api/auth/logout', { method: 'POST' });
await router.push('/login');
} catch (error) {
console.error('Logout error:', error);
// Even if logout fails, redirect to login
await router.push('/login');
}
};
</script>
<style scoped>
.v-navigation-drawer {
top: 0 !important;
}
</style>

View File

@ -45,12 +45,16 @@ export default defineNuxtRouteMiddleware(async (to) => {
console.log(`[MIDDLEWARE] Retrying auth check (attempt ${retries + 1})`)
},
onResponseError({ response }) {
// Clear cache on auth errors
if (response.status === 401 || response.status === 403) {
console.log('[MIDDLEWARE] Auth error detected, clearing cache')
// Clear cache only on actual auth errors, not 404s or other errors
if (response.status === 401) {
console.log('[MIDDLEWARE] Unauthorized error detected, clearing cache')
sessionManager.clearCache();
delete nuxtApp.payload.data?.authState;
} else if (response.status === 403) {
console.log('[MIDDLEWARE] Forbidden error detected, partial cache clear')
// Don't clear cache on 403 as user is authenticated but lacks permissions
}
// Ignore 404s and other errors - they're not authentication issues
}
}) as any;

View File

@ -346,8 +346,9 @@ const ExpenseCreateModal = defineAsyncComponent(() => import('@/components/Expen
// Page meta
definePageMeta({
middleware: ['authentication'],
layout: 'dashboard'
middleware: ['authentication', 'authorization'],
layout: 'dashboard',
roles: ['sales', 'admin']
});
useHead({