This commit is contained in:
2026-01-05 02:02:51 +08:00
parent c6715c2e35
commit 93879b532b
10 changed files with 184 additions and 143 deletions

View File

@@ -1,81 +0,0 @@
import { User } from '../models/user.ts';
import http from 'node:http';
import cookie from 'cookie';
export const error = (msg: string, code = 500) => {
return JSON.stringify({ code, message: msg });
};
type CheckAuthOptions = {
check401?: boolean; // 是否返回权限信息
};
/**
* 手动验证token如果token不存在则返回401
* @param req
* @param res
* @returns
*/
export const checkAuth = async (req: http.IncomingMessage, res: http.ServerResponse, opts?: CheckAuthOptions) => {
let token = (req.headers?.['authorization'] as string) || (req.headers?.['Authorization'] as string) || '';
const url = new URL(req.url || '', 'http://localhost');
const check401 = opts?.check401 ?? true; // 是否返回401错误
const resNoPermission = () => {
res.statusCode = 401;
res.end(error('Invalid authorization'));
return { tokenUser: null, token: null, hasToken: false };
};
if (!token) {
token = url.searchParams.get('token') || '';
}
if (!token) {
const parsedCookies = cookie.parse(req.headers.cookie || '');
token = parsedCookies.token || '';
}
if (!token && check401) {
return resNoPermission();
}
if (token) {
token = token.replace('Bearer ', '');
}
let tokenUser;
const hasToken = !!token; // 是否有token存在
try {
tokenUser = await User.verifyToken(token);
} catch (e) {
console.log('checkAuth error', e);
res.statusCode = 401;
res.end(error('Invalid token'));
return { tokenUser: null, token: null, hasToken: false };
}
return { tokenUser, token, hasToken };
};
/**
* 获取登录用户有则获取无则返回null
* @param req
* @returns
*/
export const getLoginUser = async (req: http.IncomingMessage) => {
let token = (req.headers?.['authorization'] as string) || (req.headers?.['Authorization'] as string) || '';
const url = new URL(req.url || '', 'http://localhost');
if (!token) {
token = url.searchParams.get('token') || '';
}
if (!token) {
const parsedCookies = cookie.parse(req.headers.cookie || '');
token = parsedCookies.token || '';
}
if (token) {
token = token.replace('Bearer ', '');
}
if (!token) {
return null;
}
let tokenUser;
try {
tokenUser = await User.verifyToken(token);
return { tokenUser, token };
} catch (e) {
return null;
}
};

View File

@@ -1,56 +0,0 @@
import { User } from '../models/user.ts';
import type { App } from '@kevisual/router';
/**
* 添加auth中间件, 用于验证token
* 添加 id: auth 必须需要user成功
* 添加 id: auth-can 可以不需要user成功有则赋值
*
* @param app
*/
export const addAuth = (app: App) => {
app
.route({
path: 'auth',
id: 'auth',
})
.define(async (ctx) => {
const token = ctx.query.token;
if (!token) {
app.throw(401, 'Token is required');
}
const user = await User.getOauthUser(token);
if (!user) {
app.throw(401, 'Token is invalid');
}
if (ctx.state) {
ctx.state.tokenUser = user;
} else {
ctx.state = {
tokenUser: user,
};
}
})
.addTo(app);
app
.route({
path: 'auth',
key: 'can',
id: 'auth-can',
})
.define(async (ctx) => {
if (ctx.query?.token) {
const token = ctx.query.token;
const user = await User.getOauthUser(token);
if (ctx.state) {
ctx.state.tokenUser = user;
} else {
ctx.state = {
tokenUser: user,
};
}
}
})
.addTo(app);
};

View File

@@ -476,3 +476,22 @@ export const routerViews = pgTable("router_views", {
index('router_title_idx').using('btree', table.title.asc().nullsLast()), index('router_title_idx').using('btree', table.title.asc().nullsLast()),
index('router_views_views_idx').using('gin', table.views), index('router_views_views_idx').using('gin', table.views),
]); ]);
export const queryViews = pgTable("query_views", {
id: uuid().primaryKey().notNull().defaultRandom(),
uid: uuid(),
title: text('title').default(''),
summary: text('summary').default(''),
description: text('description').default(''),
tags: jsonb().default([]),
link: text('link').default(''),
data: jsonb().default({}),
createdAt: timestamp('createdAt').notNull().defaultNow(),
updatedAt: timestamp('updatedAt').notNull().defaultNow(),
}, (table) => [
index('query_views_uid_idx').using('btree', table.uid.asc().nullsLast()),
index('query_title_idx').using('btree', table.title.asc().nullsLast()),
]);

View File

@@ -2,8 +2,8 @@ import { WsProxyManager } from './manager.ts';
import { getLoginUserByToken } from '@/modules/auth.ts'; import { getLoginUserByToken } from '@/modules/auth.ts';
import { logger } from '../logger.ts'; import { logger } from '../logger.ts';
export const wsProxyManager = new WsProxyManager(); export const wsProxyManager = new WsProxyManager();
import { WebScoketListenerFun } from '@kevisual/router/src/server/server-type.ts' import { WebSocketListenerFun } from '@kevisual/router/src/server/server-type.ts'
export const wssFun: WebScoketListenerFun = async (req, res) => { export const wssFun: WebSocketListenerFun = async (req, res) => {
// do nothing, just to enable ws upgrade event // do nothing, just to enable ws upgrade event
const { id, ws, token, data, emitter } = req; const { id, ws, token, data, emitter } = req;
logger.debug('ws proxy connected, id=', id, ' token=', token, ' data=', data); logger.debug('ws proxy connected, id=', id, ' token=', token, ' data=', data);

View File

@@ -21,6 +21,10 @@ export const addAuth = (app: App) => {
}) })
.define(async (ctx) => { .define(async (ctx) => {
const token = ctx.query.token; const token = ctx.query.token;
// 已经有用户信息则直接返回,不需要重复验证
if (ctx.state.tokenUser) {
return;
}
if (!token) { if (!token) {
app.throw(401, 'Token is required'); app.throw(401, 'Token is required');
} }
@@ -44,6 +48,10 @@ export const addAuth = (app: App) => {
description: '验证token可以不成功错误不返回401正确赋值到ctx.state.tokenUser失败赋值null', description: '验证token可以不成功错误不返回401正确赋值到ctx.state.tokenUser失败赋值null',
}) })
.define(async (ctx) => { .define(async (ctx) => {
// 已经有用户信息则直接返回,不需要重复验证
if (ctx.state.tokenUser) {
return;
}
if (ctx.query?.token) { if (ctx.query?.token) {
const token = ctx.query.token; const token = ctx.query.token;
const user = await User.getOauthUser(token); const user = await User.getOauthUser(token);
@@ -76,6 +84,9 @@ app
if (!tokenUser) { if (!tokenUser) {
ctx.throw(401, 'No User For authorized'); ctx.throw(401, 'No User For authorized');
} }
if (typeof ctx.state.isAdmin !== 'undefined' && ctx.state.isAdmin === true) {
return;
}
try { try {
const user = await User.findOne({ const user = await User.findOne({
where: { where: {
@@ -92,6 +103,7 @@ app
} else { } else {
ctx.throw(403, 'forbidden'); ctx.throw(403, 'forbidden');
} }
ctx.state.isAdmin = true;
} catch (e) { } catch (e) {
console.error(`auth-admin error`, e); console.error(`auth-admin error`, e);
console.error('tokenUser', tokenUser?.id, tokenUser?.username, tokenUser?.uid); console.error('tokenUser', tokenUser?.id, tokenUser?.username, tokenUser?.uid);
@@ -111,6 +123,9 @@ app
if (!tokenUser) { if (!tokenUser) {
ctx.throw(401, 'No User For authorized'); ctx.throw(401, 'No User For authorized');
} }
if (typeof ctx.state.isAdmin !== 'undefined') {
return;
}
try { try {
const user = await User.findOne({ const user = await User.findOne({
@@ -125,12 +140,15 @@ app
const orgs = await user.getOrgs(); const orgs = await user.getOrgs();
if (orgs.includes('admin')) { if (orgs.includes('admin')) {
ctx.body = 'admin'; ctx.body = 'admin';
ctx.state.isAdmin = true;
ctx.state.tokenAdmin = { ctx.state.tokenAdmin = {
id: user.id, id: user.id,
username: user.username, username: user.username,
orgs, orgs,
}; };
return; return;
} else {
ctx.state.isAdmin = false;
} }
ctx.body = 'not admin'; ctx.body = 'not admin';
} catch (e) { } catch (e) {

View File

@@ -41,7 +41,7 @@ app.route({
text, text,
cost: cost, cost: cost,
model, model,
payload action: payload
} }
}).addTo(app) }).addTo(app)

View File

@@ -16,3 +16,5 @@ import './ai/index.ts';
import './prompts/index.ts' import './prompts/index.ts'
import './views/index.ts'; import './views/index.ts';
import './query-views/index.ts';

View File

@@ -0,0 +1 @@
import './list.ts'

View File

@@ -0,0 +1,138 @@
import { desc, eq, count, or, like, and } from 'drizzle-orm';
import { schema, app, db } from '@/app.ts'
app.route({
path: 'query-views',
key: 'list',
middleware: ['auth'],
description: '获取查询视图列表',
}).define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const uid = tokenUser.id;
const { page = 1, pageSize = 20, search, sort = 'DESC' } = ctx.query || {};
const offset = (page - 1) * pageSize;
const orderByField = sort === 'ASC' ? schema.queryViews.updatedAt : desc(schema.queryViews.updatedAt);
let whereCondition = eq(schema.queryViews.uid, uid);
if (search) {
whereCondition = and(
eq(schema.queryViews.uid, uid),
or(
like(schema.queryViews.title, `%${search}%`),
like(schema.queryViews.summary, `%${search}%`)
)
);
}
const [list, totalCount] = await Promise.all([
db.select()
.from(schema.queryViews)
.where(whereCondition)
.limit(pageSize)
.offset(offset)
.orderBy(orderByField),
db.select({ count: count() })
.from(schema.queryViews)
.where(whereCondition)
]);
ctx.body = {
list,
pagination: {
page,
current: page,
pageSize,
total: totalCount[0]?.count || 0,
},
};
return ctx;
}).addTo(app);
const viewUpdate = `创建或更新一个查询视图, 参数定义:
title: 视图标题, 必填
data: 数据, 对象, 选填
`;
app.route({
path: 'query-views',
key: 'update',
middleware: ['auth'],
description: viewUpdate,
}).define(async (ctx) => {
const { id, uid, updatedAt, ...rest } = ctx.query.data || {};
const tokenUser = ctx.state.tokenUser;
let view;
if (!id) {
view = await db.insert(schema.queryViews).values({
title: rest.title || '',
description: rest.description || '',
summary: rest.summary || '',
tags: rest.tags || [],
link: rest.link || '',
data: rest.data || { items: [] },
uid: tokenUser.id,
}).returning();
} else {
const existing = await db.select().from(schema.queryViews).where(eq(schema.queryViews.id, id)).limit(1);
if (existing.length === 0) {
ctx.throw(404, '没有找到对应的查询视图');
}
if (existing[0].uid !== tokenUser.id) {
ctx.throw(403, '没有权限更新该查询视图');
}
view = await db.update(schema.queryViews).set({
title: rest.title,
description: rest.description,
summary: rest.summary,
tags: rest.tags,
link: rest.link,
data: rest.data,
}).where(eq(schema.queryViews.id, id)).returning();
}
ctx.body = view;
}).addTo(app);
app.route({
path: 'query-views',
key: 'delete',
middleware: ['auth'],
description: '删除查询视图, 参数: data.id 视图ID',
}).define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const { id } = ctx.query.data || {};
if (!id) {
ctx.throw(400, 'id 参数缺失');
}
const existing = await db.select().from(schema.queryViews).where(eq(schema.queryViews.id, id)).limit(1);
if (existing.length === 0) {
ctx.throw(404, '没有找到对应的查询视图');
}
if (existing[0].uid !== tokenUser.id) {
ctx.throw(403, '没有权限删除该查询视图');
}
await db.delete(schema.queryViews).where(eq(schema.queryViews.id, id));
ctx.body = { success: true };
}).addTo(app);
app.route({
path: 'query-views',
key: 'get',
middleware: ['auth'],
description: '获取单个查询视图, 参数: data.id 视图ID',
}).define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const { id } = ctx.query.data || {};
if (!id) {
ctx.throw(400, 'id 参数缺失');
}
const existing = await db.select().from(schema.queryViews).where(eq(schema.queryViews.id, id)).limit(1);
if (existing.length === 0) {
ctx.throw(404, '没有找到对应的查询视图');
}
if (existing[0].uid !== tokenUser.id) {
ctx.throw(403, '没有权限查看该查询视图');
}
ctx.body = existing[0];
}).addTo(app);

View File

@@ -101,7 +101,7 @@ app.route({
path: 'views', path: 'views',
key: 'delete', key: 'delete',
middleware: ['auth'], middleware: ['auth'],
description: '删除视图, 参数: id 视图ID', description: '删除视图, 参数: data.id 视图ID',
}).define(async (ctx) => { }).define(async (ctx) => {
const tokenUser = ctx.state.tokenUser; const tokenUser = ctx.state.tokenUser;
const { id } = ctx.query.data || {}; const { id } = ctx.query.data || {};
@@ -123,7 +123,7 @@ app.route({
path: 'views', path: 'views',
key: 'get', key: 'get',
middleware: ['auth'], middleware: ['auth'],
description: '获取单个视图, 参数: id 视图ID', description: '获取单个视图, 参数: data.id 视图ID',
}).define(async (ctx) => { }).define(async (ctx) => {
const tokenUser = ctx.state.tokenUser; const tokenUser = ctx.state.tokenUser;
const { id } = ctx.query.data || {}; const { id } = ctx.query.data || {};