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_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 { logger } from '../logger.ts';
|
||||
export const wsProxyManager = new WsProxyManager();
|
||||
import { WebScoketListenerFun } from '@kevisual/router/src/server/server-type.ts'
|
||||
export const wssFun: WebScoketListenerFun = async (req, res) => {
|
||||
import { WebSocketListenerFun } from '@kevisual/router/src/server/server-type.ts'
|
||||
export const wssFun: WebSocketListenerFun = async (req, res) => {
|
||||
// do nothing, just to enable ws upgrade event
|
||||
const { id, ws, token, data, emitter } = req;
|
||||
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) => {
|
||||
const token = ctx.query.token;
|
||||
// 已经有用户信息则直接返回,不需要重复验证
|
||||
if (ctx.state.tokenUser) {
|
||||
return;
|
||||
}
|
||||
if (!token) {
|
||||
app.throw(401, 'Token is required');
|
||||
}
|
||||
@@ -44,6 +48,10 @@ export const addAuth = (app: App) => {
|
||||
description: '验证token,可以不成功,错误不返回401,正确赋值到ctx.state.tokenUser,失败赋值null',
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
// 已经有用户信息则直接返回,不需要重复验证
|
||||
if (ctx.state.tokenUser) {
|
||||
return;
|
||||
}
|
||||
if (ctx.query?.token) {
|
||||
const token = ctx.query.token;
|
||||
const user = await User.getOauthUser(token);
|
||||
@@ -76,6 +84,9 @@ app
|
||||
if (!tokenUser) {
|
||||
ctx.throw(401, 'No User For authorized');
|
||||
}
|
||||
if (typeof ctx.state.isAdmin !== 'undefined' && ctx.state.isAdmin === true) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const user = await User.findOne({
|
||||
where: {
|
||||
@@ -92,6 +103,7 @@ app
|
||||
} else {
|
||||
ctx.throw(403, 'forbidden');
|
||||
}
|
||||
ctx.state.isAdmin = true;
|
||||
} catch (e) {
|
||||
console.error(`auth-admin error`, e);
|
||||
console.error('tokenUser', tokenUser?.id, tokenUser?.username, tokenUser?.uid);
|
||||
@@ -111,6 +123,9 @@ app
|
||||
if (!tokenUser) {
|
||||
ctx.throw(401, 'No User For authorized');
|
||||
}
|
||||
if (typeof ctx.state.isAdmin !== 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await User.findOne({
|
||||
@@ -125,12 +140,15 @@ app
|
||||
const orgs = await user.getOrgs();
|
||||
if (orgs.includes('admin')) {
|
||||
ctx.body = 'admin';
|
||||
ctx.state.isAdmin = true;
|
||||
ctx.state.tokenAdmin = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
orgs,
|
||||
};
|
||||
return;
|
||||
} else {
|
||||
ctx.state.isAdmin = false;
|
||||
}
|
||||
ctx.body = 'not admin';
|
||||
} catch (e) {
|
||||
|
||||
@@ -41,7 +41,7 @@ app.route({
|
||||
text,
|
||||
cost: cost,
|
||||
model,
|
||||
payload
|
||||
action: payload
|
||||
}
|
||||
}).addTo(app)
|
||||
|
||||
|
||||
@@ -16,3 +16,5 @@ import './ai/index.ts';
|
||||
import './prompts/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',
|
||||
key: 'delete',
|
||||
middleware: ['auth'],
|
||||
description: '删除视图, 参数: id 视图ID',
|
||||
description: '删除视图, 参数: data.id 视图ID',
|
||||
}).define(async (ctx) => {
|
||||
const tokenUser = ctx.state.tokenUser;
|
||||
const { id } = ctx.query.data || {};
|
||||
@@ -123,7 +123,7 @@ app.route({
|
||||
path: 'views',
|
||||
key: 'get',
|
||||
middleware: ['auth'],
|
||||
description: '获取单个视图, 参数: id 视图ID',
|
||||
description: '获取单个视图, 参数: data.id 视图ID',
|
||||
}).define(async (ctx) => {
|
||||
const tokenUser = ctx.state.tokenUser;
|
||||
const { id } = ctx.query.data || {};
|
||||
|
||||
Reference in New Issue
Block a user