powr-sdk-api
Version:
Shared API core library for PowrStack projects. Zero dependencies - works with Express, Next.js API routes, and other frameworks. All features are optional and install only what you need.
463 lines (438 loc) • 13.3 kB
JavaScript
"use strict";
const express = require('express');
const router = express.Router();
const {
ObjectId
} = require('mongodb');
const {
getDb
} = require('../services/mongo');
// Get all conversations for a user
router.get('/conversations', async (req, res) => {
try {
const userId = req.user.powrId;
const userAccess = req.user.access;
const projectId = req.projectId;
console.log('Current user ID:', userId, 'Access:', userAccess, 'Project:', projectId);
const db = await getDb();
// Show all conversations user is in (1:1 DMs and groups, any project)
const conversations = await db.collection('conversations').find({
participants: userId
}).toArray();
// Build response for each conversation
const conversationsWithUsers = await Promise.all(conversations.map(async conv => {
const isGroup = conv.type === 'group';
// Get last message
const lastMessage = await db.collection('messages').findOne({
conversationId: conv._id.toString()
}, {
sort: {
createdAt: -1
}
});
// Get unread count
const unreadCount = await db.collection('messages').countDocuments({
conversationId: conv._id.toString(),
senderId: {
$ne: userId
},
read: false
});
let displayName, avatar;
if (isGroup) {
displayName = conv.name || 'Group';
avatar = null; // Groups typically use icon, not user avatar
} else {
const otherUserId = conv.participants.find(p => String(p) !== String(userId));
const otherUser = otherUserId ? await db.collection('users').findOne({
_id: new ObjectId(otherUserId)
}, {
projection: {
fullName: 1,
avatar: 1
}
}) : null;
displayName = (otherUser === null || otherUser === void 0 ? void 0 : otherUser.fullName) || 'Unknown User';
avatar = otherUser === null || otherUser === void 0 ? void 0 : otherUser.avatar;
}
return {
id: conv._id.toString(),
type: isGroup ? 'group' : 'dm',
name: displayName,
avatar,
lastMessage: (lastMessage === null || lastMessage === void 0 ? void 0 : lastMessage.content) || '',
lastMessageTime: (lastMessage === null || lastMessage === void 0 ? void 0 : lastMessage.createdAt) || conv.createdAt,
unreadCount,
isOnline: false
};
}));
return res.json({
success: true,
data: conversationsWithUsers
});
} catch (error) {
console.error('Error fetching conversations:', error);
return res.status(500).json({
success: false,
message: "Failed to fetch conversations."
});
}
});
// Get messages for a conversation
router.get('/conversations/:conversationId/messages', async (req, res) => {
try {
const {
conversationId
} = req.params;
const userId = req.user.powrId;
const projectId = req.projectId;
const db = await getDb();
// Verify user is part of conversation
const conversation = await db.collection('conversations').findOne({
_id: new ObjectId(conversationId),
participants: userId
});
if (!conversation) {
return res.status(404).json({
success: false,
message: 'Conversation not found'
});
}
const messages = await db.collection('messages').find({
conversationId: conversationId
}).sort({
createdAt: 1
}).toArray();
// Mark messages as read
await db.collection('messages').updateMany({
conversationId: conversationId,
senderId: {
$ne: userId
},
read: false
}, {
$set: {
read: true
}
});
const messagesWithOwnership = messages.map(msg => ({
id: msg._id.toString(),
sender: msg.senderName,
message: msg.content,
timestamp: msg.createdAt,
isOwn: msg.senderId === userId
}));
return res.json({
success: true,
data: messagesWithOwnership
});
} catch (error) {
console.error('Error fetching messages:', error);
return res.status(500).json({
success: false,
message: "Failed to fetch messages."
});
}
});
// Send a message
router.post('/conversations/:conversationId/messages', async (req, res) => {
try {
const {
conversationId
} = req.params;
const {
content,
projectId: bodyProjectId
} = req.body;
const userId = req.user.powrId;
const projectId = bodyProjectId || req.projectId;
if (!content || content.trim() === '') {
return res.status(400).json({
success: false,
message: "Message content is required"
});
}
const db = await getDb();
// Verify user is part of conversation
const conversation = await db.collection('conversations').findOne({
_id: new ObjectId(conversationId),
participants: userId
});
if (!conversation) {
return res.status(404).json({
success: false,
message: "Conversation not found"
});
}
// Get current user's name from database
const currentUser = await db.collection('users').findOne({
_id: new ObjectId(userId)
}, {
projection: {
fullName: 1
}
});
const message = {
conversationId: conversationId,
senderId: userId,
senderName: (currentUser === null || currentUser === void 0 ? void 0 : currentUser.fullName) || 'Unknown User',
content: content.trim(),
createdAt: new Date(),
read: false,
projectId: projectId
};
const result = await db.collection('messages').insertOne(message);
// Update conversation last message
await db.collection('conversations').updateOne({
_id: new ObjectId(conversationId)
}, {
$set: {
updatedAt: new Date()
}
});
const responseMessage = {
id: result.insertedId.toString(),
sender: message.senderName,
message: message.content,
timestamp: message.createdAt,
isOwn: true
};
return res.status(201).json({
success: true,
data: responseMessage
});
} catch (error) {
console.error('Error sending message:', error);
return res.status(500).json({
success: false,
message: "Failed to send message."
});
}
});
// Create a new conversation (DM or group)
router.post('/conversations', async (req, res) => {
try {
const {
participantId,
participantIds,
type,
name,
projectId: bodyProjectId
} = req.body;
const userId = req.user.powrId;
const userAccess = req.user.access;
const projectId = bodyProjectId || req.projectId;
const db = await getDb();
// --- GROUP ---
if (type === 'group') {
if (!participantIds || !Array.isArray(participantIds) || participantIds.length === 0) {
return res.status(400).json({
success: false,
message: "participantIds (array) is required for group"
});
}
if (!name || typeof name !== 'string' || !name.trim()) {
return res.status(400).json({
success: false,
message: "name is required for group"
});
}
// Normalize participant IDs (ensure strings, dedupe, include creator)
const normalizedIds = [...new Set([userId, ...participantIds.map(id => String(id))])];
// Check if group with same projectId + name already exists
const existingGroup = await db.collection('conversations').findOne({
type: 'group',
projectId: projectId,
name: name.trim()
});
if (existingGroup) {
// Return existing if creator is a participant
const isParticipant = existingGroup.participants.some(p => String(p) === String(userId));
if (isParticipant) {
return res.json({
success: true,
data: {
id: existingGroup._id.toString(),
message: 'Group already exists'
}
});
}
}
const conversation = {
type: 'group',
participants: normalizedIds,
name: name.trim(),
projectId: projectId,
createdBy: userId,
createdAt: new Date(),
updatedAt: new Date()
};
const result = await db.collection('conversations').insertOne(conversation);
return res.status(201).json({
success: true,
data: {
id: result.insertedId.toString(),
message: 'Group created successfully'
}
});
}
// --- DM (default) ---
if (!participantId) {
return res.status(400).json({
success: false,
message: "Participant ID is required for DM (or use type: 'group' with participantIds)"
});
}
// Check if user has permission to chat with this participant
const participant = await db.collection('users').findOne({
_id: new ObjectId(participantId)
}, {
projection: {
access: 1
}
});
if (!participant) {
return res.status(404).json({
success: false,
message: "Participant not found"
});
}
// Validate access permissions
let hasPermission = false;
if (userAccess === 100) {
hasPermission = true;
} else if (userAccess === 900) {
hasPermission = participant.access === 100;
} else {
hasPermission = participant.access === 100 || participant.access === null;
}
if (!hasPermission) {
return res.status(403).json({
success: false,
message: "You do not have permission to chat with this user"
});
}
// Check if conversation already exists
const existingConversation = await db.collection('conversations').findOne({
type: {
$ne: 'group'
},
participants: {
$all: [userId, participantId]
},
projectId: projectId
});
if (existingConversation) {
return res.json({
success: true,
data: {
id: existingConversation._id.toString(),
message: 'Conversation already exists'
}
});
}
const conversation = {
type: 'dm',
participants: [userId, participantId],
projectId: projectId,
createdAt: new Date(),
updatedAt: new Date()
};
const result = await db.collection('conversations').insertOne(conversation);
return res.status(201).json({
success: true,
data: {
id: result.insertedId.toString(),
message: 'Conversation created successfully'
}
});
} catch (error) {
console.error('Error creating conversation:', error);
return res.status(500).json({
success: false,
message: "Failed to create conversation."
});
}
});
// Get users for starting new conversations
router.get('/users', async (req, res) => {
try {
const userId = req.user.powrId;
const userAccess = req.user.access;
const projectId = req.projectId;
console.log('Current user access level:', userAccess, 'Project:', projectId);
const db = await getDb();
// Get users from the current project
const projectUsers = await db.collection('profiles').find({
projectId: projectId
}).toArray();
console.log('Project users found:', projectUsers.length, 'for projectId:', projectId);
const userIds = projectUsers.map(profile => profile.userId);
console.log('User IDs extracted:', userIds);
// If no users found for this project, return empty array
if (userIds.length === 0) {
console.log('No users found for project:', projectId);
return res.json({
success: true,
data: []
});
}
let userQuery = {
_id: {
$in: userIds.map(id => new ObjectId(id))
},
_id: {
$ne: new ObjectId(userId)
}
};
// Apply role-based filtering
if (userAccess === 100) {
// Admin can chat with everyone
console.log('Admin access - can chat with everyone');
} else if (userAccess === 900) {
// Client can only chat with admins
userQuery.access = 100;
console.log('Client access - can only chat with admins');
} else {
// Employee (access null) can chat with employees and admins
userQuery.$or = [{
access: 100
},
// Admins
{
access: null
} // Employees
];
console.log('Employee access - can chat with employees and admins');
}
// Get filtered users
const users = await db.collection('users').find(userQuery, {
projection: {
fullName: 1,
username: 1,
avatar: 1,
_id: 1,
access: 1
}
}).toArray();
const formattedUsers = users.map(user => ({
id: user._id.toString(),
name: user.fullName || 'Unknown User',
username: user.username,
avatar: user.avatar,
access: user.access
}));
console.log('Available users for chat:', formattedUsers);
return res.json({
success: true,
data: formattedUsers
});
} catch (error) {
console.error('Error fetching users:', error);
return res.status(500).json({
success: false,
message: "Failed to fetch users."
});
}
});
module.exports = router;