Compare commits

...

44 Commits

Author SHA1 Message Date
xiaojunnuo b49ddbfef9 v1.39.15 2026-05-13 13:49:09 +08:00
xiaojunnuo b92fd73f53 build: prepare to build 2026-05-13 13:33:12 +08:00
xiaojunnuo 41b8f51a6a chore: 1 2026-05-13 13:32:01 +08:00
xiaojunnuo aad9045de5 perf(network): 新增全局公共http请求 headers设置
1. 新增公共请求头配置项,支持在系统设置中配置全局请求头
2. 实现请求头解析工具方法,支持多行KEY=VALUE格式配置
3. 在请求发起时自动附加全局公共请求头,且不会覆盖请求中已存在的同名Header
4. 添加多语言配置与前端表单组件,完善配置界面
5. 新增单元测试验证全局请求头合并逻辑
2026-05-13 12:09:01 +08:00
xiaojunnuo fdd5848df4 perf: icon选择器增加一套logo集 2026-05-13 11:20:55 +08:00
xiaojunnuo 118c15d046 perf(monitor/site): 新增站点监控页面禁用启用、检查状态两个筛选条件 2026-05-13 11:14:00 +08:00
xiaojunnuo bae4f8e320 fix: 修复第三方登录彩虹登录不上的bug 2026-05-13 11:03:10 +08:00
xiaojunnuo e0189a566e docs(donate): update privilege comparison table to add commercial edition details 2026-05-12 15:08:46 +08:00
xiaojunnuo 1cd8d73cdb Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-05-12 10:51:41 +08:00
xiaojunnuo d6e9e5987b perf: 优化申请时报错日志增加对应域名打印 2026-05-12 10:51:34 +08:00
xiaojunnuo 8c5aa37745 build: release 2026-05-12 00:51:59 +08:00
xiaojunnuo a18a871ac3 build: publish 2026-05-12 00:22:37 +08:00
xiaojunnuo 90cbff9cf9 build: trigger build image 2026-05-12 00:22:25 +08:00
xiaojunnuo bae5a04dcc v1.39.14 2026-05-12 00:21:06 +08:00
xiaojunnuo 7146570392 build: prepare to build 2026-05-12 00:06:37 +08:00
xiaojunnuo ae88f85d8e chore: 1 2026-05-12 00:06:02 +08:00
xiaojunnuo a362860137 fix: 修复阿里云订阅流水线创建对话框无法获取阿里订单列表的bug 2026-05-11 23:47:48 +08:00
xiaojunnuo c966896522 fix: 修复启动时报密钥备份不存在的问题 2026-05-11 22:31:11 +08:00
xiaojunnuo 5f88da1985 build: release 2026-05-11 10:26:12 +08:00
xiaojunnuo 043b80a298 chore: 1 2026-05-11 09:41:47 +08:00
xiaojunnuo ed0da28896 Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-05-11 09:40:22 +08:00
xiaojunnuo 61a0d69d58 chore: 打包报错, 固定pnpm@10.33.4 2026-05-11 09:39:04 +08:00
xiaojunnuo 3833a9216e chore: 1 2026-05-11 01:21:44 +08:00
xiaojunnuo e59566b5e2 chore: 1 2026-05-11 01:20:00 +08:00
xiaojunnuo e4be0ce464 chore: 1 2026-05-11 01:13:44 +08:00
xiaojunnuo 022dbf0cab chore: 1 2026-05-11 00:54:50 +08:00
xiaojunnuo 1e6b559b89 chore: 1 2026-05-11 00:48:05 +08:00
xiaojunnuo 74bae2005d build: publish 2026-05-11 00:28:04 +08:00
xiaojunnuo 1731a35d94 build: trigger build image 2026-05-11 00:27:48 +08:00
xiaojunnuo 9f7d766cb3 v1.39.13 2026-05-11 00:26:11 +08:00
xiaojunnuo 6c0d0b00c9 build: prepare to build 2026-05-11 00:21:28 +08:00
xiaojunnuo a82a38421d chore: sql 2026-05-11 00:20:25 +08:00
xiaojunnuo c4b01da384 fix(plugin-aliyun): 过滤非CAS证书并优化日志信息
过滤证书列表仅保留CAS类型证书,并更新相关日志信息以更准确反映操作情况
2026-05-11 00:07:48 +08:00
xiaojunnuo 3e1473dba5 chore: 1 2026-05-11 00:03:16 +08:00
xiaojunnuo d383706554 chore(用户资料): 添加头像和昵称的编辑按钮
- 在头像和昵称旁边添加编辑按钮,提升用户操作便捷性
- 移除原有的更新资料按钮,优化界面布局
- 调整角色信息显示位置至用户名旁边
2026-05-10 23:58:23 +08:00
xiaojunnuo e0eb0e21f6 perf(用户资料): 新增手机号邮箱绑定功能
实现用户邮箱和手机号的绑定与修改功能,包括:
1. 添加联系方式绑定API接口
2. 实现身份验证流程
3. 添加前端绑定对话框组件
4. 完善用户资料页面的联系方式展示和编辑入口
5. 添加联系方式冲突检测逻辑
6. 实现验证码校验功能
2026-05-10 23:51:45 +08:00
xiaojunnuo 7266af1749 fix: cnameProvider域名支持设置子域名托管 2026-05-10 22:41:07 +08:00
xiaojunnuo f93bc09438 chore(plugin-cert): 更新EAB选项标签和生成按钮文本
- 移除EAB选项中的免费说明文字
- 将"刷新"按钮文本改为"生成"
- 更新相关提示信息以保持一致性
2026-05-10 18:28:03 +08:00
xiaojunnuo fe3bb7c1b4 chore(plugin-cert): 修改刷新按钮文本为更简洁的表述 2026-05-10 17:15:47 +08:00
xiaojunnuo 923676c7d5 build: 添加 cross-env 依赖并更新单元测试脚本
在 package.json 和 pnpm-lock.yaml 中添加 cross-env 依赖,用于在单元测试时设置 NODE_ENV=unittest 环境变量。同时更新 AGENTS.md 文档说明单元测试环境要求。
2026-05-10 17:06:00 +08:00
xiaojunnuo 4755216505 perf: 重构自动加载模块并优化EAB授权处理
refactor(ui): 将分散的auto-*模块整合为统一命名的auto-register模块
perf(plugin-cert): 增强EAB授权功能,支持账号私钥刷新和类型选择
test: 添加EAB授权服务和ACME账号配置的单元测试
docs: 更新AGENTS.md补充ACME/EAB使用注意事项
chore: 统一各package.json中的测试脚本配置
2026-05-10 16:57:12 +08:00
xiaojunnuo 37d03c10f9 chore: 1 2026-05-08 22:28:10 +08:00
xiaojunnuo 490b724808 chore: 更新腾讯云证书(Id不变)插件下架 2026-05-08 22:19:31 +08:00
xiaojunnuo d8f132919d chore: 1 2026-05-08 15:50:28 +08:00
104 changed files with 2575 additions and 503 deletions
+8
View File
@@ -142,6 +142,12 @@ Certd 是一个支持私有化部署的 SSL/TLS 证书自动化管理平台。
如果只是某个服务商或部署目标的问题,不要轻易修改共享 pipeline/core 行为,除非确实是可复用的公共能力。
### ACME / EAB 注意事项
- 公共 EAB(尤其是 Google EAB)可能只能创建一次 ACME 账号。要跨用户复用公共 EAB,应保存并复用同一个 ACME account private key`accountUrl` 如果存到 `userContext` 里,只能视为当前用户缓存,因为 `userContext` 跟用户 id 走。
- ACME 协议的 `newAccount` 支持 `onlyReturnExisting`。使用同一个 account private key 调用 `newAccount({ onlyReturnExisting: true })` 可以取回已创建账号的 URL,且不会再次消费 EAB。
- 修改 EAB 的 `kid` 后,应重新生成绑定该 `kid` 的 account private key;否则应阻止继续申请并提示用户刷新账号私钥。
## 数据与迁移
后端使用 TypeORM 实体加 SQL 迁移。
@@ -161,6 +167,7 @@ Certd 是一个支持私有化部署的 SSL/TLS 证书自动化管理平台。
- 优先沿用现有模块、插件、服务模式,再考虑新增抽象。
- `packages/ui/certd-server/data/``logs/`、生成的 metadata/dist 等通常视为运行时或构建产物,除非任务明确要求处理它们。
- 注意本地数据和配置里可能包含凭据、证书材料等敏感信息。
- 本仓库代码注释优先使用中文,尤其是解释业务规则、兼容逻辑、协议细节和隐藏风险时;除非文件已有明确英文注释风格或引用外部英文术语,否则不要新增英文说明性注释。
## 插件开发技能
@@ -207,6 +214,7 @@ Get-ChildItem packages\ui\certd-client\src\views\certd
- 实现新功能或修复行为缺陷前,先补对应单元测试,并先运行测试确认它处于失败状态;再实现功能或修复代码,反复运行聚焦单元测试直到通过。若某项改动确实不适合先写单元测试,应在回复中说明原因和替代验证方式。
- 后补单元测试时,应先基于对正确行为的实际预期编写测试,而不是为了迎合现有实现改写预期;如果运行后出现红灯,且通过测试需要修改已有实现,应先向用户确认这是确实的 bug,还是原本需求/既有行为就是如此;确认后再修改原始实现,避免把测试补充变成未经确认的行为改动。
- 后端纯单元测试用例放在 `src` 目录内,并尽量与被测文件相邻,例如 `src/utils/random.test.ts`;对应 `test:unit` 只跑 `src/**/*.test.ts`,构建/打包配置应排除这些 `*.test.ts` 文件。
- 单元测试需要 mock ESM 静态 import 时,优先使用 `esmock`,不要为了测试把业务代码改成构造函数注入或把逻辑挪到调用方;各包 `test:unit` 脚本应显式设置 `NODE_ENV=unittest`
- 单个 monorepo 包运行单元测试时,优先使用 `corepack pnpm --dir <包目录> test:unit`,例如 `corepack pnpm --dir packages\ui\certd-server test:unit``corepack pnpm --dir packages\core\basic test:unit``corepack pnpm --dir packages\plugins\plugin-lib test:unit`;也可以用包名过滤,例如 `corepack pnpm --filter @certd/ui-server test:unit`。前端 `packages\ui\certd-client` 暂时不跑单元测试。
- 前端 TS/Vue/locale 等文件改动后,优先只对本次改动文件运行项目现有自动格式化/修复;Windows/PowerShell 下 Prettier 已验证可用命令为 `packages\ui\certd-client\node_modules\.bin\prettier.cmd --write <files>`ESLint 可用命令为 `packages\ui\certd-client\node_modules\.bin\eslint.cmd --fix <files>`;不要为了格式化无关文件而扩大 diff。项目保留了 `tslint` 依赖,但当前主要使用 ESLint + Prettier。
- 优先对改动包运行聚焦的测试或类型检查;只有跨包影响明显时再考虑全 monorepo 构建。
+40
View File
@@ -3,6 +3,46 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
### Bug Fixes
* 修复第三方登录彩虹登录不上的bug ([bae4f8e](https://github.com/certd/certd/commit/bae4f8e3209d9f9869ecbd7c01655383bac2fe21))
### Performance Improvements
* 优化申请时报错日志增加对应域名打印 ([d6e9e59](https://github.com/certd/certd/commit/d6e9e5987bd52ea12ee18745615486eadd4c87ff))
* icon选择器增加一套logo集 ([fdd5848](https://github.com/certd/certd/commit/fdd5848df4055a6ee07dc5eabaaf6b718672882d))
* **monitor/site:** 新增站点监控页面禁用启用、检查状态两个筛选条件 ([118c15d](https://github.com/certd/certd/commit/118c15d04633a6ef06f2d9e7a7849d20f596e02c))
* **network:** 新增全局公共http请求 headers设置 ([aad9045](https://github.com/certd/certd/commit/aad9045de55e76cb2ad09cac74a7bd60a4b47124))
## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11)
### Bug Fixes
* 修复阿里云订阅流水线创建对话框无法获取阿里订单列表的bug ([a362860](https://github.com/certd/certd/commit/a362860137bfb7072893c844fe775edc46070ee1))
* 修复启动时报密钥备份不存在的问题 ([c966896](https://github.com/certd/certd/commit/c9668965226af6b54e0e576931dcba8b3d188ef3))
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Bug Fixes
* **aliyun-access:** 添加阿里云密钥校验失败的错误处理 ([b75c625](https://github.com/certd/certd/commit/b75c625ddcc0b3110699d8e6175681ef157b25df))
* cnameProvider域名支持设置子域名托管 ([7266af1](https://github.com/certd/certd/commit/7266af17491a98338022cfb18cfedfb93ca6ef8f))
* **plugin-aliyun:** 过滤非CAS证书并优化日志信息 ([c4b01da](https://github.com/certd/certd/commit/c4b01da384bc40a241a673ea8bc01ca733c04d83))
### Performance Improvements
* **设置:** 添加首页启用开关配置 ([25ad1e6](https://github.com/certd/certd/commit/25ad1e6f861e43288cc8bd90d4903628e6faec29))
* 新增agents.md ([aa176b0](https://github.com/certd/certd/commit/aa176b081a92837d2d6809d16546a8dfc2e5dd36))
* **用户资料:** 新增手机号邮箱绑定功能 ([e0eb0e2](https://github.com/certd/certd/commit/e0eb0e21f6dae24b639c944f9aba2c90496ab1c0))
* 域名注册过期时间获取再次优化 ([91a1b97](https://github.com/certd/certd/commit/91a1b9755066bf280e194dabf7c3a9f936e2643f))
* **证书流水线:** 添加批量更新证书申请参数功能 ([63be1c1](https://github.com/certd/certd/commit/63be1c1cbd9b09a3b48f26130c296b1cedcca1ac))
* 支持火山云vke ([bb46cb0](https://github.com/certd/certd/commit/bb46cb08f71f6ae921543f7e4a6c5f4e0190556e))
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
* **domain:** 添加域名过期时间同步进度显示功能 ([9d2937d](https://github.com/certd/certd/commit/9d2937dd4b14ffab73e9b096edd2aa8539811182))
* **plugin-volcengine:** 支持火山引擎VKE部署插件 ([b8a64a6](https://github.com/certd/certd/commit/b8a64a6b5bf3691a47177de42bc49b798e795feb))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
### Bug Fixes
+16 -12
View File
@@ -179,19 +179,23 @@ https://certd.handfree.work/
[50元专业版优惠券限时领取](https://app.handfree.work/subject/#/app/certd/product)
专业版特权对比
| 功能 | 免费版 | 专业版 |
|---------|---------------------------------------|--------------------------------|
| 免费证书申请 | 免费无限制 | 免费无限制 |
| 证书域名数量 | 无限制 | 无限制 |
| 证书流水线条数 | 无限制 | 无限制 |
| 自动部署插件 | 阿里云CDN、腾讯云、七牛CDN、主机部署、宝塔、1Panel等大部分插件 | 群晖、威联通、proxmox等 |
| 通知 | 邮件通知、自定义webhook | 邮件免配置、企微、钉钉、飞书、anpush、server酱等 |
| 站点监控 | 限制1条 | 无限制 |
| 批量操作 | 无 | 流水线模版,流水线复制,批量运行,批量设置通知、定时等 |
| VIP群 | 无 | 可加,一对一技术支持,必要时可申请远程协助 |
专业版、商业版特权对比
| 功能&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | 免费版 | 专业版 | 商业版 |
|---------|---------------------------------------|--------------------------------|---------------------------------|
| 证书申请 | 无限制 | 无限制 | 无限制 |
| 证书域名数量 | 无限制 | 无限制 | 无限制 |
| 证书流水线条数 | 无限制 | 无限制 | 无限制 |
| 自动部署插件 | 阿里云CDN、腾讯云、七牛CDN、主机部署、宝塔、1Panel等大部分插件 | 群晖、威联通、proxmox等 | 同专业版 |
| 通知 | 邮件通知、自定义webhook | 邮件免配置、企微、钉钉、飞书、anpush、server酱等 | 同专业版 |
| 站点监控 | 限制1条 | 无限制 | 无限制 |
| 批量操作 | 无 | 流水线模版,流水线复制,批量运行,批量设置通知、定时等 | 同专业版 |
| VIP群 | 无 | 可加,一对一技术支持,必要时可申请远程协助 | 商业版技术支持 |
| 站点个性化 | 无 | 无 | 可自定义站点名称、Logo等,移除Certd元素,首页警告等 |
| 套餐功能 | 无 | 无 | 支持配置套餐供用户购买 |
| 数据统计 | 无 | 无 | 支持站点各类统计数据 |
| 插件管理 | 无 | 无 | 支持公共EAB设置,插件选项配置 |
| 是否可商用 | 不允许 | 不允许 | 可对外运营 |
## 九、贡献代码
+27
View File
@@ -3,6 +3,33 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11)
### Bug Fixes
* 修复阿里云订阅流水线创建对话框无法获取阿里订单列表的bug ([a362860](https://github.com/certd/certd/commit/a362860137bfb7072893c844fe775edc46070ee1))
* 修复启动时报密钥备份不存在的问题 ([c966896](https://github.com/certd/certd/commit/c9668965226af6b54e0e576931dcba8b3d188ef3))
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Bug Fixes
* **aliyun-access:** 添加阿里云密钥校验失败的错误处理 ([b75c625](https://github.com/certd/certd/commit/b75c625ddcc0b3110699d8e6175681ef157b25df))
* cnameProvider域名支持设置子域名托管 ([7266af1](https://github.com/certd/certd/commit/7266af17491a98338022cfb18cfedfb93ca6ef8f))
* **plugin-aliyun:** 过滤非CAS证书并优化日志信息 ([c4b01da](https://github.com/certd/certd/commit/c4b01da384bc40a241a673ea8bc01ca733c04d83))
### Performance Improvements
* **设置:** 添加首页启用开关配置 ([25ad1e6](https://github.com/certd/certd/commit/25ad1e6f861e43288cc8bd90d4903628e6faec29))
* 新增agents.md ([aa176b0](https://github.com/certd/certd/commit/aa176b081a92837d2d6809d16546a8dfc2e5dd36))
* **用户资料:** 新增手机号邮箱绑定功能 ([e0eb0e2](https://github.com/certd/certd/commit/e0eb0e21f6dae24b639c944f9aba2c90496ab1c0))
* 域名注册过期时间获取再次优化 ([91a1b97](https://github.com/certd/certd/commit/91a1b9755066bf280e194dabf7c3a9f936e2643f))
* **证书流水线:** 添加批量更新证书申请参数功能 ([63be1c1](https://github.com/certd/certd/commit/63be1c1cbd9b09a3b48f26130c296b1cedcca1ac))
* 支持火山云vke ([bb46cb0](https://github.com/certd/certd/commit/bb46cb08f71f6ae921543f7e4a6c5f4e0190556e))
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
* **domain:** 添加域名过期时间同步进度显示功能 ([9d2937d](https://github.com/certd/certd/commit/9d2937dd4b14ffab73e9b096edd2aa8539811182))
* **plugin-volcengine:** 支持火山引擎VKE部署插件 ([b8a64a6](https://github.com/certd/certd/commit/b8a64a6b5bf3691a47177de42bc49b798e795feb))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
### Bug Fixes
+16 -10
View File
@@ -16,16 +16,22 @@
****------------------****
## 专业版特权对比
| 功能&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | 免费版 | 专业版 |
|---------|---------------------------------------|--------------------------------|
| 证书申请 | 无限制 | 无限制 |
| 证书域名数量 | 无限制 | 无限制 |
| 证书流水线条数 | 无限制 | 无限制 |
| 自动部署插件 | 阿里云CDN、腾讯云、七牛CDN、主机部署、宝塔、1Panel等大部分插件 | 群晖、威联通、proxmox等 |
| 通知 | 邮件通知、自定义webhook | 邮件免配置、企微、钉钉、飞书、anpush、server酱等 |
| 站点监控 | 限制1条 | 无限制 |
| 批量操作 | 无 | 流水线模版,流水线复制,批量运行,批量设置通知、定时等 |
| VIP群 | 无 | 可加,一对一技术支持,必要时可申请远程协助 |
| 功能&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | 免费版 | 专业版 | 商业版 |
|---------|---------------------------------------|--------------------------------|---------------------------------|
| 证书申请 | 无限制 | 无限制 | 无限制 |
| 证书域名数量 | 无限制 | 无限制 | 无限制 |
| 证书流水线条数 | 无限制 | 无限制 | 无限制 |
| 自动部署插件 | 阿里云CDN、腾讯云、七牛CDN、主机部署、宝塔、1Panel等大部分插件 | 群晖、威联通、proxmox等 | 同专业版 |
| 通知 | 邮件通知、自定义webhook | 邮件免配置、企微、钉钉、飞书、anpush、server酱等 | 同专业版 |
| 站点监控 | 限制1条 | 无限制 | 无限制 |
| 批量操作 | 无 | 流水线模版,流水线复制,批量运行,批量设置通知、定时等 | 同专业版 |
| VIP群 | 无 | 可加,一对一技术支持,必要时可申请远程协助 | 商业版技术支持 |
| 站点个性化 | 无 | 无 | 可自定义站点名称、Logo等,移除Certd元素,首页警告等 |
| 套餐功能 | 无 | 无 | 支持配置套餐供用户购买 |
| 数据统计 | 无 | 无 | 支持站点各类统计数据 |
| 插件管理 | 无 | 无 | 支持公共EAB设置,插件选项配置 |
| 是否可商用 | 不允许 | 不允许 | 可对外运营 |
## 专业版激活方式
+1 -1
View File
@@ -1,5 +1,5 @@
# 任务插件
`132` 款任务插件
`131` 款任务插件
## 1. 证书申请
| 序号 | 名称 | 说明 |
+1 -1
View File
@@ -9,5 +9,5 @@
}
},
"npmClient": "pnpm",
"version": "1.39.12"
"version": "1.39.15"
}
+2 -1
View File
@@ -9,6 +9,7 @@
"@lerna-lite/run": "^3.9.3",
"@lerna-lite/version": "^3.9.3",
"axios": "^1.9.0",
"cross-env": "^7.0.3",
"medium-zoom": "^1.1.0",
"vitepress": "^2.0.0-alpha.4",
"vitepress-plugin-lightbox": "^1.0.2"
@@ -35,7 +36,7 @@
"docs:dev": "vitepress dev docs",
"docs:build": "pnpm run copylogs && vitepress build docs",
"docs:preview": "vitepress preview docs",
"test:unit": "pnpm -r --workspace-concurrency=1 run test:unit",
"test:unit": "cross-env NODE_ENV=unittest pnpm -r --workspace-concurrency=1 run test:unit",
"pub": "echo 1",
"dev": "pnpm run -r --parallel compile ",
"pub_all":"pnpm run -r --parallel pub ",
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.15](https://github.com/publishlab/node-acme-client/compare/v1.39.14...v1.39.15) (2026-05-13)
### Performance Improvements
* 优化申请时报错日志增加对应域名打印 ([d6e9e59](https://github.com/publishlab/node-acme-client/commit/d6e9e5987bd52ea12ee18745615486eadd4c87ff))
## [1.39.14](https://github.com/publishlab/node-acme-client/compare/v1.39.13...v1.39.14) (2026-05-11)
**Note:** Version bump only for package @certd/acme-client
## [1.39.13](https://github.com/publishlab/node-acme-client/compare/v1.39.12...v1.39.13) (2026-05-10)
### Performance Improvements
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/publishlab/node-acme-client/commit/4755216505ad18555a50da9d8008c2207c48df86))
## [1.39.12](https://github.com/publishlab/node-acme-client/compare/v1.39.11...v1.39.12) (2026-04-29)
### Performance Improvements
+6 -4
View File
@@ -3,7 +3,7 @@
"description": "Simple and unopinionated ACME client",
"private": false,
"author": "nmorsman",
"version": "1.39.12",
"version": "1.39.15",
"type": "module",
"module": "./dist/index.js",
"main": "./dist/index.js",
@@ -18,7 +18,7 @@
"types"
],
"dependencies": {
"@certd/basic": "^1.39.12",
"@certd/basic": "^1.39.15",
"@peculiar/x509": "^1.11.0",
"asn1js": "^3.0.5",
"axios": "^1.9.0",
@@ -35,10 +35,12 @@
"@typescript-eslint/parser": "^8.26.1",
"chai": "^4.4.1",
"chai-as-promised": "^7.1.2",
"cross-env": "^7.0.3",
"eslint": "^8.57.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^4.2.1",
"esmock": "^2.7.5",
"jsdoc-to-markdown": "^8.0.1",
"mocha": "^10.6.0",
"nock": "^13.5.4",
@@ -55,7 +57,7 @@
"prepublishOnly": "npm run build && npm run build-docs",
"test": "mocha -t 60000 \"test/setup.js\" \"test/**/*.spec.js\"",
"before-test:unit": "node -e \"const fs=require('fs');fs.rmSync('dist-test',{recursive:true,force:true});fs.rmSync('tsconfig.test.tsbuildinfo',{force:true});\"",
"test:unit": "npm run before-test:unit && tsc -p tsconfig.test.json --skipLibCheck && mocha -t 60000 \"dist-test/**/*.test.js\"",
"test:unit": "cross-env NODE_ENV=unittest npm run before-test:unit && cross-env NODE_ENV=unittest tsc -p tsconfig.test.json --skipLibCheck && cross-env NODE_ENV=unittest mocha -t 60000 \"dist-test/**/*.test.js\"",
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch"
},
@@ -74,5 +76,5 @@
"bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues"
},
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
"gitHead": "bae5a04dcc0a679c290a9805c3ac4a6020eb6ec0"
}
+2
View File
@@ -167,6 +167,7 @@ export default async (client, userOpts) => {
await client.completeChallenge(challenge);
}catch (e) {
await deactivateAuth(e);
e.message = `[${d}] ${e.message || "completeChallenge error"}`;
throw e;
}
challengeCompleted = true;
@@ -178,6 +179,7 @@ export default async (client, userOpts) => {
} catch (e) {
log(`[auto] [${d}] challengeCreateFn threw error: ${e.message || e}`);
await deactivateAuth(e);
e.message = `[${d}] ${e.message || "challengeCreateFn error"}`;
throw e;
}
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
### Performance Improvements
* **network:** 新增全局公共http请求 headers设置 ([aad9045](https://github.com/certd/certd/commit/aad9045de55e76cb2ad09cac74a7bd60a4b47124))
## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11)
**Note:** Version bump only for package @certd/basic
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Performance Improvements
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
### Performance Improvements
+1 -1
View File
@@ -1 +1 @@
23:06
13:33
+5 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/basic",
"private": false,
"version": "1.39.12",
"version": "1.39.15",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -13,7 +13,7 @@
"dev-build": "npm run build",
"preview": "vite preview",
"test": "mocha --loader=ts-node/esm",
"test:unit": "mocha --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
"test:unit": "cross-env NODE_ENV=unittest mocha --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch"
},
@@ -40,9 +40,11 @@
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"chai": "4.3.10",
"cross-env": "^7.0.3",
"eslint": "^8.41.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"esmock": "^2.7.5",
"mocha": "^10.2.0",
"prettier": "^2.8.8",
"rimraf": "^5.0.5",
@@ -50,5 +52,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
"gitHead": "bae5a04dcc0a679c290a9805c3ac4a6020eb6ec0"
}
@@ -0,0 +1,53 @@
import { expect } from "chai";
import { createAxiosService, HttpClient, setGlobalHeaders } from "./util.request.js";
import { ILogger } from "./util.log.js";
const testLogger = {
info() {},
error() {},
} as unknown as ILogger;
describe("util.request", () => {
afterEach(() => {
setGlobalHeaders({});
});
it("should merge global headers without overriding request headers", async () => {
setGlobalHeaders({
"X-Common": "common",
"X-Override": "global",
});
const http = createAxiosService({ logger: testLogger }) as HttpClient;
const res = await http.request({
url: "http://example.com",
method: "get",
logReq: false,
logRes: false,
headers: {
"X-Override": "request",
"X-Request": "request",
},
adapter: async config => {
const headers = config.headers;
return {
config,
data: {
common: headers.get("X-Common"),
override: headers.get("X-Override"),
request: headers.get("X-Request"),
},
headers: {},
status: 200,
statusText: "OK",
};
},
});
expect(res).to.deep.equal({
common: "common",
override: "request",
request: "request",
});
});
});
+18 -2
View File
@@ -26,7 +26,7 @@ export class HttpError extends Error {
return;
}
let message = error?.message || error?.response.statusText || error?.code;
let message = error?.message || error?.response?.statusText || error?.code;
if (message && typeof message === "string" && message.indexOf) {
for (const key in errorMap) {
if (message.indexOf(key) > -1) {
@@ -82,6 +82,7 @@ export class HttpError extends Error {
export const HttpCommonError = HttpError;
let defaultAgents = createAgent();
let defaultHeaders: Record<string, string> = {};
export function setGlobalProxy(opts: { httpProxy?: string; httpsProxy?: string }) {
logger.info("setGlobalProxy:", opts);
@@ -92,6 +93,15 @@ export function getGlobalAgents() {
return defaultAgents;
}
export function setGlobalHeaders(headers: Record<string, string> = {}) {
logger.info("setGlobalHeaders:", Object.keys(headers));
defaultHeaders = { ...headers };
}
export function getGlobalHeaders() {
return defaultHeaders;
}
/**
* @description
*/
@@ -148,6 +158,12 @@ export function createAxiosService({ logger }: { logger: ILogger }) {
config.httpsAgent = agents.httpsAgent;
config.httpAgent = agents.httpAgent;
if (Object.keys(defaultHeaders).length > 0) {
const headers = AxiosHeaders.from(defaultHeaders);
headers.set(config.headers || {});
config.headers = headers;
}
// const agent = new https.Agent({
// rejectUnauthorized: false // 允许自签名证书
// });
@@ -267,7 +283,7 @@ export function createAxiosService({ logger }: { logger: ILogger }) {
logger.error(`请求出错:${errorMessage} status:${status},statusText:${error.response?.statusText || error.code},url:${error.config?.url},method:${error.config?.method}`);
logger.error("返回数据:", JSON.stringify(error.response?.data));
if (error.response?.data) {
const message = error.response.data.message || error.response.data.msg || error.response.data.error;
const message = error.response?.data?.message || error.response?.data?.msg || error.response?.data?.error;
if (typeof message === "string") {
error.message = message;
}
+18
View File
@@ -3,6 +3,24 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
**Note:** Version bump only for package @certd/pipeline
## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11)
**Note:** Version bump only for package @certd/pipeline
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Bug Fixes
* cnameProvider域名支持设置子域名托管 ([7266af1](https://github.com/certd/certd/commit/7266af17491a98338022cfb18cfedfb93ca6ef8f))
### Performance Improvements
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
### Performance Improvements
+7 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/pipeline",
"private": false,
"version": "1.39.12",
"version": "1.39.15",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -14,13 +14,13 @@
"build3": "rollup -c",
"preview": "vite preview",
"test": "mocha --loader=ts-node/esm",
"test:unit": "mocha --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
"test:unit": "cross-env NODE_ENV=unittest mocha --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch"
},
"dependencies": {
"@certd/basic": "^1.39.12",
"@certd/plus-core": "^1.39.12",
"@certd/basic": "^1.39.15",
"@certd/plus-core": "^1.39.15",
"dayjs": "^1.11.7",
"lodash-es": "^4.17.21",
"reflect-metadata": "^0.1.13"
@@ -37,9 +37,11 @@
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"chai": "4.3.10",
"cross-env": "^7.0.3",
"eslint": "^8.41.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"esmock": "^2.7.5",
"mocha": "^10.2.0",
"prettier": "^2.8.8",
"rimraf": "^5.0.5",
@@ -47,5 +49,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
"gitHead": "bae5a04dcc0a679c290a9805c3ac4a6020eb6ec0"
}
@@ -3,6 +3,7 @@ import { IAccess } from "../access/index.js";
export type CnameProvider = {
id: any;
domain: string;
subdomain?: string;
title?: string;
dnsProviderType?: string;
access?: IAccess;
+14
View File
@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
**Note:** Version bump only for package @certd/lib-huawei
## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11)
**Note:** Version bump only for package @certd/lib-huawei
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Performance Improvements
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
**Note:** Version bump only for package @certd/lib-huawei
+5 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-huawei",
"private": false,
"version": "1.39.12",
"version": "1.39.15",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
"types": "./dist/d/index.d.ts",
@@ -11,7 +11,7 @@
"build": "npm run before-build && rollup -c ",
"dev-build": "npm run build",
"preview": "vite preview",
"test:unit": "echo no unit tests",
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
"pub": "npm publish"
},
"dependencies": {
@@ -22,8 +22,10 @@
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"cross-env": "^7.0.3",
"esmock": "^2.7.5",
"prettier": "^2.8.8",
"tslib": "^2.8.1"
},
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
"gitHead": "bae5a04dcc0a679c290a9805c3ac4a6020eb6ec0"
}
+14
View File
@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
**Note:** Version bump only for package @certd/lib-iframe
## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11)
**Note:** Version bump only for package @certd/lib-iframe
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Performance Improvements
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
**Note:** Version bump only for package @certd/lib-iframe
+5 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-iframe",
"private": false,
"version": "1.39.12",
"version": "1.39.15",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -14,7 +14,7 @@
"build3": "rollup -c",
"build2": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"test:unit": "echo no unit tests",
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
"pub": "npm publish"
},
"dependencies": {
@@ -24,13 +24,15 @@
"@types/chai": "^4.3.3",
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"cross-env": "^7.0.3",
"eslint": "^8.24.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"esmock": "^2.7.5",
"prettier": "^2.8.8",
"rimraf": "^5.0.5",
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
"gitHead": "bae5a04dcc0a679c290a9805c3ac4a6020eb6ec0"
}
+14
View File
@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
**Note:** Version bump only for package @certd/jdcloud
## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11)
**Note:** Version bump only for package @certd/jdcloud
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Performance Improvements
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
**Note:** Version bump only for package @certd/jdcloud
+5 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/jdcloud",
"version": "1.39.12",
"version": "1.39.15",
"description": "jdcloud openApi sdk",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
@@ -8,7 +8,7 @@
"scripts": {
"build": "rollup -c ",
"dev-build": "npm run build",
"test:unit": "echo no unit tests",
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
"pub": "npm publish"
},
"author": "",
@@ -30,7 +30,8 @@
"@typescript-eslint/parser": "^8.26.1",
"chai": "^4.1.2",
"config": "^1.30.0",
"cross-env": "^5.1.4",
"cross-env": "^7.0.3",
"esmock": "^2.7.5",
"js-yaml": "^3.11.0",
"mocha": "^5.0.0",
"prettier": "^2.8.8",
@@ -57,5 +58,5 @@
"fetch"
]
},
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
"gitHead": "bae5a04dcc0a679c290a9805c3ac4a6020eb6ec0"
}
+14
View File
@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
**Note:** Version bump only for package @certd/lib-k8s
## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11)
**Note:** Version bump only for package @certd/lib-k8s
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Performance Improvements
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
**Note:** Version bump only for package @certd/lib-k8s
+6 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-k8s",
"private": false,
"version": "1.39.12",
"version": "1.39.15",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -14,25 +14,27 @@
"build3": "rollup -c",
"build2": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"test:unit": "echo no unit tests",
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch"
},
"dependencies": {
"@certd/basic": "^1.39.12",
"@certd/basic": "^1.39.15",
"@kubernetes/client-node": "0.21.0"
},
"devDependencies": {
"@types/chai": "^4.3.3",
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"cross-env": "^7.0.3",
"eslint": "^8.24.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"esmock": "^2.7.5",
"prettier": "^2.8.8",
"rimraf": "^5.0.5",
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
"gitHead": "bae5a04dcc0a679c290a9805c3ac4a6020eb6ec0"
}
+19
View File
@@ -3,6 +3,25 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
### Performance Improvements
* **network:** 新增全局公共http请求 headers设置 ([aad9045](https://github.com/certd/certd/commit/aad9045de55e76cb2ad09cac74a7bd60a4b47124))
## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11)
### Bug Fixes
* 修复启动时报密钥备份不存在的问题 ([c966896](https://github.com/certd/certd/commit/c9668965226af6b54e0e576931dcba8b3d188ef3))
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Performance Improvements
* **设置:** 添加首页启用开关配置 ([25ad1e6](https://github.com/certd/certd/commit/25ad1e6f861e43288cc8bd90d4903628e6faec29))
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
### Performance Improvements
+10 -9
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/lib-server",
"version": "1.39.12",
"version": "1.39.15",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -12,7 +12,7 @@
"build": "npm run before-build && tsc --skipLibCheck",
"dev-build": "npm run build",
"test": "midway-bin test --ts -V",
"test:unit": "mocha --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
"test:unit": "cross-env NODE_ENV=unittest mocha --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
"test1": "midway-bin test --ts -V -f test/blank.test.ts -t 'hash-check'",
"cov": "midway-bin cov --ts",
"lint": "mwts check",
@@ -29,11 +29,11 @@
],
"license": "AGPL",
"dependencies": {
"@certd/acme-client": "^1.39.12",
"@certd/basic": "^1.39.12",
"@certd/pipeline": "^1.39.12",
"@certd/plugin-lib": "^1.39.12",
"@certd/plus-core": "^1.39.12",
"@certd/acme-client": "^1.39.15",
"@certd/basic": "^1.39.15",
"@certd/pipeline": "^1.39.15",
"@certd/plugin-lib": "^1.39.15",
"@certd/plus-core": "^1.39.15",
"@midwayjs/cache": "3.14.0",
"@midwayjs/core": "3.20.11",
"@midwayjs/i18n": "3.20.13",
@@ -44,7 +44,6 @@
"@midwayjs/upload": "3.20.13",
"@midwayjs/validate": "3.20.13",
"better-sqlite3": "^11.1.2",
"cross-env": "^7.0.3",
"dayjs": "^1.11.7",
"lodash-es": "^4.17.21",
"mwts": "^1.3.0",
@@ -57,9 +56,11 @@
"@types/node": "^18",
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"cross-env": "^7.0.3",
"eslint": "^8.24.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"esmock": "^2.7.5",
"mocha": "^10.2.0",
"prettier": "^2.8.8",
"rimraf": "^5.0.5",
@@ -68,5 +69,5 @@
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
"gitHead": "bae5a04dcc0a679c290a9805c3ac4a6020eb6ec0"
}
@@ -80,6 +80,7 @@ export class SysPrivateSettings extends BaseSettings {
httpsProxy? = '';
httpProxy? = '';
commonHeaders?: string = '';
reverseProxies?: Record<string, string> = {};
@@ -5,7 +5,7 @@ import { SysSettingsEntity } from '../entity/sys-settings.js';
import { BaseSettings, SysInstallInfo, SysPrivateSettings, SysPublicSettings, SysSecret, SysSecretBackup } from './models.js';
import { getAllSslProviderDomains, setSslProviderReverseProxies, setWalkFromAuthoritative } from '@certd/acme-client';
import { cache, logger, mergeUtils, setGlobalProxy } from '@certd/basic';
import { cache, logger, mergeUtils, setGlobalHeaders, setGlobalProxy } from '@certd/basic';
import { isPlus } from '@certd/plus-core';
import * as dns from 'node:dns';
import { BaseService, setAdminMode } from '../../../basic/index.js';
@@ -167,6 +167,7 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
httpsProxy: privateSetting.httpsProxy,
};
setGlobalProxy(opts);
setGlobalHeaders(this.parseKeyValueText(privateSetting.commonHeaders));
if (privateSetting.dnsResultOrder) {
dns.setDefaultResultOrder(privateSetting.dnsResultOrder as any);
@@ -185,12 +186,12 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
}
setEnvironmentVars(vars: string) {
const envVars = {}
if (typeof vars !== 'string') {
vars = ""
parseKeyValueText(text: string) {
const values = {};
if (typeof text !== 'string') {
text = "";
}
vars.split('\n').forEach(line => {
text.split('\n').forEach(line => {
line = line.trim();
if (!line || line.startsWith('#')) {
return
@@ -204,11 +205,18 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
return
}
const [key, value] = line.split('=');
const eqIndex = line.indexOf('=');
const key = line.substring(0, eqIndex).trim();
const value = line.substring(eqIndex + 1).trim();
if (key && value) {
envVars[key.trim()] = value.trim();
values[key] = value;
}
});
return values;
}
setEnvironmentVars(vars: string) {
const envVars = this.parseKeyValueText(vars);
//先删除旧环境变量
if (lastSaveEnvVars) {
for (const key in lastSaveEnvVars) {
@@ -0,0 +1,30 @@
import assert from "assert";
import { AccessService } from "./access-service.js";
describe("AccessService", () => {
it("does not write id into access setting when updating selected fields", async () => {
let updateParam: any;
const service = new AccessService();
service.info = async () => ({
id: 12,
type: "eab",
} as any);
service.decryptAccessEntity = () => ({
kid: "kid-1",
});
service.update = async (param: any) => {
updateParam = param;
return param;
};
await service.updateAccess({
id: 12,
accountKey: "account-key",
});
assert.deepEqual(JSON.parse(updateParam.setting), {
kid: "kid-1",
accountKey: "account-key",
});
});
});
@@ -123,6 +123,25 @@ export class AccessService extends BaseService<AccessEntity> {
return await super.update(param);
}
async updateAccess(access: any) {
const oldEntity = await this.info(access.id);
if (oldEntity == null) {
throw new ValidateException('该授权配置不存在,请确认是否已被删除');
}
const setting = this.decryptAccessEntity(oldEntity);
for (const key of Object.keys(access)) {
if (key === 'id') {
continue;
}
setting[key] = access[key];
}
return await this.update({
id: access.id,
type: oldEntity.type,
setting: JSON.stringify(setting),
});
}
async getSimpleInfo(id: number) {
const entity = await this.info(id);
if (entity == null) {
@@ -1,4 +1,4 @@
import { Init, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { Encryptor, SysSecret, SysSettingsService } from '../../../system/index.js';
/**
@@ -12,8 +12,7 @@ export class EncryptService {
@Inject()
sysSettingService: SysSettingsService;
@Init()
async init() {
async doInit() {
const secret: SysSecret = await this.sysSettingService.getSecret();
this.encryptor = new Encryptor(secret.encryptSecret);
}
@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Performance Improvements
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
**Note:** Version bump only for package @certd/midway-flyway-js
+5 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/midway-flyway-js",
"version": "1.39.12",
"version": "1.39.15",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -13,7 +13,7 @@
"dev-build": "npm run build",
"test": "midway-bin test --ts -V",
"test1": "midway-bin test --ts -V -f test/blank.test.ts -t 'hash-check'",
"test:unit": "echo no unit tests",
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
"cov": "midway-bin cov --ts",
"prepublish": "npm run build",
"pub": "npm publish"
@@ -36,16 +36,18 @@
"@types/node": "^18",
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"cross-env": "^7.0.3",
"eslint": "^8.24.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^4.2.1",
"esmock": "^2.7.5",
"prettier": "^2.8.8",
"rimraf": "^5.0.5",
"tslib": "^2.8.1",
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
"gitHead": "bae5a04dcc0a679c290a9805c3ac4a6020eb6ec0"
}
+14
View File
@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
**Note:** Version bump only for package @certd/plugin-cert
## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11)
**Note:** Version bump only for package @certd/plugin-cert
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Performance Improvements
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
**Note:** Version bump only for package @certd/plugin-cert
+9 -7
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-cert",
"private": false,
"version": "1.39.12",
"version": "1.39.15",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -13,15 +13,15 @@
"build3": "rollup -c",
"build2": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"test:unit": "echo no unit tests",
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch"
},
"dependencies": {
"@certd/acme-client": "^1.39.12",
"@certd/basic": "^1.39.12",
"@certd/pipeline": "^1.39.12",
"@certd/plugin-lib": "^1.39.12",
"@certd/acme-client": "^1.39.15",
"@certd/basic": "^1.39.15",
"@certd/pipeline": "^1.39.15",
"@certd/plugin-lib": "^1.39.15",
"psl": "^1.9.0",
"punycode.js": "^2.3.1"
},
@@ -31,13 +31,15 @@
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"chai": "^4.3.6",
"cross-env": "^7.0.3",
"eslint": "^8.24.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"esmock": "^2.7.5",
"mocha": "^10.1.0",
"prettier": "^2.8.8",
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
"gitHead": "bae5a04dcc0a679c290a9805c3ac4a6020eb6ec0"
}
+14
View File
@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
**Note:** Version bump only for package @certd/plugin-lib
## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11)
**Note:** Version bump only for package @certd/plugin-lib
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Performance Improvements
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
**Note:** Version bump only for package @certd/plugin-lib
+9 -7
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-lib",
"private": false,
"version": "1.39.12",
"version": "1.39.15",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -13,7 +13,7 @@
"build3": "rollup -c",
"build2": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"test:unit": "mocha --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
"test:unit": "cross-env NODE_ENV=unittest mocha --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch"
},
@@ -23,10 +23,10 @@
"@alicloud/pop-core": "^1.7.10",
"@alicloud/tea-util": "^1.4.11",
"@aws-sdk/client-s3": "^3.964.0",
"@certd/acme-client": "^1.39.12",
"@certd/basic": "^1.39.12",
"@certd/pipeline": "^1.39.12",
"@certd/plus-core": "^1.39.12",
"@certd/acme-client": "^1.39.15",
"@certd/basic": "^1.39.15",
"@certd/pipeline": "^1.39.15",
"@certd/plus-core": "^1.39.15",
"@kubernetes/client-node": "0.21.0",
"ali-oss": "^6.22.0",
"basic-ftp": "^5.0.5",
@@ -50,14 +50,16 @@
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"chai": "^4.3.6",
"cross-env": "^7.0.3",
"eslint": "^8.24.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"esmock": "^2.7.5",
"mocha": "^10.1.0",
"prettier": "^2.8.8",
"ts-node": "^10.9.2",
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
"gitHead": "bae5a04dcc0a679c290a9805c3ac4a6020eb6ec0"
}
+10 -5
View File
@@ -1,4 +1,4 @@
FROM node:22-alpine AS builder
FROM node:22-alpine3.21 AS builder
# RUN apk add build-base
# RUN wget -O - https://github.com/jemalloc/jemalloc/releases/download/5.3.0/jemalloc-5.3.0.tar.bz2 | tar -xj && \
@@ -10,17 +10,22 @@ FROM node:22-alpine AS builder
WORKDIR /workspace/
COPY . /workspace/
# armv7 目前只能用node18 pnpm9不支持node18,所以pnpm只能用8.15.7版本
# https://github.com/nodejs/docker-node/issues/1946
RUN npm install -g pnpm
# pnpm v11打包会报错([ERR_PNPM_IGNORED_BUILDS] Ignored build scripts),暂时固定10.33.4版本
# https://pnpm.io/zh/migration
RUN npm install -g pnpm@10.33.4
#RUN cd /workspace/certd-client && pnpm install && npm run build
RUN cp /workspace/certd-client/dist/* /workspace/certd-server/public/ -rf
RUN cd /workspace/certd-server && pnpm install && npm run build-on-docker
# RUN cd /workspace/certd-server && \
# pnpm install --ignore-scripts && \
# yes | pnpm approve-builds && \
# pnpm rebuild && \
# npm run build-on-docker
FROM node:22-alpine
FROM node:22-alpine3.21
EXPOSE 7001
EXPOSE 7002
+29
View File
@@ -3,6 +3,35 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
### Performance Improvements
* icon选择器增加一套logo集 ([fdd5848](https://github.com/certd/certd/commit/fdd5848df4055a6ee07dc5eabaaf6b718672882d))
* **monitor/site:** 新增站点监控页面禁用启用、检查状态两个筛选条件 ([118c15d](https://github.com/certd/certd/commit/118c15d04633a6ef06f2d9e7a7849d20f596e02c))
* **network:** 新增全局公共http请求 headers设置 ([aad9045](https://github.com/certd/certd/commit/aad9045de55e76cb2ad09cac74a7bd60a4b47124))
## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11)
### Bug Fixes
* 修复阿里云订阅流水线创建对话框无法获取阿里订单列表的bug ([a362860](https://github.com/certd/certd/commit/a362860137bfb7072893c844fe775edc46070ee1))
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Bug Fixes
* cnameProvider域名支持设置子域名托管 ([7266af1](https://github.com/certd/certd/commit/7266af17491a98338022cfb18cfedfb93ca6ef8f))
### Performance Improvements
* **设置:** 添加首页启用开关配置 ([25ad1e6](https://github.com/certd/certd/commit/25ad1e6f861e43288cc8bd90d4903628e6faec29))
* **用户资料:** 新增手机号邮箱绑定功能 ([e0eb0e2](https://github.com/certd/certd/commit/e0eb0e21f6dae24b639c944f9aba2c90496ab1c0))
* 域名注册过期时间获取再次优化 ([91a1b97](https://github.com/certd/certd/commit/91a1b9755066bf280e194dabf7c3a9f936e2643f))
* **证书流水线:** 添加批量更新证书申请参数功能 ([63be1c1](https://github.com/certd/certd/commit/63be1c1cbd9b09a3b48f26130c296b1cedcca1ac))
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
* **domain:** 添加域名过期时间同步进度显示功能 ([9d2937d](https://github.com/certd/certd/commit/9d2937dd4b14ffab73e9b096edd2aa8539811182))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
### Bug Fixes
+11 -10
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/ui-client",
"version": "1.39.12",
"version": "1.39.15",
"private": true,
"scripts": {
"dev": "vite --open",
@@ -12,7 +12,7 @@
"debug:force": "vite --force --mode debug",
"build": "cross-env NODE_OPTIONS=--max-old-space-size=40960 vite build ",
"dev-build": "echo 1",
"test:unit": "echo no unit tests",
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
"test:vue": "vitest run",
"serve": "vite preview",
"preview": "vite preview",
@@ -34,11 +34,11 @@
"@aws-sdk/s3-request-presigner": "^3.964.0",
"@certd/vue-js-cron-light": "^4.0.14",
"@ctrl/tinycolor": "^4.1.0",
"@fast-crud/editor-code": "^1.27.8",
"@fast-crud/fast-crud": "^1.27.8",
"@fast-crud/fast-extends": "^1.27.8",
"@fast-crud/ui-antdv4": "^1.27.8",
"@fast-crud/ui-interface": "^1.27.8",
"@fast-crud/editor-code": "^1.28.1",
"@fast-crud/fast-crud": "^1.28.1",
"@fast-crud/fast-extends": "^1.28.1",
"@fast-crud/ui-antdv4": "^1.28.1",
"@fast-crud/ui-interface": "^1.28.1",
"@iconify/tailwind": "^1.2.0",
"@iconify/vue": "^4.1.1",
"@manypkg/get-packages": "^2.2.2",
@@ -62,7 +62,6 @@
"cos-js-sdk-v5": "^1.7.0",
"cron-parser": "^4.9.0",
"cropperjs": "^1.6.1",
"cross-env": "^7.0.3",
"cssnano": "^7.0.6",
"dayjs": "^1.11.7",
"defu": "^6.1.4",
@@ -107,8 +106,8 @@
"zod-defaults": "^0.1.3"
},
"devDependencies": {
"@certd/lib-iframe": "^1.39.12",
"@certd/pipeline": "^1.39.12",
"@certd/lib-iframe": "^1.39.15",
"@certd/pipeline": "^1.39.15",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12",
@@ -127,6 +126,7 @@
"autoprefixer": "^10.4.21",
"caller-path": "^4.0.0",
"chai": "^5.1.0",
"cross-env": "^7.0.3",
"dependency-cruiser": "^16.2.3",
"dot": "^1.1.3",
"eslint": "8.57.0",
@@ -136,6 +136,7 @@
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-vue": "^9.23.0",
"esmock": "^2.7.5",
"less": "^4.2.0",
"less-loader": "^12.2.0",
"postcss": "^8.4.35",
@@ -0,0 +1,106 @@
<template>
<div class="refresh-input">
<div class="refresh-input-line">
<a-input class="refresh-input-control" :value="value" :placeholder="placeholder" allow-clear @update:value="emit('update:value', $event)"></a-input>
<fs-button :loading="loading" type="primary" :text="buttonText" :icon="icon" @click="doRefresh"></fs-button>
</div>
<div class="helper" :class="{ error: hasError }">
{{ message }}
</div>
</div>
</template>
<script setup lang="ts">
import { ComponentPropsType, doRequest } from "/@/components/plugins/lib";
import { computed, inject, ref } from "vue";
import { Form } from "ant-design-vue";
import { getInputFromForm } from "./utils";
defineOptions({
name: "RefreshInput",
});
type RefreshInputProps = ComponentPropsType & {
buttonText?: string;
icon?: string;
placeholder?: string;
successMessage?: string;
};
const fromType: any = inject("getFromType");
const getScope: any = inject("get:scope");
const getPluginType: any = inject("get:plugin:type", () => {
return "access";
});
const formItemContext = Form.useInjectFormItemContext();
const props = defineProps<RefreshInputProps>();
const emit = defineEmits<{
"update:value": [value: string];
}>();
const loading = ref(false);
const message = ref("");
const hasError = ref(false);
const action = computed(() => props.action);
const buttonText = computed(() => props.buttonText || "刷新");
const icon = computed(() => props.icon || "ion:refresh-outline");
const placeholder = computed(() => props.placeholder || "");
const successMessage = computed(() => props.successMessage || "刷新成功,请保存配置");
const doRefresh = async () => {
if (loading.value) {
return;
}
if (!action.value) {
hasError.value = true;
message.value = "缺少刷新动作配置";
return;
}
formItemContext.onFieldChange();
const { form } = getScope();
const pluginType = getPluginType();
const { input, record } = getInputFromForm(form, pluginType);
loading.value = true;
message.value = "";
hasError.value = false;
try {
const res = await doRequest(
{
type: pluginType,
typeName: form.type,
action: action.value,
input,
record,
fromType,
},
{
onError(err: any) {
hasError.value = true;
message.value = err.message;
},
showErrorNotify: false,
}
);
emit("update:value", res);
message.value = successMessage.value;
} finally {
loading.value = false;
}
};
</script>
<style lang="less" scoped>
.refresh-input-line {
display: flex;
gap: 8px;
align-items: center;
}
.refresh-input-control {
flex: 1;
}
</style>
@@ -74,7 +74,6 @@ const props = defineProps<
uploadCert?: UploadCertProps;
} & ComponentPropsType
>();
debugger;
const emit = defineEmits<{
"update:value": any;
}>();
@@ -141,8 +140,7 @@ const getOptions = async () => {
}
}
}
message.value = "";
message.value = "获取中...";
hasError.value = false;
loading.value = true;
const pageNo = pagerRef.value.pageNo;
@@ -12,6 +12,7 @@ import AccessSelector from "/@/views/certd/access/access-selector/index.vue";
import InputPassword from "./common/input-password.vue";
import CertInfoUpdater from "/@/views/certd/pipeline/cert-upload/index.vue";
import ApiTest from "./common/api-test.vue";
import RefreshInput from "./common/refresh-input.vue";
import ParamsShow from "./common/params-show.vue";
export * from "./cert/index.js";
export default {
@@ -23,6 +24,7 @@ export default {
app.component("CertInfoUpdater", CertInfoUpdater);
app.component("ApiTest", ApiTest);
app.component("RefreshInput", RefreshInput);
app.component("SynologyDeviceIdGetter", SynologyIdDeviceGetter);
app.component("RemoteAutoComplete", RemoteAutoComplete);
@@ -1,13 +1,16 @@
export default {
cnameTitle: "CNAME Service Configuration",
cnameDescription:
"The domain name configured here serves as a proxy for verifying other domains. When other domains apply for certificates, they map to this domain via CNAME for ownership verification. The advantage is that any domain can apply for a certificate this way without providing an AccessSecret.",
"The domain name configured here serves as a proxy for verifying other domains. When other domains apply for certificates, they map to this domain via CNAME for ownership verification. The advantage is that any domain can apply for a certificate this way without providing an AccessSecret.",
cnameLinkText: "CNAME principle and usage instructions",
cnameDomain: "CNAME Domain",
cnameDomainPlaceholder: "cname.handsfree.work",
cnameDomainHelper:
"Requires a domain registered with a DNS provider on the right (or you can transfer other domain DNS servers here).\nOnce the CNAME domain is set, it cannot be changed. It is recommended to use a first-level subdomain.",
"Requires a domain registered with a DNS provider on the right (or you can transfer other domain DNS servers here).\nOnce the CNAME domain is set, it cannot be changed. It is recommended to use a first-level subdomain.",
cnameDomainPattern: "Domain name cannot contain *",
cnameProviderSubdomain: "Delegated Subdomain",
cnameProviderSubdomainPlaceholder: "sub.example.com",
cnameProviderSubdomainHelper: "Fill this when the CNAME domain is hosted under a delegated subdomain, for example CNAME domain cname.sub.example.com and DNS zone sub.example.com.",
dnsProvider: "DNS Provider",
dnsProviderAuthorization: "DNS Provider Authorization",
};
@@ -91,6 +91,8 @@ export default {
reverseProxyEmpty: "No reverse proxy list configured",
environmentVars: "Environment Variables",
environmentVarsHelper: "configure the runtime environment variables, one per line, format: KEY=VALUE",
commonHeaders: "Common Headers",
commonHeadersHelper: "Common headers automatically added to server-side HTTP requests, one per line, format: KEY=VALUE. Existing request headers with the same name are not overwritten.",
bindUrl: "Bind URL",
bindUrlHelper: "Bind URL, used as your site URL in notifications",
@@ -6,6 +6,9 @@ export default {
cnameDomainPlaceholder: "cname.handsfree.work",
cnameDomainHelper: "需要一个右边DNS提供商注册的域名(也可以将其他域名的dns服务器转移到这几家来)。\nCNAME域名一旦确定不可修改,建议使用一级子域名",
cnameDomainPattern: "域名不能使用星号",
cnameProviderSubdomain: "托管子域名",
cnameProviderSubdomainPlaceholder: "sub.example.com",
cnameProviderSubdomainHelper: "当CNAME域名本身托管在子域名下时填写,例如 CNAME域名为 cname.sub.example.com,实际DNS托管域为 sub.example.com",
dnsProvider: "DNS提供商",
dnsProviderAuthorization: "DNS提供商授权",
};
@@ -89,6 +89,8 @@ export default {
reverseProxyEmpty: "未配置反向代理",
environmentVars: "环境变量",
environmentVarsHelper: "配置运行时环境变量,每行一个,格式:KEY=VALUE",
commonHeaders: "公共请求头",
commonHeadersHelper: "服务端发起 HTTP 请求时自动附加的公共请求头,每行一个,格式:KEY=VALUE;请求中已设置同名 Header 时不会覆盖\n注意: 不要将token等敏感内容放在此处,仅限个人和公司内部使用,商业版不要设置",
bindUrl: "绑定URL",
bindUrlHelper: "绑定URL,在各类通知中显示你的站点URL",
},
@@ -99,6 +99,7 @@ export type SuiteSetting = {
export type SysPrivateSetting = {
httpProxy?: string;
httpsProxy?: string;
commonHeaders?: string;
reverseProxies?: any;
dnsResultOrder?: string;
commonCnameEnabled?: boolean;
@@ -11,7 +11,17 @@ export type MergeScriptContext = {
export function useReference(formItem: any) {
if (formItem.mergeScript) {
const ctx = {
compute,
compute: (opts: any) => {
const func = (context: any) => {
let form = context.form || {};
form = form.input || form.body || form.access || form;
return opts({
...context,
form,
});
};
return compute(func);
},
asyncCompute,
computed,
};
@@ -23,6 +23,37 @@ export async function UpdateProfile(form: any) {
});
}
export async function GetContactCapability() {
return await request({
url: "/mine/contact/capability",
method: "POST",
});
}
export async function UpdateMobile(form: any) {
return await request({
url: "/mine/contact/mobile",
method: "POST",
data: form,
});
}
export async function VerifyContactIdentity(form: any) {
return await request({
url: "/mine/contact/verifyIdentity",
method: "POST",
data: form,
});
}
export async function UpdateEmail(form: any) {
return await request({
url: "/mine/contact/email",
method: "POST",
data: form,
});
}
export async function GetOauthBounds() {
return await request({
url: "/oauth/bounds",
@@ -0,0 +1,31 @@
import { defineComponent } from "vue";
import SmsCode from "/@/views/framework/login/sms-code.vue";
import EmailCode from "/@/views/framework/register/email-code.vue";
export const ContactCodeInput = defineComponent({
name: "ContactCodeInput",
props: {
modelValue: {
type: String,
default: "",
},
form: {
type: Object,
required: true,
},
type: {
type: String,
required: true,
},
},
emits: ["update:modelValue"],
setup(props, { emit }) {
const onChange = (value: string) => emit("update:modelValue", value);
return () => {
if (props.type === "email") {
return <EmailCode value={props.modelValue} captcha={props.form.contactCaptcha} email={props.form.email} verificationType="bindEmail" onUpdate:value={onChange} />;
}
return <SmsCode value={props.modelValue} captcha={props.form.contactCaptcha} mobile={props.form.mobile} phoneCode={props.form.phoneCode} verificationType="bindMobile" onUpdate:value={onChange} />;
};
},
});
@@ -0,0 +1,33 @@
import { defineComponent } from "vue";
import SmsCode from "/@/views/framework/login/sms-code.vue";
import EmailCode from "/@/views/framework/register/email-code.vue";
export const IdentityCodeInput = defineComponent({
name: "IdentityCodeInput",
props: {
modelValue: {
type: String,
default: "",
},
form: {
type: Object,
required: true,
},
userInfo: {
type: Object,
required: true,
},
},
emits: ["update:modelValue"],
setup(props, { emit }) {
const onChange = (value: string) => emit("update:modelValue", value);
return () => {
if (props.form.identityType === "email") {
return <EmailCode value={props.modelValue} captcha={props.form.identityCaptcha} email={props.userInfo.email} verificationType="contactIdentity" onUpdate:value={onChange} />;
}
return (
<SmsCode value={props.modelValue} captcha={props.form.identityCaptcha} mobile={props.userInfo.mobile} phoneCode={props.userInfo.phoneCode || "86"} verificationType="contactIdentity" onUpdate:value={onChange} />
);
};
},
});
@@ -1,20 +1,22 @@
// useUserProfile, 获取 openEditProfileDialog ,参考 useTemplate方法
import { useFormWrapper } from "@fast-crud/fast-crud";
import { ref } from "vue";
import { cloneDeep, merge } from "lodash-es";
import { compute, dict } from "@fast-crud/fast-crud";
// 假设的 API 导入
import * as userProfileApi from "./api";
import { useUserStore } from "/@/store/user";
import { useI18n } from "/src/locales";
import CaptchaInput from "/@/components/captcha/captcha-input.vue";
import { message } from "ant-design-vue";
import { ContactCodeInput } from "./contact-code-input";
import { IdentityCodeInput } from "./identity-code-input";
import { useFormDialog } from "/@/use/use-dialog";
/**
*
* @returns {{openEditProfileDialog: openEditProfileDialog}}
*/
export function useUserProfile() {
const { openCrudFormDialog } = useFormWrapper();
const wrapperRef = ref();
const { openFormDialog } = useFormDialog();
async function openEditProfileDialog(req: { onUpdated?: (ctx: any) => void }) {
const detail = await userProfileApi.getMineInfo();
if (!detail) {
@@ -24,31 +26,28 @@ export function useUserProfile() {
const { t } = useI18n();
const userStore = useUserStore();
const userProfileFormRef = ref();
async function doSubmit(opts: { form: any }) {
const form = opts.form;
async function doSubmit(form: any) {
const { id } = await userProfileApi.UpdateProfile(form);
if (req.onUpdated) {
req.onUpdated({ id });
}
}
const crudOptions: any = {
form: {
doSubmit,
wrapper: {
title: `编辑用户资料`,
width: 1100,
onOpened(opts: { form: any }) {
merge(opts.form, detail);
},
},
await openFormDialog({
title: `编辑用户资料`,
wrapper: {
width: 600,
},
initialForm: detail,
onSubmit: doSubmit,
columns: {
nickName: {
title: t("certd.nickName"),
type: "text",
form: {
col: {
span: 24,
},
component: {
placeholder: t("certd.nickName"),
},
@@ -71,6 +70,9 @@ export function useUserProfile() {
},
},
form: {
col: {
span: 24,
},
component: {
vModel: "modelValue",
valueType: "key",
@@ -98,10 +100,7 @@ export function useUserProfile() {
},
},
},
};
const wrapper = await openCrudFormDialog({ crudOptions });
wrapperRef.value = wrapper;
});
}
return {
@@ -110,26 +109,20 @@ export function useUserProfile() {
}
export function usePasskeyRegister() {
const { openCrudFormDialog } = useFormWrapper();
const wrapperRef = ref();
const { openFormDialog } = useFormDialog();
async function openRegisterDialog(req: { onSubmit?: (ctx: any) => void }) {
const { t } = useI18n();
const userStore = useUserStore();
const deviceNameRef = ref();
const crudOptions: any = {
form: {
wrapper: {
title: t("authentication.registerPasskey"),
width: 500,
onOpened(opts: { form: any }) {
opts.form.deviceName = "";
},
},
onSubmit: req.onSubmit,
afterSubmit: null,
onSuccess: null,
await openFormDialog({
title: t("authentication.registerPasskey"),
wrapper: {
width: 500,
},
initialForm: {
deviceName: "",
},
onSubmit: async (form: any) => {
await req.onSubmit?.({ form });
},
columns: {
deviceName: {
@@ -147,15 +140,229 @@ export function usePasskeyRegister() {
},
},
},
};
const wrapper = await openCrudFormDialog({ crudOptions });
wrapperRef.value = wrapper;
return wrapper;
});
}
return {
openRegisterDialog,
};
}
export function useContactBind() {
const { openFormDialog } = useFormDialog();
async function openContactBindDialog(req: { type: "mobile" | "email"; userInfo: any; contactCapability: { smsEnabled?: boolean }; onUpdated?: () => Promise<void> | void }) {
const methods = [{ label: "密码", value: "password" }];
if (req.userInfo.email) {
methods.push({ label: "邮箱", value: "email" });
}
if (req.contactCapability.smsEnabled && req.userInfo.mobile) {
methods.push({ label: "手机号", value: "mobile" });
}
async function openChangeDialog(identityValidationCode: string) {
const isMobile = req.type === "mobile";
await openFormDialog({
title: isMobile ? (req.userInfo.mobile ? "修改手机号" : "绑定手机号") : req.userInfo.email ? "修改邮箱" : "绑定邮箱",
wrapper: {
width: 560,
},
initialForm: {
phoneCode: req.userInfo.phoneCode || "86",
mobile: req.userInfo.mobile || "",
email: req.userInfo.email || "",
contactCaptcha: null,
contactValidateCode: "",
},
async onSubmit(form: any) {
if (isMobile) {
await userProfileApi.UpdateMobile({
phoneCode: form.phoneCode,
mobile: form.mobile,
validateCode: form.contactValidateCode,
identityValidationCode,
});
} else {
await userProfileApi.UpdateEmail({
email: form.email,
validateCode: form.contactValidateCode,
identityValidationCode,
});
}
message.success("绑定信息已更新");
await req.onUpdated?.();
},
columns: {
phoneCode: {
title: "区号",
type: "text",
form: {
col: {
span: 24,
},
show: isMobile,
component: {
placeholder: "区号",
},
rules: [{ required: isMobile, message: "请输入区号" }],
},
},
mobile: {
title: "手机号",
type: "text",
form: {
col: {
span: 24,
},
show: isMobile,
component: {
placeholder: "请输入手机号",
},
rules: [
{ required: isMobile, message: "请输入手机号" },
{ pattern: /^\d{4,20}$/, message: "请输入正确的手机号" },
],
},
},
email: {
title: "邮箱",
type: "text",
form: {
col: {
span: 24,
},
show: !isMobile,
component: {
placeholder: "请输入邮箱",
},
rules: [
{ required: !isMobile, message: "请输入邮箱" },
{ type: "email", message: "请输入正确的邮箱" },
],
},
},
contactCaptcha: {
title: "图形验证码",
form: {
col: {
span: 24,
},
component: {
name: CaptchaInput,
vModel: "modelValue",
},
rules: [{ required: true, message: "请完成图形验证码" }],
},
},
contactValidateCode: {
title: isMobile ? "新手机号验证码" : "新邮箱验证码",
form: {
col: {
span: 24,
},
component: {
name: ContactCodeInput,
vModel: "modelValue",
form: compute(({ form }) => form),
type: req.type,
},
rules: [{ required: true, message: "请输入验证码" }],
},
},
},
});
}
await openFormDialog({
title: "验证本人操作",
wrapper: {
width: 520,
},
initialForm: {
identityType: "password",
identityPassword: "",
identityCaptcha: null,
identityValidateCode: "",
},
async onSubmit(form: any) {
const res = await userProfileApi.VerifyContactIdentity({
identityType: form.identityType,
identityPassword: form.identityPassword,
identityValidateCode: form.identityValidateCode,
});
await openChangeDialog(res.validationCode);
},
columns: {
identityType: {
title: "验证方式",
form: {
col: {
span: 24,
},
component: {
name: "fs-dict-radio",
vModel: "value",
dict: dict({
data: methods,
}),
},
rules: [{ required: true, message: "请选择验证方式" }],
valueChange({ form }: { form: any }) {
form.identityPassword = "";
form.identityCaptcha = null;
form.identityValidateCode = "";
},
},
},
identityPassword: {
title: "登录密码",
type: "password",
form: {
col: {
span: 24,
},
show: compute(({ form }) => form.identityType === "password"),
component: {
placeholder: "请输入登录密码",
},
rules: [{ required: true, message: "请输入登录密码" }],
},
},
identityCaptcha: {
title: "图形验证码",
form: {
col: {
span: 24,
},
show: compute(({ form }) => form.identityType !== "password"),
component: {
name: CaptchaInput,
vModel: "modelValue",
},
rules: [{ required: true, message: "请完成图形验证码" }],
},
},
identityValidateCode: {
title: "验证码",
form: {
col: {
span: 24,
},
show: compute(({ form }) => form.identityType !== "password"),
component: {
name: IdentityCodeInput,
vModel: "modelValue",
form: compute(({ form }) => form),
userInfo: req.userInfo,
},
rules: [{ required: true, message: "请输入验证码" }],
},
},
},
});
}
return {
openContactBindDialog,
};
}
@@ -13,32 +13,41 @@
<a-avatar v-else size="100" class="user-avatar default-avatar">
{{ userInfo.username }}
</a-avatar>
<a-button type="text" size="small" class="avatar-edit-btn" title="修改资料" @click.stop="doUpdate">
<template #icon><fs-icon icon="ion:create-outline" /></template>
</a-button>
<!-- <div class="status-indicator"></div> -->
</div>
<div class="user-info">
<h2 class="user-name flex items-center">
{{ userInfo.nickName }}
<fs-values-format :model-value="userInfo.roleIds" :dict="roleDict" color="blue" />
<span>{{ userInfo.nickName }}</span>
<a-button type="text" size="small" class="detail-edit-btn" title="修改资料" @click.stop="doUpdate">
<template #icon><fs-icon icon="ion:create-outline" /></template>
</a-button>
</h2>
<div class="user-details">
<a-tag color="blue" class="detail-tag">
<span class="tag-icon">👤</span>
{{ userInfo.username }}
<span class="tag-text">{{ userInfo.username }}</span>
<fs-values-format :model-value="userInfo.roleIds" type="text" :dict="roleDict" color="blue" />
</a-tag>
<a-tag v-if="userInfo.email" color="green" class="detail-tag">
<a-tag color="green" class="detail-tag">
<span class="tag-icon">📧</span>
{{ userInfo.email }}
<span class="tag-text">{{ userInfo.email || "未绑定邮箱" }}</span>
<a-button type="text" size="small" class="detail-edit-btn" title="修改邮箱" @click.stop="openBindContact('email')">
<template #icon><fs-icon icon="ion:create-outline" /></template>
</a-button>
</a-tag>
<a-tag v-if="userInfo.mobile" color="purple" class="detail-tag">
<a-tag v-if="contactCapability.smsEnabled" color="purple" class="detail-tag">
<span class="tag-icon">📱</span>
{{ userInfo.mobile }}
<span class="tag-text">{{ userInfo.mobile || "未绑定手机号" }}</span>
<a-button type="text" size="small" class="detail-edit-btn" title="修改手机号" @click.stop="openBindContact('mobile')">
<template #icon><fs-icon icon="ion:create-outline" /></template>
</a-button>
</a-tag>
</div>
</div>
<div class="action-buttons gap-2">
<a-button type="primary" class="action-btn" @click="doUpdate">
{{ t("authentication.updateProfile") }}
</a-button>
<change-password-button :show-button="true" />
<a-button type="primary" class="action-btn" @click="goSecuritySetting">
@@ -139,7 +148,7 @@ import * as api from "./api";
import { computed, onMounted, Ref, ref } from "vue";
import ChangePasswordButton from "/@/views/certd/mine/change-password-button.vue";
import { useI18n } from "/src/locales";
import { useUserProfile } from "./use";
import { useContactBind, useUserProfile } from "./use";
import { usePasskeyRegister } from "./use";
import { message, Modal, notification } from "ant-design-vue";
import { useSettingStore } from "/@/store/settings";
@@ -160,6 +169,9 @@ const settingStore = useSettingStore();
const userInfo: Ref = ref({});
const passkeys = ref([]);
const passkeySupported = ref(false);
const contactCapability = ref({
smsEnabled: false,
});
const getUserInfo = async () => {
userInfo.value = await api.getMineInfo();
@@ -177,6 +189,7 @@ function doUpdate() {
openEditProfileDialog({
onUpdated: async () => {
await getUserInfo();
userStore.setUserInfo(userInfo.value);
},
});
}
@@ -237,6 +250,23 @@ async function loadPasskeys() {
}
}
async function loadContactCapability() {
contactCapability.value = await api.GetContactCapability();
}
const { openContactBindDialog } = useContactBind();
async function openBindContact(type: "mobile" | "email") {
await openContactBindDialog({
type,
userInfo: userInfo.value,
contactCapability: contactCapability.value,
onUpdated: async () => {
await getUserInfo();
userStore.setUserInfo(userInfo.value);
},
});
}
async function unbindPasskey(id: number) {
Modal.confirm({
title: "确认解绑吗?",
@@ -366,6 +396,7 @@ const userAvatar = computed(() => {
onMounted(async () => {
await getUserInfo();
await loadContactCapability();
await loadOauthBounds();
await loadOauthProviders();
await loadPasskeys();
@@ -567,6 +598,24 @@ onMounted(async () => {
flex-shrink: 0;
}
.avatar-edit-btn {
position: absolute;
right: 2px;
bottom: 2px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
min-width: 28px;
padding: 0;
color: #667eea;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 50%;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
}
.user-avatar {
border: 4px solid #ffffff;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
@@ -613,6 +662,18 @@ onMounted(async () => {
font-size: 13px;
}
.detail-edit-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
min-width: 20px;
margin: -2px -6px -2px 0;
padding: 0;
color: #667eea;
}
.tag-icon {
font-size: 14px;
}
@@ -863,18 +924,84 @@ onMounted(async () => {
}
@media (max-width: 768px) {
.card-header {
padding: 32px 16px;
}
.header-content {
flex-direction: column;
text-align: center;
gap: 18px;
}
.avatar-wrapper {
margin-bottom: 2px;
}
.user-avatar {
width: 88px !important;
height: 88px !important;
line-height: 88px !important;
}
.avatar-edit-btn {
right: -2px;
bottom: 0;
}
.user-name {
justify-content: center;
margin-bottom: 14px;
font-size: 22px;
gap: 6px;
}
.user-details {
justify-content: center;
flex-direction: column;
align-items: center;
gap: 6px;
width: 100%;
}
.detail-tag {
justify-content: flex-start;
width: min(230px, calc(100vw - 96px));
min-height: 34px;
padding: 5px 8px 5px 12px;
border-radius: 12px;
white-space: nowrap;
margin-right: 0;
text-align: left;
}
.tag-icon {
flex: 0 0 auto;
}
.tag-text {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.detail-edit-btn {
margin: 0 -4px 0 2px;
flex: 0 0 auto;
}
.action-buttons {
justify-content: center;
width: 100%;
gap: 10px;
margin-top: 2px;
}
.action-buttons :deep(.ant-btn) {
min-width: 90px;
height: 32px;
padding: 4px 12px;
}
}
}
@@ -320,6 +320,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
title: t("monitor.siteName"),
search: {
show: true,
col: {
span: 3,
},
},
type: "text",
form: {
@@ -333,6 +336,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
title: t("monitor.domain"),
search: {
show: true,
col: {
span: 3,
},
},
type: "text",
form: {
@@ -411,6 +417,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
title: t("monitor.certDomains"),
search: {
show: true,
col: {
span: 3,
},
},
type: "text",
form: {
@@ -451,6 +460,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
title: t("monitor.certStatus"),
search: {
show: true,
col: {
span: 2,
},
},
type: "dict-select",
dict: dict({
@@ -472,7 +484,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
checkStatus: {
title: t("monitor.checkStatus"),
search: {
show: false,
show: true,
col: {
span: 2,
},
},
type: "dict-select",
dict: checkStatusDict,
@@ -578,6 +593,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
type: "dict-select",
search: {
show: true,
col: {
span: 3,
},
},
dict: groupDictRef,
form: {
@@ -631,7 +649,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
disabled: {
title: t("monitor.disabled"),
search: {
show: false,
show: true,
col: {
span: 2,
},
},
type: "dict-switch",
dict: dict({
@@ -94,9 +94,15 @@ export function setRunnableIds(pipeline: any) {
return JSON.parse(content);
}
export function useCertPipelineCreator() {
export function useCertPipelineCreator({ formWrapperRef }: { formWrapperRef: Ref<any> }) {
const { t } = useI18n();
const { openCrudFormDialog } = useFormWrapper();
function open(opts: any) {
return new Promise((resolve, reject) => {
formWrapperRef.value.open(opts);
});
}
const { openCrudFormDialog } = useFormWrapper({ open });
const pluginStore = usePluginStore();
const settingStore = useSettingStore();
@@ -111,10 +117,10 @@ export function useCertPipelineCreator() {
// inputs[inputKey].form.show = true;
const inputDefine = cloneDeep(certPlugin.input[inputKey]);
if (inputDefine.maybeNeed) {
moreParams.push(inputKey);
moreParams.push("input." + inputKey);
}
useReference(inputDefine);
inputs[inputKey] = {
inputs["input." + inputKey] = {
title: inputDefine.title,
form: {
...inputDefine,
@@ -135,17 +141,19 @@ export function useCertPipelineCreator() {
const DEFAULT_RENEW_DAYS = settingStore.sysPublic.defaultCertRenewDays || settingStore.sysPublic.defaultWillExpireDays || 20;
merge(inputs, {
renewDays: {
"input.renewDays": {
form: {
value: DEFAULT_RENEW_DAYS,
},
},
});
const initialForm = req.initialForm || {};
initialForm.type = certPlugin.name;
return {
crudOptions: {
form: {
initialForm: req.initialForm || {},
initialForm: initialForm,
doSubmit,
wrapper: {
wrapClassName: "cert_pipeline_create_form",
@@ -164,44 +172,6 @@ export function useCertPipelineCreator() {
},
},
columns: {
// certApplyPlugin: {
// title: t("certd.plugin.selectTitle"),
// type: "dict-select",
// dict: dict({
// data: [
// { value: "CertApply", label: "JS-ACME" },
// { value: "CertApplyLego", label: "Lego-ACME" },
// { value: "CertApplyGetFormAliyun", label: "Aliyun-Order" },
// ],
// }),
// form: {
// order: 0,
// value: "CertApply",
// helper: {
// render: () => {
// return (
// <ul>
// <li>{t("certd.plugin.jsAcme")}</li>
// <li>{t("certd.plugin.legoAcme")}</li>
// <li>{t("certd.plugin.aliyunOrder")}</li>
// </ul>
// );
// },
// },
// valueChange: {
// handle: async ({ form, value }) => {
// const config = await pluginStore.getPluginConfig({
// name: value,
// type: "builtIn",
// });
// if (config.sysSetting?.input) {
// merge(form, config.sysSetting.input);
// }
// },
// immediate: true,
// },
// },
// },
...inputs,
triggerCron: {
title: t("certd.pipelineForm.triggerCronTitle"),
@@ -346,20 +316,20 @@ export function useCertPipelineCreator() {
await checkPipelineLimit();
//设置系统初始值
const initialForm: any = {};
const initialForm: any = { input: {} };
const pluginSysConfig = await pluginStore.getPluginConfig({ name: req.pluginName, type: "builtIn" });
if (pluginSysConfig.sysSetting?.input) {
for (const key in pluginSysConfig.sysSetting?.input) {
initialForm[key] = pluginSysConfig.sysSetting?.input[key];
initialForm.input[key] = pluginSysConfig.sysSetting?.input[key];
}
}
async function doSubmit({ form }: any) {
// const certDetail = readCertDetail(form.cert.crt);
// 添加certd pipeline
const pluginInput = omit(form, ["triggerCron", "notification", "notificationTarget", "notificationWhen", "certApplyPlugin", "groupId"]);
const pluginInput = form.input;
let pipeline: any = {
title: form.domains[0] + "证书自动化",
title: pluginInput.domains[0] + "证书自动化",
runnableType: "pipeline",
stages: [
{
@@ -50,6 +50,7 @@
<div>{{ t("certd.applyCertificate") }}</div>
</template>
</fs-crud>
<fs-form-wrapper ref="formWrapperRef"></fs-form-wrapper>
</fs-page>
</template>
@@ -90,10 +91,9 @@ function onActionbarMoreItemClick(req: { key: string; item: any }) {
openCertApplyDialog({ key: req.key, title: req.item?.title });
}
const certdFormRef = ref<typeof CertdForm>();
const currentPluginRef = ref();
provide("getCurrentPluginDefine", () => {
return currentPluginRef.value;
return currentPluginRef;
});
const addMorePipelineBtns = computed(() => {
@@ -104,7 +104,9 @@ const addMorePipelineBtns = computed(() => {
{ key: "BatchAddPipeline", title: t("certd.pipelinePage.batchAddPipeline"), icon: "ion:duplicate" },
];
});
const { openAddCertdPipelineDialog } = useCertPipelineCreator();
const formWrapperRef = ref<any>();
const { openAddCertdPipelineDialog } = useCertPipelineCreator({ formWrapperRef });
function openCertApplyDialog(req: { key: string; title: string }) {
if (req.key === "AddPipeline") {
crudExpose.openAdd({});
@@ -48,7 +48,7 @@ async function handleOauthToken() {
//
userStore.onLoginSuccess(res);
//
router.replace("/");
router.replace("/index");
return;
}
if (res.bindRequired) {
@@ -97,6 +97,21 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
width: 200,
},
},
subdomain: {
title: t("certd.cnameProviderSubdomain"),
type: "text",
form: {
component: {
placeholder: t("certd.cnameProviderSubdomainPlaceholder"),
},
helper: t("certd.cnameProviderSubdomainHelper"),
rules: [{ pattern: /^[^*]+$/, message: t("certd.cnameDomainPattern") }],
},
column: {
width: 200,
show: false,
},
},
dnsProviderType: {
title: t("certd.dnsProvider"),
type: "dict-select",
@@ -19,6 +19,11 @@
<div class="helper">{{ t("certd.sys.setting.environmentVarsHelper") }}</div>
</a-form-item>
<a-form-item :label="t('certd.sys.setting.commonHeaders')" :name="['private', 'commonHeaders']">
<a-textarea v-model:value="formState.private.commonHeaders" :placeholder="commonHeadersExample" rows="4" />
<div class="helper">{{ t("certd.sys.setting.commonHeadersHelper") }}</div>
</a-form-item>
<a-form-item :label="t('certd.dualStackNetwork')" :name="['private', 'dnsResultOrder']">
<a-select v-model:value="formState.private.dnsResultOrder">
<a-select-option value="verbatim">{{ t("certd.default") }}</a-select-option>
@@ -64,6 +69,10 @@ const environmentVarsExample = ref(
`ALIYUN_CLIENT_CONNECT_TIMEOUT=16000 #连接超时,单位毫秒
ALIYUN_CLIENT_READ_TIMEOUT=16000 #读取数据超时单位毫秒`
);
const commonHeadersExample = ref(
`User-Agent=certd
X-Custom-Header=value`
);
const formState = reactive<Partial<SysSettings>>({
public: {},
+33
View File
@@ -3,6 +3,39 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
### Bug Fixes
* 修复第三方登录彩虹登录不上的bug ([bae4f8e](https://github.com/certd/certd/commit/bae4f8e3209d9f9869ecbd7c01655383bac2fe21))
### Performance Improvements
* icon选择器增加一套logo集 ([fdd5848](https://github.com/certd/certd/commit/fdd5848df4055a6ee07dc5eabaaf6b718672882d))
## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11)
### Bug Fixes
* 修复启动时报密钥备份不存在的问题 ([c966896](https://github.com/certd/certd/commit/c9668965226af6b54e0e576931dcba8b3d188ef3))
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Bug Fixes
* **aliyun-access:** 添加阿里云密钥校验失败的错误处理 ([b75c625](https://github.com/certd/certd/commit/b75c625ddcc0b3110699d8e6175681ef157b25df))
* cnameProvider域名支持设置子域名托管 ([7266af1](https://github.com/certd/certd/commit/7266af17491a98338022cfb18cfedfb93ca6ef8f))
* **plugin-aliyun:** 过滤非CAS证书并优化日志信息 ([c4b01da](https://github.com/certd/certd/commit/c4b01da384bc40a241a673ea8bc01ca733c04d83))
### Performance Improvements
* **用户资料:** 新增手机号邮箱绑定功能 ([e0eb0e2](https://github.com/certd/certd/commit/e0eb0e21f6dae24b639c944f9aba2c90496ab1c0))
* 域名注册过期时间获取再次优化 ([91a1b97](https://github.com/certd/certd/commit/91a1b9755066bf280e194dabf7c3a9f936e2643f))
* **证书流水线:** 添加批量更新证书申请参数功能 ([63be1c1](https://github.com/certd/certd/commit/63be1c1cbd9b09a3b48f26130c296b1cedcca1ac))
* 支持火山云vke ([bb46cb0](https://github.com/certd/certd/commit/bb46cb08f71f6ae921543f7e4a6c5f4e0190556e))
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
* **plugin-volcengine:** 支持火山引擎VKE部署插件 ([b8a64a6](https://github.com/certd/certd/commit/b8a64a6b5bf3691a47177de42bc49b798e795feb))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
### Bug Fixes
@@ -0,0 +1 @@
ALTER TABLE cd_cname_provider ADD COLUMN subdomain varchar(100);
@@ -0,0 +1 @@
ALTER TABLE cd_cname_provider ADD COLUMN subdomain varchar(100);
@@ -0,0 +1 @@
ALTER TABLE cd_cname_provider ADD COLUMN subdomain varchar(100);
@@ -3,6 +3,26 @@ title: EAB授权
desc: ZeroSSL证书申请需要EAB授权
icon: ic:outline-lock
input:
eabType:
title: EAB类型
component:
name: a-select
options:
- value: google
label: Google
icon: flat-color-icons:google
- value: zerossl
label: ZeroSSL
icon: emojione:digit-zero
- value: litessl
label: litessl
icon: roentgen:free
- value: sslcom
label: SSL.com
icon: la:expeditedssl
helper: 请选择EAB类型
required: true
encrypt: false
kid:
title: KID
component:
@@ -24,8 +44,20 @@ input:
rules:
- type: email
message: 请输入正确的邮箱
helper: Google的EAB申请证书,更换邮箱会导致EAB失效,可以在此处绑定一个邮箱避免此问题
helper: 绑定一个邮箱避免失效
required: true
accountKey:
title: ACME账号私钥
component:
name: refresh-input
action: GenerateAccountKey
buttonText: 生成
successMessage: 账号私钥已生成,请保存授权配置
required: true
helper: |-
如果修改了KID,请点击生成重新生成账号私钥
注意:google的EAB只能生成一次账号私钥,更新私钥需要获取一个新的EAB授权
encrypt: true
pluginType: access
type: builtIn
scriptFilePath: /plugins/plugin-cert/access/eab-access.js
@@ -50,6 +50,18 @@ input:
component:
name: fs-icon-selector
vModel: modelValue
iconSets:
- streamline-logos
- logos
- fa-brands
- fa-solid
- fa-regular
- carbon
- ion
- ant-design
- mdi
- twemoji
- svg-spinners
required: false
appId:
title: AppId
@@ -10,6 +10,18 @@ input:
component:
name: fs-icon-selector
vModel: modelValue
iconSets:
- streamline-logos
- logos
- fa-brands
- fa-solid
- fa-regular
- carbon
- ion
- ant-design
- mdi
- twemoji
- svg-spinners
required: false
clientId:
title: ClientId
@@ -8,6 +8,7 @@ desc: 根据证书id一键更新腾讯云证书并自动部署(Id不变),
icon: svg:icon-tencentcloud
group: tencent
needPlus: false
deprecated: 腾讯更新证书(Id不变)接口已失效,本插件已下架,请使用其他接口
input:
cert:
title: 域名证书
@@ -68,12 +68,12 @@ input:
component:
name: remote-select
vModel: value
mode: tags
mode: default
type: plugin
action: onGetClusterList
search: false
pager: false
multi: true
multi: false
watches:
- certDomains
- accessId
@@ -102,8 +102,6 @@ input:
value: Public
- label: 私网
value: Private
- label: 集群内
value: TargetCluster
value: Public
required: true
order: 0
+16 -15
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/ui-server",
"version": "1.39.12",
"version": "1.39.15",
"description": "fast-server base midway",
"private": true,
"type": "module",
@@ -53,20 +53,20 @@
"@aws-sdk/client-sts": "^3.990.0",
"@azure/arm-dns": "^5.1.0",
"@azure/identity": "^4.13.1",
"@certd/acme-client": "^1.39.12",
"@certd/basic": "^1.39.12",
"@certd/commercial-core": "^1.39.12",
"@certd/acme-client": "^1.39.15",
"@certd/basic": "^1.39.15",
"@certd/commercial-core": "^1.39.15",
"@certd/cv4pve-api-javascript": "^8.4.2",
"@certd/jdcloud": "^1.39.12",
"@certd/lib-huawei": "^1.39.12",
"@certd/lib-k8s": "^1.39.12",
"@certd/lib-server": "^1.39.12",
"@certd/midway-flyway-js": "^1.39.12",
"@certd/pipeline": "^1.39.12",
"@certd/plugin-cert": "^1.39.12",
"@certd/plugin-lib": "^1.39.12",
"@certd/plugin-plus": "^1.39.12",
"@certd/plus-core": "^1.39.12",
"@certd/jdcloud": "^1.39.15",
"@certd/lib-huawei": "^1.39.15",
"@certd/lib-k8s": "^1.39.15",
"@certd/lib-server": "^1.39.15",
"@certd/midway-flyway-js": "^1.39.15",
"@certd/pipeline": "^1.39.15",
"@certd/plugin-cert": "^1.39.15",
"@certd/plugin-lib": "^1.39.15",
"@certd/plugin-plus": "^1.39.15",
"@certd/plus-core": "^1.39.15",
"@google-cloud/dns": "^5.3.1",
"@google-cloud/publicca": "^1.3.0",
"@huaweicloud/huaweicloud-sdk-cdn": "^3.1.185",
@@ -101,7 +101,6 @@
"cache-manager": "^6.1.0",
"cos-nodejs-sdk-v5": "^2.14.6",
"cron-parser": "^4.9.0",
"cross-env": "^7.0.3",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.7",
"esdk-obs-nodejs": "^3.25.6",
@@ -159,6 +158,8 @@
"@types/node": "^18",
"@types/nodemailer": "^6.4.8",
"c8": "^10.1.2",
"cross-env": "^7.0.3",
"esmock": "^2.7.5",
"mocha": "^10.2.0",
"prettier": "^2.8.8",
"rimraf": "^5.0.5",
@@ -112,7 +112,10 @@ export class ConnectController extends BaseController {
userInfo,
});
const state = JSON.parse(utils.hash.base64Decode(query.state));
let state = {forType:""}
if (query.state) {
state = JSON.parse(utils.hash.base64Decode(query.state));
}
const redirectUrl = `${bindUrl}#/oauth/callback/${type}?validationCode=${validationCode}&forType=${state.forType}`;
this.ctx.redirect(redirectUrl);
@@ -1,9 +1,10 @@
import { BaseController, Constants } from '@certd/lib-server';
import { 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 { ApiTags } from '@midwayjs/swagger';
import { CodeService } from '../../../modules/basic/service/code-service.js';
/**
*/
@@ -20,8 +21,13 @@ export class MineController extends BaseController {
@Inject()
passkeyService: PasskeyService;
@Inject()
codeService: CodeService;
@Post('/info', { description: Constants.per.authOnly, summary: "查询用户信息" })
@Inject()
sysSettingsService: SysSettingsService;
@Post('/info', { description: Constants.per.authOnly, summary: '查询用户信息' })
public async info() {
const userId = this.getUserId();
const user = await this.userService.info(userId);
@@ -35,21 +41,75 @@ export class MineController extends BaseController {
return this.ok(user);
}
@Post('/changePassword', { description: Constants.per.authOnly, summary: "修改密码" })
@Post('/changePassword', { description: Constants.per.authOnly, summary: '修改密码' })
public async changePassword(@Body(ALL) body: any) {
const userId = this.getUserId();
await this.userService.changePassword(userId, body);
return this.ok({});
}
@Post('/updateProfile', { description: Constants.per.authOnly, summary: "更新用户资料" })
@Post('/updateProfile', { description: Constants.per.authOnly, summary: '更新用户资料' })
public async updateProfile(@Body(ALL) body: any) {
const userId = this.getUserId();
await this.userService.updateProfile(userId, {
avatar: body.avatar,
nickName: body.nickName,
});
return this.ok({});
}
@Post('/contact/capability', { description: Constants.per.authOnly, summary: '查询联系方式绑定能力' })
public async contactCapability() {
const settings = await this.sysSettingsService.getPrivateSettings();
return this.ok({
smsEnabled: !!settings.sms?.config?.accessId,
});
}
@Post('/contact/verifyIdentity', { description: Constants.per.authOnly, summary: '验证本人操作' })
public async verifyContactIdentity(@Body(ALL) body: { identityType: 'password' | 'email' | 'mobile'; identityPassword?: string; identityValidateCode?: string }) {
const userId = this.getUserId();
await this.userService.verifyIdentity(userId, body, this.codeService);
const validationCode = this.codeService.setValidationValue({
type: 'contactIdentity',
userId,
identityType: body.identityType,
});
return this.ok({ validationCode });
}
@Post('/contact/mobile', { description: Constants.per.authOnly, summary: '绑定或修改手机号' })
public async updateMobile(@Body(ALL) body: { phoneCode?: string; mobile: string; validateCode: string; identityValidationCode: string }) {
const userId = this.getUserId();
this.userService.checkContactIdentityValidation(userId, body.identityValidationCode, this.codeService);
await this.codeService.checkSmsCode({
mobile: body.mobile,
phoneCode: body.phoneCode || '86',
smsCode: body.validateCode,
verificationType: 'bindMobile',
throwError: true,
});
await this.userService.updateMobile(userId, {
phoneCode: body.phoneCode,
mobile: body.mobile,
});
return this.ok({});
}
@Post('/contact/email', { description: Constants.per.authOnly, summary: '绑定或修改邮箱' })
public async updateEmail(@Body(ALL) body: { email: string; validateCode: string; identityValidationCode: string }) {
const userId = this.getUserId();
this.userService.checkContactIdentityValidation(userId, body.identityValidationCode, this.codeService);
this.codeService.checkEmailCode({
email: body.email,
validateCode: body.validateCode,
verificationType: 'bindEmail',
throwError: true,
});
await this.userService.updateEmail(userId, {
email: body.email,
});
return this.ok({});
}
}
@@ -0,0 +1,44 @@
import { Autoload, Init, Inject, Scope, ScopeEnum } from "@midwayjs/core";
import { AutoInitSite } from "./auto-init-site.js";
import { AutoLoadPlugins } from "./auto-load-plugins.js";
import { AutoCron } from "./auto-cron.js";
import { AutoMitterRegister } from "./auto-mitter-register.js";
import { AutoPipelineEmitterRegister } from "./auto-pipeline-emitter-register.js";
import { AutoFix } from "./auto-fix.js";
import { AutoPrint } from "./auto-print.js";
@Autoload()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class AutoARegister { //这个A是必须,让他排在第一个 进行init,否则会被其他init模块抢先注册导致报错
@Inject()
autoInitSite: AutoInitSite;
@Inject()
autoLoadPlugins: AutoLoadPlugins;
@Inject()
autoCron: AutoCron;
@Inject()
autoMitterRegister: AutoMitterRegister;
@Inject()
autoPipelineEmitterRegister: AutoPipelineEmitterRegister;
@Inject()
autoPrint: AutoPrint;
@Inject()
autoFix: AutoFix;
@Init()
async init() {
await this.autoInitSite.init();
await this.autoLoadPlugins.init();
await this.autoCron.init();
await this.autoMitterRegister.init();
await this.autoPipelineEmitterRegister.init();
await this.autoFix.init();
await this.autoPrint.init();
}
}
@@ -1,7 +1,7 @@
import { logger } from '@certd/basic';
import { SysSettingsService, SysSiteInfo } from '@certd/lib-server';
import { getPlusInfo, isPlus } from "@certd/plus-core";
import { Autoload, Config, Init, Inject, Scope, ScopeEnum } from '@midwayjs/core';
import { Config, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import dayjs from "dayjs";
import { Between } from "typeorm";
import { DomainService } from '../cert/service/domain-service.js';
@@ -14,9 +14,9 @@ import { PipelineService } from '../pipeline/service/pipeline-service.js';
import { UserService } from "../sys/authority/service/user-service.js";
import { ProjectService } from '../sys/enterprise/service/project-service.js';
@Autoload()
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class AutoCRegisterCron {
export class AutoCron {
@Inject()
pipelineService: PipelineService;
@@ -53,7 +53,6 @@ export class AutoCRegisterCron {
@Init()
async init() {
logger.info('加载定时trigger开始');
await this.pipelineService.onStartup(this.immediateTriggerOnce, this.onlyAdminUser);
@@ -0,0 +1,182 @@
import assert from "assert";
import esmock from "esmock";
import { AutoFix, buildEabAccountKeyValue, buildLegacyGoogleAccountConfigWhere, parseStorageValue } from "./auto-fix.js";
function createAutoFix(options: { pluginConfigService?: any; accessService?: any; storageService?: any }) {
const autoFix = new AutoFix();
autoFix.pluginConfigService = options.pluginConfigService;
autoFix.accessService = options.accessService;
autoFix.storageService = options.storageService;
return autoFix;
}
describe("AutoFix", () => {
it("parses legacy storage values", () => {
const config = parseStorageValue(
JSON.stringify({
value: {
key: "legacy-private-key",
accountUrl: "https://example.com/acct/1",
},
})
);
assert.equal(config.key, "legacy-private-key");
});
it("builds the EAB account key payload", () => {
const payload = JSON.parse(buildEabAccountKeyValue("kid-1", "private-key"));
assert.deepEqual(payload, {
kid: "kid-1",
privateKey: "private-key",
});
});
it("builds legacy Google account config query by exact email key only", () => {
assert.deepEqual(buildLegacyGoogleAccountConfigWhere("user@example.com"), {
userId: 1,
scope: "user",
namespace: "1",
key: "acme.config.google.user@example.com",
});
});
it("finds legacy Google account config by exact email key only", async () => {
let findOneWhere: any;
let findCalled = false;
const autoFix = createAutoFix({
pluginConfigService: null as any,
accessService: null as any,
storageService: {
getRepository() {
return {
async findOne(options: any) {
findOneWhere = options.where;
return {
value: JSON.stringify({
value: {
privateKey: "legacy-private-key",
},
}),
};
},
async find() {
findCalled = true;
return [];
},
};
},
} as any,
});
const config = await autoFix.getLegacyGoogleAccountConfig("user@example.com");
assert.equal(config.privateKey, "legacy-private-key");
assert.deepEqual(findOneWhere, buildLegacyGoogleAccountConfigWhere("user@example.com"));
assert.equal(findCalled, false);
});
it("does not query legacy Google account config without email", async () => {
let repositoryCalled = false;
const autoFix = createAutoFix({
pluginConfigService: null as any,
accessService: null as any,
storageService: {
getRepository() {
repositoryCalled = true;
return {};
},
} as any,
});
const config = await autoFix.getLegacyGoogleAccountConfig();
assert.equal(config, null);
assert.equal(repositoryCalled, false);
});
it("skips Google common EAB account key fix outside commercial edition", async () => {
let pluginConfigCalled = false;
const autoFix = createAutoFix({
pluginConfigService: {
async getPluginConfig() {
pluginConfigCalled = true;
return null;
},
} as any,
accessService: null as any,
storageService: null as any,
});
await autoFix.init();
assert.equal(pluginConfigCalled, false);
});
it("fixes Google common EAB account key in commercial edition", async () => {
const { AutoFix: MockedAutoFix } = await esmock("./auto-fix.js", {
"@certd/plus-core": {
isComm: () => true,
},
});
let getAccessByIdArgs: any[] = [];
let findOneWhere: any;
let updateAccessParam: any;
const autoFix = new MockedAutoFix();
autoFix.pluginConfigService = {
async getPluginConfig(options: any) {
assert.deepEqual(options, {
name: "CertApply",
type: "builtIn",
});
return {
sysSetting: {
input: {
googleCommonEabAccessId: 12,
},
},
};
},
};
autoFix.accessService = {
async getAccessById(...args: any[]) {
getAccessByIdArgs = args;
return {
kid: "kid-1",
email: "user@example.com",
};
},
async updateAccess(param: any) {
updateAccessParam = param;
},
};
autoFix.storageService = {
getRepository() {
return {
async findOne(options: any) {
findOneWhere = options.where;
return {
value: JSON.stringify({
value: {
privateKey: "legacy-private-key",
},
}),
};
},
};
},
};
await autoFix.fixGoogleCommonEabAccountKey();
assert.deepEqual(getAccessByIdArgs, [12, false]);
assert.deepEqual(findOneWhere, buildLegacyGoogleAccountConfigWhere("user@example.com"));
assert.deepEqual(updateAccessParam, {
id: 12,
eabType: "google",
accountKey: buildEabAccountKeyValue("kid-1", "legacy-private-key"),
});
});
});
@@ -0,0 +1,107 @@
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { logger } from "@certd/basic";
import { AccessService } from "@certd/lib-server";
import { isComm } from "@certd/plus-core";
import { PluginConfigService } from "../plugin/service/plugin-config-service.js";
import { StorageService } from "../pipeline/service/storage-service.js";
export function parseStorageValue(value?: string) {
if (!value) {
return null;
}
try {
const parsed = JSON.parse(value);
return parsed?.value || null;
} catch {
return null;
}
}
export function buildEabAccountKeyValue(kid: string, privateKey: string) {
return JSON.stringify({
kid,
privateKey,
});
}
export function buildLegacyGoogleAccountConfigWhere(email: string) {
return {
userId: 1,
scope: "user",
namespace: "1",
key: `acme.config.google.${email}`,
};
}
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class AutoFix {
@Inject()
pluginConfigService: PluginConfigService;
@Inject()
accessService: AccessService;
@Inject()
storageService: StorageService;
async init() {
await this.fixGoogleCommonEabAccountKey();
}
async fixGoogleCommonEabAccountKey() {
if (!isComm()) {
return;
}
try {
const certApplyConfig = await this.pluginConfigService.getPluginConfig({
name: "CertApply",
type: "builtIn",
});
const googleCommonEabAccessId = certApplyConfig?.sysSetting?.input?.googleCommonEabAccessId;
if (!googleCommonEabAccessId) {
return;
}
const eabAccess = await this.accessService.getAccessById(googleCommonEabAccessId, false);
if (eabAccess.accountKey) {
return;
}
if (!eabAccess.kid) {
logger.info("公共Google EAB授权缺少KID,跳过历史ACME账号私钥修复");
return;
}
const accountConfig = await this.getLegacyGoogleAccountConfig(eabAccess.email);
const privateKey = accountConfig?.privateKey || accountConfig?.key || accountConfig?.accountKey;
if (!privateKey) {
logger.info("未找到可迁移到公共Google EAB授权的历史ACME账号私钥");
return;
}
const accountKey = buildEabAccountKeyValue(eabAccess.kid, privateKey);
await this.accessService.updateAccess({ id: googleCommonEabAccessId, eabType: "google", accountKey });
logger.info(`已修复公共Google EAB授权的ACME账号私钥,accessId=${googleCommonEabAccessId}`);
} catch (e: any) {
logger.error("修复公共Google EAB授权ACME账号私钥失败", e);
}
}
async getLegacyGoogleAccountConfig(email?: string) {
if (!email) {
return null;
}
const repository = this.storageService.getRepository();
const exact = await repository.findOne({
where: buildLegacyGoogleAccountConfigWhere(email),
});
const exactValue = this.parseStorageValue(exact?.value);
if (exactValue?.key || exactValue?.privateKey || exactValue?.accountKey) {
return exactValue;
}
return null;
}
parseStorageValue(value?: string) {
return parseStorageValue(value);
}
}
@@ -1,14 +1,14 @@
import { logger } from '@certd/basic';
import { PlusService, SysInstallInfo, SysPrivateSettings, SysSettingsService } from '@certd/lib-server';
import { Autoload, Config, Init, Inject, Scope, ScopeEnum } from '@midwayjs/core';
import { EncryptService, PlusService, SysInstallInfo, SysPrivateSettings, SysSettingsService } from '@certd/lib-server';
import { Config, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import crypto from 'crypto';
import { nanoid } from 'nanoid';
import { UserService } from '../sys/authority/service/user-service.js';
import { SafeService } from "../sys/settings/safe-service.js";
@Autoload()
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class AutoAInitSite {
export class AutoInitSite {
@Inject()
userService: UserService;
@@ -22,7 +22,10 @@ export class AutoAInitSite {
@Inject()
safeService: SafeService;
@Init()
@Inject()
encryptService: EncryptService;
async init() {
logger.info('初始化站点开始');
await this.startOptimizeDb();
@@ -51,6 +54,8 @@ export class AutoAInitSite {
//加载一次密钥
await this.sysSettingsService.getSecret();
//初始化加密服务
await this.encryptService.doInit();
// 授权许可
try {
@@ -1,16 +1,15 @@
import { Autoload, Init, Inject, Scope, ScopeEnum } from "@midwayjs/core";
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { logger } from "@certd/basic";
import { PluginService } from "../plugin/service/plugin-service.js";
import { registerPaymentProviders } from "../suite/payments/index.js";
@Autoload()
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class AutoBLoadPlugins {
export class AutoLoadPlugins {
@Inject()
pluginService: PluginService;
@Init()
async init() {
logger.info(`加载插件开始,加载模式:${process.env.certd_plugin_loadmode}`);
if (process.env.certd_plugin_loadmode === "metadata") {
@@ -1,14 +1,13 @@
import { logger, utils } from '@certd/basic';
import { UserSuiteService } from '@certd/commercial-core';
import { Autoload, Init, Inject, Scope, ScopeEnum } from '@midwayjs/core';
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
@Autoload()
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class AutoDMitterRegister {
export class AutoMitterRegister {
@Inject()
userSuiteService: UserSuiteService;
@Init()
async init() {
await this.registerOnNewUser();
}
@@ -1,21 +1,21 @@
import { Autoload, Init, Inject, Scope, ScopeEnum } from "@midwayjs/core";
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { CertInfoService } from "../monitor/index.js";
import { pipelineEmitter } from "@certd/pipeline";
import { CertInfo, EVENT_CERT_APPLY_SUCCESS } from "@certd/plugin-cert";
import { PipelineEvent } from "@certd/pipeline";
@Autoload()
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class AutoEPipelineEmitterRegister {
export class AutoPipelineEmitterRegister {
@Inject()
certInfoService: CertInfoService;
@Init()
async init() {
await this.onCertApplySuccess();
}
async onCertApplySuccess() {
pipelineEmitter.on(EVENT_CERT_APPLY_SUCCESS, async (event: PipelineEvent<{cert:CertInfo,file:string}>) => {
pipelineEmitter.on(EVENT_CERT_APPLY_SUCCESS, async (event: PipelineEvent<{ cert: CertInfo; file: string }>) => {
await this.certInfoService.updateCertByPipelineId(event.pipeline.id, event.event.cert, event.event.file);
});
}
@@ -1,4 +1,4 @@
import { App, Autoload, Config, Init, Inject, Scope, ScopeEnum } from '@midwayjs/core';
import { App, Config, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { getPlusInfo, isPlus } from '@certd/plus-core';
import { isDev, logger } from '@certd/basic';
@@ -11,9 +11,9 @@ import { UserService } from '../sys/authority/service/user-service.js';
import { UserSettingsService } from '../mine/service/user-settings-service.js';
import { startProxyServer } from './proxy/server.js';
@Autoload()
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class AutoZPrint {
export class AutoPrint {
@Inject()
sysSettingsService: SysSettingsService;
@@ -34,7 +34,6 @@ export class AutoZPrint {
@Config('system.resetAdminPasswd')
private resetAdminPasswd: boolean;
@Init()
async init() {
//监听https
this.startHttpsServer();
@@ -11,6 +11,8 @@ export class CnameProviderEntity {
userId: number;
@Column({ comment: '域名', length: 100 })
domain: string;
@Column({ comment: '子域名托管', length: 100, nullable: true })
subdomain: string;
@Column({ comment: 'DNS提供商类型', name: 'dns_provider_type', length: 20 })
dnsProviderType: string;
@Column({ comment: 'DNS授权Id', name: 'access_id' })
@@ -98,6 +98,18 @@ export class CnameProviderService extends BaseService<CnameProviderEntity> {
return null;
}
async getSubDomains() {
const list = await this.repository.find({
select: {
subdomain: true,
},
where: {
disabled: false,
},
});
return list.map(item => item.subdomain?.trim()).filter((item): item is string => !!item);
}
async list(req: ListReq): Promise<any[]> {
const list = await super.list(req);
const sysPrivateSettings = await this.settingsService.getSetting<SysPrivateSettings>(SysPrivateSettings);
@@ -0,0 +1,28 @@
import assert from "node:assert/strict";
import { describe, it } from "mocha";
import { SubDomainsGetter } from "./sub-domain-getter.js";
describe("SubDomainsGetter", () => {
it("returns subdomains configured on system cname providers", async () => {
const subDomainService = {
async getListByUserId() {
return ["example.com"];
},
} as any;
const domainService = {
async findOne() {
return null;
},
} as any;
const cnameProviderService = {
async getSubDomains() {
return ["cname-hosted.example.com"];
},
} as any;
const getter = new SubDomainsGetter(1, 2, subDomainService, domainService, cnameProviderService);
assert.deepEqual(await getter.getSubDomains(), ["cname-hosted.example.com", "example.com"]);
assert.equal(await getter.hasSubDomain("txt.certd.cname-hosted.example.com"), "cname-hosted.example.com");
});
});
@@ -1,22 +1,30 @@
import { ISubDomainsGetter } from "@certd/plugin-cert";
import { SubDomainService } from "../sub-domain-service.js";
import { DomainService } from "../../../cert/service/domain-service.js";
import { CnameProviderService } from "../../../cname/service/cname-provider-service.js";
export class SubDomainsGetter implements ISubDomainsGetter {
userId: number;
projectId: number;
subDomainService: SubDomainService;
domainService: DomainService;
cnameProviderService: CnameProviderService;
constructor(userId: number, projectId: number, subDomainService: SubDomainService, domainService: DomainService) {
constructor(userId: number, projectId: number, subDomainService: SubDomainService, domainService: DomainService, cnameProviderService: CnameProviderService) {
this.userId = userId;
this.projectId = projectId;
this.subDomainService = subDomainService;
this.domainService = domainService;
this.cnameProviderService = cnameProviderService;
}
async getSubDomains() {
return await this.subDomainService.getListByUserId(this.userId, this.projectId)
const projectSubDomains = await this.subDomainService.getListByUserId(this.userId, this.projectId) || [];
const cnameProviderSubDomains = await this.cnameProviderService.getSubDomains();
return [...projectSubDomains, ...cnameProviderSubDomains]
.map(item => item?.trim())
.filter((item): item is string => !!item)
.sort((a, b) => b.length - a.length);
}
async hasSubDomain(fullDomain: string) {
@@ -12,6 +12,7 @@ import { SubDomainService } from "../sub-domain-service.js";
import { CertInfoGetter } from "./cert-info-getter.js";
import { CertInfoService } from "../../../monitor/index.js";
import { ICertInfoGetter } from "@certd/plugin-lib";
import { CnameProviderService } from "../../../cname/service/cname-provider-service.js";
const serviceNames = [
'ocrService',
@@ -53,7 +54,8 @@ export class TaskServiceGetter implements IServiceGetter{
async getSubDomainsGetter(): Promise<SubDomainsGetter> {
const subDomainsService:SubDomainService = await this.appCtx.getAsync("subDomainService")
const domainService:DomainService = await this.appCtx.getAsync("domainService")
return new SubDomainsGetter(this.userId,this.projectId, subDomainsService,domainService)
const cnameProviderService:CnameProviderService = await this.appCtx.getAsync("cnameProviderService")
return new SubDomainsGetter(this.userId,this.projectId, subDomainsService,domainService,cnameProviderService)
}
async getCertInfoGetter(): Promise<ICertInfoGetter> {
@@ -102,4 +104,3 @@ export type TaskServiceCreateReq = {
@@ -0,0 +1,27 @@
/// <reference types="mocha" />
import assert from 'node:assert/strict';
import { Not } from 'typeorm';
import { buildUserContactConflictWhere } from './user-service.js';
describe('buildUserContactConflictWhere', () => {
it('checks username, mobile and email conflicts except current user', () => {
const where = buildUserContactConflictWhere('user@example.com', 12);
assert.deepEqual(where, [
{ username: 'user@example.com', id: Not(12) },
{ mobile: 'user@example.com', id: Not(12) },
{ email: 'user@example.com', id: Not(12) },
]);
});
it('trims contact value before building conflict query', () => {
const where = buildUserContactConflictWhere(' 13800138000 ', 3);
assert.deepEqual(where, [
{ username: '13800138000', id: Not(3) },
{ mobile: '13800138000', id: Not(3) },
{ email: '13800138000', id: Not(3) },
]);
});
});
@@ -1,6 +1,6 @@
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import {EntityManager, In, MoreThan, Not, Repository} from 'typeorm';
import { EntityManager, In, MoreThan, Not, Repository } from 'typeorm';
import { UserEntity } from '../entity/user.js';
import * as _ from 'lodash-es';
import { BaseService, CommonException, Constants, FileService, SysInstallInfo, SysSettingsService } from '@certd/lib-server';
@@ -18,15 +18,22 @@ import { OauthBoundService } from '../../../login/service/oauth-bound-service.js
export type RegisterType = 'username' | 'mobile' | 'email';
export type ForgotPasswordType = 'mobile' | 'email';
export const AdminRoleId = 1
export const AdminRoleId = 1;
export function buildUserContactConflictWhere(value: string, userId: number) {
const contact = value?.trim();
return [
{ username: contact, id: Not(userId) },
{ mobile: contact, id: Not(userId) },
{ email: contact, id: Not(userId) },
];
}
/**
*
*/
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class UserService extends BaseService<UserEntity> {
@InjectEntityModel(UserEntity)
repository: Repository<UserEntity>;
@Inject()
@@ -44,10 +51,9 @@ export class UserService extends BaseService<UserEntity> {
@Inject()
dbAdapter: DbAdapter;
@Inject()
@Inject()
oauthBoundService: OauthBoundService;
//@ts-ignore
getRepository() {
return this.repository;
@@ -145,7 +151,7 @@ export class UserService extends BaseService<UserEntity> {
return bcrypt.hashSync(plainPassword, salt);
}
async findOne(param: Record<string,any>) {
async findOne(param: Record<string, any>) {
return this.repository.findOne({
where: param,
});
@@ -177,12 +183,11 @@ export class UserService extends BaseService<UserEntity> {
return await this.roleService.getPermissionByRoleIds(roleIds);
}
async register(type: string, user: UserEntity,withTx?:(tx: EntityManager)=>Promise<void>) {
async register(type: string, user: UserEntity, withTx?: (tx: EntityManager) => Promise<void>) {
if (!user.password) {
user.password = simpleNanoId();
}
if (user.username) {
const username = user.username;
const old = await this.findOne([{ username: username }, { mobile: username }, { email: username }]);
@@ -208,7 +213,6 @@ export class UserService extends BaseService<UserEntity> {
}
}
if (!user.username) {
user.username = 'user_' + simpleNanoId();
}
@@ -235,7 +239,7 @@ export class UserService extends BaseService<UserEntity> {
const userRole: UserRoleEntity = UserRoleEntity.of(newUser.id, Constants.role.defaultUser);
await txManager.save(userRole);
if(withTx) {
if (withTx) {
await withTx(txManager);
}
});
@@ -247,35 +251,26 @@ export class UserService extends BaseService<UserEntity> {
return newUser;
}
async forgotPassword(
data: {
type: ForgotPasswordType;
input?: string,
phoneCode?: string,
validateCode: string,
password: string,
confirmPassword: string,
}
) {
if(!data.type) {
async forgotPassword(data: { type: ForgotPasswordType; input?: string; phoneCode?: string; validateCode: string; password: string; confirmPassword: string }) {
if (!data.type) {
throw new CommonException('找回类型不能为空');
}
if(data.password !== data.confirmPassword) {
if (data.password !== data.confirmPassword) {
throw new CommonException('两次输入的密码不一致');
}
const where :any= {
const where: any = {
[data.type]: data.input,
};
if (data.type === 'mobile' ) {
where.phoneCode = data.phoneCode ?? '86';
if (data.type === 'mobile') {
where.phoneCode = data.phoneCode ?? '86';
}
const user = await this.findOne({ [data.type]: data.input });
console.log('user', user)
if(!user) {
console.log('user', user);
if (!user) {
throw new CommonException('用户不存在');
// return;
}
await this.resetPassword(user.id, data.password)
await this.resetPassword(user.id, data.password);
return user.username;
}
@@ -376,30 +371,102 @@ export class UserService extends BaseService<UserEntity> {
}
async getAdmins() {
const admins = await this.userRoleService.find({
where: {
roleId: AdminRoleId,
},
});
const admins = await this.userRoleService.find({
where: {
roleId: AdminRoleId,
},
});
const userIds = admins.map(item => item.userId);
return await this.repository.find({
where: {
id: In(userIds),
status: 1,
},
order: {
updateTime: 'DESC',
},
})
const userIds = admins.map(item => item.userId);
return await this.repository.find({
where: {
id: In(userIds),
status: 1,
},
order: {
updateTime: 'DESC',
},
});
}
async updateProfile(userId: any, body: any) {
await this.update({
id: userId,
...body,
})
});
}
async verifyIdentity(userId: number, body: { identityType: 'password' | 'email' | 'mobile'; identityPassword?: string; identityValidateCode?: string }, codeService: any) {
const user = await this.info(userId);
if (body.identityType === 'password') {
const passwordChecked = await this.checkPassword(body.identityPassword, user.password, user.passwordVersion);
if (!passwordChecked) {
throw new CommonException('密码错误');
}
return;
}
if (body.identityType === 'email') {
if (!user.email) {
throw new CommonException('当前账号未绑定邮箱');
}
codeService.checkEmailCode({
email: user.email,
validateCode: body.identityValidateCode,
verificationType: 'contactIdentity',
throwError: true,
});
return;
}
if (body.identityType === 'mobile') {
if (!user.mobile) {
throw new CommonException('当前账号未绑定手机号');
}
await codeService.checkSmsCode({
mobile: user.mobile,
phoneCode: user.phoneCode || '86',
smsCode: body.identityValidateCode,
verificationType: 'contactIdentity',
throwError: true,
});
return;
}
throw new CommonException('不支持的验证方式');
}
checkContactIdentityValidation(userId: number, validationCode: string, codeService: any) {
const validationValue = codeService.getValidationValue(validationCode);
if (!validationValue || validationValue.type !== 'contactIdentity' || validationValue.userId !== userId) {
throw new CommonException('请先验证本人操作');
}
}
async updateMobile(userId: number, body: { phoneCode?: string; mobile: string }) {
const mobile = body.mobile?.trim();
if (!mobile) {
throw new CommonException('手机号不能为空');
}
const old = await this.findOne(buildUserContactConflictWhere(mobile, userId));
if (old != null) {
throw new CommonException('手机号已被占用');
}
await this.repository.update(userId, {
phoneCode: body.phoneCode || '86',
mobile,
});
}
async updateEmail(userId: number, body: { email: string }) {
const email = body.email?.trim();
if (!email) {
throw new CommonException('邮箱不能为空');
}
const old = await this.findOne(buildUserContactConflictWhere(email, userId));
if (old != null) {
throw new CommonException('邮箱已被占用');
}
await this.repository.update(userId, {
email,
});
}
async getAllUserIds() {
@@ -408,7 +475,7 @@ export class UserService extends BaseService<UserEntity> {
where: {
status: 1,
},
})
});
return users.map(item => item.id);
}
}
@@ -275,11 +275,13 @@ export class AliyunDeployCertToESA extends AbstractTaskPlugin {
}
});
const list = certListRes.Result;
let list = certListRes.Result || [];
list = list.filter((item: any) => item.Type === "cas");
if (!list || list.length === 0) {
this.logger.info(`站点[${siteId}]没有证书, 无需删除`);
this.logger.info(`站点[${siteId}]没有CAS证书, 无需删除`);
return
}
if (list.length < certLimit) {
this.logger.info(`站点[${siteId}]证书数量(${list.length})未超限制, 无需删除`);
return;
@@ -0,0 +1,20 @@
import assert from "assert";
import { EabAccess } from "./eab-access.js";
describe("EabAccess", () => {
it("generates an account key payload for the current kid", async () => {
const access = new EabAccess();
access.kid = "kid-1";
const payload = JSON.parse(await access.onGenerateAccountKey());
assert.equal(payload.kid, "kid-1");
assert.match(payload.privateKey, /BEGIN (RSA )?PRIVATE KEY/);
});
it("requires kid before generating the account key payload", async () => {
const access = new EabAccess();
await assert.rejects(() => access.onGenerateAccountKey(), /请先填写KID/);
});
});
@@ -1,4 +1,5 @@
import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline";
import * as acme from "@certd/acme-client";
@IsAccess({
name: "eab",
@@ -7,6 +8,23 @@ import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline";
icon: "ic:outline-lock",
})
export class EabAccess extends BaseAccess {
@AccessInput({
title: "EAB类型",
component: {
name: "a-select",
options: [
{ value: "google", label: "Google", icon: "flat-color-icons:google" },
{ value: "zerossl", label: "ZeroSSL", icon: "emojione:digit-zero" },
{ value: "litessl", label: "litessl", icon: "roentgen:free" },
{ value: "sslcom", label: "SSL.com", icon: "la:expeditedssl" },
],
},
helper: "请选择EAB类型",
required: true,
encrypt: false,
})
eabType = "";
@AccessInput({
title: "KID",
component: {
@@ -34,10 +52,35 @@ export class EabAccess extends BaseAccess {
placeholder: "绑定一个邮箱",
},
rules: [{ type: "email", message: "请输入正确的邮箱" }],
helper: "Google的EAB申请证书,更换邮箱会导致EAB失效,可以在此处绑定一个邮箱避免此问题",
helper: "绑定一个邮箱避免失效",
required: true,
})
email = "";
@AccessInput({
title: "ACME账号私钥",
component: {
name: "refresh-input",
action: "GenerateAccountKey",
buttonText: "生成",
successMessage: "账号私钥已生成,请保存授权配置",
},
required: true,
helper: "如果修改了KID,请点击生成重新生成账号私钥\n注意:google的EAB只能生成一次账号私钥,更新私钥需要获取一个新的EAB授权",
encrypt: true,
})
accountKey = "";
async onGenerateAccountKey() {
if (!this.kid) {
throw new Error("请先填写KID");
}
const key = await acme.crypto.createPrivateKey(2048);
return JSON.stringify({
kid: this.kid,
privateKey: key.toString(),
});
}
}
new EabAccess();
@@ -0,0 +1,176 @@
import assert from "assert";
import { AcmeService } from "./acme.js";
const logger = {
info() {},
error() {},
warn() {},
debug() {},
};
describe("AcmeService account config", () => {
it("keeps legacy email-based account config when EAB has no saved account key", async () => {
const userContext = {
async getObj(key: string) {
if (key === "acme.config.google.user@example.com") {
return {
key: "legacy-email-key",
accountUrl: "https://dv.acme-v02.api.pki.goog/acme/acct/legacy",
};
}
return null;
},
async setObj() {},
};
const service = new AcmeService({
userId: 1,
userContext: userContext as any,
logger: logger as any,
sslProvider: "google",
eab: {
id: 12,
kid: "kid-1",
hmacKey: "hmac",
} as any,
domainParser: {} as any,
});
const conf = await service.getAccountConfig("user@example.com", { enabled: false, mappings: {} });
assert.equal(conf.key, "legacy-email-key");
assert.equal(conf.accountUrl, "https://dv.acme-v02.api.pki.goog/acme/acct/legacy");
});
it("uses the account key saved on the EAB access before legacy email config", async () => {
const userContext = {
async getObj(key: string) {
if (key === "acme.config.google.access.12") {
return { accountUrl: "https://dv.acme-v02.api.pki.goog/acme/acct/1" };
}
if (key === "acme.config.google.user@example.com") {
return { key: "legacy-email-key" };
}
return null;
},
async setObj() {},
};
const service = new AcmeService({
userId: 1,
userContext: userContext as any,
logger: logger as any,
sslProvider: "google",
eab: {
id: 12,
kid: "kid-1",
hmacKey: "hmac",
accountKey: JSON.stringify({ kid: "kid-1", privateKey: "eab-account-key" }),
} as any,
domainParser: {} as any,
});
const conf = await service.getAccountConfig("user@example.com", { enabled: false, mappings: {} });
assert.equal(conf.key, "eab-account-key");
assert.equal(conf.accountUrl, "https://dv.acme-v02.api.pki.goog/acme/acct/1");
});
it("rejects an EAB account key generated for another kid", async () => {
const service = new AcmeService({
userId: 1,
userContext: {} as any,
logger: logger as any,
sslProvider: "google",
eab: {
id: 12,
kid: "kid-2",
hmacKey: "hmac",
accountKey: JSON.stringify({ kid: "kid-1", privateKey: "eab-account-key" }),
} as any,
domainParser: {} as any,
});
assert.throws(() => service.getEabAccountPrivateKey(), /请点击刷新重新生成ACME账号私钥/);
});
it("formats expired EAB errors with a Chinese recovery hint", () => {
const service = new AcmeService({
userId: 1,
userContext: {} as any,
logger: logger as any,
sslProvider: "google",
eab: {
id: 12,
kid: "kid-1",
hmacKey: "hmac",
} as any,
domainParser: {} as any,
});
const error = service.formatCreateAccountError(new Error("Unknown external account binding (EAB) key. This may be due to the EAB key expiring"));
assert.match(error.message, /EAB授权已失效或已过期/);
assert.match(error.message, /请重新获取EAB授权并刷新ACME账号私钥后重试/);
});
});
describe("AcmeService challenge", () => {
it("parses cname TXT full record to choose the delegated DNS zone", async () => {
const parseCalls: string[] = [];
const service = new AcmeService({
userId: 1,
userContext: {} as any,
logger: logger as any,
sslProvider: "letsencrypt",
domainParser: {
async parse(fullDomain: string) {
parseCalls.push(fullDomain);
if (fullDomain === "certd-key.cname.sub.example.com") {
return "sub.example.com";
}
return "example.com";
},
} as any,
});
const dnsProvider = {
usePunyCode() {
return false;
},
async createRecord(recordReq: any) {
assert.equal(recordReq.domain, "sub.example.com");
assert.equal(recordReq.fullRecord, "certd-key.cname.sub.example.com");
assert.equal(recordReq.hostRecord, "certd-key.cname");
return { id: "record-id" };
},
} as any;
await service.challengeCreateFn(
{
identifier: {
value: "www.example.com",
},
challenges: [
{
type: "dns-01",
},
],
},
async () => "key-auth",
{
domainsVerifyPlan: {
"www.example.com": {
type: "cname",
domain: "www.example.com",
mainDomain: "example.com",
cnameVerifyPlan: {
domain: "cname.sub.example.com",
fullRecord: "certd-key.cname.sub.example.com",
dnsProvider,
},
},
},
}
);
assert.deepEqual(parseCalls, ["www.example.com", "certd-key.cname.sub.example.com"]);
});
});
@@ -49,11 +49,15 @@ export type CertInfo = {
};
export type SSLProvider = "letsencrypt" | "google" | "zerossl" | "sslcom" | "letsencrypt_staging";
export type PrivateKeyType = "rsa_1024" | "rsa_2048" | "rsa_3072" | "rsa_4096" | "ec_256" | "ec_384" | "ec_521";
type AcmeEabOptions = ClientExternalAccountBindingOptions & {
id?: number;
accountKey?: string;
};
type AcmeServiceOptions = {
userContext: IContext;
logger: ILogger;
sslProvider: SSLProvider;
eab?: ClientExternalAccountBindingOptions;
eab?: AcmeEabOptions;
skipLocalVerify?: boolean;
useMappingProxy?: boolean;
reverseProxy?: string;
@@ -71,7 +75,7 @@ export class AcmeService {
logger: ILogger;
sslProvider: SSLProvider;
skipLocalVerify = true;
eab?: ClientExternalAccountBindingOptions;
eab?: AcmeEabOptions;
constructor(options: AcmeServiceOptions) {
this.options = options;
this.userContext = options.userContext;
@@ -85,7 +89,14 @@ export class AcmeService {
}
async getAccountConfig(email: string, urlMapping: UrlMapping): Promise<any> {
const conf = (await this.userContext.getObj(this.buildAccountKey(email))) || {};
let conf = (await this.userContext.getObj(this.buildAccountKey(email))) || {};
const eabAccountKey = this.getEabAccountPrivateKey();
if (eabAccountKey) {
conf = {
...((await this.userContext.getObj(this.buildAccessAccountKey())) || {}),
key: eabAccountKey,
};
}
if (urlMapping && urlMapping.mappings) {
for (const key in urlMapping.mappings) {
if (Object.prototype.hasOwnProperty.call(urlMapping.mappings, key)) {
@@ -104,16 +115,49 @@ export class AcmeService {
return `acme.config.${this.sslProvider}.${email}`;
}
buildAccessAccountKey() {
return `acme.config.${this.sslProvider}.access.${this.eab.id}`;
}
getEabAccountPrivateKey() {
if (!this.eab?.accountKey) {
return null;
}
let accountKey;
try {
accountKey = JSON.parse(this.eab.accountKey);
} catch {
return this.eab.accountKey;
}
if (accountKey.kid !== this.eab.kid) {
throw new Error("EAB的KID已变化,请点击刷新重新生成ACME账号私钥");
}
return accountKey.privateKey;
}
formatCreateAccountError(e: any) {
const message = e?.message || "";
if (message.includes("Unknown external account binding (EAB) key")) {
return new Error(`EAB授权已失效或已过期,请重新获取EAB授权并刷新ACME账号私钥后重试。原始错误:${message}`);
}
return e;
}
async saveAccountConfig(email: string, conf: any) {
if (this.getEabAccountPrivateKey()) {
// userContext 跟用户走。公共 EAB 场景下这里仅作为当前用户缓存;
// 其他用户会通过 onlyReturnExisting 用同一个账号私钥取回 accountUrl。
await this.userContext.setObj(this.buildAccessAccountKey(), { accountUrl: conf.accountUrl });
return;
}
await this.userContext.setObj(this.buildAccountKey(email), conf);
}
async getAcmeClient(email: string): Promise<acme.Client> {
const directoryUrl = acme.getDirectoryUrl({ sslProvider: this.sslProvider, pkType: this.options.privateKeyType });
let targetUrl = directoryUrl.replace("https://", "");
targetUrl = targetUrl.substring(0, targetUrl.indexOf("/"));
const mappings = {
"acme-v02.api.letsencrypt.org": "le.px.certd.handfree.work",
"dv.acme-v02.api.pki.goog": "gg.px.certd.handfree.work",
@@ -171,7 +215,23 @@ export class AcmeService {
contact: [`mailto:${email}`],
externalAccountBinding: this.eab,
};
await client.createAccount(accountPayload);
if (this.getEabAccountPrivateKey()) {
try {
// RFC 8555 的 newAccount 支持 onlyReturnExisting。
// 使用同一个账号私钥时,CA 会返回已存在账号的 URL,不会再次消费 EAB。
await client.createAccount({ onlyReturnExisting: true });
conf.accountUrl = client.getAccountUrl();
await this.saveAccountConfig(email, conf);
return client;
} catch (e: any) {
this.logger.info(`未找到已存在的ACME账号,准备创建新账号:${e.message}`);
}
}
try {
await client.createAccount(accountPayload);
} catch (e: any) {
throw this.formatCreateAccountError(e);
}
conf.accountUrl = client.getAccountUrl();
await this.saveAccountConfig(email, conf);
}
@@ -264,8 +324,8 @@ export class AcmeService {
const cname: CnameVerifyPlan = domainVerifyPlan.cnameVerifyPlan;
if (cname) {
dnsProvider = cname.dnsProvider;
domain = await this.options.domainParser.parse(cname.domain);
fullRecord = cname.fullRecord;
domain = await this.options.domainParser.parse(fullRecord);
} else {
this.logger.error(`未找到域名${fullDomain}的CNAME校验计划,请修改证书申请配置`);
}
@@ -0,0 +1,13 @@
export const IconSets = [
"streamline-logos",
"logos",
"fa-brands",
"fa-solid",
"fa-regular",
"carbon",
"ion",
"ant-design",
"mdi",
"twemoji",
"svg-spinners"
]
@@ -1,5 +1,6 @@
import { AddonInput, BaseAddon, IsAddon } from "@certd/lib-server";
import { BuildLoginUrlReq, BuildLogoutUrlReq, IOauthProvider, OnCallbackReq } from "../api.js";
import { IconSets } from "../iconsets.js";
@IsAddon({
addonType: "oauth",
@@ -47,7 +48,8 @@ export class CloginOauthProvider extends BaseAddon implements IOauthProvider {
title: "自定义图标",
component: {
name:"fs-icon-selector",
vModel:"modelValue"
vModel:"modelValue",
iconSets: IconSets,
},
required: false,
})
@@ -73,9 +75,9 @@ export class CloginOauthProvider extends BaseAddon implements IOauthProvider {
async buildLoginUrl(params: BuildLoginUrlReq) {
let redirectUri = params.redirectUri || ""
if(redirectUri.indexOf("localhost:3008")>=0){
redirectUri = redirectUri.replace("localhost:3008", "certd.handfree.work")
}
// if(redirectUri.indexOf("localhost:3008")>=0){
// redirectUri = redirectUri.replace("localhost:3008", "certd.handfree.work")
// }
const res = await this.ctx.http.request({
url: `${this.endpoint}/connect.php?act=login&appid=${this.appId}&appkey=${this.appKey}&type=${this.loginType}&redirect_uri=${redirectUri}`
})
@@ -102,7 +104,7 @@ export class CloginOauthProvider extends BaseAddon implements IOauthProvider {
const code = req.code || ""
const tokenEndpoint = `http://clogin.yfy.docmirror.cn/connect.php?act=callback&appid=${this.appId}&appkey=${this.appKey}&type=${this.loginType}&code=${code}`
const tokenEndpoint = `${this.endpoint}/connect.php?act=callback&appid=${this.appId}&appkey=${this.appKey}&type=${this.loginType}&code=${code}`
const res = await this.ctx.utils.http.request({
url: tokenEndpoint,
method: "post",
@@ -1,5 +1,6 @@
import { AddonInput, BaseAddon, IsAddon } from "@certd/lib-server";
import { BuildLoginUrlReq, BuildLogoutUrlReq, IOauthProvider, OnCallbackReq } from "../api.js";
import { IconSets } from "../iconsets.js";
@IsAddon({
addonType: "oauth",
@@ -15,7 +16,8 @@ export class OidcOauthProvider extends BaseAddon implements IOauthProvider {
title: "自定义图标",
component: {
name:"fs-icon-selector",
vModel:"modelValue"
vModel:"modelValue",
iconSets: IconSets,
},
required: false,
})
@@ -21,6 +21,7 @@ import { omit } from "lodash-es";
//插件分组
group: pluginGroups.tencent.key,
needPlus: false,
deprecated: "腾讯更新证书(Id不变)接口已失效,本插件已下架,请使用其他接口",
default: {
//默认值配置照抄即可
strategy: {

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