update
This commit is contained in:
@@ -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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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);
|
|
||||||
};
|
|
||||||
@@ -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()),
|
||||||
|
]);
|
||||||
@@ -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);
|
||||||
|
|||||||
18
src/route.ts
18
src/route.ts
@@ -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) {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ app.route({
|
|||||||
text,
|
text,
|
||||||
cost: cost,
|
cost: cost,
|
||||||
model,
|
model,
|
||||||
payload
|
action: payload
|
||||||
}
|
}
|
||||||
}).addTo(app)
|
}).addTo(app)
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
1
src/routes/query-views/index.ts
Normal file
1
src/routes/query-views/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import './list.ts'
|
||||||
138
src/routes/query-views/list.ts
Normal file
138
src/routes/query-views/list.ts
Normal 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);
|
||||||
@@ -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 || {};
|
||||||
|
|||||||
Reference in New Issue
Block a user