210 lines
9.0 KiB
Go
210 lines
9.0 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.pcux.cn/xuans/Scert/backend/internal/config"
|
|
"git.pcux.cn/xuans/Scert/backend/internal/setup"
|
|
)
|
|
|
|
// NotificationInput 不接受客户端指定凭据引用;新密码只写入加密存储。
|
|
// 省略整个通知对象时保留原配置,提交对象时按完整表单应用配置。
|
|
type NotificationInput struct {
|
|
HTTPEnabled bool `json:"http_enabled"`
|
|
SMTPEnabled bool `json:"smtp_enabled"`
|
|
Provider string `json:"provider"`
|
|
Endpoint string `json:"endpoint"`
|
|
HTTPPath string `json:"http_path"`
|
|
HTTPLoginPath string `json:"http_login_path"`
|
|
HTTPAuthMode string `json:"http_auth_mode"`
|
|
HTTPCredentials map[string]string `json:"http_credentials"`
|
|
HTTPHeaders map[string]string `json:"http_headers"`
|
|
EventMode string `json:"event_mode"`
|
|
IncludeDeploymentDetails bool `json:"include_deployment_details"`
|
|
IncludeCertificateSerial bool `json:"include_certificate_serial"`
|
|
IncludeManagementURL bool `json:"include_management_url"`
|
|
SenderName string `json:"sender_name"`
|
|
SenderEmail string `json:"sender_email"`
|
|
Recipients []string `json:"recipients"`
|
|
TimeoutSecs int `json:"timeout_secs"`
|
|
RetryCount int `json:"retry_count"`
|
|
RetryIntervalsSecs []int `json:"retry_intervals_secs"`
|
|
SMTPHost string `json:"smtp_host"`
|
|
SMTPPort int `json:"smtp_port"`
|
|
SMTPUsername string `json:"smtp_username"`
|
|
SMTPPassword string `json:"smtp_password"`
|
|
SMTPMode string `json:"smtp_mode"`
|
|
SMTPAllowPlaintextAuth bool `json:"smtp_allow_plaintext_auth"`
|
|
}
|
|
|
|
func (completer *Completer) prepareNotification(ctx context.Context, current config.NotificationConfig, input *NotificationInput) (config.NotificationConfig, func() error, error) {
|
|
if input == nil {
|
|
return current, nil, nil
|
|
}
|
|
candidate := config.NotificationConfig{
|
|
HTTPEnabled: input.HTTPEnabled, SMTPEnabled: input.SMTPEnabled,
|
|
Provider: strings.TrimSpace(input.Provider), Endpoint: strings.TrimSpace(input.Endpoint),
|
|
HTTPPath: strings.TrimSpace(input.HTTPPath), HTTPLoginPath: strings.TrimSpace(input.HTTPLoginPath),
|
|
HTTPAuthMode: strings.ToLower(strings.TrimSpace(input.HTTPAuthMode)), HTTPHeaders: input.HTTPHeaders,
|
|
EventMode: strings.TrimSpace(input.EventMode), IncludeDeploymentDetails: input.IncludeDeploymentDetails,
|
|
IncludeCertificateSerial: input.IncludeCertificateSerial, IncludeManagementURL: input.IncludeManagementURL,
|
|
SenderName: strings.TrimSpace(input.SenderName), SenderEmail: strings.TrimSpace(input.SenderEmail),
|
|
Recipients: input.Recipients, TimeoutSecs: input.TimeoutSecs,
|
|
RetryCount: input.RetryCount, RetryIntervalsSecs: input.RetryIntervalsSecs,
|
|
SMTPHost: strings.TrimSpace(input.SMTPHost), SMTPPort: input.SMTPPort,
|
|
SMTPUsername: strings.TrimSpace(input.SMTPUsername), SMTPMode: strings.ToLower(strings.TrimSpace(input.SMTPMode)),
|
|
SMTPAllowPlaintextAuth: input.SMTPAllowPlaintextAuth,
|
|
}
|
|
if candidate.HTTPAuthMode == "" || !candidate.HTTPEnabled {
|
|
candidate.HTTPAuthMode = "none"
|
|
}
|
|
if candidate.HTTPPath == "" {
|
|
candidate.HTTPPath = "/api/email/send"
|
|
}
|
|
if candidate.HTTPLoginPath == "" {
|
|
candidate.HTTPLoginPath = "/api/login"
|
|
}
|
|
if candidate.EventMode == "" {
|
|
candidate.EventMode = "detailed"
|
|
}
|
|
if candidate.SenderName == "" {
|
|
candidate.SenderName = "Scert"
|
|
}
|
|
if candidate.TimeoutSecs == 0 {
|
|
candidate.TimeoutSecs = 10
|
|
}
|
|
if candidate.SMTPPort == 0 {
|
|
candidate.SMTPPort = 587
|
|
}
|
|
if candidate.SMTPMode == "" {
|
|
candidate.SMTPMode = "starttls"
|
|
}
|
|
if candidate.RetryCount < 0 || len(candidate.RetryIntervalsSecs) != candidate.RetryCount || candidate.TimeoutSecs < 1 {
|
|
return candidate, nil, completeError(CompleteErrorInput, "通知超时或重试参数无效", nil)
|
|
}
|
|
if candidate.RetryCount == 0 {
|
|
candidate.RetryIntervalsSecs = []int{}
|
|
}
|
|
if candidate.HTTPEnabled && candidate.Endpoint == "" {
|
|
return candidate, nil, completeError(CompleteErrorInput, "启用 HTTP 通知时必须配置服务地址", nil)
|
|
}
|
|
if candidate.SMTPEnabled && (candidate.SMTPHost == "" || candidate.SMTPUsername == "" || input.SMTPPassword == "" ||
|
|
(candidate.SMTPMode == "plain" && !candidate.SMTPAllowPlaintextAuth)) {
|
|
return candidate, nil, completeError(CompleteErrorInput, "SMTP 通知主机、账号、密码或连接安全参数无效", nil)
|
|
}
|
|
credentials := make(map[string]map[string]string)
|
|
if candidate.SMTPEnabled {
|
|
name, email, err := candidate.SenderIdentity()
|
|
if err != nil {
|
|
return candidate, nil, completeError(CompleteErrorInput, "SMTP 发件账号无效,请使用 Smail 系统通知页提供的完整用户名", err)
|
|
}
|
|
candidate.SenderName, candidate.SenderEmail = name, email
|
|
}
|
|
if candidate.HTTPEnabled && candidate.HTTPAuthMode != "none" {
|
|
values, err := notificationCredentials(candidate.HTTPAuthMode, input.HTTPCredentials)
|
|
if err != nil {
|
|
return candidate, nil, completeError(CompleteErrorInput, "HTTP 通知认证凭据无效", err)
|
|
}
|
|
credentials["http"] = values
|
|
}
|
|
if candidate.SMTPEnabled {
|
|
credentials["smtp"] = map[string]string{"username": candidate.SMTPUsername, "password": input.SMTPPassword}
|
|
}
|
|
if len(credentials) == 0 {
|
|
return candidate, nil, nil
|
|
}
|
|
store, ok := completer.credentials.(DatabaseSecretStore)
|
|
if !ok {
|
|
return candidate, nil, completeError(CompleteErrorInternal, "通知加密凭据存储尚未就绪", nil)
|
|
}
|
|
references := make([]string, 0, len(credentials))
|
|
cleanup := sync.OnceValue(func() error {
|
|
var failures []error
|
|
for _, reference := range references {
|
|
cleanupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
failures = append(failures, store.Delete(cleanupCtx, reference))
|
|
cancel()
|
|
}
|
|
return errors.Join(failures...)
|
|
})
|
|
for _, kind := range []string{"http", "smtp"} {
|
|
values, exists := credentials[kind]
|
|
if !exists {
|
|
continue
|
|
}
|
|
token, err := randomToken(24)
|
|
if err != nil {
|
|
return candidate, cleanup, completeError(CompleteErrorInternal, "生成通知凭据引用失败", err)
|
|
}
|
|
reference := "secret://notification/" + token
|
|
references = append(references, reference)
|
|
if err := store.Put(ctx, reference, values); err != nil {
|
|
return candidate, cleanup, completeError(CompleteErrorDependency, "加密保存通知凭据失败", err)
|
|
}
|
|
if kind == "http" {
|
|
candidate.HTTPCredentialRef = reference
|
|
} else {
|
|
candidate.SMTPCredentialRef = reference
|
|
}
|
|
}
|
|
return candidate, cleanup, nil
|
|
}
|
|
|
|
func notificationCredentials(mode string, input map[string]string) (map[string]string, error) {
|
|
first := func(keys ...string) string {
|
|
for _, key := range keys {
|
|
if value := input[key]; strings.TrimSpace(value) != "" {
|
|
return value
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
switch mode {
|
|
case "bearer":
|
|
if value := first("token", "access_token"); value != "" {
|
|
return map[string]string{"token": value}, nil
|
|
}
|
|
case "hmac_sha256":
|
|
if value := first("secret", "hmac_secret", "key", "token"); value != "" {
|
|
return map[string]string{"secret": value}, nil
|
|
}
|
|
case "session":
|
|
if account := first("account", "username", "identifier"); account != "" && input["password"] != "" {
|
|
return map[string]string{"account": account, "password": input["password"]}, nil
|
|
}
|
|
}
|
|
return nil, errors.New("通知认证方式或必填凭据无效")
|
|
}
|
|
|
|
func applyNotificationInput(input *setup.Input, notification config.NotificationConfig) {
|
|
input.EmailHTTPEnabled = notification.HTTPEnabled
|
|
input.EmailSMTPEnabled = notification.SMTPEnabled
|
|
input.EmailSMTPHost = notification.SMTPHost
|
|
input.EmailSMTPPort = notification.SMTPPort
|
|
input.EmailSMTPUsername = notification.SMTPUsername
|
|
input.EmailSMTPCredentialRef = notification.SMTPCredentialRef
|
|
input.EmailSMTPMode = notification.SMTPMode
|
|
input.EmailSMTPAllowPlaintextAuth = notification.SMTPAllowPlaintextAuth
|
|
input.EmailProvider = notification.Provider
|
|
input.EmailEndpoint = notification.Endpoint
|
|
input.EmailHTTPPath = notification.HTTPPath
|
|
input.EmailHTTPLoginPath = notification.HTTPLoginPath
|
|
input.EmailHTTPAuthMode = notification.HTTPAuthMode
|
|
input.EmailHTTPCredentialRef = notification.HTTPCredentialRef
|
|
input.EmailHTTPHeaders = notification.HTTPHeaders
|
|
input.EmailEventMode = notification.EventMode
|
|
input.EmailIncludeDeploymentDetails = notification.IncludeDeploymentDetails
|
|
input.EmailIncludeCertificateSerial = notification.IncludeCertificateSerial
|
|
input.EmailIncludeManagementURL = notification.IncludeManagementURL
|
|
input.EmailSenderName = notification.SenderName
|
|
input.EmailSenderEmail = notification.SenderEmail
|
|
input.EmailRecipients = notification.Recipients
|
|
input.EmailTimeoutSecs = notification.TimeoutSecs
|
|
input.EmailRetryCount = notification.RetryCount
|
|
input.EmailRetryIntervalsSecs = notification.RetryIntervalsSecs
|
|
}
|