chore: 域名自动同步初步

This commit is contained in:
xiaojunnuo
2026-01-16 18:18:39 +08:00
parent 8685aa371a
commit be1a70299f
22 changed files with 248 additions and 10 deletions

View File

@@ -124,6 +124,7 @@ export default defineConfig({
{text: "子域名托管", link: "/guide/use/cert/subdomain.md"},
{text: "流水线有效期", link: "/guide/use/pipeline/valid.md"},
{text: "IP证书申请", link: "/guide/use/cert/ip.md"},
{text: "插件开发", link: "/guide/use/dev/plugin.md"},
]
},
{

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -0,0 +1,19 @@
# 插件开发
## 插件创建
点击自定义插件按钮,填写插件基本信息
![plugin-create.png](images/plugin-create.png)
创建成功后,会默认打开插件编辑页面,里面默认带有示例代码说明,可以在此基础上进行你的自定义开发
![plugin-edit.png](images/plugin-edit.png)
## 插件测试
在流水线中添加插件任务
![plugin-test.png](images/plugin-test1.png)
配置插件任务参数
![plugin-test.png](images/plugin-test2.png)
点击运行,查看插件任务运行结果
![plugin-test.png](images/plugin-test3.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -0,0 +1,2 @@
# 第三方登录配置

View File

@@ -0,0 +1,11 @@
# 用户有效期功能
可以为用户设置有效期,超过有效期后,用户的流水线将停止运行
## 开启用户有效期功能
![开启用户有效期功能](images/user_valid_enable.png)
## 设置用户有效期
![设置用户有效期](images/user_valid_set.png)

View File

@@ -11,11 +11,11 @@ export type PageSearch = {
// sortOrder?: "asc" | "desc";
};
export type PageRes = {
export type PageRes<T = any> = {
pageNo?: number;
pageSize?: number;
total?: string;
list: any[];
total?: number;
list: T[];
};
export class Pager {

View File

@@ -1,5 +1,5 @@
import { HttpClient, ILogger, utils } from "@certd/basic";
import { IAccess, IServiceGetter, Registrable } from "@certd/pipeline";
import { IAccess, IServiceGetter, Pager, PageRes, Registrable } from "@certd/pipeline";
export type DnsProviderDefine = Registrable & {
accessType: string;
@@ -28,6 +28,12 @@ export type DnsProviderContext = {
serviceGetter: IServiceGetter;
};
export type DomainRecord = {
id: string;
domain: string;
thirdDns: boolean;
};
export interface IDnsProvider<T = any> {
onInstance(): Promise<void>;
@@ -51,6 +57,8 @@ export interface IDnsProvider<T = any> {
//中文域名是否需要punycode转码如果返回True则使用punycode来添加解析记录否则使用中文域名添加解析记录
usePunyCode(): boolean;
getDomainListPage(pager: Pager): Promise<PageRes<DomainRecord>>;
}
export interface ISubDomainsGetter {

View File

@@ -1,4 +1,5 @@
import { CreateRecordOptions, DnsProviderContext, DnsProviderDefine, IDnsProvider, RemoveRecordOptions } from "./api.js";
import { Pager, PageRes } from "@certd/pipeline";
import { CreateRecordOptions, DnsProviderContext, DnsProviderDefine, DomainRecord, IDnsProvider, RemoveRecordOptions } from "./api.js";
import { dnsProviderRegistry } from "./registry.js";
import { HttpClient, ILogger } from "@certd/basic";
import punycode from "punycode.js";
@@ -44,6 +45,10 @@ export abstract class AbstractDnsProvider<T = any> implements IDnsProvider<T> {
abstract onInstance(): Promise<void>;
abstract removeRecord(options: RemoveRecordOptions<T>): Promise<void>;
async getDomainListPage(pager: Pager): Promise<PageRes<DomainRecord>> {
throw new Error("Method not implemented.");
}
}
export async function createDnsProvider(opts: { dnsProviderType: string; context: DnsProviderContext }): Promise<IDnsProvider> {

View File

@@ -36,6 +36,7 @@ export default {
title: "Framework",
home: "Home",
},
helpDocLink: "Help Docs",
title: "Certificate Automation",
pipeline: "Pipeline",
pipelineEdit: "Edit Pipeline",

View File

@@ -40,7 +40,7 @@ export default {
title: "框架",
home: "首页",
},
helpDocLink: "帮助文档",
title: "证书自动化",
pipeline: "证书自动化流水线",
pipelineEdit: "编辑流水线",

View File

@@ -34,7 +34,7 @@
<a-select-option value="ipv4first">{{ t("certd.ipv4Priority") }}</a-select-option>
<a-select-option value="ipv6first">{{ t("certd.ipv6Priority") }}</a-select-option>
</a-select>
<div class="helper">{{ t("certd.dualStackNetworkHelper") }}</div>
<div class="helper">{{ t("certd.dualStackNetworkHelper") }}, <a href="https://certd.docmirror.cn/guide/use/setting/ipv6.html" target="_blank">{{ t("certd.helpDocLink") }}</a></div>
</a-form-item>
<a-form-item :label="t('certd.sys.setting.showRunStrategy')" :name="['public', 'showRunStrategy']">

View File

@@ -21,7 +21,10 @@
<a-switch v-model:checked="formState.public.certDomainAddToMonitorEnabled" :disabled="!settingsStore.isPlus" />
<vip-button class="ml-5" mode="button"></vip-button>
</div>
<div class="helper">{{ t("certd.sys.setting.certDomainAddToMonitorEnabledHelper") }}</div>
<div class="helper">
{{ t("certd.sys.setting.certDomainAddToMonitorEnabledHelper") }}
<a href="https://certd.docmirror.cn/guide/use/setting/user-valid.html" target="_blank">{{ t("certd.helpDocLink") }}</a>
</div>
</a-form-item>
<a-form-item :label="t('certd.sys.setting.fixedCertExpireDays')" :name="['public', 'fixedCertExpireDays']">

View File

@@ -12,7 +12,10 @@
<a-switch v-model:checked="formState.public.userValidTimeEnabled" :disabled="!settingsStore.isPlus" />
<vip-button class="ml-5" mode="button"></vip-button>
</div>
<div class="helper">{{ t("certd.userValidityPeriodHelper") }}</div>
<div class="helper">
{{ t("certd.userValidityPeriodHelper") }}
<a href="https://certd.docmirror.cn/guide/use/setting/user-valid.html" target="_blank">{{ t("certd.helpDocLink") }}</a>
</div>
</a-form-item>
<template v-if="formState.public.registerEnabled">
<a-form-item :label="t('certd.enableUsernameRegistration')" :name="['public', 'usernameRegisterEnabled']">

View File

@@ -78,4 +78,15 @@ export class DomainController extends CrudController<DomainService> {
return this.ok();
}
@Post('/sync/submit', { summary: Constants.per.authOnly })
async sync(@Body(ALL) body: any) {
const { dnsProviderType, dnsProviderAccessId } = body;
const req = {
dnsProviderType, dnsProviderAccessId, userId: this.getUserId(),
}
await this.service.syncFromProvider(req);
return this.ok();
}
}

View File

@@ -4,12 +4,20 @@ import {In, Not, Repository} from 'typeorm';
import {AccessService, BaseService} from '@certd/lib-server';
import {DomainEntity} from '../entity/domain.js';
import {SubDomainService} from "../../pipeline/service/sub-domain-service.js";
import {DomainParser} from "@certd/plugin-cert";
import {createDnsProvider, DomainParser} from "@certd/plugin-lib";
import {DomainVerifiers} from "@certd/plugin-cert";
import { SubDomainsGetter } from '../../pipeline/service/getter/sub-domain-getter.js';
import { CnameRecordService } from '../../cname/service/cname-record-service.js';
import { CnameRecordEntity } from "../../cname/entity/cname-record.js";
import { http, logger, utils } from '@certd/basic';
import { TaskServiceBuilder } from '../../pipeline/service/getter/task-service-getter.js';
import { Pager } from '@certd/pipeline';
export interface SyncFromProviderReq {
userId: number;
dnsProviderType: string;
dnsProviderAccessId: string;
}
/**
*
@@ -28,6 +36,9 @@ export class DomainService extends BaseService<DomainEntity> {
@Inject()
cnameRecordService: CnameRecordService;
@Inject()
taskServiceBuilder: TaskServiceBuilder;
//@ts-ignore
getRepository() {
return this.repository;
@@ -187,4 +198,79 @@ export class DomainService extends BaseService<DomainEntity> {
return domainVerifiers;
}
async syncFromProvider(req: SyncFromProviderReq) {
const { userId, dnsProviderType, dnsProviderAccessId } = req;
const subDomainGetter = new SubDomainsGetter(userId, this.subDomainService)
const domainParser = new DomainParser(subDomainGetter)
const serviceGetter = this.taskServiceBuilder.create({ userId });
const access = await this.accessService.getById(dnsProviderAccessId, userId);
const context = { access, logger, http, utils, domainParser, serviceGetter };
// 翻页查询dns的记录
const dnsProvider = await createDnsProvider({dnsProviderType,context})
const pager = new Pager({
pageNo: 1,
pageSize: 100,
})
const challengeType = "dns"
const importDomain = async(domainRecord: any) =>{
const domain = domainRecord.domain
const old = await this.findOne({
where: {
domain,
userId,
}
})
if (old) {
//更新
await this.update({
id: old.id,
dnsProviderType,
dnsProviderAccess: dnsProviderAccessId,
challengeType,
})
} else {
//添加
await this.add({
userId,
domain,
dnsProviderType,
dnsProviderAccess: dnsProviderAccessId,
challengeType,
})
}
}
const start = async ()=>{
let count = 0
while(true){
const pageRes = await dnsProvider.getDomainListPage(pager)
if(!pageRes || !pageRes.list || pageRes.list.length === 0){
//遍历完成
break
}
//处理
for (const domainRecord of pageRes.list) {
if (domainRecord.thirdDns) {
//域名由第三方dns解析不导入
continue
}
await importDomain(domainRecord)
}
count += pageRes.list.length
if(pageRes.total>0 && count >= pageRes.total){
//遍历完成
break
}
pager.pageNo++
}
}
start()
}
}

View File

@@ -0,0 +1,88 @@
import { logger } from "@certd/basic"
export class BackTaskExecutor{
tasks :Record<string,Record<string,BackTask>> = {}
add(type:string,task: BackTask){
if (!this.tasks[type]) {
this.tasks[type] = {}
}
this.tasks[type][task.key] = task
}
get(type: string,key: string){
return this.tasks[type][key]
}
removeIsEnd(type: string,key: string){
const task = this.tasks[type]?.[key]
if (task && task.status !== "running") {
this.clear(type,key);
}
}
clear(type: string,key: string){
const task = this.tasks[type]?.[key]
if (task) {
task.clearTimeout();
delete this.tasks[type][key]
}
}
async run(type:string,key: string){
const task = this.tasks[type]?.[key]
if (!task) {
throw new Error(`任务 ${key} 不存在`)
}
task.startTime = Date.now();
task.clearTimeout();
try{
task.status = "running";
return await task.run();
}catch(e){
logger.error(`任务 ${task.title}[${task.key}] 执行失败`, e.message);
task.status = "failed";
task.error = e.message;
}finally{
task.endTime = Date.now();
task.status = "done";
task.timeoutId = setTimeout(() => {
this.clear(type,task.key);
}, 60*60*1000);
}
}
}
export class BackTask{
key:string;
title: string;
total: number = 0;
current: number = 0;
startTime: number;
endTime: number;
status: string = "pending";
error?: string;
timeoutId?: NodeJS.Timeout;
run: () => Promise<void>;
constructor(key:string,title: string,run: () => Promise<void>){
this.key = key;
this.title = title;
Object.defineProperty(this, 'run', {
value: run,
writable: true,
enumerable: false, // 设置为false使其不可遍历
configurable: true
});
}
clearTimeout(){
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = null;
}
}
}