diff --git a/go.mod b/go.mod index 868236d5..c79d1c70 100644 --- a/go.mod +++ b/go.mod @@ -46,6 +46,7 @@ require ( github.com/alibabacloud-go/tea-utils/v2 v2.0.6 // 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 diff --git a/go.sum b/go.sum index c0b10af7..8f075f98 100644 --- a/go.sum +++ b/go.sum @@ -225,6 +225,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/blinkbean/dingtalk v1.1.3 h1:MbidFZYom7DTFHD/YIs+eaI7kRy52kmWE/sy0xjo6E4= github.com/blinkbean/dingtalk v1.1.3/go.mod h1:9BaLuGSBqY3vT5hstValh48DbsKO7vaHaJnG9pXwbto= +github.com/byteplus-sdk/byteplus-sdk-golang v1.0.35 h1:bM18V4iw9ylRc2LahQaq3k3gjEVJdyQYvptLVZaCa54= +github.com/byteplus-sdk/byteplus-sdk-golang v1.0.35/go.mod h1:7iCaE+dR9EycrJU0GQyMhptbInLbQhsKXiDKDjNi8Vs= github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c/go.mod h1:l/bIBLeOl9eX+wxJAzxS4TveKRtAqlyDpHjhkfO0MEI= github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= @@ -751,6 +753,7 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= @@ -1265,6 +1268,7 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= diff --git a/internal/deployer/byteplus_cdn.go b/internal/deployer/byteplus_cdn.go new file mode 100644 index 00000000..abf8f8b6 --- /dev/null +++ b/internal/deployer/byteplus_cdn.go @@ -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://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 +} diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index fbb582f5..23e4cb15 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -42,6 +42,7 @@ const ( targetK8sSecret = "k8s-secret" targetVolcengineLive = "volcengine-live" targetVolcengineCDN = "volcengine-cdn" + targetByteplusCDN = "byteplus-cdn" ) type DeployerOption struct { @@ -156,6 +157,8 @@ func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, dep return NewVolcengineLiveDeployer(option) case targetVolcengineCDN: return NewVolcengineCDNDeployer(option) + case targetByteplusCDN: + return NewByteplusCDNDeployer(option) } return nil, errors.New("unsupported deploy target") } diff --git a/internal/domain/access.go b/internal/domain/access.go index bf2515d3..98716ff4 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -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"` diff --git a/internal/pkg/core/uploader/providers/byteplus-cdn/byteplus_cdn.go b/internal/pkg/core/uploader/providers/byteplus-cdn/byteplus_cdn.go new file mode 100644 index 00000000..f27aebbc --- /dev/null +++ b/internal/pkg/core/uploader/providers/byteplus-cdn/byteplus_cdn.go @@ -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 +} diff --git a/ui/public/imgs/providers/byteplus.svg b/ui/public/imgs/providers/byteplus.svg new file mode 100644 index 00000000..66d692f8 --- /dev/null +++ b/ui/public/imgs/providers/byteplus.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/src/components/certimate/DeployEditDialog.tsx b/ui/src/components/certimate/DeployEditDialog.tsx index 3545db5f..2e7c42be 100644 --- a/ui/src/components/certimate/DeployEditDialog.tsx +++ b/ui/src/components/certimate/DeployEditDialog.tsx @@ -1,305 +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 DeployToVolcengineLive from "./DeployToVolcengineLive" -import DeployToVolcengineCDN from "./DeployToVolcengineCDN" -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({ - access: "", - type: "", - }); - - const [errors, setErrors] = useState>({}); - - 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 = ; - break; - case "aliyun-cdn": - case "aliyun-dcdn": - childComponent = ; - break; - case "aliyun-clb": - childComponent = ; - break; - case "aliyun-alb": - childComponent = ; - break; - case "aliyun-nlb": - childComponent = ; - break; - case "tencent-cdn": - case "tencent-ecdn": - childComponent = ; - break; - case "tencent-clb": - childComponent = ; - break; - case "tencent-cos": - childComponent = ; - break; - case "tencent-teo": - childComponent = ; - break; - case "huaweicloud-cdn": - childComponent = ; - break; - case "huaweicloud-elb": - childComponent = ; - break; - case "baiducloud-cdn": - childComponent = ; - break; - case "qiniu-cdn": - childComponent = ; - break; - case "dogecloud-cdn": - childComponent = ; - break; - case "local": - childComponent = ; - break; - case "ssh": - childComponent = ; - break; - case "webhook": - childComponent = ; - break; - case "k8s-secret": - childComponent = ; - break; - case "volcengine-live": - childComponent = ; - break; - case "volcengine-cdn": - childComponent = ; - break; - } - - return ( - - - {trigger} - { - event.preventDefault(); - }} - > - - {t("domain.deployment.tab")} - - - - - - {/* 部署方式 */} - - {t("domain.deployment.form.type.label")} - - { - setConfig({ ...locDeployConfig, type: val }); - }} - > - - - - - - {t("domain.deployment.form.type.list")} - {Array.from(deployTargetsMap.entries()).map(([key, target]) => ( - - - - {t(target.name)} - - - ))} - - - - - {errors.type} - - - {/* 授权配置 */} - - - {t("domain.deployment.form.access.label")} - - - {t("common.add")} - - } - op="add" - /> - - - { - setConfig({ ...locDeployConfig, access: val }); - }} - > - - - - - - {t("domain.deployment.form.access.list")} - {targetAccesses.map((item) => ( - - - - {item.name} - - - ))} - - - - - {errors.access} - - - {/* 其他参数 */} - {childComponent} - - - - - { - e.stopPropagation(); - handleSaveClick(); - }} - > - {t("common.save")} - - - - - - ); -}; - -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({ + access: "", + type: "", + }); + + const [errors, setErrors] = useState>({}); + + 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 = ; + break; + case "aliyun-cdn": + case "aliyun-dcdn": + childComponent = ; + break; + case "aliyun-clb": + childComponent = ; + break; + case "aliyun-alb": + childComponent = ; + break; + case "aliyun-nlb": + childComponent = ; + break; + case "tencent-cdn": + case "tencent-ecdn": + childComponent = ; + break; + case "tencent-clb": + childComponent = ; + break; + case "tencent-cos": + childComponent = ; + break; + case "tencent-teo": + childComponent = ; + break; + case "huaweicloud-cdn": + childComponent = ; + break; + case "huaweicloud-elb": + childComponent = ; + break; + case "baiducloud-cdn": + childComponent = ; + break; + case "qiniu-cdn": + childComponent = ; + break; + case "dogecloud-cdn": + childComponent = ; + break; + case "local": + childComponent = ; + break; + case "ssh": + childComponent = ; + break; + case "webhook": + childComponent = ; + break; + case "k8s-secret": + childComponent = ; + break; + case "volcengine-live": + childComponent = ; + break; + case "volcengine-cdn": + childComponent = ; + break; + case "byteplus-cdn": + childComponent = ; + break; + } + + return ( + + + {trigger} + { + event.preventDefault(); + }} + > + + {t("domain.deployment.tab")} + + + + + + {/* 部署方式 */} + + {t("domain.deployment.form.type.label")} + + { + setConfig({ ...locDeployConfig, type: val }); + }} + > + + + + + + {t("domain.deployment.form.type.list")} + {Array.from(deployTargetsMap.entries()).map(([key, target]) => ( + + + + {t(target.name)} + + + ))} + + + + + {errors.type} + + + {/* 授权配置 */} + + + {t("domain.deployment.form.access.label")} + + + {t("common.add")} + + } + op="add" + /> + + + { + setConfig({ ...locDeployConfig, access: val }); + }} + > + + + + + + {t("domain.deployment.form.access.list")} + {targetAccesses.map((item) => ( + + + + {item.name} + + + ))} + + + + + {errors.access} + + + {/* 其他参数 */} + {childComponent} + + + + + { + e.stopPropagation(); + handleSaveClick(); + }} + > + {t("common.save")} + + + + + + ); +}; + +export default DeployEditDialog; diff --git a/ui/src/components/certimate/DeployToByteplusCDN.tsx b/ui/src/components/certimate/DeployToByteplusCDN.tsx new file mode 100644 index 00000000..a1944b99 --- /dev/null +++ b/ui/src/components/certimate/DeployToByteplusCDN.tsx @@ -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(); + + 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 ( + + + {t("domain.deployment.form.domain.label.wildsupported")} + { + const nv = produce(config, (draft) => { + draft.config ??= {}; + draft.config.domain = e.target.value?.trim(); + }); + setConfig(nv); + }} + /> + {errors?.domain} + + + ); +}; + +export default DeployToByteplusCDN; diff --git a/ui/src/domain/domain.ts b/ui/src/domain/domain.ts index 9d8d6c2d..4b86d167 100644 --- a/ui/src/domain/domain.ts +++ b/ui/src/domain/domain.ts @@ -94,5 +94,6 @@ export const deployTargetsMap: Map = new Map ["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 }]) ); diff --git a/ui/src/i18n/locales/en/nls.common.json b/ui/src/i18n/locales/en/nls.common.json index e020be43..559557b4 100644 --- a/ui/src/i18n/locales/en/nls.common.json +++ b/ui/src/i18n/locales/en/nls.common.json @@ -93,5 +93,7 @@ "common.provider.bark": "Bark", "common.provider.volcengine": "Volcengine", "common.provider.volcengine.live": "Volcengine - Live", - "common.provider.volcengine.cdn": "Volcengine - CDN" + "common.provider.volcengine.cdn": "Volcengine - CDN", + "common.provider.byteplus": "BytePlus", + "common.provider.byteplus.cdn": "BytePlus - CDN" } diff --git a/ui/src/i18n/locales/zh/nls.common.json b/ui/src/i18n/locales/zh/nls.common.json index ef9f8029..691e9a34 100644 --- a/ui/src/i18n/locales/zh/nls.common.json +++ b/ui/src/i18n/locales/zh/nls.common.json @@ -93,5 +93,7 @@ "common.provider.bark": "Bark", "common.provider.volcengine": "火山引擎", "common.provider.volcengine.live": "火山引擎 - 视频直播", - "common.provider.volcengine.cdn": "火山引擎 - CDN" + "common.provider.volcengine.cdn": "火山引擎 - CDN", + "common.provider.byteplus": "BytePlus", + "common.provider.byteplus.cdn": "BytePlus - CDN" }