diff --git a/go.mod b/go.mod
index 23063186..0140ddc8 100644
--- a/go.mod
+++ b/go.mod
@@ -35,6 +35,7 @@ require (
github.com/blinkbean/dingtalk v1.1.3
github.com/byteplus-sdk/byteplus-sdk-golang v1.0.56
github.com/go-acme/lego/v4 v4.25.2
+ github.com/go-cmd/cmd v1.4.3
github.com/go-lark/lark v1.16.0
github.com/go-resty/resty/v2 v2.16.5
github.com/go-viper/mapstructure/v2 v2.4.0
@@ -65,6 +66,7 @@ require (
github.com/volcengine/ve-tos-golang-sdk/v2 v2.7.21
github.com/volcengine/volc-sdk-golang v1.0.219
github.com/volcengine/volcengine-go-sdk v1.1.30
+ github.com/xhit/go-str2duration/v2 v2.1.0
gitlab.ecloud.com/ecloud/ecloudsdkclouddns v1.0.1
gitlab.ecloud.com/ecloud/ecloudsdkcore v1.0.0
golang.org/x/crypto v0.41.0
@@ -97,7 +99,6 @@ require (
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-acme/alidns-20150109/v4 v4.5.10 // indirect
github.com/go-acme/tencentclouddnspod v1.0.1208 // indirect
- github.com/go-cmd/cmd v1.4.3 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
@@ -127,6 +128,7 @@ require (
github.com/namedotcom/go/v4 v4.0.2 // indirect
github.com/nrdcg/bunny-go v0.0.0-20250327222614-988a091fc7ea // indirect
github.com/nrdcg/desec v0.11.0 // indirect
+ github.com/nrdcg/goacmedns v0.2.0 // indirect
github.com/nrdcg/mailinabox v0.2.0 // indirect
github.com/nrdcg/porkbun v0.4.0 // indirect
github.com/peterhellberg/link v1.2.0 // indirect
@@ -206,7 +208,7 @@ require (
github.com/nrdcg/namesilo v0.2.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/spf13/cast v1.9.2 // indirect
- github.com/spf13/cobra v1.9.1 // indirect
+ github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.9
github.com/tjfoc/gmsm v1.4.1 // indirect
golang.org/x/image v0.29.0 // indirect
@@ -217,7 +219,7 @@ require (
golang.org/x/sys v0.35.0 // indirect
golang.org/x/term v0.34.0 // indirect
golang.org/x/text v0.28.0 // indirect
- golang.org/x/time v0.12.0
+ golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.36.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
diff --git a/go.sum b/go.sum
index e925f0e7..74227562 100644
--- a/go.sum
+++ b/go.sum
@@ -404,9 +404,8 @@ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8Wd
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
-github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
-github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
+github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw=
@@ -691,6 +690,8 @@ github.com/nrdcg/bunny-go v0.0.0-20250327222614-988a091fc7ea h1:OSgRS4kqOs/WuxuF
github.com/nrdcg/bunny-go v0.0.0-20250327222614-988a091fc7ea/go.mod h1:IDRRngAngb2eTEaWgpO0hukQFI/vJId46fT1KErMytA=
github.com/nrdcg/desec v0.11.0 h1:XZVHy07sg12y8FozMp+l7XvzPsdzog0AYXuQMaHBsfs=
github.com/nrdcg/desec v0.11.0/go.mod h1:5+4vyhMRTs49V9CNoODF/HwT8Mwxv9DJ6j+7NekUnBs=
+github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0=
+github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg=
github.com/nrdcg/mailinabox v0.2.0 h1:IKq8mfKiVwNW2hQii/ng1dJ4yYMMv3HAP3fMFIq2CFk=
github.com/nrdcg/mailinabox v0.2.0/go.mod h1:0yxqeYOiGyxAu7Sb94eMxHPIOsPYXAjTeA9ZhePhGnc=
github.com/nrdcg/namesilo v0.2.1 h1:kLjCjsufdW/IlC+iSfAqj0iQGgKjlbUUeDJio5Y6eMg=
@@ -893,6 +894,8 @@ github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
+github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
+github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
diff --git a/internal/certapply/applicators/sp_acmedns.go b/internal/certapply/applicators/sp_acmedns.go
new file mode 100644
index 00000000..2a9ca423
--- /dev/null
+++ b/internal/certapply/applicators/sp_acmedns.go
@@ -0,0 +1,28 @@
+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/acmedns"
+ xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
+)
+
+func init() {
+ if err := ACMEDns01Registries.Register(domain.ACMEDns01ProviderTypeACMEDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) {
+ credentials := domain.AccessConfigForACMEDNS{}
+ if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {
+ return nil, fmt.Errorf("failed to populate provider access config: %w", err)
+ }
+
+ provider, err := acmedns.NewChallengeProvider(&acmedns.ChallengeProviderConfig{
+ ServerUrl: credentials.ServerUrl,
+ Credentials: credentials.Credentials,
+ })
+ return provider, err
+ }); err != nil {
+ panic(err)
+ }
+}
diff --git a/internal/certapply/client_certifier.go b/internal/certapply/client_certifier.go
index e547aa5f..30c1574e 100644
--- a/internal/certapply/client_certifier.go
+++ b/internal/certapply/client_certifier.go
@@ -22,8 +22,9 @@ import (
)
type ObtainCertificateRequest struct {
- Domains []string
- KeyType certcrypto.KeyType
+ Domains []string
+ KeyType certcrypto.KeyType
+ ValidityTo time.Time
// 提供商相关
ChallengeType string
@@ -153,6 +154,7 @@ func (c *ACMEClient) ObtainCertificate(request *ObtainCertificateRequest) (*Obta
Domains: request.Domains,
Bundle: true,
Profile: request.ACMEProfile,
+ NotAfter: request.ValidityTo,
ReplacesCertID: lo.If(request.ARIReplacesAcctUrl == c.account.ACMEAcctUrl, request.ARIReplacesCertId).Else(""),
}
resp, err := c.client.Certificate.Obtain(req)
diff --git a/internal/domain/access.go b/internal/domain/access.go
index de11b2af..d43462ed 100644
--- a/internal/domain/access.go
+++ b/internal/domain/access.go
@@ -32,6 +32,11 @@ type AccessConfigForACMECA struct {
Endpoint string `json:"endpoint"`
}
+type AccessConfigForACMEDNS struct {
+ ServerUrl string `json:"serverUrl"`
+ Credentials string `json:"credentials"`
+}
+
type AccessConfigForACMEHttpReq struct {
Endpoint string `json:"endpoint"`
Mode string `json:"mode,omitempty"`
diff --git a/internal/domain/provider.go b/internal/domain/provider.go
index 4da89030..c7f32c2c 100644
--- a/internal/domain/provider.go
+++ b/internal/domain/provider.go
@@ -11,6 +11,7 @@ NOTICE: If you add new constant, please keep ASCII order.
const (
AccessProviderType1Panel = AccessProviderType("1panel")
AccessProviderTypeACMECA = AccessProviderType("acmeca")
+ AccessProviderTypeACMEDNS = AccessProviderType("acmedns")
AccessProviderTypeACMEHttpReq = AccessProviderType("acmehttpreq")
AccessProviderTypeAkamai = AccessProviderType("akamai") // Akamai(预留)
AccessProviderTypeAliyun = AccessProviderType("aliyun")
@@ -121,6 +122,7 @@ ACME DNS-01 提供商常量值。
NOTICE: If you add new constant, please keep ASCII order.
*/
const (
+ ACMEDns01ProviderTypeACMEDNS = ACMEDns01ProviderType(AccessProviderTypeACMEDNS)
ACMEDns01ProviderTypeACMEHttpReq = ACMEDns01ProviderType(AccessProviderTypeACMEHttpReq)
ACMEDns01ProviderTypeAliyun = ACMEDns01ProviderType(AccessProviderTypeAliyun) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAliyunDNS]
ACMEDns01ProviderTypeAliyunDNS = ACMEDns01ProviderType(AccessProviderTypeAliyun + "-dns")
diff --git a/internal/domain/workflow.go b/internal/domain/workflow.go
index d3b48432..f5b1f13b 100644
--- a/internal/domain/workflow.go
+++ b/internal/domain/workflow.go
@@ -150,6 +150,7 @@ func (c WorkflowNodeConfig) AsBizApply() WorkflowNodeConfigForBizApply {
CAProvider: xmaps.GetString(c, "caProvider"),
CAProviderAccessId: xmaps.GetString(c, "caProviderAccessId"),
CAProviderConfig: xmaps.GetKVMapAny(c, "caProviderConfig"),
+ ValidityLifetime: xmaps.GetString(c, "validityLifetime"),
ACMEProfile: xmaps.GetString(c, "acmeProfile"),
Nameservers: nameservers,
DnsPropagationWait: xmaps.GetInt32(c, "dnsPropagationWait"),
@@ -216,10 +217,11 @@ type WorkflowNodeConfigForBizApply struct {
Provider string `json:"provider"` // 质询提供商
ProviderAccessId string `json:"providerAccessId"` // 质询提供商授权记录 ID
ProviderConfig map[string]any `json:"providerConfig,omitempty"` // 质询提供商额外配置
- KeyAlgorithm string `json:"keyAlgorithm,omitempty"` // 证书算法
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,omitempty"` // 证书算法
+ ValidityLifetime string `json:"validityLifetime,omitempty"` // 证书有效期,形如 "30d"、"6h"
ACMEProfile string `json:"acmeProfile,omitempty"` // ACME Profiles Extension
Nameservers []string `json:"nameservers,omitempty"` // DNS 服务器列表,以半角分号分隔
DnsPropagationWait int32 `json:"dnsPropagationWait,omitempty"` // DNS 传播等待时间,等同于 lego 的 `--dns-propagation-wait` 参数
diff --git a/internal/tools/mproc/sender.go b/internal/tools/mproc/sender.go
index 822be69c..2a2f3add 100644
--- a/internal/tools/mproc/sender.go
+++ b/internal/tools/mproc/sender.go
@@ -41,7 +41,7 @@ func (s *sender[TIn, TOut]) SendWithContext(ctx context.Context, params *TIn) (*
aesCryptor := xcrypto.NewAESCryptor(aesKey)
// 准备临时输入文件
- tempIn, err := os.CreateTemp("", "certimate_mprocin_*.tmp")
+ tempIn, err := os.CreateTemp("", "certimate.mprocin_*.tmp")
if err != nil {
return nil, fmt.Errorf("failed to create temp input file: %w", err)
} else {
@@ -64,7 +64,7 @@ func (s *sender[TIn, TOut]) SendWithContext(ctx context.Context, params *TIn) (*
defer os.Remove(tempIn.Name())
// 准备临时输出文件
- tempOut, err := os.CreateTemp("", "certimate_mprocout_*.tmp")
+ tempOut, err := os.CreateTemp("", "certimate.mprocout_*.tmp")
if err != nil {
return nil, fmt.Errorf("failed to create temp output file: %w", err)
} else {
@@ -73,7 +73,7 @@ func (s *sender[TIn, TOut]) SendWithContext(ctx context.Context, params *TIn) (*
defer os.Remove(tempOut.Name())
// 准备临时错误文件
- tempErr, err := os.CreateTemp("", "certimate_mprocerr_*.tmp")
+ tempErr, err := os.CreateTemp("", "certimate.mprocerr_*.tmp")
if err != nil {
return nil, fmt.Errorf("failed to create temp error file: %w", err)
} else {
diff --git a/internal/workflow/engine/executor_bizapply.go b/internal/workflow/engine/executor_bizapply.go
index 68f107e2..a69d09ed 100644
--- a/internal/workflow/engine/executor_bizapply.go
+++ b/internal/workflow/engine/executor_bizapply.go
@@ -15,6 +15,7 @@ import (
"github.com/go-acme/lego/v4/lego"
legolog "github.com/go-acme/lego/v4/log"
"github.com/samber/lo"
+ "github.com/xhit/go-str2duration/v2"
"github.com/certimate-go/certimate/internal/app"
"github.com/certimate-go/certimate/internal/certapply"
@@ -261,7 +262,15 @@ func (ne *bizApplyNodeExecutor) executeObtain(execCtx *NodeExecutionContext, nod
DnsPropagationTimeout: nodeCfg.DnsPropagationTimeout,
DnsTTL: nodeCfg.DnsTTL,
HttpDelayWait: nodeCfg.HttpDelayWait,
- ACMEProfile: nodeCfg.ACMEProfile,
+ ValidityTo: lo.If(nodeCfg.ValidityLifetime == "", time.Time{}).
+ ElseF(func() time.Time {
+ duration, err := str2duration.ParseDuration(nodeCfg.ValidityLifetime)
+ if err != nil {
+ return time.Time{}
+ }
+ return time.Now().Add(duration)
+ }),
+ ACMEProfile: nodeCfg.ACMEProfile,
ARIReplacesAcctUrl: lo.If(lastCertificate == nil, "").
ElseF(func() string {
if lastCertificate.ACMERenewed {
diff --git a/pkg/core/ssl-applicator/acme-dns01/providers/acmedns/acmedns.go b/pkg/core/ssl-applicator/acme-dns01/providers/acmedns/acmedns.go
new file mode 100644
index 00000000..33f95652
--- /dev/null
+++ b/pkg/core/ssl-applicator/acme-dns01/providers/acmedns/acmedns.go
@@ -0,0 +1,44 @@
+package acmedns
+
+import (
+ "errors"
+ "fmt"
+ "os"
+
+ "github.com/go-acme/lego/v4/providers/dns/acmedns"
+
+ "github.com/certimate-go/certimate/pkg/core"
+)
+
+type ChallengeProviderConfig struct {
+ ServerUrl string `json:"serverUrl"`
+ Credentials string `json:"credentials"`
+}
+
+func NewChallengeProvider(config *ChallengeProviderConfig) (core.ACMEChallenger, error) {
+ if config == nil {
+ return nil, errors.New("the configuration of the acme challenge provider is nil")
+ }
+
+ tempfile, err := os.CreateTemp("", "certimate.acmedns_*.tmp")
+ if err != nil {
+ return nil, fmt.Errorf("failed to create temp credentials file: %w", err)
+ } else {
+ if _, err := tempfile.Write([]byte(config.Credentials)); err != nil {
+ return nil, fmt.Errorf("failed to write temp credentials file: %w", err)
+ }
+
+ tempfile.Close()
+ }
+
+ providerConfig := acmedns.NewDefaultConfig()
+ providerConfig.APIBase = config.ServerUrl
+ providerConfig.StoragePath = tempfile.Name()
+
+ provider, err := acmedns.NewDNSProviderConfig(providerConfig)
+ if err != nil {
+ return nil, err
+ }
+
+ return provider, nil
+}
diff --git a/ui/public/imgs/providers/acmedns.png b/ui/public/imgs/providers/acmedns.png
new file mode 100644
index 00000000..9d0f5a9f
Binary files /dev/null and b/ui/public/imgs/providers/acmedns.png differ
diff --git a/ui/src/components/access/AccessForm.tsx b/ui/src/components/access/AccessForm.tsx
index a339ed3f..742782a8 100644
--- a/ui/src/components/access/AccessForm.tsx
+++ b/ui/src/components/access/AccessForm.tsx
@@ -13,6 +13,7 @@ import { FormNestedFieldsContextProvider } from "./forms/_context";
import { useProviderFilterByUsage } from "./forms/_hooks";
import AccessConfigFieldsProvider1Panel from "./forms/AccessConfigFieldsProvider1Panel";
import AccessConfigFieldsProviderACMECA from "./forms/AccessConfigFieldsProviderACMECA";
+import AccessConfigFieldsProviderACMEDNS from "./forms/AccessConfigFieldsProviderACMEDNS";
import AccessConfigFieldsProviderACMEHttpReq from "./forms/AccessConfigFieldsProviderACMEHttpReq";
import AccessConfigFieldsProviderAliyun from "./forms/AccessConfigFieldsProviderAliyun";
import AccessConfigFieldsProviderAPISIX from "./forms/AccessConfigFieldsProviderAPISIX";
@@ -133,6 +134,9 @@ const AccessForm = ({ className, style, disabled, initialValues, mode, usage, ..
case ACCESS_PROVIDERS.ACMECA: {
return ;
}
+ case ACCESS_PROVIDERS.ACMEDNS: {
+ return ;
+ }
case ACCESS_PROVIDERS.ACMEHTTPREQ: {
return ;
}
diff --git a/ui/src/components/access/forms/AccessConfigFieldsProvider1Panel.tsx b/ui/src/components/access/forms/AccessConfigFieldsProvider1Panel.tsx
index c2e79e45..6441e14e 100644
--- a/ui/src/components/access/forms/AccessConfigFieldsProvider1Panel.tsx
+++ b/ui/src/components/access/forms/AccessConfigFieldsProvider1Panel.tsx
@@ -21,6 +21,7 @@ const AccessConfigFormFieldsProvider1Panel = () => {
name={[parentNamePath, "serverUrl"]}
initialValue={initialValues.serverUrl}
label={t("access.form.1panel_server_url.label")}
+ extra={t("access.form.1panel_server_url.help")}
rules={[formRule]}
>
diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderACMEDNS.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderACMEDNS.tsx
new file mode 100644
index 00000000..c26cd012
--- /dev/null
+++ b/ui/src/components/access/forms/AccessConfigFieldsProviderACMEDNS.tsx
@@ -0,0 +1,77 @@
+import { getI18n, useTranslation } from "react-i18next";
+import { Form, Input } from "antd";
+import { createSchemaFieldRule } from "antd-zod";
+import { z } from "zod/v4";
+
+import TextFileInput from "@/components/TextFileInput";
+
+import { useFormNestedFieldsContext } from "./_context";
+
+const AccessConfigFieldsProviderACMEDNS = () => {
+ 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 {
+ serverUrl: "https://auth.acme-dns.io/",
+ credentials: "",
+ };
+};
+
+const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
+ const { t } = i18n;
+
+ return z.object({
+ serverUrl: z.url(t("common.errmsg.url_invalid")),
+ credentials: z
+ .string()
+ .max(20480, t("common.errmsg.string_max", { max: 20480 }))
+ .refine((v) => {
+ if (!v) return false;
+
+ try {
+ const obj = JSON.parse(v);
+ return typeof obj === "object" && !Array.isArray(obj);
+ } catch {
+ return false;
+ }
+ }, t("access.form.acmedns_credentials.errmsg.json_invalid")),
+ });
+};
+
+const _default = Object.assign(AccessConfigFieldsProviderACMEDNS, {
+ getInitialValues,
+ getSchema,
+});
+
+export default _default;
diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderBaotaPanel.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderBaotaPanel.tsx
index c5eca4b5..67802dd6 100644
--- a/ui/src/components/access/forms/AccessConfigFieldsProviderBaotaPanel.tsx
+++ b/ui/src/components/access/forms/AccessConfigFieldsProviderBaotaPanel.tsx
@@ -21,6 +21,7 @@ const AccessConfigFormFieldsProviderBaotaPanel = () => {
name={[parentNamePath, "serverUrl"]}
initialValue={initialValues.serverUrl}
label={t("access.form.baotapanel_server_url.label")}
+ extra={t("access.form.baotapanel_server_url.help")}
rules={[formRule]}
>
diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderBaotaWAF.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderBaotaWAF.tsx
index dc4cd422..980534fd 100644
--- a/ui/src/components/access/forms/AccessConfigFieldsProviderBaotaWAF.tsx
+++ b/ui/src/components/access/forms/AccessConfigFieldsProviderBaotaWAF.tsx
@@ -21,6 +21,7 @@ const AccessConfigFormFieldsProviderBaotaWAF = () => {
name={[parentNamePath, "serverUrl"]}
initialValue={initialValues.serverUrl}
label={t("access.form.baotawaf_server_url.label")}
+ extra={t("access.form.baotawaf_server_url.help")}
rules={[formRule]}
>
diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderRatPanel.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderRatPanel.tsx
index 09c4d156..675617b4 100644
--- a/ui/src/components/access/forms/AccessConfigFieldsProviderRatPanel.tsx
+++ b/ui/src/components/access/forms/AccessConfigFieldsProviderRatPanel.tsx
@@ -21,6 +21,7 @@ const AccessConfigFormFieldsProviderRatPanel = () => {
name={[parentNamePath, "serverUrl"]}
initialValue={initialValues.serverUrl}
label={t("access.form.ratpanel_server_url.label")}
+ extra={t("access.form.ratpanel_server_url.help")}
rules={[formRule]}
>
diff --git a/ui/src/components/access/forms/AccessConfigFormProvider1Panel.tsx b/ui/src/components/access/forms/AccessConfigFormProvider1Panel.tsx
deleted file mode 100644
index c2e79e45..00000000
--- a/ui/src/components/access/forms/AccessConfigFormProvider1Panel.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-import { getI18n, useTranslation } from "react-i18next";
-import { Form, Input, Select, Switch } from "antd";
-import { createSchemaFieldRule } from "antd-zod";
-import { z } from "zod";
-
-import { useFormNestedFieldsContext } from "./_context";
-
-const AccessConfigFormFieldsProvider1Panel = () => {
- 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 {
- serverUrl: "http://:20410/",
- apiVersion: "v1",
- apiKey: "",
- };
-};
-
-const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => {
- const { t } = i18n;
-
- return z.object({
- serverUrl: z.url(t("common.errmsg.url_invalid")),
- apiVersion: z.string().nonempty(t("access.form.1panel_api_version.placeholder")),
- apiKey: z
- .string()
- .min(1, t("access.form.1panel_api_key.placeholder"))
- .max(64, t("common.errmsg.string_max", { max: 64 })),
- allowInsecureConnections: z.boolean().nullish(),
- });
-};
-
-const _default = Object.assign(AccessConfigFormFieldsProvider1Panel, {
- getInitialValues,
- getSchema,
-});
-
-export default _default;
diff --git a/ui/src/components/workflow/designer/forms/BizApplyNodeConfigForm.tsx b/ui/src/components/workflow/designer/forms/BizApplyNodeConfigForm.tsx
index 69922e02..423d96f6 100644
--- a/ui/src/components/workflow/designer/forms/BizApplyNodeConfigForm.tsx
+++ b/ui/src/components/workflow/designer/forms/BizApplyNodeConfigForm.tsx
@@ -4,7 +4,7 @@ import { Link } from "react-router";
import { type FlowNodeEntity, getNodeForm } from "@flowgram.ai/fixed-layout-editor";
import { IconChevronRight, IconCircleMinus, IconPlus } from "@tabler/icons-react";
import { useControllableValue, useMount } from "ahooks";
-import { type AnchorProps, AutoComplete, Button, Divider, Flex, Form, type FormInstance, Input, InputNumber, Select, Switch, Typography } from "antd";
+import { type AnchorProps, AutoComplete, Button, Divider, Flex, Form, type FormInstance, Input, InputNumber, Select, Space, Switch, Typography } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
@@ -317,9 +317,20 @@ const BizApplyNodeConfigForm = ({ node, ...props }: BizApplyNodeConfigFormProps)
+ }
+ >
+
+
+
}
>
@@ -526,6 +537,82 @@ const InternalEmailInput = memo(
}
);
+const InternalValidityLifetimeInput = memo(
+ ({ disabled, placeholder, ...props }: { disabled?: boolean; placeholder?: string; value?: string; onChange?: (value: string) => void }) => {
+ const { t } = useTranslation();
+
+ const [value, setValue] = useControllableValue(props, {
+ valuePropName: "value",
+ defaultValuePropName: "defaultValue",
+ trigger: "onChange",
+ });
+
+ const parseCombinedValue = (val: string): [string | undefined, string | undefined] => {
+ const match = String(val).match(/^(\d+)([a-zA-Z]+)$/);
+ if (match) {
+ return [match[1], match[2]];
+ }
+
+ return [undefined, undefined];
+ };
+
+ const [inputValue, setInputValue] = useState(parseCombinedValue(value)[0]);
+ const [selectValue, setSelectValue] = useState(parseCombinedValue(value)[1] || "d");
+ useEffect(() => {
+ const [v, u] = parseCombinedValue(value);
+ setInputValue(v);
+ setSelectValue(u || "d");
+ }, [value]);
+
+ const handleInputClear = () => {
+ setValue("");
+ };
+
+ const handleInputChange = (e: React.ChangeEvent) => {
+ setInputValue(e.currentTarget.value);
+
+ if (e.currentTarget.value) {
+ setValue(`${e.currentTarget.value}${selectValue}`);
+ } else {
+ setValue("");
+ }
+ };
+
+ const handleSelectChange = (value: string) => {
+ setSelectValue(value);
+
+ if (inputValue) {
+ setValue(`${inputValue}${value}`);
+ }
+ };
+
+ return (
+
+
+
+
+
+ );
+ }
+);
+
const getAnchorItems = ({ i18n = getI18n() }: { i18n?: ReturnType }): Required["items"] => {
const { t } = i18n;
@@ -595,6 +682,13 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType })
(v) => (v == null || v === "" ? void 0 : Number(v)),
z.number().int(t("workflow_node.apply.form.dns_ttl.placeholder")).gte(1, t("workflow_node.apply.form.dns_ttl.placeholder")).nullish()
),
+ validityLifetime: z
+ .string()
+ .nullish()
+ .refine((v) => {
+ if (!v) return true;
+ return /^\d+[d|h]$/.test(v) && parseInt(v) > 0;
+ }, t("workflow_node.apply.form.validity_lifetime.placeholder")),
acmeProfile: z.string().nullish(),
disableFollowCNAME: z.boolean().nullish(),
disableARI: z.boolean().nullish(),
diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts
index ea643a75..13bd180d 100644
--- a/ui/src/domain/provider.ts
+++ b/ui/src/domain/provider.ts
@@ -6,6 +6,7 @@
export const ACCESS_PROVIDERS = Object.freeze({
["1PANEL"]: "1panel",
ACMECA: "acmeca",
+ ACMEDNS: "acmedns",
ACMEHTTPREQ: "acmehttpreq",
ALIYUN: "aliyun",
APISIX: "apisix",
@@ -173,6 +174,7 @@ export const accessProvidersMap: Map = new
NOTICE: If you add new constant, please keep ASCII order.
*/
export const ACME_DNS01_PROVIDERS = Object.freeze({
+ ACMEDNS: `${ACCESS_PROVIDERS.ACMEDNS}`,
ACMEHTTPREQ: `${ACCESS_PROVIDERS.ACMEHTTPREQ}`,
ALIYUN: `${ACCESS_PROVIDERS.ALIYUN}`, // 兼容旧值,等同于 `ALIYUN_DNS`
ALIYUN_DNS: `${ACCESS_PROVIDERS.ALIYUN}-dns`,
@@ -369,6 +372,7 @@ export const acmeDns01ProvidersMap: Map
).map(([type, name]) => [
diff --git a/ui/src/domain/workflow.ts b/ui/src/domain/workflow.ts
index ec70fa20..7c04bd7f 100644
--- a/ui/src/domain/workflow.ts
+++ b/ui/src/domain/workflow.ts
@@ -102,6 +102,7 @@ export type WorkflowNodeConfigForBizApply = {
caProviderAccessId?: string;
caProviderConfig?: Record;
keyAlgorithm: string;
+ validityLifetime?: string;
acmeProfile?: string;
nameservers?: string;
dnsPropagationTimeout?: number;
diff --git a/ui/src/i18n/locales/en/nls.access.json b/ui/src/i18n/locales/en/nls.access.json
index 2b2dd784..f2ae220d 100644
--- a/ui/src/i18n/locales/en/nls.access.json
+++ b/ui/src/i18n/locales/en/nls.access.json
@@ -44,6 +44,7 @@
"access.form.provider.search.placeholder": "Search provider ...",
"access.form.1panel_server_url.label": "1Panel server URL",
"access.form.1panel_server_url.placeholder": "Please enter 1Panel server URL",
+ "access.form.1panel_server_url.help": "Notes: DO NOT include the security entrance suffix.",
"access.form.1panel_api_version.label": "1Panel version",
"access.form.1panel_api_version.placeholder": "Please select 1Panel version",
"access.form.1panel_api_key.label": "1Panel API key",
@@ -59,6 +60,12 @@
"access.form.acmeca_eab_kid.placeholder": "Please enter ACME EAB KID",
"access.form.acmeca_eab_hmac_key.label": "ACME EAB HMAC key (Optional)",
"access.form.acmeca_eab_hmac_key.placeholder": "Please enter ACME EAB HMAC key",
+ "access.form.acmedns_server_url.label": "ACME-DNS server URL",
+ "access.form.acmedns_server_url.placeholder": "Please enter ACME-DNS server URL",
+ "access.form.acmedns_credentials.label": "ACME-DNS credentials",
+ "access.form.acmedns_credentials.placeholder": "Please enter ACME-DNS credentials",
+ "access.form.acmedns_credentials.tooltip": "For more information, see https://github.com/joohoi/acme-dns",
+ "access.form.acmedns_credentials.errmsg.json_invalid": "Please enter a valiod JSON string",
"access.form.acmehttpreq_endpoint.label": "Endpoint",
"access.form.acmehttpreq_endpoint.placeholder": "Please enter endpoint",
"access.form.acmehttpreq_endpoint.tooltip": "For more information, see https://go-acme.github.io/lego/dns/httpreq/",
@@ -116,11 +123,13 @@
"access.form.baishan_api_token.placeholder": "Please enter Baishan Cloud API token",
"access.form.baotapanel_server_url.label": "aaPanel server URL",
"access.form.baotapanel_server_url.placeholder": "Please enter aaPanel server URL",
+ "access.form.baotapanel_server_url.help": "Notes: DO NOT include the security entrance suffix.",
"access.form.baotapanel_api_key.label": "aaPanel API key",
"access.form.baotapanel_api_key.placeholder": "Please enter aaPanel API key",
"access.form.baotapanel_api_key.tooltip": "For more information, see https://www.bt.cn/bbs/thread-20376-1-1.html",
"access.form.baotawaf_server_url.label": "aaWAF server URL",
"access.form.baotawaf_server_url.placeholder": "Please enter aaWAF server URL",
+ "access.form.baotawaf_server_url.help": "Notes: DO NOT include the security entrance suffix.",
"access.form.baotawaf_api_key.label": "aaWAF API key",
"access.form.baotawaf_api_key.placeholder": "Please enter aaWAF API key",
"access.form.baotawaf_api_key.tooltip": "For more information, see https://github.com/aaPanel/aaWAF/blob/main/API.md",
@@ -387,6 +396,7 @@
"access.form.rainyun_api_key.tooltip": "For more information, see https://app.rainyun.com/account/settings/api-key",
"access.form.ratpanel_server_url.label": "RatPanel server URL",
"access.form.ratpanel_server_url.placeholder": "Please enter RatPanel server URL",
+ "access.form.ratpanel_server_url.help": "Notes: DO NOT include the security entrance suffix.",
"access.form.ratpanel_access_token_id.label": "RatPanel access token ID",
"access.form.ratpanel_access_token_id.placeholder": "Please enter RatPanel access token ID",
"access.form.ratpanel_access_token_id.tooltip": "For more information, see https://ratpanel.github.io/advanced/api.html",
diff --git a/ui/src/i18n/locales/en/nls.provider.json b/ui/src/i18n/locales/en/nls.provider.json
index 27dabd46..0607dbbf 100644
--- a/ui/src/i18n/locales/en/nls.provider.json
+++ b/ui/src/i18n/locales/en/nls.provider.json
@@ -3,6 +3,7 @@
"provider.1panel.console": "1Panel - Console itself",
"provider.1panel.site": "1Panel - Website",
"provider.acmeca": "ACME Custom CA Endpoint",
+ "provider.acmedns": "ACME-DNS",
"provider.acmehttpreq": "ACME Custom HTTP Endpoint",
"provider.aliyun": "Alibaba Cloud",
"provider.aliyun.alb": "Alibaba Cloud - ALB (Application Load Balancer)",
diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json
index 0d93192c..6971b368 100644
--- a/ui/src/i18n/locales/en/nls.workflow.nodes.json
+++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json
@@ -22,7 +22,7 @@
"workflow_node.apply.help": "Apply for SSL certificate issuance from the certificate authority.",
"workflow_node.apply.default_name": "Application",
"workflow_node.apply.form_anchor.parameters.tab": "Parameters",
- "workflow_node.apply.form_anchor.certificate.tab": "CA Config",
+ "workflow_node.apply.form_anchor.certificate.tab": "Certificate",
"workflow_node.apply.form_anchor.certificate.title": "Certificate settings",
"workflow_node.apply.form_anchor.advanced.tab": "Advanced",
"workflow_node.apply.form_anchor.advanced.title": "Advanced settings",
@@ -71,9 +71,16 @@
"workflow_node.apply.form.ca_provider_access.label": "Certificate authority credential",
"workflow_node.apply.form.ca_provider_access.placeholder": "Please select an credential of the certificate authority",
"workflow_node.apply.form.ca_provider_access.button": "Create",
- "workflow_node.apply.form.acme_profile.label": "ACME certificate profile (Optional)",
- "workflow_node.apply.form.acme_profile.placeholder": "Please enter ACME certificate profile",
- "workflow_node.apply.form.acme_profile.tooltip": "It determines the ACME profile which will be used to affect issuance of the certificate requested. If you don't understand this option, just keep it by default.
Click here to learn more.",
+ "workflow_node.apply.form.validity_lifetime.label": "Certificate validity lifetime (Optional)",
+ "workflow_node.apply.form.validity_lifetime.placeholder": "Please enter certificate's validity lifetime",
+ "workflow_node.apply.form.validity_lifetime.help": "Notes: Not all CAs support this feature.",
+ "workflow_node.apply.form.validity_lifetime.tooltip": "It determines the NotAfter field of the certificate in the ACME protocol. If you don't understand this option, just keep it by default.",
+ "workflow_node.apply.form.validity_lifetime.units.h": "Hour(s)",
+ "workflow_node.apply.form.validity_lifetime.units.d": "Day(s)",
+ "workflow_node.apply.form.acme_profile.label": "Certificate ACME profile (Optional)",
+ "workflow_node.apply.form.acme_profile.placeholder": "Please enter certificate's ACME profile",
+ "workflow_node.apply.form.acme_profile.help": "Notes: Not all CAs support this feature.",
+ "workflow_node.apply.form.acme_profile.tooltip": "It determines the Profile field of the certificate in the ACME protocol. If you don't understand this option, just keep it by default.
Click here to learn more.",
"workflow_node.apply.form.nameservers.label": "DNS recursive nameservers (Optional)",
"workflow_node.apply.form.nameservers.placeholder": "Please enter DNS recursive nameservers (separated by semicolons)",
"workflow_node.apply.form.nameservers.tooltip": "It determines whether to custom DNS recursive nameservers during ACME DNS-01 challenge. If you don't understand this option, just keep it by default.
Click here to learn more.",
diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json
index d8e5ad3b..9aef0305 100644
--- a/ui/src/i18n/locales/zh/nls.access.json
+++ b/ui/src/i18n/locales/zh/nls.access.json
@@ -46,6 +46,7 @@
"access.form.common_allow_insecure_conns.switch.off": "不允许",
"access.form.1panel_server_url.label": "1Panel 服务地址",
"access.form.1panel_server_url.placeholder": "请输入 1Panel 服务地址",
+ "access.form.1panel_server_url.help": "提示:请勿包含安全入口后缀。",
"access.form.1panel_api_version.label": "1Panel 版本",
"access.form.1panel_api_version.placeholder": "请选择 1Panel 版本",
"access.form.1panel_api_key.label": "1Panel 接口密钥",
@@ -58,6 +59,12 @@
"access.form.acmeca_eab_kid.placeholder": "请输入 ACME EAB KID",
"access.form.acmeca_eab_hmac_key.label": "ACME EAB HMAC Key(可选)",
"access.form.acmeca_eab_hmac_key.placeholder": "请输入 ACME EAB HMAC Key",
+ "access.form.acmedns_server_url.label": "ACME-DNS 服务地址",
+ "access.form.acmedns_server_url.placeholder": "请输入 ACME-DNS 服务地址",
+ "access.form.acmedns_credentials.label": "ACME-DNS 凭证文件",
+ "access.form.acmedns_credentials.placeholder": "请输入 ACME-DNS 凭证文件",
+ "access.form.acmedns_credentials.tooltip": "这是什么?请参阅 https://github.com/joohoi/acme-dns",
+ "access.form.acmedns_credentials.errmsg.json_invalid": "请输入有效的 JSON 格式字符串",
"access.form.acmehttpreq_endpoint.label": "服务端点",
"access.form.acmehttpreq_endpoint.placeholder": "请输入服务端点",
"access.form.acmehttpreq_endpoint.tooltip": "这是什么?请参阅 https://go-acme.github.io/lego/dns/httpreq/",
@@ -112,11 +119,13 @@
"access.form.baishan_api_token.placeholder": "请输入白山云 API Token",
"access.form.baotapanel_server_url.label": "宝塔面板服务地址",
"access.form.baotapanel_server_url.placeholder": "请输入宝塔面板服务地址",
+ "access.form.baotapanel_server_url.help": "提示:请勿包含安全入口后缀。",
"access.form.baotapanel_api_key.label": "宝塔面板接口密钥",
"access.form.baotapanel_api_key.placeholder": "请输入宝塔面板接口密钥",
"access.form.baotapanel_api_key.tooltip": "这是什么?请参阅 https://www.bt.cn/bbs/thread-113890-1-1.html",
"access.form.baotawaf_server_url.label": "堡塔云 WAF 服务地址",
"access.form.baotawaf_server_url.placeholder": "请输入堡塔云 WAF 服务地址",
+ "access.form.baotawaf_server_url.help": "提示:请勿包含安全入口后缀。",
"access.form.baotawaf_api_key.label": "堡塔云 WAF 接口密钥",
"access.form.baotawaf_api_key.placeholder": "请输入 堡塔云 WAF 接口密钥",
"access.form.baotawaf_api_key.tooltip": "这是什么?请参阅 https://github.com/aaPanel/aaWAF/blob/main/API.md",
@@ -386,6 +395,7 @@
"access.form.rainyun_api_key.tooltip": "这是什么?请参阅 https://app.rainyun.com/account/settings/api-key",
"access.form.ratpanel_server_url.label": "耗子面板服务地址",
"access.form.ratpanel_server_url.placeholder": "请输入耗子面板服务地址",
+ "access.form.ratpanel_server_url.help": "提示:请勿包含安全入口后缀。",
"access.form.ratpanel_access_token_id.label": "耗子面板 AccessToken ID",
"access.form.ratpanel_access_token_id.placeholder": "请输入耗子面板 AccessToken ID",
"access.form.ratpanel_access_token_id.tooltip": "这是什么?请参阅 https://ratpanel.github.io/advanced/api.html",
diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json
index d58eb272..2f1b6429 100644
--- a/ui/src/i18n/locales/zh/nls.provider.json
+++ b/ui/src/i18n/locales/zh/nls.provider.json
@@ -3,6 +3,7 @@
"provider.1panel.console": "1Panel - 面板自身",
"provider.1panel.site": "1Panel - 网站",
"provider.acmeca": "ACME 自定义 CA 端点",
+ "provider.acmedns": "ACME-DNS",
"provider.acmehttpreq": "ACME 自定义 HTTP 端点",
"provider.aliyun": "阿里云",
"provider.aliyun.alb": "阿里云 - 应用型负载均衡 ALB",
diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json
index 0398cef5..a83826c0 100644
--- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json
+++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json
@@ -63,15 +63,22 @@
"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.key_algorithm.label": "证书算法",
- "workflow_node.apply.form.key_algorithm.placeholder": "请选择证书算法",
+ "workflow_node.apply.form.key_algorithm.placeholder": "请选择证书的算法",
"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.acme_profile.label": "ACME 证书配置(可选)",
- "workflow_node.apply.form.acme_profile.placeholder": "请输入 ACME 证书配置",
+ "workflow_node.apply.form.validity_lifetime.label": "证书有效期(可选)",
+ "workflow_node.apply.form.validity_lifetime.placeholder": "请输入证书的有效期",
+ "workflow_node.apply.form.validity_lifetime.help": "注意:并非所有证书颁发机构都支持此特性。",
+ "workflow_node.apply.form.validity_lifetime.tooltip": "表示证书的有效期。如果你不了解该选项的用途,保持默认即可。",
+ "workflow_node.apply.form.validity_lifetime.units.h": "小时",
+ "workflow_node.apply.form.validity_lifetime.units.d": "天",
+ "workflow_node.apply.form.acme_profile.label": "证书 ACME 配置(可选)",
+ "workflow_node.apply.form.acme_profile.placeholder": "请输入证书的 ACME 配置",
+ "workflow_node.apply.form.acme_profile.help": "注意:并非所有证书颁发机构都支持此特性。",
"workflow_node.apply.form.acme_profile.tooltip": "表示证书颁发时使用的 ACME 证书配置。如果你不了解该选项的用途,保持默认即可。
点此了解更多。",
"workflow_node.apply.form.nameservers.label": "DNS 递归服务器(可选)",
"workflow_node.apply.form.nameservers.placeholder": "请输入 DNS 递归服务器(多个值请用半角分号隔开)",