diff --git a/internal/applicant/providers.go b/internal/applicant/providers.go
index f967aef9..e35aee3e 100644
--- a/internal/applicant/providers.go
+++ b/internal/applicant/providers.go
@@ -17,6 +17,7 @@ import (
pClouDNS "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/cloudns"
pCMCCCloud "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/cmcccloud"
pDeSEC "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/desec"
+ pDigitalOcean "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/digitalocean"
pDNSLA "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/dnsla"
pDynv6 "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/dynv6"
pGcore "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/gcore"
@@ -247,6 +248,21 @@ func createApplicantProvider(options *applicantProviderOptions) (challenge.Provi
return applicant, err
}
+ case domain.ACMEDns01ProviderTypeDigitalOcean:
+ {
+ access := domain.AccessConfigForDigitalOcean{}
+ if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
+ return nil, fmt.Errorf("failed to populate provider access config: %w", err)
+ }
+
+ applicant, err := pDigitalOcean.NewChallengeProvider(&pDigitalOcean.ChallengeProviderConfig{
+ AccessToken: access.AccessToken,
+ DnsPropagationTimeout: options.DnsPropagationTimeout,
+ DnsTTL: options.DnsTTL,
+ })
+ return applicant, err
+ }
+
case domain.ACMEDns01ProviderTypeDNSLA:
{
access := domain.AccessConfigForDNSLA{}
diff --git a/internal/domain/access.go b/internal/domain/access.go
index 30088bdf..c3530a4f 100644
--- a/internal/domain/access.go
+++ b/internal/domain/access.go
@@ -112,6 +112,10 @@ type AccessConfigForDeSEC struct {
Token string `json:"token"`
}
+type AccessConfigForDigitalOcean struct {
+ AccessToken string `json:"accessToken"`
+}
+
type AccessConfigForDingTalkBot struct {
WebhookUrl string `json:"webhookUrl"`
Secret string `json:"secret"`
diff --git a/internal/domain/provider.go b/internal/domain/provider.go
index 4552553d..52a71e64 100644
--- a/internal/domain/provider.go
+++ b/internal/domain/provider.go
@@ -31,6 +31,7 @@ const (
AccessProviderTypeCTCCCloud = AccessProviderType("ctcccloud") // 天翼云(预留)
AccessProviderTypeCUCCCloud = AccessProviderType("cucccloud") // 联通云(预留)
AccessProviderTypeDeSEC = AccessProviderType("desec")
+ AccessProviderTypeDigitalOcean = AccessProviderType("digitalocean")
AccessProviderTypeDingTalkBot = AccessProviderType("dingtalkbot")
AccessProviderTypeDNSLA = AccessProviderType("dnsla")
AccessProviderTypeDogeCloud = AccessProviderType("dogecloud")
@@ -127,6 +128,7 @@ const (
ACMEDns01ProviderTypeClouDNS = ACMEDns01ProviderType(AccessProviderTypeClouDNS)
ACMEDns01ProviderTypeCMCCCloud = ACMEDns01ProviderType(AccessProviderTypeCMCCCloud)
ACMEDns01ProviderTypeDeSEC = ACMEDns01ProviderType(AccessProviderTypeDeSEC)
+ ACMEDns01ProviderTypeDigitalOcean = ACMEDns01ProviderType(AccessProviderTypeDigitalOcean)
ACMEDns01ProviderTypeDNSLA = ACMEDns01ProviderType(AccessProviderTypeDNSLA)
ACMEDns01ProviderTypeDynv6 = ACMEDns01ProviderType(AccessProviderTypeDynv6)
ACMEDns01ProviderTypeGcore = ACMEDns01ProviderType(AccessProviderTypeGcore)
diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/digitalocean/digitalocean.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/digitalocean/digitalocean.go
new file mode 100644
index 00000000..0e3cb358
--- /dev/null
+++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/digitalocean/digitalocean.go
@@ -0,0 +1,36 @@
+package namedotcom
+
+import (
+ "time"
+
+ "github.com/go-acme/lego/v4/challenge"
+ "github.com/go-acme/lego/v4/providers/dns/digitalocean"
+)
+
+type ChallengeProviderConfig struct {
+ AccessToken string `json:"accessToken"`
+ 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 := digitalocean.NewDefaultConfig()
+ providerConfig.AuthToken = config.AccessToken
+ if config.DnsPropagationTimeout != 0 {
+ providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
+ }
+ if config.DnsTTL != 0 {
+ providerConfig.TTL = int(config.DnsTTL)
+ }
+
+ provider, err := digitalocean.NewDNSProviderConfig(providerConfig)
+ if err != nil {
+ return nil, err
+ }
+
+ return provider, nil
+}
diff --git a/ui/public/imgs/providers/digitalocean.svg b/ui/public/imgs/providers/digitalocean.svg
new file mode 100644
index 00000000..94b90bf9
--- /dev/null
+++ b/ui/public/imgs/providers/digitalocean.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 6f4d5086..4fd547e8 100644
--- a/ui/src/components/access/AccessForm.tsx
+++ b/ui/src/components/access/AccessForm.tsx
@@ -29,6 +29,7 @@ import AccessFormCloudflareConfig from "./AccessFormCloudflareConfig";
import AccessFormClouDNSConfig from "./AccessFormClouDNSConfig";
import AccessFormCMCCCloudConfig from "./AccessFormCMCCCloudConfig";
import AccessFormDeSECConfig from "./AccessFormDeSECConfig";
+import AccessFormDigitalOceanConfig from "./AccessFormDigitalOceanConfig";
import AccessFormDingTalkBotConfig from "./AccessFormDingTalkBotConfig";
import AccessFormDNSLAConfig from "./AccessFormDNSLAConfig";
import AccessFormDogeCloudConfig from "./AccessFormDogeCloudConfig";
@@ -216,6 +217,8 @@ const AccessForm = forwardRef(({ className,
return ;
case ACCESS_PROVIDERS.DESEC:
return ;
+ case ACCESS_PROVIDERS.DIGITALOCEAN:
+ return ;
case ACCESS_PROVIDERS.DINGTALKBOT:
return ;
case ACCESS_PROVIDERS.DNSLA:
diff --git a/ui/src/components/access/AccessFormDigitalOceanConfig.tsx b/ui/src/components/access/AccessFormDigitalOceanConfig.tsx
new file mode 100644
index 00000000..f4aafc4f
--- /dev/null
+++ b/ui/src/components/access/AccessFormDigitalOceanConfig.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 AccessConfigForDigitalOcean } from "@/domain/access";
+
+type AccessFormDigitalOceanConfigFieldValues = Nullish;
+
+export type AccessFormDigitalOceanConfigProps = {
+ form: FormInstance;
+ formName: string;
+ disabled?: boolean;
+ initialValues?: AccessFormDigitalOceanConfigFieldValues;
+ onValuesChange?: (values: AccessFormDigitalOceanConfigFieldValues) => void;
+};
+
+const initFormModel = (): AccessFormDigitalOceanConfigFieldValues => {
+ return {
+ accessToken: "",
+ };
+};
+
+const AccessFormDigitalOceanConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormDigitalOceanConfigProps) => {
+ const { t } = useTranslation();
+
+ const formSchema = z.object({
+ accessToken: z.string().nonempty(t("access.form.digitalocean_access_token.placeholder")).trim(),
+ });
+ const formRule = createSchemaFieldRule(formSchema);
+
+ const handleFormChange = (_: unknown, values: z.infer) => {
+ onValuesChange?.(values);
+ };
+
+ return (
+ }
+ >
+
+
+
+ );
+};
+
+export default AccessFormDigitalOceanConfig;
diff --git a/ui/src/domain/access.ts b/ui/src/domain/access.ts
index 1576fd67..32a1b128 100644
--- a/ui/src/domain/access.ts
+++ b/ui/src/domain/access.ts
@@ -170,6 +170,10 @@ export type AccessConfigForDeSEC = {
token: string;
};
+export type AccessConfigForDigitalOcean = {
+ accessToken: string;
+};
+
export type AccessConfigForDingTalkBot = {
webhookUrl: string;
secret?: string;
diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts
index 712530d7..6d1ad303 100644
--- a/ui/src/domain/provider.ts
+++ b/ui/src/domain/provider.ts
@@ -23,6 +23,7 @@ export const ACCESS_PROVIDERS = Object.freeze({
CLOUDNS: "cloudns",
CMCCCLOUD: "cmcccloud",
DESEC: "desec",
+ DIGITALOCEAN: "digitalocean",
DINGTALKBOT: "dingtalkbot",
DNSLA: "dnsla",
DOGECLOUD: "dogecloud",
@@ -139,6 +140,7 @@ export const accessProvidersMap: Maphttps://desec.readthedocs.io/en/latest/auth/tokens.html",
+ "access.form.digitalocean_access_token.label": "DigitalOcean access token",
+ "access.form.digitalocean_access_token.placeholder": "Please enter DigitalOcean access token",
+ "access.form.digitalocean_access_token.tooltip": "For more information, see https://docs.digitalocean.com/reference/api/create-personal-access-token/",
"access.form.dingtalkbot_webhook_url.label": "DingTalk bot Webhook URL",
"access.form.dingtalkbot_webhook_url.placeholder": "Please enter DingTalk bot Webhook URL",
"access.form.dingtalkbot_webhook_url.tooltip": "For more information, see https://open.dingtalk.com/document/orgapp/obtain-the-webhook-address-of-a-custom-robot",
diff --git a/ui/src/i18n/locales/en/nls.provider.json b/ui/src/i18n/locales/en/nls.provider.json
index e9122149..c623bb27 100644
--- a/ui/src/i18n/locales/en/nls.provider.json
+++ b/ui/src/i18n/locales/en/nls.provider.json
@@ -58,6 +58,7 @@
"provider.ctcccloud": "China Telecom Cloud (State Cloud)",
"provider.cucccloud": "China Unicom Cloud",
"provider.desec": "deSEC",
+ "provider.digitalocean": "DigitalOcean",
"provider.dingtalkbot": "DingTalk Bot",
"provider.dnsla": "DNS.LA",
"provider.dogecloud": "Doge Cloud",
diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json
index e2cacc1f..f3a39ec2 100644
--- a/ui/src/i18n/locales/zh/nls.access.json
+++ b/ui/src/i18n/locales/zh/nls.access.json
@@ -146,6 +146,9 @@
"access.form.desec_token.label": "deSEC Token",
"access.form.desec_token.placeholder": "请输入 deSEC Token",
"access.form.desec_token.tooltip": "这是什么?请参阅 https://desec.readthedocs.io/en/latest/auth/tokens.html",
+ "access.form.digitalocean_access_token.label": "DigitalOcean Access Token",
+ "access.form.digitalocean_access_token.placeholder": "请输入 DigitalOcean Access Token",
+ "access.form.digitalocean_access_token.tooltip": "这是什么?请参阅 https://docs.digitalocean.com/reference/api/create-personal-access-token/",
"access.form.dingtalkbot_webhook_url.label": "钉钉群机器人 Webhook 地址",
"access.form.dingtalkbot_webhook_url.placeholder": "请输入钉钉群机器人 Webhook 地址",
"access.form.dingtalkbot_webhook_url.tooltip": "这是什么?请参阅 https://open.dingtalk.com/document/orgapp/obtain-the-webhook-address-of-a-custom-robot",
diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json
index a6b94779..a68a1a09 100644
--- a/ui/src/i18n/locales/zh/nls.provider.json
+++ b/ui/src/i18n/locales/zh/nls.provider.json
@@ -58,6 +58,7 @@
"provider.ctcccloud": "联通云",
"provider.cucccloud": "天翼云",
"provider.desec": "deSEC",
+ "provider.digitalocean": "DigitalOcean",
"provider.dingtalkbot": "钉钉群机器人",
"provider.dnsla": "DNS.LA",
"provider.dogecloud": "多吉云",