mirror of
https://github.com/certd/certd.git
synced 2026-04-23 19:57:27 +08:00
Merge branch 'v2-dev' into v2-dev-buy
This commit is contained in:
@@ -27,7 +27,7 @@ const development = {
|
||||
},
|
||||
keys: 'certd',
|
||||
koa: {
|
||||
hostname:"::",
|
||||
hostname: "::",
|
||||
port: 7001,
|
||||
},
|
||||
https: {
|
||||
|
||||
@@ -20,9 +20,13 @@ import * as commercial from '@certd/commercial-core';
|
||||
import * as upload from '@midwayjs/upload';
|
||||
import { setLogger } from '@certd/acme-client';
|
||||
import {HiddenMiddleware} from "./middleware/hidden.js";
|
||||
|
||||
process.on('uncaughtException', error => {
|
||||
console.error('未捕获的异常:', error);
|
||||
// 在这里可以添加日志记录、发送错误通知等操作
|
||||
if(error?.message?.includes('address family not supported')){
|
||||
logger.error("您的服务器不支持监听IPV6格式的地址(::),请配置环境变量: certd_koa_hostname=0.0.0.0");
|
||||
}
|
||||
});
|
||||
|
||||
@Configuration({
|
||||
@@ -107,5 +111,6 @@ export class MainConfiguration {
|
||||
});
|
||||
|
||||
logger.info('当前环境:', this.app.getEnv()); // prod
|
||||
// throw new Error("address family not supported")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ALL, Body, Controller, Inject, Post, Provide, Query } from '@midwayjs/core';
|
||||
import { AccessService, Constants } from '@certd/lib-server';
|
||||
import { AccessController } from '../../user/pipeline/access-controller.js';
|
||||
import { checkComm } from '@certd/plus-core';
|
||||
import { ALL, Body, Controller, Inject, Post, Provide, Query } from "@midwayjs/core";
|
||||
import { AccessService, Constants } from "@certd/lib-server";
|
||||
import { AccessController } from "../../user/pipeline/access-controller.js";
|
||||
|
||||
/**
|
||||
* 授权
|
||||
@@ -17,7 +16,7 @@ export class SysAccessController extends AccessController {
|
||||
}
|
||||
|
||||
getUserId() {
|
||||
checkComm();
|
||||
// checkComm();
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { BaseController } from '@certd/lib-server';
|
||||
import { ALL, Body, Controller, Inject, Post, Provide } from '@midwayjs/core';
|
||||
import { NetTestService } from '../../../modules/sys/nettest/nettest-service.js';
|
||||
|
||||
|
||||
@Provide()
|
||||
@Controller('/api/sys/nettest/')
|
||||
export class SysNetTestController extends BaseController {
|
||||
|
||||
@Inject()
|
||||
netTestService: NetTestService;
|
||||
|
||||
|
||||
@Post('/domainResolve', { summary: 'sys:settings:view' })
|
||||
public async domainResolve(@Body(ALL) body: { domain: string }) {
|
||||
|
||||
const { domain } = body;
|
||||
const result = await this.netTestService.domainResolve(domain);
|
||||
return this.ok(result);
|
||||
}
|
||||
|
||||
// ping
|
||||
@Post('/ping', { summary: 'sys:settings:view' })
|
||||
public async ping(@Body(ALL) body: { domain: string }) {
|
||||
|
||||
const { domain } = body;
|
||||
const result = await this.netTestService.ping(domain);
|
||||
return this.ok(result);
|
||||
}
|
||||
|
||||
// telnet
|
||||
@Post('/telnet', { summary: 'sys:settings:view' })
|
||||
public async telnet(@Body(ALL) body: { domain: string, port: number }) {
|
||||
|
||||
const { domain, port } = body;
|
||||
const result = await this.netTestService.telnet(domain, port);
|
||||
return this.ok(result);
|
||||
}
|
||||
|
||||
// telnet
|
||||
@Post('/serverInfo', { summary: 'sys:settings:view' })
|
||||
public async serverInfo() {
|
||||
|
||||
const result = await this.netTestService.serverInfo();
|
||||
return this.ok(result);
|
||||
}
|
||||
}
|
||||
@@ -192,4 +192,11 @@ export class SysSettingsController extends CrudController<SysSettingsService> {
|
||||
await this.service.saveSetting(blankSetting);
|
||||
return this.ok({});
|
||||
}
|
||||
|
||||
|
||||
@Post("/captchaTest", { summary: "sys:settings:edit" })
|
||||
async captchaTest(@Body(ALL) body: any) {
|
||||
await this.codeService.checkCaptcha(body)
|
||||
return this.ok({});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,56 +11,59 @@ import {
|
||||
import { AuthService } from "../../../modules/sys/authority/service/auth-service.js";
|
||||
import { checkPlus } from "@certd/plus-core";
|
||||
import { http, logger, utils } from "@certd/basic";
|
||||
import { TaskServiceBuilder } from "../../../modules/pipeline/service/getter/task-service-getter.js";
|
||||
|
||||
/**
|
||||
* Addon
|
||||
*/
|
||||
@Provide()
|
||||
@Controller('/api/addon')
|
||||
@Controller("/api/addon")
|
||||
export class AddonController extends CrudController<AddonService> {
|
||||
@Inject()
|
||||
service: AddonService;
|
||||
@Inject()
|
||||
authService: AuthService;
|
||||
@Inject()
|
||||
taskServiceBuilder:TaskServiceBuilder
|
||||
|
||||
getService(): AddonService {
|
||||
return this.service;
|
||||
}
|
||||
|
||||
@Post('/page', { summary: Constants.per.authOnly })
|
||||
@Post("/page", { summary: Constants.per.authOnly })
|
||||
async page(@Body(ALL) body) {
|
||||
body.query = body.query ?? {};
|
||||
delete body.query.userId;
|
||||
const buildQuery = qb => {
|
||||
qb.andWhere('user_id = :userId', { userId: this.getUserId() });
|
||||
qb.andWhere("user_id = :userId", { userId: this.getUserId() });
|
||||
};
|
||||
const res = await this.service.page({
|
||||
query: body.query,
|
||||
page: body.page,
|
||||
sort: body.sort,
|
||||
buildQuery,
|
||||
buildQuery
|
||||
});
|
||||
return this.ok(res);
|
||||
}
|
||||
|
||||
@Post('/list', { summary: Constants.per.authOnly })
|
||||
@Post("/list", { summary: Constants.per.authOnly })
|
||||
async list(@Body(ALL) body) {
|
||||
body.query = body.query ?? {};
|
||||
body.query.userId = this.getUserId();
|
||||
return super.list(body);
|
||||
}
|
||||
|
||||
@Post('/add', { summary: Constants.per.authOnly })
|
||||
@Post("/add", { summary: Constants.per.authOnly })
|
||||
async add(@Body(ALL) bean) {
|
||||
bean.userId = this.getUserId();
|
||||
const type = bean.type;
|
||||
const addonType = bean.addonType;
|
||||
if (! type || !addonType){
|
||||
throw new ValidateException('请选择Addon类型');
|
||||
if (!type || !addonType) {
|
||||
throw new ValidateException("请选择Addon类型");
|
||||
}
|
||||
const define: AddonDefine = this.service.getDefineByType(type,addonType);
|
||||
const define: AddonDefine = this.service.getDefineByType(type, addonType);
|
||||
if (!define) {
|
||||
throw new ValidateException('Addon类型不存在');
|
||||
throw new ValidateException("Addon类型不存在");
|
||||
}
|
||||
if (define.needPlus) {
|
||||
checkPlus();
|
||||
@@ -68,19 +71,19 @@ export class AddonController extends CrudController<AddonService> {
|
||||
return super.add(bean);
|
||||
}
|
||||
|
||||
@Post('/update', { summary: Constants.per.authOnly })
|
||||
@Post("/update", { summary: Constants.per.authOnly })
|
||||
async update(@Body(ALL) bean) {
|
||||
await this.service.checkUserId(bean.id, this.getUserId());
|
||||
const old = await this.service.info(bean.id);
|
||||
if (!old) {
|
||||
throw new ValidateException('Addon配置不存在');
|
||||
throw new ValidateException("Addon配置不存在");
|
||||
}
|
||||
if (old.type !== bean.type ) {
|
||||
if (old.type !== bean.type) {
|
||||
const addonType = old.type;
|
||||
const type = bean.type;
|
||||
const define: AddonDefine = this.service.getDefineByType(type,addonType);
|
||||
const define: AddonDefine = this.service.getDefineByType(type, addonType);
|
||||
if (!define) {
|
||||
throw new ValidateException('Addon类型不存在');
|
||||
throw new ValidateException("Addon类型不存在");
|
||||
}
|
||||
if (define.needPlus) {
|
||||
checkPlus();
|
||||
@@ -89,26 +92,27 @@ export class AddonController extends CrudController<AddonService> {
|
||||
delete bean.userId;
|
||||
return super.update(bean);
|
||||
}
|
||||
@Post('/info', { summary: Constants.per.authOnly })
|
||||
async info(@Query('id') id: number) {
|
||||
|
||||
@Post("/info", { summary: Constants.per.authOnly })
|
||||
async info(@Query("id") id: number) {
|
||||
await this.service.checkUserId(id, this.getUserId());
|
||||
return super.info(id);
|
||||
}
|
||||
|
||||
@Post('/delete', { summary: Constants.per.authOnly })
|
||||
async delete(@Query('id') id: number) {
|
||||
@Post("/delete", { summary: Constants.per.authOnly })
|
||||
async delete(@Query("id") id: number) {
|
||||
await this.service.checkUserId(id, this.getUserId());
|
||||
return super.delete(id);
|
||||
}
|
||||
|
||||
@Post('/define', { summary: Constants.per.authOnly })
|
||||
async define(@Query('type') type: string,@Query('addonType') addonType: string) {
|
||||
const notification = this.service.getDefineByType(type,addonType);
|
||||
@Post("/define", { summary: Constants.per.authOnly })
|
||||
async define(@Query("type") type: string, @Query("addonType") addonType: string) {
|
||||
const notification = this.service.getDefineByType(type, addonType);
|
||||
return this.ok(notification);
|
||||
}
|
||||
|
||||
@Post('/getTypeDict', { summary: Constants.per.authOnly })
|
||||
async getTypeDict(@Query('addonType') addonType: string) {
|
||||
@Post("/getTypeDict", { summary: Constants.per.authOnly })
|
||||
async getTypeDict(@Query("addonType") addonType: string) {
|
||||
const list: any = this.service.getDefineList(addonType);
|
||||
let dict = [];
|
||||
for (const item of list) {
|
||||
@@ -116,7 +120,7 @@ export class AddonController extends CrudController<AddonService> {
|
||||
value: item.name,
|
||||
label: item.title,
|
||||
needPlus: item.needPlus ?? false,
|
||||
icon: item.icon,
|
||||
icon: item.icon
|
||||
});
|
||||
}
|
||||
dict = dict.sort(a => {
|
||||
@@ -125,13 +129,13 @@ export class AddonController extends CrudController<AddonService> {
|
||||
return this.ok(dict);
|
||||
}
|
||||
|
||||
@Post('/simpleInfo', { summary: Constants.per.authOnly })
|
||||
async simpleInfo(@Query('addonType') addonType: string,@Query('id') id: number) {
|
||||
@Post("/simpleInfo", { summary: Constants.per.authOnly })
|
||||
async simpleInfo(@Query("addonType") addonType: string, @Query("id") id: number) {
|
||||
if (id === 0) {
|
||||
//获取默认
|
||||
const res = await this.service.getDefault(this.getUserId(),addonType);
|
||||
const res = await this.service.getDefault(this.getUserId(), addonType);
|
||||
if (!res) {
|
||||
throw new ValidateException('默认Addon配置不存在');
|
||||
throw new ValidateException("默认Addon配置不存在");
|
||||
}
|
||||
const simple = await this.service.getSimpleInfo(res.id);
|
||||
return this.ok(simple);
|
||||
@@ -141,27 +145,27 @@ export class AddonController extends CrudController<AddonService> {
|
||||
return this.ok(res);
|
||||
}
|
||||
|
||||
@Post('/getDefaultId', { summary: Constants.per.authOnly })
|
||||
async getDefaultId(@Query('addonType') addonType: string) {
|
||||
const res = await this.service.getDefault(this.getUserId(),addonType);
|
||||
@Post("/getDefaultId", { summary: Constants.per.authOnly })
|
||||
async getDefaultId(@Query("addonType") addonType: string) {
|
||||
const res = await this.service.getDefault(this.getUserId(), addonType);
|
||||
return this.ok(res?.id);
|
||||
}
|
||||
|
||||
@Post('/setDefault', { summary: Constants.per.authOnly })
|
||||
async setDefault(@Query('addonType') addonType: string,@Query('id') id: number) {
|
||||
@Post("/setDefault", { summary: Constants.per.authOnly })
|
||||
async setDefault(@Query("addonType") addonType: string, @Query("id") id: number) {
|
||||
await this.service.checkUserId(id, this.getUserId());
|
||||
const res = await this.service.setDefault(id, this.getUserId(),addonType);
|
||||
const res = await this.service.setDefault(id, this.getUserId(), addonType);
|
||||
return this.ok(res);
|
||||
}
|
||||
|
||||
|
||||
@Post('/options', { summary: Constants.per.authOnly })
|
||||
async options(@Query('addonType') addonType: string) {
|
||||
@Post("/options", { summary: Constants.per.authOnly })
|
||||
async options(@Query("addonType") addonType: string) {
|
||||
const res = await this.service.list({
|
||||
query: {
|
||||
userId: this.getUserId(),
|
||||
addonType
|
||||
},
|
||||
}
|
||||
});
|
||||
for (const item of res) {
|
||||
delete item.setting;
|
||||
@@ -170,7 +174,7 @@ export class AddonController extends CrudController<AddonService> {
|
||||
}
|
||||
|
||||
|
||||
@Post('/handle', { summary: Constants.per.authOnly })
|
||||
@Post("/handle", { summary: Constants.per.authOnly })
|
||||
async handle(@Body(ALL) body: AddonRequestHandleReq) {
|
||||
const userId = this.getUserId();
|
||||
let inputAddon = body.input.addon;
|
||||
@@ -178,21 +182,24 @@ export class AddonController extends CrudController<AddonService> {
|
||||
const oldEntity = await this.service.info(body.input.id);
|
||||
if (oldEntity) {
|
||||
if (oldEntity.userId !== userId) {
|
||||
throw new Error('addon not found');
|
||||
throw new Error("addon not found");
|
||||
}
|
||||
// const param: any = {
|
||||
// type: body.typeName,
|
||||
// setting: JSON.stringify(body.input.access),
|
||||
// };
|
||||
inputAddon = JSON.parse( oldEntity.setting)
|
||||
inputAddon = JSON.parse(oldEntity.setting);
|
||||
}
|
||||
}
|
||||
const serviceGetter = this.taskServiceBuilder.create({ userId });
|
||||
|
||||
const ctx = {
|
||||
http: http,
|
||||
logger:logger,
|
||||
utils:utils,
|
||||
}
|
||||
const addon = await newAddon(body.addonType,body.typeName, inputAddon,ctx);
|
||||
logger: logger,
|
||||
utils: utils,
|
||||
serviceGetter
|
||||
};
|
||||
const addon = await newAddon(body.addonType, body.typeName, inputAddon, ctx);
|
||||
const res = await addon.onRequest(body);
|
||||
return this.ok(res);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { ALL, Body, Controller, Inject, Post, Provide, Query } from '@midwayjs/core';
|
||||
import { Constants, CrudController } from '@certd/lib-server';
|
||||
import { AuthService } from '../../../modules/sys/authority/service/auth-service.js';
|
||||
import { GroupService } from '../../../modules/basic/service/group-service.js';
|
||||
|
||||
/**
|
||||
* 通知
|
||||
*/
|
||||
@Provide()
|
||||
@Controller('/api/basic/group')
|
||||
export class GroupController extends CrudController<GroupService> {
|
||||
@Inject()
|
||||
service: GroupService;
|
||||
@Inject()
|
||||
authService: AuthService;
|
||||
|
||||
getService(): GroupService {
|
||||
return this.service;
|
||||
}
|
||||
|
||||
@Post('/page', { summary: Constants.per.authOnly })
|
||||
async page(@Body(ALL) body: any) {
|
||||
body.query = body.query ?? {};
|
||||
delete body.query.userId;
|
||||
const buildQuery = qb => {
|
||||
qb.andWhere('user_id = :userId', { userId: this.getUserId() });
|
||||
};
|
||||
const res = await this.service.page({
|
||||
query: body.query,
|
||||
page: body.page,
|
||||
sort: body.sort,
|
||||
buildQuery,
|
||||
});
|
||||
return this.ok(res);
|
||||
}
|
||||
|
||||
@Post('/list', { summary: Constants.per.authOnly })
|
||||
async list(@Body(ALL) body: any) {
|
||||
body.query = body.query ?? {};
|
||||
body.query.userId = this.getUserId();
|
||||
return await super.list(body);
|
||||
}
|
||||
|
||||
@Post('/add', { summary: Constants.per.authOnly })
|
||||
async add(@Body(ALL) bean: any) {
|
||||
bean.userId = this.getUserId();
|
||||
return await super.add(bean);
|
||||
}
|
||||
|
||||
@Post('/update', { summary: Constants.per.authOnly })
|
||||
async update(@Body(ALL) bean) {
|
||||
await this.service.checkUserId(bean.id, this.getUserId());
|
||||
delete bean.userId;
|
||||
return await super.update(bean);
|
||||
}
|
||||
@Post('/info', { summary: Constants.per.authOnly })
|
||||
async info(@Query('id') id: number) {
|
||||
await this.service.checkUserId(id, this.getUserId());
|
||||
return await super.info(id);
|
||||
}
|
||||
|
||||
@Post('/delete', { summary: Constants.per.authOnly })
|
||||
async delete(@Query('id') id: number) {
|
||||
await this.service.checkUserId(id, this.getUserId());
|
||||
return await super.delete(id);
|
||||
}
|
||||
|
||||
@Post('/all', { summary: Constants.per.authOnly })
|
||||
async all(@Query('type') type: string) {
|
||||
const list: any = await this.service.find({
|
||||
where: {
|
||||
userId: this.getUserId(),
|
||||
type,
|
||||
},
|
||||
});
|
||||
return this.ok(list);
|
||||
}
|
||||
}
|
||||
@@ -85,10 +85,18 @@ export class CnameRecordController extends CrudController<CnameRecordService> {
|
||||
}
|
||||
|
||||
@Post('/verify', { summary: Constants.per.authOnly })
|
||||
async verify(@Body(ALL) body: { id: string }) {
|
||||
async verify(@Body(ALL) body: { id: number }) {
|
||||
const userId = this.getUserId();
|
||||
await this.service.checkUserId(body.id, userId);
|
||||
const res = await this.service.verify(body.id);
|
||||
return this.ok(res);
|
||||
}
|
||||
|
||||
@Post('/resetStatus', { summary: Constants.per.authOnly })
|
||||
async resetStatus(@Body(ALL) body: { id: number }) {
|
||||
const userId = this.getUserId();
|
||||
await this.service.checkUserId(body.id, userId);
|
||||
const res = await this.service.resetStatus(body.id);
|
||||
return this.ok(res);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +92,14 @@ export class SiteInfoController extends CrudController<SiteInfoService> {
|
||||
return await super.delete(id);
|
||||
}
|
||||
|
||||
|
||||
@Post('/batchDelete', { summary: Constants.per.authOnly })
|
||||
async batchDelete(@Body(ALL) body: any) {
|
||||
const userId = this.getUserId();
|
||||
await this.service.batchDelete(body.ids,userId);
|
||||
return this.ok();
|
||||
}
|
||||
|
||||
@Post('/check', { summary: Constants.per.authOnly })
|
||||
async check(@Body('id') id: number) {
|
||||
await this.service.checkUserId(id, this.getUserId());
|
||||
@@ -111,6 +119,7 @@ export class SiteInfoController extends CrudController<SiteInfoService> {
|
||||
const userId = this.getUserId();
|
||||
await this.service.doImport({
|
||||
text:body.text,
|
||||
groupId:body.groupId,
|
||||
userId
|
||||
})
|
||||
return this.ok();
|
||||
|
||||
@@ -107,6 +107,9 @@ export class NotificationController extends CrudController<NotificationService>
|
||||
icon: item.icon,
|
||||
});
|
||||
}
|
||||
dict = dict.sort(a => {
|
||||
return a.order ? 0 : -1;
|
||||
});
|
||||
dict = dict.sort(a => {
|
||||
return a.needPlus ? 0 : -1;
|
||||
});
|
||||
|
||||
@@ -4,6 +4,8 @@ import { PipelineService } from '../../../modules/pipeline/service/pipeline-serv
|
||||
import { PipelineEntity } from '../../../modules/pipeline/entity/pipeline.js';
|
||||
import { HistoryService } from '../../../modules/pipeline/service/history-service.js';
|
||||
import { AuthService } from '../../../modules/sys/authority/service/auth-service.js';
|
||||
import { SiteInfoService } from '../../../modules/monitor/index.js';
|
||||
import { isPlus } from '@certd/plus-core';
|
||||
|
||||
/**
|
||||
* 证书
|
||||
@@ -20,6 +22,9 @@ export class PipelineController extends CrudController<PipelineService> {
|
||||
@Inject()
|
||||
sysSettingsService: SysSettingsService;
|
||||
|
||||
@Inject()
|
||||
siteInfoService: SiteInfoService;
|
||||
|
||||
getService() {
|
||||
return this.service;
|
||||
}
|
||||
@@ -74,13 +79,30 @@ export class PipelineController extends CrudController<PipelineService> {
|
||||
}
|
||||
|
||||
@Post('/save', { summary: Constants.per.authOnly })
|
||||
async save(@Body(ALL) bean: PipelineEntity) {
|
||||
async save(@Body(ALL) bean: {addToMonitorEnabled: boolean, addToMonitorDomains: string} & PipelineEntity) {
|
||||
if (bean.id > 0) {
|
||||
await this.authService.checkEntityUserId(this.ctx, this.getService(), bean.id);
|
||||
} else {
|
||||
bean.userId = this.getUserId();
|
||||
}
|
||||
|
||||
if(!this.isAdmin()){
|
||||
// 非管理员用户 不允许设置流水线有效期
|
||||
delete bean.validTime
|
||||
}
|
||||
|
||||
await this.service.save(bean);
|
||||
//是否增加证书监控
|
||||
if (bean.addToMonitorEnabled && bean.addToMonitorDomains) {
|
||||
const sysPublicSettings = await this.sysSettingsService.getPublicSettings();
|
||||
if (isPlus() && sysPublicSettings.certDomainAddToMonitorEnabled) {
|
||||
//增加证书监控
|
||||
await this.siteInfoService.doImport({
|
||||
text: bean.addToMonitorDomains,
|
||||
userId: this.getUserId(),
|
||||
});
|
||||
}
|
||||
}
|
||||
return this.ok(bean.id);
|
||||
}
|
||||
|
||||
@@ -101,7 +123,7 @@ export class PipelineController extends CrudController<PipelineService> {
|
||||
@Post('/trigger', { summary: Constants.per.authOnly })
|
||||
async trigger(@Query('id') id: number, @Query('stepId') stepId?: string) {
|
||||
await this.authService.checkEntityUserId(this.ctx, this.getService(), id);
|
||||
await this.service.trigger(id, stepId);
|
||||
await this.service.trigger(id, stepId,true);
|
||||
return this.ok({});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {ALL, Body, Controller, Inject, Post, Provide, Query} from '@midwayjs/core';
|
||||
import {Constants, CrudController} from '@certd/lib-server';
|
||||
import {SubDomainService} from "../../../modules/pipeline/service/sub-domain-service.js";
|
||||
import {DomainParser} from '@certd/plugin-cert/dist/dns-provider/domain-parser.js';
|
||||
import {DomainParser} from '@certd/plugin-cert';
|
||||
import { SubDomainsGetter } from '../../../modules/pipeline/service/getter/sub-domain-getter.js';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Autoload, Config, Init, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import { IMidwayKoaContext, IWebMiddleware, NextFunction } from '@midwayjs/koa';
|
||||
import { CommonException } from '@certd/lib-server';
|
||||
import { CommonException, SysSettingsService } from "@certd/lib-server";
|
||||
import { UserService } from '../../modules/sys/authority/service/user-service.js';
|
||||
import { logger } from '@certd/basic';
|
||||
import {UserSettingsService} from "../../modules/mine/service/user-settings-service.js";
|
||||
@@ -17,6 +17,8 @@ export class ResetPasswdMiddleware implements IWebMiddleware {
|
||||
|
||||
@Inject()
|
||||
userSettingsService: UserSettingsService;
|
||||
@Inject()
|
||||
sysSettingsService: SysSettingsService;
|
||||
|
||||
@Config('system.resetAdminPasswd')
|
||||
private resetAdminPasswd: boolean;
|
||||
@@ -40,8 +42,12 @@ export class ResetPasswdMiddleware implements IWebMiddleware {
|
||||
userId: 1,
|
||||
key:"user.two.factor"
|
||||
})
|
||||
const publicSettings = await this.sysSettingsService.getPublicSettings()
|
||||
publicSettings.captchaEnabled = false
|
||||
await this.sysSettingsService.savePublicSettings(publicSettings);
|
||||
|
||||
const user = await this.userService.info(1);
|
||||
logger.info(`重置1号管理员用户的密码完成,2FA设置已删除,用户名:${user.username},新密码:${newPasswd},请在登录进去之后尽快修改密码`);
|
||||
logger.info(`重置1号管理员用户的密码完成,2FA设置已删除,验证码登录已禁用,用户名:${user.username},新密码:${newPasswd},请在登录进去之后尽快修改密码`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Autoload, Init, Inject, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import { CertInfoService } from "../monitor/index.js";
|
||||
import { pipelineEmitter } from "@certd/pipeline";
|
||||
import { CertInfo, EVENT_CERT_APPLY_SUCCESS } from "@certd/plugin-cert";
|
||||
import { PipelineEvent } from "@certd/pipeline/dist/service/emit.js";
|
||||
import { PipelineEvent } from "@certd/pipeline";
|
||||
|
||||
@Autoload()
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
|
||||
@@ -19,6 +19,8 @@ export class AutoZPrint {
|
||||
|
||||
@Config('https')
|
||||
httpsConfig: HttpsServerOptions;
|
||||
@Config('koa')
|
||||
koaConfig: any;
|
||||
|
||||
@Init()
|
||||
async init() {
|
||||
@@ -58,6 +60,7 @@ export class AutoZPrint {
|
||||
httpsServer.start({
|
||||
...this.httpsConfig,
|
||||
app: this.app,
|
||||
hostname: this.httpsConfig.hostname || this.koaConfig.hostname,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {logger, safePromise} from '@certd/basic';
|
||||
export type HttpsServerOptions = {
|
||||
enabled: boolean;
|
||||
app?: Application;
|
||||
hostname?: string;
|
||||
port: number;
|
||||
key: string;
|
||||
cert: string;
|
||||
@@ -58,7 +59,7 @@ export class HttpsServer {
|
||||
opts.app.callback()
|
||||
);
|
||||
this.server = httpServer;
|
||||
const hostname = '::';
|
||||
let hostname = opts.hostname || '::';
|
||||
// A function that runs in the context of the http server
|
||||
// and reports what type of server listens on which port
|
||||
function listeningReporter() {
|
||||
@@ -70,7 +71,19 @@ export class HttpsServer {
|
||||
httpServer.listen(opts.port, hostname, listeningReporter);
|
||||
return httpServer;
|
||||
} catch (e) {
|
||||
logger.error('启动https服务失败', e);
|
||||
if ( e.message?.includes("address family not supported")) {
|
||||
hostname = "0.0.0.0"
|
||||
logger.error(`${e.message},尝试监听${hostname}`, e);
|
||||
try{
|
||||
httpServer.listen(opts.port, hostname, listeningReporter);
|
||||
return httpServer;
|
||||
}catch (e) {
|
||||
logger.error('启动https服务失败', e);
|
||||
}
|
||||
}else{
|
||||
logger.error('启动https服务失败', e);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
export const GROUP_TYPE_SITE = 'site';
|
||||
|
||||
@Entity('cd_group')
|
||||
export class GroupEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ name: 'user_id', comment: '用户id' })
|
||||
userId: number;
|
||||
|
||||
@Column({ name: 'name', comment: '分组名称' })
|
||||
name: string;
|
||||
|
||||
@Column({ name: 'icon', comment: '图标' })
|
||||
icon: string;
|
||||
|
||||
@Column({ name: 'favorite', comment: '收藏' })
|
||||
favorite: boolean;
|
||||
|
||||
@Column({ name: 'type', comment: '类型', length: 512 })
|
||||
type: string;
|
||||
|
||||
@Column({
|
||||
name: 'create_time',
|
||||
comment: '创建时间',
|
||||
default: () => 'CURRENT_TIMESTAMP',
|
||||
})
|
||||
createTime: Date;
|
||||
|
||||
@Column({
|
||||
name: 'update_time',
|
||||
comment: '修改时间',
|
||||
default: () => 'CURRENT_TIMESTAMP',
|
||||
})
|
||||
updateTime: Date;
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import { AddonService, SysSettingsService } from "@certd/lib-server";
|
||||
import { SysSettingsService } from "@certd/lib-server";
|
||||
import { logger } from "@certd/basic";
|
||||
import { ICaptchaAddon } from "../../../plugins/plugin-captcha/api.js";
|
||||
import { AddonGetterService } from "../../pipeline/service/addon-getter-service.js";
|
||||
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
@@ -9,45 +10,48 @@ export class CaptchaService {
|
||||
@Inject()
|
||||
sysSettingsService: SysSettingsService;
|
||||
@Inject()
|
||||
addonService: AddonService;
|
||||
addonGetterService: AddonGetterService;
|
||||
|
||||
|
||||
async getCaptcha(captchaAddonId?:number){
|
||||
async getCaptcha(captchaAddonId?: number) {
|
||||
if (!captchaAddonId) {
|
||||
const settings = await this.sysSettingsService.getPublicSettings()
|
||||
captchaAddonId = settings.captchaAddonId ?? 0
|
||||
const settings = await this.sysSettingsService.getPublicSettings();
|
||||
captchaAddonId = settings.captchaAddonId ?? 0;
|
||||
}
|
||||
const addon:ICaptchaAddon = await this.addonService.getAddonById(captchaAddonId,true,0)
|
||||
const addon: ICaptchaAddon = await this.addonGetterService.getAddonById(captchaAddonId, true, 0, {
|
||||
type: "captcha",
|
||||
name: "image"
|
||||
});
|
||||
if (!addon) {
|
||||
throw new Error('验证码插件还未配置')
|
||||
throw new Error("验证码插件还未配置");
|
||||
}
|
||||
return await addon.getCaptcha()
|
||||
return await addon.getCaptcha();
|
||||
}
|
||||
|
||||
|
||||
async doValidate(opts:{form:any,must?:boolean,captchaAddonId?:number}){
|
||||
async doValidate(opts: { form: any, must?: boolean, captchaAddonId?: number }) {
|
||||
if (!opts.captchaAddonId) {
|
||||
const settings = await this.sysSettingsService.getPublicSettings()
|
||||
opts.captchaAddonId = settings.captchaAddonId ?? 0
|
||||
const settings = await this.sysSettingsService.getPublicSettings();
|
||||
opts.captchaAddonId = settings.captchaAddonId ?? 0;
|
||||
}
|
||||
const addon = await this.addonService.getById(opts.captchaAddonId,0)
|
||||
const addon = await this.addonGetterService.getById(opts.captchaAddonId, 0);
|
||||
if (!addon) {
|
||||
if (opts.must) {
|
||||
throw new Error('请先配置验证码插件');
|
||||
throw new Error("请先配置验证码插件");
|
||||
}
|
||||
logger.warn('验证码插件还未配置,忽略验证码校验')
|
||||
return true
|
||||
logger.warn("验证码插件还未配置,忽略验证码校验");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!opts.form) {
|
||||
throw new Error('请输入验证码');
|
||||
throw new Error("请输入验证码");
|
||||
}
|
||||
const res = await addon.onValidate(opts.form)
|
||||
const res = await addon.onValidate(opts.form);
|
||||
if (!res) {
|
||||
throw new Error('验证码错误');
|
||||
throw new Error("验证码错误");
|
||||
}
|
||||
|
||||
return true
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { cache, isDev, randomNumber } from '@certd/basic';
|
||||
import { SysSettingsService, SysSiteInfo } from '@certd/lib-server';
|
||||
import { SmsServiceFactory } from '../sms/factory.js';
|
||||
import { ISmsService } from '../sms/api.js';
|
||||
import { CodeErrorException } from '@certd/lib-server/dist/basic/exception/code-error-exception.js';
|
||||
import { CodeErrorException } from '@certd/lib-server';
|
||||
import { EmailService } from './email-service.js';
|
||||
import { AccessService } from '@certd/lib-server';
|
||||
import { AccessSysGetter } from '@certd/lib-server';
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import { BaseService } from '@certd/lib-server';
|
||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { merge } from 'lodash-es';
|
||||
import { GroupEntity } from '../entity/group.js';
|
||||
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
export class GroupService extends BaseService<GroupEntity> {
|
||||
@InjectEntityModel(GroupEntity)
|
||||
repository: Repository<GroupEntity>;
|
||||
|
||||
//@ts-ignore
|
||||
getRepository() {
|
||||
return this.repository;
|
||||
}
|
||||
|
||||
async add(bean: any) {
|
||||
if (!bean.type) {
|
||||
throw new Error('type is required');
|
||||
}
|
||||
bean = merge(
|
||||
{
|
||||
favorite: false,
|
||||
},
|
||||
bean
|
||||
);
|
||||
return await this.repository.save(bean);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ 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/dist/dns-provider/domain-parser.js";
|
||||
import {DomainParser} from "@certd/plugin-cert";
|
||||
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';
|
||||
|
||||
@@ -13,6 +13,8 @@ export class CnameRecordEntity {
|
||||
|
||||
@Column({ comment: '证书申请域名', length: 100 })
|
||||
domain: string;
|
||||
@Column({ comment: '主域名', name: 'main_domain', length: 100 })
|
||||
mainDomain:string;
|
||||
|
||||
@Column({ comment: '主机记录', name: 'host_record', length: 100 })
|
||||
hostRecord: string;
|
||||
|
||||
@@ -17,7 +17,7 @@ import { getAuthoritativeDnsResolver, walkTxtRecord } from "@certd/acme-client";
|
||||
import { CnameProviderService } from "./cname-provider-service.js";
|
||||
import { CnameProviderEntity } from "../entity/cname-provider.js";
|
||||
import { CommonDnsProvider } from "./common-provider.js";
|
||||
import { DomainParser } from "@certd/plugin-cert/dist/dns-provider/domain-parser.js";
|
||||
import { DomainParser } from "@certd/plugin-cert";
|
||||
import punycode from "punycode.js";
|
||||
import { SubDomainService } from "../../pipeline/service/sub-domain-service.js";
|
||||
import { SubDomainsGetter } from "../../pipeline/service/getter/sub-domain-getter.js";
|
||||
@@ -37,7 +37,7 @@ type CnameCheckCacheValue = {
|
||||
* 授权
|
||||
*/
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Request, {allowDowngrade: true})
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
export class CnameRecordService extends BaseService<CnameRecordEntity> {
|
||||
@InjectEntityModel(CnameRecordEntity)
|
||||
repository: Repository<CnameRecordEntity>;
|
||||
@@ -71,16 +71,16 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
|
||||
*/
|
||||
async add(param: any): Promise<CnameRecordEntity> {
|
||||
if (!param.domain) {
|
||||
throw new ValidateException('域名不能为空');
|
||||
throw new ValidateException("域名不能为空");
|
||||
}
|
||||
if (!param.userId) {
|
||||
throw new ValidateException('userId不能为空');
|
||||
throw new ValidateException("userId不能为空");
|
||||
}
|
||||
if (param.domain.startsWith('*.')) {
|
||||
if (param.domain.startsWith("*.")) {
|
||||
param.domain = param.domain.substring(2);
|
||||
}
|
||||
param.domain = param.domain.trim()
|
||||
const info = await this.getRepository().findOne({where: {domain: param.domain, userId: param.userId}});
|
||||
param.domain = param.domain.trim();
|
||||
const info = await this.getRepository().findOne({ where: { domain: param.domain, userId: param.userId } });
|
||||
if (info) {
|
||||
return info;
|
||||
}
|
||||
@@ -90,63 +90,64 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
|
||||
//获取默认的cnameProviderId
|
||||
cnameProvider = await this.cnameProviderService.getByPriority();
|
||||
if (cnameProvider == null) {
|
||||
throw new ValidateException('找不到CNAME服务,请先前往“系统管理->CNAME服务设置”添加CNAME服务');
|
||||
throw new ValidateException("找不到CNAME服务,请先前往“系统管理->CNAME服务设置”添加CNAME服务");
|
||||
}
|
||||
} else {
|
||||
cnameProvider = await this.cnameProviderService.info(param.cnameProviderId);
|
||||
}
|
||||
await this.cnameProviderChanged(param.userId, param, cnameProvider);
|
||||
|
||||
param.status = 'cname';
|
||||
const {id} = await super.add(param);
|
||||
param.status = "cname";
|
||||
const { id } = await super.add(param);
|
||||
return await this.info(id);
|
||||
}
|
||||
|
||||
private async cnameProviderChanged(userId: number, param: any, cnameProvider: CnameProviderEntity) {
|
||||
param.cnameProviderId = cnameProvider.id;
|
||||
|
||||
const subDomainGetter = new SubDomainsGetter(userId, this.subDomainService)
|
||||
const subDomainGetter = new SubDomainsGetter(userId, this.subDomainService);
|
||||
const domainParser = new DomainParser(subDomainGetter);
|
||||
|
||||
const realDomain = await domainParser.parse(param.domain);
|
||||
const prefix = param.domain.replace(realDomain, '');
|
||||
const prefix = param.domain.replace(realDomain, "");
|
||||
let hostRecord = `_acme-challenge.${prefix}`;
|
||||
if (hostRecord.endsWith('.')) {
|
||||
if (hostRecord.endsWith(".")) {
|
||||
hostRecord = hostRecord.substring(0, hostRecord.length - 1);
|
||||
}
|
||||
param.hostRecord = hostRecord;
|
||||
param.mainDomain = realDomain;
|
||||
|
||||
const randomKey = utils.id.simpleNanoId(6).toLowerCase();
|
||||
|
||||
const userIdHex = utils.hash.toHex(userId)
|
||||
let userKeyHash = ""
|
||||
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo)
|
||||
userKeyHash = `${installInfo.siteId}_${userIdHex}_${randomKey}`
|
||||
userKeyHash = utils.hash.md5(userKeyHash).substring(0, 10)
|
||||
logger.info(`userKeyHash:${userKeyHash},subjectId:${installInfo.siteId},randomKey:${randomKey},userIdHex:${userIdHex}`)
|
||||
const userIdHex = utils.hash.toHex(userId);
|
||||
let userKeyHash = "";
|
||||
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
|
||||
userKeyHash = `${installInfo.siteId}_${userIdHex}_${randomKey}`;
|
||||
userKeyHash = utils.hash.md5(userKeyHash).substring(0, 10);
|
||||
logger.info(`userKeyHash:${userKeyHash},subjectId:${installInfo.siteId},randomKey:${randomKey},userIdHex:${userIdHex}`);
|
||||
const cnameKey = `${userKeyHash}-${userIdHex}-${randomKey}`;
|
||||
const safeDomain = param.domain.replaceAll('.', '-');
|
||||
const safeDomain = param.domain.replaceAll(".", "-");
|
||||
param.recordValue = `${safeDomain}.${cnameKey}.${cnameProvider.domain}`;
|
||||
}
|
||||
|
||||
async update(param: any) {
|
||||
if (!param.id) {
|
||||
throw new ValidateException('id不能为空');
|
||||
throw new ValidateException("id不能为空");
|
||||
}
|
||||
//hostRecord包含所有权校验信息,不允许用户修改hostRecord
|
||||
delete param.hostRecord
|
||||
|
||||
const old = await this.info(param.id);
|
||||
if (!old) {
|
||||
throw new ValidateException('数据不存在');
|
||||
throw new ValidateException("数据不存在");
|
||||
}
|
||||
if (old.domain !== param.domain) {
|
||||
throw new ValidateException('域名不允许修改');
|
||||
if (param.domain && old.domain !== param.domain) {
|
||||
throw new ValidateException("域名不允许修改");
|
||||
}
|
||||
if (old.cnameProviderId !== param.cnameProviderId) {
|
||||
if (param.cnameProviderId && old.cnameProviderId !== param.cnameProviderId) {
|
||||
const cnameProvider = await this.cnameProviderService.info(param.cnameProviderId);
|
||||
await this.cnameProviderChanged(old.userId, param, cnameProvider);
|
||||
param.status = 'cname';
|
||||
param.status = "cname";
|
||||
}
|
||||
return await super.update(param);
|
||||
}
|
||||
@@ -171,7 +172,7 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
|
||||
} else {
|
||||
record.commonDnsProvider = new CommonDnsProvider({
|
||||
config: record.cnameProvider,
|
||||
plusService: this.plusService,
|
||||
plusService: this.plusService
|
||||
});
|
||||
}
|
||||
|
||||
@@ -180,19 +181,22 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
|
||||
|
||||
async getByDomain(domain: string, userId: number, createOnNotFound = true) {
|
||||
if (!domain) {
|
||||
throw new ValidateException('domain不能为空');
|
||||
throw new ValidateException("domain不能为空");
|
||||
}
|
||||
if (userId == null) {
|
||||
throw new ValidateException('userId不能为空');
|
||||
throw new ValidateException("userId不能为空");
|
||||
}
|
||||
let record = await this.getRepository().findOne({where: {domain, userId}});
|
||||
let record = await this.getRepository().findOne({ where: { domain, userId } });
|
||||
if (record == null) {
|
||||
if (createOnNotFound) {
|
||||
record = await this.add({domain, userId});
|
||||
record = await this.add({ domain, userId });
|
||||
} else {
|
||||
throw new ValidateException(`找不到${domain}的CNAME记录`);
|
||||
}
|
||||
}
|
||||
|
||||
await this.fillMainDomain(record);
|
||||
|
||||
const provider = await this.cnameProviderService.info(record.cnameProviderId);
|
||||
if (provider == null) {
|
||||
throw new ValidateException(`找不到${domain}的CNAME服务`);
|
||||
@@ -201,25 +205,53 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
|
||||
return {
|
||||
...record,
|
||||
cnameProvider: {
|
||||
...provider,
|
||||
} as CnameProvider,
|
||||
...provider
|
||||
} as CnameProvider
|
||||
} as CnameRecord;
|
||||
}
|
||||
|
||||
async fillMainDomain(record: CnameRecordEntity, update = true) {
|
||||
const notMainDomain = !record.mainDomain;
|
||||
const hasErrorMainDomain = record.mainDomain && !record.mainDomain.includes(".");
|
||||
if (notMainDomain || hasErrorMainDomain) {
|
||||
let domainPrefix = record.hostRecord.replace("_acme-challenge", "");
|
||||
if (domainPrefix.startsWith(".")) {
|
||||
domainPrefix = domainPrefix.substring(1);
|
||||
}
|
||||
|
||||
if (domainPrefix) {
|
||||
const prefixStr = domainPrefix + ".";
|
||||
record.mainDomain = record.domain.substring(prefixStr.length);
|
||||
}else{
|
||||
record.mainDomain = record.domain;
|
||||
}
|
||||
|
||||
if (update) {
|
||||
await this.update({
|
||||
id: record.id,
|
||||
mainDomain: record.mainDomain
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证是否配置好cname
|
||||
* @param id
|
||||
*/
|
||||
async verify(id: string) {
|
||||
async verify(id: number) {
|
||||
const bean = await this.info(id);
|
||||
if (!bean) {
|
||||
throw new ValidateException(`CnameRecord:${id} 不存在`);
|
||||
}
|
||||
if (bean.status === 'valid') {
|
||||
if (bean.status === "valid") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const subDomainGetter = new SubDomainsGetter(bean.userId, this.subDomainService)
|
||||
await this.getByDomain(bean.domain, bean.userId);
|
||||
|
||||
const subDomainGetter = new SubDomainsGetter(bean.userId, this.subDomainService);
|
||||
const domainParser = new DomainParser(subDomainGetter);
|
||||
|
||||
const cacheKey = `cname.record.verify.${bean.id}`;
|
||||
@@ -229,7 +261,7 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
|
||||
value = {
|
||||
validating: false,
|
||||
pass: false,
|
||||
startTime: new Date().getTime(),
|
||||
startTime: new Date().getTime()
|
||||
};
|
||||
}
|
||||
let ttl = 5 * 60 * 1000;
|
||||
@@ -251,16 +283,16 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
|
||||
//公共CNAME
|
||||
return new CommonDnsProvider({
|
||||
config: cnameProvider,
|
||||
plusService: this.plusService,
|
||||
plusService: this.plusService
|
||||
});
|
||||
}
|
||||
|
||||
const serviceGetter = this.taskServiceBuilder.create({userId:cnameProvider.userId})
|
||||
const serviceGetter = this.taskServiceBuilder.create({ userId: cnameProvider.userId });
|
||||
const access = await this.accessService.getById(cnameProvider.accessId, cnameProvider.userId);
|
||||
const context = {access, logger, http, utils, domainParser,serviceGetter};
|
||||
const context = { access, logger, http, utils, domainParser, serviceGetter };
|
||||
const dnsProvider: IDnsProvider = await createDnsProvider({
|
||||
dnsProviderType: cnameProvider.dnsProviderType,
|
||||
context,
|
||||
context
|
||||
});
|
||||
return dnsProvider;
|
||||
};
|
||||
@@ -268,15 +300,15 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
|
||||
const clearVerifyRecord = async () => {
|
||||
cache.delete(cacheKey);
|
||||
try {
|
||||
let dnsProvider = value.dnsProvider
|
||||
let dnsProvider = value.dnsProvider;
|
||||
if (!dnsProvider) {
|
||||
dnsProvider = await buildDnsProvider();
|
||||
}
|
||||
await dnsProvider.removeRecord({
|
||||
recordReq: value.recordReq,
|
||||
recordRes: value.recordRes,
|
||||
recordRes: value.recordRes
|
||||
});
|
||||
logger.info('删除CNAME的校验DNS记录成功');
|
||||
logger.info("删除CNAME的校验DNS记录成功");
|
||||
} catch (e) {
|
||||
logger.error(`删除CNAME的校验DNS记录失败, ${e.message},req:${JSON.stringify(value.recordReq)},recordRes:${JSON.stringify(value.recordRes)}`, e);
|
||||
}
|
||||
@@ -289,8 +321,8 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
|
||||
if (value.startTime + ttl < new Date().getTime()) {
|
||||
logger.warn(`cname验证超时,停止检查,${bean.domain} ${testRecordValue}`);
|
||||
clearInterval(value.intervalId);
|
||||
await this.updateStatus(bean.id, 'timeout');
|
||||
await clearVerifyRecord()
|
||||
await this.updateStatus(bean.id, "timeout");
|
||||
await clearVerifyRecord();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -301,7 +333,7 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
|
||||
logger.info(`检查CNAME配置 ${fullDomain} ${testRecordValue}`);
|
||||
|
||||
//检查是否有重复的acme配置
|
||||
await this.checkRepeatAcmeChallengeRecords(fullDomain,bean.recordValue)
|
||||
await this.checkRepeatAcmeChallengeRecords(fullDomain, bean.recordValue);
|
||||
|
||||
// const txtRecords = await dns.promises.resolveTxt(fullDomain);
|
||||
// if (txtRecords.length) {
|
||||
@@ -318,9 +350,9 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
|
||||
if (success) {
|
||||
clearInterval(value.intervalId);
|
||||
logger.info(`检测到CNAME配置,修改状态 ${fullDomain} ${testRecordValue}`);
|
||||
await this.updateStatus(bean.id, 'valid', "");
|
||||
await this.updateStatus(bean.id, "valid", "");
|
||||
value.pass = true;
|
||||
await clearVerifyRecord()
|
||||
await clearVerifyRecord();
|
||||
return success;
|
||||
}
|
||||
};
|
||||
@@ -331,88 +363,88 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
|
||||
}
|
||||
|
||||
cache.set(cacheKey, value, {
|
||||
ttl: ttl,
|
||||
ttl: ttl
|
||||
});
|
||||
|
||||
const domain = await domainParser.parse(bean.recordValue);
|
||||
const fullRecord = bean.recordValue;
|
||||
const hostRecord = fullRecord.replace(`.${domain}`, '');
|
||||
const hostRecord = fullRecord.replace(`.${domain}`, "");
|
||||
const req = {
|
||||
domain: domain,
|
||||
fullRecord: fullRecord,
|
||||
hostRecord: hostRecord,
|
||||
type: 'TXT',
|
||||
value: testRecordValue,
|
||||
type: "TXT",
|
||||
value: testRecordValue
|
||||
};
|
||||
|
||||
const dnsProvider = await buildDnsProvider();
|
||||
if(dnsProvider.usePunyCode()){
|
||||
if (dnsProvider.usePunyCode()) {
|
||||
//是否需要中文转英文
|
||||
req.domain = dnsProvider.punyCodeEncode(req.domain)
|
||||
req.fullRecord = dnsProvider.punyCodeEncode(req.fullRecord)
|
||||
req.hostRecord = dnsProvider.punyCodeEncode(req.hostRecord)
|
||||
req.value = dnsProvider.punyCodeEncode(req.value)
|
||||
req.domain = dnsProvider.punyCodeEncode(req.domain);
|
||||
req.fullRecord = dnsProvider.punyCodeEncode(req.fullRecord);
|
||||
req.hostRecord = dnsProvider.punyCodeEncode(req.hostRecord);
|
||||
req.value = dnsProvider.punyCodeEncode(req.value);
|
||||
}
|
||||
const recordRes = await dnsProvider.createRecord(req);
|
||||
value.dnsProvider = dnsProvider;
|
||||
value.validating = true;
|
||||
value.recordReq = req;
|
||||
value.recordRes = recordRes;
|
||||
await this.updateStatus(bean.id, 'validating', "");
|
||||
await this.updateStatus(bean.id, "validating", "");
|
||||
|
||||
value.intervalId = setInterval(async () => {
|
||||
try {
|
||||
await checkRecordValue();
|
||||
} catch (e) {
|
||||
logger.error('检查cname出错:', e);
|
||||
logger.error("检查cname出错:", e);
|
||||
await this.updateError(bean.id, e.message);
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
async updateStatus(id: number, status: CnameRecordStatusType, error?: string) {
|
||||
const updated: any = {status}
|
||||
const updated: any = { status };
|
||||
if (error != null) {
|
||||
updated.error = error
|
||||
updated.error = error;
|
||||
}
|
||||
await this.getRepository().update(id, updated);
|
||||
}
|
||||
|
||||
async updateError(id: number, error: string) {
|
||||
await this.getRepository().update(id, {error});
|
||||
await this.getRepository().update(id, { error });
|
||||
}
|
||||
|
||||
async checkRepeatAcmeChallengeRecords(acmeRecordDomain: string,targetCnameDomain:string) {
|
||||
async checkRepeatAcmeChallengeRecords(acmeRecordDomain: string, targetCnameDomain: string) {
|
||||
|
||||
let dnsResolver = null
|
||||
try{
|
||||
dnsResolver = await getAuthoritativeDnsResolver(acmeRecordDomain)
|
||||
}catch (e) {
|
||||
logger.error(`获取${acmeRecordDomain}的权威DNS服务器失败,${e.message}`)
|
||||
return
|
||||
let dnsResolver = null;
|
||||
try {
|
||||
dnsResolver = await getAuthoritativeDnsResolver(acmeRecordDomain);
|
||||
} catch (e) {
|
||||
logger.error(`获取${acmeRecordDomain}的权威DNS服务器失败,${e.message}`);
|
||||
return;
|
||||
}
|
||||
let cnameRecords = []
|
||||
try{
|
||||
let cnameRecords = [];
|
||||
try {
|
||||
cnameRecords = await dnsResolver.resolveCname(acmeRecordDomain);
|
||||
}catch (e) {
|
||||
logger.error(`查询CNAME记录失败:${e.message}`)
|
||||
return
|
||||
} catch (e) {
|
||||
logger.error(`查询CNAME记录失败:${e.message}`);
|
||||
return;
|
||||
}
|
||||
targetCnameDomain = targetCnameDomain.toLowerCase()
|
||||
targetCnameDomain = punycode.toASCII(targetCnameDomain)
|
||||
targetCnameDomain = targetCnameDomain.toLowerCase();
|
||||
targetCnameDomain = punycode.toASCII(targetCnameDomain);
|
||||
if (cnameRecords.length > 0) {
|
||||
for (const cnameRecord of cnameRecords) {
|
||||
if(cnameRecord.toLowerCase() !== targetCnameDomain){
|
||||
if (cnameRecord.toLowerCase() !== targetCnameDomain) {
|
||||
//确保只有一个cname记录
|
||||
throw new Error(`${acmeRecordDomain}存在多个CNAME记录,请删除多余的CNAME记录:${cnameRecord}`)
|
||||
throw new Error(`${acmeRecordDomain}存在多个CNAME记录,请删除多余的CNAME记录:${cnameRecord}`);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 确保权威服务器里面没有纯粹的TXT记录
|
||||
let txtRecords = []
|
||||
try{
|
||||
let txtRecords = [];
|
||||
try {
|
||||
const txtRecordRes = await dnsResolver.resolveTxt(acmeRecordDomain);
|
||||
|
||||
if (txtRecordRes && txtRecordRes.length > 0) {
|
||||
@@ -420,13 +452,13 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
|
||||
logger.info(`TXT records: ${JSON.stringify(txtRecords)}`);
|
||||
txtRecords = txtRecords.concat(...txtRecordRes);
|
||||
}
|
||||
}catch (e) {
|
||||
logger.error(`查询Txt记录失败:${e.message}`)
|
||||
} catch (e) {
|
||||
logger.error(`查询Txt记录失败:${e.message}`);
|
||||
}
|
||||
|
||||
if (txtRecords.length === 0) {
|
||||
//如果权威服务器中查不到txt,无需继续检查
|
||||
return
|
||||
return;
|
||||
}
|
||||
if (cnameRecords.length > 0) {
|
||||
// 从cname记录中获取txt记录
|
||||
@@ -435,11 +467,18 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
|
||||
if (res.length > 0) {
|
||||
for (const txtRecord of txtRecords) {
|
||||
if (!res.includes(txtRecord)) {
|
||||
throw new Error(`${acmeRecordDomain}存在多个TXT记录,请删除多余的TXT记录:${txtRecord}`)
|
||||
throw new Error(`${acmeRecordDomain}存在多个TXT记录,请删除多余的TXT记录:${txtRecord}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async resetStatus(id: number) {
|
||||
if (!id) {
|
||||
throw new ValidateException("id不能为空");
|
||||
}
|
||||
await this.getRepository().update(id, { status: "cname", mainDomain: "" });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,12 +11,12 @@ import {
|
||||
import { RoleService } from "../../sys/authority/service/role-service.js";
|
||||
import { UserEntity } from "../../sys/authority/entity/user.js";
|
||||
import { cache, utils } from "@certd/basic";
|
||||
import { LoginErrorException } from "@certd/lib-server/dist/basic/exception/login-error-exception.js";
|
||||
import { LoginErrorException } from "@certd/lib-server";
|
||||
import { CodeService } from "../../basic/service/code-service.js";
|
||||
import { TwoFactorService } from "../../mine/service/two-factor-service.js";
|
||||
import { UserSettingsService } from "../../mine/service/user-settings-service.js";
|
||||
import { isPlus } from "@certd/plus-core";
|
||||
import { AddonService } from "@certd/lib-server/dist/user/addon/service/addon-service.js";
|
||||
import { AddonService } from "@certd/lib-server";
|
||||
|
||||
/**
|
||||
* 系统用户
|
||||
|
||||
@@ -28,6 +28,7 @@ export class UserSiteMonitorSetting extends BaseSettings {
|
||||
cron?:string = undefined;
|
||||
retryTimes?:number = 3;
|
||||
dnsServer?:string[] = undefined;
|
||||
certValidDays?:number = 10;
|
||||
}
|
||||
|
||||
export class UserEmailSetting extends BaseSettings {
|
||||
|
||||
@@ -56,6 +56,12 @@ export class SiteInfoEntity {
|
||||
@Column({ name: 'disabled', comment: '禁用启用' })
|
||||
disabled: boolean;
|
||||
|
||||
@Column({ name: 'remark', comment: '备注', length: 512 })
|
||||
remark: string;
|
||||
|
||||
@Column({ name: 'group_id', comment: '分组id' })
|
||||
groupId: number;
|
||||
|
||||
@Column({ name: 'create_time', comment: '创建时间', default: () => 'CURRENT_TIMESTAMP' })
|
||||
createTime: Date;
|
||||
@Column({ name: 'update_time', comment: '修改时间', default: () => 'CURRENT_TIMESTAMP' })
|
||||
|
||||
@@ -169,8 +169,9 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
|
||||
if (!notify) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.sendExpiresNotify(site);
|
||||
await this.sendExpiresNotify(site.id);
|
||||
} catch (e) {
|
||||
logger.error("send notify error", e);
|
||||
}
|
||||
@@ -186,7 +187,7 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.sendCheckErrorNotify(site);
|
||||
await this.sendCheckErrorNotify(site.id);
|
||||
} catch (e) {
|
||||
logger.error("send notify error", e);
|
||||
}
|
||||
@@ -231,8 +232,7 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
|
||||
ipErrorCount: errorCount
|
||||
});
|
||||
try {
|
||||
site = await this.info(site.id);
|
||||
await this.sendCheckErrorNotify(site, true);
|
||||
await this.sendCheckErrorNotify(site.id, true);
|
||||
} catch (e) {
|
||||
logger.error("send notify error", e);
|
||||
}
|
||||
@@ -254,7 +254,8 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
|
||||
return await this.doCheck(site, notify, retryTimes);
|
||||
}
|
||||
|
||||
async sendCheckErrorNotify(site: SiteInfoEntity, fromIpCheck = false) {
|
||||
async sendCheckErrorNotify(siteId: number, fromIpCheck = false) {
|
||||
const site = await this.info(siteId);
|
||||
const url = await this.notificationService.getBindUrl("#/certd/monitor/site");
|
||||
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(site.userId, UserSiteMonitorSetting)
|
||||
// 发邮件
|
||||
@@ -274,14 +275,14 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
|
||||
);
|
||||
}
|
||||
|
||||
async sendExpiresNotify(site: SiteInfoEntity) {
|
||||
|
||||
const tipDays = 10;
|
||||
async sendExpiresNotify(siteId: number) {
|
||||
const site = await this.info(siteId);
|
||||
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(site.userId, UserSiteMonitorSetting)
|
||||
const tipDays = setting?.certValidDays || 10;
|
||||
|
||||
const expires = site.certExpiresTime;
|
||||
const validDays = dayjs(expires).diff(dayjs(), "day");
|
||||
const url = await this.notificationService.getBindUrl("#/certd/monitor/site");
|
||||
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(site.userId, UserSiteMonitorSetting)
|
||||
const content = `站点名称: ${site.name} \n站点域名: ${site.domain} \n证书域名: ${site.certDomains} \n颁发机构: ${site.certProvider} \n过期时间: ${dayjs(site.certExpiresTime).format("YYYY-MM-DD")} \n`;
|
||||
if (validDays >= 0 && validDays < tipDays) {
|
||||
// 发通知
|
||||
@@ -392,7 +393,7 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
|
||||
}
|
||||
}
|
||||
|
||||
async doImport(req: { text: string; userId: number }) {
|
||||
async doImport(req: { text: string; userId: number,groupId?:number }) {
|
||||
if (!req.text) {
|
||||
throw new Error("text is required");
|
||||
}
|
||||
@@ -420,17 +421,22 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
|
||||
} catch (e) {
|
||||
throw new Error(`${item}格式错误`);
|
||||
}
|
||||
|
||||
}
|
||||
if (arr.length > 2) {
|
||||
name = arr[2] || domain;
|
||||
}
|
||||
let remark:string = "";
|
||||
if (arr.length > 3) {
|
||||
remark = arr[3] || "";
|
||||
}
|
||||
|
||||
list.push({
|
||||
domain,
|
||||
name,
|
||||
httpsPort: port,
|
||||
userId: req.userId
|
||||
userId: req.userId,
|
||||
remark,
|
||||
groupId: req.groupId
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -43,28 +43,19 @@ export class PipelineEntity {
|
||||
@Column({ name:"is_template", comment: '是否模版', nullable: true, default: false })
|
||||
isTemplate: boolean;
|
||||
|
||||
@Column({
|
||||
name: 'last_history_time',
|
||||
comment: '最后一次执行时间',
|
||||
nullable: true,
|
||||
})
|
||||
@Column({name: 'last_history_time',comment: '最后一次执行时间',nullable: true,})
|
||||
lastHistoryTime: number;
|
||||
|
||||
@Column({name: 'valid_time',comment: '到期时间',nullable: true,default: 0})
|
||||
validTime: number;
|
||||
|
||||
// 变量
|
||||
lastVars: any;
|
||||
|
||||
@Column({
|
||||
name: 'order',
|
||||
comment: '排序',
|
||||
nullable: true,
|
||||
})
|
||||
@Column({name: 'order', comment: '排序', nullable: true,})
|
||||
order: number;
|
||||
|
||||
@Column({
|
||||
name: 'create_time',
|
||||
comment: '创建时间',
|
||||
default: () => 'CURRENT_TIMESTAMP',
|
||||
})
|
||||
@Column({name: 'create_time',comment: '创建时间', default: () => 'CURRENT_TIMESTAMP',})
|
||||
createTime: Date;
|
||||
@Column({
|
||||
name: 'update_time',
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import { http, logger, utils } from "@certd/basic";
|
||||
import { TaskServiceBuilder } from "./getter/task-service-getter.js";
|
||||
import { AddonService, newAddon, PermissionException, ValidateException } from "@certd/lib-server";
|
||||
|
||||
/**
|
||||
* Addon
|
||||
*/
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
export class AddonGetterService {
|
||||
|
||||
@Inject()
|
||||
taskServiceBuilder: TaskServiceBuilder;
|
||||
@Inject()
|
||||
addonService: AddonService;
|
||||
|
||||
|
||||
async getAddonById(id: any, checkUserId: boolean, userId?: number, defaultAddon?:{type:string,name:string} ): Promise<any> {
|
||||
const serviceGetter = this.taskServiceBuilder.create({
|
||||
userId
|
||||
});
|
||||
const ctx = {
|
||||
http,
|
||||
logger,
|
||||
utils,
|
||||
serviceGetter
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
if (!defaultAddon) {
|
||||
return null;
|
||||
}
|
||||
return await newAddon(defaultAddon.type, defaultAddon.name, {}, ctx);
|
||||
}
|
||||
const entity = await this.addonService.info(id);
|
||||
if (entity == null) {
|
||||
if (!defaultAddon) {
|
||||
return null;
|
||||
}
|
||||
return await newAddon(defaultAddon.type, defaultAddon.name, {}, ctx);
|
||||
}
|
||||
if (checkUserId) {
|
||||
if (userId == null) {
|
||||
throw new ValidateException("userId不能为空");
|
||||
}
|
||||
if (userId !== entity.userId) {
|
||||
throw new PermissionException("您对该Addon无访问权限");
|
||||
}
|
||||
}
|
||||
|
||||
const setting = JSON.parse(entity.setting ?? "{}");
|
||||
const input = {
|
||||
id: entity.id,
|
||||
...setting
|
||||
};
|
||||
|
||||
return await newAddon(entity.addonType, entity.type, input, ctx);
|
||||
}
|
||||
|
||||
async getById(id: any, userId: number): Promise<any> {
|
||||
return await this.getAddonById(id, true, userId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -32,7 +32,7 @@ export class TaskServiceGetter implements IServiceGetter{
|
||||
return await this.getNotificationService() as T
|
||||
} else if (serviceName === 'domainVerifierGetter') {
|
||||
return await this.getDomainVerifierGetter() as T
|
||||
} else{
|
||||
}else{
|
||||
if(!serviceNames.includes(serviceName)){
|
||||
throw new Error(`${serviceName} not in whitelist`)
|
||||
}
|
||||
@@ -53,6 +53,7 @@ export class TaskServiceGetter implements IServiceGetter{
|
||||
return new AccessGetter(this.userId, accessService.getById.bind(accessService));
|
||||
}
|
||||
|
||||
|
||||
async getCnameProxyService(): Promise<CnameProxyService> {
|
||||
const cnameRecordService:CnameRecordService = await this.appCtx.getAsync("cnameRecordService")
|
||||
return new CnameProxyService(this.userId, cnameRecordService.getWithAccessByDomain.bind(cnameRecordService));
|
||||
@@ -68,10 +69,6 @@ export class TaskServiceGetter implements IServiceGetter{
|
||||
return new DomainVerifierGetter(this.userId, domainService);
|
||||
}
|
||||
}
|
||||
export type TaskServiceCreateReq = {
|
||||
userId: number;
|
||||
}
|
||||
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
export class TaskServiceBuilder {
|
||||
@@ -84,6 +81,10 @@ export class TaskServiceBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
export type TaskServiceCreateReq = {
|
||||
userId: number;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -255,6 +255,11 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
||||
bean.title = pipeline.title;
|
||||
}
|
||||
pipeline.id = bean.id;
|
||||
|
||||
if (pipeline.version == null) {
|
||||
pipeline.version = 0;
|
||||
}
|
||||
pipeline.version++;
|
||||
bean.content = JSON.stringify(pipeline);
|
||||
await this.addOrUpdate(bean);
|
||||
await this.registerTrigger(bean);
|
||||
@@ -368,17 +373,21 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
||||
}
|
||||
|
||||
if (immediateTriggerOnce) {
|
||||
await this.trigger(pipeline.id);
|
||||
await sleep(200);
|
||||
try{
|
||||
await this.trigger(pipeline.id);
|
||||
await sleep(200);
|
||||
}catch(e){
|
||||
logger.error(e);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
async trigger(id: any, stepId?: string) {
|
||||
async trigger(id: any, stepId?: string , doCheck = false) {
|
||||
const entity: PipelineEntity = await this.info(id);
|
||||
if (isComm()) {
|
||||
await this.checkHasDeployCount(id, entity.userId);
|
||||
if (doCheck) {
|
||||
await this.beforeCheck(entity);
|
||||
}
|
||||
await this.checkUserStatus(entity.userId);
|
||||
this.cron.register({
|
||||
name: `pipeline.${id}.trigger.once`,
|
||||
cron: null,
|
||||
@@ -457,11 +466,11 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
||||
return;
|
||||
}
|
||||
cron = cron.trim();
|
||||
if (cron.startsWith("* *")) {
|
||||
cron = cron.replace("\* \*", "0 0");
|
||||
if (cron.startsWith("* * ")) {
|
||||
cron = "0 0 " + cron.substring(5);
|
||||
}
|
||||
if (cron.startsWith("*")) {
|
||||
cron = cron.replace("\*", "0");
|
||||
if (cron.startsWith("* ")) {
|
||||
cron = "0 " + cron.substring(2);
|
||||
}
|
||||
const triggerId = trigger.id;
|
||||
const name = this.buildCronKey(pipelineId, triggerId);
|
||||
@@ -485,6 +494,17 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
||||
logger.info("当前定时器数量:", this.cron.getTaskSize());
|
||||
}
|
||||
|
||||
|
||||
async isPipelineValidTimeEnabled(entity: PipelineEntity) {
|
||||
const settings = await this.sysSettingsService.getPublicSettings();
|
||||
if (isPlus() && settings.pipelineValidTimeEnabled){
|
||||
if (entity.validTime > 0 && entity.validTime < Date.now()){
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param id
|
||||
@@ -496,20 +516,34 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
||||
await this.doRun(entity, triggerId, stepId);
|
||||
}
|
||||
|
||||
async doRun(entity: PipelineEntity, triggerId: string, stepId?: string) {
|
||||
const id = entity.id;
|
||||
async beforeCheck(entity: PipelineEntity) {
|
||||
const validTimeEnabled = await this.isPipelineValidTimeEnabled(entity)
|
||||
if (!validTimeEnabled) {
|
||||
throw new Error(`流水线${entity.id}已过期,不予执行`);
|
||||
}
|
||||
|
||||
let suite: UserSuiteEntity = null;
|
||||
if (isComm()) {
|
||||
suite = await this.checkHasDeployCount(id, entity.userId);
|
||||
suite = await this.checkHasDeployCount(entity.id, entity.userId);
|
||||
}
|
||||
try {
|
||||
await this.checkUserStatus(entity.userId);
|
||||
await this.checkUserStatus(entity.userId);
|
||||
|
||||
return {
|
||||
suite
|
||||
}
|
||||
}
|
||||
|
||||
async doRun(entity: PipelineEntity, triggerId: string, stepId?: string) {
|
||||
|
||||
let suite:any = null
|
||||
try{
|
||||
const res = await this.beforeCheck(entity);
|
||||
suite = res.suite
|
||||
} catch (e) {
|
||||
logger.info(e.message);
|
||||
return;
|
||||
logger.error(`流水线${entity.id}触发${triggerId}失败:${e.message}`);
|
||||
}
|
||||
|
||||
|
||||
const id = entity.id;
|
||||
const pipeline = JSON.parse(entity.content);
|
||||
if (!pipeline.id) {
|
||||
pipeline.id = id;
|
||||
|
||||
@@ -20,7 +20,7 @@ export class AuthService {
|
||||
return true;
|
||||
}
|
||||
|
||||
async isAdmin(ctx: any) {
|
||||
isAdmin(ctx: any) {
|
||||
const roleIds: number[] = ctx.user.roles;
|
||||
if (roleIds.includes(1)) {
|
||||
return true;
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import { http, logger, utils } from '@certd/basic';
|
||||
|
||||
// 使用@certd/basic包中已有的utils.sp.spawn函数替代自定义的asyncExec
|
||||
// 该函数已经内置了Windows系统编码问题的解决方案
|
||||
|
||||
export type NetTestResult = {
|
||||
success: boolean; //是否成功
|
||||
message: string; //结果
|
||||
testLog: string; //测试日志
|
||||
error?: string; //执行错误信息
|
||||
}
|
||||
|
||||
@Provide('nettestService')
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
export class NetTestService {
|
||||
/**
|
||||
* 执行Telnet测试
|
||||
* @param domain 域名
|
||||
* @param port 端口
|
||||
* @returns 测试结果
|
||||
*/
|
||||
async telnet(domain: string, port: number): Promise<NetTestResult> {
|
||||
try {
|
||||
let command = '';
|
||||
|
||||
if (this.isWindows()) {
|
||||
// Windows系统使用PowerShell执行测试,避免输入重定向问题
|
||||
// 使用PowerShell的Test-NetConnection命令进行端口测试
|
||||
command = `powershell -Command "& { $result = Test-NetConnection -ComputerName ${domain} -Port ${port} -InformationLevel Quiet; if ($result) { Write-Host '端口连接成功' } else { Write-Host '端口连接失败' } }"`;
|
||||
} else {
|
||||
// Linux系统使用nc命令进行端口测试
|
||||
command = `nc -zv -w 5 ${domain} ${port} 2>&1`;
|
||||
}
|
||||
|
||||
// 使用utils.sp.spawn执行命令,它会自动处理Windows编码问题
|
||||
const output = await utils.sp.spawn({
|
||||
cmd: command,
|
||||
logger: undefined // 可以根据需要传入logger
|
||||
});
|
||||
|
||||
// 判断测试是否成功
|
||||
const success = this.isWindows()
|
||||
? output.includes('端口连接成功')
|
||||
: output.includes(' open');
|
||||
|
||||
// 处理结果
|
||||
return {
|
||||
success,
|
||||
message: success ? '端口连接测试成功' : '端口连接测试失败',
|
||||
testLog: output,
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Telnet测试执行失败',
|
||||
testLog: error.stdout || error.stderr || error?.message || String(error),
|
||||
error: error.stderr || error?.message || String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行Ping测试
|
||||
* @param domain 域名
|
||||
* @returns 测试结果
|
||||
*/
|
||||
async ping(domain: string): Promise<NetTestResult> {
|
||||
try {
|
||||
let command = '';
|
||||
|
||||
if (this.isWindows()) {
|
||||
// Windows系统ping命令,发送4个包
|
||||
command = `ping -n 4 ${domain}`;
|
||||
} else {
|
||||
// Linux系统ping命令,发送4个包
|
||||
command = `ping -c 4 ${domain}`;
|
||||
}
|
||||
|
||||
// 使用utils.sp.spawn执行命令
|
||||
const output = await utils.sp.spawn({
|
||||
cmd: command,
|
||||
logger: undefined
|
||||
});
|
||||
|
||||
// 判断测试是否成功
|
||||
const success = this.isWindows()
|
||||
? output.includes('TTL=')
|
||||
: output.includes('time=');
|
||||
|
||||
return {
|
||||
success,
|
||||
message: success ? 'Ping测试成功' : 'Ping测试失败',
|
||||
testLog: output,
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
success: false,
|
||||
message: 'Ping测试执行失败',
|
||||
testLog: error.stderr|| error.stdout || errorMessage,
|
||||
error: errorMessage
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private isWindows() {
|
||||
return process.platform === 'win32';
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行域名解析测试
|
||||
* @param domain 域名
|
||||
* @returns 解析结果
|
||||
*/
|
||||
async domainResolve(domain: string): Promise<NetTestResult> {
|
||||
try {
|
||||
let command = '';
|
||||
if (this.isWindows()) {
|
||||
// Windows系统使用nslookup命令
|
||||
command = `nslookup ${domain}`;
|
||||
} else {
|
||||
// Linux系统优先使用dig命令,如果没有则回退到nslookup
|
||||
command = `which dig > /dev/null && dig ${domain} || nslookup ${domain}`;
|
||||
}
|
||||
|
||||
// 使用utils.sp.spawn执行命令
|
||||
const output = await utils.sp.spawn({
|
||||
cmd: command,
|
||||
logger: undefined
|
||||
});
|
||||
|
||||
// 判断测试是否成功
|
||||
const success = output.includes('Address:') || output.includes('IN A') || output.includes('IN AAAA') ||
|
||||
(this.isWindows() && output.includes('Name:'));
|
||||
|
||||
return {
|
||||
success,
|
||||
message: success ? '域名解析测试成功' : '域名解析测试失败',
|
||||
testLog: output,
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
success: false,
|
||||
message: '域名解析测试执行失败',
|
||||
testLog: error.stdoout || error.stderr || errorMessage,
|
||||
error: errorMessage
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async getLocalIP(): Promise<string[]> {
|
||||
try {
|
||||
const output = await utils.sp.spawn({
|
||||
cmd: 'ip a | grep \'inet \' | grep -v \'127.0.0.1\' | awk \'{print $2}\' | cut -d/ -f1',
|
||||
logger: undefined
|
||||
});
|
||||
// 去除 inet 前缀
|
||||
let ips = output.trim().replace(/inet /g, '');
|
||||
return ips.split('\n').filter(ip => ip.length > 0);
|
||||
} catch (error) {
|
||||
return [error instanceof Error ? error.message : String(error)];
|
||||
}
|
||||
}
|
||||
|
||||
async getPublicIP(): Promise<string[]> {
|
||||
try {
|
||||
const res = await http.request({
|
||||
url:"https://ipinfo.io/ip",
|
||||
method:"GET",
|
||||
})
|
||||
return[res]
|
||||
} catch (error) {
|
||||
return [error instanceof Error ? error.message : String(error)]
|
||||
}
|
||||
}
|
||||
|
||||
async getDNSservers(): Promise<string[]> {
|
||||
let dnsServers: string[] = [];
|
||||
try {
|
||||
const output = await utils.sp.spawn({
|
||||
cmd: 'cat /etc/resolv.conf | grep nameserver | awk \'{print $2}\'',
|
||||
logger: undefined
|
||||
});
|
||||
dnsServers = output.trim().split('\n');
|
||||
} catch (error) {
|
||||
dnsServers = [error instanceof Error ? error.message : String(error)];
|
||||
}
|
||||
try{
|
||||
/**
|
||||
* /app # cat /etc/resolv.conf | grep "ExtServers"
|
||||
# ExtServers: [223.5.5.5 223.6.6.6]
|
||||
*/
|
||||
const extDnsServers = await utils.sp.spawn({
|
||||
cmd: 'cat /etc/resolv.conf | grep "ExtServers"',
|
||||
logger: undefined
|
||||
});
|
||||
const line = extDnsServers.trim()
|
||||
if (line.includes('ExtServers') && line.includes('[')) {
|
||||
const extDns = line.substring(line.indexOf('[') + 1, line.indexOf(']')).split(' ');
|
||||
const dnsList = extDns.map(item=>`Ext:${item}`)
|
||||
dnsServers = dnsServers.concat(dnsList);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('获取DNS ExtServers 服务器失败', error);
|
||||
// dnsServers.push(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
return dnsServers;
|
||||
}
|
||||
/**
|
||||
* 获取服务器信息(包括本地IP、外网IP和DNS服务器)
|
||||
* @returns 服务器信息
|
||||
*/
|
||||
async serverInfo(): Promise<any> {
|
||||
|
||||
const res = {
|
||||
localIP: [],
|
||||
publicIP: [],
|
||||
dnsServers: [],
|
||||
}
|
||||
|
||||
res.localIP = await this.getLocalIP();
|
||||
res.publicIP = await this.getPublicIP();
|
||||
res.dnsServers = await this.getDNSservers();
|
||||
return res
|
||||
}
|
||||
}
|
||||
@@ -37,3 +37,4 @@ export * from './plugin-dokploy/index.js'
|
||||
export * from './plugin-godaddy/index.js'
|
||||
export * from './plugin-captcha/index.js'
|
||||
export * from './plugin-xinnet/index.js'
|
||||
export * from './plugin-xinnetconnet/index.js'
|
||||
|
||||
@@ -6,15 +6,15 @@ import { AbstractPlusTaskPlugin } from "@certd/plugin-plus";
|
||||
import JSZip from "jszip";
|
||||
import * as os from "node:os";
|
||||
import { OssClientContext, ossClientFactory, OssClientRemoveByOpts, SshAccess, SshClient } from "@certd/plugin-lib";
|
||||
|
||||
const defaultBackupDir = 'certd_backup';
|
||||
const defaultFilePrefix = 'db_backup';
|
||||
import { pipeline } from "stream/promises";
|
||||
const defaultBackupDir = "certd_backup";
|
||||
const defaultFilePrefix = "db_backup";
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: 'DBBackupPlugin',
|
||||
title: '数据库备份',
|
||||
icon: 'lucide:database-backup',
|
||||
desc: '【仅管理员可用】仅支持备份SQLite数据库',
|
||||
name: "DBBackupPlugin",
|
||||
title: "数据库备份",
|
||||
icon: "lucide:database-backup",
|
||||
desc: "【仅管理员可用】仅支持备份SQLite数据库",
|
||||
group: pluginGroups.admin.key,
|
||||
showRunStrategy: true,
|
||||
default: {
|
||||
@@ -22,32 +22,32 @@ const defaultFilePrefix = 'db_backup';
|
||||
runStrategy: RunStrategy.AlwaysRun,
|
||||
},
|
||||
},
|
||||
onlyAdmin:true,
|
||||
onlyAdmin: true,
|
||||
needPlus: true,
|
||||
})
|
||||
export class DBBackupPlugin extends AbstractPlusTaskPlugin {
|
||||
@TaskInput({
|
||||
title: '备份方式',
|
||||
value: 'local',
|
||||
title: "备份方式",
|
||||
value: "local",
|
||||
component: {
|
||||
name: 'a-select',
|
||||
name: "a-select",
|
||||
options: [
|
||||
{label: '本地复制', value: 'local'},
|
||||
{label: 'ssh上传', value: 'ssh'},
|
||||
{label: 'oss上传', value: 'oss'},
|
||||
{ label: "本地复制", value: "local" },
|
||||
{ label: "ssh上传", value: "ssh" },
|
||||
{ label: "oss上传", value: "oss" },
|
||||
],
|
||||
placeholder: '',
|
||||
placeholder: "",
|
||||
},
|
||||
helper: '支持本地复制、ssh上传',
|
||||
helper: "支持本地复制、ssh上传",
|
||||
required: true,
|
||||
})
|
||||
backupMode = 'local';
|
||||
backupMode = "local";
|
||||
|
||||
@TaskInput({
|
||||
title: '主机登录授权',
|
||||
title: "主机登录授权",
|
||||
component: {
|
||||
name: 'access-selector',
|
||||
type: 'ssh',
|
||||
name: "access-selector",
|
||||
type: "ssh",
|
||||
},
|
||||
mergeScript: `
|
||||
return {
|
||||
@@ -60,19 +60,18 @@ export class DBBackupPlugin extends AbstractPlusTaskPlugin {
|
||||
})
|
||||
sshAccessId!: number;
|
||||
|
||||
|
||||
@TaskInput({
|
||||
title: 'OSS类型',
|
||||
title: "OSS类型",
|
||||
component: {
|
||||
name: 'a-select',
|
||||
name: "a-select",
|
||||
options: [
|
||||
{value: "alioss", label: "阿里云OSS"},
|
||||
{value: "s3", label: "MinIO/S3"},
|
||||
{value: "qiniuoss", label: "七牛云"},
|
||||
{value: "tencentcos", label: "腾讯云COS"},
|
||||
{value: "ftp", label: "Ftp"},
|
||||
{value: "sftp", label: "Sftp"},
|
||||
]
|
||||
{ value: "alioss", label: "阿里云OSS" },
|
||||
{ value: "s3", label: "MinIO/S3" },
|
||||
{ value: "qiniuoss", label: "七牛云" },
|
||||
{ value: "tencentcos", label: "腾讯云COS" },
|
||||
{ value: "ftp", label: "Ftp" },
|
||||
{ value: "sftp", label: "Sftp" },
|
||||
],
|
||||
},
|
||||
mergeScript: `
|
||||
return {
|
||||
@@ -86,9 +85,9 @@ export class DBBackupPlugin extends AbstractPlusTaskPlugin {
|
||||
ossType!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: 'OSS授权',
|
||||
title: "OSS授权",
|
||||
component: {
|
||||
name: 'access-selector',
|
||||
name: "access-selector",
|
||||
},
|
||||
mergeScript: `
|
||||
return {
|
||||
@@ -106,12 +105,11 @@ export class DBBackupPlugin extends AbstractPlusTaskPlugin {
|
||||
})
|
||||
ossAccessId!: number;
|
||||
|
||||
|
||||
@TaskInput({
|
||||
title: '备份保存目录',
|
||||
title: "备份保存目录",
|
||||
component: {
|
||||
name: 'a-input',
|
||||
type: 'value',
|
||||
name: "a-input",
|
||||
type: "value",
|
||||
placeholder: `默认${defaultBackupDir}`,
|
||||
},
|
||||
helper: `ssh方式默认保存在当前用户的${defaultBackupDir}目录下,本地方式默认保存在data/${defaultBackupDir}目录下,也可以填写绝对路径`,
|
||||
@@ -120,10 +118,10 @@ export class DBBackupPlugin extends AbstractPlusTaskPlugin {
|
||||
backupDir: string = defaultBackupDir;
|
||||
|
||||
@TaskInput({
|
||||
title: '备份文件前缀',
|
||||
title: "备份文件前缀",
|
||||
component: {
|
||||
name: 'a-input',
|
||||
vModel: 'value',
|
||||
name: "a-input",
|
||||
vModel: "value",
|
||||
placeholder: `默认${defaultFilePrefix}`,
|
||||
},
|
||||
required: false,
|
||||
@@ -131,11 +129,11 @@ export class DBBackupPlugin extends AbstractPlusTaskPlugin {
|
||||
filePrefix: string = defaultFilePrefix;
|
||||
|
||||
@TaskInput({
|
||||
title: '附加上传文件',
|
||||
title: "附加上传文件",
|
||||
value: true,
|
||||
component: {
|
||||
name: 'a-switch',
|
||||
vModel: 'checked',
|
||||
name: "a-switch",
|
||||
vModel: "checked",
|
||||
placeholder: `是否备份上传的头像等文件`,
|
||||
},
|
||||
required: false,
|
||||
@@ -143,99 +141,119 @@ export class DBBackupPlugin extends AbstractPlusTaskPlugin {
|
||||
withUpload = true;
|
||||
|
||||
@TaskInput({
|
||||
title: '删除过期备份',
|
||||
title: "删除过期备份",
|
||||
component: {
|
||||
name: 'a-input-number',
|
||||
vModel: 'value',
|
||||
placeholder: '20',
|
||||
name: "a-input-number",
|
||||
vModel: "value",
|
||||
placeholder: "20",
|
||||
},
|
||||
helper: '删除多少天前的备份,不填则不删除,windows暂不支持',
|
||||
helper: "删除多少天前的备份,不填则不删除,windows暂不支持",
|
||||
required: false,
|
||||
})
|
||||
retainDays!: number;
|
||||
|
||||
async onInstance() {
|
||||
}
|
||||
async onInstance() {}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
|
||||
if (!this.isAdmin()) {
|
||||
throw new Error('只有管理员才能运行此任务');
|
||||
throw new Error("只有管理员才能运行此任务");
|
||||
}
|
||||
|
||||
this.logger.info('开始备份数据库');
|
||||
this.logger.info("开始备份数据库");
|
||||
|
||||
let dbPath = process.env.certd_typeorm_dataSource_default_database;
|
||||
dbPath = dbPath || './data/db.sqlite';
|
||||
dbPath = dbPath || "./data/db.sqlite";
|
||||
if (!fs.existsSync(dbPath)) {
|
||||
this.logger.error('数据库文件不存在:', dbPath);
|
||||
this.logger.error("数据库文件不存在:", dbPath);
|
||||
return;
|
||||
}
|
||||
const dbTmpFilename = `${this.filePrefix}_${dayjs().format('YYYYMMDD_HHmmss')}_sqlite`;
|
||||
const dbTmpFilename = `${this.filePrefix}_${dayjs().format("YYYYMMDD_HHmmss")}_sqlite`;
|
||||
const dbZipFilename = `${dbTmpFilename}.zip`;
|
||||
const tempDir = path.resolve(os.tmpdir(), 'certd_backup');
|
||||
const tempDir = path.resolve(os.tmpdir(), "certd_backup");
|
||||
if (!fs.existsSync(tempDir)) {
|
||||
await fs.promises.mkdir(tempDir, {recursive: true});
|
||||
await fs.promises.mkdir(tempDir, { recursive: true });
|
||||
}
|
||||
const dbTmpPath = path.resolve(tempDir, dbTmpFilename);
|
||||
const dbZipPath = path.resolve(tempDir, dbZipFilename);
|
||||
|
||||
//复制到临时目录
|
||||
await fs.promises.copyFile(dbPath, dbTmpPath);
|
||||
//本地压缩
|
||||
const zip = new JSZip();
|
||||
const stream = fs.createReadStream(dbTmpPath);
|
||||
// 使用流的方式添加文件内容
|
||||
zip.file(dbTmpFilename, stream, {binary: true, compression: 'DEFLATE'});
|
||||
try {
|
||||
//复制到临时目录
|
||||
await fs.promises.copyFile(dbPath, dbTmpPath);
|
||||
// //本地压缩
|
||||
// const zip = new JSZip();
|
||||
// const stream = fs.createReadStream(dbTmpPath);
|
||||
// // 使用流的方式添加文件内容
|
||||
// zip.file(dbTmpFilename, stream, {binary: true, compression: 'DEFLATE'});
|
||||
|
||||
const uploadDir = path.resolve('data', 'upload');
|
||||
if (this.withUpload && fs.existsSync(uploadDir)) {
|
||||
zip.folder(uploadDir);
|
||||
}
|
||||
// const uploadDir = path.resolve('data', 'upload');
|
||||
// if (this.withUpload && fs.existsSync(uploadDir)) {
|
||||
// zip.folder(uploadDir);
|
||||
// }
|
||||
|
||||
const content = await zip.generateAsync({type: 'nodebuffer'});
|
||||
// const content = await zip.generateAsync({type: 'nodebuffer'});
|
||||
|
||||
await fs.promises.writeFile(dbZipPath, content);
|
||||
this.logger.info(`数据库文件压缩完成:${dbZipPath}`);
|
||||
// await fs.promises.writeFile(dbZipPath, content);
|
||||
// 创建可写流
|
||||
const outputStream = fs.createWriteStream(dbZipPath);
|
||||
const zip = new JSZip();
|
||||
|
||||
this.logger.info('开始备份,当前备份方式:', this.backupMode);
|
||||
const backupDir = this.backupDir || defaultBackupDir;
|
||||
const backupFilePath = `${backupDir}/${dbZipFilename}`;
|
||||
// 添加数据库文件
|
||||
const dbStream = fs.createReadStream(dbTmpPath);
|
||||
zip.file(dbTmpFilename, dbStream, { binary: true, compression: "DEFLATE" });
|
||||
|
||||
try{
|
||||
if (this.backupMode === 'local') {
|
||||
// 处理上传目录
|
||||
const uploadDir = path.resolve("data", "upload");
|
||||
if (this.withUpload && fs.existsSync(uploadDir)) {
|
||||
zip.folder("upload"); // 注意:这里应该是相对路径
|
||||
}
|
||||
|
||||
// 使用流式生成
|
||||
const zipStream = zip.generateNodeStream({
|
||||
type: "nodebuffer",
|
||||
streamFiles: true,
|
||||
compression: "DEFLATE",
|
||||
});
|
||||
|
||||
// 管道传输
|
||||
await pipeline(zipStream, outputStream);
|
||||
this.logger.info(`数据库文件压缩完成:${dbZipPath}`);
|
||||
|
||||
this.logger.info("开始备份,当前备份方式:", this.backupMode);
|
||||
const backupDir = this.backupDir || defaultBackupDir;
|
||||
const backupFilePath = `${backupDir}/${dbZipFilename}`;
|
||||
|
||||
if (this.backupMode === "local") {
|
||||
await this.localBackup(dbZipPath, backupDir, backupFilePath);
|
||||
} else if (this.backupMode === 'ssh') {
|
||||
} else if (this.backupMode === "ssh") {
|
||||
await this.sshBackup(dbZipPath, backupDir, backupFilePath);
|
||||
} else if (this.backupMode === 'oss') {
|
||||
} else if (this.backupMode === "oss") {
|
||||
await this.ossBackup(dbZipPath, backupDir, backupFilePath);
|
||||
} else {
|
||||
throw new Error(`不支持的备份方式:${this.backupMode}`);
|
||||
}
|
||||
}finally{
|
||||
} finally {
|
||||
//删除临时目录
|
||||
await fs.promises.rm(tempDir, {recursive: true, force: true});
|
||||
await fs.promises.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
|
||||
this.logger.info('数据库备份完成');
|
||||
this.logger.info("数据库备份完成");
|
||||
}
|
||||
|
||||
private async localBackup(dbPath: string, backupDir: string, backupPath: string) {
|
||||
if (!backupPath.startsWith('/')) {
|
||||
backupPath = path.join('./data/', backupPath);
|
||||
if (!backupPath.startsWith("/")) {
|
||||
backupPath = path.join("./data/", backupPath);
|
||||
}
|
||||
const dir = path.dirname(backupPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
await fs.promises.mkdir(dir, {recursive: true});
|
||||
await fs.promises.mkdir(dir, { recursive: true });
|
||||
}
|
||||
backupPath = path.resolve(backupPath);
|
||||
await fs.promises.copyFile(dbPath, backupPath);
|
||||
this.logger.info('备份文件路径:', backupPath);
|
||||
this.logger.info("备份文件路径:", backupPath);
|
||||
|
||||
if (this.retainDays > 0) {
|
||||
// 删除过期备份
|
||||
this.logger.info('开始删除过期备份文件');
|
||||
this.logger.info("开始删除过期备份文件");
|
||||
const files = fs.readdirSync(dir);
|
||||
const now = Date.now();
|
||||
let count = 0;
|
||||
@@ -245,76 +263,76 @@ export class DBBackupPlugin extends AbstractPlusTaskPlugin {
|
||||
if (now - stat.mtimeMs > this.retainDays * 24 * 60 * 60 * 1000) {
|
||||
fs.unlinkSync(filePath as fs.PathLike);
|
||||
count++;
|
||||
this.logger.info('删除过期备份文件:', filePath);
|
||||
this.logger.info("删除过期备份文件:", filePath);
|
||||
}
|
||||
});
|
||||
this.logger.info('删除过期备份文件数:', count);
|
||||
this.logger.info("删除过期备份文件数:", count);
|
||||
}
|
||||
}
|
||||
|
||||
private async sshBackup(dbPath: string, backupDir: string, backupPath: string) {
|
||||
const access: SshAccess = await this.getAccess(this.sshAccessId);
|
||||
const sshClient = new SshClient(this.logger);
|
||||
this.logger.info('备份目录:', backupPath);
|
||||
this.logger.info("备份目录:", backupPath);
|
||||
await sshClient.uploadFiles({
|
||||
connectConf: access,
|
||||
transports: [{localPath: dbPath, remotePath: backupPath}],
|
||||
transports: [{ localPath: dbPath, remotePath: backupPath }],
|
||||
mkdirs: true,
|
||||
});
|
||||
this.logger.info('备份文件上传完成');
|
||||
this.logger.info("备份文件上传完成");
|
||||
|
||||
if (this.retainDays > 0) {
|
||||
// 删除过期备份
|
||||
this.logger.info('开始删除过期备份文件');
|
||||
this.logger.info("开始删除过期备份文件");
|
||||
const isWin = access.windows;
|
||||
let script: string[] = [];
|
||||
if (isWin) {
|
||||
throw new Error('删除过期文件暂不支持windows系统');
|
||||
throw new Error("删除过期文件暂不支持windows系统");
|
||||
// script = `forfiles /p ${backupDir} /s /d -${this.retainDays} /c "cmd /c del @path"`;
|
||||
} else {
|
||||
script = [`cd ${backupDir}`, 'echo 备份目录', 'pwd', `find . -type f -mtime +${this.retainDays} -name '${this.filePrefix}*' -exec rm -f {} \\;`];
|
||||
script = [`cd ${backupDir}`, "echo 备份目录", "pwd", `find . -type f -mtime +${this.retainDays} -name '${this.filePrefix}*' -exec rm -f {} \\;`];
|
||||
}
|
||||
|
||||
await sshClient.exec({
|
||||
connectConf: access,
|
||||
script,
|
||||
});
|
||||
this.logger.info('删除过期备份文件完成');
|
||||
this.logger.info("删除过期备份文件完成");
|
||||
}
|
||||
}
|
||||
|
||||
private async ossBackup(dbPath: string, backupDir: string, backupPath: string) {
|
||||
if (!this.ossAccessId) {
|
||||
throw new Error('未配置ossAccessId');
|
||||
throw new Error("未配置ossAccessId");
|
||||
}
|
||||
const access = await this.getAccess(this.ossAccessId);
|
||||
const ossType = this.ossType
|
||||
const ossType = this.ossType;
|
||||
|
||||
const ctx: OssClientContext = {
|
||||
logger: this.logger,
|
||||
utils: this.ctx.utils,
|
||||
accessService:this.accessService
|
||||
}
|
||||
accessService: this.accessService,
|
||||
};
|
||||
|
||||
this.logger.info(`开始备份文件到:${ossType}`);
|
||||
const client = await ossClientFactory.createOssClientByType(ossType, {
|
||||
const client = await ossClientFactory.createOssClientByType(ossType, {
|
||||
access,
|
||||
ctx,
|
||||
})
|
||||
});
|
||||
|
||||
await client.upload(backupPath, dbPath);
|
||||
|
||||
if (this.retainDays > 0) {
|
||||
// 删除过期备份
|
||||
this.logger.info('开始删除过期备份文件');
|
||||
this.logger.info("开始删除过期备份文件");
|
||||
const removeByOpts: OssClientRemoveByOpts = {
|
||||
dir: backupDir,
|
||||
beforeDays: this.retainDays,
|
||||
};
|
||||
await client.removeBy(removeByOpts);
|
||||
this.logger.info('删除过期备份文件完成');
|
||||
}else{
|
||||
this.logger.info('已禁止删除过期文件');
|
||||
this.logger.info("删除过期备份文件完成");
|
||||
} else {
|
||||
this.logger.info("已禁止删除过期文件");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
import { IAccessService } from '@certd/pipeline';
|
||||
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert';
|
||||
|
||||
import { AliesaAccess, AliyunAccess, AliyunClientV2 } from '@certd/plugin-lib';
|
||||
|
||||
@IsDnsProvider({
|
||||
name: 'aliesa',
|
||||
title: '阿里ESA',
|
||||
desc: '阿里ESA DNS解析',
|
||||
accessType: 'aliesa',
|
||||
icon: 'svg:icon-aliyun',
|
||||
order: 0,
|
||||
})
|
||||
export class AliesaDnsProvider extends AbstractDnsProvider {
|
||||
|
||||
|
||||
client: AliyunClientV2
|
||||
async onInstance() {
|
||||
const access: AliesaAccess = this.ctx.access as AliesaAccess
|
||||
const accessGetter = await this.ctx.serviceGetter.get("accessService") as IAccessService
|
||||
const aliAccess = await accessGetter.getById(access.accessId) as AliyunAccess
|
||||
const endpoint = `esa.${access.region}.aliyuncs.com`
|
||||
this.client = aliAccess.getClient(endpoint)
|
||||
}
|
||||
|
||||
|
||||
async getSiteItem(domain: string) {
|
||||
const ret = await this.client.doRequest({
|
||||
// 接口名称
|
||||
action: "ListSites",
|
||||
// 接口版本
|
||||
version: "2024-09-10",
|
||||
// 接口协议
|
||||
protocol: "HTTPS",
|
||||
// 接口 HTTP 方法
|
||||
method: "GET",
|
||||
authType: "AK",
|
||||
style: "RPC",
|
||||
data: {
|
||||
query: {
|
||||
SiteName: domain,
|
||||
// ["SiteSearchType"] = "exact";
|
||||
SiteSearchType: "exact",
|
||||
AccessType: "NS"
|
||||
}
|
||||
}
|
||||
})
|
||||
const list = ret.Sites
|
||||
if (list?.length === 0) {
|
||||
throw new Error(`阿里云ESA中不存在此域名站点:${domain},请确认域名已添加到ESA中,且为NS接入方式`);
|
||||
}
|
||||
return list[0]
|
||||
|
||||
}
|
||||
|
||||
async createRecord(options: CreateRecordOptions): Promise<any> {
|
||||
const { fullRecord, value, type, domain } = options;
|
||||
this.logger.info('添加域名解析:', fullRecord, value, domain);
|
||||
|
||||
|
||||
const siteItem = await this.getSiteItem(domain)
|
||||
const siteId = siteItem.SiteId
|
||||
|
||||
|
||||
const res = await this.client.doRequest({
|
||||
action: "CreateRecord",
|
||||
version: "2024-09-10",
|
||||
method: "POST",
|
||||
data: {
|
||||
query: {
|
||||
SiteId: siteId,
|
||||
RecordName: fullRecord,
|
||||
Type: type,
|
||||
// queries["Ttl"] = 1231311;
|
||||
Ttl: 100,
|
||||
Data: JSON.stringify({ Value: value }),
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.logger.info('添加域名解析成功:', fullRecord, value, res.RecordId);
|
||||
return {
|
||||
RecordId: res.RecordId,
|
||||
SiteId: siteId,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async removeRecord(options: RemoveRecordOptions<any>): Promise<any> {
|
||||
const record = options.recordRes;
|
||||
|
||||
await this.client.doRequest({
|
||||
action: "DeleteRecord",
|
||||
version: "2024-09-10",
|
||||
data: {
|
||||
query: {
|
||||
RecordId: record.RecordId,
|
||||
}
|
||||
}
|
||||
})
|
||||
this.logger.info('删除域名解析成功:', record.RecordId);
|
||||
}
|
||||
}
|
||||
|
||||
new AliesaDnsProvider();
|
||||
@@ -1 +1,2 @@
|
||||
import './aliyun-dns-provider.js';
|
||||
import './aliesa-dns-provider.js';
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
createRemoteSelectInputDefine
|
||||
} from "@certd/plugin-lib";
|
||||
import { CertApplyPluginNames, CertInfo, CertReader } from "@certd/plugin-cert";
|
||||
import {optionsUtils} from "@certd/basic/dist/utils/util.options.js";
|
||||
import {optionsUtils} from "@certd/basic";
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: 'DeployCertToAliyunApig',
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import {AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput} from '@certd/pipeline';
|
||||
import {AliyunAccess, createCertDomainGetterInputDefine, createRemoteSelectInputDefine} from "@certd/plugin-lib";
|
||||
import {CertApplyPluginNames, CertInfo} from '@certd/plugin-cert';
|
||||
import {optionsUtils} from "@certd/basic/dist/utils/util.options.js";
|
||||
import {optionsUtils} from "@certd/basic";
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: 'DeployCertToAliyunApiGateway',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
|
||||
import { AliyunAccess, AliyunClient, AliyunSslClient, createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from '@certd/plugin-lib';
|
||||
import { optionsUtils } from '@certd/basic/dist/utils/util.options.js';
|
||||
import { optionsUtils } from '@certd/basic';
|
||||
import { CertApplyPluginNames, CertReader } from "@certd/plugin-cert";
|
||||
@IsTaskPlugin({
|
||||
name: 'DeployCertToAliyunCDN',
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "@certd/plugin-lib";
|
||||
import { CertInfo } from '@certd/plugin-cert';
|
||||
import { CertApplyPluginNames} from '@certd/plugin-cert';
|
||||
import { optionsUtils } from "@certd/basic/dist/utils/util.options.js";
|
||||
import { optionsUtils } from "@certd/basic";
|
||||
@IsTaskPlugin({
|
||||
name: 'DeployCertToAliyunDCDN',
|
||||
title: '阿里云-部署证书至DCDN',
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
title: "阿里云-部署至ESA",
|
||||
icon: "svg:icon-aliyun",
|
||||
group: pluginGroups.aliyun.key,
|
||||
desc: "部署证书到阿里云ESA(边缘安全加速)",
|
||||
desc: "部署证书到阿里云ESA(边缘安全加速),自动删除过期证书",
|
||||
needPlus: false,
|
||||
default: {
|
||||
strategy: {
|
||||
@@ -125,6 +125,7 @@ export class AliyunDeployCertToESA extends AbstractTaskPlugin {
|
||||
const { certId, certName } = await this.getAliyunCertId(access);
|
||||
|
||||
for (const siteId of this.siteIds) {
|
||||
|
||||
try {
|
||||
const res = await client.doRequest({
|
||||
// 接口名称
|
||||
@@ -159,6 +160,7 @@ export class AliyunDeployCertToESA extends AbstractTaskPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async getClient(access: AliyunAccess) {
|
||||
const endpoint = `esa.${this.regionId}.aliyuncs.com`;
|
||||
return access.getClient(endpoint);
|
||||
@@ -211,19 +213,24 @@ export class AliyunDeployCertToESA extends AbstractTaskPlugin {
|
||||
this.logger.info(`证书${item.Name}状态:${item.Status}`);
|
||||
if (item.Status === "Expired") {
|
||||
this.logger.info(`证书${item.Name}已过期,执行删除`);
|
||||
await client.doRequest({
|
||||
action: "DeleteCertificate",
|
||||
version: "2024-09-10",
|
||||
// 接口 HTTP 方法
|
||||
method: "GET",
|
||||
data:{
|
||||
query: {
|
||||
SiteId: siteId,
|
||||
Id: item.id
|
||||
}
|
||||
}
|
||||
});
|
||||
this.logger.info(`证书${item.Name}已删除`);
|
||||
try{
|
||||
await client.doRequest({
|
||||
action: "DeleteCertificate",
|
||||
version: "2024-09-10",
|
||||
// 接口 HTTP 方法
|
||||
method: "GET",
|
||||
data:{
|
||||
query: {
|
||||
SiteId: siteId,
|
||||
Id: item.id
|
||||
}
|
||||
}
|
||||
});
|
||||
this.logger.info(`证书${item.Name}已删除`);
|
||||
}catch (e) {
|
||||
this.logger.error(`过期证书${item.Name}删除失败:`,e.message)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from '@certd/plugin-lib';
|
||||
import {CertInfo, CertReader} from '@certd/plugin-cert';
|
||||
import { CertApplyPluginNames} from '@certd/plugin-cert';
|
||||
import {optionsUtils} from "@certd/basic/dist/utils/util.options.js";
|
||||
import {optionsUtils} from "@certd/basic";
|
||||
import {isArray} from "lodash-es";
|
||||
@IsTaskPlugin({
|
||||
name: 'DeployCertToAliyunOSS',
|
||||
@@ -121,7 +121,7 @@ export class DeployCertToAliyunOSS extends AbstractTaskPlugin {
|
||||
name: 'a-select',
|
||||
options: [
|
||||
{ value: 'cn-hangzhou', label: '中国大陆' },
|
||||
{ value: 'southeast-1', label: '新加坡' },
|
||||
{ value: 'ap-southeast-1', label: '新加坡' },
|
||||
{ value: 'eu-central-1', label: '德国(法兰克福)' },
|
||||
],
|
||||
},
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||
import { AwsAccess, AwsRegions } from "../access.js";
|
||||
import { AwsAcmClient } from "../libs/aws-acm-client.js";
|
||||
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
import { optionsUtils } from "@certd/basic/dist/utils/util.options.js";
|
||||
import { optionsUtils } from "@certd/basic";
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: 'AwsDeployToCloudFront',
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './geetest/index.js';
|
||||
export * from './image/index.js';
|
||||
export * from './tencent/index.js';
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { AddonInput, BaseAddon, IsAddon } from "@certd/lib-server";
|
||||
import { ICaptchaAddon } from "../api.js";
|
||||
import { TencentAccess } from "@certd/plugin-lib";
|
||||
|
||||
@IsAddon({
|
||||
addonType: "captcha",
|
||||
name: "tencent",
|
||||
title: "腾讯云验证码",
|
||||
desc: "",
|
||||
showTest: false,
|
||||
})
|
||||
export class TencentCaptcha extends BaseAddon implements ICaptchaAddon {
|
||||
@AddonInput({
|
||||
title: "腾讯云授权",
|
||||
helper: "腾讯云授权",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
vModel: "modelValue",
|
||||
from: "sys",
|
||||
type: "tencent", //固定授权类型
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId: number;
|
||||
|
||||
@AddonInput({
|
||||
title: "验证ID",
|
||||
component: {
|
||||
name: "a-input-number",
|
||||
placeholder: "CaptchaAppId",
|
||||
},
|
||||
helper: "[腾讯云验证码](https://cloud.tencent.com/act/cps/redirect?redirect=37716&cps_key=b3ef73330335d7a6efa4a4bbeeb6b2c9)",
|
||||
required: true,
|
||||
})
|
||||
captchaAppId: number;
|
||||
|
||||
@AddonInput({
|
||||
title: "验证Key",
|
||||
component: {
|
||||
placeholder: "AppSecretKey",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
appSecretKey = "";
|
||||
|
||||
async onValidate(data?: any) {
|
||||
if (!data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const access = await this.getAccess<TencentAccess>(this.accessId);
|
||||
|
||||
const sdk = await import("tencentcloud-sdk-nodejs/tencentcloud/services/captcha/v20190722/index.js");
|
||||
|
||||
const CaptchaClient = sdk.v20190722.Client;
|
||||
|
||||
const clientConfig = {
|
||||
credential: {
|
||||
secretId: access.secretId,
|
||||
secretKey: access.secretKey,
|
||||
},
|
||||
region: "",
|
||||
profile: {
|
||||
httpProfile: {
|
||||
endpoint: "captcha.tencentcloudapi.com",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// 实例化要请求产品的client对象,clientProfile是可选的
|
||||
const client = new CaptchaClient(clientConfig);
|
||||
const params = {
|
||||
CaptchaType: 9, //固定值9
|
||||
UserIp: "127.0.0.1",
|
||||
Ticket: data.ticket,
|
||||
Randstr: data.randstr,
|
||||
AppSecretKey: this.appSecretKey,
|
||||
CaptchaAppId: this.captchaAppId,
|
||||
};
|
||||
try {
|
||||
const res = await client.DescribeCaptchaResult(params);
|
||||
if (res.CaptchaCode == 1) {
|
||||
// 验证成功
|
||||
// verification successful
|
||||
return true;
|
||||
} else {
|
||||
// 验证失败
|
||||
// verification failed
|
||||
this.logger.error("腾讯云验证码验证失败", res.CaptchaMsg);
|
||||
return false;
|
||||
}
|
||||
} catch (err) {
|
||||
if (data.ticket.startsWith("trerror_") && err.message.includes("账户已欠费")) {
|
||||
this.logger.error("腾讯云验证码账户欠费,临时放行:", err.message);
|
||||
return true;
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async getCaptcha(): Promise<any> {
|
||||
return {
|
||||
captchaAppId: this.captchaAppId,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
|
||||
import { CertInfo, CertReader } from '@certd/plugin-cert';
|
||||
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from '@certd/plugin-lib';
|
||||
import { optionsUtils } from '@certd/basic/dist/utils/util.options.js';
|
||||
import { optionsUtils } from '@certd/basic';
|
||||
import { CertApplyPluginNames} from '@certd/plugin-cert';
|
||||
@IsTaskPlugin({
|
||||
//命名规范,插件类型+功能(就是目录plugin-demo中的demo),大写字母开头,驼峰命名
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
SshAccess,
|
||||
SshClient
|
||||
} from "@certd/plugin-lib";
|
||||
import path from "node:path";
|
||||
|
||||
@IsTaskPlugin({
|
||||
//命名规范,插件类型+功能(就是目录plugin-demo中的demo),大写字母开头,驼峰命名
|
||||
@@ -57,7 +56,7 @@ export class FnOSDeployToNAS extends AbstractTaskPlugin {
|
||||
@TaskInput(
|
||||
createRemoteSelectInputDefine({
|
||||
title: "证书Id",
|
||||
helper: "要更新的证书id",
|
||||
helper: "面板证书请选择fnOS,其他FTP、webdav等证书请选择已使用,可多选(如果证书域名都匹配的话)",
|
||||
action: FnOSDeployToNAS.prototype.onGetCertList.name
|
||||
})
|
||||
)
|
||||
@@ -87,7 +86,9 @@ export class FnOSDeployToNAS extends AbstractTaskPlugin {
|
||||
this.logger.info(`----------- 找到证书,开始部署:${item.sum},${item.domain}`)
|
||||
const certPath = item.certificate;
|
||||
const keyPath = item.privateKey;
|
||||
const certDir = path.dirname(keyPath)
|
||||
const certDir = keyPath.substring(0, keyPath.lastIndexOf("/"));
|
||||
const fullchainPath = certDir+ "/fullchain.crt"
|
||||
const caPath = certDir+ "/issuer_certificate.crt"
|
||||
const cmd = `
|
||||
sudo tee ${certPath} > /dev/null <<'EOF'
|
||||
${this.cert.crt}
|
||||
@@ -95,6 +96,12 @@ EOF
|
||||
sudo tee ${keyPath} > /dev/null <<'EOF'
|
||||
${this.cert.key}
|
||||
EOF
|
||||
sudo tee ${fullchainPath} > /dev/null <<'EOF'
|
||||
${this.cert.crt}
|
||||
EOF
|
||||
sudo tee ${caPath} > /dev/null <<'EOF'
|
||||
${this.cert.ic}
|
||||
EOF
|
||||
|
||||
sudo chmod 0755 "${certDir}/" -R
|
||||
|
||||
@@ -157,7 +164,7 @@ echo "服务重启完成!"
|
||||
}
|
||||
|
||||
if (!list || list.length === 0) {
|
||||
throw new Error("没有找到证书,请先在证书管理也没上传一次证书");
|
||||
throw new Error("没有找到证书,请先在证书管理页面上传一次证书");
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
@@ -246,7 +246,7 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
|
||||
rows: 5,
|
||||
placeholder: 'systemctl restart nginx ',
|
||||
},
|
||||
helper: '上传后执行脚本命令,让证书生效(比如重启nginx),不填则不执行\n注意:sudo需要配置免密\n注意:如果目标主机是windows,且终端是cmd,系统会自动将多行命令通过“&&”连接成一行',
|
||||
helper: '上传后执行脚本命令,让证书生效(比如重启nginx),不填则不执行\n注意:sudo需要配置免密,不要使用-i这种交互式命令\n注意:如果目标主机是windows,且终端是cmd,系统会自动将多行命令通过“&&”连接成一行',
|
||||
required: false,
|
||||
})
|
||||
script!: string;
|
||||
|
||||
@@ -10,7 +10,7 @@ import { resetLogConfigure } from "@certd/basic";
|
||||
title: '华为云-上传证书至CCM',
|
||||
icon: 'svg:icon-huawei',
|
||||
group: pluginGroups.huawei.key,
|
||||
desc: '上传证书到华为云CCM',
|
||||
desc: '上传证书到华为云云证书管理(CCM)',
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||
import { optionsUtils } from "@certd/basic/dist/utils/util.options.js";
|
||||
import { optionsUtils } from "@certd/basic";
|
||||
import { JDCloudAccess } from "../access.js";
|
||||
|
||||
@IsTaskPlugin({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||
import { optionsUtils } from "@certd/basic/dist/utils/util.options.js";
|
||||
import { optionsUtils } from "@certd/basic";
|
||||
import { JDCloudAccess } from "../access.js";
|
||||
|
||||
@IsTaskPlugin({
|
||||
|
||||
@@ -4,6 +4,7 @@ import { BaseNotification, IsNotification, NotificationBody, NotificationInput }
|
||||
name: 'email',
|
||||
title: '电子邮件',
|
||||
desc: '电子邮件通知',
|
||||
order: -100,
|
||||
})
|
||||
export class EmailNotification extends BaseNotification {
|
||||
@NotificationInput({
|
||||
|
||||
@@ -12,4 +12,5 @@ export * from './bark/index.js';
|
||||
export * from './feishu/index.js';
|
||||
export * from './dingtalk/index.js';
|
||||
export * from './vocechat/index.js';
|
||||
export * from './onebot/index.js';
|
||||
export * from './onebot/index.js';
|
||||
export * from './meow/index.js';
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { BaseNotification, IsNotification, NotificationBody, NotificationInput } from '@certd/pipeline';
|
||||
|
||||
/**
|
||||
* POST请求
|
||||
支持格式
|
||||
application/json
|
||||
text/plain
|
||||
multipart/form-data
|
||||
application/x-www-form-urlencoded
|
||||
表单格式
|
||||
POST /{昵称}/[title] HTTP/1.1
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
title=可选标题&msg=必填内容&url=可选链接&msgType=html&htmlHeight=400
|
||||
|
||||
纯文本格式
|
||||
POST /{昵称}/[title] HTTP/1.1
|
||||
Content-Type: text/plain
|
||||
|
||||
这里放置消息内容
|
||||
|
||||
POST JSON示例
|
||||
POST /JohnDoe?msgType=html&htmlHeight=350 HTTP/1.1
|
||||
Host: api.chuckfang.com
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"title": "系统通知",
|
||||
"msg": "<p><b>欢迎使用</b>,这是 <i>HTML</i> 格式的消息</p>",
|
||||
"url": "https://example.com"
|
||||
}
|
||||
|
||||
===
|
||||
返回值:
|
||||
{
|
||||
"status": 200,
|
||||
"message": "推送成功"
|
||||
}
|
||||
*/
|
||||
@IsNotification({
|
||||
name: 'meow',
|
||||
title: 'MeoW通知',
|
||||
desc: 'https://api.chuckfang.com/',
|
||||
needPlus: false,
|
||||
})
|
||||
export class MeowNotification extends BaseNotification {
|
||||
|
||||
@NotificationInput({
|
||||
title: 'MeoW接口地址',
|
||||
component: {
|
||||
placeholder: 'https://api.xxxxxx.com',
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
endpoint = '';
|
||||
|
||||
@NotificationInput({
|
||||
title: '昵称',
|
||||
component: {
|
||||
placeholder: '',
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
nickName = '';
|
||||
|
||||
async send(body: NotificationBody) {
|
||||
if (!this.nickName) {
|
||||
throw new Error('昵称不能为空');
|
||||
}
|
||||
let endpoint = this.endpoint;
|
||||
if (!endpoint.endsWith('/')) {
|
||||
endpoint += '/';
|
||||
}
|
||||
const url = `${endpoint}${this.nickName}/`;
|
||||
const res = await this.http.request({
|
||||
url: url,
|
||||
method: 'POST',
|
||||
data: {
|
||||
text: body.title,
|
||||
msg: body.content,
|
||||
url: body.url,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error(res.message || res.msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import qs from 'qs';
|
||||
name: 'webhook',
|
||||
title: '自定义webhook',
|
||||
desc: '根据模版自定义http请求',
|
||||
order: -100,
|
||||
})
|
||||
export class WebhookNotification extends BaseNotification {
|
||||
@NotificationInput({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
|
||||
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine, QiniuAccess, QiniuClient } from '@certd/plugin-lib';
|
||||
import { CertInfo } from '@certd/plugin-cert';
|
||||
import { optionsUtils } from '@certd/basic/dist/utils/util.options.js';
|
||||
import { optionsUtils } from '@certd/basic';
|
||||
import { CertApplyPluginNames} from '@certd/plugin-cert';
|
||||
@IsTaskPlugin({
|
||||
name: 'QiniuDeployCertToCDN',
|
||||
|
||||
@@ -2,7 +2,7 @@ import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput
|
||||
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||
import { UpyunAccess } from "../access.js";
|
||||
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
import { optionsUtils } from "@certd/basic/dist/utils/util.options.js";
|
||||
import { optionsUtils } from "@certd/basic";
|
||||
import { UpyunClient } from "../client.js";
|
||||
|
||||
@IsTaskPlugin({
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||
import { optionsUtils } from "@certd/basic/dist/utils/util.options.js";
|
||||
import { optionsUtils } from "@certd/basic";
|
||||
import { VolcengineAccess } from "../access.js";
|
||||
import { VolcengineCdnClient } from "../cdn-client.js";
|
||||
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import { IsAccess, AccessInput, BaseAccess, Pager, PageSearch } from "@certd/pipeline";
|
||||
import crypto from "crypto";
|
||||
/**
|
||||
* 这个注解将注册一个授权配置
|
||||
* 在certd的后台管理系统中,用户可以选择添加此类型的授权
|
||||
*/
|
||||
@IsAccess({
|
||||
name: "xinnetagent",
|
||||
title: "新网授权(代理方式)",
|
||||
icon: "lsicon:badge-new-filled",
|
||||
desc: ""
|
||||
})
|
||||
export class XinnetAgentAccess extends BaseAccess {
|
||||
|
||||
/**
|
||||
* 授权属性配置
|
||||
*/
|
||||
@AccessInput({
|
||||
title: "代理账号",
|
||||
component: {
|
||||
placeholder: "代理账号,如:agent0001"
|
||||
},
|
||||
required: true,
|
||||
encrypt: false
|
||||
})
|
||||
agentCode = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "API密钥",
|
||||
component: {
|
||||
name: "a-input-password",
|
||||
vModel: "value",
|
||||
placeholder: "API密钥"
|
||||
},
|
||||
required: true,
|
||||
encrypt: true
|
||||
})
|
||||
appSecret = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "测试",
|
||||
component: {
|
||||
name: "api-test",
|
||||
action: "TestRequest"
|
||||
},
|
||||
helper: "点击测试接口是否正常"
|
||||
})
|
||||
testRequest = true;
|
||||
|
||||
async onTestRequest() {
|
||||
|
||||
// const client = new XinnetClient({
|
||||
// access: this,
|
||||
// logger: this.ctx.logger,
|
||||
// http: this.ctx.http
|
||||
// });
|
||||
await this.getDomainList({ pageNo: 1, pageSize: 1 });
|
||||
|
||||
return "ok";
|
||||
}
|
||||
|
||||
|
||||
|
||||
async getDomainList(req:PageSearch) {
|
||||
const pager = new Pager(req);
|
||||
const conf = {
|
||||
url: "/api/domain/list",
|
||||
data: {
|
||||
pageNo: String(pager.pageNo),
|
||||
pageSize: String(pager.pageSize)
|
||||
}
|
||||
}
|
||||
return await this.doRequest(conf);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 生成 UTC 0 时区的时间戳
|
||||
*/
|
||||
generateTimestamp() {
|
||||
const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, "Z").replaceAll(":", "").replaceAll("-", "");
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 字节转16进制字符串
|
||||
*/
|
||||
bytesToHex(bytes:any) {
|
||||
return bytes.toString('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成签名
|
||||
*/
|
||||
generateSignature(timestamp, urlPath, requestBody) {
|
||||
const algorithm = 'HMAC-SHA256';
|
||||
const requestMethod = 'POST';
|
||||
|
||||
// 构建待签名字符串
|
||||
const stringToSign = `${algorithm}\n${timestamp}\n${requestMethod}\n${urlPath}\n${requestBody}`;
|
||||
|
||||
// 使用 HMAC-SHA256 计算签名
|
||||
const hmac = crypto.createHmac('sha256', this.appSecret);
|
||||
hmac.update(stringToSign);
|
||||
const signatureBytes = hmac.digest();
|
||||
|
||||
// 转换为16进制字符串
|
||||
return this.bytesToHex(signatureBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 authorization header
|
||||
*/
|
||||
generateAuthorization(timestamp, urlPath, requestBody) {
|
||||
const signature = this.generateSignature(timestamp, urlPath, requestBody);
|
||||
return `HMAC-SHA256 Access=${this.agentCode}, Signature=${signature}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询域名分页列表
|
||||
*/
|
||||
async doRequest(req:any) {
|
||||
|
||||
const baseURL = 'https://apiv2.xinnet.com';
|
||||
const urlPath = req.url;
|
||||
const requestURL = baseURL + urlPath; // 实际请求URL去掉最后的斜杠
|
||||
|
||||
// 请求体
|
||||
const requestBody = JSON.stringify(req.data);
|
||||
|
||||
// 生成时间戳和授权头
|
||||
const timestamp = this.generateTimestamp();
|
||||
const authorization = this.generateAuthorization(timestamp, urlPath+"/", requestBody);
|
||||
|
||||
// 请求配置
|
||||
const config = {
|
||||
method: 'POST',
|
||||
url: requestURL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'timestamp': timestamp,
|
||||
'authorization': authorization
|
||||
},
|
||||
data: requestBody,
|
||||
};
|
||||
|
||||
const res = await this.ctx.http.request(config);
|
||||
|
||||
if (res.code !="0"){
|
||||
throw new Error(`API Error: ${res.code} ${res.requestId} - ${JSON.stringify(res.msg)}`);
|
||||
}
|
||||
return res.data;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
new XinnetAgentAccess();
|
||||
@@ -0,0 +1,90 @@
|
||||
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert";
|
||||
import { XinnetAgentAccess } from "./access-agent.js";
|
||||
|
||||
export type XinnetAgentRecord = {
|
||||
recordId: number;
|
||||
domainName: string;
|
||||
};
|
||||
|
||||
// 这里通过IsDnsProvider注册一个dnsProvider
|
||||
@IsDnsProvider({
|
||||
name: "xinnetagent",
|
||||
title: "新网(代理方式)",
|
||||
desc: "新网域名解析(代理方式)",
|
||||
icon: "lsicon:badge-new-filled",
|
||||
// 这里是对应的 cloudflare的access类型名称
|
||||
accessType: "xinnetagent",
|
||||
order: 7
|
||||
})
|
||||
export class XinnetAgentProvider extends AbstractDnsProvider<XinnetAgentRecord> {
|
||||
access!: XinnetAgentAccess;
|
||||
|
||||
async onInstance() {
|
||||
//一些初始化的操作
|
||||
// 也可以通过ctx成员变量传递context
|
||||
this.access = this.ctx.access as XinnetAgentAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建dns解析记录,用于验证域名所有权
|
||||
*/
|
||||
async createRecord(options: CreateRecordOptions): Promise<XinnetAgentRecord> {
|
||||
/**
|
||||
* fullRecord: '_acme-challenge.test.example.com',
|
||||
* value: 一串uuid
|
||||
* type: 'TXT',
|
||||
* domain: 'example.com'
|
||||
*/
|
||||
const { fullRecord, value, type, domain } = options;
|
||||
this.logger.info("添加域名解析:", fullRecord, value, type, domain);
|
||||
|
||||
|
||||
/**
|
||||
* /api/dns/create
|
||||
* domainName 是 string 域名名称 test-xinnet-0516-ceshi.cn
|
||||
recordName 是 string 记录名 test1.test-xinnet-0516-ceshi.cn,如果是@和空字符只需要传域名即可
|
||||
type 是 string 解析记录的类型 可选择类型如下: NS A CNAME MX TXT URL SRV AAAA A
|
||||
value 是 string 解析内容 192.168.1.50
|
||||
line 是 string 线路 只能传"默认"
|
||||
*/
|
||||
|
||||
const res = await this.access.doRequest({
|
||||
url:"/api/dns/create",
|
||||
data:{
|
||||
domainName: domain,
|
||||
recordName: fullRecord,
|
||||
type: type,
|
||||
value: value,
|
||||
line: "默认"
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
recordId:res,
|
||||
domainName: domain
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 删除dns解析记录,清理申请痕迹
|
||||
* @param options
|
||||
*/
|
||||
async removeRecord(options: RemoveRecordOptions<XinnetAgentRecord>): Promise<void> {
|
||||
|
||||
const {domainName,recordId} = options.recordRes;
|
||||
await this.access.doRequest({
|
||||
url:"/api/dns/delete",
|
||||
data:{
|
||||
recordId: recordId,
|
||||
domainName: domainName
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
//实例化这个provider,将其自动注册到系统中
|
||||
new XinnetAgentProvider();
|
||||
@@ -1,2 +1,5 @@
|
||||
export * from './dns-provider.js';
|
||||
export * from './access.js';
|
||||
|
||||
export * from './access-agent.js';
|
||||
export * from './dns-provider-agent.js';
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import { IsAccess, AccessInput, BaseAccess } from '@certd/pipeline';
|
||||
|
||||
/**
|
||||
* 这个注解将注册一个授权配置
|
||||
* 在certd的后台管理系统中,用户可以选择添加此类型的授权
|
||||
*/
|
||||
@IsAccess({
|
||||
name: 'xinnetconnect',
|
||||
title: '新网互联授权',
|
||||
icon: 'lsicon:badge-new-filled',
|
||||
desc: '仅支持代理账号,ip需要加入白名单',
|
||||
})
|
||||
export class XinnetConnectAccess extends BaseAccess {
|
||||
/**
|
||||
* 授权属性配置
|
||||
*/
|
||||
@AccessInput({
|
||||
title: '用户名',
|
||||
component: {
|
||||
placeholder: '代理用户名,如:agent001',
|
||||
help: '新网互联的代理用户名',
|
||||
},
|
||||
required: true,
|
||||
encrypt: false,
|
||||
})
|
||||
username = '';
|
||||
|
||||
@AccessInput({
|
||||
title: '密码',
|
||||
component: {
|
||||
name: "a-input-password",
|
||||
vModel: "value",
|
||||
placeholder: '密码',
|
||||
},
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
password = '';
|
||||
|
||||
|
||||
|
||||
async addDnsRecord(req: {domain:string,hostRecord:string, value:string, type:string}): Promise<any> {
|
||||
const { domain,hostRecord, value, type } = req;
|
||||
const bodyXml =`
|
||||
<add>
|
||||
<domainname>${domain}</domainname>
|
||||
<resolvetype>${type}</resolvetype>
|
||||
<resolvehost>${hostRecord}</resolvehost>
|
||||
<resolvevalue>${value}</resolvevalue>
|
||||
<mxlevel>10</mxlevel>
|
||||
</add>`
|
||||
|
||||
const res = await this.doRequest({
|
||||
url: "/addDnsRecordService",
|
||||
bodyXml: bodyXml,
|
||||
service: "addDnsRecord",
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
async delDnsRecord(req: {domain:string,hostRecord:string, type:string,value:string}): Promise<any> {
|
||||
const { domain,hostRecord, type,value } = req;
|
||||
const bodyXml =`
|
||||
<del>
|
||||
<domainname>${domain}</domainname>
|
||||
<resolvetype>${type}</resolvetype>
|
||||
<resolvehost>${hostRecord}</resolvehost>
|
||||
<resolveoldvalue>${value}</resolveoldvalue>
|
||||
<mxlevel>10</mxlevel>
|
||||
</del>`
|
||||
|
||||
const res = await this.doRequest({
|
||||
url: "/delDnsRecordService",
|
||||
bodyXml: bodyXml,
|
||||
service: "delDnsRecord",
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
|
||||
|
||||
buildUserXml(){
|
||||
return `
|
||||
<user>
|
||||
<name>${this.username}</name>
|
||||
<password>${this.password}</password>
|
||||
</user>
|
||||
`
|
||||
}
|
||||
|
||||
|
||||
async doRequest(req: {bodyXml:string,service:string,url:string}) {
|
||||
|
||||
const xml2js = await import('xml2js');
|
||||
|
||||
const soapRequest = `
|
||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ws="http://ws/">
|
||||
<soapenv:Header/>
|
||||
<soapenv:Body>
|
||||
<ws:${req.service}>
|
||||
${this.buildUserXml()}
|
||||
${req.bodyXml}
|
||||
</ws:${req.service}>
|
||||
</soapenv:Body>
|
||||
</soapenv:Envelope>
|
||||
`;
|
||||
|
||||
|
||||
const response = await this.ctx.http.request({
|
||||
url: req.url,
|
||||
baseURL: "https://api.bizcn.com/rrpservices",
|
||||
data: soapRequest,
|
||||
headers: {
|
||||
'Content-Type': 'text/xml; charset=utf-8',
|
||||
'SOAPAction': '' // 根据WSDL,soapAction为空
|
||||
},
|
||||
method: "POST",
|
||||
returnOriginRes: true,
|
||||
})
|
||||
|
||||
|
||||
// 解析SOAP响应
|
||||
const parser = new xml2js.Parser({ explicitArray: false });
|
||||
const result = await parser.parseStringPromise(response.data);
|
||||
|
||||
// 提取返回结果
|
||||
const soapBody = result['soap:Envelope']['soap:Body'];
|
||||
const addDnsRecordResponse = soapBody["ns1:addDnsRecordResponse"];
|
||||
console.log(addDnsRecordResponse)
|
||||
const resultData = addDnsRecordResponse.response.result;
|
||||
|
||||
const res = {
|
||||
code: resultData.$.code,
|
||||
msg: resultData.msg
|
||||
}
|
||||
console.log('操作结果:', res);
|
||||
|
||||
if (res.code != "200") {
|
||||
throw new Error(res.msg + " code:" + res.code);
|
||||
}
|
||||
|
||||
return resultData;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
new XinnetConnectAccess();
|
||||
@@ -0,0 +1,68 @@
|
||||
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert";
|
||||
import { XinnetConnectAccess } from "./access.js";
|
||||
|
||||
|
||||
export type XinnetConnectRecord = {
|
||||
domain: string;
|
||||
hostRecord: string;
|
||||
type: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
// 这里通过IsDnsProvider注册一个dnsProvider
|
||||
@IsDnsProvider({
|
||||
name: 'xinnetconnect',
|
||||
title: '新网互联',
|
||||
desc: '新网互联',
|
||||
icon: 'lsicon:badge-new-filled',
|
||||
// 这里是对应的 cloudflare的access类型名称
|
||||
accessType: 'xinnetconnect',
|
||||
order:999,
|
||||
})
|
||||
export class XinnetConnectDnsProvider extends AbstractDnsProvider<XinnetConnectRecord> {
|
||||
access!: XinnetConnectAccess;
|
||||
|
||||
async onInstance() {
|
||||
//一些初始化的操作
|
||||
// 也可以通过ctx成员变量传递context
|
||||
this.access = this.ctx.access as XinnetConnectAccess;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建dns解析记录,用于验证域名所有权
|
||||
*/
|
||||
async createRecord(options: CreateRecordOptions): Promise<XinnetConnectRecord> {
|
||||
const { fullRecord,hostRecord, value, type, domain } = options;
|
||||
this.logger.info('添加域名解析:', fullRecord, value, type, domain);
|
||||
|
||||
const recordReq = {
|
||||
domain: domain,
|
||||
type: 'TXT',
|
||||
hostRecord: hostRecord,
|
||||
value: value,
|
||||
}
|
||||
await this.access.addDnsRecord(recordReq)
|
||||
return recordReq;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 删除dns解析记录,清理申请痕迹
|
||||
* @param options
|
||||
*/
|
||||
async removeRecord(options: RemoveRecordOptions<XinnetConnectRecord>): Promise<void> {
|
||||
const { fullRecord, value } = options.recordReq;
|
||||
const record = options.recordRes;
|
||||
this.logger.info('删除域名解析:', fullRecord, value);
|
||||
if (!record) {
|
||||
this.logger.info('record为空,不执行删除');
|
||||
return;
|
||||
}
|
||||
await this.access.delDnsRecord(record)
|
||||
this.logger.info(`删除域名解析成功:fullRecord=${fullRecord}`);
|
||||
}
|
||||
}
|
||||
|
||||
//实例化这个provider,将其自动注册到系统中
|
||||
new XinnetConnectDnsProvider();
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './dns-provider.js';
|
||||
export * from './access.js';
|
||||
Reference in New Issue
Block a user