From 198ea85225a25c5e847950ebd0841c2a76057f5b Mon Sep 17 00:00:00 2001 From: samanhappy Date: Thu, 2 Oct 2025 15:11:08 +0800 Subject: [PATCH] feat: implement user management features with add, edit, and delete functionality --- frontend/src/components/AddUserForm.tsx | 153 +++++++++++++++++++++ frontend/src/components/EditUserForm.tsx | 161 +++++++++++++++++++++++ frontend/src/components/UserCard.tsx | 96 ++++++++++++++ frontend/src/hooks/useUserData.ts | 100 ++++++++++++++ frontend/src/pages/UsersPage.tsx | 121 ++++++++++++++++- src/services/dataServicex.ts | 71 ++++++++++ 6 files changed, 700 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/AddUserForm.tsx create mode 100644 frontend/src/components/EditUserForm.tsx create mode 100644 frontend/src/components/UserCard.tsx create mode 100644 frontend/src/hooks/useUserData.ts create mode 100644 src/services/dataServicex.ts diff --git a/frontend/src/components/AddUserForm.tsx b/frontend/src/components/AddUserForm.tsx new file mode 100644 index 0000000..4dfc10c --- /dev/null +++ b/frontend/src/components/AddUserForm.tsx @@ -0,0 +1,153 @@ +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useUserData } from '@/hooks/useUserData'; +import { UserFormData } from '@/types'; + +interface AddUserFormProps { + onAdd: () => void; + onCancel: () => void; +} + +const AddUserForm = ({ onAdd, onCancel }: AddUserFormProps) => { + const { t } = useTranslation(); + const { createUser } = useUserData(); + const [error, setError] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + const [formData, setFormData] = useState({ + username: '', + password: '', + isAdmin: false, + }); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + + if (!formData.username.trim()) { + setError(t('users.usernameRequired')); + return; + } + + if (!formData.password.trim()) { + setError(t('users.passwordRequired')); + return; + } + + if (formData.password.length < 6) { + setError(t('users.passwordTooShort')); + return; + } + + setIsSubmitting(true); + + try { + const result = await createUser(formData); + if (result?.success) { + onAdd(); + } else { + setError(result?.message || t('users.createError')); + } + } catch (err) { + setError(err instanceof Error ? err.message : t('users.createError')); + } finally { + setIsSubmitting(false); + } + }; + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value, type, checked } = e.target; + setFormData(prev => ({ + ...prev, + [name]: type === 'checkbox' ? checked : value + })); + }; + + return ( +
+
+
+

{t('users.addNew')}

+ + {error && ( +
+

{error}

+
+ )} + +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ + +
+
+
+
+ ); +}; + +export default AddUserForm; diff --git a/frontend/src/components/EditUserForm.tsx b/frontend/src/components/EditUserForm.tsx new file mode 100644 index 0000000..68f409a --- /dev/null +++ b/frontend/src/components/EditUserForm.tsx @@ -0,0 +1,161 @@ +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useUserData } from '@/hooks/useUserData'; +import { User, UserUpdateData } from '@/types'; + +interface EditUserFormProps { + user: User; + onEdit: () => void; + onCancel: () => void; +} + +const EditUserForm = ({ user, onEdit, onCancel }: EditUserFormProps) => { + const { t } = useTranslation(); + const { updateUser } = useUserData(); + const [error, setError] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + const [formData, setFormData] = useState({ + isAdmin: user.isAdmin, + newPassword: '', + confirmPassword: '', + }); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + + // Validate passwords match if changing password + if (formData.newPassword && formData.newPassword !== formData.confirmPassword) { + setError(t('users.passwordMismatch')); + return; + } + + if (formData.newPassword && formData.newPassword.length < 6) { + setError(t('users.passwordTooShort')); + return; + } + + setIsSubmitting(true); + + try { + const updateData: UserUpdateData = { + isAdmin: formData.isAdmin, + }; + + if (formData.newPassword) { + updateData.newPassword = formData.newPassword; + } + + const result = await updateUser(user.username, updateData); + if (result?.success) { + onEdit(); + } else { + setError(result?.message || t('users.updateError')); + } + } catch (err) { + setError(err instanceof Error ? err.message : t('users.updateError')); + } finally { + setIsSubmitting(false); + } + }; + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value, type, checked } = e.target; + setFormData(prev => ({ + ...prev, + [name]: type === 'checkbox' ? checked : value + })); + }; + + return ( +
+
+
+

+ {t('users.edit')} - {user.username} +

+ + {error && ( +
+

{error}

+
+ )} + +
+
+ + +
+ +
+ + +
+ + {formData.newPassword && ( +
+ + +
+ )} +
+ +
+ + +
+
+
+
+ ); +}; + +export default EditUserForm; diff --git a/frontend/src/components/UserCard.tsx b/frontend/src/components/UserCard.tsx new file mode 100644 index 0000000..f2951a9 --- /dev/null +++ b/frontend/src/components/UserCard.tsx @@ -0,0 +1,96 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { User, IUser } from '@/types'; +import { Edit, Trash } from '@/components/icons/LucideIcons'; +import DeleteDialog from '@/components/ui/DeleteDialog'; + +interface UserCardProps { + user: User; + currentUser: IUser | null; + onEdit: (user: User) => void; + onDelete: (username: string) => void; +} + +const UserCard: React.FC = ({ user, currentUser, onEdit, onDelete }) => { + const { t } = useTranslation(); + const [showDeleteDialog, setShowDeleteDialog] = useState(false); + + const handleDeleteClick = () => { + setShowDeleteDialog(true); + }; + + const handleConfirmDelete = () => { + onDelete(user.username); + setShowDeleteDialog(false); + }; + + const isCurrentUser = currentUser?.username === user.username; + const canDelete = !isCurrentUser; // Can't delete own account + + return ( +
+
+
+
+
+ + {user.username.charAt(0).toUpperCase()} + +
+
+

+ {user.username} + {isCurrentUser && ( + + {t('users.currentUser')} + + )} +

+
+ + {user.isAdmin ? t('users.admin') : t('users.user')} + +
+
+
+
+ +
+ + + {canDelete && ( + + )} +
+
+ + setShowDeleteDialog(false)} + onConfirm={handleConfirmDelete} + serverName={user.username} + isGroup={false} + isUser={true} + /> +
+ ); +}; + +export default UserCard; diff --git a/frontend/src/hooks/useUserData.ts b/frontend/src/hooks/useUserData.ts new file mode 100644 index 0000000..d19fcce --- /dev/null +++ b/frontend/src/hooks/useUserData.ts @@ -0,0 +1,100 @@ +import { useState, useEffect, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { User, ApiResponse, UserFormData, UserUpdateData } from '@/types'; +import { apiDelete, apiGet, apiPost, apiPut } from '../utils/fetchInterceptor'; + +export const useUserData = () => { + const { t } = useTranslation(); + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [refreshKey, setRefreshKey] = useState(0); + + const fetchUsers = useCallback(async () => { + try { + setLoading(true); + const data: ApiResponse = await apiGet('/users'); + if (!data.success) { + setError(data.message || t('users.fetchError')); + return; + } + + if (data && data.success && Array.isArray(data.data)) { + setUsers(data.data); + } else { + console.error('Invalid user data format:', data); + setUsers([]); + } + + setError(null); + } catch (err) { + console.error('Error fetching users:', err); + setError(err instanceof Error ? err.message : 'Failed to fetch users'); + setUsers([]); + } finally { + setLoading(false); + } + }, []); + + // Trigger a refresh of the users data + const triggerRefresh = useCallback(() => { + setRefreshKey((prev) => prev + 1); + }, []); + + // Create a new user + const createUser = async (userData: UserFormData) => { + try { + const result: ApiResponse = await apiPost('/users', userData); + triggerRefresh(); + return result; + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to create user'); + return null; + } + }; + + // Update an existing user + const updateUser = async (username: string, data: UserUpdateData) => { + try { + const result: ApiResponse = await apiPut(`/users/${username}`, data); + triggerRefresh(); + return result || null; + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to update user'); + return null; + } + }; + + // Delete a user + const deleteUser = async (username: string) => { + try { + const result = await apiDelete(`/users/${username}`); + if (!result?.success) { + setError(result?.message || t('users.deleteError')); + return result; + } + + triggerRefresh(); + return result; + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to delete user'); + return false; + } + }; + + // Fetch users when the component mounts or refreshKey changes + useEffect(() => { + fetchUsers(); + }, [fetchUsers, refreshKey]); + + return { + users, + loading, + error, + setError, + triggerRefresh, + createUser, + updateUser, + deleteUser, + }; +}; diff --git a/frontend/src/pages/UsersPage.tsx b/frontend/src/pages/UsersPage.tsx index 4d8ee0c..c47e745 100644 --- a/frontend/src/pages/UsersPage.tsx +++ b/frontend/src/pages/UsersPage.tsx @@ -1,8 +1,125 @@ -import React from 'react'; +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { User } from '@/types'; +import { useUserData } from '@/hooks/useUserData'; +import { useAuth } from '@/contexts/AuthContext'; +import AddUserForm from '@/components/AddUserForm'; +import EditUserForm from '@/components/EditUserForm'; +import UserCard from '@/components/UserCard'; const UsersPage: React.FC = () => { + const { t } = useTranslation(); + const { auth } = useAuth(); + const currentUser = auth.user; + const { + users, + loading: usersLoading, + error: userError, + setError: setUserError, + deleteUser, + triggerRefresh + } = useUserData(); + + const [editingUser, setEditingUser] = useState(null); + const [showAddForm, setShowAddForm] = useState(false); + + // Check if current user is admin + if (!currentUser?.isAdmin) { + return ( +
+

{t('users.adminRequired')}

+
+ ); + } + + const handleEditClick = (user: User) => { + setEditingUser(user); + }; + + const handleEditComplete = () => { + setEditingUser(null); + triggerRefresh(); // Refresh the users list after editing + }; + + const handleDeleteUser = async (username: string) => { + const result = await deleteUser(username); + if (!result?.success) { + setUserError(result?.message || t('users.deleteError')); + } + }; + + const handleAddUser = () => { + setShowAddForm(true); + }; + + const handleAddComplete = () => { + setShowAddForm(false); + triggerRefresh(); // Refresh the users list after adding + }; + return ( -
+
+
+

{t('pages.users.title')}

+
+ +
+
+ + {userError && ( +
+

{userError}

+
+ )} + + {usersLoading ? ( +
+
+ + + + +

{t('app.loading')}

+
+
+ ) : users.length === 0 ? ( +
+

{t('users.noUsers')}

+
+ ) : ( +
+ {users.map((user) => ( + + ))} +
+ )} + + {showAddForm && ( + + )} + + {editingUser && ( + setEditingUser(null)} + /> + )} +
); }; diff --git a/src/services/dataServicex.ts b/src/services/dataServicex.ts new file mode 100644 index 0000000..0dfd5f7 --- /dev/null +++ b/src/services/dataServicex.ts @@ -0,0 +1,71 @@ +import { IUser, McpSettings, UserConfig } from '../types/index.js'; +import { DataService } from './dataService.js'; +import { UserContextService } from './userContextService.js'; + +export class DataServicex implements DataService { + foo() { + console.log('default implementation'); + } + + filterData(data: any[], user?: IUser): any[] { + // Use passed user parameter if available, otherwise fall back to context + const currentUser = user || UserContextService.getInstance().getCurrentUser(); + if (!currentUser || currentUser.isAdmin) { + return data; + } else { + return data.filter((item) => item.owner === currentUser?.username); + } + } + + filterSettings(settings: McpSettings, user?: IUser): McpSettings { + // Use passed user parameter if available, otherwise fall back to context + const currentUser = user || UserContextService.getInstance().getCurrentUser(); + if (!currentUser || currentUser.isAdmin) { + const result = { ...settings }; + delete result.userConfigs; + return result; + } else { + const result = { ...settings }; + result.systemConfig = settings.userConfigs?.[currentUser?.username || ''] || {}; + delete result.userConfigs; + return result; + } + } + + mergeSettings(all: McpSettings, newSettings: McpSettings, user?: IUser): McpSettings { + // Use passed user parameter if available, otherwise fall back to context + const currentUser = user || UserContextService.getInstance().getCurrentUser(); + if (!currentUser || currentUser.isAdmin) { + const result = { ...all }; + result.users = newSettings.users; + result.systemConfig = newSettings.systemConfig; + return result; + } else { + const result = JSON.parse(JSON.stringify(all)); + if (!result.userConfigs) { + result.userConfigs = {}; + } + const systemConfig = newSettings.systemConfig || {}; + const userConfig: UserConfig = { + routing: systemConfig.routing + ? { + enableGlobalRoute: systemConfig.routing.enableGlobalRoute, + enableGroupNameRoute: systemConfig.routing.enableGroupNameRoute, + enableBearerAuth: systemConfig.routing.enableBearerAuth, + bearerAuthKey: systemConfig.routing.bearerAuthKey, + } + : undefined, + }; + result.userConfigs[currentUser?.username || ''] = userConfig; + return result; + } + } + + getPermissions(user: IUser): string[] { + if (user && user.isAdmin) { + return ['*', 'x']; + } else { + return ['']; + } + } +}