diff --git a/internal/certacme/certifiers/sp_dynadot.go b/internal/certacme/certifiers/sp_dynadot.go
new file mode 100644
index 000000000..2a7168210
--- /dev/null
+++ b/internal/certacme/certifiers/sp_dynadot.go
@@ -0,0 +1,27 @@
+package certifiers
+
+import (
+ "fmt"
+
+ "github.com/certimate-go/certimate/internal/domain"
+ "github.com/certimate-go/certimate/pkg/core"
+ "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/dynadot"
+ xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
+)
+
+func init() {
+ ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeDynadot, func(options *ProviderFactoryOptions) (core.ACMEChallenger, error) {
+ credentials := domain.AccessConfigForDynadot{}
+ if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {
+ return nil, fmt.Errorf("failed to populate provider access config: %w", err)
+ }
+
+ provider, err := dynadot.NewChallenger(&dynadot.ChallengerConfig{
+ ApiKey: credentials.ApiKey,
+ ApiSecret: credentials.ApiSecret,
+ DnsPropagationTimeout: options.DnsPropagationTimeout,
+ DnsTTL: options.DnsTTL,
+ })
+ return provider, err
+ })
+}
diff --git a/internal/domain/access.go b/internal/domain/access.go
index 2ed71e329..b0e234db8 100644
--- a/internal/domain/access.go
+++ b/internal/domain/access.go
@@ -225,6 +225,11 @@ type AccessConfigForDuckDNS struct {
Token string `json:"token"`
}
+type AccessConfigForDynadot struct {
+ ApiKey string `json:"apiKey"`
+ ApiSecret string `json:"apiSecret"`
+}
+
type AccessConfigForDynu struct {
ApiKey string `json:"apiKey"`
}
diff --git a/internal/domain/provider.go b/internal/domain/provider.go
index 14754671b..13de3b476 100644
--- a/internal/domain/provider.go
+++ b/internal/domain/provider.go
@@ -50,6 +50,7 @@ const (
AccessProviderTypeDogeCloud = AccessProviderType("dogecloud")
AccessProviderTypeDokploy = AccessProviderType("dokploy")
AccessProviderTypeDuckDNS = AccessProviderType("duckdns")
+ AccessProviderTypeDynadot = AccessProviderType("dynadot")
AccessProviderTypeDynu = AccessProviderType("dynu")
AccessProviderTypeDynv6 = AccessProviderType("dynv6")
AccessProviderTypeEmail = AccessProviderType("email")
@@ -193,6 +194,7 @@ const (
ACMEDns01ProviderTypeDNSLA = ACMEDns01ProviderType(AccessProviderTypeDNSLA)
ACMEDns01ProviderTypeDNSMadeEasy = ACMEDns01ProviderType(AccessProviderTypeDNSMadeEasy)
ACMEDns01ProviderTypeDuckDNS = ACMEDns01ProviderType(AccessProviderTypeDuckDNS)
+ ACMEDns01ProviderTypeDynadot = ACMEDns01ProviderType(AccessProviderTypeDynadot)
ACMEDns01ProviderTypeDynu = ACMEDns01ProviderType(AccessProviderTypeDynu)
ACMEDns01ProviderTypeDynv6 = ACMEDns01ProviderType(AccessProviderTypeDynv6)
ACMEDns01ProviderTypeGandinet = ACMEDns01ProviderType(AccessProviderTypeGandinet)
diff --git a/pkg/core/certifier/challengers/dns01/dynadot/dynadot.go b/pkg/core/certifier/challengers/dns01/dynadot/dynadot.go
new file mode 100644
index 000000000..0b124b3c9
--- /dev/null
+++ b/pkg/core/certifier/challengers/dns01/dynadot/dynadot.go
@@ -0,0 +1,39 @@
+package dynadot
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/certimate-go/certimate/pkg/core/certifier"
+ "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/dynadot/internal"
+)
+
+type ChallengerConfig struct {
+ ApiKey string `json:"apiKey"`
+ ApiSecret string `json:"apiSecret"`
+ DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"`
+ DnsTTL int `json:"dnsTTL,omitempty"`
+}
+
+func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {
+ if config == nil {
+ return nil, fmt.Errorf("the configuration of the acme challenge provider is nil")
+ }
+
+ providerConfig := internal.NewDefaultConfig()
+ providerConfig.APIKey = config.ApiKey
+ providerConfig.APISecret = config.ApiSecret
+ if config.DnsPropagationTimeout != 0 {
+ providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
+ }
+ if config.DnsTTL != 0 {
+ providerConfig.TTL = config.DnsTTL
+ }
+
+ provider, err := internal.NewDNSProviderConfig(providerConfig)
+ if err != nil {
+ return nil, err
+ }
+
+ return provider, nil
+}
diff --git a/pkg/core/certifier/challengers/dns01/dynadot/internal/lego.go b/pkg/core/certifier/challengers/dns01/dynadot/internal/lego.go
new file mode 100644
index 000000000..e70734428
--- /dev/null
+++ b/pkg/core/certifier/challengers/dns01/dynadot/internal/lego.go
@@ -0,0 +1,154 @@
+package internal
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/go-acme/lego/v4/challenge"
+ "github.com/go-acme/lego/v4/challenge/dns01"
+ "github.com/go-acme/lego/v4/platform/config/env"
+ "github.com/samber/lo"
+
+ dynadotsdk "github.com/certimate-go/certimate/pkg/sdk3rd/dynadot"
+)
+
+const (
+ envNamespace = "DYNADOT_"
+
+ EnvAPIKey = envNamespace + "API_KEY"
+ EnvAPISecret = envNamespace + "API_SECRET"
+
+ EnvTTL = envNamespace + "TTL"
+ EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
+ EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
+ EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
+)
+
+var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
+
+type Config struct {
+ APIKey string
+ APISecret string
+
+ PropagationTimeout time.Duration
+ PollingInterval time.Duration
+ TTL int
+ HTTPTimeout time.Duration
+}
+
+type DNSProvider struct {
+ config *Config
+ client *dynadotsdk.Client
+}
+
+type dnsRecordCacheEntry struct {
+ Zone string
+ SubHost string
+ Value string
+}
+
+func NewDefaultConfig() *Config {
+ return &Config{
+ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
+ PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
+ PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
+ HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
+ }
+}
+
+func NewDNSProvider() (*DNSProvider, error) {
+ values, err := env.Get(EnvAPIKey, EnvAPISecret)
+ if err != nil {
+ return nil, fmt.Errorf("dynadot: %w", err)
+ }
+
+ config := NewDefaultConfig()
+ config.APIKey = values[EnvAPIKey]
+ config.APISecret = values[EnvAPISecret]
+
+ return NewDNSProviderConfig(config)
+}
+
+func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
+ if config == nil {
+ return nil, fmt.Errorf("dynadot: the configuration of the DNS provider is nil")
+ }
+
+ client, err := dynadotsdk.NewClient(config.APIKey, config.APISecret)
+ if err != nil {
+ return nil, fmt.Errorf("dynadot: %w", err)
+ } else {
+ client.SetTimeout(config.HTTPTimeout)
+ }
+
+ return &DNSProvider{
+ config: config,
+ client: client,
+ }, nil
+}
+
+func (d *DNSProvider) Present(domain, token, keyAuth string) error {
+ info := dns01.GetChallengeInfo(domain, keyAuth)
+
+ authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
+ if err != nil {
+ return fmt.Errorf("dynadot: could not find zone for domain %q: %w", domain, err)
+ }
+
+ subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
+ if err != nil {
+ return fmt.Errorf("dynadot: %w", err)
+ }
+
+ // REF: https://www.dynadot.com/domain/api-document (set_dns)
+ request := &dynadotsdk.SetDnsRequest{
+ SubList: []*dynadotsdk.DnsSubRecord{
+ {
+ SubHost: subDomain,
+ RecordType: "TXT",
+ RecordValue1: info.Value,
+ },
+ },
+ TTL: lo.ToPtr(int64(d.config.TTL)),
+ AddDnsToCurrentSetting: lo.ToPtr(true),
+ }
+ if _, err := d.client.SetDns(dns01.UnFqdn(authZone), request); err != nil {
+ return fmt.Errorf("dynadot: error when create record: %w", err)
+ }
+
+ return nil
+}
+
+func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
+ info := dns01.GetChallengeInfo(domain, keyAuth)
+
+ authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
+ if err != nil {
+ return fmt.Errorf("dynadot: could not find zone for domain %q: %w", domain, err)
+ }
+
+ subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
+ if err != nil {
+ return fmt.Errorf("dynadot: %w", err)
+ }
+
+ // REF: https://www.dynadot.com/domain/api-document (remove_dns)
+ request := &dynadotsdk.RemoveDnsRequest{
+ SubList: []*dynadotsdk.DnsSubRecord{
+ {
+ SubHost: subDomain,
+ RecordType: "TXT",
+ RecordValue1: info.Value,
+ },
+ },
+ }
+ if _, err := d.client.RemoveDns(dns01.UnFqdn(authZone), request); err != nil {
+ return fmt.Errorf("dynadot: error when delete record: %w", err)
+ }
+
+ return nil
+}
+
+func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
+ return d.config.PropagationTimeout, d.config.PollingInterval
+}
diff --git a/pkg/sdk3rd/dynadot/api_remove_dns.go b/pkg/sdk3rd/dynadot/api_remove_dns.go
new file mode 100644
index 000000000..adc975676
--- /dev/null
+++ b/pkg/sdk3rd/dynadot/api_remove_dns.go
@@ -0,0 +1,40 @@
+package dynadot
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+)
+
+type RemoveDnsRequest struct {
+ DnsMainList []*DnsMainRecord `json:"dns_main_list,omitempty"`
+ SubList []*DnsSubRecord `json:"sub_list,omitempty"`
+}
+
+type RemoveDnsResponse struct {
+ sdkResponseBase
+}
+
+func (c *Client) RemoveDns(domain string, req *RemoveDnsRequest) (*RemoveDnsResponse, error) {
+ return c.RemoveDnsWithContext(context.Background(), domain, req)
+}
+
+func (c *Client) RemoveDnsWithContext(ctx context.Context, domain string, req *RemoveDnsRequest) (*RemoveDnsResponse, error) {
+ if domain == "" {
+ return nil, fmt.Errorf("sdkerr: unset domain")
+ }
+
+ httpreq, err := c.newRequest(http.MethodDelete, fmt.Sprintf("/restful/v2/domains/%s/records", domain), req)
+ if err != nil {
+ return nil, err
+ } else {
+ httpreq.SetContext(ctx)
+ }
+
+ result := &RemoveDnsResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/pkg/sdk3rd/dynadot/api_set_dns.go b/pkg/sdk3rd/dynadot/api_set_dns.go
new file mode 100644
index 000000000..7fcb4e51a
--- /dev/null
+++ b/pkg/sdk3rd/dynadot/api_set_dns.go
@@ -0,0 +1,42 @@
+package dynadot
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+)
+
+type SetDnsRequest struct {
+ DnsMainList []*DnsMainRecord `json:"dns_main_list,omitempty"`
+ SubList []*DnsSubRecord `json:"sub_list,omitempty"`
+ TTL *int64 `json:"ttl,omitempty"`
+ AddDnsToCurrentSetting *bool `json:"add_dns_to_current_setting,omitempty"`
+}
+
+type SetDnsResponse struct {
+ sdkResponseBase
+}
+
+func (c *Client) SetDns(domain string, req *SetDnsRequest) (*SetDnsResponse, error) {
+ return c.SetDnsWithContext(context.Background(), domain, req)
+}
+
+func (c *Client) SetDnsWithContext(ctx context.Context, domain string, req *SetDnsRequest) (*SetDnsResponse, error) {
+ if domain == "" {
+ return nil, fmt.Errorf("sdkerr: unset domain")
+ }
+
+ httpreq, err := c.newRequest(http.MethodPost, fmt.Sprintf("/restful/v2/domains/%s/records", domain), req)
+ if err != nil {
+ return nil, err
+ } else {
+ httpreq.SetContext(ctx)
+ }
+
+ result := &SetDnsResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/pkg/sdk3rd/dynadot/client.go b/pkg/sdk3rd/dynadot/client.go
new file mode 100644
index 000000000..754b898df
--- /dev/null
+++ b/pkg/sdk3rd/dynadot/client.go
@@ -0,0 +1,149 @@
+package dynadot
+
+import (
+ "crypto/hmac"
+ "crypto/sha256"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/go-resty/resty/v2"
+
+ "github.com/certimate-go/certimate/internal/app"
+)
+
+type Client struct {
+ apiKey string
+ apiSecret string
+
+ client *resty.Client
+}
+
+func NewClient(apiKey, apiSecret string) (*Client, error) {
+ if apiKey == "" {
+ return nil, fmt.Errorf("sdkerr: unset apiKey")
+ }
+ if apiSecret == "" {
+ return nil, fmt.Errorf("sdkerr: unset apiSecret")
+ }
+
+ client := resty.New().
+ SetBaseURL("https://api.dynadot.com").
+ SetHeader("Accept", "application/json").
+ SetHeader("Content-Type", "application/json").
+ SetHeader("User-Agent", app.AppUserAgent)
+
+ return &Client{
+ apiKey: apiKey,
+ apiSecret: apiSecret,
+ client: client,
+ }, nil
+}
+
+func (c *Client) SetTimeout(timeout time.Duration) *Client {
+ c.client.SetTimeout(timeout)
+ return c
+}
+
+func (c *Client) newRequest(method string, path string, params any) (*resty.Request, error) {
+ if method == "" {
+ return nil, fmt.Errorf("sdkerr: unset method")
+ }
+ if path == "" {
+ return nil, fmt.Errorf("sdkerr: unset path")
+ }
+
+ var body string
+ if params != nil {
+ jsonb, err := json.Marshal(params)
+ if err != nil {
+ return nil, fmt.Errorf("sdkerr: failed to marshal request body: %w", err)
+ }
+ body = string(jsonb)
+ }
+
+ req := c.client.R()
+ req.Method = method
+ req.URL = path
+ req.SetHeader("Authorization", "Bearer "+c.apiKey)
+ req.SetHeader("X-Signature", generateSignature(c.apiKey, c.apiSecret, path, "", body))
+ if body != "" {
+ req.SetBody(body)
+ }
+ return req, nil
+}
+
+func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {
+ if req == nil {
+ return nil, fmt.Errorf("sdkerr: nil request")
+ }
+
+ // WARN:
+ // PLEASE DO NOT USE `req.SetBody` or `req.SetFormData` HERE! USE `newRequest` INSTEAD.
+ // PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.
+
+ resp, err := req.Send()
+ if err != nil {
+ return resp, fmt.Errorf("sdkerr: failed to send request: %w", err)
+ } else if resp.IsError() {
+ return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String())
+ }
+
+ return resp, nil
+}
+
+func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {
+ if req == nil {
+ return nil, fmt.Errorf("sdkerr: nil request")
+ }
+
+ resp, err := c.doRequest(req)
+ if err != nil {
+ if resp != nil {
+ json.Unmarshal(resp.Body(), &res)
+ }
+ return resp, err
+ }
+
+ if len(resp.Body()) != 0 {
+ if err := json.Unmarshal(resp.Body(), &res); err != nil {
+ return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String())
+ } else {
+ // NOTE: Unlike most providers in this project (which use `0` for success),
+ // Dynadot mirrors HTTP-style status codes inside the response body and
+ // always returns `200` on success.
+ // REF: https://www.dynadot.com/domain/api-document
+ if tcode := res.GetCode(); tcode != 200 {
+ return resp, fmt.Errorf("sdkerr: api error: code='%d', message='%s'", tcode, res.GetMessage())
+ }
+ }
+ }
+
+ return resp, nil
+}
+
+// generateSignature builds the X-Signature header value as required by the
+// Dynadot RESTful v2 API. It signs the concatenation of:
+//
+// apiKey + "\n" + fullPathAndQuery + "\n" + xRequestId + "\n" + requestBody
+//
+// using HMAC-SHA256 with the API secret as key, then encodes the result with
+// standard Base64.
+//
+// REF: https://www.dynadot.com/domain/api-document
+func generateSignature(apiKey, apiSecret, fullPathAndQuery, xRequestId, requestBody string) string {
+ var sb strings.Builder
+ sb.WriteString(apiKey)
+ sb.WriteString("\n")
+ sb.WriteString(fullPathAndQuery)
+ sb.WriteString("\n")
+ sb.WriteString(xRequestId)
+ sb.WriteString("\n")
+ sb.WriteString(requestBody)
+
+ mac := hmac.New(sha256.New, []byte(apiSecret))
+ mac.Write([]byte(sb.String()))
+ return base64.StdEncoding.EncodeToString(mac.Sum(nil))
+}
diff --git a/pkg/sdk3rd/dynadot/types.go b/pkg/sdk3rd/dynadot/types.go
new file mode 100644
index 000000000..56738b4a1
--- /dev/null
+++ b/pkg/sdk3rd/dynadot/types.go
@@ -0,0 +1,34 @@
+package dynadot
+
+type sdkResponse interface {
+ GetCode() int
+ GetMessage() string
+}
+
+type sdkResponseBase struct {
+ Code int `json:"code"`
+ Message string `json:"message"`
+}
+
+func (r *sdkResponseBase) GetCode() int {
+ return r.Code
+}
+
+func (r *sdkResponseBase) GetMessage() string {
+ return r.Message
+}
+
+var _ sdkResponse = (*sdkResponseBase)(nil)
+
+type DnsMainRecord struct {
+ RecordType string `json:"record_type"`
+ RecordValue1 string `json:"record_value1"`
+ RecordValue2 string `json:"record_value2,omitempty"`
+}
+
+type DnsSubRecord struct {
+ SubHost string `json:"sub_host"`
+ RecordType string `json:"record_type"`
+ RecordValue1 string `json:"record_value1"`
+ RecordValue2 string `json:"record_value2,omitempty"`
+}
diff --git a/ui/public/imgs/providers/dynadot.svg b/ui/public/imgs/providers/dynadot.svg
new file mode 100644
index 000000000..ef253a55f
--- /dev/null
+++ b/ui/public/imgs/providers/dynadot.svg
@@ -0,0 +1 @@
+
diff --git a/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx b/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx
index 002c1294c..930c3242a 100644
--- a/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx
+++ b/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx
@@ -42,6 +42,7 @@ import AccessConfigFieldsProviderDNSMadeEasy from "./AccessConfigFieldsProviderD
import AccessConfigFieldsProviderDogeCloud from "./AccessConfigFieldsProviderDogeCloud";
import AccessConfigFieldsProviderDokploy from "./AccessConfigFieldsProviderDokploy";
import AccessConfigFieldsProviderDuckDNS from "./AccessConfigFieldsProviderDuckDNS";
+import AccessConfigFieldsProviderDynadot from "./AccessConfigFieldsProviderDynadot";
import AccessConfigFieldsProviderDynu from "./AccessConfigFieldsProviderDynu";
import AccessConfigFieldsProviderDynv6 from "./AccessConfigFieldsProviderDynv6";
import AccessConfigFieldsProviderEmail from "./AccessConfigFieldsProviderEmail";
@@ -159,6 +160,7 @@ const providerComponentMap: Partial {
+ 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 {
+ apiKey: "",
+ apiSecret: "",
+ };
+};
+
+const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
+ const { t: _ } = i18n;
+
+ return z.object({
+ apiKey: z.string().nonempty(),
+ apiSecret: z.string().nonempty(),
+ });
+};
+
+const _default = Object.assign(AccessConfigFormFieldsProviderDynadot, {
+ getInitialValues,
+ getSchema,
+});
+
+export default _default;
diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts
index 0f8afafe8..e56de0046 100644
--- a/ui/src/domain/provider.ts
+++ b/ui/src/domain/provider.ts
@@ -55,6 +55,7 @@ export const ACCESS_PROVIDERS = Object.freeze({
DOGECLOUD: "dogecloud",
DOKPLOY: "dokploy",
DUCKDNS: "duckdns",
+ DYNADOT: "dynadot",
DYNU: "dynu",
DYNV6: "dynv6",
EMAIL: "email",
@@ -218,6 +219,7 @@ export const accessProvidersMap: Maphttps://www.duckdns.org/spec.jsp"
},
+ "dynadot_api_key": {
+ "label": "Dynadot API key",
+ "placeholder": "Please enter Dynadot API key",
+ "tooltip": "For more information, see https://www.dynadot.com/domain/api-document"
+ },
+ "dynadot_api_secret": {
+ "label": "Dynadot API secret",
+ "placeholder": "Please enter Dynadot API secret",
+ "tooltip": "For more information, see https://www.dynadot.com/domain/api-document"
+ },
"dynu_api_key": {
"label": "Dynu API key",
"placeholder": "Please enter Dynu API key",
diff --git a/ui/src/i18n/resources/en/nls.provider.json b/ui/src/i18n/resources/en/nls.provider.json
index 4d3c0d54c..828bd211f 100644
--- a/ui/src/i18n/resources/en/nls.provider.json
+++ b/ui/src/i18n/resources/en/nls.provider.json
@@ -119,6 +119,7 @@
"dogecloud": "Doge Cloud",
"dokploy": "Dokploy",
"duckdns": "Duck DNS",
+ "dynadot": "Dynadot",
"dynu": "Dynu",
"dynv6": "dynv6",
"email": "Email (SMTP)",
diff --git a/ui/src/i18n/resources/zh/nls.access.json b/ui/src/i18n/resources/zh/nls.access.json
index 081699b59..b778ee1b4 100644
--- a/ui/src/i18n/resources/zh/nls.access.json
+++ b/ui/src/i18n/resources/zh/nls.access.json
@@ -496,6 +496,16 @@
"placeholder": "请输入 DuckDNS Token",
"tooltip": "这是什么?请参阅 https://www.duckdns.org/spec.jsp"
},
+ "dynadot_api_key": {
+ "label": "Dynadot API Key",
+ "placeholder": "请输入 Dynadot API Key",
+ "tooltip": "这是什么?请参阅 https://www.dynadot.com/zh/domain/api-document"
+ },
+ "dynadot_api_secret": {
+ "label": "Dynadot API Secret",
+ "placeholder": "请输入 Dynadot API Secret",
+ "tooltip": "这是什么?请参阅 https://www.dynadot.com/zh/domain/api-document"
+ },
"dynu_api_key": {
"label": "Dynu API Key",
"placeholder": "请输入 Dynu API Key",
diff --git a/ui/src/i18n/resources/zh/nls.provider.json b/ui/src/i18n/resources/zh/nls.provider.json
index 4e32e53e2..ef722a94f 100644
--- a/ui/src/i18n/resources/zh/nls.provider.json
+++ b/ui/src/i18n/resources/zh/nls.provider.json
@@ -119,6 +119,7 @@
"dogecloud_cdn": "多吉云 - 内容分发网络 CDN",
"dokploy": "Dokploy",
"duckdns": "Duck DNS",
+ "dynadot": "Dynadot",
"dynu": "Dynu",
"dynv6": "dynv6",
"email": "邮件(SMTP)",