diff --git a/src/auth/middleware/auth-manual.ts b/src/auth/middleware/auth-manual.ts deleted file mode 100644 index caa095e..0000000 --- a/src/auth/middleware/auth-manual.ts +++ /dev/null @@ -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; - } -}; diff --git a/src/auth/middleware/auth.ts b/src/auth/middleware/auth.ts deleted file mode 100644 index d1f6dac..0000000 --- a/src/auth/middleware/auth.ts +++ /dev/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); -}; diff --git a/src/db/drizzle/schema.ts b/src/db/drizzle/schema.ts index d3f2746..3d6e6ae 100644 --- a/src/db/drizzle/schema.ts +++ b/src/db/drizzle/schema.ts @@ -475,4 +475,23 @@ export const routerViews = pgTable("router_views", { index('router_views_uid_idx').using('btree', table.uid.asc().nullsLast()), 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()), ]); \ No newline at end of file diff --git a/src/modules/ws-proxy/index.ts b/src/modules/ws-proxy/index.ts index 76cbc84..5c5a5be 100644 --- a/src/modules/ws-proxy/index.ts +++ b/src/modules/ws-proxy/index.ts @@ -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); diff --git a/src/route.ts b/src/route.ts index ace6b67..003a667 100644 --- a/src/route.ts +++ b/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) { diff --git a/src/routes/ai/index.ts b/src/routes/ai/index.ts index aa895dd..44287be 100644 --- a/src/routes/ai/index.ts +++ b/src/routes/ai/index.ts @@ -41,7 +41,7 @@ app.route({ text, cost: cost, model, - payload + action: payload } }).addTo(app) diff --git a/src/routes/index.ts b/src/routes/index.ts index 2160a15..b5c67e0 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -15,4 +15,6 @@ import './ai/index.ts'; import './prompts/index.ts' -import './views/index.ts'; \ No newline at end of file +import './views/index.ts'; + +import './query-views/index.ts'; \ No newline at end of file diff --git a/src/routes/query-views/index.ts b/src/routes/query-views/index.ts new file mode 100644 index 0000000..9166f9d --- /dev/null +++ b/src/routes/query-views/index.ts @@ -0,0 +1 @@ +import './list.ts' \ No newline at end of file diff --git a/src/routes/query-views/list.ts b/src/routes/query-views/list.ts new file mode 100644 index 0000000..65d28b8 --- /dev/null +++ b/src/routes/query-views/list.ts @@ -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); \ No newline at end of file diff --git a/src/routes/views/list.ts b/src/routes/views/list.ts index 1444613..ed96e6d 100644 --- a/src/routes/views/list.ts +++ b/src/routes/views/list.ts @@ -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 || {};