Added user creation for the admin integrated with keycloak

This commit is contained in:
gulimabr
2025-12-04 16:36:45 -03:00
parent cdd7668560
commit 18f44c0e85
9 changed files with 556 additions and 3 deletions

View File

@@ -24,6 +24,7 @@
"title": "Member Roles",
"loadingMembers": "Loading members...",
"noMembers": "No members found",
"addUserButton": "+ Add User",
"tableHeaders": {
"user": "User",
"role": "Role",
@@ -33,6 +34,22 @@
"cannotDemoteSelf": "You cannot demote yourself. Ask another admin to change your role.",
"errorUpdating": "Failed to update member role"
},
"createUserModal": {
"title": "Create New User",
"username": "Username",
"usernamePlaceholder": "Enter username",
"email": "Email",
"emailPlaceholder": "Enter email address",
"temporaryPassword": "Temporary Password",
"passwordPlaceholder": "Enter temporary password",
"passwordHint": "User will be required to change this password on first login.",
"firstName": "First Name",
"lastName": "Last Name",
"role": "Role",
"createButton": "Create User",
"successMessage": "User created successfully!",
"errorCreating": "Failed to create user"
},
"relationshipTypes": {
"title": "Relationship Types",
"addButton": "+ Add Type",

View File

@@ -24,6 +24,7 @@
"title": "Funções dos Membros",
"loadingMembers": "Carregando membros...",
"noMembers": "Nenhum membro encontrado",
"addUserButton": "+ Adicionar Usuário",
"tableHeaders": {
"user": "Usuário",
"role": "Função",
@@ -33,6 +34,22 @@
"cannotDemoteSelf": "Você não pode rebaixar a si mesmo. Peça a outro administrador para alterar sua função.",
"errorUpdating": "Falha ao atualizar função do membro"
},
"createUserModal": {
"title": "Criar Novo Usuário",
"username": "Nome de Usuário",
"usernamePlaceholder": "Digite o nome de usuário",
"email": "E-mail",
"emailPlaceholder": "Digite o endereço de e-mail",
"temporaryPassword": "Senha Temporária",
"passwordPlaceholder": "Digite a senha temporária",
"passwordHint": "O usuário será obrigado a alterar esta senha no primeiro login.",
"firstName": "Nome",
"lastName": "Sobrenome",
"role": "Função",
"createButton": "Criar Usuário",
"successMessage": "Usuário criado com sucesso!",
"errorCreating": "Falha ao criar usuário"
},
"relationshipTypes": {
"title": "Tipos de Relacionamento",
"addButton": "+ Adicionar Tipo",

View File

@@ -47,6 +47,7 @@ export default function AdminPage() {
const [showEditRelTypeModal, setShowEditRelTypeModal] = useState(false)
const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false)
const [selectedRelType, setSelectedRelType] = useState<RelationshipType | null>(null)
const [showCreateUserModal, setShowCreateUserModal] = useState(false)
// Relationship type form state
const [relTypeName, setRelTypeName] = useState('')
@@ -55,6 +56,17 @@ export default function AdminPage() {
const [relTypeFormLoading, setRelTypeFormLoading] = useState(false)
const [relTypeFormError, setRelTypeFormError] = useState<string | null>(null)
// Create user form state
const [newUsername, setNewUsername] = useState('')
const [newEmail, setNewEmail] = useState('')
const [newPassword, setNewPassword] = useState('')
const [newFirstName, setNewFirstName] = useState('')
const [newLastName, setNewLastName] = useState('')
const [newRoleId, setNewRoleId] = useState(1)
const [createUserLoading, setCreateUserLoading] = useState(false)
const [createUserError, setCreateUserError] = useState<string | null>(null)
const [createUserSuccess, setCreateUserSuccess] = useState<string | null>(null)
// Check if user is admin
const isAdmin = user?.role_id === 3
@@ -286,6 +298,56 @@ export default function AdminPage() {
setShowDeleteConfirmModal(true)
}
// Reset create user form
const resetCreateUserForm = () => {
setNewUsername('')
setNewEmail('')
setNewPassword('')
setNewFirstName('')
setNewLastName('')
setNewRoleId(1)
setCreateUserError(null)
setCreateUserSuccess(null)
}
// Handle create user
const handleCreateUser = async (e: React.FormEvent) => {
e.preventDefault()
if (!currentProject) return
try {
setCreateUserLoading(true)
setCreateUserError(null)
setCreateUserSuccess(null)
await userService.createUser(currentProject.id, {
username: newUsername.trim(),
email: newEmail.trim(),
password: newPassword,
first_name: newFirstName.trim() || undefined,
last_name: newLastName.trim() || undefined,
role_id: newRoleId,
})
// Refresh members list
const fetchedMembers = await userService.getProjectMembers(currentProject.id)
setMembers(fetchedMembers)
setCreateUserSuccess(t('createUserModal.successMessage'))
// Close modal after a short delay
setTimeout(() => {
setShowCreateUserModal(false)
resetCreateUserForm()
}, 1500)
} catch (err: any) {
console.error('Failed to create user:', err)
setCreateUserError(err.message || t('createUserModal.errorCreating'))
} finally {
setCreateUserLoading(false)
}
}
if (!isAdmin) {
return null
}
@@ -429,7 +491,18 @@ export default function AdminPage() {
{/* Members Tab */}
{activeTab === 'members' && (
<div className="bg-white border border-gray-200 rounded-lg p-6">
<h2 className="text-xl font-semibold text-gray-800 mb-6">{t('memberRoles.title')}</h2>
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-semibold text-gray-800">{t('memberRoles.title')}</h2>
<button
onClick={() => {
resetCreateUserForm()
setShowCreateUserModal(true)
}}
className="px-4 py-2 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700"
>
{t('memberRoles.addUserButton')}
</button>
</div>
{membersError && (
<div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded text-sm">
@@ -812,6 +885,153 @@ export default function AdminPage() {
</div>
</div>
)}
{/* Create User Modal */}
{showCreateUserModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl w-full max-w-md mx-4">
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
<h2 className="text-xl font-semibold text-gray-800">{t('createUserModal.title')}</h2>
<button
onClick={() => {
setShowCreateUserModal(false)
resetCreateUserForm()
}}
className="text-gray-400 hover:text-gray-600"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<form onSubmit={handleCreateUser}>
<div className="px-6 py-4 space-y-4">
{createUserError && (
<div className="p-3 bg-red-100 border border-red-400 text-red-700 rounded text-sm">
{createUserError}
</div>
)}
{createUserSuccess && (
<div className="p-3 bg-green-100 border border-green-400 text-green-700 rounded text-sm">
{createUserSuccess}
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{t('createUserModal.username')} <span className="text-red-500">*</span>
</label>
<input
type="text"
value={newUsername}
onChange={(e) => setNewUsername(e.target.value)}
placeholder={t('createUserModal.usernamePlaceholder')}
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{t('createUserModal.email')} <span className="text-red-500">*</span>
</label>
<input
type="email"
value={newEmail}
onChange={(e) => setNewEmail(e.target.value)}
placeholder={t('createUserModal.emailPlaceholder')}
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{t('createUserModal.temporaryPassword')} <span className="text-red-500">*</span>
</label>
<input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
placeholder={t('createUserModal.passwordPlaceholder')}
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500"
required
minLength={6}
/>
<p className="text-xs text-gray-500 mt-1">
{t('createUserModal.passwordHint')}
</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{t('createUserModal.firstName')}
</label>
<input
type="text"
value={newFirstName}
onChange={(e) => setNewFirstName(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{t('createUserModal.lastName')}
</label>
<input
type="text"
value={newLastName}
onChange={(e) => setNewLastName(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
{t('createUserModal.role')}
</label>
<select
value={newRoleId}
onChange={(e) => setNewRoleId(parseInt(e.target.value))}
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500"
>
{roles.map((role) => (
<option key={role.id} value={role.id}>
{role.display_name}
</option>
))}
</select>
</div>
</div>
<div className="flex justify-end gap-3 px-6 py-4 border-t border-gray-200 bg-gray-50 rounded-b-lg">
<button
type="button"
onClick={() => {
setShowCreateUserModal(false)
resetCreateUserForm()
}}
className="px-4 py-2 border border-gray-400 rounded text-sm font-medium text-gray-700 hover:bg-gray-100"
disabled={createUserLoading}
>
{tCommon('cancel')}
</button>
<button
type="submit"
className="px-4 py-2 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700 disabled:opacity-50"
disabled={createUserLoading}
>
{createUserLoading ? tCommon('creating') : t('createUserModal.createButton')}
</button>
</div>
</form>
</div>
</div>
)}
</div>
)
}

View File

@@ -20,6 +20,25 @@ export interface UserRoleUpdateRequest {
role_id: number
}
export interface UserCreateRequest {
username: string
email: string
password: string
first_name?: string
last_name?: string
role_id: number
}
export interface UserCreateResponse {
id: number
username: string
email: string
full_name: string | null
role_id: number
role_name: string
role_display_name: string
}
class UserService {
/**
* Get all available roles.
@@ -86,6 +105,34 @@ class UserService {
return await response.json()
}
/**
* Create a new user and add them to a project.
* Only admins can call this endpoint.
*/
async createUser(
projectId: number,
userData: UserCreateRequest
): Promise<UserCreateResponse> {
const response = await fetch(
`${API_BASE_URL}/projects/${projectId}/users`,
{
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData),
}
)
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
throw new Error(errorData.detail || `HTTP error! status: ${response.status}`)
}
return await response.json()
}
}
export const userService = new UserService()