diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index 9db3a0fb..bf7483d9 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -40,6 +40,7 @@ import ( pCdnfly "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/cdnfly" pDogeCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/dogecloud-cdn" pEdgioApplications "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/edgio-applications" + pFlexCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/flexcdn" pGcoreCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/gcore-cdn" pGoEdge "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/goedge" pHuaweiCloudCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-cdn" @@ -557,6 +558,25 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer return deployer, err } + case domain.DeploymentProviderTypeFlexCDN: + { + access := domain.AccessConfigForFlexCDN{} + if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + deployer, err := pFlexCDN.NewDeployer(&pFlexCDN.DeployerConfig{ + ApiUrl: access.ApiUrl, + ApiRole: access.ApiRole, + AccessKeyId: access.AccessKeyId, + AccessKey: access.AccessKey, + AllowInsecureConnections: access.AllowInsecureConnections, + ResourceType: pFlexCDN.ResourceType(maputil.GetString(options.ProviderExtendedConfig, "resourceType")), + CertificateId: maputil.GetInt64(options.ProviderExtendedConfig, "certificateId"), + }) + return deployer, err + } + case domain.DeploymentProviderTypeGcoreCDN: { access := domain.AccessConfigForGcore{} diff --git a/internal/domain/access.go b/internal/domain/access.go index 0321cb41..30e52412 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -140,6 +140,14 @@ type AccessConfigForEmail struct { DefaultReceiverAddress string `json:"defaultReceiverAddress,omitempty"` } +type AccessConfigForFlexCDN struct { + ApiUrl string `json:"apiUrl"` + ApiRole string `json:"apiRole"` + AccessKeyId string `json:"accessKeyId"` + AccessKey string `json:"accessKey"` + AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` +} + type AccessConfigForGcore struct { ApiToken string `json:"apiToken"` } diff --git a/internal/domain/provider.go b/internal/domain/provider.go index d4d9af2e..45c72c05 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -36,8 +36,8 @@ const ( AccessProviderTypeDynv6 = AccessProviderType("dynv6") AccessProviderTypeEdgio = AccessProviderType("edgio") AccessProviderTypeEmail = AccessProviderType("email") - AccessProviderTypeFastly = AccessProviderType("fastly") // Fastly(预留) - AccessProviderTypeFlexCDN = AccessProviderType("flexcdn") // FlexCDN(预留) + AccessProviderTypeFastly = AccessProviderType("fastly") // Fastly(预留) + AccessProviderTypeFlexCDN = AccessProviderType("flexcdn") AccessProviderTypeGname = AccessProviderType("gname") AccessProviderTypeGcore = AccessProviderType("gcore") AccessProviderTypeGoDaddy = AccessProviderType("godaddy") @@ -196,7 +196,7 @@ const ( DeploymentProviderTypeCdnfly = DeploymentProviderType(AccessProviderTypeCdnfly) DeploymentProviderTypeDogeCloudCDN = DeploymentProviderType(AccessProviderTypeDogeCloud + "-cdn") DeploymentProviderTypeEdgioApplications = DeploymentProviderType(AccessProviderTypeEdgio + "-applications") - DeploymentProviderTypeFlexCDN = DeploymentProviderType(AccessProviderTypeFlexCDN) // FlexCDN(预留) + DeploymentProviderTypeFlexCDN = DeploymentProviderType(AccessProviderTypeFlexCDN) DeploymentProviderTypeGcoreCDN = DeploymentProviderType(AccessProviderTypeGcore + "-cdn") DeploymentProviderTypeGoEdge = DeploymentProviderType(AccessProviderTypeGoEdge) DeploymentProviderTypeHuaweiCloudCDN = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-cdn") diff --git a/internal/pkg/core/deployer/providers/flexcdn/consts.go b/internal/pkg/core/deployer/providers/flexcdn/consts.go new file mode 100644 index 00000000..be55a475 --- /dev/null +++ b/internal/pkg/core/deployer/providers/flexcdn/consts.go @@ -0,0 +1,8 @@ +package flexcdn + +type ResourceType string + +const ( + // 资源类型:替换指定证书。 + RESOURCE_TYPE_CERTIFICATE = ResourceType("certificate") +) diff --git a/internal/pkg/core/deployer/providers/flexcdn/flexcdn.go b/internal/pkg/core/deployer/providers/flexcdn/flexcdn.go new file mode 100644 index 00000000..3d0f05e9 --- /dev/null +++ b/internal/pkg/core/deployer/providers/flexcdn/flexcdn.go @@ -0,0 +1,144 @@ +package flexcdn + +import ( + "context" + "crypto/tls" + "encoding/base64" + "errors" + "fmt" + "log/slog" + "net/url" + "time" + + "github.com/usual2970/certimate/internal/pkg/core/deployer" + flexcdnsdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/flexcdn" + certutil "github.com/usual2970/certimate/internal/pkg/utils/cert" +) + +type DeployerConfig struct { + // FlexCDN URL。 + ApiUrl string `json:"apiUrl"` + // FlexCDN 用户角色。 + ApiRole string `json:"apiRole"` + // FlexCDN AccessKeyId。 + AccessKeyId string `json:"accessKeyId"` + // FlexCDN AccessKey。 + AccessKey string `json:"accessKey"` + // 是否允许不安全的连接。 + AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` + // 部署资源类型。 + ResourceType ResourceType `json:"resourceType"` + // 证书 ID。 + // 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。 + CertificateId int64 `json:"certificateId,omitempty"` +} + +type DeployerProvider struct { + config *DeployerConfig + logger *slog.Logger + sdkClient *flexcdnsdk.Client +} + +var _ deployer.Deployer = (*DeployerProvider)(nil) + +func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { + if config == nil { + panic("config is nil") + } + + client, err := createSdkClient(config.ApiUrl, config.ApiRole, config.AccessKeyId, config.AccessKey, config.AllowInsecureConnections) + if err != nil { + return nil, fmt.Errorf("failed to create sdk client: %w", err) + } + + return &DeployerProvider{ + config: config, + logger: slog.Default(), + sdkClient: client, + }, nil +} + +func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { + if logger == nil { + d.logger = slog.Default() + } else { + d.logger = logger + } + return d +} + +func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) { + // 根据部署资源类型决定部署方式 + switch d.config.ResourceType { + case RESOURCE_TYPE_CERTIFICATE: + if err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil { + return nil, err + } + + default: + return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) + } + + return &deployer.DeployResult{}, nil +} + +func (d *DeployerProvider) deployToCertificate(ctx context.Context, certPEM string, privkeyPEM string) error { + if d.config.CertificateId == 0 { + return errors.New("config `certificateId` is required") + } + + // 解析证书内容 + certX509, err := certutil.ParseCertificateFromPEM(certPEM) + if err != nil { + return err + } + + // 修改证书 + // REF: https://flexcdn.cloud/dev/api/service/SSLCertService?role=user#updateSSLCert + updateSSLCertReq := &flexcdnsdk.UpdateSSLCertRequest{ + SSLCertId: d.config.CertificateId, + IsOn: true, + Name: fmt.Sprintf("certimate-%d", time.Now().UnixMilli()), + Description: "upload from certimate", + ServerName: certX509.Subject.CommonName, + IsCA: false, + CertData: base64.StdEncoding.EncodeToString([]byte(certPEM)), + KeyData: base64.StdEncoding.EncodeToString([]byte(privkeyPEM)), + TimeBeginAt: certX509.NotBefore.Unix(), + TimeEndAt: certX509.NotAfter.Unix(), + DNSNames: certX509.DNSNames, + CommonNames: []string{certX509.Subject.CommonName}, + } + updateSSLCertResp, err := d.sdkClient.UpdateSSLCert(updateSSLCertReq) + d.logger.Debug("sdk request 'flexcdn.UpdateSSLCert'", slog.Any("request", updateSSLCertReq), slog.Any("response", updateSSLCertResp)) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'flexcdn.UpdateSSLCert': %w", err) + } + + return nil +} + +func createSdkClient(apiUrl, apiRole, accessKeyId, accessKey string, skipTlsVerify bool) (*flexcdnsdk.Client, error) { + if _, err := url.Parse(apiUrl); err != nil { + return nil, errors.New("invalid flexcdn api url") + } + + if apiRole != "user" && apiRole != "admin" { + return nil, errors.New("invalid flexcdn api role") + } + + if accessKeyId == "" { + return nil, errors.New("invalid flexcdn access key id") + } + + if accessKey == "" { + return nil, errors.New("invalid flexcdn access key") + } + + client := flexcdnsdk.NewClient(apiUrl, apiRole, accessKeyId, accessKey) + if skipTlsVerify { + client.WithTLSConfig(&tls.Config{InsecureSkipVerify: true}) + } + + return client, nil +} diff --git a/internal/pkg/core/deployer/providers/flexcdn/flexcdn_test.go b/internal/pkg/core/deployer/providers/flexcdn/flexcdn_test.go new file mode 100644 index 00000000..4a693dc9 --- /dev/null +++ b/internal/pkg/core/deployer/providers/flexcdn/flexcdn_test.go @@ -0,0 +1,83 @@ +package flexcdn_test + +import ( + "context" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/flexcdn" +) + +var ( + fInputCertPath string + fInputKeyPath string + fApiUrl string + fAccessKeyId string + fAccessKey string + fCertificateId int +) + +func init() { + argsPrefix := "CERTIMATE_DEPLOYER_FLEXCDN_" + + flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") + flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") + flag.StringVar(&fApiUrl, argsPrefix+"APIURL", "", "") + flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") + flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "") + flag.IntVar(&fCertificateId, argsPrefix+"CERTIFICATEID", 0, "") +} + +/* +Shell command to run this test: + + go test -v ./flexcdn_test.go -args \ + --CERTIMATE_DEPLOYER_FLEXCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_DEPLOYER_FLEXCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_DEPLOYER_FLEXCDN_APIURL="http://127.0.0.1:7788" \ + --CERTIMATE_DEPLOYER_FLEXCDN_ACCESSKEYID="your-access-key-id" \ + --CERTIMATE_DEPLOYER_FLEXCDN_ACCESSKEY="your-access-key" \ + --CERTIMATE_DEPLOYER_FLEXCDN_CERTIFICATEID="your-cerficiate-id" +*/ +func TestDeploy(t *testing.T) { + flag.Parse() + + t.Run("Deploy", func(t *testing.T) { + t.Log(strings.Join([]string{ + "args:", + fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), + fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), + fmt.Sprintf("APIURL: %v", fApiUrl), + fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), + fmt.Sprintf("ACCESSKEY: %v", fAccessKey), + fmt.Sprintf("CERTIFICATEID: %v", fCertificateId), + }, "\n")) + + deployer, err := provider.NewDeployer(&provider.DeployerConfig{ + ApiUrl: fApiUrl, + ApiRole: "user", + AccessKeyId: fAccessKeyId, + AccessKey: fAccessKey, + AllowInsecureConnections: true, + ResourceType: provider.RESOURCE_TYPE_CERTIFICATE, + CertificateId: int64(fCertificateId), + }) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + fInputCertData, _ := os.ReadFile(fInputCertPath) + fInputKeyData, _ := os.ReadFile(fInputKeyPath) + res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + t.Logf("ok: %v", res) + }) +} diff --git a/internal/pkg/core/deployer/providers/goedge/goedge_test.go b/internal/pkg/core/deployer/providers/goedge/goedge_test.go index c8c32b37..928fb420 100644 --- a/internal/pkg/core/deployer/providers/goedge/goedge_test.go +++ b/internal/pkg/core/deployer/providers/goedge/goedge_test.go @@ -58,6 +58,7 @@ func TestDeploy(t *testing.T) { deployer, err := provider.NewDeployer(&provider.DeployerConfig{ ApiUrl: fApiUrl, + ApiRole: "user", AccessKeyId: fAccessKeyId, AccessKey: fAccessKey, AllowInsecureConnections: true, diff --git a/internal/pkg/sdk3rd/flexcdn/api.go b/internal/pkg/sdk3rd/flexcdn/api.go new file mode 100644 index 00000000..07fb2b34 --- /dev/null +++ b/internal/pkg/sdk3rd/flexcdn/api.go @@ -0,0 +1,46 @@ +package flexcdn + +import ( + "encoding/json" + "fmt" + "net/http" + "time" +) + +func (c *Client) getAccessToken() error { + req := &getAPIAccessTokenRequest{ + Type: c.apiRole, + AccessKeyId: c.accessKeyId, + AccessKey: c.accessKey, + } + res, err := c.sendRequest(http.MethodPost, "/APIAccessTokenService/getAPIAccessToken", req) + if err != nil { + return err + } + + resp := &getAPIAccessTokenResponse{} + if err := json.Unmarshal(res.Body(), &resp); err != nil { + return fmt.Errorf("flexcdn api error: failed to unmarshal response: %w", err) + } else if resp.GetCode() != 200 { + return fmt.Errorf("flexcdn get access token failed: code: %d, message: %s", resp.GetCode(), resp.GetMessage()) + } + + c.accessTokenMtx.Lock() + c.accessToken = resp.Data.Token + c.accessTokenExp = time.Unix(resp.Data.ExpiresAt, 0) + c.accessTokenMtx.Unlock() + + return nil +} + +func (c *Client) UpdateSSLCert(req *UpdateSSLCertRequest) (*UpdateSSLCertResponse, error) { + if c.accessToken == "" || c.accessTokenExp.Before(time.Now()) { + if err := c.getAccessToken(); err != nil { + return nil, err + } + } + + resp := &UpdateSSLCertResponse{} + err := c.sendRequestWithResult(http.MethodPost, "/SSLCertService/updateSSLCert", req, resp) + return resp, err +} diff --git a/internal/pkg/sdk3rd/flexcdn/client.go b/internal/pkg/sdk3rd/flexcdn/client.go new file mode 100644 index 00000000..6b5626ee --- /dev/null +++ b/internal/pkg/sdk3rd/flexcdn/client.go @@ -0,0 +1,103 @@ +package flexcdn + +import ( + "crypto/tls" + "encoding/json" + "fmt" + "net/http" + "strings" + "sync" + "time" + + "github.com/go-resty/resty/v2" +) + +type Client struct { + apiHost string + apiRole string + accessKeyId string + accessKey string + + accessToken string + accessTokenExp time.Time + accessTokenMtx sync.Mutex + + client *resty.Client +} + +func NewClient(apiHost, apiRole, accessKeyId, accessKey string) *Client { + client := resty.New() + + return &Client{ + apiHost: strings.TrimRight(apiHost, "/"), + apiRole: apiRole, + accessKeyId: accessKeyId, + accessKey: accessKey, + client: client, + } +} + +func (c *Client) WithTimeout(timeout time.Duration) *Client { + c.client.SetTimeout(timeout) + return c +} + +func (c *Client) WithTLSConfig(config *tls.Config) *Client { + c.client.SetTLSClientConfig(config) + return c +} + +func (c *Client) sendRequest(method string, path string, params interface{}) (*resty.Response, error) { + req := c.client.R().SetBasicAuth(c.accessKeyId, c.accessKey) + req.Method = method + req.URL = c.apiHost + path + if strings.EqualFold(method, http.MethodGet) { + qs := make(map[string]string) + if params != nil { + temp := make(map[string]any) + jsonb, _ := json.Marshal(params) + json.Unmarshal(jsonb, &temp) + for k, v := range temp { + if v != nil { + qs[k] = fmt.Sprintf("%v", v) + } + } + } + + req = req. + SetHeader("X-Cloud-Access-Token", c.accessToken). + SetQueryParams(qs) + } else { + req = req. + SetHeader("Content-Type", "application/json"). + SetHeader("X-Cloud-Access-Token", c.accessToken). + SetBody(params) + } + + resp, err := req.Send() + if err != nil { + return resp, fmt.Errorf("flexcdn api error: failed to send request: %w", err) + } else if resp.IsError() { + return resp, fmt.Errorf("flexcdn api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.String()) + } + + return resp, nil +} + +func (c *Client) sendRequestWithResult(method string, path string, params interface{}, result BaseResponse) error { + resp, err := c.sendRequest(method, path, params) + if err != nil { + if resp != nil { + json.Unmarshal(resp.Body(), &result) + } + return err + } + + if err := json.Unmarshal(resp.Body(), &result); err != nil { + return fmt.Errorf("flexcdn api error: failed to unmarshal response: %w", err) + } else if errcode := result.GetCode(); errcode != 200 { + return fmt.Errorf("flexcdn api error: code='%d', message='%s'", errcode, result.GetMessage()) + } + + return nil +} diff --git a/internal/pkg/sdk3rd/flexcdn/models.go b/internal/pkg/sdk3rd/flexcdn/models.go new file mode 100644 index 00000000..c976eccc --- /dev/null +++ b/internal/pkg/sdk3rd/flexcdn/models.go @@ -0,0 +1,52 @@ +package flexcdn + +type BaseResponse interface { + GetCode() int32 + GetMessage() string +} + +type baseResponse struct { + Code int32 `json:"code"` + Message string `json:"message"` +} + +func (r *baseResponse) GetCode() int32 { + return r.Code +} + +func (r *baseResponse) GetMessage() string { + return r.Message +} + +type getAPIAccessTokenRequest struct { + Type string `json:"type"` + AccessKeyId string `json:"accessKeyId"` + AccessKey string `json:"accessKey"` +} + +type getAPIAccessTokenResponse struct { + baseResponse + Data *struct { + Token string `json:"token"` + ExpiresAt int64 `json:"expiresAt"` + } `json:"data,omitempty"` +} + +type UpdateSSLCertRequest struct { + SSLCertId int64 `json:"sslCertId"` + IsOn bool `json:"isOn"` + Name string `json:"name"` + Description string `json:"description"` + ServerName string `json:"serverName"` + IsCA bool `json:"isCA"` + CertData string `json:"certData"` + KeyData string `json:"keyData"` + TimeBeginAt int64 `json:"timeBeginAt"` + TimeEndAt int64 `json:"timeEndAt"` + DNSNames []string `json:"dnsNames"` + CommonNames []string `json:"commonNames"` +} + +type UpdateSSLCertResponse struct { + baseResponse +} diff --git a/internal/pkg/sdk3rd/goedge/client.go b/internal/pkg/sdk3rd/goedge/client.go index e0a2ce49..39ad8900 100644 --- a/internal/pkg/sdk3rd/goedge/client.go +++ b/internal/pkg/sdk3rd/goedge/client.go @@ -65,8 +65,8 @@ func (c *Client) sendRequest(method string, path string, params interface{}) (*r } req = req. - SetQueryParams(qs). - SetHeader("X-Edge-Access-Token", c.accessToken) + SetHeader("X-Edge-Access-Token", c.accessToken). + SetQueryParams(qs) } else { req = req. SetHeader("Content-Type", "application/json"). diff --git a/internal/pkg/sdk3rd/upyun/console/client.go b/internal/pkg/sdk3rd/upyun/console/client.go index 7a7ea7de..6b560adc 100644 --- a/internal/pkg/sdk3rd/upyun/console/client.go +++ b/internal/pkg/sdk3rd/upyun/console/client.go @@ -52,8 +52,8 @@ func (c *Client) sendRequest(method string, path string, params interface{}) (*r } req = req. - SetQueryParams(qs). - SetHeader("Cookie", c.loginCookie) + SetHeader("Cookie", c.loginCookie). + SetQueryParams(qs) } else { req = req. SetHeader("Content-Type", "application/json"). diff --git a/ui/public/imgs/providers/flexcdn.png b/ui/public/imgs/providers/flexcdn.png new file mode 100644 index 00000000..00805598 Binary files /dev/null and b/ui/public/imgs/providers/flexcdn.png differ diff --git a/ui/src/components/access/AccessForm.tsx b/ui/src/components/access/AccessForm.tsx index 4981a045..7f3d8e8e 100644 --- a/ui/src/components/access/AccessForm.tsx +++ b/ui/src/components/access/AccessForm.tsx @@ -34,6 +34,7 @@ import AccessFormDogeCloudConfig from "./AccessFormDogeCloudConfig"; import AccessFormDynv6Config from "./AccessFormDynv6Config"; import AccessFormEdgioConfig from "./AccessFormEdgioConfig"; import AccessFormEmailConfig from "./AccessFormEmailConfig"; +import AccessFormFlexCDNConfig from "./AccessFormFlexCDNConfig"; import AccessFormGcoreConfig from "./AccessFormGcoreConfig"; import AccessFormGnameConfig from "./AccessFormGnameConfig"; import AccessFormGoDaddyConfig from "./AccessFormGoDaddyConfig"; @@ -218,6 +219,12 @@ const AccessForm = forwardRef(({ className, return ; case ACCESS_PROVIDERS.DYNV6: return ; + case ACCESS_PROVIDERS.EDGIO: + return ; + case ACCESS_PROVIDERS.EMAIL: + return ; + case ACCESS_PROVIDERS.FLEXCDN: + return ; case ACCESS_PROVIDERS.GCORE: return ; case ACCESS_PROVIDERS.GNAME: @@ -228,10 +235,6 @@ const AccessForm = forwardRef(({ className, return ; case ACCESS_PROVIDERS.GOOGLETRUSTSERVICES: return ; - case ACCESS_PROVIDERS.EDGIO: - return ; - case ACCESS_PROVIDERS.EMAIL: - return ; case ACCESS_PROVIDERS.HUAWEICLOUD: return ; case ACCESS_PROVIDERS.JDCLOUD: diff --git a/ui/src/components/access/AccessFormFlexCDNConfig.tsx b/ui/src/components/access/AccessFormFlexCDNConfig.tsx new file mode 100644 index 00000000..6ca020bf --- /dev/null +++ b/ui/src/components/access/AccessFormFlexCDNConfig.tsx @@ -0,0 +1,90 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input, Radio, Switch } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type AccessConfigForFlexCDN } from "@/domain/access"; + +type AccessFormFlexCDNConfigFieldValues = Nullish; + +export type AccessFormFlexCDNConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: AccessFormFlexCDNConfigFieldValues; + onValuesChange?: (values: AccessFormFlexCDNConfigFieldValues) => void; +}; + +const initFormModel = (): AccessFormFlexCDNConfigFieldValues => { + return { + apiUrl: "http://:8000/", + apiRole: "user", + accessKeyId: "", + accessKey: "", + }; +}; + +const AccessFormFlexCDNConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormFlexCDNConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + apiUrl: z.string().url(t("common.errmsg.url_invalid")), + role: z.union([z.literal("user"), z.literal("admin")], { + message: t("access.form.flexcdn_api_role.placeholder"), + }), + accessKeyId: z.string().nonempty(t("access.form.flexcdn_access_key_id.placeholder")).trim(), + accessKey: z.string().nonempty(t("access.form.flexcdn_access_key.placeholder")).trim(), + allowInsecureConnections: z.boolean().nullish(), + }); + const formRule = createSchemaFieldRule(formSchema); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ + + + + + ({ label: t(`access.form.flexcdn_api_role.option.${s}.label`), value: s }))} /> + + + } + > + + + + } + > + + + + + + +
+ ); +}; + +export default AccessFormFlexCDNConfig; diff --git a/ui/src/components/access/AccessFormGoEdgeConfig.tsx b/ui/src/components/access/AccessFormGoEdgeConfig.tsx index ced9b09a..9c03f2be 100644 --- a/ui/src/components/access/AccessFormGoEdgeConfig.tsx +++ b/ui/src/components/access/AccessFormGoEdgeConfig.tsx @@ -32,16 +32,8 @@ const AccessFormGoEdgeConfig = ({ form: formInst, formName, disabled, initialVal role: z.union([z.literal("user"), z.literal("admin")], { message: t("access.form.goedge_api_role.placeholder"), }), - accessKeyId: z - .string() - .min(1, t("access.form.goedge_access_key_id.placeholder")) - .max(64, t("common.errmsg.string_max", { max: 64 })) - .trim(), - accessKey: z - .string() - .min(1, t("access.form.goedge_access_key.placeholder")) - .max(64, t("common.errmsg.string_max", { max: 64 })) - .trim(), + accessKeyId: z.string().nonempty(t("access.form.goedge_access_key_id.placeholder")).trim(), + accessKey: z.string().nonempty(t("access.form.goedge_access_key.placeholder")).trim(), allowInsecureConnections: z.boolean().nullish(), }); const formRule = createSchemaFieldRule(formSchema); diff --git a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx index b77ff9cf..981d7183 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx @@ -46,6 +46,7 @@ import DeployNodeConfigFormBytePlusCDNConfig from "./DeployNodeConfigFormBytePlu import DeployNodeConfigFormCdnflyConfig from "./DeployNodeConfigFormCdnflyConfig"; import DeployNodeConfigFormDogeCloudCDNConfig from "./DeployNodeConfigFormDogeCloudCDNConfig"; import DeployNodeConfigFormEdgioApplicationsConfig from "./DeployNodeConfigFormEdgioApplicationsConfig"; +import DeployNodeConfigFormFlexCDNConfig from "./DeployNodeConfigFormFlexCDNConfig"; import DeployNodeConfigFormGcoreCDNConfig from "./DeployNodeConfigFormGcoreCDNConfig"; import DeployNodeConfigFormGoEdgeConfig from "./DeployNodeConfigFormGoEdgeConfig"; import DeployNodeConfigFormHuaweiCloudCDNConfig from "./DeployNodeConfigFormHuaweiCloudCDNConfig"; @@ -240,6 +241,8 @@ const DeployNodeConfigForm = forwardRef; case DEPLOYMENT_PROVIDERS.EDGIO_APPLICATIONS: return ; + case DEPLOYMENT_PROVIDERS.FLEXCDN: + return ; case DEPLOYMENT_PROVIDERS.GCORE_CDN: return ; case DEPLOYMENT_PROVIDERS.GOEDGE: diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormCdnflyConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormCdnflyConfig.tsx index 45662e75..4d6ae25c 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormCdnflyConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormCdnflyConfig.tsx @@ -79,13 +79,23 @@ const DeployNodeConfigFormCdnflyConfig = ({ form: formInst, formName, disabled, - + } + > - + } + > diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormFlexCDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormFlexCDNConfig.tsx new file mode 100644 index 00000000..e24652be --- /dev/null +++ b/ui/src/components/workflow/node/DeployNodeConfigFormFlexCDNConfig.tsx @@ -0,0 +1,84 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input, Select } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import Show from "@/components/Show"; + +type DeployNodeConfigFormFlexCDNConfigFieldValues = Nullish<{ + resourceType: string; + certificateId?: string | number; +}>; + +export type DeployNodeConfigFormFlexCDNConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: DeployNodeConfigFormFlexCDNConfigFieldValues; + onValuesChange?: (values: DeployNodeConfigFormFlexCDNConfigFieldValues) => void; +}; + +const RESOURCE_TYPE_CERTIFICATE = "certificate" as const; + +const initFormModel = (): DeployNodeConfigFormFlexCDNConfigFieldValues => { + return { + resourceType: RESOURCE_TYPE_CERTIFICATE, + certificateId: "", + }; +}; + +const DeployNodeConfigFormFlexCDNConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: DeployNodeConfigFormFlexCDNConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + resourceType: z.literal(RESOURCE_TYPE_CERTIFICATE, { + message: t("workflow_node.deploy.form.flexcdn_resource_type.placeholder"), + }), + certificateId: z + .union([z.string(), z.number().int()]) + .nullish() + .refine((v) => { + if (fieldResourceType !== RESOURCE_TYPE_CERTIFICATE) return true; + return /^\d+$/.test(v + "") && +v! > 0; + }, t("workflow_node.deploy.form.flexcdn_certificate_id.placeholder")), + }); + const formRule = createSchemaFieldRule(formSchema); + + const fieldResourceType = Form.useWatch("resourceType", formInst); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ + + + + + } + > + + + +
+ ); +}; + +export default DeployNodeConfigFormFlexCDNConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormGoEdgeConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormGoEdgeConfig.tsx index 89dffb5f..5e0f7f85 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormGoEdgeConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormGoEdgeConfig.tsx @@ -68,7 +68,12 @@ const DeployNodeConfigFormGoEdgeConfig = ({ form: formInst, formName, disabled, - + } + > diff --git a/ui/src/domain/access.ts b/ui/src/domain/access.ts index c676b5f7..48882eb7 100644 --- a/ui/src/domain/access.ts +++ b/ui/src/domain/access.ts @@ -29,6 +29,7 @@ export interface AccessModel extends BaseModel { | AccessConfigForDynv6 | AccessConfigForEdgio | AccessConfigForEmail + | AccessConfigForFlexCDN | AccessConfigForGcore | AccessConfigForGname | AccessConfigForGoDaddy @@ -194,6 +195,14 @@ export type AccessConfigForEmail = { defaultReceiverAddress?: string; }; +export type AccessConfigForFlexCDN = { + apiUrl: string; + apiRole: string; + accessKeyId: string; + accessKey: string; + allowInsecureConnections?: boolean; +}; + export type AccessConfigForGcore = { apiToken: string; }; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 2c6f0f0f..4cad8606 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -28,6 +28,7 @@ export const ACCESS_PROVIDERS = Object.freeze({ DYNV6: "dynv6", EDGIO: "edgio", EMAIL: "email", + FLEXCDN: "flexcdn", GCORE: "gcore", GNAME: "gname", GODADDY: "godaddy", @@ -126,6 +127,7 @@ export const accessProvidersMap: Maphttps://flexcdn.cn/docs/api/auth", + "access.form.flexcdn_access_key.label": "FlexCDN AccessKey", + "access.form.flexcdn_access_key.placeholder": "Please enter FlexCDN AccessKey", + "access.form.flexcdn_access_key.tooltip": "For more information, see https://flexcdn.cn/docs/api/auth", + "access.form.flexcdn_allow_insecure_conns.label": "Insecure SSL/TLS connections", + "access.form.flexcdn_allow_insecure_conns.switch.on": "Allow", + "access.form.flexcdn_allow_insecure_conns.switch.off": "Disallow", "access.form.gcore_api_token.label": "Gcore API token", "access.form.gcore_api_token.placeholder": "Please enter Gcore API token", "access.form.gcore_api_token.tooltip": "For more information, see https://api.gcore.com/docs/iam#section/Authentication", diff --git a/ui/src/i18n/locales/en/nls.provider.json b/ui/src/i18n/locales/en/nls.provider.json index dad0ae6f..337820a3 100644 --- a/ui/src/i18n/locales/en/nls.provider.json +++ b/ui/src/i18n/locales/en/nls.provider.json @@ -63,6 +63,7 @@ "provider.edgio.applications": "Edgio - Applications", "provider.email": "Email", "provider.fastly": "Fastly", + "provider.flexcdn": "FlexCDN", "provider.gcore": "Gcore", "provider.gcore.cdn": "Gcore - CDN (Content Delivery Network)", "provider.gname": "GNAME", diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index 006979f1..a998c26c 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -347,14 +347,22 @@ "workflow_node.deploy.form.cdnfly_resource_type.option.certificate.label": "Certificate", "workflow_node.deploy.form.cdnfly_site_id.label": "Cdnfly site ID", "workflow_node.deploy.form.cdnfly_site_id.placeholder": "Please enter Cdnfly site ID", + "workflow_node.deploy.form.cdnfly_site_id.tooltip": "You can find it on Cdnfly WebUI.", "workflow_node.deploy.form.cdnfly_certificate_id.label": "Cdnfly certificate ID", "workflow_node.deploy.form.cdnfly_certificate_id.placeholder": "Please enter Cdnfly certificate ID", + "workflow_node.deploy.form.cdnfly_certificate_id.tooltip": "You can find it on Cdnfly WebUI.", "workflow_node.deploy.form.dogecloud_cdn_domain.label": "Doge Cloud CDN domain", "workflow_node.deploy.form.dogecloud_cdn_domain.placeholder": "Please enter Doge Cloud CDN domain name", "workflow_node.deploy.form.dogecloud_cdn_domain.tooltip": "For more information, see https://console.dogecloud.com/", "workflow_node.deploy.form.edgio_applications_environment_id.label": "Edgio Applications environment ID", "workflow_node.deploy.form.edgio_applications_environment_id.placeholder": "Please enter Edgio Applications environment ID", "workflow_node.deploy.form.edgio_applications_environment_id.tooltip": "For more information, see https://edgio.app/", + "workflow_node.deploy.form.flexcdn_resource_type.label": "Resource type", + "workflow_node.deploy.form.flexcdn_resource_type.placeholder": "Please select resource type", + "workflow_node.deploy.form.flexcdn_resource_type.option.certificate.label": "Certificate", + "workflow_node.deploy.form.flexcdn_certificate_id.label": "FlexCDN certificate ID", + "workflow_node.deploy.form.flexcdn_certificate_id.placeholder": "Please enter FlexCDN certificate ID", + "workflow_node.deploy.form.flexcdn_certificate_id.tooltip": "You can find it on FlexCDN WebUI.", "workflow_node.deploy.form.gcore_cdn_resource_id.label": "Gcore CDN resource ID", "workflow_node.deploy.form.gcore_cdn_resource_id.placeholder": "Please enter Gcore CDN resource ID", "workflow_node.deploy.form.gcore_cdn_resource_id.tooltip": "For more information, see https://cdn.gcore.com/resources/list", @@ -366,6 +374,7 @@ "workflow_node.deploy.form.goedge_resource_type.option.certificate.label": "Certificate", "workflow_node.deploy.form.goedge_certificate_id.label": "GoEdge certificate ID", "workflow_node.deploy.form.goedge_certificate_id.placeholder": "Please enter GoEdge certificate ID", + "workflow_node.deploy.form.goedge_certificate_id.tooltip": "You can find it on GoEdge WebUI.", "workflow_node.deploy.form.huaweicloud_cdn_region.label": "Huawei Cloud CDN region", "workflow_node.deploy.form.huaweicloud_cdn_region.placeholder": "Please enter Huawei Cloud CDN region (e.g. cn-north-1)", "workflow_node.deploy.form.huaweicloud_cdn_region.tooltip": "For more information, see https://console-intl.huaweicloud.com/apiexplorer/#/endpoint", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index 1ca78d07..649d44f5 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -187,6 +187,21 @@ "access.form.email_default_sender_address.placeholder": "请输入默认的发送邮箱地址", "access.form.email_default_receiver_address.label": "默认的接收邮箱地址(可选)", "access.form.email_default_receiver_address.placeholder": "请输入默认的接收邮箱地址", + "access.form.flexcdn_api_url.label": "FlexCDN API URL", + "access.form.flexcdn_api_url.placeholder": "请输入 FlexCDN API URL", + "access.form.flexcdn_api_role.label": "FlexCDN 用户角色", + "access.form.flexcdn_api_role.placeholder": "请选择 FlexCDN 用户角色", + "access.form.flexcdn_api_role.option.user.label": "平台用户", + "access.form.flexcdn_api_role.option.admin.label": "系统管理员", + "access.form.flexcdn_access_key_id.label": "FlexCDN AccessKeyId", + "access.form.flexcdn_access_key_id.placeholder": "请输入 FlexCDN AccessKeyId", + "access.form.flexcdn_access_key_id.tooltip": "这是什么?请参阅 https://flexcdn.cn/docs/api/auth", + "access.form.flexcdn_access_key.label": "FlexCDN AccessKey", + "access.form.flexcdn_access_key.placeholder": "请输入 FlexCDN AccessKey", + "access.form.flexcdn_access_key.tooltip": "这是什么?请参阅 https://flexcdn.cn/docs/api/auth", + "access.form.flexcdn_allow_insecure_conns.label": "忽略 SSL/TLS 证书错误", + "access.form.flexcdn_allow_insecure_conns.switch.on": "允许", + "access.form.flexcdn_allow_insecure_conns.switch.off": "不允许", "access.form.gcore_api_token.label": "Gcore API Token", "access.form.gcore_api_token.placeholder": "请输入 Gcore API Token", "access.form.gcore_api_token.tooltip": "这是什么?请参阅 https://api.gcore.com/docs/iam#section/Authentication", diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json index e7c6ea31..27abe56c 100644 --- a/ui/src/i18n/locales/zh/nls.provider.json +++ b/ui/src/i18n/locales/zh/nls.provider.json @@ -63,6 +63,7 @@ "provider.edgio.applications": "Edgio - Applications", "provider.email": "邮件", "provider.fastly": "Fastly", + "provider.flexcdn": "FlexCDN", "provider.gcore": "Gcore", "provider.gcore.cdn": "Gcore - 内容分发网络 CDN", "provider.gname": "GNAME", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index acc5657f..e462f34a 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -346,14 +346,22 @@ "workflow_node.deploy.form.cdnfly_resource_type.option.certificate.label": "替换指定证书", "workflow_node.deploy.form.cdnfly_site_id.label": "Cdnfly 网站 ID", "workflow_node.deploy.form.cdnfly_site_id.placeholder": "请输入 Cdnfly 网站 ID", + "workflow_node.deploy.form.cdnfly_site_id.tooltip": "请登录 Cdnfly 管理平台查看。", "workflow_node.deploy.form.cdnfly_certificate_id.label": "Cdnfly 证书 ID", "workflow_node.deploy.form.cdnfly_certificate_id.placeholder": "请输入 Cdnfly 证书 ID", + "workflow_node.deploy.form.cdnfly_certificate_id.tooltip": "请登录 Cdnfly 管理平台查看。", "workflow_node.deploy.form.dogecloud_cdn_domain.label": "多吉云 CDN 加速域名", "workflow_node.deploy.form.dogecloud_cdn_domain.placeholder": "请输入多吉云 CDN 加速域名", "workflow_node.deploy.form.dogecloud_cdn_domain.tooltip": "这是什么?请参阅 https://console.dogecloud.com", "workflow_node.deploy.form.edgio_applications_environment_id.label": "Edgio Applications 环境 ID", "workflow_node.deploy.form.edgio_applications_environment_id.placeholder": "请输入 Edgio Applications 环境 ID", "workflow_node.deploy.form.edgio_applications_environment_id.tooltip": "这是什么?请参阅 https://edgio.app/", + "workflow_node.deploy.form.flexcdn_resource_type.label": "证书替换方式", + "workflow_node.deploy.form.flexcdn_resource_type.placeholder": "请选择证书替换方式", + "workflow_node.deploy.form.flexcdn_resource_type.option.certificate.label": "替换指定证书", + "workflow_node.deploy.form.flexcdn_certificate_id.label": "FlexCDN 证书 ID", + "workflow_node.deploy.form.flexcdn_certificate_id.placeholder": "请输入 FlexCDN 证书 ID", + "workflow_node.deploy.form.flexcdn_certificate_id.tooltip": "请登录 FlexCDN 管理平台查看。", "workflow_node.deploy.form.gcore_cdn_resource_id.label": "Gcore CDN 资源 ID", "workflow_node.deploy.form.gcore_cdn_resource_id.placeholder": "请输入 Gcore CDN 资源 ID", "workflow_node.deploy.form.gcore_cdn_resource_id.tooltip": "这是什么?请参阅 https://cdn.gcore.com/resources/list", @@ -365,6 +373,7 @@ "workflow_node.deploy.form.goedge_resource_type.option.certificate.label": "替换指定证书", "workflow_node.deploy.form.goedge_certificate_id.label": "GoEdge 证书 ID", "workflow_node.deploy.form.goedge_certificate_id.placeholder": "请输入 GoEdge 证书 ID", + "workflow_node.deploy.form.goedge_certificate_id.tooltip": "请登录 GoEdge 管理平台查看。", "workflow_node.deploy.form.huaweicloud_cdn_region.label": "华为云 CDN 服务区域", "workflow_node.deploy.form.huaweicloud_cdn_region.placeholder": "请输入华为云 CDN 服务区域(例如:cn-north-1)", "workflow_node.deploy.form.huaweicloud_cdn_region.tooltip": "这是什么?请参阅 https://console.huaweicloud.com/apiexplorer/#/endpoint",