feat: новая функция

This commit is contained in:
2025-10-23 21:44:14 +03:00
parent 918da882d2
commit 6e21887c3b
17 changed files with 959 additions and 462 deletions

View File

@@ -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();

View File

@@ -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) {

View File

@@ -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++;

View File

@@ -449,33 +449,36 @@ class UniversalGuestService {
await db.getQuery()(
`INSERT INTO messages (
user_id,
conversation_id,
sender_id,
sender_type_encrypted,
content_encrypted,
channel_encrypted,
role_encrypted,
direction_encrypted,
attachment_filename_encrypted,
attachment_mimetype_encrypted,
attachment_filename,
attachment_mimetype,
attachment_size,
attachment_data,
message_type,
user_id,
role,
direction,
created_at
) VALUES (
$1, $2,
encrypt_text($3, $14),
encrypt_text($4, $14),
encrypt_text($5, $14),
encrypt_text($6, $14),
encrypt_text($7, $14),
encrypt_text($8, $14),
encrypt_text($9, $14),
$10, $11, $12, $13
encrypt_text($3, $17),
encrypt_text($4, $17),
encrypt_text($5, $17),
encrypt_text($6, $17),
encrypt_text($7, $17),
$8, $9, $10, $11,
$12, $13, $14, $15,
$16
)`,
[
userId,
conversationId,
userId, // sender_id
senderType,
msg.content,
msg.channel,
@@ -485,7 +488,10 @@ class UniversalGuestService {
msg.attachment_mimetype,
msg.attachment_size,
msg.attachment_data,
'public', // message_type для мигрированных сообщений
'user_chat', // message_type для мигрированных сообщений (личный чат с ИИ)
userId, // user_id
role, // role (незашифрованное)
direction, // direction (незашифрованное)
msg.created_at,
encryptionKey
]

View File

@@ -29,13 +29,14 @@ const logger = require('../utils/logger');
function shouldGenerateAiReply(params) {
const { senderType, userId, recipientId } = params;
// Обычные пользователи (USER, READONLY) всегда получают AI ответ
if (senderType !== 'editor') {
// Если recipientId не указан или равен userId - это личный чат с ИИ
// ИИ должен отвечать в личных чатах
if (!recipientId || recipientId === userId) {
return true;
}
// Админы-редакторы (EDITOR) НЕ получают AI ответы
// ни себе, ни другим админам (по спецификации)
// Если recipientId отличается от userId - это публичный чат между пользователями
// ИИ НЕ должен отвечать на сообщения между пользователями
return false;
}

View File

@@ -30,12 +30,12 @@ async function getOrCreateConversation(userId, title = 'Новая беседа'
// Ищем существующую активную беседу
const { rows: existing } = await db.getQuery()(
`SELECT id, user_id, decrypt_text(title_encrypted, $2) as title, created_at, updated_at
`SELECT id, user_id, title, created_at, updated_at
FROM conversations
WHERE user_id = $1
ORDER BY updated_at DESC
LIMIT 1`,
[userId, encryptionKey]
[userId]
);
if (existing.length > 0) {
@@ -44,10 +44,10 @@ async function getOrCreateConversation(userId, title = 'Новая беседа'
// Создаем новую беседу
const { rows: newConv } = await db.getQuery()(
`INSERT INTO conversations (user_id, title_encrypted)
VALUES ($1, encrypt_text($2, $3))
RETURNING id, user_id, decrypt_text(title_encrypted, $3) as title, created_at, updated_at`,
[userId, title, encryptionKey]
`INSERT INTO conversations (user_id, title)
VALUES ($1, $2)
RETURNING id, user_id, title, created_at, updated_at`,
[userId, title]
);
logger.info('[ConversationService] Создана новая беседа:', newConv[0].id);
@@ -59,6 +59,60 @@ async function getOrCreateConversation(userId, title = 'Новая беседа'
}
}
/**
* Получить или создать публичную беседу между двумя пользователями
* @param {number} userId1 - ID первого пользователя
* @param {number} userId2 - ID второго пользователя
* @returns {Promise<Object>}
*/
async function getOrCreatePublicConversation(userId1, userId2) {
try {
// Ищем существующую публичную беседу между этими пользователями
const { rows: existing } = await db.getQuery()(
`SELECT c.id, c.user_id, c.title, c.created_at, c.updated_at, c.conversation_type
FROM conversations c
INNER JOIN conversation_participants cp1 ON c.id = cp1.conversation_id
INNER JOIN conversation_participants cp2 ON c.id = cp2.conversation_id
WHERE c.conversation_type = 'public_chat'
AND cp1.user_id = $1 AND cp2.user_id = $2
ORDER BY c.created_at DESC
LIMIT 1`,
[userId1, userId2]
);
if (existing.length > 0) {
return existing[0];
}
// Создаем новую публичную беседу
const { rows: newConv } = await db.getQuery()(
`INSERT INTO conversations (user_id, title, conversation_type)
VALUES ($1, $2, 'public_chat')
RETURNING id, user_id, title, created_at, updated_at, conversation_type`,
[userId1, `Публичная беседа ${userId1}-${userId2}`]
);
const conversation = newConv[0];
// Добавляем участников
await db.getQuery()(
`INSERT INTO conversation_participants (conversation_id, user_id) VALUES ($1, $2)`,
[conversation.id, userId1]
);
await db.getQuery()(
`INSERT INTO conversation_participants (conversation_id, user_id) VALUES ($1, $2)`,
[conversation.id, userId2]
);
logger.info('[ConversationService] Создана публичная беседа:', conversation.id);
return conversation;
} catch (error) {
logger.error('[ConversationService] Ошибка создания публичной беседы:', error);
throw error;
}
}
/**
* Получить беседу по ID
* @param {number} conversationId - ID беседы
@@ -69,10 +123,10 @@ async function getConversationById(conversationId) {
const encryptionKey = encryptionUtils.getEncryptionKey();
const { rows } = await db.getQuery()(
`SELECT id, user_id, decrypt_text(title_encrypted, $2) as title, created_at, updated_at
`SELECT id, user_id, title, created_at, updated_at
FROM conversations
WHERE id = $1`,
[conversationId, encryptionKey]
[conversationId]
);
return rows.length > 0 ? rows[0] : null;
@@ -93,11 +147,11 @@ async function getUserConversations(userId) {
const encryptionKey = encryptionUtils.getEncryptionKey();
const { rows } = await db.getQuery()(
`SELECT id, user_id, decrypt_text(title_encrypted, $2) as title, created_at, updated_at
`SELECT id, user_id, title, created_at, updated_at
FROM conversations
WHERE user_id = $1
ORDER BY updated_at DESC`,
[userId, encryptionKey]
[userId]
);
return rows;
@@ -164,10 +218,10 @@ async function updateConversationTitle(conversationId, userId, newTitle) {
const { rows } = await db.getQuery()(
`UPDATE conversations
SET title_encrypted = encrypt_text($3, $4), updated_at = NOW()
SET title = $3, updated_at = NOW()
WHERE id = $1 AND user_id = $2
RETURNING id, user_id, decrypt_text(title_encrypted, $4) as title, created_at, updated_at`,
[conversationId, userId, newTitle, encryptionKey]
RETURNING id, user_id, title, created_at, updated_at`,
[conversationId, userId, newTitle]
);
return rows.length > 0 ? rows[0] : null;
@@ -180,6 +234,7 @@ async function updateConversationTitle(conversationId, userId, newTitle) {
module.exports = {
getOrCreateConversation,
getOrCreatePublicConversation,
getConversationById,
getUserConversations,
touchConversation,

View File

@@ -18,6 +18,60 @@ const conversationService = require('./conversationService');
const adminLogicService = require('./adminLogicService');
const universalGuestService = require('./UniversalGuestService');
const identityService = require('./identity-service');
/**
* Определить тип сообщения по контексту
* @param {number|null} recipientId - ID получателя
* @param {number} userId - ID отправителя
* @param {boolean} isAdminSender - Является ли отправитель админом
* @returns {string} - Тип сообщения: 'user_chat', 'admin_chat', 'public'
*/
function determineMessageType(recipientId, userId, isAdminSender) {
// 1. Личный чат с ИИ (recipientId не указан или равен userId)
if (!recipientId || recipientId === userId) {
return 'user_chat';
}
// 2. Приватное сообщение к редактору (recipientId = 1)
if (recipientId === 1) {
return 'admin_chat';
}
// 3. Публичное сообщение между пользователями
return 'public';
}
/**
* Определить тип беседы
* @param {string} messageType - Тип сообщения
* @param {number|null} recipientId - ID получателя
* @param {number} userId - ID отправителя
* @returns {string} - Тип беседы: 'user_chat', 'private', 'public'
*/
function determineConversationType(messageType, recipientId, userId) {
switch (messageType) {
case 'user_chat':
return 'user_chat'; // Личная беседа с ИИ
case 'admin_chat':
return 'private'; // Приватная беседа с редактором
case 'public':
return 'public_chat'; // Публичная беседа между пользователями
default:
return 'user_chat';
}
}
/**
* Определить, нужно ли генерировать AI ответ
* @param {string} messageType - Тип сообщения
* @param {number|null} recipientId - ID получателя
* @param {number} userId - ID отправителя
* @returns {boolean}
*/
function shouldGenerateAiReply(messageType, recipientId, userId) {
// ИИ отвечает только в личных чатах
return messageType === 'user_chat';
}
const { broadcastMessagesUpdate } = require('../wsHub');
// НОВАЯ СИСТЕМА РОЛЕЙ: используем shared/permissions.js
const { hasPermission, ROLES, PERMISSIONS } = require('/app/shared/permissions');
@@ -92,24 +146,39 @@ async function processMessage(messageData) {
// НОВАЯ СИСТЕМА РОЛЕЙ: определяем права через новую систему
const isAdmin = userRole === ROLES.EDITOR || userRole === ROLES.READONLY;
// 4. Определяем нужно ли генерировать AI ответ
const shouldGenerateAi = adminLogicService.shouldGenerateAiReply({
senderType: isAdmin ? 'editor' : 'user',
userId: userId,
recipientId: recipientId || userId,
channel: channel
});
// 4. Определяем тип сообщения по контексту
const messageType = determineMessageType(recipientId, userId, isAdmin);
// 5. Определяем нужно ли генерировать AI ответ
const shouldGenerateAi = shouldGenerateAiReply(messageType, recipientId, userId);
logger.info('[UnifiedMessageProcessor] Генерация AI:', { shouldGenerateAi, userRole, isAdmin });
// 5. Получаем или создаем беседу
// 6. Получаем или создаем беседу с правильным типом
let conversation;
const conversationType = determineConversationType(messageType, recipientId, userId);
if (inputConversationId) {
conversation = await conversationService.getConversationById(inputConversationId);
}
if (!conversation) {
conversation = await conversationService.getOrCreateConversation(userId, 'Беседа');
// Для публичных сообщений создаем беседу между пользователями
if (messageType === 'public') {
conversation = await conversationService.getOrCreatePublicConversation(userId, recipientId);
} else {
// Для личных и админских чатов используем стандартную логику
conversation = await conversationService.getOrCreateConversation(userId, 'Беседа');
}
// Обновляем тип беседы в БД, если он не соответствует
if (conversation.conversation_type !== conversationType) {
await db.getQuery()(
'UPDATE conversations SET conversation_type = $1 WHERE id = $2',
[conversationType, conversation.id]
);
conversation.conversation_type = conversationType;
}
}
const conversationId = conversation.id;
@@ -133,34 +202,38 @@ async function processMessage(messageData) {
const { rows } = await db.getQuery()(
`INSERT INTO messages (
user_id,
conversation_id,
sender_id,
sender_type_encrypted,
content_encrypted,
channel_encrypted,
role_encrypted,
direction_encrypted,
attachment_filename_encrypted,
attachment_mimetype_encrypted,
attachment_filename,
attachment_mimetype,
attachment_size,
attachment_data,
message_type,
user_id,
role,
direction,
created_at
) VALUES (
$1, $2,
encrypt_text($3, $13),
encrypt_text($4, $13),
encrypt_text($5, $13),
encrypt_text($6, $13),
encrypt_text($7, $13),
encrypt_text($8, $13),
encrypt_text($9, $13),
encrypt_text($3, $16),
encrypt_text($4, $16),
encrypt_text($5, $16),
encrypt_text($6, $16),
encrypt_text($7, $16),
$8,
$9,
$10, $11, $12,
$13, $14, $15,
NOW()
) RETURNING id`,
[
userId,
conversationId,
userId, // sender_id
isAdmin ? 'editor' : 'user',
content,
channel,
@@ -170,7 +243,10 @@ async function processMessage(messageData) {
attachment_mimetype,
attachment_size,
attachment_data,
'user_chat', // message_type
messageType, // message_type
recipientId || userId, // user_id (получатель для публичных сообщений)
'user', // role (незашифрованное)
'incoming', // direction (незашифрованное)
encryptionKey
]
);
@@ -220,34 +296,40 @@ async function processMessage(messageData) {
// Сохраняем ответ AI
const { rows: aiMessageRows } = await db.getQuery()(
`INSERT INTO messages (
user_id,
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, $9),
encrypt_text($4, $9),
encrypt_text($5, $9),
encrypt_text($6, $9),
encrypt_text($7, $9),
$8,
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()
) RETURNING id`,
[
userId,
conversationId,
userId, // sender_id
'assistant',
aiResponse.response,
channel,
'assistant',
'outgoing',
'user_chat',
messageType,
userId, // user_id
'assistant', // role (незашифрованное)
'outgoing', // direction (незашифрованное)
encryptionKey
]
);

View File

@@ -89,13 +89,7 @@ async function deleteUserById(userId) {
);
console.log('[DELETE] Удалено user_tag_links:', resTagLinks.rows.length);
// 9. Удаляем global_read_status
console.log('[DELETE] Начинаем удаление global_read_status для userId:', userId);
const resReadStatus = await db.getQuery()(
'DELETE FROM global_read_status WHERE user_id = $1 RETURNING user_id',
[userId]
);
console.log('[DELETE] Удалено global_read_status:', resReadStatus.rows.length);
// 9. global_read_status - таблица не существует, пропускаем
// 10. Удаляем самого пользователя
console.log('[DELETE] Начинаем удаление пользователя из users:', userId);