add permission check

This commit is contained in:
xion 2025-04-06 01:43:31 +08:00
parent 2ff8590ceb
commit bfe8463212
8 changed files with 214 additions and 15 deletions

View File

@ -27,6 +27,8 @@
"packageManager": "pnpm@10.7.1",
"type": "module",
"dependencies": {
"@kevisual/cache": "^0.0.2",
"@kevisual/permission": "^0.0.1",
"@kevisual/router": "0.0.10"
},
"devDependencies": {

31
pnpm-lock.yaml generated
View File

@ -8,6 +8,12 @@ importers:
.:
dependencies:
'@kevisual/cache':
specifier: ^0.0.2
version: 0.0.2(rollup@4.39.0)(tslib@2.8.1)(typescript@5.8.2)
'@kevisual/permission':
specifier: ^0.0.1
version: 0.0.1
'@kevisual/router':
specifier: 0.0.10
version: 0.0.10
@ -398,6 +404,9 @@ packages:
'@kevisual/auth@1.0.5':
resolution: {integrity: sha512-GwsLj7unKXi7lmMiIIgdig4LwwLiDJnOy15HHZR5gMbyK6s5/uJiMY5RXPB2+onGzTNDqFo/hXjsD2wkerHPVg==}
'@kevisual/cache@0.0.2':
resolution: {integrity: sha512-2Cl5KF2Gi27uLfhO6CdTMFnRzx9vYnqevAo7d9ab3rOaqTgF8tLeAXglXyRbaWW3WUbHU2XaOb4r98uUsqIQQw==}
'@kevisual/code-center-module@0.0.18':
resolution: {integrity: sha512-BfANmxLEO1AwVmqpa6VDgxk//YN8asf1r5jIPpyKDQm12kyyrYgHND9AgGCDRH8lvq6rYVe0svCZXD5b06UPWQ==}
peerDependencies:
@ -414,6 +423,9 @@ packages:
'@kevisual/mark@0.0.7':
resolution: {integrity: sha512-PiEEy4yvWEpixw76PzgrIWeNelzm+FrhtzFmqJU92o5GkgawaFwighcvIxqcVZRKeEFF4uvlTjFrGeQvXw6F4A==}
'@kevisual/permission@0.0.1':
resolution: {integrity: sha512-nSX2LzbPkU3YAMegbUFGU8tfmtFb7dcF5edqzm+gI6crcyCL1JzIB9HAYNEeEVIljLxuREwM/vVg9aFmF4cz9Q==}
'@kevisual/query@0.0.13':
resolution: {integrity: sha512-gSEIDiCvwSaLLAFZv4vam4wSrMsaCuQ3VGjE3kwRwZ8urlVH1TOA+NUO908A22p9m1Iij7Y1Q/JlfSJi2QzuKQ==}
@ -1747,6 +1759,9 @@ packages:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'}
idb-keyval@6.2.1:
resolution: {integrity: sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==}
ignore-by-default@1.0.1:
resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==}
@ -3347,6 +3362,18 @@ snapshots:
'@kevisual/auth@1.0.5': {}
'@kevisual/cache@0.0.2(rollup@4.39.0)(tslib@2.8.1)(typescript@5.8.2)':
dependencies:
'@rollup/plugin-commonjs': 28.0.3(rollup@4.39.0)
'@rollup/plugin-node-resolve': 16.0.1(rollup@4.39.0)
'@rollup/plugin-typescript': 12.1.2(rollup@4.39.0)(tslib@2.8.1)(typescript@5.8.2)
idb-keyval: 6.2.1
rollup-plugin-dts: 6.2.1(rollup@4.39.0)(typescript@5.8.2)
transitivePeerDependencies:
- rollup
- tslib
- typescript
'@kevisual/code-center-module@0.0.18(@kevisual/auth@1.0.5)(@kevisual/router@0.0.10)(@kevisual/use-config@1.0.10(dotenv@16.4.7))(ioredis@5.6.0)(pg@8.14.1)(sequelize@6.37.7(pg-hstore@2.3.4)(pg@8.14.1))':
dependencies:
'@kevisual/auth': 1.0.5
@ -3393,6 +3420,8 @@ snapshots:
- tedious
- utf-8-validate
'@kevisual/permission@0.0.1': {}
'@kevisual/query@0.0.13(ws@8.18.1)(zod@3.24.2)':
dependencies:
openai: 4.91.1(ws@8.18.1)(zod@3.24.2)
@ -4882,6 +4911,8 @@ snapshots:
dependencies:
safer-buffer: 2.1.2
idb-keyval@6.2.1: {}
ignore-by-default@1.0.1: {}
ignore@5.3.2: {}

View File

@ -1,3 +1,4 @@
import { Permission } from '@kevisual/permission';
import CryptoJS from 'crypto-js';
// 加密函数
@ -73,6 +74,7 @@ export type AIConfig = {
description?: string;
models: AIModel[];
secretKeys: SecretKey[];
permission?: Permission;
filter?: {
objectKey: string;
type: 'array' | 'object';

View File

@ -2,7 +2,7 @@ import { app } from '@/app.ts';
import { ChatServices } from './services/chat-services.ts';
import { ChatConfigServices } from './services/chat-config-srevices.ts';
import { AiChatHistoryModel } from './models/ai-chat-history.ts';
import { UserPermission } from '@kevisual/permission';
app
.route({
path: 'ai',
@ -21,19 +21,20 @@ app
if (!aiChatHistory) {
ctx.throw(400, 'aiChatHistory not found');
}
if (aiChatHistory.uid !== tokenUser.uid) {
if (aiChatHistory.uid !== tokenUser.id) {
ctx.throw(403, 'not permission');
}
username = username || aiChatHistory.username;
username = username || aiChatHistory.username || tokenUsername;
model = model || aiChatHistory.model;
group = group || aiChatHistory.group;
} else {
username = username || tokenUsername;
}
const isSelf = username === tokenUsername;
if (!Array.isArray(messages)) {
ctx.throw(400, 'chat messages is not array');
}
// 初始化服务
const chatServices = await ChatServices.createServices({
owner: username,
@ -41,6 +42,15 @@ app
group,
username: tokenUsername,
});
if (!isSelf && username !== 'root') {
const aiConfig = chatServices.aiConfig;
const permission = new UserPermission({ permission: aiConfig.permission, owner: username });
const checkPermission = permission.checkPermissionSuccess({ username: tokenUsername, password: options.password });
if (!checkPermission.success) {
ctx.throw(403, checkPermission.message);
}
}
const chatConfigServices = new ChatConfigServices(username, tokenUsername);
await chatConfigServices.checkUserCanChat(tokenUsername);
await chatServices.checkCanChat();
@ -72,6 +82,10 @@ app
prompt_tokens: aiChatHistory.prompt_tokens + usage.prompt_tokens,
completion_tokens: aiChatHistory.completion_tokens + usage.completion_tokens,
total_tokens: aiChatHistory.total_tokens + usage.total_tokens,
version: aiChatHistory.version + 1,
model: model,
group: group,
username: username,
};
if (hook) {
needUpdateData.data = {
@ -126,19 +140,46 @@ app
.define(async (ctx) => {
const username = ctx.query.username || 'root';
const tokenUser = ctx.state.tokenUser;
const usernames = ctx.query.data?.usernames || [];
const tokenUsername = tokenUser.username;
const isSameUser = username === tokenUser.username;
const configObject: Record<string, any> = {};
const configArray: any[] = [];
const services = new ChatConfigServices(username, tokenUser.username);
const res = await services.getChatConfig(true, ctx.query.token);
configObject[username] = res;
configArray.push({
username,
config: res,
});
if (!isSameUser) {
const selfServices = new ChatConfigServices(tokenUser.username, tokenUser.username);
const selfRes = await selfServices.getChatConfig(true, ctx.query.token);
configObject['self'] = selfRes;
} else {
configObject['self'] = res;
configArray.push({
username: tokenUser.username,
self: true,
config: selfRes,
});
}
ctx.body = configObject;
for (const username of usernames) {
const services = new ChatConfigServices(username, tokenUser.username);
const res = await services.getChatConfig(true, ctx.query.token);
const aiConfig = services.aiConfig;
const permission = new UserPermission({ permission: aiConfig.permission, owner: username });
const checkPermission = permission.checkPermissionSuccess({ username: tokenUsername, password: '-----------------' });
if (!checkPermission.success) {
// ctx.throw(403, `[${username}] ${checkPermission.message}`);
configArray.push({
username,
config: null,
error: checkPermission.message,
});
} else {
configArray.push({
username,
config: res,
});
}
}
ctx.body = configArray;
})
.addTo(app);
@ -146,7 +187,7 @@ app
.route({
path: 'ai',
key: 'get-chat-usage',
description: '获取chat使用情况',
description: '获取chat使用情况 只获取root的使用情况',
middleware: ['auth'],
})
.define(async (ctx) => {

View File

@ -12,12 +12,121 @@ app
const tokenUser = ctx.state.tokenUser;
const aiChatList = await AiChatHistoryModel.findAll({
where: {
uid: tokenUser.uid,
uid: tokenUser.id,
},
order: [['createdAt', 'DESC']],
order: [['updatedAt', 'DESC']],
});
ctx.body = {
list: aiChatList,
};
})
.addTo(app);
app
.route({
path: 'ai',
key: 'update-chat',
description: '更新chat',
middleware: ['auth'],
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const uid = tokenUser.id;
const { id, data, prompt_tokens, total_tokens, completion_tokens, createdAt, updatedAt, ...rest } = ctx.query.data || {};
let aiChat: AiChatHistoryModel | null = null;
if (id) {
aiChat = await AiChatHistoryModel.findByPk(id);
if (!aiChat) {
ctx.throw(404, 'chat not found');
}
if (aiChat.uid !== uid) {
ctx.throw(403, 'no permission');
}
await aiChat.update({ data: { ...aiChat.data, ...data }, ...rest, version: aiChat.version + 1 });
} else {
aiChat = await AiChatHistoryModel.create({
...rest,
uid: uid,
});
}
ctx.body = aiChat;
})
.addTo(app);
app
.route({
path: 'ai',
key: 'get-chat',
description: '获取chat',
middleware: ['auth'],
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const { id } = ctx.query.data || {};
if (!id) {
ctx.throw(400, 'id is required');
}
const aiChat = await AiChatHistoryModel.findByPk(id);
if (!aiChat) {
ctx.throw(404, 'chat not found');
}
if (aiChat.uid !== tokenUser.id) {
ctx.throw(403, 'no permission');
}
ctx.body = aiChat;
})
.addTo(app);
app
.route({
path: 'ai',
key: 'get-chat-version',
description: '获取chat版本',
middleware: ['auth'],
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const { id } = ctx.query.data || {};
if (!id) {
ctx.throw(400, 'id is required');
}
const aiChat = await AiChatHistoryModel.findByPk(id);
if (!aiChat) {
ctx.throw(404, 'chat not found');
}
if (aiChat.uid !== tokenUser.id) {
ctx.throw(403, 'no permission');
}
ctx.body = {
id: aiChat.id,
version: aiChat.version,
};
})
.addTo(app);
app
.route({
path: 'ai',
key: 'delete-chat',
description: '删除chat',
middleware: ['auth'],
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const { id } = ctx.query.data || {};
if (!id) {
ctx.throw(400, 'id is required');
}
const aiChat = await AiChatHistoryModel.findByPk(id);
if (!aiChat) {
ctx.throw(404, 'chat not found');
}
if (aiChat.uid !== tokenUser.id) {
ctx.throw(403, 'no permission');
}
await aiChat.destroy();
ctx.body = {
success: true,
};
})
.addTo(app);

View File

@ -33,6 +33,8 @@ export class AiChatHistoryModel extends Model {
declare total_tokens: number;
declare completion_tokens: number;
declare version: number;
declare createdAt: Date;
declare updatedAt: Date;
}
@ -47,14 +49,17 @@ AiChatHistoryModel.init(
username: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: '',
},
model: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: '',
},
group: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: '',
},
title: {
type: DataTypes.STRING,
@ -82,6 +87,10 @@ AiChatHistoryModel.init(
type: DataTypes.JSONB,
defaultValue: {},
},
version: {
type: DataTypes.INTEGER,
defaultValue: 0,
},
uid: {
type: DataTypes.UUID,
allowNull: true,

View File

@ -8,6 +8,8 @@ export class ChatConfigServices {
owner: string;
// 使用者
username: string;
aiConfig?: AIConfig;
/**
* username 使使
* @param username
@ -50,6 +52,7 @@ export class ChatConfigServices {
const cacheTime = 60 * 60 * 24 * 40; // 1天
await redis.set(key, JSON.stringify(modelConfig), 'EX', cacheTime);
}
this.aiConfig = modelConfig;
if (needClearSecret) {
modelConfig = this.filterApiKey(modelConfig);
}

View File

@ -1,4 +1,4 @@
import { AIConfigParser, ProviderResult } from '@/provider/utils/parse-config.ts';
import { AIConfig, AIConfigParser, ProviderResult } from '@/provider/utils/parse-config.ts';
import { ProviderManager, ChatMessage, BaseChat, ChatMessageOptions } from '@/provider/index.ts';
import { redis } from '@/modules/db.ts';
import { CustomError } from '@kevisual/router';
@ -36,6 +36,7 @@ export class ChatServices {
*
*/
modelConfig?: ProviderResult;
aiConfig?: AIConfig;
chatProvider?: BaseChat;
constructor(opts: ChatServicesConfig) {
this.owner = opts.owner;
@ -65,6 +66,7 @@ export class ChatServices {
},
});
that.modelConfig = { ...providerResult, apiKey };
that.aiConfig = config;
return that.modelConfig;
}
/**
@ -108,7 +110,7 @@ export class ChatServices {
async createNewMessage(messages: ChastHistoryMessage[]) {
return messages.map((item) => {
if (!item.id) {
item.id = 'chat' + nanoid();
item.id = 'chat-' + nanoid();
item.createdAt = Date.now();
item.updatedAt = Date.now();
}