Files
pagodera/lib/socket/server.ts
T
Evandro Fonseca Junior 7f42def478 feat(market): add Minha Intenções tab with panel, hook, and badge
- Introduced "Minhas Intenções" tab visible only to logged-in users.
- Integrated MyPurchaseIntentionsPanel and useMyPurchaseIntentions hook.
- Added tab badge showing count of pending intentions.
- Extended session API to include user’s main character for downstream features.
2025-11-07 17:25:51 -03:00

271 lines
7.6 KiB
TypeScript

/**
* Socket.io Server
* Integrado com Next.js
*/
import { Server as SocketIOServer } from "socket.io";
import type { Server as HTTPServer } from "http";
import { verifyAccessToken } from "@/lib/auth/jwt";
import { secureLogger } from "@/lib/secure-logger";
import { Message } from "@/lib/mongodb/models/Message";
import { Chat } from "@/lib/mongodb/models/Chat";
import { connectMongoDB } from "@/lib/mongodb/connection";
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;
}