Compare commits

..

32 Commits

Author SHA1 Message Date
xiaojunnuo
bef6b981e2 v1.24.1 2024-09-02 23:57:42 +08:00
xiaojunnuo
a77cd65789 build: prepare to build 2024-09-02 23:56:04 +08:00
xiaojunnuo
415b731d9a chore: 2024-09-02 23:55:38 +08:00
xiaojunnuo
6c0099d600 chore: 2024-09-02 23:47:15 +08:00
xiaojunnuo
98b77f8084 perf: 支持阿里云 DCDN 2024-09-02 23:46:28 +08:00
Greper
2f47ffb76b Merge pull request #148 from wujingke/v2
pref: 添加阿里云DCDN 废弃SetDomainServerCertificate接口 改为SetCdnDomainSSLCertificate
2024-09-02 22:00:36 +08:00
w
35a3603c41 添加阿里云DCDN 废弃SetDomainServerCertificate接口 改为SetCdnDomainSSLCertificate 2024-09-02 19:33:17 +08:00
xiaojunnuo
ea775adae1 perf: 支持已跳过的步骤重新运行 2024-09-02 18:36:12 +08:00
xiaojunnuo
724a85028b perf: 支持cdnfly 2024-09-02 16:59:49 +08:00
xiaojunnuo
b2d595e85c Merge remote-tracking branch 'origin/v2' into v2 2024-09-02 15:50:43 +08:00
xiaojunnuo
d9b1ff8c5c chore: 2024-09-02 15:49:56 +08:00
xiaojunnuo
1c17970b98 fix: 激活仅限管理员 2024-09-02 01:02:41 +08:00
xiaojunnuo
b9bddbfabb perf: 支持ftp上传 2024-09-01 04:49:26 +08:00
xiaojunnuo
ee617095ef perf: 部署插件支持宝塔、易盾云等 2024-08-30 18:52:31 +08:00
xiaojunnuo
bee20c7f51 chore: 1 2024-08-29 11:15:45 +08:00
xiaojunnuo
b8e05e9b44 chore: 2024-08-29 10:09:22 +08:00
xiaojunnuo
869e14bad9 pref: 自动优化数据库,释放被删除空间 2024-08-29 09:57:27 +08:00
xiaojunnuo
952e01ab7d chore: 2024-08-28 14:45:57 +08:00
xiaojunnuo
db61033633 perf: 优化内存占用 2024-08-28 14:40:50 +08:00
xiaojunnuo
42a56b581d perf: 授权配置支持加密
原本已经添加的授权配置,再次编辑保存即变成加密配置
2024-08-27 13:46:19 +08:00
xiaojunnuo
d6bb9f6af4 chore: 2024-08-26 12:37:42 +08:00
xiaojunnuo
a430b27034 chore: 2024-08-26 12:32:36 +08:00
xiaojunnuo
0f6679425f fix: 修复在没有勾选使用代理的情况下,仍然会使用代理的bug 2024-08-26 11:34:01 +08:00
xiaojunnuo
4b9d1eb4b5 chore: 2024-08-26 11:06:46 +08:00
xiaojunnuo
ca4a1b8d92 chore: 2024-08-26 11:06:28 +08:00
xiaojunnuo
08a702a758 chore: 2024-08-26 10:19:06 +08:00
xiaojunnuo
589191244f chore: 2024-08-26 10:09:53 +08:00
xiaojunnuo
f3ddcd3054 Merge remote-tracking branch 'origin/v2' into v2 2024-08-26 09:59:10 +08:00
xiaojunnuo
f923655d91 chore: 2024-08-26 09:58:51 +08:00
xiaojunnuo
879e2609ca chore: 2024-08-25 15:35:04 +08:00
xiaojunnuo
d227dd64e3 chore: 2024-08-25 15:34:00 +08:00
xiaojunnuo
d2997624b0 build: trigger build image 2024-08-25 14:28:10 +08:00
120 changed files with 1059 additions and 1041 deletions

View File

@@ -58,8 +58,14 @@ jobs:
username: ${{ secrets.aliyun_cs_username }}
password: ${{ secrets.aliyun_cs_password }}
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.dockerhub_username }}
password: ${{ secrets.dockerhub_password }}
- name: Build and push
uses: docker/build-push-action@v6.5.0
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
push: true
@@ -67,3 +73,5 @@ jobs:
tags: |
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{steps.get_certd_version.outputs.result}}
greper/certd:latest
greper/certd:${{steps.get_certd_version.outputs.result}}

1
.gitignore vendored
View File

@@ -39,3 +39,4 @@ tsconfig.tsbuildinfo
test/**/*.js
/packages/ui/certd-server/data/db.sqlite
/packages/ui/certd-server/data/keys.yaml
/packages/pro/

View File

@@ -3,6 +3,23 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.24.1](https://github.com/certd/certd/compare/v1.24.0...v1.24.1) (2024-09-02)
### Bug Fixes
* 激活仅限管理员 ([1c17970](https://github.com/certd/certd/commit/1c17970b981f0987c506744ee6b2283fd5e40493))
* 修复在没有勾选使用代理的情况下仍然会使用代理的bug ([0f66794](https://github.com/certd/certd/commit/0f6679425f6a736bb0128527dd99c085fac17d84))
### Performance Improvements
* 部署插件支持宝塔、易盾云等 ([ee61709](https://github.com/certd/certd/commit/ee617095efa1171548cf52fd45f0f98a368555a3))
* 授权配置支持加密 ([42a56b5](https://github.com/certd/certd/commit/42a56b581d754c3e5f9838179d19ab0d004ef2eb))
* 优化内存占用 ([db61033](https://github.com/certd/certd/commit/db6103363364440b650bc10bb334834e4a9470c7))
* 支持阿里云 DCDN ([98b77f8](https://github.com/certd/certd/commit/98b77f80843834616fb26f83b4c42245326abd06))
* 支持已跳过的步骤重新运行 ([ea775ad](https://github.com/certd/certd/commit/ea775adae18d57a04470cfba6b9460d761d74035))
* 支持cdnfly ([724a850](https://github.com/certd/certd/commit/724a85028b4a7146c9e3b4df4497dcf2a7bf7c67))
* 支持ftp上传 ([b9bddbf](https://github.com/certd/certd/commit/b9bddbfabb5664365f1232e9432532187c98006c))
# [1.24.0](https://github.com/certd/certd/compare/v1.23.1...v1.24.0) (2024-08-25)
### Bug Fixes

View File

@@ -10,10 +10,15 @@ Certd 是一个免费全自动申请和自动部署更新SSL证书的工具。
https://afdian.com/a/greper
发电权益:
1. 可加入发电专属群(先加我好友,发送发电截图,我拉你进群)
2. 的需求优先实现
3. 可以获得作者一对一技术支持
4. 更多权益陆续增加中...
1. 可加入发电专属群,可以获得作者一对一技术支持
2. 的需求我们将优先实现,并且将作为专业版功能提供
3. 一年期专业版激活码
4. 赠送国外免费服务器部署方案0成本使用Certd不过该服务器需要翻墙
专业版特权
1. 证书流水线条数无限制免费版限制10条
2. 免配置发邮件功能
3. 更多功能增加中...
************************
## 一、特性
@@ -93,8 +98,11 @@ docker compose up -d
> https://docs.docker.com/compose/install/linux/
#### 镜像说明:
* certd镜像地址:
* 国内镜像地址:
* `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest`
* DockerHub地址
* `https://hub.docker.com/r/greper/certd`
* `docker pull greper/certd:latest`
* 镜像构建通过`Actions`自动执行,过程公开透明,请放心使用
* [点我查看镜像构建日志](https://github.com/certd/certd/actions/workflows/build-image.yml)
@@ -112,11 +120,16 @@ http://your_server_ip:7001
## 五、 升级
如果使用固定版本号
1. 修改`docker-compose.yaml`中的镜像版本号
2. 运行 `docker compose up -d` 即可
2. 运行`docker compose up -d` 即可
如果使用`latest`版本
1. 重新拉取镜像 `docker pull registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest`
2. 重新启动容器 `docker compose restart`
```shell
#重新拉取镜像
docker pull registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest
# 重新启动容器
docker compose down
docker compose up -d
```
> 数据默认存在`/data/certd`目录下,不用担心数据丢失

View File

@@ -1 +1 @@
11:39
1

View File

@@ -0,0 +1,14 @@
version: '3.3'
services:
ftp:
# 镜像 # ↓↓↓↓↓ --- 1、 镜像版本号,建议改成固定版本号【可选】
image: gists/pure-ftpd
container_name: ftp # 容器名
restart: unless-stopped # 自动重启
volumes:
- /data/ftp2/:/home/ftpuser
ports: # 端口映射
- "21:21"
- "30000-30009:30000-30009"
environment: # 环境变量
- TZ=Asia/Shanghai

View File

@@ -9,5 +9,5 @@
}
},
"npmClient": "pnpm",
"version": "1.24.0"
"version": "1.24.1"
}

1
packages/certd-pro Submodule

Submodule packages/certd-pro added at 0dec0d461f

View File

@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.24.1](https://github.com/publishlab/node-acme-client/compare/v1.24.0...v1.24.1) (2024-09-02)
### Bug Fixes
* 修复在没有勾选使用代理的情况下仍然会使用代理的bug ([0f66794](https://github.com/publishlab/node-acme-client/commit/0f6679425f6a736bb0128527dd99c085fac17d84))
### Performance Improvements
* 部署插件支持宝塔、易盾云等 ([ee61709](https://github.com/publishlab/node-acme-client/commit/ee617095efa1171548cf52fd45f0f98a368555a3))
* 授权配置支持加密 ([42a56b5](https://github.com/publishlab/node-acme-client/commit/42a56b581d754c3e5f9838179d19ab0d004ef2eb))
# [1.24.0](https://github.com/publishlab/node-acme-client/compare/v1.23.1...v1.24.0) (2024-08-25)
### Bug Fixes

View File

@@ -3,7 +3,7 @@
"description": "Simple and unopinionated ACME client",
"private": false,
"author": "nmorsman",
"version": "1.24.0",
"version": "1.24.1",
"main": "src/index.js",
"types": "types/index.d.ts",
"license": "MIT",

View File

@@ -13,8 +13,12 @@ const defaultOpts = {
termsOfServiceAgreed: false,
skipChallengeVerification: false,
challengePriority: ['http-01', 'dns-01'],
challengeCreateFn: async () => { throw new Error('Missing challengeCreateFn()'); },
challengeRemoveFn: async () => { throw new Error('Missing challengeRemoveFn()'); },
challengeCreateFn: async () => {
throw new Error('Missing challengeCreateFn()');
},
challengeRemoveFn: async () => {
throw new Error('Missing challengeRemoveFn()');
},
};
/**
@@ -209,6 +213,7 @@ module.exports = async (client, userOpts) => {
}
log(`[auto] challengeGroups:${allChallengePromises.length}`);
function runAllPromise(tasks) {
let promise = Promise.resolve();
tasks.forEach((task) => {
@@ -228,48 +233,48 @@ module.exports = async (client, userOpts) => {
return Promise.all(results);
}
try {
log(`开始challenge${allChallengePromises.length}`);
let i = 0;
// eslint-disable-next-line no-restricted-syntax
for (const challengePromises of allChallengePromises) {
i += 1;
log(`开始第${i}`);
if (opts.signal && opts.signal.aborted) {
throw new Error('用户取消');
}
log(`开始challenge${allChallengePromises.length}`);
let i = 0;
// eslint-disable-next-line no-restricted-syntax
for (const challengePromises of allChallengePromises) {
i += 1;
log(`开始第${i}`);
if (opts.signal && opts.signal.aborted) {
throw new Error('用户取消');
}
try {
// eslint-disable-next-line no-await-in-loop
await runPromisePa(challengePromises);
}
log('challenge结束');
// log('[auto] Waiting for challenge valid status');
// await Promise.all(challengePromises);
/**
* Finalize order and download certificate
*/
log('[auto] Finalizing order and downloading certificate');
const finalized = await client.finalizeOrder(order, opts.csr);
return await client.getCertificate(finalized, opts.preferredChain);
}
catch (e) {
log('证书申请失败');
log(e);
throw new Error(`证书申请失败:${e.message}`);
}
finally {
log(`清理challenge痕迹length:${clearTasks.length}`);
try {
await runAllPromise(clearTasks);
}
catch (e) {
log('清理challenge失败');
log(e);
log(`证书申请失败${e.message}`);
throw e;
}
finally {
log(`清理challenge痕迹length:${clearTasks.length}`);
try {
// eslint-disable-next-line no-await-in-loop
await runAllPromise(clearTasks);
}
catch (e) {
log('清理challenge失败');
log(e);
}
}
}
log('challenge结束');
// log('[auto] Waiting for challenge valid status');
// await Promise.all(challengePromises);
/**
* Finalize order and download certificate
*/
log('[auto] Finalizing order and downloading certificate');
const finalized = await client.finalizeOrder(order, opts.csr);
const res = await client.getCertificate(finalized, opts.preferredChain);
return res;
// try {
// await Promise.allSettled(challengePromises);
// }

View File

@@ -290,7 +290,6 @@ exports.readCsrDomains = (csrPem) => {
if (Buffer.isBuffer(csrPem)) {
csrPem = csrPem.toString();
}
const dec = x509.PemConverter.decodeFirst(csrPem);
const csr = new x509.Pkcs10CertificateRequest(dec);
return parseDomains(csr);

View File

@@ -55,7 +55,7 @@ class HttpClient {
*/
async request(url, method, opts = {}) {
if (this.urlMapping && this.urlMapping.mappings) {
if (this.urlMapping && this.urlMapping.enabled && this.urlMapping.mappings) {
// eslint-disable-next-line no-restricted-syntax
for (const key in this.urlMapping.mappings) {
if (url.includes(key)) {

View File

@@ -3,6 +3,15 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.24.1](https://github.com/certd/certd/compare/v1.24.0...v1.24.1) (2024-09-02)
### Performance Improvements
* 部署插件支持宝塔、易盾云等 ([ee61709](https://github.com/certd/certd/commit/ee617095efa1171548cf52fd45f0f98a368555a3))
* 授权配置支持加密 ([42a56b5](https://github.com/certd/certd/commit/42a56b581d754c3e5f9838179d19ab0d004ef2eb))
* 支持阿里云 DCDN ([98b77f8](https://github.com/certd/certd/commit/98b77f80843834616fb26f83b4c42245326abd06))
* 支持已跳过的步骤重新运行 ([ea775ad](https://github.com/certd/certd/commit/ea775adae18d57a04470cfba6b9460d761d74035))
# [1.24.0](https://github.com/certd/certd/compare/v1.23.1...v1.24.0) (2024-08-25)
### Bug Fixes

View File

@@ -1 +1 @@
14:26
23:56

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/pipeline",
"private": false,
"version": "1.24.0",
"version": "1.24.1",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -14,6 +14,7 @@
"test": "mocha --loader=ts-node/esm"
},
"dependencies": {
"@certd/plus": "1.22.1",
"axios": "^1.7.2",
"fix-path": "^4.0.0",
"lodash-es": "^4.17.21",

View File

@@ -4,6 +4,7 @@ import { FormItemProps } from "../dt/index.js";
export type AccessInputDefine = FormItemProps & {
title: string;
required?: boolean;
encrypt?: boolean;
};
export type AccessDefine = Registrable & {
input?: {

View File

@@ -36,6 +36,8 @@ export class Executor {
options: ExecutorOptions;
abort: AbortController = new AbortController();
_inited = false;
onChanged: (history: RunHistory) => Promise<void>;
constructor(options: ExecutorOptions) {
this.options = options;
@@ -50,6 +52,10 @@ export class Executor {
}
async init() {
if (this._inited) {
return;
}
this._inited = true;
const lastRuntime = await this.pipelineContext.getObj(`lastRuntime`);
this.lastRuntime = lastRuntime;
this.lastStatusMap = new RunnableCollection(lastRuntime?.pipeline);
@@ -77,9 +83,9 @@ export class Executor {
await this.notification("turnToSuccess");
}
await this.notification("success");
} catch (e) {
} catch (e: any) {
await this.notification("error", e);
this.logger.error("pipeline 执行失败", e);
this.logger.error("pipeline 执行失败", e.stack);
} finally {
await this.pipelineContext.setObj("lastRuntime", this.runtime);
this.logger.info(`pipeline.${this.pipeline.id} end`);
@@ -315,4 +321,8 @@ export class Executor {
}
}
}
clearLastStatus(stepId: string) {
this.lastStatusMap.clearById(stepId);
}
}

View File

@@ -1,14 +0,0 @@
import { isPlus, verify } from "./license.js";
import { equal } from "assert";
describe("license", function () {
it("#license", async function () {
const req = {
subjectId: "999",
license: "",
};
const plus = isPlus();
equal(plus, false);
const res = await verify(req);
equal(res, true);
});
});

View File

@@ -1,138 +1,4 @@
import { createVerify } from "node:crypto";
import { logger } from "../utils/index.js";
import dayjs from "dayjs";
let SecreteKey =
"LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJDZ0tDQVFFQW9VWE1EWUhjdi82WFROWEZFSUI2RlpuR2FER0cwZnR5bTV1dVhPck9NaVl0UkxSb1lvSGMKNVZxenE0N00rdEFqRFBhaTBlOFhWS1c3aytUQUw3MUs0N2JCQVEyWTBxNU5Ya3lYcE5PTVdueVFMYXBwb0tWNgpPMkFJMnpFVURWMVJVa0ZtMFZTVjU0VXNzMDcrdjI2aW5aQU1CWitDMU42eWFDc2tZL3grNnVlNkVRNVcyZXdFCjZOWEhJcUU1bHdEUmU2SXJtdEpnU2doSnlHTS91azIyejN6NGEraFVPVUlWMy9DbEhYV0VhRHBBRFFsakt3NSsKeHR0dURiTHZyUmdzdWp6czB0dEI2OE1SbXE0R0FJL0JtNWVPWkhlNGxFQjBFVVhFUXdVWE1jV1N1VFZSMUE2cApUM21LRGo5MGcwVDFZUlNOdE5TMm9aRzgvRWIwOVlxK3Z3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K";
let appKey = "kQth6FHM71IPV3qdWc";
if (process.env.NODE_ENV !== "production") {
SecreteKey =
"LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJDZ0tDQVFFQXY3TGtMaUp1dGM0NzhTU3RaTExjajVGZXh1YjJwR2NLMGxwa0hwVnlZWjhMY29rRFhuUlAKUGQ5UlJSTVRTaGJsbFl2Mzd4QUhOV1ZIQ0ZsWHkrQklVU001bUlBU1NDQTV0azlJNmpZZ2F4bEFDQm1BY0lGMwozKzBjeGZIYVkrVW9YdVluMkZ6YUt2Ym5GdFZIZ0lkMDg4a3d4clZTZzlCT3BDRVZIR1pxR2I5TWN5MXVHVXhUClFTVENCbmpoTWZlZ0p6cXVPYWVOY0ZPSE5tbmtWRWpLTythbTBPeEhNS1lyS3ZnQnVEbzdoVnFENlBFMUd6V3AKZHdwZUV4QXZDSVJxL2pWTkdRK3FtMkRWOVNJZ3U5bmF4MktmSUtFeU50dUFFS1VpekdqL0VmRFhDM1cxMExhegpKaGNYNGw1SUFZU1o3L3JWVmpGbExWSVl0WDU1T054L1Z3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K";
appKey = "z4nXOeTeSnnpUpnmsV1";
}
export const AppKey = appKey;
export type LicenseVerifyReq = {
subjectId: string;
license: string;
};
type License = {
appKey: string;
code: string;
subjectId: string;
expireTime: number;
activeTime: number;
duration: number;
version: number;
secret: string;
vipType: string;
signature: string;
};
class LicenseHolder {
isPlus = false;
expireTime = 0;
vipType = "";
message?: string = undefined;
secret?: string = undefined;
}
const holder = new LicenseHolder();
holder.isPlus = false;
class LicenseVerifier {
checked = false;
licenseReq?: LicenseVerifyReq = undefined;
async reVerify(req: LicenseVerifyReq) {
this.checked = false;
return await this.verify(req);
}
setPlus(value: boolean, info: any = {}) {
if (value && info) {
holder.isPlus = true;
holder.expireTime = info.expireTime;
holder.secret = info.secret;
holder.vipType = info.vipType;
} else {
holder.isPlus = false;
holder.expireTime = 0;
holder.vipType = "";
holder.message = info.message;
holder.secret = undefined;
}
return {
...holder,
};
}
async verify(req: LicenseVerifyReq) {
this.licenseReq = req;
if (this.checked) {
return this.setPlus(false);
}
const license = req?.license;
if (!license) {
this.checked = true;
return this.setPlus(false);
}
const licenseJson = Buffer.from(license, "base64").toString();
const json: License = JSON.parse(licenseJson);
if (json.expireTime < Date.now()) {
logger.warn("授权已过期");
return this.setPlus(false, { message: "授权已过期" });
}
const content = `${appKey},${this.licenseReq.subjectId},${json.code},${json.secret},${json.vipType},${json.activeTime},${json.duration},${json.expireTime},${json.version}`;
// content := fmt.Sprintf("%s,%s,%s,%s,%d,%d,%d,%d,%d", entity.AppKey, entity.SubjectId, entity.Code, entity.Secret, entity.Level, entity.ActiveTime, entity.Duration, entity.ExpireTime, entity.Version)
//z4nXOeTeSnnpUpnmsV,_m9jFTdNHktdaEN4xBDw_,HZz7rAAR3h3zGlDMhScO1wGBYPjXpZ9S_1,uUpr9I8p6K3jWSzu2Wh5NECvgG2FNynU,0,1724199847470,365,1787271324416,1
logger.debug("content:", content);
const publicKey = Buffer.from(SecreteKey, "base64").toString();
const res = this.verifySignature(content, json.signature, publicKey);
this.checked = true;
if (!res) {
logger.warn("授权校验失败");
return this.setPlus(false, { message: "授权校验失败" });
}
logger.info(`授权校验成功,到期时间:${dayjs(json.expireTime).format("YYYY-MM-DD HH:mm:ss")}`);
return this.setPlus(true, {
expireTime: json.expireTime,
vipType: json.vipType || "plus",
secret: json.secret,
});
}
verifySignature(content: string, signature: any, publicKey: string) {
const verify = createVerify("RSA-SHA256");
verify.update(content);
return verify.verify(publicKey, signature, "base64");
}
}
const verifier = new LicenseVerifier();
export function isPlus() {
return holder.isPlus && holder.expireTime > Date.now();
}
export function isCommercial() {
return holder.isPlus && holder.vipType === "comm" && holder.expireTime > Date.now();
}
export function getPlusInfo() {
return {
isPlus: holder.isPlus,
vipType: holder.vipType,
expireTime: holder.expireTime,
secret: holder.secret,
};
}
export async function verify(req: LicenseVerifyReq) {
try {
return await verifier.reVerify(req);
} catch (e) {
logger.error(e);
return verifier.setPlus(false, { message: "授权校验失败" });
}
}
import { setLogger } from "@certd/plus";
setLogger(logger);
export * from "@certd/plus";

View File

@@ -104,12 +104,13 @@ export class RunHistory {
log(runnable: Runnable, text: string) {
// @ts-ignore
this._loggers[runnable.id].info(`[${runnable.title}]<id:${runnable.id}> [${runnable.runnableType}]`, text);
this._loggers[runnable.id].info(`[${runnable.runnableType}] [${runnable.title}]<id:${runnable.id}> `, text);
}
logError(runnable: Runnable, e: Error) {
// @ts-ignore
this._loggers[runnable.id].error(`[${runnable.title}]<id:${runnable.id}> [${runnable.runnableType}]`, e);
const errorInfo = runnable.runnableType == "step" ? e.stack : e.message;
this._loggers[runnable.id].error(`[${runnable.runnableType}] [${runnable.title}]<id:${runnable.id}> ${errorInfo}`);
}
finally(runnable: Runnable) {
@@ -166,6 +167,14 @@ export class RunnableCollection {
});
}
clearById(id: string) {
const runnable = this.collection[id];
if (runnable?.status) {
runnable.status.status = ResultType.none;
runnable.status.result = ResultType.none;
}
}
add(runnable: Runnable) {
this.collection[runnable.id] = runnable;
}

View File

@@ -40,6 +40,8 @@ export type PluginDefine = Registrable & {
dest: string;
type: "computed";
}[];
needPlus?: boolean;
};
export type ITaskPlugin = {

View File

@@ -1,8 +1,31 @@
import axios from "axios";
// @ts-ignore
import qs from "qs";
import { logger } from "./util.log.js";
import { Logger } from "log4js";
export class HttpError extends Error {
request?: { url: string; method: string; data?: any };
response?: { data: any };
status?: number;
statusText?: string;
constructor(error: any) {
if (!error) {
return;
}
super(error.message);
this.name = error.name;
this.stack = error.stack;
this.status = error?.response?.status;
this.statusText = error?.response?.statusText;
this.request = {
url: error?.response?.config?.url,
method: error?.response?.config?.method,
data: error?.response?.config?.data,
};
this.response = {
data: error?.response?.data,
};
}
}
/**
* @description 创建请求实例
*/
@@ -12,13 +35,6 @@ export function createAxiosService({ logger }: { logger: Logger }) {
// 请求拦截
service.interceptors.request.use(
(config: any) => {
if (config.formData) {
config.data = qs.stringify(config.formData, {
arrayFormat: "indices",
allowDots: true,
}); // 序列化请求参数
delete config.formData;
}
logger.info(`http request:${config.url}method:${config.method}`);
return config;
},
@@ -50,26 +66,12 @@ export function createAxiosService({ logger }: { logger: Logger }) {
// case 505: error.message = 'HTTP版本不受支持'; break
// default: break
// }
logger.error(`请求出错url:${error?.response?.config.url},method:${error?.response?.config?.method},status:${error?.response?.status}`);
logger.info("返回数据:", JSON.stringify(error?.response?.data));
delete error.config;
const data = error?.response?.data;
if (!data) {
error.message = data.message || data.msg || data.error || data;
}
if (error?.response) {
return Promise.reject({
status: error?.response?.status,
statusText: error?.response?.statusText,
request: {
url: error?.response?.config?.url,
method: error?.response?.config?.method,
data: error?.response?.data,
},
data: error?.response?.data,
});
}
return Promise.reject(error);
logger.error(
`请求出错status:${error?.response?.status},statusText:${error?.response?.statusText},url:${error?.config?.url},method:${error?.config?.method}`
);
logger.error("返回数据:", JSON.stringify(error?.response?.data));
const err = new HttpError(error);
return Promise.reject(err);
}
);
return service;

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.24.1](https://github.com/certd/certd/compare/v1.24.0...v1.24.1) (2024-09-02)
**Note:** Version bump only for package @certd/lib-huawei
## [1.22.1](https://github.com/certd/certd/compare/v1.22.0...v1.22.1) (2024-07-20)
**Note:** Version bump only for package @certd/lib-huawei

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/lib-huawei",
"private": false,
"version": "1.22.1",
"version": "1.24.1",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
"types": "./dist/d/index.d.ts",

View File

@@ -26,7 +26,7 @@ module.exports = {
rootDir: "src",
declaration: true,
declarationDir: "dist/d",
exclude: ["./node_modules/**", "./src/**/*.vue"],
exclude: ["./node_modules/**", "./src/**/*.vue", "./src/**/*.spec.ts"],
allowSyntheticDefaultImports: true,
}),
json(),

View File

@@ -1,5 +1,5 @@
{
"compileOnSave": true,
"compileOnSave": false,
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.24.1](https://github.com/certd/certd/compare/v1.24.0...v1.24.1) (2024-09-02)
**Note:** Version bump only for package @certd/lib-k8s
# [1.24.0](https://github.com/certd/certd/compare/v1.23.1...v1.24.0) (2024-08-25)
### Performance Improvements

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/lib-k8s",
"private": false,
"version": "1.24.0",
"version": "1.24.1",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -16,7 +16,7 @@
"@kubernetes/client-node": "0.21.0"
},
"devDependencies": {
"@certd/pipeline": "^1.24.0",
"@certd/pipeline": "^1.24.1",
"@rollup/plugin-commonjs": "^23.0.4",
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.0.1",

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.24.1](https://github.com/certd/certd/compare/v1.24.0...v1.24.1) (2024-09-02)
### Performance Improvements
* 授权配置支持加密 ([42a56b5](https://github.com/certd/certd/commit/42a56b581d754c3e5f9838179d19ab0d004ef2eb))
# [1.24.0](https://github.com/certd/certd/compare/v1.23.1...v1.24.0) (2024-08-25)
### Bug Fixes

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-cert",
"private": false,
"version": "1.24.0",
"version": "1.24.1",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -13,8 +13,8 @@
"preview": "vite preview"
},
"dependencies": {
"@certd/acme-client": "^1.24.0",
"@certd/pipeline": "^1.24.0",
"@certd/acme-client": "^1.24.1",
"@certd/pipeline": "^1.24.1",
"jszip": "^3.10.1",
"node-forge": "^0.10.0",
"psl": "^1.9.0"

View File

@@ -13,6 +13,7 @@ export class EabAccess {
},
helper: "EAB KID",
required: true,
encrypt: true,
})
kid = "";
@AccessInput({
@@ -22,6 +23,7 @@ export class EabAccess {
},
helper: "EAB HMAC Key",
required: true,
encrypt: true,
})
hmacKey = "";
}

View File

@@ -7,7 +7,7 @@ import { IContext } from "@certd/pipeline";
import { IDnsProvider } from "../../dns-provider/index.js";
import psl from "psl";
import { ClientExternalAccountBindingOptions, UrlMapping } from "@certd/acme-client";
import { utils } from "@certd/pipeline";
export type CertInfo = {
crt: string;
key: string;
@@ -90,6 +90,13 @@ export class AcmeService {
}
if (this.options.useMappingProxy) {
urlMapping.enabled = true;
} else {
//测试directory是否可以访问
const isOk = await this.testDirectory(directoryUrl);
if (!isOk) {
this.logger.info("测试访问失败,自动使用代理");
urlMapping.enabled = true;
}
}
const client = new acme.Client({
directoryUrl: directoryUrl,
@@ -295,4 +302,19 @@ export class AcmeService {
altNames,
};
}
private async testDirectory(directoryUrl: string) {
try {
await utils.http({
url: directoryUrl,
method: "GET",
timeout: 5000,
});
} catch (e) {
this.logger.error(`${directoryUrl},测试访问失败`, e);
return false;
}
this.logger.info(`${directoryUrl},测试访问成功`);
return true;
}
}

View File

@@ -80,7 +80,8 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
name: "pi-dns-provider-selector",
},
required: true,
helper: "请选择dns解析提供商",
helper:
"请选择dns解析提供商您的域名是在哪里注册的或者域名的dns解析服务器属于哪个平台\n如果这里没有您的dns解析提供商您可以将域名解析服务器设置成上面的任意一个提供商",
})
dnsProviderType!: string;
@@ -108,7 +109,6 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
name: "a-switch",
vModel: "checked",
},
maybeNeed: true,
helper: "如果acme-v02.api.letsencrypt.org或dv.acme-v02.api.pki.goog被墙无法访问请尝试开启此选项",
})
useProxy = false;

View File

@@ -1,463 +0,0 @@
/** @type {import('dependency-cruiser').IConfiguration} */
module.exports = {
forbidden: [
/* rules from the 'recommended' preset: */
{
name: 'no-circular',
severity: 'warn',
comment:
'This dependency is part of a circular relationship. You might want to revise ' +
'your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ',
from: {},
to: {
circular: true
}
},
{
name: 'no-orphans',
comment:
"This is an orphan module - it's likely not used (anymore?). Either use it or " +
"remove it. If it's logical this module is an orphan (i.e. it's a config file), " +
"add an exception for it in your dependency-cruiser configuration. By default " +
"this rule does not scrutinize dot-files (e.g. .eslintrc.js), TypeScript declaration " +
"files (.d.ts), tsconfig.json and some of the babel and webpack configs.",
severity: 'warn',
from: {
orphan: true,
pathNot: [
'(^|/)\\.[^/]+\\.(js|cjs|mjs|ts|json)$', // dot files
'\\.d\\.ts$', // TypeScript declaration files
'(^|/)tsconfig\\.json$', // TypeScript config
'(^|/)(babel|webpack)\\.config\\.(js|cjs|mjs|ts|json)$' // other configs
]
},
to: {},
},
{
name: 'no-deprecated-core',
comment:
'A module depends on a node core module that has been deprecated. Find an alternative - these are ' +
"bound to exist - node doesn't deprecate lightly.",
severity: 'warn',
from: {},
to: {
dependencyTypes: [
'core'
],
path: [
'^(v8\/tools\/codemap)$',
'^(v8\/tools\/consarray)$',
'^(v8\/tools\/csvparser)$',
'^(v8\/tools\/logreader)$',
'^(v8\/tools\/profile_view)$',
'^(v8\/tools\/profile)$',
'^(v8\/tools\/SourceMap)$',
'^(v8\/tools\/splaytree)$',
'^(v8\/tools\/tickprocessor-driver)$',
'^(v8\/tools\/tickprocessor)$',
'^(node-inspect\/lib\/_inspect)$',
'^(node-inspect\/lib\/internal\/inspect_client)$',
'^(node-inspect\/lib\/internal\/inspect_repl)$',
'^(async_hooks)$',
'^(punycode)$',
'^(domain)$',
'^(constants)$',
'^(sys)$',
'^(_linklist)$',
'^(_stream_wrap)$'
],
}
},
{
name: 'not-to-deprecated',
comment:
'This module uses a (version of an) npm module that has been deprecated. Either upgrade to a later ' +
'version of that module, or find an alternative. Deprecated modules are a security risk.',
severity: 'warn',
from: {},
to: {
dependencyTypes: [
'deprecated'
]
}
},
{
name: 'no-non-package-json',
severity: 'error',
comment:
"This module depends on an npm package that isn't in the 'dependencies' section of your package.json. " +
"That's problematic as the package either (1) won't be available on live (2 - worse) will be " +
"available on live with an non-guaranteed version. Fix it by adding the package to the dependencies " +
"in your package.json.",
from: {},
to: {
dependencyTypes: [
'npm-no-pkg',
'npm-unknown'
]
}
},
{
name: 'not-to-unresolvable',
comment:
"This module depends on a module that cannot be found ('resolved to disk'). If it's an npm " +
'module: add it to your package.json. In all other cases you likely already know what to do.',
severity: 'error',
from: {},
to: {
couldNotResolve: true
}
},
{
name: 'no-duplicate-dep-types',
comment:
"Likely this module depends on an external ('npm') package that occurs more than once " +
"in your package.json i.e. bot as a devDependencies and in dependencies. This will cause " +
"maintenance problems later on.",
severity: 'warn',
from: {},
to: {
moreThanOneDependencyType: true,
// as it's pretty common to have a type import be a type only import
// _and_ (e.g.) a devDependency - don't consider type-only dependency
// types for this rule
dependencyTypesNot: ["type-only"]
}
},
/* rules you might want to tweak for your specific situation: */
{
name: 'not-to-test',
comment:
"This module depends on code within a folder that should only contain tests. As tests don't " +
"implement functionality this is odd. Either you're writing a test outside the test folder " +
"or there's something in the test folder that isn't a test.",
severity: 'error',
from: {
pathNot: '^(tests)'
},
to: {
path: '^(tests)'
}
},
{
name: 'not-to-spec',
comment:
'This module depends on a spec (test) file. The sole responsibility of a spec file is to test code. ' +
"If there's something in a spec that's of use to other modules, it doesn't have that single " +
'responsibility anymore. Factor it out into (e.g.) a separate utility/ helper or a mock.',
severity: 'error',
from: {},
to: {
path: '\\.(spec|test)\\.(js|mjs|cjs|ts|ls|coffee|litcoffee|coffee\\.md)$'
}
},
{
name: 'not-to-dev-dep',
severity: 'error',
comment:
"This module depends on an npm package from the 'devDependencies' section of your " +
'package.json. It looks like something that ships to production, though. To prevent problems ' +
"with npm packages that aren't there on production declare it (only!) in the 'dependencies'" +
'section of your package.json. If this module is development only - add it to the ' +
'from.pathNot re of the not-to-dev-dep rule in the dependency-cruiser configuration',
from: {
path: '^(src)',
pathNot: '\\.(spec|test)\\.(js|mjs|cjs|ts|ls|coffee|litcoffee|coffee\\.md)$'
},
to: {
dependencyTypes: [
'npm-dev'
]
}
},
{
name: 'optional-deps-used',
severity: 'info',
comment:
"This module depends on an npm package that is declared as an optional dependency " +
"in your package.json. As this makes sense in limited situations only, it's flagged here. " +
"If you're using an optional dependency here by design - add an exception to your" +
"dependency-cruiser configuration.",
from: {},
to: {
dependencyTypes: [
'npm-optional'
]
}
},
{
name: 'peer-deps-used',
comment:
"This module depends on an npm package that is declared as a peer dependency " +
"in your package.json. This makes sense if your package is e.g. a plugin, but in " +
"other cases - maybe not so much. If the use of a peer dependency is intentional " +
"add an exception to your dependency-cruiser configuration.",
severity: 'warn',
from: {},
to: {
dependencyTypes: [
'npm-peer'
]
}
}
],
options: {
/* conditions specifying which files not to follow further when encountered:
- path: a regular expression to match
- dependencyTypes: see https://github.com/sverweij/dependency-cruiser/blob/master/doc/rules-reference.md#dependencytypes-and-dependencytypesnot
for a complete list
*/
doNotFollow: {
path: 'node_modules'
},
/* conditions specifying which dependencies to exclude
- path: a regular expression to match
- dynamic: a boolean indicating whether to ignore dynamic (true) or static (false) dependencies.
leave out if you want to exclude neither (recommended!)
*/
// exclude : {
// path: '',
// dynamic: true
// },
/* pattern specifying which files to include (regular expression)
dependency-cruiser will skip everything not matching this pattern
*/
// includeOnly : '',
/* dependency-cruiser will include modules matching against the focus
regular expression in its output, as well as their neighbours (direct
dependencies and dependents)
*/
// focus : '',
/* list of module systems to cruise */
// moduleSystems: ['amd', 'cjs', 'es6', 'tsd'],
/* prefix for links in html and svg output (e.g. 'https://github.com/you/yourrepo/blob/develop/'
to open it on your online repo or `vscode://file/${process.cwd()}/` to
open it in visual studio code),
*/
// prefix: '',
/* false (the default): ignore dependencies that only exist before typescript-to-javascript compilation
true: also detect dependencies that only exist before typescript-to-javascript compilation
"specify": for each dependency identify whether it only exists before compilation or also after
*/
tsPreCompilationDeps: true,
/*
list of extensions to scan that aren't javascript or compile-to-javascript.
Empty by default. Only put extensions in here that you want to take into
account that are _not_ parsable.
*/
// extraExtensionsToScan: [".json", ".jpg", ".png", ".svg", ".webp"],
/* if true combines the package.jsons found from the module up to the base
folder the cruise is initiated from. Useful for how (some) mono-repos
manage dependencies & dependency definitions.
*/
// combinedDependencies: false,
/* if true leave symlinks untouched, otherwise use the realpath */
// preserveSymlinks: false,
/* TypeScript project file ('tsconfig.json') to use for
(1) compilation and
(2) resolution (e.g. with the paths property)
The (optional) fileName attribute specifies which file to take (relative to
dependency-cruiser's current working directory). When not provided
defaults to './tsconfig.json'.
*/
tsConfig: {
fileName: 'tsconfig.json'
},
/* Webpack configuration to use to get resolve options from.
The (optional) fileName attribute specifies which file to take (relative
to dependency-cruiser's current working directory. When not provided defaults
to './webpack.conf.js'.
The (optional) `env` and `args` attributes contain the parameters to be passed if
your webpack config is a function and takes them (see webpack documentation
for details)
*/
// webpackConfig: {
// fileName: './webpack.config.js',
// env: {},
// args: {},
// },
/* Babel config ('.babelrc', '.babelrc.json', '.babelrc.json5', ...) to use
for compilation (and whatever other naughty things babel plugins do to
source code). This feature is well tested and usable, but might change
behavior a bit over time (e.g. more precise results for used module
systems) without dependency-cruiser getting a major version bump.
*/
// babelConfig: {
// fileName: './.babelrc'
// },
/* List of strings you have in use in addition to cjs/ es6 requires
& imports to declare module dependencies. Use this e.g. if you've
re-declared require, use a require-wrapper or use window.require as
a hack.
*/
// exoticRequireStrings: [],
/* options to pass on to enhanced-resolve, the package dependency-cruiser
uses to resolve module references to disk. You can set most of these
options in a webpack.conf.js - this section is here for those
projects that don't have a separate webpack config file.
Note: settings in webpack.conf.js override the ones specified here.
*/
enhancedResolveOptions: {
/* List of strings to consider as 'exports' fields in package.json. Use
['exports'] when you use packages that use such a field and your environment
supports it (e.g. node ^12.19 || >=14.7 or recent versions of webpack).
If you have an `exportsFields` attribute in your webpack config, that one
will have precedence over the one specified here.
*/
exportsFields: ["exports"],
/* List of conditions to check for in the exports field. e.g. use ['imports']
if you're only interested in exposed es6 modules, ['require'] for commonjs,
or all conditions at once `(['import', 'require', 'node', 'default']`)
if anything goes for you. Only works when the 'exportsFields' array is
non-empty.
If you have a 'conditionNames' attribute in your webpack config, that one will
have precedence over the one specified here.
*/
conditionNames: ["import", "require", "node", "default"],
/*
The extensions, by default are the same as the ones dependency-cruiser
can access (run `npx depcruise --info` to see which ones that are in
_your_ environment. If that list is larger than what you need (e.g.
it contains .js, .jsx, .ts, .tsx, .cts, .mts - but you don't use
TypeScript you can pass just the extensions you actually use (e.g.
[".js", ".jsx"]). This can speed up the most expensive step in
dependency cruising (module resolution) quite a bit.
*/
// extensions: [".js", ".jsx", ".ts", ".tsx", ".d.ts"],
/*
If your TypeScript project makes use of types specified in 'types'
fields in package.jsons of external dependencies, specify "types"
in addition to "main" in here, so enhanced-resolve (the resolver
dependency-cruiser uses) knows to also look there. You can also do
this if you're not sure, but still use TypeScript. In a future version
of dependency-cruiser this will likely become the default.
*/
mainFields: ["main", "types"],
},
reporterOptions: {
dot: {
/* pattern of modules that can be consolidated in the detailed
graphical dependency graph. The default pattern in this configuration
collapses everything in node_modules to one folder deep so you see
the external modules, but not the innards your app depends upon.
*/
collapsePattern: 'node_modules/(@[^/]+/[^/]+|[^/]+)',
/* Options to tweak the appearance of your graph.See
https://github.com/sverweij/dependency-cruiser/blob/master/doc/options-reference.md#reporteroptions
for details and some examples. If you don't specify a theme
don't worry - dependency-cruiser will fall back to the default one.
*/
// theme: {
// graph: {
// /* use splines: "ortho" for straight lines. Be aware though
// graphviz might take a long time calculating ortho(gonal)
// routings.
// */
// splines: "true"
// },
// modules: [
// {
// criteria: { matchesFocus: true },
// attributes: {
// fillcolor: "lime",
// penwidth: 2,
// },
// },
// {
// criteria: { matchesFocus: false },
// attributes: {
// fillcolor: "lightgrey",
// },
// },
// {
// criteria: { matchesReaches: true },
// attributes: {
// fillcolor: "lime",
// penwidth: 2,
// },
// },
// {
// criteria: { matchesReaches: false },
// attributes: {
// fillcolor: "lightgrey",
// },
// },
// {
// criteria: { source: "^src/model" },
// attributes: { fillcolor: "#ccccff" }
// },
// {
// criteria: { source: "^src/view" },
// attributes: { fillcolor: "#ccffcc" }
// },
// ],
// dependencies: [
// {
// criteria: { "rules[0].severity": "error" },
// attributes: { fontcolor: "red", color: "red" }
// },
// {
// criteria: { "rules[0].severity": "warn" },
// attributes: { fontcolor: "orange", color: "orange" }
// },
// {
// criteria: { "rules[0].severity": "info" },
// attributes: { fontcolor: "blue", color: "blue" }
// },
// {
// criteria: { resolved: "^src/model" },
// attributes: { color: "#0000ff77" }
// },
// {
// criteria: { resolved: "^src/view" },
// attributes: { color: "#00770077" }
// }
// ]
// }
},
archi: {
/* pattern of modules that can be consolidated in the high level
graphical dependency graph. If you use the high level graphical
dependency graph reporter (`archi`) you probably want to tweak
this collapsePattern to your situation.
*/
collapsePattern: '^(packages|src|lib|app|bin|test(s?)|spec(s?))/[^/]+|node_modules/(@[^/]+/[^/]+|[^/]+)',
/* Options to tweak the appearance of your graph.See
https://github.com/sverweij/dependency-cruiser/blob/master/doc/options-reference.md#reporteroptions
for details and some examples. If you don't specify a theme
for 'archi' dependency-cruiser will use the one specified in the
dot section (see above), if any, and otherwise use the default one.
*/
// theme: {
// },
},
"text": {
"highlightFocused": true
},
}
}
};
// generated: dependency-cruiser@12.11.0 on 2023-03-24T14:11:38.647Z

View File

@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.24.1](https://github.com/certd/certd/compare/v1.24.0...v1.24.1) (2024-09-02)
### Bug Fixes
* 激活仅限管理员 ([1c17970](https://github.com/certd/certd/commit/1c17970b981f0987c506744ee6b2283fd5e40493))
### Performance Improvements
* 授权配置支持加密 ([42a56b5](https://github.com/certd/certd/commit/42a56b581d754c3e5f9838179d19ab0d004ef2eb))
* 支持阿里云 DCDN ([98b77f8](https://github.com/certd/certd/commit/98b77f80843834616fb26f83b4c42245326abd06))
* 支持已跳过的步骤重新运行 ([ea775ad](https://github.com/certd/certd/commit/ea775adae18d57a04470cfba6b9460d761d74035))
* 支持cdnfly ([724a850](https://github.com/certd/certd/commit/724a85028b4a7146c9e3b4df4497dcf2a7bf7c67))
* 支持ftp上传 ([b9bddbf](https://github.com/certd/certd/commit/b9bddbfabb5664365f1232e9432532187c98006c))
# [1.24.0](https://github.com/certd/certd/compare/v1.23.1...v1.24.0) (2024-08-25)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/ui-client",
"version": "1.24.0",
"version": "1.24.1",
"private": true,
"scripts": {
"dev": "vite --open",
@@ -23,13 +23,14 @@
"license": "AGPL-3.0",
"dependencies": {
"@ant-design/colors": "^7.0.2",
"@ant-design/icons-vue": "^7.0.1",
"@ant-design/icons-vue": "^6.1.0",
"@fast-crud/fast-crud": "^1.21.2",
"@fast-crud/fast-extends": "^1.21.2",
"@fast-crud/ui-antdv4": "^1.21.2",
"@fast-crud/ui-interface": "^1.21.2",
"@iconify/vue": "^4.1.1",
"@soerenmartius/vue3-clipboard": "^0.1.2",
"@vue-js-cron/light": "^4.0.5",
"ant-design-vue": "^4.1.2",
"axios": "^1.7.2",
"axios-mock-adapter": "^1.22.0",
@@ -57,7 +58,7 @@
"vuedraggable": "^2.24.3"
},
"devDependencies": {
"@certd/pipeline": "^1.24.0",
"@certd/pipeline": "^1.24.1",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12",
@@ -104,7 +105,7 @@
"tslint": "^6.1.3",
"typescript": "5.4.2",
"unplugin-vue-define-options": "^1.4.2",
"vite": "^5.1.6",
"vite": "^5.3.1",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-html": "^3.2.2",
"vite-plugin-windicss": "^1.9.3",

View File

@@ -0,0 +1,80 @@
<template>
<div class="cron-editor">
<div class="flex-o">
<cron-light
:disabled="disabled"
:readonly="readonly"
:period="period"
class="flex-o cron-ant"
locale="zh-CN"
format="quartz"
:model-value="modelValue"
@update:model-value="onUpdate"
@error="onError"
/>
</div>
<div class="mt-5">
<a-input :disabled="true" :readonly="readonly" :value="modelValue" @change="onChange"></a-input>
</div>
<div class="fs-helper">{{ errorMessage }}</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
const props = defineProps<{
modelValue?: string;
disabled?: boolean;
readonly?: boolean;
}>();
const period = ref<string>("day");
const emit = defineEmits<{
"update:modelValue": any;
}>();
const errorMessage = ref<string | null>(null);
const onUpdate = (value: string) => {
if (value === props.modelValue) {
return;
}
emit("update:modelValue", value);
errorMessage.value = undefined;
};
const onPeriod = (value: string) => {
period.value = value;
};
const onChange = (e: any) => {
const value = e.target.value;
onUpdate(value);
};
const onError = (error: any) => {
errorMessage.value = error;
};
</script>
<style lang="less">
.cron-editor {
.cron-ant {
flex-wrap: wrap;
&* > {
margin-bottom: 2px;
display: flex;
align-items: center;
}
.vcron-select-list {
min-width: 56px;
}
.vcron-select-input {
min-height: 22px;
}
.vcron-select-container {
display: flex;
align-items: center;
}
}
}
</style>

View File

@@ -5,6 +5,9 @@ import PiOutputSelector from "../views/certd/pipeline/pipeline/component/output-
import PiEditable from "./editable.vue";
import VipButton from "./vip-button/index.vue";
import { CheckCircleOutlined, InfoCircleOutlined, UndoOutlined } from "@ant-design/icons-vue";
import CronEditor from "./cron-editor/index.vue";
import { CronLight } from "@vue-js-cron/light";
import "@vue-js-cron/light/dist/light.css";
export default {
install(app: any) {
app.component("PiContainer", PiContainer);
@@ -13,6 +16,8 @@ export default {
app.component("PiOutputSelector", PiOutputSelector);
app.component("PiDnsProviderSelector", PiDnsProviderSelector);
app.component("VipButton", VipButton);
app.component("CronLight", CronLight);
app.component("CronEditor", CronEditor);
app.component("CheckCircleOutlined", CheckCircleOutlined);
app.component("InfoCircleOutlined", InfoCircleOutlined);

View File

@@ -1,20 +1,13 @@
<template>
<div class="layout-vip isPlus">
<div class="layout-vip isPlus" @click="openUpgrade">
<contextHolder />
<fs-icon icon="mingcute:vip-1-line"></fs-icon>
<div class="text">
<template v-if="userStore.isPlus">
<a-tooltip>
<template #title> 到期时间{{ expireTime }} </template>
<span @click="openUpgrade">{{ texts.plus }}</span>
</a-tooltip>
</template>
<template v-else>
<a-tooltip>
<template #title> 升级专业版享受更多VIP特权 </template>
<span @click="openUpgrade"> {{ texts.free }} {{ expiredDays }} </span>
</a-tooltip>
</template>
<fs-icon icon="mingcute:vip-1-line" :title="text.title" />
<div v-if="mode !== 'icon'" class="text">
<a-tooltip>
<template #title> {{ text.title }}</template>
<span>{{ text.name }}</span>
</a-tooltip>
</div>
</div>
</template>
@@ -26,24 +19,53 @@ import { message, Modal } from "ant-design-vue";
import * as api from "./api";
import { useSettingStore } from "/@/store/modules/settings";
const props = defineProps<{
mode?: "button" | "nav";
}>();
type Texts = {
plus: string;
free: string;
const props = withDefaults(
defineProps<{
mode?: "button" | "nav" | "icon";
}>(),
{
mode: "button"
}
);
type Text = {
name: string;
title?: string;
};
const texts = computed<Texts>(() => {
if (props.mode === "button") {
return {
plus: "专业版已开通",
free: "此为专业版功能"
};
const text = computed<Text>(() => {
const map = {
isPlus: {
button: {
name: "专业版已开通",
title: "到期时间:" + expireTime.value
},
icon: {
name: "",
title: "专业版已开通"
},
nav: {
name: "专业版",
title: "到期时间:" + expireTime.value
}
},
free: {
button: {
name: "此为专业版功能",
title: "升级专业版享受更多VIP特权"
},
icon: {
name: "",
title: "此为专业版功能"
},
nav: {
name: "免费版",
title: "升级专业版享受更多VIP特权"
}
}
};
if (userStore.isPlus) {
return map.isPlus[props.mode];
} else {
return {
plus: "专业版",
free: "免费版"
};
return map.free[props.mode];
}
});
@@ -86,10 +108,16 @@ async function doActive() {
const settingStore = useSettingStore();
const computedSiteId = computed(() => settingStore.installInfo?.siteId);
const [modal, contextHolder] = Modal.useModal();
function openUpgrade() {
if (!userStore.isAdmin) {
message.info("仅限管理员操作");
return;
}
const placeholder = "请输入激活码";
const isPlus = userStore.isPlus;
modal.confirm({
title: "升级/续期专业版",
title: isPlus ? "续期专业版" : "激活专业版",
async onOk() {
return await doActive();
},
@@ -107,7 +135,8 @@ function openUpgrade() {
</ul>
</div>
<div>
<h3 class="block-header">立刻激活/续期</h3>
<h3 class="block-header">{isPlus ? "续期" : "立刻激活"}</h3>
<div>{isPlus ? "当前专业版已激活,到期时间" + dayjs(userStore.plusInfo.expireTime).format("YYYY-MM-DD") : ""}</div>
<div class="mt-10">
<div class="flex-o w-100">
<span>站点ID</span>
@@ -137,6 +166,7 @@ function openUpgrade() {
justify-content: center;
height: 100%;
cursor: pointer;
&.isPlus {
color: #c5913f;
}

View File

@@ -18,7 +18,7 @@
<MenuFoldOutlined v-else />
</div>
<fs-menu class="header-menu" mode="horizontal" :expand-selected="false" :selectable="false" :menus="frameworkMenus" />
<vip-button class="flex-center header-btn"></vip-button>
<vip-button class="flex-center header-btn" mode="nav" />
</div>
<div class="header-right header-buttons">
<!-- <button-->

View File

@@ -45,6 +45,10 @@ h1, h2, h3, h4, h5, h6 {
vertical-align: 0 !important;
}
.pointer{
cursor: pointer;
}
.flex-center{
display: flex;
justify-content: center;
@@ -64,7 +68,9 @@ h1, h2, h3, h4, h5, h6 {
flex: 1;
}
.mb-2{
margin-bottom:2px;
}
.ml-5{
margin-left:5px;
}
@@ -84,6 +90,9 @@ h1, h2, h3, h4, h5, h6 {
.mr-15{
margin-right: 15px;
}
.mt-5{
margin-top:5px;
}
.mt-10{
margin-top:10px;
}
@@ -117,3 +126,8 @@ h1, h2, h3, h4, h5, h6 {
padding-bottom:3px;
border-bottom: 1px solid #dedede;
}
.color-blue{
color: #1890ff;
}

View File

@@ -16,7 +16,7 @@ export function getCommonColumnDefine(crudExpose: any, typeRef: any) {
}
};
function buildDefineFields(define: any) {
function buildDefineFields(define: any, form: any) {
const formWrapperRef = crudExpose.getFormWrapperRef();
const columnsRef = toRef(formWrapperRef.formOptions, "columns");
@@ -32,7 +32,12 @@ export function getCommonColumnDefine(crudExpose: any, typeRef: any) {
...value,
key
};
columnsRef.value[key] = _.merge({ title: key }, defaultPluginConfig, field);
const column = _.merge({ title: key }, defaultPluginConfig, field);
if (column.value != null && _.get(form, key) == null) {
//设置默认值
_.set(form, key, column.value);
}
columnsRef.value[key] = column;
console.log("form", columnsRef.value);
});
}
@@ -55,13 +60,16 @@ export function getCommonColumnDefine(crudExpose: any, typeRef: any) {
rules: [{ required: true, message: "请选择类型" }],
valueChange: {
immediate: true,
async handle({ value, mode, form }) {
async handle({ value, mode, form, immediate }) {
if (value == null) {
return;
}
const define = await api.GetProviderDefine(value);
console.log("define", define);
buildDefineFields(define);
if (!immediate) {
form.access = {};
}
buildDefineFields(define, form);
}
}
},

View File

@@ -59,11 +59,11 @@ export function Save(pipelineEntity: any) {
});
}
export function Trigger(id: any) {
export function Trigger(id: any, stepId?: string) {
return request({
url: apiPrefix + "/trigger",
method: "post",
params: { id }
params: { id, stepId }
});
}

View File

@@ -39,7 +39,8 @@ export default function (certPluginGroup: PluginGroup, formWrapperRef: any): Cre
form: {
wrapper: {
width: "1150px",
saveRemind: false
saveRemind: false,
title: "创建证书申请流水线"
}
},
columns: {
@@ -73,6 +74,8 @@ export default function (certPluginGroup: PluginGroup, formWrapperRef: any): Cre
type: "text",
form: {
component: {
name: "cron-editor",
vModel: "modelValue",
placeholder: "0 0 4 * * *"
},
helper: "请输入cron表达式, 例如0 0 4 * * *每天凌晨4点触发",

View File

@@ -2,7 +2,7 @@ import * as api from "./api";
import { useI18n } from "vue-i18n";
import { computed, ref } from "vue";
import { useRouter } from "vue-router";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes, useUi } from "@fast-crud/fast-crud";
import { statusUtil } from "/@/views/certd/pipeline/pipeline/utils/util.status";
import { nanoid } from "nanoid";
import { message, Modal } from "ant-design-vue";
@@ -29,9 +29,16 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
};
const addRequest = async ({ form }: AddReq) => {
form.content = JSON.stringify({
title: form.title
});
if (form.content == null) {
form.content = JSON.stringify({
title: form.title
});
} else {
const content = JSON.parse(form.content);
content.title = form.title;
form.content = JSON.stringify(content);
}
const res = await api.AddObj(form);
lastResRef.value = res;
return res;
@@ -112,7 +119,7 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
},
addCertd: {
order: 1,
text: "添加证书流水线",
text: "创建证书流水线",
type: "primary",
click() {
addCertdPipeline();
@@ -136,6 +143,18 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
router.push({ path: "/certd/pipeline/detail", query: { id: row.id, editMode: "false" } });
}
},
copy: {
click: async (context) => {
const { ui } = useUi();
// @ts-ignore
const row = context[ui.tableColumn.row];
row.title = row.title + "_copy";
await crudExpose.openCopy({
row: row,
index: context.index
});
}
},
config: {
order: 1,
title: null,
@@ -158,7 +177,7 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
const files = await api.GetFiles(row.id);
Modal.success({
title: "文件下载",
okText: "↑↑↑ 点击链接下载",
okText: "↑↑↑ 点击上面链接下载",
content: () => {
const children = [];
for (const file of files) {

View File

@@ -54,9 +54,9 @@ export default defineComponent({
content: JSON.stringify(pipelineConfig)
});
},
async doTrigger(options: { pipelineId: number }) {
const { pipelineId } = options;
await api.Trigger(pipelineId);
async doTrigger(options: { pipelineId: number; stepId?: string }) {
const { pipelineId, stepId } = options;
await api.Trigger(pipelineId, stepId);
}
};

View File

@@ -5,7 +5,7 @@
</template>
<p>
<fs-date-format :model-value="runnable.status?.startTime"></fs-date-format>
<a-tag class="ml-1" :color="status.color" :closable="status.value === 'start'" @close="cancelTask">
<a-tag class="ml-5" :color="status.color" :closable="status.value === 'start'" @close="cancelTask">
{{ status.label }}
</a-tag>
<a-tag v-if="isCurrent" class="pointer" color="green" :closable="true" @close="cancel">当前</a-tag>

View File

@@ -1,5 +1,5 @@
<template>
<span v-if="statusRef" class="pi-status-show">
<span v-if="statusRef" class="pi-status-show flex-o">
<template v-if="type === 'icon'">
<fs-icon class="status-icon" v-bind="statusRef" :style="{ color: statusRef.color }" />
</template>

View File

@@ -25,6 +25,7 @@
<template #title>
<a-avatar :src="item.icon || '/images/plugin.png'" />
<span class="title">{{ item.title }}</span>
<vip-button v-if="item.needPlus" mode="icon" />
</template>
<template #description>
<span :title="item.desc">{{ item.desc }}</span>
@@ -81,6 +82,7 @@ import _ from "lodash-es";
import { nanoid } from "nanoid";
import { CopyOutlined } from "@ant-design/icons-vue";
import { PluginGroups } from "/@/views/certd/pipeline/pipeline/type";
import { useUserStore } from "/@/store/modules/user";
export default {
name: "PiStepForm",
@@ -98,6 +100,7 @@ export default {
* @returns
*/
function useStepForm() {
const useStore = useUserStore();
const getPluginGroups: any = inject("getPluginGroups");
const pluginGroups: PluginGroups = getPluginGroups();
const mode: Ref = ref("add");
@@ -117,6 +120,10 @@ export default {
});
const stepTypeSelected = (item: any) => {
if (item.needPlus && !useStore.isPlus) {
message.warn("此插件需要开通专业版才能使用");
throw new Error("此插件需要开通专业版才能使用");
}
currentStep.value.type = item.name;
currentStep.value.title = item.title;
console.log("currentStepTypeChanged:", currentStep.value);
@@ -128,6 +135,7 @@ export default {
message.warn("请先选择类型");
return;
}
// 给step的input设置默认值
changeCurrentPlugin(currentStep.value);
@@ -347,7 +355,7 @@ export default {
overflow-y: hidden;
.ant-card-meta-description {
font-size: 10px;
font-size: 12px;
line-height: 20px;
height: 40px;
color: #7f7f7f;

View File

@@ -15,13 +15,7 @@
</template>
<template v-if="currentTrigger">
<pi-container>
<a-form
ref="triggerFormRef"
class="trigger-form"
:model="currentTrigger"
:label-col="labelCol"
:wrapper-col="wrapperCol"
>
<a-form ref="triggerFormRef" class="trigger-form" :model="currentTrigger" :label-col="labelCol" :wrapper-col="wrapperCol">
<fs-form-item
v-model="currentTrigger.title"
:item="{
@@ -59,8 +53,8 @@
key: 'props.cron',
component: {
disabled: !editMode,
name: 'a-input',
vModel: 'value'
name: 'cron-editor',
vModel: 'modelValue'
},
helper: 'cron表达式例如 0 0 3 * * * 表示每天凌晨3点触发',
rules: [{ required: true, message: '此项必填' }]

View File

@@ -82,10 +82,20 @@
</div>
<div class="task">
<a-button shape="round" @click="taskEdit(stage, index, task, taskIndex)">
<span class="flex-o w-100">
<span class="ellipsis flex-1" :class="{ 'mr-15': editMode }">{{ task.title }}</span>
<pi-status-show :status="task.status?.result"></pi-status-show>
</span>
<a-popover title="步骤">
<!-- :open="true"-->
<template #content>
<div v-for="(item, index) of task.steps" class="flex-o w-100">
<span class="ellipsis flex-1">{{ index + 1 }}. {{ item.title }} </span>
<pi-status-show :status="item.status?.result"></pi-status-show>
<fs-icon class="pointer color-blue" title="重新运行此步骤" icon="SyncOutlined" @click="run(item.id)"></fs-icon>
</div>
</template>
<span class="flex-o w-100">
<span class="ellipsis flex-1" :class="{ 'mr-15': editMode }">{{ task.title }}</span>
<pi-status-show :status="task.status?.result"></pi-status-show>
</span>
</a-popover>
</a-button>
<fs-icon v-if="editMode" class="copy" title="复制" icon="ion:copy-outline" @click="taskCopy(stage, index, task)"></fs-icon>
</div>
@@ -226,10 +236,11 @@ import { nanoid } from "nanoid";
import { PipelineDetail, PipelineOptions, PluginGroups, RunHistory } from "./type";
import type { Runnable } from "@certd/pipeline";
import PiHistoryTimelineItem from "/@/views/certd/pipeline/pipeline/component/history-timeline-item.vue";
import { FsIcon } from "@fast-crud/fast-crud";
export default defineComponent({
name: "PipelineEdit",
// eslint-disable-next-line vue/no-unused-components
components: { PiHistoryTimelineItem, PiTaskForm, PiTriggerForm, PiTaskView, PiStatusShow, PiNotificationForm },
components: { FsIcon, PiHistoryTimelineItem, PiTaskForm, PiTriggerForm, PiTaskView, PiStatusShow, PiNotificationForm },
props: {
pipelineId: {
type: [Number, String],
@@ -529,7 +540,7 @@ export default defineComponent({
function useActions() {
const saveLoading = ref();
const run = async () => {
const run = async (stepId?: string) => {
if (props.editMode) {
message.warn("请先保存,再运行管道");
return;
@@ -549,11 +560,12 @@ export default defineComponent({
//@ts-ignore
await changeCurrentHistory(null);
watchNewHistoryList();
await props.options.doTrigger({ pipelineId: pipeline.value.id });
await props.options.doTrigger({ pipelineId: pipeline.value.id, stepId: stepId });
notification.success({ message: "管道已经开始运行" });
}
});
};
function toggleEditMode(editMode: boolean) {
ctx.emit("update:editMode", editMode);
}

View File

@@ -117,7 +117,7 @@ export class PluginGroups {
}
export type PipelineOptions = {
doTrigger(options: { pipelineId: number }): Promise<void>;
doTrigger(options: { pipelineId: number; stepId?: string }): Promise<void>;
doSave(pipelineConfig: Pipeline): Promise<void>;
getPipelineDetail(query: { pipelineId: number }): Promise<PipelineDetail>;
getHistoryList(query: { pipelineId: number }): Promise<RunHistory[]>;

View File

@@ -16,4 +16,4 @@ run/
/data/db.sqlite
*/node_modules
certd-server/tools/windows/
.clinic

View File

@@ -1,6 +1,9 @@
koa:
port: 7001
#plus:
# server:
# baseUrl: 'http://127.0.0.1:11007'
plus:
server:
baseUrl: 'http://127.0.0.1:11007'
baseUrl: 'https://api.ai.handsfree.work'

View File

@@ -16,3 +16,4 @@ run/
/test/setup.js
/test/setup.ts
/data/
.clinic

View File

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.24.1](https://github.com/certd/certd/compare/v1.24.0...v1.24.1) (2024-09-02)
### Performance Improvements
* 部署插件支持宝塔、易盾云等 ([ee61709](https://github.com/certd/certd/commit/ee617095efa1171548cf52fd45f0f98a368555a3))
* 授权配置支持加密 ([42a56b5](https://github.com/certd/certd/commit/42a56b581d754c3e5f9838179d19ab0d004ef2eb))
* 优化内存占用 ([db61033](https://github.com/certd/certd/commit/db6103363364440b650bc10bb334834e4a9470c7))
* 支持阿里云 DCDN ([98b77f8](https://github.com/certd/certd/commit/98b77f80843834616fb26f83b4c42245326abd06))
* 支持已跳过的步骤重新运行 ([ea775ad](https://github.com/certd/certd/commit/ea775adae18d57a04470cfba6b9460d761d74035))
* 支持cdnfly ([724a850](https://github.com/certd/certd/commit/724a85028b4a7146c9e3b4df4497dcf2a7bf7c67))
* 支持ftp上传 ([b9bddbf](https://github.com/certd/certd/commit/b9bddbfabb5664365f1232e9432532187c98006c))
# [1.24.0](https://github.com/certd/certd/compare/v1.23.1...v1.24.0) (2024-08-25)
### Bug Fixes

View File

@@ -0,0 +1 @@
alter table cd_access add "encrypt_setting" text;

View File

@@ -0,0 +1,2 @@
alter table cd_access add COLUMN "encrypt_setting" text;

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/ui-server",
"version": "1.24.0",
"version": "1.24.1",
"description": "fast-server base midway",
"private": true,
"type": "module",
@@ -15,19 +15,20 @@
"ci": "npm run cov",
"build": "mwtsc --cleanOutDir --skipLibCheck",
"build-on-docker": "node ./before-build.js && npm run build",
"up-mw-deps": "npx midway-version -u -w"
"up-mw-deps": "npx midway-version -u -w",
"clinic": "clinic heapprofiler -- node ./bootstrap.js"
},
"dependencies": {
"@alicloud/cs20151215": "^3.0.3",
"@alicloud/openapi-client": "^0.4.0",
"@alicloud/pop-core": "^1.7.10",
"@certd/acme-client": "^1.24.0",
"@certd/lib-huawei": "^1.22.1",
"@certd/lib-k8s": "^1.24.0",
"@certd/acme-client": "^1.24.1",
"@certd/lib-huawei": "^1.24.1",
"@certd/lib-k8s": "^1.24.1",
"@certd/midway-flyway-js": "^1.22.6",
"@certd/pipeline": "^1.24.0",
"@certd/plugin-cert": "^1.24.0",
"@koa/cors": "^3.4.3",
"@certd/pipeline": "^1.24.1",
"@certd/plugin-cert": "^1.24.1",
"@certd/plugin-plus": "^1.24.1",
"@koa/cors": "^5.0.0",
"@midwayjs/bootstrap": "^3.16.2",
"@midwayjs/cache": "^3.14.0",
"@midwayjs/core": "^3.16.2",
@@ -39,6 +40,7 @@
"@midwayjs/typeorm": "^3.16.4",
"@midwayjs/validate": "^3.16.4",
"axios": "^1.7.2",
"basic-ftp": "^5.0.5",
"bcryptjs": "^2.4.3",
"better-sqlite3": "^11.1.2",
"cache-manager": "^3.6.3",
@@ -48,7 +50,7 @@
"https-proxy-agent": "^7.0.4",
"iconv-lite": "^0.6.3",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^8.5.1",
"jsonwebtoken": "^9.0.0",
"koa-send": "^5.0.1",
"kubernetes-client": "^9.0.0",
"lodash-es": "^4.17.21",
@@ -63,9 +65,7 @@
"ssh2": "^1.15.0",
"svg-captcha": "^1.4.0",
"tencentcloud-sdk-nodejs": "^4.0.44",
"tencentcloud-sdk-nodejs-dnspod": "^4.0.866",
"tencentcloud-sdk-nodejs-teo": "^4.0.919",
"typeorm": "^0.3.11"
"typeorm": "^0.3.20"
},
"devDependencies": {
"@midwayjs/mock": "^3.16.4",

View File

@@ -17,7 +17,7 @@ export abstract class BaseService<T> {
async transaction(callback: (entityManager: EntityManager) => Promise<any>) {
const dataSource = this.dataSourceManager.getDataSource('default');
await dataSource.transaction(callback);
await dataSource.transaction(callback as any);
}
/**

View File

@@ -12,13 +12,11 @@ import { PipelineEntity } from '../modules/pipeline/entity/pipeline.js';
//import { logger } from '../utils/logger';
// load .env file in process.cwd
import { mergeConfig } from './loader.js';
import { Keys } from './keys.js';
const env = process.env.NODE_ENV || 'development';
const keys = Keys.load();
const development = {
keys: keys.cookieKeys,
keys: 'certd',
koa: {
port: 7001,
},
@@ -78,7 +76,6 @@ const development = {
auth: {
jwt: {
secret: keys.jwtKey,
expire: 7 * 24 * 60 * 60, //单位秒
},
},

View File

@@ -1,31 +0,0 @@
import fs from 'fs';
import yaml from 'js-yaml';
import * as _ from 'lodash-es';
import { nanoid } from 'nanoid';
import path from 'path';
const KEYS_FILE = './data/keys.yaml';
export class Keys {
jwtKey: string = nanoid();
cookieKeys: string[] = [nanoid()];
static load(): Keys {
const keys = new Keys();
if (fs.existsSync(KEYS_FILE)) {
const content = fs.readFileSync(KEYS_FILE, 'utf8');
const json = yaml.load(content);
_.merge(keys, json);
}
keys.save();
return keys;
}
save() {
const parent = path.dirname(KEYS_FILE);
if (!fs.existsSync(parent)) {
fs.mkdirSync(parent, {
recursive: true,
});
}
fs.writeFileSync(KEYS_FILE, yaml.dump(this));
}
}

View File

@@ -1,21 +1,31 @@
import { Config, Inject, MidwayWebRouterService, Provide } from '@midwayjs/core';
import { Init, Inject, MidwayWebRouterService, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { IMidwayKoaContext, IWebMiddleware, NextFunction } from '@midwayjs/koa';
import jwt from 'jsonwebtoken';
import { Constants } from '../basic/constants.js';
import { logger } from '../utils/logger.js';
import { AuthService } from '../modules/authority/service/auth-service.js';
import { SysSettingsService } from '../modules/system/service/sys-settings-service.js';
import { SysPrivateSettings } from '../modules/system/service/models.js';
/**
* 权限校验
*/
@Provide()
@Scope(ScopeEnum.Singleton)
export class AuthorityMiddleware implements IWebMiddleware {
@Config('auth.jwt.secret')
private secret: string;
@Inject()
webRouterService: MidwayWebRouterService;
@Inject()
authService: AuthService;
@Inject()
sysSettingsService: SysSettingsService;
secret: string;
@Init()
async init() {
const setting: SysPrivateSettings = await this.sysSettingsService.getSetting(SysPrivateSettings);
this.secret = setting.jwtKey;
}
resolve() {
return async (ctx: IMidwayKoaContext, next: NextFunction) => {

View File

@@ -49,7 +49,7 @@ export class PermissionController extends CrudController<PermissionService> {
@Post('/delete', { summary: 'sys:auth:per:remove' })
async delete(
@Query('id')
id
id : number
) {
return await super.delete(id);
}

View File

@@ -1,12 +1,4 @@
import {
ALL,
Body,
Controller,
Inject,
Post,
Provide,
Query,
} from '@midwayjs/core';
import { ALL, Body, Controller, Inject, Post, Provide, Query } from '@midwayjs/core';
import { CrudController } from '../../../basic/crud-controller.js';
import { RoleService } from '../service/role-service.js';
@@ -55,7 +47,7 @@ export class RoleController extends CrudController<RoleService> {
@Post('/delete', { summary: 'sys:auth:role:remove' })
async delete(
@Query('id')
id
id: number
) {
return await super.delete(id);
}
@@ -63,7 +55,7 @@ export class RoleController extends CrudController<RoleService> {
@Post('/getPermissionTree', { summary: 'sys:auth:role:view' })
async getPermissionTree(
@Query('id')
id
id: number
) {
const ret = await this.service.getPermissionTreeByRoleId(id);
return this.ok(ret);
@@ -72,7 +64,7 @@ export class RoleController extends CrudController<RoleService> {
@Post('/getPermissionIds', { summary: 'sys:auth:role:view' })
async getPermissionIds(
@Query('id')
id
id: number
) {
const ret = await this.service.getPermissionIdsByRoleId(id);
return this.ok(ret);

View File

@@ -73,7 +73,7 @@ export class UserController extends CrudController<UserService> {
@Post('/delete', { summary: 'sys:auth:user:remove' })
async delete(
@Query('id')
id
id : number
) {
return await super.delete(id);
}

View File

@@ -11,10 +11,10 @@ import { PermissionService } from './permission-service.js';
import { UserRoleService } from './user-role-service.js';
import { Constants } from '../../../basic/constants.js';
import { UserRoleEntity } from '../entity/user-role.js';
import { randomText } from 'svg-captcha';
import bcrypt from 'bcryptjs';
import { SysSettingsService } from '../../system/service/sys-settings-service.js';
import { SysInstallInfo } from '../../system/service/models.js';
import { RandomUtil } from '../../../utils/random.js';
/**
* 系统用户
@@ -64,7 +64,7 @@ export class UserService extends BaseService<UserEntity> {
if (!_.isEmpty(exists)) {
throw new CommonException('用户名已经存在');
}
const plainPassword = param.password ?? randomText(6);
const plainPassword = param.password ?? RandomUtil.randomStr(6);
param.passwordVersion = 2;
param.password = await this.genPassword(plainPassword, param.passwordVersion); // 默认密码 建议未改密码不能登陆
await super.add(param);
@@ -156,7 +156,7 @@ export class UserService extends BaseService<UserEntity> {
passwordVersion: 2,
});
if (!newUser.password) {
newUser.password = randomText(6);
newUser.password = RandomUtil.randomStr(6);
}
newUser.password = await this.genPassword(newUser.password, newUser.passwordVersion);

View File

@@ -1,11 +1,11 @@
import { Autoload, Init, Inject, Scope, ScopeEnum } from '@midwayjs/core';
import { Autoload, Config, Init, Inject, Scope, ScopeEnum } from '@midwayjs/core';
import { logger } from '../../utils/logger.js';
import { UserService } from '../authority/service/user-service.js';
import { SysSettingsService } from '../system/service/sys-settings-service.js';
import { nanoid } from 'nanoid';
import { SysInstallInfo, SysLicenseInfo } from '../system/service/models.js';
import { SysInstallInfo, SysLicenseInfo, SysPrivateSettings } from '../system/service/models.js';
import { verify } from '@certd/pipeline';
import crypto from 'crypto';
export type InstallInfo = {
installTime: number;
instanceId?: string;
@@ -17,12 +17,17 @@ export class AutoInitSite {
@Inject()
userService: UserService;
@Config('typeorm.dataSource.default.type')
dbType: string;
@Inject()
sysSettingsService: SysSettingsService;
@Init()
async init() {
logger.info('初始化站点开始');
await this.startOptimizeDb();
//安装信息
const installInfo: SysInstallInfo = await this.sysSettingsService.getSetting(SysInstallInfo);
if (!installInfo.siteId) {
installInfo.siteId = nanoid();
@@ -33,6 +38,19 @@ export class AutoInitSite {
await this.sysSettingsService.saveSetting(installInfo);
}
//private信息
const privateInfo = await this.sysSettingsService.getSetting<SysPrivateSettings>(SysPrivateSettings);
if (!privateInfo.jwtKey) {
privateInfo.jwtKey = nanoid();
await this.sysSettingsService.saveSetting(privateInfo);
}
if (!privateInfo.encryptSecret) {
const secretKey = crypto.randomBytes(32);
privateInfo.encryptSecret = secretKey.toString('base64');
await this.sysSettingsService.saveSetting(privateInfo);
}
// 授权许可
const licenseInfo: SysLicenseInfo = await this.sysSettingsService.getSetting(SysLicenseInfo);
const req = {
@@ -43,4 +61,25 @@ export class AutoInitSite {
logger.info('初始化站点完成');
}
async startOptimizeDb() {
//优化数据库
//检查当前数据库类型为sqlite
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数据库自动优化已开启');
}
const optimizeDb = async () => {
logger.info('sqlite数据库空间优化开始');
await this.userService.repository.query('VACUUM');
logger.info('sqlite数据库空间优化完成');
};
await optimizeDb();
setInterval(optimizeDb, 1000 * 60 * 60 * 24);
}
}
}

View File

@@ -1,6 +1,5 @@
import { Inject, Provide } from '@midwayjs/core';
import { CacheManager } from '@midwayjs/cache';
import svgCaptcha from 'svg-captcha';
// {data: '<svg.../svg>', text: 'abcd'}
/**
@@ -14,6 +13,7 @@ export class CodeService {
*/
async generateCaptcha(randomStr) {
console.assert(randomStr < 10, 'randomStr 过长');
const svgCaptcha = await import('svg-captcha');
const c = svgCaptcha.create();
//{data: '<svg.../svg>', text: 'abcd'}
const imgCode = c.text; // = RandomUtil.randomStr(4, true);

View File

@@ -14,7 +14,7 @@ export class PlusService {
@Config('plus.server.baseUrl')
plusServerBaseUrl;
async requestWithoutSign(config: any) {
async requestWithoutSign(config: any): Promise<any> {
config.baseURL = this.plusServerBaseUrl;
return await request(config);
}

View File

@@ -4,6 +4,8 @@ import jwt from 'jsonwebtoken';
import { CommonException } from '../../../basic/exception/common-exception.js';
import { RoleService } from '../../authority/service/role-service.js';
import { UserEntity } from '../../authority/entity/user.js';
import { SysSettingsService } from '../../system/service/sys-settings-service.js';
import { SysPrivateSettings } from '../../system/service/models.js';
/**
* 系统用户
@@ -17,6 +19,9 @@ export class LoginService {
@Config('auth.jwt')
private jwt: any;
@Inject()
sysSettingsService: SysSettingsService;
/**
* login
*/
@@ -47,7 +52,11 @@ export class LoginService {
roles: roleIds,
};
const expire = this.jwt.expire;
const token = jwt.sign(tokenInfo, this.jwt.secret, {
const setting = await this.sysSettingsService.getSetting<SysPrivateSettings>(SysPrivateSettings);
const jwtSecret = setting.jwtKey;
const token = jwt.sign(tokenInfo, jwtSecret, {
expiresIn: expire,
});

View File

@@ -49,13 +49,13 @@ export class UserSettingsController extends CrudController<UserSettingsService>
return super.update(bean);
}
@Post('/info', { summary: Constants.per.authOnly })
async info(@Query('id') id) {
async info(@Query('id') id: number) {
await this.service.checkUserId(id, this.ctx.user.id);
return super.info(id);
}
@Post('/delete', { summary: Constants.per.authOnly })
async delete(@Query('id') id) {
async delete(@Query('id') id: number) {
await this.service.checkUserId(id, this.ctx.user.id);
return super.delete(id);
}

View File

@@ -30,10 +30,7 @@ export class UserSettingsService extends BaseService<UserSettingsEntity> {
};
}
async getByKey(
key: string,
userId: number
): Promise<UserSettingsEntity | null> {
async getByKey(key: string, userId: number): Promise<UserSettingsEntity | null> {
if (!key || !userId) {
return null;
}

View File

@@ -1,12 +1,4 @@
import {
ALL,
Body,
Controller,
Inject,
Post,
Provide,
Query,
} from '@midwayjs/core';
import { ALL, Body, Controller, Inject, Post, Provide, Query } from '@midwayjs/core';
import { CrudController } from '../../../basic/crud-controller.js';
import { AccessService } from '../service/access-service.js';
import { Constants } from '../../../basic/constants.js';
@@ -28,7 +20,7 @@ export class AccessController extends CrudController<AccessService> {
async page(@Body(ALL) body) {
body.query = body.query ?? {};
body.query.userId = this.ctx.user.id;
return super.page(body);
return await super.page(body);
}
@Post('/list', { summary: Constants.per.authOnly })
@@ -49,19 +41,19 @@ export class AccessController extends CrudController<AccessService> {
return super.update(bean);
}
@Post('/info', { summary: Constants.per.authOnly })
async info(@Query('id') id) {
async info(@Query('id') id: number) {
await this.service.checkUserId(id, this.ctx.user.id);
return super.info(id);
}
@Post('/delete', { summary: Constants.per.authOnly })
async delete(@Query('id') id) {
async delete(@Query('id') id: number) {
await this.service.checkUserId(id, this.ctx.user.id);
return super.delete(id);
}
@Post('/define', { summary: Constants.per.authOnly })
async define(@Query('type') type) {
async define(@Query('type') type:string) {
const access = this.service.getDefineByType(type);
return this.ok(access);
}

View File

@@ -13,7 +13,7 @@ export class DnsProviderController extends BaseController {
service: DnsProviderService;
@Post('/list', { summary: Constants.per.authOnly })
async list(@Query(ALL) query) {
async list(@Query(ALL) query:any) {
query.userId = this.ctx.user.id;
const list = this.service.getList();
return this.ok(list);

View File

@@ -99,14 +99,14 @@ export class HistoryController extends CrudController<HistoryService> {
}
@Post('/delete', { summary: Constants.per.authOnly })
async delete(@Query('id') id) {
async delete(@Query('id') id: number) {
await this.authService.checkEntityUserId(this.ctx, this.getService(), id);
await super.delete(id);
return this.ok();
}
@Post('/deleteByIds', { summary: Constants.per.authOnly })
async deleteByIds(@Body(ALL) body) {
async deleteByIds(@Body(ALL) body: any) {
await this.authService.checkEntityUserId(this.ctx, this.getService(), body.ids);
const isAdmin = await this.authService.isAdmin(this.ctx);
const userId = isAdmin ? null : this.ctx.user.id;
@@ -115,21 +115,21 @@ export class HistoryController extends CrudController<HistoryService> {
}
@Post('/detail', { summary: Constants.per.authOnly })
async detail(@Query('id') id) {
async detail(@Query('id') id: number) {
await this.authService.checkEntityUserId(this.ctx, this.getService(), id);
const detail = await this.service.detail(id);
return this.ok(detail);
}
@Post('/logs', { summary: Constants.per.authOnly })
async logs(@Query('id') id) {
async logs(@Query('id') id: number) {
await this.authService.checkEntityUserId(this.ctx, this.logService, id);
const logInfo = await this.logService.info(id);
return this.ok(logInfo);
}
@Post('/files', { summary: Constants.per.authOnly })
async files(@Query('pipelineId') pipelineId, @Query('historyId') historyId) {
async files(@Query('pipelineId') pipelineId: number, @Query('historyId') historyId: number) {
await this.authService.checkEntityUserId(this.ctx, this.service, historyId);
const files = await this.getFiles(historyId, pipelineId);
return this.ok(files);
@@ -153,7 +153,7 @@ export class HistoryController extends CrudController<HistoryService> {
}
@Get('/download', { summary: Constants.per.authOnly })
async download(@Query('pipelineId') pipelineId, @Query('historyId') historyId, @Query('fileId') fileId) {
async download(@Query('pipelineId') pipelineId: number, @Query('historyId') historyId: number, @Query('fileId') fileId: string) {
await this.authService.checkEntityUserId(this.ctx, this.service, historyId);
const files = await this.getFiles(historyId, pipelineId);
const file = files.find(f => f.id === fileId);

View File

@@ -73,28 +73,28 @@ export class PipelineController extends CrudController<PipelineService> {
}
@Post('/delete', { summary: Constants.per.authOnly })
async delete(@Query('id') id) {
async delete(@Query('id') id: number) {
await this.authService.checkEntityUserId(this.ctx, this.getService(), id);
await this.service.delete(id);
return this.ok({});
}
@Post('/detail', { summary: Constants.per.authOnly })
async detail(@Query('id') id) {
async detail(@Query('id') id: number) {
await this.authService.checkEntityUserId(this.ctx, this.getService(), id);
const detail = await this.service.detail(id);
return this.ok(detail);
}
@Post('/trigger', { summary: Constants.per.authOnly })
async trigger(@Query('id') id) {
async trigger(@Query('id') id: number, @Query('stepId') stepId?: string) {
await this.authService.checkEntityUserId(this.ctx, this.getService(), id);
await this.service.trigger(id);
await this.service.trigger(id, stepId);
return this.ok({});
}
@Post('/cancel', { summary: Constants.per.authOnly })
async cancel(@Query('historyId') historyId) {
async cancel(@Query('historyId') historyId: number) {
await this.authService.checkEntityUserId(this.ctx, this.historyService, historyId);
await this.service.cancel(historyId);
return this.ok({});

View File

@@ -13,14 +13,14 @@ export class PluginController extends BaseController {
service: PluginService;
@Post('/list', { summary: Constants.per.authOnly })
async list(@Query(ALL) query) {
async list(@Query(ALL) query: any) {
query.userId = this.ctx.user.id;
const list = this.service.getList();
return this.ok(list);
}
@Post('/groups', { summary: Constants.per.authOnly })
async groups(@Query(ALL) query) {
async groups(@Query(ALL) query: any) {
query.userId = this.ctx.user.id;
const group = this.service.getGroups();
return this.ok(group);

View File

@@ -15,9 +15,12 @@ export class AccessEntity {
@Column({ comment: '类型', length: 100 })
type: string;
@Column({ name: 'setting', comment: '设置', length: 1024, nullable: true })
@Column({ name: 'setting', comment: '设置', length: 10240, nullable: true })
setting: string;
@Column({ name: 'encrypt_setting', comment: '已加密设置', length: 10240, nullable: true })
encryptSetting: string;
@Column({
name: 'create_time',
comment: '创建时间',

View File

@@ -1,33 +1,128 @@
import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { BaseService } from '../../../basic/base-service.js';
import { AccessEntity } from '../entity/access.js';
import { accessRegistry, IAccessService } from '@certd/pipeline';
import { AccessDefine, accessRegistry, IAccessService } from '@certd/pipeline';
import { EncryptService } from './encrypt-service.js';
import { ValidateException } from '../../../basic/exception/validation-exception.js';
/**
* 授权
*/
@Provide()
@Scope(ScopeEnum.Singleton)
export class AccessService
extends BaseService<AccessEntity>
implements IAccessService
{
export class AccessService extends BaseService<AccessEntity> implements IAccessService {
@InjectEntityModel(AccessEntity)
repository: Repository<AccessEntity>;
@Inject()
encryptService: EncryptService;
getRepository() {
return this.repository;
}
async page(query, page = { offset: 0, limit: 20 }, order, buildQuery) {
const res = await super.page(query, page, order, buildQuery);
res.records = res.records.map(item => {
delete item.encryptSetting;
return item;
});
return res;
}
async add(param) {
this.encryptSetting(param, null);
return await super.add(param);
}
encryptSetting(param: any, oldSettingEntity?: AccessEntity) {
const accessType = param.type;
const accessDefine: AccessDefine = accessRegistry.getDefine(accessType);
if (!accessDefine) {
throw new ValidateException(`授权类型${accessType}不存在`);
}
const setting = param.setting;
if (!setting) {
return;
}
const json = JSON.parse(setting);
let oldSetting = {};
let encryptSetting = {};
const firstEncrypt = !oldSettingEntity || !oldSettingEntity.encryptSetting || oldSettingEntity.encryptSetting === '{}';
if (oldSettingEntity) {
oldSetting = JSON.parse(oldSettingEntity.setting || '{}');
encryptSetting = JSON.parse(oldSettingEntity.encryptSetting || '{}');
}
for (const key in json) {
//加密
const value = json[key];
const accessInputDefine = accessDefine.input[key];
if (!accessInputDefine) {
throw new ValidateException(`授权类型${accessType}不存在字段${key}`);
}
if (!accessInputDefine.encrypt || !value || typeof value !== 'string') {
//定义无需加密、value为空、不是字符串 这些不需要加密
encryptSetting[key] = {
value: value,
encrypt: false,
};
continue;
}
if (firstEncrypt || oldSetting[key] !== value) {
//星号保护
const length = value.length;
const subIndex = Math.min(2, length);
let starLength = length - subIndex * 2;
starLength = Math.max(2, starLength);
const starString = '*'.repeat(starLength);
json[key] = value.substring(0, subIndex) + starString + value.substring(value.length - subIndex);
encryptSetting[key] = {
value: this.encryptService.encrypt(value),
encrypt: true,
};
}
//未改变情况下,不做修改
}
param.encryptSetting = JSON.stringify(encryptSetting);
param.setting = JSON.stringify(json);
}
/**
* 修改
* @param param 数据
*/
async update(param) {
const oldEntity = await this.info(param.id);
if (oldEntity == null) {
throw new ValidateException('该授权配置不存在,请确认是否已被删除');
}
this.encryptSetting(param, oldEntity);
return await super.update(param);
}
async getById(id: any): Promise<any> {
const entity = await this.info(id);
if (entity == null) {
throw new Error(`该授权配置不存在,请确认是否已被删除:id=${id}`);
}
// const access = accessRegistry.get(entity.type);
const setting = JSON.parse(entity.setting);
let setting = {};
if (entity.encryptSetting && entity.encryptSetting !== '{}') {
setting = JSON.parse(entity.encryptSetting);
for (const key in setting) {
//解密
const encryptValue = setting[key];
let value = encryptValue.value;
if (encryptValue.encrypt) {
value = this.encryptService.decrypt(value);
}
setting[key] = value;
}
} else if (entity.setting) {
setting = JSON.parse(entity.setting);
}
return {
id: entity.id,
...setting,

View File

@@ -1,7 +1,7 @@
import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { Provide } from '@midwayjs/core';
import { dnsProviderRegistry } from '@certd/plugin-cert';
@Provide()
@Scope(ScopeEnum.Singleton)
export class DnsProviderService {
getList() {
return dnsProviderRegistry.getDefineList();

View File

@@ -0,0 +1,44 @@
import { Init, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import crypto from 'crypto';
import { SysSettingsService } from '../../system/service/sys-settings-service.js';
import { SysPrivateSettings } from '../../system/service/models.js';
/**
* 授权
*/
@Provide()
@Scope(ScopeEnum.Singleton)
export class EncryptService {
secretKey: Buffer;
@Inject()
sysSettingService: SysSettingsService;
@Init()
async init() {
const privateInfo: SysPrivateSettings = await this.sysSettingService.getSetting(SysPrivateSettings);
this.secretKey = Buffer.from(privateInfo.encryptSecret, 'base64');
}
// 加密函数
encrypt(text: string) {
const iv = crypto.randomBytes(16); // 初始化向量
// const secretKey = crypto.randomBytes(32);
// const key = Buffer.from(secretKey);
const cipher = crypto.createCipheriv('aes-256-cbc', this.secretKey, iv);
let encrypted = cipher.update(text);
encrypted = Buffer.concat([encrypted, cipher.final()]);
return iv.toString('hex') + ':' + encrypted.toString('hex');
}
// 解密函数
decrypt(encryptedText: string) {
const textParts = encryptedText.split(':');
const iv = Buffer.from(textParts.shift(), 'hex');
const encrypted = Buffer.from(textParts.join(':'), 'hex');
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(this.secretKey), iv);
let decrypted = decipher.update(encrypted);
decrypted = Buffer.concat([decrypted, decipher.final()]);
return decrypted.toString();
}
}

View File

@@ -44,7 +44,7 @@ export class HistoryService extends BaseService<HistoryEntity> {
}
}
async detail(historyId: string) {
async detail(historyId: number) {
const entity = await this.info(historyId);
const log = await this.logService.info(historyId);
return new HistoryDetail(entity, log);

View File

@@ -197,14 +197,14 @@ export class PipelineService extends BaseService<PipelineEntity> {
}
}
async trigger(id) {
async trigger(id: any, stepId?: string) {
this.cron.register({
name: `pipeline.${id}.trigger.once`,
cron: null,
job: async () => {
logger.info('用户手动启动job');
try {
await this.run(id, null);
await this.run(id, null, stepId);
} catch (e) {
logger.error('手动job执行失败', e);
}
@@ -279,7 +279,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
logger.info('当前定时器数量:', this.cron.getTaskSize());
}
async run(id: number, triggerId: string) {
async run(id: number, triggerId: string, stepId?: string) {
const entity: PipelineEntity = await this.info(id);
const pipeline = JSON.parse(entity.content);
@@ -333,6 +333,10 @@ export class PipelineService extends BaseService<PipelineEntity> {
try {
runningTasks.set(historyId, executor);
await executor.init();
if (stepId) {
// 清除该step的状态
executor.clearLastStatus(stepId);
}
await executor.run(historyId, triggerType);
} catch (e) {
logger.error('执行失败:', e);

View File

@@ -45,13 +45,13 @@ export class SysSettingsController extends CrudController<SysSettingsService> {
return super.update(bean);
}
@Post('/info', { summary: 'sys:settings:view' })
async info(@Query('id') id) {
async info(@Query('id') id: number) {
await this.service.checkUserId(id, this.ctx.user.id);
return super.info(id);
}
@Post('/delete', { summary: 'sys:settings:edit' })
async delete(@Query('id') id) {
async delete(@Query('id') id: number) {
await this.service.checkUserId(id, this.ctx.user.id);
return super.delete(id);
}

View File

@@ -19,6 +19,8 @@ export class SysPrivateSettings extends BaseSettings {
static __title__ = '系统私有设置';
static __access__ = 'private';
static __key__ = 'sys.private';
jwtKey?: string;
encryptSecret?: string;
}
export class SysInstallInfo extends BaseSettings {

View File

@@ -74,14 +74,15 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
async getSetting<T>(type: any): Promise<T> {
const key = type.__key__;
const cacheKey = type.getCacheKey();
let settings: T = await this.cache.get(cacheKey);
let settingInstance: T = new type();
if (settings == null) {
settings = await this.getSettingByKey(key);
settingInstance = _.merge(settingInstance, settings);
await this.cache.set(key, settingInstance);
const settings: T = await this.cache.get(cacheKey);
if (settings) {
return settings;
}
return settingInstance;
let newSetting: T = new type();
const savedSettings = await this.getSettingByKey(key);
newSetting = _.merge(newSetting, savedSettings);
await this.cache.set(cacheKey, newSetting);
return newSetting;
}
async saveSetting<T extends BaseSettings>(bean: T) {

View File

@@ -1,4 +1,5 @@
export * from '@certd/plugin-cert';
export * from '@certd/plugin-plus';
export * from './plugin-aliyun/index.js';
export * from './plugin-tencent/index.js';
export * from './plugin-host/index.js';

View File

@@ -11,7 +11,7 @@ export class AliyunAccess {
component: {
placeholder: 'accessKeyId',
},
helper: '注意证书申请需要dns解析权限其他阿里云插件也需要对应的权限比如证书上传需要证书管理权限',
helper: '登录阿里云控制台->AccessKey管理页面获取。',
required: true,
})
accessKeyId = '';
@@ -21,6 +21,8 @@ export class AliyunAccess {
placeholder: 'accessKeySecret',
},
required: true,
encrypt: true,
helper: '注意证书申请需要dns解析权限其他阿里云插件需要对应的权限比如证书上传需要证书管理权限嫌麻烦就用主账号的全量权限的accessKey',
})
accessKeySecret = '';
}

View File

@@ -1,10 +1,4 @@
import Core from '@alicloud/pop-core';
import {
AbstractDnsProvider,
CreateRecordOptions,
IsDnsProvider,
RemoveRecordOptions,
} from '@certd/plugin-cert';
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert';
import { Autowire, ILogger } from '@certd/pipeline';
import { AliyunAccess } from '../access/index.js';
@@ -22,7 +16,8 @@ export class AliyunDnsProvider extends AbstractDnsProvider {
logger!: ILogger;
async onInstance() {
const access: any = this.access;
this.client = new Core({
const Core = await import('@alicloud/pop-core');
this.client = new Core.default({
accessKeyId: access.accessKeyId,
accessKeySecret: access.accessKeySecret,
endpoint: 'https://alidns.aliyuncs.com',
@@ -110,11 +105,7 @@ export class AliyunDnsProvider extends AbstractDnsProvider {
};
try {
const ret = await this.client.request(
'AddDomainRecord',
params,
requestOption
);
const ret = await this.client.request('AddDomainRecord', params, requestOption);
this.logger.info('添加域名解析成功:', value, value, ret.RecordId);
return ret.RecordId;
} catch (e: any) {
@@ -136,11 +127,7 @@ export class AliyunDnsProvider extends AbstractDnsProvider {
method: 'POST',
};
const ret = await this.client.request(
'DeleteDomainRecord',
params,
requestOption
);
const ret = await this.client.request('DeleteDomainRecord', params, requestOption);
this.logger.info('删除域名解析成功:', fullRecord, value, ret.RecordId);
return ret.RecordId;
}

View File

@@ -1,3 +1,3 @@
export * from './access/index.js';
export * from './dns-provider/index.js';
export * from './plugin/index.js';
export * from './access/index.js';

View File

@@ -1,8 +1,5 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, utils } from '@certd/pipeline';
// @ts-ignore
import { ROAClient } from '@alicloud/pop-core';
import { AliyunAccess } from '../../access/index.js';
import { K8sClient } from '@certd/lib-k8s';
import { appendTimeSuffix } from '../../utils/index.js';
import { CertInfo } from '@certd/plugin-cert';
@@ -91,6 +88,7 @@ export class DeployCertToAliyunAckIngressPlugin extends AbstractTaskPlugin {
helper: '请选择前置任务输出的域名证书',
component: {
name: 'pi-output-selector',
from: 'CertApply',
},
required: true,
})
@@ -105,17 +103,20 @@ export class DeployCertToAliyunAckIngressPlugin extends AbstractTaskPlugin {
required: true,
})
accessId!: string;
async onInstance(): Promise<void> {}
K8sClient: any;
async onInstance() {
const sdk = await import('@certd/lib-k8s');
this.K8sClient = sdk.K8sClient;
}
async execute(): Promise<void> {
console.log('开始部署证书到阿里云cdn');
const { regionId, ingressClass, clusterId, isPrivateIpAddress, cert } = this;
const access = (await this.accessService.getById(this.accessId)) as AliyunAccess;
const client = this.getClient(access, regionId);
const client = await this.getClient(access, regionId);
const kubeConfigStr = await this.getKubeConfig(client, clusterId, isPrivateIpAddress);
this.logger.info('kubeconfig已成功获取');
const k8sClient = new K8sClient({
const k8sClient = new this.K8sClient({
kubeConfigStr,
logger: this.logger,
});
@@ -131,7 +132,7 @@ export class DeployCertToAliyunAckIngressPlugin extends AbstractTaskPlugin {
// await this.restartIngress({ k8sClient, props })
}
async restartIngress(options: { k8sClient: K8sClient }) {
async restartIngress(options: { k8sClient: any }) {
const { k8sClient } = options;
const { namespace } = this;
@@ -168,7 +169,7 @@ export class DeployCertToAliyunAckIngressPlugin extends AbstractTaskPlugin {
}
}
async patchNginxCertSecret(options: { cert: CertInfo; k8sClient: K8sClient }) {
async patchNginxCertSecret(options: { cert: CertInfo; k8sClient: any }) {
const { cert, k8sClient } = options;
const crt = cert.crt;
const key = cert.key;
@@ -198,8 +199,10 @@ export class DeployCertToAliyunAckIngressPlugin extends AbstractTaskPlugin {
}
}
getClient(aliyunProvider: any, regionId: string) {
return new ROAClient({
async getClient(aliyunProvider: any, regionId: string) {
const Core = await import('@alicloud/pop-core');
return new Core.default({
accessKeyId: aliyunProvider.accessKeyId,
accessKeySecret: aliyunProvider.accessKeySecret,
endpoint: `https://cs.${regionId}.aliyuncs.com`,

View File

@@ -1,14 +1,11 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import dayjs from 'dayjs';
import Core from '@alicloud/pop-core';
import RPCClient from '@alicloud/pop-core';
import { AliyunAccess } from '../../access/index.js';
@IsTaskPlugin({
name: 'DeployCertToAliyunCDN',
title: '部署证书至阿里云CDN',
group: pluginGroups.aliyun.key,
desc: '依赖证书申请前置任务,自动部署域名证书至阿里云CDN',
desc: '自动部署域名证书至阿里云CDN',
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed,
@@ -34,6 +31,7 @@ export class DeployCertToAliyunCDN extends AbstractTaskPlugin {
helper: '请选择前置任务输出的域名证书',
component: {
name: 'pi-output-selector',
from: 'CertApply',
},
required: true,
})
@@ -54,14 +52,16 @@ export class DeployCertToAliyunCDN extends AbstractTaskPlugin {
async execute(): Promise<void> {
console.log('开始部署证书到阿里云cdn');
const access = (await this.accessService.getById(this.accessId)) as AliyunAccess;
const client = this.getClient(access);
const client = await this.getClient(access);
const params = await this.buildParams();
await this.doRequest(client, params);
console.log('部署完成');
}
getClient(access: AliyunAccess) {
return new Core({
async getClient(access: AliyunAccess) {
const Core = await import('@alicloud/pop-core');
return new Core.default({
accessKeyId: access.accessKeyId,
accessKeySecret: access.accessKeySecret,
endpoint: 'https://cdn.aliyuncs.com',
@@ -73,21 +73,20 @@ export class DeployCertToAliyunCDN extends AbstractTaskPlugin {
const CertName = (this.certName ?? 'certd') + '-' + dayjs().format('YYYYMMDDHHmmss');
const cert: any = this.cert;
return {
RegionId: 'cn-hangzhou',
DomainName: this.domainName,
ServerCertificateStatus: 'on',
SSLProtocol: 'on',
CertName: CertName,
CertType: 'upload',
ServerCertificate: cert.crt,
PrivateKey: cert.key,
SSLPub: cert.crt,
SSLPri: cert.key,
};
}
async doRequest(client: RPCClient, params: any) {
async doRequest(client: any, params: any) {
const requestOption = {
method: 'POST',
};
const ret: any = await client.request('SetDomainServerCertificate', params, requestOption);
const ret: any = await client.request('SetCdnDomainSSLCertificate', params, requestOption);
this.checkRet(ret);
this.logger.info('设置cdn证书成功:', ret.RequestId);
}

View File

@@ -0,0 +1,100 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import dayjs from 'dayjs';
import { AliyunAccess } from '../../access/index.js';
@IsTaskPlugin({
name: 'DeployCertToAliyunDCDN',
title: '部署证书至阿里云DCDN',
group: pluginGroups.aliyun.key,
desc: '依赖证书申请前置任务自动部署域名证书至阿里云DCDN',
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed,
},
},
})
export class DeployCertToAliyunDCDN extends AbstractTaskPlugin {
@TaskInput({
title: 'DCDN加速域名',
helper: '你在阿里云上配置的CDN加速域名比如:certd.docmirror.cn',
required: true,
})
domainName!: string;
@TaskInput({
title: '证书名称',
helper: '上传后将以此名称作为前缀备注',
})
certName!: string;
@TaskInput({
title: '域名证书',
helper: '请选择前置任务输出的域名证书',
component: {
name: 'pi-output-selector',
from: 'CertApply',
},
required: true,
})
cert!: string;
@TaskInput({
title: 'Access授权',
helper: '阿里云授权AccessKeyId、AccessKeySecret',
component: {
name: 'pi-access-selector',
type: 'aliyun',
},
required: true,
})
accessId!: string;
async onInstance() {}
async execute(): Promise<void> {
this.logger.info('开始部署证书到阿里云DCDN');
const access = (await this.accessService.getById(this.accessId)) as AliyunAccess;
const client = await this.getClient(access);
const params = await this.buildParams();
await this.doRequest(client, params);
this.logger.info('部署完成');
}
async getClient(access: AliyunAccess) {
const sdk = await import('@alicloud/pop-core');
return new sdk.default({
accessKeyId: access.accessKeyId,
accessKeySecret: access.accessKeySecret,
endpoint: 'https://dcdn.aliyuncs.com',
apiVersion: '2018-01-15',
});
}
async buildParams() {
const CertName = (this.certName ?? 'certd') + '-' + dayjs().format('YYYYMMDDHHmmss');
const cert: any = this.cert;
return {
DomainName: this.domainName,
SSLProtocol: 'on',
CertName: CertName,
CertType: 'upload',
SSLPub: cert.crt,
SSLPri: cert.key,
};
}
async doRequest(client: any, params: any) {
const requestOption = {
method: 'POST',
formatParams: false,
};
const ret: any = await client.request('SetDcdnDomainSSLCertificate', params, requestOption);
this.checkRet(ret);
this.logger.info('设置Dcdn证书成功:', ret.RequestId);
}
checkRet(ret: any) {
if (ret.code != null) {
throw new Error('执行失败:' + ret.Message);
}
}
}
new DeployCertToAliyunDCDN();

View File

@@ -1,3 +1,4 @@
export * from './deploy-to-cdn/index.js';
export * from './deploy-to-dcdn/index.js';
export * from './deploy-to-ack-ingress/index.js';
export * from './upload-to-aliyun/index.js';

View File

@@ -1,7 +1,6 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, TaskOutput } from '@certd/pipeline';
import Core from '@alicloud/pop-core';
import { appendTimeSuffix, checkRet } from '../../utils/index.js';
import { AliyunAccess } from '../../access/index.js';
import { appendTimeSuffix, checkRet, ZoneOptions } from '../../utils/index.js';
@IsTaskPlugin({
name: 'uploadCertToAliyun',
@@ -27,7 +26,7 @@ export class UploadCertToAliyun extends AbstractTaskPlugin {
component: {
name: 'a-auto-complete',
vModel: 'value',
options: ZoneOptions,
options: [{ value: 'cn-hangzhou' }, { value: 'eu-central-1' }, { value: 'ap-southeast-1' }],
},
required: true,
})
@@ -38,6 +37,7 @@ export class UploadCertToAliyun extends AbstractTaskPlugin {
helper: '请选择前置任务输出的域名证书',
component: {
name: 'pi-output-selector',
from: 'CertApply',
},
required: true,
})
@@ -62,9 +62,9 @@ export class UploadCertToAliyun extends AbstractTaskPlugin {
async onInstance() {}
async execute(): Promise<void> {
console.log('开始部署证书到阿里云cdn');
const access = (await this.accessService.getById(this.accessId)) as AliyunAccess;
const client = this.getClient(access);
this.logger.info('开始部署证书到阿里云cdn');
const access: AliyunAccess = await this.accessService.getById(this.accessId);
const client = await this.getClient(access);
const certName = appendTimeSuffix(this.name);
const params = {
RegionId: this.regionId || 'cn-hangzhou',
@@ -77,7 +77,7 @@ export class UploadCertToAliyun extends AbstractTaskPlugin {
method: 'POST',
};
const ret = (await client.request('CreateUserCertificate', params, requestOption)) as any;
const ret: any = await client.request('CreateUserCertificate', params, requestOption);
checkRet(ret);
this.logger.info('证书上传成功aliyunCertId=', ret.CertId);
@@ -85,8 +85,9 @@ export class UploadCertToAliyun extends AbstractTaskPlugin {
this.aliyunCertId = ret.CertId;
}
getClient(aliyunProvider: AliyunAccess) {
return new Core({
async getClient(aliyunProvider: AliyunAccess) {
const Core = await import('@alicloud/pop-core');
return new Core.default({
accessKeyId: aliyunProvider.accessKeyId,
accessKeySecret: aliyunProvider.accessKeySecret,
endpoint: 'https://cas.aliyuncs.com',

View File

@@ -20,6 +20,7 @@ export class CloudflareAccess {
},
helper: '前往 https://dash.cloudflare.com/profile/api-tokens 获取API令牌 token权限必须包含[Zone区域-Zone区域-Edit编辑], [Zone区域-DNS-Edit编辑]',
required: true,
encrypt: true,
})
apiToken = '';
}

View File

@@ -54,6 +54,7 @@ export class CloudflareDeployToCDNPlugin extends AbstractTaskPlugin {
helper: '请选择前置任务输出的域名证书',
component: {
name: 'pi-output-selector',
from: 'CertApply',
},
required: true,
})

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