- 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.
271 lines
7.6 KiB
TypeScript
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;
|
|
}
|
|
|