chore: 域名自动同步初步
@@ -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"},
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
BIN
docs/guide/use/dev/images/plugin-create.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
docs/guide/use/dev/images/plugin-edit.png
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
docs/guide/use/dev/images/plugin-test1.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
docs/guide/use/dev/images/plugin-test2.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
docs/guide/use/dev/images/plugin-test3.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
19
docs/guide/use/dev/plugin.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# 插件开发
|
||||
|
||||
## 插件创建
|
||||
点击自定义插件按钮,填写插件基本信息
|
||||

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

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

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

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

|
||||
BIN
docs/guide/use/setting/images/user_valid_enable.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
docs/guide/use/setting/images/user_valid_set.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
2
docs/guide/use/setting/oauth.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# 第三方登录配置
|
||||
|
||||
11
docs/guide/use/setting/user-valid.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# 用户有效期功能
|
||||
|
||||
可以为用户设置有效期,超过有效期后,用户的流水线将停止运行
|
||||
|
||||
## 开启用户有效期功能
|
||||
|
||||

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

|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -36,6 +36,7 @@ export default {
|
||||
title: "Framework",
|
||||
home: "Home",
|
||||
},
|
||||
helpDocLink: "Help Docs",
|
||||
title: "Certificate Automation",
|
||||
pipeline: "Pipeline",
|
||||
pipelineEdit: "Edit Pipeline",
|
||||
|
||||
@@ -40,7 +40,7 @@ export default {
|
||||
title: "框架",
|
||||
home: "首页",
|
||||
},
|
||||
|
||||
helpDocLink: "帮助文档",
|
||||
title: "证书自动化",
|
||||
pipeline: "证书自动化流水线",
|
||||
pipelineEdit: "编辑流水线",
|
||||
|
||||
@@ -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']">
|
||||
|
||||
@@ -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']">
|
||||
|
||||
@@ -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']">
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||