* 优化图标和LOGO

* 修改登录页面动画的速度为3

* 增加对websocket的异常处理

* 修复了用户组和用户名唯一判断错误的问题

* 提示版本号

* 修复readme错别字

* 修复单词拼写错误的问题

* 修复代码格式

* 修改Windows资产属性名称

* Docker 打包流程增加 upx 压缩

* 升级依赖文件,修改sqlite驱动为 github.com/glebarez/sqlite

* 修复第一次查询「授权令牌」的错误

* 移除无关代码

* 修改docker打包脚本

* 增加打包脚本

* 增加微信群

* 修复单词拼写错误的问题

* 修复代码格式

* 修改Windows资产属性名称

* Docker 打包流程增加 upx 压缩

* 修改docker打包脚本

* - 替换 sqlite 驱动为 github.com/glebarez/sqlite
- 修复数据库锁定的问题
- 修复部分代码不完善的问题
- 修复策略显示不完整的问题
- 修复编辑文件换行符的问题
- 优化guacd连接
This commit is contained in:
dushixiang 2022-03-29 22:40:26 +08:00 committed by GitHub
parent 3ec6e5332e
commit 04f8d0079b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 436 additions and 568 deletions

View File

@ -168,10 +168,7 @@ func (api AccountApi) LoginWithTotpEndpoint(c echo.Context) error {
func (api AccountApi) LogoutEndpoint(c echo.Context) error { func (api AccountApi) LogoutEndpoint(c echo.Context) error {
token := GetToken(c) token := GetToken(c)
err := service.UserService.LogoutByToken(token) service.UserService.Logout(token)
if err != nil {
return err
}
return Success(c, nil) return Success(c, nil)
} }
@ -317,6 +314,10 @@ func (api AccountApi) AccountAssetEndpoint(c echo.Context) error {
if err != nil { if err != nil {
return err return err
} }
for i := range items {
items[i].IP = ""
items[i].Port = 0
}
return Success(c, Map{ return Success(c, Map{
"total": total, "total": total,

View File

@ -28,7 +28,7 @@ func (assetApi AssetApi) AssetCreateEndpoint(c echo.Context) error {
account, _ := GetCurrentAccount(c) account, _ := GetCurrentAccount(c)
m["owner"] = account.ID m["owner"] = account.ID
if _, err := service.AssetService.Create(m); err != nil { if _, err := service.AssetService.Create(context.TODO(), m); err != nil {
return err return err
} }

View File

@ -25,7 +25,7 @@ func (api BackupApi) BackupExportEndpoint(c echo.Context) error {
if err != nil { if err != nil {
return err return err
} }
c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=next-terminal_backup_%s.json", time.Now().Format("20060102150405"))) c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=backup_%s.json", time.Now().Format("20060102150405")))
return c.Stream(http.StatusOK, echo.MIMEOctetStream, bytes.NewReader(jsonBytes)) return c.Stream(http.StatusOK, echo.MIMEOctetStream, bytes.NewReader(jsonBytes))
} }

View File

@ -64,7 +64,7 @@ func (api CredentialApi) CredentialCreateEndpoint(c echo.Context) error {
item.Encrypted = true item.Encrypted = true
if err := service.CredentialService.Create(&item); err != nil { if err := service.CredentialService.Create(context.TODO(), &item); err != nil {
return err return err
} }

View File

@ -159,6 +159,7 @@ func (api GuacamoleApi) Guacamole(c echo.Context) error {
guacamoleHandler := NewGuacamoleHandler(ws, guacdTunnel) guacamoleHandler := NewGuacamoleHandler(ws, guacdTunnel)
guacamoleHandler.Start() guacamoleHandler.Start()
defer guacamoleHandler.Stop()
for { for {
_, message, err := ws.ReadMessage() _, message, err := ws.ReadMessage()
@ -168,7 +169,6 @@ func (api GuacamoleApi) Guacamole(c echo.Context) error {
_ = guacdTunnel.Close() _ = guacdTunnel.Close()
service.SessionService.CloseSessionById(sessionId, Normal, "用户正常退出") service.SessionService.CloseSessionById(sessionId, Normal, "用户正常退出")
guacamoleHandler.Stop()
return nil return nil
} }
_, err = guacdTunnel.WriteAndFlush(message) _, err = guacdTunnel.WriteAndFlush(message)
@ -193,7 +193,7 @@ func (api GuacamoleApi) setAssetConfig(attributes map[string]string, s model.Ses
} }
realPath := path.Join(service.StorageService.GetBaseDrivePath(), storageId) realPath := path.Join(service.StorageService.GetBaseDrivePath(), storageId)
configuration.SetParameter(guacd.EnableDrive, "true") configuration.SetParameter(guacd.EnableDrive, "true")
configuration.SetParameter(guacd.DriveName, "Next Terminal Filesystem") configuration.SetParameter(guacd.DriveName, "Filesystem")
configuration.SetParameter(guacd.DrivePath, realPath) configuration.SetParameter(guacd.DrivePath, realPath)
log.Debugf("[%v] 会话 %v:%v 映射目录地址为 %v", s.ID, s.IP, s.Port, realPath) log.Debugf("[%v] 会话 %v:%v 映射目录地址为 %v", s.ID, s.IP, s.Port, realPath)
} else { } else {
@ -258,6 +258,7 @@ func (api GuacamoleApi) GuacamoleMonitor(c echo.Context) error {
guacamoleHandler := NewGuacamoleHandler(ws, guacdTunnel) guacamoleHandler := NewGuacamoleHandler(ws, guacdTunnel)
guacamoleHandler.Start() guacamoleHandler.Start()
defer guacamoleHandler.Stop()
for { for {
_, message, err := ws.ReadMessage() _, message, err := ws.ReadMessage()
@ -269,7 +270,6 @@ func (api GuacamoleApi) GuacamoleMonitor(c echo.Context) error {
observerId := nextSession.ID observerId := nextSession.ID
forObsSession.Observer.Del <- observerId forObsSession.Observer.Del <- observerId
log.Debugf("[%v:%v] 观察者[%v]退出会话", sessionId, connectionId, observerId) log.Debugf("[%v:%v] 观察者[%v]退出会话", sessionId, connectionId, observerId)
guacamoleHandler.Stop()
return nil return nil
} }
_, err = guacdTunnel.WriteAndFlush(message) _, err = guacdTunnel.WriteAndFlush(message)

View File

@ -2,7 +2,6 @@ package api
import ( import (
"context" "context"
"time"
"next-terminal/server/guacd" "next-terminal/server/guacd"
"next-terminal/server/log" "next-terminal/server/log"
@ -12,73 +11,47 @@ import (
) )
type GuacamoleHandler struct { type GuacamoleHandler struct {
ws *websocket.Conn ws *websocket.Conn
tunnel *guacd.Tunnel tunnel *guacd.Tunnel
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
dataChan chan []byte
tick *time.Ticker
} }
func NewGuacamoleHandler(ws *websocket.Conn, tunnel *guacd.Tunnel) *GuacamoleHandler { func NewGuacamoleHandler(ws *websocket.Conn, tunnel *guacd.Tunnel) *GuacamoleHandler {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
tick := time.NewTicker(time.Millisecond * time.Duration(60))
return &GuacamoleHandler{ return &GuacamoleHandler{
ws: ws, ws: ws,
tunnel: tunnel, tunnel: tunnel,
ctx: ctx, ctx: ctx,
cancel: cancel, cancel: cancel,
dataChan: make(chan []byte),
tick: tick,
} }
} }
func (r GuacamoleHandler) Start() { func (r GuacamoleHandler) Start() {
go r.readFormTunnel() go func() {
go r.writeToWebsocket() for {
} select {
case <-r.ctx.Done():
func (r GuacamoleHandler) Stop() {
r.tick.Stop()
r.cancel()
}
func (r GuacamoleHandler) readFormTunnel() {
for {
select {
case <-r.ctx.Done():
return
default:
instruction, err := r.tunnel.Read()
if err != nil {
utils.Disconnect(r.ws, TunnelClosed, "远程连接已关闭")
return return
} default:
if len(instruction) == 0 { instruction, err := r.tunnel.Read()
continue if err != nil {
} utils.Disconnect(r.ws, TunnelClosed, "远程连接已关闭")
r.dataChan <- instruction return
} }
} if len(instruction) == 0 {
} continue
}
func (r GuacamoleHandler) writeToWebsocket() { err = r.ws.WriteMessage(websocket.TextMessage, instruction)
var buf []byte
for {
select {
case <-r.ctx.Done():
return
case <-r.tick.C:
if len(buf) > 0 {
err := r.ws.WriteMessage(websocket.TextMessage, buf)
if err != nil { if err != nil {
log.Debugf("WebSocket写入失败即将关闭Guacd连接...") log.Debugf("WebSocket写入失败即将关闭Guacd连接...")
return return
} }
buf = []byte{}
} }
case data := <-r.dataChan:
buf = append(buf, data...)
} }
} }()
}
func (r GuacamoleHandler) Stop() {
r.cancel()
} }

View File

@ -25,7 +25,7 @@ func (api JobApi) JobCreateEndpoint(c echo.Context) error {
item.ID = utils.UUID() item.ID = utils.UUID()
item.Created = utils.NowJsonTime() item.Created = utils.NowJsonTime()
if err := service.JobService.Create(&item); err != nil { if err := service.JobService.Create(context.TODO(), &item); err != nil {
return err return err
} }
return Success(c, "") return Success(c, "")

View File

@ -2,6 +2,7 @@ package api
import ( import (
"context" "context"
"next-terminal/server/service"
"next-terminal/server/dto" "next-terminal/server/dto"
"next-terminal/server/repository" "next-terminal/server/repository"
@ -42,7 +43,7 @@ func (api ResourceSharerApi) ResourceAddByUserIdAssignEndPoint(c echo.Context) e
return err return err
} }
if err := repository.ResourceSharerRepository.AddSharerResources(ru.UserGroupId, ru.UserId, ru.StrategyId, ru.ResourceType, ru.ResourceIds); err != nil { if err := service.UserService.AddSharerResources(context.TODO(), ru.UserGroupId, ru.UserId, ru.StrategyId, ru.ResourceType, ru.ResourceIds); err != nil {
return err return err
} }

View File

@ -312,16 +312,16 @@ func (api SessionApi) SessionDownloadEndpoint(c echo.Context) error {
if s.Download != "1" { if s.Download != "1" {
return errors.New("禁止操作") return errors.New("禁止操作")
} }
remoteFile := c.QueryParam("file") file := c.QueryParam("file")
// 获取带后缀的文件名称 // 获取带后缀的文件名称
filenameWithSuffix := path.Base(remoteFile) filenameWithSuffix := path.Base(file)
if "ssh" == s.Protocol { if "ssh" == s.Protocol {
nextSession := session.GlobalSessionManager.GetById(sessionId) nextSession := session.GlobalSessionManager.GetById(sessionId)
if nextSession == nil { if nextSession == nil {
return errors.New("获取会话失败") return errors.New("获取会话失败")
} }
dstFile, err := nextSession.NextTerminal.SftpClient.Open(remoteFile) dstFile, err := nextSession.NextTerminal.SftpClient.Open(file)
if err != nil { if err != nil {
return err return err
} }
@ -337,7 +337,7 @@ func (api SessionApi) SessionDownloadEndpoint(c echo.Context) error {
return c.Stream(http.StatusOK, echo.MIMEOctetStream, bytes.NewReader(buff.Bytes())) return c.Stream(http.StatusOK, echo.MIMEOctetStream, bytes.NewReader(buff.Bytes()))
} else if "rdp" == s.Protocol { } else if "rdp" == s.Protocol {
storageId := s.StorageId storageId := s.StorageId
return service.StorageService.StorageDownload(c, remoteFile, storageId) return service.StorageService.StorageDownload(c, file, storageId)
} }
return err return err
@ -541,7 +541,9 @@ func (api SessionApi) SessionRecordingEndpoint(c echo.Context) error {
_ = repository.SessionRepository.UpdateReadByIds(context.TODO(), true, []string{sessionId}) _ = repository.SessionRepository.UpdateReadByIds(context.TODO(), 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)
http.ServeFile(c.Response(), c.Request(), recording)
return nil
} }
func (api SessionApi) SessionGetEndpoint(c echo.Context) error { func (api SessionApi) SessionGetEndpoint(c echo.Context) error {

View File

@ -134,7 +134,7 @@ func (api StorageApi) StorageDeleteEndpoint(c echo.Context) error {
split := strings.Split(ids, ",") split := strings.Split(ids, ",")
for i := range split { for i := range split {
id := split[i] id := split[i]
if err := service.StorageService.DeleteStorageById(id, false); err != nil { if err := service.StorageService.DeleteStorageById(context.TODO(), id, false); err != nil {
return err return err
} }
} }
@ -173,8 +173,8 @@ func (api StorageApi) StorageDownloadEndpoint(c echo.Context) error {
if err := api.PermissionCheck(c, storageId); err != nil { if err := api.PermissionCheck(c, storageId); err != nil {
return err return err
} }
remoteFile := c.QueryParam("file") file := c.QueryParam("file")
return service.StorageService.StorageDownload(c, remoteFile, storageId) return service.StorageService.StorageDownload(c, file, storageId)
} }
func (api StorageApi) StorageUploadEndpoint(c echo.Context) error { func (api StorageApi) StorageUploadEndpoint(c echo.Context) error {

View File

@ -157,6 +157,7 @@ func (api WebTerminalApi) SshEndpoint(c echo.Context) error {
termHandler := NewTermHandler(sessionId, isRecording, ws, nextTerminal) termHandler := NewTermHandler(sessionId, isRecording, ws, nextTerminal)
termHandler.Start() termHandler.Start()
defer termHandler.Stop()
for { for {
_, message, err := ws.ReadMessage() _, message, err := ws.ReadMessage()
@ -164,7 +165,6 @@ func (api WebTerminalApi) SshEndpoint(c echo.Context) error {
// web socket会话关闭后主动关闭ssh会话 // web socket会话关闭后主动关闭ssh会话
log.Debugf("WebSocket已关闭") log.Debugf("WebSocket已关闭")
service.SessionService.CloseSessionById(sessionId, Normal, "用户正常退出") service.SessionService.CloseSessionById(sessionId, Normal, "用户正常退出")
termHandler.Stop()
break break
} }

View File

@ -20,7 +20,7 @@ func (userGroupApi UserGroupApi) UserGroupCreateEndpoint(c echo.Context) error {
return err return err
} }
if _, err := service.UserGroupService.Create(item.Name, item.Members); err != nil { if _, err := service.UserGroupService.Create(context.TODO(), item.Name, item.Members); err != nil {
return err return err
} }

View File

@ -2,7 +2,7 @@ package api
import ( import (
"context" "context"
"fmt"
"strconv" "strconv"
"strings" "strings"
@ -141,7 +141,11 @@ func (userApi UserApi) UserChangePasswordEndpoint(c echo.Context) error {
} }
if user.Mail != "" { if user.Mail != "" {
go service.MailService.SendMail(user.Mail, "[Next Terminal] 密码修改通知", "你好,"+user.Nickname+"。管理员已将你的密码修改为:"+password) subject := "密码修改通知"
text := fmt.Sprintf(`您好%s
管理员已将你的密码修改为%s
`, user.Username, password)
go service.MailService.SendMail(user.Mail, subject, text)
} }
return Success(c, "") return Success(c, "")

View File

@ -26,13 +26,6 @@ func newApp() *App {
func init() { func init() {
setupCache() setupCache()
app = newApp() app = newApp()
if err := app.InitDBData(); err != nil {
panic(err)
}
if err := app.ReloadData(); err != nil {
panic(err)
}
app.Server = setupRoutes()
} }
func (app App) InitDBData() (err error) { func (app App) InitDBData() (err error) {
@ -96,7 +89,15 @@ func (app App) ReloadData() error {
func Run() error { func Run() error {
fmt.Printf(constant.Banner, constant.Version) fmt.Printf(constant.AppBanner, constant.AppVersion)
if err := app.InitDBData(); err != nil {
panic(err)
}
if err := app.ReloadData(); err != nil {
panic(err)
}
app.Server = setupRoutes()
if config.GlobalCfg.Debug { if config.GlobalCfg.Debug {
jsonBytes, err := json.MarshalIndent(config.GlobalCfg, "", " ") jsonBytes, err := json.MarshalIndent(config.GlobalCfg, "", " ")

View File

@ -5,8 +5,9 @@ import (
) )
const ( const (
Version = "v1.2.5" AppVersion = "v1.2.5"
Banner = ` AppName = "Next Terminal"
AppBanner = `
_______ __ ___________ .__ .__ _______ __ ___________ .__ .__
\ \ ____ ___ ____/ |_ \__ ___/__________ _____ |__| ____ _____ | | \ \ ____ ___ ____/ |_ \__ ___/__________ _____ |__| ____ _____ | |
/ | \_/ __ \\ \/ /\ __\ | |_/ __ \_ __ \/ \| |/ \\__ \ | | / | \_/ __ \\ \/ /\ __\ | |_/ __ \_ __ \/ \| |/ \\__ \ | |
@ -72,8 +73,9 @@ const (
SocksProxyUsername = "socks-proxy-username" SocksProxyUsername = "socks-proxy-username"
SocksProxyPassword = "socks-proxy-password" SocksProxyPassword = "socks-proxy-password"
LoginToken = "login-token" LoginToken = "login-token"
AccessToken = "access-token" AccessToken = "access-token"
ShareSession = "share-session"
Anonymous = "anonymous" Anonymous = "anonymous"
) )

6
server/env/db.go vendored
View File

@ -36,8 +36,10 @@ func setupDB() *gorm.DB {
Logger: logMode, Logger: logMode,
}) })
} else { } else {
db, err = gorm.Open(sqlite.Open(config.GlobalCfg.Sqlite.File), &gorm.Config{ dsn := fmt.Sprintf("file:%s?cache=shared&mode=rwc", config.GlobalCfg.Sqlite.File)
Logger: logMode, db, err = gorm.Open(sqlite.Open(dsn), &gorm.Config{
Logger: logMode,
SkipDefaultTransaction: true,
}) })
} }

View File

@ -13,14 +13,9 @@ type baseRepository struct {
} }
func (b *baseRepository) GetDB(c context.Context) *gorm.DB { func (b *baseRepository) GetDB(c context.Context) *gorm.DB {
db := c.Value(constant.DB) db, ok := c.Value(constant.DB).(*gorm.DB)
if db == nil { if !ok {
return env.GetDB()
}
switch val := db.(type) {
case gorm.DB:
return &val
default:
return env.GetDB() return env.GetDB()
} }
return db
} }

View File

@ -7,7 +7,6 @@ import (
"next-terminal/server/utils" "next-terminal/server/utils"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/pkg/errors"
"gorm.io/gorm" "gorm.io/gorm"
) )
@ -101,57 +100,8 @@ func (r *resourceSharerRepository) DeleteByUserGroupId(c context.Context, userGr
return r.GetDB(c).Where("user_group_id = ?", userGroupId).Delete(&model.ResourceSharer{}).Error return r.GetDB(c).Where("user_group_id = ?", userGroupId).Delete(&model.ResourceSharer{}).Error
} }
func (r *resourceSharerRepository) AddSharerResources(userGroupId, userId, strategyId, resourceType string, resourceIds []string) error { func (r *resourceSharerRepository) AddSharerResource(c context.Context, m *model.ResourceSharer) error {
return r.GetDB(context.TODO()).Transaction(func(tx *gorm.DB) (err error) { return r.GetDB(c).Create(m).Error
for i := range resourceIds {
resourceId := resourceIds[i]
var owner string
// 检查资产是否存在
switch resourceType {
case "asset":
resource := model.Asset{}
if err = tx.Where("id = ?", resourceId).First(&resource).Error; err != nil {
return errors.Wrap(err, "find asset fail")
}
owner = resource.Owner
case "command":
resource := model.Command{}
if err = tx.Where("id = ?", resourceId).First(&resource).Error; err != nil {
return errors.Wrap(err, "find command fail")
}
owner = resource.Owner
case "credential":
resource := model.Credential{}
if err = tx.Where("id = ?", resourceId).First(&resource).Error; err != nil {
return errors.Wrap(err, "find credential fail")
}
owner = resource.Owner
}
if owner == userId {
return echo.NewHTTPError(400, "参数错误")
}
// 保证同一个资产只能分配给一个用户或者组
id := utils.Sign([]string{resourceId, resourceType, userId, userGroupId})
resource := &model.ResourceSharer{
ID: id,
ResourceId: resourceId,
ResourceType: resourceType,
StrategyId: strategyId,
UserId: userId,
UserGroupId: userGroupId,
}
err = tx.Create(resource).Error
if err != nil {
return err
}
}
return nil
})
} }
func (r *resourceSharerRepository) FindAssetIdsByUserId(c context.Context, userId string) (assetIds []string, err error) { func (r *resourceSharerRepository) FindAssetIdsByUserId(c context.Context, userId string) (assetIds []string, err error) {
@ -228,3 +178,7 @@ func (r *resourceSharerRepository) FindAll(c context.Context) (o []model.Resourc
err = r.GetDB(c).Find(&o).Error err = r.GetDB(c).Find(&o).Error
return return
} }
func (r *resourceSharerRepository) DeleteById(ctx context.Context, id string) error {
return r.GetDB(ctx).Where("id = ?", id).Delete(&model.ResourceSharer{}).Error
}

View File

@ -19,14 +19,9 @@ type accessTokenService struct {
baseService baseService
} }
func (service accessTokenService) FindByUserId(userId string) (model.AccessToken, error) {
return repository.AccessTokenRepository.FindByUserId(context.TODO(), userId)
}
func (service accessTokenService) GenAccessToken(userId string) error { func (service accessTokenService) GenAccessToken(userId string) error {
return env.GetDB().Transaction(func(tx *gorm.DB) error { return env.GetDB().Transaction(func(tx *gorm.DB) error {
ctx := service.Context(tx) ctx := service.Context(tx)
user, err := repository.UserRepository.FindById(ctx, userId) user, err := repository.UserRepository.FindById(ctx, userId)
if err != nil { if err != nil {
return err return err

View File

@ -119,7 +119,7 @@ func (s assetService) FindByIdAndDecrypt(c context.Context, id string) (model.As
func (s assetService) CheckStatus(accessGatewayId string, ip string, port int) (active bool, err error) { func (s assetService) CheckStatus(accessGatewayId string, ip string, port int) (active bool, err error) {
if accessGatewayId != "" && accessGatewayId != "-" { if accessGatewayId != "" && accessGatewayId != "-" {
g, e1 := GatewayService.GetGatewayAndReconnectById(accessGatewayId) g, e1 := GatewayService.GetGatewayAndReconnectById(accessGatewayId)
if err != nil { if e1 != nil {
return false, e1 return false, e1
} }
@ -141,7 +141,7 @@ func (s assetService) CheckStatus(accessGatewayId string, ip string, port int) (
return active, err return active, err
} }
func (s assetService) Create(m echo.Map) (model.Asset, error) { func (s assetService) Create(ctx context.Context, m echo.Map) (model.Asset, error) {
data, err := json.Marshal(m) data, err := json.Marshal(m)
if err != nil { if err != nil {
@ -156,29 +156,36 @@ func (s assetService) Create(m echo.Map) (model.Asset, error) {
item.Created = utils.NowJsonTime() item.Created = utils.NowJsonTime()
item.Active = true item.Active = true
return item, env.GetDB().Transaction(func(tx *gorm.DB) error { if s.InTransaction(ctx) {
c := s.Context(tx) return item, s.create(ctx, item, m)
} else {
return item, env.GetDB().Transaction(func(tx *gorm.DB) error {
c := s.Context(tx)
return s.create(c, item, m)
})
}
}
if err := s.Encrypt(&item, config.GlobalCfg.EncryptionPassword); err != nil { func (s assetService) create(c context.Context, item model.Asset, m echo.Map) error {
return err if err := s.Encrypt(&item, config.GlobalCfg.EncryptionPassword); err != nil {
} return err
if err := repository.AssetRepository.Create(c, &item); err != nil { }
return err if err := repository.AssetRepository.Create(c, &item); err != nil {
} return err
}
if err := repository.AssetRepository.UpdateAttributes(c, item.ID, item.Protocol, m); err != nil { if err := repository.AssetRepository.UpdateAttributes(c, item.ID, item.Protocol, m); err != nil {
return err return err
} }
go func() { //go func() {
active, _ := s.CheckStatus(item.AccessGatewayId, item.IP, item.Port) // active, _ := s.CheckStatus(item.AccessGatewayId, item.IP, item.Port)
//
if item.Active != active { // if item.Active != active {
_ = repository.AssetRepository.UpdateActiveById(context.TODO(), active, item.ID) // _ = repository.AssetRepository.UpdateActiveById(context.TODO(), active, item.ID)
} // }
}() //}()
return nil return nil
})
} }
func (s assetService) DeleteById(id string) error { func (s assetService) DeleteById(id string) error {

View File

@ -128,12 +128,12 @@ func (service backupService) Export() (error, *dto.Backup) {
func (service backupService) Import(backup *dto.Backup) error { func (service backupService) Import(backup *dto.Backup) error {
return env.GetDB().Transaction(func(tx *gorm.DB) error { return env.GetDB().Transaction(func(tx *gorm.DB) error {
c := service.Context(tx) ctx := service.Context(tx)
var userIdMapping = make(map[string]string) var userIdMapping = make(map[string]string)
if len(backup.Users) > 0 { if len(backup.Users) > 0 {
for _, item := range backup.Users { for _, item := range backup.Users {
oldId := item.ID oldId := item.ID
exist, err := repository.UserRepository.ExistByUsername(c, item.Username) exist, err := repository.UserRepository.ExistByUsername(ctx, item.Username)
if err != nil { if err != nil {
return err return err
} }
@ -144,7 +144,7 @@ func (service backupService) Import(backup *dto.Backup) error {
newId := utils.UUID() newId := utils.UUID()
item.ID = newId item.ID = newId
item.Password = utils.GenPassword() item.Password = utils.GenPassword()
if err := repository.UserRepository.Create(c, &item); err != nil { if err := repository.UserRepository.Create(ctx, &item); err != nil {
return err return err
} }
userIdMapping[oldId] = newId userIdMapping[oldId] = newId
@ -163,7 +163,7 @@ func (service backupService) Import(backup *dto.Backup) error {
} }
} }
userGroup, err := UserGroupService.Create(item.Name, members) userGroup, err := UserGroupService.Create(ctx, item.Name, members)
if err != nil { if err != nil {
if errors.Is(constant.ErrNameAlreadyUsed, err) { if errors.Is(constant.ErrNameAlreadyUsed, err) {
// 删除名称重复的用户组 // 删除名称重复的用户组
@ -187,7 +187,7 @@ func (service backupService) Import(backup *dto.Backup) error {
item.ID = utils.UUID() item.ID = utils.UUID()
item.Owner = owner item.Owner = owner
item.Created = utils.NowJsonTime() item.Created = utils.NowJsonTime()
if err := repository.StorageRepository.Create(c, &item); err != nil { if err := repository.StorageRepository.Create(ctx, &item); err != nil {
return err return err
} }
} }
@ -200,7 +200,7 @@ func (service backupService) Import(backup *dto.Backup) error {
newId := utils.UUID() newId := utils.UUID()
item.ID = newId item.ID = newId
item.Created = utils.NowJsonTime() item.Created = utils.NowJsonTime()
if err := repository.StrategyRepository.Create(c, &item); err != nil { if err := repository.StrategyRepository.Create(ctx, &item); err != nil {
return err return err
} }
strategyIdMapping[oldId] = newId strategyIdMapping[oldId] = newId
@ -210,7 +210,7 @@ func (service backupService) Import(backup *dto.Backup) error {
if len(backup.AccessSecurities) > 0 { if len(backup.AccessSecurities) > 0 {
for _, item := range backup.AccessSecurities { for _, item := range backup.AccessSecurities {
item.ID = utils.UUID() item.ID = utils.UUID()
if err := repository.SecurityRepository.Create(c, &item); err != nil { if err := repository.SecurityRepository.Create(ctx, &item); err != nil {
return err return err
} }
// 更新内存中的安全规则 // 更新内存中的安全规则
@ -231,7 +231,7 @@ func (service backupService) Import(backup *dto.Backup) error {
newId := utils.UUID() newId := utils.UUID()
item.ID = newId item.ID = newId
item.Created = utils.NowJsonTime() item.Created = utils.NowJsonTime()
if err := repository.GatewayRepository.Create(c, &item); err != nil { if err := repository.GatewayRepository.Create(ctx, &item); err != nil {
return err return err
} }
accessGatewayIdMapping[oldId] = newId accessGatewayIdMapping[oldId] = newId
@ -242,7 +242,7 @@ func (service backupService) Import(backup *dto.Backup) error {
for _, item := range backup.Commands { for _, item := range backup.Commands {
item.ID = utils.UUID() item.ID = utils.UUID()
item.Created = utils.NowJsonTime() item.Created = utils.NowJsonTime()
if err := repository.CommandRepository.Create(c, &item); err != nil { if err := repository.CommandRepository.Create(ctx, &item); err != nil {
return err return err
} }
} }
@ -254,7 +254,7 @@ func (service backupService) Import(backup *dto.Backup) error {
oldId := item.ID oldId := item.ID
newId := utils.UUID() newId := utils.UUID()
item.ID = newId item.ID = newId
if err := CredentialService.Create(&item); err != nil { if err := CredentialService.Create(ctx, &item); err != nil {
return err return err
} }
credentialIdMapping[oldId] = newId credentialIdMapping[oldId] = newId
@ -282,7 +282,7 @@ func (service backupService) Import(backup *dto.Backup) error {
} }
oldId := m["id"].(string) oldId := m["id"].(string)
asset, err := AssetService.Create(m) asset, err := AssetService.Create(ctx, m)
if err != nil { if err != nil {
return err return err
} }
@ -299,7 +299,7 @@ func (service backupService) Import(backup *dto.Backup) error {
strategyId := strategyIdMapping[item.StrategyId] strategyId := strategyIdMapping[item.StrategyId]
resourceId := assetIdMapping[item.ResourceId] resourceId := assetIdMapping[item.ResourceId]
if err := repository.ResourceSharerRepository.AddSharerResources(userGroupId, userId, strategyId, item.ResourceType, []string{resourceId}); err != nil { if err := UserService.AddSharerResources(ctx, userGroupId, userId, strategyId, item.ResourceType, []string{resourceId}); err != nil {
return err return err
} }
} }
@ -311,6 +311,7 @@ func (service backupService) Import(backup *dto.Backup) error {
continue continue
} }
item.ID = utils.UUID()
resourceIds := strings.Split(item.ResourceIds, ",") resourceIds := strings.Split(item.ResourceIds, ",")
if len(resourceIds) > 0 { if len(resourceIds) > 0 {
var newResourceIds = make([]string, 0) var newResourceIds = make([]string, 0)
@ -319,7 +320,7 @@ func (service backupService) Import(backup *dto.Backup) error {
} }
item.ResourceIds = strings.Join(newResourceIds, ",") item.ResourceIds = strings.Join(newResourceIds, ",")
} }
if err := JobService.Create(&item); err != nil { if err := JobService.Create(ctx, &item); err != nil {
return err return err
} }
} }

View File

@ -14,3 +14,8 @@ type baseService struct {
func (service baseService) Context(db *gorm.DB) context.Context { func (service baseService) Context(db *gorm.DB) context.Context {
return context.WithValue(context.TODO(), constant.DB, db) return context.WithValue(context.TODO(), constant.DB, db)
} }
func (service baseService) InTransaction(ctx context.Context) bool {
_, ok := ctx.Value(constant.DB).(*gorm.DB)
return ok
}

View File

@ -99,8 +99,8 @@ func (s credentialService) Decrypt(item *model.Credential, password []byte) erro
return nil return nil
} }
func (s credentialService) FindByIdAndDecrypt(c context.Context, id string) (o model.Credential, err error) { func (s credentialService) FindByIdAndDecrypt(ctx context.Context, id string) (o model.Credential, err error) {
credential, err := repository.CredentialRepository.FindById(c, id) credential, err := repository.CredentialRepository.FindById(ctx, id)
if err != nil { if err != nil {
return o, err return o, err
} }
@ -110,10 +110,10 @@ func (s credentialService) FindByIdAndDecrypt(c context.Context, id string) (o m
return credential, nil return credential, nil
} }
func (s credentialService) Create(item *model.Credential) error { func (s credentialService) Create(ctx context.Context, item *model.Credential) error {
// 加密密码之后进行存储 // 加密密码之后进行存储
if err := s.Encrypt(item, config.GlobalCfg.EncryptionPassword); err != nil { if err := s.Encrypt(item, config.GlobalCfg.EncryptionPassword); err != nil {
return err return err
} }
return repository.CredentialRepository.Create(context.TODO(), item) return repository.CredentialRepository.Create(ctx, item)
} }

View File

@ -103,7 +103,7 @@ func (r jobService) InitJob() error {
return nil return nil
} }
func (r jobService) Create(o *model.Job) (err error) { func (r jobService) Create(ctx context.Context, o *model.Job) (err error) {
if o.Status == constant.JobStatusRunning { if o.Status == constant.JobStatusRunning {
j, err := getJob(o) j, err := getJob(o)
@ -117,7 +117,7 @@ func (r jobService) Create(o *model.Job) (err error) {
o.CronJobId = int(jobId) o.CronJobId = int(jobId)
} }
return repository.JobRepository.Create(context.TODO(), o) return repository.JobRepository.Create(ctx, o)
} }
func (r jobService) DeleteJobById(id string) error { func (r jobService) DeleteJobById(id string) error {

View File

@ -2,6 +2,7 @@ package service
import ( import (
"context" "context"
"fmt"
"net/smtp" "net/smtp"
"next-terminal/server/constant" "next-terminal/server/constant"
@ -27,7 +28,7 @@ func (r mailService) SendMail(to, subject, text string) {
} }
e := email.NewEmail() e := email.NewEmail()
e.From = "Next Terminal <" + username + ">" e.From = fmt.Sprintf("%s <%s>", constant.AppName, username)
e.To = []string{to} e.To = []string{to}
e.Subject = subject e.Subject = subject
e.Text = []byte(text) e.Text = []byte(text)

View File

@ -17,161 +17,63 @@ type propertyService struct {
baseService baseService
} }
var deprecatedPropertyNames = []string{
guacd.EnableDrive,
guacd.DrivePath,
guacd.DriveName,
guacd.DisableGlyphCaching,
guacd.CreateRecordingPath,
}
var defaultProperties = map[string]string{
guacd.EnableRecording: "true",
guacd.FontName: "menlo",
guacd.FontSize: "12",
guacd.ColorScheme: "gray-black",
guacd.EnableWallpaper: "true",
guacd.EnableTheming: "true",
guacd.EnableFontSmoothing: "true",
guacd.EnableFullWindowDrag: "true",
guacd.EnableDesktopComposition: "true",
guacd.EnableMenuAnimations: "true",
guacd.DisableBitmapCaching: "false",
guacd.DisableOffscreenCaching: "false",
"cron-log-saved-limit": "360",
"login-log-saved-limit": "360",
"session-saved-limit": "360",
"user-default-storage-size": "5120",
}
func (service propertyService) InitProperties() error { func (service propertyService) InitProperties() error {
propertyMap := repository.PropertyRepository.FindAllMap(context.TODO()) propertyMap := repository.PropertyRepository.FindAllMap(context.TODO())
if len(propertyMap[guacd.EnableRecording]) == 0 { for name, value := range defaultProperties {
property := model.Property{ if err := service.CreateIfAbsent(propertyMap, name, value); err != nil {
Name: guacd.EnableRecording,
Value: "true",
}
if err := repository.PropertyRepository.Create(context.TODO(), &property); err != nil {
return err return err
} }
} }
if len(propertyMap[guacd.CreateRecordingPath]) == 0 { return nil
property := model.Property{ }
Name: guacd.CreateRecordingPath,
Value: "true",
}
if err := repository.PropertyRepository.Create(context.TODO(), &property); err != nil {
return err
}
}
if len(propertyMap[guacd.FontName]) == 0 { func (service propertyService) CreateIfAbsent(propertyMap map[string]string, name, value string) error {
if len(propertyMap[name]) == 0 {
property := model.Property{ property := model.Property{
Name: guacd.FontName, Name: name,
Value: "menlo", Value: value,
}
if err := repository.PropertyRepository.Create(context.TODO(), &property); err != nil {
return err
}
}
if len(propertyMap[guacd.FontSize]) == 0 {
property := model.Property{
Name: guacd.FontSize,
Value: "12",
}
if err := repository.PropertyRepository.Create(context.TODO(), &property); err != nil {
return err
}
}
if len(propertyMap[guacd.ColorScheme]) == 0 {
property := model.Property{
Name: guacd.ColorScheme,
Value: "gray-black",
}
if err := repository.PropertyRepository.Create(context.TODO(), &property); err != nil {
return err
}
}
if len(propertyMap[guacd.EnableWallpaper]) == 0 {
property := model.Property{
Name: guacd.EnableWallpaper,
Value: "false",
}
if err := repository.PropertyRepository.Create(context.TODO(), &property); err != nil {
return err
}
}
if len(propertyMap[guacd.EnableTheming]) == 0 {
property := model.Property{
Name: guacd.EnableTheming,
Value: "false",
}
if err := repository.PropertyRepository.Create(context.TODO(), &property); err != nil {
return err
}
}
if len(propertyMap[guacd.EnableFontSmoothing]) == 0 {
property := model.Property{
Name: guacd.EnableFontSmoothing,
Value: "false",
}
if err := repository.PropertyRepository.Create(context.TODO(), &property); err != nil {
return err
}
}
if len(propertyMap[guacd.EnableFullWindowDrag]) == 0 {
property := model.Property{
Name: guacd.EnableFullWindowDrag,
Value: "false",
}
if err := repository.PropertyRepository.Create(context.TODO(), &property); err != nil {
return err
}
}
if len(propertyMap[guacd.EnableDesktopComposition]) == 0 {
property := model.Property{
Name: guacd.EnableDesktopComposition,
Value: "false",
}
if err := repository.PropertyRepository.Create(context.TODO(), &property); err != nil {
return err
}
}
if len(propertyMap[guacd.EnableMenuAnimations]) == 0 {
property := model.Property{
Name: guacd.EnableMenuAnimations,
Value: "false",
}
if err := repository.PropertyRepository.Create(context.TODO(), &property); err != nil {
return err
}
}
if len(propertyMap[guacd.DisableBitmapCaching]) == 0 {
property := model.Property{
Name: guacd.DisableBitmapCaching,
Value: "false",
}
if err := repository.PropertyRepository.Create(context.TODO(), &property); err != nil {
return err
}
}
if len(propertyMap[guacd.DisableOffscreenCaching]) == 0 {
property := model.Property{
Name: guacd.DisableOffscreenCaching,
Value: "false",
}
if err := repository.PropertyRepository.Create(context.TODO(), &property); err != nil {
return err
}
}
if len(propertyMap[guacd.DisableGlyphCaching]) > 0 {
if err := repository.PropertyRepository.DeleteByName(context.TODO(), guacd.DisableGlyphCaching); err != nil {
return err
} }
return repository.PropertyRepository.Create(context.TODO(), &property)
} }
return nil return nil
} }
func (service propertyService) DeleteDeprecatedProperty() error { func (service propertyService) DeleteDeprecatedProperty() error {
propertyMap := repository.PropertyRepository.FindAllMap(context.TODO()) propertyMap := repository.PropertyRepository.FindAllMap(context.TODO())
if propertyMap[guacd.EnableDrive] != "" { for _, name := range deprecatedPropertyNames {
if err := repository.PropertyRepository.DeleteByName(context.TODO(), guacd.DriveName); err != nil { if propertyMap[name] == "" {
return err continue
} }
} if err := repository.PropertyRepository.DeleteByName(context.TODO(), name); err != nil {
if propertyMap[guacd.DrivePath] != "" {
if err := repository.PropertyRepository.DeleteByName(context.TODO(), guacd.DrivePath); err != nil {
return err
}
}
if propertyMap[guacd.DriveName] != "" {
if err := repository.PropertyRepository.DeleteByName(context.TODO(), guacd.DriveName); err != nil {
return err return err
} }
} }

View File

@ -4,11 +4,14 @@ import (
"bufio" "bufio"
"context" "context"
"errors" "errors"
"fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"mime/multipart" "mime/multipart"
"net/http"
"os" "os"
"path" "path"
"strconv"
"strings" "strings"
"next-terminal/server/config" "next-terminal/server/config"
@ -33,7 +36,7 @@ func (service storageService) InitStorages() error {
userId := users[i].ID userId := users[i].ID
_, err := repository.StorageRepository.FindByOwnerIdAndDefault(context.TODO(), userId, true) _, err := repository.StorageRepository.FindByOwnerIdAndDefault(context.TODO(), userId, true)
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
err = service.CreateStorageByUser(&users[i]) err = service.CreateStorageByUser(context.TODO(), &users[i])
if err != nil { if err != nil {
return err return err
} }
@ -58,7 +61,7 @@ func (service storageService) InitStorages() error {
} }
if !userExist { if !userExist {
if err := service.DeleteStorageById(storage.ID, true); err != nil { if err := service.DeleteStorageById(context.TODO(), storage.ID, true); err != nil {
return err return err
} }
} }
@ -75,14 +78,29 @@ func (service storageService) InitStorages() error {
return nil return nil
} }
func (service storageService) CreateStorageByUser(user *model.User) error { func (service storageService) CreateStorageByUser(c context.Context, user *model.User) error {
drivePath := service.GetBaseDrivePath() drivePath := service.GetBaseDrivePath()
var limitSize int64 = -1
property, err := repository.PropertyRepository.FindByName(c, "user-default-storage-size")
if err != nil {
return err
}
limitSize, err = strconv.ParseInt(property.Value, 10, 64)
if err != nil {
return err
}
limitSize = limitSize * 1024 * 1024
if limitSize < 0 {
limitSize = -1
}
storage := model.Storage{ storage := model.Storage{
ID: user.ID, ID: user.ID,
Name: user.Nickname + "的默认空间", Name: user.Nickname + "的默认空间",
IsShare: false, IsShare: false,
IsDefault: true, IsDefault: true,
LimitSize: -1, LimitSize: limitSize,
Owner: user.ID, Owner: user.ID,
Created: utils.NowJsonTime(), Created: utils.NowJsonTime(),
} }
@ -91,8 +109,9 @@ func (service storageService) CreateStorageByUser(user *model.User) error {
return err return err
} }
log.Infof("创建storage:「%v」文件夹: %v", storage.Name, storageDir) log.Infof("创建storage:「%v」文件夹: %v", storage.Name, storageDir)
err := repository.StorageRepository.Create(context.TODO(), &storage) err = repository.StorageRepository.Create(c, &storage)
if err != nil { if err != nil {
_ = os.RemoveAll(storageDir)
return err return err
} }
return nil return nil
@ -135,9 +154,9 @@ func (service storageService) GetBaseDrivePath() string {
return config.GlobalCfg.Guacd.Drive return config.GlobalCfg.Guacd.Drive
} }
func (service storageService) DeleteStorageById(id string, force bool) error { func (service storageService) DeleteStorageById(c context.Context, id string, force bool) error {
drivePath := service.GetBaseDrivePath() drivePath := service.GetBaseDrivePath()
storage, err := repository.StorageRepository.FindById(context.TODO(), id) storage, err := repository.StorageRepository.FindById(c, id)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil return nil
@ -152,7 +171,7 @@ func (service storageService) DeleteStorageById(id string, force bool) error {
if err := os.RemoveAll(path.Join(drivePath, id)); err != nil { if err := os.RemoveAll(path.Join(drivePath, id)); err != nil {
return err return err
} }
if err := repository.StorageRepository.DeleteById(context.TODO(), id); err != nil { if err := repository.StorageRepository.DeleteById(c, id); err != nil {
return err return err
} }
return nil return nil
@ -229,14 +248,20 @@ func (service storageService) StorageEdit(file string, fileContent string, stora
return nil return nil
} }
func (service storageService) StorageDownload(c echo.Context, remoteFile, storageId string) error { func (service storageService) StorageDownload(c echo.Context, file, storageId string) error {
drivePath := service.GetBaseDrivePath() drivePath := service.GetBaseDrivePath()
if strings.Contains(remoteFile, "../") { if strings.Contains(file, "../") {
return errors.New("非法请求 :(") return errors.New("非法请求 :(")
} }
// 获取带后缀的文件名称 // 获取带后缀的文件名称
filenameWithSuffix := path.Base(remoteFile) filenameWithSuffix := path.Base(file)
return c.Attachment(path.Join(path.Join(drivePath, storageId), remoteFile), filenameWithSuffix) p := path.Join(path.Join(drivePath, storageId), file)
//log.Infof("download %v", p)
c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filenameWithSuffix))
c.Response().Header().Set("Content-Type", "application/octet-stream")
http.ServeFile(c.Response(), c.Request(), p)
return nil
} }
func (service storageService) StorageLs(remoteDir, storageId string) (error, []File) { func (service storageService) StorageLs(remoteDir, storageId string) (error, []File) {

View File

@ -90,31 +90,31 @@ func (service userService) FixUserOnlineState() error {
return nil return nil
} }
func (service userService) Logout(token string) {
cache.TokenManager.Delete(token)
}
func (service userService) LogoutByToken(token string) (err error) { func (service userService) LogoutByToken(token string) (err error) {
return env.GetDB().Transaction(func(tx *gorm.DB) error { loginLog, err := repository.LoginLogRepository.FindById(context.TODO(), token)
c := service.Context(tx) if err != nil {
loginLog, err := repository.LoginLogRepository.FindById(c, token)
if err != nil {
return err
}
cache.TokenManager.Delete(token)
loginLogForUpdate := &model.LoginLog{LogoutTime: utils.NowJsonTime(), ID: token}
err = repository.LoginLogRepository.Update(c, loginLogForUpdate)
if err != nil {
return err
}
loginLogs, err := repository.LoginLogRepository.FindAliveLoginLogsByUsername(c, loginLog.Username)
if err != nil {
return err
}
if len(loginLogs) == 0 {
err = repository.UserRepository.UpdateOnlineByUsername(c, loginLog.Username, false)
}
return err return err
}) }
loginLogForUpdate := &model.LoginLog{LogoutTime: utils.NowJsonTime(), ID: token}
err = repository.LoginLogRepository.Update(context.TODO(), loginLogForUpdate)
if err != nil {
return err
}
loginLogs, err := repository.LoginLogRepository.FindAliveLoginLogsByUsername(context.TODO(), loginLog.Username)
if err != nil {
return err
}
if len(loginLogs) == 0 {
err = repository.UserRepository.UpdateOnlineByUsername(context.TODO(), loginLog.Username, false)
}
return err
} }
func (service userService) LogoutById(c context.Context, id string) error { func (service userService) LogoutById(c context.Context, id string) error {
@ -130,13 +130,26 @@ func (service userService) LogoutById(c context.Context, id string) error {
for j := range loginLogs { for j := range loginLogs {
token := loginLogs[j].ID token := loginLogs[j].ID
if err := service.LogoutByToken(token); err != nil { service.Logout(token)
return err
}
} }
return nil return nil
} }
func (service userService) GetUserLoginToken(c context.Context, username string) ([]string, error) {
loginLogs, err := repository.LoginLogRepository.FindAliveLoginLogsByUsername(c, username)
if err != nil {
return nil, err
}
var tokens []string
for j := range loginLogs {
token := loginLogs[j].ID
tokens = append(tokens, token)
}
return tokens, nil
}
func (service userService) OnEvicted(token string, value interface{}) { func (service userService) OnEvicted(token string, value interface{}) {
if strings.HasPrefix(token, "forever") { if strings.HasPrefix(token, "forever") {
@ -144,30 +157,24 @@ func (service userService) OnEvicted(token string, value interface{}) {
} else { } else {
log.Debugf("用户Token「%v」过期", token) log.Debugf("用户Token「%v」过期", token)
err := service.LogoutByToken(token) err := service.LogoutByToken(token)
if err != nil { if err != nil && !errors.Is(gorm.ErrRecordNotFound, err) {
log.Errorf("退出登录失败 %v", err) log.Errorf("退出登录失败 %v", err)
} }
} }
} }
func (service userService) UpdateStatusById(id string, status string) error { func (service userService) UpdateStatusById(id string, status string) error {
return env.GetDB().Transaction(func(tx *gorm.DB) error { if constant.StatusDisabled == status {
c := service.Context(tx) // 将该用户下线
if c.Value(constant.DB) == nil { if err := service.LogoutById(context.TODO(), id); err != nil {
c = context.WithValue(c, constant.DB, env.GetDB()) return err
} }
if constant.StatusDisabled == status { }
// 将该用户下线 u := model.User{
if err := service.LogoutById(c, id); err != nil { ID: id,
return err Status: status,
} }
} return repository.UserRepository.Update(context.TODO(), &u)
u := model.User{
ID: id,
Status: status,
}
return repository.UserRepository.Update(c, &u)
})
} }
@ -231,13 +238,19 @@ func (service userService) CreateUser(user model.User) (err error) {
if err := repository.UserRepository.Create(c, &user); err != nil { if err := repository.UserRepository.Create(c, &user); err != nil {
return err return err
} }
err = StorageService.CreateStorageByUser(&user) err = StorageService.CreateStorageByUser(c, &user)
if err != nil { if err != nil {
return err return err
} }
if user.Mail != "" { if user.Mail != "" {
go MailService.SendMail(user.Mail, "[Next Terminal] 注册通知", "你好,"+user.Nickname+"。管理员为你注册了账号:"+user.Username+" 密码:"+password) subject := fmt.Sprintf("%s 注册通知", constant.AppName)
text := fmt.Sprintf(`您好%s
管理员为你开通了账户
账号%s
密码%s
`, user.Username, user.Username, password)
go MailService.SendMail(user.Mail, subject, text)
} }
return nil return nil
}) })
@ -245,16 +258,19 @@ func (service userService) CreateUser(user model.User) (err error) {
} }
func (service userService) DeleteUserById(userId string) error { func (service userService) DeleteUserById(userId string) error {
return env.GetDB().Transaction(func(tx *gorm.DB) error { user, err := repository.UserRepository.FindById(context.TODO(), userId)
if err != nil {
return err
}
username := user.Username
// 下线该用户
loginTokens, err := service.GetUserLoginToken(context.TODO(), username)
if err != nil {
return err
}
err = env.GetDB().Transaction(func(tx *gorm.DB) error {
c := service.Context(tx) c := service.Context(tx)
// 下线该用户
if err := service.LogoutById(c, userId); err != nil {
return err
}
// 删除用户
if err := repository.UserRepository.DeleteById(c, userId); err != nil {
return err
}
// 删除用户与用户组的关系 // 删除用户与用户组的关系
if err := repository.UserGroupMemberRepository.DeleteByUserId(c, userId); err != nil { if err := repository.UserGroupMemberRepository.DeleteByUserId(c, userId); err != nil {
return err return err
@ -264,19 +280,37 @@ func (service userService) DeleteUserById(userId string) error {
return err return err
} }
// 删除用户的默认磁盘空间 // 删除用户的默认磁盘空间
if err := StorageService.DeleteStorageById(userId, true); err != nil { if err := StorageService.DeleteStorageById(c, userId, true); err != nil {
return err
}
// 删除用户
if err := repository.UserRepository.DeleteById(c, userId); err != nil {
return err return err
} }
return nil return nil
}) })
if err != nil {
return err
}
for _, token := range loginTokens {
service.Logout(token)
}
return nil
} }
func (service userService) DeleteLoginLogs(tokens []string) error { func (service userService) DeleteLoginLogs(tokens []string) error {
if len(tokens) > 0 { if len(tokens) > 0 {
for _, token := range tokens { for _, token := range tokens {
// 手动触发用户退出登录
if err := service.LogoutByToken(token); err != nil { if err := service.LogoutByToken(token); err != nil {
return err return err
} }
// 移除缓存中的token
service.Logout(token)
// 删除登录日志
if err := repository.LoginLogRepository.DeleteById(context.TODO(), token); err != nil { if err := repository.LoginLogRepository.DeleteById(context.TODO(), token); err != nil {
return err return err
} }
@ -337,3 +371,37 @@ func (service userService) UpdateUser(id string, user model.User) error {
}) })
} }
func (service userService) AddSharerResources(ctx context.Context, userGroupId, userId, strategyId, resourceType string, resourceIds []string) error {
if service.InTransaction(ctx) {
return service.addSharerResources(ctx, resourceIds, userGroupId, userId, strategyId, resourceType)
} else {
return env.GetDB().Transaction(func(tx *gorm.DB) error {
ctx2 := service.Context(tx)
return service.addSharerResources(ctx2, resourceIds, userGroupId, userId, strategyId, resourceType)
})
}
}
func (service userService) addSharerResources(ctx context.Context, resourceIds []string, userGroupId string, userId string, strategyId string, resourceType string) error {
for i := range resourceIds {
resourceId := resourceIds[i]
// 保证同一个资产只能分配给一个用户或者组
id := utils.Sign([]string{resourceId, resourceType, userId, userGroupId})
if err := repository.ResourceSharerRepository.DeleteById(ctx, id); err != nil {
return err
}
rs := &model.ResourceSharer{
ID: id,
ResourceId: resourceId,
ResourceType: resourceType,
StrategyId: strategyId,
UserId: userId,
UserGroupId: userGroupId,
}
if err := repository.ResourceSharerRepository.AddSharerResource(ctx, rs); err != nil {
return err
}
}
return nil
}

View File

@ -13,11 +13,12 @@ import (
) )
type userGroupService struct { type userGroupService struct {
baseService
} }
func (service userGroupService) DeleteById(userGroupId string) error { func (service userGroupService) DeleteById(userGroupId string) error {
return env.GetDB().Transaction(func(tx *gorm.DB) error { return env.GetDB().Transaction(func(tx *gorm.DB) error {
c := context.WithValue(context.TODO(), constant.DB, tx) c := service.Context(tx)
// 删除用户组 // 删除用户组
if err := repository.UserGroupRepository.DeleteById(c, userGroupId); err != nil { if err := repository.UserGroupRepository.DeleteById(c, userGroupId); err != nil {
return err return err
@ -34,8 +35,8 @@ func (service userGroupService) DeleteById(userGroupId string) error {
}) })
} }
func (service userGroupService) Create(name string, members []string) (model.UserGroup, error) { func (service userGroupService) Create(ctx context.Context, name string, members []string) (model.UserGroup, error) {
exist, err := repository.UserGroupRepository.ExistByName(context.TODO(), name) exist, err := repository.UserGroupRepository.ExistByName(ctx, name)
if err != nil { if err != nil {
return model.UserGroup{}, err return model.UserGroup{}, err
} }
@ -51,26 +52,33 @@ func (service userGroupService) Create(name string, members []string) (model.Use
Name: name, Name: name,
} }
return userGroup, env.GetDB().Transaction(func(tx *gorm.DB) error { if service.InTransaction(ctx) {
c := context.WithValue(context.TODO(), constant.DB, tx) return userGroup, service.create(ctx, userGroup, members, userGroupId)
if err := repository.UserGroupRepository.Create(c, &userGroup); err != nil { } else {
return err return userGroup, env.GetDB().Transaction(func(tx *gorm.DB) error {
} c := service.Context(tx)
if len(members) > 0 { return service.create(c, userGroup, members, userGroupId)
for _, member := range members { })
userGroupMember := model.UserGroupMember{ }
ID: utils.Sign([]string{userGroupId, member}), }
UserId: member,
UserGroupId: userGroupId, func (service userGroupService) create(c context.Context, userGroup model.UserGroup, members []string, userGroupId string) error {
} if err := repository.UserGroupRepository.Create(c, &userGroup); err != nil {
if err := repository.UserGroupMemberRepository.Create(c, &userGroupMember); err != nil { return err
return err }
} if len(members) > 0 {
for _, member := range members {
userGroupMember := model.UserGroupMember{
ID: utils.Sign([]string{userGroupId, member}),
UserId: member,
UserGroupId: userGroupId,
}
if err := repository.UserGroupMemberRepository.Create(c, &userGroupMember); err != nil {
return err
} }
} }
return nil }
}) return nil
} }
func (service userGroupService) Update(userGroupId string, name string, members []string) (err error) { func (service userGroupService) Update(userGroupId string, name string, members []string) (err error) {
@ -91,7 +99,7 @@ func (service userGroupService) Update(userGroupId string, name string, members
} }
return env.GetDB().Transaction(func(tx *gorm.DB) error { return env.GetDB().Transaction(func(tx *gorm.DB) error {
c := context.WithValue(context.TODO(), constant.DB, tx) c := service.Context(tx)
userGroup := model.UserGroup{ userGroup := model.UserGroup{
ID: userGroupId, ID: userGroupId,
Name: name, Name: name,

View File

@ -131,7 +131,7 @@ func (sshd sshd) sessionHandler(sess *ssh.Session) {
func (sshd sshd) Serve() { func (sshd sshd) Serve() {
ssh.Handle(func(s ssh.Session) { ssh.Handle(func(s ssh.Session) {
_, _ = io.WriteString(s, fmt.Sprintf(constant.Banner, constant.Version)) _, _ = io.WriteString(s, fmt.Sprintf(constant.AppBanner, constant.AppVersion))
sshd.sessionHandler(&s) sshd.sessionHandler(&s)
}) })

View File

@ -59,19 +59,22 @@ func (gui Gui) AssetUI(sess *ssh.Session, user model.User) {
return return
} }
for i := range assets {
assets[i].IP = ""
assets[i].Port = 0
}
quitItem := model.Asset{ID: "quit", Name: "返回上级菜单", Description: "这里是返回上级菜单的选项"} quitItem := model.Asset{ID: "quit", Name: "返回上级菜单", Description: "这里是返回上级菜单的选项"}
assets = append([]model.Asset{quitItem}, assets...) assets = append([]model.Asset{quitItem}, assets...)
templates := &promptui.SelectTemplates{ templates := &promptui.SelectTemplates{
Label: "{{ . }}?", Label: "{{ . }}?",
Active: "\U0001F336 {{ .Name | cyan }} ({{ .IP | red }}:{{ .Port | red }})", Active: "\U0001F336 {{ .Name | cyan }}",
Inactive: " {{ .Name | cyan }} ({{ .IP | red }}:{{ .Port | red }})", Inactive: " {{ .Name | cyan }}",
Selected: "\U0001F336 {{ .Name | red | cyan }}", Selected: "\U0001F336 {{ .Name | red | cyan }}",
Details: ` Details: `
--------- 详细信息 ---------- --------- 详细信息 ----------
{{ "名称:" | faint }} {{ .Name }} {{ "名称:" | faint }} {{ .Name }}
{{ "主机:" | faint }} {{ .IP }}
{{ "端口:" | faint }} {{ .Port }}
{{ "标签:" | faint }} {{ .Tags }} {{ "标签:" | faint }} {{ .Tags }}
{{ "备注:" | faint }} {{ .Description }} {{ "备注:" | faint }} {{ .Description }}
`, `,

View File

@ -18,7 +18,6 @@
"react-router": "^5.2.0", "react-router": "^5.2.0",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-scripts": "^4.0.0", "react-scripts": "^4.0.0",
"react-tsparticles": "^1.37.5",
"xterm": "^4.9.0", "xterm": "^4.9.0",
"xterm-addon-fit": "^0.4.0", "xterm-addon-fit": "^0.4.0",
"xterm-addon-web-links": "^0.4.0" "xterm-addon-web-links": "^0.4.0"

View File

@ -5,8 +5,7 @@ import request from "../common/request";
import {message} from "antd/es"; import {message} from "antd/es";
import {withRouter} from "react-router-dom"; import {withRouter} from "react-router-dom";
import {LockOutlined, OneToOneOutlined, UserOutlined} from '@ant-design/icons'; import {LockOutlined, OneToOneOutlined, UserOutlined} from '@ant-design/icons';
import Particles from "react-tsparticles"; import Background from '../images/bg.jpg'
import Background from '../images/bg.png'
import {setToken} from "../utils/utils"; import {setToken} from "../utils/utils";
const {Title} = Typography; const {Title} = Typography;
@ -109,90 +108,7 @@ class LoginForm extends Component {
render() { render() {
return ( return (
<div className='login-bg' <div className='login-bg'
style={{width: this.state.width, height: this.state.height}}> style={{width: this.state.width, height: this.state.height, background: `url(${Background})`}}>
<Particles
id="tsparticles"
options={{
background: {
color: {
// value: "#0d47a1",
},
image: `url(${Background})`,
repeat: 'no-repeat',
size: '100% 100%'
},
fpsLimit: 60,
interactivity: {
events: {
onClick: {
enable: true,
mode: "push",
},
onHover: {
enable: true,
mode: "repulse",
},
resize: true,
},
modes: {
bubble: {
distance: 400,
duration: 2,
opacity: 0.8,
size: 40,
},
push: {
quantity: 4,
},
repulse: {
distance: 200,
duration: 0.4,
},
},
},
particles: {
color: {
value: "#ffffff",
},
links: {
color: "#ffffff",
distance: 150,
enable: true,
opacity: 0.5,
width: 1,
},
collisions: {
enable: true,
},
move: {
direction: "none",
enable: true,
outMode: "bounce",
random: false,
speed: 3,
straight: false,
},
number: {
density: {
enable: true,
value_area: 800,
},
value: 80,
},
opacity: {
value: 0.5,
},
shape: {
type: "circle",
},
size: {
random: true,
value: 5,
},
},
detectRetina: true,
}}
/>
<Card className='login-card' title={null}> <Card className='login-card' title={null}>
<div style={{textAlign: "center", margin: '15px auto 30px auto', color: '#1890ff'}}> <div style={{textAlign: "center", margin: '15px auto 30px auto', color: '#1890ff'}}>
<Title level={1}>Next Terminal</Title> <Title level={1}>Next Terminal</Title>

View File

@ -127,7 +127,7 @@ class Access extends Component {
sendClipboard(data) { sendClipboard(data) {
if (this.state.session['paste'] === '0') { if (this.state.session['paste'] === '0') {
message.warn('禁止粘贴'); // message.warn('禁止粘贴');
return return
} }
let writer; let writer;
@ -338,7 +338,7 @@ class Access extends Component {
clientClipboardReceived = (stream, mimetype) => { clientClipboardReceived = (stream, mimetype) => {
if (this.state.session['copy'] === '0') { if (this.state.session['copy'] === '0') {
message.warn('禁止复制'); // message.warn('禁止复制');
return return
} }
let reader; let reader;

View File

@ -82,7 +82,9 @@ class Term extends Component {
document.body.oncopy = (event) => { document.body.oncopy = (event) => {
event.preventDefault(); event.preventDefault();
if (this.state.session['copy'] === '0') { if (this.state.session['copy'] === '0') {
message.warn('禁止复制') // message.warn('禁止复制')
return false;
}else {
if (event.clipboardData) { if (event.clipboardData) {
return event.clipboardData.setData('text', ''); return event.clipboardData.setData('text', '');
} else { } else {
@ -90,13 +92,12 @@ class Term extends Component {
return window.clipboardData.setData("text", ''); return window.clipboardData.setData("text", '');
} }
} }
return true;
} }
document.body.onpaste = (event) => { document.body.onpaste = (event) => {
event.preventDefault(); event.preventDefault();
if (this.state.session['paste'] === '0') { if (this.state.session['paste'] === '0') {
message.warn('禁止粘贴') // message.warn('禁止粘贴')
return false; return false;
} }
return true; return true;

View File

@ -412,7 +412,7 @@ Windows需要对远程应用程序的名称使用特殊的符号。
<Panel header={<Text strong>映射网络驱动器</Text>} key="storage"> <Panel header={<Text strong>映射网络驱动器</Text>} key="storage">
<Form.Item <Form.Item
name="enable-drive" name="enable-drive"
label="启用映射网络驱动器" label="启用"
valuePropName="checked" valuePropName="checked"
> >
<Switch checkedChildren="开启" unCheckedChildren="关闭" <Switch checkedChildren="开启" unCheckedChildren="关闭"
@ -424,7 +424,7 @@ Windows需要对远程应用程序的名称使用特殊的符号。
enableDrive ? enableDrive ?
<Form.Item <Form.Item
name="drive-path" name="drive-path"
label="映射网络驱动器" label="映射空间"
tooltip='用于文件传输的映射网络驱动器,为空时使用操作人的默认空间' tooltip='用于文件传输的映射网络驱动器,为空时使用操作人的默认空间'
> >
<Select onChange={null} allowClear placeholder='为空时使用操作人的默认空间'> <Select onChange={null} allowClear placeholder='为空时使用操作人的默认空间'>
@ -456,7 +456,7 @@ Windows需要对远程应用程序的名称使用特殊的符号。
setSshMode(value) setSshMode(value)
}}> }}>
<Option value="">guacd</Option> <Option value="">guacd</Option>
<Option value="native">原生</Option> <Option value="naive">原生</Option>
</Select> </Select>
</Form.Item> </Form.Item>
</Panel> </Panel>

View File

@ -18,7 +18,6 @@ import {
import { import {
CheckCircleOutlined, CheckCircleOutlined,
CodeOutlined, CodeOutlined,
DesktopOutlined,
ExclamationCircleOutlined, ExclamationCircleOutlined,
SyncOutlined, SyncOutlined,
TagsOutlined, TagsOutlined,
@ -195,14 +194,6 @@ class MyAsset extends Component {
style={{width: 200}} style={{width: 200}}
/> />
<Search
ref={this.inputRefOfIp}
placeholder="资产IP"
allowClear
onSearch={this.handleSearchByIp}
style={{width: 200}}
/>
<Select mode="multiple" <Select mode="multiple"
allowClear allowClear
value={this.state.selectedTags} value={this.state.selectedTags}
@ -302,9 +293,6 @@ class MyAsset extends Component {
<Descriptions.Item label={<div><CodeOutlined/> 资产协议</div>}> <Descriptions.Item label={<div><CodeOutlined/> 资产协议</div>}>
<strong>{item['protocol']}</strong> <strong>{item['protocol']}</strong>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label={<div><DesktopOutlined/> 主机地址</div>}>
<strong>{item['ip'] + ':' + item['port']}</strong>
</Descriptions.Item>
<Descriptions.Item label={<div><TagsOutlined/> 标签</div>}> <Descriptions.Item label={<div><TagsOutlined/> 标签</div>}>
<strong>{this.renderTags(item['tags'])}</strong> <strong>{this.renderTags(item['tags'])}</strong>
</Descriptions.Item> </Descriptions.Item>

View File

@ -9,7 +9,7 @@ const formItemLayout = {
const StorageModal = ({title, visible, handleOk, handleCancel, confirmLoading, model}) => { const StorageModal = ({title, visible, handleOk, handleCancel, confirmLoading, model}) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
if(!model){ if (!model) {
model = { model = {
isShare: false isShare: false
} }
@ -46,11 +46,13 @@ const StorageModal = ({title, visible, handleOk, handleCancel, confirmLoading, m
<Input autoComplete="off" placeholder="网盘的名称"/> <Input autoComplete="off" placeholder="网盘的名称"/>
</Form.Item> </Form.Item>
<Form.Item label="是否共享" name='isShare' rules={[{required: true, message: '请选择是否共享'}]} valuePropName="checked"> <Form.Item label="是否共享" name='isShare' rules={[{required: true, message: '请选择是否共享'}]}
<Switch checkedChildren="是" unCheckedChildren="否" /> valuePropName="checked">
<Switch checkedChildren="是" unCheckedChildren="否"/>
</Form.Item> </Form.Item>
<Form.Item label="大小限制" name='limitSize' rules={[{required: true, message: '请输入大小限制'}]}> <Form.Item label="大小限制" name='limitSize' rules={[{required: true, message: '请输入大小限制'}]}
tooltip='无限制请填写-1'>
<Input type={'number'} min={-1} suffix="MB"/> <Input type={'number'} min={-1} suffix="MB"/>
</Form.Item> </Form.Item>

View File

@ -25,7 +25,6 @@ class Setting extends Component {
state = { state = {
refs: [], refs: [],
properties: {}, properties: {},
ldapUserSyncLoading: false
} }
rdpSettingFormRef = React.createRef(); rdpSettingFormRef = React.createRef();
@ -33,8 +32,8 @@ class Setting extends Component {
vncSettingFormRef = React.createRef(); vncSettingFormRef = React.createRef();
guacdSettingFormRef = React.createRef(); guacdSettingFormRef = React.createRef();
mailSettingFormRef = React.createRef(); mailSettingFormRef = React.createRef();
ldapSettingFormRef = React.createRef();
logSettingFormRef = React.createRef(); logSettingFormRef = React.createRef();
otherSettingFormRef = React.createRef();
componentDidMount() { componentDidMount() {
// eslint-disable-next-line no-extend-native // eslint-disable-next-line no-extend-native
@ -43,7 +42,15 @@ class Setting extends Component {
}; };
this.setState({ this.setState({
refs: [this.rdpSettingFormRef, this.sshSettingFormRef, this.vncSettingFormRef, this.guacdSettingFormRef, this.mailSettingFormRef, this.logSettingFormRef] refs: [
this.rdpSettingFormRef,
this.sshSettingFormRef,
this.vncSettingFormRef,
this.guacdSettingFormRef,
this.mailSettingFormRef,
this.logSettingFormRef,
this.otherSettingFormRef
]
}, this.getProperties) }, this.getProperties)
} }
@ -121,26 +128,6 @@ class Setting extends Component {
reader.readAsText(files[0]); reader.readAsText(files[0]);
} }
ldapUserSync = async () => {
const id = 'ldap-user-sync'
try {
this.setState({
ldapUserSyncLoading: true
});
message.info({content: '同步中...', key: id, duration: 5});
let result = await request.post(`/properties/ldap-user-sync`);
if (result.code !== 1) {
message.error({content: result.message, key: id, duration: 10});
return;
}
message.success({content: '同步成功。', key: id, duration: 3});
} finally {
this.setState({
ldapUserSyncLoading: false
});
}
}
render() { render() {
return ( return (
<> <>
@ -551,7 +538,7 @@ class Setting extends Component {
</TabPane> </TabPane>
<TabPane tab="日志配置" key="log"> <TabPane tab="日志配置" key="log">
<Title level={3}>其他配置</Title> <Title level={3}>日志配置</Title>
<Form ref={this.logSettingFormRef} name="log" onFinish={this.changeProperties} <Form ref={this.logSettingFormRef} name="log" onFinish={this.changeProperties}
layout="vertical"> layout="vertical">
@ -593,6 +580,28 @@ class Setting extends Component {
</Form> </Form>
</TabPane> </TabPane>
<TabPane tab="其他配置" key="other">
<Title level={3}>其他配置</Title>
<Form ref={this.otherSettingFormRef} name="other" onFinish={this.changeProperties}
layout="vertical">
<Form.Item
{...formItemLayout}
name="user-default-storage-size"
label="用户空间默认大小"
tooltip='无限制请填写-1'
>
<Input type={'number'} min={-1} suffix="MB"/>
</Form.Item>
<Form.Item {...formTailLayout}>
<Button type="primary" htmlType="submit">
更新
</Button>
</Form.Item>
</Form>
</TabPane>
<TabPane tab="备份与恢复" key="backup"> <TabPane tab="备份与恢复" key="backup">
<Title level={3}>备份与恢复</Title> <Title level={3}>备份与恢复</Title>

View File

@ -280,7 +280,7 @@ class User extends Component {
let result = await request.delete('/users/' + id); let result = await request.delete('/users/' + id);
if (result.code === 1) { if (result.code === 1) {
message.success('操作成功', 3); message.success('操作成功', 3);
await this.loadTableData(this.state.queryParams); this.loadTableData(this.state.queryParams);
} else { } else {
message.error(result.message, 10); message.error(result.message, 10);
} }

View File

@ -425,8 +425,11 @@ class UserShareSelectedAsset extends Component {
<div> <div>
<p>上传{renderStatus(item['strategy']['upload'])}</p> <p>上传{renderStatus(item['strategy']['upload'])}</p>
<p>下载{renderStatus(item['strategy']['download'])}</p> <p>下载{renderStatus(item['strategy']['download'])}</p>
<p>编辑{renderStatus(item['strategy']['edit'])}</p>
<p>删除{renderStatus(item['strategy']['delete'])}</p> <p>删除{renderStatus(item['strategy']['delete'])}</p>
<p>改名{renderStatus(item['strategy']['rename'])}</p> <p>重命名{renderStatus(item['strategy']['rename'])}</p>
<p>复制{renderStatus(item['strategy']['copy'])}</p>
<p>粘贴{renderStatus(item['strategy']['paste'])}</p>
</div> </div>
); );
} }

BIN
web/src/images/bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB