Files
DLE/backend/routes/users.js
2025-10-30 22:41:04 +03:00

863 lines
35 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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/VC-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;