diff --git a/HISTORY.md b/HISTORY.md index 6b5f64f..554b73b 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -3,6 +3,7 @@ 迁移管理页面,在正式环境中,需要验证env中配置的密码参数才能打开 2025.05.15 0.1.6 +增加socketio 路由 支持 增加socketio server 支持 修正文件分类后端api路由查询表名为file_categories 将react版本降为18.3.1 diff --git a/server/app.tsx b/server/app.tsx index 9fe35cc..7d4d17a 100644 --- a/server/app.tsx +++ b/server/app.tsx @@ -4,6 +4,7 @@ import React from 'hono/jsx' import type { Context as HonoContext } from 'hono' import { serveStatic } from 'hono/deno' import { APIClient } from '@d8d-appcontainer/api' +import { Auth } from '@d8d-appcontainer/auth'; import debug from "debug" import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; @@ -54,16 +55,17 @@ interface EsmScriptConfig { // 定义模块参数接口 interface ModuleParams { - apiClient: APIClient + apiClient: APIClient, + auth: Auth, app: Hono moduleDir: string } -export default function({ apiClient, app, moduleDir }: ModuleParams) { +export default function({ apiClient, app, moduleDir , auth}: ModuleParams) { const honoApp = app // 创建路由 - const router = createRouter(apiClient, moduleDir) + const router = createRouter(apiClient, moduleDir, auth) honoApp.route('/', router) // 首页路由 - SSR diff --git a/server/middlewares.ts b/server/middlewares.ts index 4fad3f5..546673c 100644 --- a/server/middlewares.ts +++ b/server/middlewares.ts @@ -45,11 +45,11 @@ export const withAuth = async (c: HonoContext<{ Variables: Variables }>, next: ( export type WithAuth = typeof withAuth; // 环境变量设置中间件 -export const setEnvVariables = (apiClient: APIClient, moduleDir: string) => { +export const setEnvVariables = (apiClient: APIClient, moduleDir: string, auth: Auth) => { return async (c: HonoContext<{ Variables: Variables }>, next: () => Promise) => { c.set('apiClient', apiClient) c.set('moduleDir', moduleDir) - c.set('auth', await initAuth(apiClient)) + c.set('auth', auth) c.set('systemSettings', await initSystemSettings(apiClient)) await next() } @@ -58,37 +58,7 @@ export const setEnvVariables = (apiClient: APIClient, moduleDir: string) => { // CORS中间件 export const corsMiddleware = cors() -// 初始化Auth实例 -const initAuth = async (apiClient: APIClient) => { - try { - log.auth('正在初始化Auth实例') - - const auth = new Auth(apiClient as any, { - jwtSecret: Deno.env.get("JWT_SECRET") || 'your-jwt-secret-key', - initialUsers: [], - storagePrefix: '', - userTable: 'users', - fieldNames: { - id: 'id', - username: 'username', - password: 'password', - phone: 'phone', - email: 'email', - is_disabled: 'is_disabled', - is_deleted: 'is_deleted' - }, - tokenExpiry: 24 * 60 * 60, - refreshTokenExpiry: 7 * 24 * 60 * 60 - }) - - log.auth('Auth实例初始化完成') - return auth - - } catch (error) { - log.auth('Auth初始化失败:', error) - throw error - } -} + // 初始化系统设置 const initSystemSettings = async (apiClient: APIClient) => { diff --git a/server/router.ts b/server/router.ts index b601883..9a78777 100644 --- a/server/router.ts +++ b/server/router.ts @@ -2,6 +2,7 @@ import { Hono } from 'hono' import { corsMiddleware, withAuth, setEnvVariables } from './middlewares.ts' import type { APIClient } from '@d8d-appcontainer/api' +import { Auth } from '@d8d-appcontainer/auth'; // 导入路由模块 import { createAuthRoutes } from "./routes_auth.ts" @@ -17,7 +18,7 @@ import { createMessagesRoutes } from "./routes_messages.ts" import { createMigrationsRoutes } from "./routes_migrations.ts" import { createHomeRoutes } from "./routes_home.ts" -export function createRouter(apiClient: APIClient, moduleDir: string) { +export function createRouter(apiClient: APIClient, moduleDir: string , auth: Auth) { const router = new Hono() // 添加CORS中间件 @@ -27,7 +28,7 @@ export function createRouter(apiClient: APIClient, moduleDir: string) { const api = new Hono() // 设置环境变量 - api.use('*', setEnvVariables(apiClient, moduleDir)) + api.use('*', setEnvVariables(apiClient, moduleDir, auth)) // 注册所有路由 api.route('/auth', createAuthRoutes(withAuth)) diff --git a/server/router_io.ts b/server/router_io.ts new file mode 100644 index 0000000..f942046 --- /dev/null +++ b/server/router_io.ts @@ -0,0 +1,73 @@ +import { Socket, Server } from "socket.io"; +import { Auth } from '@d8d-appcontainer/auth'; +import type { User as AuthUser } from '@d8d-appcontainer/auth'; +import { APIClient } from '@d8d-appcontainer/api'; +import { setupMessageEvents } from './routes_io_messages.ts'; +import debug from "debug"; + +const log = debug('socketio:auth'); + +interface SetupSocketIOProps { + io: Server, auth: Auth, apiClient: APIClient +} + +export interface SocketWithUser extends Socket { + user?: AuthUser; +} + +// 定义自定义上下文类型 +export interface Variables { + socket: SocketWithUser + auth: Auth + user: AuthUser + apiClient: APIClient + // moduleDir: string + // systemSettings?: SystemSettingRecord +} + +export function setupSocketIO({ io, auth, apiClient }:SetupSocketIOProps) { + // Socket.IO认证中间件 + io.use(async (socket: SocketWithUser) => { + try { + const token = socket.handshake.query.get('socket_token'); + if (!token) { + log(`未提供token,拒绝连接: ${socket.id}`); + throw new Error('未授权') + } + + const userData = await auth.verifyToken(token); + if (!userData) { + log(`无效token,拒绝连接: ${socket.id}`); + throw new Error('无效凭证') + } + + socket.user = userData; + log(`认证成功: ${socket.id} 用户: ${userData.username}`); + } catch (error) { + log(`认证错误: ${socket.id}`, error); + } + }); + + io.on("connection", (socket: SocketWithUser) => { + if (!socket.user) { + socket.disconnect(true); + return; + } + + console.log(`socket ${socket.id} 已连接,用户: ${socket.user.username}`); + + socket.on("disconnect", (reason) => { + console.log(`socket ${socket.id} 断开连接,原因: ${reason}`); + }); + + const context: Variables = { + socket, + auth, + apiClient, + user: socket.user, + } + + // 初始化消息路由 + setupMessageEvents(context); + }); +} \ No newline at end of file diff --git a/server/routes_io_messages.ts b/server/routes_io_messages.ts new file mode 100644 index 0000000..9812707 --- /dev/null +++ b/server/routes_io_messages.ts @@ -0,0 +1,226 @@ +import { SocketWithUser , Variables} from './router_io.ts'; +import { MessageType, MessageStatus } from '../client/share/types.ts' +import { APIClient } from "@d8d-appcontainer/api"; + +interface MessageSendData { + title: string; + content: string; + type: MessageType; + receiver_ids: number[]; +} + +interface MessageListData { + page?: number; + pageSize?: number; + type?: MessageType; + status?: MessageStatus; +} + +export function setupMessageEvents({ socket , apiClient }:Variables) { + // 发送消息 + socket.on('message:send', async (data: MessageSendData) => { + try { + const { title, content, type, receiver_ids } = data; + + if (!title || !content || !type || !receiver_ids?.length) { + socket.emit('error', '缺少必要参数'); + return; + } + + const user = socket.user; + if (!user) { + socket.emit('error', '未授权访问'); + return; + } + + // 创建消息 + const [messageId] = await apiClient.database.table('messages').insert({ + title, + content, + type, + sender_id: user.id, + sender_name: user.username, + created_at: apiClient.database.fn.now(), + updated_at: apiClient.database.fn.now() + }); + + // 关联用户消息 + const userMessages = receiver_ids.map((userId: number) => ({ + user_id: userId, + message_id: messageId, + status: MessageStatus.UNREAD, + created_at: apiClient.database.fn.now(), + updated_at: apiClient.database.fn.now() + })); + + await apiClient.database.table('user_messages').insert(userMessages); + + socket.emit('message:sent', { + message: '消息发送成功', + data: { id: messageId } + }); + } catch (error) { + console.error('发送消息失败:', error); + socket.emit('error', '发送消息失败'); + } + }); + + // 获取消息列表 + socket.on('message:list', async (data: MessageListData) => { + try { + const { page = 1, pageSize = 20, type, status } = data; + const user = socket.user; + if (!user) { + socket.emit('error', '未授权访问'); + return; + } + + const query = apiClient.database.table('user_messages as um') + .select('m.*', 'um.status as user_status', 'um.read_at', 'um.id as user_message_id') + .leftJoin('messages as m', 'um.message_id', 'm.id') + .where('um.user_id', user.id) + .where('um.is_deleted', 0) + .orderBy('m.created_at', 'desc') + .limit(pageSize) + .offset((page - 1) * pageSize); + + if (type) query.where('m.type', type); + if (status) query.where('um.status', status); + + const countQuery = query.clone(); + const messages = await query; + + // 获取总数用于分页 + const total = await countQuery.count(); + const totalCount = Number(total); + const totalPages = Math.ceil(totalCount / pageSize); + + socket.emit('message:list', { + data: messages, + pagination: { + total: totalCount, + current: page, + pageSize, + totalPages + } + }); + } catch (error) { + console.error('获取消息列表失败:', error); + socket.emit('error', '获取消息列表失败'); + } + }); + + // 获取消息详情 + socket.on('message:detail', async (messageId: number) => { + try { + const user = socket.user; + if (!user) { + socket.emit('error', '未授权访问'); + return; + } + + const message = await apiClient.database.table('user_messages as um') + .select('m.*', 'um.status as user_status', 'um.read_at') + .leftJoin('messages as m', 'um.message_id', 'm.id') + .where('um.user_id', user.id) + .where('um.message_id', messageId) + .first(); + + if (!message) { + socket.emit('error', '消息不存在或无权访问'); + return; + } + + // 标记为已读 + if (message.user_status === MessageStatus.UNREAD) { + await apiClient.database.table('user_messages') + .where('user_id', user.id) + .where('message_id', messageId) + .update({ + status: MessageStatus.READ, + read_at: apiClient.database.fn.now(), + updated_at: apiClient.database.fn.now() + }); + } + + socket.emit('message:detail', { + message: '获取消息成功', + data: message + }); + } catch (error) { + console.error('获取消息详情失败:', error); + socket.emit('error', '获取消息详情失败'); + } + }); + + // 删除消息 + socket.on('message:delete', async (messageId: number) => { + try { + const user = socket.user; + if (!user) { + socket.emit('error', '未授权访问'); + return; + } + + await apiClient.database.table('user_messages') + .where('user_id', user.id) + .where('message_id', messageId) + .update({ + is_deleted: 1, + updated_at: apiClient.database.fn.now() + }); + + socket.emit('message:deleted', { message: '消息已删除' }); + } catch (error) { + console.error('删除消息失败:', error); + socket.emit('error', '删除消息失败'); + } + }); + + // 获取未读消息数 + socket.on('message:count', async () => { + try { + const user = socket.user; + if (!user) { + socket.emit('error', '未授权访问'); + return; + } + + const count = await apiClient.database.table('user_messages') + .where('user_id', user.id) + .where('status', MessageStatus.UNREAD) + .where('is_deleted', 0) + .count(); + + socket.emit('message:count', { count: Number(count) }); + } catch (error) { + console.error('获取未读消息数失败:', error); + socket.emit('error', '获取未读消息数失败'); + } + }); + + // 标记消息为已读 + socket.on('message:read', async (messageId: number) => { + try { + const user = socket.user; + if (!user) { + socket.emit('error', '未授权访问'); + return; + } + + await apiClient.database.table('user_messages') + .where('user_id', user.id) + .where('message_id', messageId) + .update({ + status: MessageStatus.READ, + read_at: apiClient.database.fn.now(), + updated_at: apiClient.database.fn.now() + }); + + socket.emit('message:read', { message: '消息已标记为已读' }); + } catch (error) { + console.error('标记消息为已读失败:', error); + socket.emit('error', '标记消息为已读失败'); + } + }); +} \ No newline at end of file diff --git a/server/routes_socketio.ts b/server/routes_socketio.ts deleted file mode 100644 index 6f5bd57..0000000 --- a/server/routes_socketio.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Socket, Server } from "socket.io"; - -export function setupSocketIO(io: Server) { - io.on("connection", (socket: Socket) => { - console.log(`socket ${socket.id} connected`); - - socket.emit("hello", "world"); - - socket.on("disconnect", (reason) => { - console.log(`socket ${socket.id} disconnected due to ${reason}`); - }); - - // 可在此添加更多socket事件处理 - }); -} \ No newline at end of file diff --git a/server/run_app.ts b/server/run_app.ts index 20e0f76..b168123 100644 --- a/server/run_app.ts +++ b/server/run_app.ts @@ -1,11 +1,12 @@ // 导入所需模块 import { Hono } from 'hono' import { APIClient } from '@d8d-appcontainer/api' +import { Auth } from '@d8d-appcontainer/auth'; import debug from "debug" import { cors } from 'hono/cors' import { Server } from "socket.io" import httpServer from './app.tsx' -import { setupSocketIO } from './routes_socketio.ts' +import { setupSocketIO } from './router_io.ts' // 初始化debug实例 const log = { @@ -39,6 +40,37 @@ const getApiClient = async (workspaceKey: string, serverUrl?: string) => { throw error } } +// 初始化Auth实例 +const initAuth = async (apiClient: APIClient) => { + try { + log.auth('正在初始化Auth实例') + + const auth = new Auth(apiClient as any, { + jwtSecret: Deno.env.get("JWT_SECRET") || 'your-jwt-secret-key', + initialUsers: [], + storagePrefix: '', + userTable: 'users', + fieldNames: { + id: 'id', + username: 'username', + password: 'password', + phone: 'phone', + email: 'email', + is_disabled: 'is_disabled', + is_deleted: 'is_deleted' + }, + tokenExpiry: 24 * 60 * 60, + refreshTokenExpiry: 7 * 24 * 60 * 60 + }) + + log.auth('Auth实例初始化完成') + return auth + + } catch (error) { + log.auth('Auth初始化失败:', error) + throw error + } +} // 初始化API Client // 注意:WORKSPACE_KEY 需要在 多八多(www.d8d.fun) 平台注册并开通工作空间后获取 @@ -47,6 +79,7 @@ if (!workspaceKey) { console.warn('未设置WORKSPACE_KEY,请前往 多八多(www.d8d.fun) 注册并开通工作空间以获取密钥') } const apiClient = await getApiClient(workspaceKey) +const auth = await initAuth(apiClient); // 创建Hono应用实例 const app = new Hono() @@ -63,10 +96,10 @@ const io = new Server({ } }) -setupSocketIO(io); +setupSocketIO({io, auth, apiClient}); // 动态加载并运行模板 -const runTemplate = async () => { +const runTemplate = () => { try { // 创建基础app实例 const moduleApp = new Hono() @@ -75,7 +108,8 @@ const runTemplate = async () => { const appInstance = httpServer({ apiClient: apiClient, app: moduleApp, - moduleDir: './' + moduleDir: './', + auth }) // 启动服务器 Deno.serve({ @@ -92,4 +126,4 @@ const runTemplate = async () => { } // 执行模板 -runTemplate() \ No newline at end of file +runTemplate() \ No newline at end of file