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:
parent
c6f81a6686
commit
7ee2cb3368
4
app.vue
4
app.vue
|
|
@ -1,5 +1,7 @@
|
|||
<template>
|
||||
<NuxtPwaManifest />
|
||||
<NuxtPage />
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
<GlobalToast />
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
Loading…
Reference in New Issue