Compare commits

..

50 Commits

Author SHA1 Message Date
xiaojunnuo 83df29d832 chore: 1 2026-01-22 11:10:53 +08:00
xiaojunnuo 607afe864a perf: cname记录支持批量导入和导出 2026-01-22 10:56:45 +08:00
xiaojunnuo a97cee84f3 perf: 支持同步域名过期时间 2026-01-22 00:59:28 +08:00
xiaojunnuo ad64384891 perf: 域名导入 2026-01-21 18:24:03 +08:00
xiaojunnuo f75c73d739 perf: 优化流水线创建入口,各种证书申请任务类型拆分成多个按钮 2026-01-21 13:27:14 +08:00
xiaojunnuo 418bcddc95 fix: 修复流水线复制出错的bug 2026-01-20 16:56:36 +08:00
xiaojunnuo 61192b998a fix: 修复插件修改名字和删除后没有注销注册的bug 2026-01-20 11:52:14 +08:00
xiaojunnuo 5ea2b09dc3 fix: 编辑插件author不允许出现符号 2026-01-20 11:18:10 +08:00
xiaojunnuo 5bfc2c4a9b chore: 1 2026-01-20 00:15:31 +08:00
xiaojunnuo 8ec47c3894 Merge branch 'v2-domain-sync' of https://github.com/certd/certd into v2-domain-sync 2026-01-20 00:13:10 +08:00
xiaojunnuo f4423638a2 perf: 支持从提供商导入域名列表 2026-01-20 00:13:05 +08:00
xiaojunnuo 7b3444308b chore: docs 2026-01-19 16:27:34 +08:00
xiaojunnuo 5ec9916817 chore: FormDialog 2026-01-19 11:01:48 +08:00
xiaojunnuo be1a70299f chore: 域名自动同步初步 2026-01-16 18:18:39 +08:00
xiaojunnuo 8685aa371a chore: publish github 2026-01-16 15:47:25 +08:00
xiaojunnuo 0224faa184 chore: publish github 2026-01-16 12:57:14 +08:00
xiaojunnuo 8546e326cf build: update github actions 2026-01-16 09:30:47 +08:00
xiaojunnuo 9956fd2f04 Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-01-16 09:12:51 +08:00
xiaojunnuo 4f669ca82f build: release 2026-01-16 01:32:11 +08:00
xiaojunnuo 1cd3881aa8 chore: docs 2026-01-16 01:29:50 +08:00
xiaojunnuo e634513f7b chore: docs 2026-01-16 01:22:51 +08:00
xiaojunnuo 7b6cde6ae3 build: publish 2026-01-16 01:00:16 +08:00
xiaojunnuo 18146fdf9e build: trigger build image 2026-01-16 01:00:03 +08:00
xiaojunnuo 2c80c35b21 v1.38.1 2026-01-16 00:58:32 +08:00
xiaojunnuo 54b73769b8 build: prepare to build 2026-01-16 00:55:54 +08:00
xiaojunnuo f7983ee4d9 chore: docs 2026-01-16 00:46:57 +08:00
xiaojunnuo 9eace86aee perf: 自定义插件支持使用_ctx.import("/@/xxx.js")以绝对路径引用模块 2026-01-16 00:46:26 +08:00
xiaojunnuo 2fbb58eb2b fix: 修复自定义插件name丢失author导致找不到插件的bug 2026-01-15 23:43:07 +08:00
xiaojunnuo 187d04e3a1 chore: docs 2026-01-15 11:19:26 +08:00
xiaojunnuo d5d7d73440 chore: publish 2026-01-14 16:07:15 +08:00
xiaojunnuo b747e281b7 chore: publish 2026-01-14 16:00:49 +08:00
xiaojunnuo e024d50476 chore: publish 2026-01-14 15:57:46 +08:00
xiaojunnuo a6ba48c075 chore: publish 2026-01-14 15:54:46 +08:00
xiaojunnuo e19375387d chore: 1 2026-01-14 15:25:58 +08:00
xiaojunnuo a9f68187d4 chore: release 2026-01-14 15:25:27 +08:00
xiaojunnuo 4d754fa78d chore: 拆分git publish 2026-01-14 15:23:42 +08:00
xiaojunnuo 6d07ab2bc5 chore: release 2026-01-14 13:51:40 +08:00
xiaojunnuo a60b00c440 chore: docs 2026-01-14 12:15:04 +08:00
xiaojunnuo d0f3f303b6 build: release 2026-01-14 12:07:02 +08:00
xiaojunnuo 4fc8acce8c perf: 优化内存占用 2026-01-14 11:37:20 +08:00
xiaojunnuo 0797a4f99d chore: build 2026-01-14 02:06:08 +08:00
xiaojunnuo db453c8038 chore: 修复metadata的一些bug 2026-01-14 02:05:31 +08:00
xiaojunnuo c776c34cfd chore: build 2026-01-14 00:14:00 +08:00
xiaojunnuo 170b39fde6 chore: release 2026-01-14 00:12:43 +08:00
xiaojunnuo fc27a66825 chore: build only 2026-01-14 00:03:58 +08:00
xiaojunnuo 06b49c140e chore: publish gitee 2026-01-14 00:00:10 +08:00
xiaojunnuo 3ab45c91e1 chore: publish release to gitee 2026-01-13 23:58:50 +08:00
xiaojunnuo 6660161cec chore: prebuild export md 2026-01-13 23:33:30 +08:00
xiaojunnuo 8c6e207008 build: publish 2026-01-13 23:30:47 +08:00
xiaojunnuo 4180e3c540 build: trigger build image 2026-01-13 23:30:35 +08:00
150 changed files with 2435 additions and 1353 deletions
-45
View File
@@ -1,45 +0,0 @@
name: build-node-base-image
# 废弃,比默认的占用内存更多
on:
push:
branches: ['v2-dev1']
paths:
- "scripts/build/Dockerfile"
# schedule:
# - # 国际时间 19:17 执行,北京时间3:17 ↙↙↙ 改成你想要每天自动执行的时间
# - cron: '17 19 * * *'
permissions:
contents: read
packages: write
jobs:
build-node-base-image:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: true
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.dockerhub_username }}
password: ${{ secrets.dockerhub_password }}
- name: Build default platforms
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
context: ./scripts/build/
tags: |
greper/node-base:22-alpine-2
+1 -1
View File
@@ -19,6 +19,7 @@ permissions:
jobs:
deploy-certd-demo:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Checkout Code
uses: actions/checkout@v4
@@ -55,4 +56,3 @@ jobs:
}
retry-count: 3
retry-delay: 5000
+5 -4
View File
@@ -4,10 +4,10 @@ on:
branches: ['v2-dev']
paths:
- "trigger/publish.trigger"
# workflow_run:
# workflows: [ "deploy-demo" ]
# types:
# - completed
workflow_run:
workflows: [ "build-image-for-release" ]
types:
- completed
# schedule:
# - # 国际时间 19:17 执行,北京时间3:17 ↙↙↙ 改成你想要每天自动执行的时间
@@ -19,6 +19,7 @@ permissions:
jobs:
publish-atomgit:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Checkout Code
uses: actions/checkout@v4
+39
View File
@@ -0,0 +1,39 @@
name: publish-gitee
on:
push:
branches: ['v2-dev']
paths:
- "trigger/publish.trigger"
workflow_run:
workflows: [ "build-image-for-release" ]
types:
- completed
# schedule:
# - # 国际时间 19:17 执行,北京时间3:17 ↙↙↙ 改成你想要每天自动执行的时间
# - cron: '17 19 * * *'
permissions:
contents: read
packages: write
jobs:
publish-gitee:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: true
- name: publish_to_gitee
id: publish_to_gitee
run: |
export GITEE_TOKEN=${{ secrets.GITEE_TOKEN }}
rm -rf ./pnpm*.yaml
npm install -g pnpm
pnpm install
npm run publish_to_gitee
working-directory: ./
+39
View File
@@ -0,0 +1,39 @@
name: publish-github
on:
push:
branches: ['v2-dev']
paths:
- "trigger/publish.trigger"
workflow_run:
workflows: [ "build-image-for-release" ]
types:
- completed
# schedule:
# - # 国际时间 19:17 执行,北京时间3:17 ↙↙↙ 改成你想要每天自动执行的时间
# - cron: '17 19 * * *'
permissions:
contents: read
packages: write
jobs:
publish-github:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: true
- name: publish_to_github
id: publish_to_github
run: |
export GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}
rm -rf ./pnpm*.yaml
npm install -g pnpm
pnpm install
npm run publish_to_github
working-directory: ./
+1 -10
View File
@@ -130,13 +130,4 @@ jobs:
Content-Type: application/json
retry-count: 3
retry-delay: 5000
- name: publish_to_atomgit
id: publish_to_atomgit
run: |
rm -rf ./packages/ui/certd-client/dist/**/*.gz
cd ./packages/ui/certd-client/dist && zip -r ../../../ui.zip .
export ATOMGIT_TOKEN=${{ secrets.ATOMGIT_TOKEN }}
pnpm install
npm run publish_to_atomgit
working-directory: ./
+4 -1
View File
@@ -10,5 +10,8 @@
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"editor.tabSize": 2,
"explorer.autoReveal": false
"explorer.autoReveal": false,
"[javascript]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
}
}
+11
View File
@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.38.1](https://github.com/certd/certd/compare/v1.38.0...v1.38.1) (2026-01-15)
### Bug Fixes
* 修复自定义插件name丢失author导致找不到插件的bug ([2fbb58e](https://github.com/certd/certd/commit/2fbb58eb2b239eab4864f90aa72b0ef2ada38e8f))
### Performance Improvements
* 优化内存占用 ([4fc8acc](https://github.com/certd/certd/commit/4fc8acce8c1beec38c24b0977b71ff6b18cb52c9))
* 自定义插件支持使用_ctx.import("/@/xxx.js")以绝对路径引用模块 ([9eace86](https://github.com/certd/certd/commit/9eace86aeeb48c23b55102fc5d42088294d9eb97))
# [1.38.0](https://github.com/certd/certd/compare/v1.37.17...v1.38.0) (2026-01-13)
### Bug Fixes
+2 -1
View File
@@ -108,12 +108,12 @@ export default defineConfig({
text: "常见问题",
items: [
{text: "QA", link: "/guide/qa/use.md"},
{text: "忘记密码/无法登录", link: "/guide/use/forgotpasswd/"},
{text: "群晖证书部署", link: "/guide/use/synology/"},
{text: "腾讯云密钥获取", link: "/guide/use/tencent/"},
{text: "连接windows主机", link: "/guide/use/host/windows.md"},
{text: "Google EAB获取", link: "/guide/use/google/"},
{text: "阿里云相关", link: "/guide/use/aliyun/"},
{text: "忘记密码", link: "/guide/use/forgotpasswd/"},
{text: "数据备份", link: "/guide/use/backup/"},
{text: "Certd本身的证书更新", link: "/guide/use/https/index.md"},
{text: "js脚本插件使用", link: "/guide/use/custom-script/index.md"},
@@ -124,6 +124,7 @@ export default defineConfig({
{text: "子域名托管", link: "/guide/use/cert/subdomain.md"},
{text: "流水线有效期", link: "/guide/use/pipeline/valid.md"},
{text: "IP证书申请", link: "/guide/use/cert/ip.md"},
{text: "插件开发", link: "/guide/use/dev/plugin.md"},
]
},
{
+42
View File
@@ -3,6 +3,48 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.38.1](https://github.com/certd/certd/compare/v1.38.0...v1.38.1) (2026-01-15)
### Bug Fixes
* 修复自定义插件name丢失author导致找不到插件的bug ([2fbb58e](https://github.com/certd/certd/commit/2fbb58eb2b239eab4864f90aa72b0ef2ada38e8f))
### Performance Improvements
* 优化内存占用 ([4fc8acc](https://github.com/certd/certd/commit/4fc8acce8c1beec38c24b0977b71ff6b18cb52c9))
* 自定义插件支持使用_ctx.import("/@/xxx.js")以绝对路径引用模块 ([9eace86](https://github.com/certd/certd/commit/9eace86aeeb48c23b55102fc5d42088294d9eb97))
# [1.38.0](https://github.com/certd/certd/compare/v1.37.17...v1.38.0) (2026-01-13)
### Bug Fixes
* 修复禁用第三方登录自动注册无效的bug ([7ee39fd](https://github.com/certd/certd/commit/7ee39fd4eddfc847bcef879f0904a4319993d081))
* 修复又拍云upyun密码错误没有报错的bug ([235972f](https://github.com/certd/certd/commit/235972f3dabe0b87879a2d9950367dc45edfebe8))
* 修复重启certd后,再启用流水线,不会自动执行的bug ([468ccbf](https://github.com/certd/certd/commit/468ccbf2b725fc4b78ce4b950a114e4a4be57698))
* 优化源码部署缺少wget的提示 ([f193341](https://github.com/certd/certd/commit/f193341eaef765b7586a0b6e7c73015470536cc2))
### Features
* 【破坏性更新】插件改为metadata加载模式,plugin-cert、plugin-lib包部分代码转移到certd-server中,影响自定义插件,需要修改相关import引用 ([a3fb249](https://github.com/certd/certd/commit/a3fb24993d7ac8fbb0bb354fa02ef067f609021e))
* 通过metadata加载插件,降低内存占用 ([7634f15](https://github.com/certd/certd/commit/7634f153b7004462f207062c0502d8345e318cc7))
### Performance Improvements
* 流水线页面可以查看证书过期时间 ([be03d8e](https://github.com/certd/certd/commit/be03d8e13752c355dbec158da78b9cb4c3b3bb5d))
* 每页记录条数保持 ([14f9987](https://github.com/certd/certd/commit/14f99875fb3f535fa5ffb7bf5db3960b105aa7aa))
* 手机号登录放到前面 ([26ac081](https://github.com/certd/certd/commit/26ac08118219407c5dd3afc35130cdd48b8fab05))
* 新增部署1panel面板证书插件 ([4243622](https://github.com/certd/certd/commit/42436224148d6fffe5da8e5e0185a698e079032b))
* 优化微信支付对接文档 ([64e0d9a](https://github.com/certd/certd/commit/64e0d9a4d54b0d9da028be2c5e0ece7a97b2c250))
* 优化站点监控,支持设置忽略主站证书一致性,支持开启和关闭自动同步ip ([26f75c7](https://github.com/certd/certd/commit/26f75c71ba8866278dbe117f1bfaf671e7f70781))
* 增加邮件发送证书模版配置 ([cabc4da](https://github.com/certd/certd/commit/cabc4da3ac003a8c699c69f5bffea4c149be185c))
* 站点监控增加是否自动同步IP开关 ([5268904](https://github.com/certd/certd/commit/52689049ae8e004e1252ab1e2872fbf676e0295f))
* 证书流水线可以开启webhook ([840bd52](https://github.com/certd/certd/commit/840bd526714072315244a6900c95395d2d62f647))
* 支持部署到exsiopenwrt ([dae87e2](https://github.com/certd/certd/commit/dae87e26a3266a2bf26afe1ef4c489a3f6bf41e4))
* 支持公告功能 ([a79fe1f](https://github.com/certd/certd/commit/a79fe1f350f2991af9e5b50825f1776029677fc5))
* 支持webhook触发流水线,新增触发类型图标显示 ([1a29541](https://github.com/certd/certd/commit/1a2954114063a8b994c257a90e5814e0a3a8d924))
* webhook触发器一个流水线限制只能添加一个 ([6c39d7b](https://github.com/certd/certd/commit/6c39d7b1eecb679cb6506b0e3557e8152e01417d))
* zenlayer证书更新 ([9ba6c83](https://github.com/certd/certd/commit/9ba6c838215d0750cda925778a47002a521f05e9))
## [1.37.17](https://github.com/certd/certd/compare/v1.37.16...v1.37.17) (2025-12-29)
### Bug Fixes
Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 305 KiB

+3 -3
View File
@@ -4,9 +4,9 @@
| 序号 | 名称 | 说明 |
|-----|-----|-----|
| 1.| **商用证书托管** | 手动上传自定义证书后,自动部署(每次证书有更新,都需要手动上传一次) |
| 2.| **获取阿里云订阅证书** | 从阿里云拉取订阅模式的商用证书 |
| 3.| **证书申请(JS版)** | 免费通配符域名证书申请,支持多个域名打到同一个证书上 |
| 1.| **证书申请(JS版)** | 免费通配符域名证书申请,支持多个域名打到同一个证书上 |
| 2.| **商用证书托管** | 手动上传自定义证书后,自动部署(每次证书有更新,都需要手动上传一次) |
| 3.| **获取阿里云订阅证书** | 从阿里云拉取订阅模式的商用证书 |
| 4.| **证书申请(Lego** | 支持海量DNS解析提供商,推荐使用,一样的免费通配符域名证书申请,支持多个域名打到同一个证书上 |
## 2. 主机
Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

+19
View File
@@ -0,0 +1,19 @@
# 插件开发
## 插件创建
点击自定义插件按钮,填写插件基本信息
![plugin-create.png](images/plugin-create.png)
创建成功后,会默认打开插件编辑页面,里面默认带有示例代码说明,可以在此基础上进行你的自定义开发
![plugin-edit.png](images/plugin-edit.png)
## 插件测试
在流水线中添加插件任务
![plugin-test.png](images/plugin-test1.png)
配置插件任务参数
![plugin-test.png](images/plugin-test2.png)
点击运行,查看插件任务运行结果
![plugin-test.png](images/plugin-test3.png)
+21 -6
View File
@@ -1,7 +1,15 @@
# 忘记管理员密码
# 忘记密码/无法登录
无法登录的情况:
1、忘记管理员密码
2、仅有第三方登录,但第三方登录失效,导致无法登录
请查看如下方法恢复的登录
## 一、忘记管理员密码
解决方法如下:
## 1. 修改环境变量
### 1. 修改环境变量
docker部署的:
修改docker-compose.yaml文件,将环境变量`certd_system_resetAdminPasswd`改为`true`
@@ -18,21 +26,28 @@ services:
certd_system_resetAdminPasswd=true
```
## 2. 重启容器
### 2. 重启容器
```shell
docker compose up -d
docker logs -f --tail 500 certd
# 观察日志,当日志中输出“重置1号管理员用户密码完成”,即可操作下一步
# 这里会打印1号管理员记录的用户名,如果你修改过管理员用户名,请注意查看此条日志
```
## 3. 恢复环境变量
### 3. 恢复环境变量
修改docker-compose.yaml,将`certd_system_resetAdminPasswd`改回`false`
## 4. 再次重启容器
### 4. 再次重启容器
```shell
docker compose up -d
```
## 5. 默认密码登录
### 5. 默认密码登录
使用`原管理员账号/123456`登录系统,请及时修改管理员密码
> 默认管理员账号: admin
> 如果忘记管理员账号,请查看修改密码时的启动日志,会打印管理员账号名
## 二、仅有第三方登录,没有登录窗口
当开启仅使用第三方登录模式时,如果第三方登录未配置或已失效,则会导致无法登录
您可以通过访问 `http://你的certd地址/#/login?oauthOnly=false` 来临时关闭仅使用第三方登录模式,以使用密码登录。
Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

+2
View File
@@ -0,0 +1,2 @@
# 第三方登录配置
+11
View File
@@ -0,0 +1,11 @@
# 用户有效期功能
可以为用户设置有效期,超过有效期后,用户的流水线将停止运行
## 开启用户有效期功能
![开启用户有效期功能](images/user_valid_enable.png)
## 设置用户有效期
![设置用户有效期](images/user_valid_set.png)
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+28 -1
View File
@@ -67,4 +67,31 @@
![](./images/deploy4.png)
## 6. 配置通知和自动运行
![](./images/notify.png)
![](./images/notify.png)
## 三、 常见问题
### 1. 登录超时 status:ECONNABORTED
如果您的certd部署在群晖里面,可能会遇到登录超时的问题
```
httpRequest:https://dms.xxxxx.com:5001/webapi/entry.cgi, method:get
请求出错: status:ECONNABORTED, statusText:ECONNABORTED
Axio:sError: timeout of 120000ms exceeded
```
可能的原因是是您的dsm域名指向的ip地址在容器内无法访问,导致登录超时
您可以通过配置域名映射来解决
1. 获取群晖dsm内部地址
进入certd后台->系统管理->网络测试, 一般会看到 `172.xx.0.2` ,记住这个xx是多少
![](./images/nettest.png)
2. 修改容器编排 docker-compose.yaml
```
services:
certd:
...
extra_hosts: # 放开这段注释
- "你的dsm域名地址:172.xx.0.1" # 将xx替换成上面记住的数字
```
+1 -1
View File
@@ -9,5 +9,5 @@
}
},
"npmClient": "pnpm",
"version": "1.38.0"
"version": "1.38.1"
}
+6 -4
View File
@@ -18,15 +18,15 @@
"start:server": "cd ./packages/ui/certd-server && npm start",
"devb": "lerna run dev-build",
"i-all": "lerna link && lerna exec npm install ",
"publish": "npm run prepublishOnly2 && lerna publish --force-publish=pro/plus-core --conventional-commits --create-release github && npm run afterpublishOnly ",
"afterpublishOnly": "npm run plugin-doc-gen && npm run copylogs && time /t >trigger/build.trigger && git add ./trigger/build.trigger && git commit -m \"build: trigger build image\" && TIMEOUT /T 10 && npm run commitAll",
"publish": "npm run prepublishOnly2 && lerna publish --force-publish=pro/plus-core --conventional-commits && npm run afterpublishOnly ",
"afterpublishOnly": "npm run copylogs && time /t >trigger/build.trigger && git add ./trigger/build.trigger && git commit -m \"build: trigger build image\" && TIMEOUT /T 10 && npm run commitAll",
"transform-sql": "cd ./packages/ui/certd-server/db/ && node --experimental-json-modules transform.js",
"plugin-doc-gen": "cd ./packages/ui/certd-server/ && npm run export-md",
"plugin-doc-gen": "cd ./packages/ui/certd-server/ && npm run export-metadata",
"commitAll": "git add . && git commit -m \"build: publish\" && git push && npm run commitPro",
"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 ",
"prepublishOnly2": "npm run check && npm run before-build && lerna run build && npm run plugin-doc-gen",
"before-build": "npm run transform-sql && cd ./packages/core/basic && time /t >build.md && git add ./build.md && git commit -m \"build: prepare to build\"",
"deploy1": "node --experimental-json-modules ./scripts/deploy.js ",
"check": "node --experimental-json-modules ./scripts/publish-check.js",
@@ -39,6 +39,8 @@
"dev": "pnpm run -r --parallel compile ",
"release": "time /t >trigger/release.trigger && git add trigger/release.trigger && git commit -m \"build: release\" && git push",
"publish_to_atomgit": "node --experimental-json-modules ./scripts/publish-atomgit.js",
"publish_to_gitee": "node --experimental-json-modules ./scripts/publish-gitee.js",
"publish_to_github": "node --experimental-json-modules ./scripts/publish-github.js",
"get_version": "node --experimental-json-modules ./scripts/version.js"
},
"license": "AGPL-3.0",
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.38.1](https://github.com/publishlab/node-acme-client/compare/v1.38.0...v1.38.1) (2026-01-15)
**Note:** Version bump only for package @certd/acme-client
# [1.38.0](https://github.com/publishlab/node-acme-client/compare/v1.37.17...v1.38.0) (2026-01-13)
**Note:** Version bump only for package @certd/acme-client
+3 -3
View File
@@ -3,7 +3,7 @@
"description": "Simple and unopinionated ACME client",
"private": false,
"author": "nmorsman",
"version": "1.38.0",
"version": "1.38.1",
"type": "module",
"module": "scr/index.js",
"main": "src/index.js",
@@ -18,7 +18,7 @@
"types"
],
"dependencies": {
"@certd/basic": "^1.38.0",
"@certd/basic": "^1.38.1",
"@peculiar/x509": "^1.11.0",
"asn1js": "^3.0.5",
"axios": "^1.9.0",
@@ -70,5 +70,5 @@
"bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues"
},
"gitHead": "786780ce9b0ee9b9ebb104f54abb161ae9a924e9"
"gitHead": "2c80c35b21b3f435e835167fca13db510bbc38a2"
}
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.38.1](https://github.com/certd/certd/compare/v1.38.0...v1.38.1) (2026-01-15)
**Note:** Version bump only for package @certd/basic
# [1.38.0](https://github.com/certd/certd/compare/v1.37.17...v1.38.0) (2026-01-13)
**Note:** Version bump only for package @certd/basic
+1 -1
View File
@@ -1 +1 @@
23:23
00:55
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/basic",
"private": false,
"version": "1.38.0",
"version": "1.38.1",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -47,5 +47,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "786780ce9b0ee9b9ebb104f54abb161ae9a924e9"
"gitHead": "2c80c35b21b3f435e835167fca13db510bbc38a2"
}
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.38.1](https://github.com/certd/certd/compare/v1.38.0...v1.38.1) (2026-01-15)
### Bug Fixes
* 修复自定义插件name丢失author导致找不到插件的bug ([2fbb58e](https://github.com/certd/certd/commit/2fbb58eb2b239eab4864f90aa72b0ef2ada38e8f))
# [1.38.0](https://github.com/certd/certd/compare/v1.37.17...v1.38.0) (2026-01-13)
### Features
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/pipeline",
"private": false,
"version": "1.38.0",
"version": "1.38.1",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -18,8 +18,8 @@
"compile": "tsc --skipLibCheck --watch"
},
"dependencies": {
"@certd/basic": "^1.38.0",
"@certd/plus-core": "^1.38.0",
"@certd/basic": "^1.38.1",
"@certd/plus-core": "^1.38.1",
"dayjs": "^1.11.7",
"lodash-es": "^4.17.21",
"reflect-metadata": "^0.1.13"
@@ -45,5 +45,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "786780ce9b0ee9b9ebb104f54abb161ae9a924e9"
"gitHead": "2c80c35b21b3f435e835167fca13db510bbc38a2"
}
+29 -3
View File
@@ -11,11 +11,11 @@ export type PageSearch = {
// sortOrder?: "asc" | "desc";
};
export type PageRes = {
export type PageRes<T = any> = {
pageNo?: number;
pageSize?: number;
total?: string;
list: any[];
total?: number;
list: T[];
};
export class Pager {
@@ -34,3 +34,29 @@ export class Pager {
this.pageNo = Math.ceil(offset / (this.pageSize ?? 50)) + 1;
}
}
export async function doPageTurn<T>(req: { pager: Pager; getPage: (pager: Pager) => Promise<PageRes<T>>; itemHandle?: (item: T) => Promise<void>; batchHandle?: (pageRes: PageRes<T>) => Promise<void> }) {
let count = 0;
const { pager, getPage, itemHandle, batchHandle } = req;
while (true) {
const pageRes = await getPage(pager);
if (!pageRes || !pageRes.list || pageRes.list.length === 0) {
break;
}
count += pageRes.list.length;
if (batchHandle) {
await batchHandle(pageRes);
}
if (itemHandle) {
for (const item of pageRes.list) {
await itemHandle(item);
}
}
if (pageRes.total && pageRes.total >= 0 && count >= pageRes.total) {
//遍历完成
break;
}
pager.pageNo++;
}
return count;
}
+5 -2
View File
@@ -276,7 +276,10 @@ export class Executor {
const lastStatus = this.lastStatusMap.get(step.id);
//执行任务
const plugin: RegistryItem<AbstractTaskPlugin> = pluginRegistry.get(step.type);
if (!plugin) {
currentLogger.error(`未找到插件${step.type}`);
throw new Error(`未找到插件${step.type}`);
}
//@ts-ignore
let instance: ITaskPlugin = null;
try {
@@ -285,7 +288,7 @@ export class Executor {
//@ts-ignore
instance = new pluginCls();
} catch (e: any) {
currentLogger.error(`实例化插件失败:${e.message}`);
currentLogger.error(`实例化插件失败:${step.type}:${e.message}`);
throw new Error(`实例化插件失败`, e);
}
+12 -1
View File
@@ -22,4 +22,15 @@ const onRegister = ({ key, value }: OnRegisterContext<AbstractTaskPlugin>) => {
}
pluginGroups.other.plugins.push(value.define);
};
export const pluginRegistry = createRegistry<AbstractTaskPlugin>("plugin", onRegister);
const onUnRegister = ({ key }: OnRegisterContext<AbstractTaskPlugin>) => {
for (const group of Object.values(pluginGroups)) {
const index = group.plugins.findIndex(plugin => plugin.name === key);
if (index > -1) {
group.plugins.splice(index, 1);
return;
}
}
};
export const pluginRegistry = createRegistry<AbstractTaskPlugin>("plugin", onRegister, onUnRegister);
@@ -27,10 +27,12 @@ export class Registry<T = any> {
} = {};
onRegister?: OnRegister<T>;
onUnRegister?: OnRegister<T>;
constructor(type: string, onRegister?: OnRegister<T>) {
constructor(type: string, onRegister?: OnRegister<T>, onUnRegister?: OnRegister<T>) {
this.type = type;
this.onRegister = onRegister;
this.onUnRegister = onUnRegister;
}
register(key: string, value: RegistryItem<T>) {
@@ -49,6 +51,13 @@ export class Registry<T = any> {
}
unRegister(key: string) {
if (this.onUnRegister) {
this.onUnRegister({
registry: this,
key,
value: this.storage[key],
});
}
delete this.storage[key];
logger.info(`反注册插件:${this.type}:${key}`);
}
@@ -108,7 +117,7 @@ export class Registry<T = any> {
}
}
export function createRegistry<T>(type: string, onRegister?: OnRegister<T>): Registry<T> {
export function createRegistry<T>(type: string, onRegister?: OnRegister<T>, onUnRegister?: OnRegister<T>): Registry<T> {
const pipelineregistrycacheKey = "PIPELINE_REGISTRY_CACHE";
// @ts-ignore
let cached: any = global[pipelineregistrycacheKey];
@@ -121,7 +130,7 @@ export function createRegistry<T>(type: string, onRegister?: OnRegister<T>): Reg
if (cached[type]) {
return cached[type];
}
const newRegistry = new Registry<T>(type, onRegister);
const newRegistry = new Registry<T>(type, onRegister, onUnRegister);
cached[type] = newRegistry;
return newRegistry;
}
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.38.1](https://github.com/certd/certd/compare/v1.38.0...v1.38.1) (2026-01-15)
**Note:** Version bump only for package @certd/lib-huawei
# [1.38.0](https://github.com/certd/certd/compare/v1.37.17...v1.38.0) (2026-01-13)
**Note:** Version bump only for package @certd/lib-huawei
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-huawei",
"private": false,
"version": "1.38.0",
"version": "1.38.1",
"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": "786780ce9b0ee9b9ebb104f54abb161ae9a924e9"
"gitHead": "2c80c35b21b3f435e835167fca13db510bbc38a2"
}
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.38.1](https://github.com/certd/certd/compare/v1.38.0...v1.38.1) (2026-01-15)
**Note:** Version bump only for package @certd/lib-iframe
# [1.38.0](https://github.com/certd/certd/compare/v1.37.17...v1.38.0) (2026-01-13)
**Note:** Version bump only for package @certd/lib-iframe
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-iframe",
"private": false,
"version": "1.38.0",
"version": "1.38.1",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -31,5 +31,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "786780ce9b0ee9b9ebb104f54abb161ae9a924e9"
"gitHead": "2c80c35b21b3f435e835167fca13db510bbc38a2"
}
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.38.1](https://github.com/certd/certd/compare/v1.38.0...v1.38.1) (2026-01-15)
**Note:** Version bump only for package @certd/jdcloud
# [1.38.0](https://github.com/certd/certd/compare/v1.37.17...v1.38.0) (2026-01-13)
**Note:** Version bump only for package @certd/jdcloud
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/jdcloud",
"version": "1.38.0",
"version": "1.38.1",
"description": "jdcloud openApi sdk",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
@@ -56,5 +56,5 @@
"fetch"
]
},
"gitHead": "786780ce9b0ee9b9ebb104f54abb161ae9a924e9"
"gitHead": "2c80c35b21b3f435e835167fca13db510bbc38a2"
}
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.38.1](https://github.com/certd/certd/compare/v1.38.0...v1.38.1) (2026-01-15)
**Note:** Version bump only for package @certd/lib-k8s
# [1.38.0](https://github.com/certd/certd/compare/v1.37.17...v1.38.0) (2026-01-13)
**Note:** Version bump only for package @certd/lib-k8s
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-k8s",
"private": false,
"version": "1.38.0",
"version": "1.38.1",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -17,7 +17,7 @@
"pub": "npm publish"
},
"dependencies": {
"@certd/basic": "^1.38.0",
"@certd/basic": "^1.38.1",
"@kubernetes/client-node": "0.21.0"
},
"devDependencies": {
@@ -32,5 +32,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "786780ce9b0ee9b9ebb104f54abb161ae9a924e9"
"gitHead": "2c80c35b21b3f435e835167fca13db510bbc38a2"
}
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.38.1](https://github.com/certd/certd/compare/v1.38.0...v1.38.1) (2026-01-15)
**Note:** Version bump only for package @certd/lib-server
# [1.38.0](https://github.com/certd/certd/compare/v1.37.17...v1.38.0) (2026-01-13)
### Features
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/lib-server",
"version": "1.38.0",
"version": "1.38.1",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -28,11 +28,11 @@
],
"license": "AGPL",
"dependencies": {
"@certd/acme-client": "^1.38.0",
"@certd/basic": "^1.38.0",
"@certd/pipeline": "^1.38.0",
"@certd/plugin-lib": "^1.38.0",
"@certd/plus-core": "^1.38.0",
"@certd/acme-client": "^1.38.1",
"@certd/basic": "^1.38.1",
"@certd/pipeline": "^1.38.1",
"@certd/plugin-lib": "^1.38.1",
"@certd/plus-core": "^1.38.1",
"@midwayjs/cache": "3.14.0",
"@midwayjs/core": "3.20.11",
"@midwayjs/i18n": "3.20.13",
@@ -64,5 +64,5 @@
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "786780ce9b0ee9b9ebb104f54abb161ae9a924e9"
"gitHead": "2c80c35b21b3f435e835167fca13db510bbc38a2"
}
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.38.1](https://github.com/certd/certd/compare/v1.38.0...v1.38.1) (2026-01-15)
**Note:** Version bump only for package @certd/midway-flyway-js
# [1.38.0](https://github.com/certd/certd/compare/v1.37.17...v1.38.0) (2026-01-13)
**Note:** Version bump only for package @certd/midway-flyway-js
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/midway-flyway-js",
"version": "1.38.0",
"version": "1.38.1",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -46,5 +46,5 @@
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "786780ce9b0ee9b9ebb104f54abb161ae9a924e9"
"gitHead": "2c80c35b21b3f435e835167fca13db510bbc38a2"
}
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.38.1](https://github.com/certd/certd/compare/v1.38.0...v1.38.1) (2026-01-15)
**Note:** Version bump only for package @certd/plugin-cert
# [1.38.0](https://github.com/certd/certd/compare/v1.37.17...v1.38.0) (2026-01-13)
### Features
+6 -6
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-cert",
"private": false,
"version": "1.38.0",
"version": "1.38.1",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -17,10 +17,10 @@
"compile": "tsc --skipLibCheck --watch"
},
"dependencies": {
"@certd/acme-client": "^1.38.0",
"@certd/basic": "^1.38.0",
"@certd/pipeline": "^1.38.0",
"@certd/plugin-lib": "^1.38.0",
"@certd/acme-client": "^1.38.1",
"@certd/basic": "^1.38.1",
"@certd/pipeline": "^1.38.1",
"@certd/plugin-lib": "^1.38.1",
"psl": "^1.9.0",
"punycode.js": "^2.3.1"
},
@@ -38,5 +38,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "786780ce9b0ee9b9ebb104f54abb161ae9a924e9"
"gitHead": "2c80c35b21b3f435e835167fca13db510bbc38a2"
}
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.38.1](https://github.com/certd/certd/compare/v1.38.0...v1.38.1) (2026-01-15)
### Bug Fixes
* 修复自定义插件name丢失author导致找不到插件的bug ([2fbb58e](https://github.com/certd/certd/commit/2fbb58eb2b239eab4864f90aa72b0ef2ada38e8f))
# [1.38.0](https://github.com/certd/certd/compare/v1.37.17...v1.38.0) (2026-01-13)
### Features
+6 -6
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-lib",
"private": false,
"version": "1.38.0",
"version": "1.38.1",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -22,10 +22,10 @@
"@alicloud/pop-core": "^1.7.10",
"@alicloud/tea-util": "^1.4.11",
"@aws-sdk/client-s3": "^3.964.0",
"@certd/acme-client": "^1.38.0",
"@certd/basic": "^1.38.0",
"@certd/pipeline": "^1.38.0",
"@certd/plus-core": "^1.38.0",
"@certd/acme-client": "^1.38.1",
"@certd/basic": "^1.38.1",
"@certd/pipeline": "^1.38.1",
"@certd/plus-core": "^1.38.1",
"@kubernetes/client-node": "0.21.0",
"ali-oss": "^6.22.0",
"basic-ftp": "^5.0.5",
@@ -57,5 +57,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "786780ce9b0ee9b9ebb104f54abb161ae9a924e9"
"gitHead": "2c80c35b21b3f435e835167fca13db510bbc38a2"
}
@@ -1,5 +1,5 @@
import { HttpClient, ILogger, utils } from "@certd/basic";
import { IAccess, IServiceGetter, Registrable } from "@certd/pipeline";
import { IAccess, IServiceGetter, Pager, PageRes, Registrable } from "@certd/pipeline";
export type DnsProviderDefine = Registrable & {
accessType: string;
@@ -28,6 +28,11 @@ export type DnsProviderContext = {
serviceGetter: IServiceGetter;
};
export type DomainRecord = {
id: string;
domain: string;
};
export interface IDnsProvider<T = any> {
onInstance(): Promise<void>;
@@ -51,6 +56,8 @@ export interface IDnsProvider<T = any> {
//中文域名是否需要punycode转码,如果返回True,则使用punycode来添加解析记录,否则使用中文域名添加解析记录
usePunyCode(): boolean;
getDomainListPage(pager: Pager): Promise<PageRes<DomainRecord>>;
}
export interface ISubDomainsGetter {
@@ -1,4 +1,5 @@
import { CreateRecordOptions, DnsProviderContext, DnsProviderDefine, IDnsProvider, RemoveRecordOptions } from "./api.js";
import { Pager, PageRes } from "@certd/pipeline";
import { CreateRecordOptions, DnsProviderContext, DnsProviderDefine, DomainRecord, IDnsProvider, RemoveRecordOptions } from "./api.js";
import { dnsProviderRegistry } from "./registry.js";
import { HttpClient, ILogger } from "@certd/basic";
import punycode from "punycode.js";
@@ -44,6 +45,10 @@ export abstract class AbstractDnsProvider<T = any> implements IDnsProvider<T> {
abstract onInstance(): Promise<void>;
abstract removeRecord(options: RemoveRecordOptions<T>): Promise<void>;
async getDomainListPage(pager: Pager): Promise<PageRes<DomainRecord>> {
throw new Error("Method not implemented.");
}
}
export async function createDnsProvider(opts: { dnsProviderType: string; context: DnsProviderContext }): Promise<IDnsProvider> {
@@ -4,6 +4,14 @@ import psl from "psl";
import { ILogger, utils, logger as globalLogger } from "@certd/basic";
import { resolveDomainBySoaRecord } from "@certd/acme-client";
export function parseDomainByPsl(fullDomain: string) {
const parsed = psl.parse(fullDomain) as psl.ParsedDomain;
if (parsed.error) {
throw new Error(`解析${fullDomain}域名失败:` + JSON.stringify(parsed.error));
}
return parsed;
}
export class DomainParser implements IDomainParser {
subDomainsGetter: ISubDomainsGetter;
logger: ILogger;
@@ -13,11 +21,7 @@ export class DomainParser implements IDomainParser {
}
parseDomainByPsl(fullDomain: string) {
const parsed = psl.parse(fullDomain) as psl.ParsedDomain;
if (parsed.error) {
throw new Error(`解析${fullDomain}域名失败:` + JSON.stringify(parsed.error));
}
return parsed.domain as string;
return parseDomainByPsl(fullDomain).domain as string;
}
async parse(fullDomain: string) {
@@ -1,17 +0,0 @@
import { AbstractTaskPlugin, TaskInstanceContext } from "@certd/pipeline";
import { isPlus } from "@certd/plus-core";
export function mustPlus() {
if (!isPlus()) {
throw new Error("此插件仅供专业版中使用");
}
}
export abstract class AbstractPlusTaskPlugin extends AbstractTaskPlugin {
setCtx(ctx: TaskInstanceContext) {
super.setCtx(ctx);
mustPlus();
}
abstract execute(): Promise<void>;
}
@@ -1,2 +1 @@
export * from "./ocr-api.js";
export * from "./check.js";
+1 -1
View File
@@ -53,7 +53,7 @@ RUN ARCH=$(uname -m) && \
ENV TZ=Asia/Shanghai
ENV NODE_ENV=production
ENV MIDWAY_SERVER_ENV=production
CMD ["npm", "run","start"]
CMD ["node", "--optimize-for-size", "./bootstrap.js"]
+1 -1
View File
@@ -3,7 +3,7 @@ VITE_APP_API=api
VITE_APP_PM_ENABLED=true
VITE_APP_TITLE=Certd
VITE_APP_SLOGAN=让你的证书永不过期
VITE_APP_COPYRIGHT_YEAR=2021-2025
VITE_APP_COPYRIGHT_YEAR=2021-2026
VITE_APP_COPYRIGHT_NAME=handsfree.work
VITE_APP_COPYRIGHT_URL=https://certd.handsfree.work
VITE_APP_LOGO=static/images/logo/logo.svg
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.38.1](https://github.com/certd/certd/compare/v1.38.0...v1.38.1) (2026-01-15)
**Note:** Version bump only for package @certd/ui-client
# [1.38.0](https://github.com/certd/certd/compare/v1.37.17...v1.38.0) (2026-01-13)
### Bug Fixes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/ui-client",
"version": "1.38.0",
"version": "1.38.1",
"private": true,
"scripts": {
"dev": "vite --open",
@@ -106,8 +106,8 @@
"zod-defaults": "^0.1.3"
},
"devDependencies": {
"@certd/lib-iframe": "^1.38.0",
"@certd/pipeline": "^1.38.0",
"@certd/lib-iframe": "^1.38.1",
"@certd/pipeline": "^1.38.1",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12",
@@ -1,5 +1,5 @@
<template>
<a-select>
<a-select :value="value" @update:value="onChange">
<a-select-option v-for="item of options" :key="item.value" :value="item.value" :label="item.label">
<span class="flex-o">
<fs-icon :icon="item.icon" class="fs-16 color-blue mr-5" />
@@ -12,5 +12,11 @@
<script lang="ts" setup>
const props = defineProps<{
options: { value: any; label: string; icon: string }[];
value: any;
}>();
const emit = defineEmits(["update:value"]);
function onChange(value: any) {
emit("update:value", value);
}
</script>
@@ -1,5 +1,5 @@
<template>
<icon-select class="dns-provider-selector" :value="modelValue" :options="options" @update:value="onChanged"> </icon-select>
<icon-select class="dns-provider-selector" :value="modelValue" :options="options" @update:value="atChange"> </icon-select>
</template>
<script lang="ts">
@@ -37,7 +37,7 @@ export default {
}
onCreate();
function onChanged(value: any) {
function atChange(value: any) {
ctx.emit("update:modelValue", value);
onSelectedChange(value);
}
@@ -52,7 +52,7 @@ export default {
}
return {
options,
onChanged,
atChange,
};
},
};
@@ -28,4 +28,10 @@ export const Dicts = {
{ label: "SSH(已废弃,请选择SFTP方式)", value: "ssh", disabled: true },
],
}),
domainFromTypeDict: dict({
data: [
{ value: "manual", label: "手动" },
{ value: "auto", label: "自动" },
],
}),
};
@@ -13,6 +13,13 @@ export default {
title: "Operation",
},
},
pipelinePage: {
addMore: "Add More Pipelines",
aliyunSubscriptionPipeline: "Aliyun Subscription Pipeline",
legoCertPipeline: "Lego Certificate Pipeline",
customPipeline: "Custom Pipeline",
batchAddPipeline: "Add Pipeline Use Template",
},
order: {
confirmTitle: "Order Confirmation",
package: "Package",
@@ -36,6 +43,7 @@ export default {
title: "Framework",
home: "Home",
},
helpDocLink: "Help Docs",
title: "Certificate Automation",
pipeline: "Pipeline",
pipelineEdit: "Edit Pipeline",
@@ -721,6 +729,7 @@ export default {
cnameDomainPlaceholder: "cname.handsfree.work",
cnameDomainHelper:
"Requires a domain registered with a DNS provider on the right (or you can transfer other domain DNS servers here).\nOnce the CNAME domain is set, it cannot be changed. It is recommended to use a first-level subdomain.",
cnameDomainPattern: "Domain name cannot contain *",
dnsProvider: "DNS Provider",
dnsProviderAuthorization: "DNS Provider Authorization",
setDefault: "Set Default",
@@ -831,6 +840,8 @@ export default {
disabled: "Disabled",
challengeSetting: "Challenge Setting",
gotoCnameTip: "Please go to CNAME Record Page",
fromType: "From Type",
expirationDate: "Expiration Date",
},
addonSelector: {
select: "Select",
@@ -17,6 +17,13 @@ export default {
title: "操作列",
},
},
pipelinePage: {
addMore: "添加更多流水线",
aliyunSubscriptionPipeline: "阿里云订阅流水线",
legoCertPipeline: "Lego证书流水线",
customPipeline: "自定义流水线",
batchAddPipeline: "模版批量创建流水线",
},
order: {
confirmTitle: "订单确认",
package: "套餐",
@@ -40,7 +47,7 @@ export default {
title: "框架",
home: "首页",
},
helpDocLink: "帮助文档",
title: "证书自动化",
pipeline: "证书自动化流水线",
pipelineEdit: "编辑流水线",
@@ -729,6 +736,7 @@ export default {
cnameDomain: "CNAME域名",
cnameDomainPlaceholder: "cname.handsfree.work",
cnameDomainHelper: "需要一个右边DNS提供商注册的域名(也可以将其他域名的dns服务器转移到这几家来)。\nCNAME域名一旦确定不可修改,建议使用一级子域名",
cnameDomainPattern: "域名不能使用星号",
dnsProvider: "DNS提供商",
dnsProviderAuthorization: "DNS提供商授权",
setDefault: "设置默认",
@@ -810,7 +818,7 @@ export default {
oauthAutoRedirect: "自动跳转第三方登录",
oauthAutoRedirectHelper: "是否自动跳转第三方登录(使用第一个已启用的第三方登录类型)",
oauthOnly: "仅使用第三方登录",
oauthOnlyHelper: "是否仅使用第三方登录,关闭密码登录(注意:请务必在测试第三方登录功能正常后再开启",
oauthOnlyHelper: "是否仅使用第三方登录,关闭密码登录(注意:请务必在测试第三方登录功能正常后再开启,否则会导致无法登录)\n 如果无法登录,请访问 http://你的certd地址/#/login?oauthOnly=false 来临时关闭此模式",
email: {
templates: "邮件模板",
@@ -845,6 +853,8 @@ export default {
disabled: "禁用/启用",
challengeSetting: "校验配置",
gotoCnameTip: "CNAME域名配置请前往CNAME记录页面添加",
fromType: "来源类型",
expirationDate: "到期时间",
},
addonSelector: {
select: "选择",
@@ -0,0 +1,38 @@
import { useFormWrapper } from "@fast-crud/fast-crud";
export type FormOptionReq = {
title: string;
columns: any;
onSubmit?: any;
};
export function useFormDialog() {
const { openCrudFormDialog } = useFormWrapper();
async function openFormDialog(req: FormOptionReq) {
function createCrudOptions() {
return {
crudOptions: {
columns: req.columns,
form: {
wrapper: {
title: req.title,
saveRemind: false,
},
async afterSubmit() {},
async doSubmit({ form }: any) {
if (req.onSubmit) {
await req.onSubmit(form);
}
},
},
},
};
}
const { crudOptions } = createCrudOptions();
await openCrudFormDialog({ crudOptions });
}
return {
openFormDialog,
};
}
@@ -72,7 +72,7 @@ export default defineComponent({
}
async function emitValue(value) {
if (pipeline?.value && target?.value && pipeline.value.userId !== target.value.userId) {
if (pipeline && pipeline?.value && target?.value && pipeline.value.userId !== target.value.userId) {
message.error("对不起,您不能修改他人流水线的授权");
return;
}
@@ -57,3 +57,18 @@ export async function DeleteBatch(ids: any[]) {
data: { ids },
});
}
export async function SyncSubmit(body: any) {
return await request({
url: apiPrefix + "/sync/import",
method: "post",
data: body,
});
}
export async function SyncDomainsExpiration() {
return await request({
url: apiPrefix + "/sync/expiration",
method: "post",
});
}
@@ -7,7 +7,8 @@ import { useUserStore } from "/@/store/user";
import { useSettingStore } from "/@/store/settings";
import { Dicts } from "/@/components/plugins/lib/dicts";
import { createAccessApi } from "/@/views/certd/access/api";
import { Modal } from "ant-design-vue";
import { Modal, notification } from "ant-design-vue";
import { useDomainImport } from "./use";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter();
@@ -49,6 +50,8 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const dnsProviderTypeDict = dict({
url: "pi/dnsProvider/dnsProviderTypeDict",
});
const openDomainImportDialog = useDomainImport();
return {
crudOptions: {
settings: {
@@ -88,6 +91,45 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
}
},
},
actionbar: {
buttons: {
add: {
icon: "ion:add-circle-outline",
},
import: {
title: "从域名提供商导入域名",
type: "primary",
text: "从域名提供商导入",
needPlus: true,
color: "gold",
icon: "mingcute:vip-1-line",
click: () => {
openDomainImportDialog({
afterSubmit: () => {
setTimeout(() => {
crudExpose.doRefresh();
}, 2000);
},
});
},
},
syncExpirationDate: {
title: "同步域名过期时间",
type: "primary",
icon: "ion:refresh-outline",
text: "同步域名过期时间",
click: async () => {
await api.SyncDomainsExpiration();
notification.success({
message: "同步任务已提交",
});
setTimeout(() => {
crudExpose.doRefresh();
}, 2000);
},
},
},
},
columns: {
id: {
title: "ID",
@@ -119,6 +161,13 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
sorter: true,
},
},
expirationDate: {
title: t("certd.domain.expirationDate"),
type: "date",
form: {
show: false,
},
},
challengeType: {
title: t("certd.domain.challengeType"),
type: "dict-select",
@@ -285,6 +334,16 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
},
},
fromType: {
title: t("certd.domain.fromType"),
type: "dict-select",
dict: Dicts.domainFromTypeDict,
column: {
component: {
color: "auto",
},
},
},
disabled: {
title: t("certd.domain.disabled"),
type: "dict-switch",
@@ -0,0 +1,67 @@
import { message } from "ant-design-vue";
import * as api from "./api";
import { useFormDialog } from "/@/use/use-dialog";
import { compute } from "@fast-crud/fast-crud";
export function useDomainImport() {
const { openFormDialog } = useFormDialog();
const columns = {
dnsProviderType: {
title: "域名提供商",
type: "text",
form: {
component: {
name: "dns-provider-selector",
},
on: {
//@ts-ignore
onSelectedChange: ({ form, $event }) => {
form.dnsProviderAccessType = $event.accessType;
},
},
//@ts-ignore
valueChange({ form }) {
form.dnsProviderAccessId = null;
},
},
},
dnsProviderAccessType: {
title: "域名提供商访问类型",
type: "text",
form: {
show: false,
},
},
dnsProviderAccessId: {
title: "域名提供商授权",
type: "text",
form: {
component: {
name: "access-selector",
vModel: "modelValue",
type: compute(({ form }) => {
return form.dnsProviderAccessType || form.dnsProviderType;
}),
},
},
},
};
return function openDomainImportDialog(req: { afterSubmit?: () => void }) {
openFormDialog({
title: "从域名提供商导入域名",
columns: columns,
onSubmit: async (form: any) => {
await api.SyncSubmit({
dnsProviderType: form.dnsProviderType,
dnsProviderAccessId: form.dnsProviderAccessId,
});
message.success("导入任务已提交");
if (req.afterSubmit) {
req.afterSubmit();
}
},
});
};
}
@@ -77,3 +77,11 @@ export async function ResetStatus(id: number) {
},
});
}
export async function Import(form: { domainList: string; cnameProviderId: any }) {
return await request({
url: apiPrefix + "/import",
method: "post",
data: form,
});
}
@@ -7,7 +7,9 @@ import { useUserStore } from "/@/store/user";
import { useSettingStore } from "/@/store/settings";
import { message, Modal } from "ant-design-vue";
import CnameTip from "/@/components/plugins/cert/domains-verify-plan-editor/cname-tip.vue";
import { useCnameImport } from "./use";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const crudBinding = crudExpose.crudBinding;
const router = useRouter();
const { t } = useI18n();
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
@@ -27,10 +29,13 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
return res;
};
const openCnameImportDialog = useCnameImport();
const userStore = useUserStore();
const settingStore = useSettingStore();
const selectedRowKeys: Ref<any[]> = ref([]);
context.selectedRowKeys = selectedRowKeys;
const dictRef = dict({
data: [
{ label: t("certd.pending_cname_setup"), value: "cname", color: "warning" },
@@ -64,6 +69,32 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
editRequest,
delRequest,
},
actionbar: {
buttons: {
import: {
title: "导入CNAME记录",
type: "primary",
text: "批量导入",
click: () => {
openCnameImportDialog({
afterSubmit: () => {
setTimeout(() => {
crudExpose?.doRefresh();
}, 2000);
},
});
},
},
export: {
title: "导出CNAME记录之后,可用于批量导入cname解析到域名注册商",
type: "primary",
text: "批量导出",
click: () => {
crudBinding.value.toolbar.buttons.export.click({});
},
},
},
},
tabs: {
name: "status",
show: true,
@@ -0,0 +1,56 @@
import { dict } from "@fast-crud/fast-crud";
import { message } from "ant-design-vue";
import * as api from "./api";
import { useFormDialog } from "/@/use/use-dialog";
export const cnameProviderDict = dict({
url: "/cname/provider/list",
value: "id",
label: "domain",
});
export function useCnameImport() {
const { openFormDialog } = useFormDialog();
const columns = {
domainList: {
title: "域名列表",
type: "text",
form: {
component: {
name: "a-textarea",
rows: 5,
},
col: {
span: 24,
},
required: true,
helper: "每个域名一行,批量导入\n泛域名请去掉*.\n已经存在的会自动跳过",
},
},
cnameProviderId: {
title: "CNAME服务",
type: "dict-select",
dict: cnameProviderDict,
form: {
required: true,
},
},
};
return function openCnameImportDialog(req: { afterSubmit?: () => void }) {
openFormDialog({
title: "导入CNAME记录",
columns: columns,
onSubmit: async (form: any) => {
await api.Import({
domainList: form.domainList,
cnameProviderId: form.cnameProviderId,
});
message.success("导入任务已提交");
if (req.afterSubmit) {
req.afterSubmit();
}
},
});
};
}
@@ -85,46 +85,23 @@ export function useCertPipelineCreator() {
const settingStore = useSettingStore();
const router = useRouter();
function createCrudOptions(certPlugins: any[], getFormData: any, doSubmit: any): CreateCrudOptionsRet {
function createCrudOptions(req: { certPlugin: any; doSubmit: any; title?: string }): CreateCrudOptionsRet {
const inputs: any = {};
const moreParams = [];
for (const plugin of certPlugins) {
for (const inputKey in plugin.input) {
if (inputs[inputKey]) {
//如果两个插件有的字段,直接显示
inputs[inputKey].form.show = true;
continue;
}
const inputDefine = cloneDeep(plugin.input[inputKey]);
if (!inputDefine.required && !inputDefine.maybeNeed) {
moreParams.push(inputKey);
// continue;
}
useReference(inputDefine);
inputs[inputKey] = {
title: inputDefine.title,
form: {
...inputDefine,
show: compute(ctx => {
const form = getFormData();
if (!form) {
return false;
}
let inputDefineShow = true;
if (inputDefine.show != null) {
const computeShow = inputDefine.show as any;
if (computeShow === false) {
inputDefineShow = false;
} else if (computeShow && computeShow.computeFn) {
inputDefineShow = computeShow.computeFn({ form });
}
}
return form?.certApplyPlugin === plugin.name && inputDefineShow;
}),
},
};
const doSubmit = req.doSubmit;
for (const inputKey in req.certPlugin.input) {
// inputs[inputKey].form.show = true;
const inputDefine = cloneDeep(req.certPlugin.input[inputKey]);
if (inputDefine.maybeNeed) {
moreParams.push(inputKey);
}
useReference(inputDefine);
inputs[inputKey] = {
title: inputDefine.title,
form: {
...inputDefine,
},
};
}
const pluginStore = usePluginStore();
@@ -146,7 +123,7 @@ export function useCertPipelineCreator() {
wrapClassName: "cert_pipeline_create_form",
width: 1350,
saveRemind: false,
title: t("certd.pipelineForm.createTitle"),
title: req.title || t("certd.pipelineForm.createTitle"),
},
group: {
groups: {
@@ -159,44 +136,44 @@ export function useCertPipelineCreator() {
},
},
columns: {
certApplyPlugin: {
title: t("certd.plugin.selectTitle"),
type: "dict-select",
dict: dict({
data: [
{ value: "CertApply", label: "JS-ACME" },
{ value: "CertApplyLego", label: "Lego-ACME" },
{ value: "CertApplyGetFormAliyun", label: "Aliyun-Order" },
],
}),
form: {
order: 0,
value: "CertApply",
helper: {
render: () => {
return (
<ul>
<li>{t("certd.plugin.jsAcme")}</li>
<li>{t("certd.plugin.legoAcme")}</li>
<li>{t("certd.plugin.aliyunOrder")}</li>
</ul>
);
},
},
valueChange: {
handle: async ({ form, value }) => {
const config = await pluginStore.getPluginConfig({
name: value,
type: "builtIn",
});
if (config.sysSetting?.input) {
merge(form, config.sysSetting.input);
}
},
immediate: true,
},
},
},
// certApplyPlugin: {
// title: t("certd.plugin.selectTitle"),
// type: "dict-select",
// dict: dict({
// data: [
// { value: "CertApply", label: "JS-ACME" },
// { value: "CertApplyLego", label: "Lego-ACME" },
// { value: "CertApplyGetFormAliyun", label: "Aliyun-Order" },
// ],
// }),
// form: {
// order: 0,
// value: "CertApply",
// helper: {
// render: () => {
// return (
// <ul>
// <li>{t("certd.plugin.jsAcme")}</li>
// <li>{t("certd.plugin.legoAcme")}</li>
// <li>{t("certd.plugin.aliyunOrder")}</li>
// </ul>
// );
// },
// },
// valueChange: {
// handle: async ({ form, value }) => {
// const config = await pluginStore.getPluginConfig({
// name: value,
// type: "builtIn",
// });
// if (config.sysSetting?.input) {
// merge(form, config.sysSetting.input);
// }
// },
// immediate: true,
// },
// },
// },
...inputs,
triggerCron: {
title: t("certd.pipelineForm.triggerCronTitle"),
@@ -336,18 +313,10 @@ export function useCertPipelineCreator() {
return certPlugins;
}
async function openAddCertdPipelineDialog(req: { defaultGroupId?: number }) {
async function openAddCertdPipelineDialog(req: { pluginName: string; defaultGroupId?: number; title?: string }) {
//检查是否流水线数量超出限制
await checkPipelineLimit();
const wrapperRef = ref();
function getFormData() {
if (!wrapperRef.value) {
return null;
}
return wrapperRef.value.getFormData();
}
async function doSubmit({ form }: any) {
// const certDetail = readCertDetail(form.cert.crt);
// 添加certd pipeline
@@ -375,7 +344,7 @@ export function useCertPipelineCreator() {
strategy: {
runStrategy: 0, // 正常执行
},
type: form.certApplyPlugin,
type: req.pluginName,
},
],
},
@@ -410,11 +379,19 @@ export function useCertPipelineCreator() {
router.push({ path: "/certd/pipeline/detail", query: { id, editMode: "true" } });
}
const certPlugins = await getCertPlugins();
const { crudOptions } = createCrudOptions(certPlugins, getFormData, doSubmit);
const certPlugin = certPlugins.find(plugin => plugin.name === req.pluginName);
if (!certPlugin) {
message.error("该证书申请插件不存在");
return;
}
const { crudOptions } = createCrudOptions({
certPlugin,
doSubmit,
title: req.title,
});
//@ts-ignore
crudOptions.columns.groupId.form.value = req.defaultGroupId || undefined;
const wrapper = await openCrudFormDialog({ crudOptions });
wrapperRef.value = wrapper;
await openCrudFormDialog({ crudOptions });
}
return {
@@ -1,29 +1,26 @@
import * as api from "./api";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes, useUi } from "@fast-crud/fast-crud";
import { Modal, notification } from "ant-design-vue";
import dayjs from "dayjs";
import { computed, ref } from "vue";
import { useRouter } from "vue-router";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes, useUi } from "@fast-crud/fast-crud";
import { statusUtil } from "/@/views/certd/pipeline/pipeline/utils/util.status";
import { Modal, notification } from "ant-design-vue";
import { useUserStore } from "/@/store/user";
import dayjs from "dayjs";
import * as api from "./api";
import { GetDetail } from "./api";
import { groupDictRef } from "./group/dicts";
import { useSettingStore } from "/@/store/settings";
import { cloneDeep } from "lodash-es";
import { eachStages } from "./utils";
import { setRunnableIds, useCertPipelineCreator } from "/@/views/certd/pipeline/certd-form/use";
import { useUserStore } from "/@/store/user";
import { useCertUpload } from "/@/views/certd/pipeline/cert-upload/use";
import { setRunnableIds } from "/@/views/certd/pipeline/certd-form/use";
import GroupSelector from "/@/views/certd/pipeline/group/group-selector.vue";
import { statusUtil } from "/@/views/certd/pipeline/pipeline/utils/util.status";
import { useCertViewer } from "/@/views/certd/pipeline/use";
import { useI18n } from "/src/locales";
import { GetDetail, GetObj } from "./api";
import { groupDictRef } from "./group/dicts";
export default function ({ crudExpose, context: { selectedRowKeys } }: CreateCrudOptionsProps): CreateCrudOptionsRet {
export default function ({ crudExpose, context: { selectedRowKeys, openCertApplyDialog } }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter();
const lastResRef = ref();
const { t } = useI18n();
const { openAddCertdPipelineDialog } = useCertPipelineCreator();
const { openUploadCreateDialog } = useCertUpload();
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
@@ -51,7 +48,10 @@ export default function ({ crudExpose, context: { selectedRowKeys } }: CreateCru
delete form.lastVars;
delete form.createTime;
delete form.id;
let pipeline = JSON.parse(form.content);
let pipeline = form.content;
if (typeof pipeline === "string" && pipeline.startsWith("{")) {
pipeline = JSON.parse(form.content);
}
pipeline.title = form.title;
pipeline = setRunnableIds(pipeline);
form.content = JSON.stringify(pipeline);
@@ -105,7 +105,8 @@ export default function ({ crudExpose, context: { selectedRowKeys } }: CreateCru
actionbar: {
buttons: {
add: {
order: 5,
order: 99,
show: false,
icon: "ion:ios-add-circle-outline",
text: t("certd.customPipeline"),
},
@@ -115,9 +116,7 @@ export default function ({ crudExpose, context: { selectedRowKeys } }: CreateCru
type: "primary",
icon: "ion:ios-add-circle-outline",
click() {
const searchForm = crudExpose.getSearchValidatedFormData();
const defaultGroupId = searchForm.groupId;
openAddCertdPipelineDialog({ defaultGroupId });
openCertApplyDialog({ key: "CertApply" });
},
},
uploadCert: {
@@ -9,6 +9,28 @@
</template>
</a-alert> -->
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #actionbar-right>
<a-dropdown class="ml-1">
<a-button type="primary" class="ant-dropdown-link" @click.prevent>
{{ t("certd.pipelinePage.addMore") }}
<DownOutlined />
</a-button>
<template #overlay>
<a-menu @click="onActionbarMoreItemClick">
<!-- <a-menu-item key="CertApplyUpload" class="flex items-center">
<fs-icon icon="ion:business-outline" />
商用证书托管流水线
</a-menu-item> -->
<a-menu-item v-for="item in addMorePipelineBtns" :key="item.key" :title="item.title">
<div class="flex items-center">
<fs-icon :icon="item.icon" />
<span class="ml-2">{{ item.title }}</span>
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
<div v-if="selectedRowKeys.length > 0" class="batch-actions">
<div class="batch-actions-inner">
<span>{{ t("certd.selectedCount", { count: selectedRowKeys.length }) }}</span>
@@ -19,7 +41,6 @@
<change-trigger :selected-row-keys="selectedRowKeys" @change="batchFinished"></change-trigger>
</div>
</div>
<template #actionbar-right> </template>
<template #form-bottom>
<div>{{ t("certd.applyCertificate") }}</div>
</template>
@@ -28,7 +49,7 @@
</template>
<script lang="ts" setup>
import { onActivated, onMounted, ref } from "vue";
import { computed, onActivated, onMounted, ref } from "vue";
import { dict, useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import ChangeGroup from "./components/change-group.vue";
@@ -42,6 +63,8 @@ const { t } = useI18n();
import ChangeNotification from "/@/views/certd/pipeline/components/change-notification.vue";
import { useSettingStore } from "/@/store/settings";
import { groupDictRef } from "./group/dicts";
import { useCertPipelineCreator } from "./certd-form/use";
import { useRouter } from "vue-router";
defineOptions({
name: "PipelineManager",
@@ -51,6 +74,36 @@ const selectedRowKeys = ref([]);
const context: any = {
selectedRowKeys,
};
const router = useRouter();
const { openAddCertdPipelineDialog } = useCertPipelineCreator();
function onActionbarMoreItemClick(req: { key: string; item: any }) {
openCertApplyDialog({ key: req.key, title: req.item?.title });
}
const addMorePipelineBtns = computed(() => {
return [
{ key: "CertApplyGetFormAliyun", title: t("certd.pipelinePage.aliyunSubscriptionPipeline"), icon: "svg:icon-aliyun" },
{ key: "CertApplyLego", title: t("certd.pipelinePage.legoCertPipeline"), icon: "cbi:lego" },
{ key: "AddPipeline", title: t("certd.pipelinePage.customPipeline"), icon: "ion:add-circle-outline" },
{ key: "BatchAddPipeline", title: t("certd.pipelinePage.batchAddPipeline"), icon: "ion:duplicate" },
];
});
function openCertApplyDialog(req: { key: string; title: string }) {
if (req.key === "AddPipeline") {
crudExpose.openAdd({});
return;
}
if (req.key === "BatchAddPipeline") {
router.push({ path: "/certd/pipeline/template" });
return;
}
const searchForm = crudExpose.getSearchValidatedFormData();
const defaultGroupId = searchForm.groupId;
openAddCertdPipelineDialog({ pluginName: req.key, defaultGroupId, title: req.title });
}
context.openCertApplyDialog = openCertApplyDialog;
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context });
//
@@ -992,7 +992,7 @@ export default defineComponent({
const { viewCert, downloadCert } = useCertViewer();
const isCert = computed(() => {
return currentPipeline.value?.type?.startsWith("cert");
return currentPipeline.value?.type?.startsWith("cert") || pipelineDetail.value.lastVars?.certExpiresTime;
});
const hasWebhookTrigger = computed(() => {
@@ -88,7 +88,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
placeholder: t("certd.cnameDomainPlaceholder"),
},
helper: t("certd.cnameDomainHelper"),
rules: [{ required: true, message: t("certd.requiredField") }],
rules: [
{ required: true, message: t("certd.requiredField") },
{ pattern: /^[^*]+$/, message: t("certd.cnameDomainPattern") },
],
},
column: {
width: 200,
@@ -228,7 +228,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
rules: [
{ required: true },
{
type: "regexp",
type: "pattern",
pattern: /^[a-zA-Z][a-zA-Z0-9]+$/,
message: t("certd.pluginNameRuleMsg"),
},
@@ -257,7 +257,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
rules: [
{ required: true },
{
type: "regexp",
type: "pattern",
pattern: /^[a-zA-Z][a-zA-Z0-9]+$/,
message: t("certd.authorRuleMsg"),
},
@@ -34,7 +34,7 @@
<a-select-option value="ipv4first">{{ t("certd.ipv4Priority") }}</a-select-option>
<a-select-option value="ipv6first">{{ t("certd.ipv6Priority") }}</a-select-option>
</a-select>
<div class="helper">{{ t("certd.dualStackNetworkHelper") }}</div>
<div class="helper">{{ t("certd.dualStackNetworkHelper") }}, <a href="https://certd.docmirror.cn/guide/use/setting/ipv6.html" target="_blank">{{ t("certd.helpDocLink") }}</a></div>
</a-form-item>
<a-form-item :label="t('certd.sys.setting.showRunStrategy')" :name="['public', 'showRunStrategy']">
@@ -21,7 +21,10 @@
<a-switch v-model:checked="formState.public.certDomainAddToMonitorEnabled" :disabled="!settingsStore.isPlus" />
<vip-button class="ml-5" mode="button"></vip-button>
</div>
<div class="helper">{{ t("certd.sys.setting.certDomainAddToMonitorEnabledHelper") }}</div>
<div class="helper">
{{ t("certd.sys.setting.certDomainAddToMonitorEnabledHelper") }}
<a href="https://certd.docmirror.cn/guide/use/setting/user-valid.html" target="_blank">{{ t("certd.helpDocLink") }}</a>
</div>
</a-form-item>
<a-form-item :label="t('certd.sys.setting.fixedCertExpireDays')" :name="['public', 'fixedCertExpireDays']">
@@ -12,7 +12,10 @@
<a-switch v-model:checked="formState.public.userValidTimeEnabled" :disabled="!settingsStore.isPlus" />
<vip-button class="ml-5" mode="button"></vip-button>
</div>
<div class="helper">{{ t("certd.userValidityPeriodHelper") }}</div>
<div class="helper">
{{ t("certd.userValidityPeriodHelper") }}
<a href="https://certd.docmirror.cn/guide/use/setting/user-valid.html" target="_blank">{{ t("certd.helpDocLink") }}</a>
</div>
</a-form-item>
<template v-if="formState.public.registerEnabled">
<a-form-item :label="t('certd.enableUsernameRegistration')" :name="['public', 'usernameRegisterEnabled']">
+1 -1
View File
@@ -1,2 +1,2 @@
LEGO_VERSION=4.30.1
certd_plugin_loadmode=metadata
certd_plugin_loadmode=dev
+10
View File
@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.38.1](https://github.com/certd/certd/compare/v1.38.0...v1.38.1) (2026-01-15)
### Bug Fixes
* 修复自定义插件name丢失author导致找不到插件的bug ([2fbb58e](https://github.com/certd/certd/commit/2fbb58eb2b239eab4864f90aa72b0ef2ada38e8f))
### Performance Improvements
* 自定义插件支持使用_ctx.import("/@/xxx.js")以绝对路径引用模块 ([9eace86](https://github.com/certd/certd/commit/9eace86aeeb48c23b55102fc5d42088294d9eb97))
# [1.38.0](https://github.com/certd/certd/compare/v1.37.17...v1.38.0) (2026-01-13)
### Bug Fixes
@@ -0,0 +1,3 @@
ALTER TABLE cd_domain ADD COLUMN from_type varchar(20);
ALTER TABLE cd_domain ADD COLUMN registration_date integer;
ALTER TABLE cd_domain ADD COLUMN expiration_date integer;
+12 -1
View File
@@ -80,6 +80,9 @@ async function genMetadata(){
for (const key in modules) {
const module = modules[key]
const entry = Object.entries(module)
if (entry.length >1) {
console.log(`[warning] 文件 ${key} 导出了 ${entry.length} 个对象: ${entry.map(([name, value]) => name).join(", ")}`)
}
for (const [name, value] of entry) {
//如果有define属性
if(value.define){
@@ -229,8 +232,16 @@ table th:nth-of-type(2) {
// setTimeout(() => why(), 100); // 延迟打印原因
async function main(){
await genMetadata()
await genPluginMd()
console.log("genMetadata success")
// 获取args genmd
const args = process.argv.slice(2)
if(!args.includes("docoff")){
await genPluginMd()
console.log("genPluginMd success")
}
process.exit()
}
main()
@@ -440,4 +440,4 @@ output:
type: certZip
pluginType: deploy
type: builtIn
scriptFilePath: /plugins/plugin-cert/plugin/cert-plugin/index.js
scriptFilePath: /plugins/plugin-cert/plugin/cert-plugin/apply.js
+18 -16
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/ui-server",
"version": "1.38.0",
"version": "1.38.1",
"description": "fast-server base midway",
"private": true,
"type": "module",
@@ -23,10 +23,12 @@
"lint": "mwts check",
"lint:fix": "mwts fix",
"ci": "npm run cov",
"build": "cross-env NODE_ENV=production mwtsc --cleanOutDir --skipLibCheck && npm run export-metadata",
"build-only": "cross-env NODE_ENV=production mwtsc --cleanOutDir --skipLibCheck",
"build": "npm run build-only && npm run export-metadata",
"export-metadata": "node export-plugin-yaml.js",
"export-metadata-only": "node export-plugin-yaml.js docoff",
"dev-build": "echo 1",
"build-on-docker": "node ./before-build.js && npm run build",
"build-on-docker": "node ./before-build.js && npm run build-only && npm run export-metadata-only",
"up-mw-deps": "npx midway-version -u -w",
"heap": "cross-env NODE_ENV=production clinic heapprofiler -- node --optimize-for-size --inspect ./bootstrap.js",
"flame": "clinic flame -- node ./bootstrap.js",
@@ -46,20 +48,20 @@
"@aws-sdk/client-iam": "^3.964.0",
"@aws-sdk/client-route-53": "^3.964.0",
"@aws-sdk/client-s3": "^3.964.0",
"@certd/acme-client": "^1.38.0",
"@certd/basic": "^1.38.0",
"@certd/commercial-core": "^1.38.0",
"@certd/acme-client": "^1.38.1",
"@certd/basic": "^1.38.1",
"@certd/commercial-core": "^1.38.1",
"@certd/cv4pve-api-javascript": "^8.4.2",
"@certd/jdcloud": "^1.38.0",
"@certd/lib-huawei": "^1.38.0",
"@certd/lib-k8s": "^1.38.0",
"@certd/lib-server": "^1.38.0",
"@certd/midway-flyway-js": "^1.38.0",
"@certd/pipeline": "^1.38.0",
"@certd/plugin-cert": "^1.38.0",
"@certd/plugin-lib": "^1.38.0",
"@certd/plugin-plus": "^1.38.0",
"@certd/plus-core": "^1.38.0",
"@certd/jdcloud": "^1.38.1",
"@certd/lib-huawei": "^1.38.1",
"@certd/lib-k8s": "^1.38.1",
"@certd/lib-server": "^1.38.1",
"@certd/midway-flyway-js": "^1.38.1",
"@certd/pipeline": "^1.38.1",
"@certd/plugin-cert": "^1.38.1",
"@certd/plugin-lib": "^1.38.1",
"@certd/plugin-plus": "^1.38.1",
"@certd/plus-core": "^1.38.1",
"@google-cloud/publicca": "^1.3.0",
"@huaweicloud/huaweicloud-sdk-cdn": "^3.1.120",
"@huaweicloud/huaweicloud-sdk-core": "^3.1.120",
@@ -1,53 +0,0 @@
// 扫描目录,列出文件,然后加载为模块
import { join } from 'path';
import fs from 'fs'
import { pathToFileURL } from "node:url";
import path from 'path'
function scanDir(dir) {
const files = fs.readdirSync(dir);
const result = [];
// 扫描目录及子目录
for (const file of files) {
if (file.includes("index.js")) {
continue;
}
const filePath = join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
result.push(...scanDir(filePath));
} else {
if (!file.endsWith(".js")) {
continue;
}
result.push(filePath);
}
}
return result
}
export default async function loadModules(dir) {
const files = scanDir(dir);
const modules = {}
for (const file of files) {
try {
// 转换为 file:// URLWindows 必需)
const moduleUrl = pathToFileURL(file).href
const module = await import(moduleUrl)
// 如果模块有默认导出,优先使用
modules[file] = module.default || module
} catch (err) {
console.error(`加载模块 ${file} 失败:`, err)
}
}
return modules;
}
const modules = await loadModules('./dist/plugins');
for (const key in modules) {
console.log(key)
}
@@ -1,6 +1,7 @@
import { ALL, Body, Controller, Inject, Post, Provide, Query } from '@midwayjs/core';
import { Constants, CrudController } from '@certd/lib-server';
import {DomainService} from "../../../modules/cert/service/domain-service.js";
import { checkPlus } from '@certd/plus-core';
/**
*
@@ -78,4 +79,24 @@ export class DomainController extends CrudController<DomainService> {
return this.ok();
}
@Post('/sync/import', { summary: Constants.per.authOnly })
async syncImport(@Body(ALL) body: any) {
const { dnsProviderType, dnsProviderAccessId } = body;
const req = {
dnsProviderType, dnsProviderAccessId, userId: this.getUserId(),
}
checkPlus()
await this.service.doSyncFromProvider(req);
return this.ok();
}
@Post('/sync/expiration', { summary: Constants.per.authOnly })
async syncExpiration(@Body(ALL) body: any) {
await this.service.doSyncDomainsExpirationDate({
userId: this.getUserId(),
})
return this.ok();
}
}
@@ -99,4 +99,15 @@ export class CnameRecordController extends CrudController<CnameRecordService> {
const res = await this.service.resetStatus(body.id);
return this.ok(res);
}
@Post('/import', { summary: Constants.per.authOnly })
async import(@Body(ALL) body: { domainList: string; cnameProviderId: any }) {
const userId = this.getUserId();
const res = await this.service.doImport({
userId,
domainList: body.domainList,
cnameProviderId: body.cnameProviderId,
});
return this.ok(res);
}
}
@@ -6,11 +6,12 @@ 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 {getPlusInfo, isPlus} 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";
import {Between} from "typeorm";
import { DomainService } from '../cert/service/domain-service.js';
@Autoload()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
@@ -44,6 +45,9 @@ export class AutoCRegisterCron {
@Inject()
userService: UserService;
@Inject()
domainService: DomainService;
@Init()
async init() {
@@ -60,7 +64,9 @@ export class AutoCRegisterCron {
await this.registerPlusExpireCheckCron();
await this.registerUserExpireCheckCron()
await this.registerUserExpireCheckCron();
await this.registerDomainExpireCheckCron();
}
async registerSiteMonitorCron() {
@@ -199,4 +205,23 @@ export class AutoCRegisterCron {
}
})
}
registerDomainExpireCheckCron(){
if (!isPlus()){
return
}
// 添加域名即将到期检查任务
const randomWeek = Math.floor(Math.random() * 7) + 1
const randomHour = Math.floor(Math.random() * 24)
const randomMinute = Math.floor(Math.random() * 60)
logger.info(`注册域名注册过期时间检查任务,每周${randomWeek} ${randomHour}:${randomMinute}检查一次`)
this.cron.register({
name: 'domain-expire-check',
cron: `0 ${randomMinute} ${randomHour} ? * ${randomWeek}`, // 每周随机一天检查一次
job: async () => {
await this.domainService.doSyncDomainsExpirationDate({})
}
})
}
}
@@ -37,7 +37,7 @@ export class AutoZPrint {
logger.info(`当前版本:${version}`);
const plusInfo = getPlusInfo();
if (isPlus()) {
logger.info(`授权信息:${plusInfo.vipType},${dayjs(plusInfo.expireTime).format('YYYY-MM-DD')}`);
logger.info(`授权信息:${plusInfo.vipType},${plusInfo.expireTime === -1 ? '永久' : dayjs(plusInfo.expireTime).format('YYYY-MM-DD')}`);
}
logger.info('Certd已启动');
logger.info('=========================================');
@@ -25,6 +25,15 @@ export class DomainEntity {
@Column({ comment: '是否禁用', name: 'disabled' })
disabled: boolean;
@Column({ comment: '注册时间', name: 'registration_date' })
registrationDate: number;
@Column({ comment: '过期时间', name: 'expiration_date' })
expirationDate: number;
@Column({ comment: '来源', name: 'from_type', length: 50 })
fromType: string;
@Column({ comment: 'http上传类型', name: 'http_uploader_type', length: 50 })
httpUploaderType: string;
@@ -1,21 +1,33 @@
import {Inject, Provide, Scope, ScopeEnum} from '@midwayjs/core';
import {InjectEntityModel} from '@midwayjs/typeorm';
import {In, Not, Repository} from 'typeorm';
import {AccessService, BaseService} from '@certd/lib-server';
import {DomainEntity} from '../entity/domain.js';
import {SubDomainService} from "../../pipeline/service/sub-domain-service.js";
import {DomainParser} from "@certd/plugin-cert";
import {DomainVerifiers} from "@certd/plugin-cert";
import { SubDomainsGetter } from '../../pipeline/service/getter/sub-domain-getter.js';
import { CnameRecordService } from '../../cname/service/cname-record-service.js';
import { http, logger, utils } from '@certd/basic';
import { AccessService, BaseService } from '@certd/lib-server';
import { doPageTurn, Pager, PageRes } from '@certd/pipeline';
import { DomainVerifiers } from "@certd/plugin-cert";
import { createDnsProvider, DomainParser, parseDomainByPsl } from "@certd/plugin-lib";
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import dayjs from 'dayjs';
import { In, Not, Repository } from 'typeorm';
import { CnameRecordEntity } from "../../cname/entity/cname-record.js";
import { CnameRecordService } from '../../cname/service/cname-record-service.js';
import { SubDomainsGetter } from '../../pipeline/service/getter/sub-domain-getter.js';
import { TaskServiceBuilder } from '../../pipeline/service/getter/task-service-getter.js';
import { SubDomainService } from "../../pipeline/service/sub-domain-service.js";
import { DomainEntity } from '../entity/domain.js';
import { BackTask, taskExecutor } from './task-executor.js';
export interface SyncFromProviderReq {
userId: number;
dnsProviderType: string;
dnsProviderAccessId: string;
}
/**
*
*/
@Provide()
@Scope(ScopeEnum.Request, {allowDowngrade: true})
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class DomainService extends BaseService<DomainEntity> {
@InjectEntityModel(DomainEntity)
repository: Repository<DomainEntity>;
@@ -28,13 +40,16 @@ export class DomainService extends BaseService<DomainEntity> {
@Inject()
cnameRecordService: CnameRecordService;
@Inject()
taskServiceBuilder: TaskServiceBuilder;
//@ts-ignore
getRepository() {
return this.repository;
}
async add(param) {
if (param.userId == null ){
if (param.userId == null) {
throw new Error('userId 不能为空');
}
if (!param.domain) {
@@ -49,6 +64,9 @@ export class DomainService extends BaseService<DomainEntity> {
if (old) {
throw new Error(`域名(${param.domain})不能重复`);
}
if (!param.fromType) {
param.fromType = 'manual'
}
return await super.add(param);
}
@@ -83,9 +101,9 @@ export class DomainService extends BaseService<DomainEntity> {
* @param userId
* @param domains //去除* 且去重之后的域名列表
*/
async getDomainVerifiers(userId: number, domains: string[]):Promise<DomainVerifiers> {
async getDomainVerifiers(userId: number, domains: string[]): Promise<DomainVerifiers> {
const mainDomainMap:Record<string, string> = {}
const mainDomainMap: Record<string, string> = {}
const subDomainGetter = new SubDomainsGetter(userId, this.subDomainService)
const domainParser = new DomainParser(subDomainGetter)
@@ -97,7 +115,7 @@ export class DomainService extends BaseService<DomainEntity> {
}
//匹配DNS记录
let allDomains = [...domains,...mainDomains]
let allDomains = [...domains, ...mainDomains]
//去重
allDomains = [...new Set(allDomains)]
@@ -106,16 +124,16 @@ export class DomainService extends BaseService<DomainEntity> {
where: {
domain: In(allDomains),
userId,
disabled:false,
disabled: false,
}
})
const dnsMap = domainRecords.filter(item=>item.challengeType === 'dns').reduce((pre, item) => {
const dnsMap = domainRecords.filter(item => item.challengeType === 'dns').reduce((pre, item) => {
pre[item.domain] = item
return pre
}, {})
const httpMap = domainRecords.filter(item=>item.challengeType === 'http').reduce((pre, item) => {
const httpMap = domainRecords.filter(item => item.challengeType === 'http').reduce((pre, item) => {
pre[item.domain] = item
return pre
}, {})
@@ -136,7 +154,7 @@ export class DomainService extends BaseService<DomainEntity> {
}, {})
//构建域名验证计划
const domainVerifiers:DomainVerifiers = {}
const domainVerifiers: DomainVerifiers = {}
for (const domain of domains) {
const mainDomain = mainDomainMap[domain]
@@ -154,7 +172,7 @@ export class DomainService extends BaseService<DomainEntity> {
}
continue
}
const cnameRecord:CnameRecordEntity = cnameMap[domain]
const cnameRecord: CnameRecordEntity = cnameMap[domain]
if (cnameRecord) {
domainVerifiers[domain] = {
domain,
@@ -180,11 +198,200 @@ export class DomainService extends BaseService<DomainEntity> {
httpUploadRootDir: httpRecord.httpUploadRootDir
}
}
continue
continue
}
domainVerifiers[domain] = null
}
return domainVerifiers;
}
async doSyncFromProvider(req: SyncFromProviderReq) {
taskExecutor.start('syncFromProviderTask', new BackTask({
key: `user_${req.userId}`,
title: `同步用户${req.userId}从域名提供商导入域名`,
run: async (task: BackTask) => {
await this._syncFromProvider(req, task)
},
}))
}
private async _syncFromProvider(req: SyncFromProviderReq, task: BackTask) {
const { userId, dnsProviderType, dnsProviderAccessId } = req;
const subDomainGetter = new SubDomainsGetter(userId, this.subDomainService)
const domainParser = new DomainParser(subDomainGetter)
const serviceGetter = this.taskServiceBuilder.create({ userId });
const access = await this.accessService.getById(dnsProviderAccessId, userId);
const context = { access, logger, http, utils, domainParser, serviceGetter };
// 翻页查询dns的记录
const dnsProvider = await createDnsProvider({ dnsProviderType, context })
const pager = new Pager({
pageNo: 1,
pageSize: 100,
})
const challengeType = "dns"
const importDomain = async (domainRecord: any) => {
task.incrementCurrent()
const domain = domainRecord.domain
const old = await this.findOne({
where: {
domain,
userId,
}
})
if (old) {
if (old.fromType !== 'auto') {
//如果是手动的,跳过更新校验配置
return
}
const updateObj: any = {
id: old.id,
dnsProviderType,
dnsProviderAccess: dnsProviderAccessId,
challengeType,
}
//更新
await super.update(updateObj)
} else {
//添加
await this.add({
userId,
domain,
dnsProviderType,
dnsProviderAccess: dnsProviderAccessId,
challengeType,
disabled: false,
fromType: 'auto',
})
}
}
const batchHandle = async (pageRes: PageRes<any>) => {
task.setTotal(pageRes.total || 0)
}
const start = async () => {
await doPageTurn({ pager, getPage: dnsProvider.getDomainListPage, itemHandle: importDomain, batchHandle })
}
start()
}
async doSyncDomainsExpirationDate(req: { userId?: number }) {
const userId = req.userId
taskExecutor.start('syncDomainsExpirationDateTask', new BackTask({
key: `user_${userId}`,
title: `同步用户(${userId ?? '全部'})注册域名过期时间`,
run: async (task: BackTask) => {
await this._syncDomainsExpirationDate({ userId, task })
}
}))
}
private async _syncDomainsExpirationDate(req: { userId?: number, task: BackTask }) {
//同步所有域名的过期时间
const pager = new Pager({
pageNo: 1,
pageSize: 100,
})
const dnsJson = await http.request({
url: "https://data.iana.org/rdap/dns.json",
method: "GET",
})
const rdapMap: Record<string, string> = {}
for (const item of dnsJson.services) {
// [["store","work"], ["https://rdap.centralnic.com/store/"]],
const suffixes = item[0]
const urls = item[1]
for (const suffix of suffixes) {
rdapMap[suffix] = urls[0]
}
}
const getDomainExpirationDate = async (domain: string) => {
const parsed = parseDomainByPsl(domain)
const mainDomain = parsed.domain || ''
if (mainDomain !== domain) {
logger.warn(`${domain}为子域名,跳过同步`)
return
}
const suffix = parsed.tld || ''
const rdapUrl = rdapMap[suffix]
if (!rdapUrl) {
throw new Error(`未找到${suffix}的rdap地址`)
}
// https://rdap.nic.work/domain/handsfree.work
const rdap = await http.request({
url: `${rdapUrl}domain/${domain}`,
method: "GET",
})
let res: any = {}
const events = rdap.events || []
for (const item of events) {
if (item.eventAction === 'expiration') {
res.expirationDate = dayjs(item.eventDate).valueOf()
} else if (item.eventAction === 'registration') {
res.registrationDate = dayjs(item.eventDate).valueOf()
}
}
return res
}
const query: any = {
challengeType: "dns",
}
if (req.userId!=null) {
query.userId = req.userId
}
const getDomainPage = async (pager: Pager) => {
const pageRes = await this.page({
query: query,
// buildQuery(bq) {
// bq.andWhere(" (expiration_date is null or expiration_date < :now) ", { now: dayjs().add(1, 'month').valueOf() })
// },
page: {
offset: pager.getOffset(),
limit: pager.pageSize,
}
})
req.task.total = pageRes.total
return {
list: pageRes.records,
total: pageRes.total,
}
}
const itemHandle = async (item: any) => {
req.task.incrementCurrent()
try {
const res = await getDomainExpirationDate(item.domain)
if (!res) {
return
}
const { expirationDate, registrationDate } = res
if (!expirationDate) {
logger.error(`获取域名${item.domain}过期时间失败`)
return
}
logger.info(`更新域名${item.domain}过期时间:${dayjs(expirationDate).format('YYYY-MM-DD')}`)
const updateObj: any = {
id: item.id,
expirationDate: expirationDate,
registrationDate: registrationDate,
}
//更新
await super.update(updateObj)
} catch (error) {
logger.error(`更新域名${item.domain}过期时间失败:${error}`)
} finally {
await utils.sleep(1000)
}
}
await doPageTurn({ pager, getPage: getDomainPage, itemHandle: itemHandle })
}
}
@@ -0,0 +1,106 @@
import { logger } from "@certd/basic"
export class BackTaskExecutor {
tasks: Record<string, Record<string, BackTask>> = {}
start(type: string, task: BackTask) {
if (!this.tasks[type]) {
this.tasks[type] = {}
}
const oldTask = this.tasks[type][task.key]
if (oldTask && oldTask.status === "running") {
throw new Error(`任务 ${type}${task.key} 正在运行中`)
}
this.tasks[type][task.key] = task
this.run(type, task);
}
get(type: string, key: string) {
if (!this.tasks[type]) {
this.tasks[type] = {}
}
return this.tasks[type][key]
}
removeIsEnd(type: string, key: string) {
const task = this.tasks[type]?.[key]
if (task && task.status !== "running") {
this.clear(type, key);
}
}
clear(type: string, key: string) {
const task = this.tasks[type]?.[key]
if (task) {
task.clearTimeout();
delete this.tasks[type][key]
}
}
private async run(type: string, task: any) {
if (task.status === "running") {
throw new Error(`任务 ${type}${task.key} 正在运行中`)
}
task.startTime = Date.now();
task.clearTimeout();
try {
task.status = "running";
return await task.run(task);
} catch (e) {
logger.error(`任务 ${task.title}[${type}-${task.key}] 执行失败`, e.message);
task.status = "failed";
task.error = e.message;
} finally {
task.endTime = Date.now();
task.status = "done";
task.timeoutId = setTimeout(() => {
this.clear(type, task.key);
}, 24 * 60 * 60 * 1000);
delete task.run;
}
}
}
export class BackTask {
key: string;
title: string;
total: number = 0;
current: number = 0;
startTime: number;
endTime: number;
status: string = "pending";
error?: string;
timeoutId?: NodeJS.Timeout;
run: (task: BackTask) => Promise<void>;
constructor(opts:{
key: string, title: string, run: (task: BackTask) => Promise<void>
}) {
const {key, title, run} = opts
this.key = key;
this.title = title;
Object.defineProperty(this, 'run', {
value: run,
writable: true,
enumerable: false, // 设置为false使其不可遍历
configurable: true
});
}
clearTimeout() {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = null;
}
}
setTotal(total: number) {
this.total = total;
}
incrementCurrent() {
this.current++
}
}
export const taskExecutor = new BackTaskExecutor();
@@ -22,6 +22,7 @@ import punycode from "punycode.js";
import { SubDomainService } from "../../pipeline/service/sub-domain-service.js";
import { SubDomainsGetter } from "../../pipeline/service/getter/sub-domain-getter.js";
import { TaskServiceBuilder } from "../../pipeline/service/getter/task-service-getter.js";
import { BackTask, taskExecutor } from "../../cert/service/task-executor.js";
type CnameCheckCacheValue = {
validating: boolean;
@@ -487,4 +488,49 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
}
await this.getRepository().update(id, { status: "cname", mainDomain: "" });
}
async doImport(req:{ userId: number; domainList: string; cnameProviderId: any }) {
const {userId,cnameProviderId,domainList} = req;
const domains = domainList.split("\n").map(item => item.trim()).filter(item => item.length > 0);
if (domains.length === 0) {
throw new ValidateException("域名列表不能为空");
}
if (!req.cnameProviderId) {
throw new ValidateException("CNAME服务提供商不能为空");
}
taskExecutor.start("cnameImport",new BackTask({
key: "user_"+userId,
title: "导入CNAME记录",
run: async (task) => {
await this._import({ userId, domains, cnameProviderId },task);
}
}));
}
async _import(req :{ userId: number; domains: string[]; cnameProviderId: any },task:BackTask) {
const userId = req.userId;
for (const domain of req.domains) {
const old = await this.getRepository().findOne({
where: {
userId: req.userId,
domain,
},
});
if (old) {
logger.warn(`域名${domain}已存在,跳过`);
}
//开始导入
try{
await this.add({
userId,
domain: domain,
cnameProviderId: req.cnameProviderId,
});
}catch(e){
logger.error(`导入域名${domain}失败:${e.message}`);
}
}
}
}

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