Scert/backend/internal/httpapi/settings_api.go

407 lines
16 KiB
Go

package httpapi
import (
"errors"
"net/http"
"reflect"
"strings"
"github.com/gin-gonic/gin"
"git.pcux.cn/xuans/Scert/backend/internal/config"
)
type settingsView struct {
Server settingsServerView `json:"server"`
Web settingsWebView `json:"web"`
Paths settingsPathsView `json:"paths"`
ACME settingsACMEView `json:"acme"`
DNS settingsDNSView `json:"dns"`
Notification settingsNotificationView `json:"notification"`
}
type settingsServerView struct {
Host string `json:"host"`
Port int `json:"port"`
HTTPSMode string `json:"https_mode"`
}
type settingsWebView struct {
Enabled bool `json:"enabled"`
Hostname string `json:"hostname"`
PublicIP string `json:"public_ip"`
}
type settingsPathsView struct {
Certificate string `json:"certificate"`
}
type settingsACMEView struct {
CA string `json:"ca"`
Environment string `json:"environment"`
AccountEmail string `json:"account_email"`
RenewalCheckIntervalDays int `json:"renewal_check_interval_days"`
RenewalNearExpiryDays int `json:"renewal_near_expiry_days"`
RenewalThresholdDays int `json:"renewal_threshold_days"`
RenewalUrgentThresholdDays int `json:"renewal_urgent_threshold_days"`
RenewalMaxRetries int `json:"renewal_max_retries"`
RenewalRetryIntervalMins int `json:"renewal_retry_interval_mins"`
PropagationTimeoutSecs int `json:"propagation_timeout_secs"`
PropagationPollingIntervalSecs int `json:"propagation_polling_interval_secs"`
}
type settingsDNSView struct {
Provider string `json:"provider"`
VerificationState string `json:"verification_state"`
}
type settingsNotificationView struct {
HTTPEnabled bool `json:"http_enabled"`
SMTPEnabled bool `json:"smtp_enabled"`
SMTPHost string `json:"smtp_host"`
SMTPPort int `json:"smtp_port"`
SMTPUsername string `json:"smtp_username"`
SMTPMode string `json:"smtp_mode"`
SMTPAllowPlaintextAuth bool `json:"smtp_allow_plaintext_auth"`
SMTPCredentialConfigured bool `json:"smtp_credential_configured"`
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"`
HTTPCredentialConfigured bool `json:"http_credential_configured"`
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"`
}
type settingsRequest struct {
Server *settingsServerRequest `json:"server"`
Web *settingsWebRequest `json:"web"`
Paths *settingsPathsRequest `json:"paths"`
ACME *settingsACMERequest `json:"acme"`
DNS *settingsDNSRequest `json:"dns"`
Notification *settingsNotificationRequest `json:"notification"`
}
type settingsServerRequest struct {
Host *string `json:"host"`
Port *int `json:"port"`
HTTPSMode *string `json:"https_mode"`
}
type settingsWebRequest struct {
Enabled *bool `json:"enabled"`
Hostname *string `json:"hostname"`
PublicIP *string `json:"public_ip"`
}
type settingsPathsRequest struct {
Certificate *string `json:"certificate"`
}
type settingsACMERequest struct {
CA *string `json:"ca"`
Environment *string `json:"environment"`
AccountEmail *string `json:"account_email"`
RenewalCheckIntervalDays *int `json:"renewal_check_interval_days"`
RenewalNearExpiryDays *int `json:"renewal_near_expiry_days"`
RenewalThresholdDays *int `json:"renewal_threshold_days"`
RenewalUrgentThresholdDays *int `json:"renewal_urgent_threshold_days"`
RenewalMaxRetries *int `json:"renewal_max_retries"`
RenewalRetryIntervalMins *int `json:"renewal_retry_interval_mins"`
PropagationTimeoutSecs *int `json:"propagation_timeout_secs"`
PropagationPollingIntervalSecs *int `json:"propagation_polling_interval_secs"`
}
type settingsDNSRequest struct {
Provider *string `json:"provider"`
}
type settingsNotificationRequest struct {
HTTPEnabled *bool `json:"http_enabled"`
SMTPEnabled *bool `json:"smtp_enabled"`
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"`
HTTPCredentials map[string]string `json:"http_credentials"`
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"`
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"`
}
func (s *Server) getSettings(c *gin.Context) {
s.configMu.RLock()
cfg := s.config
s.configMu.RUnlock()
c.JSON(http.StatusOK, settingsViewFromConfig(cfg))
}
func (s *Server) updateSettings(c *gin.Context) {
if strings.TrimSpace(s.configPath) == "" {
s.writeError(c, http.StatusInternalServerError, "配置文件路径未配置", errors.New("配置文件路径为空"))
return
}
var request settingsRequest
if err := bindJSON(c, &request, jsonRequestBodyLimit); err != nil {
s.writeError(c, http.StatusBadRequest, "设置请求格式不正确", err)
return
}
s.configMu.Lock()
previous := s.config
updated := s.config
applySettingsRequest(&updated, request)
updated.Normalize()
createdCredentialRefs, err := s.saveSettingsNotificationCredentials(c.Request.Context(), &updated, request)
if err != nil {
s.rollbackSetupCredentialsWithLog(createdCredentialRefs)
s.configMu.Unlock()
s.writeError(c, http.StatusBadRequest, "保存通知凭据失败", err)
return
}
if err := updated.Validate(); err != nil {
s.rollbackSetupCredentialsWithLog(createdCredentialRefs)
s.configMu.Unlock()
s.writeError(c, http.StatusBadRequest, "设置内容校验失败", err)
return
}
if err := updated.Save(s.configPath); err != nil {
s.rollbackSetupCredentialsWithLog(createdCredentialRefs)
s.configMu.Unlock()
s.writeError(c, http.StatusInternalServerError, "保存设置失败", err)
return
}
restartRequired := previous.Server.Host != updated.Server.Host ||
previous.Server.Port != updated.Server.Port ||
previous.Server.HTTPSMode != updated.Server.HTTPSMode ||
previous.Paths.Certificate != updated.Paths.Certificate ||
!reflect.DeepEqual(previous.ACME, updated.ACME) ||
!reflect.DeepEqual(previous.DNS, updated.DNS) ||
!reflect.DeepEqual(previous.Notification, updated.Notification)
message := "设置已保存"
if restartRequired {
message = "设置已保存;服务监听地址、端口或 HTTPS 模式已变更,请重启 Scert 服务"
}
s.config = updated
s.configMu.Unlock()
oldCredentialRefs := []string{
previous.Notification.HTTPCredentialRef,
previous.Notification.SMTPCredentialRef,
}
newCredentialRefs := map[string]struct{}{
updated.Notification.HTTPCredentialRef: {},
updated.Notification.SMTPCredentialRef: {},
}
var obsoleteCredentialRefs []string
for _, reference := range oldCredentialRefs {
reference = strings.TrimSpace(reference)
if reference == "" {
continue
}
if _, stillUsed := newCredentialRefs[reference]; !stillUsed {
obsoleteCredentialRefs = append(obsoleteCredentialRefs, reference)
}
}
if len(obsoleteCredentialRefs) > 0 {
if err := s.rollbackSetupCredentials(obsoleteCredentialRefs); err != nil && s.logger != nil {
s.logger.Warn("\u6e05\u7406\u5df2\u66ff\u6362\u7684\u901a\u77e5\u654f\u611f\u51ed\u636e\u5931\u8d25", "error", err)
}
}
c.JSON(http.StatusOK, gin.H{
"settings": settingsViewFromConfig(updated),
"restart_required": restartRequired,
"message": message,
})
}
func settingsViewFromConfig(cfg config.Config) settingsView {
return settingsView{
Server: settingsServerView{Host: cfg.Server.Host, Port: cfg.Server.Port, HTTPSMode: cfg.Server.HTTPSMode},
Web: settingsWebView{Enabled: cfg.Web.Enabled, Hostname: cfg.Web.Hostname, PublicIP: cfg.Web.PublicIP},
Paths: settingsPathsView{Certificate: cfg.Paths.Certificate},
ACME: settingsACMEView{
CA: cfg.ACME.CA, Environment: cfg.ACME.Environment, AccountEmail: cfg.ACME.AccountEmail,
RenewalCheckIntervalDays: cfg.ACME.RenewalCheckIntervalDays, RenewalNearExpiryDays: cfg.ACME.RenewalNearExpiryDays,
RenewalThresholdDays: cfg.ACME.RenewalThresholdDays, RenewalUrgentThresholdDays: cfg.ACME.RenewalUrgentThresholdDays,
RenewalMaxRetries: cfg.ACME.RenewalMaxRetries, RenewalRetryIntervalMins: cfg.ACME.RenewalRetryIntervalMins,
PropagationTimeoutSecs: cfg.ACME.PropagationTimeoutSecs, PropagationPollingIntervalSecs: cfg.ACME.PropagationPollingIntervalSecs,
},
DNS: settingsDNSView{Provider: cfg.DNS.Provider, VerificationState: cfg.DNS.VerificationState},
Notification: settingsNotificationView{
HTTPEnabled: cfg.Notification.HTTPEnabled, SMTPEnabled: cfg.Notification.SMTPEnabled, Provider: cfg.Notification.Provider,
SMTPHost: cfg.Notification.SMTPHost, SMTPPort: cfg.Notification.SMTPPort, SMTPUsername: cfg.Notification.SMTPUsername,
SMTPMode: cfg.Notification.SMTPMode, SMTPAllowPlaintextAuth: cfg.Notification.SMTPAllowPlaintextAuth,
SMTPCredentialConfigured: strings.TrimSpace(cfg.Notification.SMTPCredentialRef) != "",
Endpoint: cfg.Notification.Endpoint, HTTPPath: cfg.Notification.HTTPPath, HTTPLoginPath: cfg.Notification.HTTPLoginPath,
HTTPAuthMode: cfg.Notification.HTTPAuthMode, HTTPCredentialConfigured: strings.TrimSpace(cfg.Notification.HTTPCredentialRef) != "",
EventMode: cfg.Notification.EventMode, IncludeDeploymentDetails: cfg.Notification.IncludeDeploymentDetails,
IncludeCertificateSerial: cfg.Notification.IncludeCertificateSerial, IncludeManagementURL: cfg.Notification.IncludeManagementURL,
SenderName: cfg.Notification.SenderName, SenderEmail: cfg.Notification.SenderEmail,
Recipients: append([]string(nil), cfg.Notification.Recipients...), TimeoutSecs: cfg.Notification.TimeoutSecs,
RetryCount: cfg.Notification.RetryCount, RetryIntervalsSecs: append([]int(nil), cfg.Notification.RetryIntervalsSecs...),
},
}
}
func applySettingsRequest(cfg *config.Config, request settingsRequest) {
if cfg == nil {
return
}
if request.Server != nil {
if request.Server.Host != nil {
cfg.Server.Host = strings.TrimSpace(*request.Server.Host)
}
if request.Server.Port != nil {
cfg.Server.Port = *request.Server.Port
}
if request.Server.HTTPSMode != nil {
cfg.Server.HTTPSMode = strings.TrimSpace(*request.Server.HTTPSMode)
}
}
if request.Web != nil {
if request.Web.Enabled != nil {
cfg.Web.Enabled = *request.Web.Enabled
}
if request.Web.Hostname != nil {
cfg.Web.Hostname = strings.TrimSpace(*request.Web.Hostname)
}
if request.Web.PublicIP != nil {
cfg.Web.PublicIP = strings.TrimSpace(*request.Web.PublicIP)
}
}
if request.Paths != nil && request.Paths.Certificate != nil {
cfg.Paths.Certificate = strings.TrimSpace(*request.Paths.Certificate)
}
if request.ACME != nil {
if request.ACME.CA != nil {
cfg.ACME.CA = strings.TrimSpace(*request.ACME.CA)
}
if request.ACME.Environment != nil {
cfg.ACME.Environment = strings.TrimSpace(*request.ACME.Environment)
}
if request.ACME.AccountEmail != nil {
cfg.ACME.AccountEmail = strings.TrimSpace(*request.ACME.AccountEmail)
}
if request.ACME.RenewalThresholdDays != nil {
cfg.ACME.RenewalThresholdDays = *request.ACME.RenewalThresholdDays
}
if request.ACME.RenewalCheckIntervalDays != nil {
cfg.ACME.RenewalCheckIntervalDays = *request.ACME.RenewalCheckIntervalDays
}
if request.ACME.RenewalNearExpiryDays != nil {
cfg.ACME.RenewalNearExpiryDays = *request.ACME.RenewalNearExpiryDays
}
if request.ACME.RenewalUrgentThresholdDays != nil {
cfg.ACME.RenewalUrgentThresholdDays = *request.ACME.RenewalUrgentThresholdDays
}
if request.ACME.RenewalMaxRetries != nil {
cfg.ACME.RenewalMaxRetries = *request.ACME.RenewalMaxRetries
}
if request.ACME.RenewalRetryIntervalMins != nil {
cfg.ACME.RenewalRetryIntervalMins = *request.ACME.RenewalRetryIntervalMins
}
if request.ACME.PropagationTimeoutSecs != nil {
cfg.ACME.PropagationTimeoutSecs = *request.ACME.PropagationTimeoutSecs
}
if request.ACME.PropagationPollingIntervalSecs != nil {
cfg.ACME.PropagationPollingIntervalSecs = *request.ACME.PropagationPollingIntervalSecs
}
}
if request.DNS != nil && request.DNS.Provider != nil {
cfg.DNS.Provider = strings.TrimSpace(*request.DNS.Provider)
}
if request.Notification != nil {
item := request.Notification
if item.HTTPEnabled != nil {
cfg.Notification.HTTPEnabled = *item.HTTPEnabled
}
if item.SMTPEnabled != nil {
cfg.Notification.SMTPEnabled = *item.SMTPEnabled
}
if item.SMTPHost != nil {
cfg.Notification.SMTPHost = strings.TrimSpace(*item.SMTPHost)
}
if item.SMTPPort != nil {
cfg.Notification.SMTPPort = *item.SMTPPort
}
if item.SMTPUsername != nil {
cfg.Notification.SMTPUsername = strings.TrimSpace(*item.SMTPUsername)
}
if item.SMTPMode != nil {
cfg.Notification.SMTPMode = strings.TrimSpace(*item.SMTPMode)
}
if item.SMTPAllowPlaintextAuth != nil {
cfg.Notification.SMTPAllowPlaintextAuth = *item.SMTPAllowPlaintextAuth
}
if item.Provider != nil {
cfg.Notification.Provider = strings.TrimSpace(*item.Provider)
}
if item.Endpoint != nil {
cfg.Notification.Endpoint = strings.TrimSpace(*item.Endpoint)
}
if item.HTTPPath != nil {
cfg.Notification.HTTPPath = strings.TrimSpace(*item.HTTPPath)
}
if item.HTTPLoginPath != nil {
cfg.Notification.HTTPLoginPath = strings.TrimSpace(*item.HTTPLoginPath)
}
if item.HTTPAuthMode != nil {
cfg.Notification.HTTPAuthMode = strings.TrimSpace(*item.HTTPAuthMode)
}
if item.EventMode != nil {
cfg.Notification.EventMode = strings.TrimSpace(*item.EventMode)
}
if item.IncludeDeploymentDetails != nil {
cfg.Notification.IncludeDeploymentDetails = *item.IncludeDeploymentDetails
}
if item.IncludeCertificateSerial != nil {
cfg.Notification.IncludeCertificateSerial = *item.IncludeCertificateSerial
}
if item.IncludeManagementURL != nil {
cfg.Notification.IncludeManagementURL = *item.IncludeManagementURL
}
if item.SenderName != nil {
cfg.Notification.SenderName = strings.TrimSpace(*item.SenderName)
}
if item.SenderEmail != nil {
cfg.Notification.SenderEmail = strings.TrimSpace(*item.SenderEmail)
}
if item.Recipients != nil {
cfg.Notification.Recipients = append([]string(nil), (*item.Recipients)...)
}
if item.TimeoutSecs != nil {
cfg.Notification.TimeoutSecs = *item.TimeoutSecs
}
if item.RetryCount != nil {
cfg.Notification.RetryCount = *item.RetryCount
}
if item.RetryIntervalsSecs != nil {
cfg.Notification.RetryIntervalsSecs = append([]int(nil), (*item.RetryIntervalsSecs)...)
}
}
}