From deb3b2f412fb623a1f0741d332d8ce1691505699 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Sun, 30 Mar 2025 10:36:05 +0800 Subject: [PATCH 01/20] feat: manage ca authorizations --- internal/applicant/acme_ca.go | 28 ++- internal/applicant/applicant.go | 4 +- internal/domain/access.go | 10 + internal/domain/provider.go | 120 +++++++----- migrations/1742209200_upgrade.go | 40 ++-- migrations/1743264000_upgrade.go | 171 ++++++++++++++++++ ui/public/imgs/{acme => providers}/google.svg | 0 .../imgs/{acme => providers}/letsencrypt.svg | 0 .../imgs/{acme => providers}/zerossl.svg | 0 ui/src/components/access/AccessForm.tsx | 6 + .../AccessFormGoogleTrustServicesConfig.tsx | 82 +++++++++ .../access/AccessFormZeroSSLConfig.tsx | 76 ++++++++ .../provider/AccessProviderSelect.tsx | 23 +-- ui/src/domain/access.ts | 12 ++ ui/src/domain/provider.ts | 110 ++++++----- ui/src/domain/settings.ts | 4 +- ui/src/i18n/locales/en/nls.access.json | 20 +- ui/src/i18n/locales/en/nls.provider.json | 4 + ui/src/i18n/locales/en/nls.settings.json | 8 +- ui/src/i18n/locales/zh/nls.access.json | 20 +- ui/src/i18n/locales/zh/nls.provider.json | 6 +- ui/src/pages/settings/SettingsSSLProvider.tsx | 8 +- 22 files changed, 592 insertions(+), 160 deletions(-) create mode 100644 migrations/1743264000_upgrade.go rename ui/public/imgs/{acme => providers}/google.svg (100%) rename ui/public/imgs/{acme => providers}/letsencrypt.svg (100%) rename ui/public/imgs/{acme => providers}/zerossl.svg (100%) create mode 100644 ui/src/components/access/AccessFormGoogleTrustServicesConfig.tsx create mode 100644 ui/src/components/access/AccessFormZeroSSLConfig.tsx diff --git a/internal/applicant/acme_ca.go b/internal/applicant/acme_ca.go index 57f24b6e..afd86bdf 100644 --- a/internal/applicant/acme_ca.go +++ b/internal/applicant/acme_ca.go @@ -1,25 +1,21 @@ package applicant -const ( - sslProviderLetsEncrypt = "letsencrypt" - sslProviderLetsEncryptStaging = "letsencrypt_staging" - sslProviderZeroSSL = "zerossl" - sslProviderGoogleTrustServices = "gts" -) -const defaultSSLProvider = sslProviderLetsEncrypt +import "github.com/usual2970/certimate/internal/domain" const ( - letsencryptUrl = "https://acme-v02.api.letsencrypt.org/directory" - letsencryptStagingUrl = "https://acme-staging-v02.api.letsencrypt.org/directory" - zerosslUrl = "https://acme.zerossl.com/v2/DV90" - gtsUrl = "https://dv.acme-v02.api.pki.goog/directory" + sslProviderLetsEncrypt = string(domain.ApplyCAProviderTypeLetsEncrypt) + sslProviderLetsEncryptStaging = string(domain.ApplyCAProviderTypeLetsEncryptStaging) + sslProviderGoogleTrustServices = string(domain.ApplyCAProviderTypeGoogleTrustServices) + sslProviderZeroSSL = string(domain.ApplyCAProviderTypeZeroSSL) + + sslProviderDefault = sslProviderLetsEncrypt ) var sslProviderUrls = map[string]string{ - sslProviderLetsEncrypt: letsencryptUrl, - sslProviderLetsEncryptStaging: letsencryptStagingUrl, - sslProviderZeroSSL: zerosslUrl, - sslProviderGoogleTrustServices: gtsUrl, + sslProviderLetsEncrypt: "https://acme-v02.api.letsencrypt.org/directory", + sslProviderLetsEncryptStaging: "https://acme-staging-v02.api.letsencrypt.org/directory", + sslProviderGoogleTrustServices: "https://dv.acme-v02.api.pki.goog/directory", + sslProviderZeroSSL: "https://acme.zerossl.com/v2/DV90", } type acmeSSLProviderConfig struct { @@ -29,7 +25,7 @@ type acmeSSLProviderConfig struct { type acmeSSLProviderConfigContent struct { ZeroSSL acmeSSLProviderEabConfig `json:"zerossl"` - GoogleTrustServices acmeSSLProviderEabConfig `json:"gts"` + GoogleTrustServices acmeSSLProviderEabConfig `json:"googletrustservices"` } type acmeSSLProviderEabConfig struct { diff --git a/internal/applicant/applicant.go b/internal/applicant/applicant.go index 8cd8b6d0..8b452aab 100644 --- a/internal/applicant/applicant.go +++ b/internal/applicant/applicant.go @@ -111,7 +111,7 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap sslProviderConfig := &acmeSSLProviderConfig{ Config: acmeSSLProviderConfigContent{}, - Provider: defaultSSLProvider, + Provider: sslProviderDefault, } if settings != nil { if err := json.Unmarshal([]byte(settings.Content), sslProviderConfig); err != nil { @@ -120,7 +120,7 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap } if sslProviderConfig.Provider == "" { - sslProviderConfig.Provider = defaultSSLProvider + sslProviderConfig.Provider = sslProviderDefault } acmeUser, err := newAcmeUser(sslProviderConfig.Provider, options.ContactEmail) diff --git a/internal/domain/access.go b/internal/domain/access.go index dc796c83..e88a3906 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -125,6 +125,11 @@ type AccessConfigForGoDaddy struct { ApiSecret string `json:"apiSecret"` } +type AccessConfigForGoogleTrustServices struct { + EabKid string `json:"eabKid"` + EabHmacKey string `json:"eabHmacKey"` +} + type AccessConfigForHuaweiCloud struct { AccessKeyId string `json:"accessKeyId"` SecretAccessKey string `json:"secretAccessKey"` @@ -228,3 +233,8 @@ type AccessConfigForWestcn struct { Username string `json:"username"` ApiPassword string `json:"password"` } + +type AccessConfigForZeroSSL struct { + EabKid string `json:"eabKid"` + EabHmacKey string `json:"eabHmacKey"` +} diff --git a/internal/domain/provider.go b/internal/domain/provider.go index 0e4bcce3..26b7054a 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -9,55 +9,75 @@ type AccessProviderType string NOTICE: If you add new constant, please keep ASCII order. */ const ( - AccessProviderType1Panel = AccessProviderType("1panel") - AccessProviderTypeACMEHttpReq = AccessProviderType("acmehttpreq") - AccessProviderTypeAkamai = AccessProviderType("akamai") // Akamai(预留) - AccessProviderTypeAliyun = AccessProviderType("aliyun") - AccessProviderTypeAWS = AccessProviderType("aws") - AccessProviderTypeAzure = AccessProviderType("azure") - AccessProviderTypeBaiduCloud = AccessProviderType("baiducloud") - AccessProviderTypeBaishan = AccessProviderType("baishan") - AccessProviderTypeBaotaPanel = AccessProviderType("baotapanel") - AccessProviderTypeBytePlus = AccessProviderType("byteplus") - AccessProviderTypeCacheFly = AccessProviderType("cachefly") - AccessProviderTypeCdnfly = AccessProviderType("cdnfly") - AccessProviderTypeCloudflare = AccessProviderType("cloudflare") - AccessProviderTypeClouDNS = AccessProviderType("cloudns") - AccessProviderTypeCMCCCloud = AccessProviderType("cmcccloud") - AccessProviderTypeCTCCCloud = AccessProviderType("ctcccloud") // 联通云(预留) - AccessProviderTypeCUCCCloud = AccessProviderType("cucccloud") // 天翼云(预留) - AccessProviderTypeDeSEC = AccessProviderType("desec") - AccessProviderTypeDNSLA = AccessProviderType("dnsla") - AccessProviderTypeDogeCloud = AccessProviderType("dogecloud") - AccessProviderTypeDynv6 = AccessProviderType("dynv6") - AccessProviderTypeEdgio = AccessProviderType("edgio") - AccessProviderTypeFastly = AccessProviderType("fastly") // Fastly(预留) - AccessProviderTypeGname = AccessProviderType("gname") - AccessProviderTypeGcore = AccessProviderType("gcore") - AccessProviderTypeGoDaddy = AccessProviderType("godaddy") - AccessProviderTypeGoEdge = AccessProviderType("goedge") // GoEdge(预留) - AccessProviderTypeHuaweiCloud = AccessProviderType("huaweicloud") - AccessProviderTypeJDCloud = AccessProviderType("jdcloud") - AccessProviderTypeKubernetes = AccessProviderType("k8s") - AccessProviderTypeLocal = AccessProviderType("local") - AccessProviderTypeNamecheap = AccessProviderType("namecheap") - AccessProviderTypeNameDotCom = AccessProviderType("namedotcom") - AccessProviderTypeNameSilo = AccessProviderType("namesilo") - AccessProviderTypeNS1 = AccessProviderType("ns1") - AccessProviderTypePorkbun = AccessProviderType("porkbun") - AccessProviderTypePowerDNS = AccessProviderType("powerdns") - AccessProviderTypeQiniu = AccessProviderType("qiniu") - AccessProviderTypeQingCloud = AccessProviderType("qingcloud") // 青云(预留) - AccessProviderTypeRainYun = AccessProviderType("rainyun") - AccessProviderTypeSafeLine = AccessProviderType("safeline") - AccessProviderTypeSSH = AccessProviderType("ssh") - AccessProviderTypeTencentCloud = AccessProviderType("tencentcloud") - AccessProviderTypeUCloud = AccessProviderType("ucloud") - AccessProviderTypeUpyun = AccessProviderType("upyun") - AccessProviderTypeVercel = AccessProviderType("vercel") - AccessProviderTypeVolcEngine = AccessProviderType("volcengine") - AccessProviderTypeWebhook = AccessProviderType("webhook") - AccessProviderTypeWestcn = AccessProviderType("westcn") + AccessProviderType1Panel = AccessProviderType("1panel") + AccessProviderTypeACMEHttpReq = AccessProviderType("acmehttpreq") + AccessProviderTypeAkamai = AccessProviderType("akamai") // Akamai(预留) + AccessProviderTypeAliyun = AccessProviderType("aliyun") + AccessProviderTypeAWS = AccessProviderType("aws") + AccessProviderTypeAzure = AccessProviderType("azure") + AccessProviderTypeBaiduCloud = AccessProviderType("baiducloud") + AccessProviderTypeBaishan = AccessProviderType("baishan") + AccessProviderTypeBaotaPanel = AccessProviderType("baotapanel") + AccessProviderTypeBytePlus = AccessProviderType("byteplus") + AccessProviderTypeCacheFly = AccessProviderType("cachefly") + AccessProviderTypeCdnfly = AccessProviderType("cdnfly") + AccessProviderTypeCloudflare = AccessProviderType("cloudflare") + AccessProviderTypeClouDNS = AccessProviderType("cloudns") + AccessProviderTypeCMCCCloud = AccessProviderType("cmcccloud") + AccessProviderTypeCTCCCloud = AccessProviderType("ctcccloud") // 联通云(预留) + AccessProviderTypeCUCCCloud = AccessProviderType("cucccloud") // 天翼云(预留) + AccessProviderTypeDeSEC = AccessProviderType("desec") + AccessProviderTypeDNSLA = AccessProviderType("dnsla") + AccessProviderTypeDogeCloud = AccessProviderType("dogecloud") + AccessProviderTypeDynv6 = AccessProviderType("dynv6") + AccessProviderTypeEdgio = AccessProviderType("edgio") + AccessProviderTypeFastly = AccessProviderType("fastly") // Fastly(预留) + AccessProviderTypeGname = AccessProviderType("gname") + AccessProviderTypeGcore = AccessProviderType("gcore") + AccessProviderTypeGoDaddy = AccessProviderType("godaddy") + AccessProviderTypeGoEdge = AccessProviderType("goedge") // GoEdge(预留) + AccessProviderTypeGoogleTrustServices = AccessProviderType("googletrustservices") + AccessProviderTypeHuaweiCloud = AccessProviderType("huaweicloud") + AccessProviderTypeJDCloud = AccessProviderType("jdcloud") + AccessProviderTypeKubernetes = AccessProviderType("k8s") + AccessProviderTypeLetsEncrypt = AccessProviderType("letsencrypt") + AccessProviderTypeLetsEncryptStaging = AccessProviderType("letsencryptstaging") + AccessProviderTypeLocal = AccessProviderType("local") + AccessProviderTypeNamecheap = AccessProviderType("namecheap") + AccessProviderTypeNameDotCom = AccessProviderType("namedotcom") + AccessProviderTypeNameSilo = AccessProviderType("namesilo") + AccessProviderTypeNS1 = AccessProviderType("ns1") + AccessProviderTypePorkbun = AccessProviderType("porkbun") + AccessProviderTypePowerDNS = AccessProviderType("powerdns") + AccessProviderTypeQiniu = AccessProviderType("qiniu") + AccessProviderTypeQingCloud = AccessProviderType("qingcloud") // 青云(预留) + AccessProviderTypeRainYun = AccessProviderType("rainyun") + AccessProviderTypeSafeLine = AccessProviderType("safeline") + AccessProviderTypeSSH = AccessProviderType("ssh") + AccessProviderTypeTencentCloud = AccessProviderType("tencentcloud") + AccessProviderTypeUCloud = AccessProviderType("ucloud") + AccessProviderTypeUpyun = AccessProviderType("upyun") + AccessProviderTypeVercel = AccessProviderType("vercel") + AccessProviderTypeVolcEngine = AccessProviderType("volcengine") + AccessProviderTypeWebhook = AccessProviderType("webhook") + AccessProviderTypeWestcn = AccessProviderType("westcn") + AccessProviderTypeZeroSSL = AccessProviderType("zerossl") +) + +type ApplyCAProviderType string + +/* +申请证书 CA 提供商常量值。 +始终等于授权提供商类型。 + + 注意:如果追加新的常量值,请保持以 ASCII 排序。 + NOTICE: If you add new constant, please keep ASCII order. +*/ +const ( + ApplyCAProviderTypeGoogleTrustServices = ApplyCAProviderType("googletrustservices") + ApplyCAProviderTypeLetsEncrypt = ApplyCAProviderType("letsencrypt") + ApplyCAProviderTypeLetsEncryptStaging = ApplyCAProviderType("letsencryptstaging") + ApplyCAProviderTypeZeroSSL = ApplyCAProviderType("zerossl") ) type ApplyDNSProviderType string @@ -111,7 +131,7 @@ const ( type DeployProviderType string /* -部署目标提供商常量值。 +部署证书主机提供商常量值。 短横线前的部分始终等于授权提供商类型。 注意:如果追加新的常量值,请保持以 ASCII 排序。 diff --git a/migrations/1742209200_upgrade.go b/migrations/1742209200_upgrade.go index 0a980972..d2ed7f9d 100644 --- a/migrations/1742209200_upgrade.go +++ b/migrations/1742209200_upgrade.go @@ -7,8 +7,6 @@ import ( "github.com/pocketbase/pocketbase/core" m "github.com/pocketbase/pocketbase/migrations" - - "github.com/usual2970/certimate/internal/domain" ) func init() { @@ -179,20 +177,20 @@ func init() { } for _, workflowRun := range workflowRuns { - type oldWorkflowRunLogRecord struct { + type dWorkflowRunLogRecord struct { Time string `json:"time"` Level string `json:"level"` Content string `json:"content"` Error string `json:"error"` } - type oldWorkflowRunLog struct { - NodeId string `json:"nodeId"` - NodeName string `json:"nodeName"` - Records []oldWorkflowRunLogRecord `json:"records"` - Error string `json:"error"` + type dWorkflowRunLog struct { + NodeId string `json:"nodeId"` + NodeName string `json:"nodeName"` + Records []dWorkflowRunLogRecord `json:"records"` + Error string `json:"error"` } - logs := make([]oldWorkflowRunLog, 0) + logs := make([]dWorkflowRunLog, 0) if err := workflowRun.UnmarshalJSONField("logs", &logs); err != nil { continue } @@ -259,8 +257,20 @@ func init() { return err } + type dWorkflowNode struct { + Id string `json:"id"` + Type string `json:"type"` + Name string `json:"name"` + Config map[string]any `json:"config"` + Inputs map[string]any `json:"inputs"` + Outputs map[string]any `json:"outputs"` + Next *dWorkflowNode `json:"next,omitempty"` + Branches []dWorkflowNode `json:"branches,omitempty"` + Validated bool `json:"validated"` + } + for _, workflowRun := range workflowRuns { - node := &domain.WorkflowNode{} + node := &dWorkflowNode{} for _, workflowOutput := range workflowOutputs { if workflowOutput.GetString("runId") != workflowRun.Get("id") { continue @@ -270,8 +280,8 @@ func init() { continue } - if node.Type != domain.WorkflowNodeTypeApply { - node = &domain.WorkflowNode{} + if node.Type != "apply" { + node = &dWorkflowNode{} continue } } @@ -286,7 +296,7 @@ func init() { } else { workflow, _ := app.FindRecordById("workflow", workflowRun.GetString("workflowId")) if workflow != nil { - rootNode := &domain.WorkflowNode{} + rootNode := &dWorkflowNode{} if err := workflow.UnmarshalJSONField("content", rootNode); err != nil { return err } @@ -294,9 +304,9 @@ func init() { rootNode.Next = node workflowRun.Set("detail", rootNode) } else { - rootNode := &domain.WorkflowNode{ + rootNode := &dWorkflowNode{ Id: core.GenerateDefaultRandomId(), - Type: domain.WorkflowNodeTypeStart, + Type: "start", Name: "开始", Config: map[string]any{ "trigger": "manual", diff --git a/migrations/1743264000_upgrade.go b/migrations/1743264000_upgrade.go new file mode 100644 index 00000000..50fd93f6 --- /dev/null +++ b/migrations/1743264000_upgrade.go @@ -0,0 +1,171 @@ +package migrations + +import ( + "github.com/pocketbase/pocketbase/core" + m "github.com/pocketbase/pocketbase/migrations" +) + +func init() { + m.Register(func(app core.App) error { + // update collection `settings` + { + collection, err := app.FindCollectionByNameOrId("dy6ccjb60spfy6p") + if err != nil { + return err + } + + records, err := app.FindRecordsByFilter(collection, "name='sslProvider'", "-created", 1, 0) + if err != nil { + return err + } + + if len(records) == 1 { + record := records[0] + + content := make(map[string]any) + if err := record.UnmarshalJSONField("content", &content); err != nil { + return err + } + + if provider, ok := content["provider"]; ok { + if providerStr, ok := provider.(string); ok { + if providerStr == "letsencrypt_staging" { + content["provider"] = "letsencryptstaging" + } + } + } + + if config, ok := content["config"]; ok { + if configMap, ok := config.(map[string]any); ok { + if _, ok := configMap["letsencrypt_staging"]; ok { + configMap["letsencryptstaging"] = configMap["letsencrypt_staging"] + delete(configMap, "letsencrypt_staging") + } + if _, ok := configMap["gts"]; ok { + configMap["googletrustservices"] = configMap["gts"] + delete(configMap, "gts") + } + } + } + + record.Set("content", content) + if err := app.Save(record); err != nil { + return err + } + } + } + + // update collection `access` + { + collection, err := app.FindCollectionByNameOrId("4yzbv8urny5ja1e") + if err != nil { + return err + } + + // update field + if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{ + "hidden": false, + "id": "hwy7m03o", + "maxSelect": 1, + "name": "provider", + "presentable": false, + "required": false, + "system": false, + "type": "select", + "values": [ + "1panel", + "acmehttpreq", + "akamai", + "aliyun", + "aws", + "azure", + "baiducloud", + "baishan", + "baotapanel", + "byteplus", + "cachefly", + "cdnfly", + "cloudflare", + "cloudns", + "cmcccloud", + "ctcccloud", + "cucccloud", + "desec", + "dnsla", + "dogecloud", + "dynv6", + "edgio", + "fastly", + "gname", + "gcore", + "godaddy", + "goedge", + "googletrustservices", + "huaweicloud", + "jdcloud", + "k8s", + "letsencrypt", + "letsencryptstaging", + "local", + "namecheap", + "namedotcom", + "namesilo", + "ns1", + "porkbun", + "powerdns", + "qiniu", + "qingcloud", + "rainyun", + "safeline", + "ssh", + "tencentcloud", + "ucloud", + "upyun", + "vercel", + "volcengine", + "webhook", + "westcn", + "zerossl" + ] + }`)); err != nil { + return err + } + + if err := app.Save(collection); err != nil { + return err + } + } + + // update collection `acme_accounts` + { + collection, err := app.FindCollectionByNameOrId("012d7abbod1hwvr") + if err != nil { + return err + } + + records, err := app.FindRecordsByFilter(collection, "ca='letsencrypt_staging' || ca='gts'", "-created", 0, 0) + if err != nil { + return err + } + + for _, record := range records { + ca := record.GetString("ca") + if ca == "letsencrypt_staging" { + record.Set("ca", "letsencryptstaging") + } else if ca == "gts" { + record.Set("ca", "googletrustservices") + } else { + continue + } + + if err := app.Save(record); err != nil { + return err + } + } + } + + return nil + }, func(app core.App) error { + return nil + }) +} diff --git a/ui/public/imgs/acme/google.svg b/ui/public/imgs/providers/google.svg similarity index 100% rename from ui/public/imgs/acme/google.svg rename to ui/public/imgs/providers/google.svg diff --git a/ui/public/imgs/acme/letsencrypt.svg b/ui/public/imgs/providers/letsencrypt.svg similarity index 100% rename from ui/public/imgs/acme/letsencrypt.svg rename to ui/public/imgs/providers/letsencrypt.svg diff --git a/ui/public/imgs/acme/zerossl.svg b/ui/public/imgs/providers/zerossl.svg similarity index 100% rename from ui/public/imgs/acme/zerossl.svg rename to ui/public/imgs/providers/zerossl.svg diff --git a/ui/src/components/access/AccessForm.tsx b/ui/src/components/access/AccessForm.tsx index 487d449a..75660cb2 100644 --- a/ui/src/components/access/AccessForm.tsx +++ b/ui/src/components/access/AccessForm.tsx @@ -31,6 +31,7 @@ import AccessFormEdgioConfig from "./AccessFormEdgioConfig"; import AccessFormGcoreConfig from "./AccessFormGcoreConfig"; import AccessFormGnameConfig from "./AccessFormGnameConfig"; import AccessFormGoDaddyConfig from "./AccessFormGoDaddyConfig"; +import AccessFormGoogleTrustServicesConfig from "./AccessFormGoogleTrustServicesConfig"; import AccessFormHuaweiCloudConfig from "./AccessFormHuaweiCloudConfig"; import AccessFormJDCloudConfig from "./AccessFormJDCloudConfig"; import AccessFormKubernetesConfig from "./AccessFormKubernetesConfig"; @@ -52,6 +53,7 @@ import AccessFormVercelConfig from "./AccessFormVercelConfig"; import AccessFormVolcEngineConfig from "./AccessFormVolcEngineConfig"; import AccessFormWebhookConfig from "./AccessFormWebhookConfig"; import AccessFormWestcnConfig from "./AccessFormWestcnConfig"; +import AccessFormZeroSSLConfig from "./AccessFormZeroSSLConfig"; type AccessFormFieldValues = Partial>; type AccessFormPresets = "add" | "edit"; @@ -147,6 +149,8 @@ const AccessForm = forwardRef(({ className, return ; case ACCESS_PROVIDERS.GODADDY: return ; + case ACCESS_PROVIDERS.GOOGLETRUSTSERVICES: + return ; case ACCESS_PROVIDERS.EDGIO: return ; case ACCESS_PROVIDERS.HUAWEICLOUD: @@ -191,6 +195,8 @@ const AccessForm = forwardRef(({ className, return ; case ACCESS_PROVIDERS.WESTCN: return ; + case ACCESS_PROVIDERS.ZEROSSL: + return ; } }, [disabled, initialValues?.config, fieldProvider, nestedFormInst, nestedFormName]); diff --git a/ui/src/components/access/AccessFormGoogleTrustServicesConfig.tsx b/ui/src/components/access/AccessFormGoogleTrustServicesConfig.tsx new file mode 100644 index 00000000..95eb6270 --- /dev/null +++ b/ui/src/components/access/AccessFormGoogleTrustServicesConfig.tsx @@ -0,0 +1,82 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type AccessConfigForGoogleTrustServices } from "@/domain/access"; + +type AccessFormGoogleTrustServicesConfigFieldValues = Nullish; + +export type AccessFormGoogleTrustServicesConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: AccessFormGoogleTrustServicesConfigFieldValues; + onValuesChange?: (values: AccessFormGoogleTrustServicesConfigFieldValues) => void; +}; + +const initFormModel = (): AccessFormGoogleTrustServicesConfigFieldValues => { + return { + eabKid: "", + eabHmacKey: "", + }; +}; + +const AccessFormGoogleTrustServicesConfig = ({ + form: formInst, + formName, + disabled, + initialValues, + onValuesChange, +}: AccessFormGoogleTrustServicesConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + eabKid: z + .string() + .min(1, t("access.form.googletrustservices_eab_kid.placeholder")) + .max(256, t("common.errmsg.string_max", { max: 256 })) + .trim(), + eabHmacKey: z + .string() + .min(1, t("access.form.googletrustservices_eab_hmac_key.placeholder")) + .max(256, t("common.errmsg.string_max", { max: 256 })) + .trim(), + }); + const formRule = createSchemaFieldRule(formSchema); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ } + > + + + + } + > + + +
+ ); +}; + +export default AccessFormGoogleTrustServicesConfig; diff --git a/ui/src/components/access/AccessFormZeroSSLConfig.tsx b/ui/src/components/access/AccessFormZeroSSLConfig.tsx new file mode 100644 index 00000000..336777b0 --- /dev/null +++ b/ui/src/components/access/AccessFormZeroSSLConfig.tsx @@ -0,0 +1,76 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type AccessConfigForZeroSSL } from "@/domain/access"; + +type AccessFormZeroSSLConfigFieldValues = Nullish; + +export type AccessFormZeroSSLConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: AccessFormZeroSSLConfigFieldValues; + onValuesChange?: (values: AccessFormZeroSSLConfigFieldValues) => void; +}; + +const initFormModel = (): AccessFormZeroSSLConfigFieldValues => { + return { + eabKid: "", + eabHmacKey: "", + }; +}; + +const AccessFormZeroSSLConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormZeroSSLConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + eabKid: z + .string() + .min(1, t("access.form.zerossl_eab_kid.placeholder")) + .max(256, t("common.errmsg.string_max", { max: 256 })) + .trim(), + eabHmacKey: z + .string() + .min(1, t("access.form.zerossl_eab_hmac_key.placeholder")) + .max(256, t("common.errmsg.string_max", { max: 256 })) + .trim(), + }); + const formRule = createSchemaFieldRule(formSchema); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ } + > + + + + } + > + + +
+ ); +}; + +export default AccessFormZeroSSLConfig; diff --git a/ui/src/components/provider/AccessProviderSelect.tsx b/ui/src/components/provider/AccessProviderSelect.tsx index 0e0a992c..fb38419e 100644 --- a/ui/src/components/provider/AccessProviderSelect.tsx +++ b/ui/src/components/provider/AccessProviderSelect.tsx @@ -9,9 +9,10 @@ export type AccessProviderSelectProps = Omit< "filterOption" | "filterSort" | "labelRender" | "options" | "optionFilterProp" | "optionLabelProp" | "optionRender" > & { filter?: (record: AccessProvider) => boolean; + showOptionTags?: boolean; }; -const AccessProviderSelect = ({ filter, ...props }: AccessProviderSelectProps) => { +const AccessProviderSelect = ({ filter, showOptionTags, ...props }: AccessProviderSelectProps = { showOptionTags: true }) => { const { t } = useTranslation(); const [options, setOptions] = useState>([]); @@ -38,18 +39,14 @@ const AccessProviderSelect = ({ filter, ...props }: AccessProviderSelectProps) = {t(provider?.name ?? "")} -
- {provider?.usages?.includes(ACCESS_USAGES.APPLY) && ( - <> - {t("access.props.provider.usage.dns")} - - )} - {provider?.usages?.includes(ACCESS_USAGES.DEPLOY) && ( - <> - {t("access.props.provider.usage.host")} - - )} -
+ {!showOptionTags && ( +
+ {provider?.usages?.includes(ACCESS_USAGES.DNS) && {t("access.props.provider.usage.dns")}} + {provider?.usages?.includes(ACCESS_USAGES.HOSTING) && {t("access.props.provider.usage.hosting")}} + {/* {provider?.usages?.includes(ACCESS_USAGES.CA) && {t("access.props.provider.usage.ca")}} */} + {/* {provider?.usages?.includes(ACCESS_USAGES.NOTIFICATION) && {t("access.props.provider.usage.notification")}} */} +
+ )} ); }; diff --git a/ui/src/domain/access.ts b/ui/src/domain/access.ts index ed1bc1ee..0e14d2bf 100644 --- a/ui/src/domain/access.ts +++ b/ui/src/domain/access.ts @@ -28,6 +28,7 @@ export interface AccessModel extends BaseModel { | AccessConfigForGcore | AccessConfigForGname | AccessConfigForGoDaddy + | AccessConfigForGoogleTrustServices | AccessConfigForHuaweiCloud | AccessConfigForJDCloud | AccessConfigForKubernetes @@ -48,6 +49,7 @@ export interface AccessModel extends BaseModel { | AccessConfigForVolcEngine | AccessConfigForWebhook | AccessConfigForWestcn + | AccessConfigForZeroSSL ); } @@ -163,6 +165,11 @@ export type AccessConfigForGoDaddy = { apiSecret: string; }; +export type AccessConfigForGoogleTrustServices = { + eabKid: string; + eabHmacKey: string; +}; + export type AccessConfigForHuaweiCloud = { accessKeyId: string; secretAccessKey: string; @@ -266,4 +273,9 @@ export type AccessConfigForWestcn = { username: string; apiPassword: string; }; + +export type AccessConfigForZeroSSL = { + eabKid: string; + eabHmacKey: string; +}; // #endregion diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index d4f7ba46..31cd02e3 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -22,13 +22,16 @@ export const ACCESS_PROVIDERS = Object.freeze({ DNSLA: "dnsla", DOGECLOUD: "dogecloud", DYNV6: "dynv6", + EDGIO: "edgio", GCORE: "gcore", GNAME: "gname", GODADDY: "godaddy", - EDGIO: "edgio", + GOOGLETRUSTSERVICES: "googletrustservices", HUAWEICLOUD: "huaweicloud", JDCLOUD: "jdcloud", KUBERNETES: "k8s", + LETSENCRYPT: "letsencrypt", + LETSENCRYPTSTAGING: "letsencryptstaging", LOCAL: "local", NAMECHEAP: "namecheap", NAMEDOTCOM: "namedotcom", @@ -47,13 +50,16 @@ export const ACCESS_PROVIDERS = Object.freeze({ VOLCENGINE: "volcengine", WEBHOOK: "webhook", WESTCN: "westcn", + ZEROSSL: "zerossl", } as const); export type AccessProviderType = (typeof ACCESS_PROVIDERS)[keyof typeof ACCESS_PROVIDERS]; export const ACCESS_USAGES = Object.freeze({ - APPLY: "apply", - DEPLOY: "deploy", + DNS: "dns", + HOSTING: "hosting", + CA: "ca", + NOTIFICATION: "notification", } as const); export type AccessUsageType = (typeof ACCESS_USAGES)[keyof typeof ACCESS_USAGES]; @@ -67,55 +73,61 @@ export type AccessProvider = { export const accessProvidersMap: Map = new Map( /* - 注意:此处的顺序决定显示在前端的顺序。 - NOTICE: The following order determines the order displayed at the frontend. - */ + 注意:此处的顺序决定显示在前端的顺序。 + NOTICE: The following order determines the order displayed at the frontend. + */ [ - [ACCESS_PROVIDERS.LOCAL, "provider.local", "/imgs/providers/local.svg", [ACCESS_USAGES.DEPLOY]], - [ACCESS_PROVIDERS.SSH, "provider.ssh", "/imgs/providers/ssh.svg", [ACCESS_USAGES.DEPLOY]], - [ACCESS_PROVIDERS.WEBHOOK, "provider.webhook", "/imgs/providers/webhook.svg", [ACCESS_USAGES.DEPLOY]], - [ACCESS_PROVIDERS.KUBERNETES, "provider.kubernetes", "/imgs/providers/kubernetes.svg", [ACCESS_USAGES.DEPLOY]], - [ACCESS_PROVIDERS.ALIYUN, "provider.aliyun", "/imgs/providers/aliyun.svg", [ACCESS_USAGES.APPLY, ACCESS_USAGES.DEPLOY]], - [ACCESS_PROVIDERS.TENCENTCLOUD, "provider.tencentcloud", "/imgs/providers/tencentcloud.svg", [ACCESS_USAGES.APPLY, ACCESS_USAGES.DEPLOY]], - [ACCESS_PROVIDERS.BAIDUCLOUD, "provider.baiducloud", "/imgs/providers/baiducloud.svg", [ACCESS_USAGES.APPLY, ACCESS_USAGES.DEPLOY]], - [ACCESS_PROVIDERS.HUAWEICLOUD, "provider.huaweicloud", "/imgs/providers/huaweicloud.svg", [ACCESS_USAGES.APPLY, ACCESS_USAGES.DEPLOY]], - [ACCESS_PROVIDERS.VOLCENGINE, "provider.volcengine", "/imgs/providers/volcengine.svg", [ACCESS_USAGES.APPLY, ACCESS_USAGES.DEPLOY]], - [ACCESS_PROVIDERS.JDCLOUD, "provider.jdcloud", "/imgs/providers/jdcloud.svg", [ACCESS_USAGES.APPLY, ACCESS_USAGES.DEPLOY]], - [ACCESS_PROVIDERS.AWS, "provider.aws", "/imgs/providers/aws.svg", [ACCESS_USAGES.APPLY, ACCESS_USAGES.DEPLOY]], - [ACCESS_PROVIDERS.AZURE, "provider.azure", "/imgs/providers/azure.svg", [ACCESS_USAGES.APPLY, ACCESS_USAGES.DEPLOY]], - [ACCESS_PROVIDERS.GCORE, "provider.gcore", "/imgs/providers/gcore.png", [ACCESS_USAGES.APPLY, ACCESS_USAGES.DEPLOY]], + [ACCESS_PROVIDERS.LOCAL, "provider.local", "/imgs/providers/local.svg", [ACCESS_USAGES.HOSTING]], + [ACCESS_PROVIDERS.SSH, "provider.ssh", "/imgs/providers/ssh.svg", [ACCESS_USAGES.HOSTING]], + [ACCESS_PROVIDERS.WEBHOOK, "provider.webhook", "/imgs/providers/webhook.svg", [ACCESS_USAGES.HOSTING, ACCESS_USAGES.NOTIFICATION]], + [ACCESS_PROVIDERS.KUBERNETES, "provider.kubernetes", "/imgs/providers/kubernetes.svg", [ACCESS_USAGES.HOSTING]], - [ACCESS_PROVIDERS.QINIU, "provider.qiniu", "/imgs/providers/qiniu.svg", [ACCESS_USAGES.DEPLOY]], - [ACCESS_PROVIDERS.UPYUN, "provider.upyun", "/imgs/providers/upyun.svg", [ACCESS_USAGES.DEPLOY]], - [ACCESS_PROVIDERS.BAISHAN, "provider.baishan", "/imgs/providers/baishan.png", [ACCESS_USAGES.DEPLOY]], - [ACCESS_PROVIDERS.DOGECLOUD, "provider.dogecloud", "/imgs/providers/dogecloud.png", [ACCESS_USAGES.DEPLOY]], - [ACCESS_PROVIDERS.BYTEPLUS, "provider.byteplus", "/imgs/providers/byteplus.svg", [ACCESS_USAGES.DEPLOY]], - [ACCESS_PROVIDERS.UCLOUD, "provider.ucloud", "/imgs/providers/ucloud.svg", [ACCESS_USAGES.DEPLOY]], - [ACCESS_PROVIDERS.SAFELINE, "provider.safeline", "/imgs/providers/safeline.svg", [ACCESS_USAGES.DEPLOY]], - [ACCESS_PROVIDERS["1PANEL"], "provider.1panel", "/imgs/providers/1panel.svg", [ACCESS_USAGES.DEPLOY]], - [ACCESS_PROVIDERS.BAOTAPANEL, "provider.baotapanel", "/imgs/providers/baotapanel.svg", [ACCESS_USAGES.DEPLOY]], - [ACCESS_PROVIDERS.CACHEFLY, "provider.cachefly", "/imgs/providers/cachefly.png", [ACCESS_USAGES.DEPLOY]], - [ACCESS_PROVIDERS.CDNFLY, "provider.cdnfly", "/imgs/providers/cdnfly.png", [ACCESS_USAGES.DEPLOY]], - [ACCESS_PROVIDERS.EDGIO, "provider.edgio", "/imgs/providers/edgio.svg", [ACCESS_USAGES.DEPLOY]], + [ACCESS_PROVIDERS.ALIYUN, "provider.aliyun", "/imgs/providers/aliyun.svg", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]], + [ACCESS_PROVIDERS.TENCENTCLOUD, "provider.tencentcloud", "/imgs/providers/tencentcloud.svg", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]], + [ACCESS_PROVIDERS.BAIDUCLOUD, "provider.baiducloud", "/imgs/providers/baiducloud.svg", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]], + [ACCESS_PROVIDERS.HUAWEICLOUD, "provider.huaweicloud", "/imgs/providers/huaweicloud.svg", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]], + [ACCESS_PROVIDERS.VOLCENGINE, "provider.volcengine", "/imgs/providers/volcengine.svg", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]], + [ACCESS_PROVIDERS.JDCLOUD, "provider.jdcloud", "/imgs/providers/jdcloud.svg", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]], + [ACCESS_PROVIDERS.AWS, "provider.aws", "/imgs/providers/aws.svg", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]], + [ACCESS_PROVIDERS.AZURE, "provider.azure", "/imgs/providers/azure.svg", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]], + [ACCESS_PROVIDERS.GCORE, "provider.gcore", "/imgs/providers/gcore.png", [ACCESS_USAGES.DNS, ACCESS_USAGES.HOSTING]], - [ACCESS_PROVIDERS.CLOUDFLARE, "provider.cloudflare", "/imgs/providers/cloudflare.svg", [ACCESS_USAGES.APPLY]], - [ACCESS_PROVIDERS.CLOUDNS, "provider.cloudns", "/imgs/providers/cloudns.png", [ACCESS_USAGES.APPLY]], - [ACCESS_PROVIDERS.DESEC, "provider.desec", "/imgs/providers/desec.svg", [ACCESS_USAGES.APPLY]], - [ACCESS_PROVIDERS.DNSLA, "provider.dnsla", "/imgs/providers/dnsla.svg", [ACCESS_USAGES.APPLY]], - [ACCESS_PROVIDERS.DYNV6, "provider.dynv6", "/imgs/providers/dynv6.svg", [ACCESS_USAGES.APPLY]], - [ACCESS_PROVIDERS.GNAME, "provider.gname", "/imgs/providers/gname.png", [ACCESS_USAGES.APPLY]], - [ACCESS_PROVIDERS.GODADDY, "provider.godaddy", "/imgs/providers/godaddy.svg", [ACCESS_USAGES.APPLY]], - [ACCESS_PROVIDERS.NAMECHEAP, "provider.namecheap", "/imgs/providers/namecheap.svg", [ACCESS_USAGES.APPLY]], - [ACCESS_PROVIDERS.NAMEDOTCOM, "provider.namedotcom", "/imgs/providers/namedotcom.svg", [ACCESS_USAGES.APPLY]], - [ACCESS_PROVIDERS.NAMESILO, "provider.namesilo", "/imgs/providers/namesilo.svg", [ACCESS_USAGES.APPLY]], - [ACCESS_PROVIDERS.NS1, "provider.ns1", "/imgs/providers/ns1.svg", [ACCESS_USAGES.APPLY]], - [ACCESS_PROVIDERS.PORKBUN, "provider.porkbun", "/imgs/providers/porkbun.svg", [ACCESS_USAGES.APPLY]], - [ACCESS_PROVIDERS.VERCEL, "provider.vercel", "/imgs/providers/vercel.svg", [ACCESS_USAGES.APPLY]], - [ACCESS_PROVIDERS.CMCCCLOUD, "provider.cmcccloud", "/imgs/providers/cmcccloud.svg", [ACCESS_USAGES.APPLY]], - [ACCESS_PROVIDERS.RAINYUN, "provider.rainyun", "/imgs/providers/rainyun.svg", [ACCESS_USAGES.APPLY]], - [ACCESS_PROVIDERS.WESTCN, "provider.westcn", "/imgs/providers/westcn.svg", [ACCESS_USAGES.APPLY]], - [ACCESS_PROVIDERS.POWERDNS, "provider.powerdns", "/imgs/providers/powerdns.svg", [ACCESS_USAGES.APPLY]], - [ACCESS_PROVIDERS.ACMEHTTPREQ, "provider.acmehttpreq", "/imgs/providers/acmehttpreq.svg", [ACCESS_USAGES.APPLY]], + [ACCESS_PROVIDERS.QINIU, "provider.qiniu", "/imgs/providers/qiniu.svg", [ACCESS_USAGES.HOSTING]], + [ACCESS_PROVIDERS.UPYUN, "provider.upyun", "/imgs/providers/upyun.svg", [ACCESS_USAGES.HOSTING]], + [ACCESS_PROVIDERS.BAISHAN, "provider.baishan", "/imgs/providers/baishan.png", [ACCESS_USAGES.HOSTING]], + [ACCESS_PROVIDERS.DOGECLOUD, "provider.dogecloud", "/imgs/providers/dogecloud.png", [ACCESS_USAGES.HOSTING]], + [ACCESS_PROVIDERS.BYTEPLUS, "provider.byteplus", "/imgs/providers/byteplus.svg", [ACCESS_USAGES.HOSTING]], + [ACCESS_PROVIDERS.UCLOUD, "provider.ucloud", "/imgs/providers/ucloud.svg", [ACCESS_USAGES.HOSTING]], + [ACCESS_PROVIDERS.SAFELINE, "provider.safeline", "/imgs/providers/safeline.svg", [ACCESS_USAGES.HOSTING]], + [ACCESS_PROVIDERS["1PANEL"], "provider.1panel", "/imgs/providers/1panel.svg", [ACCESS_USAGES.HOSTING]], + [ACCESS_PROVIDERS.BAOTAPANEL, "provider.baotapanel", "/imgs/providers/baotapanel.svg", [ACCESS_USAGES.HOSTING]], + [ACCESS_PROVIDERS.CACHEFLY, "provider.cachefly", "/imgs/providers/cachefly.png", [ACCESS_USAGES.HOSTING]], + [ACCESS_PROVIDERS.CDNFLY, "provider.cdnfly", "/imgs/providers/cdnfly.png", [ACCESS_USAGES.HOSTING]], + [ACCESS_PROVIDERS.EDGIO, "provider.edgio", "/imgs/providers/edgio.svg", [ACCESS_USAGES.HOSTING]], + + [ACCESS_PROVIDERS.CLOUDFLARE, "provider.cloudflare", "/imgs/providers/cloudflare.svg", [ACCESS_USAGES.DNS]], + [ACCESS_PROVIDERS.CLOUDNS, "provider.cloudns", "/imgs/providers/cloudns.png", [ACCESS_USAGES.DNS]], + [ACCESS_PROVIDERS.DESEC, "provider.desec", "/imgs/providers/desec.svg", [ACCESS_USAGES.DNS]], + [ACCESS_PROVIDERS.DNSLA, "provider.dnsla", "/imgs/providers/dnsla.svg", [ACCESS_USAGES.DNS]], + [ACCESS_PROVIDERS.DYNV6, "provider.dynv6", "/imgs/providers/dynv6.svg", [ACCESS_USAGES.DNS]], + [ACCESS_PROVIDERS.GNAME, "provider.gname", "/imgs/providers/gname.png", [ACCESS_USAGES.DNS]], + [ACCESS_PROVIDERS.GODADDY, "provider.godaddy", "/imgs/providers/godaddy.svg", [ACCESS_USAGES.DNS]], + [ACCESS_PROVIDERS.NAMECHEAP, "provider.namecheap", "/imgs/providers/namecheap.svg", [ACCESS_USAGES.DNS]], + [ACCESS_PROVIDERS.NAMEDOTCOM, "provider.namedotcom", "/imgs/providers/namedotcom.svg", [ACCESS_USAGES.DNS]], + [ACCESS_PROVIDERS.NAMESILO, "provider.namesilo", "/imgs/providers/namesilo.svg", [ACCESS_USAGES.DNS]], + [ACCESS_PROVIDERS.NS1, "provider.ns1", "/imgs/providers/ns1.svg", [ACCESS_USAGES.DNS]], + [ACCESS_PROVIDERS.PORKBUN, "provider.porkbun", "/imgs/providers/porkbun.svg", [ACCESS_USAGES.DNS]], + [ACCESS_PROVIDERS.VERCEL, "provider.vercel", "/imgs/providers/vercel.svg", [ACCESS_USAGES.DNS]], + [ACCESS_PROVIDERS.CMCCCLOUD, "provider.cmcccloud", "/imgs/providers/cmcccloud.svg", [ACCESS_USAGES.DNS]], + [ACCESS_PROVIDERS.RAINYUN, "provider.rainyun", "/imgs/providers/rainyun.svg", [ACCESS_USAGES.DNS]], + [ACCESS_PROVIDERS.WESTCN, "provider.westcn", "/imgs/providers/westcn.svg", [ACCESS_USAGES.DNS]], + [ACCESS_PROVIDERS.POWERDNS, "provider.powerdns", "/imgs/providers/powerdns.svg", [ACCESS_USAGES.DNS]], + [ACCESS_PROVIDERS.ACMEHTTPREQ, "provider.acmehttpreq", "/imgs/providers/acmehttpreq.svg", [ACCESS_USAGES.DNS]], + + [ACCESS_PROVIDERS.LETSENCRYPT, "provider.letsencrypt", "/imgs/providers/letsencrypt.svg", [ACCESS_USAGES.CA]], + [ACCESS_PROVIDERS.LETSENCRYPTSTAGING, "provider.letsencryptstaging", "/imgs/providers/letsencrypt.svg", [ACCESS_USAGES.CA]], + [ACCESS_PROVIDERS.GOOGLETRUSTSERVICES, "provider.googletrustservices", "/imgs/providers/google.svg", [ACCESS_USAGES.CA]], + [ACCESS_PROVIDERS.ZEROSSL, "provider.zerossl", "/imgs/providers/zerossl.svg", [ACCESS_USAGES.CA]], ].map((e) => [ e[0] as string, { diff --git a/ui/src/domain/settings.ts b/ui/src/domain/settings.ts index e34105e0..445e1dc6 100644 --- a/ui/src/domain/settings.ts +++ b/ui/src/domain/settings.ts @@ -136,9 +136,9 @@ export const notifyChannelsMap: Map = new // #region Settings: SSLProvider export const SSLPROVIDERS = Object.freeze({ LETS_ENCRYPT: "letsencrypt", - LETS_ENCRYPT_STAGING: "letsencrypt_staging", + LETS_ENCRYPT_STAGING: "letsencryptstaging", ZERO_SSL: "zerossl", - GOOGLE_TRUST_SERVICES: "gts", + GOOGLE_TRUST_SERVICES: "googletrustservices", } as const); export type SSLProviders = (typeof SSLPROVIDERS)[keyof typeof SSLPROVIDERS]; diff --git a/ui/src/i18n/locales/en/nls.access.json b/ui/src/i18n/locales/en/nls.access.json index ef2f481d..5cb4e7b0 100644 --- a/ui/src/i18n/locales/en/nls.access.json +++ b/ui/src/i18n/locales/en/nls.access.json @@ -14,7 +14,11 @@ "access.props.name": "Name", "access.props.provider": "Provider", "access.props.provider.usage.dns": "DNS provider", - "access.props.provider.usage.host": "Host provider", + "access.props.provider.usage.hosting": "Hosting provider", + "access.props.provider.usage.ca": "CA", + "access.props.provider.usage.notification": "Notification channel", + "access.props.ca": "Certificate authority", + "access.props.channel": "Notification channel", "access.props.created_at": "Created at", "access.props.updated_at": "Updated at", @@ -165,6 +169,12 @@ "access.form.godaddy_api_secret.label": "GoDaddy API secret", "access.form.godaddy_api_secret.placeholder": "Please enter GoDaddy API secret", "access.form.godaddy_api_secret.tooltip": "For more information, see https://developer.godaddy.com/", + "access.form.googletrustservices_eab_kid.label": "ACME EAB KID", + "access.form.googletrustservices_eab_kid.placeholder": "Please enter ACME EAB KID", + "access.form.googletrustservices_eab_kid.tooltip": "For more information, see https://cloud.google.com/certificate-manager/docs/public-ca-tutorial", + "access.form.googletrustservices_eab_hmac_key.label": "ACME EAB HMAC key", + "access.form.googletrustservices_eab_hmac_key.placeholder": "Please enter ACME EAB HMAC key", + "access.form.googletrustservices_eab_hmac_key.tooltip": "For more information, see https://cloud.google.com/certificate-manager/docs/public-ca-tutorial", "access.form.huaweicloud_access_key_id.label": "Huawei Cloud AccessKeyId", "access.form.huaweicloud_access_key_id.placeholder": "Please enter Huawei Cloud AccessKeyId", "access.form.huaweicloud_access_key_id.tooltip": "For more information, see https://support.huaweicloud.com/intl/en-us/usermanual-ca/ca_01_0003.html", @@ -284,5 +294,11 @@ "access.form.westcn_username.tooltip": "For more information, see https://www.west.cn/CustomerCenter/doc/apiv2.html", "access.form.westcn_api_password.label": "West.cn API password", "access.form.westcn_api_password.placeholder": "Please enter West.cn API password", - "access.form.westcn_api_password.tooltip": "For more information, see https://www.west.cn/CustomerCenter/doc/apiv2.html" + "access.form.westcn_api_password.tooltip": "For more information, see https://www.west.cn/CustomerCenter/doc/apiv2.html", + "access.form.zerossl_eab_kid.label": "ACME EAB KID", + "access.form.zerossl_eab_kid.placeholder": "Please enter ACME EAB KID", + "access.form.zerossl_eab_kid.tooltip": "For more information, see https://zerossl.com/documentation/acme/", + "access.form.zerossl_eab_hmac_key.label": "ACME EAB HMAC key", + "access.form.zerossl_eab_hmac_key.placeholder": "Please enter ACME EAB HMAC key", + "access.form.zerossl_eab_hmac_key.tooltip": "For more information, see https://zerossl.com/documentation/acme/" } diff --git a/ui/src/i18n/locales/en/nls.provider.json b/ui/src/i18n/locales/en/nls.provider.json index b7c3fc93..f2e286a9 100644 --- a/ui/src/i18n/locales/en/nls.provider.json +++ b/ui/src/i18n/locales/en/nls.provider.json @@ -61,6 +61,7 @@ "provider.godaddy": "GoDaddy", "provider.goedge": "GoEdge", "provider.goedge.cdn": "GoEdge - CDN (Content Delivery Network)", + "provider.googletrustservices": "Google Trust Services", "provider.huaweicloud": "Huawei Cloud", "provider.huaweicloud.cdn": "Huawei Cloud - CDN (Content Delivery Network)", "provider.huaweicloud.dns": "Huawei Cloud - DNS (Domain Name Service)", @@ -75,6 +76,8 @@ "provider.jdcloud.vod": "JD Cloud - VOD (Video on Demand)", "provider.kubernetes": "Kubernetes", "provider.kubernetes.secret": "Kubernetes - Secret", + "provider.letsencrypt": "Let's Encrypt", + "provider.letsencryptstaging": "Let's Encrypt Staging Environment", "provider.local": "Local deployment", "provider.namecheap": "Namecheap", "provider.namedotcom": "Name.com", @@ -121,6 +124,7 @@ "provider.volcengine.tos": "Volcengine - TOS (Tinder Object Storage)", "provider.webhook": "Webhook", "provider.westcn": "West.cn", + "provider.zerossl": "ZeroSSL", "provider.category.all": "All", "provider.category.cdn": "CDN", diff --git a/ui/src/i18n/locales/en/nls.settings.json b/ui/src/i18n/locales/en/nls.settings.json index 74e869bd..7c73f6e0 100644 --- a/ui/src/i18n/locales/en/nls.settings.json +++ b/ui/src/i18n/locales/en/nls.settings.json @@ -82,14 +82,14 @@ "settings.sslprovider.form.zerossl_eab_kid.label": "EAB KID", "settings.sslprovider.form.zerossl_eab_kid.placeholder": "Please enter EAB KID", "settings.sslprovider.form.zerossl_eab_kid.tooltip": "For more information, see https://zerossl.com/documentation/acme/", - "settings.sslprovider.form.zerossl_eab_hmac_key.label": "EAB HMAC Key", - "settings.sslprovider.form.zerossl_eab_hmac_key.placeholder": "Please enter EAB HMAC Key", + "settings.sslprovider.form.zerossl_eab_hmac_key.label": "EAB HMAC key", + "settings.sslprovider.form.zerossl_eab_hmac_key.placeholder": "Please enter EAB HMAC key", "settings.sslprovider.form.zerossl_eab_hmac_key.tooltip": "For more information, see https://zerossl.com/documentation/acme/", "settings.sslprovider.form.gts_eab_kid.label": "EAB KID", "settings.sslprovider.form.gts_eab_kid.placeholder": "Please enter EAB KID", "settings.sslprovider.form.gts_eab_kid.tooltip": "For more information, see https://cloud.google.com/certificate-manager/docs/public-ca-tutorial", - "settings.sslprovider.form.gts_eab_hmac_key.label": "EAB HMAC Key", - "settings.sslprovider.form.gts_eab_hmac_key.placeholder": "Please enter EAB HMAC Key", + "settings.sslprovider.form.gts_eab_hmac_key.label": "EAB HMAC key", + "settings.sslprovider.form.gts_eab_hmac_key.placeholder": "Please enter EAB HMAC key", "settings.sslprovider.form.gts_eab_hmac_key.tooltip": "For more information, see https://cloud.google.com/certificate-manager/docs/public-ca-tutorial", "settings.persistence.tab": "Persistence", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index bec170b6..016392fb 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -14,7 +14,11 @@ "access.props.name": "名称", "access.props.provider": "提供商", "access.props.provider.usage.dns": "DNS 提供商", - "access.props.provider.usage.host": "主机提供商", + "access.props.provider.usage.hosting": "主机提供商", + "access.props.provider.usage.ca": "证书颁发机构", + "access.props.provider.usage.notification": "通知渠道", + "access.props.ca": "证书颁发机构", + "access.props.channel": "通知渠道", "access.props.created_at": "创建时间", "access.props.updated_at": "更新时间", @@ -159,6 +163,12 @@ "access.form.godaddy_api_secret.label": "GoDaddy API Secret", "access.form.godaddy_api_secret.placeholder": "请输入 GoDaddy API Secret", "access.form.godaddy_api_secret.tooltip": "这是什么?请参阅 https://developer.godaddy.com/", + "access.form.googletrustservices_eab_kid.label": "ACME EAB KID", + "access.form.googletrustservices_eab_kid.placeholder": "请输入 ACME EAB KID", + "access.form.googletrustservices_eab_kid.tooltip": "这是什么?请参阅 https://cloud.google.com/certificate-manager/docs/public-ca-tutorial", + "access.form.googletrustservices_eab_hmac_key.label": "ACME EAB HMAC Key", + "access.form.googletrustservices_eab_hmac_key.placeholder": "请输入 ACME EAB HMAC Key", + "access.form.googletrustservices_eab_hmac_key.tooltip": "这是什么?请参阅 https://cloud.google.com/certificate-manager/docs/public-ca-tutorial", "access.form.huaweicloud_access_key_id.label": "华为云 AccessKeyId", "access.form.huaweicloud_access_key_id.placeholder": "请输入华为云 AccessKeyId", "access.form.huaweicloud_access_key_id.tooltip": "这是什么?请参阅 https://support.huaweicloud.com/usermanual-ca/ca_01_0003.html", @@ -284,5 +294,11 @@ "access.form.westcn_username.tooltip": "这是什么?请参阅 https://www.west.cn/CustomerCenter/doc/apiv2.html", "access.form.westcn_api_password.label": "西部数码 API 密码", "access.form.westcn_api_password.placeholder": "请输入西部数码 API 密码", - "access.form.westcn_api_password.tooltip": "这是什么?请参阅 https://www.west.cn/CustomerCenter/doc/apiv2.html" + "access.form.westcn_api_password.tooltip": "这是什么?请参阅 https://www.west.cn/CustomerCenter/doc/apiv2.html", + "access.form.zerossl_eab_kid.label": "ACME EAB KID", + "access.form.zerossl_eab_kid.placeholder": "请输入 ACME EAB KID", + "access.form.zerossl_eab_kid.tooltip": "这是什么?请参阅 https://zerossl.com/documentation/acme/", + "access.form.zerossl_eab_hmac_key.label": "ACME EAB HMAC Key", + "access.form.zerossl_eab_hmac_key.placeholder": "请输入 ACME EAB HMAC Key", + "access.form.zerossl_eab_hmac_key.tooltip": "这是什么?请参阅 https://zerossl.com/documentation/acme/" } diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json index 62806a9d..341ceb1b 100644 --- a/ui/src/i18n/locales/zh/nls.provider.json +++ b/ui/src/i18n/locales/zh/nls.provider.json @@ -61,6 +61,7 @@ "provider.godaddy": "GoDaddy", "provider.goedge": "GoEdge", "provider.goedge.cdn": "GoEdge - 内容分发网络 CDN", + "provider.googletrustservices": "Google Trust Services", "provider.huaweicloud": "华为云", "provider.huaweicloud.cdn": "华为云 - 内容分发网络 CDN", "provider.huaweicloud.dns": "华为云 - 云解析 DNS", @@ -75,11 +76,13 @@ "provider.jdcloud.vod": "京东云 - 视频点播", "provider.kubernetes": "Kubernetes", "provider.kubernetes.secret": "Kubernetes - Secret", + "provider.letsencrypt": "Let's Encrypt", + "provider.letsencryptstaging": "Let's Encrypt 测试环境", "provider.local": "本地部署", "provider.namecheap": "Namecheap", "provider.namedotcom": "Name.com", "provider.namesilo": "NameSilo", - "provider.ns1": "NS1(IBM NS1 Connect)", + "provider.ns1": "NS1 (IBM NS1 Connect)", "provider.porkbun": "Porkbun", "provider.powerdns": "PowerDNS", "provider.qiniu": "七牛云", @@ -121,6 +124,7 @@ "provider.volcengine.tos": "火山引擎 - 对象存储 TOS", "provider.webhook": "Webhook", "provider.westcn": "西部数码", + "provider.zerossl": "ZeroSSL", "provider.category.all": "全部", "provider.category.cdn": "CDN", diff --git a/ui/src/pages/settings/SettingsSSLProvider.tsx b/ui/src/pages/settings/SettingsSSLProvider.tsx index c082c4e7..face7d9d 100644 --- a/ui/src/pages/settings/SettingsSSLProvider.tsx +++ b/ui/src/pages/settings/SettingsSSLProvider.tsx @@ -317,28 +317,28 @@ const SettingsSSLProvider = () => { setProviderType(value as SSLProviders)}> } + avatar={} size="small" title={t("settings.sslprovider.form.provider.option.letsencrypt.label")} description="letsencrypt.org" value={SSLPROVIDERS.LETS_ENCRYPT} /> } + avatar={} size="small" title={t("settings.sslprovider.form.provider.option.letsencrypt_staging.label")} description="letsencrypt.org" value={SSLPROVIDERS.LETS_ENCRYPT_STAGING} /> } + avatar={} size="small" title={t("settings.sslprovider.form.provider.option.zerossl.label")} description="zerossl.com" value={SSLPROVIDERS.ZERO_SSL} /> } + avatar={} size="small" title={t("settings.sslprovider.form.provider.option.gts.label")} description="pki.goog" From 6ad0d8e42fe309848cd2c0734ec6967006a41074 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Sun, 30 Mar 2025 13:09:18 +0800 Subject: [PATCH 02/20] feat: support configuring independent ca in workflows --- internal/applicant/acme_ca.go | 14 +- internal/applicant/acme_user.go | 67 +++++---- internal/applicant/applicant.go | 119 ++++++++------- internal/applicant/providers.go | 10 +- internal/domain/workflow.go | 100 ++++++------- internal/pkg/utils/maputil/getter.go | 22 +++ .../workflow/node-processor/apply_node.go | 12 ++ .../components/provider/CAProviderSelect.tsx | 83 +++++++++++ ...oviderPicker.tsx => DNSProviderPicker.tsx} | 6 +- ...oviderSelect.tsx => DNSProviderSelect.tsx} | 6 +- ...erPicker.tsx => HostingProviderPicker.tsx} | 6 +- ...erSelect.tsx => HostingProviderSelect.tsx} | 6 +- .../workflow/node/ApplyNodeConfigForm.tsx | 135 ++++++++++++++++-- .../workflow/node/DeployNodeConfigForm.tsx | 19 +-- ...loyNodeConfigFormAliyunCASDeployConfig.tsx | 6 + ...ployNodeConfigFormBaotaPanelSiteConfig.tsx | 3 + ...eConfigFormTencentCloudSSLDeployConfig.tsx | 3 + ui/src/domain/provider.ts | 42 +++++- ui/src/domain/workflow.ts | 3 + ui/src/i18n/locales/en/nls.provider.json | 4 +- .../i18n/locales/en/nls.workflow.nodes.json | 6 + ui/src/i18n/locales/zh/nls.provider.json | 4 +- .../i18n/locales/zh/nls.workflow.nodes.json | 6 + 23 files changed, 496 insertions(+), 186 deletions(-) create mode 100644 ui/src/components/provider/CAProviderSelect.tsx rename ui/src/components/provider/{ApplyDNSProviderPicker.tsx => DNSProviderPicker.tsx} (91%) rename ui/src/components/provider/{ApplyDNSProviderSelect.tsx => DNSProviderSelect.tsx} (91%) rename ui/src/components/provider/{DeployProviderPicker.tsx => HostingProviderPicker.tsx} (94%) rename ui/src/components/provider/{DeployProviderSelect.tsx => HostingProviderSelect.tsx} (91%) diff --git a/internal/applicant/acme_ca.go b/internal/applicant/acme_ca.go index afd86bdf..074b2a18 100644 --- a/internal/applicant/acme_ca.go +++ b/internal/applicant/acme_ca.go @@ -19,16 +19,6 @@ var sslProviderUrls = map[string]string{ } type acmeSSLProviderConfig struct { - Config acmeSSLProviderConfigContent `json:"config"` - Provider string `json:"provider"` -} - -type acmeSSLProviderConfigContent struct { - ZeroSSL acmeSSLProviderEabConfig `json:"zerossl"` - GoogleTrustServices acmeSSLProviderEabConfig `json:"googletrustservices"` -} - -type acmeSSLProviderEabConfig struct { - EabHmacKey string `json:"eabHmacKey"` - EabKid string `json:"eabKid"` + Config map[domain.ApplyCAProviderType]map[string]any `json:"config"` + Provider string `json:"provider"` } diff --git a/internal/applicant/acme_user.go b/internal/applicant/acme_user.go index 107e417c..75afa708 100644 --- a/internal/applicant/acme_user.go +++ b/internal/applicant/acme_user.go @@ -14,6 +14,7 @@ import ( "github.com/usual2970/certimate/internal/domain" "github.com/usual2970/certimate/internal/pkg/utils/certutil" + "github.com/usual2970/certimate/internal/pkg/utils/maputil" "github.com/usual2970/certimate/internal/repository" ) @@ -76,16 +77,11 @@ func (u *acmeUser) getPrivateKeyPEM() string { return u.privkey } -type acmeAccountRepository interface { - GetByCAAndEmail(ca, email string) (*domain.AcmeAccount, error) - Save(ca, email, key string, resource *registration.Resource) error -} - var registerGroup singleflight.Group -func registerAcmeUserWithSingleFlight(client *lego.Client, sslProviderConfig *acmeSSLProviderConfig, user *acmeUser) (*registration.Resource, error) { - resp, err, _ := registerGroup.Do(fmt.Sprintf("register_acme_user_%s_%s", sslProviderConfig.Provider, user.GetEmail()), func() (interface{}, error) { - return registerAcmeUser(client, sslProviderConfig, user) +func registerAcmeUserWithSingleFlight(client *lego.Client, user *acmeUser, userRegisterOptions map[string]any) (*registration.Resource, error) { + resp, err, _ := registerGroup.Do(fmt.Sprintf("register_acme_user_%s_%s", user.CA, user.Email), func() (interface{}, error) { + return registerAcmeUser(client, user, userRegisterOptions) }) if err != nil { @@ -95,45 +91,62 @@ func registerAcmeUserWithSingleFlight(client *lego.Client, sslProviderConfig *ac return resp.(*registration.Resource), nil } -func registerAcmeUser(client *lego.Client, sslProviderConfig *acmeSSLProviderConfig, user *acmeUser) (*registration.Resource, error) { +func registerAcmeUser(client *lego.Client, user *acmeUser, userRegisterOptions map[string]any) (*registration.Resource, error) { var reg *registration.Resource var err error - switch sslProviderConfig.Provider { - case sslProviderZeroSSL: - reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{ - TermsOfServiceAgreed: true, - Kid: sslProviderConfig.Config.ZeroSSL.EabKid, - HmacEncoded: sslProviderConfig.Config.ZeroSSL.EabHmacKey, - }) - case sslProviderGoogleTrustServices: - reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{ - TermsOfServiceAgreed: true, - Kid: sslProviderConfig.Config.GoogleTrustServices.EabKid, - HmacEncoded: sslProviderConfig.Config.GoogleTrustServices.EabHmacKey, - }) + switch user.CA { case sslProviderLetsEncrypt, sslProviderLetsEncryptStaging: reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) + + case sslProviderGoogleTrustServices: + { + access := domain.AccessConfigForGoogleTrustServices{} + if err := maputil.Populate(userRegisterOptions, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{ + TermsOfServiceAgreed: true, + Kid: access.EabKid, + HmacEncoded: access.EabHmacKey, + }) + } + + case sslProviderZeroSSL: + { + access := domain.AccessConfigForZeroSSL{} + if err := maputil.Populate(userRegisterOptions, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{ + TermsOfServiceAgreed: true, + Kid: access.EabKid, + HmacEncoded: access.EabHmacKey, + }) + } + default: - err = fmt.Errorf("unsupported ssl provider: %s", sslProviderConfig.Provider) + err = fmt.Errorf("unsupported ca provider: %s", user.CA) } if err != nil { return nil, err } repo := repository.NewAcmeAccountRepository() - resp, err := repo.GetByCAAndEmail(sslProviderConfig.Provider, user.GetEmail()) + resp, err := repo.GetByCAAndEmail(user.CA, user.Email) if err == nil { user.privkey = resp.Key return resp.Resource, nil } if _, err := repo.Save(context.Background(), &domain.AcmeAccount{ - CA: sslProviderConfig.Provider, - Email: user.GetEmail(), + CA: user.CA, + Email: user.Email, Key: user.getPrivateKeyPEM(), Resource: reg, }); err != nil { - return nil, fmt.Errorf("failed to save registration: %w", err) + return nil, fmt.Errorf("failed to save acme account registration: %w", err) } return reg, nil diff --git a/internal/applicant/applicant.go b/internal/applicant/applicant.go index 8b452aab..1e996976 100644 --- a/internal/applicant/applicant.go +++ b/internal/applicant/applicant.go @@ -37,18 +37,21 @@ type Applicant interface { } type applicantOptions struct { - Domains []string - ContactEmail string - Provider domain.ApplyDNSProviderType - ProviderAccessConfig map[string]any - ProviderApplyConfig map[string]any - KeyAlgorithm string - Nameservers []string - DnsPropagationTimeout int32 - DnsTTL int32 - DisableFollowCNAME bool - ReplacedARIAcctId string - ReplacedARICertId string + Domains []string + ContactEmail string + Provider domain.ApplyDNSProviderType + ProviderAccessConfig map[string]any + ProviderExtendedConfig map[string]any + CAProvider domain.ApplyCAProviderType + CAProviderAccessConfig map[string]any + CAProviderExtendedConfig map[string]any + KeyAlgorithm string + Nameservers []string + DnsPropagationTimeout int32 + DnsTTL int32 + DisableFollowCNAME bool + ReplacedARIAcctId string + ReplacedARICertId string } func NewWithApplyNode(node *domain.WorkflowNode) (Applicant, error) { @@ -58,22 +61,55 @@ func NewWithApplyNode(node *domain.WorkflowNode) (Applicant, error) { nodeConfig := node.GetConfigForApply() options := &applicantOptions{ - Domains: sliceutil.Filter(strings.Split(nodeConfig.Domains, ";"), func(s string) bool { return s != "" }), - ContactEmail: nodeConfig.ContactEmail, - Provider: domain.ApplyDNSProviderType(nodeConfig.Provider), - ProviderApplyConfig: nodeConfig.ProviderConfig, - KeyAlgorithm: nodeConfig.KeyAlgorithm, - Nameservers: sliceutil.Filter(strings.Split(nodeConfig.Nameservers, ";"), func(s string) bool { return s != "" }), - DnsPropagationTimeout: nodeConfig.DnsPropagationTimeout, - DnsTTL: nodeConfig.DnsTTL, - DisableFollowCNAME: nodeConfig.DisableFollowCNAME, + Domains: sliceutil.Filter(strings.Split(nodeConfig.Domains, ";"), func(s string) bool { return s != "" }), + ContactEmail: nodeConfig.ContactEmail, + Provider: domain.ApplyDNSProviderType(nodeConfig.Provider), + ProviderAccessConfig: make(map[string]any), + ProviderExtendedConfig: nodeConfig.ProviderConfig, + CAProvider: domain.ApplyCAProviderType(nodeConfig.CAProvider), + CAProviderAccessConfig: make(map[string]any), + CAProviderExtendedConfig: nodeConfig.CAProviderConfig, + KeyAlgorithm: nodeConfig.KeyAlgorithm, + Nameservers: sliceutil.Filter(strings.Split(nodeConfig.Nameservers, ";"), func(s string) bool { return s != "" }), + DnsPropagationTimeout: nodeConfig.DnsPropagationTimeout, + DnsTTL: nodeConfig.DnsTTL, + DisableFollowCNAME: nodeConfig.DisableFollowCNAME, } accessRepo := repository.NewAccessRepository() - if access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId); err != nil { - return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err) - } else { - options.ProviderAccessConfig = access.Config + if nodeConfig.ProviderAccessId != "" { + if access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId); err != nil { + return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err) + } else { + options.ProviderAccessConfig = access.Config + } + } + if nodeConfig.CAProviderAccessId != "" { + if access, err := accessRepo.GetById(context.Background(), nodeConfig.CAProviderAccessId); err != nil { + return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.CAProviderAccessId, err) + } else { + options.CAProviderAccessConfig = access.Config + } + } + + settingsRepo := repository.NewSettingsRepository() + if string(options.CAProvider) == "" { + settings, _ := settingsRepo.GetByName(context.Background(), "sslProvider") + + sslProviderConfig := &acmeSSLProviderConfig{ + Config: make(map[domain.ApplyCAProviderType]map[string]any), + Provider: sslProviderDefault, + } + if settings != nil { + if err := json.Unmarshal([]byte(settings.Content), sslProviderConfig); err != nil { + return nil, err + } else if sslProviderConfig.Provider == "" { + sslProviderConfig.Provider = sslProviderDefault + } + } + + options.CAProvider = domain.ApplyCAProviderType(sslProviderConfig.Provider) + options.CAProviderAccessConfig = sslProviderConfig.Config[options.CAProvider] } certRepo := repository.NewCertificateRepository() @@ -106,24 +142,7 @@ func NewWithApplyNode(node *domain.WorkflowNode) (Applicant, error) { } func apply(challengeProvider challenge.Provider, options *applicantOptions) (*ApplyCertResult, error) { - settingsRepo := repository.NewSettingsRepository() - settings, _ := settingsRepo.GetByName(context.Background(), "sslProvider") - - sslProviderConfig := &acmeSSLProviderConfig{ - Config: acmeSSLProviderConfigContent{}, - Provider: sslProviderDefault, - } - if settings != nil { - if err := json.Unmarshal([]byte(settings.Content), sslProviderConfig); err != nil { - return nil, err - } - } - - if sslProviderConfig.Provider == "" { - sslProviderConfig.Provider = sslProviderDefault - } - - acmeUser, err := newAcmeUser(sslProviderConfig.Provider, options.ContactEmail) + user, err := newAcmeUser(string(options.CAProvider), options.ContactEmail) if err != nil { return nil, err } @@ -133,8 +152,8 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap os.Setenv("LEGO_DISABLE_CNAME_SUPPORT", strconv.FormatBool(options.DisableFollowCNAME)) // Create an ACME client config - config := lego.NewConfig(acmeUser) - config.CADirURL = sslProviderUrls[sslProviderConfig.Provider] + config := lego.NewConfig(user) + config.CADirURL = sslProviderUrls[user.CA] config.Certificate.KeyType = parseKeyAlgorithm(domain.CertificateKeyAlgorithmType(options.KeyAlgorithm)) // Create an ACME client @@ -152,12 +171,12 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap client.Challenge.SetDNS01Provider(challengeProvider, challengeOptions...) // New users need to register first - if !acmeUser.hasRegistration() { - reg, err := registerAcmeUserWithSingleFlight(client, sslProviderConfig, acmeUser) + if !user.hasRegistration() { + reg, err := registerAcmeUserWithSingleFlight(client, user, options.CAProviderAccessConfig) if err != nil { return nil, fmt.Errorf("failed to register: %w", err) } - acmeUser.Registration = reg + user.Registration = reg } // Obtain a certificate @@ -165,7 +184,7 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap Domains: options.Domains, Bundle: true, } - if options.ReplacedARICertId != "" && options.ReplacedARIAcctId != acmeUser.Registration.URI { + if options.ReplacedARICertId != "" && options.ReplacedARIAcctId != user.Registration.URI { certRequest.ReplacesCertID = options.ReplacedARICertId } certResource, err := client.Certificate.Obtain(certRequest) @@ -177,7 +196,7 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap CertificateFullChain: strings.TrimSpace(string(certResource.Certificate)), IssuerCertificate: strings.TrimSpace(string(certResource.IssuerCertificate)), PrivateKey: strings.TrimSpace(string(certResource.PrivateKey)), - ACMEAccountUrl: acmeUser.Registration.URI, + ACMEAccountUrl: user.Registration.URI, ACMECertUrl: certResource.CertURL, ACMECertStableUrl: certResource.CertStableURL, CSR: strings.TrimSpace(string(certResource.CSR)), diff --git a/internal/applicant/providers.go b/internal/applicant/providers.go index 90e8a972..55164eeb 100644 --- a/internal/applicant/providers.go +++ b/internal/applicant/providers.go @@ -86,8 +86,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { applicant, err := pAWSRoute53.NewChallengeProvider(&pAWSRoute53.ChallengeProviderConfig{ AccessKeyId: access.AccessKeyId, SecretAccessKey: access.SecretAccessKey, - Region: maputil.GetString(options.ProviderApplyConfig, "region"), - HostedZoneId: maputil.GetString(options.ProviderApplyConfig, "hostedZoneId"), + Region: maputil.GetString(options.ProviderExtendedConfig, "region"), + HostedZoneId: maputil.GetString(options.ProviderExtendedConfig, "hostedZoneId"), DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) @@ -278,7 +278,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { applicant, err := pHuaweiCloud.NewChallengeProvider(&pHuaweiCloud.ChallengeProviderConfig{ AccessKeyId: access.AccessKeyId, SecretAccessKey: access.SecretAccessKey, - Region: maputil.GetString(options.ProviderApplyConfig, "region"), + Region: maputil.GetString(options.ProviderExtendedConfig, "region"), DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) @@ -295,7 +295,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { applicant, err := pJDCloud.NewChallengeProvider(&pJDCloud.ChallengeProviderConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, - RegionId: maputil.GetString(options.ProviderApplyConfig, "regionId"), + RegionId: maputil.GetString(options.ProviderExtendedConfig, "regionId"), DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) @@ -432,7 +432,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { applicant, err := pTencentCloudEO.NewChallengeProvider(&pTencentCloudEO.ChallengeProviderConfig{ SecretId: access.SecretId, SecretKey: access.SecretKey, - ZoneId: maputil.GetString(options.ProviderApplyConfig, "zoneId"), + ZoneId: maputil.GetString(options.ProviderExtendedConfig, "zoneId"), DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) diff --git a/internal/domain/workflow.go b/internal/domain/workflow.go index 50069865..068920ae 100644 --- a/internal/domain/workflow.go +++ b/internal/domain/workflow.go @@ -62,19 +62,22 @@ type WorkflowNode struct { } type WorkflowNodeConfigForApply struct { - Domains string `json:"domains"` // 域名列表,以半角分号分隔 - ContactEmail string `json:"contactEmail"` // 联系邮箱 - ChallengeType string `json:"challengeType"` // TODO: 验证方式。目前仅支持 dns-01 - Provider string `json:"provider"` // DNS 提供商 - ProviderAccessId string `json:"providerAccessId"` // DNS 提供商授权记录 ID - ProviderConfig map[string]any `json:"providerConfig"` // DNS 提供商额外配置 - KeyAlgorithm string `json:"keyAlgorithm"` // 密钥算法 - Nameservers string `json:"nameservers"` // DNS 服务器列表,以半角分号分隔 - DnsPropagationTimeout int32 `json:"dnsPropagationTimeout"` // DNS 传播超时时间(零值取决于提供商的默认值) - DnsTTL int32 `json:"dnsTTL"` // DNS TTL(零值取决于提供商的默认值) - DisableFollowCNAME bool `json:"disableFollowCNAME"` // 是否关闭 CNAME 跟随 - DisableARI bool `json:"disableARI"` // 是否关闭 ARI - SkipBeforeExpiryDays int32 `json:"skipBeforeExpiryDays"` // 证书到期前多少天前跳过续期(零值将使用默认值 30) + Domains string `json:"domains"` // 域名列表,以半角分号分隔 + ContactEmail string `json:"contactEmail"` // 联系邮箱 + ChallengeType string `json:"challengeType"` // TODO: 验证方式。目前仅支持 dns-01 + Provider string `json:"provider"` // DNS 提供商 + ProviderAccessId string `json:"providerAccessId"` // DNS 提供商授权记录 ID + ProviderConfig map[string]any `json:"providerConfig"` // DNS 提供商额外配置 + CAProvider string `json:"caProvider,omitempty"` // CA 提供商(零值将使用全局配置) + CAProviderAccessId string `json:"caProviderAccessId,omitempty"` // CA 提供商授权记录 ID + CAProviderConfig map[string]any `json:"caProviderConfig,omitempty"` // CA 提供商额外配置 + KeyAlgorithm string `json:"keyAlgorithm"` // 密钥算法 + Nameservers string `json:"nameservers,omitempty"` // DNS 服务器列表,以半角分号分隔 + DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"` // DNS 传播超时时间(零值取决于提供商的默认值) + DnsTTL int32 `json:"dnsTTL,omitempty"` // DNS TTL(零值取决于提供商的默认值) + DisableFollowCNAME bool `json:"disableFollowCNAME,omitempty"` // 是否关闭 CNAME 跟随 + DisableARI bool `json:"disableARI,omitempty"` // 是否关闭 ARI + SkipBeforeExpiryDays int32 `json:"skipBeforeExpiryDays,omitempty"` // 证书到期前多少天前跳过续期(零值将使用默认值 30) } type WorkflowNodeConfigForUpload struct { @@ -97,73 +100,54 @@ type WorkflowNodeConfigForNotify struct { Message string `json:"message"` // 通知内容 } -func (n *WorkflowNode) getConfigString(key string) string { - return maputil.GetString(n.Config, key) -} - -func (n *WorkflowNode) getConfigBool(key string) bool { - return maputil.GetBool(n.Config, key) -} - -func (n *WorkflowNode) getConfigInt32(key string) int32 { - return maputil.GetInt32(n.Config, key) -} - -func (n *WorkflowNode) getConfigMap(key string) map[string]any { - if val, ok := n.Config[key]; ok { - if result, ok := val.(map[string]any); ok { - return result - } - } - - return make(map[string]any) -} - func (n *WorkflowNode) GetConfigForApply() WorkflowNodeConfigForApply { - skipBeforeExpiryDays := n.getConfigInt32("skipBeforeExpiryDays") + skipBeforeExpiryDays := maputil.GetInt32(n.Config, "skipBeforeExpiryDays") if skipBeforeExpiryDays == 0 { skipBeforeExpiryDays = 30 } return WorkflowNodeConfigForApply{ - Domains: n.getConfigString("domains"), - ContactEmail: n.getConfigString("contactEmail"), - Provider: n.getConfigString("provider"), - ProviderAccessId: n.getConfigString("providerAccessId"), - ProviderConfig: n.getConfigMap("providerConfig"), - KeyAlgorithm: n.getConfigString("keyAlgorithm"), - Nameservers: n.getConfigString("nameservers"), - DnsPropagationTimeout: n.getConfigInt32("dnsPropagationTimeout"), - DnsTTL: n.getConfigInt32("dnsTTL"), - DisableFollowCNAME: n.getConfigBool("disableFollowCNAME"), - DisableARI: n.getConfigBool("disableARI"), + Domains: maputil.GetString(n.Config, "domains"), + ContactEmail: maputil.GetString(n.Config, "contactEmail"), + Provider: maputil.GetString(n.Config, "provider"), + ProviderAccessId: maputil.GetString(n.Config, "providerAccessId"), + ProviderConfig: maputil.GetAnyMap(n.Config, "providerConfig"), + CAProvider: maputil.GetString(n.Config, "caProvider"), + CAProviderAccessId: maputil.GetString(n.Config, "caProviderAccessId"), + CAProviderConfig: maputil.GetAnyMap(n.Config, "caProviderConfig"), + KeyAlgorithm: maputil.GetString(n.Config, "keyAlgorithm"), + Nameservers: maputil.GetString(n.Config, "nameservers"), + DnsPropagationTimeout: maputil.GetInt32(n.Config, "dnsPropagationTimeout"), + DnsTTL: maputil.GetInt32(n.Config, "dnsTTL"), + DisableFollowCNAME: maputil.GetBool(n.Config, "disableFollowCNAME"), + DisableARI: maputil.GetBool(n.Config, "disableARI"), SkipBeforeExpiryDays: skipBeforeExpiryDays, } } func (n *WorkflowNode) GetConfigForUpload() WorkflowNodeConfigForUpload { return WorkflowNodeConfigForUpload{ - Certificate: n.getConfigString("certificate"), - PrivateKey: n.getConfigString("privateKey"), - Domains: n.getConfigString("domains"), + Certificate: maputil.GetString(n.Config, "certificate"), + PrivateKey: maputil.GetString(n.Config, "privateKey"), + Domains: maputil.GetString(n.Config, "domains"), } } func (n *WorkflowNode) GetConfigForDeploy() WorkflowNodeConfigForDeploy { return WorkflowNodeConfigForDeploy{ - Certificate: n.getConfigString("certificate"), - Provider: n.getConfigString("provider"), - ProviderAccessId: n.getConfigString("providerAccessId"), - ProviderConfig: n.getConfigMap("providerConfig"), - SkipOnLastSucceeded: n.getConfigBool("skipOnLastSucceeded"), + Certificate: maputil.GetString(n.Config, "certificate"), + Provider: maputil.GetString(n.Config, "provider"), + ProviderAccessId: maputil.GetString(n.Config, "providerAccessId"), + ProviderConfig: maputil.GetAnyMap(n.Config, "providerConfig"), + SkipOnLastSucceeded: maputil.GetBool(n.Config, "skipOnLastSucceeded"), } } func (n *WorkflowNode) GetConfigForNotify() WorkflowNodeConfigForNotify { return WorkflowNodeConfigForNotify{ - Channel: n.getConfigString("channel"), - Subject: n.getConfigString("subject"), - Message: n.getConfigString("message"), + Channel: maputil.GetString(n.Config, "channel"), + Subject: maputil.GetString(n.Config, "subject"), + Message: maputil.GetString(n.Config, "message"), } } diff --git a/internal/pkg/utils/maputil/getter.go b/internal/pkg/utils/maputil/getter.go index 36561381..9ba22875 100644 --- a/internal/pkg/utils/maputil/getter.go +++ b/internal/pkg/utils/maputil/getter.go @@ -180,3 +180,25 @@ func GetOrDefaultBool(dict map[string]any, key string, defaultValue bool) bool { return defaultValue } + +// 以 `map[string]any` 形式从字典中获取指定键的值。 +// +// 入参: +// - dict: 字典。 +// - key: 键。 +// +// 出参: +// - 字典中键对应的 `map[string]any` 对象。 +func GetAnyMap(dict map[string]any, key string) map[string]any { + if dict == nil { + return make(map[string]any) + } + + if val, ok := dict[key]; ok { + if result, ok := val.(map[string]any); ok { + return result + } + } + + return make(map[string]any) +} diff --git a/internal/workflow/node-processor/apply_node.go b/internal/workflow/node-processor/apply_node.go index a1ca977d..b9769f3d 100644 --- a/internal/workflow/node-processor/apply_node.go +++ b/internal/workflow/node-processor/apply_node.go @@ -109,12 +109,24 @@ func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workflo if currentNodeConfig.ContactEmail != lastNodeConfig.ContactEmail { return false, "the configuration item 'ContactEmail' changed" } + if currentNodeConfig.Provider != lastNodeConfig.Provider { + return false, "the configuration item 'Provider' changed" + } if currentNodeConfig.ProviderAccessId != lastNodeConfig.ProviderAccessId { return false, "the configuration item 'ProviderAccessId' changed" } if !maps.Equal(currentNodeConfig.ProviderConfig, lastNodeConfig.ProviderConfig) { return false, "the configuration item 'ProviderConfig' changed" } + if currentNodeConfig.CAProvider != lastNodeConfig.CAProvider { + return false, "the configuration item 'CAProvider' changed" + } + if currentNodeConfig.CAProviderAccessId != lastNodeConfig.CAProviderAccessId { + return false, "the configuration item 'CAProviderAccessId' changed" + } + if !maps.Equal(currentNodeConfig.CAProviderConfig, lastNodeConfig.CAProviderConfig) { + return false, "the configuration item 'CAProviderConfig' changed" + } if currentNodeConfig.KeyAlgorithm != lastNodeConfig.KeyAlgorithm { return false, "the configuration item 'KeyAlgorithm' changed" } diff --git a/ui/src/components/provider/CAProviderSelect.tsx b/ui/src/components/provider/CAProviderSelect.tsx new file mode 100644 index 00000000..5d9637ac --- /dev/null +++ b/ui/src/components/provider/CAProviderSelect.tsx @@ -0,0 +1,83 @@ +import { memo, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Avatar, Select, type SelectProps, Space, Typography } from "antd"; + +import { type ApplyCAProvider, applyCAProvidersMap } from "@/domain/provider"; + +export type CAProviderSelectProps = Omit< + SelectProps, + "filterOption" | "filterSort" | "labelRender" | "options" | "optionFilterProp" | "optionLabelProp" | "optionRender" +> & { + filter?: (record: ApplyCAProvider) => boolean; +}; + +const CAProviderSelect = ({ filter, ...props }: CAProviderSelectProps) => { + const { t } = useTranslation(); + + const [options, setOptions] = useState>([]); + useEffect(() => { + const allItems = Array.from(applyCAProvidersMap.values()); + const filteredItems = filter != null ? allItems.filter(filter) : allItems; + setOptions([ + { + key: "", + value: "", + label: "provider.default_ca_provider.label", + data: {} as ApplyCAProvider, + }, + ...filteredItems.map((item) => ({ + key: item.type, + value: item.type, + label: t(item.name), + data: item, + })), + ]); + }, [filter]); + + const renderOption = (key: string) => { + if (key === "") { + return ( + + + {t("provider.default_ca_provider.label")} + + + ); + } + + const provider = applyCAProvidersMap.get(key); + return ( + + + + {t(provider?.name ?? "")} + + + ); + }; + + return ( + ({ @@ -364,6 +476,9 @@ const ApplyNodeConfigForm = forwardRef { formInst.setFieldValue("nameservers", e.target.value); }} + onClear={() => { + formInst.setFieldValue("nameservers", undefined); + }} /> { + const handleProviderSelect = (value?: string | undefined) => { if (fieldProvider === value) return; // 切换部署目标时重置表单,避免其他部署目标的配置字段影响当前部署目标 @@ -310,7 +310,7 @@ const DeployNodeConfigForm = forwardRef } + fallback={} > - @@ -384,13 +385,13 @@ const DeployNodeConfigForm = forwardRef - {t("workflow_node.deploy.form.provider_access.button")} + } afterSubmit={(record) => { const provider = accessProvidersMap.get(record.provider); - if (provider?.usages?.includes(ACCESS_USAGES.DEPLOY)) { + if (provider?.usages?.includes(ACCESS_USAGES.HOSTING)) { formInst.setFieldValue("providerAccessId", record.id); } }} @@ -406,7 +407,7 @@ const DeployNodeConfigForm = forwardRef diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunCASDeployConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunCASDeployConfig.tsx index 065f752c..f6182f28 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunCASDeployConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunCASDeployConfig.tsx @@ -100,6 +100,9 @@ const DeployNodeConfigFormAliyunCASDeployConfig = ({ onChange={(e) => { formInst.setFieldValue("resourceIds", e.target.value); }} + onClear={() => { + formInst.setFieldValue("resourceIds", ""); + }} /> { formInst.setFieldValue("contactIds", e.target.value); }} + onClear={() => { + formInst.setFieldValue("contactIds", ""); + }} /> { formInst.setFieldValue("siteNames", e.target.value); }} + onClear={() => { + formInst.setFieldValue("siteNames", ""); + }} /> { formInst.setFieldValue("resourceIds", e.target.value); }} + onClear={() => { + formInst.setFieldValue("resourceIds", ""); + }} /> = new Map( + /* + 注意:此处的顺序决定显示在前端的顺序。 + NOTICE: The following order determines the order displayed at the frontend. + */ + [[APPLY_CA_PROVIDERS.LETSENCRYPT], [APPLY_CA_PROVIDERS.LETSENCRYPTSTAGING], [APPLY_CA_PROVIDERS.ZEROSSL], [APPLY_CA_PROVIDERS.GOOGLETRUSTSERVICES]].map( + ([type]) => [ + type, + { + type: type as ApplyCAProviderType, + name: accessProvidersMap.get(type.split("-")[0])!.name, + icon: accessProvidersMap.get(type.split("-")[0])!.icon, + provider: type.split("-")[0] as AccessProviderType, + }, + ] + ) +); +// #endregion + +// #region ApplyDNSProvider /* 注意:如果追加新的常量值,请保持以 ASCII 排序。 NOTICE: If you add new constant, please keep ASCII order. diff --git a/ui/src/domain/workflow.ts b/ui/src/domain/workflow.ts index 07841902..77f4ba12 100644 --- a/ui/src/domain/workflow.ts +++ b/ui/src/domain/workflow.ts @@ -126,6 +126,9 @@ export type WorkflowNodeConfigForApply = { provider: string; providerAccessId: string; providerConfig?: Record; + caProvider?: string; + caProviderAccessId?: string; + caProviderConfig?: Record; keyAlgorithm: string; nameservers?: string; dnsPropagationTimeout?: number; diff --git a/ui/src/i18n/locales/en/nls.provider.json b/ui/src/i18n/locales/en/nls.provider.json index f2e286a9..89bf68e6 100644 --- a/ui/src/i18n/locales/en/nls.provider.json +++ b/ui/src/i18n/locales/en/nls.provider.json @@ -134,5 +134,7 @@ "provider.category.av": "Audio/Video", "provider.category.serverless": "Serverless", "provider.category.website": "Website", - "provider.category.other": "Other" + "provider.category.other": "Other", + + "provider.default_ca_provider.label": "Follow global settings" } diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index 39c0361c..594575c8 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -55,6 +55,12 @@ "workflow_node.apply.form.tencentcloud_eo_zone_id.placeholder": "Please enter Tencent Cloud EdgeOne zone ID", "workflow_node.apply.form.tencentcloud_eo_zone_id.tooltip": "For more information, see https://console.tencentcloud.com/edgeone", "workflow_node.apply.form.advanced_config.label": "Advanced settings", + "workflow_node.apply.form.ca_provider.label": "Certificate authority", + "workflow_node.apply.form.ca_provider.placeholder": "Follow global settings", + "workflow_node.apply.form.ca_provider.button": "Configure", + "workflow_node.apply.form.ca_provider_access.label": "Certificate authority authorization", + "workflow_node.apply.form.ca_provider_access.placeholder": "Please select an authorization of the certificate authority", + "workflow_node.apply.form.ca_provider_access.button": "Create", "workflow_node.apply.form.key_algorithm.label": "Certificate key algorithm", "workflow_node.apply.form.key_algorithm.placeholder": "Please select certificate key algorithm", "workflow_node.apply.form.nameservers.label": "DNS recursive nameservers (Optional)", diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json index 341ceb1b..a164e360 100644 --- a/ui/src/i18n/locales/zh/nls.provider.json +++ b/ui/src/i18n/locales/zh/nls.provider.json @@ -134,5 +134,7 @@ "provider.category.av": "音视频", "provider.category.serverless": "Serverless", "provider.category.website": "网站托管", - "provider.category.other": "其他" + "provider.category.other": "其他", + + "provider.default_ca_provider.label": "跟随全局设置" } diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 99382f80..4e7450e0 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -55,6 +55,12 @@ "workflow_node.apply.form.tencentcloud_eo_zone_id.placeholder": "请输入腾讯云 EdgeOne 站点 ID", "workflow_node.apply.form.tencentcloud_eo_zone_id.tooltip": "这是什么?请参阅 https://console.cloud.tencent.com/edgeone", "workflow_node.apply.form.advanced_config.label": "高级设置", + "workflow_node.apply.form.ca_provider.label": "证书颁发机构", + "workflow_node.apply.form.ca_provider.placeholder": "跟随全局设置", + "workflow_node.apply.form.ca_provider.button": "去配置", + "workflow_node.apply.form.ca_provider_access.label": "证书颁发机构授权", + "workflow_node.apply.form.ca_provider_access.placeholder": "请选择证书颁发机构授权", + "workflow_node.apply.form.ca_provider_access.button": "新建", "workflow_node.apply.form.key_algorithm.label": "数字证书算法", "workflow_node.apply.form.key_algorithm.placeholder": "请选择数字证书算法", "workflow_node.apply.form.nameservers.label": "DNS 递归服务器(可选)", From 09b5a21af1026473e5bd2718ae9704feb1b9b629 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Sun, 30 Mar 2025 13:53:32 +0800 Subject: [PATCH 03/20] feat: make the builtin providers access field non mandatory --- internal/deployer/deployer.go | 25 +++++++------ internal/domain/access.go | 2 -- internal/domain/workflow.go | 10 +++--- ui/src/components/access/AccessForm.tsx | 3 -- .../access/AccessFormLocalConfig.tsx | 36 ------------------- .../provider/AccessProviderSelect.tsx | 3 +- .../workflow/node/ApplyNodeConfigForm.tsx | 8 +++-- .../workflow/node/DeployNodeConfigForm.tsx | 23 ++++++++++-- ui/src/domain/access.ts | 3 -- ui/src/domain/provider.ts | 35 +++++++++++------- ui/src/domain/workflow.ts | 4 +-- .../i18n/locales/en/nls.workflow.nodes.json | 2 +- .../i18n/locales/zh/nls.workflow.nodes.json | 2 +- 13 files changed, 74 insertions(+), 82 deletions(-) delete mode 100644 ui/src/components/access/AccessFormLocalConfig.tsx diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index 6efc001c..c023a236 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -32,18 +32,23 @@ func NewWithDeployNode(node *domain.WorkflowNode, certdata struct { } nodeConfig := node.GetConfigForDeploy() - - accessRepo := repository.NewAccessRepository() - access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId) - if err != nil { - return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err) + options := &deployerOptions{ + Provider: domain.DeployProviderType(nodeConfig.Provider), + ProviderAccessConfig: make(map[string]any), + ProviderDeployConfig: nodeConfig.ProviderConfig, } - deployer, err := createDeployer(&deployerOptions{ - Provider: domain.DeployProviderType(nodeConfig.Provider), - ProviderAccessConfig: access.Config, - ProviderDeployConfig: nodeConfig.ProviderConfig, - }) + accessRepo := repository.NewAccessRepository() + if nodeConfig.ProviderAccessId != "" { + access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId) + if err != nil { + return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err) + } else { + options.ProviderAccessConfig = access.Config + } + } + + deployer, err := createDeployer(options) if err != nil { return nil, err } diff --git a/internal/domain/access.go b/internal/domain/access.go index e88a3906..ccc33592 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -144,8 +144,6 @@ type AccessConfigForKubernetes struct { KubeConfig string `json:"kubeConfig,omitempty"` } -type AccessConfigForLocal struct{} - type AccessConfigForNamecheap struct { Username string `json:"username"` ApiKey string `json:"apiKey"` diff --git a/internal/domain/workflow.go b/internal/domain/workflow.go index 068920ae..841a041d 100644 --- a/internal/domain/workflow.go +++ b/internal/domain/workflow.go @@ -87,11 +87,11 @@ type WorkflowNodeConfigForUpload struct { } type WorkflowNodeConfigForDeploy struct { - Certificate string `json:"certificate"` // 前序节点输出的证书,形如“${NodeId}#certificate” - Provider string `json:"provider"` // 主机提供商 - ProviderAccessId string `json:"providerAccessId"` // 主机提供商授权记录 ID - ProviderConfig map[string]any `json:"providerConfig"` // 主机提供商额外配置 - SkipOnLastSucceeded bool `json:"skipOnLastSucceeded"` // 上次部署成功时是否跳过 + Certificate string `json:"certificate"` // 前序节点输出的证书,形如“${NodeId}#certificate” + Provider string `json:"provider"` // 主机提供商 + ProviderAccessId string `json:"providerAccessId,omitempty"` // 主机提供商授权记录 ID + ProviderConfig map[string]any `json:"providerConfig,omitempty"` // 主机提供商额外配置 + SkipOnLastSucceeded bool `json:"skipOnLastSucceeded"` // 上次部署成功时是否跳过 } type WorkflowNodeConfigForNotify struct { diff --git a/ui/src/components/access/AccessForm.tsx b/ui/src/components/access/AccessForm.tsx index 75660cb2..7b1a8101 100644 --- a/ui/src/components/access/AccessForm.tsx +++ b/ui/src/components/access/AccessForm.tsx @@ -35,7 +35,6 @@ import AccessFormGoogleTrustServicesConfig from "./AccessFormGoogleTrustServices import AccessFormHuaweiCloudConfig from "./AccessFormHuaweiCloudConfig"; import AccessFormJDCloudConfig from "./AccessFormJDCloudConfig"; import AccessFormKubernetesConfig from "./AccessFormKubernetesConfig"; -import AccessFormLocalConfig from "./AccessFormLocalConfig"; import AccessFormNamecheapConfig from "./AccessFormNamecheapConfig"; import AccessFormNameDotComConfig from "./AccessFormNameDotComConfig"; import AccessFormNameSiloConfig from "./AccessFormNameSiloConfig"; @@ -159,8 +158,6 @@ const AccessForm = forwardRef(({ className, return ; case ACCESS_PROVIDERS.KUBERNETES: return ; - case ACCESS_PROVIDERS.LOCAL: - return ; case ACCESS_PROVIDERS.NAMECHEAP: return ; case ACCESS_PROVIDERS.NAMEDOTCOM: diff --git a/ui/src/components/access/AccessFormLocalConfig.tsx b/ui/src/components/access/AccessFormLocalConfig.tsx deleted file mode 100644 index cde72374..00000000 --- a/ui/src/components/access/AccessFormLocalConfig.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Form, type FormInstance } from "antd"; - -import { type AccessConfigForLocal } from "@/domain/access"; - -type AccessFormLocalConfigFieldValues = Nullish; - -export type AccessFormLocalConfigProps = { - form: FormInstance; - formName: string; - disabled?: boolean; - initialValues?: AccessFormLocalConfigFieldValues; - onValuesChange?: (values: AccessFormLocalConfigFieldValues) => void; -}; - -const initFormModel = (): AccessFormLocalConfigFieldValues => { - return {}; -}; - -const AccessFormLocalConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormLocalConfigProps) => { - const handleFormChange = (_: unknown, values: any) => { - onValuesChange?.(values); - }; - - return ( -
- ); -}; - -export default AccessFormLocalConfig; diff --git a/ui/src/components/provider/AccessProviderSelect.tsx b/ui/src/components/provider/AccessProviderSelect.tsx index fb38419e..bd68227c 100644 --- a/ui/src/components/provider/AccessProviderSelect.tsx +++ b/ui/src/components/provider/AccessProviderSelect.tsx @@ -24,6 +24,7 @@ const AccessProviderSelect = ({ filter, showOptionTags, ...props }: AccessProvid key: item.type, value: item.type, label: t(item.name), + disabled: item.builtin, data: item, })) ); @@ -35,7 +36,7 @@ const AccessProviderSelect = ({ filter, showOptionTags, ...props }: AccessProvid
- + {t(provider?.name ?? "")} diff --git a/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx b/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx index 9b88a65c..5bb4fb0c 100644 --- a/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx @@ -97,7 +97,9 @@ const ApplyNodeConfigForm = forwardRef { if (!fieldCAProvider) return true; - return !!v; + + const provider = applyCAProvidersMap.get(fieldCAProvider); + return !!provider?.builtin || !!v; }, t("workflow_node.apply.form.ca_provider_access.placeholder")), caProviderConfig: z.any().nullish(), keyAlgorithm: z @@ -158,8 +160,10 @@ const ApplyNodeConfigForm = forwardRef { + // 内置的 CA 提供商(如 Let's Encrypt)无需显示授权信息字段 if (fieldCAProvider) { - setShowCAProviderAccess(true); + const provider = applyCAProvidersMap.get(fieldCAProvider); + setShowCAProviderAccess(!provider?.builtin); } else { setShowCAProviderAccess(false); } diff --git a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx index c0ced71b..9b905f87 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx @@ -125,8 +125,14 @@ const DeployNodeConfigForm = forwardRef { + if (!fieldProvider) return true; + + const provider = deployProvidersMap.get(fieldProvider); + return !!provider?.builtin || !!v; + }, t("workflow_node.deploy.form.provider_access.placeholder")), + providerConfig: z.any().nullish(), skipOnLastSucceeded: z.boolean().nullish(), }); const formRule = createSchemaFieldRule(formSchema); @@ -137,6 +143,17 @@ const DeployNodeConfigForm = forwardRef { + // 内置的部署提供商(如本地部署)无需显示授权信息字段 + if (fieldProvider) { + const provider = deployProvidersMap.get(fieldProvider); + setShowProviderAccess(!provider?.builtin); + } else { + setShowProviderAccess(false); + } + }, [fieldProvider]); + const [nestedFormInst] = Form.useForm(); const nestedFormName = useAntdFormName({ form: nestedFormInst, name: "workflowNodeDeployConfigFormProviderConfigForm" }); const nestedFormEl = useMemo(() => { @@ -368,7 +385,7 @@ const DeployNodeConfigForm = forwardRef - +