- 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.
455 lines
17 KiB
Vue
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> |