- 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.
580 lines
20 KiB
Vue
580 lines
20 KiB
Vue
<script setup>
|
|
definePageMeta({
|
|
title: "Create User",
|
|
middleware: ["auth"],
|
|
requiresAuth: true,
|
|
breadcrumb: [
|
|
{ name: "Dashboard", path: "/dashboard" },
|
|
{ name: "Users", path: "/users" },
|
|
{ name: "Create User", path: "/users/create", type: "current" }
|
|
]
|
|
});
|
|
|
|
import { ref, reactive, computed, onMounted } from 'vue'
|
|
|
|
// Form state - SIMPLIFIED
|
|
const userForm = reactive({
|
|
// Basic Information
|
|
firstName: '',
|
|
lastName: '',
|
|
email: '',
|
|
username: '',
|
|
password: '',
|
|
confirmPassword: '',
|
|
|
|
// Application Assignment (ESSENTIAL)
|
|
application: '',
|
|
|
|
// Group Assignment (Groups contain roles)
|
|
groups: [],
|
|
|
|
// Direct Role Assignment (optional - for specific cases)
|
|
additionalRoles: [],
|
|
|
|
// Account Settings
|
|
isActive: true,
|
|
mustChangePassword: true,
|
|
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', 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', 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)
|
|
|
|
// Computed
|
|
const isFormValid = computed(() => {
|
|
return userForm.username &&
|
|
userForm.email &&
|
|
userForm.firstName &&
|
|
userForm.lastName &&
|
|
userForm.password &&
|
|
userForm.password === userForm.confirmPassword &&
|
|
userForm.password.length >= 8 &&
|
|
userForm.application
|
|
})
|
|
|
|
const passwordStrength = computed(() => {
|
|
const password = userForm.password
|
|
if (!password) return { score: 0, label: 'No Password', color: 'gray' }
|
|
|
|
let score = 0
|
|
if (password.length >= 8) score++
|
|
if (/[A-Z]/.test(password)) score++
|
|
if (/[a-z]/.test(password)) score++
|
|
if (/[0-9]/.test(password)) score++
|
|
if (/[^A-Za-z0-9]/.test(password)) score++
|
|
|
|
const labels = ['Very Weak', 'Weak', 'Fair', 'Good', 'Strong']
|
|
const colors = ['red', 'orange', 'yellow', 'blue', 'green']
|
|
|
|
return {
|
|
score,
|
|
label: labels[score] || 'Very Weak',
|
|
color: colors[score] || 'red'
|
|
}
|
|
})
|
|
|
|
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 = {}
|
|
|
|
if (!userForm.username) {
|
|
errors.value.username = 'Username is required'
|
|
}
|
|
|
|
if (!userForm.email) {
|
|
errors.value.email = 'Email is required'
|
|
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(userForm.email)) {
|
|
errors.value.email = 'Invalid email format'
|
|
}
|
|
|
|
if (!userForm.firstName) {
|
|
errors.value.firstName = 'First name is required'
|
|
}
|
|
|
|
if (!userForm.lastName) {
|
|
errors.value.lastName = 'Last name is required'
|
|
}
|
|
|
|
if (!userForm.password) {
|
|
errors.value.password = 'Password is required'
|
|
} else if (userForm.password.length < 8) {
|
|
errors.value.password = 'Password must be at least 8 characters'
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
const generateRandomPassword = () => {
|
|
const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*'
|
|
let password = ''
|
|
for (let i = 0; i < 12; i++) {
|
|
password += charset.charAt(Math.floor(Math.random() * charset.length))
|
|
}
|
|
userForm.password = password
|
|
userForm.confirmPassword = password
|
|
}
|
|
|
|
// Clear groups and roles when application changes
|
|
const onApplicationChange = () => {
|
|
userForm.groups = []
|
|
userForm.additionalRoles = []
|
|
}
|
|
|
|
const createUser = async () => {
|
|
if (!validateForm()) {
|
|
return
|
|
}
|
|
|
|
isLoading.value = true
|
|
|
|
try {
|
|
// Prepare user data - SIMPLIFIED
|
|
const userData = {
|
|
username: userForm.username,
|
|
email: userForm.email,
|
|
firstName: userForm.firstName,
|
|
lastName: userForm.lastName,
|
|
password: userForm.password,
|
|
application: userForm.application,
|
|
groups: userForm.groups,
|
|
additionalRoles: userForm.additionalRoles,
|
|
isActive: userForm.isActive,
|
|
mustChangePassword: userForm.mustChangePassword,
|
|
sendInvitation: userForm.sendInvitation
|
|
}
|
|
|
|
// API call to create user
|
|
const response = await $fetch('/api/users/create', {
|
|
method: 'POST',
|
|
body: userData
|
|
})
|
|
|
|
if (response.success) {
|
|
await navigateTo('/users')
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Failed to create user:', error)
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
const resetForm = () => {
|
|
Object.assign(userForm, {
|
|
firstName: '',
|
|
lastName: '',
|
|
email: '',
|
|
username: '',
|
|
password: '',
|
|
confirmPassword: '',
|
|
application: '',
|
|
groups: [],
|
|
additionalRoles: [],
|
|
isActive: true,
|
|
mustChangePassword: true,
|
|
sendInvitation: true
|
|
})
|
|
errors.value = {}
|
|
}
|
|
|
|
// Initialize
|
|
onMounted(() => {
|
|
// Load additional data if needed
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<LayoutsBreadcrumb />
|
|
|
|
<!-- Header -->
|
|
<div class="mb-6">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Add New User</h1>
|
|
<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">
|
|
<Icon name="ph:arrow-clockwise" class="w-4 h-4 mr-2" />
|
|
Reset Form
|
|
</rs-button>
|
|
<rs-button @click="createUser" :disabled="!isFormValid || isLoading">
|
|
<Icon name="ph:user-plus" class="w-4 h-4 mr-2" />
|
|
{{ isLoading ? 'Creating...' : 'Create User' }}
|
|
</rs-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<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">
|
|
|
|
<!-- Basic Information -->
|
|
<rs-card>
|
|
<template #header>
|
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Basic Information</h3>
|
|
</template>
|
|
<template #body>
|
|
<div class="space-y-4">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<FormKit
|
|
v-model="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.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>
|
|
</rs-card>
|
|
|
|
<!-- Password Settings -->
|
|
<rs-card>
|
|
<template #header>
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Password Settings</h3>
|
|
<rs-button @click="generateRandomPassword" variant="primary-outline" size="sm">
|
|
<Icon name="ph:key" class="w-4 h-4 mr-1" />
|
|
Generate
|
|
</rs-button>
|
|
</div>
|
|
</template>
|
|
<template #body>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<FormKit
|
|
v-model="userForm.password"
|
|
type="password"
|
|
label="Password"
|
|
placeholder="Enter password"
|
|
validation="required|length:8"
|
|
validation-visibility="dirty"
|
|
:validation-messages="{
|
|
required: 'Password is required',
|
|
length: 'Password must be at least 8 characters'
|
|
}"
|
|
/>
|
|
|
|
<FormKit
|
|
v-model="userForm.confirmPassword"
|
|
type="password"
|
|
label="Confirm Password"
|
|
placeholder="Confirm password"
|
|
validation="required|confirm:password"
|
|
validation-visibility="dirty"
|
|
:validation-messages="{
|
|
required: 'Confirm password is required',
|
|
confirm: 'Passwords do not match'
|
|
}"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Password Strength Indicator -->
|
|
<div v-if="userForm.password" class="mt-4">
|
|
<div class="flex items-center justify-between text-xs">
|
|
<span class="text-gray-500">Password Strength</span>
|
|
<span :class="`text-${passwordStrength.color}-600`">{{ passwordStrength.label }}</span>
|
|
</div>
|
|
<div class="mt-1 w-full bg-gray-200 rounded-full h-1">
|
|
<div
|
|
:class="`bg-${passwordStrength.color}-500`"
|
|
class="h-1 rounded-full transition-all duration-300"
|
|
:style="{ width: (passwordStrength.score / 5 * 100) + '%' }"
|
|
></div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</rs-card>
|
|
|
|
<!-- Account Settings -->
|
|
<rs-card>
|
|
<template #header>
|
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Account Settings</h3>
|
|
</template>
|
|
<template #body>
|
|
<div class="space-y-4">
|
|
<FormKit
|
|
v-model="userForm.isActive"
|
|
type="checkbox"
|
|
label="Account Active"
|
|
help="User can log in when active"
|
|
/>
|
|
|
|
<FormKit
|
|
v-model="userForm.mustChangePassword"
|
|
type="checkbox"
|
|
label="Must Change Password on First Login"
|
|
help="Force password change on first login"
|
|
/>
|
|
|
|
<FormKit
|
|
v-model="userForm.sendInvitation"
|
|
type="checkbox"
|
|
label="Send Email Invitation"
|
|
help="Send welcome email with login instructions"
|
|
/>
|
|
</div>
|
|
</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>
|
|
/* Custom styles */
|
|
</style> |