mirror of
https://github.com/certd/certd.git
synced 2026-10-15 03:01:02 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcbf1045fe | ||
|
|
f5cfe054b7 | ||
|
|
5ded5ff2b6 | ||
|
|
8d31346cd3 | ||
|
|
44b41b6a08 | ||
|
|
4420472f8f | ||
|
|
c4497a7018 | ||
|
|
03f1a7bd0e | ||
|
|
97d2b50074 | ||
|
|
2d0b85b305 | ||
|
|
28052c9387 | ||
|
|
c4277350bc | ||
|
|
97160373d6 | ||
|
|
0ce24618a1 | ||
|
|
f60e63fbaf | ||
|
|
2735feb330 | ||
|
|
b6381d79fc | ||
|
|
241b2e5584 | ||
|
|
bb1f232f41 | ||
|
|
f82d3a5fcc | ||
|
|
bbeaf617d1 | ||
|
|
ea7d14c289 | ||
|
|
3272a68ff6 | ||
|
|
b5715d99ff | ||
|
|
3f5ba14fde | ||
|
|
07f0d3b4d6 | ||
|
|
3dbbe72fc1 | ||
|
|
7f006d1359 | ||
|
|
d30f1c4785 | ||
|
|
96e0122b93 | ||
|
|
7786a87b21 | ||
|
|
34476ba14f | ||
|
|
4b942385a8 |
@@ -3,7 +3,7 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "版本号(如 v1.42.5)"
|
||||
description: "版本号(如 1.42.5)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
|
||||
+2
-1
@@ -42,4 +42,5 @@ output/
|
||||
.uploads/
|
||||
.certd-plugin-history/
|
||||
.tmp
|
||||
.plugin-dev/
|
||||
.plugin-dev/
|
||||
.tmp_chunks/
|
||||
@@ -392,6 +392,66 @@ export class OAuthDemoAccess extends BaseAccess {
|
||||
}
|
||||
```
|
||||
|
||||
## 字段动态显隐(mergeScript)
|
||||
|
||||
多种鉴权方式共用一个授权时,用 `@AccessInput` 上的 `mergeScript` 按所选方式显隐字段:
|
||||
|
||||
```typescript
|
||||
@AccessInput({
|
||||
title: '鉴权方式',
|
||||
value: 'apikey',
|
||||
component: {
|
||||
name: 'a-select',
|
||||
vModel: 'value',
|
||||
options: [
|
||||
{ label: 'AccessKey签名', value: 'aksk' },
|
||||
{ label: 'ApiKey', value: 'apikey' },
|
||||
],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
authType = 'apikey';
|
||||
|
||||
@AccessInput({
|
||||
title: 'accessKeyId',
|
||||
component: { name: 'a-input', vModel: 'value' },
|
||||
required: true,
|
||||
mergeScript: `
|
||||
return {
|
||||
show: ctx.compute(({form})=>{
|
||||
return !form.access.authType || form.access.authType === 'aksk';
|
||||
})
|
||||
}
|
||||
`,
|
||||
})
|
||||
accessKeyId = '';
|
||||
```
|
||||
|
||||
要点:
|
||||
|
||||
- `mergeScript` 返回的对象会深合并进本字段定义,可动态化的属性有 `show`、`required`、`helper`、
|
||||
`component` 下的属性(`options`、`placeholder` 等)以及 `component.on` 事件。
|
||||
- **动态判断必须写在 `ctx.compute(({form})=>{ ... })` 回调里**。`mergeScript` 本身在插件类
|
||||
定义阶段就被拼成字符串,那时表单还没打开,用参数在服务端判断字段值没有意义。
|
||||
- 授权表单字段值在 `form.access.xxx`(任务表单才是 `form.xxx`)。
|
||||
- 历史授权没有新字段,条件里要用 `!form.access.authType || ...` 兜底走旧逻辑,保证老授权还能编辑保存。
|
||||
- 联动其他字段用 `component.on` 事件回调,回调参数是 `{ form }`:
|
||||
|
||||
```typescript
|
||||
mergeScript: `
|
||||
return {
|
||||
component: {
|
||||
on: {
|
||||
selectedChange: (scope) => {
|
||||
const form = scope.form;
|
||||
form.acmeAccountAccessId = null;
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`,
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **插件命名**:插件名称应简洁明了,反映其功能。
|
||||
|
||||
@@ -9,7 +9,7 @@ description: 用于通过 Certd API 开发、修改、读取、保存和恢复
|
||||
|
||||
本 Skill 使用 Certd HTTP API,不使用 WebSocket,也不依赖浏览器传递代码草稿。
|
||||
|
||||
- 前端只生成包含需求、API 地址和认证 Token 的启动提示词。
|
||||
- 前端只生成包含需求、插件作者、API 地址和认证 Token 的启动提示词。
|
||||
- Agent 直接调用 Certd API 读取和保存插件 YAML。
|
||||
- Agent 的进度、日志和代码修改在 Codex/Trae 中查看。
|
||||
- 插件开发临时文件和修改历史统一保存在 Agent 工作区的 `.plugin-dev/` 下,不保存到浏览器或 Certd 后端。
|
||||
@@ -43,15 +43,13 @@ Certd 插件按来源分为三类:
|
||||
|
||||
## API 认证
|
||||
|
||||
提示词会提供 Certd API 地址和仅限 AI 插件开发接口的受限 Token。调用 API 时使用:
|
||||
提示词会提供 Certd API 地址和仅限 AI 插件开发接口的受限的短时效的 Token。调用 API 时使用:
|
||||
|
||||
```http
|
||||
Authorization: <token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
不要把 Token 写入代码、历史摘要、日志、提交信息或插件 YAML。
|
||||
|
||||
所有 Certd API 请求统一使用 Node.js 的 `fetch`。不要使用 PowerShell 的 `Invoke-RestMethod`、`Invoke-WebRequest` 或 .NET HTTP 客户端发送插件 YAML/JSON;它们在 Windows 上可能造成中文乱码或使完整 YAML 导入请求长时间无响应。
|
||||
|
||||
Node 请求须直接读取 UTF-8 文件或在 Node 内构造 JSON,并使用 `JSON.stringify`:
|
||||
@@ -118,6 +116,7 @@ const response = await fetch(`${apiBase}/scoped/sys/ai/plugin/find`, {
|
||||
- 修改后保存修改摘要。
|
||||
- 恢复前再次备份当前版本。
|
||||
- 不上传历史文件,不保存 Token、证书、私钥或真实授权值。
|
||||
- 修改完一版后立即提交更新到certd,但不要发布到市场。
|
||||
|
||||
详细格式见 `references/local-history.md`。
|
||||
|
||||
@@ -125,6 +124,7 @@ const response = await fetch(`${apiBase}/scoped/sys/ai/plugin/find`, {
|
||||
|
||||
插件始终以完整 YAML 传递和保存,脚本源码放在顶层 `content` 字段。
|
||||
|
||||
- 新插件的 `author` 必须使用启动提示词中给出的插件作者,不要自行编造作者名;修改已有插件时保留原 `author`,不要改成其他作者。
|
||||
- 统一使用 `await _ctx.import(...)` 引用模块。
|
||||
- `"/@/..."` 表示以绝对路径引用 `server/src/` 下的模块。
|
||||
- 最后返回继承目标基类的 class。
|
||||
@@ -135,6 +135,28 @@ const response = await fetch(`${apiBase}/scoped/sys/ai/plugin/find`, {
|
||||
|
||||
需要字段格式时读取 `references/online-yaml-format.md`。
|
||||
需要组件示例时读取 `references/component-examples.md`。
|
||||
需要按其他字段动态显隐/必填/联动时读取 `references/merge-script.md`。
|
||||
|
||||
### 字段动态显隐(mergeScript)
|
||||
|
||||
字段定义支持 `mergeScript`:脚本 `return` 的对象会合并进本字段,用来做「选了 A 才显示 B」这类联动。
|
||||
|
||||
```yaml
|
||||
mergeScript: |2-
|
||||
|
||||
return {
|
||||
show: ctx.compute(({form})=>{
|
||||
return form.access.authType === 'apikey';
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
- 动态逻辑必须写在 `ctx.compute(({form})=>{ ... })` 回调里;`mergeScript` 本身在插件类定义阶段就
|
||||
被拼成字符串,那时表单还没打开,用参数在 node 侧判断字段值没有任何意义。
|
||||
- 授权表单字段值在 `form.access.xxx`,任务表单字段值在 `form.xxx`。
|
||||
- 新增可选字段时要给历史数据兜底,例如 `!form.access.authType || form.access.authType === 'aksk'`。
|
||||
|
||||
完整用法(能力、可动态化的属性、事件联动、常见错误、验证方式)见 `references/merge-script.md`。
|
||||
|
||||
## 示例插件
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: DemoAccess
|
||||
icon: logos:airflow-icon
|
||||
title: Demo-授权插件示例 # 模块-插件名
|
||||
title: Demo-授权插件示例 # 格式:平台-功能 ,例如: 网宿-授权 ; 阿里云-授权 ; 天翼云-授权
|
||||
group: null
|
||||
desc: 这只是一个示例
|
||||
version: 1.0.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: DemoDeploy
|
||||
icon: logos:amp-icon
|
||||
title: Demo-部署插件示例 # 模块-插件名
|
||||
title: Demo-部署插件示例 # 格式:平台-功能 ,例如: 网宿-更新CDNpro证书 ; 阿里云-上传到CAS证书管理服务 ; 天翼云-更新CDN证书
|
||||
group: cdn
|
||||
desc: 这仅仅是一个示例
|
||||
version: 1.0.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: DemoDnsProvider
|
||||
icon: fa-solid:frog
|
||||
title: Demo-Dns提供商插件示例 # 模块-插件名
|
||||
title: Demo-Dns提供商插件示例 # 格式:平台-功能 ,例如: 阿里云-DNS提供商 ;腾讯云-DNS提供商
|
||||
desc: 这只是一个示例
|
||||
type: custom
|
||||
version: 1.0.0
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
# mergeScript 用法
|
||||
|
||||
来源:前端 `packages/ui/certd-client/src/use/use-refrence.tsx`,类型定义来自 `@fast-crud/fast-crud`。
|
||||
|
||||
`mergeScript` 用来让输入字段的定义**依赖当前表单里的其他字段值动态变化**,例如:
|
||||
选了某个鉴权方式才显示对应字段、切换类型后清空已选值、下拉选项跟着另一个字段变。
|
||||
|
||||
它写成字符串放在字段定义上(内置插件放在 `@AccessInput` / `@TaskInput` 里,在线插件放在 `input.<字段>` 下):
|
||||
|
||||
```ts
|
||||
@AccessInput({
|
||||
title: "API Key",
|
||||
component: { name: "a-input", vModel: "value" },
|
||||
mergeScript: `
|
||||
return {
|
||||
show: ctx.compute(({form})=>{
|
||||
//access里面 要用 form.access.[字段名]
|
||||
return form.access.authType === 'apikey';
|
||||
//deploy插件里面要用form.[字段名]
|
||||
return form.authType === 'apikey';
|
||||
})
|
||||
}
|
||||
`,
|
||||
})
|
||||
apiKey = "";
|
||||
```
|
||||
|
||||
## 执行时机
|
||||
|
||||
字段定义被前端加载时,对**每一个字段**执行一次:
|
||||
|
||||
```js
|
||||
const func = new Function('ctx', formItem.mergeScript);
|
||||
const merged = func(ctx); // 字段脚本只执行这一次
|
||||
merge(formItem, merged); // 返回值深合并进该字段的定义
|
||||
delete formItem.mergeScript; // 执行完就删掉,最终定义里不再有 mergeScript
|
||||
```
|
||||
|
||||
要点:
|
||||
|
||||
- 脚本体内**必须 `return` 一个对象**,返回的对象会深合并到本字段的定义上。
|
||||
- 脚本本身只在加载字段时跑一次,**拿不到、也不应该判断用户填了什么值**。
|
||||
- 真正的动态逻辑写在返回对象里的 `ctx.compute(...)` 回调中,由 fast-crud 在渲染时求值。
|
||||
|
||||
## ctx 提供的能力
|
||||
|
||||
| 能力 | 说明 |
|
||||
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `ctx.compute(fn)` | 最常用。返回一个响应式的计算值:`fn` 里读到的表单字段会被自动追踪,字段变化时重新计算,视图跟着刷新 |
|
||||
|
||||
`ctx.compute` 的回调参数是一个 context 对象,常用字段:
|
||||
|
||||
- `form`:当前表单数据。**这是唯一常用的一个**。
|
||||
- `row`:行数据。
|
||||
- `mode`:表单模式 `add` / `edit` / `view`。
|
||||
- `getComponentRef(key)`:取其他字段组件的 ref。
|
||||
|
||||
注意:Certd 在把 `form` 传进回调前做过一次兼容处理(`form.input || form.body || form`),
|
||||
所以两种表单取值路径不同:
|
||||
|
||||
- **授权(Access)表单**:字段值在 `form.access.xxx`(字段 key 会被加上 `access.` 前缀)。
|
||||
- **任务(Task)表单**:字段值直接是 `form.xxx`。
|
||||
|
||||
## 可以动态化的属性
|
||||
|
||||
返回的对象里,任何字段定义属性都可以用 `ctx.compute`,常用的有:
|
||||
|
||||
| 属性 | 用途 |
|
||||
| -------------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| `show` | 显隐(最常用) |
|
||||
| `required` | 动态必填,例如 `required: ctx.compute(({form}) => form.targetType === 'ingress')` |
|
||||
| `disabled` | 动态禁用,例如账号已创建后不允许再改:`disabled: ctx.compute(({form}) => !!form.access?.account)` |
|
||||
| `helper` | 动态帮助文案,回调里按条件 `return` 不同字符串 |
|
||||
| `value` | 动态默认值 |
|
||||
| `component` 下的属性 | 动态 `options`、`placeholder`、`inputKey`、`type`、`disabled` 等 |
|
||||
| `component.on` | 事件处理,见下面「联动」 |
|
||||
| `col` / `order` | 布局与排序 |
|
||||
|
||||
只写了一层 `show` 时最简形式就是本文开头的例子。
|
||||
|
||||
## 联动(监听事件并改动其他字段)
|
||||
|
||||
`component.on` 的事件回调**不**用 `ctx.compute` 包,它本身就是回调,参数是
|
||||
`{ form, $event, ... }`;`form` 同样做过 `form.input || form.body || form` 的兼容处理。
|
||||
事件名按组件而定,例如 `selectedChange`。
|
||||
|
||||
```ts
|
||||
mergeScript: `
|
||||
return {
|
||||
component: {
|
||||
on: {
|
||||
selectedChange: (scope) => {
|
||||
const form = scope.form;
|
||||
// 切换颁发机构后清空 ACME 账号,避免账号与机构不匹配
|
||||
form.acmeAccountAccessId = null;
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`,
|
||||
```
|
||||
|
||||
另一种写法是把回调也做成计算值(需要用到组件上配置的 `onSelectedChange` / `onChange` 属性):
|
||||
|
||||
```ts
|
||||
mergeScript: `
|
||||
return {
|
||||
component: {
|
||||
onSelectedChange: ctx.compute(({form}) => {
|
||||
return ($event) => {
|
||||
form.dnsProviderAccessType = $event.accessType;
|
||||
};
|
||||
}),
|
||||
},
|
||||
}
|
||||
`,
|
||||
```
|
||||
|
||||
两种写法都能用,优先参考同目录已有插件的写法,保持一致。
|
||||
|
||||
## 常见错误
|
||||
|
||||
- **不要用 node 侧变量决定条件**。`mergeScript` 是在插件类定义阶段(node 侧)拼出来的字符串,
|
||||
此时表单还没打开,拿不到任何用户输入;用参数拼条件等于把逻辑写死在服务端。
|
||||
条件必须原样落到 `ctx.compute` 回调里。
|
||||
- **不要用 `if` 直接判断字段值**。回调必须 `return` 一个值,写成
|
||||
`show: ctx.compute(({form}) => { if (...) { return true } })` 也可以,但不要漏掉 `return`。
|
||||
- **不要漏掉历史数据兼容**。新增可选字段时,历史数据里没有这个字段(值为 `undefined`),
|
||||
条件要按旧行为兜底,例如 `!form.access.authType || form.access.authType === 'aksk'`。
|
||||
- **不要访问 `form.access` 以外的路径**去读授权字段,也不要在授权表单里按任务表单的路径取值。
|
||||
- **不要依赖脚本执行时的副作用**(打印日志、改全局状态),它在每次打开表单时都会重新执行。
|
||||
|
||||
## YAML 写法(在线插件)
|
||||
|
||||
`mergeScript` 是含换行的脚本字符串,必须用 YAML 块标量:
|
||||
|
||||
```yaml
|
||||
input:
|
||||
apiKey:
|
||||
title: API Key
|
||||
component:
|
||||
name: a-input
|
||||
vModel: value
|
||||
mergeScript: |2-
|
||||
|
||||
return {
|
||||
show: ctx.compute(({form})=>{
|
||||
return form.access.authType === 'apikey';
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
导出生成的缩进比较特殊(块标量内容统一缩进),**不要手工调整缩进**:
|
||||
用脚本生成 YAML,并保证导出后再导入内容一致。转换成 block scalar 时注意:
|
||||
|
||||
- `|` / `|2-` 等块标量不需要转义换行,不要把脚本写成带 `\n` 的单行。
|
||||
- 脚本里不要出现 `??` 这类容易被误判为乱码的写法,用显式 `if (x == null)` 代替。
|
||||
|
||||
## 验证方式
|
||||
|
||||
前端表单没法在插件开发流程里直接断言,可以在后端加一个 `src/**/*.test.ts`,
|
||||
按前端 `useReference` 的路径执行字段上的 `mergeScript`:
|
||||
|
||||
```ts
|
||||
const computedGetters = [];
|
||||
const ctx = {
|
||||
compute: getter => {
|
||||
computedGetters.push(getter);
|
||||
return getter;
|
||||
},
|
||||
asyncCompute: () => undefined,
|
||||
computed: () => undefined,
|
||||
};
|
||||
new Function('ctx', mergeScript)(ctx);
|
||||
// 用假的 form 求值,断言显隐结果
|
||||
const show = computedGetters[0]({ form: { access: { authType: 'apikey' } } });
|
||||
```
|
||||
|
||||
参考实现:`packages/ui/certd-server/src/plugins/plugin-wangsu/access-input.test.ts`。
|
||||
要点是断言「同一个 show 函数换 `authType` 结果会变」,证明显隐确实是运行时算的,
|
||||
而不是在 node 侧定死的。
|
||||
@@ -7,7 +7,7 @@
|
||||
```yaml
|
||||
name: DemoTask
|
||||
author: demo
|
||||
title: Demo 任务
|
||||
title: Demo-插件名称 # 格式:平台-功能 ,例如: 网宿-更新CDNpro证书 ; 阿里云-上传到CAS证书管理服务 ; 天翼云-更新CDN证书
|
||||
desc: 插件说明
|
||||
icon: clarity:plugin-line
|
||||
pluginType: task
|
||||
@@ -61,6 +61,8 @@ content: |
|
||||
### 编辑规则
|
||||
|
||||
- 修改已有插件时保留 `name`、`author`、`pluginType` 和已有兼容字段。
|
||||
- 新插件的 `author` 使用启动提示词中给出的插件作者,不要自行编造作者名;启动提示词未给出作者时先向用户确认。
|
||||
- `fullName` 由 `author/name` 组成,作者变化会导致插件身份变化,不要随意修改。
|
||||
- 只修改需求涉及的字段,避免删除未知的 YAML 字段。
|
||||
- 脚本过长时仍放在同一个 `content` block scalar 中。
|
||||
- 提交前检查 YAML 可解析、`content` 非空、版本和插件类型没有被意外修改。
|
||||
|
||||
@@ -57,4 +57,41 @@ return class DemoAccess extends BaseAccess {
|
||||
- `onTestRequest` 应调用实际 API 方法并在失败时抛出异常。
|
||||
- 对外 API 方法应统一处理分页、错误和返回字段。
|
||||
- 使用 `this.logger` 或框架提供的 logger,禁止 `console.log`。
|
||||
- 一般将API接口方法封装到Access中,其他plugin调用access的方法当做client sdk使用
|
||||
- 一般将API接口方法封装到Access中,其他plugin调用access的方法当做client sdk使用
|
||||
|
||||
## 字段动态显隐
|
||||
|
||||
多种鉴权方式(例如「ApiKey」和「AccessKey签名」)共用一个授权时,
|
||||
用字段上的 `mergeScript` 按所选方式显隐对应字段,只让用户看到当前方式需要的字段:
|
||||
|
||||
```yaml
|
||||
input:
|
||||
authType:
|
||||
title: 鉴权方式
|
||||
value: apikey
|
||||
component:
|
||||
name: a-select
|
||||
vModel: value
|
||||
options:
|
||||
- { label: AccessKey签名, value: aksk }
|
||||
- { label: ApiKey, value: apikey }
|
||||
accessKeyId:
|
||||
title: accessKeyId
|
||||
component:
|
||||
name: a-input
|
||||
vModel: value
|
||||
mergeScript: |2-
|
||||
|
||||
return {
|
||||
show: ctx.compute(({form})=>{
|
||||
return !form.access.authType || form.access.authType === 'aksk';
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
- 判断必须写在 `ctx.compute(({form})=>{ ... })` 回调里;脚本本身在插件定义阶段就拼好了,
|
||||
那时表单还没打开,用参数在服务端判断字段值没有意义。
|
||||
- 授权表单的字段值在 `form.access.xxx`(任务表单才是 `form.xxx`)。
|
||||
- 历史授权没有 `authType` 字段,条件里要用 `!form.access.authType || ...` 兜底走旧逻辑。
|
||||
|
||||
完整用法见父 Skill 的 `references/merge-script.md`。
|
||||
@@ -3,6 +3,29 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [BUG]使用天翼云CDN部署证书时无法匹配域名 ([bb1f232](https://github.com/certd/certd/commit/bb1f232f415c522851dede979f5d609dda844fd3))
|
||||
* 缺失运行时嵌套依赖时添加清除缓存提示 ([2d0b85b](https://github.com/certd/certd/commit/2d0b85b3054fc1aa82b3ab5db34a2a56308f71f2))
|
||||
* 支持google persist校验方式 ([97d2b50](https://github.com/certd/certd/commit/97d2b5007409ec79ea864d437c473a2f9b5dfae4))
|
||||
* **config:** 修复环境变量加载逻辑并更新配置文件 ([07f0d3b](https://github.com/certd/certd/commit/07f0d3b4d626628a0e3dfc7e5c52e8c4a98bad47))
|
||||
* **ctyun-cdn:** 修复天翼云CDN部署插件的域名选择问题 ([ea7d14c](https://github.com/certd/certd/commit/ea7d14c289b8720781bf101bfef59eac7bb03765))
|
||||
* **webhook:** 修复模版转义换行符,避免手动上传的证书报错 ([2735feb](https://github.com/certd/certd/commit/2735feb3302f8308708dafc8b295adcd4b91d7f0))
|
||||
|
||||
### Features
|
||||
|
||||
* **client:** 新增客户端管理功能 ([3dbbe72](https://github.com/certd/certd/commit/3dbbe72fc15c114ca6b5e8ff45cb21f258b80650))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 宝塔站点列表支持搜索和排序 ([4420472](https://github.com/certd/certd/commit/4420472f8fac272d30fb8ccb1cc2ee9763989001))
|
||||
* 网宿插件双鉴权支持 ([241b2e5](https://github.com/certd/certd/commit/241b2e55848471e61b5d2be0c87a7dc9a0d29557))
|
||||
* **monitor:** 站点证书监控支持手动选中触发检查 ([f60e63f](https://github.com/certd/certd/commit/f60e63fbaf00460d5050cd7705bd75d57dec202d))
|
||||
* **open-api:** openkey增加用途备注 ([bbeaf61](https://github.com/certd/certd/commit/bbeaf617d19750515f6eca51863e74e23bc1de50))
|
||||
* **tencent clb:** 修改所有CLB地域的endpoint为就近接入 ([c427735](https://github.com/certd/certd/commit/c4277350bcc8cf034cf4e76db2a6daa1842bc5d7))
|
||||
|
||||
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -3,6 +3,19 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复添加步骤会修改所有步骤标题的bug ([51e83e4](https://github.com/certd/certd/commit/51e83e4c38f7f319314fdc38ef33cf5720b2dde0))
|
||||
* 修复证书插件配置无法打开的bug ([5563abb](https://github.com/certd/certd/commit/5563abb13a62140f4dee668ab768cf4ff9371ddb))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 新增余额管理,管理员充值功能 ([bded368](https://github.com/certd/certd/commit/bded368cbb8974b6937ae260e448c25fae5c33ad))
|
||||
* **acme-account-access:** AcmeAccount授权允许修改eab,重新生成account ([6a649aa](https://github.com/certd/certd/commit/6a649aaca083ccd3b517f7d676a58220aa33b37d))
|
||||
* **ui:** 商业版套餐购买不要求设置支付方式 ([e971145](https://github.com/certd/certd/commit/e9711456860d7db40914a0c295534f7bca56f83c))
|
||||
|
||||
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
`Certd`默认启用https,监听7002端口
|
||||
如果你想关闭https,或者修改端口,可以在环境变量中配置
|
||||
```shell
|
||||
CERTD_HTTPS_ENABLE=true
|
||||
CERTD_HTTPS_port=7002
|
||||
certd_https_enabled=true
|
||||
certd_https_port=7002
|
||||
|
||||
```
|
||||
|
||||
|
||||
+1
-1
@@ -9,5 +9,5 @@
|
||||
}
|
||||
},
|
||||
"npmClient": "pnpm",
|
||||
"version": "1.44.4"
|
||||
"version": "1.45.0"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.45.0](https://github.com/publishlab/node-acme-client/compare/v1.44.4...v1.45.0) (2026-10-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 支持google persist校验方式 ([97d2b50](https://github.com/publishlab/node-acme-client/commit/97d2b5007409ec79ea864d437c473a2f9b5dfae4))
|
||||
|
||||
## [1.44.4](https://github.com/publishlab/node-acme-client/compare/v1.44.3...v1.44.4) (2026-09-12)
|
||||
|
||||
**Note:** Version bump only for package @certd/acme-client
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"description": "Simple and unopinionated ACME client",
|
||||
"private": false,
|
||||
"author": "nmorsman",
|
||||
"version": "1.44.4",
|
||||
"version": "1.45.0",
|
||||
"type": "module",
|
||||
"module": "./dist/index.js",
|
||||
"main": "./dist/index.js",
|
||||
@@ -18,7 +18,7 @@
|
||||
"types"
|
||||
],
|
||||
"dependencies": {
|
||||
"@certd/basic": "^1.44.4",
|
||||
"@certd/basic": "^1.45.0",
|
||||
"@peculiar/x509": "^1.11.0",
|
||||
"asn1js": "^3.0.5",
|
||||
"axios": "^1.9.0",
|
||||
@@ -27,6 +27,7 @@
|
||||
"https-proxy-agent": "^7.0.5",
|
||||
"lodash-es": "^4.17.21",
|
||||
"node-forge": "^1.3.1",
|
||||
"psl": "^1.15.0",
|
||||
"punycode.js": "^2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -76,5 +77,5 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/publishlab/node-acme-client/issues"
|
||||
},
|
||||
"gitHead": "cc60cd4cd1d914110be65284bcf16cd3c5a63b8d"
|
||||
"gitHead": "14809127d6cd3aa8e2a7c5fab6b1f35e95e45e43"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// @ts-ignore
|
||||
import psl from "psl";
|
||||
|
||||
export function getDnsPersistIssuer(directoryUrl?: string) {
|
||||
if (!directoryUrl) {
|
||||
return "letsencrypt.org";
|
||||
}
|
||||
|
||||
let directory: URL;
|
||||
try {
|
||||
directory = new URL(directoryUrl);
|
||||
} catch {
|
||||
throw new Error(`ACME Directory URL无效,无法生成DNS持久验证Issuer:${directoryUrl}`);
|
||||
}
|
||||
|
||||
const issuer = psl.get(directory.hostname);
|
||||
if (!issuer) {
|
||||
throw new Error(`无法从ACME Directory URL获取可注册主域名:${directory.hostname}`);
|
||||
}
|
||||
return issuer;
|
||||
}
|
||||
@@ -79,3 +79,4 @@ export * from './verify.js'
|
||||
export * from './error.js'
|
||||
|
||||
export * from './util.js'
|
||||
export * from './dns-persist.js'
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **webhook:** 修复模版转义换行符,避免手动上传的证书报错 ([2735feb](https://github.com/certd/certd/commit/2735feb3302f8308708dafc8b295adcd4b91d7f0))
|
||||
|
||||
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
|
||||
|
||||
**Note:** Version bump only for package @certd/basic
|
||||
|
||||
@@ -1 +1 @@
|
||||
19:17
|
||||
00:51
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/basic",
|
||||
"private": false,
|
||||
"version": "1.44.4",
|
||||
"version": "1.45.0",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
@@ -54,5 +54,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "cc60cd4cd1d914110be65284bcf16cd3c5a63b8d"
|
||||
"gitHead": "14809127d6cd3aa8e2a7c5fab6b1f35e95e45e43"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export const stringUtils = {
|
||||
replaceTemplate(target: string, body: Record<string, unknown>, urlEncode = false) {
|
||||
let bodyStr = target;
|
||||
for (const key of Object.keys(body)) {
|
||||
const rawValue = body[key] == null ? "" : String(body[key]);
|
||||
let value = urlEncode ? encodeURIComponent(rawValue) : rawValue;
|
||||
value = value.replace(/[\u0000-\u001f]/g, char => {
|
||||
const escapes: Record<string, string> = { "\b": "\\b", "\f": "\\f", "\n": "\\n", "\r": "\\r", "\t": "\\t" };
|
||||
return escapes[char] ?? `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`;
|
||||
});
|
||||
bodyStr = bodyStr.replaceAll(`\${${key}}`, value);
|
||||
bodyStr = bodyStr.replaceAll(`{${key}}`, value);
|
||||
}
|
||||
return bodyStr;
|
||||
},
|
||||
|
||||
maxLength(str?: string, length = 100) {
|
||||
if (str) {
|
||||
return str.length > length ? str.slice(0, length) + "..." : str;
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 缺失运行时嵌套依赖时添加清除缓存提示 ([2d0b85b](https://github.com/certd/certd/commit/2d0b85b3054fc1aa82b3ab5db34a2a56308f71f2))
|
||||
|
||||
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
|
||||
|
||||
**Note:** Version bump only for package @certd/pipeline
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/pipeline",
|
||||
"private": false,
|
||||
"version": "1.44.4",
|
||||
"version": "1.45.0",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
@@ -21,8 +21,8 @@
|
||||
"lint": "eslint --fix --ext .ts src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@certd/basic": "^1.44.4",
|
||||
"@certd/plus-core": "^1.44.4",
|
||||
"@certd/basic": "^1.45.0",
|
||||
"@certd/plus-core": "^1.45.0",
|
||||
"dayjs": "^1.11.7",
|
||||
"lodash-es": "^4.17.21",
|
||||
"reflect-metadata": "^0.2.2"
|
||||
@@ -51,5 +51,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "cc60cd4cd1d914110be65284bcf16cd3c5a63b8d"
|
||||
"gitHead": "14809127d6cd3aa8e2a7c5fab6b1f35e95e45e43"
|
||||
}
|
||||
|
||||
@@ -64,6 +64,28 @@ describe("RuntimeDepsService", () => {
|
||||
assert.equal(mod.value, 42);
|
||||
});
|
||||
|
||||
it("logs a cache clearing hint when a nested runtime dependency is missing", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-missing-nested-"));
|
||||
const packageDir = path.join(rootDir, "node_modules", "runtime-only");
|
||||
fs.mkdirSync(packageDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(rootDir, "package.json"), JSON.stringify({ name: "runtime-root", type: "module" }), "utf8");
|
||||
fs.writeFileSync(path.join(packageDir, "package.json"), JSON.stringify({ name: "runtime-only", main: "index.js" }), "utf8");
|
||||
fs.writeFileSync(path.join(packageDir, "index.js"), "module.exports = require('missing-nested');\n", "utf8");
|
||||
const logs: string[] = [];
|
||||
const logger = {
|
||||
info() {},
|
||||
warn() {},
|
||||
error(message: string, ...args: any[]) {
|
||||
logs.push([message, ...args.map(item => item?.message || String(item))].join(" "));
|
||||
},
|
||||
};
|
||||
const service = new RuntimeDepsService({ rootDir }, null);
|
||||
|
||||
await assert.rejects(() => service.importRuntime("runtime-only", logger));
|
||||
assert.ok(logs.some(message => message.includes("Cannot find module 'missing-nested'")));
|
||||
assert.ok(logs.some(message => message.includes("插件管理页面->清除缓存依赖->重启certd")));
|
||||
});
|
||||
|
||||
it("installs configured lazy dependency when import target is missing", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-lazy-"));
|
||||
const service = new RuntimeDepsService({ rootDir, lazyDependencies: { "lazy-pkg": "^1.2.3" } }, null);
|
||||
|
||||
@@ -251,6 +251,7 @@ export class RuntimeDepsService {
|
||||
}
|
||||
|
||||
let resolved: string;
|
||||
let fromRuntimeDeps = false;
|
||||
try {
|
||||
//尝试加载本项目依赖
|
||||
resolved = this.resolveProjectSpecifier(specifier).resolved;
|
||||
@@ -258,13 +259,22 @@ export class RuntimeDepsService {
|
||||
if (!this.isModuleNotFoundError(error)) {
|
||||
throw error;
|
||||
}
|
||||
fromRuntimeDeps = true;
|
||||
resolved = await this.resolveLazyOrInstallSpecifier(specifier, logger);
|
||||
}
|
||||
if (!resolved) {
|
||||
throw new Error(`依赖未安装成功: ${specifier}`);
|
||||
}
|
||||
|
||||
return await import(pathToFileURL(resolved).href);
|
||||
try {
|
||||
return await import(pathToFileURL(resolved).href);
|
||||
} catch (error: any) {
|
||||
if (fromRuntimeDeps && this.isModuleNotFoundError(error)) {
|
||||
logger.error?.("动态依赖加载失败:", error);
|
||||
logger.error?.("动态依赖加载失败,请前往插件管理页面->清除缓存依赖->重启certd后重试");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
//动态获取模块解析
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-huawei
|
||||
|
||||
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-huawei
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/lib-huawei",
|
||||
"private": false,
|
||||
"version": "1.44.4",
|
||||
"version": "1.45.0",
|
||||
"main": "./dist/bundle.js",
|
||||
"module": "./dist/bundle.js",
|
||||
"types": "./dist/d/index.d.ts",
|
||||
@@ -31,5 +31,5 @@
|
||||
"prettier": "3.3.3",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"gitHead": "cc60cd4cd1d914110be65284bcf16cd3c5a63b8d"
|
||||
"gitHead": "14809127d6cd3aa8e2a7c5fab6b1f35e95e45e43"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-iframe
|
||||
|
||||
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-iframe
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/lib-iframe",
|
||||
"private": false,
|
||||
"version": "1.44.4",
|
||||
"version": "1.45.0",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
@@ -37,5 +37,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "cc60cd4cd1d914110be65284bcf16cd3c5a63b8d"
|
||||
"gitHead": "14809127d6cd3aa8e2a7c5fab6b1f35e95e45e43"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
|
||||
|
||||
**Note:** Version bump only for package @certd/jdcloud
|
||||
|
||||
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
|
||||
|
||||
**Note:** Version bump only for package @certd/jdcloud
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@certd/jdcloud",
|
||||
"version": "1.44.4",
|
||||
"version": "1.45.0",
|
||||
"description": "jdcloud openApi sdk",
|
||||
"main": "./dist/bundle.js",
|
||||
"module": "./dist/bundle.js",
|
||||
@@ -63,5 +63,5 @@
|
||||
"fetch"
|
||||
]
|
||||
},
|
||||
"gitHead": "cc60cd4cd1d914110be65284bcf16cd3c5a63b8d"
|
||||
"gitHead": "14809127d6cd3aa8e2a7c5fab6b1f35e95e45e43"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-k8s
|
||||
|
||||
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
|
||||
|
||||
**Note:** Version bump only for package @certd/lib-k8s
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/lib-k8s",
|
||||
"private": false,
|
||||
"version": "1.44.4",
|
||||
"version": "1.45.0",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
@@ -21,7 +21,7 @@
|
||||
"lint": "eslint --fix --ext .ts src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@certd/basic": "^1.44.4",
|
||||
"@certd/basic": "^1.45.0",
|
||||
"@kubernetes/client-node": "0.21.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -38,5 +38,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "cc60cd4cd1d914110be65284bcf16cd3c5a63b8d"
|
||||
"gitHead": "14809127d6cd3aa8e2a7c5fab6b1f35e95e45e43"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
|
||||
|
||||
### Features
|
||||
|
||||
* **client:** 新增客户端管理功能 ([3dbbe72](https://github.com/certd/certd/commit/3dbbe72fc15c114ca6b5e8ff45cb21f258b80650))
|
||||
|
||||
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@certd/lib-server",
|
||||
"version": "1.44.4",
|
||||
"version": "1.45.0",
|
||||
"description": "midway with flyway, sql upgrade way ",
|
||||
"private": false,
|
||||
"type": "module",
|
||||
@@ -29,11 +29,11 @@
|
||||
],
|
||||
"license": "AGPL",
|
||||
"dependencies": {
|
||||
"@certd/acme-client": "^1.44.4",
|
||||
"@certd/basic": "^1.44.4",
|
||||
"@certd/pipeline": "^1.44.4",
|
||||
"@certd/plugin-lib": "^1.44.4",
|
||||
"@certd/plus-core": "^1.44.4",
|
||||
"@certd/acme-client": "^1.45.0",
|
||||
"@certd/basic": "^1.45.0",
|
||||
"@certd/pipeline": "^1.45.0",
|
||||
"@certd/plugin-lib": "^1.45.0",
|
||||
"@certd/plus-core": "^1.45.0",
|
||||
"@midwayjs/cache": "3.14.0",
|
||||
"@midwayjs/core": "3.20.11",
|
||||
"@midwayjs/i18n": "3.20.13",
|
||||
@@ -69,5 +69,5 @@
|
||||
"typeorm": "^0.3.20",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "cc60cd4cd1d914110be65284bcf16cd3c5a63b8d"
|
||||
"gitHead": "14809127d6cd3aa8e2a7c5fab6b1f35e95e45e43"
|
||||
}
|
||||
|
||||
@@ -174,11 +174,11 @@ export class AccessService extends BaseService<AccessEntity> {
|
||||
throw new ValidateException("userId不能为空");
|
||||
}
|
||||
if (userId !== entity.userId) {
|
||||
throw new PermissionException("您对该Access授权无访问权限");
|
||||
throw new PermissionException(`您对该Access授权${entity.name}(${entity.id})无访问权限`);
|
||||
}
|
||||
}
|
||||
if (projectId != null && projectId !== entity.projectId) {
|
||||
throw new PermissionException("您对该Access授权无访问权限");
|
||||
throw new PermissionException(`您对该Access授权${entity.name}(${entity.id})无访问权限`);
|
||||
}
|
||||
|
||||
// const access = accessRegistry.get(entity.type);
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
|
||||
|
||||
**Note:** Version bump only for package @certd/midway-flyway-js
|
||||
|
||||
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
|
||||
|
||||
**Note:** Version bump only for package @certd/midway-flyway-js
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@certd/midway-flyway-js",
|
||||
"version": "1.44.4",
|
||||
"version": "1.45.0",
|
||||
"description": "midway with flyway, sql upgrade way ",
|
||||
"private": false,
|
||||
"type": "module",
|
||||
@@ -52,5 +52,5 @@
|
||||
"typeorm": "^0.3.20",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "cc60cd4cd1d914110be65284bcf16cd3c5a63b8d"
|
||||
"gitHead": "14809127d6cd3aa8e2a7c5fab6b1f35e95e45e43"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
|
||||
|
||||
**Note:** Version bump only for package @certd/plugin-cert
|
||||
|
||||
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
|
||||
|
||||
**Note:** Version bump only for package @certd/plugin-cert
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/plugin-cert",
|
||||
"private": false,
|
||||
"version": "1.44.4",
|
||||
"version": "1.45.0",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -20,7 +20,7 @@
|
||||
"lint": "eslint --fix --ext .ts src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@certd/plugin-lib": "^1.44.4"
|
||||
"@certd/plugin-lib": "^1.45.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.3.12",
|
||||
@@ -38,5 +38,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "cc60cd4cd1d914110be65284bcf16cd3c5a63b8d"
|
||||
"gitHead": "14809127d6cd3aa8e2a7c5fab6b1f35e95e45e43"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
|
||||
|
||||
**Note:** Version bump only for package @certd/plugin-lib
|
||||
|
||||
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
|
||||
|
||||
**Note:** Version bump only for package @certd/plugin-lib
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/plugin-lib",
|
||||
"private": false,
|
||||
"version": "1.44.4",
|
||||
"version": "1.45.0",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -17,9 +17,9 @@
|
||||
"lint": "eslint --fix --ext .ts src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@certd/acme-client": "^1.44.4",
|
||||
"@certd/basic": "^1.44.4",
|
||||
"@certd/pipeline": "^1.44.4",
|
||||
"@certd/acme-client": "^1.45.0",
|
||||
"@certd/basic": "^1.45.0",
|
||||
"@certd/pipeline": "^1.45.0",
|
||||
"dayjs": "^1.11.7",
|
||||
"jszip": "^3.10.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
@@ -45,5 +45,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "cc60cd4cd1d914110be65284bcf16cd3c5a63b8d"
|
||||
"gitHead": "14809127d6cd3aa8e2a7c5fab6b1f35e95e45e43"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,17 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
|
||||
|
||||
### Features
|
||||
|
||||
* **client:** 新增客户端管理功能 ([3dbbe72](https://github.com/certd/certd/commit/3dbbe72fc15c114ca6b5e8ff45cb21f258b80650))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **monitor:** 站点证书监控支持手动选中触发检查 ([f60e63f](https://github.com/certd/certd/commit/f60e63fbaf00460d5050cd7705bd75d57dec202d))
|
||||
* **open-api:** openkey增加用途备注 ([bbeaf61](https://github.com/certd/certd/commit/bbeaf617d19750515f6eca51863e74e23bc1de50))
|
||||
|
||||
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@certd/ui-client",
|
||||
"version": "1.44.4",
|
||||
"version": "1.45.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite --open",
|
||||
@@ -104,8 +104,8 @@
|
||||
"zod-defaults": "^0.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@certd/lib-iframe": "^1.44.4",
|
||||
"@certd/pipeline": "^1.44.4",
|
||||
"@certd/lib-iframe": "^1.45.0",
|
||||
"@certd/pipeline": "^1.45.0",
|
||||
"@rollup/plugin-commonjs": "^25.0.7",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@types/chai": "^4.3.12",
|
||||
|
||||
@@ -19,6 +19,7 @@ import sysauthority from "./certd/sys-authority";
|
||||
import syscname from "./certd/sys-cname";
|
||||
import tutorial from "./certd/tutorial";
|
||||
import cron from "./certd/cron";
|
||||
import client from "./certd/client";
|
||||
|
||||
// Note: @ is reserved in locale messages; use {'@'} when needed.
|
||||
export default {
|
||||
@@ -43,4 +44,5 @@ export default {
|
||||
...syscname,
|
||||
...tutorial,
|
||||
...cron,
|
||||
...client,
|
||||
};
|
||||
|
||||
@@ -19,6 +19,7 @@ export default {
|
||||
subdomainConfirmContent: "{domain} appears to be a subdomain. Only delegated subdomains and free second-level subdomains need to be maintained here. Otherwise certificate application may fail. Continue?",
|
||||
importFromProvider: "Import from Domain Provider",
|
||||
importFromResolveRecords: "Import from DNS Records",
|
||||
importFromResolveRecordsTooltip: "Import DNS records as monitoring sites",
|
||||
syncExpirationDate: "Sync Domain Expiration Time",
|
||||
syncTaskSubmitted: "Sync task submitted",
|
||||
syncExpirationProgress: "Sync Domain Expiration Progress",
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
export default {
|
||||
clientManager: "Client Manager",
|
||||
client: {
|
||||
title: "Client Manager",
|
||||
description: "View online status and managed site count of connected Certd clients. Clients report a heartbeat every 10 minutes and are marked offline if no heartbeat arrives for 30 minutes.",
|
||||
plusRequired: "This feature requires the Pro edition",
|
||||
machineName: "Machine",
|
||||
clientId: "Client ID",
|
||||
version: "Version",
|
||||
os: "OS",
|
||||
status: "Status",
|
||||
online: "Online",
|
||||
offline: "Offline",
|
||||
siteCount: "Sites",
|
||||
httpsSiteCount: "HTTPS Sites",
|
||||
syncedSiteCount: "Synced",
|
||||
failedSiteCount: "Failed",
|
||||
lastHeartbeatAt: "Last Heartbeat",
|
||||
},
|
||||
};
|
||||
@@ -3,6 +3,9 @@ export default {
|
||||
scopeOpenApiOnly: "Open API Only",
|
||||
scopeFullAccount: "Full Account Permissions",
|
||||
scopeHelper: "Open API only allows access to open APIs; full account permissions allow access to all APIs",
|
||||
keyRemark: "Key Purpose",
|
||||
keyRemarkHelper: "Describe the purpose of this key for easier identification",
|
||||
keyRemarkMaxChars: "Maximum 512 characters",
|
||||
gen: {
|
||||
text: "API Test",
|
||||
title: "x-certd-token",
|
||||
|
||||
@@ -7,6 +7,8 @@ export default {
|
||||
settingLink: "Site Monitoring Settings",
|
||||
limitInfo: "Basic edition limited to 1, professional and above unlimited, current",
|
||||
checkAll: "Check All",
|
||||
checkBatch: "Check Selected",
|
||||
batchCheckConfirm: "Check the {count} selected sites now?",
|
||||
checkNow: "Check Now",
|
||||
batchDelete: "Batch Delete",
|
||||
confirmTitle: "Confirm",
|
||||
|
||||
@@ -19,6 +19,7 @@ import sysauthority from "./certd/sys-authority";
|
||||
import syscname from "./certd/sys-cname";
|
||||
import tutorial from "./certd/tutorial";
|
||||
import cron from "./certd/cron";
|
||||
import client from "./certd/client";
|
||||
|
||||
// Note: @ is reserved in locale messages; use {'@'} when needed.
|
||||
export default {
|
||||
@@ -43,4 +44,5 @@ export default {
|
||||
...syscname,
|
||||
...tutorial,
|
||||
...cron,
|
||||
...client,
|
||||
};
|
||||
|
||||
@@ -19,6 +19,7 @@ export default {
|
||||
subdomainConfirmContent: "检测到{domain}为子域名,只有托管子域名和免费二级子域名才需要在此处维护,否则会导致申请证书失败,请确认是否继续?",
|
||||
importFromProvider: "从域名提供商导入",
|
||||
importFromResolveRecords: "从解析记录导入",
|
||||
importFromResolveRecordsTooltip: "从域名的解析记录中导入并作为监控站点",
|
||||
syncExpirationDate: "同步域名过期时间",
|
||||
syncTaskSubmitted: "同步任务已提交",
|
||||
syncExpirationProgress: "同步域名过期时间进度",
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
export default {
|
||||
clientManager: "客户端管理",
|
||||
client: {
|
||||
title: "客户端管理",
|
||||
description: "查看已接入的 Certd 客户端在线状态与管理的站点数量;客户端每 10 分钟上报一次心跳,超过 30 分钟未上报则标记为离线",
|
||||
plusRequired: "此功能需要专业版",
|
||||
machineName: "机器名",
|
||||
clientId: "客户端ID",
|
||||
version: "版本",
|
||||
os: "系统",
|
||||
status: "在线状态",
|
||||
online: "在线",
|
||||
offline: "离线",
|
||||
siteCount: "站点数",
|
||||
httpsSiteCount: "HTTPS站点",
|
||||
syncedSiteCount: "已同步",
|
||||
failedSiteCount: "异常",
|
||||
lastHeartbeatAt: "上次心跳",
|
||||
},
|
||||
};
|
||||
@@ -3,6 +3,9 @@ export default {
|
||||
scopeOpenApiOnly: "仅开放接口",
|
||||
scopeFullAccount: "账户所有权限",
|
||||
scopeHelper: "仅开放接口只可以访问开放接口,账户所有权限可以访问所有接口",
|
||||
keyRemark: "密钥用途",
|
||||
keyRemarkHelper: "说明该密钥的用途,便于区分和查找",
|
||||
keyRemarkMaxChars: "最大512个字符",
|
||||
gen: {
|
||||
text: "接口测试",
|
||||
title: "x-certd-token",
|
||||
|
||||
@@ -7,6 +7,8 @@ export default {
|
||||
settingLink: "站点监控设置",
|
||||
limitInfo: "基础版限制1条,专业版以上无限制,当前",
|
||||
checkAll: "检查全部",
|
||||
checkBatch: "检查选中",
|
||||
batchCheckConfirm: "确认触发检查选中的{count}个站点吗?",
|
||||
checkNow: "立即检查",
|
||||
batchDelete: "批量删除",
|
||||
confirmTitle: "确认",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useProjectStore } from "/@/store/project";
|
||||
import { useProjectStore } from "/@/store/project";
|
||||
import { useSettingStore } from "/@/store/settings";
|
||||
|
||||
export const certdResources = [
|
||||
@@ -128,6 +128,17 @@ export const certdResources = [
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "certd.clientManager",
|
||||
name: "ClientManager",
|
||||
path: "/cert/client",
|
||||
component: "/certd/client/index.vue",
|
||||
meta: {
|
||||
icon: "ion:desktop-outline",
|
||||
auth: true,
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "certd.sysResources.currentProject",
|
||||
name: "ProjectMemberManager",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<template #header>
|
||||
<div class="title">
|
||||
DNS持久验证记录
|
||||
<span class="red sub" style="color: red">当前仅 Let's Encrypt 测试环境 可以申请 DNS 持久验证证书。</span>
|
||||
<span class="red sub" style="color: red">当前仅 Google证书 可以申请 DNS 持久验证。</span>
|
||||
</div>
|
||||
</template>
|
||||
<fs-crud ref="crudRef" v-bind="crudBinding"></fs-crud>
|
||||
|
||||
@@ -235,7 +235,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
} else if (value === "dns-persist") {
|
||||
Modal.confirm({
|
||||
title: "请前往DNS持久验证记录页面添加记录",
|
||||
content: "DNS持久验证需要先配置ACME账号和_validation-persist持久TXT记录,续期时不再增删DNS记录;当前仅 Let's Encrypt 测试环境 可以申请。",
|
||||
content: "DNS持久验证需要先配置ACME账号和_validation-persist持久TXT记录,续期时不再增删DNS记录;当前仅 Google证书 可以申请。",
|
||||
async onOk() {
|
||||
router.push({
|
||||
path: "/cert/cert/dns-persist",
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { request } from "/src/api/service";
|
||||
|
||||
const apiPrefix = "/client";
|
||||
|
||||
export const clientApi = {
|
||||
async GetList(query: any) {
|
||||
return await request({
|
||||
url: apiPrefix + "/page",
|
||||
method: "post",
|
||||
data: query,
|
||||
});
|
||||
},
|
||||
|
||||
async GetObj(id: number) {
|
||||
return await request({
|
||||
url: apiPrefix + "/info",
|
||||
method: "post",
|
||||
params: { id },
|
||||
});
|
||||
},
|
||||
|
||||
async DelObj(id: number) {
|
||||
return await request({
|
||||
url: apiPrefix + "/delete",
|
||||
method: "post",
|
||||
params: { id },
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,243 @@
|
||||
// @ts-ignore
|
||||
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
|
||||
import dayjs from "dayjs";
|
||||
import { useDicts } from "../dicts";
|
||||
import { useProjectStore } from "/@/store/project";
|
||||
import { useI18n } from "/src/locales";
|
||||
import { clientApi } from "./api";
|
||||
|
||||
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||
const { t } = useI18n();
|
||||
const api = clientApi;
|
||||
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
|
||||
return await api.GetList(query);
|
||||
};
|
||||
const delRequest = async (req: DelReq) => {
|
||||
const { row } = req;
|
||||
return await api.DelObj(row.id);
|
||||
};
|
||||
const editRequest = async (req: EditReq) => {};
|
||||
const addRequest = async (req: AddReq) => {};
|
||||
|
||||
const { myProjectDict } = useDicts();
|
||||
const projectStore = useProjectStore();
|
||||
|
||||
return {
|
||||
id: "clientCrud",
|
||||
crudOptions: {
|
||||
request: {
|
||||
pageRequest,
|
||||
addRequest,
|
||||
editRequest,
|
||||
delRequest,
|
||||
},
|
||||
pagination: {
|
||||
pageSizeOptions: ["10", "20", "50", "100", "200"],
|
||||
},
|
||||
actionbar: {
|
||||
buttons: {
|
||||
add: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
rowHandle: {
|
||||
fixed: "right",
|
||||
width: 120,
|
||||
buttons: {
|
||||
view: {
|
||||
show: false,
|
||||
},
|
||||
edit: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
search: {
|
||||
initialForm: {
|
||||
...projectStore.getSearchForm(),
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
id: {
|
||||
title: "ID",
|
||||
key: "id",
|
||||
type: "number",
|
||||
search: {
|
||||
show: false,
|
||||
},
|
||||
column: {
|
||||
width: 70,
|
||||
align: "center",
|
||||
},
|
||||
form: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
machineName: {
|
||||
title: t("certd.client.machineName"),
|
||||
search: {
|
||||
show: true,
|
||||
},
|
||||
type: "text",
|
||||
form: {
|
||||
show: false,
|
||||
},
|
||||
column: {
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
},
|
||||
},
|
||||
clientId: {
|
||||
title: t("certd.client.clientId"),
|
||||
search: {
|
||||
show: true,
|
||||
},
|
||||
type: "text",
|
||||
form: {
|
||||
show: false,
|
||||
},
|
||||
column: {
|
||||
width: 220,
|
||||
ellipsis: true,
|
||||
},
|
||||
},
|
||||
version: {
|
||||
title: t("certd.client.version"),
|
||||
search: {
|
||||
show: false,
|
||||
},
|
||||
type: "text",
|
||||
form: {
|
||||
show: false,
|
||||
},
|
||||
column: {
|
||||
width: 90,
|
||||
align: "center",
|
||||
},
|
||||
},
|
||||
os: {
|
||||
title: t("certd.client.os"),
|
||||
search: {
|
||||
show: false,
|
||||
},
|
||||
type: "text",
|
||||
form: {
|
||||
show: false,
|
||||
},
|
||||
column: {
|
||||
width: 100,
|
||||
align: "center",
|
||||
},
|
||||
},
|
||||
online: {
|
||||
title: t("certd.client.status"),
|
||||
search: {
|
||||
show: false,
|
||||
},
|
||||
type: "text",
|
||||
form: {
|
||||
show: false,
|
||||
},
|
||||
column: {
|
||||
width: 100,
|
||||
align: "center",
|
||||
cellRender({ value, row }) {
|
||||
const online = value === true;
|
||||
const heartbeat = row.lastHeartbeatAt ? dayjs(row.lastHeartbeatAt).format("YYYY-MM-DD HH:mm:ss") : "";
|
||||
return (
|
||||
<a-tooltip title={heartbeat}>
|
||||
<a-tag color={online ? "green" : "red"}>{online ? t("certd.client.online") : t("certd.client.offline")}</a-tag>
|
||||
</a-tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
siteCount: {
|
||||
title: t("certd.client.siteCount"),
|
||||
search: {
|
||||
show: false,
|
||||
},
|
||||
type: "number",
|
||||
form: {
|
||||
show: false,
|
||||
},
|
||||
column: {
|
||||
width: 90,
|
||||
align: "center",
|
||||
},
|
||||
},
|
||||
httpsSiteCount: {
|
||||
title: t("certd.client.httpsSiteCount"),
|
||||
search: {
|
||||
show: false,
|
||||
},
|
||||
type: "number",
|
||||
form: {
|
||||
show: false,
|
||||
},
|
||||
column: {
|
||||
width: 110,
|
||||
align: "center",
|
||||
},
|
||||
},
|
||||
syncedSiteCount: {
|
||||
title: t("certd.client.syncedSiteCount"),
|
||||
search: {
|
||||
show: false,
|
||||
},
|
||||
type: "number",
|
||||
form: {
|
||||
show: false,
|
||||
},
|
||||
column: {
|
||||
width: 90,
|
||||
align: "center",
|
||||
},
|
||||
},
|
||||
failedSiteCount: {
|
||||
title: t("certd.client.failedSiteCount"),
|
||||
search: {
|
||||
show: false,
|
||||
},
|
||||
type: "number",
|
||||
form: {
|
||||
show: false,
|
||||
},
|
||||
column: {
|
||||
width: 80,
|
||||
align: "center",
|
||||
},
|
||||
},
|
||||
lastHeartbeatAt: {
|
||||
title: t("certd.client.lastHeartbeatAt"),
|
||||
search: {
|
||||
show: false,
|
||||
},
|
||||
type: "text",
|
||||
form: {
|
||||
show: false,
|
||||
},
|
||||
column: {
|
||||
width: 170,
|
||||
sorter: true,
|
||||
cellRender({ value }) {
|
||||
if (!value) {
|
||||
return "-";
|
||||
}
|
||||
return dayjs(value).format("YYYY-MM-DD HH:mm:ss");
|
||||
},
|
||||
},
|
||||
},
|
||||
projectId: {
|
||||
title: t("certd.fields.projectName"),
|
||||
type: "dict-select",
|
||||
dict: myProjectDict,
|
||||
form: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<fs-page>
|
||||
<template #header>
|
||||
<div class="title flex items-center">
|
||||
{{ t("certd.client.title") }}
|
||||
<div class="sub flex items-center flex-1">
|
||||
<div>
|
||||
{{ t("certd.client.description") }}
|
||||
</div>
|
||||
<vip-button class="ml-5" mode="button"></vip-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<fs-crud ref="crudRef" v-bind="crudBinding"></fs-crud>
|
||||
</fs-page>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useFs } from "@fast-crud/fast-crud";
|
||||
import createCrudOptions from "./crud";
|
||||
import { useMounted } from "/@/use/use-mounted";
|
||||
import { useSettingStore } from "/@/store/settings";
|
||||
import { useI18n } from "/src/locales";
|
||||
const { t } = useI18n();
|
||||
const settingStore = useSettingStore();
|
||||
defineOptions({
|
||||
name: "ClientManager",
|
||||
});
|
||||
const context: any = {
|
||||
permission: {
|
||||
isProjectPermission: true,
|
||||
},
|
||||
};
|
||||
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context });
|
||||
|
||||
useMounted(() => crudExpose.doRefresh());
|
||||
</script>
|
||||
@@ -63,6 +63,13 @@ export const siteInfoApi = {
|
||||
method: "post",
|
||||
});
|
||||
},
|
||||
async CheckBatch(ids: number[]) {
|
||||
return await request({
|
||||
url: apiPrefix + "/checkBatch",
|
||||
method: "post",
|
||||
data: { ids },
|
||||
});
|
||||
},
|
||||
|
||||
async Import(form: any) {
|
||||
return await request({
|
||||
|
||||
@@ -100,6 +100,23 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
});
|
||||
}
|
||||
|
||||
function checkBatch() {
|
||||
if (selectedRowKeys.value.length === 0) {
|
||||
notification.error({ message: t("monitor.selectRecordsFirst") });
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: t("monitor.confirmTitle"),
|
||||
content: t("monitor.batchCheckConfirm", { count: selectedRowKeys.value.length }),
|
||||
onOk: async () => {
|
||||
await siteInfoApi.CheckBatch(selectedRowKeys.value);
|
||||
notification.success({ message: t("monitor.checkSubmitted") });
|
||||
selectedRowKeys.value = [];
|
||||
await crudExpose.doRefresh();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const GroupTypeSite = "site";
|
||||
const groupDictRef = createGroupDictRef(GroupTypeSite);
|
||||
|
||||
@@ -253,6 +270,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
show: hasActionPermission("write"),
|
||||
title: t("certd.domain.importFromResolveRecords"),
|
||||
text: t("certd.domain.importFromResolveRecords"),
|
||||
tooltip: { title: t("certd.domain.importFromResolveRecordsTooltip") },
|
||||
type: "primary",
|
||||
// needPlus: true,
|
||||
color: "gold",
|
||||
@@ -274,6 +292,15 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
checkAll();
|
||||
},
|
||||
},
|
||||
checkBatch: {
|
||||
show: true,
|
||||
text: t("monitor.checkBatch"),
|
||||
type: "primary",
|
||||
icon: "ion:play-forward-outline",
|
||||
click() {
|
||||
checkBatch();
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
rowHandle: {
|
||||
|
||||
@@ -13,10 +13,15 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
|
||||
return await api.GetList(query);
|
||||
};
|
||||
// 编辑允许修改备注与权限范围。
|
||||
// keyId/keySecret 属于密钥本体,列表中 keySecret 是脱敏串,误提交会把真实密钥写坏,故不提交。
|
||||
const editRequest = async (req: EditReq) => {
|
||||
const { form, row } = req;
|
||||
form.id = row.id;
|
||||
const res = await api.UpdateObj(form);
|
||||
const res = await api.UpdateObj({
|
||||
id: row.id,
|
||||
remark: form.remark,
|
||||
scope: form.scope,
|
||||
});
|
||||
return res;
|
||||
};
|
||||
const delRequest = async (req: DelReq) => {
|
||||
@@ -68,7 +73,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
buttons: {
|
||||
view: { show: true },
|
||||
copy: { show: false },
|
||||
edit: { show: false },
|
||||
edit: { show: true },
|
||||
remove: { show: true },
|
||||
gen: {
|
||||
text: t("certd.gen.text"),
|
||||
@@ -193,6 +198,22 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
sorter: true,
|
||||
},
|
||||
},
|
||||
remark: {
|
||||
title: t("certd.keyRemark"),
|
||||
type: "text",
|
||||
search: {
|
||||
show: true,
|
||||
},
|
||||
column: {
|
||||
width: 200,
|
||||
sorter: true,
|
||||
},
|
||||
form: {
|
||||
show: true,
|
||||
helper: t("certd.keyRemarkHelper"),
|
||||
rules: [{ max: 512, message: t("certd.keyRemarkMaxChars") }],
|
||||
},
|
||||
},
|
||||
projectId: {
|
||||
title: t("certd.fields.projectName"),
|
||||
type: "dict-select",
|
||||
|
||||
+60
-15
@@ -21,7 +21,17 @@
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="step === 1"
|
||||
><label class="plugin-ai-dev__label">开发需求</label><a-textarea v-model:value="requirement" :rows="6" /><a-button
|
||||
><div v-if="mode === 'new'" class="plugin-ai-dev__field">
|
||||
<label>插件作者</label>
|
||||
<PluginAuthorField v-model="author" editable />
|
||||
<div class="plugin-ai-dev__hint">自动读取绑定账号已注册的插件作者,也可以手动输入作者名称。</div>
|
||||
</div>
|
||||
<div v-else class="plugin-ai-dev__field">
|
||||
<label>插件作者</label>
|
||||
<a-input :value="author" disabled />
|
||||
<div class="plugin-ai-dev__hint">修改已有插件时沿用该插件当前作者,作者变化会导致插件身份变化。</div>
|
||||
</div>
|
||||
<label class="plugin-ai-dev__label">开发需求</label><a-textarea v-model:value="requirement" :rows="6" /><a-button
|
||||
class="plugin-ai-dev__generate"
|
||||
type="primary"
|
||||
:loading="creating"
|
||||
@@ -120,6 +130,7 @@ import * as api from "../api";
|
||||
import * as pipelineApi from "/@/views/certd/pipeline/api";
|
||||
import * as historyApi from "/@/views/certd/pipeline/api.history";
|
||||
import DependPluginsInput from "./depend-plugins-input.vue";
|
||||
import PluginAuthorField from "./plugin-author-field.vue";
|
||||
import PluginEditDialogBody from "./plugin-edit-dialog-body.vue";
|
||||
import PipelineEditDialog from "/@/views/certd/pipeline/components/pipeline-edit-dialog.vue";
|
||||
import { useFormDialog } from "/@/use/use-dialog";
|
||||
@@ -135,6 +146,7 @@ const pluginTypeOptions = [
|
||||
{ value: "notification", label: "通知插件", desc: "通过消息、邮件等方式发送通知" },
|
||||
// { value: "addon", label: "Addon", desc: "扩展流水线的通用辅助能力" },
|
||||
];
|
||||
const author = ref("");
|
||||
const requirement = ref(
|
||||
"例如:开发一个部署证书到阿里云DCDN的插件\nAPI接口请参考:https://help.aliyun.com/zh/edge-security-acceleration/dcdn/developer-reference/api-dcdn-2018-01-15-overview。\n要求: \n1、能够引用上传到阿里云CAS步骤输出的证书。\n2、支持在UI上选择账户下某区域下的DCDN的域名列表。\n3、支持一次性选择多个域名进行部署"
|
||||
);
|
||||
@@ -183,7 +195,33 @@ async function createPrompt() {
|
||||
try {
|
||||
const token = await api.GetScopedAccessToken(["sys/ai"]);
|
||||
accessToken.value = token.token;
|
||||
prompt.value = `你是 Certd 在线插件开发 Agent。\n\n开发模式:${mode.value === "new" ? "开发新插件" : "修改已有插件"}\n插件类型:${pluginType.value || "按已有插件类型"}\n插件 ID:${pluginPath.value.at(-1) || "无"}\n用户需求:\n${requirement.value.trim()}\n\n如果用户需求描述与他选择的插件类型有冲突,你需要跟用户确认是否选错插件类型。 \nCertd 地址:${window.location.origin}\n受限 AccessToken(6小时有效):${token.token}\n\n请先读取 .trae/skills/certd-online-plugin-dev/SKILL.md,按插件类型开发并提交版本。仅调用 /scoped/sys/ai/plugin/ 前缀接口,完成后报告提交结果,不自动发布。\n如果当前工作目录不是 Certd 项目,或缺少 certd-online-plugin-dev Skill,先拉取 Certd 仓库代码并切换到仓库内工作(git clone https://atomgit.com/certd/certd --depth 1 )`;
|
||||
const authorName = author.value.trim();
|
||||
let authorText = authorName;
|
||||
if (!authorText && mode.value === "edit") {
|
||||
// 修改已有插件时作者保持原样,不能凭空补一个作者出来
|
||||
authorText = "无(保持原插件的 author 字段不变,不要新增作者)";
|
||||
}
|
||||
if (!authorText) {
|
||||
authorText = "未指定(请先向用户确认作者名称,不要自行编造作者名)";
|
||||
}
|
||||
prompt.value = `
|
||||
你是 Certd 在线插件开发 Agent。
|
||||
|
||||
开发模式:${mode.value === "new" ? "开发新插件" : "修改已有插件"}
|
||||
插件类型:${pluginType.value || "按已有插件类型"}
|
||||
插件 ID:${pluginPath.value.at(-1) || "无"}
|
||||
插件作者:${authorText}
|
||||
用户需求:
|
||||
${requirement.value.trim()}
|
||||
|
||||
如果用户需求描述与他选择的插件类型有冲突,你需要跟用户确认是否选错插件类型。
|
||||
Certd 地址:${window.location.origin}
|
||||
受限 AccessToken(6小时有效):${token.token}
|
||||
|
||||
请先读取 .trae/skills/certd-online-plugin-dev/SKILL.md,按插件类型开发并提交版本。仅调用 /scoped/sys/ai/plugin/ 前缀接口,完成后报告提交结果,不自动发布。
|
||||
如果当前工作目录不是 Certd 项目,或缺少 certd-online-plugin-dev Skill,先拉取 Certd 仓库代码并切换到仓库内工作(git clone https://atomgit.com/certd/certd --depth 1 )
|
||||
本次开发的在线插件无需写单元测试。
|
||||
`;
|
||||
notification.success({ message: "启动提示词已生成" });
|
||||
} finally {
|
||||
creating.value = false;
|
||||
@@ -259,24 +297,31 @@ async function loadRecentPlugins(force = false) {
|
||||
async function loadSelectedPlugin() {
|
||||
const selectedValue = pluginPath.value.at(-1);
|
||||
if (!selectedValue) {
|
||||
recentPlugins.value = [];
|
||||
setSelectedPlugin(null);
|
||||
return;
|
||||
}
|
||||
if (props.pluginId && selectedValue === String(props.pluginId)) {
|
||||
const selected = await api.GetObj(props.pluginId);
|
||||
recentPlugins.value = selected ? [selected] : [];
|
||||
setSelectedPlugin(selected);
|
||||
return;
|
||||
}
|
||||
let selected = await api.GetObj(selectedValue);
|
||||
if (selected) {
|
||||
recentPlugins.value = [selected];
|
||||
return;
|
||||
if (!selected) {
|
||||
const fullName = selectedValue.includes(":") ? selectedValue.slice(selectedValue.indexOf(":") + 1) : selectedValue;
|
||||
const result: any = await api.FindPlugins({ includeBuiltIn: true, includeStore: true, keyword: fullName });
|
||||
const records: any[] = Array.isArray(result) ? result : result?.records || [];
|
||||
selected = records.find(item => String(item.fullName || item.name) === fullName) || records[0];
|
||||
}
|
||||
setSelectedPlugin(selected);
|
||||
}
|
||||
|
||||
function setSelectedPlugin(plugin: any) {
|
||||
recentPlugins.value = plugin ? [plugin] : [];
|
||||
const pluginAuthor = String(plugin?.author || "").trim();
|
||||
if (pluginAuthor) {
|
||||
// 修改已有插件时,作者默认沿用该插件当前作者,避免插件身份被改错
|
||||
author.value = pluginAuthor;
|
||||
}
|
||||
const fullName = selectedValue.includes(":") ? selectedValue.slice(selectedValue.indexOf(":") + 1) : selectedValue;
|
||||
const result: any = await api.FindPlugins({ includeBuiltIn: true, includeStore: true, keyword: fullName });
|
||||
const records: any[] = Array.isArray(result) ? result : result?.records || [];
|
||||
selected = records.find(item => String(item.fullName || item.name) === fullName) || records[0];
|
||||
recentPlugins.value = selected ? [selected] : [];
|
||||
}
|
||||
|
||||
function formatTime(value: any) {
|
||||
@@ -361,9 +406,9 @@ watch(mode, value => {
|
||||
<style lang="less">
|
||||
.plugin-ai-dev {
|
||||
display: flex;
|
||||
height: 60vh;
|
||||
height: 65vh;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
gap: 10px;
|
||||
|
||||
&__steps {
|
||||
flex: none;
|
||||
@@ -373,7 +418,7 @@ watch(mode, value => {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
gap: 10px;
|
||||
padding: 8px 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="plugin-author-field">
|
||||
<a-input :value="modelValue" placeholder="请输入作者名称" :disabled="authorRegistered" @update:value="emit('update:modelValue', $event)" />
|
||||
<a-input :value="modelValue" placeholder="请输入作者名称" :disabled="inputDisabled" @update:value="emit('update:modelValue', $event)" />
|
||||
<a-button type="link" size="small" :loading="loading" @click="handleAuthorAction">
|
||||
{{ authorRegistered ? "修改邮箱" : "注册作者" }}
|
||||
</a-button>
|
||||
@@ -14,12 +14,22 @@ import { usePluginPublish } from "../use-publish";
|
||||
|
||||
defineOptions({ name: "PluginAuthorField" });
|
||||
|
||||
const props = defineProps<{ modelValue?: string }>();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue?: string;
|
||||
// 允许手动输入作者名称,默认使用后端已注册作者且不可修改
|
||||
editable?: boolean;
|
||||
}>(),
|
||||
{
|
||||
editable: false,
|
||||
}
|
||||
);
|
||||
const emit = defineEmits<{ (event: "update:modelValue", value: string): void }>();
|
||||
const loading = ref(false);
|
||||
const { registerPluginAuthor, updatePluginAuthorEmail } = usePluginPublish();
|
||||
const registeredAuthor = ref<api.OnlinePluginAuthorBean>();
|
||||
const authorRegistered = computed(() => !!registeredAuthor.value?.id);
|
||||
const inputDisabled = computed(() => authorRegistered.value && !props.editable);
|
||||
const isLocalAuthor = computed(() => {
|
||||
return (
|
||||
String(props.modelValue || "")
|
||||
|
||||
@@ -14,10 +14,13 @@ import { usePluginStore } from "/@/store/plugin";
|
||||
import PluginAuthorField from "./components/plugin-author-field.vue";
|
||||
import { usePluginAiDev } from "./use-ai-dev";
|
||||
import { useOnlineInstall } from "./use-online-install";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||
const { t } = useI18n();
|
||||
|
||||
const lastSyncTime = context.lastSyncTime;
|
||||
const syncLoading = context.syncLoading;
|
||||
const syncOnlinePlugins = context.syncOnlinePlugins;
|
||||
let lastType = "";
|
||||
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
|
||||
if (lastType && lastType != query?.query?.type) {
|
||||
@@ -65,10 +68,8 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
|
||||
const settingStore = useSettingStore();
|
||||
const pluginStore = usePluginStore();
|
||||
const syncLoading = ref(false);
|
||||
const upgradeAllLoading = ref(false);
|
||||
const lastSyncTime = ref(0);
|
||||
const autoSyncInterval = 30 * 24 * 60 * 60 * 1000;
|
||||
|
||||
const syncButtonTitle = computed(() => {
|
||||
if (!lastSyncTime.value) {
|
||||
return t("certd.onlinePluginNotSynced");
|
||||
@@ -79,39 +80,13 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
});
|
||||
|
||||
function formatSyncTime(time: number) {
|
||||
return new Date(time).toLocaleString();
|
||||
}
|
||||
|
||||
function needAutoSync(time: number) {
|
||||
if (!time) {
|
||||
return true;
|
||||
}
|
||||
return Date.now() - time > autoSyncInterval;
|
||||
return dayjs(time).format("YYYY-MM-DD HH:mm:ss");
|
||||
}
|
||||
|
||||
function isEditablePlugin(row: any) {
|
||||
return row.editable === true;
|
||||
}
|
||||
|
||||
async function syncOnlinePlugins(options?: { showSuccess?: boolean }) {
|
||||
if (syncLoading.value) {
|
||||
return;
|
||||
}
|
||||
syncLoading.value = true;
|
||||
try {
|
||||
await api.OnlinePluginSync();
|
||||
const setting = await api.OnlinePluginSetting();
|
||||
lastSyncTime.value = setting.lastSyncTime || Date.now();
|
||||
await pluginStore.reload();
|
||||
crudExpose.doRefresh();
|
||||
if (options?.showSuccess !== false) {
|
||||
notification.success({ message: t("certd.onlinePluginSyncSuccess") });
|
||||
}
|
||||
} finally {
|
||||
syncLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function upgradeAllOnlinePlugins() {
|
||||
if (upgradeAllLoading.value) {
|
||||
return;
|
||||
@@ -157,18 +132,6 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
upgradeAllLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadOnlinePluginSetting() {
|
||||
const setting = await api.OnlinePluginSetting();
|
||||
lastSyncTime.value = setting.lastSyncTime || 0;
|
||||
if (needAutoSync(lastSyncTime.value)) {
|
||||
await syncOnlinePlugins({ showSuccess: false });
|
||||
}
|
||||
}
|
||||
|
||||
loadOnlinePluginSetting().catch(e => {
|
||||
console.warn("load online plugin setting failed", e);
|
||||
});
|
||||
return {
|
||||
crudOptions: {
|
||||
settings: {
|
||||
|
||||
@@ -22,18 +22,42 @@ import PluginItemCard from "./components/plugin-item-card.vue";
|
||||
import OnlinePluginDetail from "./components/online-plugin-detail.vue";
|
||||
import { useI18n } from "/src/locales";
|
||||
import { useMounted } from "/@/use/use-mounted";
|
||||
import { computed, onBeforeUnmount, onMounted } from "vue";
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
||||
import { usePluginStore } from "/@/store/plugin";
|
||||
import { useFormDialog } from "/@/use/use-dialog";
|
||||
|
||||
import { notification } from "ant-design-vue";
|
||||
import * as api from "./api";
|
||||
const { t } = useI18n();
|
||||
const pluginStore = usePluginStore();
|
||||
const { openFormDialog } = useFormDialog();
|
||||
|
||||
const lastSyncTime = ref<number>(0);
|
||||
const syncLoading = ref(false);
|
||||
const autoSyncInterval = 3 * 24 * 60 * 60 * 1000;
|
||||
defineOptions({
|
||||
name: "SysPlugin",
|
||||
});
|
||||
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
|
||||
|
||||
async function syncOnlinePlugins(options?: { showSuccess?: boolean }) {
|
||||
if (syncLoading.value) {
|
||||
return;
|
||||
}
|
||||
syncLoading.value = true;
|
||||
try {
|
||||
await api.OnlinePluginSync();
|
||||
const setting = await api.OnlinePluginSetting();
|
||||
lastSyncTime.value = setting.lastSyncTime || Date.now();
|
||||
await pluginStore.clear();
|
||||
crudExpose.doRefresh();
|
||||
if (options?.showSuccess !== false) {
|
||||
notification.success({ message: t("certd.onlinePluginSyncSuccess") });
|
||||
}
|
||||
} finally {
|
||||
syncLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const context = { lastSyncTime, syncLoading, syncOnlinePlugins };
|
||||
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context });
|
||||
const pluginList = computed(() => {
|
||||
return crudBinding.value?.data || [];
|
||||
});
|
||||
@@ -77,9 +101,25 @@ async function handlePluginChanged(payload: { action: string }) {
|
||||
crudExpose.doRefresh();
|
||||
}
|
||||
|
||||
function needAutoSync(time: number) {
|
||||
if (!time) {
|
||||
return true;
|
||||
}
|
||||
return Date.now() - time > autoSyncInterval;
|
||||
}
|
||||
|
||||
async function autoSyncOnlinePlugins() {
|
||||
const setting = await api.OnlinePluginSetting();
|
||||
lastSyncTime.value = setting.lastSyncTime || 0;
|
||||
if (needAutoSync(lastSyncTime.value)) {
|
||||
await syncOnlinePlugins({ showSuccess: false });
|
||||
}
|
||||
}
|
||||
|
||||
// 页面打开后获取列表数据
|
||||
useMounted(async () => {
|
||||
await crudExpose.doRefresh();
|
||||
autoSyncOnlinePlugins();
|
||||
crudExpose.doRefresh();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
certd_plugin_loadmode=metadata
|
||||
# certd_release_mode=dev
|
||||
@@ -3,6 +3,28 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [BUG]使用天翼云CDN部署证书时无法匹配域名 ([bb1f232](https://github.com/certd/certd/commit/bb1f232f415c522851dede979f5d609dda844fd3))
|
||||
* 支持google persist校验方式 ([97d2b50](https://github.com/certd/certd/commit/97d2b5007409ec79ea864d437c473a2f9b5dfae4))
|
||||
* **config:** 修复环境变量加载逻辑并更新配置文件 ([07f0d3b](https://github.com/certd/certd/commit/07f0d3b4d626628a0e3dfc7e5c52e8c4a98bad47))
|
||||
* **ctyun-cdn:** 修复天翼云CDN部署插件的域名选择问题 ([ea7d14c](https://github.com/certd/certd/commit/ea7d14c289b8720781bf101bfef59eac7bb03765))
|
||||
* **webhook:** 修复模版转义换行符,避免手动上传的证书报错 ([2735feb](https://github.com/certd/certd/commit/2735feb3302f8308708dafc8b295adcd4b91d7f0))
|
||||
|
||||
### Features
|
||||
|
||||
* **client:** 新增客户端管理功能 ([3dbbe72](https://github.com/certd/certd/commit/3dbbe72fc15c114ca6b5e8ff45cb21f258b80650))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 宝塔站点列表支持搜索和排序 ([4420472](https://github.com/certd/certd/commit/4420472f8fac272d30fb8ccb1cc2ee9763989001))
|
||||
* 网宿插件双鉴权支持 ([241b2e5](https://github.com/certd/certd/commit/241b2e55848471e61b5d2be0c87a7dc9a0d29557))
|
||||
* **monitor:** 站点证书监控支持手动选中触发检查 ([f60e63f](https://github.com/certd/certd/commit/f60e63fbaf00460d5050cd7705bd75d57dec202d))
|
||||
* **open-api:** openkey增加用途备注 ([bbeaf61](https://github.com/certd/certd/commit/bbeaf617d19750515f6eca51863e74e23bc1de50))
|
||||
* **tencent clb:** 修改所有CLB地域的endpoint为就近接入 ([c427735](https://github.com/certd/certd/commit/c4277350bcc8cf034cf4e76db2a6daa1842bc5d7))
|
||||
|
||||
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
|
||||
-- 客户端登记表:certd-client 心跳上报后记录在线状态;客户端自身信息保留独立列,站点统计等快照存 content JSON,后续扩展直接往 JSON 加字段。
|
||||
CREATE TABLE `cd_client`
|
||||
(
|
||||
`id` bigint PRIMARY KEY AUTO_INCREMENT NOT NULL,
|
||||
`user_id` bigint NOT NULL,
|
||||
`project_id` bigint,
|
||||
`client_id` varchar(64) NOT NULL,
|
||||
`key_id` varchar(64),
|
||||
`machine_name` varchar(255),
|
||||
`version` varchar(64),
|
||||
`os` varchar(64),
|
||||
`content` longtext,
|
||||
`last_heartbeat_at` bigint NOT NULL,
|
||||
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE = InnoDB ROW_FORMAT = DYNAMIC;
|
||||
|
||||
CREATE INDEX `index_client_user_id` ON `cd_client` (`user_id`);
|
||||
CREATE INDEX `index_client_project_id` ON `cd_client` (`project_id`);
|
||||
CREATE UNIQUE INDEX `index_client_user_client` ON `cd_client` (`user_id`, `client_id`);
|
||||
|
||||
-- 开放接口密钥用途备注
|
||||
ALTER TABLE cd_open_key ADD COLUMN `remark` varchar(512);
|
||||
@@ -0,0 +1,24 @@
|
||||
|
||||
-- 客户端登记表:certd-client 心跳上报后记录在线状态;客户端自身信息保留独立列,站点统计等快照存 content JSON,后续扩展直接往 JSON 加字段。
|
||||
CREATE TABLE "cd_client"
|
||||
(
|
||||
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||
"user_id" bigint NOT NULL,
|
||||
"project_id" bigint,
|
||||
"client_id" varchar(64) NOT NULL,
|
||||
"key_id" varchar(64),
|
||||
"machine_name" varchar(255),
|
||||
"version" varchar(64),
|
||||
"os" varchar(64),
|
||||
"content" text,
|
||||
"last_heartbeat_at" bigint NOT NULL,
|
||||
"create_time" timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
"update_time" timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
||||
);
|
||||
|
||||
CREATE INDEX "index_client_user_id" ON "cd_client" ("user_id");
|
||||
CREATE INDEX "index_client_project_id" ON "cd_client" ("project_id");
|
||||
CREATE UNIQUE INDEX "index_client_user_client" ON "cd_client" ("user_id", "client_id");
|
||||
|
||||
-- 开放接口密钥用途备注
|
||||
ALTER TABLE cd_open_key ADD COLUMN "remark" varchar(512);
|
||||
@@ -0,0 +1,24 @@
|
||||
|
||||
-- 客户端登记表:certd-client 心跳上报后记录在线状态;客户端自身信息保留独立列,站点统计等快照存 content JSON,后续扩展直接往 JSON 加字段。
|
||||
CREATE TABLE "cd_client"
|
||||
(
|
||||
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
"user_id" integer NOT NULL,
|
||||
"project_id" integer,
|
||||
"client_id" varchar(64) NOT NULL,
|
||||
"key_id" varchar(64),
|
||||
"machine_name" varchar(255),
|
||||
"version" varchar(64),
|
||||
"os" varchar(64),
|
||||
"content" text,
|
||||
"last_heartbeat_at" integer NOT NULL,
|
||||
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
||||
);
|
||||
|
||||
CREATE INDEX "index_client_user_id" ON "cd_client" ("user_id");
|
||||
CREATE INDEX "index_client_project_id" ON "cd_client" ("project_id");
|
||||
CREATE UNIQUE INDEX "index_client_user_client" ON "cd_client" ("user_id", "client_id");
|
||||
|
||||
-- 开放接口密钥用途备注
|
||||
ALTER TABLE cd_open_key ADD COLUMN "remark" varchar(512);
|
||||
@@ -76,15 +76,20 @@ async function genMetadata() {
|
||||
|
||||
fs.rmSync("./metadata", { recursive: true });
|
||||
fs.mkdirSync("./metadata", { recursive: true });
|
||||
const warnings = []
|
||||
for (const key in modules) {
|
||||
const module = modules[key];
|
||||
const entry = Object.entries(module);
|
||||
if (entry.length > 1) {
|
||||
console.log(`[warning] 文件 ${key} 导出了 ${entry.length} 个对象: ${entry.map(([name, value]) => name).join(", ")}`);
|
||||
}
|
||||
// if (entry.length > 1) {
|
||||
// warnings.push(`[warning] 文件 ${key} 导出了 ${entry.length} 个对象: ${entry.map(([name, value]) => name).join(", ")}`);
|
||||
// }
|
||||
const isManyExport = entry.length > 1;
|
||||
for (const [name, value] of entry) {
|
||||
//如果有define属性
|
||||
if (value.define) {
|
||||
if (isManyExport) {
|
||||
throw new Error(`插件文件禁止导出多个对象, ${key} 导出了 ${entry.length} 个对象: ${entry.map(([name, value]) => name).join(", ")}`);
|
||||
}
|
||||
//那么就是插件
|
||||
let location = key.substring(4);
|
||||
location = location.substring(0, location.length - 3);
|
||||
@@ -121,6 +126,11 @@ async function genMetadata() {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (warnings.length > 0) {
|
||||
for (const warning of warnings) {
|
||||
console.log(warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function genPluginMd() {
|
||||
|
||||
@@ -3,25 +3,87 @@ title: 网宿授权
|
||||
desc: ''
|
||||
icon: svg:icon-wangsu
|
||||
input:
|
||||
authType:
|
||||
title: 鉴权方式
|
||||
value: aksk
|
||||
component:
|
||||
name: a-select
|
||||
vModel: value
|
||||
allowClear: false
|
||||
options:
|
||||
- label: AccessKey签名
|
||||
value: aksk
|
||||
- label: ApiKey(CDN Pro必须用这个)
|
||||
value: apikey
|
||||
helper: 网宿接口鉴权方式
|
||||
required: false
|
||||
accessKeyId:
|
||||
title: accessKeyId
|
||||
component:
|
||||
name: a-input
|
||||
vModel: value
|
||||
placeholder: accessKeyId
|
||||
component:
|
||||
name: a-input
|
||||
vModel: value
|
||||
helper: '[点击前往获取AccessKey](https://console.wangsu.com/account/accessKey?rsr=ws)'
|
||||
encrypt: false
|
||||
required: true
|
||||
mergeScript: |2-
|
||||
|
||||
return {
|
||||
show: ctx.compute(({form})=>{
|
||||
return !form.access.authType || form.access.authType === 'aksk';
|
||||
})
|
||||
}
|
||||
|
||||
accessKeySecret:
|
||||
title: accessKeySecret
|
||||
component:
|
||||
name: a-input
|
||||
vModel: value
|
||||
placeholder: accessKeySecret
|
||||
component:
|
||||
name: a-input
|
||||
vModel: value
|
||||
encrypt: true
|
||||
required: true
|
||||
mergeScript: |2-
|
||||
|
||||
return {
|
||||
show: ctx.compute(({form})=>{
|
||||
return !form.access.authType || form.access.authType === 'aksk';
|
||||
})
|
||||
}
|
||||
|
||||
apiUser:
|
||||
title: API账号名
|
||||
component:
|
||||
name: a-input
|
||||
vModel: value
|
||||
placeholder: API账号名
|
||||
helper: 控制台里的 API 账号名,与 API Key 配套使用
|
||||
encrypt: false
|
||||
required: false
|
||||
mergeScript: |2-
|
||||
|
||||
return {
|
||||
show: ctx.compute(({form})=>{
|
||||
return form.access.authType === 'apikey';
|
||||
})
|
||||
}
|
||||
|
||||
apiKey:
|
||||
title: API Key
|
||||
component:
|
||||
name: a-input
|
||||
vModel: value
|
||||
placeholder: API Key
|
||||
helper: 控制台里的 API Key,[点击前往获取](https://console.wangsu.com/account/apiManage)
|
||||
encrypt: true
|
||||
required: false
|
||||
mergeScript: |2-
|
||||
|
||||
return {
|
||||
show: ctx.compute(({form})=>{
|
||||
return form.access.authType === 'apikey';
|
||||
})
|
||||
}
|
||||
|
||||
testRequest:
|
||||
title: 测试
|
||||
component:
|
||||
|
||||
@@ -59,6 +59,7 @@ input:
|
||||
vModel: value
|
||||
mode: tags
|
||||
action: GetSiteList
|
||||
search: true
|
||||
watches:
|
||||
- certDomains
|
||||
- accessId
|
||||
|
||||
@@ -80,17 +80,17 @@ input:
|
||||
helper: >
|
||||
1. <b>DNS直接验证</b>:当域名dns解析已被本系统支持时(即下方DNS解析服务商选项中可选),推荐选择此方式
|
||||
|
||||
2.
|
||||
2. <b>DNS持久验证</b>:需要先配置ACME账号和_validation-persist持久TXT记录,续期时不再增删DNS记录;当前仅
|
||||
Google证书 可以申请
|
||||
|
||||
3.
|
||||
<b>CNAME代理验证</b>:支持任何注册商的域名,第一次需要手动添加[CNAME记录](#/certd/cname/record)(如果经常申请失败,建议将DNS服务器修改为阿里云/腾讯云的,然后使用DNS直接验证)
|
||||
|
||||
3. <b>HTTP文件验证</b>:不支持泛域名,需要配置网站文件上传(IP证书必须选它)
|
||||
4. <b>HTTP文件验证</b>:不支持泛域名,需要配置网站文件上传(IP证书必须选它)
|
||||
|
||||
4. <b>多DNS提供商</b>:每个域名可以选择独立的DNS提供商
|
||||
5. <b>多DNS提供商</b>:每个域名可以选择独立的DNS提供商或校验方式
|
||||
|
||||
5. <b>自动匹配</b>:此处无需选择校验方式,需要在[域名管理](#/certd/cert/domain)中提前配置好校验方式
|
||||
|
||||
6. <b>DNS持久验证</b>:需要先配置ACME账号和_validation-persist持久TXT记录,续期时不再增删DNS记录;当前仅
|
||||
Let's Encrypt 测试环境 可以申请
|
||||
6. <b>自动匹配</b>:此处无需选择校验方式,需要在[域名管理](#/certd/cert/domain)中提前配置好校验方式
|
||||
order: 0
|
||||
dnsProviderType:
|
||||
title: DNS解析服务商
|
||||
|
||||
@@ -80,6 +80,7 @@ input:
|
||||
watches:
|
||||
- certDomains
|
||||
- accessId
|
||||
- productCode
|
||||
required: true
|
||||
mergeScript: |2-
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@certd/ui-server",
|
||||
"version": "1.44.4",
|
||||
"version": "1.45.0",
|
||||
"description": "fast-server base midway",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
@@ -43,20 +43,20 @@
|
||||
"lint1": "eslint --fix --ext .ts src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@certd/acme-client": "^1.44.4",
|
||||
"@certd/basic": "^1.44.4",
|
||||
"@certd/commercial-core": "^1.44.4",
|
||||
"@certd/acme-client": "^1.45.0",
|
||||
"@certd/basic": "^1.45.0",
|
||||
"@certd/commercial-core": "^1.45.0",
|
||||
"@certd/cv4pve-api-javascript": "^8.4.2",
|
||||
"@certd/jdcloud": "^1.44.4",
|
||||
"@certd/lib-huawei": "^1.44.4",
|
||||
"@certd/lib-k8s": "^1.44.4",
|
||||
"@certd/lib-server": "^1.44.4",
|
||||
"@certd/midway-flyway-js": "^1.44.4",
|
||||
"@certd/pipeline": "^1.44.4",
|
||||
"@certd/plugin-cert": "^1.44.4",
|
||||
"@certd/plugin-lib": "^1.44.4",
|
||||
"@certd/plugin-plus": "^1.44.4",
|
||||
"@certd/plus-core": "^1.44.4",
|
||||
"@certd/jdcloud": "^1.45.0",
|
||||
"@certd/lib-huawei": "^1.45.0",
|
||||
"@certd/lib-k8s": "^1.45.0",
|
||||
"@certd/lib-server": "^1.45.0",
|
||||
"@certd/midway-flyway-js": "^1.45.0",
|
||||
"@certd/pipeline": "^1.45.0",
|
||||
"@certd/plugin-cert": "^1.45.0",
|
||||
"@certd/plugin-lib": "^1.45.0",
|
||||
"@certd/plugin-plus": "^1.45.0",
|
||||
"@certd/plus-core": "^1.45.0",
|
||||
"@koa/cors": "^5.0.0",
|
||||
"@midwayjs/bootstrap": "3.20.11",
|
||||
"@midwayjs/cache": "3.14.0",
|
||||
|
||||
@@ -249,6 +249,7 @@ const development = {
|
||||
],
|
||||
},
|
||||
} as MidwayConfig;
|
||||
loadDotEnv(`.env.${env}`);
|
||||
loadDotEnv();
|
||||
|
||||
mergeConfig(development, "development");
|
||||
|
||||
@@ -47,10 +47,18 @@ export function mergeConfig(config: any, envType: string) {
|
||||
return config;
|
||||
}
|
||||
|
||||
export function loadDotEnv() {
|
||||
const envStr = fs.readFileSync(".env").toString();
|
||||
export function loadDotEnv(envName = ".env") {
|
||||
if (!fs.existsSync(envName)) {
|
||||
return;
|
||||
}
|
||||
logger.info("load env file:", envName);
|
||||
const envStr = fs.readFileSync(envName).toString();
|
||||
envStr.split("\n").forEach(line => {
|
||||
const [key, value] = line.trim().split("=");
|
||||
if (key.trim().startsWith("#") || key.trim() === "") {
|
||||
return;
|
||||
}
|
||||
logger.info("load env var:", key, value);
|
||||
const oldValue = process.env[key];
|
||||
if (!oldValue) {
|
||||
process.env[key] = value;
|
||||
|
||||
@@ -116,11 +116,10 @@ export class MainConfiguration {
|
||||
//授权处理
|
||||
AuthorityMiddleware,
|
||||
AuditLogMiddleware,
|
||||
|
||||
//resetPasswd,重置密码模式下不提供服务
|
||||
ResetPasswdMiddleware,
|
||||
]);
|
||||
|
||||
|
||||
this.app.getMiddleware().insertFirst(async (ctx: IMidwayKoaContext, next: NextFunction) => {
|
||||
await next();
|
||||
const path = ctx.path;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { getReleaseMode, normalizeReleaseVersion } from "./app-controller.js";
|
||||
import { normalizeReleaseVersion } from "./app-controller.js";
|
||||
|
||||
describe("AppController.normalizeReleaseVersion", () => {
|
||||
it("normalizes AtomGit release tag names", () => {
|
||||
@@ -15,29 +15,3 @@ describe("AppController.normalizeReleaseVersion", () => {
|
||||
assert.equal(normalizeReleaseVersion({ name: "v1.40.0" }), "1.40.0");
|
||||
});
|
||||
});
|
||||
|
||||
describe("AppController.getReleaseMode", () => {
|
||||
it("returns 'latest' when env var is not set", () => {
|
||||
delete process.env.certd_release_mode;
|
||||
assert.equal(getReleaseMode(), "latest");
|
||||
});
|
||||
|
||||
it("returns 'latest' when env var is empty", () => {
|
||||
process.env.certd_release_mode = "";
|
||||
assert.equal(getReleaseMode(), "latest");
|
||||
});
|
||||
|
||||
it("returns 'stable' when env var is 'stable'", () => {
|
||||
process.env.certd_release_mode = "stable";
|
||||
assert.equal(getReleaseMode(), "stable");
|
||||
});
|
||||
|
||||
it("returns 'latest' when env var is an unknown value", () => {
|
||||
process.env.certd_release_mode = "unknown";
|
||||
assert.equal(getReleaseMode(), "latest");
|
||||
});
|
||||
|
||||
after(() => {
|
||||
delete process.env.certd_release_mode;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Body, Controller, Get, Inject, Post, Provide } from "@midwayjs/core";
|
||||
import { Body, Config, Controller, Get, Inject, Post, Provide } from "@midwayjs/core";
|
||||
import { BaseController, Constants, FileService, SysSettingsService, SysSiteInfo } from "@certd/lib-server";
|
||||
import { http, logger } from "@certd/basic";
|
||||
import { isComm } from "@certd/plus-core";
|
||||
@@ -8,10 +8,6 @@ export function normalizeReleaseVersion(release: { tag_name?: string; name?: str
|
||||
return version.replace(/^v/i, "");
|
||||
}
|
||||
|
||||
export function getReleaseMode(): "stable" | "latest" {
|
||||
return process.env.certd_release_mode === "stable" ? "stable" : "latest";
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
@Provide()
|
||||
@@ -22,9 +18,14 @@ export class AppController extends BaseController {
|
||||
@Inject()
|
||||
fileService: FileService;
|
||||
|
||||
@Config("release.mode")
|
||||
releaseMode: "stable" | "latest";
|
||||
|
||||
config: any;
|
||||
|
||||
@Get("/latest", { description: Constants.per.authOnly })
|
||||
async latest(): Promise<any> {
|
||||
const mode = getReleaseMode();
|
||||
const mode = this.releaseMode;
|
||||
try {
|
||||
let latest = "";
|
||||
if (mode === "stable") {
|
||||
|
||||
@@ -4,8 +4,7 @@ import { AppKey, getPlusInfo, isComm } from "@certd/plus-core";
|
||||
import { SysInviteCommissionSetting } from "@certd/commercial-core";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import { getVersion } from "../../utils/version.js";
|
||||
import { http } from "@certd/basic";
|
||||
import { getReleaseMode } from "./app-controller.js";
|
||||
import { http, logger } from "@certd/basic";
|
||||
|
||||
/**
|
||||
*/
|
||||
@@ -17,6 +16,9 @@ export class BasicSettingsController extends BaseController {
|
||||
@Config("account.server.baseUrl")
|
||||
accountServerBaseUrl: any;
|
||||
|
||||
@Config("release.mode")
|
||||
releaseMode: "stable" | "latest";
|
||||
|
||||
@Config("agent")
|
||||
agentConfig: SysSiteEnv["agent"];
|
||||
|
||||
@@ -97,6 +99,7 @@ export class BasicSettingsController extends BaseController {
|
||||
const suiteSetting = await this.getSuiteSetting();
|
||||
const inviteSetting = await this.getInviteSetting();
|
||||
const version = await getVersion();
|
||||
logger.info(`当前版本模式:${this.releaseMode}, 当前版本:${version}`);
|
||||
return this.ok({
|
||||
sysPublic,
|
||||
installInfo,
|
||||
@@ -109,7 +112,7 @@ export class BasicSettingsController extends BaseController {
|
||||
app: {
|
||||
time: new Date().getTime(),
|
||||
version,
|
||||
releaseMode: getReleaseMode(),
|
||||
releaseMode: this.releaseMode,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { ALL, Body, Controller, Inject, Post, Provide } from "@midwayjs/core";
|
||||
import { CodeException, Constants } from "@certd/lib-server";
|
||||
import { isPlus } from "@certd/plus-core";
|
||||
import { ApiTags } from "@midwayjs/swagger";
|
||||
import { OpenKey } from "../../../modules/open/service/open-key-service.js";
|
||||
import { BaseOpenController } from "../base-open-controller.js";
|
||||
import { ClientHeartbeatReq, ClientService } from "../../../modules/client/service/client-service.js";
|
||||
|
||||
/**
|
||||
* certd-client 客户端上报接口。
|
||||
*/
|
||||
@Provide()
|
||||
@Controller("/api/v1/client")
|
||||
@ApiTags(["openapi"])
|
||||
export class OpenClientController extends BaseOpenController {
|
||||
@Inject()
|
||||
clientService: ClientService;
|
||||
|
||||
@Post("/heartbeat", { description: Constants.per.open, summary: "客户端心跳上报" })
|
||||
async heartbeat(@Body(ALL) bean: ClientHeartbeatReq) {
|
||||
const openKey: OpenKey = this.ctx.openKey;
|
||||
const userId = openKey.userId;
|
||||
if (userId == null) {
|
||||
throw new CodeException(Constants.res.openKeyError);
|
||||
}
|
||||
// 客户端上报仅专业版可用;非专业版直接忽略,不保存也不报错。
|
||||
if (!isPlus()) {
|
||||
return this.ok({ success: true });
|
||||
}
|
||||
await this.clientService.heartbeat(userId, openKey.projectId, openKey.keyId, bean);
|
||||
return this.ok({ success: true });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ALL, Body, Controller, Inject, Post, Provide, Query } from "@midwayjs/core";
|
||||
import { Constants, CrudController } from "@certd/lib-server";
|
||||
import { ApiTags } from "@midwayjs/swagger";
|
||||
import { ClientService } from "../../../modules/client/service/client-service.js";
|
||||
|
||||
/**
|
||||
* 客户端管理接口。
|
||||
*/
|
||||
@Provide()
|
||||
@Controller("/api/client")
|
||||
@ApiTags(["client"])
|
||||
export class ClientController extends CrudController<ClientService> {
|
||||
@Inject()
|
||||
service: ClientService;
|
||||
|
||||
getService(): ClientService {
|
||||
return this.service;
|
||||
}
|
||||
|
||||
@Post("/page", { description: Constants.per.authOnly, summary: "查询客户端分页列表" })
|
||||
async page(@Body(ALL) body: any) {
|
||||
body.query = body.query ?? {};
|
||||
const { projectId, userId } = await this.getProjectUserIdRead();
|
||||
body.query.projectId = projectId;
|
||||
body.query.userId = userId;
|
||||
const res = await this.service.page({
|
||||
query: body.query,
|
||||
page: body.page,
|
||||
sort: body.sort,
|
||||
});
|
||||
return this.ok(res);
|
||||
}
|
||||
|
||||
@Post("/info", { description: Constants.per.authOnly, summary: "查询客户端详情" })
|
||||
async info(@Query("id") id: number) {
|
||||
await this.checkOwner(this.getService(), id, "read");
|
||||
const bean = await this.service.info(id);
|
||||
return this.ok(bean);
|
||||
}
|
||||
|
||||
@Post("/delete", { description: Constants.per.authOnly, summary: "删除客户端记录" })
|
||||
async delete(@Query("id") id: number) {
|
||||
await this.checkOwner(this.getService(), id, "write");
|
||||
const res = await super.delete(id);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
@@ -134,6 +134,14 @@ export class SiteInfoController extends CrudController<SiteInfoService> {
|
||||
return this.ok();
|
||||
}
|
||||
|
||||
@Post("/checkBatch", { description: Constants.per.authOnly, summary: "批量检查站点监控" })
|
||||
async checkBatch(@Body(ALL) body: any) {
|
||||
const { projectId, userId } = await this.getProjectUserIdRead();
|
||||
const count = await this.service.checkBatch(body.ids, userId, projectId);
|
||||
this.auditLog({ content: `批量检查了${count}条站点监控` });
|
||||
return this.ok({ count });
|
||||
}
|
||||
|
||||
@Post("/checkAll", { description: Constants.per.authOnly, summary: "检查所有站点监控" })
|
||||
async checkAll() {
|
||||
const { projectId, userId } = await this.getProjectUserIdWrite();
|
||||
|
||||
@@ -62,7 +62,7 @@ export class OpenKeyController extends CrudController<OpenKeyService> {
|
||||
body.userId = userId;
|
||||
const res = await this.service.add(body);
|
||||
this.auditLog({
|
||||
content: `新增了API密钥(ID:${res.id}, scope:${body.scope})`,
|
||||
content: `新增了API密钥(ID:${res.id}, scope:${body.scope}, remark:${body.remark})`,
|
||||
});
|
||||
return this.ok(res);
|
||||
}
|
||||
@@ -72,9 +72,14 @@ export class OpenKeyController extends CrudController<OpenKeyService> {
|
||||
await this.checkOwner(this.getService(), bean.id, "write");
|
||||
delete bean.userId;
|
||||
delete bean.projectId;
|
||||
await this.service.update(bean);
|
||||
const upd = {
|
||||
id: bean.id,
|
||||
remark: bean.remark,
|
||||
scope: bean.scope,
|
||||
};
|
||||
await this.service.update(upd);
|
||||
this.auditLog({
|
||||
content: `修改了API密钥(ID:${bean.id})`,
|
||||
content: `修改了API密钥(ID:${bean.id}, scope:${bean.scope}, remark:${bean.remark})`,
|
||||
});
|
||||
return this.ok();
|
||||
}
|
||||
@@ -100,9 +105,10 @@ export class OpenKeyController extends CrudController<OpenKeyService> {
|
||||
@Post("/delete", { description: Constants.per.authOnly, summary: "删除开放API密钥" })
|
||||
async delete(@Query("id") id: number) {
|
||||
await this.checkOwner(this.getService(), id, "write");
|
||||
const info = await this.service.info(id);
|
||||
const res = await super.delete(id);
|
||||
this.auditLog({
|
||||
content: `删除了API密钥(ID:${id})`,
|
||||
content: `删除了API密钥(ID:${id}, scope:${info.scope}, remark:${info.remark})`,
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
+42
-3
@@ -1,7 +1,16 @@
|
||||
import assert from "assert";
|
||||
import { getDnsPersistIssuer } from "@certd/acme-client";
|
||||
import { buildDnsPersistRecordValue, DnsPersistRecordService } from "./dns-persist-record-service.js";
|
||||
|
||||
describe("DnsPersistRecordService", () => {
|
||||
it("gets issuer from the ACME directory URL", () => {
|
||||
assert.equal(getDnsPersistIssuer("https://dv.acme-v02.api.pki.goog/directory"), "pki.goog");
|
||||
assert.equal(getDnsPersistIssuer("https://acme-v02.api.letsencrypt.org/directory"), "letsencrypt.org");
|
||||
assert.equal(getDnsPersistIssuer("https://acme.example.co.uk/directory"), "example.co.uk");
|
||||
assert.equal(getDnsPersistIssuer(), "letsencrypt.org");
|
||||
assert.throws(() => getDnsPersistIssuer("not a url"), /Directory URL无效/);
|
||||
assert.throws(() => getDnsPersistIssuer("https://localhost/directory"), /可注册主域名/);
|
||||
});
|
||||
it("builds dns-persist-01 record value", () => {
|
||||
const value = buildDnsPersistRecordValue({
|
||||
accountUri: "https://example.com/acct/1",
|
||||
@@ -12,6 +21,36 @@ describe("DnsPersistRecordService", () => {
|
||||
assert.equal(value, "letsencrypt.org; accounturi=https://example.com/acct/1; policy=wildcard; persistUntil=1893456000");
|
||||
});
|
||||
|
||||
it("uses the registrable domain from the ACME directory URL", async () => {
|
||||
const service = new DnsPersistRecordService();
|
||||
(service as any).accessService = {
|
||||
async getAccessById() {
|
||||
return {
|
||||
account: JSON.stringify({
|
||||
accountKey: "private-key",
|
||||
accountUri: "https://pki.goog/acct/12345678",
|
||||
caType: "custom",
|
||||
directoryUrl: "https://dv.acme-v02.api.pki.goog/directory",
|
||||
}),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const buildRequest = {
|
||||
domain: "example.com",
|
||||
caType: "custom",
|
||||
acmeAccountAccessId: 12,
|
||||
userId: 1,
|
||||
};
|
||||
const acmeAccount = await service.getAcmeAccount(buildRequest);
|
||||
|
||||
assert.equal(acmeAccount.account.issuer, "pki.goog");
|
||||
|
||||
const record = await service.buildRecordByAcmeAccount(buildRequest);
|
||||
|
||||
assert.equal(record.recordValue, "pki.goog; accounturi=https://pki.goog/acct/12345678; policy=wildcard");
|
||||
});
|
||||
|
||||
it("builds validation host from wildcard domain", async () => {
|
||||
const service = new DnsPersistRecordService();
|
||||
|
||||
@@ -68,7 +107,7 @@ describe("DnsPersistRecordService", () => {
|
||||
assert.equal(record.accountUri, "https://example.com/acct/1");
|
||||
assert.equal(record.hostRecord, "_validation-persist");
|
||||
assert.equal(record.mainDomain, "example.com");
|
||||
assert.equal(record.recordValue, "letsencrypt.org; accounturi=https://example.com/acct/1; policy=wildcard");
|
||||
assert.equal(record.recordValue, "example.com; accounturi=https://example.com/acct/1; policy=wildcard");
|
||||
assert.equal(record.policy, "wildcard");
|
||||
assert.equal(record.status, "pending");
|
||||
});
|
||||
@@ -140,7 +179,7 @@ describe("DnsPersistRecordService", () => {
|
||||
assert.equal(record.id, 77);
|
||||
assert.equal(record.hostRecord, "_validation-persist");
|
||||
assert.equal(record.mainDomain, "example.com");
|
||||
assert.equal(record.recordValue, "letsencrypt.org; accounturi=https://example.com/acct/1; policy=wildcard");
|
||||
assert.equal(record.recordValue, "example.com; accounturi=https://example.com/acct/1; policy=wildcard");
|
||||
assert.equal(record.status, "pending");
|
||||
});
|
||||
|
||||
@@ -245,7 +284,7 @@ describe("DnsPersistRecordService", () => {
|
||||
assert.equal(record.policy, "wildcard");
|
||||
assert.equal(record.status, "pending");
|
||||
assert.equal(record.mainDomain, "example.com");
|
||||
assert.equal(record.recordValue, "letsencrypt.org; accounturi=https://example.com/acct/1; policy=wildcard");
|
||||
assert.equal(record.recordValue, "example.com; accounturi=https://example.com/acct/1; policy=wildcard");
|
||||
assert.equal(record.recordRes, null);
|
||||
});
|
||||
|
||||
|
||||
@@ -2,14 +2,13 @@ import { BaseService } from "@certd/lib-server";
|
||||
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import { InjectEntityModel } from "@midwayjs/typeorm";
|
||||
import { In, Repository } from "typeorm";
|
||||
import { createChallengeFn } from "@certd/acme-client";
|
||||
import { createChallengeFn, getDnsPersistIssuer } from "@certd/acme-client";
|
||||
import { AccessService } from "@certd/lib-server";
|
||||
import { http, logger, utils } from "@certd/basic";
|
||||
import { createDnsProvider, DomainParser } from "@certd/plugin-lib";
|
||||
import { DnsPersistRecordEntity } from "../entity/dns-persist-record.js";
|
||||
import { TaskServiceBuilder } from "../../pipeline/service/getter/task-service-getter.js";
|
||||
import { DomainEntity } from "../entity/domain.js";
|
||||
|
||||
export function buildDnsPersistRecordValue(req: { issuer?: string; accountUri: string; wildcard?: boolean; persistUntil?: number }) {
|
||||
const parts = [req.issuer || "letsencrypt.org", `accounturi=${req.accountUri}`];
|
||||
if (req.wildcard !== false) {
|
||||
@@ -93,8 +92,12 @@ export class DnsPersistRecordService extends BaseService<DnsPersistRecordEntity>
|
||||
if (!parsed.accountKey || !parsed.accountUri) {
|
||||
throw new Error("ACME账号授权无效,请重新生成ACME账号");
|
||||
}
|
||||
if (!parsed.issuer) {
|
||||
parsed.issuer = getDnsPersistIssuer(parsed.directoryUrl || parsed.accountUri);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
|
||||
async getAcmeAccount(req: DnsPersistRecordBuildReq & { userId: number; projectId?: number }) {
|
||||
const accessId = req.acmeAccountAccessId || req.commonAcmeAccountAccessId;
|
||||
@@ -123,7 +126,7 @@ export class DnsPersistRecordService extends BaseService<DnsPersistRecordEntity>
|
||||
};
|
||||
}
|
||||
|
||||
async buildRecord(req: { domain: string; accountUri: string; wildcard?: boolean; persistUntil?: number; userId?: number; projectId?: number }) {
|
||||
async buildRecord(req: { domain: string; accountUri: string; issuer?: string; wildcard?: boolean; persistUntil?: number; userId?: number; projectId?: number }) {
|
||||
const domain = this.normalizeDomain(req.domain);
|
||||
const mainDomain = await this.parseMainDomain(domain, req.userId, req.projectId);
|
||||
return {
|
||||
@@ -141,6 +144,7 @@ export class DnsPersistRecordService extends BaseService<DnsPersistRecordEntity>
|
||||
const record = await this.buildRecord({
|
||||
domain: req.domain,
|
||||
accountUri: account.accountUri,
|
||||
issuer: account.issuer,
|
||||
wildcard: true,
|
||||
persistUntil: req.persistUntil,
|
||||
userId: req.userId,
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||
|
||||
/**
|
||||
* certd-client 客户端登记信息。
|
||||
* 客户端自身信息(机器名、版本、系统、关联OpenKey)保留独立列;
|
||||
* 站点统计等快照统一存 content JSON,后续扩展直接往 JSON 增加字段,无需变更表结构。
|
||||
*/
|
||||
@Entity("cd_client")
|
||||
export class ClientEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ name: "user_id", comment: "用户id" })
|
||||
userId: number;
|
||||
|
||||
@Column({ name: "project_id", comment: "项目id", nullable: true })
|
||||
projectId: number;
|
||||
|
||||
@Column({ name: "client_id", comment: "客户端唯一标识", length: 64 })
|
||||
clientId: string;
|
||||
|
||||
@Column({ name: "key_id", comment: "关联OpenKey", length: 64, nullable: true })
|
||||
keyId: string;
|
||||
|
||||
@Column({ name: "machine_name", comment: "本机名称", length: 255, nullable: true })
|
||||
machineName: string;
|
||||
|
||||
@Column({ name: "version", comment: "客户端版本", length: 64, nullable: true })
|
||||
version: string;
|
||||
|
||||
@Column({ name: "os", comment: "操作系统", length: 64, nullable: true })
|
||||
os: string;
|
||||
|
||||
@Column({ name: "content", comment: "站点统计等快照(JSON)", type: "text", nullable: true })
|
||||
content: string;
|
||||
|
||||
@Column({ name: "last_heartbeat_at", comment: "上次心跳时间" })
|
||||
lastHeartbeatAt: number;
|
||||
|
||||
@Column({
|
||||
name: "create_time",
|
||||
comment: "创建时间",
|
||||
default: () => "CURRENT_TIMESTAMP",
|
||||
})
|
||||
createTime: Date;
|
||||
|
||||
@Column({
|
||||
name: "update_time",
|
||||
comment: "修改时间",
|
||||
default: () => "CURRENT_TIMESTAMP",
|
||||
})
|
||||
updateTime: Date;
|
||||
|
||||
// 以下均为非数据库字段:分页查询时从 content 解析展开,便于前端直接展示。
|
||||
appCount?: number;
|
||||
siteCount?: number;
|
||||
httpsSiteCount?: number;
|
||||
syncedSiteCount?: number;
|
||||
failedSiteCount?: number;
|
||||
lastSyncAt?: number;
|
||||
lastSyncStatus?: string;
|
||||
online?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./entity/client.js";
|
||||
export * from "./service/client-service.js";
|
||||
@@ -0,0 +1,120 @@
|
||||
import assert from "assert";
|
||||
import { ClientService, isOnline } from "./client-service.js";
|
||||
|
||||
function createService() {
|
||||
return new ClientService();
|
||||
}
|
||||
|
||||
describe("ClientService", () => {
|
||||
describe("isOnline", () => {
|
||||
it("30分钟内判定在线", () => {
|
||||
const now = Date.now();
|
||||
assert.equal(isOnline(now - 29 * 60 * 1000, now), true);
|
||||
assert.equal(isOnline(now - 30 * 60 * 1000, now), true);
|
||||
});
|
||||
|
||||
it("超过30分钟判定离线", () => {
|
||||
const now = Date.now();
|
||||
assert.equal(isOnline(now - 30 * 60 * 1000 - 1, now), false);
|
||||
});
|
||||
|
||||
it("从未心跳判定离线", () => {
|
||||
const now = Date.now();
|
||||
assert.equal(isOnline(null, now), false);
|
||||
assert.equal(isOnline(undefined, now), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("heartbeat", () => {
|
||||
it("首次心跳创建记录,统计信息打包进 content JSON", async () => {
|
||||
const service = createService();
|
||||
service.repository = {
|
||||
async findOne() {
|
||||
return null;
|
||||
},
|
||||
} as any;
|
||||
let saved: any = null;
|
||||
service.repository.save = async (entity: any) => {
|
||||
saved = entity;
|
||||
entity.id = 1;
|
||||
return entity;
|
||||
};
|
||||
|
||||
const id = await service.heartbeat(2, 3, "key-1", {
|
||||
clientId: "client-a",
|
||||
machineName: "web-01",
|
||||
version: "0.3.0",
|
||||
os: "linux",
|
||||
appCount: 2,
|
||||
siteCount: 10,
|
||||
httpsSiteCount: 8,
|
||||
syncedSiteCount: 5,
|
||||
failedSiteCount: 1,
|
||||
});
|
||||
|
||||
assert.equal(id, 1);
|
||||
assert.equal(saved.userId, 2);
|
||||
assert.equal(saved.projectId, 3);
|
||||
assert.equal(saved.clientId, "client-a");
|
||||
assert.equal(saved.keyId, "key-1");
|
||||
assert.equal(saved.machineName, "web-01");
|
||||
assert.equal(saved.version, "0.3.0");
|
||||
assert.equal(saved.os, "linux");
|
||||
assert.ok(saved.lastHeartbeatAt > 0);
|
||||
|
||||
const snapshot = JSON.parse(saved.content);
|
||||
assert.equal(snapshot.appCount, 2);
|
||||
assert.equal(snapshot.siteCount, 10);
|
||||
assert.equal(snapshot.httpsSiteCount, 8);
|
||||
assert.equal(snapshot.syncedSiteCount, 5);
|
||||
assert.equal(snapshot.failedSiteCount, 1);
|
||||
});
|
||||
|
||||
it("已有记录时更新,不重复创建", async () => {
|
||||
const service = createService();
|
||||
const existing = { id: 9 };
|
||||
service.repository = {
|
||||
async findOne() {
|
||||
return existing;
|
||||
},
|
||||
} as any;
|
||||
let updatedWhere: any = null;
|
||||
let updatedSet: any = null;
|
||||
service.repository.update = async (where: any, set: any) => {
|
||||
updatedWhere = where;
|
||||
updatedSet = set;
|
||||
return {} as any;
|
||||
};
|
||||
|
||||
const id = await service.heartbeat(2, undefined, "key-2", {
|
||||
clientId: "client-a",
|
||||
machineName: "web-01",
|
||||
version: "0.3.1",
|
||||
os: "windows",
|
||||
siteCount: 3,
|
||||
});
|
||||
|
||||
assert.equal(id, 9);
|
||||
assert.equal(updatedWhere.id, 9);
|
||||
assert.equal(updatedSet.machineName, "web-01");
|
||||
assert.equal(updatedSet.version, "0.3.1");
|
||||
assert.equal(updatedSet.os, "windows");
|
||||
assert.ok(updatedSet.lastHeartbeatAt > 0);
|
||||
const snapshot = JSON.parse(updatedSet.content);
|
||||
assert.equal(snapshot.siteCount, 3);
|
||||
// 未传的统计字段默认 0
|
||||
assert.equal(snapshot.appCount, 0);
|
||||
assert.equal(snapshot.httpsSiteCount, 0);
|
||||
});
|
||||
|
||||
it("clientId 为空时抛错", async () => {
|
||||
const service = createService();
|
||||
service.repository = {
|
||||
async findOne() {
|
||||
return null;
|
||||
},
|
||||
} as any;
|
||||
await assert.rejects(() => service.heartbeat(2, undefined, "key", {} as any), /clientId不能为空/);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
import { Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import { BaseService, PageReq } from "@certd/lib-server";
|
||||
import { InjectEntityModel } from "@midwayjs/typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
import { ClientEntity } from "../entity/client.js";
|
||||
|
||||
export type ClientHeartbeatReq = {
|
||||
clientId: string;
|
||||
machineName?: string;
|
||||
version?: string;
|
||||
os?: string;
|
||||
appCount?: number;
|
||||
siteCount?: number;
|
||||
httpsSiteCount?: number;
|
||||
syncedSiteCount?: number;
|
||||
failedSiteCount?: number;
|
||||
lastSyncAt?: number;
|
||||
lastSyncStatus?: string;
|
||||
};
|
||||
|
||||
export type ClientSnapshot = {
|
||||
appCount?: number;
|
||||
siteCount?: number;
|
||||
httpsSiteCount?: number;
|
||||
syncedSiteCount?: number;
|
||||
failedSiteCount?: number;
|
||||
lastSyncAt?: number;
|
||||
lastSyncStatus?: string;
|
||||
};
|
||||
|
||||
// 客户端心跳上报间隔为 10 分钟,在线阈值取 30 分钟(3 倍),容忍网络抖动与偶发失败。
|
||||
const ONLINE_THRESHOLD_MS = 30 * 60 * 1000;
|
||||
|
||||
export function isOnline(lastHeartbeatAt: number | null | undefined, now: number): boolean {
|
||||
return lastHeartbeatAt != null && now - lastHeartbeatAt <= ONLINE_THRESHOLD_MS;
|
||||
}
|
||||
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
export class ClientService extends BaseService<ClientEntity> {
|
||||
@InjectEntityModel(ClientEntity)
|
||||
repository: Repository<ClientEntity>;
|
||||
|
||||
//@ts-ignore
|
||||
getRepository() {
|
||||
return this.repository;
|
||||
}
|
||||
|
||||
async page(pageReq: PageReq<ClientEntity>) {
|
||||
const pageRet = await super.page(pageReq);
|
||||
const now = Date.now();
|
||||
for (const item of pageRet.records) {
|
||||
const snapshot = this.parseContent(item.content);
|
||||
item.appCount = snapshot.appCount ?? 0;
|
||||
item.siteCount = snapshot.siteCount ?? 0;
|
||||
item.httpsSiteCount = snapshot.httpsSiteCount ?? 0;
|
||||
item.syncedSiteCount = snapshot.syncedSiteCount ?? 0;
|
||||
item.failedSiteCount = snapshot.failedSiteCount ?? 0;
|
||||
item.lastSyncAt = snapshot.lastSyncAt;
|
||||
item.lastSyncStatus = snapshot.lastSyncStatus;
|
||||
item.online = isOnline(item.lastHeartbeatAt, now);
|
||||
}
|
||||
return pageRet;
|
||||
}
|
||||
|
||||
async heartbeat(userId: number, projectId: number | undefined, keyId: string, bean: ClientHeartbeatReq) {
|
||||
if (!bean?.clientId) {
|
||||
throw new Error("clientId不能为空");
|
||||
}
|
||||
const content = JSON.stringify({
|
||||
appCount: bean.appCount ?? 0,
|
||||
siteCount: bean.siteCount ?? 0,
|
||||
httpsSiteCount: bean.httpsSiteCount ?? 0,
|
||||
syncedSiteCount: bean.syncedSiteCount ?? 0,
|
||||
failedSiteCount: bean.failedSiteCount ?? 0,
|
||||
lastSyncAt: bean.lastSyncAt,
|
||||
lastSyncStatus: bean.lastSyncStatus,
|
||||
});
|
||||
const now = Date.now();
|
||||
const existing = await this.repository.findOne({
|
||||
where: { userId, clientId: bean.clientId },
|
||||
});
|
||||
if (existing) {
|
||||
await this.repository.update(
|
||||
{ id: existing.id },
|
||||
{
|
||||
projectId,
|
||||
keyId,
|
||||
machineName: bean.machineName,
|
||||
version: bean.version,
|
||||
os: bean.os,
|
||||
content,
|
||||
lastHeartbeatAt: now,
|
||||
updateTime: new Date(),
|
||||
}
|
||||
);
|
||||
return existing.id;
|
||||
}
|
||||
|
||||
const entity = new ClientEntity();
|
||||
entity.userId = userId;
|
||||
entity.projectId = projectId;
|
||||
entity.clientId = bean.clientId;
|
||||
entity.keyId = keyId;
|
||||
entity.machineName = bean.machineName;
|
||||
entity.version = bean.version;
|
||||
entity.os = bean.os;
|
||||
entity.content = content;
|
||||
entity.lastHeartbeatAt = now;
|
||||
entity.createTime = new Date();
|
||||
entity.updateTime = new Date();
|
||||
await this.repository.save(entity);
|
||||
return entity.id;
|
||||
}
|
||||
|
||||
private parseContent(content: string): ClientSnapshot {
|
||||
if (!content) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
return JSON.parse(content) as ClientSnapshot;
|
||||
} catch (err) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { SiteInfoService } from "./site-info-service.js";
|
||||
|
||||
/**
|
||||
* 批量导入的站点(doImport → add)必须带上明确的 ipCheck/ipSyncAuto/ipIgnoreCoherence 默认值。
|
||||
* 否则数据库里这三列为 NULL,编辑弹窗的“开启IP检查”开关看着是关闭状态,
|
||||
* 但必填校验认为没有值,用户必须手动开关一次才能保存(issue #803)。
|
||||
*/
|
||||
function createServiceForAdd() {
|
||||
const service = new SiteInfoService();
|
||||
service.checkMonitorLimit = async () => {};
|
||||
service.repository = {
|
||||
async findOne() {
|
||||
return null;
|
||||
},
|
||||
async save(data: any) {
|
||||
return data;
|
||||
},
|
||||
async create(data: any) {
|
||||
return data;
|
||||
},
|
||||
} as any;
|
||||
return service;
|
||||
}
|
||||
|
||||
describe("SiteInfoService", () => {
|
||||
describe("add 站点默认值(issue #803)", () => {
|
||||
it("新增站点未传 ipCheck 时,落库默认值为 false(而不是 undefined/null)", async () => {
|
||||
const service = createServiceForAdd();
|
||||
let savedData: any = null;
|
||||
service.repository.save = async (data: any) => {
|
||||
savedData = data;
|
||||
return data;
|
||||
};
|
||||
|
||||
await service.add({
|
||||
domain: "a.com",
|
||||
name: "a.com",
|
||||
httpsPort: 443,
|
||||
userId: 1,
|
||||
} as any);
|
||||
|
||||
assert.equal(savedData.ipCheck, false, "ipCheck 必须有明确的布尔默认值 false");
|
||||
assert.equal(savedData.ipSyncAuto, true, "ipSyncAuto 必须有明确的布尔默认值 true");
|
||||
assert.equal(savedData.ipIgnoreCoherence, false, "ipIgnoreCoherence 必须有明确的布尔默认值 false");
|
||||
});
|
||||
|
||||
it("显式传入的 ipCheck 不被默认值覆盖", async () => {
|
||||
const service = createServiceForAdd();
|
||||
let savedData: any = null;
|
||||
service.repository.save = async (data: any) => {
|
||||
savedData = data;
|
||||
return data;
|
||||
};
|
||||
|
||||
await service.add({
|
||||
domain: "b.com",
|
||||
name: "b.com",
|
||||
httpsPort: 443,
|
||||
userId: 1,
|
||||
ipCheck: true,
|
||||
ipSyncAuto: false,
|
||||
ipIgnoreCoherence: true,
|
||||
} as any);
|
||||
|
||||
assert.equal(savedData.ipCheck, true);
|
||||
assert.equal(savedData.ipSyncAuto, false);
|
||||
assert.equal(savedData.ipIgnoreCoherence, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("doImport 批量导入(issue #803)", () => {
|
||||
it("导入的每个站点落库都带上 ipCheck 默认值 false,且字段解析正确", async () => {
|
||||
const service = createServiceForAdd();
|
||||
const savedList: any[] = [];
|
||||
service.repository.save = async (data: any) => {
|
||||
savedList.push(data);
|
||||
return data;
|
||||
};
|
||||
|
||||
await service.doImport({
|
||||
text: "a.com\nb.com:8443:c.com:备注",
|
||||
userId: 1,
|
||||
projectId: 2,
|
||||
});
|
||||
|
||||
// add() 里已有站点时会 return {id},此处 findOne 返回 null,两条都会真实落库
|
||||
assert.equal(savedList.length, 2);
|
||||
for (const item of savedList) {
|
||||
assert.equal(item.ipCheck, false, `站点 ${item.domain} 必须带 ipCheck 默认值`);
|
||||
}
|
||||
assert.equal(savedList[0].domain, "a.com");
|
||||
assert.equal(savedList[0].httpsPort, 443);
|
||||
assert.equal(savedList[0].name, "a.com");
|
||||
assert.equal(savedList[1].domain, "b.com");
|
||||
assert.equal(savedList[1].httpsPort, 8443);
|
||||
assert.equal(savedList[1].name, "c.com");
|
||||
assert.equal(savedList[1].remark, "备注");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -99,6 +99,18 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
|
||||
|
||||
data.disabled = false;
|
||||
|
||||
// 批量导入等入口不会传这几个IP检查字段,必须补上明确的布尔默认值,
|
||||
// 否则数据库里存 NULL,编辑弹窗的开关看着是关闭状态却过不了必填校验(issue #803)
|
||||
if (data.ipCheck == null) {
|
||||
data.ipCheck = false;
|
||||
}
|
||||
if (data.ipSyncAuto == null) {
|
||||
data.ipSyncAuto = true;
|
||||
}
|
||||
if (data.ipIgnoreCoherence == null) {
|
||||
data.ipIgnoreCoherence = false;
|
||||
}
|
||||
|
||||
const found = await this.repository.findOne({
|
||||
where: {
|
||||
domain: data.domain,
|
||||
@@ -312,6 +324,23 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
|
||||
return;
|
||||
}
|
||||
|
||||
async checkBatch(ids: number[], userId: number, projectId?: number) {
|
||||
const validIds = this.filterIds(ids);
|
||||
if (validIds.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
const userProjectQuery = this.buildUserProjectQuery(userId, projectId);
|
||||
const sites = await this.repository.find({
|
||||
where: {
|
||||
id: In(validIds),
|
||||
...userProjectQuery,
|
||||
disabled: false,
|
||||
},
|
||||
});
|
||||
await this.checkList(sites);
|
||||
return sites.length;
|
||||
}
|
||||
|
||||
async sendCheckErrorNotify(siteId: number, fromIpCheck = false, setting: UserSiteMonitorSetting) {
|
||||
const site = await this.info(siteId);
|
||||
const url = await this.notificationService.getBindUrl("#/certd/monitor/site");
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import { getMetadataArgsStorage } from "typeorm";
|
||||
import { OpenKeyEntity } from "./open-key.js";
|
||||
|
||||
describe("OpenKeyEntity", () => {
|
||||
it("应映射密钥用途备注字段", () => {
|
||||
const remarkColumn = getMetadataArgsStorage().columns.find(column => {
|
||||
return column.target === OpenKeyEntity && column.propertyName === "remark";
|
||||
});
|
||||
|
||||
assert.ok(remarkColumn);
|
||||
assert.equal(remarkColumn.options.name, "remark");
|
||||
assert.equal(remarkColumn.options.length, 512);
|
||||
assert.equal(remarkColumn.options.nullable, true);
|
||||
});
|
||||
});
|
||||
@@ -17,6 +17,9 @@ export class OpenKeyEntity {
|
||||
@Column({ name: "scope", comment: "权限范围" })
|
||||
scope: string; // open 仅开放接口、 user 用户所有权限
|
||||
|
||||
@Column({ name: "remark", comment: "密钥用途备注", length: 512, nullable: true })
|
||||
remark: string; // 用于说明该密钥的用途,便于区分和查找
|
||||
|
||||
@Column({ name: "project_id", comment: "项目id" })
|
||||
projectId: number;
|
||||
|
||||
|
||||
@@ -31,10 +31,11 @@ export class OpenKeyService extends BaseService<OpenKeyEntity> {
|
||||
}
|
||||
|
||||
async add(bean: OpenKeyEntity) {
|
||||
return await this.generate(bean.userId, bean.projectId, bean.scope);
|
||||
return await this.generate({ userId: bean.userId, projectId: bean.projectId, scope: bean.scope, remark: bean.remark });
|
||||
}
|
||||
|
||||
async generate(userId: number, projectId?: number, scope = "open") {
|
||||
async generate(req: { userId: number; projectId?: number; scope?: string; remark?: string }) {
|
||||
const { userId, projectId, scope, remark } = req;
|
||||
const keyId = utils.id.simpleNanoId(18) + "_key";
|
||||
const secretKey = crypto.randomBytes(32);
|
||||
const keySecret = Buffer.from(secretKey).toString("hex");
|
||||
@@ -44,6 +45,7 @@ export class OpenKeyService extends BaseService<OpenKeyEntity> {
|
||||
entity.keyId = keyId;
|
||||
entity.keySecret = keySecret;
|
||||
entity.scope = scope ?? "open";
|
||||
entity.remark = remark;
|
||||
await this.repository.save(entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user