395 lines
11 KiB
JavaScript
395 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 emailServices = require('../services/email');
|
|
const { validateS3Keys } = require('../utils/s3KeyValidator');
|
|
const { IMAGE_LIMITS } = require('../config/imageLimits');
|
|
const router = express.Router();
|
|
|
|
// Get all messages for the current user (inbox)
|
|
router.get('/', authenticateToken, async (req, res, next) => {
|
|
try {
|
|
const messages = await Message.findAll({
|
|
where: { receiverId: req.user.id },
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: 'sender',
|
|
attributes: ['id', 'firstName', 'lastName', 'imageFilename']
|
|
}
|
|
],
|
|
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
|
|
});
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Get conversations grouped by user pairs
|
|
router.get('/conversations', authenticateToken, async (req, res, next) => {
|
|
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', 'imageFilename']
|
|
},
|
|
{
|
|
model: User,
|
|
as: 'receiver',
|
|
attributes: ['id', 'firstName', 'lastName', 'imageFilename']
|
|
}
|
|
],
|
|
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
|
|
});
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Get sent messages
|
|
router.get('/sent', authenticateToken, async (req, res, next) => {
|
|
try {
|
|
const messages = await Message.findAll({
|
|
where: { senderId: req.user.id },
|
|
include: [
|
|
{
|
|
model: User,
|
|
as: 'receiver',
|
|
attributes: ['id', 'firstName', 'lastName', 'imageFilename']
|
|
}
|
|
],
|
|
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
|
|
});
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Get a single message
|
|
router.get('/:id', authenticateToken, async (req, res, next) => {
|
|
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', 'imageFilename']
|
|
},
|
|
{
|
|
model: User,
|
|
as: 'receiver',
|
|
attributes: ['id', 'firstName', 'lastName', 'imageFilename']
|
|
}
|
|
]
|
|
});
|
|
|
|
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
|
|
});
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Send a new message
|
|
router.post('/', authenticateToken, async (req, res, next) => {
|
|
try {
|
|
const { receiverId, content, imageFilename } = req.body;
|
|
|
|
// Validate imageFilename if provided
|
|
if (imageFilename) {
|
|
const keyValidation = validateS3Keys([imageFilename], 'messages', { maxKeys: IMAGE_LIMITS.messages });
|
|
if (!keyValidation.valid) {
|
|
return res.status(400).json({
|
|
error: keyValidation.error,
|
|
details: keyValidation.invalidKeys
|
|
});
|
|
}
|
|
}
|
|
|
|
// 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,
|
|
content,
|
|
imageFilename: imageFilename || null
|
|
});
|
|
|
|
const messageWithSender = await Message.findByPk(message.id, {
|
|
include: [{
|
|
model: User,
|
|
as: 'sender',
|
|
attributes: ['id', 'firstName', 'lastName', 'imageFilename']
|
|
}]
|
|
});
|
|
|
|
// 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,
|
|
stack: emailError.stack,
|
|
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
|
|
});
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Mark message as read
|
|
router.put('/:id/read', authenticateToken, async (req, res, next) => {
|
|
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
|
|
});
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Get unread message count
|
|
router.get('/unread/count', authenticateToken, async (req, res, next) => {
|
|
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
|
|
});
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
module.exports = router; |