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..17c51522 --- /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://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 +} 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/deployer/tencent_cdn.go b/internal/deployer/tencent_cdn.go index a0a44b6a..f5230910 100644 --- a/internal/deployer/tencent_cdn.go +++ b/internal/deployer/tencent_cdn.go @@ -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 diff --git a/internal/domain/access.go b/internal/domain/access.go index 18b87a00..1afcd506 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -16,6 +16,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/domains/deploy.go b/internal/domains/deploy.go index c9df14d3..d664ecf3 100644 --- a/internal/domains/deploy.go +++ b/internal/domains/deploy.go @@ -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 +} 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/migrations/1731872250_add_byteplus.go b/migrations/1731872250_add_byteplus.go new file mode 100644 index 00000000..63daae39 --- /dev/null +++ b/migrations/1731872250_add_byteplus.go @@ -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) + }) +} 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/AccessByteplusForm.tsx b/ui/src/components/certimate/AccessByteplusForm.tsx new file mode 100644 index 00000000..82b3dbad --- /dev/null +++ b/ui/src/components/certimate/AccessByteplusForm.tsx @@ -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>({ + resolver: zodResolver(formSchema), + defaultValues: { + id: data?.id, + name: data?.name || "", + configType: "byteplus", + accessKey: config.accessKey, + secretKey: config.secretKey, + }, + }); + + const onSubmit = async (data: z.infer) => { + 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, { + type: "manual", + message: value.message, + }); + }); + + return; + } + }; + + return ( + <> +
+ { + e.stopPropagation(); + form.handleSubmit(onSubmit)(e); + }} + className="space-y-8" + > + ( + + {t("access.authorization.form.name.label")} + + + + + + + )} + /> + + ( + + {t("access.authorization.form.config.label")} + + + + + + + )} + /> + + ( + + {t("access.authorization.form.config.label")} + + + + + + + )} + /> + + ( + + {t("access.authorization.form.access_key.label")} + + + + + + + )} + /> + + ( + + {t("access.authorization.form.secret_key.label")} + + + + + + + )} + /> + + + +
+ +
+ + + + ); +}; + +export default AccessByteplusForm; diff --git a/ui/src/components/certimate/AccessEditDialog.tsx b/ui/src/components/certimate/AccessEditDialog.tsx index 510297ed..10bede52 100644 --- a/ui/src/components/certimate/AccessEditDialog.tsx +++ b/ui/src/components/certimate/AccessEditDialog.tsx @@ -22,6 +22,7 @@ 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"; @@ -235,6 +236,17 @@ const AccessEditDialog = ({ trigger, op, data, className, outConfigType }: Acces /> ); break; + case "byteplus": + childComponent = ( + { + setOpen(false); + }} + /> + ); + break; } return ( diff --git a/ui/src/components/certimate/DeployEditDialog.tsx b/ui/src/components/certimate/DeployEditDialog.tsx index 97ff9c10..cc25e8f5 100644 --- a/ui/src/components/certimate/DeployEditDialog.tsx +++ b/ui/src/components/certimate/DeployEditDialog.tsx @@ -1,7 +1,6 @@ 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"; @@ -29,6 +28,7 @@ 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"; @@ -182,6 +182,9 @@ const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogPro case "volcengine-cdn": childComponent = ; break; + case "byteplus-cdn": + childComponent = ; + break; } return ( 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 ( +
+
+ + { + 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/access.ts b/ui/src/domain/access.ts index 9629e2a7..27090202 100644 --- a/ui/src/domain/access.ts +++ b/ui/src/domain/access.ts @@ -18,6 +18,7 @@ export const accessProviders = [ ["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", "火山引擎:volcengine"], + ["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"], @@ -53,6 +54,7 @@ export const accessTypeFormSchema = z.union( z.literal("webhook"), z.literal("k8s"), z.literal("volcengine"), + z.literal("byteplus"), ], { message: "access.authorization.form.type.placeholder" } ); @@ -79,7 +81,8 @@ export type Access = { | SSHConfig | WebhookConfig | KubernetesConfig - | VolcengineConfig; + | VolcengineConfig + | ByteplusConfig; deleted?: string; created?: string; updated?: string; @@ -172,3 +175,8 @@ export type VolcengineConfig = { accessKeyId: string; secretAccessKey: string; }; + +export type ByteplusConfig = { + accessKey: string; + secretKey: string; +}; diff --git a/ui/src/domain/domain.ts b/ui/src/domain/domain.ts index 7a4bf4ea..63f6195b 100644 --- a/ui/src/domain/domain.ts +++ b/ui/src/domain/domain.ts @@ -93,6 +93,7 @@ export const deployTargetList: string[][] = [ ["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"], ]; export const deployTargetsMap: Map = new Map( 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" }