From 37df602ae88a8a8840e7d954f83094390967adc0 Mon Sep 17 00:00:00 2001 From: ssongliu <73214554+ssongliu@users.noreply.github.com> Date: Mon, 19 Aug 2024 18:04:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8F=90=E5=8F=96=E5=88=86=E7=BB=84?= =?UTF-8?q?=E5=8F=8A=E5=BF=AB=E9=80=9F=E5=91=BD=E4=BB=A4=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=20(#6169)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- agent/app/api/v2/backup.go | 15 ++ agent/app/api/v2/entry.go | 4 - agent/app/api/v2/terminal.go | 55 ----- agent/app/api/v2/website.go | 12 + agent/app/dto/common_req.go | 5 + agent/app/dto/monitor.go | 12 + agent/app/model/command.go | 14 -- agent/app/model/group.go | 8 - agent/app/repo/cronjob.go | 6 +- agent/app/repo/ftp.go | 7 + agent/app/repo/host.go | 94 -------- agent/app/repo/website.go | 6 + agent/app/service/backup.go | 43 +++- agent/app/service/command.go | 183 --------------- agent/app/service/entry.go | 2 - agent/app/service/ftp.go | 4 +- agent/app/service/ssh.go | 4 - agent/app/service/website.go | 9 +- agent/init/migration/migrate.go | 2 - agent/init/migration/migrations/init.go | 42 ---- agent/router/backup.go | 24 ++ agent/router/common.go | 3 +- agent/router/ro_host.go | 21 -- agent/router/ro_terminal.go | 16 -- agent/router/ro_website.go | 1 + {agent => core}/app/api/v2/command.go | 121 ++-------- core/app/api/v2/entry.go | 3 + {agent => core}/app/api/v2/group.go | 18 +- {agent => core}/app/api/v2/host.go | 132 +++++++++-- core/app/dto/command.go | 38 +++ core/app/dto/common.go | 4 + {agent => core}/app/dto/group.go | 0 {agent => core}/app/dto/host.go | 12 - core/app/model/backup.go | 5 +- core/app/model/command.go | 9 + core/app/model/group.go | 8 + {agent => core}/app/model/host.go | 0 {agent => core}/app/repo/command.go | 58 +---- core/app/repo/common.go | 20 ++ {agent => core}/app/repo/group.go | 44 +++- core/app/repo/host.go | 113 +++++++++ core/app/service/backup.go | 14 +- core/app/service/command.go | 128 ++++++++++ core/app/service/entry.go | 3 + {agent => core}/app/service/group.go | 66 ++++-- {agent => core}/app/service/host.go | 28 ++- core/constant/errs.go | 1 + core/i18n/lang/zh.yaml | 1 + core/init/migration/migrate.go | 1 + core/init/migration/migrations/init.go | 34 ++- core/router/command.go | 21 ++ core/router/common.go | 3 + {agent => core}/router/ro_group.go | 6 +- core/router/ro_host.go | 25 ++ core/utils/encrypt/encrypt.go | 3 +- core/utils/http/new.go | 69 ++++++ core/utils/terminal/local_cmd.go | 93 ++++++++ core/utils/terminal/ws_local_session.go | 122 ++++++++++ core/utils/terminal/ws_session.go | 218 ++++++++++++++++++ core/utils/xpack/xpack.go | 8 + frontend/src/api/interface/command.ts | 7 +- frontend/src/api/interface/group.ts | 4 +- frontend/src/api/modules/backup.ts | 82 +++++++ frontend/src/api/modules/command.ts | 22 ++ frontend/src/api/modules/group.ts | 10 +- frontend/src/api/modules/host.ts | 84 +------ frontend/src/api/modules/terminal.ts | 51 ++++ frontend/src/assets/iconfont/iconfont.css | 44 +--- frontend/src/assets/iconfont/iconfont.js | 2 +- frontend/src/assets/iconfont/iconfont.json | 63 +---- frontend/src/assets/iconfont/iconfont.svg | 18 +- frontend/src/assets/iconfont/iconfont.ttf | Bin 35252 -> 32980 bytes frontend/src/assets/iconfont/iconfont.woff | Bin 21816 -> 20132 bytes frontend/src/assets/iconfont/iconfont.woff2 | Bin 18856 -> 17308 bytes frontend/src/components/group/change.vue | 2 +- frontend/src/components/group/index.vue | 59 +++-- frontend/src/routers/modules/cronjob.ts | 2 +- frontend/src/routers/modules/host.ts | 11 - frontend/src/routers/modules/log.ts | 2 +- frontend/src/routers/modules/setting.ts | 2 +- frontend/src/routers/modules/terminal.ts | 26 +++ .../views/database/redis/command/index.vue | 47 +++- frontend/src/views/database/redis/index.vue | 4 +- .../{host => }/terminal/command/index.vue | 13 +- .../terminal/host/change-group/index.vue | 4 +- .../views/{host => }/terminal/host/index.vue | 6 +- .../terminal/host/operate/index.vue | 4 +- .../src/views/{host => }/terminal/index.vue | 6 +- .../terminal/terminal/host-create.vue | 2 +- .../{host => }/terminal/terminal/index.vue | 11 +- .../website/config/basic/other/index.vue | 2 +- .../views/website/website/create/index.vue | 2 +- frontend/src/views/website/website/index.vue | 2 +- go.mod | 2 + go.sum | 3 + 95 files changed, 1657 insertions(+), 963 deletions(-) delete mode 100644 agent/app/model/command.go delete mode 100644 agent/app/model/group.go delete mode 100644 agent/app/service/command.go create mode 100644 agent/router/backup.go delete mode 100644 agent/router/ro_terminal.go rename {agent => core}/app/api/v2/command.go (53%) rename {agent => core}/app/api/v2/group.go (88%) rename {agent => core}/app/api/v2/host.go (64%) create mode 100644 core/app/dto/command.go rename {agent => core}/app/dto/group.go (100%) rename {agent => core}/app/dto/host.go (84%) create mode 100644 core/app/model/command.go create mode 100644 core/app/model/group.go rename {agent => core}/app/model/host.go (100%) rename {agent => core}/app/repo/command.go (59%) rename {agent => core}/app/repo/group.go (60%) create mode 100644 core/app/repo/host.go create mode 100644 core/app/service/command.go rename {agent => core}/app/service/group.go (52%) rename {agent => core}/app/service/host.go (90%) create mode 100644 core/router/command.go rename {agent => core}/router/ro_group.go (68%) create mode 100644 core/router/ro_host.go create mode 100644 core/utils/http/new.go create mode 100644 core/utils/terminal/local_cmd.go create mode 100644 core/utils/terminal/ws_local_session.go create mode 100644 core/utils/terminal/ws_session.go create mode 100644 frontend/src/api/modules/backup.ts create mode 100644 frontend/src/api/modules/command.ts create mode 100644 frontend/src/api/modules/terminal.ts create mode 100644 frontend/src/routers/modules/terminal.ts rename frontend/src/views/{host => }/terminal/command/index.vue (98%) rename frontend/src/views/{host => }/terminal/host/change-group/index.vue (96%) rename frontend/src/views/{host => }/terminal/host/index.vue (97%) rename frontend/src/views/{host => }/terminal/host/operate/index.vue (98%) rename frontend/src/views/{host => }/terminal/index.vue (91%) rename frontend/src/views/{host => }/terminal/terminal/host-create.vue (99%) rename frontend/src/views/{host => }/terminal/terminal/index.vue (97%) diff --git a/agent/app/api/v2/backup.go b/agent/app/api/v2/backup.go index 27255f86d..3b2f1bf63 100644 --- a/agent/app/api/v2/backup.go +++ b/agent/app/api/v2/backup.go @@ -10,6 +10,21 @@ import ( "github.com/gin-gonic/gin" ) +func (b *BaseApi) CheckBackupUsed(c *gin.Context) { + id, err := helper.GetIntParamByKey(c, "id") + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil) + return + } + + if err := backupService.CheckUsed(id); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil) + return + } + + helper.SuccessWithOutData(c) +} + // @Tags Backup Account // @Summary Page backup records // @Description 获取备份记录列表分页 diff --git a/agent/app/api/v2/entry.go b/agent/app/api/v2/entry.go index 294f46cf0..972ca2ca8 100644 --- a/agent/app/api/v2/entry.go +++ b/agent/app/api/v2/entry.go @@ -30,8 +30,6 @@ var ( cronjobService = service.NewICronjobService() - hostService = service.NewIHostService() - groupService = service.NewIGroupService() fileService = service.NewIFileService() sshService = service.NewISSHService() firewallService = service.NewIFirewallService() @@ -45,8 +43,6 @@ var ( settingService = service.NewISettingService() backupService = service.NewIBackupService() - commandService = service.NewICommandService() - websiteService = service.NewIWebsiteService() websiteDnsAccountService = service.NewIWebsiteDnsAccountService() websiteSSLService = service.NewIWebsiteSSLService() diff --git a/agent/app/api/v2/terminal.go b/agent/app/api/v2/terminal.go index 5a461e1fe..b85ff956c 100644 --- a/agent/app/api/v2/terminal.go +++ b/agent/app/api/v2/terminal.go @@ -12,67 +12,12 @@ import ( "github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/utils/cmd" - "github.com/1Panel-dev/1Panel/agent/utils/copier" - "github.com/1Panel-dev/1Panel/agent/utils/ssh" "github.com/1Panel-dev/1Panel/agent/utils/terminal" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" "github.com/pkg/errors" ) -func (b *BaseApi) WsSsh(c *gin.Context) { - wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil) - if err != nil { - global.LOG.Errorf("gin context http handler failed, err: %v", err) - return - } - defer wsConn.Close() - - id, err := strconv.Atoi(c.Query("id")) - if wshandleError(wsConn, errors.WithMessage(err, "invalid param id in request")) { - return - } - cols, err := strconv.Atoi(c.DefaultQuery("cols", "80")) - if wshandleError(wsConn, errors.WithMessage(err, "invalid param cols in request")) { - return - } - rows, err := strconv.Atoi(c.DefaultQuery("rows", "40")) - if wshandleError(wsConn, errors.WithMessage(err, "invalid param rows in request")) { - return - } - host, err := hostService.GetHostInfo(uint(id)) - if wshandleError(wsConn, errors.WithMessage(err, "load host info by id failed")) { - return - } - var connInfo ssh.ConnInfo - _ = copier.Copy(&connInfo, &host) - connInfo.PrivateKey = []byte(host.PrivateKey) - if len(host.PassPhrase) != 0 { - connInfo.PassPhrase = []byte(host.PassPhrase) - } - - client, err := connInfo.NewClient() - if wshandleError(wsConn, errors.WithMessage(err, "failed to set up the connection. Please check the host information")) { - return - } - defer client.Close() - sws, err := terminal.NewLogicSshWsSession(cols, rows, true, connInfo.Client, wsConn) - if wshandleError(wsConn, err) { - return - } - defer sws.Close() - - quitChan := make(chan bool, 3) - sws.Start(quitChan) - go sws.Wait(quitChan) - - <-quitChan - - if wshandleError(wsConn, err) { - return - } -} - func (b *BaseApi) RedisWsSsh(c *gin.Context) { wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil) if err != nil { diff --git a/agent/app/api/v2/website.go b/agent/app/api/v2/website.go index 035cdc38b..084c00197 100644 --- a/agent/app/api/v2/website.go +++ b/agent/app/api/v2/website.go @@ -957,3 +957,15 @@ func (b *BaseApi) UpdateLoadBalanceFile(c *gin.Context) { } helper.SuccessWithOutData(c) } + +func (b *BaseApi) ChangeWebsiteGroup(c *gin.Context) { + var req dto.UpdateGroup + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteService.ChangeGroup(req.Group, req.NewGroup); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} diff --git a/agent/app/dto/common_req.go b/agent/app/dto/common_req.go index b493fac55..a3e68ef34 100644 --- a/agent/app/dto/common_req.go +++ b/agent/app/dto/common_req.go @@ -52,3 +52,8 @@ type OperationWithNameAndType struct { Name string `json:"name"` Type string `json:"type" validate:"required"` } + +type UpdateGroup struct { + Group uint `json:"group"` + NewGroup uint `json:"newGroup"` +} diff --git a/agent/app/dto/monitor.go b/agent/app/dto/monitor.go index 780b1c790..fa9baf56d 100644 --- a/agent/app/dto/monitor.go +++ b/agent/app/dto/monitor.go @@ -14,3 +14,15 @@ type MonitorData struct { Date []time.Time `json:"date"` Value []interface{} `json:"value"` } + +type MonitorSetting struct { + MonitorStatus string `json:"monitorStatus"` + MonitorStoreDays string `json:"monitorStoreDays"` + MonitorInterval string `json:"monitorInterval"` + DefaultNetwork string `json:"defaultNetwork"` +} + +type MonitorSettingUpdate struct { + Key string `json:"key" validate:"required,oneof=MonitorStatus MonitorStoreDays MonitorInterval DefaultNetwork"` + Value string `json:"value"` +} diff --git a/agent/app/model/command.go b/agent/app/model/command.go deleted file mode 100644 index 2e72e714d..000000000 --- a/agent/app/model/command.go +++ /dev/null @@ -1,14 +0,0 @@ -package model - -type Command struct { - BaseModel - Name string `gorm:"unique;not null" json:"name"` - GroupID uint `json:"groupID"` - Command string `gorm:"not null" json:"command"` -} - -type RedisCommand struct { - BaseModel - Name string `gorm:"unique;not null" json:"name"` - Command string `gorm:"not null" json:"command"` -} diff --git a/agent/app/model/group.go b/agent/app/model/group.go deleted file mode 100644 index 0a6758531..000000000 --- a/agent/app/model/group.go +++ /dev/null @@ -1,8 +0,0 @@ -package model - -type Group struct { - BaseModel - IsDefault bool `json:"isDefault"` - Name string `gorm:"not null" json:"name"` - Type string `gorm:"not null" json:"type"` -} diff --git a/agent/app/repo/cronjob.go b/agent/app/repo/cronjob.go index d59f4a7fc..f6e9acf2d 100644 --- a/agent/app/repo/cronjob.go +++ b/agent/app/repo/cronjob.go @@ -21,7 +21,7 @@ type ICronjobRepo interface { Create(cronjob *model.Cronjob) error WithByJobID(id int) DBOption WithByDbName(name string) DBOption - WithByDefaultDownload(account string) DBOption + WithByDownloadAccountID(id uint) DBOption WithByRecordDropID(id int) DBOption WithByRecordFile(file string) DBOption Save(id uint, cronjob model.Cronjob) error @@ -124,9 +124,9 @@ func (c *CronjobRepo) WithByDbName(name string) DBOption { } } -func (c *CronjobRepo) WithByDefaultDownload(account string) DBOption { +func (c *CronjobRepo) WithByDownloadAccountID(id uint) DBOption { return func(g *gorm.DB) *gorm.DB { - return g.Where("default_download = ?", account) + return g.Where("download_account_id = ?", id) } } diff --git a/agent/app/repo/ftp.go b/agent/app/repo/ftp.go index 8861b25ee..35e071efe 100644 --- a/agent/app/repo/ftp.go +++ b/agent/app/repo/ftp.go @@ -15,6 +15,7 @@ type IFtpRepo interface { Create(ftp *model.Ftp) error Update(id uint, vars map[string]interface{}) error Delete(opts ...DBOption) error + WithLikeUser(user string) DBOption WithByUser(user string) DBOption } @@ -33,6 +34,12 @@ func (u *FtpRepo) Get(opts ...DBOption) (model.Ftp, error) { } func (h *FtpRepo) WithByUser(user string) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("user = ?", user) + } +} + +func (h *FtpRepo) WithLikeUser(user string) DBOption { return func(g *gorm.DB) *gorm.DB { if len(user) == 0 { return g diff --git a/agent/app/repo/host.go b/agent/app/repo/host.go index eac052ab7..05081136a 100644 --- a/agent/app/repo/host.go +++ b/agent/app/repo/host.go @@ -3,23 +3,11 @@ package repo import ( "github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/global" - "gorm.io/gorm" ) type HostRepo struct{} type IHostRepo interface { - Get(opts ...DBOption) (model.Host, error) - GetList(opts ...DBOption) ([]model.Host, error) - Page(limit, offset int, opts ...DBOption) (int64, []model.Host, error) - WithByInfo(info string) DBOption - WithByPort(port uint) DBOption - WithByUser(user string) DBOption - WithByAddr(addr string) DBOption - Create(host *model.Host) error - Update(id uint, vars map[string]interface{}) error - Delete(opts ...DBOption) error - GetFirewallRecord(opts ...DBOption) (model.Firewall, error) ListFirewallRecord() ([]model.Firewall, error) SaveFirewallRecord(firewall *model.Firewall) error @@ -31,88 +19,6 @@ func NewIHostRepo() IHostRepo { return &HostRepo{} } -func (h *HostRepo) Get(opts ...DBOption) (model.Host, error) { - var host model.Host - db := global.DB - for _, opt := range opts { - db = opt(db) - } - err := db.First(&host).Error - return host, err -} - -func (h *HostRepo) GetList(opts ...DBOption) ([]model.Host, error) { - var hosts []model.Host - db := global.DB.Model(&model.Host{}) - for _, opt := range opts { - db = opt(db) - } - err := db.Find(&hosts).Error - return hosts, err -} - -func (h *HostRepo) Page(page, size int, opts ...DBOption) (int64, []model.Host, error) { - var users []model.Host - db := global.DB.Model(&model.Host{}) - for _, opt := range opts { - db = opt(db) - } - count := int64(0) - db = db.Count(&count) - err := db.Limit(size).Offset(size * (page - 1)).Find(&users).Error - return count, users, err -} - -func (h *HostRepo) WithByInfo(info string) DBOption { - return func(g *gorm.DB) *gorm.DB { - if len(info) == 0 { - return g - } - infoStr := "%" + info + "%" - return g.Where("name LIKE ? OR addr LIKE ?", infoStr, infoStr) - } -} - -func (h *HostRepo) WithByPort(port uint) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("port = ?", port) - } -} -func (h *HostRepo) WithByUser(user string) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("user = ?", user) - } -} -func (h *HostRepo) WithByAddr(addr string) DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("addr = ?", addr) - } -} -func (h *HostRepo) WithByGroup(group string) DBOption { - return func(g *gorm.DB) *gorm.DB { - if len(group) == 0 { - return g - } - return g.Where("group_belong = ?", group) - } -} - -func (h *HostRepo) Create(host *model.Host) error { - return global.DB.Create(host).Error -} - -func (h *HostRepo) Update(id uint, vars map[string]interface{}) error { - return global.DB.Model(&model.Host{}).Where("id = ?", id).Updates(vars).Error -} - -func (h *HostRepo) Delete(opts ...DBOption) error { - db := global.DB - for _, opt := range opts { - db = opt(db) - } - return db.Delete(&model.Host{}).Error -} - func (h *HostRepo) GetFirewallRecord(opts ...DBOption) (model.Firewall, error) { var firewall model.Firewall db := global.DB diff --git a/agent/app/repo/website.go b/agent/app/repo/website.go index d6bc2c47f..98c4417ae 100644 --- a/agent/app/repo/website.go +++ b/agent/app/repo/website.go @@ -31,6 +31,8 @@ type IWebsiteRepo interface { DeleteBy(ctx context.Context, opts ...DBOption) error Create(ctx context.Context, app *model.Website) error DeleteAll(ctx context.Context) error + + UpdateGroup(group, newGroup uint) error } func NewIWebsiteRepo() IWebsiteRepo { @@ -158,3 +160,7 @@ func (w *WebsiteRepo) DeleteBy(ctx context.Context, opts ...DBOption) error { func (w *WebsiteRepo) DeleteAll(ctx context.Context) error { return getTx(ctx).Where("1 = 1 ").Delete(&model.Website{}).Error } + +func (w *WebsiteRepo) UpdateGroup(group, newGroup uint) error { + return global.DB.Model(&model.Website{}).Where("website_group_id = ?", group).Updates(map[string]interface{}{"website_group_id": newGroup}).Error +} diff --git a/agent/app/service/backup.go b/agent/app/service/backup.go index d89368542..4577a6c71 100644 --- a/agent/app/service/backup.go +++ b/agent/app/service/backup.go @@ -3,17 +3,20 @@ package service import ( "bytes" "context" + "encoding/base64" "encoding/json" "fmt" "net/http" "os" "path" "sort" + "strconv" "strings" "sync" "github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/utils/cloud_storage" @@ -26,6 +29,8 @@ import ( type BackupService struct{} type IBackupService interface { + CheckUsed(id uint) error + SearchRecordsWithPage(search dto.RecordSearch) (int64, []dto.BackupRecords, error) SearchRecordsByCronjobWithPage(search dto.RecordSearchByCronjob) (int64, []dto.BackupRecords, error) DownloadRecord(info dto.DownloadRecord) (string, error) @@ -97,6 +102,22 @@ func (u *BackupService) SearchRecordsByCronjobWithPage(search dto.RecordSearchBy return total, datas, err } +func (u *BackupService) CheckUsed(id uint) error { + cronjobs, _ := cronjobRepo.List() + for _, job := range cronjobs { + if job.DownloadAccountID == id { + return buserr.New(constant.ErrBackupInUsed) + } + ids := strings.Split(job.SourceAccountIDs, ",") + for _, idItem := range ids { + if idItem == fmt.Sprintf("%v", id) { + return buserr.New(constant.ErrBackupInUsed) + } + } + } + return nil +} + type loadSizeHelper struct { isOk bool backupPath string @@ -309,7 +330,15 @@ func NewBackupClientMap(ids []string) (map[string]backupClientHelper, error) { accounts[i].Credential, _ = encrypt.StringDecryptWithKey(accounts[i].Credential, setting.Value) } } else { - bodyItem, err := json.Marshal(ids) + var idItems []uint + for i := 0; i < len(ids); i++ { + item, _ := strconv.Atoi(ids[i]) + idItems = append(idItems, uint(item)) + } + operateByIDs := struct { + IDs []uint `json:"ids"` + }{IDs: idItems} + bodyItem, err := json.Marshal(operateByIDs) if err != nil { return nil, err } @@ -327,6 +356,18 @@ func NewBackupClientMap(ids []string) (map[string]backupClientHelper, error) { } clientMap := make(map[string]backupClientHelper) for _, item := range accounts { + if !global.IsMaster { + accessItem, err := base64.StdEncoding.DecodeString(item.AccessKey) + if err != nil { + return nil, err + } + item.AccessKey = string(accessItem) + secretItem, err := base64.StdEncoding.DecodeString(item.Credential) + if err != nil { + return nil, err + } + item.Credential = string(secretItem) + } backClient, err := newClient(&item) if err != nil { return nil, err diff --git a/agent/app/service/command.go b/agent/app/service/command.go deleted file mode 100644 index 9cbca0777..000000000 --- a/agent/app/service/command.go +++ /dev/null @@ -1,183 +0,0 @@ -package service - -import ( - "github.com/1Panel-dev/1Panel/agent/app/dto" - "github.com/1Panel-dev/1Panel/agent/app/model" - "github.com/1Panel-dev/1Panel/agent/constant" - "github.com/jinzhu/copier" - "github.com/pkg/errors" -) - -type CommandService struct{} - -type ICommandService interface { - List() ([]dto.CommandInfo, error) - SearchForTree() ([]dto.CommandTree, error) - SearchWithPage(search dto.SearchCommandWithPage) (int64, interface{}, error) - Create(commandDto dto.CommandOperate) error - Update(id uint, upMap map[string]interface{}) error - Delete(ids []uint) error - - SearchRedisCommandWithPage(search dto.SearchWithPage) (int64, interface{}, error) - ListRedisCommand() ([]dto.RedisCommand, error) - SaveRedisCommand(commandDto dto.RedisCommand) error - DeleteRedisCommand(ids []uint) error -} - -func NewICommandService() ICommandService { - return &CommandService{} -} - -func (u *CommandService) List() ([]dto.CommandInfo, error) { - commands, err := commandRepo.GetList(commonRepo.WithOrderBy("name")) - if err != nil { - return nil, constant.ErrRecordNotFound - } - var dtoCommands []dto.CommandInfo - for _, command := range commands { - var item dto.CommandInfo - if err := copier.Copy(&item, &command); err != nil { - return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - dtoCommands = append(dtoCommands, item) - } - return dtoCommands, err -} - -func (u *CommandService) SearchForTree() ([]dto.CommandTree, error) { - cmdList, err := commandRepo.GetList(commonRepo.WithOrderBy("name")) - if err != nil { - return nil, err - } - groups, err := groupRepo.GetList(commonRepo.WithByType("command")) - if err != nil { - return nil, err - } - var lists []dto.CommandTree - for _, group := range groups { - var data dto.CommandTree - data.ID = group.ID + 10000 - data.Label = group.Name - for _, cmd := range cmdList { - if cmd.GroupID == group.ID { - data.Children = append(data.Children, dto.CommandInfo{ID: cmd.ID, Name: cmd.Name, Command: cmd.Command}) - } - } - if len(data.Children) != 0 { - lists = append(lists, data) - } - } - return lists, err -} - -func (u *CommandService) SearchWithPage(search dto.SearchCommandWithPage) (int64, interface{}, error) { - total, commands, err := commandRepo.Page(search.Page, search.PageSize, commandRepo.WithLikeName(search.Name), commonRepo.WithLikeName(search.Info), commonRepo.WithByGroupID(search.GroupID), commonRepo.WithOrderRuleBy(search.OrderBy, search.Order)) - if err != nil { - return 0, nil, err - } - groups, _ := groupRepo.GetList(commonRepo.WithByType("command"), commonRepo.WithOrderBy("name")) - var dtoCommands []dto.CommandInfo - for _, command := range commands { - var item dto.CommandInfo - if err := copier.Copy(&item, &command); err != nil { - return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - for _, group := range groups { - if command.GroupID == group.ID { - item.GroupBelong = group.Name - item.GroupID = group.ID - } - } - dtoCommands = append(dtoCommands, item) - } - return total, dtoCommands, err -} - -func (u *CommandService) Create(commandDto dto.CommandOperate) error { - command, _ := commandRepo.Get(commonRepo.WithByName(commandDto.Name)) - if command.ID != 0 { - return constant.ErrRecordExist - } - if err := copier.Copy(&command, &commandDto); err != nil { - return errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - if err := commandRepo.Create(&command); err != nil { - return err - } - return nil -} - -func (u *CommandService) Delete(ids []uint) error { - if len(ids) == 1 { - command, _ := commandRepo.Get(commonRepo.WithByID(ids[0])) - if command.ID == 0 { - return constant.ErrRecordNotFound - } - return commandRepo.Delete(commonRepo.WithByID(ids[0])) - } - return commandRepo.Delete(commonRepo.WithIdsIn(ids)) -} - -func (u *CommandService) Update(id uint, upMap map[string]interface{}) error { - return commandRepo.Update(id, upMap) -} - -func (u *CommandService) SearchRedisCommandWithPage(search dto.SearchWithPage) (int64, interface{}, error) { - total, commands, err := commandRepo.PageRedis(search.Page, search.PageSize, commandRepo.WithLikeName(search.Info)) - if err != nil { - return 0, nil, err - } - var dtoCommands []dto.RedisCommand - for _, command := range commands { - var item dto.RedisCommand - if err := copier.Copy(&item, &command); err != nil { - return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - dtoCommands = append(dtoCommands, item) - } - return total, dtoCommands, err -} - -func (u *CommandService) ListRedisCommand() ([]dto.RedisCommand, error) { - commands, err := commandRepo.GetRedisList() - if err != nil { - return nil, constant.ErrRecordNotFound - } - var dtoCommands []dto.RedisCommand - for _, command := range commands { - var item dto.RedisCommand - if err := copier.Copy(&item, &command); err != nil { - return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - dtoCommands = append(dtoCommands, item) - } - return dtoCommands, err -} - -func (u *CommandService) SaveRedisCommand(req dto.RedisCommand) error { - if req.ID == 0 { - command, _ := commandRepo.GetRedis(commonRepo.WithByName(req.Name)) - if command.ID != 0 { - return constant.ErrRecordExist - } - } - var command model.RedisCommand - if err := copier.Copy(&command, &req); err != nil { - return errors.WithMessage(constant.ErrStructTransform, err.Error()) - } - if err := commandRepo.SaveRedis(&command); err != nil { - return err - } - return nil -} - -func (u *CommandService) DeleteRedisCommand(ids []uint) error { - if len(ids) == 1 { - command, _ := commandRepo.GetRedis(commonRepo.WithByID(ids[0])) - if command.ID == 0 { - return constant.ErrRecordNotFound - } - return commandRepo.DeleteRedis(commonRepo.WithByID(ids[0])) - } - return commandRepo.DeleteRedis(commonRepo.WithIdsIn(ids)) -} diff --git a/agent/app/service/entry.go b/agent/app/service/entry.go index 8864347fa..01976eaae 100644 --- a/agent/app/service/entry.go +++ b/agent/app/service/entry.go @@ -22,8 +22,6 @@ var ( cronjobRepo = repo.NewICronjobRepo() hostRepo = repo.NewIHostRepo() - groupRepo = repo.NewIGroupRepo() - commandRepo = repo.NewICommandRepo() ftpRepo = repo.NewIFtpRepo() clamRepo = repo.NewIClamRepo() monitorRepo = repo.NewIMonitorRepo() diff --git a/agent/app/service/ftp.go b/agent/app/service/ftp.go index d0dfef158..a7440873c 100644 --- a/agent/app/service/ftp.go +++ b/agent/app/service/ftp.go @@ -74,7 +74,7 @@ func (u *FtpService) Operate(operation string) error { } func (f *FtpService) SearchWithPage(req dto.SearchWithPage) (int64, interface{}, error) { - total, lists, err := ftpRepo.Page(req.Page, req.PageSize, ftpRepo.WithByUser(req.Info), commonRepo.WithOrderBy("created_at desc")) + total, lists, err := ftpRepo.Page(req.Page, req.PageSize, ftpRepo.WithLikeUser(req.Info), commonRepo.WithOrderBy("created_at desc")) if err != nil { return 0, nil, err } @@ -142,7 +142,7 @@ func (f *FtpService) Create(req dto.FtpCreate) (uint, error) { if err != nil { return 0, err } - userInDB, _ := ftpRepo.Get(hostRepo.WithByUser(req.User)) + userInDB, _ := ftpRepo.Get(ftpRepo.WithByUser(req.User)) if userInDB.ID != 0 { return 0, constant.ErrRecordExist } diff --git a/agent/app/service/ssh.go b/agent/app/service/ssh.go index 46e911119..b9a887047 100644 --- a/agent/app/service/ssh.go +++ b/agent/app/service/ssh.go @@ -181,10 +181,6 @@ func (u *SSHService) Update(req dto.SSHUpdate) error { if err := NewIFirewallService().UpdatePortRule(ruleItem); err != nil { global.LOG.Errorf("reset firewall rules %s -> %s failed, err: %v", req.OldValue, req.NewValue, err) } - - if err = NewIHostService().Update(1, map[string]interface{}{"port": req.NewValue}); err != nil { - global.LOG.Errorf("reset host port %s -> %s failed, err: %v", req.OldValue, req.NewValue, err) - } } _, _ = cmd.Execf("%s systemctl restart %s", sudo, serviceName) diff --git a/agent/app/service/website.go b/agent/app/service/website.go index c82162354..68e4dfeb8 100644 --- a/agent/app/service/website.go +++ b/agent/app/service/website.go @@ -10,7 +10,6 @@ import ( "encoding/pem" "errors" "fmt" - "github.com/1Panel-dev/1Panel/agent/app/task" "os" "path" "reflect" @@ -20,6 +19,8 @@ import ( "syscall" "time" + "github.com/1Panel-dev/1Panel/agent/app/task" + "github.com/1Panel-dev/1Panel/agent/utils/common" "github.com/jinzhu/copier" @@ -114,6 +115,8 @@ type IWebsiteService interface { DeleteLoadBalance(req request.WebsiteLBDelete) error UpdateLoadBalance(req request.WebsiteLBUpdate) error UpdateLoadBalanceFile(req request.WebsiteLBUpdateFile) error + + ChangeGroup(group, newGroup uint) error } func NewIWebsiteService() IWebsiteService { @@ -3193,3 +3196,7 @@ func (w WebsiteService) UpdateLoadBalanceFile(req request.WebsiteLBUpdateFile) e }() return opNginx(nginxInstall.ContainerName, constant.NginxReload) } + +func (w WebsiteService) ChangeGroup(group, newGroup uint) error { + return websiteRepo.UpdateGroup(group, newGroup) +} diff --git a/agent/init/migration/migrate.go b/agent/init/migration/migrate.go index c28190410..2f6567f67 100644 --- a/agent/init/migration/migrate.go +++ b/agent/init/migration/migrate.go @@ -11,10 +11,8 @@ func Init() { m := gormigrate.New(global.DB, gormigrate.DefaultOptions, []*gormigrate.Migration{ migrations.AddTable, migrations.AddMonitorTable, - migrations.InitHost, migrations.InitSetting, migrations.InitImageRepo, - migrations.InitDefaultGroup, migrations.InitDefaultCA, migrations.InitPHPExtensions, migrations.AddTask, diff --git a/agent/init/migration/migrations/init.go b/agent/init/migration/migrations/init.go index 2c4ea07d0..e17ff08f8 100644 --- a/agent/init/migration/migrations/init.go +++ b/agent/init/migration/migrations/init.go @@ -28,7 +28,6 @@ var AddTable = &gormigrate.Migration{ &model.App{}, &model.BackupRecord{}, &model.Clam{}, - &model.Command{}, &model.ComposeTemplate{}, &model.Compose{}, &model.Cronjob{}, @@ -39,15 +38,12 @@ var AddTable = &gormigrate.Migration{ &model.Forward{}, &model.Firewall{}, &model.Ftp{}, - &model.Group{}, - &model.Host{}, &model.ImageRepo{}, &model.JobRecords{}, &model.MonitorBase{}, &model.MonitorIO{}, &model.MonitorNetwork{}, &model.PHPExtensions{}, - &model.RedisCommand{}, &model.Runtime{}, &model.Setting{}, &model.Snapshot{}, @@ -74,25 +70,6 @@ var AddMonitorTable = &gormigrate.Migration{ }, } -var InitHost = &gormigrate.Migration{ - ID: "20240722-init-host", - Migrate: func(tx *gorm.DB) error { - group := model.Group{ - Name: "default", Type: "host", IsDefault: true, - } - if err := tx.Create(&group).Error; err != nil { - return err - } - host := model.Host{ - Name: "localhost", Addr: "127.0.0.1", User: "root", Port: 22, AuthMode: "password", GroupID: group.ID, - } - if err := tx.Create(&host).Error; err != nil { - return err - } - return nil - }, -} - var InitSetting = &gormigrate.Migration{ ID: "20240722-init-setting", Migrate: func(tx *gorm.DB) error { @@ -228,25 +205,6 @@ var InitImageRepo = &gormigrate.Migration{ }, } -var InitDefaultGroup = &gormigrate.Migration{ - ID: "20240722-init-default-group", - Migrate: func(tx *gorm.DB) error { - websiteGroup := &model.Group{ - Name: "默认", - IsDefault: true, - Type: "website", - } - if err := tx.Create(websiteGroup).Error; err != nil { - return err - } - commandGroup := &model.Group{IsDefault: true, Name: "默认", Type: "command"} - if err := tx.Create(commandGroup).Error; err != nil { - return err - } - return nil - }, -} - var InitDefaultCA = &gormigrate.Migration{ ID: "20240722-init-default-ca", Migrate: func(tx *gorm.DB) error { diff --git a/agent/router/backup.go b/agent/router/backup.go new file mode 100644 index 000000000..c951f3380 --- /dev/null +++ b/agent/router/backup.go @@ -0,0 +1,24 @@ +package router + +import ( + v2 "github.com/1Panel-dev/1Panel/agent/app/api/v2" + "github.com/gin-gonic/gin" +) + +type BackupRouter struct{} + +func (s *BackupRouter) InitRouter(Router *gin.RouterGroup) { + backupRouter := Router.Group("backups") + baseApi := v2.ApiGroupApp.BaseApi + { + backupRouter.GET("/check/:id", baseApi.CheckBackupUsed) + backupRouter.POST("/backup", baseApi.Backup) + backupRouter.POST("/recover", baseApi.Recover) + backupRouter.POST("/recover/byupload", baseApi.RecoverByUpload) + backupRouter.POST("/search/files", baseApi.LoadFilesFromBackup) + backupRouter.POST("/record/search", baseApi.SearchBackupRecords) + backupRouter.POST("/record/search/bycronjob", baseApi.SearchBackupRecordsByCronjob) + backupRouter.POST("/record/download", baseApi.DownloadRecord) + backupRouter.POST("/record/del", baseApi.DeleteBackupRecord) + } +} diff --git a/agent/router/common.go b/agent/router/common.go index b96153767..f54a07ddc 100644 --- a/agent/router/common.go +++ b/agent/router/common.go @@ -8,12 +8,11 @@ func commonGroups() []CommonRouter { &LogRouter{}, &FileRouter{}, &ToolboxRouter{}, - &TerminalRouter{}, &CronjobRouter{}, + &BackupRouter{}, &SettingRouter{}, &AppRouter{}, &WebsiteRouter{}, - &WebsiteGroupRouter{}, &WebsiteDnsAccountRouter{}, &WebsiteAcmeAccountRouter{}, &WebsiteSSLRouter{}, diff --git a/agent/router/ro_host.go b/agent/router/ro_host.go index 15990d97a..4e599c716 100644 --- a/agent/router/ro_host.go +++ b/agent/router/ro_host.go @@ -11,15 +11,6 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) { hostRouter := Router.Group("hosts") baseApi := v2.ApiGroupApp.BaseApi { - hostRouter.POST("", baseApi.CreateHost) - hostRouter.POST("/del", baseApi.DeleteHost) - hostRouter.POST("/update", baseApi.UpdateHost) - hostRouter.POST("/update/group", baseApi.UpdateHostGroup) - hostRouter.POST("/search", baseApi.SearchHost) - hostRouter.POST("/tree", baseApi.HostTree) - hostRouter.POST("/test/byinfo", baseApi.TestByInfo) - hostRouter.POST("/test/byid/:id", baseApi.TestByID) - hostRouter.GET("/firewall/base", baseApi.LoadFirewallBaseInfo) hostRouter.POST("/firewall/search", baseApi.SearchFirewallRule) hostRouter.POST("/firewall/operate", baseApi.OperateFirewall) @@ -47,18 +38,6 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) { hostRouter.POST("/ssh/conffile/update", baseApi.UpdateSSHByfile) hostRouter.POST("/ssh/operate", baseApi.OperateSSH) - hostRouter.GET("/command", baseApi.ListCommand) - hostRouter.POST("/command", baseApi.CreateCommand) - hostRouter.POST("/command/del", baseApi.DeleteCommand) - hostRouter.POST("/command/search", baseApi.SearchCommand) - hostRouter.GET("/command/tree", baseApi.SearchCommandTree) - hostRouter.POST("/command/update", baseApi.UpdateCommand) - - hostRouter.GET("/command/redis", baseApi.ListRedisCommand) - hostRouter.POST("/command/redis", baseApi.SaveRedisCommand) - hostRouter.POST("/command/redis/search", baseApi.SearchRedisCommand) - hostRouter.POST("/command/redis/del", baseApi.DeleteRedisCommand) - hostRouter.POST("/tool", baseApi.GetToolStatus) hostRouter.POST("/tool/init", baseApi.InitToolConfig) hostRouter.POST("/tool/operate", baseApi.OperateTool) diff --git a/agent/router/ro_terminal.go b/agent/router/ro_terminal.go deleted file mode 100644 index ec17a3146..000000000 --- a/agent/router/ro_terminal.go +++ /dev/null @@ -1,16 +0,0 @@ -package router - -import ( - v2 "github.com/1Panel-dev/1Panel/agent/app/api/v2" - "github.com/gin-gonic/gin" -) - -type TerminalRouter struct{} - -func (s *TerminalRouter) InitRouter(Router *gin.RouterGroup) { - terminalRouter := Router.Group("terminals") - baseApi := v2.ApiGroupApp.BaseApi - { - terminalRouter.GET("", baseApi.WsSsh) - } -} diff --git a/agent/router/ro_website.go b/agent/router/ro_website.go index 35050c89f..ff7b5af6c 100644 --- a/agent/router/ro_website.go +++ b/agent/router/ro_website.go @@ -24,6 +24,7 @@ func (a *WebsiteRouter) InitRouter(Router *gin.RouterGroup) { websiteRouter.GET("/:id", baseApi.GetWebsite) websiteRouter.POST("/del", baseApi.DeleteWebsite) websiteRouter.POST("/default/server", baseApi.ChangeDefaultServer) + websiteRouter.POST("/group/change", baseApi.ChangeWebsiteGroup) websiteRouter.GET("/domains/:websiteId", baseApi.GetWebDomains) websiteRouter.POST("/domains/del", baseApi.DeleteWebDomain) diff --git a/agent/app/api/v2/command.go b/core/app/api/v2/command.go similarity index 53% rename from agent/app/api/v2/command.go rename to core/app/api/v2/command.go index b9cb437b1..54c1b848e 100644 --- a/agent/app/api/v2/command.go +++ b/core/app/api/v2/command.go @@ -1,9 +1,9 @@ package v2 import ( - "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" - "github.com/1Panel-dev/1Panel/agent/app/dto" - "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/core/app/api/v2/helper" + "github.com/1Panel-dev/1Panel/core/app/dto" + "github.com/1Panel-dev/1Panel/core/constant" "github.com/gin-gonic/gin" ) @@ -14,7 +14,7 @@ import ( // @Param request body dto.CommandOperate true "request" // @Success 200 // @Security ApiKeyAuth -// @Router /hosts/command [post] +// @Router /core/commands [post] // @x-panel-log {"bodyKeys":["name","command"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建快捷命令 [name][command]","formatEN":"create quick command [name][command]"} func (b *BaseApi) CreateCommand(c *gin.Context) { var req dto.CommandOperate @@ -29,28 +29,6 @@ func (b *BaseApi) CreateCommand(c *gin.Context) { helper.SuccessWithData(c, nil) } -// @Tags Redis Command -// @Summary Save redis command -// @Description 保存 Redis 快速命令 -// @Accept json -// @Param request body dto.RedisCommand true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /hosts/command/redis [post] -// @x-panel-log {"bodyKeys":["name","command"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"保存 redis 快捷命令 [name][command]","formatEN":"save quick command for redis [name][command]"} -func (b *BaseApi) SaveRedisCommand(c *gin.Context) { - var req dto.RedisCommand - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := commandService.SaveRedisCommand(req); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - // @Tags Command // @Summary Page commands // @Description 获取快速命令列表分页 @@ -58,7 +36,7 @@ func (b *BaseApi) SaveRedisCommand(c *gin.Context) { // @Param request body dto.SearchWithPage true "request" // @Success 200 {object} dto.PageResult // @Security ApiKeyAuth -// @Router /hosts/command/search [post] +// @Router /core/commands/search [post] func (b *BaseApi) SearchCommand(c *gin.Context) { var req dto.SearchCommandWithPage if err := helper.CheckBindAndValidate(&req, c); err != nil { @@ -77,57 +55,21 @@ func (b *BaseApi) SearchCommand(c *gin.Context) { }) } -// @Tags Redis Command -// @Summary Page redis commands -// @Description 获取 redis 快速命令列表分页 -// @Accept json -// @Param request body dto.SearchWithPage true "request" -// @Success 200 {object} dto.PageResult -// @Security ApiKeyAuth -// @Router /hosts/command/redis/search [post] -func (b *BaseApi) SearchRedisCommand(c *gin.Context) { - var req dto.SearchWithPage - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - total, list, err := commandService.SearchRedisCommandWithPage(req) - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - - helper.SuccessWithData(c, dto.PageResult{ - Items: list, - Total: total, - }) -} - // @Tags Command // @Summary Tree commands // @Description 获取快速命令树 // @Accept json +// @Param request body dto.OperateByType true "request" // @Success 200 {Array} dto.CommandTree // @Security ApiKeyAuth -// @Router /hosts/command/tree [get] +// @Router /core/commands/tree [get] func (b *BaseApi) SearchCommandTree(c *gin.Context) { - list, err := commandService.SearchForTree() - if err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + var req dto.OperateByType + if err := helper.CheckBindAndValidate(&req, c); err != nil { return } - helper.SuccessWithData(c, list) -} - -// @Tags Redis Command -// @Summary List redis commands -// @Description 获取 redis 快速命令列表 -// @Success 200 {Array} dto.RedisCommand -// @Security ApiKeyAuth -// @Router /hosts/command/redis [get] -func (b *BaseApi) ListRedisCommand(c *gin.Context) { - list, err := commandService.ListRedisCommand() + list, err := commandService.SearchForTree(req) if err != nil { helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) return @@ -139,11 +81,18 @@ func (b *BaseApi) ListRedisCommand(c *gin.Context) { // @Tags Command // @Summary List commands // @Description 获取快速命令列表 +// @Accept json +// @Param request body dto.OperateByType true "request" // @Success 200 {object} dto.CommandInfo // @Security ApiKeyAuth -// @Router /hosts/command [get] +// @Router /core/commands/command [get] func (b *BaseApi) ListCommand(c *gin.Context) { - list, err := commandService.List() + var req dto.OperateByType + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + list, err := commandService.List(req) if err != nil { helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) return @@ -156,40 +105,18 @@ func (b *BaseApi) ListCommand(c *gin.Context) { // @Summary Delete command // @Description 删除快速命令 // @Accept json -// @Param request body dto.BatchDeleteReq true "request" +// @Param request body dto.OperateByIDs true "request" // @Success 200 // @Security ApiKeyAuth -// @Router /hosts/command/del [post] +// @Router /core/commands/del [post] // @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"commands","output_column":"name","output_value":"names"}],"formatZH":"删除快捷命令 [names]","formatEN":"delete quick command [names]"} func (b *BaseApi) DeleteCommand(c *gin.Context) { - var req dto.BatchDeleteReq + var req dto.OperateByIDs if err := helper.CheckBindAndValidate(&req, c); err != nil { return } - if err := commandService.Delete(req.Ids); err != nil { - helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) - return - } - helper.SuccessWithData(c, nil) -} - -// @Tags Redis Command -// @Summary Delete redis command -// @Description 删除 redis 快速命令 -// @Accept json -// @Param request body dto.BatchDeleteReq true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Router /hosts/command/redis/del [post] -// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"redis_commands","output_column":"name","output_value":"names"}],"formatZH":"删除 redis 快捷命令 [names]","formatEN":"delete quick command of redis [names]"} -func (b *BaseApi) DeleteRedisCommand(c *gin.Context) { - var req dto.BatchDeleteReq - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := commandService.DeleteRedisCommand(req.Ids); err != nil { + if err := commandService.Delete(req.IDs); err != nil { helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) return } @@ -203,7 +130,7 @@ func (b *BaseApi) DeleteRedisCommand(c *gin.Context) { // @Param request body dto.CommandOperate true "request" // @Success 200 // @Security ApiKeyAuth -// @Router /hosts/command/update [post] +// @Router /core/commands/update [post] // @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新快捷命令 [name]","formatEN":"update quick command [name]"} func (b *BaseApi) UpdateCommand(c *gin.Context) { var req dto.CommandOperate diff --git a/core/app/api/v2/entry.go b/core/app/api/v2/entry.go index b83ba1774..196897634 100644 --- a/core/app/api/v2/entry.go +++ b/core/app/api/v2/entry.go @@ -9,9 +9,12 @@ type ApiGroup struct { var ApiGroupApp = new(ApiGroup) var ( + hostService = service.NewIHostService() authService = service.NewIAuthService() backupService = service.NewIBackupService() settingService = service.NewISettingService() logService = service.NewILogService() upgradeService = service.NewIUpgradeService() + groupService = service.NewIGroupService() + commandService = service.NewICommandService() ) diff --git a/agent/app/api/v2/group.go b/core/app/api/v2/group.go similarity index 88% rename from agent/app/api/v2/group.go rename to core/app/api/v2/group.go index 233a814fa..ef6a56f4f 100644 --- a/agent/app/api/v2/group.go +++ b/core/app/api/v2/group.go @@ -1,9 +1,9 @@ package v2 import ( - "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" - "github.com/1Panel-dev/1Panel/agent/app/dto" - "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/core/app/api/v2/helper" + "github.com/1Panel-dev/1Panel/core/app/dto" + "github.com/1Panel-dev/1Panel/core/constant" "github.com/gin-gonic/gin" ) @@ -14,7 +14,7 @@ import ( // @Param request body dto.GroupCreate true "request" // @Success 200 // @Security ApiKeyAuth -// @Router /groups [post] +// @Router /core/groups [post] // @x-panel-log {"bodyKeys":["name","type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建组 [name][type]","formatEN":"create group [name][type]"} func (b *BaseApi) CreateGroup(c *gin.Context) { var req dto.GroupCreate @@ -36,7 +36,7 @@ func (b *BaseApi) CreateGroup(c *gin.Context) { // @Param request body dto.OperateByID true "request" // @Success 200 // @Security ApiKeyAuth -// @Router /groups/del [post] +// @Router /core/groups/del [post] // @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"groups","output_column":"name","output_value":"name"},{"input_column":"id","input_value":"id","isList":false,"db":"groups","output_column":"type","output_value":"type"}],"formatZH":"删除组 [type][name]","formatEN":"delete group [type][name]"} func (b *BaseApi) DeleteGroup(c *gin.Context) { var req dto.OperateByID @@ -58,7 +58,7 @@ func (b *BaseApi) DeleteGroup(c *gin.Context) { // @Param request body dto.GroupUpdate true "request" // @Success 200 // @Security ApiKeyAuth -// @Router /groups/update [post] +// @Router /core/groups/update [post] // @x-panel-log {"bodyKeys":["name","type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新组 [name][type]","formatEN":"update group [name][type]"} func (b *BaseApi) UpdateGroup(c *gin.Context) { var req dto.GroupUpdate @@ -78,11 +78,11 @@ func (b *BaseApi) UpdateGroup(c *gin.Context) { // @Description 查询系统组 // @Accept json // @Param request body dto.GroupSearch true "request" -// @Success 200 {array} dto.GroupInfo +// @Success 200 {array} dto.OperateByType // @Security ApiKeyAuth -// @Router /groups/search [post] +// @Router /core/groups/search [post] func (b *BaseApi) ListGroup(c *gin.Context) { - var req dto.GroupSearch + var req dto.OperateByType if err := helper.CheckBindAndValidate(&req, c); err != nil { return } diff --git a/agent/app/api/v2/host.go b/core/app/api/v2/host.go similarity index 64% rename from agent/app/api/v2/host.go rename to core/app/api/v2/host.go index 957fb7fc4..0d4c0bd0a 100644 --- a/agent/app/api/v2/host.go +++ b/core/app/api/v2/host.go @@ -1,11 +1,23 @@ package v2 import ( - "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" - "github.com/1Panel-dev/1Panel/agent/app/dto" - "github.com/1Panel-dev/1Panel/agent/constant" - "github.com/1Panel-dev/1Panel/agent/utils/encrypt" + "encoding/base64" + "encoding/json" + "net/http" + "strconv" + "time" + + "github.com/1Panel-dev/1Panel/core/app/api/v2/helper" + "github.com/1Panel-dev/1Panel/core/app/dto" + "github.com/1Panel-dev/1Panel/core/constant" + "github.com/1Panel-dev/1Panel/core/global" + "github.com/1Panel-dev/1Panel/core/utils/copier" + "github.com/1Panel-dev/1Panel/core/utils/encrypt" + "github.com/1Panel-dev/1Panel/core/utils/ssh" + "github.com/1Panel-dev/1Panel/core/utils/terminal" "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" + "github.com/pkg/errors" ) // @Tags Host @@ -15,7 +27,7 @@ import ( // @Param request body dto.HostOperate true "request" // @Success 200 // @Security ApiKeyAuth -// @Router /hosts [post] +// @Router /core/hosts [post] // @x-panel-log {"bodyKeys":["name","addr"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建主机 [name][addr]","formatEN":"create host [name][addr]"} func (b *BaseApi) CreateHost(c *gin.Context) { var req dto.HostOperate @@ -38,7 +50,7 @@ func (b *BaseApi) CreateHost(c *gin.Context) { // @Param request body dto.HostConnTest true "request" // @Success 200 // @Security ApiKeyAuth -// @Router /hosts/test/byinfo [post] +// @Router /core/hosts/test/byinfo [post] func (b *BaseApi) TestByInfo(c *gin.Context) { var req dto.HostConnTest if err := helper.CheckBindAndValidate(&req, c); err != nil { @@ -56,15 +68,20 @@ func (b *BaseApi) TestByInfo(c *gin.Context) { // @Param id path integer true "request" // @Success 200 {boolean} connStatus // @Security ApiKeyAuth -// @Router /hosts/test/byid/:id [post] +// @Router /core/hosts/test/byid/:id [post] func (b *BaseApi) TestByID(c *gin.Context) { - id, err := helper.GetParamID(c) + idParam, ok := c.Params.Get("id") + if !ok { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, errors.New("no such params find in request")) + return + } + intNum, err := strconv.Atoi(idParam) if err != nil { helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) return } - connStatus := hostService.TestLocalConn(id) + connStatus := hostService.TestLocalConn(uint(intNum)) helper.SuccessWithData(c, connStatus) } @@ -75,10 +92,10 @@ func (b *BaseApi) TestByID(c *gin.Context) { // @Param request body dto.SearchForTree true "request" // @Success 200 {array} dto.HostTree // @Security ApiKeyAuth -// @Router /hosts/tree [post] +// @Router /core/hosts/tree [post] func (b *BaseApi) HostTree(c *gin.Context) { var req dto.SearchForTree - if err := helper.CheckBind(&req, c); err != nil { + if err := helper.CheckBindAndValidate(&req, c); err != nil { return } @@ -98,7 +115,7 @@ func (b *BaseApi) HostTree(c *gin.Context) { // @Param request body dto.SearchHostWithPage true "request" // @Success 200 {array} dto.HostTree // @Security ApiKeyAuth -// @Router /hosts/search [post] +// @Router /core/hosts/search [post] func (b *BaseApi) SearchHost(c *gin.Context) { var req dto.SearchHostWithPage if err := helper.CheckBindAndValidate(&req, c); err != nil { @@ -124,15 +141,15 @@ func (b *BaseApi) SearchHost(c *gin.Context) { // @Param request body dto.BatchDeleteReq true "request" // @Success 200 // @Security ApiKeyAuth -// @Router /hosts/del [post] +// @Router /core/hosts/del [post] // @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"hosts","output_column":"addr","output_value":"addrs"}],"formatZH":"删除主机 [addrs]","formatEN":"delete host [addrs]"} func (b *BaseApi) DeleteHost(c *gin.Context) { - var req dto.BatchDeleteReq + var req dto.OperateByIDs if err := helper.CheckBindAndValidate(&req, c); err != nil { return } - if err := hostService.Delete(req.Ids); err != nil { + if err := hostService.Delete(req.IDs); err != nil { helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) return } @@ -146,7 +163,7 @@ func (b *BaseApi) DeleteHost(c *gin.Context) { // @Param request body dto.HostOperate true "request" // @Success 200 // @Security ApiKeyAuth -// @Router /hosts/update [post] +// @Router /core/hosts/update [post] // @x-panel-log {"bodyKeys":["name","addr"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新主机信息 [name][addr]","formatEN":"update host [name][addr]"} func (b *BaseApi) UpdateHost(c *gin.Context) { var req dto.HostOperate @@ -212,7 +229,7 @@ func (b *BaseApi) UpdateHost(c *gin.Context) { // @Param request body dto.ChangeHostGroup true "request" // @Success 200 // @Security ApiKeyAuth -// @Router /hosts/update/group [post] +// @Router /core/hosts/update/group [post] // @x-panel-log {"bodyKeys":["id","group"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"hosts","output_column":"addr","output_value":"addr"}],"formatZH":"切换主机[addr]分组 => [group]","formatEN":"change host [addr] group => [group]"} func (b *BaseApi) UpdateHostGroup(c *gin.Context) { var req dto.ChangeHostGroup @@ -228,3 +245,84 @@ func (b *BaseApi) UpdateHostGroup(c *gin.Context) { } helper.SuccessWithData(c, nil) } + +func (b *BaseApi) WsSsh(c *gin.Context) { + wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + global.LOG.Errorf("gin context http handler failed, err: %v", err) + return + } + defer wsConn.Close() + + id, err := strconv.Atoi(c.Query("id")) + if wshandleError(wsConn, errors.WithMessage(err, "invalid param id in request")) { + return + } + cols, err := strconv.Atoi(c.DefaultQuery("cols", "80")) + if wshandleError(wsConn, errors.WithMessage(err, "invalid param cols in request")) { + return + } + rows, err := strconv.Atoi(c.DefaultQuery("rows", "40")) + if wshandleError(wsConn, errors.WithMessage(err, "invalid param rows in request")) { + return + } + host, err := hostService.GetHostInfo(uint(id)) + if wshandleError(wsConn, errors.WithMessage(err, "load host info by id failed")) { + return + } + var connInfo ssh.ConnInfo + _ = copier.Copy(&connInfo, &host) + connInfo.PrivateKey = []byte(host.PrivateKey) + if len(host.PassPhrase) != 0 { + connInfo.PassPhrase = []byte(host.PassPhrase) + } + + client, err := connInfo.NewClient() + if wshandleError(wsConn, errors.WithMessage(err, "failed to set up the connection. Please check the host information")) { + return + } + defer client.Close() + sws, err := terminal.NewLogicSshWsSession(cols, rows, true, connInfo.Client, wsConn) + if wshandleError(wsConn, err) { + return + } + defer sws.Close() + + quitChan := make(chan bool, 3) + sws.Start(quitChan) + go sws.Wait(quitChan) + + <-quitChan + + if wshandleError(wsConn, err) { + return + } +} + +var upGrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024 * 1024 * 10, + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +func wshandleError(ws *websocket.Conn, err error) bool { + if err != nil { + global.LOG.Errorf("handler ws faled:, err: %v", err) + dt := time.Now().Add(time.Second) + if ctlerr := ws.WriteControl(websocket.CloseMessage, []byte(err.Error()), dt); ctlerr != nil { + wsData, err := json.Marshal(terminal.WsMsg{ + Type: terminal.WsMsgCmd, + Data: base64.StdEncoding.EncodeToString([]byte(err.Error())), + }) + if err != nil { + _ = ws.WriteMessage(websocket.TextMessage, []byte("{\"type\":\"cmd\",\"data\":\"failed to encoding to json\"}")) + } else { + _ = ws.WriteMessage(websocket.TextMessage, wsData) + } + } + return true + } + return false +} diff --git a/core/app/dto/command.go b/core/app/dto/command.go new file mode 100644 index 000000000..1369de72e --- /dev/null +++ b/core/app/dto/command.go @@ -0,0 +1,38 @@ +package dto + +type SearchCommandWithPage struct { + PageInfo + OrderBy string `json:"orderBy" validate:"required,oneof=name command created_at"` + Order string `json:"order" validate:"required,oneof=null ascending descending"` + GroupID uint `json:"groupID"` + Type string `josn:"type" validate:"required,oneof=redis command"` + Info string `json:"info"` +} + +type CommandOperate struct { + ID uint `json:"id"` + Type string `josn:"type"` + GroupID uint `json:"groupID"` + GroupBelong string `json:"groupBelong"` + Name string `json:"name" validate:"required"` + Command string `json:"command" validate:"required"` +} + +type CommandInfo struct { + ID uint `json:"id"` + GroupID uint `json:"groupID"` + Name string `json:"name"` + Command string `json:"command"` + GroupBelong string `json:"groupBelong"` +} + +type CommandTree struct { + ID uint `json:"id"` + Label string `json:"label"` + Children []CommandInfo `json:"children"` +} + +type CommandDelete struct { + Type string `json:"type" validate:"required,oneof=redis command"` + IDs []uint `json:"ids"` +} diff --git a/core/app/dto/common.go b/core/app/dto/common.go index b602b79df..3d8cc1212 100644 --- a/core/app/dto/common.go +++ b/core/app/dto/common.go @@ -31,6 +31,10 @@ type Options struct { Option string `json:"option"` } +type OperateByType struct { + Type string `json:"type"` +} + type OperateByID struct { ID uint `json:"id"` } diff --git a/agent/app/dto/group.go b/core/app/dto/group.go similarity index 100% rename from agent/app/dto/group.go rename to core/app/dto/group.go diff --git a/agent/app/dto/host.go b/core/app/dto/host.go similarity index 84% rename from agent/app/dto/host.go rename to core/app/dto/host.go index e82f221af..9853d8f34 100644 --- a/agent/app/dto/host.go +++ b/core/app/dto/host.go @@ -73,15 +73,3 @@ type TreeChild struct { ID uint `json:"id"` Label string `json:"label"` } - -type MonitorSetting struct { - MonitorStatus string `json:"monitorStatus"` - MonitorStoreDays string `json:"monitorStoreDays"` - MonitorInterval string `json:"monitorInterval"` - DefaultNetwork string `json:"defaultNetwork"` -} - -type MonitorSettingUpdate struct { - Key string `json:"key" validate:"required,oneof=MonitorStatus MonitorStoreDays MonitorInterval DefaultNetwork"` - Value string `json:"value"` -} diff --git a/core/app/model/backup.go b/core/app/model/backup.go index 019a8fb61..375c08a92 100644 --- a/core/app/model/backup.go +++ b/core/app/model/backup.go @@ -1,5 +1,7 @@ package model +import "time" + type BackupAccount struct { BaseModel Name string `gorm:"unique;not null" json:"name"` @@ -11,6 +13,7 @@ type BackupAccount struct { Vars string `json:"vars"` RememberAuth bool `json:"rememberAuth"` - InUsed bool `json:"inUsed"` EntryID uint `json:"entryID"` + + DeletedAt time.Time `json:"deletedAt"` } diff --git a/core/app/model/command.go b/core/app/model/command.go new file mode 100644 index 000000000..0c76e13f4 --- /dev/null +++ b/core/app/model/command.go @@ -0,0 +1,9 @@ +package model + +type Command struct { + BaseModel + Type string `gorm:"not null" json:"type"` + Name string `gorm:"not null" json:"name"` + GroupID uint `gorm:"not null" json:"groupID"` + Command string `gorm:"not null" json:"command"` +} diff --git a/core/app/model/group.go b/core/app/model/group.go new file mode 100644 index 000000000..c8999b703 --- /dev/null +++ b/core/app/model/group.go @@ -0,0 +1,8 @@ +package model + +type Group struct { + BaseModel + IsDefault bool `json:"isDefault"` + Name string `json:"name"` + Type string `json:"type"` +} diff --git a/agent/app/model/host.go b/core/app/model/host.go similarity index 100% rename from agent/app/model/host.go rename to core/app/model/host.go diff --git a/agent/app/repo/command.go b/core/app/repo/command.go similarity index 59% rename from agent/app/repo/command.go rename to core/app/repo/command.go index e2e9c7124..a17a257c9 100644 --- a/agent/app/repo/command.go +++ b/core/app/repo/command.go @@ -1,8 +1,8 @@ package repo import ( - "github.com/1Panel-dev/1Panel/agent/app/model" - "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/core/app/model" + "github.com/1Panel-dev/1Panel/core/global" "gorm.io/gorm" ) @@ -14,15 +14,10 @@ type ICommandRepo interface { WithByInfo(info string) DBOption Create(command *model.Command) error Update(id uint, vars map[string]interface{}) error + UpdateGroup(group, newGroup uint) error Delete(opts ...DBOption) error Get(opts ...DBOption) (model.Command, error) WithLikeName(name string) DBOption - - PageRedis(limit, offset int, opts ...DBOption) (int64, []model.RedisCommand, error) - GetRedis(opts ...DBOption) (model.RedisCommand, error) - GetRedisList(opts ...DBOption) ([]model.RedisCommand, error) - SaveRedis(command *model.RedisCommand) error - DeleteRedis(opts ...DBOption) error } func NewICommandRepo() ICommandRepo { @@ -39,16 +34,6 @@ func (u *CommandRepo) Get(opts ...DBOption) (model.Command, error) { return command, err } -func (u *CommandRepo) GetRedis(opts ...DBOption) (model.RedisCommand, error) { - var command model.RedisCommand - db := global.DB - for _, opt := range opts { - db = opt(db) - } - err := db.First(&command).Error - return command, err -} - func (u *CommandRepo) Page(page, size int, opts ...DBOption) (int64, []model.Command, error) { var users []model.Command db := global.DB.Model(&model.Command{}) @@ -61,18 +46,6 @@ func (u *CommandRepo) Page(page, size int, opts ...DBOption) (int64, []model.Com return count, users, err } -func (u *CommandRepo) PageRedis(page, size int, opts ...DBOption) (int64, []model.RedisCommand, error) { - var users []model.RedisCommand - db := global.DB.Model(&model.RedisCommand{}) - for _, opt := range opts { - db = opt(db) - } - count := int64(0) - db = db.Count(&count) - err := db.Limit(size).Offset(size * (page - 1)).Find(&users).Error - return count, users, err -} - func (u *CommandRepo) GetList(opts ...DBOption) ([]model.Command, error) { var commands []model.Command db := global.DB.Model(&model.Command{}) @@ -83,16 +56,6 @@ func (u *CommandRepo) GetList(opts ...DBOption) ([]model.Command, error) { return commands, err } -func (u *CommandRepo) GetRedisList(opts ...DBOption) ([]model.RedisCommand, error) { - var commands []model.RedisCommand - db := global.DB.Model(&model.RedisCommand{}) - for _, opt := range opts { - db = opt(db) - } - err := db.Find(&commands).Error - return commands, err -} - func (c *CommandRepo) WithByInfo(info string) DBOption { return func(g *gorm.DB) *gorm.DB { if len(info) == 0 { @@ -107,13 +70,12 @@ func (u *CommandRepo) Create(command *model.Command) error { return global.DB.Create(command).Error } -func (u *CommandRepo) SaveRedis(command *model.RedisCommand) error { - return global.DB.Save(command).Error -} - func (u *CommandRepo) Update(id uint, vars map[string]interface{}) error { return global.DB.Model(&model.Command{}).Where("id = ?", id).Updates(vars).Error } +func (h *CommandRepo) UpdateGroup(group, newGroup uint) error { + return global.DB.Model(&model.Command{}).Where("group_id = ?", group).Updates(map[string]interface{}{"group_id": newGroup}).Error +} func (u *CommandRepo) Delete(opts ...DBOption) error { db := global.DB @@ -123,14 +85,6 @@ func (u *CommandRepo) Delete(opts ...DBOption) error { return db.Delete(&model.Command{}).Error } -func (u *CommandRepo) DeleteRedis(opts ...DBOption) error { - db := global.DB - for _, opt := range opts { - db = opt(db) - } - return db.Delete(&model.RedisCommand{}).Error -} - func (a CommandRepo) WithLikeName(name string) DBOption { return func(g *gorm.DB) *gorm.DB { if len(name) == 0 { diff --git a/core/app/repo/common.go b/core/app/repo/common.go index 284ef6400..013db44ef 100644 --- a/core/app/repo/common.go +++ b/core/app/repo/common.go @@ -1,6 +1,9 @@ package repo import ( + "fmt" + + "github.com/1Panel-dev/1Panel/core/constant" "gorm.io/gorm" ) @@ -12,6 +15,8 @@ type ICommonRepo interface { WithByIDs(ids []uint) DBOption WithByType(ty string) DBOption WithOrderBy(orderStr string) DBOption + + WithOrderRuleBy(orderBy, order string) DBOption } type CommonRepo struct{} @@ -51,3 +56,18 @@ func (c *CommonRepo) WithOrderBy(orderStr string) DBOption { return g.Order(orderStr) } } + +func (c *CommonRepo) WithOrderRuleBy(orderBy, order string) DBOption { + switch order { + case constant.OrderDesc: + order = "desc" + case constant.OrderAsc: + order = "asc" + default: + orderBy = "created_at" + order = "desc" + } + return func(g *gorm.DB) *gorm.DB { + return g.Order(fmt.Sprintf("%s %s", orderBy, order)) + } +} diff --git a/agent/app/repo/group.go b/core/app/repo/group.go similarity index 60% rename from agent/app/repo/group.go rename to core/app/repo/group.go index 1f5b27ae9..9ce1336ef 100644 --- a/agent/app/repo/group.go +++ b/core/app/repo/group.go @@ -1,8 +1,8 @@ package repo import ( - "github.com/1Panel-dev/1Panel/agent/app/model" - "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/core/app/model" + "github.com/1Panel-dev/1Panel/core/global" "gorm.io/gorm" ) @@ -14,14 +14,42 @@ type IGroupRepo interface { Create(group *model.Group) error Update(id uint, vars map[string]interface{}) error Delete(opts ...DBOption) error + + WithByID(id uint) DBOption + WithByGroupID(id uint) DBOption + WithByGroupType(ty string) DBOption + WithByDefault(isDefalut bool) DBOption CancelDefault(groupType string) error - WithByHostDefault() DBOption } func NewIGroupRepo() IGroupRepo { return &GroupRepo{} } +func (c *GroupRepo) WithByID(id uint) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("id = ?", id) + } +} + +func (c *GroupRepo) WithByGroupID(id uint) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("group_id = ?", id) + } +} + +func (c *GroupRepo) WithByGroupType(ty string) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("`type` = ?", ty) + } +} + +func (c *GroupRepo) WithByDefault(isDefalut bool) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("is_default = ?", isDefalut) + } +} + func (u *GroupRepo) Get(opts ...DBOption) (model.Group, error) { var group model.Group db := global.DB @@ -50,12 +78,6 @@ func (u *GroupRepo) Update(id uint, vars map[string]interface{}) error { return global.DB.Model(&model.Group{}).Where("id = ?", id).Updates(vars).Error } -func (u *GroupRepo) WithByHostDefault() DBOption { - return func(g *gorm.DB) *gorm.DB { - return g.Where("is_default = ? AND type = ?", 1, "host") - } -} - func (u *GroupRepo) Delete(opts ...DBOption) error { db := global.DB for _, opt := range opts { @@ -65,5 +87,7 @@ func (u *GroupRepo) Delete(opts ...DBOption) error { } func (u *GroupRepo) CancelDefault(groupType string) error { - return global.DB.Model(&model.Group{}).Where("is_default = ? AND type = ?", 1, groupType).Updates(map[string]interface{}{"is_default": 0}).Error + return global.DB.Model(&model.Group{}). + Where("is_default = ? AND type = ?", 1, groupType). + Updates(map[string]interface{}{"is_default": 0}).Error } diff --git a/core/app/repo/host.go b/core/app/repo/host.go new file mode 100644 index 000000000..c0edb0faa --- /dev/null +++ b/core/app/repo/host.go @@ -0,0 +1,113 @@ +package repo + +import ( + "github.com/1Panel-dev/1Panel/core/app/model" + "github.com/1Panel-dev/1Panel/core/global" + "gorm.io/gorm" +) + +type HostRepo struct{} + +type IHostRepo interface { + Get(opts ...DBOption) (model.Host, error) + GetList(opts ...DBOption) ([]model.Host, error) + Page(limit, offset int, opts ...DBOption) (int64, []model.Host, error) + WithByInfo(info string) DBOption + WithByPort(port uint) DBOption + WithByUser(user string) DBOption + WithByAddr(addr string) DBOption + Create(host *model.Host) error + Update(id uint, vars map[string]interface{}) error + UpdateGroup(group, newGroup uint) error + Delete(opts ...DBOption) error +} + +func NewIHostRepo() IHostRepo { + return &HostRepo{} +} + +func (h *HostRepo) Get(opts ...DBOption) (model.Host, error) { + var host model.Host + db := global.DB + for _, opt := range opts { + db = opt(db) + } + err := db.First(&host).Error + return host, err +} + +func (h *HostRepo) GetList(opts ...DBOption) ([]model.Host, error) { + var hosts []model.Host + db := global.DB.Model(&model.Host{}) + for _, opt := range opts { + db = opt(db) + } + err := db.Find(&hosts).Error + return hosts, err +} + +func (h *HostRepo) Page(page, size int, opts ...DBOption) (int64, []model.Host, error) { + var users []model.Host + db := global.DB.Model(&model.Host{}) + for _, opt := range opts { + db = opt(db) + } + count := int64(0) + db = db.Count(&count) + err := db.Limit(size).Offset(size * (page - 1)).Find(&users).Error + return count, users, err +} + +func (h *HostRepo) WithByInfo(info string) DBOption { + return func(g *gorm.DB) *gorm.DB { + if len(info) == 0 { + return g + } + infoStr := "%" + info + "%" + return g.Where("name LIKE ? OR addr LIKE ?", infoStr, infoStr) + } +} + +func (h *HostRepo) WithByPort(port uint) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("port = ?", port) + } +} +func (h *HostRepo) WithByUser(user string) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("user = ?", user) + } +} +func (h *HostRepo) WithByAddr(addr string) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("addr = ?", addr) + } +} +func (h *HostRepo) WithByGroup(group string) DBOption { + return func(g *gorm.DB) *gorm.DB { + if len(group) == 0 { + return g + } + return g.Where("group_belong = ?", group) + } +} + +func (h *HostRepo) Create(host *model.Host) error { + return global.DB.Create(host).Error +} + +func (h *HostRepo) Update(id uint, vars map[string]interface{}) error { + return global.DB.Model(&model.Host{}).Where("id = ?", id).Updates(vars).Error +} + +func (h *HostRepo) UpdateGroup(group, newGroup uint) error { + return global.DB.Model(&model.Host{}).Where("group_id = ?", group).Updates(map[string]interface{}{"group_id": newGroup}).Error +} + +func (h *HostRepo) Delete(opts ...DBOption) error { + db := global.DB + for _, opt := range opts { + db = opt(db) + } + return db.Delete(&model.Host{}).Error +} diff --git a/core/app/service/backup.go b/core/app/service/backup.go index 4308ed25f..987899f44 100644 --- a/core/app/service/backup.go +++ b/core/app/service/backup.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "encoding/json" "fmt" + "net/http" "os" "path" "strings" @@ -19,6 +20,8 @@ import ( "github.com/1Panel-dev/1Panel/core/utils/cloud_storage/client" "github.com/1Panel-dev/1Panel/core/utils/encrypt" fileUtils "github.com/1Panel-dev/1Panel/core/utils/files" + httpUtils "github.com/1Panel-dev/1Panel/core/utils/http" + "github.com/1Panel-dev/1Panel/core/utils/xpack" "github.com/jinzhu/copier" "github.com/pkg/errors" "github.com/robfig/cron/v3" @@ -271,12 +274,17 @@ func (u *BackupService) Delete(id uint) error { if backup.Type == constant.Local { return buserr.New(constant.ErrBackupLocalDelete) } - if backup.InUsed { - return buserr.New(constant.ErrBackupInUsed) - } if backup.Type == constant.OneDrive { global.Cron.Remove(cron.EntryID(backup.EntryID)) } + if _, err := httpUtils.NewLocalClient(fmt.Sprintf("/api/v2/backups/check/%v", id), http.MethodGet, nil); err != nil { + global.LOG.Errorf("check used of local cronjob failed, err: %v", err) + return buserr.New(constant.ErrBackupInUsed) + } + if err := xpack.CheckBackupUsed(id); err != nil { + global.LOG.Errorf("check used of node cronjob failed, err: %v", err) + return buserr.New(constant.ErrBackupInUsed) + } return backupRepo.Delete(commonRepo.WithByID(id)) } diff --git a/core/app/service/command.go b/core/app/service/command.go new file mode 100644 index 000000000..0fe1d4939 --- /dev/null +++ b/core/app/service/command.go @@ -0,0 +1,128 @@ +package service + +import ( + "github.com/1Panel-dev/1Panel/core/app/dto" + "github.com/1Panel-dev/1Panel/core/app/repo" + "github.com/1Panel-dev/1Panel/core/constant" + "github.com/jinzhu/copier" + "github.com/pkg/errors" +) + +type CommandService struct{} + +type ICommandService interface { + List(req dto.OperateByType) ([]dto.CommandInfo, error) + SearchForTree(req dto.OperateByType) ([]dto.CommandTree, error) + SearchWithPage(search dto.SearchCommandWithPage) (int64, interface{}, error) + Create(commandDto dto.CommandOperate) error + Update(id uint, upMap map[string]interface{}) error + Delete(ids []uint) error +} + +func NewICommandService() ICommandService { + return &CommandService{} +} + +func (u *CommandService) List(req dto.OperateByType) ([]dto.CommandInfo, error) { + commands, err := commandRepo.GetList(commonRepo.WithOrderBy("name"), commonRepo.WithByType(req.Type)) + if err != nil { + return nil, constant.ErrRecordNotFound + } + var dtoCommands []dto.CommandInfo + for _, command := range commands { + var item dto.CommandInfo + if err := copier.Copy(&item, &command); err != nil { + return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + dtoCommands = append(dtoCommands, item) + } + return dtoCommands, err +} + +func (u *CommandService) SearchForTree(req dto.OperateByType) ([]dto.CommandTree, error) { + cmdList, err := commandRepo.GetList(commonRepo.WithOrderBy("name"), commonRepo.WithByType(req.Type)) + if err != nil { + return nil, err + } + groups, err := groupRepo.GetList(commonRepo.WithByType(req.Type)) + if err != nil { + return nil, err + } + var lists []dto.CommandTree + for _, group := range groups { + var data dto.CommandTree + data.ID = group.ID + 10000 + data.Label = group.Name + for _, cmd := range cmdList { + if cmd.GroupID == group.ID { + data.Children = append(data.Children, dto.CommandInfo{ID: cmd.ID, Name: cmd.Name, Command: cmd.Command}) + } + } + if len(data.Children) != 0 { + lists = append(lists, data) + } + } + return lists, err +} + +func (u *CommandService) SearchWithPage(req dto.SearchCommandWithPage) (int64, interface{}, error) { + options := []repo.DBOption{ + commonRepo.WithOrderRuleBy(req.OrderBy, req.Order), + commonRepo.WithByType(req.Type), + } + if len(req.Info) != 0 { + options = append(options, commandRepo.WithLikeName(req.Info)) + } + if req.GroupID != 0 { + options = append(options, groupRepo.WithByGroupID(req.GroupID)) + } + total, commands, err := commandRepo.Page(req.Page, req.PageSize, options...) + if err != nil { + return 0, nil, err + } + groups, _ := groupRepo.GetList(commonRepo.WithByType(req.Type)) + var dtoCommands []dto.CommandInfo + for _, command := range commands { + var item dto.CommandInfo + if err := copier.Copy(&item, &command); err != nil { + return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + for _, group := range groups { + if command.GroupID == group.ID { + item.GroupBelong = group.Name + item.GroupID = group.ID + } + } + dtoCommands = append(dtoCommands, item) + } + return total, dtoCommands, err +} + +func (u *CommandService) Create(commandDto dto.CommandOperate) error { + command, _ := commandRepo.Get(commonRepo.WithByName(commandDto.Name)) + if command.ID != 0 { + return constant.ErrRecordExist + } + if err := copier.Copy(&command, &commandDto); err != nil { + return errors.WithMessage(constant.ErrStructTransform, err.Error()) + } + if err := commandRepo.Create(&command); err != nil { + return err + } + return nil +} + +func (u *CommandService) Delete(ids []uint) error { + if len(ids) == 1 { + command, _ := commandRepo.Get(commonRepo.WithByID(ids[0])) + if command.ID == 0 { + return constant.ErrRecordNotFound + } + return commandRepo.Delete(commonRepo.WithByID(ids[0])) + } + return commandRepo.Delete(commonRepo.WithByIDs(ids)) +} + +func (u *CommandService) Update(id uint, upMap map[string]interface{}) error { + return commandRepo.Update(id, upMap) +} diff --git a/core/app/service/entry.go b/core/app/service/entry.go index f6b25a2c6..229a7dfa5 100644 --- a/core/app/service/entry.go +++ b/core/app/service/entry.go @@ -3,8 +3,11 @@ package service import "github.com/1Panel-dev/1Panel/core/app/repo" var ( + hostRepo = repo.NewIHostRepo() + commandRepo = repo.NewICommandRepo() commonRepo = repo.NewICommonRepo() settingRepo = repo.NewISettingRepo() backupRepo = repo.NewIBackupRepo() logRepo = repo.NewILogRepo() + groupRepo = repo.NewIGroupRepo() ) diff --git a/agent/app/service/group.go b/core/app/service/group.go similarity index 52% rename from agent/app/service/group.go rename to core/app/service/group.go index 97ec2e2e6..33e3a8b80 100644 --- a/agent/app/service/group.go +++ b/core/app/service/group.go @@ -1,9 +1,17 @@ package service import ( - "github.com/1Panel-dev/1Panel/agent/app/dto" - "github.com/1Panel-dev/1Panel/agent/buserr" - "github.com/1Panel-dev/1Panel/agent/constant" + "bytes" + "fmt" + "net/http" + + "github.com/1Panel-dev/1Panel/core/app/dto" + "github.com/1Panel-dev/1Panel/core/app/model" + "github.com/1Panel-dev/1Panel/core/app/repo" + "github.com/1Panel-dev/1Panel/core/buserr" + "github.com/1Panel-dev/1Panel/core/constant" + httpUtils "github.com/1Panel-dev/1Panel/core/utils/http" + "github.com/1Panel-dev/1Panel/core/utils/xpack" "github.com/jinzhu/copier" "github.com/pkg/errors" ) @@ -11,7 +19,7 @@ import ( type GroupService struct{} type IGroupService interface { - List(req dto.GroupSearch) ([]dto.GroupInfo, error) + List(req dto.OperateByType) ([]dto.GroupInfo, error) Create(req dto.GroupCreate) error Update(req dto.GroupUpdate) error Delete(id uint) error @@ -21,8 +29,17 @@ func NewIGroupService() IGroupService { return &GroupService{} } -func (u *GroupService) List(req dto.GroupSearch) ([]dto.GroupInfo, error) { - groups, err := groupRepo.GetList(commonRepo.WithByType(req.Type), commonRepo.WithOrderBy("is_default desc"), commonRepo.WithOrderBy("created_at desc")) +func (u *GroupService) List(req dto.OperateByType) ([]dto.GroupInfo, error) { + options := []repo.DBOption{ + commonRepo.WithByType(req.Type), + commonRepo.WithOrderBy("is_default desc"), + commonRepo.WithOrderBy("created_at desc"), + } + var ( + groups []model.Group + err error + ) + groups, err = groupRepo.GetList(options...) if err != nil { return nil, constant.ErrRecordNotFound } @@ -56,22 +73,33 @@ func (u *GroupService) Delete(id uint) error { if group.ID == 0 { return constant.ErrRecordNotFound } + if group.IsDefault { + return buserr.New(constant.ErrGroupIsDefault) + } + defaultGroup, err := groupRepo.Get(commonRepo.WithByType(group.Type), groupRepo.WithByDefault(true)) + if err != nil { + return err + } switch group.Type { - case "website": - websites, _ := websiteRepo.GetBy(websiteRepo.WithGroupID(id)) - if len(websites) > 0 { - return buserr.New(constant.ErrGroupIsUsed) - } - case "command": - commands, _ := commandRepo.GetList(commonRepo.WithByGroupID(id)) - if len(commands) > 0 { - return buserr.New(constant.ErrGroupIsUsed) - } case "host": - hosts, _ := hostRepo.GetList(commonRepo.WithByGroupID(id)) - if len(hosts) > 0 { - return buserr.New(constant.ErrGroupIsUsed) + err = hostRepo.UpdateGroup(id, defaultGroup.ID) + case "command": + err = commandRepo.UpdateGroup(id, defaultGroup.ID) + case "node": + err = xpack.UpdateGroup("node", id, defaultGroup.ID) + case "website": + bodyItem := []byte(fmt.Sprintf(`{"Group":%v, "NewGroup":%v}`, id, defaultGroup.ID)) + if _, err := httpUtils.NewLocalClient("/api/v2/websites/group/change", http.MethodPost, bytes.NewReader(bodyItem)); err != nil { + return err } + if err := xpack.UpdateGroup("node", id, defaultGroup.ID); err != nil { + return err + } + default: + return constant.ErrNotSupportType + } + if err != nil { + return err } return groupRepo.Delete(commonRepo.WithByID(id)) } diff --git a/agent/app/service/host.go b/core/app/service/host.go similarity index 90% rename from agent/app/service/host.go rename to core/app/service/host.go index 85a93fba3..30959239c 100644 --- a/agent/app/service/host.go +++ b/core/app/service/host.go @@ -4,11 +4,12 @@ import ( "encoding/base64" "fmt" - "github.com/1Panel-dev/1Panel/agent/app/dto" - "github.com/1Panel-dev/1Panel/agent/app/model" - "github.com/1Panel-dev/1Panel/agent/constant" - "github.com/1Panel-dev/1Panel/agent/utils/encrypt" - "github.com/1Panel-dev/1Panel/agent/utils/ssh" + "github.com/1Panel-dev/1Panel/core/app/dto" + "github.com/1Panel-dev/1Panel/core/app/model" + "github.com/1Panel-dev/1Panel/core/app/repo" + "github.com/1Panel-dev/1Panel/core/constant" + "github.com/1Panel-dev/1Panel/core/utils/encrypt" + "github.com/1Panel-dev/1Panel/core/utils/ssh" "github.com/jinzhu/copier" "github.com/pkg/errors" ) @@ -149,8 +150,15 @@ func (u *HostService) GetHostInfo(id uint) (*model.Host, error) { return &host, err } -func (u *HostService) SearchWithPage(search dto.SearchHostWithPage) (int64, interface{}, error) { - total, hosts, err := hostRepo.Page(search.Page, search.PageSize, hostRepo.WithByInfo(search.Info), commonRepo.WithByGroupID(search.GroupID)) +func (u *HostService) SearchWithPage(req dto.SearchHostWithPage) (int64, interface{}, error) { + var options []repo.DBOption + if len(req.Info) != 0 { + options = append(options, commandRepo.WithLikeName(req.Info)) + } + if req.GroupID != 0 { + options = append(options, groupRepo.WithByGroupID(req.GroupID)) + } + total, hosts, err := hostRepo.Page(req.Page, req.PageSize, options...) if err != nil { return 0, nil, err } @@ -249,7 +257,7 @@ func (u *HostService) Create(req dto.HostOperate) (*dto.HostInfo, error) { return nil, errors.WithMessage(constant.ErrStructTransform, err.Error()) } if req.GroupID == 0 { - group, err := groupRepo.Get(groupRepo.WithByHostDefault()) + group, err := groupRepo.Get(commonRepo.WithByType("host"), groupRepo.WithByDefault(true)) if err != nil { return nil, errors.New("get default group failed") } @@ -299,7 +307,7 @@ func (u *HostService) Create(req dto.HostOperate) (*dto.HostInfo, error) { } func (u *HostService) Delete(ids []uint) error { - hosts, _ := hostRepo.GetList(commonRepo.WithIdsIn(ids)) + hosts, _ := hostRepo.GetList(commonRepo.WithByIDs(ids)) for _, host := range hosts { if host.ID == 0 { return constant.ErrRecordNotFound @@ -308,7 +316,7 @@ func (u *HostService) Delete(ids []uint) error { return errors.New("the local connection information cannot be deleted!") } } - return hostRepo.Delete(commonRepo.WithIdsIn(ids)) + return hostRepo.Delete(commonRepo.WithByIDs(ids)) } func (u *HostService) Update(id uint, upMap map[string]interface{}) error { diff --git a/core/constant/errs.go b/core/constant/errs.go index bbefe0bd7..0c776c204 100644 --- a/core/constant/errs.go +++ b/core/constant/errs.go @@ -37,6 +37,7 @@ var ( ErrPortInUsed = "ErrPortInUsed" ErrCmdTimeout = "ErrCmdTimeout" ErrGroupIsUsed = "ErrGroupIsUsed" + ErrGroupIsDefault = "ErrGroupIsDefault" ) // api diff --git a/core/i18n/lang/zh.yaml b/core/i18n/lang/zh.yaml index 98f4b3eab..273ea35f0 100644 --- a/core/i18n/lang/zh.yaml +++ b/core/i18n/lang/zh.yaml @@ -14,6 +14,7 @@ ErrProxy: "请求错误,请检查该节点状态: {{ .detail }}" ErrDemoEnvironment: "演示服务器,禁止此操作!" ErrCmdTimeout: "命令执行超时!" ErrEntrance: "安全入口信息错误,请检查后重试!" +ErrGroupIsDefault: '默认分组,无法删除' ErrGroupIsUsed: "分组正在使用中,无法删除" ErrLocalDelete: "无法删除本地节点!" ErrMasterAddr: "当前未设置主节点地址,无法添加节点!" diff --git a/core/init/migration/migrate.go b/core/init/migration/migrate.go index 41f53d671..8b76f7e9f 100644 --- a/core/init/migration/migrate.go +++ b/core/init/migration/migrate.go @@ -13,6 +13,7 @@ func Init() { migrations.InitSetting, migrations.InitOneDrive, migrations.InitMasterAddr, + migrations.InitHost, }) if err := m.Migrate(); err != nil { global.LOG.Error(err) diff --git a/core/init/migration/migrations/init.go b/core/init/migration/migrations/init.go index d8578f73f..d36af12a0 100644 --- a/core/init/migration/migrations/init.go +++ b/core/init/migration/migrations/init.go @@ -15,13 +15,16 @@ import ( ) var AddTable = &gormigrate.Migration{ - ID: "20240808-add-table", + ID: "20240819-add-table", Migrate: func(tx *gorm.DB) error { return tx.AutoMigrate( &model.OperationLog{}, &model.LoginLog{}, &model.Setting{}, &model.BackupAccount{}, + &model.Group{}, + &model.Host{}, + &model.Command{}, ) }, } @@ -144,6 +147,35 @@ var InitSetting = &gormigrate.Migration{ }, } +var InitHost = &gormigrate.Migration{ + ID: "20240816-init-host", + Migrate: func(tx *gorm.DB) error { + hostGroup := &model.Group{Name: "default", Type: "host", IsDefault: true} + if err := tx.Create(hostGroup).Error; err != nil { + return err + } + if err := tx.Create(&model.Group{Name: "default", Type: "node", IsDefault: true}).Error; err != nil { + return err + } + if err := tx.Create(&model.Group{Name: "default", Type: "command", IsDefault: true}).Error; err != nil { + return err + } + if err := tx.Create(&model.Group{Name: "default", Type: "website", IsDefault: true}).Error; err != nil { + return err + } + if err := tx.Create(&model.Group{Name: "default", Type: "redis", IsDefault: true}).Error; err != nil { + return err + } + host := model.Host{ + Name: "localhost", Addr: "127.0.0.1", User: "root", Port: 22, AuthMode: "password", GroupID: hostGroup.ID, + } + if err := tx.Create(&host).Error; err != nil { + return err + } + return nil + }, +} + var InitOneDrive = &gormigrate.Migration{ ID: "20240808-init-one-drive", Migrate: func(tx *gorm.DB) error { diff --git a/core/router/command.go b/core/router/command.go new file mode 100644 index 000000000..16a2e0736 --- /dev/null +++ b/core/router/command.go @@ -0,0 +1,21 @@ +package router + +import ( + v2 "github.com/1Panel-dev/1Panel/core/app/api/v2" + "github.com/gin-gonic/gin" +) + +type CommandRouter struct{} + +func (s *CommandRouter) InitRouter(Router *gin.RouterGroup) { + commandRouter := Router.Group("commands") + baseApi := v2.ApiGroupApp.BaseApi + { + commandRouter.POST("/list", baseApi.ListCommand) + commandRouter.POST("", baseApi.CreateCommand) + commandRouter.POST("/del", baseApi.DeleteCommand) + commandRouter.POST("/search", baseApi.SearchCommand) + commandRouter.POST("/tree", baseApi.SearchCommandTree) + commandRouter.POST("/update", baseApi.UpdateCommand) + } +} diff --git a/core/router/common.go b/core/router/common.go index 78748f230..748d81896 100644 --- a/core/router/common.go +++ b/core/router/common.go @@ -6,5 +6,8 @@ func commonGroups() []CommonRouter { &BackupRouter{}, &LogRouter{}, &SettingRouter{}, + &CommandRouter{}, + &HostRouter{}, + &GroupRouter{}, } } diff --git a/agent/router/ro_group.go b/core/router/ro_group.go similarity index 68% rename from agent/router/ro_group.go rename to core/router/ro_group.go index 4cf92a080..827dd1446 100644 --- a/agent/router/ro_group.go +++ b/core/router/ro_group.go @@ -1,14 +1,14 @@ package router import ( - v2 "github.com/1Panel-dev/1Panel/agent/app/api/v2" + v2 "github.com/1Panel-dev/1Panel/core/app/api/v2" "github.com/gin-gonic/gin" ) -type WebsiteGroupRouter struct { +type GroupRouter struct { } -func (a *WebsiteGroupRouter) InitRouter(Router *gin.RouterGroup) { +func (a *GroupRouter) InitRouter(Router *gin.RouterGroup) { groupRouter := Router.Group("groups") baseApi := v2.ApiGroupApp.BaseApi diff --git a/core/router/ro_host.go b/core/router/ro_host.go new file mode 100644 index 000000000..0e03cdf8d --- /dev/null +++ b/core/router/ro_host.go @@ -0,0 +1,25 @@ +package router + +import ( + v2 "github.com/1Panel-dev/1Panel/core/app/api/v2" + "github.com/gin-gonic/gin" +) + +type HostRouter struct{} + +func (s *HostRouter) InitRouter(Router *gin.RouterGroup) { + hostRouter := Router.Group("hosts") + baseApi := v2.ApiGroupApp.BaseApi + { + hostRouter.POST("", baseApi.CreateHost) + hostRouter.POST("/del", baseApi.DeleteHost) + hostRouter.POST("/update", baseApi.UpdateHost) + hostRouter.POST("/update/group", baseApi.UpdateHostGroup) + hostRouter.POST("/search", baseApi.SearchHost) + hostRouter.POST("/tree", baseApi.HostTree) + hostRouter.POST("/test/byinfo", baseApi.TestByInfo) + hostRouter.POST("/test/byid/:id", baseApi.TestByID) + + hostRouter.GET("/terminal", baseApi.WsSsh) + } +} diff --git a/core/utils/encrypt/encrypt.go b/core/utils/encrypt/encrypt.go index 5e596b73b..68cea03ef 100644 --- a/core/utils/encrypt/encrypt.go +++ b/core/utils/encrypt/encrypt.go @@ -5,7 +5,6 @@ import ( "crypto/aes" "crypto/cipher" "crypto/rand" - "encoding/base32" "encoding/base64" "fmt" "io" @@ -31,7 +30,7 @@ func StringDecryptWithBase64(text string) (string, error) { if err != nil { return "", err } - return base32.StdEncoding.EncodeToString([]byte(decryptItem)), nil + return base64.StdEncoding.EncodeToString([]byte(decryptItem)), nil } func StringEncrypt(text string) (string, error) { diff --git a/core/utils/http/new.go b/core/utils/http/new.go new file mode 100644 index 000000000..1db6ecd18 --- /dev/null +++ b/core/utils/http/new.go @@ -0,0 +1,69 @@ +package http + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/url" + "os" + + "github.com/1Panel-dev/1Panel/core/app/dto" +) + +func NewLocalClient(reqUrl, reqMethod string, body io.Reader) (interface{}, error) { + sockPath := "/tmp/agent.sock" + if _, err := os.Stat(sockPath); err != nil { + return nil, fmt.Errorf("no such agent.sock find in localhost, err: %v", err) + } + dialUnix := func() (conn net.Conn, err error) { + return net.Dial("unix", sockPath) + } + transport := &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return dialUnix() + }, + } + client := &http.Client{ + Transport: transport, + } + parsedURL, err := url.Parse("http://unix") + if err != nil { + return nil, fmt.Errorf("handle url Parse failed, err: %v \n", err) + } + rURL := &url.URL{ + Scheme: "http", + Path: reqUrl, + Host: parsedURL.Host, + } + + req, err := http.NewRequest(reqMethod, rURL.String(), body) + if err != nil { + return nil, fmt.Errorf("creating request failed, err: %v", err) + } + + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("client do request failed, err: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("do request failed, err: %v", resp.Status) + } + bodyByte, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("read resp body from request failed, err: %v", err) + } + var respJson dto.Response + if err := json.Unmarshal(bodyByte, &respJson); err != nil { + return nil, fmt.Errorf("json umarshal resp data failed, err: %v", err) + } + if respJson.Code != http.StatusOK { + return nil, fmt.Errorf("do request success but handle failed, err: %v", respJson.Message) + } + + return respJson.Data, nil +} diff --git a/core/utils/terminal/local_cmd.go b/core/utils/terminal/local_cmd.go new file mode 100644 index 000000000..56f023a29 --- /dev/null +++ b/core/utils/terminal/local_cmd.go @@ -0,0 +1,93 @@ +package terminal + +import ( + "os" + "os/exec" + "syscall" + "time" + "unsafe" + + "github.com/1Panel-dev/1Panel/core/global" + "github.com/creack/pty" + "github.com/pkg/errors" +) + +const ( + DefaultCloseSignal = syscall.SIGINT + DefaultCloseTimeout = 10 * time.Second +) + +type LocalCommand struct { + closeSignal syscall.Signal + closeTimeout time.Duration + + cmd *exec.Cmd + pty *os.File +} + +func NewCommand(commands []string) (*LocalCommand, error) { + cmd := exec.Command("docker", commands...) + + pty, err := pty.Start(cmd) + if err != nil { + return nil, errors.Wrapf(err, "failed to start command") + } + + lcmd := &LocalCommand{ + closeSignal: DefaultCloseSignal, + closeTimeout: DefaultCloseTimeout, + + cmd: cmd, + pty: pty, + } + + return lcmd, nil +} + +func (lcmd *LocalCommand) Read(p []byte) (n int, err error) { + return lcmd.pty.Read(p) +} + +func (lcmd *LocalCommand) Write(p []byte) (n int, err error) { + return lcmd.pty.Write(p) +} + +func (lcmd *LocalCommand) Close() error { + if lcmd.cmd != nil && lcmd.cmd.Process != nil { + _ = lcmd.cmd.Process.Kill() + } + _ = lcmd.pty.Close() + return nil +} + +func (lcmd *LocalCommand) ResizeTerminal(width int, height int) error { + window := struct { + row uint16 + col uint16 + x uint16 + y uint16 + }{ + uint16(height), + uint16(width), + 0, + 0, + } + _, _, errno := syscall.Syscall( + syscall.SYS_IOCTL, + lcmd.pty.Fd(), + syscall.TIOCSWINSZ, + uintptr(unsafe.Pointer(&window)), + ) + if errno != 0 { + return errno + } else { + return nil + } +} + +func (lcmd *LocalCommand) Wait(quitChan chan bool) { + if err := lcmd.cmd.Wait(); err != nil { + global.LOG.Errorf("ssh session wait failed, err: %v", err) + setQuit(quitChan) + } +} diff --git a/core/utils/terminal/ws_local_session.go b/core/utils/terminal/ws_local_session.go new file mode 100644 index 000000000..2fd14e0ce --- /dev/null +++ b/core/utils/terminal/ws_local_session.go @@ -0,0 +1,122 @@ +package terminal + +import ( + "encoding/base64" + "encoding/json" + "sync" + + "github.com/1Panel-dev/1Panel/core/global" + "github.com/gorilla/websocket" + "github.com/pkg/errors" +) + +type LocalWsSession struct { + slave *LocalCommand + wsConn *websocket.Conn + + allowCtrlC bool + writeMutex sync.Mutex +} + +func NewLocalWsSession(cols, rows int, wsConn *websocket.Conn, slave *LocalCommand, allowCtrlC bool) (*LocalWsSession, error) { + if err := slave.ResizeTerminal(cols, rows); err != nil { + global.LOG.Errorf("ssh pty change windows size failed, err: %v", err) + } + + return &LocalWsSession{ + slave: slave, + wsConn: wsConn, + + allowCtrlC: allowCtrlC, + }, nil +} + +func (sws *LocalWsSession) Start(quitChan chan bool) { + go sws.handleSlaveEvent(quitChan) + go sws.receiveWsMsg(quitChan) +} + +func (sws *LocalWsSession) handleSlaveEvent(exitCh chan bool) { + defer setQuit(exitCh) + defer global.LOG.Debug("thread of handle slave event has exited now") + + buffer := make([]byte, 1024) + for { + select { + case <-exitCh: + return + default: + n, _ := sws.slave.Read(buffer) + _ = sws.masterWrite(buffer[:n]) + } + } +} + +func (sws *LocalWsSession) masterWrite(data []byte) error { + sws.writeMutex.Lock() + defer sws.writeMutex.Unlock() + wsData, err := json.Marshal(WsMsg{ + Type: WsMsgCmd, + Data: base64.StdEncoding.EncodeToString(data), + }) + if err != nil { + return errors.Wrapf(err, "failed to encoding to json") + } + err = sws.wsConn.WriteMessage(websocket.TextMessage, wsData) + if err != nil { + return errors.Wrapf(err, "failed to write to master") + } + return nil +} + +func (sws *LocalWsSession) receiveWsMsg(exitCh chan bool) { + defer func() { + if r := recover(); r != nil { + global.LOG.Errorf("A panic occurred during receive ws message, error message: %v", r) + } + }() + wsConn := sws.wsConn + defer setQuit(exitCh) + defer global.LOG.Debug("thread of receive ws msg has exited now") + for { + select { + case <-exitCh: + return + default: + _, wsData, err := wsConn.ReadMessage() + if err != nil { + global.LOG.Errorf("reading webSocket message failed, err: %v", err) + return + } + msgObj := WsMsg{} + _ = json.Unmarshal(wsData, &msgObj) + switch msgObj.Type { + case WsMsgResize: + if msgObj.Cols > 0 && msgObj.Rows > 0 { + if err := sws.slave.ResizeTerminal(msgObj.Cols, msgObj.Rows); err != nil { + global.LOG.Errorf("ssh pty change windows size failed, err: %v", err) + } + } + case WsMsgCmd: + decodeBytes, err := base64.StdEncoding.DecodeString(msgObj.Data) + if err != nil { + global.LOG.Errorf("websock cmd string base64 decoding failed, err: %v", err) + } + if string(decodeBytes) != "\x03" || sws.allowCtrlC { + sws.sendWebsocketInputCommandToSshSessionStdinPipe(decodeBytes) + } + case WsMsgHeartbeat: + err = wsConn.WriteMessage(websocket.TextMessage, wsData) + if err != nil { + global.LOG.Errorf("ssh sending heartbeat to webSocket failed, err: %v", err) + } + } + } + } +} + +func (sws *LocalWsSession) sendWebsocketInputCommandToSshSessionStdinPipe(cmdBytes []byte) { + if _, err := sws.slave.Write(cmdBytes); err != nil { + global.LOG.Errorf("ws cmd bytes write to ssh.stdin pipe failed, err: %v", err) + } +} diff --git a/core/utils/terminal/ws_session.go b/core/utils/terminal/ws_session.go new file mode 100644 index 000000000..c278ba947 --- /dev/null +++ b/core/utils/terminal/ws_session.go @@ -0,0 +1,218 @@ +package terminal + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "io" + "sync" + "time" + + "github.com/1Panel-dev/1Panel/core/global" + "github.com/gorilla/websocket" + "golang.org/x/crypto/ssh" +) + +type safeBuffer struct { + buffer bytes.Buffer + mu sync.Mutex +} + +func (w *safeBuffer) Write(p []byte) (int, error) { + w.mu.Lock() + defer w.mu.Unlock() + return w.buffer.Write(p) +} +func (w *safeBuffer) Bytes() []byte { + w.mu.Lock() + defer w.mu.Unlock() + return w.buffer.Bytes() +} +func (w *safeBuffer) Reset() { + w.mu.Lock() + defer w.mu.Unlock() + w.buffer.Reset() +} + +const ( + WsMsgCmd = "cmd" + WsMsgResize = "resize" + WsMsgHeartbeat = "heartbeat" +) + +type WsMsg struct { + Type string `json:"type"` + Data string `json:"data,omitempty"` // WsMsgCmd + Cols int `json:"cols,omitempty"` // WsMsgResize + Rows int `json:"rows,omitempty"` // WsMsgResize + Timestamp int `json:"timestamp,omitempty"` // WsMsgHeartbeat +} + +type LogicSshWsSession struct { + stdinPipe io.WriteCloser + comboOutput *safeBuffer + logBuff *safeBuffer + inputFilterBuff *safeBuffer + session *ssh.Session + wsConn *websocket.Conn + isAdmin bool + IsFlagged bool +} + +func NewLogicSshWsSession(cols, rows int, isAdmin bool, sshClient *ssh.Client, wsConn *websocket.Conn) (*LogicSshWsSession, error) { + sshSession, err := sshClient.NewSession() + if err != nil { + return nil, err + } + + stdinP, err := sshSession.StdinPipe() + if err != nil { + return nil, err + } + + comboWriter := new(safeBuffer) + logBuf := new(safeBuffer) + inputBuf := new(safeBuffer) + sshSession.Stdout = comboWriter + sshSession.Stderr = comboWriter + + modes := ssh.TerminalModes{ + ssh.ECHO: 1, + ssh.TTY_OP_ISPEED: 14400, + ssh.TTY_OP_OSPEED: 14400, + } + if err := sshSession.RequestPty("xterm", rows, cols, modes); err != nil { + return nil, err + } + if err := sshSession.Shell(); err != nil { + return nil, err + } + return &LogicSshWsSession{ + stdinPipe: stdinP, + comboOutput: comboWriter, + logBuff: logBuf, + inputFilterBuff: inputBuf, + session: sshSession, + wsConn: wsConn, + isAdmin: isAdmin, + IsFlagged: false, + }, nil +} + +func (sws *LogicSshWsSession) Close() { + if sws.session != nil { + sws.session.Close() + } + if sws.logBuff != nil { + sws.logBuff = nil + } + if sws.comboOutput != nil { + sws.comboOutput = nil + } +} +func (sws *LogicSshWsSession) Start(quitChan chan bool) { + go sws.receiveWsMsg(quitChan) + go sws.sendComboOutput(quitChan) +} + +func (sws *LogicSshWsSession) receiveWsMsg(exitCh chan bool) { + defer func() { + if r := recover(); r != nil { + global.LOG.Errorf("[A panic occurred during receive ws message, error message: %v", r) + } + }() + wsConn := sws.wsConn + defer setQuit(exitCh) + for { + select { + case <-exitCh: + return + default: + _, wsData, err := wsConn.ReadMessage() + if err != nil { + return + } + msgObj := WsMsg{} + _ = json.Unmarshal(wsData, &msgObj) + switch msgObj.Type { + case WsMsgResize: + if msgObj.Cols > 0 && msgObj.Rows > 0 { + if err := sws.session.WindowChange(msgObj.Rows, msgObj.Cols); err != nil { + global.LOG.Errorf("ssh pty change windows size failed, err: %v", err) + } + } + case WsMsgCmd: + decodeBytes, err := base64.StdEncoding.DecodeString(msgObj.Data) + if err != nil { + global.LOG.Errorf("websock cmd string base64 decoding failed, err: %v", err) + } + sws.sendWebsocketInputCommandToSshSessionStdinPipe(decodeBytes) + case WsMsgHeartbeat: + // 接收到心跳包后将心跳包原样返回,可以用于网络延迟检测等情况 + err = wsConn.WriteMessage(websocket.TextMessage, wsData) + if err != nil { + global.LOG.Errorf("ssh sending heartbeat to webSocket failed, err: %v", err) + } + } + } + } +} + +func (sws *LogicSshWsSession) sendWebsocketInputCommandToSshSessionStdinPipe(cmdBytes []byte) { + if _, err := sws.stdinPipe.Write(cmdBytes); err != nil { + global.LOG.Errorf("ws cmd bytes write to ssh.stdin pipe failed, err: %v", err) + } +} + +func (sws *LogicSshWsSession) sendComboOutput(exitCh chan bool) { + wsConn := sws.wsConn + defer setQuit(exitCh) + + tick := time.NewTicker(time.Millisecond * time.Duration(60)) + defer tick.Stop() + for { + select { + case <-tick.C: + if sws.comboOutput == nil { + return + } + bs := sws.comboOutput.Bytes() + if len(bs) > 0 { + wsData, err := json.Marshal(WsMsg{ + Type: WsMsgCmd, + Data: base64.StdEncoding.EncodeToString(bs), + }) + if err != nil { + global.LOG.Errorf("encoding combo output to json failed, err: %v", err) + continue + } + err = wsConn.WriteMessage(websocket.TextMessage, wsData) + if err != nil { + global.LOG.Errorf("ssh sending combo output to webSocket failed, err: %v", err) + } + _, err = sws.logBuff.Write(bs) + if err != nil { + global.LOG.Errorf("combo output to log buffer failed, err: %v", err) + } + sws.comboOutput.buffer.Reset() + } + if string(bs) == string([]byte{13, 10, 108, 111, 103, 111, 117, 116, 13, 10}) { + sws.Close() + return + } + + case <-exitCh: + return + } + } +} + +func (sws *LogicSshWsSession) Wait(quitChan chan bool) { + if err := sws.session.Wait(); err != nil { + setQuit(quitChan) + } +} + +func setQuit(ch chan bool) { + ch <- true +} diff --git a/core/utils/xpack/xpack.go b/core/utils/xpack/xpack.go index 322a53b00..503a20e5b 100644 --- a/core/utils/xpack/xpack.go +++ b/core/utils/xpack/xpack.go @@ -9,3 +9,11 @@ import ( func Proxy(c *gin.Context, currentNode string) error { return nil } + +func UpdateGroup(name string, group, newGroup uint) error { + return nil +} + +func CheckBackupUsed(id uint) error { + return nil +} diff --git a/frontend/src/api/interface/command.ts b/frontend/src/api/interface/command.ts index fb81dc181..baa1449f9 100644 --- a/frontend/src/api/interface/command.ts +++ b/frontend/src/api/interface/command.ts @@ -1,19 +1,16 @@ export namespace Command { export interface CommandInfo { id: number; + type: string; name: string; groupID: number; command: string; } export interface CommandOperate { id: number; + type: string; name: string; groupID: number; command: string; } - export interface RedisCommand { - id: number; - name: string; - command: string; - } } diff --git a/frontend/src/api/interface/group.ts b/frontend/src/api/interface/group.ts index 4d925361f..728ba9427 100644 --- a/frontend/src/api/interface/group.ts +++ b/frontend/src/api/interface/group.ts @@ -4,6 +4,7 @@ export namespace Group { name: string; type: string; isDefault: boolean; + isDelete: boolean; } export interface GroupCreate { id: number; @@ -15,7 +16,4 @@ export namespace Group { name: string; isDefault: boolean; } - export interface GroupSearch { - type: string; - } } diff --git a/frontend/src/api/modules/backup.ts b/frontend/src/api/modules/backup.ts new file mode 100644 index 000000000..e609345f8 --- /dev/null +++ b/frontend/src/api/modules/backup.ts @@ -0,0 +1,82 @@ +import http from '@/api'; +import { deepCopy } from '@/utils/util'; +import { Base64 } from 'js-base64'; +import { ResPage } from '../interface'; +import { Backup } from '../interface/backup'; +import { TimeoutEnum } from '@/enums/http-enum'; + +// backup-agent +export const handleBackup = (params: Backup.Backup) => { + return http.post(`/backups/backup`, params, TimeoutEnum.T_1H); +}; +export const handleRecover = (params: Backup.Recover) => { + return http.post(`/backups/recover`, params, TimeoutEnum.T_1D); +}; +export const handleRecoverByUpload = (params: Backup.Recover) => { + return http.post(`/backups/recover/byupload`, params, TimeoutEnum.T_1D); +}; +export const downloadBackupRecord = (params: Backup.RecordDownload) => { + return http.post(`/backups/record/download`, params, TimeoutEnum.T_10M); +}; +export const deleteBackupRecord = (params: { ids: number[] }) => { + return http.post(`/backups/record/del`, params); +}; +export const searchBackupRecords = (params: Backup.SearchBackupRecord) => { + return http.post>(`/backups/record/search`, params, TimeoutEnum.T_5M); +}; +export const searchBackupRecordsByCronjob = (params: Backup.SearchBackupRecordByCronjob) => { + return http.post>(`/backups/record/search/bycronjob`, params, TimeoutEnum.T_5M); +}; +export const getFilesFromBackup = (type: string) => { + return http.post>(`/backups/search/files`, { type: type }); +}; + +// backup-core +export const refreshOneDrive = () => { + return http.post(`/core/backup/refresh/onedrive`, {}); +}; +export const getBackupList = () => { + return http.get>(`/core/backup/options`); +}; +export const getLocalBackupDir = () => { + return http.get(`/core/backup/local`); +}; +export const searchBackup = (params: Backup.SearchWithType) => { + return http.post>(`/core/backup/search`, params); +}; +export const getOneDriveInfo = () => { + return http.get(`/core/backup/onedrive`); +}; +export const addBackup = (params: Backup.BackupOperate) => { + let request = deepCopy(params) as Backup.BackupOperate; + if (request.accessKey) { + request.accessKey = Base64.encode(request.accessKey); + } + if (request.credential) { + request.credential = Base64.encode(request.credential); + } + return http.post(`/core/backup`, request, TimeoutEnum.T_60S); +}; +export const editBackup = (params: Backup.BackupOperate) => { + let request = deepCopy(params) as Backup.BackupOperate; + if (request.accessKey) { + request.accessKey = Base64.encode(request.accessKey); + } + if (request.credential) { + request.credential = Base64.encode(request.credential); + } + return http.post(`/core/backup/update`, request); +}; +export const deleteBackup = (params: { id: number }) => { + return http.post(`/core/backup/del`, params); +}; +export const listBucket = (params: Backup.ForBucket) => { + let request = deepCopy(params) as Backup.BackupOperate; + if (request.accessKey) { + request.accessKey = Base64.encode(request.accessKey); + } + if (request.credential) { + request.credential = Base64.encode(request.credential); + } + return http.post(`/core/backup/buckets`, request); +}; diff --git a/frontend/src/api/modules/command.ts b/frontend/src/api/modules/command.ts new file mode 100644 index 000000000..32847127d --- /dev/null +++ b/frontend/src/api/modules/command.ts @@ -0,0 +1,22 @@ +import http from '@/api'; +import { ResPage, SearchWithPage } from '../interface'; +import { Command } from '../interface/command'; + +export const getCommandList = (type: string) => { + return http.post>(`/core/commands/list`, { type: type }); +}; +export const getCommandPage = (params: SearchWithPage) => { + return http.post>(`/core/commands/search`, params); +}; +export const getCommandTree = (type: string) => { + return http.post(`/core/commands/tree`, { type: type }); +}; +export const addCommand = (params: Command.CommandOperate) => { + return http.post(`/core/commands`, params); +}; +export const editCommand = (params: Command.CommandOperate) => { + return http.post(`/core/commands/update`, params); +}; +export const deleteCommand = (params: { ids: number[] }) => { + return http.post(`/core/commands/del`, params); +}; diff --git a/frontend/src/api/modules/group.ts b/frontend/src/api/modules/group.ts index 096f7c1f2..ec09de4e9 100644 --- a/frontend/src/api/modules/group.ts +++ b/frontend/src/api/modules/group.ts @@ -1,15 +1,15 @@ import { Group } from '../interface/group'; import http from '@/api'; -export const GetGroupList = (params: Group.GroupSearch) => { - return http.post>(`/groups/search`, params); +export const GetGroupList = (type: string) => { + return http.post>(`/core/groups/search`, { type: type }); }; export const CreateGroup = (params: Group.GroupCreate) => { - return http.post(`/groups`, params); + return http.post(`/core/groups`, params); }; export const UpdateGroup = (params: Group.GroupUpdate) => { - return http.post(`/groups/update`, params); + return http.post(`/core/groups/update`, params); }; export const DeleteGroup = (id: number) => { - return http.post(`/groups/del`, { id: id }); + return http.post(`/core/groups/del`, { id: id }); }; diff --git a/frontend/src/api/modules/host.ts b/frontend/src/api/modules/host.ts index 9c7eeb667..901d26ffa 100644 --- a/frontend/src/api/modules/host.ts +++ b/frontend/src/api/modules/host.ts @@ -1,90 +1,8 @@ import http from '@/api'; -import { ResPage, SearchWithPage } from '../interface'; -import { Command } from '../interface/command'; +import { ResPage } from '../interface'; import { Host } from '../interface/host'; -import { Base64 } from 'js-base64'; -import { deepCopy } from '@/utils/util'; import { TimeoutEnum } from '@/enums/http-enum'; -export const searchHosts = (params: Host.SearchWithPage) => { - return http.post>(`/hosts/search`, params); -}; -export const getHostTree = (params: Host.ReqSearch) => { - return http.post>(`/hosts/tree`, params); -}; -export const addHost = (params: Host.HostOperate) => { - let request = deepCopy(params) as Host.HostOperate; - if (request.password) { - request.password = Base64.encode(request.password); - } - if (request.privateKey) { - request.privateKey = Base64.encode(request.privateKey); - } - return http.post(`/hosts`, request); -}; -export const testByInfo = (params: Host.HostConnTest) => { - let request = deepCopy(params) as Host.HostOperate; - if (request.password) { - request.password = Base64.encode(request.password); - } - if (request.privateKey) { - request.privateKey = Base64.encode(request.privateKey); - } - return http.post(`/hosts/test/byinfo`, request); -}; -export const testByID = (id: number) => { - return http.post(`/hosts/test/byid/${id}`); -}; -export const editHost = (params: Host.HostOperate) => { - let request = deepCopy(params) as Host.HostOperate; - if (request.password) { - request.password = Base64.encode(request.password); - } - if (request.privateKey) { - request.privateKey = Base64.encode(request.privateKey); - } - return http.post(`/hosts/update`, request); -}; -export const editHostGroup = (params: Host.GroupChange) => { - return http.post(`/hosts/update/group`, params); -}; -export const deleteHost = (params: { ids: number[] }) => { - return http.post(`/hosts/del`, params); -}; - -// command -export const getCommandList = () => { - return http.get>(`/hosts/command`, {}); -}; -export const getCommandPage = (params: SearchWithPage) => { - return http.post>(`/hosts/command/search`, params); -}; -export const getCommandTree = () => { - return http.get(`/hosts/command/tree`); -}; -export const addCommand = (params: Command.CommandOperate) => { - return http.post(`/hosts/command`, params); -}; -export const editCommand = (params: Command.CommandOperate) => { - return http.post(`/hosts/command/update`, params); -}; -export const deleteCommand = (params: { ids: number[] }) => { - return http.post(`/hosts/command/del`, params); -}; - -export const getRedisCommandList = () => { - return http.get>(`/hosts/command/redis`, {}); -}; -export const getRedisCommandPage = (params: SearchWithPage) => { - return http.post>(`/hosts/command/redis/search`, params); -}; -export const saveRedisCommand = (params: Command.RedisCommand) => { - return http.post(`/hosts/command/redis`, params); -}; -export const deleteRedisCommand = (params: { ids: number[] }) => { - return http.post(`/hosts/command/redis/del`, params); -}; - // firewall export const loadFireBaseInfo = () => { return http.get(`/hosts/firewall/base`); diff --git a/frontend/src/api/modules/terminal.ts b/frontend/src/api/modules/terminal.ts new file mode 100644 index 000000000..3c46e12e1 --- /dev/null +++ b/frontend/src/api/modules/terminal.ts @@ -0,0 +1,51 @@ +import http from '@/api'; +import { ResPage } from '../interface'; +import { Host } from '../interface/host'; +import { Base64 } from 'js-base64'; +import { deepCopy } from '@/utils/util'; + +export const searchHosts = (params: Host.SearchWithPage) => { + return http.post>(`/core/hosts/search`, params); +}; +export const getHostTree = (params: Host.ReqSearch) => { + return http.post>(`/core/hosts/tree`, params); +}; +export const addHost = (params: Host.HostOperate) => { + let request = deepCopy(params) as Host.HostOperate; + if (request.password) { + request.password = Base64.encode(request.password); + } + if (request.privateKey) { + request.privateKey = Base64.encode(request.privateKey); + } + return http.post(`/core/hosts`, request); +}; +export const testByInfo = (params: Host.HostConnTest) => { + let request = deepCopy(params) as Host.HostOperate; + if (request.password) { + request.password = Base64.encode(request.password); + } + if (request.privateKey) { + request.privateKey = Base64.encode(request.privateKey); + } + return http.post(`/core/hosts/test/byinfo`, request); +}; +export const testByID = (id: number) => { + return http.post(`/core/hosts/test/byid/${id}`); +}; +export const editHost = (params: Host.HostOperate) => { + let request = deepCopy(params) as Host.HostOperate; + if (request.password) { + request.password = Base64.encode(request.password); + } + if (request.privateKey) { + request.privateKey = Base64.encode(request.privateKey); + } + return http.post(`/core/hosts/update`, request); +}; +export const editHostGroup = (params: Host.GroupChange) => { + return http.post(`/core/hosts/update/group`, params); +}; +export const deleteHost = (params: { ids: number[] }) => { + return http.post(`/core/hosts/del`, params); +}; diff --git a/frontend/src/assets/iconfont/iconfont.css b/frontend/src/assets/iconfont/iconfont.css index e5aedcbc5..fa1de0c3c 100644 --- a/frontend/src/assets/iconfont/iconfont.css +++ b/frontend/src/assets/iconfont/iconfont.css @@ -1,9 +1,9 @@ @font-face { font-family: "panel"; /* Project id 3575356 */ - src: url('iconfont.woff2?t=1717570629440') format('woff2'), - url('iconfont.woff?t=1717570629440') format('woff'), - url('iconfont.ttf?t=1717570629440') format('truetype'), - url('iconfont.svg?t=1717570629440#panel') format('svg'); + src: url('iconfont.woff2?t=1723798525783') format('woff2'), + url('iconfont.woff?t=1723798525783') format('woff'), + url('iconfont.ttf?t=1723798525783') format('truetype'), + url('iconfont.svg?t=1723798525783#panel') format('svg'); } .panel { @@ -14,6 +14,10 @@ -moz-osx-font-smoothing: grayscale; } +.p-zhongduan:before { + content: "\e731"; +} + .p-docker1:before { content: "\e76a"; } @@ -178,18 +182,10 @@ content: "\e856"; } -.p-webdav:before { - content: "\e622"; -} - .p-xiangqing:before { content: "\e677"; } -.p-onedrive:before { - content: "\e601"; -} - .p-caidan:before { content: "\e61d"; } @@ -198,14 +194,6 @@ content: "\e744"; } -.p-tengxunyun1:before { - content: "\e651"; -} - -.p-qiniuyun:before { - content: "\e62c"; -} - .p-file-png:before { content: "\e7ae"; } @@ -250,10 +238,6 @@ content: "\e60f"; } -.p-aws:before { - content: "\e600"; -} - .p-taolun:before { content: "\e602"; } @@ -262,22 +246,10 @@ content: "\e616"; } -.p-SFTP:before { - content: "\e647"; -} - .p-huaban88:before { content: "\e67c"; } -.p-oss:before { - content: "\e607"; -} - -.p-minio:before { - content: "\e63c"; -} - .p-logout:before { content: "\e8fe"; } diff --git a/frontend/src/assets/iconfont/iconfont.js b/frontend/src/assets/iconfont/iconfont.js index e7cd569dd..e980784df 100644 --- a/frontend/src/assets/iconfont/iconfont.js +++ b/frontend/src/assets/iconfont/iconfont.js @@ -1 +1 @@ -window._iconfont_svg_string_3575356='',function(c){var l=(l=document.getElementsByTagName("script"))[l.length-1],a=l.getAttribute("data-injectcss"),l=l.getAttribute("data-disable-injectsvg");if(!l){var h,t,p,z,v,i=function(l,a){a.parentNode.insertBefore(l,a)};if(a&&!c.__iconfont__svg__cssinject__){c.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(l){console&&console.log(l)}}h=function(){var l,a=document.createElement("div");a.innerHTML=c._iconfont_svg_string_3575356,(a=a.getElementsByTagName("svg")[0])&&(a.setAttribute("aria-hidden","true"),a.style.position="absolute",a.style.width=0,a.style.height=0,a.style.overflow="hidden",a=a,(l=document.body).firstChild?i(a,l.firstChild):l.appendChild(a))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(h,0):(t=function(){document.removeEventListener("DOMContentLoaded",t,!1),h()},document.addEventListener("DOMContentLoaded",t,!1)):document.attachEvent&&(p=h,z=c.document,v=!1,d(),z.onreadystatechange=function(){"complete"==z.readyState&&(z.onreadystatechange=null,m())})}function m(){v||(v=!0,p())}function d(){try{z.documentElement.doScroll("left")}catch(l){return void setTimeout(d,50)}m()}}(window); \ No newline at end of file +window._iconfont_svg_string_3575356='',function(c){var l=(l=document.getElementsByTagName("script"))[l.length-1],a=l.getAttribute("data-injectcss"),l=l.getAttribute("data-disable-injectsvg");if(!l){var h,t,p,z,v,i=function(l,a){a.parentNode.insertBefore(l,a)};if(a&&!c.__iconfont__svg__cssinject__){c.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(l){console&&console.log(l)}}h=function(){var l,a=document.createElement("div");a.innerHTML=c._iconfont_svg_string_3575356,(a=a.getElementsByTagName("svg")[0])&&(a.setAttribute("aria-hidden","true"),a.style.position="absolute",a.style.width=0,a.style.height=0,a.style.overflow="hidden",a=a,(l=document.body).firstChild?i(a,l.firstChild):l.appendChild(a))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(h,0):(t=function(){document.removeEventListener("DOMContentLoaded",t,!1),h()},document.addEventListener("DOMContentLoaded",t,!1)):document.attachEvent&&(p=h,z=c.document,v=!1,d(),z.onreadystatechange=function(){"complete"==z.readyState&&(z.onreadystatechange=null,m())})}function m(){v||(v=!0,p())}function d(){try{z.documentElement.doScroll("left")}catch(l){return void setTimeout(d,50)}m()}}(window); \ No newline at end of file diff --git a/frontend/src/assets/iconfont/iconfont.json b/frontend/src/assets/iconfont/iconfont.json index 22ab3cc64..356677049 100644 --- a/frontend/src/assets/iconfont/iconfont.json +++ b/frontend/src/assets/iconfont/iconfont.json @@ -5,6 +5,13 @@ "css_prefix_text": "p-", "description": "", "glyphs": [ + { + "icon_id": "30053592", + "name": "终端", + "font_class": "zhongduan", + "unicode": "e731", + "unicode_decimal": 59185 + }, { "icon_id": "1064806", "name": "docker", @@ -292,13 +299,6 @@ "unicode": "e856", "unicode_decimal": 59478 }, - { - "icon_id": "23044673", - "name": "webdav", - "font_class": "webdav", - "unicode": "e622", - "unicode_decimal": 58914 - }, { "icon_id": "10293150", "name": "详情", @@ -306,13 +306,6 @@ "unicode": "e677", "unicode_decimal": 58999 }, - { - "icon_id": "13015332", - "name": "onedrive", - "font_class": "onedrive", - "unicode": "e601", - "unicode_decimal": 58881 - }, { "icon_id": "7708032", "name": "菜单", @@ -327,20 +320,6 @@ "unicode": "e744", "unicode_decimal": 59204 }, - { - "icon_id": "12959160", - "name": "腾讯云", - "font_class": "tengxunyun1", - "unicode": "e651", - "unicode_decimal": 58961 - }, - { - "icon_id": "24877229", - "name": "七牛云", - "font_class": "qiniuyun", - "unicode": "e62c", - "unicode_decimal": 58924 - }, { "icon_id": "19671162", "name": "png-1", @@ -418,13 +397,6 @@ "unicode": "e60f", "unicode_decimal": 58895 }, - { - "icon_id": "32101973", - "name": "Amazon_Web_Services_Logo", - "font_class": "aws", - "unicode": "e600", - "unicode_decimal": 58880 - }, { "icon_id": "1760690", "name": "讨论", @@ -439,13 +411,6 @@ "unicode": "e616", "unicode_decimal": 58902 }, - { - "icon_id": "13532955", - "name": "SFTP", - "font_class": "SFTP", - "unicode": "e647", - "unicode_decimal": 58951 - }, { "icon_id": "15337722", "name": "Logo GitHub", @@ -453,20 +418,6 @@ "unicode": "e67c", "unicode_decimal": 59004 }, - { - "icon_id": "16268521", - "name": "oss", - "font_class": "oss", - "unicode": "e607", - "unicode_decimal": 58887 - }, - { - "icon_id": "20290513", - "name": "minio", - "font_class": "minio", - "unicode": "e63c", - "unicode_decimal": 58940 - }, { "icon_id": "924436", "name": "logout", diff --git a/frontend/src/assets/iconfont/iconfont.svg b/frontend/src/assets/iconfont/iconfont.svg index 6d72b92c2..e89877662 100644 --- a/frontend/src/assets/iconfont/iconfont.svg +++ b/frontend/src/assets/iconfont/iconfont.svg @@ -14,6 +14,8 @@ /> + + @@ -96,20 +98,12 @@ - - - - - - - - @@ -132,20 +126,12 @@ - - - - - - - - diff --git a/frontend/src/assets/iconfont/iconfont.ttf b/frontend/src/assets/iconfont/iconfont.ttf index 9a67b14c068a9d88e6d32f2bdca2d745863d21f9..13679617121d5670c305ea6b41d3a2422669175a 100644 GIT binary patch delta 2228 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 s?~_gzj~002rv0002000020 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 BtlaEY;R*>002;&0002E0002E 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 HM*si-gCUb+ literal 18856 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 diff --git a/frontend/src/components/group/change.vue b/frontend/src/components/group/change.vue index c87b3c273..4ddc05735 100644 --- a/frontend/src/components/group/change.vue +++ b/frontend/src/components/group/change.vue @@ -72,7 +72,7 @@ const rules = reactive({ }); const loadGroups = async (groupName: string) => { - const res = await GetGroupList({ type: dialogData.value.groupType }); + const res = await GetGroupList(dialogData.value.groupType); groupList.value = res.data; for (const group of groupList.value) { if (group.name === groupName) { diff --git a/frontend/src/components/group/index.vue b/frontend/src/components/group/index.vue index 6b883ab0e..7a1138b77 100644 --- a/frontend/src/components/group/index.vue +++ b/frontend/src/components/group/index.vue @@ -21,7 +21,13 @@ {{ $t('commons.table.default') }} {{ row.name }} - ({{ $t('commons.table.default') }}) + + ({{ $t('commons.table.default') }}) + + + + {{ $t('app.takeDown') }} + @@ -31,6 +37,24 @@ + + + + +