Added user creation for the admin integrated with keycloak
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user