Reflex/backend/server.js
2025-05-26 12:05:51 +07:00

339 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 adminRoutes = require('./routes/adminRoutes'); // Импортируем маршруты админа
const reportRoutes = require('./routes/reportRoutes'); // Импортируем маршруты для жалоб
const Message = require('./models/Message'); // Импорт модели Message
const Conversation = require('./models/Conversation'); // Импорт модели Conversation
const initAdminAccount = require('./utils/initAdmin'); // Импорт инициализации админ-аккаунта
// Загружаем переменные окружения до их использования
// 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);
// Подключение к базе данных - проверяем, что импортировали функцию
if (typeof connectDBModule === 'function') { try {
connectDBModule() // Вызов функции подключения
.then(async () => {
console.log('Успешное подключение к базе данных');
console.log('Инициализация административного аккаунта...');
// Инициализируем админ-аккаунт после подключения к БД
try {
await initAdminAccount();
console.log('Инициализация администратора завершена');
} catch (adminError) {
console.error('Ошибка при инициализации административного аккаунта:', adminError);
}
})
.catch(err => {
console.error('Ошибка при подключении к базе данных:', err);
});
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 работает!');
});
// Подключаем маршруты
app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes); // 2. Подключаем userRoutes с префиксом /api/users
app.use('/api/actions', actionRoutes);
app.use('/api/conversations', conversationRoutes);
app.use('/api/reports', reportRoutes); // Подключаем маршруты для жалоб
app.use('/api/admin', adminRoutes); // Подключаем маршруты для админ-панели
// 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}`);
});
// Отправка сообщения
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,
status: 'sending' // Начальный статус - отправка
});
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,
status: populatedMessage.status
};
// После успешного сохранения в БД меняем статус на "delivered"
await Message.findByIdAndUpdate(newMessage._id, { status: 'delivered' });
messageToSend.status = 'delivered'; // Убедимся, что статус установлен явно
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)
},
{
$addToSet: { readBy: userId }, // Добавить ID текущего пользователя в массив readBy
$set: { status: 'read' } // Обновляем статус сообщения на "прочитано"
}
);
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,
readerId: userId, // ID пользователя, который прочитал сообщения
status: 'read' // Добавляем статус "прочитано"
});
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 соединения`);
});