From f1432b68861a6ed3f28c0923e1699bbf62422c14 Mon Sep 17 00:00:00 2001 From: dushixiang Date: Fri, 12 Nov 2021 20:07:15 +0800 Subject: [PATCH] =?UTF-8?q?-=20=E4=BF=AE=E5=A4=8D=E3=80=8C=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E6=8E=A5=E5=85=A5=E7=BD=91=E5=85=B3=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E3=80=8D=E7=9A=84=E9=97=AE=E9=A2=98=20-=20=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E3=80=8C[=E5=8A=9F=E8=83=BD=E8=AF=B7=E6=B1=82]=E5=AE=A1?= =?UTF-8?q?=E8=AE=A1=E7=9A=84=E5=8E=86=E5=8F=B2=E4=BC=9A=E8=AF=9D=E5=BB=BA?= =?UTF-8?q?=E8=AE=AE=E6=B7=BB=E5=8A=A0=E2=80=9C=E5=B7=B2=E9=98=85=E2=80=9D?= =?UTF-8?q?=E7=9A=84=E5=8A=9F=E8=83=BD=E3=80=8Dclose=20#194=20-=20?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=B8=80=E9=94=AE=E5=88=A0=E9=99=A4=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E6=97=A5=E5=BF=97=E5=92=8C=E5=8E=86=E5=8F=B2=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 1 + server/api/access_gateway.go | 3 +- server/api/account.go | 16 +- server/api/backup.go | 102 ++++++++ server/api/login-log.go | 35 ++- server/api/routes.go | 13 +- server/api/security.go | 2 +- server/api/session.go | 41 +++- server/api/user.go | 37 +-- server/constant/const.go | 3 + server/model/session.go | 2 + server/model/user.go | 4 +- server/repository/access_gateway.go | 7 +- server/repository/access_security.go | 5 +- server/repository/command.go | 5 + server/repository/login_log.go | 5 + server/repository/resource_sharer.go | 5 + server/repository/session.go | 21 +- server/repository/user.go | 8 +- server/repository/user_group.go | 11 +- server/service/access_gateway.go | 7 +- server/service/session.go | 38 +++ server/service/storage.go | 5 +- server/service/user.go | 60 ++++- server/term/ssh.go | 23 ++ web/src/components/asset/AccessGateway.js | 88 ++++--- web/src/components/dashboard/Dashboard.js | 8 +- web/src/components/devops/LoginLog.js | 46 +++- web/src/components/session/OfflineSession.css | 3 + web/src/components/session/OfflineSession.js | 218 +++++++++++++++--- web/src/components/session/Playback.js | 6 +- web/src/components/setting/Setting.js | 35 ++- web/src/components/user/User.js | 171 +++++++------- web/src/components/user/UserModal.js | 1 - 34 files changed, 801 insertions(+), 234 deletions(-) create mode 100644 server/api/backup.go create mode 100644 web/src/components/session/OfflineSession.css diff --git a/go.mod b/go.mod index 6d443f8..965be91 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ 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 diff --git a/server/api/access_gateway.go b/server/api/access_gateway.go index b4bc57f..81d1b81 100644 --- a/server/api/access_gateway.go +++ b/server/api/access_gateway.go @@ -78,8 +78,7 @@ func AccessGatewayUpdateEndpoint(c echo.Context) error { if err := accessGatewayRepository.UpdateById(&item, id); err != nil { return err } - accessGatewayService.DisconnectById(id) - _, _ = accessGatewayService.GetGatewayAndReconnectById(id) + accessGatewayService.ReConnect(&item) return Success(c, nil) } diff --git a/server/api/account.go b/server/api/account.go index ef2b58e..b4ce7b7 100644 --- a/server/api/account.go +++ b/server/api/account.go @@ -1,6 +1,7 @@ package api import ( + "next-terminal/server/constant" "path" "strconv" "strings" @@ -49,8 +50,6 @@ func LoginEndpoint(c echo.Context) error { return err } - user, err := userRepository.FindByUsername(loginAccount.Username) - // 存储登录失败次数信息 loginFailCountKey := c.RealIP() + loginAccount.Username v, ok := cache.GlobalCache.Get(loginFailCountKey) @@ -62,6 +61,7 @@ func LoginEndpoint(c echo.Context) error { return Fail(c, -1, "登录失败次数过多,请等待5分钟后再试") } + user, err := userRepository.FindByUsername(loginAccount.Username) if err != nil { count++ cache.GlobalCache.Set(loginFailCountKey, count, time.Minute*time.Duration(5)) @@ -72,6 +72,10 @@ func LoginEndpoint(c echo.Context) error { return FailWithData(c, -1, "您输入的账号或密码不正确", count) } + if user.Status == constant.StatusDisabled { + return Fail(c, -1, "该账户已停用") + } + if err := utils.Encoder.Match([]byte(user.Password), []byte(loginAccount.Password)); err != nil { count++ cache.GlobalCache.Set(loginFailCountKey, count, time.Minute*time.Duration(5)) @@ -172,6 +176,10 @@ func loginWithTotpEndpoint(c echo.Context) error { return FailWithData(c, -1, "您输入的账号或密码不正确", count) } + if user.Status == constant.StatusDisabled { + return Fail(c, -1, "该账户已停用") + } + if err := utils.Encoder.Match([]byte(user.Password), []byte(loginAccount.Password)); err != nil { count++ cache.GlobalCache.Set(loginFailCountKey, count, time.Minute*time.Duration(5)) @@ -206,9 +214,7 @@ func loginWithTotpEndpoint(c echo.Context) error { func LogoutEndpoint(c echo.Context) error { token := GetToken(c) - cacheKey := userService.BuildCacheKeyByToken(token) - cache.GlobalCache.Delete(cacheKey) - err := userService.Logout(token) + err := userService.LogoutByToken(token) if err != nil { return err } diff --git a/server/api/backup.go b/server/api/backup.go new file mode 100644 index 0000000..cfd4ff6 --- /dev/null +++ b/server/api/backup.go @@ -0,0 +1,102 @@ +package api + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/labstack/echo/v4" + "net/http" + "next-terminal/server/model" + "time" +) + +type Backup struct { + Users []model.User `json:"users"` + UserGroups []model.UserGroup `json:"user_groups"` + UserGroupMembers []model.UserGroupMember `json:"user_group_members"` + + 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"` +} + +func BackupExportEndpoint(c echo.Context) error { + users, err := userRepository.FindAll() + if err != nil { + return err + } + for i := range users { + users[i].Password = "" + } + userGroups, err := userGroupRepository.FindAll() + if err != nil { + return err + } + userGroupMembers, err := userGroupRepository.FindAllUserGroupMembers() + if err != nil { + return err + } + + strategies, err := strategyRepository.FindAll() + if err != nil { + return err + } + jobs, err := jobRepository.FindAll() + if err != nil { + return err + } + accessSecurities, err := accessSecurityRepository.FindAll() + if err != nil { + return err + } + accessGateways, err := accessGatewayRepository.FindAll() + if err != nil { + return err + } + commands, err := commandRepository.FindAll() + if err != nil { + return err + } + credentials, err := credentialRepository.FindAll() + if err != nil { + return err + } + assets, err := assetRepository.FindAll() + if err != nil { + return err + } + resourceSharers, err := resourceSharerRepository.FindAll() + if err != nil { + return err + } + + backup := Backup{ + Users: users, + UserGroups: userGroups, + UserGroupMembers: userGroupMembers, + Strategies: strategies, + Jobs: jobs, + AccessSecurities: accessSecurities, + AccessGateways: accessGateways, + Commands: commands, + Credentials: credentials, + Assets: assets, + ResourceSharers: resourceSharers, + } + + jsonBytes, err := json.Marshal(backup) + if err != nil { + return err + } + c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=next-terminal_backup_%s.json", time.Now().Format("20060102150405"))) + return c.Stream(http.StatusOK, echo.MIMEOctetStream, bytes.NewReader(jsonBytes)) +} + +func BackupImportEndpoint(c echo.Context) error { + return nil +} diff --git a/server/api/login-log.go b/server/api/login-log.go index 5d141ab..cd235c5 100644 --- a/server/api/login-log.go +++ b/server/api/login-log.go @@ -4,9 +4,6 @@ import ( "strconv" "strings" - "next-terminal/server/global/cache" - "next-terminal/server/log" - "github.com/labstack/echo/v4" ) @@ -31,24 +28,26 @@ func LoginLogPagingEndpoint(c echo.Context) error { func LoginLogDeleteEndpoint(c echo.Context) error { ids := c.Param("id") - split := strings.Split(ids, ",") - for i := range split { - token := split[i] - cache.GlobalCache.Delete(token) - if err := userService.Logout(token); err != nil { - log.WithError(err).Error("Cache Delete Failed") - } - } - if err := loginLogRepository.DeleteByIdIn(split); err != nil { + tokens := strings.Split(ids, ",") + if err := userService.DeleteLoginLogs(tokens); err != nil { return err } return Success(c, nil) } -//func LoginLogClearEndpoint(c echo.Context) error { -// loginLogs, err := loginLogRepository.FindAliveLoginLogs() -// if err != nil { -// return err -// } -//} +func LoginLogClearEndpoint(c echo.Context) error { + loginLogs, err := loginLogRepository.FindAllLoginLogs() + if err != nil { + return err + } + var tokens = make([]string, 0) + for i := range loginLogs { + tokens = append(tokens, loginLogs[i].ID) + } + + if err := userService.DeleteLoginLogs(tokens); err != nil { + return err + } + return Success(c, nil) +} diff --git a/server/api/routes.go b/server/api/routes.go index adb96a4..6645c9b 100644 --- a/server/api/routes.go +++ b/server/api/routes.go @@ -110,6 +110,7 @@ func SetupRoutes(db *gorm.DB) *echo.Echo { users.POST("", UserCreateEndpoint) users.GET("/paging", UserPagingEndpoint) users.PUT("/:id", UserUpdateEndpoint) + users.PATCH("/:id/status", UserUpdateStatusEndpoint) users.DELETE("/:id", UserDeleteEndpoint) users.GET("/:id", UserGetEndpoint) users.POST("/:id/change-password", UserChangePasswordEndpoint) @@ -169,6 +170,10 @@ func SetupRoutes(db *gorm.DB) *echo.Echo { sessions.DELETE("/:id", Admin(SessionDeleteEndpoint)) sessions.GET("/:id/recording", Admin(SessionRecordingEndpoint)) sessions.GET("/:id", Admin(SessionGetEndpoint)) + sessions.POST("/:id/reviewed", Admin(SessionReviewedEndpoint)) + sessions.POST("/:id/unreviewed", Admin(SessionUnViewedEndpoint)) + sessions.POST("/clear", Admin(SessionClearEndpoint)) + sessions.POST("/reviewed", Admin(SessionReviewedAllEndpoint)) sessions.POST("", SessionCreateEndpoint) sessions.POST("/:id/connect", SessionConnectEndpoint) @@ -195,7 +200,7 @@ func SetupRoutes(db *gorm.DB) *echo.Echo { { loginLogs.GET("/paging", LoginLogPagingEndpoint) loginLogs.DELETE("/:id", LoginLogDeleteEndpoint) - //loginLogs.DELETE("/clear", LoginLogClearEndpoint) + loginLogs.POST("/clear", LoginLogClearEndpoint) } e.GET("/properties", Admin(PropertyGetEndpoint)) @@ -268,6 +273,12 @@ func SetupRoutes(db *gorm.DB) *echo.Echo { accessGateways.POST("/:id/reconnect", AccessGatewayReconnectEndpoint) } + backup := e.Group("/backup", Admin) + { + backup.GET("/export", BackupExportEndpoint) + backup.POST("/import", BackupImportEndpoint) + } + return e } diff --git a/server/api/security.go b/server/api/security.go index fc1f02b..7378323 100644 --- a/server/api/security.go +++ b/server/api/security.go @@ -36,7 +36,7 @@ func SecurityCreateEndpoint(c echo.Context) error { } func ReloadAccessSecurity() error { - rules, err := accessSecurityRepository.FindAllAccessSecurities() + rules, err := accessSecurityRepository.FindAll() if err != nil { return err } diff --git a/server/api/session.go b/server/api/session.go index f44b5d7..651790b 100644 --- a/server/api/session.go +++ b/server/api/session.go @@ -35,8 +35,9 @@ func SessionPagingEndpoint(c echo.Context) error { clientIp := c.QueryParam("clientIp") assetId := c.QueryParam("assetId") protocol := c.QueryParam("protocol") + reviewed := c.QueryParam("reviewed") - items, total, err := sessionRepository.Find(pageIndex, pageSize, status, userId, clientIp, assetId, protocol) + items, total, err := sessionRepository.Find(pageIndex, pageSize, status, userId, clientIp, assetId, protocol, reviewed) if err != nil { return err @@ -69,9 +70,8 @@ func SessionPagingEndpoint(c echo.Context) error { } func SessionDeleteEndpoint(c echo.Context) error { - sessionIds := c.Param("id") - split := strings.Split(sessionIds, ",") - err := sessionRepository.DeleteByIds(split) + sessionIds := strings.Split(c.Param("id"), ",") + err := sessionRepository.DeleteByIds(sessionIds) if err != nil { return err } @@ -79,6 +79,37 @@ func SessionDeleteEndpoint(c echo.Context) error { return Success(c, nil) } +func SessionClearEndpoint(c echo.Context) error { + err := sessionService.ClearOfflineSession() + if err != nil { + return err + } + return Success(c, nil) +} + +func SessionReviewedEndpoint(c echo.Context) error { + sessionIds := strings.Split(c.Param("id"), ",") + if err := sessionRepository.UpdateReadByIds(true, sessionIds); err != nil { + return err + } + return Success(c, nil) +} + +func SessionUnViewedEndpoint(c echo.Context) error { + sessionIds := strings.Split(c.Param("id"), ",") + if err := sessionRepository.UpdateReadByIds(false, sessionIds); err != nil { + return err + } + return Success(c, nil) +} + +func SessionReviewedAllEndpoint(c echo.Context) error { + if err := sessionService.ReviewedAll(); err != nil { + return err + } + return Success(c, nil) +} + func SessionConnectEndpoint(c echo.Context) error { sessionId := c.Param("id") @@ -299,6 +330,7 @@ func SessionCreateEndpoint(c echo.Context) error { Edit: edit, StorageId: storageId, AccessGatewayId: asset.AccessGatewayId, + Reviewed: false, } if asset.AccountType == "credential" { @@ -645,6 +677,7 @@ func SessionRecordingEndpoint(c echo.Context) error { } else { recording = s.Recording + "/recording" } + _ = sessionRepository.UpdateReadByIds(true, []string{sessionId}) log.Debugf("读取录屏文件:%v,是否存在: %v, 是否为文件: %v", recording, utils.FileExists(recording), utils.IsFile(recording)) return c.File(recording) diff --git a/server/api/user.go b/server/api/user.go index 773598a..a736234 100644 --- a/server/api/user.go +++ b/server/api/user.go @@ -2,6 +2,7 @@ package api import ( "errors" + "next-terminal/server/constant" "strconv" "strings" @@ -29,6 +30,7 @@ func UserCreateEndpoint(c echo.Context) (err error) { item.ID = utils.UUID() item.Created = utils.NowJsonTime() + item.Status = constant.StatusEnabled if err := userRepository.Create(&item); err != nil { return err @@ -82,6 +84,21 @@ func UserUpdateEndpoint(c echo.Context) error { return Success(c, nil) } +func UserUpdateStatusEndpoint(c echo.Context) error { + id := c.Param("id") + status := c.QueryParam("status") + account, _ := GetCurrentAccount(c) + if account.ID == id { + return Fail(c, -1, "不能操作自身账户") + } + + if err := userService.UpdateStatusById(id, status); err != nil { + return err + } + + return Success(c, nil) +} + func UserDeleteEndpoint(c echo.Context) error { ids := c.Param("id") account, found := GetCurrentAccount(c) @@ -94,26 +111,10 @@ func UserDeleteEndpoint(c echo.Context) error { if account.ID == userId { return Fail(c, -1, "不允许删除自身账户") } - user, err := userRepository.FindById(userId) - if err != nil { + // 下线该用户 + if err := userService.LogoutById(userId); err != nil { return err } - // 将用户强制下线 - loginLogs, err := loginLogRepository.FindAliveLoginLogsByUsername(user.Username) - if err != nil { - return err - } - - for j := range loginLogs { - token := loginLogs[j].ID - cacheKey := userService.BuildCacheKeyByToken(token) - cache.GlobalCache.Delete(cacheKey) - if err := userService.Logout(token); err != nil { - log.WithError(err).WithField("id:", token).Error("Cache Deleted Error") - return Fail(c, 500, "强制下线错误") - } - } - // 删除用户 if err := userRepository.DeleteById(userId); err != nil { return err diff --git a/server/constant/const.go b/server/constant/const.go index f679ead..73f0439 100644 --- a/server/constant/const.go +++ b/server/constant/const.go @@ -56,6 +56,9 @@ const ( TypeUser = "user" // 普通用户 TypeAdmin = "admin" // 管理员 + + StatusEnabled = "enabled" + StatusDisabled = "disabled" ) var SSHParameterNames = []string{guacd.FontName, guacd.FontSize, guacd.ColorScheme, guacd.Backspace, guacd.TerminalType, SshMode} diff --git a/server/model/session.go b/server/model/session.go index bd614a4..be8349c 100644 --- a/server/model/session.go +++ b/server/model/session.go @@ -34,6 +34,7 @@ type Session struct { CreateDir string `gorm:"type:varchar(1)" json:"createDir"` StorageId string `gorm:"type:varchar(36)" json:"storageId"` AccessGatewayId string `gorm:"type:varchar(36)" json:"accessGatewayId"` + Reviewed bool `gorm:"type:tinyint(1)" json:"reviewed"` } func (r *Session) TableName() string { @@ -61,6 +62,7 @@ type SessionForPage struct { Code int `json:"code"` Message string `json:"message"` Mode string `json:"mode"` + Reviewed bool `json:"reviewed"` } type SessionForAccess struct { diff --git a/server/model/user.go b/server/model/user.go index 9cf201d..ae1588f 100644 --- a/server/model/user.go +++ b/server/model/user.go @@ -11,7 +11,7 @@ type User struct { Nickname string `gorm:"type:varchar(500)" json:"nickname"` TOTPSecret string `json:"-"` Online bool `json:"online"` - Enabled bool `json:"enabled"` + Status string `gorm:"type:varchar(10)" json:"status"` Created utils.JsonTime `json:"created"` Type string `gorm:"type:varchar(20)" json:"type"` Mail string `gorm:"type:varchar(500)" json:"mail"` @@ -24,7 +24,7 @@ type UserForPage struct { TOTPSecret string `json:"totpSecret"` Mail string `json:"mail"` Online bool `json:"online"` - Enabled bool `json:"enabled"` + Status string `json:"status"` Created utils.JsonTime `json:"created"` Type string `json:"type"` SharerAssetCount int64 `json:"sharerAssetCount"` diff --git a/server/repository/access_gateway.go b/server/repository/access_gateway.go index 8bf3414..57c3871 100644 --- a/server/repository/access_gateway.go +++ b/server/repository/access_gateway.go @@ -75,11 +75,6 @@ func (r AccessGatewayRepository) FindById(id string) (o model.AccessGateway, err } func (r AccessGatewayRepository) FindAll() (o []model.AccessGateway, err error) { - t := model.AccessGateway{} - db := r.DB.Table(t.TableName()) - err = db.Find(&o).Error - if o == nil { - o = make([]model.AccessGateway, 0) - } + err = r.DB.Find(&o).Error return } diff --git a/server/repository/access_security.go b/server/repository/access_security.go index c5cb92e..1e40bec 100644 --- a/server/repository/access_security.go +++ b/server/repository/access_security.go @@ -15,9 +15,8 @@ func NewAccessSecurityRepository(db *gorm.DB) *AccessSecurityRepository { return accessSecurityRepository } -func (r AccessSecurityRepository) FindAllAccessSecurities() (o []model.AccessSecurity, err error) { - db := r.DB - err = db.Order("priority asc").Find(&o).Error +func (r AccessSecurityRepository) FindAll() (o []model.AccessSecurity, err error) { + err = r.DB.Order("priority asc").Find(&o).Error return } diff --git a/server/repository/command.go b/server/repository/command.go index be1f679..7982c52 100644 --- a/server/repository/command.go +++ b/server/repository/command.go @@ -94,3 +94,8 @@ func (r CommandRepository) FindByUser(account model.User) (o []model.CommandForP } return } + +func (r CommandRepository) FindAll() (o []model.Command, err error) { + err = r.DB.Find(&o).Error + return +} diff --git a/server/repository/login_log.go b/server/repository/login_log.go index 93635ec..d1b89b2 100644 --- a/server/repository/login_log.go +++ b/server/repository/login_log.go @@ -54,6 +54,11 @@ func (r LoginLogRepository) FindAliveLoginLogs() (o []model.LoginLog, err error) return } +func (r LoginLogRepository) FindAllLoginLogs() (o []model.LoginLog, err error) { + err = r.DB.Find(&o).Error + return +} + func (r LoginLogRepository) FindAliveLoginLogsByUsername(username string) (o []model.LoginLog, err error) { err = r.DB.Where("state = '1' and logout_time is null and username = ?", username).Find(&o).Error return diff --git a/server/repository/resource_sharer.go b/server/repository/resource_sharer.go index 01780e1..c6617aa 100644 --- a/server/repository/resource_sharer.go +++ b/server/repository/resource_sharer.go @@ -218,3 +218,8 @@ func (r *ResourceSharerRepository) Find(resourceId, resourceType, userId, userGr err = db.Find(&resourceSharers).Error return } + +func (r *ResourceSharerRepository) FindAll() (o []model.ResourceSharer, err error) { + err = r.DB.Find(&o).Error + return +} diff --git a/server/repository/session.go b/server/repository/session.go index 1ef937b..230a158 100644 --- a/server/repository/session.go +++ b/server/repository/session.go @@ -23,14 +23,14 @@ func NewSessionRepository(db *gorm.DB) *SessionRepository { return sessionRepository } -func (r SessionRepository) Find(pageIndex, pageSize int, status, userId, clientIp, assetId, protocol string) (results []model.SessionForPage, total int64, err error) { +func (r SessionRepository) Find(pageIndex, pageSize int, status, userId, clientIp, assetId, protocol, reviewed string) (results []model.SessionForPage, total int64, err error) { db := r.DB var params []interface{} params = append(params, status) - itemSql := "SELECT s.id,s.mode, s.protocol,s.recording, s.connection_id, s.asset_id, s.creator, s.client_ip, s.width, s.height, s.ip, s.port, s.username, s.status, s.connected_time, s.disconnected_time,s.code, s.message, a.name AS asset_name, u.nickname AS creator_name FROM sessions s LEFT JOIN assets a ON s.asset_id = a.id LEFT JOIN users u ON s.creator = u.id WHERE s.STATUS = ? " + itemSql := "SELECT s.id,s.mode, s.protocol,s.recording, s.connection_id, s.asset_id, s.creator, s.client_ip, s.width, s.height, s.ip, s.port, s.username, s.status, s.connected_time, s.disconnected_time,s.code,s.reviewed, s.message, a.name AS asset_name, u.nickname AS creator_name FROM sessions s LEFT JOIN assets a ON s.asset_id = a.id LEFT JOIN users u ON s.creator = u.id WHERE s.STATUS = ? " countSql := "select count(*) from sessions as s where s.status = ? " if len(userId) > 0 { @@ -57,6 +57,13 @@ func (r SessionRepository) Find(pageIndex, pageSize int, status, userId, clientI params = append(params, protocol) } + if reviewed != "" { + bReviewed := reviewed == "true" + itemSql += " and s.reviewed = ?" + countSql += " and s.reviewed = ?" + params = append(params, bReviewed) + } + params = append(params, (pageIndex-1)*pageSize, pageSize) itemSql += " order by s.connected_time desc LIMIT ?, ?" @@ -209,3 +216,13 @@ func (r SessionRepository) OverviewAccess(account model.User) (o []model.Session } return } + +func (r SessionRepository) UpdateReadByIds(reviewed bool, ids []string) error { + sql := "update sessions set reviewed = ? where id in ?" + return r.DB.Exec(sql, reviewed, ids).Error +} + +func (r SessionRepository) FindAllUnReviewed() (o []model.Session, err error) { + err = r.DB.Where("reviewed = false or reviewed is null").Find(&o).Error + return +} diff --git a/server/repository/user.go b/server/repository/user.go index 7f54799..eacbf62 100644 --- a/server/repository/user.go +++ b/server/repository/user.go @@ -16,15 +16,13 @@ func NewUserRepository(db *gorm.DB) *UserRepository { return userRepository } -func (r UserRepository) FindAll() (o []model.User) { - if r.DB.Find(&o).Error != nil { - return nil - } +func (r UserRepository) FindAll() (o []model.User, err error) { + err = r.DB.Find(&o).Error return } func (r UserRepository) Find(pageIndex, pageSize int, username, nickname, mail, order, field string, account model.User) (o []model.UserForPage, total int64, err error) { - db := r.DB.Table("users").Select("users.id,users.username,users.nickname,users.mail,users.online,users.enabled,users.created,users.type, count(resource_sharers.user_id) as sharer_asset_count, users.totp_secret").Joins("left join resource_sharers on users.id = resource_sharers.user_id and resource_sharers.resource_type = 'asset'").Group("users.id") + db := r.DB.Table("users").Select("users.id,users.username,users.nickname,users.mail,users.online,users.created,users.type,users.status, count(resource_sharers.user_id) as sharer_asset_count, users.totp_secret").Joins("left join resource_sharers on users.id = resource_sharers.user_id and resource_sharers.resource_type = 'asset'").Group("users.id") dbCounter := r.DB.Table("users") if constant.TypeUser == account.Type { diff --git a/server/repository/user_group.go b/server/repository/user_group.go index ade6c42..450ccea 100644 --- a/server/repository/user_group.go +++ b/server/repository/user_group.go @@ -16,10 +16,8 @@ func NewUserGroupRepository(db *gorm.DB) *UserGroupRepository { return userGroupRepository } -func (r UserGroupRepository) FindAll() (o []model.UserGroup) { - if r.DB.Find(&o).Error != nil { - return nil - } +func (r UserGroupRepository) FindAll() (o []model.UserGroup, err error) { + err = r.DB.Find(&o).Error return } @@ -141,3 +139,8 @@ func AddUserGroupMembers(tx *gorm.DB, userIds []string, userGroupId string) erro } return nil } + +func (r UserGroupRepository) FindAllUserGroupMembers() (o []model.UserGroupMember, err error) { + err = r.DB.Find(&o).Error + return +} diff --git a/server/service/access_gateway.go b/server/service/access_gateway.go index 97bdbce..09530c5 100644 --- a/server/service/access_gateway.go +++ b/server/service/access_gateway.go @@ -39,9 +39,12 @@ func (r AccessGatewayService) ReConnectAll() error { if err != nil { return err } - for i := range gateways { - r.ReConnect(&gateways[i]) + if len(gateways) > 0 { + for i := range gateways { + r.ReConnect(&gateways[i]) + } } + return nil } diff --git a/server/service/session.go b/server/service/session.go index ce0c15e..02bc7fe 100644 --- a/server/service/session.go +++ b/server/service/session.go @@ -37,3 +37,41 @@ func (r SessionService) FixSessionState() error { func (r SessionService) EmptyPassword() error { return r.sessionRepository.EmptyPassword() } + +func (r SessionService) ClearOfflineSession() error { + sessions, err := r.sessionRepository.FindByStatus(constant.Disconnected) + if err != nil { + return err + } + sessionIds := make([]string, 0) + for i := range sessions { + sessionIds = append(sessionIds, sessions[i].ID) + } + return r.sessionRepository.DeleteByIds(sessionIds) +} + +func (r SessionService) ReviewedAll() error { + sessions, err := r.sessionRepository.FindAllUnReviewed() + if err != nil { + return err + } + var sessionIds = make([]string, 0) + total := len(sessions) + for i := range sessions { + sessionIds = append(sessionIds, sessions[i].ID) + if i >= 100 && i%100 == 0 { + if err := r.sessionRepository.UpdateReadByIds(true, sessionIds); err != nil { + return err + } + sessionIds = nil + } else { + if i == total-1 { + if err := r.sessionRepository.UpdateReadByIds(true, sessionIds); err != nil { + return err + } + } + } + + } + return nil +} diff --git a/server/service/storage.go b/server/service/storage.go index 36576a6..1893843 100644 --- a/server/service/storage.go +++ b/server/service/storage.go @@ -26,7 +26,10 @@ func NewStorageService(storageRepository *repository.StorageRepository, userRepo } func (r StorageService) InitStorages() error { - users := r.userRepository.FindAll() + users, err := r.userRepository.FindAll() + if err != nil { + return err + } for i := range users { userId := users[i].ID _, err := r.storageRepository.FindByOwnerIdAndDefault(userId, true) diff --git a/server/service/user.go b/server/service/user.go index 3c429df..89b4807 100644 --- a/server/service/user.go +++ b/server/service/user.go @@ -1,6 +1,7 @@ package service import ( + "next-terminal/server/global/cache" "strings" "next-terminal/server/constant" @@ -21,7 +22,10 @@ func NewUserService(userRepository *repository.UserRepository, loginLogRepositor func (r UserService) InitUser() (err error) { - users := r.userRepository.FindAll() + users, err := r.userRepository.FindAll() + if err != nil { + return err + } if len(users) == 0 { initPassword := "admin" @@ -37,6 +41,7 @@ func (r UserService) InitUser() (err error) { Nickname: "超级管理员", Type: constant.TypeAdmin, Created: utils.NowJsonTime(), + Status: constant.StatusEnabled, } if err := r.userRepository.Create(&user); err != nil { return err @@ -83,12 +88,14 @@ func (r UserService) FixUserOnlineState() error { return nil } -func (r UserService) Logout(token string) (err error) { +func (r UserService) LogoutByToken(token string) (err error) { loginLog, err := r.loginLogRepository.FindById(token) if err != nil { log.Warnf("登录日志「%v」获取失败", token) return } + cacheKey := r.BuildCacheKeyByToken(token) + cache.GlobalCache.Delete(cacheKey) loginLogForUpdate := &model.LoginLog{LogoutTime: utils.NowJsonTime(), ID: token} err = r.loginLogRepository.Update(loginLogForUpdate) @@ -107,6 +114,26 @@ func (r UserService) Logout(token string) (err error) { return } +func (r UserService) LogoutById(id string) error { + user, err := r.userRepository.FindById(id) + if err != nil { + return err + } + username := user.Username + loginLogs, err := r.loginLogRepository.FindAliveLoginLogsByUsername(username) + if err != nil { + return err + } + + for j := range loginLogs { + token := loginLogs[j].ID + if err := r.LogoutByToken(token); err != nil { + return err + } + } + return nil +} + func (r UserService) BuildCacheKeyByToken(token string) string { cacheKey := strings.Join([]string{constant.Token, token}, ":") return cacheKey @@ -121,9 +148,36 @@ func (r UserService) OnEvicted(key string, value interface{}) { if strings.HasPrefix(key, constant.Token) { token := r.GetTokenFormCacheKey(key) log.Debugf("用户Token「%v」过期", token) - err := r.Logout(token) + err := r.LogoutByToken(token) if err != nil { log.Errorf("退出登录失败 %v", err) } } } + +func (r UserService) UpdateStatusById(id string, status string) error { + if constant.StatusDisabled == status { + // 将该用户下线 + if err := r.LogoutById(id); err != nil { + return err + } + } + u := model.User{ + ID: id, + Status: status, + } + return r.userRepository.Update(&u) +} + +func (r UserService) DeleteLoginLogs(tokens []string) error { + for i := range tokens { + token := tokens[i] + if err := r.LogoutByToken(token); err != nil { + return err + } + if err := r.loginLogRepository.DeleteById(token); err != nil { + return err + } + } + return nil +} diff --git a/server/term/ssh.go b/server/term/ssh.go index f680ed5..9e0b032 100644 --- a/server/term/ssh.go +++ b/server/term/ssh.go @@ -49,6 +49,29 @@ func NewSshClient(ip string, port int, username, password, privateKey, passphras } addr := fmt.Sprintf("%s:%d", ip, port) + // + //socks5, err := proxy.SOCKS5("tcp", "", + // &proxy.Auth{User: "username", Password: "password"}, + // &net.Dialer{ + // Timeout: 30 * time.Second, + // KeepAlive: 30 * time.Second, + // }, + //) + //if err != nil { + // return nil, err + //} + // + //conn, err := socks5.Dial("tcp", addr) + //if err != nil { + // return nil, err + //} + // + //clientConn, channels, requests, err := ssh.NewClientConn(conn, addr, config) + //if err != nil { + // return nil, err + //} + // + //ssh.NewClient(clientConn, channels, requests) return ssh.Dial("tcp", addr, config) } diff --git a/web/src/components/asset/AccessGateway.js b/web/src/components/asset/AccessGateway.js index 74b90b6..2f92d60 100644 --- a/web/src/components/asset/AccessGateway.js +++ b/web/src/components/asset/AccessGateway.js @@ -141,15 +141,27 @@ class AccessGateway extends Component { await this.showModal('更新接入网关', result.data); } - async reconnect(id) { - message.info({content: '正在重连中...', key: id, duration: 5}); - let result = await request.post(`/access-gateways/${id}/reconnect`); - if (result.code !== 1) { - message.error({content: result.message, key: id, duration: 10}); - return; + async reconnect(id, index) { + let items = this.state.items; + try { + items[index]['reconnectLoading'] = true; + this.setState({ + items: items + }); + message.info({content: '正在重连中...', key: id, duration: 5}); + let result = await request.post(`/access-gateways/${id}/reconnect`); + if (result.code !== 1) { + message.error({content: result.message, key: id, duration: 10}); + return; + } + message.success({content: '重连完成。', key: id, duration: 3}); + this.loadTableData(this.state.queryParams); + } finally { + items[index]['reconnectLoading'] = false; + this.setState({ + items: items + }); } - message.success({content: '重连完成。', key: id, duration: 3}); - this.loadTableData(this.state.queryParams); } showModal(title, obj) { @@ -173,37 +185,39 @@ class AccessGateway extends Component { modalConfirmLoading: true }); - if (formData.id) { - // 向后台提交数据 - const result = await request.put('/access-gateways/' + formData.id, formData); - if (result.code === 1) { - message.success('更新成功'); + try { + if (formData.id) { + // 向后台提交数据 + const result = await request.put('/access-gateways/' + formData.id, formData); + if (result.code === 1) { + message.success('更新成功'); - this.setState({ - modalVisible: false - }); - this.loadTableData(this.state.queryParams); + this.setState({ + modalVisible: false + }); + this.loadTableData(this.state.queryParams); + } else { + message.error('更新失败 :( ' + result.message, 10); + } } else { - message.error('更新失败 :( ' + result.message, 10); - } - } else { - // 向后台提交数据 - const result = await request.post('/access-gateways', formData); - if (result.code === 1) { - message.success('新增成功'); + // 向后台提交数据 + const result = await request.post('/access-gateways', formData); + if (result.code === 1) { + message.success('新增成功'); - this.setState({ - modalVisible: false - }); - this.loadTableData(this.state.queryParams); - } else { - message.error('新增失败 :( ' + result.message, 10); + this.setState({ + modalVisible: false + }); + this.loadTableData(this.state.queryParams); + } else { + message.error('新增失败 :( ' + result.message, 10); + } } + } finally { + this.setState({ + modalConfirmLoading: false + }); } - - this.setState({ - modalConfirmLoading: false - }); }; batchDelete = async () => { @@ -340,11 +354,11 @@ class AccessGateway extends Component { return (
- - + diff --git a/web/src/components/dashboard/Dashboard.js b/web/src/components/dashboard/Dashboard.js index 79e772c..2612368 100644 --- a/web/src/components/dashboard/Dashboard.js +++ b/web/src/components/dashboard/Dashboard.js @@ -131,7 +131,7 @@ class Dashboard extends Component {
- + }/> @@ -139,7 +139,7 @@ class Dashboard extends Component { - + }/> @@ -147,7 +147,7 @@ class Dashboard extends Component { - + }/> @@ -156,7 +156,7 @@ class Dashboard extends Component { - + }/> diff --git a/web/src/components/devops/LoginLog.js b/web/src/components/devops/LoginLog.js index 75aedbc..94b9642 100644 --- a/web/src/components/devops/LoginLog.js +++ b/web/src/components/devops/LoginLog.js @@ -20,7 +20,7 @@ import qs from "qs"; import request from "../../common/request"; import {formatDate, isEmpty} from "../../utils/utils"; import {message} from "antd/es"; -import {DeleteOutlined, ExclamationCircleOutlined, SyncOutlined, UndoOutlined} from "@ant-design/icons"; +import {ClearOutlined, DeleteOutlined, ExclamationCircleOutlined, SyncOutlined, UndoOutlined} from "@ant-design/icons"; const confirm = Modal.confirm; @@ -152,6 +152,30 @@ class LoginLog extends Component { } } + clearLoginLogs = async () => { + this.setState({ + clearBtnLoading: true + }) + try { + let result = await request.post('/login-logs/clear'); + if (result.code === 1) { + message.success('操作成功,即将跳转至登录页面。', 3); + this.setState({ + selectedRowKeys: [] + }) + setTimeout(function () { + window.location.reload(); + }, 3000); + } else { + message.error(result.message, 10); + } + } finally { + this.setState({ + clearBtnLoading: false + }) + } + } + render() { const columns = [{ @@ -353,6 +377,26 @@ class LoginLog extends Component { + + + diff --git a/web/src/components/session/OfflineSession.css b/web/src/components/session/OfflineSession.css new file mode 100644 index 0000000..d3d5857 --- /dev/null +++ b/web/src/components/session/OfflineSession.css @@ -0,0 +1,3 @@ +.unreviewed{ + background: #e6f7ff; +} \ No newline at end of file diff --git a/web/src/components/session/OfflineSession.js b/web/src/components/session/OfflineSession.js index a6016c4..581a666 100644 --- a/web/src/components/session/OfflineSession.js +++ b/web/src/components/session/OfflineSession.js @@ -20,9 +20,16 @@ import request from "../../common/request"; import {differTime} from "../../utils/utils"; import Playback from "./Playback"; import {message} from "antd/es"; -import {DeleteOutlined, ExclamationCircleOutlined, SyncOutlined, UndoOutlined} from "@ant-design/icons"; +import { + CheckOutlined, + ClearOutlined, + DeleteOutlined, + ExclamationCircleOutlined, EyeInvisibleOutlined, EyeOutlined, + SyncOutlined, + UndoOutlined +} from "@ant-design/icons"; import {MODE_COLORS, PROTOCOL_COLORS} from "../../common/constants"; - +import './OfflineSession.css' import dayjs from "dayjs"; const confirm = Modal.confirm; @@ -42,7 +49,8 @@ class OfflineSession extends Component { pageSize: 10, protocol: '', userId: undefined, - assetId: undefined + assetId: undefined, + reviewed: undefined }, loading: false, playbackVisible: false, @@ -147,6 +155,16 @@ class OfflineSession extends Component { this.loadTableData(query); } + handleChangeByRead = reviewed => { + let query = { + ...this.state.queryParams, + 'pageIndex': 1, + 'pageSize': this.state.queryParams.pageSize, + 'reviewed': reviewed, + } + this.loadTableData(query); + } + handleSearchByNickname = async nickname => { const result = await request.get(`/users/paging?pageIndex=1&pageSize=1000&nickname=${nickname}`); if (result.code !== 1) { @@ -213,6 +231,110 @@ class OfflineSession extends Component { } } + handleAllReviewed = async () => { + this.setState({ + reviewedAllBtnLoading: true + }) + try { + let result = await request.post(`/sessions/reviewed`); + 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({ + reviewedAllBtnLoading: false + }) + } + } + + handleReviewed = async () => { + this.setState({ + reviewedBtnLoading: true + }) + try { + let result = await request.post(`/sessions/${this.state.selectedRowKeys.join(',')}/reviewed`); + 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({ + reviewedBtnLoading: false + }) + } + } + + handleUnreviewed = async () => { + this.setState({ + unreviewedBtnLoading: true + }) + try { + let result = await request.post(`/sessions/${this.state.selectedRowKeys.join(',')}/unreviewed`); + 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({ + unreviewedBtnLoading: false + }) + } + } + + del = async (id) => { + const result = await request.delete(`/sessions/${id}`); + if (result.code === 1) { + notification['success']({ + message: '提示', + description: '删除成功', + }); + this.loadTableData(); + } else { + notification['error']({ + message: '提示', + description: result.message, + }); + } + } + + clearSession = async () => { + this.setState({ + clearBtnLoading: true + }) + try { + let result = await request.post('/sessions/clear'); + 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({ + clearBtnLoading: false + }) + } + } + render() { const columns = [{ @@ -221,7 +343,7 @@ class OfflineSession extends Component { key: 'id', render: (id, record, index) => { return index + 1; - } + }, }, { title: '来源IP', dataIndex: 'clientIp', @@ -312,34 +434,17 @@ class OfflineSession extends Component { } }); }}>禁用IP -
) @@ -366,10 +471,10 @@ class OfflineSession extends Component {
- + 离线会话列表 - + + + diff --git a/web/src/components/setting/Setting.js b/web/src/components/setting/Setting.js index 0a534d9..b59dffd 100644 --- a/web/src/components/setting/Setting.js +++ b/web/src/components/setting/Setting.js @@ -1,8 +1,10 @@ import React, {Component} from 'react'; -import {Button, Form, Input, Layout, Select, Switch, Tabs, Tooltip, Typography} from "antd"; +import {Alert, Button, Form, Input, Layout, Select, Space, Switch, Tabs, Tooltip, Typography} from "antd"; import request from "../../common/request"; import {message} from "antd/es"; import {ExclamationCircleOutlined} from "@ant-design/icons"; +import {download, getToken} from "../../utils/utils"; +import {server} from "../../common/env"; const {Content} = Layout; const {Option} = Select; @@ -91,6 +93,10 @@ class Setting extends Component { if (this.mailSettingFormRef.current) { this.mailSettingFormRef.current.setFieldsValue(properties) } + + if (this.otherSettingFormRef.current) { + this.otherSettingFormRef.current.setFieldsValue(properties) + } } else { message.error(result['message']); } @@ -443,7 +449,6 @@ class Setting extends Component { } - + + + + + + diff --git a/web/src/components/user/User.js b/web/src/components/user/User.js index fa498ed..ab8e2e6 100644 --- a/web/src/components/user/User.js +++ b/web/src/components/user/User.js @@ -14,6 +14,7 @@ import { Modal, Row, Space, + Switch, Table, Tag, Tooltip, @@ -26,14 +27,14 @@ import {message} from "antd/es"; import { DeleteOutlined, DownOutlined, - ExclamationCircleOutlined, + ExclamationCircleOutlined, FrownOutlined, InsuranceOutlined, LockOutlined, PlusOutlined, SyncOutlined, UndoOutlined } from '@ant-design/icons'; -import {hasPermission} from "../../service/permission"; +import {getCurrentUser} from "../../service/permission"; import dayjs from "dayjs"; import UserShareSelectedAsset from "./UserShareSelectedAsset"; @@ -73,16 +74,6 @@ class User extends Component { this.loadTableData(); } - async delete(id) { - let result = await request.delete('/users/' + 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 @@ -129,11 +120,10 @@ class User extends Component { queryParams: queryParams }); - this.loadTableData(queryParams).then(r => { - }) + this.loadTableData(queryParams); }; - showDeleteConfirm(id, content) { + showDeleteConfirm(id, content, index) { let self = this; confirm({ title: '您确定要删除此用户吗?', @@ -142,7 +132,7 @@ class User extends Component { okType: 'danger', cancelText: '取消', onOk() { - self.delete(id); + self.delete(id, index); } }); }; @@ -167,7 +157,6 @@ class User extends Component { this.setState({ modalConfirmLoading: true }); - if (formData.id) { // 向后台提交数据 const result = await request.put('/users/' + formData.id, formData); @@ -293,6 +282,72 @@ class User extends Component { this.loadTableData(query); } + async delete(id, index) { + let items = this.state.items; + try { + items[index]['delLoading'] = true; + this.setState({ + items: items + }); + let result = await request.delete('/users/' + id); + if (result.code === 1) { + message.success('操作成功', 3); + await this.loadTableData(this.state.queryParams); + } else { + message.error(result.message, 10); + } + } finally { + items[index]['delLoading'] = false; + this.setState({ + items: items + }); + } + } + + changeUserStatus = async (id, checked, index) => { + let items = this.state.items; + try { + items[index]['statusLoading'] = true; + this.setState({ + items: items + }); + let result = await request.patch(`/users/${id}/status?status=${checked ? 'enabled' : 'disabled'}`); + if (result['code'] !== 1) { + message.error(result['message']); + return + } + this.loadTableData(this.state.queryParams); + } finally { + items[index]['statusLoading'] = false; + this.setState({ + items: items + }); + } + } + + resetTOTP = async (id, index) => { + let items = this.state.items; + try { + items[index]['resetTOTPLoading'] = true; + this.setState({ + items: items + }); + let result = await request.post(`/users/${id}/reset-totp`); + if (result['code'] === 1) { + message.success('操作成功', 3); + this.loadTableData(); + } else { + message.error(result['message'], 10); + } + } finally { + items[index]['resetTOTPLoading'] = false; + this.setState({ + items: items + }); + } + + } + render() { const columns = [{ @@ -348,15 +403,28 @@ class User extends Component { dataIndex: 'mail', key: 'mail', }, { - title: '二次认证', + title: '状态', + dataIndex: 'status', + key: 'status', + render: (status, record, index) => { + return { + this.changeUserStatus(record['id'], checked, index); + }}/> + } + }, { + title: '双因素认证', dataIndex: 'totpSecret', key: 'totpSecret', render: (text, record) => { if (text === '1') { - return } color="success">开启; + return } color="success">已开启; } else { - return } color="warning">关闭; + return } color="warning">未开启; } } }, { @@ -398,7 +466,7 @@ class User extends Component { { title: '操作', key: 'action', - render: (text, record) => { + render: (text, record, index) => { const menu = ( @@ -414,6 +482,7 @@ class User extends Component { @@ -446,8 +509,9 @@ class User extends Component { + disabled={getCurrentUser()['id'] === record['id']} + loading={record['delLoading']} + onClick={() => this.showDeleteConfirm(record.id, record.name, index)}>删除 ); @@ -544,53 +608,6 @@ class User extends Component { - {/**/} - {/* */} - {/**/} - - {/**/} - {/* */} - {/**/} -