diff --git a/README.md b/README.md index dbfcd1ee..e295adbb 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ make local.run | AWS | √ | | 可签发在 AWS Route53 托管的域名 | | CloudFlare | √ | | 可签发在 CloudFlare 注册的域名;CloudFlare 服务自带 SSL 证书 | | GoDaddy | √ | | 可签发在 GoDaddy 注册的域名 | +| Name.com | √ | | 可签发在 Name.com 注册的域名 | | NameSilo | √ | | 可签发在 NameSilo 注册的域名 | | PowerDNS | √ | | 可签发在 PowerDNS 托管的域名 | | HTTP 请求 | √ | | 可签发允许通过 HTTP 请求修改 DNS 的域名 | diff --git a/README_EN.md b/README_EN.md index 88e03baf..181f5190 100644 --- a/README_EN.md +++ b/README_EN.md @@ -87,6 +87,7 @@ password:1234567890 | AWS | √ | | Supports domains managed on AWS Route53 | | CloudFlare | √ | | Supports domains registered on CloudFlare; CloudFlare services come with SSL certificates | | GoDaddy | √ | | Supports domains registered on GoDaddy | +| Name.com | √ | | Supports domains registered on Name.com | | NameSilo | √ | | Supports domains registered on NameSilo | | PowerDNS | √ | | Supports domains managed on PowerDNS | | HTTP Request | √ | | Supports domains which allow managing DNS by HTTP request | diff --git a/go.mod b/go.mod index 9433acba..8fa0f6a6 100644 --- a/go.mod +++ b/go.mod @@ -63,6 +63,7 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/technoweenie/multipartstreamer v1.0.1 // indirect github.com/x448/float16 v0.8.4 // indirect diff --git a/go.sum b/go.sum index 4549f32b..abf63404 100644 --- a/go.sum +++ b/go.sum @@ -640,6 +640,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 h1:o6uBwrhM5C8Ll3MAAxrQxRHEu7FkapwTuI2WmL1rw4g= +github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8= github.com/nats-io/jwt v1.2.2/go.mod h1:/xX356yQA6LuXI9xWW7mZNpxgF2mBmGecH+Fj34sP5Q= github.com/nats-io/jwt/v2 v2.0.3/go.mod h1:VRP+deawSXyhNjXmxPCHskrR6Mq50BqpEI5SEcNiGlY= github.com/nats-io/nats-server/v2 v2.5.0/go.mod h1:Kj86UtrXAL6LwYRA6H4RqzkHhK0Vcv2ZnKD5WbQ1t3g= diff --git a/internal/applicant/applicant.go b/internal/applicant/applicant.go index c4804559..d2056863 100644 --- a/internal/applicant/applicant.go +++ b/internal/applicant/applicant.go @@ -39,6 +39,7 @@ const ( configTypeCloudflare = "cloudflare" configTypeGoDaddy = "godaddy" configTypeHuaweiCloud = "huaweicloud" + configTypeNameDotCom = "namedotcom" configTypeNameSilo = "namesilo" configTypePowerDNS = "powerdns" configTypeTencentCloud = "tencentcloud" @@ -225,6 +226,8 @@ func GetWithTypeOption(t string, option *ApplyOption) (Applicant, error) { return NewGoDaddyApplicant(option), nil case configTypeHuaweiCloud: return NewHuaweiCloudApplicant(option), nil + case configTypeNameDotCom: + return NewNameDotComApplicant(option), nil case configTypeNameSilo: return NewNamesiloApplicant(option), nil case configTypePowerDNS: diff --git a/internal/applicant/namedotcom.go b/internal/applicant/namedotcom.go new file mode 100644 index 00000000..5bcc39f2 --- /dev/null +++ b/internal/applicant/namedotcom.go @@ -0,0 +1,38 @@ +package applicant + +import ( + "encoding/json" + "time" + + "github.com/go-acme/lego/v4/providers/dns/namedotcom" + "github.com/usual2970/certimate/internal/domain" +) + +type nameDotComApplicant struct { + option *ApplyOption +} + +func NewNameDotComApplicant(option *ApplyOption) Applicant { + return &nameDotComApplicant{ + option: option, + } +} + +func (a *nameDotComApplicant) Apply() (*Certificate, error) { + access := &domain.NameDotComAccess{} + json.Unmarshal([]byte(a.option.Access), access) + + config := namedotcom.NewDefaultConfig() + config.Username = access.Username + config.APIToken = access.ApiToken + if a.option.Timeout != 0 { + config.PropagationTimeout = time.Duration(a.option.Timeout) * time.Second + } + + provider, err := namedotcom.NewDNSProviderConfig(config) + if err != nil { + return nil, err + } + + return apply(a.option, provider) +} diff --git a/internal/domain/access.go b/internal/domain/access.go index f040c5ca..348e6c46 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -77,6 +77,11 @@ type GodaddyAccess struct { ApiSecret string `json:"apiSecret"` } +type NameDotComAccess struct { + Username string `json:"username"` + ApiToken string `json:"apiToken"` +} + type PdnsAccess struct { ApiUrl string `json:"apiUrl"` ApiKey string `json:"apiKey"` diff --git a/migrations/1735032595_add_namedotcom.go b/migrations/1735032595_add_namedotcom.go new file mode 100644 index 00000000..b464a93c --- /dev/null +++ b/migrations/1735032595_add_namedotcom.go @@ -0,0 +1,109 @@ +package migrations + +import ( + "encoding/json" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/daos" + m "github.com/pocketbase/pocketbase/migrations" + "github.com/pocketbase/pocketbase/models/schema" +) + +func init() { + m.Register(func(db dbx.Builder) error { + dao := daos.New(db) + + collection, err := dao.FindCollectionByNameOrId("4yzbv8urny5ja1e") + if err != nil { + return err + } + + // update + edit_configType := &schema.SchemaField{} + if err := json.Unmarshal([]byte(`{ + "system": false, + "id": "hwy7m03o", + "name": "configType", + "type": "select", + "required": false, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 1, + "values": [ + "aliyun", + "tencent", + "huaweicloud", + "qiniu", + "aws", + "cloudflare", + "namesilo", + "godaddy", + "pdns", + "httpreq", + "local", + "ssh", + "webhook", + "k8s", + "baiducloud", + "dogecloud", + "volcengine", + "byteplus", + "namedotcom" + ] + } + }`), edit_configType); err != nil { + return err + } + collection.Schema.AddField(edit_configType) + + return dao.SaveCollection(collection) + }, func(db dbx.Builder) error { + dao := daos.New(db) + + collection, err := dao.FindCollectionByNameOrId("4yzbv8urny5ja1e") + if err != nil { + return err + } + + // update + edit_configType := &schema.SchemaField{} + if err := json.Unmarshal([]byte(`{ + "system": false, + "id": "hwy7m03o", + "name": "configType", + "type": "select", + "required": false, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 1, + "values": [ + "aliyun", + "tencent", + "huaweicloud", + "qiniu", + "aws", + "cloudflare", + "namesilo", + "godaddy", + "pdns", + "httpreq", + "local", + "ssh", + "webhook", + "k8s", + "baiducloud", + "dogecloud", + "volcengine", + "byteplus" + ] + } + }`), edit_configType); err != nil { + return err + } + collection.Schema.AddField(edit_configType) + + return dao.SaveCollection(collection) + }) +} diff --git a/ui/public/imgs/providers/namedotcom.svg b/ui/public/imgs/providers/namedotcom.svg new file mode 100644 index 00000000..2b7c7131 --- /dev/null +++ b/ui/public/imgs/providers/namedotcom.svg @@ -0,0 +1 @@ + diff --git a/ui/src/components/access/AccessEditForm.tsx b/ui/src/components/access/AccessEditForm.tsx index 01f182d1..511c28f4 100644 --- a/ui/src/components/access/AccessEditForm.tsx +++ b/ui/src/components/access/AccessEditForm.tsx @@ -19,6 +19,7 @@ import AccessEditFormGoDaddyConfig from "./AccessEditFormGoDaddyConfig"; import AccessEditFormHuaweiCloudConfig from "./AccessEditFormHuaweiCloudConfig"; import AccessEditFormKubernetesConfig from "./AccessEditFormKubernetesConfig"; import AccessEditFormLocalConfig from "./AccessEditFormLocalConfig"; +import AccessEditFormNameDotComConfig from "./AccessEditFormNameDotComConfig"; import AccessEditFormNameSiloConfig from "./AccessEditFormNameSiloConfig"; import AccessEditFormPowerDNSConfig from "./AccessEditFormPowerDNSConfig"; import AccessEditFormQiniuConfig from "./AccessEditFormQiniuConfig"; @@ -98,6 +99,8 @@ const AccessEditForm = forwardRef(( return ; case ACCESS_PROVIDER_TYPES.LOCAL: return ; + case ACCESS_PROVIDER_TYPES.NAMEDOTCOM: + return ; case ACCESS_PROVIDER_TYPES.NAMESILO: return ; case ACCESS_PROVIDER_TYPES.POWERDNS: diff --git a/ui/src/components/access/AccessEditFormNameDotComConfig.tsx b/ui/src/components/access/AccessEditFormNameDotComConfig.tsx new file mode 100644 index 00000000..5b810ce8 --- /dev/null +++ b/ui/src/components/access/AccessEditFormNameDotComConfig.tsx @@ -0,0 +1,77 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useDeepCompareEffect } from "ahooks"; +import { Form, Input, type FormInstance } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type NameDotComAccessConfig } from "@/domain/access"; + +type AccessEditFormNameDotComConfigModelType = Partial; + +export type AccessEditFormNameDotComConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormNameDotComConfigModelType; + onModelChange?: (model: AccessEditFormNameDotComConfigModelType) => void; +}; + +const initModel = () => { + return { + username: "", + apiToken: "", + } as AccessEditFormNameDotComConfigModelType; +}; + +const AccessEditFormNameDotComConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormNameDotComConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + username: z + .string() + .trim() + .min(1, t("access.form.namedotcom_username.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + apiToken: z + .string() + .trim() + .min(1, t("access.form.namedotcom_api_token.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useDeepCompareEffect(() => { + setInitialValues(model ?? initModel()); + }, [model]); + + const handleFormChange = (_: unknown, fields: AccessEditFormNameDotComConfigModelType) => { + onModelChange?.(fields); + }; + + return ( +
+ } + > + + + + } + > + + +
+ ); +}; + +export default AccessEditFormNameDotComConfig; diff --git a/ui/src/domain/access.ts b/ui/src/domain/access.ts index 9e08fa1e..23adc541 100644 --- a/ui/src/domain/access.ts +++ b/ui/src/domain/access.ts @@ -13,6 +13,7 @@ export const ACCESS_PROVIDER_TYPE_GODADDY = "godaddy" as const; export const ACCESS_PROVIDER_TYPE_HUAWEICLOUD = "huaweicloud" as const; export const ACCESS_PROVIDER_TYPE_KUBERNETES = "k8s" as const; export const ACCESS_PROVIDER_TYPE_LOCAL = "local" as const; +export const ACCESS_PROVIDER_TYPE_NAMEDOTCOM = "namedotcom" as const; export const ACCESS_PROVIDER_TYPE_NAMESILO = "namesilo" as const; export const ACCESS_PROVIDER_TYPE_POWERDNS = "powerdns" as const; export const ACCESS_PROVIDER_TYPE_QINIU = "qiniu" as const; @@ -32,6 +33,7 @@ export const ACCESS_PROVIDER_TYPES = Object.freeze({ HUAWEICLOUD: ACCESS_PROVIDER_TYPE_HUAWEICLOUD, KUBERNETES: ACCESS_PROVIDER_TYPE_KUBERNETES, LOCAL: ACCESS_PROVIDER_TYPE_LOCAL, + NAMEDOTCOM: ACCESS_PROVIDER_TYPE_NAMEDOTCOM, NAMESILO: ACCESS_PROVIDER_TYPE_NAMESILO, POWERDNS: ACCESS_PROVIDER_TYPE_POWERDNS, QINIU: ACCESS_PROVIDER_TYPE_QINIU, @@ -75,6 +77,7 @@ export interface AccessModel extends BaseModel { | HuaweiCloudAccessConfig | KubernetesAccessConfig | LocalAccessConfig + | NameDotComAccessConfig | NameSiloAccessConfig | PowerDNSAccessConfig | QiniuAccessConfig @@ -142,6 +145,11 @@ export type KubernetesAccessConfig = { export type LocalAccessConfig = NonNullable; +export type NameDotComAccessConfig = { + username: string; + apiToken: string; +}; + export type NameSiloAccessConfig = { apiKey: string; }; @@ -194,6 +202,9 @@ export const accessProvidersMap: Map = n NOTICE: The following order determines the order displayed at the frontend. */ [ + [ACCESS_PROVIDER_TYPE_LOCAL, "common.provider.local", "/imgs/providers/local.svg", "deploy"], + [ACCESS_PROVIDER_TYPE_SSH, "common.provider.ssh", "/imgs/providers/ssh.svg", "deploy"], + [ACCESS_PROVIDER_TYPE_WEBHOOK, "common.provider.webhook", "/imgs/providers/webhook.svg", "deploy"], [ACCESS_PROVIDER_TYPE_ALIYUN, "common.provider.aliyun", "/imgs/providers/aliyun.svg", "all"], [ACCESS_PROVIDER_TYPE_TENCENTCLOUD, "common.provider.tencentcloud", "/imgs/providers/tencentcloud.svg", "all"], [ACCESS_PROVIDER_TYPE_HUAWEICLOUD, "common.provider.huaweicloud", "/imgs/providers/huaweicloud.svg", "all"], @@ -204,12 +215,10 @@ export const accessProvidersMap: Map = n [ACCESS_PROVIDER_TYPE_BYTEPLUS, "common.provider.byteplus", "/imgs/providers/byteplus.svg", "all"], [ACCESS_PROVIDER_TYPE_AWS, "common.provider.aws", "/imgs/providers/aws.svg", "apply"], [ACCESS_PROVIDER_TYPE_CLOUDFLARE, "common.provider.cloudflare", "/imgs/providers/cloudflare.svg", "apply"], + [ACCESS_PROVIDER_TYPE_NAMEDOTCOM, "common.provider.namedotcom", "/imgs/providers/namedotcom.svg", "apply"], [ACCESS_PROVIDER_TYPE_NAMESILO, "common.provider.namesilo", "/imgs/providers/namesilo.svg", "apply"], [ACCESS_PROVIDER_TYPE_GODADDY, "common.provider.godaddy", "/imgs/providers/godaddy.svg", "apply"], [ACCESS_PROVIDER_TYPE_POWERDNS, "common.provider.powerdns", "/imgs/providers/powerdns.svg", "apply"], - [ACCESS_PROVIDER_TYPE_LOCAL, "common.provider.local", "/imgs/providers/local.svg", "deploy"], - [ACCESS_PROVIDER_TYPE_SSH, "common.provider.ssh", "/imgs/providers/ssh.svg", "deploy"], - [ACCESS_PROVIDER_TYPE_WEBHOOK, "common.provider.webhook", "/imgs/providers/webhook.svg", "deploy"], [ACCESS_PROVIDER_TYPE_KUBERNETES, "common.provider.kubernetes", "/imgs/providers/kubernetes.svg", "deploy"], [ACCESS_PROVIDER_TYPE_ACMEHTTPREQ, "common.provider.acmehttpreq", "/imgs/providers/acmehttpreq.svg", "apply"], ].map(([type, name, icon, usage]) => [type, { type, name, icon, usage: usage as AccessProviderUsages }]) diff --git a/ui/src/i18n/locales/en/nls.access.json b/ui/src/i18n/locales/en/nls.access.json index e24657d1..0d00395a 100644 --- a/ui/src/i18n/locales/en/nls.access.json +++ b/ui/src/i18n/locales/en/nls.access.json @@ -91,6 +91,12 @@ "access.form.k8s_kubeconfig.placeholder": "Please enter KubeConfig file", "access.form.k8s_kubeconfig.upload": "Choose File ...", "access.form.k8s_kubeconfig.tooltip": "For more information, see https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/

Leave blank to use the Pod's ServiceAccount.", + "access.form.namedotcom_username.label": "Name.com Username", + "access.form.namedotcom_username.placeholder": "Please enter Name.com Username", + "access.form.namedotcom_username.tooltip": "For more information, see https://www.name.com/account/settings/api", + "access.form.namedotcom_api_token.label": "Name.com API Token", + "access.form.namedotcom_api_token.placeholder": "Please enter Name.com API Token", + "access.form.namedotcom_api_token.tooltip": "For more information, see https://www.name.com/support/articles/31142639244819-how-to-manage-your-api-tokens", "access.form.namesilo_api_key.label": "NameSilo API Key", "access.form.namesilo_api_key.placeholder": "Please enter NameSilo API Key", "access.form.namesilo_api_key.tooltip": "For more information, see https://www.namesilo.com/support/v2/articles/account-options/api-manager", diff --git a/ui/src/i18n/locales/en/nls.common.json b/ui/src/i18n/locales/en/nls.common.json index 3cc8d1ba..3356ff58 100644 --- a/ui/src/i18n/locales/en/nls.common.json +++ b/ui/src/i18n/locales/en/nls.common.json @@ -71,6 +71,7 @@ "common.provider.kubernetes": "Kubernetes", "common.provider.kubernetes.secret": "Kubernetes - Secret", "common.provider.local": "Local Deployment", + "common.provider.namedotcom": "Name.com", "common.provider.namesilo": "NameSilo", "common.provider.powerdns": "PowerDNS", "common.provider.qiniu": "Qiniu", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index ea4ecafb..0de78b3c 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -91,6 +91,12 @@ "access.form.k8s_kubeconfig.placeholder": "请选择 KubeConfig 文件", "access.form.k8s_kubeconfig.upload": "选择文件", "access.form.k8s_kubeconfig.tooltip": "这是什么?请参阅 https://kubernetes.io/zh-cn/docs/concepts/configuration/organize-cluster-access-kubeconfig/

为空时,将使用 Pod 的 ServiceAccount 作为凭证。", + "access.form.namedotcom_username.label": "Name.com 用户名", + "access.form.namedotcom_username.placeholder": "请输入 Name.com 用户名", + "access.form.namedotcom_username.tooltip": "这是什么?请参阅 https://www.name.com/account/settings/api", + "access.form.namedotcom_api_token.label": "Name.com API Token", + "access.form.namedotcom_api_token.placeholder": "请输入 Name.com API Token", + "access.form.namedotcom_api_token.tooltip": "这是什么?请参阅 https://www.name.com/support/articles/31142639244819-how-to-manage-your-api-tokens", "access.form.namesilo_api_key.label": "NameSilo API Key", "access.form.namesilo_api_key.placeholder": "请输入 NameSilo API Key", "access.form.namesilo_api_key.tooltip": "这是什么?请参阅 https://www.namesilo.com/support/v2/articles/account-options/api-manager", diff --git a/ui/src/i18n/locales/zh/nls.common.json b/ui/src/i18n/locales/zh/nls.common.json index fd707ff2..30140399 100644 --- a/ui/src/i18n/locales/zh/nls.common.json +++ b/ui/src/i18n/locales/zh/nls.common.json @@ -71,6 +71,7 @@ "common.provider.kubernetes": "Kubernetes", "common.provider.kubernetes.secret": "Kubernetes - Secret", "common.provider.local": "本地部署", + "common.provider.namedotcom": "Name.com", "common.provider.namesilo": "NameSilo", "common.provider.powerdns": "PowerDNS", "common.provider.qiniu": "七牛云",