Afiq f05dd42c16 Enhance README and implement RBAC system with Authentik integration
- Updated README.md to reflect the new project name and provide an overview of the Role-Based Access Control (RBAC) system.
- Added new components for RBAC management, including:
  - PermissionExample.vue: Demonstrates permission-based navigation.
  - GroupCard.vue: Displays group information and assigned roles.
  - PermissionMatrix.vue: Visual representation of permissions across roles and resources.
  - RoleTemplates.vue: Quick role templates for applying pre-configured permissions.
  - StatsCards.vue: Displays statistics related to users, groups, and roles.
- Introduced useRbacPermissions.js for managing permission checks.
- Created docker-compose.yml for PostgreSQL and Redis services.
- Developed comprehensive documentation for application management and Authentik integration.
- Added multiple pages for managing applications, groups, roles, and users, including bulk operations and templates.
- Updated navigation structure to include new RBAC management paths.
2025-05-31 15:58:41 +08:00

455 lines
17 KiB
Vue

<script setup>
definePageMeta({
title: "Application Resources",
middleware: ["auth"],
requiresAuth: true,
breadcrumb: [
{ name: "Dashboard", path: "/dashboard" },
{ name: "Applications", path: "/applications" },
{ name: "Resources", path: "/applications/resources", type: "current" }
]
});
import { ref, reactive, computed } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const activeTab = ref('menus')
// Application selector (in real app, this would be populated from API)
const applications = ref([
{ id: '1', name: 'corradAF', description: 'Main Application', status: 'active' },
{ id: '2', name: 'HR System', description: 'Human Resources', status: 'active' },
{ id: '3', name: 'Finance System', description: 'Financial Management', status: 'development' }
])
const selectedAppId = ref(route.query.appId || '1')
// Resources state management
const resources = reactive({
menus: [
{ id: '1', key: 'menu.dashboard', name: 'Dashboard', path: '/dashboard', level: 0 },
{ id: '2', key: 'menu.users', name: 'Users', path: '/users', level: 0 }
],
components: [
{ id: '1', key: 'component.user.edit_button', name: 'User Edit Button' },
{ id: '2', key: 'component.user.delete_button', name: 'User Delete Button' }
],
features: [
{ id: '1', key: 'feature.export.data', name: 'Export Data' },
{ id: '2', key: 'feature.approve.requests', name: 'Approve Requests' }
]
})
// Form states
const menuForm = reactive({
name: '',
key: '',
path: '',
level: 0
})
const componentForm = reactive({
name: '',
key: ''
})
const featureForm = reactive({
name: '',
key: ''
})
// Form handlers
const handleMenuSubmit = (data) => {
const newMenu = {
id: Date.now().toString(),
...data
}
resources.menus.push(newMenu)
menuForm.name = ''
menuForm.key = ''
menuForm.path = ''
menuForm.level = 0
}
const handleComponentSubmit = (data) => {
const newComponent = {
id: Date.now().toString(),
...data
}
resources.components.push(newComponent)
componentForm.name = ''
componentForm.key = ''
}
const handleFeatureSubmit = (data) => {
const newFeature = {
id: Date.now().toString(),
...data
}
resources.features.push(newFeature)
featureForm.name = ''
featureForm.key = ''
}
// Delete handlers
const deleteMenu = (id) => {
resources.menus = resources.menus.filter(menu => menu.id !== id)
}
const deleteComponent = (id) => {
resources.components = resources.components.filter(component => component.id !== id)
}
const deleteFeature = (id) => {
resources.features = resources.features.filter(feature => feature.id !== id)
}
// Auto-generate keys based on names
const generateKey = (prefix, name) => {
return `${prefix}.${name.toLowerCase().replace(/\s+/g, '_')}`
}
const updateMenuKey = (name) => {
menuForm.key = generateKey('menu', name)
}
const updateComponentKey = (name) => {
componentForm.key = generateKey('component', name)
}
const updateFeatureKey = (name) => {
featureForm.key = generateKey('feature', name)
}
</script>
<template>
<div>
<LayoutsBreadcrumb />
<!-- Header -->
<rs-card class="mb-6">
<template #body>
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between">
<div class="mb-4 lg:mb-0">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Application Resources</h1>
<p class="text-gray-600 dark:text-gray-400">Manage menus, components, and features for your application</p>
</div>
<!-- Application Selector -->
<div class="min-w-48">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Application</label>
<select
v-model="selectedAppId"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
>
<option v-for="app in applications" :key="app.id" :value="app.id">
{{ app.name }}
</option>
</select>
</div>
</div>
</template>
</rs-card>
<!-- Tab Navigation -->
<div class="mb-6">
<nav class="flex space-x-8 border-b border-gray-200 dark:border-gray-700">
<button
v-for="tab in [
{ id: 'menus', name: 'Menus', icon: 'ph:list' },
{ id: 'components', name: 'Components', icon: 'ph:squares-four' },
{ id: 'features', name: 'Features', icon: 'ph:gear' }
]"
:key="tab.id"
@click="activeTab = tab.id"
:class="[
'flex items-center py-2 px-1 border-b-2 font-medium text-sm transition-colors',
activeTab === tab.id
? 'border-primary text-primary'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
]"
>
<Icon :name="tab.icon" class="w-5 h-5 mr-2" />
{{ tab.name }}
</button>
</nav>
</div>
<!-- Content -->
<div class="space-y-6">
<!-- Menus Tab -->
<div v-if="activeTab === 'menus'" class="space-y-6">
<!-- Add Menu Form -->
<rs-card>
<template #header>
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Add New Menu</h3>
</template>
<template #body>
<FormKit
type="form"
:config="{ validationVisibility: 'submit' }"
@submit="handleMenuSubmit"
:value="menuForm"
:actions="false"
>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<FormKit
type="text"
name="name"
label="Menu Name"
validation="required"
@input="updateMenuKey($event)"
/>
<FormKit
type="text"
name="key"
label="Menu Key"
validation="required"
help="Unique identifier for this menu"
:disabled="true"
/>
<FormKit
type="text"
name="path"
label="Menu Path"
validation="required"
help="URL path for this menu item (e.g., /dashboard)"
/>
<FormKit
type="number"
name="level"
label="Menu Level"
validation="required|min:0"
help="0 for root level, 1+ for nested items"
/>
</div>
<div class="mt-6 flex justify-end">
<rs-button type="submit" variant="primary">
<Icon name="ph:plus" class="w-4 h-4 mr-2" />
Add Menu
</rs-button>
</div>
</FormKit>
</template>
</rs-card>
<!-- Menu List -->
<rs-card>
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Menu Items</h3>
<rs-badge variant="secondary">{{ resources.menus.length }} items</rs-badge>
</div>
</template>
<template #body>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-800">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Name</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Key</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Path</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Level</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
<tr v-for="menu in resources.menus" :key="menu.id">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
{{ menu.name }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400 font-mono">
{{ menu.key }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{{ menu.path }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{{ menu.level }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm">
<rs-button @click="deleteMenu(menu.id)" variant="danger-outline" size="sm">
<Icon name="ph:trash" class="w-4 h-4" />
</rs-button>
</td>
</tr>
</tbody>
</table>
</div>
</template>
</rs-card>
</div>
<!-- Components Tab -->
<div v-if="activeTab === 'components'" class="space-y-6">
<!-- Add Component Form -->
<rs-card>
<template #header>
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Add New Component</h3>
</template>
<template #body>
<FormKit
type="form"
:config="{ validationVisibility: 'submit' }"
@submit="handleComponentSubmit"
:value="componentForm"
:actions="false"
>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<FormKit
type="text"
name="name"
label="Component Name"
validation="required"
@input="updateComponentKey($event)"
/>
<FormKit
type="text"
name="key"
label="Component Key"
validation="required"
help="Unique identifier for this component"
:disabled="true"
/>
</div>
<div class="mt-6 flex justify-end">
<rs-button type="submit" variant="primary">
<Icon name="ph:plus" class="w-4 h-4 mr-2" />
Add Component
</rs-button>
</div>
</FormKit>
</template>
</rs-card>
<!-- Component List -->
<rs-card>
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Components</h3>
<rs-badge variant="secondary">{{ resources.components.length }} items</rs-badge>
</div>
</template>
<template #body>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-800">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Name</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Key</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
<tr v-for="component in resources.components" :key="component.id">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
{{ component.name }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400 font-mono">
{{ component.key }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm">
<rs-button @click="deleteComponent(component.id)" variant="danger-outline" size="sm">
<Icon name="ph:trash" class="w-4 h-4" />
</rs-button>
</td>
</tr>
</tbody>
</table>
</div>
</template>
</rs-card>
</div>
<!-- Features Tab -->
<div v-if="activeTab === 'features'" class="space-y-6">
<!-- Add Feature Form -->
<rs-card>
<template #header>
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Add New Feature</h3>
</template>
<template #body>
<FormKit
type="form"
:config="{ validationVisibility: 'submit' }"
@submit="handleFeatureSubmit"
:value="featureForm"
:actions="false"
>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<FormKit
type="text"
name="name"
label="Feature Name"
validation="required"
@input="updateFeatureKey($event)"
/>
<FormKit
type="text"
name="key"
label="Feature Key"
validation="required"
help="Unique identifier for this feature"
:disabled="true"
/>
</div>
<div class="mt-6 flex justify-end">
<rs-button type="submit" variant="primary">
<Icon name="ph:plus" class="w-4 h-4 mr-2" />
Add Feature
</rs-button>
</div>
</FormKit>
</template>
</rs-card>
<!-- Feature List -->
<rs-card>
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Features</h3>
<rs-badge variant="secondary">{{ resources.features.length }} items</rs-badge>
</div>
</template>
<template #body>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-800">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Name</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Key</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
<tr v-for="feature in resources.features" :key="feature.id">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
{{ feature.name }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400 font-mono">
{{ feature.key }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm">
<rs-button @click="deleteFeature(feature.id)" variant="danger-outline" size="sm">
<Icon name="ph:trash" class="w-4 h-4" />
</rs-button>
</td>
</tr>
</tbody>
</table>
</div>
</template>
</rs-card>
</div>
</div>
</div>
</template>