Refactor authentication system with tier-based access control
Build And Push Image / docker (push) Successful in 2m59s
Details
Build And Push Image / docker (push) Successful in 2m59s
Details
- Replace group-based auth with user/board/admin tier system - Add direct login functionality alongside OAuth - Implement role-based middleware for route protection - Create dashboard pages and admin API endpoints - Add error handling page and improved user management - Maintain backward compatibility with legacy role methods
This commit is contained in:
parent
2c2c0f5c33
commit
cd29123e23
|
|
@ -1,64 +1,156 @@
|
||||||
import type { AuthState } from '~/utils/types';
|
import type { User } from '~/utils/types';
|
||||||
|
|
||||||
export const useAuth = () => {
|
export const useAuth = () => {
|
||||||
const authState = useState<AuthState>('auth.state', () => ({
|
const user = ref<User | null>(null);
|
||||||
authenticated: false,
|
const isAuthenticated = computed(() => !!user.value);
|
||||||
user: null,
|
const loading = ref(false);
|
||||||
groups: [],
|
const error = ref<string | null>(null);
|
||||||
}));
|
|
||||||
|
|
||||||
const login = () => {
|
// Tier-based computed properties
|
||||||
return navigateTo('/api/auth/login');
|
const userTier = computed(() => user.value?.tier || 'user');
|
||||||
|
const isUser = computed(() => user.value?.tier === 'user');
|
||||||
|
const isBoard = computed(() => user.value?.tier === 'board');
|
||||||
|
const isAdmin = computed(() => user.value?.tier === 'admin');
|
||||||
|
const firstName = computed(() => {
|
||||||
|
if (user.value?.firstName) return user.value.firstName;
|
||||||
|
if (user.value?.name) return user.value.name.split(' ')[0];
|
||||||
|
return 'User';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper methods
|
||||||
|
const hasTier = (requiredTier: 'user' | 'board' | 'admin') => {
|
||||||
|
return user.value?.tier === requiredTier;
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = async () => {
|
const hasGroup = (groupName: string) => {
|
||||||
|
return user.value?.groups?.includes(groupName) || false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Legacy compatibility
|
||||||
|
const hasRole = (role: string) => {
|
||||||
|
return hasGroup(role);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Direct login method
|
||||||
|
const login = async (credentials: { username: string; password: string; rememberMe?: boolean }) => {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await $fetch('/api/auth/logout', { method: 'POST' });
|
const response = await $fetch<{
|
||||||
authState.value = {
|
success: boolean;
|
||||||
authenticated: false,
|
redirectTo?: string;
|
||||||
user: null,
|
user?: User;
|
||||||
groups: [],
|
}>('/api/auth/direct-login', {
|
||||||
};
|
method: 'POST',
|
||||||
await navigateTo('/login');
|
body: credentials
|
||||||
} catch (error) {
|
});
|
||||||
console.error('Logout error:', error);
|
|
||||||
await navigateTo('/login');
|
if (response.success && response.user) {
|
||||||
|
user.value = response.user;
|
||||||
|
|
||||||
|
// Redirect to dashboard or intended page
|
||||||
|
await navigateTo(response.redirectTo || '/dashboard');
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, error: 'Login failed' };
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value = err.data?.message || 'Login failed';
|
||||||
|
return { success: false, error: error.value };
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// OAuth login method (fallback)
|
||||||
|
const loginOAuth = () => {
|
||||||
|
return navigateTo('/api/auth/login');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Password reset method
|
||||||
|
const requestPasswordReset = async (email: string) => {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await $fetch<{
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
}>('/api/auth/forgot-password', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { email }
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true, message: response.message };
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value = err.data?.message || 'Password reset failed';
|
||||||
|
return { success: false, error: error.value };
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check authentication status
|
||||||
const checkAuth = async () => {
|
const checkAuth = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await $fetch<AuthState>('/api/auth/session');
|
const response = await $fetch<{
|
||||||
authState.value = response;
|
authenticated: boolean;
|
||||||
return response.authenticated;
|
user: User | null;
|
||||||
} catch (error) {
|
}>('/api/auth/session');
|
||||||
console.error('Auth check error:', error);
|
|
||||||
authState.value = {
|
if (response.authenticated && response.user) {
|
||||||
authenticated: false,
|
user.value = response.user;
|
||||||
user: null,
|
return true;
|
||||||
groups: [],
|
} else {
|
||||||
};
|
user.value = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Auth check error:', err);
|
||||||
|
user.value = null;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isAdmin = computed(() => {
|
// Logout method
|
||||||
return authState.value.groups?.includes('admin') || false;
|
const logout = async () => {
|
||||||
});
|
try {
|
||||||
|
await $fetch('/api/auth/logout', { method: 'POST' });
|
||||||
const hasRole = (role: string) => {
|
user.value = null;
|
||||||
return authState.value.groups?.includes(role) || false;
|
await navigateTo('/login');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Logout error:', err);
|
||||||
|
user.value = null;
|
||||||
|
await navigateTo('/login');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
authState: readonly(authState),
|
// State
|
||||||
user: computed(() => authState.value.user),
|
user: readonly(user),
|
||||||
authenticated: computed(() => authState.value.authenticated),
|
isAuthenticated,
|
||||||
groups: computed(() => authState.value.groups),
|
loading: readonly(loading),
|
||||||
|
error: readonly(error),
|
||||||
|
|
||||||
|
// Tier-based properties
|
||||||
|
userTier,
|
||||||
|
isUser,
|
||||||
|
isBoard,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
hasRole,
|
firstName,
|
||||||
|
|
||||||
|
// Helper methods
|
||||||
|
hasTier,
|
||||||
|
hasGroup,
|
||||||
|
hasRole, // Legacy compatibility
|
||||||
|
|
||||||
|
// Actions
|
||||||
login,
|
login,
|
||||||
|
loginOAuth,
|
||||||
logout,
|
logout,
|
||||||
|
requestPasswordReset,
|
||||||
checkAuth,
|
checkAuth,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,214 @@
|
||||||
|
<template>
|
||||||
|
<div class="error-page">
|
||||||
|
<v-app>
|
||||||
|
<v-main>
|
||||||
|
<v-container class="fill-height">
|
||||||
|
<v-row justify="center" align="center" class="fill-height">
|
||||||
|
<v-col cols="12" md="8" lg="6" class="text-center">
|
||||||
|
<!-- Logo -->
|
||||||
|
<v-img
|
||||||
|
src="/MONACOUSA-Flags_376x376.png"
|
||||||
|
width="120"
|
||||||
|
height="120"
|
||||||
|
class="mx-auto mb-6"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Error Code -->
|
||||||
|
<h1 class="text-h1 font-weight-bold mb-4" style="color: #a31515;">
|
||||||
|
{{ error.statusCode }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<!-- Error Title -->
|
||||||
|
<h2 class="text-h3 mb-4 text-grey-darken-2">
|
||||||
|
{{ getErrorTitle(error.statusCode) }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<!-- Error Message -->
|
||||||
|
<p class="text-h6 mb-6 text-medium-emphasis" style="max-width: 600px; margin: 0 auto;">
|
||||||
|
{{ getErrorMessage(error.statusCode) }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Additional Info for 403 -->
|
||||||
|
<v-alert
|
||||||
|
v-if="error.statusCode === 403"
|
||||||
|
type="warning"
|
||||||
|
variant="tonal"
|
||||||
|
class="mb-6 text-left"
|
||||||
|
style="max-width: 500px; margin: 0 auto;"
|
||||||
|
>
|
||||||
|
<v-alert-title>Access Restricted</v-alert-title>
|
||||||
|
<p class="mb-2">This resource requires specific permissions:</p>
|
||||||
|
<ul class="ml-4">
|
||||||
|
<li v-if="error.statusMessage?.includes('Board')">Board membership required</li>
|
||||||
|
<li v-if="error.statusMessage?.includes('Admin')">Administrator privileges required</li>
|
||||||
|
<li v-if="!error.statusMessage?.includes('Board') && !error.statusMessage?.includes('Admin')">
|
||||||
|
Higher access level required
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</v-alert>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="d-flex flex-column flex-sm-row justify-center gap-4 mb-6">
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
style="background-color: #a31515;"
|
||||||
|
@click="goHome"
|
||||||
|
>
|
||||||
|
<v-icon start>mdi-home</v-icon>
|
||||||
|
Go to Dashboard
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
variant="outlined"
|
||||||
|
size="large"
|
||||||
|
style="border-color: #a31515; color: #a31515;"
|
||||||
|
@click="goBack"
|
||||||
|
>
|
||||||
|
<v-icon start>mdi-arrow-left</v-icon>
|
||||||
|
Go Back
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contact Support for 403 -->
|
||||||
|
<div v-if="error.statusCode === 403" class="mt-8">
|
||||||
|
<v-divider class="mb-4" />
|
||||||
|
<p class="text-body-2 text-medium-emphasis mb-3">
|
||||||
|
Need access to this resource?
|
||||||
|
</p>
|
||||||
|
<v-btn
|
||||||
|
variant="text"
|
||||||
|
color="primary"
|
||||||
|
@click="contactSupport"
|
||||||
|
>
|
||||||
|
<v-icon start>mdi-email</v-icon>
|
||||||
|
Contact Administrator
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Debug Info (development only) -->
|
||||||
|
<div v-if="isDevelopment" class="mt-8 pa-4 bg-grey-lighten-4 rounded">
|
||||||
|
<p class="text-caption text-grey-darken-1 mb-2">Debug Information:</p>
|
||||||
|
<p class="text-caption font-mono">{{ error.statusMessage }}</p>
|
||||||
|
<p class="text-caption font-mono">{{ error.url }}</p>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</v-main>
|
||||||
|
</v-app>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
interface ErrorProps {
|
||||||
|
error: {
|
||||||
|
statusCode: number;
|
||||||
|
statusMessage: string;
|
||||||
|
url?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<ErrorProps>();
|
||||||
|
|
||||||
|
// Check if we're in development mode
|
||||||
|
const isDevelopment = process.dev;
|
||||||
|
|
||||||
|
// Error title mapping
|
||||||
|
const getErrorTitle = (code: number): string => {
|
||||||
|
switch (code) {
|
||||||
|
case 403: return 'Access Denied';
|
||||||
|
case 404: return 'Page Not Found';
|
||||||
|
case 500: return 'Server Error';
|
||||||
|
case 401: return 'Unauthorized';
|
||||||
|
default: return 'Something Went Wrong';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Error message mapping
|
||||||
|
const getErrorMessage = (code: number): string => {
|
||||||
|
switch (code) {
|
||||||
|
case 403:
|
||||||
|
return 'You do not have the required permissions to access this resource. Please contact your administrator if you believe this is an error.';
|
||||||
|
case 404:
|
||||||
|
return 'The page you are looking for could not be found. It may have been moved, deleted, or you may have entered the wrong URL.';
|
||||||
|
case 500:
|
||||||
|
return 'An internal server error occurred. Our team has been notified and is working to resolve the issue. Please try again later.';
|
||||||
|
case 401:
|
||||||
|
return 'You need to be logged in to access this resource. Please sign in and try again.';
|
||||||
|
default:
|
||||||
|
return 'An unexpected error occurred. Please try refreshing the page or contact support if the problem persists.';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Navigation methods
|
||||||
|
const goHome = () => {
|
||||||
|
navigateTo('/dashboard');
|
||||||
|
};
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
if (window.history.length > 1) {
|
||||||
|
window.history.back();
|
||||||
|
} else {
|
||||||
|
navigateTo('/dashboard');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const contactSupport = () => {
|
||||||
|
// TODO: Implement support contact (email, help desk, etc.)
|
||||||
|
window.location.href = 'mailto:support@monacousa.org?subject=Access Request&body=I need access to a restricted resource.';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set page title
|
||||||
|
useHead({
|
||||||
|
title: `Error ${props.error.statusCode} - MonacoUSA Portal`,
|
||||||
|
meta: [
|
||||||
|
{ name: 'robots', content: 'noindex' }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.error-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-main {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-mono {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-btn {
|
||||||
|
text-transform: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-alert {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-alert ul {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-alert li {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.text-h1 {
|
||||||
|
font-size: 4rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-h3 {
|
||||||
|
font-size: 1.75rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-h6 {
|
||||||
|
font-size: 1.1rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,260 @@
|
||||||
|
<template>
|
||||||
|
<v-app>
|
||||||
|
<v-navigation-drawer v-model="drawer" app width="280">
|
||||||
|
<!-- Logo Section -->
|
||||||
|
<v-list-item class="pa-4 text-center">
|
||||||
|
<v-img
|
||||||
|
src="/MONACOUSA-Flags_376x376.png"
|
||||||
|
width="80"
|
||||||
|
height="80"
|
||||||
|
class="mx-auto mb-2"
|
||||||
|
/>
|
||||||
|
<div class="text-h6 font-weight-bold" style="color: #a31515;">
|
||||||
|
MonacoUSA Portal
|
||||||
|
</div>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
|
||||||
|
<!-- Navigation Menu -->
|
||||||
|
<v-list nav>
|
||||||
|
<!-- Always visible items -->
|
||||||
|
<v-list-item
|
||||||
|
to="/dashboard"
|
||||||
|
prepend-icon="mdi-view-dashboard"
|
||||||
|
title="Dashboard"
|
||||||
|
value="dashboard"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
to="/profile"
|
||||||
|
prepend-icon="mdi-account"
|
||||||
|
title="My Profile"
|
||||||
|
value="profile"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Board-only items -->
|
||||||
|
<template v-if="isBoard || isAdmin">
|
||||||
|
<v-divider class="my-2" />
|
||||||
|
<v-list-subheader>Board Tools</v-list-subheader>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
to="/meetings"
|
||||||
|
prepend-icon="mdi-calendar-clock"
|
||||||
|
title="Meetings"
|
||||||
|
value="meetings"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
to="/members"
|
||||||
|
prepend-icon="mdi-account-group"
|
||||||
|
title="Members"
|
||||||
|
value="members"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
to="/reports"
|
||||||
|
prepend-icon="mdi-chart-line"
|
||||||
|
title="Reports"
|
||||||
|
value="reports"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Admin-only items -->
|
||||||
|
<template v-if="isAdmin">
|
||||||
|
<v-divider class="my-2" />
|
||||||
|
<v-list-subheader>Administration</v-list-subheader>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
to="/admin/users"
|
||||||
|
prepend-icon="mdi-account-cog"
|
||||||
|
title="User Management"
|
||||||
|
value="admin-users"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
to="/admin/logs"
|
||||||
|
prepend-icon="mdi-file-document-outline"
|
||||||
|
title="Audit Logs"
|
||||||
|
value="admin-logs"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
to="/admin/settings"
|
||||||
|
prepend-icon="mdi-cog"
|
||||||
|
title="System Settings"
|
||||||
|
value="admin-settings"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</v-list>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<template v-slot:append>
|
||||||
|
<div class="pa-4 text-center">
|
||||||
|
<v-chip
|
||||||
|
:color="getTierColor(userTier)"
|
||||||
|
size="small"
|
||||||
|
variant="elevated"
|
||||||
|
>
|
||||||
|
<v-icon start :icon="getTierIcon(userTier)" />
|
||||||
|
{{ userTier.toUpperCase() }}
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-navigation-drawer>
|
||||||
|
|
||||||
|
<v-app-bar app color="primary" elevation="2">
|
||||||
|
<v-app-bar-nav-icon @click="drawer = !drawer" color="white" />
|
||||||
|
|
||||||
|
<v-toolbar-title class="text-white font-weight-bold">
|
||||||
|
MonacoUSA Portal
|
||||||
|
</v-toolbar-title>
|
||||||
|
|
||||||
|
<v-spacer />
|
||||||
|
|
||||||
|
<!-- User Menu -->
|
||||||
|
<v-menu offset-y>
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn icon v-bind="props" color="white">
|
||||||
|
<v-avatar size="36" color="white">
|
||||||
|
<v-icon color="primary">mdi-account</v-icon>
|
||||||
|
</v-avatar>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-list min-width="200">
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-title class="font-weight-bold">
|
||||||
|
{{ user?.name || 'User' }}
|
||||||
|
</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
{{ user?.email }}
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
<v-chip
|
||||||
|
:color="getTierColor(userTier)"
|
||||||
|
size="x-small"
|
||||||
|
variant="flat"
|
||||||
|
>
|
||||||
|
{{ userTier.toUpperCase() }} TIER
|
||||||
|
</v-chip>
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
|
||||||
|
<v-list-item @click="navigateToProfile">
|
||||||
|
<v-list-item-title>
|
||||||
|
<v-icon start>mdi-account</v-icon>
|
||||||
|
Profile
|
||||||
|
</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item @click="navigateToSettings">
|
||||||
|
<v-list-item-title>
|
||||||
|
<v-icon start>mdi-cog</v-icon>
|
||||||
|
Settings
|
||||||
|
</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
|
||||||
|
<v-list-item @click="handleLogout" class="text-error">
|
||||||
|
<v-list-item-title>
|
||||||
|
<v-icon start>mdi-logout</v-icon>
|
||||||
|
Logout
|
||||||
|
</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
</v-app-bar>
|
||||||
|
|
||||||
|
<v-main>
|
||||||
|
<v-container fluid class="pa-0">
|
||||||
|
<slot />
|
||||||
|
</v-container>
|
||||||
|
</v-main>
|
||||||
|
</v-app>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { user, userTier, isBoard, isAdmin, logout } = useAuth();
|
||||||
|
const drawer = ref(true);
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
const getTierColor = (tier: string) => {
|
||||||
|
switch (tier) {
|
||||||
|
case 'admin': return 'error';
|
||||||
|
case 'board': return 'primary';
|
||||||
|
case 'user': return 'info';
|
||||||
|
default: return 'grey';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTierIcon = (tier: string) => {
|
||||||
|
switch (tier) {
|
||||||
|
case 'admin': return 'mdi-shield-crown';
|
||||||
|
case 'board': return 'mdi-shield-account';
|
||||||
|
case 'user': return 'mdi-account';
|
||||||
|
default: return 'mdi-account';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Navigation methods
|
||||||
|
const navigateToProfile = () => {
|
||||||
|
navigateTo('/profile');
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateToSettings = () => {
|
||||||
|
navigateTo('/settings');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
await logout();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Responsive drawer behavior
|
||||||
|
const { width } = useDisplay();
|
||||||
|
watch(width, (newWidth) => {
|
||||||
|
drawer.value = newWidth >= 1024; // Show drawer on desktop by default
|
||||||
|
}, { immediate: true });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.v-navigation-drawer {
|
||||||
|
border-right: 1px solid rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-list-item {
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 2px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-list-item--active {
|
||||||
|
background-color: rgba(163, 21, 21, 0.1) !important;
|
||||||
|
color: #a31515 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-list-item--active .v-icon {
|
||||||
|
color: #a31515 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-list-subheader {
|
||||||
|
color: #a31515 !important;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-app-bar {
|
||||||
|
background: linear-gradient(135deg, #a31515 0%, #8b1212 100%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-main {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
export default defineNuxtRouteMiddleware((to, from) => {
|
||||||
|
const { isAuthenticated, isAdmin } = useAuth();
|
||||||
|
|
||||||
|
if (!isAuthenticated.value) {
|
||||||
|
return navigateTo('/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAdmin.value) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 403,
|
||||||
|
statusMessage: 'Access denied. Administrator privileges required.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
export default defineNuxtRouteMiddleware((to, from) => {
|
||||||
|
const { isAuthenticated, isBoard, isAdmin } = useAuth();
|
||||||
|
|
||||||
|
if (!isAuthenticated.value) {
|
||||||
|
return navigateTo('/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isBoard.value && !isAdmin.value) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 403,
|
||||||
|
statusMessage: 'Access denied. Board membership required.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
export default defineNuxtRouteMiddleware((to, from) => {
|
||||||
|
const { isAuthenticated } = useAuth();
|
||||||
|
|
||||||
|
if (!isAuthenticated.value) {
|
||||||
|
return navigateTo('/login');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,518 @@
|
||||||
|
<template>
|
||||||
|
<v-container>
|
||||||
|
<!-- Welcome Header -->
|
||||||
|
<v-row class="mb-6">
|
||||||
|
<v-col>
|
||||||
|
<h1 class="text-h3 font-weight-bold" style="color: #a31515;">
|
||||||
|
Welcome Back, {{ firstName }}!
|
||||||
|
</h1>
|
||||||
|
<p class="text-h6 text-medium-emphasis">
|
||||||
|
MonacoUSA Administration Portal
|
||||||
|
</p>
|
||||||
|
<v-chip color="error" variant="elevated" class="mt-2">
|
||||||
|
<v-icon start>mdi-shield-crown</v-icon>
|
||||||
|
Administrator
|
||||||
|
</v-chip>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- System Status -->
|
||||||
|
<v-row class="mb-6">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card class="pa-4" elevation="2">
|
||||||
|
<h3 class="mb-3">
|
||||||
|
<v-icon class="mr-2" color="success">mdi-server</v-icon>
|
||||||
|
System Status
|
||||||
|
</h3>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="6" md="3">
|
||||||
|
<v-chip color="success" variant="elevated">
|
||||||
|
<v-icon start>mdi-check-circle</v-icon>
|
||||||
|
System Healthy
|
||||||
|
</v-chip>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6" md="3">
|
||||||
|
<v-chip color="info" variant="elevated">
|
||||||
|
<v-icon start>mdi-account-multiple</v-icon>
|
||||||
|
{{ systemStats.totalUsers }} Users
|
||||||
|
</v-chip>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6" md="3">
|
||||||
|
<v-chip color="warning" variant="elevated">
|
||||||
|
<v-icon start>mdi-database</v-icon>
|
||||||
|
{{ systemStats.diskUsage }} Disk
|
||||||
|
</v-chip>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6" md="3">
|
||||||
|
<v-chip color="primary" variant="elevated">
|
||||||
|
<v-icon start>mdi-memory</v-icon>
|
||||||
|
{{ systemStats.memoryUsage }} Memory
|
||||||
|
</v-chip>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Admin Tools -->
|
||||||
|
<v-row class="mb-6">
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-card class="pa-4 text-center" elevation="2" hover>
|
||||||
|
<v-icon size="48" color="error" class="mb-2">mdi-account-cog</v-icon>
|
||||||
|
<h3 class="mb-2">User Management</h3>
|
||||||
|
<p class="text-body-2 mb-4">Add, edit, and manage user accounts</p>
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
variant="outlined"
|
||||||
|
style="border-color: #f44336; color: #f44336;"
|
||||||
|
@click="navigateToUserManagement"
|
||||||
|
>
|
||||||
|
Manage Users
|
||||||
|
</v-btn>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-card class="pa-4 text-center" elevation="2" hover>
|
||||||
|
<v-icon size="48" color="error" class="mb-2">mdi-file-document-outline</v-icon>
|
||||||
|
<h3 class="mb-2">Audit Logs</h3>
|
||||||
|
<p class="text-body-2 mb-4">View system and user activity logs</p>
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
variant="outlined"
|
||||||
|
style="border-color: #f44336; color: #f44336;"
|
||||||
|
@click="navigateToAuditLogs"
|
||||||
|
>
|
||||||
|
View Logs
|
||||||
|
</v-btn>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-card class="pa-4 text-center" elevation="2" hover>
|
||||||
|
<v-icon size="48" color="error" class="mb-2">mdi-cog</v-icon>
|
||||||
|
<h3 class="mb-2">System Config</h3>
|
||||||
|
<p class="text-body-2 mb-4">Configure system settings</p>
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
variant="outlined"
|
||||||
|
style="border-color: #f44336; color: #f44336;"
|
||||||
|
@click="navigateToSystemConfig"
|
||||||
|
>
|
||||||
|
Settings
|
||||||
|
</v-btn>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- System Metrics -->
|
||||||
|
<v-row class="mb-6">
|
||||||
|
<v-col cols="12" md="8">
|
||||||
|
<v-card elevation="2">
|
||||||
|
<v-card-title class="pa-4" style="background-color: #f5f5f5;">
|
||||||
|
<v-icon class="mr-2" color="primary">mdi-chart-line</v-icon>
|
||||||
|
System Metrics
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="6" md="3" class="text-center">
|
||||||
|
<div class="text-h4 font-weight-bold" style="color: #a31515;">{{ systemStats.totalUsers }}</div>
|
||||||
|
<div class="text-body-2">Total Users</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6" md="3" class="text-center">
|
||||||
|
<div class="text-h4 font-weight-bold" style="color: #a31515;">{{ systemStats.activeUsers }}</div>
|
||||||
|
<div class="text-body-2">Active Users</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6" md="3" class="text-center">
|
||||||
|
<div class="text-h4 font-weight-bold" style="color: #a31515;">{{ systemStats.totalSessions }}</div>
|
||||||
|
<div class="text-body-2">Active Sessions</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6" md="3" class="text-center">
|
||||||
|
<div class="text-h4 font-weight-bold" style="color: #a31515;">{{ systemStats.uptime }}</div>
|
||||||
|
<div class="text-body-2">System Uptime</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-card elevation="2">
|
||||||
|
<v-card-title class="pa-4" style="background-color: #f5f5f5;">
|
||||||
|
<v-icon class="mr-2" color="primary">mdi-backup-restore</v-icon>
|
||||||
|
Last Backup
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<div class="text-h6 mb-2">System Backup</div>
|
||||||
|
<div class="text-body-2 mb-2">
|
||||||
|
<v-icon size="small" class="mr-1">mdi-calendar</v-icon>
|
||||||
|
{{ lastBackup.date }}
|
||||||
|
</div>
|
||||||
|
<div class="text-body-2 mb-4">
|
||||||
|
<v-icon size="small" class="mr-1">mdi-clock</v-icon>
|
||||||
|
{{ lastBackup.time }}
|
||||||
|
</div>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
style="border-color: #a31515; color: #a31515;"
|
||||||
|
@click="initiateBackup"
|
||||||
|
>
|
||||||
|
Create Backup
|
||||||
|
</v-btn>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Recent Admin Activity -->
|
||||||
|
<v-row class="mb-6">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card elevation="2">
|
||||||
|
<v-card-title class="pa-4" style="background-color: #f5f5f5;">
|
||||||
|
<v-icon class="mr-2" color="primary">mdi-history</v-icon>
|
||||||
|
Recent Admin Activity
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<v-list>
|
||||||
|
<v-list-item v-for="activity in recentActivity" :key="activity.id">
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>{{ activity.title }}</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
{{ activity.description }} - {{ activity.timestamp }}
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
</v-list-item-content>
|
||||||
|
<v-list-item-action>
|
||||||
|
<v-chip :color="activity.type" size="small">{{ activity.status }}</v-chip>
|
||||||
|
</v-list-item-action>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Security & Monitoring -->
|
||||||
|
<v-row class="mb-6">
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card elevation="2">
|
||||||
|
<v-card-title class="pa-4" style="background-color: #f5f5f5;">
|
||||||
|
<v-icon class="mr-2" color="error">mdi-security</v-icon>
|
||||||
|
Security Alerts
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<v-list>
|
||||||
|
<v-list-item v-for="alert in securityAlerts" :key="alert.id">
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>{{ alert.title }}</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ alert.description }}</v-list-item-subtitle>
|
||||||
|
</v-list-item-content>
|
||||||
|
<v-list-item-action>
|
||||||
|
<v-chip :color="alert.severity" size="small">{{ alert.level }}</v-chip>
|
||||||
|
</v-list-item-action>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card elevation="2">
|
||||||
|
<v-card-title class="pa-4" style="background-color: #f5f5f5;">
|
||||||
|
<v-icon class="mr-2" color="primary">mdi-monitor-dashboard</v-icon>
|
||||||
|
System Health
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="d-flex justify-space-between mb-1">
|
||||||
|
<span>CPU Usage</span>
|
||||||
|
<span>{{ systemHealth.cpu }}%</span>
|
||||||
|
</div>
|
||||||
|
<v-progress-linear
|
||||||
|
:model-value="systemHealth.cpu"
|
||||||
|
color="primary"
|
||||||
|
height="8"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="d-flex justify-space-between mb-1">
|
||||||
|
<span>Memory Usage</span>
|
||||||
|
<span>{{ systemHealth.memory }}%</span>
|
||||||
|
</div>
|
||||||
|
<v-progress-linear
|
||||||
|
:model-value="systemHealth.memory"
|
||||||
|
color="warning"
|
||||||
|
height="8"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="d-flex justify-space-between mb-1">
|
||||||
|
<span>Disk Usage</span>
|
||||||
|
<span>{{ systemHealth.disk }}%</span>
|
||||||
|
</div>
|
||||||
|
<v-progress-linear
|
||||||
|
:model-value="systemHealth.disk"
|
||||||
|
color="success"
|
||||||
|
height="8"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Quick Admin Actions -->
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card elevation="2">
|
||||||
|
<v-card-title class="pa-4" style="background-color: #f5f5f5;">
|
||||||
|
<v-icon class="mr-2" color="primary">mdi-lightning-bolt</v-icon>
|
||||||
|
Quick Admin Actions
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
variant="outlined"
|
||||||
|
block
|
||||||
|
style="border-color: #f44336; color: #f44336;"
|
||||||
|
@click="createNewUser"
|
||||||
|
>
|
||||||
|
<v-icon start>mdi-account-plus</v-icon>
|
||||||
|
Create User
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
variant="outlined"
|
||||||
|
block
|
||||||
|
style="border-color: #f44336; color: #f44336;"
|
||||||
|
@click="generateSystemReport"
|
||||||
|
>
|
||||||
|
<v-icon start>mdi-file-chart</v-icon>
|
||||||
|
System Report
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
variant="outlined"
|
||||||
|
block
|
||||||
|
style="border-color: #f44336; color: #f44336;"
|
||||||
|
@click="managePermissions"
|
||||||
|
>
|
||||||
|
<v-icon start>mdi-shield-key</v-icon>
|
||||||
|
Permissions
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
variant="outlined"
|
||||||
|
block
|
||||||
|
style="border-color: #f44336; color: #f44336;"
|
||||||
|
@click="systemMaintenance"
|
||||||
|
>
|
||||||
|
<v-icon start>mdi-wrench</v-icon>
|
||||||
|
Maintenance
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'dashboard',
|
||||||
|
middleware: 'auth'
|
||||||
|
});
|
||||||
|
|
||||||
|
const { firstName, isAdmin } = useAuth();
|
||||||
|
|
||||||
|
// Check admin access on mount
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!isAdmin.value) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 403,
|
||||||
|
statusMessage: 'Access denied. Administrator privileges required.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load admin dashboard data
|
||||||
|
await loadAdminStats();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reactive data
|
||||||
|
const systemStats = ref({
|
||||||
|
totalUsers: 0,
|
||||||
|
activeUsers: 0,
|
||||||
|
totalSessions: 0,
|
||||||
|
diskUsage: '0%',
|
||||||
|
memoryUsage: '0%',
|
||||||
|
uptime: '0d'
|
||||||
|
});
|
||||||
|
|
||||||
|
const lastBackup = ref({
|
||||||
|
date: 'January 7, 2025',
|
||||||
|
time: '3:00 AM EST'
|
||||||
|
});
|
||||||
|
|
||||||
|
const systemHealth = ref({
|
||||||
|
cpu: 45,
|
||||||
|
memory: 62,
|
||||||
|
disk: 38
|
||||||
|
});
|
||||||
|
|
||||||
|
const recentActivity = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'User Created',
|
||||||
|
description: 'New user account created for john.doe@example.com',
|
||||||
|
timestamp: '2 hours ago',
|
||||||
|
type: 'success',
|
||||||
|
status: 'Completed'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'System Backup',
|
||||||
|
description: 'Automated system backup completed successfully',
|
||||||
|
timestamp: '6 hours ago',
|
||||||
|
type: 'info',
|
||||||
|
status: 'Completed'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'Permission Update',
|
||||||
|
description: 'User permissions updated for board member',
|
||||||
|
timestamp: '1 day ago',
|
||||||
|
type: 'warning',
|
||||||
|
status: 'Completed'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const securityAlerts = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Failed Login Attempts',
|
||||||
|
description: '3 failed login attempts from IP 192.168.1.100',
|
||||||
|
severity: 'warning',
|
||||||
|
level: 'Medium'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'System Update Available',
|
||||||
|
description: 'Security update available for Keycloak',
|
||||||
|
severity: 'info',
|
||||||
|
level: 'Low'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Load admin statistics
|
||||||
|
const loadAdminStats = async () => {
|
||||||
|
try {
|
||||||
|
const stats = await $fetch<{
|
||||||
|
totalUsers: number;
|
||||||
|
activeUsers: number;
|
||||||
|
totalSessions: number;
|
||||||
|
diskUsage: string;
|
||||||
|
memoryUsage: string;
|
||||||
|
}>('/api/admin/stats');
|
||||||
|
|
||||||
|
systemStats.value = {
|
||||||
|
totalUsers: stats.totalUsers || 0,
|
||||||
|
activeUsers: stats.activeUsers || 0,
|
||||||
|
totalSessions: stats.totalSessions || 0,
|
||||||
|
diskUsage: stats.diskUsage || '0%',
|
||||||
|
memoryUsage: stats.memoryUsage || '0%',
|
||||||
|
uptime: '5d 12h'
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load admin stats:', error);
|
||||||
|
// Use mock data on error
|
||||||
|
systemStats.value = {
|
||||||
|
totalUsers: 156,
|
||||||
|
activeUsers: 45,
|
||||||
|
totalSessions: 67,
|
||||||
|
diskUsage: '45%',
|
||||||
|
memoryUsage: '62%',
|
||||||
|
uptime: '5d 12h'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Navigation methods (placeholder implementations)
|
||||||
|
const navigateToUserManagement = () => {
|
||||||
|
console.log('Navigate to user management');
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateToAuditLogs = () => {
|
||||||
|
console.log('Navigate to audit logs');
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateToSystemConfig = () => {
|
||||||
|
console.log('Navigate to system config');
|
||||||
|
};
|
||||||
|
|
||||||
|
const initiateBackup = () => {
|
||||||
|
console.log('Initiate system backup');
|
||||||
|
};
|
||||||
|
|
||||||
|
const createNewUser = () => {
|
||||||
|
console.log('Create new user');
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateSystemReport = () => {
|
||||||
|
console.log('Generate system report');
|
||||||
|
};
|
||||||
|
|
||||||
|
const managePermissions = () => {
|
||||||
|
console.log('Manage permissions');
|
||||||
|
};
|
||||||
|
|
||||||
|
const systemMaintenance = () => {
|
||||||
|
console.log('System maintenance');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.v-card {
|
||||||
|
border-radius: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
transition: transform 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-btn {
|
||||||
|
text-transform: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-icon {
|
||||||
|
color: #a31515 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-body-2 {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-chip {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-progress-linear {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,345 @@
|
||||||
|
<template>
|
||||||
|
<v-container>
|
||||||
|
<!-- Welcome Header -->
|
||||||
|
<v-row class="mb-6">
|
||||||
|
<v-col>
|
||||||
|
<h1 class="text-h3 font-weight-bold" style="color: #a31515;">
|
||||||
|
Welcome Back, {{ firstName }}!
|
||||||
|
</h1>
|
||||||
|
<p class="text-h6 text-medium-emphasis">
|
||||||
|
MonacoUSA Board Portal
|
||||||
|
</p>
|
||||||
|
<v-chip color="primary" variant="elevated" class="mt-2">
|
||||||
|
<v-icon start>mdi-shield-account</v-icon>
|
||||||
|
Board Member
|
||||||
|
</v-chip>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Board Tools -->
|
||||||
|
<v-row class="mb-6">
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-card class="pa-4 text-center" elevation="2" hover>
|
||||||
|
<v-icon size="48" color="primary" class="mb-2">mdi-calendar-clock</v-icon>
|
||||||
|
<h3 class="mb-2">Meetings</h3>
|
||||||
|
<p class="text-body-2 mb-4">Schedule and manage board meetings</p>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
style="border-color: #a31515; color: #a31515;"
|
||||||
|
@click="navigateToMeetings"
|
||||||
|
>
|
||||||
|
Manage Meetings
|
||||||
|
</v-btn>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-card class="pa-4 text-center" elevation="2" hover>
|
||||||
|
<v-icon size="48" color="primary" class="mb-2">mdi-account-group</v-icon>
|
||||||
|
<h3 class="mb-2">Members</h3>
|
||||||
|
<p class="text-body-2 mb-4">View and manage association members</p>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
style="border-color: #a31515; color: #a31515;"
|
||||||
|
@click="navigateToMembers"
|
||||||
|
>
|
||||||
|
View Members
|
||||||
|
</v-btn>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-card class="pa-4 text-center" elevation="2" hover>
|
||||||
|
<v-icon size="48" color="primary" class="mb-2">mdi-chart-line</v-icon>
|
||||||
|
<h3 class="mb-2">Reports</h3>
|
||||||
|
<p class="text-body-2 mb-4">Financial and activity reports</p>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
style="border-color: #a31515; color: #a31515;"
|
||||||
|
@click="navigateToReports"
|
||||||
|
>
|
||||||
|
View Reports
|
||||||
|
</v-btn>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-card class="pa-4 text-center" elevation="2" hover>
|
||||||
|
<v-icon size="48" color="primary" class="mb-2">mdi-tools</v-icon>
|
||||||
|
<h3 class="mb-2">Tools</h3>
|
||||||
|
<p class="text-body-2 mb-4">Board management tools</p>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
style="border-color: #a31515; color: #a31515;"
|
||||||
|
@click="navigateToTools"
|
||||||
|
>
|
||||||
|
Access Tools
|
||||||
|
</v-btn>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Board Statistics -->
|
||||||
|
<v-row class="mb-6">
|
||||||
|
<v-col cols="12" md="8">
|
||||||
|
<v-card elevation="2">
|
||||||
|
<v-card-title class="pa-4" style="background-color: #f5f5f5;">
|
||||||
|
<v-icon class="mr-2" color="primary">mdi-chart-box-outline</v-icon>
|
||||||
|
Board Overview
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="6" md="3" class="text-center">
|
||||||
|
<div class="text-h4 font-weight-bold" style="color: #a31515;">{{ stats.totalMembers }}</div>
|
||||||
|
<div class="text-body-2">Total Members</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6" md="3" class="text-center">
|
||||||
|
<div class="text-h4 font-weight-bold" style="color: #a31515;">{{ stats.activeMembers }}</div>
|
||||||
|
<div class="text-body-2">Active Members</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6" md="3" class="text-center">
|
||||||
|
<div class="text-h4 font-weight-bold" style="color: #a31515;">{{ stats.upcomingMeetings }}</div>
|
||||||
|
<div class="text-body-2">Upcoming Meetings</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6" md="3" class="text-center">
|
||||||
|
<div class="text-h4 font-weight-bold" style="color: #a31515;">{{ stats.pendingActions }}</div>
|
||||||
|
<div class="text-body-2">Pending Actions</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-card elevation="2">
|
||||||
|
<v-card-title class="pa-4" style="background-color: #f5f5f5;">
|
||||||
|
<v-icon class="mr-2" color="primary">mdi-calendar-today</v-icon>
|
||||||
|
Next Meeting
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<div class="text-h6 mb-2">Board Meeting</div>
|
||||||
|
<div class="text-body-2 mb-2">
|
||||||
|
<v-icon size="small" class="mr-1">mdi-calendar</v-icon>
|
||||||
|
{{ nextMeeting.date }}
|
||||||
|
</div>
|
||||||
|
<div class="text-body-2 mb-4">
|
||||||
|
<v-icon size="small" class="mr-1">mdi-clock</v-icon>
|
||||||
|
{{ nextMeeting.time }}
|
||||||
|
</div>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
style="border-color: #a31515; color: #a31515;"
|
||||||
|
@click="viewMeetingDetails"
|
||||||
|
>
|
||||||
|
View Details
|
||||||
|
</v-btn>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Recent Board Activity -->
|
||||||
|
<v-row class="mb-6">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card elevation="2">
|
||||||
|
<v-card-title class="pa-4" style="background-color: #f5f5f5;">
|
||||||
|
<v-icon class="mr-2" color="primary">mdi-history</v-icon>
|
||||||
|
Recent Board Activity
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<v-list>
|
||||||
|
<v-list-item v-for="activity in recentActivity" :key="activity.id">
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>{{ activity.title }}</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ activity.description }}</v-list-item-subtitle>
|
||||||
|
</v-list-item-content>
|
||||||
|
<v-list-item-action>
|
||||||
|
<v-chip :color="activity.type" size="small">{{ activity.status }}</v-chip>
|
||||||
|
</v-list-item-action>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card elevation="2">
|
||||||
|
<v-card-title class="pa-4" style="background-color: #f5f5f5;">
|
||||||
|
<v-icon class="mr-2" color="primary">mdi-lightning-bolt</v-icon>
|
||||||
|
Quick Actions
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
block
|
||||||
|
style="border-color: #a31515; color: #a31515;"
|
||||||
|
@click="scheduleNewMeeting"
|
||||||
|
>
|
||||||
|
<v-icon start>mdi-plus</v-icon>
|
||||||
|
Schedule New Meeting
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
block
|
||||||
|
style="border-color: #a31515; color: #a31515;"
|
||||||
|
@click="createAnnouncement"
|
||||||
|
>
|
||||||
|
<v-icon start>mdi-bullhorn</v-icon>
|
||||||
|
Create Announcement
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
block
|
||||||
|
style="border-color: #a31515; color: #a31515;"
|
||||||
|
@click="generateReport"
|
||||||
|
>
|
||||||
|
<v-icon start>mdi-file-chart</v-icon>
|
||||||
|
Generate Report
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'dashboard',
|
||||||
|
middleware: 'auth'
|
||||||
|
});
|
||||||
|
|
||||||
|
const { firstName, isBoard, isAdmin } = useAuth();
|
||||||
|
|
||||||
|
// Check board access on mount
|
||||||
|
onMounted(() => {
|
||||||
|
if (!isBoard.value && !isAdmin.value) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 403,
|
||||||
|
statusMessage: 'Access denied. Board membership required.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock data for board dashboard
|
||||||
|
const stats = ref({
|
||||||
|
totalMembers: 156,
|
||||||
|
activeMembers: 142,
|
||||||
|
upcomingMeetings: 3,
|
||||||
|
pendingActions: 7
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextMeeting = ref({
|
||||||
|
date: 'January 15, 2025',
|
||||||
|
time: '7:00 PM EST'
|
||||||
|
});
|
||||||
|
|
||||||
|
const recentActivity = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Monthly Board Meeting',
|
||||||
|
description: 'Meeting minutes approved and distributed',
|
||||||
|
type: 'success',
|
||||||
|
status: 'Completed'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Budget Review',
|
||||||
|
description: 'Q4 financial report under review',
|
||||||
|
type: 'warning',
|
||||||
|
status: 'In Progress'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'Member Application',
|
||||||
|
description: 'New member application pending approval',
|
||||||
|
type: 'info',
|
||||||
|
status: 'Pending'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Navigation methods (placeholder implementations)
|
||||||
|
const navigateToMeetings = () => {
|
||||||
|
console.log('Navigate to meetings');
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateToMembers = () => {
|
||||||
|
console.log('Navigate to members');
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateToReports = () => {
|
||||||
|
console.log('Navigate to reports');
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateToTools = () => {
|
||||||
|
console.log('Navigate to tools');
|
||||||
|
};
|
||||||
|
|
||||||
|
const viewMeetingDetails = () => {
|
||||||
|
console.log('View meeting details');
|
||||||
|
};
|
||||||
|
|
||||||
|
const scheduleNewMeeting = () => {
|
||||||
|
console.log('Schedule new meeting');
|
||||||
|
};
|
||||||
|
|
||||||
|
const createAnnouncement = () => {
|
||||||
|
console.log('Create announcement');
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateReport = () => {
|
||||||
|
console.log('Generate report');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.v-card {
|
||||||
|
border-radius: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
transition: transform 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-btn {
|
||||||
|
text-transform: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-icon {
|
||||||
|
color: #a31515 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-body-2 {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-chip {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
<template>
|
||||||
|
<div class="dashboard-router">
|
||||||
|
<v-container v-if="loading" class="fill-height">
|
||||||
|
<v-row justify="center" align="center">
|
||||||
|
<v-col cols="auto" class="text-center">
|
||||||
|
<v-progress-circular
|
||||||
|
indeterminate
|
||||||
|
color="primary"
|
||||||
|
size="64"
|
||||||
|
width="6"
|
||||||
|
/>
|
||||||
|
<p class="mt-4 text-h6">Loading your dashboard...</p>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'auth'
|
||||||
|
});
|
||||||
|
|
||||||
|
const { user, userTier, isAuthenticated } = useAuth();
|
||||||
|
const loading = ref(true);
|
||||||
|
|
||||||
|
// Route to tier-specific dashboard
|
||||||
|
watchEffect(async () => {
|
||||||
|
if (isAuthenticated.value && user.value) {
|
||||||
|
const tierRoute = `/dashboard/${userTier.value}`;
|
||||||
|
console.log('🔄 Routing to tier-specific dashboard:', tierRoute);
|
||||||
|
await navigateTo(tierRoute, { replace: true });
|
||||||
|
} else if (!isAuthenticated.value) {
|
||||||
|
console.log('🔄 User not authenticated, redirecting to login');
|
||||||
|
await navigateTo('/login');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// Small delay to ensure auth state is loaded
|
||||||
|
setTimeout(() => {
|
||||||
|
loading.value = false;
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dashboard-router {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-progress-circular {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,211 @@
|
||||||
|
<template>
|
||||||
|
<v-container>
|
||||||
|
<!-- Welcome Header -->
|
||||||
|
<v-row class="mb-6">
|
||||||
|
<v-col>
|
||||||
|
<h1 class="text-h3 font-weight-bold" style="color: #a31515;">
|
||||||
|
Welcome Back, {{ firstName }}!
|
||||||
|
</h1>
|
||||||
|
<p class="text-h6 text-medium-emphasis">
|
||||||
|
MonacoUSA Member Portal
|
||||||
|
</p>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<v-row class="mb-6">
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-card class="pa-4 text-center" elevation="2" hover>
|
||||||
|
<v-icon size="48" color="primary" class="mb-2">mdi-account</v-icon>
|
||||||
|
<h3 class="mb-2">My Profile</h3>
|
||||||
|
<p class="text-body-2 mb-4">View and update your information</p>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
style="border-color: #a31515; color: #a31515;"
|
||||||
|
@click="navigateToProfile"
|
||||||
|
>
|
||||||
|
View Profile
|
||||||
|
</v-btn>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-card class="pa-4 text-center" elevation="2" hover>
|
||||||
|
<v-icon size="48" color="primary" class="mb-2">mdi-calendar</v-icon>
|
||||||
|
<h3 class="mb-2">Events</h3>
|
||||||
|
<p class="text-body-2 mb-4">View upcoming association events</p>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
style="border-color: #a31515; color: #a31515;"
|
||||||
|
@click="navigateToEvents"
|
||||||
|
>
|
||||||
|
View Events
|
||||||
|
</v-btn>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-card class="pa-4 text-center" elevation="2" hover>
|
||||||
|
<v-icon size="48" color="primary" class="mb-2">mdi-file-document</v-icon>
|
||||||
|
<h3 class="mb-2">Resources</h3>
|
||||||
|
<p class="text-body-2 mb-4">Access member resources</p>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
style="border-color: #a31515; color: #a31515;"
|
||||||
|
@click="navigateToResources"
|
||||||
|
>
|
||||||
|
View Resources
|
||||||
|
</v-btn>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Recent Activity Section -->
|
||||||
|
<v-row class="mb-6">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card elevation="2">
|
||||||
|
<v-card-title class="pa-4" style="background-color: #f5f5f5;">
|
||||||
|
<v-icon class="mr-2" color="primary">mdi-clock-outline</v-icon>
|
||||||
|
Recent Activity
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<v-list>
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>Welcome to MonacoUSA Portal!</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
You've successfully logged in to your member dashboard.
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
</v-list-item-content>
|
||||||
|
<v-list-item-action>
|
||||||
|
<v-chip color="success" size="small">New</v-chip>
|
||||||
|
</v-list-item-action>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Member Information -->
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card elevation="2">
|
||||||
|
<v-card-title class="pa-4" style="background-color: #f5f5f5;">
|
||||||
|
<v-icon class="mr-2" color="primary">mdi-information-outline</v-icon>
|
||||||
|
Member Information
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<v-list>
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>Name</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ user?.name || 'Not provided' }}</v-list-item-subtitle>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>Email</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ user?.email || 'Not provided' }}</v-list-item-subtitle>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>Member Type</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
<v-chip color="primary" size="small">{{ userTier.toUpperCase() }}</v-chip>
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card elevation="2">
|
||||||
|
<v-card-title class="pa-4" style="background-color: #f5f5f5;">
|
||||||
|
<v-icon class="mr-2" color="primary">mdi-help-circle-outline</v-icon>
|
||||||
|
Need Help?
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<p class="mb-4">
|
||||||
|
If you need assistance or have questions about your membership,
|
||||||
|
please don't hesitate to contact our support team.
|
||||||
|
</p>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
style="border-color: #a31515; color: #a31515;"
|
||||||
|
@click="contactSupport"
|
||||||
|
>
|
||||||
|
<v-icon start>mdi-email</v-icon>
|
||||||
|
Contact Support
|
||||||
|
</v-btn>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'dashboard',
|
||||||
|
middleware: 'auth'
|
||||||
|
});
|
||||||
|
|
||||||
|
const { firstName, user, userTier } = useAuth();
|
||||||
|
|
||||||
|
// Navigation methods (placeholder implementations)
|
||||||
|
const navigateToProfile = () => {
|
||||||
|
// TODO: Implement profile navigation
|
||||||
|
console.log('Navigate to profile');
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateToEvents = () => {
|
||||||
|
// TODO: Implement events navigation
|
||||||
|
console.log('Navigate to events');
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateToResources = () => {
|
||||||
|
// TODO: Implement resources navigation
|
||||||
|
console.log('Navigate to resources');
|
||||||
|
};
|
||||||
|
|
||||||
|
const contactSupport = () => {
|
||||||
|
// TODO: Implement support contact
|
||||||
|
console.log('Contact support');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.v-card {
|
||||||
|
border-radius: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
transition: transform 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-btn {
|
||||||
|
text-transform: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-icon {
|
||||||
|
color: #a31515 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-body-2 {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -5,8 +5,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const { authenticated } = useAuth();
|
const { isAuthenticated } = useAuth();
|
||||||
|
|
||||||
// Redirect based on authentication status
|
// Redirect based on authentication status
|
||||||
await navigateTo(authenticated.value ? '/dashboard' : '/login');
|
await navigateTo(isAuthenticated.value ? '/dashboard' : '/login');
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
console.log('📊 Admin stats requested at:', new Date().toISOString());
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if user is admin (middleware should handle this, but double-check)
|
||||||
|
const sessionManager = createSessionManager();
|
||||||
|
const cookieHeader = getHeader(event, 'cookie');
|
||||||
|
const session = sessionManager.getSession(cookieHeader);
|
||||||
|
|
||||||
|
if (!session || session.user.tier !== 'admin') {
|
||||||
|
console.warn('🚨 Unauthorized admin stats access attempt');
|
||||||
|
throw createError({
|
||||||
|
statusCode: 403,
|
||||||
|
statusMessage: 'Admin access required'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Admin access verified for user:', session.user.email);
|
||||||
|
|
||||||
|
// For now, return mock data - later integrate with actual data sources
|
||||||
|
const stats = {
|
||||||
|
totalUsers: 156,
|
||||||
|
activeUsers: 45,
|
||||||
|
totalSessions: 67,
|
||||||
|
systemHealth: 'healthy',
|
||||||
|
lastBackup: new Date().toISOString(),
|
||||||
|
diskUsage: '45%',
|
||||||
|
memoryUsage: '62%',
|
||||||
|
recentActivity: [
|
||||||
|
{
|
||||||
|
action: 'User login',
|
||||||
|
user: 'john@example.com',
|
||||||
|
timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(),
|
||||||
|
type: 'info'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'Password reset',
|
||||||
|
user: 'jane@example.com',
|
||||||
|
timestamp: new Date(Date.now() - 4 * 60 * 60 * 1000).toISOString(),
|
||||||
|
type: 'warning'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'User created',
|
||||||
|
user: 'admin@monacousa.org',
|
||||||
|
timestamp: new Date(Date.now() - 6 * 60 * 60 * 1000).toISOString(),
|
||||||
|
type: 'success'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
systemMetrics: {
|
||||||
|
cpu: 45,
|
||||||
|
memory: 62,
|
||||||
|
disk: 38,
|
||||||
|
uptime: '5d 12h 30m'
|
||||||
|
},
|
||||||
|
securityAlerts: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Failed Login Attempts',
|
||||||
|
description: '3 failed login attempts detected',
|
||||||
|
severity: 'medium',
|
||||||
|
timestamp: new Date(Date.now() - 1 * 60 * 60 * 1000).toISOString()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'System Update Available',
|
||||||
|
description: 'Security update available for Keycloak',
|
||||||
|
severity: 'low',
|
||||||
|
timestamp: new Date(Date.now() - 12 * 60 * 60 * 1000).toISOString()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('✅ Admin stats retrieved successfully');
|
||||||
|
return stats;
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ Admin stats error:', error);
|
||||||
|
|
||||||
|
if (error.statusCode) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
statusMessage: 'Failed to retrieve system statistics'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -36,14 +36,24 @@ export default defineEventHandler(async (event) => {
|
||||||
// Get user info
|
// Get user info
|
||||||
const userInfo = await keycloak.getUserInfo(tokens.access_token);
|
const userInfo = await keycloak.getUserInfo(tokens.access_token);
|
||||||
|
|
||||||
|
// Tier determination logic - admin > board > user priority
|
||||||
|
const determineTier = (groups: string[]): 'user' | 'board' | 'admin' => {
|
||||||
|
if (groups.includes('admin')) return 'admin';
|
||||||
|
if (groups.includes('board')) return 'board';
|
||||||
|
return 'user'; // Default tier
|
||||||
|
};
|
||||||
|
|
||||||
// Create session
|
// Create session
|
||||||
const sessionData = {
|
const sessionData = {
|
||||||
user: {
|
user: {
|
||||||
id: userInfo.sub,
|
id: userInfo.sub,
|
||||||
email: userInfo.email,
|
email: userInfo.email,
|
||||||
name: userInfo.name || `${userInfo.given_name} ${userInfo.family_name}`.trim(),
|
name: userInfo.name || `${userInfo.given_name || ''} ${userInfo.family_name || ''}`.trim(),
|
||||||
groups: userInfo.groups || [],
|
firstName: userInfo.given_name,
|
||||||
tier: userInfo.tier,
|
lastName: userInfo.family_name,
|
||||||
|
username: userInfo.preferred_username,
|
||||||
|
tier: determineTier(userInfo.groups || []),
|
||||||
|
groups: userInfo.groups || ['user'],
|
||||||
},
|
},
|
||||||
tokens: {
|
tokens: {
|
||||||
accessToken: tokens.access_token,
|
accessToken: tokens.access_token,
|
||||||
|
|
|
||||||
|
|
@ -203,15 +203,24 @@ export default defineEventHandler(async (event) => {
|
||||||
name: userInfo.name
|
name: userInfo.name
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Tier determination logic - admin > board > user priority
|
||||||
|
const determineTier = (groups: string[]): 'user' | 'board' | 'admin' => {
|
||||||
|
if (groups.includes('admin')) return 'admin';
|
||||||
|
if (groups.includes('board')) return 'board';
|
||||||
|
return 'user'; // Default tier
|
||||||
|
};
|
||||||
|
|
||||||
// Create session data with extended expiry if remember me
|
// Create session data with extended expiry if remember me
|
||||||
const sessionData = {
|
const sessionData = {
|
||||||
user: {
|
user: {
|
||||||
id: userInfo.sub,
|
id: userInfo.sub,
|
||||||
email: userInfo.email,
|
email: userInfo.email,
|
||||||
name: userInfo.name || `${userInfo.given_name || ''} ${userInfo.family_name || ''}`.trim(),
|
name: userInfo.name || `${userInfo.given_name || ''} ${userInfo.family_name || ''}`.trim(),
|
||||||
groups: userInfo.groups || [],
|
firstName: userInfo.given_name,
|
||||||
tier: userInfo.tier,
|
lastName: userInfo.family_name,
|
||||||
username: userInfo.preferred_username || username
|
username: userInfo.preferred_username || username,
|
||||||
|
tier: determineTier(userInfo.groups || []),
|
||||||
|
groups: userInfo.groups || ['user']
|
||||||
},
|
},
|
||||||
tokens: {
|
tokens: {
|
||||||
accessToken: tokens.access_token,
|
accessToken: tokens.access_token,
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,11 @@ export interface User {
|
||||||
id: string;
|
id: string;
|
||||||
email: string;
|
email: string;
|
||||||
name: string;
|
name: string;
|
||||||
groups?: string[];
|
firstName?: string;
|
||||||
tier?: string;
|
lastName?: string;
|
||||||
|
username?: string;
|
||||||
|
tier: 'user' | 'board' | 'admin';
|
||||||
|
groups: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthState {
|
export interface AuthState {
|
||||||
|
|
@ -60,23 +63,19 @@ export interface UserInfo {
|
||||||
given_name?: string;
|
given_name?: string;
|
||||||
family_name?: string;
|
family_name?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
preferred_username?: string;
|
||||||
groups?: string[];
|
groups?: string[];
|
||||||
tier?: string;
|
tier?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SessionData {
|
export interface SessionData {
|
||||||
user: {
|
user: User;
|
||||||
id: string;
|
|
||||||
email: string;
|
|
||||||
name: string;
|
|
||||||
groups?: string[];
|
|
||||||
tier?: string;
|
|
||||||
};
|
|
||||||
tokens: {
|
tokens: {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
expiresAt: number;
|
expiresAt: number;
|
||||||
};
|
};
|
||||||
|
rememberMe?: boolean;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
lastActivity: number;
|
lastActivity: number;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue