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

287 lines
9.1 KiB
Vue

<script setup>
definePageMeta({
title: "Groups",
middleware: ["auth"],
requiresAuth: true,
breadcrumb: [
{ name: "Dashboard", path: "/dashboard" },
{ name: "Groups", path: "/groups", type: "current" }
]
});
import { ref, reactive, onMounted } from 'vue'
// Sample groups data
const groups = ref([
{
id: 1,
name: 'IT Department',
description: 'Information Technology team members',
members: 12,
roles: ['Admin', 'Developer'],
parentGroup: null,
authentikUUID: 'uuid-it-dept',
isActive: true,
created: '2024-01-15'
},
{
id: 2,
name: 'HR Department',
description: 'Human Resources team',
members: 8,
roles: ['HR Manager', 'HR Staff'],
parentGroup: null,
authentikUUID: 'uuid-hr-dept',
isActive: true,
created: '2024-01-10'
},
{
id: 3,
name: 'Development Team',
description: 'Software development team',
members: 15,
roles: ['Senior Developer', 'Developer', 'Junior Developer'],
parentGroup: 'IT Department',
authentikUUID: 'uuid-dev-team',
isActive: true,
created: '2024-01-20'
},
{
id: 4,
name: 'QA Team',
description: 'Quality Assurance team',
members: 6,
roles: ['QA Lead', 'QA Tester'],
parentGroup: 'IT Department',
authentikUUID: 'uuid-qa-team',
isActive: true,
created: '2024-01-25'
}
])
const isLoading = ref(false)
const searchQuery = ref('')
// Computed
const filteredGroups = computed(() => {
if (!searchQuery.value) return groups.value
return groups.value.filter(group =>
group.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
group.description.toLowerCase().includes(searchQuery.value.toLowerCase())
)
})
const totalMembers = computed(() =>
groups.value.reduce((sum, group) => sum + group.members, 0)
)
// Methods
const refreshGroups = async () => {
isLoading.value = true
try {
// API call to refresh groups
await new Promise(resolve => setTimeout(resolve, 1000))
} finally {
isLoading.value = false
}
}
const viewGroup = (groupId) => {
// Navigate to group details
console.log('View group:', groupId)
}
const editGroup = (groupId) => {
// Navigate to edit group
console.log('Edit group:', groupId)
}
const deleteGroup = (groupId) => {
// Handle group deletion
console.log('Delete group:', groupId)
}
const getRolesBadgeVariant = (rolesCount) => {
if (rolesCount >= 3) return 'success'
if (rolesCount >= 2) return 'warning'
return 'secondary'
}
// Initialize
onMounted(() => {
// Load groups data
})
</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">Groups</h1>
<p class="text-gray-600 dark:text-gray-400">Organize users into groups and manage access levels</p>
</div>
<div class="flex space-x-3">
<rs-button @click="refreshGroups" :disabled="isLoading" variant="primary-outline">
<Icon name="ph:arrows-clockwise" class="w-4 h-4 mr-2" />
{{ isLoading ? 'Loading...' : 'Refresh' }}
</rs-button>
<rs-button @click="navigateTo('/groups/create')">
<Icon name="ph:plus" class="w-4 h-4 mr-2" />
Add Group
</rs-button>
</div>
</div>
</div>
<!-- Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6">
<rs-card>
<template #body>
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-blue-100 dark:bg-blue-900 rounded-lg flex items-center justify-center">
<Icon name="ph:users-three" class="w-5 h-5 text-blue-600 dark:text-blue-400" />
</div>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Total Groups</p>
<p class="text-2xl font-bold text-gray-900 dark:text-white">{{ groups.length }}</p>
</div>
</div>
</template>
</rs-card>
<rs-card>
<template #body>
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-green-100 dark:bg-green-900 rounded-lg flex items-center justify-center">
<Icon name="ph:users" class="w-5 h-5 text-green-600 dark:text-green-400" />
</div>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Total Members</p>
<p class="text-2xl font-bold text-gray-900 dark:text-white">{{ totalMembers }}</p>
</div>
</div>
</template>
</rs-card>
<rs-card>
<template #body>
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-purple-100 dark:bg-purple-900 rounded-lg flex items-center justify-center">
<Icon name="ph:tree-structure" class="w-5 h-5 text-purple-600 dark:text-purple-400" />
</div>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Parent Groups</p>
<p class="text-2xl font-bold text-gray-900 dark:text-white">{{ groups.filter(g => !g.parentGroup).length }}</p>
</div>
</div>
</template>
</rs-card>
<rs-card>
<template #body>
<div class="flex items-center">
<div class="flex-shrink-0">
<div class="w-8 h-8 bg-yellow-100 dark:bg-yellow-900 rounded-lg flex items-center justify-center">
<Icon name="ph:check-circle" class="w-5 h-5 text-yellow-600 dark:text-yellow-400" />
</div>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Active Groups</p>
<p class="text-2xl font-bold text-gray-900 dark:text-white">{{ groups.filter(g => g.isActive).length }}</p>
</div>
</div>
</template>
</rs-card>
</div>
<!-- Groups Table -->
<rs-card>
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">All Groups</h3>
<rs-badge variant="info">{{ groups.length }} groups</rs-badge>
</div>
</template>
<template #body>
<RsTable
:field="['group', 'members', 'status', 'parentGroup', 'actions']"
:data="groups"
:advanced="true"
:options="{
variant: 'default',
striped: false,
bordered: false,
hover: true
}"
:optionsAdvanced="{
sortable: true,
outsideBorder: false
}"
:pageSize="10"
>
<!-- Group Column -->
<template #group="{ value }">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10">
<div class="h-10 w-10 rounded-full bg-primary flex items-center justify-center">
<span class="text-sm font-medium text-white">{{ value.name[0] }}{{ value.name.split(' ')[1]?.[0] || value.name[1] || '' }}</span>
</div>
</div>
<div class="ml-4">
<div class="text-sm font-medium text-gray-900 dark:text-white">{{ value.name }}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">{{ value.description }}</div>
</div>
</div>
</template>
<!-- Members Column -->
<template #members="{ value }">
<span class="text-sm text-gray-900 dark:text-white">{{ value.members }}</span>
</template>
<!-- Status Column -->
<template #status="{ value }">
<rs-badge :variant="value.isActive ? 'success' : 'secondary'">
{{ value.isActive ? 'Active' : 'Inactive' }}
</rs-badge>
</template>
<!-- Parent Group Column -->
<template #parentGroup="{ value }">
<span class="text-sm text-gray-500 dark:text-gray-400">{{ value.parentGroup || 'Root Group' }}</span>
</template>
<!-- Actions Column -->
<template #actions="{ value }">
<div class="flex items-center justify-end space-x-2">
<button @click="viewGroup(value.id)" class="text-primary hover:text-primary/80">
<Icon name="ph:eye" class="w-4 h-4" />
</button>
<button @click="editGroup(value.id)" class="text-primary hover:text-primary/80">
<Icon name="ph:pencil" class="w-4 h-4" />
</button>
<button @click="deleteGroup(value.id)" class="text-red-600 hover:text-red-800">
<Icon name="ph:trash" class="w-4 h-4" />
</button>
</div>
</template>
</RsTable>
</template>
</rs-card>
</div>
</template>
<style scoped>
/* Custom styles */
</style>