diff --git a/main.go b/main.go index d7f5e87..f5fffd9 100644 --- a/main.go +++ b/main.go @@ -135,6 +135,12 @@ func Run() error { if err := global.DB.AutoMigrate(&model.Resource{}); err != nil { return err } + if err := global.DB.AutoMigrate(&model.UserGroup{}); err != nil { + return err + } + if err := global.DB.AutoMigrate(&model.UserGroupMember{}); err != nil { + return err + } if err := global.DB.AutoMigrate(&model.Num{}); err != nil { return err } diff --git a/pkg/api/routes.go b/pkg/api/routes.go index ec34233..b8fd34d 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -51,6 +51,17 @@ func SetupRoutes() *echo.Echo { users.GET("/:id", UserGetEndpoint) } + userGroups := e.Group("/user-groups") + { + userGroups.POST("", UserGroupCreateEndpoint) + userGroups.GET("/paging", UserGroupPagingEndpoint) + userGroups.PUT("/:id", UserGroupUpdateEndpoint) + userGroups.DELETE("/:id", UserGroupDeleteEndpoint) + userGroups.GET("/:id", UserGroupGetEndpoint) + userGroups.POST("/:id/members", UserGroupAddMembersEndpoint) + userGroups.DELETE("/:id/members/:memberId", UserGroupDelMembersEndpoint) + } + assets := e.Group("/assets", Auth) { assets.GET("", AssetAllEndpoint) diff --git a/pkg/api/user-group.go b/pkg/api/user-group.go new file mode 100644 index 0000000..24831af --- /dev/null +++ b/pkg/api/user-group.go @@ -0,0 +1,114 @@ +package api + +import ( + "github.com/labstack/echo/v4" + "next-terminal/pkg/global" + "next-terminal/pkg/model" + "next-terminal/pkg/utils" + "strconv" + "strings" +) + +type UserGroup struct { + Name string `json:"name"` + Members []string `json:"members"` +} + +func UserGroupCreateEndpoint(c echo.Context) error { + var item UserGroup + if err := c.Bind(&item); err != nil { + return err + } + + userGroup := model.UserGroup{ + ID: utils.UUID(), + Created: utils.NowJsonTime(), + Name: item.Name, + } + + if err := model.CreateNewUserGroup(&userGroup, item.Members); err != nil { + return err + } + + return Success(c, item) +} + +func UserGroupPagingEndpoint(c echo.Context) error { + pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex")) + pageSize, _ := strconv.Atoi(c.QueryParam("pageSize")) + name := c.QueryParam("name") + + items, total, err := model.FindPageUserGroup(pageIndex, pageSize, name) + if err != nil { + return err + } + + return Success(c, H{ + "total": total, + "items": items, + }) +} + +func UserGroupUpdateEndpoint(c echo.Context) error { + id := c.Param("id") + + var item model.UserGroup + if err := c.Bind(&item); err != nil { + return err + } + + model.UpdateUserGroupById(&item, id) + + return Success(c, nil) +} + +func UserGroupDeleteEndpoint(c echo.Context) error { + ids := c.Param("id") + split := strings.Split(ids, ",") + for i := range split { + userId := split[i] + model.DeleteUserGroupById(userId) + } + + return Success(c, nil) +} + +func UserGroupGetEndpoint(c echo.Context) error { + id := c.Param("id") + + item, err := model.FindUserGroupById(id) + if err != nil { + return err + } + + return Success(c, item) +} + +func UserGroupAddMembersEndpoint(c echo.Context) error { + id := c.Param("id") + + var items []string + if err := c.Bind(&items); err != nil { + return err + } + + if err := model.AddUserGroupMembers(global.DB, items, id); err != nil { + return err + } + return Success(c, "") +} + +func UserGroupDelMembersEndpoint(c echo.Context) (err error) { + id := c.Param("id") + memberIdsStr := c.Param("memberId") + memberIds := strings.Split(memberIdsStr, ",") + for i := range memberIds { + memberId := memberIds[i] + err = global.DB.Where("user_group_id = ? and user_id = ?", id, memberId).Delete(&model.UserGroupMember{}).Error + if err != nil { + return err + } + } + + return Success(c, "") +} diff --git a/pkg/model/asset-group.go b/pkg/model/asset-group.go new file mode 100644 index 0000000..8b53790 --- /dev/null +++ b/pkg/model/asset-group.go @@ -0,0 +1 @@ +package model diff --git a/pkg/model/asset.go b/pkg/model/asset.go index 38faf0e..7eaeafc 100644 --- a/pkg/model/asset.go +++ b/pkg/model/asset.go @@ -43,11 +43,6 @@ func (r *Asset) TableName() string { return "assets" } -func FindAllAsset() (o []Asset, err error) { - err = global.DB.Find(&o).Error - return -} - func FindAssetByConditions(protocol string, account User) (o []Asset, err error) { db := global.DB.Table("assets").Select("assets.id,assets.name,assets.ip,assets.port,assets.protocol,assets.active,assets.owner,assets.created, users.nickname as owner_name,COUNT(resources.user_id) as sharer_count").Joins("left join users on assets.owner = users.id").Joins("left join resources on assets.id = resources.resource_id").Group("assets.id") diff --git a/pkg/model/user-group-member.go b/pkg/model/user-group-member.go new file mode 100644 index 0000000..2d2f4d2 --- /dev/null +++ b/pkg/model/user-group-member.go @@ -0,0 +1,11 @@ +package model + +type UserGroupMember struct { + ID string `gorm:"primary_key" json:"name"` + UserId string `json:"userId"` + UserGroupId string `json:"userGroupId"` +} + +func (r *UserGroupMember) TableName() string { + return "user_group_members" +} diff --git a/pkg/model/user-group.go b/pkg/model/user-group.go new file mode 100644 index 0000000..be55e90 --- /dev/null +++ b/pkg/model/user-group.go @@ -0,0 +1,97 @@ +package model + +import ( + "gorm.io/gorm" + "next-terminal/pkg/global" + "next-terminal/pkg/utils" +) + +type UserGroup struct { + ID string `gorm:"primary_key" json:"id"` + Name string `json:"name"` + Created utils.JsonTime `json:"created"` +} + +type UserGroupVo struct { + ID string `gorm:"primary_key" json:"id"` + Name string `json:"name"` + Created utils.JsonTime `json:"created"` + MemberCount int64 `json:"memberCount"` +} + +func (r *UserGroup) TableName() string { + return "user_groups" +} + +func FindPageUserGroup(pageIndex, pageSize int, name string) (o []UserGroupVo, total int64, err error) { + db := global.DB.Table("user_groups").Select("user_groups.id, user_groups.name, user_groups.created, count(user_group_members.user_id) as member_count").Joins("left join user_group_members on user_groups.id = user_group_members.user_group_id").Group("user_groups.id") + dbCounter := global.DB.Table("user_groups") + if len(name) > 0 { + db = db.Where("user_groups.name like ?", "%"+name+"%") + dbCounter = dbCounter.Where("name like ?", "%"+name+"%") + } + + err = dbCounter.Count(&total).Error + if err != nil { + return nil, 0, err + } + err = db.Order("user_groups.created desc").Find(&o).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Error + if o == nil { + o = make([]UserGroupVo, 0) + } + return +} + +func CreateNewUserGroup(o *UserGroup, members []string) (err error) { + return global.DB.Transaction(func(tx *gorm.DB) error { + err = tx.Create(o).Error + if err != nil { + return err + } + + if members != nil { + userGroupId := o.ID + err = AddUserGroupMembers(tx, members, userGroupId) + if err != nil { + return err + } + } + return err + }) +} + +func AddUserGroupMembers(tx *gorm.DB, userIds []string, userGroupId string) error { + for i := range userIds { + userId := userIds[i] + _, err := FindUserById(userId) + if err != nil { + return err + } + + userGroupMember := UserGroupMember{ + ID: utils.Sign([]string{userGroupId, userId}), + UserId: userId, + UserGroupId: userGroupId, + } + err = tx.Create(&userGroupMember).Error + if err != nil { + return err + } + } + return nil +} + +func FindUserGroupById(id string) (o UserGroup, err error) { + err = global.DB.Where("id = ?", id).First(&o).Error + return +} + +func UpdateUserGroupById(o *UserGroup, id string) { + o.ID = id + global.DB.Updates(o) +} + +func DeleteUserGroupById(id string) { + global.DB.Where("id = ?", id).Delete(&UserGroup{}) + global.DB.Where("user_group_id = ?", id).Delete(&UserGroupMember{}) +} diff --git a/web/src/App.js b/web/src/App.js index 10b6881..14ae9f5 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -35,6 +35,7 @@ import Setting from "./components/setting/Setting"; import BatchCommand from "./components/command/BatchCommand"; import {NT_PACKAGE} from "./utils/utils"; import {isAdmin} from "./service/permission"; +import UserGroup from "./components/user/UserGroup"; const {Footer, Sider} = Layout; @@ -188,25 +189,18 @@ class App extends Component { - {/*}>*/} - {/* }>*/} - {/* */} - {/* 用户组管理*/} - {/* */} - {/* */} - - {/* }>*/} - {/* */} - {/* 用户管理*/} - {/* */} - {/* */} - {/**/} - - }> - - 用户管理 - - + }> + }> + + 用户管理 + + + }> + + 用户组管理 + + + : undefined } @@ -246,6 +240,7 @@ class App extends Component { + diff --git a/web/src/components/asset/Asset.js b/web/src/components/asset/Asset.js index 10e3b64..5eb8395 100644 --- a/web/src/components/asset/Asset.js +++ b/web/src/components/asset/Asset.js @@ -466,7 +466,7 @@ class Asset extends Component { {isAdmin() ? - + diff --git a/web/src/components/session/OfflineSession.js b/web/src/components/session/OfflineSession.js index 821fd72..f131fdf 100644 --- a/web/src/components/session/OfflineSession.js +++ b/web/src/components/session/OfflineSession.js @@ -165,7 +165,7 @@ class OfflineSession extends Component { } handleSearchByNickname = async nickname => { - const result = await request.get(`/users/paging?pageIndex=1&pageSize=100&nickname=${nickname}`); + const result = await request.get(`/users/paging?pageIndex=1&pageSize=1000&nickname=${nickname}`); if (result.code !== 1) { message.error(result.message, 10); return; diff --git a/web/src/components/user/User.js b/web/src/components/user/User.js index a1fe057..1c05470 100644 --- a/web/src/components/user/User.js +++ b/web/src/components/user/User.js @@ -369,7 +369,7 @@ class User extends Component { @@ -483,21 +483,15 @@ class User extends Component { loading={this.state.loading} /> - { - this.state.modalVisible ? - - - : null - } - - + + ); diff --git a/web/src/components/user/UserGroup.js b/web/src/components/user/UserGroup.js new file mode 100644 index 0000000..e8358b0 --- /dev/null +++ b/web/src/components/user/UserGroup.js @@ -0,0 +1,403 @@ +import React, {Component} from 'react'; +import {itemRender} from '../../utils/utils' + +import {Button, Col, Divider, Input, Layout, Modal, PageHeader, Row, Space, Table, Tooltip, Typography,} from "antd"; +import qs from "qs"; +import request from "../../common/request"; +import {message} from "antd/es"; +import {DeleteOutlined, ExclamationCircleOutlined, PlusOutlined, SyncOutlined, UndoOutlined} from '@ant-design/icons'; +import Logout from "./Logout"; +import UserGroupModal from "./UserGroupModal"; + +const confirm = Modal.confirm; +const {Search} = Input; +const {Title, Text} = Typography; +const {Content} = Layout; + +const routes = [ + { + path: '', + breadcrumbName: '首页', + }, + { + path: 'user', + breadcrumbName: '用户组', + } +]; + +class UserGroup extends Component { + + inputRefOfName = React.createRef(); + + state = { + items: [], + total: 0, + queryParams: { + pageIndex: 1, + pageSize: 10 + }, + loading: false, + modalVisible: false, + modalTitle: '', + modalConfirmLoading: false, + model: null, + selectedRowKeys: [], + delBtnLoading: false, + users: [], + members: [] + }; + + componentDidMount() { + this.loadTableData(); + } + + async delete(id) { + let result = await request.delete('/user-groups/' + id); + if (result.code === 1) { + message.success('操作成功', 3); + await this.loadTableData(this.state.queryParams); + } else { + message.error('删除失败 :( ' + result.message, 10); + } + } + + async loadTableData(queryParams) { + this.setState({ + loading: true + }); + + queryParams = queryParams || this.state.queryParams; + + let paramsStr = qs.stringify(queryParams); + + let data = { + items: [], + total: 0 + }; + + try { + let result = await request.get('/user-groups/paging?' + paramsStr); + if (result.code === 1) { + data = result.data; + } else { + message.error(result.message, 10); + } + } catch (e) { + + } finally { + const items = data.items.map(item => { + return {'key': item['id'], ...item} + }) + this.setState({ + items: items, + total: data.total, + queryParams: queryParams, + loading: false + }); + } + + } + + handleChangPage = (pageIndex, pageSize) => { + let queryParams = this.state.queryParams; + queryParams.pageIndex = pageIndex; + queryParams.pageSize = pageSize; + + this.setState({ + queryParams: queryParams + }); + + this.loadTableData(queryParams).then(r => { + }) + }; + + showDeleteConfirm(id, content) { + let self = this; + confirm({ + title: '您确定要删除此用户组吗?', + content: content, + okText: '确定', + okType: 'danger', + cancelText: '取消', + onOk() { + self.delete(id); + } + }); + }; + + showModal(title, user = {}) { + + this.handleSearchByNickname(''); + this.setState({ + model: user, + modalVisible: true, + modalTitle: title + }); + }; + + handleCancelModal = e => { + this.setState({ + modalVisible: false, + modalTitle: '' + }); + }; + + handleOk = async (formData) => { + // 弹窗 form 传来的数据 + this.setState({ + modalConfirmLoading: true + }); + + if (formData.id) { + // 向后台提交数据 + const result = await request.put('/user-groups/' + formData.id, formData); + if (result.code === 1) { + message.success('操作成功', 3); + + this.setState({ + modalVisible: false + }); + await this.loadTableData(this.state.queryParams); + } else { + message.error('操作失败 :( ' + result.message, 10); + } + } else { + // 向后台提交数据 + const result = await request.post('/user-groups', formData); + if (result.code === 1) { + message.success('操作成功', 3); + + this.setState({ + modalVisible: false + }); + await this.loadTableData(this.state.queryParams); + } else { + message.error('操作失败 :( ' + result.message, 10); + } + } + + this.setState({ + modalConfirmLoading: false + }); + }; + + handleSearchByName = name => { + let query = { + ...this.state.queryParams, + 'pageIndex': 1, + 'pageSize': this.state.queryParams.pageSize, + 'name': name, + } + + this.loadTableData(query); + }; + + batchDelete = async () => { + this.setState({ + delBtnLoading: true + }) + try { + let result = await request.delete('/user-groups/' + this.state.selectedRowKeys.join(',')); + if (result.code === 1) { + message.success('操作成功', 3); + this.setState({ + selectedRowKeys: [] + }) + await this.loadTableData(this.state.queryParams); + } else { + message.error('删除失败 :( ' + result.message, 10); + } + } finally { + this.setState({ + delBtnLoading: false + }) + } + } + + handleSearchByNickname = async nickname => { + const result = await request.get(`/users/paging?pageIndex=1&pageSize=1000&nickname=${nickname}`); + if (result.code !== 1) { + message.error(result.message, 10); + return; + } + + this.setState({ + users: result.data.items + }) + } + + render() { + + const columns = [{ + title: '序号', + dataIndex: 'id', + key: 'id', + render: (id, record, index) => { + return index + 1; + } + }, { + title: '名称', + dataIndex: 'name', + }, { + title: '成员人数', + dataIndex: 'memberCount', + key: 'memberCount', + render: (text, record, index) => { + return + } + }, { + title: '创建日期', + dataIndex: 'created', + key: 'created' + }, + { + title: '操作', + key: 'action', + render: (text, record) => { + + + return ( +
+ + +
+ ) + }, + } + ]; + + const selectedRowKeys = this.state.selectedRowKeys; + const rowSelection = { + selectedRowKeys: this.state.selectedRowKeys, + onChange: (selectedRowKeys, selectedRows) => { + this.setState({selectedRowKeys}); + }, + }; + const hasSelected = selectedRowKeys.length > 0; + + return ( + <> + + ]} + subTitle="平台用户管理" + > + + + +
+ + + 用户组列表 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + `总计 ${total} 条` + }} + loading={this.state.loading} + /> + + + + + + + ); + } +} + +export default UserGroup; diff --git a/web/src/components/user/UserGroupModal.js b/web/src/components/user/UserGroupModal.js new file mode 100644 index 0000000..f22cdbd --- /dev/null +++ b/web/src/components/user/UserGroupModal.js @@ -0,0 +1,69 @@ +import React, {useState} from 'react'; +import {Form, Input, Modal, Select} from "antd/lib/index"; + +const UserGroupModal = ({ + title, + visible, + handleOk, + handleCancel, + confirmLoading, + model, + users, + members, + }) => { + + const [form] = Form.useForm(); + + const formItemLayout = { + labelCol: {span: 6}, + wrapperCol: {span: 14}, + }; + + return ( + { + form + .validateFields() + .then(values => { + form.resetFields(); + handleOk(values); + }) + .catch(info => { + }); + }} + onCancel={handleCancel} + confirmLoading={confirmLoading} + okText='确定' + cancelText='取消' + > + +
+ + + + + + + + + + + + +
+ ) +}; + +export default UserGroupModal; diff --git a/web/src/components/user/UserModal.js b/web/src/components/user/UserModal.js index 5625045..e8a0fcd 100644 --- a/web/src/components/user/UserModal.js +++ b/web/src/components/user/UserModal.js @@ -19,7 +19,7 @@ const UserModal = ({title, visible, handleOk, handleCancel, confirmLoading, mode form .validateFields() .then(values => { - // form.resetFields(); + form.resetFields(); handleOk(values); }) .catch(info => {