diff --git a/internal/applicant/providers.go b/internal/applicant/providers.go index 81cf8156..f967aef9 100644 --- a/internal/applicant/providers.go +++ b/internal/applicant/providers.go @@ -22,6 +22,7 @@ import ( pGcore "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/gcore" pGname "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/gname" pGoDaddy "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/godaddy" + pHetzner "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/hetzner" pHuaweiCloud "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/huaweicloud" pJDCloud "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/jdcloud" pNamecheap "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/namecheap" @@ -324,6 +325,21 @@ func createApplicantProvider(options *applicantProviderOptions) (challenge.Provi return applicant, err } + case domain.ACMEDns01ProviderTypeHetzner: + { + access := domain.AccessConfigForHetzner{} + if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + applicant, err := pHetzner.NewChallengeProvider(&pHetzner.ChallengeProviderConfig{ + ApiToken: access.ApiToken, + DnsPropagationTimeout: options.DnsPropagationTimeout, + DnsTTL: options.DnsTTL, + }) + return applicant, err + } + case domain.ACMEDns01ProviderTypeHuaweiCloud, domain.ACMEDns01ProviderTypeHuaweiCloudDNS: { access := domain.AccessConfigForHuaweiCloud{} diff --git a/internal/domain/access.go b/internal/domain/access.go index 7e1549f1..30088bdf 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -181,6 +181,10 @@ type AccessConfigForGoogleTrustServices struct { EabHmacKey string `json:"eabHmacKey"` } +type AccessConfigForHetzner struct { + ApiToken string `json:"apiToken"` +} + type AccessConfigForHuaweiCloud struct { AccessKeyId string `json:"accessKeyId"` SecretAccessKey string `json:"secretAccessKey"` diff --git a/internal/domain/provider.go b/internal/domain/provider.go index db0de59d..4552553d 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -44,6 +44,7 @@ const ( AccessProviderTypeGoDaddy = AccessProviderType("godaddy") AccessProviderTypeGoEdge = AccessProviderType("goedge") AccessProviderTypeGoogleTrustServices = AccessProviderType("googletrustservices") + AccessProviderTypeHetzner = AccessProviderType("hetzner") AccessProviderTypeHuaweiCloud = AccessProviderType("huaweicloud") AccessProviderTypeJDCloud = AccessProviderType("jdcloud") AccessProviderTypeKubernetes = AccessProviderType("k8s") @@ -131,6 +132,7 @@ const ( ACMEDns01ProviderTypeGcore = ACMEDns01ProviderType(AccessProviderTypeGcore) ACMEDns01ProviderTypeGname = ACMEDns01ProviderType(AccessProviderTypeGname) ACMEDns01ProviderTypeGoDaddy = ACMEDns01ProviderType(AccessProviderTypeGoDaddy) + ACMEDns01ProviderTypeHetzner = ACMEDns01ProviderType(AccessProviderTypeHetzner) ACMEDns01ProviderTypeHuaweiCloud = ACMEDns01ProviderType(AccessProviderTypeHuaweiCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeHuaweiCloudDNS] ACMEDns01ProviderTypeHuaweiCloudDNS = ACMEDns01ProviderType(AccessProviderTypeHuaweiCloud + "-dns") ACMEDns01ProviderTypeJDCloud = ACMEDns01ProviderType(AccessProviderTypeJDCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeJDCloudDNS] diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/hetzner/hetzner.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/hetzner/hetzner.go new file mode 100644 index 00000000..c202cc78 --- /dev/null +++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/hetzner/hetzner.go @@ -0,0 +1,36 @@ +package namedotcom + +import ( + "time" + + "github.com/go-acme/lego/v4/challenge" + "github.com/go-acme/lego/v4/providers/dns/hetzner" +) + +type ChallengeProviderConfig struct { + ApiToken string `json:"apiToken"` + DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"` + DnsTTL int32 `json:"dnsTTL,omitempty"` +} + +func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) { + if config == nil { + panic("config is nil") + } + + providerConfig := hetzner.NewDefaultConfig() + providerConfig.APIKey = config.ApiToken + if config.DnsPropagationTimeout != 0 { + providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second + } + if config.DnsTTL != 0 { + providerConfig.TTL = int(config.DnsTTL) + } + + provider, err := hetzner.NewDNSProviderConfig(providerConfig) + if err != nil { + return nil, err + } + + return provider, nil +} diff --git a/ui/public/imgs/providers/hetzner.svg b/ui/public/imgs/providers/hetzner.svg new file mode 100644 index 00000000..670cdda8 --- /dev/null +++ b/ui/public/imgs/providers/hetzner.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/src/components/access/AccessForm.tsx b/ui/src/components/access/AccessForm.tsx index fdf4f93f..6f4d5086 100644 --- a/ui/src/components/access/AccessForm.tsx +++ b/ui/src/components/access/AccessForm.tsx @@ -41,6 +41,7 @@ import AccessFormGnameConfig from "./AccessFormGnameConfig"; import AccessFormGoDaddyConfig from "./AccessFormGoDaddyConfig"; import AccessFormGoEdgeConfig from "./AccessFormGoEdgeConfig"; import AccessFormGoogleTrustServicesConfig from "./AccessFormGoogleTrustServicesConfig"; +import AccessFormHetznerConfig from "./AccessFormHetznerConfig"; import AccessFormHuaweiCloudConfig from "./AccessFormHuaweiCloudConfig"; import AccessFormJDCloudConfig from "./AccessFormJDCloudConfig"; import AccessFormKubernetesConfig from "./AccessFormKubernetesConfig"; @@ -239,6 +240,8 @@ const AccessForm = forwardRef(({ className, return ; case ACCESS_PROVIDERS.GOOGLETRUSTSERVICES: return ; + case ACCESS_PROVIDERS.HETZNER: + return ; case ACCESS_PROVIDERS.HUAWEICLOUD: return ; case ACCESS_PROVIDERS.JDCLOUD: diff --git a/ui/src/components/access/AccessFormHetznerConfig.tsx b/ui/src/components/access/AccessFormHetznerConfig.tsx new file mode 100644 index 00000000..12bf21b2 --- /dev/null +++ b/ui/src/components/access/AccessFormHetznerConfig.tsx @@ -0,0 +1,57 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type AccessConfigForHetzner } from "@/domain/access"; + +type AccessFormHetznerConfigFieldValues = Nullish; + +export type AccessFormHetznerConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: AccessFormHetznerConfigFieldValues; + onValuesChange?: (values: AccessFormHetznerConfigFieldValues) => void; +}; + +const initFormModel = (): AccessFormHetznerConfigFieldValues => { + return { + apiToken: "", + }; +}; + +const AccessFormHetznerConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormHetznerConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + apiToken: z.string().nonempty(t("access.form.hetzner_api_token.placeholder")).trim(), + }); + const formRule = createSchemaFieldRule(formSchema); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ } + > + + +
+ ); +}; + +export default AccessFormHetznerConfig; diff --git a/ui/src/domain/access.ts b/ui/src/domain/access.ts index 3765310a..1576fd67 100644 --- a/ui/src/domain/access.ts +++ b/ui/src/domain/access.ts @@ -36,6 +36,7 @@ export interface AccessModel extends BaseModel { | AccessConfigForGoDaddy | AccessConfigForGoEdge | AccessConfigForGoogleTrustServices + | AccessConfigForHetzner | AccessConfigForHuaweiCloud | AccessConfigForJDCloud | AccessConfigForKubernetes @@ -238,6 +239,10 @@ export type AccessConfigForGoogleTrustServices = { eabHmacKey: string; }; +export type AccessConfigForHetzner = { + apiToken: string; +}; + export type AccessConfigForHuaweiCloud = { accessKeyId: string; secretAccessKey: string; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 15542455..712530d7 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -35,6 +35,7 @@ export const ACCESS_PROVIDERS = Object.freeze({ GODADDY: "godaddy", GOEDGE: "goedge", GOOGLETRUSTSERVICES: "googletrustservices", + HETZNER: "hetzner", HUAWEICLOUD: "huaweicloud", JDCLOUD: "jdcloud", KUBERNETES: "k8s", @@ -142,6 +143,7 @@ export const accessProvidersMap: Maphttps://cloud.google.com/certificate-manager/docs/public-ca-tutorial", + "access.form.hetzner_api_token.label": "Hetzner API token", + "access.form.hetzner_api_token.placeholder": "Please enter Hetzner API token", + "access.form.hetzner_api_token.tooltip": "For more information, see https://docs.hetzner.com/cloud/api/getting-started/generating-api-token", "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", diff --git a/ui/src/i18n/locales/en/nls.provider.json b/ui/src/i18n/locales/en/nls.provider.json index a3bbfecc..e9122149 100644 --- a/ui/src/i18n/locales/en/nls.provider.json +++ b/ui/src/i18n/locales/en/nls.provider.json @@ -74,6 +74,7 @@ "provider.godaddy": "GoDaddy", "provider.goedge": "GoEdge", "provider.googletrustservices": "Google Trust Services", + "provider.hetzner": "Hetzner", "provider.huaweicloud": "Huawei Cloud", "provider.huaweicloud.cdn": "Huawei Cloud - CDN (Content Delivery Network)", "provider.huaweicloud.dns": "Huawei Cloud - DNS (Domain Name Service)", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index fdae1705..e2cacc1f 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -231,6 +231,9 @@ "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.hetzner_api_token.label": "Hetzner API Token", + "access.form.hetzner_api_token.placeholder": "请输入 Hetzner API Token", + "access.form.hetzner_api_token.tooltip": "这是什么?请参阅 https://docs.hetzner.com/cloud/api/getting-started/generating-api-token", "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", diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json index b9fb8777..a6b94779 100644 --- a/ui/src/i18n/locales/zh/nls.provider.json +++ b/ui/src/i18n/locales/zh/nls.provider.json @@ -74,6 +74,7 @@ "provider.godaddy": "GoDaddy", "provider.goedge": "GoEdge", "provider.googletrustservices": "Google Trust Services", + "provider.hetzner": "Hetzner", "provider.huaweicloud": "华为云", "provider.huaweicloud.cdn": "华为云 - 内容分发网络 CDN", "provider.huaweicloud.dns": "华为云 - 云解析 DNS",