46 KiB
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
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
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
[
{
"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)
[
{
"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)
[
{
"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
<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
<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
<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
<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
<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
<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
<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
<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
# 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 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:
- Struktur folder yang terorganisir dengan pemisahan yang jelas
- Konfigurasi lengkap dengan environment variables dan setup production
- UI/UX yang konsisten dengan design system Vuetify
- Sistem keamanan yang komprehensif
- Dokumentasi lengkap untuk development dan deployment
- Type safety penuh dengan TypeScript
- Performance optimization built-in
- Responsive design untuk semua perangkat
Sistem ini siap untuk dikembangkan lebih lanjut dan dapat di-deploy ke production environment.