chore: format

This commit is contained in:
xiaojunnuo
2026-05-31 01:41:33 +08:00
parent acd440106b
commit 4b57a0d729
557 changed files with 12530 additions and 14039 deletions
@@ -9,7 +9,8 @@ import { AutoPrint } from "./auto-print.js";
@Autoload()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class AutoARegister { //这个A是必须,让他排在第一个 进行init,否则会被其他init模块抢先注册导致报错
export class AutoARegister {
//这个A是必须,让他排在第一个 进行init,否则会被其他init模块抢先注册导致报错
@Inject()
autoInitSite: AutoInitSite;
@@ -1,18 +1,18 @@
import { logger } from '@certd/basic';
import { SysSettingsService, SysSiteInfo } from '@certd/lib-server';
import { logger } from "@certd/basic";
import { SysSettingsService, SysSiteInfo } from "@certd/lib-server";
import { getPlusInfo, isPlus } from "@certd/plus-core";
import { Config, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { Config, Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import dayjs from "dayjs";
import { Between } from "typeorm";
import { DomainService } from '../cert/service/domain-service.js';
import { Cron } from '../cron/cron.js';
import { DomainService } from "../cert/service/domain-service.js";
import { Cron } from "../cron/cron.js";
import { UserSiteMonitorSetting } from "../mine/service/models.js";
import { UserSettingsService } from "../mine/service/user-settings-service.js";
import { SiteInfoService } from '../monitor/index.js';
import { SiteInfoService } from "../monitor/index.js";
import { NotificationService } from "../pipeline/service/notification-service.js";
import { PipelineService } from '../pipeline/service/pipeline-service.js';
import { PipelineService } from "../pipeline/service/pipeline-service.js";
import { UserService } from "../sys/authority/service/user-service.js";
import { ProjectService } from '../sys/enterprise/service/project-service.js';
import { ProjectService } from "../sys/enterprise/service/project-service.js";
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
@@ -20,13 +20,13 @@ export class AutoCron {
@Inject()
pipelineService: PipelineService;
@Config('cron.onlyAdminUser')
@Config("cron.onlyAdminUser")
private onlyAdminUser: boolean;
@Config('cron.immediateTriggerOnce')
@Config("cron.immediateTriggerOnce")
private immediateTriggerOnce = false;
@Config('cron.immediateTriggerSiteMonitor')
@Config("cron.immediateTriggerSiteMonitor")
private immediateTriggerSiteMonitor = false;
@Inject()
@@ -51,12 +51,10 @@ export class AutoCron {
@Inject()
projectService: ProjectService;
async init() {
logger.info('加载定时trigger开始');
logger.info("加载定时trigger开始");
await this.pipelineService.onStartup(this.immediateTriggerOnce, this.onlyAdminUser);
logger.info('加载定时trigger完成');
logger.info("加载定时trigger完成");
//
// const meta = getClassMetadata(CLASS_KEY, this.echoPlugin);
// console.log('meta', meta);
@@ -64,183 +62,180 @@ export class AutoCron {
// console.log('metas', metas);
await this.registerSiteMonitorCron();
await this.registerPlusExpireCheckCron();
await this.registerUserExpireCheckCron();
await this.registerDomainExpireCheckCron();
}
async registerSiteMonitorCron() {
//先注册公共job
logger.info(`注册公共站点证书检查定时任务`)
const randomMinute = Math.floor(Math.random() * 60)
logger.info(`注册公共站点证书检查定时任务`);
const randomMinute = Math.floor(Math.random() * 60);
this.cron.register({
name: 'siteMonitor',
name: "siteMonitor",
cron: `0 ${randomMinute} 0 * * *`,
job:async ()=>{
logger.info(`开始公共站点证书检查任务`)
await this.siteInfoService.triggerCommonJob()
logger.info(`公共站点证书检查任务完成`)
job: async () => {
logger.info(`开始公共站点证书检查任务`);
await this.siteInfoService.triggerCommonJob();
logger.info(`公共站点证书检查任务完成`);
},
});
logger.info(`注册公共站点证书检查定时任务完成`)
logger.info(`注册公共站点证书检查定时任务完成`);
//注册用户独立的检查时间
logger.info(`注册用户独立站点证书检查定时任务`)
logger.info(`注册用户独立站点证书检查定时任务`);
const monitorSettingList = await this.userSettingsService.list({
query:{
query: {
key: UserSiteMonitorSetting.__key__,
}
})
},
});
for (const item of monitorSettingList) {
const setting = item.setting ? JSON.parse(item.setting):{}
if(!setting?.cron){
continue
const setting = item.setting ? JSON.parse(item.setting) : {};
if (!setting?.cron) {
continue;
}
await this.siteInfoService.registerSiteMonitorJob(item.userId,item.projectId)
await this.siteInfoService.registerSiteMonitorJob(item.userId, item.projectId);
}
logger.info(`注册用户独立站点证书检查定时任务完成`)
logger.info(`注册用户独立站点证书检查定时任务完成`);
if (this.immediateTriggerSiteMonitor) {
logger.info(`立即触发一次公共站点证书检查任务`)
await this.siteInfoService.triggerCommonJob()
logger.info(`立即触发一次公共站点证书检查任务`);
await this.siteInfoService.triggerCommonJob();
}
}
registerPlusExpireCheckCron(){
registerPlusExpireCheckCron() {
// 添加plus即将到期检查任务
this.cron.register({
name: 'plus-expire-check',
name: "plus-expire-check",
cron: `0 10 9 * * *`, // 一天只能检查一次,否则会重复发送通知
job: async () => {
const plusInfo = getPlusInfo()
if (!plusInfo.originVipType || plusInfo.originVipType==="free" ) {
return
const plusInfo = getPlusInfo();
if (!plusInfo.originVipType || plusInfo.originVipType === "free") {
return;
}
let label ="专业版"
if( plusInfo.originVipType === 'comm'){
label = "商业版"
let label = "专业版";
if (plusInfo.originVipType === "comm") {
label = "商业版";
}
const siteInfo = await this.sysSettingsService.getSetting<SysSiteInfo>(SysSiteInfo)
const siteInfo = await this.sysSettingsService.getSetting<SysSiteInfo>(SysSiteInfo);
const appTitle = siteInfo.title || "certd"
const expiresDate = dayjs(plusInfo.expireTime).format("YYYY-MM-DD")
const appTitle = siteInfo.title || "certd";
const expiresDate = dayjs(plusInfo.expireTime).format("YYYY-MM-DD");
// plusInfo.expireTime= dayjs("2025-06-10").valueOf()
let expiresDays =Math.floor((plusInfo.expireTime - new Date().getTime())/ 1000 / 60 / 60 / 24)
let title = ""
let content =""
if(expiresDays === 20 ||expiresDays === 10 || expiresDays === 3 || expiresDays === 1 || expiresDays === 0){
title = `vip(${label})即将到期`
content = `您的${appTitle} vip (${label})剩余${expiresDays}天(${expiresDate})到期,请及时续期,以免影响业务`
}else if (expiresDays === -1 || expiresDays === -3 || expiresDays === -7) {
title = `vip(${label})已过期`
content = `您的${appTitle} vip (${label})已过期${Math.abs(expiresDays)}天(${expiresDate}),请尽快续期,以免影响业务`
const expiresDays = Math.floor((plusInfo.expireTime - new Date().getTime()) / 1000 / 60 / 60 / 24);
let title = "";
let content = "";
if (expiresDays === 20 || expiresDays === 10 || expiresDays === 3 || expiresDays === 1 || expiresDays === 0) {
title = `vip(${label})即将到期`;
content = `您的${appTitle} vip (${label})剩余${expiresDays}天(${expiresDate})到期,请及时续期,以免影响业务`;
} else if (expiresDays === -1 || expiresDays === -3 || expiresDays === -7) {
title = `vip(${label})已过期`;
content = `您的${appTitle} vip (${label})已过期${Math.abs(expiresDays)}天(${expiresDate}),请尽快续期,以免影响业务`;
}
if(title){
logger.warn(title)
logger.warn(content)
if (title) {
logger.warn(title);
logger.warn(content);
const url = await this.notificationService.getBindUrl("");
const adminUsers = await this.userService.getAdmins()
const adminUsers = await this.userService.getAdmins();
for (const adminUser of adminUsers) {
logger.info(`发送vip到期通知给管理员:${adminUser.username}`)
await this.notificationService.send({
useDefault: true,
logger: logger,
body:{
title,
content,
errorMessage:title,
url,
notificationType: "vipExpireRemind",
}
},adminUser.id)
logger.info(`发送vip到期通知给管理员:${adminUser.username}`);
await this.notificationService.send(
{
useDefault: true,
logger: logger,
body: {
title,
content,
errorMessage: title,
url,
notificationType: "vipExpireRemind",
},
},
adminUser.id
);
}
}
}
})
},
});
}
registerUserExpireCheckCron() {
// 添加plus即将到期检查任务
this.cron.register({
name: 'user-expire-check',
name: "user-expire-check",
cron: `0 20 9 * * *`, // 一天只能检查一次,否则会重复发送通知
job: async () => {
const getExpiresDaysUsers = async (days: number) => {
const targetDate = dayjs().add(days, 'day')
const startTime = targetDate.startOf('day').valueOf()
const endTime = targetDate.endOf('day').valueOf()
const targetDate = dayjs().add(days, "day");
const startTime = targetDate.startOf("day").valueOf();
const endTime = targetDate.endOf("day").valueOf();
return await this.userService.find({
where: {
validTime: Between(startTime, endTime),
status: 1
}
})
}
status: 1,
},
});
};
const notifyExpiresDaysUsers = async (days: number) => {
const list = await getExpiresDaysUsers(days)
const list = await getExpiresDaysUsers(days);
if (list.length === 0) {
return
return;
}
let title = `账号即将到期`
let content = `您的账号剩余${days}天到期,请及时续期,以免影响业务`
let title = `账号即将到期`;
let content = `您的账号剩余${days}天到期,请及时续期,以免影响业务`;
if (days <= 0) {
title = `账号已过期`
content = `您的账号已过期${Math.abs(days)}天,请尽快续期,以免影响业务`
title = `账号已过期`;
content = `您的账号已过期${Math.abs(days)}天,请尽快续期,以免影响业务`;
}
const url = await this.notificationService.getBindUrl("");
for (const user of list) {
logger.info(`发送到期通知给用户:${user.username}`)
await this.notificationService.send({
useDefault: true,
logger: logger,
body: {
title,
content,
errorMessage: title,
url,
notificationType: "userExpireRemind",
}
}, user.id)
logger.info(`发送到期通知给用户:${user.username}`);
await this.notificationService.send(
{
useDefault: true,
logger: logger,
body: {
title,
content,
errorMessage: title,
url,
notificationType: "userExpireRemind",
},
},
user.id
);
}
}
};
await notifyExpiresDaysUsers(7)
await notifyExpiresDaysUsers(3)
await notifyExpiresDaysUsers(1)
await notifyExpiresDaysUsers(0)
await notifyExpiresDaysUsers(-1)
await notifyExpiresDaysUsers(-3)
}
})
await notifyExpiresDaysUsers(7);
await notifyExpiresDaysUsers(3);
await notifyExpiresDaysUsers(1);
await notifyExpiresDaysUsers(0);
await notifyExpiresDaysUsers(-1);
await notifyExpiresDaysUsers(-3);
},
});
}
registerDomainExpireCheckCron(){
if (!isPlus()){
return
registerDomainExpireCheckCron() {
if (!isPlus()) {
return;
}
// 添加域名即将到期同步任务
const randomWeek = Math.floor(Math.random() * 7) + 1
const randomHour = Math.floor(Math.random() * 24)
const randomMinute = Math.floor(Math.random() * 60)
logger.info(`注册域名注册过期时间同步任务,每周${randomWeek} ${randomHour}:${randomMinute}检查一次`)
const randomWeek = Math.floor(Math.random() * 7) + 1;
const randomHour = Math.floor(Math.random() * 24);
const randomMinute = Math.floor(Math.random() * 60);
logger.info(`注册域名注册过期时间同步任务,每周${randomWeek} ${randomHour}:${randomMinute}检查一次`);
this.cron.register({
name: 'domain-expire-check',
name: "domain-expire-check",
cron: `0 ${randomMinute} ${randomHour} ? * ${randomWeek}`, // 每周随机一天检查一次
job: async () => {
await this.domainService.startSyncExpirationTask({})
}
})
await this.domainService.startSyncExpirationTask({});
},
});
}
}
@@ -1,9 +1,9 @@
import { logger } from '@certd/basic';
import { EncryptService, PlusService, SysInstallInfo, SysPrivateSettings, SysSettingsService } from '@certd/lib-server';
import { Config, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import crypto from 'crypto';
import { nanoid } from 'nanoid';
import { UserService } from '../sys/authority/service/user-service.js';
import { logger } from "@certd/basic";
import { EncryptService, PlusService, SysInstallInfo, SysPrivateSettings, SysSettingsService } from "@certd/lib-server";
import { Config, Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import crypto from "crypto";
import { nanoid } from "nanoid";
import { UserService } from "../sys/authority/service/user-service.js";
import { SafeService } from "../sys/settings/safe-service.js";
@Provide()
@@ -12,7 +12,7 @@ export class AutoInitSite {
@Inject()
userService: UserService;
@Config('typeorm.dataSource.default.type')
@Config("typeorm.dataSource.default.type")
dbType: string;
@Inject()
@@ -24,10 +24,9 @@ export class AutoInitSite {
@Inject()
encryptService: EncryptService;
async init() {
logger.info('初始化站点开始');
logger.info("初始化站点开始");
await this.startOptimizeDb();
//安装信息
const installInfo: SysInstallInfo = await this.sysSettingsService.getSetting(SysInstallInfo);
@@ -45,7 +44,7 @@ export class AutoInitSite {
if (!privateInfo.encryptSecret) {
const secretKey = crypto.randomBytes(32);
privateInfo.encryptSecret = secretKey.toString('base64');
privateInfo.encryptSecret = secretKey.toString("base64");
await this.sysSettingsService.saveSetting(privateInfo);
}
@@ -61,32 +60,32 @@ export class AutoInitSite {
try {
await this.plusService.verify();
} catch (e) {
logger.error('授权许可验证失败', e);
logger.error("授权许可验证失败", e);
}
//加载设置
await this.sysSettingsService.reloadSettings();
//加载站点隐藏配置
await this.safeService.reloadHiddenStatus(true)
logger.info('初始化站点完成');
await this.safeService.reloadHiddenStatus(true);
logger.info("初始化站点完成");
}
async startOptimizeDb() {
//优化数据库
//检查当前数据库类型为sqlite
if (this.dbType === 'better-sqlite3') {
const res = await this.userService.repository.query('PRAGMA auto_vacuum;');
if (this.dbType === "better-sqlite3") {
const res = await this.userService.repository.query("PRAGMA auto_vacuum;");
if (!(res && res.length > 0 && res[0].auto_vacuum > 0)) {
//未开启自动优化
await this.userService.repository.query('PRAGMA auto_vacuum = INCREMENTAL;');
logger.info('sqlite数据库自动优化已开启');
await this.userService.repository.query("PRAGMA auto_vacuum = INCREMENTAL;");
logger.info("sqlite数据库自动优化已开启");
}
const optimizeDb = async () => {
logger.info('sqlite数据库空间优化开始');
await this.userService.repository.query('VACUUM');
logger.info('sqlite数据库空间优化完成');
logger.info("sqlite数据库空间优化开始");
await this.userService.repository.query("VACUUM");
logger.info("sqlite数据库空间优化完成");
};
await optimizeDb();
setInterval(optimizeDb, 1000 * 60 * 60 * 24);
@@ -9,28 +9,26 @@ export class AutoLoadPlugins {
@Inject()
pluginService: PluginService;
async init() {
logger.info(`加载插件开始,加载模式:${process.env.certd_plugin_loadmode}`);
if (process.env.certd_plugin_loadmode === "metadata") {
await this.pluginService.registerFromLocal("./metadata")
}else{
await this.pluginService.registerFromLocal("./metadata");
} else {
// await import("../../plugins/index.js")
const fs = await import("fs");
const list = fs.readdirSync("./dist/plugins");
console.log("list", list);
for (const file of list) {
if (!file.includes(".")){
if (!file.includes(".")) {
logger.info(`加载插件文件:${file}`);
await import(`../../plugins/${file}/index.js`);
}
}
}
// await import("../../plugins/index.js")
await this.pluginService.registerFromDb()
await this.pluginService.registerFromDb();
await registerPaymentProviders();
logger.info(`加载插件完成,加载模式:${process.env.certd_plugin_loadmode}`);
}
}
@@ -1,6 +1,6 @@
import { logger, utils } from '@certd/basic';
import { UserSuiteService } from '@certd/commercial-core';
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { logger, utils } from "@certd/basic";
import { UserSuiteService } from "@certd/commercial-core";
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
@@ -12,8 +12,8 @@ export class AutoMitterRegister {
await this.registerOnNewUser();
}
async registerOnNewUser() {
utils.mitter.on('register', async (req: { userId: number }) => {
logger.info('register event', req.userId);
utils.mitter.on("register", async (req: { userId: number }) => {
logger.info("register event", req.userId);
await this.userSuiteService.presentGiftSuite(req.userId);
});
}
@@ -1,15 +1,15 @@
import { App, Config, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { getPlusInfo, isPlus } from '@certd/plus-core';
import { isDev, logger } from '@certd/basic';
import { App, Config, Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { getPlusInfo, isPlus } from "@certd/plus-core";
import { isDev, logger } from "@certd/basic";
import { SysInstallInfo, SysSettingsService } from '@certd/lib-server';
import { getVersion } from '../../utils/version.js';
import dayjs from 'dayjs';
import { Application } from '@midwayjs/koa';
import { httpsServer, HttpsServerOptions } from './https/server.js';
import { UserService } from '../sys/authority/service/user-service.js';
import { UserSettingsService } from '../mine/service/user-settings-service.js';
import { startProxyServer } from './proxy/server.js';
import { SysInstallInfo, SysSettingsService } from "@certd/lib-server";
import { getVersion } from "../../utils/version.js";
import dayjs from "dayjs";
import { Application } from "@midwayjs/koa";
import { httpsServer, HttpsServerOptions } from "./https/server.js";
import { UserService } from "../sys/authority/service/user-service.js";
import { UserSettingsService } from "../mine/service/user-settings-service.js";
import { startProxyServer } from "./proxy/server.js";
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
@@ -20,18 +20,18 @@ export class AutoPrint {
@App()
app: Application;
@Config('https')
@Config("https")
httpsConfig: HttpsServerOptions;
@Config('koa')
@Config("koa")
koaConfig: any;
@Inject()
userService: UserService;
@Inject()
userSettingsService: UserSettingsService;
@Config('system.resetAdminPasswd')
@Config("system.resetAdminPasswd")
private resetAdminPasswd: boolean;
async init() {
@@ -43,31 +43,31 @@ export class AutoPrint {
this.startHeapLog();
}
const installInfo: SysInstallInfo = await this.sysSettingsService.getSetting(SysInstallInfo);
logger.info('=========================================');
logger.info('当前站点ID:', installInfo.siteId);
logger.info("=========================================");
logger.info("当前站点ID:", installInfo.siteId);
const version = await getVersion();
logger.info(`当前版本:${version}`);
const plusInfo = getPlusInfo();
if (isPlus()) {
logger.info(`授权信息:${plusInfo.vipType},${plusInfo.expireTime === -1 ? '永久' : dayjs(plusInfo.expireTime).format('YYYY-MM-DD')}`);
logger.info(`授权信息:${plusInfo.vipType},${plusInfo.expireTime === -1 ? "永久" : dayjs(plusInfo.expireTime).format("YYYY-MM-DD")}`);
}
logger.info('Certd已启动');
logger.info('=========================================');
logger.info("Certd已启动");
logger.info("=========================================");
await this.resetPasswd();
}
async resetPasswd(){
async resetPasswd() {
if (this.resetAdminPasswd === true) {
logger.info('开始重置1号管理员用户的密码');
const newPasswd = '123456';
logger.info("开始重置1号管理员用户的密码");
const newPasswd = "123456";
await this.userService.resetPassword(1, newPasswd);
await this.userService.updateStatus(1, 1);
await this.userSettingsService.deleteWhere({
userId: 1,
key:"user.two.factor"
})
const publicSettings = await this.sysSettingsService.getPublicSettings()
publicSettings.captchaEnabled = false
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);
@@ -77,7 +77,7 @@ export class AutoPrint {
startHeapLog() {
function format(bytes: any) {
return (bytes / 1024 / 1024).toFixed(2) + ' MB';
return (bytes / 1024 / 1024).toFixed(2) + " MB";
}
function printHeapLog() {
const mu = process.memoryUsage();
@@ -89,7 +89,7 @@ export class AutoPrint {
startHttpsServer() {
if (!this.httpsConfig.enabled) {
logger.info('Https server is not enabled');
logger.info("Https server is not enabled");
return;
}
httpsServer.start({
@@ -100,6 +100,6 @@ export class AutoPrint {
}
startProxyServer() {
startProxyServer({port: 7003});
startProxyServer({ port: 7003 });
}
}
@@ -38,10 +38,10 @@ export class CertInfoWildcardDomainCountFix {
if (fixedCount > 0) {
logger.info(`已修复证书泛域名数量历史数据,数量=${fixedCount}`);
}
return true
return true;
} catch (e: any) {
logger.error("修复证书泛域名数量历史数据失败", e);
}
return false
return false;
}
}
@@ -63,7 +63,6 @@ export class CommonEabToAcmeAccountFix {
@Inject()
storageService: StorageService;
async init() {
try {
const certApplyConfig = await this.pluginConfigService.getPluginConfig({
@@ -56,12 +56,12 @@ export class GoogleCommonEabAccountKeyFix {
});
const googleCommonEabAccessId = certApplyConfig?.sysSetting?.input?.googleCommonEabAccessId;
if (!googleCommonEabAccessId) {
return true;
return true;
}
const eabAccess = await this.accessService.getAccessById(googleCommonEabAccessId, false);
if (eabAccess.accountKey) {
return true;
return true;
}
if (!eabAccess.kid) {
logger.info("公共Google EAB授权缺少KID,跳过历史ACME账号私钥修复");
@@ -82,7 +82,7 @@ export class GoogleCommonEabAccountKeyFix {
} catch (e: any) {
logger.error("修复公共Google EAB授权ACME账号私钥失败", e);
}
return false
return false;
}
async getLegacyGoogleAccountConfig(email?: string) {
@@ -74,7 +74,6 @@ export class LegacyAcmeAccountAccessFix {
@Inject()
accessService: AccessService;
async init() {
try {
const repository = this.storageService.getRepository();
@@ -41,7 +41,7 @@ export class OauthSubtypeBoundTypeFix {
await this.convertLegacyAddonLoginTypeToArray(addonEntity, legacyLoginType, manager);
}
});
return true
return true;
} catch (e: any) {
logger.error("修复OAuth subtype绑定历史数据失败", e);
}
@@ -14,7 +14,6 @@ export function fixSuiteContentWildcardDomainCount(contentValue?: string) {
return JSON.stringify(content);
}
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class SuiteContentWildcardDomainCountFix {
@@ -33,11 +32,11 @@ export class SuiteContentWildcardDomainCountFix {
if (fixedCount > 0) {
logger.info(`已修复套餐最大泛域名数量历史数据,数量=${fixedCount}`);
}
return true
return true;
} catch (e: any) {
logger.error("修复套餐最大泛域名数量历史数据失败", e);
}
return false
return false;
}
private async fixSuiteContentWildcardDomainCountByTable(entityManager: any, tableName: string) {
@@ -1,8 +1,8 @@
import { logger } from '@certd/basic';
import fs from 'fs';
import { logger } from "@certd/basic";
import fs from "fs";
// @ts-ignore
import forge from 'node-forge';
import path from 'path';
import forge from "node-forge";
import path from "path";
export function createSelfCertificate(opts: { crtPath: string; keyPath: string }) {
// 生成密钥对
@@ -11,14 +11,14 @@ export function createSelfCertificate(opts: { crtPath: string; keyPath: string }
// 创建自签名证书
const cert = forge.pki.createCertificate();
cert.publicKey = keypair.publicKey;
cert.serialNumber = '01';
cert.serialNumber = "01";
cert.validFrom = new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString(); // 1天前
cert.validTo = new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 10).toISOString(); // 10年后
// 创建主题
const attrs = [
{
name: 'commonName',
value: 'self-certificate.certd', // 或者你的域名
name: "commonName",
value: "self-certificate.certd", // 或者你的域名
},
];
cert.setSubject(attrs);
@@ -30,7 +30,7 @@ export function createSelfCertificate(opts: { crtPath: string; keyPath: string }
const pemKey = forge.pki.privateKeyToPem(keypair.privateKey);
// 写入文件
logger.info('生成自签名证书成功');
logger.info("生成自签名证书成功");
logger.info(`自签证书保存路径: ${opts.crtPath}`);
logger.info(`自签私钥保存路径: ${opts.keyPath}`);
const crtDir = path.dirname(opts.crtPath);
@@ -1,8 +1,8 @@
import https from 'node:https';
import fs from 'fs';
import { Application } from '@midwayjs/koa';
import { createSelfCertificate } from './self-certificate.js';
import {logger, safePromise} from '@certd/basic';
import https from "node:https";
import fs from "fs";
import { Application } from "@midwayjs/koa";
import { createSelfCertificate } from "./self-certificate.js";
import { logger, safePromise } from "@certd/basic";
export type HttpsServerOptions = {
enabled: boolean;
@@ -33,24 +33,24 @@ export class HttpsServer {
start(opts: HttpsServerOptions) {
if (!opts) {
logger.error('https配置不能为空');
logger.error("https配置不能为空");
return;
}
this.opts = opts;
logger.info('=========================================');
logger.info("=========================================");
if (!opts.key || !opts.cert) {
logger.error('证书路径未配置,无法启动https服务,请先配置:koa.https.key和koa.https.cert');
logger.error("证书路径未配置,无法启动https服务,请先配置:koa.https.key和koa.https.cert");
return;
}
if (!fs.existsSync(opts.key) || !fs.existsSync(opts.cert)) {
logger.info('证书文件不存在,将生成自签名证书');
logger.info("证书文件不存在,将生成自签名证书");
createSelfCertificate({
crtPath: opts.cert,
keyPath: opts.key,
});
}
logger.info('准备启动https服务');
logger.info("准备启动https服务");
const httpServer = https.createServer(
{
cert: fs.readFileSync(opts.cert),
@@ -59,7 +59,7 @@ export class HttpsServer {
opts.app.callback()
);
this.server = httpServer;
let hostname = opts.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() {
@@ -71,19 +71,18 @@ export class HttpsServer {
httpServer.listen(opts.port, hostname, listeningReporter);
return httpServer;
} catch (e) {
if ( e.message?.includes("address family not supported")) {
hostname = "0.0.0.0"
if (e.message?.includes("address family not supported")) {
hostname = "0.0.0.0";
logger.error(`${e.message},尝试监听${hostname}`, e);
try{
try {
httpServer.listen(opts.port, hostname, listeningReporter);
return httpServer;
}catch (e) {
logger.error('启动https服务失败', e);
} catch (e) {
logger.error("启动https服务失败", e);
}
}else{
logger.error('启动https服务失败', e);
} else {
logger.error("启动https服务失败", e);
}
}
}
}
@@ -1,13 +1,12 @@
// proxy-server.js
import http from 'http';
import https from 'https';
import url from 'url';
import net from 'net';
import { logger } from '@certd/basic';
import http from "http";
import https from "https";
import url from "url";
import net from "net";
import { logger } from "@certd/basic";
export function startProxyServer(opts:{port:number}) {
const {port} = opts;
export function startProxyServer(opts: { port: number }) {
const { port } = opts;
// 创建 HTTP 代理服务器
const proxyServer = http.createServer((clientReq, clientRes) => {
@@ -17,31 +16,31 @@ export function startProxyServer(opts:{port:number}) {
const parsedUrl = url.parse(clientReq.url);
const options = {
hostname: parsedUrl.hostname,
port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
port: parsedUrl.port || (parsedUrl.protocol === "https:" ? 443 : 80),
path: parsedUrl.path,
method: clientReq.method,
headers: clientReq.headers
headers: clientReq.headers,
};
// 根据协议选择不同的模块
const protocol = parsedUrl.protocol === 'https:' ? https : http;
const protocol = parsedUrl.protocol === "https:" ? https : http;
// 移除可能会引起问题的 headers
delete options.headers['proxy-connection'];
delete options.headers['connection'];
delete options.headers['keep-alive'];
delete options.headers["proxy-connection"];
delete options.headers["connection"];
delete options.headers["keep-alive"];
// 创建到目标服务器的请求
const proxyReq = protocol.request(options, (proxyRes) => {
const proxyReq = protocol.request(options, proxyRes => {
// 将目标服务器的响应返回给客户端
clientRes.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(clientRes);
});
proxyReq.on('error', (err) => {
logger.error('[proxy] 代理请求错误:', err);
proxyReq.on("error", err => {
logger.error("[proxy] 代理请求错误:", err);
clientRes.writeHead(500);
clientRes.end('代理服务器错误');
clientRes.end("代理服务器错误");
});
// 将客户端请求体转发到目标服务器
@@ -49,18 +48,16 @@ export function startProxyServer(opts:{port:number}) {
});
// 处理 CONNECT 方法(HTTPS 代理)
proxyServer.on('connect', (req, clientSocket, head) => {
proxyServer.on("connect", (req, clientSocket, head) => {
logger.log(`[proxy] HTTPS 连接请求: ${req.url}`);
const [hostname, port] = req.url.split(':');
const [hostname, port] = req.url.split(":");
const portNum = parseInt(port) || 443;
// 连接到目标服务器
const serverSocket = net.connect(portNum, hostname, () => {
// 告诉客户端连接已建立
clientSocket.write('HTTP/1.1 200 Connection Established\r\n' +
'Proxy-agent: Node.js-Proxy\r\n' +
'\r\n');
clientSocket.write("HTTP/1.1 200 Connection Established\r\n" + "Proxy-agent: Node.js-Proxy\r\n" + "\r\n");
// 建立双向数据流
serverSocket.write(head);
@@ -68,25 +65,25 @@ export function startProxyServer(opts:{port:number}) {
clientSocket.pipe(serverSocket);
});
serverSocket.on('error', (err) => {
logger.error('[proxy] HTTPS 代理错误:', err);
serverSocket.on("error", err => {
logger.error("[proxy] HTTPS 代理错误:", err);
clientSocket.end();
});
clientSocket.on('error', (err) => {
logger.error('[proxy] 客户端 socket 错误:', err);
clientSocket.on("error", err => {
logger.error("[proxy] 客户端 socket 错误:", err);
serverSocket.end();
});
});
// 监听端口
proxyServer.listen(port, () => {
logger.info(`[proxy] 正向代理服务器运行在 http://0.0.0.0:${port}`);
logger.info(`[proxy] 正向代理服务器运行在 http://0.0.0.0:${port}`);
});
proxyServer.close(() => {
logger.info('[proxy] 正向代理服务器已关闭');
logger.info("[proxy] 正向代理服务器已关闭");
});
return proxyServer
}
return proxyServer;
}
@@ -1,41 +1,41 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
export const GROUP_TYPE_SITE = 'site';
export const GROUP_TYPE_SITE = "site";
@Entity('cd_group')
@Entity("cd_group")
export class GroupEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'user_id', comment: '用户id' })
@Column({ name: "user_id", comment: "用户id" })
userId: number;
@Column({ name: 'name', comment: '分组名称' })
@Column({ name: "name", comment: "分组名称" })
name: string;
@Column({ name: 'icon', comment: '图标' })
@Column({ name: "icon", comment: "图标" })
icon: string;
@Column({ name: 'favorite', comment: '收藏' })
@Column({ name: "favorite", comment: "收藏" })
favorite: boolean;
@Column({ name: 'type', comment: '类型', length: 512 })
@Column({ name: "type", comment: "类型", length: 512 })
type: string;
@Column({ name: 'project_id', comment: '项目Id' })
@Column({ name: "project_id", comment: "项目Id" })
projectId: number;
@Column({
name: 'create_time',
comment: '创建时间',
default: () => 'CURRENT_TIMESTAMP',
name: "create_time",
comment: "创建时间",
default: () => "CURRENT_TIMESTAMP",
})
createTime: Date;
@Column({
name: 'update_time',
comment: '修改时间',
default: () => 'CURRENT_TIMESTAMP',
name: "update_time",
comment: "修改时间",
default: () => "CURRENT_TIMESTAMP",
})
updateTime: Date;
}
@@ -12,15 +12,14 @@ export class CaptchaService {
@Inject()
addonGetterService: AddonGetterService;
async getCaptcha(captchaAddonId?: number) {
if (!captchaAddonId) {
const settings = await this.sysSettingsService.getPublicSettings();
captchaAddonId = settings.captchaAddonId ?? 0;
}
const addon: ICaptchaAddon = await this.addonGetterService.getAddonById(captchaAddonId, true, 0,null, {
const addon: ICaptchaAddon = await this.addonGetterService.getAddonById(captchaAddonId, true, 0, null, {
type: "captcha",
name: "image"
name: "image",
});
if (!addon) {
throw new Error("验证码插件还未配置");
@@ -28,8 +27,7 @@ export class CaptchaService {
return await addon.getCaptcha();
}
async doValidate(opts: { form: any, must?: boolean, captchaAddonId?: number,req:CaptchaRequest }) {
async doValidate(opts: { form: any; must?: boolean; captchaAddonId?: number; req: CaptchaRequest }) {
if (!opts.captchaAddonId) {
const settings = await this.sysSettingsService.getPublicSettings();
opts.captchaAddonId = settings.captchaAddonId ?? 0;
@@ -46,13 +44,11 @@ export class CaptchaService {
if (!opts.form) {
throw new Error("请输入验证码");
}
const res = await addon.onValidate(opts.form,opts.req);
const res = await addon.onValidate(opts.form, opts.req);
if (!res) {
throw new Error("验证码错误");
}
return true;
}
}
@@ -1,11 +1,11 @@
import { cache, isDev, randomNumber, simpleNanoId } from '@certd/basic';
import { AccessService, AccessSysGetter, CodeErrorException, SysSettingsService } from '@certd/lib-server';
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { ISmsService } from '../sms/api.js';
import { SmsServiceFactory } from '../sms/factory.js';
import { cache, isDev, randomNumber, simpleNanoId } from "@certd/basic";
import { AccessService, AccessSysGetter, CodeErrorException, SysSettingsService } from "@certd/lib-server";
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { ISmsService } from "../sms/api.js";
import { SmsServiceFactory } from "../sms/factory.js";
import { CaptchaService } from "./captcha-service.js";
import { EmailService } from './email-service.js';
import { CaptchaRequest } from '../../../plugins/plugin-captcha/api.js';
import { EmailService } from "./email-service.js";
import { CaptchaRequest } from "../../../plugins/plugin-captcha/api.js";
// {data: '<svg.../svg>', text: 'abcd'}
/**
@@ -24,32 +24,30 @@ export class CodeService {
@Inject()
captchaService: CaptchaService;
async checkCaptcha(body:any,req:CaptchaRequest) {
return await this.captchaService.doValidate({form:body,req});
async checkCaptcha(body: any, req: CaptchaRequest) {
return await this.captchaService.doValidate({ form: body, req });
}
/**
*/
async sendSmsCode(
phoneCode = '86',
phoneCode = "86",
mobile: string,
opts?: {
duration?: number,
verificationType?: string,
verificationCodeLength?: number,
},
duration?: number;
verificationType?: string;
verificationCodeLength?: number;
}
) {
if (!mobile) {
throw new Error('手机号不能为空');
throw new Error("手机号不能为空");
}
const verificationCodeLength = Math.floor(Math.max(Math.min(opts?.verificationCodeLength || 4, 8), 4));
const verificationCodeLength = Math.floor(Math.max(Math.min(opts?.verificationCodeLength || 4, 8), 4));
const duration = Math.floor(Math.max(Math.min(opts?.duration || 5, 15), 1));
const sysSettings = await this.sysSettingsService.getPrivateSettings();
if (!sysSettings.sms?.config?.accessId) {
throw new Error('当前站点还未配置短信');
throw new Error("当前站点还未配置短信");
}
const smsType = sysSettings.sms.type;
const smsConfig = sysSettings.sms.config;
@@ -66,7 +64,7 @@ export class CodeService {
phoneCode,
});
const key = this.buildSmsCodeKey(phoneCode, mobile, opts?.verificationType);
const key = this.buildSmsCodeKey(phoneCode, mobile, opts?.verificationType);
cache.set(key, smsCode, {
ttl: duration * 60 * 1000, //5分钟
});
@@ -81,38 +79,38 @@ export class CodeService {
async sendEmailCode(
email: string,
opts?: {
duration?: number,
verificationType?: string,
verificationCodeLength?: number,
},
duration?: number;
verificationType?: string;
verificationCodeLength?: number;
}
) {
if (!email) {
throw new Error('Email不能为空');
throw new Error("Email不能为空");
}
const verificationCodeLength = Math.floor(Math.max(Math.min(opts?.verificationCodeLength || 4, 8), 4));
const verificationCodeLength = Math.floor(Math.max(Math.min(opts?.verificationCodeLength || 4, 8), 4));
const duration = Math.floor(Math.max(Math.min(opts?.duration || 5, 15), 1));
const code = randomNumber(verificationCodeLength);
const templateData = {
code, duration,
code,
duration,
title: "验证码",
content:`您的验证码是${code},请勿泄露`,
notificationType: "registerCode"
}
if (opts?.verificationType === 'forgotPassword') {
templateData.title = '找回密码';
templateData.notificationType = "forgotPassword"
content: `您的验证码是${code},请勿泄露`,
notificationType: "registerCode",
};
if (opts?.verificationType === "forgotPassword") {
templateData.title = "找回密码";
templateData.notificationType = "forgotPassword";
}
await this.emailService.sendByTemplate({
type: templateData.notificationType,
data: templateData,
receivers: [email],
type: templateData.notificationType,
data: templateData,
receivers: [email],
});
const key = this.buildEmailCodeKey(email,opts?.verificationType);
const key = this.buildEmailCodeKey(email, opts?.verificationType);
cache.set(key, code, {
ttl: duration * 60 * 1000, //5分钟
});
@@ -122,44 +120,43 @@ export class CodeService {
/**
* checkSms
*/
async checkSmsCode(opts: { mobile: string; phoneCode: string; smsCode: string; verificationType?: string; throwError: boolean; maxErrorCount?: number }) {
async checkSmsCode(opts: { mobile: string; phoneCode: string; smsCode: string; verificationType?: string; throwError: boolean; maxErrorCount?: number }) {
const key = this.buildSmsCodeKey(opts.phoneCode, opts.mobile, opts.verificationType);
return this.checkValidateCode("sms",key, opts.smsCode, opts.throwError, opts.maxErrorCount);
return this.checkValidateCode("sms", key, opts.smsCode, opts.throwError, opts.maxErrorCount);
}
buildSmsCodeKey(phoneCode: string, mobile: string, verificationType?: string) {
return ['sms', verificationType, phoneCode, mobile].filter(item => !!item).join(':');
return ["sms", verificationType, phoneCode, mobile].filter(item => !!item).join(":");
}
buildEmailCodeKey(email: string, verificationType?: string) {
return ['email', verificationType, email].filter(item => !!item).join(':');
return ["email", verificationType, email].filter(item => !!item).join(":");
}
checkValidateCode(type:string,key: string, userCode: string, throwError = true, maxErrorCount = 3) {
checkValidateCode(type: string, key: string, userCode: string, throwError = true, maxErrorCount = 3) {
// 记录异常次数key
if (isDev() && userCode==="1234567") {
if (isDev() && userCode === "1234567") {
return true;
}
const err_num_key = key + ':err_num';
const err_num_key = key + ":err_num";
//验证邮件验证码
const code = cache.get(key);
if (code == null || code !== userCode) {
let maxRetryCount = false;
if (!!code && maxErrorCount > 0) {
const err_num = cache.get(err_num_key) || 0
if(err_num >= maxErrorCount - 1) {
const err_num = cache.get(err_num_key) || 0;
if (err_num >= maxErrorCount - 1) {
maxRetryCount = true;
cache.delete(key);
cache.delete(err_num_key);
} else {
cache.set(err_num_key, err_num + 1, {
ttl: 30 * 60 * 1000
ttl: 30 * 60 * 1000,
});
}
}
if (throwError) {
const label = type ==='sms' ? '手机' : '邮箱';
throw new CodeErrorException(!maxRetryCount ? `${label}验证码错误`: `${label}验证码错误请获取新的验证码`);
const label = type === "sms" ? "手机" : "邮箱";
throw new CodeErrorException(!maxRetryCount ? `${label}验证码错误` : `${label}验证码错误请获取新的验证码`);
}
return false;
}
@@ -169,8 +166,8 @@ export class CodeService {
}
checkEmailCode(opts: { validateCode: string; email: string; verificationType?: string; throwError: boolean; maxErrorCount?: number }) {
const key = this.buildEmailCodeKey(opts.email, opts.verificationType);
return this.checkValidateCode('email',key, opts.validateCode, opts.throwError, opts.maxErrorCount);
const key = this.buildEmailCodeKey(opts.email, opts.verificationType);
return this.checkValidateCode("email", key, opts.validateCode, opts.throwError, opts.maxErrorCount);
}
compile(templateString: string) {
@@ -183,11 +180,10 @@ export class CodeService {
);
}
buildValidationValueKey(code:string) {
buildValidationValueKey(code: string) {
return `validationValue:${code}`;
}
setValidationValue(value:any) {
setValidationValue(value: any) {
const randomCode = simpleNanoId(12);
const key = this.buildValidationValueKey(randomCode);
cache.set(key, value, {
@@ -195,7 +191,7 @@ export class CodeService {
});
return randomCode;
}
getValidationValue(code:string) {
getValidationValue(code: string) {
return cache.get(this.buildValidationValueKey(code));
}
}
@@ -1,18 +1,18 @@
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import type { EmailSend, EmailSendByTemplateReq } from '@certd/pipeline';
import { IEmailService } from '@certd/pipeline';
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import type { EmailSend, EmailSendByTemplateReq } from "@certd/pipeline";
import { IEmailService } from "@certd/pipeline";
import { logger } from '@certd/basic';
import { isComm, isPlus } from '@certd/plus-core';
import { logger } from "@certd/basic";
import { isComm, isPlus } from "@certd/plus-core";
import nodemailer from 'nodemailer';
import { SendMailOptions } from 'nodemailer';
import { UserSettingsService } from '../../mine/service/user-settings-service.js';
import { AddonService, PlusService, SysEmailConf, SysInstallInfo, SysSettingsService, SysSiteInfo } from '@certd/lib-server';
import { getEmailSettings } from '../../sys/settings/fix.js';
import nodemailer from "nodemailer";
import { SendMailOptions } from "nodemailer";
import { UserSettingsService } from "../../mine/service/user-settings-service.js";
import { AddonService, PlusService, SysEmailConf, SysInstallInfo, SysSettingsService, SysSiteInfo } from "@certd/lib-server";
import { getEmailSettings } from "../../sys/settings/fix.js";
import { UserEmailSetting } from "../../mine/service/models.js";
import { AddonGetterService } from '../../pipeline/service/addon-getter-service.js';
import { EmailContent, ITemplateProvider } from '../../../plugins/plugin-template/api.js';
import { AddonGetterService } from "../../pipeline/service/addon-getter-service.js";
import { EmailContent, ITemplateProvider } from "../../../plugins/plugin-template/api.js";
export type EmailConfig = {
host: string;
@@ -43,12 +43,11 @@ export class EmailService implements IEmailService {
@Inject()
addonGetterService: AddonGetterService;
@Inject()
addonService: AddonService
addonService: AddonService;
async sendByPlus(email: EmailSend) {
if (!isPlus()) {
throw new Error('plus not enabled');
throw new Error("plus not enabled");
}
/**
@@ -63,13 +62,13 @@ export class EmailService implements IEmailService {
/**
*/
async send(email: EmailSend) {
logger.info('sendEmail', email);
logger.info("sendEmail", email);
if (!email.receivers || email.receivers.length === 0) {
throw new Error('收件人不能为空');
throw new Error("收件人不能为空");
}
let sysTitle = 'Certd';
let sysTitle = "Certd";
if (isComm()) {
const siteInfo = await this.sysSettingsService.getSetting<SysSiteInfo>(SysSiteInfo);
if (siteInfo) {
@@ -79,11 +78,10 @@ export class EmailService implements IEmailService {
let subject = email.subject;
if (!subject) {
logger.error(new Error('邮件标题不能为空'));
logger.error(new Error("邮件标题不能为空"));
subject = `邮件标题为空,请联系管理员排查`;
}
if (!subject.includes(`${sysTitle}`)) {
subject = `${sysTitle}${subject}`;
}
@@ -96,27 +94,25 @@ export class EmailService implements IEmailService {
//自动使用plus发邮件
return await this.sendByPlus(email);
}
throw new Error('邮件服务器还未设置');
throw new Error("邮件服务器还未设置");
}
if (emailConf.usePlus && isPlus()) {
return await this.sendByPlus(email);
}
await this.sendByCustom(emailConf, email, sysTitle);
logger.info('sendEmail complete: ', email);
logger.info("sendEmail complete: ", email);
}
private async sendByCustom(emailConfig: EmailConfig, email: EmailSend, sysTitle: string) {
const transporter = nodemailer.createTransport(emailConfig);
let from = `${sysTitle} <${emailConfig.sender}>`;
if (emailConfig.sender.includes('<')) {
if (emailConfig.sender.includes("<")) {
from = emailConfig.sender;
}
const mailOptions = {
from: from,
to: email.receivers.join(', '), // list of receivers
to: email.receivers.join(", "), // list of receivers
subject: email.subject,
text: email.content,
html: email.html,
@@ -129,8 +125,8 @@ export class EmailService implements IEmailService {
await this.sendByTemplate({
type: "common",
data: {
title: '测试邮件,from certd',
content: '测试邮件,from certd',
title: "测试邮件,from certd",
content: "测试邮件,from certd",
url: await this.getTestEmailUrl(),
},
receivers: [receiver],
@@ -147,41 +143,41 @@ export class EmailService implements IEmailService {
}
async list(userId: any) {
const userEmailSetting = await this.settingsService.getSetting<UserEmailSetting>(userId,null, UserEmailSetting)
const userEmailSetting = await this.settingsService.getSetting<UserEmailSetting>(userId, null, UserEmailSetting);
return userEmailSetting.list;
}
async delete(userId: any, email: string) {
const userEmailSetting = await this.settingsService.getSetting<UserEmailSetting>(userId, null, UserEmailSetting)
const userEmailSetting = await this.settingsService.getSetting<UserEmailSetting>(userId, null, UserEmailSetting);
userEmailSetting.list = userEmailSetting.list.filter(item => item !== email);
await this.settingsService.saveSetting(userId, null, userEmailSetting)
await this.settingsService.saveSetting(userId, null, userEmailSetting);
}
async add(userId: any, email: string) {
const userEmailSetting = await this.settingsService.getSetting<UserEmailSetting>(userId, null, UserEmailSetting)
const userEmailSetting = await this.settingsService.getSetting<UserEmailSetting>(userId, null, UserEmailSetting);
//如果已存在
if (userEmailSetting.list.includes(email)) {
return
return;
}
userEmailSetting.list.unshift(email)
await this.settingsService.saveSetting(userId, null, userEmailSetting)
userEmailSetting.list.unshift(email);
await this.settingsService.saveSetting(userId, null, userEmailSetting);
}
async sendByTemplate(req: EmailSendByTemplateReq) {
let content = null
let content = null;
const emailConf = await this.sysSettingsService.getSetting<SysEmailConf>(SysEmailConf);
const template = emailConf?.templates?.[req.type]
if (isPlus() && template && template.addonId) {
const addon: ITemplateProvider<EmailContent> = await this.addonGetterService.getAddonById(template.addonId, true, 0,null)
const template = emailConf?.templates?.[req.type];
if (isPlus() && template && template.addonId) {
const addon: ITemplateProvider<EmailContent> = await this.addonGetterService.getAddonById(template.addonId, true, 0, null);
if (addon) {
content = await addon.buildContent({ data: req.data })
content = await addon.buildContent({ data: req.data });
}
}
if (isPlus() && !content ) {
if (isPlus() && !content) {
//看看有没有通用模版
if (emailConf?.templates?.common && emailConf?.templates?.common.addonId) {
const addon: ITemplateProvider<EmailContent> = await this.addonGetterService.getAddonById(emailConf.templates.common.addonId, true, 0,null)
const addon: ITemplateProvider<EmailContent> = await this.addonGetterService.getAddonById(emailConf.templates.common.addonId, true, 0, null);
if (addon) {
content = await addon.buildContent({ data: req.data })
content = await addon.buildContent({ data: req.data });
}
}
}
@@ -189,21 +185,21 @@ export class EmailService implements IEmailService {
// 没有找到模版,使用默认模版
if (!content) {
try {
const addon: ITemplateProvider<EmailContent> = await this.addonGetterService.getBlank("emailTemplate", req.type)
content = await addon.buildDefaultContent({ data: req.data })
const addon: ITemplateProvider<EmailContent> = await this.addonGetterService.getBlank("emailTemplate", req.type);
content = await addon.buildDefaultContent({ data: req.data });
} catch (e) {
// 对应的通知类型模版可能没有注册或者开发
}
}
if (!content) {
const addon: ITemplateProvider<EmailContent> = await this.addonGetterService.getBlank("emailTemplate", "common")
content = await addon.buildDefaultContent({ data: req.data })
const addon: ITemplateProvider<EmailContent> = await this.addonGetterService.getBlank("emailTemplate", "common");
content = await addon.buildDefaultContent({ data: req.data });
}
return await this.send({
...content,
receivers: req.receivers,
attachments: req.attachments,
})
});
}
}
@@ -1,9 +1,9 @@
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';
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 })
@@ -18,7 +18,7 @@ export class GroupService extends BaseService<GroupEntity> {
async add(bean: any) {
if (!bean.type) {
throw new Error('type is required');
throw new Error("type is required");
}
bean = merge(
{
@@ -1,50 +1,50 @@
import { logger } from "@certd/basic"
import { logger } from "@certd/basic";
export class BackTaskExecutor {
tasks: Record<string, Record<string, BackTask>> = {}
tasks: Record<string, Record<string, BackTask>> = {};
start(task: BackTask) {
const type = task.type || 'default'
const type = task.type || "default";
if (!this.tasks[type]) {
this.tasks[type] = {}
this.tasks[type] = {};
}
const oldTask = this.tasks[type][task.key]
if (oldTask ){
const oldTask = this.tasks[type][task.key];
if (oldTask) {
if (oldTask.status === "running") {
throw new Error(`任务 ${task.title} 正在运行中`)
throw new Error(`任务 ${task.title} 正在运行中`);
}
this.clear(type, task.key);
}
this.tasks[type][task.key] = task
this.tasks[type][task.key] = task;
this.run(task);
}
get(type: string, key: string) {
if (!this.tasks[type]) {
this.tasks[type] = {}
this.tasks[type] = {};
}
return this.tasks[type][key]
return this.tasks[type][key];
}
removeIsEnd(type: string, key: string) {
const task = this.tasks[type]?.[key]
const task = this.tasks[type]?.[key];
if (task && task.status !== "running") {
this.clear(type, key);
}
}
clear(type: string, key: string) {
const task = this.tasks[type]?.[key]
const task = this.tasks[type]?.[key];
if (task) {
task.clearTimeout();
delete this.tasks[type][key]
delete this.tasks[type][key];
}
}
private async run(task: BackTask) {
const type = task.type || 'default'
const type = task.type || "default";
if (task.status === "running") {
throw new Error(`任务 ${type}${task.key} 正在运行中`)
throw new Error(`任务 ${type}${task.key} 正在运行中`);
}
task.startTime = Date.now();
task.clearTimeout();
@@ -54,84 +54,78 @@ export class BackTaskExecutor {
} catch (e) {
logger.error(`任务 ${task.title}[${type}-${task.key}] 执行失败`, e.message);
task.status = "failed";
task.addError(e.message)
task.addError(e.message);
} finally {
task.endTime = Date.now();
task.status = "done";
task.setTimeoutId(setTimeout(() => {
this.clear(type, task.key);
}, 24 * 60 * 60 * 1000));
task.setTimeoutId(
setTimeout(() => {
this.clear(type, task.key);
}, 24 * 60 * 60 * 1000)
);
delete task.run;
}
}
}
export class BackTask {
type: string;
key: string;
title: string;
total: number = 0;
current: number = 0;
skip: number = 0;
total = 0;
current = 0;
skip = 0;
startTime: number;
endTime: number;
status: string = "pending";
status = "pending";
errors?: string[] = [];
private _timeoutId?: NodeJS.Timeout;
private _run: (task: BackTask) => Promise<void>;
constructor(opts: {
type: string,
key: string, title: string, run: (task: BackTask) => Promise<void>
}) {
const { key, title, run, type } = opts
this.type = type
constructor(opts: { type: string; key: string; title: string; run: (task: BackTask) => Promise<void> }) {
const { key, title, run, type } = opts;
this.type = type;
this.key = key;
this.title = title;
this._run = run;
Object.defineProperty(this, '_run', {
Object.defineProperty(this, "_run", {
enumerable: false,
value: this._run
})
Object.defineProperty(this, '_timeoutId', {
enumerable: false,
value: null
})
Object.defineProperty(this, 'progress', {
get: ()=> {
return this.getProgress()
},
enumerable: true, // 关键:设置为可枚举
configurable: true
value: this._run,
});
Object.defineProperty(this, 'successCount', {
get: ()=> {
return this.getSuccessCount()
Object.defineProperty(this, "_timeoutId", {
enumerable: false,
value: null,
});
Object.defineProperty(this, "progress", {
get: () => {
return this.getProgress();
},
enumerable: true, // 关键:设置为可枚举
configurable: true
})
Object.defineProperty(this, 'errorCount', {
get: ()=> {
return this.getErrorCount()
enumerable: true, // 关键:设置为可枚举
configurable: true,
});
Object.defineProperty(this, "successCount", {
get: () => {
return this.getSuccessCount();
},
enumerable: true, // 关键:设置为可枚举
configurable: true
})
Object.defineProperty(this, 'skipCount', {
get: ()=> {
return this.getSkipCount()
enumerable: true, // 关键:设置为可枚举
configurable: true,
});
Object.defineProperty(this, "errorCount", {
get: () => {
return this.getErrorCount();
},
enumerable: true, // 关键:设置为可枚举
configurable: true
})
enumerable: true, // 关键:设置为可枚举
configurable: true,
});
Object.defineProperty(this, "skipCount", {
get: () => {
return this.getSkipCount();
},
enumerable: true, // 关键:设置为可枚举
configurable: true,
});
}
async run(task: BackTask) {
@@ -145,7 +139,6 @@ export class BackTask {
}
}
setTimeoutId(timeoutId: NodeJS.Timeout) {
this.clearTimeout();
this._timeoutId = timeoutId;
@@ -155,34 +148,33 @@ export class BackTask {
this.total = total;
}
incrementCurrent() {
this.current++
this.current++;
}
addError(error: string) {
logger.error(error)
this.errors.push(error)
logger.error(error);
this.errors.push(error);
}
getErrorCount() {
return this.errors.length
return this.errors.length;
}
getSkipCount() {
return this.skip
return this.skip;
}
getSuccessCount() {
return this.current - this.errors.length
return this.current - this.errors.length;
}
getProgress() {
return (this.current * 1.0 / this.total * 100.0);
return ((this.current * 1.0) / this.total) * 100.0;
}
incrementSkip() {
this.skip++
this.skip++;
}
}
export const taskExecutor = new BackTaskExecutor();
export const taskExecutor = new BackTaskExecutor();
@@ -1,6 +1,6 @@
import { logger } from '@certd/basic';
import { ISmsService, PluginInputs, SmsPluginCtx } from './api.js';
import { AliyunAccess, AliyunClient } from '../../../plugins/plugin-lib/aliyun/index.js';
import { logger } from "@certd/basic";
import { ISmsService, PluginInputs, SmsPluginCtx } from "./api.js";
import { AliyunAccess, AliyunClient } from "../../../plugins/plugin-lib/aliyun/index.js";
export type AliyunSmsConfig = {
accessId: string;
@@ -11,30 +11,30 @@ export type AliyunSmsConfig = {
export class AliyunSmsService implements ISmsService {
static getDefine() {
return {
name: 'aliyun',
desc: '阿里云短信服务',
name: "aliyun",
desc: "阿里云短信服务",
input: {
accessId: {
title: '阿里云授权',
title: "阿里云授权",
component: {
name: 'access-selector',
type: 'aliyun',
name: "access-selector",
type: "aliyun",
},
required: true,
},
signName: {
title: '签名',
title: "签名",
component: {
name: 'a-input',
vModel: 'value',
name: "a-input",
vModel: "value",
},
required: true,
},
codeTemplateId: {
title: '验证码模板Id',
title: "验证码模板Id",
component: {
name: 'a-input',
vModel: 'value',
name: "a-input",
vModel: "value",
},
required: true,
},
@@ -55,8 +55,8 @@ export class AliyunSmsService implements ISmsService {
await aliyunClinet.init({
accessKeyId: access.accessKeyId,
accessKeySecret: access.accessKeySecret,
endpoint: 'https://dysmsapi.aliyuncs.com',
apiVersion: '2017-05-25',
endpoint: "https://dysmsapi.aliyuncs.com",
apiVersion: "2017-05-25",
});
const smsConfig = this.ctx.config;
const phoneNumber = phoneCode + mobile;
@@ -67,6 +67,6 @@ export class AliyunSmsService implements ISmsService {
TemplateParam: `{"code":"${code}"}`,
};
await aliyunClinet.request('SendSms', params);
await aliyunClinet.request("SendSms", params);
}
}
@@ -1,4 +1,4 @@
import { FormItemProps, IAccessService } from '@certd/pipeline';
import { FormItemProps, IAccessService } from "@certd/pipeline";
export interface ISmsService {
sendSmsCode(opts: { mobile: string; code: string; phoneCode: string }): Promise<void>;
@@ -1,4 +1,3 @@
export class SmsServiceFactory {
static async createSmsService(type: string) {
const cls = await this.GetClassByType(type);
@@ -7,17 +6,17 @@ export class SmsServiceFactory {
static async GetClassByType(type: string) {
switch (type) {
case 'aliyun':
const {AliyunSmsService} = await import("./aliyun-sms.js")
case "aliyun":
const { AliyunSmsService } = await import("./aliyun-sms.js");
return AliyunSmsService;
case 'yfysms':
const {YfySmsService} = await import("./yfy-sms.js")
case "yfysms":
const { YfySmsService } = await import("./yfy-sms.js");
return YfySmsService;
case 'tencent':
const {TencentSmsService} = await import("./tencent-sms.js")
case "tencent":
const { TencentSmsService } = await import("./tencent-sms.js");
return TencentSmsService;
default:
throw new Error('不支持的短信服务类型');
throw new Error("不支持的短信服务类型");
}
}
@@ -1,5 +1,5 @@
import { TencentAccess } from '../../../plugins/plugin-lib/tencent/access.js';
import {ISmsService, PluginInputs, SmsPluginCtx} from './api.js';
import { TencentAccess } from "../../../plugins/plugin-lib/tencent/access.js";
import { ISmsService, PluginInputs, SmsPluginCtx } from "./api.js";
export type TencentSmsConfig = {
accessId: string;
@@ -12,53 +12,53 @@ export type TencentSmsConfig = {
export class TencentSmsService implements ISmsService {
static getDefine() {
return {
name: 'tencent',
desc: '腾讯云短信服务',
name: "tencent",
desc: "腾讯云短信服务",
input: {
accessId: {
title: '腾讯云授权',
title: "腾讯云授权",
component: {
name: 'access-selector',
type: 'tencent',
name: "access-selector",
type: "tencent",
},
required: true,
},
region: {
title: '区域',
value:"ap-beijing",
title: "区域",
value: "ap-beijing",
component: {
name: 'a-select',
vModel: 'value',
options:[
{value:"ap-beijing",label:"华北地区(北京)"},
{value:"ap-guangzhou",label:"华南地区(广州)"},
{value:"ap-nanjing",label:"华东地区(南京)"},
]
name: "a-select",
vModel: "value",
options: [
{ value: "ap-beijing", label: "华北地区(北京)" },
{ value: "ap-guangzhou", label: "华南地区(广州)" },
{ value: "ap-nanjing", label: "华东地区(南京)" },
],
},
helper:"随便选一个",
helper: "随便选一个",
required: true,
},
signName: {
title: '签名',
title: "签名",
component: {
name: 'a-input',
vModel: 'value',
name: "a-input",
vModel: "value",
},
required: true,
},
appId: {
title: '应用ID',
title: "应用ID",
component: {
name: 'a-input',
vModel: 'value',
name: "a-input",
vModel: "value",
},
required: true,
},
codeTemplateId: {
title: '验证码模板Id',
title: "验证码模板Id",
component: {
name: 'a-input',
vModel: 'value',
name: "a-input",
vModel: "value",
},
required: true,
},
@@ -72,13 +72,11 @@ export class TencentSmsService implements ISmsService {
this.ctx = ctx;
}
async getClient() {
const sdk = await import('tencentcloud-sdk-nodejs/tencentcloud/services/sms/v20210111/index.js');
const sdk = await import("tencentcloud-sdk-nodejs/tencentcloud/services/sms/v20210111/index.js");
const client = sdk.v20210111.Client;
const access = await this.ctx.accessService.getById<TencentAccess>(this.ctx.config.accessId);
// const region = this.region;
const clientConfig = {
credential: {
@@ -102,15 +100,11 @@ export class TencentSmsService implements ISmsService {
const client = await this.getClient();
const smsConfig = this.ctx.config;
const params = {
"PhoneNumberSet": [
`+${phoneCode}${mobile}`
],
"SmsSdkAppId": smsConfig.appId,
"TemplateId": smsConfig.codeTemplateId,
"SignName": smsConfig.signName,
"TemplateParamSet": [
code
]
PhoneNumberSet: [`+${phoneCode}${mobile}`],
SmsSdkAppId: smsConfig.appId,
TemplateId: smsConfig.codeTemplateId,
SignName: smsConfig.signName,
TemplateParamSet: [code],
};
const ret = await client.SendSms(params);
this.checkRet(ret);
@@ -118,7 +112,7 @@ export class TencentSmsService implements ISmsService {
checkRet(ret: any) {
if (!ret || ret.Error) {
throw new Error('执行失败:' + ret.Error.Code + ',' + ret.Error.Message);
throw new Error("执行失败:" + ret.Error.Code + "," + ret.Error.Message);
}
}
}
@@ -1,6 +1,6 @@
import { http, utils } from '@certd/basic';
import { ISmsService, PluginInputs, SmsPluginCtx } from './api.js';
import { YfySmsAccess } from '../../../plugins/plugin-plus/yidun/access-sms.js';
import { http, utils } from "@certd/basic";
import { ISmsService, PluginInputs, SmsPluginCtx } from "./api.js";
import { YfySmsAccess } from "../../../plugins/plugin-plus/yidun/access-sms.js";
export type YfySmsConfig = {
accessId: string;
@@ -10,22 +10,22 @@ export type YfySmsConfig = {
export class YfySmsService implements ISmsService {
static getDefine() {
return {
name: 'yfysms',
desc: '易发云短信',
name: "yfysms",
desc: "易发云短信",
input: {
accessId: {
title: '易发云短信授权',
title: "易发云短信授权",
component: {
name: 'access-selector',
type: 'yfysms',
name: "access-selector",
type: "yfysms",
},
required: true,
},
signName: {
title: '签名',
title: "签名",
component: {
name: 'a-input',
vModel: 'value',
name: "a-input",
vModel: "value",
},
required: true,
},
@@ -44,10 +44,10 @@ export class YfySmsService implements ISmsService {
const access = await this.ctx.accessService.getById<YfySmsAccess>(this.ctx.config.accessId);
const res = await http.request({
url: 'http://sms.yfyidc.cn/sms/',
method: 'post',
url: "http://sms.yfyidc.cn/sms/",
method: "post",
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
"Content-Type": "application/x-www-form-urlencoded",
},
data: {
/**
@@ -75,40 +75,40 @@ export class YfySmsService implements ISmsService {
* 9 用户已封禁
* 10 未实名认证
*/
let message = '';
let message = "";
switch (res) {
case 1:
message = '余额不足';
message = "余额不足";
break;
case 2:
message = '用户不存在';
message = "用户不存在";
break;
case 3:
message = 'KEY错误';
message = "KEY错误";
break;
case 4:
message = '发送失败';
message = "发送失败";
break;
case 5:
message = '签名不存在';
message = "签名不存在";
break;
case 6:
message = '签名审核未通过';
message = "签名审核未通过";
break;
case 7:
message = '当前发信短信已达到上限';
message = "当前发信短信已达到上限";
break;
case 8:
message = '有违规词';
message = "有违规词";
break;
case 9:
message = '用户已封禁';
message = "用户已封禁";
break;
case 10:
message = '未实名认证';
message = "未实名认证";
break;
default:
message = '未知错误';
message = "未知错误";
}
throw new Error(`发送短信失败:${message}`);
}
@@ -1,62 +1,61 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
/**
* 域名管理
*/
@Entity('cd_domain')
@Entity("cd_domain")
export class DomainEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ comment: '用户ID', name: 'user_id' })
@Column({ comment: "用户ID", name: "user_id" })
userId: number;
@Column({ comment: '主域名', length: 100 })
@Column({ comment: "主域名", length: 100 })
domain: string;
@Column({ comment: '校验类型', name: 'challenge_type', length: 50 })
challengeType : string;
@Column({ comment: "校验类型", name: "challenge_type", length: 50 })
challengeType: string;
@Column({ comment: 'DNS提供商', name: 'dns_provider_type', length: 50 })
@Column({ comment: "DNS提供商", name: "dns_provider_type", length: 50 })
dnsProviderType: string;
@Column({ comment: 'DNS提供商授权', name: 'dns_provider_access' })
@Column({ comment: "DNS提供商授权", name: "dns_provider_access" })
dnsProviderAccess: number;
@Column({ comment: '是否禁用', name: 'disabled' })
@Column({ comment: "是否禁用", name: "disabled" })
disabled: boolean;
@Column({ comment: '注册时间', name: 'registration_date' })
@Column({ comment: "注册时间", name: "registration_date" })
registrationDate: number;
@Column({ comment: '过期时间', name: 'expiration_date' })
@Column({ comment: "过期时间", name: "expiration_date" })
expirationDate: number;
@Column({ comment: '来源', name: 'from_type', length: 50 })
@Column({ comment: "来源", name: "from_type", length: 50 })
fromType: string;
@Column({ comment: 'http上传类型', name: 'http_uploader_type', length: 50 })
@Column({ comment: "http上传类型", name: "http_uploader_type", length: 50 })
httpUploaderType: string;
@Column({ comment: 'http上传授权', name: 'http_uploader_access' })
@Column({ comment: "http上传授权", name: "http_uploader_access" })
httpUploaderAccess: number;
@Column({ comment: 'http上传根目录', name: 'http_upload_root_dir', length: 512 })
@Column({ comment: "http上传根目录", name: "http_upload_root_dir", length: 512 })
httpUploadRootDir: string;
@Column({ name: 'project_id', comment: '项目Id' })
@Column({ name: "project_id", comment: "项目Id" })
projectId: number;
@Column({
comment: '创建时间',
name: 'create_time',
default: () => 'CURRENT_TIMESTAMP',
comment: "创建时间",
name: "create_time",
default: () => "CURRENT_TIMESTAMP",
})
createTime: Date;
@Column({
comment: '修改时间',
name: 'update_time',
default: () => 'CURRENT_TIMESTAMP',
comment: "修改时间",
name: "update_time",
default: () => "CURRENT_TIMESTAMP",
})
updateTime: Date;
}
@@ -1,24 +1,24 @@
import { http, logger, utils } from '@certd/basic';
import { AccessService, BaseService, isEnterprise } from '@certd/lib-server';
import { doPageTurn, Pager, PageRes } from '@certd/pipeline';
import { http, logger, utils } from "@certd/basic";
import { AccessService, BaseService, isEnterprise } from "@certd/lib-server";
import { doPageTurn, Pager, PageRes } from "@certd/pipeline";
import { DomainVerifiers } from "@certd/plugin-cert";
import { createDnsProvider, dnsProviderRegistry, DomainParser } from "@certd/plugin-lib";
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import dayjs from 'dayjs';
import { merge } from 'lodash-es';
import { In, LessThan, Not, Repository } from 'typeorm';
import { BackTask, taskExecutor } from '../../basic/service/task-executor.js';
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { InjectEntityModel } from "@midwayjs/typeorm";
import dayjs from "dayjs";
import { merge } from "lodash-es";
import { In, LessThan, Not, Repository } from "typeorm";
import { BackTask, taskExecutor } from "../../basic/service/task-executor.js";
import { CnameRecordEntity } from "../../cname/entity/cname-record.js";
import { CnameRecordService } from '../../cname/service/cname-record-service.js';
import { Cron } from '../../cron/cron.js';
import { UserDomainImportSetting, UserDomainMonitorSetting } from '../../mine/service/models.js';
import { UserSettingsService } from '../../mine/service/user-settings-service.js';
import { JobHistoryService } from '../../monitor/service/job-history-service.js';
import { TaskServiceBuilder } from '../../pipeline/service/getter/task-service-getter.js';
import { CnameRecordService } from "../../cname/service/cname-record-service.js";
import { Cron } from "../../cron/cron.js";
import { UserDomainImportSetting, UserDomainMonitorSetting } from "../../mine/service/models.js";
import { UserSettingsService } from "../../mine/service/user-settings-service.js";
import { JobHistoryService } from "../../monitor/service/job-history-service.js";
import { TaskServiceBuilder } from "../../pipeline/service/getter/task-service-getter.js";
import { SubDomainService } from "../../pipeline/service/sub-domain-service.js";
import { DomainEntity } from '../entity/domain.js';
import { TldClient } from './tld-client.js';
import { DomainEntity } from "../entity/domain.js";
import { TldClient } from "./tld-client.js";
export interface SyncFromProviderReq {
userId: number;
@@ -27,12 +27,10 @@ export interface SyncFromProviderReq {
dnsProviderAccessId: number;
}
const DOMAIN_IMPORT_TASK_TYPE = "domainImportTask";
const DOMAIN_EXPIRE_TASK_TYPE = "domainExpirationSyncTask";
const DOMAIN_IMPORT_TASK_TYPE = 'domainImportTask'
const DOMAIN_EXPIRE_TASK_TYPE = 'domainExpirationSyncTask'
const DOMAIN_EXPIRE_CHECK_TYPE = 'domainExpirationCheck'
const DOMAIN_EXPIRE_CHECK_TYPE = "domainExpirationCheck";
/**
*
@@ -63,8 +61,6 @@ export class DomainService extends BaseService<DomainEntity> {
@Inject()
cron: Cron;
//@ts-ignore
getRepository() {
return this.repository;
@@ -72,50 +68,48 @@ export class DomainService extends BaseService<DomainEntity> {
async add(param) {
if (param.userId == null) {
throw new Error('userId 不能为空');
throw new Error("userId 不能为空");
}
if (!param.domain) {
throw new Error('domain 不能为空');
throw new Error("domain 不能为空");
}
const old = await this.repository.findOne({
where: {
domain: param.domain,
userId: param.userId
}
userId: param.userId,
},
});
if (old) {
throw new Error(`域名(${param.domain})不能重复`);
}
if (!param.fromType) {
param.fromType = 'manual'
param.fromType = "manual";
}
return await super.add(param);
}
async update(param) {
if (!param.id) {
throw new Error('id 不能为空');
throw new Error("id 不能为空");
}
const old = await this.info(param.id)
const old = await this.info(param.id);
if (!old) {
throw new Error('domain记录不存在');
throw new Error("domain记录不存在");
}
const same = await this.repository.findOne({
where: {
domain: param.domain,
userId: old.userId,
id: Not(param.id)
}
id: Not(param.id),
},
});
if (same) {
throw new Error(`域名(${param.domain})不能重复`);
}
delete param.userId
delete param.userId;
return await super.update(param);
}
/**
@@ -124,23 +118,22 @@ export class DomainService extends BaseService<DomainEntity> {
* @param domains //去除* 且去重之后的域名列表
*/
async getDomainVerifiers(userId: number, projectId: number, domains: string[]): Promise<DomainVerifiers> {
const mainDomainMap: Record<string, string> = {}
const mainDomainMap: Record<string, string> = {};
const taskService = this.taskServiceBuilder.create({ userId: userId, projectId: projectId });
const subDomainGetter = await taskService.getSubDomainsGetter();
const domainParser = new DomainParser(subDomainGetter)
const domainParser = new DomainParser(subDomainGetter);
const mainDomains = []
const mainDomains = [];
for (const domain of domains) {
const mainDomain = await domainParser.parse(domain);
mainDomainMap[domain] = mainDomain;
mainDomains.push(mainDomain)
mainDomains.push(mainDomain);
}
//匹配DNS记录
let allDomains = [...domains, ...mainDomains]
let allDomains = [...domains, ...mainDomains];
//去重
allDomains = [...new Set(allDomains)]
allDomains = [...new Set(allDomains)];
//从 domain 表中获取配置
const domainRecords = await this.find({
@@ -149,19 +142,22 @@ export class DomainService extends BaseService<DomainEntity> {
userId,
projectId,
disabled: false,
}
})
},
});
const dnsMap = domainRecords.filter(item => item.challengeType === 'dns').reduce((pre, item) => {
pre[item.domain] = item
return pre
}, {})
const httpMap = domainRecords.filter(item => item.challengeType === 'http').reduce((pre, item) => {
pre[item.domain] = item
return pre
}, {})
const dnsMap = domainRecords
.filter(item => item.challengeType === "dns")
.reduce((pre, item) => {
pre[item.domain] = item;
return pre;
}, {});
const httpMap = domainRecords
.filter(item => item.challengeType === "http")
.reduce((pre, item) => {
pre[item.domain] = item;
return pre;
}, {});
//从cname record表中获取配置
const cnameRecords = await this.cnameRecordService.find({
@@ -170,91 +166,95 @@ export class DomainService extends BaseService<DomainEntity> {
userId,
projectId,
status: "valid",
}
})
},
});
const cnameMap = cnameRecords.reduce((pre, item) => {
pre[item.domain] = item
return pre
}, {})
pre[item.domain] = item;
return pre;
}, {});
//构建域名验证计划
const domainVerifiers: DomainVerifiers = {}
const domainVerifiers: DomainVerifiers = {};
for (const domain of domains) {
const mainDomain = mainDomainMap[domain]
const mainDomain = mainDomainMap[domain];
const dnsRecord = dnsMap[mainDomain]
const dnsRecord = dnsMap[mainDomain];
if (dnsRecord) {
domainVerifiers[domain] = {
domain,
mainDomain,
type: 'dns',
type: "dns",
dns: {
dnsProviderType: dnsRecord.dnsProviderType,
dnsProviderAccessId: dnsRecord.dnsProviderAccess
}
}
continue
dnsProviderAccessId: dnsRecord.dnsProviderAccess,
},
};
continue;
}
const cnameRecord: CnameRecordEntity = cnameMap[domain]
const cnameRecord: CnameRecordEntity = cnameMap[domain];
if (cnameRecord) {
domainVerifiers[domain] = {
domain,
mainDomain,
type: 'cname',
type: "cname",
cname: {
domain: cnameRecord.domain,
hostRecord: cnameRecord.hostRecord,
recordValue: cnameRecord.recordValue
}
}
continue
recordValue: cnameRecord.recordValue,
},
};
continue;
}
const httpRecord = httpMap[domain]
const httpRecord = httpMap[domain];
if (httpRecord) {
domainVerifiers[domain] = {
domain,
mainDomain,
type: 'http',
type: "http",
http: {
httpUploaderType: httpRecord.httpUploaderType,
httpUploaderAccess: httpRecord.httpUploaderAccess,
httpUploadRootDir: httpRecord.httpUploadRootDir
}
}
continue
httpUploadRootDir: httpRecord.httpUploadRootDir,
},
};
continue;
}
domainVerifiers[domain] = null
domainVerifiers[domain] = null;
}
return domainVerifiers;
}
async startDomainImportTask(req: { userId: number; projectId: number; key: string }) {
const key = req.key;
const setting = await this.userSettingService.getSetting<UserDomainImportSetting>(req.userId, req.projectId, UserDomainImportSetting);
async startDomainImportTask(req: { userId: number, projectId: number, key: string }) {
const key = req.key
const setting = await this.userSettingService.getSetting<UserDomainImportSetting>(req.userId, req.projectId, UserDomainImportSetting)
const item = setting.domainImportList.find(item => item.key === key)
const item = setting.domainImportList.find(item => item.key === key);
if (!item) {
throw new Error(`域名导入任务配置(${key})还未注册`)
throw new Error(`域名导入任务配置(${key})还未注册`);
}
const { dnsProviderType, dnsProviderAccessId, title } = item
const { dnsProviderType, dnsProviderAccessId, title } = item;
taskExecutor.start(new BackTask({
type: DOMAIN_IMPORT_TASK_TYPE,
key,
title: title,
run: async (task: BackTask) => {
await this._syncFromProvider({
userId: req.userId,
projectId: req.projectId,
dnsProviderType,
dnsProviderAccessId,
}, task)
},
}))
taskExecutor.start(
new BackTask({
type: DOMAIN_IMPORT_TASK_TYPE,
key,
title: title,
run: async (task: BackTask) => {
await this._syncFromProvider(
{
userId: req.userId,
projectId: req.projectId,
dnsProviderType,
dnsProviderAccessId,
},
task
);
},
})
);
}
private async _syncFromProvider(req: SyncFromProviderReq, task: BackTask) {
@@ -262,28 +262,28 @@ export class DomainService extends BaseService<DomainEntity> {
const serviceGetter = this.taskServiceBuilder.create({ userId, projectId });
const subDomainGetter = await serviceGetter.getSubDomainsGetter();
const domainParser = new DomainParser(subDomainGetter)
const domainParser = new DomainParser(subDomainGetter);
const access = await this.accessService.getById(dnsProviderAccessId, userId, projectId);
const context = { access, logger, http, utils, domainParser, serviceGetter };
// 翻页查询dns的记录
const dnsProvider = await createDnsProvider({ dnsProviderType, context })
const dnsProvider = await createDnsProvider({ dnsProviderType, context });
const pager = new Pager({
pageNo: 1,
pageSize: 100,
})
const challengeType = "dns"
});
const challengeType = "dns";
const getPage = async (pager: Pager) => {
return await dnsProvider.getDomainListPage(pager)
}
return await dnsProvider.getDomainListPage(pager);
};
const itemHandle = async (domainRecord: any) => {
task.incrementCurrent()
let domain = domainRecord.domain
task.incrementCurrent();
let domain = domainRecord.domain;
if (domain.endsWith(".")) {
domain = domain.slice(0, -1)
domain = domain.slice(0, -1);
}
const old = await this.findOne({
@@ -291,8 +291,8 @@ export class DomainService extends BaseService<DomainEntity> {
domain,
userId,
projectId,
}
})
},
});
if (old) {
// if (old.fromType !== 'auto') {
// //如果是手动的,跳过更新校验配置
@@ -300,17 +300,17 @@ export class DomainService extends BaseService<DomainEntity> {
// }
if (old) {
//如果old存在,直接跳过
task.incrementSkip()
return
task.incrementSkip();
return;
}
const updateObj: any = {
id: old.id,
dnsProviderType,
dnsProviderAccess: dnsProviderAccessId,
challengeType,
}
};
//更新
await super.update(updateObj)
await super.update(updateObj);
} else {
//添加
await this.add({
@@ -321,81 +321,80 @@ export class DomainService extends BaseService<DomainEntity> {
dnsProviderAccess: dnsProviderAccessId,
challengeType,
disabled: false,
fromType: 'manual',
})
logger.info(`导入域名${domain}到用户${userId}`)
fromType: "manual",
});
logger.info(`导入域名${domain}到用户${userId}`);
}
}
};
const batchHandle = async (pageRes: PageRes<any>) => {
task.setTotal(pageRes.total || 0)
}
await doPageTurn({ pager, getPage, itemHandle, batchHandle })
const key = `user_${userId || 0}`
logger.info(`从域名提供商${dnsProviderType}导入域名完成(${key}),共导入${task.total}个域名,跳过${task.getSkipCount()}个域名,成功${task.getSuccessCount()}个域名,失败${task.getErrorCount()}个域名`)
task.setTotal(pageRes.total || 0);
};
await doPageTurn({ pager, getPage, itemHandle, batchHandle });
const key = `user_${userId || 0}`;
logger.info(`从域名提供商${dnsProviderType}导入域名完成(${key}),共导入${task.total}个域名,跳过${task.getSkipCount()}个域名,成功${task.getSuccessCount()}个域名,失败${task.getErrorCount()}个域名`);
}
async getDomainImportTaskStatus(req: { userId?: number, projectId?: number }) {
const userId = req.userId || 0
const projectId = req.projectId
async getDomainImportTaskStatus(req: { userId?: number; projectId?: number }) {
const userId = req.userId || 0;
const projectId = req.projectId;
const setting = await this.userSettingService.getSetting<UserDomainImportSetting>(userId, projectId, UserDomainImportSetting)
const list = setting?.domainImportList || []
const setting = await this.userSettingService.getSetting<UserDomainImportSetting>(userId, projectId, UserDomainImportSetting);
const list = setting?.domainImportList || [];
const taskList: any = []
const taskList: any = [];
for (const item of list) {
const { key } = item
const { key } = item;
const task = taskExecutor.get(DOMAIN_IMPORT_TASK_TYPE, key)
const task = taskExecutor.get(DOMAIN_IMPORT_TASK_TYPE, key);
taskList.push({
...item,
task: task,
})
});
}
return taskList
return taskList;
}
async getProviderTitle(req: { userId?: number, projectId?: number, dnsProviderType: string, dnsProviderAccessId: number }) {
const userId = req.userId || 0
const projectId = req.projectId
const { dnsProviderType, dnsProviderAccessId } = req
const dnsProviderDefine = dnsProviderRegistry.getDefine(dnsProviderType)
async getProviderTitle(req: { userId?: number; projectId?: number; dnsProviderType: string; dnsProviderAccessId: number }) {
const userId = req.userId || 0;
const projectId = req.projectId;
const { dnsProviderType, dnsProviderAccessId } = req;
const dnsProviderDefine = dnsProviderRegistry.getDefine(dnsProviderType);
if (!dnsProviderDefine) {
throw new Error(`该域名提供商(${dnsProviderType})不存在,请检查是否已被注册`)
throw new Error(`该域名提供商(${dnsProviderType})不存在,请检查是否已被注册`);
}
const access = await this.accessService.getSimpleInfo(dnsProviderAccessId)
const access = await this.accessService.getSimpleInfo(dnsProviderAccessId);
if (!access || access.userId !== userId) {
throw new Error(`该授权(${dnsProviderAccessId})不存在,请检查是否已被删除`)
throw new Error(`该授权(${dnsProviderAccessId})不存在,请检查是否已被删除`);
}
if (projectId && access.projectId !== projectId) {
throw new Error(`该授权(${dnsProviderAccessId})不存在,请检查是否已被删除`)
throw new Error(`该授权(${dnsProviderAccessId})不存在,请检查是否已被删除`);
}
return {
title: `${dnsProviderDefine.title}_${access.name || ''}`,
title: `${dnsProviderDefine.title}_${access.name || ""}`,
//@ts-ignore
icon: dnsProviderDefine.icon || '',
}
icon: dnsProviderDefine.icon || "",
};
}
async addDomainImportTask(req: { userId?: number, projectId?: number, dnsProviderType: string, dnsProviderAccessId: number, index?: number }) {
const userId = req.userId || 0
const projectId = req.projectId
const { dnsProviderType, dnsProviderAccessId, index = 0 } = req
const key = `user_${userId}_${dnsProviderType}_${dnsProviderAccessId}`
async addDomainImportTask(req: { userId?: number; projectId?: number; dnsProviderType: string; dnsProviderAccessId: number; index?: number }) {
const userId = req.userId || 0;
const projectId = req.projectId;
const { dnsProviderType, dnsProviderAccessId, index = 0 } = req;
const key = `user_${userId}_${dnsProviderType}_${dnsProviderAccessId}`;
const { title, icon } = await this.getProviderTitle(req)
const { title, icon } = await this.getProviderTitle(req);
const setting = await this.userSettingService.getSetting<UserDomainImportSetting>(userId, projectId, UserDomainImportSetting)
setting.domainImportList = setting.domainImportList || []
const setting = await this.userSettingService.getSetting<UserDomainImportSetting>(userId, projectId, UserDomainImportSetting);
setting.domainImportList = setting.domainImportList || [];
if (setting.domainImportList.find(item => item.key === key)) {
throw new Error(`该域名导入任务${key}已存在`)
throw new Error(`该域名导入任务${key}已存在`);
}
const access = await this.accessService.getAccessById(dnsProviderAccessId, true, userId, projectId)
const access = await this.accessService.getAccessById(dnsProviderAccessId, true, userId, projectId);
if (!access) {
throw new Error(`该授权(${dnsProviderAccessId})不存在,请检查是否已被删除`)
throw new Error(`该授权(${dnsProviderAccessId})不存在,请检查是否已被删除`);
}
const item = {
@@ -403,100 +402,98 @@ export class DomainService extends BaseService<DomainEntity> {
dnsProviderAccessId,
key,
title,
icon: icon || '',
}
setting.domainImportList.splice(index, 0, item)
await this.userSettingService.saveSetting(userId, projectId, setting)
icon: icon || "",
};
setting.domainImportList.splice(index, 0, item);
await this.userSettingService.saveSetting(userId, projectId, setting);
return item
return item;
}
async deleteDomainImportTask(req: { userId?: number, projectId?: number, key: string }) {
const userId = req.userId || 0
const { key } = req
async deleteDomainImportTask(req: { userId?: number; projectId?: number; key: string }) {
const userId = req.userId || 0;
const { key } = req;
const projectId = req.projectId
const setting = await this.userSettingService.getSetting<UserDomainImportSetting>(userId, projectId, UserDomainImportSetting)
setting.domainImportList = setting.domainImportList || []
const index = setting.domainImportList.findIndex(item => item.key === key)
const projectId = req.projectId;
const setting = await this.userSettingService.getSetting<UserDomainImportSetting>(userId, projectId, UserDomainImportSetting);
setting.domainImportList = setting.domainImportList || [];
const index = setting.domainImportList.findIndex(item => item.key === key);
if (index === -1) {
throw new Error(`该域名导入任务${key}不存在`)
throw new Error(`该域名导入任务${key}不存在`);
}
setting.domainImportList.splice(index, 1)
taskExecutor.clear(DOMAIN_IMPORT_TASK_TYPE, key)
await this.userSettingService.saveSetting(userId, projectId, setting)
setting.domainImportList.splice(index, 1);
taskExecutor.clear(DOMAIN_IMPORT_TASK_TYPE, key);
await this.userSettingService.saveSetting(userId, projectId, setting);
}
async saveDomainImportTask(req: { userId?: number, projectId?: number, dnsProviderType: string, dnsProviderAccessId: number, key?: string }) {
const userId = req.userId || 0
const projectId = req.projectId
const { dnsProviderType, dnsProviderAccessId, key } = req
const setting = await this.userSettingService.getSetting<UserDomainImportSetting>(userId, projectId, UserDomainImportSetting)
setting.domainImportList = setting.domainImportList || []
async saveDomainImportTask(req: { userId?: number; projectId?: number; dnsProviderType: string; dnsProviderAccessId: number; key?: string }) {
const userId = req.userId || 0;
const projectId = req.projectId;
const { dnsProviderType, dnsProviderAccessId, key } = req;
const setting = await this.userSettingService.getSetting<UserDomainImportSetting>(userId, projectId, UserDomainImportSetting);
setting.domainImportList = setting.domainImportList || [];
let index = 0
let index = 0;
if (key) {
index = setting.domainImportList.findIndex(item => item.key === key)
index = setting.domainImportList.findIndex(item => item.key === key);
if (index === -1) {
throw new Error(`该域名导入任务${key}不存在`)
throw new Error(`该域名导入任务${key}不存在`);
}
await this.deleteDomainImportTask({ userId, projectId, key })
await this.deleteDomainImportTask({ userId, projectId, key });
}
return await this.addDomainImportTask({ userId, projectId, dnsProviderType, dnsProviderAccessId, index })
return await this.addDomainImportTask({ userId, projectId, dnsProviderType, dnsProviderAccessId, index });
}
async getSyncExpirationTaskStatus(req: { userId?: number, projectId?: number }) {
const userId = req.userId ?? 'all'
const projectId = req.projectId
let key = `user_${userId}`
async getSyncExpirationTaskStatus(req: { userId?: number; projectId?: number }) {
const userId = req.userId ?? "all";
const projectId = req.projectId;
let key = `user_${userId}`;
if (projectId != null) {
key += `_${projectId}`
key += `_${projectId}`;
}
const task = taskExecutor.get(DOMAIN_EXPIRE_TASK_TYPE, key)
return task
const task = taskExecutor.get(DOMAIN_EXPIRE_TASK_TYPE, key);
return task;
}
async startSyncExpirationTask(req: { userId?: number, projectId?: number }) {
const userId = req.userId
const projectId = req.projectId
let key = `user_${userId ?? 'all'}`
async startSyncExpirationTask(req: { userId?: number; projectId?: number }) {
const userId = req.userId;
const projectId = req.projectId;
let key = `user_${userId ?? "all"}`;
if (projectId != null) {
key += `_${projectId}`
key += `_${projectId}`;
}
taskExecutor.start(new BackTask({
type: DOMAIN_EXPIRE_TASK_TYPE,
key,
title: `同步注册域名过期时间(${key}))`,
run: async (task: BackTask) => {
await this._syncDomainsExpirationDate({ userId, projectId, task })
if (userId != null) {
await this.startCheckDomainExpiration({ userId, projectId })
}
}
}))
taskExecutor.start(
new BackTask({
type: DOMAIN_EXPIRE_TASK_TYPE,
key,
title: `同步注册域名过期时间(${key}))`,
run: async (task: BackTask) => {
await this._syncDomainsExpirationDate({ userId, projectId, task });
if (userId != null) {
await this.startCheckDomainExpiration({ userId, projectId });
}
},
})
);
}
private async _syncDomainsExpirationDate(req: { userId?: number, projectId?: number, task: BackTask }) {
private async _syncDomainsExpirationDate(req: { userId?: number; projectId?: number; task: BackTask }) {
//同步所有域名的过期时间
const pager = new Pager({
pageNo: 1,
pageSize: 100,
})
});
const tldClient = new TldClient();
const query: any = {
challengeType: "dns",
}
};
if (req.userId != null) {
query.userId = req.userId
query.userId = req.userId;
}
if (req.projectId != null) {
query.projectId = req.projectId
query.projectId = req.projectId;
}
const getDomainPage = async (pager: Pager) => {
const pageRes = await this.page({
@@ -507,62 +504,61 @@ export class DomainService extends BaseService<DomainEntity> {
page: {
offset: pager.getOffset(),
limit: pager.pageSize,
}
})
req.task.total = pageRes.total
},
});
req.task.total = pageRes.total;
return {
list: pageRes.records,
total: pageRes.total,
}
}
};
};
const itemHandle = async (item: any) => {
req.task.incrementCurrent()
req.task.incrementCurrent();
try {
const res = await tldClient.getDomainExpirationDate(item.domain)
const { expirationDate, registrationDate } = res
const res = await tldClient.getDomainExpirationDate(item.domain);
const { expirationDate, registrationDate } = res;
if (!expirationDate) {
req.task.addError(`${item.domain}】获取域名${item.domain}过期时间失败`)
return
req.task.addError(`${item.domain}】获取域名${item.domain}过期时间失败`);
return;
}
logger.info(`${item.domain}】更新域名过期时间:${dayjs(expirationDate).format('YYYY-MM-DD')}`)
logger.info(`${item.domain}】更新域名过期时间:${dayjs(expirationDate).format("YYYY-MM-DD")}`);
const updateObj: any = {
id: item.id,
expirationDate: expirationDate,
registrationDate: registrationDate,
}
};
//更新
await super.update(updateObj)
await super.update(updateObj);
} catch (error) {
const errorMsg = `${item.domain}${error.message ?? error}`
req.task.addError(errorMsg)
logger.error(errorMsg)
const errorMsg = `${item.domain}${error.message ?? error}`;
req.task.addError(errorMsg);
logger.error(errorMsg);
} finally {
await utils.sleep(1000)
await utils.sleep(1000);
}
}
};
await doPageTurn({ pager, getPage: getDomainPage, itemHandle: itemHandle })
const key = `user_${req.userId || 'all'}`
const log = `同步用户(${key})注册域名过期时间完成(${req.task.getSuccessCount()}个成功,${req.task.getErrorCount()}个失败)`
logger.info(log)
await doPageTurn({ pager, getPage: getDomainPage, itemHandle: itemHandle });
const key = `user_${req.userId || "all"}`;
const log = `同步用户(${key})注册域名过期时间完成(${req.task.getSuccessCount()}个成功,${req.task.getErrorCount()}个失败)`;
logger.info(log);
}
public async startCheckDomainExpiration(req: { userId?: number, projectId?: number }) {
const { userId, projectId } = req
public async startCheckDomainExpiration(req: { userId?: number; projectId?: number }) {
const { userId, projectId } = req;
if (userId == null) {
throw new Error('userId is required');
throw new Error("userId is required");
}
if (projectId && !isEnterprise()) {
logger.warn(`当前未开启企业模式,跳过检查项目(${projectId})的域名过期时间`)
return
logger.warn(`当前未开启企业模式,跳过检查项目(${projectId})的域名过期时间`);
return;
}
const setting = await this.monitorSettingGet({ userId, projectId })
const setting = await this.monitorSettingGet({ userId, projectId });
if (!setting || !setting.enabled) {
return
return;
}
const jobHistory: any = {
@@ -572,46 +568,46 @@ export class DomainService extends BaseService<DomainEntity> {
title: `检查注册域名过期时间`,
startAt: dayjs().valueOf(),
result: "start",
}
await this.jobHistoryService.add(jobHistory)
};
await this.jobHistoryService.add(jobHistory);
const expireDays = setting.willExpireDays || 30
const ltTime = dayjs().add(expireDays, 'day').valueOf()
const expireDays = setting.willExpireDays || 30;
const ltTime = dayjs().add(expireDays, "day").valueOf();
const total = await this.repository.count({
where:{
where: {
userId,
projectId,
disabled: false,
}
})
},
});
//开始检查域名过期时间
const list = await this.repository.find({
where: {
userId,
projectId,
disabled: false,
expirationDate: LessThan(ltTime)
}
})
expirationDate: LessThan(ltTime),
},
});
const now = dayjs().valueOf()
let willExpireDomains = []
let hasExpireDomains = []
const now = dayjs().valueOf();
const willExpireDomains = [];
const hasExpireDomains = [];
for (const item of list) {
const { expirationDate } = item
const leftDays = dayjs(expirationDate).diff(dayjs(), 'day')
const { expirationDate } = item;
const leftDays = dayjs(expirationDate).diff(dayjs(), "day");
//@ts-ignore
item.leftDays = leftDays
item.leftDays = leftDays;
if (expirationDate < now) {
hasExpireDomains.push(item)
hasExpireDomains.push(item);
} else {
willExpireDomains.push(item)
willExpireDomains.push(item);
}
}
const title = `域名过期检查:即将过期 ${willExpireDomains.length} 个域名,已过期 ${hasExpireDomains.length} 个域名,共 ${total} 个域名`
const title = `域名过期检查:即将过期 ${willExpireDomains.length} 个域名,已过期 ${hasExpireDomains.length} 个域名,共 ${total} 个域名`;
try {
await this.jobHistoryService.update({
@@ -619,30 +615,29 @@ export class DomainService extends BaseService<DomainEntity> {
content: title,
result: "done",
endAt: dayjs().valueOf(),
})
});
} catch (error) {
logger.error(`更新域名过期检查任务状态失败:${error.message ?? error}`)
logger.error(`更新域名过期检查任务状态失败:${error.message ?? error}`);
}
if (list.length == 0) {
//没有过期域名 不发通知
return
return;
}
//发送通知
const willExpireDomainsStr = willExpireDomains.map(item => `${item.domain} (剩余${item.leftDays}天)`).join('\n ')
const hasExpireDomainsStr = hasExpireDomains.map(item => `${item.domain} (已过期${item.leftDays}天)`).join('\n ')
const willExpireDomainsStr = willExpireDomains.map(item => `${item.domain} (剩余${item.leftDays}天)`).join("\n ");
const hasExpireDomainsStr = hasExpireDomains.map(item => `${item.domain} (已过期${item.leftDays}天)`).join("\n ");
const content = `您有域名即将过期,请尽快续费
即将过期域名: ${willExpireDomains.length} 个 (有效期<${expireDays}天)
${willExpireDomainsStr}
已过期域名: ${hasExpireDomains.length}
${hasExpireDomainsStr}`
${hasExpireDomainsStr}`;
const taskService = this.taskServiceBuilder.create({ userId: userId, projectId: projectId });
const notificationService = await taskService.getNotificationService()
const notificationService = await taskService.getNotificationService();
const url = await notificationService.getBindUrl("#/certd/cert/domain");
await notificationService.send({
id: setting.notificationId,
@@ -656,39 +651,37 @@ export class DomainService extends BaseService<DomainEntity> {
notificationType: DOMAIN_EXPIRE_CHECK_TYPE,
willExpireDomains,
hasExpireDomains,
}
})
},
});
}
public async monitorSettingGet(req: { userId?: number, projectId?: number }) {
const { userId, projectId } = req
const setting = await this.userSettingService.getSetting<UserDomainMonitorSetting>(userId, projectId, UserDomainMonitorSetting)
return setting || {}
public async monitorSettingGet(req: { userId?: number; projectId?: number }) {
const { userId, projectId } = req;
const setting = await this.userSettingService.getSetting<UserDomainMonitorSetting>(userId, projectId, UserDomainMonitorSetting);
return setting || {};
}
public async monitorSettingSave(req: { userId?: number, projectId?: number, setting?: any }) {
const { userId, projectId, setting } = req
const bean: UserDomainMonitorSetting = new UserDomainMonitorSetting()
merge(bean, setting)
await this.userSettingService.saveSetting<UserDomainMonitorSetting>(userId, projectId, bean)
await this.registerMonitorCron({ userId, projectId })
public async monitorSettingSave(req: { userId?: number; projectId?: number; setting?: any }) {
const { userId, projectId, setting } = req;
const bean: UserDomainMonitorSetting = new UserDomainMonitorSetting();
merge(bean, setting);
await this.userSettingService.saveSetting<UserDomainMonitorSetting>(userId, projectId, bean);
await this.registerMonitorCron({ userId, projectId });
}
public async registerMonitorCron(req: { userId?: number, projectId?: number }) {
const { userId, projectId } = req
const setting = await this.monitorSettingGet(req)
const key = `${DOMAIN_EXPIRE_CHECK_TYPE}:${userId}_${projectId || ''}`
this.cron.remove(key)
public async registerMonitorCron(req: { userId?: number; projectId?: number }) {
const { userId, projectId } = req;
const setting = await this.monitorSettingGet(req);
const key = `${DOMAIN_EXPIRE_CHECK_TYPE}:${userId}_${projectId || ""}`;
this.cron.remove(key);
if (setting.enabled) {
this.cron.register({
cron: setting.cron,
name: key,
job: async () => {
await this.startCheckDomainExpiration({ userId, projectId })
await this.startCheckDomainExpiration({ userId, projectId });
},
})
});
}
}
}
@@ -73,7 +73,7 @@ export class TldClient {
method: "GET",
});
let res: DomainInfo = {};
const res: DomainInfo = {};
const events = rdap.events || [];
for (const item of events) {
if (item.eventAction === "expiration") {
@@ -96,7 +96,7 @@ export class TldClient {
timeout: 5000,
});
let res: DomainInfo = {};
const res: DomainInfo = {};
/**
* {
"Domain Status": [
@@ -1,39 +1,39 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
/**
* cname配置
*/
@Entity('cd_cname_provider')
@Entity("cd_cname_provider")
export class CnameProviderEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ comment: 'userId', name: 'user_id' })
@Column({ comment: "userId", name: "user_id" })
userId: number;
@Column({ comment: '域名', length: 100 })
@Column({ comment: "域名", length: 100 })
domain: string;
@Column({ comment: '子域名托管', length: 100, nullable: true })
@Column({ comment: "子域名托管", length: 100, nullable: true })
subdomain: string;
@Column({ comment: 'DNS提供商类型', name: 'dns_provider_type', length: 20 })
@Column({ comment: "DNS提供商类型", name: "dns_provider_type", length: 20 })
dnsProviderType: string;
@Column({ comment: 'DNS授权Id', name: 'access_id' })
@Column({ comment: "DNS授权Id", name: "access_id" })
accessId: number;
@Column({ comment: '是否默认', name: 'is_default' })
@Column({ comment: "是否默认", name: "is_default" })
isDefault: boolean;
@Column({ comment: '是否禁用', name: 'disabled' })
@Column({ comment: "是否禁用", name: "disabled" })
disabled: boolean;
@Column({ comment: '备注', length: 200 })
@Column({ comment: "备注", length: 200 })
remark: string;
@Column({
comment: '创建时间',
name: 'create_time',
default: () => 'CURRENT_TIMESTAMP',
comment: "创建时间",
name: "create_time",
default: () => "CURRENT_TIMESTAMP",
})
createTime: Date;
@Column({
comment: '修改时间',
name: 'update_time',
default: () => 'CURRENT_TIMESTAMP',
comment: "修改时间",
name: "update_time",
default: () => "CURRENT_TIMESTAMP",
})
updateTime: Date;
@@ -1,49 +1,49 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
export type CnameRecordStatusType = 'cname' | 'validating' | 'valid' | 'error' | 'timeout';
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
export type CnameRecordStatusType = "cname" | "validating" | "valid" | "error" | "timeout";
/**
* cname record配置
*/
@Entity('cd_cname_record')
@Entity("cd_cname_record")
export class CnameRecordEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ comment: '用户ID', name: 'user_id' })
@Column({ comment: "用户ID", name: "user_id" })
userId: number;
@Column({ comment: '证书申请域名', length: 100 })
@Column({ comment: "证书申请域名", length: 100 })
domain: string;
@Column({ comment: '主域名', name: 'main_domain', length: 100 })
mainDomain:string;
@Column({ comment: "主域名", name: "main_domain", length: 100 })
mainDomain: string;
@Column({ comment: '主机记录', name: 'host_record', length: 100 })
@Column({ comment: "主机记录", name: "host_record", length: 100 })
hostRecord: string;
@Column({ comment: '记录值', name: 'record_value', length: 200 })
@Column({ comment: "记录值", name: "record_value", length: 200 })
recordValue: string;
@Column({ comment: 'CNAME提供者', name: 'cname_provider_id' })
@Column({ comment: "CNAME提供者", name: "cname_provider_id" })
cnameProviderId: number;
@Column({ comment: '验证状态', length: 20 })
@Column({ comment: "验证状态", length: 20 })
status: string;
@Column({ comment: '错误信息' })
error: string
@Column({ comment: "错误信息" })
error: string;
@Column({ name: 'project_id', comment: '项目Id' })
@Column({ name: "project_id", comment: "项目Id" })
projectId: number;
@Column({
comment: '创建时间',
name: 'create_time',
default: () => 'CURRENT_TIMESTAMP',
comment: "创建时间",
name: "create_time",
default: () => "CURRENT_TIMESTAMP",
})
createTime: Date;
@Column({
comment: '修改时间',
name: 'update_time',
default: () => 'CURRENT_TIMESTAMP',
comment: "修改时间",
name: "update_time",
default: () => "CURRENT_TIMESTAMP",
})
updateTime: Date;
}
@@ -1,9 +1,9 @@
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { BaseService, ListReq, SysPrivateSettings, SysSettingsService, ValidateException } from '@certd/lib-server';
import { CnameProviderEntity } from '../entity/cname-provider.js';
import { CommonProviders } from './common-provider.js';
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { InjectEntityModel } from "@midwayjs/typeorm";
import { Repository } from "typeorm";
import { BaseService, ListReq, SysPrivateSettings, SysSettingsService, ValidateException } from "@certd/lib-server";
import { CnameProviderEntity } from "../entity/cname-provider.js";
import { CommonProviders } from "./common-provider.js";
/**
* 授权
@@ -63,7 +63,7 @@ export class CnameProviderService extends BaseService<CnameProviderEntity> {
for (const id of ids) {
const info = await this.info(id);
if (info.isDefault) {
throw new ValidateException('默认的CNAME服务不能删除,请先修改为非默认值');
throw new ValidateException("默认的CNAME服务不能删除,请先修改为非默认值");
}
}
await super.delete(ids);
@@ -85,7 +85,7 @@ export class CnameProviderService extends BaseService<CnameProviderEntity> {
if (def) {
return def;
}
const founds = await this.repository.find({ take: 1, order: { createTime: 'DESC' }, where: { disabled: false } });
const founds = await this.repository.find({ take: 1, order: { createTime: "DESC" }, where: { disabled: false } });
if (founds && founds.length > 0) {
return founds[0];
}
@@ -127,5 +127,4 @@ export class CnameProviderService extends BaseService<CnameProviderEntity> {
}
return await super.info(id, infoIgnoreProperty);
}
}
@@ -1,13 +1,6 @@
import { createChallengeFn, getAuthoritativeDnsResolver } from "@certd/acme-client";
import { cache, http, isDev, logger, utils } from "@certd/basic";
import {
AccessService,
BaseService,
PlusService,
SysInstallInfo,
SysSettingsService,
ValidateException
} from "@certd/lib-server";
import { AccessService, BaseService, PlusService, SysInstallInfo, SysSettingsService, ValidateException } from "@certd/lib-server";
import { CnameProvider, CnameRecord } from "@certd/pipeline";
import { createDnsProvider, DomainParser, IDnsProvider } from "@certd/plugin-cert";
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
@@ -58,7 +51,6 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
@Inject()
taskServiceBuilder: TaskServiceBuilder;
//@ts-ignore
getRepository() {
return this.repository;
@@ -135,7 +127,7 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
throw new ValidateException("id不能为空");
}
//hostRecord包含所有权校验信息,不允许用户修改hostRecord
delete param.hostRecord
delete param.hostRecord;
const old = await this.info(param.id);
if (!old) {
@@ -164,32 +156,32 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
// // 3. 检查原域名是否有cname记录
// }
async getWithAccessByDomain(domain: string, userId: number,projectId?:number) {
const record: CnameRecord = await this.getByDomain(domain, userId,projectId);
async getWithAccessByDomain(domain: string, userId: number, projectId?: number) {
const record: CnameRecord = await this.getByDomain(domain, userId, projectId);
if (record.cnameProvider.id > 0) {
//自定义cname服务
record.cnameProvider.access = await this.accessService.getAccessById(record.cnameProvider.accessId, false);
} else {
record.commonDnsProvider = new CommonDnsProvider({
config: record.cnameProvider,
plusService: this.plusService
plusService: this.plusService,
});
}
return record;
}
async getByDomain(domain: string, userId: number,projectId?:number, createOnNotFound = true) {
async getByDomain(domain: string, userId: number, projectId?: number, createOnNotFound = true) {
if (!domain) {
throw new ValidateException("domain不能为空");
}
if (userId == null) {
throw new ValidateException("userId不能为空");
}
let record = await this.getRepository().findOne({ where: { domain, userId,projectId } });
let record = await this.getRepository().findOne({ where: { domain, userId, projectId } });
if (record == null) {
if (createOnNotFound) {
record = await this.add({ domain, userId,projectId });
record = await this.add({ domain, userId, projectId });
} else {
throw new ValidateException(`找不到${domain}的CNAME记录`);
}
@@ -205,8 +197,8 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
return {
...record,
cnameProvider: {
...provider
} as CnameProvider
...provider,
} as CnameProvider,
} as CnameRecord;
}
@@ -222,17 +214,16 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
if (domainPrefix) {
const prefixStr = domainPrefix + ".";
record.mainDomain = record.domain.substring(prefixStr.length);
}else{
} else {
record.mainDomain = record.domain;
}
if (update) {
await this.update({
id: record.id,
mainDomain: record.mainDomain
mainDomain: record.mainDomain,
});
}
}
}
@@ -241,8 +232,7 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
* @param id
*/
async verify(id: number) {
const {walkTxtRecord} = createChallengeFn({logger});
const { walkTxtRecord } = createChallengeFn({ logger });
const bean = await this.info(id);
if (!bean) {
throw new ValidateException(`CnameRecord:${id} 不存在`);
@@ -251,7 +241,7 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
return true;
}
await this.getByDomain(bean.domain, bean.userId,bean.projectId);
await this.getByDomain(bean.domain, bean.userId, bean.projectId);
const taskService = this.taskServiceBuilder.create({ userId: bean.userId, projectId: bean.projectId });
const subDomainGetter = await taskService.getSubDomainsGetter();
const domainParser = new DomainParser(subDomainGetter);
@@ -263,7 +253,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;
@@ -285,17 +275,17 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
//公共CNAME
return new CommonDnsProvider({
config: cnameProvider,
plusService: this.plusService
plusService: this.plusService,
});
}
const record = await this.getWithAccessByDomain(bean.domain, bean.userId,bean.projectId);
const record = await this.getWithAccessByDomain(bean.domain, bean.userId, bean.projectId);
const access = record.cnameProvider.access
const context = { access, logger, http, utils, domainParser, serviceGetter:taskService };
const access = record.cnameProvider.access;
const context = { access, logger, http, utils, domainParser, serviceGetter: taskService };
const dnsProvider: IDnsProvider = await createDnsProvider({
dnsProviderType: cnameProvider.dnsProviderType,
context
context,
});
return dnsProvider;
};
@@ -309,7 +299,7 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
}
await dnsProvider.removeRecord({
recordReq: value.recordReq,
recordRes: value.recordRes
recordRes: value.recordRes,
});
logger.info("删除CNAME的校验DNS记录成功");
} catch (e) {
@@ -329,7 +319,6 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
return false;
}
const originDomain = await domainParser.parse(bean.domain);
const fullDomain = `${bean.hostRecord}.${originDomain}`;
@@ -366,7 +355,7 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
}
cache.set(cacheKey, value, {
ttl: ttl
ttl: ttl,
});
const domain = await domainParser.parse(bean.recordValue);
@@ -377,7 +366,7 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
fullRecord: fullRecord,
hostRecord: hostRecord,
type: "TXT",
value: testRecordValue
value: testRecordValue,
};
const dnsProvider = await buildDnsProvider();
@@ -418,8 +407,6 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
}
async checkRepeatAcmeChallengeRecords(acmeRecordDomain: string, targetCnameDomain: string) {
let dnsResolver = null;
try {
dnsResolver = await getAuthoritativeDnsResolver(acmeRecordDomain);
@@ -443,7 +430,6 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
throw new Error(`${acmeRecordDomain}存在多个CNAME记录,请删除多余的CNAME记录:${cnameRecord}`);
}
}
}
// 确保权威服务器里面没有纯粹的TXT记录
@@ -465,7 +451,7 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
return;
}
const {walkTxtRecord} = createChallengeFn({logger});
const { walkTxtRecord } = createChallengeFn({ logger });
if (cnameRecords.length > 0) {
// 从cname记录中获取txt记录
@@ -479,7 +465,6 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
}
}
}
}
async resetStatus(id: number) {
@@ -489,27 +474,32 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
await this.getRepository().update(id, { status: "cname", mainDomain: "" });
}
async doImport(req:{ userId: number; projectId: number; domainList: string; cnameProviderId: any }) {
const {userId,projectId,cnameProviderId,domainList} = req;
const domains = domainList.split("\n").map(item => item.trim()).filter(item => item.length > 0);
async doImport(req: { userId: number; projectId: number; domainList: string; cnameProviderId: any }) {
const { userId, projectId, cnameProviderId, domainList } = req;
const domains = domainList
.split("\n")
.map(item => item.trim())
.filter(item => item.length > 0);
if (domains.length === 0) {
throw new ValidateException("域名列表不能为空");
}
if (!req.cnameProviderId) {
throw new ValidateException("CNAME服务提供商不能为空");
}
taskExecutor.start(new BackTask({
type:"cnameImport",
key: "user_"+userId,
title: "导入CNAME记录",
run: async (task) => {
await this._import({ userId,projectId, domains, cnameProviderId },task);
}
}));
taskExecutor.start(
new BackTask({
type: "cnameImport",
key: "user_" + userId,
title: "导入CNAME记录",
run: async task => {
await this._import({ userId, projectId, domains, cnameProviderId }, task);
},
})
);
}
async _import(req :{ userId: number; projectId: number; domains: string[]; cnameProviderId: any },task:BackTask) {
async _import(req: { userId: number; projectId: number; domains: string[]; cnameProviderId: any }, task: BackTask) {
const userId = req.userId;
for (const domain of req.domains) {
const old = await this.getRepository().findOne({
@@ -523,17 +513,16 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
logger.warn(`域名${domain}已存在,跳过`);
}
//开始导入
try{
await this.add({
userId,
domain: domain,
projectId: req.projectId,
cnameProviderId: req.cnameProviderId,
});
}catch(e){
try {
await this.add({
userId,
domain: domain,
projectId: req.projectId,
cnameProviderId: req.cnameProviderId,
});
} catch (e) {
logger.error(`导入域名${domain}失败:${e.message}`);
}
}
}
}
@@ -1,7 +1,7 @@
import {CreateRecordOptions, DnsProviderContext, DomainRecord, IDnsProvider, RemoveRecordOptions} from '@certd/plugin-cert';
import {PlusService} from '@certd/lib-server';
import punycode from 'punycode.js'
import { Pager, PageRes } from '@certd/pipeline';
import { CreateRecordOptions, DnsProviderContext, DomainRecord, IDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert";
import { PlusService } from "@certd/lib-server";
import punycode from "punycode.js";
import { Pager, PageRes } from "@certd/pipeline";
export type CommonCnameProvider = {
id: number;
domain: string;
@@ -10,8 +10,8 @@ export type CommonCnameProvider = {
export const CommonProviders = [
{
id: -1,
domain: 'cname.certd.com.cn',
title: '公共CNAME服务',
domain: "cname.certd.com.cn",
title: "公共CNAME服务",
},
];
@@ -25,7 +25,7 @@ export class CommonDnsProvider implements IDnsProvider {
this.plusService = opts.plusService;
}
getDomainListPage(pager: Pager): Promise<PageRes<DomainRecord>> {
throw new Error('公共CNAME服务不支持获取域名列表');
throw new Error("公共CNAME服务不支持获取域名列表");
}
/**
@@ -44,24 +44,22 @@ export class CommonDnsProvider implements IDnsProvider {
return punycode.decode(domain);
}
usePunyCode(): boolean {
return false
return false;
}
async onInstance() {
}
async onInstance() {}
async createRecord(options: CreateRecordOptions) {
if (!this.config.domain.endsWith(options.domain)) {
throw new Error('cname服务域名不匹配');
throw new Error("cname服务域名不匹配");
}
await this.plusService.register();
const res = await this.plusService.requestWithToken({
url: '/activation/certd/cname/recordCreate',
method: 'post',
url: "/activation/certd/cname/recordCreate",
method: "post",
data: {
subjectId: await this.plusService.getSubjectId(),
domain: options.domain,
@@ -75,8 +73,8 @@ export class CommonDnsProvider implements IDnsProvider {
async removeRecord(options: RemoveRecordOptions<any>) {
const res = await this.plusService.requestWithToken({
url: '/activation/certd/cname/recordRemove',
method: 'post',
url: "/activation/certd/cname/recordRemove",
method: "post",
data: {
subjectId: await this.plusService.getSubjectId(),
domain: options.recordReq.domain,
@@ -1,10 +1,10 @@
import { logger } from '@certd/basic';
import { Config, Configuration, IMidwayContainer } from '@midwayjs/core';
import { Cron } from './cron.js';
import { logger } from "@certd/basic";
import { Config, Configuration, IMidwayContainer } from "@midwayjs/core";
import { Cron } from "./cron.js";
// ... (see below) ...
@Configuration({
namespace: 'cron',
namespace: "cron",
//importConfigs: [join(__dirname, './config')],
})
export class CronConfiguration {
@@ -12,13 +12,13 @@ export class CronConfiguration {
config;
cron: Cron;
async onReady(container: IMidwayContainer) {
logger.info('cron start');
logger.info("cron start");
this.cron = new Cron({
logger: logger,
...this.config,
});
container.registerObject('cron', this.cron);
container.registerObject("cron", this.cron);
this.cron.start();
logger.info('cron started');
logger.info("cron started");
}
}
@@ -1,5 +1,5 @@
import parser from 'cron-parser';
import { ILogger, logger } from '@certd/basic';
import parser from "cron-parser";
import { ILogger, logger } from "@certd/basic";
export type CronTaskReq = {
/**
@@ -57,7 +57,7 @@ export class Cron {
}
start() {
this.logger.info('[cron] start');
this.logger.info("[cron] start");
this.queue.forEach(task => {
task.genNextTime();
});
@@ -85,11 +85,11 @@ export class Cron {
}
this.logger.info(`[cron] register cron : [${req.name}] ,${req.cron}`);
this.remove(req.name)
this.remove(req.name);
const task = new CronTask(req, this.logger);
this.queue.push(task);
this.logger.info('当前定时任务数量:', this.getTaskSize());
this.logger.info("当前定时任务数量:", this.getTaskSize());
}
remove(taskName: string) {
@@ -99,7 +99,7 @@ export class Cron {
this.queue[index].stop();
this.queue.splice(index, 1);
}
this.logger.info('当前定时任务数量:', this.getTaskSize());
this.logger.info("当前定时任务数量:", this.getTaskSize());
}
getTaskSize() {
@@ -1,5 +1,5 @@
// src/index.ts
export { CronConfiguration as Configuration } from './configuration.js';
export { CronConfiguration as Configuration } from "./configuration.js";
// export * from './controller/user';
// export * from './controller/api';
// export * from './service/user';
@@ -1,14 +1,14 @@
import { SqliteAdapter } from './sqlite.js';
import { PostgresqlAdapter } from './postgresql.js';
import { Config, Init, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { SqlAdapter } from './d.js';
import { MysqlAdapter } from './mysql.js';
import { SqliteAdapter } from "./sqlite.js";
import { PostgresqlAdapter } from "./postgresql.js";
import { Config, Init, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { SqlAdapter } from "./d.js";
import { MysqlAdapter } from "./mysql.js";
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class DbAdapter implements SqlAdapter {
adapter: SqlAdapter;
@Config('typeorm.dataSource.default.type')
@Config("typeorm.dataSource.default.type")
dbType: string;
@Init()
@@ -25,13 +25,13 @@ export class DbAdapter implements SqlAdapter {
}
isSqlite() {
return this.dbType === 'better-sqlite3';
return this.dbType === "better-sqlite3";
}
isPostgresql() {
return this.dbType === 'postgres';
return this.dbType === "postgres";
}
isMysql() {
return this.dbType === 'mysql' || this.dbType === 'mariadb';
return this.dbType === "mysql" || this.dbType === "mariadb";
}
date(columnName: string) {
@@ -1,4 +1,4 @@
import { SqlAdapter } from './d.js';
import { SqlAdapter } from "./d.js";
export class MysqlAdapter implements SqlAdapter {
date(columnName: string) {
@@ -1,4 +1,4 @@
import { SqlAdapter } from './d.js';
import { SqlAdapter } from "./d.js";
export class PostgresqlAdapter implements SqlAdapter {
date(columnName: string) {
@@ -1,4 +1,4 @@
import { SqlAdapter } from './d.js';
import { SqlAdapter } from "./d.js";
export class SqliteAdapter implements SqlAdapter {
date(columnName: string) {
@@ -1,22 +1,22 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity('cd_oauth_bound')
@Entity("cd_oauth_bound")
export class OauthBoundEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'user_id', comment: '用户id' })
@Column({ name: "user_id", comment: "用户id" })
userId: number;
@Column({ name: 'type', comment: '第三方类型' })
@Column({ name: "type", comment: "第三方类型" })
type: string; // oidc, wechat, github, gitee , qq , alipay
@Column({ name: 'open_id', comment: '第三方openid' })
@Column({ name: "open_id", comment: "第三方openid" })
openId: string;
@Column({ name: 'create_time',comment: '创建时间', default: () => 'CURRENT_TIMESTAMP',})
@Column({ name: "create_time", comment: "创建时间", default: () => "CURRENT_TIMESTAMP" })
createTime: Date;
@Column({ name: 'update_time', comment: '修改时间',default: () => 'CURRENT_TIMESTAMP',})
@Column({ name: "update_time", comment: "修改时间", default: () => "CURRENT_TIMESTAMP" })
updateTime: Date;
}
@@ -1,35 +1,34 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity('sys_passkey')
@Entity("sys_passkey")
export class PasskeyEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'user_id', comment: '用户id' })
@Column({ name: "user_id", comment: "用户id" })
userId: number;
@Column({ name: 'device_name', comment: '设备名称' })
@Column({ name: "device_name", comment: "设备名称" })
deviceName: string;
@Column({ name: 'passkey_id', comment: 'passkey_id' })
@Column({ name: "passkey_id", comment: "passkey_id" })
passkeyId: string;
@Column({ name: 'public_key', comment: '公钥', type: 'text' })
@Column({ name: "public_key", comment: "公钥", type: "text" })
publicKey: string;
@Column({ name: 'counter', comment: '计数器' })
@Column({ name: "counter", comment: "计数器" })
counter: number;
@Column({ name: 'transports', comment: '传输方式', type: 'text', nullable: true })
@Column({ name: "transports", comment: "传输方式", type: "text", nullable: true })
transports: string;
@Column({ name: 'registered_at', comment: '注册时间' })
@Column({ name: "registered_at", comment: "注册时间" })
registeredAt: number;
@Column({ name: 'create_time', comment: '创建时间', default: () => 'CURRENT_TIMESTAMP' })
@Column({ name: "create_time", comment: "创建时间", default: () => "CURRENT_TIMESTAMP" })
createTime: Date;
@Column({ name: 'update_time', comment: '修改时间', default: () => 'CURRENT_TIMESTAMP' })
@Column({ name: "update_time", comment: "修改时间", default: () => "CURRENT_TIMESTAMP" })
updateTime: Date;
}
@@ -1,13 +1,7 @@
import { Config, Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { UserService } from "../../sys/authority/service/user-service.js";
import jwt from "jsonwebtoken";
import {
AuthException,
CommonException,
Need2FAException,
SysPrivateSettings,
SysSettingsService
} from "@certd/lib-server";
import { AuthException, CommonException, Need2FAException, SysPrivateSettings, SysSettingsService } from "@certd/lib-server";
import { RoleService } from "../../sys/authority/service/role-service.js";
import { UserEntity } from "../../sys/authority/entity/user.js";
import { cache, utils } from "@certd/basic";
@@ -24,7 +18,7 @@ import { InviteService } from "@certd/commercial-core";
/**
*/
@Provide()
@Scope(ScopeEnum.Request, {allowDowngrade: true})
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class LoginService {
@Inject()
userService: UserService;
@@ -33,7 +27,7 @@ export class LoginService {
@Inject()
codeService: CodeService;
@Config('auth.jwt')
@Config("auth.jwt")
private jwt: any;
@Inject()
@@ -57,7 +51,7 @@ export class LoginService {
const blockDurationKey = `login_block_duration:${username}`;
const value = cache.get(blockDurationKey);
if (value) {
const ttl = cache.getRemainingTTL(blockDurationKey)
const ttl = cache.getRemainingTTL(blockDurationKey);
const leftMin = Math.ceil(ttl / 1000 / 60);
throw new CommonException(`账号被锁定,请${leftMin}分钟后重试`);
}
@@ -102,7 +96,7 @@ export class LoginService {
const leftMin = Math.ceil(ttl / 1000 / 60);
cache.set(blockDurationKey, 1, {
ttl: ttl,
})
});
// 清除error次数
cache.delete(errorTimesKey);
throw new LoginErrorException(`登录失败次数过多,请${leftMin}分钟后重试`, 0);
@@ -114,10 +108,8 @@ export class LoginService {
throw new LoginErrorException(errorMessage, leftTimes);
}
async loginBySmsCode(req: { mobile: string; phoneCode: string; smsCode: string; randomStr: string; inviteCode?: string }) {
this.checkIsBlocked(req.mobile)
this.checkIsBlocked(req.mobile);
const smsChecked = await this.codeService.checkSmsCode({
mobile: req.mobile,
@@ -126,19 +118,19 @@ export class LoginService {
throwError: false,
});
const {mobile, phoneCode} = req;
const { mobile, phoneCode } = req;
if (!smsChecked) {
this.addErrorTimes(mobile, '手机验证码错误');
this.addErrorTimes(mobile, "手机验证码错误");
}
let info = await this.userService.findOne({phoneCode, mobile: mobile});
let info = await this.userService.findOne({ phoneCode, mobile: mobile });
if (info == null) {
//用户不存在,注册
const registerUser = {
phoneCode,
mobile,
password: '',
password: "",
} as any;
info = await this.userService.register('mobile', registerUser, async txManager => {
info = await this.userService.register("mobile", registerUser, async txManager => {
await this.inviteService.bindInvitee({ manager: txManager }, { inviteeUserId: registerUser.id, inviteCode: req.inviteCode });
});
}
@@ -147,18 +139,22 @@ export class LoginService {
}
async loginByPassword(req: { username: string; password: string; phoneCode: string }) {
this.checkIsBlocked(req.username)
const {username, password, phoneCode} = req;
const info = await this.userService.findOne([{username: username}, {email: username}, {
phoneCode,
mobile: username
}]);
this.checkIsBlocked(req.username);
const { username, password, phoneCode } = req;
const info = await this.userService.findOne([
{ username: username },
{ email: username },
{
phoneCode,
mobile: username,
},
]);
if (info == null) {
throw new CommonException('用户名或密码错误');
throw new CommonException("用户名或密码错误");
}
const right = await this.userService.checkPassword(password, info.password, info.passwordVersion);
if (!right) {
this.addErrorTimes(username, '用户名或密码错误');
this.addErrorTimes(username, "用户名或密码错误");
}
this.clearCacheOnSuccess(username);
return this.onLoginSuccess(info);
@@ -167,56 +163,54 @@ export class LoginService {
async checkTwoFactorEnabled(userId: number) {
//检查是否开启多重认证
if (!isPlus()) {
return true
return true;
}
const twoFactorSetting = await this.twoFactorService.getSetting(userId)
const twoFactorSetting = await this.twoFactorService.getSetting(userId);
const authenticatorSetting = twoFactorSetting.authenticator
const authenticatorSetting = twoFactorSetting.authenticator;
if (authenticatorSetting.enabled) {
//要检查
const randomKey = utils.id.simpleNanoId(12)
const randomKey = utils.id.simpleNanoId(12);
cache.set(`login_2fa_code:${randomKey}`, userId, {
ttl: 60 * 1000 * 2,
})
throw new Need2FAException('已开启多重认证,请在2分钟内输入OPT验证码',randomKey)
});
throw new Need2FAException("已开启多重认证,请在2分钟内输入OPT验证码", randomKey);
}
}
async loginByTwoFactor(req: { loginId: string; verifyCode: string }) {
//检查是否开启多重认证
if (!isPlus()) {
throw new Error('本功能需要开通Certd专业版')
throw new Error("本功能需要开通Certd专业版");
}
const userId = cache.get(`login_2fa_code:${req.loginId}`)
const userId = cache.get(`login_2fa_code:${req.loginId}`);
if (!userId) {
throw new AuthException('已超时,请返回重新登录')
throw new AuthException("已超时,请返回重新登录");
}
await this.twoFactorService.verifyAuthenticatorCode(userId, req.verifyCode)
await this.twoFactorService.verifyAuthenticatorCode(userId, req.verifyCode);
const user = await this.userService.info(userId);
if (!user) {
throw new AuthException('用户不存在')
throw new AuthException("用户不存在");
}
return this.generateToken(user)
return this.generateToken(user);
}
private async onLoginSuccess(info: UserEntity) {
if (info.status === 0) {
throw new CommonException('用户已被禁用');
throw new CommonException("用户已被禁用");
}
await this.checkTwoFactorEnabled(info.id)
await this.checkTwoFactorEnabled(info.id);
return this.generateToken(info);
}
writeTokenCookie(ctx:any,token: { expire: any; token: any }) {
writeTokenCookie(ctx: any, token: { expire: any; token: any }) {
ctx.cookies.set("certd_token", token.token, {
maxAge: 1000 * token.expire
maxAge: 1000 * token.expire,
});
}
/**
* 生成token
* @param user 用户对象
@@ -224,7 +218,7 @@ export class LoginService {
*/
async generateToken(user: UserEntity) {
if (user.status === 0) {
throw new CommonException('用户已被禁用');
throw new CommonException("用户已被禁用");
}
const roleIds = await this.roleService.getRoleIdsByUserId(user.id);
@@ -248,26 +242,26 @@ export class LoginService {
};
}
async loginByOpenId(req: { openId: string, type:string }) {
const {openId, type} = req;
async loginByOpenId(req: { openId: string; type: string }) {
const { openId, type } = req;
const oauthBound = await this.oauthBoundService.findOne({
where:{openId, type}
where: { openId, type },
});
if (oauthBound == null) {
return null
return null;
}
const info = await this.userService.findOne({id: oauthBound.userId});
const info = await this.userService.findOne({ id: oauthBound.userId });
if (info == null) {
// 用户已被删除,删除此oauth绑定
await this.oauthBoundService.delete([oauthBound.id]);
return null
return null;
}
return this.generateToken(info);
}
async loginByPasskey(req: { credential: any; challenge: string }, ctx: any) {
const {credential, challenge} = req;
const { credential, challenge } = req;
const user = await this.passkeyService.loginByPasskey(credential, challenge, ctx);
return this.generateToken(user);
}}
}
}
@@ -4,27 +4,24 @@ import { InjectEntityModel } from "@midwayjs/typeorm";
import { Repository } from "typeorm";
import { OauthBoundEntity } from "../entity/oauth-bound.js";
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class OauthBoundService extends BaseService<OauthBoundEntity> {
@InjectEntityModel(OauthBoundEntity)
repository: Repository<OauthBoundEntity>;
@Inject()
sysSettingsService: SysSettingsService;
//@ts-ignore
getRepository() {
return this.repository;
}
async unbind(req: { userId: any; type: any; }) {
async unbind(req: { userId: any; type: any }) {
const { userId, type } = req;
if (!userId || !type) {
throw new Error('参数错误');
throw new Error("参数错误");
}
await this.repository.delete({
@@ -33,10 +30,10 @@ export class OauthBoundService extends BaseService<OauthBoundEntity> {
});
}
async bind(req: { userId: any; type: any; openId: any; }) {
async bind(req: { userId: any; type: any; openId: any }) {
const { userId, type, openId } = req;
if (!userId || !type || !openId) {
throw new Error('参数错误');
throw new Error("参数错误");
}
const exist = await this.repository.findOne({
where: {
@@ -44,11 +41,11 @@ export class OauthBoundService extends BaseService<OauthBoundEntity> {
type,
},
});
if (exist ) {
if(exist.userId === userId){
if (exist) {
if (exist.userId === userId) {
return;
}
throw new Error('该第三方账号已绑定其他用户');
throw new Error("该第三方账号已绑定其他用户");
}
const exist2 = await this.repository.findOne({
@@ -65,7 +62,7 @@ export class OauthBoundService extends BaseService<OauthBoundEntity> {
openId,
});
return;
}
}
//新增
await this.add({
userId,
@@ -73,5 +70,4 @@ export class OauthBoundService extends BaseService<OauthBoundEntity> {
openId,
});
}
}
@@ -10,7 +10,6 @@ import { PasskeyEntity } from "../entity/passkey.js";
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class PasskeyService extends BaseService<PasskeyEntity> {
@Inject()
userService: UserService;
@@ -25,7 +24,7 @@ export class PasskeyService extends BaseService<PasskeyEntity> {
}
async getRpInfo() {
let rpName = "Certd"
let rpName = "Certd";
if (isComm()) {
const siteInfo = await this.sysSettingsService.getSetting<SysSiteInfo>(SysSiteInfo);
rpName = siteInfo.title || rpName;
@@ -42,7 +41,7 @@ export class PasskeyService extends BaseService<PasskeyEntity> {
rpName,
rpId,
origin,
}
};
}
async generateRegistrationOptions(userId: number, username: string, remoteIp: string, ctx: any) {
const { generateRegistrationOptions } = await import("@simplewebauthn/server");
@@ -50,7 +49,6 @@ export class PasskeyService extends BaseService<PasskeyEntity> {
const { rpName, rpId } = await this.getRpInfo();
const options = await generateRegistrationOptions({
rpName: rpName,
rpID: rpId,
@@ -60,15 +58,15 @@ export class PasskeyService extends BaseService<PasskeyEntity> {
timeout: 60000,
attestationType: "none",
excludeCredentials: [],
preferredAuthenticatorType: 'localDevice',
preferredAuthenticatorType: "localDevice",
authenticatorSelection: {
authenticatorAttachment: "cross-platform",
userVerification: "preferred",
authenticatorAttachment: "cross-platform",
userVerification: "preferred",
residentKey: "preferred",
requireResidentKey: false
requireResidentKey: false,
},
});
logger.info('[passkey] 注册选项:', JSON.stringify(options));
logger.info("[passkey] 注册选项:", JSON.stringify(options));
cache.set(`passkey:registration:${options.challenge}`, userId, {
ttl: 5 * 60 * 1000,
});
@@ -78,12 +76,7 @@ export class PasskeyService extends BaseService<PasskeyEntity> {
};
}
async verifyRegistrationResponse(
userId: number,
response: any,
challenge: string,
ctx: any
) {
async verifyRegistrationResponse(userId: number, response: any, challenge: string, ctx: any) {
const { verifyRegistrationResponse } = await import("@simplewebauthn/server");
const storedUserId = cache.get(`passkey:registration:${challenge}`);
@@ -105,13 +98,12 @@ export class PasskeyService extends BaseService<PasskeyEntity> {
verification = await verifyRegistrationResponse(verifyReq);
} catch (error) {
// 后端验证时
logger.error('[passkey] 注册验证失败:', JSON.stringify(verifyReq));
logger.error("[passkey] 注册验证失败:", JSON.stringify(verifyReq));
throw new AuthException(`注册验证失败:${error.message || error}`);
}
if (!verification.verified) {
throw new AuthException("注册验证失败");
}
cache.delete(`passkey:registration:${challenge}`);
@@ -129,7 +121,7 @@ export class PasskeyService extends BaseService<PasskeyEntity> {
rpID: rpId,
timeout: 60000,
allowCredentials: [],
userVerification: 'preferred' //'required' | 'preferred' | 'discouraged';
userVerification: "preferred", //'required' | 'preferred' | 'discouraged';
});
// cache.set(`passkey:authentication:${options.challenge}`, userId, {
@@ -141,11 +133,7 @@ export class PasskeyService extends BaseService<PasskeyEntity> {
};
}
async verifyAuthenticationResponse(
credential: any,
challenge: string,
ctx: any
) {
async verifyAuthenticationResponse(credential: any, challenge: string, ctx: any) {
const { verifyAuthenticationResponse } = await import("@simplewebauthn/server");
const passkey = await this.repository.findOne({
@@ -168,7 +156,7 @@ export class PasskeyService extends BaseService<PasskeyEntity> {
requireUserVerification: false,
credential: {
id: passkey.passkeyId,
publicKey: new Uint8Array(Buffer.from(passkey.publicKey, 'base64')),
publicKey: new Uint8Array(Buffer.from(passkey.publicKey, "base64")),
counter: passkey.counter,
transports: passkey.transports as any,
},
@@ -187,24 +175,13 @@ export class PasskeyService extends BaseService<PasskeyEntity> {
};
}
async registerPasskey(
userId: number,
response: any,
challenge: string,
deviceName: string,
ctx: any
) {
const verification = await this.verifyRegistrationResponse(
userId,
response,
challenge,
ctx
);
async registerPasskey(userId: number, response: any, challenge: string, deviceName: string, ctx: any) {
const verification = await this.verifyRegistrationResponse(userId, response, challenge, ctx);
await this.add({
userId,
passkeyId: verification.credentialId,
publicKey: Buffer.from(verification.credentialPublicKey).toString('base64'),
publicKey: Buffer.from(verification.credentialPublicKey).toString("base64"),
counter: verification.counter,
deviceName,
registeredAt: Date.now(),
@@ -214,11 +191,7 @@ export class PasskeyService extends BaseService<PasskeyEntity> {
}
async loginByPasskey(credential: any, challenge: string, ctx: any) {
const verification = await this.verifyAuthenticationResponse(
credential,
challenge,
ctx
);
const verification = await this.verifyAuthenticationResponse(credential, challenge, ctx);
const passkey = await this.repository.findOne({
where: {
@@ -1,34 +1,33 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
/**
*/
@Entity('user_settings')
@Entity("user_settings")
export class UserSettingsEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'user_id', comment: '用户id' })
@Column({ name: "user_id", comment: "用户id" })
userId: number;
@Column({ comment: 'key', length: 100 })
@Column({ comment: "key", length: 100 })
key: string;
@Column({ comment: '名称', length: 100 })
@Column({ comment: "名称", length: 100 })
title: string;
@Column({ name: 'setting', comment: '设置', length: 1024, nullable: true })
@Column({ name: "setting", comment: "设置", length: 1024, nullable: true })
setting: string;
@Column({ name: 'project_id', comment: '项目Id' })
@Column({ name: "project_id", comment: "项目Id" })
projectId: number;
@Column({
name: 'create_time',
comment: '创建时间',
default: () => 'CURRENT_TIMESTAMP',
name: "create_time",
comment: "创建时间",
default: () => "CURRENT_TIMESTAMP",
})
createTime: Date;
@Column({
name: 'update_time',
comment: '修改时间',
default: () => 'CURRENT_TIMESTAMP',
name: "update_time",
comment: "修改时间",
default: () => "CURRENT_TIMESTAMP",
})
updateTime: Date;
}
@@ -4,65 +4,57 @@ export type TwoFactorAuthenticator = {
enabled: boolean;
secret?: string;
type?: string;
verified?:boolean;
}
verified?: boolean;
};
export class UserTwoFactorSetting extends BaseSettings {
static __title__ = "用户多重认证设置";
static __key__ = "user.two.factor";
authenticator: TwoFactorAuthenticator = {
enabled:false,
verified:false,
enabled: false,
verified: false,
};
}
export class UserSiteMonitorSetting extends BaseSettings {
static __title__ = "站点监控设置";
static __key__ = "user.site.monitor";
notificationId?:number= 0;
cron?:string = undefined;
retryTimes?:number = 3;
dnsServer?:string[] = undefined;
certValidDays?:number = 14;
notificationId?: number = 0;
cron?: string = undefined;
retryTimes?: number = 3;
dnsServer?: string[] = undefined;
certValidDays?: number = 14;
}
export class UserDomainMonitorSetting extends BaseSettings {
static __title__ = "域名到期监控设置";
static __key__ = "user.domain.monitor";
enabled?:boolean = false;
notificationId?:number= 0;
cron?:string = undefined;
willExpireDays?:number = 30;
enabled?: boolean = false;
notificationId?: number = 0;
cron?: string = undefined;
willExpireDays?: number = 30;
}
export class UserEmailSetting extends BaseSettings {
static __title__ = "用户邮箱设置";
static __key__ = "user.email";
list:string[] = [];
list: string[] = [];
}
export class UserGrantSetting extends BaseSettings {
static __title__ = "用户授权设置";
static __key__ = "user.grant";
allowAdminViewCerts:boolean = false;
allowAdminViewCerts = false;
}
export class UserDomainImportSetting extends BaseSettings {
static __title__ = "用户域名导入设置";
static __key__ = "user.domain.import";
domainImportList:{dnsProviderType:string,dnsProviderAccessId:number,key:string,title:string,icon?:string}[];
domainImportList: { dnsProviderType: string; dnsProviderAccessId: number; key: string; title: string; icon?: string }[];
}
@@ -14,15 +14,14 @@ export class TwoFactorService {
@Inject()
userService: UserService;
async getAuthenticatorQrCode(userId: any) {
const setting = await this.getSetting(userId)
const setting = await this.getSetting(userId);
const authenticatorSetting = setting.authenticator;
if (!authenticatorSetting.secret) {
const { authenticator } = await import("otplib");
const { authenticator } = await import("otplib");
authenticatorSetting.secret = authenticator.generateSecret()
authenticatorSetting.secret = authenticator.generateSecret();
await this.userSettingsService.saveSetting(userId, null, setting);
}
@@ -34,14 +33,13 @@ export class TwoFactorService {
//生成qrcode base64
const qrcode = await import("qrcode");
const qrcodeBase64 = await qrcode.toDataURL(qrcodeContent);
return {qrcode:qrcodeBase64,link:qrcodeContent,secret}
return { qrcode: qrcodeBase64, link: qrcodeContent, secret };
}
async saveAuthenticator(req: { userId: any; verifyCode: any }) {
const userId = req.userId;
const { authenticator } = await import("otplib");
const setting = await this.getSetting(userId)
const setting = await this.getSetting(userId);
const authenticatorSetting = setting.authenticator;
if (!authenticatorSetting.secret) {
@@ -62,26 +60,25 @@ export class TwoFactorService {
await this.userSettingsService.saveSetting(userId, null, setting);
}
async offAuthenticator(userId:number) {
async offAuthenticator(userId: number) {
if (!userId || userId <= 0) {
throw new Error("userId is required");
}
const setting = await this.getSetting(userId)
const setting = await this.getSetting(userId);
setting.authenticator.enabled = false;
setting.authenticator.verified = false;
setting.authenticator.secret = '';
setting.authenticator.secret = "";
await this.userSettingsService.saveSetting(userId, null, setting);
}
async getSetting(userId:number) {
async getSetting(userId: number) {
return await this.userSettingsService.getSetting<UserTwoFactorSetting>(userId, null, UserTwoFactorSetting);
}
async verifyAuthenticatorCode(userId: any, verifyCode: string) {
const { authenticator } = await import("otplib");
const setting = await this.getSetting(userId)
const setting = await this.getSetting(userId);
if (!setting.authenticator.enabled) {
throw new Error("authenticator 未开启");
}
@@ -4,7 +4,7 @@ import { Repository } from "typeorm";
import { BaseService, BaseSettings } from "@certd/lib-server";
import { UserSettingsEntity } from "../entity/user-settings.js";
import { LocalCache, mergeUtils } from "@certd/basic";
const {merge} = mergeUtils
const { merge } = mergeUtils;
const UserSettingCache = new LocalCache({
clearInterval: 5 * 60 * 1000,
@@ -33,13 +33,13 @@ export class UserSettingsService extends BaseService<UserSettingsEntity> {
const setting = JSON.parse(entity.setting);
return {
id: entity.id,
...setting
...setting,
};
}
async getByKey(key: string, userId: number, projectId: number): Promise<UserSettingsEntity | null> {
if(userId == null){
throw new Error('userId is required');
if (userId == null) {
throw new Error("userId is required");
}
if (!key) {
return null;
@@ -48,14 +48,14 @@ export class UserSettingsService extends BaseService<UserSettingsEntity> {
where: {
key,
userId,
projectId
}
projectId,
},
});
}
async getSettingByKey(key: string, userId: number, projectId: number): Promise<any | null> {
if(userId == null){
throw new Error('userId is required');
if (userId == null) {
throw new Error("userId is required");
}
const entity = await this.getByKey(key, userId, projectId);
if (!entity) {
@@ -69,8 +69,8 @@ export class UserSettingsService extends BaseService<UserSettingsEntity> {
where: {
key: bean.key,
userId: bean.userId,
projectId: bean.projectId
}
projectId: bean.projectId,
},
});
if (entity) {
entity.setting = bean.setting;
@@ -81,15 +81,14 @@ export class UserSettingsService extends BaseService<UserSettingsEntity> {
}
}
async getSetting<T>( userId: number, projectId: number,type: any, cache:boolean = false): Promise<T> {
if(userId==null){
throw new Error('userId is required');
async getSetting<T>(userId: number, projectId: number, type: any, cache = false): Promise<T> {
if (userId == null) {
throw new Error("userId is required");
}
const key = type.__key__;
let cacheKey = key + '_' + userId ;
let cacheKey = key + "_" + userId;
if (projectId) {
cacheKey += '_' + projectId;
cacheKey += "_" + projectId;
}
if (cache) {
@@ -109,23 +108,23 @@ export class UserSettingsService extends BaseService<UserSettingsEntity> {
return newSetting;
}
async saveSetting<T extends BaseSettings>(userId:number, projectId: number,bean: T) {
if(userId == null){
throw new Error('userId is required');
async saveSetting<T extends BaseSettings>(userId: number, projectId: number, bean: T) {
if (userId == null) {
throw new Error("userId is required");
}
const old = await this.getSetting(userId, projectId,bean.constructor)
bean = merge(old,bean)
const old = await this.getSetting(userId, projectId, bean.constructor);
bean = merge(old, bean);
const type: any = bean.constructor;
const key = type.__key__;
if(!key){
if (!key) {
throw new Error(`${type.name} must have __key__`);
}
const entity = await this.getByKey(key,userId, projectId);
const entity = await this.getByKey(key, userId, projectId);
const newEntity = new UserSettingsEntity();
if (entity) {
newEntity.id = entity.id;
}else{
} else {
newEntity.key = key;
newEntity.title = type.__title__;
newEntity.userId = userId;
@@ -134,5 +133,4 @@ export class UserSettingsService extends BaseService<UserSettingsEntity> {
newEntity.setting = JSON.stringify(bean);
await this.repository.save(newEntity);
}
}
@@ -1,64 +1,64 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { PipelineEntity } from '../../pipeline/entity/pipeline.js';
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
import { PipelineEntity } from "../../pipeline/entity/pipeline.js";
@Entity('cd_cert_info')
@Entity("cd_cert_info")
export class CertInfoEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'user_id', comment: '用户id' })
@Column({ name: "user_id", comment: "用户id" })
userId: number;
@Column({ name: 'domain', comment: '主域名' })
@Column({ name: "domain", comment: "主域名" })
domain: string;
@Column({ name: 'domains', comment: '域名' })
@Column({ name: "domains", comment: "域名" })
domains: string;
@Column({ name: 'domain_count', comment: '域名数量' })
@Column({ name: "domain_count", comment: "域名数量" })
domainCount: number;
@Column({ name: 'wildcard_domain_count', comment: '泛域名数量', default: 0 })
@Column({ name: "wildcard_domain_count", comment: "泛域名数量", default: 0 })
wildcardDomainCount: number;
@Column({ name: 'pipeline_id', comment: '关联流水线id' })
@Column({ name: "pipeline_id", comment: "关联流水线id" })
pipelineId: number;
@Column({ name: 'apply_time', comment: '申请时间' })
@Column({ name: "apply_time", comment: "申请时间" })
applyTime: number;
@Column({ name: 'from_type', comment: '来源' })
@Column({ name: "from_type", comment: "来源" })
fromType: string;
@Column({ name: 'cert_provider', comment: '证书颁发机构' })
@Column({ name: "cert_provider", comment: "证书颁发机构" })
certProvider: string;
@Column({ name: 'effective_time', comment: '生效时间' })
@Column({ name: "effective_time", comment: "生效时间" })
effectiveTime: number;
@Column({ name: 'expires_time', comment: '过期时间' })
@Column({ name: "expires_time", comment: "过期时间" })
expiresTime: number;
@Column({ name: 'cert_info', comment: '证书详情' })
@Column({ name: "cert_info", comment: "证书详情" })
certInfo: string;
@Column({ name: 'cert_file', comment: '证书下载' })
@Column({ name: "cert_file", comment: "证书下载" })
certFile: string;
@Column({ name: 'project_id', comment: '项目id' })
@Column({ name: "project_id", comment: "项目id" })
projectId: number;
@Column({
name: 'create_time',
comment: '创建时间',
default: () => 'CURRENT_TIMESTAMP',
name: "create_time",
comment: "创建时间",
default: () => "CURRENT_TIMESTAMP",
})
createTime: Date;
@Column({
name: 'update_time',
comment: '修改时间',
default: () => 'CURRENT_TIMESTAMP',
name: "update_time",
comment: "修改时间",
default: () => "CURRENT_TIMESTAMP",
})
updateTime: Date;
@@ -1,50 +1,49 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { PipelineEntity } from '../../pipeline/entity/pipeline.js';
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
import { PipelineEntity } from "../../pipeline/entity/pipeline.js";
@Entity('cd_job_history')
@Entity("cd_job_history")
export class JobHistoryEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'user_id', comment: '用户id' })
@Column({ name: "user_id", comment: "用户id" })
userId: number;
@Column({ name: 'project_id', comment: '项目id' })
@Column({ name: "project_id", comment: "项目id" })
projectId: number;
@Column({ name: 'type', comment: '类型' })
@Column({ name: "type", comment: "类型" })
type: string;
@Column({ name: 'title', comment: '标题' })
@Column({ name: "title", comment: "标题" })
title: string;
@Column({ name: 'content', comment: '内容' })
@Column({ name: "content", comment: "内容" })
content: string;
@Column({ name: 'related_id', comment: '关联id' })
@Column({ name: "related_id", comment: "关联id" })
relatedId: string;
@Column({ name: 'result', comment: '结果' })
@Column({ name: "result", comment: "结果" })
result: string;
@Column({ name: 'start_at', comment: '开始时间' })
@Column({ name: "start_at", comment: "开始时间" })
startAt: number;
@Column({ name: 'end_at', comment: '结束时间' })
@Column({ name: "end_at", comment: "结束时间" })
endAt: number;
@Column({
name: 'create_time',
comment: '创建时间',
default: () => 'CURRENT_TIMESTAMP',
name: "create_time",
comment: "创建时间",
default: () => "CURRENT_TIMESTAMP",
})
createTime: Date;
@Column({
name: 'update_time',
comment: '修改时间',
default: () => 'CURRENT_TIMESTAMP',
name: "update_time",
comment: "修改时间",
default: () => "CURRENT_TIMESTAMP",
})
updateTime: Date;
@@ -1,84 +1,82 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
/**
*/
@Entity('cd_site_info')
@Entity("cd_site_info")
export class SiteInfoEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'user_id', comment: '用户id' })
@Column({ name: "user_id", comment: "用户id" })
userId: number;
@Column({ name: 'name', comment: '站点名称', length: 100 })
@Column({ name: "name", comment: "站点名称", length: 100 })
name: string;
@Column({ name: 'domain', comment: '域名', length: 100 })
@Column({ name: "domain", comment: "域名", length: 100 })
domain: string;
@Column({ name: 'https_port', comment: '端口' })
@Column({ name: "https_port", comment: "端口" })
httpsPort: number;
@Column({ name: 'cert_domains', comment: '证书域名', length: 4096 })
@Column({ name: "cert_domains", comment: "证书域名", length: 4096 })
certDomains: string;
@Column({ name: 'cert_info', comment: '证书详情', length: 4096 })
@Column({ name: "cert_info", comment: "证书详情", length: 4096 })
certInfo: string;
@Column({ name: 'cert_status', comment: '证书状态', length: 100 })
@Column({ name: "cert_status", comment: "证书状态", length: 100 })
certStatus: string;
@Column({ name: 'cert_provider', comment: '证书颁发机构', length: 100 })
@Column({ name: "cert_provider", comment: "证书颁发机构", length: 100 })
certProvider: string;
@Column({ name: 'cert_effective_time', comment: '证书生效时间' })
@Column({ name: "cert_effective_time", comment: "证书生效时间" })
certEffectiveTime: number;
@Column({ name: 'cert_expires_time', comment: '证书到期时间' })
@Column({ name: "cert_expires_time", comment: "证书到期时间" })
certExpiresTime: number;
@Column({ name: 'last_check_time', comment: '上次检查时间' })
@Column({ name: "last_check_time", comment: "上次检查时间" })
lastCheckTime: number;
@Column({ name: 'check_status', comment: '检查状态' })
@Column({ name: "check_status", comment: "检查状态" })
checkStatus: string;
@Column({ name: 'error', comment: '错误信息' })
@Column({ name: "error", comment: "错误信息" })
error: string;
@Column({ name: 'pipeline_id', comment: '关联流水线id' })
@Column({ name: "pipeline_id", comment: "关联流水线id" })
pipelineId: number;
@Column({ name: 'cert_info_id', comment: '证书id' })
@Column({ name: "cert_info_id", comment: "证书id" })
certInfoId: number;
@Column({ name: 'ip_check', comment: '是否检查IP' })
@Column({ name: "ip_check", comment: "是否检查IP" })
ipCheck: boolean;
@Column({ name: 'ip_sync_auto', comment: '是否自动同步IP' })
@Column({ name: "ip_sync_auto", comment: "是否自动同步IP" })
ipSyncAuto: boolean;
@Column({ name: 'ip_sync_mode', comment: 'IP同步模式' })
@Column({ name: "ip_sync_mode", comment: "IP同步模式" })
ipSyncMode: string;
@Column({ name: 'ip_ignore_coherence', comment: '忽略证书一致性' })
@Column({ name: "ip_ignore_coherence", comment: "忽略证书一致性" })
ipIgnoreCoherence: boolean;
@Column({ name: 'ip_count', comment: 'ip数量' })
ipCount: number
@Column({ name: "ip_count", comment: "ip数量" })
ipCount: number;
@Column({ name: 'ip_error_count', comment: 'ip异常数量' })
ipErrorCount: number
@Column({ name: "ip_error_count", comment: "ip异常数量" })
ipErrorCount: number;
@Column({ name: 'disabled', comment: '禁用启用' })
@Column({ name: "disabled", comment: "禁用启用" })
disabled: boolean;
@Column({ name: 'remark', comment: '备注', length: 512 })
@Column({ name: "remark", comment: "备注", length: 512 })
remark: string;
@Column({ name: 'group_id', comment: '分组id' })
@Column({ name: "group_id", comment: "分组id" })
groupId: number;
@Column({ name: 'ip_address', comment: 'IP地址', length: 128 })
@Column({ name: "ip_address", comment: "IP地址", length: 128 })
ipAddress: string;
@Column({ name: 'project_id', comment: '项目id' })
@Column({ name: "project_id", comment: "项目id" })
projectId: number;
@Column({ name: 'create_time', comment: '创建时间', default: () => 'CURRENT_TIMESTAMP' })
@Column({ name: "create_time", comment: "创建时间", default: () => "CURRENT_TIMESTAMP" })
createTime: Date;
@Column({ name: 'update_time', comment: '修改时间', default: () => 'CURRENT_TIMESTAMP' })
@Column({ name: "update_time", comment: "修改时间", default: () => "CURRENT_TIMESTAMP" })
updateTime: Date;
}
@@ -1,43 +1,43 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
/**
*/
@Entity('cd_site_ip')
@Entity("cd_site_ip")
export class SiteIpEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'user_id', comment: '用户id' })
@Column({ name: "user_id", comment: "用户id" })
userId: number;
@Column({ name: 'site_id', comment: '站点id' })
@Column({ name: "site_id", comment: "站点id" })
siteId: number;
@Column({ name: 'ip_address', comment: 'IP', length: 100 })
@Column({ name: "ip_address", comment: "IP", length: 100 })
ipAddress: string;
@Column({ name: 'cert_domains', comment: '证书域名', length: 4096 })
@Column({ name: "cert_domains", comment: "证书域名", length: 4096 })
certDomains: string;
@Column({ name: 'cert_status', comment: '证书状态', length: 100 })
@Column({ name: "cert_status", comment: "证书状态", length: 100 })
certStatus: string;
@Column({ name: 'cert_provider', comment: '证书颁发机构', length: 100 })
@Column({ name: "cert_provider", comment: "证书颁发机构", length: 100 })
certProvider: string;
@Column({ name: 'cert_expires_time', comment: '证书到期时间' })
@Column({ name: "cert_expires_time", comment: "证书到期时间" })
certExpiresTime: number;
@Column({ name: 'last_check_time', comment: '上次检查时间' })
@Column({ name: "last_check_time", comment: "上次检查时间" })
lastCheckTime: number;
@Column({ name: 'check_status', comment: '检查状态' })
@Column({ name: "check_status", comment: "检查状态" })
checkStatus: string;
@Column({ name: 'error', comment: '错误信息' })
@Column({ name: "error", comment: "错误信息" })
error: string;
@Column({ name: 'from', comment: '来源' })
from: string
@Column({ name: 'remark', comment: '备注' })
@Column({ name: "from", comment: "来源" })
from: string;
@Column({ name: "remark", comment: "备注" })
remark: string;
@Column({ name: "disabled", comment: "禁用启用" })
disabled: boolean;
@Column({ name: 'project_id', comment: '项目id' })
@Column({ name: "project_id", comment: "项目id" })
projectId: number;
@Column({ name: 'create_time', comment: '创建时间', default: () => 'CURRENT_TIMESTAMP' })
@Column({ name: "create_time", comment: "创建时间", default: () => "CURRENT_TIMESTAMP" })
createTime: Date;
@Column({ name: 'update_time', comment: '修改时间', default: () => 'CURRENT_TIMESTAMP' })
@Column({ name: "update_time", comment: "修改时间", default: () => "CURRENT_TIMESTAMP" })
updateTime: Date;
}
@@ -10,25 +10,23 @@ import { CertInfoService } from "../service/cert-info-service.js";
import { DomainService } from "../../cert/service/domain-service.js";
import { DomainVerifierGetter } from "../../pipeline/service/getter/domain-verifier-getter.js";
@Provide("CertInfoFacade")
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class CertInfoFacade {
export class CertInfoFacade {
@Inject()
pipelineService: PipelineService;
@Inject()
certInfoService: CertInfoService ;
certInfoService: CertInfoService;
@Inject()
domainService: DomainService
domainService: DomainService;
@Inject()
userSettingsService : UserSettingsService
userSettingsService: UserSettingsService;
async getCertInfo(req: { domains?: string; certId?: number; userId: number, projectId:number, autoApply?:boolean,format?:string }) {
const { domains, certId, userId,projectId } = req;
async getCertInfo(req: { domains?: string; certId?: number; userId: number; projectId: number; autoApply?: boolean; format?: string }) {
const { domains, certId, userId, projectId } = req;
if (certId) {
return await this.certInfoService.getCertInfoById({ id: certId, userId, projectId });
}
@@ -38,41 +36,38 @@ export class CertInfoFacade {
message: "参数错误,certId和domains必须传一个",
});
}
const domainArr = domains.split(',');
const domainArr = domains.split(",");
const matchedList = await this.certInfoService.getMatchCertList({domains:domainArr,userId,projectId})
const matchedList = await this.certInfoService.getMatchCertList({ domains: domainArr, userId, projectId });
if (matchedList.length === 0 ) {
if(req.autoApply === true){
if (matchedList.length === 0) {
if (req.autoApply === true) {
//自动申请,先创建自动申请流水线
const pipeline:PipelineEntity = await this.createAutoPipeline({domains:domainArr,userId,projectId})
await this.triggerApplyPipeline({pipelineId:pipeline.id})
}else{
const pipeline: PipelineEntity = await this.createAutoPipeline({ domains: domainArr, userId, projectId });
await this.triggerApplyPipeline({ pipelineId: pipeline.id });
} else {
throw new CodeException({
...Constants.res.openCertNotFound,
message:"在证书仓库中没有找到匹配域名的证书,请先创建证书流水线,或传入autoApply参数,自动创建"
message: "在证书仓库中没有找到匹配域名的证书,请先创建证书流水线,或传入autoApply参数,自动创建",
});
}
}
let matched = this.getMinixMatched(matchedList);
const matched = this.getMinixMatched(matchedList);
if (!matched) {
if(req.autoApply === true){
if (req.autoApply === true) {
//如果没有找到有效期内的证书,则自动触发一次申请
const first = matchedList[0]
await this.triggerApplyPipeline({pipelineId:first.pipelineId})
return
}else{
const first = matchedList[0];
await this.triggerApplyPipeline({ pipelineId: first.pipelineId });
return;
} else {
throw new CodeException({
...Constants.res.openCertNotFound,
message:"证书已过期,请触发流水线申请,或者传入autoApply参数,自动触发"
message: "证书已过期,请触发流水线申请,或者传入autoApply参数,自动触发",
});
}
}
return await this.certInfoService.getCertInfoById({ id: matched.id, userId: userId,projectId,format:req.format });
return await this.certInfoService.getCertInfoById({ id: matched.id, userId: userId, projectId, format: req.format });
}
public getMinixMatched(matchedList: CertInfoEntity[]) {
@@ -103,59 +98,55 @@ export class CertInfoFacade {
return matched;
}
async createAutoPipeline(req:{domains:string[],userId:number,projectId:number}){
async createAutoPipeline(req: { domains: string[]; userId: number; projectId: number }) {
const verifierGetter = new DomainVerifierGetter(req.userId, req.projectId, this.domainService);
const verifierGetter = new DomainVerifierGetter(req.userId, req.projectId, this.domainService)
const allDomains = []
const allDomains = [];
for (const item of req.domains) {
allDomains.push(item.replaceAll("*.",""))
allDomains.push(item.replaceAll("*.", ""));
}
const verifiers = await verifierGetter.getVerifiers(allDomains)
const verifiers = await verifierGetter.getVerifiers(allDomains);
for (const item of allDomains) {
if (!verifiers[item]){
if (!verifiers[item]) {
throw new CodeException({
...Constants.res.openDomainNoVerifier,
message:`域名${item}没有配置校验方式,请先在域名管理页面配置`,
data:{
domain:item
}
message: `域名${item}没有配置校验方式,请先在域名管理页面配置`,
data: {
domain: item,
},
});
}
}
const userEmailSetting = await this.userSettingsService.getSetting<UserEmailSetting>(req.userId,null, UserEmailSetting)
if(!userEmailSetting.list){
throw new CodeException(Constants.res.openEmailNotFound)
const userEmailSetting = await this.userSettingsService.getSetting<UserEmailSetting>(req.userId, null, UserEmailSetting);
if (!userEmailSetting.list) {
throw new CodeException(Constants.res.openEmailNotFound);
}
const email = userEmailSetting.list[0]
const email = userEmailSetting.list[0];
return await this.pipelineService.createAutoPipeline({
domains: req.domains,
email,
projectId: req.projectId,
userId: req.userId,
from: "OpenAPI"
})
}
async triggerApplyPipeline(req:{pipelineId:number}){
//查询流水线状态
const status = await this.pipelineService.getStatus(req.pipelineId)
if (status != 'running') {
await this.pipelineService.trigger(req.pipelineId)
await utils.sleep(1000)
}
const certInfo = await this.certInfoService.getByPipelineId(req.pipelineId)
throw new CodeException({
...Constants.res.openCertApplying,
data:{
pipelineId:req.pipelineId,
certId:certInfo?.id
}
from: "OpenAPI",
});
}
async triggerApplyPipeline(req: { pipelineId: number }) {
//查询流水线状态
const status = await this.pipelineService.getStatus(req.pipelineId);
if (status != "running") {
await this.pipelineService.trigger(req.pipelineId);
await utils.sleep(1000);
}
const certInfo = await this.certInfoService.getByPipelineId(req.pipelineId);
throw new CodeException({
...Constants.res.openCertApplying,
data: {
pipelineId: req.pipelineId,
certId: certInfo?.id,
},
});
}
}
@@ -1,5 +1,5 @@
export * from './entity/site-info.js';
export * from './entity/cert-info.js';
export * from "./entity/site-info.js";
export * from "./entity/cert-info.js";
export * from './service/cert-info-service.js';
export * from './service/site-info-service.js';
export * from "./service/cert-info-service.js";
export * from "./service/site-info-service.js";
@@ -12,10 +12,9 @@ export type UploadCertReq = {
fromType?: string;
userId?: number;
projectId?: number;
file?:any
file?: any;
};
@Provide("CertInfoService")
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class CertInfoService extends BaseService<CertInfoEntity> {
@@ -32,19 +31,19 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
}
async getUserDomainCount(userId: number) {
if (userId==null) {
throw new Error('userId is required');
if (userId == null) {
throw new Error("userId is required");
}
return await this.repository.sum('domainCount', {
return await this.repository.sum("domainCount", {
userId,
});
}
async getUserWildcardDomainCount(userId: number) {
if (userId == null) {
throw new Error('userId is required');
throw new Error("userId is required");
}
return await this.repository.sum('wildcardDomainCount', {
return await this.repository.sum("wildcardDomainCount", {
userId,
});
}
@@ -56,7 +55,7 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
return domains.filter(domain => domain?.trim().toLowerCase().startsWith("*.")).length;
}
async updateDomains(pipelineId: number, userId: number, projectId: number, domains: string[],fromType?:string) {
async updateDomains(pipelineId: number, userId: number, projectId: number, domains: string[], fromType?: string) {
const found = await this.repository.findOne({
where: {
pipelineId,
@@ -73,26 +72,26 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
bean.pipelineId = pipelineId;
bean.userId = userId;
bean.projectId = projectId;
bean.fromType = fromType
bean.fromType = fromType;
if (!domains || domains.length === 0) {
return;
}
}
if (!domains || domains.length === 0) {
bean.domain = '';
bean.domains = '';
bean.domain = "";
bean.domains = "";
bean.domainCount = 0;
bean.wildcardDomainCount = 0;
} else {
bean.domain = domains[0];
bean.domains = domains.join(',');
bean.domains = domains.join(",");
bean.domainCount = domains.length;
bean.wildcardDomainCount = this.countWildcardDomains(domains);
}
await this.addOrUpdate(bean);
return bean.id
return bean.id;
}
async deleteByPipelineId(id: number) {
@@ -104,12 +103,12 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
});
}
async getMatchCertList(params: { domains: string[]; userId: number,projectId?:number }) {
const { domains, userId,projectId } = params;
async getMatchCertList(params: { domains: string[]; userId: number; projectId?: number }) {
const { domains, userId, projectId } = params;
if (!domains) {
throw new CodeException({
...Constants.res.openCertNotFound,
message:"域名不能为空"
message: "域名不能为空",
});
}
@@ -117,27 +116,27 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
select: {
id: true,
domains: true,
expiresTime:true,
pipelineId:true,
expiresTime: true,
pipelineId: true,
},
where: {
userId,
projectId,
},
order: {
id: 'DESC',
id: "DESC",
},
});
//遍历查找
return list.filter(item => {
const itemDomains = item.domains.split(',');
const itemDomains = item.domains.split(",");
return utils.domain.match(domains, itemDomains);
});
}
async getCertInfoById(req: { id: number; userId: number,projectId:number,format?:string }) {
async getCertInfoById(req: { id: number; userId: number; projectId: number; format?: string }) {
const entity = await this.info(req.id);
if (!entity || entity.userId !== req.userId ) {
if (!entity || entity.userId !== req.userId) {
throw new CodeException(Constants.res.openCertNotFound);
}
if (req.projectId && entity.projectId !== req.projectId) {
@@ -153,13 +152,13 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
...certReader.toCertInfo(req.format),
detail: {
id: entity.id,
domains: entity.domains.split(','),
domains: entity.domains.split(","),
notAfter: certReader.expires,
},
};
}
async updateCertByPipelineId(pipelineId: number, cert: CertInfo,file?:string,fromType = 'pipeline') {
async updateCertByPipelineId(pipelineId: number, cert: CertInfo, file?: string, fromType = "pipeline") {
const found = await this.repository.findOne({
where: {
pipelineId,
@@ -169,14 +168,14 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
id: found?.id,
certReader: new CertReader(cert),
fromType,
file
file,
});
return bean;
}
private async updateCert(req: UploadCertReq) {
const bean = new CertInfoEntity();
const { id, fromType,userId, certReader } = req;
const { id, fromType, userId, certReader } = req;
if (id) {
bean.id = id;
} else {
@@ -186,7 +185,7 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
bean.certInfo = JSON.stringify(certInfo);
bean.applyTime = new Date().getTime();
const domains = certReader.detail.domains.altNames;
bean.domains = domains.join(',');
bean.domains = domains.join(",");
bean.domain = domains[0];
bean.domainCount = domains.length;
bean.wildcardDomainCount = this.countWildcardDomains(domains);
@@ -195,8 +194,8 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
bean.certProvider = certReader.detail.issuer.commonName;
bean.userId = userId;
bean.projectId = req.projectId;
if(req.file){
bean.certFile = req.file
if (req.file) {
bean.certFile = req.file;
}
await this.addOrUpdate(bean);
return bean;
@@ -210,7 +209,7 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
});
}
async count({ userId,projectId }: { userId: number,projectId?:number }) {
async count({ userId, projectId }: { userId: number; projectId?: number }) {
const total = await this.repository.count({
where: {
userId,
@@ -1,17 +1,17 @@
import {LocalCache, logger} from '@certd/basic';
import dnsSdk, {AnyRecord} from 'dns'
import {LookupAddress} from "node:dns";
import { LocalCache, logger } from "@certd/basic";
import dnsSdk, { AnyRecord } from "dns";
import { LookupAddress } from "node:dns";
const dns = dnsSdk.promises
const dns = dnsSdk.promises;
export class DnsCustom{
export class DnsCustom {
private resolver: dnsSdk.promises.Resolver;
// private cache = new LRUCache<string, any>({
// max: 1000,
// ttl: 1000 * 60 * 5,
// });
constructor(dnsServers:string[]) {
constructor(dnsServers: string[]) {
const resolver = new dns.Resolver();
resolver.setServers(dnsServers);
this.resolver = resolver;
@@ -27,103 +27,100 @@ export class DnsCustom{
// this.cache.set(cacheKey,res)
// return res
// }
async lookup(hostname:string,options?:{ family: any, hints: number, all: boolean }):Promise<LookupAddress[]>{
async lookup(hostname: string, options?: { family: any; hints: number; all: boolean }): Promise<LookupAddress[]> {
// { family: undefined, hints: 0, all: true }
let v4:LookupAddress[] = []
let v6:LookupAddress[] = []
let errors = []
const queryV6 = async ()=>{
try{
const list = await this.resolver.resolve6(hostname)
let v4: LookupAddress[] = [];
let v6: LookupAddress[] = [];
const errors = [];
const queryV6 = async () => {
try {
const list = await this.resolver.resolve6(hostname);
if (list && list.length > 0) {
v6 = list.map(item=>{
v6 = list.map(item => {
return {
address: item,
family: 6
}
})
family: 6,
};
});
}
}catch (e) {
logger.warn("query v6 error",e)
errors.push(e)
} catch (e) {
logger.warn("query v6 error", e);
errors.push(e);
}
}
const queryV4 = async ()=>{
try{
const list =await this.resolver.resolve4(hostname)
};
const queryV4 = async () => {
try {
const list = await this.resolver.resolve4(hostname);
if (list && list.length > 0) {
v4 = list.map(item=>{
v4 = list.map(item => {
return {
address: item,
family: 4
}
})
family: 4,
};
});
}
}catch (e) {
logger.warn("query v4 error",e)
errors.push(e)
} catch (e) {
logger.warn("query v4 error", e);
errors.push(e);
}
};
const queries: Promise<any>[] = [];
const { family, all } = options;
if (all) {
queries.push(queryV6());
queries.push(queryV4());
} else {
if (family === 6) {
queries.push(queryV6());
}
if (family === 4) {
queries.push(queryV4());
}
}
const queries:Promise<any>[] = []
const {family, all} = options
if (all){
queries.push(queryV6())
queries.push(queryV4())
}else{
if(family === 6 ){
queries.push(queryV6())
}
if(family === 4 ){
queries.push(queryV4())
}
}
await Promise.all(queries)
const res = [...v4,...v6]
if(res.length === 0){
if (errors.length > 0){
const e = new Error(errors[0])
await Promise.all(queries);
const res = [...v4, ...v6];
if (res.length === 0) {
if (errors.length > 0) {
const e = new Error(errors[0]);
// @ts-ignore
e.errors = errors
throw e
e.errors = errors;
throw e;
}
}
return res
return res;
}
async resolve4(hostname:string):Promise<string[]>{
return await this.resolver.resolve4(hostname)
async resolve4(hostname: string): Promise<string[]> {
return await this.resolver.resolve4(hostname);
}
async resolve6(hostname:string):Promise<string[]>{
return await this.resolver.resolve6(hostname)
async resolve6(hostname: string): Promise<string[]> {
return await this.resolver.resolve6(hostname);
}
async resolveAny(hostname:string):Promise<AnyRecord[]>{
return await this.resolver.resolveAny(hostname)
async resolveAny(hostname: string): Promise<AnyRecord[]> {
return await this.resolver.resolveAny(hostname);
}
async resolveCname(hostname:string):Promise<string[]>{
return await this.resolver.resolveCname(hostname)
async resolveCname(hostname: string): Promise<string[]> {
return await this.resolver.resolveCname(hostname);
}
}
export class DnsContainer{
bucket: LocalCache<DnsCustom> = new LocalCache()
export class DnsContainer {
bucket: LocalCache<DnsCustom> = new LocalCache();
constructor() {}
getDns(server:string[]){
const key = server.join(',')
let dns = this.bucket.get(key)
if (dns){
return dns
getDns(server: string[]) {
const key = server.join(",");
let dns = this.bucket.get(key);
if (dns) {
return dns;
}
dns = new DnsCustom(server)
this.bucket.set(key,dns)
return dns
dns = new DnsCustom(server);
this.bucket.set(key, dns);
return dns;
}
}
export const dnsContainer = new DnsContainer()
export const dnsContainer = new DnsContainer();
@@ -5,14 +5,12 @@ import { Repository } from "typeorm";
import { UserSettingsService } from "../../mine/service/user-settings-service.js";
import { JobHistoryEntity } from "../entity/job-history.js";
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class JobHistoryService extends BaseService<JobHistoryEntity> {
@InjectEntityModel(JobHistoryEntity)
repository: Repository<JobHistoryEntity>;
@Inject()
userSettingsService: UserSettingsService;
@@ -1,20 +1,20 @@
import {Inject, Provide, Scope, ScopeEnum} from "@midwayjs/core";
import {BaseService, Constants, isEnterprise, NeedSuiteException, NeedVIPException, SysSettingsService} from "@certd/lib-server";
import {InjectEntityModel} from "@midwayjs/typeorm";
import {In, Repository} from "typeorm";
import {SiteInfoEntity} from "../entity/site-info.js";
import {siteTester} from "./site-tester.js";
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { BaseService, Constants, isEnterprise, NeedSuiteException, NeedVIPException, SysSettingsService } from "@certd/lib-server";
import { InjectEntityModel } from "@midwayjs/typeorm";
import { In, Repository } from "typeorm";
import { SiteInfoEntity } from "../entity/site-info.js";
import { siteTester } from "./site-tester.js";
import dayjs from "dayjs";
import {logger, utils} from "@certd/basic";
import {PeerCertificate} from "tls";
import {NotificationService} from "../../pipeline/service/notification-service.js";
import {isComm, isPlus} from "@certd/plus-core";
import {UserSuiteService} from "@certd/commercial-core";
import {UserSettingsService} from "../../mine/service/user-settings-service.js";
import {UserSiteMonitorSetting} from "../../mine/service/models.js";
import {SiteIpService} from "./site-ip-service.js";
import {SiteIpEntity} from "../entity/site-ip.js";
import {Cron} from "../../cron/cron.js";
import { logger, utils } from "@certd/basic";
import { PeerCertificate } from "tls";
import { NotificationService } from "../../pipeline/service/notification-service.js";
import { isComm, isPlus } from "@certd/plus-core";
import { UserSuiteService } from "@certd/commercial-core";
import { UserSettingsService } from "../../mine/service/user-settings-service.js";
import { UserSiteMonitorSetting } from "../../mine/service/models.js";
import { SiteIpService } from "./site-ip-service.js";
import { SiteIpEntity } from "../entity/site-ip.js";
import { Cron } from "../../cron/cron.js";
import { dnsContainer } from "./dns-custom.js";
import { merge } from "lodash-es";
import { JobHistoryService } from "./job-history-service.js";
@@ -23,7 +23,7 @@ import { UserService } from "../../sys/authority/service/user-service.js";
import { ProjectService } from "../../sys/enterprise/service/project-service.js";
@Provide()
@Scope(ScopeEnum.Request, {allowDowngrade: true})
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class SiteInfoService extends BaseService<SiteInfoEntity> {
@InjectEntityModel(SiteInfoEntity)
repository: Repository<SiteInfoEntity>;
@@ -87,18 +87,18 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
}
await this.checkMonitorLimit(data.userId);
data.disabled = false;
const found = await this.repository.findOne({
where: {
domain: data.domain,
userId: data.userId,
httpsPort: data.httpsPort || 443
}
httpsPort: data.httpsPort || 443,
},
});
if (found) {
return {id: found.id};
return { id: found.id };
}
return await super.add(data);
@@ -113,11 +113,11 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
}
async getUserMonitorCount(userId: number) {
if (userId==null) {
if (userId == null) {
throw new Error("userId is required");
}
return await this.repository.count({
where: {userId}
where: { userId },
});
}
@@ -132,18 +132,18 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
throw new Error("站点域名不能为空");
}
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(site.userId,site.projectId, UserSiteMonitorSetting);
const dnsServer = setting.dnsServer
let customDns = null
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(site.userId, site.projectId, UserSiteMonitorSetting);
const dnsServer = setting.dnsServer;
let customDns = null;
if (dnsServer && dnsServer.length > 0) {
customDns = dnsContainer.getDns(dnsServer) as any
customDns = dnsContainer.getDns(dnsServer) as any;
}
try {
await this.update({
id: site.id,
checkStatus: "checking",
lastCheckTime: dayjs().valueOf()
lastCheckTime: dayjs().valueOf(),
});
const res = await siteTester.test({
host: site.domain,
@@ -177,56 +177,56 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
certExpiresTime: dayjs(expires).valueOf(),
lastCheckTime: dayjs().valueOf(),
error: null,
checkStatus: "ok"
checkStatus: "ok",
};
logger.info(`测试站点成功:id=${updateData.id},site=${site.name},certEffectiveTime=${updateData.certEffectiveTime},expiresTime=${updateData.certExpiresTime}`)
logger.info(`测试站点成功:id=${updateData.id},site=${site.name},certEffectiveTime=${updateData.certEffectiveTime},expiresTime=${updateData.certExpiresTime}`);
if (site.ipCheck) {
delete updateData.checkStatus
delete updateData.checkStatus;
}
await this.update(updateData);
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(site.userId,site.projectId, UserSiteMonitorSetting)
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(site.userId, site.projectId, UserSiteMonitorSetting);
merge(site,updateData)
merge(site, updateData);
//检查ip
await this.checkAllIp(site,retryTimes,setting);
await this.checkAllIp(site, retryTimes, setting);
if (!notify) {
return;
}
try {
await this.sendExpiresNotify(site.id,setting);
await this.sendExpiresNotify(site.id, setting);
} catch (e) {
logger.error("send notify error", e);
}
} catch (e) {
logger.error("check site error", e);
let message = e.message
if (!message){
message = e.code
let message = e.message;
if (!message) {
message = e.code;
}
if (e.errors &&e.errors.length > 0){
message += "\n"+e.errors.map((item:any)=>item.message).join("\n")
if (e.errors && e.errors.length > 0) {
message += "\n" + e.errors.map((item: any) => item.message).join("\n");
}
await this.update({
id: site.id,
checkStatus: "error",
lastCheckTime: dayjs().valueOf(),
error: message
error: message,
});
if (!notify) {
return;
}
try {
await this.sendCheckErrorNotify(site.id,false,setting);
await this.sendCheckErrorNotify(site.id, false, setting);
} catch (e) {
logger.error("send notify error", e);
}
}
}
async checkAllIp(site: SiteInfoEntity,retryTimes = null,setting: UserSiteMonitorSetting) {
async checkAllIp(site: SiteInfoEntity, retryTimes = null, setting: UserSiteMonitorSetting) {
if (!site.ipCheck) {
return;
}
@@ -248,11 +248,11 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
errorMessage += `${item.ipAddress}${item.error} \n`;
} else if (item.certExpiresTime !== certExpiresTime && !site.ipIgnoreCoherence) {
errorMessage += `${item.ipAddress}:与主站证书过期时间不一致(主站:${dayjs(certExpiresTime).format("YYYY-MM-DD")}IP${dayjs(item.certExpiresTime).format("YYYY-MM-DD")}) \n`;
} else if (isExpired){
} else if (isExpired) {
errorMessage += `${item.ipAddress}:证书已过期(过期时间:${dayjs(item.certExpiresTime).format("YYYY-MM-DD")}) \n`;
}else if (isWillExpired){
} else if (isWillExpired) {
errorMessage += `${item.ipAddress}:证书将过期(过期时间:${dayjs(item.certExpiresTime).format("YYYY-MM-DD")}) \n`;
}else {
} else {
errorCount--;
}
}
@@ -262,7 +262,7 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
id: site.id,
checkStatus: "ok",
error: "",
ipErrorCount: 0
ipErrorCount: 0,
});
return;
}
@@ -270,20 +270,19 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
id: site.id,
checkStatus: "error",
error: errorMessage,
ipErrorCount: errorCount
ipErrorCount: errorCount,
});
try {
await this.sendCheckErrorNotify(site.id, true,setting);
await this.sendCheckErrorNotify(site.id, true, setting);
} catch (e) {
logger.error("send notify error", e);
}
};
if (site.ipSyncAuto === false) {
await this.siteIpService.checkAll(site, retryTimes,onFinished);
}else{
await this.siteIpService.syncAndCheck(site, retryTimes,onFinished);
await this.siteIpService.checkAll(site, retryTimes, onFinished);
} else {
await this.siteIpService.syncAndCheck(site, retryTimes, onFinished);
}
}
/**
@@ -298,13 +297,13 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
throw new Error("站点不存在");
}
this.doCheck(site, notify, retryTimes).catch((err) => {
this.doCheck(site, notify, retryTimes).catch(err => {
logger.error("check site error", err);
});
return
return;
}
async sendCheckErrorNotify(siteId: number, fromIpCheck = false,setting: UserSiteMonitorSetting) {
async sendCheckErrorNotify(siteId: number, fromIpCheck = false, setting: UserSiteMonitorSetting) {
const site = await this.info(siteId);
const url = await this.notificationService.getBindUrl("#/certd/monitor/site");
// 发邮件
@@ -319,13 +318,13 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
content: `站点名称: ${site.name} \n站点域名: ${site.domain} \n错误信息:${site.error}`,
errorMessage: site.error,
notificationType: "siteCheckError",
}
},
},
site.userId
);
}
async sendExpiresNotify(siteId: number,setting: UserSiteMonitorSetting) {
async sendExpiresNotify(siteId: number, setting: UserSiteMonitorSetting) {
const tipDays = setting?.certValidDays || 10;
const site = await this.info(siteId);
const expires = site.certExpiresTime;
@@ -345,7 +344,7 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
url,
errorMessage: "站点证书即将过期",
notificationType: "siteCertExpireRemind",
}
},
},
site.userId
);
@@ -362,7 +361,7 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
url,
errorMessage: "站点证书已过期",
notificationType: "siteCertExpireRemind",
}
},
},
site.userId
);
@@ -370,45 +369,44 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
}
async checkList(sites: SiteInfoEntity[]) {
const cache = {}
const getFromCache = async (userId: number,projectId?: number) =>{
const key = `${userId}_${projectId??""}`
const cache = {};
const getFromCache = async (userId: number, projectId?: number) => {
const key = `${userId}_${projectId ?? ""}`;
if (cache[key]) {
return cache[key];
}
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(userId,projectId, UserSiteMonitorSetting)
cache[key] = setting
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(userId, projectId, UserSiteMonitorSetting);
cache[key] = setting;
return setting;
}
};
for (const site of sites) {
const setting = await getFromCache(site.userId,site.projectId)
let retryTimes = setting?.retryTimes
this.doCheck(site,true,retryTimes).catch(e => {
const setting = await getFromCache(site.userId, site.projectId);
const retryTimes = setting?.retryTimes;
this.doCheck(site, true, retryTimes).catch(e => {
logger.error(`检查站点证书失败,${site.domain}`, e.message);
});
await utils.sleep(100);
}
}
async getSetting(userId: number,projectId?: number) {
return await this.userSettingsService.getSetting<UserSiteMonitorSetting>(userId,projectId, UserSiteMonitorSetting);
async getSetting(userId: number, projectId?: number) {
return await this.userSettingsService.getSetting<UserSiteMonitorSetting>(userId, projectId, UserSiteMonitorSetting);
}
async saveSetting(userId: number,projectId: number, bean: UserSiteMonitorSetting) {
await this.userSettingsService.saveSetting(userId,projectId, bean);
if(bean.cron){
async saveSetting(userId: number, projectId: number, bean: UserSiteMonitorSetting) {
await this.userSettingsService.saveSetting(userId, projectId, bean);
if (bean.cron) {
//注册job
await this.registerSiteMonitorJob(userId,projectId);
}else{
this.clearSiteMonitorJob(userId,projectId);
await this.registerSiteMonitorJob(userId, projectId);
} else {
this.clearSiteMonitorJob(userId, projectId);
}
}
async ipCheckChange(req: { id: any; ipCheck: any }) {
await this.update({
id: req.id,
ipCheck: req.ipCheck
ipCheck: req.ipCheck,
});
if (req.ipCheck) {
const site = await this.info(req.id);
@@ -419,7 +417,7 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
async disabledChange(req: { disabled: any; id: any }) {
await this.update({
id: req.id,
disabled: req.disabled
disabled: req.disabled,
});
if (!req.disabled) {
const site = await this.info(req.id);
@@ -427,7 +425,7 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
}
}
async doImport(req: { text: string; userId: number,groupId?:number,projectId?:number }) {
async doImport(req: { text: string; userId: number; groupId?: number; projectId?: number }) {
if (!req.text) {
throw new Error("text is required");
}
@@ -459,7 +457,7 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
if (arr.length > 2) {
name = arr[2] || domain;
}
let remark:string = "";
let remark = "";
if (arr.length > 3) {
remark = arr[3] || "";
}
@@ -471,7 +469,7 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
userId: req.userId,
remark,
groupId: req.groupId,
projectId: req.projectId
projectId: req.projectId,
});
}
@@ -485,77 +483,77 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
await batchAdd(list);
}
clearSiteMonitorJob(userId: number,projectId?: number) {
this.cron.remove(`siteMonitor_${userId}_${projectId||""}`);
clearSiteMonitorJob(userId: number, projectId?: number) {
this.cron.remove(`siteMonitor_${userId}_${projectId || ""}`);
}
async registerSiteMonitorJob(userId?: number,projectId?: number) {
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(userId,projectId, UserSiteMonitorSetting);
async registerSiteMonitorJob(userId?: number, projectId?: number) {
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(userId, projectId, UserSiteMonitorSetting);
if (!setting.cron) {
return;
}
//注册个人的 或项目的
this.cron.register({
name: `siteMonitor_${userId}_${projectId||""}`,
name: `siteMonitor_${userId}_${projectId || ""}`,
cron: setting.cron,
job: () => this.triggerJobOnce(userId,projectId),
job: () => this.triggerJobOnce(userId, projectId),
});
}
async triggerCommonJob(){
async triggerCommonJob() {
//遍历用户
const userIds = await this.userService.getAllUserIds()
const userIds = await this.userService.getAllUserIds();
for (const userId of userIds) {
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(userId,null,UserSiteMonitorSetting)
if(setting && setting.cron){
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(userId, null, UserSiteMonitorSetting);
if (setting && setting.cron) {
//该用户有自定义检查时间,跳过公共job
continue
continue;
}
await this.triggerJobOnce(userId)
await this.triggerJobOnce(userId);
}
//遍历项目
const projectIds = await this.projectService.getAllProjectIds()
const projectIds = await this.projectService.getAllProjectIds();
for (const projectId of projectIds) {
const userId = Constants.enterpriseUserId
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(userId,projectId,UserSiteMonitorSetting)
if(setting && setting.cron){
const userId = Constants.enterpriseUserId;
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(userId, projectId, UserSiteMonitorSetting);
if (setting && setting.cron) {
//该项目有自定义检查时间,跳过公共job
continue
continue;
}
await this.triggerJobOnce(userId,projectId)
await this.triggerJobOnce(userId, projectId);
}
}
async triggerJobOnce(userId?:number,projectId?:number) {
if(userId==null){
async triggerJobOnce(userId?: number, projectId?: number) {
if (userId == null) {
throw new Error("userId is required");
}
const query:any = { disabled: false };
const query: any = { disabled: false };
query.userId = userId;
if(projectId){
if (projectId) {
query.projectId = projectId;
}
const siteCount = await this.repository.count({
where: query,
});
if (siteCount === 0) {
logger.info(`用户/项目[${userId}_${projectId||""}]没有站点证书需要检查`)
logger.info(`用户/项目[${userId}_${projectId || ""}]没有站点证书需要检查`);
return;
}
logger.info(`站点证书检查开始执行[${userId}_${projectId||""}]`);
let jobEntity :Partial<JobHistoryEntity> = null;
jobEntity = {
logger.info(`站点证书检查开始执行[${userId}_${projectId || ""}]`);
let jobEntity: Partial<JobHistoryEntity> = null;
jobEntity = {
userId,
projectId,
type:"siteCertMonitor",
title: '站点证书检查',
result:"start",
startAt:new Date().getTime(),
}
type: "siteCertMonitor",
title: "站点证书检查",
result: "start",
startAt: new Date().getTime(),
};
await this.jobHistoryService.add(jobEntity);
let offset = 0;
const limit = 50;
@@ -575,17 +573,17 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
await this.checkList(records);
}
logger.info(`站点证书检查完成[${userId}_${projectId||""}]`);
logger.info(`站点证书检查完成[${userId}_${projectId || ""}]`);
await this.jobHistoryService.update({
id: jobEntity.id,
result: "done",
content:`共检查${count}个站点`,
endAt:new Date().getTime(),
updateTime:new Date(),
content: `共检查${count}个站点`,
endAt: new Date().getTime(),
updateTime: new Date(),
});
}
async batchDelete(ids: number[], userId: number,projectId?:number): Promise<void> {
async batchDelete(ids: number[], userId: number, projectId?: number): Promise<void> {
await this.repository.delete({
id: In(ids),
userId,
@@ -1,21 +1,21 @@
import {Inject, Provide, Scope, ScopeEnum} from "@midwayjs/core";
import {BaseService, SysSettingsService} from "@certd/lib-server";
import {InjectEntityModel} from "@midwayjs/typeorm";
import {Repository} from "typeorm";
import {SiteInfoEntity} from "../entity/site-info.js";
import {NotificationService} from "../../pipeline/service/notification-service.js";
import {UserSuiteService} from "@certd/commercial-core";
import {UserSettingsService} from "../../mine/service/user-settings-service.js";
import {SiteIpEntity} from "../entity/site-ip.js";
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { BaseService, SysSettingsService } from "@certd/lib-server";
import { InjectEntityModel } from "@midwayjs/typeorm";
import { Repository } from "typeorm";
import { SiteInfoEntity } from "../entity/site-info.js";
import { NotificationService } from "../../pipeline/service/notification-service.js";
import { UserSuiteService } from "@certd/commercial-core";
import { UserSettingsService } from "../../mine/service/user-settings-service.js";
import { SiteIpEntity } from "../entity/site-ip.js";
import dnsSdk from "dns";
import {logger} from "@certd/basic";
import { logger } from "@certd/basic";
import dayjs from "dayjs";
import {siteTester} from "./site-tester.js";
import {PeerCertificate} from "tls";
import { siteTester } from "./site-tester.js";
import { PeerCertificate } from "tls";
import { UserSiteMonitorSetting } from "../../mine/service/models.js";
import { dnsContainer } from "./dns-custom.js";
const dns = dnsSdk.promises;
const dns = dnsSdk.promises;
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
@@ -47,12 +47,11 @@ export class SiteIpService extends BaseService<SiteIpEntity> {
throw new Error("userId is required");
}
data.disabled = false;
const res= await super.add(data);
await this.updateIpCount(data.siteId)
return res
const res = await super.add(data);
await this.updateIpCount(data.siteId);
return res;
}
async update(data: any) {
if (!data.id) {
throw new Error("id is required");
@@ -61,52 +60,52 @@ export class SiteIpService extends BaseService<SiteIpEntity> {
await super.update(data);
}
async sync(entity: SiteInfoEntity,check:boolean = true) {
async sync(entity: SiteInfoEntity, check = true) {
const domain = entity.domain;
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(entity.userId,entity.projectId, UserSiteMonitorSetting);
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(entity.userId, entity.projectId, UserSiteMonitorSetting);
const dnsServer = setting.dnsServer
let resolver = dns
const dnsServer = setting.dnsServer;
let resolver = dns;
if (dnsServer && dnsServer.length > 0) {
resolver = dnsContainer.getDns(dnsServer) as any
resolver = dnsContainer.getDns(dnsServer) as any;
}
//从域名解析中获取所有ip
const ips = await this.getAllIpsFromDomain(domain,resolver,entity.ipSyncMode);
if (ips.length === 0 ) {
logger.warn(`没有发现${domain}的IP`)
return
const ips = await this.getAllIpsFromDomain(domain, resolver, entity.ipSyncMode);
if (ips.length === 0) {
logger.warn(`没有发现${domain}的IP`);
return;
}
const oldIps = await this.repository.find({
where:{
siteId: entity.id,
from:"sync"
}
})
where: {
siteId: entity.id,
from: "sync",
},
});
let hasChanged = true
if (oldIps.length === ips.length ){
let hasChanged = true;
if (oldIps.length === ips.length) {
//检查是否有变化
const oldIpList = oldIps.map(ip=>ip.ipAddress).sort().join(",")
const newIpList = ips.sort().join(",")
if(oldIpList === newIpList){
//无变化
hasChanged = false
}
const oldIpList = oldIps
.map(ip => ip.ipAddress)
.sort()
.join(",");
const newIpList = ips.sort().join(",");
if (oldIpList === newIpList) {
//无变化
hasChanged = false;
}
}
if(hasChanged){
logger.info(`发现${domain}的IP变化,需要更新,旧IP:${oldIps.map(ip=>ip.ipAddress).join(",")},新IP:${ips.join(",")}`)
if (hasChanged) {
logger.info(`发现${domain}的IP变化,需要更新,旧IP:${oldIps.map(ip => ip.ipAddress).join(",")},新IP:${ips.join(",")}`);
//有变化需要更新
//删除所有的ip
await this.repository.delete({
siteId: entity.id,
from: "sync"
from: "sync",
});
//添加新的ip
@@ -116,36 +115,36 @@ export class SiteIpService extends BaseService<SiteIpEntity> {
userId: entity.userId,
siteId: entity.id,
from: "sync",
disabled:false,
disabled: false,
});
await this.updateIpCount(entity.id)
await this.updateIpCount(entity.id);
}
}
if (check){
if (check) {
await this.checkAll(entity);
}
}
async check(ipId: number, domain: string, port: number,retryTimes = null ,tipDays = 10) {
if(!ipId){
return
async check(ipId: number, domain: string, port: number, retryTimes = null, tipDays = 10) {
if (!ipId) {
return;
}
const entity = await this.info(ipId);
if (!entity) {
return;
}
logger.info(`开始测试站点ip: id=${entity.id},ip=${entity.ipAddress}`)
logger.info(`开始测试站点ip: id=${entity.id},ip=${entity.ipAddress}`);
try {
await this.update({
id: entity.id,
checkStatus: "checking",
lastCheckTime: dayjs().valueOf()
lastCheckTime: dayjs().valueOf(),
});
const res = await siteTester.test({
host: domain,
port: port,
retryTimes : retryTimes??3,
ipAddress: entity.ipAddress
retryTimes: retryTimes ?? 3,
ipAddress: entity.ipAddress,
});
const certi: PeerCertificate = res.certificate;
@@ -170,39 +169,38 @@ export class SiteIpService extends BaseService<SiteIpEntity> {
certExpiresTime: dayjs(expires).valueOf(),
lastCheckTime: dayjs().valueOf(),
error: null,
checkStatus: "ok"
checkStatus: "ok",
};
await this.update(updateData);
logger.info(`测试站点ip成功: id=${updateData.id},ip=${entity.ipAddress},expiresTime=${updateData.certExpiresTime}`)
logger.info(`测试站点ip成功: id=${updateData.id},ip=${entity.ipAddress},expiresTime=${updateData.certExpiresTime}`);
return {
...updateData,
ipAddress: entity.ipAddress,
}
};
} catch (e) {
logger.error("check site ip error", e);
await this.update({
id: entity.id,
checkStatus: "error",
lastCheckTime: dayjs().valueOf(),
error: e.message
error: e.message,
});
return {
id: entity.id,
ipAddress: entity.ipAddress,
error: e.message
}
error: e.message,
};
}
}
async checkAll(siteInfo: SiteInfoEntity,retryTimes = null,onFinish?: (e: any) => void) {
async checkAll(siteInfo: SiteInfoEntity, retryTimes = null, onFinish?: (e: any) => void) {
const siteId = siteInfo.id;
const ips = await this.repository.find({
where: {
siteId: siteId
}
siteId: siteId,
},
});
const domain = siteInfo.domain;
const port = siteInfo.httpsPort;
@@ -210,44 +208,44 @@ export class SiteIpService extends BaseService<SiteIpEntity> {
for (const item of ips) {
const func = async () => {
try {
return await this.check(item.id, domain, port,retryTimes);
return await this.check(item.id, domain, port, retryTimes);
} catch (e) {
logger.error("check site item error", e);
return {
...item,
error:e.message
}
error: e.message,
};
}
}
};
promiseList.push(func());
}
Promise.all(promiseList).then((res)=>{
const finished = res.filter(item=>{
return item!=null
})
Promise.all(promiseList).then(res => {
const finished = res.filter(item => {
return item != null;
});
if (onFinish) {
onFinish && onFinish(finished)
onFinish && onFinish(finished);
}
})
});
}
async getAllIpsFromDomain(domain: string,resolver:any = dns,ipSyncMode:string = "all"):Promise<string[]> {
const getFromV4 = async ():Promise<string[]> => {
try{
async getAllIpsFromDomain(domain: string, resolver: any = dns, ipSyncMode = "all"): Promise<string[]> {
const getFromV4 = async (): Promise<string[]> => {
try {
return await resolver.resolve4(domain);
}catch (err) {
logger.warn(`[${domain}] resolve4 error`, err)
return []
} catch (err) {
logger.warn(`[${domain}] resolve4 error`, err);
return [];
}
}
const getFromV6 = async ():Promise<string[]> => {
try{
};
const getFromV6 = async (): Promise<string[]> => {
try {
return await resolver.resolve6(domain);
}catch (err) {
logger.warn(`[${domain}] resolve6 error`, err)
return []
} catch (err) {
logger.warn(`[${domain}] resolve6 error`, err);
return [];
}
}
};
if (ipSyncMode === "ipv4") {
return await getFromV4();
@@ -261,24 +259,25 @@ export class SiteIpService extends BaseService<SiteIpEntity> {
});
}
async updateIpCount(siteId:number){
async updateIpCount(siteId: number) {
const count = await this.repository.count({
where:{
siteId
}
})
await this.siteInfoRepository.update({
//where
id:siteId,
},
where: {
siteId,
},
});
await this.siteInfoRepository.update(
{
//where
id: siteId,
},
{
//update
ipCount:count
})
ipCount: count,
}
);
}
async doImport(req: { text: string; userId:number, siteId:number,projectId?:number }) {
async doImport(req: { text: string; userId: number; siteId: number; projectId?: number }) {
if (!req.text) {
throw new Error("text is required");
}
@@ -289,9 +288,9 @@ export class SiteIpService extends BaseService<SiteIpEntity> {
const siteEntity = await this.siteInfoRepository.findOne({
where: {
id: req.siteId,
userId:req.userId,
projectId:req.projectId
}
userId: req.userId,
projectId: req.projectId,
},
});
if (!siteEntity) {
throw new Error(`站点${req.siteId}不存在`);
@@ -307,11 +306,11 @@ export class SiteIpService extends BaseService<SiteIpEntity> {
continue;
}
list.push({
ipAddress:item,
ipAddress: item,
userId: userId,
siteId: req.siteId,
from: "import",
disabled:false,
disabled: false,
projectId: req.projectId,
});
}
@@ -324,8 +323,8 @@ export class SiteIpService extends BaseService<SiteIpEntity> {
await batchAdd(list);
}
async syncAndCheck(siteEntity:SiteInfoEntity,retryTimes = null,onFinish?: (e: any) => void){
await this.sync(siteEntity,false);
await this.checkAll(siteEntity,retryTimes,onFinish);
async syncAndCheck(siteEntity: SiteInfoEntity, retryTimes = null, onFinish?: (e: any) => void) {
await this.sync(siteEntity, false);
await this.checkAll(siteEntity, retryTimes, onFinish);
}
}
@@ -2,7 +2,7 @@ import { logger, safePromise, utils } from "@certd/basic";
import { merge } from "lodash-es";
import https from "https";
import { PeerCertificate } from "tls";
import {DnsCustom} from "./dns-custom.js";
import { DnsCustom } from "./dns-custom.js";
export type SiteTestReq = {
host: string; // 只用域名部分
@@ -20,8 +20,8 @@ export type SiteTestRes = {
export class SiteTester {
async test(req: SiteTestReq): Promise<SiteTestRes> {
const req_ = {...req}
delete req_.customDns
const req_ = { ...req };
delete req_.customDns;
logger.info("测试站点:", JSON.stringify(req_));
const maxRetryTimes = req.retryTimes == null ? 3 : req.retryTimes;
let tryCount = 0;
@@ -49,40 +49,40 @@ export class SiteTester {
{
port: 443,
method: "GET",
rejectUnauthorized: false
rejectUnauthorized: false,
},
req
);
let customLookup = null
let customLookup = null;
if (req.ipAddress) {
//使用固定的ip
const ipAddress = req.ipAddress;
options.headers = {
host: options.host,
//sni
servername: options.host
servername: options.host,
};
options.host = ipAddress;
}else if (req.customDns ) {
} else if (req.customDns) {
// 非ip address 请求时
const customDns = req.customDns
customLookup = async (hostname:string, options:any, callback)=> {
const customDns = req.customDns;
customLookup = async (hostname: string, options: any, callback) => {
console.log(hostname, options);
// { family: undefined, hints: 0, all: true }
const res = await customDns.lookup(hostname, options)
console.log("custom lookup res:",res)
const res = await customDns.lookup(hostname, options);
console.log("custom lookup res:", res);
if (!res || res.length === 0) {
callback(new Error("没有解析到IP"));
}
callback(null, res);
}
};
}
const agentOptions:any = { keepAlive: false };
const agentOptions: any = { keepAlive: false };
if (customLookup) {
agentOptions.lookup = customLookup
agentOptions.lookup = customLookup;
}
options.agent = new https.Agent(agentOptions);
@@ -95,19 +95,19 @@ export class SiteTester {
});
// ✅ 关键:在 'socket' 事件中获取证书(握手完成后立即执行)
req.on('socket', (socket:any) => {
socket.on('secureConnect', () => {
req.on("socket", (socket: any) => {
socket.on("secureConnect", () => {
// TLS握手完成,证书已经可用
const certificate = socket.getPeerCertificate();
if (certificate.subject) {
logger.info('证书获取成功', certificate.subject);
logger.info("证书获取成功", certificate.subject);
resolve({
certificate
certificate,
});
}else{
} else {
logger.warn("证书信息为空");
resolve({
certificate: null
certificate: null,
});
}
});
@@ -1,28 +1,28 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity('cd_open_key')
@Entity("cd_open_key")
export class OpenKeyEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'user_id', comment: '用户id' })
@Column({ name: "user_id", comment: "用户id" })
userId: number;
@Column({ name: 'key_id', comment: 'keyId' })
@Column({ name: "key_id", comment: "keyId" })
keyId: string;
@Column({ name: 'key_secret', comment: 'keySecret' })
@Column({ name: "key_secret", comment: "keySecret" })
keySecret: string;
@Column({ name: 'scope', comment: '权限范围' })
@Column({ name: "scope", comment: "权限范围" })
scope: string; // open 仅开放接口、 user 用户所有权限
@Column({ name: 'project_id', comment: '项目id' })
@Column({ name: "project_id", comment: "项目id" })
projectId: number;
@Column({ name: 'create_time', comment: '创建时间', default: () => 'CURRENT_TIMESTAMP' })
@Column({ name: "create_time", comment: "创建时间", default: () => "CURRENT_TIMESTAMP" })
createTime: Date;
@Column({ name: 'update_time', comment: '修改时间', default: () => 'CURRENT_TIMESTAMP' })
@Column({ name: "update_time", comment: "修改时间", default: () => "CURRENT_TIMESTAMP" })
updateTime: Date;
}
@@ -1,11 +1,11 @@
import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { BaseService, Constants, CodeException, PageReq } from '@certd/lib-server';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { OpenKeyEntity } from '../entity/open-key.js';
import { utils } from '@certd/basic';
import crypto from 'crypto';
import dayjs from 'dayjs';
import { Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { BaseService, Constants, CodeException, PageReq } from "@certd/lib-server";
import { InjectEntityModel } from "@midwayjs/typeorm";
import { Repository } from "typeorm";
import { OpenKeyEntity } from "../entity/open-key.js";
import { utils } from "@certd/basic";
import crypto from "crypto";
import dayjs from "dayjs";
export type OpenKey = {
userId: number;
@@ -31,34 +31,34 @@ export class OpenKeyService extends BaseService<OpenKeyEntity> {
}
async add(bean: OpenKeyEntity) {
return await this.generate(bean.userId,bean.projectId, bean.scope);
return await this.generate(bean.userId, bean.projectId, bean.scope);
}
async generate(userId: number, projectId?: number, scope: string = 'open') {
const keyId = utils.id.simpleNanoId(18) + '_key';
async generate(userId: number, projectId?: number, scope = "open") {
const keyId = utils.id.simpleNanoId(18) + "_key";
const secretKey = crypto.randomBytes(32);
const keySecret = Buffer.from(secretKey).toString('hex');
const keySecret = Buffer.from(secretKey).toString("hex");
const entity = new OpenKeyEntity();
entity.userId = userId;
entity.projectId = projectId;
entity.keyId = keyId;
entity.keySecret = keySecret;
entity.scope = scope ?? 'open';
entity.scope = scope ?? "open";
await this.repository.save(entity);
return entity;
}
async getByKeyId(keyId: string) {
if (!keyId) {
throw new Error('keyId不能为空');
throw new Error("keyId不能为空");
}
return this.repository.findOne({ where: { keyId } });
}
async verifyOpenKey(openKey: string): Promise<OpenKey> {
// openkey 组成,content = base64({keyId,t,encrypt,signType}) ,sign = md5({keyId,t,encrypt,signType}secret) , key = content.sign
const [content, sign] = openKey.split('.');
const contentJson = Buffer.from(content, 'base64').toString();
const [content, sign] = openKey.split(".");
const contentJson = Buffer.from(content, "base64").toString();
const { keyId, t, encrypt, signType } = JSON.parse(contentJson);
// 正负不超过3分钟 ,timestamps单位为秒
if (Math.abs(Number(t) - Math.floor(Date.now() / 1000)) > 180) {
@@ -67,22 +67,22 @@ export class OpenKeyService extends BaseService<OpenKeyEntity> {
const entity = await this.getByKeyId(keyId);
if (!entity) {
throw new Error('openKey不存在');
throw new Error("openKey不存在");
}
const secret = entity.keySecret;
let computedSign = '';
if (signType === 'md5') {
let computedSign = "";
if (signType === "md5") {
computedSign = utils.hash.md5(contentJson + secret);
} else if (signType === 'sha256') {
} else if (signType === "sha256") {
computedSign = utils.hash.sha256(contentJson + secret);
} else {
throw new CodeException(Constants.res.openKeySignTypeError);
}
if (Buffer.from(computedSign).toString('base64') !== sign) {
if (Buffer.from(computedSign).toString("base64") !== sign) {
throw new CodeException(Constants.res.openKeySignError);
}
if (entity.userId==null) {
if (entity.userId == null) {
throw new CodeException(Constants.res.openKeyError);
}
@@ -98,21 +98,21 @@ export class OpenKeyService extends BaseService<OpenKeyEntity> {
async getApiToken(id: number) {
if (!id) {
throw new Error('id不能为空');
throw new Error("id不能为空");
}
const entity = await this.repository.findOne({ where: { id } });
if (!entity) {
throw new Error('id不存在');
throw new Error("id不存在");
}
const { keyId, keySecret } = entity;
const openKey = {
keyId,
t: dayjs().unix(),
encrypt: false,
signType: 'md5',
signType: "md5",
};
const content = JSON.stringify(openKey);
const sign = utils.hash.md5(content + keySecret);
return Buffer.from(content).toString('base64') + '.' + Buffer.from(sign).toString('base64');
return Buffer.from(content).toString("base64") + "." + Buffer.from(sign).toString("base64");
}
}
@@ -1,44 +1,44 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity('pi_history_log')
@Entity("pi_history_log")
export class HistoryLogEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'user_id', comment: '用户id' })
@Column({ name: "user_id", comment: "用户id" })
userId: number;
@Column({ name: 'pipeline_id', comment: '流水线' })
@Column({ name: "pipeline_id", comment: "流水线" })
pipelineId: number;
@Column({ name: 'history_id', comment: '历史id' })
@Column({ name: "history_id", comment: "历史id" })
historyId: number;
@Column({
name: 'node_id',
comment: '任务节点id',
name: "node_id",
comment: "任务节点id",
length: 100,
nullable: true,
})
nodeId: string;
@Column({ comment: '日志内容', length: 40960, nullable: true })
@Column({ comment: "日志内容", length: 40960, nullable: true })
logs: string;
@Column({ name: 'project_id', comment: '项目id' })
@Column({ name: "project_id", comment: "项目id" })
projectId: number;
@Column({
name: 'create_time',
comment: '创建时间',
default: () => 'CURRENT_TIMESTAMP',
name: "create_time",
comment: "创建时间",
default: () => "CURRENT_TIMESTAMP",
})
createTime: Date;
@Column({
name: 'update_time',
comment: '修改时间',
default: () => 'CURRENT_TIMESTAMP',
name: "update_time",
comment: "修改时间",
default: () => "CURRENT_TIMESTAMP",
})
updateTime: Date;
}
@@ -1,47 +1,46 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity('pi_history')
@Entity("pi_history")
export class HistoryEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'user_id', comment: '用户id' })
@Column({ name: "user_id", comment: "用户id" })
userId: number;
@Column({ name: 'pipeline_id', comment: '流水线' })
@Column({ name: "pipeline_id", comment: "流水线" })
pipelineId: number;
@Column({ comment: '运行状态', length: 40960, nullable: true })
@Column({ comment: "运行状态", length: 40960, nullable: true })
pipeline: string;
@Column({ comment: '结果状态', length: 20, nullable: true })
@Column({ comment: "结果状态", length: 20, nullable: true })
status: string;
@Column({ name: 'trigger_type',comment: '触发类型', length: 20, nullable: true })
@Column({ name: "trigger_type", comment: "触发类型", length: 20, nullable: true })
triggerType: string;
@Column({
name: 'end_time',
comment: '结束时间',
name: "end_time",
comment: "结束时间",
nullable: true,
})
endTime: Date;
@Column({ name: 'project_id', comment: '项目id' })
@Column({ name: "project_id", comment: "项目id" })
projectId: number;
@Column({
name: 'create_time',
comment: '创建时间',
default: () => 'CURRENT_TIMESTAMP',
name: "create_time",
comment: "创建时间",
default: () => "CURRENT_TIMESTAMP",
})
createTime: Date;
@Column({
name: 'update_time',
comment: '修改时间',
default: () => 'CURRENT_TIMESTAMP',
name: "update_time",
comment: "修改时间",
default: () => "CURRENT_TIMESTAMP",
})
updateTime: Date;
pipelineTitle: string;
@@ -1,41 +1,41 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity('pi_notification')
@Entity("pi_notification")
export class NotificationEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'key_id', comment: 'key_id', length: 100 })
@Column({ name: "key_id", comment: "key_id", length: 100 })
keyId: string;
@Column({ name: 'user_id', comment: 'UserId' })
@Column({ name: "user_id", comment: "UserId" })
userId: number;
@Column({ name: 'type', comment: '通知类型' })
@Column({ name: "type", comment: "通知类型" })
type: string;
@Column({ name: 'name', comment: '名称' })
@Column({ name: "name", comment: "名称" })
name: string;
@Column({ name: 'setting', comment: '通知配置', length: 10240 })
@Column({ name: "setting", comment: "通知配置", length: 10240 })
setting: string;
@Column({ name: 'is_default', comment: '是否默认' })
@Column({ name: "is_default", comment: "是否默认" })
isDefault: boolean;
@Column({ name: 'project_id', comment: '项目id' })
@Column({ name: "project_id", comment: "项目id" })
projectId: number;
@Column({
name: 'create_time',
comment: '创建时间',
default: () => 'CURRENT_TIMESTAMP',
name: "create_time",
comment: "创建时间",
default: () => "CURRENT_TIMESTAMP",
})
createTime: Date;
@Column({
name: 'update_time',
comment: '修改时间',
default: () => 'CURRENT_TIMESTAMP',
name: "update_time",
comment: "修改时间",
default: () => "CURRENT_TIMESTAMP",
})
updateTime: Date;
}
@@ -1,36 +1,36 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity('pi_pipeline_group')
@Entity("pi_pipeline_group")
export class PipelineGroupEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'user_id', comment: '用户id' })
@Column({ name: "user_id", comment: "用户id" })
userId: number;
@Column({ name: 'name', comment: '分组名称' })
@Column({ name: "name", comment: "分组名称" })
name: string;
@Column({ name: 'icon', comment: '图标' })
@Column({ name: "icon", comment: "图标" })
icon: string;
@Column({ name: 'favorite', comment: '收藏' })
@Column({ name: "favorite", comment: "收藏" })
favorite: boolean;
@Column({ name: 'project_id', comment: '项目id' })
@Column({ name: "project_id", comment: "项目id" })
projectId: number;
@Column({
name: 'create_time',
comment: '创建时间',
default: () => 'CURRENT_TIMESTAMP',
name: "create_time",
comment: "创建时间",
default: () => "CURRENT_TIMESTAMP",
})
createTime: Date;
@Column({
name: 'update_time',
comment: '修改时间',
default: () => 'CURRENT_TIMESTAMP',
name: "update_time",
comment: "修改时间",
default: () => "CURRENT_TIMESTAMP",
})
updateTime: Date;
}
@@ -1,58 +1,58 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity('pi_pipeline')
@Entity("pi_pipeline")
export class PipelineEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'user_id', comment: '用户id' })
@Column({ name: "user_id", comment: "用户id" })
userId: number;
@Column({ name: 'title', comment: '标题' })
@Column({ name: "title", comment: "标题" })
title: string;
@Column({ comment: '配置', length: 40960 })
@Column({ comment: "配置", length: 40960 })
content: string;
@Column({ name: 'keep_history_count', comment: '历史记录保持数量', nullable: true })
@Column({ name: "keep_history_count", comment: "历史记录保持数量", nullable: true })
keepHistoryCount: number;
@Column({ name: 'group_id', comment: '分组id', nullable: true })
@Column({ name: "group_id", comment: "分组id", nullable: true })
groupId: number;
@Column({ comment: '备注', length: 100, nullable: true })
@Column({ comment: "备注", length: 100, nullable: true })
remark: string;
@Column({ comment: '状态', length: 100, nullable: true })
@Column({ comment: "状态", length: 100, nullable: true })
status: string;
@Column({ comment: '启用/禁用', nullable: true, default: false })
@Column({ comment: "启用/禁用", nullable: true, default: false })
disabled: boolean;
// cert_apply: 证书申请;cert_upload: 证书上传; backup: 备份; custom:自定义; template: 模板
@Column({ comment: '类型', nullable: true, default: 'cert' })
@Column({ comment: "类型", nullable: true, default: "cert" })
type: string;
@Column({ name: 'webhook_key', comment: 'webhookkey', length: 100, nullable: true })
@Column({ name: "webhook_key", comment: "webhookkey", length: 100, nullable: true })
webhookKey: string;
@Column({ name: 'trigger_count', comment: '触发器数量', nullable: true, default: 0 })
@Column({ name: "trigger_count", comment: "触发器数量", nullable: true, default: 0 })
triggerCount: number;
// custom: 自定义; monitor: 监控;
@Column({ comment: '来源', nullable: true, default: '' })
@Column({ comment: "来源", nullable: true, default: "" })
from: string;
@Column({ name:"template_id", comment: '关联模版id', nullable: true, default: 0 })
@Column({ name: "template_id", comment: "关联模版id", nullable: true, default: 0 })
templateId: number;
@Column({ name:"is_template", comment: '是否模版', nullable: true, default: false })
@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})
@Column({ name: "valid_time", comment: "到期时间", nullable: true, default: 0 })
validTime: number;
// 变量
@@ -60,19 +60,18 @@ export class PipelineEntity {
nextRunTime: number;
@Column({name: 'order', comment: '排序', nullable: true,})
@Column({ name: "order", comment: "排序", nullable: true })
order: number;
@Column({ name: 'project_id', comment: '项目id' })
@Column({ name: "project_id", comment: "项目id" })
projectId: number;
@Column({name: 'create_time',comment: '创建时间', default: () => 'CURRENT_TIMESTAMP',})
@Column({ name: "create_time", comment: "创建时间", default: () => "CURRENT_TIMESTAMP" })
createTime: Date;
@Column({
name: 'update_time',
comment: '修改时间',
default: () => 'CURRENT_TIMESTAMP',
name: "update_time",
comment: "修改时间",
default: () => "CURRENT_TIMESTAMP",
})
updateTime: Date;
}
@@ -1,41 +1,41 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity('pi_storage')
@Entity("pi_storage")
export class StorageEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'user_id', comment: '用户id' })
@Column({ name: "user_id", comment: "用户id" })
userId: number;
@Column({ name: 'scope', comment: '范围' })
@Column({ name: "scope", comment: "范围" })
scope: string;
@Column({ name: 'namespace', comment: '命名空间' })
@Column({ name: "namespace", comment: "命名空间" })
namespace: string;
@Column({ comment: 'version', length: 100, nullable: true })
@Column({ comment: "version", length: 100, nullable: true })
version: string;
@Column({ comment: 'key', length: 100, nullable: true })
@Column({ comment: "key", length: 100, nullable: true })
key: string;
@Column({ comment: 'value', length: 40960, nullable: true })
@Column({ comment: "value", length: 40960, nullable: true })
value: string;
@Column({ name: 'project_id', comment: '项目id' })
@Column({ name: "project_id", comment: "项目id" })
projectId: number;
@Column({
name: 'create_time',
comment: '创建时间',
default: () => 'CURRENT_TIMESTAMP',
name: "create_time",
comment: "创建时间",
default: () => "CURRENT_TIMESTAMP",
})
createTime: Date;
@Column({
name: 'update_time',
comment: '修改时间',
default: () => 'CURRENT_TIMESTAMP',
name: "update_time",
comment: "修改时间",
default: () => "CURRENT_TIMESTAMP",
})
updateTime: Date;
}
@@ -1,35 +1,35 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
/**
*
*/
@Entity('pi_sub_domain')
@Entity("pi_sub_domain")
export class SubDomainEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'user_id', comment: 'UserId' })
@Column({ name: "user_id", comment: "UserId" })
userId: number;
@Column({ name: 'domain', comment: '子域名' })
@Column({ name: "domain", comment: "子域名" })
domain: string;
@Column({ name: 'disabled', comment: '禁用' })
@Column({ name: "disabled", comment: "禁用" })
disabled: boolean;
@Column({ name: 'project_id', comment: '项目Id' })
@Column({ name: "project_id", comment: "项目Id" })
projectId: number;
@Column({
name: 'create_time',
comment: '创建时间',
default: () => 'CURRENT_TIMESTAMP',
name: "create_time",
comment: "创建时间",
default: () => "CURRENT_TIMESTAMP",
})
createTime: Date;
@Column({
name: 'update_time',
comment: '修改时间',
default: () => 'CURRENT_TIMESTAMP',
name: "update_time",
comment: "修改时间",
default: () => "CURRENT_TIMESTAMP",
})
updateTime: Date;
}
@@ -1,57 +1,55 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
export type PipelineTemplateType = {
input: {
[key: string]: {
value: string;
value: string;
};
}
}
};
};
@Entity('pi_template')
@Entity("pi_template")
export class TemplateEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'user_id', comment: '用户id' })
@Column({ name: "user_id", comment: "用户id" })
userId: number;
@Column({ name: 'pipeline_id', comment: '流水线id' })
@Column({ name: "pipeline_id", comment: "流水线id" })
pipelineId: number;
@Column({ name: 'title', comment: '标题' })
@Column({ name: "title", comment: "标题" })
title: string;
@Column({ name: 'desc', comment: '说明' })
@Column({ name: "desc", comment: "说明" })
desc: string;
@Column({ comment: '配置', length: 40960 })
@Column({ comment: "配置", length: 40960 })
content: string;
@Column({ comment: '启用/禁用', nullable: true, default: false })
@Column({ comment: "启用/禁用", nullable: true, default: false })
disabled: boolean;
@Column({
name: 'order',
comment: '排序',
name: "order",
comment: "排序",
nullable: true,
})
order: number;
@Column({ name: 'project_id', comment: '项目id' })
@Column({ name: "project_id", comment: "项目id" })
projectId: number;
@Column({
name: 'create_time',
comment: '创建时间',
default: () => 'CURRENT_TIMESTAMP',
name: "create_time",
comment: "创建时间",
default: () => "CURRENT_TIMESTAMP",
})
createTime: Date;
@Column({
name: 'update_time',
comment: '修改时间',
default: () => 'CURRENT_TIMESTAMP',
name: "update_time",
comment: "修改时间",
default: () => "CURRENT_TIMESTAMP",
})
updateTime: Date;
}
@@ -1,5 +1,5 @@
import { HistoryEntity } from '../history.js';
import { HistoryLogEntity } from '../history-log.js';
import { HistoryEntity } from "../history.js";
import { HistoryLogEntity } from "../history-log.js";
export class HistoryDetail {
history: HistoryEntity;
@@ -1,6 +1,6 @@
import { PipelineEntity } from '../pipeline.js';
import { HistoryEntity } from '../history.js';
import { HistoryLogEntity } from '../history-log.js';
import { PipelineEntity } from "../pipeline.js";
import { HistoryEntity } from "../history.js";
import { HistoryLogEntity } from "../history-log.js";
export class PipelineDetail {
pipeline: PipelineEntity;
@@ -8,15 +8,13 @@ import { AddonService, newAddon, PermissionException, ValidateException } from "
*/
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class AddonGetterService {
export class AddonGetterService {
@Inject()
taskServiceBuilder: TaskServiceBuilder;
@Inject()
addonService: AddonService;
async getAddonById(id: any, checkUserId: boolean, userId?: number, projectId?: number, defaultAddon?:{type:string,name:string} ): Promise<any> {
async getAddonById(id: any, checkUserId: boolean, userId?: number, projectId?: number, defaultAddon?: { type: string; name: string }): Promise<any> {
const serviceGetter = this.taskServiceBuilder.create({
userId,
projectId,
@@ -25,8 +23,8 @@ export class AddonGetterService {
http,
logger,
utils,
serviceGetter
}
serviceGetter,
};
if (!id) {
if (!defaultAddon) {
@@ -53,7 +51,7 @@ export class AddonGetterService {
const setting = JSON.parse(entity.setting ?? "{}");
const input = {
id: entity.id,
...setting
...setting,
};
return await newAddon(entity.addonType, entity.type, input, ctx);
@@ -63,11 +61,10 @@ export class AddonGetterService {
return await this.getAddonById(id, true, userId, projectId);
}
async getBlank(addonType:string,subType:string,projectId?: number){
return await this.getAddonById(null,false,0,projectId,{
type: addonType, name:subType
})
async getBlank(addonType: string, subType: string, projectId?: number) {
return await this.getAddonById(null, false, 0, projectId, {
type: addonType,
name: subType,
});
}
}
@@ -1,6 +1,6 @@
import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { pluginGroups, pluginRegistry } from '@certd/pipeline';
import { cloneDeep } from 'lodash-es';
import { Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { pluginGroups, pluginRegistry } from "@certd/pipeline";
import { cloneDeep } from "lodash-es";
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
@@ -14,23 +14,23 @@ export class BuiltInPluginService {
continue;
}
//@ts-ignore
if(Plugin.define?.type && Plugin.define?.type.toLowerCase() !== 'builtin'){
if (Plugin.define?.type && Plugin.define?.type.toLowerCase() !== "builtin") {
continue;
}
list.push({ ...Plugin.define, key });
}
list = list.sort((a, b) => {
return (a.order ?? 10 )- (b.order ?? 10);
return (a.order ?? 10) - (b.order ?? 10);
});
return list;
}
getGroups() {
const groups:any = cloneDeep(pluginGroups);
const groups: any = cloneDeep(pluginGroups);
for (const key in groups) {
const group = groups[key];
group.plugins = group.plugins.sort((a, b) => {
return (a.order ?? 10 )- (b.order ?? 10);
return (a.order ?? 10) - (b.order ?? 10);
});
}
return groups;
@@ -1,5 +1,5 @@
import { IStorage } from '@certd/pipeline';
import { StorageService } from './storage-service.js';
import { IStorage } from "@certd/pipeline";
import { StorageService } from "./storage-service.js";
export class DbStorage implements IStorage {
/**
@@ -13,7 +13,7 @@ export class DbStorage implements IStorage {
}
async remove(scope: string, namespace: string, version: string, key: string): Promise<void> {
throw new Error('Method not implemented.');
throw new Error("Method not implemented.");
}
async get(scope: string, namespace: string, version: string, key: string): Promise<string | null> {
@@ -1,5 +1,5 @@
import { Provide } from '@midwayjs/core';
import { dnsProviderRegistry } from '@certd/plugin-cert';
import { Provide } from "@midwayjs/core";
import { dnsProviderRegistry } from "@certd/plugin-cert";
@Provide()
export class DnsProviderService {
@@ -1,7 +1,7 @@
import { CertInfo, CertReader, ICertInfoGetter } from '@certd/plugin-lib';
import { CertInfoService } from '../../../monitor/index.js';
import { CertInfo, CertReader, ICertInfoGetter } from "@certd/plugin-lib";
import { CertInfoService } from "../../../monitor/index.js";
export class CertInfoGetter implements ICertInfoGetter {
export class CertInfoGetter implements ICertInfoGetter {
userId: number;
projectId: number;
certInfoService: CertInfoService;
@@ -12,20 +12,20 @@ export class CertInfoGetter implements ICertInfoGetter {
}
async getByPipelineId(pipelineId: number): Promise<CertInfo> {
if (!pipelineId) {
throw new Error(`流水线id不能为空`)
}
const query :any= {
pipelineId,
userId: this.userId,
throw new Error(`流水线id不能为空`);
}
const query: any = {
pipelineId,
userId: this.userId,
};
if (this.projectId) {
query.projectId = this.projectId
query.projectId = this.projectId;
}
const entity = await this.certInfoService.findOne({
where:query
})
const entity = await this.certInfoService.findOne({
where: query,
});
if (!entity || !entity.certInfo) {
throw new Error(`流水线(${pipelineId})还未生成证书,请先运行一次流水线`)
throw new Error(`流水线(${pipelineId})还未生成证书,请先运行一次流水线`);
}
return new CertReader(JSON.parse(entity.certInfo)).cert;
}
@@ -1,4 +1,4 @@
import { CnameRecord, ICnameProxyService } from '@certd/pipeline';
import { CnameRecord, ICnameProxyService } from "@certd/pipeline";
export class CnameProxyService implements ICnameProxyService {
userId: number;
@@ -1,5 +1,5 @@
import {DomainVerifiers, IDomainVerifierGetter} from "@certd/plugin-cert";
import {DomainService} from "../../../cert/service/domain-service.js";
import { DomainVerifiers, IDomainVerifierGetter } from "@certd/plugin-cert";
import { DomainService } from "../../../cert/service/domain-service.js";
export class DomainVerifierGetter implements IDomainVerifierGetter {
private userId: number;
@@ -12,8 +12,7 @@ export class DomainVerifierGetter implements IDomainVerifierGetter {
this.domainService = domainService;
}
async getVerifiers(domains: string[]): Promise<DomainVerifiers>{
return await this.domainService.getDomainVerifiers(this.userId,this.projectId,domains);
async getVerifiers(domains: string[]): Promise<DomainVerifiers> {
return await this.domainService.getDomainVerifiers(this.userId, this.projectId, domains);
}
}
@@ -1,5 +1,5 @@
import { INotificationService, NotificationSendReq } from '@certd/pipeline';
import {NotificationService} from "../notification-service.js";
import { INotificationService, NotificationSendReq } from "@certd/pipeline";
import { NotificationService } from "../notification-service.js";
export class NotificationGetter implements INotificationService {
userId: number;
@@ -1,20 +1,18 @@
import { SysSettingsService, SysInstallInfo } from "@certd/lib-server";
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { SiteInfo ,ISiteInfoGetter} from "@certd/plugin-lib";
import { SiteInfo, ISiteInfoGetter } from "@certd/plugin-lib";
@Provide("siteInfoGetter")
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class SiteInfoGetter implements ISiteInfoGetter{
@Inject()
sysSettingsService: SysSettingsService;
export class SiteInfoGetter implements ISiteInfoGetter {
@Inject()
sysSettingsService: SysSettingsService;
async getSiteInfo(): Promise<SiteInfo> {
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
async getSiteInfo(): Promise<SiteInfo> {
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
return {
siteUrl: installInfo?.bindUrl || "",
}
}
return {
siteUrl: installInfo?.bindUrl || "",
};
}
}
@@ -19,7 +19,7 @@ export class SubDomainsGetter implements ISubDomainsGetter {
}
async getSubDomains() {
const projectSubDomains = await this.subDomainService.getListByUserId(this.userId, this.projectId) || [];
const projectSubDomains = (await this.subDomainService.getListByUserId(this.userId, this.projectId)) || [];
const cnameProviderSubDomains = await this.cnameProviderService.getSubDomains();
return [...projectSubDomains, ...cnameProviderSubDomains]
.map(item => item?.trim())
@@ -28,8 +28,8 @@ export class SubDomainsGetter implements ISubDomainsGetter {
}
async hasSubDomain(fullDomain: string) {
let arr = fullDomain.split(".")
const subDomains = await this.getSubDomains()
let arr = fullDomain.split(".");
const subDomains = await this.getSubDomains();
if (subDomains && subDomains.length > 0) {
const fullDomainDot = "." + fullDomain;
for (const subDomain of subDomains) {
@@ -41,40 +41,38 @@ export class SubDomainsGetter implements ISubDomainsGetter {
if (subDomain.startsWith("*.")) {
//如果子域名配置的是泛域名,说明这一层及以下的子域名都是托管的
//以fullDomain在这一层的子域名作为返回值
const nonStarDomain = subDomain.slice(1)
const nonStarDomain = subDomain.slice(1);
if (fullDomainDot.endsWith(nonStarDomain)) {
//提取fullDomain在这一层的子域名
const fullArr = arr.reverse()
const subArr = subDomain.split(".").reverse()
let strBuilder = ""
for (let i =0 ;i<subArr.length;i++) {
const fullArr = arr.reverse();
const subArr = subDomain.split(".").reverse();
let strBuilder = "";
for (let i = 0; i < subArr.length; i++) {
if (strBuilder) {
strBuilder = fullArr[i] + "." + strBuilder
strBuilder = fullArr[i] + "." + strBuilder;
} else {
strBuilder = fullArr[i]
strBuilder = fullArr[i];
}
}
return strBuilder
return strBuilder;
}
}
}
}
while (arr.length > 0) {
const subDomain = arr.join(".")
const subDomain = arr.join(".");
const domain = await this.domainService.findOne({
where: {
userId: this.userId,
domain: subDomain,
challengeType: "dns",
}
})
},
});
if (domain) {
return subDomain
return subDomain;
}
arr = arr.slice(1)
arr = arr.slice(1);
}
return null
return null;
}
}
@@ -14,93 +14,87 @@ import { CertInfoService } from "../../../monitor/index.js";
import { ICertInfoGetter } from "@certd/plugin-lib";
import { CnameProviderService } from "../../../cname/service/cname-provider-service.js";
const serviceNames = [
'ocrService',
]
export class TaskServiceGetter implements IServiceGetter{
const serviceNames = ["ocrService"];
export class TaskServiceGetter implements IServiceGetter {
private userId: number;
private projectId: number;
private appCtx : IMidwayContainer;
constructor(userId:number,projectId:number,appCtx:IMidwayContainer) {
private appCtx: IMidwayContainer;
constructor(userId: number, projectId: number, appCtx: IMidwayContainer) {
this.userId = userId;
this.projectId = projectId;
this.appCtx = appCtx
this.appCtx = appCtx;
}
async get<T>(serviceName: string): Promise<T> {
if(serviceName === 'subDomainsGetter'){
return await this.getSubDomainsGetter() as T
} if (serviceName === 'accessService') {
return await this.getAccessService() as T
} else if (serviceName === 'cnameProxyService') {
return await this.getCnameProxyService() as T
} else if (serviceName === 'notificationService') {
return await this.getNotificationService() as T
} else if (serviceName === 'domainVerifierGetter') {
return await this.getDomainVerifierGetter() as T
} else if (serviceName === 'certInfoGetter') {
return await this.getCertInfoGetter() as T
}else{
if(!serviceNames.includes(serviceName)){
throw new Error(`${serviceName} not in whitelist`)
if (serviceName === "subDomainsGetter") {
return (await this.getSubDomainsGetter()) as T;
}
if (serviceName === "accessService") {
return (await this.getAccessService()) as T;
} else if (serviceName === "cnameProxyService") {
return (await this.getCnameProxyService()) as T;
} else if (serviceName === "notificationService") {
return (await this.getNotificationService()) as T;
} else if (serviceName === "domainVerifierGetter") {
return (await this.getDomainVerifierGetter()) as T;
} else if (serviceName === "certInfoGetter") {
return (await this.getCertInfoGetter()) as T;
} else {
if (!serviceNames.includes(serviceName)) {
throw new Error(`${serviceName} not in whitelist`);
}
const service = await this.appCtx.getAsync(serviceName)
if (! service){
throw new Error(`${serviceName} not found`)
const service = await this.appCtx.getAsync(serviceName);
if (!service) {
throw new Error(`${serviceName} not found`);
}
}
}
async getSubDomainsGetter(): Promise<SubDomainsGetter> {
const subDomainsService:SubDomainService = await this.appCtx.getAsync("subDomainService")
const domainService:DomainService = await this.appCtx.getAsync("domainService")
const cnameProviderService:CnameProviderService = await this.appCtx.getAsync("cnameProviderService")
return new SubDomainsGetter(this.userId,this.projectId, subDomainsService,domainService,cnameProviderService)
const subDomainsService: SubDomainService = await this.appCtx.getAsync("subDomainService");
const domainService: DomainService = await this.appCtx.getAsync("domainService");
const cnameProviderService: CnameProviderService = await this.appCtx.getAsync("cnameProviderService");
return new SubDomainsGetter(this.userId, this.projectId, subDomainsService, domainService, cnameProviderService);
}
async getCertInfoGetter(): Promise<ICertInfoGetter> {
const certInfoService:CertInfoService = await this.appCtx.getAsync("certInfoService")
return new CertInfoGetter(this.userId, this.projectId, certInfoService)
const certInfoService: CertInfoService = await this.appCtx.getAsync("certInfoService");
return new CertInfoGetter(this.userId, this.projectId, certInfoService);
}
async getAccessService(): Promise<AccessGetter> {
const accessService:AccessService = await this.appCtx.getAsync("accessService")
const accessService: AccessService = await this.appCtx.getAsync("accessService");
return new AccessGetter(this.userId, this.projectId, accessService.getById.bind(accessService));
}
async getCnameProxyService(): Promise<CnameProxyService> {
const cnameRecordService:CnameRecordService = await this.appCtx.getAsync("cnameRecordService")
const cnameRecordService: CnameRecordService = await this.appCtx.getAsync("cnameRecordService");
return new CnameProxyService(this.userId, this.projectId, cnameRecordService.getWithAccessByDomain.bind(cnameRecordService));
}
async getNotificationService(): Promise<NotificationGetter> {
const notificationService:NotificationService = await this.appCtx.getAsync("notificationService")
const notificationService: NotificationService = await this.appCtx.getAsync("notificationService");
return new NotificationGetter(this.userId, this.projectId, notificationService);
}
async getDomainVerifierGetter(): Promise<DomainVerifierGetter> {
const domainService:DomainService = await this.appCtx.getAsync("domainService")
const domainService: DomainService = await this.appCtx.getAsync("domainService");
return new DomainVerifierGetter(this.userId, this.projectId, domainService);
}
}
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class TaskServiceBuilder {
export class TaskServiceBuilder {
@ApplicationContext()
appCtx: IMidwayContainer;
create(req:TaskServiceCreateReq){
create(req: TaskServiceCreateReq) {
const userId = req.userId;
const projectId = req.projectId;
return new TaskServiceGetter(userId,projectId,this.appCtx)
return new TaskServiceGetter(userId, projectId, this.appCtx);
}
}
export type TaskServiceCreateReq = {
userId: number;
projectId?: number;
}
};
@@ -1,8 +1,8 @@
import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { In, Repository } from 'typeorm';
import { BaseService } from '@certd/lib-server';
import { HistoryLogEntity } from '../entity/history-log.js';
import { Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { InjectEntityModel } from "@midwayjs/typeorm";
import { In, Repository } from "typeorm";
import { BaseService } from "@certd/lib-server";
import { HistoryLogEntity } from "../entity/history-log.js";
/**
*
@@ -1,16 +1,16 @@
import { Config, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { In, MoreThan, Repository } from 'typeorm';
import { BaseService, PageReq } from '@certd/lib-server';
import { HistoryEntity } from '../entity/history.js';
import { PipelineEntity } from '../entity/pipeline.js';
import { HistoryDetail } from '../entity/vo/history-detail.js';
import { HistoryLogService } from './history-log-service.js';
import { FileItem, FileStore, Pipeline, RunnableCollection } from '@certd/pipeline';
import { Config, Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { InjectEntityModel } from "@midwayjs/typeorm";
import { In, MoreThan, Repository } from "typeorm";
import { BaseService, PageReq } from "@certd/lib-server";
import { HistoryEntity } from "../entity/history.js";
import { PipelineEntity } from "../entity/pipeline.js";
import { HistoryDetail } from "../entity/vo/history-detail.js";
import { HistoryLogService } from "./history-log-service.js";
import { FileItem, FileStore, Pipeline, RunnableCollection } from "@certd/pipeline";
import dayjs from 'dayjs';
import { DbAdapter } from '../../db/index.js';
import { logger } from '@certd/basic';
import dayjs from "dayjs";
import { DbAdapter } from "../../db/index.js";
import { logger } from "@certd/basic";
/**
*
@@ -29,7 +29,7 @@ export class HistoryService extends BaseService<HistoryEntity> {
@Inject()
dbAdapter: DbAdapter;
@Config('certd')
@Config("certd")
private certdConfig: any;
//@ts-ignore
@@ -60,12 +60,12 @@ export class HistoryService extends BaseService<HistoryEntity> {
return new HistoryDetail(entity, log);
}
async start(pipeline: PipelineEntity,triggerType:string) {
async start(pipeline: PipelineEntity, triggerType: string) {
const bean = {
userId: pipeline.userId,
pipelineId: pipeline.id,
title: pipeline.title,
status: 'start',
status: "start",
triggerType,
projectId: pipeline.projectId,
};
@@ -104,7 +104,7 @@ export class HistoryService extends BaseService<HistoryEntity> {
pipelineId,
},
order: {
id: 'ASC',
id: "ASC",
},
skip: 0,
take: deleteCountBatch,
@@ -130,7 +130,7 @@ export class HistoryService extends BaseService<HistoryEntity> {
pipelineId,
},
order: {
id: 'DESC',
id: "DESC",
},
});
}
@@ -139,7 +139,7 @@ export class HistoryService extends BaseService<HistoryEntity> {
const status: Pipeline = JSON.parse(history.pipeline);
const files: FileItem[] = [];
RunnableCollection.each([status], runnable => {
if (runnable.runnableType !== 'step') {
if (runnable.runnableType !== "step") {
return;
}
if (runnable.status?.files != null) {
@@ -174,32 +174,32 @@ export class HistoryService extends BaseService<HistoryEntity> {
try {
const fileStore = new FileStore({
rootDir: this.certdConfig.fileRootDir,
scope: id + '',
parent: '0',
scope: id + "",
parent: "0",
});
fileStore.deleteByParent(id + '', '');
fileStore.deleteByParent(id + "", "");
} catch (e) {
logger.error('删除文件失败', e);
logger.error("删除文件失败", e);
}
}
async countPerDay(param: { days: number; userId?: any,projectId?:number }) {
const todayEnd = dayjs().endOf('day');
async countPerDay(param: { days: number; userId?: any; projectId?: number }) {
const todayEnd = dayjs().endOf("day");
const where: any = {
createTime: MoreThan(todayEnd.add(-param.days, 'day').toDate()),
createTime: MoreThan(todayEnd.add(-param.days, "day").toDate()),
};
if (param.projectId > 0) {
where.projectId = param.projectId;
}else if (param.userId > 0) {
} else if (param.userId > 0) {
where.userId = param.userId;
}
const result = await this.getRepository()
.createQueryBuilder('main')
.select(`${this.dbAdapter.date('main.createTime')} AS date`) // 将UNIX时间戳转换为日期
.addSelect('COUNT(1) AS count')
.createQueryBuilder("main")
.select(`${this.dbAdapter.date("main.createTime")} AS date`) // 将UNIX时间戳转换为日期
.addSelect("COUNT(1) AS count")
.where(where)
.groupBy('date')
.groupBy("date")
.getRawMany();
return result;
@@ -1,19 +1,12 @@
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import {
BaseService,
NeedVIPException,
SysInstallInfo,
SysSettingsService,
SysSiteInfo,
ValidateException
} from "@certd/lib-server";
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { NotificationEntity } from '../entity/notification.js';
import { NotificationInstanceConfig, notificationRegistry, NotificationSendReq, sendNotification } from '@certd/pipeline';
import { http, utils } from '@certd/basic';
import { EmailService } from '../../basic/service/email-service.js';
import { isComm, isPlus } from '@certd/plus-core';
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { BaseService, NeedVIPException, SysInstallInfo, SysSettingsService, SysSiteInfo, ValidateException } from "@certd/lib-server";
import { InjectEntityModel } from "@midwayjs/typeorm";
import { Repository } from "typeorm";
import { NotificationEntity } from "../entity/notification.js";
import { NotificationInstanceConfig, notificationRegistry, NotificationSendReq, sendNotification } from "@certd/pipeline";
import { http, utils } from "@certd/basic";
import { EmailService } from "../../basic/service/email-service.js";
import { isComm, isPlus } from "@certd/plus-core";
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
@@ -35,7 +28,7 @@ export class NotificationService extends BaseService<NotificationEntity> {
async getSimpleInfo(id: number) {
const entity = await this.info(id);
if (entity == null) {
throw new ValidateException('该通知配置不存在,请确认是否已被删除');
throw new ValidateException("该通知配置不存在,请确认是否已被删除");
}
return {
id: entity.id,
@@ -56,32 +49,31 @@ export class NotificationService extends BaseService<NotificationEntity> {
async add(bean: NotificationEntity) {
this.checkNeedPlus(bean.type);
const res = await super.add(bean);
if(bean.isDefault){
if (bean.isDefault) {
await this.setDefault(res.id, bean.userId);
}
bean.keyId = "nt_" + utils.id.simpleNanoId();
return res
return res;
}
async update(bean: NotificationEntity) {
const old = await this.info(bean.id);
this.checkNeedPlus(old.type);
delete bean.userId;
delete bean.type
delete bean.keyId
delete bean.type;
delete bean.keyId;
const res = await super.update(bean);
if(bean.isDefault){
if (bean.isDefault) {
await this.setDefault(bean.id, old.userId);
}
return res
return res;
}
checkNeedPlus(type: string){
const define = this.getDefineByType(type)
//@ts-ignore
checkNeedPlus(type: string) {
const define = this.getDefineByType(type);
//@ts-ignore
if (define.needPlus && !isPlus()) {
throw new NeedVIPException("此通知类型为Certd专业版功能,请升级到专业版或以上级别");
}
@@ -89,10 +81,10 @@ export class NotificationService extends BaseService<NotificationEntity> {
async getById(id: number, userId: number, projectId?: number): Promise<NotificationInstanceConfig> {
if (!id) {
throw new ValidateException('id不能为空');
throw new ValidateException("id不能为空");
}
if (userId==null) {
throw new ValidateException('userId不能为空');
if (userId == null) {
throw new ValidateException("userId不能为空");
}
const res = await this.repository.findOne({
where: {
@@ -125,7 +117,7 @@ export class NotificationService extends BaseService<NotificationEntity> {
projectId,
},
order: {
isDefault: 'DESC',
isDefault: "DESC",
},
});
if (!res) {
@@ -136,30 +128,24 @@ export class NotificationService extends BaseService<NotificationEntity> {
async setDefault(id: number, userId: number, projectId?: number) {
if (!id) {
throw new ValidateException('id不能为空');
throw new ValidateException("id不能为空");
}
if (userId==null) {
throw new ValidateException('userId不能为空');
if (userId == null) {
throw new ValidateException("userId不能为空");
}
const query:any = {
userId,
const query: any = {
userId,
};
if (projectId) {
query.projectId = projectId;
}
if (projectId){
query.projectId = projectId
}
await this.repository.update(
query,
{
isDefault: false,
}
);
query.id = id
await this.repository.update(
query,
{
isDefault: true,
}
);
await this.repository.update(query, {
isDefault: false,
});
query.id = id;
await this.repository.update(query, {
isDefault: true,
});
}
async getOrCreateDefault(email: string, userId: any, projectId?: number) {
@@ -172,8 +158,8 @@ export class NotificationService extends BaseService<NotificationEntity> {
};
const res = await this.repository.save({
userId,
type: 'email',
name: '邮件通知',
type: "email",
name: "邮件通知",
setting: JSON.stringify(setting),
isDefault: true,
projectId,
@@ -199,11 +185,11 @@ export class NotificationService extends BaseService<NotificationEntity> {
if (notifyConfig) {
//发送通知
logger.info('发送通知, 使用通知渠道:' + notifyConfig.name);
logger.info("发送通知, 使用通知渠道:" + notifyConfig.name);
if (notifyConfig.type != 'email') {
if (notifyConfig.type != "email") {
//非邮件通知,需要加上站点名称
let siteTitle = 'Certd';
let siteTitle = "Certd";
if (isComm()) {
const siteInfo = await this.sysSettingsService.getSetting<SysSiteInfo>(SysSiteInfo);
siteTitle = siteInfo?.title || siteTitle;
@@ -223,7 +209,7 @@ export class NotificationService extends BaseService<NotificationEntity> {
});
} else {
if (req.useEmail && req.emailAddress) {
logger.info('使用邮件通知');
logger.info("使用邮件通知");
await this.emailService.send({
receivers: [req.emailAddress],
subject: req.body.title,
@@ -235,7 +221,7 @@ export class NotificationService extends BaseService<NotificationEntity> {
async getBindUrl(path: string) {
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
const bindUrl = installInfo.bindUrl || 'http://127.0.0.1:7001';
const bindUrl = installInfo.bindUrl || "http://127.0.0.1:7001";
return bindUrl + path;
}
}
@@ -115,7 +115,10 @@ describe("pipeline batch update", () => {
},
};
assert.equal(updateCertApplyStepInputs(pipeline, {}, stepType => inputDefines[stepType]), 0);
assert.equal(
updateCertApplyStepInputs(pipeline, {}, stepType => inputDefines[stepType]),
0
);
assert.equal(
updateCertApplyStepInputs(
pipeline,
@@ -1,9 +1,9 @@
import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { BaseService } from '@certd/lib-server';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { PipelineGroupEntity } from '../entity/pipeline-group.js';
import { merge } from 'lodash-es';
import { Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { BaseService } from "@certd/lib-server";
import { InjectEntityModel } from "@midwayjs/typeorm";
import { Repository } from "typeorm";
import { PipelineGroupEntity } from "../entity/pipeline-group.js";
import { merge } from "lodash-es";
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
@@ -1,32 +1,10 @@
import { Config, Inject, Provide, Scope, ScopeEnum, sleep } from "@midwayjs/core";
import { InjectEntityModel } from "@midwayjs/typeorm";
import { In, MoreThan, Repository } from "typeorm";
import {
AccessService,
BaseService,
isEnterprise,
NeedSuiteException,
NeedVIPException,
PageReq,
SysPublicSettings,
SysSettingsService,
SysSiteInfo
} from "@certd/lib-server";
import { AccessService, BaseService, isEnterprise, NeedSuiteException, NeedVIPException, PageReq, SysPublicSettings, SysSettingsService, SysSiteInfo } from "@certd/lib-server";
import { PipelineEntity } from "../entity/pipeline.js";
import { PipelineDetail } from "../entity/vo/pipeline-detail.js";
import {
Executor,
IAccessService,
ICnameProxyService,
INotificationService, Notification,
Pipeline,
pluginRegistry,
ResultType,
RunHistory,
RunnableCollection,
SysInfo,
UserInfo
} from "@certd/pipeline";
import { Executor, IAccessService, ICnameProxyService, INotificationService, Notification, Pipeline, pluginRegistry, ResultType, RunHistory, RunnableCollection, SysInfo, UserInfo } from "@certd/pipeline";
import { DbStorage } from "./db-storage.js";
import { StorageService } from "./storage-service.js";
import { Cron } from "../../cron/cron.js";
@@ -56,7 +34,6 @@ import { CertApplyStepInputPatch, updateCertApplyStepInputs } from "./pipeline-b
import { calcNextSuiteCountUsed } from "./pipeline-suite-limit.js";
const runningTasks: Map<string | number, Executor> = new Map();
/**
*
*/
@@ -154,9 +131,9 @@ export class PipelineService extends BaseService<PipelineEntity> {
//获取下次执行时间
if (pipeline.triggers?.length > 0) {
const triggers = pipeline.triggers.filter((item) => item.type === 'timer');
const triggers = pipeline.triggers.filter(item => item.type === "timer");
if (triggers && triggers.length > 0) {
let nextTimes: any = [];
const nextTimes: any = [];
for (const item of triggers) {
if (!item.props?.cron) {
continue;
@@ -164,9 +141,8 @@ export class PipelineService extends BaseService<PipelineEntity> {
const ret = this.getCronNextTimes(item.props?.cron, 1);
nextTimes.push(...ret);
}
item.nextRunTime = nextTimes[0]
item.nextRunTime = nextTimes[0];
}
}
delete item.content;
@@ -175,7 +151,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
return result;
}
getCronNextTimes(cron: string, count: number = 1) {
getCronNextTimes(cron: string, count = 1) {
if (cron == null) {
return [];
}
@@ -188,7 +164,6 @@ export class PipelineService extends BaseService<PipelineEntity> {
return nextTimes;
}
private async fillLastVars(records: PipelineEntity[]) {
const pipelineIds: number[] = [];
const recordMap = {};
@@ -235,8 +210,6 @@ export class PipelineService extends BaseService<PipelineEntity> {
}
}
/**
*
* @param id
@@ -270,7 +243,6 @@ export class PipelineService extends BaseService<PipelineEntity> {
const isUpdate = bean.id > 0 && old != null;
const pipeline = JSON.parse(bean.content || "{}");
RunnableCollection.initPipelineRunnableType(pipeline);
pipeline.userId = bean.userId;
@@ -334,12 +306,12 @@ export class PipelineService extends BaseService<PipelineEntity> {
}
pipeline.version++;
bean.triggerCount = pipeline.triggers?.filter((trigger) => trigger.type === "timer").length || 0;
bean.triggerCount = pipeline.triggers?.filter(trigger => trigger.type === "timer").length || 0;
bean.content = JSON.stringify(pipeline);
await this.addOrUpdate(bean);
await this.registerTrigger(bean);
return bean
return bean;
}
private async checkMaxPipelineCount(bean: PipelineEntity, pipeline: Pipeline, domains: string[], old?: PipelineEntity) {
@@ -351,7 +323,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
// }
if (isEnterprise()) {
//企业模式不限制
checkPlus()
checkPlus();
return;
}
@@ -397,18 +369,17 @@ export class PipelineService extends BaseService<PipelineEntity> {
}
}
}
}
async foreachPipeline(callback: (pipeline: PipelineEntity) => void) {
const idEntityList = await this.repository.find({
select: {
id: true
id: true,
},
where: {
disabled: false,
isTemplate: false
}
isTemplate: false,
},
});
const ids = idEntityList.map(item => {
return item.id;
@@ -428,7 +399,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
//分段加载记录
for (const idArr of idsSpan) {
const list = await this.repository.findBy({
id: In(idArr)
id: In(idArr),
});
for (const entity of list) {
@@ -478,12 +449,9 @@ export class PipelineService extends BaseService<PipelineEntity> {
} catch (e) {
logger.error(e);
}
}
}
async trigger(id: any, stepId?: string, doCheck = false) {
const entity: PipelineEntity = await this.info(id);
if (doCheck) {
@@ -499,7 +467,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
} catch (e) {
logger.error("手动job执行失败:", e);
}
}
},
});
}
@@ -511,7 +479,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
logger.error(e.message);
await this.update({
id: pipelineId,
status: "no_deploy_count"
status: "no_deploy_count",
});
}
throw e;
@@ -595,22 +563,21 @@ export class PipelineService extends BaseService<PipelineEntity> {
} catch (e) {
logger.error("定时job执行失败:", e);
}
}
},
});
}
},
});
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 false;
}
}
return true
return true;
}
/**
@@ -629,13 +596,12 @@ export class PipelineService extends BaseService<PipelineEntity> {
}
async beforeCheck(entity: PipelineEntity) {
if (isEnterprise()) {
checkPlus()
return {}
checkPlus();
return {};
}
const validTimeEnabled = await this.isPipelineValidTimeEnabled(entity)
const validTimeEnabled = await this.isPipelineValidTimeEnabled(entity);
if (!validTimeEnabled) {
throw new Error(`流水线${entity.id}已过期,不予执行`);
}
@@ -647,16 +613,15 @@ export class PipelineService extends BaseService<PipelineEntity> {
await this.checkUserStatus(entity.userId);
return {
suite
}
suite,
};
}
async doRun(entity: PipelineEntity, triggerId: string, stepId?: string) {
let suite: any = null
let suite: any = null;
try {
const res = await this.beforeCheck(entity);
suite = res.suite
suite = res.suite;
} catch (e) {
logger.error(`流水线${entity.id}触发失败(${triggerId}):${e.message}`);
return;
@@ -668,7 +633,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
pipeline.id = id;
}
if(entity.userId !=null){
if (entity.userId != null) {
pipeline.userId = entity.userId;
pipeline.projectId = entity.projectId;
}
@@ -688,7 +653,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
return;
}
}
const doSaveHistory = async (history: RunHistory) => {
//保存执行历史
try {
@@ -707,8 +672,8 @@ export class PipelineService extends BaseService<PipelineEntity> {
class HistorySaver {
latest: RunHistory = null;
interval: any = null;
started: boolean = false;
async save(){
started = false;
async save() {
const latest = this.latest;
this.latest = null;
if (latest == null) {
@@ -716,43 +681,43 @@ export class PipelineService extends BaseService<PipelineEntity> {
}
await doSaveHistory(latest);
}
async start(){
this.started = true
async start() {
this.started = true;
//先存一次,确保有数据
await this.save();
setTimeout(()=>{
setTimeout(() => {
//2秒后保存一次,尽快显示第一个任务的状态
this.save();
this.save();
}, 1000 * 2);
this.interval = setInterval(()=>{
this.interval = setInterval(() => {
//之后每5秒保存一次
this.save();
}, 1000 * 5);
}
async push(history: RunHistory){
async push(history: RunHistory) {
this.latest = history;
if(!this.started){
await this.start();
if (!this.started) {
await this.start();
}
}
async done(){
async done() {
clearInterval(this.interval);
await this.save();
}
}
const historySaver = new HistorySaver();
const onChanged = async (history: RunHistory)=>{
const onChanged = async (history: RunHistory) => {
await historySaver.push(history);
}
const onFinished = async (history: RunHistory)=>{
};
const onFinished = async (history: RunHistory) => {
await onChanged(history);
await historySaver.done();
}
};
const userId = entity.userId;
const projectId = entity.projectId;
let userIsAdmin = false
let userIsAdmin = false;
if (projectId && projectId > 0) {
userIsAdmin = await this.projectService.isAdmin(projectId);
@@ -761,7 +726,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
}
const user: UserInfo = {
id: userId,
role: userIsAdmin ? "admin" : "user"
role: userIsAdmin ? "admin" : "user",
};
const historyId = await this.historyService.start(entity, triggerType);
@@ -773,7 +738,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
const taskServiceGetter = this.taskServiceBuilder.create({
userId,
projectId
projectId,
});
const accessGetter = await taskServiceGetter.get<IAccessService>("accessService");
const notificationGetter = await taskServiceGetter.get<INotificationService>("notificationService");
@@ -792,7 +757,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
notificationService: notificationGetter,
fileRootDir: this.certdConfig.fileRootDir,
sysInfo,
serviceGetter: taskServiceGetter
serviceGetter: taskServiceGetter,
});
try {
runningTasks.set(historyId, executor);
@@ -875,13 +840,13 @@ export class PipelineService extends BaseService<PipelineEntity> {
},
});
if (!pipelineEntity) {
return null
return null;
}
return pipelineEntity.projectId;
}
private async saveHistory(history: RunHistory) {
//修改pipeline状态
let pipelineEntity = new PipelineEntity();
const pipelineEntity = new PipelineEntity();
pipelineEntity.id = parseInt(history.pipeline.id);
pipelineEntity.status = history.pipeline.status.result + "";
pipelineEntity.lastHistoryTime = history.pipeline.status.startTime;
@@ -909,18 +874,18 @@ export class PipelineService extends BaseService<PipelineEntity> {
await this.historyLogService.addOrUpdate(logEntity);
}
async count(param: { userId?: any, projectId?: number }) {
async count(param: { userId?: any; projectId?: number }) {
const count = await this.repository.count({
where: {
userId: param.userId,
projectId: param.projectId,
isTemplate: false
}
isTemplate: false,
},
});
return count;
}
async statusCount(param: { userId?: any, projectId?: number } = {}) {
async statusCount(param: { userId?: any; projectId?: number } = {}) {
const statusCount = await this.repository
.createQueryBuilder()
.select("status")
@@ -928,14 +893,14 @@ export class PipelineService extends BaseService<PipelineEntity> {
.where({
userId: param.userId,
projectId: param.projectId,
isTemplate: false
isTemplate: false,
})
.groupBy("status")
.getRawMany();
return statusCount;
}
async enableCount(param: { userId?: any, projectId?: number } = {}) {
async enableCount(param: { userId?: any; projectId?: number } = {}) {
const statusCount = await this.repository
.createQueryBuilder()
.select("disabled")
@@ -943,7 +908,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
.where({
userId: param.userId,
projectId: param.projectId,
isTemplate: false
isTemplate: false,
})
.groupBy("disabled")
.getRawMany();
@@ -962,14 +927,14 @@ export class PipelineService extends BaseService<PipelineEntity> {
select: {
id: true,
title: true,
status: true
status: true,
},
where: {
userId,
disabled: false,
projectId,
isTemplate: false
}
isTemplate: false,
},
});
await this.fillLastVars(list);
list = list.filter(item => {
@@ -991,7 +956,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
.where({
// 0点
createTime: MoreThan(todayEnd.add(-param.days, "day").toDate()),
isTemplate: false
isTemplate: false,
})
.groupBy("date")
.getRawMany();
@@ -1008,7 +973,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
await this.checkUserId(id, userId);
}
if (projectId) {
await this.checkUserId(id, projectId, "projectId")
await this.checkUserId(id, projectId, "projectId");
}
await this.delete(id);
}
@@ -1018,7 +983,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
if (!isPlus()) {
throw new NeedVIPException("此功能需要升级Certd专业版");
}
const query: any = {}
const query: any = {};
if (userId && userId > 0) {
query.userId = userId;
}
@@ -1028,15 +993,12 @@ export class PipelineService extends BaseService<PipelineEntity> {
await this.repository.update(
{
id: In(ids),
...query
...query,
},
{ groupId }
);
}
/**
*
*/
@@ -1050,7 +1012,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
if (!projectId || projectId <= 0) {
throw new Error("projectId不能为空");
}
const userId = -1 // 强制为-1
const userId = -1; // 强制为-1
async function eachSteps(pipeline, callback) {
for (const stage of pipeline.stages) {
@@ -1062,7 +1024,6 @@ export class PipelineService extends BaseService<PipelineEntity> {
}
}
for (const id of ids) {
const pipelineEntity = await this.info(id);
if (!pipelineEntity) {
@@ -1080,16 +1041,16 @@ export class PipelineService extends BaseService<PipelineEntity> {
userId: userId,
projectId: projectId,
groupId: null,
}
};
const pipeline = JSON.parse(pipelineEntity.content);
pipeline.userId = userId;
pipeline.projectId = projectId;
//转移和修改access 和 Notification
await eachSteps(pipeline, async (step) => {
await eachSteps(pipeline, async step => {
const type = step.type;
//plugin
//plugin
const pluginDefine: any = pluginRegistry.getDefine(type);
if (pluginDefine) {
for (const key in step.input) {
@@ -1097,48 +1058,40 @@ export class PipelineService extends BaseService<PipelineEntity> {
if (!value || value <= 0) {
continue;
}
if (!pluginDefine.input[key]){
if (!pluginDefine.input[key]) {
continue;
}
const componentName = pluginDefine.input[key].component?.name;
if (componentName === "access-selector" || componentName === "AccessSelector") {
//这是一个授权ID属性,检查是否需要转移授权
const newAccessId = await this.accessService.copyTo(value,projectId);
const newAccessId = await this.accessService.copyTo(value, projectId);
step.input[key] = newAccessId;
}
}
}
})
pipeline.notifications = [
});
(pipeline.notifications = [
{
"type": "custom",
"when": [
"error",
"turnToSuccess"
],
"notificationId": 0,
"title": "使用默认通知",
"id": nanoid()
}
],
entity.content = JSON.stringify(pipeline);
type: "custom",
when: ["error", "turnToSuccess"],
notificationId: 0,
title: "使用默认通知",
id: nanoid(),
},
]),
(entity.content = JSON.stringify(pipeline));
await this.unregisterTriggers(entity.id);
await this.repository.save(entity);
await this.save(entity)
await this.save(entity);
}
}
async batchUpdateTrigger(ids: number[], trigger: any, userId: any, projectId?: number) {
if (!isPlus()) {
throw new NeedVIPException("此功能需要升级Certd专业版");
}
//允许管理员修改,userId=null
const query: any = {}
const query: any = {};
if (userId && userId > 0) {
query.userId = userId;
}
@@ -1148,15 +1101,15 @@ export class PipelineService extends BaseService<PipelineEntity> {
const list = await this.find({
where: {
id: In(ids),
...query
}
...query,
},
});
for (const item of list) {
const pipeline = JSON.parse(item.content);
if (trigger.props === false) {
//清除trigger
pipeline.triggers = []
pipeline.triggers = [];
} else {
if (trigger.random === true) {
//随机时间
@@ -1170,20 +1123,21 @@ export class PipelineService extends BaseService<PipelineEntity> {
const endTime = dayjs(end).valueOf();
const randomTime = Math.floor(Math.random() * (endTime - startTime)) + startTime;
const time = dayjs(randomTime).format(" ss:mm:HH").replaceAll(":", " ").replaceAll(" 0", " ").trim();
set(trigger, "props.cron", `${time} * * *`)
set(trigger, "props.cron", `${time} * * *`);
}
delete trigger.random
delete trigger.random;
delete trigger.randomRange;
pipeline.triggers = [{
id: nanoid(),
title: "定时触发",
...trigger
}];
pipeline.triggers = [
{
id: nanoid(),
title: "定时触发",
...trigger,
},
];
}
await this.doUpdatePipelineJson(item, pipeline);
}
}
async batchUpdateNotifications(ids: number[], notification: Notification, userId: any, projectId?: number) {
@@ -1191,7 +1145,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
throw new NeedVIPException("此功能需要升级Certd专业版");
}
//允许管理员修改,userId=null
const query: any = {}
const query: any = {};
if (userId && userId > 0) {
query.userId = userId;
}
@@ -1201,26 +1155,28 @@ export class PipelineService extends BaseService<PipelineEntity> {
const list = await this.find({
where: {
id: In(ids),
...query
}
...query,
},
});
for (const item of list) {
const pipeline = JSON.parse(item.content);
pipeline.notifications = [{
id: nanoid(),
title: "通知",
/**
* type: NotificationType;
* when: NotificationWhen[];
* options: EmailOptions;
* notificationId: number;
* title: string;
* subType: string;
*/
type: "other",
...notification
}];
pipeline.notifications = [
{
id: nanoid(),
title: "通知",
/**
* type: NotificationType;
* when: NotificationWhen[];
* options: EmailOptions;
* notificationId: number;
* title: string;
* subType: string;
*/
type: "other",
...notification,
},
];
await this.doUpdatePipelineJson(item, pipeline);
}
}
@@ -1229,7 +1185,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
if (!isPlus()) {
throw new NeedVIPException("此功能需要升级Certd专业版");
}
const query: any = {}
const query: any = {};
if (userId && userId > 0) {
query.userId = userId;
}
@@ -1239,8 +1195,8 @@ export class PipelineService extends BaseService<PipelineEntity> {
const list = await this.find({
where: {
id: In(ids),
...query
}
...query,
},
});
for (const item of list) {
@@ -1267,15 +1223,15 @@ export class PipelineService extends BaseService<PipelineEntity> {
const where: any = {
id: In(ids),
userId,
}
};
if (projectId) {
where.projectId = projectId
where.projectId = projectId;
}
const list = await this.repository.find({
select: {
id: true
id: true,
},
where: where
where: where,
});
ids = list.map(item => item.id);
@@ -1293,12 +1249,11 @@ export class PipelineService extends BaseService<PipelineEntity> {
} else {
await this.run(id, null);
}
}
},
});
}
}
async getUserPipelineCount(userId) {
return await this.repository.count({ where: { userId } });
}
@@ -1307,21 +1262,20 @@ export class PipelineService extends BaseService<PipelineEntity> {
return await this.repository.find({
select: {
id: true,
title: true
title: true,
},
where: {
id: In(pipelineIds),
userId,
projectId
}
projectId,
},
});
}
private async checkUserStatus(userId: number) {
if (isEnterprise()) {
//企业模式不检查用户状态,都允许运行流水线
return
return;
}
const userEntity = await this.userService.info(userId);
if (userEntity == null) {
@@ -1344,12 +1298,12 @@ export class PipelineService extends BaseService<PipelineEntity> {
}
}
async createAutoPipeline(req: { domains: string[]; email: string; userId: number, projectId?: number, from: string }) {
async createAutoPipeline(req: { domains: string[]; email: string; userId: number; projectId?: number; from: string }) {
const randomHour = Math.floor(Math.random() * 6);
const randomMin = Math.floor(Math.random() * 60);
const randomCron = `0 ${randomMin} ${randomHour} * * *`;
let pipeline: any = {
const pipeline: any = {
title: req.domains[0] + `证书自动申请【${req.from ?? "OpenAPI"}`,
runnableType: "pipeline",
triggers: [
@@ -1359,8 +1313,8 @@ export class PipelineService extends BaseService<PipelineEntity> {
props: {
cron: randomCron,
},
type: "timer"
}
type: "timer",
},
],
notifications: [
{
@@ -1369,7 +1323,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
when: ["error", "turnToSuccess", "success"],
notificationId: 0,
title: "默认通知",
}
},
],
stages: [
{
@@ -1391,28 +1345,28 @@ export class PipelineService extends BaseService<PipelineEntity> {
renewDays: 20,
domains: req.domains,
email: req.email,
"challengeType": "auto",
"sslProvider": "letsencrypt",
"privateKeyType": "rsa_2048",
"certProfile": "classic",
"preferredChain": "ISRG Root X1",
"useProxy": false,
"skipLocalVerify": false,
"maxCheckRetryCount": 20,
"waitDnsDiffuseTime": 30,
"pfxArgs": "-macalg SHA1 -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES",
"successNotify": true
challengeType: "auto",
sslProvider: "letsencrypt",
privateKeyType: "rsa_2048",
certProfile: "classic",
preferredChain: "ISRG Root X1",
useProxy: false,
skipLocalVerify: false,
maxCheckRetryCount: 20,
waitDnsDiffuseTime: 30,
pfxArgs: "-macalg SHA1 -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES",
successNotify: true,
},
strategy: {
runStrategy: 0 // 正常执行
runStrategy: 0, // 正常执行
},
type: "CertApply"
}
]
}
]
}
]
type: "CertApply",
},
],
},
],
},
],
};
const bean = new PipelineEntity();
@@ -1421,11 +1375,10 @@ export class PipelineService extends BaseService<PipelineEntity> {
bean.userId = req.userId;
bean.status = "none";
bean.type = "cert_auto";
bean.disabled = false
bean.keepHistoryCount = 30
bean.projectId = req.projectId
await this.save(bean)
bean.disabled = false;
bean.keepHistoryCount = 30;
bean.projectId = req.projectId;
await this.save(bean);
return bean;
}
@@ -1433,11 +1386,11 @@ export class PipelineService extends BaseService<PipelineEntity> {
async getStatus(pipelineId: number) {
const res = await this.repository.findOne({
select: {
status: true
status: true,
},
where: {
id: pipelineId
}
id: pipelineId,
},
});
return res?.status;
}
@@ -1445,11 +1398,11 @@ export class PipelineService extends BaseService<PipelineEntity> {
async getPipelineUserId(pipelineId: number) {
const res = await this.repository.findOne({
select: {
userId: true
userId: true,
},
where: {
id: pipelineId
}
id: pipelineId,
},
});
return res?.userId;
}
@@ -1474,9 +1427,9 @@ export class PipelineService extends BaseService<PipelineEntity> {
content: true,
},
where: {
webhookKey
}
})
webhookKey,
},
});
if (!pipelineEntity) {
throw new Error("webhookKey不存在");
}
@@ -1,8 +1,8 @@
import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { In, Repository } from 'typeorm';
import { BaseService } from '@certd/lib-server';
import { StorageEntity } from '../entity/storage.js';
import { Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { InjectEntityModel } from "@midwayjs/typeorm";
import { In, Repository } from "typeorm";
import { BaseService } from "@certd/lib-server";
import { StorageEntity } from "../entity/storage.js";
/**
*/
@@ -19,7 +19,7 @@ export class StorageService extends BaseService<StorageEntity> {
async get(where: { scope: any; namespace: any; userId: number; version: string; key: string }) {
if (where.userId == null) {
throw new Error('userId 不能为空');
throw new Error("userId 不能为空");
}
return await this.repository.findOne({
where,
@@ -34,7 +34,7 @@ export class StorageService extends BaseService<StorageEntity> {
if (ret != null) {
entity.id = ret.id;
if (ret.userId !== entity.userId) {
throw new Error('您没有权限修改此数据');
throw new Error("您没有权限修改此数据");
}
await this.repository.save(entity);
} else {
@@ -49,9 +49,9 @@ export class StorageService extends BaseService<StorageEntity> {
}
return await this.repository.find({
where: {
scope: 'pipeline',
scope: "pipeline",
namespace: In(pipelineIds),
key: 'vars',
key: "vars",
},
});
}
@@ -62,9 +62,9 @@ export class StorageService extends BaseService<StorageEntity> {
}
const res = await this.repository.findOne({
where: {
scope: 'pipeline',
namespace: pipelineId + '',
key: 'privateVars',
scope: "pipeline",
namespace: pipelineId + "",
key: "privateVars",
},
});
if (!res) {
@@ -1,9 +1,9 @@
import {Inject, Provide, Scope, ScopeEnum} from '@midwayjs/core';
import {BaseService, SysSettingsService} from '@certd/lib-server';
import {InjectEntityModel} from '@midwayjs/typeorm';
import {Repository} from 'typeorm';
import {SubDomainEntity} from '../entity/sub-domain.js';
import {EmailService} from '../../basic/service/email-service.js';
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { BaseService, SysSettingsService } from "@certd/lib-server";
import { InjectEntityModel } from "@midwayjs/typeorm";
import { Repository } from "typeorm";
import { SubDomainEntity } from "../entity/sub-domain.js";
import { EmailService } from "../../basic/service/email-service.js";
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
@@ -22,8 +22,8 @@ export class SubDomainService extends BaseService<SubDomainEntity> {
return this.repository;
}
async getListByUserId(userId:number, projectId?: number):Promise<string[]>{
if (userId==null) {
async getListByUserId(userId: number, projectId?: number): Promise<string[]> {
if (userId == null) {
return [];
}
const list = await this.find({
@@ -34,16 +34,16 @@ export class SubDomainService extends BaseService<SubDomainEntity> {
},
});
return list.map(item=>item.domain);
return list.map(item => item.domain);
}
async add(bean: SubDomainEntity) {
const {domain, userId, projectId} = bean;
const { domain, userId, projectId } = bean;
if (!domain) {
throw new Error('域名不能为空');
throw new Error("域名不能为空");
}
if (userId==null) {
throw new Error('用户ID不能为空');
if (userId == null) {
throw new Error("用户ID不能为空");
}
const exist = await this.repository.findOne({
where: {
@@ -52,10 +52,9 @@ export class SubDomainService extends BaseService<SubDomainEntity> {
projectId,
},
});
if (exist) {
throw new Error('域名已存在');
}
return await super.add(bean)
if (exist) {
throw new Error("域名已存在");
}
return await super.add(bean);
}
}
@@ -1,15 +1,15 @@
import {Inject, Provide, Scope, ScopeEnum} from '@midwayjs/core';
import {BaseService, SysSettingsService} from '@certd/lib-server';
import {InjectEntityModel} from '@midwayjs/typeorm';
import {In, Repository} from 'typeorm';
import {TemplateEntity} from '../entity/template.js';
import {PipelineService} from './pipeline-service.js';
import {cloneDeep} from "lodash-es";
import {PipelineEntity} from "../entity/pipeline.js";
import {Pipeline} from "@certd/pipeline";
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { BaseService, SysSettingsService } from "@certd/lib-server";
import { InjectEntityModel } from "@midwayjs/typeorm";
import { In, Repository } from "typeorm";
import { TemplateEntity } from "../entity/template.js";
import { PipelineService } from "./pipeline-service.js";
import { cloneDeep } from "lodash-es";
import { PipelineEntity } from "../entity/pipeline.js";
import { Pipeline } from "@certd/pipeline";
@Provide()
@Scope(ScopeEnum.Request, {allowDowngrade: true})
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class TemplateService extends BaseService<TemplateEntity> {
@InjectEntityModel(TemplateEntity)
repository: Repository<TemplateEntity>;
@@ -31,82 +31,77 @@ export class TemplateService extends BaseService<TemplateEntity> {
const pipelineEntity = await this.pipelineService.info(pipelineId);
if (!pipelineEntity) {
throw new Error('pipeline not found');
throw new Error("pipeline not found");
}
if (pipelineEntity.userId !== param.userId) {
throw new Error('permission denied');
throw new Error("permission denied");
}
let template = null
let template = null;
await this.transaction(async (tx: any) => {
template = await tx.getRepository(TemplateEntity).save(param);
let newPipeline = cloneDeep(pipelineEntity)
let newPipeline = cloneDeep(pipelineEntity);
//创建pipeline模版
newPipeline.id = undefined;
newPipeline.title = template.title + "模版流水线"
newPipeline.templateId = template.id
newPipeline.isTemplate = true
newPipeline.userId = template.userId
newPipeline.title = template.title + "模版流水线";
newPipeline.templateId = template.id;
newPipeline.isTemplate = true;
newPipeline.userId = template.userId;
const pipelineJson: Pipeline = JSON.parse(newPipeline.content)
delete pipelineJson.triggers
pipelineJson.userId = template.userId
pipelineJson.title = newPipeline.title
newPipeline.content = JSON.stringify(pipelineJson)
newPipeline = await tx.getRepository(PipelineEntity).save(newPipeline)
const pipelineJson: Pipeline = JSON.parse(newPipeline.content);
delete pipelineJson.triggers;
pipelineJson.userId = template.userId;
pipelineJson.title = newPipeline.title;
newPipeline.content = JSON.stringify(pipelineJson);
newPipeline = await tx.getRepository(PipelineEntity).save(newPipeline);
const update: any = {}
update.id = template.id
update.pipelineId = newPipeline.id
await tx.getRepository(TemplateEntity).save(update)
})
return template
const update: any = {};
update.id = template.id;
update.pipelineId = newPipeline.id;
await tx.getRepository(TemplateEntity).save(update);
});
return template;
}
async detail(id: number, userId: number,projectId?:number) {
const info = await this.info(id)
async detail(id: number, userId: number, projectId?: number) {
const info = await this.info(id);
if (!info) {
throw new Error('模板不存在');
throw new Error("模板不存在");
}
if (info.userId !== userId) {
throw new Error('无权限');
throw new Error("无权限");
}
if (projectId && info.projectId !== projectId) {
throw new Error('无权限');
throw new Error("无权限");
}
let pipeline = null
let pipeline = null;
if (info.pipelineId) {
const pipelineEntity = await this.pipelineService.info(info.pipelineId);
pipeline = JSON.parse(pipelineEntity.content)
pipeline = JSON.parse(pipelineEntity.content);
}
return {
template: info,
pipeline,
}
};
}
async batchDelete(ids: number[], userId: number,projectId?:number) {
async batchDelete(ids: number[], userId: number, projectId?: number) {
const where: any = {
id: In(ids),
}
};
if (userId != null) {
where.userId = userId
where.userId = userId;
}
if (projectId) {
where.projectId = projectId
where.projectId = projectId;
}
const list = await this.getRepository().find({where})
ids = list.map(item => item.id)
const pipelineIds = list.map(item => item.pipelineId)
const list = await this.getRepository().find({ where });
ids = list.map(item => item.id);
const pipelineIds = list.map(item => item.pipelineId);
await this.delete(ids);
await this.pipelineService.batchDelete(pipelineIds, userId, projectId)
await this.pipelineService.batchDelete(pipelineIds, userId, projectId);
}
async createPipelineByTemplate(body: PipelineEntity) {
@@ -114,21 +109,20 @@ export class TemplateService extends BaseService<TemplateEntity> {
const template = await this.info(templateId);
if (!template && template.userId !== body.userId) {
throw new Error('模板不存在');
throw new Error("模板不存在");
}
const tempPipeline = await this.pipelineService.info(template.pipelineId)
const tempPipeline = await this.pipelineService.info(template.pipelineId);
const newPipeline = {
type: tempPipeline.type,
from : "template",
from: "template",
keepHistoryCount: tempPipeline.keepHistoryCount,
... body,
}
...body,
};
await this.pipelineService.save(newPipeline)
await this.pipelineService.save(newPipeline);
return newPipeline
return newPipeline;
}
}
@@ -1,6 +1,6 @@
import { IUrlService } from '@certd/pipeline';
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { SysInstallInfo, SysSettingsService } from '@certd/lib-server';
import { IUrlService } from "@certd/pipeline";
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { SysInstallInfo, SysSettingsService } from "@certd/lib-server";
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
@@ -10,7 +10,7 @@ export class UrlService implements IUrlService {
async getPipelineDetailUrl(pipelineId: number, historyId: number): Promise<string> {
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
let baseUrl = 'http://127.0.0.1:7001';
let baseUrl = "http://127.0.0.1:7001";
if (installInfo.bindUrl) {
baseUrl = installInfo.bindUrl;
}
@@ -1,66 +1,65 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity('pi_plugin')
@Entity("pi_plugin")
export class PluginEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'name', comment: 'Key' })
@Column({ name: "name", comment: "Key" })
name: string;
@Column({ name: 'icon', comment: '图标' })
@Column({ name: "icon", comment: "图标" })
icon: string;
@Column({ name: 'title', comment: '标题' })
@Column({ name: "title", comment: "标题" })
title: string;
@Column({ name: 'group', comment: '分组' })
@Column({ name: "group", comment: "分组" })
group: string;
@Column({ name: 'desc', comment: '描述' })
@Column({ name: "desc", comment: "描述" })
desc: string;
@Column({ comment: '配置', length: 40960 })
@Column({ comment: "配置", length: 40960 })
setting: string;
@Column({ name: 'sys_setting', comment: '系统配置', length: 40960 })
@Column({ name: "sys_setting", comment: "系统配置", length: 40960 })
sysSetting: string;
@Column({ comment: '脚本', length: 40960 })
@Column({ comment: "脚本", length: 40960 })
content: string;
@Column({ comment: '类型', length: 100, nullable: true })
@Column({ comment: "类型", length: 100, nullable: true })
type: string; // builtIn | local | download
@Column({ comment: '启用/禁用', default: false })
@Column({ comment: "启用/禁用", default: false })
disabled: boolean;
@Column({ comment: '版本', length: 100, nullable: true })
@Column({ comment: "版本", length: 100, nullable: true })
version: string;
@Column({ comment: '插件类型', length: 100, nullable: true })
@Column({ comment: "插件类型", length: 100, nullable: true })
pluginType: string;
@Column({ comment: '元数据', length: 40960, nullable: true })
@Column({ comment: "元数据", length: 40960, nullable: true })
metadata: string;
@Column({ comment: '额外配置', length: 40960, nullable: true })
@Column({ comment: "额外配置", length: 40960, nullable: true })
extra: string;
@Column({ comment: '作者', length: 100, nullable: true })
@Column({ comment: "作者", length: 100, nullable: true })
author: string;
@Column({
name: 'create_time',
comment: '创建时间',
default: () => 'CURRENT_TIMESTAMP',
name: "create_time",
comment: "创建时间",
default: () => "CURRENT_TIMESTAMP",
})
createTime: Date;
@Column({
name: 'update_time',
comment: '修改时间',
default: () => 'CURRENT_TIMESTAMP',
name: "update_time",
comment: "修改时间",
default: () => "CURRENT_TIMESTAMP",
})
updateTime: Date;
}
@@ -1,6 +1,5 @@
import { CertApplyPluginNames } from "@certd/plugin-cert";
export function getDefaultAccessPlugin() {
const metadata = `
input:
@@ -18,7 +17,7 @@ input:
component:
name: a-input
allowClear: true
`
`;
const script = `
// 必须使用 await import 来引入模块
@@ -31,19 +30,18 @@ return class DemoAccess extends BaseAccess {
}
`;
return {
metadata:metadata,
content: script
metadata: metadata,
content: script,
};
}
export function getDefaultDeployPlugin() {
let certApplyNames = ''
let certApplyNames = "";
for (const name of CertApplyPluginNames) {
certApplyNames += `
- "${name}"`
- "${name}"`;
}
const metadata =`
const metadata = `
input: #
cert:
title: 前置任务证书
@@ -90,8 +88,7 @@ input: # 插件的输入参数
#output: #
# outputName:
#
`
`;
const script = `
// 要用await来import模块
@@ -129,10 +126,10 @@ return class DemoTask extends AbstractTaskPlugin {
// this.outputName = xxxx //设置输出参数,可以被其他插件选择使用
}
}
`
`;
return {
metadata: metadata,
content: script
content: script,
};
}
@@ -144,7 +141,7 @@ accessType: aliyun # 授权类型名称
#dependLibs: #
# aliyun: *
`
`;
const script = `
const { AbstractDnsProvider } = await import("@certd/pipeline")
@@ -186,10 +183,10 @@ return class DemoDnsProvider extends AbstractDnsProvider {
}
}
`
`;
return {
metadata: metadata,
content: script
}
content: script,
};
}
@@ -1,6 +1,6 @@
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { IPluginConfigService, PluginConfig } from '@certd/pipeline';
import { PluginConfigService } from './plugin-config-service.js';
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { IPluginConfigService, PluginConfig } from "@certd/pipeline";
import { PluginConfigService } from "./plugin-config-service.js";
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
@@ -11,7 +11,7 @@ export class PluginConfigGetter implements IPluginConfigService {
async getPluginConfig(pluginName: string): Promise<PluginConfig> {
const res = await this.pluginConfigService.getPluginConfig({
name: pluginName,
type: 'builtIn',
type: "builtIn",
});
return {
name: res.name,
@@ -1,5 +1,5 @@
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { PluginService } from './plugin-service.js';
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { PluginService } from "./plugin-service.js";
export type PluginConfig = {
name: string;
@@ -20,7 +20,6 @@ export type PluginFindReq = {
type: string;
};
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class PluginConfigService {
@@ -31,18 +30,18 @@ export class PluginConfigService {
const configs: CommPluginConfig = {};
configs.CertApply = await this.getPluginConfig({
name: 'CertApply',
type: 'builtIn',
name: "CertApply",
type: "builtIn",
});
return configs;
}
async saveCommPluginConfig(config: CommPluginConfig) {
config.CertApply.name = 'CertApply';
config.CertApply.name = "CertApply";
await this.savePluginConfig(config.CertApply);
}
async savePluginConfig( config: PluginConfig) {
async savePluginConfig(config: PluginConfig) {
const name = config.name;
const sysSetting = config?.sysSetting;
if (!sysSetting) {
@@ -55,12 +54,12 @@ export class PluginConfigService {
await this.pluginService.add({
name,
sysSetting: JSON.stringify(sysSetting),
type: 'builtIn',
type: "builtIn",
disabled: false,
author: "certd",
});
} else {
let setting = JSON.parse(pluginEntity.sysSetting || "{}");
const setting = JSON.parse(pluginEntity.sysSetting || "{}");
if (sysSetting.metadata) {
setting.metadata = sysSetting.metadata;
}
@@ -73,7 +72,7 @@ export class PluginConfigService {
async get(req: PluginFindReq) {
if (!req.name && !req.id) {
throw new Error('plugin s name or id is required');
throw new Error("plugin s name or id is required");
}
return await this.pluginService.getRepository().findOne({
where: {
@@ -15,21 +15,21 @@ import fs from "fs";
import path from "path";
export type PluginImportReq = {
content: string,
content: string;
override?: boolean;
};
async function importer(modulePath: string) {
async function importer(modulePath: string) {
if (!modulePath) {
throw new Error("modules path 不能为空")
throw new Error("modules path 不能为空");
}
if (!modulePath.startsWith("/@/")) {
return await import(modulePath)
return await import(modulePath);
}
modulePath = modulePath.replace("/@/", "")
modulePath = modulePath.replace("/@/", "");
//替换@为相对地址
modulePath = `../../../${modulePath}`
return await import(modulePath)
modulePath = `../../../${modulePath}`;
return await import(modulePath);
}
@Provide()
@@ -48,7 +48,6 @@ export class PluginService extends BaseService<PluginEntity> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async page(pageReq: PageReq<PluginEntity>) {
if (pageReq.query.type && pageReq.query.type !== "builtIn") {
return await super.page(pageReq);
}
@@ -56,7 +55,6 @@ export class PluginService extends BaseService<PluginEntity> {
const offset = pageReq.page.offset;
const limit = pageReq.page.limit;
const builtInList = await this.getBuiltInEntityList();
//获取分页数据
@@ -66,11 +64,11 @@ export class PluginService extends BaseService<PluginEntity> {
records: data,
total: builtInList.length,
offset: offset,
limit: limit
limit: limit,
};
}
async getEnabledBuildInGroup(opts?: { isSimple?: boolean, withSetting?: boolean }) {
async getEnabledBuildInGroup(opts?: { isSimple?: boolean; withSetting?: boolean }) {
const groups = this.builtInPluginService.getGroups();
if (opts?.isSimple) {
for (const key in groups) {
@@ -81,7 +79,6 @@ export class PluginService extends BaseService<PluginEntity> {
}
}
if (!isComm()) {
return groups;
}
@@ -91,14 +88,14 @@ export class PluginService extends BaseService<PluginEntity> {
select: {
id: true,
name: true,
sysSetting: true
sysSetting: true,
},
where: {
sysSetting: Not(IsNull())
}
})
sysSetting: Not(IsNull()),
},
});
//合并插件配置
const pluginSettingMap: any = {}
const pluginSettingMap: any = {};
for (const item of settingPlugins) {
if (!item.sysSetting) {
continue;
@@ -113,17 +110,16 @@ export class PluginService extends BaseService<PluginEntity> {
for (const item of group.plugins) {
const pluginSetting = pluginSettingMap[item.name];
if (pluginSetting) {
item.sysSetting = pluginSetting
item.sysSetting = pluginSetting;
}
}
}
//排除禁用的
const list = await this.list({
query: {
disabled: true
}
disabled: true,
},
});
const disabledNames = list.map(it => it.name);
for (const key in groups) {
@@ -145,8 +141,8 @@ export class PluginService extends BaseService<PluginEntity> {
const list = await this.list({
query: {
type: "builtIn",
disabled: true
}
disabled: true,
},
});
const disabledNames = list.map(it => it.name);
@@ -159,8 +155,8 @@ export class PluginService extends BaseService<PluginEntity> {
const builtInList = this.builtInPluginService.getList();
const list = await this.list({
query: {
type: "builtIn"
}
type: "builtIn",
},
});
const records: PluginEntity[] = [];
@@ -177,7 +173,7 @@ export class PluginService extends BaseService<PluginEntity> {
type: "builtIn",
icon: item.icon,
desc: item.desc,
group: item.group
group: item.group,
});
records.push(record);
}
@@ -215,12 +211,11 @@ export class PluginService extends BaseService<PluginEntity> {
* @param param
*/
async add(param: any) {
const old = await this.repository.findOne({
where: {
name: param.name,
author: param.author
}
author: param.author,
},
});
if (old) {
@@ -248,11 +243,11 @@ export class PluginService extends BaseService<PluginEntity> {
const res = await super.add({
...param,
...plugin
...plugin,
});
await this.registerById(res.id);
return res
return res;
}
async registerById(id: any) {
@@ -276,20 +271,20 @@ export class PluginService extends BaseService<PluginEntity> {
}
let name = item.name;
if (item.author && !item.name.startsWith(`${item.author}/`)) {
name = `${item.author}/${item.name}`
name = `${item.author}/${item.name}`;
}
if (item.pluginType === "access") {
accessRegistry.unRegister(name)
accessRegistry.unRegister(name);
} else if (item.pluginType === "deploy") {
pluginRegistry.unRegister(name)
pluginRegistry.unRegister(name);
} else if (item.pluginType === "dnsProvider") {
dnsProviderRegistry.unRegister(name)
dnsProviderRegistry.unRegister(name);
} else if (item.pluginType === "notification") {
notificationRegistry.unRegister(name)
}else if (item.pluginType === "addon") {
addonRegistry.unRegister(name)
notificationRegistry.unRegister(name);
} else if (item.pluginType === "addon") {
addonRegistry.unRegister(name);
} else {
logger.warn(`不支持的插件类型:${item.pluginType}`)
logger.warn(`不支持的插件类型:${item.pluginType}`);
}
}
@@ -297,8 +292,8 @@ export class PluginService extends BaseService<PluginEntity> {
const old = await this.repository.findOne({
where: {
name: param.name,
author: param.author
}
author: param.author,
},
});
if (old && old.id !== param.id) {
@@ -309,22 +304,21 @@ export class PluginService extends BaseService<PluginEntity> {
const res = await super.update(param);
await this.registerById(param.id);
return res
return res;
}
async compile(code: string) {
const ts = await import("typescript");
return ts.transpileModule(code, {
compilerOptions: { module: ts.ModuleKind.ESNext }
compilerOptions: { module: ts.ModuleKind.ESNext },
}).outputText;
}
private async getPluginClassFromFile(item: any) {
const scriptFilePath = item.scriptFilePath;
const res = await import((`../../..${scriptFilePath}`))
const classNames = Object.keys(res)
return res[classNames[classNames.length - 1]]
const res = await import(`../../..${scriptFilePath}`);
const classNames = Object.keys(res);
return res[classNames[classNames.length - 1]];
}
async getPluginClassFromDb(pluginName: string) {
@@ -341,14 +335,13 @@ export class PluginService extends BaseService<PluginEntity> {
const info = await this.find({
where: {
name: name,
author: author
}
author: author,
},
});
if (info && info.length > 0) {
const plugin = info[0];
try {
const AsyncFunction = Object.getPrototypeOf(async () => {
}).constructor;
const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor;
// const script = await this.compile(plugin.content);
const script = plugin.content;
const getPluginClass = new AsyncFunction("_ctx", script);
@@ -357,7 +350,6 @@ export class PluginService extends BaseService<PluginEntity> {
logger.error("编译插件失败:", e);
throw e;
}
}
throw new Error(`插件${pluginName}不存在`);
}
@@ -367,11 +359,11 @@ export class PluginService extends BaseService<PluginEntity> {
*/
async registerFromDb() {
const res = await this.list({
buildQuery: ((bq) => {
buildQuery: bq => {
bq.andWhere("type != :type", {
type: "builtIn"
type: "builtIn",
});
})
},
});
for (const item of res) {
@@ -382,7 +374,7 @@ export class PluginService extends BaseService<PluginEntity> {
async registerFromLocal(localDir: string) {
//scan path
const files = fs.readdirSync(localDir);
let list = []
let list = [];
for (const file of files) {
if (!file.endsWith(".yaml")) {
continue;
@@ -390,7 +382,6 @@ export class PluginService extends BaseService<PluginEntity> {
const item = yaml.load(fs.readFileSync(path.join(localDir, file), "utf8"));
list.push(item);
}
//排序
list = list.sort((a, b) => {
@@ -408,7 +399,7 @@ export class PluginService extends BaseService<PluginEntity> {
const item = {
...plugin,
...metadata,
...extra
...extra,
};
delete item.metadata;
delete item.content;
@@ -416,7 +407,7 @@ export class PluginService extends BaseService<PluginEntity> {
if (item.author && !item.name.startsWith(`${item.author}/`)) {
item.name = item.author + "/" + item.name;
}
let name = item.name
let name = item.name;
if (item.addonType) {
name = item.addonType + ":" + name;
}
@@ -444,7 +435,7 @@ export class PluginService extends BaseService<PluginEntity> {
} else {
return await this.getPluginClassFromDb(name);
}
}
},
});
}
@@ -466,35 +457,34 @@ export class PluginService extends BaseService<PluginEntity> {
...info,
...metadata,
...extra,
content
content,
};
return yaml.dump(plugin) as string;
}
async importPlugin(req: PluginImportReq) {
const loaded = yaml.load(req.content);
if (!loaded) {
throw new Error("插件内容不能为空");
}
delete loaded.id
delete loaded.id;
const old = await this.repository.findOne({
where: {
name: loaded.name,
author: loaded.author
}
author: loaded.author,
},
});
const metadata = {
input: loaded.input,
output: loaded.output
output: loaded.output,
};
const extra = {
dependPlugins: loaded.dependPlugins,
default: loaded.default,
showRunStrategy: loaded.showRunStrategy
showRunStrategy: loaded.showRunStrategy,
};
const pluginEntity = {
@@ -502,7 +492,7 @@ export class PluginService extends BaseService<PluginEntity> {
metadata: yaml.dump(metadata),
extra: yaml.dump(extra),
content: loaded.content,
disabled: false
disabled: false,
};
if (!pluginEntity.pluginType) {
throw new Error(`插件类型不能为空`);
@@ -521,7 +511,7 @@ export class PluginService extends BaseService<PluginEntity> {
//update
await this.update(pluginEntity);
return {
id: pluginEntity.id
id: pluginEntity.id,
};
}
async deleteByIds(ids: any[]) {
@@ -531,5 +521,4 @@ export class PluginService extends BaseService<PluginEntity> {
await this.delete(id);
}
}
}

Some files were not shown because too many files have changed in this diff Show More