diff --git a/.goreleaser.yml b/.goreleaser.yml index 4d53fcdd..8b4b64fe 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -36,7 +36,7 @@ release: archives: - id: archive_noncgo builds: [build_noncgo] - format: zip + format: "zip" files: - CHANGELOG.md - LICENSE.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a70706ba..3e8fcee1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,7 +9,7 @@ ## 前提条件 -- Go 1.22+ (用于修改 Go 代码) +- Go 1.24+ (用于修改 Go 代码) - Node 20+ (用于修改 UI) 如果还没有这样做,你可以 fork Certimate 的主仓库,并克隆到本地以便进行修改: diff --git a/CONTRIBUTING_EN.md b/CONTRIBUTING_EN.md index 3d848ec8..59a783e9 100644 --- a/CONTRIBUTING_EN.md +++ b/CONTRIBUTING_EN.md @@ -9,7 +9,7 @@ Thank you for taking the time to improve Certimate! Below is a guide for submitt ## Prerequisites -- Go 1.22+ (for Go code changes) +- Go 1.24+ (for Go code changes) - Node 20+ (for Admin UI changes) If you haven't done so already, you can fork the Certimate repository and clone your fork to work locally: diff --git a/README.md b/README.md index f0face9d..82f81275 100644 --- a/README.md +++ b/README.md @@ -48,8 +48,8 @@ Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决 - 灵活的工作流编排方式,证书从申请到部署完全自动化; - 支持单域名、多域名、泛域名证书,可选 RSA、ECC 签名算法; - 支持 PEM、PFX、JKS 等多种格式输出证书; -- 支持 20+ 域名托管商(如阿里云、腾讯云、Cloudflare 等,[点此查看完整清单](https://docs.certimate.me/docs/reference/providers#supported-dns-providers)); -- 支持 70+ 部署目标(如 Kubernetes、CDN、WAF、负载均衡等,[点此查看完整清单](https://docs.certimate.me/docs/reference/providers#supported-host-providers)); +- 支持 30+ 域名托管商(如阿里云、腾讯云、Cloudflare 等,[点此查看完整清单](https://docs.certimate.me/docs/reference/providers#supported-dns-providers)); +- 支持 80+ 部署目标(如 Kubernetes、CDN、WAF、负载均衡等,[点此查看完整清单](https://docs.certimate.me/docs/reference/providers#supported-host-providers)); - 支持邮件、钉钉、飞书、企业微信、Webhook 等多种通知渠道; - 支持 Let's Encrypt、Buypass、Google Trust Services、SSL.com、ZeroSSL 等多种 ACME 证书颁发机构; - 更多特性等待探索。 diff --git a/README_EN.md b/README_EN.md index 5b62f12d..472efd66 100644 --- a/README_EN.md +++ b/README_EN.md @@ -38,8 +38,8 @@ Certimate aims to provide users with a secure and user-friendly SSL certificate - Flexible workflow orchestration, fully automation from certificate application to deployment; - Supports single-domain, multi-domain, wildcard certificates, with options for RSA or ECC. - Supports various certificate formats such as PEM, PFX, JKS. -- Supports more than 20+ domain registrars (e.g., Alibaba Cloud, Tencent Cloud, Cloudflare, etc. [Check out this link](https://docs.certimate.me/en/docs/reference/providers#supported-dns-providers)); -- Supports more than 70+ deployment targets (e.g., Kubernetes, CDN, WAF, load balancers, etc. [Check out this link](https://docs.certimate.me/en/docs/reference/providers#supported-host-providers)); +- Supports more than 30+ domain registrars (e.g., Alibaba Cloud, Tencent Cloud, Cloudflare, etc. [Check out this link](https://docs.certimate.me/en/docs/reference/providers#supported-dns-providers)); +- Supports more than 80+ deployment targets (e.g., Kubernetes, CDN, WAF, load balancers, etc. [Check out this link](https://docs.certimate.me/en/docs/reference/providers#supported-host-providers)); - Supports multiple notification channels including email, DingTalk, Feishu, WeCom, Webhook, and more; - Supports multiple ACME CAs including Let's Encrypt, Buypass, Google Trust Services,SSL.com, ZeroSSL, and more; - More features waiting to be discovered. diff --git a/internal/applicant/providers.go b/internal/applicant/providers.go index 454f9376..90a3cf72 100644 --- a/internal/applicant/providers.go +++ b/internal/applicant/providers.go @@ -27,6 +27,8 @@ import ( pNamecheap "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/namecheap" pNameDotCom "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/namedotcom" pNameSilo "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/namesilo" + pNetcup "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/netcup" + pNetlify "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/netlify" pNS1 "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/ns1" pPorkbun "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/porkbun" pPowerDNS "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/powerdns" @@ -402,6 +404,38 @@ func createApplicantProvider(options *applicantProviderOptions) (challenge.Provi return applicant, err } + case domain.ACMEDns01ProviderTypeNetcup: + { + access := domain.AccessConfigForNetcup{} + if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + applicant, err := pNetcup.NewChallengeProvider(&pNetcup.ChallengeProviderConfig{ + CustomerNumber: access.CustomerNumber, + ApiKey: access.ApiKey, + ApiPassword: access.ApiPassword, + DnsPropagationTimeout: options.DnsPropagationTimeout, + DnsTTL: options.DnsTTL, + }) + return applicant, err + } + + case domain.ACMEDns01ProviderTypeNetlify: + { + access := domain.AccessConfigForNetlify{} + if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + applicant, err := pNetlify.NewChallengeProvider(&pNetlify.ChallengeProviderConfig{ + ApiToken: access.ApiToken, + DnsPropagationTimeout: options.DnsPropagationTimeout, + DnsTTL: options.DnsTTL, + }) + return applicant, err + } + case domain.ACMEDns01ProviderTypeNS1: { access := domain.AccessConfigForNS1{} diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index 30cf9d41..d69b05b7 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -52,6 +52,7 @@ import ( pJDCloudVOD "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/jdcloud-vod" pK8sSecret "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/k8s-secret" pLocal "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/local" + pNetlifySite "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/netlify-site" pProxmoxVE "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/proxmoxve" pQiniuCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/qiniu-cdn" pQiniuPili "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/qiniu-pili" @@ -306,6 +307,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer AccessKeyId: access.AccessKeyId, SecretAccessKey: access.SecretAccessKey, Region: maputil.GetString(options.ProviderExtendedConfig, "region"), + CertificateArn: maputil.GetString(options.ProviderExtendedConfig, "certificateArn"), }) return deployer, err @@ -581,6 +583,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer deployer, err := pGoEdge.NewDeployer(&pGoEdge.DeployerConfig{ ApiUrl: access.ApiUrl, + ApiRole: access.ApiRole, AccessKeyId: access.AccessKeyId, AccessKey: access.AccessKey, AllowInsecureConnections: access.AllowInsecureConnections, @@ -693,16 +696,18 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer case domain.DeploymentProviderTypeLocal: { deployer, err := pLocal.NewDeployer(&pLocal.DeployerConfig{ - ShellEnv: pLocal.ShellEnvType(maputil.GetString(options.ProviderExtendedConfig, "shellEnv")), - PreCommand: maputil.GetString(options.ProviderExtendedConfig, "preCommand"), - PostCommand: maputil.GetString(options.ProviderExtendedConfig, "postCommand"), - OutputFormat: pLocal.OutputFormatType(maputil.GetOrDefaultString(options.ProviderExtendedConfig, "format", string(pLocal.OUTPUT_FORMAT_PEM))), - OutputCertPath: maputil.GetString(options.ProviderExtendedConfig, "certPath"), - OutputKeyPath: maputil.GetString(options.ProviderExtendedConfig, "keyPath"), - PfxPassword: maputil.GetString(options.ProviderExtendedConfig, "pfxPassword"), - JksAlias: maputil.GetString(options.ProviderExtendedConfig, "jksAlias"), - JksKeypass: maputil.GetString(options.ProviderExtendedConfig, "jksKeypass"), - JksStorepass: maputil.GetString(options.ProviderExtendedConfig, "jksStorepass"), + ShellEnv: pLocal.ShellEnvType(maputil.GetString(options.ProviderExtendedConfig, "shellEnv")), + PreCommand: maputil.GetString(options.ProviderExtendedConfig, "preCommand"), + PostCommand: maputil.GetString(options.ProviderExtendedConfig, "postCommand"), + OutputFormat: pLocal.OutputFormatType(maputil.GetOrDefaultString(options.ProviderExtendedConfig, "format", string(pLocal.OUTPUT_FORMAT_PEM))), + OutputCertPath: maputil.GetString(options.ProviderExtendedConfig, "certPath"), + OutputServerCertPath: maputil.GetString(options.ProviderExtendedConfig, "certPathForServerOnly"), + OutputIntermediaCertPath: maputil.GetString(options.ProviderExtendedConfig, "certPathForIntermediaOnly"), + OutputKeyPath: maputil.GetString(options.ProviderExtendedConfig, "keyPath"), + PfxPassword: maputil.GetString(options.ProviderExtendedConfig, "pfxPassword"), + JksAlias: maputil.GetString(options.ProviderExtendedConfig, "jksAlias"), + JksKeypass: maputil.GetString(options.ProviderExtendedConfig, "jksKeypass"), + JksStorepass: maputil.GetString(options.ProviderExtendedConfig, "jksStorepass"), }) return deployer, err } @@ -725,6 +730,20 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer return deployer, err } + case domain.DeploymentProviderTypeNetlifySite: + { + access := domain.AccessConfigForNetlify{} + if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + deployer, err := pNetlifySite.NewDeployer(&pNetlifySite.DeployerConfig{ + ApiToken: access.ApiToken, + SiteId: maputil.GetString(options.ProviderExtendedConfig, "siteId"), + }) + return deployer, err + } + case domain.DeploymentProviderTypeProxmoxVE: { access := domain.AccessConfigForProxmoxVE{} @@ -819,22 +838,24 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer } deployer, err := pSSH.NewDeployer(&pSSH.DeployerConfig{ - SshHost: access.Host, - SshPort: access.Port, - SshUsername: access.Username, - SshPassword: access.Password, - SshKey: access.Key, - SshKeyPassphrase: access.KeyPassphrase, - UseSCP: maputil.GetBool(options.ProviderExtendedConfig, "useSCP"), - PreCommand: maputil.GetString(options.ProviderExtendedConfig, "preCommand"), - PostCommand: maputil.GetString(options.ProviderExtendedConfig, "postCommand"), - OutputFormat: pSSH.OutputFormatType(maputil.GetOrDefaultString(options.ProviderExtendedConfig, "format", string(pSSH.OUTPUT_FORMAT_PEM))), - OutputCertPath: maputil.GetString(options.ProviderExtendedConfig, "certPath"), - OutputKeyPath: maputil.GetString(options.ProviderExtendedConfig, "keyPath"), - PfxPassword: maputil.GetString(options.ProviderExtendedConfig, "pfxPassword"), - JksAlias: maputil.GetString(options.ProviderExtendedConfig, "jksAlias"), - JksKeypass: maputil.GetString(options.ProviderExtendedConfig, "jksKeypass"), - JksStorepass: maputil.GetString(options.ProviderExtendedConfig, "jksStorepass"), + SshHost: access.Host, + SshPort: access.Port, + SshUsername: access.Username, + SshPassword: access.Password, + SshKey: access.Key, + SshKeyPassphrase: access.KeyPassphrase, + UseSCP: maputil.GetBool(options.ProviderExtendedConfig, "useSCP"), + PreCommand: maputil.GetString(options.ProviderExtendedConfig, "preCommand"), + PostCommand: maputil.GetString(options.ProviderExtendedConfig, "postCommand"), + OutputFormat: pSSH.OutputFormatType(maputil.GetOrDefaultString(options.ProviderExtendedConfig, "format", string(pSSH.OUTPUT_FORMAT_PEM))), + OutputCertPath: maputil.GetString(options.ProviderExtendedConfig, "certPath"), + OutputServerCertPath: maputil.GetString(options.ProviderExtendedConfig, "certPathForServerOnly"), + OutputIntermediaCertPath: maputil.GetString(options.ProviderExtendedConfig, "certPathForIntermediaOnly"), + OutputKeyPath: maputil.GetString(options.ProviderExtendedConfig, "keyPath"), + PfxPassword: maputil.GetString(options.ProviderExtendedConfig, "pfxPassword"), + JksAlias: maputil.GetString(options.ProviderExtendedConfig, "jksAlias"), + JksKeypass: maputil.GetString(options.ProviderExtendedConfig, "jksKeypass"), + JksStorepass: maputil.GetString(options.ProviderExtendedConfig, "jksStorepass"), }) return deployer, err } diff --git a/internal/domain/access.go b/internal/domain/access.go index 84afd292..35dd9b0a 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -149,6 +149,7 @@ type AccessConfigForGoDaddy struct { type AccessConfigForGoEdge struct { ApiUrl string `json:"apiUrl"` + ApiRole string `json:"apiRole"` AccessKeyId string `json:"accessKeyId"` AccessKey string `json:"accessKey"` AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` @@ -198,6 +199,16 @@ type AccessConfigForNameSilo struct { ApiKey string `json:"apiKey"` } +type AccessConfigForNetcup struct { + CustomerNumber string `json:"customerNumber"` + ApiKey string `json:"apiKey"` + ApiPassword string `json:"apiPassword"` +} + +type AccessConfigForNetlify struct { + ApiToken string `json:"apiToken"` +} + type AccessConfigForNS1 struct { ApiKey string `json:"apiKey"` } diff --git a/internal/domain/provider.go b/internal/domain/provider.go index 728f89b6..c635aad1 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -10,6 +10,7 @@ type AccessProviderType string */ const ( AccessProviderType1Panel = AccessProviderType("1panel") + AccessProviderTypeACMECA = AccessProviderType("acmeca") // ACME CA(预留) AccessProviderTypeACMEHttpReq = AccessProviderType("acmehttpreq") AccessProviderTypeAkamai = AccessProviderType("akamai") // Akamai(预留) AccessProviderTypeAliyun = AccessProviderType("aliyun") @@ -35,7 +36,8 @@ const ( AccessProviderTypeDynv6 = AccessProviderType("dynv6") AccessProviderTypeEdgio = AccessProviderType("edgio") AccessProviderTypeEmail = AccessProviderType("email") - AccessProviderTypeFastly = AccessProviderType("fastly") // Fastly(预留) + AccessProviderTypeFastly = AccessProviderType("fastly") // Fastly(预留) + AccessProviderTypeFlexCDN = AccessProviderType("flexcdn") // FlexCDN(预留) AccessProviderTypeGname = AccessProviderType("gname") AccessProviderTypeGcore = AccessProviderType("gcore") AccessProviderTypeGoDaddy = AccessProviderType("godaddy") @@ -47,11 +49,14 @@ const ( AccessProviderTypeLarkBot = AccessProviderType("larkbot") AccessProviderTypeLetsEncrypt = AccessProviderType("letsencrypt") AccessProviderTypeLetsEncryptStaging = AccessProviderType("letsencryptstaging") + AccessProviderTypeLeCDN = AccessProviderType("lecdn") // LeCDN(预留) AccessProviderTypeLocal = AccessProviderType("local") AccessProviderTypeMattermost = AccessProviderType("mattermost") AccessProviderTypeNamecheap = AccessProviderType("namecheap") AccessProviderTypeNameDotCom = AccessProviderType("namedotcom") AccessProviderTypeNameSilo = AccessProviderType("namesilo") + AccessProviderTypeNetcup = AccessProviderType("netcup") + AccessProviderTypeNetlify = AccessProviderType("netlify") AccessProviderTypeNS1 = AccessProviderType("ns1") AccessProviderTypePorkbun = AccessProviderType("porkbun") AccessProviderTypePowerDNS = AccessProviderType("powerdns") @@ -130,6 +135,8 @@ const ( ACMEDns01ProviderTypeNamecheap = ACMEDns01ProviderType(AccessProviderTypeNamecheap) ACMEDns01ProviderTypeNameDotCom = ACMEDns01ProviderType(AccessProviderTypeNameDotCom) ACMEDns01ProviderTypeNameSilo = ACMEDns01ProviderType(AccessProviderTypeNameSilo) + ACMEDns01ProviderTypeNetcup = ACMEDns01ProviderType(AccessProviderTypeNetcup) + ACMEDns01ProviderTypeNetlify = ACMEDns01ProviderType(AccessProviderTypeNetlify) ACMEDns01ProviderTypeNS1 = ACMEDns01ProviderType(AccessProviderTypeNS1) ACMEDns01ProviderTypePorkbun = ACMEDns01ProviderType(AccessProviderTypePorkbun) ACMEDns01ProviderTypePowerDNS = ACMEDns01ProviderType(AccessProviderTypePowerDNS) @@ -165,6 +172,7 @@ const ( DeploymentProviderTypeAliyunDDoS = DeploymentProviderType(AccessProviderTypeAliyun + "-ddos") DeploymentProviderTypeAliyunESA = DeploymentProviderType(AccessProviderTypeAliyun + "-esa") DeploymentProviderTypeAliyunFC = DeploymentProviderType(AccessProviderTypeAliyun + "-fc") + DeploymentProviderTypeAliyunGA = DeploymentProviderType(AccessProviderTypeAliyun + "-ga") // 阿里云全球加速(预留) DeploymentProviderTypeAliyunLive = DeploymentProviderType(AccessProviderTypeAliyun + "-live") DeploymentProviderTypeAliyunNLB = DeploymentProviderType(AccessProviderTypeAliyun + "-nlb") DeploymentProviderTypeAliyunOSS = DeploymentProviderType(AccessProviderTypeAliyun + "-oss") @@ -186,6 +194,7 @@ const ( DeploymentProviderTypeCdnfly = DeploymentProviderType(AccessProviderTypeCdnfly) DeploymentProviderTypeDogeCloudCDN = DeploymentProviderType(AccessProviderTypeDogeCloud + "-cdn") DeploymentProviderTypeEdgioApplications = DeploymentProviderType(AccessProviderTypeEdgio + "-applications") + DeploymentProviderTypeFlexCDN = DeploymentProviderType(AccessProviderTypeFlexCDN) // FlexCDN(预留) DeploymentProviderTypeGcoreCDN = DeploymentProviderType(AccessProviderTypeGcore + "-cdn") DeploymentProviderTypeGoEdge = DeploymentProviderType(AccessProviderTypeGoEdge) DeploymentProviderTypeHuaweiCloudCDN = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-cdn") @@ -197,7 +206,9 @@ const ( DeploymentProviderTypeJDCloudLive = DeploymentProviderType(AccessProviderTypeJDCloud + "-live") DeploymentProviderTypeJDCloudVOD = DeploymentProviderType(AccessProviderTypeJDCloud + "-vod") DeploymentProviderTypeKubernetesSecret = DeploymentProviderType(AccessProviderTypeKubernetes + "-secret") + DeploymentProviderTypeLeCDN = DeploymentProviderType(AccessProviderTypeLeCDN) // LeCDN(预留) DeploymentProviderTypeLocal = DeploymentProviderType(AccessProviderTypeLocal) + DeploymentProviderTypeNetlifySite = DeploymentProviderType(AccessProviderTypeNetlify + "-site") DeploymentProviderTypeProxmoxVE = DeploymentProviderType(AccessProviderTypeProxmoxVE) DeploymentProviderTypeQiniuCDN = DeploymentProviderType(AccessProviderTypeQiniu + "-cdn") DeploymentProviderTypeQiniuKodo = DeploymentProviderType(AccessProviderTypeQiniu + "-kodo") @@ -228,7 +239,9 @@ const ( DeploymentProviderTypeVolcEngineImageX = DeploymentProviderType(AccessProviderTypeVolcEngine + "-imagex") DeploymentProviderTypeVolcEngineLive = DeploymentProviderType(AccessProviderTypeVolcEngine + "-live") DeploymentProviderTypeVolcEngineTOS = DeploymentProviderType(AccessProviderTypeVolcEngine + "-tos") + DeploymentProviderTypeWangsuCDN = DeploymentProviderType(AccessProviderTypeWangsu + "-cdn") // 网宿 CDN(预留) DeploymentProviderTypeWangsuCDNPro = DeploymentProviderType(AccessProviderTypeWangsu + "-cdnpro") + DeploymentProviderTypeWangsuCert = DeploymentProviderType(AccessProviderTypeWangsu + "-cert") // 网宿证书管理(预留) DeploymentProviderTypeWebhook = DeploymentProviderType(AccessProviderTypeWebhook) ) diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/netcup/netcup.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/netcup/netcup.go new file mode 100644 index 00000000..43d7a694 --- /dev/null +++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/netcup/netcup.go @@ -0,0 +1,40 @@ +package netcup + +import ( + "time" + + "github.com/go-acme/lego/v4/challenge" + "github.com/go-acme/lego/v4/providers/dns/netcup" +) + +type ChallengeProviderConfig struct { + CustomerNumber string `json:"customerNumber"` + ApiKey string `json:"apiKey"` + ApiPassword string `json:"apiPassword"` + DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"` + DnsTTL int32 `json:"dnsTTL,omitempty"` +} + +func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) { + if config == nil { + panic("config is nil") + } + + providerConfig := netcup.NewDefaultConfig() + providerConfig.Customer = config.CustomerNumber + providerConfig.Key = config.ApiKey + providerConfig.Password = config.ApiPassword + if config.DnsPropagationTimeout != 0 { + providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second + } + if config.DnsTTL != 0 { + providerConfig.TTL = int(config.DnsTTL) + } + + provider, err := netcup.NewDNSProviderConfig(providerConfig) + if err != nil { + return nil, err + } + + return provider, nil +} diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/netlify/netlify.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/netlify/netlify.go new file mode 100644 index 00000000..f590372b --- /dev/null +++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/netlify/netlify.go @@ -0,0 +1,36 @@ +package netcup + +import ( + "time" + + "github.com/go-acme/lego/v4/challenge" + "github.com/go-acme/lego/v4/providers/dns/netlify" +) + +type ChallengeProviderConfig struct { + ApiToken string `json:"apiToken"` + DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"` + DnsTTL int32 `json:"dnsTTL,omitempty"` +} + +func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) { + if config == nil { + panic("config is nil") + } + + providerConfig := netlify.NewDefaultConfig() + providerConfig.Token = config.ApiToken + if config.DnsPropagationTimeout != 0 { + providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second + } + if config.DnsTTL != 0 { + providerConfig.TTL = int(config.DnsTTL) + } + + provider, err := netlify.NewDNSProviderConfig(providerConfig) + if err != nil { + return nil, err + } + + return provider, nil +} diff --git a/internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb.go b/internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb.go index 41a78968..d443514e 100644 --- a/internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb.go +++ b/internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "log/slog" - "strings" aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client" alislb "github.com/alibabacloud-go/slb-20140515/v4/client" @@ -310,22 +309,10 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*alislb.Clien } func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Uploader, error) { - casRegion := region - if casRegion != "" { - // 阿里云 CAS 服务接入点是独立于 CLB 服务的 - // 国内版固定接入点:华东一杭州 - // 国际版固定接入点:亚太东南一新加坡 - if !strings.HasPrefix(casRegion, "cn-") { - casRegion = "ap-southeast-1" - } else { - casRegion = "cn-hangzhou" - } - } - uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ AccessKeyId: accessKeyId, AccessKeySecret: accessKeySecret, - Region: casRegion, + Region: region, }) return uploader, err } diff --git a/internal/pkg/core/deployer/providers/aws-acm/aws_acm.go b/internal/pkg/core/deployer/providers/aws-acm/aws_acm.go index 3e817dcf..a9e90b60 100644 --- a/internal/pkg/core/deployer/providers/aws-acm/aws_acm.go +++ b/internal/pkg/core/deployer/providers/aws-acm/aws_acm.go @@ -5,9 +5,15 @@ import ( "fmt" "log/slog" + aws "github.com/aws/aws-sdk-go-v2/aws" + awscfg "github.com/aws/aws-sdk-go-v2/config" + awscred "github.com/aws/aws-sdk-go-v2/credentials" + awsacm "github.com/aws/aws-sdk-go-v2/service/acm" + "github.com/usual2970/certimate/internal/pkg/core/deployer" "github.com/usual2970/certimate/internal/pkg/core/uploader" uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aws-acm" + certutil "github.com/usual2970/certimate/internal/pkg/utils/cert" ) type DeployerConfig struct { @@ -17,11 +23,15 @@ type DeployerConfig struct { SecretAccessKey string `json:"secretAccessKey"` // AWS 区域。 Region string `json:"region"` + // ACM 证书 ARN。 + // 选填。零值时表示新建证书;否则表示更新证书。 + CertificateArn string `json:"certificateArn,omitempty"` } type DeployerProvider struct { config *DeployerConfig logger *slog.Logger + sdkClient *awsacm.Client sslUploader uploader.Uploader } @@ -32,6 +42,11 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { panic("config is nil") } + client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey, config.Region) + if err != nil { + return nil, fmt.Errorf("failed to create sdk client: %w", err) + } + uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ AccessKeyId: config.AccessKeyId, SecretAccessKey: config.SecretAccessKey, @@ -44,6 +59,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { return &DeployerProvider{ config: config, logger: slog.Default(), + sdkClient: client, sslUploader: uploader, }, nil } @@ -59,13 +75,48 @@ func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { } func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) { - // 上传证书到 ACM - upres, err := d.sslUploader.Upload(ctx, certPEM, privkeyPEM) - if err != nil { - return nil, fmt.Errorf("failed to upload certificate file: %w", err) + if d.config.CertificateArn == "" { + // 上传证书到 ACM + upres, err := d.sslUploader.Upload(ctx, certPEM, privkeyPEM) + if err != nil { + return nil, fmt.Errorf("failed to upload certificate file: %w", err) + } else { + d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) + } } else { - d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) + // 提取服务器证书 + serverCertPEM, intermediaCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM) + if err != nil { + return nil, fmt.Errorf("failed to extract certs: %w", err) + } + + // 导入证书 + // REF: https://docs.aws.amazon.com/en_us/acm/latest/APIReference/API_ImportCertificate.html + importCertificateReq := &awsacm.ImportCertificateInput{ + CertificateArn: aws.String(d.config.CertificateArn), + Certificate: ([]byte)(serverCertPEM), + CertificateChain: ([]byte)(intermediaCertPEM), + PrivateKey: ([]byte)(privkeyPEM), + } + importCertificateResp, err := d.sdkClient.ImportCertificate(context.TODO(), importCertificateReq) + d.logger.Debug("sdk request 'acm.ImportCertificate'", slog.Any("request", importCertificateReq), slog.Any("response", importCertificateResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'acm.ImportCertificate': %w", err) + } } return &deployer.DeployResult{}, nil } + +func createSdkClient(accessKeyId, secretAccessKey, region string) (*awsacm.Client, error) { + cfg, err := awscfg.LoadDefaultConfig(context.TODO()) + if err != nil { + return nil, err + } + + client := awsacm.NewFromConfig(cfg, func(o *awsacm.Options) { + o.Region = region + o.Credentials = aws.NewCredentialsCache(awscred.NewStaticCredentialsProvider(accessKeyId, secretAccessKey, "")) + }) + return client, nil +} diff --git a/internal/pkg/core/deployer/providers/azure-keyvault/azure_keyvault.go b/internal/pkg/core/deployer/providers/azure-keyvault/azure_keyvault.go index b1f21fdc..b8f8df99 100644 --- a/internal/pkg/core/deployer/providers/azure-keyvault/azure_keyvault.go +++ b/internal/pkg/core/deployer/providers/azure-keyvault/azure_keyvault.go @@ -32,7 +32,7 @@ type DeployerConfig struct { // Key Vault 名称。 KeyVaultName string `json:"keyvaultName"` // Key Vault 证书名称。 - // 选填。 + // 选填。零值时表示新建证书;否则表示更新证书。 CertificateName string `json:"certificateName,omitempty"` } diff --git a/internal/pkg/core/deployer/providers/baishan-cdn/baishan_cdn.go b/internal/pkg/core/deployer/providers/baishan-cdn/baishan_cdn.go index 2294ed03..e3efa6e4 100644 --- a/internal/pkg/core/deployer/providers/baishan-cdn/baishan_cdn.go +++ b/internal/pkg/core/deployer/providers/baishan-cdn/baishan_cdn.go @@ -20,7 +20,7 @@ type DeployerConfig struct { // 加速域名(支持泛域名)。 Domain string `json:"domain"` // 证书 ID。 - // 选填。 + // 选填。零值时表示新建证书;否则表示更新证书。 CertificateId string `json:"certificateId,omitempty"` } diff --git a/internal/pkg/core/deployer/providers/baotapanel-site/baotapanel_site.go b/internal/pkg/core/deployer/providers/baotapanel-site/baotapanel_site.go index f8266791..4d481f7f 100644 --- a/internal/pkg/core/deployer/providers/baotapanel-site/baotapanel_site.go +++ b/internal/pkg/core/deployer/providers/baotapanel-site/baotapanel_site.go @@ -20,11 +20,11 @@ type DeployerConfig struct { ApiKey string `json:"apiKey"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` - // 站点类型。 + // 网站类型。 SiteType string `json:"siteType"` - // 站点名称(单个)。 + // 网站名称(单个)。 SiteName string `json:"siteName,omitempty"` - // 站点名称(多个)。 + // 网站名称(多个)。 SiteNames []string `json:"siteNames,omitempty"` } diff --git a/internal/pkg/core/deployer/providers/cachefly/cachefly.go b/internal/pkg/core/deployer/providers/cachefly/cachefly.go index e3e819d7..21cb4dd0 100644 --- a/internal/pkg/core/deployer/providers/cachefly/cachefly.go +++ b/internal/pkg/core/deployer/providers/cachefly/cachefly.go @@ -51,6 +51,7 @@ func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) { // 上传证书 + // REF: https://api.cachefly.com/api/2.5/docs#tag/Certificates/paths/~1certificates/post createCertificateReq := &cfsdk.CreateCertificateRequest{ Certificate: certPEM, CertificateKey: privkeyPEM, diff --git a/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go b/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go index 3dd202a3..195c202e 100644 --- a/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go +++ b/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go @@ -56,18 +56,18 @@ func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { } func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) { - // 提取 Edgio 所需的服务端证书和中间证书内容 - privateCertPEM, intermediateCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM) + // 提取服务器证书和中间证书 + serverCertPEM, intermediaCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to extract certs: %w", err) } // 上传 TLS 证书 // REF: https://docs.edg.io/rest_api/#tag/tls-certs/operation/postConfigV01TlsCerts uploadTlsCertReq := edgiodtos.UploadTlsCertRequest{ EnvironmentID: d.config.EnvironmentId, - PrimaryCert: privateCertPEM, - IntermediateCert: intermediateCertPEM, + PrimaryCert: serverCertPEM, + IntermediateCert: intermediaCertPEM, PrivateKey: privkeyPEM, } uploadTlsCertResp, err := d.sdkClient.UploadTlsCert(uploadTlsCertReq) diff --git a/internal/pkg/core/deployer/providers/gcore-cdn/gcore_cdn.go b/internal/pkg/core/deployer/providers/gcore-cdn/gcore_cdn.go index af2db885..780f91a7 100644 --- a/internal/pkg/core/deployer/providers/gcore-cdn/gcore_cdn.go +++ b/internal/pkg/core/deployer/providers/gcore-cdn/gcore_cdn.go @@ -24,7 +24,7 @@ type DeployerConfig struct { // CDN 资源 ID。 ResourceId int64 `json:"resourceId"` // 证书 ID。 - // 选填。 + // 选填。零值时表示新建证书;否则表示更新证书。 CertificateId int64 `json:"certificateId,omitempty"` } @@ -112,7 +112,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE ValidateRootCA: false, } changeCertificateResp, err := d.sdkClients.SSLCerts.Update(context.TODO(), getCertificateDetailResp.ID, changeCertificateReq) - d.logger.Debug("sdk request 'sslcerts.Create'", slog.Any("request", changeCertificateReq), slog.Any("response", changeCertificateResp)) + d.logger.Debug("sdk request 'sslcerts.Update'", slog.Any("sslId", getCertificateDetailResp.ID), slog.Any("request", changeCertificateReq), slog.Any("response", changeCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'sslcerts.Update': %w", err) } diff --git a/internal/pkg/core/deployer/providers/goedge/goedge.go b/internal/pkg/core/deployer/providers/goedge/goedge.go index 61153b1b..73eade64 100644 --- a/internal/pkg/core/deployer/providers/goedge/goedge.go +++ b/internal/pkg/core/deployer/providers/goedge/goedge.go @@ -18,9 +18,11 @@ import ( type DeployerConfig struct { // GoEdge URL。 ApiUrl string `json:"apiUrl"` - // GoEdge 用户 AccessKeyId。 + // GoEdge 用户角色。 + ApiRole string `json:"apiRole"` + // GoEdge AccessKeyId。 AccessKeyId string `json:"accessKeyId"` - // GoEdge 用户 AccessKey。 + // GoEdge AccessKey。 AccessKey string `json:"accessKey"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` @@ -44,7 +46,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { panic("config is nil") } - client, err := createSdkClient(config.ApiUrl, config.AccessKeyId, config.AccessKey, config.AllowInsecureConnections) + client, err := createSdkClient(config.ApiUrl, config.ApiRole, config.AccessKeyId, config.AccessKey, config.AllowInsecureConnections) if err != nil { return nil, fmt.Errorf("failed to create sdk client: %w", err) } @@ -116,11 +118,15 @@ func (d *DeployerProvider) deployToCertificate(ctx context.Context, certPEM stri return nil } -func createSdkClient(apiUrl, accessKeyId, accessKey string, skipTlsVerify bool) (*goedgesdk.Client, error) { +func createSdkClient(apiUrl, apiRole, accessKeyId, accessKey string, skipTlsVerify bool) (*goedgesdk.Client, error) { if _, err := url.Parse(apiUrl); err != nil { return nil, errors.New("invalid goedge api url") } + if apiRole != "user" && apiRole != "admin" { + return nil, errors.New("invalid goedge api role") + } + if accessKeyId == "" { return nil, errors.New("invalid goedge access key id") } @@ -129,7 +135,7 @@ func createSdkClient(apiUrl, accessKeyId, accessKey string, skipTlsVerify bool) return nil, errors.New("invalid goedge access key") } - client := goedgesdk.NewClient(apiUrl, "user", accessKeyId, accessKey) + client := goedgesdk.NewClient(apiUrl, apiRole, accessKeyId, accessKey) if skipTlsVerify { client.WithTLSConfig(&tls.Config{InsecureSkipVerify: true}) } diff --git a/internal/pkg/core/deployer/providers/local/local.go b/internal/pkg/core/deployer/providers/local/local.go index 77f96543..a71ad9d3 100644 --- a/internal/pkg/core/deployer/providers/local/local.go +++ b/internal/pkg/core/deployer/providers/local/local.go @@ -25,6 +25,12 @@ type DeployerConfig struct { OutputFormat OutputFormatType `json:"outputFormat,omitempty"` // 输出证书文件路径。 OutputCertPath string `json:"outputCertPath,omitempty"` + // 输出服务器证书文件路径。 + // 选填。 + OutputServerCertPath string `json:"outputServerCertPath,omitempty"` + // 输出中间证书文件路径。 + // 选填。 + OutputIntermediaCertPath string `json:"outputIntermediaCertPath,omitempty"` // 输出私钥文件路径。 OutputKeyPath string `json:"outputKeyPath,omitempty"` // PFX 导出密码。 @@ -69,6 +75,12 @@ func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { } func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) { + // 提取服务器证书和中间证书 + serverCertPEM, intermediaCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM) + if err != nil { + return nil, fmt.Errorf("failed to extract certs: %w", err) + } + // 执行前置命令 if d.config.PreCommand != "" { stdout, stderr, err := execCommand(d.config.ShellEnv, d.config.PreCommand) @@ -86,6 +98,20 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE } d.logger.Info("ssl certificate file saved", slog.String("path", d.config.OutputCertPath)) + if d.config.OutputServerCertPath != "" { + if err := fileutil.WriteString(d.config.OutputServerCertPath, serverCertPEM); err != nil { + return nil, fmt.Errorf("failed to save server certificate file: %w", err) + } + d.logger.Info("ssl server certificate file saved", slog.String("path", d.config.OutputServerCertPath)) + } + + if d.config.OutputIntermediaCertPath != "" { + if err := fileutil.WriteString(d.config.OutputIntermediaCertPath, intermediaCertPEM); err != nil { + return nil, fmt.Errorf("failed to save intermedia certificate file: %w", err) + } + d.logger.Info("ssl intermedia certificate file saved", slog.String("path", d.config.OutputIntermediaCertPath)) + } + if err := fileutil.WriteString(d.config.OutputKeyPath, privkeyPEM); err != nil { return nil, fmt.Errorf("failed to save private key file: %w", err) } diff --git a/internal/pkg/core/deployer/providers/netlify-site/netlify_site.go b/internal/pkg/core/deployer/providers/netlify-site/netlify_site.go new file mode 100644 index 00000000..908b78c3 --- /dev/null +++ b/internal/pkg/core/deployer/providers/netlify-site/netlify_site.go @@ -0,0 +1,89 @@ +package netlifysite + +import ( + "context" + "errors" + "fmt" + "log/slog" + + "github.com/usual2970/certimate/internal/pkg/core/deployer" + netlifysdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/netlify" + certutil "github.com/usual2970/certimate/internal/pkg/utils/cert" +) + +type DeployerConfig struct { + // netlify API Token。 + ApiToken string `json:"apiToken"` + // netlify 网站 ID。 + SiteId string `json:"siteId"` +} + +type DeployerProvider struct { + config *DeployerConfig + logger *slog.Logger + sdkClient *netlifysdk.Client +} + +var _ deployer.Deployer = (*DeployerProvider)(nil) + +func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { + if config == nil { + panic("config is nil") + } + + client, err := createSdkClient(config.ApiToken) + if err != nil { + return nil, fmt.Errorf("failed to create sdk client: %w", err) + } + + return &DeployerProvider{ + config: config, + logger: slog.Default(), + sdkClient: client, + }, nil +} + +func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { + if logger == nil { + d.logger = slog.Default() + } else { + d.logger = logger + } + return d +} + +func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) { + if d.config.SiteId == "" { + return nil, errors.New("config `siteId` is required") + } + + // 提取服务器证书和中间证书 + serverCertPEM, intermediaCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM) + if err != nil { + return nil, fmt.Errorf("failed to extract certs: %w", err) + } + + // 上传网站证书 + // REF: https://open-api.netlify.com/#tag/sniCertificate/operation/provisionSiteTLSCertificate + provisionSiteTLSCertificateReq := &netlifysdk.ProvisionSiteTLSCertificateParams{ + Certificate: serverCertPEM, + CACertificates: intermediaCertPEM, + Key: privkeyPEM, + } + provisionSiteTLSCertificateResp, err := d.sdkClient.ProvisionSiteTLSCertificate(d.config.SiteId, provisionSiteTLSCertificateReq) + d.logger.Debug("sdk request 'netlify.provisionSiteTLSCertificate'", slog.String("siteId", d.config.SiteId), slog.Any("request", provisionSiteTLSCertificateReq), slog.Any("response", provisionSiteTLSCertificateResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'netlify.provisionSiteTLSCertificate': %w", err) + } + + return &deployer.DeployResult{}, nil +} + +func createSdkClient(apiToken string) (*netlifysdk.Client, error) { + if apiToken == "" { + return nil, errors.New("invalid netlify api token") + } + + client := netlifysdk.NewClient(apiToken) + return client, nil +} diff --git a/internal/pkg/core/deployer/providers/netlify-site/netlify_site_test.go b/internal/pkg/core/deployer/providers/netlify-site/netlify_site_test.go new file mode 100644 index 00000000..eb4a447c --- /dev/null +++ b/internal/pkg/core/deployer/providers/netlify-site/netlify_site_test.go @@ -0,0 +1,70 @@ +package netlifysite_test + +import ( + "context" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/netlify-site" +) + +var ( + fInputCertPath string + fInputKeyPath string + fApiToken string + fSiteId int64 +) + +func init() { + argsPrefix := "CERTIMATE_DEPLOYER_NETLIFYSITE_" + + flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") + flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") + flag.StringVar(&fApiToken, argsPrefix+"APITOKEN", "", "") + flag.Int64Var(&fSiteId, argsPrefix+"SITEID", 0, "") +} + +/* +Shell command to run this test: + + go test -v ./netlify_site_test.go -args \ + --CERTIMATE_DEPLOYER_NETLIFYSITE_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_DEPLOYER_NETLIFYSITE_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_DEPLOYER_NETLIFYSITE_APITOKEN="your-api-token" \ + --CERTIMATE_DEPLOYER_NETLIFYSITE_SITEID="your-site-id" +*/ +func TestDeploy(t *testing.T) { + flag.Parse() + + t.Run("Deploy", func(t *testing.T) { + t.Log(strings.Join([]string{ + "args:", + fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), + fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), + fmt.Sprintf("APITOKEN: %v", fApiToken), + fmt.Sprintf("SITEID: %v", fSiteId), + }, "\n")) + + deployer, err := provider.NewDeployer(&provider.DeployerConfig{ + ApiToken: fApiToken, + SiteId: fSiteId, + }) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + fInputCertData, _ := os.ReadFile(fInputCertPath) + fInputKeyData, _ := os.ReadFile(fInputKeyPath) + res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + t.Logf("ok: %v", res) + }) +} diff --git a/internal/pkg/core/deployer/providers/ssh/ssh.go b/internal/pkg/core/deployer/providers/ssh/ssh.go index 4b8b433d..cf09214b 100644 --- a/internal/pkg/core/deployer/providers/ssh/ssh.go +++ b/internal/pkg/core/deployer/providers/ssh/ssh.go @@ -41,6 +41,12 @@ type DeployerConfig struct { OutputFormat OutputFormatType `json:"outputFormat,omitempty"` // 输出证书文件路径。 OutputCertPath string `json:"outputCertPath,omitempty"` + // 输出服务器证书文件路径。 + // 选填。 + OutputServerCertPath string `json:"outputServerCertPath,omitempty"` + // 输出中间证书文件路径。 + // 选填。 + OutputIntermediaCertPath string `json:"outputIntermediaCertPath,omitempty"` // 输出私钥文件路径。 OutputKeyPath string `json:"outputKeyPath,omitempty"` // PFX 导出密码。 @@ -85,6 +91,12 @@ func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { } func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) { + // 提取服务器证书和中间证书 + serverCertPEM, intermediaCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM) + if err != nil { + return nil, fmt.Errorf("failed to extract certs: %w", err) + } + // 连接 client, err := createSshClient( d.config.SshHost, @@ -118,6 +130,20 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE } d.logger.Info("ssl certificate file uploaded", slog.String("path", d.config.OutputCertPath)) + if d.config.OutputServerCertPath != "" { + if err := writeFileString(client, d.config.UseSCP, d.config.OutputServerCertPath, serverCertPEM); err != nil { + return nil, fmt.Errorf("failed to save server certificate file: %w", err) + } + d.logger.Info("ssl server certificate file uploaded", slog.String("path", d.config.OutputServerCertPath)) + } + + if d.config.OutputIntermediaCertPath != "" { + if err := writeFileString(client, d.config.UseSCP, d.config.OutputIntermediaCertPath, intermediaCertPEM); err != nil { + return nil, fmt.Errorf("failed to save intermedia certificate file: %w", err) + } + d.logger.Info("ssl intermedia certificate file uploaded", slog.String("path", d.config.OutputIntermediaCertPath)) + } + if err := writeFileString(client, d.config.UseSCP, d.config.OutputKeyPath, privkeyPEM); err != nil { return nil, fmt.Errorf("failed to upload private key file: %w", err) } diff --git a/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro.go b/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro.go index 2faf1b03..ee16b08a 100644 --- a/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro.go +++ b/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro.go @@ -34,7 +34,7 @@ type DeployerConfig struct { // 加速域名(支持泛域名)。 Domain string `json:"domain"` // 证书 ID。 - // 选填。 + // 选填。零值时表示新建证书;否则表示更新证书。 CertificateId string `json:"certificateId,omitempty"` // Webhook ID。 // 选填。 diff --git a/internal/pkg/core/deployer/providers/webhook/webhook.go b/internal/pkg/core/deployer/providers/webhook/webhook.go index 07b2eaaa..418b2c1a 100644 --- a/internal/pkg/core/deployer/providers/webhook/webhook.go +++ b/internal/pkg/core/deployer/providers/webhook/webhook.go @@ -75,6 +75,12 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE return nil, fmt.Errorf("failed to parse x509: %w", err) } + // 提取服务器证书和中间证书 + serverCertPEM, intermediaCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM) + if err != nil { + return nil, fmt.Errorf("failed to extract certs: %w", err) + } + // 处理 Webhook URL webhookUrl, err := url.Parse(d.config.WebhookUrl) if err != nil { @@ -134,6 +140,8 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE replaceJsonValueRecursively(webhookData, "${DOMAIN}", certX509.Subject.CommonName) replaceJsonValueRecursively(webhookData, "${DOMAINS}", strings.Join(certX509.DNSNames, ";")) replaceJsonValueRecursively(webhookData, "${CERTIFICATE}", certPEM) + replaceJsonValueRecursively(webhookData, "${SERVER_CERTIFICATE}", serverCertPEM) + replaceJsonValueRecursively(webhookData, "${INTERMEDIA_CERTIFICATE}", intermediaCertPEM) replaceJsonValueRecursively(webhookData, "${PRIVATE_KEY}", privkeyPEM) if webhookMethod == http.MethodGet || webhookContentType == CONTENT_TYPE_FORM || webhookContentType == CONTENT_TYPE_MULTIPART { diff --git a/internal/pkg/core/uploader/providers/aws-acm/aws_acm.go b/internal/pkg/core/uploader/providers/aws-acm/aws_acm.go index f808083c..05cc70e3 100644 --- a/internal/pkg/core/uploader/providers/aws-acm/aws_acm.go +++ b/internal/pkg/core/uploader/providers/aws-acm/aws_acm.go @@ -65,9 +65,11 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE return nil, err } - // 生成 AWS 业务参数 - scertPEM, _ := certutil.ConvertCertificateToPEM(certX509) - bcertPEM := certPEM + // 提取服务器证书 + serverCertPEM, intermediaCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM) + if err != nil { + return nil, fmt.Errorf("failed to extract certs: %w", err) + } // 获取证书列表,避免重复上传 // REF: https://docs.aws.amazon.com/en_us/acm/latest/APIReference/API_ListCertificates.html @@ -145,8 +147,8 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE // 导入证书 // REF: https://docs.aws.amazon.com/en_us/acm/latest/APIReference/API_ImportCertificate.html importCertificateReq := &awsacm.ImportCertificateInput{ - Certificate: ([]byte)(scertPEM), - CertificateChain: ([]byte)(bcertPEM), + Certificate: ([]byte)(serverCertPEM), + CertificateChain: ([]byte)(intermediaCertPEM), PrivateKey: ([]byte)(privkeyPEM), } importCertificateResp, err := u.sdkClient.ImportCertificate(context.TODO(), importCertificateReq) diff --git a/internal/pkg/sdk3rd/1panel/api.go b/internal/pkg/sdk3rd/1panel/api.go index baab2313..68bcca36 100644 --- a/internal/pkg/sdk3rd/1panel/api.go +++ b/internal/pkg/sdk3rd/1panel/api.go @@ -1,4 +1,4 @@ -package onepanelsdk +package onepanel import ( "fmt" diff --git a/internal/pkg/sdk3rd/1panel/client.go b/internal/pkg/sdk3rd/1panel/client.go index 02dc8f58..0afbe2a5 100644 --- a/internal/pkg/sdk3rd/1panel/client.go +++ b/internal/pkg/sdk3rd/1panel/client.go @@ -1,4 +1,4 @@ -package onepanelsdk +package onepanel import ( "crypto/md5" @@ -97,7 +97,7 @@ func (c *Client) sendRequestWithResult(method string, path string, params interf if err := json.Unmarshal(resp.Body(), &result); err != nil { return fmt.Errorf("1panel api error: failed to parse response: %w", err) } else if errcode := result.GetCode(); errcode/100 != 2 { - return fmt.Errorf("1panel api error: %d - %s", errcode, result.GetMessage()) + return fmt.Errorf("1panel api error: code='%d', message='%s'", errcode, result.GetMessage()) } return nil diff --git a/internal/pkg/sdk3rd/1panel/models.go b/internal/pkg/sdk3rd/1panel/models.go index 13f144d9..57ea5154 100644 --- a/internal/pkg/sdk3rd/1panel/models.go +++ b/internal/pkg/sdk3rd/1panel/models.go @@ -1,4 +1,4 @@ -package onepanelsdk +package onepanel type BaseResponse interface { GetCode() int32 diff --git a/internal/pkg/sdk3rd/baishan/api.go b/internal/pkg/sdk3rd/baishan/api.go index 9abc6427..dba90fad 100644 --- a/internal/pkg/sdk3rd/baishan/api.go +++ b/internal/pkg/sdk3rd/baishan/api.go @@ -1,4 +1,4 @@ -package baishansdk +package baishan import ( "net/http" diff --git a/internal/pkg/sdk3rd/baishan/client.go b/internal/pkg/sdk3rd/baishan/client.go index ad906cbe..408bcbc9 100644 --- a/internal/pkg/sdk3rd/baishan/client.go +++ b/internal/pkg/sdk3rd/baishan/client.go @@ -1,4 +1,4 @@ -package baishansdk +package baishan import ( "encoding/json" @@ -93,7 +93,7 @@ func (c *Client) sendRequestWithResult(method string, path string, params interf if err := json.Unmarshal(resp.Body(), &result); err != nil { return fmt.Errorf("baishan api error: failed to parse response: %w", err) } else if errcode := result.GetCode(); errcode != 0 { - return fmt.Errorf("baishan api error: %d - %s", errcode, result.GetMessage()) + return fmt.Errorf("baishan api error: code='%d', message='%s'", errcode, result.GetMessage()) } return nil diff --git a/internal/pkg/sdk3rd/baishan/models.go b/internal/pkg/sdk3rd/baishan/models.go index 397061d4..457729e8 100644 --- a/internal/pkg/sdk3rd/baishan/models.go +++ b/internal/pkg/sdk3rd/baishan/models.go @@ -1,4 +1,4 @@ -package baishansdk +package baishan import "encoding/json" diff --git a/internal/pkg/sdk3rd/btpanel/api.go b/internal/pkg/sdk3rd/btpanel/api.go index a2b63127..926e03e5 100644 --- a/internal/pkg/sdk3rd/btpanel/api.go +++ b/internal/pkg/sdk3rd/btpanel/api.go @@ -1,4 +1,4 @@ -package btpanelsdk +package btpanel func (c *Client) ConfigSavePanelSSL(req *ConfigSavePanelSSLRequest) (*ConfigSavePanelSSLResponse, error) { resp := &ConfigSavePanelSSLResponse{} diff --git a/internal/pkg/sdk3rd/btpanel/client.go b/internal/pkg/sdk3rd/btpanel/client.go index 1e48f734..781e9a75 100644 --- a/internal/pkg/sdk3rd/btpanel/client.go +++ b/internal/pkg/sdk3rd/btpanel/client.go @@ -1,4 +1,4 @@ -package btpanelsdk +package btpanel import ( "crypto/md5" @@ -104,7 +104,7 @@ func (c *Client) sendRequestWithResult(path string, params interface{}, result B if result.GetMessage() == nil { return fmt.Errorf("baota api error: unknown error") } else { - return fmt.Errorf("baota api error: %s", *result.GetMessage()) + return fmt.Errorf("baota api error: message='%s'", *result.GetMessage()) } } diff --git a/internal/pkg/sdk3rd/btpanel/models.go b/internal/pkg/sdk3rd/btpanel/models.go index 8625e539..923efeda 100644 --- a/internal/pkg/sdk3rd/btpanel/models.go +++ b/internal/pkg/sdk3rd/btpanel/models.go @@ -1,4 +1,4 @@ -package btpanelsdk +package btpanel type BaseResponse interface { GetStatus() *bool diff --git a/internal/pkg/sdk3rd/bunny/api.go b/internal/pkg/sdk3rd/bunny/api.go index 9c6aa285..01f27606 100644 --- a/internal/pkg/sdk3rd/bunny/api.go +++ b/internal/pkg/sdk3rd/bunny/api.go @@ -1,4 +1,4 @@ -package bunnysdk +package bunny import ( "fmt" diff --git a/internal/pkg/sdk3rd/bunny/client.go b/internal/pkg/sdk3rd/bunny/client.go index 90b43f39..2b1ed4d1 100644 --- a/internal/pkg/sdk3rd/bunny/client.go +++ b/internal/pkg/sdk3rd/bunny/client.go @@ -1,4 +1,4 @@ -package bunnysdk +package bunny import ( "encoding/json" diff --git a/internal/pkg/sdk3rd/bunny/models.go b/internal/pkg/sdk3rd/bunny/models.go index 3306cf5b..3920eba1 100644 --- a/internal/pkg/sdk3rd/bunny/models.go +++ b/internal/pkg/sdk3rd/bunny/models.go @@ -1,4 +1,4 @@ -package bunnysdk +package bunny type AddCustomCertificateRequest struct { Hostname string `json:"Hostname"` diff --git a/internal/pkg/sdk3rd/cachefly/api.go b/internal/pkg/sdk3rd/cachefly/api.go index b4219fa1..0b4ae265 100644 --- a/internal/pkg/sdk3rd/cachefly/api.go +++ b/internal/pkg/sdk3rd/cachefly/api.go @@ -1,4 +1,4 @@ -package cacheflysdk +package cachefly import ( "net/http" diff --git a/internal/pkg/sdk3rd/cachefly/client.go b/internal/pkg/sdk3rd/cachefly/client.go index a460ae96..b1777ea9 100644 --- a/internal/pkg/sdk3rd/cachefly/client.go +++ b/internal/pkg/sdk3rd/cachefly/client.go @@ -1,4 +1,4 @@ -package cacheflysdk +package cachefly import ( "encoding/json" diff --git a/internal/pkg/sdk3rd/cachefly/models.go b/internal/pkg/sdk3rd/cachefly/models.go index 4026f2f6..bcab441a 100644 --- a/internal/pkg/sdk3rd/cachefly/models.go +++ b/internal/pkg/sdk3rd/cachefly/models.go @@ -1,4 +1,4 @@ -package cacheflysdk +package cachefly type BaseResponse interface { GetMessage() string diff --git a/internal/pkg/sdk3rd/cdnfly/api.go b/internal/pkg/sdk3rd/cdnfly/api.go index 4091a84d..ee827b8c 100644 --- a/internal/pkg/sdk3rd/cdnfly/api.go +++ b/internal/pkg/sdk3rd/cdnfly/api.go @@ -1,4 +1,4 @@ -package cdnflysdk +package cdnfly import ( "fmt" diff --git a/internal/pkg/sdk3rd/cdnfly/client.go b/internal/pkg/sdk3rd/cdnfly/client.go index c1a810d9..0061e363 100644 --- a/internal/pkg/sdk3rd/cdnfly/client.go +++ b/internal/pkg/sdk3rd/cdnfly/client.go @@ -1,4 +1,4 @@ -package cdnflysdk +package cdnfly import ( "crypto/tls" @@ -89,7 +89,7 @@ func (c *Client) sendRequestWithResult(method string, path string, params interf if err := json.Unmarshal(resp.Body(), &result); err != nil { return fmt.Errorf("cdnfly api error: failed to parse response: %w", err) } else if errcode := result.GetCode(); errcode != "" && errcode != "0" { - return fmt.Errorf("cdnfly api error: %s - %s", errcode, result.GetMessage()) + return fmt.Errorf("cdnfly api error: code='%s', message='%s'", errcode, result.GetMessage()) } return nil diff --git a/internal/pkg/sdk3rd/cdnfly/models.go b/internal/pkg/sdk3rd/cdnfly/models.go index 9622627a..d676becd 100644 --- a/internal/pkg/sdk3rd/cdnfly/models.go +++ b/internal/pkg/sdk3rd/cdnfly/models.go @@ -1,4 +1,4 @@ -package cdnflysdk +package cdnfly import "fmt" diff --git a/internal/pkg/sdk3rd/dnsla/api.go b/internal/pkg/sdk3rd/dnsla/api.go index 4b53a2ee..6f999ce7 100644 --- a/internal/pkg/sdk3rd/dnsla/api.go +++ b/internal/pkg/sdk3rd/dnsla/api.go @@ -1,4 +1,4 @@ -package dnslasdk +package dnsla import ( "fmt" diff --git a/internal/pkg/sdk3rd/dnsla/client.go b/internal/pkg/sdk3rd/dnsla/client.go index d557635b..7dfbd00a 100644 --- a/internal/pkg/sdk3rd/dnsla/client.go +++ b/internal/pkg/sdk3rd/dnsla/client.go @@ -1,4 +1,4 @@ -package dnslasdk +package dnsla import ( "encoding/json" @@ -78,7 +78,7 @@ func (c *Client) sendRequestWithResult(method string, path string, params interf if err := json.Unmarshal(resp.Body(), &result); err != nil { return fmt.Errorf("dnsla api error: failed to parse response: %w", err) } else if errcode := result.GetCode(); errcode/100 != 2 { - return fmt.Errorf("dnsla api error: %d - %s", errcode, result.GetMessage()) + return fmt.Errorf("dnsla api error: code='%d', message='%s'", errcode, result.GetMessage()) } return nil diff --git a/internal/pkg/sdk3rd/dnsla/models.go b/internal/pkg/sdk3rd/dnsla/models.go index 701c7a83..38fd623b 100644 --- a/internal/pkg/sdk3rd/dnsla/models.go +++ b/internal/pkg/sdk3rd/dnsla/models.go @@ -1,4 +1,4 @@ -package dnslasdk +package dnsla type BaseResponse interface { GetCode() int32 diff --git a/internal/pkg/sdk3rd/dogecloud/client.go b/internal/pkg/sdk3rd/dogecloud/client.go index 965580c2..46f3513d 100644 --- a/internal/pkg/sdk3rd/dogecloud/client.go +++ b/internal/pkg/sdk3rd/dogecloud/client.go @@ -1,4 +1,4 @@ -package dogecloudsdk +package dogecloud import ( "crypto/hmac" diff --git a/internal/pkg/sdk3rd/dogecloud/models.go b/internal/pkg/sdk3rd/dogecloud/models.go index 1642ad2d..8dd78a3b 100644 --- a/internal/pkg/sdk3rd/dogecloud/models.go +++ b/internal/pkg/sdk3rd/dogecloud/models.go @@ -1,4 +1,4 @@ -package dogecloudsdk +package dogecloud type BaseResponse struct { Code *int `json:"code,omitempty"` diff --git a/internal/pkg/sdk3rd/gname/api.go b/internal/pkg/sdk3rd/gname/api.go index 5a4c681f..16b80065 100644 --- a/internal/pkg/sdk3rd/gname/api.go +++ b/internal/pkg/sdk3rd/gname/api.go @@ -1,4 +1,4 @@ -package gnamesdk +package gname func (c *Client) AddDomainResolution(req *AddDomainResolutionRequest) (*AddDomainResolutionResponse, error) { resp := &AddDomainResolutionResponse{} diff --git a/internal/pkg/sdk3rd/gname/client.go b/internal/pkg/sdk3rd/gname/client.go index 017a3315..9424e8ec 100644 --- a/internal/pkg/sdk3rd/gname/client.go +++ b/internal/pkg/sdk3rd/gname/client.go @@ -1,4 +1,4 @@ -package gnamesdk +package gname import ( "crypto/md5" @@ -97,7 +97,7 @@ func (c *Client) sendRequestWithResult(path string, params interface{}, result B if err := json.Unmarshal(resp.Body(), &result); err != nil { return fmt.Errorf("gname api error: failed to parse response: %w", err) } else if errcode := result.GetCode(); errcode != 1 { - return fmt.Errorf("gname api error: %d - %s", errcode, result.GetMessage()) + return fmt.Errorf("gname api error: code='%d', message='%s'", errcode, result.GetMessage()) } return nil diff --git a/internal/pkg/sdk3rd/gname/models.go b/internal/pkg/sdk3rd/gname/models.go index 58283c50..a50b609b 100644 --- a/internal/pkg/sdk3rd/gname/models.go +++ b/internal/pkg/sdk3rd/gname/models.go @@ -1,4 +1,4 @@ -package gnamesdk +package gname import "encoding/json" diff --git a/internal/pkg/sdk3rd/goedge/api.go b/internal/pkg/sdk3rd/goedge/api.go index 67ee9194..c217e8ae 100644 --- a/internal/pkg/sdk3rd/goedge/api.go +++ b/internal/pkg/sdk3rd/goedge/api.go @@ -9,7 +9,7 @@ import ( func (c *Client) getAccessToken() error { req := &getAPIAccessTokenRequest{ - Type: c.apiUserType, + Type: c.apiRole, AccessKeyId: c.accessKeyId, AccessKey: c.accessKey, } diff --git a/internal/pkg/sdk3rd/goedge/client.go b/internal/pkg/sdk3rd/goedge/client.go index 96291fb3..c42b798b 100644 --- a/internal/pkg/sdk3rd/goedge/client.go +++ b/internal/pkg/sdk3rd/goedge/client.go @@ -14,7 +14,7 @@ import ( type Client struct { apiHost string - apiUserType string + apiRole string accessKeyId string accessKey string @@ -25,12 +25,12 @@ type Client struct { client *resty.Client } -func NewClient(apiHost, apiUserType, accessKeyId, accessKey string) *Client { +func NewClient(apiHost, apiRole, accessKeyId, accessKey string) *Client { client := resty.New() return &Client{ apiHost: strings.TrimRight(apiHost, "/"), - apiUserType: apiUserType, + apiRole: apiRole, accessKeyId: accessKeyId, accessKey: accessKey, client: client, @@ -96,7 +96,7 @@ func (c *Client) sendRequestWithResult(method string, path string, params interf if err := json.Unmarshal(resp.Body(), &result); err != nil { return fmt.Errorf("goedge api error: failed to parse response: %w", err) } else if errcode := result.GetCode(); errcode != 200 { - return fmt.Errorf("goedge api error: %d - %s", errcode, result.GetMessage()) + return fmt.Errorf("goedge api error: code='%d', message='%s'", errcode, result.GetMessage()) } return nil diff --git a/internal/pkg/sdk3rd/netlify/api.go b/internal/pkg/sdk3rd/netlify/api.go new file mode 100644 index 00000000..095ff303 --- /dev/null +++ b/internal/pkg/sdk3rd/netlify/api.go @@ -0,0 +1,17 @@ +package netlify + +import ( + "fmt" + "net/http" + "net/url" +) + +func (c *Client) ProvisionSiteTLSCertificate(siteId string, params *ProvisionSiteTLSCertificateParams) (*ProvisionSiteTLSCertificateResponse, error) { + if siteId == "" { + return nil, fmt.Errorf("netlify api error: invalid parameter: SiteId") + } + + resp := &ProvisionSiteTLSCertificateResponse{} + err := c.sendRequestWithResult(http.MethodPost, fmt.Sprintf("/sites/%s/ssl", url.PathEscape(siteId)), params, nil, resp) + return resp, err +} diff --git a/internal/pkg/sdk3rd/netlify/client.go b/internal/pkg/sdk3rd/netlify/client.go new file mode 100644 index 00000000..5f8bc6c2 --- /dev/null +++ b/internal/pkg/sdk3rd/netlify/client.go @@ -0,0 +1,97 @@ +package netlify + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "github.com/go-resty/resty/v2" +) + +type Client struct { + apiToken string + + client *resty.Client +} + +func NewClient(apiToken string) *Client { + client := resty.New() + + return &Client{ + apiToken: apiToken, + client: client, + } +} + +func (c *Client) WithTimeout(timeout time.Duration) *Client { + c.client.SetTimeout(timeout) + return c +} + +func (c *Client) sendRequest(method string, path string, queryParams interface{}, payloadParams interface{}) (*resty.Response, error) { + req := c.client.R().SetHeader("Authorization", "Bearer "+c.apiToken) + req.Method = method + req.URL = "https://api.netlify.com/api/v1" + path + + if queryParams != nil { + qs := make(map[string]string) + temp := make(map[string]any) + jsonb, _ := json.Marshal(queryParams) + json.Unmarshal(jsonb, &temp) + for k, v := range temp { + if v != nil { + qs[k] = fmt.Sprintf("%v", v) + } + } + req = req.SetQueryParams(qs) + } + + if strings.EqualFold(method, http.MethodGet) { + qs := make(map[string]string) + if payloadParams != nil { + temp := make(map[string]any) + jsonb, _ := json.Marshal(payloadParams) + json.Unmarshal(jsonb, &temp) + for k, v := range temp { + if v != nil { + qs[k] = fmt.Sprintf("%v", v) + } + } + } + + req = req.SetQueryParams(qs) + } else { + req = req. + SetHeader("Content-Type", "application/json"). + SetBody(payloadParams) + } + + resp, err := req.Send() + if err != nil { + return resp, fmt.Errorf("netlify api error: failed to send request: %w", err) + } else if resp.IsError() { + return resp, fmt.Errorf("netlify api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body()) + } + + return resp, nil +} + +func (c *Client) sendRequestWithResult(method string, path string, queryParams interface{}, payloadParams interface{}, result BaseResponse) error { + resp, err := c.sendRequest(method, path, queryParams, payloadParams) + if err != nil { + if resp != nil { + json.Unmarshal(resp.Body(), &result) + } + return err + } + + if err := json.Unmarshal(resp.Body(), &result); err != nil { + return fmt.Errorf("netlify api error: failed to parse response: %w", err) + } else if errcode := result.GetCode(); errcode != 0 { + return fmt.Errorf("netlify api error: code='%d', message='%s'", errcode, result.GetMessage()) + } + + return nil +} diff --git a/internal/pkg/sdk3rd/netlify/models.go b/internal/pkg/sdk3rd/netlify/models.go new file mode 100644 index 00000000..196ee6cc --- /dev/null +++ b/internal/pkg/sdk3rd/netlify/models.go @@ -0,0 +1,40 @@ +package netlify + +type BaseResponse interface { + GetCode() int32 + GetMessage() string +} + +type baseResponse struct { + Code *int32 `json:"code,omitempty"` + Message *string `json:"message,omitempty"` +} + +func (r *baseResponse) GetCode() int32 { + if r.Code != nil { + return *r.Code + } + return 0 +} + +func (r *baseResponse) GetMessage() string { + if r.Message != nil { + return *r.Message + } + return "" +} + +type ProvisionSiteTLSCertificateParams struct { + Certificate string `json:"certificate"` + CACertificates string `json:"key"` + Key string `json:"ca_certificates"` +} + +type ProvisionSiteTLSCertificateResponse struct { + baseResponse + Domains []string `json:"domains,omitempty"` + State string `json:"state,omitempty"` + ExpiresAt string `json:"expires_at,omitempty"` + CreatedAt string `json:"created_at,omitempty"` + UpdatedAt string `json:"updated_at,omitempty"` +} diff --git a/internal/pkg/sdk3rd/qiniu/auth.go b/internal/pkg/sdk3rd/qiniu/auth.go index d27fbe03..7f053f56 100644 --- a/internal/pkg/sdk3rd/qiniu/auth.go +++ b/internal/pkg/sdk3rd/qiniu/auth.go @@ -1,4 +1,4 @@ -package qiniusdk +package qiniu import ( "net/http" diff --git a/internal/pkg/sdk3rd/qiniu/client.go b/internal/pkg/sdk3rd/qiniu/client.go index ece26b9a..0d777aa3 100644 --- a/internal/pkg/sdk3rd/qiniu/client.go +++ b/internal/pkg/sdk3rd/qiniu/client.go @@ -1,4 +1,4 @@ -package qiniusdk +package qiniu import ( "context" diff --git a/internal/pkg/sdk3rd/qiniu/models.go b/internal/pkg/sdk3rd/qiniu/models.go index 1f643c35..1c832caf 100644 --- a/internal/pkg/sdk3rd/qiniu/models.go +++ b/internal/pkg/sdk3rd/qiniu/models.go @@ -1,4 +1,4 @@ -package qiniusdk +package qiniu type BaseResponse struct { Code *int `json:"code,omitempty"` diff --git a/internal/pkg/sdk3rd/rainyun/api.go b/internal/pkg/sdk3rd/rainyun/api.go index d4a9135e..cdb0eccb 100644 --- a/internal/pkg/sdk3rd/rainyun/api.go +++ b/internal/pkg/sdk3rd/rainyun/api.go @@ -1,4 +1,4 @@ -package rainyunsdk +package rainyun import ( "fmt" diff --git a/internal/pkg/sdk3rd/rainyun/client.go b/internal/pkg/sdk3rd/rainyun/client.go index e710128b..0ead39cd 100644 --- a/internal/pkg/sdk3rd/rainyun/client.go +++ b/internal/pkg/sdk3rd/rainyun/client.go @@ -1,4 +1,4 @@ -package rainyunsdk +package rainyun import ( "encoding/json" @@ -67,7 +67,7 @@ func (c *Client) sendRequestWithResult(method string, path string, params interf if err := json.Unmarshal(resp.Body(), &result); err != nil { return fmt.Errorf("rainyun api error: failed to parse response: %w", err) } else if errcode := result.GetCode(); errcode/100 != 2 { - return fmt.Errorf("rainyun api error: %d - %s", errcode, result.GetMessage()) + return fmt.Errorf("rainyun api error: code='%d', message='%s'", errcode, result.GetMessage()) } return nil diff --git a/internal/pkg/sdk3rd/rainyun/models.go b/internal/pkg/sdk3rd/rainyun/models.go index bcc44963..e6ef4671 100644 --- a/internal/pkg/sdk3rd/rainyun/models.go +++ b/internal/pkg/sdk3rd/rainyun/models.go @@ -1,4 +1,4 @@ -package rainyunsdk +package rainyun type BaseResponse interface { GetCode() int32 diff --git a/internal/pkg/sdk3rd/safeline/api.go b/internal/pkg/sdk3rd/safeline/api.go index 48ce1def..52024370 100644 --- a/internal/pkg/sdk3rd/safeline/api.go +++ b/internal/pkg/sdk3rd/safeline/api.go @@ -1,4 +1,4 @@ -package safelinesdk +package safeline func (c *Client) UpdateCertificate(req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) { resp := &UpdateCertificateResponse{} diff --git a/internal/pkg/sdk3rd/safeline/client.go b/internal/pkg/sdk3rd/safeline/client.go index c56e3485..4705d74e 100644 --- a/internal/pkg/sdk3rd/safeline/client.go +++ b/internal/pkg/sdk3rd/safeline/client.go @@ -1,4 +1,4 @@ -package safelinesdk +package safeline import ( "crypto/tls" @@ -66,9 +66,9 @@ func (c *Client) sendRequestWithResult(path string, params interface{}, result B return fmt.Errorf("safeline api error: failed to parse response: %w", err) } else if errcode := result.GetErrCode(); errcode != nil && *errcode != "" { if result.GetErrMsg() == nil { - return fmt.Errorf("safeline api error: %s", *errcode) + return fmt.Errorf("safeline api error: code='%s'", *errcode) } else { - return fmt.Errorf("safeline api error: %s - %s", *errcode, *result.GetErrMsg()) + return fmt.Errorf("safeline api error: code='%s', message='%s'", *errcode, *result.GetErrMsg()) } } diff --git a/internal/pkg/sdk3rd/safeline/models.go b/internal/pkg/sdk3rd/safeline/models.go index 9fbfb7c9..f37b4348 100644 --- a/internal/pkg/sdk3rd/safeline/models.go +++ b/internal/pkg/sdk3rd/safeline/models.go @@ -1,4 +1,4 @@ -package safelinesdk +package safeline type BaseResponse interface { GetErrCode() *string diff --git a/internal/pkg/sdk3rd/upyun/console/client.go b/internal/pkg/sdk3rd/upyun/console/client.go index 56e7a86e..90d74bc3 100644 --- a/internal/pkg/sdk3rd/upyun/console/client.go +++ b/internal/pkg/sdk3rd/upyun/console/client.go @@ -90,7 +90,7 @@ func (c *Client) sendRequestWithResult(method string, path string, params interf } else if tdata := tresp.GetData(); tdata == nil { return fmt.Errorf("upyun api error: empty data") } else if errcode := tdata.GetErrorCode(); errcode > 0 { - return fmt.Errorf("upyun api error: %d - %s", errcode, tdata.GetErrorMessage()) + return fmt.Errorf("upyun api error: code='%d', message='%s'", errcode, tdata.GetErrorMessage()) } return nil diff --git a/internal/pkg/utils/cert/extractor.go b/internal/pkg/utils/cert/extractor.go index 110f4772..94d0a8da 100644 --- a/internal/pkg/utils/cert/extractor.go +++ b/internal/pkg/utils/cert/extractor.go @@ -12,9 +12,9 @@ import ( // // 出参: // - serverCertPEM: 服务器证书的 PEM 内容。 -// - interCertPEM: 中间证书的 PEM 内容。 +// - intermediaCertPEM: 中间证书的 PEM 内容。 // - err: 错误。 -func ExtractCertificatesFromPEM(certPEM string) (serverCertPEM string, interCertPEM string, err error) { +func ExtractCertificatesFromPEM(certPEM string) (serverCertPEM string, intermediaCertPEM string, err error) { pemBlocks := make([]*pem.Block, 0) pemData := []byte(certPEM) for { @@ -28,7 +28,7 @@ func ExtractCertificatesFromPEM(certPEM string) (serverCertPEM string, interCert } serverCertPEM = "" - interCertPEM = "" + intermediaCertPEM = "" if len(pemBlocks) == 0 { return "", "", errors.New("failed to decode PEM block") @@ -40,9 +40,9 @@ func ExtractCertificatesFromPEM(certPEM string) (serverCertPEM string, interCert if len(pemBlocks) > 1 { for i := 1; i < len(pemBlocks); i++ { - interCertPEM += string(pem.EncodeToMemory(pemBlocks[i])) + intermediaCertPEM += string(pem.EncodeToMemory(pemBlocks[i])) } } - return serverCertPEM, interCertPEM, nil + return serverCertPEM, intermediaCertPEM, nil } diff --git a/migrations/1747314000_upgrade.go b/migrations/1747314000_upgrade.go new file mode 100644 index 00000000..19a25bb2 --- /dev/null +++ b/migrations/1747314000_upgrade.go @@ -0,0 +1,44 @@ +package migrations + +import ( + "github.com/pocketbase/pocketbase/core" + m "github.com/pocketbase/pocketbase/migrations" +) + +func init() { + m.Register(func(app core.App) error { + // migrate data + { + accesses, err := app.FindAllRecords("access") + if err != nil { + return err + } + + for _, access := range accesses { + changed := false + + if access.GetString("provider") == "goedge" { + config := make(map[string]any) + if err := access.UnmarshalJSONField("config", &config); err != nil { + return err + } + + config["apiRole"] = "user" + access.Set("config", config) + changed = true + } + + if changed { + err = app.Save(access) + if err != nil { + return err + } + } + } + } + + return nil + }, func(app core.App) error { + return nil + }) +} diff --git a/ui/package-lock.json b/ui/package-lock.json index 03ad4927..44a4d2b6 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -10,6 +10,13 @@ "dependencies": { "@ant-design/icons": "^6.0.0", "@ant-design/pro-components": "^2.8.7", + "@codemirror/lang-json": "^6.0.1", + "@codemirror/lang-yaml": "^6.1.2", + "@codemirror/language": "^6.11.0", + "@codemirror/legacy-modes": "^6.5.1", + "@uiw/codemirror-extensions-basic-setup": "^4.23.12", + "@uiw/codemirror-theme-vscode": "^4.23.12", + "@uiw/react-codemirror": "^4.23.12", "ahooks": "^3.8.4", "antd": "^5.25.1", "antd-zod": "^6.1.0", @@ -2107,6 +2114,121 @@ "react": ">=16.12.0" } }, + "node_modules/@codemirror/autocomplete": { + "version": "6.18.6", + "resolved": "https://registry.npmmirror.com/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz", + "integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.8.1", + "resolved": "https://registry.npmmirror.com/@codemirror/commands/-/commands-6.8.1.tgz", + "integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-json": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/@codemirror/lang-json/-/lang-json-6.0.1.tgz", + "integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-yaml": { + "version": "6.1.2", + "resolved": "https://registry.npmmirror.com/@codemirror/lang-yaml/-/lang-yaml-6.1.2.tgz", + "integrity": "sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.2.0", + "@lezer/lr": "^1.0.0", + "@lezer/yaml": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.11.0", + "resolved": "https://registry.npmmirror.com/@codemirror/language/-/language-6.11.0.tgz", + "integrity": "sha512-A7+f++LodNNc1wGgoRDTt78cOwWm9KVezApgjOMp1W4hM0898nsqBXwF+sbePE7ZRcjN7Sa1Z5m2oN27XkmEjQ==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/legacy-modes": { + "version": "6.5.1", + "resolved": "https://registry.npmmirror.com/@codemirror/legacy-modes/-/legacy-modes-6.5.1.tgz", + "integrity": "sha512-DJYQQ00N1/KdESpZV7jg9hafof/iBNp9h7TYo1SLMk86TWl9uDsVdho2dzd81K+v4retmK6mdC7WpuOQDytQqw==", + "dependencies": { + "@codemirror/language": "^6.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.8.5", + "resolved": "https://registry.npmmirror.com/@codemirror/lint/-/lint-6.8.5.tgz", + "integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.35.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.5.10", + "resolved": "https://registry.npmmirror.com/@codemirror/search/-/search-6.5.10.tgz", + "integrity": "sha512-RMdPdmsrUf53pb2VwflKGHEe1XVM07hI7vV2ntgw1dmqhimpatSJKva4VA9h4TLUDOD4EIF02201oZurpnEFsg==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.2", + "resolved": "https://registry.npmmirror.com/@codemirror/state/-/state-6.5.2.tgz", + "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.2", + "resolved": "https://registry.npmmirror.com/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz", + "integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.36.8", + "resolved": "https://registry.npmmirror.com/@codemirror/view/-/view-6.36.8.tgz", + "integrity": "sha512-yoRo4f+FdnD01fFt4XpfpMCcCAo9QvZOtbrXExn4SqzH32YC6LgzqxfLZw/r6Ge65xyY03mK/UfUqrVw1gFiFg==", + "dependencies": { + "@codemirror/state": "^6.5.0", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@ctrl/tinycolor": { "version": "3.6.1", "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", @@ -2839,6 +2961,52 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lezer/common": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/@lezer/common/-/common-1.2.3.tgz", + "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/@lezer/highlight/-/highlight-1.2.1.tgz", + "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/json": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@lezer/json/-/json-1.0.3.tgz", + "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.2", + "resolved": "https://registry.npmmirror.com/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/yaml": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@lezer/yaml/-/yaml-1.0.3.tgz", + "integrity": "sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.4.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3628,6 +3796,86 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@uiw/codemirror-extensions-basic-setup": { + "version": "4.23.12", + "resolved": "https://registry.npmmirror.com/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.23.12.tgz", + "integrity": "sha512-l9vuiXOTFDBetYrRLDmz3jDxQHDsrVAZ2Y6dVfmrqi2AsulsDu+y7csW0JsvaMqo79rYkaIZg8yeqmDgMb7VyQ==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/autocomplete": ">=6.0.0", + "@codemirror/commands": ">=6.0.0", + "@codemirror/language": ">=6.0.0", + "@codemirror/lint": ">=6.0.0", + "@codemirror/search": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, + "node_modules/@uiw/codemirror-theme-vscode": { + "version": "4.23.12", + "resolved": "https://registry.npmmirror.com/@uiw/codemirror-theme-vscode/-/codemirror-theme-vscode-4.23.12.tgz", + "integrity": "sha512-ePBaUQiixrpmSoZJWCGXUStKmcM8G0VBv3UqwPR+kNGBjqDife76Gbhv77izSeEI3zRPzL+683BOdclkvWnsMg==", + "dependencies": { + "@uiw/codemirror-themes": "4.23.12" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-themes": { + "version": "4.23.12", + "resolved": "https://registry.npmmirror.com/@uiw/codemirror-themes/-/codemirror-themes-4.23.12.tgz", + "integrity": "sha512-8etEByfS9yttFZW0rcWhdZc7/JXJKRWlU5lHmJCI3GydZNGCzydNA+HtK9nWKpJUndVc58Q2sqSC5OIcwq8y6A==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/language": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, + "node_modules/@uiw/react-codemirror": { + "version": "4.23.12", + "resolved": "https://registry.npmmirror.com/@uiw/react-codemirror/-/react-codemirror-4.23.12.tgz", + "integrity": "sha512-yseqWdzoAAGAW7i/NiU8YrfSLVOEBjQvSx1KpDTFVV/nn0AlAZoDVTIPEBgdXrPlVUQoCrwgpEaj3uZCklk9QA==", + "dependencies": { + "@babel/runtime": "^7.18.6", + "@codemirror/commands": "^6.1.0", + "@codemirror/state": "^6.1.1", + "@codemirror/theme-one-dark": "^6.0.0", + "@uiw/codemirror-extensions-basic-setup": "4.23.12", + "codemirror": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.11.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/theme-one-dark": ">=6.0.0", + "@codemirror/view": ">=6.0.0", + "codemirror": ">=6.0.0", + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@umijs/route-utils": { "version": "4.0.1", "resolved": "https://registry.npmmirror.com/@umijs/route-utils/-/route-utils-4.0.1.tgz", @@ -4337,6 +4585,20 @@ "node": ">=6" } }, + "node_modules/codemirror": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/codemirror/-/codemirror-6.0.1.tgz", + "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz", @@ -4403,6 +4665,11 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" + }, "node_modules/cron-parser": { "version": "5.2.0", "resolved": "https://registry.npmmirror.com/cron-parser/-/cron-parser-5.2.0.tgz", @@ -8689,6 +8956,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==" + }, "node_modules/stylis": { "version": "4.3.4", "resolved": "https://registry.npmmirror.com/stylis/-/stylis-4.3.4.tgz", @@ -9363,6 +9635,11 @@ "node": ">=0.10.0" } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmmirror.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" + }, "node_modules/warning": { "version": "4.0.3", "resolved": "https://registry.npmmirror.com/warning/-/warning-4.0.3.tgz", diff --git a/ui/package.json b/ui/package.json index 194ffb31..bec7eda9 100644 --- a/ui/package.json +++ b/ui/package.json @@ -12,6 +12,13 @@ "dependencies": { "@ant-design/icons": "^6.0.0", "@ant-design/pro-components": "^2.8.7", + "@codemirror/lang-json": "^6.0.1", + "@codemirror/lang-yaml": "^6.1.2", + "@codemirror/language": "^6.11.0", + "@codemirror/legacy-modes": "^6.5.1", + "@uiw/codemirror-extensions-basic-setup": "^4.23.12", + "@uiw/codemirror-theme-vscode": "^4.23.12", + "@uiw/react-codemirror": "^4.23.12", "ahooks": "^3.8.4", "antd": "^5.25.1", "antd-zod": "^6.1.0", diff --git a/ui/public/imgs/providers/netcup.png b/ui/public/imgs/providers/netcup.png new file mode 100644 index 00000000..2f56ce11 Binary files /dev/null and b/ui/public/imgs/providers/netcup.png differ diff --git a/ui/public/imgs/providers/netlify.png b/ui/public/imgs/providers/netlify.png new file mode 100644 index 00000000..a04eeeba Binary files /dev/null and b/ui/public/imgs/providers/netlify.png differ diff --git a/ui/src/components/CodeInput.tsx b/ui/src/components/CodeInput.tsx new file mode 100644 index 00000000..a784af46 --- /dev/null +++ b/ui/src/components/CodeInput.tsx @@ -0,0 +1,97 @@ +import { useMemo, useRef } from "react"; +import { json } from "@codemirror/lang-json"; +import { yaml } from "@codemirror/lang-yaml"; +import { StreamLanguage } from "@codemirror/language"; +import { powerShell } from "@codemirror/legacy-modes/mode/powershell"; +import { shell } from "@codemirror/legacy-modes/mode/shell"; +import { basicSetup } from "@uiw/codemirror-extensions-basic-setup"; +import { vscodeDark, vscodeLight } from "@uiw/codemirror-theme-vscode"; +import CodeMirror, { type ReactCodeMirrorProps, type ReactCodeMirrorRef } from "@uiw/react-codemirror"; +import { useFocusWithin } from "ahooks"; +import { theme } from "antd"; + +import { useBrowserTheme } from "@/hooks"; +import { mergeCls } from "@/utils/css"; + +export interface CodeInputProps extends Omit { + disabled?: boolean; + language?: string | string[]; +} + +const CodeInput = ({ className, style, disabled, language, ...props }: CodeInputProps) => { + const { token: themeToken } = theme.useToken(); + + const { theme: browserTheme } = useBrowserTheme(); + + const cmRef = useRef(null); + const isFocusWithin = useFocusWithin(cmRef.current?.editor); + + const cmTheme = useMemo(() => { + if (browserTheme === "dark") { + return vscodeDark; + } + return vscodeLight; + }, [browserTheme]); + + const cmExtensions = useMemo(() => { + const temp: NonNullable = [ + basicSetup({ + foldGutter: false, + dropCursor: false, + allowMultipleSelections: false, + indentOnInput: false, + }), + ]; + + const langs = Array.isArray(language) ? language : [language]; + langs.forEach((lang) => { + switch (lang) { + case "shell": + temp.push(StreamLanguage.define(shell)); + break; + case "json": + temp.push(json()); + break; + case "powershell": + temp.push(StreamLanguage.define(powerShell)); + break; + case "yaml": + temp.push(yaml()); + break; + } + }); + + return temp; + }, [language]); + + return ( +
+ +
+ ); +}; + +export default CodeInput; diff --git a/ui/src/components/TextFileInput.tsx b/ui/src/components/TextFileInput.tsx new file mode 100644 index 00000000..bae83c56 --- /dev/null +++ b/ui/src/components/TextFileInput.tsx @@ -0,0 +1,51 @@ +import { type ChangeEvent, useRef } from "react"; +import { useTranslation } from "react-i18next"; +import { UploadOutlined as UploadOutlinedIcon } from "@ant-design/icons"; +import { Button, type ButtonProps, Input, Space, type UploadProps } from "antd"; +import { type TextAreaProps } from "antd/es/input/TextArea"; + +import { mergeCls } from "@/utils/css"; +import { readFileContent } from "@/utils/file"; + +export interface TextFileInputProps extends Omit { + accept?: UploadProps["accept"]; + uploadButtonProps?: Omit; + uploadText?: string; + onChange?: (value: string) => void; +} + +const TextFileInput = ({ className, style, accept, disabled, readOnly, uploadText, uploadButtonProps, onChange, ...props }: TextFileInputProps) => { + const { t } = useTranslation(); + + const fileInputRef = useRef(null); + + const handleButtonClick = () => { + if (fileInputRef.current) { + fileInputRef.current.click(); + } + }; + + const handleFileChange = async (e: ChangeEvent) => { + const { files } = e.target as HTMLInputElement; + if (files?.length) { + const value = await readFileContent(files[0]); + onChange?.(value); + } + }; + + return ( + + onChange?.(e.target.value)} /> + {!readOnly && ( + <> + + + + )} + + ); +}; + +export default TextFileInput; diff --git a/ui/src/components/access/AccessForm.tsx b/ui/src/components/access/AccessForm.tsx index 02a71854..0dd2828a 100644 --- a/ui/src/components/access/AccessForm.tsx +++ b/ui/src/components/access/AccessForm.tsx @@ -46,6 +46,8 @@ import AccessFormMattermostConfig from "./AccessFormMattermostConfig"; import AccessFormNamecheapConfig from "./AccessFormNamecheapConfig"; import AccessFormNameDotComConfig from "./AccessFormNameDotComConfig"; import AccessFormNameSiloConfig from "./AccessFormNameSiloConfig"; +import AccessFormNetcupConfig from "./AccessFormNetcupConfig"; +import AccessFormNetlifyConfig from "./AccessFormNetlifyConfig"; import AccessFormNS1Config from "./AccessFormNS1Config"; import AccessFormPorkbunConfig from "./AccessFormPorkbunConfig"; import AccessFormPowerDNSConfig from "./AccessFormPowerDNSConfig"; @@ -242,6 +244,10 @@ const AccessForm = forwardRef(({ className, return ; case ACCESS_PROVIDERS.NAMESILO: return ; + case ACCESS_PROVIDERS.NETCUP: + return ; + case ACCESS_PROVIDERS.NETLIFY: + return ; case ACCESS_PROVIDERS.NS1: return ; case ACCESS_PROVIDERS.PORKBUN: diff --git a/ui/src/components/access/AccessFormGoEdgeConfig.tsx b/ui/src/components/access/AccessFormGoEdgeConfig.tsx index eb4140f4..ced9b09a 100644 --- a/ui/src/components/access/AccessFormGoEdgeConfig.tsx +++ b/ui/src/components/access/AccessFormGoEdgeConfig.tsx @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input, Switch } from "antd"; +import { Form, type FormInstance, Input, Radio, Switch } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; @@ -18,6 +18,7 @@ export type AccessFormGoEdgeConfigProps = { const initFormModel = (): AccessFormGoEdgeConfigFieldValues => { return { apiUrl: "http://:7788/", + apiRole: "user", accessKeyId: "", accessKey: "", }; @@ -28,6 +29,9 @@ const AccessFormGoEdgeConfig = ({ form: formInst, formName, disabled, initialVal const formSchema = z.object({ apiUrl: z.string().url(t("common.errmsg.url_invalid")), + role: z.union([z.literal("user"), z.literal("admin")], { + message: t("access.form.goedge_api_role.placeholder"), + }), accessKeyId: z .string() .min(1, t("access.form.goedge_access_key_id.placeholder")) @@ -59,6 +63,10 @@ const AccessFormGoEdgeConfig = ({ form: formInst, formName, disabled, initialVal + + ({ label: t(`access.form.goedge_api_role.option.${s}.label`), value: s }))} /> + + ; @@ -34,24 +32,6 @@ const AccessFormKubernetesConfig = ({ form: formInst, formName, disabled, initia }); const formRule = createSchemaFieldRule(formSchema); - const fieldKubeConfig = Form.useWatch("kubeConfig", formInst); - const [fieldKubeFileList, setFieldKubeFileList] = useState([]); - useEffect(() => { - setFieldKubeFileList(initialValues?.kubeConfig?.trim() ? [{ uid: "-1", name: "kubeconfig", status: "done" }] : []); - }, [initialValues?.kubeConfig]); - - const handleKubeFileChange: UploadProps["onChange"] = async ({ file }) => { - if (file && file.status !== "removed") { - formInst.setFieldValue("kubeConfig", await readFileContent(file.originFileObj ?? (file as unknown as File))); - setFieldKubeFileList([file]); - } else { - formInst.setFieldValue("kubeConfig", ""); - setFieldKubeFileList([]); - } - - onValuesChange?.(formInst.getFieldsValue(true)); - }; - const handleFormChange = (_: unknown, values: z.infer) => { onValuesChange?.(values); }; @@ -65,16 +45,13 @@ const AccessFormKubernetesConfig = ({ form: formInst, formName, disabled, initia name={formName} onValuesChange={handleFormChange} > - - } > - false} fileList={fieldKubeFileList} maxCount={1} onChange={handleKubeFileChange}> - - + ); diff --git a/ui/src/components/access/AccessFormNetcupConfig.tsx b/ui/src/components/access/AccessFormNetcupConfig.tsx new file mode 100644 index 00000000..c5d4bfc6 --- /dev/null +++ b/ui/src/components/access/AccessFormNetcupConfig.tsx @@ -0,0 +1,79 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type AccessConfigForNetcup } from "@/domain/access"; + +type AccessFormNetcupConfigFieldValues = Nullish; + +export type AccessFormNetcupConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: AccessFormNetcupConfigFieldValues; + onValuesChange?: (values: AccessFormNetcupConfigFieldValues) => void; +}; + +const initFormModel = (): AccessFormNetcupConfigFieldValues => { + return { + customerNumber: "", + apiKey: "", + apiPassword: "", + }; +}; + +const AccessFormNetcupConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormNetcupConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + customerNumber: z.string().nonempty(t("access.form.netcup_customer_number.placeholder")).trim(), + apiKey: z.string().nonempty(t("access.form.netcup_api_key.placeholder")).trim(), + apiPassword: z.string().nonempty(t("access.form.netcup_api_password.placeholder")).trim(), + }); + const formRule = createSchemaFieldRule(formSchema); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ } + > + + + + } + > + + + + } + > + + +
+ ); +}; + +export default AccessFormNetcupConfig; diff --git a/ui/src/components/access/AccessFormNetlifyConfig.tsx b/ui/src/components/access/AccessFormNetlifyConfig.tsx new file mode 100644 index 00000000..7fa4c8ae --- /dev/null +++ b/ui/src/components/access/AccessFormNetlifyConfig.tsx @@ -0,0 +1,57 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type AccessConfigForNetlify } from "@/domain/access"; + +type AccessFormNetlifyConfigFieldValues = Nullish; + +export type AccessFormNetlifyConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: AccessFormNetlifyConfigFieldValues; + onValuesChange?: (values: AccessFormNetlifyConfigFieldValues) => void; +}; + +const initFormModel = (): AccessFormNetlifyConfigFieldValues => { + return { + apiToken: "", + }; +}; + +const AccessFormNetlifyConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormNetlifyConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + apiToken: z.string().nonempty(t("access.form.netlify_api_token.placeholder")).trim(), + }); + const formRule = createSchemaFieldRule(formSchema); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ } + > + + +
+ ); +}; + +export default AccessFormNetlifyConfig; diff --git a/ui/src/components/access/AccessFormSSHConfig.tsx b/ui/src/components/access/AccessFormSSHConfig.tsx index ebaeba90..db1790a4 100644 --- a/ui/src/components/access/AccessFormSSHConfig.tsx +++ b/ui/src/components/access/AccessFormSSHConfig.tsx @@ -1,12 +1,10 @@ -import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { UploadOutlined as UploadOutlinedIcon } from "@ant-design/icons"; -import { Button, Form, type FormInstance, Input, InputNumber, Upload, type UploadFile, type UploadProps } from "antd"; +import { Form, type FormInstance, Input, InputNumber } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; +import TextFileInput from "@/components/TextFileInput"; import { type AccessConfigForSSH } from "@/domain/access"; -import { readFileContent } from "@/utils/file"; import { validDomainName, validIPv4Address, validIPv6Address, validPortNumber } from "@/utils/validators"; type AccessFormSSHConfigFieldValues = Nullish; @@ -59,24 +57,6 @@ const AccessFormSSHConfig = ({ form: formInst, formName, disabled, initialValues }); const formRule = createSchemaFieldRule(formSchema); - const fieldKey = Form.useWatch("key", formInst); - const [fieldKeyFileList, setFieldKeyFileList] = useState([]); - useEffect(() => { - setFieldKeyFileList(initialValues?.key?.trim() ? [{ uid: "-1", name: "sshkey", status: "done" }] : []); - }, [initialValues?.key]); - - const handleKeyFileChange: UploadProps["onChange"] = async ({ file }) => { - if (file && file.status !== "removed") { - formInst.setFieldValue("key", await readFileContent(file.originFileObj ?? (file as unknown as File))); - setFieldKeyFileList([file]); - } else { - formInst.setFieldValue("key", ""); - setFieldKeyFileList([]); - } - - onValuesChange?.(formInst.getFieldsValue(true)); - }; - const handleFormChange = (_: unknown, values: z.infer) => { onValuesChange?.(values); }; @@ -104,48 +84,36 @@ const AccessFormSSHConfig = ({ form: formInst, formName, disabled, initialValues -
-
- - - -
+ + + -
- } - > - - -
-
+ } + > + + -
-
- - - }> - false} fileList={fieldKeyFileList} maxCount={1} onChange={handleKeyFileChange}> - - - -
+ } + > + + -
- } - > - - -
-
+ } + > + + ); }; diff --git a/ui/src/components/access/AccessFormWebhookConfig.tsx b/ui/src/components/access/AccessFormWebhookConfig.tsx index 0108d7b3..69286aa8 100644 --- a/ui/src/components/access/AccessFormWebhookConfig.tsx +++ b/ui/src/components/access/AccessFormWebhookConfig.tsx @@ -4,6 +4,7 @@ import { Alert, Button, Dropdown, Form, type FormInstance, Input, Select, Switch import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; +import CodeInput from "@/components/CodeInput"; import Show from "@/components/Show"; import { type AccessConfigForWebhook } from "@/domain/access"; @@ -105,8 +106,8 @@ const AccessFormWebhookConfig = ({ form: formInst, formName, disabled, initialVa formInst.setFieldValue("headers", value); }; - const handleWebhookDataForDeploymentBlur = (e: React.FocusEvent) => { - const value = e.target.value; + const handleWebhookDataForDeploymentBlur = () => { + const value = formInst.getFieldValue("defaultDataForDeployment"); try { const json = JSON.stringify(JSON.parse(value), null, 2); formInst.setFieldValue("defaultDataForDeployment", json); @@ -115,8 +116,8 @@ const AccessFormWebhookConfig = ({ form: formInst, formName, disabled, initialVa } }; - const handleWebhookDataForNotificationBlur = (e: React.FocusEvent) => { - const value = e.target.value; + const handleWebhookDataForNotificationBlur = () => { + const value = formInst.getFieldValue("defaultDataForNotification"); try { const json = JSON.stringify(JSON.parse(value), null, 2); formInst.setFieldValue("defaultDataForNotification", json); @@ -279,7 +280,7 @@ const AccessFormWebhookConfig = ({ form: formInst, formName, disabled, initialVa rules={[formRule]} tooltip={} > - +
@@ -297,9 +298,11 @@ const AccessFormWebhookConfig = ({ form: formInst, formName, disabled, initialVa - @@ -338,9 +341,11 @@ const AccessFormWebhookConfig = ({ form: formInst, formName, disabled, initialVa - diff --git a/ui/src/components/certificate/CertificateDetail.tsx b/ui/src/components/certificate/CertificateDetail.tsx index 6c842a36..1023bf16 100644 --- a/ui/src/components/certificate/CertificateDetail.tsx +++ b/ui/src/components/certificate/CertificateDetail.tsx @@ -75,7 +75,7 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => { - + @@ -92,7 +92,7 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => { - + diff --git a/ui/src/components/notification/NotifyTemplate.tsx b/ui/src/components/notification/NotifyTemplate.tsx index b7cf4e6d..9921cda5 100644 --- a/ui/src/components/notification/NotifyTemplate.tsx +++ b/ui/src/components/notification/NotifyTemplate.tsx @@ -108,7 +108,7 @@ const NotifyTemplateForm = ({ className, style }: NotifyTemplateFormProps) => { rules={[formRule]} > diff --git a/ui/src/components/provider/AccessProviderPicker.tsx b/ui/src/components/provider/AccessProviderPicker.tsx index 86563008..002d2519 100644 --- a/ui/src/components/provider/AccessProviderPicker.tsx +++ b/ui/src/components/provider/AccessProviderPicker.tsx @@ -4,6 +4,7 @@ import { Avatar, Card, Col, Empty, Flex, Input, type InputRef, Row, Tag, Typogra import Show from "@/components/Show"; import { ACCESS_USAGES, type AccessProvider, type AccessUsageType, accessProvidersMap } from "@/domain/provider"; +import { mergeCls } from "@/utils/css"; export type AccessProviderPickerProps = { className?: string; @@ -73,17 +74,23 @@ const AccessProviderPicker = ({ className, style, autoFocus, filter, placeholder return ( { + if (provider.builtin) { + return; + } + handleProviderTypeSelect(provider.type); }} >
- {t(provider.name)} + + {t(provider.name)} +
{t("access.props.provider.builtin")} diff --git a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx index f23e5582..9e4ade18 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx @@ -57,6 +57,7 @@ import DeployNodeConfigFormJDCloudLiveConfig from "./DeployNodeConfigFormJDCloud import DeployNodeConfigFormJDCloudVODConfig from "./DeployNodeConfigFormJDCloudVODConfig"; import DeployNodeConfigFormKubernetesSecretConfig from "./DeployNodeConfigFormKubernetesSecretConfig"; import DeployNodeConfigFormLocalConfig from "./DeployNodeConfigFormLocalConfig"; +import DeployNodeConfigFormNetlifySiteConfig from "./DeployNodeConfigFormNetlifySiteConfig"; import DeployNodeConfigFormProxmoxVEConfig from "./DeployNodeConfigFormProxmoxVEConfig"; import DeployNodeConfigFormQiniuCDNConfig from "./DeployNodeConfigFormQiniuCDNConfig"; import DeployNodeConfigFormQiniuKodoConfig from "./DeployNodeConfigFormQiniuKodoConfig"; @@ -260,6 +261,8 @@ const DeployNodeConfigForm = forwardRef; case DEPLOYMENT_PROVIDERS.LOCAL: return ; + case DEPLOYMENT_PROVIDERS.NETLIFY_SITE: + return ; case DEPLOYMENT_PROVIDERS.PROXMOXVE: return ; case DEPLOYMENT_PROVIDERS.QINIU_CDN: diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAWSACMConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAWSACMConfig.tsx index 60b49f54..f0964493 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormAWSACMConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormAWSACMConfig.tsx @@ -5,6 +5,7 @@ import { z } from "zod"; type DeployNodeConfigFormAWSACMConfigFieldValues = Nullish<{ region: string; + certificateArn?: string; }>; export type DeployNodeConfigFormAWSACMConfigProps = { @@ -27,6 +28,7 @@ const DeployNodeConfigFormAWSACMConfig = ({ form: formInst, formName, disabled, .string({ message: t("workflow_node.deploy.form.aws_acm_region.placeholder") }) .nonempty(t("workflow_node.deploy.form.aws_acm_region.placeholder")) .trim(), + certificateArn: z.string({ message: t("workflow_node.deploy.form.aws_acm_certificate_arn.placeholder") }).nullish(), }); const formRule = createSchemaFieldRule(formSchema); @@ -51,6 +53,15 @@ const DeployNodeConfigFormAWSACMConfig = ({ form: formInst, formName, disabled, > + + } + > + + ); }; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAzureKeyVaultConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAzureKeyVaultConfig.tsx index 6826d277..2a54bb99 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormAzureKeyVaultConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormAzureKeyVaultConfig.tsx @@ -37,7 +37,7 @@ const DeployNodeConfigFormAzureKeyVaultConfig = ({ certificateName: z .string({ message: t("workflow_node.deploy.form.azure_keyvault_certificate_name.placeholder") }) .nullish() - .refine((v) =>{ + .refine((v) => { if (!v) return true; return /^[a-zA-Z0-9-]{1,127}$/.test(v); }, t("workflow_node.deploy.form.azure_keyvault_certificate_name.errmsg.invalid")), diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormLocalConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormLocalConfig.tsx index cf34f94b..75853eb7 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormLocalConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormLocalConfig.tsx @@ -4,20 +4,23 @@ import { Alert, Button, Dropdown, Form, type FormInstance, Input, Select } from import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; +import CodeInput from "@/components/CodeInput"; import Show from "@/components/Show"; import { CERTIFICATE_FORMATS } from "@/domain/certificate"; type DeployNodeConfigFormLocalConfigFieldValues = Nullish<{ format: string; certPath: string; - keyPath?: string | null; - pfxPassword?: string | null; - jksAlias?: string | null; - jksKeypass?: string | null; - jksStorepass?: string | null; - shellEnv?: string | null; - preCommand?: string | null; - postCommand?: string | null; + certPathForServerOnly?: string; + certPathForIntermediaOnly?: string; + keyPath?: string; + pfxPassword?: string; + jksAlias?: string; + jksKeypass?: string; + jksStorepass?: string; + shellEnv?: string; + preCommand?: string; + postCommand?: string; }>; export type DeployNodeConfigFormLocalConfigProps = { @@ -49,6 +52,8 @@ export const initPresetScript = ( key: "sh_backup_files" | "ps_backup_files" | "sh_reload_nginx" | "ps_binding_iis" | "ps_binding_netsh" | "ps_binding_rdp", params?: { certPath?: string; + certPathForServerOnly?: string; + certPathForIntermediaOnly?: string; keyPath?: string; pfxPassword?: string; jksAlias?: string; @@ -74,19 +79,22 @@ if (Test-Path -Path "${params?.keyPath || ""}" -PathType Leaf) { `.trim(); case "sh_reload_nginx": - return `sudo service nginx reload`; + return `# *** 需要 root 权限 *** + +sudo service nginx reload + `.trim(); case "ps_binding_iis": - return `# 需要管理员权限 + return `# *** 需要管理员权限 *** + # 请将以下变量替换为实际值 -$pfxPath = "${params?.certPath || ""}" # PFX 文件路径 -$pfxPassword = "${params?.pfxPassword || ""}" # PFX 密码 +$pfxPath = "${params?.certPath || ""}" # PFX 文件路径(与表单中保持一致) +$pfxPassword = "${params?.pfxPassword || ""}" # PFX 密码(与表单中保持一致) $siteName = "" # IIS 网站名称 $domain = "" # 域名 $ipaddr = "" # 绑定 IP,“*”表示所有 IP 绑定 $port = "" # 绑定端口 - # 导入证书到本地计算机的个人存储区 $cert = Import-PfxCertificate -FilePath "$pfxPath" -CertStoreLocation Cert:\\LocalMachine\\My -Password (ConvertTo-SecureString -String "$pfxPassword" -AsPlainText -Force) -Exportable # 获取 Thumbprint @@ -108,16 +116,16 @@ Remove-Item -Path "$pfxPath" -Force `.trim(); case "ps_binding_netsh": - return `# 需要管理员权限 + return `# *** 需要管理员权限 *** + # 请将以下变量替换为实际值 -$pfxPath = "${params?.certPath || ""}" # PFX 文件路径 -$pfxPassword = "${params?.pfxPassword || ""}" # PFX 密码 -$ipaddr = "" # 绑定 IP,“0.0.0.0”表示所有 IP 绑定,可填入域名。 +$pfxPath = "${params?.certPath || ""}" # PFX 文件路径(与表单中保持一致) +$pfxPassword = "${params?.pfxPassword || ""}" # PFX 密码(与表单中保持一致) +$ipaddr = "" # 绑定 IP,“0.0.0.0”表示所有 IP 绑定,可填入域名 $port = "" # 绑定端口 -$addr = $ipaddr + ":" + $port - # 导入证书到本地计算机的个人存储区 +$addr = $ipaddr + ":" + $port $cert = Import-PfxCertificate -FilePath "$pfxPath" -CertStoreLocation Cert:\\LocalMachine\\My -Password (ConvertTo-SecureString -String "$pfxPassword" -AsPlainText -Force) -Exportable # 获取 Thumbprint $thumbprint = $cert.Thumbprint @@ -131,10 +139,11 @@ Remove-Item -Path "$pfxPath" -Force `.trim(); case "ps_binding_rdp": - return `# 需要管理员权限 + return `# *** 需要管理员权限 *** + # 请将以下变量替换为实际值 -$pfxPath = "${params?.certPath || ""}" # PFX 文件路径 -$pfxPassword = "${params?.pfxPassword || ""}" # PFX 密码 +$pfxPath = "${params?.certPath || ""}" # PFX 文件路径(与表单中保持一致) +$pfxPassword = "${params?.pfxPassword || ""}" # PFX 密码(与表单中保持一致) # 导入证书到本地计算机的个人存储区 $cert = Import-PfxCertificate -FilePath "$pfxPath" -CertStoreLocation Cert:\\LocalMachine\\My -Password (ConvertTo-SecureString -String "$pfxPassword" -AsPlainText -Force) -Exportable @@ -159,6 +168,16 @@ const DeployNodeConfigFormLocalConfig = ({ form: formInst, formName, disabled, i .min(1, t("workflow_node.deploy.form.local_cert_path.tooltip")) .max(256, t("common.errmsg.string_max", { max: 256 })) .trim(), + certPathForServerOnly: z + .string() + .max(256, t("common.errmsg.string_max", { max: 256 })) + .trim() + .nullish(), + certPathForIntermediaOnly: z + .string() + .max(256, t("common.errmsg.string_max", { max: 256 })) + .trim() + .nullish(), keyPath: z .string() .max(256, t("common.errmsg.string_max", { max: 256 })) @@ -325,6 +344,24 @@ const DeployNodeConfigFormLocalConfig = ({ form: formInst, formName, disabled, i > + + } + > + + + + } + > + + @@ -407,7 +444,13 @@ const DeployNodeConfigFormLocalConfig = ({ form: formInst, formName, disabled, i
- + @@ -437,7 +480,13 @@ const DeployNodeConfigFormLocalConfig = ({ form: formInst, formName, disabled, i
- +
diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormNetlifySiteConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormNetlifySiteConfig.tsx new file mode 100644 index 00000000..ffa38fae --- /dev/null +++ b/ui/src/components/workflow/node/DeployNodeConfigFormNetlifySiteConfig.tsx @@ -0,0 +1,63 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +type DeployNodeConfigFormNetlifySiteConfigFieldValues = Nullish<{ + siteId: string; +}>; + +export type DeployNodeConfigFormNetlifySiteConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: DeployNodeConfigFormNetlifySiteConfigFieldValues; + onValuesChange?: (values: DeployNodeConfigFormNetlifySiteConfigFieldValues) => void; +}; + +const initFormModel = (): DeployNodeConfigFormNetlifySiteConfigFieldValues => { + return { + siteId: "", + }; +}; + +const DeployNodeConfigFormNetlifySiteConfig = ({ + form: formInst, + formName, + disabled, + initialValues, + onValuesChange, +}: DeployNodeConfigFormNetlifySiteConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + siteId: z.string().nonempty(t("workflow_node.deploy.form.netlify_site_id.placeholder")), + }); + const formRule = createSchemaFieldRule(formSchema); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ } + > + + +
+ ); +}; + +export default DeployNodeConfigFormNetlifySiteConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx index 042e40c5..e99a2431 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx @@ -4,21 +4,24 @@ import { Button, Dropdown, Form, type FormInstance, Input, Select, Switch } from import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; +import CodeInput from "@/components/CodeInput"; import Show from "@/components/Show"; import { CERTIFICATE_FORMATS } from "@/domain/certificate"; -import { initPresetScript } from "./DeployNodeConfigFormLocalConfig"; +import { initPresetScript as _initPresetScript } from "./DeployNodeConfigFormLocalConfig"; type DeployNodeConfigFormSSHConfigFieldValues = Nullish<{ format: string; certPath: string; - keyPath?: string | null; - pfxPassword?: string | null; - jksAlias?: string | null; - jksKeypass?: string | null; - jksStorepass?: string | null; - preCommand?: string | null; - postCommand?: string | null; + certPathForServerOnly?: string; + certPathForIntermediaOnly?: string; + keyPath?: string; + pfxPassword?: string; + jksAlias?: string; + jksKeypass?: string; + jksStorepass?: string; + preCommand?: string; + postCommand?: string; useSCP?: boolean; }>; @@ -42,6 +45,125 @@ const initFormModel = (): DeployNodeConfigFormSSHConfigFieldValues => { }; }; +const initPresetScript = ( + key: Parameters[0] | "sh_replace_synologydsm_ssl" | "sh_replace_fnos_ssl", + params?: Parameters[1] +) => { + switch (key) { + case "sh_replace_synologydsm_ssl": + return `# *** 需要 root 权限 *** +# 脚本参考 https://github.com/catchdave/ssl-certs/blob/main/replace_synology_ssl_certs.sh + +# 请将以下变量替换为实际值 +$tmpFullchainPath = "${params?.certPath || ""}" # 证书文件路径(与表单中保持一致) +$tmpCertPath = "${params?.certPathForServerOnly || ""}" # 服务器证书文件路径(与表单中保持一致) +$tmpKeyPath = "${params?.keyPath || ""}" # 私钥文件路径(与表单中保持一致) + +DEBUG=1 +error_exit() { echo "[ERROR] $1"; exit 1; } +warn() { echo "[WARN] $1"; } +info() { echo "[INFO] $1"; } +debug() { [[ "\${DEBUG}" ]] && echo "[DEBUG] $1"; } + +certs_src_dir="/usr/syno/etc/certificate/system/default" +target_cert_dirs=( + "/usr/syno/etc/certificate/system/FQDN" + "/usr/local/etc/certificate/ScsiTarget/pkg-scsi-plugin-server/" + "/usr/local/etc/certificate/SynologyDrive/SynologyDrive/" + "/usr/local/etc/certificate/WebDAVServer/webdav/" + "/usr/local/etc/certificate/ActiveBackup/ActiveBackup/" + "/usr/syno/etc/certificate/smbftpd/ftpd/") + +# 获取证书目录 +default_dir_name=$(/dev/null && /usr/syno/bin/synopkg restart ScsiTarget +/usr/syno/bin/synopkg is_onoff SynologyDrive 1>/dev/null && /usr/syno/bin/synopkg restart SynologyDrive +/usr/syno/bin/synopkg is_onoff WebDAVServer 1>/dev/null && /usr/syno/bin/synopkg restart WebDAVServer +/usr/syno/bin/synopkg is_onoff ActiveBackup 1>/dev/null && /usr/syno/bin/synopkg restart ActiveBackup +if ! /usr/syno/bin/synow3tool --gen-all && sudo /usr/syno/bin/synosystemctl restart nginx; then + warn "nginx failed to restart" +fi + +info "Completed" + `.trim(); + + case "sh_replace_fnos_ssl": + return `# *** 需要 root 权限 *** +# 脚本参考 https://github.com/lfgyx/fnos_certificate_update/blob/main/src/update_cert.sh + +# 请将以下变量替换为实际值 +# 飞牛证书实际存放路径请在 \`/usr/trim/etc/network_cert_all.conf\` 中查看,注意不要修改文件名 +$tmpFullchainPath = "${params?.certPath || ""}" # 证书文件路径(与表单中保持一致) +$tmpCertPath = "${params?.certPathForServerOnly || ""}" # 服务器证书文件路径(与表单中保持一致) +$tmpKeyPath = "${params?.keyPath || ""}" # 私钥文件路径(与表单中保持一致) +$fnFullchainPath = "/usr/trim/var/trim_connect/ssls/example.com/1234567890/fullchain.crt" # 飞牛证书文件路径 +$fnCertPath = "/usr/trim/var/trim_connect/ssls/example.com/1234567890/example.com.crt" # 飞牛服务器证书文件路径 +$fnKeyPath = "/usr/trim/var/trim_connect/ssls/example.com/1234567890/example.com.key" # 飞牛私钥文件路径 +$domain = "" # 域名 + +# 复制文件 +cp -rf "$tmpFullchainPath" "$fnFullchainPath" +cp -rf "$tmpCertPath" "$fnCertPath" +cp -rf "$tmpKeyPath" "$fnKeyPath" +chmod 755 "$fnCertPath" +chmod 755 "$fnKeyPath" +chmod 755 "$fnFullchainPath" + +# 更新数据库 +NEW_EXPIRY_DATE=$(openssl x509 -enddate -noout -in "$fnCertPath" | sed "s/^.*=\\(.*\\)$/\\1/") +NEW_EXPIRY_TIMESTAMP=$(date -d "$NEW_EXPIRY_DATE" +%s%3N) +psql -U postgres -d trim_connect -c "UPDATE cert SET valid_to=$NEW_EXPIRY_TIMESTAMP WHERE domain='$domain'" + +# 重启服务 +systemctl restart webdav.service +systemctl restart smbftpd.service +systemctl restart trim_nginx.service + `.trim(); + } + + return _initPresetScript(key as Parameters[0], params); +}; + const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: DeployNodeConfigFormSSHConfigProps) => { const { t } = useTranslation(); @@ -60,6 +182,16 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini .trim() .nullish() .refine((v) => fieldFormat !== FORMAT_PEM || !!v?.trim(), { message: t("workflow_node.deploy.form.ssh_key_path.tooltip") }), + certPathForServerOnly: z + .string() + .max(256, t("common.errmsg.string_max", { max: 256 })) + .trim() + .nullish(), + certPathForIntermediaOnly: z + .string() + .max(256, t("common.errmsg.string_max", { max: 256 })) + .trim() + .nullish(), pfxPassword: z .string() .max(64, t("common.errmsg.string_max", { max: 256 })) @@ -147,6 +279,24 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini const handlePresetPostScriptClick = (key: string) => { switch (key) { case "sh_reload_nginx": + { + formInst.setFieldValue("postCommand", initPresetScript(key)); + } + break; + + case "sh_replace_synologydsm_ssl": + case "sh_replace_fnos_ssl": + { + const presetScriptParams = { + certPath: formInst.getFieldValue("certPath"), + certPathForServerOnly: formInst.getFieldValue("certPathForServerOnly"), + certPathForIntermediaOnly: formInst.getFieldValue("certPathForIntermediaOnly"), + keyPath: formInst.getFieldValue("keyPath"), + }; + formInst.setFieldValue("postCommand", initPresetScript(key, presetScriptParams)); + } + break; + case "ps_binding_iis": case "ps_binding_netsh": case "ps_binding_rdp": @@ -206,6 +356,24 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini > + + } + > + + + + } + > + +
@@ -248,10 +416,6 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini - -