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++;
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
]
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
248
database_schema_check.md
Normal file
248
database_schema_check.md
Normal file
@@ -0,0 +1,248 @@
|
||||
# Проверка схемы базы данных
|
||||
|
||||
## Таблица `users`
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username VARCHAR(255),
|
||||
email VARCHAR(255) UNIQUE,
|
||||
address VARCHAR(255) UNIQUE,
|
||||
first_name_encrypted TEXT,
|
||||
last_name_encrypted TEXT,
|
||||
status VARCHAR(50) DEFAULT 'active',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
role user_role DEFAULT 'user',
|
||||
first_name VARCHAR(255),
|
||||
last_name VARCHAR(255),
|
||||
preferred_language JSONB,
|
||||
is_blocked BOOLEAN DEFAULT false,
|
||||
blocked_at TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
**Колонки:**
|
||||
- `id` - SERIAL PRIMARY KEY
|
||||
- `username` - VARCHAR(255)
|
||||
- `email` - VARCHAR(255) UNIQUE
|
||||
- `address` - VARCHAR(255) UNIQUE
|
||||
- `first_name_encrypted` - TEXT (зашифрованное)
|
||||
- `last_name_encrypted` - TEXT (зашифрованное)
|
||||
- `status` - VARCHAR(50) DEFAULT 'active'
|
||||
- `created_at` - TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
- `updated_at` - TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
- `role` - user_role DEFAULT 'user'
|
||||
- `first_name` - VARCHAR(255) (незашифрованное)
|
||||
- `last_name` - VARCHAR(255) (незашифрованное)
|
||||
- `preferred_language` - JSONB
|
||||
- `is_blocked` - BOOLEAN DEFAULT false
|
||||
- `blocked_at` - TIMESTAMP
|
||||
|
||||
## Таблица `conversations`
|
||||
```sql
|
||||
CREATE TABLE conversations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
title VARCHAR(255),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
conversation_type VARCHAR(50) DEFAULT 'user_chat'
|
||||
);
|
||||
```
|
||||
|
||||
**Колонки:**
|
||||
- `id` - SERIAL PRIMARY KEY
|
||||
- `user_id` - INTEGER REFERENCES users(id)
|
||||
- `title` - VARCHAR(255) (НЕ зашифрованное!)
|
||||
- `created_at` - TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
- `updated_at` - TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
- `conversation_type` - VARCHAR(50) DEFAULT 'user_chat'
|
||||
|
||||
## Таблица `messages`
|
||||
```sql
|
||||
CREATE TABLE messages (
|
||||
id SERIAL PRIMARY KEY,
|
||||
conversation_id INTEGER REFERENCES conversations(id) ON DELETE CASCADE,
|
||||
sender_type_encrypted TEXT NOT NULL,
|
||||
sender_id INTEGER,
|
||||
content_encrypted TEXT,
|
||||
channel_encrypted TEXT NOT NULL,
|
||||
role_encrypted TEXT NOT NULL DEFAULT 'user',
|
||||
direction_encrypted TEXT,
|
||||
metadata JSONB,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
tokens_used INTEGER DEFAULT 0,
|
||||
is_processed BOOLEAN DEFAULT false,
|
||||
role VARCHAR(20) NOT NULL DEFAULT 'user',
|
||||
attachment_filename TEXT,
|
||||
attachment_mimetype TEXT,
|
||||
attachment_size BIGINT,
|
||||
attachment_data BYTEA,
|
||||
direction VARCHAR(8),
|
||||
message_type VARCHAR(20) DEFAULT 'public'
|
||||
);
|
||||
```
|
||||
|
||||
**Колонки:**
|
||||
- `id` - SERIAL PRIMARY KEY
|
||||
- `conversation_id` - INTEGER REFERENCES conversations(id)
|
||||
- `sender_type_encrypted` - TEXT NOT NULL (зашифрованное)
|
||||
- `sender_id` - INTEGER
|
||||
- `content_encrypted` - TEXT (зашифрованное)
|
||||
- `channel_encrypted` - TEXT NOT NULL (зашифрованное)
|
||||
- `role_encrypted` - TEXT NOT NULL DEFAULT 'user' (зашифрованное)
|
||||
- `direction_encrypted` - TEXT (зашифрованное)
|
||||
- `metadata` - JSONB
|
||||
- `created_at` - TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
- `user_id` - INTEGER REFERENCES users(id)
|
||||
- `tokens_used` - INTEGER DEFAULT 0
|
||||
- `is_processed` - BOOLEAN DEFAULT false
|
||||
- `role` - VARCHAR(20) NOT NULL DEFAULT 'user' (НЕ зашифрованное!)
|
||||
- `attachment_filename` - TEXT
|
||||
- `attachment_mimetype` - TEXT
|
||||
- `attachment_size` - BIGINT
|
||||
- `attachment_data` - BYTEA
|
||||
- `direction` - VARCHAR(8) (НЕ зашифрованное!)
|
||||
- `message_type` - VARCHAR(20) DEFAULT 'public'
|
||||
|
||||
**Триггеры:**
|
||||
- `trg_set_message_user_id` - автоматически устанавливает user_id
|
||||
|
||||
## Таблица `conversation_participants`
|
||||
```sql
|
||||
CREATE TABLE conversation_participants (
|
||||
id SERIAL PRIMARY KEY,
|
||||
conversation_id INTEGER REFERENCES conversations(id) ON DELETE CASCADE,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(conversation_id, user_id)
|
||||
);
|
||||
```
|
||||
|
||||
**Колонки:**
|
||||
- `id` - SERIAL PRIMARY KEY
|
||||
- `conversation_id` - INTEGER REFERENCES conversations(id)
|
||||
- `user_id` - INTEGER REFERENCES users(id)
|
||||
- `created_at` - TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
|
||||
**Индексы:**
|
||||
- PRIMARY KEY на `id`
|
||||
- UNIQUE CONSTRAINT на `(conversation_id, user_id)`
|
||||
- Индекс на `conversation_id`
|
||||
- Индекс на `user_id`
|
||||
|
||||
## Таблица `admin_read_messages`
|
||||
```sql
|
||||
CREATE TABLE admin_read_messages (
|
||||
admin_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
last_read_at TIMESTAMP NOT NULL,
|
||||
PRIMARY KEY (admin_id, user_id)
|
||||
);
|
||||
```
|
||||
|
||||
**Колонки:**
|
||||
- `admin_id` - INTEGER NOT NULL REFERENCES users(id) (админ)
|
||||
- `user_id` - INTEGER NOT NULL REFERENCES users(id) (пользователь)
|
||||
- `last_read_at` - TIMESTAMP NOT NULL (время последнего прочтения)
|
||||
|
||||
**Индексы:**
|
||||
- PRIMARY KEY на `(admin_id, user_id)`
|
||||
|
||||
## Таблица `admin_read_contacts`
|
||||
```sql
|
||||
CREATE TABLE admin_read_contacts (
|
||||
admin_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
contact_id TEXT NOT NULL,
|
||||
read_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (admin_id, contact_id)
|
||||
);
|
||||
```
|
||||
|
||||
**Колонки:**
|
||||
- `admin_id` - INTEGER NOT NULL REFERENCES users(id) (админ)
|
||||
- `contact_id` - TEXT NOT NULL (ID контакта)
|
||||
- `read_at` - TIMESTAMP NOT NULL DEFAULT NOW() (время прочтения)
|
||||
|
||||
**Индексы:**
|
||||
- PRIMARY KEY на `(admin_id, contact_id)`
|
||||
- Индекс на `admin_id`
|
||||
- Индекс на `contact_id`
|
||||
|
||||
## Таблица `user_identities`
|
||||
```sql
|
||||
CREATE TABLE user_identities (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
provider_encrypted TEXT NOT NULL,
|
||||
provider_id_encrypted TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
**Колонки:**
|
||||
- `id` - SERIAL PRIMARY KEY
|
||||
- `user_id` - INTEGER REFERENCES users(id) (пользователь)
|
||||
- `provider_encrypted` - TEXT NOT NULL (зашифрованный провайдер)
|
||||
- `provider_id_encrypted` - TEXT NOT NULL (зашифрованный ID провайдера)
|
||||
- `created_at` - TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
|
||||
**Индексы:**
|
||||
- PRIMARY KEY на `id`
|
||||
- Индекс на `user_id`
|
||||
|
||||
## Таблица `user_preferences`
|
||||
```sql
|
||||
CREATE TABLE user_preferences (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
preference_key VARCHAR(50) NOT NULL,
|
||||
preference_value TEXT,
|
||||
metadata JSONB DEFAULT '{}',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(user_id, preference_key)
|
||||
);
|
||||
```
|
||||
|
||||
**Колонки:**
|
||||
- `id` - SERIAL PRIMARY KEY
|
||||
- `user_id` - INTEGER NOT NULL REFERENCES users(id) (пользователь)
|
||||
- `preference_key` - VARCHAR(50) NOT NULL (ключ настройки)
|
||||
- `preference_value` - TEXT (значение настройки)
|
||||
- `metadata` - JSONB DEFAULT '{}' (метаданные)
|
||||
- `created_at` - TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
- `updated_at` - TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
|
||||
**Индексы:**
|
||||
- PRIMARY KEY на `id`
|
||||
- Индекс на `user_id`
|
||||
- UNIQUE CONSTRAINT на `(user_id, preference_key)`
|
||||
|
||||
## Таблица `user_tag_links`
|
||||
```sql
|
||||
CREATE TABLE user_tag_links (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
tag_id INTEGER NOT NULL REFERENCES user_rows(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(user_id, tag_id)
|
||||
);
|
||||
```
|
||||
|
||||
**Колонки:**
|
||||
- `id` - SERIAL PRIMARY KEY
|
||||
- `user_id` - INTEGER NOT NULL REFERENCES users(id) (пользователь)
|
||||
- `tag_id` - INTEGER NOT NULL REFERENCES user_rows(id) (тег)
|
||||
- `created_at` - TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
|
||||
**Индексы:**
|
||||
- PRIMARY KEY на `id`
|
||||
- Индекс на `user_id`
|
||||
- Индекс на `tag_id`
|
||||
- UNIQUE CONSTRAINT на `(user_id, tag_id)`
|
||||
|
||||
## Анализ проблем в коде
|
||||
|
||||
Теперь, имея полную схему базы данных, давайте проверим код на соответствие:
|
||||
@@ -522,10 +522,11 @@ async function handleAiReply() {
|
||||
.chat-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
max-height: 100vh;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
@@ -533,6 +534,7 @@ async function handleAiReply() {
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
padding-bottom: 8px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
@@ -544,49 +546,13 @@ async function handleAiReply() {
|
||||
right: 0;
|
||||
border-radius: 12px 12px 0 0;
|
||||
box-shadow: 0 -2px 8px rgba(0,0,0,0.04);
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-height: 500px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
background: transparent;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
padding: var(--spacing-lg);
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
background: var(--color-white);
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
border-top: 1px solid #e9ecef;
|
||||
flex-shrink: 0;
|
||||
transition: all var(--transition-normal);
|
||||
z-index: 10;
|
||||
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.05);
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.chat-input textarea {
|
||||
width: 100%;
|
||||
border: none;
|
||||
|
||||
@@ -82,18 +82,19 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-if="canViewContacts"><input type="checkbox" v-model="selectAll" @change="toggleSelectAll" /></th>
|
||||
<th>ID</th>
|
||||
<th>Тип</th>
|
||||
<th>Имя</th>
|
||||
<th>Email</th>
|
||||
<th>Telegram</th>
|
||||
<th>Кошелек</th>
|
||||
<th>Дата создания</th>
|
||||
<th>Действие</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="contact in filteredContacts" :key="contact.id" :class="{ 'new-contact-row': newIds.includes(contact.id) }">
|
||||
<td v-if="canViewContacts"><input type="checkbox" v-model="selectedIds" :value="contact.id" /></td>
|
||||
<tr v-for="contact in filteredContacts" :key="contact.id" :class="{ 'new-contact-row': newIds.includes(contact.id) }" @click="goToContactDetails(contact.id)" style="cursor: pointer;">
|
||||
<td v-if="canViewContacts" @click.stop><input type="checkbox" v-model="selectedIds" :value="contact.id" /></td>
|
||||
<td>{{ contact.id }}</td>
|
||||
<td>
|
||||
<span
|
||||
v-if="getRoleDisplayName(contact.role)"
|
||||
@@ -104,14 +105,10 @@
|
||||
<span v-else class="user-badge">Неизвестно</span>
|
||||
</td>
|
||||
<td>{{ contact.name || '-' }}</td>
|
||||
<td>{{ contact.email || '-' }}</td>
|
||||
<td>{{ contact.telegram || '-' }}</td>
|
||||
<td>{{ contact.wallet || '-' }}</td>
|
||||
<td>{{ maskPersonalData(contact.email) }}</td>
|
||||
<td>{{ maskPersonalData(contact.telegram) }}</td>
|
||||
<td>{{ maskPersonalData(contact.wallet) }}</td>
|
||||
<td>{{ contact.created_at ? new Date(contact.created_at).toLocaleString() : '-' }}</td>
|
||||
<td>
|
||||
<span v-if="newMsgUserIds.includes(String(contact.id))" class="new-msg-icon" title="Новое сообщение">✉️</span>
|
||||
<button class="details-btn" @click="showDetails(contact)">Подробнее</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -132,6 +129,7 @@ import { useTagsWebSocket } from '../composables/useTagsWebSocket';
|
||||
import { useContactsAndMessagesWebSocket } from '../composables/useContactsWebSocket';
|
||||
import { usePermissions } from '@/composables/usePermissions';
|
||||
import { useAuthContext } from '@/composables/useAuth';
|
||||
import { PERMISSIONS } from '/app/shared/permissions.js';
|
||||
import api from '../api/axios';
|
||||
import { sendMessage, getPrivateUnreadCount } from '../services/messagesService';
|
||||
import { useRoles } from '@/composables/useRoles';
|
||||
@@ -147,7 +145,7 @@ const contactsArray = computed(() => props.contacts || []);
|
||||
const newIds = computed(() => props.newContacts.map(c => c.id));
|
||||
const newMsgUserIds = computed(() => props.newMessages.map(m => String(m.user_id)));
|
||||
const router = useRouter();
|
||||
const { canViewContacts, canSendToUsers, canDeleteData, canDeleteMessages, canManageSettings, canChatWithAdmins, canEditData } = usePermissions();
|
||||
const { canViewContacts, canSendToUsers, canDeleteData, canDeleteMessages, canManageSettings, canChatWithAdmins, canEditData, hasPermission } = usePermissions();
|
||||
const { userAccessLevel, userId, isAuthenticated } = useAuthContext();
|
||||
const { roles, getRoleDisplayName, getRoleClass, fetchRoles, clearRoles } = useRoles();
|
||||
|
||||
@@ -175,6 +173,19 @@ async function loadPrivateUnreadCount() {
|
||||
}
|
||||
}
|
||||
|
||||
// Функция маскировки персональных данных для читателей
|
||||
function maskPersonalData(data) {
|
||||
if (!data || data === '-') return '-';
|
||||
|
||||
// Если пользователь имеет права редактора, показываем полные данные
|
||||
if (hasPermission(PERMISSIONS.MANAGE_LEGAL_DOCS)) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// Для читателей маскируем данные полностью звездочками
|
||||
return '***';
|
||||
}
|
||||
|
||||
// Новый фильтр тегов через мультисвязи
|
||||
const availableTags = ref([]);
|
||||
const selectedTagIds = ref([]);
|
||||
@@ -404,14 +415,14 @@ function formatDate(date) {
|
||||
if (!date) return '-';
|
||||
return new Date(date).toLocaleString();
|
||||
}
|
||||
async function showDetails(contact) {
|
||||
async function goToContactDetails(contactId) {
|
||||
if (props.markContactAsRead) {
|
||||
await props.markContactAsRead(contact.id);
|
||||
await props.markContactAsRead(contactId);
|
||||
}
|
||||
if (props.markMessagesAsReadForUser) {
|
||||
props.markMessagesAsReadForUser(contact.id);
|
||||
props.markMessagesAsReadForUser(contactId);
|
||||
}
|
||||
router.push({ name: 'contact-details', params: { id: contact.id } });
|
||||
router.push({ name: 'contact-details', params: { id: contactId } });
|
||||
}
|
||||
|
||||
function onImported() {
|
||||
@@ -430,7 +441,7 @@ async function openChatForSelected() {
|
||||
if (!contact) return;
|
||||
|
||||
// Открываем чат с этим контактом (user_chat)
|
||||
await showDetails(contact);
|
||||
await goToContactDetails(contact.id);
|
||||
}
|
||||
|
||||
// Новая функция для отправки публичного сообщения
|
||||
@@ -448,7 +459,7 @@ function sendPublicMessage() {
|
||||
}
|
||||
|
||||
// Открываем страницу детали контакта с чатом для публичных сообщений
|
||||
showDetails(contact);
|
||||
goToContactDetails(contactId);
|
||||
}
|
||||
|
||||
// Функция для открытия приватного чата
|
||||
|
||||
@@ -40,6 +40,11 @@
|
||||
<!-- Текстовый контент, если есть -->
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-if="message.content" class="message-content" v-html="formattedContent" />
|
||||
|
||||
<!-- Ссылка "Ответить" для публичных сообщений от других пользователей -->
|
||||
<div v-if="shouldShowReplyLink" class="message-reply-link">
|
||||
<a :href="replyLink" class="reply-link">Ответить</a>
|
||||
</div>
|
||||
|
||||
<!-- Кнопки для системного сообщения -->
|
||||
<div v-if="message.sender_type === 'system' && (message.telegramBotUrl || message.supportEmail)" class="system-actions">
|
||||
@@ -127,6 +132,11 @@ const isCurrentUserMessage = computed(() => {
|
||||
return props.message.sender_id === props.message.user_id;
|
||||
}
|
||||
|
||||
// Для публичных сообщений сравниваем sender_id с currentUserId
|
||||
if (props.message.message_type === 'public' && props.currentUserId) {
|
||||
return props.message.sender_id == props.currentUserId;
|
||||
}
|
||||
|
||||
// Для обычных сообщений используем стандартную логику
|
||||
return props.message.sender_type === 'user' || props.message.role === 'user';
|
||||
});
|
||||
@@ -145,6 +155,22 @@ const formatWalletAddress = (address) => {
|
||||
return address;
|
||||
};
|
||||
|
||||
// --- Логика ссылки "Ответить" для публичных сообщений ---
|
||||
const shouldShowReplyLink = computed(() => {
|
||||
// Показываем ссылку только для публичных сообщений от других пользователей
|
||||
return props.message.message_type === 'public' &&
|
||||
!isCurrentUserMessage.value &&
|
||||
props.message.sender_id &&
|
||||
props.currentUserId &&
|
||||
props.message.sender_id !== props.currentUserId;
|
||||
});
|
||||
|
||||
const replyLink = computed(() => {
|
||||
if (!shouldShowReplyLink.value) return '';
|
||||
// Ссылка ведет на страницу контакта отправителя
|
||||
return `/contacts/${props.message.sender_id}`;
|
||||
});
|
||||
|
||||
// --- Работа с вложениями ---
|
||||
const attachment = computed(() => {
|
||||
// Ожидаем массив attachments, даже если там только один элемент
|
||||
@@ -554,6 +580,30 @@ function copyEmail(email) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Стили для ссылки "Ответить" */
|
||||
.message-reply-link {
|
||||
margin-top: var(--spacing-xs);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.reply-link {
|
||||
color: var(--color-primary, #007bff);
|
||||
text-decoration: none;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
border-radius: var(--radius-sm);
|
||||
background-color: rgba(0, 123, 255, 0.1);
|
||||
transition: all 0.2s ease;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.reply-link:hover {
|
||||
background-color: rgba(0, 123, 255, 0.2);
|
||||
color: var(--color-primary-dark, #0056b3);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Адаптивность для мобильных устройств */
|
||||
@media (max-width: 768px) {
|
||||
.private-current-user,
|
||||
|
||||
@@ -105,10 +105,16 @@ export function useChat(auth) {
|
||||
let totalMessages = -1;
|
||||
if (initial || messageLoading.value.offset === 0) {
|
||||
try {
|
||||
const countResponse = await api.get('/messages/public', { params: { count_only: true } });
|
||||
if (!countResponse.data.success) throw new Error('Не удалось получить количество сообщений');
|
||||
totalMessages = countResponse.data.total || countResponse.data.count || 0;
|
||||
// console.log(`[useChat] Всего сообщений в истории: ${totalMessages}`);
|
||||
// Получаем количество личных сообщений с ИИ
|
||||
const personalCountResponse = await api.get('/chat/history', { params: { count_only: true } });
|
||||
const personalCount = personalCountResponse.data.success ? (personalCountResponse.data.total || 0) : 0;
|
||||
|
||||
// Получаем количество публичных сообщений
|
||||
const publicCountResponse = await api.get('/messages/public', { params: { count_only: true } });
|
||||
const publicCount = publicCountResponse.data.success ? (publicCountResponse.data.total || 0) : 0;
|
||||
|
||||
totalMessages = personalCount + publicCount;
|
||||
// console.log(`[useChat] Всего сообщений в истории: ${totalMessages} (личные: ${personalCount}, публичные: ${publicCount})`);
|
||||
} catch(countError) {
|
||||
// console.error('[useChat] Ошибка получения количества сообщений:', countError);
|
||||
// Не прерываем выполнение, попробуем загрузить без total
|
||||
@@ -122,13 +128,41 @@ export function useChat(auth) {
|
||||
// console.log(`[useChat] Рассчитано начальное смещение: ${effectiveOffset}`);
|
||||
}
|
||||
|
||||
// Используем новый API для публичных сообщений с пагинацией
|
||||
const response = await api.get('/messages/public', {
|
||||
// Загружаем личные сообщения с ИИ
|
||||
const personalResponse = await api.get('/chat/history', {
|
||||
params: {
|
||||
offset: effectiveOffset,
|
||||
limit: messageLoading.value.limit
|
||||
}
|
||||
});
|
||||
|
||||
// Загружаем публичные сообщения от других пользователей
|
||||
const publicResponse = await api.get('/messages/public', {
|
||||
params: {
|
||||
offset: 0,
|
||||
limit: 50
|
||||
}
|
||||
});
|
||||
|
||||
// Объединяем сообщения
|
||||
let allMessages = [];
|
||||
if (personalResponse.data.success && personalResponse.data.messages) {
|
||||
allMessages = [...allMessages, ...personalResponse.data.messages];
|
||||
}
|
||||
if (publicResponse.data.success && publicResponse.data.messages) {
|
||||
allMessages = [...allMessages, ...publicResponse.data.messages];
|
||||
}
|
||||
|
||||
// Сортируем по времени создания
|
||||
allMessages.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
|
||||
|
||||
const response = {
|
||||
data: {
|
||||
success: true,
|
||||
messages: allMessages,
|
||||
total: allMessages.length
|
||||
}
|
||||
};
|
||||
|
||||
if (response.data.success && response.data.messages) {
|
||||
const loadedMessages = response.data.messages;
|
||||
|
||||
@@ -166,4 +166,13 @@ export async function markPrivateMessagesAsRead(conversationId) {
|
||||
conversationId
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
// Функция для загрузки личных сообщений с ИИ
|
||||
export async function getPersonalChatHistory(options = {}) {
|
||||
const { limit = 50, offset = 0 } = options;
|
||||
const { data } = await api.get('/chat/history', {
|
||||
params: { limit, offset }
|
||||
});
|
||||
return data;
|
||||
}
|
||||
@@ -33,6 +33,7 @@
|
||||
:messages="messages"
|
||||
:is-loading="isLoading || isConnectingWallet"
|
||||
:has-more-messages="messageLoading.hasMoreMessages"
|
||||
:currentUserId="auth.userId"
|
||||
v-model:newMessage="newMessage"
|
||||
v-model:attachments="attachments"
|
||||
@send-message="handleSendMessage"
|
||||
@@ -44,6 +45,7 @@
|
||||
:messages="messages"
|
||||
:is-loading="isLoading || isConnectingWallet"
|
||||
:has-more-messages="messageLoading.hasMoreMessages"
|
||||
:currentUserId="auth.userId"
|
||||
v-model:newMessage="newMessage"
|
||||
v-model:attachments="attachments"
|
||||
@send-message="handleSendMessage"
|
||||
@@ -161,6 +163,8 @@
|
||||
background-color: var(--color-white);
|
||||
border-radius: var(--radius-lg);
|
||||
height: calc(100vh - 40px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -171,6 +175,7 @@
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 2px solid #e9ecef;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-content h1 {
|
||||
@@ -190,6 +195,7 @@
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
<h2>Детали контакта</h2>
|
||||
<button class="close-btn" @click="goBack">×</button>
|
||||
</div>
|
||||
<div class="contact-info-block">
|
||||
<div class="contact-info-section">
|
||||
<div class="contact-info-block">
|
||||
<div><strong>ID пользователя:</strong> {{ contact.id }}</div>
|
||||
<div>
|
||||
<strong>Имя:</strong>
|
||||
<template v-if="canEditContacts">
|
||||
@@ -32,9 +34,9 @@
|
||||
{{ contact.name }}
|
||||
</template>
|
||||
</div>
|
||||
<div><strong>Email:</strong> {{ contact.email || '-' }}</div>
|
||||
<div><strong>Telegram:</strong> {{ contact.telegram || '-' }}</div>
|
||||
<div><strong>Кошелек:</strong> {{ contact.wallet || '-' }}</div>
|
||||
<div><strong>Email:</strong> {{ maskPersonalData(contact.email) }}</div>
|
||||
<div><strong>Telegram:</strong> {{ maskPersonalData(contact.telegram) }}</div>
|
||||
<div><strong>Кошелек:</strong> {{ maskPersonalData(contact.wallet) }}</div>
|
||||
<div>
|
||||
<strong>Язык:</strong>
|
||||
<div class="multi-select">
|
||||
@@ -101,6 +103,7 @@
|
||||
<button class="delete-history-btn" @click="deleteMessagesHistory">Удалить историю сообщений</button>
|
||||
<button class="delete-btn" @click="deleteContact">Удалить контакт</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="messages-block">
|
||||
<h3>Чат с пользователем</h3>
|
||||
@@ -109,9 +112,10 @@
|
||||
:isLoading="isLoadingMessages"
|
||||
:attachments="chatAttachments"
|
||||
:newMessage="chatNewMessage"
|
||||
:canSend="canSendToUsers"
|
||||
:canSend="canSendToUsers && !!address"
|
||||
:canGenerateAI="canGenerateAI"
|
||||
:canSelectMessages="canGenerateAI"
|
||||
:currentUserId="currentUserId"
|
||||
@send-message="handleSendMessage"
|
||||
@update:newMessage="val => chatNewMessage = val"
|
||||
@update:attachments="val => chatAttachments = val"
|
||||
@@ -160,11 +164,13 @@ import Message from '../../components/Message.vue';
|
||||
import ChatInterface from '../../components/ChatInterface.vue';
|
||||
import contactsService from '../../services/contactsService.js';
|
||||
import messagesService from '../../services/messagesService.js';
|
||||
import { getPublicMessages, getConversationByUserId } from '../../services/messagesService.js';
|
||||
import { getPublicMessages, getConversationByUserId, sendMessage, getPersonalChatHistory } from '../../services/messagesService.js';
|
||||
import { useAuthContext } from '@/composables/useAuth';
|
||||
import { usePermissions } from '@/composables/usePermissions';
|
||||
import { PERMISSIONS } from '/app/shared/permissions.js';
|
||||
import { useContactsAndMessagesWebSocket } from '@/composables/useContactsWebSocket';
|
||||
const { canEditContacts, canDeleteData, canManageTags, canBlockUsers, canSendToUsers, canGenerateAI, canViewContacts } = usePermissions();
|
||||
const { canEditContacts, canDeleteData, canManageTags, canBlockUsers, canSendToUsers, canGenerateAI, canViewContacts, hasPermission } = usePermissions();
|
||||
const { address, userId: currentUserId } = useAuthContext();
|
||||
const { markContactAsRead } = useContactsAndMessagesWebSocket();
|
||||
|
||||
// Подписываемся на централизованные события очистки и обновления данных
|
||||
@@ -214,6 +220,19 @@ const tagsTableId = ref(null);
|
||||
const { onTagsUpdate } = useTagsWebSocket();
|
||||
let unsubscribeFromTags = null;
|
||||
|
||||
// Функция маскировки персональных данных для читателей
|
||||
function maskPersonalData(data) {
|
||||
if (!data || data === '-') return '-';
|
||||
|
||||
// Если пользователь имеет права редактора, показываем полные данные
|
||||
if (hasPermission(PERMISSIONS.MANAGE_LEGAL_DOCS)) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// Для читателей маскируем данные полностью звездочками
|
||||
return '***';
|
||||
}
|
||||
|
||||
async function ensureTagsTable() {
|
||||
// Получаем все пользовательские таблицы
|
||||
const tables = await tablesService.getTables();
|
||||
@@ -402,16 +421,42 @@ async function loadMessages() {
|
||||
console.log('[ContactDetailsView] 📥 loadMessages START for:', contact.value.id);
|
||||
isLoadingMessages.value = true;
|
||||
try {
|
||||
// Загружаем только публичные сообщения этого пользователя с пагинацией
|
||||
const response = await getPublicMessages(contact.value.id, { limit: 50, offset: 0 });
|
||||
console.log('[ContactDetailsView] 📩 Loaded messages:', response.messages?.length || 0, 'for', contact.value.id);
|
||||
// Проверяем, является ли контакт собственным ID пользователя
|
||||
const isOwnContact = currentUserId.value && contact.value.id == currentUserId.value;
|
||||
|
||||
if (response.success && response.messages) {
|
||||
messages.value = response.messages;
|
||||
let allMessages = [];
|
||||
|
||||
if (isOwnContact) {
|
||||
// Для собственного ID загружаем И личные сообщения с ИИ, И публичные сообщения от других пользователей
|
||||
console.log('[ContactDetailsView] 🔍 Loading personal chat with AI + public messages for own ID:', contact.value.id);
|
||||
|
||||
// Загружаем личные сообщения с ИИ
|
||||
const personalResponse = await getPersonalChatHistory({ limit: 50, offset: 0 });
|
||||
if (personalResponse.success && personalResponse.messages) {
|
||||
allMessages = [...allMessages, ...personalResponse.messages];
|
||||
}
|
||||
|
||||
// Загружаем публичные сообщения от других пользователей (входящие)
|
||||
const publicResponse = await getPublicMessages(contact.value.id, { limit: 50, offset: 0 });
|
||||
if (publicResponse.success && publicResponse.messages) {
|
||||
allMessages = [...allMessages, ...publicResponse.messages];
|
||||
}
|
||||
|
||||
// Сортируем по времени создания
|
||||
allMessages.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
|
||||
|
||||
} else {
|
||||
messages.value = [];
|
||||
// Для других пользователей загружаем публичные сообщения между текущим пользователем и выбранным контактом
|
||||
console.log('[ContactDetailsView] 🔍 Loading public messages between current user and contact:', contact.value.id);
|
||||
const response = await getPublicMessages(contact.value.id, { limit: 50, offset: 0 });
|
||||
if (response.success && response.messages) {
|
||||
allMessages = response.messages;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[ContactDetailsView] 📩 Loaded messages:', allMessages.length, 'for', contact.value.id);
|
||||
messages.value = allMessages;
|
||||
|
||||
if (messages.value.length > 0) {
|
||||
lastMessageDate.value = messages.value[messages.value.length - 1].created_at;
|
||||
} else {
|
||||
@@ -487,27 +532,23 @@ async function handleSendMessage({ message, attachments }) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await messagesService.broadcastMessage({
|
||||
userId: contact.value.id,
|
||||
message,
|
||||
attachments
|
||||
const result = await sendMessage({
|
||||
recipientId: contact.value.id,
|
||||
content: message,
|
||||
messageType: 'public'
|
||||
});
|
||||
// Формируем текст результата для отображения админу
|
||||
let resultText = '';
|
||||
if (result && Array.isArray(result.results)) {
|
||||
resultText = 'Результат рассылки по каналам:';
|
||||
for (const r of result.results) {
|
||||
resultText += `\n${r.channel}: ${(r.status === 'sent' || r.status === 'saved') ? 'Успех' : 'Ошибка'}${r.error ? ' (' + r.error + ')' : ''}`;
|
||||
|
||||
if (result && result.success) {
|
||||
// Очищаем поле ввода после успешной отправки
|
||||
chatNewMessage.value = '';
|
||||
// Обновляем список сообщений
|
||||
await loadMessages();
|
||||
if (typeof ElMessageBox === 'function') {
|
||||
ElMessageBox.alert('Сообщение отправлено успешно', 'Успех', { type: 'success' });
|
||||
}
|
||||
} else {
|
||||
resultText = 'Не удалось получить подробный ответ от сервера.';
|
||||
throw new Error(result?.message || 'Неизвестная ошибка');
|
||||
}
|
||||
if (typeof ElMessageBox === 'function') {
|
||||
ElMessageBox.alert(resultText, 'Результат рассылки', { type: 'info' });
|
||||
} else {
|
||||
console.log('Результат рассылки:', resultText);
|
||||
}
|
||||
await loadMessages();
|
||||
} catch (e) {
|
||||
if (typeof ElMessageBox === 'function') {
|
||||
ElMessageBox.alert('Ошибка отправки: ' + (e?.response?.data?.error || e?.message || e), 'Ошибка', { type: 'error' });
|
||||
@@ -701,23 +742,28 @@ watch(userId, async () => {
|
||||
|
||||
<style scoped>
|
||||
.contact-details-page {
|
||||
padding: 32px 0;
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.contact-details-content {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 32px rgba(0,0,0,0.12);
|
||||
padding: 32px 24px 24px 24px;
|
||||
padding: 24px;
|
||||
width: 100%;
|
||||
margin-top: 40px;
|
||||
position: relative;
|
||||
overflow-x: auto;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
.contact-details-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.close-btn {
|
||||
background: none;
|
||||
@@ -730,8 +776,14 @@ watch(userId, async () => {
|
||||
.close-btn:hover {
|
||||
color: #333;
|
||||
}
|
||||
.contact-info-section {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.contact-info-block {
|
||||
margin-bottom: 18px;
|
||||
font-size: 1.08rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
@@ -752,6 +804,7 @@ watch(userId, async () => {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.delete-history-btn {
|
||||
@@ -858,6 +911,11 @@ watch(userId, async () => {
|
||||
border-radius: 10px;
|
||||
padding: 18px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 500px;
|
||||
max-height: 70vh;
|
||||
}
|
||||
.messages-list {
|
||||
max-height: 350px;
|
||||
|
||||
@@ -186,6 +186,54 @@ function getRoleDescription(role) {
|
||||
return descriptions[role] || 'Неизвестная роль';
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить, может ли отправитель отправлять сообщения получателю
|
||||
* @param {string} senderRole - Роль отправителя
|
||||
* @param {string} recipientRole - Роль получателя
|
||||
* @param {number} senderId - ID отправителя
|
||||
* @param {number} recipientId - ID получателя
|
||||
* @returns {Object} { canSend: boolean, errorMessage?: string }
|
||||
*/
|
||||
function canSendMessage(senderRole, recipientRole, senderId, recipientId) {
|
||||
// Проверяем базовое право на отправку сообщений
|
||||
if (!hasPermission(senderRole, PERMISSIONS.SEND_TO_USERS)) {
|
||||
return {
|
||||
canSend: false,
|
||||
errorMessage: 'У вас нет права на отправку сообщений'
|
||||
};
|
||||
}
|
||||
|
||||
// Собственный чат - всегда разрешен (для ИИ ассистента)
|
||||
if (senderId === recipientId) {
|
||||
return { canSend: true };
|
||||
}
|
||||
|
||||
// USER и READONLY могут писать только EDITOR
|
||||
if ((senderRole === 'user' || senderRole === 'readonly') && recipientRole === 'editor') {
|
||||
return { canSend: true };
|
||||
}
|
||||
|
||||
// EDITOR может писать всем (USER, READONLY, EDITOR)
|
||||
if (senderRole === 'editor') {
|
||||
return { canSend: true };
|
||||
}
|
||||
|
||||
// USER и READONLY НЕ могут писать друг другу
|
||||
if ((senderRole === 'user' || senderRole === 'readonly') &&
|
||||
(recipientRole === 'user' || recipientRole === 'readonly')) {
|
||||
return {
|
||||
canSend: false,
|
||||
errorMessage: 'Пользователи и читатели не могут отправлять сообщения друг другу'
|
||||
};
|
||||
}
|
||||
|
||||
// Остальные случаи запрещены
|
||||
return {
|
||||
canSend: false,
|
||||
errorMessage: `Роль ${senderRole} не может отправлять сообщения роли ${recipientRole}`
|
||||
};
|
||||
}
|
||||
|
||||
// Экспорты для CommonJS (Node.js)
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = {
|
||||
@@ -196,7 +244,8 @@ if (typeof module !== 'undefined' && module.exports) {
|
||||
getPermissionsForRole,
|
||||
hasAnyPermission,
|
||||
hasAllPermissions,
|
||||
getRoleDescription
|
||||
getRoleDescription,
|
||||
canSendMessage
|
||||
};
|
||||
}
|
||||
|
||||
@@ -209,7 +258,8 @@ export {
|
||||
getPermissionsForRole,
|
||||
hasAnyPermission,
|
||||
hasAllPermissions,
|
||||
getRoleDescription
|
||||
getRoleDescription,
|
||||
canSendMessage
|
||||
};
|
||||
|
||||
// CommonJS для Backend
|
||||
@@ -222,7 +272,8 @@ if (typeof module !== 'undefined' && module.exports) {
|
||||
getPermissionsForRole,
|
||||
hasAnyPermission,
|
||||
hasAllPermissions,
|
||||
getRoleDescription
|
||||
getRoleDescription,
|
||||
canSendMessage
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user