// auth controller const express = require('express'); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const { sendResponse, HttpsStatus } = require('../../utils/response'); const { generateAccessToken, generateRefreshToken, expiryDateFromNow} = require('../../utils/tokens'); const { User, RefreshToken, Organization, sequelize, SharedFile, UserDevice } = require('../../models'); const { verifyRefreshToken } = require('../../utils/tokens'); const { Op } = require('sequelize'); const path = require('path'); const fs = require('fs'); const crypto = require('crypto'); const sendEmail = require('../../utils/sendEmail'); const EVENTS = require('../../utils/socketEvents'); // exports.refreshToken = async (req, res) =>{ // try{ // const { refreshToken } = req.body; // if(!refreshToken){ // return sendResponse(res, HttpsStatus.BAD_REQUEST, false, 'Refresh token required', null, { refreshToken: 'Missing' }); // } // let payload; // try{ // payload = verifyRefreshToken(refreshToken); // }catch(err){ // return sendResponse(res, HttpsStatus.UNAUTHORIZED, false, 'Invalid refresh token', null, { refreshToken: 'Invalid or expired' }); // } // const stored = await RefreshToken.findOne({ where: { user_id: payload.id, token: refreshToken }}); // if(!stored){ // return sendResponse(res, HttpsStatus.UNAUTHORIZED, false, 'Refresh token not recognized', null, { refreshToken: 'Not found' }); // } // if(stored.expires_at && new Date(stored.expires_at) < new Date()){ // await RefreshToken.destroy({ where: { id: stored.id }}); // return sendResponse(res, HttpsStatus.UNAUTHORIZED, false, 'Refresh token expired', null, { refreshToken: 'Expired' }); // } // const user = await User.findByPk(payload.id); // if(!user){ // return sendResponse(res, HttpsStatus.UNAUTHORIZED, false, 'User not found', null, { user: 'Not found' }); // } // const newAccessToken = generateAccessToken({ id: user.id, email: user.email }); // const newRefreshToken = generateRefreshToken({ id: user.id, email: user.email }); // await RefreshToken.update({ token: newRefreshToken, expires_at: expiryDateFromNow()}, { where: {id: stored.id }}); // return sendResponse(res, HttpsStatus.OK, true, 'Token refreshed', { accessToken: newAccessToken, refreshToken: newRefreshToken }); // }catch(err){ // console.error('refreshToken error:', err); // return sendResponse(res, HttpsStatus.INTERNAL_SERVER_ERROR, false, 'Server error', null, { server: err.message }); // } // }; // exports.adminRegister = async (req,res) => { // try{ // const { // organization_name, // employee_size, // website, // full_name, // role = 'admin', // designation, // phone, // email, // password // } = req.body; // const file = req.file; // const errors = {}; // if(!organization_name){ // errors.organization_name = 'Organization name is required'; // } // if(!employee_size){ // errors.employee_size = 'Employee size is required'; // } // if(!full_name){ // errors.full_name = 'Full name is required'; // } // if(!designation){ // errors.designation = 'Job role is required'; // } // if(!email){ // errors.email = 'Email is required'; // } // if(!password){ // errors.password = 'Password is required'; // } // if(Object.keys(errors).length > 0){ // return sendResponse(res, HttpsStatus.BAD_REQUEST, false, 'Missing fields', null, errors); // } // const existingEmail = await User.findOne({ where: { email } }); // if(existingEmail){ // return sendResponse(res, HttpsStatus.BAD_REQUEST, false, 'Email already exists!'); // } // const hashedPassword = await bcrypt.hash(password, 10); // const t = await sequelize.transaction(); // try{ // const organization = await Organization.create({'name': organization_name, employee_size, website }, { transaction: t }) // const user = await User.create({ // full_name, // email, // phone, // role, // 'password': hashedPassword, // 'organization_id': organization.id, // designation // }, // { transaction: t}); // const defaultFileUrl = '/uploads/default/profile_pic.jpg'; // const defaultFilePath = path.join(__dirname,'../../uploads/default/profile_pic.jpg'); // if (fs.existsSync(defaultFilePath)) { // const stats = fs.statSync(defaultFilePath); // await SharedFile.create( // { // user_id: user.id, // file_name: 'profile_pic.jpg', // file_url: defaultFileUrl, // file_type: 'image', // file_size: stats.size, // mime_type: 'image/jpeg', // }, // { // transaction: t, // } // ); // } // await t.commit(); // return sendResponse(res, HttpsStatus.CREATED, true, 'User created successfully!',user); // }catch(err){ // await t.rollback(); // return sendResponse(res, HttpsStatus.INTERNAL_SERVER_ERROR, false, 'Server error!', null, { server: err.message }); // } // }catch(err){ // return sendResponse(res, HttpsStatus.INTERNAL_SERVER_ERROR, false, 'Server error!', null, { server: err.message }); // } // }; // exports.userRegister = async (req,res) => { // try{ // const { // full_name, // email, // password, // phone, // role = 'member', // designation, // } = req.body; // const errors = {}; // if(!full_name){ // errors.full_name = 'Full name is required'; // } // if(!designation){ // errors.designation = 'Designation is required'; // } // if(!email){ // errors.email = 'Email is required'; // } // if(!password){ // errors.password = 'Password is required'; // } // if(Object.keys(errors).length > 0){ // return sendResponse(res, HttpsStatus.BAD_REQUEST, false, 'Missing fields', null, errors); // } // // console.log('file path' ,path.join(__dirname,'../../uploads/default/profile_pic.jpg')) // // console.log('if condition',fs.existsSync(defaultFilePath)) // const existingEmail = await User.findOne({ where: { email } }); // if(existingEmail){ // return sendResponse(res, HttpsStatus.BAD_REQUEST, false, 'Email already exists!'); // } // const hashedPassword = await bcrypt.hash(password, 10); // const t = await sequelize.transaction(); // try{ // // const organization = await Organization.create({'name': organization_name, employee_size, website }, { transaction: t }) // const user = await User.create({ // full_name, // email, // phone, // role, // 'password': hashedPassword, // designation // }, // { transaction: t}); // const defaultFileUrl = '/uploads/default/profile_pic.jpg'; // const defaultFilePath = path.join(__dirname,'../../uploads/default/profile_pic.jpg'); // if (fs.existsSync(defaultFilePath)) { // const stats = fs.statSync(defaultFilePath); // await SharedFile.create( // { // user_id: user.id, // file_name: 'profile_pic.jpg', // file_url: defaultFileUrl, // file_type: 'image', // file_size: stats.size, // mime_type: 'image/jpeg', // }, // { // transaction: t, // } // ); // } // await t.commit(); // return sendResponse(res, HttpsStatus.CREATED, true, 'User created successfully!',user); // }catch(err){ // console.log(err); // await t.rollback(); // return sendResponse(res, HttpsStatus.INTERNAL_SERVER_ERROR, false, 'Server error!', null, { server: err.message }); // } // }catch(err){ // return sendResponse(res, HttpsStatus.INTERNAL_SERVER_ERROR, false, 'Server error!', null, { server: err.message }); // } // }; exports.login = async (req, res) => { try{ const { email, password, device_id, device_type, fcm_token, force_login = false } = req.body const errors = {}; if(!email){ errors.email = 'Email is required'; } if(!password){ errors.password = 'Password is required'; } if(!device_id){ errors.device_id = 'Device id is required'; } if(!device_type){ errors.device_type = 'Device type is required'; } if(!fcm_token){ errors.fcm_token = 'FCM token is required'; } if(Object.keys(errors).length > 0){ return sendResponse(res, HttpsStatus.BAD_REQUEST, false, 'Validation failed!', null, errors); } const user = await User.findOne({ where: { email }}); if(!user){ return sendResponse(res, HttpsStatus.BAD_REQUEST, false, 'Invalid credentials!', null, {email: 'Invalid credentials!'}); } let matchPassword = await bcrypt.compare(password, user.password); if(!user || !matchPassword){ return sendResponse(res, HttpsStatus.BAD_REQUEST, false, 'Invalid credentials!'); } const existingSession = await UserDevice.findOne({ where: { user_id: user.id, is_active: true } }); if(existingSession && existingSession.device_id !== device_id && !force_login){ return sendResponse(res, HttpsStatus.FORBIDDEN, false, 'User already logged in another device', { already_logged_in: true }); } const io = req.app.get('io'); if(force_login && existingSession){ /** * 1️⃣ Get previously active devices except current device */ const previousDevices = await UserDevice.findAll({ where: { user_id: user.id, is_active: true, device_id: { [Op.ne]: device_id } } }); /** * 2️⃣ Destroy refresh tokens of previous devices */ await RefreshToken.destroy({ where: { user_id: user.id, device_id: { [Op.ne]: device_id } } }); /** * 3️⃣ Deactivate previous devices */ await UserDevice.update( { is_active: false }, { where: { user_id: user.id, device_id: { [Op.ne]: device_id } } } ); /** * 4️⃣ Notify previous devices */ // for (const device of previousDevices) { // await notifyUser(io, { // recipient_id: user.id, // type: 'security', // event: EVENTS.FORCE_LOGOUT, // title: 'Logged out from another device', // body: 'Your account was logged in from another device.' // }); // } } const payload = {id: user.id, email: user.email}; const accessToken = generateAccessToken(payload); const refreshToken = generateRefreshToken(payload); // const t = await sequelize.transaction(); // try{ // await RefreshToken.destroy({where: { user_id: user.id }, transaction: t }); await RefreshToken.create({ user_id: user.id, token: refreshToken, device_id, expires_at: expiryDateFromNow() }); await UserDevice.upsert({ user_id: user.id, device_id, device_type, fcm_token, is_active: true, last_seen_at: new Date() }); const orgIds = [ user.organization_id, user.org_2, user.org_3, user.org_4, user.org_5, user.org_6, user.org_7, user.org_8, user.org_9, user.org_10, ].filter(Boolean); const organizations = await Organization.findAll({ where: { id: orgIds }, attributes: ["id", "name"], }); // await t.commit(); return sendResponse(res, HttpsStatus.OK, true, 'Login successful', {accessToken,refreshToken, user: {...user.toJSON(),organizations } }); // }catch(err){ // await t.rollback(); // return sendResponse(res, HttpsStatus.INTERNAL_SERVER_ERROR, false, 'Server error!', null, { server: err.message }); // } }catch(err){ return sendResponse(res, HttpsStatus.INTERNAL_SERVER_ERROR, false, 'Server error!', null, { server: err.message }); } }; exports.logout = async (req, res) => { try { // const { device_id } = req.body; const device_id = req.device_id; if (!device_id) { return sendResponse( res, HttpsStatus.BAD_REQUEST, false, 'Device id is required' ); } const session = await RefreshToken.findOne({ where: { device_id } }); if(!session){ return sendResponse( res, HttpsStatus.BAD_REQUEST, false, 'User session not found!' ); } await RefreshToken.destroy({ where: { device_id: device_id } }); await UserDevice.update( { is_active: false }, { where: { device_id } } ); return sendResponse( res, HttpsStatus.OK, true, 'Logged out successfully' ); } catch (err) { return sendResponse( res, HttpsStatus.INTERNAL_SERVER_ERROR, false, 'Server error!', null, { server: err.message } ); } }; // exports.logoutFromAllDevice = async (req, res) => { // try { // const userId = req.user.id; // await RefreshToken.destroy({ // where: { // user_id: userId // } // }); // return sendResponse(res, HttpsStatus.OK, true, 'Logged out from all devices successfull!'); // } catch (err) { // return sendResponse(res, HttpsStatus.INTERNAL_SERVER_ERROR, false, 'Server error!', null, { server: err.message } ); // } // } exports.requestPasswordOtp = async (req, res) => { try { const { email } = req.body; if(!email){ return sendResponse(res, HttpsStatus.BAD_REQUEST, false, 'Mail is required!'); } const user = await User.findOne({ where: { email } }); if(!user){ return sendResponse(res, HttpsStatus.BAD_REQUEST, false, 'Mail not found!'); } const otp = Math.floor(100000 + Math.random() * 900000).toString(); const hashedOtp = crypto.createHash('sha256').update(otp).digest('hex'); const expiry = new Date(Date.now() + 10 * 60 * 1000); await user.update({ reset_password_otp: hashedOtp, reset_password_expiry: expiry }); await sendEmail({ to: user.email, subject: "Your Password Reset OTP", text: `Your OTP for password reset is ${otp}. It expires in 10 minutes.`, html: `

Your OTP for password reset is ${otp}. It expires in 10 minutes.

` }); return sendResponse(res, HttpsStatus.OK, true, 'Please check your mail for the OTP!'); } catch (err) { console.log('errr --- ',err); return sendResponse(res, HttpsStatus.INTERNAL_SERVER_ERROR, false, 'Server error!', null, { server: err.message }); } }; exports.verifyOtp = async (req, res) => { try { const { email, otp } = req.body; const errors = {}; if (!email) errors.email = "Email is required!"; if (!otp) errors.otp = "OTP is required!"; if (Object.keys(errors).length > 0) { return sendResponse(res, HttpsStatus.BAD_REQUEST, false, "Missing fields!", null, errors); } // Find the user by email const user = await User.findOne({ where: { email } }); if (!user || !user.reset_password_otp || !user.reset_password_expiry) { return sendResponse(res, HttpsStatus.BAD_REQUEST, false, "Invalid OTP!"); } // Check if OTP expired const currentTime = new Date(); if (currentTime > new Date(user.reset_password_expiry)) { return sendResponse(res, HttpsStatus.BAD_REQUEST, false, "OTP expired!"); } // Hash received OTP const hashedOtp = crypto.createHash("sha256").update(otp).digest("hex"); if (hashedOtp !== user.reset_password_otp) { return sendResponse(res, HttpsStatus.BAD_REQUEST, false, "Invalid OTP!"); } // Clear OTP so it can’t be reused await user.update({ reset_password_otp: null, reset_password_expiry: null }); return sendResponse(res, HttpsStatus.OK, true, "OTP verified successfully!"); } catch (err) { console.error("verifyOtp error:", err); return sendResponse( res, HttpsStatus.INTERNAL_SERVER_ERROR, false, "Server error!", null, { server: err.message } ); } }; exports.updatePassword = async (req, res) => { try { const { email, password, confirmPassword } = req.body; const errors = {}; if(!email){ errors.email = 'Email is required'; } if(!password){ errors.password = 'Password is required'; } if(!confirmPassword){ errors.confirmPassword = 'Confirm Password is required'; } if(Object.keys(errors).length > 0){ return sendResponse(res, HttpsStatus.BAD_REQUEST, false, 'Missing fields!', null, errors); } const user = await User.findUserByEmail(email); if(!user){ return sendResponse(res, HttpsStatus.BAD_REQUEST, false, 'Invalid email!'); } if(password !== confirmPassword){ return sendResponse(res, HttpsStatus.BAD_REQUEST, false, 'Password mismatch!'); } const hashedPassword = await bcrypt.hash(password, 10); await User.update({ password: hashedPassword }, { where: { id: user.id } }); return sendResponse(res, HttpsStatus.OK, true, 'Password change!', null, { password: 'Password changed successfully' }); }catch(err){ return sendResponse(res, HttpsStatus.INTERNAL_SERVER_ERROR, false, 'Something went wrong!', null, { server: err.message }); } }; // chat controller const express = require('express'); const { sequelize, Chat, ChatMember, Message, MessageStatus, User, SharedFile } = require('../../models'); const { sendResponse, HttpsStatus } = require('../../utils/response'); const { getOnlineUsers } = require('../../utils/onlineUsersRedis'); const { Op } = require('sequelize'); const EVENTS = require('../../utils/socketEvents'); const { notifyUser } = require('../../utils/notificationService'); const path = require('path'); const fs = require('fs'); // exports.createPrivateChat = async (req, res) => { // const t = await sequelize.transaction(); // try{ // const { user_id } = req.body; // const currentUserId = req.user.id; // const org_id = req.org_id; // const io = req.app.get('io'); // if (!user_id) { // await t.rollback(); // return sendResponse(res, HttpsStatus.BAD_REQUEST, false, 'User id is required!'); // } // if (user_id == currentUserId) { // await t.rollback(); // return sendResponse(res, HttpsStatus.BAD_REQUEST, false, 'You cannot create a private chat with yourself!'); // } // const targetUser = await User.findOne({ // where: { id: user_id, is_deleted: false } // }); // if (!targetUser) { // await t.rollback(); // return sendResponse(res, HttpsStatus.NOT_FOUND, false, 'User not found in organization!'); // } // const existingChat = await Chat.findOne({ // where: { type: 'private', organization_id: org_id }, // include: [ // { // model: ChatMember, // as: 'memberships', // required: true, // forces Inner Join // where: { // user_id: { // [Op.in]: [user_id, currentUserId] // } // }, // attributes: [] // } // ], // group: ['Chat.id'], // having: sequelize.literal(`COUNT(DISTINCT "memberships"."user_id") = 2`), // subQuery: false // prEVENTSs sequelize from breaking group by in findone // }); // if(existingChat){ // await t.rollback(); // return sendResponse(res, HttpsStatus.BAD_REQUEST, false, 'Private chat already exists!'); // } // const chat = await Chat.create({type: 'private', created_by: currentUserId, organization_id: org_id}, { transaction: t }); // const chatMember = await ChatMember.bulkCreate([ // { chat_id: chat.id, user_id: currentUserId}, // { chat_id: chat.id, user_id} // ], // { transaction: t }); // await t.commit(); // const users = [currentUserId, user_id]; // const chatPayload = { // id: chat.id, // type: chat.type, // created_by: currentUserId, // members: users, // last_message: null, // unread_count: 0, // created_at: chat.createdAt, // }; // for(const uid of users){ // // const room = io.sockets.adapter.rooms.get(`user_${uid}`); // // const isOnline = room && room.size > 0; // // if(isOnline){ // io.to(`user_${uid}`).emit(EVENTS.CHAT_CREATED, chatPayload); // io.to(`user_${uid}`).emit(EVENTS.CHAT_LIST_UPDATE, { // action: 'new_chat', // data: chatPayload // }); // // }else{ // if (uid === currentUserId) continue; // await notifyUser(io, { // recipient_id: uid, // sender_id: currentUserId, // chat_id: chat.id, // type: "chat", // event: EVENTS.NOTIFICATION, // title: "New Chat Created", // body: "A private chat has been created with you" // }); // // } // } // // Emit chat created to both users // // io.to(`user_${currentUserId}`).emit(EVENTS.CHAT_CREATED, chatPayload); // // io.to(`user_${currentUserId}`).emit(EVENTS.CHAT_LIST_UPDATE, { // // action: "new_chat", // // chat: chatPayload // // }); // // // Check if other user online // // const otherRoom = io.sockets.adapter.rooms.get(`user_${user_id}`); // // const isOnline = otherRoom && otherRoom.size > 0; // // if (isOnline) { // // io.to(`user_${user_id}`).emit(EVENTS.CHAT_CREATED, chatPayload); // // io.to(`user_${user_id}`).emit(EVENTS.CHAT_LIST_UPDATE, { // // action: "new_chat", // // chat: chatPayload // // }); // // } else { // // await notifyUser(io, { // // recipient_id: user_id, // // sender_id: currentUserId, // // chat_id: chat.id, // // type: "chat", // // EVENTS: EVENTS.NOTIFICATION, // // title: "New Chat Created", // // body: "A private chat has been created with you" // // }); // // } // return sendResponse(res, HttpsStatus.CREATED, true, 'Private chat created successfully!', chatPayload) // // return sendResponse(res, HttpsStatus.CREATED, true, 'Private chat created successfully!', payload, null ) // }catch(err){ // if(!t.finished){ // await t.rollback(); // } // console.log('error',err) // return sendResponse(res, HttpsStatus.INTERNAL_SERVER_ERROR, false, 'Server error!', null, { server: err.message }); // } // } exports.createPrivateChat = async (req, res) => { const t = await sequelize.transaction(); try { const { user_id } = req.body; const currentUserId = req.user.id; const org_id = req.org_id; const io = req.app.get('io'); if (!user_id) { await t.rollback(); return sendResponse(res, HttpsStatus.BAD_REQUEST, false, 'User id is required!'); } if (user_id === currentUserId) { await t.rollback(); return sendResponse(res, HttpsStatus.BAD_REQUEST, false, 'You cannot create a private chat with yourself!'); } /** * Validate user belongs to same organization */ const targetUser = await User.findOne({ where: { id: user_id, is_deleted: false } }); if (!targetUser) { await t.rollback(); return sendResponse(res, HttpsStatus.NOT_FOUND, false, 'User not found!'); } /** * Check existing private chat */ const existingChat = await Chat.findOne({ where: { type: 'private', organization_id: org_id }, include: [ { model: ChatMember, as: 'memberships', where: { user_id: { [Op.in]: [user_id, currentUserId] } }, attributes: [] } ], group: ['Chat.id'], having: sequelize.literal(`COUNT(DISTINCT "memberships"."user_id") = 2`), subQuery: false }); if (existingChat) { await t.rollback(); return sendResponse(res, HttpsStatus.BAD_REQUEST, false, 'Private chat already exists!'); } /** * Create chat */ const chat = await Chat.create({ type: 'private', created_by: currentUserId, organization_id: org_id }, { transaction: t }); await ChatMember.bulkCreate([ { chat_id: chat.id, user_id: currentUserId }, { chat_id: chat.id, user_id } ], { transaction: t }); await t.commit(); const users = [currentUserId, user_id]; const chatPayload = { id: chat.id, type: 'private', created_by: currentUserId, members: users, created_at: chat.createdAt, last_message: null, unread_count: 0 }; for (const uid of users) { io.to(`user_${uid}`).emit(EVENTS.CHAT_CREATED, chatPayload); io.to(`user_${uid}`).emit(EVENTS.CHAT_LIST_UPDATE, { action: 'new_chat', data: chatPayload }); if (uid === currentUserId) continue; await notifyUser(io, { recipient_id: uid, sender_id: currentUserId, chat_id: chat.id, type: 'chat', event: EVENTS.NOTIFICATION, title: 'New Chat Created', body: 'A private chat has been created with you' }); } return sendResponse(res, HttpsStatus.CREATED, true, 'Private chat created successfully!', chatPayload); } catch (err) { if (!t.finished) await t.rollback(); return sendResponse(res, HttpsStatus.INTERNAL_SERVER_ERROR, false, 'Server error!', null, { server: err.message }); } }; // exports.createGroup = async (req, res) => { // const t = await sequelize.transaction(); // try{ // const { group_name, group_members, } = req.body; // const currentUserId = req.user.id; // const io =req.app.get('io'); // const errors = {}; // if(!group_name){ // errors.group_name = 'Group name is required'; // } // if(!Array.isArray(group_members)){ // errors.group_members = 'Chat members should be in array format'; // }else { // if(group_members.length < 2){ // errors.group_members = 'At least 2 chat member is required!'; // } // const uniqueMembers = new Set(group_members); // if(uniqueMembers.size !== group_members.length){ // errors.group_members = 'Duplicate user IDs are not allowed in group_members'; // } // if(group_members.includes(currentUserId)){ // errors.group_members = 'Admin user should not be included in group_members'; // } // } // if(Object.keys(errors).length > 0){ // await t.rollback(); // return sendResponse(res, HttpsStatus.BAD_REQUEST, false, 'Validation failed!', null, errors); // } // const chat = await Chat.create({ // type: 'group', // group_name, // created_by: currentUserId // }, // { // transaction: t // }); // const defaultFileUrl = '/uploads/default/group_image.png'; // const defaultFilePath = path.join(__dirname,'../../uploads/default/group_image.png'); // if (fs.existsSync(defaultFilePath)) { // const stats = fs.statSync(defaultFilePath); // await SharedFile.create( // { // chat_id: chat.id, // file_name: 'group_image.png', // file_url: defaultFileUrl, // file_type: 'image', // file_size: stats.size, // mime_type: 'image/jpeg', // }, // { // transaction: t, // } // ); // } // const groupMembers = group_members.map(user_id => ({ // chat_id: chat.id, // user_id, // role: 'member' // })); // groupMembers.push({ // chat_id: chat.id, // user_id: currentUserId, // role: 'admin' // }); // const chatMember = await ChatMember.bulkCreate(groupMembers, { transaction: t }); // await t.commit(); // const allMembers = [...group_members, currentUserId]; // const groupPayload = { // id: chat.id, // type: 'group', // group_name, // created_by: currentUserId, // members: allMembers, // created_at: chat.createdAt, // last_message: null, // unread_count: 0 // }; // for (const userId of allMembers) { // // const room = io.sockets.adapter.rooms.get(`user_${userId}`); // // const isOnline = room && room.size > 0; // // if (isOnline) { // io.to(`user_${userId}`).emit(EVENTS.CHAT_CREATED, groupPayload); // io.to(`user_${userId}`).emit(EVENTS.CHAT_LIST_UPDATE, { // action: "new_chat", // data: groupPayload // }); // // } else { // if (uid === currentUserId) continue; // await notifyUser(io, { // recipient_id: userId, // sender_id: currentUserId, // chat_id: chat.id, // type: 'group', // event: EVENTS.NOTIFICATION, // title: 'Added to Group', // body: `You were added to ${group_name}` // }); // // } // } // // const notifications = []; // // for (const userId of allMembers) { // // io.to(`user_${userId}`).emit(EVENTS.CHAT_CREATED, groupPayload); // // io.to(`user_${userId}`).emit(EVENTS.CHAT_LIST_UPDATE); // // if (userId !== currentUserId) { // // await notifyUser(io, { // // recipient_id: userId, // // sender_id: currentUserId, // // chat_id: chat.id, // // type: 'group', // // EVENTS: EVENTS.CHAT_CREATED, // // title: 'Added to Group', // // body: `You were added to ${group_name}` // // }); // // } // // } // // return sendResponse(res, HttpsStatus.CREATED, true, 'Group chat created successfully!',{chat: chat, notifications: notifications}, null ) // return sendResponse(res, HttpsStatus.CREATED, true, 'Group chat created successfully!', groupPayload, null ) // }catch(err){ // if (!t.finished) await t.rollback(); // console.error('Sequelize Error:', err); // return sendResponse(res, HttpsStatus.INTERNAL_SERVER_ERROR, false, 'Server error!', null, { server: err.message }); // } // } exports.createGroup = async (req, res) => { const t = await sequelize.transaction(); try { const { group_name, group_members } = req.body; const currentUserId = req.user.id; const org_id = req.org_id; const io = req.app.get('io'); const errors = {}; if (!group_name) errors.group_name = 'Group name is required'; if (!Array.isArray(group_members)) { errors.group_members = 'Group members must be array'; } if (group_members?.length < 2) { errors.group_members = 'At least 2 members required'; } if (Object.keys(errors).length > 0) { await t.rollback(); return sendResponse(res, HttpsStatus.BAD_REQUEST, false, 'Validation failed!', null, errors); } const chat = await Chat.create({ type: 'group', group_name, created_by: currentUserId, organization_id: org_id }, { transaction: t }); const members = group_members.map(uid => ({ chat_id: chat.id, user_id: uid, role: 'member' })); members.push({ chat_id: chat.id, user_id: currentUserId, role: 'admin' }); await ChatMember.bulkCreate(members, { transaction: t }); await t.commit(); const allMembers = [...group_members, currentUserId]; const payload = { id: chat.id, type: 'group', group_name, created_by: currentUserId, members: allMembers, created_at: chat.createdAt, last_message: null, unread_count: 0 }; for (const uid of allMembers) { io.to(`user_${uid}`).emit(EVENTS.CHAT_CREATED, payload); io.to(`user_${uid}`).emit(EVENTS.CHAT_LIST_UPDATE, { action: 'new_chat', data: payload }); if (uid === currentUserId) continue; await notifyUser(io, { recipient_id: uid, sender_id: currentUserId, chat_id: chat.id, type: 'group', event: EVENTS.NOTIFICATION, title: 'Added to Group', body: `You were added to ${group_name}` }); } return sendResponse(res, HttpsStatus.CREATED, true, 'Group chat created successfully!', payload); } catch (err) { if (!t.finished) await t.rollback(); return sendResponse(res, HttpsStatus.INTERNAL_SERVER_ERROR, false, 'Server error!', null, { server: err.message }); } }; exports.groupDetails = async (req, res) => { try { const { chat_id } = req.params; const user_id = req.user.id; const org_id = req.org_id; if (!chat_id) { return sendResponse( res, HttpsStatus.BAD_REQUEST, false, "Chat id is required" ); } /** * 1️⃣ Validate group belongs to organization */ const group = await Chat.findOne({ where: { id: chat_id, type: "group", organization_id: org_id, is_deleted: false }, attributes: [ "id", "group_name", "group_image", "created_by", ["created_at", "createdAt"] ], include: [ { model: ChatMember, as: "memberships", attributes: ["role", "joined_at", "muted"], include: [ { model: User, as: "user", attributes: [ "id", "full_name", "designation", "position", "is_online", "last_seen" ], include: [ { model: SharedFile, as: "uploadedFiles", attributes: ["file_url"], required: false, where: { file_type: "image" } } ] } ] }, { model: SharedFile, as: "files", attributes: [ "id", "file_name", "file_url", "file_type", "created_at" ], include: [ { model: User, as: "uploader", attributes: ["id", "full_name"], include: [ { model: SharedFile, as: "uploadedFiles", attributes: ["file_url"], required: false, where: { file_type: "image" } } ] } ] } ] }); if (!group) { return sendResponse( res, HttpsStatus.NOT_FOUND, false, "Group not found" ); } /** * 2️⃣ Check membership */ const isMember = await ChatMember.findOne({ where: { chat_id, user_id } }); if (!isMember) { return sendResponse( res, HttpsStatus.FORBIDDEN, false, "You are not a member of this group" ); } /** * 3️⃣ Normalize response */ const memberships = group.memberships || []; const sharedFiles = group.files || []; const response = { group_id: group.id, group_name: group.group_name, group_image: group.group_image, created_at: group.createdAt, created_by: group.created_by, total_members: memberships.length, members: memberships.map(m => ({ id: m.user?.id, name: m.user?.full_name, designation: m.user?.designation, position: m.user?.position, profile_url: m.user?.uploadedFiles?.[0]?.file_url || null, role: m.role, joined_at: m.joined_at, muted: m.muted, is_online: m.user?.is_online, last_seen: m.user?.last_seen })), profile_image : sharedFiles.map(f => ({ id: f.id, file_name: f.file_name, file_url: f.file_url, file_type: f.file_type, created_at: f.created_at, uploaded_by: { id: f.uploader?.id, name: f.uploader?.full_name, profile_url: f.uploader?.uploadedFiles?.[0]?.file_url } })) }; return sendResponse( res, HttpsStatus.OK, true, "Group details fetched successfully", response ); } catch (err) { console.error("groupDetails error:", err); return sendResponse( res, HttpsStatus.INTERNAL_SERVER_ERROR, false, "Server error!", null, { server: err.message } ); } }; exports.addGroupMember = async (req, res) => { try { const { chat_id, user_id } = req.body; const org_id = req.org_id; if (!chat_id || !user_id) { return sendResponse(res, HttpsStatus.BAD_REQUEST, false, 'chat_id and user_id required'); } const chat = await Chat.findOne({ where: { id: chat_id, organization_id: org_id } }); if (!chat) { return sendResponse(res, HttpsStatus.FORBIDDEN, false, 'Invalid organization chat'); } const admin = await ChatMember.findOne({ where: { chat_id, user_id: req.user.id, role: 'admin' } }); if (!admin) { return sendResponse(res, HttpsStatus.FORBIDDEN, false, 'Only admin can add users'); } const member = await ChatMember.create({ chat_id, user_id, role: 'member' }); return sendResponse(res, HttpsStatus.CREATED, true, 'User added!', member); } catch (err) { if (err.name === 'SequelizeUniqueConstraintError') { return sendResponse(res, HttpsStatus.BAD_REQUEST, false, 'User already in group!'); } return sendResponse(res, HttpsStatus.INTERNAL_SERVER_ERROR, false, 'Server error!', null, { server: err.message }); } }; exports.removeGroupMember = async (req, res) => { try { const { chat_id, user_id } = req.body; const org_id = req.org_id; const chat = await Chat.findOne({ where: { id: chat_id, organization_id: org_id } }); if (!chat) { return sendResponse(res, HttpsStatus.FORBIDDEN, false, 'Invalid organization chat'); } const admin = await ChatMember.findOne({ where: { chat_id, user_id: req.user.id, role: 'admin' } }); if (!admin) { return sendResponse(res, HttpsStatus.FORBIDDEN, false, 'Only admin can remove users'); } const removed = await ChatMember.destroy({ where: { chat_id, user_id } }); return sendResponse(res, HttpsStatus.OK, true, 'User removed!', removed); } catch (err) { return sendResponse(res, HttpsStatus.INTERNAL_SERVER_ERROR, false, 'Server error!', null, { server: err.message }); } }; exports.openChat = async (req, res) => { try { const { chat_id } = req.params; const user_id = req.user.id; const org_id = req.org_id; if (!chat_id) { return sendResponse(res, HttpsStatus.BAD_REQUEST, false, 'Chat id required!'); } const chat = await Chat.findOne({ where: { id: chat_id, organization_id: org_id, is_deleted: false } }); if (!chat) { return sendResponse(res, HttpsStatus.FORBIDDEN, false, 'Invalid chat!'); } const membership = await ChatMember.findOne({ where: { chat_id, user_id } }); if (!membership) { return sendResponse(res, HttpsStatus.FORBIDDEN, false, 'Not authorized!'); } const messages = await Message.findAll({ where: { chat_id }, order: [['created_at', 'ASC']] }); return sendResponse(res, HttpsStatus.OK, true, 'Messages retrieved!', messages); } catch (err) { return sendResponse(res, HttpsStatus.INTERNAL_SERVER_ERROR, false, 'Server error!', null, { server: err.message }); } }; // exports.getChatList = async (req, res) => { // try { // const user_id = req.user.id // if(!user_id){ // return sendResponse(res, HttpsStatus.BAD_REQUEST, false, "User id is required!"); // } // // console.log('---- userid *****',user_id) // // 1. Get chat_ids only // const chatMembers = await ChatMember.findAll({ // where: { user_id }, // attributes: ['chat_id'] // }); // const chat_ids = chatMembers.map(c => c.chat_id) // // console.log('chat_ids -----------------======================', chat_ids); // // 2. Fetch chats // const chats = await Chat.findAll({ // where : { id: { [Op.in]: chat_ids } } // }) // // console.log('chats -----------------======================', chats); // const chatList = [] // for (const chat of chats) { // // 3. Last message (ANY sender) // const lastMessage = await Message.findOne({ // where: { chat_id: chat.id }, // order: [['created_at', 'DESC']], // include: [{ // model: User, // as: 'sender', // attributes: ['id', 'full_name'] // }] // }) // // 4. Unread count // const unreadCount = await MessageStatus.count({ // where: { // user_id, // status: { [Op.ne]: 'read' } // }, // include: [{ // model: Message, // where: { // chat_id: chat.id, // sender_id: { [Op.ne]: user_id } // } // }] // }) // // 5. Chat name logic // let groupName = chat.group_name // if (chat.type === 'private') { // const otherMember = await ChatMember.findOne({ // where: { // chat_id: chat.id, // user_id: { [Op.ne]: user_id } // }, // include: [{ model: User, attributes: ['full_name'] }] // }) // groupName = otherMember?.User?.full_name // } // chatList.push({ // chat_id: chat.id, // type: chat.type, // group_name: groupName, // last_message: lastMessage, // unread_count: unreadCount // }) // } // // 6. Sort by last message time // chatList.sort((a, b) => { // const t1 = a.last_message?.created_at || 0 // const t2 = b.last_message?.created_at || 0 // return new Date(t2) - new Date(t1) // }) // // console.log('chat list ============##################### ',chatList); // return sendResponse(res, HttpsStatus.OK, true, 'chat list retrieved!', chatList); // } catch (err) { // console.error(err) // return sendResponse(res, HttpsStatus.INTERNAL_SERVER_ERROR, false, 'Server error!', null, err.message); // } // } exports.chatList = async (req, res) => { try { const user_id = req.user.id; const org_id = req.org_id; /** * 1️⃣ Get chats where user is member */ // const chatMembers = await ChatMember.findAll({ // where: { user_id }, // attributes: ['chat_id'], // include: [ // { // model: Chat, // as: 'chat', // attributes: ['id', 'type', 'group_name', 'created_at'], // include: [ // { // model: ChatMember, // as: 'memberships', // attributes: ['user_id'], // include: [ // { // model: User, // as: 'user', // attributes: ['id', 'full_name', 'profile_url', 'is_online'] // } // ] // } // ] // } // ] // }); const chatMembers = await ChatMember.findAll({ where: { user_id }, attributes: ['chat_id'], include: [ { model: Chat, as: 'chat', where: { organization_id: org_id, is_deleted: false }, attributes: ['id', 'type', 'group_name', 'created_at'], include: [ { model: ChatMember, as: 'memberships', attributes: ['user_id'], include: [ { model: User, as: 'user', attributes: [ 'id', 'full_name', 'is_online', ], include: [ { model: SharedFile, as: 'uploadedFiles', attributes: [], required: false, where: { file_type: 'image' } } ] } ] } ] } ] }); if (!chatMembers.length) { return sendResponse(res, HttpsStatus.OK, true, 'Chat list retrieved!', []); } const chatIds = chatMembers.map(cm => cm.chat_id); /** * 2️⃣ Fetch last message per chat */ const lastMessages = await Message.findAll({ where: { chat_id: { [Op.in]: chatIds } }, attributes: [ 'chat_id', 'content', 'message_type', 'sender_id', 'created_at' ], include: [ { model: User, as: 'sender', attributes: ['id', 'full_name'] } ], order: [['created_at', 'DESC']] }); const lastMessageMap = {}; for (const msg of lastMessages) { if (!lastMessageMap[msg.chat_id]) { lastMessageMap[msg.chat_id] = msg; } } /** * 3️⃣ Unread count per chat */ const unreadCounts = await MessageStatus.findAll({ where: { user_id, status: { [Op.ne]: 'read' } }, include: [ { model: Message, as: 'message', attributes: ['chat_id'], where: { chat_id: { [Op.in]: chatIds }, sender_id: { [Op.ne]: user_id } } } ] }); const unreadMap = {}; for (const row of unreadCounts) { const chatId = row.message.chat_id; unreadMap[chatId] = (unreadMap[chatId] || 0) + 1; } /** * 4️⃣ Build final response */ const chatList = chatMembers.map(cm => { const chat = cm.chat; const lastMessage = lastMessageMap[chat.id] || null; let name = null; let profile_url = null; let is_online = false; if (chat.type === 'private') { // ✅ get other user const otherUser = chat.memberships .map(m => m.user) .find(u => u.id !== user_id); name = otherUser?.full_name || null; // profile_url = otherUser?.profile_url || null; profile_url = otherUser?.uploadedFiles?.[0]?.file_url || null; is_online =otherUser?.is_online || false; } else { // ✅ group chat name = chat.group_name; profile_url = null; // frontend default image } // console.log("lastMessage- ", lastMessage) const last_message = lastMessage ? { content: lastMessage.content, message_type: lastMessage.message_type, created_at: lastMessage?.dataValues?.created_at, sender_name: lastMessage.sender_id === user_id ? 'You' : lastMessage.sender?.full_name || null } : null; return { chat_id: chat.id, type: chat.type, name, profile_url, is_online, last_message, unread_count: unreadMap[chat.id] || 0 }; }); /** * 5️⃣ Sort by last message time */ chatList.sort((a, b) => { const t1 = a.last_message?.created_at || 0; const t2 = b.last_message?.created_at || 0; return new Date(t2) - new Date(t1); }); return sendResponse( res, HttpsStatus.OK, true, 'Chat list retrieved!', chatList ); } catch (err) { console.error('fetchChatList error:', err); return sendResponse( res, HttpsStatus.INTERNAL_SERVER_ERROR, false, 'Server error!', null, { server: err.message } ); } }; exports.allPrivateChats = async (req, res) => { try { const user_id = req.user.id; const org_id = req.org_id; if (!user_id) { return sendResponse( res, HttpsStatus.BAD_REQUEST, false, 'User id is required!' ); } /** * 1️⃣ Get private chats where user is member */ const chatMembers = await ChatMember.findAll({ where: { user_id }, attributes: ['chat_id'], include: [ { model: Chat, as: 'chat', where: { type: 'private', organization_id: org_id, is_deleted: false }, attributes: ['id', 'type', 'group_name', 'created_at'], include: [ { model: ChatMember, as: 'memberships', attributes: ['user_id'], include: [ { model: User, as: 'user', attributes: ['id', 'full_name', 'is_online'], include: [ { model: SharedFile, as: 'uploadedFiles', attributes: ['file_url'], required: false, where: { file_type: 'image' } } ] } ] } ] } ] }); if (!chatMembers.length) { return sendResponse( res, HttpsStatus.OK, true, 'Private chat list retrieved!', [] ); } const chatIds = chatMembers.map(cm => cm.chat_id); /** * 2️⃣ Last message per chat */ const lastMessages = await Message.findAll({ where: { chat_id: { [Op.in]: chatIds } }, attributes: [ 'chat_id', 'content', 'message_type', 'sender_id', 'created_at' ], include: [ { model: User, as: 'sender', attributes: ['id', 'full_name'] } ], order: [['created_at', 'DESC']] }); const lastMessageMap = {}; for (const msg of lastMessages) { if (!lastMessageMap[msg.chat_id]) { lastMessageMap[msg.chat_id] = msg; } } /** * 3️⃣ Unread message count */ const unreadCounts = await MessageStatus.findAll({ where: { user_id, status: { [Op.ne]: 'read' } }, include: [ { model: Message, as: 'message', attributes: ['chat_id'], where: { chat_id: { [Op.in]: chatIds }, sender_id: { [Op.ne]: user_id } } } ] }); const unreadMap = {}; for (const row of unreadCounts) { const chatId = row.message.chat_id; unreadMap[chatId] = (unreadMap[chatId] || 0) + 1; } /** * 4️⃣ Build response */ const privateChats = chatMembers.map(cm => { const chat = cm.chat; const lastMessage = lastMessageMap[chat.id] || null; const otherUser = chat.memberships ?.map(m => m.user) ?.find(u => u?.id !== user_id); const profile_url = otherUser?.uploadedFiles?.[0]?.file_url || null; const last_message = lastMessage ? { content: lastMessage.content, message_type: lastMessage.message_type, created_at: lastMessage.created_at, sender_name: lastMessage.sender_id === user_id ? 'You' : lastMessage.sender?.full_name || null } : null; return { chat_id: chat.id, type: chat.type, name: otherUser?.full_name || null, profile_url, is_online: otherUser?.is_online || false, last_message, unread_count: unreadMap[chat.id] || 0 }; }); /** * 5️⃣ Sort chats by latest message */ privateChats.sort((a, b) => { const t1 = a.last_message?.created_at || 0; const t2 = b.last_message?.created_at || 0; return new Date(t2) - new Date(t1); }); return sendResponse( res, HttpsStatus.OK, true, 'Private chat list retrieved!', privateChats ); } catch (err) { console.error('fetchPrivateChats error:', err); return sendResponse( res, HttpsStatus.INTERNAL_SERVER_ERROR, false, 'Server error!', null, { server: err.message } ); } }; exports.allGroupChats = async (req, res) => { try { const user_id = req.user.id; const org_id = req.org_id; if (!user_id) { return sendResponse( res, HttpsStatus.BAD_REQUEST, false, 'User id is required!' ); } /** * 1️⃣ Fetch group chats where user is member */ const chatMembers = await ChatMember.findAll({ where: { user_id }, attributes: ['chat_id'], include: [ { model: Chat, as: 'chat', where: { type: 'group', organization_id: org_id, is_deleted: false }, attributes: ['id', 'type', 'group_name', 'created_at'], include: [ { model: ChatMember, as: 'memberships', attributes: ['user_id'], include: [ { model: User, as: 'user', attributes: ['id', 'full_name', 'is_online'], include: [ { model: SharedFile, as: 'uploadedFiles', attributes: ['file_url'], required: false, where: { file_type: 'image' } } ] } ] }, { model: SharedFile, as: 'files', attributes: ['file_url'], required: false, where: { file_type: 'group_profile' } } ] } ] }); if (!chatMembers.length) { return sendResponse( res, HttpsStatus.OK, true, 'Group chat list retrieved!', [] ); } const chatIds = chatMembers.map(cm => cm.chat_id); /** * 2️⃣ Last message per chat */ const lastMessages = await Message.findAll({ where: { chat_id: { [Op.in]: chatIds } }, attributes: [ 'chat_id', 'content', 'message_type', 'sender_id', 'created_at' ], include: [ { model: User, as: 'sender', attributes: ['id', 'full_name'] } ], order: [['created_at', 'DESC']] }); const lastMessageMap = {}; for (const msg of lastMessages) { if (!lastMessageMap[msg.chat_id]) { lastMessageMap[msg.chat_id] = msg; } } /** * 3️⃣ Unread counts */ const unreadCounts = await MessageStatus.findAll({ where: { user_id, status: { [Op.ne]: 'read' } }, include: [ { model: Message, as: 'message', attributes: ['chat_id'], where: { chat_id: { [Op.in]: chatIds }, sender_id: { [Op.ne]: user_id } } } ] }); const unreadMap = {}; for (const row of unreadCounts) { const chatId = row.message.chat_id; unreadMap[chatId] = (unreadMap[chatId] || 0) + 1; } /** * 4️⃣ Build response */ const groupChats = chatMembers.map(cm => { const chat = cm.chat; const lastMessage = lastMessageMap[chat.id] || null; const groupProfile = chat.files?.[0]?.file_url || null; const last_message = lastMessage ? { content: lastMessage.content, message_type: lastMessage.message_type, created_at: lastMessage.created_at, sender_name: lastMessage.sender_id === user_id ? 'You' : lastMessage.sender?.full_name || null } : null; return { chat_id: chat.id, type: chat.type, name: chat.group_name, profile_url: groupProfile, is_online: false, last_message, unread_count: unreadMap[chat.id] || 0 }; }); /** * 5️⃣ Sort chats by latest message */ groupChats.sort((a, b) => { const t1 = a.last_message?.created_at || 0; const t2 = b.last_message?.created_at || 0; return new Date(t2) - new Date(t1); }); return sendResponse( res, HttpsStatus.OK, true, 'Group chat list retrieved!', groupChats ); } catch (err) { console.error('fetchGroupChats error:', err); return sendResponse( res, HttpsStatus.INTERNAL_SERVER_ERROR, false, 'Server error!', null, { server: err.message } ); } }; exports.chatHistory = async (req, res) => { try { const { chat_id } = req.params; const currentUserId = req.user.id; const org_id = req.org_id; /** * 1️⃣ Validate chat belongs to organization */ const chat = await Chat.findOne({ where: { id: chat_id, organization_id: org_id, is_deleted: false } }); if (!chat) { return sendResponse( res, HttpsStatus.FORBIDDEN, false, "Invalid organization chat!" ); } /** * 2️⃣ Check membership */ const isMember = await ChatMember.findOne({ where: { chat_id, user_id: currentUserId } }); if (!isMember) { return sendResponse( res, HttpsStatus.FORBIDDEN, false, "Not authorized!" ); } /** * 3️⃣ Fetch messages */ const messages = await Message.findAll({ where: { chat_id }, include: [ { model: User, as: "sender", attributes: ["id", "full_name"], include: [ { model: SharedFile, as: "uploadedFiles", attributes: ["file_url"], required: false, where: { file_type: "image" } } ] }, { model: SharedFile, as: "files", required: false }, { model: MessageStatus, as: "statuses", where: { user_id: currentUserId }, required: false } ], order: [["created_at", "ASC"]] }); /** * 4️⃣ Format messages */ const formattedMessages = messages.map(msg => { const isYou = msg.sender_id === currentUserId; return { id: msg.id, chat_id: msg.chat_id, content: msg.content, message_type: msg.message_type, created_at: msg.createdAt, sender_id: msg.sender_id, is_you: isYou, sender: { id: msg.sender?.id, full_name: msg.sender?.full_name, profile_url: msg.sender?.uploadedFiles?.[0]?.file_url || null }, status: msg.statuses?.[0]?.status || "sent", files: msg.files?.map(file => ({ id: file.id, file_name: file.file_name, file_url: file.file_url, file_type: file.file_type, mime_type: file.mime_type, file_size: file.file_size, thumbnail_url: file.thumbnail_url, duration: file.duration })) || [] }; }); return sendResponse( res, HttpsStatus.OK, true, "Messages retrieved successfully!", formattedMessages ); } catch (err) { console.error("Fetch messages error:", err); return sendResponse( res, HttpsStatus.INTERNAL_SERVER_ERROR, false, "Server error!", null, { server: err.message } ); } }; // message controller const { Chat, ChatMember, Message, MessageStatus, MessageMention, SharedFile, User, sequelize, } = require("../../models"); const { sendResponse, HttpsStatus } = require("../../utils/response"); const { getFileType } = require("../../utils/fileType"); const EVENTS = require("../../utils/socketEvents"); const { notifyUser } = require('../../utils/notificationService'); const { userBelongsToOrg } = require('../../utils/organizationFilter'); const { Op } = require("sequelize"); exports.sendMessage = async (req, res) => { const t = await sequelize.transaction(); try { const sender_id = req.user.id; const { chat_id, content = "", mentioned_user_ids = [] // optional array } = req.body; const org_id = req.org_id; const io = req.app.get('io'); if (!chat_id) { await t.rollback(); return sendResponse( res, HttpsStatus.BAD_REQUEST, false, 'chat_id is required' ); } // 1️⃣ Check chat exists & user is member const chat = await Chat.findOne({ where: { id: chat_id, organization_id: org_id, is_deleted: false }, include: [ { model: ChatMember, as: 'memberships', attributes: ['user_id'] } ], transaction: t }); if (!chat) { await t.rollback(); return sendResponse(res, HttpsStatus.NOT_FOUND, false, 'Chat not found'); } const memberIds = chat.memberships.map(m => m.user_id); if (!memberIds.includes(sender_id)) { await t.rollback(); return sendResponse(res, HttpsStatus.FORBIDDEN, false, 'Not a chat member'); } /** * 3️⃣ Detect message type */ let message_type = "text"; const hasFiles = req.files && req.files.length > 0; const hasContent = content && content.trim().length > 0; if (hasFiles && hasContent) message_type = "mixed"; else if (hasFiles) message_type = "file"; // const users = await User.findAll({ // where: { id: memberIds }, // attributes: [ // 'id', // 'organization_id', // 'org_2', // 'org_3', // 'org_4', // 'org_5', // 'org_6', // 'org_7', // 'org_8', // 'org_9', // 'org_10' // ], // transaction: t // }); // const invalidUsers = users.filter(user => !userBelongsToOrg(user, org_id)); // if (invalidUsers.length > 0) { // await t.rollback(); // return sendResponse( // res, // HttpsStatus.FORBIDDEN, // false, // 'Users are not from the same organization' // ); // } // 2️⃣ Create message const message = await Message.create( { chat_id, sender_id, content: hasContent ? content : null, message_type }, { transaction: t } ); // 3️⃣ Create message status for each member // const statuses = memberIds.map(user_id => ({ // message_id: message.id, // chat_id, // user_id, // status: user_id === sender_id ? 'read' : 'sent' // })); const statuses = []; for (const member_id of memberIds) { if (member_id === sender_id) { statuses.push({ message_id: message.id, chat_id, user_id: member_id, status: "read", read_at: new Date() }); continue; } const room = io.sockets.adapter.rooms.get(`user_${member_id}`); const isOnline = room && room.size > 0; statuses.push({ message_id: message.id, chat_id, user_id: member_id, status: isOnline ? "delivered" : "sent", delivered_at: isOnline ? new Date() : null }); } await MessageStatus.bulkCreate(statuses, { transaction: t }); // 4️⃣ Handle mentions if (Array.isArray(mentioned_user_ids) && mentioned_user_ids.length) { const mentions = mentioned_user_ids .filter(uid => memberIds.includes(uid)) .map(uid => ({ message_id: message.id, mentioned_user_id: uid })); if (mentions.length) { await MessageMention.bulkCreate(mentions, { transaction: t }); } } // 5️⃣ Handle file uploads (if any) let files = []; if (hasFiles) { const filesPayload = req.files.map(file => ({ message_id: message.id, chat_id, user_id: sender_id, file_name: file.originalname, file_url: file.path, file_type: file.mimetype.split('/')[0], mime_type: file.mimetype, file_size: file.size })); console.log("files payload -",filesPayload); files = await SharedFile.bulkCreate(filesPayload, { transaction: t, returning: true }); } // 6️⃣ Commit DB transaction FIRST await t.commit(); const messagePayload = { message_id: message.id, chat_id, sender_id, content: message.content, message_type, files, last_message: content, created_at: message.createdAt }; // Emit to chat room // io.to(`chat_${chat_id}`).emit(EVENTS.NEW_MESSAGE, messagePayload); io.to(`chat_${chat_id}`).emit(EVENTS.NEW_MESSAGE, { ...messagePayload, statuses }); // console.log(`Emitted new_message to chat_${chat_id}:`, messagePayload); // =========================== // 🔔 NOTIFICATIONS (AFTER COMMIT) // =========================== for (const member_id of memberIds) { io.to(`user_${member_id}`).emit( EVENTS.CHAT_LIST_UPDATE, { action: "new_message", data: messagePayload } ); } const sender = await User.findByPk(sender_id); for (const member_id of memberIds) { // Update chat list realtime // io.to(`user_${member_id}`).emit(EVENTS.CHAT_LIST_UPDATE, { // chat_id, // last_message: content, // sender_id, // created_at: message.createdAt // }); // io.to(`user_${member_id}`).emit(EVENTS.CHAT_LIST_UPDATE, { // action: 'new_message', // data: messagePayload // }); if (member_id === sender_id) continue; await notifyUser(io, { recipient_id: member_id, sender_id, chat_id, message_id: message.id, type: 'message', event: EVENTS.NEW_MESSAGE, title: chat.type === 'private' ? sender.full_name : chat.group_name, body: content || 'Attachment' }); } // 7️⃣ Mention notifications (extra) for (const mentionedUserId of mentioned_user_ids || []) { if (mentionedUserId === sender_id) continue; await notifyUser(io,{ recipient_id: mentionedUserId, sender_id, chat_id, message_id: message.id, type: 'mention', event: 'mentioned', title: 'You were mentioned', body: `${sender.full_name} mentioned you` }); } // 8️⃣ Success response return sendResponse( res, HttpsStatus.CREATED, true, 'Message sent successfully', messagePayload ); } catch (err) { if (!t.finished) { await t.rollback(); } console.error('sendMessage error:', err); return sendResponse( res, HttpsStatus.INTERNAL_SERVER_ERROR, false, 'Failed to send message', null, { server: err.message } ); } }; // exports.sendMessage = async (req, res) => { // const t = await sequelize.transaction(); // try { // const { chat_id, content } = req.body; // const senderId = req.user.id; // const io = req.app.get("io"); // if (!chat_id) { // return sendResponse(res, HttpsStatus.BAD_REQUEST, false, "Chat id is required"); // } // const isMember = await ChatMember.findOne({ // where: { chat_id, user_id: senderId }, // }); // if (!isMember) { // return sendResponse(res, HttpsStatus.FORBIDDEN, false, "Not a chat member"); // } // const members = await ChatMember.findAll({ where: { chat_id } }); // const hasFiles = Array.isArray(req.files) && req.files.length > 0; // const hasContent = typeof content === "string" && content.trim().length > 0; // if (!hasFiles && !hasContent) { // return sendResponse(res, HttpsStatus.BAD_REQUEST, false, "Message cannot be empty!"); // } // let messageType = "text"; // if (hasFiles && hasContent) { // messageType = "mixed"; // } else if (hasFiles) { // messageType = "file"; // } // const message = await Message.create( // { // chat_id, // sender_id: senderId, // message_type: messageType, // content: hasContent ? content : null, // }, // { // transaction: t, // }, // ); // let attachedFiles = []; // if (hasFiles) { // for (const file of req.files) { // const sharedFile = await SharedFile.create( // { // message_id: message.id, // chat_id, // user_id: senderId, // file_name: file.originalname, // file_url: `/uploads/${file.filename}`, // file_type: getFileType(file.mimetype), // file_size: file.size, // mime_type: file.mimetype, // }, // { // transaction: t, // }, // ); // attachedFiles.push(sharedFile); // } // } // await MessageStatus.bulkCreate( // members.map((m) => ({ // message_id: message.id, // user_id: m.user_id, // chat_id, // status: "sent", // })), // { transaction: t }, // ); // await t.commit(); // const payload = { // id: message.id, // chat_id, // sender_id: senderId, // message_type: messageType, // content: message.content, // files: attachedFiles, // created_at: message.created_at, // }; // io.to(`chat_${chat_id}`).emit(EVENTS.NEW_MESSAGE, payload); // return sendResponse(res, HttpsStatus.CREATED, true, "Message Sent!", payload,); // // const messagesPayload = []; // // // ❗ TEXT ONLY (no files) // // if (!req.files || req.files.length === 0) { // // const message = await Message.create( // // { // // chat_id, // // sender_id: senderId, // // message_type: "text", // // content, // // }, // // { transaction: t } // // ); // // await MessageStatus.bulkCreate( // // members.map((m) => ({ // // message_id: message.id, // // user_id: m.user_id, // // chat_id, // // status: "sent", // // })), // // { transaction: t } // // ); // // const textPayload = { // // id: message.id, // // chat_id, // // sender_id: senderId, // // message_type: "text", // // content, // // file: null, // // created_at: message.created_at, // // }; // // io.to(`chat_${chat_id}`).emit(EVENTS.NEW_MESSAGE, textPayload); // // await t.commit(); // // return sendResponse( // // res, // // HttpsStatus.CREATED, // // true, // // "Message created!", // // textPayload // // ); // // } // // // ❗ FILES PRESENT // // if (req.files && req.files.length > 0) { // // for (const file of req.files) { // // let messageType = "file"; // // if (file.mimetype.startsWith("image")) messageType = "image"; // // if (file.mimetype.startsWith("video")) messageType = "video"; // // if (file.mimetype.startsWith("audio")) messageType = "audio"; // // const message = await Message.create( // // { // // chat_id, // // sender_id: senderId, // // message_type: messageType, // // content: content || null, // // }, // // { transaction: t } // // ); // // const fileUrl = `/uploads/${file.filename}`; // // const sharedFile = await SharedFile.create( // // { // // message_id: message.id, // // chat_id, // // user_id: senderId, // // file_name: file.originalname, // // file_url: fileUrl, // // file_type: messageType, // // file_size: file.size, // // mime_type: file.mimetype, // // }, // // { transaction: t } // // ); // // await MessageStatus.bulkCreate( // // members.map((m) => ({ // // message_id: message.id, // // user_id: m.user_id, // // chat_id, // // status: "sent", // // })), // // { transaction: t } // // ); // // messagesPayload.push({ // // id: message.id, // // chat_id, // // sender_id: senderId, // // message_type: messageType, // // content: content || null, // // file: sharedFile, // // created_at: message.created_at, // // }); // // } // // io.to(`chat_${chat_id}`).emit(EVENTS.NEW_MESSAGE, messagesPayload); // // await t.commit(); // // return sendResponse( // // res, // // HttpsStatus.CREATED, // // true, // // "Messages created!", // // messagesPayload // // ); // // } // } catch (err) { // await t.rollback(); // return sendResponse(res, 500, false, "Server error", null, { server: err.message }); // } // }; exports.editMessage = async (req, res) => { const t = await sequelize.transaction(); try { const { message_id } = req.params; const { content, removed_file_ids = [] } = req.body; const userId = req.user.id; const org_id = req.org_id; const io = req.app.get("io"); /** * 1️⃣ Fetch message */ const message = await Message.findOne({ where: { id: message_id, is_deleted: false }, transaction: t }); if (!message) { await t.rollback(); return sendResponse( res, HttpsStatus.NOT_FOUND, false, "Message not found!" ); } /** * 2️⃣ Validate organization chat */ const chat = await Chat.findOne({ where: { id: message.chat_id, organization_id: org_id, is_deleted: false } }); if (!chat) { await t.rollback(); return sendResponse( res, HttpsStatus.FORBIDDEN, false, "Invalid organization chat!" ); } /** * 3️⃣ Validate membership */ const membership = await ChatMember.findOne({ where: { chat_id: message.chat_id, user_id: userId } }); if (!membership) { await t.rollback(); return sendResponse( res, HttpsStatus.FORBIDDEN, false, "Not a chat member!" ); } /** * 4️⃣ Only sender can edit */ if (message.sender_id !== userId) { await t.rollback(); return sendResponse( res, HttpsStatus.FORBIDDEN, false, "You can only edit your own message!" ); } /** * 5️⃣ Remove selected files */ if (Array.isArray(removed_file_ids) && removed_file_ids.length) { await SharedFile.destroy({ where: { id: removed_file_ids, message_id: message.id }, transaction: t }); } /** * 6️⃣ Add new uploaded files */ if (req.files?.length) { const newFiles = req.files.map(file => ({ message_id: message.id, chat_id: message.chat_id, user_id: userId, file_name: file.originalname, file_url: `/uploads/${file.filename}`, file_type: file.mimetype.split("/")[0], mime_type: file.mimetype, file_size: file.size })); await SharedFile.bulkCreate(newFiles, { transaction: t }); } /** * 7️⃣ Update message content */ if (typeof content === "string") { message.content = content.trim(); } /** * 8️⃣ Determine message type */ const filesCount = await SharedFile.count({ where: { message_id: message.id }, transaction: t }); const hasFiles = filesCount > 0; const hasContent = message.content && message.content.trim().length > 0; if (!hasFiles && !hasContent) { await t.rollback(); return sendResponse( res, HttpsStatus.BAD_REQUEST, false, "Message cannot be empty after edit" ); } if (hasFiles && hasContent) message.message_type = "mixed"; else if (hasFiles) message.message_type = "file"; else message.message_type = "text"; /** * 9️⃣ Update edit metadata */ message.edited_at = new Date(); message.edit_count += 1; await message.save({ transaction: t }); await t.commit(); /** * 🔟 Fetch updated files */ const files = await SharedFile.findAll({ where: { message_id: message.id } }); /** * Socket payload */ const payload = { message_id: message.id, chat_id: message.chat_id, sender_id: message.sender_id, content: message.content, message_type: message.message_type, files, edited_at: message.edited_at, edit_count: message.edit_count }; /** * Emit realtime update */ io.to(`chat_${message.chat_id}`).emit( EVENTS.MESSAGE_UPDATED, payload ); return sendResponse( res, HttpsStatus.OK, true, "Message updated successfully", payload ); } catch (err) { if (!t.finished) { await t.rollback(); } console.error("editMessage error:", err); return sendResponse( res, HttpsStatus.INTERNAL_SERVER_ERROR, false, "Server error!", null, { server: err.message } ); } }; exports.deleteMessage = async (req, res) => { try { const { message_id } = req.params; const userId = req.user.id; const org_id = req.org_id; const io = req.app.get("io"); /** * 1️⃣ Fetch message */ const message = await Message.findOne({ where: { id: message_id, is_deleted: false } }); if (!message) { return sendResponse( res, HttpsStatus.NOT_FOUND, false, "Message not found!" ); } /** * 2️⃣ Validate chat belongs to org */ const chat = await Chat.findOne({ where: { id: message.chat_id, organization_id: org_id, is_deleted: false } }); if (!chat) { return sendResponse( res, HttpsStatus.FORBIDDEN, false, "Invalid organization chat!" ); } /** * 3️⃣ Validate membership */ const membership = await ChatMember.findOne({ where: { chat_id: message.chat_id, user_id: userId } }); if (!membership) { return sendResponse( res, HttpsStatus.FORBIDDEN, false, "You are not a member of this chat!" ); } /** * 4️⃣ Only sender can delete */ if (message.sender_id !== userId) { return sendResponse( res, HttpsStatus.FORBIDDEN, false, "You cannot delete this message!" ); } /** * 5️⃣ Soft delete message */ await Message.update( { is_deleted: true }, { where: { id: message_id } } ); /** * 6️⃣ Emit realtime update */ io.to(`chat_${message.chat_id}`).emit( EVENTS.MESSAGE_DELETED, { message_id, chat_id: message.chat_id, deleted_by: userId } ); return sendResponse( res, HttpsStatus.OK, true, "Message deleted successfully!" ); } catch (err) { console.error("deleteMessage error:", err); return sendResponse( res, HttpsStatus.INTERNAL_SERVER_ERROR, false, "Server error!", null, { server: err.message } ); } }; // exports.deliveredMessage = async (req, res) => { // try { // const { message_id, chat_id } = req.body; // const userId = req.user.id; // const io = req.app.get("io"); // const [updatedCount] = await MessageStatus.update( // { // status: "delivered", // delivered_at: new Date(), // }, // { // where: { // message_id, // user_id: userId, // chat_id, // status: "sent", // }, // }, // ); // if (!updatedCount) { // return sendResponse( // res, // HttpsStatus.BAD_REQUEST, // false, // "Message was already delivered or invalid message.", // ); // } // io.to(`chat_${chat_id}`).emit("message_status_updated", { // message_id, // user_id: userId, // status: "delivered", // }); // return sendResponse( // res, // HttpsStatus.OK, // true, // "Message marked as delivered!", // updatedCount, // ); // } catch (err) { // return sendResponse( // res, // HttpsStatus.INTERNAL_SERVER_ERROR, // false, // "Server error!", // null, // { server: err.message }, // ); // } // }; // exports.readMessage = async (req, res) => { // try { // const { message_id, chat_id } = req.body; // const userId = req.user.id; // const io = req.app.get("io"); // const [updatedCount] = await MessageStatus.update( // { // status: "read", // read_at: new Date(), // }, // { // where: { // message_id, // user_id: userId, // chat_id, // status: { [Op.in]: ["sent", "delivered"] }, // }, // }, // ); // if (!updatedCount) { // return sendResponse( // res, // HttpsStatus.BAD_REQUEST, // false, // "Message was already read or invalid message.", // ); // } // io.to(`chat_${chat_id}`).emit("message_status_update", { // message_id, // user_id: userId, // status: "read", // }); // return sendResponse( // res, // HttpsStatus.OK, // true, // "Message marked read!", // updatedCount, // ); // } catch (err) { // return sendResponse( // res, // HttpsStatus.INTERNAL_SERVER_ERROR, // false, // "Server error!", // null, // { server: err.message }, // ); // } // }; // exports.startTyping = async (req, res) => { // try { // const { chat_id } = req.body; // const userId = req.user.id; // const io = req.app.get("io"); // if (!chat_id) { // return sendResponse( // res, // HttpsStatus.BAD_REQUEST, // false, // "Chat id is required!", // ); // } // io.to(`chat_${chat_id}`).emit(EVENTS.USER_TYPING, { // chat_id, // user_id: userId, // }); // return sendResponse(res, HttpsStatus.OK, true, "Typing event sent"); // } catch (err) { // return sendResponse( // res, // HttpsStatus.INTERNAL_SERVER_ERROR, // false, // "Server error!", // null, // { server: err.message }, // ); // } // }; // exports.stopTyping = async (req, res) => { // try { // const { chat_id } = req.body; // const userId = req.user.id; // const io = req.app.get("io"); // if (!chat_id) { // return sendResponse( // res, // HttpsStatus.BAD_REQUEST, // false, // "Chat id is required!", // ); // } // io.to(`chat_${chat_id}`).emit(EVENTS.USER_STOP_TYPING, { // chat_id, // user_id: userId, // }); // return sendResponse(res, HttpsStatus.OK, true, "Stop typing event sent!"); // } catch (err) { // return sendResponse( // res, // HttpsStatus.INTERNAL_SERVER_ERROR, // false, // "Server error!", // null, // { server: err.message }, // ); // } // }; exports.forwardMessage = async (req, res) => { const t = await sequelize.transaction(); try { const { message_id, forwarded_chat_ids } = req.body; const senderId = req.user.id; const org_id = req.org_id; const io = req.app.get("io"); if (!message_id || !Array.isArray(forwarded_chat_ids) || !forwarded_chat_ids.length) { await t.rollback(); return sendResponse( res, HttpsStatus.BAD_REQUEST, false, "Invalid payload!" ); } /** * 1️⃣ Fetch original message */ const originalMessage = await Message.findOne({ where: { id: message_id, is_deleted: false }, include: [{ model: SharedFile }], transaction: t }); if (!originalMessage) { await t.rollback(); return sendResponse( res, HttpsStatus.NOT_FOUND, false, "Message not found!" ); } const forwardedMessages = []; for (const chat_id of forwarded_chat_ids) { /** * 2️⃣ Validate chat */ const chat = await Chat.findOne({ where: { id: chat_id, organization_id: org_id, is_deleted: false }, transaction: t }); if (!chat) continue; /** * 3️⃣ Validate membership */ const isMember = await ChatMember.findOne({ where: { chat_id, user_id: senderId }, transaction: t }); if (!isMember) continue; /** * 4️⃣ Fetch chat members */ const members = await ChatMember.findAll({ where: { chat_id }, transaction: t }); /** * 5️⃣ Create forwarded message */ const message = await Message.create( { chat_id, sender_id: senderId, message_type: originalMessage.message_type, content: originalMessage.content, forwarded_from_message_id: originalMessage.id, forwarded_from_user_id: originalMessage.sender_id, forwarded_from_chat_id: originalMessage.chat_id }, { transaction: t } ); /** * 6️⃣ Copy files */ let files = []; if (originalMessage.SharedFiles?.length) { for (const file of originalMessage.SharedFiles) { const newFile = await SharedFile.create( { message_id: message.id, chat_id, user_id: senderId, file_name: file.file_name, file_url: file.file_url, file_type: file.file_type, file_size: file.file_size, mime_type: file.mime_type, duration: file.duration, thumbnail_url: file.thumbnail_url }, { transaction: t } ); files.push(newFile); } } /** * 7️⃣ Create message statuses */ await MessageStatus.bulkCreate( members.map(m => ({ message_id: message.id, user_id: m.user_id, chat_id, status: m.user_id === senderId ? "read" : "sent" })), { transaction: t } ); const payload = { message_id: message.id, chat_id, sender_id: senderId, message_type: message.message_type, content: message.content, files, forwarded_from_message_id: originalMessage.id, created_at: message.createdAt }; io.to(`chat_${chat_id}`).emit(EVENTS.NEW_MESSAGE, payload); forwardedMessages.push(payload); } if (!forwardedMessages.length) { await t.rollback(); return sendResponse( res, HttpsStatus.FORBIDDEN, false, "You are not a member of target chats" ); } await t.commit(); return sendResponse( res, HttpsStatus.CREATED, true, "Message forwarded successfully!", forwardedMessages ); } catch (err) { await t.rollback(); return sendResponse( res, HttpsStatus.INTERNAL_SERVER_ERROR, false, "Server error!", null, { server: err.message } ); } }; // exports.mentionUser = async (req, res) => { // const t = await sequelize.transaction(); // try { // const { chat_id, content, mentioned_user_ids = [] } = req.body; // const senderId = req.user.id; // const io = req.app.get("io"); // if (!chat_id || !content?.trim()) { // await t.rollback(); // return sendResponse( // res, // HttpsStatus.BAD_REQUEST, // false, // "Invalid payload!", // ); // } // const isMember = await ChatMember.findOne({ // where: { chat_id, user_id: senderId }, // }); // if (!isMember) { // await t.rollback(); // return sendResponse( // res, // HttpsStatus.FORBIDDEN, // false, // "Not a chat member", // ); // } // let validMentionedUsers = []; // if (Array.isArray(mentioned_user_ids) && mentioned_user_ids.length) { // const members = await ChatMember.findAll({ // where: { // chat_id, // user_id: mentioned_user_ids, // }, // attributes: ["user_id"], // transaction: t, // }); // validMentionedUsers = members // .map((m) => m.user_id) // .filter((id) => id !== senderId); // } // const message = await Message.create( // { // chat_id, // sender_id: senderId, // message_type: "text", // content, // }, // { transaction: t }, // ); // const members = await ChatMember.findAll({ // where: { chat_id }, // transaction: t, // }); // await MessageStatus.bulkCreate( // members.map((m) => ({ // message_id: message.id, // user_id: m.user_id, // chat_id, // status: "sent", // })), // { transaction: t }, // ); // if (validMentionedUsers.length) { // await MessageMention.bulkCreate( // validMentionedUsers.map((userId) => ({ // message_id: message.id, // mentioned_user_id: userId, // })), // { transaction: t }, // ); // } // await t.commit(); // const payload = { // id: message.id, // chat_id, // sender_id: senderId, // content, // mentioned_user_ids: validMentionedUsers, // created_at: message.created_at, // }; // io.to(`chat_${chat_id}`).emit(EVENTS.NEW_MESSAGE, payload); // validMentionedUsers.forEach((userId) => { // io.to(`user_${userId}`).emit(EVENTS.USER_MENTIONED, { // chat_id, // message_id: message.id, // mentioned_by: senderId, // content, // }); // }); // return sendResponse( // res, // HttpsStatus.CREATED, // true, // "Message sent with mentions!", // payload, // ); // } catch (err) { // await t.rollback(); // return sendResponse( // res, // HttpsStatus.INTERNAL_SERVER_ERROR, // false, // "Server error!", // null, // { server: err.message }, // ); // } // }; // search controllerconst { sequelize, User, Chat, ChatMember, Message, SharedFile } = require('../../models'); const { Op } = require('sequelize'); const { sendResponse, HttpsStatus } = require('../../utils/response'); exports.searchAll = async (req, res) => { try { const user_id = req.user.id; const org_id = req.org_id; const { search } = req.query; const q = search.trim(); if (!q) { return sendResponse( res, HttpsStatus.BAD_REQUEST, false, "Search bar is empty!", { users: [], groups: [], messages: [], files: [] } ); } /** * 1️⃣ USERS + PRIVATE CHAT SEARCH */ const usersRaw = await User.findAll({ where: { id: { [Op.ne]: user_id }, is_deleted: false, // ✅ Group search condition properly [Op.and]: [ { // full_name OR email [Op.or]: [ { full_name: { [Op.iLike]: `%${q}%` } }, { email: { [Op.iLike]: `%${q}%` } } ] }, { // organization must match [Op.or]: [ { organization_id: org_id }, { org_2: org_id }, { org_3: org_id }, { org_4: org_id }, { org_5: org_id }, { org_6: org_id }, { org_7: org_id }, { org_8: org_id }, { org_9: org_id }, { org_10: org_id } ] } ] }, attributes: ["id", "full_name"], include: [ { model: SharedFile, as: "uploadedFiles", attributes: ["file_url"], required: false } ] }); const users = []; for (const u of usersRaw) { const privateChat = await ChatMember.findOne({ attributes: ["chat_id"], where: { user_id: { [Op.in]: [user_id, u.id] } }, group: ["chat_id"], having: sequelize.literal(`COUNT(DISTINCT "user_id") = 2`), raw: true }); users.push({ user_id: u.id, name: u.full_name, profile_image: u.uploadedFiles?.[0]?.file_url ?? null, chat_id: privateChat?.chat_id ?? null, chat_type: "private" }); } /** * 2️⃣ GROUP SEARCH */ const groupsRaw = await Chat.findAll({ where: { type: "group", organization_id: org_id, group_name: { [Op.iLike]: `%${q}%` }, is_deleted: false }, include: [ { model: ChatMember, as: "memberships", where: { user_id }, attributes: [] }, { model: SharedFile, as: "files", attributes: ["file_url"], required: false, // where: { file_type: "image" } } ], attributes: ["id", "group_name"], distinct: true }); const groups = groupsRaw.map(g => ({ group_id: g.id, group_name: g.group_name, group_image: g.files?.[0]?.file_url ?? null, chat_id: g.id, chat_type: "group" })); /** * 3️⃣ MESSAGE SEARCH */ const messagesRaw = await Message.findAll({ where: { content: { [Op.iLike]: `%${q}%` } }, include: [ { model: Chat, as: "chat", attributes: ["id", "type", "group_name"], where: { organization_id: org_id, is_deleted: false }, include: [ { model: ChatMember, as: "memberships", where: { user_id }, attributes: [] } ] }, { model: User, as: "sender", attributes: ["id", "full_name"] } ], attributes: [ "id", "chat_id", "sender_id", "content", "message_type", "created_at" ], order: [["created_at", "DESC"]], distinct: true }); const messages = messagesRaw.map(m => ({ message_id: m.id, chat_id: m.chat_id, chat_type: m.chat?.type, sender_id: m.sender_id, sender_name: m.sender?.full_name ?? null, message: m.content, message_type: m.message_type, created_at: m.created_at })); /** * 4️⃣ FILE SEARCH */ const filesRaw = await SharedFile.findAll({ where: { file_name: { [Op.iLike]: `%${q}%` } }, include: [ { model: Chat, as: "chat", attributes: ["id", "type"], where: { organization_id: org_id, is_deleted: false }, include: [ { model: ChatMember, as: "memberships", where: { user_id }, attributes: [] } ] } ], attributes: [ "message_id", "chat_id", "file_name", "file_url", "file_type", "user_id", "created_at" ], distinct: true }); const files = filesRaw.map(f => ({ message_id: f.message_id, chat_id: f.chat_id, chat_type: f.chat?.type, file_name: f.file_name, file_url: f.file_url, file_type: f.file_type, uploaded_by: f.user_id, created_at: f.created_at })); /** * FINAL RESPONSE */ return res.json({ status: true, query: q, data: { users, groups, messages, files } }); } catch (err) { console.error("searchAll error:", err); return sendResponse( res, HttpsStatus.INTERNAL_SERVER_ERROR, false, "Server error!", null, { server: err.message } ); } }; exports.searchChatMessages = async (req, res) => { try { const { chat_id } = req.params; const { search } = req.query; const q = search.trim(); const user_id = req.user.id; const org_id = req.org_id; if (!q) { return sendResponse(res, HttpsStatus.BAD_REQUEST, false, "Search query is empty!", []); } /** * Check chat belongs to organization */ const chat = await Chat.findOne({ where: { id: chat_id, organization_id: org_id, is_deleted: false } }); if (!chat) { return sendResponse(res, HttpsStatus.FORBIDDEN, false, "Invalid organization chat"); } /** * Check membership */ const membership = await ChatMember.findOne({ where: { chat_id, user_id } }); if (!membership) { return sendResponse(res, HttpsStatus.FORBIDDEN, false, "You are not a member of this chat"); } /** * Search messages */ const messages = await Message.findAll({ where: { chat_id, [Op.or]: [ { content: { [Op.iLike]: `%${q}%` } }, { "$files.file_name$": { [Op.iLike]: `%${q}%` } } // ✅ key fix ] }, include: [ { model: SharedFile, as: "files", attributes: ["file_name", "file_url"], required: false, // where: { // file_name: { [Op.iLike]: `%${q}%` } // } }, { model: User, as: "sender", attributes: ["id", "full_name"], include: [ { model: SharedFile, as: "uploadedFiles", attributes: ["file_url"], required: false, // where: { file_type: "image" } } ] } ], attributes: ["id", "chat_id", "sender_id", "content", "created_at"], order: [["created_at", "DESC"]], distinct: true, subQuery: false }); return sendResponse( res, HttpsStatus.OK, true, "Chat messages retrieved successfully!", messages ); } catch (err) { return sendResponse( res, HttpsStatus.INTERNAL_SERVER_ERROR, false, "Server error!", null, { server: err.message } ); } }; exports.searchUsers = async (req, res) => { try { const { search } = req.query; const q = search.trim(); const org_id = req.org_id; const currentUserId = req.user.id; if (!q) { return sendResponse( res, HttpsStatus.BAD_REQUEST, false, "Search query is empty!", { users: [] } ); } const users = await User.findAll({ where: { id: { [Op.ne]: currentUserId }, is_deleted: false, [Op.and]: [ // 🔎 search by name or email { [Op.or]: [ { full_name: { [Op.iLike]: `%${q}%` } }, { email: { [Op.iLike]: `%${q}%` } } ] }, // 🏢 organization filter { [Op.or]: [ { organization_id: org_id }, { org_2: org_id }, { org_3: org_id }, { org_4: org_id }, { org_5: org_id }, { org_6: org_id }, { org_7: org_id }, { org_8: org_id }, { org_9: org_id }, { org_10: org_id } ] } ] }, attributes: [ "id", "full_name", "email", "phone", "role", "designation", ], include: [ { model: SharedFile, as: "uploadedFiles", attributes: ["file_url"], required: false } ], distinct: true }); const formattedUsers = users.map(user => ({ id: user.id, full_name: user.full_name, email: user.email, phone: user.phone, role: user.role, designation: user.designation, profile_url: user.uploadedFiles?.[0]?.file_url ?? null })); return sendResponse( res, HttpsStatus.OK, true, "Users retrieved successfully!", formattedUsers ); } catch (err) { return sendResponse( res, HttpsStatus.INTERNAL_SERVER_ERROR, false, "Server error!", null, { server: err.message } ); } }; // user controller const express = require("express"); const bcrypt = require("bcryptjs"); const jwt = require("jsonwebtoken"); const { sendResponse, HttpsStatus } = require("../../utils/response"); const { Op } = require("sequelize"); const { sequelize, User, SharedFile } = require("../../models"); exports.usersByOrgId = async (req, res) => { try { const orgId = req.org_id; const currentUserId = req.user.id; const users = await User.findAll({ where: { is_deleted: false, id: { [Op.ne]: currentUserId }, [Op.or]: [ { organization_id: orgId }, { org_2: orgId }, { org_3: orgId }, { org_4: orgId }, { org_5: orgId }, { org_6: orgId }, { org_7: orgId }, { org_8: orgId }, { org_9: orgId }, { org_10: orgId } ] }, attributes: [ "id", "full_name", "email", "designation", "is_online", "last_seen" ], include: [ { model: SharedFile, as: "uploadedFiles", attributes: ["file_url"], required: false, where: { file_type: "image" } } ] }); const formattedUsers = users.map(user => ({ id: user.id, full_name: user.full_name, email: user.email, designation: user.designation, is_online: user.is_online, last_seen: user.last_seen, profile_url: user?.uploadedFiles?.[0]?.file_url || null })); return sendResponse( res, HttpsStatus.OK, true, "Organization users retrieved successfully!", formattedUsers ); } catch (err) { return sendResponse( res, HttpsStatus.INTERNAL_SERVER_ERROR, false, "Server error!", null, { server: err.message } ); } }; exports.activeUsers = async (req, res) => { try { const org_id = req.org_id; const currentUserId = req.user.id; if (!org_id) { return sendResponse( res, HttpsStatus.BAD_REQUEST, false, "Organization id is missing!" ); } const users = await User.findAll({ where: { is_deleted: false, is_online: true, id: { [Op.ne]: currentUserId }, [Op.or]: [ { organization_id: org_id }, { org_2: org_id }, { org_3: org_id }, { org_4: org_id }, { org_5: org_id }, { org_6: org_id }, { org_7: org_id }, { org_8: org_id }, { org_9: org_id }, { org_10: org_id } ] }, attributes: [ "id", "full_name", "designation", "is_online", "last_seen" ], include: [ { model: SharedFile, as: "uploadedFiles", attributes: ["file_url"], required: false, where: { file_type: "image" } } ] }); const formattedUsers = users.map(u => ({ id: u.id, full_name: u.full_name, designation: u.designation, is_online: u.is_online, last_seen: u.last_seen, profile_url: u?.uploadedFiles?.[0]?.file_url || null })); return sendResponse( res, HttpsStatus.OK, true, "Active users retrieved successfully!", formattedUsers ); } catch (err) { return sendResponse( res, HttpsStatus.INTERNAL_SERVER_ERROR, false, "Server error!", null, { server: err.message } ); } }; exports.updateProfile = async (req, res) => { const t = await sequelize.transaction(); try { const file = req.file; const { bio } = req.body; const userId = req.user.id; if (!file && !bio) { return sendResponse( res, HttpsStatus.BAD_REQUEST, false, "Nothing to update!" ); } // const user = await User.findByPk(userId, { transaction: t }); // if(!user){ // return sendResponse(res, HttpsStatus.BAD_REQUEST, false, 'User not found!'); // } if (bio) { await User.update(bio, { where: { id: userId }, transaction: t }); } if (file) { const fileUrl = `/uploads/${file.filename}`; const file_type = file.mimetype.startsWith("image") ? "image" : null; await sharedFile.create( { user_id: userId, file_name: file.originalname, file_url: fileUrl, file_type, file_size: file.size, mime_type: file.mimetype, }, { transaction: t, } ); } await t.commit(); return sendResponse( res, HttpsStatus.OK, true, "Profile updated successfully!" ); } catch (err) { await t.rollback(); return sendResponse( res, HttpsStatus.INTERNAL_SERVER_ERROR, false, "Server error!", null, { server: err.message } ); } };