Compare commits

...

14 Commits

Author SHA1 Message Date
xiaojunnuo
56a36aa595 chore: 修复商业版翻译导致报错的bug 2025-07-13 23:20:04 +08:00
xiaojunnuo
0b3158fdd5 fix: 修复某些页面翻译不全显示错误的bug 2025-07-13 23:14:28 +08:00
xiaojunnuo
896cd950e9 chore: auto 2025-07-13 23:08:00 +08:00
xiaojunnuo
af5e1b805f chore: 2025-07-13 18:30:04 +08:00
xiaojunnuo
3f9943270c perf: 支持自动选择校验方式申请证书 2025-07-13 18:25:09 +08:00
xiaojunnuo
785bee2b39 chore: auto 2025-07-12 23:00:04 +08:00
xiaojunnuo
4b335db31c chore: 2025-07-11 18:17:11 +08:00
xiaojunnuo
4bef527ebb Merge branch 'v2-dev' into v2-dev-auto
# Conflicts:
#	packages/ui/certd-client/src/locales/langs/en-US/certd.ts
#	packages/ui/certd-client/src/locales/langs/zh-CN/certd.ts
2025-07-11 17:44:04 +08:00
xiaojunnuo
8273031d7e docs: 自动升级帮助文档 2025-07-11 17:37:33 +08:00
xiaojunnuo
24d3096752 build: publish 2025-07-11 10:51:21 +08:00
xiaojunnuo
9a3754fbf8 build: trigger build image 2025-07-11 10:51:05 +08:00
xiaojunnuo
39dc5c8160 chore: domain manager 2025-07-10 17:00:47 +08:00
xiaojunnuo
f3002e4fb6 chore: domain manager 2025-07-10 16:32:12 +08:00
xiaojunnuo
c451823c2b chore: chore: auto domain entity 2025-07-09 16:00:55 +08:00
97 changed files with 1366 additions and 285 deletions

View File

@@ -1 +1 @@
23:42
10:51

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.5](https://github.com/certd/certd/compare/v1.36.4...v1.36.5) (2025-07-11)
### Bug Fixes
* 某些插件找不到的bug ([4b3f4a8](https://github.com/certd/certd/commit/4b3f4a868a8b0800c5c59de9d0f47bddc079e7b3))
## [1.36.4](https://github.com/certd/certd/compare/v1.36.3...v1.36.4) (2025-07-10)
### Bug Fixes

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -13,4 +13,54 @@
:::
## 升级日志
可以查看最新版本号,以及所有版本的更新日志
[CHANGELOG](../changelogs/CHANGELOG.md)
## 自动升级配置
### 1. 方法一使用watchtower监控
修改docker-compose.yaml文件增加如下配置 使用watchtower监控自动升级
```yaml
services:
certd:
...
labels:
com.centurylinklabs.watchtower.enable: "true"
# ↓↓↓↓ --------------------------------------------------------- 自动升级上面certd的版本号要保持为latest
certd-updater: # 添加 Watchtower 服务
image: containrrr/watchtower:latest
container_name: certd-updater
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
# 配置 自动更新
environment:
- WATCHTOWER_CLEANUP=true # 自动清理旧版本容器
- WATCHTOWER_INCLUDE_STOPPED=false # 不更新已停止的容器
- WATCHTOWER_LABEL_ENABLE=true # 根据容器标签进行更新
- WATCHTOWER_POLL_INTERVAL=600 # 每 10 分钟检查一次更新
```
### 2. 方法二使用Certd版本监控功能
选择Github-检查Release版本插件
![](./images/github-release.png)
按如下图填写配置
![](./images/github-release-2.png)
检测到新版本后执行宿主机升级命令:
```shell
# 拉取最新镜像
docker pull registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest
# 升级容器命令, 替换成你自己的certd更新命令
export RESTART_CERT='sleep 10; cd ~/deploy/certd/ ; docker compose down; docker compose up -d'
# 构造一个脚本10s后在后台执行避免容器销毁时执行太快导致流水线任务无法结束
nohup sh -c '$RESTART_CERT' >/dev/null 2>&1 & echo '10秒后重启' && exit
```

View File

@@ -69,5 +69,5 @@
"bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues"
},
"gitHead": "0ff700849ff6427f79cce241f92257eca35af65d"
"gitHead": "c2a95a13fe6edf05ea0f72f5f7c76f9eea3ab0bd"
}

View File

@@ -45,5 +45,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "0ff700849ff6427f79cce241f92257eca35af65d"
"gitHead": "c2a95a13fe6edf05ea0f72f5f7c76f9eea3ab0bd"
}

View File

@@ -44,5 +44,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "0ff700849ff6427f79cce241f92257eca35af65d"
"gitHead": "c2a95a13fe6edf05ea0f72f5f7c76f9eea3ab0bd"
}

View File

@@ -24,5 +24,5 @@
"prettier": "^2.8.8",
"tslib": "^2.8.1"
},
"gitHead": "0ff700849ff6427f79cce241f92257eca35af65d"
"gitHead": "c2a95a13fe6edf05ea0f72f5f7c76f9eea3ab0bd"
}

View File

@@ -31,5 +31,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "0ff700849ff6427f79cce241f92257eca35af65d"
"gitHead": "c2a95a13fe6edf05ea0f72f5f7c76f9eea3ab0bd"
}

View File

@@ -61,5 +61,5 @@
"fetch"
]
},
"gitHead": "0ff700849ff6427f79cce241f92257eca35af65d"
"gitHead": "c2a95a13fe6edf05ea0f72f5f7c76f9eea3ab0bd"
}

View File

@@ -32,5 +32,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "0ff700849ff6427f79cce241f92257eca35af65d"
"gitHead": "c2a95a13fe6edf05ea0f72f5f7c76f9eea3ab0bd"
}

View File

@@ -61,5 +61,5 @@
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "0ff700849ff6427f79cce241f92257eca35af65d"
"gitHead": "c2a95a13fe6edf05ea0f72f5f7c76f9eea3ab0bd"
}

View File

@@ -1,6 +1,6 @@
import {Inject, Provide, Scope, ScopeEnum} from '@midwayjs/core';
import {InjectEntityModel} from '@midwayjs/typeorm';
import {Repository} from 'typeorm';
import { In, Repository } from "typeorm";
import {AccessGetter, BaseService, PageReq, PermissionException, ValidateException} from '../../../index.js';
import {AccessEntity} from '../entity/access.js';
import {AccessDefine, accessRegistry, newAccess} from '@certd/pipeline';
@@ -175,4 +175,27 @@ export class AccessService extends BaseService<AccessEntity> {
getDefineByType(type: string) {
return accessRegistry.getDefine(type);
}
async getSimpleByIds(ids: number[], userId: any) {
if (ids.length === 0) {
return [];
}
if (!userId) {
return [];
}
return await this.repository.find({
where: {
id: In(ids),
userId,
},
select: {
id: true,
name: true,
type: true,
userId:true
},
});
}
}

View File

@@ -46,5 +46,5 @@
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "0ff700849ff6427f79cce241f92257eca35af65d"
"gitHead": "c2a95a13fe6edf05ea0f72f5f7c76f9eea3ab0bd"
}

View File

@@ -43,5 +43,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "0ff700849ff6427f79cce241f92257eca35af65d"
"gitHead": "c2a95a13fe6edf05ea0f72f5f7c76f9eea3ab0bd"
}

View File

@@ -59,3 +59,38 @@ export interface ISubDomainsGetter {
export interface IDomainParser {
parse(fullDomain: string): Promise<string>;
}
export type DnsVerifier = {
// dns直接校验
dnsProviderType?: string;
dnsProviderAccessId?: number;
};
export type CnameVerifier = {
hostRecord: string;
domain: string;
recordValue: string;
};
export type HttpVerifier = {
// http校验
httpUploaderType: string;
httpUploaderAccess: number;
httpUploadRootDir: string;
};
export type DomainVerifier = {
domain: string;
mainDomain: string;
type: string;
dns?: DnsVerifier;
cname?: CnameVerifier;
http?: HttpVerifier;
};
export type DomainVerifiers = {
[key: string]: DomainVerifier;
};
export interface IDomainVerifierGetter {
getVerifiers(domains: string[]): Promise<DomainVerifiers>;
}

View File

@@ -23,10 +23,11 @@ export type HttpVerifyPlan = {
export type DomainVerifyPlan = {
domain: string;
mainDomain: string;
type: "cname" | "dns" | "http";
dnsProvider?: IDnsProvider;
cnameVerifyPlan?: Record<string, CnameVerifyPlan>;
httpVerifyPlan?: Record<string, HttpVerifyPlan>;
cnameVerifyPlan?: CnameVerifyPlan;
httpVerifyPlan?: HttpVerifyPlan;
};
export type DomainsVerifyPlan = {
[key: string]: DomainVerifyPlan;
@@ -233,23 +234,20 @@ export class AcmeService {
let dnsProvider = providers.dnsProvider;
let fullRecord = `_acme-challenge.${fullDomain}`;
const origDomain = punycode.toUnicode(domain);
// const origDomain = punycode.toUnicode(domain);
const origFullDomain = punycode.toUnicode(fullDomain);
if (providers.domainsVerifyPlan) {
//按照计划执行
const domainVerifyPlan = providers.domainsVerifyPlan[origDomain];
const domainVerifyPlan = providers.domainsVerifyPlan[origFullDomain];
if (domainVerifyPlan) {
if (domainVerifyPlan.type === "dns") {
dnsProvider = domainVerifyPlan.dnsProvider;
} else if (domainVerifyPlan.type === "cname") {
const cnameVerifyPlan = domainVerifyPlan.cnameVerifyPlan;
if (cnameVerifyPlan) {
const cname = cnameVerifyPlan[origFullDomain];
if (cname) {
dnsProvider = cname.dnsProvider;
domain = await this.options.domainParser.parse(cname.domain);
fullRecord = cname.fullRecord;
}
const cname: CnameVerifyPlan = domainVerifyPlan.cnameVerifyPlan;
if (cname) {
dnsProvider = cname.dnsProvider;
domain = await this.options.domainParser.parse(cname.domain);
fullRecord = cname.fullRecord;
} else {
this.logger.error(`未找到域名${fullDomain}的CNAME校验计划请修改证书申请配置`);
}
@@ -257,13 +255,12 @@ export class AcmeService {
throw new Error(`未找到域名${fullDomain}CNAME校验计划的DnsProvider请修改证书申请配置`);
}
} else if (domainVerifyPlan.type === "http") {
const httpVerifyPlan = domainVerifyPlan.httpVerifyPlan;
if (httpVerifyPlan) {
const plan: HttpVerifyPlan = domainVerifyPlan.httpVerifyPlan;
if (plan) {
const httpChallenge = getChallenge("http-01");
if (httpChallenge == null) {
throw new Error("该域名不支持http-01方式校验");
}
const plan = httpVerifyPlan[fullDomain];
return await doHttpVerify(httpChallenge, plan.httpUploader);
} else {
throw new Error("未找到域名【" + fullDomain + "】的http校验配置");
@@ -272,9 +269,12 @@ export class AcmeService {
throw new Error("不支持的校验类型", domainVerifyPlan.type);
}
} else {
this.logger.info("未找到域名校验计划使用默认的dnsProvider");
this.logger.warn(`未找到域名${fullDomain}校验计划使用默认的dnsProvider`);
}
}
if (!dnsProvider) {
throw new Error(`域名${fullDomain}没有匹配到任何校验方式,证书申请失败`);
}
const dnsChallenge = getChallenge("dns-01");
return await doDnsVerify(dnsChallenge, fullRecord, dnsProvider);

View File

@@ -1,16 +1,16 @@
import { CancelError, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
import { utils } from "@certd/basic";
import type { CertInfo, CnameVerifyPlan, DomainsVerifyPlan, HttpVerifyPlan, PrivateKeyType, SSLProvider } from "./acme.js";
import { AcmeService } from "./acme.js";
import { AcmeService, CertInfo, DomainsVerifyPlan, DomainVerifyPlan, PrivateKeyType, SSLProvider } from "./acme.js";
import * as _ from "lodash-es";
import { createDnsProvider, DnsProviderContext, IDnsProvider, ISubDomainsGetter } from "../../dns-provider/index.js";
import { createDnsProvider, DnsProviderContext, DnsVerifier, DomainVerifiers, HttpVerifier, IDnsProvider, IDomainVerifierGetter, ISubDomainsGetter } from "../../dns-provider/index.js";
import { CertReader } from "./cert-reader.js";
import { CertApplyBasePlugin } from "./base.js";
import { GoogleClient } from "../../libs/google.js";
import { EabAccess } from "../../access";
import { DomainParser } from "../../dns-provider/domain-parser.js";
import { ossClientFactory } from "@certd/plugin-lib";
export * from "./base.js";
export type { CertInfo };
export * from "./cert-reader.js";
@@ -66,13 +66,15 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
{ value: "cname", label: "CNAME代理验证" },
{ value: "http", label: "HTTP文件验证" },
{ value: "dnses", label: "多DNS提供商" },
{ value: "auto", label: "自动匹配" },
],
},
required: true,
helper: `1. <b>DNS直接验证</b>域名dns解析是在阿里云/腾讯云/华为云/CF/NameSilo/西数/火山/dns.la/京东云/51dns的选它
2. <b>CNAME代理验证</b>支持任何注册商的域名第一次需要手动添加CNAME记录建议将DNS服务器修改为阿里云/腾讯云的然后使用DNS直接验证
2. <b>CNAME代理验证</b>:支持任何注册商的域名,第一次需要手动添加[CNAME记录](#/certd/cname/record)建议将DNS服务器修改为阿里云/腾讯云的然后使用DNS直接验证
3. <b>HTTP文件验证</b>:不支持泛域名,需要配置网站文件上传
4. <b>多DNS提供商</b>每个域名可以选择独立的DNS提供商
5. <b>自动匹配</b>:需要在[域名管理](#/certd/cert/domain)中事先配置好校验方式
`,
})
challengeType!: string;
@@ -333,6 +335,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
acme!: AcmeService;
eab!: EabAccess;
async onInit() {
let eab: EabAccess = null;
@@ -408,7 +411,9 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
let dnsProvider: IDnsProvider = null;
let domainsVerifyPlan: DomainsVerifyPlan = null;
if (this.challengeType === "cname" || this.challengeType === "http" || this.challengeType === "dnses") {
domainsVerifyPlan = await this.createDomainsVerifyPlan();
domainsVerifyPlan = await this.createDomainsVerifyPlan(domains, this.domainsVerifyPlan);
} else if (this.challengeType === "auto") {
domainsVerifyPlan = await this.createDomainsVerifyPlanByAuto(domains);
} else {
const dnsProviderType = this.dnsProviderType;
const access = await this.getAccess(this.dnsProviderAccess);
@@ -444,73 +449,126 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
async createDnsProvider(dnsProviderType: string, dnsProviderAccess: any): Promise<IDnsProvider> {
const domainParser = this.acme.options.domainParser;
const context: DnsProviderContext = { access: dnsProviderAccess, logger: this.logger, http: this.ctx.http, utils, domainParser };
const context: DnsProviderContext = {
access: dnsProviderAccess,
logger: this.logger,
http: this.ctx.http,
utils,
domainParser,
};
return await createDnsProvider({
dnsProviderType,
context,
});
}
async createDomainsVerifyPlan(): Promise<DomainsVerifyPlan> {
async createDomainsVerifyPlan(domains: string[], verifyPlanSetting: DomainsVerifyPlanInput): Promise<DomainsVerifyPlan> {
const plan: DomainsVerifyPlan = {};
for (const domain in this.domainsVerifyPlan) {
const domainVerifyPlan = this.domainsVerifyPlan[domain];
let dnsProvider = null;
const cnameVerifyPlan: Record<string, CnameVerifyPlan> = {};
const httpVerifyPlan: Record<string, HttpVerifyPlan> = {};
if (domainVerifyPlan.type === "dns") {
const access = await this.getAccess(domainVerifyPlan.dnsProviderAccessId);
dnsProvider = await this.createDnsProvider(domainVerifyPlan.dnsProviderType, access);
} else if (domainVerifyPlan.type === "cname") {
for (const key in domainVerifyPlan.cnameVerifyPlan) {
const cnameRecord = await this.ctx.cnameProxyService.getByDomain(key);
let dnsProvider = cnameRecord.commonDnsProvider;
if (cnameRecord.cnameProvider.id > 0) {
dnsProvider = await this.createDnsProvider(cnameRecord.cnameProvider.dnsProviderType, cnameRecord.cnameProvider.access);
}
cnameVerifyPlan[key] = {
type: "cname",
domain: cnameRecord.cnameProvider.domain,
fullRecord: cnameRecord.recordValue,
dnsProvider,
};
}
} else if (domainVerifyPlan.type === "http") {
const httpUploaderContext = {
accessService: this.ctx.accessService,
logger: this.logger,
utils,
};
for (const key in domainVerifyPlan.httpVerifyPlan) {
const httpRecord = domainVerifyPlan.httpVerifyPlan[key];
const access = await this.getAccess(httpRecord.httpUploaderAccess);
let rootDir = httpRecord.httpUploadRootDir;
if (!rootDir.endsWith("/") && !rootDir.endsWith("\\")) {
rootDir = rootDir + "/";
}
this.logger.info("上传方式", httpRecord.httpUploaderType);
const httpUploader = await ossClientFactory.createOssClientByType(httpRecord.httpUploaderType, {
access,
rootDir: rootDir,
ctx: httpUploaderContext,
});
httpVerifyPlan[key] = {
type: "http",
domain: key,
httpUploader,
};
}
const domainParser = this.acme.options.domainParser;
for (const fullDomain of domains) {
const domain = fullDomain.replaceAll("*.", "");
const mainDomain = await domainParser.parse(domain);
const planSetting: DomainVerifyPlanInput = verifyPlanSetting[mainDomain];
if (planSetting.type === "dns") {
plan[domain] = await this.createDnsDomainVerifyPlan(planSetting, domain, mainDomain);
} else if (planSetting.type === "cname") {
plan[domain] = await this.createCnameDomainVerifyPlan(domain, mainDomain);
} else if (planSetting.type === "http") {
plan[domain] = await this.createHttpDomainVerifyPlan(planSetting.httpVerifyPlan[domain], domain, mainDomain);
}
plan[domain] = {
domain,
type: domainVerifyPlan.type,
dnsProvider,
cnameVerifyPlan,
httpVerifyPlan,
};
}
return plan;
}
private async createDomainsVerifyPlanByAuto(domains: string[]) {
//从数据库里面自动选择校验方式
// domain list
const domainList = new Set<string>();
//整理域名
for (let domain of domains) {
domain = domain.replaceAll("*.", "");
domainList.add(domain);
}
const domainVerifierGetter: IDomainVerifierGetter = await this.ctx.serviceGetter.get("domainVerifierGetter");
const verifiers: DomainVerifiers = await domainVerifierGetter.getVerifiers([...domainList]);
const plan: DomainsVerifyPlan = {};
for (const domain in verifiers) {
const verifier = verifiers[domain];
if (verifier.type === "dns") {
plan[domain] = await this.createDnsDomainVerifyPlan(verifier.dns, domain, verifier.mainDomain);
} else if (verifier.type === "cname") {
plan[domain] = await this.createCnameDomainVerifyPlan(domain, verifier.mainDomain);
} else if (verifier.type === "http") {
plan[domain] = await this.createHttpDomainVerifyPlan(verifier.http, domain, verifier.mainDomain);
}
}
return plan;
}
private async createDnsDomainVerifyPlan(planSetting: DnsVerifier, domain: string, mainDomain: string): Promise<DomainVerifyPlan> {
const access = await this.getAccess(planSetting.dnsProviderAccessId);
return {
type: "dns",
mainDomain,
domain,
dnsProvider: await this.createDnsProvider(planSetting.dnsProviderType, access),
};
}
private async createHttpDomainVerifyPlan(httpSetting: HttpVerifier, domain: string, mainDomain: string): Promise<DomainVerifyPlan> {
const httpUploaderContext = {
accessService: this.ctx.accessService,
logger: this.logger,
utils,
};
const access = await this.getAccess(httpSetting.httpUploaderAccess);
let rootDir = httpSetting.httpUploadRootDir;
if (!rootDir.endsWith("/") && !rootDir.endsWith("\\")) {
rootDir = rootDir + "/";
}
this.logger.info("上传方式", httpSetting.httpUploaderType);
const httpUploader = await ossClientFactory.createOssClientByType(httpSetting.httpUploaderType, {
access,
rootDir: rootDir,
ctx: httpUploaderContext,
});
return {
type: "http",
domain,
mainDomain,
httpVerifyPlan: {
type: "http",
domain,
httpUploader,
},
};
}
private async createCnameDomainVerifyPlan(domain: string, mainDomain: string): Promise<DomainVerifyPlan> {
const cnameRecord = await this.ctx.cnameProxyService.getByDomain(domain);
if (cnameRecord == null) {
throw new Error(`请先配置${domain}的CNAME记录并通过校验`);
}
let dnsProvider = cnameRecord.commonDnsProvider;
if (cnameRecord.cnameProvider.id > 0) {
dnsProvider = await this.createDnsProvider(cnameRecord.cnameProvider.dnsProviderType, cnameRecord.cnameProvider.access);
}
return {
type: "cname",
domain,
mainDomain,
cnameVerifyPlan: {
domain: cnameRecord.cnameProvider.domain,
fullRecord: cnameRecord.recordValue,
dnsProvider,
},
};
}
}
new CertApplyPlugin();

View File

@@ -53,5 +53,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "0ff700849ff6427f79cce241f92257eca35af65d"
"gitHead": "c2a95a13fe6edf05ea0f72f5f7c76f9eea3ab0bd"
}

View File

@@ -491,7 +491,7 @@ export class SshClient {
* Set-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Windows\System32\cmd.exe"
* @param options
*/
async exec(options: { connectConf: SshAccess; script: string | Array<string>; env?: any; throwOnStdErr?: boolean }): Promise<string> {
async exec(options: { connectConf: SshAccess; script: string | Array<string>; env?: any; throwOnStdErr?: boolean; stopOnError?: boolean }): Promise<string> {
let { script } = options;
const { connectConf, throwOnStdErr } = options;
@@ -506,6 +506,10 @@ export class SshClient {
isWinCmd = await this.isCmd(conn);
}
if (isLinux && options.stopOnError !== false) {
script = "set -e\n" + script;
}
if (options.env) {
for (const key in options.env) {
if (isLinux) {
@@ -538,6 +542,7 @@ export class SshClient {
script = envScripts.join(newLine) + newLine + script;
}
}
return await conn.exec(script as string, { throwOnStdErr });
},
});

View File

@@ -33,6 +33,7 @@
import { Ref, ref, watch, nextTick } from "vue";
import { HttpRecord } from "/@/components/plugins/cert/domains-verify-plan-editor/type";
import { dict } from "@fast-crud/fast-crud";
import { Dicts } from "/@/components/plugins/lib/dicts";
defineOptions({
name: "HttpVerifyPlan",
@@ -68,17 +69,7 @@ async function onRecordChange() {
emit("change", records.value);
}
const uploaderTypeDict = dict({
data: [
{ label: "SFTP", value: "sftp" },
{ label: "FTP", value: "ftp" },
{ label: "阿里云OSS", value: "alioss" },
{ label: "腾讯云COS", value: "tencentcos" },
{ label: "七牛OSS", value: "qiniuoss" },
{ label: "S3/Minio", value: "s3" },
{ label: "SSH(已废弃请选择SFTP方式)", value: "ssh", disabled: true },
],
});
const uploaderTypeDict = Dicts.uploaderTypeDict;
</script>
<style lang="less">

View File

@@ -0,0 +1,31 @@
import { dict } from "@fast-crud/fast-crud";
export const Dicts = {
sslProviderDict: dict({
data: [
{ value: "letsencrypt", label: "Lets Encrypt" },
{ value: "zerossl", label: "ZeroSSL" },
],
}),
challengeTypeDict: dict({
data: [
{ value: "dns", label: "DNS校验", color: "green" },
{ value: "cname", label: "CNAME代理校验", color: "blue" },
{ value: "http", label: "HTTP校验", color: "yellow" },
],
}),
dnsProviderTypeDict: dict({
url: "pi/dnsProvider/dnsProviderTypeDict",
}),
uploaderTypeDict: dict({
data: [
{ label: "SFTP", value: "sftp" },
{ label: "FTP", value: "ftp" },
{ label: "阿里云OSS", value: "alioss" },
{ label: "腾讯云COS", value: "tencentcos" },
{ label: "七牛OSS", value: "qiniuoss" },
{ label: "S3/Minio", value: "s3" },
{ label: "SSH(已废弃请选择SFTP方式)", value: "ssh", disabled: true },
],
}),
};

View File

@@ -471,7 +471,7 @@ export default {
statusError: "Error",
actionImportBatch: "Batch Import",
actionSyncIp: "Sync IP",
TitleSyncIp: "Sync IP",
modalTitleSyncIp: "Sync IP",
modalContentSyncIp: "Are you sure to sync IP?",
notificationSyncComplete: "Sync Complete",
actionCheckAll: "Check All",
@@ -712,4 +712,18 @@ export default {
close: "Close",
viewCertificateTitle: "View Certificate",
},
domain: {
domainManager: "Domain Manager",
domainDescription: "used to auto apply for certificate", //管理域名的校验方式,用于申请证书时自动选择验证方式
domain: "Domain",
challengeType: "Challenge Type",
dnsProviderType: "DNS Provider Type",
dnsProviderAccess: "DNS Provider Access",
httpUploaderType: "HTTP Uploader Type",
httpUploaderAccess: "HTTP Uploader Access",
httpUploadRootDir: "HTTP Upload Root Dir",
disabled: "Disabled",
challengeSetting: "Challenge Setting",
gotoCnameTip: "Please go to CNAME Record Page",
},
};

View File

@@ -14,6 +14,8 @@ export default {
search: "Search",
enabled: "Enabled",
disabled: "Disabled",
enable: "Enable",
disable: "Disable",
edit: "Edit",
delete: "Delete",
create: "Create",

View File

@@ -715,4 +715,18 @@ export default {
close: "关闭",
viewCertificateTitle: "查看证书",
},
domain: {
domainManager: "域名管理",
domainDescription: "管理域名的校验方式,用于申请证书时自动选择验证方式",
domain: "域名",
challengeType: "校验类型",
dnsProviderType: "DNS提供商类型",
dnsProviderAccess: "DNS提供商授权",
httpUploaderType: "上传方式",
httpUploaderAccess: "上传授权信息",
httpUploadRootDir: "网站根路径",
disabled: "禁用/启用",
challengeSetting: "校验配置",
gotoCnameTip: "CNAME域名配置请前往CNAME记录页面添加",
},
};

View File

@@ -14,6 +14,8 @@ export default {
search: "搜索",
enabled: "已启用",
disabled: "已禁用",
enable: "启用",
disable: "禁用",
edit: "修改",
delete: "删除",
create: "新增",

View File

@@ -116,6 +116,17 @@ export const certdResources = [
keepAlive: true,
},
},
{
title: "certd.domain.domainManager",
name: "DomainManager",
path: "/certd/cert/domain",
component: "/certd/cert/domain/index.vue",
meta: {
icon: "ion:globe-outline",
auth: true,
keepAlive: true,
},
},
{
title: "certd.cnameRecord",
name: "CnameRecord",

View File

@@ -2,7 +2,7 @@
import { ref } from "vue";
import { getCommonColumnDefine } from "/@/views/certd/access/common";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { t } = useI18n();

View File

@@ -55,6 +55,14 @@ export function createAccessApi(from = "user") {
});
},
async GetDictByIds(ids: number[]) {
return await request({
url: apiPrefix + "/getDictByIds",
method: "post",
data: { ids },
});
},
async GetSecretPlain(id: number, key: string) {
return await request({
url: apiPrefix + "/getSecretPlain",

View File

@@ -1,5 +1,5 @@
// @ts-ignore
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
import { ref } from "vue";
import { getCommonColumnDefine } from "/@/views/certd/access/common";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";

View File

@@ -15,7 +15,7 @@ import { defineComponent, onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { createAccessApi } from "/@/views/certd/access/api";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
export default defineComponent({
name: "AccessManager",

View File

@@ -0,0 +1,60 @@
import { request } from "/src/api/service";
const apiPrefix = "/cert/domain";
export async function GetList(query: any) {
return await request({
url: apiPrefix + "/page",
method: "post",
data: query,
});
}
export async function AddObj(obj: any) {
return await request({
url: apiPrefix + "/add",
method: "post",
data: obj,
});
}
export async function UpdateObj(obj: any) {
return await request({
url: apiPrefix + "/update",
method: "post",
data: obj,
});
}
export async function DelObj(id: any) {
return await request({
url: apiPrefix + "/delete",
method: "post",
params: { id },
});
}
export async function GetObj(id: any) {
return await request({
url: apiPrefix + "/info",
method: "post",
params: { id },
});
}
export async function GetDetail(id: any) {
return await request({
url: apiPrefix + "/detail",
method: "post",
params: { id },
});
}
export async function DeleteBatch(ids: any[]) {
return await request({
url: apiPrefix + "/deleteByIds",
method: "post",
data: { ids },
});
}

View File

@@ -0,0 +1,330 @@
import * as api from "./api";
import { useI18n } from "/src/locales";
import { Ref, ref } from "vue";
import { useRouter } from "vue-router";
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { useUserStore } from "/@/store/user";
import { useSettingStore } from "/@/store/settings";
import { Dicts } from "/@/components/plugins/lib/dicts";
import { createAccessApi } from "/@/views/certd/access/api";
import { Modal } from "ant-design-vue";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter();
const { t } = useI18n();
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
const res = await api.UpdateObj(form);
return res;
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
const res = await api.AddObj(form);
return res;
};
const userStore = useUserStore();
const settingStore = useSettingStore();
const selectedRowKeys: Ref<any[]> = ref([]);
context.selectedRowKeys = selectedRowKeys;
const accessApi = createAccessApi();
const accessDict = dict({
value: "id",
label: "name",
url: "accessDict",
async getNodesByValues(ids: number[]) {
return await accessApi.GetDictByIds(ids);
},
});
const httpUploaderTypeDict = Dicts.uploaderTypeDict;
const dnsProviderTypeDict = dict({
url: "pi/dnsProvider/dnsProviderTypeDict",
});
return {
crudOptions: {
settings: {
plugins: {
//这里使用行选择插件生成行选择crudOptions配置最终会与crudOptions合并
rowSelection: {
enabled: true,
order: -2,
before: true,
// handle: (pluginProps,useCrudProps)=>CrudOptions,
props: {
multiple: true,
crossPage: true,
selectedRowKeys,
},
},
},
},
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
tabs: {
name: "challengeType",
show: true,
},
rowHandle: {
minWidth: 200,
fixed: "right",
},
form: {
beforeSubmit({ form }) {
if (form.challengeType === "cname") {
throw new Error("CNAME方式请前往CNAME记录页面进行管理");
}
},
},
columns: {
id: {
title: "ID",
key: "id",
type: "number",
column: {
width: 80,
},
form: {
show: false,
},
},
domain: {
title: t("certd.domain.domain"),
type: "text",
search: {
show: true,
},
form: {
required: true,
},
editForm: {
component: {
disabled: false,
},
},
column: {
sorter: true,
},
},
challengeType: {
title: t("certd.domain.challengeType"),
type: "dict-select",
dict: Dicts.challengeTypeDict,
search: {
show: true,
},
form: {
required: true,
valueChange({ value }) {
if (value === "cname") {
Modal.confirm({
title: t("certd.domain.gotoCnameTip"),
async onOk() {
router.push({
path: "/certd/cname/record",
});
crudExpose.getFormWrapperRef().close();
},
});
}
},
},
column: {
sorter: true,
show: false,
},
},
/**
* challengeType varchar(50),
* dnsProviderType varchar(50),
* dnsProviderAccess bigint,
* httpUploaderType varchar(50),
* httpUploaderAccess bigint,
* httpUploadRootDir varchar(512),
*/
dnsProviderType: {
title: t("certd.domain.dnsProviderType"),
type: "dict-select",
dict: dnsProviderTypeDict,
form: {
component: {
name: "DnsProviderSelector",
},
show: compute(({ form }) => {
return form.challengeType === "dns";
}),
required: true,
},
column: {
show: false,
component: {
color: "auto",
},
},
},
dnsProviderAccess: {
title: t("certd.domain.dnsProviderAccess"),
type: "dict-select",
dict: accessDict,
form: {
component: {
name: "AccessSelector",
vModel: "modelValue",
type: compute(({ form }) => {
return form.dnsProviderType;
}),
},
show: compute(({ form }) => {
return form.challengeType === "dns";
}),
required: true,
},
column: {
show: false,
component: {
color: "auto",
},
},
},
httpUploaderType: {
title: t("certd.domain.httpUploaderType"),
type: "dict-select",
dict: Dicts.uploaderTypeDict,
form: {
show: compute(({ form }) => {
return form.challengeType === "http";
}),
required: true,
},
column: {
show: false,
component: {
color: "auto",
},
},
},
httpUploaderAccess: {
title: t("certd.domain.httpUploaderAccess"),
type: "text",
form: {
component: {
name: "AccessSelector",
vModel: "modelValue",
type: compute(({ form }) => {
return form.httpUploaderType;
}),
},
show: compute(({ form }) => {
return form.challengeType === "http";
}),
required: true,
},
column: {
show: false,
component: {
color: "auto",
},
},
},
httpUploadRootDir: {
title: t("certd.domain.httpUploadRootDir"),
type: "text",
form: {
show: compute(({ form }) => {
return form.challengeType === "http";
}),
required: true,
},
column: {
show: false,
component: {
color: "auto",
},
},
},
challengeSetting: {
title: t("certd.domain.challengeSetting"),
type: "text",
form: { show: false },
column: {
width: 600,
conditionalRender: false,
cellRender({ row }) {
if (row.challengeType === "dns") {
return (
<div class={"flex"}>
<fs-values-format modelValue={row.challengeType} dict={Dicts.challengeTypeDict} color={"auto"}></fs-values-format>
<fs-values-format modelValue={row.dnsProviderType} dict={dnsProviderTypeDict} color={"auto"}></fs-values-format>
<fs-values-format class={"ml-5"} modelValue={row.dnsProviderAccess} dict={accessDict} color={"auto"}></fs-values-format>
</div>
);
} else if (row.challengeType === "http") {
return (
<div class={"flex"}>
<fs-values-format modelValue={row.challengeType} dict={Dicts.challengeTypeDict} color={"auto"}></fs-values-format>
<fs-values-format modelValue={row.httpUploaderType} dict={httpUploaderTypeDict} color={"auto"}></fs-values-format>
<fs-values-format class={"ml-5"} modelValue={row.httpUploaderAccess} dict={accessDict} color={"auto"}></fs-values-format>
<a-tag class={"ml-5 flex items-center"}>{row.httpUploadRootDir}</a-tag>
</div>
);
}
},
},
},
disabled: {
title: t("certd.domain.disabled"),
type: "dict-switch",
search: { show: true },
dict: dict({
data: [
{ label: t("common.enabled"), value: false, color: "green" },
{ label: t("common.disabled"), value: true, color: "red" },
],
}),
form: {
value: false,
required: true,
},
column: {
width: 100,
sorter: true,
},
},
createTime: {
title: t("certd.createTime"),
type: "datetime",
form: {
show: false,
},
column: {
sorter: true,
width: 160,
align: "center",
},
},
updateTime: {
title: t("certd.updateTime"),
type: "datetime",
form: {
show: false,
},
column: {
show: true,
},
},
},
},
};
}

View File

@@ -0,0 +1,62 @@
<template>
<fs-page class="page-cert">
<template #header>
<div class="title">
{{ t("certd.domain.domainManager") }}
<span class="sub">
{{ t("certd.domain.domainDescription") }}
</span>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #pagination-left>
<a-tooltip :title="t('certd.batch_delete')">
<fs-button icon="DeleteOutlined" @click="handleBatchDelete"></fs-button>
</a-tooltip>
</template>
</fs-crud>
</fs-page>
</template>
<script lang="ts" setup>
import { onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { message, Modal } from "ant-design-vue";
import { DeleteBatch } from "./api";
import { useI18n } from "/src/locales";
const { t } = useI18n();
defineOptions({
name: "DomainManager",
});
const { crudBinding, crudRef, crudExpose, context } = useFs({ createCrudOptions });
const selectedRowKeys = context.selectedRowKeys;
const handleBatchDelete = () => {
if (selectedRowKeys.value?.length > 0) {
Modal.confirm({
title: t("certd.confirm"),
content: t("certd.confirm_delete_count", { count: selectedRowKeys.value.length }),
async onOk() {
await DeleteBatch(selectedRowKeys.value);
message.info(t("certd.delete_successful"));
crudExpose.doRefresh();
selectedRowKeys.value = [];
},
});
} else {
message.error(t("certd.please_select_records"));
}
};
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(async () => {
await crudExpose.doRefresh();
});
</script>
<style lang="less"></style>

View File

@@ -1,5 +1,5 @@
import * as api from "./api";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
import { Ref, ref } from "vue";
import { useRouter } from "vue-router";
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";

View File

@@ -26,7 +26,7 @@ import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { message, Modal } from "ant-design-vue";
import { DeleteBatch } from "./api";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();

View File

@@ -1,5 +1,5 @@
import * as api from "./api";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
import { computed, Ref, ref } from "vue";
import { useRouter } from "vue-router";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes, utils } from "@fast-crud/fast-crud";

View File

@@ -19,7 +19,7 @@ import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { message, Modal } from "ant-design-vue";
import { DeleteBatch } from "./api";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();

View File

@@ -6,7 +6,7 @@
<script setup lang="ts">
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();
import { CrudOptions, useColumns, useFormWrapper } from "@fast-crud/fast-crud";

View File

@@ -79,7 +79,7 @@ import { UserTwoFactorSetting } from "./api";
import { Modal, notification } from "ant-design-vue";
import { merge } from "lodash-es";
import { useSettingStore } from "/@/store/settings";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();
const settingsStore = useSettingStore();

View File

@@ -29,7 +29,7 @@
import * as api from "./api";
import { Ref, ref } from "vue";
import ChangePasswordButton from "/@/views/certd/mine/change-password-button.vue";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
import { useUserProfile } from "./use";
const { t } = useI18n();

View File

@@ -1,5 +1,5 @@
// @ts-ignore
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
//
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, useFormWrapper, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { certInfoApi } from "./api";

View File

@@ -14,7 +14,7 @@
import { onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();

View File

@@ -1,5 +1,5 @@
// @ts-ignore
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
import { AddReq, ColumnCompositionProps, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { siteInfoApi } from "./api";
import dayjs from "dayjs";

View File

@@ -28,7 +28,7 @@ import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { siteInfoApi } from "./api";
import { Modal, notification } from "ant-design-vue";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();
defineOptions({

View File

@@ -1,5 +1,5 @@
// @ts-ignore
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { siteIpApi } from "./api";
import dayjs from "dayjs";

View File

@@ -47,7 +47,7 @@ import { merge } from "lodash-es";
import { useSettingStore } from "/src/store/settings";
import NotificationSelector from "/@/views/certd/notification/notification-selector/index.vue";
import { useUserStore } from "/@/store/user";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();

View File

@@ -1,6 +1,6 @@
import { useFormWrapper } from "@fast-crud/fast-crud";
import { siteInfoApi } from "./api";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
export function useSiteImport() {
const { t } = useI18n();

View File

@@ -41,7 +41,7 @@ import { dict } from "@fast-crud/fast-crud";
import createCrudOptions from "../crud";
import { notificationProvide } from "/@/views/certd/notification/common";
import { useUserStore } from "/@/store/user";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();

View File

@@ -1,5 +1,5 @@
// @ts-ignore
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { OPEN_API_DOC, openkeyApi } from "./api";
import { useModal } from "/@/use/use-modal";

View File

@@ -1,14 +0,0 @@
import { dict } from "@fast-crud/fast-crud";
export const Dicts = {
sslProviderDict: dict({
data: [
{ value: "letsencrypt", label: "Lets Encrypt" },
{ value: "zerossl", label: "ZeroSSL" },
],
}),
challengeTypeDict: dict({ data: [{ value: "dns", label: "DNS校验" }] }),
dnsProviderTypeDict: dict({
url: "pi/dnsProvider/dnsProviderTypeDict",
}),
};

View File

@@ -5,7 +5,7 @@
<script setup lang="ts">
import * as api from "../api";
import { useFormWrapper } from "@fast-crud/fast-crud";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();

View File

@@ -31,7 +31,7 @@ import ChangeGroup from "./components/change-group.vue";
import ChangeTrigger from "./components/change-trigger.vue";
import { Modal, notification } from "ant-design-vue";
import * as api from "./api";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();
import ChangeNotification from "/@/views/certd/pipeline/components/change-notification.vue";

View File

@@ -90,7 +90,7 @@ import * as _ from "lodash-es";
import { nanoid } from "nanoid";
import PiNotificationFormEmail from "./pi-notification-form-email.vue";
import NotificationSelector from "/@/views/certd/notification/notification-selector/index.vue";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
import { cloneDeep } from "lodash-es";
const { t } = useI18n();

View File

@@ -25,7 +25,7 @@
import { Ref, ref, watch } from "vue";
import { useUserStore } from "/@/store/user";
import { useSettingStore } from "/@/store/settings";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();

View File

@@ -1,7 +1,7 @@
import * as api from "./api";
import { Ref, ref } from "vue";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { t } = useI18n();

View File

@@ -22,7 +22,7 @@ import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { message, Modal } from "ant-design-vue";
import { DeleteBatch } from "./api";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();

View File

@@ -17,7 +17,7 @@
import { onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
defineOptions({
name: "PipelineTemplate",
});

View File

@@ -1,33 +1,33 @@
<template>
<a-modal v-model:open="openRef" class="order-modal" :title="$t('order.confirmTitle')" @ok="orderCreate">
<a-modal v-model:open="openRef" class="order-modal" :title="$t('certd.order.confirmTitle')" @ok="orderCreate">
<div v-if="product" class="order-box">
<div class="flex-o mt-5">
<span class="label">{{ $t("order.package") }}</span>{{ product.title }}
<span class="label">{{ $t("certd.order.package") }}</span>{{ product.title }}
</div>
<div class="flex-o mt-5">
<span class="label">{{ $t("order.description") }}</span>{{ product.intro }}
<span class="label">{{ $t("certd.order.description") }}</span>{{ product.intro }}
</div>
<div class="flex-o mt-5">
<span class="label">{{ $t("order.specifications") }}</span>
<span class="label">{{ $t("certd.order.specifications") }}</span>
<span class="flex-o flex-wrap">
<span class="flex-o"> {{ $t("order.pipeline") }}<suite-value class="ml-5" :model-value="product.content.maxPipelineCount" unit="{{$t('order.unit.pieces')}}" /> </span>
<span class="flex-o"> {{ $t("order.domain") }}<suite-value class="ml-5" :model-value="product.content.maxDomainCount" unit="{{$t('order.unit.count')}}" /> </span>
<span class="flex-o"> {{ $t("order.deployTimes") }}<suite-value class="ml-5" :model-value="product.content.maxDeployCount" unit="{{$t('order.unit.times')}}" /> </span>
<span class="flex-o"> {{ $t("certd.order.pipeline") }}<suite-value class="ml-5" :model-value="product.content.maxPipelineCount" :unit="$t('certd.order.unit.pieces')" /> </span>
<span class="flex-o"> {{ $t("certd.order.domain") }}<suite-value class="ml-5" :model-value="product.content.maxDomainCount" :unit="$t('certd.order.unit.count')" /> </span>
<span class="flex-o"> {{ $t("certd.order.deployTimes") }}<suite-value class="ml-5" :model-value="product.content.maxDeployCount" :unit="$t('certd.order.unit.times')" /> </span>
</span>
</div>
<div class="flex-o mt-5">
<span class="label">{{ $t("order.duration") }}</span>
<span class="label">{{ $t("certd.order.duration") }}</span>
<duration-value v-model="formRef.duration"></duration-value>
</div>
<div class="flex-o mt-5">
<span class="label">{{ $t("order.price") }}</span>
<span class="label">{{ $t("certd.order.price") }}</span>
<price-input :edit="false" :model-value="durationSelected.price"></price-input>
</div>
<div class="flex-o mt-5">
<span class="label">{{ $t("order.paymentMethod") }}</span>
<div v-if="durationSelected.price === 0">{{ $t("order.free") }}</div>
<span class="label">{{ $t("certd.order.paymentMethod") }}</span>
<div v-if="durationSelected.price === 0">{{ $t("certd.order.free") }}</div>
<fs-dict-select v-else v-model:value="formRef.payType" :dict="paymentsDictRef" style="width: 200px"> </fs-dict-select>
</div>
</div>

View File

@@ -1,5 +1,5 @@
import * as api from "./api";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
import { computed, Ref, ref } from "vue";
import { useRouter } from "vue-router";
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes, utils } from "@fast-crud/fast-crud";

View File

@@ -149,7 +149,7 @@ import { UserInfoRes } from "/@/store/user/api.user";
import { GetStatisticCount } from "/@/views/framework/home/dashboard/api";
import { useRouter } from "vue-router";
import * as api from "./api";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();
import { usePluginStore } from "/@/store/plugin";
defineOptions({

View File

@@ -12,7 +12,7 @@ import { useUserStore } from "/@/store/user";
import ChangePasswordButton from "/@/views/certd/mine/change-password-button.vue";
import { onMounted, ref } from "vue";
import { Modal } from "ant-design-vue";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();

View File

@@ -1,6 +1,6 @@
import * as api from "./api.js";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { t } = useI18n();

View File

@@ -28,7 +28,7 @@
import { utils } from "@fast-crud/fast-crud";
import { cloneDeep } from "lodash-es";
import { computed, defineComponent, ref } from "vue";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
export default defineComponent({
name: "FsPermissionTree",

View File

@@ -19,7 +19,7 @@ import createCrudOptions from "./crud.js";
import FsPermissionTree from "./fs-permission-tree.vue";
import { usePermission } from "/src/plugin/permission";
import { useFs, useUi } from "@fast-crud/fast-crud";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
export default defineComponent({
name: "AuthorityManager",

View File

@@ -1,6 +1,6 @@
import * as api from "./api";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
export default function ({ crudExpose, context: { authz } }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { t } = useI18n();

View File

@@ -19,7 +19,7 @@ import * as api from "./api";
import { message } from "ant-design-vue";
import FsPermissionTree from "../permission/fs-permission-tree.vue";
import { UseCrudPermissionCompProps, UseCrudPermissionExtraProps } from "/@/plugin/permission";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
function useAuthz() {
const checkedKeys = ref();

View File

@@ -4,7 +4,7 @@ import { useUserStore } from "/@/store/user";
import { Modal, notification } from "ant-design-vue";
import dayjs from "dayjs";
import { useSettingStore } from "/@/store/settings";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { t } = useI18n();
@@ -207,8 +207,8 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
type: "dict-switch",
dict: dict({
data: [
{ label: t("certd.enabled"), value: 1, color: "green" },
{ label: t("certd.disabled"), value: 0, color: "red" },
{ label: t("common.enabled"), value: 1, color: "green" },
{ label: t("common.disabled"), value: 0, color: "red" },
],
}),
column: {

View File

@@ -1,5 +1,5 @@
import * as api from "./api";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
import { computed, Ref, ref } from "vue";
import { useRouter } from "vue-router";
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes, utils } from "@fast-crud/fast-crud";

View File

@@ -27,7 +27,7 @@ import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { message, Modal } from "ant-design-vue";
import { DeleteBatch } from "./api";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();

View File

@@ -1,5 +1,5 @@
import * as api from "./api";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
import { Ref, ref } from "vue";
import { useRouter } from "vue-router";
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, useFormWrapper, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";

View File

@@ -22,7 +22,7 @@ import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { message, Modal } from "ant-design-vue";
import { DeleteBatch } from "./api";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();

View File

@@ -77,7 +77,7 @@ import * as emailApi from "./api.email";
import { notification } from "ant-design-vue";
import { useSettingStore } from "/src/store/settings";
import * as _ from "lodash-es";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();
defineOptions({

View File

@@ -1,4 +1,4 @@
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
import { Ref, ref } from "vue";
import { useRouter } from "vue-router";
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";

View File

@@ -62,7 +62,7 @@ import { merge } from "lodash-es";
import { useSettingStore } from "/@/store/settings";
import { notification } from "ant-design-vue";
import { util } from "/@/utils";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();

View File

@@ -73,7 +73,7 @@ import * as api from "/@/views/sys/settings/api";
import { merge } from "lodash-es";
import { useSettingStore } from "/@/store/settings";
import { notification } from "ant-design-vue";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();

View File

@@ -56,7 +56,7 @@ import { Modal, notification } from "ant-design-vue";
import { request } from "/@/api/service";
import { util, utils } from "/@/utils";
import { useSettingStore } from "/@/store/settings";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();
defineOptions({

View File

@@ -4,7 +4,7 @@ import SuiteValue from "./suite-value.vue";
import SuiteValueEdit from "./suite-value-edit.vue";
import PriceEdit from "./price-edit.vue";
import DurationPriceValue from "/@/views/sys/suite/product/duration-price-value.vue";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { t } = useI18n();
@@ -55,15 +55,15 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
groups: {
base: {
header: t("certd.basicInfo"),
columns: [t("certd.titlea"), t("certd.type"), t("certd.disabled"), t("certd.ordera"), t("certd.supportBuy"), t("certd.intro")],
columns: ["title", "type", "disabled", "order", "supportBuy", "intro"]
},
content: {
header: t("certd.packageContent"),
columns: [t("certd.maxDomainCount"), t("certd.maxPipelineCount"), t("certd.maxDeployCount"), t("certd.maxMonitorCount")],
columns: ["content.maxDomainCount", "content.maxPipelineCount", "content.maxDeployCount", "content.maxMonitorCount"]
},
price: {
header: t("certd.price"),
columns: [t("certd.durationPrices")],
columns: ["durationPrices"]
},
},
},

View File

@@ -1,5 +1,5 @@
import * as api from "./api";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
import { computed, Ref, ref } from "vue";
import { useRouter } from "vue-router";
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes, utils } from "@fast-crud/fast-crud";

View File

@@ -0,0 +1,19 @@
CREATE TABLE "cd_domain"
(
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
"user_id" integer,
"domain" varchar(1024),
challenge_type varchar(50),
dns_provider_type varchar(50),
dns_provider_access bigint,
http_uploader_type varchar(50),
http_uploader_access bigint,
http_upload_root_dir varchar(512),
"disabled" boolean NOT NULL DEFAULT (false),
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
);
CREATE INDEX "index_domain_user_id" ON "cd_domain" ("user_id");
CREATE INDEX "index_domain_domain" ON "cd_domain" ("domain");

View File

@@ -0,0 +1,81 @@
import { ALL, Body, Controller, Inject, Post, Provide, Query } from '@midwayjs/core';
import { Constants, CrudController } from '@certd/lib-server';
import {DomainService} from "../../../modules/cert/service/domain-service.js";
/**
* 授权
*/
@Provide()
@Controller('/api/cert/domain')
export class DomainController extends CrudController<DomainService> {
@Inject()
service: DomainService;
getService(): DomainService {
return this.service;
}
@Post('/page', { summary: Constants.per.authOnly })
async page(@Body(ALL) body: any) {
body.query = body.query ?? {};
body.query.userId = this.getUserId();
const domain = body.query.domain;
delete body.query.domain;
const bq = qb => {
if (domain) {
qb.andWhere('domain like :domain', { domain: `%${domain}%` });
}
};
const pageRet = await this.getService().page({
query: body.query,
page: body.page,
sort: body.sort,
buildQuery: bq,
});
return this.ok(pageRet);
}
@Post('/list', { summary: Constants.per.authOnly })
async list(@Body(ALL) body: any) {
body.query = body.query ?? {};
body.query.userId = this.getUserId();
const list = await this.getService().list(body);
return this.ok(list);
}
@Post('/add', { summary: Constants.per.authOnly })
async add(@Body(ALL) bean: any) {
bean.userId = this.getUserId();
return super.add(bean);
}
@Post('/update', { summary: Constants.per.authOnly })
async update(@Body(ALL) bean: any) {
await this.service.checkUserId(bean.id, this.getUserId());
delete bean.userId;
return super.update(bean);
}
@Post('/info', { summary: Constants.per.authOnly })
async info(@Query('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
return super.info(id);
}
@Post('/delete', { summary: Constants.per.authOnly })
async delete(@Query('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
return super.delete(id);
}
@Post('/deleteByIds', { summary: Constants.per.authOnly })
async deleteByIds(@Body(ALL) body: any) {
await this.service.delete(body.ids, {
userId: this.getUserId(),
});
return this.ok();
}
}

View File

@@ -101,4 +101,10 @@ export class AccessController extends CrudController<AccessService> {
const res = await this.service.getSimpleInfo(id);
return this.ok(res);
}
@Post('/getDictByIds', { summary: Constants.per.authOnly })
async getDictByIds(@Body('ids') ids: number[]) {
const res = await this.service.getSimpleByIds(ids, this.getUserId());
return this.ok(res);
}
}

View File

@@ -14,7 +14,7 @@ import {
import {EmailService} from '../../../modules/basic/service/email-service.js';
import {http, HttpRequestConfig, logger, mergeUtils, utils} from '@certd/basic';
import {NotificationService} from '../../../modules/pipeline/service/notification-service.js';
import {TaskServiceBuilder} from "../../../modules/pipeline/service/task-service-getter.js";
import {TaskServiceBuilder} from "../../../modules/pipeline/service/getter/task-service-getter.js";
@Provide()
@Controller('/api/pi/handle')

View File

@@ -1,7 +1,8 @@
import {ALL, Body, Controller, Inject, Post, Provide, Query} from '@midwayjs/core';
import {Constants, CrudController} from '@certd/lib-server';
import {SubDomainService, SubDomainsGetter} from "../../../modules/pipeline/service/sub-domain-service.js";
import {SubDomainService} from "../../../modules/pipeline/service/sub-domain-service.js";
import {DomainParser} from '@certd/plugin-cert/dist/dns-provider/domain-parser.js';
import { SubDomainsGetter } from '../../../modules/pipeline/service/getter/sub-domain-getter.js';
/**
* 子域名托管

View File

@@ -0,0 +1,50 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
/**
* 域名管理
*/
@Entity('cd_domain')
export class DomainEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ comment: '用户ID', name: 'user_id' })
userId: number;
@Column({ comment: '主域名', length: 100 })
domain: string;
@Column({ comment: '校验类型', name: 'challenge_type', length: 50 })
challengeType : string;
@Column({ comment: 'DNS提供商', name: 'dns_provider_type', length: 50 })
dnsProviderType: string;
@Column({ comment: 'DNS提供商授权', name: 'dns_provider_access' })
dnsProviderAccess: number;
@Column({ comment: '是否禁用', name: 'disabled' })
disabled: boolean;
@Column({ comment: 'http上传类型', name: 'http_uploader_type', length: 50 })
httpUploaderType: string;
@Column({ comment: 'http上传授权', name: 'http_uploader_access' })
httpUploaderAccess: number;
@Column({ comment: 'http上传根目录', name: 'http_upload_root_dir', length: 512 })
httpUploadRootDir: string;
@Column({
comment: '创建时间',
name: 'create_time',
default: () => 'CURRENT_TIMESTAMP',
})
createTime: Date;
@Column({
comment: '修改时间',
name: 'update_time',
default: () => 'CURRENT_TIMESTAMP',
})
updateTime: Date;
}

View File

@@ -0,0 +1,190 @@
import {Inject, Provide, Scope, ScopeEnum} from '@midwayjs/core';
import {InjectEntityModel} from '@midwayjs/typeorm';
import {In, Not, Repository} from 'typeorm';
import {AccessService, BaseService} from '@certd/lib-server';
import {DomainEntity} from '../entity/domain.js';
import {SubDomainService} from "../../pipeline/service/sub-domain-service.js";
import {DomainParser} from "@certd/plugin-cert/dist/dns-provider/domain-parser.js";
import {DomainVerifiers} from "@certd/plugin-cert";
import { SubDomainsGetter } from '../../pipeline/service/getter/sub-domain-getter.js';
import { CnameRecordService } from '../../cname/service/cname-record-service.js';
import { CnameRecordEntity } from "../../cname/entity/cname-record.js";
/**
*
*/
@Provide()
@Scope(ScopeEnum.Request, {allowDowngrade: true})
export class DomainService extends BaseService<DomainEntity> {
@InjectEntityModel(DomainEntity)
repository: Repository<DomainEntity>;
@Inject()
accessService: AccessService;
@Inject()
subDomainService: SubDomainService;
@Inject()
cnameRecordService: CnameRecordService;
//@ts-ignore
getRepository() {
return this.repository;
}
async add(param) {
if (param.userId == null ){
throw new Error('userId 不能为空');
}
if (!param.domain) {
throw new Error('domain 不能为空');
}
const old = await this.repository.findOne({
where: {
domain: param.domain,
userId: param.userId
}
});
if (old) {
throw new Error(`域名(${param.domain})不能重复`);
}
return await super.add(param);
}
async update(param) {
if (!param.id) {
throw new Error('id 不能为空');
}
const old = await this.info(param.id)
if (!old) {
throw new Error('domain记录不存在');
}
const same = await this.repository.findOne({
where: {
domain: param.domain,
userId: old.userId,
id: Not(param.id)
}
});
if (same) {
throw new Error(`域名(${param.domain})不能重复`);
}
delete param.userId
return await super.update(param);
}
/**
*
* @param userId
* @param domains //去除* 且去重之后的域名列表
*/
async getDomainVerifiers(userId: number, domains: string[]):Promise<DomainVerifiers> {
const mainDomainMap:Record<string, string> = {}
const subDomainGetter = new SubDomainsGetter(userId, this.subDomainService)
const domainParser = new DomainParser(subDomainGetter)
const mainDomains = []
for (const domain of domains) {
const mainDomain = await domainParser.parse(domain);
mainDomainMap[domain] = mainDomain;
mainDomains.push(mainDomain)
}
//匹配DNS记录
let allDomains = [...domains,...mainDomains]
//去重
allDomains = [...new Set(allDomains)]
//从 domain 表中获取配置
const domainRecords = await this.find({
where: {
domain: In(allDomains),
userId,
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
}, {})
//从cname record表中获取配置
const cnameRecords = await this.cnameRecordService.find({
where: {
domain: In(allDomains),
userId,
status: "valid",
}
})
const cnameMap = cnameRecords.reduce((pre, item) => {
pre[item.domain] = item
return pre
}, {})
//构建域名验证计划
const domainVerifiers:DomainVerifiers = {}
for (const domain of domains) {
const mainDomain = mainDomainMap[domain]
const dnsRecord = dnsMap[mainDomain]
if (dnsRecord) {
domainVerifiers[domain] = {
domain,
mainDomain,
type: 'dns',
dns: {
dnsProviderType: dnsRecord.dnsProviderType,
dnsProviderAccessId: dnsRecord.dnsProviderAccess
}
}
continue
}
const cnameRecord:CnameRecordEntity = cnameMap[mainDomain]
if (cnameRecord) {
domainVerifiers[domain] = {
domain,
mainDomain,
type: 'cname',
cname: {
domain: cnameRecord.domain,
hostRecord: cnameRecord.hostRecord,
recordValue: cnameRecord.recordValue
}
}
continue
}
const httpRecord = httpMap[mainDomain]
if (httpRecord) {
domainVerifiers[domain] = {
domain,
mainDomain,
type: 'http',
http: {
httpUploaderType: httpRecord.httpUploaderType,
httpUploaderAccess: httpRecord.httpUploaderAccess,
httpUploadRootDir: httpRecord.httpUploadRootDir
}
}
continue
}
domainVerifiers[domain] = null
}
return domainVerifiers;
}
}

View File

@@ -10,9 +10,10 @@ import {getAuthoritativeDnsResolver, walkTxtRecord} from '@certd/acme-client';
import {CnameProviderService} from './cname-provider-service.js';
import {CnameProviderEntity} from '../entity/cname-provider.js';
import {CommonDnsProvider} from './common-provider.js';
import {SubDomainService, SubDomainsGetter} from "../../pipeline/service/sub-domain-service.js";
import {DomainParser} from "@certd/plugin-cert/dist/dns-provider/domain-parser.js";
import punycode from 'punycode.js'
import {SubDomainService} from "../../pipeline/service/sub-domain-service.js";
import {SubDomainsGetter} from "../../pipeline/service/getter/sub-domain-getter.js";
type CnameCheckCacheValue = {
validating: boolean;
pass: boolean;

View File

@@ -0,0 +1,17 @@
import {DomainVerifiers, IDomainVerifierGetter} from "@certd/plugin-cert";
import {DomainService} from "../../../cert/service/domain-service.js";
export class DomainVerifierGetter implements IDomainVerifierGetter {
private userId: number;
private domainService: DomainService;
constructor(userId: number, domainService: DomainService) {
this.userId = userId;
this.domainService = domainService;
}
async getVerifiers(domains: string[]): Promise<DomainVerifiers>{
return await this.domainService.getDomainVerifiers(this.userId,domains);
}
}

View File

@@ -1,5 +1,5 @@
import { INotificationService, NotificationSendReq } from '@certd/pipeline';
import { NotificationService } from './notification-service.js';
import {NotificationService} from "../notification-service.js";
export class NotificationGetter implements INotificationService {
userId: number;

View File

@@ -0,0 +1,17 @@
import {ISubDomainsGetter} from "@certd/plugin-cert";
import {SubDomainService} from "../sub-domain-service.js";
export class SubDomainsGetter implements ISubDomainsGetter {
userId: number;
subDomainService: SubDomainService;
constructor(userId: number, subDomainService: SubDomainService) {
this.userId = userId;
this.subDomainService = subDomainService;
}
async getSubDomains() {
return await this.subDomainService.getListByUserId(this.userId)
}
}

View File

@@ -0,0 +1,81 @@
import {IServiceGetter} from "@certd/pipeline";
import {Inject, Provide, Scope, ScopeEnum} from "@midwayjs/core";
import {AccessGetter, AccessService} from "@certd/lib-server";
import {CnameProxyService} from "./cname-proxy-service.js";
import {NotificationGetter} from "./notification-getter.js";
import {NotificationService} from "../notification-service.js";
import {CnameRecordService} from "../../../cname/service/cname-record-service.js";
import {SubDomainsGetter} from './sub-domain-getter.js'
import {DomainVerifierGetter} from "./domain-verifier-getter.js";
import {Context} from "@midwayjs/koa";
import {DomainService} from "../../../cert/service/domain-service.js";
import {SubDomainService} from "../sub-domain-service.js";
export class TaskServiceGetter implements IServiceGetter{
private userId: number;
private ctx : Context;
constructor(userId:number,ctx:Context) {
this.userId = userId;
this.ctx = ctx
}
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{
throw new Error(`service ${serviceName} not found`)
}
}
async getSubDomainsGetter(): Promise<SubDomainsGetter> {
const subDomainsService:SubDomainService = await this.ctx.requestContext.getAsync("subDomainService")
return new SubDomainsGetter(this.userId, subDomainsService)
}
async getAccessService(): Promise<AccessGetter> {
const accessService:AccessService = await this.ctx.requestContext.getAsync("accessService")
return new AccessGetter(this.userId, accessService.getById.bind(accessService));
}
async getCnameProxyService(): Promise<CnameProxyService> {
const cnameRecordService:CnameRecordService = await this.ctx.requestContext.getAsync("cnameRecordService")
return new CnameProxyService(this.userId, cnameRecordService.getWithAccessByDomain.bind(cnameRecordService));
}
async getNotificationService(): Promise<NotificationGetter> {
const notificationService:NotificationService = await this.ctx.requestContext.getAsync("notificationService")
return new NotificationGetter(this.userId, notificationService);
}
async getDomainVerifierGetter(): Promise<DomainVerifierGetter> {
const domainService:DomainService = await this.ctx.requestContext.getAsync("domainService")
return new DomainVerifierGetter(this.userId, domainService);
}
}
export type TaskServiceCreateReq = {
userId: number;
}
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class TaskServiceBuilder {
@Inject()
ctx: Context;
create(req:TaskServiceCreateReq){
const userId = req.userId;
return new TaskServiceGetter(userId,this.ctx)
}
}

View File

@@ -44,7 +44,7 @@ import {UrlService} from "./url-service.js";
import {NotificationService} from "./notification-service.js";
import {UserSuiteEntity, UserSuiteService} from "@certd/commercial-core";
import {CertInfoService} from "../../monitor/service/cert-info-service.js";
import {TaskServiceBuilder} from "./task-service-getter.js";
import {TaskServiceBuilder} from "./getter/task-service-getter.js";
import {nanoid} from "nanoid";
import {set} from "lodash-es";

View File

@@ -4,7 +4,6 @@ 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 {ISubDomainsGetter} from "@certd/plugin-cert";
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
@@ -38,21 +37,3 @@ export class SubDomainService extends BaseService<SubDomainEntity> {
}
}
export class SubDomainsGetter implements ISubDomainsGetter {
userId: number;
subDomainService: SubDomainService;
constructor(userId: number, subDomainService: SubDomainService) {
this.userId = userId;
this.subDomainService = subDomainService;
}
async getSubDomains() {
return await this.subDomainService.getListByUserId(this.userId)
}
}

View File

@@ -1,63 +0,0 @@
import {IServiceGetter} from "@certd/pipeline";
import {Inject, Provide, Scope, ScopeEnum} from "@midwayjs/core";
import {SubDomainService, SubDomainsGetter} from "./sub-domain-service.js";
import {AccessGetter, AccessService} from "@certd/lib-server";
import {CnameProxyService} from "./cname-proxy-service.js";
import {NotificationGetter} from "./notification-getter.js";
import {NotificationService} from "./notification-service.js";
import {CnameRecordService} from "../../cname/service/cname-record-service.js";
export class TaskServiceGetter implements IServiceGetter{
serviceContainer:Record<string, any>;
constructor(serviceContainer:Record<string, any>) {
this.serviceContainer = serviceContainer;
}
async get<T>(serviceName: string): Promise<T> {
const ret = this.serviceContainer[serviceName] as T;
if(!ret){
throw new Error(`service ${serviceName} not found`)
}
return ret
}
}
export type TaskServiceCreateReq = {
userId: number;
}
export type TaskServiceContainer = {
subDomainsGetter:SubDomainsGetter;
accessService: AccessGetter;
cnameProxyService: CnameProxyService;
notificationService: NotificationGetter;
}
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class TaskServiceBuilder {
@Inject()
subDomainService: SubDomainService;
@Inject()
accessService: AccessService;
@Inject()
cnameRecordService: CnameRecordService;
@Inject()
notificationService: NotificationService;
create(req:TaskServiceCreateReq){
const userId = req.userId;
const accessGetter = new AccessGetter(userId, this.accessService.getById.bind(this.accessService));
const cnameProxyService = new CnameProxyService(userId, this.cnameRecordService.getWithAccessByDomain.bind(this.cnameRecordService));
const notificationGetter = new NotificationGetter(userId, this.notificationService);
const serviceContainer:TaskServiceContainer = {
subDomainsGetter:new SubDomainsGetter(req.userId, this.subDomainService),
accessService: accessGetter,
cnameProxyService:cnameProxyService,
notificationService:notificationGetter
}
return new TaskServiceGetter(serviceContainer)
}
}

View File

@@ -47,7 +47,7 @@ export class GithubCheckRelease extends AbstractTaskPlugin {
mode:"tags"
}
},
required:true,
required:false,
})
notificationIds!: number[];
@@ -74,9 +74,21 @@ export class GithubCheckRelease extends AbstractTaskPlugin {
name: 'a-textarea',
vModel: 'value',
rows: 6,
placeholder: `#拉取最新版镜像\ndocker pull greper/certd:latest \n#重建容器 \nnohup sh -c 'sleep 10; cd ~/deploy/certd/ ; docker compose down; docker compose up -d' >/dev/null & `,
placeholder: `
# 拉取最新镜像
docker pull registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest
# 升级容器命令, 替换成你自己的实际部署位置及更新命令
export RESTART_CERT='sleep 10; cd ~/deploy/certd/ ; docker compose down; docker compose up -d'
# 构造一个脚本10s后在后台执行避免容器销毁时执行太快导致流水线任务无法结束
nohup sh -c '$RESTART_CERT' >/dev/null 2>&1 & echo '10秒后重启' && exit`,
},
helper: '有新版本后执行命令,比如:拉取最新版镜像,然后重建容器\n注意自己升级自己需要使用nobup配合sleep',
helper: `有新版本后执行命令,比如:拉取最新版镜像,然后重建容器
注意自己升级自己需要使用nohup配合sleep
自动升级命令示例:
docker pull registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest
export RESTART_CERT='sleep 10; cd ~/deploy/certd/ ; docker compose down; docker compose up -d'
nohup sh -c '$RESTART_CERT' >/dev/null 2>&1 & echo '10秒后重启' && exit
`,
required: false,
})
script!: string;
@@ -108,24 +120,24 @@ export class GithubCheckRelease extends AbstractTaskPlugin {
//仅每行开头的* 替换成 - *号前面可以有空格
const body = res.body.replace(/^(\s*)\* /gm, "$1- ")
if (this.notificationIds == null){
this.notificationIds = [0]
}
//发送通知
for (const notificationId of this.notificationIds) {
await this.ctx.notificationService.send({
id: notificationId,
useDefault: false,
useEmail:false,
logger: this.logger,
body: {
title: `${this.repoName} 新版本 ${this.lastVersion} 发布`,
content: `${body}\n\n > [Certd](https://certd.docmirror.cn),不止证书自动化,插件解锁无限可能!\n\n`,
url: `https://github.com/${this.repoName}/releases/tag/${this.lastVersion}`,
}
})
if (this.notificationIds && this.notificationIds.length > 0){
//发送通知
for (const notificationId of this.notificationIds) {
await this.ctx.notificationService.send({
id: notificationId,
useDefault: false,
useEmail:false,
logger: this.logger,
body: {
title: `${this.repoName} 新版本 ${this.lastVersion} 发布`,
content: `${body}\n\n > [Certd](https://certd.docmirror.cn),不止证书自动化,插件解锁无限可能!\n\n`,
url: `https://github.com/${this.repoName}/releases/tag/${this.lastVersion}`,
}
})
}
}
if (this.script != null && this.script.trim() != "") {
const connectConf = await this.getAccess(this.sshAccessId);
const sshClient = new SshClient(this.logger);