/** * Socket.io Server * Integrado com Next.js */ import type { Server as HTTPServer } from "node:http"; import { Server as SocketIOServer } from "socket.io"; import { verifyAccessToken } from "@/lib/auth/jwt"; import { connectMongoDB } from "@/lib/mongodb/connection"; import { Chat } from "@/lib/mongodb/models/Chat"; import { Message } from "@/lib/mongodb/models/Message"; import { secureLogger } from "@/lib/secure-logger"; let io: SocketIOServer | null = null; /** * Inicializa o servidor Socket.io */ export function initSocketIO(httpServer?: HTTPServer): SocketIOServer { if (io) { return io; } if (!httpServer) { throw new Error("HTTP Server é necessário para inicializar Socket.io"); } io = new SocketIOServer(httpServer, { path: "/api/socket", addTrailingSlash: false, cors: { origin: process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000", methods: ["GET", "POST"], credentials: true, }, }); // Middleware de autenticação io.use(async (socket, next) => { try { // Tentar obter token do auth primeiro const token = socket.handshake.auth.token; if (token) { const payload = verifyAccessToken(token); if (payload) { socket.data.userId = payload.userId; socket.data.username = payload.username; return next(); } } // Se não tiver token, tentar via cookie sessionId const cookies = socket.handshake.headers.cookie; if (cookies) { const sessionIdMatch = cookies.match(/sessionId=([^;]+)/); if (sessionIdMatch) { const sessionId = sessionIdMatch[1]; const { getSession } = await import("@/lib/redis/session"); const session = await getSession(sessionId); if (session) { const payload = verifyAccessToken(session.accessToken); if (payload) { socket.data.userId = payload.userId; socket.data.username = payload.username; return next(); } } } } return next(new Error("Token não fornecido ou inválido")); } catch (error) { secureLogger.error("Erro na autenticação Socket.io", { error: error instanceof Error ? error.message : "Erro desconhecido", }); next(new Error("Erro na autenticação")); } }); // Event handlers io.on("connection", (socket) => { const userId = socket.data.userId; const username = socket.data.username; // Cliente conectado (log desabilitado para reduzir poluição do console) // Entrar em rooms dos chats do usuário socket.on("join_chats", async () => { try { await connectMongoDB(); // Buscar todos os chats do usuário const chats = await Chat.find({ $or: [{ buyer_id: userId }, { seller_id: userId }], is_active: true, }).select("chat_id"); chats.forEach((chat) => { socket.join(`chat:${chat.chat_id}`); }); // Usuário entrou nos chats (log desabilitado) } catch (error) { secureLogger.error("Erro ao entrar em chats", { userId, error: error instanceof Error ? error.message : "Erro desconhecido", }); } }); // Entrar em um chat específico socket.on("join_chat", async (data: { chat_id: string }) => { try { const { chat_id } = data; // Verificar se o usuário pertence ao chat await connectMongoDB(); const chat = await Chat.findOne({ chat_id }); if (!chat) { socket.emit("error", { message: "Chat não encontrado" }); return; } if (chat.buyer_id !== userId && chat.seller_id !== userId) { socket.emit("error", { message: "Você não tem permissão para acessar este chat", }); return; } socket.join(`chat:${chat_id}`); // Marcar mensagens como lidas await Message.updateMany( { chat_id, sender_id: { $ne: userId }, read_at: null, }, { $set: { read_at: new Date() }, }, ); // Atualizar contador de não lidas if (chat.buyer_id === userId) { chat.buyer_unread_count = 0; } else { chat.seller_unread_count = 0; } await chat.save(); // Usuário entrou no chat (log desabilitado) } catch (error) { secureLogger.error("Erro ao entrar no chat", { userId, error: error instanceof Error ? error.message : "Erro desconhecido", }); socket.emit("error", { message: "Erro ao entrar no chat" }); } }); // Enviar mensagem socket.on( "send_message", async (data: { chat_id: string; content: string }) => { try { const { chat_id, content } = data; if (!content || content.trim().length === 0) { socket.emit("error", { message: "Mensagem não pode estar vazia" }); return; } if (content.length > 1000) { socket.emit("error", { message: "Mensagem muito longa (máximo 1000 caracteres)", }); return; } await connectMongoDB(); // Verificar se o chat existe e o usuário pertence a ele const chat = await Chat.findOne({ chat_id }); if (!chat) { socket.emit("error", { message: "Chat não encontrado" }); return; } if (chat.buyer_id !== userId && chat.seller_id !== userId) { socket.emit("error", { message: "Você não tem permissão para enviar mensagens neste chat", }); return; } // Criar mensagem const message = new Message({ chat_id, sender_id: userId, content: content.trim(), message_type: "text", }); await message.save(); // Atualizar chat chat.last_message_at = new Date(); chat.last_message_preview = content.substring(0, 100); // Atualizar contador de não lidas if (chat.buyer_id === userId) { chat.seller_unread_count += 1; } else { chat.buyer_unread_count += 1; } await chat.save(); // Enviar mensagem para todos no chat (exceto o remetente) io?.to(`chat:${chat_id}`).emit("new_message", { message: { _id: message._id.toString(), chat_id: message.chat_id, sender_id: message.sender_id, content: message.content, created_at: message.created_at, read_at: message.read_at, message_type: message.message_type, }, }); // Confirmar para o remetente socket.emit("message_sent", { message_id: message._id.toString(), }); // Mensagem enviada (log desabilitado) } catch (error) { secureLogger.error("Erro ao enviar mensagem", { userId, error: error instanceof Error ? error.message : "Erro desconhecido", }); socket.emit("error", { message: "Erro ao enviar mensagem" }); } }, ); // Indicador de "digitando..." socket.on("typing", (data: { chat_id: string; is_typing: boolean }) => { const { chat_id, is_typing } = data; socket.to(`chat:${chat_id}`).emit("user_typing", { user_id: userId, username: username, is_typing, }); }); // Desconexão socket.on("disconnect", () => { // Cliente desconectado (log desabilitado) }); }); return io; } /** * Obtém a instância do servidor Socket.io */ export function getSocketIO(): SocketIOServer | null { return io; }