/** * Copyright (c) 2024-2025 Тарабанов Александр Викторович * All rights reserved. * * This software is proprietary and confidential. * Unauthorized copying, modification, or distribution is prohibited. * * For licensing inquiries: info@hb3-accelerator.com * Website: https://hb3-accelerator.com * GitHub: https://github.com/HB3-ACCELERATOR */ const express = require('express'); const router = express.Router(); const db = require('../db'); const logger = require('../utils/logger'); const { requireAuth } = require('../middleware/auth'); const { requirePermission } = require('../middleware/permissions'); const { PERMISSIONS, ROLES } = require('../shared/permissions'); const { deleteUserById } = require('../services/userDeleteService'); const { broadcastContactsUpdate } = require('../wsHub'); // const userService = require('../services/userService'); // console.log('[users.js] ROUTER LOADED'); router.use((req, res, next) => { // console.log('[users.js] ROUTER REQUEST:', req.method, req.originalUrl); next(); }); // Получение списка пользователей // router.get('/', (req, res) => { // res.json({ message: 'Users API endpoint' }); // }); // Получить профиль текущего пользователя /* router.get('/profile', requireAuth, async (req, res) => { try { const userId = req.session.userId; const user = await userService.getUserProfile(userId); if (!user) { return res.status(404).json({ success: false, message: 'User not found' }); } res.json({ success: true, user }); } catch (error) { // console.error('Error getting user profile:', error); res.status(500).json({ success: false, message: 'Internal server error' }); } }); */ // Обновить профиль текущего пользователя /* router.put('/profile', requireAuth, async (req, res) => { try { const userId = req.session.userId; const profileData = req.body; const updatedUser = await userService.updateUserProfile(userId, profileData); res.json({ success: true, user: updatedUser, message: 'Profile updated successfully' }); } catch (error) { // console.error('Error updating user profile:', error); // Можно добавить более специфичную обработку ошибок, например, если данные невалидны res.status(500).json({ success: false, message: 'Internal server error' }); } }); */ // Получение списка пользователей с фильтрацией (CRM/Контакты) router.get('/', requireAuth, async (req, res, next) => { try { const { tagIds = '', dateFrom = '', dateTo = '', contactType = 'all', search = '', newMessages = '', blocked = 'all' } = req.query; const adminId = req.user && req.user.id; const userRole = req.user.role; // Получаем ключ шифрования const fs = require('fs'); const path = require('path'); // Получаем ключ шифрования через унифицированную утилиту const encryptionUtils = require('../utils/encryptionUtils'); const encryptionKey = encryptionUtils.getEncryptionKey(); // --- Формируем условия --- const where = []; const params = []; let idx = 1; // Фильтрация для USER - видит только editor админов и себя if (userRole === 'user') { where.push(`(u.role = '${ROLES.EDITOR}' OR u.id = $${idx++})`); params.push(req.user.id); } // Фильтр по дате if (dateFrom) { where.push(`DATE(u.created_at) >= $${idx++}`); params.push(dateFrom); } if (dateTo) { where.push(`DATE(u.created_at) <= $${idx++}`); params.push(dateTo); } // Фильтр по типу контакта if (contactType !== 'all') { where.push(`EXISTS ( SELECT 1 FROM user_identities ui WHERE ui.user_id = u.id AND ui.provider_encrypted = encrypt_text($${idx++}, $${idx++}) )`); params.push(contactType); params.push(encryptionKey); } // Фильтр по поиску if (search) { where.push(`( LOWER(decrypt_text(u.first_name_encrypted, $${idx++})) LIKE $${idx++} OR LOWER(decrypt_text(u.last_name_encrypted, $${idx++})) LIKE $${idx++} OR EXISTS (SELECT 1 FROM user_identities ui WHERE ui.user_id = u.id AND LOWER(decrypt_text(ui.provider_id_encrypted, $${idx++})) LIKE $${idx++}) )`); params.push(encryptionKey); // Для first_name_encrypted params.push(`%${search.toLowerCase()}%`); params.push(encryptionKey); // Для last_name_encrypted params.push(`%${search.toLowerCase()}%`); params.push(encryptionKey); // Для provider_id_encrypted params.push(`%${search.toLowerCase()}%`); } // Фильтр по блокировке if (blocked === 'blocked') { where.push(`u.is_blocked = true`); } else if (blocked === 'unblocked') { where.push(`u.is_blocked = false`); } // --- Основной SQL --- let sql = ` SELECT u.id, CASE WHEN u.first_name_encrypted IS NULL OR u.first_name_encrypted = '' THEN NULL ELSE decrypt_text(u.first_name_encrypted, $${idx++}) END as first_name, CASE 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.role, CASE WHEN u.role = 'editor' THEN 'editor' WHEN u.role = 'readonly' THEN 'editor' -- readonly админы тоже editor 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 FROM users u `; params.push(encryptionKey, encryptionKey, encryptionKey, encryptionKey, encryptionKey, encryptionKey, encryptionKey, encryptionKey); // Фильтрация по тегам if (tagIds) { const tagIdArr = tagIds.split(',').map(Number).filter(Boolean); if (tagIdArr.length > 0) { sql += ` JOIN user_tag_links utl ON utl.user_id = u.id WHERE utl.tag_id = ANY($${idx++}) GROUP BY u.id HAVING COUNT(DISTINCT utl.tag_id) = $${idx++} `; params.push(tagIdArr); params.push(tagIdArr.length); } } else if (where.length > 0) { sql += ` WHERE ${where.join(' AND ')} `; } if (!tagIds) { sql += ' ORDER BY u.id '; } // --- Выполняем запрос --- const usersResult = await db.getQuery()(sql, params); let users = usersResult.rows; // --- Фильтрация по новым сообщениям --- if (newMessages === 'yes' && adminId) { // Получаем время последнего прочтения для каждого пользователя const readRes = await db.getQuery()( 'SELECT user_id, last_read_at FROM admin_read_messages WHERE admin_id = $1', [adminId] ); const readMap = {}; for (const row of readRes.rows) { readMap[row.user_id] = row.last_read_at; } // Получаем последнее сообщение для каждого пользователя const msgRes = await db.getQuery()( `SELECT user_id, MAX(created_at) as last_msg_at FROM messages GROUP BY user_id` ); const msgMap = {}; for (const row of msgRes.rows) { msgMap[row.user_id] = row.last_msg_at; } // Оставляем только тех, у кого есть новые сообщения users = users.filter(u => { const lastRead = readMap[u.id]; const lastMsg = msgMap[u.id]; return lastMsg && (!lastRead || new Date(lastMsg) > new Date(lastRead)); }); } // --- Формируем ответ для зарегистрированных пользователей --- const contacts = users.map(u => ({ id: u.id, name: [u.first_name, u.last_name].filter(Boolean).join(' ').trim() || null, email: u.email || null, telegram: u.telegram || null, wallet: u.wallet || null, created_at: u.created_at, preferred_language: u.preferred_language || [], is_blocked: u.is_blocked || false, contact_type: u.contact_type || 'user', role: u.role || 'user' })); // --- Добавляем гостевые контакты --- const guestContactsResult = await db.getQuery()( `WITH decrypted_guests AS ( SELECT id, decrypt_text(identifier_encrypted, $1) as guest_identifier, channel, created_at, user_id FROM unified_guest_messages WHERE user_id IS NULL ), guest_groups AS ( SELECT MIN(id) as guest_id, guest_identifier, channel, MIN(created_at) as created_at, MAX(created_at) as last_message_at, COUNT(*) as message_count FROM decrypted_guests GROUP BY guest_identifier, channel ) SELECT ROW_NUMBER() OVER (ORDER BY guest_id ASC) as guest_number, guest_id, guest_identifier, channel, created_at, last_message_at, message_count FROM guest_groups ORDER BY guest_id ASC`, [encryptionKey] ); const guestContacts = guestContactsResult.rows.map((g) => { const channelMap = { 'web': '🌐', 'telegram': '📱', 'email': '✉️' }; const icon = channelMap[g.channel] || '👤'; const rawId = g.guest_identifier.replace(`${g.channel}:`, ''); // Формируем имя в зависимости от канала let displayName; if (g.channel === 'email') { displayName = `${icon} ${rawId}`; } else if (g.channel === 'telegram') { displayName = `${icon} Telegram (${rawId})`; } else { displayName = `${icon} Гость ${g.guest_number}`; } return { id: `guest_${g.guest_id}`, // Используем внутренний ID для поиска guest_number: parseInt(g.guest_number), // Порядковый номер для отображения guest_identifier: g.guest_identifier, // Сохраняем для запросов name: displayName, email: g.channel === 'email' ? rawId : null, telegram: g.channel === 'telegram' ? rawId : null, wallet: null, created_at: g.created_at, preferred_language: [], is_blocked: false, contact_type: 'guest', role: 'guest', guest_info: { channel: g.channel, message_count: parseInt(g.message_count), last_message_at: g.last_message_at } }; }); // Объединяем списки const allContacts = [...contacts, ...guestContacts]; res.json({ success: true, contacts: allContacts }); } catch (error) { logger.error('Error fetching contacts:', error); next(error); } }); // GET /api/users - Получить список всех пользователей (пример, может требовать прав администратора) // В текущей реализации этот маршрут не используется и закомментирован /* router.get('/', async (req, res) => { try { // const users = await userService.getAllUsers(); // Удаляем await userService.getAllUsers(); // Просто вызываем, если нужно действие, но результат не используется // res.json({ success: true, users }); res.json({ success: true, message: "Users retrieved" }); // Пример ответа без данных } catch (error) { console.error('Error getting all users:', error); res.status(500).json({ success: false, message: 'Internal server error' }); } }); */ // Получить просмотренные контакты router.get('/read-contacts-status', async (req, res) => { try { const adminId = req.user && req.user.id; if (!adminId) { return res.status(401).json({ error: 'Unauthorized: adminId missing' }); } const result = await db.query( 'SELECT contact_id FROM admin_read_contacts WHERE admin_id = $1', [adminId] ); res.json(result.rows.map(r => r.contact_id)); } catch (e) { console.error('[ERROR] /read-contacts-status:', e); res.status(500).json({ error: e.message }); } }); // Пометить контакт как просмотренный router.post('/mark-contact-read', async (req, res) => { try { console.log('[DEBUG] /mark-contact-read: req.body:', req.body); console.log('[DEBUG] /mark-contact-read: req.user:', req.user); console.log('[DEBUG] /mark-contact-read: req.session:', req.session); console.log('[DEBUG] /mark-contact-read: req.user.userAccessLevel:', req.user?.userAccessLevel); const { contactId } = req.body; if (!contactId) { console.log('[ERROR] /mark-contact-read: contactId missing'); return res.status(400).json({ error: 'contactId required' }); } // НОВАЯ СИСТЕМА РОЛЕЙ: используем shared/permissions.js const { hasPermission, ROLES } = require('/app/shared/permissions'); // Определяем роль пользователя через новую систему let userRole = ROLES.GUEST; // По умолчанию гость if (req.user?.userAccessLevel) { // Используем новую систему ролей if (req.user.userAccessLevel.level === ROLES.READONLY || req.user.userAccessLevel.level === 'readonly') { userRole = ROLES.READONLY; } 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 для старой системы userRole = ROLES.USER; } console.log('[DEBUG] /mark-contact-read: userRole:', userRole); // Проверяем права через новую систему if (!hasPermission(userRole, PERMISSIONS.VIEW_CONTACTS)) { console.log('[ERROR] /mark-contact-read: Insufficient permissions for role:', userRole); return res.status(403).json({ error: 'Insufficient permissions' }); } // ИСПРАВЛЕННАЯ ЛОГИКА: все админы (EDITOR и READONLY) могут влиять на цвет контактов let adminId; if (req.user?.id && (userRole === ROLES.EDITOR || userRole === ROLES.READONLY)) { // Админы (редактор и чтение) могут записывать в admin_read_contacts adminId = req.user.id; console.log('[DEBUG] /mark-contact-read: Using admin ID:', adminId, 'Role:', userRole); // Админ может помечать любого контакта как прочитанного, включая самого себя } else { // Для всех остальных ролей (GUEST, USER) - НЕ записываем в БД console.log('[DEBUG] /mark-contact-read: User role is not editor/readonly, not recording in admin_read_contacts. Role:', userRole); return res.json({ success: true }); // Просто возвращаем успех без записи в БД } const contactIdStr = String(contactId); console.log('[DEBUG] /mark-contact-read: Final adminId:', adminId, 'contactId:', contactIdStr); await db.query( 'INSERT INTO admin_read_contacts (admin_id, contact_id, read_at) VALUES ($1, $2, NOW()) ON CONFLICT (admin_id, contact_id) DO UPDATE SET read_at = NOW()', [adminId, contactIdStr] ); console.log('[SUCCESS] /mark-contact-read: Contact marked as read'); res.json({ success: true }); } catch (e) { console.error('[ERROR] /mark-contact-read:', e); res.status(500).json({ error: e.message }); } }); // Заблокировать пользователя // Блокировка пользователя router.patch('/:id/block', requireAuth, requirePermission(PERMISSIONS.BLOCK_USERS), async (req, res) => { try { const userId = req.params.id; await db.query('UPDATE users SET is_blocked = true, blocked_at = NOW() WHERE id = $1', [userId]); res.json({ success: true, message: 'Пользователь заблокирован' }); } catch (e) { logger.error('Ошибка блокировки пользователя:', e); res.status(500).json({ success: false, error: e.message }); } }); // Разблокировать пользователя // Разблокировка пользователя router.patch('/:id/unblock', requireAuth, requirePermission(PERMISSIONS.BLOCK_USERS), async (req, res) => { try { const userId = req.params.id; await db.query('UPDATE users SET is_blocked = false, blocked_at = NULL WHERE id = $1', [userId]); res.json({ success: true, message: 'Пользователь разблокирован' }); } catch (e) { logger.error('Ошибка разблокировки пользователя:', e); res.status(500).json({ success: false, error: e.message }); } }); // Обновить пользователя (в том числе is_blocked) // Обновление данных пользователя router.patch('/:id', requireAuth, requirePermission(PERMISSIONS.EDIT_CONTACTS), async (req, res) => { try { const userId = req.params.id; const { first_name, last_name, name, preferred_language, language, is_blocked } = req.body; const fields = []; const values = []; let idx = 1; // Получаем ключ шифрования один раз const encryptionUtils = require('../utils/encryptionUtils'); const encryptionKey = encryptionUtils.getEncryptionKey(); // Обработка поля name - разбиваем на first_name и last_name if (name !== undefined) { const nameParts = name.trim().split(' '); const firstName = nameParts[0] || ''; const lastName = nameParts.slice(1).join(' ') || ''; fields.push(`first_name_encrypted = encrypt_text($${idx++}, $${idx++})`); values.push(firstName); values.push(encryptionKey); fields.push(`last_name_encrypted = encrypt_text($${idx++}, $${idx++})`); values.push(lastName); values.push(encryptionKey); } else { if (first_name !== undefined) { fields.push(`first_name_encrypted = encrypt_text($${idx++}, $${idx++})`); values.push(first_name); values.push(encryptionKey); } if (last_name !== undefined) { fields.push(`last_name_encrypted = encrypt_text($${idx++}, $${idx++})`); values.push(last_name); values.push(encryptionKey); } } // Обработка поля language (alias для preferred_language) const languageToUpdate = language !== undefined ? language : preferred_language; if (languageToUpdate !== undefined) { fields.push(`preferred_language = $${idx++}`); values.push(JSON.stringify(languageToUpdate)); } if (is_blocked !== undefined) { fields.push(`is_blocked = $${idx++}`); values.push(is_blocked); if (is_blocked) { fields.push(`blocked_at = NOW()`); } else { fields.push(`blocked_at = NULL`); } } if (!fields.length) return res.status(400).json({ success: false, error: 'Нет данных для обновления' }); const sql = `UPDATE users SET ${fields.join(', ')} WHERE id = $${idx}`; values.push(userId); await db.query(sql, values); broadcastContactsUpdate(); res.json({ success: true, message: 'Пользователь обновлен' }); } catch (e) { logger.error('Ошибка обновления пользователя:', e); res.status(500).json({ success: false, error: e.message }); } }); // DELETE /api/users/:id — удалить контакт и все связанные данные // Удаление пользователя router.delete('/:id', requireAuth, requirePermission(PERMISSIONS.DELETE_USER_DATA), async (req, res) => { const userIdParam = req.params.id; try { // Обработка гостевых контактов (guest_123) if (userIdParam.startsWith('guest_')) { const guestId = parseInt(userIdParam.replace('guest_', '')); if (isNaN(guestId)) { return res.status(400).json({ error: 'Invalid guest ID format' }); } // Получаем ключ шифрования const encryptionUtils = require('../utils/encryptionUtils'); const encryptionKey = encryptionUtils.getEncryptionKey(); // Находим guest_identifier по guestId const identifierResult = await db.getQuery()( `WITH decrypted_guest AS ( SELECT id, decrypt_text(identifier_encrypted, $2) as guest_identifier, channel FROM unified_guest_messages WHERE user_id IS NULL ) SELECT guest_identifier, channel FROM decrypted_guest GROUP BY guest_identifier, channel HAVING MIN(id) = $1 LIMIT 1`, [guestId, encryptionKey] ); if (identifierResult.rows.length === 0) { return res.status(404).json({ error: 'Guest contact not found' }); } const guestIdentifier = identifierResult.rows[0].guest_identifier; const guestChannel = identifierResult.rows[0].channel; // Удаляем все сообщения этого гостя const deleteResult = await db.getQuery()( `DELETE FROM unified_guest_messages WHERE decrypt_text(identifier_encrypted, $2) = $1 AND channel = $3`, [guestIdentifier, encryptionKey, guestChannel] ); broadcastContactsUpdate(); return res.json({ success: true, deleted: deleteResult.rowCount }); } // Обработка обычных пользователей const userId = Number(userIdParam); if (isNaN(userId)) { return res.status(400).json({ error: 'Invalid user ID' }); } const deletedCount = await deleteUserById(userId); if (deletedCount === 0) { return res.status(404).json({ success: false, deleted: 0, error: 'User not found' }); } broadcastContactsUpdate(); res.json({ success: true, deleted: deletedCount }); } catch (e) { console.error('[DELETE] Ошибка при удалении:', e); res.status(500).json({ error: 'DB error', details: e.message }); } }); // --- Получение ролей из базы данных (созданных через миграции) --- router.get('/roles', requireAuth, async (req, res, next) => { try { const encryptionUtils = require('../utils/encryptionUtils'); const encryptionKey = encryptionUtils.getEncryptionKey(); const sql = ` SELECT id, decrypt_text(name_encrypted, $1) as name, created_at FROM roles ORDER BY id `; const result = await db.getQuery()(sql, [encryptionKey]); res.json({ success: true, roles: result.rows }); } catch (error) { console.error('[users/roles] Ошибка при получении ролей:', error); res.status(500).json({ success: false, error: 'Ошибка при получении ролей', details: error.message }); } }); // Получить пользователя по id // Получение деталей конкретного контакта router.get('/:id', requireAuth, requirePermission(PERMISSIONS.VIEW_CONTACTS), async (req, res, next) => { const userId = req.params.id; // Получаем ключ шифрования const fs = require('fs'); const path = require('path'); // Получаем ключ шифрования через унифицированную утилиту const encryptionUtils = require('../utils/encryptionUtils'); const encryptionKey = encryptionUtils.getEncryptionKey(); try { const query = db.getQuery(); // Проверяем, это гостевой идентификатор (формат: guest_123) if (userId.startsWith('guest_')) { const guestId = parseInt(userId.replace('guest_', '')); if (isNaN(guestId)) { return res.status(400).json({ error: 'Invalid guest ID format' }); } const guestResult = await query( `WITH decrypted_guest AS ( SELECT id, decrypt_text(identifier_encrypted, $2) as guest_identifier, channel, created_at, user_id FROM unified_guest_messages WHERE user_id IS NULL ) SELECT MIN(id) as guest_id, guest_identifier, channel, MIN(created_at) as created_at, MAX(created_at) as last_message_at, COUNT(*) as message_count FROM decrypted_guest GROUP BY guest_identifier, channel HAVING MIN(id) = $1`, [guestId, encryptionKey] ); if (guestResult.rows.length === 0) { return res.status(404).json({ error: 'Guest contact not found' }); } const guest = guestResult.rows[0]; const rawId = guest.guest_identifier.replace(`${guest.channel}:`, ''); const channelMap = { 'web': '🌐', 'telegram': '📱', 'email': '✉️' }; const icon = channelMap[guest.channel] || '👤'; // Формируем имя в зависимости от канала let displayName; if (guest.channel === 'email') { displayName = `${icon} ${rawId}`; } else if (guest.channel === 'telegram') { displayName = `${icon} Telegram (${rawId})`; } else { displayName = `${icon} Гость ${guestId}`; } return res.json({ id: `guest_${guestId}`, guest_identifier: guest.guest_identifier, name: displayName, email: guest.channel === 'email' ? rawId : null, telegram: guest.channel === 'telegram' ? rawId : null, wallet: null, created_at: guest.created_at, preferred_language: [], is_blocked: false, contact_type: 'guest', role: 'guest', guest_info: { channel: guest.channel, message_count: parseInt(guest.message_count), last_message_at: guest.last_message_at, raw_identifier: rawId } }); } // Получаем пользователя (зарегистрированный) const userResult = await query('SELECT id, created_at, preferred_language, is_blocked FROM users WHERE id = $1', [userId]); if (userResult.rows.length === 0) { return res.status(404).json({ error: 'User not found' }); } const user = userResult.rows[0]; // Получаем идентификаторы const identitiesResult = await query('SELECT CASE WHEN provider_encrypted IS NULL OR provider_encrypted = \'\' THEN NULL ELSE decrypt_text(provider_encrypted, $2) END as provider, CASE WHEN provider_id_encrypted IS NULL OR provider_id_encrypted = \'\' THEN NULL ELSE decrypt_text(provider_id_encrypted, $2) END as provider_id FROM user_identities WHERE user_id = $1', [userId, encryptionKey]); const identityMap = {}; for (const id of identitiesResult.rows) { identityMap[id.provider] = id.provider_id; } // Получаем имя пользователя из зашифрованных полей const nameResult = await query('SELECT CASE WHEN first_name_encrypted IS NULL OR first_name_encrypted = \'\' THEN NULL ELSE decrypt_text(first_name_encrypted, $2) END as first_name, CASE WHEN last_name_encrypted IS NULL OR last_name_encrypted = \'\' THEN NULL ELSE decrypt_text(last_name_encrypted, $2) END as last_name FROM users WHERE id = $1', [userId, encryptionKey]); let fullName = null; if (nameResult.rows.length > 0) { const firstName = nameResult.rows[0].first_name || ''; const lastName = nameResult.rows[0].last_name || ''; fullName = [firstName, lastName].filter(Boolean).join(' ').trim() || null; } res.json({ id: user.id, name: fullName, email: identityMap.email || null, telegram: identityMap.telegram || null, wallet: identityMap.wallet || null, created_at: user.created_at, preferred_language: user.preferred_language || [], is_blocked: user.is_blocked || false }); } catch (e) { res.status(500).json({ error: e.message }); } }); // POST /api/users router.post('/', async (req, res) => { const { first_name, last_name, preferred_language } = req.body; // Получаем ключ шифрования const encryptionUtils = require('../utils/encryptionUtils'); const encryptionKey = encryptionUtils.getEncryptionKey(); // Используем централизованную систему ролей try { const result = await db.getQuery()( `INSERT INTO users (first_name_encrypted, last_name_encrypted, preferred_language, role, created_at) VALUES (encrypt_text($1, $5), encrypt_text($2, $5), $3, $4, NOW()) RETURNING *`, [first_name, last_name, JSON.stringify(preferred_language || []), ROLES.USER, encryptionKey] ); broadcastContactsUpdate(); res.json({ success: true, user: result.rows[0] }); } catch (e) { res.status(500).json({ error: 'DB error', details: e.message }); } }); // Массовый импорт контактов router.post('/import', requireAuth, async (req, res) => { // Получаем ключ шифрования const fs = require('fs'); const path = require('path'); // Получаем ключ шифрования через унифицированную утилиту const encryptionUtils = require('../utils/encryptionUtils'); const encryptionKey = encryptionUtils.getEncryptionKey(); try { const contacts = req.body; if (!Array.isArray(contacts)) { return res.status(400).json({ success: false, error: 'Ожидается массив контактов' }); } const dbq = db.getQuery(); let added = 0, updated = 0, errors = []; for (const [i, c] of contacts.entries()) { try { // Имя let first_name = null, last_name = null; if (c.name) { const parts = c.name.trim().split(' '); first_name = parts[0] || null; last_name = parts.slice(1).join(' ') || null; } // Проверка на существование по email/telegram/wallet let userId = null; let foundUser = null; if (c.email) { const r = await dbq('SELECT user_id FROM user_identities WHERE provider_encrypted = encrypt_text($1, $3) AND provider_id_encrypted = encrypt_text($2, $3)', ['email', c.email.toLowerCase(), encryptionKey]); if (r.rows.length) foundUser = r.rows[0].user_id; } if (!foundUser && c.telegram) { const r = await dbq('SELECT user_id FROM user_identities WHERE provider_encrypted = encrypt_text($1, $3) AND provider_id_encrypted = encrypt_text($2, $3)', ['telegram', c.telegram, encryptionKey]); if (r.rows.length) foundUser = r.rows[0].user_id; } if (!foundUser && c.wallet) { const r = await dbq('SELECT user_id FROM user_identities WHERE provider_encrypted = encrypt_text($1, $3) AND provider_id_encrypted = encrypt_text($2, $3)', ['wallet', c.wallet, encryptionKey]); if (r.rows.length) foundUser = r.rows[0].user_id; } if (foundUser) { userId = foundUser; updated++; // Обновляем имя, если нужно if (first_name || last_name) { await dbq('UPDATE users SET first_name_encrypted = COALESCE(encrypt_text($1, $4), first_name_encrypted), last_name_encrypted = COALESCE(encrypt_text($2, $4), last_name_encrypted) WHERE id = $3', [first_name, last_name, userId, encryptionKey]); } } else { // Создаём нового пользователя с централизованной ролью 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++; } // Добавляем идентификаторы (email, telegram, wallet) const identities = [ c.email ? { provider: 'email', provider_id: c.email.toLowerCase() } : null, c.telegram ? { provider: 'telegram', provider_id: c.telegram } : null, c.wallet ? { provider: 'wallet', provider_id: c.wallet } : null ].filter(Boolean); for (const idn of identities) { // Проверяем, есть ли уже такой идентификатор у пользователя const exists = await dbq('SELECT 1 FROM user_identities WHERE user_id = $1 AND provider_encrypted = encrypt_text($2, $4) AND provider_id_encrypted = encrypt_text($3, $4)', [userId, idn.provider, idn.provider_id, encryptionKey]); if (!exists.rows.length) { await dbq('INSERT INTO user_identities (user_id, provider_encrypted, provider_id_encrypted) VALUES ($1, encrypt_text($2, $4), encrypt_text($3, $4)) ON CONFLICT DO NOTHING', [userId, idn.provider, idn.provider_id, encryptionKey]); } } } catch (e) { errors.push({ row: i + 1, error: e.message }); } } broadcastContactsUpdate(); res.json({ success: true, added, updated, errors }); } catch (e) { res.status(500).json({ success: false, error: e.message }); } }); // --- Работа с тегами перенесена в /api/tags --- // Используйте следующие endpoints: // PATCH /api/tags/user/:id — установить теги пользователю // GET /api/tags/user/:id — получить теги пользователя // DELETE /api/tags/user/:id/tag/:tagId — удалить тег у пользователя // POST /api/tags/user/:id/multirelations — массовое обновление тегов module.exports = router;