Compare commits

...

43 Commits

Author SHA1 Message Date
xiaojunnuo 0b9933df1e perf: 查看证书增加证书详情显示,包括域名,过期时间,颁发机构,指纹等 2026-03-16 00:52:33 +08:00
xiaojunnuo 76d12d6062 perf: dns-provider 支持bind9 ,support bind9
https://github.com/certd/certd/issues/683
https://github.com/certd/certd/discussions/668
2026-03-15 23:55:49 +08:00
xiaojunnuo cf10faf61c style: 调整复制按钮的显示样式为行内弹性布局 2026-03-15 18:35:13 +08:00
xiaojunnuo 1cbf9c1cd9 chore: 增加流水线,授权等文档 2026-03-15 18:26:49 +08:00
xiaojunnuo 25e361b9f9 chore: 修改权限判断字段从summary改成description 2026-03-15 16:20:20 +08:00
xiaojunnuo b88ee33ae4 chore: tencent cos doc tip 2026-03-15 16:05:17 +08:00
xiaojunnuo 684964da4f chore: swagger support 2026-03-15 14:01:34 +08:00
xiaojunnuo 8a3841f638 perf: 支持批量转移流水线到其他项目 2026-03-15 04:17:40 +08:00
xiaojunnuo f642e42eea chore: 优化passkey 2026-03-15 02:20:39 +08:00
xiaojunnuo bbef854c02 chore: user profile 夜间模式 2026-03-13 19:44:55 +08:00
xiaojunnuo e50611666e perf: 优化个人账户页面 2026-03-13 19:39:27 +08:00
xiaojunnuo eae4f721e8 chore: passkey登录优化 2026-03-13 15:31:03 +08:00
xiaojunnuo 12fed34e10 fix: 修复提示支付失败的bug 2026-03-13 12:03:28 +08:00
xiaojunnuo 56350b54ee Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-03-12 18:11:09 +08:00
xiaojunnuo 10b7644bb7 perf: 支持passkey登录 2026-03-12 18:11:02 +08:00
xiaojunnuo d79db3bd3f perf: 获取阿里证书订单id组件增加翻页功能,突破50的上限 2026-03-12 00:46:05 +08:00
xiaojunnuo 1588461633 perf: 优化阿里云连接超时时长为10秒,支持配置环境变量 2026-03-11 23:10:37 +08:00
xiaojunnuo dd999b60a4 fix: 修复当证书更新后第一次站点检查会报与主站证书过期时间不一致错误的bug 2026-03-11 22:38:48 +08:00
xiaojunnuo 3abee72fee fix: 修复修改项目名称后,没有同步刷新的bug
https://github.com/certd/certd/issues/680
2026-03-11 22:36:05 +08:00
xiaojunnuo b5577b1d37 build: release 2026-03-10 00:21:08 +08:00
xiaojunnuo e15ffb5820 build: publish 2026-03-10 00:03:15 +08:00
xiaojunnuo 4d9a5ed4a1 build: trigger build image 2026-03-10 00:03:03 +08:00
xiaojunnuo b2bc1debe0 chore: release 2026-03-10 00:02:46 +08:00
xiaojunnuo 590ff67fcb v1.39.1 2026-03-09 23:47:08 +08:00
xiaojunnuo 209e1adf53 build: prepare to build 2026-03-09 23:44:19 +08:00
xiaojunnuo 53c08484a3 chore: project transfer 2026-03-09 23:43:23 +08:00
xiaojunnuo c6ca832737 perf: 支持迁移个人数据到企业项目中 2026-03-09 23:34:11 +08:00
xiaojunnuo 2c399a078e Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-03-09 13:35:18 +08:00
xiaojunnuo 8c519f13da chore: 1 2026-03-09 13:34:26 +08:00
xiaojunnuo 853fdc70a2 perf: install tip 2026-03-08 11:15:25 +08:00
xiaojunnuo dc4f811eaa build: release 2026-03-08 01:57:31 +08:00
xiaojunnuo d23c8b4a2a fix: 修复企业管理模式下,切换用户登录后,丢失项目列表的bug 2026-03-08 01:53:46 +08:00
xiaojunnuo 00c0dcc81d build: release 2026-03-08 01:47:46 +08:00
xiaojunnuo f77feefdb8 chore: github action update 2026-03-08 01:33:34 +08:00
xiaojunnuo 2e346e5369 build: publish 2026-03-08 01:19:01 +08:00
xiaojunnuo 17023f6b55 build: trigger build image 2026-03-08 01:18:50 +08:00
xiaojunnuo 3bb29abe32 v1.39.0 2026-03-08 01:17:39 +08:00
xiaojunnuo ac42d38b7a build: prepare to build 2026-03-08 01:15:23 +08:00
xiaojunnuo d9c0130b59 fix: 修复京东云域名申请证书报错的bug 2026-03-08 01:14:33 +08:00
xiaojunnuo 4925d5a5e7 chore: project prerelease 2026-03-08 00:48:29 +08:00
xiaojunnuo dd9a7cf5d7 chore: project fix 2026-03-05 00:11:08 +08:00
xiaojunnuo 5ee3874b7e chore: project fix 2026-03-04 23:53:19 +08:00
xiaojunnuo 17dd77cc96 chore: project userid fixed -1 2026-03-04 23:15:48 +08:00
205 changed files with 4459 additions and 1075 deletions
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
with: with:
time: '10' # for 60 seconds time: '10' # for 60 seconds
- name: deploy-certd-demo - name: deploy-certd-demo
uses: tyrrrz/action-http-request@master uses: tyrrrz/action-http-request@prime
with: with:
# 通过webhook 触发 certd-demo来部署 # 通过webhook 触发 certd-demo来部署
url: ${{ secrets.WEBHOOK_CERTD_DEMO }} url: ${{ secrets.WEBHOOK_CERTD_DEMO }}
+1 -1
View File
@@ -118,7 +118,7 @@ jobs:
# greper/certd-agent:latest # greper/certd-agent:latest
# greper/certd-agent:${{steps.get_certd_version.outputs.result}} # greper/certd-agent:${{steps.get_certd_version.outputs.result}}
- name: deploy-certd-doc - name: deploy-certd-doc
uses: tyrrrz/action-http-request@master uses: tyrrrz/action-http-request@prime
with: with:
url: ${{ secrets.WEBHOOK_CERTD_DOC }} url: ${{ secrets.WEBHOOK_CERTD_DOC }}
method: POST method: POST
+15 -1
View File
@@ -65,6 +65,20 @@ demoKeyId = '';
encrypt: true, //该属性是否需要加密 encrypt: true, //该属性是否需要加密
}) })
demoKeySecret = ''; demoKeySecret = '';
@AccessInput({
title: '另外一个授权Id',//标题
component: {
name:"access-selector", //access选择组件
vModel:"modelValue",
type: "ssh", // access类型,让用户固定选择这种类型的access
},
required: true, //text组件可以省略
})
otherAccessId;
``` ```
### 4. 实现测试方法 ### 4. 实现测试方法
@@ -93,7 +107,7 @@ async onTestRequest() {
```typescript ```typescript
/** /**
* api接口示例 取域名列表, * api接口示例 取域名列表,
*/ */
async GetDomainList(req: PageSearch): Promise<PageRes<DomainRecord>> { async GetDomainList(req: PageSearch): Promise<PageRes<DomainRecord>> {
//输出日志必须使用ctx.logger //输出日志必须使用ctx.logger
+1 -1
View File
@@ -2,7 +2,7 @@
## 什么是插件转换工具 ## 什么是插件转换工具
插件转换工具是一个用于将 Certd 插件转换为 YAML 配置文件的命令行工具。它可以分析单个插件文件,识别插件类型,并生成对应的 YAML 配置,方便插件的注册和管理 插件转换工具是一个用于将 Certd 插件转换为 YAML 配置文件的命令行工具。它可以分析单个插件文件,识别插件类型,并生成对应的 YAML 配置,可以让插件分发和在线注册
## 工具位置 ## 工具位置
+2 -1
View File
@@ -80,7 +80,8 @@ certDomains!: string[];
helper: 'demoAccess授权', helper: 'demoAccess授权',
component: { component: {
name: 'access-selector', name: 'access-selector',
type: 'demo', // 固定授权类型 vModel:"modelValue",
type: "demo", // access类型,让用户固定选择这种类型的access
}, },
// rules: [{ required: true, message: '此项必填' }], // rules: [{ required: true, message: '此项必填' }],
// required: true, // 必填 // required: true, // 必填
+39
View File
@@ -3,6 +3,45 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.1](https://github.com/certd/certd/compare/v1.39.0...v1.39.1) (2026-03-09)
### Bug Fixes
* 修复企业管理模式下,切换用户登录后,丢失项目列表的bug ([d23c8b4](https://github.com/certd/certd/commit/d23c8b4a2a5f5ab17822c6ee1d4108ac7280b9d1))
### Performance Improvements
* 支持迁移个人数据到企业项目中 ([c6ca832](https://github.com/certd/certd/commit/c6ca83273779ed84de1b23b5e477063af043d015))
* install tip ([853fdc7](https://github.com/certd/certd/commit/853fdc70a263b62d75c9ff3970607e6bf1c1593b))
# [1.39.0](https://github.com/certd/certd/compare/v1.38.12...v1.39.0) (2026-03-07)
### Bug Fixes
* 修复部署到openwrt错误的bug ([2e3d0cc](https://github.com/certd/certd/commit/2e3d0cc57c16c48ad435bc8fde729bacaedde9f5))
* 修复发件邮箱无法输入的bug ([27b0348](https://github.com/certd/certd/commit/27b0348e1d3d752f418f851965d6afbc26c0160c))
* 修复复制流水线保存后丢失分组和排序号的问题 ([bc32648](https://github.com/certd/certd/commit/bc326489abc1d50a0930b4f47aa2d62d3a486798))
* 修复获取群辉deviceid报错的bug ([79be392](https://github.com/certd/certd/commit/79be392775a2c91848dd5a66a2618adc4e4b48f6))
* 修复京东云域名申请证书报错的bug ([d9c0130](https://github.com/certd/certd/commit/d9c0130b59997144a3c274d456635b800135e43f))
* 修复偶尔下载证书报未授权的错误 ([316537e](https://github.com/certd/certd/commit/316537eb4dcbe5ec57784e8bf95ee3cdfd21dce7))
* 修复dcdn多个域名同时部署时 可能会出现证书名称重复的bug ([78c2ced](https://github.com/certd/certd/commit/78c2ced43b1a73d142b0ed783b162b97f545ab06))
* 优化dcdn部署上传多次证书 偶尔报 The CertName already exists的问题 ([72f850f](https://github.com/certd/certd/commit/72f850f675b500d12ebff2338d1b99d6fab476e1))
* **cert-plugin:** 优化又拍云客户端错误处理逻辑,当域名已绑定证书时不再抛出异常。 ([92c9ac3](https://github.com/certd/certd/commit/92c9ac382692e6c84140ff787759ab6d39ccbe96))
* esxi部署失败的bug ([1e44115](https://github.com/certd/certd/commit/1e441154617e6516a9a3610412bf597128c62696))
### Features
* 支持企业级管理模式,项目管理,细分权限 ([3734083](https://github.com/certd/certd/commit/37340838b6a61a94b86bfa13cf5da88b26f1315a))
### Performance Improvements
* 【破坏性更新】错误返回信息msg字段名统一改成message,与成功的返回结构一致 ([51ab6d6](https://github.com/certd/certd/commit/51ab6d6da1bb551b55b3a6a4a9a945c8d6ace806))
* 当域名管理中没有域名时,创建流水线时不展开域名选择框 ([bb0afe1](https://github.com/certd/certd/commit/bb0afe1fa7b0fc52fde051d24fbe6be69d52f4cc))
* 任务步骤页面增加串行执行提示说明 ([787f6ef](https://github.com/certd/certd/commit/787f6ef52893d8dc912ee2a7a5b8ce2b73c108c9))
* 站点监控支持指定ip地址检查 ([83d81b6](https://github.com/certd/certd/commit/83d81b64b3adb375366039e07c87d1ad79121c13))
* AI开发插件 skills 定义初步 ([1f68fad](https://github.com/certd/certd/commit/1f68faddb97a978c5a5e731a8895b4bb0587ad83))
* http请求增加建立连接超时配置 ([3c85602](https://github.com/certd/certd/commit/3c85602ab1fc1953cdc06a6cd75a971d14119179))
## [1.38.12](https://github.com/certd/certd/compare/v1.38.11...v1.38.12) (2026-02-18) ## [1.38.12](https://github.com/certd/certd/compare/v1.38.11...v1.38.12) (2026-02-18)
### Bug Fixes ### Bug Fixes
+39
View File
@@ -3,6 +3,45 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.1](https://github.com/certd/certd/compare/v1.39.0...v1.39.1) (2026-03-09)
### Bug Fixes
* 修复企业管理模式下,切换用户登录后,丢失项目列表的bug ([d23c8b4](https://github.com/certd/certd/commit/d23c8b4a2a5f5ab17822c6ee1d4108ac7280b9d1))
### Performance Improvements
* 支持迁移个人数据到企业项目中 ([c6ca832](https://github.com/certd/certd/commit/c6ca83273779ed84de1b23b5e477063af043d015))
* install tip ([853fdc7](https://github.com/certd/certd/commit/853fdc70a263b62d75c9ff3970607e6bf1c1593b))
# [1.39.0](https://github.com/certd/certd/compare/v1.38.12...v1.39.0) (2026-03-07)
### Bug Fixes
* 修复部署到openwrt错误的bug ([2e3d0cc](https://github.com/certd/certd/commit/2e3d0cc57c16c48ad435bc8fde729bacaedde9f5))
* 修复发件邮箱无法输入的bug ([27b0348](https://github.com/certd/certd/commit/27b0348e1d3d752f418f851965d6afbc26c0160c))
* 修复复制流水线保存后丢失分组和排序号的问题 ([bc32648](https://github.com/certd/certd/commit/bc326489abc1d50a0930b4f47aa2d62d3a486798))
* 修复获取群辉deviceid报错的bug ([79be392](https://github.com/certd/certd/commit/79be392775a2c91848dd5a66a2618adc4e4b48f6))
* 修复京东云域名申请证书报错的bug ([d9c0130](https://github.com/certd/certd/commit/d9c0130b59997144a3c274d456635b800135e43f))
* 修复偶尔下载证书报未授权的错误 ([316537e](https://github.com/certd/certd/commit/316537eb4dcbe5ec57784e8bf95ee3cdfd21dce7))
* 修复dcdn多个域名同时部署时 可能会出现证书名称重复的bug ([78c2ced](https://github.com/certd/certd/commit/78c2ced43b1a73d142b0ed783b162b97f545ab06))
* 优化dcdn部署上传多次证书 偶尔报 The CertName already exists的问题 ([72f850f](https://github.com/certd/certd/commit/72f850f675b500d12ebff2338d1b99d6fab476e1))
* **cert-plugin:** 优化又拍云客户端错误处理逻辑,当域名已绑定证书时不再抛出异常。 ([92c9ac3](https://github.com/certd/certd/commit/92c9ac382692e6c84140ff787759ab6d39ccbe96))
* esxi部署失败的bug ([1e44115](https://github.com/certd/certd/commit/1e441154617e6516a9a3610412bf597128c62696))
### Features
* 支持企业级管理模式,项目管理,细分权限 ([3734083](https://github.com/certd/certd/commit/37340838b6a61a94b86bfa13cf5da88b26f1315a))
### Performance Improvements
* 【破坏性更新】错误返回信息msg字段名统一改成message,与成功的返回结构一致 ([51ab6d6](https://github.com/certd/certd/commit/51ab6d6da1bb551b55b3a6a4a9a945c8d6ace806))
* 当域名管理中没有域名时,创建流水线时不展开域名选择框 ([bb0afe1](https://github.com/certd/certd/commit/bb0afe1fa7b0fc52fde051d24fbe6be69d52f4cc))
* 任务步骤页面增加串行执行提示说明 ([787f6ef](https://github.com/certd/certd/commit/787f6ef52893d8dc912ee2a7a5b8ce2b73c108c9))
* 站点监控支持指定ip地址检查 ([83d81b6](https://github.com/certd/certd/commit/83d81b64b3adb375366039e07c87d1ad79121c13))
* AI开发插件 skills 定义初步 ([1f68fad](https://github.com/certd/certd/commit/1f68faddb97a978c5a5e731a8895b4bb0587ad83))
* http请求增加建立连接超时配置 ([3c85602](https://github.com/certd/certd/commit/3c85602ab1fc1953cdc06a6cd75a971d14119179))
## [1.38.12](https://github.com/certd/certd/compare/v1.38.11...v1.38.12) (2026-02-18) ## [1.38.12](https://github.com/certd/certd/compare/v1.38.11...v1.38.12) (2026-02-18)
### Bug Fixes ### Bug Fixes
+7 -2
View File
@@ -16,7 +16,6 @@ https://1panel.cn/docs/installation/online_installation/
![](./images/store-1.png) ![](./images/store-1.png)
![](./images/store-2.png) ![](./images/store-2.png)
#### 1.2 访问测试: #### 1.2 访问测试:
@@ -40,6 +39,9 @@ admin/123456
1. 打开`docker-compose.yaml`,整个内容复制下来 1. 打开`docker-compose.yaml`,整个内容复制下来
https://gitee.com/certd/certd/raw/v2/docker/run/docker-compose.yaml https://gitee.com/certd/certd/raw/v2/docker/run/docker-compose.yaml
::: tip
默认使用SQLite数据库,如果需要使用MySQL、PostgreSQL数据库,请参考[多数据库支持](./install/database.md)
:::
2. 然后到 `1Panel->容器->编排->新建编排` 2. 然后到 `1Panel->容器->编排->新建编排`
输入名称,粘贴`docker-compose.yaml`原文内容 输入名称,粘贴`docker-compose.yaml`原文内容
@@ -49,7 +51,10 @@ admin/123456
![](./images/2.png) ![](./images/2.png)
> 默认使用sqlite数据库,数据保存在`/data/certd`目录下,您可以手动备份该目录 > 默认使用sqlite数据库,数据保存在`/data/certd`目录下,您可以手动备份该目录
> certd还支持`mysql`和`postgresql`数据库,[点我了解如何切换其他数据库](../database)
#### 2.2 访问测试 #### 2.2 访问测试
+3 -1
View File
@@ -30,7 +30,9 @@
点击确定,等待启动完成 点击确定,等待启动完成
![](./images/2.png) ![](./images/2.png)
> certd默认使用sqlite数据库,另外支持`mysql`和`postgresql`数据库,[点我了解如何切换其他数据库](../database) ::: tip
默认安装使用SQLite数据库,如果需要使用MySQL、PostgreSQL数据库,请参考[多数据库支持](./install/database.md)
:::
## 二、访问应用 ## 二、访问应用
+3 -2
View File
@@ -42,8 +42,9 @@ docker compose up -d
> 如果提示 没有docker compose命令,请安装docker-compose > 如果提示 没有docker compose命令,请安装docker-compose
> https://docs.docker.com/compose/install/linux/ > https://docs.docker.com/compose/install/linux/
> certd默认使用sqlite数据库,另外还支持`mysql`和`postgresql`数据库,[点我了解如何切换其他数据库](../database) ::: tip
默认安装使用SQLite数据库,如果需要使用MySQL、PostgreSQL数据库,请参考[多数据库支持](./install/database.md)
:::
### 3. 访问测试 ### 3. 访问测试
+5
View File
@@ -28,6 +28,11 @@ https://certd.handsfree.work/
2. [Docker方式部署](./install/docker/) 2. [Docker方式部署](./install/docker/)
3. [源码方式部署](./install/source/) 3. [源码方式部署](./install/source/)
::: tip
默认安装使用SQLite数据库,如果需要使用MySQL、PostgreSQL数据库,请参考[多数据库支持](./install/database.md)
:::
### 2. 访问测试 ### 2. 访问测试
+1 -1
View File
@@ -9,5 +9,5 @@
} }
}, },
"npmClient": "pnpm", "npmClient": "pnpm",
"version": "1.38.12" "version": "1.39.1"
} }
+1
View File
@@ -37,6 +37,7 @@
"docs:preview": "vitepress preview docs", "docs:preview": "vitepress preview docs",
"pub": "echo 1", "pub": "echo 1",
"dev": "pnpm run -r --parallel compile ", "dev": "pnpm run -r --parallel compile ",
"pub_all":"pnpm run -r --parallel pub ",
"release": "time /t >trigger/release.trigger && git add trigger/release.trigger && git commit -m \"build: release\" && git push", "release": "time /t >trigger/release.trigger && git add trigger/release.trigger && git commit -m \"build: release\" && git push",
"publish_to_atomgit": "node --experimental-json-modules ./scripts/publish-atomgit.js", "publish_to_atomgit": "node --experimental-json-modules ./scripts/publish-atomgit.js",
"publish_to_gitee": "node --experimental-json-modules ./scripts/publish-gitee.js", "publish_to_gitee": "node --experimental-json-modules ./scripts/publish-gitee.js",
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.1](https://github.com/publishlab/node-acme-client/compare/v1.39.0...v1.39.1) (2026-03-09)
**Note:** Version bump only for package @certd/acme-client
# [1.39.0](https://github.com/publishlab/node-acme-client/compare/v1.38.12...v1.39.0) (2026-03-07)
**Note:** Version bump only for package @certd/acme-client
## [1.38.12](https://github.com/publishlab/node-acme-client/compare/v1.38.11...v1.38.12) (2026-02-18) ## [1.38.12](https://github.com/publishlab/node-acme-client/compare/v1.38.11...v1.38.12) (2026-02-18)
**Note:** Version bump only for package @certd/acme-client **Note:** Version bump only for package @certd/acme-client
+3 -3
View File
@@ -3,7 +3,7 @@
"description": "Simple and unopinionated ACME client", "description": "Simple and unopinionated ACME client",
"private": false, "private": false,
"author": "nmorsman", "author": "nmorsman",
"version": "1.38.12", "version": "1.39.1",
"type": "module", "type": "module",
"module": "scr/index.js", "module": "scr/index.js",
"main": "src/index.js", "main": "src/index.js",
@@ -18,7 +18,7 @@
"types" "types"
], ],
"dependencies": { "dependencies": {
"@certd/basic": "^1.38.12", "@certd/basic": "^1.39.1",
"@peculiar/x509": "^1.11.0", "@peculiar/x509": "^1.11.0",
"asn1js": "^3.0.5", "asn1js": "^3.0.5",
"axios": "^1.9.0", "axios": "^1.9.0",
@@ -70,5 +70,5 @@
"bugs": { "bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues" "url": "https://github.com/publishlab/node-acme-client/issues"
}, },
"gitHead": "49457505cdf8156fd9d936b8e9ace0b48e43a6b2" "gitHead": "590ff67fcb40ff8ba0f7b2a9592b51d9fb54a2ef"
} }
+14
View File
@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.1](https://github.com/certd/certd/compare/v1.39.0...v1.39.1) (2026-03-09)
**Note:** Version bump only for package @certd/basic
# [1.39.0](https://github.com/certd/certd/compare/v1.38.12...v1.39.0) (2026-03-07)
### Bug Fixes
* esxi部署失败的bug ([1e44115](https://github.com/certd/certd/commit/1e441154617e6516a9a3610412bf597128c62696))
### Performance Improvements
* http请求增加建立连接超时配置 ([3c85602](https://github.com/certd/certd/commit/3c85602ab1fc1953cdc06a6cd75a971d14119179))
## [1.38.12](https://github.com/certd/certd/compare/v1.38.11...v1.38.12) (2026-02-18) ## [1.38.12](https://github.com/certd/certd/compare/v1.38.11...v1.38.12) (2026-02-18)
**Note:** Version bump only for package @certd/basic **Note:** Version bump only for package @certd/basic
+1 -1
View File
@@ -1 +1 @@
23:18 23:44
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "@certd/basic", "name": "@certd/basic",
"private": false, "private": false,
"version": "1.38.12", "version": "1.39.1",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
@@ -47,5 +47,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "49457505cdf8156fd9d936b8e9ace0b48e43a6b2" "gitHead": "590ff67fcb40ff8ba0f7b2a9592b51d9fb54a2ef"
} }
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.1](https://github.com/certd/certd/compare/v1.39.0...v1.39.1) (2026-03-09)
**Note:** Version bump only for package @certd/pipeline
# [1.39.0](https://github.com/certd/certd/compare/v1.38.12...v1.39.0) (2026-03-07)
**Note:** Version bump only for package @certd/pipeline
## [1.38.12](https://github.com/certd/certd/compare/v1.38.11...v1.38.12) (2026-02-18) ## [1.38.12](https://github.com/certd/certd/compare/v1.38.11...v1.38.12) (2026-02-18)
**Note:** Version bump only for package @certd/pipeline **Note:** Version bump only for package @certd/pipeline
+4 -4
View File
@@ -1,7 +1,7 @@
{ {
"name": "@certd/pipeline", "name": "@certd/pipeline",
"private": false, "private": false,
"version": "1.38.12", "version": "1.39.1",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
@@ -18,8 +18,8 @@
"compile": "tsc --skipLibCheck --watch" "compile": "tsc --skipLibCheck --watch"
}, },
"dependencies": { "dependencies": {
"@certd/basic": "^1.38.12", "@certd/basic": "^1.39.1",
"@certd/plus-core": "^1.38.12", "@certd/plus-core": "^1.39.1",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"reflect-metadata": "^0.1.13" "reflect-metadata": "^0.1.13"
@@ -45,5 +45,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "49457505cdf8156fd9d936b8e9ace0b48e43a6b2" "gitHead": "590ff67fcb40ff8ba0f7b2a9592b51d9fb54a2ef"
} }
+7 -3
View File
@@ -170,9 +170,7 @@ export abstract class AbstractTaskPlugin implements ITaskPlugin {
} }
if (this.ctx?.define?.onlyAdmin) { if (this.ctx?.define?.onlyAdmin) {
if (!this.isAdmin()) { this.checkAdmin();
throw new Error("只有管理员才能运行此任务");
}
} }
} }
@@ -284,6 +282,12 @@ export abstract class AbstractTaskPlugin implements ITaskPlugin {
return this.ctx.user.role === "admin"; return this.ctx.user.role === "admin";
} }
checkAdmin() {
if (!this.isAdmin()) {
throw new Error("只有“管理员”或“系统级项目”才有权限运行此插件任务");
}
}
getStepFromPipeline(stepId: string) { getStepFromPipeline(stepId: string) {
let found: any = null; let found: any = null;
RunnableCollection.each(this.ctx.pipeline.stages, step => { RunnableCollection.each(this.ctx.pipeline.stages, step => {
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.1](https://github.com/certd/certd/compare/v1.39.0...v1.39.1) (2026-03-09)
**Note:** Version bump only for package @certd/lib-huawei
# [1.39.0](https://github.com/certd/certd/compare/v1.38.12...v1.39.0) (2026-03-07)
**Note:** Version bump only for package @certd/lib-huawei
## [1.38.12](https://github.com/certd/certd/compare/v1.38.11...v1.38.12) (2026-02-18) ## [1.38.12](https://github.com/certd/certd/compare/v1.38.11...v1.38.12) (2026-02-18)
**Note:** Version bump only for package @certd/lib-huawei **Note:** Version bump only for package @certd/lib-huawei
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "@certd/lib-huawei", "name": "@certd/lib-huawei",
"private": false, "private": false,
"version": "1.38.12", "version": "1.39.1",
"main": "./dist/bundle.js", "main": "./dist/bundle.js",
"module": "./dist/bundle.js", "module": "./dist/bundle.js",
"types": "./dist/d/index.d.ts", "types": "./dist/d/index.d.ts",
@@ -24,5 +24,5 @@
"prettier": "^2.8.8", "prettier": "^2.8.8",
"tslib": "^2.8.1" "tslib": "^2.8.1"
}, },
"gitHead": "49457505cdf8156fd9d936b8e9ace0b48e43a6b2" "gitHead": "590ff67fcb40ff8ba0f7b2a9592b51d9fb54a2ef"
} }
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.1](https://github.com/certd/certd/compare/v1.39.0...v1.39.1) (2026-03-09)
**Note:** Version bump only for package @certd/lib-iframe
# [1.39.0](https://github.com/certd/certd/compare/v1.38.12...v1.39.0) (2026-03-07)
**Note:** Version bump only for package @certd/lib-iframe
## [1.38.12](https://github.com/certd/certd/compare/v1.38.11...v1.38.12) (2026-02-18) ## [1.38.12](https://github.com/certd/certd/compare/v1.38.11...v1.38.12) (2026-02-18)
**Note:** Version bump only for package @certd/lib-iframe **Note:** Version bump only for package @certd/lib-iframe
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "@certd/lib-iframe", "name": "@certd/lib-iframe",
"private": false, "private": false,
"version": "1.38.12", "version": "1.39.1",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
@@ -31,5 +31,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "49457505cdf8156fd9d936b8e9ace0b48e43a6b2" "gitHead": "590ff67fcb40ff8ba0f7b2a9592b51d9fb54a2ef"
} }
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.1](https://github.com/certd/certd/compare/v1.39.0...v1.39.1) (2026-03-09)
**Note:** Version bump only for package @certd/jdcloud
# [1.39.0](https://github.com/certd/certd/compare/v1.38.12...v1.39.0) (2026-03-07)
**Note:** Version bump only for package @certd/jdcloud
## [1.38.12](https://github.com/certd/certd/compare/v1.38.11...v1.38.12) (2026-02-18) ## [1.38.12](https://github.com/certd/certd/compare/v1.38.11...v1.38.12) (2026-02-18)
**Note:** Version bump only for package @certd/jdcloud **Note:** Version bump only for package @certd/jdcloud
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@certd/jdcloud", "name": "@certd/jdcloud",
"version": "1.38.12", "version": "1.39.1",
"description": "jdcloud openApi sdk", "description": "jdcloud openApi sdk",
"main": "./dist/bundle.js", "main": "./dist/bundle.js",
"module": "./dist/bundle.js", "module": "./dist/bundle.js",
@@ -56,5 +56,5 @@
"fetch" "fetch"
] ]
}, },
"gitHead": "49457505cdf8156fd9d936b8e9ace0b48e43a6b2" "gitHead": "590ff67fcb40ff8ba0f7b2a9592b51d9fb54a2ef"
} }
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.1](https://github.com/certd/certd/compare/v1.39.0...v1.39.1) (2026-03-09)
**Note:** Version bump only for package @certd/lib-k8s
# [1.39.0](https://github.com/certd/certd/compare/v1.38.12...v1.39.0) (2026-03-07)
**Note:** Version bump only for package @certd/lib-k8s
## [1.38.12](https://github.com/certd/certd/compare/v1.38.11...v1.38.12) (2026-02-18) ## [1.38.12](https://github.com/certd/certd/compare/v1.38.11...v1.38.12) (2026-02-18)
**Note:** Version bump only for package @certd/lib-k8s **Note:** Version bump only for package @certd/lib-k8s
+3 -3
View File
@@ -1,7 +1,7 @@
{ {
"name": "@certd/lib-k8s", "name": "@certd/lib-k8s",
"private": false, "private": false,
"version": "1.38.12", "version": "1.39.1",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
@@ -17,7 +17,7 @@
"pub": "npm publish" "pub": "npm publish"
}, },
"dependencies": { "dependencies": {
"@certd/basic": "^1.38.12", "@certd/basic": "^1.39.1",
"@kubernetes/client-node": "0.21.0" "@kubernetes/client-node": "0.21.0"
}, },
"devDependencies": { "devDependencies": {
@@ -32,5 +32,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "49457505cdf8156fd9d936b8e9ace0b48e43a6b2" "gitHead": "590ff67fcb40ff8ba0f7b2a9592b51d9fb54a2ef"
} }
+10
View File
@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.1](https://github.com/certd/certd/compare/v1.39.0...v1.39.1) (2026-03-09)
**Note:** Version bump only for package @certd/lib-server
# [1.39.0](https://github.com/certd/certd/compare/v1.38.12...v1.39.0) (2026-03-07)
### Performance Improvements
* 【破坏性更新】错误返回信息msg字段名统一改成message,与成功的返回结构一致 ([51ab6d6](https://github.com/certd/certd/commit/51ab6d6da1bb551b55b3a6a4a9a945c8d6ace806))
## [1.38.12](https://github.com/certd/certd/compare/v1.38.11...v1.38.12) (2026-02-18) ## [1.38.12](https://github.com/certd/certd/compare/v1.38.11...v1.38.12) (2026-02-18)
**Note:** Version bump only for package @certd/lib-server **Note:** Version bump only for package @certd/lib-server
+7 -7
View File
@@ -1,6 +1,6 @@
{ {
"name": "@certd/lib-server", "name": "@certd/lib-server",
"version": "1.38.12", "version": "1.39.1",
"description": "midway with flyway, sql upgrade way ", "description": "midway with flyway, sql upgrade way ",
"private": false, "private": false,
"type": "module", "type": "module",
@@ -28,11 +28,11 @@
], ],
"license": "AGPL", "license": "AGPL",
"dependencies": { "dependencies": {
"@certd/acme-client": "^1.38.12", "@certd/acme-client": "^1.39.1",
"@certd/basic": "^1.38.12", "@certd/basic": "^1.39.1",
"@certd/pipeline": "^1.38.12", "@certd/pipeline": "^1.39.1",
"@certd/plugin-lib": "^1.38.12", "@certd/plugin-lib": "^1.39.1",
"@certd/plus-core": "^1.38.12", "@certd/plus-core": "^1.39.1",
"@midwayjs/cache": "3.14.0", "@midwayjs/cache": "3.14.0",
"@midwayjs/core": "3.20.11", "@midwayjs/core": "3.20.11",
"@midwayjs/i18n": "3.20.13", "@midwayjs/i18n": "3.20.13",
@@ -64,5 +64,5 @@
"typeorm": "^0.3.11", "typeorm": "^0.3.11",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "49457505cdf8156fd9d936b8e9ace0b48e43a6b2" "gitHead": "590ff67fcb40ff8ba0f7b2a9592b51d9fb54a2ef"
} }
@@ -83,7 +83,7 @@ export abstract class BaseController {
let userId = this.getUserId() let userId = this.getUserId()
const projectId = await this.getProjectId(permission) const projectId = await this.getProjectId(permission)
if(projectId){ if(projectId){
userId = 0 userId = -1 // 企业管理模式下,用户id固定-1
} }
return { return {
projectId,userId projectId,userId
@@ -115,11 +115,17 @@ export abstract class BaseController {
if (projectId) { if (projectId) {
await authService.checkProjectId(service, id, projectId); await authService.checkProjectId(service, id, projectId);
}else{ }else{
if(allowAdmin){
await authService.checkUserIdButAllowAdmin(this.ctx, service, id); if(userId === Constants.systemUserId){
//系统级别,不检查权限
}else{ }else{
await authService.checkUserId(this.ctx, service, id); if(allowAdmin){
await authService.checkUserIdButAllowAdmin(this.ctx, service, id);
}else{
await authService.checkUserId( service, id, userId);
}
} }
} }
return {projectId,userId} return {projectId,userId}
} }
@@ -4,6 +4,7 @@ import { Inject } from '@midwayjs/core';
import { TypeORMDataSourceManager } from '@midwayjs/typeorm'; import { TypeORMDataSourceManager } from '@midwayjs/typeorm';
import { EntityManager } from 'typeorm/entity-manager/EntityManager.js'; import { EntityManager } from 'typeorm/entity-manager/EntityManager.js';
import { FindManyOptions } from 'typeorm'; import { FindManyOptions } from 'typeorm';
import { Constants } from './constants.js';
export type PageReq<T = any> = { export type PageReq<T = any> = {
page?: { offset: number; limit: number }; page?: { offset: number; limit: number };
@@ -258,12 +259,12 @@ export abstract class BaseService<T> {
export function checkUserProjectParam(userId: number, projectId: number) { export function checkUserProjectParam(userId: number, projectId: number) {
if (projectId != null ){ if (projectId != null ){
if( userId !==0) { if( userId !== Constants.enterpriseUserId) {
throw new ValidateException('userId projectId 错误'); throw new ValidateException('userId projectId 错误');
} }
return true return true
}else{ }else{
if( userId > 0) { if( userId != null) {
return true return true
} }
throw new ValidateException('userId不能为空'); throw new ValidateException('userId不能为空');
@@ -120,4 +120,6 @@ export const Constants = {
message: '用户邮箱还未配置', message: '用户邮箱还未配置',
}, },
}, },
systemUserId: 0, // 系统级别userid固定为0
enterpriseUserId: -1 // 企业模式用户id固定为-1
}; };
@@ -88,6 +88,10 @@ export class SysPrivateSettings extends BaseSettings {
pipelineMaxRunningCount?: number; pipelineMaxRunningCount?: number;
environmentVars?: string = '';
sms?: { sms?: {
type?: string; type?: string;
config?: any; config?: any;
@@ -11,6 +11,8 @@ import { BaseService, setAdminMode } from '../../../basic/index.js';
import { executorQueue } from '../../basic/service/executor-queue.js'; import { executorQueue } from '../../basic/service/executor-queue.js';
import { isComm } from '@certd/plus-core'; import { isComm } from '@certd/plus-core';
const { merge } = mergeUtils; const { merge } = mergeUtils;
let lastSaveEnvVars = {};
/** /**
* *
*/ */
@@ -117,12 +119,12 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
} }
async savePublicSettings(bean: SysPublicSettings) { async savePublicSettings(bean: SysPublicSettings) {
if(isComm()){ if (isComm()) {
if(bean.adminMode === 'enterprise'){ if (bean.adminMode === 'enterprise') {
throw new Error("商业版不支持使用企业管理模式") throw new Error("商业版不支持使用企业管理模式")
} }
} }
await this.saveSetting(bean); await this.saveSetting(bean);
//让设置生效 //让设置生效
await this.reloadPublicSettings(); await this.reloadPublicSettings();
@@ -173,6 +175,44 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
} }
setSslProviderReverseProxies(privateSetting.reverseProxies); setSslProviderReverseProxies(privateSetting.reverseProxies);
//加载环境变量
this.setEnvironmentVars(privateSetting.environmentVars);
}
setEnvironmentVars(vars: string) {
const envVars = {}
if (typeof vars !== 'string') {
vars = ""
}
vars.split('\n').forEach(line => {
line = line.trim();
if (!line || line.startsWith('#')) {
return
}
const arr = line.split("#")
if (arr.length > 0) {
line = arr[0].trim();
}
if (!line.includes("=")) {
return
}
const [key, value] = line.split('=');
if (key && value) {
envVars[key.trim()] = value.trim();
}
});
//先删除旧环境变量
if (lastSaveEnvVars) {
for (const key in lastSaveEnvVars) {
delete process.env[key];
}
}
merge(process.env, envVars);
lastSaveEnvVars = envVars;
} }
async updateByKey(key: string, setting: any) { async updateByKey(key: string, setting: any) {
@@ -7,8 +7,12 @@ import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
export class AccessEntity { export class AccessEntity {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number;
@Column({ name: 'key_id', comment: 'key_id', length: 100 })
keyId: string;
@Column({ name: 'user_id', comment: '用户id' }) @Column({ name: 'user_id', comment: '用户id' })
userId: number; userId: number; // 0为系统级别, -1为企业,大于1为用户
@Column({ comment: '名称', length: 100 }) @Column({ comment: '名称', length: 100 })
name: string; name: string;
@@ -24,9 +28,6 @@ export class AccessEntity {
@Column({ name: 'project_id', comment: '项目id' }) @Column({ name: 'project_id', comment: '项目id' })
projectId: number; projectId: number;
@Column({ comment: '权限等级', length: 100 })
level: string; // user common system
@Column({ @Column({
name: 'create_time', name: 'create_time',
comment: '创建时间', comment: '创建时间',
@@ -5,6 +5,7 @@ import {AccessGetter, BaseService, PageReq, PermissionException, ValidateExcepti
import {AccessEntity} from '../entity/access.js'; import {AccessEntity} from '../entity/access.js';
import {AccessDefine, accessRegistry, newAccess} from '@certd/pipeline'; import {AccessDefine, accessRegistry, newAccess} from '@certd/pipeline';
import {EncryptService} from './encrypt-service.js'; import {EncryptService} from './encrypt-service.js';
import { logger, utils } from '@certd/basic';
/** /**
* *
@@ -46,6 +47,7 @@ export class AccessService extends BaseService<AccessEntity> {
} }
delete param._copyFrom delete param._copyFrom
this.encryptSetting(param, oldEntity); this.encryptSetting(param, oldEntity);
param.keyId = "ac_" + utils.id.simpleNanoId();
return await super.add(param); return await super.add(param);
} }
@@ -117,6 +119,7 @@ export class AccessService extends BaseService<AccessEntity> {
throw new ValidateException('该授权配置不存在,请确认是否已被删除'); throw new ValidateException('该授权配置不存在,请确认是否已被删除');
} }
this.encryptSetting(param, oldEntity); this.encryptSetting(param, oldEntity);
delete param.keyId
return await super.update(param); return await super.update(param);
} }
@@ -215,4 +218,36 @@ export class AccessService extends BaseService<AccessEntity> {
}); });
} }
/**
*
* @param accessId
* @param projectId
*/
async copyTo(accessId: number,projectId?: number) {
const access = await this.info(accessId);
if (access == null) {
throw new Error(`该授权配置不存在,请确认是否已被删除:id=${accessId}`);
}
const keyId = access.keyId;
//检查目标项目里是否已经有相同keyId的配置
const existAccess = await this.repository.findOne({
where: {
keyId,
projectId,
},
});
if (existAccess) {
logger.info(`目标项目已存在相同keyId的授权配置,跳过复制:keyId=${keyId}`);
return existAccess.id;
}
const newAccess = {
...access,
id: undefined,
projectId,
}
await this.add(newAccess);
return newAccess.id;
}
} }
@@ -6,6 +6,8 @@ import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
export class AddonEntity { export class AddonEntity {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number;
@Column({ name: 'key_id', comment: 'key_id', length: 100 })
keyId: string;
@Column({ name: 'user_id', comment: '用户id' }) @Column({ name: 'user_id', comment: '用户id' })
userId: number; userId: number;
@Column({ comment: '名称', length: 100 }) @Column({ comment: '名称', length: 100 })
@@ -4,6 +4,7 @@ import { In, Repository } from "typeorm";
import { AddonDefine, BaseService, PageReq, ValidateException } from "../../../index.js"; import { AddonDefine, BaseService, PageReq, ValidateException } from "../../../index.js";
import { addonRegistry } from "../api/index.js"; import { addonRegistry } from "../api/index.js";
import { AddonEntity } from "../entity/addon.js"; import { AddonEntity } from "../entity/addon.js";
import { utils } from "@certd/basic";
/** /**
* Addon * Addon
@@ -43,6 +44,7 @@ export class AddonService extends BaseService<AddonEntity> {
} else { } else {
param.isSystem = false; param.isSystem = false;
} }
param.keyId = "ad_" + utils.id.simpleNanoId();
delete param._copyFrom; delete param._copyFrom;
return await super.add(param); return await super.add(param);
} }
@@ -57,6 +59,7 @@ export class AddonService extends BaseService<AddonEntity> {
if (oldEntity == null) { if (oldEntity == null) {
throw new ValidateException("该Addon配置不存在,请确认是否已被删除"); throw new ValidateException("该Addon配置不存在,请确认是否已被删除");
} }
delete param.keyId
return await super.update(param); return await super.update(param);
} }
@@ -67,6 +70,7 @@ export class AddonService extends BaseService<AddonEntity> {
} }
return { return {
id: entity.id, id: entity.id,
keyId: entity.keyId,
name: entity.name, name: entity.name,
userId: entity.userId, userId: entity.userId,
addonType: entity.addonType, addonType: entity.addonType,
@@ -100,6 +104,7 @@ export class AddonService extends BaseService<AddonEntity> {
}, },
select: { select: {
id: true, id: true,
keyId: true,
name: true, name: true,
addonType: true, addonType: true,
type: true, type: true,
@@ -132,6 +137,7 @@ export class AddonService extends BaseService<AddonEntity> {
const setting = JSON.parse(res.setting); const setting = JSON.parse(res.setting);
return { return {
id: res.id, id: res.id,
keyId: res.keyId,
addonType: res.addonType, addonType: res.addonType,
type: res.type, type: res.type,
name: res.name, name: res.name,
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.1](https://github.com/certd/certd/compare/v1.39.0...v1.39.1) (2026-03-09)
**Note:** Version bump only for package @certd/midway-flyway-js
# [1.39.0](https://github.com/certd/certd/compare/v1.38.12...v1.39.0) (2026-03-07)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.38.12](https://github.com/certd/certd/compare/v1.38.11...v1.38.12) (2026-02-18) ## [1.38.12](https://github.com/certd/certd/compare/v1.38.11...v1.38.12) (2026-02-18)
**Note:** Version bump only for package @certd/midway-flyway-js **Note:** Version bump only for package @certd/midway-flyway-js
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@certd/midway-flyway-js", "name": "@certd/midway-flyway-js",
"version": "1.38.12", "version": "1.39.1",
"description": "midway with flyway, sql upgrade way ", "description": "midway with flyway, sql upgrade way ",
"private": false, "private": false,
"type": "module", "type": "module",
@@ -46,5 +46,5 @@
"typeorm": "^0.3.11", "typeorm": "^0.3.11",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "49457505cdf8156fd9d936b8e9ace0b48e43a6b2" "gitHead": "590ff67fcb40ff8ba0f7b2a9592b51d9fb54a2ef"
} }
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.1](https://github.com/certd/certd/compare/v1.39.0...v1.39.1) (2026-03-09)
**Note:** Version bump only for package @certd/plugin-cert
# [1.39.0](https://github.com/certd/certd/compare/v1.38.12...v1.39.0) (2026-03-07)
**Note:** Version bump only for package @certd/plugin-cert
## [1.38.12](https://github.com/certd/certd/compare/v1.38.11...v1.38.12) (2026-02-18) ## [1.38.12](https://github.com/certd/certd/compare/v1.38.11...v1.38.12) (2026-02-18)
**Note:** Version bump only for package @certd/plugin-cert **Note:** Version bump only for package @certd/plugin-cert
+6 -6
View File
@@ -1,7 +1,7 @@
{ {
"name": "@certd/plugin-cert", "name": "@certd/plugin-cert",
"private": false, "private": false,
"version": "1.38.12", "version": "1.39.1",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -17,10 +17,10 @@
"compile": "tsc --skipLibCheck --watch" "compile": "tsc --skipLibCheck --watch"
}, },
"dependencies": { "dependencies": {
"@certd/acme-client": "^1.38.12", "@certd/acme-client": "^1.39.1",
"@certd/basic": "^1.38.12", "@certd/basic": "^1.39.1",
"@certd/pipeline": "^1.38.12", "@certd/pipeline": "^1.39.1",
"@certd/plugin-lib": "^1.38.12", "@certd/plugin-lib": "^1.39.1",
"psl": "^1.9.0", "psl": "^1.9.0",
"punycode.js": "^2.3.1" "punycode.js": "^2.3.1"
}, },
@@ -38,5 +38,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "49457505cdf8156fd9d936b8e9ace0b48e43a6b2" "gitHead": "590ff67fcb40ff8ba0f7b2a9592b51d9fb54a2ef"
} }
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.1](https://github.com/certd/certd/compare/v1.39.0...v1.39.1) (2026-03-09)
**Note:** Version bump only for package @certd/plugin-lib
# [1.39.0](https://github.com/certd/certd/compare/v1.38.12...v1.39.0) (2026-03-07)
**Note:** Version bump only for package @certd/plugin-lib
## [1.38.12](https://github.com/certd/certd/compare/v1.38.11...v1.38.12) (2026-02-18) ## [1.38.12](https://github.com/certd/certd/compare/v1.38.11...v1.38.12) (2026-02-18)
**Note:** Version bump only for package @certd/plugin-lib **Note:** Version bump only for package @certd/plugin-lib
+6 -6
View File
@@ -1,7 +1,7 @@
{ {
"name": "@certd/plugin-lib", "name": "@certd/plugin-lib",
"private": false, "private": false,
"version": "1.38.12", "version": "1.39.1",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -22,10 +22,10 @@
"@alicloud/pop-core": "^1.7.10", "@alicloud/pop-core": "^1.7.10",
"@alicloud/tea-util": "^1.4.11", "@alicloud/tea-util": "^1.4.11",
"@aws-sdk/client-s3": "^3.964.0", "@aws-sdk/client-s3": "^3.964.0",
"@certd/acme-client": "^1.38.12", "@certd/acme-client": "^1.39.1",
"@certd/basic": "^1.38.12", "@certd/basic": "^1.39.1",
"@certd/pipeline": "^1.38.12", "@certd/pipeline": "^1.39.1",
"@certd/plus-core": "^1.38.12", "@certd/plus-core": "^1.39.1",
"@kubernetes/client-node": "0.21.0", "@kubernetes/client-node": "0.21.0",
"ali-oss": "^6.22.0", "ali-oss": "^6.22.0",
"basic-ftp": "^5.0.5", "basic-ftp": "^5.0.5",
@@ -57,5 +57,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "49457505cdf8156fd9d936b8e9ace0b48e43a6b2" "gitHead": "590ff67fcb40ff8ba0f7b2a9592b51d9fb54a2ef"
} }
@@ -2,6 +2,7 @@ import fs from "fs";
import os from "os"; import os from "os";
import path from "path"; import path from "path";
import { CertificateInfo, crypto } from "@certd/acme-client"; import { CertificateInfo, crypto } from "@certd/acme-client";
import cryptoLib from "crypto";
import { ILogger } from "@certd/basic"; import { ILogger } from "@certd/basic";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { uniq } from "lodash-es"; import { uniq } from "lodash-es";
@@ -119,9 +120,28 @@ export class CertReader {
const detail = crypto.readCertificateInfo(crt.toString()); const detail = crypto.readCertificateInfo(crt.toString());
const effective = detail.notBefore; const effective = detail.notBefore;
const expires = detail.notAfter; const expires = detail.notAfter;
const fingerprints = CertReader.getFingerprintX509(crt);
// @ts-ignore
detail.fingerprints = fingerprints;
return { detail, effective, expires }; return { detail, effective, expires };
} }
static getFingerprintX509(crt: string) {
try {
// 创建X509Certificate实例
const cert = new cryptoLib.X509Certificate(crt);
// 获取指纹
return {
fingerprint: cert.fingerprint,
fingerprint256: cert.fingerprint256,
fingerprint512: cert.fingerprint512,
};
} catch (error) {
console.error("处理证书失败:", error.message);
return null;
}
}
getAllDomains() { getAllDomains() {
const { detail } = this.getCrtDetail(); const { detail } = this.getCrtDetail();
const domains = []; const domains = [];
@@ -1,5 +1,5 @@
import { HttpClient, ILogger, utils } from "@certd/basic"; import { HttpClient, ILogger, utils } from "@certd/basic";
import { IAccess, IServiceGetter, PageRes, PageSearch, Registrable } from "@certd/pipeline"; import { IAccess, IAccessService, IServiceGetter, PageRes, PageSearch, Registrable } from "@certd/pipeline";
export type DnsProviderDefine = Registrable & { export type DnsProviderDefine = Registrable & {
accessType: string; accessType: string;
@@ -26,6 +26,7 @@ export type DnsProviderContext = {
utils: typeof utils; utils: typeof utils;
domainParser: IDomainParser; domainParser: IDomainParser;
serviceGetter: IServiceGetter; serviceGetter: IServiceGetter;
accessGetter?: IAccessService;
}; };
export type DomainRecord = { export type DomainRecord = {
@@ -1,5 +1,5 @@
import { HttpClient, ILogger } from "@certd/basic"; import { HttpClient, ILogger } from "@certd/basic";
import { PageRes, PageSearch } from "@certd/pipeline"; import { IAccessService, PageRes, PageSearch } from "@certd/pipeline";
import punycode from "punycode.js"; import punycode from "punycode.js";
import { CreateRecordOptions, DnsProviderContext, DnsProviderDefine, DomainRecord, IDnsProvider, RemoveRecordOptions } from "./api.js"; import { CreateRecordOptions, DnsProviderContext, DnsProviderDefine, DomainRecord, IDnsProvider, RemoveRecordOptions } from "./api.js";
import { dnsProviderRegistry } from "./registry.js"; import { dnsProviderRegistry } from "./registry.js";
@@ -59,6 +59,11 @@ export async function createDnsProvider(opts: { dnsProviderType: string; context
if (dnsProviderDefine.deprecated) { if (dnsProviderDefine.deprecated) {
context.logger.warn(dnsProviderDefine.deprecated); context.logger.warn(dnsProviderDefine.deprecated);
} }
if (!context.accessGetter) {
const accessGetter: IAccessService = await context.serviceGetter.get("accessService");
context.accessGetter = accessGetter;
}
// @ts-ignore // @ts-ignore
const dnsProvider: IDnsProvider = new DnsProviderClass(); const dnsProvider: IDnsProvider = new DnsProviderClass();
dnsProvider.setCtx(context); dnsProvider.setCtx(context);
+4
View File
@@ -38,6 +38,10 @@ COPY ./patch/ssh2/*.js /app/node_modules/.pnpm/node_modules/ssh2/lib/protocol/
ENV LEGO_VERSION=4.30.1 ENV LEGO_VERSION=4.30.1
ENV LEGO_DOWNLOAD_DIR=/app/tools/lego ENV LEGO_DOWNLOAD_DIR=/app/tools/lego
ENV ALIYUN_CLIENT_CONNECT_TIMEOUT=10000
ENV ALIYUN_CLIENT_READ_TIMEOUT=20000
RUN mkdir -p $LEGO_DOWNLOAD_DIR RUN mkdir -p $LEGO_DOWNLOAD_DIR
# 根据架构下载不同的文件 # 根据架构下载不同的文件
+28
View File
@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.1](https://github.com/certd/certd/compare/v1.39.0...v1.39.1) (2026-03-09)
### Bug Fixes
* 修复企业管理模式下,切换用户登录后,丢失项目列表的bug ([d23c8b4](https://github.com/certd/certd/commit/d23c8b4a2a5f5ab17822c6ee1d4108ac7280b9d1))
### Performance Improvements
* 支持迁移个人数据到企业项目中 ([c6ca832](https://github.com/certd/certd/commit/c6ca83273779ed84de1b23b5e477063af043d015))
# [1.39.0](https://github.com/certd/certd/compare/v1.38.12...v1.39.0) (2026-03-07)
### Bug Fixes
* 修复发件邮箱无法输入的bug ([27b0348](https://github.com/certd/certd/commit/27b0348e1d3d752f418f851965d6afbc26c0160c))
* 修复复制流水线保存后丢失分组和排序号的问题 ([bc32648](https://github.com/certd/certd/commit/bc326489abc1d50a0930b4f47aa2d62d3a486798))
* 修复获取群辉deviceid报错的bug ([79be392](https://github.com/certd/certd/commit/79be392775a2c91848dd5a66a2618adc4e4b48f6))
* 修复偶尔下载证书报未授权的错误 ([316537e](https://github.com/certd/certd/commit/316537eb4dcbe5ec57784e8bf95ee3cdfd21dce7))
### Features
* 支持企业级管理模式,项目管理,细分权限 ([3734083](https://github.com/certd/certd/commit/37340838b6a61a94b86bfa13cf5da88b26f1315a))
### Performance Improvements
* 当域名管理中没有域名时,创建流水线时不展开域名选择框 ([bb0afe1](https://github.com/certd/certd/commit/bb0afe1fa7b0fc52fde051d24fbe6be69d52f4cc))
* 任务步骤页面增加串行执行提示说明 ([787f6ef](https://github.com/certd/certd/commit/787f6ef52893d8dc912ee2a7a5b8ce2b73c108c9))
## [1.38.12](https://github.com/certd/certd/compare/v1.38.11...v1.38.12) (2026-02-18) ## [1.38.12](https://github.com/certd/certd/compare/v1.38.11...v1.38.12) (2026-02-18)
### Bug Fixes ### Bug Fixes
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@certd/ui-client", "name": "@certd/ui-client",
"version": "1.38.12", "version": "1.39.1",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite --open", "dev": "vite --open",
@@ -106,8 +106,8 @@
"zod-defaults": "^0.1.3" "zod-defaults": "^0.1.3"
}, },
"devDependencies": { "devDependencies": {
"@certd/lib-iframe": "^1.38.12", "@certd/lib-iframe": "^1.39.1",
"@certd/pipeline": "^1.38.12", "@certd/pipeline": "^1.39.1",
"@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12", "@types/chai": "^4.3.12",
@@ -1,7 +1,7 @@
<template> <template>
<div class="remote-select"> <div class="remote-select">
<div class="flex flex-row"> <div class="flex flex-row">
<a-select class="remote-select-input" show-search :filter-option="filterOption" :options="optionsRef" :value="value" v-bind="attrs" @click="onClick" @update:value="emit('update:value', $event)"> <a-select class="remote-select-input" show-search mode="tags" :filter-option="filterOption" :options="optionsRef" :value="value" v-bind="attrs" @click="onClick" @update:value="updateValue($event)">
<template #dropdownRender="{ menuNode: menu }"> <template #dropdownRender="{ menuNode: menu }">
<template v-if="search"> <template v-if="search">
<div class="flex w-full" style="padding: 4px 8px"> <div class="flex w-full" style="padding: 4px 8px">
@@ -61,6 +61,7 @@ const props = defineProps<
watches?: string[]; watches?: string[];
search?: boolean; search?: boolean;
pager?: boolean; pager?: boolean;
multi?: boolean;
} & ComponentPropsType } & ComponentPropsType
>(); >();
@@ -68,6 +69,15 @@ const emit = defineEmits<{
"update:value": any; "update:value": any;
}>(); }>();
function updateValue(value: any) {
if (props.multi) {
emit("update:value", value);
} else {
const last = value?.[value.length - 1];
emit("update:value", last);
}
}
const attrs = useAttrs(); const attrs = useAttrs();
const getCurrentPluginDefine: any = inject("getCurrentPluginDefine", () => { const getCurrentPluginDefine: any = inject("getCurrentPluginDefine", () => {
@@ -80,6 +90,7 @@ const getPluginType: any = inject("get:plugin:type", () => {
return "plugin"; return "plugin";
}); });
debugger;
const searchKeyRef = ref(""); const searchKeyRef = ref("");
const optionsRef = ref([]); const optionsRef = ref([]);
const message = ref(""); const message = ref("");
@@ -90,7 +90,7 @@
<div class="mt-10"> <div class="mt-10">
<div class="w-100 flex-col md:flex-row"> <div class="w-100 flex-col md:flex-row">
<span>{{ t("vip.site_id") }}</span> <span>{{ t("vip.site_id") }}</span>
<fs-copyable v-model="computedSiteId" class="mr-2"></fs-copyable> <fs-copyable v-model="computedSiteId" class="mr-2 inline-flex"></fs-copyable>
<a @click="goBindAccount">{{ t("vip.not_effective") }}</a> <a @click="goBindAccount">{{ t("vip.not_effective") }}</a>
</div> </div>
</div> </div>
@@ -68,6 +68,14 @@ export default {
smsTab: "Login via SMS code", smsTab: "Login via SMS code",
passwordTab: "Password login", passwordTab: "Password login",
passkeyTab: "Passkey Login",
passkeyLogin: "Passkey Login",
passkeyHelper: "Login with your biometric or security key",
passkeyNotSupported: "Your browser does not support Passkey",
passkeyRegister: "Register Passkey",
passkeyRegistered: "Passkey Registered",
passkeyRegisterSuccess: "Passkey registered successfully",
passkeyRegisterFailed: "Passkey registration failed",
title: "Change Password", title: "Change Password",
weakPasswordWarning: "For your account security, please change your password immediately", weakPasswordWarning: "For your account security, please change your password immediately",
changeNow: "Change Now", changeNow: "Change Now",
@@ -90,4 +98,9 @@ export default {
updateProfile: "Update Profile", updateProfile: "Update Profile",
oauthLoginTitle: "Other ways of login", oauthLoginTitle: "Other ways of login",
oauthOnlyLoginTitle: "Login", oauthOnlyLoginTitle: "Login",
registerPasskey: "Register Passkey",
deviceName: "Device Name",
deviceNameHelper: "Please enter the device name used to identify the device",
passkeyRegisterHelper: "Site domain change will invalidate passkey",
userInfo: "User Info",
}; };
@@ -220,6 +220,7 @@ export default {
myProjectDetail: "Project Detail", myProjectDetail: "Project Detail",
projectJoin: "Join Project", projectJoin: "Join Project",
currentProject: "Current Project", currentProject: "Current Project",
projectMemberManager: "Project Member",
}, },
certificateRepo: { certificateRepo: {
title: "Certificate Repository", title: "Certificate Repository",
@@ -726,7 +727,7 @@ export default {
paymentSetting: "Payment Settings", paymentSetting: "Payment Settings",
captchaSetting: "Captcha Setting", captchaSetting: "Captcha Setting",
pipelineSetting: "Pipeline Settings", pipelineSetting: "Pipeline Settings",
oauthSetting: "OAuth2 Settings", oauthSetting: "Login Settings",
networkSetting: "Network Settings", networkSetting: "Network Settings",
adminModeSetting: "Admin Mode Settings", adminModeSetting: "Admin Mode Settings",
adminModeHelper: "enterprise mode : allow to create and manage pipelines, roles, users, etc.\n saas mode : only allow to create and manage pipelines", adminModeHelper: "enterprise mode : allow to create and manage pipelines, roles, users, etc.\n saas mode : only allow to create and manage pipelines",
@@ -768,6 +769,10 @@ export default {
oauthAutoRedirectHelper: "Whether to auto redirect to OAuth2 login when login (using the first enabled OAuth2 login type)", oauthAutoRedirectHelper: "Whether to auto redirect to OAuth2 login when login (using the first enabled OAuth2 login type)",
oauthOnly: "OAuth2 Login Only", oauthOnly: "OAuth2 Login Only",
oauthOnlyHelper: "Whether to only allow OAuth2 login, disable password login", oauthOnlyHelper: "Whether to only allow OAuth2 login, disable password login",
enablePasskey: "Enable Passkey Login",
passkeyHostnameNotSame: "Passkey hostname must be the same as the main domain",
passkeyEnabledHelper:
"1、Site must enable https \n2、Domain name must not change, otherwise the registered passkey will be invalid \n3、Domain name must be the same as the main domain, otherwise the registered passkey will be invalid",
email: { email: {
templates: "Email Templates", templates: "Email Templates",
@@ -788,6 +793,8 @@ export default {
reverseProxyHelper: "Reverse proxy for ACME address, used when applying for certificate", reverseProxyHelper: "Reverse proxy for ACME address, used when applying for certificate",
reverseProxyPlaceholder: "http://le.px.handfree.work", reverseProxyPlaceholder: "http://le.px.handfree.work",
reverseProxyEmpty: "No reverse proxy list configured", reverseProxyEmpty: "No reverse proxy list configured",
environmentVars: "Environment Variables",
environmentVarsHelper: "configure the runtime environment variables, one per line, format: KEY=VALUE",
}, },
}, },
modal: { modal: {
@@ -822,6 +829,9 @@ export default {
admin: "Admin", admin: "Admin",
}, },
projectMemberStatus: "Member Status", projectMemberStatus: "Member Status",
isSystem: "Is System Project",
isSystemHelper: "System-level projects allow running admin plugins",
}, },
project: { project: {
noProjectJoined: "You haven't joined any projects yet", noProjectJoined: "You haven't joined any projects yet",
@@ -839,6 +849,7 @@ export default {
applyJoinConfirm: "Are you sure you want to apply to join this project?", applyJoinConfirm: "Are you sure you want to apply to join this project?",
leaveConfirm: "Are you sure you want to leave this project?", leaveConfirm: "Are you sure you want to leave this project?",
viewDetail: "View Detail", viewDetail: "View Detail",
projectManage: "Project Manage",
}, },
addonSelector: { addonSelector: {
select: "Select", select: "Select",
@@ -68,6 +68,14 @@ export default {
smsTab: "手机号登录/注册", smsTab: "手机号登录/注册",
passwordTab: "密码登录", passwordTab: "密码登录",
passkeyTab: "Passkey登录",
passkeyLogin: "Passkey登录",
passkeyHelper: "使用您的生物识别或安全密钥登录",
passkeyNotSupported: "您的浏览器不支持Passkey",
passkeyRegister: "注册Passkey",
passkeyRegistered: "Passkey已注册",
passkeyRegisterSuccess: "Passkey注册成功",
passkeyRegisterFailed: "Passkey注册失败",
title: "修改密码", title: "修改密码",
weakPasswordWarning: "为了您的账户安全,请立即修改密码", weakPasswordWarning: "为了您的账户安全,请立即修改密码",
@@ -88,8 +96,13 @@ export default {
nickName: "昵称", nickName: "昵称",
phoneNumber: "手机号", phoneNumber: "手机号",
changePassword: "修改密码", changePassword: "修改密码",
updateProfile: "修改个人信息", updateProfile: "修改信息",
oauthLoginTitle: "其他登录方式", oauthLoginTitle: "其他登录方式",
oauthOnlyLoginTitle: "登录", oauthOnlyLoginTitle: "登录",
registerPasskey: "注册Passkey",
deviceName: "设备名称",
deviceNameHelper: "请输入当前设备名称,绑定多个时好做区分",
passkeyRegisterHelper: "1、站点域名变更会导致passkey失效;\n2、同一设备同一个用户绑定多次只有最后一次的有效,之前绑定的会失效,需要手动删除",
userInfo: "账号信息",
}; };
@@ -220,12 +220,12 @@ export default {
netTest: "网络测试", netTest: "网络测试",
enterpriseManager: "企业管理设置", enterpriseManager: "企业管理设置",
projectManager: "项目管理", projectManager: "项目管理",
projectDetail: "项目详情",
enterpriseSetting: "企业设置", enterpriseSetting: "企业设置",
myProjectManager: "我的项目", myProjectManager: "我的项目",
myProjectDetail: "项目详情", myProjectDetail: "项目详情",
projectJoin: "加入项目", projectJoin: "加入项目",
currentProject: "当前项目", currentProject: "当前项目",
projectMemberManager: "项目成员管理",
}, },
certificateRepo: { certificateRepo: {
title: "证书仓库", title: "证书仓库",
@@ -780,7 +780,9 @@ export default {
oauthAutoRedirectHelper: "是否自动跳转第三方登录(使用第一个已启用的第三方登录类型)", oauthAutoRedirectHelper: "是否自动跳转第三方登录(使用第一个已启用的第三方登录类型)",
oauthOnly: "仅使用第三方登录", oauthOnly: "仅使用第三方登录",
oauthOnlyHelper: "是否仅使用第三方登录,关闭密码登录(注意:请务必在测试第三方登录功能正常后再开启,否则会导致无法登录)\n 如果无法登录,请访问 http://你的certd地址/#/login?oauthOnly=false 来临时关闭此模式", oauthOnlyHelper: "是否仅使用第三方登录,关闭密码登录(注意:请务必在测试第三方登录功能正常后再开启,否则会导致无法登录)\n 如果无法登录,请访问 http://你的certd地址/#/login?oauthOnly=false 来临时关闭此模式",
enablePasskey: "启用Passkey登录",
passkeyHostnameNotSame: "当前域名与主绑定域名不同",
passkeyEnabledHelper: "1、站点必须启用https \n2、passkey的rpId以主绑定域名为准,当前主域名:{0} \n3、站点域名不能变,否则会导致已注册的passkey失效。",
email: { email: {
templates: "邮件模板", templates: "邮件模板",
templateType: "模板类型", templateType: "模板类型",
@@ -800,6 +802,8 @@ export default {
reverseProxyHelper: "证书颁发机构ACME地址的反向代理,在申请证书时自动使用", reverseProxyHelper: "证书颁发机构ACME地址的反向代理,在申请证书时自动使用",
reverseProxyPlaceholder: "http://le.px.handfree.work", reverseProxyPlaceholder: "http://le.px.handfree.work",
reverseProxyEmpty: "未配置反向代理", reverseProxyEmpty: "未配置反向代理",
environmentVars: "环境变量",
environmentVarsHelper: "配置运行时环境变量,每行一个,格式:KEY=VALUE",
}, },
}, },
modal: { modal: {
@@ -838,6 +842,9 @@ export default {
admin: "管理员", admin: "管理员",
}, },
projectMemberStatus: "成员状态", projectMemberStatus: "成员状态",
isSystem: "是否系统项目",
isSystemHelper: "系统级项目允许运行管理员插件",
}, },
project: { project: {
noProjectJoined: "您还没有加入任何项目", noProjectJoined: "您还没有加入任何项目",
@@ -855,5 +862,6 @@ export default {
applyJoinConfirm: "确认加入项目?", applyJoinConfirm: "确认加入项目?",
leaveConfirm: "确认退出项目?", leaveConfirm: "确认退出项目?",
viewDetail: "查看详情", viewDetail: "查看详情",
projectManage: "项目管理",
}, },
}; };
@@ -29,21 +29,6 @@ export const certdResources = [
auth: true, auth: true,
}, },
}, },
{
title: "certd.sysResources.currentProject",
name: "CurrentProject",
path: "/certd/project/detail",
component: "/certd/project/detail/index.vue",
meta: {
show: () => {
const projectStore = useProjectStore();
return projectStore.isEnterprise;
},
isMenu: true,
icon: "ion:apps",
auth: true,
},
},
{ {
title: "certd.sysResources.projectJoin", title: "certd.sysResources.projectJoin",
name: "ProjectJoin", name: "ProjectJoin",
@@ -142,6 +127,21 @@ export const certdResources = [
keepAlive: true, keepAlive: true,
}, },
}, },
{
title: "certd.sysResources.currentProject",
name: "ProjectMemberManager",
path: "/certd/project/detail",
component: "/certd/project/detail/index.vue",
meta: {
show: () => {
const projectStore = useProjectStore();
return projectStore.isEnterprise;
},
isMenu: true,
icon: "ion:apps",
auth: true,
},
},
{ {
title: "certd.settings", title: "certd.settings",
name: "MineSetting", name: "MineSetting",
@@ -275,7 +275,7 @@ export const certdResources = [
meta: { meta: {
icon: "ion:person-outline", icon: "ion:person-outline",
auth: true, auth: true,
isMenu: false, isMenu: true,
}, },
}, },
], ],
@@ -15,11 +15,18 @@ export type ProjectItem = {
export const useProjectStore = defineStore("app.project", () => { export const useProjectStore = defineStore("app.project", () => {
const myProjects = ref([]); const myProjects = ref([]);
const inited = ref(false); const inited = ref(false);
const currentProjectId = ref(); // 直接调用
function $reset() {
myProjects.value = [];
currentProjectId.value = "";
inited.value = false;
}
const userStore = useUserStore(); const userStore = useUserStore();
const userId = userStore.getUserInfo?.id; const userId = userStore.getUserInfo?.id;
const lastProjectIdCacheKey = "currentProjectId:" + userId; const lastProjectIdCacheKey = "currentProjectId:" + userId;
const lastProjectId = LocalStorage.get(lastProjectIdCacheKey); const lastProjectId = LocalStorage.get(lastProjectIdCacheKey);
const currentProjectId = ref(lastProjectId); // 直接调用 currentProjectId.value = lastProjectId;
const projects = computed(() => { const projects = computed(() => {
return myProjects.value; return myProjects.value;
@@ -118,11 +125,6 @@ export const useProjectStore = defineStore("app.project", () => {
return false; return false;
} }
function $reset() {
myProjects.value = [];
currentProjectId.value = "";
}
return { return {
projects, projects,
myProjects, myProjects,
@@ -38,6 +38,7 @@ export type SysPublicSetting = {
passwordLoginEnabled?: boolean; passwordLoginEnabled?: boolean;
smsLoginEnabled?: boolean; smsLoginEnabled?: boolean;
defaultLoginType?: string; defaultLoginType?: string;
passkeyEnabled?: boolean;
selfServicePasswordRetrievalEnabled?: boolean; selfServicePasswordRetrievalEnabled?: boolean;
limitUserPipelineCount?: number; limitUserPipelineCount?: number;
@@ -101,6 +102,8 @@ export type SysPrivateSetting = {
commonCnameEnabled?: boolean; commonCnameEnabled?: boolean;
// 同一个用户同时最大运行流水线数量 // 同一个用户同时最大运行流水线数量
pipelineMaxRunningCount?: number; pipelineMaxRunningCount?: number;
// 环境变量
environmentVars?: string;
sms?: { sms?: {
type?: string; type?: string;
@@ -107,3 +107,41 @@ export async function OauthProviders() {
method: "post", method: "post",
}); });
} }
export async function generatePasskeyRegistrationOptions() {
return await request({
url: "/passkey/generateRegistration",
method: "post",
});
}
export async function verifyPasskeyRegistration(response: any, challenge: string) {
return await request({
url: "/passkey/verifyRegistration",
method: "post",
data: { response, challenge },
});
}
export async function generatePasskeyAuthenticationOptions() {
return await request({
url: "/passkey/generateAuthentication",
method: "post",
});
}
export async function loginByPasskey(form: { credential: any; challenge: string }) {
return await request({
url: "/loginByPasskey",
method: "post",
data: form,
});
}
export async function registerPasskey(form: { response: any; challenge: string }) {
return await request({
url: "/passkey/register",
method: "post",
data: form,
});
}
@@ -92,6 +92,16 @@ export const useUserStore = defineStore({
const loginRes = await UserApi.loginByTwoFactor(form); const loginRes = await UserApi.loginByTwoFactor(form);
return await this.onLoginSuccess(loginRes); return await this.onLoginSuccess(loginRes);
}, },
async loginByPasskey(form: any) {
const loginRes = await UserApi.loginByPasskey(form);
return await this.onLoginSuccess(loginRes);
},
async registerPasskey(form: any) {
return await UserApi.registerPasskey(form);
},
async getUserInfoAction(): Promise<UserInfoRes> { async getUserInfoAction(): Promise<UserInfoRes> {
const userInfo = await UserApi.mine(); const userInfo = await UserApi.mine();
this.setUserInfo(userInfo); this.setUserInfo(userInfo);
+12 -7
View File
@@ -21,8 +21,10 @@ div#app {
height: 100%; height: 100%;
} }
pre.pre { pre{
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; &.pre,&.helper{
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" !important;
}
} }
h1, h1,
@@ -110,10 +112,10 @@ h6 {
flex: 0; flex: 0;
} }
.flex-col { // .flex-col {
display: flex; // display: flex;
flex-direction: column; // flex-direction: column;
} // }
.align-left { .align-left {
text-align: left; text-align: left;
@@ -295,10 +297,13 @@ h6 {
} }
.helper { .helper {
color: #aeaeae; color: #8f8f8f;
font-size: 12px; font-size: 12px;
margin-top: 3px; margin-top: 3px;
margin-bottom: 3px; margin-bottom: 3px;
white-space: pre-wrap;
word-wrap: break-word;
word-break: break-word;
&.error { &.error {
color: #ff4d4f; color: #ff4d4f;
+14 -8
View File
@@ -1,4 +1,5 @@
import { useFormWrapper } from "@fast-crud/fast-crud"; import { useFormWrapper } from "@fast-crud/fast-crud";
import { merge } from "lodash-es";
export type FormOptionReq = { export type FormOptionReq = {
title: string; title: string;
@@ -7,6 +8,7 @@ export type FormOptionReq = {
body?: any; body?: any;
initialForm?: any; initialForm?: any;
zIndex?: number; zIndex?: number;
wrapper?: any;
}; };
export function useFormDialog() { export function useFormDialog() {
@@ -14,19 +16,23 @@ export function useFormDialog() {
async function openFormDialog(req: FormOptionReq) { async function openFormDialog(req: FormOptionReq) {
function createCrudOptions() { function createCrudOptions() {
const warpper = merge(
{
zIndex: req.zIndex,
title: req.title,
saveRemind: false,
slots: {
"form-body-top": req.body,
},
},
req.wrapper
);
return { return {
crudOptions: { crudOptions: {
columns: req.columns, columns: req.columns,
form: { form: {
initialForm: req.initialForm, initialForm: req.initialForm,
wrapper: { wrapper: warpper,
zIndex: req.zIndex,
title: req.title,
saveRemind: false,
slots: {
"form-body-top": req.body,
},
},
async afterSubmit() {}, async afterSubmit() {},
async doSubmit({ form }: any) { async doSubmit({ form }: any) {
if (req.onSubmit) { if (req.onSubmit) {
@@ -1,5 +1,5 @@
import * as _ from "lodash-es";
import { asyncCompute, compute } from "@fast-crud/fast-crud"; import { asyncCompute, compute } from "@fast-crud/fast-crud";
import { merge } from "lodash-es";
import { computed } from "vue"; import { computed } from "vue";
export type MergeScriptContext = { export type MergeScriptContext = {
@@ -18,7 +18,7 @@ export function useReference(formItem: any) {
const script = formItem.mergeScript; const script = formItem.mergeScript;
const func = new Function("ctx", script); const func = new Function("ctx", script);
const merged = func(ctx); const merged = func(ctx);
_.merge(formItem, merged); merge(formItem, merged);
delete formItem.mergeScript; delete formItem.mergeScript;
} }
@@ -143,7 +143,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
order: 10, order: 10,
}, },
valueBuilder: ({ row, key, value }) => { valueBuilder: ({ row, key, value }) => {
row[key] = row.userId > 0 ? "user" : "sys"; row[key] = row.userId != 0 ? "user" : "sys";
}, },
}, },
...commonColumnsDefine, ...commonColumnsDefine,
@@ -121,7 +121,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
order: 10, order: 10,
}, },
valueBuilder: ({ row, key, value }) => { valueBuilder: ({ row, key, value }) => {
row[key] = row.userId > 0 ? "user" : "sys"; row[key] = row.userId != 0 ? "user" : "sys";
}, },
}, },
...commonColumnsDefine, ...commonColumnsDefine,
@@ -55,3 +55,78 @@ export async function OauthBoundUrl(type: string) {
}, },
}); });
} }
export async function GetPasskeys() {
return await request({
url: "/mine/passkey/list",
method: "POST",
});
}
export async function UnbindPasskey(id: number) {
return await request({
url: "/mine/passkey/unbind",
method: "POST",
data: { id },
});
}
export interface PasskeyRegistrationOptions {
rp: {
name: string;
id: string;
};
user: {
id: Uint8Array;
name: string;
displayName: string;
};
challenge: string;
pubKeyCredParams: {
type: string;
alg: number;
}[];
timeout: number;
attestation: string;
excludeCredentials: any[];
}
export interface PasskeyAuthenticationOptions {
rpId: string;
challenge: string;
timeout: number;
allowCredentials: any[];
}
export interface PasskeyCredential {
id: string;
type: string;
rawId: string;
response: {
attestationObject: string;
clientDataJSON: string;
};
}
export async function generatePasskeyRegistrationOptions() {
return await request({
url: "/mine/passkey/generateRegistration",
method: "post",
});
}
export async function verifyPasskeyRegistration(response: any, challenge: string, deviceName: string) {
return await request({
url: "/mine/passkey/verifyRegistration",
method: "post",
data: { response, challenge, deviceName },
});
}
export async function registerPasskey(response: any, challenge: string, deviceName: string) {
return await request({
url: "/mine/passkey/register",
method: "post",
data: { response, challenge, deviceName },
});
}
@@ -108,3 +108,54 @@ export function useUserProfile() {
openEditProfileDialog, openEditProfileDialog,
}; };
} }
export function usePasskeyRegister() {
const { openCrudFormDialog } = useFormWrapper();
const wrapperRef = ref();
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,
},
columns: {
deviceName: {
title: t("authentication.deviceName"),
type: "text",
form: {
component: {
class: "w-full",
},
col: {
span: 24,
},
helper: t("authentication.deviceNameHelper"),
rules: [{ required: true, message: t("authentication.deviceName") }],
},
},
},
};
const wrapper = await openCrudFormDialog({ crudOptions });
wrapperRef.value = wrapper;
return wrapper;
}
return {
openRegisterDialog,
};
}
@@ -3,33 +3,129 @@
<template #header> <template #header>
<div class="title">{{ t("certd.myInfo") }}</div> <div class="title">{{ t("certd.myInfo") }}</div>
</template> </template>
<div class="p-10"> <div class="profile-container md:p-8">
<a-descriptions title="" bordered :column="2"> <div class="profile-card md:rounded">
<a-descriptions-item :label="t('authentication.username')">{{ userInfo.username }}</a-descriptions-item> <div class="card-header">
<a-descriptions-item :label="t('authentication.nickName')">{{ userInfo.nickName }}</a-descriptions-item> <div class="header-bg-gradient"></div>
<a-descriptions-item :label="t('authentication.avatar')"> <div class="header-content">
<a-avatar v-if="userInfo.avatar" size="large" :src="userAvatar" style="background-color: #eee"> </a-avatar> <div class="avatar-wrapper">
<a-avatar v-else size="large" style="background-color: #00b4f5"> <a-avatar v-if="userInfo.avatar" :size="100" :src="userAvatar" class="user-avatar"> </a-avatar>
{{ userInfo.username }} <a-avatar v-else size="100" class="user-avatar default-avatar">
</a-avatar> {{ userInfo.username }}
</a-descriptions-item> </a-avatar>
<a-descriptions-item :label="t('authentication.email')">{{ userInfo.email }}</a-descriptions-item> <!-- <div class="status-indicator"></div> -->
<a-descriptions-item :label="t('authentication.phoneNumber')">{{ userInfo.phoneCode }}{{ userInfo.mobile }}</a-descriptions-item>
<a-descriptions-item v-if="settingStore.sysPublic.oauthEnabled && settingStore.isPlus" label="第三方账号绑定">
<template v-for="item in computedOauthBounds" :key="item.name">
<div v-if="item.addonId" class="flex items-center gap-2 mb-2">
<fs-icon :icon="item.icon" class="mr-2 text-blue-500 w-5 flex justify-center items-center" />
<span class="mr-2 w-36">{{ item.title }}</span>
<a-button v-if="item.bound" type="primary" danger @click="unbind(item.name)">解绑</a-button>
<a-button v-else type="primary" @click="bind(item.name)">绑定</a-button>
</div> </div>
</template> <div class="user-info">
</a-descriptions-item> <h2 class="user-name flex items-center">
<a-descriptions-item :label="t('common.handle')"> {{ userInfo.nickName }}
<a-button type="primary" @click="doUpdate">{{ t("authentication.updateProfile") }}</a-button> <fs-values-format :model-value="userInfo.roleIds" :dict="roleDict" color="blue" />
<change-password-button class="ml-10" :show-button="true"> </change-password-button> </h2>
</a-descriptions-item> <div class="user-details">
</a-descriptions> <a-tag color="blue" class="detail-tag">
<span class="tag-icon">👤</span>
{{ userInfo.username }}
</a-tag>
<a-tag v-if="userInfo.email" color="green" class="detail-tag">
<span class="tag-icon">📧</span>
{{ userInfo.email }}
</a-tag>
<a-tag v-if="userInfo.mobile" color="purple" class="detail-tag">
<span class="tag-icon">📱</span>
{{ userInfo.mobile }}
</a-tag>
</div>
</div>
<div class="action-buttons">
<a-button type="primary" class="action-btn" @click="doUpdate">
{{ t("authentication.updateProfile") }}
</a-button>
<change-password-button class="ml-10" :show-button="true" />
</div>
</div>
</div>
</div>
<div class="flex flex-wrap">
<div class="w-full md:w-1/2 md:pr-2">
<div v-if="settingStore.sysPublic.oauthEnabled && settingStore.isPlus" class="bindings-card md:rounded">
<div class="card-title">
<fs-icon icon="ion:link-outline" class="title-icon" />
<span>第三方账号绑定</span>
</div>
<div class="bindings-list">
<template v-for="item in computedOauthBounds" :key="item.name">
<div v-if="item.addonId" class="binding-item">
<div class="binding-icon">
<fs-icon :icon="item.icon" class="icon" />
</div>
<div class="binding-info">
<span class="binding-name">{{ item.title }}</span>
<span>
<a-tag v-if="item.bound" color="green" class="bound-tag1">已绑定</a-tag>
<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)">
<template #icon><fs-icon icon="ion:unlink-outline" /></template>
解绑
</a-button>
<a-button v-else type="primary" class="action-btn" @click="bind(item.name)">
<template #icon><fs-icon icon="ion:link-outline" /></template>
绑定
</a-button>
</div>
</template>
<div v-if="computedOauthBounds.length === 0" class="empty-text">暂无可用的第三方账号绑定</div>
</div>
</div>
</div>
<div class="w-full md:w-1/2 md:pl-2">
<div v-if="settingStore.sysPublic.passkeyEnabled && settingStore.isPlus" class="passkey-card md:rounded">
<div class="card-title">
<fs-icon icon="ion:finger-print" class="title-icon" />
<span>Passkey 安全密钥</span>
</div>
<div class="passkey-list">
<div v-for="passkey in passkeys" :key="passkey.id" class="passkey-item">
<div class="passkey-icon">
<fs-icon icon="ion:finger-print" class="icon" />
</div>
<div class="passkey-info">
<div class="passkey-name">{{ passkey.deviceName }}</div>
<div class="passkey-meta flex items-center">
<span class="meta-item flex items-center">
<fs-icon icon="ion:calendar-outline" class="meta-icon" />
{{ formatDate(passkey.registeredAt) }}
</span>
<span class="meta-item flex items-center">
<fs-icon icon="ion:time-outline" class="meta-icon" />
最近使用<fs-time-humanize :model-value="passkey.updateTime" />
</span>
</div>
</div>
<a-button type="primary" danger class="remove-btn" @click="unbindPasskey(passkey.id)">
<template #icon><fs-icon icon="ion:trash-outline" /></template>
移除
</a-button>
</div>
</div>
<div v-if="passkeys.length === 0" class="empty-state">
<fs-icon icon="ion:finger-print" class="empty-icon" />
<p class="empty-text">暂无Passkey</p>
</div>
<div v-if="!passkeySupported" class="warning-box">
<fs-icon icon="ion:warning-outline" class="warning-icon" />
<span>{{ t("authentication.passkeyNotSupported") }}</span>
</div>
<a-button v-if="passkeySupported" type="primary" class="add-btn" @click="registerPasskey">
<template #icon><fs-icon icon="ion:add-circle-outline" /></template>
注册新的Passkey
</a-button>
<pre class="helper pre">{{ t("authentication.passkeyRegisterHelper") }}</pre>
</div>
</div>
</div>
</div> </div>
</fs-page> </fs-page>
</template> </template>
@@ -40,9 +136,12 @@ import { computed, onMounted, Ref, ref } from "vue";
import ChangePasswordButton from "/@/views/certd/mine/change-password-button.vue"; import ChangePasswordButton from "/@/views/certd/mine/change-password-button.vue";
import { useI18n } from "/src/locales"; import { useI18n } from "/src/locales";
import { useUserProfile } from "./use"; import { useUserProfile } from "./use";
import { Modal } from "ant-design-vue"; import { usePasskeyRegister } from "./use";
import { message, Modal, notification } from "ant-design-vue";
import { useSettingStore } from "/@/store/settings"; import { useSettingStore } from "/@/store/settings";
import { isEmpty } from "lodash-es"; import { isEmpty } from "lodash-es";
import { dict } from "@fast-crud/fast-crud";
import dayjs from "dayjs";
const { t } = useI18n(); const { t } = useI18n();
@@ -53,11 +152,20 @@ defineOptions({
const settingStore = useSettingStore(); const settingStore = useSettingStore();
const userInfo: Ref = ref({}); const userInfo: Ref = ref({});
const passkeys = ref([]);
const passkeySupported = ref(false);
const getUserInfo = async () => { const getUserInfo = async () => {
userInfo.value = await api.getMineInfo(); userInfo.value = await api.getMineInfo();
}; };
const roleDict = dict({
url: "/basic/user/getSimpleRoles",
value: "id",
label: "name",
});
const { openEditProfileDialog } = useUserProfile(); const { openEditProfileDialog } = useUserProfile();
const { openRegisterDialog } = usePasskeyRegister();
function doUpdate() { function doUpdate() {
openEditProfileDialog({ openEditProfileDialog({
@@ -69,10 +177,12 @@ function doUpdate() {
const oauthBounds = ref([]); const oauthBounds = ref([]);
const oauthProviders = ref([]); const oauthProviders = ref([]);
async function loadOauthBounds() { async function loadOauthBounds() {
const res = await api.GetOauthBounds(); const res = await api.GetOauthBounds();
oauthBounds.value = res; oauthBounds.value = res;
} }
async function loadOauthProviders() { async function loadOauthProviders() {
const res = await api.GetOauthProviders(); const res = await api.GetOauthProviders();
oauthProviders.value = res; oauthProviders.value = res;
@@ -102,12 +212,124 @@ async function unbind(type: string) {
} }
async function bind(type: string) { async function bind(type: string) {
//URL
const res = await api.OauthBoundUrl(type); const res = await api.OauthBoundUrl(type);
const loginUrl = res.loginUrl; const loginUrl = res.loginUrl;
window.location.href = loginUrl; window.location.href = loginUrl;
} }
async function loadPasskeys() {
try {
const res = await api.GetPasskeys();
passkeys.value = res;
} catch (e: any) {
console.error("加载Passkey失败:", e);
}
}
async function unbindPasskey(id: number) {
Modal.confirm({
title: "确认解绑吗?",
okText: "确认",
okType: "danger",
onOk: async () => {
await api.UnbindPasskey(id);
await loadPasskeys();
},
});
}
const toBase64Url = (buffer: ArrayBuffer) => {
const bytes = new Uint8Array(buffer);
let binary = "";
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
};
async function registerPasskey() {
if (!passkeySupported.value) {
Modal.error({ title: "错误", content: "浏览器不支持Passkey" });
return;
}
await openRegisterDialog({
onSubmit: async (ctx: any) => {
const deviceName = ctx.form.deviceName;
if (!deviceName) {
return;
}
await doRegisterPasskey(deviceName);
message.success("Passkey注册成功");
},
});
}
async function doRegisterPasskey(deviceName: string) {
try {
const res: any = await api.generatePasskeyRegistrationOptions();
const options = res;
// navigator.credentials.query({
// publicKey: options,
// });
// const excludeCredentials = passkeys.value.map(item => ({
// id: new TextEncoder().encode(item.passkeyId),
// type: "public-key",
// }));
const credential = await (navigator.credentials as any).create({
publicKey: {
challenge: Uint8Array.from(atob(options.challenge.replace(/-/g, "+").replace(/_/g, "/")), c => c.charCodeAt(0)),
rp: options.rp,
pubKeyCredParams: options.pubKeyCredParams,
timeout: options.timeout || 60000,
attestation: options.attestation,
// excludeCredentials: excludeCredentials,
user: {
id: new TextEncoder().encode(options.userId + ""),
name: userInfo.value.username,
displayName: deviceName,
},
},
});
if (!credential) {
throw new Error("Passkey注册失败");
}
const response = {
id: credential.id,
type: credential.type,
rawId: toBase64Url(credential.rawId),
response: {
attestationObject: toBase64Url(credential.response.attestationObject),
clientDataJSON: toBase64Url(credential.response.clientDataJSON),
},
};
console.log("credential", credential, response);
debugger;
const verifyRes: any = await api.verifyPasskeyRegistration(response, options.challenge, deviceName);
await loadPasskeys();
} catch (e: any) {
console.error("Passkey注册失败:", e);
notification.error({ message: "错误", description: e.message || "Passkey注册失败" });
}
}
const formatDate = (dateString: string) => {
if (!dateString) return "";
return dayjs(dateString).format("YYYY-MM-DD HH:mm:ss");
};
const checkPasskeySupport = () => {
passkeySupported.value = false;
if (typeof window !== "undefined" && "credentials" in navigator && "PublicKeyCredential" in window) {
passkeySupported.value = true;
}
};
const userAvatar = computed(() => { const userAvatar = computed(() => {
if (isEmpty(userInfo.value.avatar)) { if (isEmpty(userInfo.value.avatar)) {
return ""; return "";
@@ -123,5 +345,514 @@ onMounted(async () => {
await getUserInfo(); await getUserInfo();
await loadOauthBounds(); await loadOauthBounds();
await loadOauthProviders(); await loadOauthProviders();
await loadPasskeys();
checkPasskeySupport();
}); });
</script> </script>
<style lang="less">
.page-user-profile {
:deep(.ant-descriptions-item-label) {
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
}
}
.dark {
.page-user-profile {
:deep(.ant-descriptions-item-label) {
color: rgba(255, 255, 255, 0.85);
}
}
.profile-container {
.profile-card,
.bindings-card,
.passkey-card {
background: linear-gradient(135deg, #1f1f1f 0%, #2d2d2d 100%);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
&:hover {
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4);
border-color: rgba(255, 255, 255, 0.2);
}
}
.card-header {
.header-bg-gradient {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
opacity: 0.15;
}
}
.header-content {
.user-avatar {
border-color: #3b3b3b;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
}
.user-name {
color: #e5e5e5;
}
.detail-tag {
background: #3b3b3b;
color: #e5e5e5;
.tag-icon {
color: #e5e5e5;
}
}
}
.bindings-list {
.binding-item {
background: #2d2d2d;
border-color: rgba(255, 255, 255, 0.1);
color: #e5e5e5;
&:hover {
border-color: #667eea;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
}
.binding-name {
color: #e5e5e5;
}
.binding-status {
&.bound {
background: #1a3a2f;
color: #4caf50;
}
&.unbound {
background: #3a352a;
color: #ffb300;
}
}
}
}
.passkey-list {
.passkey-item {
background: #2d2d2d;
border-color: rgba(255, 255, 255, 0.1);
color: #e5e5e5;
&:hover {
border-color: #667eea;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
}
.passkey-name {
color: #e5e5e5;
}
.passkey-meta {
.meta-item {
color: #b0b0b0;
}
.meta-icon {
color: #888888;
}
}
}
}
.empty-state {
color: #b0b0b0;
.empty-icon {
opacity: 0.6;
}
}
.warning-box {
background: #3a2a2a;
border-color: #5a3a3a;
color: #e5e5e5;
.warning-icon {
color: #ef5350;
}
}
.helper {
background: #2d2d2d;
border-color: rgba(255, 255, 255, 0.1);
color: #b0b0b0;
}
}
}
.profile-container {
display: flex;
flex-direction: column;
gap: 20px;
// max-width: 1000px;
.profile-card,
.bindings-card,
.passkey-card {
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
overflow: hidden;
transition: all 0.3s ease;
}
.bindings-card,
.passkey-card {
padding: 18px;
}
.profile-card:hover,
.bindings-card:hover,
.passkey-card:hover {
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
transform: translateY(-2px);
}
.card-header {
position: relative;
padding: 40px 30px;
}
.header-bg-gradient {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
opacity: 0.08;
}
.header-content {
position: relative;
z-index: 1;
display: flex;
align-items: center;
gap: 30px;
}
.avatar-wrapper {
position: relative;
flex-shrink: 0;
}
.user-avatar {
border: 4px solid #ffffff;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
.status-indicator {
position: absolute;
bottom: 8px;
right: 8px;
width: 16px;
height: 16px;
background: #52c41a;
border: 3px solid #ffffff;
border-radius: 50%;
}
.user-info {
flex: 1;
min-width: 0;
}
.user-name {
margin: 0 0 12px 0;
font-size: 24px;
font-weight: 600;
color: #2c3e50;
display: flex;
align-items: center;
gap: 10px;
}
.user-details {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.detail-tag {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 20px;
font-size: 13px;
}
.tag-icon {
font-size: 14px;
}
.action-buttons {
display: flex;
gap: 10px;
align-items: center;
}
.action-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 10px 20px;
font-weight: 500;
border-radius: 8px;
transition: all 0.3s ease;
}
.action-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.card-title {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 2px solid #f0f0f0;
}
.title-icon {
font-size: 20px;
color: #667eea;
}
.bindings-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.binding-item {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
background: #ffffff;
border-radius: 12px;
border: 1px solid #f0f0f0;
transition: all 0.3s ease;
}
.binding-item:hover {
border-color: #667eea;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.1);
}
.binding-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #ebefff 0%, #e5d4ff 100%);
border-radius: 10px;
}
.binding-icon .icon {
font-size: 20px;
color: #ffffff;
}
.binding-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.binding-name {
font-size: 16px;
font-weight: 500;
color: #2c3e50;
}
.binding-status {
font-size: 12px;
padding: 2px 8px;
border-radius: 4px;
font-weight: 500;
}
.binding-status.bound {
background: #e6fffa;
color: #38a169;
}
.binding-status.unbound {
background: #fffaf0;
color: #d69e2e;
}
.passkey-list {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 20px;
}
.passkey-item {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
background: #ffffff;
border-radius: 12px;
border: 1px solid #f0f0f0;
transition: all 0.3s ease;
}
.passkey-item:hover {
border-color: #667eea;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.1);
}
.passkey-icon {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
border-radius: 12px;
}
.passkey-icon .icon {
font-size: 24px;
color: #ffffff;
}
.passkey-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
}
.passkey-name {
font-size: 16px;
font-weight: 600;
color: #2c3e50;
}
.passkey-meta {
display: flex;
flex-wrap: wrap;
gap: 12px;
font-size: 13px;
}
.meta-item {
display: flex;
align-items: center;
gap: 4px;
color: #6b7280;
}
.meta-icon {
font-size: 14px;
color: #9ca3af;
}
.remove-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border-radius: 8px;
transition: all 0.3s ease;
}
.remove-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.2);
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px 10px;
color: #9ca3af;
}
.empty-icon {
font-size: 48px;
margin-bottom: 12px;
opacity: 0.5;
}
.empty-text {
margin: 0;
font-size: 14px;
}
.warning-box {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: #fff7ed;
border: 1px solid #fed7d7;
border-radius: 8px;
margin-bottom: 16px;
}
.warning-icon {
font-size: 18px;
color: #f56565;
}
.add-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 10px 20px;
border-radius: 8px;
transition: all 0.3s ease;
}
.add-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.helper {
background: #f8f9fa;
padding: 12px 16px;
border-radius: 8px;
font-size: 13px;
color: #6b7280;
border: 1px solid #e5e7eb;
margin-top: 16px;
}
@media (max-width: 768px) {
.header-content {
flex-direction: column;
text-align: center;
}
.user-details {
justify-content: center;
}
.action-buttons {
justify-content: center;
width: 100%;
}
}
}
</style>
@@ -734,7 +734,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
}), }),
}, },
column: { column: {
width: 100, width: 140,
sorter: true, sorter: true,
align: "center", align: "center",
}, },
@@ -757,7 +757,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
helper: t("monitor.ipSyncModeHelper"), helper: t("monitor.ipSyncModeHelper"),
}, },
column: { column: {
width: 100, width: 140,
sorter: true, sorter: true,
align: "center", align: "center",
}, },
@@ -779,7 +779,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
helper: t("monitor.ipIgnoreCoherenceHelper"), helper: t("monitor.ipIgnoreCoherenceHelper"),
}, },
column: { column: {
width: 100, width: 180,
sorter: true, sorter: true,
align: "center", align: "center",
}, },
@@ -50,4 +50,12 @@ export const openkeyApi = {
data: { id }, data: { id },
}); });
}, },
async GetSecret(id: number) {
return await request({
url: apiPrefix + "/getSecret",
method: "post",
data: { id },
});
},
}; };
@@ -145,6 +145,23 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
column: { column: {
width: 580, width: 580,
sorter: true, sorter: true,
cellRender: ({ row, value }) => {
async function getSecret(id: number) {
row.keySecret = await api.GetSecret(id);
}
if (value.includes("*")) {
return (
<div class="flex items-center flex-between">
{value}
<a-button type="primary" size="small" onClick={() => getSecret(row.id)}>
</a-button>
</div>
);
} else {
return <fs-copyable model-value={value}></fs-copyable>;
}
},
}, },
}, },
scope: { scope: {
@@ -26,7 +26,8 @@ const type = route.params.type as string;
const query = route.query; const query = route.query;
async function checkNotify() { async function checkNotify() {
const res = await api.Notify(type, query); // const res = await api.Notify(type, query);
const res = "success";
if (res === "success") { if (res === "success") {
return true; return true;
} }
@@ -110,6 +110,14 @@ export async function BatchUpdateNotificaiton(pipelineIds: number[], notificatio
}); });
} }
export async function BatchUpdateProject(pipelineIds: number[], toProjectId: number): Promise<void> {
return await request({
url: apiPrefix + "/batchTransfer",
method: "post",
data: { ids: pipelineIds, toProjectId: toProjectId },
});
}
export async function BatchDelete(pipelineIds: number[]): Promise<void> { export async function BatchDelete(pipelineIds: number[]): Promise<void> {
return await request({ return await request({
url: apiPrefix + "/batchDelete", url: apiPrefix + "/batchDelete",
@@ -146,6 +154,7 @@ export type CertInfo = {
ic: string; ic: string;
der: string; der: string;
pfx: string; pfx: string;
detail: any;
}; };
export async function GetCert(pipelineId: number): Promise<CertInfo> { export async function GetCert(pipelineId: number): Promise<CertInfo> {
@@ -1,12 +1,47 @@
<template> <template>
<div class="cert-view"> <div class="cert-view">
<a-list item-layout="vertical" :data-source="certFiles"> <div class="cert-detail mt-4">
<a-descriptions class="w-full" bordered :column="2" size="small">
<a-descriptions-item label="主域名">
<a-tag color="blue">{{ props.cert.detail?.domains?.commonName || "-" }}</a-tag>
</a-descriptions-item>
<a-descriptions-item label="颁发机构">
<a-tag color="green">{{ props.cert.detail?.issuer?.commonName || "-" }}</a-tag>
</a-descriptions-item>
<a-descriptions-item label="备用域名" :span="2">
<a-tag v-for="(domain, index) in props.cert.detail?.domains?.altNames || []" :key="index" color="blue">
{{ domain }}
</a-tag>
<span v-if="!props.cert.detail?.domains?.altNames?.length">-</span>
</a-descriptions-item>
<a-descriptions-item label="生效时间">
{{ formatDate(props.cert.detail?.notBefore) }}
</a-descriptions-item>
<a-descriptions-item label="过期时间">
{{ formatDate(props.cert.detail?.notAfter) }}
</a-descriptions-item>
<a-descriptions-item label="指纹">
<div class="w-full flex flex-col fingerprint">
<div class="flex flex-nowrap">
<span class="font-bold label">SHA-1:</span> <fs-copyable class="inline-flex overflow-ellipsis text" :model-value="props.cert.detail?.fingerprints?.fingerprint || '-'"></fs-copyable>
</div>
<div class="flex flex-nowrap mt-1">
<span class="font-bold label">SHA-256:</span> <fs-copyable class="inline-flex overflow-ellipsis text" :model-value="props.cert.detail?.fingerprints?.fingerprint256 || '-'"></fs-copyable>
</div>
<div class="flex flex-nowrap mt-1">
<span class="font-bold label">SHA-512:</span> <fs-copyable class="inline-flex overflow-ellipsis text" :model-value="props.cert.detail?.fingerprints?.fingerprint512 || '-'"></fs-copyable>
</div>
</div>
</a-descriptions-item>
</a-descriptions>
</div>
<a-list item-layout="vertical" :data-source="certFiles" class="cert-content">
<template #renderItem="{ item }"> <template #renderItem="{ item }">
<a-list-item key="item.key"> <a-list-item key="item.key">
<a-list-item-meta> <a-list-item-meta>
<template #title> <template #title>
<div class="title"> <div class="title">
<div>{{ item.name }}({{ item.fileName }})</div> <div class="font-bold">{{ item.name }}({{ item.fileName }})</div>
<fs-copyable :model-value="item.content" :button="{ show: false }"> <fs-copyable :model-value="item.content" :button="{ show: false }">
<a-tag color="success">复制</a-tag> <a-tag color="success">复制</a-tag>
</fs-copyable> </fs-copyable>
@@ -34,10 +69,25 @@ const certFiles = ref([
{ name: "私钥", fileName: "private.pem", key: "key", content: props.cert.key }, { name: "私钥", fileName: "private.pem", key: "key", content: props.cert.key },
{ name: "中间证书", fileName: "chain.pem", key: "ic", content: props.cert.ic }, { name: "中间证书", fileName: "chain.pem", key: "ic", content: props.cert.ic },
]); ]);
function formatDate(dateStr: string): string {
if (!dateStr) return "-";
const date = new Date(dateStr);
return date.toLocaleString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
});
}
</script> </script>
<style lang="less"> <style lang="less">
.cert-view { .cert-view {
margin-right: 25px;
.title { .title {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -46,5 +96,29 @@ const certFiles = ref([
margin-block-end: 0px !important; margin-block-end: 0px !important;
margin-top: 10px; margin-top: 10px;
} }
.cert-detail {
table {
width: 100%;
table-layout: fixed;
}
.fingerprint {
.label {
width: 70px;
flex-shrink: 0;
}
.text {
white-space: nowrap;
overflow: hidden;
flex-grow: 1;
}
}
}
.cert-content {
.ant-list-item {
padding-left: 0;
padding-right: 0;
}
}
} }
</style> </style>
@@ -6,7 +6,7 @@ import { useRouter } from "vue-router";
import { compute, CreateCrudOptionsRet, dict, useFormWrapper } from "@fast-crud/fast-crud"; import { compute, CreateCrudOptionsRet, dict, useFormWrapper } from "@fast-crud/fast-crud";
import NotificationSelector from "/@/views/certd/notification/notification-selector/index.vue"; import NotificationSelector from "/@/views/certd/notification/notification-selector/index.vue";
import { useReference } from "/@/use/use-refrence"; import { useReference } from "/@/use/use-refrence";
import { computed, ref } from "vue"; import { computed, provide, Ref, ref } from "vue";
import * as api from "../api"; import * as api from "../api";
import { PluginGroup, usePluginStore } from "/@/store/plugin"; import { PluginGroup, usePluginStore } from "/@/store/plugin";
import { createNotificationApi } from "/@/views/certd/notification/api"; import { createNotificationApi } from "/@/views/certd/notification/api";
@@ -89,9 +89,10 @@ export function useCertPipelineCreator() {
const inputs: any = {}; const inputs: any = {};
const moreParams = []; const moreParams = [];
const doSubmit = req.doSubmit; const doSubmit = req.doSubmit;
for (const inputKey in req.certPlugin.input) { const certPlugin = req.certPlugin;
for (const inputKey in certPlugin.input) {
// inputs[inputKey].form.show = true; // inputs[inputKey].form.show = true;
const inputDefine = cloneDeep(req.certPlugin.input[inputKey]); const inputDefine = cloneDeep(certPlugin.input[inputKey]);
if (inputDefine.maybeNeed) { if (inputDefine.maybeNeed) {
moreParams.push(inputKey); moreParams.push(inputKey);
} }
@@ -103,7 +104,6 @@ export function useCertPipelineCreator() {
}, },
}; };
} }
const pluginStore = usePluginStore(); const pluginStore = usePluginStore();
const randomHour = Math.floor(Math.random() * 6); const randomHour = Math.floor(Math.random() * 6);
const randomMin = Math.floor(Math.random() * 60); const randomMin = Math.floor(Math.random() * 60);
@@ -322,7 +322,7 @@ export function useCertPipelineCreator() {
return certPlugins; return certPlugins;
} }
async function openAddCertdPipelineDialog(req: { pluginName: string; defaultGroupId?: number; title?: string }) { async function openAddCertdPipelineDialog(req: { pluginName: string; defaultGroupId?: number; title?: string; currentPluginRef: Ref<any> }) {
//检查是否流水线数量超出限制 //检查是否流水线数量超出限制
await checkPipelineLimit(); await checkPipelineLimit();
@@ -393,6 +393,8 @@ export function useCertPipelineCreator() {
message.error("该证书申请插件不存在"); message.error("该证书申请插件不存在");
return; return;
} }
req.currentPluginRef.value = certPlugin;
const { crudOptions } = createCrudOptions({ const { crudOptions } = createCrudOptions({
certPlugin, certPlugin,
doSubmit, doSubmit,
@@ -0,0 +1,64 @@
<template>
<fs-button icon="mdi:format-list-group" class="need-plus" type="link" text="转到其他项目" @click="openProjectSelectDialog"></fs-button>
</template>
<script setup lang="ts">
import * as api from "../api";
import { notification } from "ant-design-vue";
import { dict, useFormWrapper } from "@fast-crud/fast-crud";
import { useSettingStore } from "/@/store/settings";
const props = defineProps<{
selectedRowKeys: any[];
}>();
const emit = defineEmits<{
change: any;
}>();
async function batchUpdateProjectRequest(toProjectId: number) {
await api.BatchUpdateProject(props.selectedRowKeys, toProjectId);
emit("change");
}
const pipelineProjectDictRef = dict({
url: "/enterprise/project/all",
value: "id",
label: "name",
});
const { openCrudFormDialog } = useFormWrapper();
const settingStore = useSettingStore();
async function openProjectSelectDialog() {
settingStore.checkPlus();
const crudOptions: any = {
columns: {
toProjectId: {
title: "转到项目",
type: "dict-select",
dict: pipelineProjectDictRef,
form: {
rules: [{ required: true, message: "请选择项目" }],
},
},
},
form: {
mode: "edit",
//@ts-ignore
async doSubmit({ form }) {
await batchUpdateProjectRequest(form.toProjectId);
},
col: {
span: 22,
},
labelCol: {
style: {
width: "100px",
},
},
wrapper: {
title: "批量转到其他项目",
width: 600,
},
},
} as any;
await openCrudFormDialog({ crudOptions });
}
</script>
@@ -16,13 +16,16 @@ import { useCertViewer } from "/@/views/certd/pipeline/use";
import { useI18n } from "/src/locales"; import { useI18n } from "/src/locales";
import { useDicts } from "../dicts"; import { useDicts } from "../dicts";
import { useProjectStore } from "/@/store/project"; import { useProjectStore } from "/@/store/project";
import { useCrudPermission } from "/@/plugin/permission";
export default function ({ crudExpose, context: { selectedRowKeys, openCertApplyDialog, hasActionPermission } }: CreateCrudOptionsProps): CreateCrudOptionsRet { export default function ({ crudExpose, context: { selectedRowKeys, openCertApplyDialog, permission } }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter(); const router = useRouter();
const lastResRef = ref(); const lastResRef = ref();
const { t } = useI18n(); const { t } = useI18n();
const { hasActionPermission } = useCrudPermission({ permission });
const { openUploadCreateDialog } = useCertUpload(); const { openUploadCreateDialog } = useCertUpload();
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => { const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
@@ -42,6 +42,7 @@
<change-group v-if="hasActionPermission('write')" :selected-row-keys="selectedRowKeys" @change="batchFinished"></change-group> <change-group v-if="hasActionPermission('write')" :selected-row-keys="selectedRowKeys" @change="batchFinished"></change-group>
<change-notification v-if="hasActionPermission('write')" :selected-row-keys="selectedRowKeys" @change="batchFinished"></change-notification> <change-notification v-if="hasActionPermission('write')" :selected-row-keys="selectedRowKeys" @change="batchFinished"></change-notification>
<change-trigger v-if="hasActionPermission('write')" :selected-row-keys="selectedRowKeys" @change="batchFinished"></change-trigger> <change-trigger v-if="hasActionPermission('write')" :selected-row-keys="selectedRowKeys" @change="batchFinished"></change-trigger>
<change-project v-if="hasActionPermission('write')" :selected-row-keys="selectedRowKeys" @change="batchFinished"></change-project>
</div> </div>
</div> </div>
<template #form-bottom> <template #form-bottom>
@@ -52,11 +53,13 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onActivated, onMounted, ref } from "vue"; import { computed, onActivated, onMounted, provide, ref } from "vue";
import { dict, useFs } from "@fast-crud/fast-crud"; import { dict, useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud"; import createCrudOptions from "./crud";
import ChangeGroup from "./components/change-group.vue"; import ChangeGroup from "./components/change-group.vue";
import ChangeTrigger from "./components/change-trigger.vue"; import ChangeTrigger from "./components/change-trigger.vue";
import ChangeProject from "./components/change-project.vue";
import BatchRerun from "./components/batch-rerun.vue"; import BatchRerun from "./components/batch-rerun.vue";
import { Modal, notification } from "ant-design-vue"; import { Modal, notification } from "ant-design-vue";
import * as api from "./api"; import * as api from "./api";
@@ -69,6 +72,7 @@ import { groupDictRef } from "./group/dicts";
import { useCertPipelineCreator } from "./certd-form/use"; import { useCertPipelineCreator } from "./certd-form/use";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { useCrudPermission } from "/@/plugin/permission"; import { useCrudPermission } from "/@/plugin/permission";
import CertdForm from "./certd-form/certd-form.vue";
defineOptions({ defineOptions({
name: "PipelineManager", name: "PipelineManager",
@@ -79,11 +83,17 @@ const context: any = {
selectedRowKeys, selectedRowKeys,
}; };
const router = useRouter(); const router = useRouter();
const { openAddCertdPipelineDialog } = useCertPipelineCreator();
function onActionbarMoreItemClick(req: { key: string; item: any }) { function onActionbarMoreItemClick(req: { key: string; item: any }) {
openCertApplyDialog({ key: req.key, title: req.item?.title }); openCertApplyDialog({ key: req.key, title: req.item?.title });
} }
const certdFormRef = ref<typeof CertdForm>();
const currentPluginRef = ref();
provide("getCurrentPluginDefine", () => {
return currentPluginRef.value;
});
const addMorePipelineBtns = computed(() => { const addMorePipelineBtns = computed(() => {
return [ return [
{ key: "CertApplyGetFormAliyun", title: t("certd.pipelinePage.aliyunSubscriptionPipeline"), icon: "svg:icon-aliyun" }, { key: "CertApplyGetFormAliyun", title: t("certd.pipelinePage.aliyunSubscriptionPipeline"), icon: "svg:icon-aliyun" },
@@ -92,6 +102,7 @@ const addMorePipelineBtns = computed(() => {
{ key: "BatchAddPipeline", title: t("certd.pipelinePage.batchAddPipeline"), icon: "ion:duplicate" }, { key: "BatchAddPipeline", title: t("certd.pipelinePage.batchAddPipeline"), icon: "ion:duplicate" },
]; ];
}); });
const { openAddCertdPipelineDialog } = useCertPipelineCreator();
function openCertApplyDialog(req: { key: string; title: string }) { function openCertApplyDialog(req: { key: string; title: string }) {
if (req.key === "AddPipeline") { if (req.key === "AddPipeline") {
crudExpose.openAdd({}); crudExpose.openAdd({});
@@ -104,7 +115,7 @@ function openCertApplyDialog(req: { key: string; title: string }) {
const searchForm = crudExpose.getSearchValidatedFormData(); const searchForm = crudExpose.getSearchValidatedFormData();
const defaultGroupId = searchForm.groupId; const defaultGroupId = searchForm.groupId;
openAddCertdPipelineDialog({ pluginName: req.key, defaultGroupId, title: req.title }); openAddCertdPipelineDialog({ pluginName: req.key, defaultGroupId, title: req.title, currentPluginRef });
} }
context.openCertApplyDialog = openCertApplyDialog; context.openCertApplyDialog = openCertApplyDialog;
context.permission = { isProjectPermission: true }; context.permission = { isProjectPermission: true };
@@ -294,13 +294,9 @@ function useStepForm() {
currentStep.value.input[key] = pluginSysConfig.sysSetting?.input[key]; currentStep.value.input[key] = pluginSysConfig.sysSetting?.input[key];
} }
} }
console.log("currentStepTypeChanged:", currentStep.value);
console.log("currentStepPlugin:", currentPlugin.value);
}; };
const stepSave = async (e: any) => { const stepSave = async (e: any) => {
console.log("currentStepSave", currentStep.value);
try { try {
await stepFormRef.value.validate(); await stepFormRef.value.validate();
} catch (e) { } catch (e) {
@@ -72,14 +72,13 @@ import * as _ from "lodash-es";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import PiStepForm from "../step-form/index.vue"; import PiStepForm from "../step-form/index.vue";
import { Modal } from "ant-design-vue"; import { Modal } from "ant-design-vue";
import { CopyOutlined } from "@ant-design/icons-vue";
import VDraggable from "vuedraggable"; import VDraggable from "vuedraggable";
import { useUserStore } from "/@/store/user"; import { useUserStore } from "/@/store/user";
import { useSettingStore } from "/@/store/settings"; import { useSettingStore } from "/@/store/settings";
import { filter } from "lodash-es"; import { filter } from "lodash-es";
export default { export default {
name: "PiTaskForm", name: "PiTaskForm",
components: { CopyOutlined, PiStepForm, VDraggable }, components: { PiStepForm, VDraggable },
props: { props: {
editMode: { editMode: {
type: Boolean, type: Boolean,
@@ -805,9 +805,11 @@ export default defineComponent({
let errorMessages: any = []; let errorMessages: any = [];
let errorIndex = 1; let errorIndex = 1;
eachSteps(pp, (step: any, task: any, stage: any) => { eachSteps(pp, (step: any, task: any, stage: any) => {
if (step.disabled !== true) { if (step.disabled === true) {
stepIds.push(step.id); return;
} }
stepIds.push(step.id);
if (step.input) { if (step.input) {
for (const key in step.input) { for (const key in step.input) {
const value = step.input[key]; const value = step.input[key];
@@ -73,3 +73,17 @@ export async function ApproveJoin(form: any) {
data: form, data: form,
}); });
} }
export async function GetSelfResources() {
return await request({
url: "/enterprise/transfer/selfResources",
method: "post",
});
}
export async function TransferResources() {
return await request({
url: "/enterprise/transfer/doTransfer",
method: "post",
});
}
@@ -9,8 +9,12 @@
<fs-values-format :model-value="project.permission" :dict="projectPermissionDict"></fs-values-format> <fs-values-format :model-value="project.permission" :dict="projectPermissionDict"></fs-values-format>
<a-divider type="vertical"></a-divider> <a-divider type="vertical"></a-divider>
<fs-values-format :model-value="project.status" :dict="projectMemberStatusDict"></fs-values-format> --> <fs-values-format :model-value="project.status" :dict="projectMemberStatusDict"></fs-values-format> -->
<a-divider type="vertical"></a-divider>
<a-button class="mr-5" type="primary" @click="openTransferDialog">个人数据迁移</a-button>
<a-button v-if="userStore.isAdmin" type="primary" @click="goProjectManager">{{ t("certd.project.projectManage") }}</a-button>
</span> </span>
</div> </div>
<div class="more"></div>
</template> </template>
<fs-crud ref="crudRef" v-bind="crudBinding"> <fs-crud ref="crudRef" v-bind="crudBinding">
<template #pagination-left> <template #pagination-left>
@@ -29,11 +33,13 @@ import createCrudOptions from "./crud";
import { message, Modal } from "ant-design-vue"; import { message, Modal } from "ant-design-vue";
import { DeleteBatch } from "./api"; import { DeleteBatch } from "./api";
import { useI18n } from "/src/locales"; import { useI18n } from "/src/locales";
import { useRoute } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { useProjectStore } from "/@/store/project"; import { useProjectStore } from "/@/store/project";
import { request } from "/@/api/service"; import { request } from "/@/api/service";
import { useDicts } from "../../dicts"; import { useDicts } from "../../dicts";
import { useCrudPermission } from "/@/plugin/permission"; import { useCrudPermission } from "/@/plugin/permission";
import { useUserStore } from "/@/store/user";
import { useTransfer } from "./use";
const { t } = useI18n(); const { t } = useI18n();
@@ -49,6 +55,14 @@ if (!projectId) {
projectId = projectStore.currentProject?.id; projectId = projectStore.currentProject?.id;
} }
const router = useRouter();
const userStore = useUserStore();
function goProjectManager() {
router.push(`/sys/enterprise/project`);
}
const { openTransferDialog } = useTransfer();
const { projectPermissionDict, projectMemberStatusDict, userDict } = useDicts(); const { projectPermissionDict, projectMemberStatusDict, userDict } = useDicts();
const project: Ref<any> = ref({}); const project: Ref<any> = ref({});
@@ -1,46 +0,0 @@
import { dict } from "@fast-crud/fast-crud";
import { useDicts } from "../../dicts";
import { useFormDialog } from "/@/use/use-dialog";
export function useApprove() {
const { openFormDialog } = useFormDialog();
const { projectPermissionDict, projectMemberStatusDict, userDict } = useDicts();
function openApproveDialog({ id, permission, onSubmit }: { id: any; permission: any; onSubmit: any }) {
openFormDialog({
title: "审批加入申请",
columns: {
permission: {
title: "成员权限",
type: "dict-select",
dict: projectPermissionDict,
},
status: {
title: "审批结果",
type: "dict-radio",
dict: dict({
data: [
{
label: "通过",
value: "approved",
},
{
label: "拒绝",
value: "rejected",
},
],
}),
},
},
onSubmit: onSubmit,
initialForm: {
id: id,
permission: permission,
status: "approved",
},
});
}
return {
openApproveDialog,
};
}
@@ -0,0 +1,134 @@
import { dict } from "@fast-crud/fast-crud";
import { useDicts } from "../../dicts";
import { useFormDialog } from "/@/use/use-dialog";
import * as api from "./api";
import { useProjectStore } from "/@/store/project";
import { message, Modal } from "ant-design-vue";
import { Ref, ref } from "vue";
export function useApprove() {
const { openFormDialog } = useFormDialog();
const { projectPermissionDict, projectMemberStatusDict, userDict } = useDicts();
function openApproveDialog({ id, permission, onSubmit }: { id: any; permission: any; onSubmit: any }) {
openFormDialog({
title: "审批加入申请",
columns: {
permission: {
title: "成员权限",
type: "dict-select",
dict: projectPermissionDict,
},
status: {
title: "审批结果",
type: "dict-radio",
dict: dict({
data: [
{
label: "通过",
value: "approved",
},
{
label: "拒绝",
value: "rejected",
},
],
}),
},
},
onSubmit: onSubmit,
initialForm: {
id: id,
permission: permission,
status: "approved",
},
});
}
return {
openApproveDialog,
};
}
export function useTransfer() {
const { openFormDialog } = useFormDialog();
async function doTransfer() {
Modal.confirm({
title: "请确认",
content: () => (
<div>
<p></p>
<p class="text-red-500"></p>
</div>
),
okText: "确认",
okType: "primary",
onOk: async () => {
await api.TransferResources();
message.success("迁移成功");
await loadMyResources();
},
});
}
const selfResources: Ref = ref({});
const projectStore = useProjectStore();
async function loadMyResources() {
selfResources.value = await api.GetSelfResources();
}
async function openTransferDialog() {
await loadMyResources();
openFormDialog({
title: "迁移我的个人资源到当前企业项目",
wrapper: {
buttons: {
ok: {
show: false,
},
reset: {
show: false,
},
},
},
body() {
return (
<div class="p-8">
<div class="flex flex-row items-center justify-evenly w-full">
<div>
<h3 class="text-lg font-bold"></h3>
<div class="mt-4">
<p>线{selfResources.value.pipeline}</p>
<p>线{selfResources.value.history}</p>
<p>线{selfResources.value.historyLog}</p>
<p>线{selfResources.value.pipelineGroup}</p>
<p>{selfResources.value.storage}</p>
<p>{selfResources.value.certInfo}</p>
<p>{selfResources.value.access}</p>
<p>{selfResources.value.siteMonitor}</p>
<p>{selfResources.value.notification}</p>
<p>{selfResources.value.group}</p>
<p>线{selfResources.value.template}</p>
<p>{selfResources.value.domain}</p>
<p>{selfResources.value.subdomain}</p>
<p>cname记录{selfResources.value.cnameRecord}</p>
</div>
</div>
<div class="text-2xl font-bold"> </div>
<div>"{projectStore.currentProject?.name}"</div>
</div>
<div class="flex flex-row items-center justify-center w-full">
<a-button type="primary" onClick={doTransfer}>
</a-button>
</div>
</div>
);
},
});
}
return {
openTransferDialog,
};
}
@@ -7,7 +7,7 @@
</div> </div>
<div class="more"> <div class="more">
<a-button v-if="userStore.isAdmin" @click="goProjectManager">{{ t("certd.project.projectManager") }}</a-button> <a-button v-if="userStore.isAdmin" type="primary" @click="goProjectManager">{{ t("certd.project.projectManage") }}</a-button>
</div> </div>
</template> </template>
<div class="project-container"> <div class="project-container">
@@ -4,7 +4,7 @@
<!-- <div class="login-title">登录</div>--> <!-- <div class="login-title">登录</div>-->
<template v-if="!isOauthOnly"> <template v-if="!isOauthOnly">
<a-tabs v-model:active-key="formState.loginType" :tab-bar-style="{ textAlign: 'center', borderBottom: 'unset' }"> <a-tabs v-model:active-key="formState.loginType" :tab-bar-style="{ textAlign: 'center', borderBottom: 'unset' }">
<a-tab-pane key="password" :tab="t('authentication.passwordTab')" :disabled="sysPublicSettings.passwordLoginEnabled !== true"> <a-tab-pane key="password" :tab="t('authentication.passwordTab')">
<template v-if="formState.loginType === 'password'"> <template v-if="formState.loginType === 'password'">
<!-- <div class="login-title">登录</div>--> <!-- <div class="login-title">登录</div>-->
<a-form-item required has-feedback name="username" :rules="rules.username"> <a-form-item required has-feedback name="username" :rules="rules.username">
@@ -46,11 +46,21 @@
</a-form-item> </a-form-item>
</template> </template>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="passkey" :tab="t('authentication.passkeyTab')">
<template v-if="formState.loginType === 'passkey'">
<div v-if="!passkeySupported" class="text-red-500 text-sm mt-2 text-center mb-10">
{{ t("authentication.passkeyNotSupported") }}
</div>
</template>
</a-tab-pane>
</a-tabs> </a-tabs>
<a-form-item> <a-form-item>
<a-button type="primary" size="large" html-type="button" :loading="loading" class="login-button" @click="handleFinish"> <a-button v-if="formState.loginType !== 'passkey'" type="primary" size="large" html-type="button" :loading="loading" class="login-button" @click="handleFinish">
{{ queryBindCode ? t("authentication.bindButton") : t("authentication.loginButton") }} {{ queryBindCode ? t("authentication.bindButton") : t("authentication.loginButton") }}
</a-button> </a-button>
<a-button v-else type="primary" size="large" html-type="button" :loading="loading" class="login-button" :disabled="!passkeySupported" @click="handlePasskeyLogin">
{{ t("authentication.passkeyLogin") }}
</a-button>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<div class="mt-2 flex justify-between items-center"> <div class="mt-2 flex justify-between items-center">
@@ -94,8 +104,8 @@
</a-form> </a-form>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { computed, defineComponent, nextTick, reactive, ref, toRaw } from "vue"; import { computed, nextTick, reactive, ref, toRaw, onMounted } from "vue";
import { useUserStore } from "/src/store/user"; import { useUserStore } from "/src/store/user";
import { useSettingStore } from "/@/store/settings"; import { useSettingStore } from "/@/store/settings";
import { utils } from "@fast-crud/fast-crud"; import { utils } from "@fast-crud/fast-crud";
@@ -103,193 +113,198 @@ import SmsCode from "/@/views/framework/login/sms-code.vue";
import { useI18n } from "/@/locales"; import { useI18n } from "/@/locales";
import { LanguageToggle } from "/@/vben/layouts"; import { LanguageToggle } from "/@/vben/layouts";
import CaptchaInput from "/@/components/captcha/captcha-input.vue"; import CaptchaInput from "/@/components/captcha/captcha-input.vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute } from "vue-router";
import OauthFooter from "/@/views/framework/oauth/oauth-footer.vue"; import OauthFooter from "/@/views/framework/oauth/oauth-footer.vue";
import * as oauthApi from "../oauth/api"; import * as oauthApi from "../oauth/api";
import { notification } from "ant-design-vue"; import { notification } from "ant-design-vue";
export default defineComponent({ import { request } from "/src/api/service";
name: "LoginPage", import * as UserApi from "/src/store/user/api.user";
components: { LanguageToggle, SmsCode, CaptchaInput, OauthFooter },
setup() {
const { t } = useI18n();
const route = useRoute();
const userStore = useUserStore();
const queryBindCode = ref(route.query.bindCode as string | undefined); const { t } = useI18n();
const route = useRoute();
const userStore = useUserStore();
const queryOauthOnly = route.query.oauthOnly as string; const queryBindCode = ref(route.query.bindCode as string | undefined);
const urlLoginType = route.query.loginType as string | undefined; const queryOauthOnly = route.query.oauthOnly as string;
const verifyCodeInputRef = ref(); const urlLoginType = route.query.loginType as string | undefined;
const loading = ref(false); const verifyCodeInputRef = ref();
const loading = ref(false);
const settingStore = useSettingStore(); const settingStore = useSettingStore();
const formRef = ref(); const formRef = ref();
let defaultLoginType = settingStore.sysPublic.defaultLoginType || "password"; let defaultLoginType = settingStore.sysPublic.defaultLoginType || "password";
if (defaultLoginType === "sms") { if (defaultLoginType === "sms") {
if (!settingStore.sysPublic.smsLoginEnabled || !settingStore.isComm) { if (!settingStore.sysPublic.smsLoginEnabled || !settingStore.isComm) {
defaultLoginType = "password"; defaultLoginType = "password";
} }
} }
const formState = reactive({ const formState = reactive({
username: "", username: "",
phoneCode: "86", phoneCode: "86",
mobile: "", mobile: "",
password: "", password: "",
loginType: urlLoginType || defaultLoginType, //password loginType: urlLoginType || defaultLoginType,
smsCode: "", smsCode: "",
captcha: null, captcha: null,
smsCaptcha: null, smsCaptcha: null,
}); });
const rules = { const rules = {
mobile: [ mobile: [
{ {
required: true, required: true,
message: "请输入手机号", message: "请输入手机号",
}, },
], ],
username: [ username: [
{ {
required: true, required: true,
message: "请输入用户名", message: "请输入用户名",
}, },
], ],
password: [ password: [
{ {
required: true, required: true,
message: "请输入登录密码", message: "请输入登录密码",
}, },
], ],
smsCode: [ smsCode: [
{ {
required: true, required: true,
message: "请输入短信验证码", message: "请输入短信验证码",
}, },
], ],
captcha: [ captcha: [
{ {
required: true, required: true,
message: "请进行验证码验证", message: "请进行验证码验证",
}, },
], ],
}; };
const layout = { const layout = {
labelCol: { labelCol: {
span: 0, span: 0,
},
wrapperCol: {
span: 24,
},
};
async function afterLoginSuccess() {
if (queryBindCode.value) {
await oauthApi.BindUser(queryBindCode.value);
notification.success({ message: "绑定第三方账号成功" });
}
}
const twoFactor = reactive({
loginId: "",
verifyCode: "",
});
const handleTwoFactorSubmit = async () => {
await userStore.loginByTwoFactor(twoFactor);
afterLoginSuccess();
};
const handleFinish = async () => {
loading.value = true;
try {
// formState.captcha = await doCaptchaValidate();
// if (!formState.captcha) {
// return;
// }
const loginType = formState.loginType;
await userStore.login(loginType, toRaw(formState));
afterLoginSuccess();
} catch (e: any) {
//@ts-ignore
if (e.code === 10020) {
//
//@ts-ignore
twoFactor.loginId = e.data;
await nextTick();
verifyCodeInputRef.value.focus();
} else {
throw e;
}
} finally {
loading.value = false;
formState.captcha = null;
}
};
const handleFinishFailed = (errors: any) => {
utils.logger.log(errors);
};
const resetForm = () => {
formRef.value.resetFields();
};
const isLoginError = ref();
const sysPublicSettings = settingStore.getSysPublic;
function hasRegisterTypeEnabled() {
return sysPublicSettings.registerEnabled && (sysPublicSettings.usernameRegisterEnabled || sysPublicSettings.emailRegisterEnabled || sysPublicSettings.mobileRegisterEnabled || sysPublicSettings.smsLoginEnabled);
}
const captchaInputRef = ref();
const captchaInputForSmsCode = ref();
const isOauthOnly = computed(() => {
if (queryOauthOnly === "false" || queryOauthOnly === "0") {
return false;
}
return sysPublicSettings.oauthOnly && settingStore.isPlus && sysPublicSettings.oauthEnabled;
});
return {
t,
loading,
formState,
formRef,
rules,
layout,
isOauthOnly,
handleFinishFailed,
handleFinish,
resetForm,
isLoginError,
sysPublicSettings,
hasRegisterTypeEnabled,
twoFactor,
handleTwoFactorSubmit,
verifyCodeInputRef,
settingStore,
captchaInputRef,
captchaInputForSmsCode,
queryBindCode,
};
}, },
wrapperCol: {
span: 24,
},
};
const twoFactor = reactive({
loginId: "",
verifyCode: "",
});
const passkeySupported = ref(false);
const passkeyEnabled = ref(false);
const checkPasskeySupport = () => {
passkeySupported.value = false;
if (typeof window !== "undefined" && "credentials" in navigator && "PublicKeyCredential" in window) {
passkeySupported.value = true;
}
};
const handlePasskeyLogin = async () => {
if (!passkeySupported.value) {
notification.error({ message: t("authentication.passkeyNotSupported") });
return;
}
loading.value = true;
try {
const optionsResponse: any = await request({
url: "/passkey/generateAuthentication",
method: "post",
});
const options = optionsResponse;
const credential = await (navigator.credentials as any).get({
publicKey: {
challenge: Uint8Array.from(atob(options.challenge.replace(/-/g, "+").replace(/_/g, "/")), c => c.charCodeAt(0)),
rpId: options.rpId,
allowCredentials: options.allowCredentials || [],
timeout: options.timeout || 60000,
},
});
if (!credential) {
throw new Error("Passkey认证失败");
}
const loginRes: any = await UserApi.loginByPasskey({
credential,
challenge: options.challenge,
});
await userStore.onLoginSuccess(loginRes);
} catch (e: any) {
console.error("Passkey登录失败:", e);
notification.error({ message: e.message || "Passkey登录失败" });
} finally {
loading.value = false;
}
};
const handleFinish = async () => {
loading.value = true;
try {
const loginType = formState.loginType;
await userStore.login(loginType, toRaw(formState));
if (queryBindCode.value) {
await oauthApi.BindUser(queryBindCode.value);
notification.success({ message: "绑定第三方账号成功" });
}
} catch (e: any) {
if (e.code === 10020) {
twoFactor.loginId = e.data;
await nextTick();
verifyCodeInputRef.value.focus();
} else {
throw e;
}
} finally {
loading.value = false;
formState.captcha = null;
}
};
const handleFinishFailed = (errors: any) => {
utils.logger.log(errors);
};
const handleTwoFactorSubmit = async () => {
await userStore.loginByTwoFactor(twoFactor);
if (queryBindCode.value) {
await oauthApi.BindUser(queryBindCode.value);
notification.success({ message: "绑定第三方账号成功" });
}
};
const sysPublicSettings = settingStore.getSysPublic;
const hasRegisterTypeEnabled = () => {
return sysPublicSettings.registerEnabled && (sysPublicSettings.usernameRegisterEnabled || sysPublicSettings.emailRegisterEnabled || sysPublicSettings.mobileRegisterEnabled || sysPublicSettings.smsLoginEnabled);
};
const isOauthOnly = computed(() => {
if (queryOauthOnly === "false" || queryOauthOnly === "0") {
return false;
}
return sysPublicSettings.oauthOnly && settingStore.isPlus && sysPublicSettings.oauthEnabled;
});
onMounted(() => {
checkPasskeySupport();
}); });
</script> </script>
<style lang="less"> <style lang="less">
.login-page.main { .login-page.main {
//margin: 20px !important;
margin-bottom: 100px; margin-bottom: 100px;
.user-layout-login { .user-layout-login {
//label {
// font-size: 14px;
//}
.fs-icon { .fs-icon {
// color: rgba(0, 0, 0, 0.45);
margin-right: 4px; margin-right: 4px;
} }
@@ -328,7 +343,6 @@ export default defineComponent({
text-align: left; text-align: left;
margin-top: 30px; margin-top: 30px;
margin-bottom: 30px; margin-bottom: 30px;
//line-height: 22px;
.item-icon { .item-icon {
font-size: 24px; font-size: 24px;
@@ -269,7 +269,7 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
title: t("certd.roles"), title: t("certd.roles"),
type: "dict-select", type: "dict-select",
dict: dict({ dict: dict({
url: "/sys/authority/role/list", url: "/basic/user/getSimpleRoles",
value: "id", value: "id",
label: "name", label: "name",
}), // 数据字典 }), // 数据字典
@@ -7,6 +7,7 @@ import * as api from "./api";
import { useSettingStore } from "/@/store/settings"; import { useSettingStore } from "/@/store/settings";
import { useUserStore } from "/@/store/user"; import { useUserStore } from "/@/store/user";
import { useI18n } from "/src/locales"; import { useI18n } from "/src/locales";
import { useProjectStore } from "/@/store/project";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet { export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter(); const router = useRouter();
@@ -29,6 +30,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
}; };
const userStore = useUserStore(); const userStore = useUserStore();
const projectStore = useProjectStore();
const settingStore = useSettingStore(); const settingStore = useSettingStore();
const selectedRowKeys: Ref<any[]> = ref([]); const selectedRowKeys: Ref<any[]> = ref([]);
context.selectedRowKeys = selectedRowKeys; context.selectedRowKeys = selectedRowKeys;
@@ -61,6 +63,12 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
minWidth: 200, minWidth: 200,
fixed: "right", fixed: "right",
}, },
form: {
onSuccess: async () => {
await projectStore.reload();
crudExpose?.doRefresh();
},
},
columns: { columns: {
id: { id: {
title: "ID", title: "ID",
@@ -90,35 +98,21 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
}, },
}, },
}, },
disabled: { isSystem: {
title: t("certd.disabled"), title: t("certd.ent.isSystem"),
type: "dict-switch", type: "dict-switch",
dict: dict({ dict: dict({
data: [ data: [
{ label: t("certd.enabled"), value: false, color: "success" }, { label: t("common.yes"), value: true, color: "success" },
{ label: t("certd.disabledLabel"), value: true, color: "error" }, { label: t("common.no"), value: false, color: "error" },
], ],
}), }),
form: { form: {
value: false, value: true,
helper: t("certd.ent.isSystemHelper"),
}, },
column: { column: {
width: 100, width: 150,
component: {
title: t("certd.clickToToggle"),
on: {
async click({ value, row }) {
Modal.confirm({
title: t("certd.prompt"),
content: t("certd.confirmToggleStatus", { action: !value ? t("certd.disable") : t("certd.enable") }),
onOk: async () => {
await api.SetDisabled(row.id, !value);
await crudExpose.doRefresh();
},
});
},
},
},
}, },
}, },
adminId: { adminId: {
@@ -122,7 +122,7 @@ const editableKeys = ref([
}, },
defaultRender(item: any) { defaultRender(item: any) {
return () => { return () => {
return <pre class={"helper"}>{item["helper"]}</pre>; return <pre class={"helper pre"}>{item["helper"]}</pre>;
}; };
}, },
editRender(item: any) { editRender(item: any) {
@@ -9,6 +9,7 @@
<div class="helper">SaaS模式每个用户管理自己的流水线和授权资源独立使用</div> <div class="helper">SaaS模式每个用户管理自己的流水线和授权资源独立使用</div>
<div class="helper">企业模式通过项目合作管理流水线证书和授权资源所有用户视为企业内部员工</div> <div class="helper">企业模式通过项目合作管理流水线证书和授权资源所有用户视为企业内部员工</div>
<div class="helper text-red-500">建议在开始使用时固定一个合适的模式之后就不要随意切换了</div> <div class="helper text-red-500">建议在开始使用时固定一个合适的模式之后就不要随意切换了</div>
<div v-if="settingsStore.isComm" class="helper text-red-500">商业版不建议设置为企业模式除非你确定要转成企业内部使用</div>
<div><a @click="adminModeIntroOpen = true"> 更多管理模式介绍</a></div> <div><a @click="adminModeIntroOpen = true"> 更多管理模式介绍</a></div>
</a-form-item> </a-form-item>
@@ -14,6 +14,11 @@
<div class="helper">{{ t("certd.httpsProxyHelper") }}</div> <div class="helper">{{ t("certd.httpsProxyHelper") }}</div>
</a-form-item> </a-form-item>
<a-form-item :label="t('certd.sys.setting.environmentVars')" :name="['private', 'environmentVars']">
<a-textarea v-model:value="formState.private.environmentVars" :placeholder="environmentVarsExample" rows="4" />
<div class="helper">{{ t("certd.sys.setting.environmentVarsHelper") }}</div>
</a-form-item>
<a-form-item :label="t('certd.dualStackNetwork')" :name="['private', 'dnsResultOrder']"> <a-form-item :label="t('certd.dualStackNetwork')" :name="['private', 'dnsResultOrder']">
<a-select v-model:value="formState.private.dnsResultOrder"> <a-select v-model:value="formState.private.dnsResultOrder">
<a-select-option value="verbatim">{{ t("certd.default") }}</a-select-option> <a-select-option value="verbatim">{{ t("certd.default") }}</a-select-option>
@@ -55,6 +60,11 @@ defineOptions({
name: "SettingNetwork", name: "SettingNetwork",
}); });
const environmentVarsExample = ref(
`ALIYUN_CLIENT_CONNECT_TIMEOUT=16000 #连接超时,单位毫秒
ALIYUN_CLIENT_READ_TIMEOUT=16000 #读取数据超时单位毫秒`
);
const formState = reactive<Partial<SysSettings>>({ const formState = reactive<Partial<SysSettings>>({
public: {}, public: {},
private: {}, private: {},
@@ -1,6 +1,16 @@
<template> <template>
<div class="sys-settings-form sys-settings-oauth"> <div class="sys-settings-form sys-settings-oauth">
<a-form :model="formState" name="register" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onFinish"> <a-form :model="formState" name="register" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onFinish">
<a-form-item :label="t('certd.sys.setting.enablePasskey')" :name="['public', 'passkeyEnabled']">
<div class="flex-o">
<a-switch v-model:checked="formState.public.passkeyEnabled" :disabled="!settingsStore.isPlus" :title="t('certd.plusFeature')" />
<vip-button class="ml-5" mode="button"></vip-button>
</div>
<pre class="helper pre">{{ t("certd.sys.setting.passkeyEnabledHelper", [bindDomain]) }}</pre>
<div v-if="!bindDomainIsSame" class="text-red-500 text-sm mt-2">
{{ t("certd.sys.setting.passkeyHostnameNotSame") }}
</div>
</a-form-item>
<a-form-item :label="t('certd.sys.setting.enableOauth')" :name="['public', 'oauthEnabled']"> <a-form-item :label="t('certd.sys.setting.enableOauth')" :name="['public', 'oauthEnabled']">
<div class="flex-o"> <div class="flex-o">
<a-switch v-model:checked="formState.public.oauthEnabled" :disabled="!settingsStore.isPlus" :title="t('certd.plusFeature')" /> <a-switch v-model:checked="formState.public.oauthEnabled" :disabled="!settingsStore.isPlus" :title="t('certd.plusFeature')" />
@@ -79,7 +89,7 @@
<script setup lang="tsx"> <script setup lang="tsx">
import { notification } from "ant-design-vue"; import { notification } from "ant-design-vue";
import { merge } from "lodash-es"; import { merge } from "lodash-es";
import { reactive, ref, Ref } from "vue"; import { computed, reactive, ref, Ref } from "vue";
import AddonSelector from "../../../certd/addon/addon-selector/index.vue"; import AddonSelector from "../../../certd/addon/addon-selector/index.vue";
import { useSettingStore } from "/@/store/settings"; import { useSettingStore } from "/@/store/settings";
import * as api from "/@/views/sys/settings/api"; import * as api from "/@/views/sys/settings/api";
@@ -101,6 +111,16 @@ async function loadOauthProviders() {
oauthProviders.value = await api.GetOauthProviders(); oauthProviders.value = await api.GetOauthProviders();
} }
const bindDomain = computed(() => {
const uri = new URL(settingsStore.installInfo.bindUrl);
return uri.hostname;
});
const bindDomainIsSame = computed(() => {
const currentHostname = window.location.hostname;
return bindDomain.value === currentHostname;
});
function fillOauthProviders(form: any) { function fillOauthProviders(form: any) {
const providers: any = {}; const providers: any = {};
for (const item of oauthProviders.value) { for (const item of oauthProviders.value) {
+4 -4
View File
@@ -93,16 +93,16 @@ export default (req: any) => {
// with options // with options
"/api": { "/api": {
//配套后端 https://github.com/fast-crud/fs-server-js //配套后端 https://github.com/fast-crud/fs-server-js
target: "https://127.0.0.1:7002", target: "http://127.0.0.1:7001",
//忽略证书 //忽略证书
agent: new https.Agent({ rejectUnauthorized: false }), // agent: new https.Agent({ rejectUnauthorized: false }),
}, },
"/certd/api": { "/certd/api": {
//配套后端 https://github.com/fast-crud/fs-server-js //配套后端 https://github.com/fast-crud/fs-server-js
target: "https://127.0.0.1:7002/api", target: "http://127.0.0.1:7001/api",
rewrite: path => path.replace(/^\/certd\/api/, ""), rewrite: path => path.replace(/^\/certd\/api/, ""),
//忽略证书 //忽略证书
agent: new https.Agent({ rejectUnauthorized: false }), // agent: new https.Agent({ rejectUnauthorized: false }),
}, },
}, },
}, },
+1 -1
View File
@@ -6,7 +6,7 @@ typeorm:
default: default:
type: mysql # mariadb type: mysql # mariadb
host: localhost host: localhost
port: 3309 port: 3308
username: root username: root
password: root password: root
database: certd database: certd
+27
View File
@@ -3,6 +3,33 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.1](https://github.com/certd/certd/compare/v1.39.0...v1.39.1) (2026-03-09)
### Performance Improvements
* 支持迁移个人数据到企业项目中 ([c6ca832](https://github.com/certd/certd/commit/c6ca83273779ed84de1b23b5e477063af043d015))
# [1.39.0](https://github.com/certd/certd/compare/v1.38.12...v1.39.0) (2026-03-07)
### Bug Fixes
* 修复部署到openwrt错误的bug ([2e3d0cc](https://github.com/certd/certd/commit/2e3d0cc57c16c48ad435bc8fde729bacaedde9f5))
* 修复复制流水线保存后丢失分组和排序号的问题 ([bc32648](https://github.com/certd/certd/commit/bc326489abc1d50a0930b4f47aa2d62d3a486798))
* 修复京东云域名申请证书报错的bug ([d9c0130](https://github.com/certd/certd/commit/d9c0130b59997144a3c274d456635b800135e43f))
* 修复偶尔下载证书报未授权的错误 ([316537e](https://github.com/certd/certd/commit/316537eb4dcbe5ec57784e8bf95ee3cdfd21dce7))
* 修复dcdn多个域名同时部署时 可能会出现证书名称重复的bug ([78c2ced](https://github.com/certd/certd/commit/78c2ced43b1a73d142b0ed783b162b97f545ab06))
* 优化dcdn部署上传多次证书 偶尔报 The CertName already exists的问题 ([72f850f](https://github.com/certd/certd/commit/72f850f675b500d12ebff2338d1b99d6fab476e1))
* **cert-plugin:** 优化又拍云客户端错误处理逻辑,当域名已绑定证书时不再抛出异常。 ([92c9ac3](https://github.com/certd/certd/commit/92c9ac382692e6c84140ff787759ab6d39ccbe96))
* esxi部署失败的bug ([1e44115](https://github.com/certd/certd/commit/1e441154617e6516a9a3610412bf597128c62696))
### Features
* 支持企业级管理模式,项目管理,细分权限 ([3734083](https://github.com/certd/certd/commit/37340838b6a61a94b86bfa13cf5da88b26f1315a))
### Performance Improvements
* 站点监控支持指定ip地址检查 ([83d81b6](https://github.com/certd/certd/commit/83d81b64b3adb375366039e07c87d1ad79121c13))
## [1.38.12](https://github.com/certd/certd/compare/v1.38.11...v1.38.12) (2026-02-18) ## [1.38.12](https://github.com/certd/certd/compare/v1.38.11...v1.38.12) (2026-02-18)
**Note:** Version bump only for package @certd/ui-server **Note:** Version bump only for package @certd/ui-server
@@ -0,0 +1,118 @@
CREATE TABLE `cd_project`
(
`id` bigint PRIMARY KEY AUTO_INCREMENT NOT NULL,
`user_id` bigint NOT NULL,
`name` varchar(512) NOT NULL,
`admin_id` bigint NOT NULL,
`disabled` boolean NOT NULL DEFAULT false,
`is_system` boolean NOT NULL DEFAULT false,
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX `index_project_user_id` ON `cd_project` (`user_id`);
CREATE INDEX `index_project_admin_id` ON `cd_project` (`admin_id`);
INSERT INTO cd_project (id, user_id, `admin_id`, `name`, `disabled`, `is_system`) VALUES (1, -1, 1,'default', false,false);
ALTER TABLE cd_cert_info ADD COLUMN project_id bigint;
CREATE INDEX `index_cert_project_id` ON `cd_cert_info` (`project_id`);
ALTER TABLE cd_site_info ADD COLUMN project_id bigint;
CREATE INDEX `index_site_project_id` ON `cd_site_info` (`project_id`);
ALTER TABLE cd_site_ip ADD COLUMN project_id bigint;
CREATE INDEX `index_site_ip_project_id` ON `cd_site_ip` (`project_id`);
ALTER TABLE cd_open_key ADD COLUMN project_id bigint;
CREATE INDEX `index_open_key_project_id` ON `cd_open_key` (`project_id`);
ALTER TABLE cd_access ADD COLUMN project_id bigint;
CREATE INDEX `index_access_project_id` ON `cd_access` (`project_id`);
ALTER TABLE cd_addon ADD COLUMN project_id bigint;
CREATE INDEX `index_addon_project_id` ON `cd_addon` (`project_id`);
ALTER TABLE pi_pipeline ADD COLUMN project_id bigint;
CREATE INDEX `index_pipeline_project_id` ON `pi_pipeline` (`project_id`);
ALTER TABLE pi_pipeline_group ADD COLUMN project_id bigint;
CREATE INDEX `index_pipeline_group_project_id` ON `pi_pipeline_group` (`project_id`);
ALTER TABLE pi_storage ADD COLUMN project_id bigint;
CREATE INDEX `index_storage_project_id` ON `pi_storage` (`project_id`);
ALTER TABLE pi_notification ADD COLUMN project_id bigint;
CREATE INDEX `index_notification_project_id` ON `pi_notification` (`project_id`);
ALTER TABLE pi_history ADD COLUMN project_id bigint;
CREATE INDEX `index_history_project_id` ON `pi_history` (`project_id`);
ALTER TABLE pi_history_log ADD COLUMN project_id bigint;
CREATE INDEX `index_history_log_project_id` ON `pi_history_log` (`project_id`);
ALTER TABLE pi_template ADD COLUMN project_id bigint;
CREATE INDEX `index_template_project_id` ON `pi_template` (`project_id`);
ALTER TABLE pi_sub_domain ADD COLUMN project_id bigint;
CREATE INDEX `index_sub_domain_project_id` ON `pi_sub_domain` (`project_id`);
ALTER TABLE cd_cname_record ADD COLUMN project_id bigint;
CREATE INDEX `index_cname_record_project_id` ON `cd_cname_record` (`project_id`);
ALTER TABLE cd_domain ADD COLUMN project_id bigint;
CREATE INDEX `index_domain_project_id` ON `cd_domain` (`project_id`);
ALTER TABLE user_settings ADD COLUMN project_id bigint;
CREATE INDEX `index_user_settings_project_id` ON `user_settings` (`project_id`);
ALTER TABLE cd_group ADD COLUMN project_id bigint;
CREATE INDEX `index_group_project_id` ON `cd_group` (`project_id`);
CREATE TABLE `cd_project_member`
(
`id` bigint PRIMARY KEY AUTO_INCREMENT NOT NULL,
`user_id` bigint NOT NULL,
`project_id` bigint NOT NULL,
`permission` varchar(128) NOT NULL DEFAULT 'read',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
);
ALTER TABLE cd_project_member ADD COLUMN status varchar(128);
CREATE INDEX `index_project_member_user_id` ON `cd_project_member` (`user_id`);
CREATE INDEX `index_project_member_project_id` ON `cd_project_member` (`project_id`);
CREATE TABLE `cd_audit_log`
(
`id` bigint PRIMARY KEY AUTO_INCREMENT NOT NULL,
`user_id` bigint NOT NULL,
`username` varchar(128) NOT NULL,
`project_id` bigint NOT NULL,
`project_name` varchar(512) NOT NULL,
`type` varchar(128) NOT NULL,
`action` varchar(128) NOT NULL DEFAULT 'read',
`content` longtext NOT NULL,
`ip_address` varchar(128) NOT NULL,
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX `index_audit_log_user_id` ON `cd_audit_log` (`user_id`);
CREATE INDEX `index_audit_log_project_id` ON `cd_audit_log` (`project_id`);
ALTER TABLE cd_site_info ADD COLUMN ip_address varchar(128);
ALTER TABLE `cd_project` ENGINE = InnoDB;
ALTER TABLE `cd_project_member` ENGINE = InnoDB;
ALTER TABLE `cd_audit_log` ENGINE = InnoDB;

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