Merge branch 'v2-dev' into v2-dev-buy

This commit is contained in:
xiaojunnuo
2025-08-25 23:22:39 +08:00
106 changed files with 2396 additions and 246 deletions

11
.editorconfig Normal file
View File

@@ -0,0 +1,11 @@
#
# http://editorconfig.org
#
root = true
[*]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true

View File

@@ -1,11 +1,11 @@
---
name: Plugin Apply
about: 请求支持新部署插件
about: 部署插件申请支持
title: "[Plugin] "
labels: feature
---
> > 感谢您支持certd请按如下规范提交issue
> > 感谢您支持certd请按如下规范提交issue
> 如果有条件,请尽量在[github上提交](https://github.com/certd/certd/issues)
# 新部署插件申请支持

View File

@@ -1,12 +1,12 @@
---
name: DNS Provider Apply
about: 请求支持新的域名提供商
about: 域名提供商申请支持
title: "[DNS] "
labels: feature
---
> 感谢您支持certd请按如下规范提交issue
> 感谢您支持certd请按如下规范提交issue
> 如果有条件,请尽量在[github上提交](https://github.com/certd/certd/issues)
# 新域名提供商支持申请
@@ -14,23 +14,23 @@ labels: feature
## 1. 基本信息
请填写如下内容:
1. 域名提供商名称:
1. 域名提供商名称:
2. 管理页面地址:
3. 是否有API接口接口地址
3. 是否有API接口接口地址
4. 如果没有API接口网页登录是否有验证码
4. 如果没有API接口网页登录是否有验证码
5. 是否可以提供测试账号?(如果可以请留下联系方式或者加作者好友)
## 2. 截图
## 2. 截图
`域名管理页面截图`

View File

@@ -1,12 +1,12 @@
---
name: Bug Report
about: 报告一个错误或问题
about: 错误或问题报告
title: "[BUG] "
labels: bug
---
> 感谢您支持certd请按如下规范提交issue
> 感谢您支持certd请按如下规范提交issue
> 如果有条件,请尽量在[github上提交](https://github.com/certd/certd/issues)
# bug提交

View File

@@ -1,15 +1,16 @@
---
name: Feature Request
about: 新需求、新特性
about: 新需求、新特性申请支持
title: "[Feature] "
labels: feature
---
> > 感谢您支持certd请按如下规范提交issue
> > 感谢您支持certd请按如下规范提交issue
> 如果有条件,请尽量在[github上提交](https://github.com/certd/certd/issues)
# 新需求申请
# 新特性申请
>注意:这里仅供如果是要申请新的部署插件,请提交插件申请
## 1. 需求描述,需求背景
`请在此处简要描述你所遇到的问题,必要时请贴出相关截图辅助理解`

View File

@@ -3,6 +3,36 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.17](https://github.com/certd/certd/compare/v1.36.16...v1.36.17) (2025-08-17)
### Bug Fixes
* 修复新部署的无法保存公共eab配置的bug ([d5dee75](https://github.com/certd/certd/commit/d5dee75df3bd635a597436e448b2de1407531f3a))
### Performance Improvements
* 阿里云 FC3.0 不在要求证书加密方式为旧版, 修复支持的协议类型可以正常选择 ([a34db74](https://github.com/certd/certd/commit/a34db7449eff6ad1dda01de673bf85579fa3865a))
* 部署到腾讯云cdn每个域名增加3每秒延迟 ([f7d43ad](https://github.com/certd/certd/commit/f7d43ad5af4663d4be369820a80d1fd9817ca4ab))
* 腾讯云关闭证书通知增加开关选项,在腾讯云授权里面 ([a77c777](https://github.com/certd/certd/commit/a77c777980dd38d97d983124eeed1596879bba95))
* 证书申请任务默认不发送申请成功通知 ([0283bd2](https://github.com/certd/certd/commit/0283bd2f978dbcd13d361129135e439dd9fbc180))
## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16)
### Bug Fixes
* 修复授权配置复制功能,无法复制已加密字段的问题 ([221e068](https://github.com/certd/certd/commit/221e068bac3af6cd5d1794f8cd4c2ec5c0bc3f45))
### Performance Improvements
* 百度云支持上传到证书托管,支持部署到负载均衡 ([798a48a](https://github.com/certd/certd/commit/798a48aa9686fd5d11cfffb6cd93eadfc40aacb3))
* 部署到百度cdn支持自动获取域名列表选择 ([4e432ed](https://github.com/certd/certd/commit/4e432ed03f4fb564e85a2f284ee26b58400b82f5))
* 验证码可重试次数设置为3次 ([1bdceee](https://github.com/certd/certd/commit/1bdceeecf4b5daecdd621a05a2596b6eb45ce8ea))
* 增加找回密码的验证码可重试次数 [@nicheng-he](https://github.com/nicheng-he) ([#496](https://github.com/certd/certd/issues/496)) ([fe03f99](https://github.com/certd/certd/commit/fe03f9942b5662fb90cad86da10782f5dc3603f5))
* 支持阿里云API网关 ([9e1e4ee](https://github.com/certd/certd/commit/9e1e4eeec2859759ca5b07834c9d24cf88a6ad33))
* 支持部署到金山云CDN ([dfa74a6](https://github.com/certd/certd/commit/dfa74a69f7cbb9009d3e20c7eecfa1b905a00cf0))
* 支持更新金山云cdn证书 ([462e22a](https://github.com/certd/certd/commit/462e22a3b0a94887462fe6aa68e4671a365e0737))
* 支持apisix证书部署 ([9b63fb4](https://github.com/certd/certd/commit/9b63fb4ee2c6b56139160c5bf63482dab0869c2b))
## [1.36.15](https://github.com/certd/certd/compare/v1.36.14...v1.36.15) (2025-08-07)
### Bug Fixes

View File

@@ -1 +1 @@
23:23
23:58

View File

@@ -11,6 +11,9 @@ services:
# ↓↓↓↓↓ -------------------------------------------------------- 数据库以及证书存储路径,默认存在宿主机的/data/certd/目录下,【您需要定时备份此目录,以保障数据容灾】
# 只要修改冒号前面的,冒号后面的/app/data不要动
- /data/certd:/app/data
# ↓↓↓↓↓ -------------------------------------------------------- 如果走时不准考虑挂载localtime文件
#- /etc/localtime:/etc/localtime
#- /etc/timezone:/etc/timezone
ports: # 端口映射
# ↓↓↓↓ ---------------------------------------------------------- 如果端口有冲突可以修改第一个7001为其他不冲突的端口号第二个7001不要动
- "7001:7001"
@@ -42,7 +45,7 @@ services:
# 设置环境变量即可自定义certd配置
# 配置项见: packages/ui/certd-server/src/config/config.default.ts
# 配置规则: certd_ + 配置项, 点号用_代替
# #↓↓↓↓ ----------------------------- 如果忘记管理员密码可以设置为true重启之后管理员密码将改成123456然后请及时修改回false
# #↓↓↓↓ ----------------------------- 如果忘记管理员密码可以设置为truedocker compose up -d 重建容器之后管理员密码将改成123456然后请及时修改回false
- certd_system_resetAdminPasswd=false
# 默认使用sqlite文件数据库如果需要使用其他数据库请设置以下环境变量

View File

@@ -3,6 +3,36 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.17](https://github.com/certd/certd/compare/v1.36.16...v1.36.17) (2025-08-17)
### Bug Fixes
* 修复新部署的无法保存公共eab配置的bug ([d5dee75](https://github.com/certd/certd/commit/d5dee75df3bd635a597436e448b2de1407531f3a))
### Performance Improvements
* 阿里云 FC3.0 不在要求证书加密方式为旧版, 修复支持的协议类型可以正常选择 ([a34db74](https://github.com/certd/certd/commit/a34db7449eff6ad1dda01de673bf85579fa3865a))
* 部署到腾讯云cdn每个域名增加3每秒延迟 ([f7d43ad](https://github.com/certd/certd/commit/f7d43ad5af4663d4be369820a80d1fd9817ca4ab))
* 腾讯云关闭证书通知增加开关选项,在腾讯云授权里面 ([a77c777](https://github.com/certd/certd/commit/a77c777980dd38d97d983124eeed1596879bba95))
* 证书申请任务默认不发送申请成功通知 ([0283bd2](https://github.com/certd/certd/commit/0283bd2f978dbcd13d361129135e439dd9fbc180))
## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16)
### Bug Fixes
* 修复授权配置复制功能,无法复制已加密字段的问题 ([221e068](https://github.com/certd/certd/commit/221e068bac3af6cd5d1794f8cd4c2ec5c0bc3f45))
### Performance Improvements
* 百度云支持上传到证书托管,支持部署到负载均衡 ([798a48a](https://github.com/certd/certd/commit/798a48aa9686fd5d11cfffb6cd93eadfc40aacb3))
* 部署到百度cdn支持自动获取域名列表选择 ([4e432ed](https://github.com/certd/certd/commit/4e432ed03f4fb564e85a2f284ee26b58400b82f5))
* 验证码可重试次数设置为3次 ([1bdceee](https://github.com/certd/certd/commit/1bdceeecf4b5daecdd621a05a2596b6eb45ce8ea))
* 增加找回密码的验证码可重试次数 [@nicheng-he](https://github.com/nicheng-he) ([#496](https://github.com/certd/certd/issues/496)) ([fe03f99](https://github.com/certd/certd/commit/fe03f9942b5662fb90cad86da10782f5dc3603f5))
* 支持阿里云API网关 ([9e1e4ee](https://github.com/certd/certd/commit/9e1e4eeec2859759ca5b07834c9d24cf88a6ad33))
* 支持部署到金山云CDN ([dfa74a6](https://github.com/certd/certd/commit/dfa74a69f7cbb9009d3e20c7eecfa1b905a00cf0))
* 支持更新金山云cdn证书 ([462e22a](https://github.com/certd/certd/commit/462e22a3b0a94887462fe6aa68e4671a365e0737))
* 支持apisix证书部署 ([9b63fb4](https://github.com/certd/certd/commit/9b63fb4ee2c6b56139160c5bf63482dab0869c2b))
## [1.36.15](https://github.com/certd/certd/compare/v1.36.14...v1.36.15) (2025-08-07)
### Bug Fixes

View File

@@ -65,8 +65,16 @@ networks:
docker logs -f --tail 200 certd
```
## 6. 容器内走时不准,或者时区不对
走时不准确,慢慢偏差越来越大
或者整个时区都不对
可以尝试挂载localtime文件
```yaml
volumes:
# ↓↓↓↓↓ -------------------- 如果走时不准请尝试挂载localtime文件
- /etc/localtime:/etc/localtime
- /etc/timezone:/etc/timezone
```

View File

@@ -9,5 +9,5 @@
}
},
"npmClient": "pnpm",
"version": "1.36.15"
"version": "1.36.17"
}

View File

@@ -31,7 +31,7 @@
"init": "lerna run build",
"init:dev": "lerna run build",
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:build": "npm run copylogs && vitepress build docs",
"docs:preview": "vitepress preview docs",
"pub": "echo 1"
},

View File

@@ -6,7 +6,7 @@ root = true
[*]
indent_style = space
indent_size = 4
indent_size = 2
trim_trailing_whitespace = true
[{*.yml,*.yaml}]

View File

@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.17](https://github.com/publishlab/node-acme-client/compare/v1.36.16...v1.36.17) (2025-08-17)
**Note:** Version bump only for package @certd/acme-client
## [1.36.16](https://github.com/publishlab/node-acme-client/compare/v1.36.15...v1.36.16) (2025-08-16)
### Performance Improvements
* 部署到百度cdn支持自动获取域名列表选择 ([4e432ed](https://github.com/publishlab/node-acme-client/commit/4e432ed03f4fb564e85a2f284ee26b58400b82f5))
## [1.36.15](https://github.com/publishlab/node-acme-client/compare/v1.36.14...v1.36.15) (2025-08-07)
**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.36.15",
"version": "1.36.17",
"type": "module",
"module": "scr/index.js",
"main": "src/index.js",
@@ -18,7 +18,7 @@
"types"
],
"dependencies": {
"@certd/basic": "^1.36.15",
"@certd/basic": "^1.36.17",
"@peculiar/x509": "^1.11.0",
"asn1js": "^3.0.5",
"axios": "^1.7.2",
@@ -69,5 +69,5 @@
"bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues"
},
"gitHead": "fb7341f1f7d05d05c5439a36594665e3855d6a00"
"gitHead": "831c325c6383ba0a6f2dfa7496451ec714784e93"
}

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.17](https://github.com/certd/certd/compare/v1.36.16...v1.36.17) (2025-08-17)
**Note:** Version bump only for package @certd/basic
## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16)
**Note:** Version bump only for package @certd/basic
## [1.36.15](https://github.com/certd/certd/compare/v1.36.14...v1.36.15) (2025-08-07)
**Note:** Version bump only for package @certd/basic

View File

@@ -1 +1 @@
23:18
23:53

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/basic",
"private": false,
"version": "1.36.15",
"version": "1.36.17",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -45,5 +45,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "fb7341f1f7d05d05c5439a36594665e3855d6a00"
"gitHead": "831c325c6383ba0a6f2dfa7496451ec714784e93"
}

View File

@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.17](https://github.com/certd/certd/compare/v1.36.16...v1.36.17) (2025-08-17)
**Note:** Version bump only for package @certd/pipeline
## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16)
### Performance Improvements
* 百度云支持上传到证书托管,支持部署到负载均衡 ([798a48a](https://github.com/certd/certd/commit/798a48aa9686fd5d11cfffb6cd93eadfc40aacb3))
* 支持部署到金山云CDN ([dfa74a6](https://github.com/certd/certd/commit/dfa74a69f7cbb9009d3e20c7eecfa1b905a00cf0))
## [1.36.15](https://github.com/certd/certd/compare/v1.36.14...v1.36.15) (2025-08-07)
**Note:** Version bump only for package @certd/pipeline

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/pipeline",
"private": false,
"version": "1.36.15",
"version": "1.36.17",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -17,8 +17,8 @@
"pub": "npm publish"
},
"dependencies": {
"@certd/basic": "^1.36.15",
"@certd/plus-core": "^1.36.15",
"@certd/basic": "^1.36.17",
"@certd/plus-core": "^1.36.17",
"dayjs": "^1.11.7",
"lodash-es": "^4.17.21",
"reflect-metadata": "^0.1.13"
@@ -44,5 +44,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "fb7341f1f7d05d05c5439a36594665e3855d6a00"
"gitHead": "831c325c6383ba0a6f2dfa7496451ec714784e93"
}

View File

@@ -11,7 +11,6 @@ export type PageSearch = {
// sortOrder?: "asc" | "desc";
};
export type PageRes = {
pageNo?: number;
pageSize?: number;

View File

@@ -27,6 +27,7 @@ export const pluginGroups = {
tencent: new PluginGroup("tencent", "腾讯云", 4, "svg:icon-tencentcloud"),
volcengine: new PluginGroup("volcengine", "火山引擎", 4, "svg:icon-volcengine"),
jdcloud: new PluginGroup("jdcloud", "京东云", 4, "svg:icon-jdcloud"),
baidu: new PluginGroup("baidu", "百度云", 4, "ant-design:baidu-outlined"),
qiniu: new PluginGroup("qiniu", "七牛云", 5, "svg:icon-qiniuyun"),
aws: new PluginGroup("aws", "亚马逊云", 6, "svg:icon-aws"),
other: new PluginGroup("other", "其他", 10, "clarity:plugin-line"),

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.17](https://github.com/certd/certd/compare/v1.36.16...v1.36.17) (2025-08-17)
**Note:** Version bump only for package @certd/lib-huawei
## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16)
**Note:** Version bump only for package @certd/lib-huawei
## [1.36.15](https://github.com/certd/certd/compare/v1.36.14...v1.36.15) (2025-08-07)
**Note:** Version bump only for package @certd/lib-huawei

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/lib-huawei",
"private": false,
"version": "1.36.15",
"version": "1.36.17",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
"types": "./dist/d/index.d.ts",
@@ -24,5 +24,5 @@
"prettier": "^2.8.8",
"tslib": "^2.8.1"
},
"gitHead": "fb7341f1f7d05d05c5439a36594665e3855d6a00"
"gitHead": "831c325c6383ba0a6f2dfa7496451ec714784e93"
}

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.17](https://github.com/certd/certd/compare/v1.36.16...v1.36.17) (2025-08-17)
**Note:** Version bump only for package @certd/lib-iframe
## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16)
**Note:** Version bump only for package @certd/lib-iframe
## [1.36.15](https://github.com/certd/certd/compare/v1.36.14...v1.36.15) (2025-08-07)
**Note:** Version bump only for package @certd/lib-iframe

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/lib-iframe",
"private": false,
"version": "1.36.15",
"version": "1.36.17",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -31,5 +31,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "fb7341f1f7d05d05c5439a36594665e3855d6a00"
"gitHead": "831c325c6383ba0a6f2dfa7496451ec714784e93"
}

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.17](https://github.com/certd/certd/compare/v1.36.16...v1.36.17) (2025-08-17)
**Note:** Version bump only for package @certd/jdcloud
## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16)
**Note:** Version bump only for package @certd/jdcloud
## [1.36.15](https://github.com/certd/certd/compare/v1.36.14...v1.36.15) (2025-08-07)
**Note:** Version bump only for package @certd/jdcloud

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/jdcloud",
"version": "1.36.15",
"version": "1.36.17",
"description": "jdcloud openApi sdk",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
@@ -61,5 +61,5 @@
"fetch"
]
},
"gitHead": "fb7341f1f7d05d05c5439a36594665e3855d6a00"
"gitHead": "831c325c6383ba0a6f2dfa7496451ec714784e93"
}

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.17](https://github.com/certd/certd/compare/v1.36.16...v1.36.17) (2025-08-17)
**Note:** Version bump only for package @certd/lib-k8s
## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16)
**Note:** Version bump only for package @certd/lib-k8s
## [1.36.15](https://github.com/certd/certd/compare/v1.36.14...v1.36.15) (2025-08-07)
**Note:** Version bump only for package @certd/lib-k8s

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/lib-k8s",
"private": false,
"version": "1.36.15",
"version": "1.36.17",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -17,7 +17,7 @@
"pub": "npm publish"
},
"dependencies": {
"@certd/basic": "^1.36.15",
"@certd/basic": "^1.36.17",
"@kubernetes/client-node": "0.21.0"
},
"devDependencies": {
@@ -32,5 +32,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "fb7341f1f7d05d05c5439a36594665e3855d6a00"
"gitHead": "831c325c6383ba0a6f2dfa7496451ec714784e93"
}

View File

@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.17](https://github.com/certd/certd/compare/v1.36.16...v1.36.17) (2025-08-17)
**Note:** Version bump only for package @certd/lib-server
## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16)
### Bug Fixes
* 修复授权配置复制功能,无法复制已加密字段的问题 ([221e068](https://github.com/certd/certd/commit/221e068bac3af6cd5d1794f8cd4c2ec5c0bc3f45))
## [1.36.15](https://github.com/certd/certd/compare/v1.36.14...v1.36.15) (2025-08-07)
**Note:** Version bump only for package @certd/lib-server

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/lib-server",
"version": "1.36.15",
"version": "1.36.17",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -27,10 +27,10 @@
],
"license": "AGPL",
"dependencies": {
"@certd/acme-client": "^1.36.15",
"@certd/basic": "^1.36.15",
"@certd/pipeline": "^1.36.15",
"@certd/plus-core": "^1.36.15",
"@certd/acme-client": "^1.36.17",
"@certd/basic": "^1.36.17",
"@certd/pipeline": "^1.36.17",
"@certd/plus-core": "^1.36.17",
"@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": "fb7341f1f7d05d05c5439a36594665e3855d6a00"
"gitHead": "831c325c6383ba0a6f2dfa7496451ec714784e93"
}

View File

@@ -34,7 +34,18 @@ export class AccessService extends BaseService<AccessEntity> {
}
async add(param) {
this.encryptSetting(param, null);
let oldEntity = null;
if (param._copyFrom){
oldEntity = await this.info(param._copyFrom);
if (oldEntity == null) {
throw new ValidateException('该授权配置不存在,请确认是否已被删除');
}
if (oldEntity.userId !== param.userId) {
throw new ValidateException('您无权查看该授权配置');
}
}
delete param._copyFrom
this.encryptSetting(param, oldEntity);
return await super.add(param);
}

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.17](https://github.com/certd/certd/compare/v1.36.16...v1.36.17) (2025-08-17)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.36.15](https://github.com/certd/certd/compare/v1.36.14...v1.36.15) (2025-08-07)
**Note:** Version bump only for package @certd/midway-flyway-js

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/midway-flyway-js",
"version": "1.36.15",
"version": "1.36.17",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -46,5 +46,5 @@
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "fb7341f1f7d05d05c5439a36594665e3855d6a00"
"gitHead": "831c325c6383ba0a6f2dfa7496451ec714784e93"
}

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.36.17](https://github.com/certd/certd/compare/v1.36.16...v1.36.17) (2025-08-17)
### Performance Improvements
* 证书申请任务默认不发送申请成功通知 ([0283bd2](https://github.com/certd/certd/commit/0283bd2f978dbcd13d361129135e439dd9fbc180))
## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16)
### Performance Improvements
* 百度云支持上传到证书托管,支持部署到负载均衡 ([798a48a](https://github.com/certd/certd/commit/798a48aa9686fd5d11cfffb6cd93eadfc40aacb3))
## [1.36.15](https://github.com/certd/certd/compare/v1.36.14...v1.36.15) (2025-08-07)
**Note:** Version bump only for package @certd/plugin-cert

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-cert",
"private": false,
"version": "1.36.15",
"version": "1.36.17",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -16,10 +16,10 @@
"pub": "npm publish"
},
"dependencies": {
"@certd/acme-client": "^1.36.15",
"@certd/basic": "^1.36.15",
"@certd/pipeline": "^1.36.15",
"@certd/plugin-lib": "^1.36.15",
"@certd/acme-client": "^1.36.17",
"@certd/basic": "^1.36.17",
"@certd/pipeline": "^1.36.17",
"@certd/plugin-lib": "^1.36.17",
"@google-cloud/publicca": "^1.3.0",
"dayjs": "^1.11.7",
"jszip": "^3.10.1",
@@ -43,5 +43,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "fb7341f1f7d05d05c5439a36594665e3855d6a00"
"gitHead": "831c325c6383ba0a6f2dfa7496451ec714784e93"
}

View File

@@ -48,6 +48,7 @@ export type CertInfo = {
der?: string;
jks?: string;
one?: string;
p7b?: string;
};
export type SSLProvider = "letsencrypt" | "google" | "zerossl";
export type PrivateKeyType = "rsa_1024" | "rsa_2048" | "rsa_3072" | "rsa_4096" | "ec_256" | "ec_384" | "ec_521";

View File

@@ -125,6 +125,10 @@ export abstract class CertApplyBaseConvertPlugin extends AbstractTaskPlugin {
cert.jks = res.jks;
}
if (cert.p7b == null && res.p7b) {
cert.p7b = res.p7b;
}
this.logger.info("转换证书格式成功");
} catch (e) {
this.logger.error("转换证书格式失败", e);
@@ -150,6 +154,7 @@ export abstract class CertApplyBaseConvertPlugin extends AbstractTaskPlugin {
zip.file("intermediate.crt", cert.ic);
zip.file("origin.crt", cert.oc);
zip.file("one.pem", cert.one);
zip.file("cert.p7b", cert.p7b);
if (cert.pfx) {
zip.file("cert.pfx", Buffer.from(cert.pfx, "base64"));
}

View File

@@ -33,7 +33,7 @@ export abstract class CertApplyBasePlugin extends CertApplyBaseConvertPlugin {
@TaskInput({
title: "证书申请成功通知",
value: true,
value: false,
component: {
name: "a-switch",
vModel: "checked",
@@ -41,7 +41,7 @@ export abstract class CertApplyBasePlugin extends CertApplyBaseConvertPlugin {
order: 100,
helper: "证书申请成功后是否发送通知,优先使用默认通知渠道",
})
successNotify = true;
successNotify = false;
// @TaskInput({
// title: "CsrInfo",

View File

@@ -17,6 +17,7 @@ export type CertReaderHandleContext = {
tmpIcPath?: string;
tmpJksPath?: string;
tmpOnePath?: string;
tmpP7bPath?: string;
};
export type CertReaderHandle = (ctx: CertReaderHandleContext) => Promise<void>;
export type HandleOpts = { logger: ILogger; handle: CertReaderHandle };
@@ -124,7 +125,7 @@ export class CertReader {
return domain;
}
saveToFile(type: "crt" | "key" | "pfx" | "der" | "oc" | "one" | "ic" | "jks", filepath?: string) {
saveToFile(type: "crt" | "key" | "pfx" | "der" | "oc" | "one" | "ic" | "jks" | "p7b", filepath?: string) {
if (!this.cert[type]) {
return;
}
@@ -138,7 +139,7 @@ export class CertReader {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
if (type === "crt" || type === "key" || type === "ic" || type === "oc" || type === "one") {
if (type === "crt" || type === "key" || type === "ic" || type === "oc" || type === "one" || type === "p7b") {
fs.writeFileSync(filepath, this.cert[type]);
} else {
fs.writeFileSync(filepath, Buffer.from(this.cert[type], "base64"));
@@ -157,17 +158,19 @@ export class CertReader {
const tmpDerPath = this.saveToFile("der");
const tmpJksPath = this.saveToFile("jks");
const tmpOnePath = this.saveToFile("one");
const tmpP7bPath = this.saveToFile("p7b");
logger.info("本地文件写入成功");
try {
return await opts.handle({
reader: this,
tmpCrtPath: tmpCrtPath,
tmpKeyPath: tmpKeyPath,
tmpPfxPath: tmpPfxPath,
tmpDerPath: tmpDerPath,
tmpIcPath: tmpIcPath,
tmpJksPath: tmpJksPath,
tmpOcPath: tmpOcPath,
tmpCrtPath,
tmpKeyPath,
tmpPfxPath,
tmpDerPath,
tmpIcPath,
tmpJksPath,
tmpOcPath,
tmpP7bPath,
tmpOnePath,
});
} catch (err) {
@@ -189,6 +192,7 @@ export class CertReader {
removeFile(tmpIcPath);
removeFile(tmpJksPath);
removeFile(tmpOnePath);
removeFile(tmpP7bPath);
}
}
@@ -211,4 +215,8 @@ export class CertReader {
}
return name + "_" + dayjs().format("YYYYMMDDHHmmssSSS");
}
static buildCertName(cert: any) {
return new CertReader(cert).buildCertName();
}
}

View File

@@ -18,11 +18,13 @@ export class CertConverter {
pfx: string;
der: string;
jks: string;
p7b: string;
}> {
const certReader = new CertReader(opts.cert);
let pfx: string;
let der: string;
let jks: string;
let p7b: string;
const handle = async (ctx: CertReaderHandleContext) => {
// 调用openssl 转pfx
pfx = await this.convertPfx(ctx, opts.pfxPassword, opts.pfxArgs);
@@ -31,6 +33,8 @@ export class CertConverter {
der = await this.convertDer(ctx);
jks = await this.convertJks(ctx, opts.pfxPassword);
p7b = await this.convertP7b(ctx);
};
await certReader.readCertFile({ logger: this.logger, handle });
@@ -39,6 +43,7 @@ export class CertConverter {
pfx,
der,
jks,
p7b,
};
}
@@ -95,6 +100,23 @@ export class CertConverter {
return derCert;
}
async convertP7b(opts: CertReaderHandleContext) {
const { tmpCrtPath } = opts;
const p7bPath = path.join(os.tmpdir(), "/certd/tmp/", Math.floor(Math.random() * 1000000) + `_cert.p7b`);
const dir = path.dirname(p7bPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
//openssl crl2pkcs7 -nocrl \
// -certfile your_domain.crt \
// -certfile intermediate.crt \
// -out chain.p7b
await this.exec(`openssl crl2pkcs7 -nocrl -certfile ${tmpCrtPath} -out ${p7bPath}`);
const fileBuffer = fs.readFileSync(p7bPath);
const p7bCert = fileBuffer.toString();
fs.unlinkSync(p7bPath);
return p7bCert;
}
async convertJks(opts: CertReaderHandleContext, pfxPassword = "") {
const jksPassword = pfxPassword || "123456";
try {
@@ -113,9 +135,7 @@ export class CertConverter {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
await this.exec(
`keytool -importkeystore -srckeystore ${p12Path} -srcstoretype PKCS12 -srcstorepass "${jksPassword}" -destkeystore ${jksPath} -deststoretype PKCS12 -deststorepass "${jksPassword}" `
);
await this.exec(`keytool -importkeystore -srckeystore ${p12Path} -srcstoretype PKCS12 -srcstorepass "${jksPassword}" -destkeystore ${jksPath} -deststoretype PKCS12 -deststorepass "${jksPassword}" `);
fs.unlinkSync(p12Path);
const fileBuffer = fs.readFileSync(jksPath);

View File

@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.17](https://github.com/certd/certd/compare/v1.36.16...v1.36.17) (2025-08-17)
### Performance Improvements
* 腾讯云关闭证书通知增加开关选项,在腾讯云授权里面 ([a77c777](https://github.com/certd/certd/commit/a77c777980dd38d97d983124eeed1596879bba95))
## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16)
**Note:** Version bump only for package @certd/plugin-lib
## [1.36.15](https://github.com/certd/certd/compare/v1.36.14...v1.36.15) (2025-08-07)
### Performance Improvements

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-lib",
"private": false,
"version": "1.36.15",
"version": "1.36.17",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -21,8 +21,8 @@
"@alicloud/pop-core": "^1.7.10",
"@alicloud/tea-util": "^1.4.10",
"@aws-sdk/client-s3": "^3.787.0",
"@certd/basic": "^1.36.15",
"@certd/pipeline": "^1.36.15",
"@certd/basic": "^1.36.17",
"@certd/pipeline": "^1.36.17",
"@kubernetes/client-node": "0.21.0",
"ali-oss": "^6.22.0",
"basic-ftp": "^5.0.5",
@@ -53,5 +53,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "fb7341f1f7d05d05c5439a36594665e3855d6a00"
"gitHead": "831c325c6383ba0a6f2dfa7496451ec714784e93"
}

View File

@@ -5,6 +5,7 @@ import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline";
title: "天翼云授权",
desc: "",
icon: "ant-design:aliyun-outlined",
order: 2,
})
export class CtyunAccess extends BaseAccess {
@AccessInput({

View File

@@ -6,6 +6,7 @@ import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
desc: "",
icon: "svg:icon-qiniuyun",
input: {},
order: 2,
})
export class QiniuAccess extends BaseAccess {
@AccessInput({

View File

@@ -5,6 +5,7 @@ import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
desc: "",
icon: "clarity:host-line",
input: {},
order: 0,
})
export class SshAccess extends BaseAccess {
@AccessInput({

View File

@@ -247,6 +247,9 @@ export class AsyncSsh2Client {
const err = this.convert(iconv, ret);
stdErr += err;
hasErrorLog = true;
if (err.includes("sudo: a password is required")) {
this.logger.warn("请配置sudo免密否则命令无法执行");
}
this.logger.error(`[${this.connConf.host}][error]: ` + err.trimEnd());
});
});

View File

@@ -4,6 +4,7 @@ import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline";
name: "tencent",
title: "腾讯云",
icon: "svg:icon-tencentcloud",
order: 0,
})
export class TencentAccess extends BaseAccess {
@AccessInput({
@@ -46,7 +47,21 @@ export class TencentAccess extends BaseAccess {
})
accountType: string;
@AccessInput({
title: "关闭证书过期通知",
value: true,
component: {
name: "a-switch",
vModel: "checked",
},
})
closeExpiresNotify: boolean = true;
isIntl() {
return this.accountType === "intl";
}
intlDomain() {
return this.isIntl() ? "intl." : "";
}
}

View File

@@ -0,0 +1,183 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TencentCosClient = void 0;
var basic_1 = require("@certd/basic");
var fs_1 = require("fs");
var TencentCosClient = /** @class */ (function () {
function TencentCosClient(opts) {
this.access = opts.access;
this.logger = opts.logger;
this.bucket = opts.bucket;
this.region = opts.region;
}
TencentCosClient.prototype.getCosClient = function () {
return __awaiter(this, void 0, void 0, function () {
var sdk, clientConfig;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, Promise.resolve().then(function () { return require("cos-nodejs-sdk-v5"); })];
case 1:
sdk = _a.sent();
clientConfig = {
SecretId: this.access.secretId,
SecretKey: this.access.secretKey,
};
return [2 /*return*/, new sdk.default(clientConfig)];
}
});
});
};
TencentCosClient.prototype.uploadFile = function (key, file) {
return __awaiter(this, void 0, void 0, function () {
var cos;
var _this = this;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, this.getCosClient()];
case 1:
cos = _a.sent();
return [2 /*return*/, (0, basic_1.safePromise)(function (resolve, reject) {
var readableStream = file;
if (typeof file === "string") {
readableStream = fs_1.default.createReadStream(file);
}
cos.putObject({
Bucket: _this.bucket /* 必须 */,
Region: _this.region /* 必须 */,
Key: key /* 必须 */,
Body: readableStream, // 上传文件对象
onProgress: function (progressData) {
console.log(JSON.stringify(progressData));
},
}, function (err, data) {
if (err) {
reject(err);
return;
}
resolve(data);
});
})];
}
});
});
};
TencentCosClient.prototype.removeFile = function (key) {
return __awaiter(this, void 0, void 0, function () {
var cos;
var _this = this;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, this.getCosClient()];
case 1:
cos = _a.sent();
return [2 /*return*/, (0, basic_1.safePromise)(function (resolve, reject) {
cos.deleteObject({
Bucket: _this.bucket,
Region: _this.region,
Key: key,
}, function (err, data) {
if (err) {
reject(err);
return;
}
resolve(data);
});
})];
}
});
});
};
TencentCosClient.prototype.downloadFile = function (key, savePath) {
return __awaiter(this, void 0, void 0, function () {
var cos, writeStream;
var _this = this;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, this.getCosClient()];
case 1:
cos = _a.sent();
writeStream = fs_1.default.createWriteStream(savePath);
return [2 /*return*/, (0, basic_1.safePromise)(function (resolve, reject) {
cos.getObject({
Bucket: _this.bucket,
Region: _this.region,
Key: key,
Output: writeStream,
}, function (err, data) {
if (err) {
reject(err);
return;
}
resolve(data);
});
})];
}
});
});
};
TencentCosClient.prototype.listDir = function (dirKey) {
return __awaiter(this, void 0, void 0, function () {
var cos;
var _this = this;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, this.getCosClient()];
case 1:
cos = _a.sent();
return [2 /*return*/, (0, basic_1.safePromise)(function (resolve, reject) {
cos.getBucket({
Bucket: _this.bucket,
Region: _this.region,
Prefix: dirKey,
MaxKeys: 1000,
}, function (err, data) {
if (err) {
reject(err);
return;
}
resolve(data.Contents);
});
})];
}
});
});
};
return TencentCosClient;
}());
exports.TencentCosClient = TencentCosClient;

View File

@@ -26,7 +26,7 @@ export class TencentSslClient {
region: this.region,
profile: {
httpProfile: {
endpoint: "ssl.tencentcloudapi.com",
endpoint: this.access.isIntl() ? "ssl.intl.tencentcloudapi.com" : "ssl.tencentcloudapi.com",
},
},
};
@@ -50,7 +50,10 @@ export class TencentSslClient {
const ret = await client.UploadCertificate(params);
this.checkRet(ret);
this.logger.info(`证书[${opts.certName}]上传成功tencentCertId=`, ret.CertificateId);
await this.switchCertNotify([ret.CertificateId], true);
if (this.access.closeExpiresNotify) {
await this.switchCertNotify([ret.CertificateId], true);
}
return ret.CertificateId;
}

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.36.17](https://github.com/certd/certd/compare/v1.36.16...v1.36.17) (2025-08-17)
**Note:** Version bump only for package @certd/ui-client
## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16)
### Bug Fixes
* 修复授权配置复制功能,无法复制已加密字段的问题 ([221e068](https://github.com/certd/certd/commit/221e068bac3af6cd5d1794f8cd4c2ec5c0bc3f45))
### Performance Improvements
* 增加找回密码的验证码可重试次数 [@nicheng-he](https://github.com/nicheng-he) ([#496](https://github.com/certd/certd/issues/496)) ([fe03f99](https://github.com/certd/certd/commit/fe03f9942b5662fb90cad86da10782f5dc3603f5))
* 支持阿里云API网关 ([9e1e4ee](https://github.com/certd/certd/commit/9e1e4eeec2859759ca5b07834c9d24cf88a6ad33))
* 支持部署到金山云CDN ([dfa74a6](https://github.com/certd/certd/commit/dfa74a69f7cbb9009d3e20c7eecfa1b905a00cf0))
## [1.36.15](https://github.com/certd/certd/compare/v1.36.14...v1.36.15) (2025-08-07)
### Performance Improvements

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/ui-client",
"version": "1.36.15",
"version": "1.36.17",
"private": true,
"scripts": {
"dev": "vite --open",
@@ -43,7 +43,7 @@
"@tailwindcss/typography": "^0.5.16",
"@tanstack/vue-store": "^0.7.0",
"@vee-validate/zod": "^4.15.0",
"@vue-js-cron/light": "^4.0.5",
"@certd/vue-js-cron-light": "^4.0.14",
"@vue/shared": "^3.5.13",
"@vueuse/core": "^10.11.0",
"ant-design-vue": "^4.2.6",
@@ -103,8 +103,8 @@
"zod-defaults": "^0.1.3"
},
"devDependencies": {
"@certd/lib-iframe": "^1.36.15",
"@certd/pipeline": "^1.36.15",
"@certd/lib-iframe": "^1.36.17",
"@certd/pipeline": "^1.36.17",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12",
@@ -120,7 +120,7 @@
"@vue/compiler-sfc": "^3.4.21",
"@vue/eslint-config-typescript": "^13.0.0",
"@vue/test-utils": "^2.4.6",
"autoprefixer": "^10.4.20",
"autoprefixer": "^10.4.21",
"caller-path": "^4.0.0",
"chai": "^5.1.0",
"dependency-cruiser": "^16.2.3",

View File

@@ -54,6 +54,36 @@
<div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xe8fb;</span>
<div class="name">social-foursquare</div>
<div class="code-name">&amp;#xe8fb;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe65a;</span>
<div class="name">ksyun-logo</div>
<div class="code-name">&amp;#xe65a;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe608;</span>
<div class="name">雨-copy</div>
<div class="code-name">&amp;#xe608;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe655;</span>
<div class="name">网宿</div>
<div class="code-name">&amp;#xe655;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe727;</span>
<div class="name">ai客服</div>
<div class="code-name">&amp;#xe727;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe6e4;</span>
<div class="name">cdn</div>
@@ -198,7 +228,7 @@
<pre><code class="language-css"
>@font-face {
font-family: 'iconfont';
src: url('iconfont.svg?t=1743267254898#iconfont') format('svg');
src: url('iconfont.svg?t=1754884110189#iconfont') format('svg');
}
</code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -224,6 +254,51 @@
<div class="content font-class">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont icon-four"></span>
<div class="name">
social-foursquare
</div>
<div class="code-name">.icon-four
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-ksyun"></span>
<div class="name">
ksyun-logo
</div>
<div class="code-name">.icon-ksyun
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-rainyun"></span>
<div class="name">
雨-copy
</div>
<div class="code-name">.icon-rainyun
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-wangsu"></span>
<div class="name">
网宿
</div>
<div class="code-name">.icon-wangsu
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-aikefu"></span>
<div class="name">
ai客服
</div>
<div class="code-name">.icon-aikefu
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-cdn"></span>
<div class="name">
@@ -440,6 +515,46 @@
<div class="content symbol">
<ul class="icon_lists dib-box">
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-four"></use>
</svg>
<div class="name">social-foursquare</div>
<div class="code-name">#icon-four</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-ksyun"></use>
</svg>
<div class="name">ksyun-logo</div>
<div class="code-name">#icon-ksyun</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-rainyun"></use>
</svg>
<div class="name">雨-copy</div>
<div class="code-name">#icon-rainyun</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-wangsu"></use>
</svg>
<div class="name">网宿</div>
<div class="code-name">#icon-wangsu</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-aikefu"></use>
</svg>
<div class="name">ai客服</div>
<div class="code-name">#icon-aikefu</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-cdn"></use>

View File

@@ -1,6 +1,6 @@
@font-face {
font-family: "iconfont"; /* Project id 4688792 */
src: url('iconfont.svg?t=1743267254898#iconfont') format('svg');
src: url('iconfont.svg?t=1754884110189#iconfont') format('svg');
}
.iconfont {
@@ -11,6 +11,26 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-four:before {
content: "\e8fb";
}
.icon-ksyun:before {
content: "\e65a";
}
.icon-rainyun:before {
content: "\e608";
}
.icon-wangsu:before {
content: "\e655";
}
.icon-aikefu:before {
content: "\e727";
}
.icon-cdn:before {
content: "\e6e4";
}

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,41 @@
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "544964",
"name": "social-foursquare",
"font_class": "four",
"unicode": "e8fb",
"unicode_decimal": 59643
},
{
"icon_id": "8567079",
"name": "ksyun-logo",
"font_class": "ksyun",
"unicode": "e65a",
"unicode_decimal": 58970
},
{
"icon_id": "42174864",
"name": "雨-copy",
"font_class": "rainyun",
"unicode": "e608",
"unicode_decimal": 58888
},
{
"icon_id": "14065547",
"name": "网宿",
"font_class": "wangsu",
"unicode": "e655",
"unicode_decimal": 58965
},
{
"icon_id": "41324539",
"name": "ai客服",
"font_class": "aikefu",
"unicode": "e727",
"unicode_decimal": 59175
},
{
"icon_id": "13592652",
"name": "cdn",

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 77 KiB

View File

@@ -4,8 +4,8 @@ import vip from "./vip-button/install.js";
import { CheckCircleOutlined, InfoCircleOutlined, UndoOutlined } from "@ant-design/icons-vue";
import CronEditor from "./cron-editor/index.vue";
import FoldBox from "./fold-box.vue";
import { CronLight } from "@vue-js-cron/light";
import "@vue-js-cron/light/dist/light.css";
import { CronLight } from "@certd/vue-js-cron-light";
import "@certd/vue-js-cron-light/dist/light.css";
import Plugins from "./plugins/index";
import LoadingButton from "./loading-button.vue";
import IconSelect from "./icon-select.vue";

View File

@@ -95,7 +95,7 @@ function install(app: App, options: any = {}) {
//不能用 !scope.value 否则switch组件设置为关之后就消失了
const { value, key, props } = scope;
return !value && key != "_index" && value != false;
return !value && key != "_index" && value != false && value != 0;
},
render() {
return "-";

View File

@@ -44,6 +44,20 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
rowHandle: {
width: 200,
buttons: {
copy: {
async click(ctx: any) {
const { row, index } = ctx;
await crudExpose.openCopy({
row: {
...row,
_copyFrom: row.id,
},
index: index,
});
},
},
},
},
columns: {
id: {

View File

@@ -41,7 +41,7 @@ const option = ref({
center: ["60%", "50%"],
name: "状态",
type: "pie",
radius: "80%",
radius: ["30%", "70%"],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 0,

View File

@@ -47,10 +47,6 @@
<div class="helper" v-html="t('certd.commonCnameHelper')"></div>
</a-form-item>
<a-form-item :label="t('certd.enableCommonSelfServicePasswordRetrieval')" :name="['public', 'selfServicePasswordRetrievalEnabled']">
<a-switch v-model:checked="formState.public.selfServicePasswordRetrievalEnabled" />
</a-form-item>
<a-form-item label=" " :colon="false" :wrapper-col="{ span: 8 }">
<a-button :loading="saveLoading" type="primary" html-type="submit">{{ t("certd.saveButton") }}</a-button>
</a-form-item>

View File

@@ -8,7 +8,7 @@
<a-form-item v-if="formState.yizhifu.enabled" label="易支付配置" :name="['yizhifu', 'accessId']" :required="true">
<access-selector v-model="formState.yizhifu.accessId" type="yizhifu" from="sys" />
<div class="helper">
<a href="https://certd.docmirror.cn/comm/payments/yizhifu.html">彩虹易支付配置帮助文档</a>
<a href="https://certd.docmirror.cn/guide/use/comm/payments/yizhifu.html">彩虹易支付配置帮助文档</a>
</div>
</a-form-item>
@@ -17,7 +17,7 @@
</a-form-item>
<a-form-item v-if="formState.alipay.enabled" label="支付宝配置" :name="['alipay', 'accessId']" :required="true">
<access-selector v-model="formState.alipay.accessId" type="alipay" from="sys" />
<div class="helper">需要开通电脑网站支付 <a href="https://certd.docmirror.cn/comm/payments/alipay.html">支付宝配置帮助文档</a></div>
<div class="helper">需要开通电脑网站支付 <a href="https://certd.docmirror.cn/guide/use/comm/payments/alipay.html">支付宝配置帮助文档</a></div>
</a-form-item>
<a-form-item label="微信支付" :name="['wxpay', 'enabled']" :required="true">
@@ -25,7 +25,7 @@
</a-form-item>
<a-form-item v-if="formState.wxpay.enabled" label="微信支付配置" :name="['wxpay', 'accessId']" :required="true">
<access-selector v-model="formState.wxpay.accessId" type="wxpay" from="sys" />
<div class="helper">需要开通Native支付 <a href="https://certd.docmirror.cn/comm/payments/wxpay.html">微信配置帮助文档</a></div>
<div class="helper">需要开通Native支付 <a href="https://certd.docmirror.cn/guide/use/comm/payments/wxpay.html">微信配置帮助文档</a></div>
</a-form-item>
<a-form-item label=" " :colon="false" :wrapper-col="{ span: 16 }">

View File

@@ -11,6 +11,9 @@
<a-form-item :label="t('certd.enableSelfRegistration')" :name="['public', 'registerEnabled']">
<a-switch v-model:checked="formState.public.registerEnabled" />
</a-form-item>
<a-form-item :label="t('certd.enableCommonSelfServicePasswordRetrieval')" :name="['public', 'selfServicePasswordRetrievalEnabled']">
<a-switch v-model:checked="formState.public.selfServicePasswordRetrievalEnabled" />
</a-form-item>
<a-form-item :label="t('certd.enableUserValidityPeriod')" :name="['public', 'userValidTimeEnabled']">
<div class="flex-o">
<a-switch v-model:checked="formState.public.userValidTimeEnabled" :disabled="!settingsStore.isPlus" />

View File

@@ -3,6 +3,29 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.17](https://github.com/certd/certd/compare/v1.36.16...v1.36.17) (2025-08-17)
### Bug Fixes
* 修复新部署的无法保存公共eab配置的bug ([d5dee75](https://github.com/certd/certd/commit/d5dee75df3bd635a597436e448b2de1407531f3a))
### Performance Improvements
* 阿里云 FC3.0 不在要求证书加密方式为旧版, 修复支持的协议类型可以正常选择 ([a34db74](https://github.com/certd/certd/commit/a34db7449eff6ad1dda01de673bf85579fa3865a))
* 部署到腾讯云cdn每个域名增加3每秒延迟 ([f7d43ad](https://github.com/certd/certd/commit/f7d43ad5af4663d4be369820a80d1fd9817ca4ab))
## [1.36.16](https://github.com/certd/certd/compare/v1.36.15...v1.36.16) (2025-08-16)
### Performance Improvements
* 百度云支持上传到证书托管,支持部署到负载均衡 ([798a48a](https://github.com/certd/certd/commit/798a48aa9686fd5d11cfffb6cd93eadfc40aacb3))
* 验证码可重试次数设置为3次 ([1bdceee](https://github.com/certd/certd/commit/1bdceeecf4b5daecdd621a05a2596b6eb45ce8ea))
* 增加找回密码的验证码可重试次数 [@nicheng-he](https://github.com/nicheng-he) ([#496](https://github.com/certd/certd/issues/496)) ([fe03f99](https://github.com/certd/certd/commit/fe03f9942b5662fb90cad86da10782f5dc3603f5))
* 支持阿里云API网关 ([9e1e4ee](https://github.com/certd/certd/commit/9e1e4eeec2859759ca5b07834c9d24cf88a6ad33))
* 支持部署到金山云CDN ([dfa74a6](https://github.com/certd/certd/commit/dfa74a69f7cbb9009d3e20c7eecfa1b905a00cf0))
* 支持更新金山云cdn证书 ([462e22a](https://github.com/certd/certd/commit/462e22a3b0a94887462fe6aa68e4671a365e0737))
* 支持apisix证书部署 ([9b63fb4](https://github.com/certd/certd/commit/9b63fb4ee2c6b56139160c5bf63482dab0869c2b))
## [1.36.15](https://github.com/certd/certd/compare/v1.36.14...v1.36.15) (2025-08-07)
### Bug Fixes

View File

@@ -10,6 +10,7 @@
```
```shell
npm run heap
```

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/ui-server",
"version": "1.36.15",
"version": "1.36.17",
"description": "fast-server base midway",
"private": true,
"type": "module",
@@ -42,20 +42,20 @@
"@aws-sdk/client-cloudfront": "^3.699.0",
"@aws-sdk/client-iam": "^3.699.0",
"@aws-sdk/client-s3": "^3.705.0",
"@certd/acme-client": "^1.36.15",
"@certd/basic": "^1.36.15",
"@certd/commercial-core": "^1.36.15",
"@certd/acme-client": "^1.36.17",
"@certd/basic": "^1.36.17",
"@certd/commercial-core": "^1.36.17",
"@certd/cv4pve-api-javascript": "^8.4.1",
"@certd/jdcloud": "^1.36.15",
"@certd/lib-huawei": "^1.36.15",
"@certd/lib-k8s": "^1.36.15",
"@certd/lib-server": "^1.36.15",
"@certd/midway-flyway-js": "^1.36.15",
"@certd/pipeline": "^1.36.15",
"@certd/plugin-cert": "^1.36.15",
"@certd/plugin-lib": "^1.36.15",
"@certd/plugin-plus": "^1.36.15",
"@certd/plus-core": "^1.36.15",
"@certd/jdcloud": "^1.36.17",
"@certd/lib-huawei": "^1.36.17",
"@certd/lib-k8s": "^1.36.17",
"@certd/lib-server": "^1.36.17",
"@certd/midway-flyway-js": "^1.36.17",
"@certd/pipeline": "^1.36.17",
"@certd/plugin-cert": "^1.36.17",
"@certd/plugin-lib": "^1.36.17",
"@certd/plugin-plus": "^1.36.17",
"@certd/plus-core": "^1.36.17",
"@huaweicloud/huaweicloud-sdk-cdn": "^3.1.120",
"@huaweicloud/huaweicloud-sdk-core": "^3.1.120",
"@koa/cors": "^5.0.0",
@@ -82,6 +82,7 @@
"cross-env": "^7.0.3",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.7",
"esdk-obs-nodejs": "^3.25.6",
"form-data": "^4.0.0",
"glob": "^11.0.0",
"https-proxy-agent": "^7.0.5",

View File

@@ -16,6 +16,9 @@ export class SmsCodeReq {
@Rule(RuleType.string().required().max(4))
imgCode: string;
@Rule(RuleType.string())
verificationType: string;
}
export class EmailCodeReq {
@@ -32,6 +35,9 @@ export class EmailCodeReq {
verificationType: string;
}
// 找回密码的验证码有效期
const FORGOT_PASSWORD_CODE_DURATION = 3
/**
*/
@Provide()
@@ -48,8 +54,18 @@ export class BasicController extends BaseController {
@Body(ALL)
body: SmsCodeReq
) {
const opts = {
verificationType: body.verificationType,
verificationCodeLength: undefined,
duration: undefined,
};
if(body?.verificationType === 'forgotPassword') {
opts.duration = FORGOT_PASSWORD_CODE_DURATION;
// opts.verificationCodeLength = 6; //部分厂商这里会设置参数长度这里就不改了
}
await this.codeService.checkCaptcha(body.randomStr, body.imgCode);
await this.codeService.sendSmsCode(body.phoneCode, body.mobile, body.randomStr);
await this.codeService.sendSmsCode(body.phoneCode, body.mobile, body.randomStr, opts);
return this.ok(null);
}
@@ -60,6 +76,7 @@ export class BasicController extends BaseController {
) {
const opts = {
verificationType: body.verificationType,
verificationCodeLength: undefined,
title: undefined,
content: undefined,
duration: undefined,
@@ -67,7 +84,8 @@ export class BasicController extends BaseController {
if(body?.verificationType === 'forgotPassword') {
opts.title = '找回密码';
opts.content = '验证码:${code}。您正在找回密码,请输入验证码并完成操作。如非本人操作请忽略';
opts.duration = 3;
opts.duration = FORGOT_PASSWORD_CODE_DURATION;
opts.verificationCodeLength = 6;
}
await this.codeService.checkCaptcha(body.randomStr, body.imgCode);

View File

@@ -28,6 +28,8 @@ export class LoginController extends BaseController {
if(!sysSettings.selfServicePasswordRetrievalEnabled) {
throw new CommonException('暂未开启自助找回');
}
// 找回密码的验证码允许错误次数
const errorNum = 5;
if(body.type === 'email') {
this.codeService.checkEmailCode({
@@ -35,6 +37,7 @@ export class LoginController extends BaseController {
email: body.input,
randomStr: body.randomStr,
validateCode: body.validateCode,
errorNum,
throwError: true,
});
} else if(body.type === 'mobile') {
@@ -44,6 +47,7 @@ export class LoginController extends BaseController {
randomStr: body.randomStr,
phoneCode: body.phoneCode,
smsCode: body.validateCode,
errorNum,
throwError: true,
});
} else {

View File

@@ -63,7 +63,8 @@ export class CodeService {
randomStr: string,
opts?: {
duration?: number,
verificationType?: string
verificationType?: string,
verificationCodeLength?: number,
},
) {
if (!mobile) {
@@ -73,7 +74,8 @@ export class CodeService {
throw new Error('randomStr不能为空');
}
const duration = Math.max(Math.floor(Math.min(opts?.duration || 5, 15)), 1);
const verificationCodeLength = Math.floor(Math.max(Math.min(opts?.verificationCodeLength || 4, 8), 4));
const duration = Math.floor(Math.max(Math.min(opts?.duration || 5, 15), 1));
const sysSettings = await this.sysSettingsService.getPrivateSettings();
if (!sysSettings.sms?.config?.accessId) {
@@ -87,7 +89,7 @@ export class CodeService {
accessService: accessGetter,
config: smsConfig,
});
const smsCode = randomNumber(4);
const smsCode = randomNumber(verificationCodeLength);
await sender.sendSmsCode({
mobile,
code: smsCode,
@@ -114,7 +116,8 @@ export class CodeService {
title?: string,
content?: string,
duration?: number,
verificationType?: string
verificationType?: string,
verificationCodeLength?: number,
},
) {
if (!email) {
@@ -132,8 +135,10 @@ export class CodeService {
}
}
const code = randomNumber(4);
const duration = Math.max(Math.floor(Math.min(opts?.duration || 5, 15)), 1);
const verificationCodeLength = Math.floor(Math.max(Math.min(opts?.verificationCodeLength || 4, 8), 4));
const duration = Math.floor(Math.max(Math.min(opts?.duration || 5, 15), 1));
const code = randomNumber(verificationCodeLength);
const title = `${siteTitle}${!!opts?.title ? opts.title : '验证码'}`;
const content = !!opts.content ? this.compile(opts.content)({code, duration}) : `您的验证码是${code},请勿泄露`;
@@ -154,12 +159,12 @@ export class CodeService {
/**
* checkSms
*/
async checkSmsCode(opts: { mobile: string; phoneCode: string; smsCode: string; randomStr: string; verificationType?: string; throwError: boolean }) {
async checkSmsCode(opts: { mobile: string; phoneCode: string; smsCode: string; randomStr: string; verificationType?: string; throwError: boolean; errorNum?: number }) {
const key = this.buildSmsCodeKey(opts.phoneCode, opts.mobile, opts.randomStr, opts.verificationType);
if (isDev()) {
return true;
}
return this.checkValidateCode(key, opts.smsCode, opts.throwError);
return this.checkValidateCode(key, opts.smsCode, opts.throwError, opts.errorNum);
}
buildSmsCodeKey(phoneCode: string, mobile: string, randomStr: string, verificationType?: string) {
@@ -169,22 +174,38 @@ export class CodeService {
buildEmailCodeKey(email: string, randomStr: string, verificationType?: string) {
return ['email', verificationType, email, randomStr].filter(item => !!item).join(':');
}
checkValidateCode(key: string, userCode: string, throwError = true) {
checkValidateCode(key: string, userCode: string, throwError = true, errorNum = 3) {
// 记录异常次数key
const err_num_key = key + ':err_num';
//验证图片验证码
const code = cache.get(key);
if (code == null || code !== userCode) {
let maxRetryCount = false;
if (!!code && errorNum > 0) {
const err_num = cache.get(err_num_key) || 0
if(err_num >= errorNum - 1) {
maxRetryCount = true;
cache.delete(key);
cache.delete(err_num_key);
} else {
cache.set(err_num_key, err_num + 1, {
ttl: 30 * 60 * 1000
});
}
}
if (throwError) {
throw new CodeErrorException('验证码错误');
throw new CodeErrorException(!maxRetryCount ? '验证码错误': '验证码错误请获取新的验证码');
}
return false;
}
cache.delete(key);
cache.delete(err_num_key);
return true;
}
checkEmailCode(opts: { randomStr: string; validateCode: string; email: string; verificationType?: string; throwError: boolean }) {
checkEmailCode(opts: { randomStr: string; validateCode: string; email: string; verificationType?: string; throwError: boolean; errorNum?: number }) {
const key = this.buildEmailCodeKey(opts.email, opts.randomStr, opts.verificationType);
return this.checkValidateCode(key, opts.validateCode, opts.throwError);
return this.checkValidateCode(key, opts.validateCode, opts.throwError, opts.errorNum);
}
compile(templateString: string) {

View File

@@ -31,3 +31,5 @@ export * from './plugin-namesilo/index.js'
export * from './plugin-proxmox/index.js'
export * from './plugin-wangsu/index.js'
export * from './plugin-admin/index.js'
export * from './plugin-ksyun/index.js'
export * from './plugin-apisix/index.js'

View File

@@ -0,0 +1,228 @@
import {AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput} from '@certd/pipeline';
import {AliyunAccess, createCertDomainGetterInputDefine, createRemoteSelectInputDefine} from "@certd/plugin-lib";
import {CertApplyPluginNames, CertInfo} from '@certd/plugin-cert';
import {optionsUtils} from "@certd/basic/dist/utils/util.options.js";
@IsTaskPlugin({
name: 'DeployCertToAliyunApiGateway',
title: '阿里云-部署证书至API网关',
icon: 'svg:icon-aliyun',
group: pluginGroups.aliyun.key,
desc: '自动部署域名证书至阿里云API网关APIGateway',
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed,
},
},
})
export class DeployCertToAliyunApiGateway extends AbstractTaskPlugin {
@TaskInput({
title: '域名证书',
helper: '请选择前置任务输出的域名证书',
component: {
name: 'output-selector',
from: [...CertApplyPluginNames],
},
required: true,
})
cert!: CertInfo;
@TaskInput(createCertDomainGetterInputDefine({ props: { required: false } }))
certDomains!: string[];
@TaskInput({
title: 'Access授权',
helper: '阿里云授权AccessKeyId、AccessKeySecret',
component: {
name: 'access-selector',
type: 'aliyun',
},
required: true,
})
accessId!: string;
@TaskInput({
title: '证书名称',
helper: '上传后将以此名称作为前缀备注',
})
certName!: string;
@TaskInput(
createRemoteSelectInputDefine({
title: '区域',
helper: '请选择区域',
action: DeployCertToAliyunApiGateway.prototype.onGetRegionList.name,
watches: ['certDomains', 'accessId'],
required: true,
component:{
name:"remote-auto-complete"
}
})
)
regionEndpoint!: string;
@TaskInput(
createRemoteSelectInputDefine({
title: 'API分组',
helper: '请选择API分组',
action: DeployCertToAliyunApiGateway.prototype.onGetGroupList.name,
watches: ['regionEndpoint', 'accessId'],
required: true,
component:{
name:"remote-auto-complete"
}
})
)
groupId!: string;
@TaskInput(
createRemoteSelectInputDefine({
title: '绑定域名',
helper: '在API分组上配置的绑定域名',
action: DeployCertToAliyunApiGateway.prototype.onGetDomainList.name,
watches: ['groupId','regionEndpoint', 'accessId'],
required: true,
})
)
customDomains!: string[];
async onInstance() {}
async execute(): Promise<void> {
this.logger.info('开始部署证书到阿里云Api网关');
if(!this.customDomains){
throw new Error('您还未选择域名');
}
const access = await this.getAccess<AliyunAccess>(this.accessId);
const client = access.getClient(this.regionEndpoint)
for (const domainName of this.customDomains ) {
this.logger.info(`[${domainName}]开始部署`)
await this.updateCert(client, domainName);
this.logger.info(`[${domainName}]部署成功`)
}
this.logger.info('部署完成');
}
async updateCert(client: any, domainName: string) {
const ret = await client.doRequest({
// 接口名称
action: "SetDomainCertificate",
// 接口版本
version: "2016-07-14",
data:{
query:{
GroupId: this.groupId,
DomainName: domainName,
CertificateName: this.buildCertName(domainName),
CertificateBody: this.cert.crt,
CertificatePrivateKey: this.cert.key
}
}
})
this.logger.info(`设置${domainName}证书成功:`, ret.RequestId);
}
async onGetGroupList(data: any) {
if (!this.accessId) {
throw new Error('请选择Access授权');
}
if (!this.regionEndpoint) {
throw new Error('请选择区域');
}
const access = await this.getAccess<AliyunAccess>(this.accessId);
const client = access.getClient(this.regionEndpoint)
const res =await client.doRequest({
// 接口名称
action: "DescribeApiGroups",
// 接口版本
version: "2016-07-14",
data:{}
})
const list = res?.ApiGroupAttributes?.ApiGroupAttribute;
if (!list || list.length === 0) {
throw new Error('没有数据您可以手动输入API网关ID');
}
return list.map((item: any) => {
return {
value: item.GroupId,
label: `${item.GroupName}<${item.GroupId}>`,
};
});
}
async onGetDomainList(data: any) {
if (!this.accessId) {
throw new Error('请选择Access授权');
}
if (!this.regionEndpoint) {
throw new Error('请选择区域');
}
if (!this.groupId) {
throw new Error('请选择分组');
}
const access = await this.getAccess<AliyunAccess>(this.accessId);
const client = access.getClient(this.regionEndpoint)
const res =await client.doRequest({
// 接口名称
action: "DescribeApiGroup",
// 接口版本
version: "2016-07-14",
data:{
query:{
GroupId: this.groupId
}
}
})
const list = res?.CustomDomains?.DomainItem;
if (!list || list.length === 0) {
throw new Error('没有数据,您可以手动输入');
}
const options = list.map((item: any) => {
return {
value: item.DomainName,
label: `${item.DomainName}<${item.CertificateName}>`,
domain: item.DomainName,
};
});
return optionsUtils.buildGroupOptions(options, this.certDomains);
}
async onGetRegionList(data: any) {
if (!this.accessId) {
throw new Error('请选择Access授权');
}
const access = await this.getAccess<AliyunAccess>(this.accessId);
const client = access.getClient("apigateway.cn-hangzhou.aliyuncs.com")
const res =await client.doRequest({
// 接口名称
action: "DescribeRegions",
// 接口版本
version: "2016-07-14",
data:{}
})
const list = res.Regions.Region ;
if (!list || list.length === 0) {
throw new Error('没有数据,您可以手动输入');
}
return list.map((item: any) => {
return {
value: item.RegionEndpoint,
label: item.LocalName,
endpoint: item.RegionEndpoint,
regionId: item.RegionId
};
});
}
}
new DeployCertToAliyunApiGateway();

View File

@@ -79,10 +79,10 @@ export class DeployCertToAliyunDCDN extends AbstractTaskPlugin {
this.domainName = [this.domainName];
}
for (const domainName of this.domainName ) {
this.logger.info(`[${this.domainName}]开始部署`)
this.logger.info(`[${domainName}]开始部署`)
const params = await this.buildParams(domainName);
await this.doRequest(client, params);
this.logger.info(`[${this.domainName}]部署成功`)
this.logger.info(`[${domainName}]部署成功`)
}
this.logger.info('部署完成');

View File

@@ -1,13 +1,17 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
import { CertApplyPluginNames, CertInfo, CertReader } from "@certd/plugin-cert";
import { AliyunAccess, createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
import fs from "fs";
import path from "path";
import { tmpdir } from "node:os";
import { sp } from "@certd/basic";
@IsTaskPlugin({
name: 'AliyunDeployCertToFC',
title: '阿里云-部署至阿里云FC(3.0)',
icon: 'svg:icon-aliyun',
group: pluginGroups.aliyun.key,
desc: '部署证书到阿里云函数计算FC3.0,【注意】证书的加密算法必须选择【pkcs1旧版】',
desc: '部署证书到阿里云函数计算FC3.0',
needPlus: false,
default: {
strategy: {
@@ -89,7 +93,7 @@ export class AliyunDeployCertToFC extends AbstractTaskPlugin {
@TaskInput(
createRemoteSelectInputDefine({
title: 'FC域名',
helper: "请选择要部署证书的域名\n【注意】证书的加密算法必须选择【pkcs1旧版】否则会报'private key' has to be in PEM format错误",
helper: "请选择要部署证书的域名",
typeName: 'AliyunDeployCertToFC',
action: AliyunDeployCertToFC.prototype.onGetDomainList.name,
watches: ['accessId', 'regionId'],
@@ -99,9 +103,10 @@ export class AliyunDeployCertToFC extends AbstractTaskPlugin {
@TaskInput({
title: '域名支持的协议类型',
value: '',
component: {
name: 'a-select',
value: '',
vModel:"value",
options: [
{ value: '', label: '保持原样适用于原来已经开启了HTTPS' },
{ value: 'HTTPS', label: '仅HTTPS' },
@@ -113,6 +118,13 @@ export class AliyunDeployCertToFC extends AbstractTaskPlugin {
async onInstance() {}
async exec(cmd: string) {
process.env.LANG = "zh_CN.GBK";
await sp.spawn({
cmd: cmd,
logger: this.logger,
});
}
async execute(): Promise<void> {
this.logger.info('开始部署证书到阿里云');
const access = await this.getAccess<AliyunAccess>(this.accessId);
@@ -121,6 +133,32 @@ export class AliyunDeployCertToFC extends AbstractTaskPlugin {
const $Util = await import('@alicloud/tea-util');
const $OpenApi = await import('@alicloud/openapi-client');
let privateKey = this.cert.key
try{
// openssl rsa -in private_key.pem -out private_key_pkcs1.pem
const tempDir = path.join(tmpdir(), "certd");
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
const keyFileName = this.ctx.utils.id.randomNumber(10);
const tempPem = `${tempDir}/${keyFileName}.pem`;
const tempPkcs1Pem =`${tempDir}/${keyFileName}_pkcs1.pem`;
fs.writeFileSync(tempPem, this.cert.key);
const oldPfxCmd = `openssl rsa -in ${tempPem} -traditional -out ${tempPkcs1Pem}`;
await this.exec(oldPfxCmd);
const fileBuffer = fs.readFileSync(tempPkcs1Pem);
privateKey = fileBuffer.toString();
fs.unlinkSync(tempPem);
fs.unlinkSync(tempPkcs1Pem);
}catch (e) {
this.logger.warn("私钥转换为PKCS#1格式失败",e);
}
for (const domainName of this.fcDomains) {
const params = new $OpenApi.Params({
// 接口名称
@@ -147,7 +185,7 @@ export class AliyunDeployCertToFC extends AbstractTaskPlugin {
certConfig: {
certName: certName,
certificate: this.cert.crt,
privateKey: this.cert.key,
privateKey: privateKey,
},
};
if (this.protocol) {

View File

@@ -9,3 +9,4 @@ export * from './deploy-to-slb/index.js';
export * from './deploy-to-fc/index.js';
export * from './deploy-to-esa/index.js';
export * from './deploy-to-vod/index.js';
export * from './deploy-to-apigateway/index.js';

View File

@@ -0,0 +1,104 @@
import {AccessInput, BaseAccess, IsAccess} from "@certd/pipeline";
import {HttpRequestConfig} from "@certd/basic";
import {CertInfo, CertReader} from "@certd/plugin-cert";
/**
*/
@IsAccess({
name: "apisix",
title: "APISIX授权",
desc: "",
icon: "svg:icon-ksyun"
})
export class ApisixAccess extends BaseAccess {
@AccessInput({
title: "Apisix管理地址",
component: {
placeholder: "http://192.168.11.11:9180",
},
required: true,
})
endpoint = '';
@AccessInput({
title: 'ApiKey',
component: {
placeholder: 'ApiKey',
},
helper: "[参考文档](https://apisix.apache.org/docs/apisix/admin-api/#using-environment-variables)在config中配置admin apiKey",
required: true,
encrypt: true,
})
apiKey = '';
@AccessInput({
title: "测试",
component: {
name: "api-test",
action: "TestRequest"
},
helper: "点击测试接口是否正常"
})
testRequest = true;
async onTestRequest() {
await this.getCertList();
return "ok"
}
async getCertList(){
const req = {
url :"/apisix/admin/ssls",
method: "get",
}
return await this.doRequest(req);
}
async createCert(opts:{cert:CertInfo}){
const certReader = new CertReader(opts.cert)
const req = {
url :"/apisix/admin/ssls",
method: "post",
data:{
cert: opts.cert.crt,
key: opts.cert.key,
snis: certReader.getAllDomains()
}
}
return await this.doRequest(req);
}
async updateCert (opts:{cert:CertInfo,id:string}){
const certReader = new CertReader(opts.cert)
const req = {
url :`/apisix/admin/ssls/${opts.id}`,
method: "put",
data:{
cert: opts.cert.crt,
key: opts.cert.key,
snis: certReader.getAllDomains()
}
}
return await this.doRequest(req);
}
async doRequest(req: HttpRequestConfig){
const headers = {
"X-API-KEY": this.apiKey,
...req.headers
};
return await this.ctx.http.request({
headers,
baseURL: this.endpoint,
...req,
logRes: true,
});
}
}
new ApisixAccess();

View File

@@ -0,0 +1,2 @@
export * from "./plugins/index.js";
export * from "./access.js";

View File

@@ -0,0 +1 @@
import "./plugin-refresh-cert.js"

View File

@@ -0,0 +1,115 @@
import {IsTaskPlugin, PageSearch, pluginGroups, RunStrategy, TaskInput} from "@certd/pipeline";
import {CertApplyPluginNames, CertInfo} from "@certd/plugin-cert";
import {createCertDomainGetterInputDefine, createRemoteSelectInputDefine} from "@certd/plugin-lib";
import {ApisixAccess} from "../access.js";
import {AbstractPlusTaskPlugin} from "@certd/plugin-plus";
@IsTaskPlugin({
//命名规范,插件类型+功能就是目录plugin-demo中的demo大写字母开头驼峰命名
name: "ApisixRefreshCert",
title: "APISIX-更新证书",
desc: "自动更新APISIX证书",
icon: "svg:icon-lucky",
//插件分组
group: pluginGroups.cdn.key,
needPlus: true,
default: {
//默认值配置照抄即可
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed
}
}
})
//类名规范跟上面插件名称name一致
export class ApisixRefreshCDNCert extends AbstractPlusTaskPlugin {
//证书选择,此项必须要有
@TaskInput({
title: "域名证书",
helper: "请选择前置任务输出的域名证书",
component: {
name: "output-selector",
from: [...CertApplyPluginNames]
}
// required: true, // 必填
})
cert!: CertInfo;
@TaskInput(createCertDomainGetterInputDefine({ props: { required: false } }))
certDomains!: string[];
//授权选择框
@TaskInput({
title: "Apisix授权",
component: {
name: "access-selector",
type: "apisix" //固定授权类型
},
required: true //必填
})
accessId!: string;
//
@TaskInput(
createRemoteSelectInputDefine({
title: "证书Id",
helper: "要更新的证书id如果这里没有请先给手动绑定一次证书",
action: ApisixRefreshCDNCert.prototype.onGetCertList.name,
pager: false,
search: false
})
)
certList!: string[];
//插件实例化时执行的方法
async onInstance() {
}
//插件执行方法
async execute(): Promise<void> {
const access = await this.getAccess<ApisixAccess>(this.accessId);
// await access.createCert({cert:this.cert})
for (const certId of this.certList) {
this.logger.info(`----------- 开始更新证书:${certId}`);
await access.updateCert({
id: certId,
cert: this.cert
});
this.logger.info(`----------- 更新证书${certId}成功`);
}
this.logger.info("部署完成");
}
async onGetCertList(data: PageSearch = {}) {
const access = await this.getAccess<ApisixAccess>(this.accessId);
const res = await access.getCertList()
const list = res.list
if (!list || list.length === 0) {
throw new Error("没有找到证书你可以直接手动输入id如果id不存在将自动创建");
}
/**
* certificate-id
* name
* dns-names
*/
const options = list.map((item: any) => {
return {
label: `${item.value.snis[0]}<${item.value.id}>`,
value: item.value.id,
domain: item.value.snis
};
});
return {
list: this.ctx.utils.options.buildGroupOptions(options, this.certDomains),
};
}
}
//实例化一下,注册插件
new ApisixRefreshCDNCert();

View File

@@ -16,6 +16,7 @@ type DemoRecord = {
icon: 'clarity:plugin-line',
// 这里是对应的云平台的access类型名称
accessType: 'demo',
order:99,
})
export class DemoDnsProvider extends AbstractDnsProvider<DemoRecord> {
access!: DemoAccess;

View File

@@ -39,6 +39,7 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
{ value: 'der', label: 'der一般用于Apache' },
{ value: 'jks', label: 'jks一般用于JAVA应用' },
{ value: 'one', label: '证书私钥一体crt+key简单合并为一个pem文件' },
{ value: 'p7b', label: 'p7b格式' },
],
},
required: true,
@@ -71,7 +72,7 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
mergeScript: `
return {
show: ctx.compute(({form})=>{
return form.certType === 'pem';
return form.certType === 'pem' || form.certType === 'p7b' ;
})
}
`,
@@ -169,6 +170,24 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
})
onePath!: string;
@TaskInput({
title: 'p7b证书保存路径',
helper: '填写应用原本的证书保存路径,路径要包含证书文件名,例如:/tmp/domain_cert.p7b',
component: {
placeholder: '/root/deploy/app/domain_cert.p7b',
},
mergeScript: `
return {
show: ctx.compute(({form})=>{
return form.certType === 'p7b';
})
}
`,
required: true,
rules: [{ type: 'filepath' }],
})
p7bPath!: string;
@TaskInput({
title: '主机登录配置',
helper: 'access授权',
@@ -277,12 +296,17 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
})
hostOnePath!: string;
@TaskOutput({
title: 'p7b证书保存路径',
})
hostP7bPath!: string;
async onInstance() {}
async execute(): Promise<void> {
const { cert, accessId } = this;
let { crtPath, keyPath, icPath, pfxPath, derPath, jksPath, onePath } = this;
let { crtPath, keyPath, icPath, pfxPath, derPath, jksPath, onePath,p7bPath } = this;
const certReader = new CertReader(cert);
const executeCmd = async ( script:string)=> {
@@ -308,6 +332,7 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
env['HOST_DER_PATH'] = this.hostDerPath || '';
env['HOST_JKS_PATH'] = this.hostJksPath || '';
env['HOST_ONE_PATH'] = this.hostOnePath || '';
env['HOST_P7B_PATH'] = this.hostOnePath || '';
}
const scripts = script.split('\n');
@@ -320,7 +345,7 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
}
const handle = async (opts: CertReaderHandleContext) => {
const { tmpCrtPath, tmpKeyPath, tmpDerPath, tmpJksPath, tmpPfxPath, tmpIcPath, tmpOnePath } = opts;
const { tmpCrtPath, tmpKeyPath, tmpDerPath, tmpJksPath, tmpPfxPath, tmpIcPath, tmpOnePath ,tmpP7bPath} = opts;
if (accessId == null) {
this.logger.error('复制到当前主机功能已迁移到 “复制到本机”插件,请换成复制到本机插件');
@@ -392,6 +417,14 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
remotePath: this.onePath,
});
}
if (this.p7bPath) {
this.logger.info(`上传p7b证书到主机${this.p7bPath}`);
p7bPath = this.p7bPath.trim();
transports.push({
localPath: tmpP7bPath,
remotePath: this.p7bPath,
});
}
this.logger.info('开始上传文件到服务器');
await sshClient.uploadFiles({
@@ -410,6 +443,7 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
this.hostDerPath = derPath;
this.hostJksPath = jksPath;
this.hostOnePath = onePath;
this.hostP7bPath = p7bPath;
};
//执行前置命令

View File

@@ -5,6 +5,7 @@ import { IsAccess, AccessInput, BaseAccess } from '@certd/pipeline';
title: '华为云授权',
desc: '',
icon: 'svg:icon-huawei',
order: 0,
})
export class HuaweiAccess extends BaseAccess {
@AccessInput({

View File

@@ -0,0 +1,185 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
import { HuaweiAccess } from "../../access/index.js";
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
@IsTaskPlugin({
name: 'HauweiDeployCertToOBS',
title: '华为云-部署证书至OBS',
icon: 'svg:icon-huawei',
group: pluginGroups.huawei.key,
desc: '',
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed,
},
},
})
export class HauweiDeployCertToOBS extends AbstractTaskPlugin {
@TaskInput({
title: '域名证书',
helper: '请选择前置任务输出的域名证书\n如果你选择使用ccm证书ID则需要在[域名管理页面右上角开启SCM授权](https://console.huaweicloud.com/cdn/#/cdn/domain)',
component: {
name: 'output-selector',
from: [...CertApplyPluginNames,'HauweiUploadToCCM'],
},
required: true,
})
cert!: CertInfo | string;
@TaskInput(createCertDomainGetterInputDefine({ props: { required: false } }))
certDomains!: string[];
@TaskInput({
title: 'Access授权',
helper: '华为云授权AccessKeyId、AccessKeySecret',
component: {
name: 'access-selector',
type: 'huawei',
},
required: true,
})
accessId!: string;
@TaskInput(
createRemoteSelectInputDefine({
title: '存储桶',
helper: '请选择存储桶',
action: HauweiDeployCertToOBS.prototype.onGetBucketList.name,
})
)
bucketList!: string[];
@TaskInput(
createRemoteSelectInputDefine({
title: '自定义域名',
helper: '请选择自定义域名',
action: HauweiDeployCertToOBS.prototype.onGetDomainList.name,
watches: ['bucketList'],
})
)
domainList!: string[];
async execute(): Promise<void> {
if (!this.cert) {
throw new Error('域名证书不能为空');
}
this.logger.info('开始部署证书到华为云obs');
for (const domainStr of this.domainList) {
const [location, bucket,domain] = domainStr.split('_');
await this.setDomainCert({
location,
bucket,
domain,
cert: this.cert
});
}
this.logger.info('部署证书到华为云cdn完成');
}
checkRet(ret: any){
if (ret?.CommonMsg?.Status>300){
throw new Error(`${ret?.CommonMsg?.Code}${ret?.CommonMsg?.Message}`);
}
}
async getObsClient(opts:{region?:string,bucket?:string} = {}) {
const { region,bucket } = opts;
const regionStr = region? `${region}.`: 'cn-north-4.';
const bucketStr = bucket? `${bucket}.` : '';
const access = await this.getAccess<HuaweiAccess>(this.accessId);
const sdk = await import('esdk-obs-nodejs');
const obsClient = new sdk.default({
// 推荐通过环境变量获取AKSK这里也可以使用其他外部引入方式传入如果使用硬编码可能会存在泄露风险
// 您可以登录访问管理控制台获取访问密钥AK/SK获取方式请参见https://support.huaweicloud.com/usermanual-ca/ca_01_0003.html
access_key_id: access.accessKeyId,
secret_access_key: access.accessKeySecret,
// 【可选】如果使用临时AK/SK和SecurityToken访问OBS同样建议您尽量避免使用硬编码以降低信息泄露风险。您可以通过环境变量获取访问密钥AK/SK也可以使用其他外部引入方式传入
// security_token: process.env.SECURITY_TOKEN,
// endpoint填写Bucket对应的Endpoint, 这里以华北-北京四为例,其他地区请按实际情况填写
server: `https://${bucketStr}obs.${regionStr}myhuaweicloud.com`,
});
return obsClient
}
async onGetBucketList(data: any) {
const obsClient = await this.getObsClient();
const res = await obsClient.listBuckets({
QueryLocation:true
})
this.checkRet(res)
const list = res.InterfaceResult?.Buckets
if (!list || list.length === 0) {
return []
}
return list.map(item => {
return {
value: `${item.Location}_${item.BucketName}`,
label: `${item.BucketName}<${item.Location}>`,
};
});
}
async onGetDomainList(data:any) {
if (!this.bucketList || this.bucketList.length === 0) {
return []
}
const optionList = []
for (const item of this.bucketList) {
const [location,bucket] = item.split('_')
const obsClient = await this.getObsClient({region:location});
const res = await obsClient.getBucketCustomDomain({
Bucket: bucket,
})
this.checkRet(res)
const list = res.InterfaceResult?.Domains
if (!list || list.length === 0) {
continue
}
const options= list.map(item => {
return {
value: `${location}_${bucket}_${item.DomainName}`,
label: `${item.DomainName}<${bucket}_${location}>`,
domain: item.DomainName,
};
});
optionList.push(...options)
}
return this.ctx.utils.options.buildGroupOptions( optionList,this.certDomains)
}
async setDomainCert(opts:{location:string,bucket:string,domain:string,cert:string|CertInfo}){
const {location,bucket,domain,cert} = opts
const obsClient = await this.getObsClient({region:location});
const params:any = {
Bucket: bucket,
DomainName: domain,
Name: this.buildCertName( domain)
};
if (typeof cert === 'string'){
params.CertificateId= cert
}else{
params.Certificate= cert.crt
params.PrivateKey = cert.key
}
const res = await obsClient.setBucketCustomDomain(params)
this.checkRet(res)
}
}
new HauweiDeployCertToOBS();

View File

@@ -1,2 +1,3 @@
export * from './deploy-to-cdn/index.js'
export * from './upload-to-ccm/index.js'
export * from './deploy-to-obs/index.js'

View File

@@ -9,6 +9,7 @@ import {AccessInput, BaseAccess, IsAccess} from '@certd/pipeline';
title: '京东云',
desc: '',
icon: 'svg:icon-jdcloud',
order: 1,
})
export class JDCloudAccess extends BaseAccess {

View File

@@ -6,7 +6,8 @@ import { JDCloudAccess } from "./access.js";
title: "京东云",
desc: "京东云DNS解析提供商",
accessType: "jdcloud",
icon: "svg:icon-jdcloud"
icon: "svg:icon-jdcloud",
order:3,
})
export class JDCloudDnsProvider extends AbstractDnsProvider {
access!: JDCloudAccess;

View File

@@ -0,0 +1,128 @@
import {AccessInput, BaseAccess, IsAccess} from "@certd/pipeline";
import {KsyunClient} from './client.js'
import {CertInfo} from "@certd/plugin-cert";
/**
*/
@IsAccess({
name: "ksyun",
title: "金山云授权",
desc: "",
icon: "svg:icon-ksyun"
})
export class KsyunAccess extends BaseAccess {
@AccessInput({
title: 'AccessKeyID',
component: {
placeholder: 'AccessKeyID',
},
helper: "[获取密钥](https://uc.console.ksyun.com/pro/iam/#/set/keyManage)",
required: true,
})
accessKeyId = '';
@AccessInput({
title: 'AccessKeySecret',
component: {
placeholder: 'AccessKeySecret',
},
required: true,
encrypt: true,
})
accessKeySecret = '';
@AccessInput({
title: "测试",
component: {
name: "api-test",
action: "TestRequest"
},
helper: "点击测试接口是否正常"
})
testRequest = true;
async onTestRequest() {
const client = await this.getCdnClient()
await this.getCertList({client})
return "ok"
}
async getCertList(opts?:{client:KsyunClient,pageNo?:number;pageSize?:number}) {
const res = await opts.client.doRequest({
action: "GetCertificates",
version: "2016-09-01",
method:"POST",
url:"/2016-09-01/cert/GetCertificates",
data:{
PageNum:opts?.pageNo || 1,
PageSize: opts?.pageSize || 30
}
})
this.ctx.logger.info(res)
return res
}
/**
* CertificateId 是 string 证书对应的唯一ID
* CertificateName 是 String 安全证书名称
* ServerCertificate 是 String 域名对应的安全证书内容
* PrivateKey
* @param opts
*/
async updateCert(opts:{
client:KsyunClient,
certId:string,
certName:string,
cert:CertInfo
}){
const res = await opts.client.doRequest({
action: "SetCertificate",
version: "2016-09-01",
method:"POST",
url:"/2016-09-01/cert/SetCertificate",
data:{
CertificateId: opts.certId,
CertificateName: opts.certName,
ServerCertificate: opts.cert.crt,
PrivateKey: opts.cert.key
}
})
this.ctx.logger.info(res)
return res
}
async getCert(opts:{client:KsyunClient,certId:string}){
const res = await opts.client.doRequest({
action: "GetCertificates",
version: "2016-09-01",
method:"POST",
url:"/2016-09-01/cert/GetCertificates",
data:{
CertificateId: opts.certId,
}
})
this.ctx.logger.info(res)
const list = res.Certificates
if (list.length > 0) {
return list[0]
}
throw new Error(`未找到证书:${opts.certId}`)
}
async getCdnClient() {
return new KsyunClient({
accessKeyId: this.accessKeyId,
secretAccessKey: this.accessKeySecret,
region: 'cn-beijing-6',
service: 'cdn',
endpoint: 'cdn.api.ksyun.com',
logger: this.ctx.logger,
http: this.ctx.http
})
}
}
new KsyunAccess();

View File

@@ -0,0 +1,357 @@
import crypto from 'crypto';
import querystring from 'querystring'
import {HttpClient, HttpRequestConfig, ILogger} from "@certd/basic";
export class KsyunClient {
accessKeyId: string;
secretAccessKey: string;
region: string;
service: string;
endpoint: string;
logger: ILogger;
http: HttpClient
constructor(opts:{accessKeyId:string; secretAccessKey:string; region?:string; service :string;endpoint :string,logger:ILogger,http:HttpClient}) {
this.accessKeyId = opts.accessKeyId;
this.secretAccessKey = opts.secretAccessKey;
this.region = opts.region || 'cn-beijing-6';
this.service = opts.service;
this.endpoint =opts.endpoint
this.logger = opts.logger;
this.http = opts.http;
}
async doRequest(opts: {action:string;version:string} &HttpRequestConfig){
const config = this.signRequest({
method: opts.method || 'GET',
url: opts.url || '/2016-09-01/domain/GetCdnDomains',
baseURL: `https://${this.endpoint}`,
params: opts.params,
headers: {
'X-Action': opts.action,
'X-Version': opts.version
},
data: opts.data
});
try{
return await this.http.request({
...config,
data: opts.data
})
}catch (e) {
this.logger.error(e.request)
if (e.response?.data?.Error?.Message){
throw new Error(e.response?.data?.Error?.Message)
}
throw e
}
}
/**
* 签名请求
* @param {Object} config Axios 请求配置
* @returns {Object} 签名后的请求配置
*/
signRequest(config) {
// 确保有必要的配置
if (!this.accessKeyId || !this.secretAccessKey) {
throw new Error('AccessKeyId and SecretAccessKey are required');
}
// 设置默认值
config.method = config.method || 'GET';
config.headers = config.headers || {};
// 获取当前时间并设置 X-Amz-Date
const requestDate = this.getRequestDate();
config.headers['x-amz-date'] = requestDate;
// 处理不同的请求方法
let canonicalQueryString = '';
let hashedPayload = this.hashPayload(config.data || '');
if (config.method.toUpperCase() === 'GET') {
// GET 请求 - 参数在 URL 中
const urlParts = config.url.split('?');
const path = urlParts[0];
const query = urlParts[1] || '';
// 合并现有查询参数和额外参数
const queryParams = {
...querystring.parse(query),
...(config.params || {})
};
// 生成规范查询字符串
canonicalQueryString = this.createCanonicalQueryString(queryParams);
config.url = `${path}?${canonicalQueryString}`;
config.params = {}; // 清空 params因为已经合并到 URL 中
} else {
// POST/PUT 等请求 - 参数在 body 中
canonicalQueryString = '';
if (config.data && typeof config.data === 'object') {
// 如果 data 是对象,转换为 JSON 字符串
config.data = JSON.stringify(config.data);
hashedPayload = this.hashPayload(config.data);
}
}
// 生成规范请求
const canonicalRequest = this.createCanonicalRequest(
config.method,
config.url,
canonicalQueryString,
config.headers,
hashedPayload
);
// 生成签名字符串
const credentialScope = this.createCredentialScope(requestDate);
const stringToSign = this.createStringToSign(requestDate, credentialScope, canonicalRequest);
// 计算签名
const signature = this.calculateSignature(requestDate, stringToSign);
// 生成 Authorization 头
const signedHeaders = this.getSignedHeaders(config.headers);
const authorizationHeader = this.createAuthorizationHeader(
credentialScope,
signedHeaders,
signature
);
// 添加 Authorization 头
config.headers.Authorization = authorizationHeader;
return config;
}
/**
* 获取当前时间 (格式: YYYYMMDD'T'HHMMSS'Z')
* @returns {string} 格式化后的时间字符串
*/
getRequestDate() {
const now = new Date();
const year = now.getUTCFullYear();
const month = String(now.getUTCMonth() + 1).padStart(2, '0');
const day = String(now.getUTCDate()).padStart(2, '0');
const hours = String(now.getUTCHours()).padStart(2, '0');
const minutes = String(now.getUTCMinutes()).padStart(2, '0');
const seconds = String(now.getUTCSeconds()).padStart(2, '0');
return `${year}${month}${day}T${hours}${minutes}${seconds}Z`;
}
/**
* 哈希 payload
* @param {string} payload 请求体内容
* @returns {string} 哈希后的16进制字符串
*/
hashPayload(payload) {
if (typeof payload !== 'string') {
payload = '';
}
return crypto.createHash('sha256').update(payload).digest('hex').toLowerCase();
}
/**
* 创建规范查询字符串
* @param {Object} params 查询参数对象
* @returns {string} 规范化的查询字符串
*/
createCanonicalQueryString(params) {
// 对参数名和值进行 URI 编码
const encodedParams = {};
for (const key in params) {
if (params.hasOwnProperty(key)) {
const encodedKey = this.uriEncode(key);
const encodedValue = this.uriEncode(params[key].toString());
encodedParams[encodedKey] = encodedValue;
}
}
// 按 ASCII 顺序排序
const sortedKeys = Object.keys(encodedParams).sort();
// 构建查询字符串
return sortedKeys.map(key => `${key}=${encodedParams[key]}`).join('&');
}
/**
* URI 编码 (符合 AWS 规范)
* @param {string} str 要编码的字符串
* @returns {string} 编码后的字符串
*/
uriEncode(str) {
return encodeURIComponent(str)
.replace(/[^A-Za-z0-9\-_.~]/g, c =>
'%' + c.charCodeAt(0).toString(16).toUpperCase());
}
/**
* 创建规范请求
* @param {string} method HTTP 方法
* @param {string} url 请求 URL
* @param {string} queryString 查询字符串
* @param {Object} headers 请求头
* @param {string} hashedPayload 哈希后的 payload
* @returns {string} 规范化的请求字符串
*/
createCanonicalRequest(method, url, queryString, headers, hashedPayload) {
// 获取规范 URI
const urlObj = new URL(url, 'http://dummy.com'); // 使用虚拟基础 URL 来解析路径
const canonicalUri = this.uriEncodePath(urlObj.pathname) || '/';
// 获取规范 headers 和 signed headers
const { canonicalHeaders, signedHeaders } = this.createCanonicalHeaders(headers);
return [
method.toUpperCase(),
canonicalUri,
queryString,
canonicalHeaders,
signedHeaders,
hashedPayload
].join('\n');
}
/**
* URI 编码路径部分
* @param {string} path 路径
* @returns {string} 编码后的路径
*/
uriEncodePath(path) {
// 分割路径为各个部分,分别编码
return path.split('/').map(part => this.uriEncode(part)).join('/');
}
/**
* 创建规范 headers 和 signed headers
* @param {Object} headers 原始请求头
* @returns {Object} { canonicalHeaders: string, signedHeaders: string }
*/
createCanonicalHeaders(headers) {
// 处理 headers
const headerMap:any = {};
// 标准化 headers
for (const key in headers) {
if (headers.hasOwnProperty(key)) {
const lowerKey = key.toLowerCase();
let value = headers[key]
if (value) {
value = value.toString().replace(/\s+/g, ' ').trim();
headerMap[lowerKey] = value;
}
}
}
// 确保 host 和 x-amz-date 存在
if (!headerMap.host) {
const url = headers.host ||this.endpoint || 'cdn.api.ksyun.com'; // 默认值
headerMap.host = url.replace(/^https?:\/\//, '').split('/')[0];
}
// 按 header 名称排序
const sortedHeaderNames = Object.keys(headerMap).sort();
// 构建规范 headers
let canonicalHeaders = '';
for (const name of sortedHeaderNames) {
canonicalHeaders += `${name}:${headerMap[name]}\n`;
}
// 构建 signed headers
const signedHeaders = sortedHeaderNames.join(';');
return { canonicalHeaders, signedHeaders };
}
/**
* 获取 signed headers
* @param {Object} headers 请求头
* @returns {string} signed headers 字符串
*/
getSignedHeaders(headers) {
const { signedHeaders } = this.createCanonicalHeaders(headers);
return signedHeaders;
}
/**
* 创建信任状范围
* @param {string} requestDate 请求日期 (YYYYMMDDTHHMMSSZ)
* @returns {string} 信任状范围字符串
*/
createCredentialScope(requestDate) {
const date = requestDate.split('T')[0];
return `${date}/${this.region}/${this.service}/aws4_request`;
}
/**
* 创建签名字符串
* @param {string} requestDate 请求日期
* @param {string} credentialScope 信任状范围
* @param {string} canonicalRequest 规范请求
* @returns {string} 签名字符串
*/
createStringToSign(requestDate, credentialScope, canonicalRequest) {
const algorithm = 'AWS4-HMAC-SHA256';
const hashedCanonicalRequest = crypto.createHash('sha256')
.update(canonicalRequest)
.digest('hex')
.toLowerCase();
return [
algorithm,
requestDate,
credentialScope,
hashedCanonicalRequest
].join('\n');
}
/**
* 计算签名
* @param {string} requestDate 请求日期
* @param {string} stringToSign 签名字符串
* @returns {string} 签名值
*/
calculateSignature(requestDate, stringToSign) {
const date = requestDate.split('T')[0];
const kDate = this.hmac(`AWS4${this.secretAccessKey}`, date);
const kRegion = this.hmac(kDate, this.region);
const kService = this.hmac(kRegion, this.service);
const kSigning = this.hmac(kService, 'aws4_request');
return this.hmac(kSigning, stringToSign, 'hex');
}
/**
* HMAC-SHA256 计算
* @param {string|Buffer} key 密钥
* @param {string} data 数据
* @param {string} [encoding] 输出编码
* @returns {string|Buffer} HMAC 结果
*/
hmac(key, data, encoding = null) {
const hmac = crypto.createHmac('sha256', key);
hmac.update(data);
return encoding ? hmac.digest(encoding) : hmac.digest();
}
/**
* 创建 Authorization 头
* @param {string} credentialScope 信任状范围
* @param {string} signedHeaders signed headers
* @param {string} signature 签名值
* @returns {string} Authorization 头值
*/
createAuthorizationHeader(credentialScope, signedHeaders, signature) {
return `AWS4-HMAC-SHA256 Credential=${this.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
}
}

View File

@@ -0,0 +1,2 @@
export * from "./plugins/index.js";
export * from "./access.js";

View File

@@ -0,0 +1 @@
import "./plugin-refresh-cert.js"

View File

@@ -0,0 +1,137 @@
import {
AbstractTaskPlugin,
IsTaskPlugin,
Pager,
PageSearch,
pluginGroups,
RunStrategy,
TaskInput
} from "@certd/pipeline";
import {CertApplyPluginNames, CertInfo} from "@certd/plugin-cert";
import {createCertDomainGetterInputDefine, createRemoteSelectInputDefine} from "@certd/plugin-lib";
import {KsyunAccess} from "../access.js";
@IsTaskPlugin({
//命名规范,插件类型+功能就是目录plugin-demo中的demo大写字母开头驼峰命名
name: "KsyunRefreshCert",
title: "金山云-更新CDN证书",
desc: "金山云自动更新CDN证书",
icon: "svg:icon-lucky",
//插件分组
group: pluginGroups.cdn.key,
needPlus: false,
default: {
//默认值配置照抄即可
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed
}
}
})
//类名规范跟上面插件名称name一致
export class KsyunRefreshCDNCert extends AbstractTaskPlugin {
//证书选择,此项必须要有
@TaskInput({
title: "域名证书",
helper: "请选择前置任务输出的域名证书",
component: {
name: "output-selector",
from: [...CertApplyPluginNames]
}
// required: true, // 必填
})
cert!: CertInfo;
@TaskInput(createCertDomainGetterInputDefine({ props: { required: false } }))
certDomains!: string[];
//授权选择框
@TaskInput({
title: "金山云授权",
component: {
name: "access-selector",
type: "ksyun" //固定授权类型
},
required: true //必填
})
accessId!: string;
//
@TaskInput(
createRemoteSelectInputDefine({
title: "证书Id",
helper: "要更新的金山云CDN证书id如果这里没有请先给cdn域名手动绑定一次证书",
action: KsyunRefreshCDNCert.prototype.onGetCertList.name,
pager: false,
search: false
})
)
certList!: string[];
//插件实例化时执行的方法
async onInstance() {
}
//插件执行方法
async execute(): Promise<void> {
const access = await this.getAccess<KsyunAccess>(this.accessId);
const client = await access.getCdnClient();
for (const certId of this.certList) {
this.logger.info(`----------- 开始更新证书:${certId}`);
const oldCert = await access.getCert({
client,
certId:certId
})
await access.updateCert({
client,
certId: certId,
certName: oldCert.CertificateName,
cert: this.cert
});
this.logger.info(`----------- 更新证书${certId}成功`);
}
this.logger.info("部署完成");
}
async onGetCertList(data: PageSearch = {}) {
const access = await this.getAccess<KsyunAccess>(this.accessId);
const client = await access.getCdnClient();
const pager = new Pager(data)
const res = await access.getCertList({client,
pageNo: pager.pageNo ,
pageSize: pager.pageSize
})
const list = res.Certificates
if (!list || list.length === 0) {
throw new Error("没有找到证书,请先在控制台手动上传一次证书");
}
const total = res.TotalCount
/**
* certificate-id
* name
* dns-names
*/
const options = list.map((item: any) => {
return {
label: `${item.CertificateName}<${item.CertificateId}-${item.ConfigDomainNames}>`,
value: item.CertificateId,
domain: item.ConfigDomainNames
};
});
return {
list: this.ctx.utils.options.buildGroupOptions(options, this.certDomains),
total: total,
pageNo: pager.pageNo,
pageSize: pager.pageSize
};
}
}
//实例化一下,注册插件
new KsyunRefreshCDNCert();

View File

@@ -9,7 +9,8 @@ import { CertInfo } from "@certd/plugin-cert";
name: "rainyun",
title: "雨云授权",
desc: "https://app.rainyun.com/",
icon: "svg:icon-lucky"
icon: "svg:icon-lucky",
order: 100
})
export class RainyunAccess extends BaseAccess {

View File

@@ -7,7 +7,6 @@ import { RainyunAccess } from "./access.js";
desc: "雨云DNS解析提供商",
accessType: "rainyun",
icon: "svg:icon-lucky",
order: 0
})
export class RainyunDnsProvider extends AbstractDnsProvider {

View File

@@ -122,7 +122,7 @@ export class DeployCertToTencentAll extends AbstractTaskPlugin {
region: this.region,
profile: {
httpProfile: {
endpoint: 'ssl.tencentcloudapi.com',
endpoint: `ssl.${access.intlDomain()}tencentcloudapi.com`,
},
},
});

View File

@@ -80,6 +80,8 @@ export class TencentDeployCertToCDNv2 extends AbstractTaskPlugin {
InstanceIdList: this.domains,
});
await this.ctx.utils.sleep(3000)
this.logger.info('部署成功', res);
}
@@ -102,7 +104,7 @@ export class TencentDeployCertToCDNv2 extends AbstractTaskPlugin {
region: '',
profile: {
httpProfile: {
endpoint: 'cdn.tencentcloudapi.com',
endpoint: `cdn.${accessProvider.intlDomain()}tencentcloudapi.com`,
},
},
};

View File

@@ -80,7 +80,7 @@ export class DeployToCdnPlugin extends AbstractTaskPlugin {
region: '',
profile: {
httpProfile: {
endpoint: 'cdn.tencentcloudapi.com',
endpoint: `cdn.${accessProvider.intlDomain()}tencentcloudapi.com`,
},
},
};

View File

@@ -119,7 +119,7 @@ export class DeployCertToTencentCLB extends AbstractTaskPlugin {
region: region,
profile: {
httpProfile: {
endpoint: 'clb.tencentcloudapi.com',
endpoint: `clb.${accessProvider.intlDomain()}tencentcloudapi.com`,
},
},
};

View File

@@ -1,5 +1,11 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
import { TencentAccess } from "@certd/plugin-lib";
import {AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput} from "@certd/pipeline";
import {
createCertDomainGetterInputDefine,
createRemoteSelectInputDefine,
TencentAccess,
TencentSslClient
} from "@certd/plugin-lib";
import {CertApplyPluginNames, CertInfo, CertReader} from "@certd/plugin-cert";
@IsTaskPlugin({
name: 'DeployCertToTencentEO',
@@ -14,16 +20,21 @@ import { TencentAccess } from "@certd/plugin-lib";
},
})
export class DeployCertToTencentEO extends AbstractTaskPlugin {
@TaskInput({
title: '已上传证书ID',
helper: '请选择前置任务上传到腾讯云的证书',
title: '域名证书',
helper: '请选择前置任务输出的域名证书,或者选择前置任务“上传证书到腾讯云”任务的证书ID',
component: {
name: 'output-selector',
from: 'UploadCertToTencent',
from: [...CertApplyPluginNames, 'UploadCertToTencent'],
},
required: true,
})
certId!: string;
cert!: CertInfo | string;
@TaskInput(createCertDomainGetterInputDefine({ props: { required: false } }))
certDomains!: string[];
@TaskInput({
title: 'Access提供者',
@@ -36,31 +47,35 @@ export class DeployCertToTencentEO extends AbstractTaskPlugin {
})
accessId!: string;
@TaskInput({
@TaskInput(createRemoteSelectInputDefine({
title: '站点ID',
helper: '类似于zone-xxxx的字符串在站点概览页面左上角或者站点列表页面站点名称下方',
action: DeployCertToTencentEO.prototype.onGetZoneList.name,
watches: ['certDomains', 'accessId'],
required: true,
})
component:{
name:"remote-auto-complete"
}
}))
zoneId!: string;
@TaskInput(
createRemoteSelectInputDefine({
title: '加速域名',
helper: '请选择域名或输入域名',
action: DeployCertToTencentEO.prototype.onGetDomainList.name,
})
)
domainNames!: string[];
@TaskInput({
title: '证书名称',
helper: '证书上传后将以此参数作为名称前缀',
})
certName!: string;
@TaskInput({
title: 'cdn加速域名',
component: {
name: 'a-select',
vModel: 'value',
mode: 'tags',
open: false,
},
helper: '支持多个域名',
rules: [{ required: true, message: '该项必填' }],
})
domainNames!: string[];
// @TaskInput({
// title: "CDN接口",
@@ -80,16 +95,41 @@ export class DeployCertToTencentEO extends AbstractTaskPlugin {
}
async execute(): Promise<void> {
const accessProvider: TencentAccess = (await this.getAccess(this.accessId)) as TencentAccess;
const accessProvider = await this.getAccess<TencentAccess>(this.accessId)
const client = this.getClient(accessProvider);
const params = this.buildParams();
const sslClient = new TencentSslClient({
access:accessProvider,
logger: this.logger,
});
let tencentCertId = this.cert as string;
if (typeof this.cert !== 'string') {
const certReader = new CertReader(this.cert);
tencentCertId = await sslClient.uploadToTencent({
certName: certReader.buildCertName(),
cert: this.cert,
});
}
const params:any = {
ZoneId: this.zoneId,
Hosts: this.domainNames,
Mode: 'sslcert',
ServerCertInfo: [
{
CertId: tencentCertId,
},
],
};
await this.doRequest(client, params);
}
getClient(accessProvider: TencentAccess) {
const TeoClient = this.Client;
const endpoint = accessProvider.isIntl()?"teo.intl.tencentcloudapi.com": "teo.tencentcloudapi.com";
//teo.intl.tencentcloudapi.com
const endpoint = `teo.${accessProvider.intlDomain()}tencentcloudapi.com`;
const clientConfig = {
credential: {
secretId: accessProvider.secretId,
@@ -106,18 +146,6 @@ export class DeployCertToTencentEO extends AbstractTaskPlugin {
return new TeoClient(clientConfig);
}
buildParams() {
return {
ZoneId: this.zoneId,
Hosts: this.domainNames,
Mode: 'sslcert',
ServerCertInfo: [
{
CertId: this.certId,
},
],
};
}
async doRequest(client: any, params: any) {
const ret = await client.ModifyHostsCertificate(params);
@@ -131,6 +159,57 @@ export class DeployCertToTencentEO extends AbstractTaskPlugin {
throw new Error('执行失败:' + ret.Error.Code + ',' + ret.Error.Message);
}
}
async onGetZoneList(data: any) {
if (!this.accessId){
throw new Error('请选择授权');
}
const access: TencentAccess = await this.getAccess<TencentAccess>(this.accessId);
const client = await this.getClient(access);
const res = await client.DescribeZones({
Limit: 100,
});
this.checkRet(res);
const list = res.Zones;
if (!list || list.length === 0) {
return [];
}
return list.map((item: any) => {
return {
label: `${item.ZoneName}<${item.ZoneId}>`,
value: item.ZoneId,
};
});
}
async onGetDomainList(data: any) {
if (!this.accessId){
throw new Error('请选择授权');
}
const access: TencentAccess = await this.getAccess<TencentAccess>(this.accessId);
const client = await this.getClient(access);
const res = await client.DescribeAccelerationDomains({
Limit: 200,
ZoneId: this.zoneId,
});
this.checkRet(res);
const list = res.AccelerationDomains
if (!list || list.length === 0) {
return [];
}
const options = list.map((item: any) => {
return {
label: item.DomainName,
value: item.DomainName,
domain: item.DomainName
};
});
return this.ctx.utils.options.buildGroupOptions(options, this.certDomains);
}
}
new DeployCertToTencentEO();

View File

@@ -101,7 +101,7 @@ export class TencentDeployCertToLive extends AbstractTaskPlugin {
region: '',
profile: {
httpProfile: {
endpoint: 'live.tencentcloudapi.com',
endpoint: `live.${accessProvider.intlDomain()}tencentcloudapi.com`,
},
},
};

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