@@ -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
cons t 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 ]
) ;
le t 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 ;