diff --git a/agent/app/api/v2/alert.go b/agent/app/api/v2/alert.go index 5a5d76b8d67f..6686261c20b6 100644 --- a/agent/app/api/v2/alert.go +++ b/agent/app/api/v2/alert.go @@ -2,11 +2,16 @@ package v2 import ( "errors" + "net/url" + "strings" + "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" "github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/gin-gonic/gin" ) +const defaultAuditUser = "system" + // @Tags Alert // @Summary Page alert // @Accept json @@ -54,7 +59,7 @@ func (b *BaseApi) CreateAlert(c *gin.Context) { if err := helper.CheckBindAndValidate(&req, c); err != nil { return } - err := alertService.CreateAlert(req) + err := alertService.CreateAlert(req, loadAuditUser(c)) if err != nil { helper.InternalServer(c, err) return @@ -98,7 +103,7 @@ func (b *BaseApi) UpdateAlert(c *gin.Context) { if err := helper.CheckBindAndValidate(&req, c); err != nil { return } - if err := alertService.UpdateAlert(req); err != nil { + if err := alertService.UpdateAlert(req, loadAuditUser(c)); err != nil { helper.InternalServer(c, err) return } @@ -246,6 +251,30 @@ func (b *BaseApi) GetAlertConfig(c *gin.Context) { helper.SuccessWithData(c, config) } +// @Tags Alert +// @Summary Page alert config +// @Accept json +// @Param request body dto.PageInfo true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /alert/config/search [post] +func (b *BaseApi) PageAlertConfig(c *gin.Context) { + var req dto.PageInfo + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + total, configs, err := alertService.PageAlertConfig(req) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, dto.PageResult{ + Total: total, + Items: configs, + }) +} + // @Tags Alert // @Summary Update alert config // @Accept json @@ -260,13 +289,24 @@ func (b *BaseApi) UpdateAlertConfig(c *gin.Context) { if err := helper.CheckBindAndValidate(&req, c); err != nil { return } - if err := alertService.UpdateAlertConfig(req); err != nil { + if err := alertService.UpdateAlertConfig(req, loadAuditUser(c)); err != nil { helper.InternalServer(c, err) return } helper.Success(c) } +func loadAuditUser(c *gin.Context) string { + userName := strings.TrimSpace(c.GetHeader("X-Panel-User")) + if userName == "" { + return defaultAuditUser + } + if decoded, err := url.QueryUnescape(userName); err == nil { + return decoded + } + return userName +} + // @Tags Alert // @Summary Delete alert config // @Accept json diff --git a/agent/app/api/v2/clam.go b/agent/app/api/v2/clam.go index d6c59a63efa3..68585d4a0d8c 100644 --- a/agent/app/api/v2/clam.go +++ b/agent/app/api/v2/clam.go @@ -21,7 +21,7 @@ func (b *BaseApi) CreateClam(c *gin.Context) { return } - if err := clamService.Create(req); err != nil { + if err := clamService.Create(req, loadAuditUser(c)); err != nil { helper.InternalServer(c, err) return } @@ -43,7 +43,7 @@ func (b *BaseApi) UpdateClam(c *gin.Context) { return } - if err := clamService.Update(req); err != nil { + if err := clamService.Update(req, loadAuditUser(c)); err != nil { helper.InternalServer(c, err) return } diff --git a/agent/app/api/v2/cronjob.go b/agent/app/api/v2/cronjob.go index 45e2a481f8ee..8d02a6b0ea83 100644 --- a/agent/app/api/v2/cronjob.go +++ b/agent/app/api/v2/cronjob.go @@ -26,7 +26,7 @@ func (b *BaseApi) CreateCronjob(c *gin.Context) { return } - if err := cronjobService.Create(req); err != nil { + if err := cronjobService.Create(req, loadAuditUser(c)); err != nil { helper.InternalServer(c, err) return } @@ -91,7 +91,7 @@ func (b *BaseApi) ImportCronjob(c *gin.Context) { return } - if err := cronjobService.Import(req.Cronjobs); err != nil { + if err := cronjobService.Import(req.Cronjobs, loadAuditUser(c)); err != nil { helper.InternalServer(c, err) return } @@ -285,7 +285,7 @@ func (b *BaseApi) UpdateCronjob(c *gin.Context) { return } - if err := cronjobService.Update(req.ID, req); err != nil { + if err := cronjobService.Update(req.ID, req, loadAuditUser(c)); err != nil { helper.InternalServer(c, err) return } diff --git a/agent/app/dto/alert.go b/agent/app/dto/alert.go index 2facd238f987..f04a6d24adb8 100644 --- a/agent/app/dto/alert.go +++ b/agent/app/dto/alert.go @@ -46,6 +46,8 @@ type AlertDTO struct { Status string `json:"status"` SendCount uint `json:"sendCount"` AdvancedParams string `json:"advancedParams"` + CreateUser string `json:"createUser"` + UpdateUser string `json:"updateUser"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } diff --git a/agent/app/model/alert.go b/agent/app/model/alert.go index 64d528ba9548..560965a76c05 100644 --- a/agent/app/model/alert.go +++ b/agent/app/model/alert.go @@ -9,9 +9,11 @@ type Alert struct { Count uint `gorm:"type:integer;not null" json:"count"` Project string `gorm:"type:varchar(64)" json:"project"` Status string `gorm:"type:varchar(64);not null" json:"status"` - Method string `gorm:"type:varchar(64);not null" json:"method"` + Method string `gorm:"type:text;not null" json:"method"` SendCount uint `gorm:"type:integer" json:"sendCount"` AdvancedParams string `gorm:"type:longText" json:"advancedParams"` + CreateUser string `gorm:"type:varchar(256)" json:"createUser"` + UpdateUser string `gorm:"type:varchar(256)" json:"updateUser"` } type AlertTask struct { @@ -19,7 +21,7 @@ type AlertTask struct { Type string `gorm:"type:varchar(64);not null" json:"type"` Quota string `gorm:"type:varchar(64)" json:"quota"` QuotaType string `gorm:"type:varchar(64)" json:"quotaType"` - Method string `gorm:"type:varchar(64);not null;default:'sms'" json:"method"` + Method string `gorm:"type:varchar(128);not null;default:'sms'" json:"method"` } type AlertLog struct { @@ -34,15 +36,17 @@ type AlertLog struct { Message string `gorm:"type:varchar(256);" json:"message"` RecordId uint `gorm:"type:integer;" json:"recordId"` LicenseId string `gorm:"type:varchar(256);not null;" json:"licenseId" ` - Method string `gorm:"type:varchar(64);not null;default:'sms'" json:"method"` + Method string `gorm:"type:varchar(128);not null;default:'sms'" json:"method"` } type AlertConfig struct { BaseModel - Type string `gorm:"type:varchar(64);not null" json:"type"` - Title string `gorm:"type:varchar(64);not null" json:"title"` - Status string `gorm:"type:varchar(64);not null" json:"status"` - Config string `gorm:"type:varchar(256);not null" json:"config"` + Type string `gorm:"type:varchar(64);not null" json:"type"` + Title string `gorm:"type:varchar(64);not null" json:"title"` + Status string `gorm:"type:varchar(64);not null" json:"status"` + Config string `gorm:"type:varchar(256);not null" json:"config"` + CreateUser string `gorm:"type:varchar(256)" json:"createUser"` + UpdateUser string `gorm:"type:varchar(256)" json:"updateUser"` } type LoginLog struct { diff --git a/agent/app/repo/alert.go b/agent/app/repo/alert.go index c1ea1c2c60f6..a39de6446867 100644 --- a/agent/app/repo/alert.go +++ b/agent/app/repo/alert.go @@ -6,6 +6,7 @@ import ( "github.com/1Panel-dev/1Panel/agent/global" "google.golang.org/genproto/googleapis/type/date" "gorm.io/gorm" + "strconv" "time" ) @@ -21,6 +22,7 @@ type IAlertRepo interface { WithByLicenseId(licenseId string) DBOption WithByRecordId(recordId uint) DBOption WithByMethod(method string) DBOption + WithByMethodConfigID(id uint) DBOption Create(alert *model.Alert) error Get(opts ...DBOption) (model.Alert, error) @@ -47,11 +49,15 @@ type IAlertRepo interface { GetLicensePushCount(method string) (uint, error) GetConfig(opts ...DBOption) (model.AlertConfig, error) + GetConfigById(id uint) (model.AlertConfig, error) AlertConfigList(opts ...DBOption) ([]model.AlertConfig, error) UpdateAlertConfig(maps map[string]interface{}, opts ...DBOption) error CreateAlertConfig(config *model.AlertConfig) error DeleteAlertConfig(opts ...DBOption) error + WithByTypeNotIn(types []string) DBOption + PageAlertConfig(page, size int, opts ...DBOption) (int64, []model.AlertConfig, error) + SyncAll(data []model.AlertConfig) error } @@ -103,7 +109,14 @@ func (a *AlertRepo) WithByRecordId(recordId uint) DBOption { func (a *AlertRepo) WithByMethod(method string) DBOption { return func(g *gorm.DB) *gorm.DB { - return g.Where("method = ?", method) + return g.Where("(method = ? OR method LIKE ? OR method LIKE ? OR method LIKE ?)", method, method+",%", "%,"+method, "%,"+method+",%") + } +} + +func (a *AlertRepo) WithByMethodConfigID(id uint) DBOption { + method := strconv.Itoa(int(id)) + return func(g *gorm.DB) *gorm.DB { + return g.Where("(method = ? OR method LIKE ? OR method LIKE ? OR method LIKE ?)", method, method+",%", "%,"+method, "%,"+method+",%") } } @@ -306,32 +319,86 @@ func (a *AlertRepo) GetConfig(opts ...DBOption) (model.AlertConfig, error) { return alertConfig, err } +func (a *AlertRepo) GetConfigById(id uint) (model.AlertConfig, error) { + var config model.AlertConfig + err := global.AlertDB.First(&config, id).Error + return config, err +} + +func (a *AlertRepo) WithByTypeNotIn(types []string) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("`type` NOT IN (?)", types) + } +} + +func (a *AlertRepo) PageAlertConfig(page, size int, opts ...DBOption) (int64, []model.AlertConfig, error) { + var configs []model.AlertConfig + db := global.AlertDB.Model(&model.AlertConfig{}) + for _, opt := range opts { + db = opt(db) + } + count := int64(0) + db = db.Count(&count) + err := db.Limit(size).Offset(size * (page - 1)).Find(&configs).Error + return count, configs, err +} + +var singletonTypes = map[string]bool{ + constant.CommonConfig: true, +} + func (a *AlertRepo) SyncAll(data []model.AlertConfig) error { tx := global.AlertDB.Begin() var oldConfigs []model.AlertConfig _ = tx.Find(&oldConfigs).Error oldConfigMap := make(map[string]uint) + nonSingletonTypes := make(map[string]struct{}) for _, item := range oldConfigs { - oldConfigMap[item.Type] = item.ID + if singletonTypes[item.Type] { + oldConfigMap[item.Type] = item.ID + continue + } + nonSingletonTypes[item.Type] = struct{}{} } for _, item := range data { - if val, ok := oldConfigMap[item.Type]; ok { - item.ID = val - delete(oldConfigMap, item.Type) - } else { - item.ID = 0 + if !singletonTypes[item.Type] { + nonSingletonTypes[item.Type] = struct{}{} } - if err := tx.Model(model.AlertConfig{}).Where("id = ?", item.ID).Save(&item).Error; err != nil { + } + for itemType := range nonSingletonTypes { + if err := tx.Where("type = ?", itemType).Delete(&model.AlertConfig{}).Error; err != nil { tx.Rollback() return err } } - for _, val := range oldConfigMap { - if err := tx.Where("id = ?", val).Delete(&model.AlertConfig{}).Error; err != nil { + for _, item := range data { + if singletonTypes[item.Type] { + if val, ok := oldConfigMap[item.Type]; ok { + item.ID = val + delete(oldConfigMap, item.Type) + } else { + item.ID = 0 + } + if err := tx.Model(model.AlertConfig{}).Where("id = ?", item.ID).Save(&item).Error; err != nil { + tx.Rollback() + return err + } + continue + } + item.ID = 0 + if err := tx.Create(&item).Error; err != nil { tx.Rollback() return err } } - tx.Commit() + for _, id := range oldConfigMap { + if err := tx.Where("id = ?", id).Delete(&model.AlertConfig{}).Error; err != nil { + tx.Rollback() + return err + } + } + if err := tx.Commit().Error; err != nil { + return err + } return nil } diff --git a/agent/app/service/alert.go b/agent/app/service/alert.go index 6b4ebd78c893..c4f4cb5b4953 100644 --- a/agent/app/service/alert.go +++ b/agent/app/service/alert.go @@ -25,15 +25,18 @@ import ( type AlertService struct{} +var eeHiddenAlertTypes = []string{"licenseException", "panelUpdate", "panelPwdEndTime"} +var hiddenAlertConfigTypes = []string{"sms"} + type IAlertService interface { PageAlert(req dto.AlertSearch) (int64, []dto.AlertDTO, error) GetAlerts() ([]dto.AlertDTO, error) - CreateAlert(create dto.AlertCreate) error - UpdateAlert(req dto.AlertUpdate) error + CreateAlert(create dto.AlertCreate, operator string) error + UpdateAlert(req dto.AlertUpdate, operator string) error DeleteAlert(id uint) error GetAlert(id uint) (dto.AlertDTO, error) UpdateStatus(id uint, status string) error - ExternalUpdateAlert(req dto.AlertCreate) error + ExternalUpdateAlert(req dto.AlertCreate, operator string) error GetDisks() ([]dto.DiskDTO, error) PageAlertLogs(req dto.AlertLogSearch) (int64, []dto.AlertLogDTO, error) @@ -42,7 +45,8 @@ type IAlertService interface { GetCronJobs(req dto.CronJobReq) ([]dto.CronJobDTO, error) GetAlertConfig() ([]model.AlertConfig, error) - UpdateAlertConfig(req dto.AlertConfigUpdate) error + PageAlertConfig(req dto.PageInfo) (int64, []model.AlertConfig, error) + UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error DeleteAlertConfig(id uint) error TestAlertConfig(req dto.AlertConfigTest) (bool, error) } @@ -56,6 +60,9 @@ func (a AlertService) PageAlert(search dto.AlertSearch) (int64, []dto.AlertDTO, opts []repo.DBOption result []dto.AlertDTO ) + if global.CONF.Base.IsEnterprise { + opts = append(opts, alertRepo.WithByTypeNotIn(eeHiddenAlertTypes)) + } if search.Status != "" { opts = append(opts, repo.WithByStatus(search.Status)) } @@ -82,6 +89,8 @@ func (a AlertService) PageAlert(search dto.AlertSearch) (int64, []dto.AlertDTO, Status: item.Status, SendCount: item.SendCount, AdvancedParams: item.AdvancedParams, + CreateUser: item.CreateUser, + UpdateUser: item.UpdateUser, CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, }) @@ -113,6 +122,8 @@ func (a AlertService) GetAlerts() ([]dto.AlertDTO, error) { Status: item.Status, SendCount: item.SendCount, AdvancedParams: item.AdvancedParams, + CreateUser: item.CreateUser, + UpdateUser: item.UpdateUser, CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, }) @@ -121,7 +132,7 @@ func (a AlertService) GetAlerts() ([]dto.AlertDTO, error) { return result, err } -func (a AlertService) CreateAlert(create dto.AlertCreate) error { +func (a AlertService) CreateAlert(create dto.AlertCreate, operator string) error { var alertID uint var alertInfo model.Alert if create.Project != "" { @@ -138,7 +149,7 @@ func (a AlertService) CreateAlert(create dto.AlertCreate) error { return buserr.WithErr("ErrStructTransform", err) } upAlert.ID = alertID - err := a.UpdateAlert(upAlert) + err := a.UpdateAlert(upAlert, operator) if err != nil { return err } @@ -147,6 +158,8 @@ func (a AlertService) CreateAlert(create dto.AlertCreate) error { if err := copier.Copy(&alertInfo, &create); err != nil { return buserr.WithErr("ErrStructTransform", err) } + alertInfo.CreateUser = operator + alertInfo.UpdateUser = operator if err := alertRepo.Create(&alertInfo); err != nil { return err @@ -157,7 +170,7 @@ func (a AlertService) CreateAlert(create dto.AlertCreate) error { return nil } -func (a AlertService) UpdateAlert(req dto.AlertUpdate) error { +func (a AlertService) UpdateAlert(req dto.AlertUpdate, operator string) error { upMap := make(map[string]interface{}) upMap["id"] = req.ID @@ -170,6 +183,7 @@ func (a AlertService) UpdateAlert(req dto.AlertUpdate) error { upMap["status"] = req.Status upMap["send_count"] = req.SendCount upMap["advanced_params"] = req.AdvancedParams + upMap["update_user"] = operator if err := alertRepo.Update(upMap, repo.WithByID(req.ID)); err != nil { return err @@ -460,12 +474,29 @@ func (a AlertService) GetAlertConfig() ([]model.AlertConfig, error) { opts []repo.DBOption configs []model.AlertConfig ) + if global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "intl" { + opts = append(opts, alertRepo.WithByTypeNotIn(hiddenAlertConfigTypes)) + } opts = append(opts, repo.WithByStatus(constant.AlertEnable)) configs, err := alertRepo.AlertConfigList(opts...) return configs, err } -func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate) error { +func (a AlertService) PageAlertConfig(req dto.PageInfo) (int64, []model.AlertConfig, error) { + opts := []repo.DBOption{ + alertRepo.WithByTypeNotIn([]string{"common"}), + repo.WithOrderDesc("created_at"), + } + if global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "intl" { + opts = append(opts, alertRepo.WithByTypeNotIn(hiddenAlertConfigTypes)) + } + return alertRepo.PageAlertConfig(req.Page, req.PageSize, opts...) +} + +func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error { + if err := a.checkAlertConfigDisplayNameUnique(req); err != nil { + return err + } if req.ID != 0 { upMap := make(map[string]interface{}) upMap["id"] = req.ID @@ -473,6 +504,7 @@ func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate) error { upMap["title"] = req.Title upMap["status"] = req.Status upMap["config"] = req.Config + upMap["update_user"] = operator if err := alertRepo.UpdateAlertConfig(upMap, repo.WithByID(req.ID)); err != nil { return err } @@ -481,6 +513,8 @@ func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate) error { if err := copier.Copy(&alertConfig, &req); err != nil { return buserr.WithErr("ErrStructTransform", err) } + alertConfig.CreateUser = operator + alertConfig.UpdateUser = operator if err := alertRepo.CreateAlertConfig(&alertConfig); err != nil { return err } @@ -489,7 +523,70 @@ func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate) error { return nil } +func (a AlertService) checkAlertConfigDisplayNameUnique(req dto.AlertConfigUpdate) error { + displayName := alertConfigDisplayName(req.Type, req.Config) + if displayName == "" { + return nil + } + + configs, err := alertRepo.AlertConfigList(alertRepo.WithByType(req.Type)) + if err != nil { + return err + } + + for _, config := range configs { + if req.ID != 0 && config.ID == req.ID { + continue + } + if alertConfigDisplayName(config.Type, config.Config) == displayName { + return buserr.New("ErrNameIsExist") + } + } + + return nil +} + +func alertConfigDisplayName(configType, configData string) string { + switch configType { + case constant.Email, constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Bark: + var cfg struct { + DisplayName string `json:"displayName"` + } + if err := json.Unmarshal([]byte(configData), &cfg); err != nil { + return "" + } + return strings.TrimSpace(cfg.DisplayName) + default: + return "" + } +} + func (a AlertService) DeleteAlertConfig(id uint) error { + config, err := alertRepo.GetConfigById(id) + if err != nil { + return err + } + usedAlerts, err := alertRepo.List(alertRepo.WithByMethodConfigID(id)) + if err != nil { + return err + } + if config.Type == constant.SMS { + legacyAlerts, err := alertRepo.List(alertRepo.WithByMethod(constant.SMS)) + if err != nil { + return err + } + usedAlerts = append(usedAlerts, legacyAlerts...) + } + if legacyMethod := legacyAlertMethodByConfigType(config.Type); legacyMethod != "" { + legacyAlerts, err := alertRepo.List(alertRepo.WithByMethod(legacyMethod)) + if err != nil { + return err + } + usedAlerts = append(usedAlerts, legacyAlerts...) + } + if len(usedAlerts) > 0 { + return fmt.Errorf("alert config is in use") + } return alertRepo.DeleteAlertConfig(repo.WithByID(id)) } @@ -522,7 +619,7 @@ func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) { return true, nil } -func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate) error { +func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator string) error { upMap := make(map[string]interface{}) var newStatus string if updateAlert.SendCount == 0 { @@ -566,7 +663,7 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate) error { } else { if updateAlert.Method != "" && updateAlert.Title != "" { updateAlert.Status = newStatus - if err := a.CreateAlert(updateAlert); err != nil { + if err := a.CreateAlert(updateAlert, operator); err != nil { return err } } @@ -574,3 +671,22 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate) error { return nil } + +func legacyAlertMethodByConfigType(configType string) string { + switch configType { + case constant.Email: + return "mail" + case constant.SMS: + return constant.SMS + case constant.Bark: + return constant.Bark + case constant.WeCom: + return constant.WeCom + case constant.DingTalk: + return constant.DingTalk + case constant.FeiShu: + return constant.FeiShu + default: + return "" + } +} diff --git a/agent/app/service/alert_helper.go b/agent/app/service/alert_helper.go index 29c067157c76..94a19b935129 100644 --- a/agent/app/service/alert_helper.go +++ b/agent/app/service/alert_helper.go @@ -174,10 +174,16 @@ func baseTask(baseAlert []dto.AlertDTO) { case "siteEndTime": loadWebsiteInfo(alert) case "panelPwdEndTime": + if global.CONF.Base.IsEnterprise { + continue + } if global.IsMaster { loadPanelPwd(alert) } case "panelUpdate": + if global.CONF.Base.IsEnterprise { + continue + } if global.IsMaster { loadPanelUpdate(alert) } @@ -210,6 +216,9 @@ func resourceTask(resourceAlert []dto.AlertDTO) { loadNodeException(alert) } case "licenseException": + if global.CONF.Base.IsEnterprise { + continue + } if execute && global.IsMaster { loadLicenseException(alert) } @@ -590,94 +599,139 @@ func sendAlerts(alert dto.AlertDTO, alertType, quota, quotaType string, params [ if newDate.IsZero() || calculateMinutesDifference(newDate) > ResourceAlertInterval { for _, m := range methods { m = strings.TrimSpace(m) - switch m { - case constant.SMS: - if !alertUtil.CheckSMSSendLimit(constant.SMS) { - continue - } - todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, constant.SMS) - if !isValid { - continue - } - create := dto.AlertLogCreate{ - Type: alertType, - AlertId: alert.ID, - Count: todayCount + 1, - } - alertErr := xpack.AlertProvider.CreateSMSAlertLog(alertType, alert, create, quotaType, params, constant.SMS) - if alertErr != nil { - global.LOG.Infof("%s alert sms push faild, err: %v", alertType, alertErr.Error()) - continue - } - alertUtil.CreateNewAlertTask(quota, alertType, quotaType, constant.SMS) - case constant.Email: - todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, constant.Email) - if !isValid { - continue - } - create := dto.AlertLogCreate{ - Type: alertType, - AlertId: alert.ID, - Count: todayCount + 1, - } - alertInfo := alert - alertInfo.Type = alertType - create.AlertRule = alertUtil.ProcessAlertRule(alert) - create.AlertDetail = alertUtil.ProcessAlertDetail(alertInfo, quotaType, params, constant.Email) - transport := xpack.MultiNodeProvider.LoadRequestTransport() - agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() - alertErr := alertUtil.CreateEmailAlertLog(create, alertInfo, params, transport, agentInfo) - if alertErr != nil { - global.LOG.Infof("%s alert email push faild, err: %v", alertType, alertErr.Error()) - continue - } - alertUtil.CreateNewAlertTask(quota, alertType, quotaType, constant.Email) - case constant.WeCom, constant.DingTalk, constant.FeiShu: - todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, m) - if !isValid { - continue - } - var create = dto.AlertLogCreate{ - Type: alertUtil.GetCronJobType(alert.Type), - AlertId: alert.ID, - Count: todayCount + 1, - } - transport := xpack.MultiNodeProvider.LoadRequestTransport() - agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() - err := xpack.AlertProvider.CreateWebhookAlertLog(alertType, alert, create, quotaType, params, m, transport, agentInfo) - if err != nil { - global.LOG.Infof("%s alert webhook %s push faild, err: %v", alertType, m, err) - continue - } - alertUtil.CreateNewAlertTask(quota, alertType, quotaType, m) - case constant.Bark: - todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, m) - if !isValid { - continue - } - var create = dto.AlertLogCreate{ - Type: alertUtil.GetCronJobType(alert.Type), - AlertId: alert.ID, - Count: todayCount + 1, - } - alertInfo := alert - alertInfo.Type = alertType - create.AlertRule = alertUtil.ProcessAlertRule(alert) - create.AlertDetail = alertUtil.ProcessAlertDetail(alertInfo, quotaType, params, m) - transport := xpack.MultiNodeProvider.LoadRequestTransport() - agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() - alertErr := alertUtil.CreateBarkAlertLog(create, alertInfo, params, transport, agentInfo) - if alertErr != nil { - global.LOG.Infof("%s alert %s push failed, err: %v", alertType, m, alertErr.Error()) - continue - } - alertUtil.CreateNewAlertTask(quota, alertType, quotaType, m) - default: + if configId, err := strconv.ParseUint(m, 10, 64); err == nil { + sendAlertsByConfigId(alert, alertType, quota, quotaType, params, uint(configId)) + } else { + sendAlertsByLegacyMethod(alert, alertType, quota, quotaType, params, m) } } } } +func sendAlertsByConfigId(alert dto.AlertDTO, alertType, quota, quotaType string, params []dto.Param, configId uint) { + config, err := alertRepo.GetConfigById(configId) + if err != nil { + global.LOG.Errorf("alert config not found for id %d: %v", configId, err) + return + } + doSendAlert(alert, alertType, quota, quotaType, params, config) +} + +func sendAlertsByLegacyMethod(alert dto.AlertDTO, alertType, quota, quotaType string, params []dto.Param, method string) { + typeMap := map[string]string{ + "mail": constant.Email, + constant.Bark: constant.Bark, + constant.SMS: constant.SMS, + } + configType, ok := typeMap[method] + if !ok { + configType = method + } + config, err := alertRepo.GetConfig(alertRepo.WithByType(configType)) + if err != nil { + global.LOG.Errorf("alert config not found for type %s: %v", configType, err) + return + } + doSendAlert(alert, alertType, quota, quotaType, params, config) +} + +func doSendAlert(alert dto.AlertDTO, alertType, quota, quotaType string, params []dto.Param, config model.AlertConfig) { + if !alertUtil.IsAlertConfigEnabled(config) { + return + } + methodStr := strconv.Itoa(int(config.ID)) + switch config.Type { + case constant.SMS: + if !alertUtil.CheckSMSSendLimit(config, methodStr) { + return + } + todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, methodStr) + if !isValid { + return + } + create := dto.AlertLogCreate{ + Type: alertType, + AlertId: alert.ID, + Count: todayCount + 1, + Method: methodStr, + } + alertErr := xpack.AlertProvider.CreateSMSAlertLog(alertType, alert, create, quotaType, params, config, methodStr) + if alertErr != nil { + global.LOG.Infof("%s alert sms push faild, err: %v", alertType, alertErr.Error()) + return + } + alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr) + + case constant.Email: + todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, methodStr) + if !isValid { + return + } + create := dto.AlertLogCreate{ + Type: alertType, + AlertId: alert.ID, + Count: todayCount + 1, + Method: methodStr, + } + alertInfo := alert + alertInfo.Type = alertType + create.AlertRule = alertUtil.ProcessAlertRule(alert) + create.AlertDetail = alertUtil.ProcessAlertDetail(alertInfo, quotaType, params, constant.Email) + transport := xpack.MultiNodeProvider.LoadRequestTransport() + agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() + alertErr := alertUtil.CreateEmailAlertLog(create, alertInfo, params, transport, agentInfo, config) + if alertErr != nil { + global.LOG.Infof("%s alert email push faild, err: %v", alertType, alertErr.Error()) + return + } + alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr) + + case constant.Bark: + todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, methodStr) + if !isValid { + return + } + create := dto.AlertLogCreate{ + Type: alertType, + AlertId: alert.ID, + Count: todayCount + 1, + Method: methodStr, + } + alertInfo := alert + alertInfo.Type = alertType + create.AlertRule = alertUtil.ProcessAlertRule(alert) + create.AlertDetail = alertUtil.ProcessAlertDetail(alertInfo, quotaType, params, constant.Bark) + transport := xpack.MultiNodeProvider.LoadRequestTransport() + agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() + alertErr := alertUtil.CreateBarkAlertLog(create, alertInfo, params, transport, agentInfo, config) + if alertErr != nil { + global.LOG.Infof("%s alert %s push failed, err: %v", alertType, methodStr, alertErr.Error()) + return + } + alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr) + + case constant.WeCom, constant.DingTalk, constant.FeiShu: + todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, methodStr) + if !isValid { + return + } + create := dto.AlertLogCreate{ + Type: alertType, + AlertId: alert.ID, + Count: todayCount + 1, + Method: methodStr, + } + transport := xpack.MultiNodeProvider.LoadRequestTransport() + agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() + alertErr := xpack.AlertProvider.CreateWebhookAlertLog(alertType, alert, create, quotaType, params, config, transport, agentInfo) + if alertErr != nil { + global.LOG.Infof("%s alert webhook %s push faild, err: %v", alertType, methodStr, alertErr) + return + } + alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr) + } +} + // ------------------------------ func getRepoOptionsByProject(project string) []repo.DBOption { var opts []repo.DBOption diff --git a/agent/app/service/alert_sender.go b/agent/app/service/alert_sender.go index b64efa7aa42f..d11ef431f916 100644 --- a/agent/app/service/alert_sender.go +++ b/agent/app/service/alert_sender.go @@ -1,12 +1,16 @@ package service import ( + "strconv" + "strings" + "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/constant" "github.com/1Panel-dev/1Panel/agent/global" alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert" "github.com/1Panel-dev/1Panel/agent/utils/xpack" - "strings" ) type AlertSender struct { @@ -22,45 +26,92 @@ func NewAlertSender(alert dto.AlertDTO, quotaType string) *AlertSender { } func (s *AlertSender) Send(quota string, params []dto.Param) { - methods := strings.Split(s.alert.Method, ",") - for _, method := range methods { - method = strings.TrimSpace(method) - switch method { - case constant.SMS: - s.sendSMS(quota, params) - case constant.Email: - s.sendEmail(quota, params) - case constant.Bark: - s.sendBark(quota, params) - case constant.WeCom, constant.DingTalk, constant.FeiShu: - s.sendWebhook(quota, params, method) + s.sendByConfigIds(s.alert.Method, quota, params, false) +} + +func (s *AlertSender) ResourceSend(quota string, params []dto.Param) { + s.sendByConfigIds(s.alert.Method, quota, params, true) +} + +func (s *AlertSender) sendByConfigIds(methodStr string, quota string, params []dto.Param, isResource bool) { + alertRepo := repo.NewIAlertRepo() + configIds := strings.Split(methodStr, ",") + for _, idStr := range configIds { + idStr = strings.TrimSpace(idStr) + configId, err := strconv.ParseUint(idStr, 10, 64) + if err != nil { + s.sendByLegacyMethod(idStr, quota, params, isResource) + continue } + config, err := alertRepo.GetConfigById(uint(configId)) + if err != nil { + global.LOG.Errorf("alert config not found for id %d: %v", configId, err) + continue + } + s.sendByConfig(config, quota, params, isResource) } } -func (s *AlertSender) ResourceSend(quota string, params []dto.Param) { - methods := strings.Split(s.alert.Method, ",") - for _, method := range methods { - method = strings.TrimSpace(method) - switch method { - case constant.SMS: - s.sendResourceSMS(quota, params) - case constant.Email: - s.sendResourceEmail(quota, params) - case constant.Bark: - s.sendResourceBark(quota, params) - case constant.WeCom, constant.DingTalk, constant.FeiShu: - s.sendResourceWebhook(quota, params, method) +func (s *AlertSender) sendByConfig(config model.AlertConfig, quota string, params []dto.Param, isResource bool) { + if !alertUtil.IsAlertConfigEnabled(config) { + return + } + switch config.Type { + case constant.SMS: + if isResource { + s.sendResourceSMSWithConfig(config, quota, params) + } else { + s.sendSMSWithConfig(config, quota, params) + } + case constant.Email: + if isResource { + s.sendResourceEmailWithConfig(config, quota, params) + } else { + s.sendEmailWithConfig(config, quota, params) + } + case constant.Bark: + if isResource { + s.sendResourceBarkWithConfig(config, quota, params) + } else { + s.sendBarkWithConfig(config, quota, params) + } + case constant.WeCom, constant.DingTalk, constant.FeiShu: + if isResource { + s.sendResourceWebhookWithConfig(config, quota, params) + } else { + s.sendWebhookWithConfig(config, quota, params) } } } -func (s *AlertSender) sendSMS(quota string, params []dto.Param) { - if !alertUtil.CheckSMSSendLimit(constant.SMS) { +func (s *AlertSender) sendByLegacyMethod(method string, quota string, params []dto.Param, isResource bool) { + alertRepo := repo.NewIAlertRepo() + typeMap := map[string]string{"mail": constant.Email, constant.Bark: constant.Bark, constant.SMS: constant.SMS} + configType := method + if mapped, ok := typeMap[method]; ok { + configType = mapped + } + config, err := alertRepo.GetConfig(alertRepo.WithByType(configType)) + if err != nil { + global.LOG.Errorf("alert config not found for type %s: %v", configType, err) + return + } + if !alertUtil.IsAlertConfigEnabled(config) { + return + } + s.sendByConfig(config, quota, params, isResource) +} + +func (s *AlertSender) sendSMSWithConfig(config model.AlertConfig, quota string, params []dto.Param) { + if !alertUtil.IsAlertConfigEnabled(config) { + return + } + method := strconv.Itoa(int(config.ID)) + if !alertUtil.CheckSMSSendLimit(config, method) { return } - totalCount, isValid := s.canSendAlert(constant.SMS) + totalCount, isValid := s.canSendAlert(method) if !isValid { return } @@ -70,18 +121,31 @@ func (s *AlertSender) sendSMS(quota string, params []dto.Param) { Count: totalCount + 1, AlertId: s.alert.ID, Type: s.alert.Type, + Method: method, } - err := xpack.AlertProvider.CreateSMSAlertLog(s.alert.Type, s.alert, create, quota, params, constant.SMS) + err := xpack.AlertProvider.CreateSMSAlertLog(s.alert.Type, s.alert, create, quota, params, config, method) if err != nil { global.LOG.Errorf("%s alert sms push failed: %v", s.alert.Type, err) return } - alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, constant.SMS) + alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, method) } -func (s *AlertSender) sendEmail(quota string, params []dto.Param) { - totalCount, isValid := s.canSendAlert(constant.Email) +func (s *AlertSender) sendSMS(quota string, params []dto.Param) { + alertRepo := repo.NewIAlertRepo() + config, err := alertRepo.GetConfig(alertRepo.WithByType(constant.SMS)) + if err != nil { + return + } + s.sendSMSWithConfig(config, quota, params) +} + +func (s *AlertSender) sendEmailWithConfig(config model.AlertConfig, quota string, params []dto.Param) { + if !alertUtil.IsAlertConfigEnabled(config) { + return + } + totalCount, isValid := s.canSendAlert(strconv.Itoa(int(config.ID))) if !isValid { return } @@ -93,91 +157,99 @@ func (s *AlertSender) sendEmail(quota string, params []dto.Param) { Type: s.alert.Type, AlertRule: alertUtil.ProcessAlertRule(s.alert), AlertDetail: alertUtil.ProcessAlertDetail(s.alert, quota, params, constant.Email), + Method: strconv.Itoa(int(config.ID)), } transport := xpack.MultiNodeProvider.LoadRequestTransport() agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() - err := alertUtil.CreateEmailAlertLog(create, s.alert, params, transport, agentInfo) + err := alertUtil.CreateEmailAlertLog(create, s.alert, params, transport, agentInfo, config) if err != nil { global.LOG.Errorf("%s alert email push failed: %v", s.alert.Type, err) return } - alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, constant.Email) + alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID))) } -func (s *AlertSender) sendBark(quota string, params []dto.Param) { - totalCount, isValid := s.canSendAlert(constant.Bark) +func (s *AlertSender) sendResourceEmailWithConfig(config model.AlertConfig, quota string, params []dto.Param) { + if !alertUtil.IsAlertConfigEnabled(config) { + return + } + todayCount, isValid := s.canResourceSendAlert(strconv.Itoa(int(config.ID))) if !isValid { return } create := dto.AlertLogCreate{ Status: constant.AlertSuccess, - Count: totalCount + 1, + Count: todayCount + 1, AlertId: s.alert.ID, Type: s.alert.Type, AlertRule: alertUtil.ProcessAlertRule(s.alert), - AlertDetail: alertUtil.ProcessAlertDetail(s.alert, quota, params, constant.Bark), + AlertDetail: alertUtil.ProcessAlertDetail(s.alert, quota, params, constant.Email), + Method: strconv.Itoa(int(config.ID)), } transport := xpack.MultiNodeProvider.LoadRequestTransport() agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() - err := alertUtil.CreateBarkAlertLog(create, s.alert, params, transport, agentInfo) - if err != nil { - global.LOG.Errorf("%s alert bark push failed: %v", s.alert.Type, err) + if err := alertUtil.CreateEmailAlertLog(create, s.alert, params, transport, agentInfo, config); err != nil { + global.LOG.Errorf("failed to send Email alert: %v", err) return } - alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, constant.Bark) + alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID))) } -func (s *AlertSender) sendWebhook(quota string, params []dto.Param, method string) { - totalCount, isValid := s.canSendAlert(method) - if !isValid { +func (s *AlertSender) sendEmail(quota string, params []dto.Param) { + alertRepo := repo.NewIAlertRepo() + config, err := alertRepo.GetConfig(alertRepo.WithByType(constant.EmailConfig)) + if err != nil { return } + s.sendEmailWithConfig(config, quota, params) +} - create := dto.AlertLogCreate{ - Status: constant.AlertSuccess, - Count: totalCount + 1, - AlertId: s.alert.ID, - Type: s.alert.Type, - } - transport := xpack.MultiNodeProvider.LoadRequestTransport() - agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() - err := xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, method, transport, agentInfo) +func (s *AlertSender) sendResourceEmail(quota string, params []dto.Param) { + alertRepo := repo.NewIAlertRepo() + config, err := alertRepo.GetConfig(alertRepo.WithByType(constant.EmailConfig)) if err != nil { - global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, method, err) return } - alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, method) + s.sendResourceEmailWithConfig(config, quota, params) } -func (s *AlertSender) sendResourceSMS(quota string, params []dto.Param) { - if !alertUtil.CheckSMSSendLimit(constant.SMS) { +func (s *AlertSender) sendBarkWithConfig(config model.AlertConfig, quota string, params []dto.Param) { + if !alertUtil.IsAlertConfigEnabled(config) { return } - - todayCount, isValid := s.canResourceSendAlert(constant.SMS) + totalCount, isValid := s.canSendAlert(strconv.Itoa(int(config.ID))) if !isValid { return } create := dto.AlertLogCreate{ - Status: constant.AlertSuccess, - Count: todayCount + 1, - AlertId: s.alert.ID, - Type: s.alert.Type, + Status: constant.AlertSuccess, + Count: totalCount + 1, + AlertId: s.alert.ID, + Type: s.alert.Type, + AlertRule: alertUtil.ProcessAlertRule(s.alert), + AlertDetail: alertUtil.ProcessAlertDetail(s.alert, quota, params, constant.Bark), + Method: strconv.Itoa(int(config.ID)), } - if err := xpack.AlertProvider.CreateSMSAlertLog(s.alert.Type, s.alert, create, quota, params, constant.SMS); err != nil { - global.LOG.Errorf("failed to send SMS alert: %v", err) + transport := xpack.MultiNodeProvider.LoadRequestTransport() + agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() + err := alertUtil.CreateBarkAlertLog(create, s.alert, params, transport, agentInfo, config) + if err != nil { + global.LOG.Errorf("%s alert bark push failed: %v", s.alert.Type, err) return } - alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, constant.SMS) + alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID))) } -func (s *AlertSender) sendResourceEmail(quota string, params []dto.Param) { - todayCount, isValid := s.canResourceSendAlert(constant.Email) +func (s *AlertSender) sendResourceBarkWithConfig(config model.AlertConfig, quota string, params []dto.Param) { + if !alertUtil.IsAlertConfigEnabled(config) { + return + } + todayCount, isValid := s.canResourceSendAlert(strconv.Itoa(int(config.ID))) if !isValid { return } @@ -188,43 +260,115 @@ func (s *AlertSender) sendResourceEmail(quota string, params []dto.Param) { AlertId: s.alert.ID, Type: s.alert.Type, AlertRule: alertUtil.ProcessAlertRule(s.alert), - AlertDetail: alertUtil.ProcessAlertDetail(s.alert, quota, params, constant.Email), + AlertDetail: alertUtil.ProcessAlertDetail(s.alert, quota, params, constant.Bark), + Method: strconv.Itoa(int(config.ID)), } transport := xpack.MultiNodeProvider.LoadRequestTransport() agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() - if err := alertUtil.CreateEmailAlertLog(create, s.alert, params, transport, agentInfo); err != nil { - global.LOG.Errorf("failed to send Email alert: %v", err) + if err := alertUtil.CreateBarkAlertLog(create, s.alert, params, transport, agentInfo, config); err != nil { + global.LOG.Errorf("failed to send Bark alert: %v", err) + return + } + alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID))) +} + +func (s *AlertSender) sendBark(quota string, params []dto.Param) { + alertRepo := repo.NewIAlertRepo() + config, err := alertRepo.GetConfig(alertRepo.WithByType(constant.Bark)) + if err != nil { return } - alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, constant.Email) + s.sendBarkWithConfig(config, quota, params) } func (s *AlertSender) sendResourceBark(quota string, params []dto.Param) { - todayCount, isValid := s.canResourceSendAlert(constant.Bark) + alertRepo := repo.NewIAlertRepo() + config, err := alertRepo.GetConfig(alertRepo.WithByType(constant.Bark)) + if err != nil { + return + } + s.sendResourceBarkWithConfig(config, quota, params) +} + +func (s *AlertSender) sendWebhookWithConfig(config model.AlertConfig, quota string, params []dto.Param) { + if !alertUtil.IsAlertConfigEnabled(config) { + return + } + totalCount, isValid := s.canSendAlert(strconv.Itoa(int(config.ID))) if !isValid { return } create := dto.AlertLogCreate{ - Status: constant.AlertSuccess, - Count: todayCount + 1, - AlertId: s.alert.ID, - Type: s.alert.Type, - AlertRule: alertUtil.ProcessAlertRule(s.alert), - AlertDetail: alertUtil.ProcessAlertDetail(s.alert, quota, params, constant.Bark), + Status: constant.AlertSuccess, + Count: totalCount + 1, + AlertId: s.alert.ID, + Type: s.alert.Type, + Method: strconv.Itoa(int(config.ID)), } + transport := xpack.MultiNodeProvider.LoadRequestTransport() + agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() + err := xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo) + if err != nil { + global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, config.Type, err) + return + } + alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID))) +} +func (s *AlertSender) sendResourceWebhookWithConfig(config model.AlertConfig, quota string, params []dto.Param) { + if !alertUtil.IsAlertConfigEnabled(config) { + return + } + todayCount, isValid := s.canResourceSendAlert(strconv.Itoa(int(config.ID))) + if !isValid { + return + } + + create := dto.AlertLogCreate{ + Status: constant.AlertSuccess, + Count: todayCount + 1, + AlertId: s.alert.ID, + Type: s.alert.Type, + Method: strconv.Itoa(int(config.ID)), + } transport := xpack.MultiNodeProvider.LoadRequestTransport() agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() - if err := alertUtil.CreateBarkAlertLog(create, s.alert, params, transport, agentInfo); err != nil { - global.LOG.Errorf("failed to send Bark alert: %v", err) + if err := xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo); err != nil { + global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, config.Type, err) return } - alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, constant.Bark) + alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID))) +} + +func (s *AlertSender) sendWebhook(quota string, params []dto.Param, method string) { + alertRepo := repo.NewIAlertRepo() + config, err := alertRepo.GetConfig(alertRepo.WithByType(method)) + if err != nil { + return + } + s.sendWebhookWithConfig(config, quota, params) } func (s *AlertSender) sendResourceWebhook(quota string, params []dto.Param, method string) { + alertRepo := repo.NewIAlertRepo() + config, err := alertRepo.GetConfig(alertRepo.WithByType(method)) + if err != nil { + return + } + s.sendResourceWebhookWithConfig(config, quota, params) +} + +func (s *AlertSender) sendResourceSMSWithConfig(config model.AlertConfig, quota string, params []dto.Param) { + if !alertUtil.IsAlertConfigEnabled(config) { + return + } + method := strconv.Itoa(int(config.ID)) + if !alertUtil.CheckSMSSendLimit(config, method) { + return + } + todayCount, isValid := s.canResourceSendAlert(method) if !isValid { return @@ -235,16 +379,25 @@ func (s *AlertSender) sendResourceWebhook(quota string, params []dto.Param, meth Count: todayCount + 1, AlertId: s.alert.ID, Type: s.alert.Type, + Method: method, } - transport := xpack.MultiNodeProvider.LoadRequestTransport() - agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() - if err := xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, method, transport, agentInfo); err != nil { - global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, method, err) + + if err := xpack.AlertProvider.CreateSMSAlertLog(s.alert.Type, s.alert, create, quota, params, config, method); err != nil { + global.LOG.Errorf("failed to send SMS alert: %v", err) return } alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, method) } +func (s *AlertSender) sendResourceSMS(quota string, params []dto.Param) { + alertRepo := repo.NewIAlertRepo() + config, err := alertRepo.GetConfig(alertRepo.WithByType(constant.SMSConfig)) + if err != nil { + return + } + s.sendResourceSMSWithConfig(config, quota, params) +} + func (s *AlertSender) canSendAlert(method string) (uint, bool) { todayCount, totalCount, err := alertRepo.LoadTaskCount(s.alert.Type, s.quotaType, method) if err != nil { diff --git a/agent/app/service/clam.go b/agent/app/service/clam.go index 2effb982ee67..66deeea4e32f 100644 --- a/agent/app/service/clam.go +++ b/agent/app/service/clam.go @@ -36,8 +36,8 @@ type IClamService interface { LoadBaseInfo() (dto.ClamBaseInfo, error) Operate(operate string) error SearchWithPage(search dto.SearchClamWithPage) (int64, interface{}, error) - Create(req dto.ClamCreate) error - Update(req dto.ClamUpdate) error + Create(req dto.ClamCreate, operator string) error + Update(req dto.ClamUpdate, operator string) error UpdateStatus(id uint, status string) error Delete(req dto.ClamDelete) error HandleOnce(id uint) error @@ -165,7 +165,7 @@ func (c *ClamService) SearchWithPage(req dto.SearchClamWithPage) (int64, interfa return total, datas, err } -func (c *ClamService) Create(req dto.ClamCreate) error { +func (c *ClamService) Create(req dto.ClamCreate, operator string) error { clam, _ := clamRepo.Get(repo.WithByName(req.Name)) if clam.ID != 0 { return buserr.New("ErrRecordExist") @@ -199,7 +199,7 @@ func (c *ClamService) Create(req dto.ClamCreate) error { Project: strconv.Itoa(int(clam.ID)), Status: constant.AlertEnable, } - err := NewIAlertService().CreateAlert(createAlert) + err := NewIAlertService().CreateAlert(createAlert, operator) if err != nil { return err } @@ -207,7 +207,7 @@ func (c *ClamService) Create(req dto.ClamCreate) error { return nil } -func (c *ClamService) Update(req dto.ClamUpdate) error { +func (c *ClamService) Update(req dto.ClamUpdate, operator string) error { if cmd.CheckIllegal(req.Path) { return buserr.New("ErrCmdIllegal") } @@ -260,7 +260,7 @@ func (c *ClamService) Update(req dto.ClamUpdate) error { Type: "clams", Project: strconv.Itoa(int(clam.ID)), } - err := NewIAlertService().ExternalUpdateAlert(updateAlert) + err := NewIAlertService().ExternalUpdateAlert(updateAlert, operator) if err != nil { return err } diff --git a/agent/app/service/cronjob.go b/agent/app/service/cronjob.go index 55e86f6a04df..b3900d418f88 100644 --- a/agent/app/service/cronjob.go +++ b/agent/app/service/cronjob.go @@ -27,10 +27,10 @@ type CronjobService struct{} type ICronjobService interface { SearchWithPage(search dto.PageCronjob) (int64, interface{}, error) SearchRecords(search dto.SearchRecord) (int64, interface{}, error) - Create(cronjobDto dto.CronjobOperate) error + Create(cronjobDto dto.CronjobOperate, operator string) error LoadNextHandle(spec string) ([]string, error) HandleOnce(id uint) error - Update(id uint, req dto.CronjobOperate) error + Update(id uint, req dto.CronjobOperate, operator string) error UpdateStatus(id uint, status string) error UpdateGroup(req dto.ChangeGroup) error Delete(req dto.CronjobBatchDelete) error @@ -39,7 +39,7 @@ type ICronjobService interface { HandleStop(id uint) error Export(req dto.OperateByIDs) (string, error) - Import(req []dto.CronjobTrans) error + Import(req []dto.CronjobTrans, operator string) error LoadScriptOptions() []dto.ScriptOptions LoadInfo(req dto.OperateByID) (*dto.CronjobOperate, error) @@ -212,7 +212,7 @@ func (u *CronjobService) Export(req dto.OperateByIDs) (string, error) { return string(itemJson), nil } -func (u *CronjobService) Import(req []dto.CronjobTrans) error { +func (u *CronjobService) Import(req []dto.CronjobTrans, operator string) error { for _, item := range req { cronjobItem, _ := cronjobRepo.Get(repo.WithByName(item.Name)) if cronjobItem.ID != 0 { @@ -405,7 +405,7 @@ func (u *CronjobService) Import(req []dto.CronjobTrans) error { Project: strconv.Itoa(int(cronjob.ID)), Status: constant.AlertEnable, } - _ = NewIAlertService().CreateAlert(createAlert) + _ = NewIAlertService().CreateAlert(createAlert, operator) } } return nil @@ -558,7 +558,7 @@ func (u *CronjobService) HandleOnce(id uint) error { return nil } -func (u *CronjobService) Create(req dto.CronjobOperate) error { +func (u *CronjobService) Create(req dto.CronjobOperate, operator string) error { cronjob, _ := cronjobRepo.Get(repo.WithByName(req.Name)) if cronjob.ID != 0 { return buserr.New("ErrRecordExist") @@ -607,7 +607,7 @@ func (u *CronjobService) Create(req dto.CronjobOperate) error { Project: strconv.Itoa(int(cronjob.ID)), Status: constant.AlertEnable, } - err := NewIAlertService().CreateAlert(createAlert) + err := NewIAlertService().CreateAlert(createAlert, operator) if err != nil { return err } @@ -678,7 +678,7 @@ func (u *CronjobService) Delete(req dto.CronjobBatchDelete) error { return nil } -func (u *CronjobService) Update(id uint, req dto.CronjobOperate) error { +func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string) error { var cronjob model.Cronjob if err := copier.Copy(&cronjob, &req); err != nil { return buserr.WithDetail("ErrStructTransform", err.Error(), nil) @@ -756,7 +756,7 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate) error { Type: cronjob.Type, Project: strconv.Itoa(int(cronModel.ID)), } - err = NewIAlertService().ExternalUpdateAlert(updateAlert) + err = NewIAlertService().ExternalUpdateAlert(updateAlert, operator) if err != nil { return err } diff --git a/agent/constant/alert.go b/agent/constant/alert.go index 546ae1bb8a82..c0b92b5b911f 100644 --- a/agent/constant/alert.go +++ b/agent/constant/alert.go @@ -20,7 +20,7 @@ const ( const ( WeChat = "wechat" SMS = "sms" - Email = "mail" + Email = "email" WeCom = "weCom" DingTalk = "dingTalk" FeiShu = "feiShu" diff --git a/agent/init/migration/migrate.go b/agent/init/migration/migrate.go index 9f83525c49ba..af5194a7e6b3 100644 --- a/agent/init/migration/migrate.go +++ b/agent/init/migration/migrate.go @@ -36,6 +36,8 @@ func InitAgentDB() { migrations.UpdateMcpServer, migrations.InitCronjobGroup, migrations.AddColumnToAlert, + migrations.MigrateAlertMethodConfigIDs, + migrations.AddAlertAuditUser, migrations.UpdateWebsiteSSL, migrations.AddQuickJump, migrations.UpdateMcpServerAddType, diff --git a/agent/init/migration/migrations/init.go b/agent/init/migration/migrations/init.go index 8dd3be2b24cc..dac29c8dd3df 100644 --- a/agent/init/migration/migrations/init.go +++ b/agent/init/migration/migrations/init.go @@ -389,16 +389,20 @@ var InitAlertConfig = &gormigrate.Migration{ Migrate: func(tx *gorm.DB) error { records := []model.AlertConfig{ { - Type: "sms", - Title: "xpack.alert.smsConfig", - Status: "Enable", - Config: `{"alertDailyNum":50}`, + Type: "sms", + Title: "xpack.alert.smsConfig", + Status: "Enable", + Config: `{"alertDailyNum":50}`, + CreateUser: "system", + UpdateUser: "system", }, { - Type: "common", - Title: "xpack.alert.commonConfig", - Status: "Enable", - Config: `{"isOffline":"Disable","alertSendTimeRange":{"noticeAlert":{"sendTimeRange":"08:00:00 - 23:59:59","type":["ssl","siteEndTime","panelPwdEndTime","panelUpdate"]},"resourceAlert":{"sendTimeRange":"00:00:00 - 23:59:59","type":["clams","cronJob","cpu","memory","load","disk"]}}}`, + Type: "common", + Title: "xpack.alert.commonConfig", + Status: "Enable", + Config: `{"isOffline":"Disable","alertSendTimeRange":{"noticeAlert":{"sendTimeRange":"08:00:00 - 23:59:59","type":["ssl","siteEndTime","panelPwdEndTime","panelUpdate"]},"resourceAlert":{"sendTimeRange":"00:00:00 - 23:59:59","type":["clams","cronJob","cpu","memory","load","disk"]}}}`, + CreateUser: "system", + UpdateUser: "system", }, } for _, r := range records { @@ -477,6 +481,95 @@ var AddColumnToAlert = &gormigrate.Migration{ }, } +var MigrateAlertMethodConfigIDs = &gormigrate.Migration{ + ID: "20251001-migrate-alert-method-config-ids", + Migrate: func(tx *gorm.DB) error { + if err := global.AlertDB.AutoMigrate(&model.Alert{}, &model.AlertLog{}, &model.AlertTask{}, &model.AlertConfig{}); err != nil { + return err + } + if err := migrateAlertMethodConfigIDs(global.AlertDB); err != nil { + return err + } + return nil + }, +} + +var AddAlertAuditUser = &gormigrate.Migration{ + ID: "20260602-add-alert-audit-user", + Migrate: func(tx *gorm.DB) error { + return global.AlertDB.AutoMigrate(&model.Alert{}, &model.AlertConfig{}) + }, +} + +func migrateAlertMethodConfigIDs(tx *gorm.DB) error { + if err := tx.Model(&model.AlertConfig{}).Where("type = ?", "mail").Update("type", constant.EmailConfig).Error; err != nil { + return err + } + + typeMap := map[string]string{ + "mail": constant.Email, + constant.Email: constant.Email, + constant.SMS: constant.SMS, + constant.Bark: constant.Bark, + constant.WeCom: constant.WeCom, + constant.DingTalk: constant.DingTalk, + constant.FeiShu: constant.FeiShu, + } + configIDs := map[string]string{} + for _, configType := range typeMap { + if _, ok := configIDs[configType]; ok { + continue + } + var config model.AlertConfig + if err := tx.Where("type = ?", configType).Order("id ASC").First(&config).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + continue + } + return err + } + configIDs[configType] = strconv.Itoa(int(config.ID)) + } + + var alerts []model.Alert + if err := tx.Find(&alerts).Error; err != nil { + return err + } + for _, alert := range alerts { + method := migrateAlertMethodValue(alert.Method, typeMap, configIDs) + if method == alert.Method { + continue + } + if err := tx.Model(&model.Alert{}).Where("id = ?", alert.ID).Update("method", method).Error; err != nil { + return err + } + } + return nil +} + +func migrateAlertMethodValue(method string, typeMap map[string]string, configIDs map[string]string) string { + items := strings.Split(method, ",") + next := make([]string, 0, len(items)) + seen := map[string]struct{}{} + for _, item := range items { + item = strings.TrimSpace(item) + if item == "" { + continue + } + configType, ok := typeMap[item] + if ok { + if id, exists := configIDs[configType]; exists { + item = id + } + } + if _, exists := seen[item]; exists { + continue + } + seen[item] = struct{}{} + next = append(next, item) + } + return strings.Join(next, ",") +} + var UpdateWebsiteSSL = &gormigrate.Migration{ ID: "20250819-update-website-ssl", Migrate: func(tx *gorm.DB) error { diff --git a/agent/router/ro_alert.go b/agent/router/ro_alert.go index d4e2055675ca..1400336c4894 100644 --- a/agent/router/ro_alert.go +++ b/agent/router/ro_alert.go @@ -25,6 +25,7 @@ func (a *AlertRouter) InitRouter(Router *gin.RouterGroup) { alertRouter.POST("/config/update", baseApi.UpdateAlertConfig) alertRouter.POST("/config/info", baseApi.GetAlertConfig) + alertRouter.POST("/config/search", baseApi.PageAlertConfig) alertRouter.POST("/config/del", baseApi.DeleteAlertConfig) alertRouter.POST("/config/test", baseApi.TestAlertConfig) } diff --git a/agent/utils/alert/alert.go b/agent/utils/alert/alert.go index 90f2dac18372..39c9e27c6fd4 100644 --- a/agent/utils/alert/alert.go +++ b/agent/utils/alert/alert.go @@ -4,6 +4,16 @@ import ( "encoding/json" "errors" "fmt" + "mime" + network "net" + "net/http" + "os" + "os/exec" + "strconv" + "strings" + "sync" + "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" @@ -16,29 +26,20 @@ import ( "github.com/1Panel-dev/1Panel/agent/utils/psutil" "github.com/1Panel-dev/1Panel/agent/utils/re" "github.com/jinzhu/copier" - "mime" - network "net" - "net/http" - "os" - "os/exec" - "strconv" - "strings" - "sync" - "time" ) var cronJobAlertTypes = []string{"shell", "app", "website", "database", "directory", "log", "snapshot", "curl", "cutWebsiteLog", "clean", "ntp"} -func CreateTaskScanEmailAlertLog(alert dto.AlertDTO, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string, transport *http.Transport, agentInfo *dto.AgentInfo) error { +func CreateTaskScanEmailAlertLog(alert dto.AlertDTO, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string, transport *http.Transport, agentInfo *dto.AgentInfo, emailConfig model.AlertConfig) error { params := CreateAlertParams(GetCronJobTypeName(pushAlert.Param)) alertDetail := ProcessAlertDetail(alert, pushAlert.TaskName, params, method) alertRule := ProcessAlertRule(alert) create.AlertRule = alertRule create.AlertDetail = alertDetail - return CreateEmailAlertLog(create, alert, params, transport, agentInfo) + return CreateEmailAlertLog(create, alert, params, transport, agentInfo, emailConfig) } -func CreateEmailAlertLog(create dto.AlertLogCreate, alert dto.AlertDTO, params []dto.Param, transport *http.Transport, agentInfo *dto.AgentInfo) error { +func CreateEmailAlertLog(create dto.AlertLogCreate, alert dto.AlertDTO, params []dto.Param, transport *http.Transport, agentInfo *dto.AgentInfo, emailConfig model.AlertConfig) error { var alertLog model.AlertLog alertRepo := repo.NewIAlertRepo() config, err := alertRepo.GetConfig(alertRepo.WithByType(constant.CommonConfig)) @@ -50,11 +51,6 @@ func CreateEmailAlertLog(create dto.AlertLogCreate, alert dto.AlertDTO, params [ if err != nil { return err } - create.Method = constant.Email - emailConfig, err := alertRepo.GetConfig(alertRepo.WithByType(constant.EmailConfig)) - if err != nil { - return err - } var emailInfo dto.AlertEmailConfig err = json.Unmarshal([]byte(emailConfig.Config), &emailInfo) if err != nil { @@ -104,17 +100,11 @@ func CreateEmailAlertLog(create dto.AlertLogCreate, alert dto.AlertDTO, params [ } } -func CreateBarkAlertLog(create dto.AlertLogCreate, alert dto.AlertDTO, params []dto.Param, transport *http.Transport, agentInfo *dto.AgentInfo) error { +func CreateBarkAlertLog(create dto.AlertLogCreate, alert dto.AlertDTO, params []dto.Param, transport *http.Transport, agentInfo *dto.AgentInfo, barkConfig model.AlertConfig) error { var alertLog model.AlertLog - alertRepo := repo.NewIAlertRepo() - create.Method = constant.Bark - barkConfig, err := alertRepo.GetConfig(alertRepo.WithByType(constant.Bark)) - if err != nil { - return err - } var barkInfo dto.AlertWebhookConfig - err = json.Unmarshal([]byte(barkConfig.Config), &barkInfo) + err := json.Unmarshal([]byte(barkConfig.Config), &barkInfo) if err != nil { return err } @@ -244,14 +234,13 @@ func CreateAlertParams(param string) []dto.Param { var checkTaskMutex sync.Mutex -func CheckSMSSendLimit(method string) bool { - alertRepo := repo.NewIAlertRepo() - config, err := alertRepo.GetConfig(alertRepo.WithByType(constant.SMSConfig)) - if err != nil { +func CheckSMSSendLimit(config model.AlertConfig, method string) bool { + if config.Type != constant.SMS { return false } + alertRepo := repo.NewIAlertRepo() var cfg dto.AlertSmsConfig - cfg, err = ParseAlertSmsConfig(config.Config) + cfg, err := ParseAlertSmsConfig(config.Config) if err != nil { return false } @@ -273,6 +262,10 @@ func CheckSMSSendLimit(method string) bool { return true } +func IsAlertConfigEnabled(config model.AlertConfig) bool { + return config.Status == constant.AlertEnable +} + type Settings struct { NoticeAlert Category `json:"noticeAlert"` ResourceAlert Category `json:"resourceAlert"` diff --git a/agent/utils/alert_push/alert_push.go b/agent/utils/alert_push/alert_push.go index 3b149b83e934..4345d78af4b5 100644 --- a/agent/utils/alert_push/alert_push.go +++ b/agent/utils/alert_push/alert_push.go @@ -1,15 +1,17 @@ package alert_push import ( + "strconv" + "strings" + "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/constant" "github.com/1Panel-dev/1Panel/agent/global" alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert" "github.com/1Panel-dev/1Panel/agent/utils/xpack" "github.com/jinzhu/copier" - "strconv" - "strings" ) func PushAlert(pushAlert dto.PushAlert) error { @@ -28,87 +30,132 @@ func PushAlert(pushAlert dto.PushAlert) error { methods := strings.Split(alert.Method, ",") for _, m := range methods { m = strings.TrimSpace(m) - switch m { - case constant.SMS: - if !alertUtil.CheckSMSSendLimit(constant.SMS) { - continue - } - todayCount, _, err := alertRepo.LoadTaskCount(alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), constant.SMS) - if err != nil || alert.SendCount <= todayCount { - continue - } - var create = dto.AlertLogCreate{ - Type: alertUtil.GetCronJobType(alert.Type), - AlertId: alert.ID, - Count: todayCount + 1, - } - err = xpack.AlertProvider.CreateTaskScanSMSAlertLog(alert, alert.Type, create, pushAlert, constant.SMS) - if err != nil { - global.LOG.Errorf("%s alert sms push failed: %v", alert.Type, err) - continue - } - alertUtil.CreateNewAlertTask(strconv.Itoa(int(pushAlert.EntryID)), alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), constant.SMS) - case constant.Email: - todayCount, _, err := alertRepo.LoadTaskCount(alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), constant.Email) - if err != nil || alert.SendCount <= todayCount { - continue - } - var create = dto.AlertLogCreate{ - Type: alertUtil.GetCronJobType(alert.Type), - AlertId: alert.ID, - Count: todayCount + 1, - } - transport := xpack.MultiNodeProvider.LoadRequestTransport() - agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() - err = alertUtil.CreateTaskScanEmailAlertLog(alert, create, pushAlert, constant.Email, transport, agentInfo) - if err != nil { - global.LOG.Errorf("%s alert email push failed: %v", alert.Type, err) - continue - } - alertUtil.CreateNewAlertTask(strconv.Itoa(int(pushAlert.EntryID)), alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), constant.Email) - case constant.Bark: - todayCount, _, err := alertRepo.LoadTaskCount(alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), constant.Bark) - if err != nil || alert.SendCount <= todayCount { - continue - } - var create = dto.AlertLogCreate{ - Type: alertUtil.GetCronJobType(alert.Type), - AlertId: alert.ID, - Count: todayCount + 1, - } - transport := xpack.MultiNodeProvider.LoadRequestTransport() - agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() - params := alertUtil.CreateAlertParams(alertUtil.GetCronJobTypeName(pushAlert.Param)) - alertDetail := alertUtil.ProcessAlertDetail(alert, pushAlert.TaskName, params, constant.Bark) - alertRule := alertUtil.ProcessAlertRule(alert) - create.AlertRule = alertRule - create.AlertDetail = alertDetail - err = alertUtil.CreateBarkAlertLog(create, alert, params, transport, agentInfo) - if err != nil { - global.LOG.Errorf("%s alert bark push failed: %v", alert.Type, err) - continue - } - alertUtil.CreateNewAlertTask(strconv.Itoa(int(pushAlert.EntryID)), alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), constant.Bark) - case constant.WeCom, constant.DingTalk, constant.FeiShu: - todayCount, _, err := alertRepo.LoadTaskCount(alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), m) - if err != nil || alert.SendCount <= todayCount { - continue - } - var create = dto.AlertLogCreate{ - Type: alertUtil.GetCronJobType(alert.Type), - AlertId: alert.ID, - Count: todayCount + 1, - } - transport := xpack.MultiNodeProvider.LoadRequestTransport() - agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() - err = xpack.AlertProvider.CreateTaskScanWebhookAlertLog(alert, alert.Type, create, pushAlert, m, transport, agentInfo) - if err != nil { - global.LOG.Errorf("%s alert %s webhook push failed: %v", alert.Type, m, err) - continue - } - alertUtil.CreateNewAlertTask(strconv.Itoa(int(pushAlert.EntryID)), alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), m) - default: + if configId, err := strconv.ParseUint(m, 10, 64); err == nil { + pushByConfigId(alertRepo, alert, pushAlert, uint(configId)) + } else { + pushByLegacyMethod(alertRepo, alert, pushAlert, m) } } return nil } + +func pushByConfigId(alertRepo repo.IAlertRepo, alert dto.AlertDTO, pushAlert dto.PushAlert, configId uint) { + config, err := alertRepo.GetConfigById(configId) + if err != nil { + global.LOG.Errorf("alert config not found for id %d: %v", configId, err) + return + } + sendAlert(alertRepo, alert, pushAlert, config) +} + +func pushByLegacyMethod(alertRepo repo.IAlertRepo, alert dto.AlertDTO, pushAlert dto.PushAlert, method string) { + typeMap := map[string]string{ + "mail": constant.Email, + constant.Bark: constant.Bark, + constant.SMS: constant.SMS, + } + configType := method + if mapped, ok := typeMap[method]; ok { + configType = mapped + } + config, err := alertRepo.GetConfig(alertRepo.WithByType(configType)) + if err != nil { + global.LOG.Errorf("alert config not found for type %s: %v", configType, err) + return + } + sendAlert(alertRepo, alert, pushAlert, config) +} + +func sendAlert(alertRepo repo.IAlertRepo, alert dto.AlertDTO, pushAlert dto.PushAlert, config model.AlertConfig) { + if !alertUtil.IsAlertConfigEnabled(config) { + return + } + methodStr := strconv.Itoa(int(config.ID)) + switch config.Type { + case constant.SMS: + if !alertUtil.CheckSMSSendLimit(config, methodStr) { + return + } + todayCount, _, err := alertRepo.LoadTaskCount(alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), methodStr) + if err != nil || alert.SendCount <= todayCount { + return + } + create := dto.AlertLogCreate{ + Type: alertUtil.GetCronJobType(alert.Type), + AlertId: alert.ID, + Count: todayCount + 1, + Method: methodStr, + } + err = xpack.AlertProvider.CreateTaskScanSMSAlertLog(alert, alert.Type, create, pushAlert, config, methodStr) + if err != nil { + global.LOG.Errorf("%s alert sms push failed: %v", alert.Type, err) + return + } + alertUtil.CreateNewAlertTask(strconv.Itoa(int(pushAlert.EntryID)), alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), methodStr) + + case constant.Email: + todayCount, _, err := alertRepo.LoadTaskCount(alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), methodStr) + if err != nil || alert.SendCount <= todayCount { + return + } + create := dto.AlertLogCreate{ + Type: alertUtil.GetCronJobType(alert.Type), + AlertId: alert.ID, + Count: todayCount + 1, + Method: methodStr, + } + transport := xpack.MultiNodeProvider.LoadRequestTransport() + agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() + err = alertUtil.CreateTaskScanEmailAlertLog(alert, create, pushAlert, constant.Email, transport, agentInfo, config) + if err != nil { + global.LOG.Errorf("%s alert email push failed: %v", alert.Type, err) + return + } + alertUtil.CreateNewAlertTask(strconv.Itoa(int(pushAlert.EntryID)), alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), methodStr) + + case constant.Bark: + todayCount, _, err := alertRepo.LoadTaskCount(alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), methodStr) + if err != nil || alert.SendCount <= todayCount { + return + } + create := dto.AlertLogCreate{ + Type: alertUtil.GetCronJobType(alert.Type), + AlertId: alert.ID, + Count: todayCount + 1, + Method: methodStr, + } + transport := xpack.MultiNodeProvider.LoadRequestTransport() + agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() + params := alertUtil.CreateAlertParams(alertUtil.GetCronJobTypeName(pushAlert.Param)) + alertDetail := alertUtil.ProcessAlertDetail(alert, pushAlert.TaskName, params, constant.Bark) + alertRule := alertUtil.ProcessAlertRule(alert) + create.AlertRule = alertRule + create.AlertDetail = alertDetail + err = alertUtil.CreateBarkAlertLog(create, alert, params, transport, agentInfo, config) + if err != nil { + global.LOG.Errorf("%s alert bark push failed: %v", alert.Type, err) + return + } + alertUtil.CreateNewAlertTask(strconv.Itoa(int(pushAlert.EntryID)), alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), methodStr) + + case constant.WeCom, constant.DingTalk, constant.FeiShu: + todayCount, _, err := alertRepo.LoadTaskCount(alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), methodStr) + if err != nil || alert.SendCount <= todayCount { + return + } + create := dto.AlertLogCreate{ + Type: alertUtil.GetCronJobType(alert.Type), + AlertId: alert.ID, + Count: todayCount + 1, + Method: methodStr, + } + transport := xpack.MultiNodeProvider.LoadRequestTransport() + agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() + err = xpack.AlertProvider.CreateTaskScanWebhookAlertLog(alert, alert.Type, create, pushAlert, config, transport, agentInfo) + if err != nil { + global.LOG.Errorf("%s alert %s webhook push failed: %v", alert.Type, methodStr, err) + return + } + alertUtil.CreateNewAlertTask(strconv.Itoa(int(pushAlert.EntryID)), alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), methodStr) + } +} diff --git a/agent/utils/xpack/helper/alert.go b/agent/utils/xpack/helper/alert.go index f1169fdb0764..1d4f758137be 100644 --- a/agent/utils/xpack/helper/alert.go +++ b/agent/utils/xpack/helper/alert.go @@ -1,9 +1,15 @@ package helper import ( + "encoding/json" + "fmt" "net/http" "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/constant" + alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert" + "github.com/1Panel-dev/1Panel/agent/utils/bark" "github.com/1Panel-dev/1Panel/agent/utils/xpack/providers" ) @@ -13,20 +19,63 @@ func NewIAlertProvider() providers.AlertProvider { return &alertHelper{} } -func (a *alertHelper) CreateTaskScanSMSAlertLog(alert dto.AlertDTO, alertType string, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string) error { - return nil +func (a *alertHelper) CreateTaskScanSMSAlertLog(alert dto.AlertDTO, alertType string, create dto.AlertLogCreate, pushAlert dto.PushAlert, config model.AlertConfig, method string) error { + params := alertUtil.CreateAlertParams(alertUtil.GetCronJobTypeName(pushAlert.Param)) + create.AlertRule = alertUtil.ProcessAlertRule(alert) + create.AlertDetail = alertUtil.ProcessAlertDetail(alert, pushAlert.TaskName, params, method) + if create.Status == "" { + create.Status = constant.AlertSuccess + } + return alertUtil.SaveAlertLog(create, &model.AlertLog{}) } -func (a *alertHelper) CreateSMSAlertLog(alertType string, info dto.AlertDTO, create dto.AlertLogCreate, project string, params []dto.Param, method string) error { - return nil +func (a *alertHelper) CreateSMSAlertLog(alertType string, info dto.AlertDTO, create dto.AlertLogCreate, project string, params []dto.Param, config model.AlertConfig, method string) error { + create.AlertRule = alertUtil.ProcessAlertRule(info) + if create.AlertDetail == "" { + create.AlertDetail = alertUtil.ProcessAlertDetail(info, project, params, method) + } + if create.Status == "" { + create.Status = constant.AlertSuccess + } + return alertUtil.SaveAlertLog(create, &model.AlertLog{}) } -func (a *alertHelper) CreateTaskScanWebhookAlertLog(alert dto.AlertDTO, alertType string, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string, transport *http.Transport, agentInfo *dto.AgentInfo) error { - return nil +func (a *alertHelper) CreateTaskScanWebhookAlertLog(alert dto.AlertDTO, alertType string, create dto.AlertLogCreate, pushAlert dto.PushAlert, config model.AlertConfig, transport *http.Transport, agentInfo *dto.AgentInfo) error { + params := alertUtil.CreateAlertParams(alertUtil.GetCronJobTypeName(pushAlert.Param)) + create.AlertRule = alertUtil.ProcessAlertRule(alert) + create.AlertDetail = alertUtil.ProcessAlertDetail(alert, pushAlert.TaskName, params, config.Type) + return a.CreateWebhookAlertLog(alertType, alert, create, pushAlert.TaskName, params, config, transport, agentInfo) } -func (a *alertHelper) CreateWebhookAlertLog(alertType string, info dto.AlertDTO, create dto.AlertLogCreate, project string, params []dto.Param, method string, transport *http.Transport, agentInfo *dto.AgentInfo) error { - return nil +func (a *alertHelper) CreateWebhookAlertLog(alertType string, info dto.AlertDTO, create dto.AlertLogCreate, project string, params []dto.Param, config model.AlertConfig, transport *http.Transport, agentInfo *dto.AgentInfo) error { + var webhookInfo dto.AlertWebhookConfig + if err := json.Unmarshal([]byte(config.Config), &webhookInfo); err != nil { + create.Message = err.Error() + create.Status = constant.AlertError + return alertUtil.SaveAlertLog(create, &model.AlertLog{}) + } + if webhookInfo.Url == "" { + create.Message = "webhook url is required" + create.Status = constant.AlertError + return alertUtil.SaveAlertLog(create, &model.AlertLog{}) + } + create.AlertRule = alertUtil.ProcessAlertRule(info) + if create.AlertDetail == "" { + create.AlertDetail = alertUtil.ProcessAlertDetail(info, project, params, config.Type) + } + content := alertUtil.GetSendContent(info.Type, params, agentInfo) + if content == "" { + content = fmt.Sprintf("%s: %s", info.Type, info.Title) + } + if err := bark.SendMessage(webhookInfo.Url, "1Panel Alert", content, transport); err != nil { + create.Message = err.Error() + create.Status = constant.AlertError + return alertUtil.SaveAlertLog(create, &model.AlertLog{}) + } + if create.Status == "" { + create.Status = constant.AlertSuccess + } + return alertUtil.SaveAlertLog(create, &model.AlertLog{}) } func (a *alertHelper) GetLicenseErrorAlert() (uint, error) { diff --git a/agent/utils/xpack/providers/alert.go b/agent/utils/xpack/providers/alert.go index 1a09489b5be8..8f885866d44c 100644 --- a/agent/utils/xpack/providers/alert.go +++ b/agent/utils/xpack/providers/alert.go @@ -4,14 +4,15 @@ import ( "net/http" "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" ) type AlertProvider interface { GetNodeErrorAlert() (uint, error) GetLicenseErrorAlert() (uint, error) - CreateTaskScanSMSAlertLog(alert dto.AlertDTO, alertType string, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string) error - CreateSMSAlertLog(alertType string, info dto.AlertDTO, create dto.AlertLogCreate, project string, params []dto.Param, method string) error - CreateTaskScanWebhookAlertLog(alert dto.AlertDTO, alertType string, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string, transport *http.Transport, agentInfo *dto.AgentInfo) error - CreateWebhookAlertLog(alertType string, info dto.AlertDTO, create dto.AlertLogCreate, project string, params []dto.Param, method string, transport *http.Transport, agentInfo *dto.AgentInfo) error + CreateTaskScanSMSAlertLog(alert dto.AlertDTO, alertType string, create dto.AlertLogCreate, pushAlert dto.PushAlert, config model.AlertConfig, method string) error + CreateSMSAlertLog(alertType string, info dto.AlertDTO, create dto.AlertLogCreate, project string, params []dto.Param, config model.AlertConfig, method string) error + CreateTaskScanWebhookAlertLog(alert dto.AlertDTO, alertType string, create dto.AlertLogCreate, pushAlert dto.PushAlert, config model.AlertConfig, transport *http.Transport, agentInfo *dto.AgentInfo) error + CreateWebhookAlertLog(alertType string, info dto.AlertDTO, create dto.AlertLogCreate, project string, params []dto.Param, config model.AlertConfig, transport *http.Transport, agentInfo *dto.AgentInfo) error } diff --git a/core/constant/alert.go b/core/constant/alert.go index bf7709cbcebe..541e1c09c1ba 100644 --- a/core/constant/alert.go +++ b/core/constant/alert.go @@ -3,7 +3,7 @@ package constant const ( WeChat = "wechat" SMS = "sms" - Email = "mail" + Email = "email" WeCom = "weCom" DingTalk = "dingTalk" FeiShu = "feiShu" diff --git a/core/init/migration/helper/alert.go b/core/init/migration/helper/alert.go new file mode 100644 index 000000000000..6e8da8d45d0b --- /dev/null +++ b/core/init/migration/helper/alert.go @@ -0,0 +1,21 @@ +package helper + +type AlertAuditAlert struct { + ID uint `gorm:"primaryKey"` + CreateUser string `gorm:"type:varchar(256)"` + UpdateUser string `gorm:"type:varchar(256)"` +} + +func (AlertAuditAlert) TableName() string { + return "alerts" +} + +type AlertAuditConfig struct { + ID uint `gorm:"primaryKey"` + CreateUser string `gorm:"type:varchar(256)"` + UpdateUser string `gorm:"type:varchar(256)"` +} + +func (AlertAuditConfig) TableName() string { + return "alert_configs" +} diff --git a/core/init/migration/migrate.go b/core/init/migration/migrate.go index ffbf0bb051c1..fc57077561e8 100644 --- a/core/init/migration/migrate.go +++ b/core/init/migration/migrate.go @@ -46,6 +46,7 @@ func Init() { migrations.AddOperationLogUser, migrations.AddLoginLogUser, migrations.AddIsOfflineSetting, + migrations.AddAlertAuditUser, }) 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 9ba2cb4747c4..6faf9238eee7 100644 --- a/core/init/migration/migrations/init.go +++ b/core/init/migration/migrations/init.go @@ -11,6 +11,7 @@ import ( "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/global" "github.com/1Panel-dev/1Panel/core/init/migration/helper" @@ -1284,3 +1285,44 @@ var AddIsOfflineSetting = &gormigrate.Migration{ return nil }, } + +var AddAlertAuditUser = &gormigrate.Migration{ + ID: "20260602-add-alert-audit-user", + Migrate: func(tx *gorm.DB) error { + if !global.AlertDB.Migrator().HasTable(&helper.AlertAuditAlert{}) || !global.AlertDB.Migrator().HasTable(&helper.AlertAuditConfig{}) { + return nil + } + if err := global.AlertDB.AutoMigrate(&helper.AlertAuditAlert{}, &helper.AlertAuditConfig{}); err != nil { + return err + } + username, err := repo.NewISettingRepo().GetValueByKey("UserName") + if err != nil || strings.TrimSpace(username) == "" { + username = global.CONF.Base.Username + } + username = strings.TrimSpace(username) + if username == "" { + return nil + } + updates := []struct { + table string + field string + }{ + {table: "alerts", field: "create_user"}, + {table: "alerts", field: "update_user"}, + {table: "alert_configs", field: "create_user"}, + {table: "alert_configs", field: "update_user"}, + } + for _, item := range updates { + if err := global.AlertDB.Exec(fmt.Sprintf( + "UPDATE %s SET %s = ? WHERE %s IS NULL OR %s = ''", + item.table, + item.field, + item.field, + item.field, + ), username).Error; err != nil { + return err + } + } + return nil + }, +} diff --git a/core/init/router/proxy.go b/core/init/router/proxy.go index 236b0951ca81..93754ce482c2 100644 --- a/core/init/router/proxy.go +++ b/core/init/router/proxy.go @@ -47,6 +47,10 @@ func Proxy() gin.HandlerFunc { return } + if userName := middleware.LoadOperationUser(c); userName != "" { + c.Request.Header.Set("X-Panel-User", url.QueryEscape(userName)) + } + if reqPath == "/api/v2/hosts/terminal/local" && (currentNode == "local" || len(currentNode) == 0) { proxyLocalAgent(c) return diff --git a/core/middleware/operation.go b/core/middleware/operation.go index 1b28c0aaba0f..faeac8d24b20 100644 --- a/core/middleware/operation.go +++ b/core/middleware/operation.go @@ -113,7 +113,7 @@ func OperationLog() gin.HandlerFunc { c.Next() - record.User = loadOperationUser(c) + record.User = LoadOperationUser(c) if len(operationDic.BeforeFunctions) != 0 { if needAgentResolve { @@ -181,7 +181,7 @@ func OperationLog() gin.HandlerFunc { } } -func loadOperationUser(c *gin.Context) string { +func LoadOperationUser(c *gin.Context) string { sessionUser, ok := c.Get(psessionUtils.GinContextSessionUserKey) if ok { psession, ok := sessionUser.(psessionUtils.SessionUser) diff --git a/frontend/src/api/interface/alert.ts b/frontend/src/api/interface/alert.ts index 66aaba141919..ed5d0bd29433 100644 --- a/frontend/src/api/interface/alert.ts +++ b/frontend/src/api/interface/alert.ts @@ -14,6 +14,8 @@ export namespace Alert { sendCount: number; sendMethod: string[]; advancedParams: string; + createUser?: string; + updateUser?: string; } export interface AlertDetail { @@ -138,6 +140,13 @@ export namespace Alert { title: string; config: string; status: string; + createUser?: string; + updateUser?: string; + } + + export interface AlertConfigPageReq { + page: number; + pageSize: number; } export interface AlertConfigUpdateReq { @@ -188,6 +197,7 @@ export namespace Alert { port?: number; encryption?: string; recipient?: string; + recipients?: string[]; }; } @@ -210,6 +220,12 @@ export namespace Alert { config: { displayName?: string; url?: string; + webhooks?: WebhookItem[]; }; } + + export interface WebhookItem { + displayName: string; + url: string; + } } diff --git a/frontend/src/api/modules/alert.ts b/frontend/src/api/modules/alert.ts index 12911d02367e..868d58934764 100644 --- a/frontend/src/api/modules/alert.ts +++ b/frontend/src/api/modules/alert.ts @@ -62,6 +62,15 @@ export const ListAlertConfigs = (currentNode?: string) => { ); }; +export const PageAlertConfigs = (req: Alert.AlertConfigPageReq, currentNode?: string) => { + return http.post>( + `/alert/config/search`, + req, + undefined, + currentNode ? { CurrentNode: currentNode } : undefined, + ); +}; + export const DeleteAlertConfig = (req: Alert.DelReq) => { return http.post(`/alert/config/del`, req); }; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 1d8a19a16e92..3917ae251c5d 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -123,9 +123,11 @@ const message = { group: 'Group', default: 'Default', createdAt: 'Created', + creator: 'Creator', publishedAt: 'Published', date: 'Date', updatedAt: 'Updated', + updater: 'Updater', operate: 'Actions', message: 'Message', description: 'Description', @@ -228,6 +230,7 @@ const message = { requiredSelect: 'Select an item in the list', illegalChar: 'Injection of characters & ; $ \' ` ( ) " > < | is currently not supported', illegalInput: "This field mustn't contains illegal characters.", + duplicate: 'This value must be unique.', commonName: 'This field must start with non-special characters and must consist of English, Chinese, numbers, ".", "-", and "_" characters with a length of 1-128.', userName: 'This field must consist of English, Chinese, numbers and "_" characters with a length of 3-30.', @@ -5452,6 +5455,13 @@ const message = { webhookName: 'Bot name', webhookUrl: 'Webhook URL', alertConfigProHelper: 'Commercial Edition also supports WeCom, DingTalk, Feishu, and SMS alerts.', + recipientPlaceholder: 'Please enter recipient email address', + addRecipient: 'Add Recipient', + webhookItem: 'Webhook', + addWebhook: 'Add Webhook', + selectAlertType: 'Select Alert Type', + configDetail: 'Config Details', + webhookCount: ' Webhook(s)', }, theme: { lingXiaGold: 'LXware Gold', diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index 586bd28ab6bf..c87d05ac2392 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -229,6 +229,7 @@ const message = { requiredSelect: 'Seleccione un elemento de la lista', illegalChar: 'Actualmente no se admite la inyección de caracteres & ; $ \' ` ( ) " > < |', illegalInput: 'Este campo no debe contener caracteres no permitidos.', + duplicate: 'Este valor debe ser único.', commonName: 'Este campo debe comenzar con un carácter no especial y debe estar compuesto por letras, caracteres chinos, números, ".", "-", y "_" con una longitud de 1 a 128.', userName: diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index e26eada1cdd6..00669266037c 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -228,6 +228,7 @@ const message = { requiredSelect: 'リスト内のアイテムを選択します', illegalChar: '現在、文字 & ; $ \' ` ( ) " > < | の注入はサポートされていません', illegalInput: 'このフィールドには違法なキャラクターが含まれてはなりません。', + duplicate: 'この値は一意である必要があります。', commonName: 'このフィールドは、特別なキャラクターではなく、英語、中国語、数字で構成されている必要があります。「。」、「」、および「_」文字が1〜128の文字で構成されている必要があります。', userName: '特殊文字で始まらない、英字、漢字、数字、および_をサポート、長さ3-30', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index 4018d4bf6305..5ff71c6ff6bb 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -226,6 +226,7 @@ const message = { requiredSelect: '목록에서 항목을 선택하세요', illegalChar: '현재 & ; $ \' ` ( ) " > < | 문자 주입은 지원되지 않습니다', illegalInput: '이 필드에는 유효하지 않은 문자가 포함될 수 없습니다.', + duplicate: '이 값은 고유해야 합니다.', commonName: '이 필드는 특수 문자로 시작할 수 없으며, 영어, 한자, 숫자, ".", "-", "_" 문자로 구성되어야 하며 길이는 1-128자여야 합니다.', userName: '특수 문자로 시작하지 않고, 영어, 한국어, 숫자 및 _, 길이 3-30 지원', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index dd3a9ff6a146..eb5d73119b2e 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -229,6 +229,7 @@ const message = { requiredSelect: 'Pilih satu item dalam senarai', illegalChar: 'Suntikan aksara & ; $ \' ` ( ) " > < | tidak disokong buat masa ini', illegalInput: 'Ruangan ini tidak boleh mengandungi aksara tidak sah.', + duplicate: 'Nilai ini mesti unik.', commonName: 'Ruangan ini mesti bermula dengan aksara bukan khas dan mesti terdiri daripada aksara rumi, Cina, nombor, ".", "-", dan "_" dengan panjang 1-128 aksara.', userName: diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index 5a793da6ba33..74c47bfce9c8 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -227,6 +227,7 @@ const message = { requiredSelect: 'Selecione um item na lista', illegalChar: 'Atualmente não há suporte para injeção dos caracteres & ; $ \' ` ( ) " > < |', illegalInput: 'Este campo não deve conter caracteres ilegais.', + duplicate: 'Este valor deve ser único.', commonName: 'Este campo deve começar com caracteres não especiais e consistir em letras, números, ".", "-", e "_" com comprimento de 1-128.', userName: 'Suporta não começar com caracteres especiais, inglês, chinês, números e _, comprimento 3-30', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index e1e91efb14f6..fb79888dce32 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -227,6 +227,7 @@ const message = { requiredSelect: 'Выберите элемент из списка', illegalChar: 'В настоящее время не поддерживается вставка символов & ; $ \' ` ( ) " > < |', illegalInput: 'Это поле не должно содержать недопустимых символов.', + duplicate: 'Значение должно быть уникальным.', commonName: 'Это поле должно начинаться с неспециальных символов и должно состоять из английских букв, китайских иероглифов, цифр, ".", "-" и "_" длиной 1-128.', userName: 'Поддерживает начало без специальных символов, английский, китайский, цифры и _, длина 3-30', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index cc977993cbfe..1e31e037cd01 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -231,6 +231,7 @@ const message = { requiredSelect: 'Listeden bir öğe seçin', illegalChar: '& ; $ ` ( ) " > < | karakterlerinin enjekte edilmesi şu anda desteklenmiyor', illegalInput: 'Bu alan yasadışı karakterler içermemelidir.', + duplicate: 'Bu değer benzersiz olmalıdır.', commonName: 'Bu alan özel olmayan karakterlerle başlamalı ve İngilizce, Çince, rakam, ".", "-", ve "_" karakterlerinden oluşmalı, uzunluk 1-128 olmalıdır.', userName: 'Bu alan İngilizce, Çince, rakam ve "_" karakterlerinden oluşmalı, uzunluk 3-30 olmalıdır.', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 8db3b58a61cc..9ab8053b06ea 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -115,8 +115,10 @@ const message = { group: '分组', default: '默认', createdAt: '创建时间', + creator: '创建人', date: '时间', updatedAt: '更新时间', + updater: '更新人', operate: '操作', message: '信息', description: '描述', @@ -212,6 +214,7 @@ const message = { requiredSelect: '请选择必选项', illegalChar: '暂不支持注入字符 & ; $ \' ` ( ) " > < |', illegalInput: '输入框中存在不合法字符', + duplicate: '内容不能重复', commonName: '支持非特殊字符开头,英文、中文、数字、.-和_,长度1-128', userName: '支持非特殊字符开头、英文、中文、数字和_,长度3-30', simpleName: '支持非下划线开头,英文、数字、_,长度3-30', @@ -4886,6 +4889,7 @@ const message = { dingTalk: '钉钉通知', feiShu: '飞书通知', mail: '邮箱通知', + email: '邮箱通知', weCom: '企业微信', bark: 'Bark', sendCountRulesHelper: '到期前发送告警的总数(每日仅发送一次)', @@ -5058,6 +5062,13 @@ const message = { webhookName: '机器人名称', webhookUrl: 'Webhook 地址', alertConfigProHelper: '商业版额外支持企业微信、钉钉、飞书及短信告警。', + recipientPlaceholder: '请输入收件人邮箱地址', + addRecipient: '添加收件人', + webhookItem: 'Webhook', + addWebhook: '添加 Webhook', + selectAlertType: '选择告警类型', + configDetail: '配置详情', + webhookCount: '个 Webhook', }, theme: { lingXiaGold: '凌霞金', diff --git a/frontend/src/views/cronjob/cronjob/operate/index.vue b/frontend/src/views/cronjob/cronjob/operate/index.vue index 67279027a60e..367490dba1f1 100644 --- a/frontend/src/views/cronjob/cronjob/operate/index.vue +++ b/frontend/src/views/cronjob/cronjob/operate/index.vue @@ -712,45 +712,44 @@ > - - - - - - - + + +
+ + {{ opt.label }} + + + {{ opt.typeLabel }} + +
+
+
@@ -837,7 +836,7 @@ import CodemirrorPro from '@/components/codemirror-pro/index.vue'; import InputTag from '@/components/input-tag/index.vue'; import LayoutCol from '@/components/layout-col/form.vue'; import CleanLogConfig from '@/views/cronjob/cronjob/config/clean-log.vue'; -import { reactive, ref } from 'vue'; +import { reactive, ref, computed, onMounted } from 'vue'; import { Rules } from '@/global/form-rules'; import { listBackupOptions } from '@/api/modules/backup'; import i18n from '@/lang'; @@ -851,6 +850,8 @@ import { useRouter } from 'vue-router'; import { listContainer } from '@/api/modules/container'; import { Database } from '@/api/interface/database'; import { listAppInstalled } from '@/api/modules/app'; +import { Alert } from '@/api/interface/alert'; +import { ListAlertConfigs } from '@/api/modules/alert'; import { loadDefaultSpec, loadDefaultSpecCustom, @@ -883,6 +884,88 @@ const baseDir = ref(); const isCreate = ref(); const defaultGroupID = ref(); + +const alertConfigs = ref([]); +const loadAlertConfigs = async () => { + try { + const res = await ListAlertConfigs(); + alertConfigs.value = res.data || []; + } catch {} +}; +onMounted(() => { + loadAlertConfigs(); +}); + +const alertConfigOptions = computed(() => { + const hiddenTypes: string[] = []; + if (isIntl.value || isEE.value) hiddenTypes.push('sms'); + return alertConfigs.value + .filter((c) => c.status === 'Enable' && c.type !== 'common' && !hiddenTypes.includes(c.type)) + .map((c) => ({ + value: String(c.id), + label: getAlertConfigOptionLabel(c), + type: c.type, + })); +}); + +const legacyAlertMethodTypeMap: Record = { + mail: 'email', + email: 'email', + sms: 'sms', + bark: 'bark', + weCom: 'weCom', + dingTalk: 'dingTalk', + feiShu: 'feiShu', +}; + +const normalizeAlertMethodItems = (methods: string[]) => { + return methods.map((method) => { + if (/^\d+$/.test(method)) return method; + const configType = legacyAlertMethodTypeMap[method]; + const matched = alertConfigOptions.value.find((item) => item.type === configType); + return matched?.value || method; + }); +}; + +const groupedAlertConfigOptions = computed(() => { + const typeMap = new Map(); + for (const opt of alertConfigOptions.value) { + if (!typeMap.has(opt.type)) typeMap.set(opt.type, []); + typeMap.get(opt.type)!.push({ value: opt.value, label: opt.label }); + } + const groups: { + type: string; + options: { value: string; label: string; typeLabel: string }[]; + }[] = []; + const typeOrder = ['email', 'sms', 'weCom', 'dingTalk', 'feiShu', 'bark']; + for (const t of typeOrder) { + if (typeMap.has(t)) { + const typeLabel = getConfigTypeLabel(t); + groups.push({ + type: t, + options: typeMap.get(t)!.map((item) => ({ + ...item, + typeLabel, + })), + }); + } + } + return groups; +}); + +const getConfigTypeLabel = (type: string): string => { + return i18n.global.t(`xpack.alert.${type === 'email' ? 'mail' : type}`); +}; + +const getAlertConfigOptionLabel = (c: Alert.AlertConfigInfo): string => { + try { + const cfg = JSON.parse(c.config || '{}'); + return cfg.displayName || i18n.global.t(`xpack.alert.${c.type === 'email' ? 'mail' : c.type}`); + } catch { + return i18n.global.t(`xpack.alert.${c.type === 'email' ? 'mail' : c.type}`); + } +}; + const form = reactive({ id: 0, name: '', @@ -1040,7 +1123,7 @@ const search = async () => { form.alertCount = res.data.alertCount || 3; form.alertTitle = res.data.alertTitle; if (res.data.alertMethod) { - form.alertMethodItems = res.data.alertMethod.split(',') || []; + form.alertMethodItems = normalizeAlertMethodItems(res.data.alertMethod.split(',') || []); } else { form.alertMethodItems = []; } @@ -1644,6 +1727,36 @@ onMounted(() => { font-size: 12px; margin-top: 5px; } + +.alert-config-option { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + width: 100%; + min-width: 0; +} + +.alert-config-option__name { + flex: 1 1 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.alert-config-option__tag { + flex: 0 0 auto; +} + +:global(.alert-config-method-dropdown .el-select-dropdown__item) { + padding-right: 52px; +} + +:global(.alert-config-method-dropdown .el-select-dropdown__item.is-selected::after) { + right: 16px; +} + .logText { line-height: 22px; font-size: 12px; diff --git a/frontend/src/views/setting/alert/dash/index.vue b/frontend/src/views/setting/alert/dash/index.vue index 572073c7d235..9d9da6ec133b 100644 --- a/frontend/src/views/setting/alert/dash/index.vue +++ b/frontend/src/views/setting/alert/dash/index.vue @@ -25,12 +25,12 @@ /> @@ -131,6 +131,17 @@ {{ formatRule(row) }} + + + { return ruleTemplates[row.type] ? ruleTemplates[row.type]() : ''; }; +const configMap = ref>(new Map()); + +const loadConfigMap = async () => { + try { + const res = await PageAlertConfigs({ page: 1, pageSize: 1000 }); + const map = new Map(); + for (const c of res.data?.items || []) { + map.set(String(c.id), c); + } + configMap.value = map; + } catch {} +}; + const formatMethod = (row: Alert.AlertInfo) => { if (!row.method) return ''; - const sendMethod = row.method.split(',').filter(Boolean); - const methodStr = sendMethod.map((item) => t('xpack.alert.' + item)).join('|'); + const resolveMethodLabel = (method: string) => { + const config = configMap.value.get(method); + if (config) { + const typeLabel = i18n.global.t(`xpack.alert.${config.type === 'email' ? 'mail' : config.type}`); + try { + const cfg = JSON.parse(config.config || '{}'); + const name = cfg.displayName || cfg.sender || cfg.phone || ''; + return name ? `${name}(${typeLabel})` : typeLabel; + } catch { + return typeLabel; + } + } + const oldLabel = i18n.global.t(`xpack.alert.${method}`); + if (!oldLabel || oldLabel === `xpack.alert.${method}` || oldLabel.includes('.')) { + return /^\d+$/.test(method) ? `#${method}` : method; + } + return oldLabel; + }; - return `「${methodStr}」`; + return `「${row.method + .split(',') + .filter(Boolean) + .map((item) => resolveMethodLabel(item.trim())) + .join('|')}」`; }; const search = async () => { @@ -285,6 +329,7 @@ const search = async () => { order: paginationConfig.order, }; try { + await loadConfigMap(); const res = await SearchAlerts(params); data.value = res.data.items || []; paginationConfig.total = res.data.total || 0; diff --git a/frontend/src/views/setting/alert/dash/task/index.vue b/frontend/src/views/setting/alert/dash/task/index.vue index edb24514a4f2..986ae1c95d2e 100644 --- a/frontend/src/views/setting/alert/dash/task/index.vue +++ b/frontend/src/views/setting/alert/dash/task/index.vue @@ -319,34 +319,40 @@ - - - - - - - + + +
+ + {{ $t('commons.table.all') }} + +
+
+ v-for="opt in configOptions" + :key="opt.value" + :value="opt.value" + :label="opt.label" + :disabled="opt.disabled" + > +
+ + {{ opt.label }} + + + {{ opt.typeLabel }} + +
+
@@ -356,6 +362,10 @@ : '' }} + + + + @@ -376,11 +386,11 @@ diff --git a/frontend/src/views/setting/alert/setting/email/index.vue b/frontend/src/views/setting/alert/setting/email/index.vue index a042878741d2..091d72975e38 100644 --- a/frontend/src/views/setting/alert/setting/email/index.vue +++ b/frontend/src/views/setting/alert/setting/email/index.vue @@ -69,8 +69,33 @@ {{ $t('xpack.alert.tlsHelper') }} - - + +
+
+ + + + +
+ + + {{ $t('xpack.alert.addRecipient') }} + +
@@ -97,6 +122,7 @@ import { MsgError, MsgSuccess } from '@/utils/message'; import { FormInstance } from 'element-plus'; import { TestAlertConfig, UpdateAlertConfig } from '@/api/modules/alert'; import { Rules } from '@/global/form-rules'; +import { Plus, Minus } from '@element-plus/icons-vue'; const emit = defineEmits<{ (e: 'search'): void }>(); @@ -105,7 +131,23 @@ const rules = { sender: [Rules.requiredInput], host: [Rules.requiredInput], port: [Rules.requiredInput], - recipient: [Rules.requiredInput], + recipients: [ + { + validator: (rule: any, value: string[], callback: any) => { + if (!value || value.length === 0) { + callback(new Error(i18n.global.t('commons.rule.requiredInput'))); + } else { + const hasEmpty = value.some((item) => !item || !item.trim()); + if (hasEmpty) { + callback(new Error(i18n.global.t('commons.rule.requiredInput'))); + } else { + callback(); + } + } + }, + trigger: 'blur', + }, + ], }; interface Config { status: string; @@ -116,7 +158,8 @@ interface Config { host: string; port: number; encryption: string; - recipient: string; + recipient?: string; + recipients?: string[]; } interface DialogProps { id: number; @@ -126,7 +169,7 @@ const drawerVisible = ref(); const loading = ref(); const form = reactive({ - id: undefined, + id: undefined as number | undefined, config: { displayName: '', sender: '', @@ -136,7 +179,7 @@ const form = reactive({ port: 465, encryption: 'NONE', status: 'Enable', - recipient: '', + recipients: [''] as string[], }, }); const isOK = ref(false); @@ -144,10 +187,39 @@ const formRef = ref(); const acceptParams = (params: DialogProps): void => { form.id = params.id; - form.config = params.config; + form.config.displayName = params.config.displayName || ''; + form.config.sender = params.config.sender || ''; + form.config.password = params.config.password || ''; + form.config.userName = params.config.userName || ''; + form.config.host = params.config.host || ''; + form.config.port = params.config.port || 465; + form.config.encryption = params.config.encryption || 'NONE'; + form.config.status = params.config.status || 'Enable'; + + if (params.config.recipients && params.config.recipients.length > 0) { + form.config.recipients = [...params.config.recipients]; + } else if (params.config.recipient) { + form.config.recipients = params.config.recipient + .split(',') + .map((r) => r.trim()) + .filter((r) => r); + } else { + form.config.recipients = ['']; + } + drawerVisible.value = true; }; +const addRecipient = () => { + form.config.recipients.push(''); +}; + +const removeRecipient = (index: number) => { + if (form.config.recipients.length > 1) { + form.config.recipients.splice(index, 1); + } +}; + const onSave = async (formEl: FormInstance | undefined) => { if (!formEl) return; formEl.validate(async (valid) => { @@ -155,7 +227,10 @@ const onSave = async (formEl: FormInstance | undefined) => { loading.value = true; try { form.config.status = 'Enable'; - const configInfo = form.config; + const configInfo = { + ...form.config, + recipient: form.config.recipients.filter((r) => r && r.trim()).join(','), + }; await UpdateAlertConfig({ id: form.id, type: 'email', @@ -180,7 +255,11 @@ const onTest = async (formEl: FormInstance | undefined) => { if (!valid) return; loading.value = true; try { - await TestAlertConfig(form.config) + const testConfig = { + ...form.config, + recipient: form.config.recipients.filter((r) => r && r.trim()).join(','), + }; + await TestAlertConfig(testConfig) .then((res) => { loading.value = false; if (res.data) { diff --git a/frontend/src/views/setting/alert/setting/index.vue b/frontend/src/views/setting/alert/setting/index.vue index a25e9b0e9c1e..974438c33ad5 100644 --- a/frontend/src/views/setting/alert/setting/index.vue +++ b/frontend/src/views/setting/alert/setting/index.vue @@ -38,8 +38,13 @@ - - + + +