From 019e697f6b60148366e7a3c93b1ae74ea2667b4e Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 23 Oct 2025 12:04:24 +0800 Subject: [PATCH] feat(provider): new acme dns-01 provider: gandinet --- internal/certapply/applicators/sp_gandinet.go | 29 +++++++++++ internal/domain/access.go | 4 ++ internal/domain/provider.go | 6 ++- .../acme-dns01/providers/gandinet/gandinet.go | 38 ++++++++++++++ .../providers/gcore-cdn/gcore_cdn.go | 2 +- .../providers/gcore-cdn/gcore_cdn.go | 2 +- ui/public/imgs/providers/gandinet.svg | 1 + ui/src/components/access/AccessForm.tsx | 4 ++ .../AccessConfigFieldsProviderGandinet.tsx | 52 +++++++++++++++++++ .../forms/AccessConfigFieldsProviderGcore.tsx | 5 +- ui/src/domain/provider.ts | 4 ++ ui/src/i18n/locales/en/nls.access.json | 7 ++- ui/src/i18n/locales/en/nls.provider.json | 5 +- .../i18n/locales/en/nls.workflow.nodes.json | 8 +-- ui/src/i18n/locales/zh/nls.access.json | 7 ++- ui/src/i18n/locales/zh/nls.provider.json | 5 +- .../i18n/locales/zh/nls.workflow.nodes.json | 8 +-- 17 files changed, 163 insertions(+), 24 deletions(-) create mode 100644 internal/certapply/applicators/sp_gandinet.go create mode 100644 pkg/core/ssl-applicator/acme-dns01/providers/gandinet/gandinet.go create mode 100644 ui/public/imgs/providers/gandinet.svg create mode 100644 ui/src/components/access/forms/AccessConfigFieldsProviderGandinet.tsx diff --git a/internal/certapply/applicators/sp_gandinet.go b/internal/certapply/applicators/sp_gandinet.go new file mode 100644 index 00000000..9ba5a1a1 --- /dev/null +++ b/internal/certapply/applicators/sp_gandinet.go @@ -0,0 +1,29 @@ +package applicators + +import ( + "fmt" + + "github.com/go-acme/lego/v4/challenge" + + "github.com/certimate-go/certimate/internal/domain" + "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/gandinet" + xmaps "github.com/certimate-go/certimate/pkg/utils/maps" +) + +func init() { + if err := ACMEDns01Registries.Register(domain.ACMEDns01ProviderTypeGandinet, func(options *ProviderFactoryOptions) (challenge.Provider, error) { + credentials := domain.AccessConfigForGandinet{} + if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + provider, err := gandinet.NewChallengeProvider(&gandinet.ChallengeProviderConfig{ + PersonalAccessToken: credentials.PersonalAccessToken, + DnsPropagationTimeout: options.DnsPropagationTimeout, + DnsTTL: options.DnsTTL, + }) + return provider, err + }); err != nil { + panic(err) + } +} diff --git a/internal/domain/access.go b/internal/domain/access.go index 3dd14b1d..422ba921 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -204,6 +204,10 @@ type AccessConfigForFlexCDN struct { AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } +type AccessConfigForGandinet struct { + PersonalAccessToken string `json:"personalAccessToken"` +} + type AccessConfigForGcore struct { ApiToken string `json:"apiToken"` } diff --git a/internal/domain/provider.go b/internal/domain/provider.go index 53e2f44e..d2f2c5f1 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -46,11 +46,12 @@ const ( AccessProviderTypeEmail = AccessProviderType("email") AccessProviderTypeFastly = AccessProviderType("fastly") // Fastly(预留) AccessProviderTypeFlexCDN = AccessProviderType("flexcdn") - AccessProviderTypeGname = AccessProviderType("gname") + AccessProviderTypeGandinet = AccessProviderType("gandinet") AccessProviderTypeGcore = AccessProviderType("gcore") + AccessProviderTypeGlobalSignAtlas = AccessProviderType("globalsignatlas") + AccessProviderTypeGname = AccessProviderType("gname") AccessProviderTypeGoDaddy = AccessProviderType("godaddy") AccessProviderTypeGoEdge = AccessProviderType("goedge") - AccessProviderTypeGlobalSignAtlas = AccessProviderType("globalsignatlas") AccessProviderTypeGoogleTrustServices = AccessProviderType("googletrustservices") AccessProviderTypeHetzner = AccessProviderType("hetzner") AccessProviderTypeHostinger = AccessProviderType("hostinger") @@ -159,6 +160,7 @@ const ( ACMEDns01ProviderTypeDNSLA = ACMEDns01ProviderType(AccessProviderTypeDNSLA) ACMEDns01ProviderTypeDuckDNS = ACMEDns01ProviderType(AccessProviderTypeDuckDNS) ACMEDns01ProviderTypeDynv6 = ACMEDns01ProviderType(AccessProviderTypeDynv6) + ACMEDns01ProviderTypeGandinet = ACMEDns01ProviderType(AccessProviderTypeGandinet) ACMEDns01ProviderTypeGcore = ACMEDns01ProviderType(AccessProviderTypeGcore) ACMEDns01ProviderTypeGname = ACMEDns01ProviderType(AccessProviderTypeGname) ACMEDns01ProviderTypeGoDaddy = ACMEDns01ProviderType(AccessProviderTypeGoDaddy) diff --git a/pkg/core/ssl-applicator/acme-dns01/providers/gandinet/gandinet.go b/pkg/core/ssl-applicator/acme-dns01/providers/gandinet/gandinet.go new file mode 100644 index 00000000..d528c56b --- /dev/null +++ b/pkg/core/ssl-applicator/acme-dns01/providers/gandinet/gandinet.go @@ -0,0 +1,38 @@ +package gandinet + +import ( + "errors" + "time" + + "github.com/go-acme/lego/v4/providers/dns/gandiv5" + + "github.com/certimate-go/certimate/pkg/core" +) + +type ChallengeProviderConfig struct { + PersonalAccessToken string `json:"personalAccessToken"` + DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"` + DnsTTL int32 `json:"dnsTTL,omitempty"` +} + +func NewChallengeProvider(config *ChallengeProviderConfig) (core.ACMEChallenger, error) { + if config == nil { + return nil, errors.New("the configuration of the acme challenge provider is nil") + } + + providerConfig := gandiv5.NewDefaultConfig() + providerConfig.PersonalAccessToken = config.PersonalAccessToken + if config.DnsPropagationTimeout != 0 { + providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second + } + if config.DnsTTL != 0 { + providerConfig.TTL = int(config.DnsTTL) + } + + provider, err := gandiv5.NewDNSProviderConfig(providerConfig) + if err != nil { + return nil, err + } + + return provider, nil +} diff --git a/pkg/core/ssl-deployer/providers/gcore-cdn/gcore_cdn.go b/pkg/core/ssl-deployer/providers/gcore-cdn/gcore_cdn.go index a64242df..d8862215 100644 --- a/pkg/core/ssl-deployer/providers/gcore-cdn/gcore_cdn.go +++ b/pkg/core/ssl-deployer/providers/gcore-cdn/gcore_cdn.go @@ -18,7 +18,7 @@ import ( ) type SSLDeployerProviderConfig struct { - // Gcore API Token。 + // G-Core API Token。 ApiToken string `json:"apiToken"` // CDN 资源 ID。 ResourceId int64 `json:"resourceId"` diff --git a/pkg/core/ssl-manager/providers/gcore-cdn/gcore_cdn.go b/pkg/core/ssl-manager/providers/gcore-cdn/gcore_cdn.go index 34539aa2..5b5b6b5a 100644 --- a/pkg/core/ssl-manager/providers/gcore-cdn/gcore_cdn.go +++ b/pkg/core/ssl-manager/providers/gcore-cdn/gcore_cdn.go @@ -15,7 +15,7 @@ import ( ) type SSLManagerProviderConfig struct { - // Gcore API Token。 + // G-Core API Token。 ApiToken string `json:"apiToken"` } diff --git a/ui/public/imgs/providers/gandinet.svg b/ui/public/imgs/providers/gandinet.svg new file mode 100644 index 00000000..985e0900 --- /dev/null +++ b/ui/public/imgs/providers/gandinet.svg @@ -0,0 +1 @@ + diff --git a/ui/src/components/access/AccessForm.tsx b/ui/src/components/access/AccessForm.tsx index df9bd24c..a10a8218 100644 --- a/ui/src/components/access/AccessForm.tsx +++ b/ui/src/components/access/AccessForm.tsx @@ -45,6 +45,7 @@ import AccessConfigFieldsProviderDuckDNS from "./forms/AccessConfigFieldsProvide import AccessConfigFieldsProviderDynv6 from "./forms/AccessConfigFieldsProviderDynv6"; import AccessConfigFieldsProviderEmail from "./forms/AccessConfigFieldsProviderEmail"; import AccessConfigFieldsProviderFlexCDN from "./forms/AccessConfigFieldsProviderFlexCDN"; +import AccessConfigFieldsProviderGandinet from "./forms/AccessConfigFieldsProviderGandinet"; import AccessConfigFieldsProviderGcore from "./forms/AccessConfigFieldsProviderGcore"; import AccessConfigFieldsProviderGlobalSignAtlas from "./forms/AccessConfigFieldsProviderGlobalSignAtlas"; import AccessConfigFieldsProviderGname from "./forms/AccessConfigFieldsProviderGname"; @@ -241,6 +242,9 @@ const AccessForm = ({ className, style, disabled, initialValues, mode, usage, on case ACCESS_PROVIDERS.FLEXCDN: { return ; } + case ACCESS_PROVIDERS.GANDINET: { + return ; + } case ACCESS_PROVIDERS.GCORE: { return ; } diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderGandinet.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderGandinet.tsx new file mode 100644 index 00000000..dd1064fe --- /dev/null +++ b/ui/src/components/access/forms/AccessConfigFieldsProviderGandinet.tsx @@ -0,0 +1,52 @@ +import { getI18n, useTranslation } from "react-i18next"; +import { Form, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { useFormNestedFieldsContext } from "./_context"; + +const AccessConfigFormFieldsProviderGandinet = () => { + const { i18n, t } = useTranslation(); + + const { parentNamePath } = useFormNestedFieldsContext(); + const formSchema = z.object({ + [parentNamePath]: getSchema({ i18n }), + }); + const formRule = createSchemaFieldRule(formSchema); + const initialValues = getInitialValues(); + + return ( + <> + } + > + + + + ); +}; + +const getInitialValues = (): Nullish>> => { + return { + personalAccessToken: "", + }; +}; + +const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { + const { t } = i18n; + + return z.object({ + personalAccessToken: z.string().nonempty(t("access.form.gandinet_personal_access_token.placeholder")), + }); +}; + +const _default = Object.assign(AccessConfigFormFieldsProviderGandinet, { + getInitialValues, + getSchema, +}); + +export default _default; diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderGcore.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderGcore.tsx index e39ac98b..e27902e4 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProviderGcore.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProviderGcore.tsx @@ -40,10 +40,7 @@ const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) = const { t } = i18n; return z.object({ - apiToken: z - .string() - .min(1, t("access.form.gcore_api_token.placeholder")) - .max(256, t("common.errmsg.string_max", { max: 256 })), + apiToken: z.string().nonempty(t("access.form.gcore_api_token.placeholder")), }); }; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index fc18b4f8..50f71663 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -49,6 +49,7 @@ export const ACCESS_PROVIDERS = Object.freeze({ DYNV6: "dynv6", EMAIL: "email", FLEXCDN: "flexcdn", + GANDINET: "gandinet", GCORE: "gcore", GLOBALSIGNATLAS: "globalsignatlas", GNAME: "gname", @@ -177,6 +178,7 @@ export const accessProvidersMap: Maphttps://flexcdn.cn/docs/api/auth", - "access.form.gcore_api_token.label": "Gcore API token", - "access.form.gcore_api_token.placeholder": "Please enter Gcore API token", + "access.form.gandinet_personal_access_token.label": "Gandi.net personal access token", + "access.form.gandinet_personal_access_token.placeholder": "Please enter Gandi.net personal access token", + "access.form.gandinet_personal_access_token.tooltip": "For more information, see https://api.gandi.net/docs/authentication/", + "access.form.gcore_api_token.label": "G-Core API token", + "access.form.gcore_api_token.placeholder": "Please enter G-Core API token", "access.form.gcore_api_token.tooltip": "For more information, see https://api.gcore.com/docs/iam#section/Authentication", "access.form.gname_app_id.label": "GNAME AppID", "access.form.gname_app_id.placeholder": "Please enter GNAME AppID", diff --git a/ui/src/i18n/locales/en/nls.provider.json b/ui/src/i18n/locales/en/nls.provider.json index ffb9a0bc..7b3f7d0d 100644 --- a/ui/src/i18n/locales/en/nls.provider.json +++ b/ui/src/i18n/locales/en/nls.provider.json @@ -85,8 +85,9 @@ "provider.email": "Email (SMTP)", "provider.fastly": "Fastly", "provider.flexcdn": "FlexCDN", - "provider.gcore": "Gcore", - "provider.gcore.cdn": "Gcore - CDN (Content Delivery Network)", + "provider.gandinet": "Gandi.net", + "provider.gcore": "G-Core", + "provider.gcore.cdn": "G-Core - CDN (Content Delivery Network)", "provider.globalsignatlas": "GlobalSign Atlas", "provider.gname": "GNAME", "provider.godaddy": "GoDaddy", diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index 1fc17c4f..d5e57818 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -521,11 +521,11 @@ "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 dashboard.", - "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.label": "G-Core CDN resource ID", + "workflow_node.deploy.form.gcore_cdn_resource_id.placeholder": "Please enter G-Core CDN resource ID", "workflow_node.deploy.form.gcore_cdn_resource_id.tooltip": "For more information, see https://cdn.gcore.com/resources/list", - "workflow_node.deploy.form.gcore_cdn_certificate_id.label": "Gcore CDN certificate ID (Optional)", - "workflow_node.deploy.form.gcore_cdn_certificate_id.placeholder": "Please enter Gcore CDN certificate ID", + "workflow_node.deploy.form.gcore_cdn_certificate_id.label": "G-Core CDN certificate ID (Optional)", + "workflow_node.deploy.form.gcore_cdn_certificate_id.placeholder": "Please enter G-Core CDN certificate ID", "workflow_node.deploy.form.gcore_cdn_certificate_id.help": "", "workflow_node.deploy.form.gcore_cdn_certificate_id.tooltip": "For more information, see https://cdn.gcore.com/ssl", "workflow_node.deploy.form.goedge_resource_type.label": "Resource type", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index 96cf8c89..0838810f 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -260,8 +260,11 @@ "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.gcore_api_token.label": "Gcore API Token", - "access.form.gcore_api_token.placeholder": "请输入 Gcore API Token", + "access.form.gandinet_personal_access_token.label": "Gandi.net Personal Access Token", + "access.form.gandinet_personal_access_token.placeholder": "请输入 Gandi.net Personal Access Token", + "access.form.gandinet_personal_access_token.tooltip": "这是什么?请参阅 https://api.gandi.net/docs/authentication/", + "access.form.gcore_api_token.label": "G-Core API Token", + "access.form.gcore_api_token.placeholder": "请输入 G-Core API Token", "access.form.gcore_api_token.tooltip": "这是什么?请参阅 https://api.gcore.com/docs/iam#section/Authentication", "access.form.gname_app_id.label": "GNAME AppID", "access.form.gname_app_id.placeholder": "请输入 GNAME AppID", diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json index 95e31d9f..bf23ceca 100644 --- a/ui/src/i18n/locales/zh/nls.provider.json +++ b/ui/src/i18n/locales/zh/nls.provider.json @@ -85,8 +85,9 @@ "provider.email": "邮件(SMTP)", "provider.fastly": "Fastly", "provider.flexcdn": "FlexCDN", - "provider.gcore": "Gcore", - "provider.gcore.cdn": "Gcore - 内容分发网络 CDN", + "provider.gandinet": "Gandi.net", + "provider.gcore": "G-Core", + "provider.gcore.cdn": "G-Core - 内容分发网络 CDN", "provider.globalsignatlas": "GlobalSign Atlas", "provider.gname": "GNAME", "provider.godaddy": "GoDaddy", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 01f4e249..238bd62d 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -519,11 +519,11 @@ "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.label": "G-Core CDN 资源 ID", + "workflow_node.deploy.form.gcore_cdn_resource_id.placeholder": "请输入 G-Core CDN 资源 ID", "workflow_node.deploy.form.gcore_cdn_resource_id.tooltip": "这是什么?请参阅 https://cdn.gcore.com/resources/list", - "workflow_node.deploy.form.gcore_cdn_certificate_id.label": "Gcore CDN 原证书 ID(可选)", - "workflow_node.deploy.form.gcore_cdn_certificate_id.placeholder": "请输入 Gcore CDN 原证书 ID", + "workflow_node.deploy.form.gcore_cdn_certificate_id.label": "G-Core CDN 原证书 ID(可选)", + "workflow_node.deploy.form.gcore_cdn_certificate_id.placeholder": "请输入 G-Core CDN 原证书 ID", "workflow_node.deploy.form.gcore_cdn_certificate_id.help": "提示:不填写时,将上传新证书;否则,将替换原证书。", "workflow_node.deploy.form.gcore_cdn_certificate_id.tooltip": "这是什么?请参阅 https://cdn.gcore.com/ssl", "workflow_node.deploy.form.goedge_resource_type.label": "证书部署方式",