Files
certd/packages/ui/certd-server/src/modules/monitor/service/site-info-service.ts
T

276 lines
7.6 KiB
TypeScript
Raw Normal View History

2025-01-15 01:05:34 +08:00
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
2024-12-26 09:02:04 +08:00
import { BaseService, NeedSuiteException, NeedVIPException, SysSettingsService } from '@certd/lib-server';
2024-12-22 14:01:10 +08:00
import { InjectEntityModel } from '@midwayjs/typeorm';
import { 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';
2024-12-23 23:33:13 +08:00
import { isComm, isPlus } from '@certd/plus-core';
import { UserSuiteService } from '@certd/commercial-core';
2025-05-25 23:38:25 +08:00
import { UserSettingsService } from "../../mine/service/user-settings-service.js";
import { UserSiteMonitorSetting } from "../../mine/service/models.js";
2025-05-28 13:57:31 +08:00
import {SiteIpService} from "./site-ip-service.js";
2024-12-22 14:01:10 +08:00
@Provide()
2025-01-15 01:05:34 +08:00
@Scope(ScopeEnum.Request, { allowDowngrade: true })
2024-12-22 14:01:10 +08:00
export class SiteInfoService extends BaseService<SiteInfoEntity> {
@InjectEntityModel(SiteInfoEntity)
repository: Repository<SiteInfoEntity>;
@Inject()
notificationService: NotificationService;
2024-12-23 23:33:13 +08:00
@Inject()
sysSettingsService: SysSettingsService;
@Inject()
userSuiteService: UserSuiteService;
2025-05-25 23:38:25 +08:00
@Inject()
userSettingsService: UserSettingsService;
2025-05-28 13:57:31 +08:00
@Inject()
siteIpService: SiteIpService;
2025-05-25 23:38:25 +08:00
2024-12-22 14:01:10 +08:00
//@ts-ignore
getRepository() {
return this.repository;
}
2024-12-23 23:33:13 +08:00
async add(data: SiteInfoEntity) {
if (!data.userId) {
throw new Error('userId is required');
}
if (isComm()) {
2024-12-26 09:02:04 +08:00
const suiteSetting = await this.userSuiteService.getSuiteSetting();
2024-12-23 23:33:13 +08:00
if (suiteSetting.enabled) {
const userSuite = await this.userSuiteService.getMySuiteDetail(data.userId);
if (userSuite.monitorCount.max != -1 && userSuite.monitorCount.max <= userSuite.monitorCount.used) {
throw new NeedSuiteException('站点监控数量已达上限,请购买或升级套餐');
}
}
2025-03-10 00:06:49 +08:00
}else if (!isPlus()) {
const count = await this.getUserMonitorCount(data.userId);
if (count >= 1) {
throw new NeedVIPException('站点监控数量已达上限,请升级专业版');
}
2024-12-23 23:33:13 +08:00
}
2025-03-10 00:06:49 +08:00
2024-12-24 01:12:12 +08:00
data.disabled = false;
return await super.add(data);
}
async update(data: any) {
if (!data.id) {
throw new Error('id is required');
}
delete data.userId;
await super.update(data);
2024-12-23 23:33:13 +08:00
}
2024-12-22 14:01:10 +08:00
async getUserMonitorCount(userId: number) {
if (!userId) {
throw new Error('userId is required');
}
return await this.repository.count({
where: { userId },
});
}
/**
* 检查站点证书过期时间
* @param site
* @param notify
* @param retryTimes
*/
async doCheck(site: SiteInfoEntity, notify = true, retryTimes = 3) {
if (!site?.domain) {
throw new Error('站点域名不能为空');
}
try {
await this.update({
id: site.id,
checkStatus: 'checking',
2025-05-28 00:57:52 +08:00
lastCheckTime: dayjs().valueOf(),
});
const res = await siteTester.test({
host: site.domain,
port: site.httpsPort,
retryTimes,
});
const certi: PeerCertificate = res.certificate;
if (!certi) {
throw new Error('没有发现证书');
}
const expires = certi.valid_to;
const allDomains = certi.subjectaltname?.replaceAll('DNS:', '').split(',') ||[];
const mainDomain = certi.subject?.CN;
let domains = allDomains;
if (!allDomains.includes(mainDomain)) {
domains = [mainDomain, ...allDomains];
}
const issuer = `${certi.issuer.O}<${certi.issuer.CN}>`;
const isExpired = dayjs().valueOf() > dayjs(expires).valueOf();
const status = isExpired ? 'expired' : 'ok';
const updateData = {
id: site.id,
certDomains: domains.join(','),
certStatus: status,
certProvider: issuer,
certExpiresTime: dayjs(expires).valueOf(),
lastCheckTime: dayjs().valueOf(),
error: null,
checkStatus: 'ok',
};
await this.update(updateData);
2025-05-28 13:57:31 +08:00
//检查ip
if( site.ipCheck){
await this.siteIpService.checkAll(site)
}
if (!notify) {
return;
}
try {
await this.sendExpiresNotify(site);
} catch (e) {
logger.error('send notify error', e);
}
} catch (e) {
logger.error('check site error', e);
await this.update({
id: site.id,
checkStatus: 'error',
lastCheckTime: dayjs().valueOf(),
error: e.message,
});
if (!notify) {
return;
}
try {
await this.sendCheckErrorNotify(site);
} catch (e) {
logger.error('send notify error', e);
}
}
}
/**
2025-05-28 13:57:31 +08:00
* 检查
* @param id
* @param notify
* @param retryTimes
*/
async check(id: number, notify = false, retryTimes = 3) {
const site = await this.info(id);
if (!site) {
throw new Error('站点不存在');
}
return await this.doCheck(site, notify, retryTimes);
}
async sendCheckErrorNotify(site: SiteInfoEntity) {
const url = await this.notificationService.getBindUrl('#/certd/monitor/site');
// 发邮件
await this.notificationService.send(
{
useDefault: true,
logger: logger,
body: {
url,
title: `站点证书检查出错<${site.name}>`,
content: `站点名称: ${site.name} \n站点域名: ${site.domain} \n错误信息:${site.error}`,
},
},
site.userId
);
}
async sendExpiresNotify(site: SiteInfoEntity) {
2025-03-17 00:19:01 +08:00
const tipDays = 10
const expires = site.certExpiresTime;
const validDays = dayjs(expires).diff(dayjs(), 'day');
2025-03-17 18:28:33 +08:00
const url = await this.notificationService.getBindUrl('#/certd/monitor/site');
2025-03-17 18:27:52 +08:00
const content = `站点名称: ${site.name} \n站点域名: ${site.domain} \n证书域名: ${site.certDomains} \n颁发机构: ${site.certProvider} \n过期时间: ${dayjs(site.certExpiresTime).format('YYYY-MM-DD')} \n`;
2025-03-17 00:19:01 +08:00
if (validDays >= 0 && validDays < tipDays) {
// 发通知
await this.notificationService.send(
{
useDefault: true,
logger: logger,
body: {
title: `站点证书即将过期,剩余${validDays}天,<${site.name}>`,
content,
url,
},
},
site.userId
);
} else if (validDays < 0) {
//发过期通知
await this.notificationService.send(
{
useDefault: true,
logger: logger,
body: {
title: `站点证书已过期${-validDays}天<${site.name}>`,
content,
url,
},
},
site.userId
);
}
}
async checkAllByUsers(userId: any) {
if (!userId) {
throw new Error('userId is required');
}
const sites = await this.repository.find({
where: { userId },
});
this.checkList(sites);
}
async checkList(sites: SiteInfoEntity[]) {
for (const site of sites) {
this.doCheck(site).catch(e => {
logger.error(`检查站点证书失败,${site.domain}`, e.message);
});
await utils.sleep(200);
}
}
2025-05-25 23:38:25 +08:00
async getSetting(userId: number){
return await this.userSettingsService.getSetting<UserSiteMonitorSetting>(userId, UserSiteMonitorSetting);
}
async saveSetting(userId: number, bean: UserSiteMonitorSetting) {
await this.userSettingsService.saveSetting(userId, bean);
}
2025-05-28 13:57:31 +08:00
async ipCheckChange(req: {id: any; ipCheck: any}) {
await this.update({
id: req.id,
ipCheck: req.ipCheck,
});
if(req.ipCheck){
const site = await this.info(req.id);
await this.siteIpService.sync(site)
}
}
2024-12-22 14:01:10 +08:00
}