diff --git a/packages/ui/certd-server/src/plugins/index.ts b/packages/ui/certd-server/src/plugins/index.ts index c1905f4b1..2b29f51bb 100644 --- a/packages/ui/certd-server/src/plugins/index.ts +++ b/packages/ui/certd-server/src/plugins/index.ts @@ -48,3 +48,4 @@ // export * from './plugin-dnsmgr/index.js' // export * from './plugin-nginx-proxy-manager/index.js' // export * from './plugin-hipmdnsmgr/index.js' +// export * from './plugin-dynadot/index.js' diff --git a/packages/ui/certd-server/src/plugins/plugin-dynadot/access.ts b/packages/ui/certd-server/src/plugins/plugin-dynadot/access.ts new file mode 100644 index 000000000..47e3c6fc2 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-dynadot/access.ts @@ -0,0 +1,102 @@ +import { AccessInput, BaseAccess, IsAccess, PageRes, PageSearch } from "@certd/pipeline"; +import { DomainRecord } from "@certd/plugin-lib"; +import { merge } from "lodash-es"; + +@IsAccess({ + name: "dynadot", + title: "Dynadot授权", + desc: "", + icon: "simple-icons:dynatrace", +}) +export class DynadotAccess extends BaseAccess { + @AccessInput({ + title: "API Key", + component: { + placeholder: "api key", + }, + helper: "前往 [Dynadot API设置](https://www.dynadot.cn/zh/account/domain/setting/api.html) 获取API Key", + required: true, + encrypt: true, + }) + apiKey = ""; + + @AccessInput({ + title: "测试", + component: { + name: "api-test", + action: "TestRequest", + }, + helper: "点击测试接口是否正常", + }) + testRequest = true; + + async onTestRequest() { + await this.getDomainListPage({ + pageNo: 1, + pageSize: 1, + }); + return "ok"; + } + + async getDomainListPage(req: PageSearch): Promise> { + const params: any = { + command: "list_domain", + key: this.apiKey, + }; + if (req.searchKey) { + params.search = req.searchKey; + } + + const res = await this.doRequest(params); + + const domains = res.ListDomainResponse?.DomainList || []; + const list = domains.map((item: any) => ({ + id: item.Name || item.DomainName, + domain: item.Name || item.DomainName, + })); + + return { + total: list.length, + list, + }; + } + + async doRequest(params: any = null) { + params = merge( + { + key: this.apiKey, + }, + params + ); + const res = await this.ctx.http.request({ + url: "/api3.json", + baseURL: "https://api.dynadot.com", + method: "get", + params, + headers: { + "Content-Type": "application/json", + }, + }); + + this.checkApiError(res); + return res; + } + + private checkApiError(res: any) { + if (!res || typeof res !== "object") { + return; + } + for (const key of Object.keys(res)) { + const value = res[key]; + if (value && typeof value === "object" && "ResponseCode" in value) { + const code = value.ResponseCode; + if (code !== 0 && code !== "0") { + const errorMsg = value.Error || value.Status || JSON.stringify(value); + throw new Error(`Dynadot API错误: ${errorMsg}`); + } + } + } + } +} + +new DynadotAccess(); diff --git a/packages/ui/certd-server/src/plugins/plugin-dynadot/dns-provider.ts b/packages/ui/certd-server/src/plugins/plugin-dynadot/dns-provider.ts new file mode 100644 index 000000000..9e358e73d --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-dynadot/dns-provider.ts @@ -0,0 +1,173 @@ +import { PageRes, PageSearch } from "@certd/pipeline"; +import { AbstractDnsProvider, CreateRecordOptions, DomainRecord, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert"; +import { DynadotAccess } from "./access.js"; + +export type DynadotRecord = { + subdomain: string; + sub_record_type: string; + sub_record: string; +}; + +@IsDnsProvider({ + name: "dynadot", + title: "Dynadot", + desc: "Dynadot DNS提供商", + icon: "simple-icons:dynatrace", + accessType: "dynadot", +}) +export class DynadotDnsProvider extends AbstractDnsProvider { + access!: DynadotAccess; + + async onInstance() { + this.access = this.ctx.access as DynadotAccess; + } + + async createRecord(options: CreateRecordOptions): Promise { + const { fullRecord, hostRecord, value, type, domain } = options; + this.logger.info("添加域名解析:", fullRecord, value, type, domain); + + try { + const existingRecords = await this.getDnsRecords(domain); + const subdomainIndex = existingRecords.subRecords.length; + + const setParams: any = { + command: "set_dns2", + domain, + key: this.access.apiKey, + }; + + for (let i = 0; i < existingRecords.mainRecords.length; i++) { + const rec = existingRecords.mainRecords[i]; + setParams[`main_record_type${i}`] = rec.type; + setParams[`main_record${i}`] = rec.value; + setParams[`main_recordx${i}`] = rec.extra || ""; + } + + for (let i = 0; i < existingRecords.subRecords.length; i++) { + const rec = existingRecords.subRecords[i]; + setParams[`subdomain${i}`] = rec.subdomain; + setParams[`sub_record_type${i}`] = rec.type; + setParams[`sub_record${i}`] = rec.value; + setParams[`sub_recordx${i}`] = rec.extra || ""; + } + + setParams[`subdomain${subdomainIndex}`] = hostRecord; + setParams[`sub_record_type${subdomainIndex}`] = type; + setParams[`sub_record${subdomainIndex}`] = value; + setParams[`sub_recordx${subdomainIndex}`] = ""; + setParams.ttl = 600; + + await this.access.doRequest(setParams); + + this.logger.info("添加域名解析成功:", fullRecord, value); + return { + subdomain: hostRecord, + sub_record_type: type, + sub_record: value, + }; + } catch (error) { + this.logger.error("创建DNS记录失败:", error); + throw new Error(`创建DNS记录失败: ${(error as Error).message}`); + } + } + + async removeRecord(options: RemoveRecordOptions): Promise { + const { fullRecord, value, domain } = options.recordReq; + const record = options.recordRes; + this.logger.info("删除域名解析:", fullRecord, value); + + try { + if (!record || !record.subdomain) { + this.logger.info("record为空,不执行删除"); + return; + } + + const existingRecords = await this.getDnsRecords(domain); + + const setParams: any = { + command: "set_dns2", + domain, + key: this.access.apiKey, + }; + + for (let i = 0; i < existingRecords.mainRecords.length; i++) { + const rec = existingRecords.mainRecords[i]; + setParams[`main_record_type${i}`] = rec.type; + setParams[`main_record${i}`] = rec.value; + setParams[`main_recordx${i}`] = rec.extra || ""; + } + + let newIndex = 0; + for (let i = 0; i < existingRecords.subRecords.length; i++) { + const rec = existingRecords.subRecords[i]; + if (rec.subdomain === record.subdomain && rec.type === record.sub_record_type && rec.value === record.sub_record) { + continue; + } + setParams[`subdomain${newIndex}`] = rec.subdomain; + setParams[`sub_record_type${newIndex}`] = rec.type; + setParams[`sub_record${newIndex}`] = rec.value; + setParams[`sub_recordx${newIndex}`] = rec.extra || ""; + newIndex++; + } + setParams.ttl = 600; + + await this.access.doRequest(setParams); + + this.logger.info("删除域名解析成功:", fullRecord, value); + } catch (error) { + this.logger.error("删除DNS记录失败:", error); + } + } + + async getDomainListPage(req: PageSearch): Promise> { + return await this.access.getDomainListPage(req); + } + + private async getDnsRecords(domain: string): Promise<{ + mainRecords: Array<{ type: string; value: string; extra: string }>; + subRecords: Array<{ subdomain: string; type: string; value: string; extra: string }>; + }> { + const res = await this.access.doRequest({ + command: "get_dns", + domain, + }); + + const dnsContent = res.GetDnsResponse?.DnsContent || res.GetDnsResponse?.dnsContent || {}; + + const mainRecords: Array<{ type: string; value: string; extra: string }> = []; + const subRecords: Array<{ subdomain: string; type: string; value: string; extra: string }> = []; + + const MAX_MAIN = 20; + const MAX_SUB = 100; + + for (let i = 0; i < MAX_MAIN; i++) { + const type = dnsContent[`MainRecordType${i}`]; + const value = dnsContent[`MainRecord${i}`]; + if (value !== undefined && value !== null && value !== "") { + mainRecords.push({ + type: type || "", + value: String(value), + extra: String(dnsContent[`MainRecordX${i}`] || ""), + }); + } + } + + for (let i = 0; i < MAX_SUB; i++) { + const subdomain = dnsContent[`SubDomain${i}`]; + const type = dnsContent[`SubRecordType${i}`]; + const value = dnsContent[`SubRecord${i}`]; + if (value !== undefined && value !== null && value !== "" && subdomain !== undefined && subdomain !== null && subdomain !== "") { + subRecords.push({ + subdomain: String(subdomain), + type: type || "", + value: String(value), + extra: String(dnsContent[`SubRecordX${i}`] || ""), + }); + } + } + + return { mainRecords, subRecords }; + } +} + +new DynadotDnsProvider(); diff --git a/packages/ui/certd-server/src/plugins/plugin-dynadot/index.ts b/packages/ui/certd-server/src/plugins/plugin-dynadot/index.ts new file mode 100644 index 000000000..d0bc76d38 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-dynadot/index.ts @@ -0,0 +1,2 @@ +export * from "./dns-provider.js"; +export * from "./access.js";