From dbffaef298bdb7c34ff6cdf466fc8acaa67f54b1 Mon Sep 17 00:00:00 2001 From: tanyar09 Date: Mon, 24 Nov 2025 14:21:56 -0500 Subject: [PATCH] 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. --- frontend/src/api/users.ts | 1 + frontend/src/pages/ManageUsers.tsx | 221 ++++++++++++++++++++++++++--- src/web/api/users.py | 84 ++++++++++- src/web/schemas/users.py | 1 + 4 files changed, 289 insertions(+), 18 deletions(-) diff --git a/frontend/src/api/users.ts b/frontend/src/api/users.ts index 8a1a45b..105eb9e 100644 --- a/frontend/src/api/users.ts +++ b/frontend/src/api/users.ts @@ -18,6 +18,7 @@ export interface UserCreateRequest { full_name: string is_active?: boolean is_admin?: boolean + give_frontend_permission?: boolean } export interface UserUpdateRequest { diff --git a/frontend/src/pages/ManageUsers.tsx b/frontend/src/pages/ManageUsers.tsx index 829b3f9..f42134d 100644 --- a/frontend/src/pages/ManageUsers.tsx +++ b/frontend/src/pages/ManageUsers.tsx @@ -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 = { + 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('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({ @@ -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() {