diff --git a/backend/routes/chat.js b/backend/routes/chat.js index 12153a3..efe5264 100644 --- a/backend/routes/chat.js +++ b/backend/routes/chat.js @@ -932,10 +932,10 @@ router.get('/history', requireAuth, async (req, res) => { try { // Если нужен только подсчет if (countOnly) { - let countQuery = 'SELECT COUNT(*) FROM messages WHERE user_id = $1'; - let countParams = [userId]; + let countQuery = 'SELECT COUNT(*) FROM messages WHERE user_id = $1 AND message_type = $2'; + let countParams = [userId, 'user_chat']; if (conversationId) { - countQuery += ' AND conversation_id = $2'; + countQuery += ' AND conversation_id = $3'; countParams.push(conversationId); } const countResult = await db.getQuery()(countQuery, countParams); @@ -944,7 +944,10 @@ router.get('/history', requireAuth, async (req, res) => { } // Загружаем сообщения через encryptedDb - const whereConditions = { user_id: userId }; + const whereConditions = { + user_id: userId, + message_type: 'user_chat' // Фильтруем только публичные сообщения + }; if (conversationId) { whereConditions.conversation_id = conversationId; } diff --git a/backend/routes/messages.js b/backend/routes/messages.js index d6533e2..6c75c7c 100644 --- a/backend/routes/messages.js +++ b/backend/routes/messages.js @@ -41,24 +41,25 @@ router.get('/', async (req, res) => { let result; if (conversationId) { result = await db.getQuery()( - `SELECT id, user_id, decrypt_text(sender_type_encrypted, $2) as sender_type, decrypt_text(content_encrypted, $2) as content, decrypt_text(channel_encrypted, $2) as channel, decrypt_text(role_encrypted, $2) as role, decrypt_text(direction_encrypted, $2) as direction, created_at, decrypt_text(attachment_filename_encrypted, $2) as attachment_filename, decrypt_text(attachment_mimetype_encrypted, $2) as attachment_mimetype, attachment_size, attachment_data + `SELECT id, user_id, decrypt_text(sender_type_encrypted, $2) as sender_type, decrypt_text(content_encrypted, $2) as content, decrypt_text(channel_encrypted, $2) as channel, decrypt_text(role_encrypted, $2) as role, decrypt_text(direction_encrypted, $2) as direction, created_at, decrypt_text(attachment_filename_encrypted, $2) as attachment_filename, decrypt_text(attachment_mimetype_encrypted, $2) as attachment_mimetype, attachment_size, attachment_data, message_type FROM messages - WHERE conversation_id = $1 + WHERE conversation_id = $1 AND message_type = 'user_chat' ORDER BY created_at ASC`, [conversationId, encryptionKey] ); } else if (userId) { result = await db.getQuery()( - `SELECT id, user_id, decrypt_text(sender_type_encrypted, $2) as sender_type, decrypt_text(content_encrypted, $2) as content, decrypt_text(channel_encrypted, $2) as channel, decrypt_text(role_encrypted, $2) as role, decrypt_text(direction_encrypted, $2) as direction, created_at, decrypt_text(attachment_filename_encrypted, $2) as attachment_filename, decrypt_text(attachment_mimetype_encrypted, $2) as attachment_mimetype, attachment_size, attachment_data + `SELECT id, user_id, decrypt_text(sender_type_encrypted, $2) as sender_type, decrypt_text(content_encrypted, $2) as content, decrypt_text(channel_encrypted, $2) as channel, decrypt_text(role_encrypted, $2) as role, decrypt_text(direction_encrypted, $2) as direction, created_at, decrypt_text(attachment_filename_encrypted, $2) as attachment_filename, decrypt_text(attachment_mimetype_encrypted, $2) as attachment_mimetype, attachment_size, attachment_data, message_type FROM messages - WHERE user_id = $1 + WHERE user_id = $1 AND message_type = 'user_chat' ORDER BY created_at ASC`, [userId, encryptionKey] ); } else { result = await db.getQuery()( - `SELECT id, user_id, decrypt_text(sender_type_encrypted, $1) as sender_type, decrypt_text(content_encrypted, $1) as content, decrypt_text(channel_encrypted, $1) as channel, decrypt_text(role_encrypted, $1) as role, decrypt_text(direction_encrypted, $1) as direction, created_at, decrypt_text(attachment_filename_encrypted, $1) as attachment_filename, decrypt_text(attachment_mimetype_encrypted, $1) as attachment_mimetype, attachment_size, attachment_data + `SELECT id, user_id, decrypt_text(sender_type_encrypted, $1) as sender_type, decrypt_text(content_encrypted, $1) as content, decrypt_text(channel_encrypted, $1) as channel, decrypt_text(role_encrypted, $1) as role, decrypt_text(direction_encrypted, $1) as direction, created_at, decrypt_text(attachment_filename_encrypted, $1) as attachment_filename, decrypt_text(attachment_mimetype_encrypted, $1) as attachment_mimetype, attachment_size, attachment_data, message_type FROM messages + WHERE message_type = 'user_chat' ORDER BY created_at ASC`, [encryptionKey] ); @@ -72,6 +73,34 @@ router.get('/', async (req, res) => { // POST /api/messages router.post('/', async (req, res) => { const { user_id, sender_type, content, channel, role, direction, attachment_filename, attachment_mimetype, attachment_size, attachment_data } = req.body; + + // Определяем тип сообщения + const senderId = req.user && req.user.id; + let messageType = 'user_chat'; // по умолчанию для публичных сообщений + + if (senderId) { + // Проверяем, является ли отправитель админом + const senderCheck = await db.getQuery()( + 'SELECT role FROM users WHERE id = $1', + [senderId] + ); + + if (senderCheck.rows.length > 0 && (senderCheck.rows[0].role === 'editor' || senderCheck.rows[0].role === 'readonly')) { + // Если отправитель админ, проверяем получателя + const recipientCheck = await db.getQuery()( + 'SELECT role FROM users WHERE id = $1', + [user_id] + ); + + // Если получатель тоже админ, то это приватное сообщение + if (recipientCheck.rows.length > 0 && (recipientCheck.rows[0].role === 'editor' || recipientCheck.rows[0].role === 'readonly')) { + messageType = 'admin_chat'; + } else { + // Если получатель обычный пользователь, то это публичное сообщение + messageType = 'user_chat'; + } + } + } // Получаем ключ шифрования const fs = require('fs'); @@ -120,29 +149,72 @@ router.post('/', async (req, res) => { return res.status(400).json({ error: 'У пользователя не привязан кошелёк. Сообщение не отправлено.' }); } } - // 1. Проверяем, есть ли беседа для user_id - 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', - [user_id, encryptionKey] - ); let conversation; - if (conversationResult.rows.length === 0) { - // 2. Если нет — создаём новую беседу - 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] - ); - conversation = newConv.rows[0]; + + if (messageType === 'admin_chat') { + // Для админских сообщений ищем приватную беседу через conversation_participants + let conversationResult = await db.getQuery()(` + SELECT c.id + FROM conversations c + INNER JOIN conversation_participants cp1 ON cp1.conversation_id = c.id AND cp1.user_id = $1 + INNER JOIN conversation_participants cp2 ON cp2.conversation_id = c.id AND cp2.user_id = $2 + WHERE c.conversation_type = 'admin_chat' + LIMIT 1 + `, [senderId, user_id]); + + if (conversationResult.rows.length === 0) { + // Создаем новую приватную беседу между админами + const title = `Приватная беседа ${senderId} - ${user_id}`; + const newConv = await db.getQuery()( + 'INSERT INTO conversations (user_id, title_encrypted, conversation_type, created_at, updated_at) VALUES ($1, encrypt_text($2, $3), $4, NOW(), NOW()) RETURNING *', + [user_id, title, encryptionKey, 'admin_chat'] + ); + conversation = newConv.rows[0]; + + // Добавляем участников в беседу + await db.getQuery()( + 'INSERT INTO conversation_participants (conversation_id, user_id) VALUES ($1, $2), ($1, $3)', + [conversation.id, senderId, user_id] + ); + } else { + conversation = { id: conversationResult.rows[0].id }; + } } else { - conversation = conversationResult.rows[0]; + // Для обычных пользовательских сообщений используем старую логику с user_id + 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', + [user_id, encryptionKey] + ); + + if (conversationResult.rows.length === 0) { + // Создаем новую беседу + const title = `Чат с пользователем ${user_id}`; + const newConv = await db.getQuery()( + 'INSERT INTO conversations (user_id, title_encrypted, conversation_type, created_at, updated_at) VALUES ($1, encrypt_text($2, $3), $4, NOW(), NOW()) RETURNING *', + [user_id, title, encryptionKey, 'user_chat'] + ); + conversation = newConv.rows[0]; + } else { + conversation = conversationResult.rows[0]; + } } // 3. Сохраняем сообщение с conversation_id - const result = await db.getQuery()( - `INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, created_at, attachment_filename_encrypted, attachment_mimetype_encrypted, attachment_size, attachment_data) - VALUES ($1,$2,encrypt_text($3,$12),encrypt_text($4,$12),encrypt_text($5,$12),encrypt_text($6,$12),encrypt_text($7,$12),NOW(),encrypt_text($8,$12),encrypt_text($9,$12),$10,$11) RETURNING *`, - [user_id, conversation.id, sender_type, content, channel, role, direction, attachment_filename, attachment_mimetype, attachment_size, attachment_data, encryptionKey] - ); + let result; + if (messageType === 'admin_chat') { + // Для админских сообщений добавляем sender_id + result = await db.getQuery()( + `INSERT INTO messages (conversation_id, user_id, sender_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, message_type, created_at, attachment_filename_encrypted, attachment_mimetype_encrypted, attachment_size, attachment_data) + VALUES ($1,$2,$3,encrypt_text($4,$13),encrypt_text($5,$13),encrypt_text($6,$13),encrypt_text($7,$13),encrypt_text($8,$13),$9,NOW(),encrypt_text($10,$13),encrypt_text($11,$13),$12,$14) RETURNING *`, + [conversation.id, user_id, senderId, sender_type, content, channel, role, direction, messageType, attachment_filename, attachment_mimetype, attachment_size, attachment_data, encryptionKey] + ); + } else { + // Для обычных сообщений без sender_id + 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, attachment_filename_encrypted, attachment_mimetype_encrypted, attachment_size, attachment_data) + VALUES ($1,$2,encrypt_text($3,$12),encrypt_text($4,$12),encrypt_text($5,$12),encrypt_text($6,$12),encrypt_text($7,$12),$13,NOW(),encrypt_text($8,$12),encrypt_text($9,$12),$10,$11) RETURNING *`, + [user_id, conversation.id, sender_type, content, channel, role, direction, messageType, attachment_filename, attachment_mimetype, attachment_size, attachment_data, encryptionKey] + ); + } // 4. Если это исходящее сообщение для Telegram — отправляем через бота if (channel === 'telegram' && direction === 'out') { try { @@ -186,7 +258,10 @@ router.post('/', async (req, res) => { // console.error('[messages.js] Ошибка отправки email:', err); } } + + // Отправляем WebSocket уведомления broadcastMessagesUpdate(); + res.json({ success: true, message: result.rows[0] }); } catch (e) { res.status(500).json({ error: 'DB error', details: e.message }); @@ -199,7 +274,8 @@ router.post('/mark-read', async (req, res) => { // console.log('[DEBUG] /mark-read req.user:', req.user); // console.log('[DEBUG] /mark-read req.body:', req.body); const adminId = req.user && req.user.id; - const { userId, lastReadAt } = req.body; + const { userId, lastReadAt, messageType = 'user_chat' } = req.body; + if (!adminId) { // console.error('[ERROR] /mark-read: adminId (req.user.id) is missing'); return res.status(401).json({ error: 'Unauthorized: adminId missing' }); @@ -208,12 +284,30 @@ router.post('/mark-read', async (req, res) => { // console.error('[ERROR] /mark-read: userId or lastReadAt missing'); return res.status(400).json({ error: 'userId and lastReadAt required' }); } - await db.query(` - 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 - `, [adminId, userId, lastReadAt]); - res.json({ success: true }); + + // Логика зависит от типа сообщения + if (messageType === 'user_chat') { + // Обновляем глобальный статус для всех админов + await db.query(` + INSERT INTO global_read_status (user_id, last_read_at, updated_by_admin_id) + VALUES ($1, $2, $3) + ON CONFLICT (user_id) DO UPDATE SET + last_read_at = EXCLUDED.last_read_at, + updated_by_admin_id = EXCLUDED.updated_by_admin_id, + updated_at = NOW() + `, [userId, lastReadAt, adminId]); + } else if (messageType === 'admin_chat') { + // Обновляем персональный статус для админских сообщений + await db.query(` + 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 + `, [adminId, userId, lastReadAt]); + } else { + return res.status(400).json({ error: 'Invalid messageType. Must be "user_chat" or "admin_chat"' }); + } + + res.json({ success: true, messageType }); } catch (e) { // console.error('[ERROR] /mark-read:', e); res.status(500).json({ error: e.message }); @@ -227,11 +321,24 @@ router.get('/read-status', async (req, res) => { // console.log('[DEBUG] /read-status req.session:', req.session); // console.log('[DEBUG] /read-status req.session.userId:', req.session && req.session.userId); const adminId = req.user && req.user.id; + const { messageType = 'user_chat' } = req.query; + if (!adminId) { // console.error('[ERROR] /read-status: adminId (req.user.id) is missing'); return res.status(401).json({ error: 'Unauthorized: adminId missing' }); } - const result = await db.query('SELECT user_id, last_read_at FROM admin_read_messages WHERE admin_id = $1', [adminId]); + + let result; + if (messageType === 'user_chat') { + // Возвращаем глобальный статус для сообщений с пользователями + result = await db.query('SELECT user_id, last_read_at FROM global_read_status'); + } else if (messageType === 'admin_chat') { + // Возвращаем персональный статус для админских сообщений + result = await db.query('SELECT user_id, last_read_at FROM admin_read_messages WHERE admin_id = $1', [adminId]); + } else { + return res.status(400).json({ error: 'Invalid messageType. Must be "user_chat" or "admin_chat"' }); + } + // console.log('[DEBUG] /read-status SQL result:', result.rows); const map = {}; for (const row of result.rows) { @@ -346,9 +453,9 @@ router.post('/broadcast', async (req, res) => { 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, 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), NOW())`, - [user_id, conversation.id, 'admin', content, 'email', 'user', 'out', encryptionKey] + `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, 'admin', content, 'email', 'user', 'out', 'user_chat', encryptionKey] ); results.push({ channel: 'email', status: 'sent' }); sent = true; @@ -363,9 +470,9 @@ router.post('/broadcast', async (req, res) => { const bot = await 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, 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), NOW())`, - [user_id, conversation.id, 'admin', content, 'telegram', 'user', 'out', encryptionKey] + `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, 'admin', content, 'telegram', 'user', 'out', 'user_chat', encryptionKey] ); results.push({ channel: 'telegram', status: 'sent' }); sent = true; @@ -435,4 +542,254 @@ router.delete('/history/:userId', async (req, res) => { } }); +// POST /api/messages/admin/send - отправка сообщения админу +router.post('/admin/send', async (req, res) => { + try { + const adminId = req.user && req.user.id; + const { recipientAdminId, content } = req.body; + + if (!adminId) { + return res.status(401).json({ error: 'Unauthorized: adminId missing' }); + } + if (!recipientAdminId || !content) { + return res.status(400).json({ error: 'recipientAdminId and content required' }); + } + + // Получаем ключ шифрования + const fs = require('fs'); + const path = require('path'); + let encryptionKey = 'default-key'; + + try { + const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key'); + if (fs.existsSync(keyPath)) { + encryptionKey = fs.readFileSync(keyPath, 'utf8').trim(); + } + } catch (keyError) { + console.error('Error reading encryption key:', keyError); + } + + // Ищем существующую приватную беседу между двумя админами через conversation_participants + let conversationResult = await db.getQuery()(` + SELECT c.id + FROM conversations c + INNER JOIN conversation_participants cp1 ON cp1.conversation_id = c.id AND cp1.user_id = $1 + INNER JOIN conversation_participants cp2 ON cp2.conversation_id = c.id AND cp2.user_id = $2 + WHERE c.conversation_type = 'admin_chat' + LIMIT 1 + `, [adminId, recipientAdminId]); + + let conversationId; + if (conversationResult.rows.length === 0) { + // Создаем новую приватную беседу между админами + const title = `Приватная беседа ${adminId} - ${recipientAdminId}`; + const newConv = await db.getQuery()( + 'INSERT INTO conversations (user_id, title_encrypted, conversation_type, created_at, updated_at) VALUES ($1, encrypt_text($2, $3), $4, NOW(), NOW()) RETURNING id', + [recipientAdminId, title, encryptionKey, 'admin_chat'] + ); + conversationId = newConv.rows[0].id; + + // Добавляем участников в беседу + await db.getQuery()( + 'INSERT INTO conversation_participants (conversation_id, user_id) VALUES ($1, $2), ($1, $3)', + [conversationId, adminId, recipientAdminId] + ); + + console.log(`[admin/send] Создана новая беседа ${conversationId} между ${adminId} и ${recipientAdminId}`); + } else { + conversationId = conversationResult.rows[0].id; + console.log(`[admin/send] Найдена существующая беседа ${conversationId} между ${adminId} и ${recipientAdminId}`); + } + + // Сохраняем сообщение с типом 'admin_chat' + const result = await db.getQuery()( + `INSERT INTO messages (conversation_id, user_id, sender_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, message_type, created_at) + VALUES ($1, $2, $3, encrypt_text($4, $9), encrypt_text($5, $9), encrypt_text($6, $9), encrypt_text($7, $9), encrypt_text($8, $9), $10, NOW()) RETURNING id`, + [conversationId, recipientAdminId, adminId, 'admin', content, 'web', 'admin', 'out', encryptionKey, 'admin_chat'] + ); + + // Отправляем WebSocket уведомления + broadcastMessagesUpdate(); + + res.json({ + success: true, + messageId: result.rows[0].id, + conversationId, + messageType: 'admin_chat' + }); + } catch (e) { + console.error('[ERROR] /admin/send:', e); + res.status(500).json({ error: e.message }); + } +}); + +// GET /api/messages/admin/conversations - получить личные чаты админа +router.get('/admin/conversations', async (req, res) => { + try { + const adminId = req.user && req.user.id; + + if (!adminId) { + return res.status(401).json({ error: 'Unauthorized: adminId missing' }); + } + + // Получаем список админов, с которыми есть переписка + const conversations = await db.query(` + SELECT DISTINCT + CASE + WHEN sender_type = 'admin' AND user_id != $1 THEN user_id + ELSE sender_id + END as admin_id, + MAX(created_at) as last_message_at + FROM messages + WHERE message_type = 'admin_chat' + AND (user_id = $1 OR sender_id = $1) + GROUP BY admin_id + ORDER BY last_message_at DESC + `, [adminId]); + + res.json({ + success: true, + conversations: conversations.rows + }); + } catch (e) { + console.error('[ERROR] /admin/conversations:', e); + res.status(500).json({ error: e.message }); + } +}); + +// GET /api/messages/admin/contacts - получить админов для приватного чата +router.get('/admin/contacts', async (req, res) => { + try { + const adminId = req.user && req.user.id; + + if (!adminId) { + return res.status(401).json({ error: 'Unauthorized: adminId missing' }); + } + + // Получаем ключ шифрования + const fs = require('fs'); + const path = require('path'); + let encryptionKey = 'default-key'; + + try { + const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key'); + if (fs.existsSync(keyPath)) { + encryptionKey = fs.readFileSync(keyPath, 'utf8').trim(); + } + } catch (keyError) { + console.error('Error reading encryption key:', keyError); + } + + // Получаем всех пользователей, с которыми есть приватные беседы через conversation_participants + const adminContacts = await db.getQuery()(` + SELECT DISTINCT + other_user.id, + COALESCE( + decrypt_text(other_user.first_name_encrypted, $2), + decrypt_text(other_user.username_encrypted, $2), + 'Пользователь ' || other_user.id + ) as name, + 'admin@system' as email, + CASE + WHEN other_user.role = 'editor' THEN 'admin' + WHEN other_user.role = 'readonly' THEN 'admin' + ELSE 'user' + END as contact_type, + MAX(m.created_at) as last_message_at, + COUNT(m.id) as message_count + FROM conversations c + INNER JOIN conversation_participants cp_current ON cp_current.conversation_id = c.id AND cp_current.user_id = $1 + INNER JOIN conversation_participants cp_other ON cp_other.conversation_id = c.id AND cp_other.user_id != $1 + INNER JOIN users other_user ON other_user.id = cp_other.user_id + LEFT JOIN messages m ON m.conversation_id = c.id AND m.message_type = 'admin_chat' + WHERE c.conversation_type = 'admin_chat' + GROUP BY + other_user.id, + other_user.first_name_encrypted, + other_user.username_encrypted, + other_user.role + ORDER BY MAX(m.created_at) DESC + `, [adminId, encryptionKey]); + + res.json({ + success: true, + contacts: adminContacts.rows.map(contact => ({ + ...contact, + created_at: contact.last_message_at, // Используем время последнего сообщения как время создания для сортировки + telegram: null, + wallet: null + })) + }); + } catch (e) { + console.error('[ERROR] /admin/contacts:', e); + res.status(500).json({ error: e.message }); + } +}); + +// GET /api/messages/admin/:adminId - получить сообщения с конкретным админом +router.get('/admin/:adminId', async (req, res) => { + try { + const currentAdminId = req.user && req.user.id; + const { adminId } = req.params; + + if (!currentAdminId) { + return res.status(401).json({ error: 'Unauthorized: adminId missing' }); + } + + // Получаем ключ шифрования + const fs = require('fs'); + const path = require('path'); + let encryptionKey = 'default-key'; + + try { + const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key'); + if (fs.existsSync(keyPath)) { + encryptionKey = fs.readFileSync(keyPath, 'utf8').trim(); + } + } catch (keyError) { + console.error('Error reading encryption key:', keyError); + } + + // Получаем сообщения из приватной беседы между админами через conversation_participants + const result = await db.getQuery()( + `SELECT m.id, m.user_id, m.sender_id, + decrypt_text(m.sender_type_encrypted, $3) as sender_type, + decrypt_text(m.content_encrypted, $3) as content, + decrypt_text(m.channel_encrypted, $3) as channel, + decrypt_text(m.role_encrypted, $3) as role, + decrypt_text(m.direction_encrypted, $3) as direction, + m.created_at, m.message_type, + -- Получаем wallet адреса отправителей (расшифровываем provider_id_encrypted) + CASE + WHEN sender_ui.provider_encrypted = encrypt_text('wallet', $3) + THEN decrypt_text(sender_ui.provider_id_encrypted, $3) + ELSE 'Админ' + END as sender_wallet, + CASE + WHEN recipient_ui.provider_encrypted = encrypt_text('wallet', $3) + THEN decrypt_text(recipient_ui.provider_id_encrypted, $3) + ELSE 'Админ' + END as recipient_wallet + FROM messages m + INNER JOIN conversations c ON c.id = m.conversation_id + INNER JOIN conversation_participants cp1 ON cp1.conversation_id = c.id AND cp1.user_id = $1 + INNER JOIN conversation_participants cp2 ON cp2.conversation_id = c.id AND cp2.user_id = $2 + LEFT JOIN user_identities sender_ui ON sender_ui.user_id = m.sender_id + LEFT JOIN user_identities recipient_ui ON recipient_ui.user_id = m.user_id + WHERE m.message_type = 'admin_chat' AND c.conversation_type = 'admin_chat' + ORDER BY m.created_at ASC`, + [currentAdminId, adminId, encryptionKey] + ); + + res.json({ + success: true, + messages: result.rows, + messageType: 'admin_chat' + }); + } catch (e) { + console.error('[ERROR] /admin/:adminId:', e); + res.status(500).json({ error: e.message }); + } +}); + module.exports = router; \ No newline at end of file diff --git a/backend/routes/users.js b/backend/routes/users.js index 8d17da0..7ae19ea 100644 --- a/backend/routes/users.js +++ b/backend/routes/users.js @@ -150,7 +150,12 @@ router.get('/', requireAuth, async (req, res, next) => { WHEN u.last_name_encrypted IS NULL OR u.last_name_encrypted = '' THEN NULL ELSE decrypt_text(u.last_name_encrypted, $${idx++}) END as last_name, - u.created_at, u.preferred_language, u.is_blocked, + u.created_at, u.preferred_language, u.is_blocked, u.role, + CASE + WHEN u.role = 'editor' THEN 'admin' + WHEN u.role = 'readonly' THEN 'admin' + ELSE 'user' + END as contact_type, (SELECT decrypt_text(provider_id_encrypted, $${idx++}) FROM user_identities WHERE user_id = u.id AND provider_encrypted = encrypt_text('email', $${idx++}) LIMIT 1) AS email, (SELECT decrypt_text(provider_id_encrypted, $${idx++}) FROM user_identities WHERE user_id = u.id AND provider_encrypted = encrypt_text('telegram', $${idx++}) LIMIT 1) AS telegram, (SELECT decrypt_text(provider_id_encrypted, $${idx++}) FROM user_identities WHERE user_id = u.id AND provider_encrypted = encrypt_text('wallet', $${idx++}) LIMIT 1) AS wallet @@ -219,7 +224,9 @@ router.get('/', requireAuth, async (req, res, next) => { wallet: u.wallet || null, created_at: u.created_at, preferred_language: u.preferred_language || [], - is_blocked: u.is_blocked || false + is_blocked: u.is_blocked || false, + contact_type: u.contact_type || 'user', + role: u.role || 'user' })); res.json({ success: true, contacts }); diff --git a/backend/services/userDeleteService.js b/backend/services/userDeleteService.js index 9c1bd64..2f1eaa9 100644 --- a/backend/services/userDeleteService.js +++ b/backend/services/userDeleteService.js @@ -10,26 +10,96 @@ * GitHub: https://github.com/HB3-ACCELERATOR */ -const encryptedDb = require('./encryptedDatabaseService'); +const db = require('../db'); async function deleteUserById(userId) { - // console.log('[DELETE] Вызван deleteUserById для userId:', userId); + console.log('[DELETE] Вызван deleteUserById для userId:', userId); try { - // console.log('[DELETE] Начинаем удаление user_identities для userId:', userId); - const resIdentities = await encryptedDb.deleteData('user_identities', { user_id: userId }); - // console.log('[DELETE] Удалено user_identities:', resIdentities.length); + // Удаляем в правильном порядке (сначала зависимые таблицы, потом основную) - // console.log('[DELETE] Начинаем удаление messages для userId:', userId); - const resMessages = await encryptedDb.deleteData('messages', { user_id: userId }); - // console.log('[DELETE] Удалено messages:', resMessages.length); + // 1. Удаляем user_identities + console.log('[DELETE] Начинаем удаление user_identities для userId:', userId); + const resIdentities = await db.getQuery()( + 'DELETE FROM user_identities WHERE user_id = $1 RETURNING id', + [userId] + ); + console.log('[DELETE] Удалено user_identities:', resIdentities.rows.length); - // console.log('[DELETE] Начинаем удаление пользователя из users:', userId); - const result = await encryptedDb.deleteData('users', { id: userId }); - // console.log('[DELETE] Результат удаления пользователя:', result.length, result); + // 2. Удаляем messages + console.log('[DELETE] Начинаем удаление messages для userId:', userId); + const resMessages = await db.getQuery()( + 'DELETE FROM messages WHERE user_id = $1 RETURNING id', + [userId] + ); + console.log('[DELETE] Удалено messages:', resMessages.rows.length); - return result.length; + // 3. Удаляем conversations + console.log('[DELETE] Начинаем удаление conversations для userId:', userId); + const resConversations = await db.getQuery()( + 'DELETE FROM conversations WHERE user_id = $1 RETURNING id', + [userId] + ); + console.log('[DELETE] Удалено conversations:', resConversations.rows.length); + + // 4. Удаляем conversation_participants + console.log('[DELETE] Начинаем удаление conversation_participants для userId:', userId); + const resParticipants = await db.getQuery()( + 'DELETE FROM conversation_participants WHERE user_id = $1 RETURNING id', + [userId] + ); + console.log('[DELETE] Удалено conversation_participants:', resParticipants.rows.length); + + // 5. Удаляем user_preferences + console.log('[DELETE] Начинаем удаление user_preferences для userId:', userId); + const resPreferences = await db.getQuery()( + 'DELETE FROM user_preferences WHERE user_id = $1 RETURNING id', + [userId] + ); + console.log('[DELETE] Удалено user_preferences:', resPreferences.rows.length); + + // 6. Удаляем verification_codes + console.log('[DELETE] Начинаем удаление verification_codes для userId:', userId); + const resCodes = await db.getQuery()( + 'DELETE FROM verification_codes WHERE user_id = $1 RETURNING id', + [userId] + ); + console.log('[DELETE] Удалено verification_codes:', resCodes.rows.length); + + // 7. Удаляем guest_user_mapping + console.log('[DELETE] Начинаем удаление guest_user_mapping для userId:', userId); + const resGuestMapping = await db.getQuery()( + 'DELETE FROM guest_user_mapping WHERE user_id = $1 RETURNING id', + [userId] + ); + console.log('[DELETE] Удалено guest_user_mapping:', resGuestMapping.rows.length); + + // 8. Удаляем user_tag_links + console.log('[DELETE] Начинаем удаление user_tag_links для userId:', userId); + const resTagLinks = await db.getQuery()( + 'DELETE FROM user_tag_links WHERE user_id = $1 RETURNING id', + [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); + + // 10. Удаляем самого пользователя + console.log('[DELETE] Начинаем удаление пользователя из users:', userId); + const result = await db.getQuery()( + 'DELETE FROM users WHERE id = $1 RETURNING id', + [userId] + ); + console.log('[DELETE] Результат удаления пользователя:', result.rows.length, result.rows); + + return result.rows.length; } catch (e) { - // console.error('[DELETE] Ошибка при удалении пользователя:', e); + console.error('[DELETE] Ошибка при удалении пользователя:', e); throw e; } } diff --git a/frontend/src/components/ContactTable.vue b/frontend/src/components/ContactTable.vue index fb69d18..adc8623 100644 --- a/frontend/src/components/ContactTable.vue +++ b/frontend/src/components/ContactTable.vue @@ -13,6 +13,9 @@ + + diff --git a/frontend/src/views/contacts/ContactDetailsView.vue b/frontend/src/views/contacts/ContactDetailsView.vue index c8cec77..64ac7ba 100644 --- a/frontend/src/views/contacts/ContactDetailsView.vue +++ b/frontend/src/views/contacts/ContactDetailsView.vue @@ -381,23 +381,22 @@ async function loadMessages() { if (!contact.value || !contact.value.id) return; isLoadingMessages.value = true; try { - // Получаем conversationId для контакта - const conv = await messagesService.getConversationByUserId(contact.value.id); - conversationId.value = conv?.id || null; - if (conversationId.value) { - messages.value = await messagesService.getMessagesByConversationId(conversationId.value); + // Загружаем ВСЕ публичные сообщения этого пользователя (как на главной странице) + messages.value = await messagesService.getMessagesByUserId(contact.value.id); if (messages.value.length > 0) { lastMessageDate.value = messages.value[messages.value.length - 1].created_at; } else { - lastMessageDate.value = null; - } - } else { - messages.value = []; lastMessageDate.value = null; } + + // Также получаем conversationId для отправки новых сообщений + const conv = await messagesService.getConversationByUserId(contact.value.id); + conversationId.value = conv?.id || null; } catch (e) { + console.error('[ContactDetailsView] Ошибка загрузки сообщений:', e); messages.value = []; lastMessageDate.value = null; + conversationId.value = null; } finally { isLoadingMessages.value = false; }