chore: 域名自动同步初步
@@ -124,6 +124,7 @@ export default defineConfig({
|
|||||||
{text: "子域名托管", link: "/guide/use/cert/subdomain.md"},
|
{text: "子域名托管", link: "/guide/use/cert/subdomain.md"},
|
||||||
{text: "流水线有效期", link: "/guide/use/pipeline/valid.md"},
|
{text: "流水线有效期", link: "/guide/use/pipeline/valid.md"},
|
||||||
{text: "IP证书申请", link: "/guide/use/cert/ip.md"},
|
{text: "IP证书申请", link: "/guide/use/cert/ip.md"},
|
||||||
|
{text: "插件开发", link: "/guide/use/dev/plugin.md"},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,19 @@
|
|||||||
|
# 插件开发
|
||||||
|
|
||||||
|
## 插件创建
|
||||||
|
点击自定义插件按钮,填写插件基本信息
|
||||||
|

|
||||||
|
|
||||||
|
创建成功后,会默认打开插件编辑页面,里面默认带有示例代码说明,可以在此基础上进行你的自定义开发
|
||||||
|

|
||||||
|
|
||||||
|
## 插件测试
|
||||||
|
|
||||||
|
在流水线中添加插件任务
|
||||||
|

|
||||||
|
|
||||||
|
配置插件任务参数
|
||||||
|

|
||||||
|
|
||||||
|
点击运行,查看插件任务运行结果
|
||||||
|

|
||||||
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 58 KiB |
@@ -0,0 +1,2 @@
|
|||||||
|
# 第三方登录配置
|
||||||
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# 用户有效期功能
|
||||||
|
|
||||||
|
可以为用户设置有效期,超过有效期后,用户的流水线将停止运行
|
||||||
|
|
||||||
|
## 开启用户有效期功能
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 设置用户有效期
|
||||||
|
|
||||||
|

|
||||||
@@ -11,11 +11,11 @@ export type PageSearch = {
|
|||||||
// sortOrder?: "asc" | "desc";
|
// sortOrder?: "asc" | "desc";
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PageRes = {
|
export type PageRes<T = any> = {
|
||||||
pageNo?: number;
|
pageNo?: number;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
total?: string;
|
total?: number;
|
||||||
list: any[];
|
list: T[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Pager {
|
export class Pager {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { HttpClient, ILogger, utils } from "@certd/basic";
|
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 & {
|
export type DnsProviderDefine = Registrable & {
|
||||||
accessType: string;
|
accessType: string;
|
||||||
@@ -28,6 +28,12 @@ export type DnsProviderContext = {
|
|||||||
serviceGetter: IServiceGetter;
|
serviceGetter: IServiceGetter;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DomainRecord = {
|
||||||
|
id: string;
|
||||||
|
domain: string;
|
||||||
|
thirdDns: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export interface IDnsProvider<T = any> {
|
export interface IDnsProvider<T = any> {
|
||||||
onInstance(): Promise<void>;
|
onInstance(): Promise<void>;
|
||||||
|
|
||||||
@@ -51,6 +57,8 @@ export interface IDnsProvider<T = any> {
|
|||||||
|
|
||||||
//中文域名是否需要punycode转码,如果返回True,则使用punycode来添加解析记录,否则使用中文域名添加解析记录
|
//中文域名是否需要punycode转码,如果返回True,则使用punycode来添加解析记录,否则使用中文域名添加解析记录
|
||||||
usePunyCode(): boolean;
|
usePunyCode(): boolean;
|
||||||
|
|
||||||
|
getDomainListPage(pager: Pager): Promise<PageRes<DomainRecord>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISubDomainsGetter {
|
export interface ISubDomainsGetter {
|
||||||
|
|||||||
@@ -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 { dnsProviderRegistry } from "./registry.js";
|
||||||
import { HttpClient, ILogger } from "@certd/basic";
|
import { HttpClient, ILogger } from "@certd/basic";
|
||||||
import punycode from "punycode.js";
|
import punycode from "punycode.js";
|
||||||
@@ -44,6 +45,10 @@ export abstract class AbstractDnsProvider<T = any> implements IDnsProvider<T> {
|
|||||||
abstract onInstance(): Promise<void>;
|
abstract onInstance(): Promise<void>;
|
||||||
|
|
||||||
abstract removeRecord(options: RemoveRecordOptions<T>): 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> {
|
export async function createDnsProvider(opts: { dnsProviderType: string; context: DnsProviderContext }): Promise<IDnsProvider> {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export default {
|
|||||||
title: "Framework",
|
title: "Framework",
|
||||||
home: "Home",
|
home: "Home",
|
||||||
},
|
},
|
||||||
|
helpDocLink: "Help Docs",
|
||||||
title: "Certificate Automation",
|
title: "Certificate Automation",
|
||||||
pipeline: "Pipeline",
|
pipeline: "Pipeline",
|
||||||
pipelineEdit: "Edit Pipeline",
|
pipelineEdit: "Edit Pipeline",
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export default {
|
|||||||
title: "框架",
|
title: "框架",
|
||||||
home: "首页",
|
home: "首页",
|
||||||
},
|
},
|
||||||
|
helpDocLink: "帮助文档",
|
||||||
title: "证书自动化",
|
title: "证书自动化",
|
||||||
pipeline: "证书自动化流水线",
|
pipeline: "证书自动化流水线",
|
||||||
pipelineEdit: "编辑流水线",
|
pipelineEdit: "编辑流水线",
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
<a-select-option value="ipv4first">{{ t("certd.ipv4Priority") }}</a-select-option>
|
<a-select-option value="ipv4first">{{ t("certd.ipv4Priority") }}</a-select-option>
|
||||||
<a-select-option value="ipv6first">{{ t("certd.ipv6Priority") }}</a-select-option>
|
<a-select-option value="ipv6first">{{ t("certd.ipv6Priority") }}</a-select-option>
|
||||||
</a-select>
|
</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>
|
||||||
|
|
||||||
<a-form-item :label="t('certd.sys.setting.showRunStrategy')" :name="['public', 'showRunStrategy']">
|
<a-form-item :label="t('certd.sys.setting.showRunStrategy')" :name="['public', 'showRunStrategy']">
|
||||||
|
|||||||
@@ -21,7 +21,10 @@
|
|||||||
<a-switch v-model:checked="formState.public.certDomainAddToMonitorEnabled" :disabled="!settingsStore.isPlus" />
|
<a-switch v-model:checked="formState.public.certDomainAddToMonitorEnabled" :disabled="!settingsStore.isPlus" />
|
||||||
<vip-button class="ml-5" mode="button"></vip-button>
|
<vip-button class="ml-5" mode="button"></vip-button>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<a-form-item :label="t('certd.sys.setting.fixedCertExpireDays')" :name="['public', 'fixedCertExpireDays']">
|
<a-form-item :label="t('certd.sys.setting.fixedCertExpireDays')" :name="['public', 'fixedCertExpireDays']">
|
||||||
|
|||||||
@@ -12,7 +12,10 @@
|
|||||||
<a-switch v-model:checked="formState.public.userValidTimeEnabled" :disabled="!settingsStore.isPlus" />
|
<a-switch v-model:checked="formState.public.userValidTimeEnabled" :disabled="!settingsStore.isPlus" />
|
||||||
<vip-button class="ml-5" mode="button"></vip-button>
|
<vip-button class="ml-5" mode="button"></vip-button>
|
||||||
</div>
|
</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>
|
</a-form-item>
|
||||||
<template v-if="formState.public.registerEnabled">
|
<template v-if="formState.public.registerEnabled">
|
||||||
<a-form-item :label="t('certd.enableUsernameRegistration')" :name="['public', 'usernameRegisterEnabled']">
|
<a-form-item :label="t('certd.enableUsernameRegistration')" :name="['public', 'usernameRegisterEnabled']">
|
||||||
|
|||||||
@@ -78,4 +78,15 @@ export class DomainController extends CrudController<DomainService> {
|
|||||||
return this.ok();
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,20 @@ import {In, Not, Repository} from 'typeorm';
|
|||||||
import {AccessService, BaseService} from '@certd/lib-server';
|
import {AccessService, BaseService} from '@certd/lib-server';
|
||||||
import {DomainEntity} from '../entity/domain.js';
|
import {DomainEntity} from '../entity/domain.js';
|
||||||
import {SubDomainService} from "../../pipeline/service/sub-domain-service.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 {DomainVerifiers} from "@certd/plugin-cert";
|
||||||
import { SubDomainsGetter } from '../../pipeline/service/getter/sub-domain-getter.js';
|
import { SubDomainsGetter } from '../../pipeline/service/getter/sub-domain-getter.js';
|
||||||
import { CnameRecordService } from '../../cname/service/cname-record-service.js';
|
import { CnameRecordService } from '../../cname/service/cname-record-service.js';
|
||||||
import { CnameRecordEntity } from "../../cname/entity/cname-record.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()
|
@Inject()
|
||||||
cnameRecordService: CnameRecordService;
|
cnameRecordService: CnameRecordService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
taskServiceBuilder: TaskServiceBuilder;
|
||||||
|
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
getRepository() {
|
getRepository() {
|
||||||
return this.repository;
|
return this.repository;
|
||||||
@@ -187,4 +198,79 @@ export class DomainService extends BaseService<DomainEntity> {
|
|||||||
|
|
||||||
return domainVerifiers;
|
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()
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||