diff --git a/common/src/main/java/org/dromara/hertzbeat/common/entity/manager/GeneralConfig.java b/common/src/main/java/org/dromara/hertzbeat/common/entity/manager/GeneralConfig.java new file mode 100644 index 00000000000..4156bd83515 --- /dev/null +++ b/common/src/main/java/org/dromara/hertzbeat/common/entity/manager/GeneralConfig.java @@ -0,0 +1,72 @@ +package org.dromara.hertzbeat.common.entity.manager; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import javax.persistence.*; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; + +import java.time.LocalDateTime; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE; + +/** + * 消息通知服务端配置实体 + * @author zqr10159 + */ +@Entity +@Table(name = "hzb_config") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Message notification server config entity | 消息通知服务端配置实体") +@EntityListeners(AuditingEntityListener.class) +public class GeneralConfig { + + @Id + @Schema(title = "Config type: 1-SMS 2-Email, primary key ", description = "配置类型: 1-短信 2-邮件, 主键", + accessMode = READ_WRITE) + @Min(1) + @NotNull + private Byte type; + + @Schema(title = "Config content", description = "配置内容,格式为json", accessMode = READ_WRITE) + @Column(length = 4096) + private String content; + + @Schema(title = "Whether to enable this policy", + description = "是否启用此配置", + example = "true", accessMode = READ_WRITE) + private boolean enable = true; + + @Schema(title = "The creator of this record", description = "此条记录创建者", example = "tom", accessMode = READ_ONLY) + @CreatedBy + private String creator; + + @Schema(title = "This record was last modified by", + description = "此条记录最新修改者", + example = "tom", accessMode = READ_ONLY) + @LastModifiedBy + private String modifier; + + @Schema(title = "This record creation time (millisecond timestamp)", + description = "记录创建时间", accessMode = READ_ONLY) + @CreatedDate + private LocalDateTime gmtCreate; + + @Schema(title = "Record the latest modification time (timestamp in milliseconds)", + description = "记录最新修改时间", accessMode = READ_ONLY) + @LastModifiedDate + private LocalDateTime gmtUpdate; +} diff --git a/manager/data/hzb_config.sql b/manager/data/hzb_config.sql new file mode 100644 index 00000000000..2845f974343 --- /dev/null +++ b/manager/data/hzb_config.sql @@ -0,0 +1,6 @@ +CREATE TABLE `hzb_config` ( + `type` tinyint(5) NOT NULL COMMENT '配置类型:1-短信,2-邮件', + `content` json DEFAULT NULL COMMENT '配置内容', + `enabled` tinyint(1) DEFAULT NULL COMMENT '标志位,使用原生配置为0,使用数据库配置为1', + PRIMARY KEY (`type`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; \ No newline at end of file diff --git a/manager/src/main/java/org/dromara/hertzbeat/manager/Manager.java b/manager/src/main/java/org/dromara/hertzbeat/manager/Manager.java index 28f59b046f9..e39378a7f21 100644 --- a/manager/src/main/java/org/dromara/hertzbeat/manager/Manager.java +++ b/manager/src/main/java/org/dromara/hertzbeat/manager/Manager.java @@ -20,6 +20,7 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.scheduling.annotation.EnableScheduling; @@ -29,7 +30,7 @@ * */ -@SpringBootApplication +@SpringBootApplication(exclude = MailSenderAutoConfiguration.class) @EnableJpaAuditing @EnableScheduling @EnableJpaRepositories(basePackages = {"org.dromara.hertzbeat"}) diff --git a/manager/src/main/java/org/dromara/hertzbeat/manager/component/alerter/impl/EmailAlertNotifyHandlerImpl.java b/manager/src/main/java/org/dromara/hertzbeat/manager/component/alerter/impl/EmailAlertNotifyHandlerImpl.java index 0802eea2b24..c073d4efebf 100644 --- a/manager/src/main/java/org/dromara/hertzbeat/manager/component/alerter/impl/EmailAlertNotifyHandlerImpl.java +++ b/manager/src/main/java/org/dromara/hertzbeat/manager/component/alerter/impl/EmailAlertNotifyHandlerImpl.java @@ -17,22 +17,28 @@ package org.dromara.hertzbeat.manager.component.alerter.impl; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.dromara.hertzbeat.common.entity.alerter.Alert; +import org.dromara.hertzbeat.common.entity.manager.GeneralConfig; import org.dromara.hertzbeat.common.entity.manager.NoticeReceiver; import org.dromara.hertzbeat.common.util.ResourceBundleUtil; import org.dromara.hertzbeat.manager.component.alerter.AlertNotifyHandler; +import org.dromara.hertzbeat.manager.config.MailConfigProperties; +import org.dromara.hertzbeat.manager.dao.GeneralConfigDao; +import org.dromara.hertzbeat.manager.pojo.dto.NoticeSender; import org.dromara.hertzbeat.manager.service.MailService; import org.dromara.hertzbeat.manager.support.exception.AlertNoticeException; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Component; import javax.mail.internet.MimeMessage; import java.util.Date; +import java.util.Properties; import java.util.ResourceBundle; /** @@ -42,19 +48,54 @@ @Component @RequiredArgsConstructor @Slf4j -@ConditionalOnProperty("spring.mail.username") final class EmailAlertNotifyHandlerImpl implements AlertNotifyHandler { + private final JavaMailSender javaMailSender; + + private final MailConfigProperties mailConfigProperties; + private final MailService mailService; @Value("${spring.mail.username}") private String emailFromUser; + private final GeneralConfigDao generalConfigDao; + + private final ObjectMapper objectMapper; + + private static final Byte TYPE = 2; + private final ResourceBundle bundle = ResourceBundleUtil.getBundle("alerter"); @Override public void send(NoticeReceiver receiver, Alert alert) throws AlertNoticeException { try { + //获取sender + JavaMailSenderImpl sender = (JavaMailSenderImpl) javaMailSender; + try { + GeneralConfig emailConfig = generalConfigDao.findByType(TYPE); + if (emailConfig != null && emailConfig.isEnable() && emailConfig.getContent() != null) { + // 若启用数据库配置 + String content = emailConfig.getContent(); + NoticeSender noticeSenderConfig = objectMapper.readValue(content, NoticeSender.class); + sender.setHost(noticeSenderConfig.getEmailHost()); + sender.setPort(noticeSenderConfig.getEmailPort()); + sender.setUsername(noticeSenderConfig.getEmailUsername()); + sender.setPassword(noticeSenderConfig.getEmailPassword()); + Properties props = sender.getJavaMailProperties(); + props.put("spring.mail.smtp.ssl.enable", noticeSenderConfig.isEmailSsl()); + emailFromUser = noticeSenderConfig.getEmailUsername(); + } else { + // 若数据库未配置则启用yml配置 + sender.setHost(mailConfigProperties.getHost()); + sender.setPort(mailConfigProperties.getPort()); + sender.setUsername(mailConfigProperties.getUsername()); + sender.setPassword(mailConfigProperties.getPassword()); + emailFromUser = mailConfigProperties.getUsername(); + } + } catch (Exception e) { + log.error("Type not found {}",e.getMessage()); + } MimeMessage mimeMessage = javaMailSender.createMimeMessage(); MimeMessageHelper messageHelper = new MimeMessageHelper(mimeMessage, true, "UTF-8"); messageHelper.setSubject(bundle.getString("alerter.notify.title")); diff --git a/manager/src/main/java/org/dromara/hertzbeat/manager/config/MailConfigProperties.java b/manager/src/main/java/org/dromara/hertzbeat/manager/config/MailConfigProperties.java new file mode 100644 index 00000000000..ee4e9929053 --- /dev/null +++ b/manager/src/main/java/org/dromara/hertzbeat/manager/config/MailConfigProperties.java @@ -0,0 +1,46 @@ +package org.dromara.hertzbeat.manager.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; +import org.springframework.stereotype.Component; + +import java.util.Properties; + +/** + * + * @author zqr10159 + */ + +@Getter +@Setter +@Component +@ConfigurationProperties(prefix = "spring.mail") +public class MailConfigProperties { + private String host; + private String username; + private String password; + private Integer port; + private boolean ssl; + + @Bean + public JavaMailSender javaMailSender() { + JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); + mailSender.setHost(host); + mailSender.setPort(port); + mailSender.setUsername(username); + mailSender.setPassword(password); + + Properties props = mailSender.getJavaMailProperties(); + props.put("spring.mail.smtp.ssl.enable", ssl); + props.put("spring.mail.smtp.socketFactory.port", port); + props.put("spring.mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory"); + props.put("spring.mail.debug", "false"); + props.put("spring.mail.default-encoding", "UTF-8"); + + return mailSender; + } +} diff --git a/manager/src/main/java/org/dromara/hertzbeat/manager/controller/NoticeSenderConfigController.java b/manager/src/main/java/org/dromara/hertzbeat/manager/controller/NoticeSenderConfigController.java new file mode 100644 index 00000000000..73931a07776 --- /dev/null +++ b/manager/src/main/java/org/dromara/hertzbeat/manager/controller/NoticeSenderConfigController.java @@ -0,0 +1,56 @@ +package org.dromara.hertzbeat.manager.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.dromara.hertzbeat.common.entity.dto.Message; +import org.dromara.hertzbeat.manager.pojo.dto.NoticeSender; +import org.dromara.hertzbeat.manager.service.impl.MailGeneralConfigServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + + +import javax.validation.Valid; + +import java.util.List; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +/** + * Alert sender Configuration API + * 告警发送端配置API + * + * @author zqr10159 + * + */ +@RestController +@RequestMapping(value = "/api/config", produces = {APPLICATION_JSON_VALUE}) +@Tag(name = "Alert sender Configuration API | 告警发送端配置API") +@Slf4j +public class NoticeSenderConfigController { + @Autowired + private MailGeneralConfigServiceImpl mailConfigService; + + @PostMapping(path = "/sender") + @Operation(summary = "Save the sender config", description = "保存发送端配置") + public ResponseEntity> saveOrUpdateConfig( + @RequestBody @Valid NoticeSender noticeSender) { + mailConfigService.saveConfig(noticeSender, noticeSender.isEmailEnable()); + return ResponseEntity.ok(new Message<>("发送端配置保存成功|The sender configuration is saved successfully")); + } + + @GetMapping(path = "/senders") + @Operation(summary = "Get the sender config", description = "获取发送端配置") + public ResponseEntity>> getConfig(){ + List senders = mailConfigService.getConfigs(); + return ResponseEntity.ok(new Message<>(senders)); + } + + @DeleteMapping(path = "/sender/{id}") + @Operation(summary = "Delete the sender config", description = "删除发送端配置") + public ResponseEntity> deleteConfig(){ + mailConfigService.deleteConfig(); + return ResponseEntity.ok(new Message<>("发送端配置删除成功|The sender configuration is deleted")); + } +} diff --git a/manager/src/main/java/org/dromara/hertzbeat/manager/dao/GeneralConfigDao.java b/manager/src/main/java/org/dromara/hertzbeat/manager/dao/GeneralConfigDao.java new file mode 100644 index 00000000000..3829f675cc7 --- /dev/null +++ b/manager/src/main/java/org/dromara/hertzbeat/manager/dao/GeneralConfigDao.java @@ -0,0 +1,35 @@ +package org.dromara.hertzbeat.manager.dao; + +import org.dromara.hertzbeat.common.entity.manager.GeneralConfig; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.stereotype.Component; + +/** + * 消息通知服务端配置Dao + * + *

该接口继承了JpaRepository和JpaSpecificationExecutor两个接口,提供基本的CRUD操作和规范查询能力。

+ * + * @version 1.0 + * @since 2023/5/9 22:39 + * @author zqr10159 + */ +@Component +public interface GeneralConfigDao extends JpaRepository, JpaSpecificationExecutor { + + /** + * 通过类型删除 + * + * @param type 类型 + * @return 返回受影响的行数 + */ + int deleteByType(Byte type); + + /** + * 通过类型查询 + * + * @param type 类型 + * @return 返回查询到的配置信息 + */ + GeneralConfig findByType(Byte type); +} \ No newline at end of file diff --git a/manager/src/main/java/org/dromara/hertzbeat/manager/pojo/dto/NoticeSender.java b/manager/src/main/java/org/dromara/hertzbeat/manager/pojo/dto/NoticeSender.java new file mode 100644 index 00000000000..6373e5ec0e3 --- /dev/null +++ b/manager/src/main/java/org/dromara/hertzbeat/manager/pojo/dto/NoticeSender.java @@ -0,0 +1,36 @@ +package org.dromara.hertzbeat.manager.pojo.dto; + +import lombok.Data; + +import javax.validation.constraints.*; + + +/** + * 邮件账号配置dto + * @author zqr + */ +@Data +public class NoticeSender { + + @NotNull(message = "类型不能为空|Type cannot be empty") + private Integer type; + + @NotBlank(message = "邮件主机不能为空|Mail host cannot be empty") + private String emailHost; + + @NotBlank(message = "用户名不能为空|Username cannot be empty") + @Email + private String emailUsername; + + @NotBlank(message = "密码不能为空|Password cannot be empty") + private String emailPassword; + + @NotNull(message = "邮件端口不能为空|Mail port cannot be null") + @Max(message = "邮件端口不得大于65535|Mail port must be less than or equal to 65535", value = 65535) + @Min(message = "邮件端口不得小于1|Mail port must be greater than or equal to 1", value = 1) + private Integer emailPort; + + private boolean emailSsl; + + private boolean emailEnable; +} diff --git a/manager/src/main/java/org/dromara/hertzbeat/manager/service/GeneralConfigService.java b/manager/src/main/java/org/dromara/hertzbeat/manager/service/GeneralConfigService.java new file mode 100644 index 00000000000..989e2413ffa --- /dev/null +++ b/manager/src/main/java/org/dromara/hertzbeat/manager/service/GeneralConfigService.java @@ -0,0 +1,41 @@ +package org.dromara.hertzbeat.manager.service; + +import java.util.List; + +/** + * ConfigService接口,提供配置的增删查改操作。 + * + *

ConfigService interface provides CRUD operations for configurations.

+ * @author zqr10159 + * @param 配置类型 + * @version 1.0 + */ +public interface GeneralConfigService { + + /** + * 保存配置。 + * + * @param config 需要保存的配置 + * @param enabled 是否启用 + */ + void saveConfig(T config, boolean enabled); + + /** + * 删除配置。 + */ + void deleteConfig(); + + /** + * 获取配置。 + * + * @return 查询到的配置 + */ + T getConfig(); + + /** + * 获取所有配置。 + * + * @return 查询到的所有配置集合 + */ + List getConfigs(); +} \ No newline at end of file diff --git a/manager/src/main/java/org/dromara/hertzbeat/manager/service/impl/AbstractGeneralConfigServiceImpl.java b/manager/src/main/java/org/dromara/hertzbeat/manager/service/impl/AbstractGeneralConfigServiceImpl.java new file mode 100644 index 00000000000..21d472bdabf --- /dev/null +++ b/manager/src/main/java/org/dromara/hertzbeat/manager/service/impl/AbstractGeneralConfigServiceImpl.java @@ -0,0 +1,138 @@ +package org.dromara.hertzbeat.manager.service.impl; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.dromara.hertzbeat.common.entity.manager.GeneralConfig; +import org.dromara.hertzbeat.manager.dao.GeneralConfigDao; +import org.dromara.hertzbeat.manager.service.GeneralConfigService; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +/** + * 提供通用配置Service的抽象实现,实现了增删查改等操作。 + * + *

Abstract implementation of GeneralConfigService, providing CRUD operations for configurations.

+ * @author zqr10159 + */ +@Slf4j +abstract class AbstractGeneralConfigServiceImpl implements GeneralConfigService { + protected final GeneralConfigDao generalConfigDao; + protected final ObjectMapper objectMapper; + protected Byte type; + protected boolean enabled; + + /** + * 构造方法,传入GeneralConfigDao、ObjectMapper和type。 + * + *

Constructor, passing in GeneralConfigDao, ObjectMapper and type.

+ * + * @param generalConfigDao 配置Dao对象 + * @param objectMapper JSON工具类对象 + * @param type 配置类型 + */ + protected AbstractGeneralConfigServiceImpl(GeneralConfigDao generalConfigDao, ObjectMapper objectMapper, Byte type) { + this.generalConfigDao = generalConfigDao; + this.objectMapper = objectMapper; + this.type = type; + } + + /** + * 保存配置。 + * + *

Save a configuration.

+ * + * @param config 需要保存的配置对象 + * @param enabled 是否启用 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void saveConfig(T config, boolean enabled) { + try { + String contentJson = objectMapper.writeValueAsString(config); + + GeneralConfig generalConfig2Save = GeneralConfig.builder() + .type(type) + .enable(enabled) + .content(contentJson) + .build(); + generalConfigDao.save(generalConfig2Save); + log.info("配置保存成功|Configuration saved successfully"); + } catch (JsonProcessingException e) { + throw new RuntimeException("配置保存失败|Configuration saved failed"); + } + } + + /** + * 删除配置。 + * + *

Delete a configuration.

+ */ + @Transactional(rollbackFor = Exception.class) + @Override + public void deleteConfig() { + int count = generalConfigDao.deleteByType(type); + if (count == 0) { + throw new RuntimeException(("配置已被删除,无法再次被删除|Configuration has been deleted and cannot be deleted again")); + } + if (count > 1) { + log.warn("配置项有多个,{}个配置项被删除|Configuration has multiple items and {} items were deleted", count, count); + } + log.info("配置项删除成功|Configuration deleted successfully"); + } + + /** + * 获取配置。 + * + *

Get a configuration.

+ * + * @return 查询到的配置对象 + */ + @Override + public T getConfig() { + GeneralConfig generalConfig = generalConfigDao.findByType(type); + if (generalConfig == null) { + return null; + } + try { + return objectMapper.readValue(generalConfig.getContent(), getTypeReference()); + } catch (JsonProcessingException e) { + throw new RuntimeException("获取配置失败|Get configuration failed"); + } + } + + /** + * 获取所有配置。 + * + *

Get all configurations.

+ * + * @return 查询到的所有配置对象集合 + */ + @Override + public List getConfigs() { + List configs = generalConfigDao.findAll(); + List result = new ArrayList<>(); + for (GeneralConfig config : configs) { + try { + T t = objectMapper.readValue(config.getContent(), getTypeReference()); + result.add(t); + } catch (JsonProcessingException e) { + throw new RuntimeException("获取配置失败|Get configuration failed"); + } + } + return result; + } + + /** + * 获取配置类型的TypeReference对象。 + * + *

Get TypeReference object of configuration type.

+ * + * @return 配置类型的TypeReference对象 + */ + protected abstract TypeReference getTypeReference(); + +} diff --git a/manager/src/main/java/org/dromara/hertzbeat/manager/service/impl/MailGeneralConfigServiceImpl.java b/manager/src/main/java/org/dromara/hertzbeat/manager/service/impl/MailGeneralConfigServiceImpl.java new file mode 100644 index 00000000000..2919d2325d9 --- /dev/null +++ b/manager/src/main/java/org/dromara/hertzbeat/manager/service/impl/MailGeneralConfigServiceImpl.java @@ -0,0 +1,72 @@ +package org.dromara.hertzbeat.manager.service.impl; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.dromara.hertzbeat.manager.dao.GeneralConfigDao; +import org.dromara.hertzbeat.manager.pojo.dto.NoticeSender; +import org.springframework.stereotype.Service; + +import java.lang.reflect.Type; + + +/** + * MailGeneralConfigServiceImpl类是通用邮件配置服务实现类,继承了AbstractGeneralConfigServiceImpl类。 + * MailGeneralConfigServiceImpl class is the implementation of general email configuration service, + * which inherits the AbstractGeneralConfigServiceImpl class. + * + * @author zqr10159 + * @version 1.0.0 + * @since 2023/5/15 + */ + +@Service +public class MailGeneralConfigServiceImpl extends AbstractGeneralConfigServiceImpl { + + /** + * MailGeneralConfigServiceImpl的构造函数,通过默认构造函数或者反序列化构造(setBeanProps)来创建该类实例。 + * 参数generalConfigDao用于操作数据的dao层,参数objectMapper用于进行对象映射。 + * MailGeneralConfigServiceImpl's constructor creates an instance of this class + * through the default constructor or deserialization construction (setBeanProps). + * The parameter generalConfigDao is used for dao layer operation data, + * and objectMapper is used for object mapping. + * + * @param generalConfigDao 数据操作的dao层,供创建该类实例所需 + * dao layer operation data, needed to create an instance of this class + * @param objectMapper 对象映射,供创建该类实例所需 + * object mapping , needed to create an instance of this class + */ + public MailGeneralConfigServiceImpl(GeneralConfigDao generalConfigDao, ObjectMapper objectMapper) { + super(generalConfigDao, objectMapper, (byte) 2); + } + + /** + * 该方法用于保存邮件的配置信息,即保存配置和启用状态。 + * This method is used to save the configuration information of email, that is, save the configuration and enable status. + * + * @param config 配置信息 + * configuration information + * @param enabled 启用状态 + * recognized as the current active state of email configuration. + */ + @Override + public void saveConfig(NoticeSender config, boolean enabled) { + super.saveConfig(config, config.isEmailEnable()); + } + + /** + * 该方法用于获取NoticeSender类型的TypeReference,以供后续处理。 + * This method is used to get the TypeReference of NoticeSender type for subsequent processing. + * + * @return NoticeSender类型的TypeReference + * a TypeReference of NoticeSender type + */ + @Override + protected TypeReference getTypeReference() { + return new TypeReference<>() { + @Override + public Type getType() { + return NoticeSender.class; + } + }; + } +} diff --git a/manager/src/main/resources/application.yml b/manager/src/main/resources/application.yml index 71e91cc1e21..4b74fd78709 100644 --- a/manager/src/main/resources/application.yml +++ b/manager/src/main/resources/application.yml @@ -87,19 +87,15 @@ spring: # Attention: this is mail server address. # 请注意此为邮件服务器地址:qq邮箱为 smtp.qq.com qq企业邮箱为 smtp.exmail.qq.com host: smtp.qq.com - username: example@tancloud.cn + username: tancloud@qq.com # Attention: this is not email account password, this requires an email authorization code # 请注意此非邮箱账户密码 此需填写邮箱授权码 - password: example - port: 465 - default-encoding: UTF-8 - properties: - mail: - smtp: - socketFactoryClass: javax.net.ssl.SSLSocketFactory - ssl: - enable: true - debug: false + password: yourpassword + #Attention: Tencent mail smtps 465,smtp 587 + #请注意腾讯邮箱465为smtps,587为smtp + port: 587 + ssl: true + common: queue: diff --git a/script/sql/schema.sql b/script/sql/schema.sql index f0e6457ecba..5bfcab65a90 100644 --- a/script/sql/schema.sql +++ b/script/sql/schema.sql @@ -277,4 +277,20 @@ CREATE TABLE hzb_history primary key (id) ) ENGINE = InnoDB DEFAULT CHARSET=utf8mb4; +-- ---------------------------- +-- Table structure for hzb_config +-- ---------------------------- +DROP TABLE IF EXISTS hzb_config ; +CREATE TABLE hzb_config +( + type tinyint not null comment '配置类型:1-短信,2-邮件', + content varchar(4096) not null comment '配置内容JSON', + enable boolean not null default true comment '告警阈值开关', + creator varchar(100) comment '创建者', + modifier varchar(100) comment '最新修改者', + gmt_create timestamp default current_timestamp comment 'create time', + gmt_update datetime default current_timestamp on update current_timestamp comment 'update time', + primary key (type) +) ENGINE = InnoDB DEFAULT CHARSET=utf8mb4; + COMMIT; diff --git a/web-app/src/app/pojo/NoticeSender.ts b/web-app/src/app/pojo/NoticeSender.ts new file mode 100644 index 00000000000..2c77a728f50 --- /dev/null +++ b/web-app/src/app/pojo/NoticeSender.ts @@ -0,0 +1,16 @@ +export class NoticeSender { + id!: number; + name!: string; + // 通知信息方式: 1-短信 2-邮箱 + type!: number; + emailHost!: string; + emailPort!: number; + emailUsername!: string; + emailPassword!: string; + emailSsl: boolean = true; + emailEnable!: boolean; + creator!: string; + modifier!: string; + gmtCreate!: number; + gmtUpdate!: number; +} diff --git a/web-app/src/app/routes/alert/alert-notice/alert-notice.component.html b/web-app/src/app/routes/alert/alert-notice/alert-notice.component.html index 4ac2e51a519..bd461e1063c 100644 --- a/web-app/src/app/routes/alert/alert-notice/alert-notice.component.html +++ b/web-app/src/app/routes/alert/alert-notice/alert-notice.component.html @@ -17,6 +17,7 @@ + @@ -215,7 +216,7 @@ - + > = [SettingTagsComponent, DefineComponent]; +const COMPONENTS: Array> = [SettingTagsComponent, DefineComponent, SettingsComponent, MessageServerComponent]; @NgModule({ imports: [ diff --git a/web-app/src/app/routes/setting/settings/message-server/message-server.component.html b/web-app/src/app/routes/setting/settings/message-server/message-server.component.html new file mode 100644 index 00000000000..a01bea4af05 --- /dev/null +++ b/web-app/src/app/routes/setting/settings/message-server/message-server.component.html @@ -0,0 +1,87 @@ + + + + + {{ 'common.button.setting' | i18n }} + + + + {{ 'alert.notice.sender.mail.host' | i18n }}:{{ emailSender.emailHost }} +
+ {{ 'alert.notice.sender.mail.username' | i18n }}:{{ emailSender.emailUsername }} +
+ {{ 'alert.notice.sender.mail.port' | i18n }}:{{ emailSender.emailPort }} +
+ {{ 'alert.notice.sender.mail.ssl' | i18n }}:{{ emailSender.emailSsl ? ('common.yes' | i18n) : ('common.no' | i18n) }} +
+ {{ 'alert.notice.sender.mail.enable' | i18n }}:{{ emailSender.emailEnable ? ('common.yes' | i18n) : ('common.no' | i18n) }} +
+
+
+ + + + {{ 'common.button.setting' | i18n }} + + + + + + + + + +
+
+ + + +
+
+ + {{ 'alert.notice.sender.mail.host' | i18n }} + + + + + + {{ 'alert.notice.sender.mail.port' | i18n }} + + + + + + {{ 'alert.notice.sender.mail.username' | i18n }} + + + + + + {{ 'alert.notice.sender.mail.password' | i18n }} + + + + + + {{ 'alert.notice.sender.mail.ssl' | i18n }} + + + + + + {{ 'alert.notice.sender.enable' | i18n }} + + + + +
+
+
diff --git a/web-app/src/app/routes/setting/settings/message-server/message-server.component.less b/web-app/src/app/routes/setting/settings/message-server/message-server.component.less new file mode 100644 index 00000000000..e69de29bb2d diff --git a/web-app/src/app/routes/setting/settings/message-server/message-server.component.spec.ts b/web-app/src/app/routes/setting/settings/message-server/message-server.component.spec.ts new file mode 100644 index 00000000000..20477c2bca4 --- /dev/null +++ b/web-app/src/app/routes/setting/settings/message-server/message-server.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MessageServerComponent } from './message-server.component'; + +describe('MessageServerComponent', () => { + let component: MessageServerComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [MessageServerComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(MessageServerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/web-app/src/app/routes/setting/settings/message-server/message-server.component.ts b/web-app/src/app/routes/setting/settings/message-server/message-server.component.ts new file mode 100644 index 00000000000..ec4cb8af729 --- /dev/null +++ b/web-app/src/app/routes/setting/settings/message-server/message-server.component.ts @@ -0,0 +1,93 @@ +import { ChangeDetectorRef, Component, Inject, OnInit } from '@angular/core'; +import { I18NService } from '@core'; +import { ALAIN_I18N_TOKEN } from '@delon/theme'; +import { NzMessageService } from 'ng-zorro-antd/message'; +import { NzNotificationService } from 'ng-zorro-antd/notification'; +import { finalize } from 'rxjs/operators'; + +import { NoticeSender } from '../../../../pojo/NoticeSender'; +import { NoticeSenderService } from '../../../../service/notice-sender.service'; + +@Component({ + selector: 'app-message-server', + templateUrl: './message-server.component.html', + styleUrls: ['./message-server.component.less'] +}) +export class MessageServerComponent implements OnInit { + constructor( + public msg: NzMessageService, + private notifySvc: NzNotificationService, + private cdr: ChangeDetectorRef, + private noticeSenderSvc: NoticeSenderService, + @Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService + ) {} + + senders!: NoticeSender[]; + senderServerLoading: boolean = true; + loading: boolean = false; + isEmailServerModalVisible: boolean = false; + emailSender = new NoticeSender(); + + ngOnInit(): void { + this.loadSenderServer(); + } + + loadSenderServer() { + this.senderServerLoading = true; + let senderInit$ = this.noticeSenderSvc.getSenders().subscribe( + message => { + this.senderServerLoading = false; + if (message.code === 0) { + this.senders = message.data; + let res = this.senders.find(s => s.type === 2); + if (res != undefined) { + this.emailSender = res; + } + } else { + console.warn(message.msg); + } + senderInit$.unsubscribe(); + }, + error => { + console.error(error.msg); + this.senderServerLoading = false; + senderInit$.unsubscribe(); + } + ); + } + + onConfigEmailServer() { + this.isEmailServerModalVisible = true; + } + + onCancelEmailServer() { + this.isEmailServerModalVisible = false; + } + + onSaveEmailServer() { + const modalOk$ = this.noticeSenderSvc + .newSender(this.emailSender) + .pipe( + finalize(() => { + modalOk$.unsubscribe(); + this.senderServerLoading = false; + }) + ) + .subscribe( + message => { + if (message.code === 0) { + this.isEmailServerModalVisible = false; + this.notifySvc.success(this.i18nSvc.fanyi('common.notify.apply-success'), ''); + } else { + this.notifySvc.error(this.i18nSvc.fanyi('common.notify.apply-fail'), message.msg); + } + }, + error => { + this.isEmailServerModalVisible = false; + this.notifySvc.error(this.i18nSvc.fanyi('common.notify.apply-fail'), error.msg); + } + ); + } + + protected readonly Boolean = Boolean; +} diff --git a/web-app/src/app/routes/setting/settings/settings.component.html b/web-app/src/app/routes/setting/settings/settings.component.html new file mode 100644 index 00000000000..6dafe801926 --- /dev/null +++ b/web-app/src/app/routes/setting/settings/settings.component.html @@ -0,0 +1,13 @@ +
+ +
+
+ {{ title }} +
+ +
+
diff --git a/web-app/src/app/routes/setting/settings/settings.component.less b/web-app/src/app/routes/setting/settings/settings.component.less new file mode 100644 index 00000000000..be10ab1a011 --- /dev/null +++ b/web-app/src/app/routes/setting/settings/settings.component.less @@ -0,0 +1,81 @@ +@import '@delon/theme/index'; + +:host { + display: block; + padding-top: 24px; + ::ng-deep { + .main { + display: flex; + width: 100%; + padding-top: 16px; + padding-bottom: 16px; + overflow: auto; + background-color: #fff; + } + + .menu { + width: 224px; + border-right: @border-width-base @border-style-base @border-color-split; + .ant-menu-inline { + border: none; + } + .ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected { + font-weight: bold; + } + } + + .content { + flex: 1; + padding-top: 8px; + padding-right: 40px; + padding-bottom: 8px; + padding-left: 40px; + .title { + margin-bottom: 12px; + color: @heading-color; + font-weight: 500; + font-size: 20px; + line-height: 28px; + } + .ant-list-split .ant-list-item:last-child { + border-bottom: 1px solid #e8e8e8; + } + .ant-list-item { + padding-top: 14px; + padding-bottom: 14px; + } + } + + @media screen and (max-width: @mobile-max) { + .main { + flex-direction: column; + .menu { + width: 100%; + border: none; + } + .content { + padding: 40px; + } + } + } + } +} + +[data-theme='dark'] { + :host ::ng-deep { + .main { + background-color: #141414; + } + .content { + .title { + color: rgba(255, 255, 255, 0.65); + } + } + .menu { + border-right-color: #303030; + } + .content .ant-list-split .ant-list-item:last-child { + border-bottom-color: #303030; + } + } +} diff --git a/web-app/src/app/routes/setting/settings/settings.component.spec.ts b/web-app/src/app/routes/setting/settings/settings.component.spec.ts new file mode 100644 index 00000000000..833805b482b --- /dev/null +++ b/web-app/src/app/routes/setting/settings/settings.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SettingsComponent } from './settings.component'; + +describe('SettingsComponent', () => { + let component: SettingsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [SettingsComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(SettingsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/web-app/src/app/routes/setting/settings/settings.component.ts b/web-app/src/app/routes/setting/settings/settings.component.ts new file mode 100644 index 00000000000..ed252984560 --- /dev/null +++ b/web-app/src/app/routes/setting/settings/settings.component.ts @@ -0,0 +1,71 @@ +import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Inject, OnDestroy } from '@angular/core'; +import { ActivationEnd, Router } from '@angular/router'; +import { I18NService } from '@core'; +import { ALAIN_I18N_TOKEN } from '@delon/theme'; +import { NzMenuModeType } from 'ng-zorro-antd/menu'; +import { fromEvent, Subscription } from 'rxjs'; +import { debounceTime, filter } from 'rxjs/operators'; + +@Component({ + selector: 'app-settings', + templateUrl: './settings.component.html', + styleUrls: ['./settings.component.less'] +}) +export class SettingsComponent implements AfterViewInit, OnDestroy { + private resize$!: Subscription; + private router$: Subscription; + mode: NzMenuModeType = 'inline'; + title!: string; + menus: Array<{ key: string; title: string; selected?: boolean }> = [ + { + key: 'server', + title: this.i18nSvc.fanyi('settings.server') + } + ]; + + constructor( + private router: Router, + private cdr: ChangeDetectorRef, + private el: ElementRef, + @Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService + ) { + this.router$ = this.router.events.pipe(filter(e => e instanceof ActivationEnd)).subscribe(() => this.setActive()); + } + + private setActive(): void { + const key = this.router.url.substr(this.router.url.lastIndexOf('/') + 1); + this.menus.forEach(i => { + i.selected = i.key === key; + }); + this.title = this.menus.find(w => w.selected)!.title; + } + + to(item: { key: string }): void { + this.router.navigateByUrl(`/setting/settings/${item.key}`); + } + + private resize(): void { + const el = this.el.nativeElement; + let mode: NzMenuModeType = 'inline'; + const { offsetWidth } = el; + if (offsetWidth < 641 && offsetWidth > 400) { + mode = 'horizontal'; + } + if (window.innerWidth < 768 && offsetWidth > 400) { + mode = 'horizontal'; + } + this.mode = mode; + this.cdr.detectChanges(); + } + + ngAfterViewInit(): void { + this.resize$ = fromEvent(window, 'resize') + .pipe(debounceTime(200)) + .subscribe(() => this.resize()); + } + + ngOnDestroy(): void { + this.resize$.unsubscribe(); + this.router$.unsubscribe(); + } +} diff --git a/web-app/src/app/service/notice-receiver.service.spec.ts b/web-app/src/app/service/notice-receiver.service.spec.ts index d1d1dfb537d..cafaf358323 100644 --- a/web-app/src/app/service/notice-receiver.service.spec.ts +++ b/web-app/src/app/service/notice-receiver.service.spec.ts @@ -1,13 +1,13 @@ import { TestBed } from '@angular/core/testing'; -import { NoticeReceiverService } from './notice-receiver.service'; +import { NoticeReceiverMailService } from './notice-receiver.service'; describe('NoticeReceiverService', () => { - let service: NoticeReceiverService; + let service: NoticeReceiverMailService; beforeEach(() => { TestBed.configureTestingModule({}); - service = TestBed.inject(NoticeReceiverService); + service = TestBed.inject(NoticeReceiverMailService); }); it('should be created', () => { diff --git a/web-app/src/app/service/notice-sender.service.ts b/web-app/src/app/service/notice-sender.service.ts new file mode 100644 index 00000000000..4e314fe62c5 --- /dev/null +++ b/web-app/src/app/service/notice-sender.service.ts @@ -0,0 +1,33 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + +import { Message } from '../pojo/Message'; +import { NoticeSender } from '../pojo/NoticeSender'; + +const notice_sender_save_uri = '/config/sender'; +const notice_sender_get_uri = '/config/senders'; +const notice_sender_delete_uri = '/config/sender'; + +@Injectable({ + providedIn: 'root' +}) +export class NoticeSenderService { + constructor(private http: HttpClient) {} + + public newSender(body: NoticeSender): Observable> { + return this.http.post>(notice_sender_save_uri, body); + } + + public editSender(body: NoticeSender): Observable> { + return this.http.post>(notice_sender_save_uri, body); + } + + public deleteSender(senderId: number): Observable> { + return this.http.delete>(`${notice_sender_delete_uri}/${senderId}`); + } + + public getSenders(): Observable> { + return this.http.get>(notice_sender_get_uri); + } +} diff --git a/web-app/src/assets/app-data.json b/web-app/src/assets/app-data.json index d2e501a9305..9cc2661defe 100644 --- a/web-app/src/assets/app-data.json +++ b/web-app/src/assets/app-data.json @@ -129,6 +129,12 @@ "badgeDot": true, "badge": 1 }, + { + "text": "settings", + "i18n": "menu.extras.settings", + "icon": "anticon-setting", + "link": "/setting/settings" + }, { "text": "helpCenter", "externalLink": "https://hertzbeat.com/docs/help/guide", diff --git a/web-app/src/assets/i18n/en-US.json b/web-app/src/assets/i18n/en-US.json index ed6e237404e..c558d393067 100644 --- a/web-app/src/assets/i18n/en-US.json +++ b/web-app/src/assets/i18n/en-US.json @@ -38,7 +38,8 @@ "tags": "Tags Manage", "define": "Monitor Template", "help": "Help Center", - "setting": "Setting" + "setting": "Setting", + "settings": "Settings" }, "more": "More" }, @@ -226,6 +227,13 @@ "alert.notice.send-test": "Send Alert Test Msg", "alert.notice.send-test.notify.success": "Send Alert Test Success!", "alert.notice.send-test.notify.failed": "Send Alert Test Failed!", + "alert.notice.sender.enable": "isEnabled", + "alert.notice.sender.mail.host": "Email Server Address", + "alert.notice.sender.mail.username": "Email Account", + "alert.notice.sender.mail.password": "Email Password", + "alert.notice.sender.mail.port": "Email Port", + "alert.notice.sender.mail.ssl": "Enable SSL", + "alert.notice.sender.mail.enable": "Enable Email Configuration", "dashboard.alerts.title": "Recently Alerts List", "dashboard.alerts.title-no": "Recently Pending Alerts", "dashboard.alerts.no": "No Pending Alerts", @@ -344,6 +352,7 @@ "common.button.cancel": "Cancel", "common.button.help": "Help", "common.button.edit": "Edit", + "common.button.setting": "Setting", "common.button.delete": "Delete", "common.button.detect": "Detect", "common.button.operation.more": "More", @@ -394,6 +403,11 @@ "define.new.code": "# Please define a new monitoring type by writing YML content here, refer to the document: https://hertzbeat.com/docs/advanced/extend-point ", "define.save-apply.no-code": "The monitoring type definition content cannot be empty.", "define.save-apply.confirm": "Please confirm whether to update and apply the monitor define? This will affect what you monitor.", + "settings.server": "Message Server Setting", + "settings.server.email": "Email Server", + "settings.server.email.setting": "Configure Email Server", + "settings.server.sms": "SMS Server", + "settings.server.sms.setting": "Configure SMS Server", "validation.email.required": "Please enter your email!", "validation.email.wrong-format": "The email address is in the wrong format!", "validation.password.required": "Please enter your password!", diff --git a/web-app/src/assets/i18n/zh-CN.json b/web-app/src/assets/i18n/zh-CN.json index 33c718c2559..1fcdd0b3931 100644 --- a/web-app/src/assets/i18n/zh-CN.json +++ b/web-app/src/assets/i18n/zh-CN.json @@ -38,7 +38,8 @@ "tags": "标签管理", "define": "监控模版", "help": "帮助中心", - "setting": "设置" + "setting": "设置", + "settings": "系统设置" }, "more": "更多" }, @@ -226,6 +227,13 @@ "alert.notice.send-test": "发送告警测试", "alert.notice.send-test.notify.success": "触发告警测试成功!", "alert.notice.send-test.notify.failed": "触发告警测试失败!", + "alert.notice.sender.enable": "是否启用", + "alert.notice.sender.mail.host": "邮箱服务器地址", + "alert.notice.sender.mail.username": "邮箱账号", + "alert.notice.sender.mail.password": "邮箱密码", + "alert.notice.sender.mail.port": "邮箱端口", + "alert.notice.sender.mail.ssl": "是否启用SSL", + "alert.notice.sender.mail.enable": "是否启用邮箱配置", "dashboard.alerts.title": "最近告警列表", "dashboard.alerts.title-no": "最近未处理告警", "dashboard.alerts.no": "暂无未处理告警", @@ -347,6 +355,7 @@ "common.button.cancel": "取消", "common.button.help": "帮助", "common.button.edit": "编辑", + "common.button.setting": "配置", "common.button.delete": "删除", "common.button.operation.more": "更多操作", "common.week.7": "星期日", @@ -392,6 +401,11 @@ "define.new.code": "# 请在此通过编写YML内容来定义新的监控类型, 参考文档: https://hertzbeat.com/docs/advanced/extend-point ", "define.save-apply.no-code": "监控类型定义内容不能为空。", "define.save-apply.confirm": "请确认是否保存修改并应用此监控类型定义? 这会影响到您的监控内容。", + "settings.server": "消息服务器配置", + "settings.server.email": "邮件服务器", + "settings.server.email.setting": "配置邮件服务器", + "settings.server.sms": "短信服务器", + "settings.server.sms.setting": "配置短信服务器", "validation.email.required": "请输入邮箱地址!", "validation.email.wrong-format": "邮箱地址格式错误!", "validation.email.invalid": "无效的邮箱地址!", diff --git a/web-app/src/assets/i18n/zh-TW.json b/web-app/src/assets/i18n/zh-TW.json index b009a91afac..0ff4cd25dd8 100644 --- a/web-app/src/assets/i18n/zh-TW.json +++ b/web-app/src/assets/i18n/zh-TW.json @@ -38,7 +38,8 @@ "tags": "標簽管理", "define": "監控模版", "help": "幫助中心", - "setting": "設置" + "setting": "設置", + "settings": "系統設置" }, "more": "更多" }, @@ -226,6 +227,13 @@ "alert.notice.send-test": "發送告警測試", "alert.notice.send-test.notify.success": "觸發告警測試成功!", "alert.notice.send-test.notify.failed": "觸發告警測試失敗!", + "alert.notice.sender.enable": "是否啟用", + "alert.notice.sender.mail.host": "郵件伺服器地址", + "alert.notice.sender.mail.username": "郵件帳號", + "alert.notice.sender.mail.password": "郵件密碼", + "alert.notice.sender.mail.port": "郵件端口", + "alert.notice.sender.mail.ssl": "是否啟用SSL", + "alert.notice.sender.mail.enable": "啟用郵件設定", "dashboard.alerts.title": "最近告警列表", "dashboard.alerts.title-no": "最近未處理告警", "dashboard.alerts.no": "暫無未處理告警", @@ -352,6 +360,7 @@ "common.button.cancel": "取消", "common.button.help": "幫助", "common.button.edit": "編輯", + "common.button.setting": "配置", "common.button.delete": "刪除", "common.button.operation.more": "更多操作", "app.theme.default": "淺色主題", @@ -390,6 +399,11 @@ "define.new.code": "# 請在此通過編寫YML內容來定義新的監控類型, 參考文檔: https://hertzbeat.com/docs/advanced/extend-point ", "define.save-apply.no-code": "監控類型定義內容不能為空。", "define.save-apply.confirm": "請確認是否保存修改並應用此監控類型定義? 這會影響到您的監控內容。", + "settings.server": "消息服務器配置", + "settings.server.email": "郵件服務器", + "settings.server.email.setting": "配置郵件服務器", + "settings.server.sms": "短信服務器", + "settings.server.sms.setting": "配置短信服務器", "validation.email.required": "請輸入郵箱地址!", "validation.email.wrong-format": "郵箱地址格式錯誤!", "validation.email.invalid": "無效的郵箱地址!",