Merge branch 'refs/heads/v2-dev' into v2-dev-buy

# Conflicts:
#	docs/.vitepress/config.ts
#	packages/ui/certd-client/src/views/certd/pipeline/sub-domain/index.vue
This commit is contained in:
xiaojunnuo
2025-09-22 22:29:59 +08:00
175 changed files with 4551 additions and 684 deletions
+5
View File
@@ -0,0 +1,5 @@
# These are supported funding model platforms
github: greper
buy_me_a_coffee: greper
custom: ['https://afdian.com/a/greper']
+50
View File
@@ -3,6 +3,56 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.21](https://github.com/certd/certd/compare/v1.36.20...v1.36.21) (2025-09-15)
### Bug Fixes
* 修复导入插件对话框无法打开的bug,修复插件编辑页面打开多个代码编辑器消失的bug ([e5a080a](https://github.com/certd/certd/commit/e5a080aebe0d2f3e3c0f86bf863f75069c1bf7ab))
* 修复ssl.com报EMAILADDRESS数量不对的bug ([c560cc5](https://github.com/certd/certd/commit/c560cc5adda6e15bf3a8865d874042550a6c2688))
## [1.36.20](https://github.com/certd/certd/compare/v1.36.19...v1.36.20) (2025-09-13)
### Bug Fixes
* 修复商业版退出登录后,丢失站点个性化设置的bug ([d75dd05](https://github.com/certd/certd/commit/d75dd058d65c85f80c49e1fa7a910e6c6f08e824))
* 修复授权类型和名称字段排到最后的bug ([43b7977](https://github.com/certd/certd/commit/43b79778ea9034065f6a15af3296274315597c6b))
* 修复证书监控某些情况下报 options.lookup不能为null的bug ([d2ecfe5](https://github.com/certd/certd/commit/d2ecfe5491b2639eb30b5cae293af6062d58bb9f))
* 修复证书手动托管时新上传的证书无效的bug ([506385e](https://github.com/certd/certd/commit/506385e5a2600887fe30854e0713583caaa2e689))
* 修复secret patch 类型多了type:的bug ([d04f383](https://github.com/certd/certd/commit/d04f3831611011a90ec0594724b9694490d5edd0))
### Performance Improvements
* 登录支持极验验证码 ([370db62](https://github.com/certd/certd/commit/370db62bf0aece241859244927beabba32d6a257))
* 登录注册、找回密码都支持极验验证码和图片验证码 ([7bdde68](https://github.com/certd/certd/commit/7bdde68ecea29fe2c570fd3cb082139db6c93d93))
* 优化加量包展示效果 ([3c65f37](https://github.com/certd/certd/commit/3c65f37d84177ba107d4a6462648af12d2fc4b7a))
* 证书到期剩余天数进度条根据实际证书有效期计算 ([#528](https://github.com/certd/certd/issues/528)) nicheng-he ([2d4586b](https://github.com/certd/certd/commit/2d4586b1c42c39f97d2a95b9453cca4bc8bfbe61))
* add preferred chain option ([#519](https://github.com/certd/certd/issues/519)) @ZeroClover ([902359f](https://github.com/certd/certd/commit/902359f24ed12eee4f9b65178f1d6a60378351d2))
* ssh配置增加脚本类型设置,bash还是sh ([ae41c60](https://github.com/certd/certd/commit/ae41c6038b27c9476e64a2402a8daf247c38a5b6))
* start.sh增加sudo ([b7271d7](https://github.com/certd/certd/commit/b7271d7a464773a1bf87d7d1f24d933ba0f86915))
## [1.36.19](https://github.com/certd/certd/compare/v1.36.18...v1.36.19) (2025-09-05)
### Bug Fixes
* 前置任务输出不存在时输出警告提示 ([b59052c](https://github.com/certd/certd/commit/b59052cc43b7b070fabd8b8e914e4c2a5e0ad61c))
* 修复批量流水线执行时日志显示错乱的问题 ([4372adc](https://github.com/certd/certd/commit/4372adc703b9a4c785664054ab2a533626d815a8))
* 修复远程数据选择无法过滤的bug ([6cbb073](https://github.com/certd/certd/commit/6cbb0739f8428d51b0712f718fe4d236cc087cf9))
* 修复mysql下购买套餐加量包无效的bug ([c26ad4c](https://github.com/certd/certd/commit/c26ad4c8075f0606d45b8da13915737968d6191a))
### Performance Improvements
* 创建证书时支持选择通知时机 ([0e96bfd](https://github.com/certd/certd/commit/0e96bfdfa377824d204e72923d1176408ae6b300))
* 创建k8s secret 时设置type为tls ([79ebabf](https://github.com/certd/certd/commit/79ebabfcfb9e5a534049c84f5f1a642b357fc856))
* 去掉宝塔url后面的斜杠 ([8a0c2b9](https://github.com/certd/certd/commit/8a0c2b9b13628da750c25757e0cb8ed3038775ba))
* 商业版隐藏文档相关链接 ([4443a1c](https://github.com/certd/certd/commit/4443a1c0308fa6b95a05efd73d15d24b65d641c9))
* 商业版隐藏文档相关链接 ([db89561](https://github.com/certd/certd/commit/db8956148083bc4f988226ccf719940d08158a27))
* 增加健康检查探针 /health/liveliness 和 /health/readiness ([44019e1](https://github.com/certd/certd/commit/44019e104289fedd32a867db00e9c6cb71b389cc))
* 支持根据id更新证书(证书Id不变接口),不过该接口为白名单功能,普通腾讯云账户无法使用 ([fe9c4f3](https://github.com/certd/certd/commit/fe9c4f3391ff07c01dd9a252225f69a129c39050))
* 支持godaddy ([b7980aa](https://github.com/certd/certd/commit/b7980aad5ab50f58662eaddf5d84aa82876a98eb))
* 支持ssl.com证书颁发机构 ([27b6dfa](https://github.com/certd/certd/commit/27b6dfa4d2ab3bddd284c3a34511a72e1a513a4c))
* 子域名托管说明 ([39a0223](https://github.com/certd/certd/commit/39a02235cf4416bb5bd1acd3831241efeaa2f602))
* ssh 增加超时断开连接,默认10分钟超时 ([c24a040](https://github.com/certd/certd/commit/c24a040c19cacafc79228d7a7649af93837d94a1))
## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28) ## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28)
### Bug Fixes ### Bug Fixes
+2 -1
View File
@@ -1,6 +1,6 @@
# Certd # Certd
[English](./README_en.md) | [中文](./README.md) 中文 | [English](./README_en.md)
Certd® 是一个免费的全自动证书管理系统,让你的网站证书永不过期。 Certd® 是一个免费的全自动证书管理系统,让你的网站证书永不过期。
后缀d取自linux守护进程的命名风格,意为证书守护进程 后缀d取自linux守护进程的命名风格,意为证书守护进程
@@ -171,6 +171,7 @@ https://afdian.com/a/greper
| 自动部署插件 | 阿里云CDN、腾讯云、七牛CDN、主机部署、宝塔、1Panel等大部分插件 | 群晖 | | 自动部署插件 | 阿里云CDN、腾讯云、七牛CDN、主机部署、宝塔、1Panel等大部分插件 | 群晖 |
| 通知 | 邮件通知、自定义webhook | 邮件免配置、企微、钉钉、飞书、anpush、server酱等 | | 通知 | 邮件通知、自定义webhook | 邮件免配置、企微、钉钉、飞书、anpush、server酱等 |
************************
************************ ************************
+3 -1
View File
@@ -1,6 +1,6 @@
# Certd # Certd
[English](./README_en.md) | [中文](./README.md) [中文](./README.md) | English
Certd® is a free, fully automated certificate management system that ensures your website certificates never expire. The suffix 'd' is inspired by the naming convention of Linux daemons, representing a certificate daemon. Certd® is a free, fully automated certificate management system that ensures your website certificates never expire. The suffix 'd' is inspired by the naming convention of Linux daemons, representing a certificate daemon.
@@ -134,6 +134,8 @@ You can also add the author as a friend.
| QR Code | <img height="230" src="./docs/guide/contact/images/me.png"> | | QR Code | <img height="230" src="./docs/guide/contact/images/me.png"> |
## 8. Donation ## 8. Donation
************************
[![Sponsor](https://img.shields.io/badge/Sponsor-%E2%9D%A4-red)](https://github.com/sponsors/greper)
************************ ************************
Support open-source projects and contribute with love. I've joined Afdian. Support open-source projects and contribute with love. I've joined Afdian.
https://afdian.com/a/greper https://afdian.com/a/greper
+1 -1
View File
@@ -1 +1 @@
00:43 21:09
+1 -1
View File
@@ -107,7 +107,6 @@ export default defineConfig({
text: "常见问题", text: "常见问题",
items: [ items: [
{text: "QA", link: "/guide/qa/use.md"}, {text: "QA", link: "/guide/qa/use.md"},
{text: "常见报错处理", link: "/guide/qa/"},
{text: "群晖证书部署", link: "/guide/use/synology/"}, {text: "群晖证书部署", link: "/guide/use/synology/"},
{text: "腾讯云密钥获取", link: "/guide/use/tencent/"}, {text: "腾讯云密钥获取", link: "/guide/use/tencent/"},
{text: "连接windows主机", link: "/guide/use/host/windows.md"}, {text: "连接windows主机", link: "/guide/use/host/windows.md"},
@@ -120,6 +119,7 @@ export default defineConfig({
{text: "邮箱配置", link: "/guide/use/email/index.md"}, {text: "邮箱配置", link: "/guide/use/email/index.md"},
{text: "IPv6支持", link: "/guide/use/setting/ipv6.md"}, {text: "IPv6支持", link: "/guide/use/setting/ipv6.md"},
{text: "ESXi", link: "/guide/use/ESXi/index.md"}, {text: "ESXi", link: "/guide/use/ESXi/index.md"},
{text: "宝塔动态IP白名单", link: "/guide/use/baota/white_list.md"},
{text: "子域名托管", link: "/guide/use/cert/subdomain.md"}, {text: "子域名托管", link: "/guide/use/cert/subdomain.md"},
] ]
}, },
+50
View File
@@ -3,6 +3,56 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.21](https://github.com/certd/certd/compare/v1.36.20...v1.36.21) (2025-09-15)
### Bug Fixes
* 修复导入插件对话框无法打开的bug,修复插件编辑页面打开多个代码编辑器消失的bug ([e5a080a](https://github.com/certd/certd/commit/e5a080aebe0d2f3e3c0f86bf863f75069c1bf7ab))
* 修复ssl.com报EMAILADDRESS数量不对的bug ([c560cc5](https://github.com/certd/certd/commit/c560cc5adda6e15bf3a8865d874042550a6c2688))
## [1.36.20](https://github.com/certd/certd/compare/v1.36.19...v1.36.20) (2025-09-13)
### Bug Fixes
* 修复商业版退出登录后,丢失站点个性化设置的bug ([d75dd05](https://github.com/certd/certd/commit/d75dd058d65c85f80c49e1fa7a910e6c6f08e824))
* 修复授权类型和名称字段排到最后的bug ([43b7977](https://github.com/certd/certd/commit/43b79778ea9034065f6a15af3296274315597c6b))
* 修复证书监控某些情况下报 options.lookup不能为null的bug ([d2ecfe5](https://github.com/certd/certd/commit/d2ecfe5491b2639eb30b5cae293af6062d58bb9f))
* 修复证书手动托管时新上传的证书无效的bug ([506385e](https://github.com/certd/certd/commit/506385e5a2600887fe30854e0713583caaa2e689))
* 修复secret patch 类型多了type:的bug ([d04f383](https://github.com/certd/certd/commit/d04f3831611011a90ec0594724b9694490d5edd0))
### Performance Improvements
* 登录支持极验验证码 ([370db62](https://github.com/certd/certd/commit/370db62bf0aece241859244927beabba32d6a257))
* 登录注册、找回密码都支持极验验证码和图片验证码 ([7bdde68](https://github.com/certd/certd/commit/7bdde68ecea29fe2c570fd3cb082139db6c93d93))
* 优化加量包展示效果 ([3c65f37](https://github.com/certd/certd/commit/3c65f37d84177ba107d4a6462648af12d2fc4b7a))
* 证书到期剩余天数进度条根据实际证书有效期计算 ([#528](https://github.com/certd/certd/issues/528)) nicheng-he ([2d4586b](https://github.com/certd/certd/commit/2d4586b1c42c39f97d2a95b9453cca4bc8bfbe61))
* add preferred chain option ([#519](https://github.com/certd/certd/issues/519)) @ZeroClover ([902359f](https://github.com/certd/certd/commit/902359f24ed12eee4f9b65178f1d6a60378351d2))
* ssh配置增加脚本类型设置,bash还是sh ([ae41c60](https://github.com/certd/certd/commit/ae41c6038b27c9476e64a2402a8daf247c38a5b6))
* start.sh增加sudo ([b7271d7](https://github.com/certd/certd/commit/b7271d7a464773a1bf87d7d1f24d933ba0f86915))
## [1.36.19](https://github.com/certd/certd/compare/v1.36.18...v1.36.19) (2025-09-05)
### Bug Fixes
* 前置任务输出不存在时输出警告提示 ([b59052c](https://github.com/certd/certd/commit/b59052cc43b7b070fabd8b8e914e4c2a5e0ad61c))
* 修复批量流水线执行时日志显示错乱的问题 ([4372adc](https://github.com/certd/certd/commit/4372adc703b9a4c785664054ab2a533626d815a8))
* 修复远程数据选择无法过滤的bug ([6cbb073](https://github.com/certd/certd/commit/6cbb0739f8428d51b0712f718fe4d236cc087cf9))
* 修复mysql下购买套餐加量包无效的bug ([c26ad4c](https://github.com/certd/certd/commit/c26ad4c8075f0606d45b8da13915737968d6191a))
### Performance Improvements
* 创建证书时支持选择通知时机 ([0e96bfd](https://github.com/certd/certd/commit/0e96bfdfa377824d204e72923d1176408ae6b300))
* 创建k8s secret 时设置type为tls ([79ebabf](https://github.com/certd/certd/commit/79ebabfcfb9e5a534049c84f5f1a642b357fc856))
* 去掉宝塔url后面的斜杠 ([8a0c2b9](https://github.com/certd/certd/commit/8a0c2b9b13628da750c25757e0cb8ed3038775ba))
* 商业版隐藏文档相关链接 ([4443a1c](https://github.com/certd/certd/commit/4443a1c0308fa6b95a05efd73d15d24b65d641c9))
* 商业版隐藏文档相关链接 ([db89561](https://github.com/certd/certd/commit/db8956148083bc4f988226ccf719940d08158a27))
* 增加健康检查探针 /health/liveliness 和 /health/readiness ([44019e1](https://github.com/certd/certd/commit/44019e104289fedd32a867db00e9c6cb71b389cc))
* 支持根据id更新证书(证书Id不变接口),不过该接口为白名单功能,普通腾讯云账户无法使用 ([fe9c4f3](https://github.com/certd/certd/commit/fe9c4f3391ff07c01dd9a252225f69a129c39050))
* 支持godaddy ([b7980aa](https://github.com/certd/certd/commit/b7980aad5ab50f58662eaddf5d84aa82876a98eb))
* 支持ssl.com证书颁发机构 ([27b6dfa](https://github.com/certd/certd/commit/27b6dfa4d2ab3bddd284c3a34511a72e1a513a4c))
* 子域名托管说明 ([39a0223](https://github.com/certd/certd/commit/39a02235cf4416bb5bd1acd3831241efeaa2f602))
* ssh 增加超时断开连接,默认10分钟超时 ([c24a040](https://github.com/certd/certd/commit/c24a040c19cacafc79228d7a7649af93837d94a1))
## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28) ## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28)
### Bug Fixes ### Bug Fixes
+3
View File
@@ -82,3 +82,6 @@ admin/123456
2. 确认宝塔防火墙是否放开对应端口 2. 确认宝塔防火墙是否放开对应端口
3. 尝试将Certd容器加入宝塔的`bridge`网络 3. 尝试将Certd容器加入宝塔的`bridge`网络
![](./images/network.png) ![](./images/network.png)
### 2. 动态IP无法加白名单问题
[Nginx代理解决方案](../../use/baota/white_list.md)
+15
View File
@@ -58,3 +58,18 @@ kill -9 $(lsof -t -i:7001)
## 四、备份恢复 ## 四、备份恢复
将备份的`db.sqlite`及同目录下的其他文件覆盖到原来的位置,重启certd即可 将备份的`db.sqlite`及同目录下的其他文件覆盖到原来的位置,重启certd即可
## 六、常见问题
### 1. npm install better-sqlite3 时,提示node-gyp需要vscode环境编译
1. 首先确保node版本为22以上
2. 将下面两行加到 ~/.npmrc 里面
3. 重新install
> better_sqlite3_binary_host=https://registry.npmmirror.com/-/binary/better-sqlite3
> better_sqlite3_binary_host_mirror=https://registry.npmmirror.com/-/binary/better-sqlite3
+20 -1
View File
@@ -1,4 +1,4 @@
# 使用问题 # 常见问题
## 1. 是否支持IP证书 ## 1. 是否支持IP证书
@@ -12,3 +12,22 @@
当证书到期前35天(创建流水线时可以修改),将会自动重新申请证书,自动部署 当证书到期前35天(创建流水线时可以修改),将会自动重新申请证书,自动部署
## 3. too many certificates 错误
当出现如下报错时,说明相同的域名短时间内申请超过5次
解决方案:可以加多一个子域名,重新执行就可以规避次错误
```
"detail": too many certificates (5) already issued for this exact set of idantifiers in the last 168hm0s
```
## ssl.com报错 CAA record does not include ssl.com which is required to issue the certificate
ssl.com申请证书要求必须设置CAA记录,表示允许ssl.com为该域名颁发证书
请按如下格式添加CAA记录
| 示例 | 类型 | 域名前缀 | flag | tag | 值 |
|-------|-----| -- |-----------|--------|----------------------|
| 顶级域名 | CAA | @ | 0 | issue | "ssl.com" (注意有双引号) |
| 一级泛域名 | CAA | * | 0 | issue/issuewild | "ssl.com" |
| 固定子域名 | CAA | sub | 0 | issue |"ssl.com" |
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

+98
View File
@@ -0,0 +1,98 @@
# 宝塔IP白名单与动态IP问题
调用宝塔接口需要添加IP白名单,但当certd部署在动态IP环境下时,IP白名单就不好添加
本章节提供两种解决方案:
1. 小范围网段放开(简单)
2. nginx代理
## 一、放开小范围网段
家庭网络IP虽然会变动,但是只会在小范围变的。
你可以分析规律,将变动的部分,设置成网段即可
> 比如出现过: 100.25.1.5 100.25.1.8
>
> 那么你可以配置 100.25.1.1-100.25.1.255
> 如果出现过: 100.25.1.5 100.25.4.8
>
> 可以尝试配置 100.25.*.5
## 二、nginx代理方案
通过在宝塔中配置一个nginx反向代理,代理宝塔自己的地址
然后在nginx中配置放开certd需要的接口,缩小影响范围
让nginx来充当防火墙
架构图如下:
```
只要将127.0.0.1加入白名单即可
certd --------> nginx -------> 宝塔
拦截除更新证书之外的地址
```
### 1. 添加nginx反向代理
![](./images/white-1.png)
### 2. 域名和代理目标
![](./images/white-2.png)
### 3. 设置放开哪些接口
![](./images/white-3.png)
![img.png](images/white-4.png)
将如下脚本填入上方文本域中,保存
```nginx configuration
set $allow_access false;
# 检查请求的URI是否在白名单中
if ($request_uri ~* "^/(site\?action=get_site_types)") {
# 允许测试
set $allow_access true;
}
if ($request_uri ~* "^/(config\?action=SavePanelSSL)") {
# 允许部署到宝塔面板本身证书
set $allow_access true;
}
if ($request_uri ~* "^/(mod/docker/com/set_ssl|site\?action=SetSSL|ssl\?action=GetSiteDomain|mod/docker/com/get_site_list)") {
# 允许部署宝塔网站证书
set $allow_access true;
}
if ($request_uri ~* "^/(ssl?action=remove_cloud_cert|ssl\?action=get_cert_list)") {
# 允许删除宝塔过期证书
set $allow_access true;
}
if ($request_uri ~* "^/(datalist/get_data_list|site/set_site_ssl)") {
set $allow_access true;
}
# 如果不在白名单,返回403禁止访问
if ($allow_access = false) {
return 405;
}
```
### 4. 接口IP白名单添加127.0.0.1
![img.png](images/white-5.png)
### 5. certd中宝塔授权配置改成新的这个域名地址
![img.png](images/white-6.png)
点击测试检查是否ok ,到这里就可以正常部署证书了
### 6. 安全加强(将请求地址改成https)
在宝塔中配置证书部署任务,选择刚才新建的这个网站,给他部署证书
勾选强制https
![img.png](images/white-safe-1.png)
更换443端口【可选】
![img.png](images/white-safe-2.png)
禁止http访问
+1 -1
View File
@@ -9,5 +9,5 @@
} }
}, },
"npmClient": "pnpm", "npmClient": "pnpm",
"version": "1.36.18" "version": "1.36.21"
} }
+14
View File
@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.21](https://github.com/publishlab/node-acme-client/compare/v1.36.20...v1.36.21) (2025-09-15)
**Note:** Version bump only for package @certd/acme-client
## [1.36.20](https://github.com/publishlab/node-acme-client/compare/v1.36.19...v1.36.20) (2025-09-13)
**Note:** Version bump only for package @certd/acme-client
## [1.36.19](https://github.com/publishlab/node-acme-client/compare/v1.36.18...v1.36.19) (2025-09-05)
### Performance Improvements
* 支持ssl.com证书颁发机构 ([27b6dfa](https://github.com/publishlab/node-acme-client/commit/27b6dfa4d2ab3bddd284c3a34511a72e1a513a4c))
## [1.36.18](https://github.com/publishlab/node-acme-client/compare/v1.36.17...v1.36.18) (2025-08-28) ## [1.36.18](https://github.com/publishlab/node-acme-client/compare/v1.36.17...v1.36.18) (2025-08-28)
**Note:** Version bump only for package @certd/acme-client **Note:** Version bump only for package @certd/acme-client
+3 -3
View File
@@ -3,7 +3,7 @@
"description": "Simple and unopinionated ACME client", "description": "Simple and unopinionated ACME client",
"private": false, "private": false,
"author": "nmorsman", "author": "nmorsman",
"version": "1.36.18", "version": "1.36.21",
"type": "module", "type": "module",
"module": "scr/index.js", "module": "scr/index.js",
"main": "src/index.js", "main": "src/index.js",
@@ -18,7 +18,7 @@
"types" "types"
], ],
"dependencies": { "dependencies": {
"@certd/basic": "^1.36.18", "@certd/basic": "^1.36.21",
"@peculiar/x509": "^1.11.0", "@peculiar/x509": "^1.11.0",
"asn1js": "^3.0.5", "asn1js": "^3.0.5",
"axios": "^1.7.2", "axios": "^1.7.2",
@@ -69,5 +69,5 @@
"bugs": { "bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues" "url": "https://github.com/publishlab/node-acme-client/issues"
}, },
"gitHead": "ea18a5ad151b296fda54fb5bcbe64c7d80cdff2f" "gitHead": "3cedef4974708d828fb972acc54af0515e3ec3a0"
} }
+1 -1
View File
@@ -502,7 +502,7 @@ class AcmeClient {
await verify[challenge.type](authz, challenge, keyAuthorization); await verify[challenge.type](authz, challenge, keyAuthorization);
}; };
log('Waiting for ACME challenge verification(等待ACME挑战验证)', this.backoffOpts); log('Waiting for ACME challenge verification(等待ACME挑战验证)');
return util.retry(verifyFn, this.backoffOpts); return util.retry(verifyFn, this.backoffOpts);
} }
+4
View File
@@ -25,6 +25,10 @@ export const directory = {
staging: 'https://acme.zerossl.com/v2/DV90', staging: 'https://acme.zerossl.com/v2/DV90',
production: 'https://acme.zerossl.com/v2/DV90', production: 'https://acme.zerossl.com/v2/DV90',
}, },
sslcom:{
staging: 'https://acme.ssl.com/sslcom-dv-rsa',
production: 'https://acme.ssl.com/sslcom-dv-rsa',
}
}; };
/** /**
+18
View File
@@ -3,6 +3,24 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.21](https://github.com/certd/certd/compare/v1.36.20...v1.36.21) (2025-09-15)
**Note:** Version bump only for package @certd/basic
## [1.36.20](https://github.com/certd/certd/compare/v1.36.19...v1.36.20) (2025-09-13)
**Note:** Version bump only for package @certd/basic
## [1.36.19](https://github.com/certd/certd/compare/v1.36.18...v1.36.19) (2025-09-05)
### Bug Fixes
* 修复批量流水线执行时日志显示错乱的问题 ([4372adc](https://github.com/certd/certd/commit/4372adc703b9a4c785664054ab2a533626d815a8))
### Performance Improvements
* 去掉宝塔url后面的斜杠 ([8a0c2b9](https://github.com/certd/certd/commit/8a0c2b9b13628da750c25757e0cb8ed3038775ba))
## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28) ## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28)
**Note:** Version bump only for package @certd/basic **Note:** Version bump only for package @certd/basic
+1 -1
View File
@@ -1 +1 @@
00:39 20:59
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "@certd/basic", "name": "@certd/basic",
"private": false, "private": false,
"version": "1.36.18", "version": "1.36.21",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
@@ -45,5 +45,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "ea18a5ad151b296fda54fb5bcbe64c7d80cdff2f" "gitHead": "3cedef4974708d828fb972acc54af0515e3ec3a0"
} }
+178 -36
View File
@@ -1,22 +1,4 @@
import log4js, { LoggingEvent, Logger } from "log4js"; import log4js, { CallStack, Level } from "log4js";
const OutputAppender = {
configure: (config: any, layouts: any, findAppender: any, levels: any) => {
let layout = layouts.basicLayout;
if (config.layout) {
layout = layouts.layout(config.layout.type, config.layout);
}
function customAppender(layout: any, timezoneOffset: any) {
return (loggingEvent: LoggingEvent) => {
if (loggingEvent.context.outputHandler?.write) {
const text = `${layout(loggingEvent, timezoneOffset)}\n`;
loggingEvent.context.outputHandler.write(text);
}
};
}
return customAppender(layout, config.timezoneOffset);
},
};
let logFilePath = "./logs/app.log"; let logFilePath = "./logs/app.log";
export function resetLogConfigure() { export function resetLogConfigure() {
@@ -24,7 +6,6 @@ export function resetLogConfigure() {
log4js.configure({ log4js.configure({
appenders: { appenders: {
std: { type: "stdout" }, std: { type: "stdout" },
output: { type: OutputAppender },
file: { file: {
type: "dateFile", type: "dateFile",
filename: logFilePath, filename: logFilePath,
@@ -33,7 +14,7 @@ export function resetLogConfigure() {
numBackups: 3, numBackups: 3,
}, },
}, },
categories: { default: { appenders: ["std", "file"], level: "info" }, pipeline: { appenders: ["std", "file", "output"], level: "info" } }, categories: { default: { appenders: ["std", "file"], level: "info" }, pipeline: { appenders: ["std", "file"], level: "info" } },
}); });
} }
resetLogConfigure(); resetLogConfigure();
@@ -44,15 +25,98 @@ export function resetLogFilePath(filePath: string) {
resetLogConfigure(); resetLogConfigure();
} }
export function buildLogger(write: (text: string) => void) { export function buildLogger(write: (text: string) => void) {
const logger = log4js.getLogger("pipeline"); return new PipelineLogger("pipeline", write);
const _secrets: string[] = []; }
//@ts-ignore
logger.addSecret = (secret: string) => { export type ILogger = {
_secrets.push(secret); readonly category: string;
}; level: Level | string;
logger.addContext("outputHandler", { log(level: Level | string, ...args: any[]): void;
write: (text: string) => {
for (const item of _secrets) { isLevelEnabled(level?: string): boolean;
isTraceEnabled(): boolean;
isDebugEnabled(): boolean;
isInfoEnabled(): boolean;
isWarnEnabled(): boolean;
isErrorEnabled(): boolean;
isFatalEnabled(): boolean;
_log(level: Level, data: any): void;
addContext(key: string, value: any): void;
removeContext(key: string): void;
clearContext(): void;
/**
* Replace the basic parse function with a new custom one
* - Note that linesToSkip will be based on the origin of the Error object in addition to the callStackLinesToSkip (at least 1)
* @param parseFunction the new parseFunction. Use `undefined` to reset to the base implementation
*/
setParseCallStackFunction(parseFunction: (error: Error, linesToSkip: number) => CallStack | undefined): void;
/**
* Adjust the value of linesToSkip when the parseFunction is called.
*
* Cannot be less than 0.
*/
callStackLinesToSkip: number;
trace(message: any, ...args: any[]): void;
debug(message: any, ...args: any[]): void;
info(message: any, ...args: any[]): void;
warn(message: any, ...args: any[]): void;
error(message: any, ...args: any[]): void;
fatal(message: any, ...args: any[]): void;
mark(message: any, ...args: any[]): void;
};
const locale = Intl.DateTimeFormat().resolvedOptions().locale;
const formatter = new Intl.DateTimeFormat(locale, {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
});
function formatDateIntl(date = new Date()) {
const milliseconds = date.getMilliseconds(); // 获取毫秒
const formattedMilliseconds = milliseconds.toString().padStart(3, "0");
return formatter.format(date) + "." + formattedMilliseconds;
}
// @ts-ignore
export class PipelineLogger implements ILogger {
callStackLinesToSkip: number = 3;
readonly category: string = "pipeline";
level: Level | string = "info";
_secrets: string[] = [];
logger: ILogger;
customWriter!: (text: string) => void;
constructor(name: string, write: (text: string) => void) {
this.customWriter = write;
this.logger = log4js.getLogger(name);
}
addSecret(secret: string) {
this._secrets.push(secret);
}
_doLog(level: string, ...args: any[]) {
let text = args.join(" ");
if (this.customWriter) {
for (const item of this._secrets) {
if (item == null) { if (item == null) {
continue; continue;
} }
@@ -66,10 +130,88 @@ export function buildLogger(write: (text: string) => void) {
text = text.replaceAll(item, "*".repeat(item.length)); text = text.replaceAll(item, "*".repeat(item.length));
} }
} }
write(text); text = `[${formatDateIntl()}] [${level.toUpperCase()}] - ${text} \n`;
}, this.customWriter(text);
}); }
return logger; // @ts-ignore
} this.logger[level](...args);
}
export type ILogger = Logger; _log(level: Level, data: any): void {}
addContext(key: string, value: any): void {}
clearContext(): void {}
debug(message: any, ...args: any[]): void {
if (this.isDebugEnabled()) {
this._doLog("debug", message, ...args);
}
}
error(message: any, ...args: any[]): void {
if (this.isErrorEnabled()) {
this._doLog("error", message, ...args);
}
}
fatal(message: any, ...args: any[]): void {
if (this.isFatalEnabled()) {
this._doLog("fatal", message, ...args);
}
}
info(message: any, ...args: any[]): void {
if (this.isInfoEnabled()) {
this._doLog("info", message, ...args);
}
}
trace(message: any, ...args: any[]): void {
if (this.isTraceEnabled()) {
this._doLog("trace", message, ...args);
}
}
warn(message: any, ...args: any[]): void {
if (this.isWarnEnabled()) {
this._doLog("warn", message, ...args);
}
}
isDebugEnabled(): boolean {
return logger.isDebugEnabled();
}
isErrorEnabled(): boolean {
return logger.isErrorEnabled();
}
isFatalEnabled(): boolean {
return logger.isFatalEnabled();
}
isInfoEnabled(): boolean {
return logger.isInfoEnabled();
}
isLevelEnabled(level?: string): boolean {
return logger.isLevelEnabled();
}
isTraceEnabled(): boolean {
return logger.isTraceEnabled();
}
isWarnEnabled(): boolean {
return logger.isWarnEnabled();
}
log(level: Level | string, ...args: any[]): void {}
mark(message: any, ...args: any[]): void {}
removeContext(key: string): void {}
setParseCallStackFunction(parseFunction: (error: Error, linesToSkip: number) => CallStack | undefined): void {}
}
+14 -7
View File
@@ -1,6 +1,5 @@
import axios, { AxiosHeaders, AxiosRequestConfig } from "axios"; import axios, { AxiosHeaders, AxiosRequestConfig } from "axios";
import { ILogger, logger } from "./util.log.js"; import { ILogger, logger } from "./util.log.js";
import { Logger } from "log4js";
import { HttpProxyAgent } from "http-proxy-agent"; import { HttpProxyAgent } from "http-proxy-agent";
import { HttpsProxyAgent } from "https-proxy-agent"; import { HttpsProxyAgent } from "https-proxy-agent";
import nodeHttp from "http"; import nodeHttp from "http";
@@ -8,6 +7,13 @@ import * as https from "node:https";
import { merge } from "lodash-es"; import { merge } from "lodash-es";
import { safePromise } from "./util.promise.js"; import { safePromise } from "./util.promise.js";
import fs from "fs"; import fs from "fs";
const errorMap: Record<string, string> = {
"ssl3_get_record:wrong version number": "http协议错误,服务端要求http协议,请检查是否使用了https请求",
"getaddrinfo EAI_AGAIN": "无法解析域名,请检查网络连接或dns配置,更换docker-compose.yaml中dns配置",
"self-signed certificate": "目标站点为自签名证书,请勾选忽略证书校验",
};
export class HttpError extends Error { export class HttpError extends Error {
status?: number; status?: number;
statusText?: string; statusText?: string;
@@ -22,11 +28,12 @@ export class HttpError extends Error {
super(error.message || error.response?.statusText); super(error.message || error.response?.statusText);
const message = error?.message; const message = error?.message;
if (message && typeof message === "string") { if (message && typeof message === "string" && message.indexOf) {
if (message.indexOf && message.indexOf("ssl3_get_record:wrong version number") >= 0) { for (const key in errorMap) {
this.message = `${message}(http协议错误,服务端要求http协议,请检查是否使用了https请求)`; if (message.indexOf(key) > -1) {
} else if (message.indexOf("getaddrinfo EAI_AGAIN") >= 0) { this.message = `${this.message}(${errorMap[key]})`;
this.message = `${message}(无法解析域名,请检查网络连接或dns配置,更换docker-compose.yaml中dns配置)`; break;
}
} }
} }
@@ -84,7 +91,7 @@ export function getGlobalAgents() {
/** /**
* @description * @description
*/ */
export function createAxiosService({ logger }: { logger: Logger }) { export function createAxiosService({ logger }: { logger: ILogger }) {
// 创建一个 axios 实例 // 创建一个 axios 实例
const service = axios.create(); const service = axios.create();
+19
View File
@@ -3,6 +3,25 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.21](https://github.com/certd/certd/compare/v1.36.20...v1.36.21) (2025-09-15)
**Note:** Version bump only for package @certd/pipeline
## [1.36.20](https://github.com/certd/certd/compare/v1.36.19...v1.36.20) (2025-09-13)
**Note:** Version bump only for package @certd/pipeline
## [1.36.19](https://github.com/certd/certd/compare/v1.36.18...v1.36.19) (2025-09-05)
### Bug Fixes
* 前置任务输出不存在时输出警告提示 ([b59052c](https://github.com/certd/certd/commit/b59052cc43b7b070fabd8b8e914e4c2a5e0ad61c))
### Performance Improvements
* 支持godaddy ([b7980aa](https://github.com/certd/certd/commit/b7980aad5ab50f58662eaddf5d84aa82876a98eb))
* 支持ssl.com证书颁发机构 ([27b6dfa](https://github.com/certd/certd/commit/27b6dfa4d2ab3bddd284c3a34511a72e1a513a4c))
## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28) ## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28)
**Note:** Version bump only for package @certd/pipeline **Note:** Version bump only for package @certd/pipeline
+4 -4
View File
@@ -1,7 +1,7 @@
{ {
"name": "@certd/pipeline", "name": "@certd/pipeline",
"private": false, "private": false,
"version": "1.36.18", "version": "1.36.21",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
@@ -17,8 +17,8 @@
"pub": "npm publish" "pub": "npm publish"
}, },
"dependencies": { "dependencies": {
"@certd/basic": "^1.36.18", "@certd/basic": "^1.36.21",
"@certd/plus-core": "^1.36.18", "@certd/plus-core": "^1.36.21",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"reflect-metadata": "^0.1.13" "reflect-metadata": "^0.1.13"
@@ -44,5 +44,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "ea18a5ad151b296fda54fb5bcbe64c7d80cdff2f" "gitHead": "3cedef4974708d828fb972acc54af0515e3ec3a0"
} }
+3 -3
View File
@@ -21,9 +21,9 @@ export type PageRes = {
export class Pager { export class Pager {
pageNo: number; pageNo: number;
pageSize: number; pageSize: number;
constructor(req: PageSearch) { constructor(req?: PageSearch) {
this.pageNo = req.pageNo ?? 1; this.pageNo = req?.pageNo ?? 1;
this.pageSize = req.pageSize || 50; this.pageSize = req?.pageSize || 50;
} }
getOffset() { getOffset() {
+1 -1
View File
@@ -314,7 +314,7 @@ export class Executor {
const outputKey = arr[2]; const outputKey = arr[2];
input[key] = this.currentStatusMap.get(id)?.status?.output[outputKey] ?? this.lastStatusMap.get(id)?.status?.output[outputKey]; input[key] = this.currentStatusMap.get(id)?.status?.output[outputKey] ?? this.lastStatusMap.get(id)?.status?.output[outputKey];
if (input[key] == null) { if (input[key] == null) {
this.logger.warn(`${item.title}的配置未找到对应的输出值,请确认对应的前置任务是否存在或者是否执行正确`); currentLogger.warn(`${item.title}的配置未找到对应的输出值,请确认对应的前置任务是否存在或者是否执行正确`);
} }
} }
} }
+2 -2
View File
@@ -253,9 +253,9 @@ export abstract class AbstractTaskPlugin implements ITaskPlugin {
return name + "_" + dayjs().format("YYYYMMDDHHmmssSSS"); return name + "_" + dayjs().format("YYYYMMDDHHmmssSSS");
} }
buildCertName(domain: string) { buildCertName(domain: string, prefix = "") {
domain = domain.replaceAll("*", "_").replaceAll(".", "_"); domain = domain.replaceAll("*", "_").replaceAll(".", "_");
return `${domain}_${dayjs().format("YYYYMMDDHHmmssSSS")}`; return `${prefix}_${domain}_${dayjs().format("YYYYMMDDHHmmssSSS")}`;
} }
async onRequest(req: PluginRequestHandleReq<any>) { async onRequest(req: PluginRequestHandleReq<any>) {
@@ -69,9 +69,15 @@ export class Registry<T = any> {
return this.storage; return this.storage;
} }
getDefineList() { getDefineList(prefix?: string) {
let list = []; let list = [];
if (prefix) {
prefix = prefix + ":";
}
for (const key in this.storage) { for (const key in this.storage) {
if (prefix && !key.startsWith(prefix)) {
continue;
}
const define = this.getDefine(key); const define = this.getDefine(key);
if (define) { if (define) {
if (define?.deprecated) { if (define?.deprecated) {
@@ -90,7 +96,10 @@ export class Registry<T = any> {
return list; return list;
} }
getDefine(key: string) { getDefine(key: string, prefix?: string) {
if (prefix) {
key = prefix + ":" + key;
}
const item = this.storage[key]; const item = this.storage[key];
if (!item) { if (!item) {
return; return;
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.21](https://github.com/certd/certd/compare/v1.36.20...v1.36.21) (2025-09-15)
**Note:** Version bump only for package @certd/lib-huawei
## [1.36.20](https://github.com/certd/certd/compare/v1.36.19...v1.36.20) (2025-09-13)
**Note:** Version bump only for package @certd/lib-huawei
## [1.36.19](https://github.com/certd/certd/compare/v1.36.18...v1.36.19) (2025-09-05)
**Note:** Version bump only for package @certd/lib-huawei
## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28) ## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28)
**Note:** Version bump only for package @certd/lib-huawei **Note:** Version bump only for package @certd/lib-huawei
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "@certd/lib-huawei", "name": "@certd/lib-huawei",
"private": false, "private": false,
"version": "1.36.18", "version": "1.36.21",
"main": "./dist/bundle.js", "main": "./dist/bundle.js",
"module": "./dist/bundle.js", "module": "./dist/bundle.js",
"types": "./dist/d/index.d.ts", "types": "./dist/d/index.d.ts",
@@ -24,5 +24,5 @@
"prettier": "^2.8.8", "prettier": "^2.8.8",
"tslib": "^2.8.1" "tslib": "^2.8.1"
}, },
"gitHead": "ea18a5ad151b296fda54fb5bcbe64c7d80cdff2f" "gitHead": "3cedef4974708d828fb972acc54af0515e3ec3a0"
} }
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.21](https://github.com/certd/certd/compare/v1.36.20...v1.36.21) (2025-09-15)
**Note:** Version bump only for package @certd/lib-iframe
## [1.36.20](https://github.com/certd/certd/compare/v1.36.19...v1.36.20) (2025-09-13)
**Note:** Version bump only for package @certd/lib-iframe
## [1.36.19](https://github.com/certd/certd/compare/v1.36.18...v1.36.19) (2025-09-05)
**Note:** Version bump only for package @certd/lib-iframe
## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28) ## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28)
**Note:** Version bump only for package @certd/lib-iframe **Note:** Version bump only for package @certd/lib-iframe
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "@certd/lib-iframe", "name": "@certd/lib-iframe",
"private": false, "private": false,
"version": "1.36.18", "version": "1.36.21",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
@@ -31,5 +31,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "ea18a5ad151b296fda54fb5bcbe64c7d80cdff2f" "gitHead": "3cedef4974708d828fb972acc54af0515e3ec3a0"
} }
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.21](https://github.com/certd/certd/compare/v1.36.20...v1.36.21) (2025-09-15)
**Note:** Version bump only for package @certd/jdcloud
## [1.36.20](https://github.com/certd/certd/compare/v1.36.19...v1.36.20) (2025-09-13)
**Note:** Version bump only for package @certd/jdcloud
## [1.36.19](https://github.com/certd/certd/compare/v1.36.18...v1.36.19) (2025-09-05)
**Note:** Version bump only for package @certd/jdcloud
## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28) ## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28)
**Note:** Version bump only for package @certd/jdcloud **Note:** Version bump only for package @certd/jdcloud
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@certd/jdcloud", "name": "@certd/jdcloud",
"version": "1.36.18", "version": "1.36.21",
"description": "jdcloud openApi sdk", "description": "jdcloud openApi sdk",
"main": "./dist/bundle.js", "main": "./dist/bundle.js",
"module": "./dist/bundle.js", "module": "./dist/bundle.js",
@@ -61,5 +61,5 @@
"fetch" "fetch"
] ]
}, },
"gitHead": "ea18a5ad151b296fda54fb5bcbe64c7d80cdff2f" "gitHead": "3cedef4974708d828fb972acc54af0515e3ec3a0"
} }
+20
View File
@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.21](https://github.com/certd/certd/compare/v1.36.20...v1.36.21) (2025-09-15)
**Note:** Version bump only for package @certd/lib-k8s
## [1.36.20](https://github.com/certd/certd/compare/v1.36.19...v1.36.20) (2025-09-13)
### Bug Fixes
* 修复secret patch 类型多了type:的bug ([d04f383](https://github.com/certd/certd/commit/d04f3831611011a90ec0594724b9694490d5edd0))
## [1.36.19](https://github.com/certd/certd/compare/v1.36.18...v1.36.19) (2025-09-05)
### Bug Fixes
* 修复远程数据选择无法过滤的bug ([6cbb073](https://github.com/certd/certd/commit/6cbb0739f8428d51b0712f718fe4d236cc087cf9))
### Performance Improvements
* 创建k8s secret 时设置type为tls ([79ebabf](https://github.com/certd/certd/commit/79ebabfcfb9e5a534049c84f5f1a642b357fc856))
## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28) ## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28)
### Performance Improvements ### Performance Improvements
+3 -3
View File
@@ -1,7 +1,7 @@
{ {
"name": "@certd/lib-k8s", "name": "@certd/lib-k8s",
"private": false, "private": false,
"version": "1.36.18", "version": "1.36.21",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
@@ -17,7 +17,7 @@
"pub": "npm publish" "pub": "npm publish"
}, },
"dependencies": { "dependencies": {
"@certd/basic": "^1.36.18", "@certd/basic": "^1.36.21",
"@kubernetes/client-node": "0.21.0" "@kubernetes/client-node": "0.21.0"
}, },
"devDependencies": { "devDependencies": {
@@ -32,5 +32,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "ea18a5ad151b296fda54fb5bcbe64c7d80cdff2f" "gitHead": "3cedef4974708d828fb972acc54af0515e3ec3a0"
} }
+10 -5
View File
@@ -1,7 +1,7 @@
import { CoreV1Api, KubeConfig, NetworkingV1Api, V1Ingress, V1Secret } from "@kubernetes/client-node"; import { CoreV1Api, KubeConfig, NetworkingV1Api, V1Ingress, V1Secret } from "@kubernetes/client-node";
import dns from "dns"; import dns from "dns";
import { ILogger } from "@certd/basic"; import { ILogger } from "@certd/basic";
import _ from "lodash-es"; import { merge } from "lodash-es";
export type K8sClientOpts = { export type K8sClientOpts = {
kubeConfigStr: string; kubeConfigStr: string;
@@ -85,7 +85,6 @@ export class K8sClient {
/** /**
* Secret * Secret
* @param opts {namespace:default, body:yamlStr} * @param opts {namespace:default, body:yamlStr}
* @returns {Promise<*>}
*/ */
async createSecret(opts: { namespace: string; body: V1Secret }) { async createSecret(opts: { namespace: string; body: V1Secret }) {
const namespace = opts.namespace || "default"; const namespace = opts.namespace || "default";
@@ -119,7 +118,13 @@ export class K8sClient {
this.logger.warn(`secret ${secretName} 不存在`); this.logger.warn(`secret ${secretName} 不存在`);
if (opts.createOnNotFound) { if (opts.createOnNotFound) {
//没有找到,则创建 //没有找到,则创建
const res = await this.createSecret({ namespace, body: opts.body }); const body = merge(
{
type: "kubernetes.io/tls",
},
opts.body
);
const res = await this.createSecret({ namespace, body });
this.logger.info(`secret ${secretName} 已创建`); this.logger.info(`secret ${secretName} 已创建`);
return res; return res;
} }
@@ -127,7 +132,7 @@ export class K8sClient {
throw e; throw e;
} }
const newSecret = _.merge(oldSecret.body, opts.body); const newSecret = merge(oldSecret.body, opts.body);
const res = await this.client.replaceNamespacedSecret(secretName, namespace, newSecret); const res = await this.client.replaceNamespacedSecret(secretName, namespace, newSecret);
this.logger.info(`secret ${secretName} 已更新`); this.logger.info(`secret ${secretName} 已更新`);
return res.body; return res.body;
@@ -161,7 +166,7 @@ export class K8sClient {
this.logger.info("patch ingress:", ingressName, namespace); this.logger.info("patch ingress:", ingressName, namespace);
const client = this.kubeconfig.makeApiClient(NetworkingV1Api); const client = this.kubeconfig.makeApiClient(NetworkingV1Api);
const oldIngress = await client.readNamespacedIngress(ingressName, namespace); const oldIngress = await client.readNamespacedIngress(ingressName, namespace);
const newIngress = _.merge(oldIngress.body, opts.body); const newIngress = merge(oldIngress.body, opts.body);
const res = await client.replaceNamespacedIngress(ingressName, namespace, newIngress); const res = await client.replaceNamespacedIngress(ingressName, namespace, newIngress);
this.logger.info("ingress patched", opts.body); this.logger.info("ingress patched", opts.body);
return res; return res;
+15
View File
@@ -3,6 +3,21 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.21](https://github.com/certd/certd/compare/v1.36.20...v1.36.21) (2025-09-15)
**Note:** Version bump only for package @certd/lib-server
## [1.36.20](https://github.com/certd/certd/compare/v1.36.19...v1.36.20) (2025-09-13)
### Performance Improvements
* 登录支持极验验证码 ([370db62](https://github.com/certd/certd/commit/370db62bf0aece241859244927beabba32d6a257))
* 登录注册、找回密码都支持极验验证码和图片验证码 ([7bdde68](https://github.com/certd/certd/commit/7bdde68ecea29fe2c570fd3cb082139db6c93d93))
## [1.36.19](https://github.com/certd/certd/compare/v1.36.18...v1.36.19) (2025-09-05)
**Note:** Version bump only for package @certd/lib-server
## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28) ## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28)
**Note:** Version bump only for package @certd/lib-server **Note:** Version bump only for package @certd/lib-server
+6 -6
View File
@@ -1,6 +1,6 @@
{ {
"name": "@certd/lib-server", "name": "@certd/lib-server",
"version": "1.36.18", "version": "1.36.21",
"description": "midway with flyway, sql upgrade way ", "description": "midway with flyway, sql upgrade way ",
"private": false, "private": false,
"type": "module", "type": "module",
@@ -27,10 +27,10 @@
], ],
"license": "AGPL", "license": "AGPL",
"dependencies": { "dependencies": {
"@certd/acme-client": "^1.36.18", "@certd/acme-client": "^1.36.21",
"@certd/basic": "^1.36.18", "@certd/basic": "^1.36.21",
"@certd/pipeline": "^1.36.18", "@certd/pipeline": "^1.36.21",
"@certd/plus-core": "^1.36.18", "@certd/plus-core": "^1.36.21",
"@midwayjs/cache": "~3.14.0", "@midwayjs/cache": "~3.14.0",
"@midwayjs/core": "~3.20.3", "@midwayjs/core": "~3.20.3",
"@midwayjs/i18n": "~3.20.3", "@midwayjs/i18n": "~3.20.3",
@@ -61,5 +61,5 @@
"typeorm": "^0.3.11", "typeorm": "^0.3.11",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "ea18a5ad151b296fda54fb5bcbe64c7d80cdff2f" "gitHead": "3cedef4974708d828fb972acc54af0515e3ec3a0"
} }
+2 -1
View File
@@ -1,8 +1,9 @@
import { SysSettingsEntity } from './system/index.js'; import { SysSettingsEntity } from './system/index.js';
import { AccessEntity } from './user/access/entity/access.js'; import { AccessEntity } from './user/access/entity/access.js';
import { AddonEntity } from "./user/index.js";
export * from './basic/index.js'; export * from './basic/index.js';
export * from './system/index.js'; export * from './system/index.js';
export * from './user/index.js'; export * from './user/index.js';
export { LibServerConfiguration as Configuration } from './configuration.js'; export { LibServerConfiguration as Configuration } from './configuration.js';
export const libServerEntities = [SysSettingsEntity, AccessEntity]; export const libServerEntities = [SysSettingsEntity, AccessEntity,AddonEntity];
@@ -30,6 +30,13 @@ export class SysPublicSettings extends BaseSettings {
mpsNo?: string; mpsNo?: string;
robots?: boolean = true; robots?: boolean = true;
aiChatEnabled = true; aiChatEnabled = true;
//验证码是否开启
captchaEnabled = false;
//验证码类型
captchaType?: string;
captchaAddonId?:number;
} }
export class SysPrivateSettings extends BaseSettings { export class SysPrivateSettings extends BaseSettings {
@@ -207,4 +214,3 @@ export class SysSafeSetting extends BaseSettings {
}; };
} }
@@ -0,0 +1,97 @@
import { HttpClient, ILogger, utils } from "@certd/basic";
import {upperFirst} from "lodash-es";
import { FormItemProps, PluginRequestHandleReq, Registrable } from "@certd/pipeline";
export type AddonRequestHandleReqInput<T = any> = {
id?: number;
title?: string;
addon: T;
};
export type AddonRequestHandleReq<T = any> = {
addonType: string;
} &PluginRequestHandleReq<AddonRequestHandleReqInput<T>>;
export type AddonInputDefine = FormItemProps & {
title: string;
required?: boolean;
};
export type AddonDefine = Registrable & {
addonType: string;
needPlus?: boolean;
input?: {
[key: string]: AddonInputDefine;
};
showTest?: boolean;
};
export type AddonInstanceConfig = {
id: number;
addonType: string;
type: string;
name: string;
userId: number;
setting: {
[key: string]: any;
};
};
export interface IAddon {
ctx: AddonContext;
[key: string]: any;
}
export type AddonContext = {
http: HttpClient;
logger: ILogger;
utils: typeof utils;
};
export abstract class BaseAddon implements IAddon {
define!: AddonDefine;
ctx!: AddonContext;
http!: HttpClient;
logger!: ILogger;
// eslint-disable-next-line @typescript-eslint/no-empty-function
async onInstance() {}
setCtx(ctx: AddonContext) {
this.ctx = ctx;
this.http = ctx.http;
this.logger = ctx.logger;
}
setDefine = (define:AddonDefine) => {
this.define = define;
};
async onRequest(req:AddonRequestHandleReq) {
if (!req.action) {
throw new Error("action is required");
}
let methodName = req.action;
if (!req.action.startsWith("on")) {
methodName = `on${upperFirst(req.action)}`;
}
// @ts-ignore
const method = this[methodName];
if (method) {
// @ts-ignore
return await this[methodName](req.data);
}
throw new Error(`action ${req.action} not found`);
}
}
export interface IAddonGetter {
getById<T = any>(id: any): Promise<T>;
getCommonById<T = any>(id: any): Promise<T>;
}
@@ -0,0 +1,65 @@
// src/decorator/memoryCache.decorator.ts
import * as _ from "lodash-es";
import { merge } from "lodash-es";
import { addonRegistry } from "./registry.js";
import { AddonContext, AddonDefine, AddonInputDefine } from "./api.js";
import { Decorator } from "@certd/pipeline";
// 提供一个唯一 key
export const ADDON_CLASS_KEY = "pipeline:addon";
export const ADDON_INPUT_KEY = "pipeline:addon:input";
export function IsAddon(define: AddonDefine): ClassDecorator {
return (target: any) => {
target = Decorator.target(target);
const inputs: any = {};
const properties = Decorator.getClassProperties(target);
for (const property in properties) {
const input = Reflect.getMetadata(ADDON_INPUT_KEY, target, property);
if (input) {
inputs[property] = input;
}
}
_.merge(define, { input: inputs });
Reflect.defineMetadata(ADDON_CLASS_KEY, define, target);
target.define = define;
const key = `${define.addonType}:${define.name}`;
addonRegistry.register(key, {
define,
target: async () => {
return target;
},
});
};
}
export function AddonInput(input?: AddonInputDefine): PropertyDecorator {
return (target, propertyKey) => {
target = Decorator.target(target, propertyKey);
// const _type = Reflect.getMetadata("design:type", target, propertyKey);
Reflect.defineMetadata(ADDON_INPUT_KEY, input, target, propertyKey);
};
}
export async function newAddon(addonType:string,type: string, input: any, ctx: AddonContext) {
const key = `${addonType}:${type}`
const register = addonRegistry.get(key);
if (register == null) {
throw new Error(`${addonType} ${type} not found`);
}
// @ts-ignore
const pluginCls = await register.target();
// @ts-ignore
const plugin = new pluginCls();
merge(plugin, input);
if (!ctx) {
throw new Error("ctx is required");
}
plugin.setDefine(register.define);
plugin.setCtx(ctx);
await plugin.onInstance();
return plugin;
}
@@ -0,0 +1,3 @@
export * from "./api.js";
export * from "./registry.js";
export * from "./decorator.js";
@@ -0,0 +1,3 @@
import { createRegistry } from "@certd/pipeline";
export const addonRegistry = createRegistry("addon");
@@ -0,0 +1,44 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
/**
*/
@Entity('cd_addon')
export class AddonEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'user_id', comment: '用户id' })
userId: number;
@Column({ comment: '名称', length: 100 })
name: string;
@Column({ name: 'addon_type', comment: 'addon类型', length: 100 })
addonType: string;
@Column({ comment: '类型', length: 100 })
type: string;
@Column({ name: 'setting', comment: '设置', length: 10240, nullable: true })
setting: string;
@Column({ name: 'is_system', comment: '是否系统级别', nullable: false, default: false })
isSystem: boolean;
@Column({ name: 'is_default', comment: '是否默认', nullable: false, default: false })
isDefault: boolean;
@Column({
name: 'create_time',
comment: '创建时间',
default: () => 'CURRENT_TIMESTAMP',
})
createTime: Date;
@Column({
name: 'update_time',
comment: '修改时间',
default: () => 'CURRENT_TIMESTAMP',
})
updateTime: Date;
}
@@ -0,0 +1,5 @@
export * from './api/index.js'
export * from './entity/addon.js'
export * from './service/addon-service.js'
export * from './service/addon-getter.js'
export * from './service/addon-sys-getter.js'
@@ -0,0 +1,18 @@
import { IAddonGetter } from "../api/index.js";
export class AddonGetter implements IAddonGetter {
userId: number;
getter: <T>(id: any, userId?: number) => Promise<T>;
constructor(userId: number, getter: (id: any, userId: number) => Promise<any>) {
this.userId = userId;
this.getter = getter;
}
async getById<T = any>(id: any) {
return await this.getter<T>(id, this.userId);
}
async getCommonById<T = any>(id: any) {
return await this.getter<T>(id, 0);
}
}
@@ -0,0 +1,231 @@
import { Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { InjectEntityModel } from "@midwayjs/typeorm";
import { In, Repository } from "typeorm";
import { AddonDefine, BaseService, PageReq, PermissionException, ValidateException } from "../../../index.js";
import { addonRegistry, newAddon } from "../api/index.js";
import { AddonEntity } from "../entity/addon.js";
import { http, logger, utils } from "@certd/basic";
/**
* Addon
*/
@Provide()
@Scope(ScopeEnum.Request, {allowDowngrade: true})
export class AddonService extends BaseService<AddonEntity> {
@InjectEntityModel(AddonEntity)
repository: Repository<AddonEntity>;
//@ts-ignore
getRepository() {
return this.repository;
}
async page(pageReq: PageReq<AddonEntity>) {
const res = await super.page(pageReq);
res.records = res.records.map(item => {
return item;
});
return res;
}
async add(param) {
let oldEntity = null;
if (param._copyFrom){
oldEntity = await this.info(param._copyFrom);
if (oldEntity == null) {
throw new ValidateException('该Addon配置不存在,请确认是否已被删除');
}
if (oldEntity.userId !== param.userId) {
throw new ValidateException('您无权查看该Addon配置');
}
}
if (!param.userId){
param.isSystem = true
}else{
param.isSystem = false
}
delete param._copyFrom
return await super.add(param);
}
/**
*
* @param param
*/
async update(param) {
const oldEntity = await this.info(param.id);
if (oldEntity == null) {
throw new ValidateException('该Addon配置不存在,请确认是否已被删除');
}
return await super.update(param);
}
async getSimpleInfo(id: number) {
const entity = await this.info(id);
if (entity == null) {
throw new ValidateException('该Addon配置不存在,请确认是否已被删除');
}
return {
id: entity.id,
name: entity.name,
userId: entity.userId,
addonType: entity.addonType,
type: entity.type,
};
}
async getAddonById(id: any, checkUserId: boolean, userId?: number): Promise<any> {
const ctx = {
http: http,
logger: logger,
utils: utils,
};
if (!id){
//使用图片验证码
return await newAddon("captcha", "image", {},ctx);
}
const entity = await this.info(id);
if (entity == null) {
//使用图片验证码
return await newAddon("captcha", "image", {},ctx);
}
if (checkUserId) {
if (userId == null) {
throw new ValidateException('userId不能为空');
}
if (userId !== entity.userId) {
throw new PermissionException('您对该Addon无访问权限');
}
}
const setting = JSON.parse(entity.setting ??"{}")
const input = {
id: entity.id,
...setting,
};
return await newAddon(entity.addonType, entity.type, input,ctx);
}
async getById(id: any, userId: number): Promise<any> {
return await this.getAddonById(id, true, userId);
}
getDefineList(addonType: string) {
return addonRegistry.getDefineList();
}
getDefineByType(type: string,prefix?: string) {
return addonRegistry.getDefine(type,prefix) as AddonDefine;
}
async getSimpleByIds(ids: number[], userId: any) {
if (ids.length === 0) {
return [];
}
if (!userId) {
return [];
}
return await this.repository.find({
where: {
id: In(ids),
userId,
},
select: {
id: true,
name: true,
addonType: true,
type: true,
userId:true,
isSystem: true,
},
});
}
async getDefault(userId: number,addonType: string): Promise<any> {
const res = await this.repository.findOne({
where: {
userId,
addonType
},
order: {
isDefault: 'DESC',
},
});
if (!res) {
return null;
}
return this.buildAddonInstanceConfig(res);
}
private buildAddonInstanceConfig(res: AddonEntity) {
const setting = JSON.parse(res.setting);
return {
id: res.id,
addonType: res.addonType,
type: res.type,
name: res.name,
userId: res.userId,
setting,
};
}
async setDefault(id: number, userId: number,addonType:string) {
if (!id) {
throw new ValidateException('id不能为空');
}
if (!userId) {
throw new ValidateException('userId不能为空');
}
await this.repository.update(
{
userId,
addonType
},
{
isDefault: false,
}
);
await this.repository.update(
{
id,
userId,
addonType
},
{
isDefault: true,
}
);
}
async getOrCreateDefault(opts:{addonType:string,type:string, inputs: any, userId: any}) {
const {addonType,type,inputs,userId} = opts;
const addonDefine = this.getDefineByType( type,addonType)
const defaultConfig = await this.getDefault(userId,addonType);
if (defaultConfig) {
return defaultConfig;
}
const setting = {
...inputs,
};
const res = await this.repository.save({
userId,
addonType,
type: type,
name: addonDefine.title,
setting: JSON.stringify(setting),
isDefault: true,
});
return this.buildAddonInstanceConfig(res);
}
}
@@ -0,0 +1,17 @@
import { IAccessService } from '@certd/pipeline';
import { AddonService } from './addon-service.js';
export class AddonSysGetter implements IAccessService {
addonService: AddonService;
constructor(addonService: AddonService) {
this.addonService = addonService;
}
async getById<T = any>(id: any) {
return await this.addonService.getById(id, 0);
}
async getCommonById<T = any>(id: any) {
return await this.addonService.getById(id, 0);
}
}
@@ -1 +1,2 @@
export * from './access/index.js'; export * from './access/index.js';
export * from './addon/index.js';
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.21](https://github.com/certd/certd/compare/v1.36.20...v1.36.21) (2025-09-15)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.36.20](https://github.com/certd/certd/compare/v1.36.19...v1.36.20) (2025-09-13)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.36.19](https://github.com/certd/certd/compare/v1.36.18...v1.36.19) (2025-09-05)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28) ## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28)
**Note:** Version bump only for package @certd/midway-flyway-js **Note:** Version bump only for package @certd/midway-flyway-js
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@certd/midway-flyway-js", "name": "@certd/midway-flyway-js",
"version": "1.36.18", "version": "1.36.21",
"description": "midway with flyway, sql upgrade way ", "description": "midway with flyway, sql upgrade way ",
"private": false, "private": false,
"type": "module", "type": "module",
@@ -46,5 +46,5 @@
"typeorm": "^0.3.11", "typeorm": "^0.3.11",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "ea18a5ad151b296fda54fb5bcbe64c7d80cdff2f" "gitHead": "3cedef4974708d828fb972acc54af0515e3ec3a0"
} }
+24
View File
@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.21](https://github.com/certd/certd/compare/v1.36.20...v1.36.21) (2025-09-15)
### Bug Fixes
* 修复ssl.com报EMAILADDRESS数量不对的bug ([c560cc5](https://github.com/certd/certd/commit/c560cc5adda6e15bf3a8865d874042550a6c2688))
## [1.36.20](https://github.com/certd/certd/compare/v1.36.19...v1.36.20) (2025-09-13)
### Bug Fixes
* 修复证书手动托管时新上传的证书无效的bug ([506385e](https://github.com/certd/certd/commit/506385e5a2600887fe30854e0713583caaa2e689))
### Performance Improvements
* 证书到期剩余天数进度条根据实际证书有效期计算 ([#528](https://github.com/certd/certd/issues/528)) nicheng-he ([2d4586b](https://github.com/certd/certd/commit/2d4586b1c42c39f97d2a95b9453cca4bc8bfbe61))
* add preferred chain option ([#519](https://github.com/certd/certd/issues/519)) @ZeroClover ([902359f](https://github.com/certd/certd/commit/902359f24ed12eee4f9b65178f1d6a60378351d2))
## [1.36.19](https://github.com/certd/certd/compare/v1.36.18...v1.36.19) (2025-09-05)
### Performance Improvements
* 支持ssl.com证书颁发机构 ([27b6dfa](https://github.com/certd/certd/commit/27b6dfa4d2ab3bddd284c3a34511a72e1a513a4c))
* 子域名托管说明 ([39a0223](https://github.com/certd/certd/commit/39a02235cf4416bb5bd1acd3831241efeaa2f602))
## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28) ## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28)
### Performance Improvements ### Performance Improvements
+6 -6
View File
@@ -1,7 +1,7 @@
{ {
"name": "@certd/plugin-cert", "name": "@certd/plugin-cert",
"private": false, "private": false,
"version": "1.36.18", "version": "1.36.21",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -16,10 +16,10 @@
"pub": "npm publish" "pub": "npm publish"
}, },
"dependencies": { "dependencies": {
"@certd/acme-client": "^1.36.18", "@certd/acme-client": "^1.36.21",
"@certd/basic": "^1.36.18", "@certd/basic": "^1.36.21",
"@certd/pipeline": "^1.36.18", "@certd/pipeline": "^1.36.21",
"@certd/plugin-lib": "^1.36.18", "@certd/plugin-lib": "^1.36.21",
"@google-cloud/publicca": "^1.3.0", "@google-cloud/publicca": "^1.3.0",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"jszip": "^3.10.1", "jszip": "^3.10.1",
@@ -43,5 +43,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "ea18a5ad151b296fda54fb5bcbe64c7d80cdff2f" "gitHead": "3cedef4974708d828fb972acc54af0515e3ec3a0"
} }
@@ -12,7 +12,7 @@ export class EabAccess extends BaseAccess {
component: { component: {
placeholder: "kid / keyId", placeholder: "kid / keyId",
}, },
helper: "EAB KID google的叫 keyId", helper: "EAB KID google的叫 keyIdssl.com的叫Account/ACME Key",
required: true, required: true,
encrypt: true, encrypt: true,
}) })
@@ -50,7 +50,7 @@ export type CertInfo = {
one?: string; one?: string;
p7b?: string; p7b?: string;
}; };
export type SSLProvider = "letsencrypt" | "google" | "zerossl"; export type SSLProvider = "letsencrypt" | "google" | "zerossl" | "sslcom";
export type PrivateKeyType = "rsa_1024" | "rsa_2048" | "rsa_3072" | "rsa_4096" | "ec_256" | "ec_384" | "ec_521"; export type PrivateKeyType = "rsa_1024" | "rsa_2048" | "rsa_3072" | "rsa_4096" | "ec_256" | "ec_384" | "ec_521";
type AcmeServiceOptions = { type AcmeServiceOptions = {
userContext: IContext; userContext: IContext;
@@ -329,8 +329,9 @@ export class AcmeService {
isTest?: boolean; isTest?: boolean;
privateKeyType?: string; privateKeyType?: string;
profile?: string; profile?: string;
preferredChain?: string;
}): Promise<CertInfo> { }): Promise<CertInfo> {
const { email, isTest, csrInfo, dnsProvider, domainsVerifyPlan, profile } = options; const { email, isTest, csrInfo, dnsProvider, domainsVerifyPlan, profile, preferredChain } = options;
const client: acme.Client = await this.getAcmeClient(email, isTest); const client: acme.Client = await this.getAcmeClient(email, isTest);
let domains = options.domains; let domains = options.domains;
@@ -373,6 +374,7 @@ export class AcmeService {
commonName, commonName,
...csrInfo, ...csrInfo,
altNames, altNames,
// emailAddress: email,
}, },
privateKey privateKey
); );
@@ -403,6 +405,7 @@ export class AcmeService {
}, },
signal: this.options.signal, signal: this.options.signal,
profile, profile,
preferredChain,
}); });
const crtString = crt.toString(); const crtString = crt.toString();
@@ -99,6 +99,7 @@ export abstract class CertApplyBaseConvertPlugin extends AbstractTaskPlugin {
const cert: CertInfo = certReader.toCertInfo(); const cert: CertInfo = certReader.toCertInfo();
this.cert = cert; this.cert = cert;
this._result.pipelineVars.certEffectiveTime = dayjs(certReader.detail.notBefore).valueOf();
this._result.pipelineVars.certExpiresTime = dayjs(certReader.detail.notAfter).valueOf(); this._result.pipelineVars.certExpiresTime = dayjs(certReader.detail.notAfter).valueOf();
if (!this._result.pipelinePrivateVars) { if (!this._result.pipelinePrivateVars) {
this._result.pipelinePrivateVars = {}; this._result.pipelinePrivateVars = {};
@@ -35,6 +35,7 @@ export class CertReader {
detail: CertificateInfo; detail: CertificateInfo;
//毫秒时间戳 //毫秒时间戳
effective: number;
expires: number; expires: number;
constructor(certInfo: CertInfo) { constructor(certInfo: CertInfo) {
this.cert = certInfo; this.cert = certInfo;
@@ -52,8 +53,9 @@ export class CertReader {
} }
try { try {
const { detail, expires } = this.getCrtDetail(this.cert.crt); const { detail, effective, expires } = this.getCrtDetail(this.cert.crt);
this.detail = detail; this.detail = detail;
this.effective = effective.getTime();
this.expires = expires.getTime(); this.expires = expires.getTime();
} catch (e) { } catch (e) {
throw new Error("证书解析失败:" + e.message); throw new Error("证书解析失败:" + e.message);
@@ -102,8 +104,9 @@ export class CertReader {
static readCertDetail(crt: string) { static readCertDetail(crt: string) {
const detail = crypto.readCertificateInfo(crt.toString()); const detail = crypto.readCertificateInfo(crt.toString());
const effective = detail.notBefore;
const expires = detail.notAfter; const expires = detail.notAfter;
return { detail, expires }; return { detail, effective, expires };
} }
getAllDomains() { getAllDomains() {
@@ -221,10 +224,10 @@ export class CertReader {
return `${prefix}_${domain}_${timeStr}.${suffix}`; return `${prefix}_${domain}_${timeStr}.${suffix}`;
} }
buildCertName() { buildCertName(prefix: string = "") {
let domain = this.getMainDomain(); let domain = this.getMainDomain();
domain = domain.replaceAll(".", "_").replaceAll("*", "_"); domain = domain.replaceAll(".", "_").replaceAll("*", "_");
return `${domain}_${dayjs().format("YYYYMMDDHHmmssSSS")}`; return `${prefix}_${domain}_${dayjs().format("YYYYMMDDHHmmssSSS")}`;
} }
static appendTimeSuffix(name?: string) { static appendTimeSuffix(name?: string) {
@@ -118,7 +118,7 @@ export class CertApplyUploadPlugin extends CertApplyBaseConvertPlugin {
} }
async execute(): Promise<string | void> { async execute(): Promise<string | void> {
const certReader = await this.getCertFromStore(); let certReader = await this.getCertFromStore();
const crtMd5 = this.ctx.utils.hash.md5(certReader.cert.crt); const crtMd5 = this.ctx.utils.hash.md5(certReader.cert.crt);
const leftDays = dayjs(certReader.expires).diff(dayjs(), "day"); const leftDays = dayjs(certReader.expires).diff(dayjs(), "day");
@@ -141,9 +141,13 @@ export class CertApplyUploadPlugin extends CertApplyBaseConvertPlugin {
this.logger.info("输入参数有变化,重新部署"); this.logger.info("输入参数有变化,重新部署");
} }
certReader = new CertReader(this.uploadCert);
this.clearLastStatus(); this.clearLastStatus();
//输出证书MD5 //输出证书MD5
this.certMd5 = crtMd5; this.certMd5 = this.ctx.utils.hash.md5(certReader.cert.crt);
const newLeftDays = dayjs(certReader.expires).diff(dayjs(), "day");
this.logger.info(`新证书过期时间${dayjs(certReader.expires).format("YYYY-MM-DD HH:mm:ss")},剩余${newLeftDays}`);
await this.output(certReader, true); await this.output(certReader, true);
//必须output之后执行 //必须output之后执行
@@ -38,6 +38,53 @@ export type DomainsVerifyPlanInput = {
[key: string]: DomainVerifyPlanInput; [key: string]: DomainVerifyPlanInput;
}; };
const preferredChainConfigs = {
letsencrypt: {
helper: "如无特殊需求保持默认即可",
options: [
{ value: "ISRG Root X1", label: "ISRG Root X1" },
{ value: "ISRG Root X2", label: "ISRG Root X2" },
],
},
google: {
helper: "GlobalSign 提供对老旧设备更好的兼容性,但证书链会变长",
options: [
{ value: "GTS Root R1", label: "GTS Root R1" },
{ value: "GlobalSign", label: "GlobalSign" },
],
},
} as const;
const preferredChainSupportedProviders = Object.keys(preferredChainConfigs);
const preferredChainMergeScript = (() => {
const configs = JSON.stringify(preferredChainConfigs);
const supportedProviders = JSON.stringify(preferredChainSupportedProviders);
const defaultProvider = JSON.stringify(preferredChainSupportedProviders[0]);
return `
const chainConfigs = ${configs};
const supportedProviders = ${supportedProviders};
const defaultProvider = ${defaultProvider};
const getConfig = (provider)=> chainConfigs[provider] || chainConfigs[defaultProvider];
return {
show: ctx.compute(({form})=> supportedProviders.includes(form.sslProvider)),
component: {
options: ctx.compute(({form})=> getConfig(form.sslProvider).options)
},
helper: ctx.compute(({form})=> getConfig(form.sslProvider).helper),
value: ctx.compute(({form})=>{
const { options } = getConfig(form.sslProvider);
const allowed = options.map(item=>item.value);
const current = form.preferredChain;
if(allowed.includes(current)){
return current;
}
return allowed[0];
})
};
`;
})();
@IsTaskPlugin({ @IsTaskPlugin({
name: "CertApply", name: "CertApply",
title: "证书申请(JS版)", title: "证书申请(JS版)",
@@ -89,9 +136,10 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
{ value: "letsencrypt", label: "Let's Encrypt", icon: "simple-icons:letsencrypt" }, { value: "letsencrypt", label: "Let's Encrypt", icon: "simple-icons:letsencrypt" },
{ value: "google", label: "Google", icon: "flat-color-icons:google" }, { value: "google", label: "Google", icon: "flat-color-icons:google" },
{ value: "zerossl", label: "ZeroSSL", icon: "emojione:digit-zero" }, { value: "zerossl", label: "ZeroSSL", icon: "emojione:digit-zero" },
{ value: "sslcom", label: "SSL.com(仅主域名和www免费)", icon: "la:expeditedssl" },
], ],
}, },
helper: "Let's Encrypt:申请最简单\nGoogle:大厂光环,兼容性好,仅首次需要翻墙获取EAB授权\nZeroSSL:需要EAB授权,无需翻墙", helper: "Let's Encrypt:申请最简单\nGoogle:大厂光环,兼容性好,仅首次需要翻墙获取EAB授权\nZeroSSL:需要EAB授权,无需翻墙\nSSL.com:仅主域名和www免费,必须设置CAA记录",
required: true, required: true,
}) })
sslProvider!: SSLProvider; sslProvider!: SSLProvider;
@@ -194,6 +242,13 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
}) })
zerosslCommonEabAccessId!: number; zerosslCommonEabAccessId!: number;
@TaskInput({
title: "SSL.com公共EAB授权",
isSys: true,
show: false,
})
sslcomCommonEabAccessId!: number;
@TaskInput({ @TaskInput({
title: "EAB授权", title: "EAB授权",
component: { component: {
@@ -203,11 +258,16 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
maybeNeed: true, maybeNeed: true,
required: false, required: false,
helper: helper:
"需要提供EAB授权\nZeroSSL:请前往[zerossl开发者中心](https://app.zerossl.com/developer),生成 'EAB Credentials'\n Google:请查看[google获取eab帮助文档](https://certd.docmirror.cn/guide/use/google/),用过一次后会绑定邮箱,后续复用EAB要用同一个邮箱", "需要提供EAB授权" +
"\nZeroSSL:请前往[zerossl开发者中心](https://app.zerossl.com/developer),生成 'EAB Credentials'" +
"\nGoogle:请查看[google获取eab帮助文档](https://certd.docmirror.cn/guide/use/google/),用过一次后会绑定邮箱,后续复用EAB要用同一个邮箱" +
"\nSSL.com:[SSL.com账号页面](https://secure.ssl.com/account),然后点击api credentials链接,然后点击编辑按钮,查看Secret key和HMAC key",
mergeScript: ` mergeScript: `
return { return {
show: ctx.compute(({form})=>{ show: ctx.compute(({form})=>{
return (form.sslProvider === 'zerossl' && !form.zerosslCommonEabAccessId) || (form.sslProvider === 'google' && !form.googleCommonEabAccessId) return (form.sslProvider === 'zerossl' && !form.zerosslCommonEabAccessId)
|| (form.sslProvider === 'google' && !form.googleCommonEabAccessId)
|| (form.sslProvider === 'sslcom' && !form.sslcomCommonEabAccessId)
}) })
} }
`, `,
@@ -279,6 +339,19 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
}) })
certProfile!: string; certProfile!: string;
@TaskInput({
title: "首选链",
component: {
name: "a-select",
vModel: "value",
options: preferredChainConfigs.letsencrypt.options,
},
helper: preferredChainConfigs.letsencrypt.helper,
required: false,
mergeScript: preferredChainMergeScript,
})
preferredChain!: string;
@TaskInput({ @TaskInput({
title: "使用代理", title: "使用代理",
value: false, value: false,
@@ -339,8 +412,8 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
async onInit() { async onInit() {
let eab: EabAccess = null; let eab: EabAccess = null;
if (this.sslProvider === "google") { if (this.sslProvider && this.sslProvider !== "letsencrypt") {
if (this.googleAccessId) { if (this.sslProvider === "google" && this.googleAccessId) {
this.logger.info("当前正在使用 google服务账号授权获取EAB"); this.logger.info("当前正在使用 google服务账号授权获取EAB");
const googleAccess = await this.getAccess(this.googleAccessId); const googleAccess = await this.getAccess(this.googleAccessId);
const googleClient = new GoogleClient({ const googleClient = new GoogleClient({
@@ -348,24 +421,19 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
logger: this.logger, logger: this.logger,
}); });
eab = await googleClient.getEab(); eab = await googleClient.getEab();
} else if (this.eabAccessId) {
this.logger.info("当前正在使用 google EAB授权");
eab = await this.getAccess(this.eabAccessId);
} else if (this.googleCommonEabAccessId) {
this.logger.info("当前正在使用 google 公共EAB授权");
eab = await this.getAccess(this.googleCommonEabAccessId, true);
} else { } else {
throw new Error("google需要配置EAB授权或服务账号授权"); const getEab = async (type: string) => {
} if (this.eabAccessId) {
} else if (this.sslProvider === "zerossl") { this.logger.info(`当前正在使用 ${type} EAB授权`);
if (this.eabAccessId) { eab = await this.getAccess(this.eabAccessId);
this.logger.info("当前正在使用 zerossl EAB授权"); } else if (this[`${type}CommonEabAccessId`]) {
eab = await this.getAccess(this.eabAccessId); this.logger.info(`当前正在使用 ${type} 公共EAB授权`);
} else if (this.zerosslCommonEabAccessId) { eab = await this.getAccess(this[`${type}CommonEabAccessId`], true);
this.logger.info("当前正在使用 zerossl 公共EAB授权"); } else {
eab = await this.getAccess(this.zerosslCommonEabAccessId, true); throw new Error(`${type}需要配置EAB授权`);
} else { }
throw new Error("zerossl需要配置EAB授权"); };
await getEab(this.sslProvider);
} }
} }
this.eab = eab; this.eab = eab;
@@ -397,12 +465,12 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
const csrInfo = _.merge( const csrInfo = _.merge(
{ {
country: "CN", // country: "CN",
state: "GuangDong", // state: "GuangDong",
locality: "ShengZhen", // locality: "ShengZhen",
organization: "CertD Org.", // organization: "CertD Org.",
organizationUnit: "IT Department", // organizationUnit: "IT Department",
emailAddress: email, // emailAddress: email,
}, },
this.csrInfo ? JSON.parse(this.csrInfo) : {} this.csrInfo ? JSON.parse(this.csrInfo) : {}
); );
@@ -430,6 +498,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
isTest: false, isTest: false,
privateKeyType: this.privateKeyType, privateKeyType: this.privateKeyType,
profile: this.certProfile, profile: this.certProfile,
preferredChain: this.preferredChain,
}); });
const certInfo = this.formatCerts(cert); const certInfo = this.formatCerts(cert);
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.21](https://github.com/certd/certd/compare/v1.36.20...v1.36.21) (2025-09-15)
**Note:** Version bump only for package @certd/plugin-lib
## [1.36.20](https://github.com/certd/certd/compare/v1.36.19...v1.36.20) (2025-09-13)
### Performance Improvements
* ssh配置增加脚本类型设置,bash还是sh ([ae41c60](https://github.com/certd/certd/commit/ae41c6038b27c9476e64a2402a8daf247c38a5b6))
## [1.36.19](https://github.com/certd/certd/compare/v1.36.18...v1.36.19) (2025-09-05)
### Performance Improvements
* ssh 增加超时断开连接,默认10分钟超时 ([c24a040](https://github.com/certd/certd/commit/c24a040c19cacafc79228d7a7649af93837d94a1))
## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28) ## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28)
### Performance Improvements ### Performance Improvements
+4 -4
View File
@@ -1,7 +1,7 @@
{ {
"name": "@certd/plugin-lib", "name": "@certd/plugin-lib",
"private": false, "private": false,
"version": "1.36.18", "version": "1.36.21",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -21,8 +21,8 @@
"@alicloud/pop-core": "^1.7.10", "@alicloud/pop-core": "^1.7.10",
"@alicloud/tea-util": "^1.4.10", "@alicloud/tea-util": "^1.4.10",
"@aws-sdk/client-s3": "^3.787.0", "@aws-sdk/client-s3": "^3.787.0",
"@certd/basic": "^1.36.18", "@certd/basic": "^1.36.21",
"@certd/pipeline": "^1.36.18", "@certd/pipeline": "^1.36.21",
"@kubernetes/client-node": "0.21.0", "@kubernetes/client-node": "0.21.0",
"ali-oss": "^6.22.0", "ali-oss": "^6.22.0",
"basic-ftp": "^5.0.5", "basic-ftp": "^5.0.5",
@@ -53,5 +53,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "ea18a5ad151b296fda54fb5bcbe64c7d80cdff2f" "gitHead": "3cedef4974708d828fb972acc54af0515e3ec3a0"
} }
@@ -64,6 +64,22 @@ export class SshAccess extends BaseAccess {
}) })
passphrase!: string; passphrase!: string;
@AccessInput({
title: "脚本类型",
helper: "bash 、sh 、fish",
component: {
name: "a-select",
vModel: "value",
options: [
{ value: "default", label: "默认" },
{ value: "sh", label: "sh" },
{ value: "bash", label: "bash" },
{ value: "fish", label: "fish(不支持set -e)" },
],
},
})
scriptType: string;
@AccessInput({ @AccessInput({
title: "伪终端", title: "伪终端",
helper: "如果登录报错:all authentication methods failed,可以尝试开启伪终端模式进行keyboard-interactive方式登录\n开启后对日志输出有一定的影响", helper: "如果登录报错:all authentication methods failed,可以尝试开启伪终端模式进行keyboard-interactive方式登录\n开启后对日志输出有一定的影响",
@@ -86,6 +102,15 @@ export class SshAccess extends BaseAccess {
}) })
socksProxy!: string; socksProxy!: string;
@AccessInput({
title: "超时时间",
helper: "执行命令的超时时间,单位秒,默认30分钟",
component: {
name: "a-input-number",
},
})
timeout: number;
@AccessInput({ @AccessInput({
title: "是否Windows", title: "是否Windows",
helper: "如果是Windows主机,请勾选此项\n并且需要windows[安装OpenSSH](https://certd.docmirror.cn/guide/use/host/windows.html)", helper: "如果是Windows主机,请勾选此项\n并且需要windows[安装OpenSSH](https://certd.docmirror.cn/guide/use/host/windows.html)",
@@ -136,9 +161,10 @@ export class SshAccess extends BaseAccess {
const { SshClient } = await import("./ssh.js"); const { SshClient } = await import("./ssh.js");
const client = new SshClient(this.ctx.logger); const client = new SshClient(this.ctx.logger);
const script = ["echo hello", "exit"];
await client.exec({ await client.exec({
connectConf: this, connectConf: this,
script: "echo hello", script: script,
}); });
return "ok"; return "ok";
} }
+18 -4
View File
@@ -469,7 +469,8 @@ export class SshClient {
async isCmd(conn: AsyncSsh2Client) { async isCmd(conn: AsyncSsh2Client) {
const spec = await conn.exec("echo %COMSPEC% "); const spec = await conn.exec("echo %COMSPEC% ");
if (spec.toString().trim() === "%COMSPEC%") { const ret = spec.toString().trim();
if (ret.includes("%COMSPEC%") && !ret.includes("echo %COMSPEC%")) {
return false; return false;
} else { } else {
return true; return true;
@@ -542,8 +543,16 @@ export class SshClient {
} }
} }
if (isLinux && options.stopOnError !== false) { if (isLinux) {
script = "set -e\n" + script; if (options.connectConf.scriptType == "bash") {
script = "#!/usr/bin/env bash \n" + script;
} else if (options.connectConf.scriptType == "sh") {
script = "#!/bin/sh\n" + script;
}
if (options.connectConf.scriptType != "fish" && options.stopOnError !== false) {
script = "set -e\n" + script;
}
} }
return await conn.exec(script as string, { throwOnStdErr }); return await conn.exec(script as string, { throwOnStdErr });
@@ -587,10 +596,15 @@ export class SshClient {
} }
throw e; throw e;
} }
let timeoutId = null;
try { try {
timeoutId = setTimeout(() => {
this.logger.info("执行超时,断开连接");
conn.end();
}, 1000 * (connectConf.timeout || 1800));
return await callable(conn); return await callable(conn);
} finally { } finally {
clearTimeout(timeoutId);
conn.end(); conn.end();
} }
} }
@@ -76,9 +76,26 @@ export class TencentSslClient {
return res; return res;
} }
async DescribeCertificates(params: any) { async DescribeHostUploadUpdateRecordDetail(params: any) {
const client = await this.getSslClient(); const client = await this.getSslClient();
const res = await client.DescribeCertificates(params); const res = await client.request("DescribeHostUploadUpdateRecordDetail", params);
this.checkRet(res);
return res;
}
async UploadUpdateCertificateInstance(params: any) {
const client = await this.getSslClient();
const res = await client.request("UploadUpdateCertificateInstance", params);
this.checkRet(res);
return res;
}
async DescribeCertificates(params: { Limit?: number; Offset?: number; SearchKey?: string }) {
const client = await this.getSslClient();
const res = await client.DescribeCertificates({
ExpirationSort: "ASC",
...params,
});
this.checkRet(res); this.checkRet(res);
return res; return res;
} }
+36
View File
@@ -3,6 +3,42 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.21](https://github.com/certd/certd/compare/v1.36.20...v1.36.21) (2025-09-15)
### Bug Fixes
* 修复导入插件对话框无法打开的bug,修复插件编辑页面打开多个代码编辑器消失的bug ([e5a080a](https://github.com/certd/certd/commit/e5a080aebe0d2f3e3c0f86bf863f75069c1bf7ab))
## [1.36.20](https://github.com/certd/certd/compare/v1.36.19...v1.36.20) (2025-09-13)
### Bug Fixes
* 修复商业版退出登录后,丢失站点个性化设置的bug ([d75dd05](https://github.com/certd/certd/commit/d75dd058d65c85f80c49e1fa7a910e6c6f08e824))
* 修复授权类型和名称字段排到最后的bug ([43b7977](https://github.com/certd/certd/commit/43b79778ea9034065f6a15af3296274315597c6b))
### Performance Improvements
* 登录支持极验验证码 ([370db62](https://github.com/certd/certd/commit/370db62bf0aece241859244927beabba32d6a257))
* 登录注册、找回密码都支持极验验证码和图片验证码 ([7bdde68](https://github.com/certd/certd/commit/7bdde68ecea29fe2c570fd3cb082139db6c93d93))
* 优化加量包展示效果 ([3c65f37](https://github.com/certd/certd/commit/3c65f37d84177ba107d4a6462648af12d2fc4b7a))
* 证书到期剩余天数进度条根据实际证书有效期计算 ([#528](https://github.com/certd/certd/issues/528)) nicheng-he ([2d4586b](https://github.com/certd/certd/commit/2d4586b1c42c39f97d2a95b9453cca4bc8bfbe61))
## [1.36.19](https://github.com/certd/certd/compare/v1.36.18...v1.36.19) (2025-09-05)
### Bug Fixes
* 修复批量流水线执行时日志显示错乱的问题 ([4372adc](https://github.com/certd/certd/commit/4372adc703b9a4c785664054ab2a533626d815a8))
* 修复远程数据选择无法过滤的bug ([6cbb073](https://github.com/certd/certd/commit/6cbb0739f8428d51b0712f718fe4d236cc087cf9))
* 修复mysql下购买套餐加量包无效的bug ([c26ad4c](https://github.com/certd/certd/commit/c26ad4c8075f0606d45b8da13915737968d6191a))
### Performance Improvements
* 创建证书时支持选择通知时机 ([0e96bfd](https://github.com/certd/certd/commit/0e96bfdfa377824d204e72923d1176408ae6b300))
* 商业版隐藏文档相关链接 ([4443a1c](https://github.com/certd/certd/commit/4443a1c0308fa6b95a05efd73d15d24b65d641c9))
* 商业版隐藏文档相关链接 ([db89561](https://github.com/certd/certd/commit/db8956148083bc4f988226ccf719940d08158a27))
* 支持根据id更新证书(证书Id不变接口),不过该接口为白名单功能,普通腾讯云账户无法使用 ([fe9c4f3](https://github.com/certd/certd/commit/fe9c4f3391ff07c01dd9a252225f69a129c39050))
* 子域名托管说明 ([39a0223](https://github.com/certd/certd/commit/39a02235cf4416bb5bd1acd3831241efeaa2f602))
## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28) ## [1.36.18](https://github.com/certd/certd/compare/v1.36.17...v1.36.18) (2025-08-28)
### Bug Fixes ### Bug Fixes
+1
View File
@@ -23,5 +23,6 @@
</div> </div>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
<script src="https://static.geetest.com/v4/gt4.js"></script>
</body> </body>
</html> </html>
+10 -9
View File
@@ -1,6 +1,6 @@
{ {
"name": "@certd/ui-client", "name": "@certd/ui-client",
"version": "1.36.18", "version": "1.36.21",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite --open", "dev": "vite --open",
@@ -32,10 +32,11 @@
"@aws-sdk/s3-request-presigner": "^3.535.0", "@aws-sdk/s3-request-presigner": "^3.535.0",
"@certd/vue-js-cron-light": "^4.0.14", "@certd/vue-js-cron-light": "^4.0.14",
"@ctrl/tinycolor": "^4.1.0", "@ctrl/tinycolor": "^4.1.0",
"@fast-crud/fast-crud": "^1.25.13", "@fast-crud/editor-code": "^1.26.6",
"@fast-crud/fast-extends": "^1.25.13", "@fast-crud/fast-crud": "^1.26.6",
"@fast-crud/ui-antdv4": "^1.25.13", "@fast-crud/fast-extends": "^1.26.6",
"@fast-crud/ui-interface": "^1.25.13", "@fast-crud/ui-antdv4": "^1.26.6",
"@fast-crud/ui-interface": "^1.26.6",
"@iconify/tailwind": "^1.2.0", "@iconify/tailwind": "^1.2.0",
"@iconify/vue": "^4.1.1", "@iconify/vue": "^4.1.1",
"@manypkg/get-packages": "^2.2.2", "@manypkg/get-packages": "^2.2.2",
@@ -103,8 +104,8 @@
"zod-defaults": "^0.1.3" "zod-defaults": "^0.1.3"
}, },
"devDependencies": { "devDependencies": {
"@certd/lib-iframe": "^1.36.18", "@certd/lib-iframe": "^1.36.21",
"@certd/pipeline": "^1.36.18", "@certd/pipeline": "^1.36.21",
"@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12", "@types/chai": "^4.3.12",
@@ -138,7 +139,7 @@
"prettier": "3.3.3", "prettier": "3.3.3",
"pretty-quick": "^4.0.0", "pretty-quick": "^4.0.0",
"rimraf": "^5.0.5", "rimraf": "^5.0.5",
"rollup": "^4.13.0", "rollup": "4.50.0",
"rollup-plugin-visualizer": "^5.12.0", "rollup-plugin-visualizer": "^5.12.0",
"stylelint": "^15.11.0", "stylelint": "^15.11.0",
"stylelint-order": "^6.0.4", "stylelint-order": "^6.0.4",
@@ -148,7 +149,7 @@
"tslint": "^6.1.3", "tslint": "^6.1.3",
"typescript": "^5.4.2", "typescript": "^5.4.2",
"unplugin-vue-define-options": "^1.4.2", "unplugin-vue-define-options": "^1.4.2",
"vite": "^5.3.1", "vite": "5.4.19",
"vite-plugin-compression": "^0.5.1", "vite-plugin-compression": "^0.5.1",
"vite-plugin-html": "^3.2.2", "vite-plugin-html": "^3.2.2",
"vite-plugin-theme": "^0.8.6", "vite-plugin-theme": "^0.8.6",
@@ -0,0 +1,50 @@
<template>
<component :is="captchaComponent" v-if="settingStore.inited" ref="captchaRef" class="captcha_input" :captcha-get="getCaptcha" @change="onChange" />
</template>
<script setup lang="ts">
import { ref, computed, defineAsyncComponent } from "vue";
import { useSettingStore } from "/@/store/settings";
import { nanoid } from "nanoid";
import { request } from "/@/api/service";
const captchaRef = ref(null);
const settingStore = useSettingStore();
const emits = defineEmits(["update:modelValue", "change"]);
const captchaImpls = import.meta.glob("./captchas/*.vue");
const captchaAddonId = computed(() => {
return settingStore.sysPublic.captchaAddonId ?? 0;
});
const captchaComponent = computed(() => {
let type = "image";
if (settingStore.sysPublic.captchaAddonId && settingStore.sysPublic.captchaType) {
type = settingStore.sysPublic.captchaType;
}
const componentName = `${type}_captcha`;
return defineAsyncComponent(captchaImpls[`./captchas/${componentName}.vue`]);
});
async function getCaptcha(): Promise<any> {
const randomStr = nanoid(10);
return await request({
url: `/basic/code/captcha/get?randomStr=${randomStr}`,
method: "post",
data: {
captchaAddonId: captchaAddonId.value,
},
});
}
function onChange(data) {
emits("update:modelValue", data);
emits("change", data);
}
async function getCaptchaForm() {
return await captchaRef.value.getCaptchaForm();
}
defineExpose({
getCaptchaForm,
});
</script>
@@ -0,0 +1,96 @@
<template>
<div ref="captchaRef" class="geetest_captcha_wrapper"></div>
</template>
<script setup lang="ts">
import { onMounted, defineProps, defineEmits, ref, onUnmounted } from "vue";
import { useSettingStore } from "/@/store/settings";
import { request } from "/src/api/service";
import { notification } from "ant-design-vue";
defineOptions({
name: "GeetestCaptcha",
});
const emit = defineEmits(["update:modelValue", "change"]);
const props = defineProps<{
captchaGet: () => Promise<any>;
}>();
const captchaRef = ref(null);
// const addonApi = createAddonApi();
const settingStore = useSettingStore();
const captchaInstanceRef = ref({});
async function init() {
// if (!initGeetest4) {
// await import("https://static.geetest.com/v4/gt4.js");
// }
const { captchaId } = await props.captchaGet();
// @ts-ignore
initGeetest4(
{
captchaId: captchaId,
},
(captcha: any) => {
// captcha
captcha.appendTo(captchaRef.value); // appendTo
captchaInstanceRef.value.instance = captcha;
captchaInstanceRef.value.captchaId = captchaId;
}
);
}
function getCaptchaForm() {
if (!captchaInstanceRef.value?.instance) {
// notification.error({
// message: "",
// });
return false;
}
const result = captchaInstanceRef.value.instance.getValidate();
if (!result) {
// notification.error({
// message: "",
// });
return false;
}
result.captcha_id = captchaInstanceRef.value.captchaId;
return result;
}
const valueRef = ref(null);
const timeoutId = setInterval(() => {
const form = getCaptchaForm();
if (form && valueRef.value != form) {
console.log("form", form);
valueRef.value = form;
emitChange(form);
}
}, 1000);
onUnmounted(() => {
clearTimeout(timeoutId);
});
function emitChange(value: string) {
emit("update:modelValue", value);
emit("change", value);
}
defineExpose({
getCaptchaForm,
});
onMounted(async () => {
await init();
});
</script>
<style lang="less">
.geetest_captcha_wrapper {
.geetest_captcha {
.geetest_holder {
width: 100%;
}
}
}
</style>
@@ -0,0 +1,59 @@
<template>
<div class="flex">
<a-input :value="valueRef" placeholder="请输入图片验证码" autocomplete="off" @update:value="onChange">
<template #prefix>
<fs-icon icon="ion:image-outline"></fs-icon>
</template>
</a-input>
<div class="input-right pointer" title="点击刷新">
<img class="image-code" :src="imageCodeSrc" @click="resetImageCode" />
</div>
</div>
</template>
<script setup lang="ts">
import { defineEmits, defineExpose, defineProps, ref } from "vue";
import { nanoid } from "nanoid";
const props = defineProps<{
captchaGet?: () => Promise<any>;
}>();
defineOptions({
name: "ImageCaptcha",
});
const emit = defineEmits(["update:modelValue", "change"]);
const valueRef = ref("");
const randomStrRef = ref();
const imageCodeSrc = ref();
async function resetImageCode() {
const res = await props.captchaGet();
randomStrRef.value = res.randomStr;
valueRef.value = "";
emitChange(null);
imageCodeSrc.value = "data:image/svg+xml," + encodeURIComponent(res.imageData);
}
function getCaptchaForm() {
return {
imageCode: valueRef.value,
randomStr: randomStrRef.value,
};
}
defineExpose({
resetImageCode,
getCaptchaForm,
});
resetImageCode();
function onChange(value: string) {
valueRef.value = value;
const form = getCaptchaForm();
emitChange(form);
}
function emitChange(value) {
emit("update:modelValue", value);
emit("change", value);
}
</script>
@@ -66,7 +66,7 @@ const getOptions = async () => {
const input = (pluginType === "plugin" ? form?.input : form) || {}; const input = (pluginType === "plugin" ? form?.input : form) || {};
for (let key in define.input) { for (let key in define.input) {
const inWatches = props.watches.includes(key); const inWatches = props.watches?.includes(key);
const inputDefine = define.input[key]; const inputDefine = define.input[key];
if (inWatches && inputDefine.required) { if (inWatches && inputDefine.required) {
const value = input[key]; const value = input[key];
@@ -105,7 +105,7 @@ const getOptions = async () => {
const input = (pluginType === "plugin" ? form?.input : form) || {}; const input = (pluginType === "plugin" ? form?.input : form) || {};
for (let key in define.input) { for (let key in define.input) {
const inWatches = props.watches.includes(key); const inWatches = props.watches?.includes(key);
const inputDefine = define.input[key]; const inputDefine = define.input[key];
if (inWatches && inputDefine.required) { if (inWatches && inputDefine.required) {
const value = input[key]; const value = input[key];
@@ -169,7 +169,7 @@ const getOptions = async () => {
}; };
const filterOption = (input: string, option: any) => { const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 || String(option.value).toLowerCase().indexOf(input.toLowerCase()); return option.label.toLowerCase().includes(input.toLowerCase()) || String(option.value).toLowerCase().includes(input.toLowerCase());
}; };
async function onClick() { async function onClick() {
@@ -0,0 +1,191 @@
<template>
<div class="remote-select">
<div class="flex flex-row">
<a-tree-select class="remote-tree-select-input" :tree-data="optionsRef" :value="value" tree-checkable allow-clear v-bind="attrs" @click="onClick" @update:value="emit('update:value', $event)"> </a-tree-select>
<div class="ml-5">
<fs-button :loading="loading" title="刷新选项" icon="ion:refresh-outline" @click="refreshOptions"></fs-button>
</div>
</div>
<div class="helper" :class="{ error: hasError }">
{{ message }}
</div>
</div>
</template>
<script setup lang="ts">
import { ComponentPropsType, doRequest } from "/@/components/plugins/lib";
import { defineComponent, inject, ref, useAttrs, watch, Ref } from "vue";
import { PluginDefine } from "@certd/pipeline";
defineOptions({
name: "RemoteTreeSelect",
});
const props = defineProps<
{
watches: string[];
search?: boolean;
pager?: boolean;
} & ComponentPropsType
>();
const emit = defineEmits<{
"update:value": any;
}>();
const attrs = useAttrs();
const getCurrentPluginDefine: any = inject("getCurrentPluginDefine", () => {
return {};
});
const getScope: any = inject("get:scope", () => {
return {};
});
const getPluginType: any = inject("get:plugin:type", () => {
return "plugin";
});
const searchKeyRef = ref("");
const optionsRef = ref([]);
const message = ref("");
const hasError = ref(false);
const loading = ref(false);
const pagerRef: Ref = ref({
current: 1,
});
const getOptions = async () => {
if (loading.value) {
return;
}
if (!getCurrentPluginDefine) {
return;
}
const define: PluginDefine = getCurrentPluginDefine()?.value;
if (!define) {
return;
}
const pluginType = getPluginType();
const { form } = getScope();
const input = (pluginType === "plugin" ? form?.input : form) || {};
for (let key in define.input) {
const inWatches = props.watches?.includes(key);
const inputDefine = define.input[key];
if (inWatches && inputDefine.required) {
const value = input[key];
if (value == null || value === "") {
console.log("remote-select required", key);
return;
}
}
}
message.value = "";
hasError.value = false;
loading.value = true;
const pageNo = pagerRef.value.pageNo;
const pageSize = pagerRef.value.pageSize;
try {
const res = await doRequest(
{
type: pluginType,
typeName: form.type,
action: props.action,
input,
data: {
searchKey: props.search ? searchKeyRef.value : "",
pageNo,
pageSize,
},
},
{
onError(err: any) {
hasError.value = true;
message.value = `获取选项出错:${err.message}`;
},
showErrorNotify: false,
}
);
const list = res?.list || res || [];
if (list.length > 0) {
message.value = "获取数据成功,请从下拉框中选择";
} else {
message.value = "获取数据成功,没有数据";
}
optionsRef.value = list;
pagerRef.value.total = list.length;
if (props.pager) {
if (res.pageNo != null) {
pagerRef.value.pageNo = res.pageNo ?? 1;
}
if (res.pageSize != null) {
pagerRef.value.pageSize = res.pageSize ?? 100;
}
if (res.total != null) {
pagerRef.value.total = res.total ?? list.length;
}
}
return res;
} finally {
loading.value = false;
}
};
async function onClick() {
if (optionsRef.value?.length === 0) {
await refreshOptions();
}
}
async function refreshOptions() {
await getOptions();
}
async function doSearch() {
pagerRef.value.pageNo = 1;
await refreshOptions();
}
watch(
() => {
const pluginType = getPluginType();
const { form, key } = getScope();
const input = (pluginType === "plugin" ? form?.input : form) || {};
const watches = {};
for (const key of props.watches) {
//@ts-ignore
watches[key] = input[key];
}
return {
form: watches,
key,
};
},
async (value, oldValue) => {
const { form } = value;
const oldForm: any = oldValue?.form;
let changed = oldForm == null || optionsRef.value.length == 0;
for (const key of props.watches) {
//@ts-ignore
if (oldForm && form[key] != oldForm[key]) {
changed = true;
break;
}
}
if (changed) {
await getOptions();
}
},
{
immediate: true,
}
);
async function onPageChange(current: any) {
await refreshOptions();
}
</script>
<style lang="less"></style>
@@ -2,6 +2,7 @@ import SynologyIdDeviceGetter from "./synology/device-id-getter.vue";
import RemoteAutoComplete from "./common/remote-auto-complete.vue"; import RemoteAutoComplete from "./common/remote-auto-complete.vue";
import RemoteSelect from "./common/remote-select.vue"; import RemoteSelect from "./common/remote-select.vue";
import RemoteInput from "./common/remote-input.vue"; import RemoteInput from "./common/remote-input.vue";
import RemoteTreeSelect from "./common/remote-tree-select.vue";
import CertDomainsGetter from "./common/cert-domains-getter.vue"; import CertDomainsGetter from "./common/cert-domains-getter.vue";
import OutputSelector from "/@/components/plugins/common/output-selector/index.vue"; import OutputSelector from "/@/components/plugins/common/output-selector/index.vue";
import DnsProviderSelector from "/@/components/plugins/cert/dns-provider-selector/index.vue"; import DnsProviderSelector from "/@/components/plugins/cert/dns-provider-selector/index.vue";
@@ -24,6 +25,7 @@ export default {
app.component("SynologyDeviceIdGetter", SynologyIdDeviceGetter); app.component("SynologyDeviceIdGetter", SynologyIdDeviceGetter);
app.component("RemoteAutoComplete", RemoteAutoComplete); app.component("RemoteAutoComplete", RemoteAutoComplete);
app.component("RemoteSelect", RemoteSelect); app.component("RemoteSelect", RemoteSelect);
app.component("RemoteTreeSelect", RemoteTreeSelect);
app.component("RemoteInput", RemoteInput); app.component("RemoteInput", RemoteInput);
app.component("CertDomainsGetter", CertDomainsGetter); app.component("CertDomainsGetter", CertDomainsGetter);
app.component("InputPassword", InputPassword); app.component("InputPassword", InputPassword);
@@ -21,6 +21,7 @@ export default {
pipeline: "Pipeline", pipeline: "Pipeline",
domain: "Domain", domain: "Domain",
deployTimes: "Deployments", deployTimes: "Deployments",
monitorCount: "DomainMonitors",
duration: "Duration", duration: "Duration",
price: "Price", price: "Price",
paymentMethod: "Payment Method", paymentMethod: "Payment Method",
@@ -118,6 +119,7 @@ export default {
scheduledTaskCount: "Scheduled Task Count", scheduledTaskCount: "Scheduled Task Count",
deployTaskCount: "Deployment Task Count", deployTaskCount: "Deployment Task Count",
remainingValidity: "Remaining Validity", remainingValidity: "Remaining Validity",
effectiveTime: "Effective time",
expiryTime: "Expiry Time", expiryTime: "Expiry Time",
status: "Status", status: "Status",
lastRun: "Last Run", lastRun: "Last Run",
@@ -218,6 +220,7 @@ export default {
triggerCronHelper: triggerCronHelper:
"Click the button above to choose a daily execution time.\nIt is recommended to trigger once per day. The task will be skipped if the certificate has not expired and will not be executed repeatedly.", "Click the button above to choose a daily execution time.\nIt is recommended to trigger once per day. The task will be skipped if the certificate has not expired and will not be executed repeatedly.",
notificationTitle: "Failure Notification", notificationTitle: "Failure Notification",
notificationWhen: "Notification Timing",
notificationHelper: "Get real-time alerts when the task fails", notificationHelper: "Get real-time alerts when the task fails",
groupIdTitle: "Pipeline Group", groupIdTitle: "Pipeline Group",
}, },
@@ -248,7 +251,9 @@ export default {
ok: "Valid", ok: "Valid",
expired: "Expired", expired: "Expired",
}, },
certEffectiveTime: "Certificate Effective",
certExpiresTime: "Certificate Expiration", certExpiresTime: "Certificate Expiration",
remainingValidity: "Remaining Validity",
expired: "expired", expired: "expired",
days: "days", days: "days",
lastCheckTime: "Last Check Time", lastCheckTime: "Last Check Time",
@@ -463,6 +468,7 @@ export default {
validDays: "Valid Days", validDays: "Valid Days",
expires: " expires", expires: " expires",
days: " days", days: " days",
effectiveTime: "Effective Time",
expireTime: "Expiration Time", expireTime: "Expiration Time",
certIssuer: "Certificate Issuer", certIssuer: "Certificate Issuer",
applyTime: "Application Time", applyTime: "Application Time",
@@ -705,10 +711,23 @@ export default {
pipeline: "Pipeline", pipeline: "Pipeline",
}, },
addonType: "Type",
addonName: "Name",
addonNameHelper: "Fill freely, helps to distinguish when multiple same type exist",
addonTypeSelect: "Select type",
sys: { sys: {
setting: { setting: {
showRunStrategy: "Show RunStrategy", showRunStrategy: "Show RunStrategy",
showRunStrategyHelper: "Allow modify the run strategy of the task", showRunStrategyHelper: "Allow modify the run strategy of the task",
captchaEnabled: "Enable Login Captcha",
captchaHelper: "Whether to enable captcha verification for login",
captchaType: "Captcha Setting",
baseSetting: "Base Settings",
registerSetting: "Register Settings",
safeSetting: "Safe Settings",
paymentSetting: "Payment Settings",
}, },
}, },
modal: { modal: {
@@ -729,4 +748,8 @@ export default {
challengeSetting: "Challenge Setting", challengeSetting: "Challenge Setting",
gotoCnameTip: "Please go to CNAME Record Page", gotoCnameTip: "Please go to CNAME Record Page",
}, },
addonSelector: {
select: "Select",
placeholder: "select please",
},
}; };
@@ -25,6 +25,7 @@ export default {
pipeline: "流水线", pipeline: "流水线",
domain: "域名", domain: "域名",
deployTimes: "部署次数", deployTimes: "部署次数",
monitorCount: "域名监控数",
duration: "时长", duration: "时长",
price: "价格", price: "价格",
paymentMethod: "支付方式", paymentMethod: "支付方式",
@@ -124,6 +125,7 @@ export default {
scheduledTaskCount: "定时任务数", scheduledTaskCount: "定时任务数",
deployTaskCount: "部署任务数", deployTaskCount: "部署任务数",
remainingValidity: "到期剩余", remainingValidity: "到期剩余",
effectiveTime: "生效时间",
expiryTime: "过期时间", expiryTime: "过期时间",
status: "状态", status: "状态",
lastRun: "最后运行", lastRun: "最后运行",
@@ -223,6 +225,7 @@ export default {
triggerCronTitle: "定时触发", triggerCronTitle: "定时触发",
triggerCronHelper: "点击上面的按钮,选择每天几点定时执行。\n建议设置为每天触发一次,证书未到期之前任务会跳过,不会重复执行", triggerCronHelper: "点击上面的按钮,选择每天几点定时执行。\n建议设置为每天触发一次,证书未到期之前任务会跳过,不会重复执行",
notificationTitle: "失败通知", notificationTitle: "失败通知",
notificationWhen: "通知时机",
notificationHelper: "任务执行失败实时提醒", notificationHelper: "任务执行失败实时提醒",
groupIdTitle: "流水线分组", groupIdTitle: "流水线分组",
}, },
@@ -253,7 +256,9 @@ export default {
ok: "正常", ok: "正常",
expired: "过期", expired: "过期",
}, },
certEffectiveTime: "证书生效时间",
certExpiresTime: "证书到期时间", certExpiresTime: "证书到期时间",
remainingValidity: "到期剩余",
expired: "过期", expired: "过期",
days: "天", days: "天",
lastCheckTime: "上次检查时间", lastCheckTime: "上次检查时间",
@@ -469,6 +474,7 @@ export default {
validDays: "有效天数", validDays: "有效天数",
expires: "过期", expires: "过期",
days: "天", days: "天",
effectiveTime: "生效时间",
expireTime: "过期时间", expireTime: "过期时间",
certIssuer: "证书颁发机构", certIssuer: "证书颁发机构",
applyTime: "申请时间", applyTime: "申请时间",
@@ -693,7 +699,10 @@ export default {
setAsDefault: "设为默认", setAsDefault: "设为默认",
disabledLabel: "禁用", disabledLabel: "禁用",
confirmToggleStatus: "确定要{action}吗?", confirmToggleStatus: "确定要{action}吗?",
addonType: "类型",
addonName: "名称",
addonNameHelper: "随意填写,相同类型助于区分即可",
addonTypeSelect: "请选择",
template: { template: {
title: "流水线模版", title: "流水线模版",
edit: "流水线模版编辑", edit: "流水线模版编辑",
@@ -712,6 +721,15 @@ export default {
setting: { setting: {
showRunStrategy: "显示运行策略选择", showRunStrategy: "显示运行策略选择",
showRunStrategyHelper: "任务设置中是否允许选择运行策略", showRunStrategyHelper: "任务设置中是否允许选择运行策略",
captchaEnabled: "启用登录验证码",
captchaHelper: "登录时是否启用验证码",
captchaType: "验证码配置",
baseSetting: "基本设置",
registerSetting: "注册设置",
safeSetting: "安全设置",
paymentSetting: "支付设置",
}, },
}, },
modal: { modal: {
@@ -732,4 +750,8 @@ export default {
challengeSetting: "校验配置", challengeSetting: "校验配置",
gotoCnameTip: "CNAME域名配置请前往CNAME记录页面添加", gotoCnameTip: "CNAME域名配置请前往CNAME记录页面添加",
}, },
addonSelector: {
select: "选择",
placeholder: "请选择",
},
}; };
@@ -0,0 +1,12 @@
import { useSettingStore } from "/@/store/settings";
export default {
mounted(el: any, binding: any, vnode: any) {
const settingStore = useSettingStore();
const isComm = settingStore.isComm;
const { value } = binding;
if ((value === false && isComm) || (value === true && !isComm)) {
el.parentNode && el.parentNode.removeChild(el);
}
},
};
@@ -0,0 +1,8 @@
import comm from "./comm-show.js";
const install = function (app: any) {
app.directive("comm", comm);
};
export default {
install,
};
@@ -2,7 +2,7 @@ import { request } from "/src/api/service";
// import "/src/mock"; // import "/src/mock";
import { ColumnCompositionProps, CrudOptions, FastCrud, PageQuery, PageRes, setLogger, TransformResProps, useColumns, UseCrudProps, UserPageQuery, useTypes, utils } from "@fast-crud/fast-crud"; import { ColumnCompositionProps, CrudOptions, FastCrud, PageQuery, PageRes, setLogger, TransformResProps, useColumns, UseCrudProps, UserPageQuery, useTypes, utils } from "@fast-crud/fast-crud";
import "@fast-crud/fast-crud/dist/style.css"; import "@fast-crud/fast-crud/dist/style.css";
import { FsExtendsCopyable, FsExtendsEditor, FsExtendsJson, FsExtendsTime, FsExtendsUploader, FsExtendsInput, FsUploaderS3SignedUrlType, FsUploaderGetAuthContext, FsUploaderAliossSTS } from "@fast-crud/fast-extends"; import { FsExtendsCopyable, FsExtendsEditor, FsExtendsJson, FsExtendsTime, FsExtendsUploader, FsExtendsInput } from "@fast-crud/fast-extends";
import "@fast-crud/fast-extends/dist/style.css"; import "@fast-crud/fast-extends/dist/style.css";
import UiAntdv from "@fast-crud/ui-antdv4"; import UiAntdv from "@fast-crud/ui-antdv4";
import "@fast-crud/ui-antdv4/dist/style.css"; import "@fast-crud/ui-antdv4/dist/style.css";
@@ -13,6 +13,9 @@ import { notification } from "ant-design-vue";
import { usePreferences } from "/@/vben/preferences"; import { usePreferences } from "/@/vben/preferences";
import { LocalStorage } from "/@/utils/util.storage"; import { LocalStorage } from "/@/utils/util.storage";
import { FsEditorCode } from "@fast-crud/editor-code";
import "@fast-crud/editor-code/dist/style.css"
class ColumnSizeSaver { class ColumnSizeSaver {
save: (key: string, size: number) => void; save: (key: string, size: number) => void;
constructor() { constructor() {
@@ -272,6 +275,7 @@ function install(app: App, options: any = {}) {
app.use(FsExtendsTime); app.use(FsExtendsTime);
app.use(FsExtendsCopyable); app.use(FsExtendsCopyable);
app.use(FsExtendsInput); app.use(FsExtendsInput);
app.use(FsEditorCode);
const { addTypes, getType } = useTypes(); const { addTypes, getType } = useTypes();
//此处演示修改官方字段类型 //此处演示修改官方字段类型
@@ -4,9 +4,13 @@ import FastCrud from "./fast-crud";
import permission from "./permission"; import permission from "./permission";
import { App } from "vue"; import { App } from "vue";
import "./validator/index.js"; import "./validator/index.js";
import directives from "./directive/index";
import { setupMonaco } from "./monaco";
function install(app: App, options: any = {}) { function install(app: App, options: any = {}) {
app.use(FastCrud, options); app.use(FastCrud, options);
app.use(permission); app.use(permission);
app.use(directives);
setupMonaco();
} }
export default { export default {
@@ -0,0 +1,15 @@
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker";
import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
import yamlWorker from "./yaml.worker?worker";
import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
import { registerWorker } from "@fast-crud/editor-code";
export function setupMonaco() {
registerWorker("json", jsonWorker);
registerWorker(["css", "less", "scss"], cssWorker);
registerWorker(["html", "handlebars", "razor"], htmlWorker);
registerWorker(["yaml", "yml"], yamlWorker);
registerWorker(["typescript", "javascript"], tsWorker);
registerWorker("*", editorWorker);
}
@@ -0,0 +1 @@
export * from "monaco-yaml/yaml.worker.js";
@@ -167,7 +167,7 @@ export const usePluginStore = defineStore({
}, },
async clear() { async clear() {
this.group = null; this.group = null;
this.originGroup = null this.originGroup = null;
}, },
async getList(): Promise<PluginDefine[]> { async getList(): Promise<PluginDefine[]> {
await this.init(); await this.init();
@@ -46,6 +46,10 @@ export type SysPublicSetting = {
aiChatEnabled?: boolean; aiChatEnabled?: boolean;
showRunStrategy?: boolean; showRunStrategy?: boolean;
captchaEnabled?: boolean;
captchaType?: number;
captchaAddonId?: number;
}; };
export type SuiteSetting = { export type SuiteSetting = {
enabled?: boolean; enabled?: boolean;
@@ -12,6 +12,7 @@ import { cloneDeep, merge } from "lodash-es";
import { useI18n } from "/src/locales"; import { useI18n } from "/src/locales";
import dayjs from "dayjs"; import dayjs from "dayjs";
export interface SettingState { export interface SettingState {
skipReset?: boolean; // 注销登录时,不清空此store的状态
sysPublic?: SysPublicSetting; sysPublic?: SysPublicSetting;
installInfo?: { installInfo?: {
siteId: string; siteId: string;
@@ -64,6 +65,7 @@ const defaultSiteInfo: SiteInfo = {
export const useSettingStore = defineStore({ export const useSettingStore = defineStore({
id: "app.setting", id: "app.setting",
state: (): SettingState => ({ state: (): SettingState => ({
skipReset: true,
plusInfo: { plusInfo: {
isPlus: false, isPlus: false,
vipType: "free", vipType: "free",
@@ -38,6 +38,9 @@ export function resetAllStores() {
} }
const allStores = (pinia as any)._s; const allStores = (pinia as any)._s;
for (const [_key, store] of allStores) { for (const [_key, store] of allStores) {
if (store.skipReset) {
continue;
}
store.$reset(); store.$reset();
} }
} }
@@ -105,9 +105,11 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
form: { form: {
rules: [{ required: true, message: t("certd.pleaseEnterName") }], rules: [{ required: true, message: t("certd.pleaseEnterName") }],
helper: t("certd.nameHelper"), helper: t("certd.nameHelper"),
order: -2,
}, },
column: { column: {
width: 200, width: 200,
order: -2,
}, },
}, },
from: { from: {
@@ -87,6 +87,7 @@ export function getCommonColumnDefine(crudExpose: any, typeRef: any, api: any) {
order: -1, order: -1,
}, },
form: { form: {
order: -1,
component: { component: {
disabled: false, disabled: false,
showSearch: true, showSearch: true,
@@ -79,6 +79,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
}, },
form: { form: {
rules: [{ required: true, message: "必填项" }], rules: [{ required: true, message: "必填项" }],
order: -11,
}, },
column: { column: {
width: 300, width: 300,
@@ -0,0 +1,178 @@
<template>
<div class="addon-selector">
<div class="flex-o w-100">
<!-- <fs-dict-select class="flex-1" :value="modelValue" :dict="optionsDictRef" :disabled="disabled" :render-label="renderLabel" :slots="selectSlots" :allow-clear="true" v-bind="select" @update:value="onChange" />-->
<span v-if="modelValue" class="mr-5 cd-flex-inline">
<a-tag class="mr-5" color="green">{{ target?.name || modelValue }}</a-tag>
<fs-icon class="cd-icon-button" icon="ion:close-circle-outline" @click="clear"></fs-icon>
</span>
<span v-else class="mlr-5 text-gray">{{ placeholder || t("certd.addonSelector.placeholder") }}</span>
<fs-table-select
ref="tableSelectRef"
class="flex-0"
:model-value="modelValue"
:dict="optionsDictRef"
:create-crud-options="createCrudOptionsWithApi"
:crud-options-override="{
search: { show: false },
table: {
scroll: {
x: 540,
},
},
}"
:show-current="false"
:show-select="false"
:dialog="{ width: 960 }"
:destroy-on-close="false"
height="400px"
v-bind="tableSelect"
@update:model-value="onChange"
@dialog-closed="doRefresh"
>
<template #default="scope">
<fs-button class="ml-5" :disabled="disabled" :size="size" type="primary" :text="t('certd.addonSelector.select')" @click="scope.open" />
</template>
</fs-table-select>
</div>
</div>
</template>
<script lang="tsx" setup>
import { inject, ref, Ref, watch } from "vue";
import { createAddonApi } from "../api";
import { message } from "ant-design-vue";
import { dict } from "@fast-crud/fast-crud";
import createCrudOptions from "../crud";
import { addonProvide } from "../common";
import { useUserStore } from "/@/store/user";
import { useI18n } from "/src/locales";
const { t } = useI18n();
defineOptions({
name: "AddonSelector",
});
const props = defineProps<{
modelValue?: number | string | number[] | string[];
type?: string;
placeholder?: string;
size?: string;
disabled?: boolean;
select?: any;
tableSelect?: any;
addonType: string;
from?: string;
}>();
const onChange = async (value: number) => {
await emitValue(value);
};
const emit = defineEmits(["update:modelValue", "selected-change", "change"]);
const api = createAddonApi({
from: props.from,
addonType: props.addonType,
});
addonProvide(api);
function createCrudOptionsWithApi(opts: any) {
opts.context = {
api,
addonType: props.addonType,
};
return createCrudOptions(opts);
}
const tableSelectRef = ref();
const optionsDictRef = dict({
url: `/addon/options?addonType=${props.addonType}`,
value: "id",
label: "name",
});
const renderLabel = (option: any) => {
return <span>{option.name}</span>;
};
async function openTableSelectDialog() {
selectOpened.value = false;
await tableSelectRef.value.open({});
await tableSelectRef.value.crudExpose.openAdd({});
}
const selectOpened = ref(false);
const selectSlots = ref({
dropdownRender({ menuNode, props }: any) {
const res = [];
res.push(menuNode);
// res.push(<a-divider style="margin: 4px 0" />);
// res.push(<a-space style="padding: 4px 8px" />);
// res.push(<fs-button class="w-100" type="text" icon="plus-outlined" text="" onClick={openTableSelectDialog}></fs-button>);
return res;
},
});
const target: Ref<any> = ref({});
function clear() {
if (props.disabled) {
return;
}
emitValue(null);
}
const userStore = useUserStore();
async function emitValue(value: any) {
// target.value = optionsDictRef.dataMap[value];
const userId = userStore.userInfo.id;
if (pipeline?.value && pipeline.value.userId !== userId) {
message.error(`对不起,您不能修改他人流水线的${props.addonType}设置`);
return;
}
emit("change", value);
emit("update:modelValue", value);
}
async function refreshTarget(value: any) {
if (value > 0) {
target.value = await api.GetSimpleInfo(value);
} else {
target.value = {
//captchaType
type: "",
};
}
}
watch(
() => {
return props.modelValue;
},
async value => {
// await optionsDictRef.loadDict();
//@ts-ignore
await refreshTarget(value);
// target.value = optionsDictRef.dataMap[value];
emit("selected-change", target.value);
},
{
immediate: true,
}
);
//pipeline
const pipeline = inject("pipeline", null);
async function doRefresh() {
await optionsDictRef.reloadDict();
}
</script>
<style lang="less">
.addon-selector {
width: 100%;
}
</style>
@@ -0,0 +1,142 @@
import { request } from "/src/api/service";
import { RequestHandleReq } from "/@/components/plugins/lib";
export function createAddonApi(opts: { from: any; addonType: string }) {
let apiPrefix = "/addon";
if (opts.from === "sys") {
apiPrefix = "/sys/addon";
}
return {
async GetList(query: any) {
return await request({
url: apiPrefix + "/page",
method: "post",
data: {
...query,
query: {
addonType: opts.addonType,
...query.query,
},
},
});
},
async AddObj(obj: any) {
return await request({
url: apiPrefix + "/add",
method: "post",
data: {
...obj,
addonType: opts.addonType,
},
});
},
async UpdateObj(obj: any) {
return await request({
url: apiPrefix + "/update",
method: "post",
data: obj,
});
},
async DelObj(id: number) {
return await request({
url: apiPrefix + "/delete",
method: "post",
params: { id },
});
},
async GetObj(id: number) {
return await request({
url: apiPrefix + "/info",
method: "post",
params: { id },
});
},
async GetOptions(id: number) {
return await request({
url: apiPrefix + `/options?addonType=${opts.addonType}`,
method: "post",
});
},
async SetDefault(id: number) {
return await request({
url: apiPrefix + "/setDefault",
method: "post",
params: { id },
});
},
async GetDefaultId() {
return await request({
url: apiPrefix + "/getDefaultId",
method: "post",
});
},
async GetSimpleInfo(id: number) {
return await request({
url: apiPrefix + `/simpleInfo?addonType=${opts.addonType}`,
method: "post",
params: { id },
});
},
async GetDefineTypes() {
return await request({
url: apiPrefix + `/getTypeDict?addonType=${opts.addonType}`,
method: "post",
});
},
async GetProviderDefine(type: string) {
return await request({
url: apiPrefix + `/define?addonType=${opts.addonType}`,
method: "post",
params: { type },
});
},
async GetProviderDefineByType(type: string) {
return await request({
url: apiPrefix + `/defineByType?addonType=${opts.addonType}`,
method: "post",
params: { type },
});
},
async Handle(req: RequestHandleReq, opts: any = {}) {
const url = `/handle/${req.type}?addonType=${opts.addonType}`;
const { typeName, action, data, input } = req;
const res = await request({
url,
method: "post",
data: {
typeName,
action,
data,
input,
},
...opts,
});
return res;
},
};
}
export const AddonTypeDefines = {
captcha: {
name: "captcha",
title: "验证码",
showDefault: false,
showTest: false,
},
};
export function getAddonTypeDefine(addonType: string) {
return AddonTypeDefines[addonType];
}

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