Refactor RBAC Management and Dashboard Integration
- Removed the RBAC Management section from the navigation structure. - Updated the dashboard page title to "RBAC Management" and modified the breadcrumb to reflect the new path. - Simplified the dashboard component by removing unused data and integrating new tab functionalities for managing applications, groups, roles, and permissions. - Enhanced the create group and create role pages to include application assignment and simplified form states. - Updated user creation page to include application assignment and filtered group/role options based on the selected application. - Deleted the obsolete rbac-permission page to streamline the project structure.
This commit is contained in:
parent
919a52fe51
commit
a2a81bd3bb
@ -64,12 +64,6 @@ export default [
|
||||
"child": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "RBAC Management",
|
||||
"path": "/rbac-permission",
|
||||
"icon": "ph:matrix-logo",
|
||||
"child": []
|
||||
}
|
||||
],
|
||||
"meta": {}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -12,19 +12,22 @@ definePageMeta({
|
||||
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
|
||||
// Form state
|
||||
// Form state - SIMPLIFIED
|
||||
const groupForm = reactive({
|
||||
name: '',
|
||||
description: '',
|
||||
permissions: []
|
||||
parentGroup: '',
|
||||
roles: [], // Groups contain roles, not permissions
|
||||
application: '', // Groups belong to applications
|
||||
isActive: true
|
||||
})
|
||||
|
||||
// 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' }
|
||||
{ id: '1', name: 'IT Department' },
|
||||
{ id: '2', name: 'HR Department' },
|
||||
{ id: '3', name: 'Finance Department' },
|
||||
{ id: '4', name: 'Management' }
|
||||
])
|
||||
|
||||
const availableRoles = ref([
|
||||
@ -34,13 +37,10 @@ const availableRoles = ref([
|
||||
{ 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' }
|
||||
const availableApplications = ref([
|
||||
{ id: '1', name: 'Main Application', description: 'Primary business application' },
|
||||
{ id: '2', name: 'HR System', description: 'Human Resources Management' },
|
||||
{ id: '3', name: 'Finance System', description: 'Financial Management' }
|
||||
])
|
||||
|
||||
// Validation state
|
||||
@ -51,6 +51,7 @@ const isLoading = ref(false)
|
||||
const isFormValid = computed(() => {
|
||||
return groupForm.name &&
|
||||
groupForm.description &&
|
||||
groupForm.application &&
|
||||
groupForm.name.length >= 3
|
||||
})
|
||||
|
||||
@ -61,10 +62,10 @@ const parentGroupOptions = computed(() =>
|
||||
}))
|
||||
)
|
||||
|
||||
const roleOptions = computed(() =>
|
||||
availableRoles.value.map(role => ({
|
||||
label: role.name,
|
||||
value: role.id
|
||||
const applicationOptions = computed(() =>
|
||||
availableApplications.value.map(app => ({
|
||||
label: app.name,
|
||||
value: app.id
|
||||
}))
|
||||
)
|
||||
|
||||
@ -81,20 +82,14 @@ const validateForm = () => {
|
||||
if (!groupForm.description) {
|
||||
errors.value.description = 'Description is required'
|
||||
}
|
||||
|
||||
if (!groupForm.application) {
|
||||
errors.value.application = 'Application 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
|
||||
@ -103,25 +98,14 @@ const createGroup = async () => {
|
||||
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
|
||||
// Prepare group data - SIMPLIFIED
|
||||
const groupData = {
|
||||
name: groupForm.name,
|
||||
description: groupForm.description,
|
||||
parentGroup: groupForm.parentGroup,
|
||||
attributes,
|
||||
defaultRoles: groupForm.defaultRoles,
|
||||
syncToAuthentik: groupForm.syncToAuthentik,
|
||||
createInAuthentik: groupForm.createInAuthentik
|
||||
roles: groupForm.roles,
|
||||
application: groupForm.application,
|
||||
isActive: groupForm.isActive
|
||||
}
|
||||
|
||||
// API call to create group
|
||||
@ -131,13 +115,11 @@ const createGroup = async () => {
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
@ -148,29 +130,16 @@ const resetForm = () => {
|
||||
name: '',
|
||||
description: '',
|
||||
parentGroup: '',
|
||||
attributes: {},
|
||||
customAttributes: [{ key: '', value: '' }],
|
||||
defaultRoles: [],
|
||||
syncToAuthentik: true,
|
||||
createInAuthentik: true
|
||||
roles: [],
|
||||
application: '',
|
||||
isActive: 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
|
||||
// Load available data
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -183,7 +152,7 @@ onMounted(() => {
|
||||
<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>
|
||||
<p class="text-gray-600 dark:text-gray-400">Groups are collections of roles for organizing users</p>
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<rs-button @click="resetForm" variant="primary-outline">
|
||||
@ -210,26 +179,43 @@ onMounted(() => {
|
||||
</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'
|
||||
}"
|
||||
/>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormKit
|
||||
v-model="groupForm.name"
|
||||
type="text"
|
||||
label="Group Name"
|
||||
placeholder="e.g., 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.application"
|
||||
type="select"
|
||||
label="Application"
|
||||
placeholder="Select application"
|
||||
:options="applicationOptions"
|
||||
validation="required"
|
||||
validation-visibility="dirty"
|
||||
help="Which application this group belongs to"
|
||||
:validation-messages="{
|
||||
required: 'Application is required'
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormKit
|
||||
v-model="groupForm.description"
|
||||
type="textarea"
|
||||
label="Description"
|
||||
placeholder="Describe the purpose and responsibilities of this group"
|
||||
placeholder="Describe the purpose of this group"
|
||||
validation="required"
|
||||
validation-visibility="dirty"
|
||||
rows="3"
|
||||
:validation-messages="{
|
||||
required: 'Description is required'
|
||||
}"
|
||||
@ -243,73 +229,13 @@ onMounted(() => {
|
||||
: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>
|
||||
<FormKit
|
||||
v-model="groupForm.isActive"
|
||||
type="checkbox"
|
||||
label="Active Group"
|
||||
help="Group can be assigned to users when active"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
@ -317,25 +243,25 @@ onMounted(() => {
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="space-y-6">
|
||||
<!-- Permissions -->
|
||||
<!-- Roles Assignment -->
|
||||
<rs-card>
|
||||
<template #header>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Permissions</h3>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Roles in Group</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
|
||||
Select roles that belong to this group. Users in this group will inherit these roles.
|
||||
</p>
|
||||
|
||||
<FormKit
|
||||
v-for="permission in availablePermissions"
|
||||
:key="permission.id"
|
||||
v-model="groupForm.permissions"
|
||||
v-for="role in availableRoles"
|
||||
:key="role.id"
|
||||
v-model="groupForm.roles"
|
||||
type="checkbox"
|
||||
:value="permission.id"
|
||||
:label="permission.name"
|
||||
:help="permission.description"
|
||||
:value="role.id"
|
||||
:label="role.name"
|
||||
:help="role.description"
|
||||
:classes="{
|
||||
wrapper: 'mb-2',
|
||||
label: '!text-sm',
|
||||
@ -359,16 +285,24 @@ onMounted(() => {
|
||||
</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>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Application:</span>
|
||||
<p class="text-sm text-gray-900 dark:text-white">
|
||||
{{ availableApplications.find(a => a.id === groupForm.application)?.name || 'Not selected' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Permissions:</span>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Roles:</span>
|
||||
<p class="text-sm text-gray-900 dark:text-white">
|
||||
{{ groupForm.permissions.length }} permission(s) selected
|
||||
{{ groupForm.roles.length }} role(s) selected
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-2">
|
||||
<rs-badge :variant="groupForm.isActive ? 'success' : 'secondary'">
|
||||
{{ groupForm.isActive ? 'Active' : 'Inactive' }}
|
||||
</rs-badge>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
@ -1,842 +0,0 @@
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
title: "RBAC Management",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
breadcrumb: [
|
||||
{ name: "Dashboard", path: "/dashboard" },
|
||||
{ name: "RBAC Management", path: "/rbac-permission", type: "current" }
|
||||
]
|
||||
});
|
||||
|
||||
import { ref, reactive, onMounted, computed, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
// Active tab state
|
||||
const activeTab = ref('overview')
|
||||
|
||||
// Get route and router for query param handling
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// Tab definitions
|
||||
const tabs = [
|
||||
{ id: 'overview', name: 'Overview', icon: 'ph:chart-pie' },
|
||||
{ id: 'groups', name: 'Groups & Roles', icon: 'ph:users-three' },
|
||||
{ id: 'permissions', name: 'Permissions', icon: 'ph:shield-check' },
|
||||
{ id: 'applications', name: 'Applications', icon: 'ph:app-window' },
|
||||
{ id: 'audit', name: 'Audit Trail', icon: 'ph:clock-clockwise' }
|
||||
]
|
||||
|
||||
// Handle tab query parameter
|
||||
watch(() => route.query.tab, (newTab) => {
|
||||
if (newTab && tabs.find(tab => tab.id === newTab)) {
|
||||
activeTab.value = newTab
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// Update URL when tab changes
|
||||
watch(activeTab, (newTab) => {
|
||||
router.push({ query: { ...route.query, tab: newTab } })
|
||||
})
|
||||
|
||||
// Application state
|
||||
const selectedAppId = ref('1')
|
||||
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' }
|
||||
])
|
||||
|
||||
// Organization state
|
||||
const selectedOrgId = ref('1')
|
||||
const organizations = ref([
|
||||
{ id: '1', name: 'Main Organization', description: 'Primary tenant' },
|
||||
{ id: '2', name: 'Branch Office', description: 'Secondary tenant' }
|
||||
])
|
||||
|
||||
// Groups from Authentik
|
||||
const authentikGroups = ref([
|
||||
{
|
||||
id: '1',
|
||||
name: 'IT Department',
|
||||
authentikUUID: 'uuid-1',
|
||||
userCount: 12,
|
||||
description: 'Information Technology Department'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'HR Department',
|
||||
authentikUUID: 'uuid-2',
|
||||
userCount: 8,
|
||||
description: 'Human Resources Department'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Finance Department',
|
||||
authentikUUID: 'uuid-3',
|
||||
userCount: 6,
|
||||
description: 'Finance and Accounting Department'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Management',
|
||||
authentikUUID: 'uuid-4',
|
||||
userCount: 4,
|
||||
description: 'Executive Management'
|
||||
}
|
||||
])
|
||||
|
||||
// Roles for selected application
|
||||
const appRoles = ref([
|
||||
{ id: '1', name: 'Administrator', description: 'Full system access', userCount: 2 },
|
||||
{ id: '2', name: 'Manager', description: 'Department management access', userCount: 8 },
|
||||
{ id: '3', name: 'Editor', description: 'Content editing access', userCount: 15 },
|
||||
{ id: '4', name: 'Viewer', description: 'Read-only access', userCount: 25 }
|
||||
])
|
||||
|
||||
// Resources for selected application
|
||||
const resources = ref({
|
||||
menus: [
|
||||
{ id: '1', key: 'menu.dashboard', name: 'Dashboard', path: '/dashboard', level: 0 },
|
||||
{ id: '2', key: 'menu.users', name: 'Users', path: '/users', level: 0 },
|
||||
{ id: '3', key: 'menu.users.list', name: 'User List', path: '/users/list', level: 1 },
|
||||
{ id: '4', key: 'menu.users.create', name: 'Create User', path: '/users/create', level: 1 },
|
||||
{ id: '5', key: 'menu.rbac', name: 'RBAC', path: '/rbac', level: 0 },
|
||||
{ id: '6', key: 'menu.reports', name: 'Reports', path: '/reports', 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' },
|
||||
{ id: '3', key: 'component.user.bulk_actions', name: 'User Bulk Actions' },
|
||||
{ id: '4', key: 'component.profile.sensitive_info', name: 'Profile Sensitive Info' },
|
||||
{ id: '5', key: 'component.financial.data', name: 'Financial Data' }
|
||||
],
|
||||
features: [
|
||||
{ id: '1', key: 'feature.export.data', name: 'Export Data' },
|
||||
{ id: '2', key: 'feature.approve.requests', name: 'Approve Requests' },
|
||||
{ id: '3', key: 'feature.system.backup', name: 'System Backup' },
|
||||
{ id: '4', key: 'feature.user.impersonation', name: 'User Impersonation' }
|
||||
]
|
||||
})
|
||||
|
||||
// Actions
|
||||
const actions = ref([
|
||||
{ id: '1', name: 'view', label: 'View', icon: 'ph:eye' },
|
||||
{ id: '2', name: 'create', label: 'Create', icon: 'ph:plus' },
|
||||
{ id: '3', name: 'edit', label: 'Edit', icon: 'ph:pencil' },
|
||||
{ id: '4', name: 'delete', label: 'Delete', icon: 'ph:trash' },
|
||||
{ id: '5', name: 'approve', label: 'Approve', icon: 'ph:check' }
|
||||
])
|
||||
|
||||
// Permission state
|
||||
const groupRoleAssignments = ref({})
|
||||
const rolePermissions = ref({})
|
||||
const isLoading = ref(false)
|
||||
|
||||
// Stats for overview
|
||||
const stats = computed(() => ({
|
||||
totalGroups: authentikGroups.value.length,
|
||||
totalRoles: appRoles.value.length,
|
||||
totalUsers: authentikGroups.value.reduce((sum, group) => sum + group.userCount, 0),
|
||||
totalResources: resources.value.menus.length + resources.value.components.length + resources.value.features.length
|
||||
}))
|
||||
|
||||
// Navigation Methods - properly linked to actual pages
|
||||
const navigateToUsers = () => {
|
||||
navigateTo('/users')
|
||||
}
|
||||
|
||||
const navigateToGroups = () => {
|
||||
navigateTo('/groups')
|
||||
}
|
||||
|
||||
const navigateToRoles = () => {
|
||||
navigateTo('/roles')
|
||||
}
|
||||
|
||||
const navigateToApplications = () => {
|
||||
navigateTo('/applications')
|
||||
}
|
||||
|
||||
const navigateToCreateRole = () => {
|
||||
navigateTo('/roles/create')
|
||||
}
|
||||
|
||||
const navigateToCreateApplication = () => {
|
||||
navigateTo('/applications/create')
|
||||
}
|
||||
|
||||
const navigateToCreateUser = () => {
|
||||
navigateTo('/users/create')
|
||||
}
|
||||
|
||||
const navigateToCreateGroup = () => {
|
||||
navigateTo('/groups/create')
|
||||
}
|
||||
|
||||
// Quick Action Handlers
|
||||
const handleQuickAction = (action) => {
|
||||
switch (action) {
|
||||
case 'manage-users':
|
||||
navigateToUsers()
|
||||
break
|
||||
case 'manage-roles':
|
||||
activeTab.value = 'groups'
|
||||
break
|
||||
case 'manage-permissions':
|
||||
activeTab.value = 'permissions'
|
||||
break
|
||||
case 'manage-applications':
|
||||
activeTab.value = 'applications'
|
||||
break
|
||||
case 'view-audit':
|
||||
activeTab.value = 'audit'
|
||||
break
|
||||
default:
|
||||
console.log('Unknown action:', action)
|
||||
}
|
||||
}
|
||||
|
||||
// Event Handlers for Components
|
||||
const handleGroupRoleChange = ({ groupId, roleId, assigned }) => {
|
||||
updateGroupRole(groupId, roleId, assigned)
|
||||
}
|
||||
|
||||
const handlePermissionChange = ({ roleId, resourceId, action, granted }) => {
|
||||
updatePermission(roleId, resourceId, action, granted)
|
||||
}
|
||||
|
||||
// Initialize
|
||||
onMounted(() => {
|
||||
// Set initial tab from URL
|
||||
if (route.query.tab) {
|
||||
activeTab.value = route.query.tab
|
||||
}
|
||||
})
|
||||
</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">RBAC Management</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400">Manage roles, permissions, and access control across applications</p>
|
||||
</div>
|
||||
|
||||
<!-- Application & Organization Selector -->
|
||||
<div class="flex flex-col sm:flex-row gap-4">
|
||||
<div class="min-w-48">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Organization</label>
|
||||
<select v-model="selectedOrgId" 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="org in organizations" :key="org.id" :value="org.id">
|
||||
{{ org.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</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 tabs"
|
||||
: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>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<div class="space-y-6">
|
||||
|
||||
<!-- Overview Tab -->
|
||||
<div v-if="activeTab === 'overview'" class="space-y-6">
|
||||
<!-- Stats Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<!-- Total Groups -->
|
||||
<rs-card>
|
||||
<template #body>
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center">
|
||||
<Icon name="ph:users-three" class="w-5 h-5 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<div class="text-2xl font-bold text-gray-900 dark:text-white">{{ stats.totalGroups }}</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">Total Groups</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Total Roles -->
|
||||
<rs-card>
|
||||
<template #body>
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-purple-500 rounded-lg flex items-center justify-center">
|
||||
<Icon name="ph:shield-check" class="w-5 h-5 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<div class="text-2xl font-bold text-gray-900 dark:text-white">{{ stats.totalRoles }}</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">Total Roles</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Total Users -->
|
||||
<rs-card>
|
||||
<template #body>
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-green-500 rounded-lg flex items-center justify-center">
|
||||
<Icon name="ph:user" class="w-5 h-5 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<div class="text-2xl font-bold text-gray-900 dark:text-white">{{ stats.totalUsers }}</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">Total Users</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Total Resources -->
|
||||
<rs-card>
|
||||
<template #body>
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-yellow-500 rounded-lg flex items-center justify-center">
|
||||
<Icon name="ph:stack" class="w-5 h-5 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<div class="text-2xl font-bold text-gray-900 dark:text-white">{{ stats.totalResources }}</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">Resources</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<rs-card>
|
||||
<template #header>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Quick Actions</h3>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<!-- Manage Users -->
|
||||
<button
|
||||
@click="handleQuickAction('manage-users')"
|
||||
class="flex items-center justify-center p-4 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg hover:border-primary hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
<div class="text-center">
|
||||
<Icon name="ph:users" class="w-8 h-8 mx-auto mb-2 text-gray-600 dark:text-gray-400" />
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">Manage Users</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">View and edit users</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- Manage Permissions -->
|
||||
<button
|
||||
@click="handleQuickAction('manage-permissions')"
|
||||
class="flex items-center justify-center p-4 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg hover:border-primary hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
<div class="text-center">
|
||||
<Icon name="ph:shield-plus" class="w-8 h-8 mx-auto mb-2 text-gray-600 dark:text-gray-400" />
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">Manage Permissions</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">Configure access control</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- View Audit -->
|
||||
<button
|
||||
@click="handleQuickAction('view-audit')"
|
||||
class="flex items-center justify-center p-4 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg hover:border-primary hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
<div class="text-center">
|
||||
<Icon name="ph:clock-clockwise" class="w-8 h-8 mx-auto mb-2 text-gray-600 dark:text-gray-400" />
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">View Audit</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">Check activity logs</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<rs-card>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Recent Activity</h3>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-start space-x-3">
|
||||
<div class="flex-shrink-0 w-2 h-2 bg-green-500 rounded-full mt-2"></div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm text-gray-900 dark:text-white">
|
||||
<span class="font-medium">John Doe</span> was assigned to <span class="font-medium">Manager</span> role
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">2 minutes ago</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start space-x-3">
|
||||
<div class="flex-shrink-0 w-2 h-2 bg-blue-500 rounded-full mt-2"></div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm text-gray-900 dark:text-white">
|
||||
<span class="font-medium">IT Department</span> permissions updated for <span class="font-medium">User Management</span>
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">15 minutes ago</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start space-x-3">
|
||||
<div class="flex-shrink-0 w-2 h-2 bg-yellow-500 rounded-full mt-2"></div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm text-gray-900 dark:text-white">
|
||||
New role <span class="font-medium">Content Editor</span> created
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">1 hour ago</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
</div>
|
||||
|
||||
<!-- Groups & Roles Tab -->
|
||||
<div v-if="activeTab === 'groups'" class="space-y-6">
|
||||
<!-- Header with Actions -->
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Groups & Role Assignments</h2>
|
||||
<div class="flex space-x-3">
|
||||
<rs-button @click="navigateToCreateGroup" variant="secondary-outline">
|
||||
<Icon name="ph:user-plus" class="w-4 h-4 mr-2" />
|
||||
Create Group
|
||||
</rs-button>
|
||||
<rs-button @click="navigateToCreateRole" variant="primary-outline">
|
||||
<Icon name="ph:shield-plus" class="w-4 h-4 mr-2" />
|
||||
Create Role
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Groups Grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<rs-card v-for="group in authentikGroups" :key="group.id">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">{{ group.name }}</h3>
|
||||
<rs-badge variant="primary">{{ group.userCount }} Users</rs-badge>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="space-y-4">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">{{ group.description }}</p>
|
||||
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-gray-600 dark:text-gray-400">Users:</span>
|
||||
<span class="font-medium">{{ group.userCount }}</span>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">Assigned Roles:</label>
|
||||
<div class="space-y-1">
|
||||
<div v-for="role in appRoles" :key="role.id" class="flex items-center">
|
||||
<input
|
||||
:id="`group-${group.id}-role-${role.id}`"
|
||||
type="checkbox"
|
||||
:checked="groupRoleAssignments[group.id]?.includes(role.id)"
|
||||
@change="handleGroupRoleChange({
|
||||
groupId: group.id,
|
||||
roleId: role.id,
|
||||
assigned: $event.target.checked
|
||||
})"
|
||||
class="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded"
|
||||
/>
|
||||
<label
|
||||
:for="`group-${group.id}-role-${role.id}`"
|
||||
class="ml-2 text-sm text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{{ role.name }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-2 pt-2">
|
||||
<rs-button @click="navigateToUsers" variant="secondary-outline" size="sm" class="flex-1">
|
||||
Manage Users
|
||||
</rs-button>
|
||||
<rs-button @click="navigateToGroups" variant="primary-outline" size="sm" class="flex-1">
|
||||
View Details
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Permissions Tab -->
|
||||
<div v-if="activeTab === 'permissions'" class="space-y-6">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Permission Matrix</h2>
|
||||
<div class="flex items-center space-x-2">
|
||||
<rs-badge :variant="selectedAppId === '1' ? 'success' : 'secondary'">
|
||||
{{ applications.find(app => app.id === selectedAppId)?.name }}
|
||||
</rs-badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Menu Permissions -->
|
||||
<rs-card>
|
||||
<template #header>
|
||||
<div class="flex items-center">
|
||||
<Icon name="ph:list" class="w-5 h-5 mr-2 text-blue-600" />
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Menu Permissions</h3>
|
||||
</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">
|
||||
Menu Item
|
||||
</th>
|
||||
<th v-for="role in appRoles" :key="role.id" class="px-6 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
{{ role.name }}
|
||||
</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 }}
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">{{ menu.path }}</div>
|
||||
</td>
|
||||
<td v-for="role in appRoles" :key="role.id" class="px-6 py-4 whitespace-nowrap text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="rolePermissions[`${role.id}-${menu.id}-1`]"
|
||||
@change="handlePermissionChange({
|
||||
roleId: role.id,
|
||||
resourceId: menu.id,
|
||||
action: '1',
|
||||
granted: $event.target.checked
|
||||
})"
|
||||
class="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Component Permissions -->
|
||||
<rs-card>
|
||||
<template #header>
|
||||
<div class="flex items-center">
|
||||
<Icon name="ph:squares-four" class="w-5 h-5 mr-2 text-green-600" />
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Component Permissions</h3>
|
||||
</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">
|
||||
Component
|
||||
</th>
|
||||
<th v-for="role in appRoles" :key="role.id" class="px-6 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
{{ role.name }}
|
||||
</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 }}
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 font-mono">{{ component.key }}</div>
|
||||
</td>
|
||||
<td v-for="role in appRoles" :key="role.id" class="px-6 py-4 whitespace-nowrap text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="rolePermissions[`${role.id}-${component.id}-1`]"
|
||||
@change="handlePermissionChange({
|
||||
roleId: role.id,
|
||||
resourceId: component.id,
|
||||
action: '1',
|
||||
granted: $event.target.checked
|
||||
})"
|
||||
class="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Feature Permissions -->
|
||||
<rs-card>
|
||||
<template #header>
|
||||
<div class="flex items-center">
|
||||
<Icon name="ph:gear" class="w-5 h-5 mr-2 text-purple-600" />
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Feature Permissions</h3>
|
||||
</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">
|
||||
Feature
|
||||
</th>
|
||||
<th v-for="role in appRoles" :key="role.id" class="px-6 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||
{{ role.name }}
|
||||
</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 }}
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 font-mono">{{ feature.key }}</div>
|
||||
</td>
|
||||
<td v-for="role in appRoles" :key="role.id" class="px-6 py-4 whitespace-nowrap text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="rolePermissions[`${role.id}-${feature.id}-1`]"
|
||||
@change="handlePermissionChange({
|
||||
roleId: role.id,
|
||||
resourceId: feature.id,
|
||||
action: '1',
|
||||
granted: $event.target.checked
|
||||
})"
|
||||
class="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
</div>
|
||||
|
||||
<!-- Applications Tab -->
|
||||
<div v-if="activeTab === 'applications'" class="space-y-6">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Application Management</h2>
|
||||
<div class="flex space-x-3">
|
||||
<rs-button @click="navigateToApplications" variant="secondary-outline">
|
||||
<Icon name="ph:gear" class="w-4 h-4 mr-2" />
|
||||
Manage Resources
|
||||
</rs-button>
|
||||
<rs-button @click="navigateToCreateApplication" variant="primary">
|
||||
<Icon name="ph:plus" class="w-4 h-4 mr-2" />
|
||||
Create Application
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Applications Grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
<rs-card v-for="app in applications" :key="app.id">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">{{ app.name }}</h3>
|
||||
<rs-badge :variant="app.status === 'active' ? 'success' : app.status === 'development' ? 'warning' : 'secondary'">
|
||||
{{ app.status }}
|
||||
</rs-badge>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="space-y-4">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">{{ app.description }}</p>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4 text-center">
|
||||
<div>
|
||||
<div class="text-lg font-semibold text-gray-900 dark:text-white">6</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">Menus</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-lg font-semibold text-gray-900 dark:text-white">5</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">Components</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-lg font-semibold text-gray-900 dark:text-white">4</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">Features</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-2">
|
||||
<rs-button
|
||||
@click="selectedAppId = app.id; activeTab = 'permissions'"
|
||||
variant="primary-outline"
|
||||
size="sm"
|
||||
class="flex-1"
|
||||
>
|
||||
Configure
|
||||
</rs-button>
|
||||
<rs-button @click="navigateToApplications" variant="primary-outline" size="sm" class="flex-1">
|
||||
View Details
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Audit Trail Tab -->
|
||||
<div v-if="activeTab === 'audit'" class="space-y-6">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Audit Trail</h2>
|
||||
<div class="flex items-center space-x-2">
|
||||
<select class="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-sm">
|
||||
<option>All Actions</option>
|
||||
<option>Permission Changes</option>
|
||||
<option>Role Assignments</option>
|
||||
<option>User Actions</option>
|
||||
</select>
|
||||
<rs-button variant="primary-outline" size="sm">
|
||||
<Icon name="ph:funnel" class="w-4 h-4 mr-2" />
|
||||
Filter
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Audit Log -->
|
||||
<rs-card>
|
||||
<template #body>
|
||||
<div class="space-y-4">
|
||||
<!-- Audit Entry -->
|
||||
<div class="flex items-start space-x-4 p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center">
|
||||
<Icon name="ph:check" class="w-4 h-4 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">Permission Granted</p>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">2 minutes ago</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
<span class="font-medium">Admin User</span> granted <span class="font-mono bg-gray-100 dark:bg-gray-800 px-1 rounded">component.user.edit_button</span> permission to <span class="font-medium">Manager</span> role
|
||||
</p>
|
||||
<div class="flex items-center space-x-4 mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span>Application: corradAF</span>
|
||||
<span>IP: 192.168.1.100</span>
|
||||
<span>Session: abc123</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Audit Entry -->
|
||||
<div class="flex items-start space-x-4 p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-blue-100 dark:bg-blue-900/30 rounded-full flex items-center justify-center">
|
||||
<Icon name="ph:user-plus" class="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">User Role Assignment</p>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">15 minutes ago</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
<span class="font-medium">HR Manager</span> assigned <span class="font-medium">John Doe</span> to <span class="font-medium">Editor</span> role in <span class="font-medium">IT Department</span> group
|
||||
</p>
|
||||
<div class="flex items-center space-x-4 mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span>Application: HR System</span>
|
||||
<span>IP: 192.168.1.105</span>
|
||||
<span>Session: def456</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Audit Entry -->
|
||||
<div class="flex items-start space-x-4 p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-yellow-100 dark:bg-yellow-900/30 rounded-full flex items-center justify-center">
|
||||
<Icon name="ph:arrows-clockwise" class="w-4 h-4 text-yellow-600 dark:text-yellow-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">Authentik Sync</p>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">1 hour ago</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
<span class="font-medium">System</span> synchronized groups and users from Authentik. 3 new users added, 1 group updated.
|
||||
</p>
|
||||
<div class="flex items-center space-x-4 mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span>Application: System</span>
|
||||
<span>Automated Process</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.permission-grid {
|
||||
@apply min-w-full;
|
||||
}
|
||||
|
||||
.permission-cell input[type="checkbox"] {
|
||||
@apply h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded;
|
||||
}
|
||||
|
||||
.permission-key {
|
||||
@apply text-gray-500 dark:text-gray-400 font-mono text-xs;
|
||||
}
|
||||
</style>
|
@ -12,199 +12,78 @@ definePageMeta({
|
||||
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
|
||||
// Form state
|
||||
// Form state - SIMPLIFIED
|
||||
const roleForm = reactive({
|
||||
name: '',
|
||||
description: '',
|
||||
permissions: {
|
||||
menus: [],
|
||||
components: [],
|
||||
features: []
|
||||
}
|
||||
application: '',
|
||||
permissions: [], // Simple list of permissions
|
||||
isActive: true
|
||||
})
|
||||
|
||||
// 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: []
|
||||
}
|
||||
}
|
||||
// Available applications
|
||||
const availableApplications = ref([
|
||||
{ id: '1', name: 'Main Application', description: 'Primary business application' },
|
||||
{ id: '2', name: 'HR System', description: 'Human Resources Management' },
|
||||
{ id: '3', name: 'Finance System', description: 'Financial Management' }
|
||||
])
|
||||
|
||||
// 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 permissions - focused on actual system functions
|
||||
const availablePermissions = ref([
|
||||
// User Management
|
||||
{ id: 'users_view', name: 'View Users', category: 'User Management', description: 'Can view user listings and profiles' },
|
||||
{ id: 'users_create', name: 'Create Users', category: 'User Management', description: 'Can create new user accounts' },
|
||||
{ id: 'users_edit', name: 'Edit Users', category: 'User Management', description: 'Can modify user information' },
|
||||
{ id: 'users_delete', name: 'Delete Users', category: 'User Management', description: 'Can delete user accounts' },
|
||||
|
||||
// Group Management
|
||||
{ id: 'groups_view', name: 'View Groups', category: 'Group Management', description: 'Can view group listings' },
|
||||
{ id: 'groups_create', name: 'Create Groups', category: 'Group Management', description: 'Can create new groups' },
|
||||
{ id: 'groups_edit', name: 'Edit Groups', category: 'Group Management', description: 'Can modify groups' },
|
||||
{ id: 'groups_delete', name: 'Delete Groups', category: 'Group Management', description: 'Can delete groups' },
|
||||
|
||||
// Role Management
|
||||
{ id: 'roles_view', name: 'View Roles', category: 'Role Management', description: 'Can view role listings' },
|
||||
{ id: 'roles_create', name: 'Create Roles', category: 'Role Management', description: 'Can create new roles' },
|
||||
{ id: 'roles_edit', name: 'Edit Roles', category: 'Role Management', description: 'Can modify roles' },
|
||||
{ id: 'roles_delete', name: 'Delete Roles', category: 'Role Management', description: 'Can delete roles' },
|
||||
|
||||
// System Access
|
||||
{ id: 'dashboard_access', name: 'Dashboard Access', category: 'System Access', description: 'Can access the dashboard' },
|
||||
{ id: 'reports_view', name: 'View Reports', category: 'System Access', description: 'Can view system reports' },
|
||||
{ id: 'settings_view', name: 'View Settings', category: 'System Access', description: 'Can view system settings' },
|
||||
{ id: 'settings_edit', name: 'Edit Settings', category: 'System Access', description: 'Can modify system settings' }
|
||||
])
|
||||
|
||||
// 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)
|
||||
roleForm.application
|
||||
})
|
||||
|
||||
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
|
||||
const applicationOptions = computed(() =>
|
||||
availableApplications.value.map(app => ({
|
||||
label: app.name,
|
||||
value: app.id
|
||||
}))
|
||||
)
|
||||
|
||||
const permissionsByCategory = computed(() => {
|
||||
const grouped = {}
|
||||
availablePermissions.value.forEach(permission => {
|
||||
if (!grouped[permission.category]) {
|
||||
grouped[permission.category] = []
|
||||
}
|
||||
grouped[permission.category].push(permission)
|
||||
})
|
||||
return grouped
|
||||
})
|
||||
|
||||
// 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
|
||||
|
||||
@ -215,16 +94,8 @@ const createRole = async () => {
|
||||
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
|
||||
permissions: roleForm.permissions,
|
||||
isActive: roleForm.isActive
|
||||
}
|
||||
|
||||
console.log('Creating role:', roleData)
|
||||
@ -246,25 +117,15 @@ 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
|
||||
application: '',
|
||||
permissions: [],
|
||||
isActive: true
|
||||
})
|
||||
}
|
||||
|
||||
// Initialize
|
||||
onMounted(() => {
|
||||
// Set default template
|
||||
roleForm.selectedTemplate = 'viewer'
|
||||
// Load additional data if needed
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -277,7 +138,7 @@ onMounted(() => {
|
||||
<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>
|
||||
<p class="text-gray-600 dark:text-gray-400">Roles define what users can do in the application</p>
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<rs-button @click="resetForm" variant="primary-outline">
|
||||
@ -322,9 +183,14 @@ onMounted(() => {
|
||||
v-model="roleForm.application"
|
||||
type="select"
|
||||
label="Application"
|
||||
placeholder="Select application"
|
||||
:options="applicationOptions"
|
||||
validation="required"
|
||||
validation-visibility="dirty"
|
||||
help="Which application this role applies to"
|
||||
:validation-messages="{
|
||||
required: 'Application is required'
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -341,189 +207,45 @@ onMounted(() => {
|
||||
}"
|
||||
/>
|
||||
|
||||
<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>
|
||||
<FormKit
|
||||
v-model="roleForm.isActive"
|
||||
type="checkbox"
|
||||
label="Active Role"
|
||||
help="Role can be assigned to users when active"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Role Templates (PRIMARY METHOD) -->
|
||||
<!-- Permissions -->
|
||||
<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>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Permissions</h3>
|
||||
</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>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Select what actions this role can perform in the system.
|
||||
</p>
|
||||
|
||||
<!-- 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
|
||||
<div
|
||||
v-for="(permissions, category) in permissionsByCategory"
|
||||
:key="category"
|
||||
class="space-y-3"
|
||||
>
|
||||
<h4 class="text-sm font-medium text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-700 pb-2">
|
||||
{{ category }}
|
||||
</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"
|
||||
v-for="permission in permissions"
|
||||
:key="permission.id"
|
||||
v-model="roleForm.permissions"
|
||||
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"
|
||||
:value="permission.id"
|
||||
:label="permission.name"
|
||||
:help="permission.description"
|
||||
:classes="{
|
||||
wrapper: 'mb-1',
|
||||
label: '!text-sm',
|
||||
@ -552,36 +274,20 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Template:</span>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Application:</span>
|
||||
<p class="text-sm text-gray-900 dark:text-white">
|
||||
{{ selectedTemplateData?.name || 'None selected' }}
|
||||
{{ availableApplications.find(a => a.id === roleForm.application)?.name || 'Not 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 :variant="roleForm.permissions.length > 0 ? 'success' : 'secondary'">
|
||||
{{ roleForm.permissions.length }} 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>
|
||||
|
@ -12,61 +12,58 @@ definePageMeta({
|
||||
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
|
||||
// Form state
|
||||
// Form state - SIMPLIFIED
|
||||
const userForm = reactive({
|
||||
// Basic Information
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
groups: [],
|
||||
roles: [],
|
||||
isActive: true,
|
||||
|
||||
// Profile
|
||||
phone: '',
|
||||
department: '',
|
||||
jobTitle: '',
|
||||
employeeId: '',
|
||||
// Application Assignment (ESSENTIAL)
|
||||
application: '',
|
||||
|
||||
// Group Assignment (Groups contain roles)
|
||||
groups: [],
|
||||
|
||||
// Direct Role Assignment (optional - for specific cases)
|
||||
additionalRoles: [],
|
||||
|
||||
// Account Settings
|
||||
emailVerified: false,
|
||||
isActive: true,
|
||||
mustChangePassword: true,
|
||||
|
||||
// Notification Settings
|
||||
sendInvitation: true
|
||||
})
|
||||
|
||||
// Available options
|
||||
const availableApplications = ref([
|
||||
{ id: '1', name: 'Main Application', description: 'Primary business application' },
|
||||
{ id: '2', name: 'HR System', description: 'Human Resources Management' },
|
||||
{ id: '3', name: 'Finance System', description: 'Financial Management' }
|
||||
])
|
||||
|
||||
const availableGroups = ref([
|
||||
{ id: '1', name: 'IT Department', authentikUUID: 'uuid-1', description: 'Information Technology Department' },
|
||||
{ id: '2', name: 'HR Department', authentikUUID: 'uuid-2', description: 'Human Resources Department' },
|
||||
{ id: '3', name: 'Finance Department', authentikUUID: 'uuid-3', description: 'Finance and Accounting Department' },
|
||||
{ id: '4', name: 'Management', authentikUUID: 'uuid-4', description: 'Executive Management' }
|
||||
{ id: '1', name: 'IT Department', application: '1', description: 'Information Technology Department' },
|
||||
{ id: '2', name: 'HR Department', application: '2', description: 'Human Resources Department' },
|
||||
{ id: '3', name: 'Finance Department', application: '3', description: 'Finance and Accounting Department' },
|
||||
{ id: '4', name: 'Development Team', application: '1', description: 'Software Development Team' },
|
||||
{ id: '5', name: 'Support Team', application: '1', description: 'Customer Support Team' }
|
||||
])
|
||||
|
||||
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' }
|
||||
])
|
||||
|
||||
const departments = ref([
|
||||
'Information Technology',
|
||||
'Human Resources',
|
||||
'Finance',
|
||||
'Marketing',
|
||||
'Operations',
|
||||
'Legal',
|
||||
'Executive'
|
||||
{ id: '1', name: 'Administrator', application: '1', description: 'Full system access' },
|
||||
{ id: '2', name: 'Manager', application: '1', description: 'Department management access' },
|
||||
{ id: '3', name: 'Editor', application: '1', description: 'Content editing access' },
|
||||
{ id: '4', name: 'Viewer', application: '1', description: 'Read-only access' },
|
||||
{ id: '5', name: 'HR Admin', application: '2', description: 'HR system administrator' },
|
||||
{ id: '6', name: 'Finance Admin', application: '3', description: 'Finance system administrator' }
|
||||
])
|
||||
|
||||
// Validation state
|
||||
const errors = ref({})
|
||||
const isLoading = ref(false)
|
||||
const showPassword = ref(false)
|
||||
|
||||
// Computed
|
||||
const isFormValid = computed(() => {
|
||||
@ -76,7 +73,8 @@ const isFormValid = computed(() => {
|
||||
userForm.lastName &&
|
||||
userForm.password &&
|
||||
userForm.password === userForm.confirmPassword &&
|
||||
userForm.password.length >= 8
|
||||
userForm.password.length >= 8 &&
|
||||
userForm.application
|
||||
})
|
||||
|
||||
const passwordStrength = computed(() => {
|
||||
@ -100,6 +98,25 @@ const passwordStrength = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const applicationOptions = computed(() =>
|
||||
availableApplications.value.map(app => ({
|
||||
label: app.name,
|
||||
value: app.id
|
||||
}))
|
||||
)
|
||||
|
||||
// Groups filtered by selected application
|
||||
const filteredGroups = computed(() => {
|
||||
if (!userForm.application) return []
|
||||
return availableGroups.value.filter(group => group.application === userForm.application)
|
||||
})
|
||||
|
||||
// Roles filtered by selected application (for additional roles)
|
||||
const filteredRoles = computed(() => {
|
||||
if (!userForm.application) return []
|
||||
return availableRoles.value.filter(role => role.application === userForm.application)
|
||||
})
|
||||
|
||||
// Methods
|
||||
const validateForm = () => {
|
||||
errors.value = {}
|
||||
@ -131,6 +148,10 @@ const validateForm = () => {
|
||||
if (userForm.password !== userForm.confirmPassword) {
|
||||
errors.value.confirmPassword = 'Passwords do not match'
|
||||
}
|
||||
|
||||
if (!userForm.application) {
|
||||
errors.value.application = 'Application is required'
|
||||
}
|
||||
|
||||
return Object.keys(errors.value).length === 0
|
||||
}
|
||||
@ -145,6 +166,12 @@ const generateRandomPassword = () => {
|
||||
userForm.confirmPassword = password
|
||||
}
|
||||
|
||||
// Clear groups and roles when application changes
|
||||
const onApplicationChange = () => {
|
||||
userForm.groups = []
|
||||
userForm.additionalRoles = []
|
||||
}
|
||||
|
||||
const createUser = async () => {
|
||||
if (!validateForm()) {
|
||||
return
|
||||
@ -153,22 +180,18 @@ const createUser = async () => {
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
// Prepare user data
|
||||
// Prepare user data - SIMPLIFIED
|
||||
const userData = {
|
||||
username: userForm.username,
|
||||
email: userForm.email,
|
||||
firstName: userForm.firstName,
|
||||
lastName: userForm.lastName,
|
||||
password: userForm.password,
|
||||
phone: userForm.phone,
|
||||
department: userForm.department,
|
||||
jobTitle: userForm.jobTitle,
|
||||
employeeId: userForm.employeeId,
|
||||
isActive: userForm.isActive,
|
||||
emailVerified: userForm.emailVerified,
|
||||
mustChangePassword: userForm.mustChangePassword,
|
||||
application: userForm.application,
|
||||
groups: userForm.groups,
|
||||
roles: userForm.roles,
|
||||
additionalRoles: userForm.additionalRoles,
|
||||
isActive: userForm.isActive,
|
||||
mustChangePassword: userForm.mustChangePassword,
|
||||
sendInvitation: userForm.sendInvitation
|
||||
}
|
||||
|
||||
@ -179,13 +202,11 @@ const createUser = async () => {
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
// Show success message
|
||||
await navigateTo('/users')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to create user:', error)
|
||||
// Handle error
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
@ -199,46 +220,16 @@ const resetForm = () => {
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
phone: '',
|
||||
department: '',
|
||||
jobTitle: '',
|
||||
employeeId: '',
|
||||
isActive: true,
|
||||
emailVerified: false,
|
||||
mustChangePassword: true,
|
||||
application: '',
|
||||
groups: [],
|
||||
roles: [],
|
||||
additionalRoles: [],
|
||||
isActive: true,
|
||||
mustChangePassword: true,
|
||||
sendInvitation: true
|
||||
})
|
||||
errors.value = {}
|
||||
}
|
||||
|
||||
const handleSubmit = (data) => {
|
||||
console.log('Creating user:', {
|
||||
...data
|
||||
})
|
||||
|
||||
// Reset form
|
||||
Object.assign(userForm, {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
groups: [],
|
||||
roles: [],
|
||||
isActive: true,
|
||||
phone: '',
|
||||
department: '',
|
||||
jobTitle: '',
|
||||
employeeId: '',
|
||||
emailVerified: false,
|
||||
mustChangePassword: true,
|
||||
sendInvitation: true
|
||||
})
|
||||
}
|
||||
|
||||
// Initialize
|
||||
onMounted(() => {
|
||||
// Load additional data if needed
|
||||
@ -254,7 +245,7 @@ onMounted(() => {
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Add New User</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400">Create a new user account with roles and permissions</p>
|
||||
<p class="text-gray-600 dark:text-gray-400">Users get permissions through groups (collections of roles)</p>
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<rs-button @click="resetForm" variant="primary-outline">
|
||||
@ -269,7 +260,7 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormKit type="form" @submit="handleSubmit">
|
||||
<FormKit type="form" @submit="createUser">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- Main Form -->
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
@ -280,41 +271,73 @@ onMounted(() => {
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Basic Information</h3>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormKit
|
||||
v-model="userForm.username"
|
||||
type="text"
|
||||
label="Username"
|
||||
placeholder="john.doe"
|
||||
validation="required"
|
||||
validation-visibility="dirty"
|
||||
/>
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormKit
|
||||
v-model="userForm.firstName"
|
||||
type="text"
|
||||
label="First Name"
|
||||
placeholder="John"
|
||||
validation="required"
|
||||
validation-visibility="dirty"
|
||||
:validation-messages="{
|
||||
required: 'First name is required'
|
||||
}"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
v-model="userForm.lastName"
|
||||
type="text"
|
||||
label="Last Name"
|
||||
placeholder="Doe"
|
||||
validation="required"
|
||||
validation-visibility="dirty"
|
||||
:validation-messages="{
|
||||
required: 'Last name is required'
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormKit
|
||||
v-model="userForm.username"
|
||||
type="text"
|
||||
label="Username"
|
||||
placeholder="john.doe"
|
||||
validation="required"
|
||||
validation-visibility="dirty"
|
||||
:validation-messages="{
|
||||
required: 'Username is required'
|
||||
}"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
v-model="userForm.email"
|
||||
type="email"
|
||||
label="Email Address"
|
||||
placeholder="john.doe@company.com"
|
||||
validation="required|email"
|
||||
validation-visibility="dirty"
|
||||
:validation-messages="{
|
||||
required: 'Email is required',
|
||||
email: 'Invalid email format'
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormKit
|
||||
v-model="userForm.email"
|
||||
type="email"
|
||||
label="Email Address"
|
||||
placeholder="john.doe@company.com"
|
||||
validation="required|email"
|
||||
validation-visibility="dirty"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
v-model="userForm.firstName"
|
||||
type="text"
|
||||
label="First Name"
|
||||
placeholder="John"
|
||||
validation="required"
|
||||
validation-visibility="dirty"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
v-model="userForm.lastName"
|
||||
type="text"
|
||||
label="Last Name"
|
||||
placeholder="Doe"
|
||||
v-model="userForm.application"
|
||||
type="select"
|
||||
label="Application"
|
||||
placeholder="Select application"
|
||||
:options="applicationOptions"
|
||||
validation="required"
|
||||
validation-visibility="dirty"
|
||||
help="Which application this user will access"
|
||||
:validation-messages="{
|
||||
required: 'Application is required'
|
||||
}"
|
||||
@input="onApplicationChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@ -341,6 +364,7 @@ onMounted(() => {
|
||||
validation="required|length:8"
|
||||
validation-visibility="dirty"
|
||||
:validation-messages="{
|
||||
required: 'Password is required',
|
||||
length: 'Password must be at least 8 characters'
|
||||
}"
|
||||
/>
|
||||
@ -353,6 +377,7 @@ onMounted(() => {
|
||||
validation="required|confirm:password"
|
||||
validation-visibility="dirty"
|
||||
:validation-messages="{
|
||||
required: 'Confirm password is required',
|
||||
confirm: 'Passwords do not match'
|
||||
}"
|
||||
/>
|
||||
@ -375,45 +400,6 @@ onMounted(() => {
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Profile Information -->
|
||||
<rs-card>
|
||||
<template #header>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Profile Information</h3>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormKit
|
||||
v-model="userForm.phone"
|
||||
type="tel"
|
||||
label="Phone Number"
|
||||
placeholder="+1 234 567 8900"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
v-model="userForm.department"
|
||||
type="select"
|
||||
label="Department"
|
||||
placeholder="Select department"
|
||||
:options="departments"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
v-model="userForm.jobTitle"
|
||||
type="text"
|
||||
label="Job Title"
|
||||
placeholder="Software Engineer"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
v-model="userForm.employeeId"
|
||||
type="text"
|
||||
label="Employee ID"
|
||||
placeholder="EMP001"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Account Settings -->
|
||||
<rs-card>
|
||||
<template #header>
|
||||
@ -428,83 +414,13 @@ onMounted(() => {
|
||||
help="User can log in when active"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
v-model="userForm.emailVerified"
|
||||
type="checkbox"
|
||||
label="Email Verified"
|
||||
help="Mark email as verified"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
v-model="userForm.mustChangePassword"
|
||||
type="checkbox"
|
||||
label="Must Change Password on First Login"
|
||||
help="Force password change on first login"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="space-y-6">
|
||||
<!-- Groups Assignment -->
|
||||
<rs-card>
|
||||
<template #header>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Groups</h3>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="space-y-3">
|
||||
<FormKit
|
||||
v-for="group in availableGroups"
|
||||
:key="group.id"
|
||||
v-model="userForm.groups"
|
||||
type="checkbox"
|
||||
:value="group.id"
|
||||
:label="group.name"
|
||||
:help="group.description"
|
||||
:classes="{
|
||||
wrapper: 'mb-2',
|
||||
label: '!text-sm',
|
||||
help: '!text-xs'
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Roles Assignment -->
|
||||
<rs-card>
|
||||
<template #header>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Roles</h3>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="space-y-3">
|
||||
<FormKit
|
||||
v-for="role in availableRoles"
|
||||
:key="role.id"
|
||||
v-model="userForm.roles"
|
||||
type="checkbox"
|
||||
:value="role.id"
|
||||
:label="role.name"
|
||||
:help="role.description"
|
||||
:classes="{
|
||||
wrapper: 'mb-2',
|
||||
label: '!text-sm',
|
||||
help: '!text-xs'
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Notification Settings -->
|
||||
<rs-card>
|
||||
<template #header>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Notification Settings</h3>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="space-y-4">
|
||||
|
||||
<FormKit
|
||||
v-model="userForm.sendInvitation"
|
||||
type="checkbox"
|
||||
@ -515,17 +431,150 @@ onMounted(() => {
|
||||
</template>
|
||||
</rs-card>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="space-y-6">
|
||||
<!-- Groups Assignment (PRIMARY) -->
|
||||
<rs-card>
|
||||
<template #header>
|
||||
<div class="flex items-center">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mr-2">Groups</h3>
|
||||
<rs-badge variant="info">Primary</rs-badge>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="space-y-3">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Groups contain roles. Users inherit all roles from their groups.
|
||||
</p>
|
||||
|
||||
<div v-if="!userForm.application" class="p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
||||
<p class="text-sm text-blue-800 dark:text-blue-200">
|
||||
Select an application first to see available groups.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="filteredGroups.length === 0" class="p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
No groups available for the selected application.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<FormKit
|
||||
v-for="group in filteredGroups"
|
||||
:key="group.id"
|
||||
v-model="userForm.groups"
|
||||
type="checkbox"
|
||||
:value="group.id"
|
||||
:label="group.name"
|
||||
:help="group.description"
|
||||
:classes="{
|
||||
wrapper: 'mb-2',
|
||||
label: '!text-sm',
|
||||
help: '!text-xs'
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Additional Roles (OPTIONAL) -->
|
||||
<rs-card>
|
||||
<template #header>
|
||||
<div class="flex items-center">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mr-2">Additional Roles</h3>
|
||||
<rs-badge variant="secondary">Optional</rs-badge>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="space-y-3">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Extra roles for specific permissions beyond group roles.
|
||||
</p>
|
||||
|
||||
<div v-if="!userForm.application" class="p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
||||
<p class="text-sm text-blue-800 dark:text-blue-200">
|
||||
Select an application first to see available roles.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="filteredRoles.length === 0" class="p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
No additional roles available for the selected application.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<FormKit
|
||||
v-for="role in filteredRoles"
|
||||
:key="role.id"
|
||||
v-model="userForm.additionalRoles"
|
||||
type="checkbox"
|
||||
:value="role.id"
|
||||
:label="role.name"
|
||||
:help="role.description"
|
||||
:classes="{
|
||||
wrapper: 'mb-2',
|
||||
label: '!text-sm',
|
||||
help: '!text-xs'
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</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">
|
||||
{{ userForm.firstName && userForm.lastName ? `${userForm.firstName} ${userForm.lastName}` : 'Not set' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Application:</span>
|
||||
<p class="text-sm text-gray-900 dark:text-white">
|
||||
{{ availableApplications.find(a => a.id === userForm.application)?.name || 'Not selected' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Groups:</span>
|
||||
<p class="text-sm text-gray-900 dark:text-white">
|
||||
{{ userForm.groups.length }} group(s) selected
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Additional Roles:</span>
|
||||
<p class="text-sm text-gray-900 dark:text-white">
|
||||
{{ userForm.additionalRoles.length }} additional role(s)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-2">
|
||||
<rs-badge :variant="userForm.isActive ? 'success' : 'secondary'">
|
||||
{{ userForm.isActive ? 'Active' : 'Inactive' }}
|
||||
</rs-badge>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
</div>
|
||||
</div>
|
||||
</FormKit>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
input:focus {
|
||||
@apply ring-2 ring-offset-2;
|
||||
}
|
||||
|
||||
select:focus {
|
||||
@apply ring-2 ring-offset-2;
|
||||
}
|
||||
/* Custom styles */
|
||||
</style>
|
Loading…
x
Reference in New Issue
Block a user