mirror of
https://github.com/certd/certd.git
synced 2026-07-12 08:17:32 +08:00
Compare commits
67 Commits
b8a64a6b5b
...
v1.40.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 5801f34b3a | |||
| 17cf16ca92 | |||
| 7015b1b232 | |||
| 3b72ca09c6 | |||
| a815d0245b | |||
| 229f22d5a9 | |||
| 22f5cfcfd8 | |||
| 90ba55c043 | |||
| 9f878a353c | |||
| af7297d671 | |||
| 2f172b56e9 | |||
| 9076c8b20e | |||
| 639756dfcd | |||
| 7aa0c7e491 | |||
| 45dedf5bc7 | |||
| 4681ec9008 | |||
| b91826c6e6 | |||
| 686856d0ae | |||
| 9b09d2578d | |||
| f8f51adf88 | |||
| f8ce639717 | |||
| 1c6dc169ac | |||
| 3e5366c74e | |||
| b49ddbfef9 | |||
| b92fd73f53 | |||
| 41b8f51a6a | |||
| aad9045de5 | |||
| fdd5848df4 | |||
| 118c15d046 | |||
| bae4f8e320 | |||
| e0189a566e | |||
| 1cd8d73cdb | |||
| d6e9e5987b | |||
| 8c5aa37745 | |||
| a18a871ac3 | |||
| 90cbff9cf9 | |||
| bae5a04dcc | |||
| 7146570392 | |||
| ae88f85d8e | |||
| a362860137 | |||
| c966896522 | |||
| 5f88da1985 | |||
| 043b80a298 | |||
| ed0da28896 | |||
| 61a0d69d58 | |||
| 3833a9216e | |||
| e59566b5e2 | |||
| e4be0ce464 | |||
| 022dbf0cab | |||
| 1e6b559b89 | |||
| 74bae2005d | |||
| 1731a35d94 | |||
| 9f7d766cb3 | |||
| 6c0d0b00c9 | |||
| a82a38421d | |||
| c4b01da384 | |||
| 3e1473dba5 | |||
| d383706554 | |||
| e0eb0e21f6 | |||
| 7266af1749 | |||
| f93bc09438 | |||
| fe3bb7c1b4 | |||
| 923676c7d5 | |||
| 4755216505 | |||
| 37d03c10f9 | |||
| 490b724808 | |||
| d8f132919d |
@@ -105,7 +105,7 @@ Certd 是一个支持私有化部署的 SSL/TLS 证书自动化管理平台。
|
||||
|
||||
- 前端 `pnpm dev`:启动 Vite 开发服务
|
||||
- 前端 `pnpm build`:生产构建
|
||||
- 前端 `pnpm tsc`:类型检查
|
||||
- 不要运行前端 `pnpm tsc` / `vue-tsc`:当前依赖组合中 `vue-tsc@1.8.27` 会直接抛内部错误 `Search string not found: "/supportedTSExtensions = .*(?=;)/"`,不是有效的项目类型检查结果。
|
||||
- 前端暂不跑单元测试;当前 `test:unit` 只是占位脚本
|
||||
|
||||
## 流水线与插件模型
|
||||
@@ -142,6 +142,12 @@ Certd 是一个支持私有化部署的 SSL/TLS 证书自动化管理平台。
|
||||
|
||||
如果只是某个服务商或部署目标的问题,不要轻易修改共享 pipeline/core 行为,除非确实是可复用的公共能力。
|
||||
|
||||
### ACME / EAB 注意事项
|
||||
|
||||
- 公共 EAB(尤其是 Google EAB)可能只能创建一次 ACME 账号。要跨用户复用公共 EAB,应保存并复用同一个 ACME account private key;`accountUrl` 如果存到 `userContext` 里,只能视为当前用户缓存,因为 `userContext` 跟用户 id 走。
|
||||
- ACME 协议的 `newAccount` 支持 `onlyReturnExisting`。使用同一个 account private key 调用 `newAccount({ onlyReturnExisting: true })` 可以取回已创建账号的 URL,且不会再次消费 EAB。
|
||||
- 修改 EAB 的 `kid` 后,应重新生成绑定该 `kid` 的 account private key;否则应阻止继续申请并提示用户刷新账号私钥。
|
||||
|
||||
## 数据与迁移
|
||||
|
||||
后端使用 TypeORM 实体加 SQL 迁移。
|
||||
@@ -161,6 +167,7 @@ Certd 是一个支持私有化部署的 SSL/TLS 证书自动化管理平台。
|
||||
- 优先沿用现有模块、插件、服务模式,再考虑新增抽象。
|
||||
- `packages/ui/certd-server/data/`、`logs/`、生成的 metadata/dist 等通常视为运行时或构建产物,除非任务明确要求处理它们。
|
||||
- 注意本地数据和配置里可能包含凭据、证书材料等敏感信息。
|
||||
- 本仓库代码注释优先使用中文,尤其是解释业务规则、兼容逻辑、协议细节和隐藏风险时;除非文件已有明确英文注释风格或引用外部英文术语,否则不要新增英文说明性注释。
|
||||
|
||||
## 插件开发技能
|
||||
|
||||
@@ -207,6 +214,7 @@ Get-ChildItem packages\ui\certd-client\src\views\certd
|
||||
- 实现新功能或修复行为缺陷前,先补对应单元测试,并先运行测试确认它处于失败状态;再实现功能或修复代码,反复运行聚焦单元测试直到通过。若某项改动确实不适合先写单元测试,应在回复中说明原因和替代验证方式。
|
||||
- 后补单元测试时,应先基于对正确行为的实际预期编写测试,而不是为了迎合现有实现改写预期;如果运行后出现红灯,且通过测试需要修改已有实现,应先向用户确认这是确实的 bug,还是原本需求/既有行为就是如此;确认后再修改原始实现,避免把测试补充变成未经确认的行为改动。
|
||||
- 后端纯单元测试用例放在 `src` 目录内,并尽量与被测文件相邻,例如 `src/utils/random.test.ts`;对应 `test:unit` 只跑 `src/**/*.test.ts`,构建/打包配置应排除这些 `*.test.ts` 文件。
|
||||
- 单元测试需要 mock ESM 静态 import 时,优先使用 `esmock`,不要为了测试把业务代码改成构造函数注入或把逻辑挪到调用方;各包 `test:unit` 脚本应显式设置 `NODE_ENV=unittest`。
|
||||
- 单个 monorepo 包运行单元测试时,优先使用 `corepack pnpm --dir <包目录> test:unit`,例如 `corepack pnpm --dir packages\ui\certd-server test:unit`、`corepack pnpm --dir packages\core\basic test:unit`、`corepack pnpm --dir packages\plugins\plugin-lib test:unit`;也可以用包名过滤,例如 `corepack pnpm --filter @certd/ui-server test:unit`。前端 `packages\ui\certd-client` 暂时不跑单元测试。
|
||||
- 前端 TS/Vue/locale 等文件改动后,优先只对本次改动文件运行项目现有自动格式化/修复;Windows/PowerShell 下 Prettier 已验证可用命令为 `packages\ui\certd-client\node_modules\.bin\prettier.cmd --write <files>`,ESLint 可用命令为 `packages\ui\certd-client\node_modules\.bin\eslint.cmd --fix <files>`;不要为了格式化无关文件而扩大 diff。项目保留了 `tslint` 依赖,但当前主要使用 ESLint + Prettier。
|
||||
- 优先对改动包运行聚焦的测试或类型检查;只有跨包影响明显时再考虑全 monorepo 构建。
|
||||
- 前端 TS/Vue/locale 等文件改动后,优先只对本次改动文件运行项目现有自动格式化/修复;Windows/PowerShell 下 Prettier 已验证可用命令为 `packages\ui\certd-client\node_modules\.bin\prettier.cmd --write <files>`,ESLint 可用命令为 `packages\ui\certd-client\node_modules\.bin\eslint.cmd --fix <files>`;不要运行 `vue-tsc` / `pnpm tsc`;不要为了格式化无关文件而扩大 diff。项目保留了 `tslint` 依赖,但当前主要使用 ESLint + Prettier。
|
||||
- 优先对改动包运行聚焦的测试;后端可按包运行单元测试,前端优先使用 Prettier/ESLint 做改动文件验证。只有跨包影响明显时再考虑全 monorepo 构建。
|
||||
|
||||
@@ -3,6 +3,69 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.40.0](https://github.com/certd/certd/compare/v1.39.16...v1.40.0) (2026-05-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复第三方登录丢失state时无法在用户信息页面绑定第三方账号的bug ([45dedf5](https://github.com/certd/certd/commit/45dedf5bc779fea852e1f33dda4f31db2765633c))
|
||||
* 修复群晖授权没有显示设备id输入框的bug ([2f172b5](https://github.com/certd/certd/commit/2f172b56e9411303ca15138d827bdb9bafdae4d1))
|
||||
* 修复自动注册后没有跳转到控制台的bug ([4681ec9](https://github.com/certd/certd/commit/4681ec90088a3eb665427b2ac4047ec5ccefd7b3))
|
||||
* 修复clogin登录丢失state问题 ([22f5cfc](https://github.com/certd/certd/commit/22f5cfcfd8462ca74128329eefb3f48b3ee0b7ea))
|
||||
* 修复clogin多选类型登录失败的bug ([9f878a3](https://github.com/certd/certd/commit/9f878a353cd49b7b10bb0a95610ad236bc920dd2))
|
||||
|
||||
### Features
|
||||
|
||||
* 彩虹登录支持选择多种登录方式 ([7aa0c7e](https://github.com/certd/certd/commit/7aa0c7e491fe660abb62e68792ff5474f19bd5b8))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 第三方登录自动注册的用户支持设置初始化密码 ([a815d02](https://github.com/certd/certd/commit/a815d0245b97efbb948b33d6fc9d49862ce06889))
|
||||
* 头像增加缓存时间 ([7015b1b](https://github.com/certd/certd/commit/7015b1b232602e5168a3eb8bee6d7f1776ae1e74))
|
||||
|
||||
## [1.39.16](https://github.com/certd/certd/compare/v1.39.15...v1.39.16) (2026-05-13)
|
||||
|
||||
**Note:** Version bump only for package root
|
||||
|
||||
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复第三方登录彩虹登录不上的bug ([bae4f8e](https://github.com/certd/certd/commit/bae4f8e3209d9f9869ecbd7c01655383bac2fe21))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 优化申请时报错日志增加对应域名打印 ([d6e9e59](https://github.com/certd/certd/commit/d6e9e5987bd52ea12ee18745615486eadd4c87ff))
|
||||
* icon选择器增加一套logo集 ([fdd5848](https://github.com/certd/certd/commit/fdd5848df4055a6ee07dc5eabaaf6b718672882d))
|
||||
* **monitor/site:** 新增站点监控页面禁用启用、检查状态两个筛选条件 ([118c15d](https://github.com/certd/certd/commit/118c15d04633a6ef06f2d9e7a7849d20f596e02c))
|
||||
* **network:** 新增全局公共http请求 headers设置 ([aad9045](https://github.com/certd/certd/commit/aad9045de55e76cb2ad09cac74a7bd60a4b47124))
|
||||
|
||||
## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复阿里云订阅流水线创建对话框无法获取阿里订单列表的bug ([a362860](https://github.com/certd/certd/commit/a362860137bfb7072893c844fe775edc46070ee1))
|
||||
* 修复启动时报密钥备份不存在的问题 ([c966896](https://github.com/certd/certd/commit/c9668965226af6b54e0e576931dcba8b3d188ef3))
|
||||
|
||||
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **aliyun-access:** 添加阿里云密钥校验失败的错误处理 ([b75c625](https://github.com/certd/certd/commit/b75c625ddcc0b3110699d8e6175681ef157b25df))
|
||||
* cnameProvider域名支持设置子域名托管 ([7266af1](https://github.com/certd/certd/commit/7266af17491a98338022cfb18cfedfb93ca6ef8f))
|
||||
* **plugin-aliyun:** 过滤非CAS证书并优化日志信息 ([c4b01da](https://github.com/certd/certd/commit/c4b01da384bc40a241a673ea8bc01ca733c04d83))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **设置:** 添加首页启用开关配置 ([25ad1e6](https://github.com/certd/certd/commit/25ad1e6f861e43288cc8bd90d4903628e6faec29))
|
||||
* 新增agents.md ([aa176b0](https://github.com/certd/certd/commit/aa176b081a92837d2d6809d16546a8dfc2e5dd36))
|
||||
* **用户资料:** 新增手机号邮箱绑定功能 ([e0eb0e2](https://github.com/certd/certd/commit/e0eb0e21f6dae24b639c944f9aba2c90496ab1c0))
|
||||
* 域名注册过期时间获取再次优化 ([91a1b97](https://github.com/certd/certd/commit/91a1b9755066bf280e194dabf7c3a9f936e2643f))
|
||||
* **证书流水线:** 添加批量更新证书申请参数功能 ([63be1c1](https://github.com/certd/certd/commit/63be1c1cbd9b09a3b48f26130c296b1cedcca1ac))
|
||||
* 支持火山云vke ([bb46cb0](https://github.com/certd/certd/commit/bb46cb08f71f6ae921543f7e4a6c5f4e0190556e))
|
||||
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
|
||||
* **domain:** 添加域名过期时间同步进度显示功能 ([9d2937d](https://github.com/certd/certd/commit/9d2937dd4b14ffab73e9b096edd2aa8539811182))
|
||||
* **plugin-volcengine:** 支持火山引擎VKE部署插件 ([b8a64a6](https://github.com/certd/certd/commit/b8a64a6b5bf3691a47177de42bc49b798e795feb))
|
||||
|
||||
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -38,6 +38,7 @@ Certd® 是一个免费的全自动证书管理系统,让你的网站证书永
|
||||
* **开放接口支持**: 提供RESTful API接口,方便集成到其他系统
|
||||
* **站点证书监控**: 定时监控网站证书的过期时间
|
||||
* **多用户管理**: 用户可以管理自己的证书流水线
|
||||
* **项目管理**: 企业级项目管理模式
|
||||
* **多语言支持**: 中英双语切换
|
||||
* **无忧升级**: 版本向下兼容
|
||||
|
||||
@@ -179,19 +180,23 @@ https://certd.handfree.work/
|
||||
|
||||
[50元专业版优惠券限时领取](https://app.handfree.work/subject/#/app/certd/product)
|
||||
|
||||
专业版特权对比
|
||||
|
||||
| 功能 | 免费版 | 专业版 |
|
||||
|---------|---------------------------------------|--------------------------------|
|
||||
| 免费证书申请 | 免费无限制 | 免费无限制 |
|
||||
| 证书域名数量 | 无限制 | 无限制 |
|
||||
| 证书流水线条数 | 无限制 | 无限制 |
|
||||
| 自动部署插件 | 阿里云CDN、腾讯云、七牛CDN、主机部署、宝塔、1Panel等大部分插件 | 群晖、威联通、proxmox等 |
|
||||
| 通知 | 邮件通知、自定义webhook | 邮件免配置、企微、钉钉、飞书、anpush、server酱等 |
|
||||
| 站点监控 | 限制1条 | 无限制 |
|
||||
| 批量操作 | 无 | 流水线模版,流水线复制,批量运行,批量设置通知、定时等 |
|
||||
| VIP群 | 无 | 可加,一对一技术支持,必要时可申请远程协助 |
|
||||
专业版、商业版特权对比
|
||||
|
||||
| 功能 | 免费版 | 专业版 | 商业版 |
|
||||
|---------|---------------------------------------|--------------------------------|---------------------------------|
|
||||
| 证书申请 | 无限制 | 无限制 | 无限制 |
|
||||
| 证书域名数量 | 无限制 | 无限制 | 无限制 |
|
||||
| 证书流水线条数 | 无限制 | 无限制 | 无限制 |
|
||||
| 自动部署插件 | 阿里云CDN、腾讯云、七牛CDN、主机部署、宝塔、1Panel等大部分插件 | 群晖、威联通、proxmox等 | 同专业版 |
|
||||
| 通知 | 邮件通知、自定义webhook | 邮件免配置、企微、钉钉、飞书、anpush、server酱等 | 同专业版 |
|
||||
| 站点监控 | 限制1条 | 无限制 | 无限制 |
|
||||
| 批量操作 | 无 | 流水线模版,流水线复制,批量运行,批量设置通知、定时等 | 同专业版 |
|
||||
| VIP群 | 无 | 可加,一对一技术支持,必要时可申请远程协助 | 商业版技术支持 |
|
||||
| 站点个性化 | 无 | 无 | 可自定义站点名称、Logo等,移除Certd元素,首页警告等 |
|
||||
| 套餐功能 | 无 | 无 | 支持配置套餐供用户购买 |
|
||||
| 数据统计 | 无 | 无 | 支持站点各类统计数据 |
|
||||
| 插件管理 | 无 | 无 | 支持公共EAB设置,插件选项配置 |
|
||||
| 是否可商用 | 不允许 | 不允许 | 可对外运营 |
|
||||
|
||||
## 九、贡献代码
|
||||
|
||||
|
||||
@@ -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_ + 配置项, 点号用_代替
|
||||
# #↓↓↓↓ ----------------------------- 如果忘记管理员密码,可以设置为true,docker 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
|
||||
@@ -3,6 +3,50 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.39.16](https://github.com/certd/certd/compare/v1.39.15...v1.39.16) (2026-05-13)
|
||||
|
||||
**Note:** Version bump only for package root
|
||||
|
||||
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复第三方登录彩虹登录不上的bug ([bae4f8e](https://github.com/certd/certd/commit/bae4f8e3209d9f9869ecbd7c01655383bac2fe21))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 优化申请时报错日志增加对应域名打印 ([d6e9e59](https://github.com/certd/certd/commit/d6e9e5987bd52ea12ee18745615486eadd4c87ff))
|
||||
* icon选择器增加一套logo集 ([fdd5848](https://github.com/certd/certd/commit/fdd5848df4055a6ee07dc5eabaaf6b718672882d))
|
||||
* **monitor/site:** 新增站点监控页面禁用启用、检查状态两个筛选条件 ([118c15d](https://github.com/certd/certd/commit/118c15d04633a6ef06f2d9e7a7849d20f596e02c))
|
||||
* **network:** 新增全局公共http请求 headers设置 ([aad9045](https://github.com/certd/certd/commit/aad9045de55e76cb2ad09cac74a7bd60a4b47124))
|
||||
|
||||
## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复阿里云订阅流水线创建对话框无法获取阿里订单列表的bug ([a362860](https://github.com/certd/certd/commit/a362860137bfb7072893c844fe775edc46070ee1))
|
||||
* 修复启动时报密钥备份不存在的问题 ([c966896](https://github.com/certd/certd/commit/c9668965226af6b54e0e576931dcba8b3d188ef3))
|
||||
|
||||
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **aliyun-access:** 添加阿里云密钥校验失败的错误处理 ([b75c625](https://github.com/certd/certd/commit/b75c625ddcc0b3110699d8e6175681ef157b25df))
|
||||
* cnameProvider域名支持设置子域名托管 ([7266af1](https://github.com/certd/certd/commit/7266af17491a98338022cfb18cfedfb93ca6ef8f))
|
||||
* **plugin-aliyun:** 过滤非CAS证书并优化日志信息 ([c4b01da](https://github.com/certd/certd/commit/c4b01da384bc40a241a673ea8bc01ca733c04d83))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **设置:** 添加首页启用开关配置 ([25ad1e6](https://github.com/certd/certd/commit/25ad1e6f861e43288cc8bd90d4903628e6faec29))
|
||||
* 新增agents.md ([aa176b0](https://github.com/certd/certd/commit/aa176b081a92837d2d6809d16546a8dfc2e5dd36))
|
||||
* **用户资料:** 新增手机号邮箱绑定功能 ([e0eb0e2](https://github.com/certd/certd/commit/e0eb0e21f6dae24b639c944f9aba2c90496ab1c0))
|
||||
* 域名注册过期时间获取再次优化 ([91a1b97](https://github.com/certd/certd/commit/91a1b9755066bf280e194dabf7c3a9f936e2643f))
|
||||
* **证书流水线:** 添加批量更新证书申请参数功能 ([63be1c1](https://github.com/certd/certd/commit/63be1c1cbd9b09a3b48f26130c296b1cedcca1ac))
|
||||
* 支持火山云vke ([bb46cb0](https://github.com/certd/certd/commit/bb46cb08f71f6ae921543f7e4a6c5f4e0190556e))
|
||||
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
|
||||
* **domain:** 添加域名过期时间同步进度显示功能 ([9d2937d](https://github.com/certd/certd/commit/9d2937dd4b14ffab73e9b096edd2aa8539811182))
|
||||
* **plugin-volcengine:** 支持火山引擎VKE部署插件 ([b8a64a6](https://github.com/certd/certd/commit/b8a64a6b5bf3691a47177de42bc49b798e795feb))
|
||||
|
||||
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
+16
-10
@@ -16,16 +16,22 @@
|
||||
****------------------****
|
||||
## 专业版特权对比
|
||||
|
||||
| 功能 | 免费版 | 专业版 |
|
||||
|---------|---------------------------------------|--------------------------------|
|
||||
| 证书申请 | 无限制 | 无限制 |
|
||||
| 证书域名数量 | 无限制 | 无限制 |
|
||||
| 证书流水线条数 | 无限制 | 无限制 |
|
||||
| 自动部署插件 | 阿里云CDN、腾讯云、七牛CDN、主机部署、宝塔、1Panel等大部分插件 | 群晖、威联通、proxmox等 |
|
||||
| 通知 | 邮件通知、自定义webhook | 邮件免配置、企微、钉钉、飞书、anpush、server酱等 |
|
||||
| 站点监控 | 限制1条 | 无限制 |
|
||||
| 批量操作 | 无 | 流水线模版,流水线复制,批量运行,批量设置通知、定时等 |
|
||||
| VIP群 | 无 | 可加,一对一技术支持,必要时可申请远程协助 |
|
||||
| 功能 | 免费版 | 专业版 | 商业版 |
|
||||
|---------|---------------------------------------|--------------------------------|---------------------------------|
|
||||
| 证书申请 | 无限制 | 无限制 | 无限制 |
|
||||
| 证书域名数量 | 无限制 | 无限制 | 无限制 |
|
||||
| 证书流水线条数 | 无限制 | 无限制 | 无限制 |
|
||||
| 自动部署插件 | 阿里云CDN、腾讯云、七牛CDN、主机部署、宝塔、1Panel等大部分插件 | 群晖、威联通、proxmox等 | 同专业版 |
|
||||
| 通知 | 邮件通知、自定义webhook | 邮件免配置、企微、钉钉、飞书、anpush、server酱等 | 同专业版 |
|
||||
| 站点监控 | 限制1条 | 无限制 | 无限制 |
|
||||
| 批量操作 | 无 | 流水线模版,流水线复制,批量运行,批量设置通知、定时等 | 同专业版 |
|
||||
| VIP群 | 无 | 可加,一对一技术支持,必要时可申请远程协助 | 商业版技术支持 |
|
||||
| 站点个性化 | 无 | 无 | 可自定义站点名称、Logo等,移除Certd元素,首页警告等 |
|
||||
| 套餐功能 | 无 | 无 | 支持配置套餐供用户购买 |
|
||||
| 数据统计 | 无 | 无 | 支持站点各类统计数据 |
|
||||
| 插件管理 | 无 | 无 | 支持公共EAB设置,插件选项配置 |
|
||||
| 是否可商用 | 不允许 | 不允许 | 可对外运营 |
|
||||
|
||||
|
||||
|
||||
## 专业版激活方式
|
||||
|
||||
@@ -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版本插件
|
||||

|
||||
按如下图填写配置
|
||||

|
||||
|
||||
|
||||
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
|
||||
```
|
||||
@@ -22,51 +22,3 @@
|
||||
可以查看最新版本号,以及所有版本的更新日志
|
||||
[CHANGELOG](../changelogs/CHANGELOG.md)
|
||||
|
||||
|
||||
## 自动升级配置
|
||||
|
||||
### 1. 方法一:使用watchtower监控
|
||||
|
||||
修改docker-compose.yaml文件增加如下配置, 使用watchtower监控自动升级
|
||||
```yaml
|
||||
services:
|
||||
certd:
|
||||
...
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
|
||||
# ↓↓↓↓ --------------------------------------------------------- 自动升级,上面certd的版本号要保持为latest
|
||||
certd-updater: # 添加 Watchtower 服务
|
||||
image: containrrr/watchtower:latest
|
||||
container_name: certd-updater
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
# 配置 自动更新
|
||||
environment:
|
||||
- WATCHTOWER_CLEANUP=true # 自动清理旧版本容器
|
||||
- WATCHTOWER_INCLUDE_STOPPED=false # 不更新已停止的容器
|
||||
- WATCHTOWER_LABEL_ENABLE=true # 根据容器标签进行更新
|
||||
- WATCHTOWER_POLL_INTERVAL=600 # 每 10 分钟检查一次更新
|
||||
|
||||
```
|
||||
|
||||
|
||||
### 2. 方法二:使用Certd版本监控功能
|
||||
|
||||
选择Github-检查Release版本插件
|
||||

|
||||
按如下图填写配置
|
||||

|
||||
|
||||
|
||||
检测到新版本后执行宿主机升级命令:
|
||||
|
||||
```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,5 +1,5 @@
|
||||
# 任务插件
|
||||
共 `132` 款任务插件
|
||||
共 `131` 款任务插件
|
||||
## 1. 证书申请
|
||||
|
||||
| 序号 | 名称 | 说明 |
|
||||
|
||||
+1
-1
@@ -9,5 +9,5 @@
|
||||
}
|
||||
},
|
||||
"npmClient": "pnpm",
|
||||
"version": "1.39.12"
|
||||
"version": "1.40.0"
|
||||
}
|
||||
|
||||
+2
-1
@@ -9,6 +9,7 @@
|
||||
"@lerna-lite/run": "^3.9.3",
|
||||
"@lerna-lite/version": "^3.9.3",
|
||||
"axios": "^1.9.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"medium-zoom": "^1.1.0",
|
||||
"vitepress": "^2.0.0-alpha.4",
|
||||
"vitepress-plugin-lightbox": "^1.0.2"
|
||||
@@ -35,7 +36,7 @@
|
||||
"docs:dev": "vitepress dev docs",
|
||||
"docs:build": "pnpm run copylogs && vitepress build docs",
|
||||
"docs:preview": "vitepress preview docs",
|
||||
"test:unit": "pnpm -r --workspace-concurrency=1 run test:unit",
|
||||
"test:unit": "cross-env NODE_ENV=unittest pnpm -r --workspace-concurrency=1 run test:unit",
|
||||
"pub": "echo 1",
|
||||
"dev": "pnpm run -r --parallel compile ",
|
||||
"pub_all":"pnpm run -r --parallel pub ",
|
||||
|
||||
@@ -3,6 +3,30 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.40.0](https://github.com/publishlab/node-acme-client/compare/v1.39.16...v1.40.0) (2026-05-14)
|
||||
|
||||
**Note:** Version bump only for package @certd/acme-client
|
||||
|
||||
## [1.39.16](https://github.com/publishlab/node-acme-client/compare/v1.39.15...v1.39.16) (2026-05-13)
|
||||
|
||||
**Note:** Version bump only for package @certd/acme-client
|
||||
|
||||
## [1.39.15](https://github.com/publishlab/node-acme-client/compare/v1.39.14...v1.39.15) (2026-05-13)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 优化申请时报错日志增加对应域名打印 ([d6e9e59](https://github.com/publishlab/node-acme-client/commit/d6e9e5987bd52ea12ee18745615486eadd4c87ff))
|
||||
|
||||
## [1.39.14](https://github.com/publishlab/node-acme-client/compare/v1.39.13...v1.39.14) (2026-05-11)
|
||||
|
||||
**Note:** Version bump only for package @certd/acme-client
|
||||
|
||||
## [1.39.13](https://github.com/publishlab/node-acme-client/compare/v1.39.12...v1.39.13) (2026-05-10)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/publishlab/node-acme-client/commit/4755216505ad18555a50da9d8008c2207c48df86))
|
||||
|
||||
## [1.39.12](https://github.com/publishlab/node-acme-client/compare/v1.39.11...v1.39.12) (2026-04-29)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"description": "Simple and unopinionated ACME client",
|
||||
"private": false,
|
||||
"author": "nmorsman",
|
||||
"version": "1.39.12",
|
||||
"version": "1.40.0",
|
||||
"type": "module",
|
||||
"module": "./dist/index.js",
|
||||
"main": "./dist/index.js",
|
||||
@@ -18,7 +18,7 @@
|
||||
"types"
|
||||
],
|
||||
"dependencies": {
|
||||
"@certd/basic": "^1.39.12",
|
||||
"@certd/basic": "^1.40.0",
|
||||
"@peculiar/x509": "^1.11.0",
|
||||
"asn1js": "^3.0.5",
|
||||
"axios": "^1.9.0",
|
||||
@@ -35,10 +35,12 @@
|
||||
"@typescript-eslint/parser": "^8.26.1",
|
||||
"chai": "^4.4.1",
|
||||
"chai-as-promised": "^7.1.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"esmock": "^2.7.5",
|
||||
"jsdoc-to-markdown": "^8.0.1",
|
||||
"mocha": "^10.6.0",
|
||||
"nock": "^13.5.4",
|
||||
@@ -55,7 +57,7 @@
|
||||
"prepublishOnly": "npm run build && npm run build-docs",
|
||||
"test": "mocha -t 60000 \"test/setup.js\" \"test/**/*.spec.js\"",
|
||||
"before-test:unit": "node -e \"const fs=require('fs');fs.rmSync('dist-test',{recursive:true,force:true});fs.rmSync('tsconfig.test.tsbuildinfo',{force:true});\"",
|
||||
"test:unit": "npm run before-test:unit && tsc -p tsconfig.test.json --skipLibCheck && mocha -t 60000 \"dist-test/**/*.test.js\"",
|
||||
"test:unit": "cross-env NODE_ENV=unittest npm run before-test:unit && cross-env NODE_ENV=unittest tsc -p tsconfig.test.json --skipLibCheck && cross-env NODE_ENV=unittest mocha -t 60000 \"dist-test/**/*.test.js\"",
|
||||
"pub": "npm publish",
|
||||
"compile": "tsc --skipLibCheck --watch"
|
||||
},
|
||||
@@ -74,5 +76,5 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/publishlab/node-acme-client/issues"
|
||||
},
|
||||
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
|
||||
"gitHead": "1c6dc169ac04fd09ef94404a912a15cbb17e1452"
|
||||
}
|
||||
|
||||
@@ -167,6 +167,7 @@ export default async (client, userOpts) => {
|
||||
await client.completeChallenge(challenge);
|
||||
}catch (e) {
|
||||
await deactivateAuth(e);
|
||||
e.message = `[${d}] ${e.message || "completeChallenge error"}`;
|
||||
throw e;
|
||||
}
|
||||
challengeCompleted = true;
|
||||
@@ -178,6 +179,7 @@ export default async (client, userOpts) => {
|
||||
} catch (e) {
|
||||
log(`[auto] [${d}] challengeCreateFn threw error: ${e.message || e}`);
|
||||
await deactivateAuth(e);
|
||||
e.message = `[${d}] ${e.message || "challengeCreateFn error"}`;
|
||||
throw e;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,30 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.40.0](https://github.com/certd/certd/compare/v1.39.16...v1.40.0) (2026-05-14)
|
||||
|
||||
**Note:** Version bump only for package @certd/basic
|
||||
|
||||
## [1.39.16](https://github.com/certd/certd/compare/v1.39.15...v1.39.16) (2026-05-13)
|
||||
|
||||
**Note:** Version bump only for package @certd/basic
|
||||
|
||||
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **network:** 新增全局公共http请求 headers设置 ([aad9045](https://github.com/certd/certd/commit/aad9045de55e76cb2ad09cac74a7bd60a4b47124))
|
||||
|
||||
## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11)
|
||||
|
||||
**Note:** Version bump only for package @certd/basic
|
||||
|
||||
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
|
||||
|
||||
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
@@ -1 +1 @@
|
||||
23:06
|
||||
00:44
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/basic",
|
||||
"private": false,
|
||||
"version": "1.39.12",
|
||||
"version": "1.40.0",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
@@ -13,7 +13,7 @@
|
||||
"dev-build": "npm run build",
|
||||
"preview": "vite preview",
|
||||
"test": "mocha --loader=ts-node/esm",
|
||||
"test:unit": "mocha --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
|
||||
"test:unit": "cross-env NODE_ENV=unittest mocha --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
|
||||
"pub": "npm publish",
|
||||
"compile": "tsc --skipLibCheck --watch"
|
||||
},
|
||||
@@ -40,9 +40,11 @@
|
||||
"@typescript-eslint/eslint-plugin": "^8.26.1",
|
||||
"@typescript-eslint/parser": "^8.26.1",
|
||||
"chai": "4.3.10",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.41.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"esmock": "^2.7.5",
|
||||
"mocha": "^10.2.0",
|
||||
"prettier": "^2.8.8",
|
||||
"rimraf": "^5.0.5",
|
||||
@@ -50,5 +52,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
|
||||
"gitHead": "1c6dc169ac04fd09ef94404a912a15cbb17e1452"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,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",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -26,7 +26,7 @@ export class HttpError extends Error {
|
||||
return;
|
||||
}
|
||||
|
||||
let message = error?.message || error?.response.statusText || error?.code;
|
||||
let message = error?.message || error?.response?.statusText || error?.code;
|
||||
if (message && typeof message === "string" && message.indexOf) {
|
||||
for (const key in errorMap) {
|
||||
if (message.indexOf(key) > -1) {
|
||||
@@ -82,6 +82,7 @@ export class HttpError extends Error {
|
||||
export const HttpCommonError = HttpError;
|
||||
|
||||
let defaultAgents = createAgent();
|
||||
let defaultHeaders: Record<string, string> = {};
|
||||
|
||||
export function setGlobalProxy(opts: { httpProxy?: string; httpsProxy?: string }) {
|
||||
logger.info("setGlobalProxy:", opts);
|
||||
@@ -92,6 +93,15 @@ export function getGlobalAgents() {
|
||||
return defaultAgents;
|
||||
}
|
||||
|
||||
export function setGlobalHeaders(headers: Record<string, string> = {}) {
|
||||
logger.info("setGlobalHeaders:", Object.keys(headers));
|
||||
defaultHeaders = { ...headers };
|
||||
}
|
||||
|
||||
export function getGlobalHeaders() {
|
||||
return defaultHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 创建请求实例
|
||||
*/
|
||||
@@ -148,6 +158,12 @@ export function createAxiosService({ logger }: { logger: ILogger }) {
|
||||
config.httpsAgent = agents.httpsAgent;
|
||||
config.httpAgent = agents.httpAgent;
|
||||
|
||||
if (Object.keys(defaultHeaders).length > 0) {
|
||||
const headers = AxiosHeaders.from(defaultHeaders);
|
||||
headers.set(config.headers || {});
|
||||
config.headers = headers;
|
||||
}
|
||||
|
||||
// const agent = new https.Agent({
|
||||
// rejectUnauthorized: false // 允许自签名证书
|
||||
// });
|
||||
@@ -267,7 +283,7 @@ export function createAxiosService({ logger }: { logger: ILogger }) {
|
||||
logger.error(`请求出错:${errorMessage} status:${status},statusText:${error.response?.statusText || error.code},url:${error.config?.url},method:${error.config?.method}。`);
|
||||
logger.error("返回数据:", JSON.stringify(error.response?.data));
|
||||
if (error.response?.data) {
|
||||
const message = error.response.data.message || error.response.data.msg || error.response.data.error;
|
||||
const message = error.response?.data?.message || error.response?.data?.msg || error.response?.data?.error;
|
||||
if (typeof message === "string") {
|
||||
error.message = message;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,32 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.40.0](https://github.com/certd/certd/compare/v1.39.16...v1.40.0) (2026-05-14)
|
||||
|
||||
**Note:** Version bump only for package @certd/pipeline
|
||||
|
||||
## [1.39.16](https://github.com/certd/certd/compare/v1.39.15...v1.39.16) (2026-05-13)
|
||||
|
||||
**Note:** Version bump only for package @certd/pipeline
|
||||
|
||||
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
|
||||
|
||||
**Note:** Version bump only for package @certd/pipeline
|
||||
|
||||
## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11)
|
||||
|
||||
**Note:** Version bump only for package @certd/pipeline
|
||||
|
||||
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* cnameProvider域名支持设置子域名托管 ([7266af1](https://github.com/certd/certd/commit/7266af17491a98338022cfb18cfedfb93ca6ef8f))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
|
||||
|
||||
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/pipeline",
|
||||
"private": false,
|
||||
"version": "1.39.12",
|
||||
"version": "1.40.0",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
@@ -14,13 +14,13 @@
|
||||
"build3": "rollup -c",
|
||||
"preview": "vite preview",
|
||||
"test": "mocha --loader=ts-node/esm",
|
||||
"test:unit": "mocha --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
|
||||
"test:unit": "cross-env NODE_ENV=unittest mocha --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
|
||||
"pub": "npm publish",
|
||||
"compile": "tsc --skipLibCheck --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@certd/basic": "^1.39.12",
|
||||
"@certd/plus-core": "^1.39.12",
|
||||
"@certd/basic": "^1.40.0",
|
||||
"@certd/plus-core": "^1.40.0",
|
||||
"dayjs": "^1.11.7",
|
||||
"lodash-es": "^4.17.21",
|
||||
"reflect-metadata": "^0.1.13"
|
||||
@@ -37,9 +37,11 @@
|
||||
"@typescript-eslint/eslint-plugin": "^8.26.1",
|
||||
"@typescript-eslint/parser": "^8.26.1",
|
||||
"chai": "4.3.10",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.41.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"esmock": "^2.7.5",
|
||||
"mocha": "^10.2.0",
|
||||
"prettier": "^2.8.8",
|
||||
"rimraf": "^5.0.5",
|
||||
@@ -47,5 +49,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
|
||||
"gitHead": "1c6dc169ac04fd09ef94404a912a15cbb17e1452"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { IAccess } from "../access/index.js";
|
||||
export type CnameProvider = {
|
||||
id: any;
|
||||
domain: string;
|
||||
subdomain?: string;
|
||||
title?: string;
|
||||
dnsProviderType?: string;
|
||||
access?: IAccess;
|
||||
|
||||
@@ -3,6 +3,24 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.40.0](https://github.com/certd/certd/compare/v1.39.16...v1.40.0) (2026-05-14)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-huawei
|
||||
|
||||
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-huawei
|
||||
|
||||
## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-huawei
|
||||
|
||||
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
|
||||
|
||||
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-huawei
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/lib-huawei",
|
||||
"private": false,
|
||||
"version": "1.39.12",
|
||||
"version": "1.40.0",
|
||||
"main": "./dist/bundle.js",
|
||||
"module": "./dist/bundle.js",
|
||||
"types": "./dist/d/index.d.ts",
|
||||
@@ -11,7 +11,7 @@
|
||||
"build": "npm run before-build && rollup -c ",
|
||||
"dev-build": "npm run build",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "echo no unit tests",
|
||||
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
|
||||
"pub": "npm publish"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -22,8 +22,10 @@
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^8.26.1",
|
||||
"@typescript-eslint/parser": "^8.26.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"esmock": "^2.7.5",
|
||||
"prettier": "^2.8.8",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
|
||||
"gitHead": "bae5a04dcc0a679c290a9805c3ac4a6020eb6ec0"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,24 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.40.0](https://github.com/certd/certd/compare/v1.39.16...v1.40.0) (2026-05-14)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-iframe
|
||||
|
||||
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-iframe
|
||||
|
||||
## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-iframe
|
||||
|
||||
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
|
||||
|
||||
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-iframe
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/lib-iframe",
|
||||
"private": false,
|
||||
"version": "1.39.12",
|
||||
"version": "1.40.0",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
@@ -14,7 +14,7 @@
|
||||
"build3": "rollup -c",
|
||||
"build2": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "echo no unit tests",
|
||||
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
|
||||
"pub": "npm publish"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -24,13 +24,15 @@
|
||||
"@types/chai": "^4.3.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.26.1",
|
||||
"@typescript-eslint/parser": "^8.26.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.24.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"esmock": "^2.7.5",
|
||||
"prettier": "^2.8.8",
|
||||
"rimraf": "^5.0.5",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
|
||||
"gitHead": "bae5a04dcc0a679c290a9805c3ac4a6020eb6ec0"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,24 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.40.0](https://github.com/certd/certd/compare/v1.39.16...v1.40.0) (2026-05-14)
|
||||
|
||||
**Note:** Version bump only for package @certd/jdcloud
|
||||
|
||||
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
|
||||
|
||||
**Note:** Version bump only for package @certd/jdcloud
|
||||
|
||||
## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11)
|
||||
|
||||
**Note:** Version bump only for package @certd/jdcloud
|
||||
|
||||
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
|
||||
|
||||
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
|
||||
|
||||
**Note:** Version bump only for package @certd/jdcloud
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@certd/jdcloud",
|
||||
"version": "1.39.12",
|
||||
"version": "1.40.0",
|
||||
"description": "jdcloud openApi sdk",
|
||||
"main": "./dist/bundle.js",
|
||||
"module": "./dist/bundle.js",
|
||||
@@ -8,7 +8,7 @@
|
||||
"scripts": {
|
||||
"build": "rollup -c ",
|
||||
"dev-build": "npm run build",
|
||||
"test:unit": "echo no unit tests",
|
||||
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
|
||||
"pub": "npm publish"
|
||||
},
|
||||
"author": "",
|
||||
@@ -30,7 +30,8 @@
|
||||
"@typescript-eslint/parser": "^8.26.1",
|
||||
"chai": "^4.1.2",
|
||||
"config": "^1.30.0",
|
||||
"cross-env": "^5.1.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"esmock": "^2.7.5",
|
||||
"js-yaml": "^3.11.0",
|
||||
"mocha": "^5.0.0",
|
||||
"prettier": "^2.8.8",
|
||||
@@ -57,5 +58,5 @@
|
||||
"fetch"
|
||||
]
|
||||
},
|
||||
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
|
||||
"gitHead": "bae5a04dcc0a679c290a9805c3ac4a6020eb6ec0"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,28 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.40.0](https://github.com/certd/certd/compare/v1.39.16...v1.40.0) (2026-05-14)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-k8s
|
||||
|
||||
## [1.39.16](https://github.com/certd/certd/compare/v1.39.15...v1.39.16) (2026-05-13)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-k8s
|
||||
|
||||
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-k8s
|
||||
|
||||
## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-k8s
|
||||
|
||||
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
|
||||
|
||||
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-k8s
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/lib-k8s",
|
||||
"private": false,
|
||||
"version": "1.39.12",
|
||||
"version": "1.40.0",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
@@ -14,25 +14,27 @@
|
||||
"build3": "rollup -c",
|
||||
"build2": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "echo no unit tests",
|
||||
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
|
||||
"pub": "npm publish",
|
||||
"compile": "tsc --skipLibCheck --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@certd/basic": "^1.39.12",
|
||||
"@certd/basic": "^1.40.0",
|
||||
"@kubernetes/client-node": "0.21.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.3.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.26.1",
|
||||
"@typescript-eslint/parser": "^8.26.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.24.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"esmock": "^2.7.5",
|
||||
"prettier": "^2.8.8",
|
||||
"rimraf": "^5.0.5",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
|
||||
"gitHead": "1c6dc169ac04fd09ef94404a912a15cbb17e1452"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,35 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.40.0](https://github.com/certd/certd/compare/v1.39.16...v1.40.0) (2026-05-14)
|
||||
|
||||
### Features
|
||||
|
||||
* 彩虹登录支持选择多种登录方式 ([7aa0c7e](https://github.com/certd/certd/commit/7aa0c7e491fe660abb62e68792ff5474f19bd5b8))
|
||||
|
||||
## [1.39.16](https://github.com/certd/certd/compare/v1.39.15...v1.39.16) (2026-05-13)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-server
|
||||
|
||||
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **network:** 新增全局公共http请求 headers设置 ([aad9045](https://github.com/certd/certd/commit/aad9045de55e76cb2ad09cac74a7bd60a4b47124))
|
||||
|
||||
## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复启动时报密钥备份不存在的问题 ([c966896](https://github.com/certd/certd/commit/c9668965226af6b54e0e576931dcba8b3d188ef3))
|
||||
|
||||
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **设置:** 添加首页启用开关配置 ([25ad1e6](https://github.com/certd/certd/commit/25ad1e6f861e43288cc8bd90d4903628e6faec29))
|
||||
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
|
||||
|
||||
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@certd/lib-server",
|
||||
"version": "1.39.12",
|
||||
"version": "1.40.0",
|
||||
"description": "midway with flyway, sql upgrade way ",
|
||||
"private": false,
|
||||
"type": "module",
|
||||
@@ -12,7 +12,7 @@
|
||||
"build": "npm run before-build && tsc --skipLibCheck",
|
||||
"dev-build": "npm run build",
|
||||
"test": "midway-bin test --ts -V",
|
||||
"test:unit": "mocha --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
|
||||
"test:unit": "cross-env NODE_ENV=unittest mocha --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
|
||||
"test1": "midway-bin test --ts -V -f test/blank.test.ts -t 'hash-check'",
|
||||
"cov": "midway-bin cov --ts",
|
||||
"lint": "mwts check",
|
||||
@@ -29,11 +29,11 @@
|
||||
],
|
||||
"license": "AGPL",
|
||||
"dependencies": {
|
||||
"@certd/acme-client": "^1.39.12",
|
||||
"@certd/basic": "^1.39.12",
|
||||
"@certd/pipeline": "^1.39.12",
|
||||
"@certd/plugin-lib": "^1.39.12",
|
||||
"@certd/plus-core": "^1.39.12",
|
||||
"@certd/acme-client": "^1.40.0",
|
||||
"@certd/basic": "^1.40.0",
|
||||
"@certd/pipeline": "^1.40.0",
|
||||
"@certd/plugin-lib": "^1.40.0",
|
||||
"@certd/plus-core": "^1.40.0",
|
||||
"@midwayjs/cache": "3.14.0",
|
||||
"@midwayjs/core": "3.20.11",
|
||||
"@midwayjs/i18n": "3.20.13",
|
||||
@@ -44,7 +44,6 @@
|
||||
"@midwayjs/upload": "3.20.13",
|
||||
"@midwayjs/validate": "3.20.13",
|
||||
"better-sqlite3": "^11.1.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"dayjs": "^1.11.7",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mwts": "^1.3.0",
|
||||
@@ -57,9 +56,11 @@
|
||||
"@types/node": "^18",
|
||||
"@typescript-eslint/eslint-plugin": "^8.26.1",
|
||||
"@typescript-eslint/parser": "^8.26.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.24.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"esmock": "^2.7.5",
|
||||
"mocha": "^10.2.0",
|
||||
"prettier": "^2.8.8",
|
||||
"rimraf": "^5.0.5",
|
||||
@@ -68,5 +69,5 @@
|
||||
"typeorm": "^0.3.11",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
|
||||
"gitHead": "1c6dc169ac04fd09ef94404a912a15cbb17e1452"
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ export class SysPublicSettings extends BaseSettings {
|
||||
type: string;
|
||||
title: string;
|
||||
addonId: number;
|
||||
icon?: string;
|
||||
}> = {};
|
||||
|
||||
notice?: string;
|
||||
@@ -80,6 +81,7 @@ export class SysPrivateSettings extends BaseSettings {
|
||||
|
||||
httpsProxy? = '';
|
||||
httpProxy? = '';
|
||||
commonHeaders?: string = '';
|
||||
|
||||
reverseProxies?: Record<string, string> = {};
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { SysSettingsEntity } from '../entity/sys-settings.js';
|
||||
import { BaseSettings, SysInstallInfo, SysPrivateSettings, SysPublicSettings, SysSecret, SysSecretBackup } from './models.js';
|
||||
|
||||
import { getAllSslProviderDomains, setSslProviderReverseProxies, setWalkFromAuthoritative } from '@certd/acme-client';
|
||||
import { cache, logger, mergeUtils, setGlobalProxy } from '@certd/basic';
|
||||
import { cache, logger, mergeUtils, setGlobalHeaders, setGlobalProxy } from '@certd/basic';
|
||||
import { isPlus } from '@certd/plus-core';
|
||||
import * as dns from 'node:dns';
|
||||
import { BaseService, setAdminMode } from '../../../basic/index.js';
|
||||
@@ -167,6 +167,7 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
|
||||
httpsProxy: privateSetting.httpsProxy,
|
||||
};
|
||||
setGlobalProxy(opts);
|
||||
setGlobalHeaders(this.parseKeyValueText(privateSetting.commonHeaders));
|
||||
|
||||
if (privateSetting.dnsResultOrder) {
|
||||
dns.setDefaultResultOrder(privateSetting.dnsResultOrder as any);
|
||||
@@ -185,12 +186,12 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
|
||||
|
||||
}
|
||||
|
||||
setEnvironmentVars(vars: string) {
|
||||
const envVars = {}
|
||||
if (typeof vars !== 'string') {
|
||||
vars = ""
|
||||
parseKeyValueText(text: string) {
|
||||
const values = {};
|
||||
if (typeof text !== 'string') {
|
||||
text = "";
|
||||
}
|
||||
vars.split('\n').forEach(line => {
|
||||
text.split('\n').forEach(line => {
|
||||
line = line.trim();
|
||||
if (!line || line.startsWith('#')) {
|
||||
return
|
||||
@@ -204,11 +205,18 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
|
||||
return
|
||||
}
|
||||
|
||||
const [key, value] = line.split('=');
|
||||
const eqIndex = line.indexOf('=');
|
||||
const key = line.substring(0, eqIndex).trim();
|
||||
const value = line.substring(eqIndex + 1).trim();
|
||||
if (key && value) {
|
||||
envVars[key.trim()] = value.trim();
|
||||
values[key] = value;
|
||||
}
|
||||
});
|
||||
return values;
|
||||
}
|
||||
|
||||
setEnvironmentVars(vars: string) {
|
||||
const envVars = this.parseKeyValueText(vars);
|
||||
//先删除旧环境变量
|
||||
if (lastSaveEnvVars) {
|
||||
for (const key in lastSaveEnvVars) {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import assert from "assert";
|
||||
import { AccessService } from "./access-service.js";
|
||||
|
||||
describe("AccessService", () => {
|
||||
it("does not write id into access setting when updating selected fields", async () => {
|
||||
let updateParam: any;
|
||||
const service = new AccessService();
|
||||
service.info = async () => ({
|
||||
id: 12,
|
||||
type: "eab",
|
||||
} as any);
|
||||
service.decryptAccessEntity = () => ({
|
||||
kid: "kid-1",
|
||||
});
|
||||
service.update = async (param: any) => {
|
||||
updateParam = param;
|
||||
return param;
|
||||
};
|
||||
|
||||
await service.updateAccess({
|
||||
id: 12,
|
||||
accountKey: "account-key",
|
||||
});
|
||||
|
||||
assert.deepEqual(JSON.parse(updateParam.setting), {
|
||||
kid: "kid-1",
|
||||
accountKey: "account-key",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -123,6 +123,25 @@ export class AccessService extends BaseService<AccessEntity> {
|
||||
return await super.update(param);
|
||||
}
|
||||
|
||||
async updateAccess(access: any) {
|
||||
const oldEntity = await this.info(access.id);
|
||||
if (oldEntity == null) {
|
||||
throw new ValidateException('该授权配置不存在,请确认是否已被删除');
|
||||
}
|
||||
const setting = this.decryptAccessEntity(oldEntity);
|
||||
for (const key of Object.keys(access)) {
|
||||
if (key === 'id') {
|
||||
continue;
|
||||
}
|
||||
setting[key] = access[key];
|
||||
}
|
||||
return await this.update({
|
||||
id: access.id,
|
||||
type: oldEntity.type,
|
||||
setting: JSON.stringify(setting),
|
||||
});
|
||||
}
|
||||
|
||||
async getSimpleInfo(id: number) {
|
||||
const entity = await this.info(id);
|
||||
if (entity == null) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Init, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import { Encryptor, SysSecret, SysSettingsService } from '../../../system/index.js';
|
||||
|
||||
/**
|
||||
@@ -12,8 +12,7 @@ export class EncryptService {
|
||||
@Inject()
|
||||
sysSettingService: SysSettingsService;
|
||||
|
||||
@Init()
|
||||
async init() {
|
||||
async doInit() {
|
||||
const secret: SysSecret = await this.sysSettingService.getSecret();
|
||||
this.encryptor = new Encryptor(secret.encryptSecret);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,24 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.40.0](https://github.com/certd/certd/compare/v1.39.16...v1.40.0) (2026-05-14)
|
||||
|
||||
**Note:** Version bump only for package @certd/midway-flyway-js
|
||||
|
||||
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
|
||||
|
||||
**Note:** Version bump only for package @certd/midway-flyway-js
|
||||
|
||||
## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11)
|
||||
|
||||
**Note:** Version bump only for package @certd/midway-flyway-js
|
||||
|
||||
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
|
||||
|
||||
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
|
||||
|
||||
**Note:** Version bump only for package @certd/midway-flyway-js
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@certd/midway-flyway-js",
|
||||
"version": "1.39.12",
|
||||
"version": "1.40.0",
|
||||
"description": "midway with flyway, sql upgrade way ",
|
||||
"private": false,
|
||||
"type": "module",
|
||||
@@ -13,7 +13,7 @@
|
||||
"dev-build": "npm run build",
|
||||
"test": "midway-bin test --ts -V",
|
||||
"test1": "midway-bin test --ts -V -f test/blank.test.ts -t 'hash-check'",
|
||||
"test:unit": "echo no unit tests",
|
||||
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
|
||||
"cov": "midway-bin cov --ts",
|
||||
"prepublish": "npm run build",
|
||||
"pub": "npm publish"
|
||||
@@ -36,16 +36,18 @@
|
||||
"@types/node": "^18",
|
||||
"@typescript-eslint/eslint-plugin": "^8.26.1",
|
||||
"@typescript-eslint/parser": "^8.26.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.24.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"esmock": "^2.7.5",
|
||||
"prettier": "^2.8.8",
|
||||
"rimraf": "^5.0.5",
|
||||
"tslib": "^2.8.1",
|
||||
"typeorm": "^0.3.11",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
|
||||
"gitHead": "bae5a04dcc0a679c290a9805c3ac4a6020eb6ec0"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,28 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.40.0](https://github.com/certd/certd/compare/v1.39.16...v1.40.0) (2026-05-14)
|
||||
|
||||
**Note:** Version bump only for package @certd/plugin-cert
|
||||
|
||||
## [1.39.16](https://github.com/certd/certd/compare/v1.39.15...v1.39.16) (2026-05-13)
|
||||
|
||||
**Note:** Version bump only for package @certd/plugin-cert
|
||||
|
||||
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
|
||||
|
||||
**Note:** Version bump only for package @certd/plugin-cert
|
||||
|
||||
## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11)
|
||||
|
||||
**Note:** Version bump only for package @certd/plugin-cert
|
||||
|
||||
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
|
||||
|
||||
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
|
||||
|
||||
**Note:** Version bump only for package @certd/plugin-cert
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/plugin-cert",
|
||||
"private": false,
|
||||
"version": "1.39.12",
|
||||
"version": "1.40.0",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -13,15 +13,15 @@
|
||||
"build3": "rollup -c",
|
||||
"build2": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "echo no unit tests",
|
||||
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
|
||||
"pub": "npm publish",
|
||||
"compile": "tsc --skipLibCheck --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@certd/acme-client": "^1.39.12",
|
||||
"@certd/basic": "^1.39.12",
|
||||
"@certd/pipeline": "^1.39.12",
|
||||
"@certd/plugin-lib": "^1.39.12",
|
||||
"@certd/acme-client": "^1.40.0",
|
||||
"@certd/basic": "^1.40.0",
|
||||
"@certd/pipeline": "^1.40.0",
|
||||
"@certd/plugin-lib": "^1.40.0",
|
||||
"psl": "^1.9.0",
|
||||
"punycode.js": "^2.3.1"
|
||||
},
|
||||
@@ -31,13 +31,15 @@
|
||||
"@typescript-eslint/eslint-plugin": "^8.26.1",
|
||||
"@typescript-eslint/parser": "^8.26.1",
|
||||
"chai": "^4.3.6",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.24.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"esmock": "^2.7.5",
|
||||
"mocha": "^10.1.0",
|
||||
"prettier": "^2.8.8",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
|
||||
"gitHead": "1c6dc169ac04fd09ef94404a912a15cbb17e1452"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,28 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.40.0](https://github.com/certd/certd/compare/v1.39.16...v1.40.0) (2026-05-14)
|
||||
|
||||
**Note:** Version bump only for package @certd/plugin-lib
|
||||
|
||||
## [1.39.16](https://github.com/certd/certd/compare/v1.39.15...v1.39.16) (2026-05-13)
|
||||
|
||||
**Note:** Version bump only for package @certd/plugin-lib
|
||||
|
||||
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
|
||||
|
||||
**Note:** Version bump only for package @certd/plugin-lib
|
||||
|
||||
## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11)
|
||||
|
||||
**Note:** Version bump only for package @certd/plugin-lib
|
||||
|
||||
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
|
||||
|
||||
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
|
||||
|
||||
**Note:** Version bump only for package @certd/plugin-lib
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/plugin-lib",
|
||||
"private": false,
|
||||
"version": "1.39.12",
|
||||
"version": "1.40.0",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -13,7 +13,7 @@
|
||||
"build3": "rollup -c",
|
||||
"build2": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "mocha --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
|
||||
"test:unit": "cross-env NODE_ENV=unittest mocha --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
|
||||
"pub": "npm publish",
|
||||
"compile": "tsc --skipLibCheck --watch"
|
||||
},
|
||||
@@ -23,10 +23,10 @@
|
||||
"@alicloud/pop-core": "^1.7.10",
|
||||
"@alicloud/tea-util": "^1.4.11",
|
||||
"@aws-sdk/client-s3": "^3.964.0",
|
||||
"@certd/acme-client": "^1.39.12",
|
||||
"@certd/basic": "^1.39.12",
|
||||
"@certd/pipeline": "^1.39.12",
|
||||
"@certd/plus-core": "^1.39.12",
|
||||
"@certd/acme-client": "^1.40.0",
|
||||
"@certd/basic": "^1.40.0",
|
||||
"@certd/pipeline": "^1.40.0",
|
||||
"@certd/plus-core": "^1.40.0",
|
||||
"@kubernetes/client-node": "0.21.0",
|
||||
"ali-oss": "^6.22.0",
|
||||
"basic-ftp": "^5.0.5",
|
||||
@@ -50,14 +50,16 @@
|
||||
"@typescript-eslint/eslint-plugin": "^8.26.1",
|
||||
"@typescript-eslint/parser": "^8.26.1",
|
||||
"chai": "^4.3.6",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.24.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"esmock": "^2.7.5",
|
||||
"mocha": "^10.1.0",
|
||||
"prettier": "^2.8.8",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
|
||||
"gitHead": "1c6dc169ac04fd09ef94404a912a15cbb17e1452"
|
||||
}
|
||||
|
||||
+10
-5
@@ -1,4 +1,4 @@
|
||||
FROM node:22-alpine AS builder
|
||||
FROM node:22-alpine3.21 AS builder
|
||||
|
||||
# RUN apk add build-base
|
||||
# RUN wget -O - https://github.com/jemalloc/jemalloc/releases/download/5.3.0/jemalloc-5.3.0.tar.bz2 | tar -xj && \
|
||||
@@ -10,17 +10,22 @@ FROM node:22-alpine AS builder
|
||||
|
||||
WORKDIR /workspace/
|
||||
COPY . /workspace/
|
||||
# armv7 目前只能用node18, pnpm9不支持node18,所以pnpm只能用8.15.7版本
|
||||
# https://github.com/nodejs/docker-node/issues/1946
|
||||
RUN npm install -g pnpm
|
||||
# pnpm v11打包会报错([ERR_PNPM_IGNORED_BUILDS] Ignored build scripts),暂时固定10.33.4版本。
|
||||
# https://pnpm.io/zh/migration
|
||||
RUN npm install -g pnpm@10.33.4
|
||||
|
||||
#RUN cd /workspace/certd-client && pnpm install && npm run build
|
||||
RUN cp /workspace/certd-client/dist/* /workspace/certd-server/public/ -rf
|
||||
RUN cd /workspace/certd-server && pnpm install && npm run build-on-docker
|
||||
|
||||
# RUN cd /workspace/certd-server && \
|
||||
# pnpm install --ignore-scripts && \
|
||||
# yes | pnpm approve-builds && \
|
||||
# pnpm rebuild && \
|
||||
# npm run build-on-docker
|
||||
|
||||
|
||||
FROM node:22-alpine
|
||||
FROM node:22-alpine3.21
|
||||
EXPOSE 7001
|
||||
EXPOSE 7002
|
||||
|
||||
|
||||
@@ -3,6 +3,55 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.40.0](https://github.com/certd/certd/compare/v1.39.16...v1.40.0) (2026-05-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复第三方登录丢失state时无法在用户信息页面绑定第三方账号的bug ([45dedf5](https://github.com/certd/certd/commit/45dedf5bc779fea852e1f33dda4f31db2765633c))
|
||||
* 修复群晖授权没有显示设备id输入框的bug ([2f172b5](https://github.com/certd/certd/commit/2f172b56e9411303ca15138d827bdb9bafdae4d1))
|
||||
* 修复自动注册后没有跳转到控制台的bug ([4681ec9](https://github.com/certd/certd/commit/4681ec90088a3eb665427b2ac4047ec5ccefd7b3))
|
||||
|
||||
### Features
|
||||
|
||||
* 彩虹登录支持选择多种登录方式 ([7aa0c7e](https://github.com/certd/certd/commit/7aa0c7e491fe660abb62e68792ff5474f19bd5b8))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 头像增加缓存时间 ([7015b1b](https://github.com/certd/certd/commit/7015b1b232602e5168a3eb8bee6d7f1776ae1e74))
|
||||
|
||||
## [1.39.16](https://github.com/certd/certd/compare/v1.39.15...v1.39.16) (2026-05-13)
|
||||
|
||||
**Note:** Version bump only for package @certd/ui-client
|
||||
|
||||
## [1.39.15](https://github.com/certd/certd/compare/v1.39.14...v1.39.15) (2026-05-13)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* icon选择器增加一套logo集 ([fdd5848](https://github.com/certd/certd/commit/fdd5848df4055a6ee07dc5eabaaf6b718672882d))
|
||||
* **monitor/site:** 新增站点监控页面禁用启用、检查状态两个筛选条件 ([118c15d](https://github.com/certd/certd/commit/118c15d04633a6ef06f2d9e7a7849d20f596e02c))
|
||||
* **network:** 新增全局公共http请求 headers设置 ([aad9045](https://github.com/certd/certd/commit/aad9045de55e76cb2ad09cac74a7bd60a4b47124))
|
||||
|
||||
## [1.39.14](https://github.com/certd/certd/compare/v1.39.13...v1.39.14) (2026-05-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复阿里云订阅流水线创建对话框无法获取阿里订单列表的bug ([a362860](https://github.com/certd/certd/commit/a362860137bfb7072893c844fe775edc46070ee1))
|
||||
|
||||
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* cnameProvider域名支持设置子域名托管 ([7266af1](https://github.com/certd/certd/commit/7266af17491a98338022cfb18cfedfb93ca6ef8f))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **设置:** 添加首页启用开关配置 ([25ad1e6](https://github.com/certd/certd/commit/25ad1e6f861e43288cc8bd90d4903628e6faec29))
|
||||
* **用户资料:** 新增手机号邮箱绑定功能 ([e0eb0e2](https://github.com/certd/certd/commit/e0eb0e21f6dae24b639c944f9aba2c90496ab1c0))
|
||||
* 域名注册过期时间获取再次优化 ([91a1b97](https://github.com/certd/certd/commit/91a1b9755066bf280e194dabf7c3a9f936e2643f))
|
||||
* **证书流水线:** 添加批量更新证书申请参数功能 ([63be1c1](https://github.com/certd/certd/commit/63be1c1cbd9b09a3b48f26130c296b1cedcca1ac))
|
||||
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
|
||||
* **domain:** 添加域名过期时间同步进度显示功能 ([9d2937d](https://github.com/certd/certd/commit/9d2937dd4b14ffab73e9b096edd2aa8539811182))
|
||||
|
||||
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@certd/ui-client",
|
||||
"version": "1.39.12",
|
||||
"version": "1.40.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite --open",
|
||||
@@ -12,7 +12,7 @@
|
||||
"debug:force": "vite --force --mode debug",
|
||||
"build": "cross-env NODE_OPTIONS=--max-old-space-size=40960 vite build ",
|
||||
"dev-build": "echo 1",
|
||||
"test:unit": "echo no unit tests",
|
||||
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
|
||||
"test:vue": "vitest run",
|
||||
"serve": "vite preview",
|
||||
"preview": "vite preview",
|
||||
@@ -34,11 +34,11 @@
|
||||
"@aws-sdk/s3-request-presigner": "^3.964.0",
|
||||
"@certd/vue-js-cron-light": "^4.0.14",
|
||||
"@ctrl/tinycolor": "^4.1.0",
|
||||
"@fast-crud/editor-code": "^1.27.8",
|
||||
"@fast-crud/fast-crud": "^1.27.8",
|
||||
"@fast-crud/fast-extends": "^1.27.8",
|
||||
"@fast-crud/ui-antdv4": "^1.27.8",
|
||||
"@fast-crud/ui-interface": "^1.27.8",
|
||||
"@fast-crud/editor-code": "^1.28.1",
|
||||
"@fast-crud/fast-crud": "^1.28.1",
|
||||
"@fast-crud/fast-extends": "^1.28.1",
|
||||
"@fast-crud/ui-antdv4": "^1.28.1",
|
||||
"@fast-crud/ui-interface": "^1.28.1",
|
||||
"@iconify/tailwind": "^1.2.0",
|
||||
"@iconify/vue": "^4.1.1",
|
||||
"@manypkg/get-packages": "^2.2.2",
|
||||
@@ -62,7 +62,6 @@
|
||||
"cos-js-sdk-v5": "^1.7.0",
|
||||
"cron-parser": "^4.9.0",
|
||||
"cropperjs": "^1.6.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"cssnano": "^7.0.6",
|
||||
"dayjs": "^1.11.7",
|
||||
"defu": "^6.1.4",
|
||||
@@ -107,8 +106,8 @@
|
||||
"zod-defaults": "^0.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@certd/lib-iframe": "^1.39.12",
|
||||
"@certd/pipeline": "^1.39.12",
|
||||
"@certd/lib-iframe": "^1.40.0",
|
||||
"@certd/pipeline": "^1.40.0",
|
||||
"@rollup/plugin-commonjs": "^25.0.7",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@types/chai": "^4.3.12",
|
||||
@@ -127,6 +126,7 @@
|
||||
"autoprefixer": "^10.4.21",
|
||||
"caller-path": "^4.0.0",
|
||||
"chai": "^5.1.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"dependency-cruiser": "^16.2.3",
|
||||
"dot": "^1.1.3",
|
||||
"eslint": "8.57.0",
|
||||
@@ -136,6 +136,7 @@
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-vue": "^9.23.0",
|
||||
"esmock": "^2.7.5",
|
||||
"less": "^4.2.0",
|
||||
"less-loader": "^12.2.0",
|
||||
"postcss": "^8.4.35",
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<div class="refresh-input">
|
||||
<div class="refresh-input-line">
|
||||
<a-input class="refresh-input-control" :value="value" :placeholder="placeholder" allow-clear @update:value="emit('update:value', $event)"></a-input>
|
||||
<fs-button :loading="loading" type="primary" :text="buttonText" :icon="icon" @click="doRefresh"></fs-button>
|
||||
</div>
|
||||
<div class="helper" :class="{ error: hasError }">
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ComponentPropsType, doRequest } from "/@/components/plugins/lib";
|
||||
import { computed, inject, ref } from "vue";
|
||||
import { Form } from "ant-design-vue";
|
||||
import { getInputFromForm } from "./utils";
|
||||
|
||||
defineOptions({
|
||||
name: "RefreshInput",
|
||||
});
|
||||
|
||||
type RefreshInputProps = ComponentPropsType & {
|
||||
buttonText?: string;
|
||||
icon?: string;
|
||||
placeholder?: string;
|
||||
successMessage?: string;
|
||||
};
|
||||
|
||||
const fromType: any = inject("getFromType");
|
||||
const getScope: any = inject("get:scope");
|
||||
const getPluginType: any = inject("get:plugin:type", () => {
|
||||
return "access";
|
||||
});
|
||||
const formItemContext = Form.useInjectFormItemContext();
|
||||
const props = defineProps<RefreshInputProps>();
|
||||
const emit = defineEmits<{
|
||||
"update:value": [value: string];
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
const message = ref("");
|
||||
const hasError = ref(false);
|
||||
|
||||
const action = computed(() => props.action);
|
||||
const buttonText = computed(() => props.buttonText || "刷新");
|
||||
const icon = computed(() => props.icon || "ion:refresh-outline");
|
||||
const placeholder = computed(() => props.placeholder || "");
|
||||
const successMessage = computed(() => props.successMessage || "刷新成功,请保存配置");
|
||||
|
||||
const doRefresh = async () => {
|
||||
if (loading.value) {
|
||||
return;
|
||||
}
|
||||
if (!action.value) {
|
||||
hasError.value = true;
|
||||
message.value = "缺少刷新动作配置";
|
||||
return;
|
||||
}
|
||||
|
||||
formItemContext.onFieldChange();
|
||||
|
||||
const { form } = getScope();
|
||||
const pluginType = getPluginType();
|
||||
const { input, record } = getInputFromForm(form, pluginType);
|
||||
|
||||
loading.value = true;
|
||||
message.value = "";
|
||||
hasError.value = false;
|
||||
try {
|
||||
const res = await doRequest(
|
||||
{
|
||||
type: pluginType,
|
||||
typeName: form.type,
|
||||
action: action.value,
|
||||
input,
|
||||
record,
|
||||
fromType,
|
||||
},
|
||||
{
|
||||
onError(err: any) {
|
||||
hasError.value = true;
|
||||
message.value = err.message;
|
||||
},
|
||||
showErrorNotify: false,
|
||||
}
|
||||
);
|
||||
emit("update:value", res);
|
||||
message.value = successMessage.value;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.refresh-input-line {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.refresh-input-control {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -74,7 +74,6 @@ const props = defineProps<
|
||||
uploadCert?: UploadCertProps;
|
||||
} & ComponentPropsType
|
||||
>();
|
||||
debugger;
|
||||
const emit = defineEmits<{
|
||||
"update:value": any;
|
||||
}>();
|
||||
@@ -141,8 +140,7 @@ const getOptions = async () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
message.value = "";
|
||||
message.value = "获取中...";
|
||||
hasError.value = false;
|
||||
loading.value = true;
|
||||
const pageNo = pagerRef.value.pageNo;
|
||||
|
||||
@@ -12,6 +12,7 @@ import AccessSelector from "/@/views/certd/access/access-selector/index.vue";
|
||||
import InputPassword from "./common/input-password.vue";
|
||||
import CertInfoUpdater from "/@/views/certd/pipeline/cert-upload/index.vue";
|
||||
import ApiTest from "./common/api-test.vue";
|
||||
import RefreshInput from "./common/refresh-input.vue";
|
||||
import ParamsShow from "./common/params-show.vue";
|
||||
export * from "./cert/index.js";
|
||||
export default {
|
||||
@@ -23,6 +24,7 @@ export default {
|
||||
app.component("CertInfoUpdater", CertInfoUpdater);
|
||||
|
||||
app.component("ApiTest", ApiTest);
|
||||
app.component("RefreshInput", RefreshInput);
|
||||
|
||||
app.component("SynologyDeviceIdGetter", SynologyIdDeviceGetter);
|
||||
app.component("RemoteAutoComplete", RemoteAutoComplete);
|
||||
|
||||
@@ -78,8 +78,13 @@ export default {
|
||||
passkeyRegisterFailed: "Passkey registration failed",
|
||||
title: "Change Password",
|
||||
weakPasswordWarning: "For your account security, please change your password immediately",
|
||||
initPasswordWarning: "This account does not have a login password yet. Please set one first",
|
||||
initPasswordTitle: "Set Password",
|
||||
changeNow: "Change Now",
|
||||
setNow: "Set Now",
|
||||
notNow: "Not Now",
|
||||
successMessage: "Changed successfully",
|
||||
initPasswordSuccessMessage: "Set successfully",
|
||||
oldPassword: "Old Password",
|
||||
oldPasswordRequired: "Please enter the old password",
|
||||
newPassword: "New Password",
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
export default {
|
||||
cnameTitle: "CNAME Service Configuration",
|
||||
cnameDescription:
|
||||
"The domain name configured here serves as a proxy for verifying other domains. When other domains apply for certificates, they map to this domain via CNAME for ownership verification. The advantage is that any domain can apply for a certificate this way without providing an AccessSecret.",
|
||||
"The domain name configured here serves as a proxy for verifying other domains. When other domains apply for certificates, they map to this domain via CNAME for ownership verification. The advantage is that any domain can apply for a certificate this way without providing an AccessSecret.",
|
||||
cnameLinkText: "CNAME principle and usage instructions",
|
||||
cnameDomain: "CNAME Domain",
|
||||
cnameDomainPlaceholder: "cname.handsfree.work",
|
||||
cnameDomainHelper:
|
||||
"Requires a domain registered with a DNS provider on the right (or you can transfer other domain DNS servers here).\nOnce the CNAME domain is set, it cannot be changed. It is recommended to use a first-level subdomain.",
|
||||
"Requires a domain registered with a DNS provider on the right (or you can transfer other domain DNS servers here).\nOnce the CNAME domain is set, it cannot be changed. It is recommended to use a first-level subdomain.",
|
||||
cnameDomainPattern: "Domain name cannot contain *",
|
||||
cnameProviderSubdomain: "Delegated Subdomain",
|
||||
cnameProviderSubdomainPlaceholder: "sub.example.com",
|
||||
cnameProviderSubdomainHelper: "Fill this when the CNAME domain is hosted under a delegated subdomain, for example CNAME domain cname.sub.example.com and DNS zone sub.example.com.",
|
||||
dnsProvider: "DNS Provider",
|
||||
dnsProviderAuthorization: "DNS Provider Authorization",
|
||||
};
|
||||
|
||||
@@ -91,6 +91,8 @@ export default {
|
||||
reverseProxyEmpty: "No reverse proxy list configured",
|
||||
environmentVars: "Environment Variables",
|
||||
environmentVarsHelper: "configure the runtime environment variables, one per line, format: KEY=VALUE",
|
||||
commonHeaders: "Common Headers",
|
||||
commonHeadersHelper: "Common headers automatically added to server-side HTTP requests, one per line, format: KEY=VALUE. Existing request headers with the same name are not overwritten.",
|
||||
|
||||
bindUrl: "Bind URL",
|
||||
bindUrlHelper: "Bind URL, used as your site URL in notifications",
|
||||
|
||||
@@ -79,8 +79,13 @@ export default {
|
||||
|
||||
title: "修改密码",
|
||||
weakPasswordWarning: "为了您的账户安全,请立即修改密码",
|
||||
initPasswordWarning: "当前账号还未设置登录密码,请先设置密码",
|
||||
initPasswordTitle: "设置密码",
|
||||
changeNow: "立即修改",
|
||||
setNow: "立即设置",
|
||||
notNow: "暂不设置",
|
||||
successMessage: "修改成功",
|
||||
initPasswordSuccessMessage: "设置成功",
|
||||
oldPassword: "旧密码",
|
||||
oldPasswordRequired: "请输入旧密码",
|
||||
newPassword: "新密码",
|
||||
|
||||
@@ -6,6 +6,9 @@ export default {
|
||||
cnameDomainPlaceholder: "cname.handsfree.work",
|
||||
cnameDomainHelper: "需要一个右边DNS提供商注册的域名(也可以将其他域名的dns服务器转移到这几家来)。\nCNAME域名一旦确定不可修改,建议使用一级子域名",
|
||||
cnameDomainPattern: "域名不能使用星号",
|
||||
cnameProviderSubdomain: "托管子域名",
|
||||
cnameProviderSubdomainPlaceholder: "sub.example.com",
|
||||
cnameProviderSubdomainHelper: "当CNAME域名本身托管在子域名下时填写,例如 CNAME域名为 cname.sub.example.com,实际DNS托管域为 sub.example.com",
|
||||
dnsProvider: "DNS提供商",
|
||||
dnsProviderAuthorization: "DNS提供商授权",
|
||||
};
|
||||
|
||||
@@ -89,6 +89,8 @@ export default {
|
||||
reverseProxyEmpty: "未配置反向代理",
|
||||
environmentVars: "环境变量",
|
||||
environmentVarsHelper: "配置运行时环境变量,每行一个,格式:KEY=VALUE",
|
||||
commonHeaders: "公共请求头",
|
||||
commonHeadersHelper: "服务端发起 HTTP 请求时自动附加的公共请求头,每行一个,格式:KEY=VALUE;请求中已设置同名 Header 时不会覆盖\n注意: 不要将token等敏感内容放在此处,仅限个人和公司内部使用,商业版不要设置",
|
||||
bindUrl: "绑定URL",
|
||||
bindUrlHelper: "绑定URL,在各类通知中显示你的站点URL",
|
||||
},
|
||||
|
||||
@@ -85,6 +85,7 @@ export type SysPublicSetting = {
|
||||
type: string;
|
||||
title: string;
|
||||
addonId: number;
|
||||
icon?: string;
|
||||
}
|
||||
>;
|
||||
// 系统通知
|
||||
@@ -99,6 +100,7 @@ export type SuiteSetting = {
|
||||
export type SysPrivateSetting = {
|
||||
httpProxy?: string;
|
||||
httpsProxy?: string;
|
||||
commonHeaders?: string;
|
||||
reverseProxies?: any;
|
||||
dnsResultOrder?: string;
|
||||
commonCnameEnabled?: boolean;
|
||||
|
||||
@@ -38,6 +38,7 @@ export interface UserInfoRes {
|
||||
avatar?: string;
|
||||
roleIds: number[];
|
||||
isWeak?: boolean;
|
||||
needInitPassword?: boolean;
|
||||
validTime?: number;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ export const useUserStore = defineStore({
|
||||
getToken(): string {
|
||||
return this.token || LocalStorage.get(TOKEN_KEY);
|
||||
},
|
||||
isLogined(): boolean {
|
||||
return !!this.getToken;
|
||||
},
|
||||
isAdmin(): boolean {
|
||||
return this.getUserInfo.roleIds?.includes(1) || this.getUserInfo.id === 1;
|
||||
},
|
||||
|
||||
@@ -11,7 +11,18 @@ export type MergeScriptContext = {
|
||||
export function useReference(formItem: any) {
|
||||
if (formItem.mergeScript) {
|
||||
const ctx = {
|
||||
compute,
|
||||
compute: (opts: any) => {
|
||||
const func = (context: any) => {
|
||||
debugger;
|
||||
let form = context.form || {};
|
||||
form = form.input || form.body || form; // form.access去掉,历史原因,access的mergeScript会处理form.access
|
||||
return opts({
|
||||
...context,
|
||||
form,
|
||||
});
|
||||
};
|
||||
return compute(func);
|
||||
},
|
||||
asyncCompute,
|
||||
computed,
|
||||
};
|
||||
|
||||
@@ -15,6 +15,14 @@ export async function changePassword(form: any) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function initPassword(form: any) {
|
||||
return await request({
|
||||
url: "/mine/initPassword",
|
||||
method: "POST",
|
||||
data: form,
|
||||
});
|
||||
}
|
||||
|
||||
export async function UpdateProfile(form: any) {
|
||||
return await request({
|
||||
url: "/mine/updateProfile",
|
||||
@@ -23,6 +31,37 @@ export async function UpdateProfile(form: any) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function GetContactCapability() {
|
||||
return await request({
|
||||
url: "/mine/contact/capability",
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
export async function UpdateMobile(form: any) {
|
||||
return await request({
|
||||
url: "/mine/contact/mobile",
|
||||
method: "POST",
|
||||
data: form,
|
||||
});
|
||||
}
|
||||
|
||||
export async function VerifyContactIdentity(form: any) {
|
||||
return await request({
|
||||
url: "/mine/contact/verifyIdentity",
|
||||
method: "POST",
|
||||
data: form,
|
||||
});
|
||||
}
|
||||
|
||||
export async function UpdateEmail(form: any) {
|
||||
return await request({
|
||||
url: "/mine/contact/email",
|
||||
method: "POST",
|
||||
data: form,
|
||||
});
|
||||
}
|
||||
|
||||
export async function GetOauthBounds() {
|
||||
return await request({
|
||||
url: "/oauth/bounds",
|
||||
@@ -37,20 +76,23 @@ export async function GetOauthProviders() {
|
||||
});
|
||||
}
|
||||
|
||||
export async function UnbindOauth(type: string) {
|
||||
export async function UnbindOauth(type: string, subtype?: string) {
|
||||
return await request({
|
||||
url: "/oauth/unbind",
|
||||
method: "POST",
|
||||
data: { type },
|
||||
data: {
|
||||
type: subtype ? `${type}:${subtype}` : type,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function OauthBoundUrl(type: string) {
|
||||
export async function OauthBoundUrl(type: string, subtype?: string) {
|
||||
return await request({
|
||||
url: "/oauth/login",
|
||||
method: "POST",
|
||||
data: {
|
||||
type,
|
||||
subtype,
|
||||
forType: "bind",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ref } from "vue";
|
||||
import { useI18n } from "/src/locales";
|
||||
|
||||
const { t } = useI18n();
|
||||
import { CrudOptions, useColumns, useFormWrapper } from "@fast-crud/fast-crud";
|
||||
import { compute, CrudOptions, useColumns, useFormWrapper } from "@fast-crud/fast-crud";
|
||||
import * as api from "/@/views/certd/mine/api";
|
||||
import { notification } from "ant-design-vue";
|
||||
import { useUserStore } from "/@/store/user";
|
||||
@@ -20,6 +20,11 @@ defineProps<{
|
||||
|
||||
let passwordFormRef = ref();
|
||||
|
||||
type OpenOptions = {
|
||||
password?: string;
|
||||
init?: boolean;
|
||||
};
|
||||
|
||||
const validatePass1 = async (rule: any, value: any) => {
|
||||
if (value === "") {
|
||||
throw new Error(t("authentication.enterPassword"));
|
||||
@@ -53,19 +58,33 @@ const passwordFormOptions: CrudOptions = {
|
||||
width: "500px",
|
||||
},
|
||||
async doSubmit({ form }) {
|
||||
await api.changePassword(form);
|
||||
if (form.init) {
|
||||
await api.initPassword(form);
|
||||
} else {
|
||||
await api.changePassword(form);
|
||||
}
|
||||
//重新加载用户信息
|
||||
await userStore.loadUserInfo();
|
||||
},
|
||||
async afterSubmit() {
|
||||
notification.success({ message: t("authentication.successMessage") });
|
||||
const formData = passwordFormRef.value?.getFormData?.();
|
||||
const message = formData?.init ? t("authentication.initPasswordSuccessMessage") : t("authentication.successMessage");
|
||||
notification.success({ message });
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
init: {
|
||||
title: "init",
|
||||
type: "text",
|
||||
form: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
password: {
|
||||
title: t("authentication.oldPassword"),
|
||||
type: "password",
|
||||
form: {
|
||||
show: compute(({ form }) => form.init !== true),
|
||||
rules: [{ required: true, message: t("authentication.oldPasswordRequired") }],
|
||||
},
|
||||
},
|
||||
@@ -97,12 +116,16 @@ const passwordFormOptions: CrudOptions = {
|
||||
},
|
||||
};
|
||||
|
||||
async function open(opts: { password: "" }) {
|
||||
async function open(opts: OpenOptions = {}) {
|
||||
const formOptions = buildFormOptions(passwordFormOptions);
|
||||
formOptions.newInstance = true; //新实例打开
|
||||
if (opts.init) {
|
||||
formOptions.wrapper.title = t("authentication.initPasswordTitle");
|
||||
}
|
||||
passwordFormRef.value = await openDialog(formOptions);
|
||||
passwordFormRef.value.setFormData({
|
||||
password: opts.password,
|
||||
init: opts.init === true,
|
||||
password: opts.password || "",
|
||||
});
|
||||
console.log(passwordFormRef.value);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { defineComponent } from "vue";
|
||||
import SmsCode from "/@/views/framework/login/sms-code.vue";
|
||||
import EmailCode from "/@/views/framework/register/email-code.vue";
|
||||
|
||||
export const ContactCodeInput = defineComponent({
|
||||
name: "ContactCodeInput",
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
form: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, { emit }) {
|
||||
const onChange = (value: string) => emit("update:modelValue", value);
|
||||
return () => {
|
||||
if (props.type === "email") {
|
||||
return <EmailCode value={props.modelValue} captcha={props.form.contactCaptcha} email={props.form.email} verificationType="bindEmail" onUpdate:value={onChange} />;
|
||||
}
|
||||
return <SmsCode value={props.modelValue} captcha={props.form.contactCaptcha} mobile={props.form.mobile} phoneCode={props.form.phoneCode} verificationType="bindMobile" onUpdate:value={onChange} />;
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { defineComponent } from "vue";
|
||||
import SmsCode from "/@/views/framework/login/sms-code.vue";
|
||||
import EmailCode from "/@/views/framework/register/email-code.vue";
|
||||
|
||||
export const IdentityCodeInput = defineComponent({
|
||||
name: "IdentityCodeInput",
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
form: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
userInfo: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, { emit }) {
|
||||
const onChange = (value: string) => emit("update:modelValue", value);
|
||||
return () => {
|
||||
if (props.form.identityType === "email") {
|
||||
return <EmailCode value={props.modelValue} captcha={props.form.identityCaptcha} email={props.userInfo.email} verificationType="contactIdentity" onUpdate:value={onChange} />;
|
||||
}
|
||||
return (
|
||||
<SmsCode value={props.modelValue} captcha={props.form.identityCaptcha} mobile={props.userInfo.mobile} phoneCode={props.userInfo.phoneCode || "86"} verificationType="contactIdentity" onUpdate:value={onChange} />
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -1,20 +1,22 @@
|
||||
// useUserProfile, 获取 openEditProfileDialog ,参考 useTemplate方法
|
||||
import { useFormWrapper } from "@fast-crud/fast-crud";
|
||||
import { ref } from "vue";
|
||||
import { cloneDeep, merge } from "lodash-es";
|
||||
import { compute, dict } from "@fast-crud/fast-crud";
|
||||
|
||||
// 假设的 API 导入
|
||||
import * as userProfileApi from "./api";
|
||||
import { useUserStore } from "/@/store/user";
|
||||
import { useI18n } from "/src/locales";
|
||||
import CaptchaInput from "/@/components/captcha/captcha-input.vue";
|
||||
import { message } from "ant-design-vue";
|
||||
import { ContactCodeInput } from "./contact-code-input";
|
||||
import { IdentityCodeInput } from "./identity-code-input";
|
||||
import { useFormDialog } from "/@/use/use-dialog";
|
||||
|
||||
/**
|
||||
* 获取用户资料编辑相关功能
|
||||
* @returns {{openEditProfileDialog: openEditProfileDialog}}
|
||||
*/
|
||||
export function useUserProfile() {
|
||||
const { openCrudFormDialog } = useFormWrapper();
|
||||
const wrapperRef = ref();
|
||||
const { openFormDialog } = useFormDialog();
|
||||
async function openEditProfileDialog(req: { onUpdated?: (ctx: any) => void }) {
|
||||
const detail = await userProfileApi.getMineInfo();
|
||||
if (!detail) {
|
||||
@@ -24,31 +26,28 @@ export function useUserProfile() {
|
||||
const { t } = useI18n();
|
||||
|
||||
const userStore = useUserStore();
|
||||
const userProfileFormRef = ref();
|
||||
async function doSubmit(opts: { form: any }) {
|
||||
const form = opts.form;
|
||||
async function doSubmit(form: any) {
|
||||
const { id } = await userProfileApi.UpdateProfile(form);
|
||||
if (req.onUpdated) {
|
||||
req.onUpdated({ id });
|
||||
}
|
||||
}
|
||||
|
||||
const crudOptions: any = {
|
||||
form: {
|
||||
doSubmit,
|
||||
wrapper: {
|
||||
title: `编辑用户资料`,
|
||||
width: 1100,
|
||||
onOpened(opts: { form: any }) {
|
||||
merge(opts.form, detail);
|
||||
},
|
||||
},
|
||||
await openFormDialog({
|
||||
title: `编辑用户资料`,
|
||||
wrapper: {
|
||||
width: 600,
|
||||
},
|
||||
initialForm: detail,
|
||||
onSubmit: doSubmit,
|
||||
columns: {
|
||||
nickName: {
|
||||
title: t("certd.nickName"),
|
||||
type: "text",
|
||||
form: {
|
||||
col: {
|
||||
span: 24,
|
||||
},
|
||||
component: {
|
||||
placeholder: t("certd.nickName"),
|
||||
},
|
||||
@@ -71,6 +70,9 @@ export function useUserProfile() {
|
||||
},
|
||||
},
|
||||
form: {
|
||||
col: {
|
||||
span: 24,
|
||||
},
|
||||
component: {
|
||||
vModel: "modelValue",
|
||||
valueType: "key",
|
||||
@@ -98,10 +100,7 @@ export function useUserProfile() {
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = await openCrudFormDialog({ crudOptions });
|
||||
wrapperRef.value = wrapper;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -110,26 +109,20 @@ export function useUserProfile() {
|
||||
}
|
||||
|
||||
export function usePasskeyRegister() {
|
||||
const { openCrudFormDialog } = useFormWrapper();
|
||||
const wrapperRef = ref();
|
||||
const { openFormDialog } = useFormDialog();
|
||||
async function openRegisterDialog(req: { onSubmit?: (ctx: any) => void }) {
|
||||
const { t } = useI18n();
|
||||
|
||||
const userStore = useUserStore();
|
||||
const deviceNameRef = ref();
|
||||
|
||||
const crudOptions: any = {
|
||||
form: {
|
||||
wrapper: {
|
||||
title: t("authentication.registerPasskey"),
|
||||
width: 500,
|
||||
onOpened(opts: { form: any }) {
|
||||
opts.form.deviceName = "";
|
||||
},
|
||||
},
|
||||
onSubmit: req.onSubmit,
|
||||
afterSubmit: null,
|
||||
onSuccess: null,
|
||||
await openFormDialog({
|
||||
title: t("authentication.registerPasskey"),
|
||||
wrapper: {
|
||||
width: 500,
|
||||
},
|
||||
initialForm: {
|
||||
deviceName: "",
|
||||
},
|
||||
onSubmit: async (form: any) => {
|
||||
await req.onSubmit?.({ form });
|
||||
},
|
||||
columns: {
|
||||
deviceName: {
|
||||
@@ -147,15 +140,229 @@ export function usePasskeyRegister() {
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = await openCrudFormDialog({ crudOptions });
|
||||
wrapperRef.value = wrapper;
|
||||
|
||||
return wrapper;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
openRegisterDialog,
|
||||
};
|
||||
}
|
||||
|
||||
export function useContactBind() {
|
||||
const { openFormDialog } = useFormDialog();
|
||||
|
||||
async function openContactBindDialog(req: { type: "mobile" | "email"; userInfo: any; contactCapability: { smsEnabled?: boolean }; onUpdated?: () => Promise<void> | void }) {
|
||||
const methods = [{ label: "密码", value: "password" }];
|
||||
if (req.userInfo.email) {
|
||||
methods.push({ label: "邮箱", value: "email" });
|
||||
}
|
||||
if (req.contactCapability.smsEnabled && req.userInfo.mobile) {
|
||||
methods.push({ label: "手机号", value: "mobile" });
|
||||
}
|
||||
|
||||
async function openChangeDialog(identityValidationCode: string) {
|
||||
const isMobile = req.type === "mobile";
|
||||
await openFormDialog({
|
||||
title: isMobile ? (req.userInfo.mobile ? "修改手机号" : "绑定手机号") : req.userInfo.email ? "修改邮箱" : "绑定邮箱",
|
||||
wrapper: {
|
||||
width: 560,
|
||||
},
|
||||
initialForm: {
|
||||
phoneCode: req.userInfo.phoneCode || "86",
|
||||
mobile: req.userInfo.mobile || "",
|
||||
email: req.userInfo.email || "",
|
||||
contactCaptcha: null,
|
||||
contactValidateCode: "",
|
||||
},
|
||||
async onSubmit(form: any) {
|
||||
if (isMobile) {
|
||||
await userProfileApi.UpdateMobile({
|
||||
phoneCode: form.phoneCode,
|
||||
mobile: form.mobile,
|
||||
validateCode: form.contactValidateCode,
|
||||
identityValidationCode,
|
||||
});
|
||||
} else {
|
||||
await userProfileApi.UpdateEmail({
|
||||
email: form.email,
|
||||
validateCode: form.contactValidateCode,
|
||||
identityValidationCode,
|
||||
});
|
||||
}
|
||||
message.success("绑定信息已更新");
|
||||
await req.onUpdated?.();
|
||||
},
|
||||
columns: {
|
||||
phoneCode: {
|
||||
title: "区号",
|
||||
type: "text",
|
||||
form: {
|
||||
col: {
|
||||
span: 24,
|
||||
},
|
||||
show: isMobile,
|
||||
component: {
|
||||
placeholder: "区号",
|
||||
},
|
||||
rules: [{ required: isMobile, message: "请输入区号" }],
|
||||
},
|
||||
},
|
||||
mobile: {
|
||||
title: "手机号",
|
||||
type: "text",
|
||||
form: {
|
||||
col: {
|
||||
span: 24,
|
||||
},
|
||||
show: isMobile,
|
||||
component: {
|
||||
placeholder: "请输入手机号",
|
||||
},
|
||||
rules: [
|
||||
{ required: isMobile, message: "请输入手机号" },
|
||||
{ pattern: /^\d{4,20}$/, message: "请输入正确的手机号" },
|
||||
],
|
||||
},
|
||||
},
|
||||
email: {
|
||||
title: "邮箱",
|
||||
type: "text",
|
||||
form: {
|
||||
col: {
|
||||
span: 24,
|
||||
},
|
||||
show: !isMobile,
|
||||
component: {
|
||||
placeholder: "请输入邮箱",
|
||||
},
|
||||
rules: [
|
||||
{ required: !isMobile, message: "请输入邮箱" },
|
||||
{ type: "email", message: "请输入正确的邮箱" },
|
||||
],
|
||||
},
|
||||
},
|
||||
contactCaptcha: {
|
||||
title: "图形验证码",
|
||||
form: {
|
||||
col: {
|
||||
span: 24,
|
||||
},
|
||||
component: {
|
||||
name: CaptchaInput,
|
||||
vModel: "modelValue",
|
||||
},
|
||||
rules: [{ required: true, message: "请完成图形验证码" }],
|
||||
},
|
||||
},
|
||||
contactValidateCode: {
|
||||
title: isMobile ? "新手机号验证码" : "新邮箱验证码",
|
||||
form: {
|
||||
col: {
|
||||
span: 24,
|
||||
},
|
||||
component: {
|
||||
name: ContactCodeInput,
|
||||
vModel: "modelValue",
|
||||
form: compute(({ form }) => form),
|
||||
type: req.type,
|
||||
},
|
||||
rules: [{ required: true, message: "请输入验证码" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await openFormDialog({
|
||||
title: "验证本人操作",
|
||||
wrapper: {
|
||||
width: 520,
|
||||
},
|
||||
initialForm: {
|
||||
identityType: "password",
|
||||
identityPassword: "",
|
||||
identityCaptcha: null,
|
||||
identityValidateCode: "",
|
||||
},
|
||||
async onSubmit(form: any) {
|
||||
const res = await userProfileApi.VerifyContactIdentity({
|
||||
identityType: form.identityType,
|
||||
identityPassword: form.identityPassword,
|
||||
identityValidateCode: form.identityValidateCode,
|
||||
});
|
||||
await openChangeDialog(res.validationCode);
|
||||
},
|
||||
columns: {
|
||||
identityType: {
|
||||
title: "验证方式",
|
||||
form: {
|
||||
col: {
|
||||
span: 24,
|
||||
},
|
||||
component: {
|
||||
name: "fs-dict-radio",
|
||||
vModel: "value",
|
||||
dict: dict({
|
||||
data: methods,
|
||||
}),
|
||||
},
|
||||
rules: [{ required: true, message: "请选择验证方式" }],
|
||||
valueChange({ form }: { form: any }) {
|
||||
form.identityPassword = "";
|
||||
form.identityCaptcha = null;
|
||||
form.identityValidateCode = "";
|
||||
},
|
||||
},
|
||||
},
|
||||
identityPassword: {
|
||||
title: "登录密码",
|
||||
type: "password",
|
||||
form: {
|
||||
col: {
|
||||
span: 24,
|
||||
},
|
||||
show: compute(({ form }) => form.identityType === "password"),
|
||||
component: {
|
||||
placeholder: "请输入登录密码",
|
||||
},
|
||||
rules: [{ required: true, message: "请输入登录密码" }],
|
||||
},
|
||||
},
|
||||
identityCaptcha: {
|
||||
title: "图形验证码",
|
||||
form: {
|
||||
col: {
|
||||
span: 24,
|
||||
},
|
||||
show: compute(({ form }) => form.identityType !== "password"),
|
||||
component: {
|
||||
name: CaptchaInput,
|
||||
vModel: "modelValue",
|
||||
},
|
||||
rules: [{ required: true, message: "请完成图形验证码" }],
|
||||
},
|
||||
},
|
||||
identityValidateCode: {
|
||||
title: "验证码",
|
||||
form: {
|
||||
col: {
|
||||
span: 24,
|
||||
},
|
||||
show: compute(({ form }) => form.identityType !== "password"),
|
||||
component: {
|
||||
name: IdentityCodeInput,
|
||||
vModel: "modelValue",
|
||||
form: compute(({ form }) => form),
|
||||
userInfo: req.userInfo,
|
||||
},
|
||||
rules: [{ required: true, message: "请输入验证码" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
openContactBindDialog,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,33 +13,42 @@
|
||||
<a-avatar v-else size="100" class="user-avatar default-avatar">
|
||||
{{ userInfo.username }}
|
||||
</a-avatar>
|
||||
<a-button type="text" size="small" class="avatar-edit-btn" title="修改资料" @click.stop="doUpdate">
|
||||
<template #icon><fs-icon icon="ion:create-outline" /></template>
|
||||
</a-button>
|
||||
<!-- <div class="status-indicator"></div> -->
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<h2 class="user-name flex items-center">
|
||||
{{ userInfo.nickName }}
|
||||
<fs-values-format :model-value="userInfo.roleIds" :dict="roleDict" color="blue" />
|
||||
<span>{{ userInfo.nickName }}</span>
|
||||
<a-button type="text" size="small" class="detail-edit-btn" title="修改资料" @click.stop="doUpdate">
|
||||
<template #icon><fs-icon icon="ion:create-outline" /></template>
|
||||
</a-button>
|
||||
</h2>
|
||||
<div class="user-details">
|
||||
<a-tag color="blue" class="detail-tag">
|
||||
<span class="tag-icon">👤</span>
|
||||
{{ userInfo.username }}
|
||||
<span class="tag-text">{{ userInfo.username }}</span>
|
||||
<fs-values-format :model-value="userInfo.roleIds" type="text" :dict="roleDict" color="blue" />
|
||||
</a-tag>
|
||||
<a-tag v-if="userInfo.email" color="green" class="detail-tag">
|
||||
<a-tag color="green" class="detail-tag">
|
||||
<span class="tag-icon">📧</span>
|
||||
{{ userInfo.email }}
|
||||
<span class="tag-text">{{ userInfo.email || "未绑定邮箱" }}</span>
|
||||
<a-button type="text" size="small" class="detail-edit-btn" title="修改邮箱" @click.stop="openBindContact('email')">
|
||||
<template #icon><fs-icon icon="ion:create-outline" /></template>
|
||||
</a-button>
|
||||
</a-tag>
|
||||
<a-tag v-if="userInfo.mobile" color="purple" class="detail-tag">
|
||||
<a-tag v-if="contactCapability.smsEnabled" color="purple" class="detail-tag">
|
||||
<span class="tag-icon">📱</span>
|
||||
{{ userInfo.mobile }}
|
||||
<span class="tag-text">{{ userInfo.mobile || "未绑定手机号" }}</span>
|
||||
<a-button type="text" size="small" class="detail-edit-btn" title="修改手机号" @click.stop="openBindContact('mobile')">
|
||||
<template #icon><fs-icon icon="ion:create-outline" /></template>
|
||||
</a-button>
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-buttons gap-2">
|
||||
<a-button type="primary" class="action-btn" @click="doUpdate">
|
||||
{{ t("authentication.updateProfile") }}
|
||||
</a-button>
|
||||
<change-password-button :show-button="true" />
|
||||
<change-password-button ref="changePasswordButtonRef" :show-button="true" />
|
||||
|
||||
<a-button type="primary" class="action-btn" @click="goSecuritySetting">
|
||||
{{ t("authentication.securitySettingTip") }}
|
||||
@@ -69,11 +78,11 @@
|
||||
<a-tag v-else color="red" class="bound-tag1">未绑定</a-tag>
|
||||
</span>
|
||||
</div>
|
||||
<a-button v-if="item.bound" type="primary" danger class="action-btn" @click="unbind(item.name)">
|
||||
<a-button v-if="item.bound" type="primary" danger class="action-btn" @click="unbind(item)">
|
||||
<template #icon><fs-icon icon="ion:unlink-outline" /></template>
|
||||
解绑
|
||||
</a-button>
|
||||
<a-button v-else type="primary" class="action-btn" @click="bind(item.name)">
|
||||
<a-button v-else type="primary" class="action-btn" @click="bind(item)">
|
||||
<template #icon><fs-icon icon="ion:link-outline" /></template>
|
||||
绑定
|
||||
</a-button>
|
||||
@@ -139,7 +148,7 @@ import * as api from "./api";
|
||||
import { computed, onMounted, Ref, ref } from "vue";
|
||||
import ChangePasswordButton from "/@/views/certd/mine/change-password-button.vue";
|
||||
import { useI18n } from "/src/locales";
|
||||
import { useUserProfile } from "./use";
|
||||
import { useContactBind, useUserProfile } from "./use";
|
||||
import { usePasskeyRegister } from "./use";
|
||||
import { message, Modal, notification } from "ant-design-vue";
|
||||
import { useSettingStore } from "/@/store/settings";
|
||||
@@ -160,6 +169,9 @@ const settingStore = useSettingStore();
|
||||
const userInfo: Ref = ref({});
|
||||
const passkeys = ref([]);
|
||||
const passkeySupported = ref(false);
|
||||
const contactCapability = ref({
|
||||
smsEnabled: false,
|
||||
});
|
||||
|
||||
const getUserInfo = async () => {
|
||||
userInfo.value = await api.getMineInfo();
|
||||
@@ -177,6 +189,7 @@ function doUpdate() {
|
||||
openEditProfileDialog({
|
||||
onUpdated: async () => {
|
||||
await getUserInfo();
|
||||
userStore.setUserInfo(userInfo.value);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -201,7 +214,7 @@ async function loadOauthProviders() {
|
||||
|
||||
const computedOauthBounds = computed(() => {
|
||||
const list = oauthProviders.value.map(item => {
|
||||
const bound = oauthBounds.value.find(bound => bound.type === item.name);
|
||||
const bound = oauthBounds.value.find(bound => bound.type === buildOauthBoundType(item));
|
||||
return {
|
||||
...item,
|
||||
bound,
|
||||
@@ -210,20 +223,24 @@ const computedOauthBounds = computed(() => {
|
||||
return list;
|
||||
});
|
||||
|
||||
async function unbind(type: string) {
|
||||
function buildOauthBoundType(item: any) {
|
||||
return item.subtype ? `${item.name}:${item.subtype}` : item.name;
|
||||
}
|
||||
|
||||
async function unbind(item: any) {
|
||||
Modal.confirm({
|
||||
title: "确认解绑吗?",
|
||||
okText: "确认",
|
||||
okType: "danger",
|
||||
onOk: async () => {
|
||||
await api.UnbindOauth(type);
|
||||
await api.UnbindOauth(item.name, item.subtype);
|
||||
await loadOauthBounds();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function bind(type: string) {
|
||||
const res = await api.OauthBoundUrl(type);
|
||||
async function bind(item: any) {
|
||||
const res = await api.OauthBoundUrl(item.name, item.subtype);
|
||||
const loginUrl = res.loginUrl;
|
||||
window.location.href = loginUrl;
|
||||
}
|
||||
@@ -237,6 +254,23 @@ async function loadPasskeys() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadContactCapability() {
|
||||
contactCapability.value = await api.GetContactCapability();
|
||||
}
|
||||
|
||||
const { openContactBindDialog } = useContactBind();
|
||||
async function openBindContact(type: "mobile" | "email") {
|
||||
await openContactBindDialog({
|
||||
type,
|
||||
userInfo: userInfo.value,
|
||||
contactCapability: contactCapability.value,
|
||||
onUpdated: async () => {
|
||||
await getUserInfo();
|
||||
userStore.setUserInfo(userInfo.value);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function unbindPasskey(id: number) {
|
||||
Modal.confirm({
|
||||
title: "确认解绑吗?",
|
||||
@@ -353,6 +387,7 @@ const checkPasskeySupport = () => {
|
||||
}
|
||||
};
|
||||
const userStore = useUserStore();
|
||||
const changePasswordButtonRef = ref();
|
||||
const userAvatar = computed(() => {
|
||||
if (isEmpty(userInfo.value.avatar)) {
|
||||
return "";
|
||||
@@ -361,11 +396,27 @@ const userAvatar = computed(() => {
|
||||
return userInfo.value.avatar;
|
||||
}
|
||||
|
||||
return `api/basic/file/download?token=${userStore.getToken}&key=${userInfo.value.avatar}`;
|
||||
return `api/basic/file/download?key=${userInfo.value.avatar}`;
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await getUserInfo();
|
||||
userStore.setUserInfo(userInfo.value);
|
||||
if (userInfo.value.needInitPassword === true) {
|
||||
Modal.confirm({
|
||||
title: t("authentication.initPasswordTitle"),
|
||||
content: t("authentication.initPasswordWarning"),
|
||||
okText: t("authentication.setNow"),
|
||||
cancelText: t("authentication.notNow"),
|
||||
closable: true,
|
||||
onOk: () => {
|
||||
changePasswordButtonRef.value.open({
|
||||
init: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
await loadContactCapability();
|
||||
await loadOauthBounds();
|
||||
await loadOauthProviders();
|
||||
await loadPasskeys();
|
||||
@@ -567,6 +618,24 @@ onMounted(async () => {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar-edit-btn {
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
bottom: 2px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
min-width: 28px;
|
||||
padding: 0;
|
||||
color: #667eea;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
border: 4px solid #ffffff;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
@@ -613,6 +682,18 @@ onMounted(async () => {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.detail-edit-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
min-width: 20px;
|
||||
margin: -2px -6px -2px 0;
|
||||
padding: 0;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.tag-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
@@ -863,18 +944,84 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.card-header {
|
||||
padding: 32px 16px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.avatar-wrapper {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 88px !important;
|
||||
height: 88px !important;
|
||||
line-height: 88px !important;
|
||||
}
|
||||
|
||||
.avatar-edit-btn {
|
||||
right: -2px;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
justify-content: center;
|
||||
margin-bottom: 14px;
|
||||
font-size: 22px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.user-details {
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.detail-tag {
|
||||
justify-content: flex-start;
|
||||
width: min(230px, calc(100vw - 96px));
|
||||
min-height: 34px;
|
||||
padding: 5px 8px 5px 12px;
|
||||
border-radius: 12px;
|
||||
white-space: nowrap;
|
||||
margin-right: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.tag-icon {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.tag-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.detail-edit-btn {
|
||||
margin: 0 -4px 0 2px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
gap: 10px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.action-buttons :deep(.ant-btn) {
|
||||
min-width: 90px;
|
||||
height: 32px;
|
||||
padding: 4px 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,6 +320,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
title: t("monitor.siteName"),
|
||||
search: {
|
||||
show: true,
|
||||
col: {
|
||||
span: 3,
|
||||
},
|
||||
},
|
||||
type: "text",
|
||||
form: {
|
||||
@@ -333,6 +336,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
title: t("monitor.domain"),
|
||||
search: {
|
||||
show: true,
|
||||
col: {
|
||||
span: 3,
|
||||
},
|
||||
},
|
||||
type: "text",
|
||||
form: {
|
||||
@@ -411,6 +417,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
title: t("monitor.certDomains"),
|
||||
search: {
|
||||
show: true,
|
||||
col: {
|
||||
span: 3,
|
||||
},
|
||||
},
|
||||
type: "text",
|
||||
form: {
|
||||
@@ -451,6 +460,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
title: t("monitor.certStatus"),
|
||||
search: {
|
||||
show: true,
|
||||
col: {
|
||||
span: 2,
|
||||
},
|
||||
},
|
||||
type: "dict-select",
|
||||
dict: dict({
|
||||
@@ -472,7 +484,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
checkStatus: {
|
||||
title: t("monitor.checkStatus"),
|
||||
search: {
|
||||
show: false,
|
||||
show: true,
|
||||
col: {
|
||||
span: 2,
|
||||
},
|
||||
},
|
||||
type: "dict-select",
|
||||
dict: checkStatusDict,
|
||||
@@ -578,6 +593,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
type: "dict-select",
|
||||
search: {
|
||||
show: true,
|
||||
col: {
|
||||
span: 3,
|
||||
},
|
||||
},
|
||||
dict: groupDictRef,
|
||||
form: {
|
||||
@@ -631,7 +649,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
disabled: {
|
||||
title: t("monitor.disabled"),
|
||||
search: {
|
||||
show: false,
|
||||
show: true,
|
||||
col: {
|
||||
span: 2,
|
||||
},
|
||||
},
|
||||
type: "dict-switch",
|
||||
dict: dict({
|
||||
|
||||
@@ -94,9 +94,15 @@ export function setRunnableIds(pipeline: any) {
|
||||
return JSON.parse(content);
|
||||
}
|
||||
|
||||
export function useCertPipelineCreator() {
|
||||
export function useCertPipelineCreator({ formWrapperRef }: { formWrapperRef: Ref<any> }) {
|
||||
const { t } = useI18n();
|
||||
const { openCrudFormDialog } = useFormWrapper();
|
||||
|
||||
function open(opts: any) {
|
||||
return new Promise((resolve, reject) => {
|
||||
formWrapperRef.value.open(opts);
|
||||
});
|
||||
}
|
||||
const { openCrudFormDialog } = useFormWrapper({ open });
|
||||
|
||||
const pluginStore = usePluginStore();
|
||||
const settingStore = useSettingStore();
|
||||
@@ -111,10 +117,10 @@ export function useCertPipelineCreator() {
|
||||
// inputs[inputKey].form.show = true;
|
||||
const inputDefine = cloneDeep(certPlugin.input[inputKey]);
|
||||
if (inputDefine.maybeNeed) {
|
||||
moreParams.push(inputKey);
|
||||
moreParams.push("input." + inputKey);
|
||||
}
|
||||
useReference(inputDefine);
|
||||
inputs[inputKey] = {
|
||||
inputs["input." + inputKey] = {
|
||||
title: inputDefine.title,
|
||||
form: {
|
||||
...inputDefine,
|
||||
@@ -135,17 +141,19 @@ export function useCertPipelineCreator() {
|
||||
const DEFAULT_RENEW_DAYS = settingStore.sysPublic.defaultCertRenewDays || settingStore.sysPublic.defaultWillExpireDays || 20;
|
||||
|
||||
merge(inputs, {
|
||||
renewDays: {
|
||||
"input.renewDays": {
|
||||
form: {
|
||||
value: DEFAULT_RENEW_DAYS,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const initialForm = req.initialForm || {};
|
||||
initialForm.type = certPlugin.name;
|
||||
return {
|
||||
crudOptions: {
|
||||
form: {
|
||||
initialForm: req.initialForm || {},
|
||||
initialForm: initialForm,
|
||||
doSubmit,
|
||||
wrapper: {
|
||||
wrapClassName: "cert_pipeline_create_form",
|
||||
@@ -164,44 +172,6 @@ export function useCertPipelineCreator() {
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
// certApplyPlugin: {
|
||||
// title: t("certd.plugin.selectTitle"),
|
||||
// type: "dict-select",
|
||||
// dict: dict({
|
||||
// data: [
|
||||
// { value: "CertApply", label: "JS-ACME" },
|
||||
// { value: "CertApplyLego", label: "Lego-ACME" },
|
||||
// { value: "CertApplyGetFormAliyun", label: "Aliyun-Order" },
|
||||
// ],
|
||||
// }),
|
||||
// form: {
|
||||
// order: 0,
|
||||
// value: "CertApply",
|
||||
// helper: {
|
||||
// render: () => {
|
||||
// return (
|
||||
// <ul>
|
||||
// <li>{t("certd.plugin.jsAcme")}</li>
|
||||
// <li>{t("certd.plugin.legoAcme")}</li>
|
||||
// <li>{t("certd.plugin.aliyunOrder")}</li>
|
||||
// </ul>
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
// valueChange: {
|
||||
// handle: async ({ form, value }) => {
|
||||
// const config = await pluginStore.getPluginConfig({
|
||||
// name: value,
|
||||
// type: "builtIn",
|
||||
// });
|
||||
// if (config.sysSetting?.input) {
|
||||
// merge(form, config.sysSetting.input);
|
||||
// }
|
||||
// },
|
||||
// immediate: true,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
...inputs,
|
||||
triggerCron: {
|
||||
title: t("certd.pipelineForm.triggerCronTitle"),
|
||||
@@ -346,20 +316,20 @@ export function useCertPipelineCreator() {
|
||||
await checkPipelineLimit();
|
||||
|
||||
//设置系统初始值
|
||||
const initialForm: any = {};
|
||||
const initialForm: any = { input: {} };
|
||||
const pluginSysConfig = await pluginStore.getPluginConfig({ name: req.pluginName, type: "builtIn" });
|
||||
if (pluginSysConfig.sysSetting?.input) {
|
||||
for (const key in pluginSysConfig.sysSetting?.input) {
|
||||
initialForm[key] = pluginSysConfig.sysSetting?.input[key];
|
||||
initialForm.input[key] = pluginSysConfig.sysSetting?.input[key];
|
||||
}
|
||||
}
|
||||
|
||||
async function doSubmit({ form }: any) {
|
||||
// const certDetail = readCertDetail(form.cert.crt);
|
||||
// 添加certd pipeline
|
||||
const pluginInput = omit(form, ["triggerCron", "notification", "notificationTarget", "notificationWhen", "certApplyPlugin", "groupId"]);
|
||||
const pluginInput = form.input;
|
||||
let pipeline: any = {
|
||||
title: form.domains[0] + "证书自动化",
|
||||
title: pluginInput.domains[0] + "证书自动化",
|
||||
runnableType: "pipeline",
|
||||
stages: [
|
||||
{
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
<div>{{ t("certd.applyCertificate") }}</div>
|
||||
</template>
|
||||
</fs-crud>
|
||||
<fs-form-wrapper ref="formWrapperRef"></fs-form-wrapper>
|
||||
</fs-page>
|
||||
</template>
|
||||
|
||||
@@ -90,10 +91,9 @@ function onActionbarMoreItemClick(req: { key: string; item: any }) {
|
||||
openCertApplyDialog({ key: req.key, title: req.item?.title });
|
||||
}
|
||||
|
||||
const certdFormRef = ref<typeof CertdForm>();
|
||||
const currentPluginRef = ref();
|
||||
provide("getCurrentPluginDefine", () => {
|
||||
return currentPluginRef.value;
|
||||
return currentPluginRef;
|
||||
});
|
||||
|
||||
const addMorePipelineBtns = computed(() => {
|
||||
@@ -104,7 +104,9 @@ const addMorePipelineBtns = computed(() => {
|
||||
{ key: "BatchAddPipeline", title: t("certd.pipelinePage.batchAddPipeline"), icon: "ion:duplicate" },
|
||||
];
|
||||
});
|
||||
const { openAddCertdPipelineDialog } = useCertPipelineCreator();
|
||||
|
||||
const formWrapperRef = ref<any>();
|
||||
const { openAddCertdPipelineDialog } = useCertPipelineCreator({ formWrapperRef });
|
||||
function openCertApplyDialog(req: { key: string; title: string }) {
|
||||
if (req.key === "AddPipeline") {
|
||||
crudExpose.openAdd({});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { request } from "/src/api/service";
|
||||
|
||||
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({
|
||||
url: apiPrefix + `/login`,
|
||||
method: "post",
|
||||
@@ -10,6 +10,7 @@ export async function OauthLogin(type: string, forType?: string, from?: string)
|
||||
type,
|
||||
forType: forType || "login",
|
||||
from: from || "web",
|
||||
subtype,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
<div>第三方({{ oauthType }})登录成功,您还未绑定账号,请选择</div>
|
||||
|
||||
<div class="mt-10">
|
||||
<a-button 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-if="!userStore.isLogined" class="w-full mt-10" type="primary" @click="goBindUser">绑定已有账号</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 class="w-full mt-10">
|
||||
@@ -48,7 +49,7 @@ async function handleOauthToken() {
|
||||
//登录成功
|
||||
userStore.onLoginSuccess(res);
|
||||
//跳转到首页
|
||||
router.replace("/");
|
||||
router.replace("/index");
|
||||
return;
|
||||
}
|
||||
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 () => {
|
||||
if (error.value) {
|
||||
return;
|
||||
@@ -70,12 +80,7 @@ onMounted(async () => {
|
||||
|
||||
if (forType === "bind") {
|
||||
//从用户中心页面,进行第三方账号的绑定
|
||||
await api.BindUser(validationCode);
|
||||
notification.success({
|
||||
message: "绑定成功",
|
||||
});
|
||||
//跳转到首页
|
||||
router.replace("/certd/mine/user-profile");
|
||||
await doBindCurrent();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -98,7 +103,7 @@ async function autoRegister() {
|
||||
//登录成功
|
||||
userStore.onLoginSuccess(res);
|
||||
//跳转到首页
|
||||
router.replace("/");
|
||||
router.replace("/index");
|
||||
}
|
||||
</script>
|
||||
<style lang="less">
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
</div>
|
||||
<div class="flex justify-center items-center gap-4 flex-wrap md:flex-nowrap">
|
||||
<passkey-login></passkey-login>
|
||||
<template v-for="item in oauthProviderList" :key="item.type">
|
||||
<div v-if="item.addonId" class="oauth-icon-button pointer" @click="goOauthLogin(item.name)">
|
||||
<template v-for="item in oauthProviderList" :key="buildProviderKey(item)">
|
||||
<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 class="ellipsis title" :title="item.addonTitle || item.title">{{ item.addonTitle || item.title }}</div>
|
||||
</div>
|
||||
@@ -22,7 +22,17 @@ import { useSettingStore } from "/@/store/settings";
|
||||
import { useRoute } from "vue-router";
|
||||
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<{
|
||||
oauthOnly?: boolean;
|
||||
}>();
|
||||
@@ -42,15 +52,19 @@ onMounted(async () => {
|
||||
if (settingStore.sysPublic.oauthAutoRedirect && queryOauthOnly !== "false") {
|
||||
const firstOauth = oauthProviderList.value.find(item => item.addonId > 0);
|
||||
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
|
||||
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;
|
||||
window.location.href = loginUrl;
|
||||
}
|
||||
|
||||
@@ -97,6 +97,21 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
width: 200,
|
||||
},
|
||||
},
|
||||
subdomain: {
|
||||
title: t("certd.cnameProviderSubdomain"),
|
||||
type: "text",
|
||||
form: {
|
||||
component: {
|
||||
placeholder: t("certd.cnameProviderSubdomainPlaceholder"),
|
||||
},
|
||||
helper: t("certd.cnameProviderSubdomainHelper"),
|
||||
rules: [{ pattern: /^[^*]+$/, message: t("certd.cnameDomainPattern") }],
|
||||
},
|
||||
column: {
|
||||
width: 200,
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
dnsProviderType: {
|
||||
title: t("certd.dnsProvider"),
|
||||
type: "dict-select",
|
||||
|
||||
@@ -114,7 +114,7 @@ export async function GetSmsTypeDefine(type: string) {
|
||||
|
||||
export async function GetOauthProviders() {
|
||||
return await request({
|
||||
url: "/oauth/providers",
|
||||
url: apiPrefix + "/oauth/providers",
|
||||
method: "post",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@
|
||||
<div class="helper">{{ t("certd.sys.setting.environmentVarsHelper") }}</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :label="t('certd.sys.setting.commonHeaders')" :name="['private', 'commonHeaders']">
|
||||
<a-textarea v-model:value="formState.private.commonHeaders" :placeholder="commonHeadersExample" rows="4" />
|
||||
<div class="helper">{{ t("certd.sys.setting.commonHeadersHelper") }}</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :label="t('certd.dualStackNetwork')" :name="['private', 'dnsResultOrder']">
|
||||
<a-select v-model:value="formState.private.dnsResultOrder">
|
||||
<a-select-option value="verbatim">{{ t("certd.default") }}</a-select-option>
|
||||
@@ -64,6 +69,10 @@ const environmentVarsExample = ref(
|
||||
`ALIYUN_CLIENT_CONNECT_TIMEOUT=16000 #连接超时,单位毫秒
|
||||
ALIYUN_CLIENT_READ_TIMEOUT=16000 #读取数据超时,单位毫秒`
|
||||
);
|
||||
const commonHeadersExample = ref(
|
||||
`User-Agent=certd
|
||||
X-Custom-Header=value`
|
||||
);
|
||||
|
||||
const formState = reactive<Partial<SysSettings>>({
|
||||
public: {},
|
||||
|
||||
@@ -109,6 +109,7 @@ const formState = reactive<Partial<SysSettings>>({
|
||||
const oauthProviders = ref([]);
|
||||
async function loadOauthProviders() {
|
||||
oauthProviders.value = await api.GetOauthProviders();
|
||||
mergeOauthProviderSettings();
|
||||
}
|
||||
|
||||
const bindDomain = computed(() => {
|
||||
@@ -164,6 +165,16 @@ const onFinish = async (form: any) => {
|
||||
function buildCallbackUrl(type: string) {
|
||||
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>
|
||||
<style lang="less">
|
||||
.sys-settings-oauth {
|
||||
|
||||
@@ -3,6 +3,60 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.40.0](https://github.com/certd/certd/compare/v1.39.16...v1.40.0) (2026-05-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复群晖授权没有显示设备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)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复启动时报密钥备份不存在的问题 ([c966896](https://github.com/certd/certd/commit/c9668965226af6b54e0e576931dcba8b3d188ef3))
|
||||
|
||||
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **aliyun-access:** 添加阿里云密钥校验失败的错误处理 ([b75c625](https://github.com/certd/certd/commit/b75c625ddcc0b3110699d8e6175681ef157b25df))
|
||||
* cnameProvider域名支持设置子域名托管 ([7266af1](https://github.com/certd/certd/commit/7266af17491a98338022cfb18cfedfb93ca6ef8f))
|
||||
* **plugin-aliyun:** 过滤非CAS证书并优化日志信息 ([c4b01da](https://github.com/certd/certd/commit/c4b01da384bc40a241a673ea8bc01ca733c04d83))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **用户资料:** 新增手机号邮箱绑定功能 ([e0eb0e2](https://github.com/certd/certd/commit/e0eb0e21f6dae24b639c944f9aba2c90496ab1c0))
|
||||
* 域名注册过期时间获取再次优化 ([91a1b97](https://github.com/certd/certd/commit/91a1b9755066bf280e194dabf7c3a9f936e2643f))
|
||||
* **证书流水线:** 添加批量更新证书申请参数功能 ([63be1c1](https://github.com/certd/certd/commit/63be1c1cbd9b09a3b48f26130c296b1cedcca1ac))
|
||||
* 支持火山云vke ([bb46cb0](https://github.com/certd/certd/commit/bb46cb08f71f6ae921543f7e4a6c5f4e0190556e))
|
||||
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
|
||||
* **plugin-volcengine:** 支持火山引擎VKE部署插件 ([b8a64a6](https://github.com/certd/certd/commit/b8a64a6b5bf3691a47177de42bc49b798e795feb))
|
||||
|
||||
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE cd_cname_provider ADD COLUMN subdomain varchar(100);
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE cd_cname_provider ADD COLUMN subdomain varchar(100);
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE cd_cname_provider ADD COLUMN subdomain varchar(100);
|
||||
@@ -3,6 +3,26 @@ title: EAB授权
|
||||
desc: ZeroSSL证书申请需要EAB授权
|
||||
icon: ic:outline-lock
|
||||
input:
|
||||
eabType:
|
||||
title: EAB类型
|
||||
component:
|
||||
name: a-select
|
||||
options:
|
||||
- value: google
|
||||
label: Google
|
||||
icon: flat-color-icons:google
|
||||
- value: zerossl
|
||||
label: ZeroSSL
|
||||
icon: emojione:digit-zero
|
||||
- value: litessl
|
||||
label: litessl
|
||||
icon: roentgen:free
|
||||
- value: sslcom
|
||||
label: SSL.com
|
||||
icon: la:expeditedssl
|
||||
helper: 请选择EAB类型
|
||||
required: true
|
||||
encrypt: false
|
||||
kid:
|
||||
title: KID
|
||||
component:
|
||||
@@ -24,8 +44,20 @@ input:
|
||||
rules:
|
||||
- type: email
|
||||
message: 请输入正确的邮箱
|
||||
helper: Google的EAB申请证书,更换邮箱会导致EAB失效,可以在此处绑定一个邮箱避免此问题
|
||||
helper: 绑定一个邮箱,避免失效
|
||||
required: true
|
||||
accountKey:
|
||||
title: ACME账号私钥
|
||||
component:
|
||||
name: refresh-input
|
||||
action: GenerateAccountKey
|
||||
buttonText: 生成
|
||||
successMessage: 账号私钥已生成,请保存授权配置
|
||||
required: true
|
||||
helper: |-
|
||||
如果修改了KID,请点击生成重新生成账号私钥
|
||||
注意:google的EAB只能生成一次账号私钥,更新私钥需要获取一个新的EAB授权
|
||||
encrypt: true
|
||||
pluginType: access
|
||||
type: builtIn
|
||||
scriptFilePath: /plugins/plugin-cert/access/eab-access.js
|
||||
|
||||
@@ -39,6 +39,8 @@ input:
|
||||
component:
|
||||
name: a-switch
|
||||
vModel: checked
|
||||
col:
|
||||
span: 24
|
||||
helper: 是否启用了双重认证
|
||||
required: true
|
||||
deviceId:
|
||||
@@ -48,6 +50,8 @@ input:
|
||||
name: synology-device-id-getter
|
||||
type: access
|
||||
typeName: synology
|
||||
col:
|
||||
span: 24
|
||||
mergeScript: |2-
|
||||
|
||||
return {
|
||||
|
||||
@@ -14,43 +14,54 @@ input:
|
||||
loginType:
|
||||
title: 登录类型
|
||||
component:
|
||||
name: a-auto-complete
|
||||
name: a-select
|
||||
vModel: value
|
||||
mode: tags
|
||||
multiple: true
|
||||
options:
|
||||
- label: QQ
|
||||
value: qq
|
||||
icon: cib:tencent-qq:#007AFF
|
||||
- label: 微信
|
||||
value: wx
|
||||
icon: simple-icons:wechat:#34C759
|
||||
- label: 支付宝
|
||||
value: alipay
|
||||
icon: simple-icons:alipay:#0099ff
|
||||
- label: 微博
|
||||
value: sina
|
||||
icon: uiw:weibo:#FF3B30
|
||||
- label: 百度
|
||||
value: baidu
|
||||
icon: simple-icons:baidu:#007AFF
|
||||
- label: 华为
|
||||
value: huawei
|
||||
icon: simple-icons:huawei:#ff0000
|
||||
- label: 小米
|
||||
value: xiaomi
|
||||
icon: simple-icons:xiaomi:#FF9500
|
||||
- label: 谷歌
|
||||
value: google
|
||||
icon: flat-color-icons:google
|
||||
- label: 微软
|
||||
value: microsoft
|
||||
icon: logos:microsoft-icon
|
||||
- label: Facebook
|
||||
value: facebook
|
||||
icon: logos:facebook
|
||||
- label: Twitter
|
||||
value: twitter
|
||||
icon: logos:twitter
|
||||
- label: 钉钉
|
||||
value: dingtalk
|
||||
icon: ant-design:dingding-outlined:#007AFF
|
||||
- label: Gitee
|
||||
value: gitee
|
||||
icon: simple-icons:gitee:#c71d23
|
||||
- label: Github
|
||||
value: github
|
||||
icon: logos:github-icon
|
||||
required: true
|
||||
icon:
|
||||
title: 自定义图标
|
||||
component:
|
||||
name: fs-icon-selector
|
||||
vModel: modelValue
|
||||
required: false
|
||||
appId:
|
||||
title: AppId
|
||||
helper: 彩虹聚合登录->应用列表->创建应用 获取
|
||||
|
||||
@@ -10,6 +10,18 @@ input:
|
||||
component:
|
||||
name: fs-icon-selector
|
||||
vModel: modelValue
|
||||
iconSets:
|
||||
- streamline-logos
|
||||
- logos
|
||||
- fa-brands
|
||||
- fa-solid
|
||||
- fa-regular
|
||||
- carbon
|
||||
- ion
|
||||
- ant-design
|
||||
- mdi
|
||||
- twemoji
|
||||
- svg-spinners
|
||||
required: false
|
||||
clientId:
|
||||
title: ClientId
|
||||
|
||||
@@ -8,6 +8,7 @@ desc: 根据证书id一键更新腾讯云证书并自动部署(Id不变),
|
||||
icon: svg:icon-tencentcloud
|
||||
group: tencent
|
||||
needPlus: false
|
||||
deprecated: 腾讯更新证书(Id不变)接口已失效,本插件已下架,请使用其他接口
|
||||
input:
|
||||
cert:
|
||||
title: 域名证书
|
||||
|
||||
@@ -68,12 +68,12 @@ input:
|
||||
component:
|
||||
name: remote-select
|
||||
vModel: value
|
||||
mode: tags
|
||||
mode: default
|
||||
type: plugin
|
||||
action: onGetClusterList
|
||||
search: false
|
||||
pager: false
|
||||
multi: true
|
||||
multi: false
|
||||
watches:
|
||||
- certDomains
|
||||
- accessId
|
||||
@@ -102,8 +102,6 @@ input:
|
||||
value: Public
|
||||
- label: 私网
|
||||
value: Private
|
||||
- label: 集群内
|
||||
value: TargetCluster
|
||||
value: Public
|
||||
required: true
|
||||
order: 0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@certd/ui-server",
|
||||
"version": "1.39.12",
|
||||
"version": "1.40.0",
|
||||
"description": "fast-server base midway",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
@@ -53,20 +53,20 @@
|
||||
"@aws-sdk/client-sts": "^3.990.0",
|
||||
"@azure/arm-dns": "^5.1.0",
|
||||
"@azure/identity": "^4.13.1",
|
||||
"@certd/acme-client": "^1.39.12",
|
||||
"@certd/basic": "^1.39.12",
|
||||
"@certd/commercial-core": "^1.39.12",
|
||||
"@certd/acme-client": "^1.40.0",
|
||||
"@certd/basic": "^1.40.0",
|
||||
"@certd/commercial-core": "^1.40.0",
|
||||
"@certd/cv4pve-api-javascript": "^8.4.2",
|
||||
"@certd/jdcloud": "^1.39.12",
|
||||
"@certd/lib-huawei": "^1.39.12",
|
||||
"@certd/lib-k8s": "^1.39.12",
|
||||
"@certd/lib-server": "^1.39.12",
|
||||
"@certd/midway-flyway-js": "^1.39.12",
|
||||
"@certd/pipeline": "^1.39.12",
|
||||
"@certd/plugin-cert": "^1.39.12",
|
||||
"@certd/plugin-lib": "^1.39.12",
|
||||
"@certd/plugin-plus": "^1.39.12",
|
||||
"@certd/plus-core": "^1.39.12",
|
||||
"@certd/jdcloud": "^1.40.0",
|
||||
"@certd/lib-huawei": "^1.40.0",
|
||||
"@certd/lib-k8s": "^1.40.0",
|
||||
"@certd/lib-server": "^1.40.0",
|
||||
"@certd/midway-flyway-js": "^1.40.0",
|
||||
"@certd/pipeline": "^1.40.0",
|
||||
"@certd/plugin-cert": "^1.40.0",
|
||||
"@certd/plugin-lib": "^1.40.0",
|
||||
"@certd/plugin-plus": "^1.40.0",
|
||||
"@certd/plus-core": "^1.40.0",
|
||||
"@google-cloud/dns": "^5.3.1",
|
||||
"@google-cloud/publicca": "^1.3.0",
|
||||
"@huaweicloud/huaweicloud-sdk-cdn": "^3.1.185",
|
||||
@@ -101,7 +101,6 @@
|
||||
"cache-manager": "^6.1.0",
|
||||
"cos-nodejs-sdk-v5": "^2.14.6",
|
||||
"cron-parser": "^4.9.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.7",
|
||||
"esdk-obs-nodejs": "^3.25.6",
|
||||
@@ -159,6 +158,8 @@
|
||||
"@types/node": "^18",
|
||||
"@types/nodemailer": "^6.4.8",
|
||||
"c8": "^10.1.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"esmock": "^2.7.5",
|
||||
"mocha": "^10.2.0",
|
||||
"prettier": "^2.8.8",
|
||||
"rimraf": "^5.0.5",
|
||||
|
||||
@@ -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 { setLogger } from '@certd/acme-client';
|
||||
import {HiddenMiddleware} from "./middleware/hidden.js";
|
||||
import { shouldSetDefaultNoCache } from './configuration-cache.js';
|
||||
// import * as swagger from '@midwayjs/swagger';
|
||||
//@ts-ignore
|
||||
// process.env.UV_THREADPOOL_SIZE = 2
|
||||
@@ -123,7 +124,7 @@ export class MainConfiguration {
|
||||
|
||||
this.app.getMiddleware().insertFirst(async (ctx: IMidwayKoaContext, next: NextFunction) => {
|
||||
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');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 { 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()
|
||||
@@ -40,8 +59,10 @@ export class FileController extends BaseController {
|
||||
userId = this.getUserId();
|
||||
}
|
||||
const filePath = this.fileService.getFile(key, userId);
|
||||
this.ctx.response.attachment(filePath);
|
||||
this.ctx.response.set('Cache-Control', 'public,max-age=2592000');
|
||||
await send(this.ctx, filePath);
|
||||
const sendOptions = getImageDownloadOptions(filePath);
|
||||
if (!sendOptions) {
|
||||
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 { 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()
|
||||
@@ -41,7 +59,7 @@ export class ConnectController extends BaseController {
|
||||
if (!publicSettings?.oauthEnabled) {
|
||||
throw new Error("OAuth功能未启用");
|
||||
}
|
||||
const setting = publicSettings?.oauthProviders?.[type || ""]
|
||||
const setting = publicSettings?.oauthProviders?.[type || ""] as OauthProviderSetting | undefined;
|
||||
if (!setting) {
|
||||
throw new Error(`未配置该OAuth类型:${type}`);
|
||||
}
|
||||
@@ -50,19 +68,38 @@ export class ConnectController extends BaseController {
|
||||
if (!addon) {
|
||||
throw new Error("初始化OAuth插件失败");
|
||||
}
|
||||
return addon as IOauthProvider;
|
||||
return {
|
||||
addon: addon as IOauthProvider,
|
||||
setting,
|
||||
};
|
||||
}
|
||||
|
||||
@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 bindUrl = installInfo?.bindUrl || "";
|
||||
//构造登录url
|
||||
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, {
|
||||
httpOnly: true,
|
||||
// secure: true,
|
||||
@@ -78,7 +115,7 @@ export class ConnectController extends BaseController {
|
||||
checkPlus()
|
||||
|
||||
//处理登录回调
|
||||
const addon = await this.getOauthProvider(type);
|
||||
const oauthProvider = await this.getOauthProvider(type);
|
||||
const request = this.ctx.request;
|
||||
// const ticketValue = this.codeService.getValidationValue(ticket);
|
||||
// if (!ticketValue) {
|
||||
@@ -98,7 +135,7 @@ export class ConnectController extends BaseController {
|
||||
const bindUrl = installInfo?.bindUrl || "";
|
||||
const currentUrl = `${bindUrl}api/oauth/callback/${type}?${request.querystring}`
|
||||
try {
|
||||
const tokenRes = await addon.onCallback({
|
||||
const tokenRes = await oauthProvider.addon.onCallback({
|
||||
code: query.code,
|
||||
state: query.state,
|
||||
ticketValue,
|
||||
@@ -108,11 +145,14 @@ export class ConnectController extends BaseController {
|
||||
const userInfo = tokenRes.userInfo;
|
||||
|
||||
const validationCode = await this.codeService.setValidationValue({
|
||||
type,
|
||||
type: getOauthBoundType(type, ticketValue.subtype),
|
||||
userInfo,
|
||||
});
|
||||
|
||||
const state = JSON.parse(utils.hash.base64Decode(query.state));
|
||||
let state = {forType:""}
|
||||
if (query.state) {
|
||||
state = JSON.parse(utils.hash.base64Decode(query.state));
|
||||
}
|
||||
|
||||
const redirectUrl = `${bindUrl}#/oauth/callback/${type}?validationCode=${validationCode}&forType=${state.forType}`;
|
||||
this.ctx.redirect(redirectUrl);
|
||||
@@ -126,8 +166,10 @@ export class ConnectController extends BaseController {
|
||||
@Post('/getLogoutUrl', { description: Constants.per.guest })
|
||||
public async logout(@Body(ALL) body: any) {
|
||||
checkPlus()
|
||||
const addon = await this.getOauthProvider(body.type);
|
||||
const { logoutUrl } = await addon.buildLogoutUrl(body);
|
||||
const oauthProvider = await this.getOauthProvider(body.type);
|
||||
const { logoutUrl } = await oauthProvider.addon.buildLogoutUrl({
|
||||
...body,
|
||||
});
|
||||
return this.ok({ logoutUrl });
|
||||
}
|
||||
|
||||
@@ -141,7 +183,7 @@ export class ConnectController extends BaseController {
|
||||
}
|
||||
|
||||
const type = validationValue.type;
|
||||
if (type !== body.type) {
|
||||
if (type !== body.type && !type.startsWith(`${body.type}:`)) {
|
||||
throw new Error("校验码错误");
|
||||
}
|
||||
const userInfo = validationValue.userInfo;
|
||||
@@ -259,16 +301,32 @@ export class ConnectController extends BaseController {
|
||||
provider.addonId = conf.addonId;
|
||||
provider.addonTitle = addonEntity.name;
|
||||
|
||||
const addon = await this.addonGetterService.getAddonById(conf.addonId,true,0,null);
|
||||
const {logoutUrl} = await addon.buildLogoutUrl();
|
||||
const addon = await this.addonGetterService.getAddonById(conf.addonId,true,0,null) as IOauthProvider & { icon?: string; types?: OauthProviderType[] };
|
||||
const {logoutUrl} = await addon.buildLogoutUrl({});
|
||||
if (logoutUrl){
|
||||
provider.logoutUrl = logoutUrl;
|
||||
}
|
||||
if(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);
|
||||
}
|
||||
|
||||
|
||||
@@ -135,6 +135,7 @@ export class SysSettingsController extends CrudController<SysSettingsService> {
|
||||
await this.service.savePrivateSettings(privateSettings);
|
||||
return this.ok({});
|
||||
}
|
||||
|
||||
@Post('/stopOtherUserTimer', { description: 'sys:settings:edit' })
|
||||
async stopOtherUserTimer(@Body(ALL) body) {
|
||||
await this.pipelineService.stopOtherUserPipeline(1);
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { BaseController, Constants } from '@certd/lib-server';
|
||||
import { ALL, Body, Controller, Inject, Post, Provide } from '@midwayjs/core';
|
||||
import { PasskeyService } from '../../../modules/login/service/passkey-service.js';
|
||||
import { RoleService } from '../../../modules/sys/authority/service/role-service.js';
|
||||
import { UserService } from '../../../modules/sys/authority/service/user-service.js';
|
||||
import { ApiTags } from '@midwayjs/swagger';
|
||||
import { BaseController, Constants, SysSettingsService } from "@certd/lib-server";
|
||||
import { ALL, Body, Controller, Inject, Post, Provide } from "@midwayjs/core";
|
||||
import { PasskeyService } from "../../../modules/login/service/passkey-service.js";
|
||||
import { RoleService } from "../../../modules/sys/authority/service/role-service.js";
|
||||
import { UserService } from "../../../modules/sys/authority/service/user-service.js";
|
||||
import { ApiTags } from "@midwayjs/swagger";
|
||||
import { CodeService } from "../../../modules/basic/service/code-service.js";
|
||||
|
||||
/**
|
||||
*/
|
||||
@Provide()
|
||||
@Controller('/api/mine')
|
||||
@ApiTags(['mine'])
|
||||
@Controller("/api/mine")
|
||||
@ApiTags(["mine"])
|
||||
export class MineController extends BaseController {
|
||||
@Inject()
|
||||
userService: UserService;
|
||||
@@ -20,36 +21,105 @@ export class MineController extends BaseController {
|
||||
@Inject()
|
||||
passkeyService: PasskeyService;
|
||||
|
||||
@Inject()
|
||||
codeService: CodeService;
|
||||
|
||||
@Post('/info', { description: Constants.per.authOnly, summary: "查询用户信息" })
|
||||
@Inject()
|
||||
sysSettingsService: SysSettingsService;
|
||||
|
||||
@Post("/info", { description: Constants.per.authOnly, summary: "查询用户信息" })
|
||||
public async info() {
|
||||
const userId = this.getUserId();
|
||||
const user = await this.userService.info(userId);
|
||||
const isWeak = await this.userService.checkPassword('123456', user.password, user.passwordVersion);
|
||||
const isWeak = await this.userService.checkPassword("123456", user.password, user.passwordVersion);
|
||||
if (isWeak) {
|
||||
//@ts-ignore
|
||||
user.isWeak = true;
|
||||
}
|
||||
const needInitPassword = user.password === "changeme";
|
||||
user.roleIds = await this.roleService.getRoleIdsByUserId(userId);
|
||||
delete user.password;
|
||||
//@ts-ignore
|
||||
user.needInitPassword = needInitPassword;
|
||||
return this.ok(user);
|
||||
}
|
||||
|
||||
@Post('/changePassword', { description: Constants.per.authOnly, summary: "修改密码" })
|
||||
@Post("/changePassword", { description: Constants.per.authOnly, summary: "修改密码" })
|
||||
public async changePassword(@Body(ALL) body: any) {
|
||||
const userId = this.getUserId();
|
||||
await this.userService.changePassword(userId, body);
|
||||
return this.ok({});
|
||||
}
|
||||
|
||||
@Post('/updateProfile', { description: Constants.per.authOnly, summary: "更新用户资料" })
|
||||
@Post("/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) {
|
||||
const userId = this.getUserId();
|
||||
|
||||
|
||||
await this.userService.updateProfile(userId, {
|
||||
avatar: body.avatar,
|
||||
nickName: body.nickName,
|
||||
});
|
||||
return this.ok({});
|
||||
}
|
||||
|
||||
@Post("/contact/capability", { description: Constants.per.authOnly, summary: "查询联系方式绑定能力" })
|
||||
public async contactCapability() {
|
||||
const settings = await this.sysSettingsService.getPrivateSettings();
|
||||
return this.ok({
|
||||
smsEnabled: !!settings.sms?.config?.accessId,
|
||||
});
|
||||
}
|
||||
|
||||
@Post("/contact/verifyIdentity", { description: Constants.per.authOnly, summary: "验证本人操作" })
|
||||
public async verifyContactIdentity(@Body(ALL) body: { identityType: "password" | "email" | "mobile"; identityPassword?: string; identityValidateCode?: string }) {
|
||||
const userId = this.getUserId();
|
||||
await this.userService.verifyIdentity(userId, body, this.codeService);
|
||||
const validationCode = this.codeService.setValidationValue({
|
||||
type: "contactIdentity",
|
||||
userId,
|
||||
identityType: body.identityType,
|
||||
});
|
||||
return this.ok({ validationCode });
|
||||
}
|
||||
|
||||
@Post("/contact/mobile", { description: Constants.per.authOnly, summary: "绑定或修改手机号" })
|
||||
public async updateMobile(@Body(ALL) body: { phoneCode?: string; mobile: string; validateCode: string; identityValidationCode: string }) {
|
||||
const userId = this.getUserId();
|
||||
this.userService.checkContactIdentityValidation(userId, body.identityValidationCode, this.codeService);
|
||||
await this.codeService.checkSmsCode({
|
||||
mobile: body.mobile,
|
||||
phoneCode: body.phoneCode || "86",
|
||||
smsCode: body.validateCode,
|
||||
verificationType: "bindMobile",
|
||||
throwError: true,
|
||||
});
|
||||
await this.userService.updateMobile(userId, {
|
||||
phoneCode: body.phoneCode,
|
||||
mobile: body.mobile,
|
||||
});
|
||||
return this.ok({});
|
||||
}
|
||||
|
||||
@Post("/contact/email", { description: Constants.per.authOnly, summary: "绑定或修改邮箱" })
|
||||
public async updateEmail(@Body(ALL) body: { email: string; validateCode: string; identityValidationCode: string }) {
|
||||
const userId = this.getUserId();
|
||||
this.userService.checkContactIdentityValidation(userId, body.identityValidationCode, this.codeService);
|
||||
this.codeService.checkEmailCode({
|
||||
email: body.email,
|
||||
validateCode: body.validateCode,
|
||||
verificationType: "bindEmail",
|
||||
throwError: true,
|
||||
});
|
||||
await this.userService.updateEmail(userId, {
|
||||
email: body.email,
|
||||
});
|
||||
return this.ok({});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Autoload, Init, Inject, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import { AutoInitSite } from "./auto-init-site.js";
|
||||
import { AutoLoadPlugins } from "./auto-load-plugins.js";
|
||||
import { AutoCron } from "./auto-cron.js";
|
||||
import { AutoMitterRegister } from "./auto-mitter-register.js";
|
||||
import { AutoPipelineEmitterRegister } from "./auto-pipeline-emitter-register.js";
|
||||
import { AutoFix } from "./auto-fix.js";
|
||||
import { AutoPrint } from "./auto-print.js";
|
||||
|
||||
@Autoload()
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
export class AutoARegister { //这个A是必须,让他排在第一个 进行init,否则会被其他init模块抢先注册导致报错
|
||||
@Inject()
|
||||
autoInitSite: AutoInitSite;
|
||||
|
||||
@Inject()
|
||||
autoLoadPlugins: AutoLoadPlugins;
|
||||
|
||||
@Inject()
|
||||
autoCron: AutoCron;
|
||||
|
||||
@Inject()
|
||||
autoMitterRegister: AutoMitterRegister;
|
||||
|
||||
@Inject()
|
||||
autoPipelineEmitterRegister: AutoPipelineEmitterRegister;
|
||||
|
||||
@Inject()
|
||||
autoPrint: AutoPrint;
|
||||
|
||||
@Inject()
|
||||
autoFix: AutoFix;
|
||||
|
||||
@Init()
|
||||
async init() {
|
||||
await this.autoInitSite.init();
|
||||
await this.autoLoadPlugins.init();
|
||||
await this.autoCron.init();
|
||||
await this.autoMitterRegister.init();
|
||||
await this.autoPipelineEmitterRegister.init();
|
||||
await this.autoFix.init();
|
||||
await this.autoPrint.init();
|
||||
}
|
||||
}
|
||||
+3
-4
@@ -1,7 +1,7 @@
|
||||
import { logger } from '@certd/basic';
|
||||
import { SysSettingsService, SysSiteInfo } from '@certd/lib-server';
|
||||
import { getPlusInfo, isPlus } from "@certd/plus-core";
|
||||
import { Autoload, Config, Init, Inject, Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import { Config, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import dayjs from "dayjs";
|
||||
import { Between } from "typeorm";
|
||||
import { DomainService } from '../cert/service/domain-service.js';
|
||||
@@ -14,9 +14,9 @@ import { PipelineService } from '../pipeline/service/pipeline-service.js';
|
||||
import { UserService } from "../sys/authority/service/user-service.js";
|
||||
import { ProjectService } from '../sys/enterprise/service/project-service.js';
|
||||
|
||||
@Autoload()
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
export class AutoCRegisterCron {
|
||||
export class AutoCron {
|
||||
@Inject()
|
||||
pipelineService: PipelineService;
|
||||
|
||||
@@ -53,7 +53,6 @@ export class AutoCRegisterCron {
|
||||
|
||||
|
||||
|
||||
@Init()
|
||||
async init() {
|
||||
logger.info('加载定时trigger开始');
|
||||
await this.pipelineService.onStartup(this.immediateTriggerOnce, this.onlyAdminUser);
|
||||
@@ -0,0 +1,301 @@
|
||||
import assert from "assert";
|
||||
import esmock from "esmock";
|
||||
import { AutoFix, buildEabAccountKeyValue, buildLegacyGoogleAccountConfigWhere, buildOauthBoundType, parseStorageValue } from "./auto-fix.js";
|
||||
|
||||
function createAutoFix(options: { pluginConfigService?: any; accessService?: any; storageService?: any; sysSettingsService?: any; oauthBoundService?: any }) {
|
||||
const autoFix = new AutoFix();
|
||||
autoFix.pluginConfigService = options.pluginConfigService;
|
||||
autoFix.accessService = options.accessService;
|
||||
autoFix.storageService = options.storageService;
|
||||
autoFix.sysSettingsService = options.sysSettingsService;
|
||||
autoFix.oauthBoundService = options.oauthBoundService;
|
||||
return autoFix;
|
||||
}
|
||||
|
||||
describe("AutoFix", () => {
|
||||
it("parses legacy storage values", () => {
|
||||
const config = parseStorageValue(
|
||||
JSON.stringify({
|
||||
value: {
|
||||
key: "legacy-private-key",
|
||||
accountUrl: "https://example.com/acct/1",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
assert.equal(config.key, "legacy-private-key");
|
||||
});
|
||||
|
||||
it("builds the EAB account key payload", () => {
|
||||
const payload = JSON.parse(buildEabAccountKeyValue("kid-1", "private-key"));
|
||||
|
||||
assert.deepEqual(payload, {
|
||||
kid: "kid-1",
|
||||
privateKey: "private-key",
|
||||
});
|
||||
});
|
||||
|
||||
it("builds legacy Google account config query by exact email key only", () => {
|
||||
assert.deepEqual(buildLegacyGoogleAccountConfigWhere("user@example.com"), {
|
||||
userId: 1,
|
||||
scope: "user",
|
||||
namespace: "1",
|
||||
key: "acme.config.google.user@example.com",
|
||||
});
|
||||
});
|
||||
|
||||
it("builds OAuth subtype bound type", () => {
|
||||
assert.equal(buildOauthBoundType("clogin", "alipay"), "clogin:alipay");
|
||||
assert.equal(buildOauthBoundType("github"), "github");
|
||||
});
|
||||
|
||||
it("finds legacy Google account config by exact email key only", async () => {
|
||||
let findOneWhere: any;
|
||||
let findCalled = false;
|
||||
const autoFix = createAutoFix({
|
||||
pluginConfigService: null as any,
|
||||
accessService: null as any,
|
||||
storageService: {
|
||||
getRepository() {
|
||||
return {
|
||||
async findOne(options: any) {
|
||||
findOneWhere = options.where;
|
||||
return {
|
||||
value: JSON.stringify({
|
||||
value: {
|
||||
privateKey: "legacy-private-key",
|
||||
},
|
||||
}),
|
||||
};
|
||||
},
|
||||
async find() {
|
||||
findCalled = true;
|
||||
return [];
|
||||
},
|
||||
};
|
||||
},
|
||||
} as any,
|
||||
});
|
||||
|
||||
const config = await autoFix.getLegacyGoogleAccountConfig("user@example.com");
|
||||
|
||||
assert.equal(config.privateKey, "legacy-private-key");
|
||||
assert.deepEqual(findOneWhere, buildLegacyGoogleAccountConfigWhere("user@example.com"));
|
||||
assert.equal(findCalled, false);
|
||||
});
|
||||
|
||||
it("does not query legacy Google account config without email", async () => {
|
||||
let repositoryCalled = false;
|
||||
const autoFix = createAutoFix({
|
||||
pluginConfigService: null as any,
|
||||
accessService: null as any,
|
||||
storageService: {
|
||||
getRepository() {
|
||||
repositoryCalled = true;
|
||||
return {};
|
||||
},
|
||||
} as any,
|
||||
});
|
||||
|
||||
const config = await autoFix.getLegacyGoogleAccountConfig();
|
||||
|
||||
assert.equal(config, null);
|
||||
assert.equal(repositoryCalled, false);
|
||||
});
|
||||
|
||||
it("skips Google common EAB account key fix outside commercial edition", async () => {
|
||||
let pluginConfigCalled = false;
|
||||
const autoFix = createAutoFix({
|
||||
pluginConfigService: {
|
||||
async getPluginConfig() {
|
||||
pluginConfigCalled = true;
|
||||
return null;
|
||||
},
|
||||
} as any,
|
||||
accessService: null as any,
|
||||
storageService: null as any,
|
||||
sysSettingsService: {
|
||||
async getPublicSettings() {
|
||||
return {
|
||||
oauthProviders: {},
|
||||
};
|
||||
},
|
||||
},
|
||||
oauthBoundService: {
|
||||
async transaction(callback: any) {
|
||||
return await callback({
|
||||
async findOne() {
|
||||
return null;
|
||||
},
|
||||
async update() {
|
||||
return { affected: 0 };
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await autoFix.init();
|
||||
|
||||
assert.equal(pluginConfigCalled, false);
|
||||
});
|
||||
|
||||
it("fixes Google common EAB account key in commercial edition", async () => {
|
||||
const { AutoFix: MockedAutoFix } = await esmock("./auto-fix.js", {
|
||||
"@certd/plus-core": {
|
||||
isComm: () => true,
|
||||
},
|
||||
});
|
||||
let getAccessByIdArgs: any[] = [];
|
||||
let findOneWhere: any;
|
||||
let updateAccessParam: any;
|
||||
const autoFix = new MockedAutoFix();
|
||||
autoFix.pluginConfigService = {
|
||||
async getPluginConfig(options: any) {
|
||||
assert.deepEqual(options, {
|
||||
name: "CertApply",
|
||||
type: "builtIn",
|
||||
});
|
||||
return {
|
||||
sysSetting: {
|
||||
input: {
|
||||
googleCommonEabAccessId: 12,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
autoFix.accessService = {
|
||||
async getAccessById(...args: any[]) {
|
||||
getAccessByIdArgs = args;
|
||||
return {
|
||||
kid: "kid-1",
|
||||
email: "user@example.com",
|
||||
};
|
||||
},
|
||||
async updateAccess(param: any) {
|
||||
updateAccessParam = param;
|
||||
},
|
||||
};
|
||||
autoFix.storageService = {
|
||||
getRepository() {
|
||||
return {
|
||||
async findOne(options: any) {
|
||||
findOneWhere = options.where;
|
||||
return {
|
||||
value: JSON.stringify({
|
||||
value: {
|
||||
privateKey: "legacy-private-key",
|
||||
},
|
||||
}),
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
await autoFix.fixGoogleCommonEabAccountKey();
|
||||
|
||||
assert.deepEqual(getAccessByIdArgs, [12, false]);
|
||||
assert.deepEqual(findOneWhere, buildLegacyGoogleAccountConfigWhere("user@example.com"));
|
||||
assert.deepEqual(updateAccessParam, {
|
||||
id: 12,
|
||||
eabType: "google",
|
||||
accountKey: buildEabAccountKeyValue("kid-1", "legacy-private-key"),
|
||||
});
|
||||
});
|
||||
|
||||
it("fixes legacy OAuth bound type from string addon loginType and converts loginType to array", async () => {
|
||||
const updates: any[] = [];
|
||||
const autoFix = createAutoFix({
|
||||
pluginConfigService: null as any,
|
||||
accessService: null as any,
|
||||
storageService: null as any,
|
||||
sysSettingsService: {
|
||||
async getPublicSettings() {
|
||||
return {
|
||||
oauthProviders: {
|
||||
clogin: {
|
||||
addonId: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
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 };
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await autoFix.fixOauthSubtypeBoundType();
|
||||
|
||||
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 autoFix = createAutoFix({
|
||||
pluginConfigService: null as any,
|
||||
accessService: null as any,
|
||||
storageService: null as any,
|
||||
sysSettingsService: {
|
||||
async getPublicSettings() {
|
||||
return {
|
||||
oauthProviders: {
|
||||
clogin: {
|
||||
addonId: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
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 };
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await autoFix.fixOauthSubtypeBoundType();
|
||||
|
||||
assert.equal(updateCalled, false);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,170 @@
|
||||
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import { logger } from "@certd/basic";
|
||||
import { AccessService, AddonEntity, SysSettingsService } from "@certd/lib-server";
|
||||
import { isComm } from "@certd/plus-core";
|
||||
import { PluginConfigService } from "../plugin/service/plugin-config-service.js";
|
||||
import { StorageService } from "../pipeline/service/storage-service.js";
|
||||
import { OauthBoundService } from "../login/service/oauth-bound-service.js";
|
||||
import { OauthBoundEntity } from "../login/entity/oauth-bound.js";
|
||||
|
||||
export function parseStorageValue(value?: string) {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return parsed?.value || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function buildEabAccountKeyValue(kid: string, privateKey: string) {
|
||||
return JSON.stringify({
|
||||
kid,
|
||||
privateKey,
|
||||
});
|
||||
}
|
||||
|
||||
export function buildLegacyGoogleAccountConfigWhere(email: string) {
|
||||
return {
|
||||
userId: 1,
|
||||
scope: "user",
|
||||
namespace: "1",
|
||||
key: `acme.config.google.${email}`,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildOauthBoundType(type: string, subtype?: string) {
|
||||
return subtype ? `${type}:${subtype}` : type;
|
||||
}
|
||||
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
export class AutoFix {
|
||||
@Inject()
|
||||
pluginConfigService: PluginConfigService;
|
||||
|
||||
@Inject()
|
||||
accessService: AccessService;
|
||||
|
||||
@Inject()
|
||||
storageService: StorageService;
|
||||
|
||||
@Inject()
|
||||
sysSettingsService: SysSettingsService;
|
||||
|
||||
@Inject()
|
||||
oauthBoundService: OauthBoundService;
|
||||
|
||||
async init() {
|
||||
await this.fixGoogleCommonEabAccountKey();
|
||||
await this.fixOauthSubtypeBoundType();
|
||||
}
|
||||
|
||||
async fixOauthSubtypeBoundType() {
|
||||
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) });
|
||||
}
|
||||
|
||||
async fixGoogleCommonEabAccountKey() {
|
||||
if (!isComm()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const certApplyConfig = await this.pluginConfigService.getPluginConfig({
|
||||
name: "CertApply",
|
||||
type: "builtIn",
|
||||
});
|
||||
const googleCommonEabAccessId = certApplyConfig?.sysSetting?.input?.googleCommonEabAccessId;
|
||||
if (!googleCommonEabAccessId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const eabAccess = await this.accessService.getAccessById(googleCommonEabAccessId, false);
|
||||
if (eabAccess.accountKey) {
|
||||
return;
|
||||
}
|
||||
if (!eabAccess.kid) {
|
||||
logger.info("公共Google EAB授权缺少KID,跳过历史ACME账号私钥修复");
|
||||
return;
|
||||
}
|
||||
|
||||
const accountConfig = await this.getLegacyGoogleAccountConfig(eabAccess.email);
|
||||
const privateKey = accountConfig?.privateKey || accountConfig?.key || accountConfig?.accountKey;
|
||||
if (!privateKey) {
|
||||
logger.info("未找到可迁移到公共Google EAB授权的历史ACME账号私钥");
|
||||
return;
|
||||
}
|
||||
|
||||
const accountKey = buildEabAccountKeyValue(eabAccess.kid, privateKey);
|
||||
await this.accessService.updateAccess({ id: googleCommonEabAccessId, eabType: "google", accountKey });
|
||||
logger.info(`已修复公共Google EAB授权的ACME账号私钥,accessId=${googleCommonEabAccessId}`);
|
||||
} catch (e: any) {
|
||||
logger.error("修复公共Google EAB授权ACME账号私钥失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
async getLegacyGoogleAccountConfig(email?: string) {
|
||||
if (!email) {
|
||||
return null;
|
||||
}
|
||||
const repository = this.storageService.getRepository();
|
||||
const exact = await repository.findOne({
|
||||
where: buildLegacyGoogleAccountConfigWhere(email),
|
||||
});
|
||||
const exactValue = this.parseStorageValue(exact?.value);
|
||||
if (exactValue?.key || exactValue?.privateKey || exactValue?.accountKey) {
|
||||
return exactValue;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
parseStorageValue(value?: string) {
|
||||
return parseStorageValue(value);
|
||||
}
|
||||
}
|
||||
+10
-5
@@ -1,14 +1,14 @@
|
||||
import { logger } from '@certd/basic';
|
||||
import { PlusService, SysInstallInfo, SysPrivateSettings, SysSettingsService } from '@certd/lib-server';
|
||||
import { Autoload, Config, Init, Inject, Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import { EncryptService, PlusService, SysInstallInfo, SysPrivateSettings, SysSettingsService } from '@certd/lib-server';
|
||||
import { Config, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import crypto from 'crypto';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { UserService } from '../sys/authority/service/user-service.js';
|
||||
import { SafeService } from "../sys/settings/safe-service.js";
|
||||
|
||||
@Autoload()
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
export class AutoAInitSite {
|
||||
export class AutoInitSite {
|
||||
@Inject()
|
||||
userService: UserService;
|
||||
|
||||
@@ -22,7 +22,10 @@ export class AutoAInitSite {
|
||||
@Inject()
|
||||
safeService: SafeService;
|
||||
|
||||
@Init()
|
||||
@Inject()
|
||||
encryptService: EncryptService;
|
||||
|
||||
|
||||
async init() {
|
||||
logger.info('初始化站点开始');
|
||||
await this.startOptimizeDb();
|
||||
@@ -51,6 +54,8 @@ export class AutoAInitSite {
|
||||
//加载一次密钥
|
||||
await this.sysSettingsService.getSecret();
|
||||
|
||||
//初始化加密服务
|
||||
await this.encryptService.doInit();
|
||||
|
||||
// 授权许可
|
||||
try {
|
||||
+3
-4
@@ -1,16 +1,15 @@
|
||||
import { Autoload, Init, Inject, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import { logger } from "@certd/basic";
|
||||
import { PluginService } from "../plugin/service/plugin-service.js";
|
||||
import { registerPaymentProviders } from "../suite/payments/index.js";
|
||||
|
||||
@Autoload()
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
export class AutoBLoadPlugins {
|
||||
export class AutoLoadPlugins {
|
||||
@Inject()
|
||||
pluginService: PluginService;
|
||||
|
||||
|
||||
@Init()
|
||||
async init() {
|
||||
logger.info(`加载插件开始,加载模式:${process.env.certd_plugin_loadmode}`);
|
||||
if (process.env.certd_plugin_loadmode === "metadata") {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user