From 1d232f72693da11f07bb1eab0bfb3b764d23b00a Mon Sep 17 00:00:00 2001 From: dushixiang Date: Mon, 15 Nov 2021 14:21:46 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=A4=87=E4=BB=BD=E5=92=8C?= =?UTF-8?q?=E5=AF=BC=E5=87=BA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 25 +-- config.yml | 2 +- go.mod | 37 +++- server/api/backup.go | 271 ++++++++++++++++++++++++-- server/api/user.go | 11 +- server/model/user_group.go | 1 + server/repository/storage.go | 6 +- server/repository/user.go | 10 + server/service/storage.go | 8 +- server/utils/jsontime.go | 56 ++++++ server/utils/password.go | 44 +++++ server/utils/utils.go | 56 ------ web/src/components/setting/Setting.js | 47 ++++- web/src/components/user/User.js | 32 +-- 14 files changed, 472 insertions(+), 134 deletions(-) create mode 100644 server/utils/jsontime.go create mode 100644 server/utils/password.go diff --git a/.dockerignore b/.dockerignore index 61ac58a..af551fa 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,27 +1,6 @@ -# Created by .ignore support plugin (hsz.mobi) -### Go template -# Binaries for programs and plugins -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, built with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# Dependency directories (remove the comment below to include it) -# vendor/ - -.gitignore -bin +.* data -docs -# guacd +guacd logs playground -screenshot web/node_modules/ -.dockerignore diff --git a/config.yml b/config.yml index d428d4f..75eb0c1 100644 --- a/config.yml +++ b/config.yml @@ -1,6 +1,6 @@ debug: true demo: false -db: mysql +db: sqlite mysql: hostname: localhost port: 3306 diff --git a/go.mod b/go.mod index 965be91..33c9970 100644 --- a/go.mod +++ b/go.mod @@ -21,10 +21,45 @@ require ( github.com/spf13/viper v1.7.1 github.com/stretchr/testify v1.6.1 golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e - golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect golang.org/x/text v0.3.6 gopkg.in/natefinch/lumberjack.v2 v2.0.0 gorm.io/driver/mysql v1.0.3 gorm.io/driver/sqlite v1.1.4 gorm.io/gorm v1.20.7 ) + +require ( + github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect + github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect + github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect + github.com/fsnotify/fsnotify v1.4.7 // indirect + github.com/go-sql-driver/mysql v1.5.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.1 // indirect + github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a // indirect + github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect + github.com/kr/fs v0.1.0 // indirect + github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a // indirect + github.com/magiconair/properties v1.8.1 // indirect + github.com/mattn/go-colorable v0.1.7 // indirect + github.com/mattn/go-isatty v0.0.12 // indirect + github.com/mattn/go-sqlite3 v1.14.5 // indirect + github.com/mitchellh/mapstructure v1.1.2 // indirect + github.com/pelletier/go-toml v1.2.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/afero v1.1.2 // indirect + github.com/spf13/cast v1.3.0 // indirect + github.com/spf13/jwalterweatherman v1.0.0 // indirect + github.com/subosito/gotenv v1.2.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.1 // indirect + golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect + golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 // indirect + golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect + gopkg.in/ini.v1 v1.51.0 // indirect + gopkg.in/yaml.v2 v2.2.4 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect +) diff --git a/server/api/backup.go b/server/api/backup.go index cfd4ff6..91d6099 100644 --- a/server/api/backup.go +++ b/server/api/backup.go @@ -4,25 +4,32 @@ import ( "bytes" "encoding/json" "fmt" - "github.com/labstack/echo/v4" "net/http" - "next-terminal/server/model" + "strings" "time" + + "next-terminal/server/config" + "next-terminal/server/constant" + "next-terminal/server/global/security" + "next-terminal/server/model" + "next-terminal/server/utils" + + "github.com/labstack/echo/v4" ) type Backup struct { - Users []model.User `json:"users"` - UserGroups []model.UserGroup `json:"user_groups"` - UserGroupMembers []model.UserGroupMember `json:"user_group_members"` + Users []model.User `json:"users"` + UserGroups []model.UserGroup `json:"user_groups"` - Strategies []model.Strategy `json:"strategies"` - Jobs []model.Job `json:"jobs"` - AccessSecurities []model.AccessSecurity `json:"access_securities"` - AccessGateways []model.AccessGateway `json:"access_gateways"` - Commands []model.Command `json:"commands"` - Credentials []model.Credential `json:"credentials"` - Assets []model.Asset `json:"assets"` - ResourceSharers []model.ResourceSharer `json:"resource_sharers"` + Storages []model.Storage `json:"storages"` + Strategies []model.Strategy `json:"strategies"` + AccessSecurities []model.AccessSecurity `json:"access_securities"` + AccessGateways []model.AccessGateway `json:"access_gateways"` + Commands []model.Command `json:"commands"` + Credentials []model.Credential `json:"credentials"` + Assets []map[string]interface{} `json:"assets"` + ResourceSharers []model.ResourceSharer `json:"resource_sharers"` + Jobs []model.Job `json:"jobs"` } func BackupExportEndpoint(c echo.Context) error { @@ -37,7 +44,17 @@ func BackupExportEndpoint(c echo.Context) error { if err != nil { return err } - userGroupMembers, err := userGroupRepository.FindAllUserGroupMembers() + if len(userGroups) > 0 { + for i := range userGroups { + members, err := userGroupRepository.FindMembersById(userGroups[i].ID) + if err != nil { + return err + } + userGroups[i].Members = members + } + } + + storages, err := storageRepository.FindAll() if err != nil { return err } @@ -66,10 +83,36 @@ func BackupExportEndpoint(c echo.Context) error { if err != nil { return err } + if len(credentials) > 0 { + for i := range credentials { + if err := credentialRepository.Decrypt(&credentials[i], config.GlobalCfg.EncryptionPassword); err != nil { + return err + } + } + } assets, err := assetRepository.FindAll() if err != nil { return err } + var assetMaps = make([]map[string]interface{}, 0) + if len(assets) > 0 { + for i := range assets { + asset := assets[i] + if err := assetRepository.Decrypt(&asset, config.GlobalCfg.EncryptionPassword); err != nil { + return err + } + attributeMap, err := assetRepository.FindAssetAttrMapByAssetId(asset.ID) + if err != nil { + return err + } + itemMap := utils.StructToMap(asset) + for key := range attributeMap { + itemMap[key] = attributeMap[key] + } + assetMaps = append(assetMaps, itemMap) + } + } + resourceSharers, err := resourceSharerRepository.FindAll() if err != nil { return err @@ -78,14 +121,14 @@ func BackupExportEndpoint(c echo.Context) error { backup := Backup{ Users: users, UserGroups: userGroups, - UserGroupMembers: userGroupMembers, + Storages: storages, Strategies: strategies, Jobs: jobs, AccessSecurities: accessSecurities, AccessGateways: accessGateways, Commands: commands, Credentials: credentials, - Assets: assets, + Assets: assetMaps, ResourceSharers: resourceSharers, } @@ -98,5 +141,199 @@ func BackupExportEndpoint(c echo.Context) error { } func BackupImportEndpoint(c echo.Context) error { - return nil + var backup Backup + if err := c.Bind(&backup); err != nil { + return err + } + + var userIdMapping = make(map[string]string, 0) + if len(backup.Users) > 0 { + for _, item := range backup.Users { + if userRepository.ExistByUsername(item.Username) { + continue + } + oldId := item.ID + newId := utils.UUID() + item.ID = newId + item.Password = utils.GenPassword() + if err := userRepository.Create(&item); err != nil { + return err + } + userIdMapping[oldId] = newId + } + } + + var userGroupIdMapping = make(map[string]string, 0) + if len(backup.UserGroups) > 0 { + for _, item := range backup.UserGroups { + oldId := item.ID + newId := utils.UUID() + item.ID = newId + + var members = make([]string, 0) + if len(item.Members) > 0 { + for _, member := range item.Members { + members = append(members, userIdMapping[member]) + } + } + + if err := userGroupRepository.Create(&item, members); err != nil { + return err + } + userGroupIdMapping[oldId] = newId + } + } + + if len(backup.Storages) > 0 { + for _, item := range backup.Storages { + item.ID = utils.UUID() + item.Owner = userIdMapping[item.Owner] + if err := storageRepository.Create(&item); err != nil { + return err + } + } + } + + var strategyIdMapping = make(map[string]string, 0) + if len(backup.Strategies) > 0 { + for _, item := range backup.Strategies { + oldId := item.ID + newId := utils.UUID() + item.ID = newId + if err := strategyRepository.Create(&item); err != nil { + return err + } + strategyIdMapping[oldId] = newId + } + } + + if len(backup.AccessSecurities) > 0 { + for _, item := range backup.AccessSecurities { + item.ID = utils.UUID() + if err := accessSecurityRepository.Create(&item); err != nil { + return err + } + // 更新内存中的安全规则 + rule := &security.Security{ + ID: item.ID, + IP: item.IP, + Rule: item.Rule, + Priority: item.Priority, + } + security.GlobalSecurityManager.Add <- rule + } + } + + var accessGatewayIdMapping = make(map[string]string, 0) + if len(backup.AccessGateways) > 0 { + for _, item := range backup.AccessGateways { + oldId := item.ID + newId := utils.UUID() + item.ID = newId + if err := accessGatewayRepository.Create(&item); err != nil { + return err + } + accessGatewayIdMapping[oldId] = newId + } + } + + if len(backup.Commands) > 0 { + for _, item := range backup.Commands { + item.ID = utils.UUID() + if err := commandRepository.Create(&item); err != nil { + return err + } + } + } + + var credentialIdMapping = make(map[string]string, 0) + if len(backup.Credentials) > 0 { + for _, item := range backup.Credentials { + oldId := item.ID + newId := utils.UUID() + item.ID = newId + if err := credentialRepository.Create(&item); err != nil { + return err + } + credentialIdMapping[oldId] = newId + } + } + + var assetIdMapping = make(map[string]string, 0) + if len(backup.Assets) > 0 { + for _, m := range backup.Assets { + data, err := json.Marshal(m) + if err != nil { + return err + } + var item model.Asset + if err := json.Unmarshal(data, &item); err != nil { + return err + } + + if item.CredentialId != "" && item.CredentialId != "-" { + item.CredentialId = credentialIdMapping[item.CredentialId] + } + if item.AccessGatewayId != "" && item.AccessGatewayId != "-" { + item.AccessGatewayId = accessGatewayIdMapping[item.AccessGatewayId] + } + + oldId := item.ID + newId := utils.UUID() + item.ID = newId + if err := assetRepository.Create(&item); err != nil { + return err + } + + if err := assetRepository.UpdateAttributes(item.ID, item.Protocol, m); err != nil { + return err + } + + go func() { + active, _ := assetService.CheckStatus(item.AccessGatewayId, item.IP, item.Port) + + if item.Active != active { + _ = assetRepository.UpdateActiveById(active, item.ID) + } + }() + + assetIdMapping[oldId] = newId + } + } + + if len(backup.ResourceSharers) > 0 { + for _, item := range backup.ResourceSharers { + + userGroupId := userGroupIdMapping[item.UserGroupId] + userId := userIdMapping[item.UserId] + strategyId := strategyIdMapping[item.StrategyId] + resourceId := assetIdMapping[item.ResourceId] + + if err := resourceSharerRepository.AddSharerResources(userGroupId, userId, strategyId, item.ResourceType, []string{resourceId}); err != nil { + return err + } + } + } + + if len(backup.Jobs) > 0 { + for _, item := range backup.Jobs { + if item.Func == constant.FuncCheckAssetStatusJob { + continue + } + + resourceIds := strings.Split(item.ResourceIds, ",") + if len(resourceIds) > 0 { + var newResourceIds = make([]string, 0) + for _, resourceId := range resourceIds { + newResourceIds = append(newResourceIds, assetIdMapping[resourceId]) + } + item.ResourceIds = strings.Join(newResourceIds, ",") + } + if err := jobService.Create(&item); err != nil { + return err + } + } + } + + return Success(c, "") } diff --git a/server/api/user.go b/server/api/user.go index a736234..e3eac21 100644 --- a/server/api/user.go +++ b/server/api/user.go @@ -2,10 +2,10 @@ package api import ( "errors" - "next-terminal/server/constant" "strconv" "strings" + "next-terminal/server/constant" "next-terminal/server/global/cache" "next-terminal/server/log" "next-terminal/server/model" @@ -20,6 +20,10 @@ func UserCreateEndpoint(c echo.Context) (err error) { if err := c.Bind(&item); err != nil { return err } + if userRepository.ExistByUsername(item.Username) { + return Fail(c, -1, "username is already in use") + } + password := item.Password var pass []byte @@ -71,6 +75,11 @@ func UserPagingEndpoint(c echo.Context) error { func UserUpdateEndpoint(c echo.Context) error { id := c.Param("id") + account, _ := GetCurrentAccount(c) + if account.ID == id { + return Fail(c, -1, "cannot modify itself") + } + var item model.User if err := c.Bind(&item); err != nil { return err diff --git a/server/model/user_group.go b/server/model/user_group.go index 37fcabf..cc39391 100644 --- a/server/model/user_group.go +++ b/server/model/user_group.go @@ -8,6 +8,7 @@ type UserGroup struct { ID string `gorm:"primary_key,type:varchar(36)" json:"id"` Name string `gorm:"type:varchar(500)" json:"name"` Created utils.JsonTime `json:"created"` + Members []string `gorm:"-" json:"members"` } type UserGroupForPage struct { diff --git a/server/repository/storage.go b/server/repository/storage.go index 5c3c48a..ba7df9d 100644 --- a/server/repository/storage.go +++ b/server/repository/storage.go @@ -79,9 +79,7 @@ func (r StorageRepository) FindById(id string) (m model.Storage, err error) { return } -func (r StorageRepository) FindAll() (o []model.Storage) { - if r.DB.Find(&o).Error != nil { - return nil - } +func (r StorageRepository) FindAll() (o []model.Storage, err error) { + err = r.DB.Find(&o).Error return } diff --git a/server/repository/user.go b/server/repository/user.go index eacbf62..22a50e7 100644 --- a/server/repository/user.go +++ b/server/repository/user.go @@ -90,6 +90,16 @@ func (r UserRepository) FindByUsername(username string) (o model.User, err error return } +func (r UserRepository) ExistByUsername(username string) (exist bool) { + count := int64(0) + err := r.DB.Table("users").Where("username = ?", username).Count(&count).Error + if err != nil { + return false + } + + return count > 0 +} + func (r UserRepository) FindOnlineUsers() (o []model.User, err error) { err = r.DB.Where("online = ?", true).Find(&o).Error return diff --git a/server/service/storage.go b/server/service/storage.go index 1893843..9685352 100644 --- a/server/service/storage.go +++ b/server/service/storage.go @@ -42,7 +42,10 @@ func (r StorageService) InitStorages() error { } drivePath := r.GetBaseDrivePath() - storages := r.storageRepository.FindAll() + storages, err := r.storageRepository.FindAll() + if err != nil { + return err + } for i := 0; i < len(storages); i++ { storage := storages[i] // 判断是否为遗留的数据:磁盘空间在,但用户已删除 @@ -137,6 +140,9 @@ func (r StorageService) DeleteStorageById(id string, force bool) error { drivePath := r.GetBaseDrivePath() storage, err := r.storageRepository.FindById(id) if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } return err } if !force && storage.IsDefault { diff --git a/server/utils/jsontime.go b/server/utils/jsontime.go new file mode 100644 index 0000000..670088b --- /dev/null +++ b/server/utils/jsontime.go @@ -0,0 +1,56 @@ +package utils + +import ( + "database/sql/driver" + "fmt" + "strings" + "time" +) + +type JsonTime struct { + time.Time +} + +func NewJsonTime(t time.Time) JsonTime { + return JsonTime{ + Time: t, + } +} + +func NowJsonTime() JsonTime { + return JsonTime{ + Time: time.Now(), + } +} + +func (j *JsonTime) MarshalJSON() ([]byte, error) { + var stamp = fmt.Sprintf("\"%s\"", j.Format("2006-01-02 15:04:05")) + return []byte(stamp), nil +} + +func (j *JsonTime) UnmarshalJSON(b []byte) error { + s := strings.ReplaceAll(string(b), "\"", "") + t, err := time.Parse("2006-01-02 15:04:05", s) + if err != nil { + return err + } + *j = NewJsonTime(t) + return nil +} + +func (j JsonTime) Value() (driver.Value, error) { + var zeroTime time.Time + if j.Time.UnixNano() == zeroTime.UnixNano() { + return nil, nil + } + return j.Time, nil +} + +func (j *JsonTime) Scan(v interface{}) error { + value, ok := v.(time.Time) + if ok { + *j = JsonTime{Time: value} + return nil + } + return fmt.Errorf("can not convert %v to timestamp", v) +} diff --git a/server/utils/password.go b/server/utils/password.go new file mode 100644 index 0000000..340b692 --- /dev/null +++ b/server/utils/password.go @@ -0,0 +1,44 @@ +package utils + +import ( + "math/rand" + "time" + + "golang.org/x/crypto/bcrypt" +) + +type Bcrypt struct { + cost int +} + +func (b *Bcrypt) Encode(password []byte) ([]byte, error) { + return bcrypt.GenerateFromPassword(password, b.cost) +} + +func (b *Bcrypt) Match(hashedPassword, password []byte) error { + return bcrypt.CompareHashAndPassword(hashedPassword, password) +} + +var Encoder = Bcrypt{ + cost: bcrypt.DefaultCost, +} + +func GenPassword() string { + rand.Seed(time.Now().UnixNano()) + digits := "0123456789" + specials := "~=+%^*/()[]{}/!@#$?|" + all := "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "abcdefghijklmnopqrstuvwxyz" + + digits + specials + length := 8 + buf := make([]byte, length) + buf[0] = digits[rand.Intn(len(digits))] + buf[1] = specials[rand.Intn(len(specials))] + for i := 2; i < length; i++ { + buf[i] = all[rand.Intn(len(all))] + } + rand.Shuffle(len(buf), func(i, j int) { + buf[i], buf[j] = buf[j], buf[i] + }) + return string(buf) +} diff --git a/server/utils/utils.go b/server/utils/utils.go index 331a858..b818cf1 100644 --- a/server/utils/utils.go +++ b/server/utils/utils.go @@ -11,7 +11,6 @@ import ( "crypto/sha256" "crypto/sha512" "crypto/x509" - "database/sql/driver" "encoding/base64" "encoding/pem" "errors" @@ -36,64 +35,9 @@ import ( "github.com/gofrs/uuid" errors2 "github.com/pkg/errors" "github.com/sirupsen/logrus" - "golang.org/x/crypto/bcrypt" "golang.org/x/crypto/pbkdf2" ) -type JsonTime struct { - time.Time -} - -func NewJsonTime(t time.Time) JsonTime { - return JsonTime{ - Time: t, - } -} - -func NowJsonTime() JsonTime { - return JsonTime{ - Time: time.Now(), - } -} - -func (t JsonTime) MarshalJSON() ([]byte, error) { - var stamp = fmt.Sprintf("\"%s\"", t.Format("2006-01-02 15:04:05")) - return []byte(stamp), nil -} - -func (t JsonTime) Value() (driver.Value, error) { - var zeroTime time.Time - if t.Time.UnixNano() == zeroTime.UnixNano() { - return nil, nil - } - return t.Time, nil -} - -func (t *JsonTime) Scan(v interface{}) error { - value, ok := v.(time.Time) - if ok { - *t = JsonTime{Time: value} - return nil - } - return fmt.Errorf("can not convert %v to timestamp", v) -} - -type Bcrypt struct { - cost int -} - -func (b *Bcrypt) Encode(password []byte) ([]byte, error) { - return bcrypt.GenerateFromPassword(password, b.cost) -} - -func (b *Bcrypt) Match(hashedPassword, password []byte) error { - return bcrypt.CompareHashAndPassword(hashedPassword, password) -} - -var Encoder = Bcrypt{ - cost: bcrypt.DefaultCost, -} - func UUID() string { v4, _ := uuid.NewV4() return v4.String() diff --git a/web/src/components/setting/Setting.js b/web/src/components/setting/Setting.js index b59dffd..7597c8b 100644 --- a/web/src/components/setting/Setting.js +++ b/web/src/components/setting/Setting.js @@ -32,7 +32,7 @@ class Setting extends Component { vncSettingFormRef = React.createRef(); guacdSettingFormRef = React.createRef(); mailSettingFormRef = React.createRef(); - otherSettingFormRef = React.createRef(); + logSettingFormRef = React.createRef(); componentDidMount() { this.getProperties(); @@ -94,8 +94,8 @@ class Setting extends Component { this.mailSettingFormRef.current.setFieldsValue(properties) } - if (this.otherSettingFormRef.current) { - this.otherSettingFormRef.current.setFieldsValue(properties) + if (this.logSettingFormRef.current) { + this.logSettingFormRef.current.setFieldsValue(properties) } } else { message.error(result['message']); @@ -106,6 +106,35 @@ class Setting extends Component { this.getProperties() } + handleImport = () => { + let files = window.document.getElementById('file-upload').files; + if (files.length === 0) { + return; + } + + const reader = new FileReader(); + reader.onload = async () => { + let backup = JSON.parse(reader.result.toString()); + this.setState({ + importBtnLoading: true + }) + try { + let result = await request.post('/backup/import', backup); + if (result['code'] === 1) { + message.success('恢复成功', 3); + } else { + message.error(result['message'], 10); + } + } finally { + this.setState({ + importBtnLoading: false + }) + window.document.getElementById('file-upload').value = ""; + } + }; + reader.readAsText(files[0]); + } + render() { return ( <> @@ -419,7 +448,7 @@ class Setting extends Component { }, ]} > - { + { this.setState({ properties: { ...this.state.properties, @@ -528,9 +557,9 @@ class Setting extends Component { - + 其他配置 -
- + diff --git a/web/src/components/user/User.js b/web/src/components/user/User.js index ab8e2e6..982a2ce 100644 --- a/web/src/components/user/User.js +++ b/web/src/components/user/User.js @@ -95,14 +95,9 @@ class User extends Component { } else { message.error(result.message, 10); } - } catch (e) { - } finally { - const items = data.items.map(item => { - return {'key': item['id'], ...item} - }) this.setState({ - items: items, + items: data.items, total: data.total, queryParams: queryParams, loading: false @@ -145,7 +140,7 @@ class User extends Component { }); }; - handleCancelModal = e => { + handleCancelModal = () => { this.setState({ modalVisible: false, modalTitle: '' @@ -245,13 +240,6 @@ class User extends Component { } } - handleAssetCancel = () => { - this.loadTableData() - this.setState({ - assetVisible: false - }) - } - handleChangePassword = async (values) => { this.setState({ changePasswordConfirmLoading: true @@ -362,7 +350,7 @@ class User extends Component { dataIndex: 'username', key: 'username', sorter: true, - render: (username, record, index) => { + render: (username, record) => { return (