pull/330/head
xiaojunnuo 2025-01-19 00:33:34 +08:00
parent 93b37a89c9
commit e79703e49b
9 changed files with 156 additions and 106 deletions

View File

@ -9,6 +9,10 @@
display: inline-flex; display: inline-flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
.fs-iconify{
font-size:18px;
margin-right: 5px;
}
} }
.cd-icon-button { .cd-icon-button {

View File

@ -1,55 +1,53 @@
import { request } from "/src/api/service"; import { request } from "/src/api/service";
export function createApi() { export const OPEN_API_DOC = "https://apifox.com/apidoc/shared-2e76f8c4-7c58-413b-a32d-a1316529af44/254949529e0";
const apiPrefix = "/open/key";
return {
async GetList(query: any) {
return await request({
url: apiPrefix + "/page",
method: "post",
data: query
});
},
async AddObj(obj: any) { const apiPrefix = "/open/key";
return await request({ export const openkeyApi = {
url: apiPrefix + "/add", async GetList(query: any) {
method: "post", return await request({
data: obj url: apiPrefix + "/page",
}); method: "post",
}, data: query
});
},
async UpdateObj(obj: any) { async AddObj(obj: any) {
return await request({ return await request({
url: apiPrefix + "/update", url: apiPrefix + "/add",
method: "post", method: "post",
data: obj data: obj
}); });
}, },
async DelObj(id: number) { async UpdateObj(obj: any) {
return await request({ return await request({
url: apiPrefix + "/delete", url: apiPrefix + "/update",
method: "post", method: "post",
params: { id } data: obj
}); });
}, },
async GetObj(id: number) { async DelObj(id: number) {
return await request({ return await request({
url: apiPrefix + "/info", url: apiPrefix + "/delete",
method: "post", method: "post",
params: { id } params: { id }
}); });
}, },
async GetApiToken(id: number) {
return await request({
url: apiPrefix + "/getApiToken",
method: "post",
data: { id }
});
}
};
}
export const pipelineGroupApi = createApi(); async GetObj(id: number) {
return await request({
url: apiPrefix + "/info",
method: "post",
params: { id }
});
},
async GetApiToken(id: number) {
return await request({
url: apiPrefix + "/getApiToken",
method: "post",
data: { id }
});
}
};

View File

@ -1,15 +1,12 @@
// @ts-ignore // @ts-ignore
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud"; import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { pipelineGroupApi } from "./api"; import { OPEN_API_DOC, openkeyApi } from "./api";
import dayjs from "dayjs";
import { Modal } from "ant-design-vue";
import CertView from "/@/views/certd/pipeline/cert-view.vue";
import { useModal } from "/@/use/use-modal"; import { useModal } from "/@/use/use-modal";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet { export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { t } = useI18n(); const { t } = useI18n();
const api = pipelineGroupApi; const api = openkeyApi;
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => { const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query); return await api.GetList(query);
}; };
@ -59,17 +56,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
actionbar: { actionbar: {
buttons: { buttons: {
add: { add: {
text: "生成新的Key", text: "生成新的Key"
click() {
Modal.confirm({
title: "确认",
content: "确定要生成新的Key?",
async onOk() {
await api.AddObj({});
await crudExpose.doRefresh();
}
});
}
} }
} }
}, },
@ -82,7 +69,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
edit: { show: false }, edit: { show: false },
remove: { show: true }, remove: { show: true },
gen: { gen: {
text: "测试ApiToken", text: " 接口测试",
size: "mini",
icon: "devicon-plain:vitest",
type: "primary",
async click({ row }) { async click({ row }) {
const apiToken = await api.GetApiToken(row.id); const apiToken = await api.GetApiToken(row.id);
@ -94,7 +84,13 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
content: () => { content: () => {
return ( return (
<div> <div>
<div class={"m-10 p-10"}>ApiKey3使</div> <div class={"m-10 p-10"}>
ApiKey3使
<a href={OPEN_API_DOC} target={"_blank"}>
</a>
</div>
<div class={"m-10 p-10"} style={{ border: "1px solid #333" }}> <div class={"m-10 p-10"} style={{ border: "1px solid #333" }}>
<fs-copyable model-value={apiToken}></fs-copyable> <fs-copyable model-value={apiToken}></fs-copyable>
</div> </div>
@ -126,26 +122,50 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
}, },
keyId: { keyId: {
title: "KeyId", title: "KeyId",
type: ["text", "copyable"],
search: { search: {
show: false show: false
}, },
form: { form: {
show: false show: false
}, },
type: "text",
column: { column: {
width: 200, width: 250,
sorter: true sorter: true
} }
}, },
keySecret: { keySecret: {
title: "KeySecret", title: "KeySecret",
type: "text", type: ["text", "copyable"],
form: { form: {
show: false show: false
}, },
column: { column: {
width: 550, width: 580,
sorter: true
}
},
scope: {
title: "权限范围",
type: "dict-radio",
dict: dict({
data: [
{ label: "仅开放接口", value: "open", color: "blue" },
{ label: "账户所有权限", value: "user", color: "red" }
]
}),
form: {
value: "open",
show: true,
rules: [{ required: true, message: "此项必填" }],
helper: "仅开放接口只可以访问开放接口,账户所有权限可以访问所有接口",
component: {
vModel: "value"
}
},
column: {
width: 120,
align: "center",
sorter: true sorter: true
} }
}, },

View File

@ -2,16 +2,20 @@
<fs-page> <fs-page>
<template #header> <template #header>
<div class="title">开放接口密钥管理</div> <div class="title">开放接口密钥管理</div>
<div class="more">
<a :href="OPEN_API_DOC" target="_blank">开放接口文档</a>
</div>
</template> </template>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud> <fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
</fs-page> </fs-page>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { defineComponent, onActivated, onMounted } from "vue"; import { onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud"; import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud"; import createCrudOptions from "./crud";
import { createApi } from "./api"; import { OPEN_API_DOC } from "/@/views/certd/open/openkey/api";
defineOptions({ defineOptions({
name: "OpenKey" name: "OpenKey"
}); });

View File

@ -4,6 +4,7 @@ CREATE TABLE "cd_open_key"
"user_id" integer, "user_id" integer,
"key_id" varchar(50), "key_id" varchar(50),
"key_secret" varchar(100), "key_secret" varchar(100),
"scope" varchar(50),
"disabled" boolean NOT NULL DEFAULT (false), "disabled" boolean NOT NULL DEFAULT (false),
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP) "update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)

View File

@ -37,10 +37,9 @@ export class OpenKeyController extends CrudController<OpenKeyService> {
} }
@Post('/add', { summary: Constants.per.authOnly }) @Post('/add', { summary: Constants.per.authOnly })
async add() { async add(@Body(ALL) body: any) {
const bean: any = {}; body.userId = this.getUserId();
bean.userId = this.getUserId(); const res = await this.service.add(body);
const res = await this.service.add(bean);
return this.ok(res); return this.ok(res);
} }

View File

@ -4,8 +4,8 @@ import jwt from 'jsonwebtoken';
import { Constants, SysPrivateSettings, SysSettingsService } from '@certd/lib-server'; import { Constants, SysPrivateSettings, SysSettingsService } from '@certd/lib-server';
import { logger } from '@certd/basic'; import { logger } from '@certd/basic';
import { AuthService } from '../modules/sys/authority/service/auth-service.js'; import { AuthService } from '../modules/sys/authority/service/auth-service.js';
import { Next } from 'koa';
import { OpenKeyService } from '../modules/open/service/open-key-service.js'; import { OpenKeyService } from '../modules/open/service/open-key-service.js';
import { RoleService } from '../modules/sys/authority/service/role-service.js';
/** /**
* *
@ -18,6 +18,8 @@ export class AuthorityMiddleware implements IWebMiddleware {
@Inject() @Inject()
authService: AuthService; authService: AuthService;
@Inject() @Inject()
roleService: RoleService;
@Inject()
openKeyService: OpenKeyService; openKeyService: OpenKeyService;
@Inject() @Inject()
sysSettingsService: SysSettingsService; sysSettingsService: SysSettingsService;
@ -50,10 +52,6 @@ export class AuthorityMiddleware implements IWebMiddleware {
return; return;
} }
if (permission === Constants.per.open) {
return this.doOpenHandler(ctx, next);
}
let token = ctx.get('Authorization') || ''; let token = ctx.get('Authorization') || '';
token = token.replace('Bearer ', '').trim(); token = token.replace('Bearer ', '').trim();
if (!token) { if (!token) {
@ -64,41 +62,61 @@ export class AuthorityMiddleware implements IWebMiddleware {
//尝试从query中获取token //尝试从query中获取token
token = (ctx.query.token as string) || ''; token = (ctx.query.token as string) || '';
} }
try {
ctx.user = jwt.verify(token, this.secret); if (token) {
} catch (err) { try {
logger.error('token verify error: ', err); ctx.user = jwt.verify(token, this.secret);
ctx.status = 401; } catch (err) {
ctx.body = Constants.res.auth; logger.error('token verify error: ', err);
return this.notAuth(ctx);
}
} else {
//找找openKey
const openKey = await this.doOpenHandler(ctx);
if (!openKey) {
return this.notAuth(ctx);
}
if (permission === Constants.per.open) {
await next();
return;
} else if (openKey.scope === 'open') {
return this.notAuth(ctx);
}
}
if (permission === Constants.per.authOnly) {
await next();
return; return;
} }
if (permission !== Constants.per.authOnly) { const pass = await this.authService.checkPermission(ctx, permission);
const pass = await this.authService.checkPermission(ctx, permission); if (!pass) {
if (!pass) { logger.info('not permission: ', ctx.req.url);
logger.info('not permission: ', ctx.req.url); ctx.status = 401;
ctx.status = 401; ctx.body = Constants.res.permission;
ctx.body = Constants.res.permission; return;
return;
}
} }
await next();
}; };
} }
async doOpenHandler(ctx: IMidwayKoaContext, next: Next) { private notAuth(ctx: IMidwayKoaContext) {
ctx.status = 401;
ctx.body = Constants.res.auth;
return;
}
async doOpenHandler(ctx: IMidwayKoaContext) {
//开放接口 //开放接口
const openKey = ctx.get('x-api-token') || ''; const openKey = ctx.get('x-certd-token') || '';
if (!openKey) { if (!openKey) {
ctx.status = 401; return null;
ctx.body = Constants.res.auth;
return;
} }
//校验 openKey //校验 openKey
const openKeyRes = await this.openKeyService.verifyOpenKey(openKey); const openKeyRes = await this.openKeyService.verifyOpenKey(openKey);
ctx.user = { id: openKeyRes.userId }; const roles = await this.roleService.getRoleIdsByUserId(openKeyRes.userId);
ctx.user = { id: openKeyRes.userId, roles };
ctx.openKey = openKeyRes; ctx.openKey = openKeyRes;
await next(); return openKeyRes;
} }
} }

View File

@ -14,6 +14,9 @@ export class OpenKeyEntity {
@Column({ name: 'key_secret', comment: 'keySecret' }) @Column({ name: 'key_secret', comment: 'keySecret' })
keySecret: string; keySecret: string;
@Column({ name: 'scope', comment: '权限范围' })
scope: string; // open 仅开放接口、 user 用户所有权限
@Column({ name: 'create_time', comment: '创建时间', default: () => 'CURRENT_TIMESTAMP' }) @Column({ name: 'create_time', comment: '创建时间', default: () => 'CURRENT_TIMESTAMP' })
createTime: Date; createTime: Date;

View File

@ -12,6 +12,7 @@ export type OpenKey = {
keyId: string; keyId: string;
keySecret: string; keySecret: string;
encrypt: boolean; encrypt: boolean;
scope: string;
}; };
@Provide() @Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true }) @Scope(ScopeEnum.Request, { allowDowngrade: true })
@ -29,10 +30,10 @@ export class OpenKeyService extends BaseService<OpenKeyEntity> {
} }
async add(bean: OpenKeyEntity) { async add(bean: OpenKeyEntity) {
return await this.generate(bean.userId); return await this.generate(bean.userId, bean.scope);
} }
async generate(userId: number) { async generate(userId: number, scope: string) {
const keyId = utils.id.simpleNanoId(18) + '_key'; const keyId = utils.id.simpleNanoId(18) + '_key';
const secretKey = crypto.randomBytes(32); const secretKey = crypto.randomBytes(32);
const keySecret = Buffer.from(secretKey).toString('hex'); const keySecret = Buffer.from(secretKey).toString('hex');
@ -40,6 +41,7 @@ export class OpenKeyService extends BaseService<OpenKeyEntity> {
entity.userId = userId; entity.userId = userId;
entity.keyId = keyId; entity.keyId = keyId;
entity.keySecret = keySecret; entity.keySecret = keySecret;
entity.scope = scope ?? 'open';
await this.repository.save(entity); await this.repository.save(entity);
return entity; return entity;
} }
@ -84,6 +86,7 @@ export class OpenKeyService extends BaseService<OpenKeyEntity> {
keyId: entity.keyId, keyId: entity.keyId,
keySecret: entity.keySecret, keySecret: entity.keySecret,
encrypt: encrypt, encrypt: encrypt,
scope: entity.scope,
}; };
} }