Afiq f05dd42c16 Enhance README and implement RBAC system with Authentik integration
- Updated README.md to reflect the new project name and provide an overview of the Role-Based Access Control (RBAC) system.
- Added new components for RBAC management, including:
  - PermissionExample.vue: Demonstrates permission-based navigation.
  - GroupCard.vue: Displays group information and assigned roles.
  - PermissionMatrix.vue: Visual representation of permissions across roles and resources.
  - RoleTemplates.vue: Quick role templates for applying pre-configured permissions.
  - StatsCards.vue: Displays statistics related to users, groups, and roles.
- Introduced useRbacPermissions.js for managing permission checks.
- Created docker-compose.yml for PostgreSQL and Redis services.
- Developed comprehensive documentation for application management and Authentik integration.
- Added multiple pages for managing applications, groups, roles, and users, including bulk operations and templates.
- Updated navigation structure to include new RBAC management paths.
2025-05-31 15:58:41 +08:00

531 lines
15 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
const userForm = reactive({
firstName: '',
lastName: '',
email: '',
username: '',
password: '',
confirmPassword: '',
groups: [],
roles: [],
isActive: true,
// Profile
phone: '',
department: '',
jobTitle: '',
employeeId: '',
// Account Settings
emailVerified: false,
mustChangePassword: true,
// Notification Settings
sendInvitation: true
})
// Available options
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' }
])
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'
])
// Validation state
const errors = ref({})
const isLoading = ref(false)
const showPassword = 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
})
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'
}
})
// 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'
}
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
}
const createUser = async () => {
if (!validateForm()) {
return
}
isLoading.value = true
try {
// Prepare user data
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,
groups: userForm.groups,
roles: userForm.roles,
sendInvitation: userForm.sendInvitation
}
// API call to create user
const response = await $fetch('/api/users/create', {
method: 'POST',
body: userData
})
if (response.success) {
// Show success message
await navigateTo('/users')
}
} catch (error) {
console.error('Failed to create user:', error)
// Handle error
} finally {
isLoading.value = false
}
}
const resetForm = () => {
Object.assign(userForm, {
firstName: '',
lastName: '',
email: '',
username: '',
password: '',
confirmPassword: '',
phone: '',
department: '',
jobTitle: '',
employeeId: '',
isActive: true,
emailVerified: false,
mustChangePassword: true,
groups: [],
roles: [],
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
})
</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">Create a new user account with roles and permissions</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="handleSubmit">
<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="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"
/>
<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"
validation="required"
validation-visibility="dirty"
/>
</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="{
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="{
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>
<!-- 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>
<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.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"
label="Send Email Invitation"
help="Send welcome email with login instructions"
/>
</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;
}
</style>