Compare commits

...

35 Commits

Author SHA1 Message Date
xiaojunnuo 541131bbc6 Merge branch 'v2-dev' into v2_admin_mode 2026-02-16 00:48:23 +08:00
xiaojunnuo d125eb56b3 chore: project 2026-02-14 00:08:13 +08:00
xiaojunnuo 956d68695c chore: project 2026-02-13 23:51:27 +08:00
xiaojunnuo 83d81b64b3 perf: 站点监控支持指定ip地址检查 2026-02-13 23:51:19 +08:00
xiaojunnuo a4ea82c04b Merge branch 'v2-dev' into v2_admin_mode 2026-02-13 22:58:07 +08:00
xiaojunnuo cfd5b388f1 chore: project 2026-02-13 22:24:04 +08:00
xiaojunnuo 4ee6e38a94 chore: project controller ok 2026-02-13 21:28:17 +08:00
xiaojunnuo 3f87752d1f Merge branch 'v2-dev' into v2_admin_mode 2026-02-13 19:04:09 +08:00
xiaojunnuo 67f347197e chore: project query 2026-02-13 00:41:40 +08:00
xiaojunnuo 99db1b1cc3 chore: history projectId 2026-02-11 18:17:46 +08:00
xiaojunnuo 638a7f0ab4 chore: history projectId 2026-02-11 18:11:33 +08:00
xiaojunnuo 806a69fef3 Merge branch 'v2_admin_mode' of https://github.com/certd/certd into v2_admin_mode 2026-02-11 16:28:53 +08:00
xiaojunnuo 8ba2e9e34c Merge branch 'v2-dev' into v2_admin_mode 2026-02-11 16:28:43 +08:00
xiaojunnuo 28dfef985c chore: project 初步 2026-02-11 00:54:56 +08:00
xiaojunnuo 1e416b9f8a chore: project controller 2026-02-11 00:07:29 +08:00
xiaojunnuo 784bcb0aa5 Merge branch 'v2-dev' into v2_admin_mode 2026-02-10 22:10:08 +08:00
xiaojunnuo 37340838b6 feat: 支持企业级管理模式,项目管理,细分权限 2026-02-10 01:57:11 +08:00
xiaojunnuo d1a8dd7817 Merge branch 'v2-dev' into v2_admin_mode 2026-02-09 23:13:43 +08:00
xiaojunnuo b16f92314b chore: 自动转换mysql表格engine 2026-02-09 18:40:15 +08:00
xiaojunnuo ad22244388 Merge branch 'v2-dev' into v2_admin_mode 2026-02-09 18:21:01 +08:00
xiaojunnuo 2e3d0cc57c fix: 修复部署到openwrt错误的bug 2026-02-09 13:50:28 +08:00
xiaojunnuo 1e44115461 fix: esxi部署失败的bug 2026-02-09 13:49:00 +08:00
xiaojunnuo 1ee1d61c74 Merge branch 'v2-dev' into v2_admin_mode 2026-02-06 16:51:57 +08:00
xiaojunnuo 7b8b9cfd2b chore: project 2026-02-06 16:16:00 +08:00
xiaojunnuo 5e7a67834b chore: docs 2026-02-06 13:25:14 +08:00
xiaojunnuo 3c85602ab1 perf: http请求增加建立连接超时配置 2026-02-06 12:00:40 +08:00
xiaojunnuo 66d0d0e213 chore: admin mode 2026-02-05 19:01:03 +08:00
xiaojunnuo 1f68faddb9 perf: AI开发插件 skills 定义初步 2026-02-05 17:26:47 +08:00
xiaojunnuo db06f06c96 docs: microsoft oauth docs 2026-02-05 17:18:26 +08:00
xiaojunnuo 79e973e9c8 Merge branch 'v2-dev' into v2_admin_mode 2026-02-05 16:31:32 +08:00
xiaojunnuo 8d8304e859 chore: 1 2026-02-04 19:03:03 +08:00
xiaojunnuo 6ddc23e2aa chore: project member 2026-02-04 18:54:00 +08:00
xiaojunnuo 2fc491015e chore: project manager 2026-02-04 18:24:15 +08:00
xiaojunnuo bb0afe1fa7 perf: 当域名管理中没有域名时,创建流水线时不展开域名选择框 2026-02-04 17:01:08 +08:00
xiaojunnuo eb46f8c776 chore: 企业管理模式初步 2026-02-04 15:49:01 +08:00
159 changed files with 5745 additions and 501 deletions
+301
View File
@@ -0,0 +1,301 @@
# Access 插件开发技能
## 什么是 Access 插件
Access 插件是 Certd 系统中用于存储用户第三方应用授权数据的插件,例如用户名密码、accessSecret 或 accessToken 等。同时,它还负责对接实现第三方的 API 接口,供其他插件调用使用。
## 开发步骤
### 1. 导入必要的依赖
```typescript
import { AccessInput, BaseAccess, IsAccess, Pager, PageRes, PageSearch } from '@certd/pipeline';
import { DomainRecord } from '@certd/plugin-lib';
```
### 2. 使用 @IsAccess 注解注册插件
```typescript
@IsAccess({
name: 'demo', // 插件唯一标识
title: '授权插件示例', // 插件标题
icon: 'clarity:plugin-line', // 插件图标
desc: '这是一个示例授权插件,用于演示如何实现一个授权插件', // 插件描述
})
export class DemoAccess extends BaseAccess {
// 插件实现...
}
```
### 3. 定义授权属性
使用 `@AccessInput` 注解定义授权属性:
```typescript
@AccessInput({
title: '授权方式',
value: 'apiKey', // 默认值
component: {
name: "a-select", // 基于 antdv 的输入组件
vModel: "value", // v-model 绑定的属性名
options: [ // 组件参数
{ label: "API密钥(推荐)", value: "apiKey" },
{ label: "账号密码", value: "account" },
],
placeholder: 'demoKeyId',
},
required: true,
})
apiType = '';
@AccessInput({
title: '密钥Id',
component: {
name:"a-input",
allowClear: true,
placeholder: 'demoKeyId',
},
required: true,
})
demoKeyId = '';
@AccessInput({
title: '密钥',//标题
required: true, //text组件可以省略
encrypt: true, //该属性是否需要加密
})
demoKeySecret = '';
```
### 4. 实现测试方法
```typescript
@AccessInput({
title: "测试",
component: {
name: "api-test",
action: "TestRequest"
},
helper: "点击测试接口是否正常"
})
testRequest = true;
/**
* 会通过上面的testRequest参数在ui界面上生成测试按钮,供用户测试接口调用是否正常
*/
async onTestRequest() {
await this.GetDomainList({});
return "ok"
}
```
### 5. 实现 API 方法
```typescript
/**
* 获api接口示例 取域名列表,
*/
async GetDomainList(req: PageSearch): Promise<PageRes<DomainRecord>> {
//输出日志必须使用ctx.logger
this.ctx.logger.info(`获取域名列表,req:${JSON.stringify(req)}`);
const pager = new Pager(req);
const resp = await this.doRequest({
action: "ListDomains",
data: {
domain: req.searchKey,
offset: pager.getOffset(),
limit: pager.pageSize,
}
});
const total = resp?.TotalCount || 0;
let list = resp?.DomainList?.map((item) => {
item.domain = item.Domain;
item.id = item.DomainId;
return item;
})
return {
total,
list
};
}
/**
* 通用api调用方法, 具体如何构造请求体,需参考对应应用的API文档
*/
async doRequest(req: { action: string, data?: any }) {
/**
this.ctx中包含很多有用的工具类
type AccessContext = {
http: HttpClient;
logger: ILogger;
utils: typeof utils;
accessService: IAccessService;
}
*/
const res = await this.ctx.http.request({
url: "https://api.demo.cn/api/",
method: "POST",
data: {
Action: req.action,
Body: req.data
}
});
if (res.Code !== 0) {
//异常处理
throw new Error(res.Message || "请求失败");
}
return res.Resp;
}
```
## 注意事项
1. **插件命名**:插件名称应简洁明了,反映其功能。
2. **属性加密**:对于敏感信息(如密钥),应设置 `encrypt: true`
3. **日志输出**:必须使用 `this.ctx.logger` 输出日志,而不是 `console`
4. **错误处理**:API 调用失败时应抛出明确的错误信息。
5. **测试方法**:实现 `onTestRequest` 方法,以便用户可以测试授权是否正常。
## 完整示例
```typescript
import { AccessInput, BaseAccess, IsAccess, Pager, PageRes, PageSearch } from '@certd/pipeline';
import { DomainRecord } from '@certd/plugin-lib';
/**
* 这个注解将注册一个授权配置
* 在certd的后台管理系统中,用户可以选择添加此类型的授权
*/
@IsAccess({
name: 'demo',
title: '授权插件示例',
icon: 'clarity:plugin-line', //插件图标
desc: '这是一个示例授权插件,用于演示如何实现一个授权插件',
})
export class DemoAccess extends BaseAccess {
/**
* 授权属性配置
*/
@AccessInput({
title: '授权方式',
value: 'apiKey', //默认值
component: {
name: "a-select", //基于antdv的输入组件
vModel: "value", // v-model绑定的属性名
options: [ //组件参数
{
label: "API密钥(推荐)",
value: "apiKey"
},
{
label: "账号密码",
value: "account"
},
],
placeholder: 'demoKeyId',
},
required: true,
})
apiType = '';
/**
* 授权属性配置
*/
@AccessInput({
title: '密钥Id',
component: {
name:"a-input",
allowClear: true,
placeholder: 'demoKeyId',
},
required: true,
})
demoKeyId = '';
@AccessInput({
title: '密钥',//标题
required: true, //text组件可以省略
encrypt: true, //该属性是否需要加密
})
demoKeySecret = '';
@AccessInput({
title: "测试",
component: {
name: "api-test",
action: "TestRequest"
},
helper: "点击测试接口是否正常"
})
testRequest = true;
/**
* 会通过上面的testRequest参数在ui界面上生成测试按钮,供用户测试接口调用是否正常
*/
async onTestRequest() {
await this.GetDomainList({});
return "ok"
}
/**
* 获api接口示例 取域名列表,
*/
async GetDomainList(req: PageSearch): Promise<PageRes<DomainRecord>> {
//输出日志必须使用ctx.logger
this.ctx.logger.info(`获取域名列表,req:${JSON.stringify(req)}`);
const pager = new Pager(req);
const resp = await this.doRequest({
action: "ListDomains",
data: {
domain: req.searchKey,
offset: pager.getOffset(),
limit: pager.pageSize,
}
});
const total = resp?.TotalCount || 0;
let list = resp?.DomainList?.map((item) => {
item.domain = item.Domain;
item.id = item.DomainId;
return item;
})
return {
total,
list
};
}
// 还可以继续编写API
/**
* 通用api调用方法, 具体如何构造请求体,需参考对应应用的API文档
*/
async doRequest(req: { action: string, data?: any }) {
/**
this.ctx中包含很多有用的工具类
type AccessContext = {
http: HttpClient;
logger: ILogger;
utils: typeof utils;
accessService: IAccessService;
}
*/
const res = await this.ctx.http.request({
url: "https://api.demo.cn/api/",
method: "POST",
data: {
Action: req.action,
Body: req.data
}
});
if (res.Code !== 0) {
//异常处理
throw new Error(res.Message || "请求失败");
}
return res.Resp;
}
}
```
@@ -0,0 +1 @@
我需要开发一个 Access 插件,用于存储和管理第三方应用的授权信息。请指导我如何实现。
@@ -0,0 +1,145 @@
# Access 插件开发指南
## 开发步骤
### 1. 导入必要的依赖
```typescript
import { AccessInput, BaseAccess, IsAccess, Pager, PageRes, PageSearch } from '@certd/pipeline';
import { DomainRecord } from '@certd/plugin-lib';
```
### 2. 使用 @IsAccess 注解注册插件
```typescript
@IsAccess({
name: 'demo', // 插件唯一标识
title: '授权插件示例', // 插件标题
icon: 'clarity:plugin-line', // 插件图标
desc: '这是一个示例授权插件,用于演示如何实现一个授权插件', // 插件描述
})
export class DemoAccess extends BaseAccess {
// 插件实现...
}
```
### 3. 定义授权属性
使用 `@AccessInput` 注解定义授权属性:
```typescript
@AccessInput({
title: '授权方式',
value: 'apiKey', // 默认值
component: {
name: "a-select", // 基于 antdv 的输入组件
vModel: "value", // v-model 绑定的属性名
options: [ // 组件参数
{ label: "API密钥(推荐)", value: "apiKey" },
{ label: "账号密码", value: "account" },
],
placeholder: 'demoKeyId',
},
required: true,
})
apiType = '';
@AccessInput({
title: '密钥Id',
component: {
name:"a-input",
allowClear: true,
placeholder: 'demoKeyId',
},
required: true,
})
demoKeyId = '';
@AccessInput({
title: '密钥',//标题
required: true, //text组件可以省略
encrypt: true, //该属性是否需要加密
})
demoKeySecret = '';
```
### 4. 实现测试方法
```typescript
@AccessInput({
title: "测试",
component: {
name: "api-test",
action: "TestRequest"
},
helper: "点击测试接口是否正常"
})
testRequest = true;
/**
* 会通过上面的testRequest参数在ui界面上生成测试按钮,供用户测试接口调用是否正常
*/
async onTestRequest() {
await this.GetDomainList({});
return "ok"
}
```
### 5. 实现 API 方法
```typescript
/**
* 获api接口示例 取域名列表,
*/
async GetDomainList(req: PageSearch): Promise<PageRes<DomainRecord>> {
//输出日志必须使用ctx.logger
this.ctx.logger.info(`获取域名列表,req:${JSON.stringify(req)}`);
const pager = new Pager(req);
const resp = await this.doRequest({
action: "ListDomains",
data: {
domain: req.searchKey,
offset: pager.getOffset(),
limit: pager.pageSize,
}
});
const total = resp?.TotalCount || 0;
let list = resp?.DomainList?.map((item) => {
item.domain = item.Domain;
item.id = item.DomainId;
return item;
})
return {
total,
list
};
}
/**
* 通用api调用方法, 具体如何构造请求体,需参考对应应用的API文档
*/
async doRequest(req: { action: string, data?: any }) {
const res = await this.ctx.http.request({
url: "https://api.demo.cn/api/",
method: "POST",
data: {
Action: req.action,
Body: req.data
}
});
if (res.Code !== 0) {
//异常处理
throw new Error(res.Message || "请求失败");
}
return res.Resp;
}
```
## 注意事项
1. **插件命名**:插件名称应简洁明了,反映其功能。
2. **属性加密**:对于敏感信息(如密钥),应设置 `encrypt: true`
3. **日志输出**:必须使用 `this.ctx.logger` 输出日志,而不是 `console`
4. **错误处理**:API 调用失败时应抛出明确的错误信息。
5. **测试方法**:实现 `onTestRequest` 方法,以便用户可以测试授权是否正常。
+212
View File
@@ -0,0 +1,212 @@
# DNS Provider 插件开发技能
## 什么是 DNS Provider 插件
DNS Provider 插件是 Certd 系统中的 DNS 提供商插件,它用于在 ACME 申请证书时给域名添加 TXT 解析记录,以验证域名所有权。
## 开发步骤
### 1. 导入必要的依赖
```typescript
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert';
import { DemoAccess } from './access.js';
import { isDev } from '../../utils/env.js';
```
### 2. 定义记录数据结构
```typescript
type DemoRecord = {
// 这里定义 Record 记录的数据结构,跟对应云平台接口返回值一样即可,一般是拿到 id 就行,用于删除 txt 解析记录,清理申请痕迹
// id:string
};
```
### 3. 使用 @IsDnsProvider 注解注册插件
```typescript
// 这里通过 IsDnsProvider 注册一个 dnsProvider
@IsDnsProvider({
name: 'demo', // 插件唯一标识
title: 'Dns提供商Demo', // 插件标题
desc: 'dns provider示例', // 插件描述
icon: 'clarity:plugin-line', // 插件图标
// 这里是对应的云平台的 access 类型名称
accessType: 'demo',
order: 99, // 排序
})
export class DemoDnsProvider extends AbstractDnsProvider<DemoRecord> {
access!: DemoAccess;
async onInstance() {
this.access = this.ctx.access as DemoAccess;
// 也可以通过 ctx 成员变量传递 context
this.logger.debug('access', this.access);
// 初始化的操作
// ...
}
// 插件实现...
}
```
### 4. 实现 createRecord 方法
```typescript
/**
* 创建 dns 解析记录,用于验证域名所有权
*/
async createRecord(options: CreateRecordOptions): Promise<any> {
/**
* options 参数说明
* fullRecord: '_acme-challenge.example.com',
* value: 一串 uuid
* type: 'TXT',
* domain: 'example.com'
*/
const { fullRecord, value, type, domain } = options;
this.logger.info('添加域名解析:', fullRecord, value, type, domain);
// 调用创建 dns 解析记录的对应的云端接口,创建 txt 类型的 dns 解析记录
// 请根据实际接口情况调用,例如:
// const createDnsRecordUrl = "xxx"
// const record = this.http.post(createDnsRecordUrl,{
// // 授权参数
// // 创建 dns 解析记录的参数
// })
// // 返回本次创建的 dns 解析记录,这个记录会在删除的时候用到
// return record
}
```
### 5. 实现 removeRecord 方法
```typescript
/**
* 删除 dns 解析记录,清理申请痕迹
* @param options
*/
async removeRecord(options: RemoveRecordOptions<DemoRecord>): Promise<void> {
const { fullRecord, value, domain } = options.recordReq;
const record = options.recordRes;
this.logger.info('删除域名解析:', domain, fullRecord, value, record);
// 这里调用删除 txt dns 解析记录接口
// 请根据实际接口情况调用,例如:
// const deleteDnsRecordUrl = "xxx"
// const res = this.http.delete(deleteDnsRecordUrl,{
// // 授权参数
// // 删除 dns 解析记录的参数
// })
this.logger.info('删除域名解析成功:', fullRecord, value);
}
```
### 6. 实例化插件
```typescript
// 实例化这个 provider,将其自动注册到系统中
if (isDev()) {
// 你的实现 要去掉这个 if,不然生产环境将不会显示
new DemoDnsProvider();
}
```
## 注意事项
1. **插件命名**:插件名称应简洁明了,反映其功能。
2. **accessType**:必须指定对应的云平台的 access 类型名称。
3. **记录结构**:定义适合对应云平台的记录数据结构,至少包含 id 字段用于删除记录。
4. **日志输出**:使用 `this.logger` 输出日志,而不是 `console`
5. **错误处理**:API 调用失败时应抛出明确的错误信息。
6. **实例化**:生产环境中应移除 `if (isDev())` 条件,确保插件在生产环境中也能被注册。
## 完整示例
```typescript
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert';
import { DemoAccess } from './access.js';
import { isDev } from '../../utils/env.js';
type DemoRecord = {
// 这里定义 Record 记录的数据结构,跟对应云平台接口返回值一样即可,一般是拿到 id 就行,用于删除 txt 解析记录,清理申请痕迹
// id:string
};
// 这里通过 IsDnsProvider 注册一个 dnsProvider
@IsDnsProvider({
name: 'demo',
title: 'Dns提供商Demo',
desc: 'dns provider示例',
icon: 'clarity:plugin-line',
// 这里是对应的云平台的 access 类型名称
accessType: 'demo',
order: 99,
})
export class DemoDnsProvider extends AbstractDnsProvider<DemoRecord> {
access!: DemoAccess;
async onInstance() {
this.access = this.ctx.access as DemoAccess;
// 也可以通过 ctx 成员变量传递 context
this.logger.debug('access', this.access);
// 初始化的操作
// ...
}
/**
* 创建 dns 解析记录,用于验证域名所有权
*/
async createRecord(options: CreateRecordOptions): Promise<any> {
/**
* options 参数说明
* fullRecord: '_acme-challenge.example.com',
* value: 一串 uuid
* type: 'TXT',
* domain: 'example.com'
*/
const { fullRecord, value, type, domain } = options;
this.logger.info('添加域名解析:', fullRecord, value, type, domain);
// 调用创建 dns 解析记录的对应的云端接口,创建 txt 类型的 dns 解析记录
// 请根据实际接口情况调用,例如:
// const createDnsRecordUrl = "xxx"
// const record = this.http.post(createDnsRecordUrl,{
// // 授权参数
// // 创建 dns 解析记录的参数
// })
// // 返回本次创建的 dns 解析记录,这个记录会在删除的时候用到
// return record
}
/**
* 删除 dns 解析记录,清理申请痕迹
* @param options
*/
async removeRecord(options: RemoveRecordOptions<DemoRecord>): Promise<void> {
const { fullRecord, value, domain } = options.recordReq;
const record = options.recordRes;
this.logger.info('删除域名解析:', domain, fullRecord, value, record);
// 这里调用删除 txt dns 解析记录接口
// 请根据实际接口情况调用,例如:
// const deleteDnsRecordUrl = "xxx"
// const res = this.http.delete(deleteDnsRecordUrl,{
// // 授权参数
// // 删除 dns 解析记录的参数
// })
this.logger.info('删除域名解析成功:', fullRecord, value);
}
}
// 实例化这个 provider,将其自动注册到系统中
if (isDev()) {
// 你的实现 要去掉这个 if,不然生产环境将不会显示
new DemoDnsProvider();
}
```
@@ -0,0 +1 @@
我需要开发一个 DNS Provider 插件,用于在 ACME 申请证书时添加 TXT 解析记录。请指导我如何实现。
@@ -0,0 +1,121 @@
# DNS Provider 插件开发指南
## 开发步骤
### 1. 导入必要的依赖
```typescript
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert';
import { DemoAccess } from './access.js';
import { isDev } from '../../utils/env.js';
```
### 2. 定义记录数据结构
```typescript
type DemoRecord = {
// 这里定义 Record 记录的数据结构,跟对应云平台接口返回值一样即可,一般是拿到 id 就行,用于删除 txt 解析记录,清理申请痕迹
// id:string
};
```
### 3. 使用 @IsDnsProvider 注解注册插件
```typescript
// 这里通过 IsDnsProvider 注册一个 dnsProvider
@IsDnsProvider({
name: 'demo', // 插件唯一标识
title: 'Dns提供商Demo', // 插件标题
desc: 'dns provider示例', // 插件描述
icon: 'clarity:plugin-line', // 插件图标
// 这里是对应的云平台的 access 类型名称
accessType: 'demo',
order: 99, // 排序
})
export class DemoDnsProvider extends AbstractDnsProvider<DemoRecord> {
access!: DemoAccess;
async onInstance() {
this.access = this.ctx.access as DemoAccess;
// 也可以通过 ctx 成员变量传递 context
this.logger.debug('access', this.access);
// 初始化的操作
// ...
}
// 插件实现...
}
```
### 4. 实现 createRecord 方法
```typescript
/**
* 创建 dns 解析记录,用于验证域名所有权
*/
async createRecord(options: CreateRecordOptions): Promise<any> {
/**
* options 参数说明
* fullRecord: '_acme-challenge.example.com',
* value: 一串 uuid
* type: 'TXT',
* domain: 'example.com'
*/
const { fullRecord, value, type, domain } = options;
this.logger.info('添加域名解析:', fullRecord, value, type, domain);
// 调用创建 dns 解析记录的对应的云端接口,创建 txt 类型的 dns 解析记录
// 请根据实际接口情况调用,例如:
// const createDnsRecordUrl = "xxx"
// const record = this.http.post(createDnsRecordUrl,{
// // 授权参数
// // 创建 dns 解析记录的参数
// })
// // 返回本次创建的 dns 解析记录,这个记录会在删除的时候用到
// return record
}
```
### 5. 实现 removeRecord 方法
```typescript
/**
* 删除 dns 解析记录,清理申请痕迹
* @param options
*/
async removeRecord(options: RemoveRecordOptions<DemoRecord>): Promise<void> {
const { fullRecord, value, domain } = options.recordReq;
const record = options.recordRes;
this.logger.info('删除域名解析:', domain, fullRecord, value, record);
// 这里调用删除 txt dns 解析记录接口
// 请根据实际接口情况调用,例如:
// const deleteDnsRecordUrl = "xxx"
// const res = this.http.delete(deleteDnsRecordUrl,{
// // 授权参数
// // 删除 dns 解析记录的参数
// })
this.logger.info('删除域名解析成功:', fullRecord, value);
}
```
### 6. 实例化插件
```typescript
// 实例化这个 provider,将其自动注册到系统中
if (isDev()) {
// 你的实现 要去掉这个 if,不然生产环境将不会显示
new DemoDnsProvider();
}
```
## 注意事项
1. **插件命名**:插件名称应简洁明了,反映其功能。
2. **accessType**:必须指定对应的云平台的 access 类型名称。
3. **记录结构**:定义适合对应云平台的记录数据结构,至少包含 id 字段用于删除记录。
4. **日志输出**:使用 `this.logger` 输出日志,而不是 `console`
5. **错误处理**:API 调用失败时应抛出明确的错误信息。
6. **实例化**:生产环境中应移除 `if (isDev())` 条件,确保插件在生产环境中也能被注册。
+201
View File
@@ -0,0 +1,201 @@
# 插件转换工具技能
## 什么是插件转换工具
插件转换工具是一个用于将 Certd 插件转换为 YAML 配置文件的命令行工具。它可以分析单个插件文件,识别插件类型,并生成对应的 YAML 配置,方便插件的注册和管理。
## 工具位置
`trae/skills/convert-plugin-to-yaml.js`
## 功能特性
- **单个插件转换**:支持指定单个插件文件进行转换,而不是扫描整个目录
- **自动类型识别**:自动识别插件类型(Access、Task、DNS Provider、Notification、Addon
- **详细日志输出**:提供详细的转换过程日志,便于调试
- **YAML 配置生成**:生成标准的 YAML 配置文件
- **配置文件保存**:自动将生成的配置保存到 `./metadata` 目录
- **可复用函数**:导出了可复用的函数,便于其他模块调用
## 使用方法
### 基本用法
```bash
node trae/skills/convert-plugin-to-yaml.js <插件文件路径>
```
### 示例
```bash
# 转换 Access 插件
node trae/skills/convert-plugin-to-yaml.js packages/ui/certd-server/src/plugins/plugin-demo/access.js
# 转换 Task 插件
node trae/skills/convert-plugin-to-yaml.js packages/ui/certd-server/src/plugins/plugin-demo/plugins/plugin-test.js
# 转换 DNS Provider 插件
node trae/skills/convert-plugin-to-yaml.js packages/ui/certd-server/src/plugins/plugin-demo/dns-provider.js
```
## 转换过程
1. **加载插件模块**:使用 `import()` 动态加载指定的插件文件
2. **分析插件定义**:检查模块导出的对象,寻找带有 `define` 属性的插件
3. **识别插件类型**:根据插件的继承关系或属性识别插件类型
4. **生成 YAML 配置**:基于插件定义生成标准的 YAML 配置
5. **保存配置文件**:将生成的配置保存到 `./metadata` 目录
## 输出说明
### 命令行输出
执行转换命令后,工具会输出以下信息:
- 插件加载状态
- 插件导出的对象列表
- 插件类型识别结果
- 生成的 YAML 配置内容
- 配置文件保存路径
### 配置文件命名规则
生成的配置文件命名规则为:
```
<插件类型>[_<子类型>]_<插件名称>.yaml
```
例如:
- `access_demo.yaml`Access 插件)
- `deploy_DemoTest.yaml`Task 插件)
- `dnsProvider_demo.yaml`DNS Provider 插件)
## 插件类型识别逻辑
工具通过以下逻辑识别插件类型:
1. **DNS Provider**:如果插件定义中包含 `accessType` 属性
2. **Task**:如果插件继承自 `AbstractTaskPlugin`
3. **Notification**:如果插件继承自 `BaseNotification`
4. **Access**:如果插件继承自 `BaseAccess`
5. **Addon**:如果插件继承自 `BaseAddon`
## 注意事项
1. **文件路径**:插件文件路径可以是相对路径或绝对路径
2. **文件格式**:仅支持 `.js` 文件,不支持 `.ts` 文件(需要先编译)
3. **依赖安装**:执行前确保已安装所有必要的依赖
4. **配置目录**:如果 `./metadata` 目录不存在,工具会自动创建
5. **错误处理**:如果插件加载失败或识别失败,工具会输出错误信息但不会终止执行
## 代码结构
### 主要函数
1. **isPrototypeOf(value, cls)**:检查对象是否是指定类的原型
2. **loadSingleModule(filePath)**:加载单个插件模块
3. **convertSinglePlugin(pluginPath)**:分析单个插件并生成 YAML 配置
4. **main()**:主函数,处理命令行参数并执行转换
### 导出函数
工具导出了以下函数,便于其他模块调用:
```javascript
export {
convertSinglePlugin, // 转换单个插件
loadSingleModule, // 加载单个模块
isPrototypeOf // 检查原型关系
};
```
## 应用场景
1. **插件开发**:在开发新插件时,快速生成配置文件
2. **插件调试**:查看插件的内部定义和配置
3. **插件管理**:批量转换现有插件为标准配置格式
4. **自动化构建**:集成到构建流程中,自动生成插件配置
## 示例输出
### 转换 Access 插件示例
```bash
$ node trae/skills/convert-plugin-to-yaml.js packages/ui/certd-server/src/plugins/plugin-demo/access.js
开始转换插件: packages/ui/certd-server/src/plugins/plugin-demo/access.js
插件模块导出了 1 个对象: DemoAccess
处理插件: DemoAccess
插件类型: access
脚本路径: packages/ui/certd-server/src/plugins/plugin-demo/access.js
生成的 YAML 配置:
name: demo
title: 授权插件示例
desc: 这是一个示例授权插件,用于演示如何实现一个授权插件
icon: clarity:plugin-line
pluginType: access
type: builtIn
scriptFilePath: packages/ui/certd-server/src/plugins/plugin-demo/access.js
YAML 配置已保存到: ./metadata/access_demo.yaml
插件转换完成!
```
### 转换 Task 插件示例
```bash
$ node trae/skills/convert-plugin-to-yaml.js packages/ui/certd-server/src/plugins/plugin-demo/plugins/plugin-test.js
开始转换插件: packages/ui/certd-server/src/plugins/plugin-demo/plugins/plugin-test.js
插件模块导出了 1 个对象: DemoTest
处理插件: DemoTest
插件类型: deploy
脚本路径: packages/ui/certd-server/src/plugins/plugin-demo/plugins/plugin-test.js
生成的 YAML 配置:
name: DemoTest
title: Demo-测试插件
desc: ""
icon: clarity:plugin-line
group: other
default:
strategy:
runStrategy: SkipWhenSucceed
pluginType: deploy
type: builtIn
scriptFilePath: packages/ui/certd-server/src/plugins/plugin-demo/plugins/plugin-test.js
YAML 配置已保存到: ./metadata/deploy_DemoTest.yaml
插件转换完成!
```
## 故障排除
### 常见问题
1. **模块加载失败**
- 原因:插件文件依赖未安装或路径错误
- 解决:确保已安装所有依赖,检查文件路径是否正确
2. **插件类型识别失败**
- 原因:插件未正确继承基类或缺少必要的属性
- 解决:检查插件代码,确保正确继承对应的基类
3. **YAML 配置生成失败**
- 原因:插件定义格式不正确
- 解决:检查插件的 `define` 属性格式是否正确
4. **配置文件保存失败**
- 原因:权限不足或磁盘空间不足
- 解决:确保有足够的权限和磁盘空间
### 调试建议
- **查看详细日志**:工具会输出详细的转换过程日志,仔细查看日志信息
- **检查插件代码**:确保插件代码符合 Certd 插件开发规范
- **尝试简化插件**:如果转换失败,尝试创建一个最小化的插件示例进行测试
- **检查依赖版本**:确保使用的依赖版本与 Certd 兼容
## 总结
插件转换工具是一个方便实用的工具,它可以帮助开发者快速生成插件的 YAML 配置文件,简化插件的注册和管理过程。通过命令行参数指定单个插件文件,工具会自动完成类型识别、配置生成和保存等操作,大大提高了插件开发和管理的效率。
@@ -0,0 +1 @@
我需要将一个插件转换为 YAML 配置文件。请指导我如何使用插件转换工具。
@@ -0,0 +1,95 @@
# 插件转换工具使用指南
## 工具说明
插件转换工具用于将单个 Certd 插件转换为 YAML 配置文件,方便插件的注册和管理。
## 工具位置
`.trae/skills/plugin-converter/resources/convert-plugin-to-yaml.js`
## 使用方法
### 基本用法
```bash
node .trae/skills/plugin-converter/resources/convert-plugin-to-yaml.js <插件文件路径>
```
### 示例
#### 转换 Access 插件
```bash
node .trae/skills/plugin-converter/resources/convert-plugin-to-yaml.js packages/ui/certd-server/src/plugins/plugin-demo/access.js
```
#### 转换 Task 插件
```bash
node .trae/skills/plugin-converter/resources/convert-plugin-to-yaml.js packages/ui/certd-server/src/plugins/plugin-demo/plugins/plugin-test.js
```
#### 转换 DNS Provider 插件
```bash
node .trae/skills/plugin-converter/resources/convert-plugin-to-yaml.js packages/ui/certd-server/src/plugins/plugin-demo/dns-provider.js
```
## 转换过程
1. **加载插件模块**:使用 `import()` 动态加载指定的插件文件
2. **分析插件定义**:检查模块导出的对象,寻找带有 `define` 属性的插件
3. **识别插件类型**:根据插件的继承关系或属性识别插件类型
4. **生成 YAML 配置**:基于插件定义生成标准的 YAML 配置
5. **保存配置文件**:将生成的配置保存到 `./metadata` 目录
## 输出说明
### 命令行输出
执行转换命令后,工具会输出以下信息:
- 插件加载状态
- 插件导出的对象列表
- 插件类型识别结果
- 生成的 YAML 配置内容
- 配置文件保存路径
### 配置文件命名规则
生成的配置文件命名规则为:
```
<插件类型>[_<子类型>]_<插件名称>.yaml
```
例如:
- `access_demo.yaml`Access 插件)
- `deploy_DemoTest.yaml`Task 插件)
- `dnsProvider_demo.yaml`DNS Provider 插件)
## 示例输出
### 转换 Access 插件示例
```bash
$ node .trae/skills/plugin-converter/resources/convert-plugin-to-yaml.js packages/ui/certd-server/src/plugins/plugin-demo/access.js
开始转换插件: packages/ui/certd-server/src/plugins/plugin-demo/access.js
插件模块导出了 1 个对象: DemoAccess
处理插件: DemoAccess
插件类型: access
脚本路径: packages/ui/certd-server/src/plugins/plugin-demo/access.js
生成的 YAML 配置:
name: demo
title: 授权插件示例
desc: 这是一个示例授权插件,用于演示如何实现一个授权插件
icon: clarity:plugin-line
pluginType: access
type: builtIn
scriptFilePath: packages/ui/certd-server/src/plugins/plugin-demo/access.js
YAML 配置已保存到: ./metadata/access_demo.yaml
插件转换完成!
```
@@ -0,0 +1,160 @@
// 转换单个插件为 YAML 配置的技能脚本
import path from "path";
import fs from "fs";
import { pathToFileURL } from "node:url";
import * as yaml from "js-yaml";
import { AbstractTaskPlugin, BaseAccess, BaseNotification} from "@certd/pipeline";
import { BaseAddon} from "@certd/lib-server";
/**
* 检查对象是否是指定类的原型
*/
function isPrototypeOf(value, cls) {
return cls.prototype.isPrototypeOf(value.prototype);
}
/**
* 加载单个插件模块
*/
async function loadSingleModule(filePath) {
try {
// 转换为 file:// URLWindows 必需)
const moduleUrl = pathToFileURL(filePath).href;
const module = await import(moduleUrl);
return module.default || module;
} catch (err) {
console.error(`加载模块 ${filePath} 失败:`, err);
return null;
}
}
/**
* 分析单个插件并生成 YAML 配置
*/
async function convertSinglePlugin(pluginPath) {
console.log(`开始转换插件: ${pluginPath}`);
// 加载插件模块
const module = await loadSingleModule(pluginPath);
if (!module) {
console.error("加载插件失败,退出");
return;
}
// 处理模块中的所有导出
const entry = Object.entries(module);
if (entry.length === 0) {
console.error("插件模块没有导出任何内容");
return;
}
console.log(`插件模块导出了 ${entry.length} 个对象: ${entry.map(([name]) => name).join(", ")}`);
// 处理每个导出的对象
for (const [name, value] of entry) {
// 检查是否是插件(有 define 属性)
if (!value.define) {
console.log(`跳过非插件对象: ${name}`);
continue;
}
console.log(`处理插件: ${name}`);
// 构建插件定义
const pluginDefine = {
...value.define
};
let subType = "";
// 确定插件类型
if (pluginDefine.accessType) {
pluginDefine.pluginType = "dnsProvider";
} else if (isPrototypeOf(value, AbstractTaskPlugin)) {
pluginDefine.pluginType = "deploy";
} else if (isPrototypeOf(value, BaseNotification)) {
pluginDefine.pluginType = "notification";
} else if (isPrototypeOf(value, BaseAccess)) {
pluginDefine.pluginType = "access";
} else if (isPrototypeOf(value, BaseAddon)) {
pluginDefine.pluginType = "addon";
subType = "_" + (pluginDefine.addonType || "");
} else {
console.log(`[warning] 未知的插件类型:${pluginDefine.name}`);
continue;
}
pluginDefine.type = "builtIn";
// 计算脚本文件路径
const relativePath = path.relative(process.cwd(), pluginPath);
const scriptFilePath = relativePath.replace(/\\/g, "/").replace(/\.js$/, ".js");
pluginDefine.scriptFilePath = scriptFilePath;
console.log(`插件类型: ${pluginDefine.pluginType}`);
console.log(`脚本路径: ${scriptFilePath}`);
// 生成 YAML 配置
const yamlContent = yaml.dump(pluginDefine);
console.log("\n生成的 YAML 配置:");
console.log(yamlContent);
// 可选:保存到文件
const outputDir = "./metadata";
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
const outputFileName = `${pluginDefine.pluginType}${subType}_${pluginDefine.name}.yaml`;
const outputPath = path.join(outputDir, outputFileName);
fs.writeFileSync(outputPath, yamlContent, 'utf8');
console.log(`\nYAML 配置已保存到: ${outputPath}`);
return pluginDefine;
}
console.error("未找到有效的插件定义");
}
/**
* 主函数
*/
async function main() {
const args = process.argv.slice(2);
if (args.length === 0) {
console.error("请指定插件文件路径");
console.log("用法: node convert-plugin-to-yaml.js <插件文件路径>");
process.exit(1);
}
const pluginPath = args[0];
if (!fs.existsSync(pluginPath)) {
console.error(`插件文件不存在: ${pluginPath}`);
process.exit(1);
}
try {
await convertSinglePlugin(pluginPath);
console.log("\n插件转换完成!");
} catch (error) {
console.error("转换过程中出错:", error);
process.exit(1);
}
}
// 如果直接运行此脚本
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
main();
}
// 导出函数,以便其他模块使用
export {
convertSinglePlugin,
loadSingleModule,
isPrototypeOf
};
+388
View File
@@ -0,0 +1,388 @@
# Task 插件开发技能
## 什么是 Task 插件
Task 插件是 Certd 系统中的部署任务插件,它继承自 `AbstractTaskPlugin` 类,被流水线调用 `execute` 方法,将证书部署到对应的应用上。
## 开发步骤
### 1. 导入必要的依赖
```typescript
import { AbstractTaskPlugin, IsTaskPlugin, PageSearch, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { CertInfo, CertReader } from '@certd/plugin-cert';
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from '@certd/plugin-lib';
import { optionsUtils } from '@certd/basic';
import { CertApplyPluginNames} from '@certd/plugin-cert';
```
### 2. 使用 @IsTaskPlugin 注解注册插件
```typescript
@IsTaskPlugin({
// 命名规范,插件类型+功能,大写字母开头,驼峰命名
name: 'DemoTest',
title: 'Demo-测试插件', // 插件标题
icon: 'clarity:plugin-line', // 插件图标
// 插件分组
group: pluginGroups.other.key,
default: {
// 默认值配置照抄即可
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed,
},
},
})
// 类名规范,跟上面插件名称(name)一致
export class DemoTest extends AbstractTaskPlugin {
// 插件实现...
}
```
### 3. 定义任务输入参数
使用 `@TaskInput` 注解定义任务输入参数:
```typescript
// 测试参数
@TaskInput({
title: '属性示例',
value: '默认值',
component: {
// 前端组件配置,具体配置见组件文档 https://www.antdv.com/components/input-cn
name: 'a-input',
vModel: 'value', // 双向绑定组件的 props 名称
},
helper: '帮助说明,[链接](https://certd.docmirror.cn)',
required: false, // 是否必填
})
text!: string;
// 证书选择,此项必须要有
@TaskInput({
title: '域名证书',
helper: '请选择前置任务输出的域名证书',
component: {
name: 'output-selector',
from: [...CertApplyPluginNames],
},
// required: true, // 必填
})
cert!: CertInfo;
@TaskInput(createCertDomainGetterInputDefine({ props: { required: false } }))
// 前端可以展示,当前申请的证书域名列表
certDomains!: string[];
// 授权选择框
@TaskInput({
title: 'demo授权',
helper: 'demoAccess授权',
component: {
name: 'access-selector',
type: 'demo', // 固定授权类型
},
// rules: [{ required: true, message: '此项必填' }],
// required: true, // 必填
})
accessId!: string;
```
### 4. 实现插件方法
#### 4.1 插件实例化时执行的方法
```typescript
// 插件实例化时执行的方法
async onInstance() {}
```
#### 4.2 插件执行方法
```typescript
// 插件执行方法
async execute(): Promise<void> {
const { select, text, cert, accessId } = this;
try {
const access = await this.getAccess(accessId);
this.logger.debug('access', access);
} catch (e) {
this.logger.error('获取授权失败', e);
}
try {
const certReader = new CertReader(cert);
this.logger.debug('certReader', certReader);
} catch (e) {
this.logger.error('读取crt失败', e);
}
this.logger.info('DemoTestPlugin execute');
this.logger.info('text:', text);
this.logger.info('select:', select);
this.logger.info('switch:', this.switch);
this.logger.info('授权id:', accessId);
// 具体的部署逻辑
// ...
}
```
#### 4.3 后端获取选项方法
```typescript
@TaskInput(
createRemoteSelectInputDefine({
title: '从后端获取选项',
helper: '选择时可以从后端获取选项',
action: DemoTest.prototype.onGetSiteList.name,
// 当以下参数变化时,触发获取选项
watches: ['certDomains', 'accessId'],
required: true,
})
)
siteName!: string | string[];
// 从后端获取选项的方法
async onGetSiteList(req: PageSearch) {
if (!this.accessId) {
throw new Error('请选择Access授权');
}
// @ts-ignore
const access = await this.getAccess(this.accessId);
// const siteRes = await access.GetDomainList(req);
// 以下是模拟数据
const siteRes = [
{ id: 1, siteName: 'site1.com' },
{ id: 2, siteName: 'site2.com' },
{ id: 3, siteName: 'site2.com' },
];
// 转换为前端所需要的格式
const options = siteRes.map((item: any) => {
return {
value: item.siteName,
label: item.siteName,
domain: item.siteName,
};
});
// 将站点域名名称根据证书域名进行匹配分组,分成匹配的和不匹配的两组选项,返回给前端,供用户选择
return optionsUtils.buildGroupOptions(options, this.certDomains);
}
```
## 注意事项
1. **插件命名**:插件名称应遵循命名规范,大写字母开头,驼峰命名。
2. **类名规范**:类名应与插件名称(name)一致。
3. **证书选择**:必须包含证书选择参数,用于获取前置任务输出的域名证书。
4. **日志输出**:使用 `this.logger` 输出日志,而不是 `console`
5. **错误处理**:执行过程中的错误应被捕获并记录。
6. **授权获取**:使用 `this.getAccess(accessId)` 获取授权信息。
## 完整示例
```typescript
import { AbstractTaskPlugin, IsTaskPlugin, PageSearch, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { CertInfo, CertReader } from '@certd/plugin-cert';
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from '@certd/plugin-lib';
import { optionsUtils } from '@certd/basic';
import { CertApplyPluginNames} from '@certd/plugin-cert';
@IsTaskPlugin({
//命名规范,插件类型+功能(就是目录plugin-demo中的demo),大写字母开头,驼峰命名
name: 'DemoTest',
title: 'Demo-测试插件',
icon: 'clarity:plugin-line',
//插件分组
group: pluginGroups.other.key,
default: {
//默认值配置照抄即可
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed,
},
},
})
//类名规范,跟上面插件名称(name)一致
export class DemoTest extends AbstractTaskPlugin {
//测试参数
@TaskInput({
title: '属性示例',
value: '默认值',
component: {
//前端组件配置,具体配置见组件文档 https://www.antdv.com/components/input-cn
name: 'a-input',
vModel: 'value', //双向绑定组件的props名称
},
helper: '帮助说明,[链接](https://certd.docmirror.cn)',
required: false, //是否必填
})
text!: string;
//测试参数
@TaskInput({
title: '选择框',
component: {
//前端组件配置,具体配置见组件文档 https://www.antdv.com/components/select-cn
name: 'a-auto-complete',
vModel: 'value',
options: [
//选项列表
{ label: '动态显', value: 'show' },
{ label: '动态隐', value: 'hide' },
],
},
})
select!: string;
@TaskInput({
title: '动态显隐',
helper: '我会根据选择框的值进行显隐',
show: true, //动态计算的值会覆盖它
//动态计算脚本, mergeScript返回的对象会合并当前配置,此处演示 show的值会被动态计算结果覆盖,show的值根据用户选择的select的值决定
mergeScript: `
return {
show: ctx.compute(({form})=>{
return form.select === 'show';
})
}
`,
})
showText!: string;
//测试参数
@TaskInput({
title: '多选框',
component: {
//前端组件配置,具体配置见组件文档 https://www.antdv.com/components/select-cn
name: 'a-select',
vModel: 'value',
mode: 'tags',
multiple: true,
options: [
{ value: '1', label: '选项1' },
{ value: '2', label: '选项2' },
],
},
})
multiSelect!: string;
//测试参数
@TaskInput({
title: 'switch',
component: {
//前端组件配置,具体配置见组件文档 https://www.antdv.com/components/switch-cn
name: 'a-switch',
vModel: 'checked',
},
})
switch!: boolean;
//证书选择,此项必须要有
@TaskInput({
title: '域名证书',
helper: '请选择前置任务输出的域名证书',
component: {
name: 'output-selector',
from: [...CertApplyPluginNames],
},
// required: true, // 必填
})
cert!: CertInfo;
@TaskInput(createCertDomainGetterInputDefine({ props: { required: false } }))
//前端可以展示,当前申请的证书域名列表
certDomains!: string[];
//授权选择框
@TaskInput({
title: 'demo授权',
helper: 'demoAccess授权',
component: {
name: 'access-selector',
type: 'demo', //固定授权类型
},
// rules: [{ required: true, message: '此项必填' }],
// required: true, //必填
})
accessId!: string;
@TaskInput(
createRemoteSelectInputDefine({
title: '从后端获取选项',
helper: '选择时可以从后端获取选项',
action: DemoTest.prototype.onGetSiteList.name,
//当以下参数变化时,触发获取选项
watches: ['certDomains', 'accessId'],
required: true,
})
)
siteName!: string | string[];
//插件实例化时执行的方法
async onInstance() {}
//插件执行方法
async execute(): Promise<void> {
const { select, text, cert, accessId } = this;
try {
const access = await this.getAccess(accessId);
this.logger.debug('access', access);
} catch (e) {
this.logger.error('获取授权失败', e);
}
try {
const certReader = new CertReader(cert);
this.logger.debug('certReader', certReader);
} catch (e) {
this.logger.error('读取crt失败', e);
}
this.logger.info('DemoTestPlugin execute');
this.logger.info('text:', text);
this.logger.info('select:', select);
this.logger.info('switch:', this.switch);
this.logger.info('授权id:', accessId);
// const res = await this.http.request({
// url: 'https://api.demo.com',
// method: 'GET',
// });
// if (res.code !== 0) {
// //检查res是否报错,你需要抛异常,来结束插件执行,否则会判定为执行成功,下次执行时会跳过本任务
// throw new Error(res.message);
// }
// this.logger.info('部署成功:', res);
}
//此方法演示,如何让前端在添加插件时可以从后端获取选项,这里是后端返回选项的方法
async onGetSiteList(req: PageSearch) {
if (!this.accessId) {
throw new Error('请选择Access授权');
}
// @ts-ignore
const access = await this.getAccess(this.accessId);
// const siteRes = await access.GetDomainList(req);
//以下是模拟数据
const siteRes = [
{ id: 1, siteName: 'site1.com' },
{ id: 2, siteName: 'site2.com' },
{ id: 3, siteName: 'site2.com' },
];
//转换为前端所需要的格式
const options = siteRes.map((item: any) => {
return {
value: item.siteName,
label: item.siteName,
domain: item.siteName,
};
});
//将站点域名名称根据证书域名进行匹配分组,分成匹配的和不匹配的两组选项,返回给前端,供用户选择
return optionsUtils.buildGroupOptions(options, this.certDomains);
}
}
```
@@ -0,0 +1 @@
我需要开发一个 Task 插件,用于将申请的证书部署到指定的应用系统中。请指导我如何实现。
@@ -0,0 +1,129 @@
# Task 插件开发指南
## 开发步骤
### 1. 导入必要的依赖
```typescript
import { AbstractTaskPlugin, IsTaskPlugin, PageSearch, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { CertInfo, CertReader } from '@certd/plugin-cert';
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from '@certd/plugin-lib';
import { optionsUtils } from '@certd/basic';
import { CertApplyPluginNames} from '@certd/plugin-cert';
```
### 2. 使用 @IsTaskPlugin 注解注册插件
```typescript
@IsTaskPlugin({
// 命名规范,插件类型+功能,大写字母开头,驼峰命名
name: 'DemoTest',
title: 'Demo-测试插件', // 插件标题
icon: 'clarity:plugin-line', // 插件图标
// 插件分组
group: pluginGroups.other.key,
default: {
// 默认值配置照抄即可
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed,
},
},
})
// 类名规范,跟上面插件名称(name)一致
export class DemoTest extends AbstractTaskPlugin {
// 插件实现...
}
```
### 3. 定义任务输入参数
使用 `@TaskInput` 注解定义任务输入参数:
```typescript
// 测试参数
@TaskInput({
title: '属性示例',
value: '默认值',
component: {
//前端组件配置,具体配置见组件文档 https://www.antdv.com/components/input-cn
name: 'a-input',
vModel: 'value', //双向绑定组件的props名称
},
helper: '帮助说明,[链接](https://certd.docmirror.cn)',
required: false, //是否必填
})
text!: string;
//证书选择,此项必须要有
@TaskInput({
title: '域名证书',
helper: '请选择前置任务输出的域名证书',
component: {
name: 'output-selector',
from: [...CertApplyPluginNames],
},
// required: true, // 必填
})
cert!: CertInfo;
@TaskInput(createCertDomainGetterInputDefine({ props: { required: false } }))
//前端可以展示,当前申请的证书域名列表
certDomains!: string[];
//授权选择框
@TaskInput({
title: 'demo授权',
helper: 'demoAccess授权',
component: {
name: 'access-selector',
type: 'demo', //固定授权类型
},
// rules: [{ required: true, message: '此项必填' }],
// required: true, //必填
})
accessId!: string;
```
### 4. 实现插件方法
```typescript
//插件实例化时执行的方法
async onInstance() {}
//插件执行方法
async execute(): Promise<void> {
const { select, text, cert, accessId } = this;
try {
const access = await this.getAccess(accessId);
this.logger.debug('access', access);
} catch (e) {
this.logger.error('获取授权失败', e);
}
try {
const certReader = new CertReader(cert);
this.logger.debug('certReader', certReader);
} catch (e) {
this.logger.error('读取crt失败', e);
}
this.logger.info('DemoTestPlugin execute');
this.logger.info('text:', text);
this.logger.info('select:', select);
this.logger.info('switch:', this.switch);
this.logger.info('授权id:', accessId);
// 具体的部署逻辑
// ...
}
```
## 注意事项
1. **插件命名**:插件名称应遵循命名规范,大写字母开头,驼峰命名。
2. **类名规范**:类名应与插件名称(name)一致。
3. **证书选择**:必须包含证书选择参数,用于获取前置任务输出的域名证书。
4. **日志输出**:使用 `this.logger` 输出日志,而不是 `console`
5. **错误处理**:执行过程中的错误应被捕获并记录。
6. **授权获取**:使用 `this.getAccess(accessId)` 获取授权信息。
+9 -9
View File
@@ -15,25 +15,25 @@
},
"scripts": {
"start": "lerna bootstrap --hoist",
"start:server": "cd ./packages/ui/certd-server && npm start",
"start:server": "cd ./packages/ui/certd-server && pnpm start",
"devb": "lerna run dev-build",
"i-all": "lerna link && lerna exec npm install ",
"publish": "npm run prepublishOnly2 && lerna publish --force-publish=pro/plus-core --conventional-commits && npm run afterpublishOnly ",
"afterpublishOnly": "npm run copylogs && time /t >trigger/build.trigger && git add ./trigger/build.trigger && git commit -m \"build: trigger build image\" && TIMEOUT /T 10 && npm run commitAll",
"publish": "pnpm run prepublishOnly2 && lerna publish --force-publish=pro/plus-core --conventional-commits && pnpm run afterpublishOnly ",
"afterpublishOnly": "pnpm run copylogs && time /t >trigger/build.trigger && git add ./trigger/build.trigger && git commit -m \"build: trigger build image\" && TIMEOUT /T 10 && pnpm run commitAll",
"transform-sql": "cd ./packages/ui/certd-server/db/ && node --experimental-json-modules transform.js",
"plugin-doc-gen": "cd ./packages/ui/certd-server/ && npm run export-metadata",
"commitAll": "git add . && git commit -m \"build: publish\" && git push && npm run commitPro",
"plugin-doc-gen": "cd ./packages/ui/certd-server/ && pnpm run export-metadata",
"commitAll": "git add . && git commit -m \"build: publish\" && git push && pnpm run commitPro",
"commitPro": "cd ./packages/pro/ && git add . && git commit -m \"build: publish\" && git push",
"copylogs": "copyfiles \"CHANGELOG.md\" ./docs/guide/changelogs/",
"prepublishOnly1": "npm run check && lerna run build ",
"prepublishOnly2": "npm run check && npm run before-build && lerna run build && npm run plugin-doc-gen",
"before-build": "npm run transform-sql && cd ./packages/core/basic && time /t >build.md && git add ./build.md && git commit -m \"build: prepare to build\"",
"prepublishOnly1": "pnpm run check && lerna run build ",
"prepublishOnly2": "pnpm run check && pnpm run before-build && lerna run build && pnpm run plugin-doc-gen",
"before-build": "pnpm run transform-sql && cd ./packages/core/basic && time /t >build.md && git add ./build.md && git commit -m \"build: prepare to build\"",
"deploy1": "node --experimental-json-modules ./scripts/deploy.js ",
"check": "node --experimental-json-modules ./scripts/publish-check.js",
"init": "lerna run build",
"init:dev": "lerna run build",
"docs:dev": "vitepress dev docs",
"docs:build": "npm run copylogs && vitepress build docs",
"docs:build": "pnpm run copylogs && vitepress build docs",
"docs:preview": "vitepress preview docs",
"pub": "echo 1",
"dev": "pnpm run -r --parallel compile ",
+1 -1
View File
@@ -53,7 +53,7 @@
"prepublishOnly": "npm run build-docs",
"test": "mocha -t 60000 \"test/setup.js\" \"test/**/*.spec.js\"",
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch"
"compile": "echo '1'"
},
"repository": {
"type": "git",
@@ -323,6 +323,7 @@ export function createAgent(opts: CreateAgentOptions = {}) {
{
autoSelectFamily: true,
autoSelectFamilyAttemptTimeout: 1000,
connectTimeout: 5000, // 连接建立超时
},
opts
);
+3
View File
@@ -123,6 +123,9 @@ export type TaskInstanceContext = {
//用户信息
user: UserInfo;
//项目id
projectId?: number;
emitter: TaskEmitter;
//service 容器
@@ -1,11 +1,17 @@
import { Inject } from '@midwayjs/core';
import { ApplicationContext, Inject } from '@midwayjs/core';
import type {IMidwayContainer} from '@midwayjs/core';
import * as koa from '@midwayjs/koa';
import { Constants } from './constants.js';
import { isEnterprise } from './mode.js';
export abstract class BaseController {
@Inject()
ctx: koa.Context;
@ApplicationContext()
applicationContext: IMidwayContainer;
/**
* 成功返回
* @param data 返回数据
@@ -55,4 +61,69 @@ export abstract class BaseController {
}
}
async getProjectId(permission:string) {
if (!isEnterprise()) {
return null
}
let projectIdStr = this.ctx.headers["project-id"] as string;
if (!projectIdStr){
projectIdStr = this.ctx.request.query["projectId"] as string;
}
if (!projectIdStr){
return null
}
if (!projectIdStr) {
throw new Error("projectId 不能为空")
}
const userId = this.getUserId()
const projectId = parseInt(projectIdStr)
await this.checkProjectPermission(userId, projectId,permission)
return projectId;
}
async getProjectUserId(permission:string){
let userId = this.getUserId()
const projectId = await this.getProjectId(permission)
if(projectId){
userId = 0
}
return {
projectId,userId
}
}
async getProjectUserIdRead(){
return await this.getProjectUserId("read")
}
async getProjectUserIdWrite(){
return await this.getProjectUserId("write")
}
async getProjectUserIdAdmin(){
return await this.getProjectUserId("admin")
}
async checkProjectPermission(userId: number, projectId: number,permission:string) {
const projectService:any = await this.applicationContext.getAsync("projectService");
await projectService.checkPermission({userId,projectId,permission})
}
/**
*
* @param service 检查记录是否属于某用户或某项目
* @param id
*/
async checkOwner(service:any,id:number,permission:string,allowAdmin:boolean = false){
let { projectId,userId } = await this.getProjectUserId(permission)
const authService:any = await this.applicationContext.getAsync("authService");
if (projectId) {
await authService.checkProjectId(service, id, projectId);
}else{
if(allowAdmin){
await authService.checkUserIdButAllowAdmin(this.ctx, service, id);
}else{
await authService.checkUserId(this.ctx, service, id);
}
}
return {projectId,userId}
}
}
@@ -206,30 +206,41 @@ export abstract class BaseService<T> {
return await qb.getMany();
}
async checkUserId(id: any = 0, userId: number, userKey = 'userId') {
const res = await this.getRepository().findOne({
async checkUserId(ids: number | number[] = 0, userId: number, userKey = 'userId') {
if (ids == null) {
throw new ValidateException('id不能为空');
}
if (userId == null) {
throw new ValidateException('userId不能为空');
}
if (!Array.isArray(ids)) {
ids = [ids];
}
const res = await this.getRepository().find({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
select: { [userKey]: true },
where: {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
id,
id: In(ids),
[userKey]: userId,
},
});
if (!res || res[userKey] === userId) {
if (!res || res.length === ids.length) {
return;
}
throw new PermissionException('权限不足');
}
async batchDelete(ids: number[], userId: number) {
if(userId >0){
async batchDelete(ids: number[], userId: number,projectId?:number) {
if(userId!=null){
const list = await this.getRepository().find({
where: {
// @ts-ignore
id: In(ids),
userId,
projectId,
},
})
// @ts-ignore
@@ -242,4 +253,19 @@ export abstract class BaseService<T> {
async findOne(options: FindOneOptions<T>) {
return await this.getRepository().findOne(options);
}
}
export function checkUserProjectParam(userId: number, projectId: number) {
if (projectId != null ){
if( userId !==0) {
throw new ValidateException('userId projectId 错误');
}
return true
}else{
if( userId > 0) {
return true
}
throw new ValidateException('userId不能为空');
}
}
@@ -5,3 +5,4 @@ export * from './enum-item.js';
export * from './exception/index.js';
export * from './result.js';
export * from './base-service.js';
export * from "./mode.js"
@@ -0,0 +1,12 @@
let adminMode = "saas"
export function setAdminMode(mode:string = "saas"){
adminMode = mode
}
export function getAdminMode(){
return adminMode
}
export function isEnterprise(){
return adminMode === "enterprise"
}
@@ -65,6 +65,8 @@ export class SysPublicSettings extends BaseSettings {
}> = {};
notice?: string;
adminMode?: "enterprise" | "saas" = "saas";
}
export class SysPrivateSettings extends BaseSettings {
@@ -7,9 +7,10 @@ import { BaseSettings, SysInstallInfo, SysPrivateSettings, SysPublicSettings, Sy
import { getAllSslProviderDomains, setSslProviderReverseProxies } from '@certd/acme-client';
import { cache, logger, mergeUtils, setGlobalProxy } from '@certd/basic';
import * as dns from 'node:dns';
import { BaseService } from '../../../basic/index.js';
import { BaseService, setAdminMode } from '../../../basic/index.js';
import { executorQueue } from '../../basic/service/executor-queue.js';
const {merge} = mergeUtils;
import { isComm } from '@certd/plus-core';
const { merge } = mergeUtils;
/**
* 设置
*/
@@ -116,7 +117,15 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
}
async savePublicSettings(bean: SysPublicSettings) {
if(isComm()){
if(bean.adminMode === 'enterprise'){
throw new Error("商业版不支持使用企业管理模式")
}
}
await this.saveSetting(bean);
//让设置生效
await this.reloadPublicSettings();
}
async getPrivateSettings(): Promise<SysPrivateSettings> {
@@ -137,23 +146,33 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
await this.reloadPrivateSettings();
}
async reloadSettings() {
await this.reloadPrivateSettings()
await this.reloadPublicSettings()
}
async reloadPublicSettings() {
const publicSetting = await this.getPublicSettings()
setAdminMode(publicSetting.adminMode)
}
async reloadPrivateSettings() {
const bean = await this.getPrivateSettings();
const privateSetting = await this.getPrivateSettings();
const opts = {
httpProxy: bean.httpProxy,
httpsProxy: bean.httpsProxy,
httpProxy: privateSetting.httpProxy,
httpsProxy: privateSetting.httpsProxy,
};
setGlobalProxy(opts);
if (bean.dnsResultOrder) {
dns.setDefaultResultOrder(bean.dnsResultOrder as any);
if (privateSetting.dnsResultOrder) {
dns.setDefaultResultOrder(privateSetting.dnsResultOrder as any);
}
if (bean.pipelineMaxRunningCount){
executorQueue.setMaxRunningCount(bean.pipelineMaxRunningCount);
if (privateSetting.pipelineMaxRunningCount) {
executorQueue.setMaxRunningCount(privateSetting.pipelineMaxRunningCount);
}
setSslProviderReverseProxies(bean.reverseProxies);
setSslProviderReverseProxies(privateSetting.reverseProxies);
}
async updateByKey(key: string, setting: any) {
@@ -21,6 +21,9 @@ export class AccessEntity {
@Column({ name: 'encrypt_setting', comment: '已加密设置', length: 10240, nullable: true })
encryptSetting: string;
@Column({ name: 'project_id', comment: '项目id' })
projectId: number;
@Column({
name: 'create_time',
comment: '创建时间',
@@ -2,17 +2,19 @@ import { IAccessService } from '@certd/pipeline';
export class AccessGetter implements IAccessService {
userId: number;
getter: <T>(id: any, userId?: number) => Promise<T>;
constructor(userId: number, getter: (id: any, userId: number) => Promise<any>) {
projectId?: number;
getter: <T>(id: any, userId?: number, projectId?: number) => Promise<T>;
constructor(userId: number, projectId: number, getter: (id: any, userId: number, projectId?: number) => Promise<any>) {
this.userId = userId;
this.projectId = projectId;
this.getter = getter;
}
async getById<T = any>(id: any) {
return await this.getter<T>(id, this.userId);
return await this.getter<T>(id, this.userId, this.projectId);
}
async getCommonById<T = any>(id: any) {
return await this.getter<T>(id, 0);
return await this.getter<T>(id, 0,null);
}
}
@@ -129,10 +129,11 @@ export class AccessService extends BaseService<AccessEntity> {
id: entity.id,
name: entity.name,
userId: entity.userId,
projectId: entity.projectId,
};
}
async getAccessById(id: any, checkUserId: boolean, userId?: number): Promise<any> {
async getAccessById(id: any, checkUserId: boolean, userId?: number, projectId?: number): Promise<any> {
const entity = await this.info(id);
if (entity == null) {
throw new Error(`该授权配置不存在,请确认是否已被删除:id=${id}`);
@@ -145,6 +146,9 @@ export class AccessService extends BaseService<AccessEntity> {
throw new PermissionException('您对该Access授权无访问权限');
}
}
if (projectId != null && projectId !== entity.projectId) {
throw new PermissionException('您对该Access授权无访问权限');
}
// const access = accessRegistry.get(entity.type);
const setting = this.decryptAccessEntity(entity);
@@ -152,12 +156,12 @@ export class AccessService extends BaseService<AccessEntity> {
id: entity.id,
...setting,
};
const accessGetter = new AccessGetter(userId, this.getById.bind(this));
const accessGetter = new AccessGetter(userId,projectId, this.getById.bind(this));
return await newAccess(entity.type, input,accessGetter);
}
async getById(id: any, userId: number): Promise<any> {
return await this.getAccessById(id, true, userId);
async getById(id: any, userId: number, projectId?: number): Promise<any> {
return await this.getAccessById(id, true, userId, projectId);
}
decryptAccessEntity(entity: AccessEntity): any {
@@ -188,23 +192,25 @@ export class AccessService extends BaseService<AccessEntity> {
}
async getSimpleByIds(ids: number[], userId: any) {
async getSimpleByIds(ids: number[], userId: any, projectId?: number) {
if (ids.length === 0) {
return [];
}
if (!userId) {
if (userId==null) {
return [];
}
return await this.repository.find({
where: {
id: In(ids),
userId,
projectId,
},
select: {
id: true,
name: true,
type: true,
userId:true
userId:true,
projectId:true,
},
});
@@ -28,6 +28,9 @@ export class AddonEntity {
@Column({ name: 'is_default', comment: '是否默认', nullable: false, default: false })
isDefault: boolean;
@Column({ name: 'project_id', comment: '项目id' })
projectId: number;
@Column({
name: 'create_time',
@@ -70,7 +70,8 @@ export class AddonService extends BaseService<AddonEntity> {
name: entity.name,
userId: entity.userId,
addonType: entity.addonType,
type: entity.type
type: entity.type,
projectId: entity.projectId
};
}
@@ -84,17 +85,18 @@ export class AddonService extends BaseService<AddonEntity> {
}
async getSimpleByIds(ids: number[], userId: any) {
async getSimpleByIds(ids: number[], userId: any,projectId?:number) {
if (ids.length === 0) {
return [];
}
if (!userId) {
if (userId==null) {
return [];
}
return await this.repository.find({
where: {
id: In(ids),
userId
userId,
projectId
},
select: {
id: true,
@@ -109,11 +111,12 @@ export class AddonService extends BaseService<AddonEntity> {
}
async getDefault(userId: number, addonType: string): Promise<any> {
async getDefault(userId: number, addonType: string,projectId?:number): Promise<any> {
const res = await this.repository.findOne({
where: {
userId,
addonType
addonType,
projectId
},
order: {
isDefault: "DESC"
@@ -133,21 +136,23 @@ export class AddonService extends BaseService<AddonEntity> {
type: res.type,
name: res.name,
userId: res.userId,
setting
setting,
projectId: res.projectId
};
}
async setDefault(id: number, userId: number, addonType: string) {
async setDefault(id: number, userId: number, addonType: string,projectId?:number) {
if (!id) {
throw new ValidateException("id不能为空");
}
if (!userId) {
if (userId==null) {
throw new ValidateException("userId不能为空");
}
await this.repository.update(
{
userId,
addonType
addonType,
projectId
},
{
isDefault: false
@@ -157,7 +162,8 @@ export class AddonService extends BaseService<AddonEntity> {
{
id,
userId,
addonType
addonType,
projectId
},
{
isDefault: true
@@ -165,12 +171,12 @@ export class AddonService extends BaseService<AddonEntity> {
);
}
async getOrCreateDefault(opts: { addonType: string, type: string, inputs: any, userId: any }) {
const { addonType, type, inputs, userId } = opts;
async getOrCreateDefault(opts: { addonType: string, type: string, inputs: any, userId: any,projectId?:number }) {
const { addonType, type, inputs, userId,projectId } = opts;
const addonDefine = this.getDefineByType(type, addonType);
const defaultConfig = await this.getDefault(userId, addonType);
const defaultConfig = await this.getDefault(userId, addonType,projectId);
if (defaultConfig) {
return defaultConfig;
}
@@ -183,17 +189,19 @@ export class AddonService extends BaseService<AddonEntity> {
type: type,
name: addonDefine.title,
setting: JSON.stringify(setting),
isDefault: true
isDefault: true,
projectId
});
return this.buildAddonInstanceConfig(res);
}
async getOneByType(req:{addonType:string,type:string,userId:number}) {
async getOneByType(req:{addonType:string,type:string,userId:number,projectId?:number}) {
return await this.repository.findOne({
where: {
addonType: req.addonType,
type: req.type,
userId: req.userId
userId: req.userId,
projectId: req.projectId
}
});
}
+8 -1
View File
@@ -3,6 +3,7 @@ import { get } from "lodash-es";
import { errorLog, errorCreate } from "./tools";
import { env } from "/src/utils/util.env";
import { useUserStore } from "/@/store/user";
import { useProjectStore } from "../store/project";
export class CodeError extends Error {
code: number;
@@ -138,11 +139,17 @@ function createRequestFunction(service: any) {
const configDefault = {
headers: {
"Content-Type": get(config, "headers.Content-Type", "application/json"),
},
} as any,
timeout: 30000,
baseURL: env.API,
data: {},
};
const projectStore = useProjectStore();
if (projectStore.isEnterprise && !config.url.startsWith("/sys")) {
configDefault.headers["project-id"] = projectStore.currentProjectId;
}
const userStore = useUserStore();
const token = userStore.getToken;
if (token != null) {
@@ -16,6 +16,7 @@ import { defineAsyncComponent } from "vue";
import NotificationSelector from "../views/certd/notification/notification-selector/index.vue";
import EmailSelector from "./email-selector/index.vue";
import ValidTimeFormat from "./valid-time-format.vue";
import ProjectSelector from "./project-selector/index.vue";
export default {
install(app: any) {
app.component(
@@ -45,5 +46,6 @@ export default {
app.component("ExpiresTimeText", ExpiresTimeText);
app.use(vip);
app.use(Plugins);
app.component("ProjectSelector", ProjectSelector);
},
};
@@ -83,6 +83,7 @@ const props = defineProps<{
search?: boolean;
pager?: boolean;
value?: any[];
open?: boolean;
}>();
const emit = defineEmits<{
@@ -0,0 +1,9 @@
import { request } from "/src/api/service";
export async function MyProjectList() {
return await request({
url: "/enterprise/project/list",
method: "post",
data: {},
});
}
@@ -0,0 +1,45 @@
<template>
<a-dropdown class="project-selector">
<template #overlay>
<a-menu @click="handleMenuClick">
<a-menu-item v-for="item in projectStore.myProjects" :key="item.id">
{{ item.name }}
</a-menu-item>
</a-menu>
</template>
<div class="rounded pl-3 pr-3 px-2 py-1 flex-center flex pointer items-center bg-accent h-10 button-text" title="当前项目">
<fs-icon icon="ion:apps" class="mr-1"></fs-icon>
当前项目{{ projectStore.currentProject?.name || "..." }}
<fs-icon icon="ion:chevron-down-outline" class="ml-1"></fs-icon>
</div>
</a-dropdown>
</template>
<script lang="ts" setup>
import { onMounted } from "vue";
import { useProjectStore } from "/@/store/project";
defineOptions({
name: "ProjectSelector",
});
const projectStore = useProjectStore();
onMounted(async () => {
await projectStore.reload();
});
function handleMenuClick({ key }: any) {
projectStore.changeCurrentProject(key);
window.location.reload();
}
</script>
<style lang="less">
.project-selector {
&.button-text {
min-width: 150px;
max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
</style>
@@ -10,6 +10,7 @@ import PageFooter from "./components/footer/index.vue";
import { useRouter } from "vue-router";
import MaxKBChat from "/@/components/ai/index.vue";
import { useI18n } from "vue-i18n";
import { useProjectStore } from "../store/project";
const { t } = useI18n();
@@ -77,10 +78,13 @@ const openChat = (q: string) => {
chatBox.value.openChat({ q });
};
provide("fn:ai.open", openChat);
const projectStore = useProjectStore();
</script>
<template>
<BasicLayout @clear-preferences-and-logout="handleLogout">
<template #header-left-0> </template>
<template #user-dropdown>
<UserDropdown :avatar="avatar" :menus="menus" :text="userStore.userInfo?.nickName || userStore.userInfo?.username" description="" tag-text="" @logout="handleLogout" />
</template>
@@ -88,13 +92,16 @@ provide("fn:ai.open", openChat);
<LockScreen :avatar @to-login="handleLogout" />
</template>
<template #header-right-0>
<div v-if="projectStore.isEnterprise" class="ml-1 mr-2">
<project-selector class="flex-center header-btn" />
</div>
<div class="hover:bg-accent ml-1 mr-2 cursor-pointer rounded-full hidden md:block">
<tutorial-button class="flex-center header-btn" mode="nav" />
</div>
<div class="hover:bg-accent ml-1 mr-2 cursor-pointer rounded-full">
<vip-button class="flex-center header-btn" mode="nav" />
</div>
<div v-if="!settingStore.isComm" class="hover:bg-accent ml-1 mr-2 cursor-pointer rounded-full">
<div v-if="!settingStore.isComm" class="hover:bg-accent ml-1 mr-2 cursor-pointer rounded-full hidden md:block">
<fs-button shape="circle" type="text" icon="ion:logo-github" :text="null" @click="goGithub" />
</div>
</template>
@@ -161,6 +161,8 @@ export default {
triggerType: "Trigger Type",
pipelineId: "Pipeline Id",
nextRunTime: "Next Run Time",
projectName: "Project",
adminId: "Admin",
},
pi: {
@@ -209,6 +211,12 @@ export default {
orderManager: "Order Management",
userSuites: "User Suites",
netTest: "Network Test",
enterpriseSetting: "Enterprise Settings",
projectManager: "Project Management",
projectUserManager: "Project User Management",
myProjectManager: "My Projects",
myProjectDetail: "Project Detail",
},
certificateRepo: {
title: "Certificate Repository",
@@ -787,6 +795,10 @@ export default {
pipelineSetting: "Pipeline Settings",
oauthSetting: "OAuth2 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",
enterpriseMode: "Enterprise Mode",
saasMode: "SaaS Mode",
showRunStrategy: "Show RunStrategy",
showRunStrategyHelper: "Allow modify the run strategy of the task",
@@ -865,6 +877,18 @@ export default {
fromType: "From Type",
expirationDate: "Expiration Date",
},
ent: {
projectName: "Project Name",
projectDescription: "Project Description",
projectDetailManager: "Project Detail",
projectDetailDescription: "Manage Project Members",
projectPermission: "Permission",
permission: {
read: "Read",
write: "Write",
admin: "Admin",
},
},
addonSelector: {
select: "Select",
placeholder: "select please",
@@ -168,6 +168,8 @@ export default {
triggerType: "触发类型",
pipelineId: "流水线Id",
nextRunTime: "下次运行时间",
projectName: "项目",
adminId: "管理员",
},
pi: {
validTime: "流水线有效期",
@@ -215,6 +217,12 @@ export default {
orderManager: "订单管理",
userSuites: "用户套餐",
netTest: "网络测试",
enterpriseManager: "企业管理设置",
projectManager: "项目管理",
projectDetail: "项目详情",
enterpriseSetting: "企业设置",
myProjectManager: "我的项目",
myProjectDetail: "项目详情",
},
certificateRepo: {
title: "证书仓库",
@@ -304,6 +312,8 @@ export default {
days: "天",
lastCheckTime: "上次检查时间",
disabled: "禁用启用",
ipAddress: "IP地址",
ipAddressHelper: "填写则固定检查此IP,不从DNS获取域名的IP地址",
ipCheck: "开启IP检查",
ipCheckHelper: "开启后,会检查IP(或源站)上的证书有效期",
ipSyncAuto: "自动同步IP",
@@ -794,6 +804,12 @@ export default {
pipelineSetting: "流水线设置",
oauthSetting: "第三方登录",
networkSetting: "网络设置",
adminModeSetting: "管理模式",
adminModeHelper: "企业管理模式: 企业内部使用,通过项目来隔离权限,流水线、授权数据属于项目。\nsaas模式:供外部用户注册使用,各个用户之间数据隔离,流水线、授权数据属于用户。",
adminMode: "管理模式",
enterpriseMode: "企业模式",
saasMode: "SaaS模式",
showRunStrategy: "显示运行策略选择",
showRunStrategyHelper: "任务设置中是否允许选择运行策略",
@@ -884,4 +900,16 @@ export default {
select: "选择",
placeholder: "请选择",
},
ent: {
projectName: "项目名称",
projectDescription: "项目描述",
projectDetailManager: "项目详情",
projectDetailDescription: "管理项目成员",
projectPermission: "权限",
permission: {
read: "读取",
write: "写入",
admin: "管理员",
},
},
};
@@ -1,6 +1,7 @@
import { useSettingStore } from "/@/store/settings";
import aboutResource from "/@/router/source/modules/about";
import i18n from "/@/locales/i18n";
import { useProjectStore } from "/@/store/project";
export const certdResources = [
{
@@ -14,6 +15,33 @@ export const certdResources = [
order: 0,
},
children: [
{
title: "certd.sysResources.myProjectManager",
name: "MyProjectManager",
path: "/certd/project",
component: "/certd/project/index.vue",
meta: {
show: () => {
const projectStore = useProjectStore();
return projectStore.isEnterprise;
},
icon: "ion:apps",
permission: "sys:settings:edit",
keepAlive: true,
},
},
{
title: "certd.sysResources.myProjectDetail",
name: "MyProjectDetail",
path: "/certd/project/detail",
component: "/certd/project/detail/index.vue",
meta: {
isMenu: false,
show: true,
icon: "ion:apps",
permission: "sys:settings:edit",
},
},
{
title: "certd.pipeline",
name: "PipelineManager",
@@ -40,6 +40,30 @@ export const sysResources = [
permission: "sys:settings:view",
},
},
{
title: "certd.sysResources.projectManager",
name: "ProjectManager",
path: "/sys/enterprise/project",
component: "/sys/enterprise/project/index.vue",
meta: {
show: true,
icon: "ion:apps",
permission: "sys:settings:edit",
keepAlive: true,
},
},
{
title: "certd.sysResources.projectDetail",
name: "ProjectDetail",
path: "/sys/enterprise/project/detail",
component: "/sys/enterprise/project/detail/index.vue",
meta: {
isMenu: false,
show: true,
icon: "ion:apps",
permission: "sys:settings:edit",
},
},
{
title: "certd.sysResources.cnameSetting",
name: "CnameSetting",
@@ -187,7 +211,6 @@ export const sysResources = [
keepAlive: true,
},
},
{
title: "certd.sysResources.suiteManager",
name: "SuiteManager",
@@ -0,0 +1,9 @@
import { request } from "/src/api/service";
export async function MyProjectList() {
return await request({
url: "/enterprise/project/list",
method: "post",
data: {},
});
}
@@ -0,0 +1,92 @@
import { defineStore } from "pinia";
import * as api from "./api";
import { message } from "ant-design-vue";
import { computed, ref } from "vue";
import { useSettingStore } from "../settings";
import { LocalStorage } from "/@/utils/util.storage";
export type ProjectItem = {
id: string;
name: string;
permission?: string;
};
export const useProjectStore = defineStore("app.project", () => {
const myProjects = ref([]);
const lastProjectId = LocalStorage.get("currentProjectId");
const currentProjectId = ref(lastProjectId); // 直接调用
const projects = computed(() => {
return myProjects.value;
});
const currentProject = computed(() => {
if (currentProjectId.value) {
const project = projects.value.find(item => item.id === currentProjectId.value);
if (project) {
return project;
}
}
if (projects.value.length > 0) {
return projects.value[0];
}
return null;
});
const settingStore = useSettingStore();
const isEnterprise = computed(() => {
return settingStore.isEnterprise;
});
function getSearchForm() {
if (!currentProjectId.value || !isEnterprise.value) {
return {};
}
return {
projectId: currentProjectId.value,
};
}
async function loadMyProjects(): Promise<ProjectItem[]> {
if (!isEnterprise.value) {
return [];
}
const projects = await api.MyProjectList();
myProjects.value = projects;
if (projects.length > 0 && !currentProjectId.value) {
changeCurrentProject(projects[0].id, true);
}
}
function changeCurrentProject(id: string, silent?: boolean) {
currentProjectId.value = id;
LocalStorage.set("currentProjectId", id);
if (!silent) {
message.success("切换项目成功");
}
}
async function reload() {
const projects = await api.MyProjectList();
myProjects.value = projects;
}
async function init() {
if (!myProjects.value) {
await reload();
}
return myProjects.value;
}
return {
projects,
myProjects,
currentProject,
currentProjectId,
isEnterprise,
getSearchForm,
loadMyProjects,
changeCurrentProject,
reload,
init,
};
});
@@ -86,6 +86,9 @@ export type SysPublicSetting = {
>;
// 系统通知
notice?: string;
// 管理员模式
adminMode?: "enterprise" | "saas";
};
export type SuiteSetting = {
enabled?: boolean;
@@ -141,6 +141,9 @@ export const useSettingStore = defineStore({
isComm(): boolean {
return this.plusInfo?.isComm && (this.plusInfo?.expireTime === -1 || this.plusInfo?.expireTime > new Date().getTime());
},
isEnterprise(): boolean {
return this.isPlus && this.sysPublic.adminMode === "enterprise";
},
isAgent(): boolean {
return this.siteEnv?.agent?.enabled === true;
},
@@ -111,7 +111,7 @@ function clearPreferencesAndLogout() {
<template v-for="slot in leftSlots.filter(item => item.index < REFERENCE_VALUE)" :key="slot.name">
<slot :name="slot.name">
<template v-if="slot.name === 'refresh'">
<VbenIconButton class="my-0 mr-1 rounded-md" @click="refresh">
<VbenIconButton class="my-0 mr-1 rounded-md hidden md:block" @click="refresh">
<RotateCw class="size-4" />
</VbenIconButton>
</template>
@@ -131,7 +131,7 @@ function clearPreferencesAndLogout() {
<template v-for="slot in rightSlots" :key="slot.name">
<slot :name="slot.name">
<template v-if="slot.name === 'global-search'">
<GlobalSearch :enable-shortcut-key="globalSearchShortcutKey" :menus="accessStore.accessMenus" class="mr-1 sm:mr-4" />
<GlobalSearch :enable-shortcut-key="globalSearchShortcutKey" :menus="accessStore.accessMenus" class="mr-1 sm:mr-4 hidden md:block" />
</template>
<template v-else-if="slot.name === 'preferences'">
@@ -144,7 +144,7 @@ function clearPreferencesAndLogout() {
<LanguageToggle class="mr-1" />
</template>
<template v-else-if="slot.name === 'fullscreen'">
<VbenFullScreen class="mr-1" />
<VbenFullScreen class="mr-1 hidden md:block" />
</template>
</slot>
</template>
@@ -210,9 +210,11 @@ const headerSlots = computed(() => {
</template>
<!-- 侧边额外区域 -->
<template #side-extra>
1111
<LayoutExtraMenu :accordion="preferences.navigation.accordion" :collapse="preferences.sidebar.extraCollapse" :menus="wrapperMenus(extraMenus)" :rounded="isMenuRounded" :theme="sidebarTheme" />
</template>
<template #side-extra-title>
234234234
<VbenLogo v-if="preferences.logo.enable" :text="preferences.app.name" :theme="theme" />
</template>
@@ -3,6 +3,8 @@ import { ref } from "vue";
import { getCommonColumnDefine } from "/@/views/certd/access/common";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { useI18n } from "/src/locales";
import { useProjectStore } from "/@/store/project";
import { myProjectDict } from "../../../dicts";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { t } = useI18n();
@@ -44,6 +46,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
context.typeRef = typeRef;
const commonColumnsDefine = getCommonColumnDefine(crudExpose, typeRef, api);
commonColumnsDefine.type.form.component.disabled = true;
const projectStore = useProjectStore();
return {
typeRef,
crudOptions: {
@@ -58,6 +61,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
search: {
show: true,
initialForm: {
...projectStore.getSearchForm(),
},
},
form: {
wrapper: {
@@ -141,6 +147,14 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
},
...commonColumnsDefine,
projectId: {
title: t("certd.fields.projectName"),
type: "dict-select",
dict: myProjectDict,
form: {
show: false,
},
},
},
},
};
@@ -1,8 +1,10 @@
// @ts-ignore
import { useI18n } from "/src/locales";
import { ref } from "vue";
import { getCommonColumnDefine } from "/@/views/certd/access/common";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { ref } from "vue";
import { myProjectDict } from "../dicts";
import { useProjectStore } from "/@/store/project";
import { getCommonColumnDefine } from "/@/views/certd/access/common";
import { useI18n } from "/src/locales";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { t } = useI18n();
@@ -29,6 +31,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const typeRef = ref();
const commonColumnsDefine = getCommonColumnDefine(crudExpose, typeRef, api);
const projectStore = useProjectStore();
return {
crudOptions: {
request: {
@@ -42,6 +45,11 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
confirmMessage: "授权如果已经被使用,可能会导致流水线无法正常运行,请谨慎操作",
},
},
search: {
initialForm: {
...projectStore.getSearchForm(),
},
},
rowHandle: {
width: 200,
buttons: {
@@ -115,6 +123,14 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
},
...commonColumnsDefine,
projectId: {
title: t("certd.fields.projectName"),
type: "dict-select",
dict: myProjectDict,
form: {
show: false,
},
},
},
},
};
@@ -0,0 +1,34 @@
import { dict } from "@fast-crud/fast-crud";
export const projectPermissionDict = dict({
data: [
{
label: "read",
value: "只读",
},
{
label: "write",
value: "读写",
},
{
label: "admin",
value: "管理员",
},
],
});
export const myProjectDict = dict({
url: "/enterprise/project/list",
value: "id",
label: "name",
});
export const userDict = dict({
url: "/sys/authority/user/getSimpleUsers",
value: "id",
onReady: ({ dict }) => {
for (const item of dict.data) {
item.label = item.nickName || item.username || item.phoneCode + item.mobile;
}
},
});
@@ -6,6 +6,8 @@ import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, Edi
import { useUserStore } from "/@/store/user";
import { useSettingStore } from "/@/store/settings";
import { statusUtil } from "/@/views/certd/pipeline/pipeline/utils/util.status";
import { myProjectDict } from "../dicts";
import { useProjectStore } from "/@/store/project";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter();
@@ -32,6 +34,8 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const selectedRowKeys: Ref<any[]> = ref([]);
context.selectedRowKeys = selectedRowKeys;
const projectStore = useProjectStore();
return {
crudOptions: {
settings: {
@@ -64,6 +68,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
},
search: {
initialForm: {
...projectStore.getSearchForm(),
},
formItem: {
labelCol: {
style: {
@@ -195,17 +202,13 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
align: "center",
},
},
createTime: {
title: t("certd.fields.createTime"),
type: "datetime",
projectId: {
title: t("certd.fields.projectName"),
type: "dict-select",
dict: myProjectDict,
form: {
show: false,
},
column: {
sorter: true,
width: 160,
align: "center",
},
},
updateTime: {
title: t("certd.fields.updateTime"),
@@ -10,6 +10,8 @@ import { notification } from "ant-design-vue";
import CertView from "/@/views/certd/pipeline/cert-view.vue";
import { useCertUpload } from "/@/views/certd/pipeline/cert-upload/use";
import { useSettingStore } from "/@/store/settings";
import { useProjectStore } from "/@/store/project";
import { myProjectDict } from "../../dicts";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { t } = useI18n();
@@ -36,7 +38,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const router = useRouter();
const settingStore = useSettingStore();
const projectStore = useProjectStore();
const model = useModal();
const viewCert = async (row: any) => {
const cert = await api.GetCert(row.id);
@@ -63,6 +65,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const expireStatus = route?.query?.expireStatus as string;
const searchInitForm = {
expiresLeft: expireStatus,
...projectStore.getSearchForm(),
};
return {
crudOptions: {
@@ -344,6 +347,14 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
},
},
projectId: {
title: t("certd.fields.projectName"),
type: "dict-select",
dict: myProjectDict,
form: {
show: false,
},
},
},
},
};
@@ -13,6 +13,8 @@ import { useSiteImport } from "/@/views/certd/monitor/site/use";
import { ref } from "vue";
import GroupSelector from "../../basic/group/group-selector.vue";
import { createGroupDictRef } from "../../basic/group/api";
import { useProjectStore } from "/@/store/project";
import { myProjectDict } from "../../dicts";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { t } = useI18n();
const api = siteInfoApi;
@@ -105,6 +107,8 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
return searchFrom.groupId;
}
}
const projectStore = useProjectStore();
return {
id: "siteMonitorCrud",
crudOptions: {
@@ -289,6 +293,11 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
// name: "disabled",
// show: true,
// },
search: {
initialForm: {
...projectStore.getSearchForm(),
},
},
columns: {
id: {
title: "ID",
@@ -547,6 +556,20 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
},
},
ipAddress: {
title: t("certd.monitor.ipAddress"),
search: {
show: false,
},
type: "text",
form: {
helper: t("certd.monitor.ipAddressHelper"),
},
column: {
width: 150,
sorter: true,
},
},
groupId: {
title: t("certd.fields.group"),
type: "dict-select",
@@ -800,6 +823,14 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
},
},
projectId: {
title: t("certd.fields.projectName"),
type: "dict-select",
dict: myProjectDict,
form: {
show: false,
},
},
},
},
};
@@ -5,6 +5,8 @@ import { forEach, get, merge, set } from "lodash-es";
import { Modal } from "ant-design-vue";
import { mitter } from "/@/utils/util.mitt";
import { useI18n } from "/src/locales";
import { useProjectStore } from "/@/store/project";
import { myProjectDict } from "../dicts";
export function notificationProvide(api: any) {
provide("notificationApi", api);
@@ -26,6 +28,8 @@ export function getCommonColumnDefine(crudExpose: any, typeRef: any, api: any) {
},
};
const projectStore = useProjectStore();
provide("getCurrentPluginDefine", () => {
return currentDefine;
});
@@ -247,5 +251,13 @@ export function getCommonColumnDefine(crudExpose: any, typeRef: any, api: any) {
},
},
} as ColumnCompositionProps,
projectId: {
title: t("certd.fields.projectName"),
type: "dict-select",
dict: myProjectDict,
form: {
show: false,
},
},
};
}
@@ -2,6 +2,7 @@ import { ref } from "vue";
import { getCommonColumnDefine } from "./common";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { createNotificationApi } from "/@/views/certd/notification/api";
import { useProjectStore } from "/@/store/project";
const api = createNotificationApi();
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
@@ -26,6 +27,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const typeRef = ref();
const commonColumnsDefine = getCommonColumnDefine(crudExpose, typeRef, api);
const projectStore = useProjectStore();
return {
crudOptions: {
request: {
@@ -34,6 +36,11 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
editRequest,
delRequest,
},
search: {
initialForm: {
...projectStore.getSearchForm(),
},
},
form: {
labelCol: {
//固定label宽度
@@ -2,6 +2,7 @@
import { ref } from "vue";
import { getCommonColumnDefine } from "../../common";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { useProjectStore } from "/@/store/project";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { crudBinding } = crudExpose;
@@ -29,6 +30,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
return res;
};
const projectStore = useProjectStore();
const selectedRowKey = ref([props.modelValue]);
const onSelectChange = (changed: any) => {
@@ -54,6 +56,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
search: {
show: false,
initialForm: {
...projectStore.getSearchForm(),
},
},
form: {
labelCol: {
@@ -3,6 +3,8 @@ import { useI18n } from "/src/locales";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { OPEN_API_DOC, openkeyApi } from "./api";
import { useModal } from "/@/use/use-modal";
import { useProjectStore } from "/@/store/project";
import { myProjectDict } from "../../dicts";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { t } = useI18n();
@@ -26,6 +28,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const res = await api.AddObj(form);
return res;
};
const projectStore = useProjectStore();
const model = useModal();
return {
crudOptions: {
@@ -37,6 +40,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
search: {
show: false,
initialForm: {
...projectStore.getSearchForm(),
},
},
form: {
labelCol: {
@@ -163,6 +169,14 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
sorter: true,
},
},
projectId: {
title: t("certd.fields.projectName"),
type: "dict-select",
dict: myProjectDict,
form: {
show: false,
},
},
createTime: {
title: t("certd.fields.createTime"),
type: "datetime",
@@ -14,6 +14,8 @@ import GroupSelector from "/@/views/certd/pipeline/group/group-selector.vue";
import { statusUtil } from "/@/views/certd/pipeline/pipeline/utils/util.status";
import { useCertViewer } from "/@/views/certd/pipeline/use";
import { useI18n } from "/src/locales";
import { myProjectDict } from "../dicts";
import { useProjectStore } from "/@/store/project";
export default function ({ crudExpose, context: { selectedRowKeys, openCertApplyDialog } }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter();
@@ -66,6 +68,8 @@ export default function ({ crudExpose, context: { selectedRowKeys, openCertApply
const userStore = useUserStore();
const settingStore = useSettingStore();
const projectStore = useProjectStore();
const DEFAULT_WILL_EXPIRE_DAYS = settingStore.sysPublic.defaultWillExpireDays || settingStore.sysPublic.defaultCertRenewDays || 15;
function onDialogOpen(opt: any) {
@@ -147,7 +151,9 @@ export default function ({ crudExpose, context: { selectedRowKeys, openCertApply
},
},
search: {
col: { span: 3 },
initialForm: {
...projectStore.getSearchForm(),
},
},
form: {
afterSubmit({ form, res, mode }) {
@@ -440,6 +446,7 @@ export default function ({ crudExpose, context: { selectedRowKeys, openCertApply
type: "dict-select",
search: {
show: true,
col: { span: 2 },
},
dict: dict({
data: statusUtil.getOptions(),
@@ -544,6 +551,7 @@ export default function ({ crudExpose, context: { selectedRowKeys, openCertApply
type: "dict-select",
search: {
show: true,
col: { span: 2 },
},
dict: dict({
data: [
@@ -641,6 +649,14 @@ export default function ({ crudExpose, context: { selectedRowKeys, openCertApply
},
},
},
projectId: {
title: t("certd.fields.projectName"),
type: "dict-select",
dict: myProjectDict,
form: {
show: false,
},
},
updateTime: {
title: t("certd.fields.updateTime"),
type: "datetime",
@@ -1,6 +1,8 @@
import { useI18n } from "/src/locales";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { pipelineGroupApi } from "./api";
import { useProjectStore } from "/@/store/project";
import { myProjectDict } from "../../dicts";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { t } = useI18n();
@@ -25,6 +27,8 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
return res;
};
const projectStore = useProjectStore();
return {
crudOptions: {
settings: {
@@ -44,6 +48,11 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
editRequest,
delRequest,
},
search: {
initialForm: {
...projectStore.getSearchForm(),
},
},
form: {
labelCol: {
//固定label宽度
@@ -127,6 +136,14 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
width: 400,
},
},
projectId: {
title: t("certd.fields.projectName"),
type: "dict-select",
dict: myProjectDict,
form: {
show: false,
},
},
},
},
};
@@ -1,7 +1,7 @@
<template>
<fs-page v-if="pipeline" class="page-pipeline-edit">
<template #header>
<div class="title flex-1">
<div class="title flex-0">
<fs-button class="back" icon="ion:chevron-back-outline" @click="goBack"></fs-button>
<text-editable v-model="pipeline.title" :hover-show="false" :disabled="!editMode"></text-editable>
</div>
@@ -6,6 +6,8 @@ import createCrudOptionsPipeline from "../crud";
import * as pipelineApi from "../api";
import { useTemplate } from "/@/views/certd/pipeline/template/use";
import { useI18n } from "/@/locales";
import { useProjectStore } from "/@/store/project";
import { myProjectDict } from "../../dicts";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const api = templateApi;
const { t } = useI18n();
@@ -28,6 +30,8 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const res = await api.AddObj(form);
return res;
};
const projectStore = useProjectStore();
const { openCrudFormDialog } = useFormWrapper();
const router = useRouter();
@@ -48,6 +52,11 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
router.push({ path: "/certd/pipeline/template/edit", query: { templateId: res.id, editMode: "true" } });
},
},
search: {
initialForm: {
...projectStore.getSearchForm(),
},
},
form: {
labelCol: {
//固定label宽度
@@ -232,6 +241,14 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
},
},
projectId: {
title: t("certd.fields.projectName"),
type: "dict-select",
dict: myProjectDict,
form: {
show: false,
},
},
},
},
};
@@ -0,0 +1,75 @@
import { request } from "/src/api/service";
const apiPrefix = "/enterprise/project";
export async function GetList(query: any) {
return await request({
url: apiPrefix + "/list",
method: "post",
data: query,
});
}
export async function GetPage(query: any) {
return await request({
url: apiPrefix + "/page",
method: "post",
data: query,
});
}
export async function AddObj(obj: any) {
return await request({
url: apiPrefix + "/add",
method: "post",
data: obj,
});
}
export async function UpdateObj(obj: any) {
return await request({
url: apiPrefix + "/update",
method: "post",
data: obj,
});
}
export async function DelObj(id: any) {
return await request({
url: apiPrefix + "/delete",
method: "post",
params: { id },
});
}
export async function GetObj(id: any) {
return await request({
url: apiPrefix + "/info",
method: "post",
params: { id },
});
}
export async function GetDetail(id: any) {
return await request({
url: apiPrefix + "/detail",
method: "post",
params: { id },
});
}
export async function DeleteBatch(ids: any[]) {
return await request({
url: apiPrefix + "/deleteByIds",
method: "post",
data: { ids },
});
}
export async function SetDisabled(id: any, disabled: boolean) {
return await request({
url: apiPrefix + "/setDisabled",
method: "post",
data: { id, disabled },
});
}
@@ -0,0 +1,165 @@
import * as api from "./api";
import { useI18n } from "/src/locales";
import { computed, Ref, ref } from "vue";
import { useRouter } from "vue-router";
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes, utils } from "@fast-crud/fast-crud";
import { useUserStore } from "/@/store/user";
import { useSettingStore } from "/@/store/settings";
import { Modal } from "ant-design-vue";
import { userDict } from "../dicts";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter();
const { t } = useI18n();
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
const list = await api.GetList(query);
return {
records: list,
total: list.length,
offset: 0,
limit: 999,
};
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
const res = await api.UpdateObj(form);
return res;
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
const res = await api.AddObj(form);
return res;
};
const userStore = useUserStore();
const settingStore = useSettingStore();
const selectedRowKeys: Ref<any[]> = ref([]);
context.selectedRowKeys = selectedRowKeys;
return {
crudOptions: {
settings: {
plugins: {
//这里使用行选择插件,生成行选择crudOptions配置,最终会与crudOptions合并
rowSelection: {
enabled: true,
order: -2,
before: true,
// handle: (pluginProps,useCrudProps)=>CrudOptions,
props: {
multiple: true,
crossPage: true,
selectedRowKeys,
},
},
},
},
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
rowHandle: {
minWidth: 200,
fixed: "right",
},
columns: {
id: {
title: "ID",
key: "id",
type: "number",
column: {
width: 100,
},
form: {
show: false,
},
},
name: {
title: t("certd.ent.projectName"),
type: "link",
search: {
show: true,
},
form: {
component: {},
rules: [{ required: true, message: t("certd.requiredField") }],
},
column: {
width: 200,
cellRender({ row }) {
return <router-link to={{ path: `/certd/project/detail`, query: { projectId: row.id } }}>{row.name}</router-link>;
},
},
},
disabled: {
title: t("certd.disabled"),
type: "dict-switch",
dict: dict({
data: [
{ label: t("certd.enabled"), value: false, color: "success" },
{ label: t("certd.disabledLabel"), value: true, color: "error" },
],
}),
form: {
value: false,
},
column: {
width: 100,
component: {
title: t("certd.clickToToggle"),
on: {
async click({ value, row }) {
Modal.confirm({
title: t("certd.prompt"),
content: t("certd.confirmToggleStatus", { action: !value ? t("certd.disable") : t("certd.enable") }),
onOk: async () => {
await api.SetDisabled(row.id, !value);
await crudExpose.doRefresh();
},
});
},
},
},
},
},
adminId: {
title: t("certd.fields.adminId"),
type: "dict-select",
dict: userDict,
column: {
width: 160,
},
},
createTime: {
title: t("certd.createTime"),
type: "datetime",
form: {
show: false,
},
column: {
sorter: true,
width: 160,
align: "center",
},
},
updateTime: {
title: t("certd.updateTime"),
type: "datetime",
form: {
show: false,
},
column: {
show: true,
width: 160,
},
},
},
},
};
}
@@ -0,0 +1,67 @@
import { request } from "/src/api/service";
const apiPrefix = "/enterprise/myProjectMember";
const userApiPrefix = "/sys/authority/user";
export async function GetList(query: any) {
return await request({
url: apiPrefix + "/page",
method: "post",
data: query,
});
}
export async function AddObj(obj: any) {
return await request({
url: apiPrefix + "/add",
method: "post",
data: obj,
});
}
export async function UpdateObj(obj: any) {
return await request({
url: apiPrefix + "/update",
method: "post",
data: obj,
});
}
export async function DelObj(id: any) {
return await request({
url: apiPrefix + "/delete",
method: "post",
params: { id },
});
}
export async function GetObj(id: any) {
return await request({
url: apiPrefix + "/info",
method: "post",
params: { id },
});
}
export async function GetDetail(id: any) {
return await request({
url: apiPrefix + "/detail",
method: "post",
params: { id },
});
}
export async function DeleteBatch(ids: any[]) {
return await request({
url: apiPrefix + "/deleteByIds",
method: "post",
data: { ids },
});
}
export async function GetUserSimpleByIds(query: any) {
return await request({
url: userApiPrefix + "/getSimpleByIds",
method: "post",
data: query,
});
}
@@ -0,0 +1,162 @@
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { Modal } from "ant-design-vue";
import { Ref, ref } from "vue";
import { useRouter } from "vue-router";
import * as api from "./api";
import { useSettingStore } from "/@/store/settings";
import { useUserStore } from "/@/store/user";
import { useI18n } from "/src/locales";
import { userDict } from "../../dicts";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter();
const { t } = useI18n();
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
const res = await api.UpdateObj(form);
return res;
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
const res = await api.AddObj(form);
return res;
};
const projectId = context.projectId;
const userStore = useUserStore();
const settingStore = useSettingStore();
const selectedRowKeys: Ref<any[]> = ref([]);
context.selectedRowKeys = selectedRowKeys;
return {
crudOptions: {
settings: {
plugins: {
//这里使用行选择插件,生成行选择crudOptions配置,最终会与crudOptions合并
rowSelection: {
enabled: true,
order: -2,
before: true,
// handle: (pluginProps,useCrudProps)=>CrudOptions,
props: {
multiple: true,
crossPage: true,
selectedRowKeys,
},
},
},
},
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
rowHandle: {
minWidth: 200,
fixed: "right",
},
search: {
initialForm: {
projectId,
},
},
columns: {
id: {
title: "ID",
key: "id",
type: "number",
column: {
width: 100,
},
form: {
show: false,
},
},
projectId: {
title: "项目ID",
type: "text",
search: {
show: false,
},
form: {
value: projectId,
show: false,
},
editForm: {
show: false,
},
column: {
width: 200,
show: false,
},
},
userId: {
title: "用户",
type: "dict-select",
dict: userDict,
search: {
show: true,
},
form: {},
editForm: {
show: false,
},
column: {
width: 200,
},
},
permission: {
title: t("certd.ent.projectPermission"),
type: "dict-select",
dict: dict({
data: [
{ label: t("certd.ent.permission.read"), value: "read", color: "cyan" },
{ label: t("certd.ent.permission.write"), value: "write", color: "blue" },
{ label: t("certd.ent.permission.admin"), value: "admin", color: "green" },
],
}),
search: {
show: true,
},
form: {
show: true,
},
column: {
width: 200,
},
},
createTime: {
title: t("certd.createTime"),
type: "datetime",
form: {
show: false,
},
column: {
sorter: true,
width: 160,
align: "center",
},
},
updateTime: {
title: t("certd.updateTime"),
type: "datetime",
form: {
show: false,
},
column: {
show: true,
width: 160,
},
},
},
},
};
}
@@ -0,0 +1,71 @@
<template>
<fs-page class="page-project-detail">
<template #header>
<div class="title">
{{ t("certd.ent.projectDetailManager") }}
<span class="sub">
{{ t("certd.ent.projectDetailDescription") }}
</span>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #pagination-left>
<a-tooltip :title="t('certd.batchDelete')">
<fs-button icon="DeleteOutlined" @click="handleBatchDelete"></fs-button>
</a-tooltip>
</template>
</fs-crud>
</fs-page>
</template>
<script lang="ts" setup>
import { onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
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";
const { t } = useI18n();
defineOptions({
name: "ProjectDetail",
});
const route = useRoute();
const projectIdStr = route.query.projectId as string;
const projectId = Number(projectIdStr);
const context: any = {
projectId,
};
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context });
const selectedRowKeys = context.selectedRowKeys;
const handleBatchDelete = () => {
if (selectedRowKeys.value?.length > 0) {
Modal.confirm({
title: t("certd.confirmTitle"),
content: t("certd.confirmDeleteBatch", { count: selectedRowKeys.value.length }),
async onOk() {
await DeleteBatch(selectedRowKeys.value);
message.info(t("certd.deleteSuccess"));
crudExpose.doRefresh();
selectedRowKeys.value = [];
},
});
} else {
message.error(t("certd.selectRecordsFirst"));
}
};
//
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(async () => {
await crudExpose.doRefresh();
});
</script>
<style lang="less"></style>
@@ -0,0 +1,61 @@
<template>
<fs-page class="page-cert">
<template #header>
<div class="title">
{{ t("certd.sysResources.myProjectManager") }}
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding">
<!-- <div v-if="crudBinding.data" class="project-list">
<div v-for="item of crudBinding.data" :key="item.id" class="project-item">
<a-card style="width: 300px">
<p>{{ item.name }}</p>
</a-card>
</div>
</div> -->
</fs-crud>
</fs-page>
</template>
<script lang="ts" setup>
import { onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { message, Modal } from "ant-design-vue";
import { DeleteBatch } from "./api";
import { useI18n } from "/src/locales";
const { t } = useI18n();
defineOptions({
name: "MyProjectManager",
});
const { crudBinding, crudRef, crudExpose, context } = useFs({ createCrudOptions });
const selectedRowKeys = context.selectedRowKeys;
const handleBatchDelete = () => {
if (selectedRowKeys.value?.length > 0) {
Modal.confirm({
title: t("certd.confirmTitle"),
content: t("certd.confirmDeleteBatch", { count: selectedRowKeys.value.length }),
async onOk() {
await DeleteBatch(selectedRowKeys.value);
message.info(t("certd.deleteSuccess"));
crudExpose.doRefresh();
selectedRowKeys.value = [];
},
});
} else {
message.error(t("certd.selectRecordsFirst"));
}
};
//
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(async () => {
await crudExpose.doRefresh();
});
</script>
<style lang="less"></style>
@@ -0,0 +1,75 @@
import { request } from "/src/api/service";
const apiPrefix = "/sys/project/provider";
export async function GetList(query: any) {
return await request({
url: apiPrefix + "/page",
method: "post",
data: query,
});
}
export async function AddObj(obj: any) {
return await request({
url: apiPrefix + "/add",
method: "post",
data: obj,
});
}
export async function UpdateObj(obj: any) {
return await request({
url: apiPrefix + "/update",
method: "post",
data: obj,
});
}
export async function DelObj(id: any) {
return await request({
url: apiPrefix + "/delete",
method: "post",
params: { id },
});
}
export async function GetObj(id: any) {
return await request({
url: apiPrefix + "/info",
method: "post",
params: { id },
});
}
export async function GetDetail(id: any) {
return await request({
url: apiPrefix + "/detail",
method: "post",
params: { id },
});
}
export async function DeleteBatch(ids: any[]) {
return await request({
url: apiPrefix + "/deleteByIds",
method: "post",
data: { ids },
});
}
export async function SetDefault(id: any) {
return await request({
url: apiPrefix + "/setDefault",
method: "post",
data: { id },
});
}
export async function SetDisabled(id: any, disabled: boolean) {
return await request({
url: apiPrefix + "/setDisabled",
method: "post",
data: { id, disabled },
});
}
@@ -0,0 +1,254 @@
import * as api from "./api";
import { useI18n } from "/src/locales";
import { computed, Ref, ref } from "vue";
import { useRouter } from "vue-router";
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes, utils } from "@fast-crud/fast-crud";
import { useUserStore } from "/@/store/user";
import { useSettingStore } from "/@/store/settings";
import { Modal } from "ant-design-vue";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter();
const { t } = useI18n();
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
const res = await api.UpdateObj(form);
return res;
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
const res = await api.AddObj(form);
return res;
};
const userStore = useUserStore();
const settingStore = useSettingStore();
const selectedRowKeys: Ref<any[]> = ref([]);
context.selectedRowKeys = selectedRowKeys;
return {
crudOptions: {
settings: {
plugins: {
//这里使用行选择插件,生成行选择crudOptions配置,最终会与crudOptions合并
rowSelection: {
enabled: true,
order: -2,
before: true,
// handle: (pluginProps,useCrudProps)=>CrudOptions,
props: {
multiple: true,
crossPage: true,
selectedRowKeys,
},
},
},
},
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
rowHandle: {
minWidth: 200,
fixed: "right",
},
columns: {
id: {
title: "ID",
key: "id",
type: "number",
column: {
width: 100,
},
form: {
show: false,
},
},
domain: {
title: t("certd.cnameDomain"),
type: "text",
editForm: {
component: {
disabled: true,
},
},
search: {
show: true,
},
form: {
component: {
placeholder: t("certd.cnameDomainPlaceholder"),
},
helper: t("certd.cnameDomainHelper"),
rules: [
{ required: true, message: t("certd.requiredField") },
{ pattern: /^[^*]+$/, message: t("certd.cnameDomainPattern") },
],
},
column: {
width: 200,
},
},
dnsProviderType: {
title: t("certd.dnsProvider"),
type: "dict-select",
search: {
show: true,
},
dict: dict({
url: "pi/dnsProvider/list",
value: "key",
label: "title",
}),
form: {
rules: [{ required: true, message: t("certd.requiredField") }],
},
column: {
width: 150,
component: {
color: "auto",
},
},
},
accessId: {
title: t("certd.dnsProviderAuthorization"),
type: "dict-select",
dict: dict({
url: "/pi/access/list",
value: "id",
label: "name",
}),
form: {
component: {
name: "access-selector",
vModel: "modelValue",
type: compute(({ form }) => {
return form.dnsProviderType;
}),
},
rules: [{ required: true, message: t("certd.requiredField") }],
},
column: {
width: 150,
component: {
color: "auto",
},
},
},
isDefault: {
title: t("certd.isDefault"),
type: "dict-switch",
dict: dict({
data: [
{ label: t("certd.yes"), value: true, color: "success" },
{ label: t("certd.no"), value: false, color: "default" },
],
}),
form: {
value: false,
rules: [{ required: true, message: t("certd.selectIsDefault") }],
},
column: {
align: "center",
width: 100,
},
},
setDefault: {
title: t("certd.setDefault"),
type: "text",
form: {
show: false,
},
column: {
width: 100,
align: "center",
conditionalRenderDisabled: true,
cellRender: ({ row }) => {
if (row.isDefault) {
return;
}
const onClick = async () => {
Modal.confirm({
title: t("certd.prompt"),
content: t("certd.confirmSetDefault"),
onOk: async () => {
await api.SetDefault(row.id);
await crudExpose.doRefresh();
},
});
};
return (
<a-button type={"link"} size={"small"} onClick={onClick}>
{t("certd.setAsDefault")}
</a-button>
);
},
},
},
disabled: {
title: t("certd.disabled"),
type: "dict-switch",
dict: dict({
data: [
{ label: t("certd.enabled"), value: false, color: "success" },
{ label: t("certd.disabledLabel"), value: true, color: "error" },
],
}),
form: {
value: false,
},
column: {
width: 100,
component: {
title: t("certd.clickToToggle"),
on: {
async click({ value, row }) {
Modal.confirm({
title: t("certd.prompt"),
content: t("certd.confirmToggleStatus", { action: !value ? t("certd.disable") : t("certd.enable") }),
onOk: async () => {
await api.SetDisabled(row.id, !value);
await crudExpose.doRefresh();
},
});
},
},
},
},
},
createTime: {
title: t("certd.createTime"),
type: "datetime",
form: {
show: false,
},
column: {
sorter: true,
width: 160,
align: "center",
},
},
updateTime: {
title: t("certd.updateTime"),
type: "datetime",
form: {
show: false,
},
column: {
show: true,
width: 160,
},
},
},
},
};
}
@@ -0,0 +1,65 @@
<template>
<fs-page class="page-cert">
<template #header>
<div class="title">
{{ t("certd.cnameTitle") }}
<span class="sub">
{{ t("certd.cnameDescription") }}
<a href="https://certd.docmirror.cn/guide/feature/cname/" target="_blank">
{{ t("certd.cnameLinkText") }}
</a>
</span>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #pagination-left>
<a-tooltip :title="t('certd.batchDelete')">
<fs-button icon="DeleteOutlined" @click="handleBatchDelete"></fs-button>
</a-tooltip>
</template>
</fs-crud>
</fs-page>
</template>
<script lang="ts" setup>
import { onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { message, Modal } from "ant-design-vue";
import { DeleteBatch } from "./api";
import { useI18n } from "/src/locales";
const { t } = useI18n();
defineOptions({
name: "CnameProvider",
});
const { crudBinding, crudRef, crudExpose, context } = useFs({ createCrudOptions });
const selectedRowKeys = context.selectedRowKeys;
const handleBatchDelete = () => {
if (selectedRowKeys.value?.length > 0) {
Modal.confirm({
title: t("certd.confirmTitle"),
content: t("certd.confirmDeleteBatch", { count: selectedRowKeys.value.length }),
async onOk() {
await DeleteBatch(selectedRowKeys.value);
message.info(t("certd.deleteSuccess"));
crudExpose.doRefresh();
selectedRowKeys.value = [];
},
});
} else {
message.error(t("certd.selectRecordsFirst"));
}
};
//
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(async () => {
await crudExpose.doRefresh();
});
</script>
<style lang="less"></style>
@@ -0,0 +1,11 @@
import { dict } from "@fast-crud/fast-crud";
export const userDict = dict({
url: "/sys/authority/user/getSimpleUsers",
value: "id",
onReady: ({ dict }) => {
for (const item of dict.data) {
item.label = item.nickName || item.username || item.phoneCode + item.mobile;
}
},
});
@@ -0,0 +1,68 @@
import { request } from "/src/api/service";
const apiPrefix = "/sys/enterprise/project";
const userApiPrefix = "/sys/authority/user";
export async function GetList(query: any) {
return await request({
url: apiPrefix + "/page",
method: "post",
data: query,
});
}
export async function AddObj(obj: any) {
return await request({
url: apiPrefix + "/add",
method: "post",
data: obj,
});
}
export async function UpdateObj(obj: any) {
return await request({
url: apiPrefix + "/update",
method: "post",
data: obj,
});
}
export async function DelObj(id: any) {
return await request({
url: apiPrefix + "/delete",
method: "post",
params: { id },
});
}
export async function GetObj(id: any) {
return await request({
url: apiPrefix + "/info",
method: "post",
params: { id },
});
}
export async function GetDetail(id: any) {
return await request({
url: apiPrefix + "/detail",
method: "post",
params: { id },
});
}
export async function DeleteBatch(ids: any[]) {
return await request({
url: apiPrefix + "/deleteByIds",
method: "post",
data: { ids },
});
}
export async function SetDisabled(id: any, disabled: boolean) {
return await request({
url: apiPrefix + "/setDisabled",
method: "post",
data: { id, disabled },
});
}
@@ -0,0 +1,158 @@
import * as api from "./api";
import { useI18n } from "/src/locales";
import { computed, Ref, ref } from "vue";
import { useRouter } from "vue-router";
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes, utils } from "@fast-crud/fast-crud";
import { useUserStore } from "/@/store/user";
import { useSettingStore } from "/@/store/settings";
import { Modal } from "ant-design-vue";
import { userDict } from "../dicts";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter();
const { t } = useI18n();
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
const res = await api.UpdateObj(form);
return res;
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
const res = await api.AddObj(form);
return res;
};
const userStore = useUserStore();
const settingStore = useSettingStore();
const selectedRowKeys: Ref<any[]> = ref([]);
context.selectedRowKeys = selectedRowKeys;
return {
crudOptions: {
settings: {
plugins: {
//这里使用行选择插件,生成行选择crudOptions配置,最终会与crudOptions合并
rowSelection: {
enabled: true,
order: -2,
before: true,
// handle: (pluginProps,useCrudProps)=>CrudOptions,
props: {
multiple: true,
crossPage: true,
selectedRowKeys,
},
},
},
},
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
rowHandle: {
minWidth: 200,
fixed: "right",
},
columns: {
id: {
title: "ID",
key: "id",
type: "number",
column: {
width: 100,
},
form: {
show: false,
},
},
name: {
title: t("certd.ent.projectName"),
type: "link",
search: {
show: true,
},
form: {
component: {},
rules: [{ required: true, message: t("certd.requiredField") }],
},
column: {
width: 200,
cellRender({ row }) {
return <router-link to={{ path: `/sys/enterprise/project/detail`, query: { projectId: row.id } }}>{row.name}</router-link>;
},
},
},
disabled: {
title: t("certd.disabled"),
type: "dict-switch",
dict: dict({
data: [
{ label: t("certd.enabled"), value: false, color: "success" },
{ label: t("certd.disabledLabel"), value: true, color: "error" },
],
}),
form: {
value: false,
},
column: {
width: 100,
component: {
title: t("certd.clickToToggle"),
on: {
async click({ value, row }) {
Modal.confirm({
title: t("certd.prompt"),
content: t("certd.confirmToggleStatus", { action: !value ? t("certd.disable") : t("certd.enable") }),
onOk: async () => {
await api.SetDisabled(row.id, !value);
await crudExpose.doRefresh();
},
});
},
},
},
},
},
adminId: {
title: t("certd.fields.adminId"),
type: "dict-select",
dict: userDict,
column: {
width: 160,
},
},
createTime: {
title: t("certd.createTime"),
type: "datetime",
form: {
show: false,
},
column: {
sorter: true,
width: 160,
align: "center",
},
},
updateTime: {
title: t("certd.updateTime"),
type: "datetime",
form: {
show: false,
},
column: {
show: true,
width: 160,
},
},
},
},
};
}
@@ -0,0 +1,67 @@
import { request } from "/src/api/service";
const apiPrefix = "/sys/enterprise/projectMember";
const userApiPrefix = "/sys/authority/user";
export async function GetList(query: any) {
return await request({
url: apiPrefix + "/page",
method: "post",
data: query,
});
}
export async function AddObj(obj: any) {
return await request({
url: apiPrefix + "/add",
method: "post",
data: obj,
});
}
export async function UpdateObj(obj: any) {
return await request({
url: apiPrefix + "/update",
method: "post",
data: obj,
});
}
export async function DelObj(id: any) {
return await request({
url: apiPrefix + "/delete",
method: "post",
params: { id },
});
}
export async function GetObj(id: any) {
return await request({
url: apiPrefix + "/info",
method: "post",
params: { id },
});
}
export async function GetDetail(id: any) {
return await request({
url: apiPrefix + "/detail",
method: "post",
params: { id },
});
}
export async function DeleteBatch(ids: any[]) {
return await request({
url: apiPrefix + "/deleteByIds",
method: "post",
data: { ids },
});
}
export async function GetUserSimpleByIds(query: any) {
return await request({
url: userApiPrefix + "/getSimpleByIds",
method: "post",
data: query,
});
}
@@ -0,0 +1,162 @@
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { Modal } from "ant-design-vue";
import { Ref, ref } from "vue";
import { useRouter } from "vue-router";
import * as api from "./api";
import { useSettingStore } from "/@/store/settings";
import { useUserStore } from "/@/store/user";
import { useI18n } from "/src/locales";
import { userDict } from "../../dicts";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter();
const { t } = useI18n();
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
const res = await api.UpdateObj(form);
return res;
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
const res = await api.AddObj(form);
return res;
};
const projectId = context.projectId;
const userStore = useUserStore();
const settingStore = useSettingStore();
const selectedRowKeys: Ref<any[]> = ref([]);
context.selectedRowKeys = selectedRowKeys;
return {
crudOptions: {
settings: {
plugins: {
//这里使用行选择插件,生成行选择crudOptions配置,最终会与crudOptions合并
rowSelection: {
enabled: true,
order: -2,
before: true,
// handle: (pluginProps,useCrudProps)=>CrudOptions,
props: {
multiple: true,
crossPage: true,
selectedRowKeys,
},
},
},
},
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
rowHandle: {
minWidth: 200,
fixed: "right",
},
search: {
initialForm: {
projectId,
},
},
columns: {
id: {
title: "ID",
key: "id",
type: "number",
column: {
width: 100,
},
form: {
show: false,
},
},
projectId: {
title: "项目ID",
type: "text",
search: {
show: false,
},
form: {
value: projectId,
show: false,
},
editForm: {
show: false,
},
column: {
width: 200,
show: false,
},
},
userId: {
title: "用户",
type: "dict-select",
dict: userDict,
search: {
show: true,
},
form: {},
editForm: {
show: false,
},
column: {
width: 200,
},
},
permission: {
title: t("certd.ent.projectPermission"),
type: "dict-select",
dict: dict({
data: [
{ label: t("certd.ent.permission.read"), value: "read", color: "cyan" },
{ label: t("certd.ent.permission.write"), value: "write", color: "blue" },
{ label: t("certd.ent.permission.admin"), value: "admin", color: "green" },
],
}),
search: {
show: true,
},
form: {
show: true,
},
column: {
width: 200,
},
},
createTime: {
title: t("certd.createTime"),
type: "datetime",
form: {
show: false,
},
column: {
sorter: true,
width: 160,
align: "center",
},
},
updateTime: {
title: t("certd.updateTime"),
type: "datetime",
form: {
show: false,
},
column: {
show: true,
width: 160,
},
},
},
},
};
}
@@ -0,0 +1,71 @@
<template>
<fs-page class="page-project-detail">
<template #header>
<div class="title">
{{ t("certd.ent.projectDetailManager") }}
<span class="sub">
{{ t("certd.ent.projectDetailDescription") }}
</span>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #pagination-left>
<a-tooltip :title="t('certd.batchDelete')">
<fs-button icon="DeleteOutlined" @click="handleBatchDelete"></fs-button>
</a-tooltip>
</template>
</fs-crud>
</fs-page>
</template>
<script lang="ts" setup>
import { onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
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";
const { t } = useI18n();
defineOptions({
name: "ProjectDetail",
});
const route = useRoute();
const projectIdStr = route.query.projectId as string;
const projectId = Number(projectIdStr);
const context: any = {
projectId,
};
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context });
const selectedRowKeys = context.selectedRowKeys;
const handleBatchDelete = () => {
if (selectedRowKeys.value?.length > 0) {
Modal.confirm({
title: t("certd.confirmTitle"),
content: t("certd.confirmDeleteBatch", { count: selectedRowKeys.value.length }),
async onOk() {
await DeleteBatch(selectedRowKeys.value);
message.info(t("certd.deleteSuccess"));
crudExpose.doRefresh();
selectedRowKeys.value = [];
},
});
} else {
message.error(t("certd.selectRecordsFirst"));
}
};
//
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(async () => {
await crudExpose.doRefresh();
});
</script>
<style lang="less"></style>
@@ -0,0 +1,59 @@
<template>
<fs-page class="page-cert">
<template #header>
<div class="title">
{{ t("certd.sysResources.projectManager") }}
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #pagination-left>
<a-tooltip :title="t('certd.batchDelete')">
<fs-button icon="DeleteOutlined" @click="handleBatchDelete"></fs-button>
</a-tooltip>
</template>
</fs-crud>
</fs-page>
</template>
<script lang="ts" setup>
import { onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { message, Modal } from "ant-design-vue";
import { DeleteBatch } from "./api";
import { useI18n } from "/src/locales";
const { t } = useI18n();
defineOptions({
name: "ProjectManager",
});
const { crudBinding, crudRef, crudExpose, context } = useFs({ createCrudOptions });
const selectedRowKeys = context.selectedRowKeys;
const handleBatchDelete = () => {
if (selectedRowKeys.value?.length > 0) {
Modal.confirm({
title: t("certd.confirmTitle"),
content: t("certd.confirmDeleteBatch", { count: selectedRowKeys.value.length }),
async onOk() {
await DeleteBatch(selectedRowKeys.value);
message.info(t("certd.deleteSuccess"));
crudExpose.doRefresh();
selectedRowKeys.value = [];
},
});
} else {
message.error(t("certd.selectRecordsFirst"));
}
};
//
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(async () => {
await crudExpose.doRefresh();
});
</script>
<style lang="less"></style>
@@ -29,6 +29,9 @@
<a-tab-pane key="network" :tab="t('certd.sys.setting.networkSetting')">
<SettingNetwork v-if="activeKey === 'network'" />
</a-tab-pane>
<a-tab-pane key="mode" :tab="t('certd.sys.setting.adminModeSetting')">
<SettingMode v-if="activeKey === 'mode'" />
</a-tab-pane>
</a-tabs>
</div>
</fs-page>
@@ -42,6 +45,7 @@ import SettingSafe from "/@/views/sys/settings/tabs/safe.vue";
import SettingCaptcha from "/@/views/sys/settings/tabs/captcha.vue";
import SettingPipeline from "/@/views/sys/settings/tabs/pipeline.vue";
import SettingOauth from "/@/views/sys/settings/tabs/oauth.vue";
import SettingMode from "/@/views/sys/settings/tabs/mode.vue";
import SettingNetwork from "/@/views/sys/settings/tabs/network.vue";
import { useRoute, useRouter } from "vue-router";
import { ref } from "vue";
@@ -0,0 +1,70 @@
<template>
<div class="sys-settings-form sys-settings-mode">
<a-form :model="formState" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onFinish">
<a-form-item :label="t('certd.sys.setting.adminMode')" :name="['public', 'adminMode']">
<fs-dict-radio v-model:value="formState.public.adminMode" :dict="adminModeDict" />
</a-form-item>
<a-form-item label=" " :colon="false" :wrapper-col="{ span: 8 }">
<a-button :loading="saveLoading" type="primary" html-type="submit">{{ t("certd.saveButton") }}</a-button>
</a-form-item>
</a-form>
</div>
</template>
<script setup lang="tsx">
import { reactive, ref } from "vue";
import { SysSettings } from "/@/views/sys/settings/api";
import * as api from "/@/views/sys/settings/api";
import { merge } from "lodash-es";
import { useSettingStore } from "/@/store/settings";
import { notification } from "ant-design-vue";
import { useI18n } from "/src/locales";
import { dict } from "@fast-crud/fast-crud";
const { t } = useI18n();
defineOptions({
name: "SettingMode",
});
const adminModeDict = dict({
data: [
{
label: t("certd.sys.setting.enterpriseMode"),
value: "enterprise",
},
{
label: t("certd.sys.setting.saasMode"),
value: "saas",
},
],
});
const formState = reactive<Partial<SysSettings>>({
public: {},
private: {},
});
async function loadSysSettings() {
const data: any = await api.SysSettingsGet();
merge(formState, data);
}
const saveLoading = ref(false);
loadSysSettings();
const settingsStore = useSettingStore();
const onFinish = async (form: any) => {
try {
saveLoading.value = true;
await api.SysSettingsSave(form);
await settingsStore.loadSysSettings();
notification.success({
message: t("certd.saveSuccess"),
});
} finally {
saveLoading.value = false;
}
};
</script>
<style lang="less"></style>
@@ -0,0 +1,36 @@
ALTER TABLE `cd_access` ENGINE = InnoDB;
ALTER TABLE `cd_addon` ENGINE = InnoDB;
ALTER TABLE `cd_cert_info` ENGINE = InnoDB;
ALTER TABLE `cd_cname_provider` ENGINE = InnoDB;
ALTER TABLE `cd_cname_record` ENGINE = InnoDB;
ALTER TABLE `cd_domain` ENGINE = InnoDB;
ALTER TABLE `cd_group` ENGINE = InnoDB;
ALTER TABLE `cd_oauth_bound` ENGINE = InnoDB;
ALTER TABLE `cd_open_key` ENGINE = InnoDB;
ALTER TABLE `cd_product` ENGINE = InnoDB;
ALTER TABLE `cd_site_info` ENGINE = InnoDB;
ALTER TABLE `cd_site_ip` ENGINE = InnoDB;
ALTER TABLE `cd_trade` ENGINE = InnoDB;
ALTER TABLE `cd_user_suite` ENGINE = InnoDB;
ALTER TABLE `flyway_history` ENGINE = InnoDB;
ALTER TABLE `pi_history` ENGINE = InnoDB;
ALTER TABLE `pi_history_log` ENGINE = InnoDB;
ALTER TABLE `pi_notification` ENGINE = InnoDB;
ALTER TABLE `pi_pipeline` ENGINE = InnoDB;
ALTER TABLE `pi_pipeline_group` ENGINE = InnoDB;
ALTER TABLE `pi_plugin` ENGINE = InnoDB;
ALTER TABLE `pi_storage` ENGINE = InnoDB;
ALTER TABLE `pi_sub_domain` ENGINE = InnoDB;
ALTER TABLE `pi_template` ENGINE = InnoDB;
ALTER TABLE `sys_permission` ENGINE = InnoDB;
ALTER TABLE `sys_role` ENGINE = InnoDB;
ALTER TABLE `sys_role_permission` ENGINE = InnoDB;
ALTER TABLE `sys_settings` ENGINE = InnoDB;
ALTER TABLE `sys_user` ENGINE = InnoDB;
ALTER TABLE `sys_user_role` ENGINE = InnoDB;
ALTER TABLE `user_settings` ENGINE = InnoDB;
ALTER TABLE `cd_audit_log` ENGINE = InnoDB;
ALTER TABLE `cd_project` ENGINE = InnoDB;
ALTER TABLE `cd_project_member` ENGINE = InnoDB;
@@ -0,0 +1,112 @@
CREATE TABLE "cd_project"
(
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
"user_id" integer NOT NULL,
"name" varchar(512) NOT NULL,
"admin_id" integer NOT NULL,
"disabled" boolean NOT NULL DEFAULT (false),
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
);
CREATE INDEX "index_project_user_id" ON "cd_project" ("user_id");
CREATE INDEX "index_project_admin_id" ON "cd_project" ("admin_id");
INSERT INTO cd_project (id, user_id, "admin_id", "name", "disabled") VALUES (1, 0, 1,'default', false);
ALTER TABLE cd_cert_info ADD COLUMN project_id integer;
CREATE INDEX "index_cert_project_id" ON "cd_cert_info" ("project_id");
ALTER TABLE cd_site_info ADD COLUMN project_id integer;
CREATE INDEX "index_site_project_id" ON "cd_site_info" ("project_id");
ALTER TABLE cd_site_ip ADD COLUMN project_id integer;
CREATE INDEX "index_site_ip_project_id" ON "cd_site_ip" ("project_id");
ALTER TABLE cd_open_key ADD COLUMN project_id integer;
CREATE INDEX "index_open_key_project_id" ON "cd_open_key" ("project_id");
ALTER TABLE cd_access ADD COLUMN project_id integer;
CREATE INDEX "index_access_project_id" ON "cd_access" ("project_id");
ALTER TABLE cd_addon ADD COLUMN project_id integer;
CREATE INDEX "index_addon_project_id" ON "cd_addon" ("project_id");
ALTER TABLE pi_pipeline ADD COLUMN project_id integer;
CREATE INDEX "index_pipeline_project_id" ON "pi_pipeline" ("project_id");
ALTER TABLE pi_pipeline_group ADD COLUMN project_id integer;
CREATE INDEX "index_pipeline_group_project_id" ON "pi_pipeline_group" ("project_id");
ALTER TABLE pi_storage ADD COLUMN project_id integer;
CREATE INDEX "index_storage_project_id" ON "pi_storage" ("project_id");
ALTER TABLE pi_notification ADD COLUMN project_id integer;
CREATE INDEX "index_notification_project_id" ON "pi_notification" ("project_id");
ALTER TABLE pi_history ADD COLUMN project_id integer;
CREATE INDEX "index_history_project_id" ON "pi_history" ("project_id");
ALTER TABLE pi_history_log ADD COLUMN project_id integer;
CREATE INDEX "index_history_log_project_id" ON "pi_history_log" ("project_id");
ALTER TABLE pi_template ADD COLUMN project_id integer;
CREATE INDEX "index_template_project_id" ON "pi_template" ("project_id");
ALTER TABLE pi_sub_domain ADD COLUMN project_id integer;
CREATE INDEX "index_sub_domain_project_id" ON "pi_sub_domain" ("project_id");
ALTER TABLE cd_cname_record ADD COLUMN project_id integer;
CREATE INDEX "index_cname_record_project_id" ON "cd_cname_record" ("project_id");
ALTER TABLE cd_domain ADD COLUMN project_id integer;
CREATE INDEX "index_domain_project_id" ON "cd_domain" ("project_id");
ALTER TABLE user_settings ADD COLUMN project_id integer;
CREATE INDEX "index_user_settings_project_id" ON "user_settings" ("project_id");
ALTER TABLE cd_group ADD COLUMN project_id integer;
CREATE INDEX "index_group_project_id" ON "cd_group" ("project_id");
CREATE TABLE "cd_project_member"
(
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
"user_id" integer NOT NULL,
"project_id" integer NOT NULL,
"permission" varchar(128) NOT NULL DEFAULT ('read'),
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
);
CREATE INDEX "index_project_member_user_id" ON "cd_project_member" ("user_id");
CREATE INDEX "index_project_member_project_id" ON "cd_project_member" ("project_id");
CREATE TABLE "cd_audit_log"
(
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
"user_id" integer NOT NULL,
"username" varchar(128) NOT NULL,
"project_id" integer NOT NULL,
"project_name" varchar(512) NOT NULL,
"type" varchar(128) NOT NULL,
"action" varchar(128) NOT NULL DEFAULT ('read'),
"content" text NOT NULL,
"ip_address" varchar(128) NOT NULL,
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
);
CREATE INDEX "index_audit_log_user_id" ON "cd_audit_log" ("user_id");
CREATE INDEX "index_audit_log_project_id" ON "cd_audit_log" ("project_id");
ALTER TABLE cd_site_info ADD COLUMN ip_address varchar(128);
+18 -2
View File
@@ -75,8 +75,24 @@ function transformMysql() {
pgSql = pgSql.replaceAll(/text/g, 'longtext');
//双引号 替换成反引号
pgSql = pgSql.replaceAll(/"/g, '`');
//create table if not exists
pgSql = pgSql.replaceAll(/CREATE TABLE ([ ]+)`/g, 'CREATE TABLE IF NOT EXISTS `');
//提取所有的 create table 的表格name
const tableNames = pgSql.match(/CREATE TABLE `([^`]*)`/g);
if (tableNames && tableNames.length > 0) {
for (const item of tableNames) {
/**
* CREATE TABLE `cd_project`
CREATE TABLE `cd_project_member`
CREATE TABLE `cd_audit_log`
*/
//提取表名
const tableName = item.match(/`([^`]*)`/)[1];
pgSql += `\nALTER TABLE \`${tableName}\` ENGINE = InnoDB;`
}
}
fs.writeFileSync(`./migration-mysql/${notFile}`, pgSql);
}
+4 -4
View File
@@ -19,16 +19,16 @@
"dev-new": "cross-env NODE_ENV=dev-new mwtsc --watch --run @midwayjs/mock/app",
"rm-newdb": "rimraf ./data/db-new.sqlite",
"test": "cross-env NODE_ENV=unittest mocha",
"cov": "cross-env c8 --all --reporter=text --reporter=lcovonly npm run test",
"cov": "cross-env c8 --all --reporter=text --reporter=lcovonly pnpm run test",
"lint": "mwts check",
"lint:fix": "mwts fix",
"ci": "npm run cov",
"ci": "pnpm run cov",
"build-only": "cross-env NODE_ENV=production mwtsc --cleanOutDir --skipLibCheck",
"build": "npm run build-only && npm run export-metadata",
"build": "pnpm run build-only && pnpm run export-metadata",
"export-metadata": "node export-plugin-yaml.js",
"export-metadata-only": "node export-plugin-yaml.js docoff",
"dev-build": "echo 1",
"build-on-docker": "node ./before-build.js && npm run build-only && npm run export-metadata-only",
"build-on-docker": "node ./before-build.js && pnpm run build-only && pnpm run export-metadata-only",
"up-mw-deps": "npx midway-version -u -w",
"heap": "cross-env NODE_ENV=production clinic heapprofiler -- node --optimize-for-size --inspect ./bootstrap.js",
"flame": "clinic flame -- node ./bootstrap.js",
@@ -46,7 +46,7 @@ export class ConnectController extends BaseController {
throw new Error(`未配置该OAuth类型:${type}`);
}
const addon = await this.addonGetterService.getAddonById(setting.addonId, true, 0);
const addon = await this.addonGetterService.getAddonById(setting.addonId, true, 0,null);
if (!addon) {
throw new Error("初始化OAuth插件失败");
}
@@ -251,7 +251,7 @@ export class ConnectController extends BaseController {
provider.addonId = conf.addonId;
provider.addonTitle = addonEntity.name;
const addon = await this.addonGetterService.getAddonById(conf.addonId,true,0);
const addon = await this.addonGetterService.getAddonById(conf.addonId,true,0,null);
const {logoutUrl} = await addon.buildLogoutUrl();
if (logoutUrl){
provider.logoutUrl = logoutUrl;
@@ -28,9 +28,11 @@ export class OpenCertController extends BaseOpenController {
async get(@Body(ALL) bean: CertGetReq, @Query(ALL) query: CertGetReq) {
const openKey: OpenKey = this.ctx.openKey;
const userId = openKey.userId;
if (!userId) {
if (userId==null) {
throw new CodeException(Constants.res.openKeyError);
}
const projectId = openKey.projectId;
const req = merge({}, bean, query)
@@ -39,7 +41,8 @@ export class OpenCertController extends BaseOpenController {
domains: req.domains,
certId: req.certId,
autoApply: req.autoApply??false,
format: req.format
format: req.format,
projectId,
});
return this.ok(res);
}
@@ -28,7 +28,7 @@ export class UserController extends CrudController<UserService> {
return this.service;
}
@Post('/getSimpleUserByIds', {summary: 'sys:auth:user:add'})
@Post('/getSimpleUserByIds', {summary: 'sys:auth:user:view'})
async getSimpleUserByIds(@Body('ids') ids: number[]) {
const users = await this.service.find({
select: {
@@ -46,6 +46,21 @@ export class UserController extends CrudController<UserService> {
return this.ok(users);
}
@Post('/getSimpleUsers', {summary: 'sys:auth:user:view'})
async getSimpleUsers() {
const users = await this.service.find({
select: {
id: true,
username: true,
nickName: true,
mobile: true,
phoneCode: true,
},
});
return this.ok(users);
}
@Post('/page', {summary: 'sys:auth:user:view'})
async page(
@Body(ALL)
@@ -153,4 +168,6 @@ export class UserController extends CrudController<UserService> {
}
}
@@ -0,0 +1,74 @@
import { CrudController, SysSettingsService } from "@certd/lib-server";
import { ALL, Body, Controller, Inject, Post, Provide, Query } from "@midwayjs/core";
import { ProjectService } from "../../../modules/sys/enterprise/service/project-service.js";
import { ProjectEntity } from "../../../modules/sys/enterprise/entity/project.js";
import { merge } from "lodash-es";
/**
*/
@Provide()
@Controller("/api/sys/enterprise/project")
export class SysProjectController extends CrudController<ProjectEntity> {
@Inject()
service: ProjectService;
@Inject()
sysSettingsService: SysSettingsService;
getService<T>() {
return this.service;
}
@Post("/page", { summary: "sys:settings:view" })
async page(@Body(ALL) body: any) {
body.query = body.query ?? {};
return await super.page(body);
}
@Post("/list", { summary: "sys:settings:view" })
async list(@Body(ALL) body: any) {
return super.list(body);
}
@Post("/add", { summary: "sys:settings:edit" })
async add(@Body(ALL) bean: any) {
const def: any = {
isDefault: false,
disabled: false,
};
merge(bean, def);
bean.userId = this.getUserId();
return super.add({
...bean,
userId:0,
adminId: bean.userId,
});
}
@Post("/update", { summary: "sys:settings:edit" })
async update(@Body(ALL) bean: any) {
bean.userId = this.getUserId();
return super.update(bean);
}
@Post("/info", { summary: "sys:settings:view" })
async info(@Query("id") id: number) {
return super.info(id);
}
@Post("/delete", { summary: "sys:settings:edit" })
async delete(@Query("id") id: number) {
return super.delete(id);
}
@Post("/deleteByIds", { summary: "sys:settings:edit" })
async deleteByIds(@Body("ids") ids: number[]) {
const res = await this.service.delete(ids);
return this.ok(res);
}
@Post("/setDisabled", { summary: "sys:settings:edit" })
async setDisabled(@Body("id") id: number, @Body("disabled") disabled: boolean) {
await this.service.setDisabled(id, disabled);
return this.ok();
}
}
@@ -0,0 +1,111 @@
import { CrudController, SysSettingsService } from "@certd/lib-server";
import { ALL, Body, Controller, Inject, Post, Provide, Query } from "@midwayjs/core";
import { ProjectMemberEntity } from "../../../modules/sys/enterprise/entity/project-member.js";
import { ProjectMemberService } from "../../../modules/sys/enterprise/service/project-member-service.js";
import { merge } from "lodash-es";
import { ProjectService } from "../../../modules/sys/enterprise/service/project-service.js";
/**
*/
@Provide()
@Controller("/api/sys/enterprise/projectMember")
export class SysProjectMemberController extends CrudController<ProjectMemberEntity> {
@Inject()
service: ProjectMemberService;
@Inject()
sysSettingsService: SysSettingsService;
@Inject()
projectService: ProjectService;
getService<T>() {
return this.service;
}
@Post("/page", { summary: "sys:settings:view" })
async page(@Body(ALL) body: any) {
body.query = body.query ?? {};
return await super.page(body);
}
@Post("/list", { summary: "sys:settings:view" })
async list(@Body(ALL) body: any) {
return super.list(body);
}
@Post("/add", { summary: "sys:settings:edit" })
async add(@Body(ALL) bean: any) {
const def: any = {
isDefault: false,
disabled: false,
};
merge(bean, def);
await this.projectService.checkAdminPermission({
userId: this.getUserId(),
projectId: bean.projectId,
});
return super.add(bean);
}
@Post("/update", { summary: "sys:settings:edit" })
async update(@Body(ALL) bean: any) {
if (!bean.id) {
throw new Error("id is required");
}
const projectId = await this.service.getProjectId(bean.id)
await this.projectService.checkAdminPermission({
userId: this.getUserId(),
projectId: projectId,
});
return super.update({
id: bean.id,
permission: bean.permission,
});
}
@Post("/info", { summary: "sys:settings:view" })
async info(@Query("id") id: number) {
if (!id) {
throw new Error("id is required");
}
const projectId = await this.service.getProjectId(id)
await this.projectService.checkReadPermission({
userId: this.getUserId(),
projectId:projectId,
});
return super.info(id);
}
@Post("/delete", { summary: "sys:settings:edit" })
async delete(@Query("id") id: number) {
if (!id) {
throw new Error("id is required");
}
const projectId = await this.service.getProjectId(id)
await this.projectService.checkAdminPermission({
userId: this.getUserId(),
projectId:projectId,
});
return super.delete(id);
}
@Post("/deleteByIds", { summary: "sys:settings:edit" })
async deleteByIds(@Body("ids") ids: number[]) {
for (const id of ids) {
if (!id) {
throw new Error("id is required");
}
const projectId = await this.service.getProjectId(id)
await this.projectService.checkAdminPermission({
userId: this.getUserId(),
projectId:projectId,
});
await this.service.delete(id as any);
}
return this.ok({});
}
}
@@ -32,10 +32,12 @@ export class AddonController extends CrudController<AddonService> {
@Post("/page", { summary: Constants.per.authOnly })
async page(@Body(ALL) body) {
const {projectId,userId} = await this.getProjectUserIdRead();
body.query = body.query ?? {};
delete body.query.userId;
body.query.projectId = projectId;
const buildQuery = qb => {
qb.andWhere("user_id = :userId", { userId: this.getUserId() });
qb.andWhere("user_id = :userId", { userId });
};
const res = await this.service.page({
query: body.query,
@@ -48,14 +50,18 @@ export class AddonController extends CrudController<AddonService> {
@Post("/list", { summary: Constants.per.authOnly })
async list(@Body(ALL) body) {
const {projectId,userId} = await this.getProjectUserIdRead();
body.query = body.query ?? {};
body.query.userId = this.getUserId();
body.query.userId = userId;
body.query.projectId = projectId;
return super.list(body);
}
@Post("/add", { summary: Constants.per.authOnly })
async add(@Body(ALL) bean) {
bean.userId = this.getUserId();
const {userId,projectId} = await this.getProjectUserIdRead();
bean.userId = userId;
bean.projectId = projectId;
const type = bean.type;
const addonType = bean.addonType;
if (!type || !addonType) {
@@ -73,7 +79,7 @@ export class AddonController extends CrudController<AddonService> {
@Post("/update", { summary: Constants.per.authOnly })
async update(@Body(ALL) bean) {
await this.service.checkUserId(bean.id, this.getUserId());
await this.checkOwner(this.getService(), bean.id, "write");
const old = await this.service.info(bean.id);
if (!old) {
throw new ValidateException("Addon配置不存在");
@@ -90,18 +96,19 @@ export class AddonController extends CrudController<AddonService> {
}
}
delete bean.userId;
delete bean.projectId;
return super.update(bean);
}
@Post("/info", { summary: Constants.per.authOnly })
async info(@Query("id") id: number) {
await this.service.checkUserId(id, this.getUserId());
await this.checkOwner(this.getService(), id, "read");
return super.info(id);
}
@Post("/delete", { summary: Constants.per.authOnly })
async delete(@Query("id") id: number) {
await this.service.checkUserId(id, this.getUserId());
await this.checkOwner(this.getService(), id, "write");
return super.delete(id);
}
@@ -133,38 +140,42 @@ export class AddonController extends CrudController<AddonService> {
async simpleInfo(@Query("addonType") addonType: string, @Query("id") id: number) {
if (id === 0) {
//获取默认
const res = await this.service.getDefault(this.getUserId(), addonType);
const {projectId,userId} = await this.getProjectUserIdRead();
const res = await this.service.getDefault(userId, addonType,projectId);
if (!res) {
throw new ValidateException("默认Addon配置不存在");
}
const simple = await this.service.getSimpleInfo(res.id);
return this.ok(simple);
}
await this.authService.checkEntityUserId(this.ctx, this.service, id);
await this.checkOwner(this.getService(), id, "read",true);
const res = await this.service.getSimpleInfo(id);
return this.ok(res);
}
@Post("/getDefaultId", { summary: Constants.per.authOnly })
async getDefaultId(@Query("addonType") addonType: string) {
const res = await this.service.getDefault(this.getUserId(), addonType);
const {projectId,userId} = await this.getProjectUserIdRead();
const res = await this.service.getDefault(userId, addonType,projectId);
return this.ok(res?.id);
}
@Post("/setDefault", { summary: Constants.per.authOnly })
async setDefault(@Query("addonType") addonType: string, @Query("id") id: number) {
await this.service.checkUserId(id, this.getUserId());
const res = await this.service.setDefault(id, this.getUserId(), addonType);
const {projectId,userId} = await this.checkOwner(this.getService(), id, "write",true);
const res = await this.service.setDefault(id, userId, addonType,projectId);
return this.ok(res);
}
@Post("/options", { summary: Constants.per.authOnly })
async options(@Query("addonType") addonType: string) {
const {projectId,userId} = await this.getProjectUserIdRead();
const res = await this.service.list({
query: {
userId: this.getUserId(),
addonType
userId,
addonType,
projectId
}
});
for (const item of res) {
@@ -176,22 +187,16 @@ export class AddonController extends CrudController<AddonService> {
@Post("/handle", { summary: Constants.per.authOnly })
async handle(@Body(ALL) body: AddonRequestHandleReq) {
const userId = this.getUserId();
let inputAddon = body.input.addon;
if (body.input.id > 0) {
await this.checkOwner(this.getService(), body.input.id, "write",true);
const oldEntity = await this.service.info(body.input.id);
if (oldEntity) {
if (oldEntity.userId !== userId) {
throw new Error("addon not found");
}
// const param: any = {
// type: body.typeName,
// setting: JSON.stringify(body.input.access),
// };
inputAddon = JSON.parse(oldEntity.setting);
}
}
const serviceGetter = this.taskServiceBuilder.create({ userId });
const {projectId,userId} = await this.getProjectUserIdRead();
const serviceGetter = this.taskServiceBuilder.create({ userId,projectId });
const ctx = {
http: http,
@@ -20,10 +20,12 @@ export class GroupController extends CrudController<GroupService> {
@Post('/page', { summary: Constants.per.authOnly })
async page(@Body(ALL) body: any) {
const {projectId,userId} = await this.getProjectUserIdRead();
body.query = body.query ?? {};
body.query.projectId = projectId;
delete body.query.userId;
const buildQuery = qb => {
qb.andWhere('user_id = :userId', { userId: this.getUserId() });
qb.andWhere('user_id = :userId', { userId });
};
const res = await this.service.page({
query: body.query,
@@ -36,40 +38,47 @@ export class GroupController extends CrudController<GroupService> {
@Post('/list', { summary: Constants.per.authOnly })
async list(@Body(ALL) body: any) {
const {projectId,userId} = await this.getProjectUserIdRead();
body.query = body.query ?? {};
body.query.userId = this.getUserId();
body.query.projectId = projectId;
body.query.userId = userId;
return await super.list(body);
}
@Post('/add', { summary: Constants.per.authOnly })
async add(@Body(ALL) bean: any) {
bean.userId = this.getUserId();
const {projectId,userId} = await this.getProjectUserIdRead();
bean.projectId = projectId;
bean.userId = userId;
return await super.add(bean);
}
@Post('/update', { summary: Constants.per.authOnly })
async update(@Body(ALL) bean) {
await this.service.checkUserId(bean.id, this.getUserId());
await this.checkOwner(this.getService(), bean.id, "write");
delete bean.userId;
delete bean.projectId;
return await super.update(bean);
}
@Post('/info', { summary: Constants.per.authOnly })
async info(@Query('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
await this.checkOwner(this.getService(), id, "read");
return await super.info(id);
}
@Post('/delete', { summary: Constants.per.authOnly })
async delete(@Query('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
await this.checkOwner(this.getService(), id, "write");
return await super.delete(id);
}
@Post('/all', { summary: Constants.per.authOnly })
async all(@Query('type') type: string) {
const {projectId,userId} = await this.getProjectUserIdRead();
const list: any = await this.service.find({
where: {
userId: this.getUserId(),
projectId,
userId,
type,
},
});
@@ -18,8 +18,10 @@ export class DomainController extends CrudController<DomainService> {
@Post('/page', { summary: Constants.per.authOnly })
async page(@Body(ALL) body: any) {
const {projectId,userId} = await this.getProjectUserIdRead();
body.query = body.query ?? {};
body.query.userId = this.getUserId();
body.query.projectId = projectId;
body.query.userId = userId;
const domain = body.query.domain;
delete body.query.domain;
@@ -40,41 +42,48 @@ export class DomainController extends CrudController<DomainService> {
@Post('/list', { summary: Constants.per.authOnly })
async list(@Body(ALL) body: any) {
const {projectId,userId} = await this.getProjectUserIdRead();
body.query = body.query ?? {};
body.query.userId = this.getUserId();
body.query.projectId = projectId;
body.query.userId = userId;
const list = await this.getService().list(body);
return this.ok(list);
}
@Post('/add', { summary: Constants.per.authOnly })
async add(@Body(ALL) bean: any) {
bean.userId = this.getUserId();
const {projectId,userId} = await this.getProjectUserIdRead();
bean.projectId = projectId;
bean.userId = userId;
return super.add(bean);
}
@Post('/update', { summary: Constants.per.authOnly })
async update(@Body(ALL) bean: any) {
await this.service.checkUserId(bean.id, this.getUserId());
await this.checkOwner(this.getService(), bean.id, "write");
delete bean.userId;
delete bean.projectId;
return super.update(bean);
}
@Post('/info', { summary: Constants.per.authOnly })
async info(@Query('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
await this.checkOwner(this.getService(), id, "read");
return super.info(id);
}
@Post('/delete', { summary: Constants.per.authOnly })
async delete(@Query('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
await this.checkOwner(this.getService(), id, "write");
return super.delete(id);
}
@Post('/deleteByIds', { summary: Constants.per.authOnly })
async deleteByIds(@Body(ALL) body: any) {
const {projectId,userId} = await this.getProjectUserIdRead();
await this.service.delete(body.ids, {
userId: this.getUserId(),
userId: userId,
projectId: projectId,
});
return this.ok();
}
@@ -83,9 +92,12 @@ export class DomainController extends CrudController<DomainService> {
@Post('/import/start', { summary: Constants.per.authOnly })
async importStart(@Body(ALL) body: any) {
checkPlus();
const {projectId,userId} = await this.getProjectUserIdRead();
const { key } = body;
const req = {
key, userId: this.getUserId(),
key,
userId: userId,
projectId: projectId,
}
await this.service.startDomainImportTask(req);
return this.ok();
@@ -93,8 +105,10 @@ export class DomainController extends CrudController<DomainService> {
@Post('/import/status', { summary: Constants.per.authOnly })
async importStatus() {
const {projectId,userId} = await this.getProjectUserIdRead();
const req = {
userId: this.getUserId(),
userId: userId,
projectId: projectId,
}
const task = await this.service.getDomainImportTaskStatus(req);
return this.ok(task);
@@ -103,9 +117,11 @@ export class DomainController extends CrudController<DomainService> {
@Post('/import/delete', { summary: Constants.per.authOnly })
async importDelete(@Body(ALL) body: any) {
const {projectId,userId} = await this.getProjectUserIdRead();
const { key } = body;
const req = {
userId: this.getUserId(),
userId: userId,
projectId: projectId,
key,
}
await this.service.deleteDomainImportTask(req);
@@ -115,9 +131,11 @@ export class DomainController extends CrudController<DomainService> {
@Post('/import/save', { summary: Constants.per.authOnly })
async importSave(@Body(ALL) body: any) {
checkPlus();
const {projectId,userId} = await this.getProjectUserIdRead();
const { dnsProviderType, dnsProviderAccessId, key } = body;
const req = {
userId: this.getUserId(),
userId: userId,
projectId: projectId,
dnsProviderType, dnsProviderAccessId, key
}
const item = await this.service.saveDomainImportTask(req);
@@ -127,15 +145,19 @@ export class DomainController extends CrudController<DomainService> {
@Post('/sync/expiration/start', { summary: Constants.per.authOnly })
async syncExpirationStart(@Body(ALL) body: any) {
const {projectId,userId} = await this.getProjectUserIdRead();
await this.service.startSyncExpirationTask({
userId: this.getUserId(),
userId: userId,
projectId: projectId,
})
return this.ok();
}
@Post('/sync/expiration/status', { summary: Constants.per.authOnly })
async syncExpirationStatus(@Body(ALL) body: any) {
const {projectId,userId} = await this.getProjectUserIdRead();
const status = await this.service.getSyncExpirationTaskStatus({
userId: this.getUserId(),
userId: userId,
projectId: projectId,
})
return this.ok(status);
}
@@ -17,8 +17,10 @@ export class CnameRecordController extends CrudController<CnameRecordService> {
@Post('/page', { summary: Constants.per.authOnly })
async page(@Body(ALL) body: any) {
const {userId,projectId} = await this.getProjectUserIdRead();
body.query = body.query ?? {};
body.query.userId = this.getUserId();
body.query.userId = userId;
body.query.projectId = projectId;
const domain = body.query.domain;
delete body.query.domain;
@@ -39,22 +41,27 @@ export class CnameRecordController extends CrudController<CnameRecordService> {
@Post('/list', { summary: Constants.per.authOnly })
async list(@Body(ALL) body: any) {
const {userId,projectId} = await this.getProjectUserIdRead();
body.query = body.query ?? {};
body.query.userId = this.getUserId();
body.query.userId = userId;
body.query.projectId = projectId;
const list = await this.getService().list(body);
return this.ok(list);
}
@Post('/add', { summary: Constants.per.authOnly })
async add(@Body(ALL) bean: any) {
bean.userId = this.getUserId();
const {userId,projectId} = await this.getProjectUserIdWrite();
bean.userId = userId;
bean.projectId = projectId;
return super.add(bean);
}
@Post('/update', { summary: Constants.per.authOnly })
async update(@Body(ALL) bean: any) {
await this.service.checkUserId(bean.id, this.getUserId());
await this.checkOwner(this.getService(), bean.id, "write");
delete bean.userId;
delete bean.projectId;
return super.update(bean);
}
@@ -0,0 +1,27 @@
import { BaseController, Constants } from '@certd/lib-server';
import { ALL, Body, Controller, Inject, Post, Provide } from '@midwayjs/core';
import { AuthService } from '../../../modules/sys/authority/service/auth-service.js';
import { ProjectService } from '../../../modules/sys/enterprise/service/project-service.js';
/**
*/
@Provide()
@Controller('/api/enterprise/project')
export class UserProjectController extends BaseController {
@Inject()
service: ProjectService;
@Inject()
authService: AuthService;
getService(): ProjectService {
return this.service;
}
@Post('/list', { summary: Constants.per.authOnly })
async list(@Body(ALL) body: any) {
const userId= this.getUserId();
const res = await this.service.getUserProjects(userId);
return this.ok(res);
}
}
@@ -22,7 +22,7 @@ export class UserTwoFactorSettingController extends BaseController {
@Post("/get", { summary: Constants.per.authOnly })
async get() {
const userId = this.getUserId();
const setting = await this.service.getSetting<UserTwoFactorSetting>(userId, UserTwoFactorSetting);
const setting = await this.service.getSetting<UserTwoFactorSetting>(userId,null, UserTwoFactorSetting);
return this.ok(setting);
}
@@ -41,7 +41,7 @@ export class UserTwoFactorSettingController extends BaseController {
setting.authenticator.verified = false;
}
await this.service.saveSetting(userId, setting);
await this.service.saveSetting(userId,null, setting);
return this.ok({});
}
@@ -65,13 +65,14 @@ export class UserSettingsController extends CrudController<UserSettingsService>
@Post('/get', { summary: Constants.per.authOnly })
async get(@Query('key') key: string) {
const entity = await this.service.getByKey(key, this.getUserId());
const {projectId,userId} = await this.getProjectUserIdRead();
const entity = await this.service.getByKey(key, userId, projectId);
return this.ok(entity);
}
@Post("/grant/get", { summary: Constants.per.authOnly })
async grantSettingsGet() {
const userId = this.getUserId();
const setting = await this.service.getSetting<UserGrantSetting>(userId, UserGrantSetting);
const setting = await this.service.getSetting<UserGrantSetting>(userId, null, UserGrantSetting);
return this.ok(setting);
}
@@ -84,7 +85,7 @@ export class UserSettingsController extends CrudController<UserSettingsService>
const setting = new UserGrantSetting();
merge(setting, bean);
await this.service.saveSetting(userId, setting);
await this.service.saveSetting(userId,null, setting);
return this.ok({});
}
@@ -30,7 +30,10 @@ export class CertInfoController extends CrudController<CertInfoService> {
@Post('/page', { summary: Constants.per.authOnly })
async page(@Body(ALL) body: any) {
body.query = body.query ?? {};
body.query.userId = this.getUserId();
const { projectId, userId } = await this.getProjectUserIdRead()
body.query.projectId = projectId
body.query.userId = userId;
const domains = body.query?.domains;
delete body.query.domains;
@@ -76,17 +79,20 @@ export class CertInfoController extends CrudController<CertInfoService> {
@Post('/list', { summary: Constants.per.authOnly })
async list(@Body(ALL) body: any) {
body.query = body.query ?? {};
body.query.userId = this.getUserId();
const { projectId, userId } = await this.getProjectUserIdRead()
body.query.projectId = projectId
body.query.userId = userId;
return await super.list(body);
}
@Post('/getOptionsByIds', { summary: Constants.per.authOnly })
async getOptionsByIds(@Body(ALL) body: {ids:any[]}) {
const { projectId, userId } = await this.getProjectUserIdRead()
const list = await this.service.list({
query:{
userId: this.getUserId(),
projectId,
userId,
},
buildQuery: (bq: SelectQueryBuilder<any>) => {
bq.andWhere('id in (:...ids)', { ids: body.ids });
@@ -107,33 +113,38 @@ export class CertInfoController extends CrudController<CertInfoService> {
@Post('/add', { summary: Constants.per.authOnly })
async add(@Body(ALL) bean: any) {
bean.userId = this.getUserId();
const { projectId, userId } = await this.getProjectUserIdWrite()
bean.projectId = projectId
bean.userId = userId;
return await super.add(bean);
}
@Post('/update', { summary: Constants.per.authOnly })
async update(@Body(ALL) bean) {
await this.service.checkUserId(bean.id, this.getUserId());
await this.checkOwner(this.service,bean.id,"write");
delete bean.userId;
delete bean.projectId;
return await super.update(bean);
}
@Post('/info', { summary: Constants.per.authOnly })
async info(@Query('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
await this.checkOwner(this.service,id,"read");
return await super.info(id);
}
@Post('/delete', { summary: Constants.per.authOnly })
async delete(@Query('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
await this.checkOwner(this.service,id,"write");
return await super.delete(id);
}
@Post('/all', { summary: Constants.per.authOnly })
async all() {
const { projectId, userId } = await this.getProjectUserIdRead()
const list: any = await this.service.find({
where: {
userId: this.getUserId(),
projectId,
userId,
},
});
return this.ok(list);
@@ -143,7 +154,7 @@ export class CertInfoController extends CrudController<CertInfoService> {
@Post('/getCert', { summary: Constants.per.authOnly })
async getCert(@Query('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
await this.checkOwner(this.getService(),id,"read");
const certInfoEntity = await this.service.info(id);
const certInfo = JSON.parse(certInfoEntity.certInfo);
return this.ok(certInfo);
@@ -151,7 +162,8 @@ export class CertInfoController extends CrudController<CertInfoService> {
@Get('/download', { summary: Constants.per.authOnly })
async download(@Query('id') id: number) {
const certInfo = await this.service.info(id)
await this.checkOwner(this.getService(),id,"read");
const certInfo = await this.getService().info(id)
if (certInfo == null) {
throw new CommonException('file not found');
}
@@ -26,7 +26,9 @@ export class SiteInfoController extends CrudController<SiteInfoService> {
@Post('/page', { summary: Constants.per.authOnly })
async page(@Body(ALL) body: any) {
body.query = body.query ?? {};
body.query.userId = this.getUserId();
const { projectId, userId } = await this.getProjectUserIdRead()
body.query.projectId = projectId
body.query.userId = userId;
const certDomains = body.query.certDomains;
const domain = body.query.domain;
const name = body.query.name;
@@ -55,13 +57,17 @@ export class SiteInfoController extends CrudController<SiteInfoService> {
@Post('/list', { summary: Constants.per.authOnly })
async list(@Body(ALL) body: any) {
body.query = body.query ?? {};
body.query.userId = this.getUserId();
const { projectId, userId } = await this.getProjectUserIdRead()
body.query.projectId = projectId
body.query.userId = userId;
return await super.list(body);
}
@Post('/add', { summary: Constants.per.authOnly })
async add(@Body(ALL) bean: any) {
bean.userId = this.getUserId();
const { projectId, userId } = await this.getProjectUserIdWrite()
bean.projectId = projectId
bean.userId = userId;
const res = await this.service.add(bean);
const entity = await this.service.info(res.id);
if (entity.disabled) {
@@ -72,8 +78,9 @@ export class SiteInfoController extends CrudController<SiteInfoService> {
@Post('/update', { summary: Constants.per.authOnly })
async update(@Body(ALL) bean) {
await this.service.checkUserId(bean.id, this.getUserId());
await this.checkOwner(this.service,bean.id,"write");
delete bean.userId;
delete bean.projectId;
await this.service.update(bean);
const entity = await this.service.info(bean.id);
if (entity.disabled) {
@@ -83,27 +90,27 @@ export class SiteInfoController extends CrudController<SiteInfoService> {
}
@Post('/info', { summary: Constants.per.authOnly })
async info(@Query('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
await this.checkOwner(this.service,id,"read");
return await super.info(id);
}
@Post('/delete', { summary: Constants.per.authOnly })
async delete(@Query('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
await this.checkOwner(this.service,id,"write");
return await super.delete(id);
}
@Post('/batchDelete', { summary: Constants.per.authOnly })
async batchDelete(@Body(ALL) body: any) {
const userId = this.getUserId();
await this.service.batchDelete(body.ids,userId);
const { projectId, userId } = await this.getProjectUserIdWrite()
await this.service.batchDelete(body.ids,userId,projectId);
return this.ok();
}
@Post('/check', { summary: Constants.per.authOnly })
async check(@Body('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
await this.checkOwner(this.service,id,"read");
await this.service.check(id, true, 0);
await utils.sleep(1000);
return this.ok();
@@ -111,26 +118,27 @@ export class SiteInfoController extends CrudController<SiteInfoService> {
@Post('/checkAll', { summary: Constants.per.authOnly })
async checkAll() {
const userId = this.getUserId();
await this.service.checkAllByUsers(userId);
const { projectId, userId } = await this.getProjectUserIdWrite()
await this.service.checkAllByUsers(userId,projectId);
return this.ok();
}
@Post('/import', { summary: Constants.per.authOnly })
async doImport(@Body(ALL) body: any) {
const userId = this.getUserId();
const { projectId, userId } = await this.getProjectUserIdWrite()
await this.service.doImport({
text:body.text,
groupId:body.groupId,
userId
userId,
projectId
})
return this.ok();
}
@Post('/ipCheckChange', { summary: Constants.per.authOnly })
async ipCheckChange(@Body(ALL) bean: any) {
const userId = this.getUserId();
await this.service.checkUserId(bean.id, userId)
await this.checkOwner(this.service,bean.id,"read");
await this.service.ipCheckChange({
id: bean.id,
ipCheck: bean.ipCheck
@@ -140,8 +148,7 @@ export class SiteInfoController extends CrudController<SiteInfoService> {
@Post('/disabledChange', { summary: Constants.per.authOnly })
async disabledChange(@Body(ALL) bean: any) {
const userId = this.getUserId();
await this.service.checkUserId(bean.id, userId)
await this.checkOwner(this.service,bean.id,"write");
await this.service.disabledChange({
id: bean.id,
disabled: bean.disabled
@@ -151,18 +158,18 @@ export class SiteInfoController extends CrudController<SiteInfoService> {
@Post("/setting/get", { summary: Constants.per.authOnly })
async get() {
const userId = this.getUserId();
const setting = await this.service.getSetting(userId)
const { userId, projectId } = await this.getProjectUserIdRead()
const setting = await this.service.getSetting(userId, projectId)
return this.ok(setting);
}
@Post("/setting/save", { summary: Constants.per.authOnly })
async save(@Body(ALL) bean: any) {
const userId = this.getUserId();
const { userId, projectId} = await this.getProjectUserIdWrite()
const setting = new UserSiteMonitorSetting();
merge(setting, bean);
await this.service.saveSetting(userId, setting);
await this.service.saveSetting(userId, projectId,setting);
return this.ok({});
}
@@ -22,8 +22,10 @@ export class SiteInfoController extends CrudController<SiteIpService> {
@Post('/page', { summary: Constants.per.authOnly })
async page(@Body(ALL) body: any) {
const { projectId, userId } = await this.getProjectUserIdRead()
body.query = body.query ?? {};
body.query.userId = this.getUserId();
body.query.userId = userId;
body.query.projectId = projectId
const res = await this.service.page({
query: body.query,
page: body.page,
@@ -35,13 +37,17 @@ export class SiteInfoController extends CrudController<SiteIpService> {
@Post('/list', { summary: Constants.per.authOnly })
async list(@Body(ALL) body: any) {
body.query = body.query ?? {};
body.query.userId = this.getUserId();
const { projectId, userId } = await this.getProjectUserIdRead()
body.query.userId = userId;
body.query.projectId = projectId
return await super.list(body);
}
@Post('/add', { summary: Constants.per.authOnly })
async add(@Body(ALL) bean: any) {
bean.userId = this.getUserId();
const { projectId, userId } = await this.getProjectUserIdWrite()
bean.userId = userId;
bean.projectId = projectId
bean.from = "manual"
const res = await this.service.add(bean);
const siteEntity = await this.siteInfoService.info(bean.siteId);
@@ -54,8 +60,9 @@ export class SiteInfoController extends CrudController<SiteIpService> {
@Post('/update', { summary: Constants.per.authOnly })
async update(@Body(ALL) bean) {
await this.service.checkUserId(bean.id, this.getUserId());
await this.checkOwner(this.service,bean.id,"write");
delete bean.userId;
delete bean.projectId;
await this.service.update(bean);
const siteEntity = await this.siteInfoService.info(bean.siteId);
if(!siteEntity.disabled){
@@ -66,23 +73,24 @@ export class SiteInfoController extends CrudController<SiteIpService> {
}
@Post('/info', { summary: Constants.per.authOnly })
async info(@Query('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
await this.checkOwner(this.service,id,"read");
return await super.info(id);
}
@Post('/delete', { summary: Constants.per.authOnly })
async delete(@Query('id') id: number) {
await this.checkOwner(this.service,id,"write");
const entity = await this.service.info(id);
await this.service.checkUserId(id, this.getUserId());
const res = await super.delete(id);
await this.service.updateIpCount(entity.siteId)
return res
}
@Post('/check', { summary: Constants.per.authOnly })
async check(@Body('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
await this.checkOwner(this.service,id,"read");
const entity = await this.service.info(id);
const siteEntity = await this.siteInfoService.info(entity.siteId);
const domain = siteEntity.domain;
@@ -93,8 +101,7 @@ export class SiteInfoController extends CrudController<SiteIpService> {
@Post('/checkAll', { summary: Constants.per.authOnly })
async checkAll(@Body('siteId') siteId: number) {
const userId = this.getUserId();
await this.siteInfoService.checkUserId(siteId, userId);
await this.getProjectUserIdRead()
const siteEntity = await this.siteInfoService.info(siteId);
await this.service.syncAndCheck(siteEntity);
return this.ok();
@@ -102,22 +109,20 @@ export class SiteInfoController extends CrudController<SiteIpService> {
@Post('/sync', { summary: Constants.per.authOnly })
async sync(@Body('siteId') siteId: number) {
const userId = this.getUserId();
await this.getProjectUserIdWrite()
const entity = await this.siteInfoService.info(siteId)
if(entity.userId != userId){
throw new Error('无权限')
}
await this.service.sync(entity);
return this.ok();
}
@Post('/import', { summary: Constants.per.authOnly })
async doImport(@Body(ALL) body: any) {
const userId = this.getUserId();
const { userId, projectId } = await this.getProjectUserIdWrite()
await this.service.doImport({
text:body.text,
userId,
siteId:body.siteId
siteId:body.siteId,
projectId
})
return this.ok();
}
@@ -19,8 +19,10 @@ export class OpenKeyController extends CrudController<OpenKeyService> {
@Post('/page', { summary: Constants.per.authOnly })
async page(@Body(ALL) body: any) {
const {projectId,userId} = await this.getProjectUserIdRead();
body.query = body.query ?? {};
body.query.userId = this.getUserId();
body.query.projectId = projectId;
body.query.userId = userId;
const res = await this.service.page({
query: body.query,
page: body.page,
@@ -31,40 +33,45 @@ export class OpenKeyController extends CrudController<OpenKeyService> {
@Post('/list', { summary: Constants.per.authOnly })
async list(@Body(ALL) body: any) {
const {projectId,userId} = await this.getProjectUserIdRead();
body.query = body.query ?? {};
body.query.userId = this.getUserId();
body.query.projectId = projectId;
body.query.userId = userId;
return await super.list(body);
}
@Post('/add', { summary: Constants.per.authOnly })
async add(@Body(ALL) body: any) {
body.userId = this.getUserId();
const {projectId,userId} = await this.getProjectUserIdRead();
body.projectId = projectId;
body.userId = userId;
const res = await this.service.add(body);
return this.ok(res);
}
@Post('/update', { summary: Constants.per.authOnly })
async update(@Body(ALL) bean) {
await this.service.checkUserId(bean.id, this.getUserId());
await this.checkOwner(this.getService(), bean.id, "write");
delete bean.userId;
delete bean.projectId;
await this.service.update(bean);
return this.ok();
}
@Post('/info', { summary: Constants.per.authOnly })
async info(@Query('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
await this.checkOwner(this.getService(), id, "read");
return await super.info(id);
}
@Post('/delete', { summary: Constants.per.authOnly })
async delete(@Query('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
await this.checkOwner(this.getService(), id, "write");
return await super.delete(id);
}
@Post('/getApiToken', { summary: Constants.per.authOnly })
async getApiToken(@Body('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
await this.checkOwner(this.getService(), id, "write");
const token = await this.service.getApiToken(id);
return this.ok(token);
}
@@ -21,9 +21,11 @@ export class AccessController extends CrudController<AccessService> {
@Post('/page', { summary: Constants.per.authOnly })
async page(@Body(ALL) body) {
const { projectId, userId } = await this.getProjectUserIdRead()
body.query = body.query ?? {};
delete body.query.userId;
body.query.userId = this.getUserId()
body.query.userId = userId;
body.query.projectId = projectId;
let name = body.query?.name;
delete body.query.name;
const buildQuery = qb => {
@@ -42,32 +44,37 @@ export class AccessController extends CrudController<AccessService> {
@Post('/list', { summary: Constants.per.authOnly })
async list(@Body(ALL) body) {
const { projectId, userId } = await this.getProjectUserIdRead()
body.query = body.query ?? {};
body.query.userId = this.getUserId();
body.query.userId = userId;
body.query.projectId = projectId;
return super.list(body);
}
@Post('/add', { summary: Constants.per.authOnly })
async add(@Body(ALL) bean) {
bean.userId = this.getUserId();
const { projectId, userId } = await this.getProjectUserIdWrite()
bean.userId = userId;
bean.projectId = projectId;
return super.add(bean);
}
@Post('/update', { summary: Constants.per.authOnly })
async update(@Body(ALL) bean) {
await this.service.checkUserId(bean.id, this.getUserId());
await this.checkOwner(this.getService(), bean.id, "write");
delete bean.userId;
delete bean.projectId;
return super.update(bean);
}
@Post('/info', { summary: Constants.per.authOnly })
async info(@Query('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
await this.checkOwner(this.getService(), id, "read");
return super.info(id);
}
@Post('/delete', { summary: Constants.per.authOnly })
async delete(@Query('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
await this.checkOwner(this.getService(), id, "write");
return super.delete(id);
}
@@ -79,7 +86,8 @@ export class AccessController extends CrudController<AccessService> {
@Post('/getSecretPlain', { summary: Constants.per.authOnly })
async getSecretPlain(@Body(ALL) body: { id: number; key: string }) {
const value = await this.service.getById(body.id, this.getUserId());
const {userId, projectId} = await this.checkOwner(this.getService(), body.id, "read");
const value = await this.service.getById(body.id, userId, projectId);
return this.ok(value[body.key]);
}
@@ -102,14 +110,16 @@ export class AccessController extends CrudController<AccessService> {
@Post('/simpleInfo', { summary: Constants.per.authOnly })
async simpleInfo(@Query('id') id: number) {
await this.authService.checkEntityUserId(this.ctx, this.service, id);
// await this.authService.checkUserIdButAllowAdmin(this.ctx, this.service, id);
await this.checkOwner(this.getService(), id, "read",true);
const res = await this.service.getSimpleInfo(id);
return this.ok(res);
}
@Post('/getDictByIds', { summary: Constants.per.authOnly })
async getDictByIds(@Body('ids') ids: number[]) {
const res = await this.service.getSimpleByIds(ids, this.getUserId());
const { userId, projectId } = await this.getProjectUserIdRead()
const res = await this.service.getSimpleByIds(ids, userId, projectId);
return this.ok(res);
}
}
@@ -21,9 +21,8 @@ export class CertController extends BaseController {
@Post('/get', { summary: Constants.per.authOnly })
async getCert(@Query('id') id: number) {
const userId = this.getUserId();
const {userId} = await this.getProjectUserIdRead()
const pipleinUserId = await this.pipelineService.getPipelineUserId(id);
@@ -34,7 +33,7 @@ export class CertController extends BaseController {
throw new PermissionException();
}
// 是否允许管理员查看
const setting = await this.userSettingsService.getSetting<UserGrantSetting>(pipleinUserId, UserGrantSetting, false);
const setting = await this.userSettingsService.getSetting<UserGrantSetting>(pipleinUserId,null, UserGrantSetting, false);
if (setting?.allowAdminViewCerts !== true) {
//不允许管理员查看
throw new PermissionException("该流水线的用户还未授权管理员查看证书,请先让用户在”设置->授权委托“中打开开关");

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