From 29b32ec3171c2af4e8675ba7a906996688387153 Mon Sep 17 00:00:00 2001 From: Mathias Wagner Date: Tue, 9 Sep 2025 13:42:49 +0200 Subject: [PATCH] Craete UserManagement page --- .../pages/UserManagement/UserManagement.jsx | 386 ++++++++++++++++++ webui/src/pages/UserManagement/index.js | 1 + webui/src/pages/UserManagement/styles.sass | 39 ++ 3 files changed, 426 insertions(+) create mode 100644 webui/src/pages/UserManagement/UserManagement.jsx create mode 100644 webui/src/pages/UserManagement/index.js create mode 100644 webui/src/pages/UserManagement/styles.sass diff --git a/webui/src/pages/UserManagement/UserManagement.jsx b/webui/src/pages/UserManagement/UserManagement.jsx new file mode 100644 index 0000000..913bad1 --- /dev/null +++ b/webui/src/pages/UserManagement/UserManagement.jsx @@ -0,0 +1,386 @@ +import React, {useState, useEffect, useContext} from 'react'; +import {UserContext} from '@/common/contexts/UserContext.jsx'; +import {useToast} from '@/common/contexts/ToastContext.jsx'; +import {getRequest, postRequest, putRequest, deleteRequest} from '@/common/utils/RequestUtil.js'; +import Button from '@/common/components/Button'; +import Input from '@/common/components/Input'; +import Select from '@/common/components/Select'; +import Modal, {ModalActions} from '@/common/components/Modal'; +import Card, {CardHeader, CardBody} from '@/common/components/Card'; +import Badge from '@/common/components/Badge'; +import Grid from '@/common/components/Grid'; +import LoadingSpinner from '@/common/components/LoadingSpinner'; +import EmptyState from '@/common/components/EmptyState'; +import PageHeader from '@/common/components/PageHeader'; +import Avatar from '@/common/components/Avatar'; +import DetailItem, {DetailList} from '@/common/components/DetailItem'; +import FormError from '@/common/components/FormError'; +import { + PlusIcon, + PencilIcon, + TrashIcon, + UserIcon, + ShieldCheckIcon, + CalendarIcon, + HardDriveIcon +} from '@phosphor-icons/react'; +import './styles.sass'; + +const UserManagement = () => { + const {user: currentUser} = useContext(UserContext); + const toast = useToast(); + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(true); + const [showModal, setShowModal] = useState(false); + const [editingUser, setEditingUser] = useState(null); + const [formData, setFormData] = useState({ + username: '', + password: '', + role: 'user', + storage_limit_gb: 10 + }); + const [formErrors, setFormErrors] = useState({}); + const [submitting, setSubmitting] = useState(false); + + useEffect(() => { + fetchUsers(); + }, []); + + const fetchUsers = async () => { + try { + setLoading(true); + const response = await getRequest('admin/users'); + setUsers(response); + } catch (error) { + console.error('Failed to fetch users:', error); + toast.error('Failed to load users. Please try again.'); + } finally { + setLoading(false); + } + }; + + const formatDate = (dateString) => { + return new Date(dateString).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + }; + + const openCreateModal = () => { + setEditingUser(null); + setFormData({ + username: '', + password: '', + role: 'user', + storage_limit_gb: 10 + }); + setFormErrors({}); + setShowModal(true); + }; + + const openEditModal = (user) => { + setEditingUser(user); + setFormData({ + username: user.username, + password: '', + role: user.role, + storage_limit_gb: user.storage_limit_gb + }); + setFormErrors({}); + setShowModal(true); + }; + + const closeModal = () => { + setShowModal(false); + setEditingUser(null); + setFormData({ + username: '', + password: '', + role: 'user', + storage_limit_gb: 10 + }); + setFormErrors({}); + }; + + const validateForm = () => { + const errors = {}; + + if (!formData.username.trim()) { + errors.username = 'Username is required'; + } else if (formData.username.length < 3) { + errors.username = 'Username must be at least 3 characters'; + } + + if (!editingUser && !formData.password) { + errors.password = 'Password is required'; + } else if (formData.password && formData.password.length < 6) { + errors.password = 'Password must be at least 6 characters'; + } + + if (formData.storage_limit_gb < 0) { + errors.storage_limit_gb = 'Storage limit cannot be negative'; + } + + setFormErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + + if (!validateForm()) { + return; + } + + setSubmitting(true); + + try { + if (editingUser) { + const updateData = { + username: formData.username, + role: formData.role, + storage_limit_gb: parseInt(formData.storage_limit_gb) + }; + + if (formData.password) { + updateData.password = formData.password; + } + + await putRequest(`admin/users/${editingUser.id}`, updateData); + toast.success(`User "${formData.username}" updated successfully`); + } else { + await postRequest('admin/users', { + username: formData.username, + password: formData.password, + role: formData.role, + storage_limit_gb: parseInt(formData.storage_limit_gb) + }); + toast.success(`User "${formData.username}" created successfully`); + } + + closeModal(); + fetchUsers(); + } catch (error) { + console.error('Failed to save user:', error); + const errorMessage = error.error || 'Failed to save user. Please try again.'; + toast.error(errorMessage); + if (error.error) { + setFormErrors({general: error.error}); + } + } finally { + setSubmitting(false); + } + }; + + const handleDelete = async (userId, username) => { + if (userId === currentUser.id) { + toast.warning('You cannot delete your own account'); + return; + } + + if (!window.confirm(`Are you sure you want to delete user "${username}"? This action cannot be undone.`)) { + return; + } + + try { + await deleteRequest(`admin/users/${userId}`); + toast.success(`User "${username}" deleted successfully`); + fetchUsers(); + } catch (error) { + console.error('Failed to delete user:', error); + toast.error('Failed to delete user. Please try again.'); + } + }; + + const handleInputChange = (e) => { + const {name, value} = e.target; + setFormData(prev => ({ + ...prev, + [name]: value + })); + + if (formErrors[name]) { + setFormErrors(prev => ({ + ...prev, + [name]: '' + })); + } + }; + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+ } + onClick={openCreateModal} + > + Add User + + } + /> + + + {users.map(user => ( + + +
+ + + +
+

{user.username}

+
+ + + {user.role} + +
+
+
+ + {user.id !== currentUser.id && ( + + )} +
+
+
+ + + + }> + Storage Limit: {user.storage_limit_gb}GB + + }> + Created: {formatDate(user.created_at)} + + + +
+ ))} +
+ + {users.length === 0 && ( + } + title="No users found" + description="Get started by creating your first user" + action={ + + } + /> + )} + + +
+ {formErrors.general} + + + + + + + + + + + +
+
+
+ ); +}; + +export default UserManagement; \ No newline at end of file diff --git a/webui/src/pages/UserManagement/index.js b/webui/src/pages/UserManagement/index.js new file mode 100644 index 0000000..b5ae5b4 --- /dev/null +++ b/webui/src/pages/UserManagement/index.js @@ -0,0 +1 @@ +export { default } from './UserManagement.jsx'; \ No newline at end of file diff --git a/webui/src/pages/UserManagement/styles.sass b/webui/src/pages/UserManagement/styles.sass new file mode 100644 index 0000000..4e8d142 --- /dev/null +++ b/webui/src/pages/UserManagement/styles.sass @@ -0,0 +1,39 @@ +.user-card-header + display: flex + align-items: flex-start + gap: 1rem + +.user-info + flex: 1 + min-width: 0 + +.user-name + font-size: 1.1rem + font-weight: 600 + color: var(--text) + margin-bottom: 0.5rem + +.user-role + display: flex + align-items: center + gap: 0.5rem + color: var(--text-dim) + font-size: 0.85rem + +.user-actions + display: flex + gap: 0.5rem + flex-shrink: 0 + +.modal-form + display: flex + flex-direction: column + gap: 1.25rem + +@media (max-width: 768px) + .user-card-header + flex-direction: column + text-align: center + + .user-actions + justify-content: center \ No newline at end of file