Compare commits

...

27 Commits

Author SHA1 Message Date
yoan
0daa9f1882 v0.2.21 2024-11-19 09:07:08 +08:00
yoan
56886dcfe9 Merge branch 'LeoChen98-fix-reapply-when-domain-list-changed' 2024-11-18 20:03:16 +08:00
yoan
81e1e4a7ff validity duration 2024-11-18 20:03:11 +08:00
yoan
9b5256716f Merge branch 'fix-reapply-when-domain-list-changed' of github.com:LeoChen98/certimate into LeoChen98-fix-reapply-when-domain-list-changed 2024-11-18 19:58:36 +08:00
usual2970
446bf80f1d Merge pull request #346 from jarod/main
feat: add deployer BytePlus CDN
2024-11-18 19:43:58 +08:00
Jarod Liu
6a80455c6c fix: byteplus access provider 2024-11-18 10:51:51 +08:00
Jarod Liu
d1df088662 fix: 补充Provider Access 的 UI 实现 2024-11-16 09:52:28 +08:00
Jarod Liu
2b0f7aaf8a feat: add deployer BytePlus CDN 2024-11-16 09:18:58 +08:00
Leo Chen
3265dd76ab edit comments for the forward changes 2024-11-15 20:45:08 +08:00
Leo Chen
d1d7b44303 Invert the changed logic to match the function name 2024-11-15 20:37:36 +08:00
Leo Chen
56eced3813 Invert the boolean value to match the function name 2024-11-15 20:36:47 +08:00
yoan
c853f2976f v0.2.20 2024-11-15 08:07:37 +08:00
usual2970
b66931003f Merge pull request #342 from belier-cn/volcengine-cdn
feat: add volcengine cdn deployer
2024-11-15 08:05:27 +08:00
Leo Chen
9a75d2ac8f add key algorithm check 2024-11-15 00:33:09 +08:00
belier
42c5aea3f7 docs: update README_EN.md 2024-11-14 14:28:39 +08:00
belier
e2fd9c4cee style: modify variable name 2024-11-14 14:28:35 +08:00
belier
f847b7ff62 improvement: improve certificate fingerprint comparison 2024-11-14 14:19:00 +08:00
belier
9eae8f5077 feat: add volcengine cdn deployer 2024-11-14 13:39:23 +08:00
usual2970
2bacf76664 Merge pull request #339 from belier-cn/main
feat: add volcengine dns provider and add volcengine live deployer
2024-11-14 09:42:26 +08:00
usual2970
b2030caedc Merge pull request #337 from fudiwei/bugfix/syntax-error
fix switch-case syntax error
2024-11-14 09:35:58 +08:00
usual2970
956c975c6d Merge pull request #333 from JiangJamm/feat/notify_setting_expand
feat: 使系统设置中的消息推送设置列表打开后能够关闭
2024-11-14 09:34:52 +08:00
Leo Chen
41bd321a4f fixed: not reapply when domain list changed
fixed #334
2024-11-13 18:52:29 +08:00
Leo Chen
952e9687d0 fix misspelling var name 2024-11-13 17:58:56 +08:00
belier
c298f8b952 docs: Add Volcengine Information to README.md 2024-11-13 16:18:04 +08:00
belier
e2562a5251 feat: add volcengine dns provider and add volcengine live deployer 2024-11-13 15:36:46 +08:00
Fu Diwei
dbdb40baf9 fix: fix switch-case syntax error 2024-11-13 13:44:44 +08:00
JiangJamm
1b899575e0 feat: 使系统设置中的消息推送设置列表打开后能够关闭 2024-11-13 01:10:26 +08:00
35 changed files with 2734 additions and 405 deletions

View File

@@ -79,6 +79,7 @@ make local.run
| 华为云 | √ | √ | 可签发在华为云注册的域名;可部署到华为云 CDN、ELB |
| 七牛云 | | √ | 可部署到七牛云 CDN |
| 多吉云 | | √ | 可部署到多吉云 CDN |
| 火山引擎 | √ | √ | 可签发在火山引擎注册的域名;可部署到火山引擎 Live、CDN |
| AWS | √ | | 可签发在 AWS Route53 托管的域名 |
| CloudFlare | √ | | 可签发在 CloudFlare 注册的域名CloudFlare 服务自带 SSL 证书 |
| GoDaddy | √ | | 可签发在 GoDaddy 注册的域名 |

View File

@@ -71,13 +71,14 @@ password1234567890
## List of Supported Providers
| Provider | Registration | Deployment | Remarks |
| :-----------: | :----------: | :--------: | ----------------------------------------------------------------------------------------------------------- |
| :-----------: | :----------: | :--------: |-------------------------------------------------------------------------------------------------------------|
| Alibaba Cloud | √ | √ | Supports domains registered on Alibaba Cloud; supports deployment to Alibaba Cloud OSS, CDN,SLB |
| Tencent Cloud | √ | √ | Supports domains registered on Tencent Cloud; supports deployment to Tencent Cloud COS, CDN, ECDN, CLB, TEO |
| Baidu Cloud | | √ | Supports deployment to Baidu Cloud CDN |
| Huawei Cloud | √ | √ | Supports domains registered on Huawei Cloud; supports deployment to Huawei Cloud CDN, ELB |
| Qiniu Cloud | | √ | Supports deployment to Qiniu Cloud CDN |
| Doge Cloud | | √ | Supports deployment to Doge Cloud CDN |
| Volcengine | √ | √ | Supports domains registered on Volcengine; supports deployment to Volcengine Live, CDN |
| AWS | √ | | Supports domains managed on AWS Route53 |
| CloudFlare | √ | | Supports domains registered on CloudFlare; CloudFlare services come with SSL certificates |
| GoDaddy | √ | | Supports domains registered on GoDaddy |

56
go.mod
View File

@@ -14,9 +14,9 @@ require (
github.com/alibabacloud-go/tea v1.2.2
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
github.com/baidubce/bce-sdk-go v0.9.197
github.com/go-acme/lego/v4 v4.19.2
github.com/go-acme/lego/v4 v4.20.2
github.com/gojek/heimdall/v7 v7.0.3
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.114
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.120
github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61
github.com/nikoksr/notify v1.0.0
github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0
@@ -26,9 +26,10 @@ require (
github.com/qiniu/go-sdk/v7 v7.22.0
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1017
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.0.1031
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1031
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1034
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.992
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo v1.0.1030
github.com/volcengine/volc-sdk-golang v1.0.184
golang.org/x/crypto v0.28.0
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
k8s.io/api v0.31.1
@@ -43,8 +44,9 @@ require (
github.com/alibabacloud-go/tea-oss-sdk v1.1.3 // indirect
github.com/alibabacloud-go/tea-oss-utils v1.1.0 // indirect
github.com/alibabacloud-go/tea-utils/v2 v2.0.6 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.43.2 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.46.0 // indirect
github.com/blinkbean/dingtalk v1.1.3 // indirect
github.com/byteplus-sdk/byteplus-sdk-golang v1.0.35 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-lark/lark v1.14.1 // indirect
@@ -86,31 +88,31 @@ require (
github.com/alibabacloud-go/openapi-util v0.1.0 // indirect
github.com/alibabacloud-go/tea-utils v1.4.5 // indirect
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.63.15 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.63.47 // indirect
github.com/aliyun/credentials-go v1.3.10 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go-v2 v1.30.5 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 // indirect
github.com/aws/aws-sdk-go-v2/config v1.27.33 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.32 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 // indirect
github.com/aws/aws-sdk-go-v2 v1.32.3 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 // indirect
github.com/aws/aws-sdk-go-v2/config v1.28.1 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.42 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.18 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.8 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.22 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.22 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.19 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.17 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.61.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 // indirect
github.com/aws/smithy-go v1.20.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.22 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.3 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.66.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.24.3 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.3 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.32.3 // indirect
github.com/aws/smithy-go v1.22.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/clbanning/mxj/v2 v2.5.6 // indirect
github.com/cloudflare/cloudflare-go v0.104.0 // indirect
github.com/cloudflare/cloudflare-go v0.108.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/disintegration/imaging v1.6.2 // indirect
github.com/domodwyer/mailyak/v3 v3.6.2
@@ -122,7 +124,7 @@ require (
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/gojek/valkyrie v0.0.0-20180215180059-6aee720afcdf // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
@@ -150,7 +152,7 @@ require (
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1034 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
@@ -167,8 +169,8 @@ require (
golang.org/x/time v0.7.0 // indirect
golang.org/x/tools v0.25.0 // indirect
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
google.golang.org/api v0.202.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect
google.golang.org/api v0.204.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect
google.golang.org/grpc v1.67.1 // indirect
google.golang.org/protobuf v1.35.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect

700
go.sum
View File

File diff suppressed because it is too large Load Diff

View File

@@ -35,6 +35,7 @@ const (
configTypeGodaddy = "godaddy"
configTypePdns = "pdns"
configTypeHttpreq = "httpreq"
configTypeVolcengine = "volcengine"
)
const defaultSSLProvider = "letsencrypt"
@@ -188,6 +189,8 @@ func Get(record *models.Record) (Applicant, error) {
return NewPdns(option), nil
case configTypeHttpreq:
return NewHttpreq(option), nil
case configTypeVolcengine:
return NewVolcengine(option), nil
default:
return nil, errors.New("unknown config type")
}

View File

@@ -0,0 +1,35 @@
package applicant
import (
"encoding/json"
"fmt"
"os"
volcengineDns "github.com/go-acme/lego/v4/providers/dns/volcengine"
"github.com/usual2970/certimate/internal/domain"
)
type volcengine struct {
option *ApplyOption
}
func NewVolcengine(option *ApplyOption) Applicant {
return &volcengine{
option: option,
}
}
func (a *volcengine) Apply() (*Certificate, error) {
access := &domain.VolcengineAccess{}
json.Unmarshal([]byte(a.option.Access), access)
os.Setenv("VOLC_ACCESSKEY", access.AccessKeyID)
os.Setenv("VOLC_SECRETKEY", access.SecretAccessKey)
os.Setenv("VOLC_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
dnsProvider, err := volcengineDns.NewDNSProvider()
if err != nil {
return nil, err
}
return apply(a.option, dnsProvider)
}

View File

@@ -93,10 +93,11 @@ func (d *AliyunCLBDeployer) createSdkClient(accessKeyId, accessKeySecret, region
var endpoint string
switch region {
case "cn-hangzhou":
case "cn-hangzhou-finance":
case "cn-shanghai-finance-1":
case "cn-shenzhen-finance-1":
case
"cn-hangzhou",
"cn-hangzhou-finance",
"cn-shanghai-finance-1",
"cn-shenzhen-finance-1":
endpoint = "slb.aliyuncs.com"
default:
endpoint = fmt.Sprintf("slb.%s.aliyuncs.com", region)

View File

@@ -0,0 +1,116 @@
package deployer
import (
"context"
"encoding/json"
"fmt"
"strings"
bytepluscdn "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/byteplus-cdn"
"github.com/byteplus-sdk/byteplus-sdk-golang/service/cdn"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
)
type ByteplusCDNDeployer struct {
option *DeployerOption
infos []string
sdkClient *cdn.CDN
sslUploader uploader.Uploader
}
func NewByteplusCDNDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.ByteplusAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client := cdn.NewInstance()
client.Client.SetAccessKey(access.AccessKey)
client.Client.SetSecretKey(access.SecretKey)
uploader, err := bytepluscdn.New(&bytepluscdn.ByteplusCDNUploaderConfig{
AccessKey: access.AccessKey,
SecretKey: access.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &ByteplusCDNDeployer{
option: option,
infos: make([]string, 0),
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *ByteplusCDNDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *ByteplusCDNDeployer) GetInfos() []string {
return d.infos
}
func (d *ByteplusCDNDeployer) Deploy(ctx context.Context) error {
apiCtx := context.Background()
// 上传证书
upres, err := d.sslUploader.Upload(apiCtx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
domains := make([]string, 0)
configDomain := d.option.DeployConfig.GetConfigAsString("domain")
if strings.HasPrefix(configDomain, "*.") {
// 获取证书可以部署的域名
// REF: https://docs.byteplus.com/en/docs/byteplus-cdn/reference-describecertconfig-9ea17
describeCertConfigReq := &cdn.DescribeCertConfigRequest{
CertId: upres.CertId,
}
describeCertConfigResp, err := d.sdkClient.DescribeCertConfig(describeCertConfigReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeCertConfig'")
}
for i := range describeCertConfigResp.Result.CertNotConfig {
// 当前未启用 HTTPS 的加速域名列表。
domains = append(domains, describeCertConfigResp.Result.CertNotConfig[i].Domain)
}
for i := range describeCertConfigResp.Result.OtherCertConfig {
// 已启用了 HTTPS 的加速域名列表。这些加速域名关联的证书不是您指定的证书。
domains = append(domains, describeCertConfigResp.Result.OtherCertConfig[i].Domain)
}
for i := range describeCertConfigResp.Result.SpecifiedCertConfig {
// 已启用了 HTTPS 的加速域名列表。这些加速域名关联了您指定的证书。
d.infos = append(d.infos, fmt.Sprintf("%s域名已配置该证书", describeCertConfigResp.Result.SpecifiedCertConfig[i].Domain))
}
if len(domains) == 0 {
if len(describeCertConfigResp.Result.SpecifiedCertConfig) > 0 {
// 所有匹配的域名都配置了该证书,跳过部署
return nil
} else {
return xerrors.Errorf("未查询到匹配的域名: %s", configDomain)
}
}
} else {
domains = append(domains, configDomain)
}
// 部署证书
// REF: https://github.com/byteplus-sdk/byteplus-sdk-golang/blob/master/service/cdn/api_list.go#L306
for i := range domains {
batchDeployCertReq := &cdn.BatchDeployCertRequest{
CertId: upres.CertId,
Domain: domains[i],
}
batchDeployCertResp, err := d.sdkClient.BatchDeployCert(batchDeployCertReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.BatchDeployCert'")
} else {
d.infos = append(d.infos, toStr(fmt.Sprintf("%s域名的证书已修改", domains[i]), batchDeployCertResp))
}
}
return nil
}

View File

@@ -40,6 +40,9 @@ const (
targetSSH = "ssh"
targetWebhook = "webhook"
targetK8sSecret = "k8s-secret"
targetVolcengineLive = "volcengine-live"
targetVolcengineCDN = "volcengine-cdn"
targetByteplusCDN = "byteplus-cdn"
)
type DeployerOption struct {
@@ -150,6 +153,12 @@ func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, dep
return NewWebhookDeployer(option)
case targetK8sSecret:
return NewK8sSecretDeployer(option)
case targetVolcengineLive:
return NewVolcengineLiveDeployer(option)
case targetVolcengineCDN:
return NewVolcengineCDNDeployer(option)
case targetByteplusCDN:
return NewByteplusCDNDeployer(option)
}
return nil, errors.New("unsupported deploy target")
}

View File

@@ -101,9 +101,9 @@ func (d *TencentCDNDeployer) Deploy(ctx context.Context) error {
}
temp := make([]string, 0)
for _, aliInstanceId := range tcInstanceIds {
if !slices.Contains(deployedDomains, aliInstanceId) {
temp = append(temp, aliInstanceId)
for _, tcInstanceId := range tcInstanceIds {
if !slices.Contains(deployedDomains, tcInstanceId) {
temp = append(temp, tcInstanceId)
}
}
tcInstanceIds = temp

View File

@@ -0,0 +1,116 @@
package deployer
import (
"context"
"encoding/json"
"fmt"
"strings"
volcenginecdn "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/volcengine-cdn"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
"github.com/volcengine/volc-sdk-golang/service/cdn"
)
type VolcengineCDNDeployer struct {
option *DeployerOption
infos []string
sdkClient *cdn.CDN
sslUploader uploader.Uploader
}
func NewVolcengineCDNDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.VolcengineAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client := cdn.NewInstance()
client.Client.SetAccessKey(access.AccessKeyID)
client.Client.SetSecretKey(access.SecretAccessKey)
uploader, err := volcenginecdn.New(&volcenginecdn.VolcengineCDNUploaderConfig{
AccessKeyId: access.AccessKeyID,
AccessKeySecret: access.SecretAccessKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &VolcengineCDNDeployer{
option: option,
infos: make([]string, 0),
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *VolcengineCDNDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *VolcengineCDNDeployer) GetInfos() []string {
return d.infos
}
func (d *VolcengineCDNDeployer) Deploy(ctx context.Context) error {
apiCtx := context.Background()
// 上传证书
upres, err := d.sslUploader.Upload(apiCtx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
domains := make([]string, 0)
configDomain := d.option.DeployConfig.GetConfigAsString("domain")
if strings.HasPrefix(configDomain, "*.") {
// 获取证书可以部署的域名
// REF: https://www.volcengine.com/docs/6454/125711
describeCertConfigReq := &cdn.DescribeCertConfigRequest{
CertId: upres.CertId,
}
describeCertConfigResp, err := d.sdkClient.DescribeCertConfig(describeCertConfigReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeCertConfig'")
}
for i := range describeCertConfigResp.Result.CertNotConfig {
// 当前未启用 HTTPS 的加速域名列表。
domains = append(domains, describeCertConfigResp.Result.CertNotConfig[i].Domain)
}
for i := range describeCertConfigResp.Result.OtherCertConfig {
// 已启用了 HTTPS 的加速域名列表。这些加速域名关联的证书不是您指定的证书。
domains = append(domains, describeCertConfigResp.Result.OtherCertConfig[i].Domain)
}
for i := range describeCertConfigResp.Result.SpecifiedCertConfig {
// 已启用了 HTTPS 的加速域名列表。这些加速域名关联了您指定的证书。
d.infos = append(d.infos, fmt.Sprintf("%s域名已配置该证书", describeCertConfigResp.Result.SpecifiedCertConfig[i].Domain))
}
if len(domains) == 0 {
if len(describeCertConfigResp.Result.SpecifiedCertConfig) > 0 {
// 所有匹配的域名都配置了该证书,跳过部署
return nil
} else {
return xerrors.Errorf("未查询到匹配的域名: %s", configDomain)
}
}
} else {
domains = append(domains, configDomain)
}
// 部署证书
// REF: https://www.volcengine.com/docs/6454/125712
for i := range domains {
batchDeployCertReq := &cdn.BatchDeployCertRequest{
CertId: upres.CertId,
Domain: domains[i],
}
batchDeployCertResp, err := d.sdkClient.BatchDeployCert(batchDeployCertReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.BatchDeployCert'")
} else {
d.infos = append(d.infos, toStr(fmt.Sprintf("%s域名的证书已修改", domains[i]), batchDeployCertResp))
}
}
return nil
}

View File

@@ -0,0 +1,148 @@
package deployer
import (
"context"
"encoding/json"
"fmt"
"regexp"
"strings"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
volcenginelive "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/volcengine-live"
"github.com/usual2970/certimate/internal/pkg/utils/cast"
"github.com/volcengine/volc-sdk-golang/base"
live "github.com/volcengine/volc-sdk-golang/service/live/v20230101"
)
type VolcengineLiveDeployer struct {
option *DeployerOption
infos []string
sdkClient *live.Live
sslUploader uploader.Uploader
}
func NewVolcengineLiveDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.VolcengineAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client := live.NewInstance()
client.SetCredential(base.Credentials{
AccessKeyID: access.AccessKeyID,
SecretAccessKey: access.SecretAccessKey,
})
uploader, err := volcenginelive.New(&volcenginelive.VolcengineLiveUploaderConfig{
AccessKeyId: access.AccessKeyID,
AccessKeySecret: access.SecretAccessKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &VolcengineLiveDeployer{
option: option,
infos: make([]string, 0),
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *VolcengineLiveDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *VolcengineLiveDeployer) GetInfos() []string {
return d.infos
}
func (d *VolcengineLiveDeployer) Deploy(ctx context.Context) error {
apiCtx := context.Background()
// 上传证书
upres, err := d.sslUploader.Upload(apiCtx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
domains := make([]string, 0)
configDomain := d.option.DeployConfig.GetConfigAsString("domain")
if strings.HasPrefix(configDomain, "*.") {
// 如果是泛域名,获取所有的域名并匹配
matchDomains, err := d.getDomainsByWildcardDomain(apiCtx, configDomain)
if err != nil {
d.infos = append(d.infos, toStr("获取域名列表失败", upres))
return xerrors.Wrap(err, "failed to execute sdk request 'live.ListDomainDetail'")
}
if len(matchDomains) == 0 {
return xerrors.Errorf("未查询到匹配的域名: %s", configDomain)
}
domains = matchDomains
} else {
domains = append(domains, configDomain)
}
// 部署证书
// REF: https://www.volcengine.com/docs/6469/1186278#%E7%BB%91%E5%AE%9A%E8%AF%81%E4%B9%A6d
for i := range domains {
bindCertReq := &live.BindCertBody{
ChainID: upres.CertId,
Domain: domains[i],
HTTPS: cast.BoolPtr(true),
}
bindCertResp, err := d.sdkClient.BindCert(apiCtx, bindCertReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'live.BindCert'")
} else {
d.infos = append(d.infos, toStr(fmt.Sprintf("%s域名的证书已修改", domains[i]), bindCertResp))
}
}
return nil
}
func (d *VolcengineLiveDeployer) getDomainsByWildcardDomain(ctx context.Context, wildcardDomain string) ([]string, error) {
pageNum := int32(1)
searchTotal := 0
domains := make([]string, 0)
for {
listDomainDetailReq := &live.ListDomainDetailBody{
PageNum: pageNum,
PageSize: 1000,
}
// 查询域名列表
// REF: https://www.volcengine.com/docs/6469/1186277#%E6%9F%A5%E8%AF%A2%E5%9F%9F%E5%90%8D%E5%88%97%E8%A1%A8
listDomainDetailResp, err := d.sdkClient.ListDomainDetail(ctx, listDomainDetailReq)
if err != nil {
return domains, err
}
if listDomainDetailResp.Result.DomainList != nil {
for _, item := range listDomainDetailResp.Result.DomainList {
if matchWildcardDomain(item.Domain, wildcardDomain) {
domains = append(domains, item.Domain)
}
}
}
searchTotal += len(listDomainDetailResp.Result.DomainList)
if int(listDomainDetailResp.Result.Total) > searchTotal {
pageNum++
} else {
break
}
}
return domains, nil
}
func matchWildcardDomain(domain, wildcardDomain string) bool {
if strings.HasPrefix(wildcardDomain, "*.") {
if "*."+domain == wildcardDomain {
return true
}
regexPattern := "^([a-zA-Z0-9_-]+)\\." + regexp.QuoteMeta(wildcardDomain[2:]) + "$"
regex := regexp.MustCompile(regexPattern)
return regex.MatchString(domain)
}
return domain == wildcardDomain
}

View File

@@ -5,6 +5,11 @@ type AliyunAccess struct {
AccessKeySecret string `json:"accessKeySecret"`
}
type ByteplusAccess struct {
AccessKey string
SecretKey string
}
type TencentAccess struct {
SecretId string `json:"secretId"`
SecretKey string `json:"secretKey"`
@@ -56,6 +61,11 @@ type PdnsAccess struct {
ApiKey string `json:"apiKey"`
}
type VolcengineAccess struct {
AccessKeyID string
SecretAccessKey string
}
type HttpreqAccess struct {
Endpoint string `json:"endpoint"`
Mode string `json:"mode"`

View File

@@ -2,14 +2,22 @@ package domains
import (
"context"
"crypto/ecdsa"
"crypto/rsa"
"fmt"
"strings"
"time"
"github.com/pocketbase/pocketbase/models"
"golang.org/x/exp/slices"
"github.com/usual2970/certimate/internal/applicant"
"github.com/usual2970/certimate/internal/deployer"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/utils/app"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
)
type Phase string
@@ -20,6 +28,8 @@ const (
deployPhase Phase = "deploy"
)
const validityDuration = time.Hour * 24 * 10
func deploy(ctx context.Context, record *models.Record) error {
defer func() {
if r := recover(); r != nil {
@@ -45,7 +55,10 @@ func deploy(ctx context.Context, record *models.Record) error {
cert := currRecord.GetString("certificate")
expiredAt := currRecord.GetDateTime("expiredAt").Time()
if cert != "" && time.Until(expiredAt) > time.Hour*24*10 && currRecord.GetBool("deployed") {
// 检查证书是否包含设置的所有域名
changed := isCertChanged(cert, currRecord)
if cert != "" && time.Until(expiredAt) > validityDuration && currRecord.GetBool("deployed") && !changed {
app.GetApp().Logger().Info("证书在有效期内")
history.record(checkPhase, "证书在有效期内且已部署,跳过", &RecordInfo{
Info: []string{fmt.Sprintf("证书有效期至 %s", expiredAt.Format("2006-01-02"))},
@@ -60,7 +73,7 @@ func deploy(ctx context.Context, record *models.Record) error {
// ############2.申请证书
history.record(applyPhase, "开始申请", nil)
if cert != "" && time.Until(expiredAt) > time.Hour*24 {
if cert != "" && time.Until(expiredAt) > validityDuration && !changed {
history.record(applyPhase, "证书在有效期内,跳过", &RecordInfo{
Info: []string{fmt.Sprintf("证书有效期至 %s", expiredAt.Format("2006-01-02"))},
})
@@ -121,3 +134,80 @@ func deploy(ctx context.Context, record *models.Record) error {
return nil
}
func isCertChanged(certificate string, record *models.Record) bool {
// 如果证书为空直接返回true
if certificate == "" {
return true
}
// 解析证书
cert, err := x509.ParseCertificateFromPEM(certificate)
if err != nil {
app.GetApp().Logger().Error("解析证书失败", "err", err)
return true
}
// 遍历域名列表检查是否都在证书中找到第一个不存在证书中域名时提前返回true
for _, domain := range strings.Split(record.GetString("domain"), ";") {
if !slices.Contains(cert.DNSNames, domain) && !slices.Contains(cert.DNSNames, "*."+removeLastSubdomain(domain)) {
return true
}
}
// 解析applyConfig
applyConfig := &domain.ApplyConfig{}
record.UnmarshalJSONField("applyConfig", applyConfig)
// 检查证书加密算法是否变更
switch pubkey := cert.PublicKey.(type) {
case *rsa.PublicKey:
bitSize := pubkey.N.BitLen()
switch bitSize {
case 2048:
// RSA2048
if applyConfig.KeyAlgorithm != "" && applyConfig.KeyAlgorithm != "RSA2048" {
return true
}
case 3072:
// RSA3072
if applyConfig.KeyAlgorithm != "RSA3072" {
return true
}
case 4096:
// RSA4096
if applyConfig.KeyAlgorithm != "RSA4096" {
return true
}
case 8192:
// RSA8192
if applyConfig.KeyAlgorithm != "RSA8192" {
return true
}
}
case *ecdsa.PublicKey:
bitSize := pubkey.Curve.Params().BitSize
switch bitSize {
case 256:
// EC256
if applyConfig.KeyAlgorithm != "EC256" {
return true
}
case 384:
// EC384
if applyConfig.KeyAlgorithm != "EC384" {
return true
}
}
}
return false
}
func removeLastSubdomain(domain string) string {
parts := strings.Split(domain, ".")
if len(parts) > 1 {
return strings.Join(parts[1:], ".")
}
return domain
}

View File

@@ -128,10 +128,11 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunSlb.Cl
var endpoint string
switch region {
case "cn-hangzhou":
case "cn-hangzhou-finance":
case "cn-shanghai-finance-1":
case "cn-shenzhen-finance-1":
case
"cn-hangzhou",
"cn-hangzhou-finance",
"cn-shanghai-finance-1",
"cn-shenzhen-finance-1":
endpoint = "slb.aliyuncs.com"
default:
endpoint = fmt.Sprintf("slb.%s.aliyuncs.com", region)

View File

@@ -0,0 +1,111 @@
package bytepluscdn
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"strings"
"time"
"github.com/byteplus-sdk/byteplus-sdk-golang/service/cdn"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
)
type ByteplusCDNUploaderConfig struct {
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
}
type ByteplusCDNUploader struct {
config *ByteplusCDNUploaderConfig
sdkClient *cdn.CDN
}
var _ uploader.Uploader = (*ByteplusCDNUploader)(nil)
func New(config *ByteplusCDNUploaderConfig) (*ByteplusCDNUploader, error) {
if config == nil {
return nil, errors.New("config is nil")
}
instance := cdn.NewInstance()
client := instance.Client
client.SetAccessKey(config.AccessKey)
client.SetSecretKey(config.SecretKey)
return &ByteplusCDNUploader{
config: config,
sdkClient: instance,
}, nil
}
func (u *ByteplusCDNUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
// 解析证书内容
certX509, err := x509.ParseCertificateFromPEM(certPem)
if err != nil {
return nil, err
}
// 查询证书列表,避免重复上传
// REF: https://docs.byteplus.com/en/docs/byteplus-cdn/reference-listcertinfo
pageNum := int64(1)
pageSize := int64(100)
certSource := "cert_center"
listCertInfoReq := &cdn.ListCertInfoRequest{
PageNum: &pageNum,
PageSize: &pageSize,
Source: &certSource,
}
searchTotal := 0
for {
listCertInfoResp, err := u.sdkClient.ListCertInfo(listCertInfoReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.ListCertInfo'")
}
if listCertInfoResp.Result.CertInfo != nil {
for _, certDetail := range listCertInfoResp.Result.CertInfo {
hash := sha256.Sum256(certX509.Raw)
isSameCert := strings.EqualFold(hex.EncodeToString(hash[:]), certDetail.CertFingerprint.Sha256)
// 如果已存在相同证书,直接返回已有的证书信息
if isSameCert {
return &uploader.UploadResult{
CertId: certDetail.CertId,
CertName: certDetail.Desc,
}, nil
}
}
}
searchTotal += len(listCertInfoResp.Result.CertInfo)
if int(listCertInfoResp.Result.Total) > searchTotal {
pageNum++
} else {
break
}
}
var certId, certName string
certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
// 上传新证书
// REF: https://docs.byteplus.com/en/docs/byteplus-cdn/reference-addcertificate
addCertificateReq := &cdn.AddCertificateRequest{
Certificate: certPem,
PrivateKey: privkeyPem,
Source: &certSource,
Desc: &certName,
}
addCertificateResp, err := u.sdkClient.AddCertificate(addCertificateReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.AddCertificate'")
}
certId = addCertificateResp.Result.CertId
return &uploader.UploadResult{
CertId: certId,
CertName: certName,
}, nil
}

View File

@@ -0,0 +1,112 @@
package volcenginecdn
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"strings"
"time"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
"github.com/volcengine/volc-sdk-golang/service/cdn"
)
type VolcengineCDNUploaderConfig struct {
AccessKeyId string `json:"accessKeyId"`
AccessKeySecret string `json:"accessKeySecret"`
}
type VolcengineCDNUploader struct {
config *VolcengineCDNUploaderConfig
sdkClient *cdn.CDN
}
var _ uploader.Uploader = (*VolcengineCDNUploader)(nil)
func New(config *VolcengineCDNUploaderConfig) (*VolcengineCDNUploader, error) {
if config == nil {
return nil, errors.New("config is nil")
}
instance := cdn.NewInstance()
client := instance.Client
client.SetAccessKey(config.AccessKeyId)
client.SetSecretKey(config.AccessKeySecret)
return &VolcengineCDNUploader{
config: config,
sdkClient: instance,
}, nil
}
func (u *VolcengineCDNUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
// 解析证书内容
certX509, err := x509.ParseCertificateFromPEM(certPem)
if err != nil {
return nil, err
}
// 查询证书列表,避免重复上传
// REF: https://www.volcengine.com/docs/6454/125709
pageNum := int64(1)
pageSize := int64(100)
certSource := "volc_cert_center"
listCertInfoReq := &cdn.ListCertInfoRequest{
PageNum: &pageNum,
PageSize: &pageSize,
Source: certSource,
}
searchTotal := 0
for {
listCertInfoResp, err := u.sdkClient.ListCertInfo(listCertInfoReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.ListCertInfo'")
}
if listCertInfoResp.Result.CertInfo != nil {
for _, certDetail := range listCertInfoResp.Result.CertInfo {
hash := sha256.Sum256(certX509.Raw)
isSameCert := strings.EqualFold(hex.EncodeToString(hash[:]), certDetail.CertFingerprint.Sha256)
// 如果已存在相同证书,直接返回已有的证书信息
if isSameCert {
return &uploader.UploadResult{
CertId: certDetail.CertId,
CertName: certDetail.Desc,
}, nil
}
}
}
searchTotal += len(listCertInfoResp.Result.CertInfo)
if int(listCertInfoResp.Result.Total) > searchTotal {
pageNum++
} else {
break
}
}
// 生成新证书名(需符合火山引擎命名规则)
var certId, certName string
certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
// 上传新证书
// REF: https://www.volcengine.com/docs/6454/1245763
addCertificateReq := &cdn.AddCertificateRequest{
Certificate: certPem,
PrivateKey: privkeyPem,
Source: &certSource,
Desc: &certName,
}
addCertificateResp, err := u.sdkClient.AddCertificate(addCertificateReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.AddCertificate'")
}
certId = addCertificateResp.Result.CertId
return &uploader.UploadResult{
CertId: certId,
CertName: certName,
}, nil
}

View File

@@ -0,0 +1,116 @@
package volcenginelive
import (
"context"
"errors"
"fmt"
"strings"
"time"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
"github.com/usual2970/certimate/internal/pkg/utils/cast"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
live "github.com/volcengine/volc-sdk-golang/service/live/v20230101"
)
type VolcengineLiveUploaderConfig struct {
AccessKeyId string `json:"accessKeyId"`
AccessKeySecret string `json:"accessKeySecret"`
}
type VolcengineLiveUploader struct {
config *VolcengineLiveUploaderConfig
sdkClient *live.Live
}
var _ uploader.Uploader = (*VolcengineLiveUploader)(nil)
func New(config *VolcengineLiveUploaderConfig) (*VolcengineLiveUploader, error) {
if config == nil {
return nil, errors.New("config is nil")
}
client := live.NewInstance()
client.SetAccessKey(config.AccessKeyId)
client.SetSecretKey(config.AccessKeySecret)
return &VolcengineLiveUploader{
config: config,
sdkClient: client,
}, nil
}
func (u *VolcengineLiveUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
// 解析证书内容
certX509, err := x509.ParseCertificateFromPEM(certPem)
if err != nil {
return nil, err
}
// 查询证书列表,避免重复上传
// REF: https://www.volcengine.com/docs/6469/1186278#%E6%9F%A5%E8%AF%A2%E8%AF%81%E4%B9%A6%E5%88%97%E8%A1%A8
listCertReq := &live.ListCertV2Body{}
listCertResp, err := u.sdkClient.ListCertV2(ctx, listCertReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'live.ListCertV2'")
}
if listCertResp.Result.CertList != nil {
for _, certDetail := range listCertResp.Result.CertList {
describeCertDetailSecretReq := &live.DescribeCertDetailSecretV2Body{
ChainID: cast.StringPtr(certDetail.ChainID),
}
// 查询证书详细信息
// REF: https://www.volcengine.com/docs/6469/1186278#%E6%9F%A5%E7%9C%8B%E8%AF%81%E4%B9%A6%E8%AF%A6%E6%83%85
describeCertDetailSecretResp, detailErr := u.sdkClient.DescribeCertDetailSecretV2(ctx, describeCertDetailSecretReq)
if detailErr != nil {
continue
}
var isSameCert bool
certificate := strings.Join(describeCertDetailSecretResp.Result.SSL.Chain, "\n\n")
if certificate == certPem {
isSameCert = true
} else {
cert, err := x509.ParseCertificateFromPEM(certificate)
if err != nil {
continue
}
isSameCert = x509.EqualCertificate(cert, certX509)
}
// 如果已存在相同证书,直接返回已有的证书信息
if isSameCert {
return &uploader.UploadResult{
CertId: certDetail.ChainID,
CertName: certDetail.CertName,
}, nil
}
}
}
// 生成新证书名(需符合火山引擎命名规则)
var certId, certName string
certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
// 上传新证书
// REF: https://www.volcengine.com/docs/6469/1186278#%E6%B7%BB%E5%8A%A0%E8%AF%81%E4%B9%A6
createCertReq := &live.CreateCertBody{
CertName: &certName,
UseWay: "https",
ProjectName: cast.StringPtr("default"),
Rsa: live.CreateCertBodyRsa{
Prikey: privkeyPem,
Pubkey: certPem,
},
}
createCertResp, err := u.sdkClient.CreateCert(ctx, createCertReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'live.CreateCert'")
}
certId = *createCertResp.Result.ChainID
return &uploader.UploadResult{
CertId: certId,
CertName: certName,
}, nil
}

View File

@@ -0,0 +1,105 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
m "github.com/pocketbase/pocketbase/migrations"
"github.com/pocketbase/pocketbase/models/schema"
)
func init() {
m.Register(func(db dbx.Builder) error {
dao := daos.New(db)
collection, err := dao.FindCollectionByNameOrId("4yzbv8urny5ja1e")
if err != nil {
return err
}
// update
edit_configType := &schema.SchemaField{}
if err := json.Unmarshal([]byte(`{
"system": false,
"id": "hwy7m03o",
"name": "configType",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"aliyun",
"tencent",
"huaweicloud",
"qiniu",
"aws",
"cloudflare",
"namesilo",
"godaddy",
"pdns",
"httpreq",
"local",
"ssh",
"webhook",
"k8s",
"baiducloud",
"dogecloud",
"volcengine"
]
}
}`), edit_configType); err != nil {
return err
}
collection.Schema.AddField(edit_configType)
return dao.SaveCollection(collection)
}, func(db dbx.Builder) error {
dao := daos.New(db)
collection, err := dao.FindCollectionByNameOrId("4yzbv8urny5ja1e")
if err != nil {
return err
}
// update
edit_configType := &schema.SchemaField{}
if err := json.Unmarshal([]byte(`{
"system": false,
"id": "hwy7m03o",
"name": "configType",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"aliyun",
"tencent",
"huaweicloud",
"qiniu",
"aws",
"cloudflare",
"namesilo",
"godaddy",
"pdns",
"httpreq",
"local",
"ssh",
"webhook",
"k8s",
"baiducloud",
"dogecloud"
]
}
}`), edit_configType); err != nil {
return err
}
collection.Schema.AddField(edit_configType)
return dao.SaveCollection(collection)
})
}

View File

@@ -0,0 +1,107 @@
package migrations
import (
"encoding/json"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/daos"
m "github.com/pocketbase/pocketbase/migrations"
"github.com/pocketbase/pocketbase/models/schema"
)
func init() {
m.Register(func(db dbx.Builder) error {
dao := daos.New(db)
collection, err := dao.FindCollectionByNameOrId("4yzbv8urny5ja1e")
if err != nil {
return err
}
// update
edit_configType := &schema.SchemaField{}
if err := json.Unmarshal([]byte(`{
"system": false,
"id": "hwy7m03o",
"name": "configType",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"aliyun",
"tencent",
"huaweicloud",
"qiniu",
"aws",
"cloudflare",
"namesilo",
"godaddy",
"pdns",
"httpreq",
"local",
"ssh",
"webhook",
"k8s",
"baiducloud",
"dogecloud",
"volcengine",
"byteplus"
]
}
}`), edit_configType); err != nil {
return err
}
collection.Schema.AddField(edit_configType)
return dao.SaveCollection(collection)
}, func(db dbx.Builder) error {
dao := daos.New(db)
collection, err := dao.FindCollectionByNameOrId("4yzbv8urny5ja1e")
if err != nil {
return err
}
// update
edit_configType := &schema.SchemaField{}
if err := json.Unmarshal([]byte(`{
"system": false,
"id": "hwy7m03o",
"name": "configType",
"type": "select",
"required": false,
"presentable": false,
"unique": false,
"options": {
"maxSelect": 1,
"values": [
"aliyun",
"tencent",
"huaweicloud",
"qiniu",
"aws",
"cloudflare",
"namesilo",
"godaddy",
"pdns",
"httpreq",
"local",
"ssh",
"webhook",
"k8s",
"baiducloud",
"dogecloud",
"volcengine"
]
}
}`), edit_configType); err != nil {
return err
}
collection.Schema.AddField(edit_configType)
return dao.SaveCollection(collection)
})
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="200" height="200">
<path d="M0 0 C0.33 12.87 0.66 25.74 1 39 C4.63 36.03 8.26 33.06 12 30 C12.99 29.67 13.98 29.34 15 29 C15 28.34 15 27.68 15 27 C16.6953125 25.4765625 16.6953125 25.4765625 19.125 23.625 C24.36818245 19.55364629 29.38002727 15.25969115 34.35205078 10.86181641 C38.78276777 7 38.78276777 7 41 7 C41 6.34 41 5.68 41 5 C45.35294118 1 45.35294118 1 49 1 C49.02312269 12.19573244 49.04091471 23.39145924 49.05181217 34.58721066 C49.05704116 39.78528115 49.0641391 44.98333536 49.07543945 50.18139648 C49.08626776 55.19342268 49.09227671 60.20543318 49.09487724 65.21747017 C49.09673159 67.13412282 49.1003524 69.05077457 49.10573006 70.96742058 C49.11293975 73.64228072 49.11399097 76.31707514 49.11352539 78.99194336 C49.11712067 79.7925116 49.12071594 80.59307983 49.12442017 81.41790771 C49.11405216 86.88594784 49.11405216 86.88594784 48 88 C46.23140807 88.09946453 44.45888549 88.13080217 42.6875 88.1328125 C41.61242187 88.13410156 40.53734375 88.13539063 39.4296875 88.13671875 C38.29789062 88.13285156 37.16609375 88.12898438 36 88.125 C34.86820312 88.12886719 33.73640625 88.13273437 32.5703125 88.13671875 C31.49523437 88.13542969 30.42015625 88.13414062 29.3125 88.1328125 C28.31863281 88.13168457 27.32476562 88.13055664 26.30078125 88.12939453 C24 88 24 88 23 87 C22.84174742 85.00217874 22.74880829 82.99911166 22.68359375 80.99609375 C22.64169922 79.78115234 22.59980469 78.56621094 22.55664062 77.31445312 C22.51732422 76.03505859 22.47800781 74.75566406 22.4375 73.4375 C22.39431641 72.15423828 22.35113281 70.87097656 22.30664062 69.54882812 C22.200152 66.36600147 22.09814038 63.18309326 22 60 C21.24074219 60.63808594 20.48148437 61.27617188 19.69921875 61.93359375 C18.70535156 62.75988281 17.71148437 63.58617187 16.6875 64.4375 C15.70136719 65.26121094 14.71523437 66.08492188 13.69921875 66.93359375 C11 69 11 69 8 70 C8 70.66 8 71.32 8 72 C6.51261765 73.35382149 4.95569449 74.63139886 3.375 75.875 C-1.2643158 79.58405196 -5.81455578 83.3707187 -10.3125 87.25 C-11.99985624 88.70517299 -13.68754171 90.15975891 -15.38134766 91.60742188 C-16.47861169 92.55143304 -17.5666265 93.50628271 -18.64599609 94.47070312 C-22.67291886 98 -22.67291886 98 -26 98 C-26 84.8 -26 71.6 -26 58 C-29.3 60.64 -32.6 63.28 -36 66 C-36.66 66 -37.32 66 -38 66 C-38 66.66 -38 67.32 -38 68 C-39.71332754 69.58533702 -41.49689047 71.09541215 -43.3125 72.5625 C-44.29863281 73.36816406 -45.28476563 74.17382812 -46.30078125 75.00390625 C-49 77 -49 77 -52 78 C-52 78.66 -52 79.32 -52 80 C-53.42720409 81.26928798 -54.91983776 82.46538593 -56.4375 83.625 C-60.85617551 87.04712316 -64.96051883 90.6632284 -68.9921875 94.53125 C-71 96 -71 96 -75 96 C-75.02312283 84.93465142 -75.04091478 73.86930855 -75.05181217 62.80394077 C-75.05704113 57.66640434 -75.06413901 52.52888437 -75.07543945 47.39135742 C-75.08626787 42.4376859 -75.09227674 37.48403026 -75.09487724 32.53034782 C-75.09673157 30.63602384 -75.10035234 28.74170076 -75.10573006 26.8473835 C-75.11293989 24.20365614 -75.11399097 21.55999529 -75.11352539 18.91625977 C-75.11712067 18.12504227 -75.12071594 17.33382477 -75.12442017 16.51863098 C-75.11405216 11.11405216 -75.11405216 11.11405216 -74 10 C-72.21928754 9.91273777 -70.4351686 9.89300959 -68.65234375 9.90234375 C-67.57275391 9.90556641 -66.49316406 9.90878906 -65.38085938 9.91210938 C-63.67639648 9.92467773 -63.67639648 9.92467773 -61.9375 9.9375 C-60.22723633 9.94426758 -60.22723633 9.94426758 -58.48242188 9.95117188 C-55.65489649 9.96300253 -52.8274736 9.9794859 -50 10 C-48.65893595 12.6821281 -48.78549581 14.88398032 -48.68359375 17.8828125 C-48.64169922 19.04941406 -48.59980469 20.21601562 -48.55664062 21.41796875 C-48.51732422 22.64128906 -48.47800781 23.86460938 -48.4375 25.125 C-48.39431641 26.35605469 -48.35113281 27.58710937 -48.30664062 28.85546875 C-48.20031296 31.90352853 -48.09828219 34.9516719 -48 38 C-46.515 37.505 -46.515 37.505 -45 37 C-45 36.34 -45 35.68 -45 35 C-43.28667246 33.41466298 -41.50310953 31.90458785 -39.6875 30.4375 C-38.70136719 29.63183594 -37.71523437 28.82617188 -36.69921875 27.99609375 C-34 26 -34 26 -31 25 C-31 24.34 -31 23.68 -31 23 C-29.3046875 21.4765625 -29.3046875 21.4765625 -26.875 19.625 C-21.63181755 15.55364629 -16.61997273 11.25969115 -11.64794922 6.86181641 C-7.21723223 3 -7.21723223 3 -5 3 C-5 2.34 -5 1.68 -5 1 C-3 0 -3 0 0 0 Z " fill="#0260F9" transform="translate(113,51)"/>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="192" height="192">
<path d="M0 0 C1.32 0 2.64 0 4 0 C9.5316738 22.52529447 14.8267431 45.0974913 19.96630859 67.715271 C20.87688366 71.72007263 21.79014366 75.72426201 22.703125 79.72851562 C24.47126161 87.48510668 26.23661634 95.24232702 28 103 C28.16081055 102.28344238 28.32162109 101.56688477 28.48730469 100.82861328 C29.22431391 97.58987302 29.98678399 94.35766013 30.75 91.125 C31.12898438 89.43310547 31.12898438 89.43310547 31.515625 87.70703125 C31.90234375 86.09248047 31.90234375 86.09248047 32.296875 84.4453125 C32.52729492 83.45031738 32.75771484 82.45532227 32.99511719 81.43017578 C34 79 34 79 36.10644531 77.65966797 C36.73131836 77.44197754 37.35619141 77.22428711 38 77 C40.25565304 80.38347955 40.99634884 83.52624948 41.87939453 87.42138672 C42.14317902 88.5511644 42.14317902 88.5511644 42.41229248 89.70376587 C42.99201316 92.19434495 43.56265709 94.68690271 44.1328125 97.1796875 C44.533134 98.91228981 44.93384541 100.64480206 45.33493042 102.37722778 C46.17438801 106.00952966 47.00941838 109.64281323 47.84130859 113.27685547 C48.90714146 117.93143946 49.98338211 122.58354121 51.06289864 127.23496819 C51.89220185 130.81312389 52.71620818 134.39248313 53.53860092 137.97223282 C53.93334028 139.6877775 54.32966359 141.40295852 54.72760391 143.11776352 C55.28354421 145.5172615 55.83239226 147.91827521 56.37939453 150.31982422 C56.54456573 151.02649231 56.70973694 151.7331604 56.87991333 152.46124268 C57.68382252 156.02635542 58.2877538 159.34133935 58 163 C57 164 57 164 54.72175503 164.12304783 C53.70501737 164.12212604 52.68827971 164.12120424 51.64073181 164.12025452 C50.47820389 164.12162918 49.31567596 164.12300385 48.11791992 164.12442017 C46.83111328 164.12082489 45.54430664 164.11722961 44.21850586 164.11352539 C42.85876976 164.11324352 41.4990335 164.11340125 40.13929749 164.1139679 C36.43698222 164.11425587 32.73469908 164.10835912 29.03239131 164.10139394 C25.16731219 164.09516043 21.30223242 164.0945566 17.43714905 164.09336853 C10.11315318 164.09025521 2.78917216 164.0820418 -4.53481716 164.07201904 C-12.87083448 164.06085794 -21.20685249 164.05534746 -29.54287541 164.05032361 C-46.69525888 164.03985919 -63.84762787 164.02225973 -81 164 C-82.14187085 160.57438746 -81.82373034 159.34227454 -81.01196289 155.88598633 C-80.77163834 154.84513382 -80.53131378 153.80428131 -80.28370667 152.73188782 C-80.01606003 151.60993515 -79.74841339 150.48798248 -79.47265625 149.33203125 C-79.20488876 148.18208176 -78.93712128 147.03213226 -78.66123962 145.84733582 C-77.80250112 142.1672533 -76.93263711 138.48990227 -76.0625 134.8125 C-75.47908424 132.32113519 -74.89639264 129.82960068 -74.31445312 127.33789062 C-72.88470561 121.22326496 -71.44581004 115.11084638 -70 109 C-68.02 108.505 -68.02 108.505 -66 108 C-65.51891725 109.41601714 -65.04021859 110.83284437 -64.5625 112.25 C-64.29566406 113.03890625 -64.02882813 113.8278125 -63.75390625 114.640625 C-63.08016922 116.74910774 -62.52533639 118.8510768 -62 121 C-57.92546649 106.54247934 -54.67580491 91.88514666 -51.3125 77.25 C-50.42369645 73.38688228 -49.53364952 69.52405334 -48.64213562 65.66156006 C-48.09081632 63.27246643 -47.54096762 60.88303285 -46.99275208 58.4932251 C-46.7454686 57.42070496 -46.49818512 56.34818481 -46.2434082 55.24316406 C-46.02675003 54.30023499 -45.81009186 53.35730591 -45.58686829 52.38580322 C-45 50 -45 50 -44 47 C-43.01 46.67 -42.02 46.34 -41 46 C-38.23625561 48.76374439 -37.91156555 51.86361199 -37.08984375 55.5625 C-36.91747696 56.30959229 -36.74511017 57.05668457 -36.56752014 57.82641602 C-36.01731597 60.21609875 -35.47695373 62.60786961 -34.9375 65 C-34.39285742 67.39172265 -33.84616188 69.78294823 -33.29676819 72.17358398 C-32.95528025 73.66196219 -32.61684088 75.15104374 -32.28172302 76.64086914 C-31.44117319 80.42190439 -31.44117319 80.42190439 -30 84 C-28.35 77.07 -26.7 70.14 -25 63 C-23.02 62.505 -23.02 62.505 -21 62 C-19.68 64.97 -18.36 67.94 -17 71 C-16.80177139 70.12525528 -16.60354279 69.25051056 -16.39930725 68.34925842 C-14.53431568 60.12241124 -12.66583508 51.89636614 -10.79338932 43.6712122 C-9.83077597 39.44213487 -8.86953458 35.21275527 -7.91137695 30.98266602 C-6.98737224 26.90358604 -6.05983134 22.82532526 -5.12978935 18.74761772 C-4.77485423 17.18852063 -4.42121379 15.62912824 -4.06886482 14.06944466 C-3.57671669 11.89194568 -3.08005547 9.71552394 -2.58227539 7.53930664 C-2.30025772 6.29798477 -2.01824005 5.0566629 -1.72767639 3.77772522 C-1 1 -1 1 0 0 Z " fill="#1678FE" transform="translate(108,0)"/>
<path d="M0 0 C4.0123413 2.6748942 4.29118676 5.67680766 5.3671875 10.12890625 C5.56982208 10.95140366 5.77245667 11.77390106 5.98123169 12.62132263 C6.40631662 14.37247717 6.82449093 16.12532127 7.23632812 17.87963867 C7.86227451 20.50904418 8.53517407 23.12326358 9.2109375 25.74023438 C12.07068499 37.41556769 12.00448657 46.42095444 9 58 C8.72833008 59.1955249 8.45666016 60.3910498 8.17675781 61.62280273 C7.39051448 65.07318639 6.54720713 68.50474629 5.6875 71.9375 C5.07306448 74.4346121 4.45981035 76.93201514 3.84765625 79.4296875 C3.54102051 80.6784668 3.23438477 81.92724609 2.91845703 83.21386719 C1.17036632 90.41997851 -0.47868082 97.64885161 -2.1340332 104.87670898 C-2.48147583 106.39373413 -2.48147583 106.39373413 -2.8359375 107.94140625 C-3.03751465 108.82513916 -3.2390918 109.70887207 -3.44677734 110.61938477 C-3.94226379 112.75155085 -4.46909406 114.87637623 -5 117 C10.84 117 26.68 117 43 117 C43 117.33 43 117.66 43 118 C15.94 118 -11.12 118 -39 118 C-40.14187085 114.57438746 -39.82373034 113.34227454 -39.01196289 109.88598633 C-38.77163834 108.84513382 -38.53131378 107.80428131 -38.28370667 106.73188782 C-38.01606003 105.60993515 -37.74841339 104.48798248 -37.47265625 103.33203125 C-37.20488876 102.18208176 -36.93712128 101.03213226 -36.66123962 99.84733582 C-35.80250112 96.1672533 -34.93263711 92.48990227 -34.0625 88.8125 C-33.47908424 86.32113519 -32.89639264 83.82960068 -32.31445312 81.33789062 C-30.88470561 75.22326496 -29.44581004 69.11084638 -28 63 C-26.02 62.505 -26.02 62.505 -24 62 C-23.51891725 63.41601714 -23.04021859 64.83284437 -22.5625 66.25 C-22.29566406 67.03890625 -22.02882813 67.8278125 -21.75390625 68.640625 C-21.08016922 70.74910774 -20.52533639 72.8510768 -20 75 C-15.92546649 60.54247934 -12.67580491 45.88514666 -9.3125 31.25 C-8.42369645 27.38688228 -7.53364952 23.52405334 -6.64213562 19.66156006 C-6.09081632 17.27246643 -5.54096762 14.88303285 -4.99275208 12.4932251 C-4.7454686 11.42070496 -4.49818512 10.34818481 -4.2434082 9.24316406 C-4.02675003 8.30023499 -3.81009186 7.35730591 -3.58686829 6.38580322 C-3 4 -3 4 -2 1 C-1.34 0.67 -0.68 0.34 0 0 Z " fill="#167AFE" transform="translate(66,46)"/>
<path d="M0 0 C1.32 0.33 2.64 0.66 4 1 C4.46678965 2.58086094 4.9227585 4.16491593 5.375 5.75 C5.63023438 6.63171875 5.88546875 7.5134375 6.1484375 8.421875 C6.87542654 11.47656291 7.4139917 14.4642372 7.90234375 17.5625 C8.98028023 24.22106762 10.55043952 30.7099525 12.1875 37.25 C15.20528073 49.49873355 17.97533372 61.78769858 20.60229492 74.12597656 C22.01333501 80.7015831 23.52136706 87.18589313 25.45800781 93.62841797 C26 96 26 96 26 101 C9.83 101 -6.34 101 -23 101 C-22.38084899 96.04679193 -21.8086416 91.74373782 -20.6171875 87 C-20.34060303 85.88230225 -20.06401855 84.76460449 -19.77905273 83.61303711 C-19.48071533 82.42073486 -19.18237793 81.22843262 -18.875 80 C-16.25563065 69.34110325 -13.7678342 58.66334124 -11.4375 47.9375 C-11.29371918 47.27721802 -11.14993835 46.61693604 -11.00180054 45.93664551 C-9.55006847 39.26293849 -8.11885949 32.58512368 -6.71751404 25.90065002 C-6.30464964 23.93161018 -5.88977083 21.96299305 -5.47485352 19.99438477 C-4.96236238 17.55715172 -4.45477927 15.11887978 -3.95288086 12.67944336 C-3.72479736 11.5941333 -3.49671387 10.50882324 -3.26171875 9.390625 C-3.06537842 8.44219727 -2.86903809 7.49376953 -2.66674805 6.51660156 C-2.03222201 4.1216201 -1.19689606 2.16094777 0 0 Z " fill="#36E1E2" transform="translate(84,62)"/>
<path d="M0 0 C0.99 0.33 1.98 0.66 3 1 C4.03887939 3.83370972 4.03887939 3.83370972 4.95800781 7.59326172 C5.2097621 8.60251363 5.2097621 8.60251363 5.46660233 9.63215446 C5.99125051 11.75173458 6.49700713 13.87518769 7 16 C7.30627319 17.26249573 7.61254639 18.52499146 7.92810059 19.82574463 C8.86110039 23.71559829 9.77067925 27.61050619 10.671875 31.5078125 C11.0024033 32.9317254 11.33299132 34.35562444 11.66363525 35.7795105 C12.34771578 38.72856805 13.02972361 41.67809603 13.71044922 44.62792969 C14.58141761 48.40048291 15.45982697 52.17126278 16.33997726 55.94168282 C17.02142891 58.86620824 17.69792035 61.79186451 18.37302399 64.71786118 C18.69498969 66.1096897 19.01873077 67.50110884 19.3443222 68.89209366 C19.79692579 70.8280578 20.24189044 72.76580301 20.68652344 74.70361328 C20.94093201 75.80198517 21.19534058 76.90035706 21.4574585 78.03201294 C21.9607087 80.78505594 22.18590561 83.21129819 22 86 C21 87 21 87 19.23413086 87.11352539 C18.48526611 87.10828857 17.73640137 87.10305176 16.96484375 87.09765625 C16.15595703 87.09443359 15.34707031 87.09121094 14.51367188 87.08789062 C13.66353516 87.07951172 12.81339844 87.07113281 11.9375 87.0625 C11.08349609 87.05798828 10.22949219 87.05347656 9.34960938 87.04882812 C7.23304337 87.03700373 5.11651315 87.01906769 3 87 C3.02868164 85.97616211 3.05736328 84.95232422 3.08691406 83.89746094 C3.00469391 80.21048752 2.5113821 77.20197915 1.609375 73.640625 C1.31353516 72.45114258 1.01769531 71.26166016 0.71289062 70.03613281 C0.39513672 68.78670898 0.07738281 67.53728516 -0.25 66.25 C-0.89501094 63.64690845 -1.53300803 61.04210523 -2.171875 58.4375 C-2.48447266 57.1690625 -2.79707031 55.900625 -3.11914062 54.59375 C-4.28889622 49.82135467 -5.43093728 45.04596159 -6.5 40.25 C-6.67176758 39.53231445 -6.84353516 38.81462891 -7.02050781 38.07519531 C-8.95876389 29.16935201 -6.66000706 20.76440249 -4.5625 12.0625 C-4.34529297 11.11826172 -4.12808594 10.17402344 -3.90429688 9.20117188 C-3.68966797 8.30076172 -3.47503906 7.40035156 -3.25390625 6.47265625 C-3.06127197 5.66062744 -2.8686377 4.84859863 -2.67016602 4.01196289 C-2 2 -2 2 0 0 Z " fill="#37E1E2" transform="translate(144,77)"/>
<path d="M0 0 C0.99 0.33 1.98 0.66 3 1 C3.94921875 3.37109375 3.94921875 3.37109375 4.6875 6.4375 C4.93886719 7.42621094 5.19023438 8.41492188 5.44921875 9.43359375 C6.90509112 18.86062549 5.3631431 27.00072147 3.0625 36.1875 C0.61192077 46.01462243 0.61192077 46.01462243 -1 56 C-4.96 56 -8.92 56 -13 56 C-14.13719943 52.5884017 -13.83299816 51.34792773 -13.04370117 47.90185547 C-12.809832 46.86423065 -12.57596283 45.82660583 -12.33500671 44.75753784 C-12.07363419 43.63872223 -11.81226166 42.51990662 -11.54296875 41.3671875 C-11.27994461 40.21960968 -11.01692047 39.07203186 -10.7459259 37.88967896 C-10.18736857 35.46280808 -9.62473117 33.03687321 -9.05834961 30.61181641 C-8.19002923 26.88585293 -7.3393032 23.15617289 -6.49023438 19.42578125 C-5.94636766 17.07267254 -5.40148044 14.71979942 -4.85546875 12.3671875 C-4.60016891 11.24432343 -4.34486908 10.12145935 -4.08183289 8.96456909 C-3.83850723 7.9332486 -3.59518158 6.9019281 -3.34448242 5.83935547 C-3.13272873 4.92685028 -2.92097504 4.01434509 -2.70280457 3.07418823 C-2 1 -2 1 0 0 Z " fill="#38E1E2" transform="translate(40,108)"/>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,194 @@
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import z from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { ClientResponseError } from "pocketbase";
import { Input } from "@/components/ui/input";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Button } from "@/components/ui/button";
import { PbErrorData } from "@/domain/base";
import { accessProvidersMap, accessTypeFormSchema, type Access, type ByteplusConfig } from "@/domain/access";
import { save } from "@/repository/access";
import { useConfigContext } from "@/providers/config";
type AccessByteplusFormProps = {
op: "add" | "edit" | "copy";
data?: Access;
onAfterReq: () => void;
};
const AccessByteplusForm = ({ data, op, onAfterReq }: AccessByteplusFormProps) => {
const { addAccess, updateAccess } = useConfigContext();
const { t } = useTranslation();
const formSchema = z.object({
id: z.string().optional(),
name: z
.string()
.min(1, "access.authorization.form.name.placeholder")
.max(64, t("common.errmsg.string_max", { max: 64 })),
configType: accessTypeFormSchema,
accessKey: z
.string()
.min(1, "access.authorization.form.access_key.placeholder")
.max(64, t("common.errmsg.string_max", { max: 64 })),
secretKey: z
.string()
.min(1, "access.authorization.form.secret_key.placeholder")
.max(64, t("common.errmsg.string_max", { max: 64 })),
});
let config: ByteplusConfig = {
accessKey: "",
secretKey: "",
};
if (data) config = data.config as ByteplusConfig;
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data?.id,
name: data?.name || "",
configType: "byteplus",
accessKey: config.accessKey,
secretKey: config.secretKey,
},
});
const onSubmit = async (data: z.infer<typeof formSchema>) => {
const req: Access = {
id: data.id as string,
name: data.name,
configType: data.configType,
usage: accessProvidersMap.get(data.configType)!.usage,
config: {
accessKey: data.accessKey,
secretKey: data.secretKey,
},
};
try {
req.id = op == "copy" ? "" : req.id;
const rs = await save(req);
onAfterReq();
req.id = rs.id;
req.created = rs.created;
req.updated = rs.updated;
if (data.id && op == "edit") {
updateAccess(req);
return;
}
addAccess(req);
} catch (e) {
const err = e as ClientResponseError;
Object.entries(err.response.data as PbErrorData).forEach(([key, value]) => {
form.setError(key as keyof z.infer<typeof formSchema>, {
type: "manual",
message: value.message,
});
});
return;
}
};
return (
<>
<Form {...form}>
<form
onSubmit={(e) => {
e.stopPropagation();
form.handleSubmit(onSubmit)(e);
}}
className="space-y-8"
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t("access.authorization.form.name.label")}</FormLabel>
<FormControl>
<Input placeholder={t("access.authorization.form.name.placeholder")} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="id"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel>{t("access.authorization.form.config.label")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="configType"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel>{t("access.authorization.form.config.label")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="accessKey"
render={({ field }) => (
<FormItem>
<FormLabel>{t("access.authorization.form.access_key.label")}</FormLabel>
<FormControl>
<Input placeholder={t("access.authorization.form.access_key.placeholder")} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="secretKey"
render={({ field }) => (
<FormItem>
<FormLabel>{t("access.authorization.form.secret_key.label")}</FormLabel>
<FormControl>
<Input placeholder={t("access.authorization.form.secret_key.placeholder")} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
<div className="flex justify-end">
<Button type="submit">{t("common.save")}</Button>
</div>
</form>
</Form>
</>
);
};
export default AccessByteplusForm;

View File

@@ -21,6 +21,8 @@ import AccessLocalForm from "./AccessLocalForm";
import AccessSSHForm from "./AccessSSHForm";
import AccessWebhookForm from "./AccessWebhookForm";
import AccessKubernetesForm from "./AccessKubernetesForm";
import AccessVolcengineForm from "./AccessVolcengineForm";
import AccessByteplusForm from "./AccessByteplusForm";
import { Access } from "@/domain/access";
import { AccessTypeSelect } from "./AccessTypeSelect";
@@ -216,6 +218,28 @@ const AccessEditDialog = ({ trigger, op, data, className }: AccessEditProps) =>
/>
);
break;
case "volcengine":
childComponent = (
<AccessVolcengineForm
data={data}
op={op}
onAfterReq={() => {
setOpen(false);
}}
/>
);
break;
case "byteplus":
childComponent = (
<AccessByteplusForm
data={data}
op={op}
onAfterReq={() => {
setOpen(false);
}}
/>
);
break;
}
return (

View File

@@ -0,0 +1,194 @@
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import z from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { ClientResponseError } from "pocketbase";
import { Input } from "@/components/ui/input";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Button } from "@/components/ui/button";
import { PbErrorData } from "@/domain/base";
import { accessProvidersMap, accessTypeFormSchema, type Access, type VolcengineConfig } from "@/domain/access";
import { save } from "@/repository/access";
import { useConfigContext } from "@/providers/config";
type AccessVolcengineFormProps = {
op: "add" | "edit" | "copy";
data?: Access;
onAfterReq: () => void;
};
const AccessVolcengineForm = ({ data, op, onAfterReq }: AccessVolcengineFormProps) => {
const { addAccess, updateAccess } = useConfigContext();
const { t } = useTranslation();
const formSchema = z.object({
id: z.string().optional(),
name: z
.string()
.min(1, "access.authorization.form.name.placeholder")
.max(64, t("common.errmsg.string_max", { max: 64 })),
configType: accessTypeFormSchema,
accessKeyId: z
.string()
.min(1, "access.authorization.form.access_key_id.placeholder")
.max(64, t("common.errmsg.string_max", { max: 64 })),
secretAccessKey: z
.string()
.min(1, "access.authorization.form.secret_access_key.placeholder")
.max(64, t("common.errmsg.string_max", { max: 64 })),
});
let config: VolcengineConfig = {
accessKeyId: "",
secretAccessKey: "",
};
if (data) config = data.config as VolcengineConfig;
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
id: data?.id,
name: data?.name || "",
configType: "volcengine",
accessKeyId: config.accessKeyId,
secretAccessKey: config.secretAccessKey,
},
});
const onSubmit = async (data: z.infer<typeof formSchema>) => {
const req: Access = {
id: data.id as string,
name: data.name,
configType: data.configType,
usage: accessProvidersMap.get(data.configType)!.usage,
config: {
accessKeyId: data.accessKeyId,
secretAccessKey: data.secretAccessKey,
},
};
try {
req.id = op == "copy" ? "" : req.id;
const rs = await save(req);
onAfterReq();
req.id = rs.id;
req.created = rs.created;
req.updated = rs.updated;
if (data.id && op == "edit") {
updateAccess(req);
return;
}
addAccess(req);
} catch (e) {
const err = e as ClientResponseError;
Object.entries(err.response.data as PbErrorData).forEach(([key, value]) => {
form.setError(key as keyof z.infer<typeof formSchema>, {
type: "manual",
message: value.message,
});
});
return;
}
};
return (
<>
<Form {...form}>
<form
onSubmit={(e) => {
e.stopPropagation();
form.handleSubmit(onSubmit)(e);
}}
className="space-y-8"
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t("access.authorization.form.name.label")}</FormLabel>
<FormControl>
<Input placeholder={t("access.authorization.form.name.placeholder")} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="id"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel>{t("access.authorization.form.config.label")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="configType"
render={({ field }) => (
<FormItem className="hidden">
<FormLabel>{t("access.authorization.form.config.label")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="accessKeyId"
render={({ field }) => (
<FormItem>
<FormLabel>{t("access.authorization.form.access_key_id.label")}</FormLabel>
<FormControl>
<Input placeholder={t("access.authorization.form.access_key_id.placeholder")} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="secretAccessKey"
render={({ field }) => (
<FormItem>
<FormLabel>{t("access.authorization.form.secret_access_key.label")}</FormLabel>
<FormControl>
<Input placeholder={t("access.authorization.form.secret_access_key.placeholder")} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
<div className="flex justify-end">
<Button type="submit">{t("common.save")}</Button>
</div>
</form>
</Form>
</>
);
};
export default AccessVolcengineForm;

View File

@@ -1,297 +1,309 @@
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Plus } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "@/components/ui/select";
import { ScrollArea } from "@/components/ui/scroll-area";
import AccessEditDialog from "./AccessEditDialog";
import { Context as DeployEditContext, type DeployEditContext as DeployEditContextType } from "./DeployEdit";
import DeployToAliyunOSS from "./DeployToAliyunOSS";
import DeployToAliyunCDN from "./DeployToAliyunCDN";
import DeployToAliyunCLB from "./DeployToAliyunCLB";
import DeployToAliyunALB from "./DeployToAliyunALB";
import DeployToAliyunNLB from "./DeployToAliyunNLB";
import DeployToTencentCDN from "./DeployToTencentCDN";
import DeployToTencentCLB from "./DeployToTencentCLB";
import DeployToTencentCOS from "./DeployToTencentCOS";
import DeployToTencentTEO from "./DeployToTencentTEO";
import DeployToHuaweiCloudCDN from "./DeployToHuaweiCloudCDN";
import DeployToHuaweiCloudELB from "./DeployToHuaweiCloudELB";
import DeployToBaiduCloudCDN from "./DeployToBaiduCloudCDN";
import DeployToQiniuCDN from "./DeployToQiniuCDN";
import DeployToDogeCloudCDN from "./DeployToDogeCloudCDN";
import DeployToLocal from "./DeployToLocal";
import DeployToSSH from "./DeployToSSH";
import DeployToWebhook from "./DeployToWebhook";
import DeployToKubernetesSecret from "./DeployToKubernetesSecret";
import { deployTargetsMap, type DeployConfig } from "@/domain/domain";
import { accessProvidersMap } from "@/domain/access";
import { useConfigContext } from "@/providers/config";
type DeployEditDialogProps = {
trigger: React.ReactNode;
deployConfig?: DeployConfig;
onSave: (deploy: DeployConfig) => void;
};
const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogProps) => {
const { t } = useTranslation();
const {
config: { accesses },
} = useConfigContext();
const [deployType, setDeployType] = useState("");
const [locDeployConfig, setLocDeployConfig] = useState<DeployConfig>({
access: "",
type: "",
});
const [errors, setErrors] = useState<Record<string, string | undefined>>({});
const [open, setOpen] = useState(false);
useEffect(() => {
if (deployConfig) {
setLocDeployConfig({ ...deployConfig });
} else {
setLocDeployConfig({
access: "",
type: "",
});
}
}, [deployConfig]);
useEffect(() => {
setDeployType(locDeployConfig.type);
setErrors({});
}, [locDeployConfig.type]);
const setConfig = useCallback(
(deploy: DeployConfig) => {
if (deploy.type !== locDeployConfig.type) {
setLocDeployConfig({ ...deploy, access: "", config: {} });
} else {
setLocDeployConfig({ ...deploy });
}
},
[locDeployConfig.type]
);
const targetAccesses = accesses.filter((item) => {
if (item.usage == "apply") {
return false;
}
if (locDeployConfig.type == "") {
return true;
}
return item.configType === deployTargetsMap.get(locDeployConfig.type)?.provider;
});
const handleSaveClick = () => {
// 验证数据
const newError = { ...errors };
newError.type = locDeployConfig.type === "" ? t("domain.deployment.form.access.placeholder") : "";
newError.access = locDeployConfig.access === "" ? t("domain.deployment.form.access.placeholder") : "";
setErrors(newError);
if (Object.values(newError).some((e) => !!e)) return;
// 保存数据
onSave(locDeployConfig);
// 清理数据
setLocDeployConfig({
access: "",
type: "",
});
setErrors({});
// 关闭弹框
setOpen(false);
};
let childComponent = <></>;
switch (deployType) {
case "aliyun-oss":
childComponent = <DeployToAliyunOSS />;
break;
case "aliyun-cdn":
case "aliyun-dcdn":
childComponent = <DeployToAliyunCDN />;
break;
case "aliyun-clb":
childComponent = <DeployToAliyunCLB />;
break;
case "aliyun-alb":
childComponent = <DeployToAliyunALB />;
break;
case "aliyun-nlb":
childComponent = <DeployToAliyunNLB />;
break;
case "tencent-cdn":
case "tencent-ecdn":
childComponent = <DeployToTencentCDN />;
break;
case "tencent-clb":
childComponent = <DeployToTencentCLB />;
break;
case "tencent-cos":
childComponent = <DeployToTencentCOS />;
break;
case "tencent-teo":
childComponent = <DeployToTencentTEO />;
break;
case "huaweicloud-cdn":
childComponent = <DeployToHuaweiCloudCDN />;
break;
case "huaweicloud-elb":
childComponent = <DeployToHuaweiCloudELB />;
break;
case "baiducloud-cdn":
childComponent = <DeployToBaiduCloudCDN />;
break;
case "qiniu-cdn":
childComponent = <DeployToQiniuCDN />;
break;
case "dogecloud-cdn":
childComponent = <DeployToDogeCloudCDN />;
break;
case "local":
childComponent = <DeployToLocal />;
break;
case "ssh":
childComponent = <DeployToSSH />;
break;
case "webhook":
childComponent = <DeployToWebhook />;
break;
case "k8s-secret":
childComponent = <DeployToKubernetesSecret />;
break;
}
return (
<DeployEditContext.Provider
value={{
config: locDeployConfig as DeployEditContextType["config"],
setConfig: setConfig as DeployEditContextType["setConfig"],
errors: errors as DeployEditContextType["errors"],
setErrors: setErrors as DeployEditContextType["setErrors"],
}}
>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger>{trigger}</DialogTrigger>
<DialogContent
className="dark:text-stone-200"
onInteractOutside={(event) => {
event.preventDefault();
}}
>
<DialogHeader>
<DialogTitle>{t("domain.deployment.tab")}</DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<ScrollArea className="max-h-[80vh]">
<div className="container py-3">
{/* 部署方式 */}
<div>
<Label>{t("domain.deployment.form.type.label")}</Label>
<Select
value={locDeployConfig.type}
onValueChange={(val: string) => {
setConfig({ ...locDeployConfig, type: val });
}}
>
<SelectTrigger className="mt-2">
<SelectValue placeholder={t("domain.deployment.form.type.placeholder")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>{t("domain.deployment.form.type.list")}</SelectLabel>
{Array.from(deployTargetsMap.entries()).map(([key, target]) => (
<SelectItem key={key} value={key}>
<div className="flex items-center space-x-2">
<img className="w-6" src={target.icon} />
<div>{t(target.name)}</div>
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<div className="text-red-500 text-sm mt-1">{errors.type}</div>
</div>
{/* 授权配置 */}
<div className="mt-8">
<Label className="flex justify-between">
<div>{t("domain.deployment.form.access.label")}</div>
<AccessEditDialog
trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
<Plus size={14} />
{t("common.add")}
</div>
}
op="add"
/>
</Label>
<Select
value={locDeployConfig.access}
onValueChange={(val: string) => {
setConfig({ ...locDeployConfig, access: val });
}}
>
<SelectTrigger className="mt-2">
<SelectValue placeholder={t("domain.deployment.form.access.placeholder")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>{t("domain.deployment.form.access.list")}</SelectLabel>
{targetAccesses.map((item) => (
<SelectItem key={item.id} value={item.id}>
<div className="flex items-center space-x-2">
<img className="w-6" src={accessProvidersMap.get(item.configType)?.icon} />
<div>{item.name}</div>
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<div className="text-red-500 text-sm mt-1">{errors.access}</div>
</div>
{/* 其他参数 */}
<div className="mt-8">{childComponent}</div>
</div>
</ScrollArea>
<DialogFooter>
<Button
onClick={(e) => {
e.stopPropagation();
handleSaveClick();
}}
>
{t("common.save")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</DeployEditContext.Provider>
);
};
export default DeployEditDialog;
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Plus } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "@/components/ui/select";
import { ScrollArea } from "@/components/ui/scroll-area";
import AccessEditDialog from "./AccessEditDialog";
import { Context as DeployEditContext, type DeployEditContext as DeployEditContextType } from "./DeployEdit";
import DeployToAliyunOSS from "./DeployToAliyunOSS";
import DeployToAliyunCDN from "./DeployToAliyunCDN";
import DeployToAliyunCLB from "./DeployToAliyunCLB";
import DeployToAliyunALB from "./DeployToAliyunALB";
import DeployToAliyunNLB from "./DeployToAliyunNLB";
import DeployToTencentCDN from "./DeployToTencentCDN";
import DeployToTencentCLB from "./DeployToTencentCLB";
import DeployToTencentCOS from "./DeployToTencentCOS";
import DeployToTencentTEO from "./DeployToTencentTEO";
import DeployToHuaweiCloudCDN from "./DeployToHuaweiCloudCDN";
import DeployToHuaweiCloudELB from "./DeployToHuaweiCloudELB";
import DeployToBaiduCloudCDN from "./DeployToBaiduCloudCDN";
import DeployToQiniuCDN from "./DeployToQiniuCDN";
import DeployToDogeCloudCDN from "./DeployToDogeCloudCDN";
import DeployToLocal from "./DeployToLocal";
import DeployToSSH from "./DeployToSSH";
import DeployToWebhook from "./DeployToWebhook";
import DeployToKubernetesSecret from "./DeployToKubernetesSecret";
import DeployToVolcengineLive from "./DeployToVolcengineLive";
import DeployToVolcengineCDN from "./DeployToVolcengineCDN";
import DeployToByteplusCDN from "./DeployToByteplusCDN";
import { deployTargetsMap, type DeployConfig } from "@/domain/domain";
import { accessProvidersMap } from "@/domain/access";
import { useConfigContext } from "@/providers/config";
type DeployEditDialogProps = {
trigger: React.ReactNode;
deployConfig?: DeployConfig;
onSave: (deploy: DeployConfig) => void;
};
const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogProps) => {
const { t } = useTranslation();
const {
config: { accesses },
} = useConfigContext();
const [deployType, setDeployType] = useState("");
const [locDeployConfig, setLocDeployConfig] = useState<DeployConfig>({
access: "",
type: "",
});
const [errors, setErrors] = useState<Record<string, string | undefined>>({});
const [open, setOpen] = useState(false);
useEffect(() => {
if (deployConfig) {
setLocDeployConfig({ ...deployConfig });
} else {
setLocDeployConfig({
access: "",
type: "",
});
}
}, [deployConfig]);
useEffect(() => {
setDeployType(locDeployConfig.type);
setErrors({});
}, [locDeployConfig.type]);
const setConfig = useCallback(
(deploy: DeployConfig) => {
if (deploy.type !== locDeployConfig.type) {
setLocDeployConfig({ ...deploy, access: "", config: {} });
} else {
setLocDeployConfig({ ...deploy });
}
},
[locDeployConfig.type]
);
const targetAccesses = accesses.filter((item) => {
if (item.usage == "apply") {
return false;
}
if (locDeployConfig.type == "") {
return true;
}
return item.configType === deployTargetsMap.get(locDeployConfig.type)?.provider;
});
const handleSaveClick = () => {
// 验证数据
const newError = { ...errors };
newError.type = locDeployConfig.type === "" ? t("domain.deployment.form.access.placeholder") : "";
newError.access = locDeployConfig.access === "" ? t("domain.deployment.form.access.placeholder") : "";
setErrors(newError);
if (Object.values(newError).some((e) => !!e)) return;
// 保存数据
onSave(locDeployConfig);
// 清理数据
setLocDeployConfig({
access: "",
type: "",
});
setErrors({});
// 关闭弹框
setOpen(false);
};
let childComponent = <></>;
switch (deployType) {
case "aliyun-oss":
childComponent = <DeployToAliyunOSS />;
break;
case "aliyun-cdn":
case "aliyun-dcdn":
childComponent = <DeployToAliyunCDN />;
break;
case "aliyun-clb":
childComponent = <DeployToAliyunCLB />;
break;
case "aliyun-alb":
childComponent = <DeployToAliyunALB />;
break;
case "aliyun-nlb":
childComponent = <DeployToAliyunNLB />;
break;
case "tencent-cdn":
case "tencent-ecdn":
childComponent = <DeployToTencentCDN />;
break;
case "tencent-clb":
childComponent = <DeployToTencentCLB />;
break;
case "tencent-cos":
childComponent = <DeployToTencentCOS />;
break;
case "tencent-teo":
childComponent = <DeployToTencentTEO />;
break;
case "huaweicloud-cdn":
childComponent = <DeployToHuaweiCloudCDN />;
break;
case "huaweicloud-elb":
childComponent = <DeployToHuaweiCloudELB />;
break;
case "baiducloud-cdn":
childComponent = <DeployToBaiduCloudCDN />;
break;
case "qiniu-cdn":
childComponent = <DeployToQiniuCDN />;
break;
case "dogecloud-cdn":
childComponent = <DeployToDogeCloudCDN />;
break;
case "local":
childComponent = <DeployToLocal />;
break;
case "ssh":
childComponent = <DeployToSSH />;
break;
case "webhook":
childComponent = <DeployToWebhook />;
break;
case "k8s-secret":
childComponent = <DeployToKubernetesSecret />;
break;
case "volcengine-live":
childComponent = <DeployToVolcengineLive />;
break;
case "volcengine-cdn":
childComponent = <DeployToVolcengineCDN />;
break;
case "byteplus-cdn":
childComponent = <DeployToByteplusCDN />;
break;
}
return (
<DeployEditContext.Provider
value={{
config: locDeployConfig as DeployEditContextType["config"],
setConfig: setConfig as DeployEditContextType["setConfig"],
errors: errors as DeployEditContextType["errors"],
setErrors: setErrors as DeployEditContextType["setErrors"],
}}
>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger>{trigger}</DialogTrigger>
<DialogContent
className="dark:text-stone-200"
onInteractOutside={(event) => {
event.preventDefault();
}}
>
<DialogHeader>
<DialogTitle>{t("domain.deployment.tab")}</DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<ScrollArea className="max-h-[80vh]">
<div className="container py-3">
{/* 部署方式 */}
<div>
<Label>{t("domain.deployment.form.type.label")}</Label>
<Select
value={locDeployConfig.type}
onValueChange={(val: string) => {
setConfig({ ...locDeployConfig, type: val });
}}
>
<SelectTrigger className="mt-2">
<SelectValue placeholder={t("domain.deployment.form.type.placeholder")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>{t("domain.deployment.form.type.list")}</SelectLabel>
{Array.from(deployTargetsMap.entries()).map(([key, target]) => (
<SelectItem key={key} value={key}>
<div className="flex items-center space-x-2">
<img className="w-6" src={target.icon} />
<div>{t(target.name)}</div>
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<div className="text-red-500 text-sm mt-1">{errors.type}</div>
</div>
{/* 授权配置 */}
<div className="mt-8">
<Label className="flex justify-between">
<div>{t("domain.deployment.form.access.label")}</div>
<AccessEditDialog
trigger={
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
<Plus size={14} />
{t("common.add")}
</div>
}
op="add"
/>
</Label>
<Select
value={locDeployConfig.access}
onValueChange={(val: string) => {
setConfig({ ...locDeployConfig, access: val });
}}
>
<SelectTrigger className="mt-2">
<SelectValue placeholder={t("domain.deployment.form.access.placeholder")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>{t("domain.deployment.form.access.list")}</SelectLabel>
{targetAccesses.map((item) => (
<SelectItem key={item.id} value={item.id}>
<div className="flex items-center space-x-2">
<img className="w-6" src={accessProvidersMap.get(item.configType)?.icon} />
<div>{item.name}</div>
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<div className="text-red-500 text-sm mt-1">{errors.access}</div>
</div>
{/* 其他参数 */}
<div className="mt-8">{childComponent}</div>
</div>
</ScrollArea>
<DialogFooter>
<Button
onClick={(e) => {
e.stopPropagation();
handleSaveClick();
}}
>
{t("common.save")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</DeployEditContext.Provider>
);
};
export default DeployEditDialog;

View File

@@ -0,0 +1,68 @@
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { z } from "zod";
import { produce } from "immer";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useDeployEditContext } from "./DeployEdit";
type DeployToByteplusCDNConfigParams = {
domain?: string;
};
const DeployToByteplusCDN = () => {
const { t } = useTranslation();
const { config, setConfig, errors, setErrors } = useDeployEditContext<DeployToByteplusCDNConfigParams>();
useEffect(() => {
if (!config.id) {
setConfig({
...config,
config: {},
});
}
}, []);
useEffect(() => {
setErrors({});
}, []);
const formSchema = z.object({
domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
}),
});
useEffect(() => {
const res = formSchema.safeParse(config.config);
setErrors({
...errors,
domain: res.error?.errors?.find((e) => e.path[0] === "domain")?.message,
});
}, [config]);
return (
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.domain.label.wildsupported")}</Label>
<Input
placeholder={t("domain.deployment.form.domain.placeholder")}
className="w-full mt-1"
value={config?.config?.domain}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.domain = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.domain}</div>
</div>
</div>
);
};
export default DeployToByteplusCDN;

View File

@@ -0,0 +1,68 @@
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { z } from "zod";
import { produce } from "immer";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useDeployEditContext } from "./DeployEdit";
type DeployToVolcengineCDNConfigParams = {
domain?: string;
};
const DeployToVolcengineCDN = () => {
const { t } = useTranslation();
const { config, setConfig, errors, setErrors } = useDeployEditContext<DeployToVolcengineCDNConfigParams>();
useEffect(() => {
if (!config.id) {
setConfig({
...config,
config: {},
});
}
}, []);
useEffect(() => {
setErrors({});
}, []);
const formSchema = z.object({
domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
}),
});
useEffect(() => {
const res = formSchema.safeParse(config.config);
setErrors({
...errors,
domain: res.error?.errors?.find((e) => e.path[0] === "domain")?.message,
});
}, [config]);
return (
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.domain.label.wildsupported")}</Label>
<Input
placeholder={t("domain.deployment.form.domain.placeholder")}
className="w-full mt-1"
value={config?.config?.domain}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.domain = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.domain}</div>
</div>
</div>
);
};
export default DeployToVolcengineCDN;

View File

@@ -0,0 +1,68 @@
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { z } from "zod";
import { produce } from "immer";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useDeployEditContext } from "./DeployEdit";
type DeployToVolcengineLiveConfigParams = {
domain?: string;
};
const DeployToVolcengineLive = () => {
const { t } = useTranslation();
const { config, setConfig, errors, setErrors } = useDeployEditContext<DeployToVolcengineLiveConfigParams>();
useEffect(() => {
if (!config.id) {
setConfig({
...config,
config: {},
});
}
}, []);
useEffect(() => {
setErrors({});
}, []);
const formSchema = z.object({
domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
}),
});
useEffect(() => {
const res = formSchema.safeParse(config.config);
setErrors({
...errors,
domain: res.error?.errors?.find((e) => e.path[0] === "domain")?.message,
});
}, [config]);
return (
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.domain.label.wildsupported")}</Label>
<Input
placeholder={t("domain.deployment.form.domain.placeholder")}
className="w-full mt-1"
value={config?.config?.domain}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.domain = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.domain}</div>
</div>
</div>
);
};
export default DeployToVolcengineLive;

View File

@@ -18,6 +18,8 @@ export const accessProvidersMap: Map<AccessProvider["type"], AccessProvider> = n
["baiducloud", "common.provider.baiducloud", "/imgs/providers/baiducloud.svg", "all", "百度智能云:百度云:baidu cloud"],
["qiniu", "common.provider.qiniu", "/imgs/providers/qiniu.svg", "deploy", "七牛云:qiniu"],
["dogecloud", "common.provider.dogecloud", "/imgs/providers/dogecloud.svg", "deploy", "多吉云:doge cloud"],
["volcengine", "common.provider.volcengine", "/imgs/providers/volcengine.svg", "all", "火山引擎"],
["byteplus", "common.provider.byteplus", "/imgs/providers/byteplus.svg", "all", "BytePlus"],
["aws", "common.provider.aws", "/imgs/providers/aws.svg", "apply", "亚马逊:amazon:aws"],
["cloudflare", "common.provider.cloudflare", "/imgs/providers/cloudflare.svg", "apply", "cloudflare:cf:cloud flare"],
["namesilo", "common.provider.namesilo", "/imgs/providers/namesilo.svg", "apply", "namesilo"],
@@ -49,6 +51,8 @@ export const accessTypeFormSchema = z.union(
z.literal("ssh"),
z.literal("webhook"),
z.literal("k8s"),
z.literal("volcengine"),
z.literal("byteplus"),
],
{ message: "access.authorization.form.type.placeholder" }
);
@@ -74,7 +78,9 @@ export type Access = {
| LocalConfig
| SSHConfig
| WebhookConfig
| KubernetesConfig;
| KubernetesConfig
| VolcengineConfig
| ByteplusConfig;
deleted?: string;
created?: string;
updated?: string;
@@ -162,3 +168,13 @@ export type WebhookConfig = {
export type KubernetesConfig = {
kubeConfig: string;
};
export type VolcengineConfig = {
accessKeyId: string;
secretAccessKey: string;
};
export type ByteplusConfig = {
accessKey: string;
secretKey: string;
};

View File

@@ -92,5 +92,8 @@ export const deployTargetsMap: Map<DeployTarget["type"], DeployTarget> = new Map
["ssh", "common.provider.ssh", "/imgs/providers/ssh.svg"],
["webhook", "common.provider.webhook", "/imgs/providers/webhook.svg"],
["k8s-secret", "common.provider.kubernetes.secret", "/imgs/providers/k8s.svg"],
["volcengine-live", "common.provider.volcengine.live", "/imgs/providers/volcengine.svg"],
["volcengine-cdn", "common.provider.volcengine.cdn", "/imgs/providers/volcengine.svg"],
["byteplus-cdn", "common.provider.byteplus.cdn", "/imgs/providers/byteplus.svg"],
].map(([type, name, icon]) => [type, { type, provider: type.split("-")[0], name, icon }])
);

View File

@@ -1 +1 @@
export const version = "Certimate v0.2.19";
export const version = "Certimate v0.2.21";

View File

@@ -90,5 +90,10 @@
"common.provider.lark": "Lark",
"common.provider.telegram": "Telegram",
"common.provider.serverchan": "ServerChan",
"common.provider.bark": "Bark"
"common.provider.bark": "Bark",
"common.provider.volcengine": "Volcengine",
"common.provider.volcengine.live": "Volcengine - Live",
"common.provider.volcengine.cdn": "Volcengine - CDN",
"common.provider.byteplus": "BytePlus",
"common.provider.byteplus.cdn": "BytePlus - CDN"
}

View File

@@ -90,5 +90,10 @@
"common.provider.lark": "飞书",
"common.provider.telegram": "Telegram",
"common.provider.serverchan": "Server酱",
"common.provider.bark": "Bark"
"common.provider.bark": "Bark",
"common.provider.volcengine": "火山引擎",
"common.provider.volcengine.live": "火山引擎 - 视频直播",
"common.provider.volcengine.cdn": "火山引擎 - CDN",
"common.provider.byteplus": "BytePlus",
"common.provider.byteplus.cdn": "BytePlus - CDN"
}

View File

@@ -29,7 +29,7 @@ const Notify = () => {
</div>
<div className="border rounded-md p-5 mt-7 shadow-lg">
<Accordion type={"single"} className="dark:text-stone-200">
<Accordion type={"single"} collapsible={true} className="dark:text-stone-200">
<AccordionItem value="item-email" className="dark:border-stone-200">
<AccordionTrigger>{t("common.provider.email")}</AccordionTrigger>
<AccordionContent>