initial-rbac
This commit is contained in:
parent
545f5c6c93
commit
8527b25ac4
@ -25,6 +25,25 @@ export default [
|
||||
],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"header": "RBAC",
|
||||
"description": "",
|
||||
"child": [
|
||||
{
|
||||
"title": "Create User",
|
||||
"path": "/create-user",
|
||||
"icon": "",
|
||||
"child": []
|
||||
},
|
||||
{
|
||||
"title": "Permission",
|
||||
"path": "/rbac-permission",
|
||||
"icon": "",
|
||||
"child": []
|
||||
}
|
||||
],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"header": "Pentadbiran",
|
||||
"description": "Urus aplikasi anda",
|
||||
|
386
pages/create-user/index.vue
Normal file
386
pages/create-user/index.vue
Normal file
@ -0,0 +1,386 @@
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
title: "Create User",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
});
|
||||
|
||||
import { ref, reactive } from 'vue'
|
||||
|
||||
// Form data
|
||||
const formData = reactive({
|
||||
fullName: '',
|
||||
username: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
status: 'active',
|
||||
mfaEnabled: true,
|
||||
mfaMethod: 'authenticator_app'
|
||||
})
|
||||
|
||||
// Form validation
|
||||
const errors = reactive({})
|
||||
const isSubmitting = ref(false)
|
||||
|
||||
|
||||
|
||||
// Status options
|
||||
const statusOptions = [
|
||||
{ value: 'active', label: 'Active' },
|
||||
{ value: 'inactive', label: 'Inactive' },
|
||||
{ value: 'pending', label: 'Pending' }
|
||||
]
|
||||
|
||||
// MFA Method options
|
||||
const mfaMethodOptions = [
|
||||
{ value: 'authenticator_app', label: 'Authenticator App' },
|
||||
{ value: 'sms', label: 'SMS' },
|
||||
{ value: 'email', label: 'Email' }
|
||||
]
|
||||
|
||||
// Validation functions
|
||||
const validateForm = () => {
|
||||
const newErrors = {}
|
||||
|
||||
if (!formData.fullName.trim()) {
|
||||
newErrors.fullName = 'Full name is required'
|
||||
}
|
||||
|
||||
if (!formData.username.trim()) {
|
||||
newErrors.username = 'Username is required'
|
||||
} else if (formData.username.length < 3) {
|
||||
newErrors.username = 'Username must be at least 3 characters'
|
||||
}
|
||||
|
||||
if (!formData.email.trim()) {
|
||||
newErrors.email = 'Email is required'
|
||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
||||
newErrors.email = 'Please enter a valid email address'
|
||||
}
|
||||
|
||||
if (!formData.phone.trim()) {
|
||||
newErrors.phone = 'Phone number is required'
|
||||
}
|
||||
|
||||
if (!formData.password) {
|
||||
newErrors.password = 'Password is required'
|
||||
} else if (formData.password.length < 8) {
|
||||
newErrors.password = 'Password must be at least 8 characters'
|
||||
}
|
||||
|
||||
if (formData.password !== formData.confirmPassword) {
|
||||
newErrors.confirmPassword = 'Passwords do not match'
|
||||
}
|
||||
|
||||
Object.assign(errors, newErrors)
|
||||
return Object.keys(newErrors).length === 0
|
||||
}
|
||||
|
||||
// Clear specific error
|
||||
const clearError = (field) => {
|
||||
delete errors[field]
|
||||
}
|
||||
|
||||
// Submit form
|
||||
const submitForm = async () => {
|
||||
if (!validateForm()) {
|
||||
return
|
||||
}
|
||||
|
||||
isSubmitting.value = true
|
||||
|
||||
try {
|
||||
// Prepare data for submission (exclude confirmPassword)
|
||||
const { confirmPassword, ...submitData } = formData
|
||||
|
||||
// Here you would typically make an API call
|
||||
console.log('Submitting user data:', submitData)
|
||||
|
||||
// Simulate API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// Success handling
|
||||
alert('User created successfully!')
|
||||
|
||||
// Reset form or redirect
|
||||
// resetForm()
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating user:', error)
|
||||
alert('Error creating user. Please try again.')
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Reset form
|
||||
const resetForm = () => {
|
||||
Object.assign(formData, {
|
||||
fullName: '',
|
||||
username: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
status: 'active',
|
||||
mfaEnabled: true,
|
||||
mfaMethod: 'authenticator_app'
|
||||
})
|
||||
Object.keys(errors).forEach(key => delete errors[key])
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<LayoutsBreadcrumb />
|
||||
<rs-card>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-xl font-semibold">Create User</h2>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<form @submit.prevent="submitForm" class="space-y-6">
|
||||
<!-- Personal Information Section -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Full Name -->
|
||||
<div>
|
||||
<label for="fullName" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Full Name <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="fullName"
|
||||
v-model="formData.fullName"
|
||||
@input="clearError('fullName')"
|
||||
type="text"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
:class="{ 'border-red-500': errors.fullName }"
|
||||
placeholder="Enter full name"
|
||||
/>
|
||||
<p v-if="errors.fullName" class="text-red-500 text-sm mt-1">{{ errors.fullName }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Username -->
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Username <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="username"
|
||||
v-model="formData.username"
|
||||
@input="clearError('username')"
|
||||
type="text"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
:class="{ 'border-red-500': errors.username }"
|
||||
placeholder="Enter username"
|
||||
/>
|
||||
<p v-if="errors.username" class="text-red-500 text-sm mt-1">{{ errors.username }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Email Address <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
v-model="formData.email"
|
||||
@input="clearError('email')"
|
||||
type="email"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
:class="{ 'border-red-500': errors.email }"
|
||||
placeholder="Enter email address"
|
||||
/>
|
||||
<p v-if="errors.email" class="text-red-500 text-sm mt-1">{{ errors.email }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Phone -->
|
||||
<div>
|
||||
<label for="phone" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Phone Number <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="phone"
|
||||
v-model="formData.phone"
|
||||
@input="clearError('phone')"
|
||||
type="tel"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
:class="{ 'border-red-500': errors.phone }"
|
||||
placeholder="+60123456789"
|
||||
/>
|
||||
<p v-if="errors.phone" class="text-red-500 text-sm mt-1">{{ errors.phone }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password Section -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Password -->
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Password <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
v-model="formData.password"
|
||||
@input="clearError('password')"
|
||||
type="password"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
:class="{ 'border-red-500': errors.password }"
|
||||
placeholder="Enter password"
|
||||
/>
|
||||
<p v-if="errors.password" class="text-red-500 text-sm mt-1">{{ errors.password }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<div>
|
||||
<label for="confirmPassword" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Confirm Password <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="confirmPassword"
|
||||
v-model="formData.confirmPassword"
|
||||
@input="clearError('confirmPassword')"
|
||||
type="password"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
:class="{ 'border-red-500': errors.confirmPassword }"
|
||||
placeholder="Confirm password"
|
||||
/>
|
||||
<p v-if="errors.confirmPassword" class="text-red-500 text-sm mt-1">{{ errors.confirmPassword }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Role and Status Section -->
|
||||
|
||||
|
||||
<!-- MFA Section -->
|
||||
<div class="border-t pt-6">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">Multi-Factor Authentication (MFA)</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- MFA Enabled -->
|
||||
<div>
|
||||
<label class="flex items-center">
|
||||
<input
|
||||
v-model="formData.mfaEnabled"
|
||||
type="checkbox"
|
||||
class="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
||||
/>
|
||||
<span class="ml-2 text-sm font-medium text-gray-700">Enable MFA</span>
|
||||
</label>
|
||||
<p class="text-sm text-gray-500 mt-1">Enhance account security with multi-factor authentication</p>
|
||||
</div>
|
||||
|
||||
<!-- MFA Method -->
|
||||
<div v-if="formData.mfaEnabled">
|
||||
<label for="mfaMethod" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
MFA Method
|
||||
</label>
|
||||
<div class="relative">
|
||||
<select
|
||||
id="mfaMethod"
|
||||
v-model="formData.mfaMethod"
|
||||
class="appearance-none w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white pr-10 cursor-pointer hover:border-gray-400 transition-colors duration-200"
|
||||
>
|
||||
<option v-for="method in mfaMethodOptions" :key="method.value" :value="method.value">
|
||||
{{ method.label }}
|
||||
</option>
|
||||
</select>
|
||||
<div class="absolute inset-y-0 right-0 flex items-center px-2 pointer-events-none">
|
||||
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="flex justify-end space-x-4 pt-6 border-t">
|
||||
<button
|
||||
type="button"
|
||||
@click="resetForm"
|
||||
class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="px-4 py-2 bg-blue-600 border border-transparent rounded-md shadow-sm text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
<span v-if="isSubmitting">Creating...</span>
|
||||
<span v-else>Create User</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
</rs-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Custom styles for better form appearance */
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.grid-cols-1 {
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.md\:grid-cols-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.gap-6 {
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.space-y-6 > * + * {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.border-t {
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.pt-6 {
|
||||
padding-top: 1.5rem;
|
||||
}
|
||||
|
||||
/* Focus styles for better accessibility */
|
||||
input:focus,
|
||||
select:focus {
|
||||
outline: none;
|
||||
ring: 2px;
|
||||
ring-color: #3b82f6;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
/* Error state styles */
|
||||
.border-red-500 {
|
||||
border-color: #ef4444;
|
||||
}
|
||||
|
||||
.text-red-500 {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Button styles */
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Checkbox styles */
|
||||
input[type="checkbox"] {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
</style>
|
1147
pages/rbac-permission/index.vue
Normal file
1147
pages/rbac-permission/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user