Compare commits

..

67 Commits

Author SHA1 Message Date
xiaojunnuo
7feece597a v1.35.5 2025-06-20 17:14:13 +08:00
xiaojunnuo
fa16c782ca build: prepare to build 2025-06-20 17:10:08 +08:00
xiaojunnuo
a03d0b6a4a chore: 2025-06-20 17:09:59 +08:00
xiaojunnuo
dff76b8912 build: prepare to build 2025-06-20 17:07:59 +08:00
xiaojunnuo
cffea9a9bc chore: 2025-06-20 17:07:26 +08:00
xiaojunnuo
43fee42198 perf: 首次打开任务日志查看页面,自动滚动到底部 2025-06-20 17:06:34 +08:00
xiaojunnuo
5cd3968929 fix: 腾讯云授权支持设置是否国际站,部署到EO插件支持国际站 2025-06-20 16:58:20 +08:00
xiaojunnuo
65dcae79f8 fix: 修复邮箱包含.号校验失败的bug 2025-06-20 16:45:29 +08:00
xiaojunnuo
e11b3becfd perf: 支持批量修改通知和定时 2025-06-18 12:29:43 +08:00
xiaojunnuo
73fa937f5c chore: 2025-06-17 13:39:40 +08:00
xiaojunnuo
6ebe2e54ac chore: 2025-06-16 23:57:11 +08:00
xiaojunnuo
fb29a11cc9 build: publish 2025-06-13 12:24:08 +08:00
xiaojunnuo
a9e06cbf92 build: trigger build image 2025-06-13 12:23:51 +08:00
xiaojunnuo
93017c044d v1.35.4 2025-06-13 12:22:12 +08:00
xiaojunnuo
c223ddbb9a build: prepare to build 2025-06-13 12:19:21 +08:00
xiaojunnuo
f00aeacb8b perf: 支持s3 access做测试 2025-06-13 12:18:26 +08:00
xiaojunnuo
5b49071d6b Merge remote-tracking branch 'origin/v2-dev' into v2-dev 2025-06-13 09:53:30 +08:00
xiaojunnuo
17053a882b chore: 2025-06-13 09:53:07 +08:00
xiaojunnuo
5e723d31a4 chore: 2025-06-13 00:40:54 +08:00
xiaojunnuo
3283bd8b75 build: publish 2025-06-13 00:40:05 +08:00
xiaojunnuo
770d3c0015 build: trigger build image 2025-06-13 00:39:33 +08:00
xiaojunnuo
d15dfafd5d v1.35.3 2025-06-13 00:29:59 +08:00
xiaojunnuo
545c13d55c build: prepare to build 2025-06-13 00:27:18 +08:00
xiaojunnuo
e2099ac9ca fix: 修复重试次数设置无效的bug 2025-06-13 00:25:08 +08:00
xiaojunnuo
c937583a50 fix: 修复消息内容存在()<>等括号情况下无法发送tg通知的bug 2025-06-13 00:24:55 +08:00
xiaojunnuo
43c7a19849 perf: 支持雨云dns解析以及雨云证书更新 2025-06-12 23:51:21 +08:00
xiaojunnuo
83543487e7 perf: 支持雨云dns解析 2025-06-12 22:41:08 +08:00
xiaojunnuo
434b259525 chore: 2025-06-11 22:40:21 +08:00
xiaojunnuo
add8efaba8 chore: 2025-06-10 18:44:32 +08:00
xiaojunnuo
12ed79ca60 chore: 雨云支持 2025-06-10 18:41:25 +08:00
xiaojunnuo
1e863382d3 perf: 授权列表类型颜色优化 2025-06-10 18:40:23 +08:00
xiaojunnuo
bad3504d4a perf: github 版本检查支持执行脚本 2025-06-10 12:13:04 +08:00
xiaojunnuo
d94f207162 chore: 首创流水线模式自动申请和部署证书,已被多个项目“借鉴”,被抄也是一种成功。 2025-06-10 10:59:40 +08:00
xiaojunnuo
2c4b7781a4 chore: 2025-06-10 10:52:58 +08:00
xiaojunnuo
4574c6ff07 chore: 2025-06-10 10:52:30 +08:00
xiaojunnuo
7b5043e87b chore: 增加专业版过期通知 2025-06-10 10:21:09 +08:00
xiaojunnuo
a06f3ac5da build: publish 2025-06-09 23:58:39 +08:00
xiaojunnuo
721346a40a build: trigger build image 2025-06-09 23:58:23 +08:00
xiaojunnuo
f252871fb8 v1.35.2 2025-06-09 23:57:04 +08:00
xiaojunnuo
107196122c build: prepare to build 2025-06-09 23:54:50 +08:00
xiaojunnuo
563c02d8da build: prepare to build 2025-06-09 23:50:44 +08:00
xiaojunnuo
765934970a chore: 数据库 2025-06-09 23:50:19 +08:00
xiaojunnuo
9cbdfda829 perf: 优化阿里云nlb支持部署扩展证书 2025-06-09 23:41:44 +08:00
xiaojunnuo
c1fbc8cd68 fix: 修复阿里云新加坡clb无法部署证书的bug 2025-06-09 23:41:21 +08:00
xiaojunnuo
a92107cc47 fix: 修复检查github release 插件无法保存最后版本的bug 2025-06-09 23:35:17 +08:00
xiaojunnuo
3e84e116e8 fix: 修复阿里云新加坡clb无法部署证书的bug 2025-06-09 22:46:59 +08:00
xiaojunnuo
7c0cdd169e perf: 子域名托管帮助链接优化为打开新窗口
Closes https://github.com/certd/certd/issues/419
2025-06-09 22:28:56 +08:00
xiaojunnuo
424fd96615 perf: 阿里云dns操作增加重试机制 2025-06-09 11:42:25 +08:00
xiaojunnuo
ebfcea88da chore: 2025-06-09 11:38:07 +08:00
xiaojunnuo
3c7eb2f5e2 chore: 小优化 2025-06-09 11:32:06 +08:00
xiaojunnuo
936167972f fix: 修复站点监控定时器多次添加的bug 2025-06-09 11:14:45 +08:00
xiaojunnuo
7f6070c960 perf: history增加触发类型显示 2025-06-09 11:13:51 +08:00
xiaojunnuo
0aea9c129c build: publish 2025-06-07 09:12:22 +08:00
xiaojunnuo
d20fb7daa8 build: trigger build image 2025-06-07 09:12:06 +08:00
xiaojunnuo
a619f8a2fe v1.35.1 2025-06-07 09:10:46 +08:00
xiaojunnuo
0acb858d7b build: prepare to build 2025-06-07 09:08:51 +08:00
xiaojunnuo
e459be76fe build: prepare to build 2025-06-07 09:05:35 +08:00
xiaojunnuo
c4c59ccc75 revert: 2025-06-07 01:19:47 +08:00
xiaojunnuo
c820315409 perf: 优化流水线页面,增加下次执行时间、查看证书显示 2025-06-07 01:19:37 +08:00
xiaojunnuo
2a19b61b7a perf: aliyun alb支持部署扩展证书 2025-06-07 00:15:16 +08:00
xiaojunnuo
e1cf64ae16 perf: 修改 HTTPS 服务器监听地址
- 将 HTTPS服务器的监听地址从 '0.0.0.0' 修改为 '::',以支持 IPv6

https://github.com/certd/certd/issues/416
2025-06-06 22:27:41 +08:00
xiaojunnuo
d3c2f8eb43 perf: 站点证书监控支持定时设置,重试次数设置 2025-06-06 18:20:30 +08:00
xiaojunnuo
a00453c83a fix: 修复站点监控通知渠道设置无效的bug 2025-06-06 16:12:30 +08:00
xiaojunnuo
2eb0e54909 perf: 证书申请支持letencrypt profile选项 2025-06-06 15:12:24 +08:00
xiaojunnuo
ac87bc57e9 fix: 某些证书提供商的证书确实commonName导致无法转换证书的问题 2025-06-06 13:53:05 +08:00
xiaojunnuo
2b8ea857f0 build: publish 2025-06-06 00:12:42 +08:00
xiaojunnuo
11c52114b2 build: trigger build image 2025-06-06 00:12:26 +08:00
106 changed files with 2542 additions and 563 deletions

View File

@@ -3,6 +3,69 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.35.5](https://github.com/certd/certd/compare/v1.35.4...v1.35.5) (2025-06-20)
### Bug Fixes
* 腾讯云授权支持设置是否国际站部署到EO插件支持国际站 ([5cd3968](https://github.com/certd/certd/commit/5cd3968929acef333cf30d3b20cf21cea6c82c5f))
* 修复邮箱包含.号校验失败的bug ([65dcae7](https://github.com/certd/certd/commit/65dcae79f8faa7a6cb425e10a0fdb6758b0719f3))
### Performance Improvements
* 首次打开任务日志查看页面,自动滚动到底部 ([43fee42](https://github.com/certd/certd/commit/43fee42198e8697185b427b1fa3eb79409603393))
* 支持批量修改通知和定时 ([e11b3be](https://github.com/certd/certd/commit/e11b3becfd4abe6547e84d09adc38ebd6e1c4b87))
## [1.35.4](https://github.com/certd/certd/compare/v1.35.3...v1.35.4) (2025-06-13)
### Performance Improvements
* 支持s3 access做测试 ([f00aeac](https://github.com/certd/certd/commit/f00aeacb8b5c81f0bafa4c1b76723dec2b6b7784))
## [1.35.3](https://github.com/certd/certd/compare/v1.35.2...v1.35.3) (2025-06-12)
### Bug Fixes
* 修复消息内容存在()<>等括号情况下无法发送tg通知的bug ([c937583](https://github.com/certd/certd/commit/c937583a50d8513d76adead3648f83eee2fcc6f9))
* 修复重试次数设置无效的bug ([e2099ac](https://github.com/certd/certd/commit/e2099ac9ca344bc70bfa4219002e9138708973ae))
### Performance Improvements
* 授权列表类型颜色优化 ([1e86338](https://github.com/certd/certd/commit/1e863382d3d1a8cc95a1abf51e75bf6eaea3244f))
* 支持雨云dns解析 ([8354348](https://github.com/certd/certd/commit/83543487e7418683bd79cfe3b9e0d792bdb977f7))
* 支持雨云dns解析以及雨云证书更新 ([43c7a19](https://github.com/certd/certd/commit/43c7a1984926f5d4647760cc134bb0aede3a7b7a))
* github 版本检查支持执行脚本 ([bad3504](https://github.com/certd/certd/commit/bad3504d4a15e6989b967b66aa9da8c6981f25bf))
## [1.35.2](https://github.com/certd/certd/compare/v1.35.1...v1.35.2) (2025-06-09)
### Bug Fixes
* 修复阿里云新加坡clb无法部署证书的bug ([c1fbc8c](https://github.com/certd/certd/commit/c1fbc8cd68ae020ef342e4e92f4d9b4869ca1ead))
* 修复阿里云新加坡clb无法部署证书的bug ([3e84e11](https://github.com/certd/certd/commit/3e84e116e863b54c6b4d7db160af372dacc5857f))
* 修复检查github release 插件无法保存最后版本的bug ([a92107c](https://github.com/certd/certd/commit/a92107cc47133883b099d5228b06373e84c8bb50))
* 修复站点监控定时器多次添加的bug ([9361679](https://github.com/certd/certd/commit/936167972fe83e519bc01a0dd961d9c0635d24ab))
### Performance Improvements
* 阿里云dns操作增加重试机制 ([424fd96](https://github.com/certd/certd/commit/424fd96615c05e949af8c837c261c1400bdffba2))
* 优化阿里云nlb支持部署扩展证书 ([9cbdfda](https://github.com/certd/certd/commit/9cbdfda829b231733d54c66c5024d46e6fc11af3))
* 子域名托管帮助链接优化为打开新窗口 ([7c0cdd1](https://github.com/certd/certd/commit/7c0cdd169e2f943e703e433677f2f437d4aa02ee))
* history增加触发类型显示 ([7f6070c](https://github.com/certd/certd/commit/7f6070c960ed7bf02add5ab36436de6573f2f1fa))
## [1.35.1](https://github.com/certd/certd/compare/v1.35.0...v1.35.1) (2025-06-07)
### Bug Fixes
* 某些证书提供商的证书确实commonName导致无法转换证书的问题 ([ac87bc5](https://github.com/certd/certd/commit/ac87bc57e957ea4679707bfd38d6840e26319bed))
* 修复站点监控通知渠道设置无效的bug ([a00453c](https://github.com/certd/certd/commit/a00453c83a58114ce2873dd6e6aaf313f1ce0f87))
### Performance Improvements
* 修改 HTTPS 服务器监听地址 ([e1cf64a](https://github.com/certd/certd/commit/e1cf64ae16d4abfe4299ff16d5088c30cf3c6365))
* 优化流水线页面,增加下次执行时间、查看证书显示 ([c820315](https://github.com/certd/certd/commit/c8203154094fae3d17198747f49f5f41ddf29a4e))
* 站点证书监控支持定时设置,重试次数设置 ([d3c2f8e](https://github.com/certd/certd/commit/d3c2f8eb436e670772d14a54acd6b541c5aa3978))
* 证书申请支持letencrypt profile选项 ([2eb0e54](https://github.com/certd/certd/commit/2eb0e54909d8ad36708e07c12fd598998159bc43))
* aliyun alb支持部署扩展证书 ([2a19b61](https://github.com/certd/certd/commit/2a19b61b7a78620c06396c2cc37cc77d738b6d12))
# [1.35.0](https://github.com/certd/certd/compare/v1.34.11...v1.35.0) (2025-06-05)
### Features

View File

@@ -1,9 +1,9 @@
# Certd
Certd 是一个免费全自动申请和自动部署更新SSL证书管理系统。
后缀d取自linux守护进程的命名风格意为证书守护进程。
Certd® 是一个免费全自动证书管理系统,让你的网站证书永不过期
首创流水线申请部署证书模式,已被多个项目“借鉴”,被抄也是一种成功。
关键字:证书自动申请、证书自动更新、证书自动续期、证书自动续签、证书管理工具
>后缀d取自linux守护进程的命名风格意为证书守护进程。
> 关于证书续期:
>* 实际上没有办法不改变证书文件本身情况下直接续期或者续签。
@@ -13,6 +13,7 @@ Certd 是一个免费全自动申请和自动部署更新SSL证书的管理系
> 流水线数量现已调整为无限制,欢迎大家使用
## 一、特性
本项目不仅支持证书申请过程自动化,还可以自动化部署更新证书,让你的证书永不过期。
@@ -32,9 +33,6 @@ Certd 是一个免费全自动申请和自动部署更新SSL证书的管理系
![](./docs/images/intro/intro.svg)
## 二、在线体验
官方Demo地址自助注册后体验

View File

@@ -1 +1 @@
23:51
12:23

View File

@@ -3,6 +3,65 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.35.4](https://github.com/certd/certd/compare/v1.35.3...v1.35.4) (2025-06-13)
### Performance Improvements
* 支持s3 access做测试 ([f00aeac](https://github.com/certd/certd/commit/f00aeacb8b5c81f0bafa4c1b76723dec2b6b7784))
## [1.35.3](https://github.com/certd/certd/compare/v1.35.2...v1.35.3) (2025-06-12)
### Bug Fixes
* 修复消息内容存在()<>等括号情况下无法发送tg通知的bug ([c937583](https://github.com/certd/certd/commit/c937583a50d8513d76adead3648f83eee2fcc6f9))
* 修复重试次数设置无效的bug ([e2099ac](https://github.com/certd/certd/commit/e2099ac9ca344bc70bfa4219002e9138708973ae))
### Performance Improvements
* 授权列表类型颜色优化 ([1e86338](https://github.com/certd/certd/commit/1e863382d3d1a8cc95a1abf51e75bf6eaea3244f))
* 支持雨云dns解析 ([8354348](https://github.com/certd/certd/commit/83543487e7418683bd79cfe3b9e0d792bdb977f7))
* 支持雨云dns解析以及雨云证书更新 ([43c7a19](https://github.com/certd/certd/commit/43c7a1984926f5d4647760cc134bb0aede3a7b7a))
* github 版本检查支持执行脚本 ([bad3504](https://github.com/certd/certd/commit/bad3504d4a15e6989b967b66aa9da8c6981f25bf))
## [1.35.2](https://github.com/certd/certd/compare/v1.35.1...v1.35.2) (2025-06-09)
### Bug Fixes
* 修复阿里云新加坡clb无法部署证书的bug ([c1fbc8c](https://github.com/certd/certd/commit/c1fbc8cd68ae020ef342e4e92f4d9b4869ca1ead))
* 修复阿里云新加坡clb无法部署证书的bug ([3e84e11](https://github.com/certd/certd/commit/3e84e116e863b54c6b4d7db160af372dacc5857f))
* 修复检查github release 插件无法保存最后版本的bug ([a92107c](https://github.com/certd/certd/commit/a92107cc47133883b099d5228b06373e84c8bb50))
* 修复站点监控定时器多次添加的bug ([9361679](https://github.com/certd/certd/commit/936167972fe83e519bc01a0dd961d9c0635d24ab))
### Performance Improvements
* 阿里云dns操作增加重试机制 ([424fd96](https://github.com/certd/certd/commit/424fd96615c05e949af8c837c261c1400bdffba2))
* 优化阿里云nlb支持部署扩展证书 ([9cbdfda](https://github.com/certd/certd/commit/9cbdfda829b231733d54c66c5024d46e6fc11af3))
* 子域名托管帮助链接优化为打开新窗口 ([7c0cdd1](https://github.com/certd/certd/commit/7c0cdd169e2f943e703e433677f2f437d4aa02ee))
* history增加触发类型显示 ([7f6070c](https://github.com/certd/certd/commit/7f6070c960ed7bf02add5ab36436de6573f2f1fa))
## [1.35.1](https://github.com/certd/certd/compare/v1.35.0...v1.35.1) (2025-06-07)
### Bug Fixes
* 某些证书提供商的证书确实commonName导致无法转换证书的问题 ([ac87bc5](https://github.com/certd/certd/commit/ac87bc57e957ea4679707bfd38d6840e26319bed))
* 修复站点监控通知渠道设置无效的bug ([a00453c](https://github.com/certd/certd/commit/a00453c83a58114ce2873dd6e6aaf313f1ce0f87))
### Performance Improvements
* 修改 HTTPS 服务器监听地址 ([e1cf64a](https://github.com/certd/certd/commit/e1cf64ae16d4abfe4299ff16d5088c30cf3c6365))
* 优化流水线页面,增加下次执行时间、查看证书显示 ([c820315](https://github.com/certd/certd/commit/c8203154094fae3d17198747f49f5f41ddf29a4e))
* 站点证书监控支持定时设置,重试次数设置 ([d3c2f8e](https://github.com/certd/certd/commit/d3c2f8eb436e670772d14a54acd6b541c5aa3978))
* 证书申请支持letencrypt profile选项 ([2eb0e54](https://github.com/certd/certd/commit/2eb0e54909d8ad36708e07c12fd598998159bc43))
* aliyun alb支持部署扩展证书 ([2a19b61](https://github.com/certd/certd/commit/2a19b61b7a78620c06396c2cc37cc77d738b6d12))
# [1.35.0](https://github.com/certd/certd/compare/v1.34.11...v1.35.0) (2025-06-05)
### Features
* 完善注释 ([6702ca1](https://github.com/certd/certd/commit/6702ca10a17f5d7dbff789b039f7269496f66b97))
* AWS 中国区 CloudFront 证书部署IAM 证书) ([8a55bed](https://github.com/certd/certd/commit/8a55beda924b3be2a53b9ba80d9487cefa8bf887))
* **lego:** support for command options ([b84159f](https://github.com/certd/certd/commit/b84159f2f11531f058837c2e82d66499f3740f20))
## [1.34.11](https://github.com/certd/certd/compare/v1.34.10...v1.34.11) (2025-06-05)
### Bug Fixes

View File

@@ -10,15 +10,15 @@
* 登录宝塔面板,在菜单栏中点击 Docker首次进入会提示安装Docker服务点击立即安装按提示完成安装
### 2、部署certd
#### 2.1 应用商店一键部署【推荐】
以下两种方式人选一种:
#### 2.1 应用商店方式一键部署【推荐】
* 在宝塔Docker应用商店中找到`certd`(要先点右上角更新应用)
* 点击安装,配置域名等基本信息即可完成安装
> 需要宝塔9.2.0及以上版本才支持
#### 2.2 容器编排部署
#### 2.2 容器编排方式部署
1. 打开`docker-compose.yaml`,整个内容复制下来
https://gitee.com/certd/certd/raw/v2/docker/run/docker-compose.yaml
@@ -43,12 +43,15 @@ admin/123456
## 三、如何升级
宝塔升级certd非常简单
`docker`->`容器编排`->`左侧选择Certd`->`更新镜像`
打开容器页面: `docker`->`容器编排`->`左侧选择Certd`->`更新镜像`
![img.png](./images/upgrade.png)
## 四、数据备份
部署方式不同,数据保存位置不同
### 4.1 应用商店部署方式
点击进入安装路径,数据保存在`./data`目录下,可以手动备份
@@ -62,7 +65,6 @@ admin/123456
数据默认保存在`/data/certd`目录下,可以手动备份
### 4.3 自动备份
> 建议配置一条 [数据库备份流水线](../../use/backup/),自动备份

View File

@@ -9,5 +9,5 @@
}
},
"npmClient": "pnpm",
"version": "1.35.0"
"version": "1.35.5"
}

View File

@@ -20,7 +20,7 @@
"afterpublishOnly": "npm run copylogs && time /t >build.trigger && git add ./build.trigger && git commit -m \"build: trigger build image\" && TIMEOUT /T 10 && git push",
"transform-sql": "cd ./packages/ui/certd-server/db/ && node --experimental-json-modules transform.js",
"commitAll": "git add . && git commit -m \"build: publish\" && git push && npm run commitPro",
"commitPro": "cd ./packages/core/ && git add . && git commit -m \"build: publish\" && git push",
"commitPro": "cd ./packages/pro/ && git add . && git commit -m \"build: publish\" && git push",
"copylogs": "copyfiles \"CHANGELOG.md\" ./docs/guide/changelogs/",
"prepublishOnly1": "npm run check && lerna run build ",
"prepublishOnly2": "npm run check && npm run before-build && lerna run build ",

View File

@@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.35.5](https://github.com/publishlab/node-acme-client/compare/v1.35.4...v1.35.5) (2025-06-20)
**Note:** Version bump only for package @certd/acme-client
## [1.35.4](https://github.com/publishlab/node-acme-client/compare/v1.35.3...v1.35.4) (2025-06-13)
**Note:** Version bump only for package @certd/acme-client
## [1.35.3](https://github.com/publishlab/node-acme-client/compare/v1.35.2...v1.35.3) (2025-06-12)
**Note:** Version bump only for package @certd/acme-client
## [1.35.2](https://github.com/publishlab/node-acme-client/compare/v1.35.1...v1.35.2) (2025-06-09)
**Note:** Version bump only for package @certd/acme-client
## [1.35.1](https://github.com/publishlab/node-acme-client/compare/v1.35.0...v1.35.1) (2025-06-07)
### Performance Improvements
* 证书申请支持letencrypt profile选项 ([2eb0e54](https://github.com/publishlab/node-acme-client/commit/2eb0e54909d8ad36708e07c12fd598998159bc43))
# [1.35.0](https://github.com/publishlab/node-acme-client/compare/v1.34.11...v1.35.0) (2025-06-05)
**Note:** Version bump only for package @certd/acme-client

View File

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

View File

@@ -75,6 +75,9 @@ export default async (client, userOpts) => {
log("[auto] Placing new certificate order with ACME provider");
const orderPayload = { identifiers: uniqueDomains.map((d) => ({ type: "dns", value: d })) };
if (opts.profile && client.sslProvider === 'letsencrypt' ){
orderPayload.profile = opts.profile;
}
const order = await client.createOrder(orderPayload);
const authorizations = await client.getAuthorizations(order);
@@ -213,12 +216,16 @@ export default async (client, userOpts) => {
return promise;
}
async function runPromisePa(tasks, waitTime = 5000) {
async function runPromisePa(tasks, waitTime = 8000) {
const results = [];
let j = 0
// eslint-disable-next-line no-await-in-loop,no-restricted-syntax
for (const task of tasks) {
j++
log(`开始第${j}个任务`);
results.push(task());
// eslint-disable-next-line no-await-in-loop
log(`wait ${waitTime}s`)
await wait(waitTime);
}
return Promise.all(results);
@@ -242,6 +249,7 @@ export default async (client, userOpts) => {
log(`跳过本地验证skipChallengeVerification=true等待 60s`);
await wait(60 * 1000);
} else {
log("开始本地校验")
await runPromisePa(localVerifyTasks, 1000);
log(`本地校验完成,等待${waitDnsDiffuseTime}s`)
await wait(waitDnsDiffuseTime * 1000)

View File

@@ -90,10 +90,12 @@ const defaultOpts = {
*/
class AcmeClient {
sslProvider
constructor(opts) {
if (!Buffer.isBuffer(opts.accountKey)) {
opts.accountKey = Buffer.from(opts.accountKey);
}
this.sslProvider = opts.sslProvider;
this.opts = { ...defaultOpts, ...opts };
this.backoffOpts = {

View File

@@ -66,6 +66,7 @@ export interface ClientAutoOptions {
challengePriority?: string[];
preferredChain?: string;
signal?: AbortSignal;
profile?:string;
}
export class Client {

View File

@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.35.5](https://github.com/certd/certd/compare/v1.35.4...v1.35.5) (2025-06-20)
**Note:** Version bump only for package @certd/basic
## [1.35.4](https://github.com/certd/certd/compare/v1.35.3...v1.35.4) (2025-06-13)
**Note:** Version bump only for package @certd/basic
## [1.35.3](https://github.com/certd/certd/compare/v1.35.2...v1.35.3) (2025-06-12)
**Note:** Version bump only for package @certd/basic
## [1.35.2](https://github.com/certd/certd/compare/v1.35.1...v1.35.2) (2025-06-09)
**Note:** Version bump only for package @certd/basic
## [1.35.1](https://github.com/certd/certd/compare/v1.35.0...v1.35.1) (2025-06-07)
**Note:** Version bump only for package @certd/basic
# [1.35.0](https://github.com/certd/certd/compare/v1.34.11...v1.35.0) (2025-06-05)
**Note:** Version bump only for package @certd/basic

View File

@@ -1 +1 @@
00:09
17:10

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/basic",
"private": false,
"version": "1.35.0",
"version": "1.35.5",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -45,5 +45,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "ab3a3156f24d7fc70f8a907c5f6fc754413a89d6"
"gitHead": "93017c044d4533ce40a2aab525f10b82761d09d0"
}

View File

@@ -3,6 +3,32 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.35.5](https://github.com/certd/certd/compare/v1.35.4...v1.35.5) (2025-06-20)
### Performance Improvements
* 支持批量修改通知和定时 ([e11b3be](https://github.com/certd/certd/commit/e11b3becfd4abe6547e84d09adc38ebd6e1c4b87))
## [1.35.4](https://github.com/certd/certd/compare/v1.35.3...v1.35.4) (2025-06-13)
### Performance Improvements
* 支持s3 access做测试 ([f00aeac](https://github.com/certd/certd/commit/f00aeacb8b5c81f0bafa4c1b76723dec2b6b7784))
## [1.35.3](https://github.com/certd/certd/compare/v1.35.2...v1.35.3) (2025-06-12)
### Bug Fixes
* 修复消息内容存在()<>等括号情况下无法发送tg通知的bug ([c937583](https://github.com/certd/certd/commit/c937583a50d8513d76adead3648f83eee2fcc6f9))
## [1.35.2](https://github.com/certd/certd/compare/v1.35.1...v1.35.2) (2025-06-09)
**Note:** Version bump only for package @certd/pipeline
## [1.35.1](https://github.com/certd/certd/compare/v1.35.0...v1.35.1) (2025-06-07)
**Note:** Version bump only for package @certd/pipeline
# [1.35.0](https://github.com/certd/certd/compare/v1.34.11...v1.35.0) (2025-06-05)
**Note:** Version bump only for package @certd/pipeline

View File

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

View File

@@ -37,6 +37,7 @@ export type AccessContext = {
http: HttpClient;
logger: ILogger;
utils: typeof utils;
accessService: IAccessService;
};
export abstract class BaseAccess implements IAccess {

View File

@@ -1,5 +1,5 @@
// src/decorator/memoryCache.decorator.ts
import { AccessContext, AccessDefine, AccessInputDefine } from "./api.js";
import { AccessContext, AccessDefine, AccessInputDefine, IAccessService } from "./api.js";
import { Decorator } from "../decorator/index.js";
import * as _ from "lodash-es";
import { accessRegistry } from "./registry.js";
@@ -41,7 +41,7 @@ export function AccessInput(input?: AccessInputDefine): PropertyDecorator {
};
}
export async function newAccess(type: string, input: any, ctx?: AccessContext) {
export async function newAccess(type: string, input: any, accessService: IAccessService, ctx?: AccessContext) {
const register = accessRegistry.get(type);
if (register == null) {
throw new Error(`access ${type} not found`);
@@ -58,6 +58,7 @@ export async function newAccess(type: string, input: any, ctx?: AccessContext) {
http,
logger,
utils,
accessService,
};
}
access.setCtx(ctx);

View File

@@ -452,12 +452,12 @@ export class Executor {
continue;
}
if (notification.type === "email") {
if (notification.type === "email" && notification.options?.receivers) {
try {
await this.options.emailService?.send({
subject,
content,
receivers: notification.options.receivers,
receivers: notification.options?.receivers,
});
} catch (e) {
logger.error("send email error", e);

View File

@@ -52,7 +52,9 @@ export type Stage = Runnable & {
export type Trigger = {
id: string;
title: string;
cron: string;
props: {
cron: string;
};
type: string;
};
@@ -78,14 +80,13 @@ export type EmailOptions = {
receivers: string[];
};
export type NotificationWhen = "error" | "success" | "turnToSuccess" | "start";
export type NotificationType = "email" | "url";
export type NotificationType = "email" | "other";
export type Notification = {
type: NotificationType;
when: NotificationWhen[];
options: EmailOptions;
options?: EmailOptions;
notificationId: number;
title: string;
subType: string;
};
export type Pipeline = Runnable & {

View File

@@ -121,9 +121,13 @@ export abstract class BaseNotification implements INotification {
async onTestRequest() {
return await this.doSend({
userId: 0,
title: "【Certd】测试通知【*.foo.com】标题长度测试、测试、测试",
content: `测试通知,*.foo.com
title: "【标题】测试通知【*.foo.com】标题长度测试、测试、测试",
content: `测试通知
域名测试: *.foo.com
换行测试
(括号测试)
<尖括号测试>
[中括号测试]
`,
pipeline: {
id: 1,

View File

@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.35.5](https://github.com/certd/certd/compare/v1.35.4...v1.35.5) (2025-06-20)
**Note:** Version bump only for package @certd/lib-huawei
## [1.35.4](https://github.com/certd/certd/compare/v1.35.3...v1.35.4) (2025-06-13)
**Note:** Version bump only for package @certd/lib-huawei
## [1.35.3](https://github.com/certd/certd/compare/v1.35.2...v1.35.3) (2025-06-12)
**Note:** Version bump only for package @certd/lib-huawei
## [1.35.2](https://github.com/certd/certd/compare/v1.35.1...v1.35.2) (2025-06-09)
**Note:** Version bump only for package @certd/lib-huawei
## [1.35.1](https://github.com/certd/certd/compare/v1.35.0...v1.35.1) (2025-06-07)
**Note:** Version bump only for package @certd/lib-huawei
# [1.35.0](https://github.com/certd/certd/compare/v1.34.11...v1.35.0) (2025-06-05)
**Note:** Version bump only for package @certd/lib-huawei

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/lib-huawei",
"private": false,
"version": "1.35.0",
"version": "1.35.5",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
"types": "./dist/d/index.d.ts",
@@ -24,5 +24,5 @@
"prettier": "^2.8.8",
"tslib": "^2.8.1"
},
"gitHead": "ab3a3156f24d7fc70f8a907c5f6fc754413a89d6"
"gitHead": "93017c044d4533ce40a2aab525f10b82761d09d0"
}

View File

@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.35.5](https://github.com/certd/certd/compare/v1.35.4...v1.35.5) (2025-06-20)
**Note:** Version bump only for package @certd/lib-iframe
## [1.35.4](https://github.com/certd/certd/compare/v1.35.3...v1.35.4) (2025-06-13)
**Note:** Version bump only for package @certd/lib-iframe
## [1.35.3](https://github.com/certd/certd/compare/v1.35.2...v1.35.3) (2025-06-12)
**Note:** Version bump only for package @certd/lib-iframe
## [1.35.2](https://github.com/certd/certd/compare/v1.35.1...v1.35.2) (2025-06-09)
**Note:** Version bump only for package @certd/lib-iframe
## [1.35.1](https://github.com/certd/certd/compare/v1.35.0...v1.35.1) (2025-06-07)
**Note:** Version bump only for package @certd/lib-iframe
# [1.35.0](https://github.com/certd/certd/compare/v1.34.11...v1.35.0) (2025-06-05)
**Note:** Version bump only for package @certd/lib-iframe

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/lib-iframe",
"private": false,
"version": "1.35.0",
"version": "1.35.5",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -31,5 +31,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "ab3a3156f24d7fc70f8a907c5f6fc754413a89d6"
"gitHead": "93017c044d4533ce40a2aab525f10b82761d09d0"
}

View File

@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.35.5](https://github.com/certd/certd/compare/v1.35.4...v1.35.5) (2025-06-20)
**Note:** Version bump only for package @certd/jdcloud
## [1.35.4](https://github.com/certd/certd/compare/v1.35.3...v1.35.4) (2025-06-13)
**Note:** Version bump only for package @certd/jdcloud
## [1.35.3](https://github.com/certd/certd/compare/v1.35.2...v1.35.3) (2025-06-12)
**Note:** Version bump only for package @certd/jdcloud
## [1.35.2](https://github.com/certd/certd/compare/v1.35.1...v1.35.2) (2025-06-09)
**Note:** Version bump only for package @certd/jdcloud
## [1.35.1](https://github.com/certd/certd/compare/v1.35.0...v1.35.1) (2025-06-07)
**Note:** Version bump only for package @certd/jdcloud
# [1.35.0](https://github.com/certd/certd/compare/v1.34.11...v1.35.0) (2025-06-05)
**Note:** Version bump only for package @certd/jdcloud

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/jdcloud",
"version": "1.35.0",
"version": "1.35.5",
"description": "jdcloud openApi sdk",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
@@ -61,5 +61,5 @@
"fetch"
]
},
"gitHead": "ab3a3156f24d7fc70f8a907c5f6fc754413a89d6"
"gitHead": "93017c044d4533ce40a2aab525f10b82761d09d0"
}

View File

@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.35.5](https://github.com/certd/certd/compare/v1.35.4...v1.35.5) (2025-06-20)
**Note:** Version bump only for package @certd/lib-k8s
## [1.35.4](https://github.com/certd/certd/compare/v1.35.3...v1.35.4) (2025-06-13)
**Note:** Version bump only for package @certd/lib-k8s
## [1.35.3](https://github.com/certd/certd/compare/v1.35.2...v1.35.3) (2025-06-12)
**Note:** Version bump only for package @certd/lib-k8s
## [1.35.2](https://github.com/certd/certd/compare/v1.35.1...v1.35.2) (2025-06-09)
**Note:** Version bump only for package @certd/lib-k8s
## [1.35.1](https://github.com/certd/certd/compare/v1.35.0...v1.35.1) (2025-06-07)
**Note:** Version bump only for package @certd/lib-k8s
# [1.35.0](https://github.com/certd/certd/compare/v1.34.11...v1.35.0) (2025-06-05)
**Note:** Version bump only for package @certd/lib-k8s

View File

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

View File

@@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.35.5](https://github.com/certd/certd/compare/v1.35.4...v1.35.5) (2025-06-20)
**Note:** Version bump only for package @certd/lib-server
## [1.35.4](https://github.com/certd/certd/compare/v1.35.3...v1.35.4) (2025-06-13)
### Performance Improvements
* 支持s3 access做测试 ([f00aeac](https://github.com/certd/certd/commit/f00aeacb8b5c81f0bafa4c1b76723dec2b6b7784))
## [1.35.3](https://github.com/certd/certd/compare/v1.35.2...v1.35.3) (2025-06-12)
**Note:** Version bump only for package @certd/lib-server
## [1.35.2](https://github.com/certd/certd/compare/v1.35.1...v1.35.2) (2025-06-09)
**Note:** Version bump only for package @certd/lib-server
## [1.35.1](https://github.com/certd/certd/compare/v1.35.0...v1.35.1) (2025-06-07)
**Note:** Version bump only for package @certd/lib-server
# [1.35.0](https://github.com/certd/certd/compare/v1.34.11...v1.35.0) (2025-06-05)
**Note:** Version bump only for package @certd/lib-server

View File

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

View File

@@ -1,16 +1,16 @@
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { BaseService, PageReq, PermissionException, ValidateException } from '../../../index.js';
import { AccessEntity } from '../entity/access.js';
import { AccessDefine, accessRegistry, newAccess } from '@certd/pipeline';
import { EncryptService } from './encrypt-service.js';
import {Inject, Provide, Scope, ScopeEnum} from '@midwayjs/core';
import {InjectEntityModel} from '@midwayjs/typeorm';
import {Repository} from 'typeorm';
import {AccessGetter, BaseService, PageReq, PermissionException, ValidateException} from '../../../index.js';
import {AccessEntity} from '../entity/access.js';
import {AccessDefine, accessRegistry, newAccess} from '@certd/pipeline';
import {EncryptService} from './encrypt-service.js';
/**
* 授权
*/
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
@Scope(ScopeEnum.Request, {allowDowngrade: true})
export class AccessService extends BaseService<AccessEntity> {
@InjectEntityModel(AccessEntity)
repository: Repository<AccessEntity>;
@@ -95,6 +95,7 @@ export class AccessService extends BaseService<AccessEntity> {
param.encryptSetting = JSON.stringify(encryptSetting);
param.setting = JSON.stringify(json);
}
/**
* 修改
* @param param 数据
@@ -140,7 +141,8 @@ export class AccessService extends BaseService<AccessEntity> {
id: entity.id,
...setting,
};
return await newAccess(entity.type, input);
const accessGetter = new AccessGetter(userId, this.getById.bind(this));
return await newAccess(entity.type, input,accessGetter);
}
async getById(id: any, userId: number): Promise<any> {

View File

@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.35.5](https://github.com/certd/certd/compare/v1.35.4...v1.35.5) (2025-06-20)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.35.4](https://github.com/certd/certd/compare/v1.35.3...v1.35.4) (2025-06-13)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.35.3](https://github.com/certd/certd/compare/v1.35.2...v1.35.3) (2025-06-12)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.35.2](https://github.com/certd/certd/compare/v1.35.1...v1.35.2) (2025-06-09)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.35.1](https://github.com/certd/certd/compare/v1.35.0...v1.35.1) (2025-06-07)
**Note:** Version bump only for package @certd/midway-flyway-js
# [1.35.0](https://github.com/certd/certd/compare/v1.34.11...v1.35.0) (2025-06-05)
**Note:** Version bump only for package @certd/midway-flyway-js

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/midway-flyway-js",
"version": "1.35.0",
"version": "1.35.5",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -46,5 +46,5 @@
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "ab3a3156f24d7fc70f8a907c5f6fc754413a89d6"
"gitHead": "93017c044d4533ce40a2aab525f10b82761d09d0"
}

View File

@@ -3,6 +3,32 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.35.5](https://github.com/certd/certd/compare/v1.35.4...v1.35.5) (2025-06-20)
**Note:** Version bump only for package @certd/plugin-cert
## [1.35.4](https://github.com/certd/certd/compare/v1.35.3...v1.35.4) (2025-06-13)
**Note:** Version bump only for package @certd/plugin-cert
## [1.35.3](https://github.com/certd/certd/compare/v1.35.2...v1.35.3) (2025-06-12)
**Note:** Version bump only for package @certd/plugin-cert
## [1.35.2](https://github.com/certd/certd/compare/v1.35.1...v1.35.2) (2025-06-09)
**Note:** Version bump only for package @certd/plugin-cert
## [1.35.1](https://github.com/certd/certd/compare/v1.35.0...v1.35.1) (2025-06-07)
### Bug Fixes
* 某些证书提供商的证书确实commonName导致无法转换证书的问题 ([ac87bc5](https://github.com/certd/certd/commit/ac87bc57e957ea4679707bfd38d6840e26319bed))
### Performance Improvements
* 证书申请支持letencrypt profile选项 ([2eb0e54](https://github.com/certd/certd/commit/2eb0e54909d8ad36708e07c12fd598998159bc43))
# [1.35.0](https://github.com/certd/certd/compare/v1.34.11...v1.35.0) (2025-06-05)
### Features

View File

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

View File

@@ -327,8 +327,9 @@ export class AcmeService {
csrInfo: any;
isTest?: boolean;
privateKeyType?: string;
profile?: string;
}): Promise<CertInfo> {
const { email, isTest, csrInfo, dnsProvider, domainsVerifyPlan } = options;
const { email, isTest, csrInfo, dnsProvider, domainsVerifyPlan, profile } = options;
const client: acme.Client = await this.getAcmeClient(email, isTest);
let domains = options.domains;
@@ -400,6 +401,7 @@ export class AcmeService {
return await this.challengeRemoveFn(authz, challenge, keyAuthorization, recordReq, recordRes, dnsProvider, httpUploader);
},
signal: this.options.signal,
profile,
});
const crtString = crt.toString();

View File

@@ -89,7 +89,10 @@ export class CertReader {
getAllDomains() {
const { detail } = this.getCrtDetail();
const domains = [detail.domains.commonName];
const domains = [];
if (detail.domains?.commonName) {
domains.push(detail.domains.commonName);
}
domains.push(...detail.domains.altNames);
//去重
return uniq(domains);
@@ -102,12 +105,23 @@ export class CertReader {
static getMainDomain(crt: string) {
const { detail } = CertReader.readCertDetail(crt);
return detail.domains.commonName;
return CertReader.getMainDomainFromDetail(detail);
}
getMainDomain() {
const { detail } = this.getCrtDetail();
return detail.domains.commonName;
return CertReader.getMainDomainFromDetail(detail);
}
static getMainDomainFromDetail(detail: CertificateInfo) {
let domain = detail?.domains?.commonName;
if (domain == null) {
domain = detail?.domains?.altNames?.[0];
}
if (domain == null) {
domain = "unknown";
}
return domain;
}
saveToFile(type: "crt" | "key" | "pfx" | "der" | "oc" | "one" | "ic" | "jks", filepath?: string) {
@@ -179,8 +193,7 @@ export class CertReader {
}
buildCertFileName(suffix: string, applyTime: any, prefix = "cert") {
const detail = this.getCrtDetail();
let domain = detail.detail.domains.commonName;
let domain = this.getMainDomain();
domain = domain.replaceAll(".", "_").replaceAll("*", "_");
const timeStr = dayjs(applyTime).format("YYYYMMDDHHmmss");
return `${prefix}_${domain}_${timeStr}.${suffix}`;
@@ -188,7 +201,7 @@ export class CertReader {
buildCertName() {
let domain = this.getMainDomain();
domain = domain.replaceAll("*", "_").replaceAll("*", "_");
domain = domain.replaceAll(".", "_").replaceAll("*", "_");
return `${domain}_${dayjs().format("YYYYMMDDHHmmssSSS")}`;
}
}

View File

@@ -248,6 +248,30 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
})
privateKeyType!: PrivateKeyType;
@TaskInput({
title: "证书配置",
value: "classic",
component: {
name: "a-select",
vModel: "value",
options: [
{ value: "classic", label: "经典classic" },
{ value: "tlsserver", label: "TLS服务器tlsserver" },
{ value: "shortlived", label: "短暂的shortlived" },
],
},
helper: "如无特殊需求,默认即可",
required: false,
mergeScript: `
return {
show: ctx.compute(({form})=>{
return form.sslProvider === 'letsencrypt'
})
}
`,
})
certProfile!: string;
@TaskInput({
title: "使用代理",
value: false,
@@ -395,6 +419,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
csrInfo,
isTest: false,
privateKeyType: this.privateKeyType,
profile: this.certProfile,
});
const certInfo = this.formatCerts(cert);

View File

@@ -3,6 +3,38 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.35.5](https://github.com/certd/certd/compare/v1.35.4...v1.35.5) (2025-06-20)
### Bug Fixes
* 腾讯云授权支持设置是否国际站部署到EO插件支持国际站 ([5cd3968](https://github.com/certd/certd/commit/5cd3968929acef333cf30d3b20cf21cea6c82c5f))
## [1.35.4](https://github.com/certd/certd/compare/v1.35.3...v1.35.4) (2025-06-13)
### Performance Improvements
* 支持s3 access做测试 ([f00aeac](https://github.com/certd/certd/commit/f00aeacb8b5c81f0bafa4c1b76723dec2b6b7784))
## [1.35.3](https://github.com/certd/certd/compare/v1.35.2...v1.35.3) (2025-06-12)
**Note:** Version bump only for package @certd/plugin-lib
## [1.35.2](https://github.com/certd/certd/compare/v1.35.1...v1.35.2) (2025-06-09)
### Bug Fixes
* 修复阿里云新加坡clb无法部署证书的bug ([3e84e11](https://github.com/certd/certd/commit/3e84e116e863b54c6b4d7db160af372dacc5857f))
### Performance Improvements
* 优化阿里云nlb支持部署扩展证书 ([9cbdfda](https://github.com/certd/certd/commit/9cbdfda829b231733d54c66c5024d46e6fc11af3))
## [1.35.1](https://github.com/certd/certd/compare/v1.35.0...v1.35.1) (2025-06-07)
### Performance Improvements
* aliyun alb支持部署扩展证书 ([2a19b61](https://github.com/certd/certd/commit/2a19b61b7a78620c06396c2cc37cc77d738b6d12))
# [1.35.0](https://github.com/certd/certd/compare/v1.34.11...v1.35.0) (2025-06-05)
**Note:** Version bump only for package @certd/plugin-lib

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-lib",
"private": false,
"version": "1.35.0",
"version": "1.35.5",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -17,11 +17,12 @@
},
"dependencies": {
"@alicloud/openapi-client": "^0.4.14",
"@alicloud/openapi-util": "^0.3.2",
"@alicloud/pop-core": "^1.7.10",
"@alicloud/tea-util": "^1.4.10",
"@aws-sdk/client-s3": "^3.787.0",
"@certd/basic": "^1.35.0",
"@certd/pipeline": "^1.35.0",
"@certd/basic": "^1.35.5",
"@certd/pipeline": "^1.35.5",
"@kubernetes/client-node": "0.21.0",
"ali-oss": "^6.22.0",
"basic-ftp": "^5.0.5",
@@ -52,5 +53,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "ab3a3156f24d7fc70f8a907c5f6fc754413a89d6"
"gitHead": "93017c044d4533ce40a2aab525f10b82761d09d0"
}

View File

@@ -54,7 +54,7 @@ export class AliyunClientV2 {
const $OpenApi = await import("@alicloud/openapi-client");
const $Util = await import("@alicloud/tea-util");
const OpenApiUtil = await import("@alicloud/openapi-util");
const params = new $OpenApi.Params({
// 接口名称
action: req.action,
@@ -74,6 +74,10 @@ export class AliyunClientV2 {
bodyType: "json",
});
if (req.data?.query) {
//@ts-ignore
req.data.query = OpenApiUtil.default.default.query(req.data.query);
}
const runtime = new $Util.RuntimeOptions({});
const request = new $OpenApi.OpenApiRequest(req.data);
// 复制代码运行请自行打印 API 的返回值

View File

@@ -29,7 +29,7 @@ export type AliyunSslUploadCertReq = {
cert: AliyunCertInfo;
};
export type CasCertInfo = { certId: number; certName: string; certIdentifier: string };
export type CasCertInfo = { certId: number; certName: string; certIdentifier: string; notAfter: number; casRegion: string };
export class AliyunSslClient {
opts: AliyunSslClientOpts;
@@ -68,6 +68,8 @@ export class AliyunSslClient {
certId: certId,
certName: res.Name,
certIdentifier: res.CertIdentifier,
notAfter: res.NotAfter,
casRegion: this.getCasRegionFromEndpoint(this.opts.endpoint),
};
}
@@ -148,4 +150,24 @@ export class AliyunSslClient {
this.checkRet(res);
return res;
}
async deleteCert(certId: any) {
await this.doRequest("DeleteUserCertificate", { CertId: certId }, { method: "POST" });
}
getCasRegionFromEndpoint(endpoint: string) {
if (!endpoint) {
return "cn-hangzhou";
}
/**
* {value: 'cas.aliyuncs.com', label: '中国大陆'},
* {value: 'cas.ap-southeast-1.aliyuncs.com', label: '新加坡'},
* {value: 'cas.eu-central-1.aliyuncs.com', label: '德国(法兰克福)'},
*/
const region = endpoint.replace(".aliyuncs.com", "").replace("cas.", "");
if (region === "cas") {
return "cn-hangzhou";
}
return region;
}
}

View File

@@ -54,6 +54,9 @@ export default class S3OssClientImpl extends BaseOssClient<S3Access> {
Prefix: dirKey, // The name of the object. For example, 'sample_upload.txt'.
};
const res = await this.client.send(new ListObjectsCommand({ ...params }));
if (!res.Contents) {
return [];
}
return res.Contents.map(item => {
return {
path: item.Key,

View File

@@ -1,4 +1,6 @@
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
import { ossClientFactory } from "../oss/index.js";
import S3OssClientImpl from "../oss/impls/s3.js";
/**
* 这个注解将注册一个授权配置
@@ -82,6 +84,32 @@ export class S3Access extends BaseAccess {
required: true,
})
bucket!: string;
@AccessInput({
title: "测试",
component: {
name: "api-test",
action: "TestRequest",
},
helper: "点击测试接口是否正常",
})
testRequest = true;
async onTestRequest() {
const client: S3OssClientImpl = await ossClientFactory.createOssClientByType("s3", {
access: this,
rootDir: "",
ctx: {
accessService: this.ctx.accessService,
logger: this.ctx.logger,
utils: this.ctx.utils,
},
});
await client.listDir("/");
return "ok";
}
}
new S3Access();

View File

@@ -8,8 +8,7 @@ import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline";
export class TencentAccess extends BaseAccess {
@AccessInput({
title: "secretId",
helper:
"使用对应的插件需要有对应的权限,比如上传证书,需要证书管理权限;部署到clb需要clb相关权限\n前往[密钥管理](https://console.cloud.tencent.com/cam/capi)进行创建",
helper: "使用对应的插件需要有对应的权限,比如上传证书,需要证书管理权限;部署到clb需要clb相关权限\n前往[密钥管理](https://console.cloud.tencent.com/cam/capi)进行创建",
component: {
placeholder: "secretId",
},
@@ -25,4 +24,29 @@ export class TencentAccess extends BaseAccess {
rules: [{ required: true, message: "该项必填" }],
})
secretKey = "";
@AccessInput({
title: "站点类型",
value: "cn",
component: {
name: "a-select",
options: [
{
label: "国内站",
value: "cn",
},
{
label: "国际站",
value: "intl",
},
],
},
encrypt: true,
rules: [{ required: true, message: "该项必填" }],
})
accountType: string;
isIntl() {
return this.accountType === "intl";
}
}

View File

@@ -3,6 +3,46 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.35.5](https://github.com/certd/certd/compare/v1.35.4...v1.35.5) (2025-06-20)
### Bug Fixes
* 修复邮箱包含.号校验失败的bug ([65dcae7](https://github.com/certd/certd/commit/65dcae79f8faa7a6cb425e10a0fdb6758b0719f3))
### Performance Improvements
* 首次打开任务日志查看页面,自动滚动到底部 ([43fee42](https://github.com/certd/certd/commit/43fee42198e8697185b427b1fa3eb79409603393))
* 支持批量修改通知和定时 ([e11b3be](https://github.com/certd/certd/commit/e11b3becfd4abe6547e84d09adc38ebd6e1c4b87))
## [1.35.4](https://github.com/certd/certd/compare/v1.35.3...v1.35.4) (2025-06-13)
**Note:** Version bump only for package @certd/ui-client
## [1.35.3](https://github.com/certd/certd/compare/v1.35.2...v1.35.3) (2025-06-12)
### Bug Fixes
* 修复重试次数设置无效的bug ([e2099ac](https://github.com/certd/certd/commit/e2099ac9ca344bc70bfa4219002e9138708973ae))
### Performance Improvements
* 授权列表类型颜色优化 ([1e86338](https://github.com/certd/certd/commit/1e863382d3d1a8cc95a1abf51e75bf6eaea3244f))
* 支持雨云dns解析以及雨云证书更新 ([43c7a19](https://github.com/certd/certd/commit/43c7a1984926f5d4647760cc134bb0aede3a7b7a))
## [1.35.2](https://github.com/certd/certd/compare/v1.35.1...v1.35.2) (2025-06-09)
### Performance Improvements
* 子域名托管帮助链接优化为打开新窗口 ([7c0cdd1](https://github.com/certd/certd/commit/7c0cdd169e2f943e703e433677f2f437d4aa02ee))
* history增加触发类型显示 ([7f6070c](https://github.com/certd/certd/commit/7f6070c960ed7bf02add5ab36436de6573f2f1fa))
## [1.35.1](https://github.com/certd/certd/compare/v1.35.0...v1.35.1) (2025-06-07)
### Performance Improvements
* 优化流水线页面,增加下次执行时间、查看证书显示 ([c820315](https://github.com/certd/certd/commit/c8203154094fae3d17198747f49f5f41ddf29a4e))
* 站点证书监控支持定时设置,重试次数设置 ([d3c2f8e](https://github.com/certd/certd/commit/d3c2f8eb436e670772d14a54acd6b541c5aa3978))
# [1.35.0](https://github.com/certd/certd/compare/v1.34.11...v1.35.0) (2025-06-05)
**Note:** Version bump only for package @certd/ui-client

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/ui-client",
"version": "1.35.0",
"version": "1.35.5",
"private": true,
"scripts": {
"dev": "vite --open",
@@ -30,10 +30,10 @@
"@aws-sdk/client-s3": "^3.535.0",
"@aws-sdk/s3-request-presigner": "^3.535.0",
"@ctrl/tinycolor": "^4.1.0",
"@fast-crud/fast-crud": "^1.25.8",
"@fast-crud/fast-extends": "^1.25.8",
"@fast-crud/ui-antdv4": "^1.25.8",
"@fast-crud/ui-interface": "^1.25.8",
"@fast-crud/fast-crud": "^1.25.13",
"@fast-crud/fast-extends": "^1.25.13",
"@fast-crud/ui-antdv4": "^1.25.13",
"@fast-crud/ui-interface": "^1.25.13",
"@iconify/tailwind": "^1.2.0",
"@iconify/vue": "^4.1.1",
"@manypkg/get-packages": "^2.2.2",
@@ -102,8 +102,8 @@
"zod-defaults": "^0.1.3"
},
"devDependencies": {
"@certd/lib-iframe": "^1.35.0",
"@certd/pipeline": "^1.35.0",
"@certd/lib-iframe": "^1.35.5",
"@certd/pipeline": "^1.35.5",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12",

View File

@@ -1,17 +1,7 @@
<template>
<div class="cron-editor">
<div class="flex-o">
<cron-light
:disabled="disabled"
:readonly="readonly"
:period="period"
class="flex-o cron-ant"
locale="zh-CN"
format="quartz"
:model-value="modelValue"
@update:model-value="onUpdate"
@error="onError"
/>
<cron-light :disabled="disabled" :readonly="readonly" :period="period" class="flex-o cron-ant" locale="zh-CN" format="quartz" :model-value="modelValue" @update:model-value="onUpdate" @error="onError" />
</div>
<div class="mt-5 flex">
<a-input :disabled="true" :readonly="readonly" :value="modelValue" @change="onChange"></a-input>
@@ -26,13 +16,15 @@
import parser from "cron-parser";
import { computed, ref } from "vue";
import dayjs from "dayjs";
import { getCronNextTimes } from "/@/components/cron-editor/utils";
defineOptions({
name: "CronEditor"
name: "CronEditor",
});
const props = defineProps<{
modelValue?: string;
disabled?: boolean;
readonly?: boolean;
allowEveryMin?: boolean;
}>();
const period = ref<string>("");
@@ -58,9 +50,12 @@ const onUpdate = (value: string) => {
if (arr[0] === "*") {
arr[0] = "0";
}
if (arr[1] === "*") {
arr[1] = "0";
if (!props.allowEveryMin) {
if (arr[1] === "*") {
arr[1] = "0";
}
}
value = arr.join(" ");
emit("update:modelValue", value);
@@ -90,10 +85,10 @@ const nextTime = computed(() => {
if (props.modelValue == null) {
return "请先设置正确的cron表达式";
}
try {
const interval = parser.parseExpression(props.modelValue);
const next = interval.next().getTime();
return dayjs(next).format("YYYY-MM-DD HH:mm:ss");
const nextTimes = getCronNextTimes(props.modelValue, 2);
return nextTimes.join("");
} catch (e) {
console.log(e);
return "请先设置正确的cron表达式";

View File

@@ -0,0 +1,15 @@
import parser from "cron-parser";
import dayjs from "dayjs";
export function getCronNextTimes(cron: string, count: number = 1) {
if (cron == null) {
return [];
}
const nextTimes = [];
const interval = parser.parseExpression(cron);
for (let i = 0; i < count; i++) {
const next = interval.next().getTime();
nextTimes.push(dayjs(next).format("YYYY-MM-DD HH:mm:ss"));
}
return nextTimes;
}

View File

@@ -76,7 +76,7 @@ export default {
.text-editable {
flex: 1;
line-height: 34px;
overflow: hidden;
span.fs-iconify {
display: inline-flex;
justify-content: center;

View File

@@ -54,7 +54,9 @@ onMounted(async () => {
async function addItem() {
const email = newEmail.value;
//验证邮箱格式
if (!/^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/.test(newEmail.value)) {
const regExp =
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+\.)+[a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{2,}))$/;
if (!regExp.test(email)) {
notification.error({
message: "请填写正确的邮箱地址",
});

View File

@@ -334,6 +334,22 @@ function install(app: App, options: any = {}) {
return columnProps;
},
});
registerMergeColumnPlugin({
name: "reset-values-format-colors",
order: 10,
handle: (columnProps: ColumnCompositionProps) => {
// 你可以在此处做你自己的处理
// 比如你可以定义一个readonly的公共属性处理该字段只读不能编辑
if (columnProps.column?.component?.name === "fs-values-format") {
// 合并column配置
if (!columnProps.column.component.autoColors) {
columnProps.column.component.autoColors = ["green", "cyan", "blue", "purple", "geekblue"];
}
}
return columnProps;
},
});
}
export default {

View File

@@ -49,38 +49,38 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
pageRequest,
addRequest,
editRequest,
delRequest
delRequest,
},
toolbar: {
show: false
show: false,
},
search: {
show: false
show: false,
},
form: {
wrapper: {
width: "1050px"
}
width: "1050px",
},
},
rowHandle: {
width: 200
width: 200,
},
table: {
scroll: {
x: 800
x: 800,
},
rowSelection: {
type: "radio",
selectedRowKeys: selectedRowKey,
onChange: onSelectChange
onChange: onSelectChange,
},
customRow: (record: any) => {
return {
onClick: () => {
onSelectChange([record.id]);
} // 点击行
}, // 点击行
};
}
},
},
columns: {
id: {
@@ -88,25 +88,25 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
key: "id",
type: "number",
column: {
width: 50
width: 50,
},
form: {
show: false
}
show: false,
},
},
name: {
title: "名称",
search: {
show: true
show: true,
},
type: ["text"],
form: {
rules: [{ required: true, message: "请填写名称" }],
helper: "随便填,当多个相同类型的授权时,便于区分"
helper: "随便填,当多个相同类型的授权时,便于区分",
},
column: {
width: 200
}
width: 200,
},
},
from: {
title: "级别",
@@ -114,29 +114,29 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
dict: dict({
data: [
{ label: "系统", value: "sys" },
{ label: "用户", value: "user" }
]
{ label: "用户", value: "user" },
],
}),
search: {
show: false
show: false,
},
form: {
show: false
show: false,
},
column: {
width: 100,
align: "center",
component: {
color: "auto"
color: "auto",
},
order: 10
order: 10,
},
valueBuilder: ({ row, key, value }) => {
row[key] = row.userId > 0 ? "user" : "sys";
}
},
},
...commonColumnsDefine
}
}
...commonColumnsDefine,
},
},
};
}

View File

@@ -2,9 +2,10 @@ import * as api from "./api";
import { useI18n } from "vue-i18n";
import { computed, Ref, ref } from "vue";
import { useRouter } from "vue-router";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, EditReq, UserPageQuery, UserPageRes, utils } from "@fast-crud/fast-crud";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes, utils } from "@fast-crud/fast-crud";
import { useUserStore } from "/@/store/user";
import { useSettingStore } from "/@/store/settings";
import { statusUtil } from "/@/views/certd/pipeline/pipeline/utils/util.status";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter();
@@ -143,6 +144,50 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
},
},
triggerType: {
title: "触发类型",
type: "dict-select",
search: {
show: true,
},
dict: dict({
data: [
{ value: "user", label: "手动执行" },
{ value: "timer", label: "定时执行" },
],
}),
form: {
show: false,
value: "custom",
},
column: {
sorter: true,
width: 90,
align: "center",
show: true,
component: {
color: "auto",
},
},
},
status: {
title: "状态",
type: "dict-select",
search: {
show: true,
},
dict: dict({
data: statusUtil.getOptions(),
}),
form: {
show: false,
},
column: {
sorter: true,
width: 120,
align: "center",
},
},
createTime: {
title: "创建时间",
type: "datetime",

View File

@@ -130,7 +130,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
await api.DoCheck(row.id);
await crudExpose.doRefresh();
notification.success({
message: "检查完成",
message: "检查任务已提交,请稍后刷新查看结果",
});
},
},

View File

@@ -137,7 +137,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
await api.DoCheck(row.id);
await crudExpose.doRefresh();
notification.success({
message: "检查任务已提交",
message: "检查任务已提交,请稍后刷新查看结果",
});
},
},

View File

@@ -3,6 +3,8 @@ import { request } from "/src/api/service";
const apiPrefix = "/monitor/site/setting";
export type UserSiteMonitorSetting = {
notificationId?: number;
retryTimes?: number;
cron?: string;
};
export async function SiteMonitorSettingsGet() {

View File

@@ -11,6 +11,19 @@
</div>
<div class="helper">设置通知渠道</div>
</a-form-item>
<a-form-item label="重试次数" :name="['retryTimes']">
<div class="flex">
<a-input-number v-model:value="formState.retryTimes" />
</div>
<div class="helper">监控请求重试次数</div>
</a-form-item>
<a-form-item label="监控定时设置" :name="['cron']">
<div class="flex flex-baseline">
<cron-editor v-model="formState.cron" :disabled="!settingsStore.isPlus" :allow-every-min="userStore.isAdmin" />
<vip-button class="ml-5" mode="button"></vip-button>
</div>
<div class="helper">定时触发监控</div>
</a-form-item>
<a-form-item label=" " :colon="false" :wrapper-col="{ span: 16 }">
<loading-button type="primary" html-type="button" :click="doSave">保存</loading-button>
</a-form-item>
@@ -27,8 +40,10 @@ import { notification } from "ant-design-vue";
import { merge } from "lodash-es";
import { useSettingStore } from "/src/store/settings";
import NotificationSelector from "/@/views/certd/notification/notification-selector/index.vue";
import { useUserStore } from "/@/store/user";
const settingsStore = useSettingStore();
const userStore = useUserStore();
defineOptions({
name: "UserSecurity",
});
@@ -56,7 +71,7 @@ const doSave = async (form: any) => {
<style lang="less">
.page-user-settings {
.user-settings-form {
width: 600px;
width: 700px;
margin: 20px;
}
}

View File

@@ -84,6 +84,23 @@ export async function BatchUpdateGroup(pipelineIds: number[], groupId: number):
});
}
export async function BatchUpdateTrigger(pipelineIds: number[], trigger: any): Promise<void> {
return await request({
url: apiPrefix + "/batchUpdateTrigger",
method: "post",
data: { ids: pipelineIds, trigger },
});
}
export async function BatchUpdateNotificaiton(pipelineIds: number[], notification: any): Promise<void> {
return await request({
url: apiPrefix + "/batchUpdateNotification",
method: "post",
data: { ids: pipelineIds, notification },
});
}
export async function BatchDelete(pipelineIds: number[]): Promise<void> {
return await request({
url: apiPrefix + "/batchDelete",
@@ -99,6 +116,8 @@ export async function BatchRerun(pipelineIds: number[]): Promise<void> {
});
}
export async function GetFiles(pipelineId: number) {
return await request({
url: historyApiPrefix + "/files",

View File

@@ -0,0 +1,96 @@
<template>
<fs-button icon="mdi:format-list-group" type="link" text="修改通知" @click="openFormDialog"></fs-button>
</template>
<script setup lang="ts">
import * as api from "../api";
import { useFormWrapper } from "@fast-crud/fast-crud";
import NotificationSelector from "/@/views/certd/notification/notification-selector/index.vue";
import { ref } from "vue";
const props = defineProps<{
selectedRowKeys: any[];
}>();
const emit = defineEmits<{
change: any;
}>();
async function batchUpdateRequest(form: any) {
/**
* type: NotificationType;
* when: NotificationWhen[];
* options?: EmailOptions;
* notificationId: number;
* title: string;
*/
await api.BatchUpdateNotificaiton(props.selectedRowKeys, {
type: "other",
title: form.title || "通知",
when: form.when,
notificationId: form.notificationId,
});
emit("change");
}
const { openCrudFormDialog } = useFormWrapper();
async function openFormDialog() {
const crudOptions: any = {
columns: {
when: {
title: "触发时机",
form: {
value: ["error", "turnToSuccess"],
component: {
name: "a-select",
vModel: "value",
mode: "multiple",
options: [
{ value: "start", label: "开始时" },
{ value: "success", label: "成功时" },
{ value: "turnToSuccess", label: "失败转成功时" },
{ value: "error", label: "失败时" },
],
},
helper: `建议仅选择'失败时'和'失败转成功'两种即可`,
rules: [{ required: true, message: "此项必填" }],
},
},
notificationId: {
title: "通知配置",
form: {
component: {
name: NotificationSelector,
on: {
selectedChange({ form, $event }: any) {
form.title = $event?.name || "通知";
},
},
},
helper: "请选择通知方式",
rules: [{ required: true, message: "此项必填" }],
},
},
},
form: {
mode: "edit",
//@ts-ignore
async doSubmit({ form }) {
await batchUpdateRequest(form);
},
col: {
span: 22,
},
labelCol: {
style: {
width: "100px",
},
},
wrapper: {
title: "批量修改通知",
width: 600,
},
},
} as any;
await openCrudFormDialog({ crudOptions });
}
</script>

View File

@@ -0,0 +1,62 @@
<template>
<fs-button icon="mdi:format-list-group" type="link" text="修改定时" @click="openFormDialog"></fs-button>
</template>
<script setup lang="ts">
import * as api from "../api";
import { useFormWrapper } from "@fast-crud/fast-crud";
const props = defineProps<{
selectedRowKeys: any[];
}>();
const emit = defineEmits<{
change: any;
}>();
async function batchUpdateRequest(form: any) {
await api.BatchUpdateTrigger(props.selectedRowKeys, {
title: "定时触发",
type: "timer",
props: form.props,
});
emit("change");
}
const { openCrudFormDialog } = useFormWrapper();
async function openFormDialog() {
const crudOptions: any = {
columns: {
"props.cron": {
title: "定时",
form: {
component: {
name: "cron-editor",
vModel: "modelValue",
},
rules: [{ required: true, message: "请选择定时Cron" }],
},
},
},
form: {
mode: "edit",
//@ts-ignore
async doSubmit({ form }) {
await batchUpdateRequest(form);
},
col: {
span: 22,
},
labelCol: {
style: {
width: "100px",
},
},
wrapper: {
title: "批量修改定时",
width: 600,
},
},
} as any;
await openCrudFormDialog({ crudOptions });
}
</script>

View File

@@ -5,17 +5,15 @@ import { useRouter } from "vue-router";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes, useUi } from "@fast-crud/fast-crud";
import { statusUtil } from "/@/views/certd/pipeline/pipeline/utils/util.status";
import { Modal, notification } from "ant-design-vue";
import { env } from "/@/utils/util.env";
import { useUserStore } from "/@/store/user";
import dayjs from "dayjs";
import { useSettingStore } from "/@/store/settings";
import { cloneDeep } from "lodash-es";
import { useModal } from "/@/use/use-modal";
import CertView from "./cert-view.vue";
import { eachStages } from "./utils";
import { setRunnableIds, useCertPipelineCreator } from "/@/views/certd/pipeline/certd-form/use";
import { useCertUpload } from "/@/views/certd/pipeline/cert-upload/use";
import GroupSelector from "/@/views/certd/pipeline/group/group-selector.vue";
import { useCertViewer } from "/@/views/certd/pipeline/use";
export default function ({ crudExpose, context: { groupDictRef, selectedRowKeys } }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter();
@@ -61,59 +59,7 @@ export default function ({ crudExpose, context: { groupDictRef, selectedRowKeys
return res;
};
const model = useModal();
const viewCert = async (row: any) => {
const cert = await api.GetCert(row.id);
if (!cert) {
notification.error({ message: "请先运行一次流水线" });
return;
}
model.success({
title: "查看证书",
maskClosable: true,
okText: "关闭",
width: 800,
content: () => {
return <CertView cert={cert}></CertView>;
},
});
};
const downloadCert = async (row: any) => {
const files = await api.GetFiles(row.id);
model.success({
title: "点击链接下载",
maskClosable: true,
okText: "关闭",
content: () => {
const children = [];
for (const file of files) {
const downloadUrl = `${env.API}/pi/history/download?pipelineId=${row.id}&fileId=${file.id}`;
children.push(
<div>
<div class={"flex-o m-5"}>
<fs-icon icon={"ant-design:cloud-download-outlined"} class={"mr-5 fs-16"}></fs-icon>
<a href={downloadUrl} target={"_blank"}>
{file.filename}
</a>
</div>
</div>
);
}
if (children.length === 0) {
return <div></div>;
}
return (
<div class={"mt-3"}>
<div> {children}</div>
</div>
);
},
});
};
const { viewCert, downloadCert } = useCertViewer();
const userStore = useUserStore();
const settingStore = useSettingStore();
@@ -208,6 +154,10 @@ export default function ({ crudExpose, context: { groupDictRef, selectedRowKeys
},
table: {
scroll: { x: 1500 },
remove: {
confirmTitle: "确定要删除吗?",
confirmMessage: "将删除该流水线相关的所有数据,包括执行历史、证书文件、证书仓库记录等",
},
},
tabs: {
name: "groupId",
@@ -281,7 +231,7 @@ export default function ({ crudExpose, context: { groupDictRef, selectedRowKeys
type: "link",
icon: "ph:certificate",
async click({ row }) {
await viewCert(row);
await viewCert(row.id);
},
},
download: {
@@ -291,7 +241,7 @@ export default function ({ crudExpose, context: { groupDictRef, selectedRowKeys
tooltip: { title: "下载证书" },
icon: "ant-design:download-outlined",
async click({ row }) {
await downloadCert(row);
await downloadCert(row.id);
},
},
remove: {

View File

@@ -34,6 +34,8 @@ const pipelineOptions: PipelineOptions = {
stages: [],
triggers: [],
...JSON.parse(detail.pipeline.content || "{}"),
type: detail.pipeline.type,
from: detail.pipeline.from,
},
} as PipelineDetail;
},

View File

@@ -7,9 +7,11 @@
<div v-if="selectedRowKeys.length > 0" class="batch-actions">
<div class="batch-actions-inner">
<span> 已选择 {{ selectedRowKeys.length }} </span>
<fs-button icon="ion:trash-outline" class="color-green" type="link" text="批量删除" @click="batchDelete"></fs-button>
<change-group class="color-green" :selected-row-keys="selectedRowKeys" @change="groupChanged"></change-group>
<fs-button icon="ion:trash-outline" class="color-red" type="link" text="批量删除" @click="batchDelete"></fs-button>
<fs-button icon="icon-park-outline:replay-music" class="need-plus" type="link" text="强制重新运行" @click="batchRerun"></fs-button>
<change-group class="color-green" :selected-row-keys="selectedRowKeys" @change="batchFinished"></change-group>
<change-notification class="color-green" :selected-row-keys="selectedRowKeys" @change="batchFinished"></change-notification>
<change-trigger class="color-green" :selected-row-keys="selectedRowKeys" @change="batchFinished"></change-trigger>
</div>
</div>
<template #actionbar-right> </template>
@@ -26,8 +28,10 @@ import { dict, useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import PiCertdForm from "./certd-form/index.vue";
import ChangeGroup from "./components/change-group.vue";
import ChangeTrigger from "./components/change-trigger.vue";
import { Modal, notification } from "ant-design-vue";
import * as api from "./api";
import ChangeNotification from "/@/views/certd/pipeline/components/change-notification.vue";
defineOptions({
name: "PipelineManager",
@@ -55,7 +59,7 @@ onActivated(async () => {
await crudExpose.doRefresh();
});
function groupChanged() {
function batchFinished() {
crudExpose.doRefresh();
selectedRowKeys.value = [];
}

View File

@@ -24,17 +24,17 @@
disabled: !editMode,
options: [
{ value: 'email', label: '邮件' },
{ value: 'other', label: '其他通知方式' }
]
{ value: 'other', label: '其他通知方式' },
],
},
rules: [{ required: true, message: '此项必填' }]
rules: [{ required: true, message: '此项必填' }],
}"
/>
<fs-form-item
v-model="currentNotification.when"
:item="{
title: '触发时机',
key: 'type',
key: 'when',
value: ['error'],
component: {
name: 'a-select',
@@ -45,11 +45,11 @@
{ value: 'start', label: '开始时' },
{ value: 'success', label: '成功时' },
{ value: 'turnToSuccess', label: '失败转成功时' },
{ value: 'error', label: '失败时' }
]
{ value: 'error', label: '失败时' },
],
},
helper: `建议仅选择'失败时'和'失败转成功'两种即可`,
rules: [{ required: true, message: '此项必填' }]
rules: [{ required: true, message: '此项必填' }],
}"
/>
<pi-notification-form-email v-if="currentNotification.type === 'email'" ref="optionsRef" v-model:options="currentNotification.options"></pi-notification-form-email>
@@ -59,14 +59,14 @@
v-model="currentNotification.notificationId"
:item="{
title: '通知配置',
key: 'type',
key: 'notificationId',
component: {
disabled: !editMode,
name: NotificationSelector,
onSelectedChange
onSelectedChange,
},
helper: '请选择通知方式',
rules: [{ required: true, message: '此项必填' }]
rules: [{ required: true, message: '此项必填' }],
}"
/>
</a-form>
@@ -96,8 +96,8 @@ export default {
props: {
editMode: {
type: Boolean,
default: true
}
default: true,
},
},
emits: ["update"],
setup(props: any, context: any) {
@@ -118,23 +118,23 @@ export default {
{
type: "string",
required: true,
message: "请选择类型"
}
message: "请选择类型",
},
],
when: [
{
type: "string",
required: true,
message: "请选择通知时机"
}
message: "请选择通知时机",
},
],
notificationId: [
{
type: "number",
required: true,
message: "请选择通知配置"
}
]
message: "请选择通知配置",
},
],
});
const notificationDrawerShow = () => {
@@ -195,7 +195,7 @@ export default {
async onOk() {
callback.value("delete");
notificationDrawerClose();
}
},
});
};
@@ -222,21 +222,21 @@ export default {
notificationDelete,
rules,
blankFn,
optionsRef
optionsRef,
};
}
return {
...useNotificationForm(),
labelCol: { span: 6 },
wrapperCol: { span: 16 }
wrapperCol: { span: 16 },
};
},
computed: {
NotificationSelector() {
return NotificationSelector;
}
}
},
},
};
</script>

View File

@@ -119,6 +119,31 @@ export default {
logs: [],
});
}
async function scrollBottom(node: any, force = false) {
let el = document.querySelector(`.pi-task-view-logs.id-${node.node.id}`);
if (!el) {
return;
}
//判断当前是否在底部
let isBottom = true;
if (el) {
isBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 5;
}
if (force) {
isBottom = true;
}
await nextTick();
el = document.querySelector(`.pi-task-view-logs.id-${node.node.id}`);
//如果在底部则滚动到底部
if (isBottom && el) {
el?.scrollTo({
top: el.scrollHeight,
behavior: "smooth",
});
}
}
for (let node of nodes) {
if (currentHistory?.value?.logs != null) {
node.logs = computed(() => {
@@ -146,30 +171,12 @@ export default {
return node.logs.value.length;
},
async () => {
let el = document.querySelector(`.pi-task-view-logs.id-${node.node.id}`);
if (!el) {
return;
}
//判断当前是否在底部
let isBottom = true;
if (el) {
isBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 5;
console.log("isBottom", isBottom, el.scrollHeight, el.scrollTop, el.clientHeight);
}
await nextTick();
el = document.querySelector(`.pi-task-view-logs.id-${node.node.id}`);
//如果在底部则滚动到底部
if (isBottom && el) {
el?.scrollTo({
top: el.scrollHeight,
behavior: "smooth",
});
}
},
{
immediate: true,
await scrollBottom(node);
}
);
nextTick(() => {
scrollBottom(node, true);
});
}
}
@@ -178,8 +185,6 @@ export default {
}
detail.value = { nodes };
console.log("nodes", nodes);
};
const taskViewClose = () => {

View File

@@ -1,12 +1,5 @@
<template>
<a-drawer
v-model:open="triggerDrawerVisible"
placement="right"
:closable="true"
width="650px"
class="pi-trigger-form"
@after-open-change="triggerDrawerOnAfterVisibleChange"
>
<a-drawer v-model:open="triggerDrawerVisible" placement="right" :closable="true" width="650px" class="pi-trigger-form" @after-open-change="triggerDrawerOnAfterVisibleChange">
<template #title>
<div>
编辑触发器
@@ -26,9 +19,9 @@
component: {
name: 'a-input',
vModel: 'value',
disabled: !editMode
disabled: !editMode,
},
rules: [{ required: true, message: '此项必填' }]
rules: [{ required: true, message: '此项必填' }],
}"
/>
@@ -42,9 +35,9 @@
name: 'a-select',
vModel: 'value',
disabled: !editMode,
options: [{ value: 'timer', label: '定时' }]
options: [{ value: 'timer', label: '定时' }],
},
rules: [{ required: true, message: '此项必填' }]
rules: [{ required: true, message: '此项必填' }],
}"
/>
@@ -56,10 +49,10 @@
component: {
disabled: !editMode,
name: 'cron-editor',
vModel: 'modelValue'
vModel: 'modelValue',
},
helper: '点击上面的按钮,选择每天几点定时执行。\n建议设置为每天触发一次证书未到期之前任务会跳过不会重复执行',
rules: [{ required: true, message: '此项必填' }]
rules: [{ required: true, message: '此项必填' }],
}"
/>
</a-form>
@@ -84,8 +77,8 @@ export default {
props: {
editMode: {
type: Boolean,
default: true
}
default: true,
},
},
emits: ["update"],
setup(props, context) {
@@ -105,9 +98,9 @@ export default {
{
type: "string",
required: true,
message: "请输入名称"
}
]
message: "请输入名称",
},
],
});
const triggerDrawerShow = () => {
@@ -117,7 +110,7 @@ export default {
triggerDrawerVisible.value = false;
};
const triggerDrawerOnAfterVisibleChange = (val) => {
const triggerDrawerOnAfterVisibleChange = val => {
console.log("triggerDrawerOnAfterVisibleChange", val);
};
@@ -128,7 +121,7 @@ export default {
triggerDrawerShow();
};
const triggerAdd = (emit) => {
const triggerAdd = emit => {
mode.value = "add";
const trigger = { id: nanoid(), title: "定时触发", type: "timer", props: {} };
triggerOpen(trigger, emit);
@@ -144,7 +137,7 @@ export default {
triggerOpen(trigger, emit);
};
const triggerSave = async (e) => {
const triggerSave = async e => {
console.log("currentTriggerSave", currentTrigger.value);
try {
await triggerFormRef.value.validate();
@@ -164,7 +157,7 @@ export default {
async onOk() {
callback.value("delete");
triggerDrawerClose();
}
},
});
};
@@ -185,16 +178,16 @@ export default {
triggerSave,
triggerDelete,
rules,
blankFn
blankFn,
};
}
return {
...useTriggerForm(),
labelCol: { span: 6 },
wrapperCol: { span: 16 }
wrapperCol: { span: 16 },
};
}
},
};
</script>

View File

@@ -1,18 +1,43 @@
<template>
<fs-page v-if="pipeline" class="page-pipeline-edit">
<template #header>
<div class="title">
<div class="title flex-1">
<fs-button class="back" icon="ion:chevron-back-outline" @click="goBack"></fs-button>
<text-editable v-model="pipeline.title" :hover-show="false" :disabled="!editMode"></text-editable>
</div>
<div class="more">
<template v-if="editMode">
<a-button type="primary" :loading="saveLoading" @click="save">保存</a-button>
<a-button class="ml-5" @click="cancel">取消</a-button>
</template>
<template v-else>
<a-button type="primary" @click="edit">编辑</a-button>
</template>
<div class="more flex items-center flex-1 justify-end">
<div v-if="isCert" class="flex items-center hidden md:block">
<a-tag class="mr-5 pointer" color="green" type="primary" text="查看证书" @click="viewCert(pipeline.id)">
<span class="flex"><fs-icon icon="ant-design:eye-outlined"></fs-icon> 查看证书</span>
</a-tag>
<a-tag class="mr-5 pointer" color="green" type="primary" text="下载证书" @click="downloadCert(pipeline.id)">
<span class="flex"> <fs-icon icon="ant-design:download-outlined"></fs-icon> 下载证书 </span>
</a-tag>
</div>
<div class="flex items-center hidden md:block">
<a-tag v-if="nextTriggerTimes" color="blue">
<span class="flex">
<fs-icon icon="ion:time-outline"></fs-icon>
下次执行时间{{ nextTriggerTimes }}
</span>
</a-tag>
<a-tag v-else-if="nextTriggerTimes === false" color="red">
<span class="flex">
<fs-icon icon="ion:caret-forward-circle-outline"></fs-icon>
未设置触发源不会自动执行
</span>
</a-tag>
</div>
<div class="basis-40 flex justify-end mr-10">
<template v-if="editMode">
<fs-button type="primary" :loading="saveLoading" @click="save">保存</fs-button>
<fs-button class="ml-5" @click="cancel">取消</fs-button>
</template>
<template v-else>
<fs-button icon="ant-design:edit-outlined" type="primary" @click="edit">编辑</fs-button>
</template>
</div>
</div>
</template>
@@ -241,7 +266,7 @@
@cancel="historyCancel()"
></pi-history-timeline-item>
</template>
<a-empty v-if="histories.length === 0"> </a-empty>
<a-empty v-if="histories.length === 0"></a-empty>
</a-timeline>
</a-page-header>
</div>
@@ -255,7 +280,7 @@
</template>
<script lang="ts">
import { defineComponent, onMounted, onUnmounted, provide, Ref, ref, watch } from "vue";
import { defineComponent, onMounted, onUnmounted, provide, Ref, ref, watch, computed } from "vue";
import { useRouter } from "vue-router";
import PiTaskForm from "./component/task-form/index.vue";
import PiTriggerForm from "./component/trigger-form/index.vue";
@@ -275,11 +300,23 @@ import { useUserStore } from "/@/store/user";
import TaskShortcuts from "./component/shortcut/task-shortcuts.vue";
import { eachSteps, findStep } from "../utils";
import { PluginGroups } from "/@/store/plugin";
import { getCronNextTimes } from "/@/components/cron-editor/utils";
import { useCertViewer } from "/@/views/certd/pipeline/use";
export default defineComponent({
name: "PipelineEdit",
// eslint-disable-next-line vue/no-unused-components
components: { FsIcon, PiHistoryTimelineItem, PiTaskForm, PiTriggerForm, PiTaskView, PiStatusShow, PiNotificationForm, VDraggable, TaskShortcuts },
components: {
FsIcon,
PiHistoryTimelineItem,
PiTaskForm,
PiTriggerForm,
PiTaskView,
PiStatusShow,
PiNotificationForm,
VDraggable,
TaskShortcuts,
},
props: {
pipelineId: {
type: [Number, String],
@@ -309,7 +346,24 @@ export default defineComponent({
const currentHistory: Ref<any> = ref({});
const nextTriggerTimes = computed(() => {
const triggers = pipeline.value.triggers;
if (!triggers || triggers.length === 0) {
return false;
}
let nextTimes: any = [];
for (const item of triggers) {
if (!item.props?.cron) {
continue;
}
const ret = getCronNextTimes(item.props?.cron, 1);
nextTimes.push(...ret);
}
return nextTimes.join("");
});
const router = useRouter();
function goBack() {
router.back();
}
@@ -365,8 +419,10 @@ export default defineComponent({
}
return true;
}
const intervalLoadHistoryRef = ref();
const isLoadingHistory = ref(false);
function watchNewHistoryList() {
intervalLoadHistoryRef.value = setInterval(async () => {
if (isLoadingHistory.value) {
@@ -390,6 +446,7 @@ export default defineComponent({
}
}, 3000);
}
onMounted(() => {
watchNewHistoryList();
});
@@ -420,7 +477,15 @@ export default defineComponent({
return;
}
const detail: PipelineDetail = await props.options.getPipelineDetail({ pipelineId: value });
currentPipeline.value = _.merge({ title: "新管道流程", stages: [], triggers: [], notifications: [] }, detail.pipeline);
currentPipeline.value = _.merge(
{
title: "新管道流程",
stages: [],
triggers: [],
notifications: [],
},
detail.pipeline
);
pipeline.value = currentPipeline.value;
await loadHistoryList(true);
},
@@ -535,6 +600,7 @@ export default defineComponent({
function isLastStage(index: number) {
return false;
}
return {
stageAdd,
isLastStage,
@@ -654,6 +720,7 @@ export default defineComponent({
}
const validateErrors: Ref = ref({});
function addValidateError(taskId: string, error: any) {
const errors = validateErrors.value[taskId] || [];
validateErrors.value[taskId] = errors;
@@ -708,6 +775,7 @@ export default defineComponent({
function hasValidateError(taskId: string) {
return validateErrors.value[taskId] != null;
}
const save = async (offEdit = true) => {
doValidate();
@@ -771,9 +839,11 @@ export default defineComponent({
};
const logsCollapse = ref(false);
function toggleLogsCollapse() {
logsCollapse.value = !logsCollapse.value;
}
return {
historyView,
historyCancel,
@@ -838,7 +908,12 @@ export default defineComponent({
};
});
const { viewCert, downloadCert } = useCertViewer();
const isCert = computed(() => {
return currentPipeline.value?.type?.startsWith("cert");
});
return {
isCert,
pipeline,
currentHistory,
histories,
@@ -852,6 +927,9 @@ export default defineComponent({
...useHistory(),
...useNotification(),
...useScroll(),
nextTriggerTimes,
viewCert,
downloadCert,
};
},
});
@@ -864,9 +942,11 @@ export default defineComponent({
text-overflow: ellipsis;
text-wrap: nowrap;
display: flex;
.back {
margin-right: 10px;
}
.text-editable {
width: 300px;
}
@@ -876,47 +956,57 @@ export default defineComponent({
.pi-status-show {
display: inline-flex;
}
.fs-page-content {
overflow-x: auto;
}
.layout {
width: 100%;
height: 100%;
position: relative;
display: flex;
overflow-x: hidden;
.layout-left {
flex: 1;
height: 100%;
}
.layout-right {
width: 350px;
height: 100%;
}
}
.pipeline-container {
width: 100%;
height: 100%;
position: relative;
overflow: auto;
}
.pipeline {
position: absolute;
left: 0;
top: 0;
height: 100%;
.stages {
display: flex;
overflow: auto;
min-width: 100%;
height: 100%;
.stage {
width: 300px;
border-right: 1px solid #c7c7c7;
.is-add {
visibility: hidden;
color: gray;
}
&:hover .is-add {
visibility: visible;
}
@@ -925,11 +1015,13 @@ export default defineComponent({
padding: 20px;
color: gray;
display: flex;
.stage-move-handle {
cursor: move;
margin-left: 4px;
}
}
//.sortable-ghost {
// .line {
// visibility: hidden;
@@ -943,6 +1035,7 @@ export default defineComponent({
&.line-left {
left: 25px;
.flow-line {
border-right: 0;
}
@@ -950,6 +1043,7 @@ export default defineComponent({
&.line-right {
right: 25px;
.flow-line {
border-left: 0;
}
@@ -960,6 +1054,7 @@ export default defineComponent({
border: 1px solid #c7c7c7;
border-top: 0;
}
.add-stage-btn {
display: inline-flex;
visibility: hidden;
@@ -969,6 +1064,7 @@ export default defineComponent({
bottom: -12px;
left: -12px;
z-index: 100;
&:hover {
color: #1890ff;
}
@@ -981,6 +1077,7 @@ export default defineComponent({
&.line-left {
left: 0;
.flow-line {
border-right: 0;
border-left: 0;
@@ -989,6 +1086,7 @@ export default defineComponent({
&.line-right {
right: 0;
.flow-line {
border-left: 0;
border-right: 0;
@@ -1008,13 +1106,16 @@ export default defineComponent({
}
}
}
&.last-stage {
.line {
width: 50% !important;
right: auto;
.flow-line {
border-right: 0;
}
.add-stage-btn {
visibility: hidden;
}
@@ -1038,6 +1139,7 @@ export default defineComponent({
}
}
}
.task {
display: flex;
flex-direction: column;
@@ -1050,6 +1152,7 @@ export default defineComponent({
&.in-edit {
margin-right: 28px;
}
&.disabled {
}
}
@@ -1061,12 +1164,15 @@ export default defineComponent({
//font-size: 18px;
cursor: pointer;
z-index: 10;
&:hover {
color: #1890ff;
}
&.copy {
right: 30px;
}
&.drag {
right: 10px;
cursor: move;
@@ -1078,6 +1184,7 @@ export default defineComponent({
}
position: relative;
.shortcut {
position: absolute;
bottom: -10px;
@@ -1092,6 +1199,7 @@ export default defineComponent({
.layout-right {
position: relative;
&.collapsed {
margin-right: -350px;
}

View File

@@ -79,7 +79,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
return (
<div>
<a href={"https://help.aliyun.com/zh/dns/subdomain-management"}></a>
<a href={"https://help.aliyun.com/zh/dns/subdomain-management"} target={"_blank"}>
</a>
</div>
);
},

View File

@@ -0,0 +1,65 @@
import * as api from "/@/views/certd/pipeline/api";
import { notification } from "ant-design-vue";
import CertView from "/@/views/certd/pipeline/cert-view.vue";
import { env } from "/@/utils/util.env";
import { useModal } from "/@/use/use-modal";
export function useCertViewer() {
const model = useModal();
const viewCert = async (id: number) => {
const cert = await api.GetCert(id);
if (!cert) {
notification.error({ message: "请先运行一次流水线" });
return;
}
model.success({
title: "查看证书",
maskClosable: true,
okText: "关闭",
width: 800,
content: () => {
return <CertView cert={cert}></CertView>;
},
});
};
const downloadCert = async (id: any) => {
const files = await api.GetFiles(id);
model.success({
title: "点击链接下载",
maskClosable: true,
okText: "关闭",
content: () => {
const children = [];
for (const file of files) {
const downloadUrl = `${env.API}/pi/history/download?pipelineId=${id}&fileId=${file.id}`;
children.push(
<div>
<div class={"flex-o m-5"}>
<fs-icon icon={"ant-design:cloud-download-outlined"} class={"mr-5 fs-16"}></fs-icon>
<a href={downloadUrl} target={"_blank"}>
{file.filename}
</a>
</div>
</div>
);
}
if (children.length === 0) {
return <div></div>;
}
return (
<div class={"mt-3"}>
<div> {children}</div>
</div>
);
},
});
};
return {
viewCert,
downloadCert,
};
}

View File

@@ -26,4 +26,4 @@ typeorm:
account:
server:
baseUrl: 'http://127.0.0.1:1017/subject'
baseUrl: 'http://localhost:1017/subject'

View File

@@ -3,6 +3,62 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.35.5](https://github.com/certd/certd/compare/v1.35.4...v1.35.5) (2025-06-20)
### Bug Fixes
* 腾讯云授权支持设置是否国际站部署到EO插件支持国际站 ([5cd3968](https://github.com/certd/certd/commit/5cd3968929acef333cf30d3b20cf21cea6c82c5f))
### Performance Improvements
* 支持批量修改通知和定时 ([e11b3be](https://github.com/certd/certd/commit/e11b3becfd4abe6547e84d09adc38ebd6e1c4b87))
## [1.35.4](https://github.com/certd/certd/compare/v1.35.3...v1.35.4) (2025-06-13)
### Performance Improvements
* 支持s3 access做测试 ([f00aeac](https://github.com/certd/certd/commit/f00aeacb8b5c81f0bafa4c1b76723dec2b6b7784))
## [1.35.3](https://github.com/certd/certd/compare/v1.35.2...v1.35.3) (2025-06-12)
### Bug Fixes
* 修复重试次数设置无效的bug ([e2099ac](https://github.com/certd/certd/commit/e2099ac9ca344bc70bfa4219002e9138708973ae))
### Performance Improvements
* 支持雨云dns解析 ([8354348](https://github.com/certd/certd/commit/83543487e7418683bd79cfe3b9e0d792bdb977f7))
* 支持雨云dns解析以及雨云证书更新 ([43c7a19](https://github.com/certd/certd/commit/43c7a1984926f5d4647760cc134bb0aede3a7b7a))
* github 版本检查支持执行脚本 ([bad3504](https://github.com/certd/certd/commit/bad3504d4a15e6989b967b66aa9da8c6981f25bf))
## [1.35.2](https://github.com/certd/certd/compare/v1.35.1...v1.35.2) (2025-06-09)
### Bug Fixes
* 修复阿里云新加坡clb无法部署证书的bug ([c1fbc8c](https://github.com/certd/certd/commit/c1fbc8cd68ae020ef342e4e92f4d9b4869ca1ead))
* 修复阿里云新加坡clb无法部署证书的bug ([3e84e11](https://github.com/certd/certd/commit/3e84e116e863b54c6b4d7db160af372dacc5857f))
* 修复检查github release 插件无法保存最后版本的bug ([a92107c](https://github.com/certd/certd/commit/a92107cc47133883b099d5228b06373e84c8bb50))
* 修复站点监控定时器多次添加的bug ([9361679](https://github.com/certd/certd/commit/936167972fe83e519bc01a0dd961d9c0635d24ab))
### Performance Improvements
* 阿里云dns操作增加重试机制 ([424fd96](https://github.com/certd/certd/commit/424fd96615c05e949af8c837c261c1400bdffba2))
* 优化阿里云nlb支持部署扩展证书 ([9cbdfda](https://github.com/certd/certd/commit/9cbdfda829b231733d54c66c5024d46e6fc11af3))
* history增加触发类型显示 ([7f6070c](https://github.com/certd/certd/commit/7f6070c960ed7bf02add5ab36436de6573f2f1fa))
## [1.35.1](https://github.com/certd/certd/compare/v1.35.0...v1.35.1) (2025-06-07)
### Bug Fixes
* 修复站点监控通知渠道设置无效的bug ([a00453c](https://github.com/certd/certd/commit/a00453c83a58114ce2873dd6e6aaf313f1ce0f87))
### Performance Improvements
* 修改 HTTPS 服务器监听地址 ([e1cf64a](https://github.com/certd/certd/commit/e1cf64ae16d4abfe4299ff16d5088c30cf3c6365))
* 优化流水线页面,增加下次执行时间、查看证书显示 ([c820315](https://github.com/certd/certd/commit/c8203154094fae3d17198747f49f5f41ddf29a4e))
* 站点证书监控支持定时设置,重试次数设置 ([d3c2f8e](https://github.com/certd/certd/commit/d3c2f8eb436e670772d14a54acd6b541c5aa3978))
* aliyun alb支持部署扩展证书 ([2a19b61](https://github.com/certd/certd/commit/2a19b61b7a78620c06396c2cc37cc77d738b6d12))
# [1.35.0](https://github.com/certd/certd/compare/v1.34.11...v1.35.0) (2025-06-05)
### Features

View File

@@ -0,0 +1,2 @@
ALTER TABLE pi_history ADD COLUMN `trigger_type` varchar(100);

View File

@@ -0,0 +1,2 @@
ALTER TABLE pi_history ADD COLUMN "trigger_type" varchar(100);

View File

@@ -0,0 +1,2 @@
ALTER TABLE pi_history ADD COLUMN "trigger_type" varchar(100);

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/ui-server",
"version": "1.35.0",
"version": "1.35.5",
"description": "fast-server base midway",
"private": true,
"type": "module",
@@ -42,20 +42,20 @@
"@aws-sdk/client-cloudfront": "^3.699.0",
"@aws-sdk/client-iam": "^3.699.0",
"@aws-sdk/client-s3": "^3.705.0",
"@certd/acme-client": "^1.35.0",
"@certd/basic": "^1.35.0",
"@certd/commercial-core": "^1.35.0",
"@certd/acme-client": "^1.35.5",
"@certd/basic": "^1.35.5",
"@certd/commercial-core": "^1.35.5",
"@certd/cv4pve-api-javascript": "^8.4.1",
"@certd/jdcloud": "^1.35.0",
"@certd/lib-huawei": "^1.35.0",
"@certd/lib-k8s": "^1.35.0",
"@certd/lib-server": "^1.35.0",
"@certd/midway-flyway-js": "^1.35.0",
"@certd/pipeline": "^1.35.0",
"@certd/plugin-cert": "^1.35.0",
"@certd/plugin-lib": "^1.35.0",
"@certd/plugin-plus": "^1.35.0",
"@certd/plus-core": "^1.35.0",
"@certd/jdcloud": "^1.35.5",
"@certd/lib-huawei": "^1.35.5",
"@certd/lib-k8s": "^1.35.5",
"@certd/lib-server": "^1.35.5",
"@certd/midway-flyway-js": "^1.35.5",
"@certd/pipeline": "^1.35.5",
"@certd/plugin-cert": "^1.35.5",
"@certd/plugin-lib": "^1.35.5",
"@certd/plugin-plus": "^1.35.5",
"@certd/plus-core": "^1.35.5",
"@huaweicloud/huaweicloud-sdk-cdn": "^3.1.120",
"@huaweicloud/huaweicloud-sdk-core": "^3.1.120",
"@koa/cors": "^5.0.0",

View File

@@ -154,4 +154,5 @@ export class SiteInfoController extends CrudController<SiteInfoService> {
await this.service.saveSetting(userId, setting);
return this.ok({});
}
}

View File

@@ -1,5 +1,5 @@
import {ALL, Body, Controller, Inject, Post, Provide} from '@midwayjs/core';
import {AccessService, BaseController, Constants} from '@certd/lib-server';
import {AccessGetter, AccessService, BaseController, Constants} from '@certd/lib-server';
import {
AccessRequestHandleReq,
IAccessService,
@@ -33,6 +33,7 @@ export class HandleController extends BaseController {
@Post('/access', { summary: Constants.per.authOnly })
async accessRequest(@Body(ALL) body: AccessRequestHandleReq) {
const userId = this.getUserId();
let inputAccess = body.input.access;
if (body.input.id > 0) {
const oldEntity = await this.accessService.info(body.input.id);
@@ -48,8 +49,8 @@ export class HandleController extends BaseController {
inputAccess = this.accessService.decryptAccessEntity(param);
}
}
const access = await newAccess(body.typeName, inputAccess);
const accessGetter = new AccessGetter(userId, this.accessService.getById.bind(this.accessService));
const access = await newAccess(body.typeName, inputAccess,accessGetter);
const res = await access.onRequest(body);

View File

@@ -123,6 +123,19 @@ export class PipelineController extends CrudController<PipelineService> {
return this.ok({});
}
@Post('/batchUpdateTrigger', { summary: Constants.per.authOnly })
async batchUpdateTrigger(@Body('ids') ids: number[], @Body('trigger') trigger: any) {
await this.service.batchUpdateTrigger(ids, trigger, this.getUserId());
return this.ok({});
}
@Post('/batchUpdateNotification', { summary: Constants.per.authOnly })
async batchUpdateNotification(@Body('ids') ids: number[], @Body('notification') notification: any) {
await this.service.batchUpdateNotifications(ids, notification, this.getUserId());
return this.ok({});
}
@Post('/batchRerun', { summary: Constants.per.authOnly })
async batchRerun(@Body('ids') ids: number[]) {
await this.service.batchRerun(ids, this.getUserId());

View File

@@ -1,9 +1,15 @@
import { Autoload, Config, Init, Inject, Scope, ScopeEnum } from '@midwayjs/core';
import { PipelineService } from '../pipeline/service/pipeline-service.js';
import { logger } from '@certd/basic';
import { SysSettingsService } from '@certd/lib-server';
import {SysSettingsService, SysSiteInfo} from '@certd/lib-server';
import { SiteInfoService } from '../monitor/index.js';
import { Cron } from '../cron/cron.js';
import {UserSettingsService} from "../mine/service/user-settings-service.js";
import {UserSiteMonitorSetting} from "../mine/service/models.js";
import {getPlusInfo} from "@certd/plus-core";
import dayjs from "dayjs";
import {NotificationService} from "../pipeline/service/notification-service.js";
import {UserService} from "../sys/authority/service/user-service.js";
@Autoload()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
@@ -22,6 +28,8 @@ export class AutoCRegisterCron {
@Inject()
sysSettingsService: SysSettingsService;
@Inject()
userSettingsService: UserSettingsService;
@Inject()
siteInfoService: SiteInfoService;
@@ -29,6 +37,13 @@ export class AutoCRegisterCron {
@Inject()
cron: Cron;
@Inject()
notificationService: NotificationService;
@Inject()
userService: UserService;
@Init()
async init() {
logger.info('加载定时trigger开始');
@@ -39,39 +54,87 @@ export class AutoCRegisterCron {
// console.log('meta', meta);
// const metas = listPropertyDataFromClass(CLASS_KEY, this.echoPlugin);
// console.log('metas', metas);
this.registerSiteMonitorCron();
await this.registerSiteMonitorCron();
await this.registerPlusExpireCheckCron();
}
registerSiteMonitorCron() {
const job = async () => {
logger.info('站点证书检查开始执行');
async registerSiteMonitorCron() {
//先注册公共job
await this.siteInfoService.registerSiteMonitorJob()
let offset = 0;
const limit = 50;
while (true) {
const res = await this.siteInfoService.page({
query: { disabled: false },
page: { offset, limit },
});
const { records } = res;
if (records.length === 0) {
break;
}
offset += records.length;
await this.siteInfoService.checkList(records);
//注册用户独立的检查时间
const monitorSettingList = await this.userSettingsService.list({
query:{
key: UserSiteMonitorSetting.__key__,
}
})
for (const item of monitorSettingList) {
const setting = item.setting ?? JSON.parse(item.setting)
if(!setting?.cron){
continue
}
await this.siteInfoService.registerSiteMonitorJob(item.userId)
}
logger.info('站点证书检查完成');
};
this.cron.register({
name: 'siteMonitor',
cron: '0 0 0 * * *',
job,
});
if (this.immediateTriggerSiteMonitor) {
job();
logger.info(`立即触发一次站点证书检查任务`)
await this.siteInfoService.triggerJobOnce()
}
}
registerPlusExpireCheckCron(){
// 添加plus即将到期检查任务
this.cron.register({
name: 'plus-expire-check',
cron: `0 10 9 * * *`, // 一天只能检查一次,否则会重复发送通知
job: async () => {
const plusInfo = getPlusInfo()
if (!plusInfo.originVipType || plusInfo.originVipType==="free" ) {
return
}
let label ="专业版"
if( plusInfo.originVipType === 'comm'){
label = "商业版"
}
const siteInfo = await this.sysSettingsService.getSetting<SysSiteInfo>(SysSiteInfo)
const appTitle = siteInfo.title || "certd"
const expiresDate = dayjs(plusInfo.expireTime).format("YYYY-MM-DD")
// plusInfo.expireTime= dayjs("2025-06-10").valueOf()
let expiresDays =Math.floor((plusInfo.expireTime - new Date().getTime())/ 1000 / 60 / 60 / 24)
let title = ""
let content =""
if(expiresDays === 20 ||expiresDays === 10 || expiresDays === 3 || expiresDays === 1 || expiresDays === 0){
title = `vip(${label})即将到期`
content = `您的${appTitle} vip (${label})剩余${expiresDays}天(${expiresDate})到期,请及时续期,以免影响业务`
}else if (expiresDays === -1 || expiresDays === -3 || expiresDays === -7) {
title = `vip(${label})已过期`
content = `您的${appTitle} vip (${label})已过期${Math.abs(expiresDays)}天(${expiresDate}),请尽快续期,以免影响业务`
}
if(title){
logger.warn(title)
logger.warn(content)
const url = await this.notificationService.getBindUrl("");
const adminUsers = await this.userService.getAdmins()
for (const adminUser of adminUsers) {
logger.info(`发送vip到期通知给管理员${adminUser.username}`)
await this.notificationService.send({
useDefault: true,
logger: logger,
body:{
title,
content,
errorMessage:title,
url
}
},adminUser.id)
}
}
}
})
}
}

View File

@@ -58,7 +58,7 @@ export class HttpsServer {
opts.app.callback()
);
this.server = httpServer;
const hostname = '0.0.0.0';
const hostname = '::';
// A function that runs in the context of the http server
// and reports what type of server listens on which port
function listeningReporter() {

View File

@@ -85,6 +85,8 @@ export class Cron {
}
this.logger.info(`[cron] register cron : [${req.name}] ,${req.cron}`);
this.remove(req.name)
const task = new CronTask(req, this.logger);
this.queue.push(task);
this.logger.info('当前定时任务数量:', this.getTaskSize());

View File

@@ -25,6 +25,8 @@ export class UserSiteMonitorSetting extends BaseSettings {
static __key__ = "user.site.monitor";
notificationId?:number= 0;
cron?:string = undefined;
retryTimes?:number = 3;
}
export class UserEmailSetting extends BaseSettings {

View File

@@ -1,22 +1,23 @@
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { BaseService, NeedSuiteException, NeedVIPException, SysSettingsService } from "@certd/lib-server";
import { InjectEntityModel } from "@midwayjs/typeorm";
import { Repository } from "typeorm";
import { SiteInfoEntity } from "../entity/site-info.js";
import { siteTester } from "./site-tester.js";
import {Inject, Provide, Scope, ScopeEnum} from "@midwayjs/core";
import {BaseService, NeedSuiteException, NeedVIPException, SysSettingsService} from "@certd/lib-server";
import {InjectEntityModel} from "@midwayjs/typeorm";
import {Repository} from "typeorm";
import {SiteInfoEntity} from "../entity/site-info.js";
import {siteTester} from "./site-tester.js";
import dayjs from "dayjs";
import { logger, utils } from "@certd/basic";
import { PeerCertificate } from "tls";
import { NotificationService } from "../../pipeline/service/notification-service.js";
import { isComm, isPlus } from "@certd/plus-core";
import { UserSuiteService } from "@certd/commercial-core";
import { UserSettingsService } from "../../mine/service/user-settings-service.js";
import { UserSiteMonitorSetting } from "../../mine/service/models.js";
import { SiteIpService } from "./site-ip-service.js";
import { SiteIpEntity } from "../entity/site-ip.js";
import {logger, utils} from "@certd/basic";
import {PeerCertificate} from "tls";
import {NotificationService} from "../../pipeline/service/notification-service.js";
import {isComm, isPlus} from "@certd/plus-core";
import {UserSuiteService} from "@certd/commercial-core";
import {UserSettingsService} from "../../mine/service/user-settings-service.js";
import {UserSiteMonitorSetting} from "../../mine/service/models.js";
import {SiteIpService} from "./site-ip-service.js";
import {SiteIpEntity} from "../entity/site-ip.js";
import {Cron} from "../../cron/cron.js";
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
@Scope(ScopeEnum.Request, {allowDowngrade: true})
export class SiteInfoService extends BaseService<SiteInfoEntity> {
@InjectEntityModel(SiteInfoEntity)
repository: Repository<SiteInfoEntity>;
@@ -36,6 +37,10 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
@Inject()
siteIpService: SiteIpService;
@Inject()
cron: Cron;
//@ts-ignore
getRepository() {
return this.repository;
@@ -70,7 +75,7 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
}
});
if (found) {
return { id: found.id };
return {id: found.id};
}
return await super.add(data);
@@ -89,7 +94,7 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
throw new Error("userId is required");
}
return await this.repository.count({
where: { userId }
where: {userId}
});
}
@@ -99,7 +104,7 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
* @param notify
* @param retryTimes
*/
async doCheck(site: SiteInfoEntity, notify = true, retryTimes = 3) {
async doCheck(site: SiteInfoEntity, notify = true, retryTimes = null) {
if (!site?.domain) {
throw new Error("站点域名不能为空");
}
@@ -141,13 +146,13 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
};
if (site.ipCheck) {
delete updateData.checkStatus
delete updateData.checkStatus
}
await this.update(updateData);
//检查ip
await this.checkAllIp(site);
await this.checkAllIp(site,retryTimes);
if (!notify) {
return;
@@ -176,7 +181,7 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
}
}
async checkAllIp(site: SiteInfoEntity) {
async checkAllIp(site: SiteInfoEntity,retryTimes = null) {
if (!site.ipCheck) {
return;
}
@@ -220,7 +225,7 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
logger.error("send notify error", e);
}
};
await this.siteIpService.checkAll(site, onFinished);
await this.siteIpService.checkAll(site, retryTimes,onFinished);
}
/**
@@ -229,7 +234,7 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
* @param notify
* @param retryTimes
*/
async check(id: number, notify = false, retryTimes = 3) {
async check(id: number, notify = false, retryTimes = null) {
const site = await this.info(id);
if (!site) {
throw new Error("站点不存在");
@@ -239,9 +244,11 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
async sendCheckErrorNotify(site: SiteInfoEntity, fromIpCheck = false) {
const url = await this.notificationService.getBindUrl("#/certd/monitor/site");
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(site.userId, UserSiteMonitorSetting)
// 发邮件
await this.notificationService.send(
{
id: setting?.notificationId,
useDefault: true,
logger: logger,
body: {
@@ -262,11 +269,13 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
const expires = site.certExpiresTime;
const validDays = dayjs(expires).diff(dayjs(), "day");
const url = await this.notificationService.getBindUrl("#/certd/monitor/site");
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(site.userId, UserSiteMonitorSetting)
const content = `站点名称: ${site.name} \n站点域名 ${site.domain} \n证书域名 ${site.certDomains} \n颁发机构 ${site.certProvider} \n过期时间 ${dayjs(site.certExpiresTime).format("YYYY-MM-DD")} \n`;
if (validDays >= 0 && validDays < tipDays) {
// 发通知
await this.notificationService.send(
{
id: setting?.notificationId,
useDefault: true,
logger: logger,
body: {
@@ -281,6 +290,7 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
//发过期通知
await this.notificationService.send(
{
id: setting?.notificationId,
useDefault: true,
logger: logger,
body: {
@@ -300,17 +310,35 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
throw new Error("userId is required");
}
const sites = await this.repository.find({
where: { userId }
where: {userId}
});
this.checkList(sites);
this.checkList(sites,false);
}
async checkList(sites: SiteInfoEntity[]) {
async checkList(sites: SiteInfoEntity[],isCommon: boolean) {
const cache = {}
const getFromCache = async (userId: number) =>{
if (cache[userId]) {
return cache[userId];
}
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(userId, UserSiteMonitorSetting)
cache[userId] = setting
return setting;
}
for (const site of sites) {
this.doCheck(site).catch(e => {
const setting = await getFromCache(site.userId)
if (isCommon) {
//公共的检查排除有设置cron的用户
if (setting?.cron) {
//设置了cron跳过公共检查
continue;
}
}
let retryTimes = setting?.retryTimes
this.doCheck(site,true,retryTimes).catch(e => {
logger.error(`检查站点证书失败,${site.domain}`, e.message);
});
await utils.sleep(200);
await utils.sleep(100);
}
}
@@ -320,6 +348,12 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
async saveSetting(userId: number, bean: UserSiteMonitorSetting) {
await this.userSettingsService.saveSetting(userId, bean);
if(bean.cron){
//注册job
await this.registerSiteMonitorJob(userId);
}else{
this.clearSiteMonitorJob(userId);
}
}
async ipCheckChange(req: { id: any; ipCheck: any }) {
@@ -396,4 +430,67 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
};
await batchAdd(list);
}
clearSiteMonitorJob(userId: number) {
this.cron.remove(`siteMonitor-${userId}`);
}
async registerSiteMonitorJob(userId?: number) {
if(!userId){
//注册公共job
logger.info(`注册站点证书检查定时任务`)
this.cron.register({
name: 'siteMonitor',
cron: '0 0 0 * * *',
job:async ()=>{
await this.triggerJobOnce()
},
});
logger.info(`注册站点证书检查定时任务完成`)
}else{
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(userId, UserSiteMonitorSetting);
if (!setting.cron) {
return;
}
//注册个人的
this.cron.register({
name: `siteMonitor-${userId}`,
cron: setting.cron,
job: () => this.triggerJobOnce(userId),
});
}
}
async triggerJobOnce(userId?:number) {
logger.info(`站点证书检查开始执行[${userId??'所有用户'}]`);
const query:any = { disabled: false };
if(userId){
query.userId = userId;
//判断是否已关闭
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(userId, UserSiteMonitorSetting);
if (!setting.cron) {
return;
}
}
let offset = 0;
const limit = 50;
while (true) {
const res = await this.page({
query: query,
page: { offset, limit },
});
const { records } = res;
if (records.length === 0) {
break;
}
offset += records.length;
const isCommon = !userId;
await this.checkList(records,isCommon);
}
logger.info(`站点证书检查完成[${userId??'所有用户'}]`);
}
}

View File

@@ -88,7 +88,7 @@ export class SiteIpService extends BaseService<SiteIpEntity> {
await this.updateIpCount(entity.id)
}
async check(ipId: number, domain: string, port: number) {
async check(ipId: number, domain: string, port: number,retryTimes = null) {
if(!ipId){
return
}
@@ -105,7 +105,7 @@ export class SiteIpService extends BaseService<SiteIpEntity> {
const res = await siteTester.test({
host: domain,
port: port,
retryTimes: 3,
retryTimes : retryTimes??3,
ipAddress: entity.ipAddress
});
@@ -154,7 +154,7 @@ export class SiteIpService extends BaseService<SiteIpEntity> {
}
}
async checkAll(siteInfo: SiteInfoEntity,onFinish?: (e: any) => void) {
async checkAll(siteInfo: SiteInfoEntity,retryTimes = null,onFinish?: (e: any) => void) {
const siteId = siteInfo.id;
const ips = await this.repository.find({
where: {
@@ -167,7 +167,7 @@ export class SiteIpService extends BaseService<SiteIpEntity> {
for (const item of ips) {
const func = async () => {
try {
return await this.check(item.id, domain, port);
return await this.check(item.id, domain, port,retryTimes);
} catch (e) {
logger.error("check site item error", e);
return {

View File

@@ -18,7 +18,7 @@ export type SiteTestRes = {
export class SiteTester {
async test(req: SiteTestReq): Promise<SiteTestRes> {
logger.info("测试站点:", JSON.stringify(req));
const maxRetryTimes = req.retryTimes ?? 3;
const maxRetryTimes = req.retryTimes==null ? 3 : req.retryTimes;
let tryCount = 0;
let result: SiteTestRes = {};
while (true) {
@@ -28,12 +28,12 @@ export class SiteTester {
} catch (e) {
tryCount++;
if (tryCount > maxRetryTimes) {
logger.error(`测试站点出错,重试${maxRetryTimes}次。`, e.message);
logger.error(`测试站点出错,已超过最大重试次数(${maxRetryTimes}`, e.message);
throw e;
}
//指数退避
const time = 2 ** tryCount;
logger.error(`测试站点出错,${time}s后重试`, e);
logger.error(`测试站点出错,${time}s后重试(${tryCount}/${maxRetryTimes})`, e);
await utils.sleep(time * 1000);
}
}

View File

@@ -16,6 +16,9 @@ export class HistoryEntity {
@Column({ comment: '结果状态', length: 20, nullable: true })
status: string;
@Column({ name: 'trigger_type',comment: '触发类型', length: 20, nullable: true })
triggerType: string;
@Column({
name: 'end_time',
comment: '结束时间',

View File

@@ -60,12 +60,13 @@ export class HistoryService extends BaseService<HistoryEntity> {
return new HistoryDetail(entity, log);
}
async start(pipeline: PipelineEntity) {
async start(pipeline: PipelineEntity,triggerType:string) {
const bean = {
userId: pipeline.userId,
pipelineId: pipeline.id,
title: pipeline.title,
status: 'start',
triggerType
};
const { id } = await this.add(bean);
//清除大于pipeline.keepHistoryCount的历史记录

View File

@@ -17,7 +17,7 @@ import {
Executor,
IAccessService,
ICnameProxyService,
INotificationService,
INotificationService, Notification,
Pipeline,
ResultType,
RunHistory,
@@ -45,6 +45,7 @@ import {NotificationService} from "./notification-service.js";
import {UserSuiteEntity, UserSuiteService} from "@certd/commercial-core";
import {CertInfoService} from "../../monitor/service/cert-info-service.js";
import {TaskServiceBuilder} from "./task-service-getter.js";
import {nanoid} from "nanoid";
const runningTasks: Map<string | number, Executor> = new Map();
@@ -110,6 +111,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
}
async add(bean: PipelineEntity) {
bean.status = ResultType.none
await this.save(bean);
return bean;
}
@@ -148,8 +150,17 @@ export class PipelineService extends BaseService<PipelineEntity> {
const info = await this.info(pipelineId);
if (info && !info.disabled) {
const pipeline = JSON.parse(info.content);
// 手动触发不要await
this.registerTriggers(pipeline);
this.registerTriggers(pipeline,false);
}
}
public async registerTrigger(info:PipelineEntity) {
if (info == null) {
return;
}
if (info && !info.disabled) {
const pipeline = JSON.parse(info.content);
this.registerTriggers(pipeline,false);
}
}
@@ -173,10 +184,11 @@ export class PipelineService extends BaseService<PipelineEntity> {
//修改
old = await this.info(bean.id);
}
const pipeline = JSON.parse(bean.content || '{}');
RunnableCollection.initPipelineRunnableType(pipeline);
const isUpdate = bean.id > 0 && old != null;
const pipeline = JSON.parse(bean.content || '{}');
RunnableCollection.initPipelineRunnableType(pipeline);
let domains = [];
if (pipeline.stages) {
RunnableCollection.each(pipeline.stages, (runnable: any) => {
@@ -191,26 +203,39 @@ export class PipelineService extends BaseService<PipelineEntity> {
await this.checkMaxPipelineCount(bean, pipeline, domains);
}
if (!bean.status ){
bean.status = ResultType.none;
}
if (!isUpdate) {
//如果是添加先保存一下获取到id更新pipeline.id
await this.addOrUpdate(bean);
}
await this.clearTriggers(bean.id);
await this.doUpdatePipelineJson(bean, pipeline);
//保存域名信息到certInfo表
let fromType = 'pipeline';
if (bean.type === 'cert_upload') {
fromType = 'upload';
}
await this.certInfoService.updateDomains(pipeline.id, pipeline.userId || bean.userId, domains, fromType);
return bean;
}
/**
* 更新Pipeline 包括trigger
* @param bean
* @param pipeline
*/
async doUpdatePipelineJson(bean: PipelineEntity, pipeline:Pipeline) {
await this.clearTriggers(bean);
if (pipeline.title) {
bean.title = pipeline.title;
}
pipeline.id = bean.id;
bean.content = JSON.stringify(pipeline);
await this.addOrUpdate(bean);
await this.registerTriggerById(bean.id);
//保存域名信息到certInfo表
let fromType = 'pipeline';
if(bean.type === 'cert_upload') {
fromType = 'upload';
}
await this.certInfoService.updateDomains(pipeline.id, pipeline.userId || bean.userId, domains,fromType);
return bean;
await this.registerTrigger(bean);
}
private async checkMaxPipelineCount(bean: PipelineEntity, pipeline: Pipeline, domains: string[]) {
@@ -371,11 +396,16 @@ export class PipelineService extends BaseService<PipelineEntity> {
await this.certInfoService.deleteByPipelineId(id);
}
async clearTriggers(id: number) {
async clearTriggers(id: number | PipelineEntity) {
if (id == null) {
return;
}
const pipeline = await this.info(id);
let pipeline:PipelineEntity = null
if (typeof id === 'number') {
pipeline = await this.info(id);
}else{
pipeline = id
}
if (!pipeline) {
return;
}
@@ -493,7 +523,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
};
const userId = entity.userId;
const historyId = await this.historyService.start(entity);
const historyId = await this.historyService.start(entity,triggerType);
const userIsAdmin = await this.userService.isAdmin(userId);
const user: UserInfo = {
id: userId,
@@ -699,6 +729,58 @@ export class PipelineService extends BaseService<PipelineEntity> {
{ groupId }
);
}
async batchUpdateTrigger(ids: number[], trigger: any, userId: any){
const list = await this.find({
where:{
id: In(ids),
userId
}
})
for (const item of list) {
const pipeline = JSON.parse(item.content);
pipeline.triggers = [{
id: nanoid(),
title: '定时触发',
...trigger
}]
await this.doUpdatePipelineJson(item,pipeline)
}
}
async batchUpdateNotifications(ids: number[], notification: Notification, userId: any){
const list = await this.find({
where:{
id: In(ids),
userId
}
})
for (const item of list) {
const pipeline = JSON.parse(item.content);
pipeline.notifications = [{
id: nanoid(),
title: '通知',
/**
* type: NotificationType;
* when: NotificationWhen[];
* options: EmailOptions;
* notificationId: number;
* title: string;
* subType: string;
*/
type: "other",
...notification
}]
await this.doUpdatePipelineJson(item,pipeline)
}
}
async batchRerun(ids: number[], userId: any) {
if (!isPlus()){
throw new NeedVIPException("此功能需要升级专业版")

View File

@@ -1,6 +1,6 @@
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { MoreThan, Not, Repository } from 'typeorm';
import {In, MoreThan, Not, Repository} from 'typeorm';
import { UserEntity } from '../entity/user.js';
import * as _ from 'lodash-es';
import { BaseService, CommonException, Constants, FileService, SysInstallInfo, SysSettingsService } from '@certd/lib-server';
@@ -15,6 +15,8 @@ import { DbAdapter } from '../../../db/index.js';
import { simpleNanoId, utils } from '@certd/basic';
export type RegisterType = 'username' | 'mobile' | 'email';
export const AdminRoleId = 1
/**
* 系统用户
*/
@@ -275,7 +277,7 @@ export class UserService extends BaseService<UserEntity> {
},
});
const roleIds = userRoles.map(item => item.roleId);
if (roleIds.includes(1)) {
if (roleIds.includes(AdminRoleId)) {
return true;
}
}
@@ -313,4 +315,23 @@ export class UserService extends BaseService<UserEntity> {
return result;
}
async getAdmins() {
const admins = await this.userRoleService.find({
where: {
roleId: AdminRoleId,
},
});
const userIds = admins.map(item => item.userId);
return await this.repository.find({
where: {
id: In(userIds),
status: 1,
},
order: {
updateTime: 'DESC',
},
})
}
}

View File

@@ -24,3 +24,4 @@ export * from './plugin-notification/index.js'
export * from './plugin-flex/index.js'
export * from './plugin-farcdn/index.js'
export * from './plugin-fnos/index.js'
export * from './plugin-rainyun/index.js'

View File

@@ -101,7 +101,6 @@ export class AliyunDnsProvider extends AbstractDnsProvider {
const requestOption = {
method: 'POST',
};
try {
const ret = await this.client.request('AddDomainRecord', params, requestOption);
this.logger.info('添加域名解析成功:', JSON.stringify(options), ret.RecordId);
@@ -110,6 +109,11 @@ export class AliyunDnsProvider extends AbstractDnsProvider {
if (e.code === 'DomainRecordDuplicate') {
return;
}
if(e.code === "LastOperationNotFinished"){
this.logger.info('上一个操作还未完成5s后重试')
await this.ctx.utils.sleep(5000)
return this.createRecord(options)
}
this.logger.info('添加域名解析出错', e);
this.resolveError(e, options);
}
@@ -132,10 +136,18 @@ export class AliyunDnsProvider extends AbstractDnsProvider {
const requestOption = {
method: 'POST',
};
const ret = await this.client.request('DeleteDomainRecord', params, requestOption);
this.logger.info('删除域名解析成功:', fullRecord, value, ret.RecordId);
return ret.RecordId;
try{
const ret = await this.client.request('DeleteDomainRecord', params, requestOption);
this.logger.info('删除域名解析成功:', fullRecord, value, ret.RecordId);
return ret.RecordId;
}catch (e) {
if(e.code === "LastOperationNotFinished"){
this.logger.info('上一个操作还未完成5s后重试')
await this.ctx.utils.sleep(5000)
return this.removeRecord(options)
}
throw e
}
}
}

View File

@@ -1,29 +1,36 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { CertInfo ,CertApplyPluginNames, CertReader} from '@certd/plugin-cert';
import { AliyunAccess, AliyunClient, AliyunSslClient, createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from '@certd/plugin-lib';
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
import { CertApplyPluginNames, CertInfo, CertReader } from "@certd/plugin-cert";
import {
AliyunAccess,
AliyunClient,
AliyunClientV2,
AliyunSslClient,
createCertDomainGetterInputDefine,
createRemoteSelectInputDefine
} from "@certd/plugin-lib";
@IsTaskPlugin({
name: 'AliyunDeployCertToALB',
title: '阿里云-部署至ALB应用负载均衡',
icon: 'svg:icon-aliyun',
name: "AliyunDeployCertToALB",
title: "阿里云-部署至ALB应用负载均衡",
icon: "svg:icon-aliyun",
group: pluginGroups.aliyun.key,
desc: 'ALB,更新监听器的默认证书',
desc: "ALB,更新监听器的默认证书",
needPlus: false,
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed,
},
},
runStrategy: RunStrategy.SkipWhenSucceed
}
}
})
export class AliyunDeployCertToALB extends AbstractTaskPlugin {
@TaskInput({
title: '域名证书',
helper: '请选择证书申请任务输出的域名证书\n或者选择前置任务“上传证书到阿里云”任务的证书ID可以减少上传到阿里云的证书数量',
title: "域名证书",
helper: "请选择证书申请任务输出的域名证书\n或者选择前置任务“上传证书到阿里云”任务的证书ID可以减少上传到阿里云的证书数量",
component: {
name: 'output-selector',
from: [...CertApplyPluginNames, 'uploadCertToAliyun'],
name: "output-selector",
from: [...CertApplyPluginNames, "uploadCertToAliyun"]
},
required: true,
required: true
})
cert!: CertInfo | number;
@@ -31,122 +38,257 @@ export class AliyunDeployCertToALB extends AbstractTaskPlugin {
certDomains!: string[];
@TaskInput({
title: 'Access授权',
helper: '阿里云授权AccessKeyId、AccessKeySecret',
title: "证书接入点",
helper: "不会选就保持默认即可",
value: "cas.aliyuncs.com",
component: {
name: 'access-selector',
type: 'aliyun',
name: "a-select",
options: [
{ value: "cas.aliyuncs.com", label: "中国大陆" },
{ value: "cas.ap-southeast-1.aliyuncs.com", label: "新加坡" },
{ value: "cas.eu-central-1.aliyuncs.com", label: "德国(法兰克福)" }
]
},
required: true,
required: true
})
casEndpoint!: string;
@TaskInput({
title: "Access授权",
helper: "阿里云授权AccessKeyId、AccessKeySecret",
component: {
name: "access-selector",
type: "aliyun"
},
required: true
})
accessId!: string;
@TaskInput(
createRemoteSelectInputDefine({
title: 'ALB所在地区',
typeName: 'AliyunDeployCertToALB',
title: "ALB所在地区",
typeName: "AliyunDeployCertToALB",
multi: false,
action: AliyunDeployCertToALB.prototype.onGetRegionList.name,
watches: ['accessId'],
watches: ["accessId"]
})
)
regionId: string;
@TaskInput(
createRemoteSelectInputDefine({
title: '负载均衡列表',
helper: '要部署证书的负载均衡ID',
typeName: 'AliyunDeployCertToALB',
title: "负载均衡列表",
helper: "要部署证书的负载均衡ID",
typeName: "AliyunDeployCertToALB",
action: AliyunDeployCertToALB.prototype.onGetLoadBalanceList.name,
watches: ['regionId'],
watches: ["regionId"]
})
)
loadBalancers!: string[];
@TaskInput(
createRemoteSelectInputDefine({
title: '监听器列表',
helper: '要部署证书的监听器列表',
typeName: 'AliyunDeployCertToALB',
title: "监听器列表",
helper: "要部署证书的监听器列表",
typeName: "AliyunDeployCertToALB",
action: AliyunDeployCertToALB.prototype.onGetListenerList.name,
watches: ['loadBalancers'],
watches: ["loadBalancers"]
})
)
listeners!: string[];
@TaskInput({
title: '证书接入点',
helper: '不会选就保持默认即可',
value: 'cas.aliyuncs.com',
component: {
name: 'a-select',
options: [
{ value: 'cas.aliyuncs.com', label: '中国大陆' },
{ value: 'cas.ap-southeast-1.aliyuncs.com', label: '新加坡' },
{ value: 'cas.eu-central-1.aliyuncs.com', label: '德国(法兰克福)' },
],
},
required: true,
})
casEndpoint!: string;
async onInstance() {}
@TaskInput({
title: "部署证书类型",
value: "default",
component: {
name: "a-select",
vModel: "value",
options: [
{
label: "默认证书",
value: "default"
},
{
label: "扩展证书",
value: "extension"
}
]
},
required: true
}
)
deployType: string = "default";
async onInstance() {
}
async getLBClient(access: AliyunAccess, region: string) {
const client = new AliyunClient({ logger: this.logger });
const version = '2020-06-16';
const version = "2020-06-16";
await client.init({
accessKeyId: access.accessKeyId,
accessKeySecret: access.accessKeySecret,
//https://wafopenapi.cn-hangzhou.aliyuncs.com
endpoint: `https://alb.${region}.aliyuncs.com`,
apiVersion: version,
apiVersion: version
});
return client;
}
getALBClientV2(access: AliyunAccess) {
return access.getClient(`alb.${this.regionId}.aliyuncs.com`);
}
async execute(): Promise<void> {
this.logger.info(`开始部署证书到阿里云(alb)`);
const access = await this.getAccess<AliyunAccess>(this.accessId);
const certId = await this.getAliyunCertId(access);
const client = await this.getLBClient(access, this.regionId);
//部署扩展证书
const albClientV2 = this.getALBClientV2(access);
if (this.deployType === "extension") {
await this.deployExtensionCert(albClientV2, certId);
} else {
const client = await this.getLBClient(access, this.regionId);
await this.deployDefaultCert(certId, client);
}
await this.ctx.utils.sleep(10000)
for (const listener of this.listeners) {
await this.clearInvalidCert(albClientV2, listener);
}
this.logger.info("执行完成");
}
private async deployDefaultCert(certId: any, client: AliyunClient) {
for (const listener of this.listeners) {
//查询原来的证书
let params: any = {};
params = {
ListenerId: listener,
Certificates: [
{
CertificateId: certId,
},
],
CertificateId: certId
}
]
};
const res = await client.request('UpdateListenerAttribute', params);
const res = await client.request("UpdateListenerAttribute", params);
this.checkRet(res);
this.logger.info(`部署${listener}监听器证书成功`, JSON.stringify(res));
//删除旧证书关联
}
this.logger.info('执行完成');
}
async deployExtensionCert(client: AliyunClientV2, certId: any) {
for (const listenerId of this.listeners) {
this.logger.info(`开始部署监听器${listenerId}的扩展证书`);
await client.doRequest({
// 接口名称
action: "AssociateAdditionalCertificatesWithListener",
// 接口版本
version: "2020-06-16",
data: {
query: {
ListenerId: listenerId,
Certificates: [
{
CertificateId: certId
}
]
}
}
});
this.logger.info(`部署监听器${listenerId}的扩展证书成功`);
}
}
async clearInvalidCert(client: AliyunClientV2, listener: string) {
this.logger.info(`开始清理监听器${listener}的过期证书`);
const req = {
// 接口名称
action: "ListListenerCertificates",
// 接口版本
version: "2020-06-16",
data: {
query: {
ListenerId: listener
}
}
};
const res = await client.doRequest(req);
const list = res.Certificates;
if (list.length === 0) {
this.logger.info(`监听器${listener}没有绑定证书`);
return
}
const sslClient = new AliyunSslClient({
access: client.access,
logger: this.logger,
endpoint: this.casEndpoint
});
const certIds = [];
for (const item of list) {
if (item.Status !== "Associated") {
continue;
}
if (item.IsDefault) {
continue;
}
certIds.push( parseInt(item.CertificateId));
}
//检查是否过期,过期则删除
const invalidCertIds = [];
for (const certId of certIds) {
const res = await sslClient.getCertInfo(certId);
if (res.notAfter < new Date().getTime()) {
invalidCertIds.push(certId);
}
}
if (invalidCertIds.length === 0) {
this.logger.info(`监听器${listener}没有过期的证书`);
return
}
this.logger.info(`开始解绑过期的证书:${invalidCertIds}`);
await client.doRequest({
// 接口名称
action: "DissociateAdditionalCertificatesFromListener",
// 接口版本
version: "2020-06-16",
data: {
query: {
ListenerId: listener,
Certificates: invalidCertIds.map((item) => {
return {
CertificateId: item
}
})
}
}
});
this.logger.info(`解绑过期证书成功`);
}
async getAliyunCertId(access: AliyunAccess) {
let certId: any = this.cert;
if (typeof this.cert === 'object') {
if (typeof this.cert === "object") {
const sslClient = new AliyunSslClient({
access,
logger: this.logger,
endpoint: this.casEndpoint,
endpoint: this.casEndpoint
});
const certName = this.buildCertName(CertReader.getMainDomain(this.cert.crt))
const certName = this.buildCertName(CertReader.getMainDomain(this.cert.crt));
certId = await sslClient.uploadCert({
name: certName,
cert: this.cert,
cert: this.cert
});
}
return certId;
@@ -154,74 +296,74 @@ export class AliyunDeployCertToALB extends AbstractTaskPlugin {
async onGetRegionList(data: any) {
if (!this.accessId) {
throw new Error('请选择Access授权');
throw new Error("请选择Access授权");
}
const access = await this.getAccess<AliyunAccess>(this.accessId);
const client = await this.getLBClient(access, 'cn-shanghai');
const client = await this.getLBClient(access, "cn-shanghai");
const res = await client.request('DescribeRegions', {});
const res = await client.request("DescribeRegions", {});
this.checkRet(res);
if (!res?.Regions || res?.Regions.length === 0) {
throw new Error('没有找到Regions列表');
throw new Error("没有找到Regions列表");
}
return res.Regions.map((item: any) => {
return {
label: item.LocalName,
value: item.RegionId,
endpoint: item.RegionEndpoint,
endpoint: item.RegionEndpoint
};
});
}
async onGetLoadBalanceList(data: any) {
if (!this.accessId) {
throw new Error('请先选择Access授权');
throw new Error("请先选择Access授权");
}
if (!this.regionId) {
throw new Error('请先选择地区');
throw new Error("请先选择地区");
}
const access = await this.getAccess<AliyunAccess>(this.accessId);
const client = await this.getLBClient(access, this.regionId);
const params = {
MaxResults: 100,
MaxResults: 100
};
const res = await client.request('ListLoadBalancers', params);
const res = await client.request("ListLoadBalancers", params);
this.checkRet(res);
if (!res?.LoadBalancers || res?.LoadBalancers.length === 0) {
throw new Error('没有找到LoadBalancers');
throw new Error("没有找到LoadBalancers");
}
return res.LoadBalancers.map((item: any) => {
const label = `${item.LoadBalancerId}<${item.LoadBalancerName}}>`;
return {
label: label,
value: item.LoadBalancerId,
value: item.LoadBalancerId
};
});
}
async onGetListenerList(data: any) {
if (!this.accessId) {
throw new Error('请先选择Access授权');
throw new Error("请先选择Access授权");
}
if (!this.regionId) {
throw new Error('请先选择地区');
throw new Error("请先选择地区");
}
const access = await this.getAccess<AliyunAccess>(this.accessId);
const client = await this.getLBClient(access, this.regionId);
const params: any = {
MaxResults: 100,
MaxResults: 100
};
if (this.loadBalancers && this.loadBalancers.length > 0) {
params.LoadBalancerIds = this.loadBalancers;
}
const res = await client.request('ListListeners', params);
const res = await client.request("ListListeners", params);
this.checkRet(res);
if (!res?.Listeners || res?.Listeners.length === 0) {
throw new Error('没有找到HTTPS监听器');
throw new Error("没有找到HTTPS监听器");
}
return res.Listeners.map((item: any) => {
@@ -229,16 +371,19 @@ export class AliyunDeployCertToALB extends AbstractTaskPlugin {
return {
label: label,
value: item.ListenerId,
lbid: item.LoadBalancerId,
lbid: item.LoadBalancerId
};
});
}
checkRet(ret: any) {
if (ret.Code != null) {
throw new Error(ret.Message);
}
}
}
new AliyunDeployCertToALB();

View File

@@ -1,6 +1,13 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { CertInfo, CertReader } from "@certd/plugin-cert";
import { AliyunAccess, AliyunClient, AliyunSslClient, createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from '@certd/plugin-lib';
import {
AliyunAccess,
AliyunClient,
AliyunClientV2,
AliyunSslClient,
createCertDomainGetterInputDefine,
createRemoteSelectInputDefine
} from "@certd/plugin-lib";
import { CertApplyPluginNames} from '@certd/plugin-cert';
@IsTaskPlugin({
name: 'AliyunDeployCertToNLB',
@@ -30,6 +37,23 @@ export class AliyunDeployCertToNLB extends AbstractTaskPlugin {
@TaskInput(createCertDomainGetterInputDefine({ props: { required: false } }))
certDomains!: string[];
@TaskInput({
title: '证书接入点',
helper: '不会选就保持默认即可',
value: 'cas.aliyuncs.com',
component: {
name: 'a-select',
options: [
{ value: 'cas.aliyuncs.com', label: '中国大陆' },
{ value: 'cas.ap-southeast-1.aliyuncs.com', label: '新加坡' },
{ value: 'cas.eu-central-1.aliyuncs.com', label: '德国(法兰克福)' },
],
},
required: true,
})
casEndpoint!: string;
@TaskInput({
title: 'Access授权',
helper: '阿里云授权AccessKeyId、AccessKeySecret',
@@ -74,21 +98,30 @@ export class AliyunDeployCertToNLB extends AbstractTaskPlugin {
)
listeners!: string[];
@TaskInput({
title: '证书接入点',
helper: '不会选就保持默认即可',
value: 'cas.aliyuncs.com',
component: {
name: 'a-select',
options: [
{ value: 'cas.aliyuncs.com', label: '中国大陆' },
{ value: 'cas.ap-southeast-1.aliyuncs.com', label: '新加坡' },
{ value: 'cas.eu-central-1.aliyuncs.com', label: '德国(法兰克福)' },
],
},
required: true,
})
casEndpoint!: string;
title: "部署证书类型",
value: "default",
component: {
name: "a-select",
vModel: "value",
options: [
{
label: "默认证书",
value: "default"
},
{
label: "扩展证书",
value: "extension"
}
]
},
required: true
}
)
deployType: string = "default";
async onInstance() {}
@@ -110,24 +143,137 @@ export class AliyunDeployCertToNLB extends AbstractTaskPlugin {
this.logger.info(`开始部署证书到阿里云(nlb)`);
const access = await this.getAccess<AliyunAccess>(this.accessId);
const certId = await this.getAliyunCertId(access);
const nlbClientV2 = this.getNLBClientV2(access);
if (this.deployType === "extension") {
//部署扩展证书
await this.deployExtensionCert(nlbClientV2, certId);
} else {
const client = await this.getLBClient(access, this.regionId);
await this.deployDefaultCert(certId, client);
}
const client = await this.getLBClient(access, this.regionId);
await this.ctx.utils.sleep(10000)
for (const listener of this.listeners) {
await this.clearInvalidCert(nlbClientV2, listener);
}
this.logger.info('执行完成');
}
async deployExtensionCert(client: AliyunClientV2, certId: any) {
for (const listenerId of this.listeners) {
this.logger.info(`开始部署监听器${listenerId}的扩展证书`);
await client.doRequest({
// 接口名称
action: "AssociateAdditionalCertificatesWithListener",
// 接口版本
version: "2022-04-30",
data: {
body: {
ListenerId: listenerId,
RegionId: this.regionId,
"AdditionalCertificateIds.1": certId
}
}
});
this.logger.info(`部署监听器${listenerId}的扩展证书成功`);
}
}
async deployDefaultCert(certId: any, client: AliyunClient) {
for (const listener of this.listeners) {
//查询原来的证书
const params: any = {
RegionId: this.regionId,
ListenerId: listener,
CertificateIds: [certId],
CertificateIds:[certId], //旧sdk
};
const res = await client.request('UpdateListenerAttribute', params);
this.checkRet(res);
this.logger.info(`部署${listener}监听器证书成功`, JSON.stringify(res));
//删除旧证书关联
}
this.logger.info('执行完成');
}
getNLBClientV2(access: AliyunAccess) {
return access.getClient(`nlb.${this.regionId}.aliyuncs.com`);
}
async clearInvalidCert(client: AliyunClientV2, listener: string) {
this.logger.info(`开始清理监听器${listener}的过期证书`);
const req = {
// 接口名称
action: "ListListenerCertificates",
// 接口版本
version: "2022-04-30",
data: {
body: {
ListenerId: listener,
RegionId: this.regionId
}
}
};
const res = await client.doRequest(req);
const list = res.Certificates;
if (list.length === 0) {
this.logger.info(`监听器${listener}没有绑定证书`);
return
}
const sslClient = new AliyunSslClient({
access: client.access,
logger: this.logger,
endpoint: this.casEndpoint
});
const certIds = [];
for (const item of list) {
if (item.Status !== "Associated") {
continue;
}
if (item.IsDefault) {
continue;
}
certIds.push( parseInt(item.CertificateId));
}
//检查是否过期,过期则删除
const invalidCertIds = [];
for (const certId of certIds) {
const res = await sslClient.getCertInfo(certId);
if (res.notAfter < new Date().getTime()) {
invalidCertIds.push(certId);
}
}
if (invalidCertIds.length === 0) {
this.logger.info(`监听器${listener}没有过期的证书`);
return
}
this.logger.info(`开始解绑过期的证书:${invalidCertIds}`);
const ids:any = {}
let i = 0
for (const certId of invalidCertIds) {
i++
ids[`AdditionalCertificateIds.${i}`] = certId;
}
await client.doRequest({
// 接口名称
action: "DissociateAdditionalCertificatesFromListener",
// 接口版本
version: "2022-04-30",
data: {
body: {
ListenerId: listener,
RegionId: this.regionId,
...ids
}
}
});
this.logger.info(`解绑过期证书成功`);
}
async getAliyunCertId(access: AliyunAccess) {

View File

@@ -233,6 +233,7 @@ export class AliyunDeployCertToSLB extends AbstractTaskPlugin {
RegionId: this.regionId,
AliCloudCertificateId: aliyunCert.certId,
AliCloudCertificateName: aliyunCert.certName,
AliCloudCertificateRegionId: aliyunCert.casRegion
};
const res = await client.request('UploadServerCertificate', params);

View File

@@ -1,5 +1,6 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, TaskOutput } from "@certd/pipeline";
import { GithubAccess } from "../access.js";
import {SshClient} from "@certd/plugin-lib";
@IsTaskPlugin({
//命名规范,插件类型+功能就是目录plugin-demo中的demo大写字母开头驼峰命名
@@ -56,6 +57,31 @@ export class GithubCheckRelease extends AbstractTaskPlugin {
lastVersion?: string;
@TaskInput({
title: '主机登录配置',
helper: '登录',
component: {
name: 'access-selector',
type: 'ssh',
},
required: false,
})
sshAccessId!: string;
@TaskInput({
title: 'shell脚本命令',
component: {
name: 'a-textarea',
vModel: 'value',
rows: 6,
placeholder: `#拉取最新版镜像\ndocker pull greper/certd:latest \n#重建容器 \nnohup sh -c 'sleep 10; cd ~/deploy/certd/ ; docker compose down; docker compose up -d' >/dev/null & `,
},
helper: '有新版本后执行命令,比如:拉取最新版镜像,然后重建容器\n注意自己升级自己需要使用nobup配合sleep',
required: false,
})
script!: string;
//插件实例化时执行的方法
async onInstance() {
}
@@ -71,6 +97,7 @@ export class GithubCheckRelease extends AbstractTaskPlugin {
if(res.tag_name == null || res.tag_name ==lastVersion){
this.logger.info(`暂无更新,${res.tag_name}`);
this.lastVersion = res.tag_name || lastVersion;
return "skip"
}
//有更新
@@ -99,6 +126,19 @@ export class GithubCheckRelease extends AbstractTaskPlugin {
})
}
if (this.script != null && this.script.trim() != "") {
const connectConf = await this.getAccess(this.sshAccessId);
const sshClient = new SshClient(this.logger);
const scripts = this.script.split('\n');
await sshClient.exec({
connectConf,
script: scripts,
env: {
REPO: this.repoName,
LAST_VERSION: this.lastVersion,
}
});
}
}

View File

@@ -59,7 +59,8 @@ export class TelegramNotification extends BaseNotification {
skipSslVerify: boolean;
replaceText(text: string) {
return text.replaceAll('.', '\\.').replaceAll('*', '\\*');
// .*()<> 等都需要用\\进行替换
return text.replace(/[\\.*()<>]/g, '\\$&');
}
async send(body: NotificationBody) {
if (!this.botToken || !this.chatId) {

View File

@@ -0,0 +1,152 @@
import {AccessInput, BaseAccess, IsAccess} from "@certd/pipeline";
import {HttpRequestConfig} from "@certd/basic";
import { CertInfo } from "@certd/plugin-cert";
/**
*/
@IsAccess({
name: "rainyun",
title: "雨云授权",
desc: "https://app.rainyun.com/",
icon: "svg:icon-lucky"
})
export class RainyunAccess extends BaseAccess {
@AccessInput({
title: "ApiKey",
component: {
placeholder: "api-key",
component: {
name: "a-input",
vModel: "value"
}
},
helper:"https://app.rainyun.com/account/settings/api-key",
encrypt: true,
required: true
})
apiKey!: string;
@AccessInput({
title: "测试",
component: {
name: "api-test",
action: "TestRequest"
},
helper: "点击测试接口是否正常"
})
testRequest = true;
async onTestRequest() {
await this.getDomainList({limit:1});
return "ok"
}
// {"columnFilters":{"domains.Domain":"domain"},"sort":[],"page":1,"perPage":20}
async getDomainList(req:{offset?:number,limit?:number,query?:string}){
const size = req.limit ?? 20;
const offset = req.offset ?? 0;
let page = Math.floor(offset / size);
if(offset % size === 0 ){
page++
}
const options ={
page: page,
perPage: size,
columnFilters: {
"domains.Domain": req.query??""
},
}
const res = await this.doRequest({
url: `/product/domain/?options=${encodeURIComponent(JSON.stringify(options))}`,
method: "GET",
});
return {
total: res.TotalRecords,
list: res.Records || [],
limit: size,
offset: offset
}
}
async getCertList(req:{offset?:number,limit?:number,query?:string}){
const size = req.limit ?? 20;
const offset = req.offset ?? 0;
let page = Math.floor(offset / size);
if(offset % size === 0 ){
page++
}
const options ={
columnFilters: {
Domain: req.query??""
},
sort:[],
page: page,
perPage: size,
}
const res = await this.doRequest({
url: `product/sslcenter/?options=${encodeURIComponent(JSON.stringify(options))}`,
method: "GET",
});
return {
total: res.TotalRecords,
list: res.Records || [],
limit: size,
offset: offset
}
}
async doCertReplace(req:{certId:number,cert:CertInfo}){
// /product/sslcenter/{id}
return await this.doRequest({
url: `product/sslcenter/${req.certId}`,
method: "PUT",
data: {
cert: req.cert.crt,
key: req.cert.key,
}
});
}
async getDomainId(domain:string){
const res = await this.getDomainList({query: domain,limit:1});
if (res.list.length === 0) {
throw new Error(`域名${domain}不存在` );
}
return res.list[0].id;
}
async doRequest(req:HttpRequestConfig){
const res = await this.ctx.http.request({
url: req.url,
baseURL:"https://api.v2.rainyun.com",
method: req.method|| "POST",
data: req.data,
params: req.params,
headers:{
"X-Api-Key": this.apiKey
},
// httpProxy: this.httpProxy||undefined,
});
if (res.code === 200) {
return res.data;
}
throw new Error(res.message || res);
}
}
new RainyunAccess();

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