Compare commits

...

21 Commits

Author SHA1 Message Date
xiaojunnuo b46948c0ba v1.42.0 2026-07-05 19:23:49 +08:00
xiaojunnuo 3024720fc2 build: prepare to build 2026-07-05 19:21:52 +08:00
xiaojunnuo cf854c9278 chore(access-selector): add auto select first item function
1. 为access-selector组件添加defaultSelect属性支持自动选中第一个匹配项
2. 补全组件的typescript语法支持,新增onMounted等依赖导入
3. 在证书申请插件中启用默认选中功能
2026-07-05 01:40:08 +08:00
xiaojunnuo 608cc2a81f perf: 优化用户体验,首次访问时弹出邮箱账号绑定用以初始化账号 2026-07-05 01:14:48 +08:00
xiaojunnuo 396670dc8f chore(locales,plugin): 更新多语言文案并优化插件依赖提示
1. 修复中文文案中"sourcee"拼写错误为"来源"
2. 将插件列表启用禁用列的提示文案替换为更简洁的"点击启用/禁用"
3. 完善插件依赖和第三方依赖的帮助提示,添加格式示例说明
2026-07-04 22:31:46 +08:00
xiaojunnuo 79f65868ca perf(passkey): passkey支持多域名rpid 2026-07-04 21:56:35 +08:00
xiaojunnuo 56e5524a0f perf(cert-plugin): 调整更新天数自动减半逻辑,仅7天ip证书生效,其他情况下不减半 2026-07-04 17:01:02 +08:00
xiaojunnuo 1ae185d0bc fix(aliyun): 修复阿里云CDN/DCDN根据证书自动匹配不到证书的bug 2026-07-03 00:21:50 +08:00
xiaojunnuo 82276b53a8 perf: 阿里云ESA证书部署支持SaaS模式 2026-07-03 00:21:10 +08:00
xiaojunnuo d5882f16be fix: 修复telegram - 符号转义问题 2026-07-02 22:54:24 +08:00
xiaojunnuo b35e7b0702 refactor(monitor,cert-plugin): 移除废弃的certFile字段相关逻辑
1.  给certFile字段添加废弃注释
2.  删除证书申请成功事件中传递的file参数
3.  简化updateCertByPipelineId方法参数,移除file相关入参和赋值逻辑
2026-07-01 01:10:08 +08:00
xiaojunnuo bce7d95838 docs(ci): add slim docker image support and update all documentations
1. 新增slim镜像版本的文档说明,补充镜像版本选择指南
2. 更新docker-compose示例注释,将arm适配改为slim镜像适配
3. 重构README、镜像说明文档的镜像表格排版
4. 调整CI构建脚本,将alpine构建改为slim构建,修正Dockerfile构建逻辑
5. 统一镜像标签命名,移除-alpine后缀改为-slim
2026-07-01 00:41:50 +08:00
xiaojunnuo 4d490d0add ci(ci): add alpine image build workflow and refactor dockerfile
重构了packages/ui的Dockerfile,支持通过build-arg指定基础镜像类型,新增了多架构的alpine镜像构建任务,适配不同场景的镜像使用需求
2026-07-01 00:22:07 +08:00
xiaojunnuo 7cff1a9842 perf: 【破坏性更新】 证书压缩包不再生成文件存储,而是实时打包下载,证书申请插件不再输出certZip
自定义插件需要压缩包时可以调用new CertReader(certInfo).buildZip() 方式获取
2026-06-30 23:41:59 +08:00
xiaojunnuo cfba7b4daa chore: 清理无用测试文件并更新配置与文档 2026-06-27 00:20:33 +08:00
xiaojunnuo 4dff48e807 perf: 支持全自动匹配部署宝塔网站证书 2026-06-27 00:20:04 +08:00
xiaojunnuo 8abe0daf20 chore: 解决arm下 WorkerThreadsTaskRunner::DelayedTaskScheduler::Start() 报错问题 2026-06-26 14:27:07 +08:00
xiaojunnuo 635f069012 perf(plugin): 在线插件编辑支持配置第三方依赖和插件依赖 2026-06-26 00:49:46 +08:00
xiaojunnuo eeb83f9024 chore(cert): add md5 hash naming and duplicate cert handling
1. 新增buildCertName方法的useHash参数,使用域名列表MD5哈希作为证书名后缀避免时间戳重复
2. 为asiaisp上传证书添加重复证书检测逻辑,已存在时直接复用已有证书
2026-06-25 23:12:38 +08:00
xiaojunnuo b48831e60b perf: 新增橙域网络(asia-isp) CDN证书部署插件 2026-06-25 22:53:22 +08:00
xiaojunnuo 095791cdc2 perf: 火山引擎点播插件支持部署到自定义源站域名 2026-06-25 22:53:01 +08:00
133 changed files with 2589 additions and 898 deletions
+16
View File
@@ -94,6 +94,22 @@ jobs:
greper/certd:${{steps.get_certd_version.outputs.result}}
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{steps.get_certd_version.outputs.result}}
- name: Build slim
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
push: true
context: ./packages/ui/
build-args: |
base_type=slim
tags: |
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:slim
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{steps.get_certd_version.outputs.result}}-slim
greper/certd:slim
greper/certd:${{steps.get_certd_version.outputs.result}}-slim
ghcr.io/${{ github.repository }}:slim
ghcr.io/${{ github.repository }}:${{steps.get_certd_version.outputs.result}}-slim
- name: Build armv7
uses: docker/build-push-action@v6
with:
+7 -1
View File
@@ -211,6 +211,9 @@ export class DemoTest extends AbstractTaskPlugin {
//当以下参数变化时,触发获取选项
watches: ['certDomains', 'accessId'],
required: true,
single: false, // 是否单选
pager: true, // 是否卡其分页查询
search: true, // 是否开启搜索
})
)
siteName!: string | string[];
@@ -260,9 +263,12 @@ export class DemoTest extends AbstractTaskPlugin {
throw new Error('请选择Access授权');
}
const pager = new Pager(req);
// @ts-ignore
const access = await this.getAccess(this.accessId);
//
// 根据接口情况是否支持翻页查询,和关键字查询, 传递对应的参数,pager.pageNo,pager.pageSize, req.searchKey
// const siteRes = await access.GetDomainList(req);
//以下是模拟数据
const siteRes = [
+19 -17
View File
@@ -1,6 +1,7 @@
# Certd 开发 Agent 上下文
进入仓库后先读本文。本文同时包含常驻规则、仓库地图、常用入口和验证命令;不要依赖分散规则文件。
思考时也要使用中文。
## 项目定位
@@ -58,13 +59,11 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
## 常用验证
- 后端聚焦单测:`corepack pnpm --dir packages\ui\certd-server test:unit`
- 后端完整测试:`corepack pnpm --dir packages\ui\certd-server test`
- 前端构建:`corepack pnpm --dir packages\ui\certd-client build`
- 前端改动文件格式化:`packages\ui\certd-client\node_modules\.bin\prettier.cmd --write <files>`
- 前端改动文件 ESLint 修复:`packages\ui\certd-client\node_modules\.bin\eslint.cmd --fix <files>`
- 后端改动文件 lint fix`corepack pnpm --dir packages\ui\certd-server run lint`
- 其他package lint fix`corepack pnpm --dir packages\xxx\xxxx run lint`
- 后端单元测试:`cd packages\ui\certd-server && npm run unit`
- 后端改动文件 lint fix`cd packages\ui\certd-server && npm run lint`
- 其他package lint fix`cd packages\xxx\xxxx && npm run lint`
## 通用工作规则
@@ -78,6 +77,20 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
- 注释优先使用中文,尤其是业务规则、兼容逻辑、协议细节和隐藏风险;文件已有英文风格或引用外部术语时可保持一致。
- 遵守 DRY 和单一职责;第三次出现的业务规则、字段转换、权限判断、Repository 选择、事务传播、金额计算等逻辑,应优先抽成合适 helper 或 service 方法。
## 测试与验证
- 务必写单元测试,覆盖主要业务逻辑。
- 实现新功能或修复行为缺陷前,优先补单元测试并先确认红灯,再实现并跑聚焦验证。
- 确实不适合先写测试时,在回复中说明原因和替代验证方式。
- 后补单元测试时,按正确行为写预期;若红灯需要修改既有实现,先向用户确认这是 bug 还是既有需求,避免未经确认改变行为。
- 后端纯单测放在 `src/**/*.test.ts`,尽量与被测文件相邻;`test:unit` 只跑这些文件,构建/打包应排除 `*.test.ts`
- 单测需要 mock ESM 静态 import 时,优先使用 `esmock`,不要为了测试改业务代码结构。
- 各包 `test:unit` 脚本应显式设置 `NODE_ENV=unittest`
- 单包单测优先用 `cd <包目录> && npm run test:unit`,例如 `cd packages\ui\certd-server && npm run test:unit`
- 优先对改动包运行聚焦测试或格式化/ESLint;只有跨包影响明显时再考虑更大范围构建。
## 后端规则
- 后端主包是 `packages/ui/certd-server`,使用 Node.js、ESM、TypeScript、MidwayJS 3、Koa、TypeORM 和 SQL 迁移。
@@ -89,7 +102,7 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
- 只有需要事务传播时才定义 `ctx`;普通查询、纯函数和简单私有方法继续使用明确参数。
- 需要按事务上下文取 Repository 时,用 `BaseService.getRepo(ctx, EntityClass)`
- 需要“有事务则复用、无事务则开启”时,用 `BaseService.transactionWithCtx(ctx, callback)`
- 拼接可选 `projectId` 查询条件时,用 `BaseService.buildUserProjectQuery(userId, projectId)`;不要直接写 `{ userId, projectId }`
- 拼接可选 `projectId` 查询条件时,**必须**使`BaseService.buildUserProjectQuery(userId, projectId)`,禁止直接写 `{ userId, projectId }`因为 `projectId` 可能为 `null`/`undefined`,直接放入查询会生成错误的 `WHERE projectId = NULL` 条件。
- `ctx` 类型复用 `BaseService` 导出的 `ServiceContext`
- 新增 service 方法避免与 `BaseService` 方法签名冲突,例如不要用 `delete(id)` 覆盖 `delete(ids, where?)`;改用 `deleteById` 等具体名称。
@@ -164,7 +177,6 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
- 插件依赖的第三方 SDK 可能通过 runtime-deps 动态安装到后端运行目录 `./data/.runtime-deps`。分析阿里云、腾讯云等 SDK 行为时,需要进入该目录阅读实际安装版本代码。
- 修改证书申请、验证、部署或通知行为时,先判断归属:ACME client、pipeline 核心、后端 module/service/entity/controller、具体插件、前端 view/form/schema。
- 单个服务商或部署目标的问题,不要轻易修改共享 pipeline/core;只有可复用公共语义或跨插件一致行为才上移到 `packages/core/pipeline``packages/plugins/plugin-lib`
- ACME / EAB:公共 EAB 可能只能创建一次账号;跨用户复用公共 EAB 时,应保存并复用同一个 ACME account private key。
- `newAccount({ onlyReturnExisting: true })` 可用同一个 account private key 取回已创建账号 URL,且不会再次消费 EAB。
- 修改 EAB `kid` 后,应重新生成绑定该 `kid` 的 account private key;否则应阻止继续申请并提示刷新账号私钥。
- 插件开发前先读对应技能:`.trae/skills/dns-provider-dev/SKILL.md``.trae/skills/task-plugin-dev/SKILL.md``.trae/skills/access-plugin-dev/SKILL.md``.trae/skills/plugin-converter/SKILL.md`
@@ -198,13 +210,3 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
- 后端业务数据、接口、实体、权限、迁移:改 `packages/ui/certd-server/src/modules``src/controller`
- 表单、列表、插件配置 UI:改 `packages/ui/certd-client/src/views/certd` 及对应 `src/api`
## 测试与验证
- 实现新功能或修复行为缺陷前,优先补单元测试并先确认红灯,再实现并跑聚焦验证。
- 确实不适合先写测试时,在回复中说明原因和替代验证方式。
- 后补单元测试时,按正确行为写预期;若红灯需要修改既有实现,先向用户确认这是 bug 还是既有需求,避免未经确认改变行为。
- 后端纯单测放在 `src/**/*.test.ts`,尽量与被测文件相邻;`test:unit` 只跑这些文件,构建/打包应排除 `*.test.ts`
- 单测需要 mock ESM 静态 import 时,优先使用 `esmock`,不要为了测试改业务代码结构。
- 各包 `test:unit` 脚本应显式设置 `NODE_ENV=unittest`
- 单包单测优先用 `corepack pnpm --dir <包目录> test:unit`,例如 `corepack pnpm --dir packages\ui\certd-server test:unit`
- 优先对改动包运行聚焦测试或格式化/ESLint;只有跨包影响明显时再考虑更大范围构建。
+28
View File
@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
### Bug Fixes
* 修复jdk证书格式的问题 ([260f5ae](https://github.com/certd/certd/commit/260f5ae777b83493b0c578fe30fd00ec0c873226))
* 修复telegram - 符号转义问题 ([d5882f1](https://github.com/certd/certd/commit/d5882f16bedb09baf09ace92049b02872620f5dc))
* **aliyun:** 修复阿里云CDN/DCDN根据证书自动匹配不到证书的bug ([1ae185d](https://github.com/certd/certd/commit/1ae185d0bc356f4678bc38ca0582ce3396f82ebe))
### Features
* 通过插件配置懒加载依赖,动态加载第三方依赖包,精简安装镜像大小 ([01568ca](https://github.com/certd/certd/commit/01568ca1489069046b5a89ebdd4ced2f7f6ddf93))
### Performance Improvements
* 阿里云ESA证书部署支持SaaS模式 ([82276b5](https://github.com/certd/certd/commit/82276b53a8474a18a3d0237050907c994fc748f0))
* 【破坏性更新】 证书压缩包不再生成文件存储,而是实时打包下载,证书申请插件不再输出certZip ([7cff1a9](https://github.com/certd/certd/commit/7cff1a98424120585205889874b3ef4956a30583))
* 火山引擎点播插件支持部署到自定义源站域名 ([095791c](https://github.com/certd/certd/commit/095791cdc2b7c1f4b913b634643afec5e30fe9b0))
* 基础镜像改成node:22-trixie-slim,对网络兼容性更好 ([c66a2bd](https://github.com/certd/certd/commit/c66a2bd77ab6dbb3e3fe2c00562b66287a9429ea))
* 新增橙域网络(asia-isp) CDN证书部署插件 ([b48831e](https://github.com/certd/certd/commit/b48831e60b0059bef7ef9a34ab61c9dd2f684641))
* 优化阿里云API网关增加翻页查询 ([ed58ae3](https://github.com/certd/certd/commit/ed58ae3c5339e4a0238a92acfe7ea6d2f566ea28))
* 优化用户体验,首次访问时弹出邮箱账号绑定用以初始化账号 ([608cc2a](https://github.com/certd/certd/commit/608cc2a81ff0b4872c9fe11ed9c9c0b4b90a12a3))
* 优化ACME账号字段的选择提示 ([bfd3cac](https://github.com/certd/certd/commit/bfd3cacc687fc5cbc3cb2ca3cadbc140de300dc2))
* 支持全自动匹配部署宝塔网站证书 ([4dff48e](https://github.com/certd/certd/commit/4dff48e807c32a7623ec9206cf39c88e88f89f6a))
* **cert-plugin:** 调整更新天数自动减半逻辑,仅7天ip证书生效,其他情况下不减半 ([56e5524](https://github.com/certd/certd/commit/56e5524a0f4af3645d70bc3b3ec750b45ba8de10))
* dns默认ipv4first ([194463b](https://github.com/certd/certd/commit/194463bea9e797315aa7a724f4b2930701570419))
* **passkey:** passkey支持多域名rpid ([79f6586](https://github.com/certd/certd/commit/79f65868ca0f5162bbc2f935ce89abc28011d816))
* **plugin:** 在线插件编辑支持配置第三方依赖和插件依赖 ([635f069](https://github.com/certd/certd/commit/635f069012d4193cfb7cb051c96e28eec1247ca2))
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
### Bug Fixes
+18 -11
View File
@@ -105,17 +105,23 @@ https://certd.handfree.work/
#### Docker镜像说明:
- 国内镜像地址:
- `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest`
- `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:armv7``[version]-armv7`
- DockerHub地址:
- `https://hub.docker.com/r/greper/certd`
- `greper/certd:latest`
- `greper/certd:armv7``greper/certd:[version]-armv7`
- GitHub Packages地址:
**镜像版本:**
- `ghcr.io/certd/certd:latest`
- `ghcr.io/certd/certd:armv7``ghcr.io/certd/certd:[version]-armv7`
| 版本标签 | 基础系统 | 说明 |
| --- | --- | --- | --- |
| `latest` / `[version]` | Alpine Linux | 默认版本,镜像体积小 |
| `slim` / `[version]-slim` | Debian slim | 基于glibcdns解析兼容性好(可能需要配置security_opt -seccomp=unconfined |
| `armv7` / `[version]-armv7` | Alpine Linux | ARMv7 架构专用版本 |
**镜像地址:**
| 镜像仓库 | latest | slim | armv7 |
| --- | --- | --- | --- |
| 阿里云 | `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest` | `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:slim` | `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:armv7` |
| Docker Hub | `greper/certd:latest` | `greper/certd:slim` | `greper/certd:armv7` |
| GitHub Packages | `ghcr.io/certd/certd:latest` | `ghcr.io/certd/certd:slim` | `ghcr.io/certd/certd:armv7` |
> 带版本号的标签请将 `latest` / `slim` / `armv7` 替换为 `[version]` / `[version]-slim` / `[version]-armv7`
- 镜像构建通过`Actions`自动执行,过程公开透明,请放心使用
- [点我查看镜像构建日志](https://github.com/certd/certd/actions/workflows/build-image.yml)
@@ -180,7 +186,8 @@ https://certd.handfree.work/
3. 获得专业版功能
> [50元专业版优惠券限时领取](https://app.handfree.work/subject/#/app/certd/product) https://app.handfree.work/subject/#/app/certd/product
> [50元专业版优惠券限时领取](https://app.handfree.work/subject/#/app/certd/product)
> https://app.handfree.work/subject/#/app/certd/product
> app.handfree.work是Certd官方激活码购买平台
+2
View File
@@ -5,6 +5,8 @@ services:
image: registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest
# image: ghcr.io/certd/certd:latest # --------- 如果 报镜像not found,可以尝试其他镜像源
# image: greper/certd:latest
# security_opt: # --------- 如果slim镜像下启动报错,尝试去掉这两行注释
# - seccomp=unconfined # 解决slim镜像下WorkerThreadsTaskRunner::DelayedTaskScheduler::Start() 报错问题
container_name: certd # 容器名
restart: unless-stopped # 自动重启
volumes:
+15 -3
View File
@@ -1,20 +1,32 @@
# 镜像说明
## 镜像版本说明
| 版本标签 | 基础系统 | OpenJDK | 说明 |
| --- | --- | --- | --- |
| `latest` / `[version]` | Alpine Linux | 有 | **推荐**,默认版本,镜像体积更小,功能完整,支持所有插件 |
| `slim` / `[version]-slim` | Debian slim | 有 | glibc 兼容性更好,适用于依赖 glibc 的特殊场景 |
| `armv7` / `[version]-armv7` | Alpine Linux | 有 | ARMv7 架构专用版本 |
## 国内镜像地址:
* `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest`
* `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest`
* `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:slim``[version]-slim`
* `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:armv7``[version]-armv7`
## DockerHub地址:
* `https://hub.docker.com/r/greper/certd`
* `greper/certd:latest`
* `greper/certd:slim``greper/certd:[version]-slim`
* `greper/certd:armv7``greper/certd:[version]-armv7`
## GitHub Packages地址:
* `ghcr.io/certd/certd:latest`
* `ghcr.io/certd/certd:slim``ghcr.io/certd/certd:[version]-slim`
* `ghcr.io/certd/certd:armv7``ghcr.io/certd/certd:[version]-armv7`
*
## 镜像构建公开
镜像构建通过`Actions`自动执行,过程公开透明,请放心使用
镜像构建通过`Actions`自动执行,过程公开透明,请放心使用
* [点我查看镜像构建日志](https://github.com/certd/certd/actions/workflows/build-image.yml)
![](../images/action/action-build.jpg)
+12
View File
@@ -2,6 +2,18 @@
## 一、安装
### 镜像版本选择
Certd 提供多种 Docker 镜像版本,您可以根据需要选择:
| 版本标签 | 基础系统 | 说明 |
| --- | --- | --- | --- |
| `latest` / `[version]` | Alpine Linux | 默认版本,镜像体积小 |
| `slim` / `[version]-slim` | Debian slim | glibc版本,dns解析兼容性更好(可能需要配置security_opt -seccomp=unconfined|
| `armv7` / `[version]-armv7` | Alpine Linux | ARMv7 架构专用版本 |
> 如果您不确定使用哪个版本,请使用默认的 `latest` 版本。
### 一键脚本安装(推荐)
如果您的服务器未安装 Docker,该脚本会自动为您安装 Docker 和 Docker Compose,并启动 Certd 容器。
+64 -63
View File
@@ -18,69 +18,70 @@
| 14.| **SFTP授权** | |
| 15.| **阿里云OSS授权** | 包含地域和Bucket |
| 16.| **APISIX授权** | |
| 17.| **亚马逊云aws授权** | |
| 18.| **亚马逊云科技(国区)授权** | |
| 19.| **微软云Azure授权** | |
| 20.| **BIND9 DNS 授权** | 通过 SSH 连接到 BIND9 服务器,使用 nsupdate 命令管理 DNS 记录 |
| 21.| **CacheFly** | CacheFly |
| 22.| **ACME账号** | 用于复用ACME账号私钥和账号地址,证书申请时不再临时创建账号 |
| 23.| **EAB授权** | ZeroSSL证书申请需要EAB授权 |
| 24.| **google cloud** | 谷歌云授权 |
| 25.| **cloudflare授权** | |
| 26.| **中国移动CND授权** | |
| 27.| **授权插件示例** | 这是一个示例授权插件,用于演示如何实现一个授权插件 |
| 28.| **dns.la授权** | |
| 29.| **彩虹DNS** | 彩虹DNS管理系统授权 |
| 30.| **多吉云** | |
| 31.| **Dokploy授权** | |
| 32.| **Dynadot授权** | |
| 33.| **farcdn授权** | |
| 34.| **FlexCDN授权** | |
| 35.| **Gcore** | Gcore |
| 36.| **Github授权** | |
| 37.| **godaddy授权** | |
| 38.| **HiPM DNSMgr** | HiPM DNSMgr API Token 授权 |
| 39.| **金山云授权** | |
| 40.| **FTP授权** | |
| 41.| **七牛OSS授权** | |
| 42.| **腾讯云COS授权** | 腾讯云对象存储授权,包含地域和存储桶 |
| 43.| **s3/minio授权** | S3/minio oss授权 |
| 44.| **namesilo授权** | |
| 45.| **Next Terminal 授权** | 用于访问 Next Terminal API 的授权配置 |
| 46.| **Nginx Proxy Manager 授权** | 用于登录 Nginx Proxy Manager,并为代理主机证书部署提供授权。 |
| 47.| **1panel授权** | 账号和密码 |
| 48.| **支付宝** | |
| 49.| **白山云授权** | |
| 50.| **宝塔云WAF授权** | 用于连接和管理宝塔云WAF服务的授权配置 |
| 51.| **cdnfly授权** | |
| 52.| **k8s授权** | |
| 53.| **括彩云cdn授权** | 括彩云CDN,每月免费30G[注册即领](https://kuocaicdn.com/register?code=8mn536rrzfbf8) |
| 54.| **LeCDN授权** | |
| 55.| **lucky** | |
| 56.| **猫云授权** | |
| 57.| **plesk授权** | |
| 58.| **长亭雷池授权** | |
| 59.| **群晖登录授权** | |
| 60.| **uniCloud** | unicloud授权 |
| 61.| **微信支付** | |
| 62.| **易盾rcdn授权** | 易盾CDN,每月免费30G[注册即领](https://rhcdn.yiduncdn.com/register?code=8mn536rrzfbf8) |
| 63.| **易发云短信** | sms.yfyidc.cn/ |
| 64.| **易盾DCDN授权** | https://user.yiduncdn.com |
| 65.| **易支付** | |
| 66.| **proxmox** | |
| 67.| **Spaceship.com 授权** | Spaceship.com API 授权插件 |
| 68.| **Technitium DNS Server** | Technitium DNS Server 自建DNS服务器授权 |
| 69.| **UCloud授权** | 优刻得授权 |
| 70.| **又拍云** | |
| 71.| **网宿授权** | |
| 72.| **西部数码授权** | |
| 73.| **我爱云授权** | 我爱云CDN |
| 74.| **新网授权(代理方式)** | |
| 75.| **新网授权** | |
| 76.| **新网互联授权** | 仅支持代理账号,ip需要加入白名单 |
| 77.| **Zenlayer授权** | Zenlayer授权 |
| 78.| **GoEdge授权** | |
| 79.| **雨云授权** | https://app.rainyun.com/ |
| 17.| **橙域网络(asia-isp)授权** | 橙域网络CDN API授权,用于部署证书到橙域CDN |
| 18.| **亚马逊云aws授权** | |
| 19.| **亚马逊云科技(国区)授权** | |
| 20.| **微软云Azure授权** | |
| 21.| **BIND9 DNS 授权** | 通过 SSH 连接到 BIND9 服务器,使用 nsupdate 命令管理 DNS 记录 |
| 22.| **CacheFly** | CacheFly |
| 23.| **ACME账号** | 用于复用ACME账号私钥和账号地址,证书申请时不再临时创建账号 |
| 24.| **EAB授权** | ZeroSSL证书申请需要EAB授权 |
| 25.| **google cloud** | 谷歌云授权 |
| 26.| **cloudflare授权** | |
| 27.| **中国移动CND授权** | |
| 28.| **授权插件示例** | 这是一个示例授权插件,用于演示如何实现一个授权插件 |
| 29.| **dns.la授权** | |
| 30.| **彩虹DNS** | 彩虹DNS管理系统授权 |
| 31.| **多吉云** | |
| 32.| **Dokploy授权** | |
| 33.| **Dynadot授权** | |
| 34.| **farcdn授权** | |
| 35.| **FlexCDN授权** | |
| 36.| **Gcore** | Gcore |
| 37.| **Github授权** | |
| 38.| **godaddy授权** | |
| 39.| **HiPM DNSMgr** | HiPM DNSMgr API Token 授权 |
| 40.| **金山云授权** | |
| 41.| **FTP授权** | |
| 42.| **七牛OSS授权** | |
| 43.| **腾讯云COS授权** | 腾讯云对象存储授权,包含地域和存储桶 |
| 44.| **s3/minio授权** | S3/minio oss授权 |
| 45.| **namesilo授权** | |
| 46.| **Next Terminal 授权** | 用于访问 Next Terminal API 的授权配置 |
| 47.| **Nginx Proxy Manager 授权** | 用于登录 Nginx Proxy Manager,并为代理主机证书部署提供授权。 |
| 48.| **1panel授权** | 账号和密码 |
| 49.| **支付宝** | |
| 50.| **白山云授权** | |
| 51.| **宝塔云WAF授权** | 用于连接和管理宝塔云WAF服务的授权配置 |
| 52.| **cdnfly授权** | |
| 53.| **k8s授权** | |
| 54.| **括彩云cdn授权** | 括彩云CDN,每月免费30G[注册即领](https://kuocaicdn.com/register?code=8mn536rrzfbf8) |
| 55.| **LeCDN授权** | |
| 56.| **lucky** | |
| 57.| **猫云授权** | |
| 58.| **plesk授权** | |
| 59.| **长亭雷池授权** | |
| 60.| **群晖登录授权** | |
| 61.| **uniCloud** | unicloud授权 |
| 62.| **微信支付** | |
| 63.| **易盾rcdn授权** | 易盾CDN,每月免费30G[注册即领](https://rhcdn.yiduncdn.com/register?code=8mn536rrzfbf8) |
| 64.| **易发云短信** | sms.yfyidc.cn/ |
| 65.| **易盾DCDN授权** | https://user.yiduncdn.com |
| 66.| **易支付** | |
| 67.| **proxmox** | |
| 68.| **Spaceship.com 授权** | Spaceship.com API 授权插件 |
| 69.| **Technitium DNS Server** | Technitium DNS Server 自建DNS服务器授权 |
| 70.| **UCloud授权** | 优刻得授权 |
| 71.| **又拍云** | |
| 72.| **网宿授权** | |
| 73.| **西部数码授权** | |
| 74.| **我爱云授权** | 我爱云CDN |
| 75.| **新网授权(代理方式)** | |
| 76.| **新网授权** | |
| 77.| **新网互联授权** | 仅支持代理账号,ip需要加入白名单 |
| 78.| **Zenlayer授权** | Zenlayer授权 |
| 79.| **GoEdge授权** | |
| 80.| **雨云授权** | https://app.rainyun.com/ |
<style module>
table th:first-of-type {
+44 -42
View File
@@ -1,5 +1,5 @@
# 任务插件
`132` 款任务插件
`134` 款任务插件
## 1. 证书申请
| 序号 | 名称 | 说明 |
@@ -25,30 +25,31 @@
| 序号 | 名称 | 说明 |
|-----|-----|-----|
| 1.| **APISIX-更新证书** | 自动更新APISIX证书 |
| 2.| **CacheFly-部署证书到CacheFly** | 部署证书到 CacheFly |
| 3.| **中国移动-部署证书到CDN** | 中国移动自动部署证书到CDN |
| 4.| **多吉云-部署到多吉云CDN** | |
| 5.| **farcdn-更新证书** | www.farcdn.net |
| 6.| **FlexCDN-更新证书** | |
| 7.| **Gcore-刷新Gcore证书** | 刷新现有的证书 |
| 8.| **Gcore-部署证书到Gcore** | 仅上传 并不会部署到cdn |
| 9.| **GoEdge-更新证书** | GoEdge |
| 10.| **金山云-更新CDN证书** | 金山云自动更新CDN证书 |
| 11.| **山云-更新证书** | |
| 12.| **cdnfly-部署证书到cdnfly** | cdnfly |
| 13.| **天翼云-部署证书到CDN** | 部署证书到天翼云CDN和全站加速 |
| 14.| **括彩云-部署到括彩云CDN** | 括彩云CDN,每月免费30G[注册即领](https://kuocaicdn.com/register?code=8mn536rrzfbf8) |
| 15.| **LeCDN-更新证书V2** | 支持新版本LeCDN |
| 16.| **LeCDN-更新证书** | |
| 17.| **Maoyun-更新猫云CDN证书** | |
| 18.| **易盾-部署到易盾DCDN** | 主要是防御,http://user.yiduncdn.com/ |
| 19.| **易盾-部署到易盾RCDN** | 易盾CDN,每月免费30G[注册即领](https://rhcdn.yiduncdn.com/register?code=8mn536rrzfbf8) |
| 20.| **雨云-更新证书** | app.rainyun.com |
| 21.| **又拍云-部署证书到CDN/USS** | 支持又拍云CDN,又拍云云存储USS |
| 22.| **网宿-更新证书** | 网宿证书自动更新 |
| 23.| **西数-部署到虚拟主机** | 西部数码部署证书到虚拟主机 |
| 24.| **我爱云-部署证书到我爱云CDN** | 部署证书到我爱云CDN |
| 25.| **Zenlayer-刷新证书** | 刷新Zenlayer CDN证书 |
| 2.| **橙域网络-部署证书到CDN** | 部署证书到橙域网络(asia-isp) CDN加速域名 |
| 3.| **CacheFly-部署证书到CacheFly** | 部署证书到 CacheFly |
| 4.| **中国移动-部署证书到CDN** | 中国移动自动部署证书到CDN |
| 5.| **多吉云-部署到多吉云CDN** | |
| 6.| **farcdn-更新证书** | www.farcdn.net |
| 7.| **FlexCDN-更新证书** | |
| 8.| **Gcore-刷新Gcore证书** | 刷新现有的证书 |
| 9.| **Gcore-部署证书到Gcore** | 仅上传 并不会部署到cdn |
| 10.| **GoEdge-更新证书** | GoEdge |
| 11.| **山云-更新CDN证书** | 金山云自动更新CDN证书 |
| 12.| **白山云-更新证书** | |
| 13.| **cdnfly-部署证书到cdnfly** | cdnfly |
| 14.| **天翼云-部署证书到CDN** | 部署证书到天翼云CDN和全站加速 |
| 15.| **括彩云-部署到括彩云CDN** | 括彩云CDN,每月免费30G[注册即领](https://kuocaicdn.com/register?code=8mn536rrzfbf8) |
| 16.| **LeCDN-更新证书V2** | 支持新版本LeCDN |
| 17.| **LeCDN-更新证书** | |
| 18.| **Maoyun-更新猫云CDN证书** | |
| 19.| **易盾-部署到易盾DCDN** | 主要是防御,http://user.yiduncdn.com/ |
| 20.| **易盾-部署到易盾RCDN** | 易盾CDN,每月免费30G[注册即领](https://rhcdn.yiduncdn.com/register?code=8mn536rrzfbf8) |
| 21.| **雨云-更新证书** | app.rainyun.com |
| 22.| **又拍云-部署证书到CDN/USS** | 支持又拍云CDN,又拍云云存储USS |
| 23.| **网宿-更新证书** | 网宿证书自动更新 |
| 24.| **西数-部署到虚拟主机** | 西部数码部署证书到虚拟主机 |
| 25.| **我爱云-部署证书到我爱云CDN** | 部署证书到我爱云CDN |
| 26.| **Zenlayer-刷新证书** | 刷新Zenlayer CDN证书 |
## 4. 面板
| 序号 | 名称 | 说明 |
@@ -62,22 +63,23 @@
| 7.| **1Panel-部署面板证书** | 更新1Panel的面板证书 |
| 8.| **1Panel-更新站点证书** | 更新1Panel的站点证书 |
| 9.| **宝塔-删除过期证书** | 删除证书夹中过期证书 |
| 10.| **宝塔-WAF证书部署** | 部署宝塔云WAF/aaWAF |
| 11.| **宝塔-面板证书部署** | 部署宝塔面板本身的ssl证书 |
| 12.| **宝塔win-网站证书部署** | 部署到Windows版宝塔管理的站点的ssl证书 |
| 13.| **宝塔-网站证书部署** | 部署宝塔管理的站点的ssl证书,目前支持宝塔网站站点、docker站点等。本插件也支持aaPanel。 |
| 14.| **K8S-Apply自定义yaml** | apply自定义yaml到k8s |
| 15.| **K8S-Ingress 证书部署** | 部署证书到k8s的Ingress |
| 16.| **K8S-部署证书到Secret** | 部署证书到k8s的secret |
| 17.| **lucky-更新Lucky证书** | |
| 18.| **Plesk-部署Plesk网站证书** | |
| 19.| **Plesk-更新证书** | 不会创建新证书记录,直接更新旧的证书 |
| 20.| **雷池-更新证书(支持控制台和防护应用)** | 更新长亭雷池WAF的证书,支持更新控制台和防护应用的证书 |
| 21.| **群晖-部署证书到群晖面板** | Synology,支持6.x以上版本 |
| 22.| **群晖-刷新OTP登录有效期** | 群晖登录状态可能30天失效,需要在失效之前登录一次,刷新有效期,您可以将其放在“部署到群晖面板”任务之后 |
| 23.| **uniCloud-部署到服务空间** | 部署到服务空间 |
| 24.| **Proxmox-上传证书到Proxmox** | |
| 25.| **威联通-部署证书到威联通** | 部署证书到qnap |
| 10.| **宝塔-全自动部署** | 根据证书域名自动匹配宝塔站点,全自动部署SSL证书。新增加速域名自动感知,自动新增部署 |
| 11.| **宝塔-WAF证书部署** | 部署宝塔云WAF/aaWAF |
| 12.| **宝塔-面板证书部署** | 部署宝塔面板本身的ssl证书 |
| 13.| **宝塔win-网站证书部署** | 部署到Windows版宝塔管理的站点的ssl证书 |
| 14.| **宝塔-网站证书部署** | 部署宝塔管理的站点的ssl证书,目前支持宝塔网站站点、docker站点等。本插件也支持aaPanel。 |
| 15.| **K8S-Apply自定义yaml** | apply自定义yaml到k8s |
| 16.| **K8S-Ingress 证书部署** | 部署证书到k8s的Ingress |
| 17.| **K8S-部署证书到Secret** | 部署证书到k8s的secret |
| 18.| **lucky-更新Lucky证书** | |
| 19.| **Plesk-部署Plesk网站证书** | |
| 20.| **Plesk-更新证书** | 不会创建新证书记录,直接更新旧的证书 |
| 21.| **雷池-更新证书(支持控制台和防护应用)** | 更新长亭雷池WAF的证书,支持更新控制台和防护应用的证书。 |
| 22.| **群晖-部署证书到群晖面板** | Synology,支持6.x以上版本 |
| 23.| **群晖-刷新OTP登录有效期** | 群晖登录状态可能30天失效,需要在失效之前登录一次,刷新有效期,您可以将其放在“部署到群晖面板”任务之后 |
| 24.| **uniCloud-部署到服务空间** | 部署到服务空间 |
| 25.| **Proxmox-上传证书到Proxmox** | |
| 26.| **威联通-部署证书到威联通** | 部署证书到qnap |
## 5. 阿里云
| 序号 | 名称 | 说明 |
@@ -90,7 +92,7 @@
| 6.| **阿里云-部署证书至API网关** | 自动部署域名证书至阿里云API网关(APIGateway |
| 7.| **阿里云-部署证书至CDN** | 自动部署域名证书至阿里云CDN |
| 8.| **阿里云-部署证书至DCDN** | 依赖证书申请前置任务,自动部署域名证书至阿里云DCDN |
| 9.| **阿里云-部署至ESA** | 部署证书到阿里云ESA(边缘安全加速),自动删除过期证书 |
| 9.| **阿里云-部署至ESA** | 部署证书到阿里云ESA(边缘安全加速),支持边缘证书和SaaS证书两种模式 |
| 10.| **阿里云-部署至阿里云FC(3.0)** | 部署证书到阿里云函数计算(FC3.0) |
| 11.| **阿里云-部署至GA** | 部署证书到阿里云GA(全球加速),支持更新默认证书和扩展证书 |
| 12.| **阿里云-部署至直播(Live** | 部署证书到阿里云视频直播(Live)域名 |
Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB

+16 -26
View File
@@ -1,15 +1,23 @@
# google证书申请教程
## 1、启用API
## 1、 添加流水线
点击“创建证书流水线”按钮
## 2、 生成google ACME账号
![](./images/google-acme.png)
## 3、 获取Google EAB
### 3.1、启用API
打开如下链接,启用 API
https://console.cloud.google.com/apis/library/publicca.googleapis.com
打开该链接后点击“启用”,随后等待右侧出现“API已启用”则可以关闭该页。
## 2、 获取授权
以下两种方式任选其一
### 2.1 直接获取EAB 【推荐】
## 3.2、 创建EAB
1. 打开“Google Cloud Shell”(在右上角点击激活CloudShell图标)。
@@ -28,31 +36,13 @@ keyId: xxxxxxxxxxxxx]
```
![](./images/google-eab.png)
3. 到Certd中,创建一条EAB授权记录,填写keyId(=kid) 和 b64MacKey 信息
3. 到Certd中,创建一条EAB授权记录,填写keyId(=kid) 和 b64MacKey 信息
注意:keyId没有`]`结尾,不要把`]`也复制了
## 4、 生成Google ACME账号
注意:EAB授权使用过一次之后,会绑定邮箱,后续再次使用时,要使用相同的邮箱,所以邮箱切记不要修改
否则会报错 `Unknown external account binding (EAB) key. This may be due to the EAB key expiring which occurs 7 days after creation`
4. 创建证书流水线,选择证书提供商为google,选择EAB授权,运行流水线申请证书
### 2.2 通过google服务账号接口获取授权
此方式可以自动获取EAB,需要服务端配置代理
1. 创建服务账号
https://console.cloud.google.com/projectselector2/iam-admin/serviceaccounts/create?walkthrough_id=iam--create-service-account&hl=zh-cn#step_index=1
2. 选择一个项目,进入创建服务账号页面
3. 给服务账号起一个名字,点击`创建并继续`
4. 向此服务账号授予对项目的访问权限: `选择角色`->`基本`->`Owner`
5. 点击完成
6. 点击服务账号,进入服务账号详情页面
7. 点击`添加密钥`->`创建新密钥`->`JSON`,下载密钥文件
8. 将json文件内容粘贴到 certd中 Google服务授权输入框中
9. 创建证书流水线,选择证书提供商为google, 选择服务账号授权,运行流水线申请证书
## 5、创建证书流水线,运行流水线申请证书
+1 -1
View File
@@ -9,5 +9,5 @@
}
},
"npmClient": "pnpm",
"version": "1.41.4"
"version": "1.42.0"
}
+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.42.0](https://github.com/publishlab/node-acme-client/compare/v1.41.4...v1.42.0) (2026-07-05)
### Performance Improvements
* 新增橙域网络(asia-isp) CDN证书部署插件 ([b48831e](https://github.com/publishlab/node-acme-client/commit/b48831e60b0059bef7ef9a34ab61c9dd2f684641))
## [1.41.4](https://github.com/publishlab/node-acme-client/compare/v1.41.3...v1.41.4) (2026-06-14)
**Note:** Version bump only for package @certd/acme-client
+2 -3
View File
@@ -3,7 +3,7 @@
"description": "Simple and unopinionated ACME client",
"private": false,
"author": "nmorsman",
"version": "1.41.4",
"version": "1.42.0",
"type": "module",
"module": "./dist/index.js",
"main": "./dist/index.js",
@@ -18,7 +18,7 @@
"types"
],
"dependencies": {
"@certd/basic": "^1.41.4",
"@certd/basic": "^1.42.0",
"@peculiar/x509": "^1.11.0",
"asn1js": "^3.0.5",
"axios": "^1.9.0",
@@ -41,7 +41,6 @@
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^5.1.3",
"esmock": "^2.7.5",
"jsdoc-to-markdown": "^8.0.1",
"mocha": "^10.6.0",
"nock": "^13.5.4",
"prettier": "3.3.3",
+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.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
**Note:** Version bump only for package @certd/basic
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
### Bug Fixes
+1 -1
View File
@@ -1 +1 @@
21:25
19:21
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/basic",
"private": false,
"version": "1.41.4",
"version": "1.42.0",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
+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.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
### Features
* 通过插件配置懒加载依赖,动态加载第三方依赖包,精简安装镜像大小 ([01568ca](https://github.com/certd/certd/commit/01568ca1489069046b5a89ebdd4ced2f7f6ddf93))
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
**Note:** Version bump only for package @certd/pipeline
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/pipeline",
"private": false,
"version": "1.41.4",
"version": "1.42.0",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -21,8 +21,8 @@
"lint": "eslint --fix"
},
"dependencies": {
"@certd/basic": "^1.41.4",
"@certd/plus-core": "^1.41.4",
"@certd/basic": "^1.42.0",
"@certd/plus-core": "^1.42.0",
"dayjs": "^1.11.7",
"lodash-es": "^4.17.21",
"reflect-metadata": "^0.2.2"
+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.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
**Note:** Version bump only for package @certd/lib-huawei
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
### Bug Fixes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-huawei",
"private": false,
"version": "1.41.4",
"version": "1.42.0",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
"types": "./dist/d/index.d.ts",
+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.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
**Note:** Version bump only for package @certd/lib-iframe
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
### Bug Fixes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-iframe",
"private": false,
"version": "1.41.4",
"version": "1.42.0",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
+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.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
**Note:** Version bump only for package @certd/jdcloud
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
### Bug Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/jdcloud",
"version": "1.41.4",
"version": "1.42.0",
"description": "jdcloud openApi sdk",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
+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.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
**Note:** Version bump only for package @certd/lib-k8s
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
**Note:** Version bump only for package @certd/lib-k8s
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-k8s",
"private": false,
"version": "1.41.4",
"version": "1.42.0",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -21,7 +21,7 @@
"lint": "eslint --fix"
},
"dependencies": {
"@certd/basic": "^1.41.4",
"@certd/basic": "^1.42.0",
"@kubernetes/client-node": "0.21.0"
},
"devDependencies": {
+13
View File
@@ -3,6 +3,19 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
### Features
* 通过插件配置懒加载依赖,动态加载第三方依赖包,精简安装镜像大小 ([01568ca](https://github.com/certd/certd/commit/01568ca1489069046b5a89ebdd4ced2f7f6ddf93))
### Performance Improvements
* 【破坏性更新】 证书压缩包不再生成文件存储,而是实时打包下载,证书申请插件不再输出certZip ([7cff1a9](https://github.com/certd/certd/commit/7cff1a98424120585205889874b3ef4956a30583))
* 优化用户体验,首次访问时弹出邮箱账号绑定用以初始化账号 ([608cc2a](https://github.com/certd/certd/commit/608cc2a81ff0b4872c9fe11ed9c9c0b4b90a12a3))
* 支持全自动匹配部署宝塔网站证书 ([4dff48e](https://github.com/certd/certd/commit/4dff48e807c32a7623ec9206cf39c88e88f89f6a))
* dns默认ipv4first ([194463b](https://github.com/certd/certd/commit/194463bea9e797315aa7a724f4b2930701570419))
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
**Note:** Version bump only for package @certd/lib-server
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/lib-server",
"version": "1.41.4",
"version": "1.42.0",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -29,11 +29,11 @@
],
"license": "AGPL",
"dependencies": {
"@certd/acme-client": "^1.41.4",
"@certd/basic": "^1.41.4",
"@certd/pipeline": "^1.41.4",
"@certd/plugin-lib": "^1.41.4",
"@certd/plus-core": "^1.41.4",
"@certd/acme-client": "^1.42.0",
"@certd/basic": "^1.42.0",
"@certd/pipeline": "^1.42.0",
"@certd/plugin-lib": "^1.42.0",
"@certd/plus-core": "^1.42.0",
"@midwayjs/cache": "3.14.0",
"@midwayjs/core": "3.20.11",
"@midwayjs/i18n": "3.20.13",
@@ -49,8 +49,6 @@
"typeorm": "^0.3.20"
},
"devDependencies": {
"mwts": "^1.3.0",
"mwtsc": "^1.4.0",
"@types/chai": "^4.3.12",
"@types/mocha": "^10.0.6",
"@types/node": "^18",
@@ -62,6 +60,8 @@
"eslint-plugin-prettier": "^5.1.3",
"esmock": "^2.7.5",
"mocha": "^10.6.0",
"mwts": "^1.3.0",
"mwtsc": "^1.4.0",
"prettier": "3.3.3",
"rimraf": "^5.0.5",
"ts-node": "^10.9.2",
@@ -1,9 +1,8 @@
import { ApplicationContext, Inject } from '@midwayjs/core';
import type {IMidwayContainer} from '@midwayjs/core';
import * as koa from '@midwayjs/koa';
import { Constants } from './constants.js';
import { isEnterprise } from './mode.js';
import { ApplicationContext, Inject } from "@midwayjs/core";
import type { IMidwayContainer } from "@midwayjs/core";
import * as koa from "@midwayjs/koa";
import { Constants } from "./constants.js";
import { isEnterprise } from "./mode.js";
export abstract class BaseController {
@Inject()
@@ -41,7 +40,7 @@ export abstract class BaseController {
getUserId() {
const userId = this.ctx.user?.id;
if (userId == null) {
throw new Error('Token已过期');
throw new Error("Token已过期");
}
return userId;
}
@@ -49,7 +48,7 @@ export abstract class BaseController {
getLoginUser() {
const user = this.ctx.user;
if (user == null) {
throw new Error('Token已过期');
throw new Error("Token已过期");
}
return user;
}
@@ -61,73 +60,71 @@ export abstract class BaseController {
}
}
async getProjectId(permission:string) {
async getProjectId(permission: string) {
if (!isEnterprise()) {
return undefined
return undefined;
}
let projectIdStr = this.ctx.headers["project-id"] as string;
if (!projectIdStr){
if (!projectIdStr) {
projectIdStr = this.ctx.request.query["projectId"] as string;
}
if (!projectIdStr) {
//这里必须抛异常,否则可能会有权限问题
throw new Error("projectId 不能为空")
throw new Error("projectId 不能为空");
}
const userId = this.getUserId()
const projectId = parseInt(projectIdStr)
await this.checkProjectPermission(userId, projectId,permission)
const userId = this.getUserId();
const projectId = parseInt(projectIdStr);
await this.checkProjectPermission(userId, projectId, permission);
return projectId;
}
async getProjectUserId(permission:string){
let userId = this.getUserId()
const projectId = await this.getProjectId(permission)
if(projectId){
userId = -1 // 企业管理模式下,用户id固定-1
async getProjectUserId(permission: string) {
let userId = this.getUserId();
const projectId = await this.getProjectId(permission);
if (projectId) {
userId = -1; // 企业管理模式下,用户id固定-1
}
return {
projectId,userId
}
projectId,
userId,
};
}
async getProjectUserIdRead(){
return await this.getProjectUserId("read")
async getProjectUserIdRead() {
return await this.getProjectUserId("read");
}
async getProjectUserIdWrite(){
return await this.getProjectUserId("write")
async getProjectUserIdWrite() {
return await this.getProjectUserId("write");
}
async getProjectUserIdAdmin(){
return await this.getProjectUserId("admin")
async getProjectUserIdAdmin() {
return await this.getProjectUserId("admin");
}
async checkProjectPermission(userId: number, projectId: number,permission:string) {
const projectService:any = await this.applicationContext.getAsync("projectService");
await projectService.checkPermission({userId,projectId,permission})
async checkProjectPermission(userId: number, projectId: number, permission: string) {
const projectService: any = await this.applicationContext.getAsync("projectService");
await projectService.checkPermission({ userId, projectId, permission });
}
/**
*
*
* @param service
* @param id
* @param id
*/
async checkOwner(service:any,id:number,permission:string,allowAdmin:boolean = false){
let { projectId,userId } = await this.getProjectUserId(permission)
const authService:any = await this.applicationContext.getAsync("authService");
async checkOwner(service: any, id: number, permission: string, allowAdmin: boolean = false) {
const { projectId, userId } = await this.getProjectUserId(permission);
const authService: any = await this.applicationContext.getAsync("authService");
if (projectId) {
await authService.checkProjectId(service, id, projectId);
}else{
if(userId === Constants.systemUserId){
} else {
if (userId === Constants.systemUserId) {
//系统级别,不检查权限
}else{
if(allowAdmin){
} else {
if (allowAdmin) {
await authService.checkUserIdButAllowAdmin(this.ctx, service, id);
}else{
await authService.checkUserId( service, id, userId);
} else {
await authService.checkUserId(service, id, userId);
}
}
}
return {projectId,userId}
return { projectId, userId };
}
}
@@ -1,10 +1,10 @@
import { PermissionException, ValidateException } from './exception/index.js';
import { EntityTarget, FindOneOptions, In, Repository, SelectQueryBuilder } from 'typeorm';
import { Inject } from '@midwayjs/core';
import { TypeORMDataSourceManager } from '@midwayjs/typeorm';
import { EntityManager } from 'typeorm/entity-manager/EntityManager.js';
import { FindManyOptions } from 'typeorm';
import { Constants } from './constants.js';
import { PermissionException, ValidateException } from "./exception/index.js";
import { EntityTarget, FindOneOptions, In, Repository, SelectQueryBuilder } from "typeorm";
import { Inject } from "@midwayjs/core";
import { TypeORMDataSourceManager } from "@midwayjs/typeorm";
import { EntityManager } from "typeorm/entity-manager/EntityManager.js";
import { FindManyOptions } from "typeorm";
import { Constants } from "./constants.js";
export type PageReq<T = any> = {
page?: { offset: number; limit: number };
@@ -34,7 +34,7 @@ export abstract class BaseService<T> {
abstract getRepository(): Repository<T>;
async transaction(callback: (entityManager: EntityManager) => Promise<any>) {
const dataSource = this.dataSourceManager.getDataSource('default');
const dataSource = this.dataSourceManager.getDataSource("default");
return await dataSource.transaction(callback as any);
}
@@ -52,11 +52,11 @@ export abstract class BaseService<T> {
if (ctx.manager) {
return ctx.manager.getRepository(entity);
}
const dataSource = this.dataSourceManager.getDataSource('default');
const dataSource = this.dataSourceManager.getDataSource("default");
return dataSource.getRepository(entity);
}
protected buildUserProjectQuery(userId: number, projectId?: number) {
public buildUserProjectQuery(userId: number, projectId?: number) {
const query: { userId: number; projectId?: number; [key: string]: any } = {
userId,
};
@@ -73,7 +73,7 @@ export abstract class BaseService<T> {
*/
async info(id, infoIgnoreProperty?): Promise<T | null> {
if (!id) {
throw new ValidateException('id不能为空');
throw new ValidateException("id不能为空");
}
const info = await this.getRepository().findOneBy({ id } as any);
if (info && infoIgnoreProperty) {
@@ -119,18 +119,18 @@ export abstract class BaseService<T> {
...where,
});
await this.modifyAfter(idArr);
return ids
return ids;
}
resolveIdArr(ids: string | any[]) {
if (!ids) {
throw new ValidateException('ids不能为空');
throw new ValidateException("ids不能为空");
}
if (typeof ids === 'string') {
return ids.split(',');
} else if(!Array.isArray(ids)){
if (typeof ids === "string") {
return ids.split(",");
} else if (!Array.isArray(ids)) {
return [ids];
}else {
} else {
return ids;
}
}
@@ -147,7 +147,7 @@ export abstract class BaseService<T> {
*
* @param param
*/
async add(param: any) {
async add(param: any): Promise<{ id: number; [key: string]: any }> {
const now = new Date();
param.createTime = now;
param.updateTime = now;
@@ -163,7 +163,7 @@ export abstract class BaseService<T> {
* @param param
*/
async update(param: any) {
if (!param.id) throw new ValidateException('id 不能为空');
if (!param.id) throw new ValidateException("id 不能为空");
param.updateTime = new Date();
await this.addOrUpdate(param);
await this.modifyAfter(param);
@@ -201,10 +201,10 @@ export abstract class BaseService<T> {
}
private buildListQuery(listReq: ListReq<T>) {
const { query, sort, buildQuery,select } = listReq;
const qb = this.getRepository().createQueryBuilder('main');
const { query, sort, buildQuery, select } = listReq;
const qb = this.getRepository().createQueryBuilder("main");
if (select) {
qb.setFindOptions({select});
qb.setFindOptions({ select });
}
if (query) {
const keys = Object.keys(query);
@@ -223,10 +223,10 @@ export abstract class BaseService<T> {
}
});
if (found) {
qb.addOrderBy('main.' + sort.prop, sort.asc ? 'ASC' : 'DESC');
qb.addOrderBy("main." + sort.prop, sort.asc ? "ASC" : "DESC");
}
}
qb.addOrderBy('id', 'DESC');
qb.addOrderBy("id", "DESC");
//自定义query
if (buildQuery) {
buildQuery(qb);
@@ -243,12 +243,12 @@ export abstract class BaseService<T> {
return await qb.getMany();
}
async checkUserId(ids: number | number[] = 0, userId: number, userKey = 'userId') {
async checkUserId(ids: number | number[] = 0, userId: number, userKey = "userId") {
if (ids == null) {
throw new ValidateException('id不能为空');
throw new ValidateException("id不能为空");
}
if (userId == null) {
throw new ValidateException('userId不能为空');
throw new ValidateException("userId不能为空");
}
if (!Array.isArray(ids)) {
ids = [ids];
@@ -268,20 +268,20 @@ export abstract class BaseService<T> {
if (!res || res.length === ids.length) {
return;
}
throw new PermissionException('权限不足');
throw new PermissionException("权限不足");
}
filterIds(ids: any[]) {
filterIds(ids: any[]) {
if (!ids) {
throw new ValidateException('ids不能为空');
throw new ValidateException("ids不能为空");
}
return ids.filter((item) => {
return item!=null && item != ""
return ids.filter(item => {
return item != null && item != "";
});
}
async batchDelete(ids: number[], userId: number,projectId?:number) {
async batchDelete(ids: number[], userId: number, projectId?: number) {
ids = this.filterIds(ids);
if(userId!=null){
if (userId != null) {
const userProjectQuery = this.buildUserProjectQuery(userId, projectId);
const list = await this.getRepository().find({
where: {
@@ -289,9 +289,9 @@ export abstract class BaseService<T> {
id: In(ids),
...userProjectQuery,
},
})
});
// @ts-ignore
ids = list.map(item => item.id)
ids = list.map(item => item.id);
}
await this.delete(ids);
@@ -300,19 +300,18 @@ export abstract class BaseService<T> {
async findOne(options: FindOneOptions<T>) {
return await this.getRepository().findOne(options);
}
}
export function checkUserProjectParam(userId: number, projectId: number) {
if (projectId != null ){
if( userId !== Constants.enterpriseUserId) {
throw new ValidateException('userId projectId 错误');
if (projectId != null) {
if (userId !== Constants.enterpriseUserId) {
throw new ValidateException("userId projectId 错误");
}
return true
}else{
if( userId != null) {
return true
return true;
} else {
if (userId != null) {
return true;
}
throw new ValidateException('userId不能为空');
throw new ValidateException("userId不能为空");
}
}
@@ -1,9 +1,9 @@
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { AppKey, PlusRequestService } from '@certd/plus-core';
import { cache, http, HttpRequestConfig, logger } from '@certd/basic';
import { SysInstallInfo, SysLicenseInfo, SysSettingsService } from '../../settings/index.js';
import { merge } from 'lodash-es';
import fs from 'fs';
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { AppKey, PlusRequestService } from "@certd/plus-core";
import { cache, http, HttpRequestConfig, logger } from "@certd/basic";
import { SysInstallInfo, SysLicenseInfo, SysSettingsService } from "../../settings/index.js";
import { merge } from "lodash-es";
import fs from "fs";
@Provide("plusService")
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class PlusService {
@@ -54,9 +54,9 @@ export class PlusService {
await plusRequestService.verify({ license: licenseInfo.license });
}
async bindUrl(url: string, url2?:string) {
async bindUrl(url: string, url2?: string) {
const plusRequestService = await this.getPlusRequestService();
const res = await plusRequestService.bindUrl(url,url2);
const res = await plusRequestService.bindUrl(url, url2);
this.plusRequestService = null;
return res;
}
@@ -66,7 +66,7 @@ export class PlusService {
const licenseInfo: SysLicenseInfo = await this.sysSettingsService.getSetting(SysLicenseInfo);
if (!licenseInfo.license) {
await plusRequestService.register();
logger.info('站点注册成功');
logger.info("站点注册成功");
this.plusRequestService = null;
}
}
@@ -74,8 +74,8 @@ export class PlusService {
async userPreBind(userId: number) {
const plusRequestService = await this.getPlusRequestService();
await plusRequestService.requestWithoutSign({
url: '/activation/subject/preBind',
method: 'POST',
url: "/activation/subject/preBind",
method: "POST",
data: {
userId,
appKey: AppKey,
@@ -91,9 +91,9 @@ export class PlusService {
if (attachments.length > 0) {
const newAttachments: any[] = [];
attachments.forEach((item: any) => {
const name = item.filename || item.path.split('/').pop();
const name = item.filename || item.path.split("/").pop();
const body = item.content || fs.readFileSync(item.path);
const bodyBase64 = Buffer.from(body).toString('base64');
const bodyBase64 = Buffer.from(body).toString("base64");
item = {
name,
body: bodyBase64,
@@ -104,7 +104,7 @@ export class PlusService {
}
await plusRequestService.request({
url: '/activation/emailSend',
url: "/activation/emailSend",
data: {
subject: email.subject,
to: email.receivers,
@@ -116,7 +116,7 @@ export class PlusService {
}
async getAccessToken() {
const cacheKey = 'certd:subject:access_token';
const cacheKey = "certd:subject:access_token";
const token = cache.get(cacheKey);
if (token) {
return token;
@@ -131,15 +131,15 @@ export class PlusService {
return res.accessToken;
}
async getVipTrial(vipType= "plus") {
async getVipTrial(vipType = "plus") {
await this.register();
const plusRequestService = await this.getPlusRequestService();
const res = await plusRequestService.request({
url: '/activation/subject/vip/trialGet',
method: 'POST',
data:{
vipType
}
url: "/activation/subject/vip/trialGet",
method: "POST",
data: {
vipType,
},
});
if (res.license) {
await this.updateLicense(res.license);
@@ -147,14 +147,14 @@ export class PlusService {
duration: res.duration,
};
} else {
throw new Error('您已经领取过VIP试用了');
throw new Error("您已经领取过VIP试用了");
}
}
async getTodayOrderCount () {
async getTodayOrderCount() {
await this.register();
const plusRequestService = await this.getPlusRequestService();
return await plusRequestService.getOrderCount()
return await plusRequestService.getOrderCount();
}
async requestWithToken(config: HttpRequestConfig) {
@@ -162,7 +162,7 @@ export class PlusService {
const token = await this.getAccessToken();
merge(config, {
baseURL: plusRequestService.getBaseURL(),
method: 'post',
method: "post",
headers: {
Authorization: `Berear ${token}`,
},
@@ -1,33 +1,33 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
/**
*/
@Entity('sys_settings')
@Entity("sys_settings")
export class SysSettingsEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ comment: 'key', length: 100 })
@Column({ comment: "key", length: 100 })
key: string;
@Column({ comment: '名称', length: 100 })
@Column({ comment: "名称", length: 100 })
title: string;
@Column({ name: 'setting', comment: '设置', length: 1024, nullable: true })
@Column({ name: "setting", comment: "设置", length: 1024, nullable: true })
setting: string;
// public 公开读,私有写, private 私有读,私有写
@Column({ name: 'access', comment: '访问权限' })
@Column({ name: "access", comment: "访问权限" })
access: string;
@Column({
name: 'create_time',
comment: '创建时间',
default: () => 'CURRENT_TIMESTAMP',
name: "create_time",
comment: "创建时间",
default: () => "CURRENT_TIMESTAMP",
})
createTime: Date;
@Column({
name: 'update_time',
comment: '修改时间',
default: () => 'CURRENT_TIMESTAMP',
name: "update_time",
comment: "修改时间",
default: () => "CURRENT_TIMESTAMP",
})
updateTime: Date;
}
@@ -1,19 +1,19 @@
import { cloneDeep } from 'lodash-es';
import { cloneDeep } from "lodash-es";
export class BaseSettings {
static __key__: string;
static __title__: string;
static __access__ = 'private';
static __access__ = "private";
static getCacheKey() {
return 'settings.' + this.__key__;
return "settings." + this.__key__;
}
}
export class SysPublicSettings extends BaseSettings {
static __key__ = 'sys.public';
static __title__ = '系统公共设置';
static __access__ = 'public';
static __key__ = "sys.public";
static __title__ = "系统公共设置";
static __access__ = "public";
registerEnabled = false;
userValidTimeEnabled?: boolean = false;
@@ -34,19 +34,15 @@ export class SysPublicSettings extends BaseSettings {
aiChatEnabled = true;
homePageEnabled = true;
//验证码是否开启
captchaEnabled = false;
//验证码类型
captchaType?: string;
captchaAddonId?: number;
//流水线是否启用有效期
pipelineValidTimeEnabled?: boolean = false;
//证书域名添加到监控
certDomainAddToMonitorEnabled?: boolean = false;
@@ -60,12 +56,15 @@ export class SysPublicSettings extends BaseSettings {
// 第三方OAuth配置
oauthEnabled?: boolean = false;
oauthProviders: Record<string, {
type: string;
title: string;
addonId: number;
icon?: string;
}> = {};
oauthProviders: Record<
string,
{
type: string;
title: string;
addonId: number;
icon?: string;
}
> = {};
notice?: string;
@@ -73,40 +72,37 @@ export class SysPublicSettings extends BaseSettings {
}
export class SysPrivateSettings extends BaseSettings {
static __title__ = '系统私有设置';
static __access__ = 'private';
static __key__ = 'sys.private';
static __title__ = "系统私有设置";
static __access__ = "private";
static __key__ = "sys.private";
jwtKey?: string;
encryptSecret?: string;
httpsProxy? = '';
httpProxy? = '';
noProxy? = '';
commonHeaders?: string = '';
httpsProxy? = "";
httpProxy? = "";
noProxy? = "";
commonHeaders?: string = "";
reverseProxies?: Record<string, string> = {};
dnsResultOrder? = '';
dnsResultOrder? = "";
commonCnameEnabled?: boolean = true;
httpRequestTimeout?: number = 30;
pipelineMaxRunningCount?: number;
environmentVars?: string = '';
environmentVars?: string = "";
acmeWalkFromAuthoritative?: boolean = true;
sms?: {
type?: string;
config?: any;
} = {
type: 'aliyun',
config: {},
};
type: "aliyun",
config: {},
};
removeSecret() {
const clone = cloneDeep(this);
@@ -117,9 +113,9 @@ export class SysPrivateSettings extends BaseSettings {
}
export class SysInstallInfo extends BaseSettings {
static __title__ = '系统安装信息';
static __key__ = 'sys.install';
static __access__ = 'private';
static __title__ = "系统安装信息";
static __key__ = "sys.install";
static __access__ = "private";
installTime?: number;
siteId?: string;
bindUserId?: number;
@@ -130,21 +126,20 @@ export class SysInstallInfo extends BaseSettings {
}
export class SysLicenseInfo extends BaseSettings {
static __title__ = '授权许可信息';
static __key__ = 'sys.license';
static __access__ = 'private';
static __title__ = "授权许可信息";
static __key__ = "sys.license";
static __access__ = "private";
license?: string;
}
export type EmailTemplate = {
addonId?: number;
}
};
export class SysEmailConf extends BaseSettings {
static __title__ = '邮箱配置';
static __key__ = 'sys.email';
static __access__ = 'private';
static __title__ = "邮箱配置";
static __key__ = "sys.email";
static __access__ = "private";
host: string;
port: number;
@@ -160,18 +155,18 @@ export class SysEmailConf extends BaseSettings {
sender: string;
usePlus?: boolean;
templates:{
registerCode?: EmailTemplate,
forgotPassword?: EmailTemplate,
pipelineResult?: EmailTemplate,
common?: EmailTemplate,
}
templates: {
registerCode?: EmailTemplate;
forgotPassword?: EmailTemplate;
pipelineResult?: EmailTemplate;
common?: EmailTemplate;
};
}
export class SysSiteInfo extends BaseSettings {
static __title__ = '站点信息';
static __key__ = 'sys.site';
static __access__ = 'public';
static __title__ = "站点信息";
static __key__ = "sys.site";
static __access__ = "public";
title?: string;
slogan?: string;
logo?: string;
@@ -179,9 +174,9 @@ export class SysSiteInfo extends BaseSettings {
}
export class SysSecretBackup extends BaseSettings {
static __title__ = '密钥信息备份';
static __key__ = 'sys.secret.backup';
static __access__ = 'private';
static __title__ = "密钥信息备份";
static __key__ = "sys.secret.backup";
static __access__ = "private";
siteId?: string;
encryptSecret?: string;
}
@@ -190,9 +185,9 @@ export class SysSecretBackup extends BaseSettings {
*
*/
export class SysSecret extends BaseSettings {
static __title__ = '密钥信息';
static __key__ = 'sys.secret';
static __access__ = 'private';
static __title__ = "密钥信息";
static __key__ = "sys.secret";
static __access__ = "private";
siteId?: string;
encryptSecret?: string;
}
@@ -215,9 +210,9 @@ export type MenuItem = {
children?: MenuItem[];
};
export class SysHeaderMenus extends BaseSettings {
static __title__ = '顶部菜单';
static __key__ = 'sys.header.menus';
static __access__ = 'public';
static __title__ = "顶部菜单";
static __key__ = "sys.header.menus";
static __access__ = "public";
menus: MenuItem[];
}
@@ -228,9 +223,9 @@ export type PaymentItem = {
};
export class SysPaymentSetting extends BaseSettings {
static __title__ = '支付设置';
static __key__ = 'sys.payment';
static __access__ = 'private';
static __title__ = "支付设置";
static __key__ = "sys.payment";
static __access__ = "private";
yizhifu?: PaymentItem = { enabled: false };
@@ -240,9 +235,9 @@ export class SysPaymentSetting extends BaseSettings {
}
export class SysSuiteSetting extends BaseSettings {
static __title__ = '套餐设置';
static __key__ = 'sys.suite';
static __access__ = 'private';
static __title__ = "套餐设置";
static __key__ = "sys.suite";
static __access__ = "private";
enabled: boolean = false;
@@ -257,26 +252,25 @@ export class SysSuiteSetting extends BaseSettings {
}
export class SysAutoFixSetting extends BaseSettings {
static __title__ = '自动修复记录';
static __key__ = 'sys.auto.fix';
static __access__ = 'private';
static __title__ = "自动修复记录";
static __key__ = "sys.auto.fix";
static __access__ = "private";
fixed: Record<string, boolean> = {};
}
export type SiteHidden = {
enabled: boolean;
openPath?: string;
//md5 hash 两次后保存
openPassword?: string;
autoHiddenTimes?: number;
hiddenOpenApi?: boolean
hiddenOpenApi?: boolean;
};
export class SysSafeSetting extends BaseSettings {
static __title__ = '站点安全设置';
static __key__ = 'sys.safe';
static __access__ = 'private';
static __title__ = "站点安全设置";
static __key__ = "sys.safe";
static __access__ = "private";
// 站点隐藏
hidden: SiteHidden = {
@@ -35,7 +35,7 @@ export class AccessService extends BaseService<AccessEntity> {
return res;
}
async add(param) {
async add(param: any): Promise<{ id: number; [key: string]: any }> {
let oldEntity = null;
if (param._copyFrom) {
oldEntity = await this.info(param._copyFrom);
@@ -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.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
### Bug Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/midway-flyway-js",
"version": "1.41.4",
"version": "1.42.0",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -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.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
**Note:** Version bump only for package @certd/plugin-cert
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
**Note:** Version bump only for package @certd/plugin-cert
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-cert",
"private": false,
"version": "1.41.4",
"version": "1.42.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -20,7 +20,7 @@
"lint": "eslint --fix"
},
"dependencies": {
"@certd/plugin-lib": "^1.41.4"
"@certd/plugin-lib": "^1.42.0"
},
"devDependencies": {
"@types/chai": "^4.3.12",
+1 -1
View File
@@ -1,5 +1,5 @@
{
"extension": ["ts"],
"spec": "test/**/*.test.ts",
"require": "ts-node/register"
"node-option": ["loader=ts-node/esm", "no-warnings"]
}
+15
View File
@@ -3,6 +3,21 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
### Bug Fixes
* 修复jdk证书格式的问题 ([260f5ae](https://github.com/certd/certd/commit/260f5ae777b83493b0c578fe30fd00ec0c873226))
### Features
* 通过插件配置懒加载依赖,动态加载第三方依赖包,精简安装镜像大小 ([01568ca](https://github.com/certd/certd/commit/01568ca1489069046b5a89ebdd4ced2f7f6ddf93))
### Performance Improvements
* 【破坏性更新】 证书压缩包不再生成文件存储,而是实时打包下载,证书申请插件不再输出certZip ([7cff1a9](https://github.com/certd/certd/commit/7cff1a98424120585205889874b3ef4956a30583))
* 新增橙域网络(asia-isp) CDN证书部署插件 ([b48831e](https://github.com/certd/certd/commit/b48831e60b0059bef7ef9a34ab61c9dd2f684641))
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
**Note:** Version bump only for package @certd/plugin-lib
+10 -10
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-lib",
"private": false,
"version": "1.41.4",
"version": "1.42.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -10,25 +10,23 @@
"before-build": "rimraf dist && rimraf tsconfig.tsbuildinfo && rimraf .rollup.cache",
"build": "npm run before-build && tsc -p tsconfig.build.json --skipLibCheck",
"dev-build": "npm run build",
"build3": "rollup -c",
"build2": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"test:unit": "cross-env NODE_ENV=unittest mocha --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
"test:unit": "cross-env NODE_ENV=unittest mocha",
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch",
"format": "prettier --write src",
"lint": "eslint --fix"
},
"dependencies": {
"@certd/acme-client": "^1.41.4",
"@certd/basic": "^1.41.4",
"@certd/pipeline": "^1.41.4",
"@certd/acme-client": "^1.42.0",
"@certd/basic": "^1.42.0",
"@certd/pipeline": "^1.42.0",
"dayjs": "^1.11.7",
"jszip": "^3.10.1",
"lodash-es": "^4.17.21",
"psl": "^1.15.0"
"psl": "^1.15.0",
"punycode.js": "^2.3.1"
},
"devDependencies": {
"rimraf": "^5.0.5",
"@types/chai": "^4.3.12",
"@types/mocha": "^10.0.6",
"@typescript-eslint/eslint-plugin": "^8.26.1",
@@ -40,7 +38,9 @@
"eslint-plugin-prettier": "^5.1.3",
"esmock": "^2.7.5",
"mocha": "^10.6.0",
"node-forge": "^1.3.1",
"prettier": "3.3.3",
"rimraf": "^5.0.5",
"ts-node": "^10.9.2",
"tslib": "^2.8.1",
"typescript": "^5.4.2"
@@ -6,6 +6,7 @@ import cryptoLib from "crypto";
import { ILogger } from "@certd/basic";
import dayjs from "dayjs";
import { uniq } from "lodash-es";
import JSZip from "jszip";
export interface ICertInfoGetter {
getByPipelineId: (pipelineId: number) => Promise<CertInfo>;
@@ -280,9 +281,14 @@ export class CertReader {
return `${prefix}_${domain}_${timeStr}.${suffix}`;
}
buildCertName(prefix: string = "") {
buildCertName(prefix: string = "", useHash: boolean = false) {
let domain = this.getMainDomain();
domain = domain.replaceAll(".", "_").replaceAll("*", "_");
if (useHash) {
const domains = JSON.stringify(this.getAllDomains());
const hash = cryptoLib.createHash("md5").update(domains).digest("hex").slice(0, 16);
return `${prefix}_${domain}_${hash}`;
}
return `${prefix}_${domain}_${dayjs().format("YYYYMMDDHHmmssSSS")}`;
}
@@ -293,7 +299,73 @@ export class CertReader {
return name + "_" + dayjs().format("YYYYMMDDHHmmssSSS");
}
static buildCertName(cert: CertInfo) {
return new CertReader(cert).buildCertName();
static buildCertName(cert: CertInfo, useHash: boolean = false) {
return new CertReader(cert).buildCertName("", useHash);
}
async buildZip(): Promise<Buffer> {
const cert = this.cert;
const zip = new JSZip();
if (cert.crt) {
zip.file("证书.pem", cert.crt);
}
if (cert.key) {
zip.file("私钥.pem", cert.key);
}
if (cert.ic) {
zip.file("中间证书.pem", cert.ic);
}
if (cert.crt) {
zip.file("cert.crt", cert.crt);
}
if (cert.key) {
zip.file("cert.key", cert.key);
}
if (cert.ic) {
zip.file("intermediate.crt", cert.ic);
}
if (cert.oc) {
zip.file("origin.crt", cert.oc);
}
if (cert.one) {
zip.file("one.pem", cert.one);
}
if (cert.p7b) {
zip.file("cert.p7b", cert.p7b);
}
if (cert.pfx) {
zip.file("cert.pfx", Buffer.from(cert.pfx, "base64"));
}
if (cert.der) {
zip.file("cert.der", Buffer.from(cert.der, "base64"));
}
if (cert.jks) {
zip.file("cert.jks", Buffer.from(cert.jks, "base64"));
}
zip.file(
"说明.txt",
`证书文件说明
cert.crtpem格式
cert.keypem格式
intermediate.crtpem格式
origin.crtpem格式
one.pem pem格式crt正文+key正文
cert.pfxpfx格式证书文件iis服务器使用
cert.derder格式证书文件
cert.jksjks格式证书文件java服务器使用
`
);
return zip.generateAsync({ type: "nodebuffer" });
}
buildZipFilename(prefix = "cert"): string {
let domain = this.getMainDomain();
domain = domain.replaceAll(".", "_").replaceAll("*", "_");
const timeStr = dayjs().format("YYYYMMDDHHmmss");
return `${prefix}_${domain}_${timeStr}.zip`;
}
}
@@ -0,0 +1,109 @@
/// <reference types="mocha" />
/// <reference types="node" />
import assert from "node:assert/strict";
import { CertReader } from "../src/cert/cert-reader.js";
import type { CertInfo } from "../src/cert/cert-reader.js";
// @ts-ignore
import forge from "node-forge";
/**
* Generate a minimal self-signed X.509 cert + key in PEM format for testing.
*/
function createSelfSignedCert(commonName: string): { crt: string; key: string } {
const keypair = forge.pki.rsa.generateKeyPair(2048);
const cert = forge.pki.createCertificate();
cert.publicKey = keypair.publicKey;
cert.serialNumber = "01";
cert.validFrom = new Date("2025-01-01").toISOString();
cert.validTo = new Date("2026-01-01").toISOString();
const attrs = [{ name: "commonName", value: commonName }];
cert.setSubject(attrs);
cert.setIssuer(attrs);
cert.sign(keypair.privateKey, forge.md.sha256.create());
return {
crt: forge.pki.certificateToPem(cert),
key: forge.pki.privateKeyToPem(keypair.privateKey),
};
}
const testCert = createSelfSignedCert("example.com");
const testCertIc = createSelfSignedCert("intermediate.ca");
const mockCertInfo: CertInfo = {
crt: testCert.crt + "\n" + testCertIc.crt,
key: testCert.key,
oc: testCert.crt,
ic: testCertIc.crt,
one: testCert.crt + "\n" + testCert.key,
p7b: "PKCS7 test content",
pfx: Buffer.from("fake-pfx-data").toString("base64"),
der: Buffer.from("fake-der-data").toString("base64"),
jks: Buffer.from("fake-jks-data").toString("base64"),
};
describe("CertReader.buildZip", () => {
it("returns a non-empty Buffer", async () => {
const reader = new CertReader(mockCertInfo);
const buf = await reader.buildZip();
assert.ok(Buffer.isBuffer(buf));
assert.ok(buf.length > 0);
});
it("produces a valid zip containing expected files", async () => {
const reader = new CertReader(mockCertInfo);
const buf = await reader.buildZip();
const { default: JSZip } = await import("jszip");
const zip = await JSZip.loadAsync(buf);
assert.ok(zip.file("证书.pem"), "should contain 证书.pem");
assert.ok(zip.file("私钥.pem"), "should contain 私钥.pem");
assert.ok(zip.file("中间证书.pem"), "should contain 中间证书.pem");
assert.ok(zip.file("cert.crt"), "should contain cert.crt");
assert.ok(zip.file("cert.key"), "should contain cert.key");
assert.ok(zip.file("intermediate.crt"), "should contain intermediate.crt");
assert.ok(zip.file("origin.crt"), "should contain origin.crt");
assert.ok(zip.file("one.pem"), "should contain one.pem");
assert.ok(zip.file("cert.p7b"), "should contain cert.p7b");
assert.ok(zip.file("cert.pfx"), "should contain cert.pfx");
assert.ok(zip.file("cert.der"), "should contain cert.der");
assert.ok(zip.file("cert.jks"), "should contain cert.jks");
assert.ok(zip.file("说明.txt"), "should contain 说明.txt");
const pemContent = await zip.file("证书.pem").async("string");
assert.ok(pemContent.includes("-----BEGIN CERTIFICATE-----"));
const pfx = await zip.file("cert.pfx").async("nodebuffer");
assert.equal(pfx.toString(), "fake-pfx-data");
});
});
describe("CertReader.buildZipFilename", () => {
it("includes the main domain and timestamp", () => {
const reader = new CertReader(mockCertInfo);
const name = reader.buildZipFilename("cert");
assert.ok(name.startsWith("cert_example_com_"));
assert.ok(name.endsWith(".zip"));
const tsPart = name.replace("cert_example_com_", "").replace(".zip", "");
assert.match(tsPart, /^\d{14}$/);
});
it("uses the default prefix when not provided", () => {
const reader = new CertReader(mockCertInfo);
const name = reader.buildZipFilename();
assert.ok(name.startsWith("cert_example_com_"));
});
it("wildcard domain replaces asterisk", () => {
const wildcardCert = createSelfSignedCert("*.example.com");
const wcInfo: CertInfo = { crt: wildcardCert.crt, key: wildcardCert.key };
const reader = new CertReader(wcInfo);
const name = reader.buildZipFilename("cert");
assert.ok(name.startsWith("cert___example_com_"), "asterisk should be replaced, got: " + name);
assert.ok(!name.includes("*"));
});
});
+17 -9
View File
@@ -1,9 +1,15 @@
# 根据目标平台选择基础镜像:amd64/arm64 用 trixie-slimarm/v7 没有 trixie-slim 发布,回退到 alpine
FROM --platform=linux/amd64 node:22-trixie-slim AS base-amd64
FROM --platform=linux/arm64 node:22-trixie-slim AS base-arm64
FROM --platform=linux/arm/v7 node:22-alpine AS base-arm-v7
ARG base_type=alpine
FROM base-${TARGETARCH}${TARGETVARIANT:+-}${TARGETVARIANT} AS builder
# 根据 base_type 参数选择基础镜像系列
FROM --platform=linux/amd64 node:22-alpine AS base-amd64-alpine
FROM --platform=linux/arm64 node:22-alpine AS base-arm64-alpine
FROM --platform=linux/arm/v7 node:22-alpine AS base-arm-v7-alpine
FROM --platform=linux/amd64 node:22-trixie-slim AS base-amd64-slim
FROM --platform=linux/arm64 node:22-trixie-slim AS base-arm64-slim
FROM --platform=linux/arm/v7 node:22-alpine AS base-arm-v7-slim
FROM base-${TARGETARCH}${TARGETVARIANT:+-}${TARGETVARIANT}-${base_type} AS builder
WORKDIR /workspace/
COPY . /workspace/
@@ -15,10 +21,14 @@ RUN cp /workspace/certd-client/dist/* /workspace/certd-server/public/ -rf
RUN cd /workspace/certd-server && pnpm install --production && npm run build-on-docker
FROM base-${TARGETARCH}${TARGETVARIANT:+-}${TARGETVARIANT}
ARG base_type=alpine
FROM base-${TARGETARCH}${TARGETVARIANT:+-}${TARGETVARIANT}-${base_type}
EXPOSE 7001
EXPOSE 7002
ARG base_type=alpine
# 根据基础镜像发行版选择包管理器
# trixie-slim -> apt-get, alpine -> apk
RUN if [ -f /etc/debian_version ]; then \
@@ -39,9 +49,7 @@ RUN if [ -f /etc/debian_version ]; then \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*; \
elif [ -f /etc/alpine-release ]; then \
apk add --no-cache \
openssl \
openjdk8-jre; \
apk add --no-cache openssl openjdk8-jre; \
else \
echo "Unsupported base image"; exit 1; \
fi
+9
View File
@@ -3,6 +3,15 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
### Performance Improvements
* 【破坏性更新】 证书压缩包不再生成文件存储,而是实时打包下载,证书申请插件不再输出certZip ([7cff1a9](https://github.com/certd/certd/commit/7cff1a98424120585205889874b3ef4956a30583))
* 优化用户体验,首次访问时弹出邮箱账号绑定用以初始化账号 ([608cc2a](https://github.com/certd/certd/commit/608cc2a81ff0b4872c9fe11ed9c9c0b4b90a12a3))
* **passkey:** passkey支持多域名rpid ([79f6586](https://github.com/certd/certd/commit/79f65868ca0f5162bbc2f935ce89abc28011d816))
* **plugin:** 在线插件编辑支持配置第三方依赖和插件依赖 ([635f069](https://github.com/certd/certd/commit/635f069012d4193cfb7cb051c96e28eec1247ca2))
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
### Bug Fixes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/ui-client",
"version": "1.41.4",
"version": "1.42.0",
"private": true,
"scripts": {
"dev": "vite --open",
@@ -105,8 +105,8 @@
"zod-defaults": "^0.1.3"
},
"devDependencies": {
"@certd/lib-iframe": "^1.41.4",
"@certd/pipeline": "^1.41.4",
"@certd/lib-iframe": "^1.42.0",
"@certd/pipeline": "^1.42.0",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12",
@@ -0,0 +1,113 @@
<template>
<div class="kv-input">
<div v-for="(item, index) of items" :key="index" class="kv-row">
<a-input :value="item.key" placeholder="名称" class="kv-key" @input="onKeyChange(index, $event)" @blur="emitValue" />
<span class="kv-sep">:</span>
<a-input :value="item.value" placeholder="版本" class="kv-value" @input="onValueChange(index, $event)" @blur="emitValue" />
<a-button type="link" danger @click="removeItem(index)">
<template #icon><DeleteOutlined /></template>
</a-button>
</div>
<a-button type="dashed" block @click="addItem">
<template #icon><PlusOutlined /></template>
添加
</a-button>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from "vue";
import { DeleteOutlined, PlusOutlined } from "@ant-design/icons-vue";
const props = defineProps<{
modelValue: Record<string, string>;
}>();
const emit = defineEmits<{
(e: "update:modelValue", value: Record<string, string>): void;
}>();
interface KvItem {
key: string;
value: string;
}
const items = ref<KvItem[]>([]);
function initItems(val: Record<string, string>) {
if (val && typeof val === "object" && !Array.isArray(val)) {
items.value = Object.entries(val).map(([k, v]) => ({
key: k,
value: v ?? "",
}));
} else {
items.value = [];
}
}
watch(
() => props.modelValue,
val => {
initItems(val);
},
{ deep: true, immediate: true }
);
function rebuildRecord(): Record<string, string> {
const record: Record<string, string> = {};
for (const item of items.value) {
const k = item.key?.trim();
if (k) {
record[k] = item.value?.trim() ?? "";
}
}
return record;
}
function emitValue() {
emit("update:modelValue", rebuildRecord());
}
function onKeyChange(index: number, event: Event) {
const target = event.target as HTMLInputElement;
items.value[index].key = target.value;
}
function onValueChange(index: number, event: Event) {
const target = event.target as HTMLInputElement;
items.value[index].value = target.value;
}
function addItem() {
items.value.push({ key: "", value: "" });
}
function removeItem(index: number) {
items.value.splice(index, 1);
emitValue();
}
</script>
<style lang="less" scoped>
.kv-input {
.kv-row {
display: flex;
align-items: center;
margin-bottom: 8px;
gap: 4px;
.kv-key {
flex: 1;
min-width: 0;
}
.kv-sep {
flex-shrink: 0;
padding: 0 2px;
color: #999;
}
.kv-value {
flex: 1;
min-width: 0;
}
}
}
</style>
@@ -26,7 +26,10 @@ export default {
store: "Store",
version: "Version",
pluginDependencies: "Plugin Dependencies",
pluginDependenciesHelper: "Dependencies to install first in format: [author/]pluginName[:version]",
pluginDependenciesHelper:
"Format: pluginType:pluginName, use * for version\nSupported: plugin:name, access:name, notification:name, dnsProvider:name, addon:subtype:name\nExample: access:AliyunAccess, plugin:DeployToAliyunCDN",
thirdPartyDependencies: "Third-party Dependencies",
thirdPartyDependenciesHelper: "Format: npmPackageName: version. Auto-installed at runtime\nExample: aliyun-sdk: ^1.0.0",
editableRunStrategy: "Editable Run Strategy",
editable: "Editable",
notEditable: "Not Editable",
@@ -70,7 +70,7 @@ export default {
confirmToggleStatus: "确定要{action}吗?",
batchDelete: "批量删除",
sourcee: "来源",
clickToToggle: "点击切换启用/禁用",
clickToToggle: "点击启用/禁用",
nickName: "昵称",
avatar: "头像",
expires: "过期",
@@ -26,7 +26,9 @@ export default {
store: "市场",
version: "版本",
pluginDependencies: "插件依赖",
pluginDependenciesHelper: "格式: [作者/]插件名[:版本],需先安装依赖插件",
pluginDependenciesHelper: "格式: 插件类型:插件名,版本号填 *\n支持: plugin:name、access:name、notification:name、dnsProvider:name、addon:subtype:name\n示例: access:AliyunAccess, plugin:DeployToAliyunCDN",
thirdPartyDependencies: "第三方依赖",
thirdPartyDependenciesHelper: "格式: npm包名: 版本号,运行时自动安装\n示例: aliyun-sdk: ^1.0.0",
editableRunStrategy: "可编辑运行策略",
editable: "可编辑",
notEditable: "不可编辑",
@@ -31,6 +31,13 @@ export function useFormDialog() {
crudOptions: {
columns: req.columns,
form: {
labelCol: {
// @ts-ignore
span: null,
style: {
width: "100px",
},
},
initialForm: req.initialForm,
wrapper: warpper,
async afterSubmit() {},
@@ -44,7 +51,7 @@ export function useFormDialog() {
};
}
const { crudOptions } = createCrudOptions();
await openCrudFormDialog({ crudOptions });
return await openCrudFormDialog({ crudOptions });
}
return {
openFormDialog,
@@ -16,8 +16,8 @@
</div>
</template>
<script>
import { defineComponent, reactive, ref, watch, inject } from "vue";
<script lang="ts">
import { defineComponent, reactive, ref, watch, inject, onMounted } from "vue";
import CertAccessModal from "./access/index.vue";
import { createAccessApi } from "../api";
import { message } from "ant-design-vue";
@@ -55,6 +55,10 @@ export default defineComponent({
type: Boolean,
default: false,
},
defaultSelect: {
type: Boolean,
default: false,
},
},
emits: ["update:modelValue", "change", "selectedChange"],
setup(props, ctx) {
@@ -158,12 +162,43 @@ export default defineComponent({
},
});
async function selectFirst(clearCurrent = false) {
if (!clearCurrent && props.modelValue) {
return;
}
const searchForm = projectStore.getSearchForm();
const query: any = {
query: {
type: props.type,
...searchForm,
},
page: { page: 1, pageSize: 1 },
sort: { prop: "id", order: "ascending" },
};
if (props.subtype) {
query.query.subtype = props.subtype;
}
const res = await api.GetList(query);
const records = res?.records || [];
if (records.length > 0) {
await emitValue(records[0].id);
}
}
onMounted(async () => {
if (!props.defaultSelect) {
return;
}
await selectFirst();
});
return {
clear,
target,
selectedId,
providerDefine,
chooseForm,
selectFirst,
};
},
});
@@ -18,6 +18,10 @@ defineProps<{
showButton: boolean;
}>();
const emit = defineEmits<{
(e: "close"): void;
}>();
let passwordFormRef = ref();
type OpenOptions = {
@@ -68,8 +72,8 @@ const passwordFormOptions: CrudOptions = {
},
async afterSubmit() {
const formData = passwordFormRef.value?.getFormData?.();
const message = formData?.init ? t("authentication.initPasswordSuccessMessage") : t("authentication.successMessage");
notification.success({ message });
const msg = formData?.init ? t("authentication.initPasswordSuccessMessage") : t("authentication.successMessage");
notification.success({ message: msg });
},
},
columns: {
@@ -84,6 +88,7 @@ const passwordFormOptions: CrudOptions = {
title: t("authentication.oldPassword"),
type: "password",
form: {
//@ts-ignore
show: compute(({ form }) => form.init !== true),
rules: [{ required: true, message: t("authentication.oldPasswordRequired") }],
},
@@ -118,16 +123,18 @@ const passwordFormOptions: CrudOptions = {
async function open(opts: OpenOptions = {}) {
const formOptions = buildFormOptions(passwordFormOptions);
formOptions.newInstance = true; //
formOptions.newInstance = true;
if (opts.init) {
formOptions.wrapper.title = t("authentication.initPasswordTitle");
}
formOptions.wrapper.onClosed = () => {
emit("close");
};
passwordFormRef.value = await openDialog(formOptions);
passwordFormRef.value.setFormData({
init: opts.init === true,
password: opts.password || "",
});
console.log(passwordFormRef.value);
}
const scope = ref({
@@ -107,6 +107,10 @@
<div class="passkey-info">
<div class="passkey-name">{{ passkey.deviceName }}</div>
<div class="passkey-meta flex items-center">
<span class="meta-item flex items-center">
<fs-icon icon="ion:globe-outline" class="meta-icon" />
{{ passkey.rpId || "-" }}
</span>
<span class="meta-item flex items-center">
<fs-icon icon="ion:calendar-outline" class="meta-icon" />
{{ formatDate(passkey.registeredAt) }}
@@ -454,6 +458,8 @@ onMounted(async () => {
}
.card-header {
background: linear-gradient(145deg, #1e1e1e, #252525);
.header-bg-gradient {
background: rgba(255, 255, 255, 0.04);
opacity: 1;
@@ -472,6 +478,7 @@ onMounted(async () => {
.detail-tag {
background: #3b3b3b;
border-color: rgba(255, 255, 255, 0.12);
color: #e5e5e5;
.tag-icon {
@@ -480,6 +487,23 @@ onMounted(async () => {
}
}
.card-title {
border-bottom-color: rgba(255, 255, 255, 0.1);
}
.binding-icon {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.22) 0%, rgba(160, 120, 234, 0.22) 100%);
}
.passkey-icon {
background: linear-gradient(135deg, rgba(17, 153, 142, 0.22) 0%, rgba(56, 239, 125, 0.22) 100%);
}
.binding-icon .icon,
.passkey-icon .icon {
color: rgba(255, 255, 255, 0.7);
}
.bindings-list {
.binding-item {
background: #2d2d2d;
@@ -138,10 +138,6 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
type: "link",
icon: "ant-design:download-outlined",
async click({ row }) {
if (!row.certFile) {
notification.error({ message: t("certd.certificateNotGenerated") });
return;
}
let url = "/api/monitor/cert/download?id=" + row.id;
if (projectStore.isEnterprise) {
url += `&projectId=${projectStore.currentProject?.id}`;
@@ -1,10 +1,10 @@
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";
import { useProjectStore } from "/@/store/project";
import { useUserStore } from "/@/store/user";
import * as api from "/@/views/certd/pipeline/api";
export function useCertViewer() {
const projectStore = useProjectStore();
@@ -29,42 +29,12 @@ export function useCertViewer() {
};
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) {
let downloadUrl = `${env.API}/pi/history/download?pipelineId=${id}&fileId=${file.id}`;
if (projectStore.isEnterprise) {
downloadUrl += `&projectId=${projectStore.currentProject?.id}`;
}
downloadUrl += `&token=${userStore.getToken}`;
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>
);
},
});
let downloadUrl = `${env.API}/pi/cert/downloadZip?id=${id}`;
if (projectStore.isEnterprise) {
downloadUrl += `&projectId=${projectStore.currentProject?.id}`;
}
downloadUrl += `&token=${userStore.getToken}`;
window.open(downloadUrl);
};
return {
viewCert,
@@ -2,22 +2,110 @@
<fs-page class="home—index bg-neutral-100 dark:bg-black">
<!-- <page-content />-->
<dashboard-user />
<change-password-button ref="changePasswordButtonRef" :show-button="false"></change-password-button>
<change-password-button ref="changePasswordButtonRef" :show-button="false" @close="checkAndSetupAccount"></change-password-button>
</fs-page>
</template>
<script lang="ts" setup>
<script lang="tsx" setup>
import DashboardUser from "./dashboard/index.vue";
import { useUserStore } from "/@/store/user";
import ChangePasswordButton from "/@/views/certd/mine/change-password-button.vue";
import { onMounted, ref } from "vue";
import { Modal } from "ant-design-vue";
import { Modal, notification } from "ant-design-vue";
import { useI18n } from "/src/locales";
import { request } from "/@/api/service";
import { useFormDialog } from "/@/use/use-dialog";
const { t } = useI18n();
const { openFormDialog } = useFormDialog();
const userStore = useUserStore();
const changePasswordButtonRef = ref();
const emailFormWrapperRef = ref<any>();
const validateEmailConfirm = async (_rule: any, value: string) => {
if (!value) {
return;
}
const formData = emailFormWrapperRef.value?.getFormData?.();
if (formData && value !== formData.email) {
throw new Error("两次输入的邮箱地址不一致");
}
};
async function checkAndSetupAccount() {
try {
const userInfo = userStore.getUserInfo as any;
if (!userInfo.needInitAccount) {
return;
}
if (userInfo.email) {
await request({
url: "/mine/accountInit",
method: "post",
});
return;
}
emailFormWrapperRef.value = await openFormDialog({
title: "绑定邮箱",
wrapper: {
width: 560,
},
initialForm: { email: "", emailConfirm: "" },
async onSubmit(form: any) {
await request({
url: "/mine/accountInit",
method: "post",
data: { email: form.email },
});
notification.success({
message: "邮箱绑定成功",
});
},
body: () => {
return <a-alert class="mb-4" message="为保证用户体验,请先绑定邮箱,初始化您的账号" type="success" show-icon></a-alert>;
},
columns: {
email: {
title: "邮箱",
type: "text",
form: {
col: { span: 24 },
component: {
placeholder: "请输入邮箱地址",
},
helper: "请输入您的邮箱",
rules: [
{ required: true, message: "请输入邮箱地址" },
{ type: "email", message: "请输入有效的邮箱地址" },
],
},
},
emailConfirm: {
title: "确认邮箱",
type: "text",
form: {
col: { span: 24 },
component: {
placeholder: "请再次输入邮箱地址",
},
helper: "请再次输入邮箱,以确认邮箱地址无误",
rules: [
{ required: true, message: "请再次输入邮箱地址" },
{ type: "email", message: "请输入有效的邮箱地址" },
{ validator: validateEmailConfirm, trigger: "blur" },
],
},
},
},
});
} catch (e) {
console.error("AcmeAccount setup failed:", e);
}
}
onMounted(() => {
if (userStore.getUserInfo.isWeak === true) {
Modal.info({
@@ -30,6 +118,9 @@ onMounted(() => {
},
okText: t("authentication.changeNow"),
});
} else {
//
checkAndSetupAccount();
}
});
</script>
@@ -7,6 +7,7 @@ import { Modal, message } from "ant-design-vue";
//@ts-ignore
import yaml from "js-yaml";
import { usePluginImport } from "./use-import";
import KvInput from "/@/components/plugins/common/kv-input.vue";
import { usePluginConfig } from "./use-config";
import { useSettingStore } from "/src/store/settings/index";
import { usePluginStore } from "/@/store/plugin";
@@ -37,6 +38,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
return res;
};
// const infoRequest = async ({ row }: AddReq) => {
// return await api.GetObj(row.id);
// };
const selectedRowKeys: Ref<any[]> = ref([]);
context.selectedRowKeys = selectedRowKeys;
@@ -66,6 +71,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
addRequest,
editRequest,
delRequest,
// infoRequest,
},
actionbar: {
buttons: {
@@ -187,6 +193,8 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
id: opts.res.id,
},
});
} else {
crudExpose.doRefresh();
}
},
},
@@ -363,10 +371,8 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
type: "text",
form: {
component: {
name: "a-select",
mode: "tags",
open: false,
allowClear: true,
name: KvInput,
vModel: "modelValue",
},
helper: t("certd.pluginDependenciesHelper"),
},
@@ -374,6 +380,20 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
show: false,
},
},
"extra.dependPackages": {
title: t("certd.thirdPartyDependencies"),
type: "text",
form: {
component: {
name: KvInput,
vModel: "modelValue",
},
helper: t("certd.thirdPartyDependenciesHelper"),
},
column: {
show: false,
},
},
"extra.showRunStrategy": {
title: t("certd.editableRunStrategy"),
type: "dict-switch",
@@ -419,18 +439,18 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
show: false,
},
valueBuilder({ row }) {
if (row.extra) {
if (typeof row.extra === "string") {
row.extra = yaml.load(row.extra);
}
},
valueResolve({ row }) {
if (row.extra) {
if (row.extra && typeof row.extra === "object") {
row.extra = yaml.dump(row.extra);
}
},
},
disabled: {
title: t("certd.enableDisable"),
title: t("certd.clickToToggle"),
type: "dict-switch",
dict: dict({
data: [
+3 -1
View File
@@ -19,6 +19,8 @@
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-this-alias": "off"
"@typescript-eslint/no-this-alias": "off",
// 允许any
"@typescript-eslint/no-unsafe-anyassignment": "off"
}
}
+24
View File
@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
### Bug Fixes
* 修复telegram - 符号转义问题 ([d5882f1](https://github.com/certd/certd/commit/d5882f16bedb09baf09ace92049b02872620f5dc))
* **aliyun:** 修复阿里云CDN/DCDN根据证书自动匹配不到证书的bug ([1ae185d](https://github.com/certd/certd/commit/1ae185d0bc356f4678bc38ca0582ce3396f82ebe))
### Features
* 通过插件配置懒加载依赖,动态加载第三方依赖包,精简安装镜像大小 ([01568ca](https://github.com/certd/certd/commit/01568ca1489069046b5a89ebdd4ced2f7f6ddf93))
### Performance Improvements
* 阿里云ESA证书部署支持SaaS模式 ([82276b5](https://github.com/certd/certd/commit/82276b53a8474a18a3d0237050907c994fc748f0))
* 【破坏性更新】 证书压缩包不再生成文件存储,而是实时打包下载,证书申请插件不再输出certZip ([7cff1a9](https://github.com/certd/certd/commit/7cff1a98424120585205889874b3ef4956a30583))
* 火山引擎点播插件支持部署到自定义源站域名 ([095791c](https://github.com/certd/certd/commit/095791cdc2b7c1f4b913b634643afec5e30fe9b0))
* 新增橙域网络(asia-isp) CDN证书部署插件 ([b48831e](https://github.com/certd/certd/commit/b48831e60b0059bef7ef9a34ab61c9dd2f684641))
* 优化阿里云API网关增加翻页查询 ([ed58ae3](https://github.com/certd/certd/commit/ed58ae3c5339e4a0238a92acfe7ea6d2f566ea28))
* 优化用户体验,首次访问时弹出邮箱账号绑定用以初始化账号 ([608cc2a](https://github.com/certd/certd/commit/608cc2a81ff0b4872c9fe11ed9c9c0b4b90a12a3))
* 优化ACME账号字段的选择提示 ([bfd3cac](https://github.com/certd/certd/commit/bfd3cacc687fc5cbc3cb2ca3cadbc140de300dc2))
* 支持全自动匹配部署宝塔网站证书 ([4dff48e](https://github.com/certd/certd/commit/4dff48e807c32a7623ec9206cf39c88e88f89f6a))
* **cert-plugin:** 调整更新天数自动减半逻辑,仅7天ip证书生效,其他情况下不减半 ([56e5524](https://github.com/certd/certd/commit/56e5524a0f4af3645d70bc3b3ec750b45ba8de10))
* **passkey:** passkey支持多域名rpid ([79f6586](https://github.com/certd/certd/commit/79f65868ca0f5162bbc2f935ce89abc28011d816))
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
### Bug Fixes
@@ -0,0 +1,6 @@
ALTER TABLE `sys_passkey` ADD COLUMN `rp_id` varchar(256) NULL;
DROP INDEX `index_passkey_passkey_id` ON `sys_passkey`;
ALTER TABLE `sys_passkey` ADD UNIQUE INDEX `index_passkey_passkey_id` (`passkey_id`);
@@ -0,0 +1,6 @@
ALTER TABLE "sys_passkey" ADD COLUMN "rp_id" varchar(256) NULL;
DROP INDEX "index_passkey_passkey_id";
CREATE UNIQUE INDEX "index_passkey_passkey_id" ON "sys_passkey" ("passkey_id");
@@ -0,0 +1,6 @@
ALTER TABLE "sys_passkey" ADD COLUMN "rp_id" varchar(256) NULL;
DROP INDEX "index_passkey_passkey_id";
CREATE UNIQUE INDEX "index_passkey_passkey_id" ON "sys_passkey" ("passkey_id");
@@ -74,7 +74,7 @@ input:
credentials链接,然后点击编辑按钮,查看Secret key和HMAC key
litessl:[litesslEAB页面](https://freessl.cn/automation/eab-manager),然后点击新增EAB
required: false
required: true
encrypt: true
mergeScript: |2-
@@ -92,7 +92,7 @@ input:
title: EAB HMAC Key
component:
placeholder: 需要EAB的颁发机构生成账号时填写
required: false
required: true
encrypt: true
mergeScript: |2-
@@ -107,7 +107,7 @@ input:
}
account:
title: ACME账号信息
title: 生成ACME账号
component:
name: refresh-input
action: GenerateAccount
@@ -118,7 +118,7 @@ input:
col:
span: 24
required: true
helper: 请生成ACME账号,账号一旦生成不允许修改
helper: 点击右边按钮生成ACME账号,账号一旦生成不允许修改
encrypt: true
mergeScript: |2-
@@ -0,0 +1,25 @@
name: asiaisp
title: 橙域网络(asia-isp)授权
desc: 橙域网络CDN API授权,用于部署证书到橙域CDN
icon: clarity:plugin-line
input:
accessKeyId:
title: AccessKeyId
component:
placeholder: 请输入 AccessKeyId
required: true
accessKeySecret:
title: AccessKeySecret
component:
placeholder: 请输入 AccessKeySecret
required: true
encrypt: true
testRequest:
title: 测试连接
component:
name: api-test
action: TestRequest
helper: 点击测试接口是否正常
pluginType: access
type: builtIn
scriptFilePath: /plugins/plugin-asiaisp/access.js
@@ -2,6 +2,8 @@ name: tencent
title: 腾讯云
icon: svg:icon-tencentcloud
order: 0
dependPackages:
tencentcloud-sdk-nodejs: ^4.1.112
input:
secretId:
title: secretId
@@ -3,6 +3,8 @@ name: tencent
title: 腾讯云验证码
desc: ''
showTest: false
dependPackages:
tencentcloud-sdk-nodejs: ^4.1.112
input:
accessId:
title: 腾讯云授权
@@ -6,7 +6,7 @@ name: AliyunDeployCertToESA
title: 阿里云-部署至ESA
icon: svg:icon-aliyun
group: aliyun
desc: 部署证书到阿里云ESA(边缘安全加速),自动删除过期证书
desc: 部署证书到阿里云ESA(边缘安全加速),支持边缘证书和SaaS证书两种模式
needPlus: false
input:
cert:
@@ -70,6 +70,20 @@ input:
type: aliyun
required: true
order: 0
deployMode:
title: 部署模式
value: edge
component:
name: a-radio-group
vModel: value
options:
- label: 边缘证书
value: edge
- label: SaaS证书
value: saas
helper: 边缘证书:将证书部署到站点的边缘节点;SaaS证书:将证书部署到站点的SaaS域名
required: true
order: 0
siteIds:
title: 站点
component:
@@ -99,6 +113,39 @@ input:
helper: 请选择要部署证书的站点
order: 0
saasDomainIds:
title: SaaS域名
component:
name: remote-select
vModel: value
mode: tags
type: plugin
action: onGetCustomHostnameList
search: false
pager: false
single: false
watches:
- certDomains
- accessId
- siteIds
- accessId
- regionId
required: false
mergeScript: |2-
return {
show: ctx.compute(({form})=>{
return form.deployMode === 'saas'
}),
component:{
form: ctx.compute(({form})=>{
return form
})
},
}
helper: 请选择要部署证书的SaaS域名(SaaS证书模式下必选)
order: 0
certLimit:
title: 免费证书数量限制
value: 2
@@ -0,0 +1,77 @@
showRunStrategy: false
default:
strategy:
runStrategy: 1
name: AsiaIspDeployToCDN
title: 橙域网络-部署证书到CDN
desc: 部署证书到橙域网络(asia-isp) CDN加速域名
icon: clarity:plugin-line
group: cdn
input:
cert:
title: 域名证书
helper: 请选择前置任务输出的域名证书
component:
name: output-selector
from:
- ':cert:'
required: true
order: 0
certDomains:
title: 当前证书域名
component:
name: cert-domains-getter
mergeScript: |2-
return {
component:{
inputKey: ctx.compute(({form})=>{
return form.cert
}),
}
}
template: false
required: false
order: 0
accessId:
title: 橙域网络授权
component:
name: access-selector
type: asiaisp
required: true
order: 0
domainList:
title: 加速域名
component:
name: remote-select
vModel: value
mode: tags
type: plugin
action: onGetDomainList
search: true
pager: true
single: false
pageSize: 10
watches:
- certDomains
- accessId
- certDomains
- accessId
required: true
mergeScript: |2-
return {
component:{
form: ctx.compute(({form})=>{
return form
})
},
}
helper: 选择要部署证书的橙域CDN加速域名
order: 0
output: {}
pluginType: deploy
type: builtIn
scriptFilePath: /plugins/plugin-asiaisp/plugin-deploy-to-cdn.js
@@ -0,0 +1,52 @@
showRunStrategy: false
default:
strategy:
runStrategy: 1
name: BaotaAutoDeploySiteCert
title: 宝塔-全自动部署
icon: svg:icon-bt
group: panel
desc: 根据证书域名自动匹配宝塔站点,全自动部署SSL证书。新增加速域名自动感知,自动新增部署
runStrategy: 0
needPlus: true
input:
cert:
title: 域名证书
helper: 请选择前置任务输出的域名证书
component:
name: output-selector
from:
- ':cert:'
required: true
order: 0
certDomains:
title: 当前证书域名
component:
name: cert-domains-getter
mergeScript: |2-
return {
component:{
inputKey: ctx.compute(({form})=>{
return form.cert
}),
}
}
template: false
required: false
order: 0
accessId:
title: 宝塔授权
helper: 将自动查找证书匹配的站点,检查证书即将过期的站点并更新
component:
name: access-selector
type: baota
required: true
order: 0
output:
deployedList:
title: 已部署过的站点
pluginType: deploy
type: builtIn
scriptFilePath: /plugins/plugin-plus/baota/plugins/plugin-deploy-automatch.js
@@ -314,8 +314,11 @@ input:
component:
name: access-selector
type: acmeAccount
defaultSelect: true
required: false
helper: 请选择颁发机构对应的ACME账号
helper: |-
直接本地生成,无需外部注册
点击选择按钮->添加->填写邮箱->生成账号即可
mergeScript: |2-
return {
@@ -554,9 +557,6 @@ output:
cert:
title: 域名证书
type: cert
certZip:
title: 域名证书压缩文件
type: certZip
pluginType: deploy
type: builtIn
scriptFilePath: /plugins/plugin-cert/plugin/cert-plugin/apply.js
@@ -146,9 +146,6 @@ output:
cert:
title: 域名证书
type: cert
certZip:
title: 域名证书压缩文件
type: certZip
pluginType: deploy
type: builtIn
scriptFilePath: /plugins/plugin-cert/plugin/cert-plugin/getter/aliyun.js
@@ -168,9 +168,6 @@ output:
cert:
title: 域名证书
type: cert
certZip:
title: 域名证书压缩文件
type: certZip
pluginType: deploy
type: builtIn
scriptFilePath: /plugins/plugin-cert/plugin/cert-plugin/lego/index.js
@@ -142,9 +142,6 @@ output:
cert:
title: 域名证书
type: cert
certZip:
title: 域名证书压缩文件
type: certZip
certMd5:
title: 证书MD5
type: certMd5
@@ -83,8 +83,8 @@ input:
mode: tags
type: plugin
action: onGetGroupList
search: false
pager: false
search: true
pager: true
single: true
watches:
- certDomains
@@ -17,15 +17,6 @@ input:
- ':cert:'
required: true
order: 0
certZip:
title: 证书压缩文件
helper: 请选择前置任务输出的域名证书压缩文件
component:
name: output-selector
from:
- ':certZip:'
required: true
order: 0
email:
title: 接收邮箱
component:
@@ -8,6 +8,8 @@ needPlus: false
icon: svg:icon-tencentcloud
group: tencent
desc: 支持负载均衡、CDN、DDoS、直播、点播、Web应用防火墙、API网关、TEO、容器服务、对象存储、轻应用服务器、云原生微服务、云开发
dependPlugins:
access:tencent: '*'
input:
accessId:
title: Access授权
@@ -7,6 +7,8 @@ title: 腾讯云-部署到CDN(废弃)
icon: svg:icon-tencentcloud
group: tencent
desc: 已废弃,请使用v2版
dependPlugins:
access:tencent: '*'
input:
cert:
title: 域名证书
@@ -7,6 +7,8 @@ title: 腾讯云-部署到CLB
icon: svg:icon-tencentcloud
group: tencent
desc: 暂时只支持单向认证证书,暂时只支持通用负载均衡
dependPlugins:
access:tencent: '*'
input:
cert:
title: 域名证书
@@ -8,6 +8,10 @@ needPlus: false
icon: svg:icon-tencentcloud
group: tencent
desc: 部署到腾讯云COS源站域名证书,注意是源站域名,加速域名请使用腾讯云CDN v2插件【注意:很不稳定,需要重试很多次偶尔才能成功一次】
dependPlugins:
access:tencent: '*'
dependPackages:
cos-nodejs-sdk-v5: ^2.14.6
input:
accessId:
title: Access授权
@@ -7,6 +7,8 @@ title: 腾讯云-部署到腾讯云EO
icon: svg:icon-tencentcloud
desc: 腾讯云边缘安全加速平台EdgeOne(EO)
group: tencent
dependPlugins:
access:tencent: '*'
input:
cert:
title: 域名证书
@@ -12,6 +12,8 @@ desc: |-
1. serverless集群请使用K8S部署插件;
2. Opaque类型需要【上传到腾讯云】作为前置任务;
3. ApiServer需要开通公网访问(或者certd可访问),实际上底层仍然是通过KubeClient进行部署
dependPlugins:
access:tencent: '*'
input:
ingressClass:
title: ingress证书类型
@@ -7,6 +7,8 @@ title: 腾讯云-实例开关机
icon: svg:icon-tencentcloud
group: tencent
desc: 腾讯云实例开关机
dependPlugins:
access:tencent: '*'
needPlus: false
input:
accessId:
@@ -7,6 +7,8 @@ title: 腾讯云-删除即将过期证书
icon: svg:icon-tencentcloud
group: tencent
desc: 仅删除未使用的证书
dependPlugins:
access:tencent: '*'
needPlus: true
input:
accessId:
@@ -7,6 +7,8 @@ title: 腾讯云-部署到CDN-v2
icon: svg:icon-tencentcloud
group: tencent
desc: 推荐使用,支持CDN域名以及COS加速域名
dependPlugins:
access:tencent: '*'
input:
cert:
title: 域名证书
@@ -8,6 +8,8 @@ icon: svg:icon-tencentcloud
desc: https://console.cloud.tencent.com/live/
group: tencent
needPlus: false
dependPlugins:
access:tencent: '*'
input:
accessId:
title: Access提供者
@@ -9,6 +9,8 @@ icon: svg:icon-tencentcloud
group: tencent
needPlus: false
deprecated: 腾讯更新证书(Id不变)接口已失效,本插件已下架,请使用其他接口
dependPlugins:
access:tencent: '*'
input:
cert:
title: 域名证书
@@ -7,6 +7,8 @@ title: 腾讯云-上传证书到腾讯云
icon: svg:icon-tencentcloud
desc: 上传成功后输出:tencentCertId
group: tencent
dependPlugins:
access:tencent: '*'
input:
accessId:
title: Access授权
@@ -47,7 +47,7 @@ input:
title: 区域
helper: 选择火山引擎区域
component:
name: select
name: a-select
options:
- value: cn-north-1
label: 华北1(北京)
@@ -99,6 +99,21 @@ input:
value: play
required: true
order: 0
sourceStationType:
title: 源站类型
helper: 选择源站类型
component:
name: a-select
vModel: value
options:
- value: 1
label: 点播源站
- value: 2
label: 自定义源站
value: 1
helper: 注意:封面加速域名不支持自定义源站
required: false
order: 0
domainList:
title: 域名
component:
@@ -117,6 +132,7 @@ input:
- accessId
- spaceName
- domainType
- sourceStationType
required: true
mergeScript: |2-
@@ -3,6 +3,8 @@ title: 腾讯云EO DNS
desc: 腾讯云EO DNS解析提供者
accessType: tencent
icon: svg:icon-tencentcloud
dependPlugins:
access:tencent: '*'
pluginType: dnsProvider
type: builtIn
scriptFilePath: /plugins/plugin-tencent/dns-provider/teo-dns-provider.js
@@ -3,6 +3,8 @@ title: 腾讯云
desc: 腾讯云域名DNS解析提供者
accessType: tencent
icon: svg:icon-tencentcloud
dependPlugins:
access:tencent: '*'
pluginType: dnsProvider
type: builtIn
scriptFilePath: /plugins/plugin-tencent/dns-provider/tencent-dns-provider.js
+38 -39
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/ui-server",
"version": "1.41.4",
"version": "1.42.0",
"description": "fast-server base midway",
"private": true,
"type": "module",
@@ -41,20 +41,20 @@
"lint1": "eslint --fix"
},
"dependencies": {
"@certd/acme-client": "^1.41.4",
"@certd/basic": "^1.41.4",
"@certd/commercial-core": "^1.41.4",
"@certd/acme-client": "^1.42.0",
"@certd/basic": "^1.42.0",
"@certd/commercial-core": "^1.42.0",
"@certd/cv4pve-api-javascript": "^8.4.2",
"@certd/jdcloud": "^1.41.4",
"@certd/lib-huawei": "^1.41.4",
"@certd/lib-k8s": "^1.41.4",
"@certd/lib-server": "^1.41.4",
"@certd/midway-flyway-js": "^1.41.4",
"@certd/pipeline": "^1.41.4",
"@certd/plugin-cert": "^1.41.4",
"@certd/plugin-lib": "^1.41.4",
"@certd/plugin-plus": "^1.41.4",
"@certd/plus-core": "^1.41.4",
"@certd/jdcloud": "^1.42.0",
"@certd/lib-huawei": "^1.42.0",
"@certd/lib-k8s": "^1.42.0",
"@certd/lib-server": "^1.42.0",
"@certd/midway-flyway-js": "^1.42.0",
"@certd/pipeline": "^1.42.0",
"@certd/plugin-cert": "^1.42.0",
"@certd/plugin-lib": "^1.42.0",
"@certd/plugin-plus": "^1.42.0",
"@certd/plus-core": "^1.42.0",
"@koa/cors": "^5.0.0",
"@midwayjs/bootstrap": "3.20.11",
"@midwayjs/cache": "3.14.0",
@@ -92,6 +92,7 @@
"log4js": "^6.9.1",
"lru-cache": "^11.0.1",
"mitt": "^3.0.1",
"mwtsc": "^1.15.1",
"mysql2": "^3.14.0",
"nanoid": "^5.0.7",
"node-forge": "^1.3.1",
@@ -117,6 +118,28 @@
"whoiser": "2.0.0-beta.10",
"xml2js": "^0.6.2"
},
"devDependencies": {
"@midwayjs/mock": "3.20.11",
"@types/ali-oss": "^6.16.11",
"@types/cache-manager": "^4.0.6",
"@types/jest": "^29.5.13",
"@types/koa": "2.15.0",
"@types/lodash-es": "^4.17.12",
"@types/mocha": "^10.0.6",
"@types/node": "^18",
"@types/nodemailer": "^6.4.8",
"c8": "^10.1.2",
"cross-env": "^7.0.3",
"esmock": "^2.7.5",
"mocha": "^10.6.0",
"mwts": "^1.3.0",
"prettier": "3.3.3",
"rimraf": "^5.0.5",
"ts-node": "^10.9.2",
"tslib": "^2.8.1",
"typescript": "^5.4.2",
"why-is-node-running": "^3.2.2"
},
"lazyDependencies": {
"@alicloud/fc20230330": "^4.1.7",
"@alicloud/tea-typescript": "^1.8.0",
@@ -147,30 +170,7 @@
"@google-cloud/publicca": "^1.3.0",
"basic-ftp": "^5.0.5",
"esdk-obs-nodejs": "^3.25.6",
"qiniu": "^7.12.0",
"mwtsc": "^1.15.1"
},
"devDependencies": {
"mwts": "^1.3.0",
"@midwayjs/mock": "3.20.11",
"@types/ali-oss": "^6.16.11",
"@types/cache-manager": "^4.0.6",
"@types/jest": "^29.5.13",
"@types/koa": "2.15.0",
"@types/lodash-es": "^4.17.12",
"@types/mocha": "^10.0.6",
"@types/node": "^18",
"@types/nodemailer": "^6.4.8",
"c8": "^10.1.2",
"cross-env": "^7.0.3",
"esmock": "^2.7.5",
"mocha": "^10.6.0",
"prettier": "3.3.3",
"rimraf": "^5.0.5",
"ts-node": "^10.9.2",
"tslib": "^2.8.1",
"typescript": "^5.4.2",
"why-is-node-running": "^3.2.2"
"qiniu": "^7.12.0"
},
"engines": {
"node": ">=20.0.0"
@@ -182,7 +182,6 @@
"pnpm": {
"neverBuiltDependencies": []
},
"author": "anonymous",
"license": "MIT"
}
@@ -131,7 +131,6 @@ export class MainConfiguration {
setLogger((text: string) => {
logger.info(text);
});
logger.info("当前环境:", this.app.getEnv()); // prod
}
}
@@ -55,7 +55,7 @@ export class FileController extends BaseController {
const key = await this.fileService.saveFile(this.getUserId(), cacheKey, "public");
return this.ok({
key,
url: `/api/basic/file/download?key=${encodeURIComponent(key)}`,
url: `/api/basic/file/download?key=${encodeURIComponent(key as string)}`,
});
}
return this.ok({
@@ -1,10 +1,14 @@
import { BaseController, Constants, SysSettingsService } from "@certd/lib-server";
import { AccessGetter, AccessService, BaseController, Constants, SysSettingsService } from "@certd/lib-server";
import { ALL, Body, Controller, Inject, Post, Provide } from "@midwayjs/core";
import { PasskeyService } from "../../../modules/login/service/passkey-service.js";
import { RoleService } from "../../../modules/sys/authority/service/role-service.js";
import { UserService } from "../../../modules/sys/authority/service/user-service.js";
import { NotificationService } from "../../../modules/pipeline/service/notification-service.js";
import { newAccess } from "@certd/pipeline";
import { http, logger, utils } from "@certd/basic";
import { ApiTags } from "@midwayjs/swagger";
import { CodeService } from "../../../modules/basic/service/code-service.js";
import { EmailService } from "../../../modules/basic/service/email-service.js";
/**
*/
@@ -27,6 +31,15 @@ export class MineController extends BaseController {
@Inject()
sysSettingsService: SysSettingsService;
@Inject()
accessService: AccessService;
@Inject()
notificationService: NotificationService;
@Inject()
emailService: EmailService;
@Post("/info", { description: Constants.per.authOnly, summary: "查询用户信息" })
public async info() {
const userId = this.getUserId();
@@ -41,6 +54,17 @@ export class MineController extends BaseController {
delete user.password;
//@ts-ignore
user.needInitPassword = needInitPassword;
const { projectId } = await this.getProjectUserIdRead();
const userProjectQuery = this.accessService.buildUserProjectQuery(userId, projectId);
const existingAccess = await this.accessService.findOne({
where: { type: "acmeAccount", subtype: "letsencrypt", ...userProjectQuery },
});
if (!existingAccess) {
//@ts-ignore
user.needInitAccount = true;
}
return this.ok(user);
}
@@ -122,4 +146,58 @@ export class MineController extends BaseController {
});
return this.ok({});
}
@Post("/accountInit", { description: Constants.per.authOnly, summary: "初始化Let's Encrypt ACME账号和邮件通知" })
public async accountInit(@Body("email") email?: string) {
const { projectId, userId } = await this.getProjectUserIdWrite();
let userEmail = email;
let user: any = null;
if (!userEmail) {
user = await this.userService.info(userId);
userEmail = user.email;
}
if (!userEmail) {
return this.ok({ needEmail: true });
}
if (email) {
if (!user) {
user = await this.userService.info(userId);
}
if (!user.email) {
await this.userService.updateEmail(userId, { email: userEmail });
}
}
await this.emailService.add(userId, userEmail);
await this.notificationService.getOrCreateDefault(userEmail, userId, projectId);
const getAccessById = this.accessService.getById.bind(this.accessService);
const accessGetter = new AccessGetter(userId, projectId, getAccessById);
const accessContext = {
http,
logger,
utils,
accessService: accessGetter,
define: undefined,
} as any;
const access = await newAccess("acmeAccount", { caType: "letsencrypt", email: userEmail }, accessGetter, accessContext);
const accountJson = await access.onGenerateAccount();
await this.accessService.add({
type: "acmeAccount",
name: "Let's Encrypt",
userId,
projectId,
setting: JSON.stringify({
caType: "letsencrypt",
email: userEmail,
account: accountJson,
}),
});
return this.ok({ success: true });
}
}
@@ -69,7 +69,7 @@ export class MinePasskeyController extends BaseController {
public async getPasskeys() {
const userId = this.getUserId();
const passkeys = await this.passkeyService.find({
select: ["id", "deviceName", "registeredAt", "transports", "passkeyId", "updateTime"],
select: ["id", "deviceName", "registeredAt", "transports", "passkeyId", "rpId", "updateTime"],
where: { userId },
order: { registeredAt: "DESC" },
});
@@ -5,7 +5,6 @@ import { CertInfoService } from "../../../modules/monitor/index.js";
import { PipelineService } from "../../../modules/pipeline/service/pipeline-service.js";
import { SelectQueryBuilder } from "typeorm";
import { logger } from "@certd/basic";
import fs from "fs";
import dayjs from "dayjs";
import { ApiTags } from "@midwayjs/swagger";
import { CertReader } from "@certd/plugin-lib";
@@ -167,29 +166,29 @@ export class CertInfoController extends CrudController<CertInfoService> {
@Get("/download", { description: Constants.per.authOnly, summary: "下载证书文件" })
async download(@Query("id") id: number) {
const { userId, projectId } = await this.checkOwner(this.getService(), id, "read");
const certInfo = await this.getService().info(id);
if (certInfo == null) {
const certInfoEntity = await this.getService().info(id);
if (certInfoEntity == null) {
throw new CommonException("file not found");
}
if (certInfo.userId !== userId) {
if (certInfoEntity.userId !== userId) {
throw new CommonException("file not found");
}
if (projectId && certInfo.projectId !== projectId) {
if (projectId && certInfoEntity.projectId !== projectId) {
throw new CommonException("file not found");
}
// koa send file
// 下载文件的名称
// const filename = file.filename;
// 要下载的文件的完整路径
const path = certInfo.certFile;
if (!path) {
throw new CommonException("file not found");
if (!certInfoEntity.certInfo) {
throw new CommonException("证书数据未生成");
}
logger.info(`download:${path}`);
// 以流的形式下载文件
this.ctx.attachment(path);
this.ctx.set("Content-Type", "application/octet-stream");
return fs.createReadStream(path);
const certInfo = JSON.parse(certInfoEntity.certInfo);
const certReader = new CertReader(certInfo);
const zipBuffer = await certReader.buildZip();
const filename = certReader.buildZipFilename("cert");
logger.info(`download cert zip: ${filename}, size: ${zipBuffer.length}`);
this.ctx.attachment(filename);
this.ctx.set("Content-Type", "application/zip");
this.ctx.body = zipBuffer;
}
}
@@ -1,4 +1,4 @@
import { Body, Controller, Inject, Post, Provide, Query } from "@midwayjs/core";
import { Body, Controller, Get, Inject, Post, Provide, Query } from "@midwayjs/core";
import { PipelineService } from "../../../modules/pipeline/service/pipeline-service.js";
import { BaseController, Constants, PermissionException } from "@certd/lib-server";
import { StorageService } from "../../../modules/pipeline/service/storage-service.js";
@@ -6,6 +6,7 @@ import { CertReader } from "@certd/plugin-cert";
import { UserSettingsService } from "../../../modules/mine/service/user-settings-service.js";
import { UserGrantSetting } from "../../../modules/mine/service/models.js";
import { ApiTags } from "@midwayjs/swagger";
import { logger } from "@certd/basic";
@Provide()
@Controller("/api/pi/cert")
@@ -57,4 +58,38 @@ export class CertController extends BaseController {
const certDetail = CertReader.readCertDetail(crt);
return this.ok(certDetail);
}
@Get("/downloadZip", { description: Constants.per.authOnly, summary: "下载流水线证书压缩包" })
async downloadZip(@Query("id") id: number) {
const { userId } = await this.getProjectUserIdRead();
const pipelineUserId = await this.pipelineService.getPipelineUserId(id);
if (pipelineUserId !== userId) {
const isAdmin = await this.isAdmin();
if (!isAdmin) {
throw new PermissionException();
}
const setting = await this.userSettingsService.getSetting<UserGrantSetting>(pipelineUserId, null, UserGrantSetting, false);
if (setting?.allowAdminViewCerts !== true) {
throw new PermissionException("该流水线的用户还未授权管理员下载证书,请先让用户在”设置->授权委托“中打开开关");
}
}
const privateVars = await this.storeService.getPipelinePrivateVars(id);
const certInfo = privateVars.cert;
if (!certInfo?.crt) {
throw new Error("该流水线还未生成证书,请先运行一次流水线");
}
const certReader = new CertReader(certInfo);
const zipBuffer = await certReader.buildZip();
const filename = certReader.buildZipFilename("cert");
logger.info(`download pipeline cert zip: ${filename}, size: ${zipBuffer.length}`);
this.ctx.attachment(filename);
this.ctx.set("Content-Type", "application/zip");
this.ctx.body = zipBuffer;
}
}

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