- 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.
383 lines
12 KiB
Vue
383 lines
12 KiB
Vue
<script setup>
|
|
definePageMeta({
|
|
title: "Create Group",
|
|
middleware: ["auth"],
|
|
requiresAuth: true,
|
|
breadcrumb: [
|
|
{ name: "Dashboard", path: "/dashboard" },
|
|
{ name: "Groups", path: "/groups" },
|
|
{ name: "Create Group", path: "/groups/create", type: "current" }
|
|
]
|
|
});
|
|
|
|
import { ref, reactive, computed, onMounted } from 'vue'
|
|
|
|
// Form state
|
|
const groupForm = reactive({
|
|
name: '',
|
|
description: '',
|
|
permissions: []
|
|
})
|
|
|
|
// Available options
|
|
const availableParentGroups = ref([
|
|
{ id: '1', name: 'IT Department', authentikUUID: 'uuid-1' },
|
|
{ id: '2', name: 'HR Department', authentikUUID: 'uuid-2' },
|
|
{ id: '3', name: 'Finance Department', authentikUUID: 'uuid-3' },
|
|
{ id: '4', name: 'Management', authentikUUID: 'uuid-4' }
|
|
])
|
|
|
|
const availableRoles = ref([
|
|
{ id: '1', name: 'Administrator', description: 'Full system access' },
|
|
{ id: '2', name: 'Manager', description: 'Department management access' },
|
|
{ id: '3', name: 'Editor', description: 'Content editing access' },
|
|
{ id: '4', name: 'Viewer', description: 'Read-only access' }
|
|
])
|
|
|
|
// Common attributes for groups
|
|
const commonAttributes = ref([
|
|
{ key: 'department', label: 'Department', type: 'text' },
|
|
{ key: 'cost_center', label: 'Cost Center', type: 'text' },
|
|
{ key: 'location', label: 'Location', type: 'text' },
|
|
{ key: 'manager_email', label: 'Manager Email', type: 'email' },
|
|
{ key: 'budget_code', label: 'Budget Code', type: 'text' }
|
|
])
|
|
|
|
// Validation state
|
|
const errors = ref({})
|
|
const isLoading = ref(false)
|
|
|
|
// Computed
|
|
const isFormValid = computed(() => {
|
|
return groupForm.name &&
|
|
groupForm.description &&
|
|
groupForm.name.length >= 3
|
|
})
|
|
|
|
const parentGroupOptions = computed(() =>
|
|
availableParentGroups.value.map(group => ({
|
|
label: group.name,
|
|
value: group.id
|
|
}))
|
|
)
|
|
|
|
const roleOptions = computed(() =>
|
|
availableRoles.value.map(role => ({
|
|
label: role.name,
|
|
value: role.id
|
|
}))
|
|
)
|
|
|
|
// Methods
|
|
const validateForm = () => {
|
|
errors.value = {}
|
|
|
|
if (!groupForm.name) {
|
|
errors.value.name = 'Group name is required'
|
|
} else if (groupForm.name.length < 3) {
|
|
errors.value.name = 'Group name must be at least 3 characters'
|
|
}
|
|
|
|
if (!groupForm.description) {
|
|
errors.value.description = 'Description is required'
|
|
}
|
|
|
|
return Object.keys(errors.value).length === 0
|
|
}
|
|
|
|
const addCustomAttribute = () => {
|
|
groupForm.customAttributes.push({ key: '', value: '' })
|
|
}
|
|
|
|
const removeCustomAttribute = (index) => {
|
|
if (groupForm.customAttributes.length > 1) {
|
|
groupForm.customAttributes.splice(index, 1)
|
|
}
|
|
}
|
|
|
|
const createGroup = async () => {
|
|
if (!validateForm()) {
|
|
return
|
|
}
|
|
|
|
isLoading.value = true
|
|
|
|
try {
|
|
// Prepare attributes object
|
|
const attributes = { ...groupForm.attributes }
|
|
|
|
// Add custom attributes
|
|
groupForm.customAttributes.forEach(attr => {
|
|
if (attr.key && attr.value) {
|
|
attributes[attr.key] = attr.value
|
|
}
|
|
})
|
|
|
|
// Prepare group data
|
|
const groupData = {
|
|
name: groupForm.name,
|
|
description: groupForm.description,
|
|
parentGroup: groupForm.parentGroup,
|
|
attributes,
|
|
defaultRoles: groupForm.defaultRoles,
|
|
syncToAuthentik: groupForm.syncToAuthentik,
|
|
createInAuthentik: groupForm.createInAuthentik
|
|
}
|
|
|
|
// API call to create group
|
|
const response = await $fetch('/api/groups/create', {
|
|
method: 'POST',
|
|
body: groupData
|
|
})
|
|
|
|
if (response.success) {
|
|
// Show success message and redirect
|
|
await navigateTo('/groups')
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Failed to create group:', error)
|
|
// Handle error
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
const resetForm = () => {
|
|
Object.assign(groupForm, {
|
|
name: '',
|
|
description: '',
|
|
parentGroup: '',
|
|
attributes: {},
|
|
customAttributes: [{ key: '', value: '' }],
|
|
defaultRoles: [],
|
|
syncToAuthentik: true,
|
|
createInAuthentik: true
|
|
})
|
|
errors.value = {}
|
|
}
|
|
|
|
const handleSubmit = (data) => {
|
|
console.log('Creating group:', data)
|
|
|
|
// Reset form
|
|
Object.assign(groupForm, {
|
|
name: '',
|
|
description: '',
|
|
permissions: []
|
|
})
|
|
}
|
|
|
|
// Initialize
|
|
onMounted(() => {
|
|
// Load available parent groups and roles
|
|
})
|
|
</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 Group</h1>
|
|
<p class="text-gray-600 dark:text-gray-400">Create a new group and sync with Authentik</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="createGroup" :disabled="!isFormValid || isLoading">
|
|
<Icon name="ph:users-three" class="w-4 h-4 mr-2" />
|
|
{{ isLoading ? 'Creating...' : 'Create Group' }}
|
|
</rs-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<FormKit type="form" @submit="createGroup">
|
|
<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">
|
|
<FormKit
|
|
v-model="groupForm.name"
|
|
type="text"
|
|
label="Group Name"
|
|
placeholder="Development Team"
|
|
validation="required|length:3"
|
|
validation-visibility="dirty"
|
|
:validation-messages="{
|
|
required: 'Group name is required',
|
|
length: 'Group name must be at least 3 characters'
|
|
}"
|
|
/>
|
|
|
|
<FormKit
|
|
v-model="groupForm.description"
|
|
type="textarea"
|
|
label="Description"
|
|
placeholder="Describe the purpose and responsibilities of this group"
|
|
validation="required"
|
|
validation-visibility="dirty"
|
|
:validation-messages="{
|
|
required: 'Description is required'
|
|
}"
|
|
/>
|
|
|
|
<FormKit
|
|
v-model="groupForm.parentGroup"
|
|
type="select"
|
|
label="Parent Group (Optional)"
|
|
placeholder="Select parent group"
|
|
:options="parentGroupOptions"
|
|
help="Create this as a sub-group under an existing group"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</rs-card>
|
|
|
|
<!-- Group Attributes -->
|
|
<rs-card>
|
|
<template #header>
|
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Group Attributes</h3>
|
|
</template>
|
|
<template #body>
|
|
<div class="space-y-6">
|
|
<!-- Common Attributes -->
|
|
<div>
|
|
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-3">Common Attributes</h4>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<FormKit
|
|
v-for="attr in commonAttributes"
|
|
:key="attr.key"
|
|
v-model="groupForm.attributes[attr.key]"
|
|
:type="attr.type"
|
|
:label="attr.label"
|
|
:placeholder="`Enter ${attr.label.toLowerCase()}`"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Custom Attributes -->
|
|
<div>
|
|
<div class="flex items-center justify-between mb-3">
|
|
<h4 class="text-sm font-medium text-gray-900 dark:text-white">Custom Attributes</h4>
|
|
<rs-button @click="addCustomAttribute" variant="primary-outline" size="sm">
|
|
<Icon name="ph:plus" class="w-4 h-4 mr-1" />
|
|
Add Attribute
|
|
</rs-button>
|
|
</div>
|
|
|
|
<div class="space-y-3">
|
|
<div
|
|
v-for="(attr, index) in groupForm.customAttributes"
|
|
:key="index"
|
|
class="flex items-center space-x-3"
|
|
>
|
|
<FormKit
|
|
v-model="attr.key"
|
|
type="text"
|
|
placeholder="Attribute name"
|
|
outer-class="flex-1"
|
|
:classes="{ outer: 'mb-0' }"
|
|
/>
|
|
<FormKit
|
|
v-model="attr.value"
|
|
type="text"
|
|
placeholder="Attribute value"
|
|
outer-class="flex-1"
|
|
:classes="{ outer: 'mb-0' }"
|
|
/>
|
|
<rs-button
|
|
@click="removeCustomAttribute(index)"
|
|
variant="danger"
|
|
size="sm"
|
|
:disabled="groupForm.customAttributes.length === 1"
|
|
>
|
|
<Icon name="ph:trash" class="w-4 h-4" />
|
|
</rs-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</rs-card>
|
|
</div>
|
|
|
|
<!-- Sidebar -->
|
|
<div class="space-y-6">
|
|
<!-- Permissions -->
|
|
<rs-card>
|
|
<template #header>
|
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Permissions</h3>
|
|
</template>
|
|
<template #body>
|
|
<div class="space-y-3">
|
|
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
|
Select permissions for this group
|
|
</p>
|
|
|
|
<FormKit
|
|
v-for="permission in availablePermissions"
|
|
:key="permission.id"
|
|
v-model="groupForm.permissions"
|
|
type="checkbox"
|
|
:value="permission.id"
|
|
:label="permission.name"
|
|
:help="permission.description"
|
|
:classes="{
|
|
wrapper: 'mb-2',
|
|
label: '!text-sm',
|
|
help: '!text-xs'
|
|
}"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</rs-card>
|
|
|
|
<!-- Preview -->
|
|
<rs-card>
|
|
<template #header>
|
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white">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">{{ groupForm.name || 'Not set' }}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Description:</span>
|
|
<p class="text-sm text-gray-900 dark:text-white">{{ groupForm.description || 'Not set' }}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Permissions:</span>
|
|
<p class="text-sm text-gray-900 dark:text-white">
|
|
{{ groupForm.permissions.length }} permission(s) selected
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</rs-card>
|
|
</div>
|
|
</div>
|
|
</FormKit>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* Custom styles */
|
|
</style> |