const express = require('express'); const helmet = require('helmet'); const { Message, User } = require('../models'); const { authenticateToken } = require('../middleware/auth'); const { uploadMessageImage } = require('../middleware/upload'); const logger = require('../utils/logger'); const { emitNewMessage, emitMessageRead } = require('../sockets/messageSocket'); const { Op } = require('sequelize'); const emailServices = require('../services/email'); const fs = require('fs'); const path = require('path'); const router = express.Router(); // Get all messages for the current user (inbox) router.get('/', authenticateToken, async (req, res) => { try { const messages = await Message.findAll({ where: { receiverId: req.user.id }, include: [ { model: User, as: 'sender', attributes: ['id', 'firstName', 'lastName', 'profileImage'] } ], order: [['createdAt', 'DESC']] }); const reqLogger = logger.withRequestId(req.id); reqLogger.info("Messages inbox fetched", { userId: req.user.id, messageCount: messages.length }); res.json(messages); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Messages inbox fetch failed", { error: error.message, stack: error.stack, userId: req.user.id }); res.status(500).json({ error: error.message }); } }); // Get conversations grouped by user pairs router.get('/conversations', authenticateToken, async (req, res) => { try { const userId = req.user.id; // Fetch all messages where user is sender or receiver const allMessages = await Message.findAll({ where: { [Op.or]: [ { senderId: userId }, { receiverId: userId } ] }, include: [ { model: User, as: 'sender', attributes: ['id', 'firstName', 'lastName', 'profileImage'] }, { model: User, as: 'receiver', attributes: ['id', 'firstName', 'lastName', 'profileImage'] } ], order: [['createdAt', 'DESC']] }); // Group messages by conversation partner const conversationsMap = new Map(); allMessages.forEach(message => { // Determine the conversation partner const partnerId = message.senderId === userId ? message.receiverId : message.senderId; const partner = message.senderId === userId ? message.receiver : message.sender; if (!conversationsMap.has(partnerId)) { conversationsMap.set(partnerId, { partnerId, partner: partner ? { id: partner.id, firstName: partner.firstName, lastName: partner.lastName, profileImage: partner.profileImage } : null, lastMessage: null, lastMessageAt: null, unreadCount: 0 }); } const conversation = conversationsMap.get(partnerId); // Count unread messages (only those received by current user) if (message.receiverId === userId && !message.isRead) { conversation.unreadCount++; } // Keep the most recent message (messages are already sorted DESC) if (!conversation.lastMessage) { conversation.lastMessage = { id: message.id, content: message.content, senderId: message.senderId, createdAt: message.createdAt, isRead: message.isRead }; conversation.lastMessageAt = message.createdAt; } }); // Convert to array and sort by most recent message first const conversations = Array.from(conversationsMap.values()) .filter(conv => conv.partner !== null) // Filter out conversations with deleted users .sort((a, b) => new Date(b.lastMessageAt) - new Date(a.lastMessageAt)); const reqLogger = logger.withRequestId(req.id); reqLogger.info("Conversations fetched", { userId: req.user.id, conversationCount: conversations.length }); res.json(conversations); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Conversations fetch failed", { error: error.message, stack: error.stack, userId: req.user.id }); res.status(500).json({ error: error.message }); } }); // Get sent messages router.get('/sent', authenticateToken, async (req, res) => { try { const messages = await Message.findAll({ where: { senderId: req.user.id }, include: [ { model: User, as: 'receiver', attributes: ['id', 'firstName', 'lastName', 'profileImage'] } ], order: [['createdAt', 'DESC']] }); const reqLogger = logger.withRequestId(req.id); reqLogger.info("Sent messages fetched", { userId: req.user.id, messageCount: messages.length }); res.json(messages); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Sent messages fetch failed", { error: error.message, stack: error.stack, userId: req.user.id }); res.status(500).json({ error: error.message }); } }); // Get a single message router.get('/:id', authenticateToken, async (req, res) => { try { const message = await Message.findOne({ where: { id: req.params.id, [require('sequelize').Op.or]: [ { senderId: req.user.id }, { receiverId: req.user.id } ] }, include: [ { model: User, as: 'sender', attributes: ['id', 'firstName', 'lastName', 'profileImage'] }, { model: User, as: 'receiver', attributes: ['id', 'firstName', 'lastName', 'profileImage'] } ] }); if (!message) { return res.status(404).json({ error: 'Message not found' }); } // Mark as read if user is the receiver const wasUnread = message.receiverId === req.user.id && !message.isRead; if (wasUnread) { await message.update({ isRead: true }); // Emit socket event to sender for real-time read receipt const io = req.app.get('io'); if (io) { emitMessageRead(io, message.senderId, { messageId: message.id, readAt: new Date().toISOString(), readBy: req.user.id }); } } const reqLogger = logger.withRequestId(req.id); reqLogger.info("Message fetched", { userId: req.user.id, messageId: req.params.id, markedAsRead: wasUnread }); res.json(message); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Message fetch failed", { error: error.message, stack: error.stack, userId: req.user.id, messageId: req.params.id }); res.status(500).json({ error: error.message }); } }); // Send a new message router.post('/', authenticateToken, uploadMessageImage, async (req, res) => { try { const { receiverId, content } = req.body; // Check if receiver exists const receiver = await User.findByPk(receiverId); if (!receiver) { return res.status(404).json({ error: 'Receiver not found' }); } // Prevent sending messages to self if (receiverId === req.user.id) { return res.status(400).json({ error: 'Cannot send messages to yourself' }); } // Extract image filename if uploaded const imagePath = req.file ? req.file.filename : null; const message = await Message.create({ senderId: req.user.id, receiverId, content, imagePath }); const messageWithSender = await Message.findByPk(message.id, { include: [{ model: User, as: 'sender', attributes: ['id', 'firstName', 'lastName', 'profileImage'] }] }); // Emit socket event to receiver for real-time notification const io = req.app.get('io'); if (io) { emitNewMessage(io, receiverId, messageWithSender.toJSON()); } // Send email notification to receiver try { const sender = await User.findByPk(req.user.id, { attributes: ['id', 'firstName', 'lastName', 'email'] }); await emailServices.messaging.sendNewMessageNotification(receiver, sender, message); } catch (emailError) { // Log email error but don't block the message send const reqLogger = logger.withRequestId(req.id); reqLogger.error("Failed to send message notification email", { error: emailError.message, messageId: message.id, receiverId: receiverId }); } const reqLogger = logger.withRequestId(req.id); reqLogger.info("Message sent", { senderId: req.user.id, receiverId: receiverId, messageId: message.id }); res.status(201).json(messageWithSender); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Message send failed", { error: error.message, stack: error.stack, senderId: req.user.id, receiverId: req.body.receiverId }); res.status(500).json({ error: error.message }); } }); // Mark message as read router.put('/:id/read', authenticateToken, async (req, res) => { try { const message = await Message.findOne({ where: { id: req.params.id, receiverId: req.user.id } }); if (!message) { return res.status(404).json({ error: 'Message not found' }); } await message.update({ isRead: true }); // Emit socket event to sender for real-time read receipt const io = req.app.get('io'); if (io) { emitMessageRead(io, message.senderId, { messageId: message.id, readAt: new Date().toISOString(), readBy: req.user.id }); } const reqLogger = logger.withRequestId(req.id); reqLogger.info("Message marked as read", { userId: req.user.id, messageId: req.params.id }); res.json(message); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Message mark as read failed", { error: error.message, stack: error.stack, userId: req.user.id, messageId: req.params.id }); res.status(500).json({ error: error.message }); } }); // Get unread message count router.get('/unread/count', authenticateToken, async (req, res) => { try { const count = await Message.count({ where: { receiverId: req.user.id, isRead: false } }); const reqLogger = logger.withRequestId(req.id); reqLogger.info("Unread message count fetched", { userId: req.user.id, unreadCount: count }); res.json({ count }); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error("Unread message count fetch failed", { error: error.message, stack: error.stack, userId: req.user.id }); res.status(500).json({ error: error.message }); } }); // Get message image (authorized) router.get('/images/:filename', authenticateToken, // Override Helmet's CORP header for cross-origin image loading helmet.crossOriginResourcePolicy({ policy: "cross-origin" }), async (req, res) => { try { const { filename } = req.params; // Verify user is sender or receiver of a message with this image const message = await Message.findOne({ where: { imagePath: filename, [Op.or]: [ { senderId: req.user.id }, { receiverId: req.user.id } ] } }); if (!message) { const reqLogger = logger.withRequestId(req.id); reqLogger.warn('Unauthorized image access attempt', { userId: req.user.id, filename }); return res.status(403).json({ error: 'Access denied' }); } // Serve the image const filePath = path.join(__dirname, '../uploads/messages', filename); if (!fs.existsSync(filePath)) { return res.status(404).json({ error: 'Image not found' }); } res.sendFile(filePath); } catch (error) { const reqLogger = logger.withRequestId(req.id); reqLogger.error('Image serve failed', { error: error.message, filename: req.params.filename }); res.status(500).json({ error: 'Failed to load image' }); } }); module.exports = router;