Merge pull request #16 from woodchen-ink:usual2970-main

Usual2970-main
This commit is contained in:
wood chen 2025-05-16 12:50:00 +08:00 committed by GitHub
commit db2654ff84
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
111 changed files with 1974 additions and 410 deletions

View File

@ -36,7 +36,7 @@ release:
archives:
- id: archive_noncgo
builds: [build_noncgo]
format: zip
format: "zip"
files:
- CHANGELOG.md
- LICENSE.md

View File

@ -9,7 +9,7 @@
## 前提条件
- Go 1.22+ (用于修改 Go 代码)
- Go 1.24+ (用于修改 Go 代码)
- Node 20+ (用于修改 UI)
如果还没有这样做,你可以 fork Certimate 的主仓库,并克隆到本地以便进行修改:

View File

@ -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:

View File

@ -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 证书颁发机构;
- 更多特性等待探索。

View File

@ -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 ServicesSSL.com, ZeroSSL, and more;
- More features waiting to be discovered.

View File

@ -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{}

View File

@ -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
}

View File

@ -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"`
}

View File

@ -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)
)

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -32,7 +32,7 @@ type DeployerConfig struct {
// Key Vault 名称。
KeyVaultName string `json:"keyvaultName"`
// Key Vault 证书名称。
// 选填。
// 选填。零值时表示新建证书;否则表示更新证书。
CertificateName string `json:"certificateName,omitempty"`
}

View File

@ -20,7 +20,7 @@ type DeployerConfig struct {
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
// 证书 ID。
// 选填。
// 选填。零值时表示新建证书;否则表示更新证书。
CertificateId string `json:"certificateId,omitempty"`
}

View File

@ -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"`
}

View File

@ -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,

View File

@ -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)

View File

@ -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)
}

View File

@ -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})
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
})
}

View File

@ -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)
}

View File

@ -34,7 +34,7 @@ type DeployerConfig struct {
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
// 证书 ID。
// 选填。
// 选填。零值时表示新建证书;否则表示更新证书。
CertificateId string `json:"certificateId,omitempty"`
// Webhook ID。
// 选填。

View File

@ -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 {

View File

@ -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)

View File

@ -1,4 +1,4 @@
package onepanelsdk
package onepanel
import (
"fmt"

View File

@ -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

View File

@ -1,4 +1,4 @@
package onepanelsdk
package onepanel
type BaseResponse interface {
GetCode() int32

View File

@ -1,4 +1,4 @@
package baishansdk
package baishan
import (
"net/http"

View File

@ -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

View File

@ -1,4 +1,4 @@
package baishansdk
package baishan
import "encoding/json"

View File

@ -1,4 +1,4 @@
package btpanelsdk
package btpanel
func (c *Client) ConfigSavePanelSSL(req *ConfigSavePanelSSLRequest) (*ConfigSavePanelSSLResponse, error) {
resp := &ConfigSavePanelSSLResponse{}

View File

@ -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())
}
}

View File

@ -1,4 +1,4 @@
package btpanelsdk
package btpanel
type BaseResponse interface {
GetStatus() *bool

View File

@ -1,4 +1,4 @@
package bunnysdk
package bunny
import (
"fmt"

View File

@ -1,4 +1,4 @@
package bunnysdk
package bunny
import (
"encoding/json"

View File

@ -1,4 +1,4 @@
package bunnysdk
package bunny
type AddCustomCertificateRequest struct {
Hostname string `json:"Hostname"`

View File

@ -1,4 +1,4 @@
package cacheflysdk
package cachefly
import (
"net/http"

View File

@ -1,4 +1,4 @@
package cacheflysdk
package cachefly
import (
"encoding/json"

View File

@ -1,4 +1,4 @@
package cacheflysdk
package cachefly
type BaseResponse interface {
GetMessage() string

View File

@ -1,4 +1,4 @@
package cdnflysdk
package cdnfly
import (
"fmt"

View File

@ -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

View File

@ -1,4 +1,4 @@
package cdnflysdk
package cdnfly
import "fmt"

View File

@ -1,4 +1,4 @@
package dnslasdk
package dnsla
import (
"fmt"

View File

@ -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

View File

@ -1,4 +1,4 @@
package dnslasdk
package dnsla
type BaseResponse interface {
GetCode() int32

View File

@ -1,4 +1,4 @@
package dogecloudsdk
package dogecloud
import (
"crypto/hmac"

View File

@ -1,4 +1,4 @@
package dogecloudsdk
package dogecloud
type BaseResponse struct {
Code *int `json:"code,omitempty"`

View File

@ -1,4 +1,4 @@
package gnamesdk
package gname
func (c *Client) AddDomainResolution(req *AddDomainResolutionRequest) (*AddDomainResolutionResponse, error) {
resp := &AddDomainResolutionResponse{}

View File

@ -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

View File

@ -1,4 +1,4 @@
package gnamesdk
package gname
import "encoding/json"

View File

@ -9,7 +9,7 @@ import (
func (c *Client) getAccessToken() error {
req := &getAPIAccessTokenRequest{
Type: c.apiUserType,
Type: c.apiRole,
AccessKeyId: c.accessKeyId,
AccessKey: c.accessKey,
}

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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"`
}

View File

@ -1,4 +1,4 @@
package qiniusdk
package qiniu
import (
"net/http"

View File

@ -1,4 +1,4 @@
package qiniusdk
package qiniu
import (
"context"

View File

@ -1,4 +1,4 @@
package qiniusdk
package qiniu
type BaseResponse struct {
Code *int `json:"code,omitempty"`

View File

@ -1,4 +1,4 @@
package rainyunsdk
package rainyun
import (
"fmt"

View File

@ -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

View File

@ -1,4 +1,4 @@
package rainyunsdk
package rainyun
type BaseResponse interface {
GetCode() int32

View File

@ -1,4 +1,4 @@
package safelinesdk
package safeline
func (c *Client) UpdateCertificate(req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) {
resp := &UpdateCertificateResponse{}

View File

@ -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())
}
}

View File

@ -1,4 +1,4 @@
package safelinesdk
package safeline
type BaseResponse interface {
GetErrCode() *string

View File

@ -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

View File

@ -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
}

View File

@ -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
})
}

277
ui/package-lock.json generated
View File

@ -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",

View File

@ -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",

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -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<ReactCodeMirrorProps, "extensions" | "lang" | "theme"> {
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<ReactCodeMirrorRef>(null);
const isFocusWithin = useFocusWithin(cmRef.current?.editor);
const cmTheme = useMemo(() => {
if (browserTheme === "dark") {
return vscodeDark;
}
return vscodeLight;
}, [browserTheme]);
const cmExtensions = useMemo(() => {
const temp: NonNullable<ReactCodeMirrorProps["extensions"]> = [
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 (
<div
className={mergeCls(className, `hover:border-[${themeToken.colorPrimaryBorderHover}]`)}
style={{
...(style ?? {}),
border: `1px solid ${isFocusWithin ? (themeToken.Input?.activeBorderColor ?? themeToken.colorPrimaryBorder) : themeToken.colorBorder}`,
borderRadius: `${themeToken.borderRadius}px`,
backgroundColor: disabled ? themeToken.colorBgContainerDisabled : themeToken.colorBgContainer,
boxShadow: isFocusWithin ? themeToken.Input?.activeShadow : undefined,
overflow: "hidden",
}}
>
<CodeMirror
ref={cmRef}
height="100%"
style={{ height: "100%" }}
{...props}
basicSetup={{
foldGutter: false,
dropCursor: false,
allowMultipleSelections: false,
indentOnInput: false,
}}
extensions={cmExtensions}
theme={cmTheme}
/>
</div>
);
};
export default CodeInput;

View File

@ -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<TextAreaProps, "onChange"> {
accept?: UploadProps["accept"];
uploadButtonProps?: Omit<ButtonProps, "disabled" | "onClick">;
uploadText?: string;
onChange?: (value: string) => void;
}
const TextFileInput = ({ className, style, accept, disabled, readOnly, uploadText, uploadButtonProps, onChange, ...props }: TextFileInputProps) => {
const { t } = useTranslation();
const fileInputRef = useRef<HTMLInputElement>(null);
const handleButtonClick = () => {
if (fileInputRef.current) {
fileInputRef.current.click();
}
};
const handleFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
const { files } = e.target as HTMLInputElement;
if (files?.length) {
const value = await readFileContent(files[0]);
onChange?.(value);
}
};
return (
<Space className={mergeCls("w-full", className)} style={style} direction="vertical" size="small">
<Input.TextArea {...props} disabled={disabled} readOnly={readOnly} onChange={(e) => onChange?.(e.target.value)} />
{!readOnly && (
<>
<Button {...uploadButtonProps} block disabled={disabled} icon={<UploadOutlinedIcon />} onClick={handleButtonClick}>
{uploadText ?? t("common.text.import_from_file")}
</Button>
<input ref={fileInputRef} type="file" style={{ display: "none" }} accept={accept} onChange={handleFileChange} />
</>
)}
</Space>
);
};
export default TextFileInput;

View File

@ -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<AccessFormInstance, AccessFormProps>(({ className,
return <AccessFormNameDotComConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.NAMESILO:
return <AccessFormNameSiloConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.NETCUP:
return <AccessFormNetcupConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.NETLIFY:
return <AccessFormNetlifyConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.NS1:
return <AccessFormNS1Config {...nestedFormProps} />;
case ACCESS_PROVIDERS.PORKBUN:

View File

@ -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://<your-host-addr>: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
<Input placeholder={t("access.form.goedge_api_url.placeholder")} />
</Form.Item>
<Form.Item name="apiRole" label={t("access.form.goedge_api_role.label")} rules={[formRule]}>
<Radio.Group options={["user", "admin"].map((s) => ({ label: t(`access.form.goedge_api_role.option.${s}.label`), value: s }))} />
</Form.Item>
<Form.Item
name="accessKeyId"
label={t("access.form.goedge_access_key_id.label")}

View File

@ -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, Upload, type UploadFile, type UploadProps } from "antd";
import { Form, type FormInstance } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import TextFileInput from "@/components/TextFileInput";
import { type AccessConfigForKubernetes } from "@/domain/access";
import { readFileContent } from "@/utils/file";
type AccessFormKubernetesConfigFieldValues = Nullish<AccessConfigForKubernetes>;
@ -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<UploadFile[]>([]);
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<typeof formSchema>) => {
onValuesChange?.(values);
};
@ -65,16 +45,13 @@ const AccessFormKubernetesConfig = ({ form: formInst, formName, disabled, initia
name={formName}
onValuesChange={handleFormChange}
>
<Form.Item name="kubeConfig" noStyle rules={[formRule]}>
<Input.TextArea autoComplete="new-password" hidden placeholder={t("access.form.k8s_kubeconfig.placeholder")} value={fieldKubeConfig} />
</Form.Item>
<Form.Item
name="kubeConfig"
label={t("access.form.k8s_kubeconfig.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.k8s_kubeconfig.tooltip") }}></span>}
>
<Upload beforeUpload={() => false} fileList={fieldKubeFileList} maxCount={1} onChange={handleKubeFileChange}>
<Button icon={<UploadOutlinedIcon />}>{t("access.form.k8s_kubeconfig.upload")}</Button>
</Upload>
<TextFileInput allowClear autoSize={{ minRows: 3, maxRows: 10 }} placeholder={t("access.form.k8s_kubeconfig.placeholder")} />
</Form.Item>
</Form>
);

View File

@ -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<AccessConfigForNetcup>;
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<typeof formSchema>) => {
onValuesChange?.(values);
};
return (
<Form
form={formInst}
disabled={disabled}
initialValues={initialValues ?? initFormModel()}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
>
<Form.Item
name="customerNumber"
label={t("access.form.netcup_customer_number.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.netcup_customer_number.tooltip") }}></span>}
>
<Input autoComplete="new-password" placeholder={t("access.form.netcup_customer_number.placeholder")} />
</Form.Item>
<Form.Item
name="apiKey"
label={t("access.form.netcup_api_key.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.netcup_api_key.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.netcup_api_key.placeholder")} />
</Form.Item>
<Form.Item
name="apiPassword"
label={t("access.form.netcup_api_password.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.netcup_api_password.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.netcup_api_password.placeholder")} />
</Form.Item>
</Form>
);
};
export default AccessFormNetcupConfig;

View File

@ -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<AccessConfigForNetlify>;
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<typeof formSchema>) => {
onValuesChange?.(values);
};
return (
<Form
form={formInst}
disabled={disabled}
initialValues={initialValues ?? initFormModel()}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
>
<Form.Item
name="apiToken"
label={t("access.form.netlify_api_token.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.netlify_api_token.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.netlify_api_token.placeholder")} />
</Form.Item>
</Form>
);
};
export default AccessFormNetlifyConfig;

View File

@ -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<AccessConfigForSSH>;
@ -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<UploadFile[]>([]);
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<typeof formSchema>) => {
onValuesChange?.(values);
};
@ -104,48 +84,36 @@ const AccessFormSSHConfig = ({ form: formInst, formName, disabled, initialValues
</div>
</div>
<div className="flex space-x-2">
<div className="w-1/2">
<Form.Item name="username" label={t("access.form.ssh_username.label")} rules={[formRule]}>
<Input autoComplete="new-password" placeholder={t("access.form.ssh_username.placeholder")} />
</Form.Item>
</div>
<Form.Item name="username" label={t("access.form.ssh_username.label")} rules={[formRule]}>
<Input autoComplete="new-password" placeholder={t("access.form.ssh_username.placeholder")} />
</Form.Item>
<div className="w-1/2">
<Form.Item
name="password"
label={t("access.form.ssh_password.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ssh_password.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.ssh_password.placeholder")} />
</Form.Item>
</div>
</div>
<Form.Item
name="password"
label={t("access.form.ssh_password.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ssh_password.tooltip") }}></span>}
>
<Input.Password allowClear autoComplete="new-password" placeholder={t("access.form.ssh_password.placeholder")} />
</Form.Item>
<div className="flex space-x-2">
<div className="w-1/2">
<Form.Item name="key" noStyle rules={[formRule]}>
<Input.TextArea autoComplete="new-password" hidden placeholder={t("access.form.ssh_key.placeholder")} value={fieldKey} />
</Form.Item>
<Form.Item label={t("access.form.ssh_key.label")} tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ssh_key.tooltip") }}></span>}>
<Upload beforeUpload={() => false} fileList={fieldKeyFileList} maxCount={1} onChange={handleKeyFileChange}>
<Button icon={<UploadOutlinedIcon />}>{t("access.form.ssh_key.upload")}</Button>
</Upload>
</Form.Item>
</div>
<Form.Item
name="key"
label={t("access.form.ssh_key.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ssh_key.tooltip") }}></span>}
>
<TextFileInput allowClear autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t("access.form.ssh_key.placeholder")} />
</Form.Item>
<div className="w-1/2">
<Form.Item
name="keyPassphrase"
label={t("access.form.ssh_key_passphrase.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ssh_key_passphrase.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.ssh_key_passphrase.placeholder")} />
</Form.Item>
</div>
</div>
<Form.Item
name="keyPassphrase"
label={t("access.form.ssh_key_passphrase.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ssh_key_passphrase.tooltip") }}></span>}
>
<Input.Password allowClear autoComplete="new-password" placeholder={t("access.form.ssh_key_passphrase.placeholder")} />
</Form.Item>
</Form>
);
};

View File

@ -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<HTMLTextAreaElement>) => {
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<HTMLTextAreaElement>) => {
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={<span dangerouslySetInnerHTML={{ __html: t("access.form.webhook_headers.tooltip") }}></span>}
>
<Input.TextArea autoSize={{ minRows: 3, maxRows: 5 }} placeholder={t("access.form.webhook_headers.placeholder")} onBlur={handleWebhookHeadersBlur} />
<Input.TextArea autoSize={{ minRows: 3, maxRows: 10 }} placeholder={t("access.form.webhook_headers.placeholder")} onBlur={handleWebhookHeadersBlur} />
</Form.Item>
<Show when={!usage || usage === "deployment"}>
@ -297,9 +298,11 @@ const AccessFormWebhookConfig = ({ form: formInst, formName, disabled, initialVa
</div>
</label>
<Form.Item name="defaultDataForDeployment" rules={[formRule]}>
<Input.TextArea
allowClear
autoSize={{ minRows: 3, maxRows: 10 }}
<CodeInput
height="auto"
minHeight="64px"
maxHeight="256px"
language="json"
placeholder={t("access.form.webhook_default_data_for_deployment.placeholder")}
onBlur={handleWebhookDataForDeploymentBlur}
/>
@ -338,9 +341,11 @@ const AccessFormWebhookConfig = ({ form: formInst, formName, disabled, initialVa
</div>
</label>
<Form.Item name="defaultDataForNotification" rules={[formRule]}>
<Input.TextArea
allowClear
autoSize={{ minRows: 3, maxRows: 10 }}
<CodeInput
height="auto"
minHeight="64px"
maxHeight="256px"
language="json"
placeholder={t("access.form.webhook_default_data_for_notification.placeholder")}
onBlur={handleWebhookDataForNotificationBlur}
/>

View File

@ -75,7 +75,7 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => {
</CopyToClipboard>
</Tooltip>
</div>
<Input.TextArea value={data.certificate} variant="filled" rows={5} autoSize={{ maxRows: 5 }} readOnly />
<Input.TextArea value={data.certificate} variant="filled" autoSize={{ minRows: 5, maxRows: 5 }} readOnly />
</Form.Item>
<Form.Item>
@ -92,7 +92,7 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => {
</CopyToClipboard>
</Tooltip>
</div>
<Input.TextArea value={data.privateKey} variant="filled" rows={5} autoSize={{ maxRows: 5 }} readOnly />
<Input.TextArea value={data.privateKey} variant="filled" autoSize={{ minRows: 5, maxRows: 5 }} readOnly />
</Form.Item>
</Form>

View File

@ -108,7 +108,7 @@ const NotifyTemplateForm = ({ className, style }: NotifyTemplateFormProps) => {
rules={[formRule]}
>
<Input.TextArea
autoSize={{ minRows: 3, maxRows: 5 }}
autoSize={{ minRows: 3, maxRows: 10 }}
placeholder={t("settings.notification.template.form.message.placeholder")}
onChange={handleInputChange}
/>

View File

@ -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 (
<Col key={index} xs={24} md={12} span={8}>
<Card
className="h-20 w-full overflow-hidden shadow-sm"
className={mergeCls("h-20 w-full overflow-hidden shadow-sm", provider.builtin ? " cursor-not-allowed" : "")}
styles={{ body: { height: "100%", padding: "0.5rem 1rem" } }}
hoverable
onClick={() => {
if (provider.builtin) {
return;
}
handleProviderTypeSelect(provider.type);
}}
>
<Flex className="size-full overflow-hidden" align="center" gap={8}>
<Avatar src={provider.icon} size="small" />
<div className="flex-1 overflow-hidden">
<Typography.Text className="mb-1 line-clamp-1">{t(provider.name)}</Typography.Text>
<Typography.Text className="mb-1 line-clamp-1" type={provider.builtin ? "secondary" : undefined}>
{t(provider.name)}
</Typography.Text>
<div className="origin-left scale-[80%]">
<Show when={provider.builtin}>
<Tag>{t("access.props.provider.builtin")}</Tag>

View File

@ -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<DeployNodeConfigFormInstance, DeployNode
return <DeployNodeConfigFormKubernetesSecretConfig {...nestedFormProps} />;
case DEPLOYMENT_PROVIDERS.LOCAL:
return <DeployNodeConfigFormLocalConfig {...nestedFormProps} />;
case DEPLOYMENT_PROVIDERS.NETLIFY_SITE:
return <DeployNodeConfigFormNetlifySiteConfig {...nestedFormProps} />;
case DEPLOYMENT_PROVIDERS.PROXMOXVE:
return <DeployNodeConfigFormProxmoxVEConfig {...nestedFormProps} />;
case DEPLOYMENT_PROVIDERS.QINIU_CDN:

View File

@ -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,
>
<Input placeholder={t("workflow_node.deploy.form.aws_acm_region.placeholder")} />
</Form.Item>
<Form.Item
name="certificateArn"
label={t("workflow_node.deploy.form.aws_acm_certificate_arn.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.aws_acm_certificate_arn.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.aws_acm_certificate_arn.placeholder")} />
</Form.Item>
</Form>
);
};

View File

@ -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")),

View File

@ -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 || "<your-key-path>"}" -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 || "<your-cert-path>"}" # PFX
$pfxPassword = "${params?.pfxPassword || "<your-pfx-password>"}" # PFX
$pfxPath = "${params?.certPath || "<your-cert-path>"}" # PFX
$pfxPassword = "${params?.pfxPassword || "<your-pfx-password>"}" # PFX
$siteName = "<your-site-name>" # IIS
$domain = "<your-domain-name>" #
$ipaddr = "<your-binding-ip>" # IP* IP
$port = "<your-binding-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 || "<your-cert-path>"}" # PFX
$pfxPassword = "${params?.pfxPassword || "<your-pfx-password>"}" # PFX
$ipaddr = "<your-binding-ip>" # IP0.0.0.0 IP
$pfxPath = "${params?.certPath || "<your-cert-path>"}" # PFX
$pfxPassword = "${params?.pfxPassword || "<your-pfx-password>"}" # PFX
$ipaddr = "<your-binding-ip>" # IP0.0.0.0 IP
$port = "<your-binding-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 || "<your-cert-path>"}" # PFX
$pfxPassword = "${params?.pfxPassword || "<your-pfx-password>"}" # PFX
$pfxPath = "${params?.certPath || "<your-cert-path>"}" # PFX
$pfxPassword = "${params?.pfxPassword || "<your-pfx-password>"}" # 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
>
<Input placeholder={t("workflow_node.deploy.form.local_key_path.placeholder")} />
</Form.Item>
<Form.Item
name="certPathForServerOnly"
label={t("workflow_node.deploy.form.local_servercert_path.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.local_servercert_path.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.local_servercert_path.placeholder")} />
</Form.Item>
<Form.Item
name="certPathForIntermediaOnly"
label={t("workflow_node.deploy.form.local_intermediacert_path.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.local_intermediacert_path.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.local_intermediacert_path.placeholder")} />
</Form.Item>
</Show>
<Show when={fieldFormat === FORMAT_PFX}>
@ -407,7 +444,13 @@ const DeployNodeConfigFormLocalConfig = ({ form: formInst, formName, disabled, i
</div>
</label>
<Form.Item name="preCommand" rules={[formRule]}>
<Input.TextArea autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t("workflow_node.deploy.form.local_pre_command.placeholder")} />
<CodeInput
height="auto"
minHeight="64px"
maxHeight="256px"
language={["shell", "powershell"]}
placeholder={t("workflow_node.deploy.form.local_pre_command.placeholder")}
/>
</Form.Item>
</Form.Item>
@ -437,7 +480,13 @@ const DeployNodeConfigFormLocalConfig = ({ form: formInst, formName, disabled, i
</div>
</label>
<Form.Item name="postCommand" rules={[formRule]}>
<Input.TextArea autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t("workflow_node.deploy.form.local_post_command.placeholder")} />
<CodeInput
height="auto"
minHeight="64px"
maxHeight="256px"
language={["shell", "powershell"]}
placeholder={t("workflow_node.deploy.form.local_post_command.placeholder")}
/>
</Form.Item>
</Form.Item>
</Form>

View File

@ -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<typeof formSchema>) => {
onValuesChange?.(values);
};
return (
<Form
form={formInst}
disabled={disabled}
initialValues={initialValues ?? initFormModel()}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
>
<Form.Item
name="siteId"
label={t("workflow_node.deploy.form.netlify_site_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.netlify_site_id.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.netlify_site_id.placeholder")} />
</Form.Item>
</Form>
);
};
export default DeployNodeConfigFormNetlifySiteConfig;

View File

@ -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<typeof _initPresetScript>[0] | "sh_replace_synologydsm_ssl" | "sh_replace_fnos_ssl",
params?: Parameters<typeof _initPresetScript>[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 || "<your-fullchain-cert-path>"}" #
$tmpCertPath = "${params?.certPathForServerOnly || "<your-server-cert-path>"}" #
$tmpKeyPath = "${params?.keyPath || "<your-key-path>"}" #
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=$(</usr/syno/etc/certificate/_archive/DEFAULT)
if [[ -n "$default_dir_name" ]]; then
target_cert_dirs+=("/usr/syno/etc/certificate/_archive/\${default_dir_name}")
debug "Default cert directory found: '/usr/syno/etc/certificate/_archive/\${default_dir_name}'"
else
warn "No default directory found. Probably unusual? Check: 'cat /usr/syno/etc/certificate/_archive/DEFAULT'"
fi
#
for proxy in /usr/syno/etc/certificate/ReverseProxy/*/; do
debug "Found proxy dir: \${proxy}"
target_cert_dirs+=("\${proxy}")
done
[[ "\${DEBUG}" ]] && set -x
#
cp -rf "$tmpFullchainPath" "\${certs_src_dir}/fullchain.pem" || error_exit "Halting because of error moving fullchain file"
cp -rf "$tmpCertPath" "\${certs_src_dir}/cert.pem" || error_exit "Halting because of error moving cert file"
cp -rf "$tmpKeyPath" "\${certs_src_dir}/privkey.pem" || error_exit "Halting because of error moving privkey file"
chown root:root "\${certs_src_dir}/"{privkey,fullchain,cert}.pem || error_exit "Halting because of error chowning files"
info "Certs moved from /tmp & chowned."
#
for target_dir in "\${target_cert_dirs[@]}"; do
if [[ ! -d "$target_dir" ]]; then
debug "Target cert directory '$target_dir' not found, skipping..."
continue
fi
info "Copying certificates to '$target_dir'"
if ! (cp "\${certs_src_dir}/"{privkey,fullchain,cert}.pem "$target_dir/" && \
chown root:root "$target_dir/"{privkey,fullchain,cert}.pem); then
warn "Error copying or chowning certs to \${target_dir}"
fi
done
#
info "Rebooting all the things..."
/usr/syno/bin/synosystemctl restart nmbd
/usr/syno/bin/synosystemctl restart avahi
/usr/syno/bin/synosystemctl restart ldap-server
/usr/syno/bin/synopkg is_onoff ScsiTarget 1>/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 || "<your-fullchain-cert-path>"}" #
$tmpCertPath = "${params?.certPathForServerOnly || "<your-server-cert-path>"}" #
$tmpKeyPath = "${params?.keyPath || "<your-key-path>"}" #
$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 = "<your-domain-name>" #
#
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<typeof _initPresetScript>[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
>
<Input placeholder={t("workflow_node.deploy.form.ssh_key_path.placeholder")} />
</Form.Item>
<Form.Item
name="certPathForServerOnly"
label={t("workflow_node.deploy.form.ssh_servercert_path.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.ssh_servercert_path.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.ssh_servercert_path.placeholder")} />
</Form.Item>
<Form.Item
name="certPathForIntermediaOnly"
label={t("workflow_node.deploy.form.ssh_intermediacert_path.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.ssh_intermediacert_path.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.ssh_intermediacert_path.placeholder")} />
</Form.Item>
</Show>
<Show when={fieldFormat === FORMAT_PFX}>
@ -248,10 +416,6 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini
</Form.Item>
</Show>
<Form.Item label={t("workflow_node.deploy.form.ssh_shell_env.label")}>
<Select options={[{ value: t("workflow_node.deploy.form.ssh_shell_env.value") }]} value={t("workflow_node.deploy.form.ssh_shell_env.value")} />
</Form.Item>
<Form.Item className="mb-0" htmlFor="null">
<label className="mb-1 block">
<div className="flex w-full items-center justify-between gap-4">
@ -278,7 +442,13 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini
</div>
</label>
<Form.Item name="preCommand" rules={[formRule]}>
<Input.TextArea autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t("workflow_node.deploy.form.ssh_pre_command.placeholder")} />
<CodeInput
height="auto"
minHeight="64px"
maxHeight="256px"
language={["shell", "powershell"]}
placeholder={t("workflow_node.deploy.form.ssh_pre_command.placeholder")}
/>
</Form.Item>
</Form.Item>
@ -291,11 +461,13 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini
<div className="text-right">
<Dropdown
menu={{
items: ["sh_reload_nginx", "ps_binding_iis", "ps_binding_netsh", "ps_binding_rdp"].map((key) => ({
key,
label: t(`workflow_node.deploy.form.ssh_preset_scripts.option.${key}.label`),
onClick: () => handlePresetPostScriptClick(key),
})),
items: ["sh_reload_nginx", "sh_replace_synologydsm_ssl", "sh_replace_fnos_ssl", "ps_binding_iis", "ps_binding_netsh", "ps_binding_rdp"].map(
(key) => ({
key,
label: t(`workflow_node.deploy.form.ssh_preset_scripts.option.${key}.label`),
onClick: () => handlePresetPostScriptClick(key),
})
),
}}
trigger={["click"]}
>
@ -308,7 +480,13 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini
</div>
</label>
<Form.Item name="postCommand" rules={[formRule]}>
<Input.TextArea autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t("workflow_node.deploy.form.ssh_post_command.placeholder")} />
<CodeInput
height="auto"
minHeight="64px"
maxHeight="256px"
language={["shell", "powershell"]}
placeholder={t("workflow_node.deploy.form.ssh_post_command.placeholder")}
/>
</Form.Item>
</Form.Item>

View File

@ -51,7 +51,7 @@ const DeployNodeConfigFormTencentCloudSSLDeployConfig = ({
if (!v) return false;
return String(v)
.split(MULTIPLE_INPUT_DELIMITER)
.every((e) => /^[A-Za-z0-9*._-]+$/.test(e));
.every((e) => /^[A-Za-z0-9*._-|]+$/.test(e));
}, t("workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.errmsg.invalid")),
});
const formRule = createSchemaFieldRule(formSchema);
@ -138,7 +138,7 @@ const ResourceIdsModalInput = memo(({ value, trigger, onChange }: { value?: stri
const formSchema = z.object({
resourceIds: z.array(z.string()).refine((v) => {
return v.every((e) => !e?.trim() || /^[A-Za-z0-9*._-]+$/.test(e));
return v.every((e) => !e?.trim() || /^[A-Za-z0-9*._-|]+$/.test(e));
}, t("workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.errmsg.invalid")),
});
const formRule = createSchemaFieldRule(formSchema);

View File

@ -1,8 +1,10 @@
import { useTranslation } from "react-i18next";
import { Alert, Form, type FormInstance, Input } from "antd";
import { Alert, Form, type FormInstance } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import CodeInput from "@/components/CodeInput";
type DeployNodeConfigFormWebhookConfigFieldValues = Nullish<{
webhookData: string;
}>;
@ -39,8 +41,8 @@ const DeployNodeConfigFormWebhookConfig = ({ form: formInst, formName, disabled,
});
const formRule = createSchemaFieldRule(formSchema);
const handleWebhookDataBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
const value = e.target.value;
const handleWebhookDataBlur = () => {
const value = formInst.getFieldValue("webhookData");
try {
const json = JSON.stringify(JSON.parse(value), null, 2);
formInst.setFieldValue("webhookData", json);
@ -68,9 +70,11 @@ const DeployNodeConfigFormWebhookConfig = ({ form: formInst, formName, disabled,
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.webhook_data.tooltip") }}></span>}
>
<Input.TextArea
allowClear
autoSize={{ minRows: 3, maxRows: 10 }}
<CodeInput
height="auto"
minHeight="64px"
maxHeight="256px"
language="json"
placeholder={t("workflow_node.deploy.form.webhook_data.placeholder")}
onBlur={handleWebhookDataBlur}
/>

View File

@ -177,7 +177,7 @@ const NotifyNodeConfigForm = forwardRef<NotifyNodeConfigFormInstance, NotifyNode
</Form.Item>
<Form.Item name="message" label={t("workflow_node.notify.form.message.label")} rules={[formRule]}>
<Input.TextArea autoSize={{ minRows: 3, maxRows: 5 }} placeholder={t("workflow_node.notify.form.message.placeholder")} />
<Input.TextArea autoSize={{ minRows: 3, maxRows: 10 }} placeholder={t("workflow_node.notify.form.message.placeholder")} />
</Form.Item>
<Form.Item className="mb-0" htmlFor="null">

View File

@ -1,8 +1,10 @@
import { useTranslation } from "react-i18next";
import { Alert, Form, type FormInstance, Input } from "antd";
import { Alert, Form, type FormInstance } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import CodeInput from "@/components/CodeInput";
type NotifyNodeConfigFormWebhookConfigFieldValues = Nullish<{
webhookData: string;
}>;
@ -39,8 +41,8 @@ const NotifyNodeConfigFormWebhookConfig = ({ form: formInst, formName, disabled,
});
const formRule = createSchemaFieldRule(formSchema);
const handleWebhookDataBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
const value = e.target.value;
const handleWebhookDataBlur = () => {
const value = formInst.getFieldValue("webhookData");
try {
const json = JSON.stringify(JSON.parse(value), null, 2);
formInst.setFieldValue("webhookData", json);
@ -68,9 +70,11 @@ const NotifyNodeConfigFormWebhookConfig = ({ form: formInst, formName, disabled,
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.notify.form.webhook_data.tooltip") }}></span>}
>
<Input.TextArea
allowClear
autoSize={{ minRows: 3, maxRows: 10 }}
<CodeInput
height="auto"
minHeight="64px"
maxHeight="256px"
language="json"
placeholder={t("workflow_node.notify.form.webhook_data.placeholder")}
onBlur={handleWebhookDataBlur}
/>

View File

@ -1,15 +1,14 @@
import { forwardRef, memo, useImperativeHandle } from "react";
import { useTranslation } from "react-i18next";
import { UploadOutlined as UploadOutlinedIcon } from "@ant-design/icons";
import { Button, Form, type FormInstance, Input, Upload, type UploadProps } from "antd";
import { Form, type FormInstance, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { validateCertificate, validatePrivateKey } from "@/api/certificates";
import TextFileInput from "@/components/TextFileInput";
import { type WorkflowNodeConfigForUpload } from "@/domain/workflow";
import { useAntdForm } from "@/hooks";
import { getErrMsg } from "@/utils/error";
import { readFileContent } from "@/utils/file";
type UploadNodeConfigFormFieldValues = Partial<WorkflowNodeConfigForUpload>;
@ -70,65 +69,53 @@ const UploadNodeConfigForm = forwardRef<UploadNodeConfigFormInstance, UploadNode
} as UploadNodeConfigFormInstance;
});
const handleCertificateFileChange: UploadProps["onChange"] = async ({ file }) => {
if (file && file.status !== "removed") {
const certificate = await readFileContent(file.originFileObj ?? (file as unknown as File));
try {
const resp = await validateCertificate(certificate);
formInst.setFields([
{
name: "domains",
value: resp.data.domains,
},
{
name: "certificate",
value: certificate,
},
]);
} catch (e) {
formInst.setFields([
{
name: "domains",
value: "",
},
{
name: "certificate",
value: "",
errors: [getErrMsg(e)],
},
]);
}
} else {
formInst.setFieldValue("certificate", "");
const handleCertificateChange = async (value: string) => {
try {
const resp = await validateCertificate(value);
formInst.setFields([
{
name: "domains",
value: resp.data.domains,
},
{
name: "certificate",
value: value,
},
]);
} catch (e) {
formInst.setFields([
{
name: "domains",
value: "",
},
{
name: "certificate",
value: value,
errors: [getErrMsg(e)],
},
]);
}
onValuesChange?.(formInst.getFieldsValue(true));
};
const handlePrivateKeyFileChange: UploadProps["onChange"] = async ({ file }) => {
if (file && file.status !== "removed") {
const privateKey = await readFileContent(file.originFileObj ?? (file as unknown as File));
try {
await validatePrivateKey(privateKey);
formInst.setFields([
{
name: "privateKey",
value: privateKey,
},
]);
} catch (e) {
formInst.setFields([
{
name: "privateKey",
value: "",
errors: [getErrMsg(e)],
},
]);
}
} else {
formInst.setFieldValue("privateKey", "");
const handlePrivateKeyChange = async (value: string) => {
try {
await validatePrivateKey(value);
formInst.setFields([
{
name: "privateKey",
value: value,
},
]);
} catch (e) {
formInst.setFields([
{
name: "privateKey",
value: value,
errors: [getErrMsg(e)],
},
]);
}
onValuesChange?.(formInst.getFieldsValue(true));
@ -141,23 +128,19 @@ const UploadNodeConfigForm = forwardRef<UploadNodeConfigFormInstance, UploadNode
</Form.Item>
<Form.Item name="certificate" label={t("workflow_node.upload.form.certificate.label")} rules={[formRule]}>
<Input.TextArea readOnly autoSize={{ minRows: 5, maxRows: 5 }} placeholder={t("workflow_node.upload.form.certificate.placeholder")} />
</Form.Item>
<Form.Item>
<Upload beforeUpload={() => false} maxCount={1} onChange={handleCertificateFileChange}>
<Button icon={<UploadOutlinedIcon />}>{t("workflow_node.upload.form.certificate.button")}</Button>
</Upload>
<TextFileInput
autoSize={{ minRows: 3, maxRows: 10 }}
placeholder={t("workflow_node.upload.form.certificate.placeholder")}
onChange={handleCertificateChange}
/>
</Form.Item>
<Form.Item name="privateKey" label={t("workflow_node.upload.form.private_key.label")} rules={[formRule]}>
<Input.TextArea readOnly autoSize={{ minRows: 5, maxRows: 5 }} placeholder={t("workflow_node.upload.form.private_key.placeholder")} />
</Form.Item>
<Form.Item>
<Upload beforeUpload={() => false} maxCount={1} onChange={handlePrivateKeyFileChange}>
<Button icon={<UploadOutlinedIcon />}>{t("workflow_node.upload.form.private_key.button")}</Button>
</Upload>
<TextFileInput
autoSize={{ minRows: 3, maxRows: 10 }}
placeholder={t("workflow_node.upload.form.private_key.placeholder")}
onChange={handlePrivateKeyChange}
/>
</Form.Item>
</Form>
);

View File

@ -41,6 +41,8 @@ export interface AccessModel extends BaseModel {
| AccessConfigForNamecheap
| AccessConfigForNameDotCom
| AccessConfigForNameSilo
| AccessConfigForNetcup
| AccessConfigForNetlify
| AccessConfigForPorkbun
| AccessConfigForPowerDNS
| AccessConfigForProxmoxVE
@ -199,6 +201,7 @@ export type AccessConfigForGoDaddy = {
export type AccessConfigForGoEdge = {
apiUrl: string;
apiRole: string;
accessKeyId: string;
accessKey: string;
allowInsecureConnections?: boolean;
@ -248,6 +251,16 @@ export type AccessConfigForNameSilo = {
apiKey: string;
};
export type AccessConfigForNetcup = {
customerNumber: string;
apiKey: string;
apiPassword: string;
};
export type AccessConfigForNetlify = {
apiToken: string;
};
export type AccessConfigForNS1 = {
apiKey: string;
};

View File

@ -43,6 +43,8 @@ export const ACCESS_PROVIDERS = Object.freeze({
NAMECHEAP: "namecheap",
NAMEDOTCOM: "namedotcom",
NAMESILO: "namesilo",
NETCUP: "netcup",
NETLIFY: "netlify",
NS1: "ns1",
PORKBUN: "porkbun",
POWERDNS: "powerdns",
@ -105,6 +107,7 @@ export const accessProvidersMap: Map<AccessProvider["type"] | string, AccessProv
[ACCESS_PROVIDERS.AZURE, "provider.azure", "/imgs/providers/azure.svg", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.BUNNY, "provider.bunny", "/imgs/providers/bunny.svg", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.GCORE, "provider.gcore", "/imgs/providers/gcore.png", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.NETLIFY, "provider.netlify", "/imgs/providers/netlify.png", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.RAINYUN, "provider.rainyun", "/imgs/providers/rainyun.svg", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.QINIU, "provider.qiniu", "/imgs/providers/qiniu.svg", [ACCESS_USAGES.HOSTING]],
@ -133,6 +136,7 @@ export const accessProvidersMap: Map<AccessProvider["type"] | string, AccessProv
[ACCESS_PROVIDERS.NAMECHEAP, "provider.namecheap", "/imgs/providers/namecheap.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.NAMEDOTCOM, "provider.namedotcom", "/imgs/providers/namedotcom.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.NAMESILO, "provider.namesilo", "/imgs/providers/namesilo.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.NETCUP, "provider.netcup", "/imgs/providers/netcup.png", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.NS1, "provider.ns1", "/imgs/providers/ns1.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.PORKBUN, "provider.porkbun", "/imgs/providers/porkbun.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.VERCEL, "provider.vercel", "/imgs/providers/vercel.svg", [ACCESS_USAGES.DNS]],
@ -249,6 +253,8 @@ export const ACME_DNS01_PROVIDERS = Object.freeze({
NAMECHEAP: `${ACCESS_PROVIDERS.NAMECHEAP}`,
NAMEDOTCOM: `${ACCESS_PROVIDERS.NAMEDOTCOM}`,
NAMESILO: `${ACCESS_PROVIDERS.NAMESILO}`,
NETCUP: `${ACCESS_PROVIDERS.NETCUP}`,
NETLIFY: `${ACCESS_PROVIDERS.NETLIFY}`,
NS1: `${ACCESS_PROVIDERS.NS1}`,
PORKBUN: `${ACCESS_PROVIDERS.PORKBUN}`,
POWERDNS: `${ACCESS_PROVIDERS.POWERDNS}`,
@ -299,6 +305,8 @@ export const acmeDns01ProvidersMap: Map<ACMEDns01Provider["type"] | string, ACME
[ACME_DNS01_PROVIDERS.NAMECHEAP, "provider.namecheap"],
[ACME_DNS01_PROVIDERS.NAMEDOTCOM, "provider.namedotcom"],
[ACME_DNS01_PROVIDERS.NAMESILO, "provider.namesilo"],
[ACME_DNS01_PROVIDERS.NETCUP, "provider.netcup"],
[ACME_DNS01_PROVIDERS.NETLIFY, "provider.netlify"],
[ACME_DNS01_PROVIDERS.NS1, "provider.ns1"],
[ACME_DNS01_PROVIDERS.PORKBUN, "provider.porkbun"],
[ACME_DNS01_PROVIDERS.VERCEL, "provider.vercel"],
@ -370,6 +378,7 @@ export const DEPLOYMENT_PROVIDERS = Object.freeze({
JDCLOUD_VOD: `${ACCESS_PROVIDERS.JDCLOUD}-vod`,
KUBERNETES_SECRET: `${ACCESS_PROVIDERS.KUBERNETES}-secret`,
LOCAL: `${ACCESS_PROVIDERS.LOCAL}`,
NETLIFY_SITE: `${ACCESS_PROVIDERS.NETLIFY}-site`,
PROXMOXVE: `${ACCESS_PROVIDERS.PROXMOXVE}`,
QINIU_CDN: `${ACCESS_PROVIDERS.QINIU}-cdn`,
QINIU_KODO: `${ACCESS_PROVIDERS.QINIU}-kodo`,
@ -505,6 +514,7 @@ export const deploymentProvidersMap: Map<DeploymentProvider["type"] | string, De
[DEPLOYMENT_PROVIDERS.CACHEFLY, "provider.cachefly", DEPLOYMENT_CATEGORIES.CDN],
[DEPLOYMENT_PROVIDERS.CDNFLY, "provider.cdnfly", DEPLOYMENT_CATEGORIES.CDN],
[DEPLOYMENT_PROVIDERS.EDGIO_APPLICATIONS, "provider.edgio.applications", DEPLOYMENT_CATEGORIES.WEBSITE],
[DEPLOYMENT_PROVIDERS.NETLIFY_SITE, "provider.netlify.site", DEPLOYMENT_CATEGORIES.WEBSITE],
[DEPLOYMENT_PROVIDERS.GCORE_CDN, "provider.gcore.cdn", DEPLOYMENT_CATEGORIES.CDN],
[DEPLOYMENT_PROVIDERS.GOEDGE, "provider.goedge", DEPLOYMENT_CATEGORIES.CDN],
[DEPLOYMENT_PROVIDERS["1PANEL_SITE"], "provider.1panel.site", DEPLOYMENT_CATEGORIES.WEBSITE],

View File

@ -1 +1 @@
export const version = "v0.3.11";
export const version = "v0.3.12";

Some files were not shown because too many files have changed in this diff Show More