From 2920aa35888b54faa8fd878b021fb213d8c9342b Mon Sep 17 00:00:00 2001 From: ssongliu Date: Wed, 15 Jan 2025 17:57:33 +0800 Subject: [PATCH] feat: Merge code from dev --- agent/app/dto/alert.go | 20 + agent/app/dto/clam.go | 5 + agent/app/dto/cronjob.go | 9 + agent/app/service/clam.go | 70 +- agent/app/service/container.go | 3 +- agent/app/service/cronjob.go | 48 +- agent/app/service/cronjob_helper.go | 17 + agent/app/service/dashboard.go | 5 +- agent/app/service/docker.go | 17 + agent/app/service/file.go | 12 +- agent/app/service/monitor.go | 17 +- agent/constant/alert.go | 11 + agent/constant/errs.go | 10 + agent/i18n/lang/en.yaml | 8 + agent/i18n/lang/zh-Hant.yaml | 12 + agent/i18n/lang/zh.yaml | 10 +- agent/init/hook/hook.go | 39 +- agent/utils/common/common.go | 27 +- agent/utils/files/file_op.go | 3 + agent/utils/files/fileinfo.go | 45 +- agent/utils/files/tar.go | 2 +- agent/utils/files/tar_gz.go | 6 +- agent/utils/websocket/process_data.go | 39 +- agent/utils/xpack/xpack.go | 18 + core/app/api/v2/setting.go | 49 ++ core/app/dto/setting.go | 10 + core/app/service/setting.go | 27 + core/cmd/server/main.go | 19 + core/configs/system.go | 4 + core/constant/errs.go | 20 +- core/i18n/lang/en.yaml | 4 + core/i18n/lang/zh-Hant.yaml | 4 + core/i18n/lang/zh.yaml | 4 + core/init/hook/hook.go | 18 + core/init/migration/migrations/init.go | 12 +- core/middleware/session.go | 58 ++ core/router/ro_setting.go | 2 + frontend/src/api/interface/cronjob.ts | 3 + frontend/src/api/interface/file.ts | 2 +- frontend/src/api/interface/setting.ts | 11 + frontend/src/api/interface/toolbox.ts | 3 + frontend/src/api/modules/files.ts | 2 +- frontend/src/api/modules/setting.ts | 8 + frontend/src/components/file-list/index.vue | 12 +- .../src/components/license-import/index.vue | 6 +- frontend/src/components/log-file/index.vue | 2 +- .../src/components/router-button/index.vue | 5 +- .../src/components/system-upgrade/index.vue | 42 +- .../system-upgrade/upgrade/index.vue | 15 +- frontend/src/components/terminal/index.vue | 6 +- .../components/v-charts/components/Line.vue | 17 +- .../components/v-charts/components/Pie.vue | 16 +- frontend/src/global/form-rules.ts | 19 + frontend/src/global/use-logo.ts | 20 + frontend/src/global/use-theme.ts | 31 +- frontend/src/lang/modules/en.ts | 41 +- frontend/src/lang/modules/tw.ts | 37 +- frontend/src/lang/modules/zh.ts | 34 + .../Sidebar/components/Collapse.vue | 3 +- .../components/Sidebar/components/Logo.vue | 10 +- .../src/layout/components/Sidebar/index.vue | 6 +- frontend/src/layout/index.vue | 2 +- frontend/src/store/interface/index.ts | 2 +- frontend/src/store/modules/global.ts | 9 +- frontend/src/styles/common.scss | 27 +- frontend/src/styles/element-dark.scss | 690 ++++++++++-------- frontend/src/styles/element.scss | 64 +- frontend/src/styles/reset.scss | 18 + frontend/src/utils/theme.ts | 8 + frontend/src/utils/util.ts | 30 +- frontend/src/utils/xpack.ts | 27 +- frontend/src/views/app-store/apps/index.vue | 16 +- frontend/src/views/app-store/detail/index.vue | 6 +- .../views/app-store/installed/check/index.vue | 15 +- .../src/views/container/container/index.vue | 2 +- .../src/views/container/image/pull/index.vue | 5 + frontend/src/views/cronjob/operate/index.vue | 70 +- .../src/views/database/mysql/check/index.vue | 12 +- frontend/src/views/database/mysql/index.vue | 5 +- .../views/database/postgresql/check/index.vue | 10 +- .../src/views/database/postgresql/index.vue | 5 +- .../src/views/database/redis/check/index.vue | 10 +- frontend/src/views/database/redis/index.vue | 5 +- frontend/src/views/home/app/index.vue | 2 +- frontend/src/views/home/index.vue | 2 +- .../file-management/code-editor/index.vue | 7 +- .../host/file-management/delete/index.vue | 16 +- .../src/views/host/file-management/index.vue | 55 +- .../host/file-management/preview/index.vue | 4 +- frontend/src/views/log/operation/index.vue | 10 + .../src/views/login/components/login-form.vue | 35 +- frontend/src/views/setting/about/index.vue | 6 +- frontend/src/views/setting/license/index.vue | 21 +- .../setting/panel/api-interface/index.vue | 175 +++++ frontend/src/views/setting/panel/index.vue | 142 +++- .../src/views/setting/panel/proxy/index.vue | 101 ++- .../views/setting/panel/theme-color/index.vue | 223 ++++++ frontend/src/views/terminal/command/index.vue | 2 +- .../src/views/terminal/terminal/index.vue | 22 +- frontend/src/views/toolbox/clam/index.vue | 2 +- .../src/views/toolbox/clam/operate/index.vue | 56 +- .../views/website/runtime/php/check/index.vue | 14 +- 102 files changed, 2361 insertions(+), 607 deletions(-) create mode 100644 agent/app/dto/alert.go create mode 100644 agent/constant/alert.go create mode 100644 frontend/src/global/use-logo.ts create mode 100644 frontend/src/utils/theme.ts create mode 100644 frontend/src/views/setting/panel/api-interface/index.vue create mode 100644 frontend/src/views/setting/panel/theme-color/index.vue diff --git a/agent/app/dto/alert.go b/agent/app/dto/alert.go new file mode 100644 index 000000000..6cf8d7829 --- /dev/null +++ b/agent/app/dto/alert.go @@ -0,0 +1,20 @@ +package dto + +type CreateOrUpdateAlert struct { + AlertTitle string `json:"alertTitle"` + AlertType string `json:"alertType"` + AlertCount uint `json:"alertCount"` + EntryID uint `json:"entryID"` +} + +type AlertBase struct { + AlertType string `json:"alertType"` + EntryID uint `json:"entryID"` +} + +type PushAlert struct { + TaskName string `json:"taskName"` + AlertType string `json:"alertType"` + EntryID uint `json:"entryID"` + Param string `json:"param"` +} diff --git a/agent/app/dto/clam.go b/agent/app/dto/clam.go index 317d84637..e34ac0d53 100644 --- a/agent/app/dto/clam.go +++ b/agent/app/dto/clam.go @@ -33,6 +33,7 @@ type ClamInfo struct { LastHandleDate string `json:"lastHandleDate"` Spec string `json:"spec"` Description string `json:"description"` + AlertCount uint `json:"alertCount"` } type ClamLogSearch struct { @@ -71,6 +72,8 @@ type ClamCreate struct { InfectedDir string `json:"infectedDir"` Spec string `json:"spec"` Description string `json:"description"` + AlertCount uint `json:"alertCount"` + AlertTitle string `json:"alertTitle"` } type ClamUpdate struct { @@ -82,6 +85,8 @@ type ClamUpdate struct { InfectedDir string `json:"infectedDir"` Spec string `json:"spec"` Description string `json:"description"` + AlertCount uint `json:"alertCount"` + AlertTitle string `json:"alertTitle"` } type ClamUpdateStatus struct { diff --git a/agent/app/dto/cronjob.go b/agent/app/dto/cronjob.go index 0265e5872..a760e916b 100644 --- a/agent/app/dto/cronjob.go +++ b/agent/app/dto/cronjob.go @@ -41,10 +41,14 @@ type CronjobCreate struct { DownloadAccountID uint `json:"downloadAccountID"` RetainCopies int `json:"retainCopies" validate:"number,min=1"` Secret string `json:"secret"` + + AlertCount uint `json:"alertCount"` + AlertTitle string `json:"alertTitle"` } type CronjobUpdate struct { ID uint `json:"id" validate:"required"` + Type string `json:"type" validate:"required"` Name string `json:"name" validate:"required"` SpecCustom bool `json:"specCustom"` Spec string `json:"spec" validate:"required"` @@ -69,6 +73,9 @@ type CronjobUpdate struct { DownloadAccountID uint `json:"downloadAccountID"` RetainCopies int `json:"retainCopies" validate:"number,min=1"` Secret string `json:"secret"` + + AlertCount uint `json:"alertCount"` + AlertTitle string `json:"alertTitle"` } type CronjobUpdateStatus struct { @@ -122,6 +129,8 @@ type CronjobInfo struct { LastRecordTime string `json:"lastRecordTime"` Status string `json:"status"` Secret string `json:"secret"` + + AlertCount uint `json:"alertCount"` } type SearchRecord struct { diff --git a/agent/app/service/clam.go b/agent/app/service/clam.go index 815124d5e..a87d9d887 100644 --- a/agent/app/service/clam.go +++ b/agent/app/service/clam.go @@ -3,15 +3,17 @@ package service import ( "bufio" "fmt" - "github.com/1Panel-dev/1Panel/agent/app/repo" "os" "os/exec" "path" "path/filepath" "sort" + "strconv" "strings" "time" + "github.com/1Panel-dev/1Panel/agent/app/repo" + "github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/buserr" @@ -155,6 +157,16 @@ func (c *ClamService) SearchWithPage(req dto.SearchClamWithPage) (int64, interfa } datas[i].LastHandleDate = t1.Format(constant.DateTimeLayout) } + alertBase := dto.AlertBase{ + AlertType: "clams", + EntryID: datas[i].ID, + } + alertCount := xpack.GetAlert(alertBase) + if alertCount != 0 { + datas[i].AlertCount = alertCount + } else { + datas[i].AlertCount = 0 + } } return total, datas, err } @@ -181,6 +193,18 @@ func (c *ClamService) Create(req dto.ClamCreate) error { if err := clamRepo.Create(&clam); err != nil { return err } + if req.AlertCount != 0 { + createAlert := dto.CreateOrUpdateAlert{ + AlertTitle: req.AlertTitle, + AlertCount: req.AlertCount, + AlertType: "clams", + EntryID: clam.ID, + } + err := xpack.CreateAlert(createAlert) + if err != nil { + return err + } + } return nil } @@ -226,6 +250,16 @@ func (c *ClamService) Update(req dto.ClamUpdate) error { if err := clamRepo.Update(req.ID, upMap); err != nil { return err } + updateAlert := dto.CreateOrUpdateAlert{ + AlertTitle: req.AlertTitle, + AlertType: "clams", + AlertCount: req.AlertCount, + EntryID: clam.ID, + } + err := xpack.UpdateAlert(updateAlert) + if err != nil { + return err + } return nil } @@ -266,6 +300,14 @@ func (c *ClamService) Delete(req dto.ClamDelete) error { if err := clamRepo.Delete(repo.WithByID(id)); err != nil { return err } + alertBase := dto.AlertBase{ + AlertType: "clams", + EntryID: clam.ID, + } + err := xpack.DeleteAlert(alertBase) + if err != nil { + return err + } } return nil } @@ -309,6 +351,7 @@ func (c *ClamService) HandleOnce(req dto.OperateByID) error { if err != nil { global.LOG.Errorf("clamdscan failed, stdout: %v, err: %v", stdout, err) } + handleAlert(stdout, clam.Name, clam.ID) }() return nil } @@ -586,3 +629,28 @@ func (c *ClamService) loadLogPath(name string) string { return "" } + +func handleAlert(stdout, clamName string, clamId uint) { + if strings.Contains(stdout, "- SCAN SUMMARY -") { + lines := strings.Split(stdout, "\n") + for _, line := range lines { + if strings.HasPrefix(line, "Infected files: ") { + var infectedFiles = 0 + infectedFiles, _ = strconv.Atoi(strings.TrimPrefix(line, "Infected files: ")) + if infectedFiles > 0 { + pushAlert := dto.PushAlert{ + TaskName: clamName, + AlertType: "clams", + EntryID: clamId, + Param: strconv.Itoa(infectedFiles), + } + err := xpack.PushAlert(pushAlert) + if err != nil { + global.LOG.Errorf("clamdscan push failed, err: %v", err) + } + break + } + } + } + } +} diff --git a/agent/app/service/container.go b/agent/app/service/container.go index 39f3642a0..e667bc798 100644 --- a/agent/app/service/container.go +++ b/agent/app/service/container.go @@ -812,8 +812,7 @@ func (u *ContainerService) StreamLogs(ctx *gin.Context, params dto.StreamLog) { } return true case err := <-errorChan: - errorMsg := fmt.Sprintf("event: error\ndata: %v\n\n", err.Error()) - _, err = fmt.Fprintf(w, errorMsg) + _, _ = fmt.Fprintf(w, "event: error\ndata: %v\n\n", err.Error()) return false case <-ctx.Request.Context().Done(): return false diff --git a/agent/app/service/cronjob.go b/agent/app/service/cronjob.go index 92967a5f5..8aee0698f 100644 --- a/agent/app/service/cronjob.go +++ b/agent/app/service/cronjob.go @@ -10,6 +10,7 @@ import ( "time" "github.com/1Panel-dev/1Panel/agent/app/repo" + "github.com/1Panel-dev/1Panel/agent/utils/xpack" "github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/model" @@ -58,6 +59,16 @@ func (u *CronjobService) SearchWithPage(search dto.PageCronjob) (int64, interfac item.LastRecordTime = "-" } item.SourceAccounts, item.DownloadAccount, _ = loadBackupNamesByID(cronjob.SourceAccountIDs, cronjob.DownloadAccountID) + alertBase := dto.AlertBase{ + AlertType: cronjob.Type, + EntryID: cronjob.ID, + } + alertCount := xpack.GetAlert(alertBase) + if alertCount != 0 { + item.AlertCount = alertCount + } else { + item.AlertCount = 0 + } dtoCronjobs = append(dtoCronjobs, item) } return total, dtoCronjobs, err @@ -208,6 +219,18 @@ func (u *CronjobService) Create(req dto.CronjobCreate) error { if err := cronjobRepo.Create(&cronjob); err != nil { return err } + if req.AlertCount != 0 { + createAlert := dto.CreateOrUpdateAlert{ + AlertTitle: req.AlertTitle, + AlertCount: req.AlertCount, + AlertType: cronjob.Type, + EntryID: cronjob.ID, + } + err := xpack.CreateAlert(createAlert) + if err != nil { + return err + } + } return nil } @@ -250,6 +273,14 @@ func (u *CronjobService) Delete(req dto.CronjobBatchDelete) error { if err := cronjobRepo.Delete(repo.WithByID(id)); err != nil { return err } + alertBase := dto.AlertBase{ + AlertType: cronjob.Type, + EntryID: cronjob.ID, + } + err := xpack.DeleteAlert(alertBase) + if err != nil { + return err + } } return nil @@ -303,7 +334,21 @@ func (u *CronjobService) Update(id uint, req dto.CronjobUpdate) error { upMap["download_account_id"] = req.DownloadAccountID upMap["retain_copies"] = req.RetainCopies upMap["secret"] = req.Secret - return cronjobRepo.Update(id, upMap) + err = cronjobRepo.Update(id, upMap) + if err != nil { + return err + } + updateAlert := dto.CreateOrUpdateAlert{ + AlertTitle: req.AlertTitle, + AlertType: cronModel.Type, + AlertCount: req.AlertCount, + EntryID: cronModel.ID, + } + err = xpack.UpdateAlert(updateAlert) + if err != nil { + return err + } + return nil } func (u *CronjobService) UpdateStatus(id uint, status string) error { @@ -315,6 +360,7 @@ func (u *CronjobService) UpdateStatus(id uint, status string) error { entryIDs string err error ) + if status == constant.StatusEnable { entryIDs, err = u.StartJob(&cronjob, false) if err != nil { diff --git a/agent/app/service/cronjob_helper.go b/agent/app/service/cronjob_helper.go index 0b8ff5049..1600a1125 100644 --- a/agent/app/service/cronjob_helper.go +++ b/agent/app/service/cronjob_helper.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/app/task" @@ -18,6 +19,7 @@ import ( "github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/files" "github.com/1Panel-dev/1Panel/agent/utils/ntp" + "github.com/1Panel-dev/1Panel/agent/utils/xpack" ) func (u *CronjobService) HandleJob(cronjob *model.Cronjob) { @@ -69,6 +71,7 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) { record.Records, _ = mkdirAndWriteFile(cronjob, record.StartTime, message) } cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records) + handleCronJobAlert(cronjob) return } if len(message) != 0 { @@ -315,3 +318,17 @@ func (u *CronjobService) removeExpiredLog(cronjob model.Cronjob) { func hasBackup(cronjobType string) bool { return cronjobType == "app" || cronjobType == "database" || cronjobType == "website" || cronjobType == "directory" || cronjobType == "snapshot" || cronjobType == "log" } + +func handleCronJobAlert(cronjob *model.Cronjob) { + pushAlert := dto.PushAlert{ + TaskName: cronjob.Name, + AlertType: cronjob.Type, + EntryID: cronjob.ID, + Param: cronjob.Type, + } + err := xpack.PushAlert(pushAlert) + if err != nil { + global.LOG.Errorf("cronjob alert push failed, err: %v", err) + return + } +} diff --git a/agent/app/service/dashboard.go b/agent/app/service/dashboard.go index 75585d68c..9c2bf6fa2 100644 --- a/agent/app/service/dashboard.go +++ b/agent/app/service/dashboard.go @@ -3,7 +3,6 @@ package service import ( "encoding/json" "fmt" - "github.com/1Panel-dev/1Panel/agent/app/repo" network "net" "os" "sort" @@ -11,6 +10,8 @@ import ( "sync" "time" + "github.com/1Panel-dev/1Panel/agent/app/repo" + "github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/constant" @@ -122,7 +123,7 @@ func (u *DashboardService) LoadCurrentInfoForNode() *dto.NodeCurrent { var currentInfo dto.NodeCurrent currentInfo.CPUTotal, _ = cpu.Counts(true) - totalPercent, _ := cpu.Percent(0, false) + totalPercent, _ := cpu.Percent(100*time.Millisecond, false) if len(totalPercent) == 1 { currentInfo.CPUUsedPercent = totalPercent[0] currentInfo.CPUUsed = currentInfo.CPUUsedPercent * 0.01 * float64(currentInfo.CPUTotal) diff --git a/agent/app/service/docker.go b/agent/app/service/docker.go index d995f947b..a716a81cb 100644 --- a/agent/app/service/docker.go +++ b/agent/app/service/docker.go @@ -202,6 +202,23 @@ func (u *DockerService) UpdateConf(req dto.SettingUpdate) error { daemonMap["exec-opts"] = []string{"native.cgroupdriver=systemd"} } } + case "http-proxy", "https-proxy": + delete(daemonMap, "proxies") + if len(req.Value) > 0 { + proxies := map[string]interface{}{ + req.Key: req.Value, + } + daemonMap["proxies"] = proxies + } + case "socks5-proxy", "close-proxy": + delete(daemonMap, "proxies") + if len(req.Value) > 0 { + proxies := map[string]interface{}{ + "http-proxy": req.Value, + "https-proxy": req.Value, + } + daemonMap["proxies"] = proxies + } } if len(daemonMap) == 0 { _ = os.Remove(constant.DaemonJsonPath) diff --git a/agent/app/service/file.go b/agent/app/service/file.go index 3e45347c8..356fa8cda 100644 --- a/agent/app/service/file.go +++ b/agent/app/service/file.go @@ -65,9 +65,13 @@ func NewIFileService() IFileService { func (f *FileService) GetFileList(op request.FileOption) (response.FileInfo, error) { var fileInfo response.FileInfo - if _, err := os.Stat(op.Path); err != nil && os.IsNotExist(err) { + data, err := os.Stat(op.Path) + if err != nil && os.IsNotExist(err) { return fileInfo, nil } + if !data.IsDir() { + op.FileOption.Path = filepath.Dir(op.FileOption.Path) + } info, err := files.NewFileInfo(op.FileOption) if err != nil { return fileInfo, err @@ -210,6 +214,12 @@ func (f *FileService) Create(op request.FileCreate) error { } func (f *FileService) Delete(op request.FileDelete) error { + if op.IsDir { + excludeDir := global.CONF.System.DataDir + if filepath.Base(op.Path) == ".1panel_clash" || op.Path == excludeDir { + return buserr.New(constant.ErrPathNotDelete) + } + } fo := files.NewFileOp() recycleBinStatus, _ := settingRepo.Get(settingRepo.WithByKey("FileRecycleBin")) if recycleBinStatus.Value == "disable" { diff --git a/agent/app/service/monitor.go b/agent/app/service/monitor.go index 69fb2f407..03d19717d 100644 --- a/agent/app/service/monitor.go +++ b/agent/app/service/monitor.go @@ -4,10 +4,11 @@ import ( "context" "encoding/json" "fmt" - "github.com/1Panel-dev/1Panel/agent/app/repo" "strconv" "time" + "github.com/1Panel-dev/1Panel/agent/app/repo" + "github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/constant" @@ -298,16 +299,20 @@ func StartMonitor(removeBefore bool, interval string) error { service := NewIMonitorService() ctx, cancel := context.WithCancel(context.Background()) monitorCancel = cancel - monitorID, err := global.Cron.AddJob(fmt.Sprintf("@every %sm", interval), service) - if err != nil { - return err - } + now := time.Now() + nextMinute := now.Truncate(time.Minute).Add(time.Minute) + time.AfterFunc(time.Until(nextMinute), func() { + monitorID, err := global.Cron.AddJob(fmt.Sprintf("@every %sm", interval), service) + if err != nil { + return + } + global.MonitorCronID = monitorID + }) service.Run() go service.saveIODataToDB(ctx, float64(intervalItem)) go service.saveNetDataToDB(ctx, float64(intervalItem)) - global.MonitorCronID = monitorID return nil } diff --git a/agent/constant/alert.go b/agent/constant/alert.go new file mode 100644 index 000000000..c3efc44b0 --- /dev/null +++ b/agent/constant/alert.go @@ -0,0 +1,11 @@ +package constant + +const ( + AlertEnable = "Enable" + AlertDisable = "Disable" + AlertSuccess = "Success" + AlertError = "Error" + AlertSyncError = "SyncError" + AlertPushError = "PushError" + AlertPushSuccess = "PushSuccess" +) \ No newline at end of file diff --git a/agent/constant/errs.go b/agent/constant/errs.go index 014a3d26c..2fee7f351 100644 --- a/agent/constant/errs.go +++ b/agent/constant/errs.go @@ -85,6 +85,7 @@ var ( ErrFileDownloadDir = "ErrFileDownloadDir" ErrCmdNotFound = "ErrCmdNotFound" ErrFavoriteExist = "ErrFavoriteExist" + ErrPathNotDelete = "ErrPathNotDelete" ) // mysql @@ -142,3 +143,12 @@ var ( var ( ErrNotExistUser = "ErrNotExistUser" ) + +// alert +var ( + ErrAlert = "ErrAlert" + ErrAlertPush = "ErrAlertPush" + ErrAlertSave = "ErrAlertSave" + ErrAlertSync = "ErrAlertSync" + ErrAlertRemote = "ErrAlertRemote" +) diff --git a/agent/i18n/lang/en.yaml b/agent/i18n/lang/en.yaml index a64ae83ea..24e15fc7d 100644 --- a/agent/i18n/lang/en.yaml +++ b/agent/i18n/lang/en.yaml @@ -96,6 +96,7 @@ ErrCmdNotFound: "{{ .name}} command does not exist, please install this command ErrSourcePathNotFound: "Source directory does not exist" ErrFavoriteExist: "This path has been collected" ErrInvalidChar: "Illegal characters are prohibited" +ErrPathNotDelete: "The selected directory cannot be deleted" #website ErrDomainIsExist: "Domain is already exist" @@ -363,3 +364,10 @@ websiteDir: "Website directory" RecoverFailedStartRollBack: "Recovery failed, starting rollback" AppBackupFileIncomplete: "Backup file is incomplete; missing app.json or app.tar.gz file" AppAttributesNotMatch: "Application type or name does not match" + +#alert +ErrAlert: "Alert information format error, please check and try again!" +ErrAlertPush: "Alert push error, please check and try again!" +ErrAlertSave: "Alert save error, please check and try again!" +ErrAlertSync: "Alert sync error, please check and try again!" +ErrAlertRemote: "Remote alert error, please check and try again!" \ No newline at end of file diff --git a/agent/i18n/lang/zh-Hant.yaml b/agent/i18n/lang/zh-Hant.yaml index 747fdc33e..ad3328755 100644 --- a/agent/i18n/lang/zh-Hant.yaml +++ b/agent/i18n/lang/zh-Hant.yaml @@ -4,6 +4,10 @@ ErrRecordExist: "記錄已存在" ErrRecordNotFound: "記錄未找到" ErrStructTransform: "類型轉換失敗: {{ .detail }}" ErrNotSupportType: "系統暫不支持當前類型: {{ .detail }}" +ErrApiConfigStatusInvalid: "API 接口禁止訪問: {{ .detail }}" +ErrApiConfigKeyInvalid: "API 接口密钥錯誤: {{ .detail }}" +ErrApiConfigIPInvalid: "调用 API 接口 IP 不在白名单: {{ .detail }}" +ErrApiConfigDisable: "此接口禁止使用 API 接口調用: {{ .detail }}" #common ErrNameIsExist: "名稱已存在" @@ -96,6 +100,7 @@ ErrFileDownloadDir: "不支持下載文件夾" ErrCmdNotFound: "{{ .name}} 命令不存在,請先在宿主機安裝此命令" ErrSourcePathNotFound: "源目錄不存在" ErrFavoriteExist: "已收藏此路徑" +ErrPathNotDelete: "所選目錄不可删除" #website ErrDomainIsExist: "域名已存在" @@ -364,3 +369,10 @@ websiteDir: "網站目錄" RecoverFailedStartRollBack: "恢復失敗,開始回滾" AppBackupFileIncomplete: "備份文件不完整,缺少 app.json 或 app.tar.gz 文件" AppAttributesNotMatch: "應用類型或名稱不一致" + +# alert +ErrAlert: "告警資訊格式錯誤,請檢查後重試!" +ErrAlertPush: "告警資訊推送錯誤,請檢查後重試!" +ErrAlertSave: "告警資訊保存錯誤,請檢查後重試!" +ErrAlertSync: "告警資訊同步錯誤,請檢查後重試!" +ErrAlertRemote: "告警資訊遠端錯誤,請檢查後重試!" \ No newline at end of file diff --git a/agent/i18n/lang/zh.yaml b/agent/i18n/lang/zh.yaml index 3f94a8675..e1dbdeea2 100644 --- a/agent/i18n/lang/zh.yaml +++ b/agent/i18n/lang/zh.yaml @@ -95,6 +95,7 @@ ErrCmdNotFound: "{{ .name}} 命令不存在,请先在宿主机安装此命令" ErrSourcePathNotFound: "源目录不存在" ErrFavoriteExist: "已收藏此路径" ErrInvalidChar: "禁止使用非法字符" +ErrPathNotDelete: "所选目录不可删除" #website ErrDomainIsExist: "域名已存在" @@ -388,4 +389,11 @@ Rollback: "回滚" websiteDir: "网站目录" RecoverFailedStartRollBack: "恢复失败,开始回滚" AppBackupFileIncomplete: "备份文件不完整 缺少 app.json 或者 app.tar.gz 文件" -AppAttributesNotMatch: "应用类型或者名称不一致" \ No newline at end of file +AppAttributesNotMatch: "应用类型或者名称不一致" + +#alert +ErrAlert: "告警信息格式错误,请检查后重试!" +ErrAlertPush: "告警信息推送错误,请检查后重试!" +ErrAlertSave: "告警信息保存错误,请检查后重试!" +ErrAlertSync: "告警信息同步错误,请检查后重试!" +ErrAlertRemote: "告警信息远端错误,请检查后重试!" \ No newline at end of file diff --git a/agent/init/hook/hook.go b/agent/init/hook/hook.go index eb64f738d..5605b2bac 100644 --- a/agent/init/hook/hook.go +++ b/agent/init/hook/hook.go @@ -3,12 +3,14 @@ package hook import ( "path" + "github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/app/service" "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/utils/common" + "github.com/1Panel-dev/1Panel/agent/utils/xpack" ) func Init() { @@ -58,11 +60,24 @@ func handleSnapStatus() { } func handleCronjobStatus() { - _ = global.DB.Model(&model.JobRecords{}).Where("status = ?", constant.StatusWaiting). - Updates(map[string]interface{}{ - "status": constant.StatusFailed, - "message": "the task was interrupted due to the restart of the 1panel service", - }).Error + var jobRecords []model.JobRecords + _ = global.DB.Where("status = ?", constant.StatusWaiting).Find(&jobRecords).Error + for _, record := range jobRecords { + err := global.DB.Model(&model.JobRecords{}).Where("status = ?", constant.StatusWaiting). + Updates(map[string]interface{}{ + "status": constant.StatusFailed, + "message": "the task was interrupted due to the restart of the 1panel service", + }).Error + + if err != nil { + global.LOG.Errorf("Failed to update job ID: %v, Error:%v", record.ID, err) + continue + } + + var cronjob *model.Cronjob + _ = global.DB.Where("id = ?", record.CronjobID).First(&cronjob).Error + handleCronJobAlert(cronjob) + } } func loadLocalDir() { @@ -73,3 +88,17 @@ func loadLocalDir() { } global.CONF.System.Backup = account.BackupPath } + +func handleCronJobAlert(cronjob *model.Cronjob) { + pushAlert := dto.PushAlert{ + TaskName: cronjob.Name, + AlertType: cronjob.Type, + EntryID: cronjob.ID, + Param: cronjob.Type, + } + err := xpack.PushAlert(pushAlert) + if err != nil { + global.LOG.Errorf("cronjob alert push failed, err: %v", err) + return + } +} diff --git a/agent/utils/common/common.go b/agent/utils/common/common.go index 2b4d3006f..278d4db28 100644 --- a/agent/utils/common/common.go +++ b/agent/utils/common/common.go @@ -3,7 +3,6 @@ package common import ( "crypto/rand" "fmt" - "github.com/gin-gonic/gin" "io" mathRand "math/rand" "net" @@ -15,6 +14,8 @@ import ( "time" "unicode" + "github.com/gin-gonic/gin" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" "golang.org/x/net/idna" ) @@ -325,6 +326,30 @@ func IsValidIP(ip string) bool { return net.ParseIP(ip) != nil } +const ( + b = uint64(1) + kb = 1024 * b + mb = 1024 * kb + gb = 1024 * mb +) + +func FormatBytes(bytes uint64) string { + switch { + case bytes < kb: + return fmt.Sprintf("%dB", bytes) + case bytes < mb: + return fmt.Sprintf("%.2fKB", float64(bytes)/float64(kb)) + case bytes < gb: + return fmt.Sprintf("%.2fMB", float64(bytes)/float64(mb)) + default: + return fmt.Sprintf("%.2fGB", float64(bytes)/float64(gb)) + } +} + +func FormatPercent(percent float64) string { + return fmt.Sprintf("%.2f%%", percent) +} + func GetLang(context *gin.Context) string { lang := context.GetHeader("Accept-Language") if strings.Contains(lang, "zh") { diff --git a/agent/utils/files/file_op.go b/agent/utils/files/file_op.go index 7959ff95c..2e7f1275c 100644 --- a/agent/utils/files/file_op.go +++ b/agent/utils/files/file_op.go @@ -677,6 +677,9 @@ func (f FileOp) decompressWithSDK(srcFile string, dst string, cType CompressType func (f FileOp) Decompress(srcFile string, dst string, cType CompressType, secret string) error { if cType == Tar || cType == Zip || cType == TarGz { shellArchiver, err := NewShellArchiver(cType) + if !f.Stat(dst) { + _ = f.CreateDir(dst, 0755) + } if err == nil { if err = shellArchiver.Extract(srcFile, dst, secret); err == nil { return nil diff --git a/agent/utils/files/fileinfo.go b/agent/utils/files/fileinfo.go index c2e318976..60a0a5ded 100644 --- a/agent/utils/files/fileinfo.go +++ b/agent/utils/files/fileinfo.go @@ -73,6 +73,9 @@ func NewFileInfo(op FileOption) (*FileInfo, error) { info, err := appFs.Stat(op.Path) if err != nil { + if os.IsNotExist(err) { + return nil, buserr.New(constant.ErrLinkPathNotFound) + } return nil, err } @@ -102,7 +105,26 @@ func NewFileInfo(op FileOption) (*FileInfo, error) { } if file.IsSymlink { - file.LinkPath = GetSymlink(op.Path) + linkPath := GetSymlink(op.Path) + if !filepath.IsAbs(linkPath) { + dir := filepath.Dir(op.Path) + var err error + linkPath, err = filepath.Abs(filepath.Join(dir, linkPath)) + if err != nil { + return nil, err + } + } + file.LinkPath = linkPath + targetInfo, err := appFs.Stat(linkPath) + if err != nil { + file.IsDir = false + file.Mode = "-" + file.User = "-" + file.Group = "-" + } else { + file.IsDir = targetInfo.IsDir() + } + file.Extension = filepath.Ext(file.LinkPath) } if op.Expand { if err := handleExpansion(file, op); err != nil { @@ -309,7 +331,26 @@ func (f *FileInfo) processFiles(files []FileSearchInfo, option FileOption) ([]*F file.FavoriteID = favorite.ID } if isSymlink { - file.LinkPath = GetSymlink(fPath) + linkPath := GetSymlink(fPath) + if !filepath.IsAbs(linkPath) { + dir := filepath.Dir(fPath) + var err error + linkPath, err = filepath.Abs(filepath.Join(dir, linkPath)) + if err != nil { + return nil, err + } + } + file.LinkPath = linkPath + targetInfo, err := file.Fs.Stat(linkPath) + if err != nil { + file.IsDir = false + file.Mode = "-" + file.User = "-" + file.Group = "-" + } else { + file.IsDir = targetInfo.IsDir() + } + file.Extension = filepath.Ext(file.LinkPath) } if df.Size() > 0 { file.MimeType = GetMimeType(fPath) diff --git a/agent/utils/files/tar.go b/agent/utils/files/tar.go index 521c989b7..a4b013c55 100644 --- a/agent/utils/files/tar.go +++ b/agent/utils/files/tar.go @@ -19,7 +19,7 @@ func NewTarArchiver(compressType CompressType) ShellArchiver { } func (t TarArchiver) Extract(FilePath string, dstDir string, secret string) error { - return cmd.ExecCmd(fmt.Sprintf("%s %s %s -C %s", t.Cmd, t.getOptionStr("extract"), FilePath, dstDir)) + return cmd.ExecCmd(fmt.Sprintf("%s %s \"%s\" -C \"%s\"", t.Cmd, t.getOptionStr("extract"), FilePath, dstDir)) } func (t TarArchiver) Compress(sourcePaths []string, dstFile string, secret string) error { diff --git a/agent/utils/files/tar_gz.go b/agent/utils/files/tar_gz.go index c0cf1d8f2..e8e95e41a 100644 --- a/agent/utils/files/tar_gz.go +++ b/agent/utils/files/tar_gz.go @@ -20,11 +20,11 @@ func (t TarGzArchiver) Extract(filePath, dstDir string, secret string) error { var err error commands := "" if len(secret) != 0 { - extraCmd := "openssl enc -d -aes-256-cbc -k '" + secret + "' -in " + filePath + " | " - commands = fmt.Sprintf("%s tar -zxvf - -C %s", extraCmd, dstDir+" > /dev/null 2>&1") + extraCmd := fmt.Sprintf("openssl enc -d -aes-256-cbc -k '%s' -in '%s' | ", secret, filePath) + commands = fmt.Sprintf("%s tar -zxvf - -C '%s' > /dev/null 2>&1", extraCmd, dstDir) global.LOG.Debug(strings.ReplaceAll(commands, fmt.Sprintf(" %s ", secret), "******")) } else { - commands = fmt.Sprintf("tar -zxvf %s %s", filePath+" -C ", dstDir+" > /dev/null 2>&1") + commands = fmt.Sprintf("tar -zxvf '%s' -C '%s' > /dev/null 2>&1", filePath, dstDir) global.LOG.Debug(commands) } if err = cmd.ExecCmd(commands); err != nil { diff --git a/agent/utils/websocket/process_data.go b/agent/utils/websocket/process_data.go index b07c741b3..6e470e8c3 100644 --- a/agent/utils/websocket/process_data.go +++ b/agent/utils/websocket/process_data.go @@ -9,6 +9,7 @@ import ( "time" "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/common" "github.com/1Panel-dev/1Panel/agent/utils/files" "github.com/shirou/gopsutil/v3/host" "github.com/shirou/gopsutil/v3/net" @@ -146,26 +147,6 @@ func getDownloadProcess(progress DownloadProgress) (res []byte, err error) { return } -const ( - b = uint64(1) - kb = 1024 * b - mb = 1024 * kb - gb = 1024 * mb -) - -func formatBytes(bytes uint64) string { - switch { - case bytes < kb: - return fmt.Sprintf("%dB", bytes) - case bytes < mb: - return fmt.Sprintf("%.2fKB", float64(bytes)/float64(kb)) - case bytes < gb: - return fmt.Sprintf("%.2fMB", float64(bytes)/float64(mb)) - default: - return fmt.Sprintf("%.2fGB", float64(bytes)/float64(gb)) - } -} - func getProcessData(processConfig PsProcessConfig) (res []byte, err error) { var processes []*process.Process processes, err = process.Processes() @@ -229,14 +210,14 @@ func getProcessData(processConfig PsProcessConfig) (res []byte, err error) { procData.CpuPercent = fmt.Sprintf("%.2f", procData.CpuValue) + "%" menInfo, procErr := proc.MemoryInfo() if procErr == nil { - procData.Rss = formatBytes(menInfo.RSS) + procData.Rss = common.FormatBytes(menInfo.RSS) procData.RssValue = menInfo.RSS - procData.Data = formatBytes(menInfo.Data) - procData.VMS = formatBytes(menInfo.VMS) - procData.HWM = formatBytes(menInfo.HWM) - procData.Stack = formatBytes(menInfo.Stack) - procData.Locked = formatBytes(menInfo.Locked) - procData.Swap = formatBytes(menInfo.Swap) + procData.Data = common.FormatBytes(menInfo.Data) + procData.VMS = common.FormatBytes(menInfo.VMS) + procData.HWM = common.FormatBytes(menInfo.HWM) + procData.Stack = common.FormatBytes(menInfo.Stack) + procData.Locked = common.FormatBytes(menInfo.Locked) + procData.Swap = common.FormatBytes(menInfo.Swap) } else { procData.Rss = "--" procData.Data = "--" @@ -250,8 +231,8 @@ func getProcessData(processConfig PsProcessConfig) (res []byte, err error) { } ioStat, procErr := proc.IOCounters() if procErr == nil { - procData.DiskWrite = formatBytes(ioStat.WriteBytes) - procData.DiskRead = formatBytes(ioStat.ReadBytes) + procData.DiskWrite = common.FormatBytes(ioStat.WriteBytes) + procData.DiskRead = common.FormatBytes(ioStat.ReadBytes) } else { procData.DiskWrite = "--" procData.DiskRead = "--" diff --git a/agent/utils/xpack/xpack.go b/agent/utils/xpack/xpack.go index bd3cbbb29..2dfeedae1 100644 --- a/agent/utils/xpack/xpack.go +++ b/agent/utils/xpack/xpack.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "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" @@ -70,3 +71,20 @@ func GetImagePrefix() string { func IsUseCustomApp() bool { return false } + +// alert +func CreateAlert(createAlert dto.CreateOrUpdateAlert) error { + return nil +} +func UpdateAlert(updateAlert dto.CreateOrUpdateAlert) error { + return nil +} +func DeleteAlert(alertBase dto.AlertBase) error { + return nil +} +func GetAlert(alertBase dto.AlertBase) uint { + return 0 +} +func PushAlert(pushAlert dto.PushAlert) error { + return nil +} diff --git a/core/app/api/v2/setting.go b/core/app/api/v2/setting.go index 163def23a..617cc252f 100644 --- a/core/app/api/v2/setting.go +++ b/core/app/api/v2/setting.go @@ -382,3 +382,52 @@ func (b *BaseApi) ReloadSSL(c *gin.Context) { } helper.SuccessWithOutData(c) } + +// @Tags System Setting +// @Summary generate api key +// @Description 生成 API 接口密钥 +// @Accept json +// @Success 200 +// @Security ApiKeyAuth +// @Router /settings/api/config/generate/key [post] +// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"生成 API 接口密钥","formatEN":"generate api key"} +func (b *BaseApi) GenerateApiKey(c *gin.Context) { + panelToken := c.GetHeader("1Panel-Token") + if panelToken != "" { + helper.ErrorWithDetail(c, constant.CodeErrUnauthorized, constant.ErrApiConfigDisable, nil) + return + } + apiKey, err := settingService.GenerateApiKey() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, apiKey) +} + +// @Tags System Setting +// @Summary Update api config +// @Description 更新 API 接口配置 +// @Accept json +// @Param request body dto.ApiInterfaceConfig true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /settings/api/config/update [post] +// @x-panel-log {"bodyKeys":["ipWhiteList"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 API 接口配置 => IP 白名单: [ipWhiteList]","formatEN":"update api config => IP White List: [ipWhiteList]"} +func (b *BaseApi) UpdateApiConfig(c *gin.Context) { + panelToken := c.GetHeader("1Panel-Token") + if panelToken != "" { + helper.ErrorWithDetail(c, constant.CodeErrUnauthorized, constant.ErrApiConfigDisable, nil) + return + } + var req dto.ApiInterfaceConfig + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := settingService.UpdateApiConfig(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} diff --git a/core/app/dto/setting.go b/core/app/dto/setting.go index 975a953ac..31ba6f0ad 100644 --- a/core/app/dto/setting.go +++ b/core/app/dto/setting.go @@ -42,6 +42,10 @@ type SettingInfo struct { ProxyUser string `json:"proxyUser"` ProxyPasswd string `json:"proxyPasswd"` ProxyPasswdKeep string `json:"proxyPasswdKeep"` + + ApiInterfaceStatus string `json:"apiInterfaceStatus"` + ApiKey string `json:"apiKey"` + IpWhiteList string `json:"ipWhiteList"` } type SettingUpdate struct { @@ -196,6 +200,12 @@ type XpackHideMenu struct { Children []XpackHideMenu `json:"children,omitempty"` } +type ApiInterfaceConfig struct { + ApiInterfaceStatus string `json:"apiInterfaceStatus"` + ApiKey string `json:"apiKey"` + IpWhiteList string `json:"ipWhiteList"` +} + type TerminalInfo struct { LineHeight string `json:"lineHeight"` LetterSpacing string `json:"letterSpacing"` diff --git a/core/app/service/setting.go b/core/app/service/setting.go index 2a0fcba3e..cb86ed41e 100644 --- a/core/app/service/setting.go +++ b/core/app/service/setting.go @@ -38,6 +38,8 @@ type ISettingService interface { UpdateSSL(c *gin.Context, req dto.SSLUpdate) error LoadFromCert() (*dto.SSLInfo, error) HandlePasswordExpired(c *gin.Context, old, new string) error + GenerateApiKey() (string, error) + UpdateApiConfig(req dto.ApiInterfaceConfig) error GetTerminalInfo() (*dto.TerminalInfo, error) UpdateTerminal(req dto.TerminalInfo) error @@ -410,6 +412,31 @@ func (u *SettingService) UpdateSystemSSL() error { return nil } +func (u *SettingService) GenerateApiKey() (string, error) { + apiKey := common.RandStr(32) + if err := settingRepo.Update("ApiKey", apiKey); err != nil { + return global.CONF.System.ApiKey, err + } + global.CONF.System.ApiKey = apiKey + return apiKey, nil +} + +func (u *SettingService) UpdateApiConfig(req dto.ApiInterfaceConfig) error { + if err := settingRepo.Update("ApiInterfaceStatus", req.ApiInterfaceStatus); err != nil { + return err + } + global.CONF.System.ApiInterfaceStatus = req.ApiInterfaceStatus + if err := settingRepo.Update("ApiKey", req.ApiKey); err != nil { + return err + } + global.CONF.System.ApiKey = req.ApiKey + if err := settingRepo.Update("IpWhiteList", req.IpWhiteList); err != nil { + return err + } + global.CONF.System.IpWhiteList = req.IpWhiteList + return nil +} + func loadInfoFromCert() (dto.SSLInfo, error) { var info dto.SSLInfo certFile := path.Join(global.CONF.System.BaseDir, "1panel/secret/server.crt") diff --git a/core/cmd/server/main.go b/core/cmd/server/main.go index 836ed3e9c..8da1807a7 100644 --- a/core/cmd/server/main.go +++ b/core/cmd/server/main.go @@ -18,6 +18,25 @@ import ( // @license.url http://www.apache.org/licenses/LICENSE-2.0.html // @host localhost // @BasePath /api/v2 +// @schemes http https + +// @securityDefinitions.apikey CustomToken +// @description 自定义 Token 格式,格式:md5('1panel' + 1Panel-Token + 1Panel-Timestamp)。 +// @description ``` +// @description 示例请求头: +// @description curl -X GET "http://localhost:4004/api/v1/resource" \ +// @description -H "1Panel-Token: <1panel_token>" \ +// @description -H "1Panel-Timestamp: " +// @description ``` +// @description - `1Panel-Token` 为面板 API 接口密钥。 +// @type apiKey +// @in Header +// @name 1Panel-Token +// @securityDefinitions.apikey Timestamp +// @type apiKey +// @in header +// @name 1Panel-Timestamp +// @description - `1Panel-Timestamp` 为当前时间的 Unix 时间戳(单位:秒)。 func main() { if err := cmd.RootCmd.Execute(); err != nil { diff --git a/core/configs/system.go b/core/configs/system.go index ffcc155f8..523c8784e 100644 --- a/core/configs/system.go +++ b/core/configs/system.go @@ -18,4 +18,8 @@ type System struct { Entrance string `mapstructure:"entrance"` IsDemo bool `mapstructure:"is_demo"` ChangeUserInfo string `mapstructure:"change_user_info"` + + ApiInterfaceStatus string `mapstructure:"api_interface_status"` + ApiKey string `mapstructure:"api_key"` + IpWhiteList string `mapstructure:"ip_white_list"` } diff --git a/core/constant/errs.go b/core/constant/errs.go index d8133192c..b84347912 100644 --- a/core/constant/errs.go +++ b/core/constant/errs.go @@ -23,14 +23,18 @@ const ( // internal var ( - ErrCaptchaCode = errors.New("ErrCaptchaCode") - ErrAuth = errors.New("ErrAuth") - ErrRecordExist = errors.New("ErrRecordExist") - ErrRecordNotFound = errors.New("ErrRecordNotFound") - ErrTransform = errors.New("ErrTransform") - ErrInitialPassword = errors.New("ErrInitialPassword") - ErrInvalidParams = errors.New("ErrInvalidParams") - ErrNotSupportType = errors.New("ErrNotSupportType") + ErrCaptchaCode = errors.New("ErrCaptchaCode") + ErrAuth = errors.New("ErrAuth") + ErrRecordExist = errors.New("ErrRecordExist") + ErrRecordNotFound = errors.New("ErrRecordNotFound") + ErrTransform = errors.New("ErrTransform") + ErrInitialPassword = errors.New("ErrInitialPassword") + ErrInvalidParams = errors.New("ErrInvalidParams") + ErrNotSupportType = errors.New("ErrNotSupportType") + ErrApiConfigStatusInvalid = "ErrApiConfigStatusInvalid" + ErrApiConfigKeyInvalid = "ErrApiConfigKeyInvalid" + ErrApiConfigIPInvalid = "ErrApiConfigIPInvalid" + ErrApiConfigDisable = "ErrApiConfigDisable" ErrTokenParse = errors.New("ErrTokenParse") ErrStructTransform = errors.New("ErrStructTransform") diff --git a/core/i18n/lang/en.yaml b/core/i18n/lang/en.yaml index c0b6d0bd1..30d8b464e 100644 --- a/core/i18n/lang/en.yaml +++ b/core/i18n/lang/en.yaml @@ -9,6 +9,10 @@ ErrNotLogin: "User is not Login: {{ .detail }}" ErrPasswordExpired: "The current password has expired: {{ .detail }}" ErrNotSupportType: "The system does not support the current type: {{ .detail }}" ErrProxy: "Request error, please check the node status: {{ .detail }}" +ErrApiConfigStatusInvalid: "API Interface access prohibited: {{ .detail }}" +ErrApiConfigKeyInvalid: "API Interface key error: {{ .detail }}" +ErrApiConfigIPInvalid: "API Interface IP is not on the whitelist: {{ .detail }}" +ErrApiConfigDisable: "This interface prohibits the use of API Interface calls: {{ .detail }}" #common ErrNameIsExist: "Name is already exist" diff --git a/core/i18n/lang/zh-Hant.yaml b/core/i18n/lang/zh-Hant.yaml index 521bd1aa7..6be502ac3 100644 --- a/core/i18n/lang/zh-Hant.yaml +++ b/core/i18n/lang/zh-Hant.yaml @@ -9,6 +9,10 @@ ErrNotLogin: "用戶未登入: {{ .detail }}" ErrPasswordExpired: "當前密碼已過期: {{ .detail }}" ErrNotSupportType: "系統暫不支持當前類型: {{ .detail }}" ErrProxy: "請求錯誤,請檢查該節點狀態: {{ .detail }}" +ErrApiConfigStatusInvalid: "API 接口禁止訪問: {{ .detail }}" +ErrApiConfigKeyInvalid: "API 接口密钥錯誤: {{ .detail }}" +ErrApiConfigIPInvalid: "调用 API 接口 IP 不在白名单: {{ .detail }}" +ErrApiConfigDisable: "此接口禁止使用 API 接口調用: {{ .detail }}" #common ErrNameIsExist: "名稱已存在" diff --git a/core/i18n/lang/zh.yaml b/core/i18n/lang/zh.yaml index 70061ecb3..3a54cd062 100644 --- a/core/i18n/lang/zh.yaml +++ b/core/i18n/lang/zh.yaml @@ -9,6 +9,10 @@ ErrNotLogin: "用户未登录: {{ .detail }}" ErrPasswordExpired: "当前密码已过期: {{ .detail }}" ErrNotSupportType: "系统暂不支持当前类型: {{ .detail }}" ErrProxy: "请求错误,请检查该节点状态: {{ .detail }}" +ErrApiConfigStatusInvalid: "API 接口禁止访问: {{ .detail }}" +ErrApiConfigKeyInvalid: "API 接口密钥错误: {{ .detail }}" +ErrApiConfigIPInvalid: "调用 API 接口 IP 不在白名单: {{ .detail }}" +ErrApiConfigDisable: "此接口禁止使用 API 接口调用: {{ .detail }}" #common ErrDemoEnvironment: "演示服务器,禁止此操作!" diff --git a/core/init/hook/hook.go b/core/init/hook/hook.go index 7c31fc5c5..8c021f93d 100644 --- a/core/init/hook/hook.go +++ b/core/init/hook/hook.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/1Panel-dev/1Panel/core/app/repo" + "github.com/1Panel-dev/1Panel/core/constant" "github.com/1Panel-dev/1Panel/core/global" "github.com/1Panel-dev/1Panel/core/utils/cmd" "github.com/1Panel-dev/1Panel/core/utils/common" @@ -22,6 +23,23 @@ func Init() { global.LOG.Errorf("load ipv6 status from setting failed, err: %v", err) } global.CONF.System.Ipv6 = ipv6Setting.Value + apiInterfaceStatusSetting, err := settingRepo.Get(repo.WithByKey("ApiInterfaceStatus")) + if err != nil { + global.LOG.Errorf("load service api interface from setting failed, err: %v", err) + } + global.CONF.System.ApiInterfaceStatus = apiInterfaceStatusSetting.Value + if apiInterfaceStatusSetting.Value == constant.StatusEnable { + apiKeySetting, err := settingRepo.Get(repo.WithByKey("ApiKey")) + if err != nil { + global.LOG.Errorf("load service api key from setting failed, err: %v", err) + } + global.CONF.System.ApiKey = apiKeySetting.Value + ipWhiteListSetting, err := settingRepo.Get(repo.WithByKey("IpWhiteList")) + if err != nil { + global.LOG.Errorf("load service ip white list from setting failed, err: %v", err) + } + global.CONF.System.IpWhiteList = ipWhiteListSetting.Value + } bindAddressSetting, err := settingRepo.Get(repo.WithByKey("BindAddress")) if err != nil { global.LOG.Errorf("load bind address from setting failed, err: %v", err) diff --git a/core/init/migration/migrations/init.go b/core/init/migration/migrations/init.go index 7e7d0ce7e..5d89548bf 100644 --- a/core/init/migration/migrations/init.go +++ b/core/init/migration/migrations/init.go @@ -89,7 +89,8 @@ var InitSetting = &gormigrate.Migration{ if err := tx.Create(&model.Setting{Key: "PrsoxyPasswdKeep", Value: ""}).Error; err != nil { return err } - if err := tx.Create(&model.Setting{Key: "XpackHideMenu", Value: "{\"id\":\"1\",\"label\":\"/xpack\",\"isCheck\":true,\"title\":\"xpack.menu\",\"children\":[{\"id\":\"2\",\"title\":\"xpack.waf.name\",\"path\":\"/xpack/waf/dashboard\",\"label\":\"Dashboard\",\"isCheck\":true},{\"id\":\"3\",\"title\":\"xpack.tamper.tamper\",\"path\":\"/xpack/tamper\",\"label\":\"Tamper\",\"isCheck\":true},{\"id\":\"4\",\"title\":\"xpack.gpu.gpu\",\"path\":\"/xpack/gpu\",\"label\":\"GPU\",\"isCheck\":true},{\"id\":\"5\",\"title\":\"xpack.setting.setting\",\"path\":\"/xpack/setting\",\"label\":\"XSetting\",\"isCheck\":true},{\"id\":\"6\",\"title\":\"xpack.monitor.name\",\"path\":\"/xpack/monitor/dashboard\",\"label\":\"MonitorDashboard\",\"isCheck\":true},{\"id\":\"7\",\"title\":\"xpack.node.nodeManagement\",\"path\":\"/xpack/node\",\"label\":\"Node\",\"isCheck\":true}]}"}).Error; err != nil { + val := `{"id":"1","label":"/xpack","isCheck":true,"title":"xpack.menu","children":[{"id":"2","label":"Dashboard","isCheck":true,"title":"xpack.waf.name","path":"/xpack/waf/dashboard"},{"id":"3","label":"Tamper","isCheck":true,"title":"xpack.tamper.tamper","path":"/xpack/tamper"},{"id":"4","label":"GPU","isCheck":true,"title":"xpack.gpu.gpu","path":"/xpack/gpu"},{"id":"5","label":"XSetting","isCheck":true,"title":"xpack.setting.setting","path":"/xpack/setting"},{"id":"6","label":"MonitorDashboard","isCheck":true,"title":"xpack.monitor.name","path":"/xpack/monitor/dashboard"},{"id":"7","label":"XAlertDashboard","isCheck":true,"title":"xpack.alert.alert","path":"/xpack/alert/dashboard"},{"id":"8","label":"Node","isCheck":true,"title":"xpack.node.nodeManagement","path":"/xpack/node"}]}` + if err := tx.Create(&model.Setting{Key: "XpackHideMenu", Value: val}).Error; err != nil { return err } @@ -144,6 +145,15 @@ var InitSetting = &gormigrate.Migration{ if err := tx.Create(&model.Setting{Key: "NoAuthSetting", Value: "200"}).Error; err != nil { return err } + if err := tx.Create(&model.Setting{Key: "ApiInterfaceStatus", Value: "disable"}).Error; err != nil { + return err + } + if err := tx.Create(&model.Setting{Key: "ApiKey", Value: ""}).Error; err != nil { + return err + } + if err := tx.Create(&model.Setting{Key: "IpWhiteList", Value: ""}).Error; err != nil { + return err + } return nil }, } diff --git a/core/middleware/session.go b/core/middleware/session.go index f301d21e1..5d920127e 100644 --- a/core/middleware/session.go +++ b/core/middleware/session.go @@ -1,6 +1,9 @@ package middleware import ( + "crypto/md5" + "encoding/hex" + "net" "strconv" "strings" @@ -21,6 +24,29 @@ func SessionAuth() gin.HandlerFunc { c.Next() return } + + panelToken := c.GetHeader("1Panel-Token") + panelTimestamp := c.GetHeader("1Panel-Timestamp") + if panelToken != "" || panelTimestamp != "" { + if global.CONF.System.ApiInterfaceStatus == "enable" { + clientIP := c.ClientIP() + if !isValid1PanelToken(panelToken, panelTimestamp) { + helper.ErrorWithDetail(c, constant.CodeErrUnauthorized, constant.ErrApiConfigKeyInvalid, nil) + return + } + + if !isIPInWhiteList(clientIP) { + helper.ErrorWithDetail(c, constant.CodeErrUnauthorized, constant.ErrApiConfigIPInvalid, nil) + return + } + c.Next() + return + } else { + helper.ErrorWithDetail(c, constant.CodeErrUnauthorized, constant.ErrApiConfigStatusInvalid, nil) + return + } + } + psession, err := global.SESSION.Get(c) if err != nil { helper.ErrorWithDetail(c, constant.CodeErrUnauthorized, constant.ErrTypeNotLogin, err) @@ -42,3 +68,35 @@ func SessionAuth() gin.HandlerFunc { c.Next() } } + +func isValid1PanelToken(panelToken string, panelTimestamp string) bool { + system1PanelToken := global.CONF.System.ApiKey + return GenerateMD5("1panel"+panelToken+panelTimestamp) == GenerateMD5("1panel"+system1PanelToken+panelTimestamp) +} + +func isIPInWhiteList(clientIP string) bool { + ipWhiteString := global.CONF.System.IpWhiteList + ipWhiteList := strings.Split(ipWhiteString, "\n") + for _, cidr := range ipWhiteList { + if cidr == "0.0.0.0" { + return true + } + _, ipNet, err := net.ParseCIDR(cidr) + if err != nil { + if cidr == clientIP { + return true + } + continue + } + if ipNet.Contains(net.ParseIP(clientIP)) { + return true + } + } + return false +} + +func GenerateMD5(input string) string { + hash := md5.New() + hash.Write([]byte(input)) + return hex.EncodeToString(hash.Sum(nil)) +} diff --git a/core/router/ro_setting.go b/core/router/ro_setting.go index d3225d599..479f240e1 100644 --- a/core/router/ro_setting.go +++ b/core/router/ro_setting.go @@ -41,6 +41,8 @@ func (s *SettingRouter) InitRouter(Router *gin.RouterGroup) { settingRouter.POST("/upgrade", baseApi.Upgrade) settingRouter.POST("/upgrade/notes", baseApi.GetNotesByVersion) settingRouter.GET("/upgrade", baseApi.GetUpgradeInfo) + settingRouter.POST("/api/config/generate/key", baseApi.GenerateApiKey) + settingRouter.POST("/api/config/update", baseApi.UpdateApiConfig) noAuthRouter.POST("/ssl/reload", baseApi.ReloadSSL) } diff --git a/frontend/src/api/interface/cronjob.ts b/frontend/src/api/interface/cronjob.ts index 5da4b1749..21a218e72 100644 --- a/frontend/src/api/interface/cronjob.ts +++ b/frontend/src/api/interface/cronjob.ts @@ -34,6 +34,9 @@ export namespace Cronjob { retainCopies: number; status: string; secret: string; + hasAlert: boolean; + alertCount: number; + alertTitle: string; } export interface Item { val: string; diff --git a/frontend/src/api/interface/file.ts b/frontend/src/api/interface/file.ts index d99c1a602..ac8e7827b 100644 --- a/frontend/src/api/interface/file.ts +++ b/frontend/src/api/interface/file.ts @@ -11,7 +11,7 @@ export namespace File { size: number; isDir: boolean; isSymlink: boolean; - linkPath: boolean; + linkPath: string; type: string; updateTime: string; modTime: string; diff --git a/frontend/src/api/interface/setting.ts b/frontend/src/api/interface/setting.ts index 14d881298..15adc5ccf 100644 --- a/frontend/src/api/interface/setting.ts +++ b/frontend/src/api/interface/setting.ts @@ -57,6 +57,10 @@ export namespace Setting { proxyUser: string; proxyPasswd: string; proxyPasswdKeep: string; + + apiInterfaceStatus: string; + apiKey: string; + ipWhiteList: string; } export interface TerminalInfo { lineHeight: string; @@ -79,6 +83,11 @@ export namespace Setting { proxyPasswd: string; proxyPasswdKeep: string; } + export interface ApiConfig { + apiInterfaceStatus: string; + apiKey: string; + ipWhiteList: string; + } export interface SSLUpdate { ssl: string; domain: string; @@ -211,6 +220,8 @@ export namespace Setting { trial: boolean; status: string; message: string; + smsUsed: number; + smsTotal: number; } export interface LicenseStatus { productPro: string; diff --git a/frontend/src/api/interface/toolbox.ts b/frontend/src/api/interface/toolbox.ts index dd94a17ba..3d8535477 100644 --- a/frontend/src/api/interface/toolbox.ts +++ b/frontend/src/api/interface/toolbox.ts @@ -139,6 +139,9 @@ export namespace Toolbox { spec: string; specObj: Cronjob.SpecObj; description: string; + hasAlert: boolean; + alertCount: number; + alertTitle: string; } export interface ClamCreate { name: string; diff --git a/frontend/src/api/modules/files.ts b/frontend/src/api/modules/files.ts index 4b958bd0b..0804604ab 100644 --- a/frontend/src/api/modules/files.ts +++ b/frontend/src/api/modules/files.ts @@ -74,7 +74,7 @@ export const WgetFile = (params: File.FileWget) => { }; export const MoveFile = (params: File.FileMove) => { - return http.post('files/move', params); + return http.post('files/move', params, TimeoutEnum.T_5M); }; export const DownloadFile = (params: File.FileDownload) => { diff --git a/frontend/src/api/modules/setting.ts b/frontend/src/api/modules/setting.ts index 6d7b18d2b..5cf33a883 100644 --- a/frontend/src/api/modules/setting.ts +++ b/frontend/src/api/modules/setting.ts @@ -150,3 +150,11 @@ export const loadReleaseNotes = (version: string) => { export const upgrade = (version: string) => { return http.post(`/core/settings/upgrade`, { version: version }); }; + +// api config +export const generateApiKey = () => { + return http.post(`/core/settings/api/config/generate/key`); +}; +export const updateApiConfig = (param: Setting.ApiConfig) => { + return http.post(`/core/settings/api/config/update`, param); +}; diff --git a/frontend/src/components/file-list/index.vue b/frontend/src/components/file-list/index.vue index 34e8fbd0b..4866010b9 100644 --- a/frontend/src/components/file-list/index.vue +++ b/frontend/src/components/file-list/index.vue @@ -141,7 +141,7 @@ const data = ref([]); const loading = ref(false); const paths = ref([]); const req = reactive({ path: '/', expand: true, page: 1, pageSize: 300, showHidden: true }); -const selectRow = ref(); +const selectRow = ref({ path: '', name: '' }); const rowRefs = ref(); const popoverVisible = ref(false); const newFolder = ref(); @@ -183,12 +183,12 @@ const selectFile = () => { const closePage = () => { popoverVisible.value = false; - selectRow.value = {}; + selectRow.value = { path: '', name: '' }; }; const openPage = () => { popoverVisible.value = true; - selectRow.value = {}; + selectRow.value.path = props.dir ? props.path || '/' : ''; rowName.value = ''; }; @@ -216,7 +216,7 @@ const open = async (row: File.File) => { } await search(req); } - selectRow.value = {}; + selectRow.value.path = props.dir ? req.path : ''; rowName.value = ''; }; @@ -230,7 +230,7 @@ const jump = async (index: number) => { } path = path || '/'; req.path = path; - selectRow.value = {}; + selectRow.value.path = props.dir ? req.path : ''; rowName.value = ''; await search(req); popoverVisible.value = true; @@ -286,7 +286,7 @@ const cancelFolder = (row: any) => { data.value.shift(); row.isCreate = false; disBtn.value = false; - selectRow.value = {}; + selectRow.value.path = props.dir ? req.path : ''; rowName.value = ''; newFolder.value = ''; }; diff --git a/frontend/src/components/license-import/index.vue b/frontend/src/components/license-import/index.vue index bf5b74768..957166e32 100644 --- a/frontend/src/components/license-import/index.vue +++ b/frontend/src/components/license-import/index.vue @@ -48,7 +48,7 @@ import { UploadFileData } from '@/api/modules/setting'; import { GlobalStore } from '@/store'; import { UploadFile, UploadFiles, UploadInstance, UploadProps, UploadRawFile, genFileId } from 'element-plus'; import { useTheme } from '@/global/use-theme'; -import { getXpackSetting } from '@/utils/xpack'; +import { getXpackSetting, initFavicon } from '@/utils/xpack'; const globalStore = GlobalStore(); const { switchTheme } = useTheme(); @@ -90,10 +90,12 @@ const submit = async () => { globalStore.isProductPro = true; const xpackRes = await getXpackSetting(); if (xpackRes) { - globalStore.themeConfig.isGold = xpackRes.data.theme === 'dark-gold'; + globalStore.themeConfig.theme = xpackRes.data.theme; + globalStore.themeConfig.themeColor = xpackRes.data.themeColor; } loading.value = false; switchTheme(); + initFavicon(); uploadRef.value!.clearFiles(); uploaderFiles.value = []; open.value = false; diff --git a/frontend/src/components/log-file/index.vue b/frontend/src/components/log-file/index.vue index 024a43f50..f4d81b8f8 100644 --- a/frontend/src/components/log-file/index.vue +++ b/frontend/src/components/log-file/index.vue @@ -322,7 +322,7 @@ defineExpose({ changeTail, onDownload, clearLog }); overflow-y: auto; overflow-x: auto; position: relative; - background-color: #1e1e1e; + background-color: var(--panel-logs-bg-color); margin-top: 10px; } diff --git a/frontend/src/components/router-button/index.vue b/frontend/src/components/router-button/index.vue index 0e31e3193..5c2adfb83 100644 --- a/frontend/src/components/router-button/index.vue +++ b/frontend/src/components/router-button/index.vue @@ -94,8 +94,9 @@ onMounted(() => { } .el-radio-button__original-radio:checked + .el-radio-button__inner { - color: $primary-color; - border-color: $primary-color !important; + color: var(--panel-button-text-color) !important; + background-color: var(--panel-button-bg-color) !important; + border-color: var(--panel-color-primary) !important; border-radius: 4px; } } diff --git a/frontend/src/components/system-upgrade/index.vue b/frontend/src/components/system-upgrade/index.vue index 983371423..175ffe279 100644 --- a/frontend/src/components/system-upgrade/index.vue +++ b/frontend/src/components/system-upgrade/index.vue @@ -2,39 +2,39 @@
- + {{ $t('setting.forum') }} - + - + {{ $t('setting.doc2') }} - + - + {{ $t('setting.project') }} - +
- - - {{ isMasterProductPro ? $t('license.pro') : $t('license.community') }} - - - {{ version }} - - - ({{ $t('setting.hasNewVersion') }}) - + + {{ isMasterProductPro ? $t('license.pro') : $t('license.community') }} + + + {{ version }} + + + + ({{ $t('setting.hasNewVersion') }}) + - - ({{ $t('setting.upgradeCheck') }}) - + ({{ $t('setting.upgradeCheck') }}) + {{ $t('setting.upgrading') }} @@ -129,7 +129,7 @@ onMounted(() => { diff --git a/frontend/src/components/terminal/index.vue b/frontend/src/components/terminal/index.vue index 8137f3664..4a361bc46 100644 --- a/frontend/src/components/terminal/index.vue +++ b/frontend/src/components/terminal/index.vue @@ -75,12 +75,13 @@ const acceptParams = (props: WsProps) => { }; const newTerm = () => { + const background = getComputedStyle(document.documentElement).getPropertyValue('--panel-terminal-bg-color').trim(); term.value = new Terminal({ lineHeight: 1.2, fontSize: 12, fontFamily: "Monaco, Menlo, Consolas, 'Courier New', monospace", theme: { - background: '#000000', + background: background, }, cursorBlink: true, cursorStyle: 'underline', @@ -251,4 +252,7 @@ onBeforeUnmount(() => { width: 100%; height: 100%; } +:deep(.xterm) { + padding: 5px !important; +} diff --git a/frontend/src/components/v-charts/components/Line.vue b/frontend/src/components/v-charts/components/Line.vue index 0a408257f..0907d4283 100644 --- a/frontend/src/components/v-charts/components/Line.vue +++ b/frontend/src/components/v-charts/components/Line.vue @@ -30,10 +30,24 @@ const props = defineProps({ option: { type: Object, required: true, - }, // option: { title , xData, yData, formatStr, yAxis, grid, tooltip} + }, }); const seriesStyle = [ + { + color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ + { + offset: 0, + color: getComputedStyle(document.documentElement).getPropertyValue('--panel-color-primary').trim(), + }, + { + offset: 1, + color: getComputedStyle(document.documentElement) + .getPropertyValue('--panel-color-primary-light-9') + .trim(), + }, + ]), + }, { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { @@ -98,6 +112,7 @@ function initChart() { series.push({ name: item?.name, type: 'line', + itemStyle: seriesStyle[index + 2], areaStyle: seriesStyle[index], data: item?.data, showSymbol: false, diff --git a/frontend/src/components/v-charts/components/Pie.vue b/frontend/src/components/v-charts/components/Pie.vue index 693bc046c..145f5eb91 100644 --- a/frontend/src/components/v-charts/components/Pie.vue +++ b/frontend/src/components/v-charts/components/Pie.vue @@ -7,7 +7,7 @@ import * as echarts from 'echarts'; import { GlobalStore } from '@/store'; import { storeToRefs } from 'pinia'; const globalStore = GlobalStore(); -const { isDarkGoldTheme, isDarkTheme } = storeToRefs(globalStore); +const { isDarkTheme } = storeToRefs(globalStore); const props = defineProps({ id: { @@ -25,7 +25,7 @@ const props = defineProps({ option: { type: Object, required: true, - }, // option: { title , data } + }, }); function initChart() { @@ -34,6 +34,12 @@ function initChart() { myChart = echarts.init(document.getElementById(props.id) as HTMLElement); } let percentText = String(props.option.data).split('.'); + const primaryLight2 = getComputedStyle(document.documentElement) + .getPropertyValue('--panel-color-primary-light-3') + .trim(); + const primaryLight1 = getComputedStyle(document.documentElement).getPropertyValue('--panel-color-primary').trim(); + const pieBgColor = getComputedStyle(document.documentElement).getPropertyValue('--panel-pie-bg-color').trim(); + const option = { title: [ { @@ -99,11 +105,11 @@ function initChart() { new echarts.graphic.LinearGradient(0, 1, 0, 0, [ { offset: 0, - color: isDarkGoldTheme.value ? '#836c4c' : 'rgba(81, 192, 255, .1)', + color: primaryLight2, }, { offset: 1, - color: isDarkGoldTheme.value ? '#eaba63' : '#4261F6', + color: primaryLight1, }, ]), ], @@ -119,7 +125,7 @@ function initChart() { label: { show: false, }, - color: isDarkTheme.value ? '#16191D' : '#fff', + color: pieBgColor, data: [ { value: 0, diff --git a/frontend/src/global/form-rules.ts b/frontend/src/global/form-rules.ts index 842b9e1a2..e725d2e69 100644 --- a/frontend/src/global/form-rules.ts +++ b/frontend/src/global/form-rules.ts @@ -557,6 +557,19 @@ const checkHttpOrHttps = (rule, value, callback) => { } }; +const checkPhone = (rule: any, value: any, callback: any) => { + if (value === '' || typeof value === 'undefined' || value == null) { + callback(); + } else { + const reg = /^(?:(?:\+|00)86)?1[3-9]\d{9}$/; + if (!reg.test(value) && value !== '') { + callback(new Error(i18n.global.t('commons.rule.phone'))); + } else { + callback(); + } + } +}; + interface CommonRule { requiredInput: FormItemRule; requiredSelect: FormItemRule; @@ -602,6 +615,7 @@ interface CommonRule { paramExtUrl: FormItemRule; paramSimple: FormItemRule; paramHttp: FormItemRule; + phone: FormItemRule; } export const Rules: CommonRule = { @@ -828,4 +842,9 @@ export const Rules: CommonRule = { validator: checkDomainOrIP, trigger: 'blur', }, + phone: { + validator: checkPhone, + required: true, + trigger: 'blur', + }, }; diff --git a/frontend/src/global/use-logo.ts b/frontend/src/global/use-logo.ts new file mode 100644 index 000000000..7b8ef7099 --- /dev/null +++ b/frontend/src/global/use-logo.ts @@ -0,0 +1,20 @@ +import { GlobalStore } from '@/store'; +import { getXpackSetting } from '@/utils/xpack'; + +export const useLogo = async () => { + const globalStore = GlobalStore(); + const res = await getXpackSetting(); + if (res) { + localStorage.setItem('1p-favicon', res.data.logo); + globalStore.themeConfig.title = res.data.title; + globalStore.themeConfig.logo = res.data.logo; + globalStore.themeConfig.logoWithText = res.data.logoWithText; + globalStore.themeConfig.favicon = res.data.favicon; + } + + const link = (document.querySelector("link[rel*='icon']") || document.createElement('link')) as HTMLLinkElement; + link.type = 'image/x-icon'; + link.rel = 'shortcut icon'; + link.href = globalStore.themeConfig.favicon ? `/api/v1/images/favicon?t=${Date.now()}` : '/public/favicon.png'; + document.getElementsByTagName('head')[0].appendChild(link); +}; diff --git a/frontend/src/global/use-theme.ts b/frontend/src/global/use-theme.ts index 9f97edd02..38ab4e0c6 100644 --- a/frontend/src/global/use-theme.ts +++ b/frontend/src/global/use-theme.ts @@ -1,22 +1,29 @@ import { GlobalStore } from '@/store'; +import { setPrimaryColor } from '@/utils/theme'; export const useTheme = () => { - const globalStore = GlobalStore(); const switchTheme = () => { - if (globalStore.themeConfig.isGold && globalStore.isMasterProductPro) { - const body = document.documentElement as HTMLElement; - body.setAttribute('class', 'dark-gold'); - return; + const globalStore = GlobalStore(); + const themeConfig = globalStore.themeConfig; + let itemTheme = themeConfig.theme; + if (itemTheme === 'auto') { + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + itemTheme = prefersDark ? 'dark' : 'light'; } + document.documentElement.className = itemTheme === 'dark' ? 'dark' : 'light'; + if (globalStore.isProductPro && themeConfig.themeColor) { + try { + const themeColor = JSON.parse(themeConfig.themeColor); + const color = itemTheme === 'dark' ? themeColor.dark : themeColor.light; - let itemTheme = globalStore.themeConfig.theme; - if (globalStore.themeConfig.theme === 'auto') { - const prefersDark = window.matchMedia('(prefers-color-scheme: dark)'); - itemTheme = prefersDark.matches ? 'dark' : 'light'; + if (color) { + themeConfig.primary = color; + setPrimaryColor(color); + } + } catch (e) { + console.error('Failed to parse themeColor', e); + } } - const body = document.documentElement as HTMLElement; - if (itemTheme === 'dark') body.setAttribute('class', 'dark'); - else body.setAttribute('class', ''); }; return { diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index e909d8c12..76bbbdb41 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -235,8 +235,7 @@ const message = { formatErr: 'Format error, please check and retry', phpExtension: 'Only supports , _ lowercase English and numbers', paramHttp: 'Must start with http:// or https://', - diffHelper: - 'The left side is the old version, the right side is the new version, after editing, click Save using custom version', + phone: 'The format of the phone number is incorrect', }, res: { paramError: 'The request failed, please try again later!', @@ -981,6 +980,7 @@ const message = { requestExpirationTime: 'Upload Request Expiration Time(Hours)', unitHours: 'Unit: Hours', + alertTitle: 'Planned Task - {0} 「{1}」 Task Failure Alert', }, monitor: { monitor: 'Monitor', @@ -1195,6 +1195,8 @@ const message = { clamLog: 'Scan Logs', freshClam: 'Update Virus Definitions', freshClamLog: 'Update Virus Definitions Logs', + alertHelper: 'Professional version supports scheduled scan and SMS alert', + alertTitle: 'Virus scan task 「{0}」 detected infected file alert', }, }, logs: { @@ -1233,6 +1235,14 @@ const message = { taskName: 'Task Name', taskRunning: 'Running', }, + alert: { + isAlert: 'Alert', + alertCount: 'Alert Count', + clamHelper: 'Trigger SMS alert when scanning infected files', + cronJobHelper: 'Trigger SMS alert when scheduled task execution fails', + licenseHelper: 'Professional version supports SMS alert', + alertCountHelper: 'Maximum daily alarm frequency', + }, file: { dir: 'Folder', upload: 'Upload', @@ -1353,6 +1363,8 @@ const message = { noNameFolder: 'Untitled Folder', noNameFile: 'Untitled File', minimap: 'Code Mini Map', + fileCanNotRead: 'File can not read', + panelInstallDir: '1Panel installation directory cannot be deleted', }, ssh: { autoStart: 'Auto Start', @@ -1449,10 +1461,34 @@ const message = { proxyHelper1: 'Downloading and synchronizing installation packages from the app store (Professional)', proxyHelper2: 'System version upgrades and retrieving update information (Professional)', proxyHelper3: 'Verification and synchronization of system licenses', + proxyHelper4: 'Docker network access will be done through a proxy server (Professional)', proxyType: 'Proxy Type', proxyUrl: 'Proxy Address', proxyPort: 'Proxy Port', proxyPasswdKeep: 'Remember Password', + proxyDocker: 'Docker Proxy', + proxyDockerHelper: + 'Synchronize proxy server configuration to Docker, support offline server image pulling and other operations', + apiInterface: 'API Interface', + apiInterfaceClose: 'Once closed, API interfaces cannot be accessed. Do you want to continue?', + apiInterfaceHelper: 'Provide panel support for API interface access', + apiInterfaceAlert1: + 'Please do not enable it in production environments as it may increase server security risks', + apiInterfaceAlert2: + 'Please do not use third-party applications to call the panel API to prevent potential security threats.', + apiInterfaceAlert3: 'API Interface Document:', + apiInterfaceAlert4: 'Usage Document:', + apiKey: 'Interface Key', + apiKeyHelper: 'Interface key is used for external applications to access API interfaces', + ipWhiteList: 'IP Whitelist', + ipWhiteListEgs: + 'When there are multiple IPs, line breaks are required for display, for example: \n172.161.10.111 \n172.161.10.0/24 ', + ipWhiteListHelper: 'IPs must be in the IP whitelist list to access the panel API interface', + apiKeyReset: 'Interface key reset', + apiKeyResetHelper: 'the associated key service will become invalid. Please add a new key to the service', + confDockerProxy: 'Configure Docker Proxy', + restartNowHelper: 'Configuring Docker proxy requires restarting the Docker service.', + restartNow: 'Restart immediately', systemIPWarning: 'The server address is not currently set. Please set it in the control panel first!', systemIPWarning1: 'The current server address is set to {0}, and quick redirection is not possible!', syncTime: 'Server Time', @@ -1821,6 +1857,7 @@ const message = { 'Upgrading to the professional version allows customization of panel logo, welcome message, and other information.', monitor: 'Upgrade to the professional version to view the real-time status of the website, visitor trends, visitor sources, request logs and other information. ', + alert: 'Upgrade to the professional version to receive alarm information via SMS and view alarm logs, fully control various key events, and ensure worry-free system operation', node: 'Upgrading to the professional version allows you to manage multiple Linux servers with 1Panel.', }, clean: { diff --git a/frontend/src/lang/modules/tw.ts b/frontend/src/lang/modules/tw.ts index ba43d9d01..d382cc9f5 100644 --- a/frontend/src/lang/modules/tw.ts +++ b/frontend/src/lang/modules/tw.ts @@ -232,6 +232,7 @@ const message = { formatErr: '格式錯誤,檢查後重試', phpExtension: '僅支持 , _ 小寫英文和數字', paramHttp: '必須以 http:// 或 https:// 開頭', + phone: '手機號碼格式不正確', }, res: { paramError: '請求失敗,請稍後重試!', @@ -934,6 +935,7 @@ const message = { requestExpirationTime: '上傳請求過期時間(小時)', unitHours: '單位:小時', + alertTitle: '計畫任務-{0}「{1}」任務失敗告警', }, monitor: { monitor: '監控', @@ -1131,6 +1133,8 @@ const message = { clamLog: '掃描日誌', freshClam: '病毒庫刷新配置', freshClamLog: '病毒庫刷新日誌', + alertHelper: '專業版支持定時掃描和短信告警功能', + alertTitle: '病毒掃描「{0}」任務检测到感染文件告警', }, }, logs: { @@ -1169,6 +1173,14 @@ const message = { taskName: '任務名稱', taskRunning: '運行中', }, + alert: { + isAlert: '是否告警', + alertCount: '告警次數', + clamHelper: '掃描到感染檔案時觸發簡訊告警', + cronJobHelper: '定時任務執行失敗時將觸發簡訊告警', + licenseHelper: '專業版支持簡訊告警功能', + alertCountHelper: '每日最大告警次數', + }, file: { dir: '文件夾', upload: '上傳', @@ -1285,6 +1297,8 @@ const message = { noNameFolder: '未命名資料夾', noNameFile: '未命名檔案', minimap: '縮略圖', + fileCanNotRead: '此文件不支持預覽', + panelInstallDir: '1Panel 安裝目錄不能删除', }, ssh: { autoStart: '開機自啟', @@ -1373,10 +1387,30 @@ const message = { proxyHelper1: '應用商店的安裝包下載和同步(專業版功能)', proxyHelper2: '系統版本升級及獲取更新說明(專業版功能)', proxyHelper3: '系統許可證的驗證和同步', + proxyHelper4: 'Docker 的網絡訪問將通過代理伺服器進行(專業版功能)', proxyType: '代理類型', proxyUrl: '代理地址', proxyPort: '代理端口', proxyPasswdKeep: '記住密碼', + proxyDocker: 'Docker 代理', + proxyDockerHelper: '將代理伺服器配寘同步至 Docker,支持離線服務器拉取鏡像等操作', + apiInterface: 'API 接口', + apiInterfaceClose: '關閉後將不能使用 API 接口進行訪問,是否繼續?', + apiInterfaceHelper: '提供面板支持 API 接口訪問', + apiInterfaceAlert1: '請不要在生產環境開啟,這可能新增服務器安全風險', + apiInterfaceAlert2: '請不要使用協力廠商應用調用面板 API,以防止潜在的安全威脅。', + apiInterfaceAlert3: 'API 接口檔案:', + apiInterfaceAlert4: '使用檔案:', + apiKey: '接口密钥', + apiKeyHelper: '接口密钥用於外部應用訪問 API 接口', + ipWhiteList: 'IP白名單', + ipWhiteListEgs: '當存在多個 IP 時,需要換行顯示,例:\n172.16.10.111 \n172.16.10.0/24', + ipWhiteListHelper: '必需在 IP 白名單清單中的 IP 才能訪問面板 API 接口', + apiKeyReset: '接口密钥重置', + apiKeyResetHelper: '重置密钥後,已關聯密钥服務將失效,請重新添加新密鑰至服務。', + confDockerProxy: '配寘 Docker 代理', + restartNowHelper: '配寘 Docker 代理需要重啓 Docker 服務。', + restartNow: '立即重啓', systemIPWarning: '當前未設置服務器地址,請先在面板設置中設置!', systemIPWarning1: '當前服務器地址設置為 {0},無法快速跳轉!', changePassword: '密碼修改', @@ -1691,6 +1725,7 @@ const message = { gpu: '升級專業版可以幫助用戶實時直觀查看到 GPU 的工作負載、溫度、顯存等重要參數。', setting: '升級專業版可以自定義面板 Logo、歡迎簡介等信息。', monitor: '升級專業版可以查看網站的即時狀態、訪客趨勢、訪客來源、請求日誌等資訊。 ', + alert: '陞級專業版可通過簡訊接收告警資訊,並查看告警日誌,全面掌控各類關鍵事件,確保系統運行無憂。', node: '升級專業版可以使用 1Panel 管理多台 linux 伺服器。', }, clean: { @@ -2274,7 +2309,7 @@ const message = { domainHelper: '一行一個網域名稱,支援*和IP位址', pushDir: '推送憑證到本機目錄', dir: '目錄', - pushDirHelper: '會在此目錄下產生兩個文件,憑證檔案:fullchain.pem 金鑰檔案:privkey.pem', + pushDirHelper: '會在此目錄下產生兩個文件,憑證檔案:fullchain.pem 密钥檔案:privkey.pem', organizationDetail: '機構詳情', fromWebsite: '從網站獲取', dnsMauanlHelper: '手動解析模式需要在建立完之後點選申請按鈕取得 DNS 解析值', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 53faf8740..a0b4690ae 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -232,6 +232,7 @@ const message = { formatErr: '格式错误,检查后重试', phpExtension: '仅支持 , _ 小写英文和数字', paramHttp: '必须以 http:// 或 https:// 开头', + phone: '手机号码格式不正确', }, res: { paramError: '请求失败,请稍后重试!', @@ -934,6 +935,7 @@ const message = { requestExpirationTime: '上传请求过期时间(小时)', unitHours: '单位:小时', + alertTitle: '计划任务-{0}「 {1} 」任务失败告警', }, monitor: { monitor: '监控', @@ -1132,6 +1134,8 @@ const message = { clamLog: '扫描日志', freshClam: '病毒库刷新配置', freshClamLog: '病毒库刷新日志', + alertHelper: '专业版支持定时扫描和短信告警功能', + alertTitle: '病毒扫描「 {0} 」任务检测到感染文件告警', }, }, logs: { @@ -1170,6 +1174,14 @@ const message = { taskName: '任务名称', taskRunning: '执行中', }, + alert: { + isAlert: '是否告警', + alertCount: '告警次数', + clamHelper: '扫描到感染文件时触发短信告警', + cronJobHelper: '定时任务执行失败时将触发短信告警', + licenseHelper: '专业版支持短信告警功能', + alertCountHelper: '每日最大告警次数', + }, file: { dir: '文件夹', upload: '上传', @@ -1286,6 +1298,8 @@ const message = { noNameFolder: '未命名文件夹', noNameFile: '未命名文件', minimap: '缩略图', + fileCanNotRead: '此文件不支持预览', + panelInstallDir: '1Panel 安装目录不能删除', }, ssh: { autoStart: '开机自启', @@ -1374,10 +1388,29 @@ const message = { proxyHelper1: '应用商店的安装包下载和同步(专业版功能)', proxyHelper2: '系统版本升级及获取更新说明(专业版功能)', proxyHelper3: '系统许可证的验证和同步', + proxyHelper4: 'Docker 的网络访问将通过代理服务器进行(专业版功能)', proxyType: '代理类型', proxyUrl: '代理地址', proxyPort: '代理端口', proxyPasswdKeep: '记住密码', + proxyDocker: 'Docker 代理', + apiInterface: 'API 接口', + apiInterfaceClose: '关闭后将不能使用 API 接口进行访问,是否继续?', + apiInterfaceHelper: '提供面板支持 API 接口访问', + apiInterfaceAlert1: '请不要在生产环境开启,这可能增加服务器安全风险', + apiInterfaceAlert2: '请不要使用第三方应用调用面板 API,以防止潜在的安全威胁。', + apiInterfaceAlert3: 'API 接口文档:', + apiInterfaceAlert4: '使用文档:', + apiKey: '接口密钥', + apiKeyHelper: '接口密钥用于外部应用访问 API 接口', + ipWhiteList: 'IP 白名单', + ipWhiteListEgs: '当存在多个 IP 时,需要换行显示,例: \n172.16.10.111 \n172.16.10.0/24', + ipWhiteListHelper: '必需在 IP 白名单列表中的 IP 才能访问面板 API 接口', + apiKeyReset: '接口密钥重置', + apiKeyResetHelper: '重置密钥后,已关联密钥服务将失效,请重新添加新密钥至服务。', + confDockerProxy: '配置 Docker 代理', + restartNowHelper: '配置 Docker 代理需要重启 Docker 服务。', + restartNow: '立即重启', systemIPWarning: '当前未设置服务器地址,请先在面板设置中设置!', systemIPWarning1: '当前服务器地址设置为 {0},无法快速跳转!', changePassword: '密码修改', @@ -1691,6 +1724,7 @@ const message = { gpu: '升级专业版可以帮助用户实时直观查看到 GPU 的工作负载、温度、显存等重要参数。', setting: '升级专业版可以自定义面板 Logo、欢迎简介等信息。', monitor: '升级专业版可以查看网站的实时状态、访客趋势、访客来源、请求日志等信息。', + alert: '升级专业版可通过短信接收告警信息,并查看告警日志,全面掌控各类关键事件,确保系统运行无忧。', node: '升级专业版可以使用 1Panel 管理多台 linux 服务器。', }, clean: { diff --git a/frontend/src/layout/components/Sidebar/components/Collapse.vue b/frontend/src/layout/components/Sidebar/components/Collapse.vue index 449762654..77d84494d 100644 --- a/frontend/src/layout/components/Sidebar/components/Collapse.vue +++ b/frontend/src/layout/components/Sidebar/components/Collapse.vue @@ -18,11 +18,12 @@ const isCollapse = computed(() => menuStore.isCollapse); display: flex; align-items: center; box-sizing: border-box; - border-top: 1px solid #e4e7ed; + border-top: 1px solid var(--panel-footer-border); height: 48px; } .collapse-icon { + color: var(--panel-main-bg-color-1); margin-left: 25px; &:hover { color: $primary-color; diff --git a/frontend/src/layout/components/Sidebar/components/Logo.vue b/frontend/src/layout/components/Sidebar/components/Logo.vue index 8ea024b3c..7aaa35ad2 100644 --- a/frontend/src/layout/components/Sidebar/components/Logo.vue +++ b/frontend/src/layout/components/Sidebar/components/Logo.vue @@ -1,13 +1,18 @@ @@ -522,8 +548,16 @@ import { weekOptions, } from './../helper'; import { loadUsers } from '@/api/modules/toolbox'; +import { storeToRefs } from 'pinia'; +import { GlobalStore } from '@/store'; +import LicenseImport from '@/components/license-import/index.vue'; const router = useRouter(); +const globalStore = GlobalStore(); +const licenseRef = ref(); +const { isProductPro } = storeToRefs(globalStore); +const alertTypes = ['app', 'website', 'database', 'directory', 'log', 'snapshot']; + interface DialogProps { title: string; rowData?: Cronjob.CronjobInfo; @@ -563,13 +597,6 @@ const acceptParams = (params: DialogProps): void => { dialogData.value.rowData.dbType = 'mysql'; dialogData.value.rowData.isDir = true; } - if (dialogData.value.rowData.sourceAccountIDs) { - dialogData.value.rowData.sourceAccounts = []; - let itemIDs = dialogData.value.rowData.sourceAccountIDs.split(','); - for (const item of itemIDs) { - dialogData.value.rowData.sourceAccounts.push(Number(item)); - } - } dialogData.value.rowData!.command = dialogData.value.rowData!.command || 'sh'; dialogData.value.rowData!.isCustom = dialogData.value.rowData!.command !== 'sh' && @@ -732,6 +759,17 @@ const verifyFiles = (rule: any, value: any, callback: any) => { callback(); }; +const checkSendCount = (rule: any, value: any, callback: any) => { + if (value === '') { + callback(); + } + const regex = /^(?:[1-9]|[12][0-9]|30)$/; + if (!regex.test(value)) { + return callback(new Error(i18n.global.t('commons.rule.numberRange', [1, 30]))); + } + callback(); +}; + const rules = reactive({ name: [Rules.requiredInput, Rules.noSpace], type: [Rules.requiredSelect], @@ -750,6 +788,7 @@ const rules = reactive({ sourceAccounts: [Rules.requiredSelect], downloadAccountID: [Rules.requiredSelect], retainCopies: [Rules.number], + alertCount: [Rules.integerNumber, { validator: checkSendCount, trigger: 'blur' }], }); type FormInstance = InstanceType; @@ -980,7 +1019,18 @@ const onSubmit = async (formEl: FormInstance | undefined) => { if (dialogData.value?.rowData?.exclusionRules) { dialogData.value.rowData.exclusionRules = dialogData.value.rowData.exclusionRules.replaceAll('\n', ','); } + + dialogData.value.rowData.alertCount = + dialogData.value.rowData!.hasAlert && isProductPro.value ? dialogData.value.rowData.alertCount : 0; + dialogData.value.rowData.alertTitle = + dialogData.value.rowData!.hasAlert && isProductPro.value + ? i18n.global.t('cronjob.alertTitle', [ + i18n.global.t('cronjob.' + dialogData.value.rowData.type), + dialogData.value.rowData.name, + ]) + : ''; if (!dialogData.value.rowData) return; + if (dialogData.value.title === 'create') { await addCronjob(dialogData.value.rowData); } @@ -994,6 +1044,10 @@ const onSubmit = async (formEl: FormInstance | undefined) => { }); }; +const toUpload = () => { + licenseRef.value.acceptParams(); +}; + defineExpose({ acceptParams, }); diff --git a/frontend/src/views/database/mysql/check/index.vue b/frontend/src/views/database/mysql/check/index.vue index ee30ba25f..79d68e42e 100644 --- a/frontend/src/views/database/mysql/check/index.vue +++ b/frontend/src/views/database/mysql/check/index.vue @@ -4,18 +4,20 @@
- - + + - {{ installData.join(',') }} +
{{ installData.join('\n') }}
diff --git a/frontend/src/views/database/mysql/index.vue b/frontend/src/views/database/mysql/index.vue index df4fd52f2..f8c7e18e5 100644 --- a/frontend/src/views/database/mysql/index.vue +++ b/frontend/src/views/database/mysql/index.vue @@ -236,11 +236,12 @@
- +
+ {{ $t('app.checkInstalledWarn', [dashboardName]) }} {{ $t('database.goInstall') }} - +