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

600 lines
21 KiB
Vue

<script setup>
definePageMeta({
title: "Create Role",
middleware: ["auth"],
requiresAuth: true,
breadcrumb: [
{ name: "Dashboard", path: "/dashboard" },
{ name: "Roles", path: "/roles" },
{ name: "Create Role", path: "/roles/create", type: "current" }
]
});
import { ref, reactive, computed, onMounted } from 'vue'
// Form state
const roleForm = reactive({
name: '',
description: '',
permissions: {
menus: [],
components: [],
features: []
}
})
// Simplified templates with clear descriptions
const roleTemplates = ref([
{
id: 'administrator',
name: '🔴 Administrator',
description: 'Complete system access - all features and permissions',
permissionCount: 45,
icon: 'ph:crown',
color: 'red',
permissions: {
menus: ['1', '2', '3', '4', '5', '6'],
components: ['1', '2', '3', '4', '5'],
features: ['1', '2', '3', '4']
}
},
{
id: 'manager',
name: '🟡 Manager',
description: 'Department management, approvals, and team oversight',
permissionCount: 28,
icon: 'ph:briefcase',
color: 'yellow',
permissions: {
menus: ['1', '2', '3', '5'],
components: ['1', '3', '4'],
features: ['2', '4']
}
},
{
id: 'editor',
name: '🟢 Editor',
description: 'Content creation, editing, and data management',
permissionCount: 18,
icon: 'ph:pencil',
color: 'green',
permissions: {
menus: ['1', '2', '3'],
components: ['1', '3'],
features: ['1']
}
},
{
id: 'viewer',
name: '🔵 Viewer',
description: 'Read-only access to view data and reports',
permissionCount: 8,
icon: 'ph:eye',
color: 'blue',
permissions: {
menus: ['1', '2'],
components: ['1'],
features: []
}
},
{
id: 'custom',
name: '⚙️ Custom Role',
description: 'Configure permissions manually for specific needs',
permissionCount: 0,
icon: 'ph:gear',
color: 'gray',
permissions: {
menus: [],
components: [],
features: []
}
}
])
// Application options
const applicationOptions = ref([
{ value: '1', label: 'corradAF - Main Application' },
{ value: '2', label: 'HR System - Human Resources' },
{ value: '3', label: 'Finance System - Financial Management' }
])
// Simplified resource lists (only shown for custom roles)
const resources = ref({
menus: [
{ id: '1', name: 'Dashboard', path: '/dashboard' },
{ id: '2', name: 'Users Management', path: '/users' },
{ id: '3', name: 'Groups Management', path: '/groups' },
{ id: '4', name: 'Roles Management', path: '/roles' },
{ id: '5', name: 'Applications', path: '/applications' },
{ id: '6', name: 'RBAC Management', path: '/rbac' }
],
components: [
{ id: '1', name: 'User Profile Actions', description: 'Edit, delete user profiles' },
{ id: '2', name: 'Data Export', description: 'Export system data' },
{ id: '3', name: 'Bulk Operations', description: 'Mass user/group operations' },
{ id: '4', name: 'System Settings', description: 'Configure system settings' },
{ id: '5', name: 'Audit Logs', description: 'View system audit trails' }
],
features: [
{ id: '1', name: 'Data Export', description: 'Export data to CSV/Excel' },
{ id: '2', name: 'Approval Workflows', description: 'Approve/reject requests' },
{ id: '3', name: 'System Backup', description: 'Create system backups' },
{ id: '4', name: 'User Impersonation', description: 'Login as other users' }
]
})
// Loading state
const isLoading = ref(false)
// Computed
const selectedTemplateData = computed(() => {
return roleTemplates.value.find(t => t.id === roleForm.selectedTemplate)
})
const isFormValid = computed(() => {
return roleForm.name &&
roleForm.description &&
roleForm.application &&
(roleForm.useTemplate ? roleForm.selectedTemplate : true)
})
const permissionSummary = computed(() => {
if (roleForm.useTemplate && selectedTemplateData.value) {
return selectedTemplateData.value.permissionCount
}
const manualCount = roleForm.selectedMenus.length +
roleForm.selectedComponents.length +
roleForm.selectedFeatures.length
return manualCount
})
// Methods
const selectTemplate = (templateId) => {
roleForm.selectedTemplate = templateId
roleForm.useTemplate = true
roleForm.showAdvancedPermissions = false
// Auto-apply template permissions if custom
if (templateId !== 'custom') {
const template = roleTemplates.value.find(t => t.id === templateId)
if (template) {
roleForm.selectedMenus = [...template.permissions.menus]
roleForm.selectedComponents = [...template.permissions.components]
roleForm.selectedFeatures = [...template.permissions.features]
}
} else {
// Clear permissions for custom role
roleForm.selectedMenus = []
roleForm.selectedComponents = []
roleForm.selectedFeatures = []
roleForm.showAdvancedPermissions = true
}
}
const toggleAdvancedPermissions = () => {
roleForm.showAdvancedPermissions = !roleForm.showAdvancedPermissions
if (roleForm.showAdvancedPermissions) {
roleForm.useTemplate = false
roleForm.selectedTemplate = 'custom'
}
}
const handleTemplateSubmit = (templateData) => {
console.log('Creating role from template:', templateData)
// Apply template to form
roleForm.name = templateData.name
roleForm.description = templateData.description
roleForm.permissions = { ...templateData.permissions }
}
const handleManualSubmit = (data) => {
console.log('Creating custom role:', data)
// Reset form
Object.assign(roleForm, {
name: '',
description: '',
permissions: {
menus: [],
components: [],
features: []
}
})
}
const createRole = async () => {
if (!isFormValid.value) return
isLoading.value = true
try {
const roleData = {
name: roleForm.name,
description: roleForm.description,
application: roleForm.application,
isGlobal: roleForm.isGlobal,
isActive: roleForm.isActive,
priority: roleForm.priority,
template: roleForm.useTemplate ? roleForm.selectedTemplate : null,
permissions: {
menus: roleForm.selectedMenus,
components: roleForm.selectedComponents,
features: roleForm.selectedFeatures
},
syncToAuthentik: roleForm.syncToAuthentik
}
console.log('Creating role:', roleData)
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1500))
// Success - redirect
await navigateTo('/roles')
} catch (error) {
console.error('Failed to create role:', error)
} finally {
isLoading.value = false
}
}
const resetForm = () => {
Object.assign(roleForm, {
name: '',
description: '',
application: '1',
isGlobal: false,
isActive: true,
priority: 50,
useTemplate: true,
selectedTemplate: '',
showAdvancedPermissions: false,
selectedMenus: [],
selectedComponents: [],
selectedFeatures: [],
selectedActions: {},
syncToAuthentik: true
})
}
// Initialize
onMounted(() => {
// Set default template
roleForm.selectedTemplate = 'viewer'
})
</script>
<template>
<div>
<LayoutsBreadcrumb />
<!-- Header -->
<div class="mb-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Create New Role</h1>
<p class="text-gray-600 dark:text-gray-400">Choose a template or create a custom role with specific permissions</p>
</div>
<div class="flex space-x-3">
<rs-button @click="resetForm" variant="primary-outline">
<Icon name="ph:arrow-clockwise" class="w-4 h-4 mr-2" />
Reset Form
</rs-button>
<rs-button @click="createRole" :disabled="!isFormValid || isLoading">
<Icon name="ph:shield-plus" class="w-4 h-4 mr-2" />
{{ isLoading ? 'Creating...' : 'Create Role' }}
</rs-button>
</div>
</div>
</div>
<FormKit type="form" @submit="createRole">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Main Form -->
<div class="lg:col-span-2 space-y-6">
<!-- Basic Information -->
<rs-card>
<template #header>
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Basic Information</h3>
</template>
<template #body>
<div class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormKit
v-model="roleForm.name"
type="text"
label="Role Name"
placeholder="e.g., Content Manager"
validation="required|length:3"
validation-visibility="dirty"
:validation-messages="{
required: 'Role name is required',
length: 'Role name must be at least 3 characters'
}"
/>
<FormKit
v-model="roleForm.application"
type="select"
label="Application"
:options="applicationOptions"
validation="required"
help="Which application this role applies to"
/>
</div>
<FormKit
v-model="roleForm.description"
type="textarea"
label="Description"
placeholder="Describe what this role can do and its responsibilities"
validation="required"
validation-visibility="dirty"
rows="3"
:validation-messages="{
required: 'Description is required'
}"
/>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<FormKit
v-model="roleForm.priority"
type="range"
label="Priority Level"
:min="0"
:max="100"
:step="10"
help="Higher priority wins in conflicts"
/>
<FormKit
v-model="roleForm.isGlobal"
type="checkbox"
label="Global Role"
help="Available across all applications"
/>
<FormKit
v-model="roleForm.isActive"
type="checkbox"
label="Active"
help="Role can be assigned to users"
/>
</div>
</div>
</template>
</rs-card>
<!-- Role Templates (PRIMARY METHOD) -->
<rs-card>
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Choose Role Template</h3>
<rs-badge variant="info">Recommended</rs-badge>
</div>
</template>
<template #body>
<div class="space-y-4">
<p class="text-sm text-gray-600 dark:text-gray-400">
Select a pre-configured role template with common permission sets. You can customize later if needed.
</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div
v-for="template in roleTemplates"
:key="template.id"
@click="selectTemplate(template.id)"
class="cursor-pointer p-4 border-2 rounded-lg transition-all hover:shadow-md"
:class="{
'border-primary bg-primary/5': roleForm.selectedTemplate === template.id,
'border-gray-200 dark:border-gray-700 hover:border-primary/50': roleForm.selectedTemplate !== template.id
}"
>
<div class="flex items-start space-x-3">
<div class="flex-shrink-0">
<div class="w-10 h-10 rounded-lg flex items-center justify-center"
:class="`bg-${template.color}-100 dark:bg-${template.color}-900/30`">
<Icon :name="template.icon" :class="`w-6 h-6 text-${template.color}-600 dark:text-${template.color}-400`" />
</div>
</div>
<div class="flex-1 min-w-0">
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-1">
{{ template.name }}
</h4>
<p class="text-xs text-gray-600 dark:text-gray-400 mb-2">
{{ template.description }}
</p>
<div class="flex items-center justify-between">
<span class="text-xs font-medium text-gray-500">
{{ template.permissionCount }} permissions
</span>
<Icon
v-if="roleForm.selectedTemplate === template.id"
name="ph:check-circle-fill"
class="w-5 h-5 text-primary"
/>
</div>
</div>
</div>
</div>
</div>
<!-- Advanced Permissions Toggle -->
<div v-if="roleForm.selectedTemplate && roleForm.selectedTemplate !== 'custom'" class="pt-4 border-t border-gray-200 dark:border-gray-700">
<rs-button
@click="toggleAdvancedPermissions"
variant="secondary-outline"
size="sm"
class="w-full"
>
<Icon name="ph:gear" class="w-4 h-4 mr-2" />
{{ roleForm.showAdvancedPermissions ? 'Hide' : 'Show' }} Advanced Permissions
</rs-button>
</div>
</div>
</template>
</rs-card>
<!-- Advanced Permissions (Hidden by default) -->
<rs-card v-if="roleForm.showAdvancedPermissions || roleForm.selectedTemplate === 'custom'">
<template #header>
<div class="flex items-center">
<Icon name="ph:gear" class="w-5 h-5 mr-2 text-orange-600" />
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Advanced Permissions</h3>
<rs-badge variant="warning" class="ml-2">Expert Mode</rs-badge>
</div>
</template>
<template #body>
<div class="space-y-6">
<div class="p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg">
<div class="flex items-start">
<Icon name="ph:warning" class="w-5 h-5 text-yellow-600 dark:text-yellow-400 mr-2 mt-0.5" />
<div class="text-sm text-yellow-800 dark:text-yellow-200">
<p class="font-medium mb-1">Advanced Configuration</p>
<p>Configure individual permissions. Most users should use templates above.</p>
</div>
</div>
</div>
<!-- Menu Access -->
<div>
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-3 flex items-center">
<Icon name="ph:list" class="w-4 h-4 mr-2 text-blue-600" />
Menu Access
</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<FormKit
v-for="menu in resources.menus"
:key="menu.id"
v-model="roleForm.selectedMenus"
type="checkbox"
:value="menu.id"
:label="menu.name"
:help="menu.path"
:classes="{
wrapper: 'mb-1',
label: '!text-sm',
help: '!text-xs'
}"
/>
</div>
</div>
<!-- Component Access -->
<div>
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-3 flex items-center">
<Icon name="ph:squares-four" class="w-4 h-4 mr-2 text-green-600" />
Component Access
</h4>
<div class="grid grid-cols-1 gap-2">
<FormKit
v-for="component in resources.components"
:key="component.id"
v-model="roleForm.selectedComponents"
type="checkbox"
:value="component.id"
:label="component.name"
:help="component.description"
:classes="{
wrapper: 'mb-1',
label: '!text-sm',
help: '!text-xs'
}"
/>
</div>
</div>
<!-- Feature Access -->
<div>
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-3 flex items-center">
<Icon name="ph:lightning" class="w-4 h-4 mr-2 text-purple-600" />
Feature Access
</h4>
<div class="grid grid-cols-1 gap-2">
<FormKit
v-for="feature in resources.features"
:key="feature.id"
v-model="roleForm.selectedFeatures"
type="checkbox"
:value="feature.id"
:label="feature.name"
:help="feature.description"
:classes="{
wrapper: 'mb-1',
label: '!text-sm',
help: '!text-xs'
}"
/>
</div>
</div>
</div>
</template>
</rs-card>
</div>
<!-- Sidebar -->
<div class="space-y-6">
<!-- Role Preview -->
<rs-card>
<template #header>
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Role Preview</h3>
</template>
<template #body>
<div class="space-y-3">
<div>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Name:</span>
<p class="text-sm text-gray-900 dark:text-white">{{ roleForm.name || 'Not set' }}</p>
</div>
<div>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Template:</span>
<p class="text-sm text-gray-900 dark:text-white">
{{ selectedTemplateData?.name || 'None selected' }}
</p>
</div>
<div>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Permissions:</span>
<rs-badge :variant="permissionSummary > 0 ? 'success' : 'secondary'">
{{ permissionSummary }} permissions
</rs-badge>
</div>
<div>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Priority:</span>
<div class="flex items-center space-x-2">
<div class="flex-1 bg-gray-200 rounded-full h-2">
<div
class="bg-primary h-2 rounded-full transition-all duration-300"
:style="{ width: roleForm.priority + '%' }"
></div>
</div>
<span class="text-sm text-gray-600">{{ roleForm.priority }}</span>
</div>
</div>
<div class="flex space-x-2">
<rs-badge :variant="roleForm.isGlobal ? 'info' : 'secondary'">
{{ roleForm.isGlobal ? 'Global' : 'App-specific' }}
</rs-badge>
<rs-badge :variant="roleForm.isActive ? 'success' : 'secondary'">
{{ roleForm.isActive ? 'Active' : 'Inactive' }}
</rs-badge>
</div>
</div>
</template>
</rs-card>
</div>
</div>
</FormKit>
</div>
</template>
<style scoped>
/* Custom styles */
</style>