package setup import ( "fmt" "net" "net/mail" "strings" "golang.org/x/crypto/bcrypt" "git.pcux.cn/xuans/Scert/backend/internal/config" "git.pcux.cn/xuans/Scert/backend/internal/passwordpolicy" ) type Input struct { AdminUsername string AdminEmail string AdminPassword string ServerPort int EmailProvider string EmailEndpoint string EmailRecipients []string DNSProvider string DNSCredentialRef string DNSVerified bool CertificateRoot string WebHostname string WebPublicIP string WebEnabled bool HTTPSMode string } func Apply(cfg *config.Config, input Input) error { if cfg == nil { return fmt.Errorf("配置不能为空") } if strings.TrimSpace(input.AdminUsername) == "" { return fmt.Errorf("超级管理员用户名不能为空") } adminEmail, err := normalizeAdminEmail(input.AdminEmail) if err != nil { return err } if err := passwordpolicy.Validate(input.AdminPassword, input.AdminUsername); err != nil { return err } if input.ServerPort < 1 || input.ServerPort > 65535 { return fmt.Errorf("主服务端口无效:%d", input.ServerPort) } if strings.TrimSpace(input.DNSProvider) == "" { return fmt.Errorf("DNS 服务商不能为空") } if strings.TrimSpace(input.DNSCredentialRef) == "" { return fmt.Errorf("DNS 凭据引用不能为空;明文密钥不会直接写入配置文件") } if !input.DNSVerified { return fmt.Errorf("DNS API 尚未通过验证测试") } if strings.TrimSpace(input.WebHostname) == "" && strings.TrimSpace(input.WebPublicIP) == "" { return fmt.Errorf("管理页面必须配置域名或公网 IP") } if input.WebHostname != "" && input.WebPublicIP != "" { return fmt.Errorf("管理页面不能同时配置域名和公网 IP") } if input.WebPublicIP != "" && net.ParseIP(input.WebPublicIP) == nil { return fmt.Errorf("管理页面公网 IP 格式无效") } if input.HTTPSMode == "" { input.HTTPSMode = "direct" } if input.HTTPSMode != "direct" && input.HTTPSMode != "proxy" { return fmt.Errorf("HTTPS 模式无效:%s", input.HTTPSMode) } if strings.TrimSpace(input.CertificateRoot) == "" { input.CertificateRoot = cfg.Paths.Certificate } hash, err := bcrypt.GenerateFromPassword([]byte(input.AdminPassword), bcrypt.DefaultCost) if err != nil { return fmt.Errorf("生成管理员密码摘要失败:%w", err) } cfg.Admin.Username = strings.TrimSpace(input.AdminUsername) cfg.Admin.Email = adminEmail cfg.Admin.PasswordHash = string(hash) cfg.Server.Port = input.ServerPort cfg.Server.HTTPSMode = input.HTTPSMode cfg.Web.Enabled = input.WebEnabled cfg.Web.Hostname = strings.TrimSpace(input.WebHostname) cfg.Web.PublicIP = strings.TrimSpace(input.WebPublicIP) cfg.Web.InitReady = true cfg.Paths.Certificate = strings.TrimSpace(input.CertificateRoot) cfg.DNS.Provider = strings.TrimSpace(input.DNSProvider) cfg.DNS.CredentialRef = strings.TrimSpace(input.DNSCredentialRef) cfg.DNS.VerificationState = "verified" cfg.Notification.Provider = strings.TrimSpace(input.EmailProvider) cfg.Notification.Endpoint = strings.TrimSpace(input.EmailEndpoint) cfg.Notification.Recipients = cleanRecipients(input.EmailRecipients) cfg.SetupReady = true return cfg.Validate() } func cleanRecipients(values []string) []string { items := make([]string, 0, len(values)) seen := make(map[string]struct{}, len(values)) for _, value := range values { for _, part := range strings.Split(value, ",") { part = strings.TrimSpace(part) if part == "" { continue } if _, ok := seen[part]; ok { continue } seen[part] = struct{}{} items = append(items, part) } } return items } func normalizeAdminEmail(value string) (string, error) { value = strings.ToLower(strings.TrimSpace(value)) if value == "" { return "", fmt.Errorf("超级管理员绑定邮箱不能为空") } parsed, err := mail.ParseAddress(value) if err != nil || parsed.Address != value || parsed.Name != "" { return "", fmt.Errorf("超级管理员绑定邮箱格式无效") } return value, nil }