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:
Afiq 2025-05-31 17:30:11 +08:00
parent 919a52fe51
commit a2a81bd3bb
6 changed files with 1267 additions and 1922 deletions

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>