Compare commits

...

80 Commits

Author SHA1 Message Date
xiaojunnuo 5801f34b3a v1.40.0 2026-05-15 00:48:37 +08:00
xiaojunnuo 17cf16ca92 build: prepare to build 2026-05-15 00:44:39 +08:00
xiaojunnuo 7015b1b232 perf: 头像增加缓存时间 2026-05-15 00:39:35 +08:00
xiaojunnuo 3b72ca09c6 chore: 支持设置初始化密码 2026-05-15 00:26:24 +08:00
xiaojunnuo a815d0245b perf: 第三方登录自动注册的用户支持设置初始化密码 2026-05-15 00:25:28 +08:00
xiaojunnuo 229f22d5a9 docs: 自动升级docker-compose 2026-05-15 00:12:44 +08:00
xiaojunnuo 22f5cfcfd8 fix: 修复clogin登录丢失state问题 2026-05-14 23:44:04 +08:00
xiaojunnuo 90ba55c043 chore: 1 2026-05-14 19:05:49 +08:00
xiaojunnuo 9f878a353c fix: 修复clogin多选类型登录失败的bug 2026-05-14 19:04:20 +08:00
xiaojunnuo af7297d671 chore: 1 2026-05-14 18:47:28 +08:00
xiaojunnuo 2f172b56e9 fix: 修复群晖授权没有显示设备id输入框的bug 2026-05-14 18:42:35 +08:00
xiaojunnuo 9076c8b20e Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-05-14 18:31:34 +08:00
xiaojunnuo 639756dfcd chore: sdk 2026-05-14 18:31:25 +08:00
xiaojunnuo 7aa0c7e491 feat: 彩虹登录支持选择多种登录方式 2026-05-14 01:39:22 +08:00
xiaojunnuo 45dedf5bc7 fix: 修复第三方登录丢失state时无法在用户信息页面绑定第三方账号的bug 2026-05-13 23:47:31 +08:00
xiaojunnuo 4681ec9008 fix: 修复自动注册后没有跳转到控制台的bug 2026-05-13 22:47:46 +08:00
xiaojunnuo b91826c6e6 chore: 1 2026-05-13 17:29:49 +08:00
xiaojunnuo 686856d0ae build: release 2026-05-13 17:29:19 +08:00
xiaojunnuo 9b09d2578d chore: 1 2026-05-13 17:14:14 +08:00
xiaojunnuo f8f51adf88 build: publish 2026-05-13 14:22:46 +08:00
xiaojunnuo f8ce639717 build: trigger build image 2026-05-13 14:22:33 +08:00
xiaojunnuo 1c6dc169ac v1.39.16 2026-05-13 14:21:21 +08:00
xiaojunnuo 3e5366c74e build: prepare to build 2026-05-13 14:13:01 +08:00
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
xiaojunnuo b8a64a6b5b perf(plugin-volcengine): 支持火山引擎VKE部署插件
- 改进Kubernetes错误处理,添加RBAC权限不足的友好提示
- 添加集群ID验证和kubeconfig解码验证
- 优化Ingress不存在时的错误提示,显示可用选项
- 移除未使用的TargetCluster选项
- 修复kubeconfig请求中集群ID未验证的问题
2026-05-08 00:25:00 +08:00
xiaojunnuo 25ad1e6f86 perf(设置): 添加首页启用开关配置
在系统设置中添加首页启用开关配置项,包括前端路由守卫检查、多语言支持和表单配置,以控制是否显示首页
2026-05-07 23:15:50 +08:00
xiaojunnuo 6b6f1604e9 style(ui): 修复批量操作区域样式溢出问题
为批量操作区域添加 overflow-x-auto 和 whitespace-nowrap 样式,防止文本溢出
2026-05-07 23:01:42 +08:00
xiaojunnuo 63be1c1cbd perf(证书流水线): 添加批量更新证书申请参数功能
实现批量更新证书申请参数功能,包括前端界面和后端处理逻辑
- 添加批量修改证书申请参数的按钮和对话框
- 实现后端批量更新证书申请参数的接口和服务
- 添加相关测试用例验证功能正确性
2026-05-07 22:54:29 +08:00
xiaojunnuo b75c625ddc fix(aliyun-access): 添加阿里云密钥校验失败的错误处理
在测试阿里云访问密钥时,增加对STS错误响应的处理逻辑
添加相关测试用例验证错误处理和正常情况
2026-05-07 22:18:34 +08:00
xiaojunnuo 7083e7aff7 test(tld-client): 添加对rdap.ss回退机制的测试
验证当rdap和whoiser都失败时,TldClient能正确委托RdapSsClient处理
2026-05-07 22:03:33 +08:00
xiaojunnuo 9d2937dd4b perf(domain): 添加域名过期时间同步进度显示功能
添加同步进度对话框,展示同步任务的实时进度和状态
更新中英文翻译文件,添加相关文案
修改AGENTS.md文档中的格式化命令说明
2026-05-07 00:22:41 +08:00
xiaojunnuo a7e281e278 chore: rdap-ss client 重构 2026-05-05 22:53:22 +08:00
xiaojunnuo 72b6597817 chore: agents update 2026-05-05 22:17:09 +08:00
xiaojunnuo 91a1b97550 perf: 域名注册过期时间获取再次优化 2026-05-05 21:56:08 +08:00
xiaojunnuo 9951ab678f chore: rm app.log 2026-05-05 19:26:36 +08:00
xiaojunnuo 930aa355e8 refactor(acme-client): 将acme-client改造成ts包并优化项目结构
重构acme-client模块,将原有JavaScript代码迁移至TypeScript
添加类型定义文件(.d.ts)和类型检查
更新构建配置和脚本以支持TypeScript编译
优化项目目录结构和模块导出方式
更新相关依赖和开发工具配置
2026-05-05 19:17:44 +08:00
xiaojunnuo e0143fa540 chore: 补充单元测试 2026-05-05 18:44:43 +08:00
190 changed files with 6174 additions and 854 deletions
+13 -4
View File
@@ -105,8 +105,8 @@ Certd 是一个支持私有化部署的 SSL/TLS 证书自动化管理平台。
- 前端 `pnpm dev`:启动 Vite 开发服务
- 前端 `pnpm build`:生产构建
- 前端 `pnpm tsc`类型检查
- 前端 `pnpm test:unit`Vitest 单元测试
- 不要运行前端 `pnpm tsc` / `vue-tsc`:当前依赖组合中 `vue-tsc@1.8.27` 会直接抛内部错误 `Search string not found: "/supportedTSExtensions = .*(?=;)/"`,不是有效的项目类型检查结果。
- 前端暂不跑单元测试;当前 `test:unit` 只是占位脚本
## 流水线与插件模型
@@ -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,5 +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` 文件。
- 前端 TS/Vue/locale 等文件改动后,优先只对本次改动文件运行项目现有自动格式化/修复,例如 `corepack pnpm --dir packages\ui\certd-client exec prettier --write <files>``corepack pnpm --dir packages\ui\certd-client exec eslint --fix <files>`不要为了格式化无关文件而扩大 diff。项目保留了 `tslint` 依赖,但当前主要使用 ESLint + Prettier
- 优先对改动包运行聚焦的测试或类型检查;只有跨包影响明显时再考虑全 monorepo 构建
- 单元测试需要 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>`;不要运行 `vue-tsc` / `pnpm tsc`;不要为了格式化无关文件而扩大 diff。项目保留了 `tslint` 依赖,但当前主要使用 ESLint + Prettier。
- 优先对改动包运行聚焦的测试;后端可按包运行单元测试,前端优先使用 Prettier/ESLint 做改动文件验证。只有跨包影响明显时再考虑全 monorepo 构建。
+63
View File
@@ -3,6 +3,69 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.40.0](https://github.com/certd/certd/compare/v1.39.16...v1.40.0) (2026-05-14)
### Bug Fixes
* 修复第三方登录丢失state时无法在用户信息页面绑定第三方账号的bug ([45dedf5](https://github.com/certd/certd/commit/45dedf5bc779fea852e1f33dda4f31db2765633c))
* 修复群晖授权没有显示设备id输入框的bug ([2f172b5](https://github.com/certd/certd/commit/2f172b56e9411303ca15138d827bdb9bafdae4d1))
* 修复自动注册后没有跳转到控制台的bug ([4681ec9](https://github.com/certd/certd/commit/4681ec90088a3eb665427b2ac4047ec5ccefd7b3))
* 修复clogin登录丢失state问题 ([22f5cfc](https://github.com/certd/certd/commit/22f5cfcfd8462ca74128329eefb3f48b3ee0b7ea))
* 修复clogin多选类型登录失败的bug ([9f878a3](https://github.com/certd/certd/commit/9f878a353cd49b7b10bb0a95610ad236bc920dd2))
### Features
* 彩虹登录支持选择多种登录方式 ([7aa0c7e](https://github.com/certd/certd/commit/7aa0c7e491fe660abb62e68792ff5474f19bd5b8))
### Performance Improvements
* 第三方登录自动注册的用户支持设置初始化密码 ([a815d02](https://github.com/certd/certd/commit/a815d0245b97efbb948b33d6fc9d49862ce06889))
* 头像增加缓存时间 ([7015b1b](https://github.com/certd/certd/commit/7015b1b232602e5168a3eb8bee6d7f1776ae1e74))
## [1.39.16](https://github.com/certd/certd/compare/v1.39.15...v1.39.16) (2026-05-13)
**Note:** Version bump only for package root
## [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
+17 -12
View File
@@ -38,6 +38,7 @@ Certd® 是一个免费的全自动证书管理系统,让你的网站证书永
* **开放接口支持**: 提供RESTful API接口,方便集成到其他系统
* **站点证书监控**: 定时监控网站证书的过期时间
* **多用户管理**: 用户可以管理自己的证书流水线
* **项目管理**: 企业级项目管理模式
* **多语言支持** 中英双语切换
* **无忧升级** 版本向下兼容
@@ -179,19 +180,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设置,插件选项配置 |
| 是否可商用 | 不允许 | 不允许 | 可对外运营 |
## 九、贡献代码
+95
View File
@@ -0,0 +1,95 @@
version: '3.3' # 兼容旧版docker-compose
services:
certd:
# 镜像 # ↓↓↓↓↓ ---- 镜像版本号,建议改成固定版本号,例如:certd:1.29.0
image: registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest
# image: ghcr.io/certd/certd:latest # --------- 如果 报镜像not found,可以尝试其他镜像源
# image: greper/certd:latest
container_name: certd # 容器名
restart: unless-stopped # 自动重启
volumes:
# ↓↓↓↓↓ -------------------------------------------------------- 数据库以及证书存储路径,默认存在宿主机的/data/certd/目录下,【您需要定时备份此目录,以保障数据容灾】
- /data/certd:/app/data # 只要修改冒号前面的,冒号后面的/app/data切记切记不要动
#- /volume1/docker/certd:/app/data:delegated #群晖使用这个配置
# ↓↓↓↓↓ -------------------------------------------------------- 如果走时不准,考虑挂载localtime文件
#- /etc/localtime:/etc/localtime
#- /etc/timezone:/etc/timezone
ports: # 端口映射
# ↓↓↓↓ ---------------------------------------------------------- 如果端口有冲突,可以修改第一个7001为其他不冲突的端口号,第二个7001不要动
- "7001:7001"
# ↓↓↓↓ ---------------------------------------------------------- https端口,可以根据实际情况,是否暴露该端口
- "7002:7002"
#↓↓↓↓ -------------------------------------------------------------- 如果出现getaddrinfo EAI_AGAIN 或 getaddrinfo ENOTFOUND 错误,可以尝试设置dns
# dns:
# - 223.5.5.5 # 阿里云公共dns
# - 223.6.6.6
# # ↓↓↓↓ --------------------------------------------------------- 如果你服务器在腾讯云,可以用这个替换上面阿里云的公共dns
# - 119.29.29.29 # 腾讯云公共dns
# - 182.254.116.116
# # ↓↓↓↓ --------------------------------------------------------- 如果你服务器部署在国外,可以用这个替换上面阿里云的公共dns
# - 8.8.8.8 # 谷歌公共dns
# - 8.8.4.4
# extra_hosts:
# # ↓↓↓↓ -------------------------------------------------------- 这里可以配置自定义hosts,外网域名可以指向本地局域网ip地址
# - "localdomain.com:192.168.1.3"
# # ↓↓↓↓ ------------------------------------------------ 直接使用主机的网络,如果网络问题实在找不到原因,可以尝试打开此参数
# network_mode: host
labels:
com.centurylinklabs.watchtower.enable: "true"
# ↓↓↓↓ -------------------------------------------------------------- 启用ipv6网络,还需要把下面networks的注释放开
# networks:
# - ip6net
environment:
# ↓↓↓↓ ----------------------------------------------------- 使用上海东八时区
- TZ=Asia/Shanghai
# 设置环境变量即可自定义certd配置
# 配置项见: packages/ui/certd-server/src/config/config.default.ts
# 配置规则: certd_ + 配置项, 点号用_代替
# #↓↓↓↓ ----------------------------- 如果忘记管理员密码,可以设置为truedocker compose up -d 重建容器之后,管理员密码将改成123456,然后请及时修改回false
- certd_system_resetAdminPasswd=false
# ↓↓↓ 要使用ipv6,将此配置修改为::
- certd_koa_hostname=0.0.0.0
# 默认使用sqlite文件数据库,如果需要使用其他数据库,请设置以下环境变量
# 注意: 选定使用一种数据库之后,不支持更换数据库。
# 数据库迁移方法:1、使用新数据库重新部署一套,然后将旧数据同步过去,注意flyway_history表的数据不要同步
# #↓↓↓↓ ----------------------------- 使用postgresql数据库,需要提前创建数据库
# - certd_flyway_scriptDir=./db/migration-pg # 升级脚本目录
# - certd_typeorm_dataSource_default_type=postgres # 数据库类型
# - certd_typeorm_dataSource_default_host=localhost # 数据库地址
# - certd_typeorm_dataSource_default_port=5433 # 数据库端口
# - certd_typeorm_dataSource_default_username=postgres # 用户名
# - certd_typeorm_dataSource_default_password=yourpasswd # 密码
# - certd_typeorm_dataSource_default_database=certd # 数据库名
# #↓↓↓↓ ----------------------------- 使用mysql8数据库,需要提前创建数据库 charset=utf8mb4, collation=utf8mb4_bin
# - certd_flyway_scriptDir=./db/migration-mysql # 升级脚本目录
# - certd_typeorm_dataSource_default_type=mysql # 数据库类型, 或者 mariadb
# - certd_typeorm_dataSource_default_host=localhost # 数据库地址
# - certd_typeorm_dataSource_default_port=3306 # 数据库端口
# - certd_typeorm_dataSource_default_username=root # 用户名
# - certd_typeorm_dataSource_default_password=yourpasswd # 密码
# - certd_typeorm_dataSource_default_database=certd # 数据库名
# ↓↓↓↓ --------------------------------------------------------- 自动升级,上面certd的版本号要保持为latest
certd-updater: # 添加 Watchtower 服务
image: containrrr/watchtower:latest
container_name: certd-updater
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
# 配置 自动更新
environment:
- WATCHTOWER_CLEANUP=true # 自动清理旧版本容器
- WATCHTOWER_INCLUDE_STOPPED=false # 不更新已停止的容器
- WATCHTOWER_LABEL_ENABLE=true # 根据容器标签进行更新
- WATCHTOWER_POLL_INTERVAL=600 # 每 10 分钟检查一次更新
# ↓↓↓↓ -------------------------------------------------------------- 启用ipv6网络,还需要把上面networks的注释放开
#networks:
# ip6net:
# enable_ipv6: true
# ipam:
# config:
# - subnet: 2001:db8::/64
+44
View File
@@ -3,6 +3,50 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.16](https://github.com/certd/certd/compare/v1.39.15...v1.39.16) (2026-05-13)
**Note:** Version bump only for package root
## [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 -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设置,插件选项配置 |
| 是否可商用 | 不允许 | 不允许 | 可对外运营 |
## 专业版激活方式
+60
View File
@@ -0,0 +1,60 @@
## 自动升级配置
### 1. 方法一:使用watchtower监控自动升级【推荐】
1. 修改docker-compose.yaml文件增加如下配置
或 [下载完整的自动升级docker-compose.yaml配置](https://gitee.com/certd/certd/raw/v2/docker/auto/docker-compose.yaml)
```yaml
services:
certd:
# 镜像 # ↓↓↓↓↓ ---- 镜像版本号 这里要保持为latest
image: registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest
... # 这里是你原来的docker-compose.yaml配置
# ↓↓↓↓ --------------------------------------------------------- 增加一个标签,表示certd需要自动升级
labels:
com.centurylinklabs.watchtower.enable: "true"
# ↓↓↓↓ --------------------------------------------------------- 自动升级watchtower配置,注意:上面certd的版本号要保持为latest
certd-updater: # 添加 Watchtower 服务
image: containrrr/watchtower:latest
container_name: certd-updater
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
# 配置 自动更新
environment:
- WATCHTOWER_CLEANUP=true # 自动清理旧版本容器
- WATCHTOWER_INCLUDE_STOPPED=false # 不更新已停止的容器
- WATCHTOWER_LABEL_ENABLE=true # 根据容器标签进行更新
- WATCHTOWER_POLL_INTERVAL=600 # 每 10 分钟检查一次更新
```
2. 重启certd容器
```shell
cd certd
docker compose down
docker compose up -d
```
### 2. 方法二:使用Certd版本监控功能【不太稳定】
1. 选择Github-检查Release版本插件
![](./images/github-release.png)
按如下图填写配置
![](./images/github-release-2.png)
2. 检测到新版本后执行宿主机升级命令:
```shell
# 拉取最新镜像
docker pull registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest
# 升级容器命令, 替换成你自己的certd更新命令
export RESTART_CERT='sleep 10; cd ~/deploy/certd/ ; docker compose down; docker compose up -d'
# 构造一个脚本10s后在后台执行,避免容器销毁时执行太快,导致流水线任务无法结束
nohup sh -c '$RESTART_CERT' >/dev/null 2>&1 & echo '10秒后重启' && exit
```
-48
View File
@@ -22,51 +22,3 @@
可以查看最新版本号,以及所有版本的更新日志
[CHANGELOG](../changelogs/CHANGELOG.md)
## 自动升级配置
### 1. 方法一:使用watchtower监控
修改docker-compose.yaml文件增加如下配置, 使用watchtower监控自动升级
```yaml
services:
certd:
...
labels:
com.centurylinklabs.watchtower.enable: "true"
# ↓↓↓↓ --------------------------------------------------------- 自动升级,上面certd的版本号要保持为latest
certd-updater: # 添加 Watchtower 服务
image: containrrr/watchtower:latest
container_name: certd-updater
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
# 配置 自动更新
environment:
- WATCHTOWER_CLEANUP=true # 自动清理旧版本容器
- WATCHTOWER_INCLUDE_STOPPED=false # 不更新已停止的容器
- WATCHTOWER_LABEL_ENABLE=true # 根据容器标签进行更新
- WATCHTOWER_POLL_INTERVAL=600 # 每 10 分钟检查一次更新
```
### 2. 方法二:使用Certd版本监控功能
选择Github-检查Release版本插件
![](./images/github-release.png)
按如下图填写配置
![](./images/github-release-2.png)
检测到新版本后执行宿主机升级命令:
```shell
# 拉取最新镜像
docker pull registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest
# 升级容器命令, 替换成你自己的certd更新命令
export RESTART_CERT='sleep 10; cd ~/deploy/certd/ ; docker compose down; docker compose up -d'
# 构造一个脚本10s后在后台执行,避免容器销毁时执行太快,导致流水线任务无法结束
nohup sh -c '$RESTART_CERT' >/dev/null 2>&1 & echo '10秒后重启' && exit
```
+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.40.0"
}
+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 ",
+12
View File
@@ -1,4 +1,13 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2022,
"sourceType": "module"
},
"ignorePatterns": [
"dist",
"node_modules"
],
"extends": [
"plugin:prettier/recommended",
"prettier"
@@ -7,9 +16,12 @@
"eslint-plugin-import"
],
"env": {
"node": true,
"es2022": true,
"mocha": true
},
"rules": {
"prettier/prettier": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/ban-ts-ignore": "off",
+5
View File
@@ -4,3 +4,8 @@ node_modules/
npm-debug.log
package-lock.json
/.idea/
/dist/
/dist-test/
/logs/
/tsconfig.tsbuildinfo
/tsconfig.test.tsbuildinfo
+24
View File
@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.40.0](https://github.com/publishlab/node-acme-client/compare/v1.39.16...v1.40.0) (2026-05-14)
**Note:** Version bump only for package @certd/acme-client
## [1.39.16](https://github.com/publishlab/node-acme-client/compare/v1.39.15...v1.39.16) (2026-05-13)
**Note:** Version bump only for package @certd/acme-client
## [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
+18 -13
View File
@@ -3,22 +3,22 @@
"description": "Simple and unopinionated ACME client",
"private": false,
"author": "nmorsman",
"version": "1.39.12",
"version": "1.40.0",
"type": "module",
"module": "scr/index.js",
"main": "src/index.js",
"types": "types/index.d.ts",
"module": "./dist/index.js",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"license": "MIT",
"homepage": "https://github.com/publishlab/node-acme-client",
"engines": {
"node": ">= 18"
},
"files": [
"src",
"dist",
"types"
],
"dependencies": {
"@certd/basic": "^1.39.12",
"@certd/basic": "^1.40.0",
"@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",
@@ -47,14 +49,17 @@
"typescript": "^5.4.2"
},
"scripts": {
"build-docs": "jsdoc2md src/client.js > docs/client.md && jsdoc2md src/crypto/index.js > docs/crypto.md && jsdoc2md src/crypto/forge.js > docs/forge.md",
"lint": "eslint .",
"lint-types": "tsd",
"prepublishOnly": "npm run build-docs",
"before-build": "node -e \"const fs=require('fs');fs.rmSync('dist',{recursive:true,force:true});fs.rmSync('tsconfig.tsbuildinfo',{force:true});\"",
"build": "npm run before-build && tsc --skipLibCheck",
"build-docs": "jsdoc2md dist/client.js > docs/client.md && jsdoc2md dist/crypto/index.js > docs/crypto.md && jsdoc2md dist/crypto/forge.js > docs/forge.md",
"lint": "eslint \"src/**/*.ts\" \"types/**/*.ts\"",
"lint-types": "tsd --files \"types/index.test-d.ts\"",
"prepublishOnly": "npm run build && npm run build-docs",
"test": "mocha -t 60000 \"test/setup.js\" \"test/**/*.spec.js\"",
"test:unit": "echo no unit tests",
"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": "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": "echo '1'"
"compile": "tsc --skipLibCheck --watch"
},
"repository": {
"type": "git",
@@ -71,5 +76,5 @@
"bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues"
},
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
"gitHead": "1c6dc169ac04fd09ef94404a912a15cbb17e1452"
}
@@ -1,3 +1,4 @@
// @ts-nocheck
/**
* ACME API client
*/
@@ -1,3 +1,4 @@
// @ts-nocheck
/**
* ACME auto helper
*/
@@ -166,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;
@@ -177,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;
}
@@ -1,3 +1,4 @@
// @ts-nocheck
/**
* Axios instance
*/
@@ -1,3 +1,4 @@
// @ts-nocheck
/**
* ACME client
*
@@ -570,7 +571,7 @@ class AcmeClient {
* ```
*/
async waitForValidStatus(item,d) {
async waitForValidStatus(item, d?) {
if (!item.url) {
throw new Error(`[${d}] Unable to verify status of item, URL not found`);
}
@@ -1,3 +1,4 @@
// @ts-nocheck
/**
* Legacy node-forge crypto interface
*
@@ -112,7 +113,7 @@ function parseDomains(obj) {
* ```
*/
export async function createPrivateKey(size = 2048) {
export async function createPrivateKey(size = 2048): Promise<Buffer> {
const keyPair = await generateKeyPair({ bits: size });
const pemKey = forge.pki.privateKeyToPem(keyPair.privateKey);
return Buffer.from(pemKey);
@@ -131,7 +132,7 @@ export async function createPrivateKey(size = 2048) {
* ```
*/
export const createPublicKey = async (key) => {
export const createPublicKey = async (key): Promise<Buffer> => {
const privateKey = forge.pki.privateKeyFromPem(key);
const publicKey = forge.pki.rsa.setPublicKey(privateKey.n, privateKey.e);
const pemKey = forge.pki.publicKeyToPem(publicKey);
@@ -174,7 +175,7 @@ export const splitPemChain = (str) => forge.pem.decode(str).map(forge.pem.encode
* ```
*/
export const getModulus = async (input) => {
export const getModulus = async (input): Promise<Buffer> => {
if (!Buffer.isBuffer(input)) {
input = Buffer.from(input);
}
@@ -197,7 +198,7 @@ export const getModulus = async (input) => {
* ```
*/
export const getPublicExponent = async (input) => {
export const getPublicExponent = async (input): Promise<Buffer> => {
if (!Buffer.isBuffer(input)) {
input = Buffer.from(input);
}
@@ -1,3 +1,4 @@
// @ts-nocheck
/**
* Native Node.js crypto interface
*
@@ -67,7 +68,7 @@ function getKeyInfo(keyPem) {
* ```
*/
export async function createPrivateRsaKey(modulusLength = 2048, encodingType = 'pkcs8') {
export async function createPrivateRsaKey(modulusLength = 2048, encodingType = 'pkcs8'): Promise<Buffer> {
const pair = await generateKeyPair('rsa', {
modulusLength,
privateKeyEncoding: {
@@ -105,7 +106,7 @@ export const createPrivateKey = createPrivateRsaKey;
* ```
*/
export const createPrivateEcdsaKey = async (namedCurve = 'P-256', encodingType = 'pkcs8') => {
export const createPrivateEcdsaKey = async (namedCurve = 'P-256', encodingType = 'pkcs8'): Promise<Buffer> => {
const pair = await generateKeyPair('ec', {
namedCurve,
privateKeyEncoding: {
@@ -129,7 +130,7 @@ export const createPrivateEcdsaKey = async (namedCurve = 'P-256', encodingType =
* ```
*/
export const getPublicKey = (keyPem) => {
export const getPublicKey = (keyPem): Buffer => {
const info = getKeyInfo(keyPem);
const publicKey = info.publicKey.export({
@@ -1,3 +1,4 @@
// @ts-nocheck
export class CancelError extends Error {
constructor(message) {
super(message);
@@ -1,3 +1,4 @@
// @ts-nocheck
/**
* ACME HTTP client
*/
@@ -0,0 +1,19 @@
import assert from "node:assert/strict";
import { directory, getAllSslProviderDomains, getDirectoryUrl } from "./index.js";
declare const describe: any;
declare const it: any;
describe("directory helpers", () => {
it("selects the provider specific directory endpoint", () => {
assert.equal(getDirectoryUrl({ sslProvider: "sslcom", pkType: "ec" }), directory.sslcom.ec);
assert.equal(getDirectoryUrl({ sslProvider: "letsencrypt", pkType: "rsa" }), directory.letsencrypt.production);
});
it("includes configured provider domains", () => {
const domains = getAllSslProviderDomains();
assert.ok(domains.includes("acme.litessl.com"));
assert.ok(domains.includes("acme.ssl.com"));
});
});
@@ -1,8 +1,9 @@
// @ts-nocheck
/**
* acme-client
*/
import AcmeClinet from './client.js'
export const Client = AcmeClinet
export { default as Client } from './client.js'
export type * from './types.js'
/**
* Directory URLs
@@ -103,4 +104,4 @@ export * from './logger.js'
export * from './verify.js'
export * from './error.js'
export * from './util.js'
export * from './util.js'
@@ -1,3 +1,4 @@
// @ts-nocheck
/**
* ACME logger
*/
+123
View File
@@ -0,0 +1,123 @@
/**
* Account
*
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.2
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.3
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.2
*/
export interface Account {
status: "valid" | "deactivated" | "revoked";
orders: string;
contact?: string[];
termsOfServiceAgreed?: boolean;
externalAccountBinding?: object;
}
export interface AccountCreateRequest {
contact?: string[];
termsOfServiceAgreed?: boolean;
onlyReturnExisting?: boolean;
externalAccountBinding?: object;
}
export interface AccountUpdateRequest {
status?: string;
contact?: string[];
termsOfServiceAgreed?: boolean;
}
/**
* Order
*
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.3
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
*/
export interface Order {
status: "pending" | "ready" | "processing" | "valid" | "invalid";
identifiers: Identifier[];
authorizations: string[];
finalize: string;
expires?: string;
notBefore?: string;
notAfter?: string;
error?: object;
certificate?: string;
}
export interface OrderCreateRequest {
identifiers: Identifier[];
notBefore?: string;
notAfter?: string;
}
/**
* Authorization
*
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.4
*/
export interface Authorization {
identifier: Identifier;
status: "pending" | "valid" | "invalid" | "deactivated" | "expired" | "revoked";
challenges: Challenge[];
expires?: string;
wildcard?: boolean;
}
export interface Identifier {
type: string;
value: string;
}
/**
* Challenge
*
* https://datatracker.ietf.org/doc/html/rfc8555#section-8
* https://datatracker.ietf.org/doc/html/rfc8555#section-8.3
* https://datatracker.ietf.org/doc/html/rfc8555#section-8.4
*/
export interface ChallengeAbstract {
type: string;
url: string;
status: "pending" | "processing" | "valid" | "invalid";
validated?: string;
error?: object;
}
export interface HttpChallenge extends ChallengeAbstract {
type: "http-01";
token: string;
}
export interface DnsChallenge extends ChallengeAbstract {
type: "dns-01";
token: string;
}
export type Challenge = HttpChallenge | DnsChallenge;
/**
* Certificate
*
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.6
*/
export enum CertificateRevocationReason {
Unspecified = 0,
KeyCompromise = 1,
CACompromise = 2,
AffiliationChanged = 3,
Superseded = 4,
CessationOfOperation = 5,
CertificateHold = 6,
RemoveFromCRL = 8,
PrivilegeWithdrawn = 9,
AACompromise = 10,
}
export interface CertificateRevocationRequest {
reason?: CertificateRevocationReason;
}
+131
View File
@@ -0,0 +1,131 @@
import type * as rfc8555 from "./rfc8555.js";
import type { Challenge } from "./rfc8555.js";
export type * from "./rfc8555.js";
export type PrivateKeyBuffer = Buffer;
export type PublicKeyBuffer = Buffer;
export type CertificateBuffer = Buffer;
export type CsrBuffer = Buffer;
export type PrivateKeyString = string;
export type PublicKeyString = string;
export type CertificateString = string;
export type CsrString = string;
export interface Order extends rfc8555.Order {
url: string;
}
export interface Authorization extends rfc8555.Authorization {
url: string;
}
export type UrlMapping = {
enabled: boolean;
mappings: Record<string, string>;
};
export interface ClientExternalAccountBindingOptions {
kid: string;
hmacKey: string;
}
export interface ClientOptions {
sslProvider: string;
directoryUrl: string;
accountKey: PrivateKeyBuffer | PrivateKeyString;
accountUrl?: string;
externalAccountBinding?: ClientExternalAccountBindingOptions;
backoffAttempts?: number;
backoffMin?: number;
backoffMax?: number;
urlMapping?: UrlMapping;
signal?: AbortSignal;
logger?: any;
}
export interface ClientAutoOptions {
csr: CsrBuffer | CsrString;
challengeCreateFn: (
authz: Authorization,
keyAuthorization: (challenge: Challenge) => Promise<string>
) => Promise<{ recordReq?: any; recordRes?: any; dnsProvider?: any; challenge: Challenge; keyAuthorization: string }>;
challengeRemoveFn: (authz: Authorization, challenge: Challenge, keyAuthorization: string, recordReq: any, recordRes: any, dnsProvider: any, httpUploader: any) => Promise<any>;
email?: string;
termsOfServiceAgreed?: boolean;
skipChallengeVerification?: boolean;
challengePriority?: string[];
preferredChain?: string;
signal?: AbortSignal;
profile?: string;
waitDnsDiffuseTime?: number;
}
export interface CertificateDomains {
commonName: string;
altNames: string[];
}
export interface CertificateIssuer {
commonName: string;
}
export interface CertificateInfo {
issuer: CertificateIssuer;
domains: CertificateDomains;
notAfter: Date;
notBefore: Date;
}
export interface CsrOptions {
keySize?: number;
commonName?: string;
altNames?: string[];
country?: string;
state?: string;
locality?: string;
organization?: string;
organizationUnit?: string;
emailAddress?: string;
}
export interface RsaPublicJwk {
e: string;
kty: string;
n: string;
}
export interface EcdsaPublicJwk {
crv: string;
kty: string;
x: string;
y: string;
}
export interface CryptoInterface {
createPrivateKey(keySize?: number, encodingType?: string): Promise<PrivateKeyBuffer>;
createPrivateRsaKey(keySize?: number, encodingType?: string): Promise<PrivateKeyBuffer>;
createPrivateEcdsaKey(namedCurve?: "P-256" | "P-384" | "P-521", encodingType?: string): Promise<PrivateKeyBuffer>;
getPublicKey(keyPem: PrivateKeyBuffer | PrivateKeyString | PublicKeyBuffer | PublicKeyString): PublicKeyBuffer;
getJwk(keyPem: PrivateKeyBuffer | PrivateKeyString | PublicKeyBuffer | PublicKeyString): RsaPublicJwk | EcdsaPublicJwk;
splitPemChain(chainPem: CertificateBuffer | CertificateString): string[];
getPemBodyAsB64u(pem: CertificateBuffer | CertificateString): string;
readCsrDomains(csrPem: CsrBuffer | CsrString): CertificateDomains;
readCertificateInfo(certPem: CertificateBuffer | CertificateString): CertificateInfo;
createCsr(data: CsrOptions, keyPem?: PrivateKeyBuffer | PrivateKeyString, encodingType?: string): Promise<[PrivateKeyBuffer, CsrBuffer]>;
createAlpnCertificate(authz: Authorization, keyAuthorization: string, keyPem?: PrivateKeyBuffer | PrivateKeyString): Promise<[PrivateKeyBuffer, CertificateBuffer]>;
isAlpnCertificateAuthorizationValid(certPem: CertificateBuffer | CertificateString, keyAuthorization: string): boolean;
}
export interface CryptoLegacyInterface {
createPrivateKey(size?: number): Promise<PrivateKeyBuffer>;
createPublicKey(key: PrivateKeyBuffer | PrivateKeyString): Promise<PublicKeyBuffer>;
getPemBody(str: string): string;
splitPemChain(str: string): string[];
getModulus(input: PrivateKeyBuffer | PrivateKeyString | PublicKeyBuffer | PublicKeyString | CertificateBuffer | CertificateString | CsrBuffer | CsrString): Promise<Buffer>;
getPublicExponent(input: PrivateKeyBuffer | PrivateKeyString | PublicKeyBuffer | PublicKeyString | CertificateBuffer | CertificateString | CsrBuffer | CsrString): Promise<Buffer>;
readCsrDomains(csr: CsrBuffer | CsrString): Promise<CertificateDomains>;
readCertificateInfo(cert: CertificateBuffer | CertificateString): Promise<CertificateInfo>;
createCsr(data: CsrOptions, key?: PrivateKeyBuffer | PrivateKeyString): Promise<[PrivateKeyBuffer, CsrBuffer]>;
}
@@ -0,0 +1,58 @@
import assert from "node:assert/strict";
import { formatResponseError, parseRetryAfterHeader, retry } from "./util.js";
declare const describe: any;
declare const it: any;
describe("util helpers", () => {
it("parses retry-after values", () => {
assert.equal(parseRetryAfterHeader("120"), 120);
assert.equal(parseRetryAfterHeader("invalid"), 0);
assert.equal(parseRetryAfterHeader("Wed, 21 Oct 2015 07:28:00 GMT"), 0);
});
it("formats response errors without newlines", () => {
const error = formatResponseError({
data: {
error: {
detail: "line 1\nline 2",
},
},
});
assert.equal(error, "line 1line 2");
});
it("retries until success", async () => {
const delays: number[] = [];
const originalSetTimeout = globalThis.setTimeout;
let attempts = 0;
(globalThis as any).setTimeout = (fn: (...args: any[]) => void, delay?: number) => {
delays.push(Number(delay));
return originalSetTimeout(fn, 0);
};
try {
const result = await retry(
async () => {
attempts += 1;
if (attempts < 3) {
throw new Error(`boom-${attempts}`);
}
return "ok";
},
{ attempts: 3, min: 10, max: 20 },
() => {}
);
assert.equal(result, "ok");
assert.equal(attempts, 3);
assert.deepEqual(delays, [10, 20]);
} finally {
(globalThis as any).setTimeout = originalSetTimeout;
}
});
});
@@ -1,3 +1,4 @@
// @ts-nocheck
/**
* Utility methods
*/
@@ -1,3 +1,4 @@
// @ts-nocheck
/**
* ACME challenge verification
*/
@@ -1,3 +1,4 @@
// @ts-nocheck
export async function wait(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
+29
View File
@@ -0,0 +1,29 @@
{
"compileOnSave": true,
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "node",
"esModuleInterop": true,
"inlineSourceMap": false,
"sourceMap": false,
"noImplicitThis": false,
"noUnusedLocals": false,
"stripInternal": true,
"skipLibCheck": true,
"pretty": true,
"declaration": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"outDir": "dist",
"rootDir": "src",
"composite": false,
"useDefineForClassFields": false,
"strict": false,
"resolveJsonModule": true,
"isolatedModules": false,
"lib": ["ESNext", "DOM"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.json"],
"exclude": ["dist", "node_modules", "src/**/*.test.ts", "test"]
}
@@ -0,0 +1,11 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "dist-test",
"declaration": false,
"declarationMap": false,
"emitDeclarationOnly": false
},
"include": ["src/**/*.ts"],
"exclude": ["dist", "dist-test", "node_modules", "test"]
}
+14 -5
View File
@@ -4,8 +4,6 @@
import { AxiosInstance } from 'axios';
import * as rfc8555 from './rfc8555';
import {CancelError} from '../src/error.js'
export * from '../src/error.js'
export type PrivateKeyBuffer = Buffer;
export type PublicKeyBuffer = Buffer;
@@ -115,6 +113,15 @@ export const directory: {
zerossl: {
staging: string,
production: string
},
sslcom: {
staging: string,
production: string,
ec: string
},
litessl: {
staging: string,
production: string
}
};
@@ -211,14 +218,16 @@ export const agents: any;
* Logger
*/
export class CancelError extends Error {
constructor(message?: string);
}
export function setLogger(fn: (message: any, ...args: any[]) => void): void;
export function createChallengeFn(opts?: {logger?:any}): any;
// export function walkTxtRecord(record: any): Promise<string[]>;
export function getAuthoritativeDnsResolver(record:string): Promise<any>;
export const CancelError: typeof CancelError;
export function resolveDomainBySoaRecord(domain: string): Promise<string>;
export function setWalkFromAuthoritative(value = true): void;
export function setWalkFromAuthoritative(value?: boolean): void;
@@ -2,7 +2,7 @@
* acme-client type definition tests
*/
import * as acme from 'acme-client';
import * as acme from '..';
(async () => {
/* Client */
@@ -10,6 +10,7 @@ import * as acme from 'acme-client';
const client = new acme.Client({
accountKey,
sslProvider: 'letsencrypt',
directoryUrl: acme.directory.letsencrypt.staging
});
@@ -52,7 +53,10 @@ import * as acme from 'acme-client';
/* Auto */
await client.auto({
csr: certCsr,
challengeCreateFn: async (authz, challenge, keyAuthorization) => {},
challengeCreateFn: async (authz, keyAuthorization) => ({
challenge: authz.challenges[0],
keyAuthorization: await keyAuthorization(authz.challenges[0])
}),
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {}
});
@@ -63,7 +67,10 @@ import * as acme from 'acme-client';
skipChallengeVerification: false,
challengePriority: ['http-01', 'dns-01'],
preferredChain: 'DST Root CA X3',
challengeCreateFn: async (authz, challenge, keyAuthorization) => {},
challengeCreateFn: async (authz, keyAuthorization) => ({
challenge: authz.challenges[0],
keyAuthorization: await keyAuthorization(authz.challenges[0])
}),
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {}
});
})();
+24
View File
@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.40.0](https://github.com/certd/certd/compare/v1.39.16...v1.40.0) (2026-05-14)
**Note:** Version bump only for package @certd/basic
## [1.39.16](https://github.com/certd/certd/compare/v1.39.15...v1.39.16) (2026-05-13)
**Note:** Version bump only for package @certd/basic
## [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
00:44
+5 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/basic",
"private": false,
"version": "1.39.12",
"version": "1.40.0",
"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": "1c6dc169ac04fd09ef94404a912a15cbb17e1452"
}
@@ -0,0 +1,42 @@
/// <reference types="mocha" />
import { expect } from "chai";
import { isDev } from "./util.env.js";
describe("isDev", () => {
const originalNodeEnv = process.env.NODE_ENV;
afterEach(() => {
if (originalNodeEnv == null) {
delete process.env.NODE_ENV;
} else {
process.env.NODE_ENV = originalNodeEnv;
}
});
it("treats missing NODE_ENV as development", () => {
delete process.env.NODE_ENV;
expect(isDev()).to.equal(true);
});
it("detects development-like NODE_ENV values", () => {
process.env.NODE_ENV = "development";
expect(isDev()).to.equal(true);
process.env.NODE_ENV = "local";
expect(isDev()).to.equal(true);
process.env.NODE_ENV = "dev-test";
expect(isDev()).to.equal(true);
});
it("rejects production-like NODE_ENV values", () => {
process.env.NODE_ENV = "production";
expect(isDev()).to.equal(false);
process.env.NODE_ENV = "test";
expect(isDev()).to.equal(false);
});
});
@@ -0,0 +1,45 @@
/// <reference types="mocha" />
import { expect } from "chai";
import { hashUtils } from "./util.hash.js";
describe("hashUtils", () => {
describe("digest helpers", () => {
it("generates md5 and sha256 hex digests by default", () => {
expect(hashUtils.md5("certd")).to.equal("3f3d9f715fcc63d54a4a224e0939a233");
expect(hashUtils.sha256("certd")).to.equal("26a6366060d2a6477185c05075155769cb438c6c71f61f509535b8516594ad92");
});
it("supports alternate digest encodings", () => {
expect(hashUtils.md5("certd", "base64")).to.equal("Pz2fcV/MY9VKSiJOCTmiMw==");
expect(hashUtils.sha256("certd", "base64")).to.equal("JqY2YGDSpkdxhcBQdRVXactDjGxx9h9QlTW4UWWUrZI=");
});
});
describe("hmac helpers", () => {
it("signs data with a provided key", () => {
expect(hashUtils.hmacSha256WithKey("secret", "certd")).to.equal("kh/kUD/Ji8FHfpt4vYUHZx+1BZvKSyyklZIiuS+Rzlg=");
});
it("uses an empty payload when only the key is provided", () => {
expect(hashUtils.hmacSha256("secret")).to.equal("+eZuF5tnR65UEI+C+K3os8Jddv0wr95sOVgixTAZYWk=");
});
});
describe("encoding helpers", () => {
it("round trips base64 values", () => {
const encoded = hashUtils.base64("证书-certd");
expect(encoded).to.equal("6K+B5LmmLWNlcnRk");
expect(hashUtils.base64Decode(encoded)).to.equal("证书-certd");
});
it("converts strings and numbers to hex", () => {
expect(hashUtils.toHex("certd")).to.equal("6365727464");
expect(hashUtils.hexToStr("6365727464")).to.equal("certd");
expect(hashUtils.toHex(255)).to.equal("ff");
expect(hashUtils.hexToNumber("ff")).to.equal(255);
});
});
});
@@ -0,0 +1,18 @@
/// <reference types="mocha" />
import { expect } from "chai";
import { randomNumber, simpleNanoId } from "./util.id.js";
describe("id utils", () => {
it("generates a four digit random number string", () => {
expect(randomNumber()).to.match(/^\d{4}$/);
});
it("generates a twelve character simple nano id", () => {
const id = simpleNanoId();
expect(id).to.have.length(12);
expect(id).to.match(/^[0-9a-zA-Z]+$/);
});
});
@@ -0,0 +1,78 @@
/// <reference types="mocha" />
import { expect } from "chai";
import { mergeUtils, UnMergeable } from "./util.merge.js";
describe("mergeUtils", () => {
describe("merge", () => {
it("deep merges plain objects", () => {
const target = { acme: { email: "admin@example.com" }, deploy: { retries: 1 } };
const result = mergeUtils.merge(target, { acme: { dnsProvider: "cloudflare" } });
expect(result).to.equal(target);
expect(result).to.deep.equal({
acme: { email: "admin@example.com", dnsProvider: "cloudflare" },
deploy: { retries: 1 },
});
});
it("replaces arrays instead of merging them by index", () => {
const result = mergeUtils.merge({ domains: ["old.example.com", "legacy.example.com"] }, { domains: ["new.example.com"] });
expect(result).to.deep.equal({ domains: ["new.example.com"] });
});
it("allows null to clear nested values", () => {
const result = mergeUtils.merge({ cert: { name: "certd" } }, { cert: null });
expect(result).to.deep.equal({ cert: null });
});
it("keeps undefined sources from overwriting existing nested values", () => {
const result = mergeUtils.merge({ cert: { name: "certd" } }, { cert: undefined });
expect(result).to.deep.equal({ cert: { name: "certd" } });
});
it("returns an UnMergeable source directly when it is merged at the top level", () => {
const source = new UnMergeable();
const result = mergeUtils.merge({ enabled: true }, source);
expect(result).to.equal(source);
});
it("replaces nested values marked as UnMergeable", () => {
const source = new UnMergeable();
const result = mergeUtils.merge({ plugin: { enabled: true } }, { plugin: source });
expect(result.plugin).to.equal(source);
});
});
describe("cloneDeep", () => {
it("deep clones plain values", () => {
const source = { acme: { email: "admin@example.com" }, domains: ["example.com"] };
const result = mergeUtils.cloneDeep(source);
expect(result).to.deep.equal(source);
expect(result).not.to.equal(source);
expect(result.acme).not.to.equal(source.acme);
expect(result.domains).not.to.equal(source.domains);
});
it("preserves references marked as not cloneable", () => {
const uncloneable = new UnMergeable();
const source = { plugin: uncloneable };
const result = mergeUtils.cloneDeep(source);
expect(result).not.to.equal(source);
expect(result.plugin).to.equal(uncloneable);
});
});
});
@@ -0,0 +1,90 @@
/// <reference types="mocha" />
import { expect } from "chai";
import { logger } from "./util.log.js";
import { promises } from "./util.promise.js";
describe("promises", () => {
describe("TimeoutPromise", () => {
it("resolves when the callback finishes before the timeout", async () => {
let completed = false;
await promises.TimeoutPromise(async () => {
completed = true;
}, 50);
expect(completed).to.equal(true);
});
it("rejects when the callback exceeds the timeout", async () => {
try {
await promises.TimeoutPromise(() => new Promise<void>(resolve => setTimeout(resolve, 30)), 5);
expect.fail("expected TimeoutPromise to reject");
} catch (e: any) {
expect(e.message).to.equal("Task timeout in 5 ms");
}
});
});
describe("safePromise", () => {
it("resolves values provided by the callback", async () => {
const result = await promises.safePromise<string>(resolve => {
resolve("ok");
});
expect(result).to.equal("ok");
});
it("rejects synchronous errors thrown by the callback", async () => {
const oldLevel = logger.level;
logger.level = "fatal";
try {
await promises.safePromise(() => {
throw new Error("boom");
});
expect.fail("expected safePromise to reject");
} catch (e: any) {
expect(e.message).to.equal("boom");
} finally {
logger.level = oldLevel;
}
});
});
describe("promisify", () => {
it("resolves callback data", async () => {
const readValue = promises.promisify((prefix: string, callback: (err: Error | null, data?: string) => void) => {
callback(null, `${prefix}-value`);
});
expect(await readValue("certd")).to.equal("certd-value");
});
it("rejects callback errors", async () => {
const failing = promises.promisify((callback: (err: Error) => void) => {
callback(new Error("callback failed"));
});
try {
await failing();
expect.fail("expected promisified function to reject");
} catch (e: any) {
expect(e.message).to.equal("callback failed");
}
});
it("rejects synchronous errors", async () => {
const failing = promises.promisify(() => {
throw new Error("sync failed");
});
try {
await failing();
expect.fail("expected promisified function to reject");
} catch (e: any) {
expect(e.message).to.equal("sync failed");
}
});
});
});
@@ -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;
}
+26
View File
@@ -3,6 +3,32 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.40.0](https://github.com/certd/certd/compare/v1.39.16...v1.40.0) (2026-05-14)
**Note:** Version bump only for package @certd/pipeline
## [1.39.16](https://github.com/certd/certd/compare/v1.39.15...v1.39.16) (2026-05-13)
**Note:** Version bump only for package @certd/pipeline
## [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.40.0",
"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.40.0",
"@certd/plus-core": "^1.40.0",
"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": "1c6dc169ac04fd09ef94404a912a15cbb17e1452"
}
@@ -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;
+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.40.0](https://github.com/certd/certd/compare/v1.39.16...v1.40.0) (2026-05-14)
**Note:** Version bump only for package @certd/lib-huawei
## [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.40.0",
"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"
}
+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.40.0](https://github.com/certd/certd/compare/v1.39.16...v1.40.0) (2026-05-14)
**Note:** Version bump only for package @certd/lib-iframe
## [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.40.0",
"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"
}
+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.40.0](https://github.com/certd/certd/compare/v1.39.16...v1.40.0) (2026-05-14)
**Note:** Version bump only for package @certd/jdcloud
## [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.40.0",
"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"
}
+22
View File
@@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.40.0](https://github.com/certd/certd/compare/v1.39.16...v1.40.0) (2026-05-14)
**Note:** Version bump only for package @certd/lib-k8s
## [1.39.16](https://github.com/certd/certd/compare/v1.39.15...v1.39.16) (2026-05-13)
**Note:** Version bump only for package @certd/lib-k8s
## [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.40.0",
"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.40.0",
"@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": "1c6dc169ac04fd09ef94404a912a15cbb17e1452"
}
+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.40.0](https://github.com/certd/certd/compare/v1.39.16...v1.40.0) (2026-05-14)
### Features
* 彩虹登录支持选择多种登录方式 ([7aa0c7e](https://github.com/certd/certd/commit/7aa0c7e491fe660abb62e68792ff5474f19bd5b8))
## [1.39.16](https://github.com/certd/certd/compare/v1.39.15...v1.39.16) (2026-05-13)
**Note:** Version bump only for package @certd/lib-server
## [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.40.0",
"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.40.0",
"@certd/basic": "^1.40.0",
"@certd/pipeline": "^1.40.0",
"@certd/plugin-lib": "^1.40.0",
"@certd/plus-core": "^1.40.0",
"@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": "1c6dc169ac04fd09ef94404a912a15cbb17e1452"
}
@@ -32,6 +32,7 @@ export class SysPublicSettings extends BaseSettings {
customFooter?: string;
robots?: boolean = true;
aiChatEnabled = true;
homePageEnabled = true;
//验证码是否开启
@@ -63,6 +64,7 @@ export class SysPublicSettings extends BaseSettings {
type: string;
title: string;
addonId: number;
icon?: string;
}> = {};
notice?: string;
@@ -79,6 +81,7 @@ export class SysPrivateSettings extends BaseSettings {
httpsProxy? = '';
httpProxy? = '';
commonHeaders?: string = '';
reverseProxies?: Record<string, string> = {};
@@ -271,4 +274,3 @@ export class SysSafeSetting extends BaseSettings {
autoHiddenTimes: 5,
};
}
@@ -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,24 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.40.0](https://github.com/certd/certd/compare/v1.39.16...v1.40.0) (2026-05-14)
**Note:** Version bump only for package @certd/midway-flyway-js
## [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.40.0",
"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"
}
+22
View File
@@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.40.0](https://github.com/certd/certd/compare/v1.39.16...v1.40.0) (2026-05-14)
**Note:** Version bump only for package @certd/plugin-cert
## [1.39.16](https://github.com/certd/certd/compare/v1.39.15...v1.39.16) (2026-05-13)
**Note:** Version bump only for package @certd/plugin-cert
## [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.40.0",
"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.40.0",
"@certd/basic": "^1.40.0",
"@certd/pipeline": "^1.40.0",
"@certd/plugin-lib": "^1.40.0",
"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": "1c6dc169ac04fd09ef94404a912a15cbb17e1452"
}
+22
View File
@@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.40.0](https://github.com/certd/certd/compare/v1.39.16...v1.40.0) (2026-05-14)
**Note:** Version bump only for package @certd/plugin-lib
## [1.39.16](https://github.com/certd/certd/compare/v1.39.15...v1.39.16) (2026-05-13)
**Note:** Version bump only for package @certd/plugin-lib
## [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.40.0",
"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.40.0",
"@certd/basic": "^1.40.0",
"@certd/pipeline": "^1.40.0",
"@certd/plus-core": "^1.40.0",
"@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": "1c6dc169ac04fd09ef94404a912a15cbb17e1452"
}
+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
+49
View File
@@ -3,6 +3,55 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.40.0](https://github.com/certd/certd/compare/v1.39.16...v1.40.0) (2026-05-14)
### Bug Fixes
* 修复第三方登录丢失state时无法在用户信息页面绑定第三方账号的bug ([45dedf5](https://github.com/certd/certd/commit/45dedf5bc779fea852e1f33dda4f31db2765633c))
* 修复群晖授权没有显示设备id输入框的bug ([2f172b5](https://github.com/certd/certd/commit/2f172b56e9411303ca15138d827bdb9bafdae4d1))
* 修复自动注册后没有跳转到控制台的bug ([4681ec9](https://github.com/certd/certd/commit/4681ec90088a3eb665427b2ac4047ec5ccefd7b3))
### Features
* 彩虹登录支持选择多种登录方式 ([7aa0c7e](https://github.com/certd/certd/commit/7aa0c7e491fe660abb62e68792ff5474f19bd5b8))
### Performance Improvements
* 头像增加缓存时间 ([7015b1b](https://github.com/certd/certd/commit/7015b1b232602e5168a3eb8bee6d7f1776ae1e74))
## [1.39.16](https://github.com/certd/certd/compare/v1.39.15...v1.39.16) (2026-05-13)
**Note:** Version bump only for package @certd/ui-client
## [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.40.0",
"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.40.0",
"@certd/pipeline": "^1.40.0",
"@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,24 +1,18 @@
import { dict } from "@fast-crud/fast-crud";
import { $t } from "/@/locales";
export const Dicts = {
sslProviderDict: dict({
data: [
{ value: "letsencrypt", label: "Let's Encrypt" },
{ value: "zerossl", label: "ZeroSSL" },
],
}),
challengeTypeDict: dict({
function createChallengeTypeDict() {
return dict({
data: [
{ value: "dns", label: $t("certd.verifyPlan.dnsChallenge"), color: "green" },
{ value: "cname", label: $t("certd.verifyPlan.cnameProxyChallenge"), color: "blue" },
{ value: "http", label: $t("certd.verifyPlan.httpChallenge"), color: "yellow" },
],
}),
dnsProviderTypeDict: dict({
url: "pi/dnsProvider/dnsProviderTypeDict",
}),
uploaderTypeDict: dict({
});
}
function createUploaderTypeDict() {
return dict({
data: [
{ label: "SFTP", value: "sftp" },
{ label: "SCP", value: "scp" },
@@ -29,11 +23,35 @@ export const Dicts = {
{ label: "S3/Minio", value: "s3" },
{ label: $t("certd.verifyPlan.uploader.sshDeprecated"), value: "ssh", disabled: true },
],
}),
domainFromTypeDict: dict({
});
}
function createDomainFromTypeDict() {
return dict({
data: [
{ value: "manual", label: $t("certd.verifyPlan.domainFrom.manual") },
{ value: "auto", label: $t("certd.verifyPlan.domainFrom.auto") },
],
});
}
export const Dicts = {
sslProviderDict: dict({
data: [
{ value: "letsencrypt", label: "Let's Encrypt" },
{ value: "zerossl", label: "ZeroSSL" },
],
}),
get challengeTypeDict() {
return createChallengeTypeDict();
},
dnsProviderTypeDict: dict({
url: "pi/dnsProvider/dnsProviderTypeDict",
}),
get uploaderTypeDict() {
return createUploaderTypeDict();
},
get domainFromTypeDict() {
return createDomainFromTypeDict();
},
};
@@ -78,8 +78,13 @@ export default {
passkeyRegisterFailed: "Passkey registration failed",
title: "Change Password",
weakPasswordWarning: "For your account security, please change your password immediately",
initPasswordWarning: "This account does not have a login password yet. Please set one first",
initPasswordTitle: "Set Password",
changeNow: "Change Now",
setNow: "Set Now",
notNow: "Not Now",
successMessage: "Changed successfully",
initPasswordSuccessMessage: "Set successfully",
oldPassword: "Old Password",
oldPasswordRequired: "Please enter the old password",
newPassword: "New Password",
@@ -20,6 +20,7 @@ export default {
importFromProvider: "Import from Domain Provider",
syncExpirationDate: "Sync Domain Expiration Time",
syncTaskSubmitted: "Sync task submitted",
syncExpirationProgress: "Sync Domain Expiration Progress",
expirationMonitorSetting: "Domain Expiration Monitor Settings",
subdomainDnsHelper: "Note: In DNS validation mode, subdomains do not need to be maintained here, otherwise certificate application may be affected (except delegated subdomains or free second-level domains).",
path: "Path",
@@ -28,6 +29,9 @@ export default {
progress: "Progress",
operation: "Operation",
total: "Total",
current: "Current",
running: "Running",
done: "Done",
skipped: "Skipped",
failed: "Failed",
notExecuted: "Not executed",
@@ -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",
};
@@ -16,6 +16,7 @@ export default {
showRunStrategy: "Show RunStrategy",
showRunStrategyHelper: "Allow modify the run strategy of the task",
homePageEnabled: "Enable Home Page",
captchaEnabled: "Enable Login Captcha",
captchaHelper: "Whether to enable captcha verification for login",
@@ -90,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",
@@ -79,8 +79,13 @@ export default {
title: "修改密码",
weakPasswordWarning: "为了您的账户安全,请立即修改密码",
initPasswordWarning: "当前账号还未设置登录密码,请先设置密码",
initPasswordTitle: "设置密码",
changeNow: "立即修改",
setNow: "立即设置",
notNow: "暂不设置",
successMessage: "修改成功",
initPasswordSuccessMessage: "设置成功",
oldPassword: "旧密码",
oldPasswordRequired: "请输入旧密码",
newPassword: "新密码",
@@ -20,6 +20,7 @@ export default {
importFromProvider: "从域名提供商导入",
syncExpirationDate: "同步域名过期时间",
syncTaskSubmitted: "同步任务已提交",
syncExpirationProgress: "同步域名过期时间进度",
expirationMonitorSetting: "域名过期监控设置",
subdomainDnsHelper: "注意:DNS校验方式下,子域名不需要在此处维护,否则会影响证书申请(子域名托管或免费二级域名除外)",
path: "路径",
@@ -28,6 +29,9 @@ export default {
progress: "进度",
operation: "操作",
total: "总数",
current: "当前",
running: "运行中",
done: "已完成",
skipped: "跳过",
failed: "失败",
notExecuted: "未执行",
@@ -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提供商授权",
};
@@ -18,6 +18,7 @@ export default {
showRunStrategy: "显示运行策略选择",
showRunStrategyHelper: "任务设置中是否允许选择运行策略",
homePageEnabled: "启用首页",
captchaEnabled: "启用登录验证码",
captchaHelper: "登录时是否启用验证码",
@@ -88,6 +89,8 @@ export default {
reverseProxyEmpty: "未配置反向代理",
environmentVars: "环境变量",
environmentVarsHelper: "配置运行时环境变量,每行一个,格式:KEY=VALUE",
commonHeaders: "公共请求头",
commonHeadersHelper: "服务端发起 HTTP 请求时自动附加的公共请求头,每行一个,格式:KEY=VALUE;请求中已设置同名 Header 时不会覆盖\n注意: 不要将token等敏感内容放在此处,仅限个人和公司内部使用,商业版不要设置",
bindUrl: "绑定URL",
bindUrlHelper: "绑定URL,在各类通知中显示你的站点URL",
},
@@ -47,6 +47,13 @@ export function setupCommonGuard(router: Router) {
const settingStore = useSettingStore();
await settingStore.initOnce();
if (to.path === "/" && settingStore.sysPublic?.homePageEnabled === false) {
return {
path: DEFAULT_HOME_PATH,
replace: true,
};
}
to.meta.loaded = loadedPaths.has(to.path);
// 页面加载进度条
@@ -48,6 +48,7 @@ export type SysPublicSetting = {
customFooter?: string;
robots?: boolean;
aiChatEnabled?: boolean;
homePageEnabled?: boolean;
showRunStrategy?: boolean;
@@ -84,6 +85,7 @@ export type SysPublicSetting = {
type: string;
title: string;
addonId: number;
icon?: string;
}
>;
// 系统通知
@@ -98,6 +100,7 @@ export type SuiteSetting = {
export type SysPrivateSetting = {
httpProxy?: string;
httpsProxy?: string;
commonHeaders?: string;
reverseProxies?: any;
dnsResultOrder?: string;
commonCnameEnabled?: boolean;
@@ -81,6 +81,7 @@ export const useSettingStore = defineStore({
registerEnabled: false,
managerOtherUserPipeline: false,
icpNo: env.ICP_NO || "",
homePageEnabled: true,
},
installInfo: {
siteId: "",
@@ -38,6 +38,7 @@ export interface UserInfoRes {
avatar?: string;
roleIds: number[];
isWeak?: boolean;
needInitPassword?: boolean;
validTime?: number;
status?: number;
}
@@ -38,6 +38,9 @@ export const useUserStore = defineStore({
getToken(): string {
return this.token || LocalStorage.get(TOKEN_KEY);
},
isLogined(): boolean {
return !!this.getToken;
},
isAdmin(): boolean {
return this.getUserInfo.roleIds?.includes(1) || this.getUserInfo.id === 1;
},
@@ -11,7 +11,18 @@ export type MergeScriptContext = {
export function useReference(formItem: any) {
if (formItem.mergeScript) {
const ctx = {
compute,
compute: (opts: any) => {
const func = (context: any) => {
debugger;
let form = context.form || {};
form = form.input || form.body || form; // form.access去掉,历史原因,access的mergeScript会处理form.access
return opts({
...context,
form,
});
};
return compute(func);
},
asyncCompute,
computed,
};
@@ -3,7 +3,7 @@ import { Modal, notification } from "ant-design-vue";
import { Ref, ref } from "vue";
import { useRouter } from "vue-router";
import * as api from "./api";
import { useDomainImportManage } from "./use";
import { useDomainImportManage, useSyncExpirationProcess } from "./use";
import { Dicts } from "/@/components/plugins/lib/dicts";
import { useSettingStore } from "/@/store/settings";
import { useUserStore } from "/@/store/user";
@@ -52,6 +52,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
});
const openDomainImportManageDialog = useDomainImportManage();
const openSyncExpirationProcessDialog = useSyncExpirationProcess({ crudExpose });
const subdomainConfirmed = ref(false);
return {
@@ -140,13 +141,11 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
icon: "ion:refresh-outline",
text: t("certd.domain.syncExpirationDate"),
click: async () => {
await api.SyncExpirationStart();
notification.success({
message: t("certd.domain.syncTaskSubmitted"),
});
setTimeout(() => {
crudExpose.doRefresh();
}, 2000);
try {
await api.SyncExpirationStart();
} finally {
await openSyncExpirationProcessDialog();
}
},
},
monitorSettingSave: {
@@ -361,7 +360,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
<fs-values-format modelValue={row.challengeType} dict={Dicts.challengeTypeDict} color={"auto"}></fs-values-format>
<fs-values-format modelValue={row.httpUploaderType} dict={httpUploaderTypeDict} color={"auto"}></fs-values-format>
<fs-values-format class={"ml-5"} modelValue={row.httpUploaderAccess} dict={accessDict} color={"auto"}></fs-values-format>
<a-tag class={"ml-5 flex items-center"}>{t("certd.domain.path")}: {row.httpUploadRootDir}</a-tag>
<a-tag class={"ml-5 flex items-center"}>
{t("certd.domain.path")}: {row.httpUploadRootDir}
</a-tag>
</div>
);
}
@@ -1,11 +1,10 @@
import { message } from "ant-design-vue";
import * as api from "./api";
import { useFormDialog } from "/@/use/use-dialog";
import { compute } from "@fast-crud/fast-crud";
import { Dicts } from "/@/components/plugins/lib/dicts";
import { useSettingStore } from "/@/store/settings";
import { Ref, ref } from "vue";
import * as api from "./api";
import DomainImportTaskStatus from "./import.vue";
import { useI18n } from "/@/locales";
import { useSettingStore } from "/@/store/settings";
import { useFormDialog } from "/@/use/use-dialog";
export function useDomainImport() {
const { openFormDialog } = useFormDialog();
const { t } = useI18n();
@@ -92,3 +91,92 @@ export function useDomainImportManage() {
});
};
}
export function useSyncExpirationProcess(opts: { crudExpose: any }) {
const { openFormDialog } = useFormDialog();
const { t } = useI18n();
return async function openSyncExpirationProcessDialog() {
const taskStatus: Ref<any> = ref({});
const errors: Ref<string[]> = ref([]);
const timerRef: Ref<any> = ref(null);
function stop() {
if (timerRef.value) {
clearTimeout(timerRef.value);
timerRef.value = null;
}
}
async function loadStatus() {
const status = await api.SyncExpirationStatus();
taskStatus.value = status || {};
errors.value = taskStatus.value.errors || [];
if (taskStatus.value.status === "running") {
stop();
timerRef.value = setTimeout(async () => {
await loadStatus();
}, 3000);
} else {
stop();
await opts.crudExpose.doRefresh();
}
}
await loadStatus();
await openFormDialog({
title: t("certd.domain.syncExpirationProgress"),
body: () => {
const progress = Math.min(Math.round(taskStatus.value.progress || 0), 100);
const isRunning = taskStatus.value.status === "running";
const errorList = errors.value.map(item => {
return <div>{item}</div>;
});
return (
<div class={"w-full"}>
<div class={"mt-4 flex flex-wrap gap-2"}>
<a-tag color={isRunning ? "processing" : "success"}>{isRunning ? t("certd.domain.running") : t("certd.domain.done")}</a-tag>
<a-tag class={"m-0"} color={"blue"}>
{t("certd.domain.total")}{taskStatus.value.total || 0}
</a-tag>
<a-tag class={"m-0"} color={"green"}>
{t("certd.success")}{taskStatus.value.successCount || 0}
</a-tag>
<a-tag class={"m-0"} color={"red"}>
{t("certd.domain.failed")}{taskStatus.value.errorCount || 0}
</a-tag>
<a-tag class={"m-0"} color={"cyan"}>
{t("certd.domain.current")}{taskStatus.value.current || 0}
</a-tag>
</div>
<div class={"mt-4 pr-4"}>
<a-progress percent={progress} status={errors.value.length > 0 ? "exception" : isRunning ? "active" : "success"} />
</div>
{errors.value.length > 0 && <div class={"mt-2 break-words text-red-500 mb-4"}>{errorList}</div>}
</div>
);
},
wrapper: {
width: 600,
footer: false,
buttons: {
cancel: {
show: false,
},
reset: {
show: false,
},
ok: {
show: true,
},
},
onClosed() {
stop();
},
},
});
};
}
@@ -15,6 +15,14 @@ export async function changePassword(form: any) {
});
}
export async function initPassword(form: any) {
return await request({
url: "/mine/initPassword",
method: "POST",
data: form,
});
}
export async function UpdateProfile(form: any) {
return await request({
url: "/mine/updateProfile",
@@ -23,6 +31,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",
@@ -37,20 +76,23 @@ export async function GetOauthProviders() {
});
}
export async function UnbindOauth(type: string) {
export async function UnbindOauth(type: string, subtype?: string) {
return await request({
url: "/oauth/unbind",
method: "POST",
data: { type },
data: {
type: subtype ? `${type}:${subtype}` : type,
},
});
}
export async function OauthBoundUrl(type: string) {
export async function OauthBoundUrl(type: string, subtype?: string) {
return await request({
url: "/oauth/login",
method: "POST",
data: {
type,
subtype,
forType: "bind",
},
});
@@ -9,7 +9,7 @@ import { ref } from "vue";
import { useI18n } from "/src/locales";
const { t } = useI18n();
import { CrudOptions, useColumns, useFormWrapper } from "@fast-crud/fast-crud";
import { compute, CrudOptions, useColumns, useFormWrapper } from "@fast-crud/fast-crud";
import * as api from "/@/views/certd/mine/api";
import { notification } from "ant-design-vue";
import { useUserStore } from "/@/store/user";
@@ -20,6 +20,11 @@ defineProps<{
let passwordFormRef = ref();
type OpenOptions = {
password?: string;
init?: boolean;
};
const validatePass1 = async (rule: any, value: any) => {
if (value === "") {
throw new Error(t("authentication.enterPassword"));
@@ -53,19 +58,33 @@ const passwordFormOptions: CrudOptions = {
width: "500px",
},
async doSubmit({ form }) {
await api.changePassword(form);
if (form.init) {
await api.initPassword(form);
} else {
await api.changePassword(form);
}
//
await userStore.loadUserInfo();
},
async afterSubmit() {
notification.success({ message: t("authentication.successMessage") });
const formData = passwordFormRef.value?.getFormData?.();
const message = formData?.init ? t("authentication.initPasswordSuccessMessage") : t("authentication.successMessage");
notification.success({ message });
},
},
columns: {
init: {
title: "init",
type: "text",
form: {
show: false,
},
},
password: {
title: t("authentication.oldPassword"),
type: "password",
form: {
show: compute(({ form }) => form.init !== true),
rules: [{ required: true, message: t("authentication.oldPasswordRequired") }],
},
},
@@ -97,12 +116,16 @@ const passwordFormOptions: CrudOptions = {
},
};
async function open(opts: { password: "" }) {
async function open(opts: OpenOptions = {}) {
const formOptions = buildFormOptions(passwordFormOptions);
formOptions.newInstance = true; //
if (opts.init) {
formOptions.wrapper.title = t("authentication.initPasswordTitle");
}
passwordFormRef.value = await openDialog(formOptions);
passwordFormRef.value.setFormData({
password: opts.password,
init: opts.init === true,
password: opts.password || "",
});
console.log(passwordFormRef.value);
}
@@ -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,33 +13,42 @@
<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" />
<change-password-button ref="changePasswordButtonRef" :show-button="true" />
<a-button type="primary" class="action-btn" @click="goSecuritySetting">
{{ t("authentication.securitySettingTip") }}
@@ -69,11 +78,11 @@
<a-tag v-else color="red" class="bound-tag1">未绑定</a-tag>
</span>
</div>
<a-button v-if="item.bound" type="primary" danger class="action-btn" @click="unbind(item.name)">
<a-button v-if="item.bound" type="primary" danger class="action-btn" @click="unbind(item)">
<template #icon><fs-icon icon="ion:unlink-outline" /></template>
解绑
</a-button>
<a-button v-else type="primary" class="action-btn" @click="bind(item.name)">
<a-button v-else type="primary" class="action-btn" @click="bind(item)">
<template #icon><fs-icon icon="ion:link-outline" /></template>
绑定
</a-button>
@@ -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);
},
});
}
@@ -201,7 +214,7 @@ async function loadOauthProviders() {
const computedOauthBounds = computed(() => {
const list = oauthProviders.value.map(item => {
const bound = oauthBounds.value.find(bound => bound.type === item.name);
const bound = oauthBounds.value.find(bound => bound.type === buildOauthBoundType(item));
return {
...item,
bound,
@@ -210,20 +223,24 @@ const computedOauthBounds = computed(() => {
return list;
});
async function unbind(type: string) {
function buildOauthBoundType(item: any) {
return item.subtype ? `${item.name}:${item.subtype}` : item.name;
}
async function unbind(item: any) {
Modal.confirm({
title: "确认解绑吗?",
okText: "确认",
okType: "danger",
onOk: async () => {
await api.UnbindOauth(type);
await api.UnbindOauth(item.name, item.subtype);
await loadOauthBounds();
},
});
}
async function bind(type: string) {
const res = await api.OauthBoundUrl(type);
async function bind(item: any) {
const res = await api.OauthBoundUrl(item.name, item.subtype);
const loginUrl = res.loginUrl;
window.location.href = loginUrl;
}
@@ -237,6 +254,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: "确认解绑吗?",
@@ -353,6 +387,7 @@ const checkPasskeySupport = () => {
}
};
const userStore = useUserStore();
const changePasswordButtonRef = ref();
const userAvatar = computed(() => {
if (isEmpty(userInfo.value.avatar)) {
return "";
@@ -361,11 +396,27 @@ const userAvatar = computed(() => {
return userInfo.value.avatar;
}
return `api/basic/file/download?token=${userStore.getToken}&key=${userInfo.value.avatar}`;
return `api/basic/file/download?key=${userInfo.value.avatar}`;
});
onMounted(async () => {
await getUserInfo();
userStore.setUserInfo(userInfo.value);
if (userInfo.value.needInitPassword === true) {
Modal.confirm({
title: t("authentication.initPasswordTitle"),
content: t("authentication.initPasswordWarning"),
okText: t("authentication.setNow"),
cancelText: t("authentication.notNow"),
closable: true,
onOk: () => {
changePasswordButtonRef.value.open({
init: true,
});
},
});
}
await loadContactCapability();
await loadOauthBounds();
await loadOauthProviders();
await loadPasskeys();
@@ -567,6 +618,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 +682,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 +944,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;
}
}
}

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