- Simplified the application creation process by consolidating form fields and enhancing validation. - Updated the create application page to streamline user experience with clearer provider options and improved layout. - Implemented SweetAlert for success and error notifications during user actions, replacing traditional alerts. - Enhanced the applications index page with dynamic filtering and improved data fetching from the Authentik API. - Refactored API endpoints to utilize slugs for application identification, ensuring consistency with Authentik's structure. - Improved authentication handling by updating the requireAuth utility to support cookie-based authentication.
398 lines
13 KiB
Vue
398 lines
13 KiB
Vue
<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 using SweetAlert
|
|
const { $swal } = useNuxtApp();
|
|
$swal.fire({
|
|
icon: 'success',
|
|
title: 'Success',
|
|
text: 'User created successfully!',
|
|
timer: 2000,
|
|
showConfirmButton: false
|
|
})
|
|
|
|
// Reset form or redirect
|
|
// resetForm()
|
|
|
|
} catch (error) {
|
|
console.error('Error creating user:', error)
|
|
const { $swal } = useNuxtApp();
|
|
$swal.fire({
|
|
icon: 'error',
|
|
title: 'Error',
|
|
text: '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> |