Compare commits

...

44 Commits

Author SHA1 Message Date
xiaojunnuo 667e7b185b fix(email-service): 优化商业版测试邮件内容中带的url链接 2026-05-16 02:54:15 +08:00
xiaojunnuo 8483ee0d41 refactor(util.cache): 优化定时清理过期缓存的定时器行为
新增定时器变量保存intervalId,并调用unref方法避免阻塞进程退出
2026-05-16 02:46:28 +08:00
xiaojunnuo c3baaf3ac7 chore: auto fix 存储标记,确保fix只运行一次 2026-05-16 02:23:21 +08:00
xiaojunnuo c63745d1ba perf: 商业版支持限制泛域名数量 2026-05-16 02:18:06 +08:00
xiaojunnuo 59b9ffadd0 fix: 固化华为云sdk版本,避免华为云调用报错 2026-05-15 23:18:51 +08:00
xiaojunnuo d131ea3790 build: release 2026-05-15 01:14:12 +08:00
xiaojunnuo b849d34be5 build: publish 2026-05-15 00:50:09 +08:00
xiaojunnuo 58fc9a551c build: trigger build image 2026-05-15 00:49:57 +08:00
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
126 changed files with 2936 additions and 581 deletions
+3 -3
View File
@@ -105,7 +105,7 @@ Certd 是一个支持私有化部署的 SSL/TLS 证书自动化管理平台。
- 前端 `pnpm dev`:启动 Vite 开发服务 - 前端 `pnpm dev`:启动 Vite 开发服务
- 前端 `pnpm build`:生产构建 - 前端 `pnpm build`:生产构建
- 前端 `pnpm tsc`类型检查 - 不要运行前端 `pnpm tsc` / `vue-tsc`:当前依赖组合中 `vue-tsc@1.8.27` 会直接抛内部错误 `Search string not found: "/supportedTSExtensions = .*(?=;)/"`,不是有效的项目类型检查结果。
- 前端暂不跑单元测试;当前 `test:unit` 只是占位脚本 - 前端暂不跑单元测试;当前 `test:unit` 只是占位脚本
## 流水线与插件模型 ## 流水线与插件模型
@@ -216,5 +216,5 @@ Get-ChildItem packages\ui\certd-client\src\views\certd
- 后端纯单元测试用例放在 `src` 目录内,并尽量与被测文件相邻,例如 `src/utils/random.test.ts`;对应 `test:unit` 只跑 `src/**/*.test.ts`,构建/打包配置应排除这些 `*.test.ts` 文件。 - 后端纯单元测试用例放在 `src` 目录内,并尽量与被测文件相邻,例如 `src/utils/random.test.ts`;对应 `test:unit` 只跑 `src/**/*.test.ts`,构建/打包配置应排除这些 `*.test.ts` 文件。
- 单元测试需要 mock ESM 静态 import 时,优先使用 `esmock`,不要为了测试把业务代码改成构造函数注入或把逻辑挪到调用方;各包 `test:unit` 脚本应显式设置 `NODE_ENV=unittest` - 单元测试需要 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` 暂时不跑单元测试。 - 单个 monorepo 包运行单元测试时,优先使用 `corepack pnpm --dir <包目录> test:unit`,例如 `corepack pnpm --dir packages\ui\certd-server test:unit``corepack pnpm --dir packages\core\basic test:unit``corepack pnpm --dir packages\plugins\plugin-lib test:unit`;也可以用包名过滤,例如 `corepack pnpm --filter @certd/ui-server test:unit`。前端 `packages\ui\certd-client` 暂时不跑单元测试。
- 前端 TS/Vue/locale 等文件改动后,优先只对本次改动文件运行项目现有自动格式化/修复;Windows/PowerShell 下 Prettier 已验证可用命令为 `packages\ui\certd-client\node_modules\.bin\prettier.cmd --write <files>`ESLint 可用命令为 `packages\ui\certd-client\node_modules\.bin\eslint.cmd --fix <files>`;不要为了格式化无关文件而扩大 diff。项目保留了 `tslint` 依赖,但当前主要使用 ESLint + Prettier。 - 前端 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。
- 优先对改动包运行聚焦的测试或类型检查;只有跨包影响明显时再考虑全 monorepo 构建。 - 优先对改动包运行聚焦的测试;后端可按包运行单元测试,前端优先使用 Prettier/ESLint 做改动文件验证。只有跨包影响明显时再考虑全 monorepo 构建。
+36
View File
@@ -3,6 +3,42 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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) ## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11)
### Bug Fixes ### Bug Fixes
+17 -12
View File
@@ -38,6 +38,7 @@ Certd® 是一个免费的全自动证书管理系统,让你的网站证书永
* **开放接口支持**: 提供RESTful API接口,方便集成到其他系统 * **开放接口支持**: 提供RESTful API接口,方便集成到其他系统
* **站点证书监控**: 定时监控网站证书的过期时间 * **站点证书监控**: 定时监控网站证书的过期时间
* **多用户管理**: 用户可以管理自己的证书流水线 * **多用户管理**: 用户可以管理自己的证书流水线
* **项目管理**: 企业级项目管理模式
* **多语言支持** 中英双语切换 * **多语言支持** 中英双语切换
* **无忧升级** 版本向下兼容 * **无忧升级** 版本向下兼容
@@ -179,19 +180,23 @@ https://certd.handfree.work/
[50元专业版优惠券限时领取](https://app.handfree.work/subject/#/app/certd/product) [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
+43
View File
@@ -3,6 +3,49 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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) ## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Bug Fixes ### Bug Fixes
+16 -10
View File
@@ -16,16 +16,22 @@
****------------------**** ****------------------****
## 专业版特权对比 ## 专业版特权对比
| 功能&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | 免费版 | 专业版 | | 功能&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | 免费版 | 专业版 | 商业版 |
|---------|---------------------------------------|--------------------------------| |---------|---------------------------------------|--------------------------------|---------------------------------|
| 证书申请 | 无限制 | 无限制 | | 证书申请 | 无限制 | 无限制 | 无限制 |
| 证书域名数量 | 无限制 | 无限制 | | 证书域名数量 | 无限制 | 无限制 | 无限制 |
| 证书流水线条数 | 无限制 | 无限制 | | 证书流水线条数 | 无限制 | 无限制 | 无限制 |
| 自动部署插件 | 阿里云CDN、腾讯云、七牛CDN、主机部署、宝塔、1Panel等大部分插件 | 群晖、威联通、proxmox等 | | 自动部署插件 | 阿里云CDN、腾讯云、七牛CDN、主机部署、宝塔、1Panel等大部分插件 | 群晖、威联通、proxmox等 | 同专业版 |
| 通知 | 邮件通知、自定义webhook | 邮件免配置、企微、钉钉、飞书、anpush、server酱等 | | 通知 | 邮件通知、自定义webhook | 邮件免配置、企微、钉钉、飞书、anpush、server酱等 | 同专业版 |
| 站点监控 | 限制1条 | 无限制 | | 站点监控 | 限制1条 | 无限制 | 无限制 |
| 批量操作 | 无 | 流水线模版,流水线复制,批量运行,批量设置通知、定时等 | | 批量操作 | 无 | 流水线模版,流水线复制,批量运行,批量设置通知、定时等 | 同专业版 |
| VIP群 | 无 | 可加,一对一技术支持,必要时可申请远程协助 | | 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) [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
@@ -9,5 +9,5 @@
} }
}, },
"npmClient": "pnpm", "npmClient": "pnpm",
"version": "1.39.14" "version": "1.40.0"
} }
+14
View File
@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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) ## [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 **Note:** Version bump only for package @certd/acme-client
+3 -3
View File
@@ -3,7 +3,7 @@
"description": "Simple and unopinionated ACME client", "description": "Simple and unopinionated ACME client",
"private": false, "private": false,
"author": "nmorsman", "author": "nmorsman",
"version": "1.39.14", "version": "1.40.0",
"type": "module", "type": "module",
"module": "./dist/index.js", "module": "./dist/index.js",
"main": "./dist/index.js", "main": "./dist/index.js",
@@ -18,7 +18,7 @@
"types" "types"
], ],
"dependencies": { "dependencies": {
"@certd/basic": "^1.39.14", "@certd/basic": "^1.40.0",
"@peculiar/x509": "^1.11.0", "@peculiar/x509": "^1.11.0",
"asn1js": "^3.0.5", "asn1js": "^3.0.5",
"axios": "^1.9.0", "axios": "^1.9.0",
@@ -76,5 +76,5 @@
"bugs": { "bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues" "url": "https://github.com/publishlab/node-acme-client/issues"
}, },
"gitHead": "9f7d766cb386b299d4098141f4a47d23e16975e3" "gitHead": "5801f34b3a40cbbd591805e401613b397bec9775"
} }
+2
View File
@@ -167,6 +167,7 @@ export default async (client, userOpts) => {
await client.completeChallenge(challenge); await client.completeChallenge(challenge);
}catch (e) { }catch (e) {
await deactivateAuth(e); await deactivateAuth(e);
e.message = `[${d}] ${e.message || "completeChallenge error"}`;
throw e; throw e;
} }
challengeCompleted = true; challengeCompleted = true;
@@ -178,6 +179,7 @@ export default async (client, userOpts) => {
} catch (e) { } catch (e) {
log(`[auto] [${d}] challengeCreateFn threw error: ${e.message || e}`); log(`[auto] [${d}] challengeCreateFn threw error: ${e.message || e}`);
await deactivateAuth(e); await deactivateAuth(e);
e.message = `[${d}] ${e.message || "challengeCreateFn error"}`;
throw e; throw e;
} }
+14
View File
@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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) ## [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 **Note:** Version bump only for package @certd/basic
+1 -1
View File
@@ -1 +1 @@
00:06 00:44
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "@certd/basic", "name": "@certd/basic",
"private": false, "private": false,
"version": "1.39.14", "version": "1.40.0",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
@@ -52,5 +52,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "9f7d766cb386b299d4098141f4a47d23e16975e3" "gitHead": "5801f34b3a40cbbd591805e401613b397bec9775"
} }
+2 -1
View File
@@ -11,9 +11,10 @@ export class LocalCache<V = any> {
cache: Map<string, { value: V; expiresAt: number }>; cache: Map<string, { value: V; expiresAt: number }>;
constructor(opts: { clearInterval?: number } = {}) { constructor(opts: { clearInterval?: number } = {}) {
this.cache = new Map(); this.cache = new Map();
setInterval(() => { const intervalId = setInterval(() => {
this.clearExpires(); this.clearExpires();
}, opts.clearInterval ?? 5 * 60 * 1000); }, opts.clearInterval ?? 5 * 60 * 1000);
intervalId.unref?.();
} }
get(key: string): V | undefined { get(key: string): V | undefined {
@@ -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",
});
});
});
@@ -82,6 +82,7 @@ export class HttpError extends Error {
export const HttpCommonError = HttpError; export const HttpCommonError = HttpError;
let defaultAgents = createAgent(); let defaultAgents = createAgent();
let defaultHeaders: Record<string, string> = {};
export function setGlobalProxy(opts: { httpProxy?: string; httpsProxy?: string }) { export function setGlobalProxy(opts: { httpProxy?: string; httpsProxy?: string }) {
logger.info("setGlobalProxy:", opts); logger.info("setGlobalProxy:", opts);
@@ -92,6 +93,15 @@ export function getGlobalAgents() {
return defaultAgents; return defaultAgents;
} }
export function setGlobalHeaders(headers: Record<string, string> = {}) {
logger.info("setGlobalHeaders:", Object.keys(headers));
defaultHeaders = { ...headers };
}
export function getGlobalHeaders() {
return defaultHeaders;
}
/** /**
* @description * @description
*/ */
@@ -148,6 +158,12 @@ export function createAxiosService({ logger }: { logger: ILogger }) {
config.httpsAgent = agents.httpsAgent; config.httpsAgent = agents.httpsAgent;
config.httpAgent = agents.httpAgent; 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({ // const agent = new https.Agent({
// rejectUnauthorized: false // 允许自签名证书 // rejectUnauthorized: false // 允许自签名证书
// }); // });
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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) ## [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 **Note:** Version bump only for package @certd/pipeline
+4 -4
View File
@@ -1,7 +1,7 @@
{ {
"name": "@certd/pipeline", "name": "@certd/pipeline",
"private": false, "private": false,
"version": "1.39.14", "version": "1.40.0",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
@@ -19,8 +19,8 @@
"compile": "tsc --skipLibCheck --watch" "compile": "tsc --skipLibCheck --watch"
}, },
"dependencies": { "dependencies": {
"@certd/basic": "^1.39.14", "@certd/basic": "^1.40.0",
"@certd/plus-core": "^1.39.14", "@certd/plus-core": "^1.40.0",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"reflect-metadata": "^0.1.13" "reflect-metadata": "^0.1.13"
@@ -49,5 +49,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "9f7d766cb386b299d4098141f4a47d23e16975e3" "gitHead": "5801f34b3a40cbbd591805e401613b397bec9775"
} }
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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) ## [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 **Note:** Version bump only for package @certd/lib-huawei
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "@certd/lib-huawei", "name": "@certd/lib-huawei",
"private": false, "private": false,
"version": "1.39.14", "version": "1.40.0",
"main": "./dist/bundle.js", "main": "./dist/bundle.js",
"module": "./dist/bundle.js", "module": "./dist/bundle.js",
"types": "./dist/d/index.d.ts", "types": "./dist/d/index.d.ts",
@@ -27,5 +27,5 @@
"prettier": "^2.8.8", "prettier": "^2.8.8",
"tslib": "^2.8.1" "tslib": "^2.8.1"
}, },
"gitHead": "9f7d766cb386b299d4098141f4a47d23e16975e3" "gitHead": "5801f34b3a40cbbd591805e401613b397bec9775"
} }
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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) ## [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 **Note:** Version bump only for package @certd/lib-iframe
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "@certd/lib-iframe", "name": "@certd/lib-iframe",
"private": false, "private": false,
"version": "1.39.14", "version": "1.40.0",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
@@ -34,5 +34,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "9f7d766cb386b299d4098141f4a47d23e16975e3" "gitHead": "5801f34b3a40cbbd591805e401613b397bec9775"
} }
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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) ## [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 **Note:** Version bump only for package @certd/jdcloud
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@certd/jdcloud", "name": "@certd/jdcloud",
"version": "1.39.14", "version": "1.40.0",
"description": "jdcloud openApi sdk", "description": "jdcloud openApi sdk",
"main": "./dist/bundle.js", "main": "./dist/bundle.js",
"module": "./dist/bundle.js", "module": "./dist/bundle.js",
@@ -58,5 +58,5 @@
"fetch" "fetch"
] ]
}, },
"gitHead": "9f7d766cb386b299d4098141f4a47d23e16975e3" "gitHead": "5801f34b3a40cbbd591805e401613b397bec9775"
} }
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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) ## [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 **Note:** Version bump only for package @certd/lib-k8s
+3 -3
View File
@@ -1,7 +1,7 @@
{ {
"name": "@certd/lib-k8s", "name": "@certd/lib-k8s",
"private": false, "private": false,
"version": "1.39.14", "version": "1.40.0",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
@@ -19,7 +19,7 @@
"compile": "tsc --skipLibCheck --watch" "compile": "tsc --skipLibCheck --watch"
}, },
"dependencies": { "dependencies": {
"@certd/basic": "^1.39.14", "@certd/basic": "^1.40.0",
"@kubernetes/client-node": "0.21.0" "@kubernetes/client-node": "0.21.0"
}, },
"devDependencies": { "devDependencies": {
@@ -36,5 +36,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "9f7d766cb386b299d4098141f4a47d23e16975e3" "gitHead": "5801f34b3a40cbbd591805e401613b397bec9775"
} }
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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) ## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11)
### Bug Fixes ### Bug Fixes
+7 -7
View File
@@ -1,6 +1,6 @@
{ {
"name": "@certd/lib-server", "name": "@certd/lib-server",
"version": "1.39.14", "version": "1.40.0",
"description": "midway with flyway, sql upgrade way ", "description": "midway with flyway, sql upgrade way ",
"private": false, "private": false,
"type": "module", "type": "module",
@@ -29,11 +29,11 @@
], ],
"license": "AGPL", "license": "AGPL",
"dependencies": { "dependencies": {
"@certd/acme-client": "^1.39.14", "@certd/acme-client": "^1.40.0",
"@certd/basic": "^1.39.14", "@certd/basic": "^1.40.0",
"@certd/pipeline": "^1.39.14", "@certd/pipeline": "^1.40.0",
"@certd/plugin-lib": "^1.39.14", "@certd/plugin-lib": "^1.40.0",
"@certd/plus-core": "^1.39.14", "@certd/plus-core": "^1.40.0",
"@midwayjs/cache": "3.14.0", "@midwayjs/cache": "3.14.0",
"@midwayjs/core": "3.20.11", "@midwayjs/core": "3.20.11",
"@midwayjs/i18n": "3.20.13", "@midwayjs/i18n": "3.20.13",
@@ -69,5 +69,5 @@
"typeorm": "^0.3.11", "typeorm": "^0.3.11",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "9f7d766cb386b299d4098141f4a47d23e16975e3" "gitHead": "5801f34b3a40cbbd591805e401613b397bec9775"
} }
@@ -64,6 +64,7 @@ export class SysPublicSettings extends BaseSettings {
type: string; type: string;
title: string; title: string;
addonId: number; addonId: number;
icon?: string;
}> = {}; }> = {};
notice?: string; notice?: string;
@@ -80,6 +81,7 @@ export class SysPrivateSettings extends BaseSettings {
httpsProxy? = ''; httpsProxy? = '';
httpProxy? = ''; httpProxy? = '';
commonHeaders?: string = '';
reverseProxies?: Record<string, string> = {}; reverseProxies?: Record<string, string> = {};
@@ -251,6 +253,14 @@ export class SysSuiteSetting extends BaseSettings {
intro?: string; intro?: string;
} }
export class SysAutoFixSetting extends BaseSettings {
static __title__ = '自动修复记录';
static __key__ = 'sys.auto.fix';
static __access__ = 'private';
fixed: Record<string, boolean> = {};
}
export type SiteHidden = { export type SiteHidden = {
enabled: boolean; enabled: boolean;
@@ -5,7 +5,7 @@ import { SysSettingsEntity } from '../entity/sys-settings.js';
import { BaseSettings, SysInstallInfo, SysPrivateSettings, SysPublicSettings, SysSecret, SysSecretBackup } from './models.js'; import { BaseSettings, SysInstallInfo, SysPrivateSettings, SysPublicSettings, SysSecret, SysSecretBackup } from './models.js';
import { getAllSslProviderDomains, setSslProviderReverseProxies, setWalkFromAuthoritative } from '@certd/acme-client'; 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 { isPlus } from '@certd/plus-core';
import * as dns from 'node:dns'; import * as dns from 'node:dns';
import { BaseService, setAdminMode } from '../../../basic/index.js'; import { BaseService, setAdminMode } from '../../../basic/index.js';
@@ -167,6 +167,7 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
httpsProxy: privateSetting.httpsProxy, httpsProxy: privateSetting.httpsProxy,
}; };
setGlobalProxy(opts); setGlobalProxy(opts);
setGlobalHeaders(this.parseKeyValueText(privateSetting.commonHeaders));
if (privateSetting.dnsResultOrder) { if (privateSetting.dnsResultOrder) {
dns.setDefaultResultOrder(privateSetting.dnsResultOrder as any); dns.setDefaultResultOrder(privateSetting.dnsResultOrder as any);
@@ -185,12 +186,12 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
} }
setEnvironmentVars(vars: string) { parseKeyValueText(text: string) {
const envVars = {} const values = {};
if (typeof vars !== 'string') { if (typeof text !== 'string') {
vars = "" text = "";
} }
vars.split('\n').forEach(line => { text.split('\n').forEach(line => {
line = line.trim(); line = line.trim();
if (!line || line.startsWith('#')) { if (!line || line.startsWith('#')) {
return return
@@ -204,11 +205,18 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
return 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) { if (key && value) {
envVars[key.trim()] = value.trim(); values[key] = value;
} }
}); });
return values;
}
setEnvironmentVars(vars: string) {
const envVars = this.parseKeyValueText(vars);
//先删除旧环境变量 //先删除旧环境变量
if (lastSaveEnvVars) { if (lastSaveEnvVars) {
for (const key in lastSaveEnvVars) { for (const key in lastSaveEnvVars) {
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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) ## [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 **Note:** Version bump only for package @certd/midway-flyway-js
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@certd/midway-flyway-js", "name": "@certd/midway-flyway-js",
"version": "1.39.14", "version": "1.40.0",
"description": "midway with flyway, sql upgrade way ", "description": "midway with flyway, sql upgrade way ",
"private": false, "private": false,
"type": "module", "type": "module",
@@ -49,5 +49,5 @@
"typeorm": "^0.3.11", "typeorm": "^0.3.11",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "9f7d766cb386b299d4098141f4a47d23e16975e3" "gitHead": "5801f34b3a40cbbd591805e401613b397bec9775"
} }
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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) ## [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 **Note:** Version bump only for package @certd/plugin-cert
+6 -6
View File
@@ -1,7 +1,7 @@
{ {
"name": "@certd/plugin-cert", "name": "@certd/plugin-cert",
"private": false, "private": false,
"version": "1.39.14", "version": "1.40.0",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -18,10 +18,10 @@
"compile": "tsc --skipLibCheck --watch" "compile": "tsc --skipLibCheck --watch"
}, },
"dependencies": { "dependencies": {
"@certd/acme-client": "^1.39.14", "@certd/acme-client": "^1.40.0",
"@certd/basic": "^1.39.14", "@certd/basic": "^1.40.0",
"@certd/pipeline": "^1.39.14", "@certd/pipeline": "^1.40.0",
"@certd/plugin-lib": "^1.39.14", "@certd/plugin-lib": "^1.40.0",
"psl": "^1.9.0", "psl": "^1.9.0",
"punycode.js": "^2.3.1" "punycode.js": "^2.3.1"
}, },
@@ -41,5 +41,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "9f7d766cb386b299d4098141f4a47d23e16975e3" "gitHead": "5801f34b3a40cbbd591805e401613b397bec9775"
} }
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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) ## [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 **Note:** Version bump only for package @certd/plugin-lib
+6 -6
View File
@@ -1,7 +1,7 @@
{ {
"name": "@certd/plugin-lib", "name": "@certd/plugin-lib",
"private": false, "private": false,
"version": "1.39.14", "version": "1.40.0",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -23,10 +23,10 @@
"@alicloud/pop-core": "^1.7.10", "@alicloud/pop-core": "^1.7.10",
"@alicloud/tea-util": "^1.4.11", "@alicloud/tea-util": "^1.4.11",
"@aws-sdk/client-s3": "^3.964.0", "@aws-sdk/client-s3": "^3.964.0",
"@certd/acme-client": "^1.39.14", "@certd/acme-client": "^1.40.0",
"@certd/basic": "^1.39.14", "@certd/basic": "^1.40.0",
"@certd/pipeline": "^1.39.14", "@certd/pipeline": "^1.40.0",
"@certd/plus-core": "^1.39.14", "@certd/plus-core": "^1.40.0",
"@kubernetes/client-node": "0.21.0", "@kubernetes/client-node": "0.21.0",
"ali-oss": "^6.22.0", "ali-oss": "^6.22.0",
"basic-ftp": "^5.0.5", "basic-ftp": "^5.0.5",
@@ -61,5 +61,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "9f7d766cb386b299d4098141f4a47d23e16975e3" "gitHead": "5801f34b3a40cbbd591805e401613b397bec9775"
} }
+28
View File
@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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) ## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11)
### Bug Fixes ### Bug Fixes
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@certd/ui-client", "name": "@certd/ui-client",
"version": "1.39.14", "version": "1.40.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite --open", "dev": "vite --open",
@@ -106,8 +106,8 @@
"zod-defaults": "^0.1.3" "zod-defaults": "^0.1.3"
}, },
"devDependencies": { "devDependencies": {
"@certd/lib-iframe": "^1.39.14", "@certd/lib-iframe": "^1.40.0",
"@certd/pipeline": "^1.39.14", "@certd/pipeline": "^1.40.0",
"@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12", "@types/chai": "^4.3.12",
@@ -78,8 +78,13 @@ export default {
passkeyRegisterFailed: "Passkey registration failed", passkeyRegisterFailed: "Passkey registration failed",
title: "Change Password", title: "Change Password",
weakPasswordWarning: "For your account security, please change your password immediately", 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", changeNow: "Change Now",
setNow: "Set Now",
notNow: "Not Now",
successMessage: "Changed successfully", successMessage: "Changed successfully",
initPasswordSuccessMessage: "Set successfully",
oldPassword: "Old Password", oldPassword: "Old Password",
oldPasswordRequired: "Please enter the old password", oldPasswordRequired: "Please enter the old password",
newPassword: "New Password", newPassword: "New Password",
@@ -6,6 +6,9 @@ export default {
specifications: "Specifications", specifications: "Specifications",
pipeline: "Pipeline", pipeline: "Pipeline",
domain: "Domain", domain: "Domain",
totalDomain: "Total Domain Count",
wildcardDomain: "Wildcard Domain",
includedWildcardDomain: "Included Wildcard Domain Count",
deployTimes: "Deployments", deployTimes: "Deployments",
monitorCount: "DomainMonitors", monitorCount: "DomainMonitors",
duration: "Duration", duration: "Duration",
@@ -24,7 +27,8 @@ export default {
please_select_package: "Please select a package", please_select_package: "Please select a package",
package: "Package", package: "Package",
addon_package: "Addon Package", addon_package: "Addon Package",
domain_count: "Domain Count", domain_count: "Total Domain Count",
wildcard_domain_count: "Wildcard Domain Count",
unit_count: "pcs", unit_count: "pcs",
pipeline_count: "Pipeline Count", pipeline_count: "Pipeline Count",
unit_item: "items", unit_item: "items",
@@ -42,6 +46,7 @@ export default {
intro: "Introduction", intro: "Introduction",
packageContent: "Package Content", packageContent: "Package Content",
maxDomainCount: "Max Domain Count", maxDomainCount: "Max Domain Count",
maxWildcardDomainCount: "Max Wildcard Domain Count",
maxPipelineCount: "Max Pipeline Count", maxPipelineCount: "Max Pipeline Count",
maxDeployCount: "Max Deploy Count", maxDeployCount: "Max Deploy Count",
maxMonitorCount: "Max Monitor Count", maxMonitorCount: "Max Monitor Count",
@@ -51,6 +56,10 @@ export default {
addon: "Addon", addon: "Addon",
typeHelper: "Suite: Only the most recently purchased one is active at a time\nAddon: Multiple can be purchased, effective immediately without affecting the suite\nThe quantities of suite and addon can be accumulated", typeHelper: "Suite: Only the most recently purchased one is active at a time\nAddon: Multiple can be purchased, effective immediately without affecting the suite\nThe quantities of suite and addon can be accumulated",
pipelineCount: "Pipeline Count", pipelineCount: "Pipeline Count",
wildcardDomainCount: "Wildcard Domain Count",
wildcardDomainCountPart: "Included Wildcard Domain Count",
wildcardDomainCountSub: "- Wildcard Domain Count",
wildcardDomainCountHelper: "Wildcard domains are also limited by the total domain count: each wildcard domain consumes both domain count and wildcard domain count quota.",
unitPipeline: "pipelines", unitPipeline: "pipelines",
deployCount: "Deployment Count", deployCount: "Deployment Count",
unitDeploy: "times", unitDeploy: "times",
@@ -91,6 +91,8 @@ export default {
reverseProxyEmpty: "No reverse proxy list configured", reverseProxyEmpty: "No reverse proxy list configured",
environmentVars: "Environment Variables", environmentVars: "Environment Variables",
environmentVarsHelper: "configure the runtime environment variables, one per line, format: KEY=VALUE", 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", bindUrl: "Bind URL",
bindUrlHelper: "Bind URL, used as your site URL in notifications", bindUrlHelper: "Bind URL, used as your site URL in notifications",
@@ -79,8 +79,13 @@ export default {
title: "修改密码", title: "修改密码",
weakPasswordWarning: "为了您的账户安全,请立即修改密码", weakPasswordWarning: "为了您的账户安全,请立即修改密码",
initPasswordWarning: "当前账号还未设置登录密码,请先设置密码",
initPasswordTitle: "设置密码",
changeNow: "立即修改", changeNow: "立即修改",
setNow: "立即设置",
notNow: "暂不设置",
successMessage: "修改成功", successMessage: "修改成功",
initPasswordSuccessMessage: "设置成功",
oldPassword: "旧密码", oldPassword: "旧密码",
oldPasswordRequired: "请输入旧密码", oldPasswordRequired: "请输入旧密码",
newPassword: "新密码", newPassword: "新密码",
@@ -6,6 +6,9 @@ export default {
specifications: "规格", specifications: "规格",
pipeline: "流水线", pipeline: "流水线",
domain: "域名", domain: "域名",
totalDomain: "域名总数量",
wildcardDomain: "泛域名",
includedWildcardDomain: "其中泛域名数量",
deployTimes: "部署次数", deployTimes: "部署次数",
monitorCount: "域名监控数", monitorCount: "域名监控数",
duration: "时长", duration: "时长",
@@ -24,7 +27,8 @@ export default {
please_select_package: "请选择套餐", please_select_package: "请选择套餐",
package: "套餐", package: "套餐",
addon_package: "加量包", addon_package: "加量包",
domain_count: "域名数量", domain_count: "域名数量",
wildcard_domain_count: "泛域名数量",
unit_count: "个", unit_count: "个",
pipeline_count: "流水线数量", pipeline_count: "流水线数量",
unit_item: "条", unit_item: "条",
@@ -42,6 +46,7 @@ export default {
intro: "介绍", intro: "介绍",
packageContent: "套餐内容", packageContent: "套餐内容",
maxDomainCount: "最大域名数", maxDomainCount: "最大域名数",
maxWildcardDomainCount: "最大泛域名数",
maxPipelineCount: "最大流水线数", maxPipelineCount: "最大流水线数",
maxDeployCount: "最大部署数", maxDeployCount: "最大部署数",
maxMonitorCount: "最大监控数", maxMonitorCount: "最大监控数",
@@ -51,6 +56,10 @@ export default {
addon: "加量包", addon: "加量包",
typeHelper: "套餐:同一时间只有最新购买的一个生效\n加量包:可购买多个,购买后立即生效,不影响套餐\n套餐和加量包数量可叠加", typeHelper: "套餐:同一时间只有最新购买的一个生效\n加量包:可购买多个,购买后立即生效,不影响套餐\n套餐和加量包数量可叠加",
pipelineCount: "流水线数量", pipelineCount: "流水线数量",
wildcardDomainCount: "泛域名数量",
wildcardDomainCountPart: "其中泛域名数量",
wildcardDomainCountSub: "- 泛域名数量",
wildcardDomainCountHelper: "泛域名数量受域名总数量限制:泛域名会同时占用域名总数量和泛域名数量额度;注意:如果域名总数有限制,泛域名数量不要设置为无限制。",
unitPipeline: "条", unitPipeline: "条",
deployCount: "部署次数", deployCount: "部署次数",
unitDeploy: "次", unitDeploy: "次",
@@ -89,6 +89,8 @@ export default {
reverseProxyEmpty: "未配置反向代理", reverseProxyEmpty: "未配置反向代理",
environmentVars: "环境变量", environmentVars: "环境变量",
environmentVarsHelper: "配置运行时环境变量,每行一个,格式:KEY=VALUE", environmentVarsHelper: "配置运行时环境变量,每行一个,格式:KEY=VALUE",
commonHeaders: "公共请求头",
commonHeadersHelper: "服务端发起 HTTP 请求时自动附加的公共请求头,每行一个,格式:KEY=VALUE;请求中已设置同名 Header 时不会覆盖\n注意: 不要将token等敏感内容放在此处,仅限个人和公司内部使用,商业版不要设置",
bindUrl: "绑定URL", bindUrl: "绑定URL",
bindUrlHelper: "绑定URL,在各类通知中显示你的站点URL", bindUrlHelper: "绑定URL,在各类通知中显示你的站点URL",
}, },
@@ -85,6 +85,7 @@ export type SysPublicSetting = {
type: string; type: string;
title: string; title: string;
addonId: number; addonId: number;
icon?: string;
} }
>; >;
// 系统通知 // 系统通知
@@ -99,6 +100,7 @@ export type SuiteSetting = {
export type SysPrivateSetting = { export type SysPrivateSetting = {
httpProxy?: string; httpProxy?: string;
httpsProxy?: string; httpsProxy?: string;
commonHeaders?: string;
reverseProxies?: any; reverseProxies?: any;
dnsResultOrder?: string; dnsResultOrder?: string;
commonCnameEnabled?: boolean; commonCnameEnabled?: boolean;
@@ -38,6 +38,7 @@ export interface UserInfoRes {
avatar?: string; avatar?: string;
roleIds: number[]; roleIds: number[];
isWeak?: boolean; isWeak?: boolean;
needInitPassword?: boolean;
validTime?: number; validTime?: number;
status?: number; status?: number;
} }
@@ -38,6 +38,9 @@ export const useUserStore = defineStore({
getToken(): string { getToken(): string {
return this.token || LocalStorage.get(TOKEN_KEY); return this.token || LocalStorage.get(TOKEN_KEY);
}, },
isLogined(): boolean {
return !!this.getToken;
},
isAdmin(): boolean { isAdmin(): boolean {
return this.getUserInfo.roleIds?.includes(1) || this.getUserInfo.id === 1; return this.getUserInfo.roleIds?.includes(1) || this.getUserInfo.id === 1;
}, },
@@ -13,8 +13,9 @@ export function useReference(formItem: any) {
const ctx = { const ctx = {
compute: (opts: any) => { compute: (opts: any) => {
const func = (context: any) => { const func = (context: any) => {
debugger;
let form = context.form || {}; let form = context.form || {};
form = form.input || form.body || form.access || form; form = form.input || form.body || form; // form.access去掉,历史原因,access的mergeScript会处理form.access
return opts({ return opts({
...context, ...context,
form, form,
@@ -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) { export async function UpdateProfile(form: any) {
return await request({ return await request({
url: "/mine/updateProfile", url: "/mine/updateProfile",
@@ -68,20 +76,23 @@ export async function GetOauthProviders() {
}); });
} }
export async function UnbindOauth(type: string) { export async function UnbindOauth(type: string, subtype?: string) {
return await request({ return await request({
url: "/oauth/unbind", url: "/oauth/unbind",
method: "POST", 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({ return await request({
url: "/oauth/login", url: "/oauth/login",
method: "POST", method: "POST",
data: { data: {
type, type,
subtype,
forType: "bind", forType: "bind",
}, },
}); });
@@ -9,7 +9,7 @@ import { ref } from "vue";
import { useI18n } from "/src/locales"; import { useI18n } from "/src/locales";
const { t } = useI18n(); 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 * as api from "/@/views/certd/mine/api";
import { notification } from "ant-design-vue"; import { notification } from "ant-design-vue";
import { useUserStore } from "/@/store/user"; import { useUserStore } from "/@/store/user";
@@ -20,6 +20,11 @@ defineProps<{
let passwordFormRef = ref(); let passwordFormRef = ref();
type OpenOptions = {
password?: string;
init?: boolean;
};
const validatePass1 = async (rule: any, value: any) => { const validatePass1 = async (rule: any, value: any) => {
if (value === "") { if (value === "") {
throw new Error(t("authentication.enterPassword")); throw new Error(t("authentication.enterPassword"));
@@ -53,19 +58,33 @@ const passwordFormOptions: CrudOptions = {
width: "500px", width: "500px",
}, },
async doSubmit({ form }) { async doSubmit({ form }) {
await api.changePassword(form); if (form.init) {
await api.initPassword(form);
} else {
await api.changePassword(form);
}
// //
await userStore.loadUserInfo(); await userStore.loadUserInfo();
}, },
async afterSubmit() { 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: { columns: {
init: {
title: "init",
type: "text",
form: {
show: false,
},
},
password: { password: {
title: t("authentication.oldPassword"), title: t("authentication.oldPassword"),
type: "password", type: "password",
form: { form: {
show: compute(({ form }) => form.init !== true),
rules: [{ required: true, message: t("authentication.oldPasswordRequired") }], 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); const formOptions = buildFormOptions(passwordFormOptions);
formOptions.newInstance = true; // formOptions.newInstance = true; //
if (opts.init) {
formOptions.wrapper.title = t("authentication.initPasswordTitle");
}
passwordFormRef.value = await openDialog(formOptions); passwordFormRef.value = await openDialog(formOptions);
passwordFormRef.value.setFormData({ passwordFormRef.value.setFormData({
password: opts.password, init: opts.init === true,
password: opts.password || "",
}); });
console.log(passwordFormRef.value); console.log(passwordFormRef.value);
} }
@@ -48,7 +48,7 @@
</div> </div>
</div> </div>
<div class="action-buttons gap-2"> <div class="action-buttons gap-2">
<change-password-button :show-button="true" /> <change-password-button ref="changePasswordButtonRef" :show-button="true" />
<a-button type="primary" class="action-btn" @click="goSecuritySetting"> <a-button type="primary" class="action-btn" @click="goSecuritySetting">
{{ t("authentication.securitySettingTip") }} {{ t("authentication.securitySettingTip") }}
@@ -78,11 +78,11 @@
<a-tag v-else color="red" class="bound-tag1">未绑定</a-tag> <a-tag v-else color="red" class="bound-tag1">未绑定</a-tag>
</span> </span>
</div> </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> <template #icon><fs-icon icon="ion:unlink-outline" /></template>
解绑 解绑
</a-button> </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> <template #icon><fs-icon icon="ion:link-outline" /></template>
绑定 绑定
</a-button> </a-button>
@@ -214,7 +214,7 @@ async function loadOauthProviders() {
const computedOauthBounds = computed(() => { const computedOauthBounds = computed(() => {
const list = oauthProviders.value.map(item => { 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 { return {
...item, ...item,
bound, bound,
@@ -223,20 +223,24 @@ const computedOauthBounds = computed(() => {
return list; 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({ Modal.confirm({
title: "确认解绑吗?", title: "确认解绑吗?",
okText: "确认", okText: "确认",
okType: "danger", okType: "danger",
onOk: async () => { onOk: async () => {
await api.UnbindOauth(type); await api.UnbindOauth(item.name, item.subtype);
await loadOauthBounds(); await loadOauthBounds();
}, },
}); });
} }
async function bind(type: string) { async function bind(item: any) {
const res = await api.OauthBoundUrl(type); const res = await api.OauthBoundUrl(item.name, item.subtype);
const loginUrl = res.loginUrl; const loginUrl = res.loginUrl;
window.location.href = loginUrl; window.location.href = loginUrl;
} }
@@ -383,6 +387,7 @@ const checkPasskeySupport = () => {
} }
}; };
const userStore = useUserStore(); const userStore = useUserStore();
const changePasswordButtonRef = ref();
const userAvatar = computed(() => { const userAvatar = computed(() => {
if (isEmpty(userInfo.value.avatar)) { if (isEmpty(userInfo.value.avatar)) {
return ""; return "";
@@ -391,11 +396,26 @@ const userAvatar = computed(() => {
return userInfo.value.avatar; 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 () => { onMounted(async () => {
await getUserInfo(); 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 loadContactCapability();
await loadOauthBounds(); await loadOauthBounds();
await loadOauthProviders(); await loadOauthProviders();
@@ -320,6 +320,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
title: t("monitor.siteName"), title: t("monitor.siteName"),
search: { search: {
show: true, show: true,
col: {
span: 3,
},
}, },
type: "text", type: "text",
form: { form: {
@@ -333,6 +336,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
title: t("monitor.domain"), title: t("monitor.domain"),
search: { search: {
show: true, show: true,
col: {
span: 3,
},
}, },
type: "text", type: "text",
form: { form: {
@@ -411,6 +417,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
title: t("monitor.certDomains"), title: t("monitor.certDomains"),
search: { search: {
show: true, show: true,
col: {
span: 3,
},
}, },
type: "text", type: "text",
form: { form: {
@@ -451,6 +460,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
title: t("monitor.certStatus"), title: t("monitor.certStatus"),
search: { search: {
show: true, show: true,
col: {
span: 2,
},
}, },
type: "dict-select", type: "dict-select",
dict: dict({ dict: dict({
@@ -472,7 +484,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
checkStatus: { checkStatus: {
title: t("monitor.checkStatus"), title: t("monitor.checkStatus"),
search: { search: {
show: false, show: true,
col: {
span: 2,
},
}, },
type: "dict-select", type: "dict-select",
dict: checkStatusDict, dict: checkStatusDict,
@@ -578,6 +593,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
type: "dict-select", type: "dict-select",
search: { search: {
show: true, show: true,
col: {
span: 3,
},
}, },
dict: groupDictRef, dict: groupDictRef,
form: { form: {
@@ -631,7 +649,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
disabled: { disabled: {
title: t("monitor.disabled"), title: t("monitor.disabled"),
search: { search: {
show: false, show: true,
col: {
span: 2,
},
}, },
type: "dict-switch", type: "dict-switch",
dict: dict({ dict: dict({
@@ -14,6 +14,7 @@ export type SuiteDetail = {
expiresTime?: number; expiresTime?: number;
pipelineCount?: SuiteValue; pipelineCount?: SuiteValue;
domainCount?: SuiteValue; domainCount?: SuiteValue;
wildcardDomainCount?: SuiteValue;
deployCount?: SuiteValue; deployCount?: SuiteValue;
monitorCount?: SuiteValue; monitorCount?: SuiteValue;
}; };
@@ -147,7 +147,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
}, },
}, },
"content.maxDomainCount": { "content.maxDomainCount": {
title: "域名数量", title: "域名数量",
type: "text", type: "text",
form: { form: {
key: ["content", "maxDomainCount"], key: ["content", "maxDomainCount"],
@@ -168,6 +168,28 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
align: "center", align: "center",
}, },
}, },
"content.maxWildcardDomainCount": {
title: "其中泛域名数量",
type: "text",
form: {
key: ["content", "maxWildcardDomainCount"],
component: {
name: SuiteValueEdit,
vModel: "modelValue",
unit: "个",
},
rules: [{ required: true, message: "此项必填" }],
},
column: {
width: 120,
component: {
name: SuiteValue,
vModel: "modelValue",
unit: "个",
},
align: "center",
},
},
"content.maxPipelineCount": { "content.maxPipelineCount": {
title: "流水线数量", title: "流水线数量",
type: "text", type: "text",
@@ -11,7 +11,10 @@
<span class="label">{{ $t("certd.order.specifications") }}</span> <span class="label">{{ $t("certd.order.specifications") }}</span>
<span class="flex-o flex-wrap"> <span class="flex-o flex-wrap">
<span class="flex-o"> {{ $t("certd.order.pipeline") }}<suite-value class="ml-5" :model-value="product.content.maxPipelineCount" :unit="$t('certd.order.unit.pieces')" /> </span> <span class="flex-o"> {{ $t("certd.order.pipeline") }}<suite-value class="ml-5" :model-value="product.content.maxPipelineCount" :unit="$t('certd.order.unit.pieces')" /> </span>
<span class="flex-o"> {{ $t("certd.order.domain") }}<suite-value class="ml-5" :model-value="product.content.maxDomainCount" :unit="$t('certd.order.unit.count')" /> </span> <span class="flex-o"> {{ $t("certd.order.totalDomain") }}<suite-value class="ml-5" :model-value="product.content.maxDomainCount" :unit="$t('certd.order.unit.count')" /> </span>
<span class="flex-o" style="padding-left: 2em">
- {{ $t("certd.order.includedWildcardDomain") }}<suite-value class="ml-5" :model-value="product.content.maxWildcardDomainCount" :unit="$t('certd.order.unit.count')" />
</span>
<span class="flex-o"> {{ $t("certd.order.deployTimes") }}<suite-value class="ml-5" :model-value="product.content.maxDeployCount" :unit="$t('certd.order.unit.times')" /> </span> <span class="flex-o"> {{ $t("certd.order.deployTimes") }}<suite-value class="ml-5" :model-value="product.content.maxDeployCount" :unit="$t('certd.order.unit.times')" /> </span>
<span class="flex-o"> {{ $t("certd.order.monitorCount") }}<suite-value class="ml-5" :model-value="product.content.maxMonitorCount" :unit="$t('certd.order.unit.times')" /> </span> <span class="flex-o"> {{ $t("certd.order.monitorCount") }}<suite-value class="ml-5" :model-value="product.content.maxMonitorCount" :unit="$t('certd.order.unit.times')" /> </span>
</span> </span>
@@ -1,7 +1,7 @@
<template> <template>
<a-card :title="product.title" class="product-card"> <a-card :title="product.title" class="product-card">
<template #extra> <template #extra>
<fs-values-format v-model="product.type" :dict="productTypeDictRef"></fs-values-format> <fs-values-format :model-value="product.type" :dict="productTypeDictRef"></fs-values-format>
</template> </template>
<div class="product-intro">{{ product.intro || "暂无介绍" }}</div> <div class="product-intro">{{ product.intro || "暂无介绍" }}</div>
@@ -12,9 +12,13 @@
<suite-value :model-value="product.content.maxPipelineCount" unit="条" /> <suite-value :model-value="product.content.maxPipelineCount" unit="条" />
</div> </div>
<div class="flex-between mt-5"> <div class="flex-between mt-5">
<div class="flex-o"><fs-icon icon="ant-design:check-outlined" class="color-green mr-5" />域名数量</div> <div class="flex-o"><fs-icon icon="ant-design:check-outlined" class="color-green mr-5" />域名数量</div>
<suite-value :model-value="product.content.maxDomainCount" unit="个" /> <suite-value :model-value="product.content.maxDomainCount" unit="个" />
</div> </div>
<div class="flex-between mt-5">
<div class="flex-o" style="padding-left: 2em">- 其中泛域名数量</div>
<suite-value :model-value="product.content.maxWildcardDomainCount" unit="个" />
</div>
<div class="flex-between mt-5"> <div class="flex-between mt-5">
<div class="flex-o"> <div class="flex-o">
<fs-icon icon="ant-design:check-outlined" class="color-green mr-5" /> <fs-icon icon="ant-design:check-outlined" class="color-green mr-5" />
@@ -20,10 +20,14 @@
<div class="flex-between mt-5"> <div class="flex-between mt-5">
<div class="flex-o"> <div class="flex-o">
<fs-icon icon="ant-design:check-outlined" class="color-green mr-5" /> <fs-icon icon="ant-design:check-outlined" class="color-green mr-5" />
域名数量 域名数量
</div> </div>
<suite-value :model-value="detail.domainCount.max" :used="detail.domainCount.used" unit="个" /> <suite-value :model-value="detail.domainCount.max" :used="detail.domainCount.used" unit="个" />
</div> </div>
<div class="flex-between mt-5">
<div class="flex-o ml-20">- 其中泛域名数量</div>
<suite-value :model-value="detail.wildcardDomainCount.max" :used="detail.wildcardDomainCount.used" unit="个" />
</div>
<div class="flex-between mt-5"> <div class="flex-between mt-5">
<div class="flex-o"> <div class="flex-o">
<fs-icon icon="ant-design:check-outlined" class="color-green mr-5" /> <fs-icon icon="ant-design:check-outlined" class="color-green mr-5" />
@@ -2,7 +2,7 @@ import { request } from "/src/api/service";
const apiPrefix = "/oauth"; const apiPrefix = "/oauth";
export async function OauthLogin(type: string, forType?: string, from?: string) { export async function OauthLogin(type: string, forType?: string, from?: string, subtype?: string) {
return await request({ return await request({
url: apiPrefix + `/login`, url: apiPrefix + `/login`,
method: "post", method: "post",
@@ -10,6 +10,7 @@ export async function OauthLogin(type: string, forType?: string, from?: string)
type, type,
forType: forType || "login", forType: forType || "login",
from: from || "web", from: from || "web",
subtype,
}, },
}); });
} }
@@ -9,8 +9,9 @@
<div>第三方{{ oauthType }}登录成功您还未绑定账号请选择</div> <div>第三方{{ oauthType }}登录成功您还未绑定账号请选择</div>
<div class="mt-10"> <div class="mt-10">
<a-button class="w-full mt-10" type="primary" @click="goBindUser">绑定已有账号</a-button> <a-button v-if="!userStore.isLogined" class="w-full mt-10" type="primary" @click="goBindUser">绑定已有账号</a-button>
<a-button v-if="settingStore.sysPublic.registerEnabled" class="w-full mt-10" type="primary" @click="autoRegister">创建新账号</a-button> <a-button v-else class="w-full mt-10" type="primary" @click="doBindCurrent">绑定当前登录账号({{ userStore.getUserInfo.username }} - {{ userStore.getUserInfo.nickName }})</a-button>
<a-button v-if="settingStore.sysPublic.registerEnabled" class="w-full mt-10" type="primary" @click="autoRegister">创建新账号绑定</a-button>
</div> </div>
<div class="w-full mt-10"> <div class="w-full mt-10">
@@ -48,7 +49,7 @@ async function handleOauthToken() {
// //
userStore.onLoginSuccess(res); userStore.onLoginSuccess(res);
// //
router.replace("/"); router.replace("/index");
return; return;
} }
if (res.bindRequired) { if (res.bindRequired) {
@@ -63,6 +64,15 @@ async function handleOauthToken() {
} }
} }
async function doBindCurrent() {
await api.BindUser(validationCode);
notification.success({
message: "绑定成功",
});
//
router.replace("/certd/mine/user-profile");
}
onMounted(async () => { onMounted(async () => {
if (error.value) { if (error.value) {
return; return;
@@ -70,12 +80,7 @@ onMounted(async () => {
if (forType === "bind") { if (forType === "bind") {
// //
await api.BindUser(validationCode); await doBindCurrent();
notification.success({
message: "绑定成功",
});
//
router.replace("/certd/mine/user-profile");
return; return;
} }
@@ -98,7 +103,7 @@ async function autoRegister() {
// //
userStore.onLoginSuccess(res); userStore.onLoginSuccess(res);
// //
router.replace("/"); router.replace("/index");
} }
</script> </script>
<style lang="less"> <style lang="less">
@@ -5,8 +5,8 @@
</div> </div>
<div class="flex justify-center items-center gap-4 flex-wrap md:flex-nowrap"> <div class="flex justify-center items-center gap-4 flex-wrap md:flex-nowrap">
<passkey-login></passkey-login> <passkey-login></passkey-login>
<template v-for="item in oauthProviderList" :key="item.type"> <template v-for="item in oauthProviderList" :key="buildProviderKey(item)">
<div v-if="item.addonId" class="oauth-icon-button pointer" @click="goOauthLogin(item.name)"> <div v-if="item.addonId" class="oauth-icon-button pointer" @click="goOauthLogin(item)">
<div><fs-icon :icon="item.icon" class="text-blue-600 text-40" /></div> <div><fs-icon :icon="item.icon" class="text-blue-600 text-40" /></div>
<div class="ellipsis title" :title="item.addonTitle || item.title">{{ item.addonTitle || item.title }}</div> <div class="ellipsis title" :title="item.addonTitle || item.title">{{ item.addonTitle || item.title }}</div>
</div> </div>
@@ -22,7 +22,17 @@ import { useSettingStore } from "/@/store/settings";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import PasskeyLogin from "../login/passkey-login.vue"; import PasskeyLogin from "../login/passkey-login.vue";
const oauthProviderList = ref([]); type OauthProviderItem = {
name: string;
type?: string;
subtype?: string;
title: string;
addonTitle?: string;
icon: string;
addonId?: number;
};
const oauthProviderList = ref<OauthProviderItem[]>([]);
const props = defineProps<{ const props = defineProps<{
oauthOnly?: boolean; oauthOnly?: boolean;
}>(); }>();
@@ -42,15 +52,19 @@ onMounted(async () => {
if (settingStore.sysPublic.oauthAutoRedirect && queryOauthOnly !== "false") { if (settingStore.sysPublic.oauthAutoRedirect && queryOauthOnly !== "false") {
const firstOauth = oauthProviderList.value.find(item => item.addonId > 0); const firstOauth = oauthProviderList.value.find(item => item.addonId > 0);
if (firstOauth) { if (firstOauth) {
goOauthLogin(firstOauth.name); goOauthLogin(firstOauth);
} }
} }
}); });
async function goOauthLogin(type: string) { function buildProviderKey(item: OauthProviderItem) {
return `${item.name}:${item.subtype || ""}`;
}
async function goOauthLogin(item: OauthProviderItem) {
//URL //URL
const from = "web"; const from = "web";
const res = await api.OauthLogin(type, from); const res = await api.OauthLogin(item.name, "login", from, item.subtype);
const loginUrl = res.loginUrl; const loginUrl = res.loginUrl;
window.location.href = loginUrl; window.location.href = loginUrl;
} }
@@ -114,7 +114,7 @@ export async function GetSmsTypeDefine(type: string) {
export async function GetOauthProviders() { export async function GetOauthProviders() {
return await request({ return await request({
url: "/oauth/providers", url: apiPrefix + "/oauth/providers",
method: "post", method: "post",
}); });
} }
@@ -19,6 +19,11 @@
<div class="helper">{{ t("certd.sys.setting.environmentVarsHelper") }}</div> <div class="helper">{{ t("certd.sys.setting.environmentVarsHelper") }}</div>
</a-form-item> </a-form-item>
<a-form-item :label="t('certd.sys.setting.commonHeaders')" :name="['private', 'commonHeaders']">
<a-textarea v-model:value="formState.private.commonHeaders" :placeholder="commonHeadersExample" rows="4" />
<div class="helper">{{ t("certd.sys.setting.commonHeadersHelper") }}</div>
</a-form-item>
<a-form-item :label="t('certd.dualStackNetwork')" :name="['private', 'dnsResultOrder']"> <a-form-item :label="t('certd.dualStackNetwork')" :name="['private', 'dnsResultOrder']">
<a-select v-model:value="formState.private.dnsResultOrder"> <a-select v-model:value="formState.private.dnsResultOrder">
<a-select-option value="verbatim">{{ t("certd.default") }}</a-select-option> <a-select-option value="verbatim">{{ t("certd.default") }}</a-select-option>
@@ -64,6 +69,10 @@ const environmentVarsExample = ref(
`ALIYUN_CLIENT_CONNECT_TIMEOUT=16000 #连接超时,单位毫秒 `ALIYUN_CLIENT_CONNECT_TIMEOUT=16000 #连接超时,单位毫秒
ALIYUN_CLIENT_READ_TIMEOUT=16000 #读取数据超时单位毫秒` ALIYUN_CLIENT_READ_TIMEOUT=16000 #读取数据超时单位毫秒`
); );
const commonHeadersExample = ref(
`User-Agent=certd
X-Custom-Header=value`
);
const formState = reactive<Partial<SysSettings>>({ const formState = reactive<Partial<SysSettings>>({
public: {}, public: {},
@@ -109,6 +109,7 @@ const formState = reactive<Partial<SysSettings>>({
const oauthProviders = ref([]); const oauthProviders = ref([]);
async function loadOauthProviders() { async function loadOauthProviders() {
oauthProviders.value = await api.GetOauthProviders(); oauthProviders.value = await api.GetOauthProviders();
mergeOauthProviderSettings();
} }
const bindDomain = computed(() => { const bindDomain = computed(() => {
@@ -164,6 +165,16 @@ const onFinish = async (form: any) => {
function buildCallbackUrl(type: string) { function buildCallbackUrl(type: string) {
return `${window.location.origin}/api/oauth/callback/${type}`; return `${window.location.origin}/api/oauth/callback/${type}`;
} }
function mergeOauthProviderSettings() {
const savedProviders = formState.public?.oauthProviders || {};
for (const item of oauthProviders.value) {
const saved = savedProviders[item.name];
if (saved) {
item.addonId = saved.addonId;
}
}
}
</script> </script>
<style lang="less"> <style lang="less">
.sys-settings-oauth { .sys-settings-oauth {
@@ -59,7 +59,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
}, },
content: { content: {
header: t("certd.packageContent"), header: t("certd.packageContent"),
columns: ["content.maxDomainCount", "content.maxPipelineCount", "content.maxDeployCount", "content.maxMonitorCount"], columns: ["content.maxDomainCount", "content.maxWildcardDomainCount", "content.maxPipelineCount", "content.maxDeployCount", "content.maxMonitorCount"],
}, },
price: { price: {
header: t("certd.price"), header: t("certd.price"),
@@ -154,6 +154,28 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
}, },
}, },
}, },
"content.maxWildcardDomainCount": {
title: t("certd.wildcardDomainCountPart"),
type: "text",
form: {
key: ["content", "maxWildcardDomainCount"],
helper: t("certd.wildcardDomainCountHelper"),
component: {
name: SuiteValueEdit,
vModel: "modelValue",
unit: t("certd.unitCount"),
},
rules: [{ required: true, message: t("certd.requiredField") }],
},
column: {
width: 120,
component: {
name: SuiteValue,
vModel: "modelValue",
unit: t("certd.unitCount"),
},
},
},
"content.maxPipelineCount": { "content.maxPipelineCount": {
title: t("certd.pipelineCount"), title: t("certd.pipelineCount"),
type: "text", type: "text",
@@ -17,6 +17,7 @@
<div style="height: 400px"> <div style="height: 400px">
<ProductManager @refreshed="onTableRefresh"></ProductManager> <ProductManager @refreshed="onTableRefresh"></ProductManager>
</div> </div>
<div class="helper">泛域名数量受域名总数量限制泛域名会同时占用域名总数量和泛域名数量额度</div>
<div class="helper">不建议设置免费套餐可以在下方配置注册赠送套餐或者在用户套餐管理中手动赠送套餐</div> <div class="helper">不建议设置免费套餐可以在下方配置注册赠送套餐或者在用户套餐管理中手动赠送套餐</div>
</a-form-item> </a-form-item>
@@ -223,6 +223,29 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
align: "center", align: "center",
}, },
}, },
"content.maxWildcardDomainCount": {
title: t("certd.wildcardDomainCountPart"),
type: "text",
form: {
show: false,
key: ["content", "maxWildcardDomainCount"],
component: {
name: SuiteValueEdit,
vModel: "modelValue",
unit: t("certd.unit_count"),
},
rules: [{ required: true, message: t("certd.field_required") }],
},
column: {
width: 120,
component: {
name: SuiteValue,
vModel: "modelValue",
unit: t("certd.unit_count"),
},
align: "center",
},
},
"content.maxPipelineCount": { "content.maxPipelineCount": {
title: t("certd.pipeline_count"), title: t("certd.pipeline_count"),
type: "text", type: "text",
+31
View File
@@ -3,6 +3,37 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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
* 修复群晖授权没有显示设备id输入框的bug ([2f172b5](https://github.com/certd/certd/commit/2f172b56e9411303ca15138d827bdb9bafdae4d1))
* 修复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 @certd/ui-server
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
### Bug Fixes
* 修复第三方登录彩虹登录不上的bug ([bae4f8e](https://github.com/certd/certd/commit/bae4f8e3209d9f9869ecbd7c01655383bac2fe21))
### Performance Improvements
* icon选择器增加一套logo集 ([fdd5848](https://github.com/certd/certd/commit/fdd5848df4055a6ee07dc5eabaaf6b718672882d))
## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11) ## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11)
### Bug Fixes ### Bug Fixes
@@ -0,0 +1 @@
ALTER TABLE cd_cert_info ADD COLUMN wildcard_domain_count integer NOT NULL DEFAULT 0;
@@ -39,6 +39,8 @@ input:
component: component:
name: a-switch name: a-switch
vModel: checked vModel: checked
col:
span: 24
helper: 是否启用了双重认证 helper: 是否启用了双重认证
required: true required: true
deviceId: deviceId:
@@ -48,6 +50,8 @@ input:
name: synology-device-id-getter name: synology-device-id-getter
type: access type: access
typeName: synology typeName: synology
col:
span: 24
mergeScript: |2- mergeScript: |2-
return { return {
@@ -14,43 +14,54 @@ input:
loginType: loginType:
title: 登录类型 title: 登录类型
component: component:
name: a-auto-complete name: a-select
vModel: value
mode: tags
multiple: true
options: options:
- label: QQ - label: QQ
value: qq value: qq
icon: cib:tencent-qq:#007AFF
- label: 微信 - label: 微信
value: wx value: wx
icon: simple-icons:wechat:#34C759
- label: 支付宝 - label: 支付宝
value: alipay value: alipay
icon: simple-icons:alipay:#0099ff
- label: 微博 - label: 微博
value: sina value: sina
icon: uiw:weibo:#FF3B30
- label: 百度 - label: 百度
value: baidu value: baidu
icon: simple-icons:baidu:#007AFF
- label: 华为 - label: 华为
value: huawei value: huawei
icon: simple-icons:huawei:#ff0000
- label: 小米 - label: 小米
value: xiaomi value: xiaomi
icon: simple-icons:xiaomi:#FF9500
- label: 谷歌 - label: 谷歌
value: google value: google
icon: flat-color-icons:google
- label: 微软 - label: 微软
value: microsoft value: microsoft
icon: logos:microsoft-icon
- label: Facebook - label: Facebook
value: facebook value: facebook
icon: logos:facebook
- label: Twitter - label: Twitter
value: twitter value: twitter
icon: logos:twitter
- label: 钉钉 - label: 钉钉
value: dingtalk value: dingtalk
icon: ant-design:dingding-outlined:#007AFF
- label: Gitee - label: Gitee
value: gitee value: gitee
icon: simple-icons:gitee:#c71d23
- label: Github - label: Github
value: github value: github
icon: logos:github-icon
required: true required: true
icon:
title: 自定义图标
component:
name: fs-icon-selector
vModel: modelValue
required: false
appId: appId:
title: AppId title: AppId
helper: 彩虹聚合登录->应用列表->创建应用 获取 helper: 彩虹聚合登录->应用列表->创建应用 获取
@@ -10,6 +10,18 @@ input:
component: component:
name: fs-icon-selector name: fs-icon-selector
vModel: modelValue vModel: modelValue
iconSets:
- streamline-logos
- logos
- fa-brands
- fa-solid
- fa-regular
- carbon
- ion
- ant-design
- mdi
- twemoji
- svg-spinners
required: false required: false
clientId: clientId:
title: ClientId title: ClientId
+18 -18
View File
@@ -1,6 +1,6 @@
{ {
"name": "@certd/ui-server", "name": "@certd/ui-server",
"version": "1.39.14", "version": "1.40.0",
"description": "fast-server base midway", "description": "fast-server base midway",
"private": true, "private": true,
"type": "module", "type": "module",
@@ -53,26 +53,26 @@
"@aws-sdk/client-sts": "^3.990.0", "@aws-sdk/client-sts": "^3.990.0",
"@azure/arm-dns": "^5.1.0", "@azure/arm-dns": "^5.1.0",
"@azure/identity": "^4.13.1", "@azure/identity": "^4.13.1",
"@certd/acme-client": "^1.39.14", "@certd/acme-client": "^1.40.0",
"@certd/basic": "^1.39.14", "@certd/basic": "^1.40.0",
"@certd/commercial-core": "^1.39.14", "@certd/commercial-core": "^1.40.0",
"@certd/cv4pve-api-javascript": "^8.4.2", "@certd/cv4pve-api-javascript": "^8.4.2",
"@certd/jdcloud": "^1.39.14", "@certd/jdcloud": "^1.40.0",
"@certd/lib-huawei": "^1.39.14", "@certd/lib-huawei": "^1.40.0",
"@certd/lib-k8s": "^1.39.14", "@certd/lib-k8s": "^1.40.0",
"@certd/lib-server": "^1.39.14", "@certd/lib-server": "^1.40.0",
"@certd/midway-flyway-js": "^1.39.14", "@certd/midway-flyway-js": "^1.40.0",
"@certd/pipeline": "^1.39.14", "@certd/pipeline": "^1.40.0",
"@certd/plugin-cert": "^1.39.14", "@certd/plugin-cert": "^1.40.0",
"@certd/plugin-lib": "^1.39.14", "@certd/plugin-lib": "^1.40.0",
"@certd/plugin-plus": "^1.39.14", "@certd/plugin-plus": "^1.40.0",
"@certd/plus-core": "^1.39.14", "@certd/plus-core": "^1.40.0",
"@google-cloud/dns": "^5.3.1", "@google-cloud/dns": "^5.3.1",
"@google-cloud/publicca": "^1.3.0", "@google-cloud/publicca": "^1.3.0",
"@huaweicloud/huaweicloud-sdk-cdn": "^3.1.185", "@huaweicloud/huaweicloud-sdk-cdn": "3.1.185",
"@huaweicloud/huaweicloud-sdk-core": "^3.1.185", "@huaweicloud/huaweicloud-sdk-core": "3.1.185",
"@huaweicloud/huaweicloud-sdk-elb": "^3.1.185", "@huaweicloud/huaweicloud-sdk-elb": "3.1.185",
"@huaweicloud/huaweicloud-sdk-iam": "^3.1.185", "@huaweicloud/huaweicloud-sdk-iam": "3.1.185",
"@koa/cors": "^5.0.0", "@koa/cors": "^5.0.0",
"@midwayjs/bootstrap": "3.20.11", "@midwayjs/bootstrap": "3.20.11",
"@midwayjs/cache": "3.14.0", "@midwayjs/cache": "3.14.0",
@@ -0,0 +1,6 @@
export function shouldSetDefaultNoCache(path: string, cacheControl?: string) {
if (cacheControl) {
return false;
}
return path === '/' || path === '/index.html' || path.startsWith('/api');
}
@@ -0,0 +1,22 @@
/// <reference types="mocha" />
/// <reference types="node" />
import assert from "node:assert/strict";
import { shouldSetDefaultNoCache } from "./configuration-cache.js";
describe("shouldSetDefaultNoCache", () => {
it("sets default no-cache for html and api responses without cache headers", () => {
assert.equal(shouldSetDefaultNoCache("/"), true);
assert.equal(shouldSetDefaultNoCache("/index.html"), true);
assert.equal(shouldSetDefaultNoCache("/api/basic/file/download"), true);
});
it("keeps explicit cache headers from file responses", () => {
assert.equal(shouldSetDefaultNoCache("/api/basic/file/download", "public,max-age=259200"), false);
});
it("ignores non-html and non-api paths", () => {
assert.equal(shouldSetDefaultNoCache("/static/images/logo.svg"), false);
});
});
@@ -20,6 +20,7 @@ import * as commercial from '@certd/commercial-core';
import * as upload from '@midwayjs/upload'; import * as upload from '@midwayjs/upload';
import { setLogger } from '@certd/acme-client'; import { setLogger } from '@certd/acme-client';
import {HiddenMiddleware} from "./middleware/hidden.js"; import {HiddenMiddleware} from "./middleware/hidden.js";
import { shouldSetDefaultNoCache } from './configuration-cache.js';
// import * as swagger from '@midwayjs/swagger'; // import * as swagger from '@midwayjs/swagger';
//@ts-ignore //@ts-ignore
// process.env.UV_THREADPOOL_SIZE = 2 // process.env.UV_THREADPOOL_SIZE = 2
@@ -123,7 +124,7 @@ export class MainConfiguration {
this.app.getMiddleware().insertFirst(async (ctx: IMidwayKoaContext, next: NextFunction) => { this.app.getMiddleware().insertFirst(async (ctx: IMidwayKoaContext, next: NextFunction) => {
await next(); await next();
if (ctx.path === '/' || ctx.path === '/index.html' || ctx.path.startsWith("/api")) { if (shouldSetDefaultNoCache(ctx.path, ctx.response.get('Cache-Control'))) {
ctx.response.set('Cache-Control', 'public,max-age=0'); ctx.response.set('Cache-Control', 'public,max-age=0');
} }
}); });
@@ -0,0 +1,39 @@
/// <reference types="mocha" />
/// <reference types="node" />
import assert from "node:assert/strict";
import { getImageDownloadOptions, isImageFile } from "./file-controller.js";
describe("FileController.isImageFile", () => {
it("detects uploaded logo image files", () => {
assert.equal(isImageFile("data/upload/public/user/logo.PNG"), true);
assert.equal(isImageFile("data/upload/public/user/logo.svg"), true);
assert.equal(isImageFile("data/upload/public/user/logo.webp"), true);
});
it("does not treat non-image downloads as logo images", () => {
assert.equal(isImageFile("data/upload/public/user/archive.zip"), false);
assert.equal(isImageFile("data/upload/public/user/cert.pem"), false);
assert.equal(isImageFile("data/upload/public/user/logo"), false);
});
it("builds koa-send options that keep image cache headers at 3 days", () => {
const options = getImageDownloadOptions("data/upload/public/user/logo.png");
assert.equal(options?.maxage, 259200000);
const headers: Record<string, string> = {};
options?.setHeaders({
setHeader(key: string, value: string) {
headers[key] = value;
},
});
assert.equal(headers["Cache-Control"], "public,max-age=259200");
});
it("does not build cache options for non-image files", () => {
assert.equal(getImageDownloadOptions("data/upload/private/user/cert.pem"), undefined);
});
});
@@ -5,6 +5,25 @@ import { nanoid } from 'nanoid';
import { cache } from '@certd/basic'; import { cache } from '@certd/basic';
import { UploadFileInfo } from '@midwayjs/upload'; import { UploadFileInfo } from '@midwayjs/upload';
const imageExtSet = new Set(['.apng', '.avif', '.bmp', '.gif', '.ico', '.jpeg', '.jpg', '.png', '.svg', '.webp']);
const imageCacheSeconds = 3 * 24 * 60 * 60;
export function isImageFile(filePath: string) {
return imageExtSet.has(filePath.substring(filePath.lastIndexOf('.')).toLowerCase());
}
export function getImageDownloadOptions(filePath: string) {
if (!isImageFile(filePath)) {
return undefined;
}
return {
maxage: imageCacheSeconds * 1000,
setHeaders(res: any) {
res.setHeader('Cache-Control', `public,max-age=${imageCacheSeconds}`);
},
};
}
/** /**
*/ */
@Provide() @Provide()
@@ -40,8 +59,10 @@ export class FileController extends BaseController {
userId = this.getUserId(); userId = this.getUserId();
} }
const filePath = this.fileService.getFile(key, userId); const filePath = this.fileService.getFile(key, userId);
this.ctx.response.attachment(filePath); const sendOptions = getImageDownloadOptions(filePath);
this.ctx.response.set('Cache-Control', 'public,max-age=2592000'); if (!sendOptions) {
await send(this.ctx, filePath); this.ctx.response.attachment(filePath);
}
await send(this.ctx, filePath, sendOptions);
} }
} }
@@ -11,6 +11,24 @@ import { UserEntity } from "../../../modules/sys/authority/entity/user.js";
import { UserService } from "../../../modules/sys/authority/service/user-service.js"; import { UserService } from "../../../modules/sys/authority/service/user-service.js";
import { IOauthProvider } from "../../../plugins/plugin-oauth/api.js"; import { IOauthProvider } from "../../../plugins/plugin-oauth/api.js";
type OauthProviderSetting = {
type: string;
title: string;
icon?: string;
addonId: number;
types?: OauthProviderType[];
};
type OauthProviderType = {
type: string;
name: string;
icon?: string;
};
function getOauthBoundType(type: string, subtype?: string) {
return subtype ? `${type}:${subtype}` : type;
}
/** /**
*/ */
@Provide() @Provide()
@@ -41,7 +59,7 @@ export class ConnectController extends BaseController {
if (!publicSettings?.oauthEnabled) { if (!publicSettings?.oauthEnabled) {
throw new Error("OAuth功能未启用"); throw new Error("OAuth功能未启用");
} }
const setting = publicSettings?.oauthProviders?.[type || ""] const setting = publicSettings?.oauthProviders?.[type || ""] as OauthProviderSetting | undefined;
if (!setting) { if (!setting) {
throw new Error(`未配置该OAuth类型:${type}`); throw new Error(`未配置该OAuth类型:${type}`);
} }
@@ -50,19 +68,38 @@ export class ConnectController extends BaseController {
if (!addon) { if (!addon) {
throw new Error("初始化OAuth插件失败"); throw new Error("初始化OAuth插件失败");
} }
return addon as IOauthProvider; return {
addon: addon as IOauthProvider,
setting,
};
} }
@Post('/login', { description: Constants.per.guest }) @Post('/login', { description: Constants.per.guest })
public async login(@Body(ALL) body: { type: string, forType?:string ,from?:string }) { public async login(@Body(ALL) body: { type: string, subtype?: string, forType?:string ,from?:string }) {
const addon = await this.getOauthProvider(body.type); const oauthProvider = await this.getOauthProvider(body.type);
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo); const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
const bindUrl = installInfo?.bindUrl || ""; const bindUrl = installInfo?.bindUrl || "";
//构造登录url //构造登录url
const redirectUrl = `${bindUrl}api/oauth/callback/${body.type}`; const redirectUrl = `${bindUrl}api/oauth/callback/${body.type}`;
const { loginUrl, ticketValue } = await addon.buildLoginUrl({ redirectUri: redirectUrl, forType: body.forType ,from: body.from || "web" });
const ticket = this.codeService.setValidationValue(ticketValue) let stateObj = {
forType: body.forType || 'login',
}
const state = utils.hash.base64(JSON.stringify(stateObj))
const { loginUrl, ticketValue } = await oauthProvider.addon.buildLoginUrl({
redirectUri: redirectUrl,
forType: body.forType,
from: body.from || "web",
subtype: body.subtype,
state,
});
const ticket = this.codeService.setValidationValue({
...ticketValue,
state,
subtype: body.subtype,
})
this.ctx.cookies.set("oauth_ticket", ticket, { this.ctx.cookies.set("oauth_ticket", ticket, {
httpOnly: true, httpOnly: true,
// secure: true, // secure: true,
@@ -78,7 +115,7 @@ export class ConnectController extends BaseController {
checkPlus() checkPlus()
//处理登录回调 //处理登录回调
const addon = await this.getOauthProvider(type); const oauthProvider = await this.getOauthProvider(type);
const request = this.ctx.request; const request = this.ctx.request;
// const ticketValue = this.codeService.getValidationValue(ticket); // const ticketValue = this.codeService.getValidationValue(ticket);
// if (!ticketValue) { // if (!ticketValue) {
@@ -98,7 +135,7 @@ export class ConnectController extends BaseController {
const bindUrl = installInfo?.bindUrl || ""; const bindUrl = installInfo?.bindUrl || "";
const currentUrl = `${bindUrl}api/oauth/callback/${type}?${request.querystring}` const currentUrl = `${bindUrl}api/oauth/callback/${type}?${request.querystring}`
try { try {
const tokenRes = await addon.onCallback({ const tokenRes = await oauthProvider.addon.onCallback({
code: query.code, code: query.code,
state: query.state, state: query.state,
ticketValue, ticketValue,
@@ -108,11 +145,14 @@ export class ConnectController extends BaseController {
const userInfo = tokenRes.userInfo; const userInfo = tokenRes.userInfo;
const validationCode = await this.codeService.setValidationValue({ const validationCode = await this.codeService.setValidationValue({
type, type: getOauthBoundType(type, ticketValue.subtype),
userInfo, userInfo,
}); });
const state = JSON.parse(utils.hash.base64Decode(query.state)); let state = {forType:""}
if (query.state) {
state = JSON.parse(utils.hash.base64Decode(query.state));
}
const redirectUrl = `${bindUrl}#/oauth/callback/${type}?validationCode=${validationCode}&forType=${state.forType}`; const redirectUrl = `${bindUrl}#/oauth/callback/${type}?validationCode=${validationCode}&forType=${state.forType}`;
this.ctx.redirect(redirectUrl); this.ctx.redirect(redirectUrl);
@@ -126,8 +166,10 @@ export class ConnectController extends BaseController {
@Post('/getLogoutUrl', { description: Constants.per.guest }) @Post('/getLogoutUrl', { description: Constants.per.guest })
public async logout(@Body(ALL) body: any) { public async logout(@Body(ALL) body: any) {
checkPlus() checkPlus()
const addon = await this.getOauthProvider(body.type); const oauthProvider = await this.getOauthProvider(body.type);
const { logoutUrl } = await addon.buildLogoutUrl(body); const { logoutUrl } = await oauthProvider.addon.buildLogoutUrl({
...body,
});
return this.ok({ logoutUrl }); return this.ok({ logoutUrl });
} }
@@ -141,7 +183,7 @@ export class ConnectController extends BaseController {
} }
const type = validationValue.type; const type = validationValue.type;
if (type !== body.type) { if (type !== body.type && !type.startsWith(`${body.type}:`)) {
throw new Error("校验码错误"); throw new Error("校验码错误");
} }
const userInfo = validationValue.userInfo; const userInfo = validationValue.userInfo;
@@ -259,16 +301,32 @@ export class ConnectController extends BaseController {
provider.addonId = conf.addonId; provider.addonId = conf.addonId;
provider.addonTitle = addonEntity.name; provider.addonTitle = addonEntity.name;
const addon = await this.addonGetterService.getAddonById(conf.addonId,true,0,null); const addon = await this.addonGetterService.getAddonById(conf.addonId,true,0,null) as IOauthProvider & { icon?: string; types?: OauthProviderType[] };
const {logoutUrl} = await addon.buildLogoutUrl(); const {logoutUrl} = await addon.buildLogoutUrl({});
if (logoutUrl){ if (logoutUrl){
provider.logoutUrl = logoutUrl; provider.logoutUrl = logoutUrl;
} }
if(addon.icon){ if(addon.icon){
provider.icon = addon.icon; provider.icon = addon.icon;
} }
if(addon.types?.length){
provider.types = addon.types;
}
} }
} }
if (provider.addonId && provider.types?.length) {
for (const subtype of provider.types) {
list.push({
...provider,
name: type,
subtype: subtype.type,
title: subtype.name,
icon: subtype.icon || provider.icon,
addonTitle: subtype.name,
});
}
continue;
}
list.push(provider); list.push(provider);
} }
@@ -135,6 +135,7 @@ export class SysSettingsController extends CrudController<SysSettingsService> {
await this.service.savePrivateSettings(privateSettings); await this.service.savePrivateSettings(privateSettings);
return this.ok({}); return this.ok({});
} }
@Post('/stopOtherUserTimer', { description: 'sys:settings:edit' }) @Post('/stopOtherUserTimer', { description: 'sys:settings:edit' })
async stopOtherUserTimer(@Body(ALL) body) { async stopOtherUserTimer(@Body(ALL) body) {
await this.pipelineService.stopOtherUserPipeline(1); await this.pipelineService.stopOtherUserPipeline(1);
@@ -1,16 +1,16 @@
import { BaseController, Constants, SysSettingsService } from '@certd/lib-server'; import { BaseController, Constants, SysSettingsService } from "@certd/lib-server";
import { ALL, Body, Controller, Inject, Post, Provide } from '@midwayjs/core'; import { ALL, Body, Controller, Inject, Post, Provide } from "@midwayjs/core";
import { PasskeyService } from '../../../modules/login/service/passkey-service.js'; import { PasskeyService } from "../../../modules/login/service/passkey-service.js";
import { RoleService } from '../../../modules/sys/authority/service/role-service.js'; import { RoleService } from "../../../modules/sys/authority/service/role-service.js";
import { UserService } from '../../../modules/sys/authority/service/user-service.js'; import { UserService } from "../../../modules/sys/authority/service/user-service.js";
import { ApiTags } from '@midwayjs/swagger'; import { ApiTags } from "@midwayjs/swagger";
import { CodeService } from '../../../modules/basic/service/code-service.js'; import { CodeService } from "../../../modules/basic/service/code-service.js";
/** /**
*/ */
@Provide() @Provide()
@Controller('/api/mine') @Controller("/api/mine")
@ApiTags(['mine']) @ApiTags(["mine"])
export class MineController extends BaseController { export class MineController extends BaseController {
@Inject() @Inject()
userService: UserService; userService: UserService;
@@ -27,28 +27,38 @@ export class MineController extends BaseController {
@Inject() @Inject()
sysSettingsService: SysSettingsService; sysSettingsService: SysSettingsService;
@Post('/info', { description: Constants.per.authOnly, summary: '查询用户信息' }) @Post("/info", { description: Constants.per.authOnly, summary: "查询用户信息" })
public async info() { public async info() {
const userId = this.getUserId(); const userId = this.getUserId();
const user = await this.userService.info(userId); const user = await this.userService.info(userId);
const isWeak = await this.userService.checkPassword('123456', user.password, user.passwordVersion); const isWeak = await this.userService.checkPassword("123456", user.password, user.passwordVersion);
if (isWeak) { if (isWeak) {
//@ts-ignore //@ts-ignore
user.isWeak = true; user.isWeak = true;
} }
const needInitPassword = user.password === "changeme";
user.roleIds = await this.roleService.getRoleIdsByUserId(userId); user.roleIds = await this.roleService.getRoleIdsByUserId(userId);
delete user.password; delete user.password;
//@ts-ignore
user.needInitPassword = needInitPassword;
return this.ok(user); return this.ok(user);
} }
@Post('/changePassword', { description: Constants.per.authOnly, summary: '修改密码' }) @Post("/changePassword", { description: Constants.per.authOnly, summary: "修改密码" })
public async changePassword(@Body(ALL) body: any) { public async changePassword(@Body(ALL) body: any) {
const userId = this.getUserId(); const userId = this.getUserId();
await this.userService.changePassword(userId, body); await this.userService.changePassword(userId, body);
return this.ok({}); return this.ok({});
} }
@Post('/updateProfile', { description: Constants.per.authOnly, summary: '更新用户资料' }) @Post("/initPassword", { description: Constants.per.authOnly, summary: "初始化密码" })
public async initPassword(@Body(ALL) body: any) {
const userId = this.getUserId();
await this.userService.initPassword(userId, body);
return this.ok({});
}
@Post("/updateProfile", { description: Constants.per.authOnly, summary: "更新用户资料" })
public async updateProfile(@Body(ALL) body: any) { public async updateProfile(@Body(ALL) body: any) {
const userId = this.getUserId(); const userId = this.getUserId();
@@ -59,7 +69,7 @@ export class MineController extends BaseController {
return this.ok({}); return this.ok({});
} }
@Post('/contact/capability', { description: Constants.per.authOnly, summary: '查询联系方式绑定能力' }) @Post("/contact/capability", { description: Constants.per.authOnly, summary: "查询联系方式绑定能力" })
public async contactCapability() { public async contactCapability() {
const settings = await this.sysSettingsService.getPrivateSettings(); const settings = await this.sysSettingsService.getPrivateSettings();
return this.ok({ return this.ok({
@@ -67,27 +77,27 @@ export class MineController extends BaseController {
}); });
} }
@Post('/contact/verifyIdentity', { description: Constants.per.authOnly, summary: '验证本人操作' }) @Post("/contact/verifyIdentity", { description: Constants.per.authOnly, summary: "验证本人操作" })
public async verifyContactIdentity(@Body(ALL) body: { identityType: 'password' | 'email' | 'mobile'; identityPassword?: string; identityValidateCode?: string }) { public async verifyContactIdentity(@Body(ALL) body: { identityType: "password" | "email" | "mobile"; identityPassword?: string; identityValidateCode?: string }) {
const userId = this.getUserId(); const userId = this.getUserId();
await this.userService.verifyIdentity(userId, body, this.codeService); await this.userService.verifyIdentity(userId, body, this.codeService);
const validationCode = this.codeService.setValidationValue({ const validationCode = this.codeService.setValidationValue({
type: 'contactIdentity', type: "contactIdentity",
userId, userId,
identityType: body.identityType, identityType: body.identityType,
}); });
return this.ok({ validationCode }); return this.ok({ validationCode });
} }
@Post('/contact/mobile', { description: Constants.per.authOnly, summary: '绑定或修改手机号' }) @Post("/contact/mobile", { description: Constants.per.authOnly, summary: "绑定或修改手机号" })
public async updateMobile(@Body(ALL) body: { phoneCode?: string; mobile: string; validateCode: string; identityValidationCode: string }) { public async updateMobile(@Body(ALL) body: { phoneCode?: string; mobile: string; validateCode: string; identityValidationCode: string }) {
const userId = this.getUserId(); const userId = this.getUserId();
this.userService.checkContactIdentityValidation(userId, body.identityValidationCode, this.codeService); this.userService.checkContactIdentityValidation(userId, body.identityValidationCode, this.codeService);
await this.codeService.checkSmsCode({ await this.codeService.checkSmsCode({
mobile: body.mobile, mobile: body.mobile,
phoneCode: body.phoneCode || '86', phoneCode: body.phoneCode || "86",
smsCode: body.validateCode, smsCode: body.validateCode,
verificationType: 'bindMobile', verificationType: "bindMobile",
throwError: true, throwError: true,
}); });
await this.userService.updateMobile(userId, { await this.userService.updateMobile(userId, {
@@ -97,14 +107,14 @@ export class MineController extends BaseController {
return this.ok({}); return this.ok({});
} }
@Post('/contact/email', { description: Constants.per.authOnly, summary: '绑定或修改邮箱' }) @Post("/contact/email", { description: Constants.per.authOnly, summary: "绑定或修改邮箱" })
public async updateEmail(@Body(ALL) body: { email: string; validateCode: string; identityValidationCode: string }) { public async updateEmail(@Body(ALL) body: { email: string; validateCode: string; identityValidationCode: string }) {
const userId = this.getUserId(); const userId = this.getUserId();
this.userService.checkContactIdentityValidation(userId, body.identityValidationCode, this.codeService); this.userService.checkContactIdentityValidation(userId, body.identityValidationCode, this.codeService);
this.codeService.checkEmailCode({ this.codeService.checkEmailCode({
email: body.email, email: body.email,
validateCode: body.validateCode, validateCode: body.validateCode,
verificationType: 'bindEmail', verificationType: "bindEmail",
throwError: true, throwError: true,
}); });
await this.userService.updateEmail(userId, { await this.userService.updateEmail(userId, {
@@ -4,7 +4,7 @@ import { AutoLoadPlugins } from "./auto-load-plugins.js";
import { AutoCron } from "./auto-cron.js"; import { AutoCron } from "./auto-cron.js";
import { AutoMitterRegister } from "./auto-mitter-register.js"; import { AutoMitterRegister } from "./auto-mitter-register.js";
import { AutoPipelineEmitterRegister } from "./auto-pipeline-emitter-register.js"; import { AutoPipelineEmitterRegister } from "./auto-pipeline-emitter-register.js";
import { AutoFix } from "./auto-fix.js"; import { AutoFix } from "./fix/auto-fix.js";
import { AutoPrint } from "./auto-print.js"; import { AutoPrint } from "./auto-print.js";
@Autoload() @Autoload()
@@ -0,0 +1,68 @@
import assert from "assert";
import { AutoFix } from "./auto-fix.js";
describe("AutoFix", () => {
it("runs unfinished fix tasks in order and marks them fixed", async () => {
const calls: string[] = [];
let savedSetting: any;
const autoFix = new AutoFix();
autoFix.sysSettingsService = {
async getSetting() {
return {
fixed: {
"oauth-subtype-bound-type": true,
},
};
},
async saveSetting(setting: any) {
savedSetting = {
fixed: { ...setting.fixed },
};
},
} as any;
autoFix.googleCommonEabAccountKeyFix = {
async init() {
calls.push("google");
},
} as any;
autoFix.oauthSubtypeBoundTypeFix = {
async init() {
calls.push("oauth");
},
} as any;
autoFix.certInfoWildcardDomainCountFix = {
async init() {
calls.push("cert");
},
} as any;
autoFix.suiteContentWildcardDomainCountFix = {
async init() {
calls.push("suite");
},
} as any;
await autoFix.init();
assert.deepEqual(calls, ["google", "cert", "suite"]);
assert.equal(savedSetting.fixed["google-common-eab-account-key"], true);
assert.equal(savedSetting.fixed["oauth-subtype-bound-type"], true);
assert.equal(savedSetting.fixed["cert-info-wildcard-domain-count"], true);
assert.equal(savedSetting.fixed["suite-content-wildcard-domain-count"], true);
});
it("initializes missing fixed map", async () => {
const autoFix = new AutoFix();
autoFix.sysSettingsService = {
async getSetting() {
return {};
},
async saveSetting() {},
} as any;
autoFix.googleCommonEabAccountKeyFix = { async init() {} } as any;
autoFix.oauthSubtypeBoundTypeFix = { async init() {} } as any;
autoFix.certInfoWildcardDomainCountFix = { async init() {} } as any;
autoFix.suiteContentWildcardDomainCountFix = { async init() {} } as any;
await autoFix.init();
});
});
@@ -0,0 +1,64 @@
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { SysAutoFixSetting, SysSettingsService } from "@certd/lib-server";
import { GoogleCommonEabAccountKeyFix } from "./google-common-eab-account-key-fix.js";
import { OauthSubtypeBoundTypeFix } from "./oauth-subtype-bound-type-fix.js";
import { CertInfoWildcardDomainCountFix } from "./cert-info-wildcard-domain-count-fix.js";
import { SuiteContentWildcardDomainCountFix } from "./suite-content-wildcard-domain-count-fix.js";
type AutoFixTask = {
key: string;
fix: {
init(): Promise<void>;
};
};
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class AutoFix {
@Inject()
sysSettingsService: SysSettingsService;
@Inject()
googleCommonEabAccountKeyFix: GoogleCommonEabAccountKeyFix;
@Inject()
oauthSubtypeBoundTypeFix: OauthSubtypeBoundTypeFix;
@Inject()
certInfoWildcardDomainCountFix: CertInfoWildcardDomainCountFix;
@Inject()
suiteContentWildcardDomainCountFix: SuiteContentWildcardDomainCountFix;
async init() {
const setting = await this.sysSettingsService.getSetting<SysAutoFixSetting>(SysAutoFixSetting);
setting.fixed = setting.fixed || {};
const tasks: AutoFixTask[] = [
{
key: "google-common-eab-account-key",
fix: this.googleCommonEabAccountKeyFix,
},
{
key: "oauth-subtype-bound-type",
fix: this.oauthSubtypeBoundTypeFix,
},
{
key: "cert-info-wildcard-domain-count",
fix: this.certInfoWildcardDomainCountFix,
},
{
key: "suite-content-wildcard-domain-count",
fix: this.suiteContentWildcardDomainCountFix,
},
];
for (const task of tasks) {
if (setting.fixed?.[task.key]) {
continue;
}
await task.fix.init();
setting.fixed[task.key] = true;
await this.sysSettingsService.saveSetting(setting);
}
}
}
@@ -0,0 +1,37 @@
import assert from "assert";
import { CertInfoWildcardDomainCountFix } from "./cert-info-wildcard-domain-count-fix.js";
describe("CertInfoWildcardDomainCountFix", () => {
it("fixes cert info wildcard domain count only when value changed", async () => {
const updated: any[] = [];
const rows = [
{ id: 1, domains: "*.a.com,a.com, *.b.com ", wildcardDomainCount: 0 },
{ id: 2, domains: "c.com", wildcardDomainCount: 0 },
{ id: 3, domains: "*.d.com", wildcardDomainCount: 1 },
];
const fix = new CertInfoWildcardDomainCountFix();
fix.certInfoService = {
countWildcardDomains(domains: string[]) {
return domains.filter(item => item.trim().toLowerCase().startsWith("*.")).length;
},
async find() {
return rows;
},
async update(value: any) {
updated.push(value);
const row = rows.find(item => item.id === value.id);
Object.assign(row, value);
},
} as any;
await fix.init();
await fix.init();
assert.deepEqual(updated, [
{
id: 1,
wildcardDomainCount: 2,
},
]);
});
});
@@ -0,0 +1,45 @@
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { logger } from "@certd/basic";
import { CertInfoService } from "../../monitor/service/cert-info-service.js";
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class CertInfoWildcardDomainCountFix {
@Inject()
certInfoService: CertInfoService;
async init() {
if (!this.certInfoService) {
return;
}
try {
const list = await this.certInfoService.find({
select: {
id: true,
domains: true,
wildcardDomainCount: true,
},
});
let fixedCount = 0;
for (const item of list) {
if (!item.domains) {
continue;
}
const wildcardDomainCount = this.certInfoService.countWildcardDomains(item.domains.split(","));
if ((item.wildcardDomainCount ?? 0) === wildcardDomainCount) {
continue;
}
await this.certInfoService.update({
id: item.id,
wildcardDomainCount,
});
fixedCount++;
}
if (fixedCount > 0) {
logger.info(`已修复证书泛域名数量历史数据,数量=${fixedCount}`);
}
} catch (e: any) {
logger.error("修复证书泛域名数量历史数据失败", e);
}
}
}
@@ -1,16 +1,8 @@
import assert from "assert"; import assert from "assert";
import esmock from "esmock"; import esmock from "esmock";
import { AutoFix, buildEabAccountKeyValue, buildLegacyGoogleAccountConfigWhere, parseStorageValue } from "./auto-fix.js"; import { buildEabAccountKeyValue, buildLegacyGoogleAccountConfigWhere, GoogleCommonEabAccountKeyFix, parseStorageValue } from "./google-common-eab-account-key-fix.js";
function createAutoFix(options: { pluginConfigService?: any; accessService?: any; storageService?: any }) { describe("GoogleCommonEabAccountKeyFix", () => {
const autoFix = new AutoFix();
autoFix.pluginConfigService = options.pluginConfigService;
autoFix.accessService = options.accessService;
autoFix.storageService = options.storageService;
return autoFix;
}
describe("AutoFix", () => {
it("parses legacy storage values", () => { it("parses legacy storage values", () => {
const config = parseStorageValue( const config = parseStorageValue(
JSON.stringify({ JSON.stringify({
@@ -45,32 +37,29 @@ describe("AutoFix", () => {
it("finds legacy Google account config by exact email key only", async () => { it("finds legacy Google account config by exact email key only", async () => {
let findOneWhere: any; let findOneWhere: any;
let findCalled = false; let findCalled = false;
const autoFix = createAutoFix({ const fix = new GoogleCommonEabAccountKeyFix();
pluginConfigService: null as any, fix.storageService = {
accessService: null as any, getRepository() {
storageService: { return {
getRepository() { async findOne(options: any) {
return { findOneWhere = options.where;
async findOne(options: any) { return {
findOneWhere = options.where; value: JSON.stringify({
return { value: {
value: JSON.stringify({ privateKey: "legacy-private-key",
value: { },
privateKey: "legacy-private-key", }),
}, };
}), },
}; async find() {
}, findCalled = true;
async find() { return [];
findCalled = true; },
return []; };
}, },
}; } as any;
},
} as any,
});
const config = await autoFix.getLegacyGoogleAccountConfig("user@example.com"); const config = await fix.getLegacyGoogleAccountConfig("user@example.com");
assert.equal(config.privateKey, "legacy-private-key"); assert.equal(config.privateKey, "legacy-private-key");
assert.deepEqual(findOneWhere, buildLegacyGoogleAccountConfigWhere("user@example.com")); assert.deepEqual(findOneWhere, buildLegacyGoogleAccountConfigWhere("user@example.com"));
@@ -79,43 +68,42 @@ describe("AutoFix", () => {
it("does not query legacy Google account config without email", async () => { it("does not query legacy Google account config without email", async () => {
let repositoryCalled = false; let repositoryCalled = false;
const autoFix = createAutoFix({ const fix = new GoogleCommonEabAccountKeyFix();
pluginConfigService: null as any, fix.storageService = {
accessService: null as any, getRepository() {
storageService: { repositoryCalled = true;
getRepository() { return {};
repositoryCalled = true; },
return {}; } as any;
},
} as any,
});
const config = await autoFix.getLegacyGoogleAccountConfig(); const config = await fix.getLegacyGoogleAccountConfig();
assert.equal(config, null); assert.equal(config, null);
assert.equal(repositoryCalled, false); assert.equal(repositoryCalled, false);
}); });
it("skips Google common EAB account key fix outside commercial edition", async () => { it("skips Google common EAB account key fix outside commercial edition", async () => {
let pluginConfigCalled = false; const { GoogleCommonEabAccountKeyFix: MockedFix } = await esmock("./google-common-eab-account-key-fix.js", {
const autoFix = createAutoFix({ "@certd/plus-core": {
pluginConfigService: { isComm: () => false,
async getPluginConfig() { },
pluginConfigCalled = true;
return null;
},
} as any,
accessService: null as any,
storageService: null as any,
}); });
let pluginConfigCalled = false;
const fix = new MockedFix();
fix.pluginConfigService = {
async getPluginConfig() {
pluginConfigCalled = true;
return null;
},
};
await autoFix.init(); await fix.init();
assert.equal(pluginConfigCalled, false); assert.equal(pluginConfigCalled, false);
}); });
it("fixes Google common EAB account key in commercial edition", async () => { it("fixes Google common EAB account key in commercial edition", async () => {
const { AutoFix: MockedAutoFix } = await esmock("./auto-fix.js", { const { GoogleCommonEabAccountKeyFix: MockedFix } = await esmock("./google-common-eab-account-key-fix.js", {
"@certd/plus-core": { "@certd/plus-core": {
isComm: () => true, isComm: () => true,
}, },
@@ -123,8 +111,8 @@ describe("AutoFix", () => {
let getAccessByIdArgs: any[] = []; let getAccessByIdArgs: any[] = [];
let findOneWhere: any; let findOneWhere: any;
let updateAccessParam: any; let updateAccessParam: any;
const autoFix = new MockedAutoFix(); const fix = new MockedFix();
autoFix.pluginConfigService = { fix.pluginConfigService = {
async getPluginConfig(options: any) { async getPluginConfig(options: any) {
assert.deepEqual(options, { assert.deepEqual(options, {
name: "CertApply", name: "CertApply",
@@ -139,7 +127,7 @@ describe("AutoFix", () => {
}; };
}, },
}; };
autoFix.accessService = { fix.accessService = {
async getAccessById(...args: any[]) { async getAccessById(...args: any[]) {
getAccessByIdArgs = args; getAccessByIdArgs = args;
return { return {
@@ -151,7 +139,7 @@ describe("AutoFix", () => {
updateAccessParam = param; updateAccessParam = param;
}, },
}; };
autoFix.storageService = { fix.storageService = {
getRepository() { getRepository() {
return { return {
async findOne(options: any) { async findOne(options: any) {
@@ -168,7 +156,7 @@ describe("AutoFix", () => {
}, },
}; };
await autoFix.fixGoogleCommonEabAccountKey(); await fix.init();
assert.deepEqual(getAccessByIdArgs, [12, false]); assert.deepEqual(getAccessByIdArgs, [12, false]);
assert.deepEqual(findOneWhere, buildLegacyGoogleAccountConfigWhere("user@example.com")); assert.deepEqual(findOneWhere, buildLegacyGoogleAccountConfigWhere("user@example.com"));
@@ -178,5 +166,4 @@ describe("AutoFix", () => {
accountKey: buildEabAccountKeyValue("kid-1", "legacy-private-key"), accountKey: buildEabAccountKeyValue("kid-1", "legacy-private-key"),
}); });
}); });
}); });
@@ -2,8 +2,8 @@ import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { logger } from "@certd/basic"; import { logger } from "@certd/basic";
import { AccessService } from "@certd/lib-server"; import { AccessService } from "@certd/lib-server";
import { isComm } from "@certd/plus-core"; import { isComm } from "@certd/plus-core";
import { PluginConfigService } from "../plugin/service/plugin-config-service.js"; import { PluginConfigService } from "../../plugin/service/plugin-config-service.js";
import { StorageService } from "../pipeline/service/storage-service.js"; import { StorageService } from "../../pipeline/service/storage-service.js";
export function parseStorageValue(value?: string) { export function parseStorageValue(value?: string) {
if (!value) { if (!value) {
@@ -35,7 +35,7 @@ export function buildLegacyGoogleAccountConfigWhere(email: string) {
@Provide() @Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true }) @Scope(ScopeEnum.Request, { allowDowngrade: true })
export class AutoFix { export class GoogleCommonEabAccountKeyFix {
@Inject() @Inject()
pluginConfigService: PluginConfigService; pluginConfigService: PluginConfigService;
@@ -46,9 +46,6 @@ export class AutoFix {
storageService: StorageService; storageService: StorageService;
async init() { async init() {
await this.fixGoogleCommonEabAccountKey();
}
async fixGoogleCommonEabAccountKey() {
if (!isComm()) { if (!isComm()) {
return; return;
} }
@@ -0,0 +1,94 @@
import assert from "assert";
import { buildOauthBoundType, OauthSubtypeBoundTypeFix } from "./oauth-subtype-bound-type-fix.js";
describe("OauthSubtypeBoundTypeFix", () => {
it("builds OAuth subtype bound type", () => {
assert.equal(buildOauthBoundType("clogin", "alipay"), "clogin:alipay");
assert.equal(buildOauthBoundType("github"), "github");
});
it("fixes legacy OAuth bound type from string addon loginType and converts loginType to array", async () => {
const updates: any[] = [];
const fix = new OauthSubtypeBoundTypeFix();
fix.sysSettingsService = {
async getPublicSettings() {
return {
oauthProviders: {
clogin: {
addonId: 1,
},
},
};
},
} as any;
fix.oauthBoundService = {
async transaction(callback: any) {
return await callback({
async findOne(entity: any, options: any) {
assert.equal(entity.name, "AddonEntity");
assert.deepEqual(options, { where: { id: 1 } });
return {
id: 1,
setting: JSON.stringify({
loginType: "alipay",
}),
};
},
async update(entity: any, where: any, value: any) {
updates.push({ entity: entity.name, where, value });
return { affected: entity.name === "OauthBoundEntity" ? 1 : 0 };
},
});
},
} as any;
await fix.init();
assert.deepEqual(updates[0], {
entity: "OauthBoundEntity",
where: { type: "clogin" },
value: { type: "clogin:alipay" },
});
assert.equal(updates[1].entity, "AddonEntity");
assert.deepEqual(updates[1].where, { id: 1 });
assert.deepEqual(JSON.parse(updates[1].value.setting).loginType, ["alipay"]);
});
it("skips OAuth subtype fix when addon loginType is already not legacy string", async () => {
let updateCalled = false;
const fix = new OauthSubtypeBoundTypeFix();
fix.sysSettingsService = {
async getPublicSettings() {
return {
oauthProviders: {
clogin: {
addonId: 1,
},
},
};
},
} as any;
fix.oauthBoundService = {
async transaction(callback: any) {
return await callback({
async findOne() {
return {
id: 1,
setting: JSON.stringify({
loginType: ["alipay", "github"],
}),
};
},
async update() {
updateCalled = true;
return { affected: 0 };
},
});
},
} as any;
await fix.init();
assert.equal(updateCalled, false);
});
});
@@ -0,0 +1,68 @@
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { logger } from "@certd/basic";
import { AddonEntity, SysSettingsService } from "@certd/lib-server";
import { OauthBoundService } from "../../login/service/oauth-bound-service.js";
import { OauthBoundEntity } from "../../login/entity/oauth-bound.js";
export function buildOauthBoundType(type: string, subtype?: string) {
return subtype ? `${type}:${subtype}` : type;
}
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class OauthSubtypeBoundTypeFix {
@Inject()
sysSettingsService: SysSettingsService;
@Inject()
oauthBoundService: OauthBoundService;
async init() {
try {
const publicSettings = await this.sysSettingsService.getPublicSettings();
const oauthProviders = publicSettings.oauthProviders || {};
await this.oauthBoundService.transaction(async manager => {
for (const [type, provider] of Object.entries(oauthProviders)) {
if (!provider.addonId) {
continue;
}
const addonEntity = await manager.findOne(AddonEntity, { where: { id: provider.addonId } });
const legacyLoginType = this.getLegacyAddonLoginType(addonEntity?.setting);
if (!legacyLoginType) {
continue;
}
const newType = buildOauthBoundType(type, legacyLoginType);
const res = await manager.update(OauthBoundEntity, { type }, { type: newType });
if (res.affected) {
logger.info(`已修复OAuth绑定历史数据,${type} -> ${newType},数量=${res.affected}`);
}
await this.convertLegacyAddonLoginTypeToArray(addonEntity, legacyLoginType, manager);
}
});
} catch (e: any) {
logger.error("修复OAuth subtype绑定历史数据失败", e);
}
}
private getLegacyAddonLoginType(settingValue?: string) {
if (!settingValue) {
return null;
}
const setting = JSON.parse(settingValue);
return typeof setting.loginType === "string" && setting.loginType ? setting.loginType : null;
}
private async convertLegacyAddonLoginTypeToArray(addonEntity: AddonEntity | null, loginType: string, manager: any) {
if (!addonEntity?.setting) {
return;
}
const setting = JSON.parse(addonEntity.setting);
if (typeof setting.loginType !== "string") {
return;
}
setting.loginType = [loginType];
await manager.update(AddonEntity, { id: addonEntity.id }, { setting: JSON.stringify(setting) });
}
}
@@ -0,0 +1,72 @@
import assert from "assert";
import { fixSuiteContentWildcardDomainCount, SuiteContentWildcardDomainCountFix } from "./suite-content-wildcard-domain-count-fix.js";
describe("SuiteContentWildcardDomainCountFix", () => {
it("fills missing suite wildcard domain count from total domain count", () => {
const fixed = fixSuiteContentWildcardDomainCount(
JSON.stringify({
maxDomainCount: 10,
})
);
assert.equal(JSON.parse(fixed).maxWildcardDomainCount, 10);
assert.equal(
JSON.parse(
fixSuiteContentWildcardDomainCount(
JSON.stringify({
maxDomainCount: -1,
})
)
).maxWildcardDomainCount,
-1
);
assert.equal(
fixSuiteContentWildcardDomainCount(
JSON.stringify({
maxDomainCount: 10,
maxWildcardDomainCount: 3,
})
),
null
);
});
it("fixes suite content wildcard domain count in product and user suite tables", async () => {
const rows = {
cd_product: [
{ id: 1, content: JSON.stringify({ maxDomainCount: 1 }) },
{ id: 2, content: JSON.stringify({ maxDomainCount: 1, maxWildcardDomainCount: 0 }) },
],
cd_user_suite: [{ id: 3, content: JSON.stringify({ maxDomainCount: 2 }) }],
};
const updates: any[] = [];
const entityManager = {
async query(sql: string) {
const table = sql.includes("cd_product") ? "cd_product" : "cd_user_suite";
return rows[table];
},
async update(tableName: string, where: any, value: any) {
updates.push({ tableName, where, value });
const row = rows[tableName].find((item: any) => item.id === where.id);
Object.assign(row, value);
},
};
const fix = new SuiteContentWildcardDomainCountFix();
fix.dataSourceManager = {
getDataSource() {
return {
manager: entityManager,
};
},
} as any;
await fix.init();
await fix.init();
assert.equal(updates.length, 2);
assert.equal(updates[0].tableName, "cd_product");
assert.equal(JSON.parse(updates[0].value.content).maxWildcardDomainCount, 1);
assert.equal(updates[1].tableName, "cd_user_suite");
assert.equal(JSON.parse(updates[1].value.content).maxWildcardDomainCount, 2);
});
});
@@ -0,0 +1,54 @@
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { logger } from "@certd/basic";
import { TypeORMDataSourceManager } from "@midwayjs/typeorm";
export function fixSuiteContentWildcardDomainCount(contentValue?: string) {
if (!contentValue) {
return null;
}
const content = JSON.parse(contentValue);
if (content.maxWildcardDomainCount != null) {
return null;
}
content.maxWildcardDomainCount = content.maxDomainCount == null || content.maxDomainCount === -1 ? -1 : content.maxDomainCount;
return JSON.stringify(content);
}
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class SuiteContentWildcardDomainCountFix {
@Inject()
dataSourceManager: TypeORMDataSourceManager;
async init() {
if (!this.dataSourceManager) {
return;
}
try {
const entityManager = this.dataSourceManager.getDataSource("default").manager;
let fixedCount = 0;
fixedCount += await this.fixSuiteContentWildcardDomainCountByTable(entityManager, "cd_product");
fixedCount += await this.fixSuiteContentWildcardDomainCountByTable(entityManager, "cd_user_suite");
if (fixedCount > 0) {
logger.info(`已修复套餐最大泛域名数量历史数据,数量=${fixedCount}`);
}
} catch (e: any) {
logger.error("修复套餐最大泛域名数量历史数据失败", e);
}
}
private async fixSuiteContentWildcardDomainCountByTable(entityManager: any, tableName: string) {
const list = await entityManager.query(`select id, content from ${tableName}`);
let fixedCount = 0;
for (const item of list) {
const content = fixSuiteContentWildcardDomainCount(item.content);
if (!content) {
continue;
}
await entityManager.update(tableName, { id: item.id }, { content });
fixedCount++;
}
return fixedCount;
}
}
@@ -8,7 +8,7 @@ import { isComm, isPlus } from '@certd/plus-core';
import nodemailer from 'nodemailer'; import nodemailer from 'nodemailer';
import { SendMailOptions } from 'nodemailer'; import { SendMailOptions } from 'nodemailer';
import { UserSettingsService } from '../../mine/service/user-settings-service.js'; import { UserSettingsService } from '../../mine/service/user-settings-service.js';
import { AddonService, PlusService, SysEmailConf, SysSettingsService, SysSiteInfo } from '@certd/lib-server'; import { AddonService, PlusService, SysEmailConf, SysInstallInfo, SysSettingsService, SysSiteInfo } from '@certd/lib-server';
import { getEmailSettings } from '../../sys/settings/fix.js'; import { getEmailSettings } from '../../sys/settings/fix.js';
import { UserEmailSetting } from "../../mine/service/models.js"; import { UserEmailSetting } from "../../mine/service/models.js";
import { AddonGetterService } from '../../pipeline/service/addon-getter-service.js'; import { AddonGetterService } from '../../pipeline/service/addon-getter-service.js';
@@ -131,12 +131,21 @@ export class EmailService implements IEmailService {
data: { data: {
title: '测试邮件,from certd', title: '测试邮件,from certd',
content: '测试邮件,from certd', content: '测试邮件,from certd',
url: "https://certd.handfree.work", url: await this.getTestEmailUrl(),
}, },
receivers: [receiver], receivers: [receiver],
}); });
} }
private async getTestEmailUrl() {
const defaultUrl = "https://certd.docmirror.cn";
if (!isComm()) {
return defaultUrl;
}
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
return installInfo?.bindUrl || installInfo?.bindUrl2 || defaultUrl;
}
async list(userId: any) { async list(userId: any) {
const userEmailSetting = await this.settingsService.getSetting<UserEmailSetting>(userId,null, UserEmailSetting) const userEmailSetting = await this.settingsService.getSetting<UserEmailSetting>(userId,null, UserEmailSetting)
return userEmailSetting.list; return userEmailSetting.list;
@@ -157,7 +166,6 @@ export class EmailService implements IEmailService {
await this.settingsService.saveSetting(userId, null, userEmailSetting) await this.settingsService.saveSetting(userId, null, userEmailSetting)
} }
async sendByTemplate(req: EmailSendByTemplateReq) { async sendByTemplate(req: EmailSendByTemplateReq) {
let content = null let content = null
const emailConf = await this.sysSettingsService.getSetting<SysEmailConf>(SysEmailConf); const emailConf = await this.sysSettingsService.getSetting<SysEmailConf>(SysEmailConf);
@@ -18,6 +18,9 @@ export class CertInfoEntity {
@Column({ name: 'domain_count', comment: '域名数量' }) @Column({ name: 'domain_count', comment: '域名数量' })
domainCount: number; domainCount: number;
@Column({ name: 'wildcard_domain_count', comment: '泛域名数量', default: 0 })
wildcardDomainCount: number;
@Column({ name: 'pipeline_id', comment: '关联流水线id' }) @Column({ name: 'pipeline_id', comment: '关联流水线id' })
pipelineId: number; pipelineId: number;
@@ -0,0 +1,29 @@
import assert from "assert";
import { CertInfoService } from "./cert-info-service.js";
describe("CertInfoService", () => {
it("counts wildcard domains by normalized prefix", () => {
const service = new CertInfoService();
assert.equal(service.countWildcardDomains(["*.a.com", "a.com", " *.B.com "]), 2);
});
it("saves wildcard domain count when updating pipeline domains", async () => {
const service = new CertInfoService();
let saved: any;
service.repository = {
async findOne() {
return null;
},
} as any;
service.addOrUpdate = async (bean: any) => {
saved = bean;
return bean;
};
await service.updateDomains(1, 2, null, ["*.a.com", "a.com", "*.b.com"], "pipeline");
assert.equal(saved.domainCount, 3);
assert.equal(saved.wildcardDomainCount, 2);
});
});
@@ -40,6 +40,22 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
}); });
} }
async getUserWildcardDomainCount(userId: number) {
if (userId == null) {
throw new Error('userId is required');
}
return await this.repository.sum('wildcardDomainCount', {
userId,
});
}
countWildcardDomains(domains?: string[]) {
if (!domains) {
return 0;
}
return domains.filter(domain => domain?.trim().toLowerCase().startsWith("*.")).length;
}
async updateDomains(pipelineId: number, userId: number, projectId: number, domains: string[],fromType?:string) { async updateDomains(pipelineId: number, userId: number, projectId: number, domains: string[],fromType?:string) {
const found = await this.repository.findOne({ const found = await this.repository.findOne({
where: { where: {
@@ -67,10 +83,12 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
bean.domain = ''; bean.domain = '';
bean.domains = ''; bean.domains = '';
bean.domainCount = 0; bean.domainCount = 0;
bean.wildcardDomainCount = 0;
} else { } else {
bean.domain = domains[0]; bean.domain = domains[0];
bean.domains = domains.join(','); bean.domains = domains.join(',');
bean.domainCount = domains.length; bean.domainCount = domains.length;
bean.wildcardDomainCount = this.countWildcardDomains(domains);
} }
await this.addOrUpdate(bean); await this.addOrUpdate(bean);
@@ -171,6 +189,7 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
bean.domains = domains.join(','); bean.domains = domains.join(',');
bean.domain = domains[0]; bean.domain = domains[0];
bean.domainCount = domains.length; bean.domainCount = domains.length;
bean.wildcardDomainCount = this.countWildcardDomains(domains);
bean.effectiveTime = certReader.effective; bean.effectiveTime = certReader.effective;
bean.expiresTime = certReader.expires; bean.expiresTime = certReader.expires;
bean.certProvider = certReader.detail.issuer.commonName; bean.certProvider = certReader.detail.issuer.commonName;
@@ -53,6 +53,7 @@ import { executorQueue } from "@certd/lib-server";
import parser from "cron-parser"; import parser from "cron-parser";
import { ProjectService } from "../../sys/enterprise/service/project-service.js"; import { ProjectService } from "../../sys/enterprise/service/project-service.js";
import { CertApplyStepInputPatch, updateCertApplyStepInputs } from "./pipeline-batch-update.js"; import { CertApplyStepInputPatch, updateCertApplyStepInputs } from "./pipeline-batch-update.js";
import { calcNextSuiteCountUsed } from "./pipeline-suite-limit.js";
const runningTasks: Map<string | number, Executor> = new Map(); const runningTasks: Map<string | number, Executor> = new Map();
@@ -76,7 +77,6 @@ export class PipelineService extends BaseService<PipelineEntity> {
historyService: HistoryService; historyService: HistoryService;
@Inject() @Inject()
historyLogService: HistoryLogService; historyLogService: HistoryLogService;
@Inject() @Inject()
pluginConfigGetter: PluginConfigGetter; pluginConfigGetter: PluginConfigGetter;
@@ -287,10 +287,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
}); });
} }
if (!isUpdate) { await this.checkMaxPipelineCount(bean, pipeline, domains, old);
//如果是添加,校验数量
await this.checkMaxPipelineCount(bean, pipeline, domains);
}
if (!bean.status) { if (!bean.status) {
bean.status = ResultType.none; bean.status = ResultType.none;
@@ -345,7 +342,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
return bean return bean
} }
private async checkMaxPipelineCount(bean: PipelineEntity, pipeline: Pipeline, domains: string[]) { private async checkMaxPipelineCount(bean: PipelineEntity, pipeline: Pipeline, domains: string[], old?: PipelineEntity) {
// if (!isPlus()) { // if (!isPlus()) {
// const count = await this.repository.count(); // const count = await this.repository.count();
// if (count >= freeCount) { // if (count >= freeCount) {
@@ -363,15 +360,31 @@ export class PipelineService extends BaseService<PipelineEntity> {
const suiteSetting = await this.userSuiteService.getSuiteSetting(); const suiteSetting = await this.userSuiteService.getSuiteSetting();
if (suiteSetting.enabled) { if (suiteSetting.enabled) {
const userSuite = await this.userSuiteService.getMySuiteDetail(bean.userId); const userSuite = await this.userSuiteService.getMySuiteDetail(bean.userId);
if (userSuite?.pipelineCount.max != -1 && userSuite?.pipelineCount.used + 1 > userSuite?.pipelineCount.max) { if (!old && userSuite?.pipelineCount.max != -1 && userSuite?.pipelineCount.used + 1 > userSuite?.pipelineCount.max) {
throw new NeedSuiteException(`对不起,您最多只能创建${userSuite?.pipelineCount.max}条流水线,请购买或升级套餐`); throw new NeedSuiteException(`对不起,您最多只能创建${userSuite?.pipelineCount.max}条流水线,请购买或升级套餐`);
} }
if (userSuite.domainCount.max != -1 && userSuite.domainCount.used + domains.length > userSuite.domainCount.max) { let oldDomainCount = 0;
let oldWildcardDomainCount = 0;
if (old?.id) {
const oldCertInfo = await this.certInfoService.getByPipelineId(old.id);
oldDomainCount = oldCertInfo?.domainCount ?? 0;
oldWildcardDomainCount = oldCertInfo?.wildcardDomainCount ?? 0;
}
const nextDomainCountUsed = calcNextSuiteCountUsed(userSuite.domainCount.used, oldDomainCount, domains.length);
if (userSuite.domainCount.max != -1 && nextDomainCountUsed > userSuite.domainCount.max) {
throw new NeedSuiteException(`对不起,您最多只能添加${userSuite.domainCount.max}个域名,请购买或升级套餐`); throw new NeedSuiteException(`对不起,您最多只能添加${userSuite.domainCount.max}个域名,请购买或升级套餐`);
} }
const suiteWildcardDomainCount = userSuite.wildcardDomainCount;
const wildcardDomainCount = this.certInfoService.countWildcardDomains(domains);
const nextWildcardDomainCountUsed = calcNextSuiteCountUsed(suiteWildcardDomainCount.used, oldWildcardDomainCount, wildcardDomainCount);
if (suiteWildcardDomainCount.max != -1 && nextWildcardDomainCountUsed > suiteWildcardDomainCount.max) {
throw new NeedSuiteException(`对不起,您最多只能添加${suiteWildcardDomainCount.max}个泛域名,请购买或升级套餐`);
}
} }
} else { } else if (!old) {
//非商业版校验用户最大流水线数量 //非商业版校验用户最大流水线数量
const userId = bean.userId; const userId = bean.userId;
const userIsAdmin = await this.userService.isAdmin(userId); const userIsAdmin = await this.userService.isAdmin(userId);
@@ -1332,7 +1345,6 @@ export class PipelineService extends BaseService<PipelineEntity> {
} }
async createAutoPipeline(req: { domains: string[]; email: string; userId: number, projectId?: number, from: string }) { async createAutoPipeline(req: { domains: string[]; email: string; userId: number, projectId?: number, from: string }) {
const randomHour = Math.floor(Math.random() * 6); const randomHour = Math.floor(Math.random() * 6);
const randomMin = Math.floor(Math.random() * 60); const randomMin = Math.floor(Math.random() * 60);
const randomCron = `0 ${randomMin} ${randomHour} * * *`; const randomCron = `0 ${randomMin} ${randomHour} * * *`;
@@ -0,0 +1,9 @@
import assert from "assert";
import { calcNextSuiteCountUsed } from "./pipeline-suite-limit.js";
describe("Pipeline suite limits", () => {
it("calculates next usage by subtracting current pipeline usage on update", () => {
assert.equal(calcNextSuiteCountUsed(2, 1, 1), 2);
assert.equal(calcNextSuiteCountUsed(2, 1, 2), 3);
});
});
@@ -0,0 +1,3 @@
export function calcNextSuiteCountUsed(used: number, oldCount: number, newCount: number) {
return (used ?? 0) - (oldCount ?? 0) + (newCount ?? 0);
}

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