Compare commits

...

31 Commits

Author SHA1 Message Date
xiaojunnuo
b649617e04 v1.31.4 2025-03-22 02:09:07 +08:00
xiaojunnuo
a4e2287101 build: prepare to build 2025-03-22 02:06:56 +08:00
xiaojunnuo
fbb66f3c43 perf: 手动上传证书部署流水线 2025-03-22 02:06:02 +08:00
xiaojunnuo
fedf90ea78 chore: 2025-03-21 23:40:31 +08:00
xiaojunnuo
d558d50102 chore: 2025-03-21 23:11:58 +08:00
xiaojunnuo
656cb89fe8 chore: 2025-03-21 12:23:59 +08:00
xiaojunnuo
1e6ddd250e chore: 2025-03-21 11:08:58 +08:00
xiaojunnuo
1de8eee6ea fix: 修复dns.la域名申请失败的bug 2025-03-21 11:07:15 +08:00
xiaojunnuo
425bba67c5 perf: 流水线增加上传证书快捷方式 2025-03-21 01:02:57 +08:00
xiaojunnuo
8b0daf7200 chore: 2025-03-20 23:19:14 +08:00
xiaojunnuo
589a373142 perf: 宝塔支持doker站点证书部署 2025-03-20 23:09:36 +08:00
xiaojunnuo
0cfc71e4bf Merge remote-tracking branch 'origin/v2-dev' into v2-dev 2025-03-19 15:01:55 +08:00
xiaojunnuo
92dabe6276 docs: docs sitemap 2025-03-19 15:01:14 +08:00
xiaojunnuo
d1b61b6bf9 chore: 支持手动上传证书并部署 2025-03-19 00:28:50 +08:00
xiaojunnuo
873f2b618b perf: 保存调整后的列宽 2025-03-18 10:00:16 +08:00
xiaojunnuo
4453070060 chore: 支持手动上传证书并部署 2025-03-18 01:02:20 +08:00
xiaojunnuo
de40be430b chore: 支持手动上传证书并部署 2025-03-18 00:52:50 +08:00
xiaojunnuo
29a6a992f0 chore: 2025-03-17 18:28:33 +08:00
xiaojunnuo
0a7d2d6264 chore: 2025-03-17 18:27:52 +08:00
xiaojunnuo
e09f92f9ee chore: 2025-03-17 18:24:55 +08:00
xiaojunnuo
9be1ecc8aa fix: 修复站点监控通知通过webhook发送失败的bug 2025-03-17 18:20:15 +08:00
xiaojunnuo
729b19c8da perf: 站点监控,手动测试也发通知 2025-03-17 16:55:23 +08:00
xiaojunnuo
a9fffa5180 perf: 支持手动上传证书并部署 2025-03-17 00:19:01 +08:00
xiaojunnuo
0069c0e399 perf: 站点证书监控支持模糊查询 2025-03-17 00:16:56 +08:00
xiaojunnuo
b6fd38e293 Merge remote-tracking branch 'origin/v2-dev' into v2-dev 2025-03-17 00:06:31 +08:00
xiaojunnuo
36aa7f82b0 perf: 创建证书流水线时,支持更多参数展开 2025-03-17 00:06:03 +08:00
xiaojunnuo
d01004d530 perf: 优化选择任务时手机版展示效果 2025-03-16 21:37:57 +08:00
xiaojunnuo
d85a02feeb perf: 流水线页面可以鼠标按住左右拖动 2025-03-16 21:16:14 +08:00
xiaojunnuo
b82e1dcd62 perf: 支持飞书通知 2025-03-14 13:16:48 +08:00
xiaojunnuo
74c6a2266f build: publish 2025-03-14 01:19:31 +08:00
xiaojunnuo
9754223f31 build: trigger build image 2025-03-14 01:19:12 +08:00
148 changed files with 3094 additions and 1332 deletions

View File

@@ -3,6 +3,27 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.31.4](https://github.com/certd/certd/compare/v1.31.3...v1.31.4) (2025-03-21)
### Bug Fixes
* 修复站点监控通知通过webhook发送失败的bug ([9be1ecc](https://github.com/certd/certd/commit/9be1ecc8aab3ea23dd0dc2dab3688f4edb90ef2c))
* 修复dns.la域名申请失败的bug ([1de8eee](https://github.com/certd/certd/commit/1de8eee6ea8307f3c11626af75303d3cc104bb95))
### Performance Improvements
* 宝塔支持doker站点证书部署 ([589a373](https://github.com/certd/certd/commit/589a373142ef7f50d64d3aa767a90b1f4b64da93))
* 保存调整后的列宽 ([873f2b6](https://github.com/certd/certd/commit/873f2b618b9d7320045baf69d6da83afe48a780f))
* 创建证书流水线时,支持更多参数展开 ([36aa7f8](https://github.com/certd/certd/commit/36aa7f82b078a053a102331b3c6f132fb9d492f9))
* 流水线页面可以鼠标按住左右拖动 ([d85a02f](https://github.com/certd/certd/commit/d85a02feeb3183c5abd6c1ea790d5923a32d7271))
* 流水线增加上传证书快捷方式 ([425bba6](https://github.com/certd/certd/commit/425bba67c539b734e2a85a83a4f9ecc9b2434fb4))
* 手动上传证书部署流水线 ([fbb66f3](https://github.com/certd/certd/commit/fbb66f3c4389489aa8a43b194d82bc8cf391607b))
* 优化选择任务时手机版展示效果 ([d01004d](https://github.com/certd/certd/commit/d01004d53071a75ac91ee21cc96bde9369f77ff3))
* 站点监控,手动测试也发通知 ([729b19c](https://github.com/certd/certd/commit/729b19c8da60d5efb5baef7cf8df0518e7f6b471))
* 站点证书监控支持模糊查询 ([0069c0e](https://github.com/certd/certd/commit/0069c0e3992946a8dd6410f299d4fc974ef0e76b))
* 支持飞书通知 ([b82e1dc](https://github.com/certd/certd/commit/b82e1dcd6217b09a7d7e21cd648bb31de320cadf))
* 支持手动上传证书并部署 ([a9fffa5](https://github.com/certd/certd/commit/a9fffa5180c83da27b35886aa2e858a92a2c5f94))
## [1.31.3](https://github.com/certd/certd/compare/v1.31.2...v1.31.3) (2025-03-13)
### Bug Fixes

View File

@@ -1 +1 @@
10:26
01:19

View File

@@ -12,6 +12,9 @@ export default defineConfig({
md.use(lightbox, {});
}
},
sitemap: {
hostname: 'https://certd.docmirror.cn'
},
head: [
// [
// 'meta',
@@ -25,9 +28,9 @@ export default defineConfig({
name: "keywords",
content: "证书自动申请、证书自动更新、证书自动续期、证书自动续签、证书管理工具、Certd、SSL证书自动部署、证书自动化https证书pfx证书der证书TLS证书nginx证书自动续签自动部署,SSL平台证书管理平台证书流水线"
}],
["meta", { name: "google-site-verification",content: "V5XLTSnXoT15uQotwpxJoQolUo2d5UbSL-TacsyOsC0"}],
// ["meta", { name: "google-site-verification",content: "V5XLTSnXoT15uQotwpxJoQolUo2d5UbSL-TacsyOsC0"}],
//<meta name="baidu-site-verification" content="codeva-MiWN8Y07Ua" />
["meta", {name: "baidu-site-verification",content: "codeva-MiWN8Y07Ua"}],
// ["meta", {name: "baidu-site-verification",content: "codeva-MiWN8Y07Ua"}],
["link", { rel: "icon", href: "/static/logo/logo.svg" }]
],
themeConfig: {

View File

@@ -3,6 +3,21 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.31.3](https://github.com/certd/certd/compare/v1.31.2...v1.31.3) (2025-03-13)
### Bug Fixes
* 修复阿里云fc获取不到列表的bug ([474b337](https://github.com/certd/certd/commit/474b3372d8ce98e6d45900bf8046bc0b3f220686))
### Performance Improvements
* 1panel支持 apikey方式授权 ([170b2af](https://github.com/certd/certd/commit/170b2afb0e3b125e4ed057f633fe895b5ac3ac22))
* 套餐支持3天7天等选项 ([0d71a8e](https://github.com/certd/certd/commit/0d71a8ee501a0e5bb69decf07e8729026e9d85bf))
* 证书仓库增加有效期显示 ([be87124](https://github.com/certd/certd/commit/be87124ada7a093f281ca29a45c86b4ea4644ead))
* 支持部署到天翼云CDN ([82a72e0](https://github.com/certd/certd/commit/82a72e0b497efa043d342ad0e33c083a2de79a05))
* 支持dns.la ([ee8af18](https://github.com/certd/certd/commit/ee8af18d0ac0af82544d6dda1e4b4c678b733041))
* cf授权支持配置http代理 ([27386ea](https://github.com/certd/certd/commit/27386ea04d3c1a5aebe3cfdd7ac48185eaa76629))
## [1.31.2](https://github.com/certd/certd/compare/v1.31.1...v1.31.2) (2025-03-12)
### Bug Fixes

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

@@ -9,5 +9,5 @@
}
},
"npmClient": "pnpm",
"version": "1.31.3"
"version": "1.31.4"
}

View File

@@ -9,7 +9,7 @@
"@lerna-lite/run": "^3.9.3",
"@lerna-lite/version": "^3.9.3",
"medium-zoom": "^1.1.0",
"vitepress": "^1.4.1",
"vitepress": "^2.0.0-alpha.4",
"vitepress-plugin-lightbox": "^1.0.2"
},
"scripts": {

View File

@@ -0,0 +1,7 @@
{
"printWidth": 220,
"bracketSpacing": true,
"singleQuote": false,
"trailingComma": "es5",
"arrowParens": "avoid"
}

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.31.4](https://github.com/publishlab/node-acme-client/compare/v1.31.3...v1.31.4) (2025-03-21)
**Note:** Version bump only for package @certd/acme-client
## [1.31.3](https://github.com/publishlab/node-acme-client/compare/v1.31.2...v1.31.3) (2025-03-13)
**Note:** Version bump only for package @certd/acme-client

View File

@@ -3,7 +3,7 @@
"description": "Simple and unopinionated ACME client",
"private": false,
"author": "nmorsman",
"version": "1.31.3",
"version": "1.31.4",
"type": "module",
"module": "scr/index.js",
"main": "src/index.js",
@@ -18,7 +18,7 @@
"types"
],
"dependencies": {
"@certd/basic": "^1.31.3",
"@certd/basic": "^1.31.4",
"@peculiar/x509": "^1.11.0",
"asn1js": "^3.0.5",
"axios": "^1.7.2",
@@ -67,5 +67,5 @@
"bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues"
},
"gitHead": "98445afd3ec9e89c5a1b2c2b89dd9ea5e725237a"
"gitHead": "cfbbac9796477f830e1f57f77777f6554da9e31d"
}

View File

@@ -16,7 +16,7 @@
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/no-explicit-any": "off",
// "no-unused-expressions": "off",
"max-len": [0, 160, 2, { "ignoreUrls": true }]
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-unused-vars": "off"
}
}

View File

@@ -0,0 +1,7 @@
{
"printWidth": 220,
"bracketSpacing": true,
"singleQuote": false,
"trailingComma": "es5",
"arrowParens": "avoid"
}

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.31.4](https://github.com/certd/certd/compare/v1.31.3...v1.31.4) (2025-03-21)
**Note:** Version bump only for package @certd/basic
## [1.31.3](https://github.com/certd/certd/compare/v1.31.2...v1.31.3) (2025-03-13)
### Performance Improvements

View File

@@ -1 +1 @@
01:15
02:06

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/basic",
"private": false,
"version": "1.31.3",
"version": "1.31.4",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -44,5 +44,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "98445afd3ec9e89c5a1b2c2b89dd9ea5e725237a"
"gitHead": "cfbbac9796477f830e1f57f77777f6554da9e31d"
}

View File

@@ -7,8 +7,8 @@ function sha256(data: string, digest: BinaryToTextEncoding = 'hex') {
return crypto.createHash('sha256').update(data).digest(digest);
}
function HmacSha256(data: string, key: string, digest: BinaryToTextEncoding = 'base64') {
return crypto.createHmac('sha256', Buffer.from(key, 'base64')).update(data).digest(digest);
function hmacSha256(data: string, digest: BinaryToTextEncoding = 'base64') {
return crypto.createHmac('sha256', data).update(Buffer.alloc(0)).digest(digest);
}
function base64(data: string) {
@@ -18,5 +18,5 @@ export const hashUtils = {
md5,
sha256,
base64,
HmacSha256,
hmacSha256,
};

View File

@@ -16,7 +16,7 @@
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/no-explicit-any": "off",
// "no-unused-expressions": "off",
"max-len": [0, 160, 2, { "ignoreUrls": true }]
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-unused-vars": "off"
}
}

View File

@@ -1,3 +1,7 @@
{
"printWidth": 160
"printWidth": 220,
"bracketSpacing": true,
"singleQuote": false,
"trailingComma": "es5",
"arrowParens": "avoid"
}

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.31.4](https://github.com/certd/certd/compare/v1.31.3...v1.31.4) (2025-03-21)
### Bug Fixes
* 修复站点监控通知通过webhook发送失败的bug ([9be1ecc](https://github.com/certd/certd/commit/9be1ecc8aab3ea23dd0dc2dab3688f4edb90ef2c))
### Performance Improvements
* 流水线增加上传证书快捷方式 ([425bba6](https://github.com/certd/certd/commit/425bba67c539b734e2a85a83a4f9ecc9b2434fb4))
* 支持飞书通知 ([b82e1dc](https://github.com/certd/certd/commit/b82e1dcd6217b09a7d7e21cd648bb31de320cadf))
* 支持手动上传证书并部署 ([a9fffa5](https://github.com/certd/certd/commit/a9fffa5180c83da27b35886aa2e858a92a2c5f94))
## [1.31.3](https://github.com/certd/certd/compare/v1.31.2...v1.31.3) (2025-03-13)
**Note:** Version bump only for package @certd/pipeline

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/pipeline",
"private": false,
"version": "1.31.3",
"version": "1.31.4",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -16,8 +16,8 @@
"test": "mocha --loader=ts-node/esm"
},
"dependencies": {
"@certd/basic": "^1.31.3",
"@certd/plus-core": "^1.31.3",
"@certd/basic": "^1.31.4",
"@certd/plus-core": "^1.31.4",
"dayjs": "^1.11.7",
"lodash-es": "^4.17.21",
"reflect-metadata": "^0.1.13"
@@ -43,5 +43,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "98445afd3ec9e89c5a1b2c2b89dd9ea5e725237a"
"gitHead": "cfbbac9796477f830e1f57f77777f6554da9e31d"
}

View File

@@ -7,7 +7,7 @@ import { createAxiosService, hashUtils, HttpRequestConfig, ILogger, logger, util
import { IAccessService } from "../access/index.js";
import { RegistryItem } from "../registry/index.js";
import { Decorator } from "../decorator/index.js";
import { ICnameProxyService, IEmailService, IPluginConfigService, IUrlService } from "../service/index.js";
import { ICnameProxyService, IEmailService, IPluginConfigService, IServiceGetter, IUrlService } from "../service/index.js";
import { FileStore } from "./file-store.js";
import { cloneDeep, forEach, merge } from "lodash-es";
import { INotificationService } from "../notification/index.js";
@@ -33,6 +33,7 @@ export type ExecutorOptions = {
user: UserInfo;
baseURL?: string;
sysInfo?: SysInfo;
serviceGetter: IServiceGetter;
};
export class Executor {
@@ -365,6 +366,7 @@ export class Executor {
step,
pipeline: this.pipeline,
}),
serviceGetter: this.options.serviceGetter,
};
instance.setCtx(taskCtx);

View File

@@ -11,11 +11,13 @@ function attachProperty(target: any, propertyKey: string | symbol) {
}
function getClassProperties(target: any) {
//获取父类
//获取父类, 向上追溯三层
const parent = Object.getPrototypeOf(target);
const pParent = Object.getPrototypeOf(parent);
const pParentMap = propertyMap[pParent] || {};
const parentMap = propertyMap[parent] || {};
const current = propertyMap[target] || {};
return _.merge({}, parentMap, current);
return _.merge({}, pParentMap, parentMap, current);
}
function target(target: any, propertyKey?: string | symbol) {

View File

@@ -119,10 +119,12 @@ export abstract class BaseNotification implements INotification {
}
async onTestRequest() {
await this.doSend({
return await this.doSend({
userId: 0,
title: "【Certd】测试通知【*.foo.com】标题长度测试、测试、测试",
content: "测试通知,*.foo.com",
content: `测试通知,*.foo.com
换行测试
`,
pipeline: {
id: 1,
title: "证书申请成功【测试流水线】",

View File

@@ -2,7 +2,7 @@ import { Registrable } from "../registry/index.js";
import { FileItem, FormItemProps, Pipeline, Runnable, Step } from "../dt/index.js";
import { FileStore } from "../core/file-store.js";
import { IAccessService } from "../access/index.js";
import { ICnameProxyService, IEmailService, IUrlService } from "../service/index.js";
import { ICnameProxyService, IEmailService, IServiceGetter, IUrlService } from "../service/index.js";
import { CancelError, IContext, RunHistory, RunnableCollection } from "../core/index.js";
import { HttpRequestConfig, ILogger, logger, utils } from "@certd/basic";
import { HttpClient } from "@certd/basic";
@@ -55,6 +55,14 @@ export type PluginDefine = Registrable & {
[key: string]: any;
};
shortcut?: {
[key: string]: {
title: string;
icon: string;
action: string;
form: any;
};
};
needPlus?: boolean;
};
@@ -114,6 +122,9 @@ export type TaskInstanceContext = {
user: UserInfo;
emitter: TaskEmitter;
//service 容器
serviceGetter?: IServiceGetter;
};
export abstract class AbstractTaskPlugin implements ITaskPlugin {
@@ -221,7 +232,7 @@ export abstract class AbstractTaskPlugin implements ITaskPlugin {
getStepFromPipeline(stepId: string) {
let found: any = null;
RunnableCollection.each(this.ctx.pipeline.stages, (step) => {
RunnableCollection.each(this.ctx.pipeline.stages, step => {
if (step.id === stepId) {
found = step;
return;

View File

@@ -3,3 +3,6 @@ export * from "./cname.js";
export * from "./config.js";
export * from "./url.js";
export * from "./emit.js";
export type IServiceGetter = {
get: (name: string) => Promise<any>;
};

View File

@@ -17,7 +17,6 @@
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-function": "off",
// "no-unused-expressions": "off",
"max-len": [0, 160, 2, { "ignoreUrls": true }]
"@typescript-eslint/no-unused-vars": "off"
}
}

View File

@@ -1,3 +1,7 @@
{
"printWidth": 160
"printWidth": 220,
"bracketSpacing": true,
"singleQuote": false,
"trailingComma": "es5",
"arrowParens": "avoid"
}

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.31.4](https://github.com/certd/certd/compare/v1.31.3...v1.31.4) (2025-03-21)
**Note:** Version bump only for package @certd/lib-huawei
## [1.31.3](https://github.com/certd/certd/compare/v1.31.2...v1.31.3) (2025-03-13)
**Note:** Version bump only for package @certd/lib-huawei

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/lib-huawei",
"private": false,
"version": "1.31.3",
"version": "1.31.4",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
"types": "./dist/d/index.d.ts",
@@ -23,5 +23,5 @@
"prettier": "^2.8.8",
"tslib": "^2.8.1"
},
"gitHead": "98445afd3ec9e89c5a1b2c2b89dd9ea5e725237a"
"gitHead": "cfbbac9796477f830e1f57f77777f6554da9e31d"
}

View File

@@ -17,7 +17,6 @@
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-function": "off",
// "no-unused-expressions": "off",
"max-len": [0, 160, 2, { "ignoreUrls": true }]
"@typescript-eslint/no-unused-vars": "off"
}
}

View File

@@ -1,7 +1,7 @@
{
"printWidth": 160,
"printWidth": 220,
"bracketSpacing": true,
"singleQuote": true,
"singleQuote": false,
"trailingComma": "es5",
"arrowParens": "avoid"
}

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.31.4](https://github.com/certd/certd/compare/v1.31.3...v1.31.4) (2025-03-21)
**Note:** Version bump only for package @certd/lib-iframe
## [1.31.3](https://github.com/certd/certd/compare/v1.31.2...v1.31.3) (2025-03-13)
**Note:** Version bump only for package @certd/lib-iframe

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/lib-iframe",
"private": false,
"version": "1.31.3",
"version": "1.31.4",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -30,5 +30,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "98445afd3ec9e89c5a1b2c2b89dd9ea5e725237a"
"gitHead": "cfbbac9796477f830e1f57f77777f6554da9e31d"
}

View File

@@ -1,7 +1,7 @@
{
"printWidth": 160,
"printWidth": 220,
"bracketSpacing": true,
"singleQuote": true,
"singleQuote": false,
"trailingComma": "es5",
"arrowParens": "avoid"
}

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.31.4](https://github.com/certd/certd/compare/v1.31.3...v1.31.4) (2025-03-21)
**Note:** Version bump only for package @certd/lib-k8s
## [1.31.3](https://github.com/certd/certd/compare/v1.31.2...v1.31.3) (2025-03-13)
**Note:** Version bump only for package @certd/lib-k8s

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/lib-k8s",
"private": false,
"version": "1.31.3",
"version": "1.31.4",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -16,7 +16,7 @@
"preview": "vite preview"
},
"dependencies": {
"@certd/basic": "^1.31.3",
"@certd/basic": "^1.31.4",
"@kubernetes/client-node": "0.21.0"
},
"devDependencies": {
@@ -31,5 +31,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "98445afd3ec9e89c5a1b2c2b89dd9ea5e725237a"
"gitHead": "cfbbac9796477f830e1f57f77777f6554da9e31d"
}

View File

@@ -1,7 +1,7 @@
{
"printWidth": 160,
"printWidth": 220,
"bracketSpacing": true,
"singleQuote": true,
"singleQuote": false,
"trailingComma": "es5",
"arrowParens": "avoid"
}

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.31.4](https://github.com/certd/certd/compare/v1.31.3...v1.31.4) (2025-03-21)
**Note:** Version bump only for package @certd/lib-server
## [1.31.3](https://github.com/certd/certd/compare/v1.31.2...v1.31.3) (2025-03-13)
**Note:** Version bump only for package @certd/lib-server

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/lib-server",
"version": "1.31.3",
"version": "1.31.4",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -27,10 +27,10 @@
],
"license": "AGPL",
"dependencies": {
"@certd/acme-client": "^1.31.3",
"@certd/basic": "^1.31.3",
"@certd/pipeline": "^1.31.3",
"@certd/plus-core": "^1.31.3",
"@certd/acme-client": "^1.31.4",
"@certd/basic": "^1.31.4",
"@certd/pipeline": "^1.31.4",
"@certd/plus-core": "^1.31.4",
"@midwayjs/cache": "~3.14.0",
"@midwayjs/core": "~3.20.3",
"@midwayjs/i18n": "~3.20.3",
@@ -61,5 +61,5 @@
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "98445afd3ec9e89c5a1b2c2b89dd9ea5e725237a"
"gitHead": "cfbbac9796477f830e1f57f77777f6554da9e31d"
}

View File

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

View File

@@ -1,7 +1,7 @@
{
"printWidth": 160,
"printWidth": 220,
"bracketSpacing": true,
"singleQuote": true,
"singleQuote": false,
"trailingComma": "es5",
"arrowParens": "avoid"
}

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.31.4](https://github.com/certd/certd/compare/v1.31.3...v1.31.4) (2025-03-21)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.31.3](https://github.com/certd/certd/compare/v1.31.2...v1.31.3) (2025-03-13)
**Note:** Version bump only for package @certd/midway-flyway-js

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/midway-flyway-js",
"version": "1.31.3",
"version": "1.31.4",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -46,5 +46,5 @@
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "98445afd3ec9e89c5a1b2c2b89dd9ea5e725237a"
"gitHead": "cfbbac9796477f830e1f57f77777f6554da9e31d"
}

View File

@@ -17,7 +17,6 @@
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-function": "off",
// "no-unused-expressions": "off",
"max-len": [0, 160, 2, { "ignoreUrls": true }]
"@typescript-eslint/no-unused-vars": "off"
}
}

View File

@@ -1,3 +1,7 @@
{
"printWidth": 160
"printWidth": 220,
"bracketSpacing": true,
"singleQuote": false,
"trailingComma": "es5",
"arrowParens": "avoid"
}

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.31.4](https://github.com/certd/certd/compare/v1.31.3...v1.31.4) (2025-03-21)
### Bug Fixes
* 修复dns.la域名申请失败的bug ([1de8eee](https://github.com/certd/certd/commit/1de8eee6ea8307f3c11626af75303d3cc104bb95))
### Performance Improvements
* 流水线增加上传证书快捷方式 ([425bba6](https://github.com/certd/certd/commit/425bba67c539b734e2a85a83a4f9ecc9b2434fb4))
* 手动上传证书部署流水线 ([fbb66f3](https://github.com/certd/certd/commit/fbb66f3c4389489aa8a43b194d82bc8cf391607b))
* 支持手动上传证书并部署 ([a9fffa5](https://github.com/certd/certd/commit/a9fffa5180c83da27b35886aa2e858a92a2c5f94))
## [1.31.3](https://github.com/certd/certd/compare/v1.31.2...v1.31.3) (2025-03-13)
### Performance Improvements

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-cert",
"private": false,
"version": "1.31.3",
"version": "1.31.4",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -15,10 +15,10 @@
"preview": "vite preview"
},
"dependencies": {
"@certd/acme-client": "^1.31.3",
"@certd/basic": "^1.31.3",
"@certd/pipeline": "^1.31.3",
"@certd/plugin-lib": "^1.31.3",
"@certd/acme-client": "^1.31.4",
"@certd/basic": "^1.31.4",
"@certd/pipeline": "^1.31.4",
"@certd/plugin-lib": "^1.31.4",
"@google-cloud/publicca": "^1.3.0",
"dayjs": "^1.11.7",
"jszip": "^3.10.1",
@@ -41,5 +41,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "98445afd3ec9e89c5a1b2c2b89dd9ea5e725237a"
"gitHead": "cfbbac9796477f830e1f57f77777f6554da9e31d"
}

View File

@@ -60,6 +60,7 @@ type AcmeServiceOptions = {
reverseProxy?: string;
privateKeyType?: PrivateKeyType;
signal?: AbortSignal;
maxCheckRetryCount?: number;
};
export class AcmeService {
@@ -144,7 +145,7 @@ export class AcmeService {
accountKey: conf.key,
accountUrl: conf.accountUrl,
externalAccountBinding: this.eab,
backoffAttempts: 20,
backoffAttempts: this.options.maxCheckRetryCount || 20,
backoffMin: 5000,
backoffMax: 10000,
urlMapping,
@@ -282,15 +283,7 @@ export class AcmeService {
* @returns {Promise}
*/
async challengeRemoveFn(
authz: any,
challenge: any,
keyAuthorization: string,
recordReq: any,
recordRes: any,
dnsProvider?: IDnsProvider,
httpUploader?: HttpChallengeUploader
) {
async challengeRemoveFn(authz: any, challenge: any, keyAuthorization: string, recordReq: any, recordRes: any, dnsProvider?: IDnsProvider, httpUploader?: HttpChallengeUploader) {
this.logger.info("执行清理");
/* http-01 */
@@ -387,14 +380,7 @@ export class AcmeService {
): Promise<{ recordReq?: any; recordRes?: any; dnsProvider?: any; challenge: Challenge; keyAuthorization: string }> => {
return await this.challengeCreateFn(authz, keyAuthorizationGetter, providers);
},
challengeRemoveFn: async (
authz: acme.Authorization,
challenge: Challenge,
keyAuthorization: string,
recordReq: any,
recordRes: any,
dnsProvider: IDnsProvider
): Promise<any> => {
challengeRemoveFn: async (authz: acme.Authorization, challenge: Challenge, keyAuthorization: string, recordReq: any, recordRes: any, dnsProvider: IDnsProvider): Promise<any> => {
return await this.challengeRemoveFn(authz, challenge, keyAuthorization, recordReq, recordRes, dnsProvider, httpUploader);
},
signal: this.options.signal,

View File

@@ -0,0 +1,188 @@
import { AbstractTaskPlugin, IContext, Step, TaskInput, TaskOutput } from "@certd/pipeline";
import dayjs from "dayjs";
import type { CertInfo } from "./acme.js";
import { CertReader } from "./cert-reader.js";
import JSZip from "jszip";
import { CertConverter } from "./convert.js";
export const EVENT_CERT_APPLY_SUCCESS = "CertApply.success";
export abstract class CertApplyBaseConvertPlugin extends AbstractTaskPlugin {
@TaskInput({
title: "域名",
component: {
name: "a-select",
vModel: "value",
mode: "tags",
open: false,
placeholder: "foo.com / *.foo.com / *.bar.com",
tokenSeparators: [",", " ", "", "、", "|"],
},
rules: [{ type: "domains" }],
required: true,
col: {
span: 24,
},
order: -999,
helper:
"1、支持多个域名打到一个证书上例如 foo.com*.foo.com*.bar.com\n" +
"2、子域名被通配符包含的不要填写例如www.foo.com已经被*.foo.com包含不要填写www.foo.com\n" +
"3、泛域名只能通配*号那一级(*.foo.com的证书不能用于xxx.yyy.foo.com、不能用于foo.com\n" +
"4、输入一个空格之后再输入下一个",
})
domains!: string[];
@TaskInput({
title: "证书加密密码",
component: {
name: "input-password",
vModel: "value",
},
required: false,
order: 100,
helper: "转换成PFX、jks格式证书是否需要加密\njks必须设置密码不传则默认123456\npfx不传则为空密码",
})
pfxPassword!: string;
@TaskInput({
title: "PFX证书转换参数",
value: "-macalg SHA1 -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES",
component: {
name: "a-auto-complete",
vModel: "value",
options: [
{ value: "", label: "兼容 Windows Server 最新" },
{ value: "-macalg SHA1 -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES", label: "兼容 Windows Server 2016" },
{ value: "-nomac -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES", label: "兼容 Windows Server 2008" },
],
},
required: false,
order: 100,
helper: "兼容Windows Server各个版本",
})
pfxArgs = "-macalg SHA1 -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES";
userContext!: IContext;
lastStatus!: Step;
@TaskOutput({
title: "域名证书",
})
cert?: CertInfo;
async onInstance() {
this.userContext = this.ctx.userContext;
this.lastStatus = this.ctx.lastStatus as Step;
await this.onInit();
}
abstract onInit(): Promise<void>;
//必须output之后执行
async emitCertApplySuccess() {
const emitter = this.ctx.emitter;
const value = {
cert: this.cert,
file: this._result.files[0].path,
};
await emitter.emit(EVENT_CERT_APPLY_SUCCESS, value);
}
async output(certReader: CertReader, isNew: boolean) {
const cert: CertInfo = certReader.toCertInfo();
this.cert = cert;
this._result.pipelineVars.certExpiresTime = dayjs(certReader.detail.notAfter).valueOf();
if (!this._result.pipelinePrivateVars) {
this._result.pipelinePrivateVars = {};
}
this._result.pipelinePrivateVars.cert = cert;
if (isNew) {
try {
const converter = new CertConverter({ logger: this.logger });
const res = await converter.convert({
cert,
pfxPassword: this.pfxPassword,
pfxArgs: this.pfxArgs,
});
if (cert.pfx == null && res.pfx) {
cert.pfx = res.pfx;
}
if (cert.der == null && res.der) {
cert.der = res.der;
}
if (cert.jks == null && res.jks) {
cert.jks = res.jks;
}
this.logger.info("转换证书格式成功");
} catch (e) {
this.logger.error("转换证书格式失败", e);
}
}
if (isNew) {
const zipFileName = certReader.buildCertFileName("zip", certReader.detail.notBefore);
await this.zipCert(cert, zipFileName);
} else {
this.extendsFiles();
}
}
async zipCert(cert: CertInfo, filename: string) {
const zip = new JSZip();
zip.file("证书.pem", cert.crt);
zip.file("私钥.pem", cert.key);
zip.file("中间证书.pem", cert.ic);
zip.file("cert.crt", cert.crt);
zip.file("cert.key", cert.key);
zip.file("intermediate.crt", cert.ic);
zip.file("origin.crt", cert.oc);
zip.file("one.pem", cert.one);
if (cert.pfx) {
zip.file("cert.pfx", Buffer.from(cert.pfx, "base64"));
}
if (cert.der) {
zip.file("cert.der", Buffer.from(cert.der, "base64"));
}
if (cert.jks) {
zip.file("cert.jks", Buffer.from(cert.jks, "base64"));
}
zip.file(
"说明.txt",
`证书文件说明
cert.crt证书文件包含证书链pem格式
cert.key私钥文件pem格式
intermediate.crt中间证书文件pem格式
origin.crt原始证书文件不含证书链pem格式
one.pem 证书和私钥简单合并成一个文件pem格式crt正文+key正文
cert.pfxpfx格式证书文件iis服务器使用
cert.derder格式证书文件
cert.jksjks格式证书文件java服务器使用
`
);
const content = await zip.generateAsync({ type: "nodebuffer" });
this.saveFile(filename, content);
this.logger.info(`已保存文件:${filename}`);
}
formatCert(pem: string) {
pem = pem.replace(/\r/g, "");
pem = pem.replace(/\n\n/g, "\n");
pem = pem.replace(/\n$/g, "");
return pem;
}
formatCerts(cert: { crt: string; key: string; csr: string }) {
const newCert: CertInfo = {
crt: this.formatCert(cert.crt),
key: this.formatCert(cert.key),
csr: this.formatCert(cert.csr),
};
return newCert;
}
}

View File

@@ -1,42 +1,10 @@
import { AbstractTaskPlugin, IContext, NotificationBody, Step, TaskEmitter, TaskInput, TaskOutput } from "@certd/pipeline";
import { NotificationBody, Step, TaskInput } from "@certd/pipeline";
import dayjs from "dayjs";
import type { CertInfo } from "./acme.js";
import { CertReader } from "./cert-reader.js";
import JSZip from "jszip";
import { CertConverter } from "./convert.js";
import { pick } from "lodash-es";
import { CertApplyBaseConvertPlugin } from "./base-convert.js";
export const EVENT_CERT_APPLY_SUCCESS = "CertApply.success";
export async function emitCertApplySuccess(emitter: TaskEmitter, cert: CertReader) {
await emitter.emit(EVENT_CERT_APPLY_SUCCESS, cert);
}
export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
@TaskInput({
title: "域名",
component: {
name: "a-select",
vModel: "value",
mode: "tags",
open: false,
placeholder: "foo.com / *.foo.com / *.bar.com",
tokenSeparators: [",", " ", "", "、", "|"],
},
rules: [{ type: "domains" }],
required: true,
col: {
span: 24,
},
order: -999,
helper:
"1、支持多个域名打到一个证书上例如 foo.com*.foo.com*.bar.com\n" +
"2、子域名被通配符包含的不要填写例如www.foo.com已经被*.foo.com包含不要填写www.foo.com\n" +
"3、泛域名只能通配*号那一级(*.foo.com的证书不能用于xxx.yyy.foo.com、不能用于foo.com\n" +
"4、输入一个空格之后再输入下一个",
})
domains!: string[];
export abstract class CertApplyBasePlugin extends CertApplyBaseConvertPlugin {
@TaskInput({
title: "邮箱",
component: {
@@ -50,36 +18,6 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
})
email!: string;
@TaskInput({
title: "证书密码",
component: {
name: "input-password",
vModel: "value",
},
required: false,
order: 100,
helper: "PFX、jks格式证书是否加密\njks必须设置密码不传则默认123456\npfx不传则为空密码",
})
pfxPassword!: string;
@TaskInput({
title: "PFX证书转换参数",
value: "-macalg SHA1 -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES",
component: {
name: "a-auto-complete",
vModel: "value",
options: [
{ value: "", label: "兼容 Windows Server 最新" },
{ value: "-macalg SHA1 -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES", label: "兼容 Windows Server 2016" },
{ value: "-nomac -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES", label: "兼容 Windows Server 2008" },
],
},
required: false,
order: 100,
helper: "兼容Windows Server各个版本",
})
pfxArgs = "-macalg SHA1 -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES";
@TaskInput({
title: "更新天数",
value: 35,
@@ -111,14 +49,6 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
// })
csrInfo!: string;
userContext!: IContext;
lastStatus!: Step;
@TaskOutput({
title: "域名证书",
})
cert?: CertInfo;
async onInstance() {
this.userContext = this.ctx.userContext;
this.lastStatus = this.ctx.lastStatus as Step;
@@ -139,7 +69,7 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
if (cert != null) {
await this.output(cert, true);
await emitCertApplySuccess(this.ctx.emitter, cert);
await this.emitCertApplySuccess();
//清空后续任务的状态,让后续任务能够重新执行
this.clearLastStatus();
@@ -151,89 +81,6 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
}
}
async output(certReader: CertReader, isNew: boolean) {
const cert: CertInfo = certReader.toCertInfo();
this.cert = cert;
this._result.pipelineVars.certExpiresTime = dayjs(certReader.detail.notAfter).valueOf();
if (!this._result.pipelinePrivateVars) {
this._result.pipelinePrivateVars = {};
}
this._result.pipelinePrivateVars.cert = cert;
if (isNew) {
try {
const converter = new CertConverter({ logger: this.logger });
const res = await converter.convert({
cert,
pfxPassword: this.pfxPassword,
pfxArgs: this.pfxArgs,
});
if (cert.pfx == null && res.pfx) {
cert.pfx = res.pfx;
}
if (cert.der == null && res.der) {
cert.der = res.der;
}
if (cert.jks == null && res.jks) {
cert.jks = res.jks;
}
this.logger.info("转换证书格式成功");
} catch (e) {
this.logger.error("转换证书格式失败", e);
}
}
if (isNew) {
const zipFileName = certReader.buildCertFileName("zip", certReader.detail.notBefore);
await this.zipCert(cert, zipFileName);
} else {
this.extendsFiles();
}
}
async zipCert(cert: CertInfo, filename: string) {
const zip = new JSZip();
zip.file("证书.pem", cert.crt);
zip.file("私钥.pem", cert.key);
zip.file("中间证书.pem", cert.ic);
zip.file("cert.crt", cert.crt);
zip.file("cert.key", cert.key);
zip.file("intermediate.crt", cert.ic);
zip.file("origin.crt", cert.oc);
zip.file("one.pem", cert.one);
if (cert.pfx) {
zip.file("cert.pfx", Buffer.from(cert.pfx, "base64"));
}
if (cert.der) {
zip.file("cert.der", Buffer.from(cert.der, "base64"));
}
if (cert.jks) {
zip.file("cert.jks", Buffer.from(cert.jks, "base64"));
}
zip.file(
"说明.txt",
`证书文件说明
cert.crt证书文件包含证书链pem格式
cert.key私钥文件pem格式
intermediate.crt中间证书文件pem格式
origin.crt原始证书文件不含证书链pem格式
one.pem 证书和私钥简单合并成一个文件pem格式crt正文+key正文
cert.pfxpfx格式证书文件iis服务器使用
cert.derder格式证书文件
cert.jksjks格式证书文件java服务器使用
`
);
const content = await zip.generateAsync({ type: "nodebuffer" });
this.saveFile(filename, content);
this.logger.info(`已保存文件:${filename}`);
}
/**
* 是否更新证书
*/
@@ -279,22 +126,6 @@ cert.jksjks格式证书文件java服务器使用
return null;
}
formatCert(pem: string) {
pem = pem.replace(/\r/g, "");
pem = pem.replace(/\n\n/g, "\n");
pem = pem.replace(/\n$/g, "");
return pem;
}
formatCerts(cert: { crt: string; key: string; csr: string }) {
const newCert: CertInfo = {
crt: this.formatCert(cert.crt),
key: this.formatCert(cert.key),
csr: this.formatCert(cert.csr),
};
return newCert;
}
async readLastCert(): Promise<CertReader | undefined> {
const cert = this.lastStatus?.status?.output?.cert;
if (cert == null) {

View File

@@ -23,6 +23,7 @@ export class CertReader {
cert: CertInfo;
detail: CertificateInfo;
//毫秒时间戳
expires: number;
constructor(certInfo: CertInfo) {
this.cert = certInfo;
@@ -39,9 +40,13 @@ export class CertReader {
this.cert.one = this.cert.crt + "\n" + this.cert.key;
}
const { detail, expires } = this.getCrtDetail(this.cert.crt);
this.detail = detail;
this.expires = expires.getTime();
try {
const { detail, expires } = this.getCrtDetail(this.cert.crt);
this.detail = detail;
this.expires = expires.getTime();
} catch (e) {
throw new Error("证书解析失败:" + e.message);
}
}
getIc() {

View File

@@ -0,0 +1,158 @@
import { IsTaskPlugin, pluginGroups, RunStrategy, Step, TaskInput, TaskOutput } from "@certd/pipeline";
import type { CertInfo } from "../acme.js";
import { CertReader } from "../cert-reader.js";
import { CertApplyBaseConvertPlugin } from "../base-convert.js";
import dayjs from "dayjs";
export { CertReader };
export type { CertInfo };
@IsTaskPlugin({
name: "CertApplyUpload",
icon: "ph:certificate",
title: "证书手动上传",
group: pluginGroups.cert.key,
desc: "在证书仓库手动上传后触发部署证书",
default: {
strategy: {
runStrategy: RunStrategy.AlwaysRun,
},
},
shortcut: {
certUpdate: {
title: "更新证书",
icon: "ion:upload",
action: "onCertUpdate",
form: {
columns: {
crt: {
title: "证书",
type: "text",
form: {
component: {
name: "pem-input",
vModel: "modelValue",
textarea: {
rows: 4,
placeholder: "-----BEGIN CERTIFICATE-----\n...\n...\n-----END CERTIFICATE-----",
},
},
rules: [{ required: true, message: "此项必填" }],
col: { span: 24 },
},
},
key: {
title: "私钥",
type: "text",
form: {
component: {
name: "pem-input",
vModel: "modelValue",
textarea: {
rows: 4,
placeholder: "-----BEGIN PRIVATE KEY-----\n...\n...\n-----END PRIVATE KEY----- ",
},
},
rules: [{ required: true, message: "此项必填" }],
col: { span: 24 },
},
},
},
},
},
},
})
export class CertApplyUploadPlugin extends CertApplyBaseConvertPlugin {
@TaskInput({
title: "手动上传证书",
component: {
name: "cert-info-updater",
vModel: "modelValue",
},
helper: "手动上传证书",
order: -9999,
required: true,
mergeScript: `
return {
component:{
on:{
updated(scope){
scope.form.input.domains = scope.$event?.domains
}
}
}
}
`,
})
uploadCert!: CertInfo;
@TaskOutput({
title: "证书MD5",
})
certMd5?: string;
async onInstance() {
this.accessService = this.ctx.accessService;
this.logger = this.ctx.logger;
this.userContext = this.ctx.userContext;
this.lastStatus = this.ctx.lastStatus as Step;
}
async onInit(): Promise<void> {}
async getCertFromStore() {
const certReader = new CertReader(this.uploadCert);
if (!certReader.expires && certReader.expires < new Date().getTime()) {
throw new Error("证书已过期,停止部署,请重新上传证书");
}
return certReader;
}
async execute(): Promise<string | void> {
const certReader = await this.getCertFromStore();
const crtMd5 = this.ctx.utils.hash.md5(certReader.cert.crt);
const leftDays = dayjs(certReader.expires).diff(dayjs(), "day");
this.logger.info(`证书过期时间${dayjs(certReader.expires).format("YYYY-MM-DD HH:mm:ss")},剩余${leftDays}`);
if (!this.ctx.inputChanged) {
this.logger.info("输入参数无变化");
const lastCrtMd5 = this.lastStatus?.status?.output?.certMd5;
this.logger.info("证书MD5", crtMd5);
this.logger.info("上次证书MD5", lastCrtMd5);
if (lastCrtMd5 === crtMd5) {
this.logger.info("证书无变化,跳过");
//输出证书MD5
this.certMd5 = crtMd5;
await this.output(certReader, false);
return "skip";
}
this.logger.info("证书有变化,重新部署");
} else {
this.logger.info("输入参数有变化,重新部署");
}
this.clearLastStatus();
//输出证书MD5
this.certMd5 = crtMd5;
await this.output(certReader, true);
//必须output之后执行
await this.emitCertApplySuccess();
return;
}
async onCertUpdate(data: any) {
const certReader = new CertReader(data);
return {
input: {
uploadCert: {
crt: data.crt,
key: data.key,
},
domains: certReader.getAllDomains(),
},
};
}
}
new CertApplyUploadPlugin();

View File

@@ -203,8 +203,7 @@ HTTP文件验证不支持泛域名需要配置网站文件上传`,
},
maybeNeed: true,
required: false,
helper:
"google服务账号授权与EAB授权选填其中一个[服务账号授权获取方法](https://certd.docmirror.cn/guide/use/google/)\n服务账号授权需要配置代理或者服务器本身在海外",
helper: "google服务账号授权与EAB授权选填其中一个[服务账号授权获取方法](https://certd.docmirror.cn/guide/use/google/)\n服务账号授权需要配置代理或者服务器本身在海外",
mergeScript: `
return {
show: ctx.compute(({form})=>{
@@ -268,6 +267,17 @@ HTTP文件验证不支持泛域名需要配置网站文件上传`,
})
skipLocalVerify = false;
@TaskInput({
title: "检查解析重试次数",
value: 35,
component: {
name: "a-input-number",
vModel: "value",
},
helper: "检查域名验证解析记录重试次数,如果你的域名服务商解析生效速度慢,可以适当增加此值",
})
maxCheckRetryCount = 35;
acme!: AcmeService;
eab!: EabAccess;
@@ -314,6 +324,7 @@ HTTP文件验证不支持泛域名需要配置网站文件上传`,
reverseProxy: this.reverseProxy,
privateKeyType: this.privateKeyType,
signal: this.ctx.signal,
maxCheckRetryCount: this.maxCheckRetryCount,
// cnameProxyService: this.ctx.cnameProxyService,
// dnsProviderCreator: this.createDnsProvider.bind(this),
});

View File

@@ -1,2 +1,6 @@
export { EVENT_CERT_APPLY_SUCCESS } from "./cert-plugin/base-convert.js";
export * from "./cert-plugin/index.js";
export * from "./cert-plugin/lego/index.js";
export * from "./cert-plugin/custom/index.js";
export const CertApplyPluginNames = ["CertApply", "CertApplyLego", "CertApplyUpload"];

View File

@@ -17,7 +17,6 @@
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-function": "off",
// "no-unused-expressions": "off",
"max-len": [0, 160, 2, { "ignoreUrls": true }]
"@typescript-eslint/no-unused-vars": "off"
}
}

View File

@@ -1,3 +1,7 @@
{
"printWidth": 160
"printWidth": 220,
"bracketSpacing": true,
"singleQuote": false,
"trailingComma": "es5",
"arrowParens": "avoid"
}

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.31.4](https://github.com/certd/certd/compare/v1.31.3...v1.31.4) (2025-03-21)
### Performance Improvements
* 流水线增加上传证书快捷方式 ([425bba6](https://github.com/certd/certd/commit/425bba67c539b734e2a85a83a4f9ecc9b2434fb4))
## [1.31.3](https://github.com/certd/certd/compare/v1.31.2...v1.31.3) (2025-03-13)
### Performance Improvements

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-lib",
"private": false,
"version": "1.31.3",
"version": "1.31.4",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -16,8 +16,8 @@
},
"dependencies": {
"@alicloud/pop-core": "^1.7.10",
"@certd/basic": "^1.31.3",
"@certd/pipeline": "^1.31.3",
"@certd/basic": "^1.31.4",
"@certd/pipeline": "^1.31.4",
"@kubernetes/client-node": "0.21.0",
"ali-oss": "^6.21.0",
"basic-ftp": "^5.0.5",
@@ -48,5 +48,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "98445afd3ec9e89c5a1b2c2b89dd9ea5e725237a"
"gitHead": "cfbbac9796477f830e1f57f77777f6554da9e31d"
}

View File

@@ -61,7 +61,17 @@ export class AsyncSsh2Client {
this.conn = conn;
resolve(this.conn);
})
.connect(this.connConf);
.connect({
...this.connConf,
algorithms: {
kex: [
"ecdh-sha2-nistp256",
"diffie-hellman-group1-sha1",
"diffie-hellman-group14-sha1", // 示例:添加服务器支持的旧算法
"diffie-hellman-group-exchange-sha256",
],
},
});
} catch (e) {
reject(e);
}

View File

@@ -3,34 +3,34 @@ module.exports = {
env: {
browser: true,
node: true,
es6: true
es6: true,
},
parser: "vue-eslint-parser",
parser: 'vue-eslint-parser',
parserOptions: {
parser: "@typescript-eslint/parser",
parser: '@typescript-eslint/parser',
ecmaVersion: 2020,
sourceType: "module",
jsxPragma: "React",
sourceType: 'module',
jsxPragma: 'React',
ecmaFeatures: {
jsx: true,
tsx: true
}
tsx: true,
},
},
extends: ["plugin:vue/vue3-recommended", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended", "prettier"],
extends: ['plugin:vue/vue3-recommended', 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', 'prettier'],
rules: {
//"max-len": [0, 200, 2, { ignoreUrls: true }],
"@typescript-eslint/no-unused-vars": "off",
"no-unused-vars": "off",
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/explicit-module-boundary-types": "off"
'@typescript-eslint/no-unused-vars': 'off',
'no-unused-vars': 'off',
'@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
// "@typescript-eslint/no-unused-vars": [
// "error",
// {
@@ -69,5 +69,5 @@ module.exports = {
// math: "always",
// },
// ],
}
},
};

View File

@@ -1,5 +1,7 @@
{
"trailingComma": "none",
"printWidth": 220
"printWidth": 220,
"bracketSpacing": true,
"singleQuote": false,
"trailingComma": "es5",
"arrowParens": "avoid"
}

View File

@@ -3,6 +3,25 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.31.4](https://github.com/certd/certd/compare/v1.31.3...v1.31.4) (2025-03-21)
### Bug Fixes
* 修复站点监控通知通过webhook发送失败的bug ([9be1ecc](https://github.com/certd/certd/commit/9be1ecc8aab3ea23dd0dc2dab3688f4edb90ef2c))
### Performance Improvements
* 宝塔支持doker站点证书部署 ([589a373](https://github.com/certd/certd/commit/589a373142ef7f50d64d3aa767a90b1f4b64da93))
* 保存调整后的列宽 ([873f2b6](https://github.com/certd/certd/commit/873f2b618b9d7320045baf69d6da83afe48a780f))
* 创建证书流水线时,支持更多参数展开 ([36aa7f8](https://github.com/certd/certd/commit/36aa7f82b078a053a102331b3c6f132fb9d492f9))
* 流水线页面可以鼠标按住左右拖动 ([d85a02f](https://github.com/certd/certd/commit/d85a02feeb3183c5abd6c1ea790d5923a32d7271))
* 流水线增加上传证书快捷方式 ([425bba6](https://github.com/certd/certd/commit/425bba67c539b734e2a85a83a4f9ecc9b2434fb4))
* 手动上传证书部署流水线 ([fbb66f3](https://github.com/certd/certd/commit/fbb66f3c4389489aa8a43b194d82bc8cf391607b))
* 优化选择任务时手机版展示效果 ([d01004d](https://github.com/certd/certd/commit/d01004d53071a75ac91ee21cc96bde9369f77ff3))
* 站点证书监控支持模糊查询 ([0069c0e](https://github.com/certd/certd/commit/0069c0e3992946a8dd6410f299d4fc974ef0e76b))
* 支持飞书通知 ([b82e1dc](https://github.com/certd/certd/commit/b82e1dcd6217b09a7d7e21cd648bb31de320cadf))
* 支持手动上传证书并部署 ([a9fffa5](https://github.com/certd/certd/commit/a9fffa5180c83da27b35886aa2e858a92a2c5f94))
## [1.31.3](https://github.com/certd/certd/compare/v1.31.2...v1.31.3) (2025-03-13)
### Performance Improvements

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/ui-client",
"version": "1.31.3",
"version": "1.31.4",
"private": true,
"scripts": {
"dev": "vite --open",
@@ -67,6 +67,7 @@
"lucide-vue-next": "^0.477.0",
"mitt": "^3.0.1",
"nanoid": "^4.0.0",
"node-forge": "^1.3.1",
"nprogress": "^0.2.0",
"object-assign": "^4.1.1",
"pinia": "2.1.7",
@@ -95,8 +96,8 @@
"zod-defaults": "^0.1.3"
},
"devDependencies": {
"@certd/lib-iframe": "^1.31.3",
"@certd/pipeline": "^1.31.3",
"@certd/lib-iframe": "^1.31.4",
"@certd/pipeline": "^1.31.4",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12",

View File

@@ -1,7 +1,7 @@
<template>
<AConfigProvider :locale="locale" :theme="tokenTheme">
<contextHolder />
<fs-form-provider>
<contextHolder />
<router-view />
</fs-form-provider>
</AConfigProvider>
@@ -21,7 +21,7 @@ import AConfigProvider from "ant-design-vue/es/config-provider";
import { Modal } from "ant-design-vue";
defineOptions({
name: "App"
name: "App",
});
const [modal, contextHolder] = Modal.useModal();
provide("modal", modal);
@@ -59,7 +59,7 @@ const tokenTheme = computed(() => {
return {
algorithm,
token: tokens
token: tokens,
};
});
//其他初始化

View File

@@ -0,0 +1,27 @@
<template>
<div class="file-input">
<a-button :type="type" @click="onClick">{{ text }}</a-button> {{ fileName }}
<div class="hidden">
<input ref="fileInputRef" type="file" @change="onFileChange" />
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, defineEmits, defineProps } from "vue";
const fileInputRef = ref<HTMLInputElement | null>(null);
const props = defineProps<{
text: string;
type: string;
}>();
const fileName = ref("");
const emit = defineEmits(["change"]);
function onClick() {
fileInputRef.value.click();
}
function onFileChange(e: any) {
fileName.value = e.target.files[0].name;
emit("change", e);
}
</script>

View File

@@ -10,10 +10,14 @@ import Plugins from "./plugins/index";
import LoadingButton from "./loading-button.vue";
import IconSelect from "./icon-select.vue";
import ExpiresTimeText from "./expires-time-text.vue";
import FileInput from "./file-input.vue";
import PemInput from "./pem-input.vue";
export default {
install(app: any) {
app.component("PiContainer", PiContainer);
app.component("TextEditable", TextEditable);
app.component("FileInput", FileInput);
app.component("PemInput", PemInput);
app.component("CronLight", CronLight);
app.component("CronEditor", CronEditor);
@@ -29,5 +33,5 @@ export default {
app.component("ExpiresTimeText", ExpiresTimeText);
app.use(vip);
app.use(Plugins);
}
},
};

View File

@@ -0,0 +1,60 @@
<template>
<div class="pem-input">
<FileInput v-bind="fileInput" class="mb-5" type="primary" text="选择文件" @change="onChange" />
<a-textarea v-bind="textarea" v-model:value="textRef"></a-textarea>
</div>
</template>
<script setup lang="ts">
import { notification } from "ant-design-vue";
import { ref, watch, defineEmits } from "vue";
import FileInput from "/@/components/file-input.vue";
const props = defineProps<{
modelValue?: string;
textarea?: any;
fileInput?: any;
}>();
const emit = defineEmits(["update:modelValue"]);
const textRef = ref();
function emitValue(value: string) {
emit("update:modelValue", value);
}
function onChange(e: any) {
const file = e.target.files[0];
const size = file.size;
if (size > 100 * 1024) {
notification.error({
message: "文件超过100k请选择正确的证书文件",
});
return;
}
const fileReader = new FileReader();
fileReader.onload = function (e: any) {
const value = e.target.result;
emitValue(value);
};
fileReader.readAsText(file); // 以文本形式读取文件
}
watch(
() => props.modelValue,
value => {
textRef.value = value;
},
{
immediate: true,
}
);
</script>
<style lang="less">
.pem-input {
display: flex;
flex-direction: column;
gap: 5px;
}
</style>

View File

@@ -60,6 +60,9 @@ const doTest = async () => {
}
);
message.value = "测试请求成功";
if (res) {
message.value += `,返回:${JSON.stringify(res)}`;
}
} finally {
loading.value = false;
}

View File

@@ -6,6 +6,7 @@ import DnsProviderSelector from "/@/components/plugins/cert/dns-provider-selecto
import DomainsVerifyPlanEditor from "/@/components/plugins/cert/domains-verify-plan-editor/index.vue";
import AccessSelector from "/@/views/certd/access/access-selector/index.vue";
import InputPassword from "./common/input-password.vue";
import CertInfoUpdater from "/@/views/certd/pipeline/cert-upload/index.vue";
import ApiTest from "./common/api-test.vue";
export * from "./cert/index.js";
export default {
@@ -14,11 +15,13 @@ export default {
app.component("DnsProviderSelector", DnsProviderSelector);
app.component("DomainsVerifyPlanEditor", DomainsVerifyPlanEditor);
app.component("AccessSelector", AccessSelector);
app.component("CertInfoUpdater", CertInfoUpdater);
app.component("ApiTest", ApiTest);
app.component("SynologyDeviceIdGetter", SynologyIdDeviceGetter);
app.component("RemoteSelect", RemoteSelect);
app.component("CertDomainsGetter", CertDomainsGetter);
app.component("InputPassword", InputPassword);
}
},
};

View File

@@ -24,9 +24,9 @@ export async function doRequest(req: RequestHandleReq, opts: any = {}) {
typeName,
action,
data,
input
input,
},
...opts
...opts,
});
return res;
}

View File

@@ -6,16 +6,48 @@ import { FsExtendsCopyable, FsExtendsEditor, FsExtendsJson, FsExtendsTime, FsExt
import "@fast-crud/fast-extends/dist/style.css";
import UiAntdv from "@fast-crud/ui-antdv4";
import "@fast-crud/ui-antdv4/dist/style.css";
import { merge } from "lodash-es";
import { debounce, merge } from "lodash-es";
import { useCrudPermission } from "../permission";
import { App } from "vue";
import { notification } from "ant-design-vue";
import { usePreferences } from "/@/vben/preferences";
import { LocalStorage } from "/@/utils/util.storage";
class ColumnSizeSaver {
save: (key: string, size: number) => void;
constructor() {
this.save = debounce((key: string, size: number) => {
const saveKey = this.getKey();
let data = LocalStorage.get(saveKey);
if (!data) {
data = {};
}
data[key] = size;
LocalStorage.set(saveKey, data);
});
}
getKey() {
const loc = window.location;
const currentUrl = `${loc.pathname}${loc.search}${loc.hash}`;
return `columnSize-${currentUrl}`;
}
get(key: string) {
const saveKey = this.getKey();
const row = LocalStorage.get(saveKey);
return row?.[key];
}
clear() {
const saveKey = this.getKey();
LocalStorage.remove(saveKey);
}
}
const columnSizeSaver = new ColumnSizeSaver();
function install(app: App, options: any = {}) {
app.use(UiAntdv);
//设置日志级别
setLogger({ level: "info" });
app.use(FastCrud, {
i18n: options.i18n,
async dictRequest({ url }: any) {
@@ -39,20 +71,21 @@ function install(app: App, options: any = {}) {
mobile: {
enabled: true,
props: {
isMobile: isMobile
}
}
}
isMobile: isMobile,
},
},
},
},
table: {
scroll: {
x: 960
x: 960,
},
size: "small",
pagination: false,
onResizeColumn: (w: number | string, col: any) => {
onResizeColumn: (w: number, col: any) => {
if (crudBinding.value?.table?.columnsMap && crudBinding.value?.table?.columnsMap[col.key]) {
crudBinding.value.table.columnsMap[col.key].width = w;
columnSizeSaver.save(col.key, w);
}
},
conditionalRender: {
@@ -70,13 +103,18 @@ function install(app: App, options: any = {}) {
},
render() {
return "-";
}
}
},
},
},
toolbar: {
export: {
fileType: "excel"
}
fileType: "excel",
},
columnsFilter: {
async onReset() {
columnSizeSaver.clear();
},
},
},
rowHandle: {
fixed: "right",
@@ -84,13 +122,15 @@ function install(app: App, options: any = {}) {
view: { type: "link", text: null, icon: "ion:eye-outline" },
copy: { show: true, type: "link", text: null, icon: "ion:copy-outline" },
edit: { type: "link", text: null, icon: "ion:create-outline" },
remove: { type: "link", style: { color: "red" }, text: null, icon: "ion:trash-outline" }
remove: { type: "link", style: { color: "red" }, text: null, icon: "ion:trash-outline" },
},
dropdown: {
more: {
type: "link"
}
}
type: "link",
},
},
resizable: true,
width: 200,
},
request: {
transformQuery: ({ page, form, sort }: PageQuery): UserPageQuery => {
@@ -103,10 +143,10 @@ function install(app: App, options: any = {}) {
return {
page: {
limit,
offset
offset,
},
query: form,
sort
sort,
};
},
transformRes: ({ res }: TransformResProps): PageRes => {
@@ -116,16 +156,16 @@ function install(app: App, options: any = {}) {
currentPage++;
}
return { currentPage, pageSize, records: res.records, total: res.total, ...res };
}
},
},
search: {
formItem: {
wrapperCol: {
style: {
width: "50%"
}
}
}
width: "50%",
},
},
},
},
form: {
display: "flex",
@@ -133,8 +173,8 @@ function install(app: App, options: any = {}) {
//固定label宽度
span: null,
style: {
width: "145px"
}
width: "145px",
},
},
async afterSubmit({ mode }) {
if (mode === "add") {
@@ -144,13 +184,13 @@ function install(app: App, options: any = {}) {
}
},
wrapperCol: {
span: null
span: null,
},
wrapper: {
saveRemind: true
saveRemind: true,
// inner: true,
// innerContainerSelector: "main.fs-framework-content"
}
},
},
columns: {
//最后一列空白,用于自动伸缩列宽
@@ -158,23 +198,23 @@ function install(app: App, options: any = {}) {
title: "",
type: "text",
form: {
show: false
show: false,
},
column: {
order: 99999,
width: -1,
columnSetShow: false,
resizable: false
}
}
}
resizable: false,
},
},
},
};
// 从 useCrud({permission}) 里获取permission参数去设置各个按钮的权限
const permission = props.context?.permission || null;
const crudPermission = useCrudPermission({ permission });
return crudPermission.merge(opts);
}
},
});
// fast-extends里面的扩展组件均为异步组件只有在使用时才会被加载并不会影响首页加载速度
@@ -202,19 +242,19 @@ function install(app: App, options: any = {}) {
url: action,
method: "post",
headers: {
"Content-Type": "multipart/form-data"
"Content-Type": "multipart/form-data",
},
timeout: 60000,
data,
onUploadProgress: (p: any) => {
onProgress({ percent: Math.round((p.loaded / p.total) * 100) });
}
},
});
},
successHandle(res: any) {
return res;
}
}
},
},
});
//安装editor
@@ -222,10 +262,10 @@ function install(app: App, options: any = {}) {
//编辑器的公共配置
wangEditor: {
editorConfig: {
MENU_CONF: {}
MENU_CONF: {},
},
toolbarConfig: {}
}
toolbarConfig: {},
},
});
app.use(FsExtendsJson);
app.use(FsExtendsTime);
@@ -250,8 +290,8 @@ function install(app: App, options: any = {}) {
column: { component: { name: "fs-date-format", format: "YYYY-MM-DD" } },
valueBuilder(context: any) {
console.log("time2,valueBuilder", context);
}
}
},
},
});
// 此处演示自定义字段合并插件
@@ -266,32 +306,14 @@ function install(app: App, options: any = {}) {
// 合并column配置
merge(columnProps, {
form: { show: false },
viewForm: { show: true }
viewForm: { show: true },
});
}
return columnProps;
}
},
});
//默认宽度,支持自动拖动调整列宽
registerMergeColumnPlugin({
name: "resize-column-plugin",
order: 2,
handle: (columnProps: ColumnCompositionProps) => {
if (!columnProps.column) {
columnProps.column = {};
}
if (columnProps.column.resizable == null) {
columnProps.column.resizable = true;
if (!columnProps.column.width) {
columnProps.column.width = 200;
}
}
return columnProps;
}
});
registerMergeColumnPlugin({
name: "resize-column-plugin",
order: 2,
@@ -300,16 +322,19 @@ function install(app: App, options: any = {}) {
columnProps.column = {};
}
columnProps.column.resizable = true;
if (columnProps.column.width == null) {
const savedColumnWidth = columnSizeSaver.get(columnProps.key as string);
if (savedColumnWidth) {
columnProps.column.width = savedColumnWidth;
} else if (columnProps.column.width == null) {
columnProps.column.width = 200;
} else if (typeof columnProps.column?.width === "string" && columnProps.column.width.indexOf("px") > -1) {
columnProps.column.width = parseInt(columnProps.column.width.replace("px", ""));
}
return columnProps;
}
},
});
}
export default {
install
install,
};

View File

@@ -67,13 +67,24 @@ export const certdResources = [
title: "设置",
name: "MineSetting",
path: "/certd/setting",
redirect: "/certd/cname/record",
redirect: "/certd/access",
meta: {
icon: "ion:settings-outline",
auth: true,
cache: true
},
children: [
{
title: "授权管理",
name: "AccessManager",
path: "/certd/access",
component: "/certd/access/index.vue",
meta: {
icon: "ion:disc-outline",
auth: true,
cache: true
}
},
{
title: "CNAME记录管理",
name: "CnameRecord",
@@ -94,17 +105,7 @@ export const certdResources = [
auth: true
}
},
{
title: "授权管理",
name: "AccessManager",
path: "/certd/access",
component: "/certd/access/index.vue",
meta: {
icon: "ion:disc-outline",
auth: true,
cache: true
}
},
{
title: "OpenKey",
name: "OpenKey",

View File

@@ -88,4 +88,9 @@ body a{
&:hover{
color: #40a9ff;
}
}
span.fs-icon-svg{
display: flex;
align-items: center;
}

View File

@@ -4,6 +4,10 @@ export function createAccessApi(from = "user") {
const apiPrefix = from === "sys" ? "/sys/access" : "/pi/access";
return {
async GetList(query: any) {
if (query?.query) {
delete query.query.access;
}
return await request({
url: apiPrefix + "/page",
method: "post",

View File

@@ -77,7 +77,7 @@ export function getCommonColumnDefine(crudExpose: any, typeRef: any, api: any) {
type: "dict-select",
dict: AccessTypeDictRef,
search: {
show: false
show: true
},
column: {
width: 200,

View File

@@ -7,7 +7,7 @@ export const certInfoApi = {
return await request({
url: apiPrefix + "/page",
method: "post",
data: query
data: query,
});
},
@@ -15,7 +15,7 @@ export const certInfoApi = {
return await request({
url: apiPrefix + "/add",
method: "post",
data: obj
data: obj,
});
},
@@ -23,7 +23,7 @@ export const certInfoApi = {
return await request({
url: apiPrefix + "/update",
method: "post",
data: obj
data: obj,
});
},
@@ -31,7 +31,7 @@ export const certInfoApi = {
return await request({
url: apiPrefix + "/delete",
method: "post",
params: { id }
params: { id },
});
},
@@ -39,27 +39,35 @@ export const certInfoApi = {
return await request({
url: apiPrefix + "/info",
method: "post",
params: { id }
params: { id },
});
},
async ListAll() {
return await request({
url: apiPrefix + "/all",
method: "post"
method: "post",
});
},
async Upload(body: { id?: number; cert: { crt: string; key: string } }) {
return await request({
url: apiPrefix + "/upload",
method: "post",
data: body
data: body,
});
},
async GetCert(id: number): Promise<CertInfo> {
return await request({
url: apiPrefix + "/getCert",
method: "post",
params: { id: id }
params: { id: id },
});
}
},
async GetOptionsByIds(ids: number[]): Promise<any[]> {
return await request({
url: apiPrefix + "/getOptionsByIds",
method: "post",
data: { ids },
});
},
};

View File

@@ -1,17 +1,15 @@
// @ts-ignore
import { useI18n } from "vue-i18n";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, EditReq, useFormWrapper, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, useFormWrapper, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { certInfoApi } from "./api";
import dayjs from "dayjs";
import { useUserStore } from "/@/store/modules/user";
import { useRouter } from "vue-router";
import { useModal } from "/@/use/use-modal";
import * as api from "/@/views/certd/pipeline/api";
import { notification } from "ant-design-vue";
import CertView from "/@/views/certd/pipeline/cert-view.vue";
import { useCertUpload } from "/@/views/certd/pipeline/cert-upload/use";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { t } = useI18n();
const api = certInfoApi;
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
@@ -50,32 +48,33 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
width: 800,
content: () => {
return <CertView cert={cert}></CertView>;
}
},
});
};
const { openUploadCreateDialog, openUpdateCertDialog } = useCertUpload();
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest
delRequest,
},
form: {
labelCol: {
//固定label宽度
span: null,
style: {
width: "100px"
}
width: "100px",
},
},
col: {
span: 22
span: 22,
},
wrapper: {
width: 600
}
width: 600,
},
},
actionbar: {
show: true,
@@ -85,59 +84,14 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
type: "primary",
show: false,
async click() {
function createCrudOptions() {
return {
crudOptions: {
request: {
addRequest: async (form: any) => {
return await api.Upload(form);
},
editRequest: async (form: any) => {
return await api.Upload(form);
}
},
columns: {
id: {
title: "ID",
type: "number",
form: {
show: false
}
},
"cert.crt": {
title: "证书",
type: "textarea",
form: {
component: {
rows: 4
},
rules: [{ required: true, message: "此项必填" }]
}
},
"cert.key": {
title: "私钥",
type: "textarea",
form: {
component: {
rows: 4
},
rules: [{ required: true, message: "此项必填" }]
}
}
},
form: {
wrapper: {
title: "上传自定义证书"
}
}
}
};
}
const { crudOptions } = createCrudOptions();
const wrapperRef = await openCrudFormDialog({ crudOptions });
}
}
}
await openUploadCreateDialog();
},
},
},
},
tabs: {
name: "fromType",
show: true,
},
rowHandle: {
width: 100,
@@ -151,12 +105,28 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
icon: "ph:certificate",
async click({ row }) {
await viewCert(row);
}
},
},
copy: { show: false },
edit: { show: false },
remove: { show: false }
}
remove: {
order: 10,
show: false,
},
download: {
order: 9,
title: "下载证书",
type: "link",
icon: "ant-design:download-outlined",
async click({ row }) {
if (!row.certFile) {
notification.error({ message: "证书还未生成,请先运行流水线" });
return;
}
window.open("/api/monitor/cert/download?id=" + row.id);
},
},
},
},
columns: {
id: {
@@ -164,73 +134,85 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
key: "id",
type: "number",
search: {
show: false
show: false,
},
column: {
width: 100,
editable: {
disabled: true
}
disabled: true,
},
},
form: {
show: false
}
show: false,
},
},
// domain: {
// title: "主域名",
// search: {
// show: true
// },
// type: "text",
// form: {
// show: false
// },
// column: {
// width: 180,
// sorter: true,
// component: {
// name: "fs-values-format"
// }
// }
// },
domains: {
title: "全部域名",
fromType: {
title: "来源",
search: {
show: true
show: true,
},
type: "dict-select",
dict: dict({
data: [
{ label: "流水线", value: "pipeline" },
{ label: "手动上传", value: "upload" },
],
}),
form: {
show: false,
},
column: {
width: 100,
sorter: true,
component: {
color: "auto",
},
conditionalRender: false,
},
valueBuilder({ value, row, key }) {
if (!value) {
row[key] = "pipeline";
}
},
},
domains: {
title: "域名",
search: {
show: true,
},
type: "text",
form: {
rules: [{ required: true, message: "请输入域名" }]
rules: [{ required: true, message: "请输入域名" }],
},
column: {
width: 450,
sorter: true,
component: {
name: "fs-values-format",
color: "auto"
}
}
color: "auto",
},
},
},
domainCount: {
title: "域名数量",
type: "number",
form: {
show: false
show: false,
},
column: {
width: 120,
sorter: true,
show: false
}
show: false,
},
},
expiresLeft: {
title: "有效天数",
search: {
show: false
show: false,
},
type: "date",
form: {
show: false
show: false,
},
column: {
sorter: true,
@@ -245,54 +227,54 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const color = leftDays < 20 ? "red" : "#389e0d";
const percent = (leftDays / 90) * 100;
return <a-progress title={expireDate + "过期"} percent={percent} strokeColor={color} format={(percent: number) => `${leftDays}`} />;
}
}
},
},
},
expiresTime: {
title: "过期时间",
search: {
show: false
show: false,
},
type: "datetime",
form: {
show: false
show: false,
},
column: {
sorter: true
}
sorter: true,
},
},
certProvider: {
title: "证书颁发机构",
search: {
show: false
show: false,
},
type: "text",
form: {
show: false
show: false,
},
column: {
width: 200
}
width: 200,
},
},
applyTime: {
title: "申请时间",
search: {
show: false
show: false,
},
type: "datetime",
form: {
show: false
show: false,
},
column: {
sorter: true
}
sorter: true,
},
},
"pipeline.title": {
title: "关联流水线",
search: { show: false },
type: "link",
form: {
show: false
show: false,
},
column: {
width: 350,
@@ -301,12 +283,12 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
on: {
onClick({ row }) {
router.push({ path: "/certd/pipeline/detail", query: { id: row.pipelineId, editMode: "false" } });
}
}
}
}
}
}
}
},
},
},
},
},
},
},
};
}

View File

@@ -3,7 +3,7 @@
<template #header>
<div class="title">
证书仓库
<span class="sub">从流水线生成的证书后续将支持手动上传证书并部署</span>
<span class="sub">从流水线生成的证书</span>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
@@ -11,12 +11,12 @@
</template>
<script lang="ts" setup>
import { defineComponent, onActivated, onMounted } from "vue";
import { onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { createApi } from "./api";
defineOptions({
name: "CertStore"
name: "CertStore",
});
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: {} });

View File

@@ -34,28 +34,35 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const settingsStore = useSettingStore();
const checkStatusDict = dict({
data: [
{ label: "成功", value: "ok", color: "green" },
{ label: "检查中", value: "checking", color: "blue" },
{ label: "异常", value: "error", color: "red" },
],
});
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest
delRequest,
},
form: {
labelCol: {
//固定label宽度
span: null,
style: {
width: "100px"
}
width: "100px",
},
},
col: {
span: 22
span: 22,
},
wrapper: {
width: 600
}
width: 600,
},
},
actionbar: {
buttons: {
@@ -65,7 +72,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
//非plus
if (crudBinding.value.data.length >= 1) {
notification.error({
message: "基础版只能添加一个监控站点,请赞助升级专业版"
message: "基础版只能添加一个监控站点,请赞助升级专业版",
});
mitter.emit("openVipModal");
return;
@@ -79,15 +86,15 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const max = suiteDetail.monitorCount.max;
if (max != -1 && max <= suiteDetail.monitorCount.used) {
notification.error({
message: `对不起,您最多只能创建条${max}监控记录,请购买或升级套餐`
message: `对不起,您最多只能创建条${max}监控记录,请购买或升级套餐`,
});
return;
}
}
await crudExpose.openAdd({});
}
}
}
},
},
},
},
rowHandle: {
fixed: "right",
@@ -98,18 +105,18 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
type: "link",
text: null,
tooltip: {
title: "立即检查"
title: "立即检查",
},
icon: "ion:play-sharp",
click: async ({ row }) => {
await api.DoCheck(row.id);
await crudExpose.doRefresh();
notification.success({
message: "检查完成"
message: "检查完成",
});
}
}
}
},
},
},
},
columns: {
id: {
@@ -117,77 +124,67 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
key: "id",
type: "number",
search: {
show: false
show: false,
},
column: {
width: 80,
align: "center"
align: "center",
},
form: {
show: false
}
show: false,
},
},
name: {
title: "站点名称",
search: {
show: true
show: true,
},
type: "text",
form: {
rules: [{ required: true, message: "请输入站点名称" }]
rules: [{ required: true, message: "请输入站点名称" }],
},
column: {
width: 160
}
width: 160,
},
},
domain: {
title: "网站域名",
search: {
show: true
show: true,
},
type: "text",
form: {
rules: [
{ required: true, message: "请输入域名" },
//@ts-ignore
{ type: "domains", message: "请输入正确的域名" }
]
{ type: "domains", message: "请输入正确的域名" },
],
},
column: {
width: 200,
width: 230,
sorter: true,
cellRender({ value }) {
cellRender({ value, row }) {
const url = `https://${value}:${row.httpsPort}`;
return (
<a-tooltip title={value} placement="left">
<fs-copyable modelValue={value}></fs-copyable>
<fs-copyable modelValue={value}>
<a target="_blank" href={url}>
{value}:{row.httpsPort}
</a>
</fs-copyable>
</a-tooltip>
);
}
}
},
httpsPort: {
title: "HTTPS端口",
search: {
show: false
},
},
type: "number",
form: {
value: 443,
rules: [{ required: true, message: "请输入端口" }]
},
column: {
align: "center",
width: 100
}
},
certDomains: {
title: "证书域名",
search: {
show: false
show: true,
},
type: "text",
form: {
show: false
show: false,
},
column: {
width: 200,
@@ -199,55 +196,56 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
{value}
</a-tooltip>
);
}
}
},
},
},
certProvider: {
title: "证书颁发者",
title: "颁发机构",
search: {
show: false
show: false,
},
type: "text",
form: {
show: false
show: false,
},
column: {
width: 200,
sorter: true,
cellRender({ value }) {
return <a-tooltip title={value}>{value}</a-tooltip>;
}
}
},
},
},
certStatus: {
title: "证书状态",
search: {
show: true
show: true,
},
type: "dict-select",
dict: dict({
data: [
{ label: "正常", value: "ok", color: "green" },
{ label: "过期", value: "expired", color: "red" }
]
{ label: "过期", value: "expired", color: "red" },
],
}),
form: {
show: false
show: false,
},
column: {
width: 100,
sorter: true,
show: false
}
show: true,
align: "center",
},
},
certExpiresTime: {
title: "证书到期时间",
search: {
show: false
show: false,
},
type: "date",
form: {
show: false
show: false,
},
column: {
sorter: true,
@@ -260,109 +258,111 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const color = leftDays < 20 ? "red" : "#389e0d";
const percent = (leftDays / 90) * 100;
return <a-progress title={expireDate + "过期"} percent={percent} strokeColor={color} format={(percent: number) => `${leftDays}`} />;
}
}
},
},
},
lastCheckTime: {
title: "上次检查时间",
search: {
show: false
show: false,
},
type: "datetime",
form: {
show: false
show: false,
},
column: {
sorter: true,
width: 155
}
},
checkStatus: {
title: "检查状态",
search: {
show: false
width: 155,
},
type: "dict-select",
dict: dict({
data: [
{ label: "正常", value: "ok", color: "green" },
{ label: "检查中", value: "checking", color: "blue" },
{ label: "异常", value: "error", color: "red" }
]
}),
form: {
show: false
},
column: {
width: 100,
align: "center",
sorter: true
}
},
error: {
title: "错误信息",
search: {
show: false
},
type: "text",
form: {
show: false
},
column: {
width: 200,
sorter: true,
cellRender({ value }) {
return <a-tooltip title={value}>{value}</a-tooltip>;
}
}
},
pipelineId: {
title: "关联流水线id",
search: {
show: false
},
form: { show: false },
type: "number",
column: {
width: 200,
sorter: true,
show: false
}
},
certInfoId: {
title: "证书id",
search: {
show: false
},
type: "number",
form: { show: false },
column: {
width: 100,
sorter: true,
show: false
}
},
disabled: {
title: "禁用启用",
search: {
show: false
show: false,
},
type: "dict-switch",
dict: dict({
data: [
{ label: "启用", value: false, color: "green" },
{ label: "禁用", value: true, color: "red" }
]
{ label: "禁用", value: true, color: "red" },
],
}),
form: {
value: false
value: false,
},
column: {
width: 90,
sorter: true
}
}
}
}
width: 100,
sorter: true,
align: "center",
},
},
checkStatus: {
title: "检查状态",
search: {
show: false,
},
type: "dict-select",
dict: checkStatusDict,
form: {
show: false,
},
column: {
width: 100,
align: "center",
sorter: true,
cellRender({ value, row, key }) {
return (
<a-tooltip title={row.error}>
<fs-values-format v-model={value} dict={checkStatusDict}></fs-values-format>
</a-tooltip>
);
},
},
},
// error: {
// title: "错误信息",
// search: {
// show: false
// },
// type: "text",
// form: {
// show: false
// },
// column: {
// width: 200,
// sorter: true,
// cellRender({ value }) {
// return <a-tooltip title={value}>{value}</a-tooltip>;
// }
// }
// },
pipelineId: {
title: "关联流水线id",
search: {
show: false,
},
form: { show: false },
type: "number",
column: {
width: 200,
sorter: true,
show: false,
},
},
certInfoId: {
title: "证书id",
search: {
show: false,
},
type: "number",
form: { show: false },
column: {
width: 100,
sorter: true,
show: false,
},
},
},
},
};
}

View File

@@ -4,7 +4,7 @@
<div class="title flex items-center">
站点证书监控
<div class="sub flex-1">
<div>每天0点检查网站证书的过期时间并发出提醒;</div>
<div>每天0点检查网站证书的过期时间到期前10天时将发出提醒使用默认通知渠道;</div>
<div class="flex items-center">基础版限制1条专业版以上无限制当前<vip-button class="ml-5" mode="nav"></vip-button></div>
</div>
</div>

View File

@@ -1,16 +1,7 @@
<template>
<div class="notification-selector">
<div class="flex-o w-100">
<fs-dict-select
class="flex-1"
:value="modelValue"
:dict="optionsDictRef"
:disabled="disabled"
:render-label="renderLabel"
:slots="selectSlots"
:allow-clear="true"
@update:value="onChange"
/>
<fs-dict-select class="flex-1" :value="modelValue" :dict="optionsDictRef" :disabled="disabled" :render-label="renderLabel" :slots="selectSlots" :allow-clear="true" @update:value="onChange" />
<fs-table-select
ref="tableSelectRef"
class="flex-0"
@@ -18,12 +9,12 @@
:dict="optionsDictRef"
:create-crud-options="createCrudOptions"
:crud-options-override="{
search: { show: false },
search: { show: false, initialForm: { fromType: 'upload' } },
table: {
scroll: {
x: 540
}
}
x: 540,
},
},
}"
:show-current="false"
:show-select="false"
@@ -50,7 +41,7 @@ import createCrudOptions from "../crud";
import { notificationProvide } from "/@/views/certd/notification/common";
defineOptions({
name: "NotificationSelector"
name: "NotificationSelector",
});
const props = defineProps<{
@@ -89,12 +80,12 @@ const optionsDictRef = dict({
{
id: 0,
name: "使用默认通知",
icon: "ion:notifications"
icon: "ion:notifications",
},
...dict.data
...dict.data,
];
dict.setData(data);
}
},
});
const renderLabel = (option: any) => {
return <span>{option.name}</span>;
@@ -115,7 +106,7 @@ const selectSlots = ref({
// res.push(<a-space style="padding: 4px 8px" />);
// res.push(<fs-button class="w-100" type="text" icon="plus-outlined" text="新建通知渠道" onClick={openTableSelectDialog}></fs-button>);
return res;
}
},
});
const target: Ref<any> = ref({});
@@ -141,13 +132,13 @@ watch(
() => {
return props.modelValue;
},
async (value) => {
async value => {
await optionsDictRef.loadDict();
target.value = optionsDictRef.dataMap[value];
emit("selectedChange", target.value);
},
{
immediate: true
immediate: true,
}
);

View File

@@ -6,8 +6,8 @@ const apiPrefix = "/pi/plugin";
const defaultInputDefine = {
component: {
name: "a-input",
vModel: "modelValue"
}
vModel: "modelValue",
},
};
function initPlugins(plugins: any) {
@@ -35,7 +35,7 @@ export async function GetList(query: any) {
const plugins = await request({
url: apiPrefix + "/list",
method: "post",
params: query
params: query,
});
initPlugins(plugins);
return plugins;
@@ -45,7 +45,7 @@ export async function GetGroups(query: any) {
const groups = await request({
url: apiPrefix + "/groups",
method: "post",
params: query
params: query,
});
const plugins: any = [];
for (const groupKey in groups) {
@@ -60,8 +60,8 @@ export async function GetPluginDefine(type: string) {
url: apiPrefix + "/getDefineByType",
method: "post",
data: {
type
}
type,
},
});
initPlugins([define]);
return define;
@@ -71,6 +71,6 @@ export async function GetPluginConfig(req: { id?: number; name: string; type: st
return await request({
url: apiPrefix + "/config",
method: "post",
data: req
data: req,
});
}

View File

@@ -8,7 +8,7 @@ export async function GetList(query: any) {
return await request({
url: apiPrefix + "/page",
method: "post",
data: query
data: query,
});
}
@@ -16,7 +16,7 @@ export async function AddObj(obj: any) {
return await request({
url: apiPrefix + "/add",
method: "post",
data: obj
data: obj,
});
}
@@ -24,7 +24,7 @@ export async function UpdateObj(obj: any) {
return await request({
url: apiPrefix + "/update",
method: "post",
data: obj
data: obj,
});
}
@@ -32,7 +32,7 @@ export async function DelObj(id: any) {
return await request({
url: apiPrefix + "/delete",
method: "post",
params: { id }
params: { id },
});
}
@@ -40,7 +40,7 @@ export async function GetObj(id: any) {
return await request({
url: apiPrefix + "/info",
method: "post",
params: { id }
params: { id },
});
}
@@ -48,7 +48,7 @@ export async function GetDetail(id: any) {
return await request({
url: apiPrefix + "/detail",
method: "post",
params: { id }
params: { id },
});
}
@@ -56,7 +56,7 @@ export async function Save(pipelineEntity: any) {
return await request({
url: apiPrefix + "/save",
method: "post",
data: pipelineEntity
data: pipelineEntity,
});
}
@@ -64,7 +64,7 @@ export async function Trigger(id: any, stepId?: string) {
return await request({
url: apiPrefix + "/trigger",
method: "post",
params: { id, stepId }
params: { id, stepId },
});
}
@@ -72,7 +72,7 @@ export async function Cancel(historyId: any) {
return await request({
url: apiPrefix + "/cancel",
method: "post",
params: { historyId }
params: { historyId },
});
}
@@ -80,7 +80,7 @@ export async function BatchUpdateGroup(pipelineIds: number[], groupId: number):
return await request({
url: apiPrefix + "/batchUpdateGroup",
method: "post",
data: { ids: pipelineIds, groupId }
data: { ids: pipelineIds, groupId },
});
}
@@ -88,7 +88,7 @@ export async function BatchDelete(pipelineIds: number[]): Promise<CertInfo> {
return await request({
url: apiPrefix + "/batchDelete",
method: "post",
data: { ids: pipelineIds }
data: { ids: pipelineIds },
});
}
@@ -96,14 +96,14 @@ export async function GetFiles(pipelineId: number) {
return await request({
url: historyApiPrefix + "/files",
method: "post",
params: { pipelineId }
params: { pipelineId },
});
}
export async function GetCount() {
return await request({
url: apiPrefix + "/count",
method: "post"
method: "post",
});
}
@@ -119,6 +119,6 @@ export async function GetCert(pipelineId: number): Promise<CertInfo> {
return await request({
url: certApiPrefix + "/get",
method: "post",
params: { id: pipelineId }
params: { id: pipelineId },
});
}

View File

@@ -0,0 +1,10 @@
import { request } from "/src/api/service";
const apiPrefix = "/monitor/cert";
export async function UploadCert(body: { id?: number; cert: { crt: string; key: string }; pipeline?: any }) {
return await request({
url: apiPrefix + "/upload",
method: "post",
data: body,
});
}

View File

@@ -0,0 +1,58 @@
<template>
<div class="cert-info-updater w-full flex items-center">
<div class="flex-o">
<a-tag>{{ domain }}</a-tag>
<fs-button type="primary" size="small" class="ml-1" icon="ion:upload" text="更新证书" @click="onUploadClick" />
</div>
</div>
</template>
<script lang="tsx" setup>
import { computed, inject } from "vue";
import { useCertUpload } from "./use";
import { getAllDomainsFromCrt } from "/@/views/certd/pipeline/utils";
defineOptions({
name: "CertInfoUpdater",
});
const props = defineProps<{
modelValue?: { crt: string; key: string };
type?: string;
placeholder?: string;
size?: string;
disabled?: boolean;
}>();
const emit = defineEmits(["updated", "update:modelValue"]);
const { openUpdateCertDialog } = useCertUpload();
const domain = computed(() => {
if (!props.modelValue?.crt) {
return "";
}
const domains = getAllDomainsFromCrt(props.modelValue?.crt);
return domains[0];
});
function onUpdated(res: { uploadCert: any }) {
debugger;
emit("update:modelValue", res.uploadCert);
const domains = getAllDomainsFromCrt(res.uploadCert.crt);
emit("updated", { domains });
}
const pipeline: any = inject("pipeline");
function onUploadClick() {
debugger;
openUpdateCertDialog({
onSubmit: onUpdated,
});
}
</script>
<style lang="less">
.cert-info-selector {
width: 100%;
}
</style>

View File

@@ -0,0 +1,237 @@
import { compute, useFormWrapper } from "@fast-crud/fast-crud";
import NotificationSelector from "/@/views/certd/notification/notification-selector/index.vue";
import { cloneDeep, omit } from "lodash-es";
import { useReference } from "/@/use/use-refrence";
import { ref } from "vue";
import * as pluginApi from "../api.plugin";
import * as api from "../api";
import { checkPipelineLimit, getAllDomainsFromCrt } from "/@/views/certd/pipeline/utils";
import { useRouter } from "vue-router";
import { nanoid } from "nanoid";
export function useCertUpload() {
const { openCrudFormDialog } = useFormWrapper();
const router = useRouter();
const certInputs = {
"uploadCert.crt": {
title: "证书",
type: "text",
form: {
component: {
name: "pem-input",
vModel: "modelValue",
textarea: {
rows: 4,
placeholder: "-----BEGIN CERTIFICATE-----\n...\n...\n-----END CERTIFICATE-----",
},
},
helper: "选择pem格式证书文件或者粘贴到此",
rules: [{ required: true, message: "此项必填" }],
col: { span: 24 },
order: -9999,
},
},
"uploadCert.key": {
title: "证书私钥",
type: "text",
form: {
component: {
name: "pem-input",
vModel: "modelValue",
textarea: {
rows: 4,
placeholder: "-----BEGIN PRIVATE KEY-----\n...\n...\n-----END PRIVATE KEY----- ",
},
},
helper: "选择pem格式证书私钥文件或者粘贴到此",
rules: [{ required: true, message: "此项必填" }],
col: { span: 24 },
order: -9999,
},
},
};
async function buildUploadCertPluginInputs(getFormData: any) {
const plugin: any = await pluginApi.GetPluginDefine("CertApplyUpload");
const inputs: any = {};
for (const inputKey in plugin.input) {
if (inputKey === "uploadCert" || inputKey === "domains") {
continue;
}
const inputDefine = cloneDeep(plugin.input[inputKey]);
useReference(inputDefine);
inputs[inputKey] = {
title: inputDefine.title,
form: {
...inputDefine,
show: compute(ctx => {
const form = getFormData();
if (!form) {
return false;
}
let inputDefineShow = true;
if (inputDefine.show != null) {
const computeShow = inputDefine.show as any;
if (computeShow === false) {
inputDefineShow = false;
} else if (computeShow && computeShow.computeFn) {
inputDefineShow = computeShow.computeFn({ form });
}
}
return inputDefineShow;
}),
},
};
}
return inputs;
}
async function openUploadCreateDialog() {
//检查是否流水线数量超出限制
await checkPipelineLimit();
const wrapperRef = ref();
function getFormData() {
if (!wrapperRef.value) {
return null;
}
return wrapperRef.value.getFormData();
}
const inputs = await buildUploadCertPluginInputs(getFormData);
function createCrudOptions() {
return {
crudOptions: {
columns: {
...cloneDeep(certInputs),
...inputs,
notification: {
title: "失败通知",
type: "text",
form: {
value: 0,
component: {
name: NotificationSelector,
vModel: "modelValue",
on: {
selectedChange({ $event, form }: any) {
form.notificationTarget = $event;
},
},
},
order: 101,
helper: "任务执行失败实时提醒",
},
},
},
form: {
wrapper: {
title: "上传证书&创建部署流水线",
saveRemind: false,
},
async doSubmit({ form }: any) {
const cert = form.uploadCert;
const domains = getAllDomainsFromCrt(cert.crt);
const notifications = [];
if (form.notification != null) {
notifications.push({
type: "custom",
when: ["error", "turnToSuccess", "success"],
notificationId: form.notification,
title: form.notificationTarget?.name || "自定义通知",
});
}
const pipelineTitle = domains[0] + "上传证书部署";
const input = omit(form, ["id", "cert", "notification", "notificationTarget"]);
const pipeline = {
title: pipelineTitle,
runnableType: "pipeline",
stages: [
{
id: nanoid(10),
title: "上传证书解析阶段",
maxTaskCount: 1,
runnableType: "stage",
tasks: [
{
id: nanoid(10),
title: "上传证书解析转换",
runnableType: "task",
steps: [
{
id: nanoid(10),
title: "上传证书解析转换",
runnableType: "step",
input: {
cert: cert,
domains: domains,
...input,
},
strategy: {
runStrategy: 0, // 正常执行
},
type: "CertApplyUpload",
},
],
},
],
},
],
notifications,
};
const id = await api.Save({
title: pipeline.title,
content: JSON.stringify(pipeline),
keepHistoryCount: 30,
type: "cert_upload",
});
router.push({
path: "/certd/pipeline/detail",
query: { id: id, editMode: "true" },
});
},
},
},
};
}
const { crudOptions } = createCrudOptions();
const wrapper = await openCrudFormDialog({ crudOptions });
wrapperRef.value = wrapper;
}
async function openUpdateCertDialog(opts: { onSubmit?: any }) {
function createCrudOptions() {
return {
crudOptions: {
columns: {
...cloneDeep(certInputs),
},
form: {
wrapper: {
title: "手动上传证书",
saveRemind: false,
},
async afterSubmit() {},
async doSubmit({ form }: any) {
if (opts.onSubmit) {
await opts.onSubmit(form);
}
},
},
},
};
}
const { crudOptions } = createCrudOptions();
await openCrudFormDialog({ crudOptions });
}
return {
openUploadCreateDialog,
openUpdateCertDialog,
};
}

View File

@@ -10,22 +10,24 @@ export default function (certPlugins: any[], formWrapperRef: any): CreateCrudOpt
const inputs: any = {};
const userStore = useUserStore();
const settingStore = useSettingStore();
const moreParams = [];
for (const plugin of certPlugins) {
for (const inputKey in plugin.input) {
if (inputs[inputKey]) {
inputs[inputKey].form.show = true;
// inputs[inputKey].form.show = true;
continue;
}
const inputDefine = _.cloneDeep(plugin.input[inputKey]);
if (!inputDefine.required && !inputDefine.maybeNeed) {
continue;
moreParams.push(inputKey);
// continue;
}
useReference(inputDefine);
inputs[inputKey] = {
title: inputDefine.title,
form: {
...inputDefine,
show: compute((ctx) => {
show: compute(ctx => {
const form = formWrapperRef.value.getFormData();
if (!form) {
return false;
@@ -34,11 +36,15 @@ export default function (certPlugins: any[], formWrapperRef: any): CreateCrudOpt
let inputDefineShow = true;
if (inputDefine.show != null) {
const computeShow = inputDefine.show as any;
inputDefineShow = computeShow.computeFn({ form });
if (computeShow === false) {
inputDefineShow = false;
} else if (computeShow && computeShow.computeFn) {
inputDefineShow = computeShow.computeFn({ form });
}
}
return form?.certApplyPlugin === plugin.name && inputDefineShow;
})
}
}),
},
};
}
}
@@ -51,8 +57,17 @@ export default function (certPlugins: any[], formWrapperRef: any): CreateCrudOpt
wrapper: {
width: 1350,
saveRemind: false,
title: "创建证书流水线"
}
title: "创建证书流水线",
},
group: {
groups: {
more: {
header: "更多参数",
columns: moreParams,
collapsed: true,
},
},
},
},
columns: {
certApplyPlugin: {
@@ -61,8 +76,8 @@ export default function (certPlugins: any[], formWrapperRef: any): CreateCrudOpt
dict: dict({
data: [
{ value: "CertApply", label: "JS-ACME" },
{ value: "CertApplyLego", label: "Lego-ACME" }
]
{ value: "CertApplyLego", label: "Lego-ACME" },
],
}),
form: {
order: 0,
@@ -75,21 +90,21 @@ export default function (certPlugins: any[], formWrapperRef: any): CreateCrudOpt
<li>Lego-ACMELego实现DNS提供商LEGO的用户可以使用</li>
</ul>
);
}
},
},
valueChange: {
handle: async ({ form, value }) => {
const config = await api.GetPluginConfig({
name: value,
type: "builtIn"
type: "builtIn",
});
if (config.sysSetting?.input) {
merge(form, config.sysSetting.input);
}
},
immediate: true
}
}
immediate: true,
},
},
},
...inputs,
triggerCron: {
@@ -100,11 +115,11 @@ export default function (certPlugins: any[], formWrapperRef: any): CreateCrudOpt
component: {
name: "cron-editor",
vModel: "modelValue",
placeholder: "0 0 4 * * *"
placeholder: "0 0 4 * * *",
},
helper: "点击上面的按钮,选择每天几点定时执行。\n建议设置为每天触发一次证书未到期之前任务会跳过不会重复执行",
order: 100
}
order: 100,
},
},
notification: {
title: "失败通知",
@@ -117,14 +132,14 @@ export default function (certPlugins: any[], formWrapperRef: any): CreateCrudOpt
on: {
selectedChange({ $event, form }) {
form.notificationTarget = $event;
}
}
},
},
},
order: 101,
helper: "任务执行失败实时提醒"
}
}
}
}
helper: "任务执行失败实时提醒",
},
},
},
},
};
}

View File

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

View File

@@ -44,8 +44,8 @@ export default {
const notificationApi = createNotificationApi();
await notificationApi.GetOrCreateDefault({ email: form.email });
}
}
}
},
},
}) as any
);
@@ -60,9 +60,9 @@ export default {
return {
formWrapperRef,
open,
formWrapperOptions
formWrapperOptions,
};
}
},
};
</script>

View File

@@ -0,0 +1,126 @@
import { checkPipelineLimit, readCertDetail } from "/@/views/certd/pipeline/utils";
import { omit } from "lodash-es";
import * as api from "/@/views/certd/pipeline/api";
import { message } from "ant-design-vue";
import { nanoid } from "nanoid";
import { useRouter } from "vue-router";
export function setRunnableIds(pipeline: any) {
const idMap: any = {};
function createId(oldId: any) {
if (oldId == null) {
return nanoid();
}
const newId = nanoid();
idMap[oldId] = newId;
return newId;
}
if (pipeline.stages) {
for (const stage of pipeline.stages) {
stage.id = createId(stage.id);
if (stage.tasks) {
for (const task of stage.tasks) {
task.id = createId(task.id);
if (task.steps) {
for (const step of task.steps) {
step.id = createId(step.id);
}
}
}
}
}
}
for (const trigger of pipeline.triggers) {
trigger.id = nanoid();
}
for (const notification of pipeline.notifications) {
notification.id = nanoid();
}
let content = JSON.stringify(pipeline);
for (const key in idMap) {
content = content.replaceAll(key, idMap[key]);
}
return JSON.parse(content);
}
export function useCertd(certdFormRef: any) {
const router = useRouter();
async function openAddCertdPipelineDialog() {
//检查是否流水线数量超出限制
await checkPipelineLimit();
certdFormRef.value.open(async ({ form }: any) => {
const certDetail = readCertDetail(form.cert.crt);
// 添加certd pipeline
const triggers = [];
if (form.triggerCron) {
triggers.push({ title: "定时触发", type: "timer", props: { cron: form.triggerCron } });
}
const notifications = [];
if (form.notification != null) {
notifications.push({
type: "custom",
when: ["error", "turnToSuccess", "success"],
notificationId: form.notification,
title: form.notificationTarget?.name || "自定义通知",
});
}
const pluginInput = omit(form, ["triggerCron", "notification", "notificationTarget", "certApplyPlugin"]);
let pipeline = {
title: form.domains[0] + "证书自动化",
runnableType: "pipeline",
stages: [
{
title: "证书申请阶段",
maxTaskCount: 1,
runnableType: "stage",
tasks: [
{
title: "证书申请任务",
runnableType: "task",
steps: [
{
title: "申请证书",
runnableType: "step",
input: {
renewDays: 35,
...pluginInput,
},
strategy: {
runStrategy: 0, // 正常执行
},
type: form.certApplyPlugin,
},
],
},
],
},
],
triggers,
notifications,
};
pipeline = setRunnableIds(pipeline);
/**
* // cert: 证书; backup: 备份; custom:自定义;
* type: string;
* // custom: 自定义; monitor: 监控;
* from: string;
*/
const id = await api.Save({
title: pipeline.title,
content: JSON.stringify(pipeline),
keepHistoryCount: 30,
type: "cert",
});
message.success("创建成功,请添加证书部署任务");
router.push({ path: "/certd/pipeline/detail", query: { id, editMode: "true" } });
});
}
return {
openAddCertdPipelineDialog,
};
}

View File

@@ -4,62 +4,26 @@ import { computed, ref } from "vue";
import { useRouter } from "vue-router";
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, notification } from "ant-design-vue";
import { Modal, notification } from "ant-design-vue";
import { env } from "/@/utils/util.env";
import { useUserStore } from "/@/store/modules/user";
import dayjs from "dayjs";
import { useSettingStore } from "/@/store/modules/settings";
import * as _ from "lodash-es";
import { cloneDeep } from "lodash-es";
import { useModal } from "/@/use/use-modal";
import CertView from "./cert-view.vue";
import { eachStages } from "./utils";
import { createNotificationApi as createNotificationApi } from "../notification/api";
import { mySuiteApi } from "/@/views/certd/suite/mine/api";
import { setRunnableIds, useCertd } from "/@/views/certd/pipeline/certd-form/use";
import { useCertUpload } from "/@/views/certd/pipeline/cert-upload/use";
export default function ({ crudExpose, context: { certdFormRef, groupDictRef, selectedRowKeys } }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter();
const { t } = useI18n();
const lastResRef = ref();
function setRunnableIds(pipeline: any) {
const idMap: any = {};
function createId(oldId: any) {
if (oldId == null) {
return nanoid();
}
const newId = nanoid();
idMap[oldId] = newId;
return newId;
}
if (pipeline.stages) {
for (const stage of pipeline.stages) {
stage.id = createId(stage.id);
if (stage.tasks) {
for (const task of stage.tasks) {
task.id = createId(task.id);
if (task.steps) {
for (const step of task.steps) {
step.id = createId(step.id);
}
}
}
}
}
}
const { openAddCertdPipelineDialog } = useCertd(certdFormRef);
const { openUploadCreateDialog } = useCertUpload();
for (const trigger of pipeline.triggers) {
trigger.id = nanoid();
}
for (const notification of pipeline.notifications) {
notification.id = nanoid();
}
let content = JSON.stringify(pipeline);
for (const key in idMap) {
content = content.replaceAll(key, idMap[key]);
}
return JSON.parse(content);
}
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
@@ -76,7 +40,7 @@ export default function ({ crudExpose, context: { certdFormRef, groupDictRef, se
const addRequest = async ({ form }: AddReq) => {
if (form.content == null) {
form.content = JSON.stringify({
title: form.title
title: form.title,
});
} else {
//复制的流水线
@@ -96,90 +60,6 @@ export default function ({ crudExpose, context: { certdFormRef, groupDictRef, se
return res;
};
const settingsStore = useSettingStore();
async function addCertdPipeline() {
//检查是否流水线数量超出限制
if (settingsStore.isComm && settingsStore.suiteSetting.enabled) {
//检查数量是否超限
const suiteDetail = await mySuiteApi.SuiteDetailGet();
const max = suiteDetail.pipelineCount.max;
if (max != -1 && max <= suiteDetail.pipelineCount.used) {
notification.error({
message: `对不起,您最多只能创建${max}条流水线,请购买或升级套餐`
});
return;
}
}
certdFormRef.value.open(async ({ form }: any) => {
// 添加certd pipeline
const triggers = [];
if (form.triggerCron) {
triggers.push({ title: "定时触发", type: "timer", props: { cron: form.triggerCron } });
}
const notifications = [];
if (form.notification != null) {
notifications.push({
type: "custom",
when: ["error", "turnToSuccess", "success"],
notificationId: form.notification,
title: form.notificationTarget?.name || "自定义通知"
});
}
let pipeline = {
title: form.domains[0] + "证书自动化",
runnableType: "pipeline",
stages: [
{
title: "证书申请阶段",
maxTaskCount: 1,
runnableType: "stage",
tasks: [
{
title: "证书申请任务",
runnableType: "task",
steps: [
{
title: "申请证书",
runnableType: "step",
input: {
renewDays: 35,
...form
},
strategy: {
runStrategy: 0 // 正常执行
},
type: form.certApplyPlugin
}
]
}
]
}
],
triggers,
notifications
};
pipeline = setRunnableIds(pipeline);
/**
* // cert: 证书; backup: 备份; custom:自定义;
* type: string;
* // custom: 自定义; monitor: 监控;
* from: string;
*/
const id = await api.Save({
title: pipeline.title,
content: JSON.stringify(pipeline),
keepHistoryCount: 30,
type: "cert",
from: "custom"
});
message.success("创建成功,请添加证书部署任务");
router.push({ path: "/certd/pipeline/detail", query: { id, editMode: "true" } });
});
}
const model = useModal();
const viewCert = async (row: any) => {
const cert = await api.GetCert(row.id);
@@ -195,7 +75,7 @@ export default function ({ crudExpose, context: { certdFormRef, groupDictRef, se
width: 800,
content: () => {
return <CertView cert={cert}></CertView>;
}
},
});
};
@@ -230,7 +110,7 @@ export default function ({ crudExpose, context: { certdFormRef, groupDictRef, se
<div> {children}</div>
</div>
);
}
},
});
};
const userStore = useUserStore();
@@ -242,7 +122,7 @@ export default function ({ crudExpose, context: { certdFormRef, groupDictRef, se
pageRequest,
addRequest,
editRequest,
delRequest
delRequest,
},
settings: {
plugins: {
@@ -259,46 +139,57 @@ export default function ({ crudExpose, context: { certdFormRef, groupDictRef, se
selectedRowKeys,
onSelectedChanged(selected) {
console.log("已选择变化:", selected);
}
}
}
}
},
},
},
},
},
actionbar: {
buttons: {
add: {
order: 5,
text: "自定义流水线"
icon: "ion:ios-add-circle-outline",
text: "自定义流水线",
},
addCertd: {
order: 1,
text: "创建证书流水线",
type: "primary",
icon: "ion:ios-add-circle-outline",
click() {
addCertdPipeline();
}
}
}
openAddCertdPipelineDialog();
},
},
uploadCert: {
order: 2,
text: "上传证书部署",
type: "primary",
icon: "ion:cloud-upload-outline",
click() {
openUploadCreateDialog();
},
},
},
},
form: {
afterSubmit({ form, res, mode }) {
if (mode === "add") {
router.push({ path: "/certd/pipeline/detail", query: { id: res.id, editMode: "true" } });
}
}
},
},
table: {
scroll: { x: 1500 }
scroll: { x: 1500 },
},
tabs: {
name: "groupId",
show: true
show: true,
},
rowHandle: {
width: 200,
fixed: "right",
dropdown: {
show: true
show: true,
},
buttons: {
play: {
@@ -313,30 +204,30 @@ export default function ({ crudExpose, context: { certdFormRef, groupDictRef, se
async onOk() {
await api.Trigger(row.id);
notification.success({ message: "管道已经开始运行" });
}
},
});
}
},
},
view: {
show: false,
click({ row }) {
router.push({ path: "/certd/pipeline/detail", query: { id: row.id, editMode: "false" } });
}
},
},
copy: {
click: async (context) => {
click: async context => {
settingStore.checkPlus();
const { ui } = useUi();
// @ts-ignore
let row = context[ui.tableColumn.row];
row = _.cloneDeep(row);
row = cloneDeep(row);
row.title = row.title + "_copy";
await crudExpose.openCopy({
row: row,
index: context.index
index: context.index,
});
},
class: "need-plus"
class: "need-plus",
},
config: {
order: 1,
@@ -346,13 +237,13 @@ export default function ({ crudExpose, context: { certdFormRef, groupDictRef, se
icon: "ant-design:edit-outlined",
click({ row }) {
router.push({ path: "/certd/pipeline/detail", query: { id: row.id, editMode: "true" } });
}
},
},
edit: {
order: 2,
title: "修改配置/分组",
icon: "ant-design:setting-outlined",
dropdown: true
dropdown: true,
},
viewCert: {
order: 3,
@@ -361,7 +252,7 @@ export default function ({ crudExpose, context: { certdFormRef, groupDictRef, se
icon: "ph:certificate",
async click({ row }) {
await viewCert(row);
}
},
},
download: {
order: 4,
@@ -370,13 +261,13 @@ export default function ({ crudExpose, context: { certdFormRef, groupDictRef, se
icon: "ant-design:download-outlined",
async click({ row }) {
await downloadCert(row);
}
},
},
remove: {
order: 5,
dropdown: true
}
}
dropdown: true,
},
},
},
columns: {
id: {
@@ -384,14 +275,14 @@ export default function ({ crudExpose, context: { certdFormRef, groupDictRef, se
key: "id",
type: "number",
search: {
show: true
show: true,
},
column: {
width: 100
width: 100,
},
form: {
show: false
}
show: false,
},
},
userId: {
title: "用户Id",
@@ -399,17 +290,17 @@ export default function ({ crudExpose, context: { certdFormRef, groupDictRef, se
search: {
show: computed(() => {
return userStore.isAdmin && settingStore.sysPublic.managerOtherUserPipeline;
})
}),
},
form: {
show: false
show: false,
},
column: {
show: computed(() => {
return userStore.isAdmin && settingStore.sysPublic.managerOtherUserPipeline;
}),
width: 100
}
width: 100,
},
},
title: {
title: "流水线名称",
@@ -418,11 +309,11 @@ export default function ({ crudExpose, context: { certdFormRef, groupDictRef, se
show: true,
title: "关键字",
component: {
name: "a-input"
}
name: "a-input",
},
},
form: {
rules: [{ required: true, message: "此项必填" }]
rules: [{ required: true, message: "此项必填" }],
},
column: {
width: 350,
@@ -432,16 +323,16 @@ export default function ({ crudExpose, context: { certdFormRef, groupDictRef, se
// 注意必须要on前缀
onClick({ row }) {
router.push({ path: "/certd/pipeline/detail", query: { id: row.id, editMode: "false" } });
}
}
}
}
},
},
},
},
},
content: {
title: "流水线内容",
form: { show: false },
column: {
show: false
show: false,
},
valueBuilder({ row }) {
if (row.content) {
@@ -463,18 +354,18 @@ export default function ({ crudExpose, context: { certdFormRef, groupDictRef, se
if (row.content) {
row.content = JSON.stringify(row.content);
}
}
},
},
_triggerCount: {
title: "定时任务数",
type: "number",
column: {
align: "center",
width: 100
width: 100,
},
form: {
show: false
}
show: false,
},
},
_stepCount: {
title: "部署任务数",
@@ -482,14 +373,14 @@ export default function ({ crudExpose, context: { certdFormRef, groupDictRef, se
form: { show: false },
column: {
align: "center",
width: 100
}
width: 100,
},
},
lastVars: {
title: "到期剩余",
type: "number",
form: {
show: false
show: false,
},
column: {
cellRender({ row }) {
@@ -501,38 +392,53 @@ export default function ({ crudExpose, context: { certdFormRef, groupDictRef, se
const percent = (leftDays / 90) * 100;
return <a-progress percent={percent} strokeColor={color} format={(percent: number) => `${leftDays}`} />;
},
width: 150
}
width: 150,
},
},
"lastVars.certExpiresTime": {
title: "过期时间",
search: {
show: false,
},
type: "datetime",
form: {
show: false,
},
column: {
sorter: true,
width: 150,
align: "center",
},
},
status: {
title: "状态",
type: "dict-select",
search: {
show: true
show: true,
},
dict: dict({
data: statusUtil.getOptions()
data: statusUtil.getOptions(),
}),
form: {
show: false
show: false,
},
column: {
sorter: true,
width: 120,
align: "center"
}
align: "center",
},
},
lastHistoryTime: {
title: "最后运行",
type: "datetime",
form: {
show: false
show: false,
},
column: {
sorter: true,
width: 150,
align: "center"
}
align: "center",
},
},
disabled: {
title: "启用",
@@ -540,12 +446,12 @@ export default function ({ crudExpose, context: { certdFormRef, groupDictRef, se
dict: dict({
data: [
{ value: false, label: "启用" },
{ value: true, label: "禁用" }
]
{ value: true, label: "禁用" },
],
}),
form: {
value: false,
show: false
show: false,
},
column: {
sorter: true,
@@ -553,30 +459,58 @@ export default function ({ crudExpose, context: { certdFormRef, groupDictRef, se
align: "center",
component: {
name: "fs-dict-switch",
vModel: "checked"
vModel: "checked",
},
async valueChange({ row, key, value }) {
return await api.UpdateObj({
id: row.id,
disabled: row[key]
disabled: row[key],
});
}
}
},
},
},
groupId: {
title: "分组",
type: "dict-select",
search: {
show: true
show: true,
},
dict: groupDictRef,
column: {
width: 130,
align: "center",
component: {
color: "auto"
}
}
color: "auto",
},
sorter: true,
},
},
type: {
title: "类型",
type: "dict-select",
search: {
show: true,
},
dict: dict({
data: [
{ value: "cert", label: "证书申请" },
{ value: "cert_upload", label: "证书上传" },
{ value: "custom", label: "自定义" },
],
}),
form: {
show: false,
value: "custom",
},
column: {
sorter: true,
width: 90,
align: "center",
show: true,
component: {
color: "auto",
},
},
},
order: {
title: "排序号",
@@ -584,48 +518,50 @@ export default function ({ crudExpose, context: { certdFormRef, groupDictRef, se
column: {
sorter: true,
align: "center",
width: 80
width: 80,
},
form: {
value: 0
}
value: 0,
},
},
keepHistoryCount: {
title: "历史记录保持数",
type: "number",
form: {
value: 20,
helper: "历史记录保持条数,多余的会被删除"
helper: "历史记录保持条数,多余的会被删除",
},
column: {
width: 130,
show: false
}
show: false,
sorter: true,
},
},
createTime: {
title: "创建时间",
type: "datetime",
form: {
show: false
show: false,
},
column: {
sorter: true,
width: 155,
align: "center"
}
align: "center",
},
},
updateTime: {
title: "更新时间",
type: "datetime",
form: {
show: false
show: false,
},
column: {
width: 125,
show: false
}
}
}
}
show: false,
sorter: true,
},
},
},
},
};
}

View File

@@ -0,0 +1,130 @@
<template>
<a-tooltip :title="title">
<div class="task-shortcut" :title="title" @click="openDialog">
<fs-icon :icon="icon" v-bind="attrs"></fs-icon>
</div>
</a-tooltip>
</template>
<script setup lang="ts">
import { doRequest } from "/@/components/plugins/lib";
import { ref, useAttrs, inject } from "vue";
import { useFormWrapper } from "@fast-crud/fast-crud";
import { notification } from "ant-design-vue";
import { mergeWith, isArray } from "lodash-es";
defineOptions({
name: "TaskShortcut",
});
const { openCrudFormDialog } = useFormWrapper();
const props = defineProps<{
icon: string;
title: string;
action: string;
form: any;
input: any;
pluginName: string;
stepId: string;
}>();
const attrs = useAttrs();
const loading = ref(false);
async function openDialog() {
function createCrudOptions() {
return {
crudOptions: {
columns: {
...props.form.columns,
immediateRun: {
title: "立即运行",
type: "switch",
span: 24,
form: {
value: true,
component: {
name: "a-switch",
vModel: "checked",
},
helper: "保存后是否立即触发运行流水线",
},
},
},
form: {
wrapper: {
title: props.title,
saveRemind: false,
},
afterSubmit() {
notification.success({ message: "操作成功" });
},
async doSubmit({ form }: any) {
return await doPluginFormSubmit(form);
},
},
},
};
}
const { crudOptions } = createCrudOptions();
await openCrudFormDialog({ crudOptions });
}
const getPipelineScope: any = inject("getPipelineScope");
const doPluginFormSubmit = async (formData: any) => {
if (loading.value) {
return;
}
loading.value = true;
try {
const res = await doRequest({
type: "plugin",
typeName: props.pluginName,
action: props.action,
input: props.input,
data: formData,
});
if (res.input) {
const { save, findStep } = getPipelineScope();
const step = findStep(props.stepId);
if (step) {
// 数组覆盖合并
mergeWith(step.input, res.input, (objValue, srcValue) => {
if (isArray(objValue)) {
return srcValue;
}
});
//保存,但不改变当前编辑状态
save(false);
}
}
if (formData.immediateRun) {
const { run } = getPipelineScope();
run();
}
return res;
} finally {
loading.value = false;
}
};
</script>
<style lang="less">
.task-shortcut {
width: 25px;
height: 22px;
border: 1px solid #e3e3e3;
border-radius: 0 0 5px 5px;
background: #fff;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
border-top: 0;
&:hover {
background: #fff;
border-color: #38a0fb;
}
}
</style>

View File

@@ -0,0 +1,51 @@
<template>
<div class="task-shortcuts">
<TaskShortcut v-for="(item, index) of shortcuts" :key="index" v-bind="item" />
</div>
</template>
<script setup lang="ts">
import { ref, watch } from "vue";
import * as pluginApi from "/@/views/certd/pipeline/api.plugin";
import TaskShortcut from "./task-shortcut.vue";
defineOptions({
name: "TaskShortcuts",
});
const props = defineProps<{
task: any;
}>();
watch(
() => props.task,
value => {
init();
},
{ immediate: true }
);
const shortcuts = ref([]);
async function init() {
const steps = props.task?.steps || [];
if (steps.length === 0) {
return;
}
const list = [];
for (const step of steps) {
const stepType = step.type;
const pluginDefine = await pluginApi.GetPluginDefine(stepType);
if (pluginDefine.shortcut) {
for (const key in pluginDefine.shortcut) {
const shortcut = pluginDefine.shortcut[key];
list.push({
...shortcut,
pluginName: stepType,
input: step.input,
stepId: step.id,
});
}
}
}
shortcuts.value = list;
}
</script>

View File

@@ -1,5 +1,5 @@
<template>
<a-drawer v-model:open="stepDrawerVisible" placement="right" :closable="true" width="760px" class="step-form-drawer" :class="{ fullscreen }">
<a-drawer v-model:open="stepDrawerVisible" :wrap-style="{ maxWidth: '100vw' }" placement="right" :closable="true" width="760px" class="step-form-drawer" :class="{ fullscreen }">
<template #title>
<div>
编辑步骤
@@ -9,7 +9,7 @@
</a-button>
</template>
</div>
<div>
<div class="hidden md:block">
<fs-icon class="icon-button" :icon="fullscreen ? 'material-symbols:fullscreen-exit' : 'material-symbols:fullscreen'" @click="fullscreen = !fullscreen"></fs-icon>
</div>
</template>
@@ -31,7 +31,7 @@
</a-col>
</a-row>
<a-row v-else :gutter="10">
<a-col v-for="item of group.plugins" :key="item.key" class="step-plugin">
<a-col v-for="item of group.plugins" :key="item.key" class="step-plugin w-full md:w-[50%]">
<a-card
hoverable
:class="{ current: item.name === currentStep.type }"
@@ -413,6 +413,10 @@ export default {
<style lang="less">
.step-form-drawer {
max-width: 100%;
.ant-tabs-right > div > .ant-tabs-nav .ant-tabs-tab {
padding: 8px 10px;
}
&.fullscreen {
.pi-step-form {
.body {
@@ -437,6 +441,10 @@ export default {
}
}
.ant-drawer-body {
padding: 10px;
}
.pi-step-form {
.bottom-button {
padding: 20px;
@@ -456,7 +464,6 @@ export default {
padding: 0px;
.step-plugin {
width: 50%;
}
.ant-tabs-content {

View File

@@ -18,12 +18,12 @@
<div class="layout">
<div class="layout-left">
<div class="pipeline-container bg-neutral-100 dark:bg-black">
<div ref="pipelineContainer" class="pipeline-container bg-neutral-100 dark:bg-black">
<div class="pipeline">
<v-draggable v-model="pipeline.stages" class="stages" item-key="id" handle=".stage-move-handle" :disabled="!settingStore.isPlus">
<template #header>
<div class="stage first-stage">
<div class="title stage-move-handle">
<div class="title">
<text-editable model-value="触发源" :disabled="true" />
</div>
<div class="tasks">
@@ -67,19 +67,19 @@
<template #item="{ element: stage, index }">
<div :key="stage.id" class="stage" :class="{ 'last-stage': isLastStage(index), ['stage_' + index]: true }">
<div class="title">
<div class="title" @mousedown.stop>
<text-editable v-model="stage.title" :disabled="!editMode"></text-editable>
<div v-plus class="icon-box stage-move-handle">
<fs-icon v-if="editMode" title="拖动排序" icon="ion:move-outline"></fs-icon>
</div>
</div>
<v-draggable v-model="stage.tasks" item-key="id" class="tasks" group="task" handle=".task-move-handle" :disabled="!settingStore.isPlus">
<v-draggable v-model="stage.tasks" item-key="id" class="tasks" group="task" handle=".task-move-handle" :disabled="!settingStore.isPlus" @mousedown.stop>
<template #item="{ element: task, index: taskIndex }">
<div
class="task-container"
:class="{
'first-task': taskIndex === 0,
'validate-error': hasValidateError(task.id)
'validate-error': hasValidateError(task.id),
}"
>
<div class="line line-left">
@@ -104,7 +104,7 @@
</template>
<span class="flex-o w-100">
<span class="ellipsis flex-1 task-title" :class="{ 'in-edit': editMode, deleted: task.disabled }">{{ task.title }}</span>
<pi-status-show :status="task.status?.result"></pi-status-show>
<pi-status-show v-if="!editMode" :status="task.status?.result"></pi-status-show>
</span>
</a-popover>
</a-button>
@@ -114,6 +114,9 @@
<div v-plus class="icon-box task-move-handle action drag">
<fs-icon v-if="editMode" title="拖动排序" icon="ion:move-outline"></fs-icon>
</div>
<div class="shortcut">
<TaskShortcuts :task="task" />
</div>
</div>
</div>
</template>
@@ -269,29 +272,31 @@ import PiHistoryTimelineItem from "/@/views/certd/pipeline/pipeline/component/hi
import { FsIcon } from "@fast-crud/fast-crud";
import { useSettingStore } from "/@/store/modules/settings";
import { useUserStore } from "/@/store/modules/user";
import TaskShortcuts from "./component/shortcut/task-shortcuts.vue";
import { eachSteps, findStep } from "../utils";
export default defineComponent({
name: "PipelineEdit",
// eslint-disable-next-line vue/no-unused-components
components: { FsIcon, PiHistoryTimelineItem, PiTaskForm, PiTriggerForm, PiTaskView, PiStatusShow, PiNotificationForm, VDraggable },
components: { FsIcon, PiHistoryTimelineItem, PiTaskForm, PiTriggerForm, PiTaskView, PiStatusShow, PiNotificationForm, VDraggable, TaskShortcuts },
props: {
pipelineId: {
type: [Number, String],
default: 0
default: 0,
},
historyId: {
type: [Number, String],
default: 0
default: 0,
},
editMode: {
type: Boolean,
default: false
default: false,
},
options: {
type: Object as PropType<PipelineOptions>,
default() {
return {};
}
}
},
},
},
emits: ["update:modelValue", "update:editMode"],
setup(props, ctx) {
@@ -341,8 +346,9 @@ export default defineComponent({
histories.value = historyList;
if (historyList.length > 0) {
//@ts-ignore
if (props.historyId > 0) {
const found = historyList.find((item) => {
const found = historyList.find(item => {
//字符串==int
return item.id == props.historyId;
});
@@ -383,7 +389,7 @@ export default defineComponent({
() => {
return props.editMode;
},
(editMode) => {
editMode => {
if (editMode) {
changeCurrentHistory();
} else if (histories.value.length > 0) {
@@ -407,7 +413,7 @@ export default defineComponent({
await loadHistoryList(true);
},
{
immediate: true
immediate: true,
}
);
@@ -438,7 +444,7 @@ export default defineComponent({
};
return {
taskViewOpen,
taskViewRef
taskViewRef,
};
}
@@ -503,7 +509,7 @@ export default defineComponent({
id: nanoid(),
title: "新阶段",
tasks: [],
status: null
status: null,
};
//stage: any, stageIndex: number, onSuccess
useTaskRet.taskAdd(stage, stageIndex, () => {
@@ -519,7 +525,7 @@ export default defineComponent({
}
return {
stageAdd,
isLastStage
isLastStage,
};
}
@@ -551,7 +557,7 @@ export default defineComponent({
return {
triggerAdd,
triggerEdit,
triggerFormRef
triggerFormRef,
};
}
@@ -586,7 +592,7 @@ export default defineComponent({
return {
notificationAdd,
notificationEdit,
notificationFormRef
notificationFormRef,
};
}
@@ -604,7 +610,7 @@ export default defineComponent({
},
onCancel() {
resolve(false);
}
},
});
});
if (!res) {
@@ -628,7 +634,7 @@ export default defineComponent({
watchNewHistoryList();
await props.options.doTrigger({ pipelineId: pipeline.value.id, stepId: stepId });
notification.success({ message: "管道已经开始运行" });
}
},
});
};
@@ -642,31 +648,18 @@ export default defineComponent({
validateErrors.value[taskId] = errors;
errors.push(error);
}
function doValidate() {
validateErrors.value = {};
const stepIds: string[] = [];
//校验output id是否正确
const pp = pipeline.value;
function eachSteps(callback: any) {
if (pp.stages) {
for (const stage of pp.stages) {
if (stage.tasks) {
for (const task of stage.tasks) {
if (task.steps) {
for (const step of task.steps) {
callback(step, task, stage);
}
}
}
}
}
}
}
//检查输出的stepid是否存在
let hasError = false;
let errorMessage = "";
eachSteps((step: any, task: any, stage: any) => {
eachSteps(pp, (step: any, task: any, stage: any) => {
stepIds.push(step.id);
if (step.input) {
for (const key in step.input) {
@@ -684,10 +677,10 @@ export default defineComponent({
hasError = true;
const message = `任务${step.title}的前置输出步骤${paramName}不存在,请重新修改此任务`;
addValidateError(task.id, {
message
message,
});
addValidateError(step.id, {
message
message,
});
errorMessage += message + "";
}
@@ -704,7 +697,7 @@ export default defineComponent({
function hasValidateError(taskId: string) {
return validateErrors.value[taskId] != null;
}
const save = async () => {
const save = async (offEdit = true) => {
doValidate();
saveLoading.value = true;
@@ -723,7 +716,9 @@ export default defineComponent({
await props.options.doSave(pipeline.value);
}
toggleEditMode(false);
if (offEdit) {
toggleEditMode(false);
}
} finally {
saveLoading.value = false;
}
@@ -738,13 +733,18 @@ export default defineComponent({
toggleEditMode(false);
};
function fundStepFromPipeline(id: string) {
return findStep(pipeline.value, id);
}
return {
run,
save,
edit,
cancel,
saveLoading,
hasValidateError
hasValidateError,
findStep: fundStepFromPipeline,
};
}
@@ -767,14 +767,66 @@ export default defineComponent({
historyView,
historyCancel,
logsCollapse,
toggleLogsCollapse
toggleLogsCollapse,
};
}
function useScroll() {
const pipelineContainer = ref();
onMounted(() => {
if (pipelineContainer.value) {
const scrollableDiv = pipelineContainer.value;
let isDragging = false;
let startX: any = null;
let scrollLeft: any = null;
scrollableDiv.addEventListener("mousedown", (e: any) => {
isDragging = true;
startX = e.pageX - scrollableDiv.offsetLeft;
scrollLeft = scrollableDiv.scrollLeft;
scrollableDiv.style.cursor = "grabbing"; // 按住时变成抓手
e.stopPropagation();
});
scrollableDiv.addEventListener("mouseleave", () => {
isDragging = false;
scrollableDiv.style.cursor = "grab"; // 离开时恢复光标
});
scrollableDiv.addEventListener("mouseup", () => {
isDragging = false;
scrollableDiv.style.cursor = "grab"; // 松开时恢复光标
});
scrollableDiv.addEventListener("mousemove", (e: any) => {
if (!isDragging) return; // 如果没有按住鼠标,退出
e.preventDefault();
const x = e.pageX - scrollableDiv.offsetLeft;
const walk = (x - startX) * 2; // 移动的速度
scrollableDiv.scrollLeft = scrollLeft - walk; // 更新滚动位置
});
}
});
return { pipelineContainer };
}
const useTaskRet = useTask();
const useStageRet = useStage(useTaskRet);
const settingStore = useSettingStore();
const userStore = useUserStore();
const actions = useActions();
const trigger = useTrigger();
provide("getPipelineScope", () => {
return {
run: actions.run,
pipeline: pipeline,
save: actions.save,
findStep: actions.findStep,
};
});
return {
pipeline,
currentHistory,
@@ -784,12 +836,13 @@ export default defineComponent({
settingStore,
...useTaskRet,
...useStageRet,
...useTrigger(),
...useActions(),
...trigger,
...actions,
...useHistory(),
...useNotification()
...useNotification(),
...useScroll(),
};
}
},
});
</script>
<style lang="less">
@@ -992,8 +1045,8 @@ export default defineComponent({
.action {
position: absolute;
right: 60px;
top: 18px;
right: 10px;
top: 17px;
//font-size: 18px;
cursor: pointer;
z-index: 10;
@@ -1001,10 +1054,10 @@ export default defineComponent({
color: #1890ff;
}
&.copy {
right: 80px;
right: 30px;
}
&.drag {
right: 60px;
right: 10px;
cursor: move;
}
}
@@ -1012,6 +1065,13 @@ export default defineComponent({
.ant-btn {
width: 200px;
}
position: relative;
.shortcut {
position: absolute;
bottom: -10px;
left: 20px;
}
}
}
}

View File

@@ -1,10 +1,14 @@
import { forEach } from "lodash-es";
import { mySuiteApi } from "/@/views/certd/suite/mine/api";
import { notification } from "ant-design-vue";
import { useSettingStore } from "/@/store/modules/settings";
//@ts-ignore
import forge from "node-forge";
export function eachStages(list: any[], exec: (item: any, runnableType: string) => void, runnableType: string = "stage") {
if (!list || list.length <= 0) {
return;
}
forEach(list, (item) => {
forEach(list, item => {
exec(item, runnableType);
if (runnableType === "stage") {
eachStages(item.tasks, exec, "task");
@@ -13,3 +17,86 @@ export function eachStages(list: any[], exec: (item: any, runnableType: string)
}
});
}
export function eachSteps(pipeline: any, callback: any) {
const pp = pipeline;
if (pp.stages) {
for (const stage of pp.stages) {
if (stage.tasks) {
for (const task of stage.tasks) {
if (task.steps) {
for (const step of task.steps) {
callback(step, task, stage);
}
}
}
}
}
}
}
export function findStep(pipeline: any, id: string) {
const pp = pipeline;
if (pp.stages) {
for (const stage of pp.stages) {
if (stage.tasks) {
for (const task of stage.tasks) {
if (task.steps) {
for (const step of task.steps) {
if (step.id === id) {
return step;
}
}
}
}
}
}
}
}
export async function checkPipelineLimit() {
const settingsStore = useSettingStore();
if (settingsStore.isComm && settingsStore.suiteSetting.enabled) {
//检查数量是否超限
const suiteDetail = await mySuiteApi.SuiteDetailGet();
const max = suiteDetail.pipelineCount.max;
if (max != -1 && max <= suiteDetail.pipelineCount.used) {
notification.error({
message: `对不起,您最多只能创建${max}条流水线,请购买或升级套餐`,
});
throw new Error("流水线数量超限");
}
}
}
export function readCertDetail(crt: string) {
const detail = forge.pki.certificateFromPem(crt);
const expires = detail.notAfter;
return { detail, expires };
}
export function getAllDomainsFromCrt(crt: string) {
const { detail } = readCertDetail(crt);
const domains = [];
// 1. 提取SAN中的DNS名称
const sanExtension = detail.extensions.find((ext: any) => ext.name === "subjectAltName");
if (sanExtension) {
sanExtension.altNames.forEach((altName: any) => {
if (altName.type === 2) {
// type=2 表示DNS名称
domains.push(altName.value);
}
});
}
// 2. 如果没有SAN回退到CN通用名称
if (domains.length === 0) {
const cnAttr = detail.subject.attributes.find((attr: any) => attr.name === "commonName");
if (cnAttr) {
domains.push(cnAttr.value);
}
}
return domains;
}

View File

@@ -16,8 +16,6 @@
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-function": "off",
// "no-unused-expressions": "off",
"max-len": [0, 160, 2, { "ignoreUrls": true }]
"@typescript-eslint/no-empty-function": "off"
}
}

View File

@@ -0,0 +1,7 @@
{
"printWidth": 220,
"bracketSpacing": true,
"singleQuote": false,
"trailingComma": "es5",
"arrowParens": "avoid"
}

View File

@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.31.4](https://github.com/certd/certd/compare/v1.31.3...v1.31.4) (2025-03-21)
### Bug Fixes
* 修复站点监控通知通过webhook发送失败的bug ([9be1ecc](https://github.com/certd/certd/commit/9be1ecc8aab3ea23dd0dc2dab3688f4edb90ef2c))
* 修复dns.la域名申请失败的bug ([1de8eee](https://github.com/certd/certd/commit/1de8eee6ea8307f3c11626af75303d3cc104bb95))
### Performance Improvements
* 流水线增加上传证书快捷方式 ([425bba6](https://github.com/certd/certd/commit/425bba67c539b734e2a85a83a4f9ecc9b2434fb4))
* 手动上传证书部署流水线 ([fbb66f3](https://github.com/certd/certd/commit/fbb66f3c4389489aa8a43b194d82bc8cf391607b))
* 站点监控,手动测试也发通知 ([729b19c](https://github.com/certd/certd/commit/729b19c8da60d5efb5baef7cf8df0518e7f6b471))
* 站点证书监控支持模糊查询 ([0069c0e](https://github.com/certd/certd/commit/0069c0e3992946a8dd6410f299d4fc974ef0e76b))
* 支持飞书通知 ([b82e1dc](https://github.com/certd/certd/commit/b82e1dcd6217b09a7d7e21cd648bb31de320cadf))
* 支持手动上传证书并部署 ([a9fffa5](https://github.com/certd/certd/commit/a9fffa5180c83da27b35886aa2e858a92a2c5f94))
## [1.31.3](https://github.com/certd/certd/compare/v1.31.2...v1.31.3) (2025-03-13)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/ui-server",
"version": "1.31.3",
"version": "1.31.4",
"description": "fast-server base midway",
"private": true,
"type": "module",
@@ -38,18 +38,18 @@
"@aws-sdk/client-acm": "^3.699.0",
"@aws-sdk/client-cloudfront": "^3.699.0",
"@aws-sdk/client-s3": "^3.705.0",
"@certd/acme-client": "^1.31.3",
"@certd/basic": "^1.31.3",
"@certd/commercial-core": "^1.31.3",
"@certd/lib-huawei": "^1.31.3",
"@certd/lib-k8s": "^1.31.3",
"@certd/lib-server": "^1.31.3",
"@certd/midway-flyway-js": "^1.31.3",
"@certd/pipeline": "^1.31.3",
"@certd/plugin-cert": "^1.31.3",
"@certd/plugin-lib": "^1.31.3",
"@certd/plugin-plus": "^1.31.3",
"@certd/plus-core": "^1.31.3",
"@certd/acme-client": "^1.31.4",
"@certd/basic": "^1.31.4",
"@certd/commercial-core": "^1.31.4",
"@certd/lib-huawei": "^1.31.4",
"@certd/lib-k8s": "^1.31.4",
"@certd/lib-server": "^1.31.4",
"@certd/midway-flyway-js": "^1.31.4",
"@certd/pipeline": "^1.31.4",
"@certd/plugin-cert": "^1.31.4",
"@certd/plugin-lib": "^1.31.4",
"@certd/plugin-plus": "^1.31.4",
"@certd/plus-core": "^1.31.4",
"@corsinvest/cv4pve-api-javascript": "^8.3.0",
"@huaweicloud/huaweicloud-sdk-cdn": "^3.1.120",
"@huaweicloud/huaweicloud-sdk-core": "^3.1.120",

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