diff --git a/ui/src/api/Admin.ts b/ui/src/api/Admin.ts index 4a3edfb..ae1397f 100644 --- a/ui/src/api/Admin.ts +++ b/ui/src/api/Admin.ts @@ -16,9 +16,11 @@ import { DomainAdminUser, DomainCreateAdminReq, DomainExportCompletionDataResp, + DomainGrantRoleReq, DomainListAdminLoginHistoryResp, DomainListAdminUserResp, DomainLoginReq, + DomainRole, DomainSetting, DomainUpdateSettingReq, GetAdminLoginHistoryParams, @@ -244,6 +246,55 @@ export const getAdminProfile = (params: RequestParams = {}) => ...params, }); +/** + * @description 获取角色列表 + * + * @tags Admin + * @name GetListRole + * @summary 获取角色列表 + * @request GET:/api/v1/admin/role + * @response `200` `(WebResp & { + data?: (DomainRole)[], + +})` OK + */ + +export const getListRole = (params: RequestParams = {}) => + request< + WebResp & { + data?: DomainRole[]; + } + >({ + path: `/api/v1/admin/role`, + method: "GET", + type: ContentType.Json, + format: "json", + ...params, + }); + +/** + * @description 授权角色 + * + * @tags Admin + * @name PostGrantRole + * @summary 授权角色 + * @request POST:/api/v1/admin/role + * @response `200` `WebResp` OK + */ + +export const postGrantRole = ( + param: DomainGrantRoleReq, + params: RequestParams = {}, +) => + request({ + path: `/api/v1/admin/role`, + method: "POST", + body: param, + type: ContentType.Json, + format: "json", + ...params, + }); + /** * @description 获取系统设置 * diff --git a/ui/src/api/UserGroup.ts b/ui/src/api/UserGroup.ts new file mode 100644 index 0000000..93968af --- /dev/null +++ b/ui/src/api/UserGroup.ts @@ -0,0 +1,231 @@ +/* eslint-disable */ +/* tslint:disable */ +// @ts-nocheck +/* + * --------------------------------------------------------------- + * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## + * ## ## + * ## AUTHOR: acacode ## + * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## + * --------------------------------------------------------------- + */ + +import request, { ContentType, RequestParams } from "./httpClient"; +import { + DeleteDeleteGroupParams, + DomainCreateUserGroupReq, + DomainGrantGroupReq, + DomainGroupAddUserReq, + DomainGroupRemoveAdminReq, + DomainGroupRemoveUserReq, + DomainListUserGroupResp, + DomainUpdateUserGroupReq, + DomainUserGroup, + GetListUserGroupParams, + WebResp, +} from "./types"; + +/** + * @description 列出用户分组 + * + * @tags UserGroup + * @name GetListUserGroup + * @summary 列出用户分组 + * @request GET:/api/v1/admin/user-group + * @response `200` `(WebResp & { + data?: DomainListUserGroupResp, + +})` OK + */ + +export const getListUserGroup = ( + query: GetListUserGroupParams, + params: RequestParams = {}, +) => + request< + WebResp & { + data?: DomainListUserGroupResp; + } + >({ + path: `/api/v1/admin/user-group`, + method: "GET", + query: query, + type: ContentType.Json, + format: "json", + ...params, + }); + +/** + * @description 更新用户分组 + * + * @tags UserGroup + * @name PutUpdateUserGroup + * @summary 更新用户分组 + * @request PUT:/api/v1/admin/user-group + * @response `200` `(WebResp & { + data?: DomainUserGroup, + +})` OK + */ + +export const putUpdateUserGroup = ( + param: DomainUpdateUserGroupReq, + params: RequestParams = {}, +) => + request< + WebResp & { + data?: DomainUserGroup; + } + >({ + path: `/api/v1/admin/user-group`, + method: "PUT", + body: param, + type: ContentType.Json, + format: "json", + ...params, + }); + +/** + * @description 创建用户分组 + * + * @tags UserGroup + * @name PostCreateGroup + * @summary 创建用户分组 + * @request POST:/api/v1/admin/user-group + * @response `200` `(WebResp & { + data?: DomainUserGroup, + +})` OK + */ + +export const postCreateGroup = ( + param: DomainCreateUserGroupReq, + params: RequestParams = {}, +) => + request< + WebResp & { + data?: DomainUserGroup; + } + >({ + path: `/api/v1/admin/user-group`, + method: "POST", + body: param, + type: ContentType.Json, + format: "json", + ...params, + }); + +/** + * @description 删除用户分组 + * + * @tags UserGroup + * @name DeleteDeleteGroup + * @summary 删除用户分组 + * @request DELETE:/api/v1/admin/user-group + * @response `200` `WebResp` OK + */ + +export const deleteDeleteGroup = ( + query: DeleteDeleteGroupParams, + params: RequestParams = {}, +) => + request({ + path: `/api/v1/admin/user-group`, + method: "DELETE", + query: query, + type: ContentType.Json, + format: "json", + ...params, + }); + +/** + * @description 从分组中移除管理员 + * + * @tags UserGroup + * @name DeleteRemoveAdminFromGroup + * @summary 从分组中移除管理员 + * @request DELETE:/api/v1/admin/user-group/admin + * @response `200` `WebResp` OK + */ + +export const deleteRemoveAdminFromGroup = ( + param: DomainGroupRemoveAdminReq, + params: RequestParams = {}, +) => + request({ + path: `/api/v1/admin/user-group/admin`, + method: "DELETE", + body: param, + type: ContentType.Json, + format: "json", + ...params, + }); + +/** + * @description 授权分组给管理员 + * + * @tags UserGroup + * @name PostGrantGroup + * @summary 授权分组给管理员 + * @request POST:/api/v1/admin/user-group/grant + * @response `200` `WebResp` OK + */ + +export const postGrantGroup = ( + param: DomainGrantGroupReq, + params: RequestParams = {}, +) => + request({ + path: `/api/v1/admin/user-group/grant`, + method: "POST", + body: param, + type: ContentType.Json, + format: "json", + ...params, + }); + +/** + * @description 添加用户到分组 + * + * @tags UserGroup + * @name PostAddUserToGroup + * @summary 添加用户到分组 + * @request POST:/api/v1/admin/user-group/user + * @response `200` `WebResp` OK + */ + +export const postAddUserToGroup = ( + param: DomainGroupAddUserReq, + params: RequestParams = {}, +) => + request({ + path: `/api/v1/admin/user-group/user`, + method: "POST", + body: param, + type: ContentType.Json, + format: "json", + ...params, + }); + +/** + * @description 从分组中移除用户 + * + * @tags UserGroup + * @name DeleteRemoveUserFromGroup + * @summary 从分组中移除用户 + * @request DELETE:/api/v1/admin/user-group/user + * @response `200` `WebResp` OK + */ + +export const deleteRemoveUserFromGroup = ( + param: DomainGroupRemoveUserReq, + params: RequestParams = {}, +) => + request({ + path: `/api/v1/admin/user-group/user`, + method: "DELETE", + body: param, + type: ContentType.Json, + format: "json", + ...params, + }); diff --git a/ui/src/api/index.ts b/ui/src/api/index.ts index c01e459..6fdf622 100644 --- a/ui/src/api/index.ts +++ b/ui/src/api/index.ts @@ -9,6 +9,7 @@ export * from './OpenAiv1' export * from './SecurityScanning' export * from './User' export * from './UserDashboard' +export * from './UserGroup' export * from './UserManage' export * from './UserRecord' export * from './UserSecurityScanning' diff --git a/ui/src/api/types.ts b/ui/src/api/types.ts index ad69672..0ed2cee 100644 --- a/ui/src/api/types.ts +++ b/ui/src/api/types.ts @@ -166,6 +166,8 @@ export interface DomainAdminUser { id?: string; /** 最后活跃时间 */ last_active_at?: number; + /** 角色 */ + role?: DomainRole; /** 用户状态 active: 正常 inactive: 禁用 */ status?: ConstsAdminStatus; /** 用户名 */ @@ -243,7 +245,7 @@ export interface DomainCheckModelReq { api_base: string; api_header?: string; /** 接口密钥 */ - api_key: string; + api_key?: string; api_version?: string; /** 模型名称 */ model_name: string; @@ -366,9 +368,11 @@ export interface DomainCompletionRecord { export interface DomainCreateAdminReq { /** 密码 */ - password?: string; + password: string; + /** 角色ID */ + role_id: number; /** 用户名 */ - username?: string; + username: string; } export interface DomainCreateModelReq { @@ -409,6 +413,11 @@ export interface DomainCreateSecurityScanningReq { workspace?: string; } +export interface DomainCreateUserGroupReq { + /** 组名称 */ + name: string; +} + export interface DomainCreateWorkspaceFileReq { /** 文件内容 */ content?: string; @@ -525,6 +534,41 @@ export interface DomainGetProviderModelListResp { models?: DomainProviderModelListItem[]; } +export interface DomainGrantGroupReq { + /** 管理员ID列表 */ + admin_ids: string[]; + /** 分组ID列表 */ + group_ids: string[]; +} + +export interface DomainGrantRoleReq { + /** 管理员ID */ + admin_id?: string; + /** 角色ID列表 */ + role_ids?: number[]; +} + +export interface DomainGroupAddUserReq { + /** 分组ID列表 */ + group_ids: string[]; + /** 用户ID列表 */ + user_ids: string[]; +} + +export interface DomainGroupRemoveAdminReq { + /** 管理员ID列表 */ + admin_ids: string[]; + /** 分组ID */ + group_id: string; +} + +export interface DomainGroupRemoveUserReq { + /** 分组ID */ + group_id: string; + /** 用户ID列表 */ + user_ids: string[]; +} + export interface DomainIPInfo { /** ASN */ asn?: string; @@ -641,6 +685,13 @@ export interface DomainListSecurityScanningResp { total_count?: number; } +export interface DomainListUserGroupResp { + groups?: DomainUserGroup[]; + has_next_page?: boolean; + next_token?: string; + total_count?: number; +} + export interface DomainListUserResp { has_next_page?: boolean; next_token?: string; @@ -832,6 +883,12 @@ export interface DomainReportReq { user_input?: string; } +export interface DomainRole { + description?: string; + id?: number; + name?: string; +} + export interface DomainSecurityScanningBrief { /** 创建时间 */ created_at?: number; @@ -1049,6 +1106,12 @@ export interface DomainUpdateSettingReq { force_two_factor_auth?: boolean; } +export interface DomainUpdateUserGroupReq { + /** 分组id */ + id: string; + name: string; +} + export interface DomainUpdateUserReq { /** 用户ID */ id: string; @@ -1108,6 +1171,16 @@ export interface DomainUserEvent { name?: string; } +export interface DomainUserGroup { + /** 关联的管理员 */ + admins?: DomainAdminUser[]; + id?: string; + /** 组名 */ + name?: string; + /** 关联的用户 */ + users?: DomainUser[]; +} + export interface DomainUserHeatmap { count?: number; date?: number; @@ -1265,6 +1338,20 @@ export interface GetAdminLoginHistoryParams { size?: number; } +export interface GetListUserGroupParams { + /** 下一页标识 */ + next_token?: string; + /** 分页 */ + page?: number; + /** 每页多少条记录 */ + size?: number; +} + +export interface DeleteDeleteGroupParams { + /** 分组id */ + id: string; +} + export interface GetChatInfoParams { /** 对话记录ID */ id: string; diff --git a/ui/src/pages/generalSetting/components/adminUser.tsx b/ui/src/pages/generalSetting/components/adminUser.tsx index 207386a..5c78880 100644 --- a/ui/src/pages/generalSetting/components/adminUser.tsx +++ b/ui/src/pages/generalSetting/components/adminUser.tsx @@ -6,10 +6,15 @@ import { IconButton, TextField, Paper, + Select, + MenuItem, + FormControl, + InputLabel, } from '@mui/material'; +import { AdminPanelSettings, Person } from '@mui/icons-material'; import { postCreateAdmin } from '@/api/Admin'; import { CopyToClipboard } from 'react-copy-to-clipboard'; -import { deleteDeleteAdmin, getListAdminUser } from '@/api/Admin'; +import { deleteDeleteAdmin, getAdminProfile, getListAdminUser, postGrantRole } from '@/api/Admin'; import { Table, Modal, message } from '@c-x/ui'; import { ColumnsType } from '@c-x/ui/dist/Table'; import { useRequest } from 'ahooks'; @@ -44,6 +49,7 @@ const AddAdminModal = ({ postCreateAdmin({ username: data.username, password, + role_id: data.roleId, // 添加 role_id 参数 }).then(() => { setIsSuccess(true); message.success('添加成功'); @@ -53,7 +59,9 @@ const AddAdminModal = ({ useEffect(() => { if (open) { - reset(); + reset({ + roleId: 2, // 设置默认角色为普通管理员 + }); } else { setIsSuccess(false); setPassword(Math.random().toString(36).substring(2, 15)); @@ -85,6 +93,29 @@ const AddAdminModal = ({ /> )} /> + ( + + 角色 + + {errors.roleId && ( + + {errors.roleId?.message as string} + + )} + + )} + /> { const [open, setOpen] = useState(false); const { data, loading, refresh } = useRequest(() => getListAdminUser({})); + const { data: currentAdmin } = useRequest(() => getAdminProfile({})); + + const handleRoleChange = (adminId: string, newRoleId: number) => { + postGrantRole({ + admin_id: adminId, + role_ids: [newRoleId], + }).then(() => { + message.success('角色更新成功'); + refresh(); + }).catch(() => { + message.error('角色更新失败'); + }); + }; + const onDeleteAdmin = (data: DomainAdminUser) => { Modal.confirm({ title: '提示', @@ -187,21 +232,65 @@ const AdminUser = () => { }, }, { - title: '加入时间', - dataIndex: 'created_at', - width: 140, - render: (text) => { - return dayjs.unix(text).fromNow(); + title: '角色', + dataIndex: 'last_active_at', + render: (text, record) => { + return ( + + + + ); }, }, { - title: '最近活跃', - dataIndex: 'last_active_at', - width: 140, + title: '活跃时间', + dataIndex: 'created_at', + width: 120, render: (text, record) => { - return record.last_active_at === 0 - ? '从未使用' - : dayjs.unix(text).fromNow(); + return + {dayjs.unix(text).fromNow()} + {record.last_active_at === 0 ? '从未使用' : dayjs.unix(text).fromNow()} + }, }, { diff --git a/ui/src/pages/memberManage/createGroupModal.tsx b/ui/src/pages/memberManage/createGroupModal.tsx new file mode 100644 index 0000000..4c5cb73 --- /dev/null +++ b/ui/src/pages/memberManage/createGroupModal.tsx @@ -0,0 +1,58 @@ +import React, { useState } from 'react'; +import { Modal, message } from '@c-x/ui'; +import { Box, TextField } from '@mui/material'; +import { postCreateGroup } from '@/api/UserGroup'; + +const CreateGroupModal = ({ + open, + onClose, + onCreated, // 添加一个回调函数,用于在创建成功后刷新列表 +}: { + open: boolean; + onClose: () => void; + onCreated?: () => void; // 可选的回调函数 +}) => { + const [groupName, setGroupName] = useState(''); + + const handleCreate = async () => { + if (!groupName.trim()) { + message.error('请输入成员组名称'); + return; + } + + try { + await postCreateGroup({ name: groupName }); + message.success('成员组创建成功'); + setGroupName(''); // 清空输入框 + onCreated?.(); // 调用回调函数刷新列表 + onClose(); // 关闭弹窗 + } catch (error) { + console.error('创建成员组失败:', error); + message.error('创建成员组失败'); + } + }; + + return ( + + + setGroupName(e.target.value)} + variant='outlined' + /> + + + ); +}; + +export default CreateGroupModal; diff --git a/ui/src/pages/memberManage/groupList.tsx b/ui/src/pages/memberManage/groupList.tsx new file mode 100644 index 0000000..8c93dee --- /dev/null +++ b/ui/src/pages/memberManage/groupList.tsx @@ -0,0 +1,309 @@ +import React, { useEffect, useState } from 'react'; +import Card from '@/components/card'; +import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; +import { useRequest } from 'ahooks'; +import { getListUser } from '@/api/User'; +import { + Stack, + Box, + Button, + IconButton, + Menu, + MenuItem, + FormControl, + InputLabel, + Select, + Chip, + SelectChangeEvent, +} from '@mui/material'; +import { Table, Modal, message } from '@c-x/ui'; +import { ColumnsType } from '@c-x/ui/dist/Table'; +import { DomainAdminUser, DomainUser, DomainUserGroup } from '@/api/types'; +import { deleteDeleteGroup, getListAdminUser, getListUserGroup } from '@/api'; +import { deleteRemoveAdminFromGroup, postGrantGroup, postAddUserToGroup, deleteRemoveUserFromGroup, putUpdateUserGroup } from '@/api/UserGroup'; +import CreateGroupModal from './createGroupModal'; +import UpdateGroupModal from './updateGroupModal'; +import { Check } from '@mui/icons-material'; + + +const GroupList = () => { + const [openCreateGroupModal, setOpenCreateGroupModal] = useState(false); + const [openUpdateGroupModal, setOpenUpdateGroupModal] = useState(false); + const [currentGroup, setCurrentGroup] = useState(null); + const groupData = useRequest(() => getListUserGroup({})); + const userData = useRequest(() => getListUser({})); + const adminData = useRequest(() => getListAdminUser({})); + + const [anchorEl, setAnchorEl] = useState(null); + + const handleClick = ( + event: React.MouseEvent, + record: DomainUserGroup + ) => { + setAnchorEl(event.currentTarget); + setCurrentGroup(record); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const onDeleteGroup = () => { + handleClose(); + Modal.confirm({ + title: '提示', + okText: '删除', + okButtonProps: { + color: 'error', + }, + content: ( + <> + 确定要删除该成员组{' '} + + {currentGroup!.name} + {' '} + 吗? + + ), + onOk: () => { + deleteDeleteGroup({ id: currentGroup!.id! }).then(() => { + message.success('删除成功'); + groupData.refresh(); + }); + }, + }); + }; + + const onUpdateGroup = () => { + handleClose(); + setOpenUpdateGroupModal(true); + }; + + const columns: ColumnsType = [ + { + title: '成员组', + dataIndex: 'name', + width: 120, + render: (text) => { + return text; + }, + }, + { + title: '成员', + dataIndex: 'users', + render: (users, record) => { + return + 成员 + + ; + }, + }, + { + title: '管理员', + dataIndex: 'admins', + render: (admins, record) => { + return + 管理员 + + ; + }, + }, + { + title: '操作', + dataIndex: 'action', + width: 80, + render: (_, record) => { + return ( + handleClick(e, record)} + sx={{ + '&:hover': { + color: 'info.main', + }, + }} + > + + + ); + }, + }, + ]; + return ( + + + + 修改成员组名称 + + + 删除成员组 + + + + + 成员组 + + + + + + setOpenCreateGroupModal(false)} + onCreated={() => groupData.refresh()} // 添加 onCreated 回调函数 + /> + setOpenUpdateGroupModal(false)} + onUpdated={() => groupData.refresh()} + group={currentGroup} + /> + + ); +}; + +export default GroupList; diff --git a/ui/src/pages/memberManage/index.tsx b/ui/src/pages/memberManage/index.tsx index c4c5757..b5d0172 100644 --- a/ui/src/pages/memberManage/index.tsx +++ b/ui/src/pages/memberManage/index.tsx @@ -14,6 +14,7 @@ import MemberManage from './memberManage'; import LoginHistory from './loginHistory'; import { message } from '@c-x/ui'; import ThirdPartyLoginSettingModal from './thirdPartyLoginSettingModal'; +import GroupList from './groupList'; const StyledCard = styled(Card)({ display: 'flex', @@ -65,14 +66,17 @@ const User = () => { - + + + + 强制启用两步认证 { updateSetting({ force_two_factor_auth: e.target.checked }); }} @@ -81,7 +85,7 @@ const User = () => { 禁止使用密码登录 updateSetting({ disable_password_login: e.target.checked }) } @@ -90,7 +94,7 @@ const User = () => { 开放自主注册(无需邀请) updateSetting({ enable_auto_login: e.target.checked }) } diff --git a/ui/src/pages/memberManage/loginHistory.tsx b/ui/src/pages/memberManage/loginHistory.tsx index b5eec7b..887c821 100644 --- a/ui/src/pages/memberManage/loginHistory.tsx +++ b/ui/src/pages/memberManage/loginHistory.tsx @@ -98,7 +98,7 @@ const LoginHistory = () => { }, ]; return ( - + { }, }, { - title: '', + title: '操作', dataIndex: 'action', width: 80, render: (_, record) => { @@ -302,7 +302,7 @@ const MemberManage = () => { }, ]; return ( - + {currentUser?.status === ConstsUserStatus.UserStatusActive && ( 锁定成员 diff --git a/ui/src/pages/memberManage/updateGroupModal.tsx b/ui/src/pages/memberManage/updateGroupModal.tsx new file mode 100644 index 0000000..6e53580 --- /dev/null +++ b/ui/src/pages/memberManage/updateGroupModal.tsx @@ -0,0 +1,70 @@ +import React, { useState, useEffect } from 'react'; +import { Modal, message } from '@c-x/ui'; +import { Box, TextField } from '@mui/material'; +import { putUpdateUserGroup } from '@/api/UserGroup'; +import { DomainUserGroup } from '@/api/types'; + +interface UpdateGroupModalProps { + open: boolean; + onClose: () => void; + onUpdated?: () => void; + group: DomainUserGroup | null; +} + +const UpdateGroupModal = ({ open, onClose, onUpdated, group }: UpdateGroupModalProps) => { + const [groupName, setGroupName] = useState(''); + + // 当group变化时,更新表单中的组名 + useEffect(() => { + if (group) { + setGroupName(group.name || ''); + } + }, [group]); + + const handleUpdate = async () => { + if (!groupName.trim()) { + message.error('请输入成员组名称'); + return; + } + + if (!group?.id) { + message.error('无效的成员组'); + return; + } + + try { + await putUpdateUserGroup({ id: group.id, name: groupName }); + message.success('成员组名称更新成功'); + setGroupName(''); // 清空输入框 + onUpdated?.(); // 调用回调函数刷新列表 + onClose(); // 关闭弹窗 + } catch (error) { + console.error('更新成员组名称失败:', error); + message.error('更新成员组名称失败'); + } + }; + + return ( + + + setGroupName(e.target.value)} + variant='outlined' + /> + + + ); +}; + +export default UpdateGroupModal; \ No newline at end of file