mirror of
https://github.com/certd/certd.git
synced 2026-08-05 21:25:53 +08:00
perf: 新增AI插件开发受限令牌与隔离API
This commit is contained in:
@@ -43,7 +43,7 @@ Certd 插件按来源分为三类:
|
||||
|
||||
## API 认证
|
||||
|
||||
提示词会提供 Certd API 地址和当前用户 Token。调用 API 时使用:
|
||||
提示词会提供 Certd API 地址和仅限 AI 插件开发接口的受限 Token。调用 API 时使用:
|
||||
|
||||
```http
|
||||
Authorization: <token>
|
||||
@@ -57,7 +57,7 @@ Content-Type: application/json
|
||||
Node 请求须直接读取 UTF-8 文件或在 Node 内构造 JSON,并使用 `JSON.stringify`:
|
||||
|
||||
```javascript
|
||||
const response = await fetch(`${apiBase}/sys/plugin/find`, {
|
||||
const response = await fetch(`${apiBase}/scoped/sys/ai/plugin/find`, {
|
||||
method: "POST",
|
||||
headers: { Authorization: token, "Content-Type": "application/json; charset=utf-8" },
|
||||
body: JSON.stringify({ keywords: ["nginx"], includeBuiltIn: true, includeStore: true }),
|
||||
@@ -66,28 +66,28 @@ const response = await fetch(`${apiBase}/sys/plugin/find`, {
|
||||
|
||||
## UTF-8 保存
|
||||
|
||||
在 Windows 上,Node 直接以 UTF-8 读取 YAML 并用 `JSON.stringify` 发送;不要让 PowerShell 转发含中文的 YAML/JSON。保存后检查中文字段不含 `?`,再调用 `/sys/plugin/find` 或 `/sys/plugin/info` 验证。
|
||||
在 Windows 上,Node 直接以 UTF-8 读取 YAML 并用 `JSON.stringify` 发送;不要让 PowerShell 转发含中文的 YAML/JSON。保存后检查中文字段不含 `?`,再调用 `/scoped/sys/ai/plugin/find` 或 `/scoped/sys/ai/plugin/info` 验证。
|
||||
|
||||
## API 工作流
|
||||
|
||||
1. 使用 `/sys/plugin/find` 查询插件和 Access,可通过 `keywords` 数组传递多个关键词。
|
||||
1. 使用 `/scoped/sys/ai/plugin/find` 查询插件和 Access,可通过 `keywords` 数组传递多个关键词。
|
||||
2. 查询结果包含 `editable`:
|
||||
- `editable: true`:允许当前 Agent 修改并保存。
|
||||
- `editable: false`:只能读取和使用,不能修改。
|
||||
3. 读取完整 YAML 时调用 `/sys/plugin/export`。
|
||||
4. 所有插件保存统一调用 `/sys/plugin/import`,并始终传递完整 YAML:
|
||||
3. 读取完整 YAML 时调用 `/scoped/sys/ai/plugin/export`。
|
||||
4. 所有插件保存统一调用 `/scoped/sys/ai/plugin/import`,并始终传递完整 YAML:
|
||||
- 新插件使用 `override: false`。
|
||||
- 已有插件使用 `override: true`;导入接口根据 `author` 和 `name` 定位并覆盖已有记录。
|
||||
5. 不使用 `/sys/plugin/add` 或 `/sys/plugin/update` 保存插件,避免保存路径分叉、字段丢失和 Windows 请求兼容性问题。
|
||||
6. 保存完成后重新调用 `/sys/plugin/find` 或 `/sys/plugin/info` 验证结果。
|
||||
6. 保存完成后重新调用 `/scoped/sys/ai/plugin/find` 或 `/scoped/sys/ai/plugin/info` 验证结果。
|
||||
|
||||
`/sys/plugin/find` 会在一次请求中分别查询内置插件和 `store` 插件,再合并返回;`store` 插件需按上述字段区分市场插件与本地插件。
|
||||
`/scoped/sys/ai/plugin/find` 会在一次请求中分别查询内置插件和 `store` 插件,再合并返回;`store` 插件需按上述字段区分市场插件与本地插件。
|
||||
|
||||
详细请求字段见 `references/certd-api.md`。
|
||||
|
||||
## Access 协作
|
||||
|
||||
开发 Task 或 DNS Provider 前,先用 `/sys/plugin/find` 查询对应 Access:
|
||||
开发 Task 或 DNS Provider 前,先用 `/scoped/sys/ai/plugin/find` 查询对应 Access:
|
||||
|
||||
1. 如果没有对应 Access,先创建 Access 插件,再创建业务插件。
|
||||
2. 如果已有 Access,先读取它的完整 YAML 和 `content`。
|
||||
|
||||
@@ -4,9 +4,9 @@ Access 插件负责保存授权配置,也负责封装平台 API/SDK,供 Task
|
||||
|
||||
## 查询顺序
|
||||
|
||||
1. 调用 `/sys/plugin/find`,使用 `pluginType: access`。
|
||||
1. 调用 `/scoped/sys/ai/plugin/find`,使用 `pluginType: access`。
|
||||
2. 根据 `name`、`author`、`fullName` 识别目标 Access。
|
||||
3. 使用 `/sys/plugin/export` 读取完整 YAML。
|
||||
3. 使用 `/scoped/sys/ai/plugin/export` 读取完整 YAML。
|
||||
4. 检查 `content` 中已经提供的方法。
|
||||
|
||||
## 修改规则
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
## 查询插件
|
||||
|
||||
```http
|
||||
POST /sys/plugin/find
|
||||
POST /scoped/sys/ai/plugin/find
|
||||
Content-Type: application/json
|
||||
Authorization: <token>
|
||||
```
|
||||
@@ -28,19 +28,19 @@ Authorization: <token>
|
||||
- `type: "store"` 且没有 `appId`、`developerId`:本地插件。
|
||||
|
||||
结果中的 `editable` 是唯一的编辑权限依据;不能只按插件来源判断是否可修改。
|
||||
列表结果只返回插件基础信息,不返回 `content`、`setting`、`sysSetting`、`metadata` 或 `extra`。需要完整 YAML 时再调用 `/sys/plugin/export`。
|
||||
列表结果只返回插件基础信息,不返回 `content`、`setting`、`sysSetting`、`metadata` 或 `extra`。需要完整 YAML 时再调用 `/scoped/sys/ai/plugin/export`。
|
||||
|
||||
## 读取插件信息
|
||||
|
||||
```http
|
||||
POST /sys/plugin/info?id=12
|
||||
POST /scoped/sys/ai/plugin/info?id=12
|
||||
Authorization: <token>
|
||||
```
|
||||
|
||||
## 导出完整 YAML
|
||||
|
||||
```http
|
||||
POST /sys/plugin/export
|
||||
POST /scoped/sys/ai/plugin/export
|
||||
Content-Type: application/json
|
||||
Authorization: <token>
|
||||
```
|
||||
@@ -53,22 +53,10 @@ Authorization: <token>
|
||||
|
||||
## 保存插件
|
||||
|
||||
已有插件使用:
|
||||
使用完整 YAML 导入:
|
||||
|
||||
```http
|
||||
POST /sys/plugin/update
|
||||
```
|
||||
|
||||
新插件使用:
|
||||
|
||||
```http
|
||||
POST /sys/plugin/add
|
||||
```
|
||||
|
||||
也可以使用完整 YAML 导入:
|
||||
|
||||
```http
|
||||
POST /sys/plugin/import
|
||||
POST /scoped/sys/ai/plugin/import
|
||||
```
|
||||
|
||||
```json
|
||||
@@ -79,4 +67,4 @@ POST /sys/plugin/import
|
||||
}
|
||||
```
|
||||
|
||||
保存后重新调用 `/sys/plugin/find` 或 `/sys/plugin/info` 验证。
|
||||
保存后重新调用 `/scoped/sys/ai/plugin/find` 或 `/scoped/sys/ai/plugin/info` 验证。
|
||||
|
||||
@@ -22,4 +22,4 @@
|
||||
- `change.md` 只记录插件 ID、版本、时间和脱敏修改摘要。
|
||||
- 不保存 Token、证书、私钥、Cookie、环境变量和真实授权值。
|
||||
- 恢复历史版本前先备份当前 YAML。
|
||||
- 恢复后通过 `/sys/plugin/update` 或 `/sys/plugin/import` 写回 Certd。
|
||||
- 恢复后通过 `/scoped/sys/ai/plugin/import` 写回 Certd。
|
||||
|
||||
@@ -51,4 +51,4 @@ return class DemoTask extends AbstractTaskPlugin {
|
||||
- 用 `this.ctx.http` 请求远程 API,用 `this.getAccess` 获取授权。
|
||||
- 外部 API 返回失败或业务失败时抛出异常。
|
||||
- 对重复执行保持幂等,避免把真实 Token、证书和私钥写入日志。
|
||||
- 修改完成后把整个 YAML 通过 Certd `/sys/plugin/update` 或 `/sys/plugin/import` 保存。
|
||||
- 修改完成后把整个 YAML 通过 Certd `/scoped/sys/ai/plugin/import` 保存。
|
||||
|
||||
@@ -85,6 +85,7 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
|
||||
|
||||
- 务必写单元测试,覆盖主要业务逻辑。
|
||||
- 实现新功能或修复行为缺陷前,优先补单元测试并先确认红灯,再实现并跑聚焦验证。
|
||||
- 单元测试应优先直接测试原始业务方法,不要为了方便测试而抽取没有业务价值的 helper;数据库、RPC 和其他外部依赖可以使用 mock 隔离。
|
||||
- 确实不适合先写测试时,在回复中说明原因和替代验证方式。
|
||||
- 后补单元测试时,按正确行为写预期;若红灯需要修改既有实现,先向用户确认这是 bug 还是既有需求,避免未经确认改变行为。
|
||||
- 后端纯单测放在 `src/**/*.test.ts`,尽量与被测文件相邻;`test:unit` 只跑这些文件,构建/打包应排除 `*.test.ts`。
|
||||
|
||||
@@ -28,6 +28,14 @@ export async function FindPlugins(query: {
|
||||
});
|
||||
}
|
||||
|
||||
export async function GetScopedAccessToken(scoped: string[]): Promise<{ token: string; expire: number; scoped: string[] }> {
|
||||
return await request({
|
||||
url: "/sys/basic/getScopedAccessToken",
|
||||
method: "post",
|
||||
data: { scoped },
|
||||
});
|
||||
}
|
||||
|
||||
export async function AddObj(obj: any) {
|
||||
return await request({
|
||||
url: apiPrefix + "/add",
|
||||
|
||||
+9
-9
@@ -21,7 +21,7 @@
|
||||
<a-tag color="blue">API 模式</a-tag>
|
||||
</div>
|
||||
<a-textarea class="plugin-ai-dev__prompt-text" :value="prompt" readonly :rows="24" placeholder="生成后复制到 Codex 或 Trae 中运行。" />
|
||||
<div class="plugin-ai-dev__prompt-warning">该提示词包含 Certd 的访问 Token,请勿泄露给他人</div>
|
||||
<div class="plugin-ai-dev__prompt-warning">提示词使用受限且短时有效的 AI 开发 Token,可正常提供给可信 Agent,请勿公开或随意转发</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -31,7 +31,6 @@
|
||||
import { onMounted, ref } from "vue";
|
||||
import { message } from "ant-design-vue";
|
||||
import * as api from "../api";
|
||||
import { useUserStore } from "/@/store/user";
|
||||
import { env } from "/src/utils/util.env";
|
||||
|
||||
defineOptions({
|
||||
@@ -43,7 +42,6 @@ const props = defineProps<{
|
||||
pluginName?: string;
|
||||
}>();
|
||||
|
||||
const userStore = useUserStore();
|
||||
const requirement = ref("");
|
||||
const selectedPluginId = ref<number | string | undefined>(props.pluginId);
|
||||
const pluginOptions = ref<{ label: string; value: number | string }[]>([]);
|
||||
@@ -93,17 +91,17 @@ async function createPrompt() {
|
||||
}
|
||||
creating.value = true;
|
||||
try {
|
||||
prompt.value = buildPrompt();
|
||||
const accessToken = await api.GetScopedAccessToken(["sys/ai"]);
|
||||
prompt.value = buildPrompt(accessToken.token);
|
||||
message.success("启动提示词已生成");
|
||||
} finally {
|
||||
creating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function buildPrompt() {
|
||||
function buildPrompt(token: string) {
|
||||
const pluginLabel = props.pluginName || selectedPluginId.value;
|
||||
const pluginText = pluginLabel ? `当前插件:${pluginLabel}` : "当前为新插件开发";
|
||||
const token = userStore.getToken || "";
|
||||
const certdUrl = window.location.origin;
|
||||
const apiBase = new URL(env.API || "/api", certdUrl).toString().replace(/\/$/, "");
|
||||
return `你是 Certd 在线插件开发 Agent。
|
||||
@@ -119,21 +117,23 @@ ${certdUrl}
|
||||
Certd API 地址:
|
||||
${apiBase}
|
||||
|
||||
当前用户 Token:
|
||||
受限的 Certd AccessToken(仅能访问插件查询、编辑相关的几个接口):
|
||||
${token}
|
||||
|
||||
如果token过期,请向用户重新申请AccessToken. (让用户重新生成提示词,到里面复制上面这一串新AccessToken给你)
|
||||
|
||||
开发流程:
|
||||
1. 开始开发前,先检查当前工作目录是否已经是 certd 项目:应能看到 package.json、packages/ui/certd-server/src/plugins/、.trae/skills/ 等特征。
|
||||
2. 检查 .trae/skills/ 下是否已经有 certd-online-plugin-dev 技能。
|
||||
3. 如果当前目录不是 certd 项目,或缺少该技能,则先拉取 certd 仓库代码:优先 https://atomgit.com/certd/certd/,如果 AtomGit 拉取失败,再使用 https://github.com/certd/certd。
|
||||
4. 加载 .trae/skills/certd-online-plugin-dev/SKILL.md,并按插件类型加载对应子 Skill。
|
||||
5. 参考 certd 项目下已有内置插件 packages/ui/certd-server/src/plugins/ 的实现方式进行开发。
|
||||
6. 使用当前 Token 调用 Certd API,通过 /sys/plugin/find 查询插件和 Access。
|
||||
6. 使用受限 Token 调用 Certd API,只能访问 /scoped/sys/ai/plugin/ 前缀接口;通过 /scoped/sys/ai/plugin/find 查询插件和 Access。
|
||||
7. 开发 Task 或 DNS 插件前,先查询对应 Access,优先复用 Access 提供的 API/SDK 能力。
|
||||
8. 如果没有 Access,先创建 Access 插件;如果 Access 的 editable 为 true 且缺少能力,可以先修改 Access。
|
||||
9. 在当前工作区创建并使用 .tmp/online-plugin-dev 作为本次插件开发临时目录,历史记录、临时 YAML、脚本草稿和调试记录都放在该目录下。
|
||||
10. 修改任何插件前,先在 .tmp/online-plugin-dev/history 下保存完整 YAML 历史记录,便于恢复。
|
||||
11. 通过 Certd API 读取和保存完整插件 YAML,不使用 WebSocket,不依赖浏览器草稿。
|
||||
11. 读取完整 YAML 使用 /scoped/sys/ai/plugin/export,保存完整 YAML 使用 /scoped/sys/ai/plugin/import;不使用 WebSocket,不依赖浏览器草稿。
|
||||
12. 保存完成后向用户报告 API 操作结果,不自动发布。
|
||||
|
||||
认证请求要求:
|
||||
|
||||
@@ -44,7 +44,6 @@ process.on("uncaughtException", error => {
|
||||
// log()
|
||||
// }127.0.0.1
|
||||
|
||||
|
||||
// startHeapLog();
|
||||
@Configuration({
|
||||
detectorOptions: {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { ALL, Body, Controller, Inject, Post, Provide, Query } from "@midwayjs/core";
|
||||
import { BaseController } from "@certd/lib-server";
|
||||
import { PluginFindReq, PluginImportReq, PluginService } from "../../../modules/plugin/service/plugin-service.js";
|
||||
import { AuditType } from "../../../modules/sys/enterprise/service/audit-constants.js";
|
||||
|
||||
@Provide()
|
||||
@Controller("/api/scoped/sys/ai/plugin")
|
||||
export class AiPluginController extends BaseController {
|
||||
@Inject()
|
||||
service: PluginService;
|
||||
|
||||
getAuditType(): string {
|
||||
return AuditType.plugin.value;
|
||||
}
|
||||
|
||||
@Post("/find", { description: "sys:settings:view", summary: "AI 查询插件" })
|
||||
async find(@Body(ALL) body: PluginFindReq) {
|
||||
const res = await this.service.findPlugins(body || {});
|
||||
return this.ok(res);
|
||||
}
|
||||
|
||||
@Post("/info", { description: "sys:settings:view", summary: "AI 查询插件信息" })
|
||||
async info(@Query("id") id: number) {
|
||||
const res = await this.service.info(id);
|
||||
return this.ok(res);
|
||||
}
|
||||
|
||||
@Post("/export", { description: "sys:settings:view", summary: "AI 导出插件" })
|
||||
async export(@Body("id") id: number) {
|
||||
const res = await this.service.exportPlugin(id);
|
||||
return this.ok(res);
|
||||
}
|
||||
|
||||
@Post("/import", { description: "sys:settings:edit", summary: "AI 导入插件" })
|
||||
async import(@Body(ALL) body: PluginImportReq) {
|
||||
const res = await this.service.importPlugin(body);
|
||||
this.auditLog({ content: "AI 导入了插件配置" });
|
||||
return this.ok(res);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { ALL, Body, Controller, Inject, Post, Provide } from "@midwayjs/core";
|
||||
import { BaseController } from "@certd/lib-server";
|
||||
import { LoginService } from "../../../modules/login/service/login-service.js";
|
||||
import { AuditType } from "../../../modules/sys/enterprise/service/audit-constants.js";
|
||||
|
||||
const AI_PLUGIN_TOKEN_SCOPE = "sys/ai";
|
||||
|
||||
@Provide()
|
||||
@Controller("/api/sys/basic")
|
||||
export class SysBasicController extends BaseController {
|
||||
@Inject()
|
||||
loginService: LoginService;
|
||||
|
||||
getAuditType(): string {
|
||||
return AuditType.settings.value;
|
||||
}
|
||||
|
||||
@Post("/getScopedAccessToken", { description: "sys:settings:edit", summary: "获取 AI 插件开发访问令牌" })
|
||||
async getScopedAccessToken(@Body(ALL) body: { scoped?: string[] }) {
|
||||
const scoped = body?.scoped || [];
|
||||
if (!Array.isArray(scoped) || scoped.length !== 1 || scoped[0] !== AI_PLUGIN_TOKEN_SCOPE) {
|
||||
throw new Error(`仅支持申请 ${AI_PLUGIN_TOKEN_SCOPE} 范围的访问令牌`);
|
||||
}
|
||||
const user = this.ctx.user;
|
||||
if (!user?.id || !user.username || !Array.isArray(user.roles)) {
|
||||
throw new Error("当前登录令牌不支持申请受限访问令牌");
|
||||
}
|
||||
const res = await this.loginService.generateScopedAccessToken(user, scoped);
|
||||
this.auditLog({ content: "获取了 AI 插件开发受限访问令牌" });
|
||||
return this.ok(res);
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@ export class PluginController extends CrudController<PluginService> {
|
||||
};
|
||||
merge(bean, def);
|
||||
bean.fullName = bean.name;
|
||||
if (bean.author){
|
||||
if (bean.author) {
|
||||
bean.fullName = bean.author + "/" + bean.name;
|
||||
}
|
||||
const res = await super.add(bean);
|
||||
@@ -189,5 +189,4 @@ export class PluginController extends CrudController<PluginService> {
|
||||
const res = await this.service.exportPlugin(id);
|
||||
return this.ok(res);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -55,3 +55,36 @@ describe("AuthorityMiddleware guestOptionalAuth", () => {
|
||||
assert.deepEqual(ctx.user.roles, [1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("AuthorityMiddleware scoped token", () => {
|
||||
it("rejects a scoped token outside its API prefix", async () => {
|
||||
const middleware = createMiddleware(Constants.per.authOnly);
|
||||
const ctx = createCtx();
|
||||
ctx.path = "/api/sys/plugin/find";
|
||||
const token = jwt.sign({ id: 1, roles: [1], scoped: ["sys/ai"] }, middleware.secret);
|
||||
ctx.get = (name: string) => (name === "Authorization" ? `Bearer ${token}` : "");
|
||||
let called = false;
|
||||
|
||||
await middleware.resolve()(ctx, async () => {
|
||||
called = true;
|
||||
});
|
||||
|
||||
assert.equal(called, false);
|
||||
assert.equal(ctx.status, 403);
|
||||
});
|
||||
|
||||
it("allows a scoped token within its API prefix", async () => {
|
||||
const middleware = createMiddleware(Constants.per.authOnly);
|
||||
const ctx = createCtx();
|
||||
ctx.path = "/api/scoped/sys/ai/plugin/find";
|
||||
const token = jwt.sign({ id: 1, roles: [1], scoped: ["sys/ai"] }, middleware.secret);
|
||||
ctx.get = (name: string) => (name === "Authorization" ? `Bearer ${token}` : "");
|
||||
let called = false;
|
||||
|
||||
await middleware.resolve()(ctx, async () => {
|
||||
called = true;
|
||||
});
|
||||
|
||||
assert.equal(called, true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -63,6 +63,9 @@ export class AuthorityMiddleware implements IWebMiddleware {
|
||||
logger.error("token verify error: ", err);
|
||||
return this.notAuth(ctx);
|
||||
}
|
||||
if (!this.isScopedTokenPathAllowed(ctx)) {
|
||||
return this.notScoped(ctx);
|
||||
}
|
||||
} else {
|
||||
if (permission === Constants.per.guestOptionalAuth) {
|
||||
await next();
|
||||
@@ -111,6 +114,35 @@ export class AuthorityMiddleware implements IWebMiddleware {
|
||||
return;
|
||||
}
|
||||
|
||||
private notScoped(ctx: IMidwayKoaContext) {
|
||||
ctx.status = 403;
|
||||
ctx.body = Constants.res.permission;
|
||||
return;
|
||||
}
|
||||
|
||||
private isScopedTokenPathAllowed(ctx: IMidwayKoaContext) {
|
||||
const user = ctx.user as { scoped?: unknown } | undefined;
|
||||
if (!user || !("scoped" in user)) {
|
||||
return true;
|
||||
}
|
||||
if (!Array.isArray(user.scoped) || user.scoped.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const requestPath = ctx.path.replace(/^\/+|\/+$/g, "");
|
||||
return user.scoped.some(scope => {
|
||||
if (typeof scope !== "string") {
|
||||
return false;
|
||||
}
|
||||
const normalizedScope = scope.trim().replace(/^\/+|\/+$/g, "");
|
||||
if (!/^[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)*$/.test(normalizedScope)) {
|
||||
return false;
|
||||
}
|
||||
const scopedPrefix = `api/scoped/${normalizedScope}`;
|
||||
return requestPath === scopedPrefix || requestPath.startsWith(`${scopedPrefix}/`);
|
||||
});
|
||||
}
|
||||
|
||||
private extractProjectId(ctx: IMidwayKoaContext) {
|
||||
const headerVal = ctx.headers["project-id"] as string;
|
||||
const queryVal = (ctx.request as any)?.query?.projectId;
|
||||
|
||||
@@ -3,3 +3,5 @@ export { CronConfiguration as Configuration } from "./configuration.js";
|
||||
// export * from './controller/user';
|
||||
// export * from './controller/api';
|
||||
// export * from './service/user';
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import assert from "node:assert/strict";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { LoginService } from "./login-service.js";
|
||||
|
||||
function createLoginService() {
|
||||
@@ -76,3 +77,28 @@ describe("LoginService.register", () => {
|
||||
assert.equal(calls.bindInvitee.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("LoginService.generateScopedAccessToken", () => {
|
||||
it("adds the requested scopes to a short-lived JWT", async () => {
|
||||
const service = new LoginService();
|
||||
(service as any).jwt = { expire: 7200 };
|
||||
service.sysSettingsService = {
|
||||
async getSetting() {
|
||||
return { jwtKey: "test-secret" };
|
||||
},
|
||||
} as any;
|
||||
|
||||
const result = await service.generateScopedAccessToken(
|
||||
{
|
||||
id: 1,
|
||||
username: "admin",
|
||||
roles: [1],
|
||||
},
|
||||
["sys/ai"]
|
||||
);
|
||||
const payload = jwt.verify(result.token, "test-secret") as jwt.JwtPayload;
|
||||
|
||||
assert.deepEqual(payload.scoped, ["sys/ai"]);
|
||||
assert.equal(result.expire, 3600);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -256,6 +256,33 @@ export class LoginService {
|
||||
};
|
||||
}
|
||||
|
||||
async generateScopedAccessToken(user: { id: number; username: string; roles: number[] }, scoped: string[]) {
|
||||
const normalizedScopes = [...new Set((scoped || []).map(item => `${item || ""}`.trim()).filter(Boolean))];
|
||||
if (normalizedScopes.length === 0) {
|
||||
throw new CommonException("scoped不能为空");
|
||||
}
|
||||
const setting = await this.sysSettingsService.getSetting<SysPrivateSettings>(SysPrivateSettings);
|
||||
const expire = Math.min(this.jwt.expire, 6 * 60 * 60);
|
||||
const token = jwt.sign(
|
||||
{
|
||||
username: user.username,
|
||||
id: user.id,
|
||||
roles: user.roles,
|
||||
scoped: normalizedScopes,
|
||||
},
|
||||
setting.jwtKey,
|
||||
{ expiresIn: expire }
|
||||
);
|
||||
|
||||
return {
|
||||
token,
|
||||
expire,
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
scoped: normalizedScopes,
|
||||
};
|
||||
}
|
||||
|
||||
async loginByOpenId(req: { openId: string; type: string }) {
|
||||
const { openId, type } = req;
|
||||
const oauthBound = await this.oauthBoundService.findOne({
|
||||
|
||||
@@ -24,6 +24,7 @@ export type CloudflareRecord = {
|
||||
icon: "simple-icons:cloudflare",
|
||||
// 这里是对应的 cloudflare的access类型名称
|
||||
accessType: "cloudflare",
|
||||
order: 1,
|
||||
})
|
||||
export class CloudflareDnsProvider extends AbstractDnsProvider<CloudflareRecord> {
|
||||
access!: CloudflareAccess;
|
||||
|
||||
@@ -19,7 +19,7 @@ export type GoogleCloudDnsRecord = {
|
||||
desc: "Google Cloud DNS提供商",
|
||||
icon: "flat-color-icons:google",
|
||||
accessType: "google",
|
||||
order: 50,
|
||||
order: 2,
|
||||
})
|
||||
export class GoogleCloudDnsProvider extends AbstractDnsProvider<GoogleCloudDnsRecord> {
|
||||
access!: GoogleAccess;
|
||||
|
||||
@@ -15,6 +15,7 @@ export type SearchRecordOptions = {
|
||||
desc: "华为云DNS解析提供商",
|
||||
accessType: "huawei",
|
||||
icon: "svg:icon-huawei",
|
||||
order: 1,
|
||||
})
|
||||
export class HuaweiDnsProvider extends AbstractDnsProvider {
|
||||
client!: HuaweiYunClient;
|
||||
|
||||
+1
@@ -8,6 +8,7 @@ const tencentDnsProviderDefine: any = {
|
||||
desc: "腾讯云域名DNS解析提供者",
|
||||
accessType: "tencent",
|
||||
icon: "svg:icon-tencentcloud",
|
||||
order: 0,
|
||||
dependPlugins: {
|
||||
"access:tencent": "*",
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ import { TencentAccess } from "../../plugin-lib/tencent/access.js";
|
||||
dependPlugins: {
|
||||
"access:tencent": "*",
|
||||
},
|
||||
order: 1,
|
||||
})
|
||||
export class TencentEoDnsProvider extends AbstractDnsProvider {
|
||||
access!: TencentAccess;
|
||||
|
||||
@@ -10,7 +10,7 @@ import { PageSearch } from "@certd/pipeline";
|
||||
desc: "火山引擎DNS解析提供商",
|
||||
accessType: "volcengine",
|
||||
icon: "svg:icon-volcengine",
|
||||
order: 2,
|
||||
order: 1,
|
||||
})
|
||||
export class VolcengineDnsProvider extends AbstractDnsProvider {
|
||||
client: VolcengineDnsClient;
|
||||
|
||||
@@ -10,7 +10,7 @@ type westRecord = {
|
||||
record_id: number;
|
||||
};
|
||||
};
|
||||
|
||||
// 这个别删,在WestDnsProvider有使用
|
||||
export class WestDnsProviderDomain extends AbstractDnsProvider<westRecord> {
|
||||
access!: WestAccess;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user