feat: новая функция
This commit is contained in:
@@ -208,22 +208,23 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
|
||||
// Получаем информацию о пользователе
|
||||
const users = await encryptedDb.getData('users', { id: userId }, 1);
|
||||
|
||||
// ✨ НОВОЕ: Валидация прав через adminLogicService
|
||||
const adminLogicService = require('../services/adminLogicService');
|
||||
// ✨ Используем централизованную проверку прав
|
||||
const { canSendMessage } = require('/app/shared/permissions');
|
||||
const sessionUserId = req.session.userId;
|
||||
const targetUserId = userId;
|
||||
const userAccessLevel = req.session.userAccessLevel || { level: 'user', tokenCount: 0, hasAccess: false };
|
||||
const canWrite = adminLogicService.canWriteToConversation({
|
||||
userAccessLevel: userAccessLevel,
|
||||
userId: sessionUserId,
|
||||
conversationUserId: targetUserId
|
||||
});
|
||||
const userRole = req.session.userAccessLevel?.level || 'user';
|
||||
|
||||
if (!canWrite) {
|
||||
logger.warn(`[Chat] Пользователь ${sessionUserId} пытался писать в беседу ${targetUserId} без прав`);
|
||||
// Получаем роль получателя
|
||||
const recipientUser = users[0];
|
||||
const recipientRole = recipientUser.role || 'user';
|
||||
|
||||
const permissionCheck = canSendMessage(userRole, recipientRole, sessionUserId, targetUserId);
|
||||
|
||||
if (!permissionCheck.canSend) {
|
||||
logger.warn(`[Chat] Пользователь ${sessionUserId} (${userRole}) пытался писать в беседу ${targetUserId} (${recipientRole}) без прав: ${permissionCheck.errorMessage}`);
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Нет прав для отправки сообщений в эту беседу'
|
||||
error: permissionCheck.errorMessage || 'Недостаточно прав для отправки сообщений'
|
||||
});
|
||||
}
|
||||
if (!users || users.length === 0) {
|
||||
@@ -327,10 +328,10 @@ router.get('/history', requireAuth, async (req, res) => {
|
||||
try {
|
||||
// Если нужен только подсчет
|
||||
if (countOnly) {
|
||||
let countQuery = 'SELECT COUNT(*) FROM messages WHERE user_id = $1 AND message_type = $2';
|
||||
let countParams = [userId, 'user_chat'];
|
||||
let countQuery = 'SELECT COUNT(*) FROM messages WHERE user_id = $1 AND (message_type = $2 OR message_type = $3)';
|
||||
let countParams = [userId, 'user_chat', 'public'];
|
||||
if (conversationId) {
|
||||
countQuery += ' AND conversation_id = $3';
|
||||
countQuery += ' AND conversation_id = $4';
|
||||
countParams.push(conversationId);
|
||||
}
|
||||
const countResult = await db.getQuery()(countQuery, countParams);
|
||||
@@ -338,17 +339,28 @@ router.get('/history', requireAuth, async (req, res) => {
|
||||
return res.json({ success: true, count: totalCount });
|
||||
}
|
||||
|
||||
// Загружаем сообщения через encryptedDb
|
||||
const whereConditions = {
|
||||
user_id: userId,
|
||||
message_type: 'user_chat' // Фильтруем только публичные сообщения
|
||||
};
|
||||
if (conversationId) {
|
||||
whereConditions.conversation_id = conversationId;
|
||||
}
|
||||
|
||||
// Изменяем логику: загружаем ПОСЛЕДНИЕ сообщения, а не с offset
|
||||
const messages = await encryptedDb.getData('messages', whereConditions, limit, 'created_at DESC', 0);
|
||||
// Загружаем сообщения: ИИ сообщения + публичные сообщения от других пользователей
|
||||
// Используем SQL запрос для правильной фильтрации
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
const result = await db.getQuery()(
|
||||
`SELECT m.id, m.user_id, m.sender_id, m.conversation_id,
|
||||
decrypt_text(m.sender_type_encrypted, $2) as sender_type,
|
||||
decrypt_text(m.content_encrypted, $2) as content,
|
||||
decrypt_text(m.channel_encrypted, $2) as channel,
|
||||
decrypt_text(m.role_encrypted, $2) as role,
|
||||
decrypt_text(m.direction_encrypted, $2) as direction,
|
||||
m.message_type, m.created_at
|
||||
FROM messages m
|
||||
WHERE m.user_id = $1
|
||||
AND (m.message_type = 'user_chat' OR m.message_type = 'public')
|
||||
ORDER BY m.created_at DESC
|
||||
LIMIT $3`,
|
||||
[userId, encryptionKey, limit]
|
||||
);
|
||||
|
||||
const messages = result.rows;
|
||||
// Переворачиваем массив для правильного порядка
|
||||
messages.reverse();
|
||||
|
||||
|
||||
@@ -39,18 +39,20 @@ router.get('/public', requireAuth, async (req, res) => {
|
||||
// Публичные сообщения видны на главной странице пользователя
|
||||
const targetUserId = userId || currentUserId;
|
||||
|
||||
|
||||
// Если нужен только подсчет
|
||||
if (countOnly) {
|
||||
const countResult = await db.getQuery()(
|
||||
`SELECT COUNT(*) FROM messages WHERE user_id = $1 AND message_type = 'public'`,
|
||||
[targetUserId]
|
||||
`SELECT COUNT(*) FROM messages WHERE message_type = 'public'
|
||||
AND ((user_id = $1 AND sender_id = $2) OR (user_id = $2 AND sender_id = $1))`,
|
||||
[targetUserId, currentUserId]
|
||||
);
|
||||
const totalCount = parseInt(countResult.rows[0].count, 10);
|
||||
return res.json({ success: true, count: totalCount, total: totalCount });
|
||||
}
|
||||
|
||||
const result = await db.getQuery()(
|
||||
`SELECT m.id, m.user_id, decrypt_text(m.sender_type_encrypted, $2) as sender_type,
|
||||
`SELECT m.id, m.user_id, m.sender_id, decrypt_text(m.sender_type_encrypted, $2) as sender_type,
|
||||
decrypt_text(m.content_encrypted, $2) as content,
|
||||
decrypt_text(m.channel_encrypted, $2) as channel,
|
||||
decrypt_text(m.role_encrypted, $2) as role,
|
||||
@@ -59,7 +61,8 @@ router.get('/public', requireAuth, async (req, res) => {
|
||||
arm.last_read_at
|
||||
FROM messages m
|
||||
LEFT JOIN admin_read_messages arm ON arm.user_id = m.user_id AND arm.admin_id = $5
|
||||
WHERE m.user_id = $1 AND m.message_type = 'public'
|
||||
WHERE m.message_type = 'public'
|
||||
AND ((m.user_id = $1 AND m.sender_id = $5) OR (m.user_id = $5 AND m.sender_id = $1))
|
||||
ORDER BY m.created_at DESC
|
||||
LIMIT $3 OFFSET $4`,
|
||||
[targetUserId, encryptionKey, limit, offset, currentUserId]
|
||||
@@ -67,8 +70,9 @@ router.get('/public', requireAuth, async (req, res) => {
|
||||
|
||||
// Получаем общее количество для пагинации
|
||||
const countResult = await db.getQuery()(
|
||||
`SELECT COUNT(*) FROM messages WHERE user_id = $1 AND message_type = 'public'`,
|
||||
[targetUserId]
|
||||
`SELECT COUNT(*) FROM messages WHERE message_type = 'public'
|
||||
AND ((user_id = $1 AND sender_id = $2) OR (user_id = $2 AND sender_id = $1))`,
|
||||
[targetUserId, currentUserId]
|
||||
);
|
||||
const totalCount = parseInt(countResult.rows[0].count, 10);
|
||||
|
||||
@@ -102,7 +106,7 @@ router.get('/private', requireAuth, async (req, res) => {
|
||||
// Если нужен только подсчет
|
||||
if (countOnly) {
|
||||
const countResult = await db.getQuery()(
|
||||
`SELECT COUNT(*) FROM messages WHERE user_id = $1 AND message_type = 'private'`,
|
||||
`SELECT COUNT(*) FROM messages WHERE user_id = $1 AND message_type = 'admin_chat'`,
|
||||
[currentUserId]
|
||||
);
|
||||
const totalCount = parseInt(countResult.rows[0].count, 10);
|
||||
@@ -120,17 +124,17 @@ router.get('/private', requireAuth, async (req, res) => {
|
||||
arm.last_read_at
|
||||
FROM messages m
|
||||
LEFT JOIN admin_read_messages arm ON arm.user_id = m.user_id AND arm.admin_id = $5
|
||||
WHERE m.user_id = $1 AND m.message_type = 'private'
|
||||
WHERE m.user_id = $1 AND m.message_type = 'admin_chat'
|
||||
ORDER BY m.created_at DESC
|
||||
LIMIT $3 OFFSET $4`,
|
||||
[currentUserId, encryptionKey, limit, offset, currentUserId]
|
||||
);
|
||||
|
||||
// Получаем общее количество для пагинации
|
||||
const countResult = await db.getQuery()(
|
||||
`SELECT COUNT(*) FROM messages WHERE user_id = $1 AND message_type = 'private'`,
|
||||
[currentUserId]
|
||||
);
|
||||
const countResult = await db.getQuery()(
|
||||
`SELECT COUNT(*) FROM messages WHERE user_id = $1 AND message_type = 'admin_chat'`,
|
||||
[currentUserId]
|
||||
);
|
||||
const totalCount = parseInt(countResult.rows[0].count, 10);
|
||||
|
||||
res.json({
|
||||
@@ -209,44 +213,7 @@ router.get('/read-status', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/conversations?userId=123
|
||||
router.get('/conversations', async (req, res) => {
|
||||
const userId = req.query.userId;
|
||||
if (!userId) return res.status(400).json({ error: 'userId required' });
|
||||
try {
|
||||
const result = await db.getQuery()(
|
||||
'SELECT * FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
|
||||
[userId]
|
||||
);
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Conversation not found' });
|
||||
}
|
||||
res.json(result.rows[0]);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: 'DB error', details: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/conversations - создать беседу для пользователя
|
||||
router.post('/conversations', async (req, res) => {
|
||||
const { userId, title } = req.body;
|
||||
if (!userId) return res.status(400).json({ error: 'userId required' });
|
||||
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
try {
|
||||
const conversationTitle = title || `Чат с пользователем ${userId}`;
|
||||
const result = await db.getQuery()(
|
||||
'INSERT INTO conversations (user_id, title_encrypted, created_at, updated_at) VALUES ($1, encrypt_text($2, $3), NOW(), NOW()) RETURNING *',
|
||||
[userId, conversationTitle, encryptionKey]
|
||||
);
|
||||
res.json(result.rows[0]);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: 'DB error', details: e.message });
|
||||
}
|
||||
});
|
||||
// УДАЛЕНО: Дублирующиеся endpoint'ы перенесены ниже
|
||||
|
||||
// Массовая рассылка сообщения во все каналы пользователя
|
||||
// Массовая рассылка сообщений
|
||||
@@ -268,7 +235,7 @@ router.post('/broadcast', requireAuth, requirePermission(PERMISSIONS.BROADCAST),
|
||||
});
|
||||
|
||||
if (!canBroadcast) {
|
||||
logger.warn(`[Messages] Пользователь ${req.session.userId} (роль: ${userRole}) пытался сделать broadcast без прав`);
|
||||
console.warn(`[Messages] Пользователь ${req.session.userId} (роль: ${userRole}) пытался сделать broadcast без прав`);
|
||||
return res.status(403).json({
|
||||
error: 'Только редакторы (editor) могут делать массовую рассылку'
|
||||
});
|
||||
@@ -287,15 +254,15 @@ router.post('/broadcast', requireAuth, requirePermission(PERMISSIONS.BROADCAST),
|
||||
const identities = identitiesRes.rows;
|
||||
// --- Найти или создать беседу (conversation) ---
|
||||
let conversationResult = await db.getQuery()(
|
||||
'SELECT id, user_id, created_at, updated_at, decrypt_text(title_encrypted, $2) as title FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
|
||||
'SELECT id, user_id, created_at, updated_at, title FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
|
||||
[user_id, encryptionKey]
|
||||
);
|
||||
let conversation;
|
||||
if (conversationResult.rows.length === 0) {
|
||||
const title = `Чат с пользователем ${user_id}`;
|
||||
const newConv = await db.getQuery()(
|
||||
'INSERT INTO conversations (user_id, title_encrypted, created_at, updated_at) VALUES ($1, encrypt_text($2, $3), NOW(), NOW()) RETURNING *',
|
||||
[user_id, title, encryptionKey]
|
||||
'INSERT INTO conversations (user_id, title, created_at, updated_at) VALUES ($1, $2, NOW(), NOW()) RETURNING *',
|
||||
[user_id, title]
|
||||
);
|
||||
conversation = newConv.rows[0];
|
||||
} else {
|
||||
@@ -312,14 +279,14 @@ router.post('/broadcast', requireAuth, requirePermission(PERMISSIONS.BROADCAST),
|
||||
await emailBot.sendEmail(email, 'Новое сообщение', content);
|
||||
// Сохраняем в messages с conversation_id
|
||||
await db.getQuery()(
|
||||
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, message_type, created_at)
|
||||
VALUES ($1, $2, encrypt_text($3, $8), encrypt_text($4, $8), encrypt_text($5, $8), encrypt_text($6, $8), encrypt_text($7, $8), $9, NOW())`,
|
||||
[user_id, conversation.id, 'editor', content, 'email', 'user', 'out', encryptionKey, 'user_chat']
|
||||
`INSERT INTO messages (conversation_id, sender_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, message_type, user_id, role, direction, created_at)
|
||||
VALUES ($1, $2, encrypt_text($3, $12), encrypt_text($4, $12), encrypt_text($5, $12), encrypt_text($6, $12), encrypt_text($7, $12), $8, $9, $10, $11, NOW())`,
|
||||
[conversation.id, req.session.userId, 'editor', content, 'email', 'user', 'out', 'user_chat', user_id, 'user', 'out', encryptionKey]
|
||||
);
|
||||
results.push({ channel: 'email', status: 'sent' });
|
||||
sent = true;
|
||||
} else {
|
||||
logger.warn('[messages.js] Email Bot не инициализирован');
|
||||
console.warn('[messages.js] Email Bot не инициализирован');
|
||||
results.push({ channel: 'email', status: 'error', error: 'Bot not initialized' });
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -335,14 +302,14 @@ router.post('/broadcast', requireAuth, requirePermission(PERMISSIONS.BROADCAST),
|
||||
const bot = telegramBot.getBot();
|
||||
await bot.telegram.sendMessage(telegram, content);
|
||||
await db.getQuery()(
|
||||
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, message_type, created_at)
|
||||
VALUES ($1, $2, encrypt_text($3, $8), encrypt_text($4, $8), encrypt_text($5, $8), encrypt_text($6, $8), encrypt_text($7, $8), $9, NOW())`,
|
||||
[user_id, conversation.id, 'editor', content, 'telegram', 'user', 'out', encryptionKey, 'user_chat']
|
||||
`INSERT INTO messages (conversation_id, sender_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, message_type, user_id, role, direction, created_at)
|
||||
VALUES ($1, $2, encrypt_text($3, $12), encrypt_text($4, $12), encrypt_text($5, $12), encrypt_text($6, $12), encrypt_text($7, $12), $8, $9, $10, $11, NOW())`,
|
||||
[conversation.id, req.session.userId, 'editor', content, 'telegram', 'user', 'out', 'user_chat', user_id, 'user', 'out', encryptionKey]
|
||||
);
|
||||
results.push({ channel: 'telegram', status: 'sent' });
|
||||
sent = true;
|
||||
} else {
|
||||
logger.warn('[messages.js] Telegram Bot не инициализирован');
|
||||
console.warn('[messages.js] Telegram Bot не инициализирован');
|
||||
results.push({ channel: 'telegram', status: 'error', error: 'Bot not initialized' });
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -354,9 +321,9 @@ router.post('/broadcast', requireAuth, requirePermission(PERMISSIONS.BROADCAST),
|
||||
if (wallet) {
|
||||
// Здесь можно реализовать отправку через web3, если нужно
|
||||
await db.getQuery()(
|
||||
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, message_type, created_at)
|
||||
VALUES ($1, $2, encrypt_text($3, $9), encrypt_text($4, $9), encrypt_text($5, $9), encrypt_text($6, $9), encrypt_text($7, $9), $8, NOW())`,
|
||||
[user_id, conversation.id, 'editor', content, 'wallet', 'user', 'out', 'user_chat', encryptionKey]
|
||||
`INSERT INTO messages (conversation_id, sender_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, message_type, user_id, role, direction, created_at)
|
||||
VALUES ($1, $2, encrypt_text($3, $12), encrypt_text($4, $12), encrypt_text($5, $12), encrypt_text($6, $12), encrypt_text($7, $12), $8, $9, $10, $11, NOW())`,
|
||||
[conversation.id, req.session.userId, 'editor', content, 'wallet', 'user', 'out', 'user_chat', user_id, 'user', 'out', encryptionKey]
|
||||
);
|
||||
results.push({ channel: 'wallet', status: 'saved' });
|
||||
sent = true;
|
||||
@@ -382,68 +349,78 @@ router.post('/send', requireAuth, async (req, res) => {
|
||||
return res.status(400).json({ error: 'messageType должен быть "public" или "private"' });
|
||||
}
|
||||
|
||||
// Определяем recipientId в зависимости от типа сообщения
|
||||
let recipientIdNum;
|
||||
if (messageType === 'private') {
|
||||
// Приватные сообщения всегда идут к редактору (ID = 1)
|
||||
recipientIdNum = 1;
|
||||
} else {
|
||||
// Конвертируем recipientId в число для публичных сообщений
|
||||
recipientIdNum = parseInt(recipientId);
|
||||
if (isNaN(recipientIdNum)) {
|
||||
return res.status(400).json({ error: 'recipientId должен быть числом' });
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Получаем информацию об отправителе
|
||||
const senderId = req.user.id;
|
||||
const senderRole = req.user.contact_type || req.user.role;
|
||||
const senderRole = req.user.role || req.user.userAccessLevel?.level || 'user';
|
||||
|
||||
console.log('[DEBUG] /messages/send: senderId:', senderId, 'senderRole:', senderRole);
|
||||
|
||||
// Получаем информацию о получателе
|
||||
const recipientResult = await db.getQuery()(
|
||||
'SELECT id, contact_type FROM users WHERE id = $1',
|
||||
[recipientId]
|
||||
'SELECT id, role FROM users WHERE id = $1',
|
||||
[recipientIdNum]
|
||||
);
|
||||
|
||||
if (recipientResult.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Получатель не найден' });
|
||||
}
|
||||
|
||||
const recipientRole = recipientResult.rows[0].contact_type;
|
||||
const recipientRole = recipientResult.rows[0].role;
|
||||
console.log('[DEBUG] /messages/send: recipientId:', recipientIdNum, 'recipientRole:', recipientRole);
|
||||
|
||||
// Проверка прав согласно матрице разрешений
|
||||
const canSend = (
|
||||
// Editor может отправлять всем
|
||||
(senderRole === 'editor') ||
|
||||
// User и readonly могут отправлять только editor
|
||||
((senderRole === 'user' || senderRole === 'readonly') && recipientRole === 'editor')
|
||||
);
|
||||
// Используем централизованную проверку прав
|
||||
const { canSendMessage } = require('/app/shared/permissions');
|
||||
const permissionCheck = canSendMessage(senderRole, recipientRole, senderId, recipientIdNum);
|
||||
|
||||
if (!canSend) {
|
||||
console.log('[DEBUG] /messages/send: canSend:', permissionCheck.canSend, 'senderRole:', senderRole, 'recipientRole:', recipientRole, 'error:', permissionCheck.errorMessage);
|
||||
|
||||
if (!permissionCheck.canSend) {
|
||||
return res.status(403).json({
|
||||
error: 'Недостаточно прав для отправки сообщения этому получателю'
|
||||
error: permissionCheck.errorMessage || 'Недостаточно прав для отправки сообщения этому получателю'
|
||||
});
|
||||
}
|
||||
|
||||
// Получаем ключ шифрования
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
// ✨ Используем unifiedMessageProcessor для унификации
|
||||
const unifiedMessageProcessor = require('../services/unifiedMessageProcessor');
|
||||
const identityService = require('../services/identity-service');
|
||||
|
||||
// Находим или создаем беседу
|
||||
let conversationResult = await db.getQuery()(
|
||||
'SELECT id FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC LIMIT 1',
|
||||
[recipientId]
|
||||
);
|
||||
|
||||
let conversationId;
|
||||
if (conversationResult.rows.length === 0) {
|
||||
const title = `Чат с пользователем ${recipientId}`;
|
||||
const newConv = await db.getQuery()(
|
||||
'INSERT INTO conversations (user_id, title_encrypted, created_at, updated_at) VALUES ($1, encrypt_text($2, $3), NOW(), NOW()) RETURNING id',
|
||||
[recipientId, title, encryptionKey]
|
||||
);
|
||||
conversationId = newConv.rows[0].id;
|
||||
} else {
|
||||
conversationId = conversationResult.rows[0].id;
|
||||
// Получаем wallet идентификатор отправителя
|
||||
const walletIdentity = await identityService.findIdentity(senderId, 'wallet');
|
||||
if (!walletIdentity) {
|
||||
return res.status(403).json({
|
||||
error: 'Требуется подключение кошелька'
|
||||
});
|
||||
}
|
||||
|
||||
// Сохраняем сообщение с типом
|
||||
const result = await db.getQuery()(
|
||||
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, message_type, created_at)
|
||||
VALUES ($1, $2, encrypt_text($3, $8), encrypt_text($4, $8), encrypt_text($5, $8), encrypt_text($6, $8), encrypt_text($7, $8), $9, NOW()) RETURNING *`,
|
||||
[recipientId, conversationId, 'editor', content, 'web', 'user', 'out', encryptionKey, messageType]
|
||||
);
|
||||
const identifier = `wallet:${walletIdentity.provider_id}`;
|
||||
|
||||
// Отправляем обновление через WebSocket
|
||||
broadcastMessagesUpdate();
|
||||
// Обрабатываем через unifiedMessageProcessor
|
||||
const result = await unifiedMessageProcessor.processMessage({
|
||||
identifier: identifier,
|
||||
content: content,
|
||||
channel: 'web',
|
||||
attachments: [],
|
||||
conversationId: null, // unifiedMessageProcessor сам найдет/создаст беседу
|
||||
recipientId: recipientIdNum,
|
||||
userId: senderId,
|
||||
metadata: {
|
||||
messageType: messageType,
|
||||
markAsRead: markAsRead
|
||||
}
|
||||
});
|
||||
|
||||
// Если нужно отметить как прочитанное
|
||||
if (markAsRead) {
|
||||
@@ -453,7 +430,7 @@ router.post('/send', requireAuth, async (req, res) => {
|
||||
`INSERT INTO admin_read_messages (admin_id, user_id, last_read_at)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (admin_id, user_id) DO UPDATE SET last_read_at = EXCLUDED.last_read_at`,
|
||||
[senderId, recipientId, lastReadAt]
|
||||
[senderId, recipientIdNum, lastReadAt]
|
||||
);
|
||||
} catch (markError) {
|
||||
console.warn('[WARNING] /send mark-read error:', markError);
|
||||
@@ -461,7 +438,7 @@ router.post('/send', requireAuth, async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ success: true, message: result.rows[0] });
|
||||
res.json({ success: true, message: result });
|
||||
} catch (e) {
|
||||
console.error('[ERROR] /send:', e);
|
||||
res.status(500).json({ error: 'DB error', details: e.message });
|
||||
@@ -478,121 +455,60 @@ router.post('/private/send', requireAuth, async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Получаем информацию об отправителе и получателе
|
||||
const senderResult = await db.getQuery()(
|
||||
'SELECT id, role FROM users WHERE id = $1',
|
||||
[senderId]
|
||||
);
|
||||
const senderRole = req.user.role || req.user.userAccessLevel?.level || 'user';
|
||||
|
||||
// Получаем информацию о получателе
|
||||
const recipientResult = await db.getQuery()(
|
||||
'SELECT id, role FROM users WHERE id = $1',
|
||||
[recipientId]
|
||||
);
|
||||
|
||||
if (senderResult.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Отправитель не найден' });
|
||||
}
|
||||
|
||||
if (recipientResult.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Получатель не найден' });
|
||||
}
|
||||
|
||||
const sender = senderResult.rows[0];
|
||||
const recipient = recipientResult.rows[0];
|
||||
const recipientRole = recipientResult.rows[0].role;
|
||||
|
||||
// Проверяем права: только к админам-редакторам
|
||||
if (recipient.role !== 'editor') {
|
||||
// Используем централизованную проверку прав
|
||||
const { canSendMessage } = require('/app/shared/permissions');
|
||||
const permissionCheck = canSendMessage(senderRole, recipientRole, senderId, recipientId);
|
||||
|
||||
if (!permissionCheck.canSend) {
|
||||
return res.status(403).json({
|
||||
error: 'Приватные сообщения можно отправлять только админам-редакторам'
|
||||
error: permissionCheck.errorMessage || 'Недостаточно прав для отправки приватного сообщения'
|
||||
});
|
||||
}
|
||||
|
||||
// Получаем ключ шифрования
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
// ✨ Используем unifiedMessageProcessor для унификации
|
||||
const unifiedMessageProcessor = require('../services/unifiedMessageProcessor');
|
||||
const identityService = require('../services/identity-service');
|
||||
|
||||
// Находим или создаем приватную беседу
|
||||
let conversationResult = await db.getQuery()(
|
||||
`SELECT id FROM conversations
|
||||
WHERE user_id = $1 AND conversation_type = 'private'
|
||||
ORDER BY updated_at DESC LIMIT 1`,
|
||||
[recipientId] // Беседа принадлежит получателю (админу)
|
||||
);
|
||||
|
||||
let conversationId;
|
||||
if (conversationResult.rows.length === 0) {
|
||||
// Создаем новую приватную беседу
|
||||
const title = `Приватный чат с пользователем ${senderId}`;
|
||||
const newConv = await db.getQuery()(
|
||||
'INSERT INTO conversations (user_id, conversation_type, title_encrypted, created_at, updated_at) VALUES ($1, $2, encrypt_text($3, $4), NOW(), NOW()) RETURNING id',
|
||||
[recipientId, 'private', title, encryptionKey]
|
||||
);
|
||||
conversationId = newConv.rows[0].id;
|
||||
|
||||
// Добавляем участников в conversation_participants
|
||||
await db.getQuery()(
|
||||
'INSERT INTO conversation_participants (conversation_id, user_id) VALUES ($1, $2) ON CONFLICT DO NOTHING',
|
||||
[conversationId, senderId]
|
||||
);
|
||||
await db.getQuery()(
|
||||
'INSERT INTO conversation_participants (conversation_id, user_id) VALUES ($1, $2) ON CONFLICT DO NOTHING',
|
||||
[conversationId, recipientId]
|
||||
);
|
||||
} else {
|
||||
conversationId = conversationResult.rows[0].id;
|
||||
// Получаем wallet идентификатор отправителя
|
||||
const walletIdentity = await identityService.findIdentity(senderId, 'wallet');
|
||||
if (!walletIdentity) {
|
||||
return res.status(403).json({
|
||||
error: 'Требуется подключение кошелька'
|
||||
});
|
||||
}
|
||||
|
||||
// Сохраняем приватное сообщение
|
||||
const result = await db.getQuery()(
|
||||
`INSERT INTO messages (
|
||||
conversation_id,
|
||||
sender_id,
|
||||
user_id,
|
||||
sender_type_encrypted,
|
||||
content_encrypted,
|
||||
channel_encrypted,
|
||||
role_encrypted,
|
||||
direction_encrypted,
|
||||
message_type,
|
||||
created_at
|
||||
) VALUES (
|
||||
$1, $2, $3,
|
||||
encrypt_text($4, $10),
|
||||
encrypt_text($5, $10),
|
||||
encrypt_text($6, $10),
|
||||
encrypt_text($7, $10),
|
||||
encrypt_text($8, $10),
|
||||
$9,
|
||||
NOW()
|
||||
) RETURNING id`,
|
||||
[
|
||||
conversationId,
|
||||
senderId, // sender_id - ID отправителя
|
||||
recipientId, // user_id - ID получателя
|
||||
sender.role, // sender_type_encrypted
|
||||
content, // content_encrypted
|
||||
'web', // channel_encrypted
|
||||
sender.role, // role_encrypted
|
||||
'outgoing', // direction_encrypted
|
||||
'private', // message_type
|
||||
encryptionKey
|
||||
]
|
||||
);
|
||||
const identifier = `wallet:${walletIdentity.provider_id}`;
|
||||
|
||||
// Обновляем время последнего обновления беседы
|
||||
await db.getQuery()(
|
||||
'UPDATE conversations SET updated_at = NOW() WHERE id = $1',
|
||||
[conversationId]
|
||||
);
|
||||
|
||||
// Отправляем обновление через WebSocket
|
||||
const { broadcastMessagesUpdate } = require('../wsHub');
|
||||
broadcastMessagesUpdate();
|
||||
// Обрабатываем через unifiedMessageProcessor
|
||||
// Для приватных сообщений recipientId всегда = 1 (редактор)
|
||||
const result = await unifiedMessageProcessor.processMessage({
|
||||
identifier: identifier,
|
||||
content: content,
|
||||
channel: 'web',
|
||||
attachments: [],
|
||||
conversationId: null, // unifiedMessageProcessor сам найдет/создаст беседу
|
||||
recipientId: 1, // Приватные сообщения всегда к редактору
|
||||
userId: senderId,
|
||||
metadata: {}
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
messageId: result.rows[0].id,
|
||||
conversationId: conversationId
|
||||
message: result
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
@@ -615,16 +531,16 @@ router.get('/private/conversations', requireAuth, async (req, res) => {
|
||||
`SELECT DISTINCT
|
||||
c.id as conversation_id,
|
||||
c.user_id,
|
||||
decrypt_text(c.title_encrypted, $2) as title,
|
||||
c.title,
|
||||
c.updated_at,
|
||||
COUNT(m.id) as message_count
|
||||
FROM conversations c
|
||||
INNER JOIN conversation_participants cp ON c.id = cp.conversation_id
|
||||
LEFT JOIN messages m ON c.id = m.conversation_id AND m.message_type = 'private'
|
||||
LEFT JOIN messages m ON c.id = m.conversation_id AND m.message_type = 'admin_chat'
|
||||
WHERE cp.user_id = $1 AND c.conversation_type = 'private'
|
||||
GROUP BY c.id, c.user_id, c.title_encrypted, c.updated_at
|
||||
GROUP BY c.id, c.user_id, c.title, c.updated_at
|
||||
ORDER BY c.updated_at DESC`,
|
||||
[currentUserId, encryptionKey]
|
||||
[currentUserId]
|
||||
);
|
||||
|
||||
console.log('[DEBUG] /messages/private/conversations result:', result.rows);
|
||||
@@ -640,55 +556,6 @@ router.get('/private/conversations', requireAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/messages/private/:conversationId - получить историю приватного чата
|
||||
router.get('/private/:conversationId', requireAuth, async (req, res) => {
|
||||
const conversationId = req.params.conversationId;
|
||||
const currentUserId = req.user.id;
|
||||
|
||||
try {
|
||||
// Проверяем, что пользователь является участником этого чата
|
||||
const participantCheck = await db.getQuery()(
|
||||
'SELECT 1 FROM conversation_participants WHERE conversation_id = $1 AND user_id = $2',
|
||||
[conversationId, currentUserId]
|
||||
);
|
||||
|
||||
if (participantCheck.rows.length === 0) {
|
||||
return res.status(403).json({ error: 'Доступ запрещен' });
|
||||
}
|
||||
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
// Получаем историю сообщений
|
||||
const result = await db.getQuery()(
|
||||
`SELECT
|
||||
m.id,
|
||||
m.sender_id,
|
||||
m.user_id,
|
||||
decrypt_text(m.sender_type_encrypted, $2) as sender_type,
|
||||
decrypt_text(m.content_encrypted, $2) as content,
|
||||
decrypt_text(m.channel_encrypted, $2) as channel,
|
||||
decrypt_text(m.role_encrypted, $2) as role,
|
||||
decrypt_text(m.direction_encrypted, $2) as direction,
|
||||
m.message_type,
|
||||
m.created_at
|
||||
FROM messages m
|
||||
WHERE m.conversation_id = $1 AND m.message_type = 'private'
|
||||
ORDER BY m.created_at ASC`,
|
||||
[conversationId, encryptionKey]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
messages: result.rows
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[ERROR] /messages/private/:conversationId:', error);
|
||||
res.status(500).json({ error: 'DB error', details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/messages/private/unread-count - получить количество непрочитанных приватных сообщений
|
||||
router.get('/private/unread-count', requireAuth, async (req, res) => {
|
||||
const currentUserId = req.user.id;
|
||||
@@ -702,7 +569,7 @@ router.get('/private/unread-count', requireAuth, async (req, res) => {
|
||||
INNER JOIN conversation_participants cp ON c.id = cp.conversation_id
|
||||
WHERE cp.user_id = $1
|
||||
AND c.conversation_type = 'private'
|
||||
AND m.message_type = 'private'
|
||||
AND m.message_type = 'admin_chat'
|
||||
AND m.user_id = $1 -- сообщения адресованные текущему пользователю
|
||||
AND m.sender_id != $1 -- исключаем собственные сообщения
|
||||
AND NOT EXISTS (
|
||||
@@ -767,6 +634,55 @@ router.post('/private/mark-read', requireAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/messages/private/:conversationId - получить историю приватного чата
|
||||
router.get('/private/:conversationId', requireAuth, async (req, res) => {
|
||||
const conversationId = req.params.conversationId;
|
||||
const currentUserId = req.user.id;
|
||||
|
||||
try {
|
||||
// Проверяем, что пользователь является участником этого чата
|
||||
const participantCheck = await db.getQuery()(
|
||||
'SELECT 1 FROM conversation_participants WHERE conversation_id = $1 AND user_id = $2',
|
||||
[conversationId, currentUserId]
|
||||
);
|
||||
|
||||
if (participantCheck.rows.length === 0) {
|
||||
return res.status(403).json({ error: 'Доступ запрещен' });
|
||||
}
|
||||
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
// Получаем историю сообщений
|
||||
const result = await db.getQuery()(
|
||||
`SELECT
|
||||
m.id,
|
||||
m.sender_id,
|
||||
m.user_id,
|
||||
decrypt_text(m.sender_type_encrypted, $2) as sender_type,
|
||||
decrypt_text(m.content_encrypted, $2) as content,
|
||||
decrypt_text(m.channel_encrypted, $2) as channel,
|
||||
decrypt_text(m.role_encrypted, $2) as role,
|
||||
decrypt_text(m.direction_encrypted, $2) as direction,
|
||||
m.message_type,
|
||||
m.created_at
|
||||
FROM messages m
|
||||
WHERE m.conversation_id = $1 AND m.message_type = 'admin_chat'
|
||||
ORDER BY m.created_at ASC`,
|
||||
[conversationId, encryptionKey]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
messages: result.rows
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[ERROR] /messages/private/:conversationId:', error);
|
||||
res.status(500).json({ error: 'DB error', details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/messages/conversations?userId=123 - получить диалоги пользователя
|
||||
router.get('/conversations', requireAuth, async (req, res) => {
|
||||
const userId = req.query.userId;
|
||||
@@ -794,9 +710,9 @@ router.post('/conversations', requireAuth, async (req, res) => {
|
||||
|
||||
try {
|
||||
const result = await db.getQuery()(
|
||||
`INSERT INTO conversations (user_id, title_encrypted, created_at, updated_at)
|
||||
VALUES ($1, encrypt_text($2, $3), NOW(), NOW()) RETURNING *`,
|
||||
[userId, title || 'Новый диалог', encryptionKey]
|
||||
`INSERT INTO conversations (user_id, title, created_at, updated_at)
|
||||
VALUES ($1, $2, NOW(), NOW()) RETURNING *`,
|
||||
[userId, title || 'Новый диалог']
|
||||
);
|
||||
res.json({ success: true, conversation: result.rows[0] });
|
||||
} catch (e) {
|
||||
|
||||
@@ -16,7 +16,7 @@ const db = require('../db');
|
||||
const logger = require('../utils/logger');
|
||||
const { requireAuth } = require('../middleware/auth');
|
||||
const { requirePermission } = require('../middleware/permissions');
|
||||
const { PERMISSIONS } = require('../shared/permissions');
|
||||
const { PERMISSIONS, ROLES } = require('../shared/permissions');
|
||||
const { deleteUserById } = require('../services/userDeleteService');
|
||||
const { broadcastContactsUpdate } = require('../wsHub');
|
||||
// const userService = require('../services/userService');
|
||||
@@ -95,7 +95,6 @@ router.get('/', requireAuth, async (req, res, next) => {
|
||||
|
||||
// Фильтрация для USER - видит только editor админов и себя
|
||||
if (userRole === 'user') {
|
||||
const { ROLES } = require('/app/shared/permissions');
|
||||
where.push(`(u.role = '${ROLES.EDITOR}' OR u.id = $${idx++})`);
|
||||
params.push(req.user.id);
|
||||
}
|
||||
@@ -375,11 +374,12 @@ router.post('/mark-contact-read', async (req, res) => {
|
||||
|
||||
if (req.user?.userAccessLevel) {
|
||||
// Используем новую систему ролей
|
||||
const { ROLES } = require('/app/shared/permissions');
|
||||
if (req.user.userAccessLevel.level === ROLES.READONLY) {
|
||||
if (req.user.userAccessLevel.level === ROLES.READONLY || req.user.userAccessLevel.level === 'readonly') {
|
||||
userRole = ROLES.READONLY;
|
||||
} else if (req.user.userAccessLevel.level === ROLES.EDITOR) {
|
||||
} else if (req.user.userAccessLevel.level === ROLES.EDITOR || req.user.userAccessLevel.level === 'editor') {
|
||||
userRole = ROLES.EDITOR;
|
||||
} else if (req.user.userAccessLevel.level === ROLES.USER || req.user.userAccessLevel.level === 'user') {
|
||||
userRole = ROLES.USER;
|
||||
}
|
||||
} else if (req.user?.id) {
|
||||
// Fallback для старой системы
|
||||
@@ -760,7 +760,6 @@ router.post('/', async (req, res) => {
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
// Используем централизованную систему ролей
|
||||
const { ROLES } = require('/app/shared/permissions');
|
||||
|
||||
try {
|
||||
const result = await db.getQuery()(
|
||||
@@ -824,7 +823,6 @@ router.post('/import', requireAuth, async (req, res) => {
|
||||
}
|
||||
} else {
|
||||
// Создаём нового пользователя с централизованной ролью
|
||||
const { ROLES } = require('/app/shared/permissions');
|
||||
const ins = await dbq('INSERT INTO users (first_name_encrypted, last_name_encrypted, role, created_at) VALUES (encrypt_text($1, $4), encrypt_text($2, $4), $3, NOW()) RETURNING id', [first_name, last_name, ROLES.USER, encryptionKey]);
|
||||
userId = ins.rows[0].id;
|
||||
added++;
|
||||
|
||||
Reference in New Issue
Block a user