Compare commits

...

36 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
176 changed files with 3943 additions and 955 deletions
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
with:
time: '10' # for 60 seconds
- name: deploy-certd-demo
uses: tyrrrz/action-http-request@master
uses: tyrrrz/action-http-request@prime
with:
# 通过webhook 触发 certd-demo来部署
url: ${{ secrets.WEBHOOK_CERTD_DEMO }}
+1 -1
View File
@@ -118,7 +118,7 @@ jobs:
# greper/certd-agent:latest
# greper/certd-agent:${{steps.get_certd_version.outputs.result}}
- name: deploy-certd-doc
uses: tyrrrz/action-http-request@master
uses: tyrrrz/action-http-request@prime
with:
url: ${{ secrets.WEBHOOK_CERTD_DOC }}
method: POST
+15 -1
View File
@@ -65,6 +65,20 @@ demoKeyId = '';
encrypt: true, //该属性是否需要加密
})
demoKeySecret = '';
@AccessInput({
title: '另外一个授权Id',//标题
component: {
name:"access-selector", //access选择组件
vModel:"modelValue",
type: "ssh", // access类型,让用户固定选择这种类型的access
},
required: true, //text组件可以省略
})
otherAccessId;
```
### 4. 实现测试方法
@@ -93,7 +107,7 @@ async onTestRequest() {
```typescript
/**
* api接口示例 取域名列表,
* api接口示例 取域名列表,
*/
async GetDomainList(req: PageSearch): Promise<PageRes<DomainRecord>> {
//输出日志必须使用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授权',
component: {
name: 'access-selector',
type: 'demo', // 固定授权类型
vModel:"modelValue",
type: "demo", // access类型,让用户固定选择这种类型的access
},
// rules: [{ required: true, message: '此项必填' }],
// required: true, // 必填
+11
View File
@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
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
+39
View File
@@ -3,6 +3,45 @@
All notable changes to this project will be documented in this file.
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)
### Bug Fixes
+7 -2
View File
@@ -16,7 +16,6 @@ https://1panel.cn/docs/installation/online_installation/
![](./images/store-1.png)
![](./images/store-2.png)
#### 1.2 访问测试:
@@ -40,6 +39,9 @@ admin/123456
1. 打开`docker-compose.yaml`,整个内容复制下来
https://gitee.com/certd/certd/raw/v2/docker/run/docker-compose.yaml
::: tip
默认使用SQLite数据库,如果需要使用MySQL、PostgreSQL数据库,请参考[多数据库支持](./install/database.md)
:::
2. 然后到 `1Panel->容器->编排->新建编排`
输入名称,粘贴`docker-compose.yaml`原文内容
@@ -49,7 +51,10 @@ admin/123456
![](./images/2.png)
> 默认使用sqlite数据库,数据保存在`/data/certd`目录下,您可以手动备份该目录
> certd还支持`mysql`和`postgresql`数据库,[点我了解如何切换其他数据库](../database)
#### 2.2 访问测试
+3 -1
View File
@@ -30,7 +30,9 @@
点击确定,等待启动完成
![](./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
> https://docs.docker.com/compose/install/linux/
> certd默认使用sqlite数据库,另外还支持`mysql`和`postgresql`数据库,[点我了解如何切换其他数据库](../database)
::: tip
默认安装使用SQLite数据库,如果需要使用MySQL、PostgreSQL数据库,请参考[多数据库支持](./install/database.md)
:::
### 3. 访问测试
+5
View File
@@ -28,6 +28,11 @@ https://certd.handsfree.work/
2. [Docker方式部署](./install/docker/)
3. [源码方式部署](./install/source/)
::: tip
默认安装使用SQLite数据库,如果需要使用MySQL、PostgreSQL数据库,请参考[多数据库支持](./install/database.md)
:::
### 2. 访问测试
+1 -1
View File
@@ -9,5 +9,5 @@
}
},
"npmClient": "pnpm",
"version": "1.39.0"
"version": "1.39.1"
}
+1
View File
@@ -37,6 +37,7 @@
"docs:preview": "vitepress preview docs",
"pub": "echo 1",
"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",
"publish_to_atomgit": "node --experimental-json-modules ./scripts/publish-atomgit.js",
"publish_to_gitee": "node --experimental-json-modules ./scripts/publish-gitee.js",
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
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
+3 -3
View File
@@ -3,7 +3,7 @@
"description": "Simple and unopinionated ACME client",
"private": false,
"author": "nmorsman",
"version": "1.39.0",
"version": "1.39.1",
"type": "module",
"module": "scr/index.js",
"main": "src/index.js",
@@ -18,7 +18,7 @@
"types"
],
"dependencies": {
"@certd/basic": "^1.39.0",
"@certd/basic": "^1.39.1",
"@peculiar/x509": "^1.11.0",
"asn1js": "^3.0.5",
"axios": "^1.9.0",
@@ -70,5 +70,5 @@
"bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues"
},
"gitHead": "49457505cdf8156fd9d936b8e9ace0b48e43a6b2"
"gitHead": "590ff67fcb40ff8ba0f7b2a9592b51d9fb54a2ef"
}
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
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
+1 -1
View File
@@ -1 +1 @@
01:15
23:44
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/basic",
"private": false,
"version": "1.39.0",
"version": "1.39.1",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -47,5 +47,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "49457505cdf8156fd9d936b8e9ace0b48e43a6b2"
"gitHead": "590ff67fcb40ff8ba0f7b2a9592b51d9fb54a2ef"
}
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
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
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/pipeline",
"private": false,
"version": "1.39.0",
"version": "1.39.1",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -18,8 +18,8 @@
"compile": "tsc --skipLibCheck --watch"
},
"dependencies": {
"@certd/basic": "^1.39.0",
"@certd/plus-core": "^1.39.0",
"@certd/basic": "^1.39.1",
"@certd/plus-core": "^1.39.1",
"dayjs": "^1.11.7",
"lodash-es": "^4.17.21",
"reflect-metadata": "^0.1.13"
@@ -45,5 +45,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "49457505cdf8156fd9d936b8e9ace0b48e43a6b2"
"gitHead": "590ff67fcb40ff8ba0f7b2a9592b51d9fb54a2ef"
}
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
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
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-huawei",
"private": false,
"version": "1.39.0",
"version": "1.39.1",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
"types": "./dist/d/index.d.ts",
@@ -24,5 +24,5 @@
"prettier": "^2.8.8",
"tslib": "^2.8.1"
},
"gitHead": "49457505cdf8156fd9d936b8e9ace0b48e43a6b2"
"gitHead": "590ff67fcb40ff8ba0f7b2a9592b51d9fb54a2ef"
}
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
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
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-iframe",
"private": false,
"version": "1.39.0",
"version": "1.39.1",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -31,5 +31,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "49457505cdf8156fd9d936b8e9ace0b48e43a6b2"
"gitHead": "590ff67fcb40ff8ba0f7b2a9592b51d9fb54a2ef"
}
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
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
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/jdcloud",
"version": "1.39.0",
"version": "1.39.1",
"description": "jdcloud openApi sdk",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
@@ -56,5 +56,5 @@
"fetch"
]
},
"gitHead": "49457505cdf8156fd9d936b8e9ace0b48e43a6b2"
"gitHead": "590ff67fcb40ff8ba0f7b2a9592b51d9fb54a2ef"
}
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
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
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-k8s",
"private": false,
"version": "1.39.0",
"version": "1.39.1",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -17,7 +17,7 @@
"pub": "npm publish"
},
"dependencies": {
"@certd/basic": "^1.39.0",
"@certd/basic": "^1.39.1",
"@kubernetes/client-node": "0.21.0"
},
"devDependencies": {
@@ -32,5 +32,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "49457505cdf8156fd9d936b8e9ace0b48e43a6b2"
"gitHead": "590ff67fcb40ff8ba0f7b2a9592b51d9fb54a2ef"
}
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
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
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/lib-server",
"version": "1.39.0",
"version": "1.39.1",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -28,11 +28,11 @@
],
"license": "AGPL",
"dependencies": {
"@certd/acme-client": "^1.39.0",
"@certd/basic": "^1.39.0",
"@certd/pipeline": "^1.39.0",
"@certd/plugin-lib": "^1.39.0",
"@certd/plus-core": "^1.39.0",
"@certd/acme-client": "^1.39.1",
"@certd/basic": "^1.39.1",
"@certd/pipeline": "^1.39.1",
"@certd/plugin-lib": "^1.39.1",
"@certd/plus-core": "^1.39.1",
"@midwayjs/cache": "3.14.0",
"@midwayjs/core": "3.20.11",
"@midwayjs/i18n": "3.20.13",
@@ -64,5 +64,5 @@
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "49457505cdf8156fd9d936b8e9ace0b48e43a6b2"
"gitHead": "590ff67fcb40ff8ba0f7b2a9592b51d9fb54a2ef"
}
@@ -115,11 +115,17 @@ export abstract class BaseController {
if (projectId) {
await authService.checkProjectId(service, id, projectId);
}else{
if(allowAdmin){
await authService.checkUserIdButAllowAdmin(this.ctx, service, id);
if(userId === Constants.systemUserId){
//系统级别,不检查权限
}else{
await authService.checkUserId( service, id, userId);
if(allowAdmin){
await authService.checkUserIdButAllowAdmin(this.ctx, service, id);
}else{
await authService.checkUserId( service, id, userId);
}
}
}
return {projectId,userId}
}
@@ -4,6 +4,7 @@ import { Inject } from '@midwayjs/core';
import { TypeORMDataSourceManager } from '@midwayjs/typeorm';
import { EntityManager } from 'typeorm/entity-manager/EntityManager.js';
import { FindManyOptions } from 'typeorm';
import { Constants } from './constants.js';
export type PageReq<T = any> = {
page?: { offset: number; limit: number };
@@ -258,7 +259,7 @@ export abstract class BaseService<T> {
export function checkUserProjectParam(userId: number, projectId: number) {
if (projectId != null ){
if( userId !==-1) {
if( userId !== Constants.enterpriseUserId) {
throw new ValidateException('userId projectId 错误');
}
return true
@@ -120,4 +120,6 @@ export const Constants = {
message: '用户邮箱还未配置',
},
},
systemUserId: 0, // 系统级别userid固定为0
enterpriseUserId: -1 // 企业模式用户id固定为-1
};
@@ -88,6 +88,10 @@ export class SysPrivateSettings extends BaseSettings {
pipelineMaxRunningCount?: number;
environmentVars?: string = '';
sms?: {
type?: string;
config?: any;
@@ -11,6 +11,8 @@ import { BaseService, setAdminMode } from '../../../basic/index.js';
import { executorQueue } from '../../basic/service/executor-queue.js';
import { isComm } from '@certd/plus-core';
const { merge } = mergeUtils;
let lastSaveEnvVars = {};
/**
*
*/
@@ -117,12 +119,12 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
}
async savePublicSettings(bean: SysPublicSettings) {
if(isComm()){
if(bean.adminMode === 'enterprise'){
if (isComm()) {
if (bean.adminMode === 'enterprise') {
throw new Error("商业版不支持使用企业管理模式")
}
}
await this.saveSetting(bean);
//让设置生效
await this.reloadPublicSettings();
@@ -173,6 +175,44 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
}
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) {
@@ -7,8 +7,12 @@ import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
export class AccessEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'key_id', comment: 'key_id', length: 100 })
keyId: string;
@Column({ name: 'user_id', comment: '用户id' })
userId: number; // 0为系统级别, -1为企业,大于1为用户
@Column({ comment: '名称', length: 100 })
name: string;
@@ -5,6 +5,7 @@ import {AccessGetter, BaseService, PageReq, PermissionException, ValidateExcepti
import {AccessEntity} from '../entity/access.js';
import {AccessDefine, accessRegistry, newAccess} from '@certd/pipeline';
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
this.encryptSetting(param, oldEntity);
param.keyId = "ac_" + utils.id.simpleNanoId();
return await super.add(param);
}
@@ -117,6 +119,7 @@ export class AccessService extends BaseService<AccessEntity> {
throw new ValidateException('该授权配置不存在,请确认是否已被删除');
}
this.encryptSetting(param, oldEntity);
delete param.keyId
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 {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'key_id', comment: 'key_id', length: 100 })
keyId: string;
@Column({ name: 'user_id', comment: '用户id' })
userId: number;
@Column({ comment: '名称', length: 100 })
@@ -4,6 +4,7 @@ import { In, Repository } from "typeorm";
import { AddonDefine, BaseService, PageReq, ValidateException } from "../../../index.js";
import { addonRegistry } from "../api/index.js";
import { AddonEntity } from "../entity/addon.js";
import { utils } from "@certd/basic";
/**
* Addon
@@ -43,6 +44,7 @@ export class AddonService extends BaseService<AddonEntity> {
} else {
param.isSystem = false;
}
param.keyId = "ad_" + utils.id.simpleNanoId();
delete param._copyFrom;
return await super.add(param);
}
@@ -57,6 +59,7 @@ export class AddonService extends BaseService<AddonEntity> {
if (oldEntity == null) {
throw new ValidateException("该Addon配置不存在,请确认是否已被删除");
}
delete param.keyId
return await super.update(param);
}
@@ -67,6 +70,7 @@ export class AddonService extends BaseService<AddonEntity> {
}
return {
id: entity.id,
keyId: entity.keyId,
name: entity.name,
userId: entity.userId,
addonType: entity.addonType,
@@ -100,6 +104,7 @@ export class AddonService extends BaseService<AddonEntity> {
},
select: {
id: true,
keyId: true,
name: true,
addonType: true,
type: true,
@@ -132,6 +137,7 @@ export class AddonService extends BaseService<AddonEntity> {
const setting = JSON.parse(res.setting);
return {
id: res.id,
keyId: res.keyId,
addonType: res.addonType,
type: res.type,
name: res.name,
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
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
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/midway-flyway-js",
"version": "1.39.0",
"version": "1.39.1",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -46,5 +46,5 @@
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "49457505cdf8156fd9d936b8e9ace0b48e43a6b2"
"gitHead": "590ff67fcb40ff8ba0f7b2a9592b51d9fb54a2ef"
}
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
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
+6 -6
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-cert",
"private": false,
"version": "1.39.0",
"version": "1.39.1",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -17,10 +17,10 @@
"compile": "tsc --skipLibCheck --watch"
},
"dependencies": {
"@certd/acme-client": "^1.39.0",
"@certd/basic": "^1.39.0",
"@certd/pipeline": "^1.39.0",
"@certd/plugin-lib": "^1.39.0",
"@certd/acme-client": "^1.39.1",
"@certd/basic": "^1.39.1",
"@certd/pipeline": "^1.39.1",
"@certd/plugin-lib": "^1.39.1",
"psl": "^1.9.0",
"punycode.js": "^2.3.1"
},
@@ -38,5 +38,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "49457505cdf8156fd9d936b8e9ace0b48e43a6b2"
"gitHead": "590ff67fcb40ff8ba0f7b2a9592b51d9fb54a2ef"
}
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
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
+6 -6
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-lib",
"private": false,
"version": "1.39.0",
"version": "1.39.1",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -22,10 +22,10 @@
"@alicloud/pop-core": "^1.7.10",
"@alicloud/tea-util": "^1.4.11",
"@aws-sdk/client-s3": "^3.964.0",
"@certd/acme-client": "^1.39.0",
"@certd/basic": "^1.39.0",
"@certd/pipeline": "^1.39.0",
"@certd/plus-core": "^1.39.0",
"@certd/acme-client": "^1.39.1",
"@certd/basic": "^1.39.1",
"@certd/pipeline": "^1.39.1",
"@certd/plus-core": "^1.39.1",
"@kubernetes/client-node": "0.21.0",
"ali-oss": "^6.22.0",
"basic-ftp": "^5.0.5",
@@ -57,5 +57,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "49457505cdf8156fd9d936b8e9ace0b48e43a6b2"
"gitHead": "590ff67fcb40ff8ba0f7b2a9592b51d9fb54a2ef"
}
@@ -2,6 +2,7 @@ import fs from "fs";
import os from "os";
import path from "path";
import { CertificateInfo, crypto } from "@certd/acme-client";
import cryptoLib from "crypto";
import { ILogger } from "@certd/basic";
import dayjs from "dayjs";
import { uniq } from "lodash-es";
@@ -119,9 +120,28 @@ export class CertReader {
const detail = crypto.readCertificateInfo(crt.toString());
const effective = detail.notBefore;
const expires = detail.notAfter;
const fingerprints = CertReader.getFingerprintX509(crt);
// @ts-ignore
detail.fingerprints = fingerprints;
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() {
const { detail } = this.getCrtDetail();
const domains = [];
@@ -1,5 +1,5 @@
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 & {
accessType: string;
@@ -26,6 +26,7 @@ export type DnsProviderContext = {
utils: typeof utils;
domainParser: IDomainParser;
serviceGetter: IServiceGetter;
accessGetter?: IAccessService;
};
export type DomainRecord = {
@@ -1,5 +1,5 @@
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 { CreateRecordOptions, DnsProviderContext, DnsProviderDefine, DomainRecord, IDnsProvider, RemoveRecordOptions } from "./api.js";
import { dnsProviderRegistry } from "./registry.js";
@@ -59,6 +59,11 @@ export async function createDnsProvider(opts: { dnsProviderType: string; context
if (dnsProviderDefine.deprecated) {
context.logger.warn(dnsProviderDefine.deprecated);
}
if (!context.accessGetter) {
const accessGetter: IAccessService = await context.serviceGetter.get("accessService");
context.accessGetter = accessGetter;
}
// @ts-ignore
const dnsProvider: IDnsProvider = new DnsProviderClass();
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_DOWNLOAD_DIR=/app/tools/lego
ENV ALIYUN_CLIENT_CONNECT_TIMEOUT=10000
ENV ALIYUN_CLIENT_READ_TIMEOUT=20000
RUN mkdir -p $LEGO_DOWNLOAD_DIR
# 根据架构下载不同的文件
+10
View File
@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
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
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/ui-client",
"version": "1.39.0",
"version": "1.39.1",
"private": true,
"scripts": {
"dev": "vite --open",
@@ -106,8 +106,8 @@
"zod-defaults": "^0.1.3"
},
"devDependencies": {
"@certd/lib-iframe": "^1.39.0",
"@certd/pipeline": "^1.39.0",
"@certd/lib-iframe": "^1.39.1",
"@certd/pipeline": "^1.39.1",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12",
@@ -1,7 +1,7 @@
<template>
<div class="remote-select">
<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 v-if="search">
<div class="flex w-full" style="padding: 4px 8px">
@@ -61,6 +61,7 @@ const props = defineProps<
watches?: string[];
search?: boolean;
pager?: boolean;
multi?: boolean;
} & ComponentPropsType
>();
@@ -68,6 +69,15 @@ const emit = defineEmits<{
"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 getCurrentPluginDefine: any = inject("getCurrentPluginDefine", () => {
@@ -80,6 +90,7 @@ const getPluginType: any = inject("get:plugin:type", () => {
return "plugin";
});
debugger;
const searchKeyRef = ref("");
const optionsRef = ref([]);
const message = ref("");
@@ -90,7 +90,7 @@
<div class="mt-10">
<div class="w-100 flex-col md:flex-row">
<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>
</div>
</div>
@@ -68,6 +68,14 @@ export default {
smsTab: "Login via SMS code",
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",
weakPasswordWarning: "For your account security, please change your password immediately",
changeNow: "Change Now",
@@ -90,4 +98,9 @@ export default {
updateProfile: "Update Profile",
oauthLoginTitle: "Other ways of 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",
};
@@ -727,7 +727,7 @@ export default {
paymentSetting: "Payment Settings",
captchaSetting: "Captcha Setting",
pipelineSetting: "Pipeline Settings",
oauthSetting: "OAuth2 Settings",
oauthSetting: "Login Settings",
networkSetting: "Network 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",
@@ -769,6 +769,10 @@ export default {
oauthAutoRedirectHelper: "Whether to auto redirect to OAuth2 login when login (using the first enabled OAuth2 login type)",
oauthOnly: "OAuth2 Login Only",
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: {
templates: "Email Templates",
@@ -789,6 +793,8 @@ export default {
reverseProxyHelper: "Reverse proxy for ACME address, used when applying for certificate",
reverseProxyPlaceholder: "http://le.px.handfree.work",
reverseProxyEmpty: "No reverse proxy list configured",
environmentVars: "Environment Variables",
environmentVarsHelper: "configure the runtime environment variables, one per line, format: KEY=VALUE",
},
},
modal: {
@@ -68,6 +68,14 @@ export default {
smsTab: "手机号登录/注册",
passwordTab: "密码登录",
passkeyTab: "Passkey登录",
passkeyLogin: "Passkey登录",
passkeyHelper: "使用您的生物识别或安全密钥登录",
passkeyNotSupported: "您的浏览器不支持Passkey",
passkeyRegister: "注册Passkey",
passkeyRegistered: "Passkey已注册",
passkeyRegisterSuccess: "Passkey注册成功",
passkeyRegisterFailed: "Passkey注册失败",
title: "修改密码",
weakPasswordWarning: "为了您的账户安全,请立即修改密码",
@@ -88,8 +96,13 @@ export default {
nickName: "昵称",
phoneNumber: "手机号",
changePassword: "修改密码",
updateProfile: "修改个人信息",
updateProfile: "修改信息",
oauthLoginTitle: "其他登录方式",
oauthOnlyLoginTitle: "登录",
registerPasskey: "注册Passkey",
deviceName: "设备名称",
deviceNameHelper: "请输入当前设备名称,绑定多个时好做区分",
passkeyRegisterHelper: "1、站点域名变更会导致passkey失效;\n2、同一设备同一个用户绑定多次只有最后一次的有效,之前绑定的会失效,需要手动删除",
userInfo: "账号信息",
};
@@ -780,7 +780,9 @@ export default {
oauthAutoRedirectHelper: "是否自动跳转第三方登录(使用第一个已启用的第三方登录类型)",
oauthOnly: "仅使用第三方登录",
oauthOnlyHelper: "是否仅使用第三方登录,关闭密码登录(注意:请务必在测试第三方登录功能正常后再开启,否则会导致无法登录)\n 如果无法登录,请访问 http://你的certd地址/#/login?oauthOnly=false 来临时关闭此模式",
enablePasskey: "启用Passkey登录",
passkeyHostnameNotSame: "当前域名与主绑定域名不同",
passkeyEnabledHelper: "1、站点必须启用https \n2、passkey的rpId以主绑定域名为准,当前主域名:{0} \n3、站点域名不能变,否则会导致已注册的passkey失效。",
email: {
templates: "邮件模板",
templateType: "模板类型",
@@ -800,6 +802,8 @@ export default {
reverseProxyHelper: "证书颁发机构ACME地址的反向代理,在申请证书时自动使用",
reverseProxyPlaceholder: "http://le.px.handfree.work",
reverseProxyEmpty: "未配置反向代理",
environmentVars: "环境变量",
environmentVarsHelper: "配置运行时环境变量,每行一个,格式:KEY=VALUE",
},
},
modal: {
@@ -127,6 +127,21 @@ export const certdResources = [
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",
name: "MineSetting",
@@ -260,22 +275,7 @@ export const certdResources = [
meta: {
icon: "ion:person-outline",
auth: true,
isMenu: false,
},
},
{
title: "certd.sysResources.projectMemberManager",
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,
},
},
],
@@ -15,11 +15,18 @@ export type ProjectItem = {
export const useProjectStore = defineStore("app.project", () => {
const myProjects = ref([]);
const inited = ref(false);
const currentProjectId = ref(); // 直接调用
function $reset() {
myProjects.value = [];
currentProjectId.value = "";
inited.value = false;
}
const userStore = useUserStore();
const userId = userStore.getUserInfo?.id;
const lastProjectIdCacheKey = "currentProjectId:" + userId;
const lastProjectId = LocalStorage.get(lastProjectIdCacheKey);
const currentProjectId = ref(lastProjectId); // 直接调用
currentProjectId.value = lastProjectId;
const projects = computed(() => {
return myProjects.value;
@@ -118,11 +125,6 @@ export const useProjectStore = defineStore("app.project", () => {
return false;
}
function $reset() {
myProjects.value = [];
currentProjectId.value = "";
}
return {
projects,
myProjects,
@@ -38,6 +38,7 @@ export type SysPublicSetting = {
passwordLoginEnabled?: boolean;
smsLoginEnabled?: boolean;
defaultLoginType?: string;
passkeyEnabled?: boolean;
selfServicePasswordRetrievalEnabled?: boolean;
limitUserPipelineCount?: number;
@@ -101,6 +102,8 @@ export type SysPrivateSetting = {
commonCnameEnabled?: boolean;
// 同一个用户同时最大运行流水线数量
pipelineMaxRunningCount?: number;
// 环境变量
environmentVars?: string;
sms?: {
type?: string;
@@ -107,3 +107,41 @@ export async function OauthProviders() {
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);
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> {
const userInfo = await UserApi.mine();
this.setUserInfo(userInfo);
+12 -7
View File
@@ -21,8 +21,10 @@ div#app {
height: 100%;
}
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{
&.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,
@@ -110,10 +112,10 @@ h6 {
flex: 0;
}
.flex-col {
display: flex;
flex-direction: column;
}
// .flex-col {
// display: flex;
// flex-direction: column;
// }
.align-left {
text-align: left;
@@ -295,10 +297,13 @@ h6 {
}
.helper {
color: #aeaeae;
color: #8f8f8f;
font-size: 12px;
margin-top: 3px;
margin-bottom: 3px;
white-space: pre-wrap;
word-wrap: break-word;
word-break: break-word;
&.error {
color: #ff4d4f;
+14 -8
View File
@@ -1,4 +1,5 @@
import { useFormWrapper } from "@fast-crud/fast-crud";
import { merge } from "lodash-es";
export type FormOptionReq = {
title: string;
@@ -7,6 +8,7 @@ export type FormOptionReq = {
body?: any;
initialForm?: any;
zIndex?: number;
wrapper?: any;
};
export function useFormDialog() {
@@ -14,19 +16,23 @@ export function useFormDialog() {
async function openFormDialog(req: FormOptionReq) {
function createCrudOptions() {
const warpper = merge(
{
zIndex: req.zIndex,
title: req.title,
saveRemind: false,
slots: {
"form-body-top": req.body,
},
},
req.wrapper
);
return {
crudOptions: {
columns: req.columns,
form: {
initialForm: req.initialForm,
wrapper: {
zIndex: req.zIndex,
title: req.title,
saveRemind: false,
slots: {
"form-body-top": req.body,
},
},
wrapper: warpper,
async afterSubmit() {},
async doSubmit({ form }: any) {
if (req.onSubmit) {
@@ -1,5 +1,5 @@
import * as _ from "lodash-es";
import { asyncCompute, compute } from "@fast-crud/fast-crud";
import { merge } from "lodash-es";
import { computed } from "vue";
export type MergeScriptContext = {
@@ -18,7 +18,7 @@ export function useReference(formItem: any) {
const script = formItem.mergeScript;
const func = new Function("ctx", script);
const merged = func(ctx);
_.merge(formItem, merged);
merge(formItem, merged);
delete formItem.mergeScript;
}
@@ -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,
};
}
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>
<div class="title">{{ t("certd.myInfo") }}</div>
</template>
<div class="p-10">
<a-descriptions title="" bordered :column="2">
<a-descriptions-item :label="t('authentication.username')">{{ userInfo.username }}</a-descriptions-item>
<a-descriptions-item :label="t('authentication.nickName')">{{ userInfo.nickName }}</a-descriptions-item>
<a-descriptions-item :label="t('authentication.avatar')">
<a-avatar v-if="userInfo.avatar" size="large" :src="userAvatar" style="background-color: #eee"> </a-avatar>
<a-avatar v-else size="large" style="background-color: #00b4f5">
{{ userInfo.username }}
</a-avatar>
</a-descriptions-item>
<a-descriptions-item :label="t('authentication.email')">{{ userInfo.email }}</a-descriptions-item>
<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 class="profile-container md:p-8">
<div class="profile-card md:rounded">
<div class="card-header">
<div class="header-bg-gradient"></div>
<div class="header-content">
<div class="avatar-wrapper">
<a-avatar v-if="userInfo.avatar" :size="100" :src="userAvatar" class="user-avatar"> </a-avatar>
<a-avatar v-else size="100" class="user-avatar default-avatar">
{{ userInfo.username }}
</a-avatar>
<!-- <div class="status-indicator"></div> -->
</div>
</template>
</a-descriptions-item>
<a-descriptions-item :label="t('common.handle')">
<a-button type="primary" @click="doUpdate">{{ t("authentication.updateProfile") }}</a-button>
<change-password-button class="ml-10" :show-button="true"> </change-password-button>
</a-descriptions-item>
</a-descriptions>
<div class="user-info">
<h2 class="user-name flex items-center">
{{ userInfo.nickName }}
<fs-values-format :model-value="userInfo.roleIds" :dict="roleDict" color="blue" />
</h2>
<div class="user-details">
<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>
</fs-page>
</template>
@@ -40,9 +136,12 @@ import { computed, onMounted, Ref, ref } from "vue";
import ChangePasswordButton from "/@/views/certd/mine/change-password-button.vue";
import { useI18n } from "/src/locales";
import { useUserProfile } from "./use";
import { Modal } from "ant-design-vue";
import { usePasskeyRegister } from "./use";
import { message, Modal, notification } from "ant-design-vue";
import { useSettingStore } from "/@/store/settings";
import { isEmpty } from "lodash-es";
import { dict } from "@fast-crud/fast-crud";
import dayjs from "dayjs";
const { t } = useI18n();
@@ -53,11 +152,20 @@ defineOptions({
const settingStore = useSettingStore();
const userInfo: Ref = ref({});
const passkeys = ref([]);
const passkeySupported = ref(false);
const getUserInfo = async () => {
userInfo.value = await api.getMineInfo();
};
const roleDict = dict({
url: "/basic/user/getSimpleRoles",
value: "id",
label: "name",
});
const { openEditProfileDialog } = useUserProfile();
const { openRegisterDialog } = usePasskeyRegister();
function doUpdate() {
openEditProfileDialog({
@@ -69,10 +177,12 @@ function doUpdate() {
const oauthBounds = ref([]);
const oauthProviders = ref([]);
async function loadOauthBounds() {
const res = await api.GetOauthBounds();
oauthBounds.value = res;
}
async function loadOauthProviders() {
const res = await api.GetOauthProviders();
oauthProviders.value = res;
@@ -102,12 +212,124 @@ async function unbind(type: string) {
}
async function bind(type: string) {
//URL
const res = await api.OauthBoundUrl(type);
const loginUrl = res.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(() => {
if (isEmpty(userInfo.value.avatar)) {
return "";
@@ -123,5 +345,514 @@ onMounted(async () => {
await getUserInfo();
await loadOauthBounds();
await loadOauthProviders();
await loadPasskeys();
checkPasskeySupport();
});
</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>
@@ -50,4 +50,12 @@ export const openkeyApi = {
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: {
width: 580,
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: {
@@ -26,7 +26,8 @@ const type = route.params.type as string;
const query = route.query;
async function checkNotify() {
const res = await api.Notify(type, query);
// const res = await api.Notify(type, query);
const res = "success";
if (res === "success") {
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> {
return await request({
url: apiPrefix + "/batchDelete",
@@ -146,6 +154,7 @@ export type CertInfo = {
ic: string;
der: string;
pfx: string;
detail: any;
};
export async function GetCert(pipelineId: number): Promise<CertInfo> {
@@ -1,12 +1,47 @@
<template>
<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 }">
<a-list-item key="item.key">
<a-list-item-meta>
<template #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 }">
<a-tag color="success">复制</a-tag>
</fs-copyable>
@@ -34,10 +69,25 @@ const certFiles = ref([
{ name: "私钥", fileName: "private.pem", key: "key", content: props.cert.key },
{ 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>
<style lang="less">
.cert-view {
margin-right: 25px;
.title {
display: flex;
justify-content: space-between;
@@ -46,5 +96,29 @@ const certFiles = ref([
margin-block-end: 0px !important;
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>
@@ -6,7 +6,7 @@ import { useRouter } from "vue-router";
import { compute, CreateCrudOptionsRet, dict, useFormWrapper } from "@fast-crud/fast-crud";
import NotificationSelector from "/@/views/certd/notification/notification-selector/index.vue";
import { useReference } from "/@/use/use-refrence";
import { computed, ref } from "vue";
import { computed, provide, Ref, ref } from "vue";
import * as api from "../api";
import { PluginGroup, usePluginStore } from "/@/store/plugin";
import { createNotificationApi } from "/@/views/certd/notification/api";
@@ -89,9 +89,10 @@ export function useCertPipelineCreator() {
const inputs: any = {};
const moreParams = [];
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;
const inputDefine = cloneDeep(req.certPlugin.input[inputKey]);
const inputDefine = cloneDeep(certPlugin.input[inputKey]);
if (inputDefine.maybeNeed) {
moreParams.push(inputKey);
}
@@ -103,7 +104,6 @@ export function useCertPipelineCreator() {
},
};
}
const pluginStore = usePluginStore();
const randomHour = Math.floor(Math.random() * 6);
const randomMin = Math.floor(Math.random() * 60);
@@ -322,7 +322,7 @@ export function useCertPipelineCreator() {
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();
@@ -393,6 +393,8 @@ export function useCertPipelineCreator() {
message.error("该证书申请插件不存在");
return;
}
req.currentPluginRef.value = certPlugin;
const { crudOptions } = createCrudOptions({
certPlugin,
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>
@@ -42,6 +42,7 @@
<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-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>
<template #form-bottom>
@@ -52,11 +53,13 @@
</template>
<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 createCrudOptions from "./crud";
import ChangeGroup from "./components/change-group.vue";
import ChangeTrigger from "./components/change-trigger.vue";
import ChangeProject from "./components/change-project.vue";
import BatchRerun from "./components/batch-rerun.vue";
import { Modal, notification } from "ant-design-vue";
import * as api from "./api";
@@ -69,6 +72,7 @@ import { groupDictRef } from "./group/dicts";
import { useCertPipelineCreator } from "./certd-form/use";
import { useRouter } from "vue-router";
import { useCrudPermission } from "/@/plugin/permission";
import CertdForm from "./certd-form/certd-form.vue";
defineOptions({
name: "PipelineManager",
@@ -79,11 +83,17 @@ const context: any = {
selectedRowKeys,
};
const router = useRouter();
const { openAddCertdPipelineDialog } = useCertPipelineCreator();
function onActionbarMoreItemClick(req: { key: string; item: any }) {
openCertApplyDialog({ key: req.key, title: req.item?.title });
}
const certdFormRef = ref<typeof CertdForm>();
const currentPluginRef = ref();
provide("getCurrentPluginDefine", () => {
return currentPluginRef.value;
});
const addMorePipelineBtns = computed(() => {
return [
{ 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" },
];
});
const { openAddCertdPipelineDialog } = useCertPipelineCreator();
function openCertApplyDialog(req: { key: string; title: string }) {
if (req.key === "AddPipeline") {
crudExpose.openAdd({});
@@ -104,7 +115,7 @@ function openCertApplyDialog(req: { key: string; title: string }) {
const searchForm = crudExpose.getSearchValidatedFormData();
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.permission = { isProjectPermission: true };
@@ -294,13 +294,9 @@ function useStepForm() {
currentStep.value.input[key] = pluginSysConfig.sysSetting?.input[key];
}
}
console.log("currentStepTypeChanged:", currentStep.value);
console.log("currentStepPlugin:", currentPlugin.value);
};
const stepSave = async (e: any) => {
console.log("currentStepSave", currentStep.value);
try {
await stepFormRef.value.validate();
} catch (e) {
@@ -72,14 +72,13 @@ import * as _ from "lodash-es";
import { nanoid } from "nanoid";
import PiStepForm from "../step-form/index.vue";
import { Modal } from "ant-design-vue";
import { CopyOutlined } from "@ant-design/icons-vue";
import VDraggable from "vuedraggable";
import { useUserStore } from "/@/store/user";
import { useSettingStore } from "/@/store/settings";
import { filter } from "lodash-es";
export default {
name: "PiTaskForm",
components: { CopyOutlined, PiStepForm, VDraggable },
components: { PiStepForm, VDraggable },
props: {
editMode: {
type: Boolean,
@@ -805,9 +805,11 @@ export default defineComponent({
let errorMessages: any = [];
let errorIndex = 1;
eachSteps(pp, (step: any, task: any, stage: any) => {
if (step.disabled !== true) {
stepIds.push(step.id);
if (step.disabled === true) {
return;
}
stepIds.push(step.id);
if (step.input) {
for (const key in step.input) {
const value = step.input[key];
@@ -73,3 +73,17 @@ export async function ApproveJoin(form: any) {
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>
<a-divider type="vertical"></a-divider>
<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>
</div>
<div class="more"></div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #pagination-left>
@@ -29,11 +33,13 @@ import createCrudOptions from "./crud";
import { message, Modal } from "ant-design-vue";
import { DeleteBatch } from "./api";
import { useI18n } from "/src/locales";
import { useRoute } from "vue-router";
import { useRoute, useRouter } from "vue-router";
import { useProjectStore } from "/@/store/project";
import { request } from "/@/api/service";
import { useDicts } from "../../dicts";
import { useCrudPermission } from "/@/plugin/permission";
import { useUserStore } from "/@/store/user";
import { useTransfer } from "./use";
const { t } = useI18n();
@@ -49,6 +55,14 @@ if (!projectId) {
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 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,
};
}
@@ -4,7 +4,7 @@
<!-- <div class="login-title">登录</div>-->
<template v-if="!isOauthOnly">
<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'">
<!-- <div class="login-title">登录</div>-->
<a-form-item required has-feedback name="username" :rules="rules.username">
@@ -46,11 +46,21 @@
</a-form-item>
</template>
</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-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") }}
</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>
<div class="mt-2 flex justify-between items-center">
@@ -94,8 +104,8 @@
</a-form>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, nextTick, reactive, ref, toRaw } from "vue";
<script lang="ts" setup>
import { computed, nextTick, reactive, ref, toRaw, onMounted } from "vue";
import { useUserStore } from "/src/store/user";
import { useSettingStore } from "/@/store/settings";
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 { LanguageToggle } from "/@/vben/layouts";
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 * as oauthApi from "../oauth/api";
import { notification } from "ant-design-vue";
export default defineComponent({
name: "LoginPage",
components: { LanguageToggle, SmsCode, CaptchaInput, OauthFooter },
setup() {
const { t } = useI18n();
const route = useRoute();
const userStore = useUserStore();
import { request } from "/src/api/service";
import * as UserApi from "/src/store/user/api.user";
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 urlLoginType = route.query.loginType as string | undefined;
const verifyCodeInputRef = ref();
const loading = ref(false);
const queryBindCode = ref(route.query.bindCode as string | undefined);
const queryOauthOnly = route.query.oauthOnly as string;
const urlLoginType = route.query.loginType as string | undefined;
const verifyCodeInputRef = ref();
const loading = ref(false);
const settingStore = useSettingStore();
const formRef = ref();
let defaultLoginType = settingStore.sysPublic.defaultLoginType || "password";
if (defaultLoginType === "sms") {
if (!settingStore.sysPublic.smsLoginEnabled || !settingStore.isComm) {
defaultLoginType = "password";
}
}
const formState = reactive({
username: "",
phoneCode: "86",
mobile: "",
password: "",
loginType: urlLoginType || defaultLoginType, //password
smsCode: "",
captcha: null,
smsCaptcha: null,
});
const settingStore = useSettingStore();
const formRef = ref();
let defaultLoginType = settingStore.sysPublic.defaultLoginType || "password";
if (defaultLoginType === "sms") {
if (!settingStore.sysPublic.smsLoginEnabled || !settingStore.isComm) {
defaultLoginType = "password";
}
}
const formState = reactive({
username: "",
phoneCode: "86",
mobile: "",
password: "",
loginType: urlLoginType || defaultLoginType,
smsCode: "",
captcha: null,
smsCaptcha: null,
});
const rules = {
mobile: [
{
required: true,
message: "请输入手机号",
},
],
username: [
{
required: true,
message: "请输入用户名",
},
],
password: [
{
required: true,
message: "请输入登录密码",
},
],
smsCode: [
{
required: true,
message: "请输入短信验证码",
},
],
captcha: [
{
required: true,
message: "请进行验证码验证",
},
],
};
const layout = {
labelCol: {
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,
};
const rules = {
mobile: [
{
required: true,
message: "请输入手机号",
},
],
username: [
{
required: true,
message: "请输入用户名",
},
],
password: [
{
required: true,
message: "请输入登录密码",
},
],
smsCode: [
{
required: true,
message: "请输入短信验证码",
},
],
captcha: [
{
required: true,
message: "请进行验证码验证",
},
],
};
const layout = {
labelCol: {
span: 0,
},
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>
<style lang="less">
.login-page.main {
//margin: 20px !important;
margin-bottom: 100px;
.user-layout-login {
//label {
// font-size: 14px;
//}
.fs-icon {
// color: rgba(0, 0, 0, 0.45);
margin-right: 4px;
}
@@ -328,7 +343,6 @@ export default defineComponent({
text-align: left;
margin-top: 30px;
margin-bottom: 30px;
//line-height: 22px;
.item-icon {
font-size: 24px;
@@ -269,7 +269,7 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
title: t("certd.roles"),
type: "dict-select",
dict: dict({
url: "/sys/authority/role/list",
url: "/basic/user/getSimpleRoles",
value: "id",
label: "name",
}), // 数据字典
@@ -7,6 +7,7 @@ import * as api from "./api";
import { useSettingStore } from "/@/store/settings";
import { useUserStore } from "/@/store/user";
import { useI18n } from "/src/locales";
import { useProjectStore } from "/@/store/project";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter();
@@ -29,6 +30,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
};
const userStore = useUserStore();
const projectStore = useProjectStore();
const settingStore = useSettingStore();
const selectedRowKeys: Ref<any[]> = ref([]);
context.selectedRowKeys = selectedRowKeys;
@@ -61,6 +63,12 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
minWidth: 200,
fixed: "right",
},
form: {
onSuccess: async () => {
await projectStore.reload();
crudExpose?.doRefresh();
},
},
columns: {
id: {
title: "ID",
@@ -122,7 +122,7 @@ const editableKeys = ref([
},
defaultRender(item: any) {
return () => {
return <pre class={"helper"}>{item["helper"]}</pre>;
return <pre class={"helper pre"}>{item["helper"]}</pre>;
};
},
editRender(item: any) {
@@ -14,6 +14,11 @@
<div class="helper">{{ t("certd.httpsProxyHelper") }}</div>
</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-select v-model:value="formState.private.dnsResultOrder">
<a-select-option value="verbatim">{{ t("certd.default") }}</a-select-option>
@@ -55,6 +60,11 @@ defineOptions({
name: "SettingNetwork",
});
const environmentVarsExample = ref(
`ALIYUN_CLIENT_CONNECT_TIMEOUT=16000 #连接超时,单位毫秒
ALIYUN_CLIENT_READ_TIMEOUT=16000 #读取数据超时单位毫秒`
);
const formState = reactive<Partial<SysSettings>>({
public: {},
private: {},
@@ -1,6 +1,16 @@
<template>
<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-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']">
<div class="flex-o">
<a-switch v-model:checked="formState.public.oauthEnabled" :disabled="!settingsStore.isPlus" :title="t('certd.plusFeature')" />
@@ -79,7 +89,7 @@
<script setup lang="tsx">
import { notification } from "ant-design-vue";
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 { useSettingStore } from "/@/store/settings";
import * as api from "/@/views/sys/settings/api";
@@ -101,6 +111,16 @@ async function loadOauthProviders() {
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) {
const providers: any = {};
for (const item of oauthProviders.value) {
+4 -4
View File
@@ -93,16 +93,16 @@ export default (req: any) => {
// with options
"/api": {
//配套后端 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": {
//配套后端 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/, ""),
//忽略证书
agent: new https.Agent({ rejectUnauthorized: false }),
// agent: new https.Agent({ rejectUnauthorized: false }),
},
},
},
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
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
@@ -0,0 +1,18 @@
CREATE TABLE "sys_passkey"
(
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
"user_id" integer NOT NULL,
"device_name" varchar(512) NOT NULL,
"passkey_id" varchar(512) NOT NULL,
"public_key" varchar(1024) NOT NULL,
"counter" integer NOT NULL,
"transports" varchar(512) NULL,
"registered_at" integer NOT NULL,
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
);
CREATE INDEX "index_passkey_user_id" ON "sys_passkey" ("user_id");
CREATE INDEX "index_passkey_passkey_id" ON "sys_passkey" ("passkey_id");
@@ -0,0 +1,12 @@
ALTER TABLE cd_access ADD COLUMN key_id varchar(100) NULL;
CREATE INDEX "index_access_key_id" ON "cd_access" ("key_id");
update cd_access set key_id = id where key_id is null;
ALTER TABLE pi_notification ADD COLUMN key_id varchar(100) NULL;
CREATE INDEX "index_notification_key_id" ON "pi_notification" ("key_id");
update pi_notification set key_id = id where key_id is null;
ALTER TABLE cd_addon ADD COLUMN key_id varchar(100) NULL;
CREATE INDEX "index_addon_key_id" ON "cd_addon" ("key_id");
update cd_addon set key_id = id where key_id is null;
+17 -14
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/ui-server",
"version": "1.39.0",
"version": "1.39.1",
"description": "fast-server base midway",
"private": true,
"type": "module",
@@ -50,20 +50,20 @@
"@aws-sdk/client-route-53": "^3.964.0",
"@aws-sdk/client-s3": "^3.964.0",
"@aws-sdk/client-sts": "^3.990.0",
"@certd/acme-client": "^1.39.0",
"@certd/basic": "^1.39.0",
"@certd/commercial-core": "^1.39.0",
"@certd/acme-client": "^1.39.1",
"@certd/basic": "^1.39.1",
"@certd/commercial-core": "^1.39.1",
"@certd/cv4pve-api-javascript": "^8.4.2",
"@certd/jdcloud": "^1.39.0",
"@certd/lib-huawei": "^1.39.0",
"@certd/lib-k8s": "^1.39.0",
"@certd/lib-server": "^1.39.0",
"@certd/midway-flyway-js": "^1.39.0",
"@certd/pipeline": "^1.39.0",
"@certd/plugin-cert": "^1.39.0",
"@certd/plugin-lib": "^1.39.0",
"@certd/plugin-plus": "^1.39.0",
"@certd/plus-core": "^1.39.0",
"@certd/jdcloud": "^1.39.1",
"@certd/lib-huawei": "^1.39.1",
"@certd/lib-k8s": "^1.39.1",
"@certd/lib-server": "^1.39.1",
"@certd/midway-flyway-js": "^1.39.1",
"@certd/pipeline": "^1.39.1",
"@certd/plugin-cert": "^1.39.1",
"@certd/plugin-lib": "^1.39.1",
"@certd/plugin-plus": "^1.39.1",
"@certd/plus-core": "^1.39.1",
"@google-cloud/publicca": "^1.3.0",
"@huaweicloud/huaweicloud-sdk-cdn": "^3.1.185",
"@huaweicloud/huaweicloud-sdk-core": "^3.1.185",
@@ -78,10 +78,13 @@
"@midwayjs/koa": "3.20.13",
"@midwayjs/logger": "3.4.2",
"@midwayjs/static-file": "3.20.13",
"@midwayjs/swagger": "3.20.11",
"@midwayjs/typeorm": "3.20.11",
"@midwayjs/upload": "3.20.13",
"@midwayjs/validate": "3.20.13",
"@peculiar/x509": "^1.11.0",
"@simplewebauthn/browser": "^13.2.2",
"@simplewebauthn/server": "^13.2.3",
"@ucloud-sdks/ucloud-sdk-js": "^0.2.4",
"@volcengine/openapi": "^1.28.1",
"ali-oss": "^6.21.0",
@@ -131,6 +131,106 @@ const development = {
contactText: '',
contactLink: '',
},
swagger: {
isGenerateTagForController: false,
routerFilter: (url: string) => {
return url.startsWith('/api/sys');
},
tags: [
{
name: 'addon',
description: '插件管理'
},
{
name: 'basic-group',
description: '基础分组管理'
},
{
name: 'basic-user',
description: '基础用户管理'
},
{
name: 'cert',
description: '证书管理'
},
{
name: 'pipeline-cname',
description: 'CNAME配置管理'
},
{
name: 'dashboard',
description: '仪表板统计'
},
{
name: 'enterprise-project',
description: '企业项目管理'
},
{
name: 'enterprise-project-member',
description: '企业项目成员管理'
},
{
name: 'mine',
description: '个人中心'
},
{
name: 'monitor',
description: '监控管理'
},
{
name: 'open',
description: '开放API KEY管理'
},
{
name: 'pipeline-access',
description: '流水线授权配置'
},
{
name: 'pipeline-cert',
description: '流水线证书'
},
{
name: 'pipeline-dns-provider',
description: '流水线DNS提供商'
},
{
name: 'pipeline-handle',
description: '插件请求处理'
},
{
name: 'pipeline-history',
description: '流水线执行历史'
},
{
name: 'pipeline-notification',
description: '流水线通知配置'
},
{
name: 'pipeline-plugin',
description: '流水线插件'
},
{
name: 'pipeline-subdomain',
description: '流水线子域名'
},
{
name: 'pipeline-template',
description: '流水线模版'
},
{
name: 'pipeline-group',
description: '流水线分组'
},
{
name: 'pipeline',
description: '流水线管理'
},
{
name: 'openapi',
description: '开放API'
},
]
}
} as MidwayConfig;
loadDotEnv();
@@ -20,7 +20,7 @@ import * as commercial from '@certd/commercial-core';
import * as upload from '@midwayjs/upload';
import { setLogger } from '@certd/acme-client';
import {HiddenMiddleware} from "./middleware/hidden.js";
import * as swagger from '@midwayjs/swagger';
//@ts-ignore
// process.env.UV_THREADPOOL_SIZE = 2
process.on('uncaughtException', error => {
@@ -62,6 +62,10 @@ process.on('uncaughtException', error => {
upload,
libServer,
commercial,
{
component: swagger,
enabledEnvironment: ['local']
},
{
component: info,
enabledEnvironment: ['local'],
@@ -13,7 +13,7 @@ export class AppController extends BaseController {
@Inject()
fileService: FileService;
@Get('/latest', { summary: Constants.per.authOnly })
@Get('/latest', { description: Constants.per.authOnly })
async latest(): Promise<any> {
try {
const res = await http.request({
@@ -30,7 +30,7 @@ export class AppController extends BaseController {
}
}
@Get('/favicon', { summary: Constants.per.guest })
@Get('/favicon', { description: Constants.per.guest })
public async getFavicon() {
if (isComm()) {
const siteInfo = await this.sysSettingsService.getSetting<SysSiteInfo>(SysSiteInfo);
@@ -47,7 +47,7 @@ export class AppController extends BaseController {
this.ctx.response.set('Cache-Control', 'public,max-age=25920');
}
@Post('/webhook', { summary: Constants.per.guest })
@Post('/webhook', { description: Constants.per.guest })
public async webhook( @Body() body: any) {
logger.info('webhook', JSON.stringify(body))
return this.ok("success")
@@ -53,13 +53,13 @@ export class BasicController extends BaseController {
@Inject()
addonGetterService: AddonGetterService;
@Post('/captcha/get', { summary: Constants.per.guest })
@Post('/captcha/get', { description: Constants.per.guest })
async getCaptcha(@Query("captchaAddonId") captchaAddonId:number) {
const form = await this.captchaService.getCaptcha(captchaAddonId)
return this.ok(form);
}
@Post('/sendSmsCode', { summary: Constants.per.guest })
@Post('/sendSmsCode', { description: Constants.per.guest })
public async sendSmsCode(
@Body(ALL)
body: SmsCodeReq,
@@ -80,7 +80,7 @@ export class BasicController extends BaseController {
return this.ok(null);
}
@Post('/sendEmailCode', { summary: Constants.per.guest })
@Post('/sendEmailCode', { description: Constants.per.guest })
public async sendEmailCode(
@Body(ALL)
body: EmailCodeReq,
@@ -13,7 +13,7 @@ export class FileController extends BaseController {
@Inject()
fileService: FileService;
@Post('/upload', { summary: Constants.per.authOnly })
@Post('/upload', { description: Constants.per.authOnly })
async upload(@Files() files: UploadFileInfo<string>[], @Fields() fields: any) {
console.log('files', files, fields);
const cacheKey = uploadTmpFileCacheKey + nanoid();
@@ -33,7 +33,7 @@ export class FileController extends BaseController {
});
}
@Get('/download', { summary: Constants.per.guest })
@Get('/download', { description: Constants.per.guest })
async download(@Query('key') key: string) {
let userId: any = null;
if (!key.startsWith('/public')) {
@@ -8,12 +8,12 @@ import {BaseController, Constants} from '@certd/lib-server';
export class HealthController extends BaseController {
@Get('/liveliness', { summary: Constants.per.guest })
@Get('/liveliness', { description: Constants.per.guest })
async liveliness(): Promise<any> {
return this.ok('ok')
}
@Get('/readiness', { summary: Constants.per.guest })
@Get('/readiness', { description: Constants.per.guest })
async readiness(): Promise<any> {
return this.ok('ok')
}

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