- 修复「修改接入网关失败」的问题
- 完成「[功能请求]审计的历史会话建议添加“已阅”的功能」close #194 - 增加一键删除登录日志和历史会话的功能
This commit is contained in:
		
							
								
								
									
										1
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								go.mod
									
									
									
									
									
								
							| @ -21,6 +21,7 @@ require ( | |||||||
| 	github.com/spf13/viper v1.7.1 | 	github.com/spf13/viper v1.7.1 | ||||||
| 	github.com/stretchr/testify v1.6.1 | 	github.com/stretchr/testify v1.6.1 | ||||||
| 	golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e | 	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 | 	golang.org/x/text v0.3.6 | ||||||
| 	gopkg.in/natefinch/lumberjack.v2 v2.0.0 | 	gopkg.in/natefinch/lumberjack.v2 v2.0.0 | ||||||
| 	gorm.io/driver/mysql v1.0.3 | 	gorm.io/driver/mysql v1.0.3 | ||||||
|  | |||||||
| @ -78,8 +78,7 @@ func AccessGatewayUpdateEndpoint(c echo.Context) error { | |||||||
| 	if err := accessGatewayRepository.UpdateById(&item, id); err != nil { | 	if err := accessGatewayRepository.UpdateById(&item, id); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	accessGatewayService.DisconnectById(id) | 	accessGatewayService.ReConnect(&item) | ||||||
| 	_, _ = accessGatewayService.GetGatewayAndReconnectById(id) |  | ||||||
| 	return Success(c, nil) | 	return Success(c, nil) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| package api | package api | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"next-terminal/server/constant" | ||||||
| 	"path" | 	"path" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
| @ -49,8 +50,6 @@ func LoginEndpoint(c echo.Context) error { | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	user, err := userRepository.FindByUsername(loginAccount.Username) |  | ||||||
|  |  | ||||||
| 	// 存储登录失败次数信息 | 	// 存储登录失败次数信息 | ||||||
| 	loginFailCountKey := c.RealIP() + loginAccount.Username | 	loginFailCountKey := c.RealIP() + loginAccount.Username | ||||||
| 	v, ok := cache.GlobalCache.Get(loginFailCountKey) | 	v, ok := cache.GlobalCache.Get(loginFailCountKey) | ||||||
| @ -62,6 +61,7 @@ func LoginEndpoint(c echo.Context) error { | |||||||
| 		return Fail(c, -1, "登录失败次数过多,请等待5分钟后再试") | 		return Fail(c, -1, "登录失败次数过多,请等待5分钟后再试") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	user, err := userRepository.FindByUsername(loginAccount.Username) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		count++ | 		count++ | ||||||
| 		cache.GlobalCache.Set(loginFailCountKey, count, time.Minute*time.Duration(5)) | 		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) | 		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 { | 	if err := utils.Encoder.Match([]byte(user.Password), []byte(loginAccount.Password)); err != nil { | ||||||
| 		count++ | 		count++ | ||||||
| 		cache.GlobalCache.Set(loginFailCountKey, count, time.Minute*time.Duration(5)) | 		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) | 		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 { | 	if err := utils.Encoder.Match([]byte(user.Password), []byte(loginAccount.Password)); err != nil { | ||||||
| 		count++ | 		count++ | ||||||
| 		cache.GlobalCache.Set(loginFailCountKey, count, time.Minute*time.Duration(5)) | 		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 { | func LogoutEndpoint(c echo.Context) error { | ||||||
| 	token := GetToken(c) | 	token := GetToken(c) | ||||||
| 	cacheKey := userService.BuildCacheKeyByToken(token) | 	err := userService.LogoutByToken(token) | ||||||
| 	cache.GlobalCache.Delete(cacheKey) |  | ||||||
| 	err := userService.Logout(token) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  | |||||||
							
								
								
									
										102
									
								
								server/api/backup.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								server/api/backup.go
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||||
|  | } | ||||||
| @ -4,9 +4,6 @@ import ( | |||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"next-terminal/server/global/cache" |  | ||||||
| 	"next-terminal/server/log" |  | ||||||
|  |  | ||||||
| 	"github.com/labstack/echo/v4" | 	"github.com/labstack/echo/v4" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @ -31,24 +28,26 @@ func LoginLogPagingEndpoint(c echo.Context) error { | |||||||
|  |  | ||||||
| func LoginLogDeleteEndpoint(c echo.Context) error { | func LoginLogDeleteEndpoint(c echo.Context) error { | ||||||
| 	ids := c.Param("id") | 	ids := c.Param("id") | ||||||
| 	split := strings.Split(ids, ",") | 	tokens := strings.Split(ids, ",") | ||||||
| 	for i := range split { | 	if err := userService.DeleteLoginLogs(tokens); err != nil { | ||||||
| 		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 { |  | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return Success(c, nil) | 	return Success(c, nil) | ||||||
| } | } | ||||||
|  |  | ||||||
| //func LoginLogClearEndpoint(c echo.Context) error { | func LoginLogClearEndpoint(c echo.Context) error { | ||||||
| //	loginLogs, err := loginLogRepository.FindAliveLoginLogs() | 	loginLogs, err := loginLogRepository.FindAllLoginLogs() | ||||||
| //	if err != nil { | 	if err != nil { | ||||||
| //		return err | 		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) | ||||||
|  | } | ||||||
|  | |||||||
| @ -110,6 +110,7 @@ func SetupRoutes(db *gorm.DB) *echo.Echo { | |||||||
| 		users.POST("", UserCreateEndpoint) | 		users.POST("", UserCreateEndpoint) | ||||||
| 		users.GET("/paging", UserPagingEndpoint) | 		users.GET("/paging", UserPagingEndpoint) | ||||||
| 		users.PUT("/:id", UserUpdateEndpoint) | 		users.PUT("/:id", UserUpdateEndpoint) | ||||||
|  | 		users.PATCH("/:id/status", UserUpdateStatusEndpoint) | ||||||
| 		users.DELETE("/:id", UserDeleteEndpoint) | 		users.DELETE("/:id", UserDeleteEndpoint) | ||||||
| 		users.GET("/:id", UserGetEndpoint) | 		users.GET("/:id", UserGetEndpoint) | ||||||
| 		users.POST("/:id/change-password", UserChangePasswordEndpoint) | 		users.POST("/:id/change-password", UserChangePasswordEndpoint) | ||||||
| @ -169,6 +170,10 @@ func SetupRoutes(db *gorm.DB) *echo.Echo { | |||||||
| 		sessions.DELETE("/:id", Admin(SessionDeleteEndpoint)) | 		sessions.DELETE("/:id", Admin(SessionDeleteEndpoint)) | ||||||
| 		sessions.GET("/:id/recording", Admin(SessionRecordingEndpoint)) | 		sessions.GET("/:id/recording", Admin(SessionRecordingEndpoint)) | ||||||
| 		sessions.GET("/:id", Admin(SessionGetEndpoint)) | 		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("", SessionCreateEndpoint) | ||||||
| 		sessions.POST("/:id/connect", SessionConnectEndpoint) | 		sessions.POST("/:id/connect", SessionConnectEndpoint) | ||||||
| @ -195,7 +200,7 @@ func SetupRoutes(db *gorm.DB) *echo.Echo { | |||||||
| 	{ | 	{ | ||||||
| 		loginLogs.GET("/paging", LoginLogPagingEndpoint) | 		loginLogs.GET("/paging", LoginLogPagingEndpoint) | ||||||
| 		loginLogs.DELETE("/:id", LoginLogDeleteEndpoint) | 		loginLogs.DELETE("/:id", LoginLogDeleteEndpoint) | ||||||
| 		//loginLogs.DELETE("/clear", LoginLogClearEndpoint) | 		loginLogs.POST("/clear", LoginLogClearEndpoint) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	e.GET("/properties", Admin(PropertyGetEndpoint)) | 	e.GET("/properties", Admin(PropertyGetEndpoint)) | ||||||
| @ -268,6 +273,12 @@ func SetupRoutes(db *gorm.DB) *echo.Echo { | |||||||
| 		accessGateways.POST("/:id/reconnect", AccessGatewayReconnectEndpoint) | 		accessGateways.POST("/:id/reconnect", AccessGatewayReconnectEndpoint) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	backup := e.Group("/backup", Admin) | ||||||
|  | 	{ | ||||||
|  | 		backup.GET("/export", BackupExportEndpoint) | ||||||
|  | 		backup.POST("/import", BackupImportEndpoint) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return e | 	return e | ||||||
| } | } | ||||||
|  |  | ||||||
|  | |||||||
| @ -36,7 +36,7 @@ func SecurityCreateEndpoint(c echo.Context) error { | |||||||
| } | } | ||||||
|  |  | ||||||
| func ReloadAccessSecurity() error { | func ReloadAccessSecurity() error { | ||||||
| 	rules, err := accessSecurityRepository.FindAllAccessSecurities() | 	rules, err := accessSecurityRepository.FindAll() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -35,8 +35,9 @@ func SessionPagingEndpoint(c echo.Context) error { | |||||||
| 	clientIp := c.QueryParam("clientIp") | 	clientIp := c.QueryParam("clientIp") | ||||||
| 	assetId := c.QueryParam("assetId") | 	assetId := c.QueryParam("assetId") | ||||||
| 	protocol := c.QueryParam("protocol") | 	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 { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| @ -69,9 +70,8 @@ func SessionPagingEndpoint(c echo.Context) error { | |||||||
| } | } | ||||||
|  |  | ||||||
| func SessionDeleteEndpoint(c echo.Context) error { | func SessionDeleteEndpoint(c echo.Context) error { | ||||||
| 	sessionIds := c.Param("id") | 	sessionIds := strings.Split(c.Param("id"), ",") | ||||||
| 	split := strings.Split(sessionIds, ",") | 	err := sessionRepository.DeleteByIds(sessionIds) | ||||||
| 	err := sessionRepository.DeleteByIds(split) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| @ -79,6 +79,37 @@ func SessionDeleteEndpoint(c echo.Context) error { | |||||||
| 	return Success(c, nil) | 	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 { | func SessionConnectEndpoint(c echo.Context) error { | ||||||
| 	sessionId := c.Param("id") | 	sessionId := c.Param("id") | ||||||
|  |  | ||||||
| @ -299,6 +330,7 @@ func SessionCreateEndpoint(c echo.Context) error { | |||||||
| 		Edit:            edit, | 		Edit:            edit, | ||||||
| 		StorageId:       storageId, | 		StorageId:       storageId, | ||||||
| 		AccessGatewayId: asset.AccessGatewayId, | 		AccessGatewayId: asset.AccessGatewayId, | ||||||
|  | 		Reviewed:        false, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if asset.AccountType == "credential" { | 	if asset.AccountType == "credential" { | ||||||
| @ -645,6 +677,7 @@ func SessionRecordingEndpoint(c echo.Context) error { | |||||||
| 	} else { | 	} else { | ||||||
| 		recording = s.Recording + "/recording" | 		recording = s.Recording + "/recording" | ||||||
| 	} | 	} | ||||||
|  | 	_ = sessionRepository.UpdateReadByIds(true, []string{sessionId}) | ||||||
|  |  | ||||||
| 	log.Debugf("读取录屏文件:%v,是否存在: %v, 是否为文件: %v", recording, utils.FileExists(recording), utils.IsFile(recording)) | 	log.Debugf("读取录屏文件:%v,是否存在: %v, 是否为文件: %v", recording, utils.FileExists(recording), utils.IsFile(recording)) | ||||||
| 	return c.File(recording) | 	return c.File(recording) | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ package api | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
|  | 	"next-terminal/server/constant" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| @ -29,6 +30,7 @@ func UserCreateEndpoint(c echo.Context) (err error) { | |||||||
|  |  | ||||||
| 	item.ID = utils.UUID() | 	item.ID = utils.UUID() | ||||||
| 	item.Created = utils.NowJsonTime() | 	item.Created = utils.NowJsonTime() | ||||||
|  | 	item.Status = constant.StatusEnabled | ||||||
|  |  | ||||||
| 	if err := userRepository.Create(&item); err != nil { | 	if err := userRepository.Create(&item); err != nil { | ||||||
| 		return err | 		return err | ||||||
| @ -82,6 +84,21 @@ func UserUpdateEndpoint(c echo.Context) error { | |||||||
| 	return Success(c, nil) | 	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 { | func UserDeleteEndpoint(c echo.Context) error { | ||||||
| 	ids := c.Param("id") | 	ids := c.Param("id") | ||||||
| 	account, found := GetCurrentAccount(c) | 	account, found := GetCurrentAccount(c) | ||||||
| @ -94,26 +111,10 @@ func UserDeleteEndpoint(c echo.Context) error { | |||||||
| 		if account.ID == userId { | 		if account.ID == userId { | ||||||
| 			return Fail(c, -1, "不允许删除自身账户") | 			return Fail(c, -1, "不允许删除自身账户") | ||||||
| 		} | 		} | ||||||
| 		user, err := userRepository.FindById(userId) | 		// 下线该用户 | ||||||
| 		if err != nil { | 		if err := userService.LogoutById(userId); err != nil { | ||||||
| 			return err | 			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 { | 		if err := userRepository.DeleteById(userId); err != nil { | ||||||
| 			return err | 			return err | ||||||
|  | |||||||
| @ -56,6 +56,9 @@ const ( | |||||||
|  |  | ||||||
| 	TypeUser  = "user"  // 普通用户 | 	TypeUser  = "user"  // 普通用户 | ||||||
| 	TypeAdmin = "admin" // 管理员 | 	TypeAdmin = "admin" // 管理员 | ||||||
|  |  | ||||||
|  | 	StatusEnabled  = "enabled" | ||||||
|  | 	StatusDisabled = "disabled" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var SSHParameterNames = []string{guacd.FontName, guacd.FontSize, guacd.ColorScheme, guacd.Backspace, guacd.TerminalType, SshMode} | var SSHParameterNames = []string{guacd.FontName, guacd.FontSize, guacd.ColorScheme, guacd.Backspace, guacd.TerminalType, SshMode} | ||||||
|  | |||||||
| @ -34,6 +34,7 @@ type Session struct { | |||||||
| 	CreateDir        string         `gorm:"type:varchar(1)" json:"createDir"` | 	CreateDir        string         `gorm:"type:varchar(1)" json:"createDir"` | ||||||
| 	StorageId        string         `gorm:"type:varchar(36)" json:"storageId"` | 	StorageId        string         `gorm:"type:varchar(36)" json:"storageId"` | ||||||
| 	AccessGatewayId  string         `gorm:"type:varchar(36)" json:"accessGatewayId"` | 	AccessGatewayId  string         `gorm:"type:varchar(36)" json:"accessGatewayId"` | ||||||
|  | 	Reviewed         bool           `gorm:"type:tinyint(1)" json:"reviewed"` | ||||||
| } | } | ||||||
|  |  | ||||||
| func (r *Session) TableName() string { | func (r *Session) TableName() string { | ||||||
| @ -61,6 +62,7 @@ type SessionForPage struct { | |||||||
| 	Code             int            `json:"code"` | 	Code             int            `json:"code"` | ||||||
| 	Message          string         `json:"message"` | 	Message          string         `json:"message"` | ||||||
| 	Mode             string         `json:"mode"` | 	Mode             string         `json:"mode"` | ||||||
|  | 	Reviewed         bool           `json:"reviewed"` | ||||||
| } | } | ||||||
|  |  | ||||||
| type SessionForAccess struct { | type SessionForAccess struct { | ||||||
|  | |||||||
| @ -11,7 +11,7 @@ type User struct { | |||||||
| 	Nickname   string         `gorm:"type:varchar(500)" json:"nickname"` | 	Nickname   string         `gorm:"type:varchar(500)" json:"nickname"` | ||||||
| 	TOTPSecret string         `json:"-"` | 	TOTPSecret string         `json:"-"` | ||||||
| 	Online     bool           `json:"online"` | 	Online     bool           `json:"online"` | ||||||
| 	Enabled    bool           `json:"enabled"` | 	Status     string         `gorm:"type:varchar(10)" json:"status"` | ||||||
| 	Created    utils.JsonTime `json:"created"` | 	Created    utils.JsonTime `json:"created"` | ||||||
| 	Type       string         `gorm:"type:varchar(20)" json:"type"` | 	Type       string         `gorm:"type:varchar(20)" json:"type"` | ||||||
| 	Mail       string         `gorm:"type:varchar(500)" json:"mail"` | 	Mail       string         `gorm:"type:varchar(500)" json:"mail"` | ||||||
| @ -24,7 +24,7 @@ type UserForPage struct { | |||||||
| 	TOTPSecret       string         `json:"totpSecret"` | 	TOTPSecret       string         `json:"totpSecret"` | ||||||
| 	Mail             string         `json:"mail"` | 	Mail             string         `json:"mail"` | ||||||
| 	Online           bool           `json:"online"` | 	Online           bool           `json:"online"` | ||||||
| 	Enabled          bool           `json:"enabled"` | 	Status           string         `json:"status"` | ||||||
| 	Created          utils.JsonTime `json:"created"` | 	Created          utils.JsonTime `json:"created"` | ||||||
| 	Type             string         `json:"type"` | 	Type             string         `json:"type"` | ||||||
| 	SharerAssetCount int64          `json:"sharerAssetCount"` | 	SharerAssetCount int64          `json:"sharerAssetCount"` | ||||||
|  | |||||||
| @ -75,11 +75,6 @@ func (r AccessGatewayRepository) FindById(id string) (o model.AccessGateway, err | |||||||
| } | } | ||||||
|  |  | ||||||
| func (r AccessGatewayRepository) FindAll() (o []model.AccessGateway, err error) { | func (r AccessGatewayRepository) FindAll() (o []model.AccessGateway, err error) { | ||||||
| 	t := model.AccessGateway{} | 	err = r.DB.Find(&o).Error | ||||||
| 	db := r.DB.Table(t.TableName()) |  | ||||||
| 	err = db.Find(&o).Error |  | ||||||
| 	if o == nil { |  | ||||||
| 		o = make([]model.AccessGateway, 0) |  | ||||||
| 	} |  | ||||||
| 	return | 	return | ||||||
| } | } | ||||||
|  | |||||||
| @ -15,9 +15,8 @@ func NewAccessSecurityRepository(db *gorm.DB) *AccessSecurityRepository { | |||||||
| 	return accessSecurityRepository | 	return accessSecurityRepository | ||||||
| } | } | ||||||
|  |  | ||||||
| func (r AccessSecurityRepository) FindAllAccessSecurities() (o []model.AccessSecurity, err error) { | func (r AccessSecurityRepository) FindAll() (o []model.AccessSecurity, err error) { | ||||||
| 	db := r.DB | 	err = r.DB.Order("priority asc").Find(&o).Error | ||||||
| 	err = db.Order("priority asc").Find(&o).Error |  | ||||||
| 	return | 	return | ||||||
| } | } | ||||||
|  |  | ||||||
|  | |||||||
| @ -94,3 +94,8 @@ func (r CommandRepository) FindByUser(account model.User) (o []model.CommandForP | |||||||
| 	} | 	} | ||||||
| 	return | 	return | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (r CommandRepository) FindAll() (o []model.Command, err error) { | ||||||
|  | 	err = r.DB.Find(&o).Error | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  | |||||||
| @ -54,6 +54,11 @@ func (r LoginLogRepository) FindAliveLoginLogs() (o []model.LoginLog, err error) | |||||||
| 	return | 	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) { | 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 | 	err = r.DB.Where("state = '1' and logout_time is null and username = ?", username).Find(&o).Error | ||||||
| 	return | 	return | ||||||
|  | |||||||
| @ -218,3 +218,8 @@ func (r *ResourceSharerRepository) Find(resourceId, resourceType, userId, userGr | |||||||
| 	err = db.Find(&resourceSharers).Error | 	err = db.Find(&resourceSharers).Error | ||||||
| 	return | 	return | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (r *ResourceSharerRepository) FindAll() (o []model.ResourceSharer, err error) { | ||||||
|  | 	err = r.DB.Find(&o).Error | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  | |||||||
| @ -23,14 +23,14 @@ func NewSessionRepository(db *gorm.DB) *SessionRepository { | |||||||
| 	return 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 | 	db := r.DB | ||||||
| 	var params []interface{} | 	var params []interface{} | ||||||
|  |  | ||||||
| 	params = append(params, status) | 	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 = ? " | 	countSql := "select count(*) from sessions as s where s.status = ? " | ||||||
|  |  | ||||||
| 	if len(userId) > 0 { | 	if len(userId) > 0 { | ||||||
| @ -57,6 +57,13 @@ func (r SessionRepository) Find(pageIndex, pageSize int, status, userId, clientI | |||||||
| 		params = append(params, protocol) | 		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) | 	params = append(params, (pageIndex-1)*pageSize, pageSize) | ||||||
| 	itemSql += " order by s.connected_time desc LIMIT ?, ?" | 	itemSql += " order by s.connected_time desc LIMIT ?, ?" | ||||||
|  |  | ||||||
| @ -209,3 +216,13 @@ func (r SessionRepository) OverviewAccess(account model.User) (o []model.Session | |||||||
| 	} | 	} | ||||||
| 	return | 	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 | ||||||
|  | } | ||||||
|  | |||||||
| @ -16,15 +16,13 @@ func NewUserRepository(db *gorm.DB) *UserRepository { | |||||||
| 	return userRepository | 	return userRepository | ||||||
| } | } | ||||||
|  |  | ||||||
| func (r UserRepository) FindAll() (o []model.User) { | func (r UserRepository) FindAll() (o []model.User, err error) { | ||||||
| 	if r.DB.Find(&o).Error != nil { | 	err = r.DB.Find(&o).Error | ||||||
| 		return nil |  | ||||||
| 	} |  | ||||||
| 	return | 	return | ||||||
| } | } | ||||||
|  |  | ||||||
| func (r UserRepository) Find(pageIndex, pageSize int, username, nickname, mail, order, field string, account model.User) (o []model.UserForPage, total int64, err error) { | 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") | 	dbCounter := r.DB.Table("users") | ||||||
|  |  | ||||||
| 	if constant.TypeUser == account.Type { | 	if constant.TypeUser == account.Type { | ||||||
|  | |||||||
| @ -16,10 +16,8 @@ func NewUserGroupRepository(db *gorm.DB) *UserGroupRepository { | |||||||
| 	return userGroupRepository | 	return userGroupRepository | ||||||
| } | } | ||||||
|  |  | ||||||
| func (r UserGroupRepository) FindAll() (o []model.UserGroup) { | func (r UserGroupRepository) FindAll() (o []model.UserGroup, err error) { | ||||||
| 	if r.DB.Find(&o).Error != nil { | 	err = r.DB.Find(&o).Error | ||||||
| 		return nil |  | ||||||
| 	} |  | ||||||
| 	return | 	return | ||||||
| } | } | ||||||
|  |  | ||||||
| @ -141,3 +139,8 @@ func AddUserGroupMembers(tx *gorm.DB, userIds []string, userGroupId string) erro | |||||||
| 	} | 	} | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (r UserGroupRepository) FindAllUserGroupMembers() (o []model.UserGroupMember, err error) { | ||||||
|  | 	err = r.DB.Find(&o).Error | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  | |||||||
| @ -39,9 +39,12 @@ func (r AccessGatewayService) ReConnectAll() error { | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  | 	if len(gateways) > 0 { | ||||||
| 		for i := range gateways { | 		for i := range gateways { | ||||||
| 			r.ReConnect(&gateways[i]) | 			r.ReConnect(&gateways[i]) | ||||||
| 		} | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | |||||||
| @ -37,3 +37,41 @@ func (r SessionService) FixSessionState() error { | |||||||
| func (r SessionService) EmptyPassword() error { | func (r SessionService) EmptyPassword() error { | ||||||
| 	return r.sessionRepository.EmptyPassword() | 	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 | ||||||
|  | } | ||||||
|  | |||||||
| @ -26,7 +26,10 @@ func NewStorageService(storageRepository *repository.StorageRepository, userRepo | |||||||
| } | } | ||||||
|  |  | ||||||
| func (r StorageService) InitStorages() error { | func (r StorageService) InitStorages() error { | ||||||
| 	users := r.userRepository.FindAll() | 	users, err := r.userRepository.FindAll() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
| 	for i := range users { | 	for i := range users { | ||||||
| 		userId := users[i].ID | 		userId := users[i].ID | ||||||
| 		_, err := r.storageRepository.FindByOwnerIdAndDefault(userId, true) | 		_, err := r.storageRepository.FindByOwnerIdAndDefault(userId, true) | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| package service | package service | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"next-terminal/server/global/cache" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"next-terminal/server/constant" | 	"next-terminal/server/constant" | ||||||
| @ -21,7 +22,10 @@ func NewUserService(userRepository *repository.UserRepository, loginLogRepositor | |||||||
|  |  | ||||||
| func (r UserService) InitUser() (err error) { | func (r UserService) InitUser() (err error) { | ||||||
|  |  | ||||||
| 	users := r.userRepository.FindAll() | 	users, err := r.userRepository.FindAll() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	if len(users) == 0 { | 	if len(users) == 0 { | ||||||
| 		initPassword := "admin" | 		initPassword := "admin" | ||||||
| @ -37,6 +41,7 @@ func (r UserService) InitUser() (err error) { | |||||||
| 			Nickname: "超级管理员", | 			Nickname: "超级管理员", | ||||||
| 			Type:     constant.TypeAdmin, | 			Type:     constant.TypeAdmin, | ||||||
| 			Created:  utils.NowJsonTime(), | 			Created:  utils.NowJsonTime(), | ||||||
|  | 			Status:   constant.StatusEnabled, | ||||||
| 		} | 		} | ||||||
| 		if err := r.userRepository.Create(&user); err != nil { | 		if err := r.userRepository.Create(&user); err != nil { | ||||||
| 			return err | 			return err | ||||||
| @ -83,12 +88,14 @@ func (r UserService) FixUserOnlineState() error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (r UserService) Logout(token string) (err error) { | func (r UserService) LogoutByToken(token string) (err error) { | ||||||
| 	loginLog, err := r.loginLogRepository.FindById(token) | 	loginLog, err := r.loginLogRepository.FindById(token) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Warnf("登录日志「%v」获取失败", token) | 		log.Warnf("登录日志「%v」获取失败", token) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  | 	cacheKey := r.BuildCacheKeyByToken(token) | ||||||
|  | 	cache.GlobalCache.Delete(cacheKey) | ||||||
|  |  | ||||||
| 	loginLogForUpdate := &model.LoginLog{LogoutTime: utils.NowJsonTime(), ID: token} | 	loginLogForUpdate := &model.LoginLog{LogoutTime: utils.NowJsonTime(), ID: token} | ||||||
| 	err = r.loginLogRepository.Update(loginLogForUpdate) | 	err = r.loginLogRepository.Update(loginLogForUpdate) | ||||||
| @ -107,6 +114,26 @@ func (r UserService) Logout(token string) (err error) { | |||||||
| 	return | 	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 { | func (r UserService) BuildCacheKeyByToken(token string) string { | ||||||
| 	cacheKey := strings.Join([]string{constant.Token, token}, ":") | 	cacheKey := strings.Join([]string{constant.Token, token}, ":") | ||||||
| 	return cacheKey | 	return cacheKey | ||||||
| @ -121,9 +148,36 @@ func (r UserService) OnEvicted(key string, value interface{}) { | |||||||
| 	if strings.HasPrefix(key, constant.Token) { | 	if strings.HasPrefix(key, constant.Token) { | ||||||
| 		token := r.GetTokenFormCacheKey(key) | 		token := r.GetTokenFormCacheKey(key) | ||||||
| 		log.Debugf("用户Token「%v」过期", token) | 		log.Debugf("用户Token「%v」过期", token) | ||||||
| 		err := r.Logout(token) | 		err := r.LogoutByToken(token) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Errorf("退出登录失败 %v", err) | 			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 | ||||||
|  | } | ||||||
|  | |||||||
| @ -49,6 +49,29 @@ func NewSshClient(ip string, port int, username, password, privateKey, passphras | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	addr := fmt.Sprintf("%s:%d", ip, port) | 	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) | 	return ssh.Dial("tcp", addr, config) | ||||||
| } | } | ||||||
|  | |||||||
| @ -141,7 +141,13 @@ class AccessGateway extends Component { | |||||||
|         await this.showModal('更新接入网关', result.data); |         await this.showModal('更新接入网关', result.data); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async reconnect(id) { |     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}); |             message.info({content: '正在重连中...', key: id, duration: 5}); | ||||||
|             let result = await request.post(`/access-gateways/${id}/reconnect`); |             let result = await request.post(`/access-gateways/${id}/reconnect`); | ||||||
|             if (result.code !== 1) { |             if (result.code !== 1) { | ||||||
| @ -150,6 +156,12 @@ class AccessGateway extends Component { | |||||||
|             } |             } | ||||||
|             message.success({content: '重连完成。', key: id, duration: 3}); |             message.success({content: '重连完成。', key: id, duration: 3}); | ||||||
|             this.loadTableData(this.state.queryParams); |             this.loadTableData(this.state.queryParams); | ||||||
|  |         } finally { | ||||||
|  |             items[index]['reconnectLoading'] = false; | ||||||
|  |             this.setState({ | ||||||
|  |                 items: items | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     showModal(title, obj) { |     showModal(title, obj) { | ||||||
| @ -173,6 +185,7 @@ class AccessGateway extends Component { | |||||||
|             modalConfirmLoading: true |             modalConfirmLoading: true | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|  |         try { | ||||||
|             if (formData.id) { |             if (formData.id) { | ||||||
|                 // 向后台提交数据 |                 // 向后台提交数据 | ||||||
|                 const result = await request.put('/access-gateways/' + formData.id, formData); |                 const result = await request.put('/access-gateways/' + formData.id, formData); | ||||||
| @ -200,10 +213,11 @@ class AccessGateway extends Component { | |||||||
|                     message.error('新增失败 :( ' + result.message, 10); |                     message.error('新增失败 :( ' + result.message, 10); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |         } finally { | ||||||
|             this.setState({ |             this.setState({ | ||||||
|                 modalConfirmLoading: false |                 modalConfirmLoading: false | ||||||
|             }); |             }); | ||||||
|  |         } | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     batchDelete = async () => { |     batchDelete = async () => { | ||||||
| @ -340,11 +354,11 @@ class AccessGateway extends Component { | |||||||
|  |  | ||||||
|                 return ( |                 return ( | ||||||
|                     <div> |                     <div> | ||||||
|                         <Button type="link" size='small' loading={this.state.items[index]['execLoading']} |                         <Button type="link" size='small' | ||||||
|                                 onClick={() => this.update(record['id'])}>编辑</Button> |                                 onClick={() => this.update(record['id'])}>编辑</Button> | ||||||
|  |  | ||||||
|                         <Button type="link" size='small' loading={this.state.items[index]['execLoading']} |                         <Button type="link" size='small' loading={this.state.items[index]['reconnectLoading']} | ||||||
|                                 onClick={() => this.reconnect(record['id'])}>重连</Button> |                                 onClick={() => this.reconnect(record['id'], index)}>重连</Button> | ||||||
|  |  | ||||||
|                         <Button type="text" size='small' danger |                         <Button type="text" size='small' danger | ||||||
|                                 onClick={() => this.showDeleteConfirm(record.id, record.name)}>删除</Button> |                                 onClick={() => this.showDeleteConfirm(record.id, record.name)}>删除</Button> | ||||||
|  | |||||||
| @ -131,7 +131,7 @@ class Dashboard extends Component { | |||||||
|                 <div style={{margin: 16, marginBottom: 0}}> |                 <div style={{margin: 16, marginBottom: 0}}> | ||||||
|                     <Row gutter={16}> |                     <Row gutter={16}> | ||||||
|                         <Col span={6}> |                         <Col span={6}> | ||||||
|                             <Card bordered={true} hoverable> |                             <Card bordered={true} hoverable={true}> | ||||||
|                                 <Link to={'/user'}> |                                 <Link to={'/user'}> | ||||||
|                                     <Statistic title="在线用户" value={this.state.counter['user']} |                                     <Statistic title="在线用户" value={this.state.counter['user']} | ||||||
|                                                prefix={<UserOutlined/>}/> |                                                prefix={<UserOutlined/>}/> | ||||||
| @ -139,7 +139,7 @@ class Dashboard extends Component { | |||||||
|                             </Card> |                             </Card> | ||||||
|                         </Col> |                         </Col> | ||||||
|                         <Col span={6}> |                         <Col span={6}> | ||||||
|                             <Card bordered={true} hoverable> |                             <Card bordered={true} hoverable={true}> | ||||||
|                                 <Link to={'/asset'}> |                                 <Link to={'/asset'}> | ||||||
|                                     <Statistic title="资产数量" value={this.state.counter['asset']} |                                     <Statistic title="资产数量" value={this.state.counter['asset']} | ||||||
|                                                prefix={<DesktopOutlined/>}/> |                                                prefix={<DesktopOutlined/>}/> | ||||||
| @ -147,7 +147,7 @@ class Dashboard extends Component { | |||||||
|                             </Card> |                             </Card> | ||||||
|                         </Col> |                         </Col> | ||||||
|                         <Col span={6}> |                         <Col span={6}> | ||||||
|                             <Card bordered={true} hoverable> |                             <Card bordered={true} hoverable={true}> | ||||||
|                                 <Link to={'/credential'} hoverable> |                                 <Link to={'/credential'} hoverable> | ||||||
|                                     <Statistic title="授权凭证" value={this.state.counter['credential']} |                                     <Statistic title="授权凭证" value={this.state.counter['credential']} | ||||||
|                                                prefix={<IdcardOutlined/>}/> |                                                prefix={<IdcardOutlined/>}/> | ||||||
| @ -156,7 +156,7 @@ class Dashboard extends Component { | |||||||
|                             </Card> |                             </Card> | ||||||
|                         </Col> |                         </Col> | ||||||
|                         <Col span={6}> |                         <Col span={6}> | ||||||
|                             <Card bordered={true} hoverable> |                             <Card bordered={true} hoverable={true}> | ||||||
|                                 <Link to={'/online-session'}> |                                 <Link to={'/online-session'}> | ||||||
|                                     <Statistic title="在线会话" value={this.state.counter['onlineSession']} |                                     <Statistic title="在线会话" value={this.state.counter['onlineSession']} | ||||||
|                                                prefix={<LinkOutlined/>}/> |                                                prefix={<LinkOutlined/>}/> | ||||||
|  | |||||||
| @ -20,7 +20,7 @@ import qs from "qs"; | |||||||
| import request from "../../common/request"; | import request from "../../common/request"; | ||||||
| import {formatDate, isEmpty} from "../../utils/utils"; | import {formatDate, isEmpty} from "../../utils/utils"; | ||||||
| import {message} from "antd/es"; | 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; | 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() { |     render() { | ||||||
|  |  | ||||||
|         const columns = [{ |         const columns = [{ | ||||||
| @ -353,6 +377,26 @@ class LoginLog extends Component { | |||||||
|                                         </Button> |                                         </Button> | ||||||
|                                     </Tooltip> |                                     </Tooltip> | ||||||
|  |  | ||||||
|  |                                     <Tooltip title="清空"> | ||||||
|  |                                         <Button type="primary" danger icon={<ClearOutlined/>} | ||||||
|  |                                                 loading={this.state.clearBtnLoading} | ||||||
|  |                                                 onClick={() => { | ||||||
|  |                                                     const title = <Text style={{color: 'red'}} | ||||||
|  |                                                                         strong>您确定要清空全部的登录日志吗?</Text>; | ||||||
|  |                                                     confirm({ | ||||||
|  |                                                         icon: <ExclamationCircleOutlined/>, | ||||||
|  |                                                         title: title, | ||||||
|  |                                                         content: '删除用户未注销的登录日志将会强制用户下线,当前登录的用户也会退出登录。', | ||||||
|  |                                                         okType: 'danger', | ||||||
|  |                                                         onOk: this.clearLoginLogs, | ||||||
|  |                                                         onCancel() { | ||||||
|  |  | ||||||
|  |                                                         }, | ||||||
|  |                                                     }); | ||||||
|  |                                                 }}> | ||||||
|  |  | ||||||
|  |                                         </Button> | ||||||
|  |                                     </Tooltip> | ||||||
|                                 </Space> |                                 </Space> | ||||||
|                             </Col> |                             </Col> | ||||||
|                         </Row> |                         </Row> | ||||||
|  | |||||||
							
								
								
									
										3
									
								
								web/src/components/session/OfflineSession.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								web/src/components/session/OfflineSession.css
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | .unreviewed{ | ||||||
|  |     background: #e6f7ff; | ||||||
|  | } | ||||||
| @ -20,9 +20,16 @@ import request from "../../common/request"; | |||||||
| import {differTime} from "../../utils/utils"; | import {differTime} from "../../utils/utils"; | ||||||
| import Playback from "./Playback"; | import Playback from "./Playback"; | ||||||
| import {message} from "antd/es"; | 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 {MODE_COLORS, PROTOCOL_COLORS} from "../../common/constants"; | ||||||
|  | import './OfflineSession.css' | ||||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||||
|  |  | ||||||
| const confirm = Modal.confirm; | const confirm = Modal.confirm; | ||||||
| @ -42,7 +49,8 @@ class OfflineSession extends Component { | |||||||
|             pageSize: 10, |             pageSize: 10, | ||||||
|             protocol: '', |             protocol: '', | ||||||
|             userId: undefined, |             userId: undefined, | ||||||
|             assetId: undefined |             assetId: undefined, | ||||||
|  |             reviewed: undefined | ||||||
|         }, |         }, | ||||||
|         loading: false, |         loading: false, | ||||||
|         playbackVisible: false, |         playbackVisible: false, | ||||||
| @ -147,6 +155,16 @@ class OfflineSession extends Component { | |||||||
|         this.loadTableData(query); |         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 => { |     handleSearchByNickname = async nickname => { | ||||||
|         const result = await request.get(`/users/paging?pageIndex=1&pageSize=1000&nickname=${nickname}`); |         const result = await request.get(`/users/paging?pageIndex=1&pageSize=1000&nickname=${nickname}`); | ||||||
|         if (result.code !== 1) { |         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() { |     render() { | ||||||
|  |  | ||||||
|         const columns = [{ |         const columns = [{ | ||||||
| @ -221,7 +343,7 @@ class OfflineSession extends Component { | |||||||
|             key: 'id', |             key: 'id', | ||||||
|             render: (id, record, index) => { |             render: (id, record, index) => { | ||||||
|                 return index + 1; |                 return index + 1; | ||||||
|             } |             }, | ||||||
|         }, { |         }, { | ||||||
|             title: '来源IP', |             title: '来源IP', | ||||||
|             dataIndex: 'clientIp', |             dataIndex: 'clientIp', | ||||||
| @ -312,34 +434,17 @@ class OfflineSession extends Component { | |||||||
|                                             } |                                             } | ||||||
|                                         }); |                                         }); | ||||||
|                                     }}>禁用IP</Button> |                                     }}>禁用IP</Button> | ||||||
|                             <Button type="link" size='small' onClick={() => { |                             <Button type="link" size='small' danger onClick={() => { | ||||||
|                                 confirm({ |                                 confirm({ | ||||||
|                                     title: '您确定要删除此会话吗?', |                                     title: '您确定要删除此会话吗?', | ||||||
|                                     content: '', |                                     content: '', | ||||||
|                                     okText: '确定', |                                     okText: '确定', | ||||||
|                                     okType: 'danger', |                                     okType: 'danger', | ||||||
|                                     cancelText: '取消', |                                     cancelText: '取消', | ||||||
|                                     onOk() { |                                     onOk: () => { | ||||||
|                                         del(record.id) |                                         this.del(record.id) | ||||||
|                                     } |                                     } | ||||||
|                                 }); |                                 }); | ||||||
|  |  | ||||||
|                                 const 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, |  | ||||||
|                                         }); |  | ||||||
|                                     } |  | ||||||
|  |  | ||||||
|                                 } |  | ||||||
|                             }}>删除</Button> |                             }}>删除</Button> | ||||||
|                         </div> |                         </div> | ||||||
|                     ) |                     ) | ||||||
| @ -366,10 +471,10 @@ class OfflineSession extends Component { | |||||||
|                 <Content className="site-layout-background page-content"> |                 <Content className="site-layout-background page-content"> | ||||||
|                     <div style={{marginBottom: 20}}> |                     <div style={{marginBottom: 20}}> | ||||||
|                         <Row justify="space-around" align="middle" gutter={24}> |                         <Row justify="space-around" align="middle" gutter={24}> | ||||||
|                             <Col span={8} key={1}> |                             <Col span={4} key={1}> | ||||||
|                                 <Title level={3}>离线会话列表</Title> |                                 <Title level={3}>离线会话列表</Title> | ||||||
|                             </Col> |                             </Col> | ||||||
|                             <Col span={16} key={2} style={{textAlign: 'right'}}> |                             <Col span={20} key={2} style={{textAlign: 'right'}}> | ||||||
|                                 <Space> |                                 <Space> | ||||||
|  |  | ||||||
|                                     <Search |                                     <Search | ||||||
| @ -380,7 +485,7 @@ class OfflineSession extends Component { | |||||||
|                                     /> |                                     /> | ||||||
|  |  | ||||||
|                                     <Select |                                     <Select | ||||||
|                                         style={{width: 150}} |                                         style={{width: 140}} | ||||||
|                                         showSearch |                                         showSearch | ||||||
|                                         value={this.state.queryParams.userId} |                                         value={this.state.queryParams.userId} | ||||||
|                                         placeholder='用户昵称' |                                         placeholder='用户昵称' | ||||||
| @ -393,7 +498,7 @@ class OfflineSession extends Component { | |||||||
|                                     </Select> |                                     </Select> | ||||||
|  |  | ||||||
|                                     <Select |                                     <Select | ||||||
|                                         style={{width: 150}} |                                         style={{width: 140}} | ||||||
|                                         showSearch |                                         showSearch | ||||||
|                                         value={this.state.queryParams.assetId} |                                         value={this.state.queryParams.assetId} | ||||||
|                                         placeholder='资产名称' |                                         placeholder='资产名称' | ||||||
| @ -404,6 +509,14 @@ class OfflineSession extends Component { | |||||||
|                                         {assetOptions} |                                         {assetOptions} | ||||||
|                                     </Select> |                                     </Select> | ||||||
|  |  | ||||||
|  |                                     <Select onChange={this.handleChangeByRead} | ||||||
|  |                                             value={this.state.queryParams.reviewed ? this.state.queryParams.reviewed : ''} | ||||||
|  |                                             style={{width: 100}}> | ||||||
|  |                                         <Select.Option value="">全部会话</Select.Option> | ||||||
|  |                                         <Select.Option value="true">只看已读</Select.Option> | ||||||
|  |                                         <Select.Option value="false">只看未读</Select.Option> | ||||||
|  |                                     </Select> | ||||||
|  |  | ||||||
|                                     <Select onChange={this.handleChangeByProtocol} |                                     <Select onChange={this.handleChangeByProtocol} | ||||||
|                                             value={this.state.queryParams.protocol ? this.state.queryParams.protocol : ''} |                                             value={this.state.queryParams.protocol ? this.state.queryParams.protocol : ''} | ||||||
|                                             style={{width: 100}}> |                                             style={{width: 100}}> | ||||||
| @ -422,9 +535,7 @@ class OfflineSession extends Component { | |||||||
|                                             this.loadTableData({ |                                             this.loadTableData({ | ||||||
|                                                 pageIndex: 1, |                                                 pageIndex: 1, | ||||||
|                                                 pageSize: 10, |                                                 pageSize: 10, | ||||||
|                                                 protocol: '', |                                                 protocol: '' | ||||||
|                                                 userId: undefined, |  | ||||||
|                                                 assetId: undefined |  | ||||||
|                                             }) |                                             }) | ||||||
|                                         }}> |                                         }}> | ||||||
|  |  | ||||||
| @ -441,6 +552,29 @@ class OfflineSession extends Component { | |||||||
|                                         </Button> |                                         </Button> | ||||||
|                                     </Tooltip> |                                     </Tooltip> | ||||||
|  |  | ||||||
|  |                                     <Tooltip title="全部标为已阅"> | ||||||
|  |                                         <Button icon={<CheckOutlined />} | ||||||
|  |                                                 loading={this.state.reviewedAllBtnLoading} | ||||||
|  |                                                 onClick={this.handleAllReviewed}> | ||||||
|  |                                         </Button> | ||||||
|  |                                     </Tooltip> | ||||||
|  |  | ||||||
|  |                                     <Tooltip title="标为已阅"> | ||||||
|  |                                         <Button disabled={!hasSelected} icon={<EyeOutlined />} | ||||||
|  |                                                 loading={this.state.reviewedBtnLoading} | ||||||
|  |                                                 onClick={this.handleReviewed}> | ||||||
|  |  | ||||||
|  |                                         </Button> | ||||||
|  |                                     </Tooltip> | ||||||
|  |  | ||||||
|  |                                     <Tooltip title="标为未阅"> | ||||||
|  |                                         <Button disabled={!hasSelected} icon={<EyeInvisibleOutlined />} | ||||||
|  |                                                 loading={this.state.unreviewedBtnLoading} | ||||||
|  |                                                 onClick={this.handleUnreviewed}> | ||||||
|  |  | ||||||
|  |                                         </Button> | ||||||
|  |                                     </Tooltip> | ||||||
|  |  | ||||||
|                                     <Tooltip title="批量删除"> |                                     <Tooltip title="批量删除"> | ||||||
|                                         <Button type="primary" danger disabled={!hasSelected} icon={<DeleteOutlined/>} |                                         <Button type="primary" danger disabled={!hasSelected} icon={<DeleteOutlined/>} | ||||||
|                                                 loading={this.state.delBtnLoading} |                                                 loading={this.state.delBtnLoading} | ||||||
| @ -457,6 +591,25 @@ class OfflineSession extends Component { | |||||||
|                                                         }, |                                                         }, | ||||||
|                                                         onCancel() { |                                                         onCancel() { | ||||||
|  |  | ||||||
|  |                                                         }, | ||||||
|  |                                                     }); | ||||||
|  |                                                 }}> | ||||||
|  |                                         </Button> | ||||||
|  |                                     </Tooltip> | ||||||
|  |  | ||||||
|  |                                     <Tooltip title="清空"> | ||||||
|  |                                         <Button type="primary" danger icon={<ClearOutlined/>} | ||||||
|  |                                                 loading={this.state.clearBtnLoading} | ||||||
|  |                                                 onClick={() => { | ||||||
|  |                                                     const content = <Text style={{color: 'red'}} | ||||||
|  |                                                                           strong>您确定要清空全部的离线会话吗?</Text>; | ||||||
|  |                                                     confirm({ | ||||||
|  |                                                         icon: <ExclamationCircleOutlined/>, | ||||||
|  |                                                         content: content, | ||||||
|  |                                                         okType: 'danger', | ||||||
|  |                                                         onOk: this.clearSession, | ||||||
|  |                                                         onCancel() { | ||||||
|  |  | ||||||
|                                                         }, |                                                         }, | ||||||
|                                                     }); |                                                     }); | ||||||
|                                                 }}> |                                                 }}> | ||||||
| @ -482,6 +635,9 @@ class OfflineSession extends Component { | |||||||
|                                showTotal: total => `总计 ${total} 条` |                                showTotal: total => `总计 ${total} 条` | ||||||
|                            }} |                            }} | ||||||
|                            loading={this.state.loading} |                            loading={this.state.loading} | ||||||
|  |                            rowClassName={(record, index) => { | ||||||
|  |                                return record['reviewed'] ? '' : 'unreviewed'; | ||||||
|  |                            }} | ||||||
|                     /> |                     /> | ||||||
|  |  | ||||||
|                     { |                     { | ||||||
|  | |||||||
| @ -249,9 +249,9 @@ class Playback extends Component { | |||||||
|                                     this.startSpeedUp(); |                                     this.startSpeedUp(); | ||||||
|                                 } |                                 } | ||||||
|                             }}> |                             }}> | ||||||
|                                 <Select.Option key="1">1x</Select.Option> |                                 <Select.Option key="1" value={1}>1x</Select.Option> | ||||||
|                                 <Select.Option key="2">2x</Select.Option> |                                 <Select.Option key="2" value={2}>2x</Select.Option> | ||||||
|                                 <Select.Option key="5">5x</Select.Option> |                                 <Select.Option key="5" value={5}>5x</Select.Option> | ||||||
|                             </Select> |                             </Select> | ||||||
|                         </Col> |                         </Col> | ||||||
|                         <Col flex='none'> |                         <Col flex='none'> | ||||||
|  | |||||||
| @ -1,8 +1,10 @@ | |||||||
| import React, {Component} from 'react'; | 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 request from "../../common/request"; | ||||||
| import {message} from "antd/es"; | import {message} from "antd/es"; | ||||||
| import {ExclamationCircleOutlined} from "@ant-design/icons"; | import {ExclamationCircleOutlined} from "@ant-design/icons"; | ||||||
|  | import {download, getToken} from "../../utils/utils"; | ||||||
|  | import {server} from "../../common/env"; | ||||||
|  |  | ||||||
| const {Content} = Layout; | const {Content} = Layout; | ||||||
| const {Option} = Select; | const {Option} = Select; | ||||||
| @ -91,6 +93,10 @@ class Setting extends Component { | |||||||
|             if (this.mailSettingFormRef.current) { |             if (this.mailSettingFormRef.current) { | ||||||
|                 this.mailSettingFormRef.current.setFieldsValue(properties) |                 this.mailSettingFormRef.current.setFieldsValue(properties) | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             if (this.otherSettingFormRef.current) { | ||||||
|  |                 this.otherSettingFormRef.current.setFieldsValue(properties) | ||||||
|  |             } | ||||||
|         } else { |         } else { | ||||||
|             message.error(result['message']); |             message.error(result['message']); | ||||||
|         } |         } | ||||||
| @ -443,7 +449,6 @@ class Setting extends Component { | |||||||
|                                 } |                                 } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|                                 <Form.Item {...formTailLayout}> |                                 <Form.Item {...formTailLayout}> | ||||||
|                                     <Button type="primary" htmlType="submit"> |                                     <Button type="primary" htmlType="submit"> | ||||||
|                                         更新 |                                         更新 | ||||||
| @ -525,7 +530,7 @@ class Setting extends Component { | |||||||
|  |  | ||||||
|                         <TabPane tab="其他配置" key="other"> |                         <TabPane tab="其他配置" key="other"> | ||||||
|                             <Title level={3}>其他配置</Title> |                             <Title level={3}>其他配置</Title> | ||||||
|                             <Form ref={this.guacdSettingFormRef} name="other" onFinish={this.changeProperties} |                             <Form ref={this.otherSettingFormRef} name="other" onFinish={this.changeProperties} | ||||||
|                                   layout="vertical"> |                                   layout="vertical"> | ||||||
|  |  | ||||||
|                                 <Form.Item |                                 <Form.Item | ||||||
| @ -565,6 +570,30 @@ class Setting extends Component { | |||||||
|                                 </Form.Item> |                                 </Form.Item> | ||||||
|                             </Form> |                             </Form> | ||||||
|                         </TabPane> |                         </TabPane> | ||||||
|  |  | ||||||
|  |                         <TabPane tab="备份与恢复" key="backup"> | ||||||
|  |                             <Title level={3}>备份与恢复</Title> | ||||||
|  |  | ||||||
|  |                             <Space direction="vertical"> | ||||||
|  |                                 <Alert | ||||||
|  |                                     message="恢复数据时,如存在登录账号相同的用户时,会保留原系统中的数据,此外由于登录密码加密之后不可逆,恢复的账户密码将随机产生。" | ||||||
|  |                                     type="info" | ||||||
|  |                                 /> | ||||||
|  |  | ||||||
|  |                                 <Space> | ||||||
|  |                                     <Button type="primary" onClick={() => { | ||||||
|  |                                         download(`${server}/backup/export?X-Auth-Token=${getToken()}&t=${new Date().getTime()}`); | ||||||
|  |                                     }}> | ||||||
|  |                                         导出备份 | ||||||
|  |                                     </Button> | ||||||
|  |  | ||||||
|  |                                     <Button type="dashed"> | ||||||
|  |                                         恢复备份 | ||||||
|  |                                     </Button> | ||||||
|  |                                 </Space> | ||||||
|  |                             </Space> | ||||||
|  |  | ||||||
|  |                         </TabPane> | ||||||
|                     </Tabs> |                     </Tabs> | ||||||
|                 </Content> |                 </Content> | ||||||
|             </> |             </> | ||||||
|  | |||||||
| @ -14,6 +14,7 @@ import { | |||||||
|     Modal, |     Modal, | ||||||
|     Row, |     Row, | ||||||
|     Space, |     Space, | ||||||
|  |     Switch, | ||||||
|     Table, |     Table, | ||||||
|     Tag, |     Tag, | ||||||
|     Tooltip, |     Tooltip, | ||||||
| @ -26,14 +27,14 @@ import {message} from "antd/es"; | |||||||
| import { | import { | ||||||
|     DeleteOutlined, |     DeleteOutlined, | ||||||
|     DownOutlined, |     DownOutlined, | ||||||
|     ExclamationCircleOutlined, |     ExclamationCircleOutlined, FrownOutlined, | ||||||
|     InsuranceOutlined, |     InsuranceOutlined, | ||||||
|     LockOutlined, |     LockOutlined, | ||||||
|     PlusOutlined, |     PlusOutlined, | ||||||
|     SyncOutlined, |     SyncOutlined, | ||||||
|     UndoOutlined |     UndoOutlined | ||||||
| } from '@ant-design/icons'; | } from '@ant-design/icons'; | ||||||
| import {hasPermission} from "../../service/permission"; | import {getCurrentUser} from "../../service/permission"; | ||||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||||
| import UserShareSelectedAsset from "./UserShareSelectedAsset"; | import UserShareSelectedAsset from "./UserShareSelectedAsset"; | ||||||
|  |  | ||||||
| @ -73,16 +74,6 @@ class User extends Component { | |||||||
|         this.loadTableData(); |         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) { |     async loadTableData(queryParams) { | ||||||
|         this.setState({ |         this.setState({ | ||||||
|             loading: true |             loading: true | ||||||
| @ -129,11 +120,10 @@ class User extends Component { | |||||||
|             queryParams: queryParams |             queryParams: queryParams | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         this.loadTableData(queryParams).then(r => { |         this.loadTableData(queryParams); | ||||||
|         }) |  | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     showDeleteConfirm(id, content) { |     showDeleteConfirm(id, content, index) { | ||||||
|         let self = this; |         let self = this; | ||||||
|         confirm({ |         confirm({ | ||||||
|             title: '您确定要删除此用户吗?', |             title: '您确定要删除此用户吗?', | ||||||
| @ -142,7 +132,7 @@ class User extends Component { | |||||||
|             okType: 'danger', |             okType: 'danger', | ||||||
|             cancelText: '取消', |             cancelText: '取消', | ||||||
|             onOk() { |             onOk() { | ||||||
|                 self.delete(id); |                 self.delete(id, index); | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|     }; |     }; | ||||||
| @ -167,7 +157,6 @@ class User extends Component { | |||||||
|         this.setState({ |         this.setState({ | ||||||
|             modalConfirmLoading: true |             modalConfirmLoading: true | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         if (formData.id) { |         if (formData.id) { | ||||||
|             // 向后台提交数据 |             // 向后台提交数据 | ||||||
|             const result = await request.put('/users/' + formData.id, formData); |             const result = await request.put('/users/' + formData.id, formData); | ||||||
| @ -293,6 +282,72 @@ class User extends Component { | |||||||
|         this.loadTableData(query); |         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() { |     render() { | ||||||
|  |  | ||||||
|         const columns = [{ |         const columns = [{ | ||||||
| @ -348,15 +403,28 @@ class User extends Component { | |||||||
|             dataIndex: 'mail', |             dataIndex: 'mail', | ||||||
|             key: 'mail', |             key: 'mail', | ||||||
|         }, { |         }, { | ||||||
|             title: '二次认证', |             title: '状态', | ||||||
|  |             dataIndex: 'status', | ||||||
|  |             key: 'status', | ||||||
|  |             render: (status, record, index) => { | ||||||
|  |                 return <Switch checkedChildren="启用" unCheckedChildren="停用" | ||||||
|  |                                disabled={getCurrentUser()['id'] === record['id']} | ||||||
|  |                                loading={record['statusLoading']} | ||||||
|  |                                checked={status !== 'disabled'} | ||||||
|  |                                onChange={checked => { | ||||||
|  |                                    this.changeUserStatus(record['id'], checked, index); | ||||||
|  |                                }}/> | ||||||
|  |             } | ||||||
|  |         }, { | ||||||
|  |             title: '双因素认证', | ||||||
|             dataIndex: 'totpSecret', |             dataIndex: 'totpSecret', | ||||||
|             key: 'totpSecret', |             key: 'totpSecret', | ||||||
|             render: (text, record) => { |             render: (text, record) => { | ||||||
|  |  | ||||||
|                 if (text === '1') { |                 if (text === '1') { | ||||||
|                     return <Tag icon={<InsuranceOutlined/>} color="success">开启</Tag>; |                     return <Tag icon={<InsuranceOutlined/>} color="success">已开启</Tag>; | ||||||
|                 } else { |                 } else { | ||||||
|                     return <Tag icon={<ExclamationCircleOutlined/>} color="warning">关闭</Tag>; |                     return <Tag icon={<FrownOutlined />} color="warning">未开启</Tag>; | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         }, { |         }, { | ||||||
| @ -398,7 +466,7 @@ class User extends Component { | |||||||
|             { |             { | ||||||
|                 title: '操作', |                 title: '操作', | ||||||
|                 key: 'action', |                 key: 'action', | ||||||
|                 render: (text, record) => { |                 render: (text, record, index) => { | ||||||
|  |  | ||||||
|                     const menu = ( |                     const menu = ( | ||||||
|                         <Menu> |                         <Menu> | ||||||
| @ -414,6 +482,7 @@ class User extends Component { | |||||||
|  |  | ||||||
|                             <Menu.Item key="2"> |                             <Menu.Item key="2"> | ||||||
|                                 <Button type="text" size='small' |                                 <Button type="text" size='small' | ||||||
|  |                                         loading={record['resetTOTPLoading']} | ||||||
|                                         onClick={() => { |                                         onClick={() => { | ||||||
|                                             confirm({ |                                             confirm({ | ||||||
|                                                 title: '您确定要重置此用户的双因素认证吗?', |                                                 title: '您确定要重置此用户的双因素认证吗?', | ||||||
| @ -421,13 +490,7 @@ class User extends Component { | |||||||
|                                                 okText: '确定', |                                                 okText: '确定', | ||||||
|                                                 cancelText: '取消', |                                                 cancelText: '取消', | ||||||
|                                                 onOk: async () => { |                                                 onOk: async () => { | ||||||
|                                                     let result = await request.post(`/users/${record['id']}/reset-totp`); |                                                     this.resetTOTP(record['id'], index); | ||||||
|                                                     if (result['code'] === 1) { |  | ||||||
|                                                         message.success('操作成功', 3); |  | ||||||
|                                                         this.loadTableData(); |  | ||||||
|                                                     } else { |  | ||||||
|                                                         message.error(result['message'], 10); |  | ||||||
|                                                     } |  | ||||||
|                                                 } |                                                 } | ||||||
|                                             }); |                                             }); | ||||||
|                                         }}>重置双因素认证</Button> |                                         }}>重置双因素认证</Button> | ||||||
| @ -446,8 +509,9 @@ class User extends Component { | |||||||
|                             <Menu.Divider/> |                             <Menu.Divider/> | ||||||
|                             <Menu.Item key="5"> |                             <Menu.Item key="5"> | ||||||
|                                 <Button type="text" size='small' danger |                                 <Button type="text" size='small' danger | ||||||
|                                         disabled={!hasPermission(record['owner'])} |                                         disabled={getCurrentUser()['id'] === record['id']} | ||||||
|                                         onClick={() => this.showDeleteConfirm(record.id, record.name)}>删除</Button> |                                         loading={record['delLoading']} | ||||||
|  |                                         onClick={() => this.showDeleteConfirm(record.id, record.name, index)}>删除</Button> | ||||||
|                             </Menu.Item> |                             </Menu.Item> | ||||||
|                         </Menu> |                         </Menu> | ||||||
|                     ); |                     ); | ||||||
| @ -544,53 +608,6 @@ class User extends Component { | |||||||
|                                         </Button> |                                         </Button> | ||||||
|                                     </Tooltip> |                                     </Tooltip> | ||||||
|  |  | ||||||
|                                     {/*<Tooltip title="批量启用">*/} |  | ||||||
|                                     {/*    <Button type="dashed" danger disabled={!hasSelected}*/} |  | ||||||
|                                     {/*            icon={<IssuesCloseOutlined/>}*/} |  | ||||||
|                                     {/*            loading={this.state.delBtnLoading}*/} |  | ||||||
|                                     {/*            onClick={() => {*/} |  | ||||||
|                                     {/*                constant content = <div>*/} |  | ||||||
|                                     {/*                    您确定要启用选中的<Text style={{color: '#1890FF'}}*/} |  | ||||||
|                                     {/*                                   strong>{this.state.selectedRowKeys.length}</Text>条记录吗?*/} |  | ||||||
|                                     {/*                </div>;*/} |  | ||||||
|                                     {/*                confirm({*/} |  | ||||||
|                                     {/*                    icon: <ExclamationCircleOutlined/>,*/} |  | ||||||
|                                     {/*                    content: content,*/} |  | ||||||
|                                     {/*                    onOk: () => {*/} |  | ||||||
|  |  | ||||||
|                                     {/*                    },*/} |  | ||||||
|                                     {/*                    onCancel() {*/} |  | ||||||
|  |  | ||||||
|                                     {/*                    },*/} |  | ||||||
|                                     {/*                });*/} |  | ||||||
|                                     {/*            }}>*/} |  | ||||||
|  |  | ||||||
|                                     {/*    </Button>*/} |  | ||||||
|                                     {/*</Tooltip>*/} |  | ||||||
|  |  | ||||||
|                                     {/*<Tooltip title="批量禁用">*/} |  | ||||||
|                                     {/*    <Button type="default" danger disabled={!hasSelected} icon={<StopOutlined/>}*/} |  | ||||||
|                                     {/*            loading={this.state.delBtnLoading}*/} |  | ||||||
|                                     {/*            onClick={() => {*/} |  | ||||||
|                                     {/*                constant content = <div>*/} |  | ||||||
|                                     {/*                    您确定要禁用选中的<Text style={{color: '#1890FF'}}*/} |  | ||||||
|                                     {/*                                   strong>{this.state.selectedRowKeys.length}</Text>条记录吗?*/} |  | ||||||
|                                     {/*                </div>;*/} |  | ||||||
|                                     {/*                confirm({*/} |  | ||||||
|                                     {/*                    icon: <ExclamationCircleOutlined/>,*/} |  | ||||||
|                                     {/*                    content: content,*/} |  | ||||||
|                                     {/*                    onOk: () => {*/} |  | ||||||
|  |  | ||||||
|                                     {/*                    },*/} |  | ||||||
|                                     {/*                    onCancel() {*/} |  | ||||||
|  |  | ||||||
|                                     {/*                    },*/} |  | ||||||
|                                     {/*                });*/} |  | ||||||
|                                     {/*            }}>*/} |  | ||||||
|  |  | ||||||
|                                     {/*    </Button>*/} |  | ||||||
|                                     {/*</Tooltip>*/} |  | ||||||
|  |  | ||||||
|                                     <Tooltip title="批量删除"> |                                     <Tooltip title="批量删除"> | ||||||
|                                         <Button type="primary" danger disabled={!hasSelected} icon={<DeleteOutlined/>} |                                         <Button type="primary" danger disabled={!hasSelected} icon={<DeleteOutlined/>} | ||||||
|                                                 loading={this.state.delBtnLoading} |                                                 loading={this.state.delBtnLoading} | ||||||
|  | |||||||
| @ -61,7 +61,6 @@ const UserModal = ({title, visible, handleOk, handleCancel, confirmLoading, mode | |||||||
|                             <Input type="password" autoComplete="new-password" placeholder="输入登录密码"/> |                             <Input type="password" autoComplete="new-password" placeholder="输入登录密码"/> | ||||||
|                         </Form.Item>) : null |                         </Form.Item>) : null | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|             </Form> |             </Form> | ||||||
|         </Modal> |         </Modal> | ||||||
|     ) |     ) | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user