Reflex/backend/server.js

321 lines
16 KiB
JavaScript
Raw Normal View History

2025-05-21 22:13:09 +07:00
const dotenv = require('dotenv');
// Загружаем переменные окружения как можно раньше
dotenv.config();
const express = require('express');
const http = require('http'); // <--- Импорт http
const { Server } = require("socket.io"); // <--- Импорт Server из socket.io
const conversationRoutes = require('./routes/conversationRoutes');
// const dotenv = require('dotenv'); // Уже импортировано выше
const cors = require('cors');
// Импортируем модуль для подключения к БД
const connectDBModule = require('./config/db');
const authRoutes = require('./routes/authRoutes'); // <--- 1. РАСКОММЕНТИРУЙ ИЛИ ДОБАВЬ ЭТОТ ИМПОРТ
const userRoutes = require('./routes/userRoutes'); // 1. Импортируем userRoutes
const actionRoutes = require('./routes/actionRoutes'); // 1. Импортируем actionRoutes
const Message = require('./models/Message'); // Импорт модели Message
const Conversation = require('./models/Conversation'); // Импорт модели Conversation
// Загружаем переменные окружения до их использования
// dotenv.config(); // Перемещено в начало файла
// Проверяем, что JWT_SECRET правильно загружен
console.log('JWT_SECRET loaded:', process.env.JWT_SECRET ? 'Yes' : 'No');
// Добавляем отладочное логирование
console.log('Тип импортированного модуля connectDB:', typeof connectDBModule);
const app = express();
const server = http.createServer(app); // <--- Создание HTTP сервера
const io = new Server(server, { // <--- Инициализация socket.io
cors: {
origin: "*", // <--- Разрешаем все источники
methods: ["GET", "POST"]
}
});
// Сделать экземпляр io доступным в запросах
app.set('io', io);
2025-05-21 22:13:09 +07:00
// Подключение к базе данных - проверяем, что импортировали функцию
if (typeof connectDBModule === 'function') {
try {
connectDBModule(); // Вызов функции подключения
console.log('Попытка подключения к базе данных была инициирована...'); // Изменил лог для ясности
} catch (err) {
// Этот catch здесь, скорее всего, не поймает асинхронные ошибки из connectDBModule,
// так как connectDBModule асинхронная и не возвращает Promise, который здесь ожидался бы с await.
// Ошибки из connectDBModule (если они есть) будут выведены внутри самой функции connectDBModule.
console.error('Синхронная ошибка при вызове connectDBModule (маловероятно):', err);
}
} else {
console.error('Ошибка: connectDB не является функцией! Тип:', typeof connectDBModule);
}
// Настройка CORS для Express
app.use(cors({
origin: "*", // <--- Разрешаем все источники
credentials: true // Если нужны куки или заголовки авторизации
}));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.get('/', (req, res) => {
res.send('API для Dating App работает!');
});
// 2. РАСКОММЕНТИРУЙ ИЛИ ДОБАВЬ ЭТИ СТРОКИ для подключения маршрутов аутентификации
app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes); // 2. Подключаем userRoutes с префиксом /api/users
app.use('/api/actions', actionRoutes);
app.use('/api/conversations', conversationRoutes);
// Socket.IO логика
let activeUsers = [];
io.on("connection", (socket) => {
console.log("A user connected:", socket.id);
// Добавление нового пользователя
socket.on("addUser", (userId) => {
// Проверяем, не добавлен ли уже пользователь с таким userId, но другим socket.id
// Это может произойти, если пользователь открыл несколько вкладок или переподключился
const existingUser = activeUsers.find(user => user.userId === userId);
if (existingUser) {
// Обновляем socket.id для существующего userId
existingUser.socketId = socket.id;
} else {
// Добавляем нового пользователя
activeUsers.push({ userId, socketId: socket.id });
}
io.emit("getUsers", activeUsers); // Отправляем обновленный список активных пользователей всем клиентам
console.log("User added to active users:", userId, socket.id);
console.log("Active users:", activeUsers);
});
// Вспомогательная функция для получения сокета пользователя
const getUserSocket = (userId) => {
return activeUsers.find(u => u.userId === userId);
};
socket.on("joinConversationRoom", (roomId) => {
socket.join(roomId);
console.log(`[Socket.IO] User ${socket.id} joined room ${roomId}`);
});
socket.on("leaveConversationRoom", (roomId) => {
socket.leave(roomId);
console.log(`[Socket.IO] User ${socket.id} left room ${roomId}`);
});
2025-05-21 22:13:09 +07:00
// Отправка сообщения
socket.on("sendMessage", async ({ senderId, receiverId, text, clientConversationId }) => { // Добавлен clientConversationId, async
console.log(`"[Socket.IO] Получено сообщение от ${senderId} для ${receiverId}: "${text}" (попытка для диалога: ${clientConversationId})"`);
const getUserSocket = (userId) => {
const user = activeUsers.find(u => u.userId === userId);
return user ? user : null; // Возвращаем объект пользователя или null
};
try {
// 1. Найти или создать диалог
let conversation = await Conversation.findOne({
participants: { $all: [senderId, receiverId] }
});
if (!conversation) {
console.log(`[Socket.IO] Диалог не найден, создаем новый между ${senderId} и ${receiverId}`);
conversation = new Conversation({
participants: [senderId, receiverId]
// lastMessage будет установлено позже
});
// Не сохраняем conversation здесь сразу, сохраним вместе с lastMessage
} else {
console.log(`[Socket.IO] Найден существующий диалог: ${conversation._id}`);
}
// 2. Создать и сохранить новое сообщение
const newMessage = new Message({
conversationId: conversation._id, // Используем ID найденного или нового (но еще не сохраненного) диалога
sender: senderId,
text: text,
2025-05-24 02:30:51 +07:00
status: 'sending' // Начальный статус - отправка
2025-05-21 22:13:09 +07:00
});
await newMessage.save();
console.log(`[Socket.IO] Сообщение сохранено в БД: ${newMessage._id}`);
// 3. Обновить lastMessage и updatedAt в диалоге
conversation.lastMessage = newMessage._id;
// conversation.lastMessageTimestamp = newMessage.createdAt; // Если используем это поле
await conversation.save(); // Теперь сохраняем диалог (или обновляем существующий)
console.log(`[Socket.IO] Диалог ${conversation._id} обновлен с lastMessage.`);
// 4. Подготовить данные сообщения для отправки клиентам (с populated sender)
const populatedMessage = await Message.findById(newMessage._id)
.populate('sender', 'name photos'); // Загружаем данные отправителя
// 5. Отправить сообщение обоим участникам диалога (отправителю для подтверждения и получателю)
const senderSocketInfo = getUserSocket(senderId);
const receiverSocketInfo = getUserSocket(receiverId);
const messageToSend = {
_id: populatedMessage._id,
conversationId: populatedMessage.conversationId,
sender: populatedMessage.sender, // Объект с name, photos
text: populatedMessage.text,
createdAt: populatedMessage.createdAt,
2025-05-24 02:30:51 +07:00
status: populatedMessage.status
2025-05-21 22:13:09 +07:00
};
2025-05-24 02:30:51 +07:00
// После успешного сохранения в БД меняем статус на "delivered"
await Message.findByIdAndUpdate(newMessage._id, { status: 'delivered' });
2025-05-24 02:45:38 +07:00
messageToSend.status = 'delivered'; // Убедимся, что статус установлен явно
2025-05-24 02:30:51 +07:00
2025-05-21 22:13:09 +07:00
if (senderSocketInfo && senderSocketInfo.socketId) {
io.to(senderSocketInfo.socketId).emit("getMessage", messageToSend);
console.log(`[Socket.IO] Сообщение (подтверждение) отправлено отправителю ${senderId}`);
}
if (receiverSocketInfo && receiverSocketInfo.socketId) {
io.to(receiverSocketInfo.socketId).emit("getMessage", messageToSend);
console.log(`[Socket.IO] Сообщение отправлено получателю ${receiverId}`);
} else {
console.log(`[Socket.IO] Получатель ${receiverId} не онлайн / сокет не найден.`);
// TODO: Push-уведомление
}
} catch (error) {
console.error('[Socket.IO] Ошибка при обработке sendMessage:', error.message, error.stack);
const senderSocketInfo = getUserSocket(senderId);
if (senderSocketInfo && senderSocketInfo.socketId) {
io.to(senderSocketInfo.socketId).emit("messageError", {
message: "Не удалось отправить сообщение.",
originalText: text
});
}
}
});
// Событие: Пользователь прочитал сообщения
socket.on("markMessagesAsRead", async ({ conversationId, userId }) => {
console.log(`[Socket.IO] User ${userId} marked messages as read in conversation ${conversationId}`);
try {
// 1. Найти все сообщения в диалоге, отправленные НЕ userId, и где userId еще нет в readBy
const result = await Message.updateMany(
{
conversationId: conversationId,
sender: { $ne: userId }, // Сообщения от другого пользователя
readBy: { $ne: userId } // Которые текущий пользователь еще не читал (его ID нет в readBy)
},
{
2025-05-24 02:30:51 +07:00
$addToSet: { readBy: userId }, // Добавить ID текущего пользователя в массив readBy
$set: { status: 'read' } // Обновляем статус сообщения на "прочитано"
2025-05-21 22:13:09 +07:00
}
);
console.log(`[Socket.IO] Messages updated for read status: ${result.modifiedCount} in conversation ${conversationId} by user ${userId}`);
if (result.modifiedCount > 0) {
// 2. Найти другого участника диалога, чтобы уведомить его
const conversation = await Conversation.findById(conversationId);
if (!conversation) {
console.log(`[Socket.IO] Conversation ${conversationId} not found for sending read notification.`);
return;
}
const otherParticipantId = conversation.participants.find(pId => pId.toString() !== userId);
if (otherParticipantId) {
const otherUserSocket = getUserSocket(otherParticipantId.toString());
if (otherUserSocket && otherUserSocket.socketId) {
io.to(otherUserSocket.socketId).emit("messagesRead", {
conversationId: conversationId,
2025-05-24 02:30:51 +07:00
readerId: userId, // ID пользователя, который прочитал сообщения
status: 'read' // Добавляем статус "прочитано"
2025-05-21 22:13:09 +07:00
});
console.log(`[Socket.IO] Sent 'messagesRead' event to ${otherParticipantId} for conversation ${conversationId}`);
} else {
console.log(`[Socket.IO] Other participant ${otherParticipantId} for messagesRead notification is not online or socket not found.`);
}
}
}
} catch (error) {
console.error('[Socket.IO] Error in markMessagesAsRead:', error.message, error.stack);
// Можно отправить ошибку обратно клиенту, если это необходимо
// socket.emit('markMessagesAsReadError', { conversationId, message: 'Failed to mark messages as read' });
}
});
// Пользователь печатает
socket.on("typing", ({ conversationId, receiverId, senderId }) => { // Добавлен senderId, conversationId
const receiver = activeUsers.find(user => user.userId === receiverId);
if (receiver) {
io.to(receiver.socketId).emit("userTyping", { conversationId, senderId }); // Добавлен conversationId
}
});
// Пользователь перестал печатать
socket.on("stopTyping", ({ conversationId, receiverId, senderId }) => { // Добавлен senderId, conversationId
const receiver = activeUsers.find(user => user.userId === receiverId);
if (receiver) {
io.to(receiver.socketId).emit("userStopTyping", { conversationId, senderId }); // Добавлен conversationId
}
});
// Отключение пользователя
socket.on("disconnect", () => {
activeUsers = activeUsers.filter(user => user.socketId !== socket.id);
io.emit("getUsers", activeUsers); // Обновляем список у всех клиентов
console.log("User disconnected:", socket.id);
console.log("Active users:", activeUsers);
});
});
const errorHandler = (err, req, res, next) => {
console.error('--- ERROR HANDLER ---');
console.error('Name:', err.name);
console.error('Message:', err.message);
console.error('StatusCode on err object:', err.statusCode);
console.error('Current res.statusCode:', res.statusCode);
// console.error('Stack:', err.stack);
let finalStatusCode;
let finalMessage = err.message || 'Произошла ошибка на сервере';
// Приоритетная обработка специфических ошибок Mongoose
if (err.name === 'CastError') {
finalStatusCode = 400; // Или 404, если считаешь, что неверный ID = "не найдено"
finalMessage = `Неверный формат ID ресурса: ${err.path} со значением '${err.value}' не является валидным ObjectId.`;
console.log(`Mongoose CastError detected. Setting status to ${finalStatusCode}`);
} else if (err.name === 'ValidationError') {
finalStatusCode = 400;
console.log(`Mongoose ValidationError detected. Setting status to ${finalStatusCode}`);
} else if (err.statusCode) {
finalStatusCode = err.statusCode;
console.log(`Error has statusCode property. Setting status to ${finalStatusCode}`);
} else {
finalStatusCode = 500; // Default to 500
console.log(`Defaulting status to ${finalStatusCode}`);
}
console.log(`[ERROR_HANDLER] Final StatusCode to be sent: ${finalStatusCode}`);
if (res.headersSent) {
console.error('[ERROR_HANDLER] Headers already sent.');
return;
}
res.status(finalStatusCode).json({
message: finalMessage,
stack: process.env.NODE_ENV === 'production' ? null : err.stack,
});
};
app.use(errorHandler);
const PORT = process.env.PORT || 5000;
// Запуск HTTP сервера вместо app.listen
server.listen(PORT, () => {
console.log(`Сервер запущен на порту ${PORT} и слушает WebSocket соединения`);
});