Files
rentall-app/backend/routes/messages.js
2025-11-10 13:05:10 -05:00

392 lines
11 KiB
JavaScript

const express = require('express');
const { Message, User } = require('../models');
const { authenticateToken } = require('../middleware/auth');
const logger = require('../utils/logger');
const { emitNewMessage, emitMessageRead } = require('../sockets/messageSocket');
const { Op } = require('sequelize');
const emailService = require('../services/emailService');
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 with replies
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']
},
{
model: Message,
as: 'replies',
include: [{
model: User,
as: 'sender',
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, async (req, res) => {
try {
const { receiverId, subject, content, parentMessageId } = 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' });
}
const message = await Message.create({
senderId: req.user.id,
receiverId,
subject,
content,
parentMessageId
});
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 emailService.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,
isReply: !!parentMessageId
});
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 });
}
});
module.exports = router;