feat: Add frontend permission option for user creation and enhance validation error handling
This commit introduces a new `give_frontend_permission` field in the user creation request, allowing admins to create users with frontend access. The frontend has been updated to include validation for required fields and improved error messaging for Pydantic validation errors. Additionally, the backend has been modified to handle the creation of users in the auth database if frontend permission is granted. Documentation has been updated to reflect these changes.
This commit is contained in:
@@ -18,6 +18,7 @@ export interface UserCreateRequest {
|
||||
full_name: string
|
||||
is_active?: boolean
|
||||
is_admin?: boolean
|
||||
give_frontend_permission?: boolean
|
||||
}
|
||||
|
||||
export interface UserUpdateRequest {
|
||||
|
||||
@@ -4,6 +4,60 @@ import { authUsersApi, AuthUserResponse, AuthUserCreateRequest, AuthUserUpdateRe
|
||||
|
||||
type TabType = 'backend' | 'frontend'
|
||||
|
||||
/**
|
||||
* Format Pydantic validation errors into user-friendly messages
|
||||
*/
|
||||
function formatValidationError(error: any): string {
|
||||
// Get the field name from location array (e.g., ['body', 'email'] -> 'email')
|
||||
const fieldPath = error.loc || []
|
||||
const fieldName = fieldPath[fieldPath.length - 1] || 'field'
|
||||
|
||||
// Convert field names to friendly labels
|
||||
const fieldLabels: Record<string, string> = {
|
||||
email: 'Email',
|
||||
password: 'Password',
|
||||
username: 'Username',
|
||||
full_name: 'Full Name',
|
||||
name: 'Name',
|
||||
is_admin: 'Role',
|
||||
is_active: 'Status',
|
||||
has_write_access: 'Write Access',
|
||||
give_frontend_permission: 'Frontend Permission',
|
||||
}
|
||||
|
||||
const friendlyFieldName = fieldLabels[fieldName] || fieldName.charAt(0).toUpperCase() + fieldName.slice(1)
|
||||
|
||||
// Get the error message
|
||||
let message = error.msg || 'Invalid value'
|
||||
|
||||
// Simplify common error messages
|
||||
if (message.includes('not a valid email address') || message.includes('email address must have an @-sign')) {
|
||||
message = 'Please enter a valid email address'
|
||||
} else if (message.includes('ensure this value has at least') || message.includes('at least')) {
|
||||
const match = message.match(/at least (\d+)/i)
|
||||
if (match && fieldName === 'password') {
|
||||
message = `Password must be at least ${match[1]} characters long`
|
||||
} else if (match) {
|
||||
message = `Must be at least ${match[1]} characters long`
|
||||
}
|
||||
} else if (message.includes('ensure this value has at most') || message.includes('at most')) {
|
||||
const match = message.match(/at most (\d+)/i)
|
||||
if (match) {
|
||||
message = `Must be no more than ${match[1]} characters long`
|
||||
}
|
||||
} else if (message.includes('field required') || message.includes('required')) {
|
||||
message = 'This field is required'
|
||||
} else if (message.includes('string type expected')) {
|
||||
message = 'Please enter text'
|
||||
} else if (message.includes('value is not a valid')) {
|
||||
// Remove the technical "value is not a valid" prefix
|
||||
message = message.replace(/^value is not a valid\s*/i, '')
|
||||
message = `Please enter a valid ${message}`
|
||||
}
|
||||
|
||||
return `${friendlyFieldName}: ${message}`
|
||||
}
|
||||
|
||||
export default function ManageUsers() {
|
||||
const [activeTab, setActiveTab] = useState<TabType>('backend')
|
||||
|
||||
@@ -23,6 +77,7 @@ export default function ManageUsers() {
|
||||
full_name: '',
|
||||
is_active: true,
|
||||
is_admin: false,
|
||||
give_frontend_permission: false,
|
||||
})
|
||||
|
||||
const [editForm, setEditForm] = useState<UserUpdateRequest>({
|
||||
@@ -95,6 +150,25 @@ export default function ManageUsers() {
|
||||
const handleCreate = async () => {
|
||||
try {
|
||||
setError(null)
|
||||
|
||||
// Frontend validation
|
||||
if (!createForm.username || createForm.username.trim() === '') {
|
||||
setError('Username is required')
|
||||
return
|
||||
}
|
||||
if (!createForm.password || createForm.password.length < 6) {
|
||||
setError('Password must be at least 6 characters long')
|
||||
return
|
||||
}
|
||||
if (!createForm.email || createForm.email.trim() === '') {
|
||||
setError('Email is required')
|
||||
return
|
||||
}
|
||||
if (!createForm.full_name || createForm.full_name.trim() === '') {
|
||||
setError('Full name is required')
|
||||
return
|
||||
}
|
||||
|
||||
await usersApi.createUser(createForm)
|
||||
setShowCreateModal(false)
|
||||
setCreateForm({
|
||||
@@ -104,16 +178,56 @@ export default function ManageUsers() {
|
||||
full_name: '',
|
||||
is_active: true,
|
||||
is_admin: false,
|
||||
give_frontend_permission: false,
|
||||
})
|
||||
loadUsers()
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.detail || 'Failed to create user')
|
||||
// Handle validation errors (422) - Pydantic returns array of errors
|
||||
if (err.response?.status === 422) {
|
||||
const detail = err.response?.data?.detail
|
||||
if (Array.isArray(detail)) {
|
||||
// Format Pydantic validation errors into user-friendly messages
|
||||
const errorMessages = detail.map(formatValidationError)
|
||||
setError(errorMessages.join('; ') || 'Please check the form and try again')
|
||||
} else {
|
||||
setError(detail || 'Please check the form and try again')
|
||||
}
|
||||
} else {
|
||||
// Handle other errors (400, 500, etc.)
|
||||
const detail = err.response?.data?.detail
|
||||
if (typeof detail === 'string') {
|
||||
setError(detail)
|
||||
} else if (Array.isArray(detail)) {
|
||||
const errorMessages = detail.map((error: any) => {
|
||||
if (typeof error === 'string') return error
|
||||
return formatValidationError(error)
|
||||
})
|
||||
setError(errorMessages.join('; '))
|
||||
} else {
|
||||
setError(err.response?.data?.message || 'Failed to create user')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleAuthCreate = async () => {
|
||||
try {
|
||||
setAuthError(null)
|
||||
|
||||
// Frontend validation
|
||||
if (!authCreateForm.email || authCreateForm.email.trim() === '') {
|
||||
setAuthError('Email is required')
|
||||
return
|
||||
}
|
||||
if (!authCreateForm.name || authCreateForm.name.trim() === '') {
|
||||
setAuthError('Name is required')
|
||||
return
|
||||
}
|
||||
if (!authCreateForm.password || authCreateForm.password.length < 6) {
|
||||
setAuthError('Password must be at least 6 characters long')
|
||||
return
|
||||
}
|
||||
|
||||
await authUsersApi.createUser(authCreateForm)
|
||||
setShowAuthCreateModal(false)
|
||||
setAuthCreateForm({
|
||||
@@ -125,7 +239,31 @@ export default function ManageUsers() {
|
||||
})
|
||||
loadAuthUsers()
|
||||
} catch (err: any) {
|
||||
setAuthError(err.response?.data?.detail || 'Failed to create auth user')
|
||||
// Handle validation errors (422) - Pydantic returns array of errors
|
||||
if (err.response?.status === 422) {
|
||||
const detail = err.response?.data?.detail
|
||||
if (Array.isArray(detail)) {
|
||||
// Format Pydantic validation errors into user-friendly messages
|
||||
const errorMessages = detail.map(formatValidationError)
|
||||
setAuthError(errorMessages.join('; ') || 'Please check the form and try again')
|
||||
} else {
|
||||
setAuthError(detail || 'Please check the form and try again')
|
||||
}
|
||||
} else {
|
||||
// Handle other errors (400, 500, etc.)
|
||||
const detail = err.response?.data?.detail
|
||||
if (typeof detail === 'string') {
|
||||
setAuthError(detail)
|
||||
} else if (Array.isArray(detail)) {
|
||||
const errorMessages = detail.map((error: any) => {
|
||||
if (typeof error === 'string') return error
|
||||
return formatValidationError(error)
|
||||
})
|
||||
setAuthError(errorMessages.join('; '))
|
||||
} else {
|
||||
setAuthError(err.response?.data?.message || 'Failed to create auth user')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,8 +381,26 @@ export default function ManageUsers() {
|
||||
<button
|
||||
onClick={() => {
|
||||
if (activeTab === 'backend') {
|
||||
setError(null)
|
||||
setCreateForm({
|
||||
username: '',
|
||||
password: '',
|
||||
email: '',
|
||||
full_name: '',
|
||||
is_active: true,
|
||||
is_admin: false,
|
||||
give_frontend_permission: false,
|
||||
})
|
||||
setShowCreateModal(true)
|
||||
} else {
|
||||
setAuthError(null)
|
||||
setAuthCreateForm({
|
||||
email: '',
|
||||
name: '',
|
||||
password: '',
|
||||
is_admin: false,
|
||||
has_write_access: false,
|
||||
})
|
||||
setShowAuthCreateModal(true)
|
||||
}
|
||||
}}
|
||||
@@ -280,18 +436,6 @@ export default function ManageUsers() {
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Error Messages */}
|
||||
{error && activeTab === 'backend' && (
|
||||
<div className="mb-4 p-4 bg-red-50 border border-red-200 text-red-700 rounded-lg">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{authError && activeTab === 'frontend' && (
|
||||
<div className="mb-4 p-4 bg-red-50 border border-red-200 text-red-700 rounded-lg">
|
||||
{authError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Backend Users Tab */}
|
||||
{activeTab === 'backend' && (
|
||||
<>
|
||||
@@ -519,6 +663,11 @@ export default function ManageUsers() {
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 w-full max-w-md">
|
||||
<h2 className="text-xl font-bold mb-4">Create New User</h2>
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-50 border border-red-200 text-red-700 rounded-lg text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
@@ -607,10 +756,35 @@ export default function ManageUsers() {
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={createForm.give_frontend_permission || false}
|
||||
onChange={(e) =>
|
||||
setCreateForm({ ...createForm, give_frontend_permission: e.target.checked })
|
||||
}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">Give the user Frontend permission</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 flex justify-end gap-3">
|
||||
<button
|
||||
onClick={() => setShowCreateModal(false)}
|
||||
onClick={() => {
|
||||
setError(null)
|
||||
setCreateForm({
|
||||
username: '',
|
||||
password: '',
|
||||
email: '',
|
||||
full_name: '',
|
||||
is_active: true,
|
||||
is_admin: false,
|
||||
give_frontend_permission: false,
|
||||
})
|
||||
setShowCreateModal(false)
|
||||
}}
|
||||
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200"
|
||||
>
|
||||
Cancel
|
||||
@@ -734,6 +908,11 @@ export default function ManageUsers() {
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 w-full max-w-md">
|
||||
<h2 className="text-xl font-bold mb-4">Create New Front End User</h2>
|
||||
{authError && (
|
||||
<div className="mb-4 p-3 bg-red-50 border border-red-200 text-red-700 rounded-lg text-sm">
|
||||
{authError}
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
@@ -814,7 +993,17 @@ export default function ManageUsers() {
|
||||
</div>
|
||||
<div className="mt-6 flex justify-end gap-3">
|
||||
<button
|
||||
onClick={() => setShowAuthCreateModal(false)}
|
||||
onClick={() => {
|
||||
setAuthError(null)
|
||||
setAuthCreateForm({
|
||||
email: '',
|
||||
name: '',
|
||||
password: '',
|
||||
is_admin: false,
|
||||
has_write_access: false,
|
||||
})
|
||||
setShowAuthCreateModal(false)
|
||||
}}
|
||||
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200"
|
||||
>
|
||||
Cancel
|
||||
|
||||
Reference in New Issue
Block a user