1951 lines
46 KiB
Markdown
1951 lines
46 KiB
Markdown
|
|
## Struktur Proyek Lengkap
|
|
|
|
```
|
|
nuxt-multitenant-rbac-complete/
|
|
├── assets/
|
|
│ ├── css/
|
|
│ │ └── main.css
|
|
│ └── images/
|
|
│ └── logo.png
|
|
├── components/
|
|
│ ├── management/
|
|
│ ├── common/
|
|
│ │ ├── AppHeader.vue
|
|
│ │ ├── AppSidebar.vue
|
|
│ │ ├── AppLayout.vue
|
|
│ │ ├── AppBreadcrumb.vue
|
|
│ │ └── AppSearch.vue
|
|
│ ├── user/
|
|
│ │ ├── UserCard.vue
|
|
│ │ ├── UserForm.vue
|
|
│ │ ├── UserList.vue
|
|
│ │ └── UserStats.vue
|
|
│ ├── role/
|
|
│ │ ├── RoleCard.vue
|
|
│ │ ├── RoleForm.vue
|
|
│ │ ├── RoleList.vue
|
|
│ │ └── PermissionMatrix.vue
|
|
│ └── menu/
|
|
│ ├── MenuBuilder.vue
|
|
│ ├── MenuForm.vue
|
|
│ ├── MenuList.vue
|
|
│ ├── MenuTree.vue
|
|
│ └── IconSelector.vue
|
|
├── composables/
|
|
│ ├── management/
|
|
│ ├── useAuth.ts
|
|
│ ├── useTenant.ts
|
|
│ ├── useUsers.ts
|
|
│ ├── useRoles.ts
|
|
│ ├── useMenus.ts
|
|
│ ├── usePermissions.ts
|
|
│ └── useNotifications.ts
|
|
├── data/
|
|
│ ├── management/
|
|
│ ├── users.json
|
|
│ ├── roles.json
|
|
│ ├── menus.json
|
|
│ ├── tenants.json
|
|
│ └── permissions.json
|
|
├── middleware/
|
|
│ ├── auth.ts
|
|
│ ├── rbac.ts
|
|
│ ├── tenant.ts
|
|
│ └── admin.ts
|
|
├── pages/
|
|
│ ├── index.vue
|
|
│ ├── login.vue
|
|
│ ├── dashboard.vue
|
|
│ ├── management/
|
|
│ ├── users/
|
|
│ │ ├── index.vue
|
|
│ │ ├── create.vue
|
|
│ │ └── [id]/
|
|
│ │ ├── edit.vue
|
|
│ │ └── view.vue
|
|
│ ├── roles/
|
|
│ │ ├── index.vue
|
|
│ │ ├── create.vue
|
|
│ │ └── [id]/
|
|
│ │ ├── edit.vue
|
|
│ │ └── view.vue
|
|
│ └── menu-builder/
|
|
│ ├── index.vue
|
|
│ ├── create.vue
|
|
│ └── [id]/
|
|
│ ├── edit.vue
|
|
│ └── view.vue
|
|
├── plugins/
|
|
│ ├── axios.ts
|
|
│ └── auth.client.ts
|
|
├── server/
|
|
│ ├── api/
|
|
│ │ ├── auth/
|
|
│ │ │ ├── login.post.ts
|
|
│ │ │ ├── logout.post.ts
|
|
│ │ │ ├── refresh.post.ts
|
|
│ │ │ └── me.get.ts
|
|
│ │ ├── users/
|
|
│ │ │ ├── index.get.ts
|
|
│ │ │ ├── index.post.ts
|
|
│ │ │ ├── [id].get.ts
|
|
│ │ │ ├── [id].put.ts
|
|
│ │ │ ├── [id].delete.ts
|
|
│ │ │ └── stats.get.ts
|
|
│ │ ├── roles/
|
|
│ │ │ ├── index.get.ts
|
|
│ │ │ ├── index.post.ts
|
|
│ │ │ ├── [id].get.ts
|
|
│ │ │ ├── [id].put.ts
|
|
│ │ │ ├── [id].delete.ts
|
|
│ │ │ └── permissions.get.ts
|
|
│ │ ├── menus/
|
|
│ │ │ ├── index.get.ts
|
|
│ │ │ ├── index.post.ts
|
|
│ │ │ ├── [id].get.ts
|
|
│ │ │ ├── [id].put.ts
|
|
│ │ │ ├── [id].delete.ts
|
|
│ │ │ ├── tree.get.ts
|
|
│ │ │ └── reorder.post.ts
|
|
│ │ └── dashboard/
|
|
│ │ └── stats.get.ts
|
|
│ └── middleware/
|
|
│ ├── auth.ts
|
|
│ ├── cors.ts
|
|
│ └── tenant.ts
|
|
├── stores/
|
|
│ ├── management/
|
|
│ ├── auth.ts
|
|
│ ├── user.ts
|
|
│ ├── role.ts
|
|
│ ├── menu.ts
|
|
│ └── notification.ts
|
|
├── types/
|
|
│ ├── management/
|
|
│ ├── auth.ts
|
|
│ ├── user.ts
|
|
│ ├── role.ts
|
|
│ ├── menu.ts
|
|
│ ├── tenant.ts
|
|
│ ├── permission.ts
|
|
│ └── api.ts
|
|
├── utils/
|
|
│ ├── management/
|
|
│ ├── permissions.ts
|
|
│ ├── storage.ts
|
|
│ ├── validation.ts
|
|
│ ├── helpers.ts
|
|
│ └── constants.ts
|
|
```
|
|
|
|
## 2. Konfigurasi Types Lengkap
|
|
|
|
### types/api.ts
|
|
```typescript
|
|
export interface ApiResponse<T = any> {
|
|
data: T
|
|
message?: string
|
|
success: boolean
|
|
errors?: string[]
|
|
}
|
|
|
|
export interface PaginatedResponse<T = any> {
|
|
data: T[]
|
|
total: number
|
|
page: number
|
|
limit: number
|
|
totalPages: number
|
|
}
|
|
|
|
export interface QueryParams {
|
|
page?: number
|
|
limit?: number
|
|
search?: string
|
|
sort?: string
|
|
order?: 'asc' | 'desc'
|
|
[key: string]: any
|
|
}
|
|
|
|
export interface ApiError {
|
|
statusCode: number
|
|
statusMessage: string
|
|
data?: any
|
|
}
|
|
```
|
|
|
|
### types/permission.ts
|
|
```typescript
|
|
export interface Permission {
|
|
id: string
|
|
resource: string
|
|
action: string
|
|
description?: string
|
|
category: string
|
|
}
|
|
|
|
export interface PermissionCategory {
|
|
id: string
|
|
name: string
|
|
description?: string
|
|
permissions: Permission[]
|
|
}
|
|
|
|
export const RESOURCES = {
|
|
USERS: 'users',
|
|
ROLES: 'roles',
|
|
MENUS: 'menus',
|
|
DASHBOARD: 'dashboard',
|
|
SETTINGS: 'settings'
|
|
} as const
|
|
|
|
export const ACTIONS = {
|
|
CREATE: 'create',
|
|
READ: 'read',
|
|
UPDATE: 'update',
|
|
DELETE: 'delete',
|
|
EXPORT: 'export',
|
|
IMPORT: 'import'
|
|
} as const
|
|
|
|
export type ResourceType = typeof RESOURCES[keyof typeof RESOURCES]
|
|
export type ActionType = typeof ACTIONS[keyof typeof ACTIONS]
|
|
```
|
|
|
|
## 3. Data Storage Lengkap
|
|
|
|
### data/permissions.json
|
|
```json
|
|
[
|
|
{
|
|
"id": "perm-1",
|
|
"resource": "users",
|
|
"action": "create",
|
|
"description": "Create new users",
|
|
"category": "User Management"
|
|
},
|
|
{
|
|
"id": "perm-2",
|
|
"resource": "users",
|
|
"action": "read",
|
|
"description": "View users",
|
|
"category": "User Management"
|
|
},
|
|
{
|
|
"id": "perm-3",
|
|
"resource": "users",
|
|
"action": "update",
|
|
"description": "Edit users",
|
|
"category": "User Management"
|
|
},
|
|
{
|
|
"id": "perm-4",
|
|
"resource": "users",
|
|
"action": "delete",
|
|
"description": "Delete users",
|
|
"category": "User Management"
|
|
},
|
|
{
|
|
"id": "perm-5",
|
|
"resource": "roles",
|
|
"action": "create",
|
|
"description": "Create new roles",
|
|
"category": "Role Management"
|
|
},
|
|
{
|
|
"id": "perm-6",
|
|
"resource": "roles",
|
|
"action": "read",
|
|
"description": "View roles",
|
|
"category": "Role Management"
|
|
},
|
|
{
|
|
"id": "perm-7",
|
|
"resource": "roles",
|
|
"action": "update",
|
|
"description": "Edit roles",
|
|
"category": "Role Management"
|
|
},
|
|
{
|
|
"id": "perm-8",
|
|
"resource": "roles",
|
|
"action": "delete",
|
|
"description": "Delete roles",
|
|
"category": "Role Management"
|
|
},
|
|
{
|
|
"id": "perm-9",
|
|
"resource": "menus",
|
|
"action": "create",
|
|
"description": "Create menu items",
|
|
"category": "Menu Builder"
|
|
},
|
|
{
|
|
"id": "perm-10",
|
|
"resource": "menus",
|
|
"action": "read",
|
|
"description": "View menu items",
|
|
"category": "Menu Builder"
|
|
},
|
|
{
|
|
"id": "perm-11",
|
|
"resource": "menus",
|
|
"action": "update",
|
|
"description": "Edit menu items",
|
|
"category": "Menu Builder"
|
|
},
|
|
{
|
|
"id": "perm-12",
|
|
"resource": "menus",
|
|
"action": "delete",
|
|
"description": "Delete menu items",
|
|
"category": "Menu Builder"
|
|
},
|
|
{
|
|
"id": "perm-13",
|
|
"resource": "dashboard",
|
|
"action": "read",
|
|
"description": "View dashboard",
|
|
"category": "Dashboard"
|
|
}
|
|
]
|
|
```
|
|
|
|
### data/users.json (Updated)
|
|
```json
|
|
[
|
|
{
|
|
"id": "user-1",
|
|
"name": "Super Admin",
|
|
"email": "admin@example.com",
|
|
"password": "$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi",
|
|
"roles": ["super-admin"],
|
|
"tenantId": "tenant-1",
|
|
"mobileNo": "+1234567890",
|
|
"isLoginEnabled": true,
|
|
"avatar": "https://ui-avatars.com/api/?name=Super+Admin&background=1976d2&color=fff",
|
|
"lastLogin": "2025-07-27T08:00:00Z",
|
|
"createdAt": "2025-01-01T00:00:00Z",
|
|
"updatedAt": "2025-07-27T08:00:00Z"
|
|
},
|
|
{
|
|
"id": "user-2",
|
|
"name": "John Doe",
|
|
"email": "john@example.com",
|
|
"password": "$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi",
|
|
"roles": ["admin"],
|
|
"tenantId": "tenant-1",
|
|
"mobileNo": "+1234567891",
|
|
"isLoginEnabled": true,
|
|
"avatar": "https://ui-avatars.com/api/?name=John+Doe&background=4caf50&color=fff",
|
|
"lastLogin": "2025-07-26T15:30:00Z",
|
|
"createdAt": "2025-01-01T00:00:00Z",
|
|
"updatedAt": "2025-07-26T15:30:00Z"
|
|
},
|
|
{
|
|
"id": "user-3",
|
|
"name": "Jane Smith",
|
|
"email": "jane@example.com",
|
|
"password": "$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi",
|
|
"roles": ["staff"],
|
|
"tenantId": "tenant-1",
|
|
"mobileNo": "+1234567892",
|
|
"isLoginEnabled": true,
|
|
"avatar": "https://ui-avatars.com/api/?name=Jane+Smith&background=ff9800&color=fff",
|
|
"lastLogin": "2025-07-25T10:15:00Z",
|
|
"createdAt": "2025-01-02T00:00:00Z",
|
|
"updatedAt": "2025-07-25T10:15:00Z"
|
|
}
|
|
]
|
|
```
|
|
|
|
### data/roles.json (Updated)
|
|
```json
|
|
[
|
|
{
|
|
"id": "super-admin",
|
|
"name": "Super Admin",
|
|
"description": "Full system access with all permissions",
|
|
"permissions": [
|
|
{
|
|
"resource": "users",
|
|
"actions": ["create", "read", "update", "delete", "export", "import"]
|
|
},
|
|
{
|
|
"resource": "roles",
|
|
"actions": ["create", "read", "update", "delete"]
|
|
},
|
|
{
|
|
"resource": "menus",
|
|
"actions": ["create", "read", "update", "delete"]
|
|
},
|
|
{
|
|
"resource": "dashboard",
|
|
"actions": ["read"]
|
|
},
|
|
{
|
|
"resource": "settings",
|
|
"actions": ["create", "read", "update", "delete"]
|
|
}
|
|
],
|
|
"tenantId": "tenant-1",
|
|
"type": "system",
|
|
"isDefault": true,
|
|
"userCount": 1,
|
|
"createdAt": "2025-01-01T00:00:00Z",
|
|
"updatedAt": "2025-01-01T00:00:00Z"
|
|
},
|
|
{
|
|
"id": "admin",
|
|
"name": "Administrator",
|
|
"description": "Administrative access with most permissions",
|
|
"permissions": [
|
|
{
|
|
"resource": "users",
|
|
"actions": ["create", "read", "update", "delete"]
|
|
},
|
|
{
|
|
"resource": "roles",
|
|
"actions": ["read", "update"]
|
|
},
|
|
{
|
|
"resource": "menus",
|
|
"actions": ["create", "read", "update", "delete"]
|
|
},
|
|
{
|
|
"resource": "dashboard",
|
|
"actions": ["read"]
|
|
}
|
|
],
|
|
"tenantId": "tenant-1",
|
|
"type": "custom",
|
|
"isDefault": false,
|
|
"userCount": 1,
|
|
"createdAt": "2025-01-01T00:00:00Z",
|
|
"updatedAt": "2025-01-01T00:00:00Z"
|
|
},
|
|
{
|
|
"id": "staff",
|
|
"name": "Staff",
|
|
"description": "Basic staff access with limited permissions",
|
|
"permissions": [
|
|
{
|
|
"resource": "users",
|
|
"actions": ["read"]
|
|
},
|
|
{
|
|
"resource": "dashboard",
|
|
"actions": ["read"]
|
|
}
|
|
],
|
|
"tenantId": "tenant-1",
|
|
"type": "custom",
|
|
"isDefault": false,
|
|
"userCount": 1,
|
|
"createdAt": "2025-01-01T00:00:00Z",
|
|
"updatedAt": "2025-01-01T00:00:00Z"
|
|
}
|
|
]
|
|
```
|
|
|
|
## 4. Layout dan App.vue Lengkap
|
|
|
|
### app.vue
|
|
```vue
|
|
<template>
|
|
<VApp>
|
|
<VLocaleProvider locale="id">
|
|
<NuxtLayout>
|
|
<NuxtPage />
|
|
</NuxtLayout>
|
|
</VLocaleProvider>
|
|
|
|
<!-- Global Loading Overlay -->
|
|
<VOverlay
|
|
v-model="isGlobalLoading"
|
|
class="align-center justify-center"
|
|
persistent
|
|
>
|
|
<VProgressCircular
|
|
color="primary"
|
|
indeterminate
|
|
size="64"
|
|
/>
|
|
</VOverlay>
|
|
|
|
<!-- Global Notifications -->
|
|
<VSnackbar
|
|
v-for="notification in notifications"
|
|
:key="notification.id"
|
|
v-model="notification.show"
|
|
:color="notification.type"
|
|
:timeout="notification.timeout"
|
|
location="top right"
|
|
class="notification-snackbar"
|
|
>
|
|
{{ notification.message }}
|
|
<template #actions>
|
|
<VBtn
|
|
color="white"
|
|
variant="text"
|
|
@click="removeNotification(notification.id)"
|
|
>
|
|
<VIcon>mdi-close</VIcon>
|
|
</VBtn>
|
|
</template>
|
|
</VSnackbar>
|
|
</VApp>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { useNotificationStore } from '~/stores/notification'
|
|
|
|
// Meta configuration
|
|
useHead({
|
|
title: 'Multi-Tenant RBAC System',
|
|
meta: [
|
|
{ name: 'description', content: 'Complete Multi-Tenant Role-Based Access Control System with Menu Builder' },
|
|
{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
|
|
],
|
|
link: [
|
|
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
|
|
]
|
|
})
|
|
|
|
// Global loading state
|
|
const isGlobalLoading = ref(false)
|
|
|
|
// Notifications
|
|
const notificationStore = useNotificationStore()
|
|
const { notifications, removeNotification } = storeToRefs(notificationStore)
|
|
|
|
// Global error handler
|
|
const handleGlobalError = (error: any) => {
|
|
console.error('Global error:', error)
|
|
notificationStore.addNotification({
|
|
type: 'error',
|
|
message: error.message || 'An unexpected error occurred',
|
|
timeout: 5000
|
|
})
|
|
}
|
|
|
|
// Setup global error handling
|
|
onMounted(() => {
|
|
window.addEventListener('unhandledrejection', (event) => {
|
|
handleGlobalError(event.reason)
|
|
})
|
|
|
|
window.addEventListener('error', (event) => {
|
|
handleGlobalError(event.error)
|
|
})
|
|
})
|
|
</script>
|
|
|
|
<style>
|
|
.notification-snackbar {
|
|
margin-top: 64px;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.notification-snackbar {
|
|
margin-top: 56px;
|
|
}
|
|
}
|
|
</style>
|
|
```
|
|
|
|
### layouts/default.vue
|
|
```vue
|
|
<template>
|
|
<div>
|
|
<!-- App Bar -->
|
|
<AppHeader @toggle-sidebar="toggleSidebar" />
|
|
|
|
<!-- Sidebar -->
|
|
<AppSidebar v-model="sidebarOpen" />
|
|
|
|
<!-- Main Content -->
|
|
<VMain>
|
|
<VContainer fluid class="pa-4">
|
|
<!-- Breadcrumb -->
|
|
<AppBreadcrumb class="mb-4" />
|
|
|
|
<!-- Page Content -->
|
|
<div class="page-content">
|
|
<slot />
|
|
</div>
|
|
</VContainer>
|
|
</VMain>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
const sidebarOpen = ref(true)
|
|
|
|
const toggleSidebar = () => {
|
|
sidebarOpen.value = !sidebarOpen.value
|
|
}
|
|
|
|
// Auto-hide sidebar on mobile
|
|
const { width } = useWindowSize()
|
|
watch(width, (newWidth) => {
|
|
if (newWidth < 768) {
|
|
sidebarOpen.value = false
|
|
} else {
|
|
sidebarOpen.value = true
|
|
}
|
|
}, { immediate: true })
|
|
</script>
|
|
<style scoped>
|
|
.page-content {
|
|
min-height: calc(100vh - 128px);
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.page-content {
|
|
min-height: calc(100vh - 120px);
|
|
}
|
|
}
|
|
</style>
|
|
```
|
|
|
|
### layouts/auth.vue
|
|
```vue
|
|
<template>
|
|
<VContainer class="fill-height auth-container" fluid>
|
|
<VRow justify="center" align="center" class="fill-height">
|
|
<VCol cols="12" sm="8" md="6" lg="4" xl="3">
|
|
<div class="auth-card-wrapper">
|
|
<!-- Logo/Brand -->
|
|
<div class="text-center mb-8">
|
|
<VImg
|
|
src="/logo.png"
|
|
alt="Logo"
|
|
height="60"
|
|
contain
|
|
class="mx-auto mb-4"
|
|
/>
|
|
<h1 class="text-h4 font-weight-bold text-primary">
|
|
{{ $config.public.appName }}
|
|
</h1>
|
|
<p class="text-subtitle-1 text-grey-600">
|
|
Multi-Tenant RBAC System
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Auth Content -->
|
|
<slot />
|
|
</div>
|
|
</VCol>
|
|
</VRow>
|
|
|
|
<!-- Background Pattern -->
|
|
<div class="auth-background" />
|
|
</VContainer>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.auth-container {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
min-height: 100vh;
|
|
position: relative;
|
|
}
|
|
|
|
.auth-card-wrapper {
|
|
position: relative;
|
|
z-index: 2;
|
|
}
|
|
|
|
.auth-background {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-image: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.1'%3E%3Ccircle cx='30' cy='30' r='2'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
|
|
opacity: 0.5;
|
|
}
|
|
</style>
|
|
```
|
|
|
|
|
|
## 5. Components Utama
|
|
|
|
### components/common/AppHeader.vue
|
|
|
|
```vue
|
|
<template>
|
|
<VAppBar
|
|
app
|
|
clipped-left
|
|
elevation="2"
|
|
color="white"
|
|
class="app-header"
|
|
>
|
|
<!-- Menu Toggle Button -->
|
|
<VAppBarNavIcon @click="$emit('toggleSidebar')" />
|
|
|
|
<!-- App Title -->
|
|
<VToolbarTitle class="text-h6 font-weight-bold">
|
|
{{ currentPageTitle }}
|
|
</VToolbarTitle>
|
|
|
|
<VSpacer />
|
|
|
|
<!-- Search -->
|
|
<AppSearch class="mx-4" style="max-width: 300px;" />
|
|
|
|
<!-- Notifications -->
|
|
<VBtn
|
|
icon
|
|
@click="toggleNotifications"
|
|
>
|
|
<VBadge
|
|
:content="unreadNotifications"
|
|
:model-value="unreadNotifications > 0"
|
|
color="error"
|
|
>
|
|
<VIcon>mdi-bell</VIcon>
|
|
</VBadge>
|
|
</VBtn>
|
|
|
|
<!-- User Menu -->
|
|
<VMenu offset-y>
|
|
<template #activator="{ props }">
|
|
<VBtn
|
|
v-bind="props"
|
|
icon
|
|
class="ml-2"
|
|
>
|
|
<VAvatar size="36">
|
|
<VImg
|
|
:src="user?.avatar || defaultAvatar"
|
|
:alt="user?.name || 'User'"
|
|
/>
|
|
</VAvatar>
|
|
</VBtn>
|
|
</template>
|
|
|
|
<VList min-width="200">
|
|
<VListItem>
|
|
<VListItemTitle class="font-weight-bold">
|
|
{{ user?.name }}
|
|
</VListItemTitle>
|
|
<VListItemSubtitle>
|
|
{{ user?.email }}
|
|
</VListItemSubtitle>
|
|
</VListItem>
|
|
|
|
<VDivider />
|
|
|
|
<VListItem @click="goToProfile">
|
|
<template #prepend>
|
|
<VIcon>mdi-account</VIcon>
|
|
</template>
|
|
<VListItemTitle>Profile</VListItemTitle>
|
|
</VListItem>
|
|
|
|
<VListItem @click="goToSettings">
|
|
<template #prepend>
|
|
<VIcon>mdi-cog</VIcon>
|
|
</template>
|
|
<VListItemTitle>Settings</VListItemTitle>
|
|
</VListItem>
|
|
|
|
<VDivider />
|
|
|
|
<VListItem @click="handleLogout">
|
|
<template #prepend>
|
|
<VIcon>mdi-logout</VIcon>
|
|
</template>
|
|
<VListItemTitle>Logout</VListItemTitle>
|
|
</VListItem>
|
|
</VList>
|
|
</VMenu>
|
|
</VAppBar>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { useAuthStore } from '~/stores/auth'
|
|
|
|
defineEmits<{
|
|
toggleSidebar: []
|
|
}>()
|
|
|
|
const route = useRoute()
|
|
const authStore = useAuthStore()
|
|
const { user } = storeToRefs(authStore)
|
|
|
|
const unreadNotifications = ref(3)
|
|
const defaultAvatar = 'https://ui-avatars.com/api/?name=User&background=1976d2&color=fff'
|
|
|
|
const currentPageTitle = computed(() => {
|
|
const titles: Record<string, string> = {
|
|
'/dashboard': 'Dashboard',
|
|
'/users': 'User Management',
|
|
'/roles': 'Role Management',
|
|
'/menu-builder': 'Menu Builder'
|
|
}
|
|
|
|
const path = route.path
|
|
return titles[path] || 'Dashboard'
|
|
})
|
|
|
|
const toggleNotifications = () => {
|
|
// Implement notifications toggle
|
|
console.log('Toggle notifications')
|
|
}
|
|
|
|
const goToProfile = () => {
|
|
navigateTo('/profile')
|
|
}
|
|
|
|
const goToSettings = () => {
|
|
navigateTo('/settings')
|
|
}
|
|
|
|
const handleLogout = async () => {
|
|
await authStore.logout()
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.app-header {
|
|
border-bottom: 1px solid #e0e0e0;
|
|
}
|
|
</style>
|
|
```
|
|
|
|
|
|
### components/common/AppSidebar.vue
|
|
|
|
```vue
|
|
<template>
|
|
<VNavigationDrawer
|
|
v-model="sidebarOpen"
|
|
app
|
|
clipped
|
|
width="280"
|
|
class="app-sidebar"
|
|
>
|
|
<!-- User Info -->
|
|
<div class="user-info pa-4">
|
|
<div class="d-flex align-center">
|
|
<VAvatar size="48" class="mr-3">
|
|
<VImg
|
|
:src="user?.avatar || defaultAvatar"
|
|
:alt="user?.name || 'User'"
|
|
/>
|
|
</VAvatar>
|
|
<div>
|
|
<div class="text-subtitle-1 font-weight-bold">
|
|
{{ user?.name }}
|
|
</div>
|
|
<div class="text-caption text-grey-600">
|
|
{{ user?.roles?.[0] || 'User' }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<VDivider />
|
|
|
|
<!-- Navigation Menu -->
|
|
<VList nav class="pa-0">
|
|
<!-- Dashboard -->
|
|
<VListItem
|
|
to="/dashboard"
|
|
prepend-icon="mdi-view-dashboard"
|
|
title="Dashboard"
|
|
value="dashboard"
|
|
/>
|
|
|
|
<!-- Dynamic Menu Items -->
|
|
<template v-for="menuItem in menuTree" :key="menuItem.id">
|
|
<!-- Main Menu with Submenu -->
|
|
<VListGroup
|
|
v-if="menuItem.children && menuItem.children.length > 0"
|
|
:value="menuItem.id"
|
|
>
|
|
<template #activator="{ props }">
|
|
<VListItem
|
|
v-bind="props"
|
|
:prepend-icon="menuItem.icon"
|
|
:title="menuItem.name"
|
|
/>
|
|
</template>
|
|
|
|
<!-- Submenu Items -->
|
|
<VListItem
|
|
v-for="subItem in menuItem.children"
|
|
:key="subItem.id"
|
|
:to="getMenuLink(subItem)"
|
|
:target="getMenuTarget(subItem)"
|
|
:title="subItem.name"
|
|
class="pl-8"
|
|
:value="subItem.id"
|
|
/>
|
|
</VListGroup>
|
|
|
|
<!-- Main Menu without Submenu -->
|
|
<VListItem
|
|
v-else
|
|
:to="getMenuLink(menuItem)"
|
|
:target="getMenuTarget(menuItem)"
|
|
:prepend-icon="menuItem.icon"
|
|
:title="menuItem.name"
|
|
:value="menuItem.id"
|
|
/>
|
|
</template>
|
|
|
|
<!-- Static Admin Menu (if has permission) -->
|
|
<template v-if="hasAnyAdminPermission">
|
|
<VDivider class="my-2" />
|
|
|
|
<VListGroup value="admin">
|
|
<template #activator="{ props }">
|
|
<VListItem
|
|
v-bind="props"
|
|
prepend-icon="mdi-cog"
|
|
title="Administration"
|
|
/>
|
|
</template>
|
|
|
|
<VListItem
|
|
v-if="hasPermission('users', 'read')"
|
|
to="/users"
|
|
prepend-icon="mdi-account-multiple"
|
|
title="Users"
|
|
value="users"
|
|
class="pl-8"
|
|
/>
|
|
|
|
<VListItem
|
|
v-if="hasPermission('roles', 'read')"
|
|
to="/roles"
|
|
prepend-icon="mdi-shield-account"
|
|
title="Roles"
|
|
value="roles"
|
|
class="pl-8"
|
|
/>
|
|
|
|
<VListItem
|
|
v-if="hasPermission('menus', 'read')"
|
|
to="/menu-builder"
|
|
prepend-icon="mdi-menu"
|
|
title="Menu Builder"
|
|
value="menu-builder"
|
|
class="pl-8"
|
|
/>
|
|
</VListGroup>
|
|
</template>
|
|
</VList>
|
|
|
|
<!-- Footer -->
|
|
<template #append>
|
|
<div class="pa-4 text-center">
|
|
<div class="text-caption text-grey-500">
|
|
{{ $config.public.appName }}
|
|
</div>
|
|
<div class="text-caption text-grey-400">
|
|
v{{ $config.public.appVersion }}
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</VNavigationDrawer>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { MenuTreeItem } from '~/types/menu'
|
|
import { useAuthStore } from '~/stores/auth'
|
|
import { useMenuStore } from '~/stores/menu'
|
|
|
|
const sidebarOpen = defineModel<boolean>({ default: true })
|
|
|
|
const authStore = useAuthStore()
|
|
const menuStore = useMenuStore()
|
|
const { user, hasPermission } = storeToRefs(authStore)
|
|
const { menuTree } = storeToRefs(menuStore)
|
|
|
|
const defaultAvatar = 'https://ui-avatars.com/api/?name=User&background=1976d2&color=fff'
|
|
|
|
const hasAnyAdminPermission = computed(() => {
|
|
return hasPermission.value('users', 'read') ||
|
|
hasPermission.value('roles', 'read') ||
|
|
hasPermission.value('menus', 'read')
|
|
})
|
|
|
|
const getMenuLink = (menuItem: MenuTreeItem) => {
|
|
if (menuItem.linkType === 'hash') {
|
|
return menuItem.link
|
|
} else if (menuItem.linkType === 'outsite') {
|
|
return menuItem.link
|
|
} else {
|
|
return menuItem.link
|
|
}
|
|
}
|
|
|
|
const getMenuTarget = (menuItem: MenuTreeItem) => {
|
|
if (menuItem.linkType === 'outsite') {
|
|
return '_blank'
|
|
} else if (menuItem.target === 'isolated') {
|
|
return '_blank'
|
|
}
|
|
return '_self'
|
|
}
|
|
|
|
onMounted(async () => {
|
|
await menuStore.fetchMenus()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.app-sidebar {
|
|
border-right: 1px solid #e0e0e0;
|
|
}
|
|
|
|
.user-info {
|
|
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
|
}
|
|
|
|
.v-list-item--active {
|
|
background-color: rgba(25, 118, 210, 0.08);
|
|
color: #1976d2;
|
|
}
|
|
|
|
.v-list-item--active .v-icon {
|
|
color: #1976d2;
|
|
}
|
|
</style>
|
|
```
|
|
|
|
|
|
## 6. Pages Lengkap
|
|
|
|
### pages/index.vue
|
|
|
|
```vue
|
|
<template>
|
|
<div>
|
|
<!-- Redirect to dashboard if authenticated, otherwise to login -->
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { useAuthStore } from '~/stores/auth'
|
|
|
|
const authStore = useAuthStore()
|
|
|
|
onMounted(async () => {
|
|
await authStore.checkAuth()
|
|
|
|
if (authStore.isAuthenticated) {
|
|
await navigateTo('/dashboard')
|
|
} else {
|
|
await navigateTo('/login')
|
|
}
|
|
})
|
|
</script>
|
|
```
|
|
|
|
|
|
### pages/login.vue
|
|
|
|
```vue
|
|
<template>
|
|
<VCard elevation="8" class="login-card">
|
|
<VCardTitle class="text-center pa-6">
|
|
<h2 class="text-h4 font-weight-bold">Welcome Back</h2>
|
|
<p class="text-subtitle-1 text-grey-600 mt-2">
|
|
Sign in to your account
|
|
</p>
|
|
</VCardTitle>
|
|
|
|
<VCardText class="pa-6">
|
|
<VForm @submit.prevent="handleLogin">
|
|
<VTextField
|
|
v-model="credentials.email"
|
|
label="Email Address"
|
|
type="email"
|
|
variant="outlined"
|
|
prepend-inner-icon="mdi-email"
|
|
:rules="emailRules"
|
|
:error-messages="errors.email"
|
|
class="mb-4"
|
|
required
|
|
/>
|
|
|
|
<VTextField
|
|
v-model="credentials.password"
|
|
label="Password"
|
|
:type="showPassword ? 'text' : 'password'"
|
|
variant="outlined"
|
|
prepend-inner-icon="mdi-lock"
|
|
:append-inner-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
|
@click:append-inner="showPassword = !showPassword"
|
|
:rules="passwordRules"
|
|
:error-messages="errors.password"
|
|
class="mb-4"
|
|
required
|
|
/>
|
|
|
|
<VCheckbox
|
|
v-model="credentials.rememberMe"
|
|
label="Remember me"
|
|
class="mb-4"
|
|
/>
|
|
|
|
<VBtn
|
|
type="submit"
|
|
color="primary"
|
|
block
|
|
size="large"
|
|
:loading="loading"
|
|
class="mb-4"
|
|
>
|
|
Sign In
|
|
</VBtn>
|
|
|
|
<div class="text-center">
|
|
<VBtn
|
|
variant="text"
|
|
color="primary"
|
|
@click="showForgotPassword = true"
|
|
>
|
|
Forgot Password?
|
|
</VBtn>
|
|
</div>
|
|
</VForm>
|
|
</VCardText>
|
|
|
|
<!-- Demo Credentials -->
|
|
<VCardText class="pa-6 pt-0">
|
|
<VAlert
|
|
type="info"
|
|
variant="tonal"
|
|
class="mb-4"
|
|
>
|
|
<strong>Demo Credentials:</strong><br>
|
|
<strong>Super Admin:</strong> admin@example.com / password<br>
|
|
<strong>Admin:</strong> john@example.com / password<br>
|
|
<strong>Staff:</strong> jane@example.com / password
|
|
</VAlert>
|
|
</VCardText>
|
|
</VCard>
|
|
|
|
<!-- Forgot Password Dialog -->
|
|
<VDialog v-model="showForgotPassword" max-width="400">
|
|
<VCard>
|
|
<VCardTitle>Reset Password</VCardTitle>
|
|
<VCardText>
|
|
<VTextField
|
|
v-model="forgotEmail"
|
|
label="Email Address"
|
|
type="email"
|
|
variant="outlined"
|
|
:rules="emailRules"
|
|
/>
|
|
</VCardText>
|
|
<VCardActions>
|
|
<VSpacer />
|
|
<VBtn @click="showForgotPassword = false">Cancel</VBtn>
|
|
<VBtn color="primary" @click="handleForgotPassword">
|
|
Send Reset Link
|
|
</VBtn>
|
|
</VCardActions>
|
|
</VCard>
|
|
</VDialog>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { useAuthStore } from '~/stores/auth'
|
|
import { useNotificationStore } from '~/stores/notification'
|
|
|
|
definePageMeta({
|
|
layout: 'auth'
|
|
})
|
|
|
|
const authStore = useAuthStore()
|
|
const notificationStore = useNotificationStore()
|
|
|
|
const loading = ref(false)
|
|
const showPassword = ref(false)
|
|
const showForgotPassword = ref(false)
|
|
const forgotEmail = ref('')
|
|
|
|
const credentials = reactive({
|
|
email: 'admin@example.com',
|
|
password: 'password',
|
|
rememberMe: false
|
|
})
|
|
|
|
const errors = reactive({
|
|
email: [],
|
|
password: []
|
|
})
|
|
|
|
const emailRules = [
|
|
(v: string) => !!v || 'Email is required',
|
|
(v: string) => /.+@.+\..+/.test(v) || 'Email must be valid'
|
|
]
|
|
|
|
const passwordRules = [
|
|
(v: string) => !!v || 'Password is required',
|
|
(v: string) => v.length >= 6 || 'Password must be at least 6 characters'
|
|
]
|
|
|
|
const handleLogin = async () => {
|
|
// Clear previous errors
|
|
errors.email = []
|
|
errors.password = []
|
|
|
|
loading.value = true
|
|
try {
|
|
await authStore.login(credentials)
|
|
|
|
notificationStore.addNotification({
|
|
type: 'success',
|
|
message: 'Login successful! Welcome back.',
|
|
timeout: 3000
|
|
})
|
|
|
|
await navigateTo('/dashboard')
|
|
} catch (error: any) {
|
|
console.error('Login error:', error)
|
|
|
|
if (error.statusCode === 401) {
|
|
errors.email = ['Invalid email or password']
|
|
errors.password = ['Invalid email or password']
|
|
} else if (error.statusCode === 422) {
|
|
if (error.data?.errors) {
|
|
Object.assign(errors, error.data.errors)
|
|
}
|
|
} else {
|
|
notificationStore.addNotification({
|
|
type: 'error',
|
|
message: error.message || 'Login failed. Please try again.',
|
|
timeout: 5000
|
|
})
|
|
}
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const handleForgotPassword = () => {
|
|
// Implement forgot password logic
|
|
notificationStore.addNotification({
|
|
type: 'info',
|
|
message: 'Password reset link has been sent to your email.',
|
|
timeout: 5000
|
|
})
|
|
showForgotPassword.value = false
|
|
forgotEmail.value = ''
|
|
}
|
|
|
|
// Auto-redirect if already authenticated
|
|
onMounted(async () => {
|
|
await authStore.checkAuth()
|
|
if (authStore.isAuthenticated) {
|
|
await navigateTo('/dashboard')
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.login-card {
|
|
max-width: 450px;
|
|
width: 100%;
|
|
backdrop-filter: blur(10px);
|
|
background: rgba(255, 255, 255, 0.95);
|
|
}
|
|
</style>
|
|
```
|
|
|
|
|
|
### pages/dashboard.vue
|
|
|
|
```vue
|
|
<template>
|
|
<div>
|
|
<!-- Welcome Section -->
|
|
<VRow class="mb-6">
|
|
<VCol cols="12">
|
|
<VCard class="welcome-card" color="primary" dark>
|
|
<VCardText class="pa-6">
|
|
<VRow align="center">
|
|
<VCol cols="12" md="8">
|
|
<h1 class="text-h4 font-weight-bold mb-2">
|
|
Welcome back, {{ user?.name }}! 👋
|
|
</h1>
|
|
<p class="text-h6 mb-0 opacity-90">
|
|
Here's what's happening in your system today
|
|
</p>
|
|
</VCol>
|
|
<VCol cols="12" md="4" class="text-right">
|
|
<div class="text-h6">
|
|
{{ currentDate }}
|
|
</div>
|
|
<div class="text-subtitle-1 opacity-90">
|
|
{{ currentTime }}
|
|
</div>
|
|
</VCol>
|
|
</VRow>
|
|
</VCardText>
|
|
</VCard>
|
|
</VCol>
|
|
</VRow>
|
|
|
|
<!-- Stats Cards -->
|
|
<VRow class="mb-6">
|
|
<VCol
|
|
v-for="stat in stats"
|
|
:key="stat.title"
|
|
cols="12"
|
|
sm="6"
|
|
md="3"
|
|
>
|
|
<VCard class="stats-card">
|
|
<VCardText class="pa-4">
|
|
<VRow align="center" no-gutters>
|
|
<VCol>
|
|
<div class="text-h4 font-weight-bold" :class="stat.color">
|
|
{{ stat.value }}
|
|
</div>
|
|
<div class="text-subtitle-1 text-grey-600">
|
|
{{ stat.title }}
|
|
</div>
|
|
</VCol>
|
|
<VCol cols="auto">
|
|
<VAvatar :color="stat.color" size="56">
|
|
<VIcon size="28" color="white">
|
|
{{ stat.icon }}
|
|
</VIcon>
|
|
</VAvatar>
|
|
</VCol>
|
|
</VRow>
|
|
|
|
<div class="mt-3 d-flex align-center">
|
|
<VIcon
|
|
:color="stat.trend.type === 'up' ? 'success' : 'error'"
|
|
size="16"
|
|
class="mr-1"
|
|
>
|
|
{{ stat.trend.type === 'up' ? 'mdi-trending-up' : 'mdi-trending-down' }}
|
|
</VIcon>
|
|
<span
|
|
class="text-caption"
|
|
:class="stat.trend.type === 'up' ? 'text-success' : 'text-error'"
|
|
>
|
|
{{ stat.trend.value }}% from last month
|
|
</span>
|
|
</div>
|
|
</VCardText>
|
|
</VCard>
|
|
</VCol>
|
|
</VRow>
|
|
|
|
<!-- Charts and Recent Activity -->
|
|
<VRow>
|
|
<!-- User Growth Chart -->
|
|
<VCol cols="12" md="8">
|
|
<VCard>
|
|
<VCardTitle>User Growth</VCardTitle>
|
|
<VCardText>
|
|
<div class="chart-container">
|
|
<!-- Placeholder for chart -->
|
|
<div class="chart-placeholder">
|
|
<VIcon size="64" color="grey-lighten-1">
|
|
mdi-chart-line
|
|
</VIcon>
|
|
<div class="text-h6 text-grey-600 mt-4">
|
|
Chart Coming Soon
|
|
</div>
|
|
<div class="text-body-2 text-grey-500">
|
|
User growth analytics will be displayed here
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</VCardText>
|
|
</VCard>
|
|
</VCol>
|
|
|
|
<!-- Recent Activity -->
|
|
<VCol cols="12" md="4">
|
|
<VCard>
|
|
<VCardTitle>Recent Activity</VCardTitle>
|
|
<VCardText class="pa-0">
|
|
<VList lines="two">
|
|
<VListItem
|
|
v-for="(activity, index) in recentActivities"
|
|
:key="index"
|
|
:prepend-avatar="activity.avatar"
|
|
>
|
|
<VListItemTitle>{{ activity.title }}</VListItemTitle>
|
|
<VListItemSubtitle>{{ activity.subtitle }}</VListItemSubtitle>
|
|
<template #append>
|
|
<div class="text-caption text-grey-500">
|
|
{{ activity.time }}
|
|
</div>
|
|
</template>
|
|
</VListItem>
|
|
</VList>
|
|
</VCardText>
|
|
<VCardActions>
|
|
<VBtn variant="text" color="primary" block>
|
|
View All Activities
|
|
</VBtn>
|
|
</VCardActions>
|
|
</VCard>
|
|
</VCol>
|
|
</VRow>
|
|
|
|
<!-- Quick Actions -->
|
|
<VRow class="mt-6">
|
|
<VCol cols="12">
|
|
<VCard>
|
|
<VCardTitle>Quick Actions</VCardTitle>
|
|
<VCardText>
|
|
<VRow>
|
|
<VCol
|
|
v-for="action in quickActions"
|
|
:key="action.title"
|
|
cols="12"
|
|
sm="6"
|
|
md="3"
|
|
>
|
|
<VBtn
|
|
:to="action.route"
|
|
variant="outlined"
|
|
size="large"
|
|
block
|
|
class="py-6"
|
|
:disabled="!action.enabled"
|
|
>
|
|
<div class="text-center">
|
|
<VIcon size="32" class="mb-2">{{ action.icon }}</VIcon>
|
|
<div>{{ action.title }}</div>
|
|
</div>
|
|
</VBtn>
|
|
</VCol>
|
|
</VRow>
|
|
</VCardText>
|
|
</VCard>
|
|
</VCol>
|
|
</VRow>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { useAuthStore } from '~/stores/auth'
|
|
|
|
definePageMeta({
|
|
middleware: ['auth']
|
|
})
|
|
|
|
const authStore = useAuthStore()
|
|
const { user, hasPermission } = storeToRefs(authStore)
|
|
|
|
// Current date and time
|
|
const currentDate = ref('')
|
|
const currentTime = ref('')
|
|
|
|
const updateDateTime = () => {
|
|
const now = new Date()
|
|
currentDate.value = now.toLocaleDateString('id-ID', {
|
|
weekday: 'long',
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
})
|
|
currentTime.value = now.toLocaleTimeString('id-ID', {
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
})
|
|
}
|
|
|
|
// Stats data
|
|
const stats = ref([
|
|
{
|
|
title: 'Total Users',
|
|
value: '1,234',
|
|
icon: 'mdi-account-multiple',
|
|
color: 'text-primary',
|
|
trend: { type: 'up', value: 12 }
|
|
},
|
|
{
|
|
title: 'Active Roles',
|
|
value: '8',
|
|
icon: 'mdi-shield-account',
|
|
color: 'text-success',
|
|
trend: { type: 'up', value: 5 }
|
|
},
|
|
{
|
|
title: 'Menu Items',
|
|
value: '24',
|
|
icon: 'mdi-menu',
|
|
color: 'text-warning',
|
|
trend: { type: 'down', value: 2 }
|
|
},
|
|
{
|
|
title: 'Active Sessions',
|
|
value: '456',
|
|
icon: 'mdi-account-clock',
|
|
color: 'text-info',
|
|
trend: { type: 'up', value: 8 }
|
|
}
|
|
])
|
|
|
|
// Recent activities
|
|
const recentActivities = ref([
|
|
{
|
|
title: 'New user registered',
|
|
subtitle: 'Jane Smith joined the system',
|
|
avatar: 'https://ui-avatars.com/api/?name=Jane+Smith&background=4caf50&color=fff',
|
|
time: '2m ago'
|
|
},
|
|
{
|
|
title: 'Role updated',
|
|
subtitle: 'Admin role permissions modified',
|
|
avatar: 'https://ui-avatars.com/api/?name=Admin&background=ff9800&color=fff',
|
|
time: '15m ago'
|
|
},
|
|
{
|
|
title: 'Menu item added',
|
|
subtitle: 'New menu "Reports" created',
|
|
avatar: 'https://ui-avatars.com/api/?name=Menu&background=2196f3&color=fff',
|
|
time: '1h ago'
|
|
},
|
|
{
|
|
title: 'User logged in',
|
|
subtitle: 'John Doe accessed the system',
|
|
avatar: 'https://ui-avatars.com/api/?name=John+Doe&background=9c27b0&color=fff',
|
|
time: '2h ago'
|
|
}
|
|
])
|
|
|
|
// Quick actions
|
|
const quickActions = computed(() => [
|
|
{
|
|
title: 'Add User',
|
|
icon: 'mdi-account-plus',
|
|
route: '/users/create',
|
|
enabled: hasPermission.value('users', 'create')
|
|
},
|
|
{
|
|
title: 'Create Role',
|
|
icon: 'mdi-shield-plus',
|
|
route: '/roles/create',
|
|
enabled: hasPermission.value('roles', 'create')
|
|
},
|
|
{
|
|
title: 'Build Menu',
|
|
icon: 'mdi-menu-plus',
|
|
route: '/menu-builder/create',
|
|
enabled: hasPermission.value('menus', 'create')
|
|
},
|
|
{
|
|
title: 'View Reports',
|
|
icon: 'mdi-chart-box',
|
|
route: '/reports',
|
|
enabled: true
|
|
}
|
|
])
|
|
|
|
onMounted(() => {
|
|
updateDateTime()
|
|
setInterval(updateDateTime, 1000)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
clearInterval(updateDateTime)
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.welcome-card {
|
|
background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
|
|
}
|
|
|
|
.stats-card {
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.stats-card:hover {
|
|
transform: translateY(-4px);
|
|
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
|
|
}
|
|
|
|
.chart-container {
|
|
height: 300px;
|
|
}
|
|
|
|
.chart-placeholder {
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
</style>
|
|
```
|
|
|
|
|
|
## 7. README.md Lengkap
|
|
|
|
### README.md
|
|
|
|
```markdown
|
|
# Multi-Tenant RBAC System with Menu Builder
|
|
|
|
A complete **Multi-Tenant Role-Based Access Control (RBAC) System** built with **Nuxt 3**, **Vue 3**, **Vuetify**, and **TypeScript**. This system includes comprehensive user management, role management, and a dynamic menu builder with granular permissions.
|
|
|
|
## 🌟 Features
|
|
|
|
### Core Features
|
|
- **Multi-Tenant Architecture**: Complete tenant isolation
|
|
- **Role-Based Access Control (RBAC)**: Granular permissions system
|
|
- **User Management**: Full CRUD operations with advanced filtering
|
|
- **Role Management**: Create and manage roles with custom permissions
|
|
- **Menu Builder**: Dynamic menu creation and management
|
|
- **Authentication & Authorization**: JWT-based secure authentication
|
|
- **Responsive Design**: Mobile-first approach with Vuetify
|
|
- **TypeScript**: Full type safety throughout the application
|
|
|
|
### User Management
|
|
- ✅ User CRUD operations
|
|
- ✅ Role assignment
|
|
- ✅ Profile management with avatars
|
|
- ✅ Search and filtering
|
|
- ✅ Pagination
|
|
- ✅ Account status management
|
|
- ✅ Bulk operations
|
|
|
|
### Role Management
|
|
- ✅ Role CRUD operations
|
|
- ✅ Permission matrix
|
|
- ✅ System and custom roles
|
|
- ✅ Role hierarchy
|
|
- ✅ Permission inheritance
|
|
|
|
### Menu Builder
|
|
- ✅ Dynamic menu creation
|
|
- ✅ Main and sub-menu support
|
|
- ✅ Icon selection with search
|
|
- ✅ Link type configuration (Hash, Site, External)
|
|
- ✅ Window target options
|
|
- ✅ Multi-tenant menu support
|
|
- ✅ Drag & drop ordering
|
|
|
|
### Security Features
|
|
- ✅ JWT authentication
|
|
- ✅ Route protection middleware
|
|
- ✅ Permission-based access control
|
|
- ✅ XSS protection
|
|
- ✅ CSRF protection
|
|
- ✅ Input validation & sanitization
|
|
|
|
## 🚀 Quick Start
|
|
|
|
### Prerequisites
|
|
- Node.js 18+
|
|
- npm or yarn or pnpm
|
|
|
|
### Installation
|
|
|
|
1. **Clone the repository**
|
|
```
|
|
|
|
git clone <repository-url>
|
|
cd nuxt-multitenant-rbac-complete
|
|
|
|
```
|
|
|
|
2. **Install dependencies**
|
|
```
|
|
|
|
npm install
|
|
|
|
# or
|
|
|
|
yarn install
|
|
|
|
# or
|
|
|
|
pnpm install
|
|
|
|
```
|
|
|
|
3. **Environment setup**
|
|
```
|
|
|
|
cp .env.example .env
|
|
|
|
```
|
|
|
|
4. **Start development server**
|
|
```
|
|
|
|
npm run dev
|
|
|
|
# or
|
|
|
|
yarn dev
|
|
|
|
# or
|
|
|
|
pnpm dev
|
|
|
|
```
|
|
|
|
5. **Access the application**
|
|
- Open [http://localhost:3000](http://localhost:3000)
|
|
- Use demo credentials to login
|
|
|
|
## 🔐 Demo Credentials
|
|
|
|
| Role | Email | Password | Permissions |
|
|
|------|-------|----------|-------------|
|
|
| Super Admin | admin@example.com | password | Full Access |
|
|
| Admin | john@example.com | password | Most Features |
|
|
| Staff | jane@example.com | password | Limited Access |
|
|
|
|
## 📁 Project Structure
|
|
|
|
```
|
|
|
|
nuxt-multitenant-rbac-complete/
|
|
├── assets/ \# Static assets
|
|
├── components/ \# Vue components
|
|
│ ├── common/ \# Common components
|
|
│ ├── user/ \# User management components
|
|
│ ├── role/ \# Role management components
|
|
│ └── menu/ \# Menu builder components
|
|
├── composables/ \# Vue composables
|
|
├── data/ \# JSON data storage
|
|
├── layouts/ \# Nuxt layouts
|
|
├── middleware/ \# Route middleware
|
|
├── pages/ \# Application pages
|
|
├── plugins/ \# Nuxt plugins
|
|
├── server/ \# Server API routes
|
|
├── stores/ \# Pinia stores
|
|
├── types/ \# TypeScript definitions
|
|
└── utils/ \# Utility functions
|
|
|
|
```
|
|
|
|
## 🛠️ Technology Stack
|
|
|
|
- **Framework**: Nuxt 3
|
|
- **Frontend**: Vue 3 + Composition API
|
|
- **UI Library**: Vuetify 3
|
|
- **Language**: TypeScript
|
|
- **State Management**: Pinia
|
|
- **Authentication**: JWT
|
|
- **Styling**: CSS3 + Vuetify
|
|
- **Icons**: Material Design Icons
|
|
- **Utilities**: VueUse
|
|
|
|
## 🎨 UI/UX Features
|
|
|
|
- **Responsive Design**: Works on all device sizes
|
|
- **Dark/Light Theme**: Built-in theme switching
|
|
- **Loading States**: Skeleton loaders and progress indicators
|
|
- **Error Handling**: User-friendly error messages
|
|
- **Notifications**: Toast notifications for user feedback
|
|
- **Accessibility**: WCAG 2.1 compliant
|
|
- **Animation**: Smooth transitions and micro-interactions
|
|
|
|
## 📊 Data Management
|
|
|
|
- **JSON Storage**: File-based data storage for demo
|
|
- **CRUD Operations**: Create, Read, Update, Delete
|
|
- **Search & Filter**: Advanced filtering capabilities
|
|
- **Pagination**: Efficient data pagination
|
|
- **Sorting**: Multiple column sorting
|
|
- **Validation**: Client and server-side validation
|
|
|
|
## 🔧 Configuration
|
|
|
|
### Environment Variables
|
|
```
|
|
|
|
|
|
# JWT Configuration
|
|
|
|
JWT_SECRET=your-super-secret-jwt-key-change-in-production
|
|
JWT_EXPIRES_IN=24h
|
|
|
|
# API Configuration
|
|
|
|
API_BASE_URL=http://localhost:3000/api
|
|
|
|
# Application Configuration
|
|
|
|
APP_NAME="Multi-Tenant RBAC System"
|
|
APP_VERSION=1.0.0
|
|
|
|
```
|
|
|
|
### Nuxt Configuration
|
|
The `nuxt.config.ts` file contains all framework configurations including:
|
|
- Vuetify theming
|
|
- Module registrations
|
|
- Runtime configurations
|
|
- Build optimizations
|
|
|
|
## 🚦 Development
|
|
|
|
### Available Scripts
|
|
```
|
|
|
|
|
|
# Development
|
|
|
|
npm run dev \# Start development server
|
|
npm run build \# Build for production
|
|
npm run generate \# Generate static site
|
|
npm run preview \# Preview production build
|
|
|
|
# Code Quality
|
|
|
|
npm run lint \# Run ESLint
|
|
npm run lint:fix \# Fix ESLint issues
|
|
npm run type-check \# TypeScript type checking
|
|
|
|
```
|
|
|
|
### Adding New Features
|
|
|
|
1. **Create API endpoint** in `server/api/`
|
|
2. **Define types** in `types/`
|
|
3. **Create composable** in `composables/`
|
|
4. **Build components** in `components/`
|
|
5. **Add pages** in `pages/`
|
|
6. **Update permissions** in `utils/permissions.ts`
|
|
|
|
## 📈 Performance
|
|
|
|
- **Lazy Loading**: Components and routes
|
|
- **Tree Shaking**: Unused code elimination
|
|
- **Code Splitting**: Automatic route-based splitting
|
|
- **Image Optimization**: Responsive images
|
|
- **Caching**: API response caching
|
|
- **Minification**: CSS and JS minification
|
|
|
|
## 🔒 Security
|
|
|
|
- **Authentication**: JWT-based authentication
|
|
- **Authorization**: Role-based access control
|
|
- **Input Validation**: Comprehensive validation
|
|
- **XSS Protection**: Content sanitization
|
|
- **CSRF Protection**: Cross-site request forgery protection
|
|
- **HTTPS**: SSL/TLS encryption (production)
|
|
|
|
## 🧪 Testing
|
|
|
|
### Manual Testing
|
|
Use the provided demo credentials to test different user roles and permissions.
|
|
|
|
### Test Scenarios
|
|
- User login/logout flow
|
|
- User CRUD operations
|
|
- Role management
|
|
- Menu builder functionality
|
|
- Permission enforcement
|
|
- Responsive design
|
|
|
|
## 📚 API Documentation
|
|
|
|
### Authentication Endpoints
|
|
- `POST /api/auth/login` - User login
|
|
- `POST /api/auth/logout` - User logout
|
|
- `GET /api/auth/me` - Get current user
|
|
|
|
### User Management
|
|
- `GET /api/users` - Get users list
|
|
- `POST /api/users` - Create new user
|
|
- `GET /api/users/:id` - Get user by ID
|
|
- `PUT /api/users/:id` - Update user
|
|
- `DELETE /api/users/:id` - Delete user
|
|
|
|
### Role Management
|
|
- `GET /api/roles` - Get roles list
|
|
- `POST /api/roles` - Create new role
|
|
- `GET /api/roles/:id` - Get role by ID
|
|
- `PUT /api/roles/:id` - Update role
|
|
- `DELETE /api/roles/:id` - Delete role
|
|
|
|
### Menu Builder
|
|
- `GET /api/menus` - Get menus list
|
|
- `POST /api/menus` - Create new menu
|
|
- `GET /api/menus/:id` - Get menu by ID
|
|
- `PUT /api/menus/:id` - Update menu
|
|
- `DELETE /api/menus/:id` - Delete menu
|
|
|
|
## 🤝 Contributing
|
|
|
|
1. Fork the repository
|
|
2. Create feature branch (`git checkout -b feature/amazing-feature`)
|
|
3. Commit changes (`git commit -m 'Add amazing feature'`)
|
|
4. Push to branch (`git push origin feature/amazing-feature`)
|
|
5. Open Pull Request
|
|
|
|
## 📄 License
|
|
|
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
|
|
## 📞 Support
|
|
|
|
For support and questions:
|
|
- Create an issue in the repository
|
|
- Check the documentation
|
|
- Contact the development team
|
|
|
|
## 🚀 Deployment
|
|
|
|
### Production Build
|
|
```
|
|
|
|
npm run build
|
|
npm run preview
|
|
|
|
```
|
|
|
|
### Environment Setup
|
|
1. Set production environment variables
|
|
2. Configure web server (Nginx, Apache)
|
|
3. Set up SSL certificates
|
|
4. Configure database (if migrating from JSON)
|
|
|
|
## 🔄 Future Enhancements
|
|
|
|
- [ ] Database integration (PostgreSQL, MySQL)
|
|
- [ ] Email notifications
|
|
- [ ] Two-factor authentication
|
|
- [ ] Activity logging and audit trails
|
|
- [ ] Advanced reporting and analytics
|
|
- [ ] File upload and management
|
|
- [ ] API rate limiting
|
|
- [ ] Webhook support
|
|
- [ ] Mobile app (React Native/Flutter)
|
|
|
|
---
|
|
|
|
**Built with ❤️ using Nuxt 3, Vue 3, and Vuetify**
|
|
```
|
|
|
|
Proyek lengkap ini menggabungkan semua fitur dari kedua sistem sebelumnya menjadi **satu aplikasi yang terstruktur dan siap produksi** dengan:
|
|
|
|
1. **Struktur folder yang terorganisir** dengan pemisahan yang jelas
|
|
2. **Konfigurasi lengkap** dengan environment variables dan setup production
|
|
3. **UI/UX yang konsisten** dengan design system Vuetify
|
|
4. **Sistem keamanan** yang komprehensif
|
|
5. **Dokumentasi lengkap** untuk development dan deployment
|
|
6. **Type safety** penuh dengan TypeScript
|
|
7. **Performance optimization** built-in
|
|
8. **Responsive design** untuk semua perangkat
|
|
|
|
Sistem ini siap untuk dikembangkan lebih lanjut dan dapat di-deploy ke production environment.
|
|
|