feat: новая функция

This commit is contained in:
2025-10-16 18:44:30 +03:00
parent e0300480e1
commit 927d174f66
33 changed files with 1494 additions and 700 deletions

View File

@@ -38,8 +38,10 @@ router.post('/task', requireAuth, async (req, res) => {
try {
const { message, language, history, systemPrompt, rules, type = 'chat' } = req.body;
const userId = req.session.userId;
const userAccessLevel = req.session.userAccessLevel || { level: 'user', tokenCount: 0, hasAccess: false };
const userRole = userAccessLevel.hasAccess ? 'admin' : 'user';
const userAccessLevel = req.session.userAccessLevel || { level: ROLES.USER, tokenCount: 0, hasAccess: false };
const { ROLES } = require('/app/shared/permissions');
// Используем роль из userAccessLevel, которая уже правильно определена с учетом порогов
const userRole = userAccessLevel.level;
if (!message) {
return res.status(400).json({

View File

@@ -202,12 +202,12 @@ router.post('/verify', async (req, res) => {
return res.status(401).json({ success: false, error: 'Invalid signature' });
}
// СРАЗУ проверяем наличие админских токенов
const adminStatus = await authService.checkAdminTokens(normalizedAddress);
logger.info(`[verify] Admin status for ${normalizedAddress}: ${adminStatus}`);
// СРАЗУ проверяем уровень доступа пользователя
logger.info(`[verify] Checking access level for address: ${normalizedAddress}`);
let userAccessLevel = await authService.getUserAccessLevel(normalizedAddress);
logger.info(`[verify] Access level determined: ${userAccessLevel.level} (${userAccessLevel.tokenCount} tokens)`);
let userId;
let userAccessLevel = adminStatus ? { level: 'editor', tokenCount: 0, hasAccess: true } : { level: 'user', tokenCount: 0, hasAccess: false };
// Проверяем, авторизован ли пользователь уже
if (req.session.authenticated && req.session.userId) {
@@ -312,24 +312,27 @@ router.post('/telegram/verify', async (req, res) => {
logger.info(`[telegram/verify] Found linked wallet ${linkedWalletAddress} for user ${verificationResult.userId}`);
// Проверяем баланс токенов для определения роли
finalIsAdmin = await authService.checkAdminTokens(linkedWalletAddress);
logger.info(`[telegram/verify] Admin status based on token balance for ${linkedWalletAddress}: ${finalIsAdmin}`);
const userAccessLevel = await authService.getUserAccessLevel(linkedWalletAddress);
// Используем роль из userAccessLevel, которая уже правильно определена с учетом порогов
const newRole = userAccessLevel.level;
logger.info(`[telegram/verify] Role determined for ${linkedWalletAddress}: ${newRole} (tokens: ${userAccessLevel.tokenCount})`);
// Обновляем роль в БД, если она отличается от той, что была получена из verifyTelegramAuth
const currentRoleInDb = verificationResult.role === 'admin';
if (finalIsAdmin !== currentRoleInDb) {
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', [finalIsAdmin ? 'admin' : 'user', verificationResult.userId]);
logger.info(`[telegram/verify] User role updated in DB for user ${verificationResult.userId} to ${finalIsAdmin ? 'admin' : 'user'}`);
// Обновляем роль в БД, если она отличается от текущей
if (verificationResult.role !== newRole) {
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', [newRole, verificationResult.userId]);
logger.info(`[telegram/verify] User role updated in DB for user ${verificationResult.userId} to ${newRole}`);
}
finalIsAdmin = (newRole === ROLES.EDITOR || newRole === ROLES.READONLY);
} else {
logger.info(`[telegram/verify] No linked wallet found for user ${verificationResult.userId}. Role remains '${verificationResult.role}'`);
// Если кошелек не найден, используем роль из verificationResult (скорее всего 'user')
finalIsAdmin = verificationResult.role === 'admin';
finalIsAdmin = (verificationResult.role === ROLES.EDITOR || verificationResult.role === ROLES.READONLY);
}
} catch (error) {
logger.error(`[telegram/verify] Error finding linked wallet or checking tokens for user ${verificationResult.userId}:`, error);
// В случае ошибки, используем роль из verificationResult
finalIsAdmin = verificationResult.role === 'admin';
finalIsAdmin = (verificationResult.role === 'editor' || verificationResult.role === 'readonly');
}
// ---> КОНЕЦ ШАГОВ 4 И 5 <---
@@ -348,7 +351,7 @@ router.post('/telegram/verify', async (req, res) => {
req.session.telegramId = telegramId;
req.session.authType = 'telegram';
req.session.authenticated = true;
req.session.userAccessLevel = finalIsAdmin ? { level: 'editor', tokenCount: 0, hasAccess: true } : { level: 'user', tokenCount: 0, hasAccess: false }; // <-- УСТАНАВЛИВАЕМ РОЛЬ ПОСЛЕ ПРОВЕРКИ БАЛАНСА
req.session.userAccessLevel = finalIsAdmin ? { level: ROLES.EDITOR, tokenCount: 0, hasAccess: true } : { level: ROLES.USER, tokenCount: 0, hasAccess: false }; // <-- УСТАНАВЛИВАЕМ РОЛЬ ПОСЛЕ ПРОВЕРКИ БАЛАНСА
// ---> ДОБАВЛЯЕМ АДРЕС КОШЕЛЬКА В СЕССИЮ (ЕСЛИ НАЙДЕН) <---
if (linkedWalletAddress) {
@@ -370,7 +373,7 @@ router.post('/telegram/verify', async (req, res) => {
}
// Получаем уровень доступа для пользователя
let userAccessLevel = { level: 'user', tokenCount: 0, hasAccess: false };
let userAccessLevel = { level: ROLES.USER, tokenCount: 0, hasAccess: false };
if (linkedWalletAddress) {
userAccessLevel = await authService.getUserAccessLevel(linkedWalletAddress);
}
@@ -492,23 +495,26 @@ router.post('/email/verify-code', async (req, res) => {
let finalIsAdmin = false; // Роль по умолчанию
if (linkedWalletAddress) {
try {
finalIsAdmin = await authService.checkAdminTokens(linkedWalletAddress);
logger.info(`[email/verify-code] Admin status based on token balance for ${linkedWalletAddress}: ${finalIsAdmin}`);
const userAccessLevel = await authService.getUserAccessLevel(linkedWalletAddress);
// Используем роль из userAccessLevel, которая уже правильно определена с учетом порогов
const newRole = userAccessLevel.level;
logger.info(`[email/verify-code] Role determined for ${linkedWalletAddress}: ${newRole} (tokens: ${userAccessLevel.tokenCount})`);
// Обновляем роль в БД, если она отличается от текущей
const currentRole = authResult.role === 'admin';
if (finalIsAdmin !== currentRole) {
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', [finalIsAdmin ? 'admin' : 'user', authResult.userId]);
logger.info(`[email/verify-code] User role updated in DB for user ${authResult.userId} to ${finalIsAdmin ? 'admin' : 'user'}`);
if (authResult.role !== newRole) {
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', [newRole, authResult.userId]);
logger.info(`[email/verify-code] User role updated in DB for user ${authResult.userId} to ${newRole}`);
}
finalIsAdmin = (newRole === ROLES.EDITOR || newRole === ROLES.READONLY);
} catch (tokenCheckError) {
logger.error(`[email/verify-code] Error checking admin tokens for ${linkedWalletAddress}:`, tokenCheckError);
logger.error(`[email/verify-code] Error checking tokens for ${linkedWalletAddress}:`, tokenCheckError);
// В случае ошибки проверки токенов, используем роль из authResult
finalIsAdmin = authResult.role === 'admin';
finalIsAdmin = (authResult.role === 'editor' || authResult.role === 'readonly');
}
} else {
// Если кошелек не привязан, используем роль из authResult (вероятно, 'user')
finalIsAdmin = authResult.role === 'admin';
finalIsAdmin = (authResult.role === 'editor' || authResult.role === 'readonly');
logger.info(`[email/verify-code] No linked wallet found for user ${authResult.userId}. Using role from authResult: ${authResult.role}`);
}
// ---> КОНЕЦ ОПРЕДЕЛЕНИЯ РОЛИ <---
@@ -663,8 +669,11 @@ router.get('/check', async (req, res) => {
if (roleResult.rows.length > 0) {
const role = roleResult.rows[0].role;
// Преобразуем старую роль в новый формат
if (role === 'admin') {
userAccessLevel = { level: 'editor', tokenCount: 1, hasAccess: true };
// Определяем userAccessLevel на основе роли
if (role === 'editor') {
userAccessLevel = { level: 'editor', tokenCount: 5999998, hasAccess: true };
} else if (role === 'readonly') {
userAccessLevel = { level: 'readonly', tokenCount: 100, hasAccess: true };
} else {
userAccessLevel = { level: 'user', tokenCount: 0, hasAccess: false };
}
@@ -813,7 +822,15 @@ router.post('/refresh-session', async (req, res) => {
req.session.authenticated = true;
req.session.userId = user.id;
req.session.address = address.toLowerCase();
req.session.userAccessLevel = user.role === 'admin' ? { level: 'editor', tokenCount: 0, hasAccess: true } : { level: 'user', tokenCount: 0, hasAccess: false };
let userAccessLevel;
if (user.role === 'editor') {
userAccessLevel = { level: 'editor', tokenCount: 5999998, hasAccess: true };
} else if (user.role === 'readonly') {
userAccessLevel = { level: 'readonly', tokenCount: 100, hasAccess: true };
} else {
userAccessLevel = { level: 'user', tokenCount: 0, hasAccess: false };
}
req.session.userAccessLevel = userAccessLevel;
req.session.authType = 'wallet';
// Сохраняем обновленную сессию
@@ -870,7 +887,7 @@ router.post('/wallet', async (req, res) => {
// Обновляем роль пользователя в базе данных, если нужно
if (userAccessLevel.hasAccess) {
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', ['admin', userId]);
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', ['editor', userId]);
}
// Сохраняем идентификаторы
@@ -1058,7 +1075,7 @@ router.post('/wallet-with-link', authLimiter, async (req, res) => {
req.session.address = address.toLowerCase();
req.session.authenticated = true;
req.session.authType = 'wallet';
const hasAccess = (role === 'admin' || role === 'editor' || role === 'readonly');
const hasAccess = (role === 'editor' || role === 'readonly');
req.session.userAccessLevel = hasAccess ? { level: role === 'editor' ? 'editor' : 'readonly', tokenCount: 0, hasAccess: true } : { level: 'user', tokenCount: 0, hasAccess: false };
await sessionService.saveSession(req.session, 'wallet-with-link');

View File

@@ -21,8 +21,133 @@ const { requirePermission } = require('../middleware/permissions');
// НОВАЯ СИСТЕМА РОЛЕЙ: используем shared/permissions.js
const { hasPermission, ROLES, PERMISSIONS } = require('/app/shared/permissions');
// GET /api/messages?userId=123
// Просмотр сообщений конкретного пользователя (для админов в CRM)
// GET /api/messages/public?userId=123 - получить публичные сообщения пользователя
router.get('/public', requireAuth, async (req, res) => {
const userId = req.query.userId;
const currentUserId = req.user.id;
// Параметры пагинации
const limit = parseInt(req.query.limit, 10) || 30;
const offset = parseInt(req.query.offset, 10) || 0;
const countOnly = req.query.count_only === 'true';
// Получаем ключ шифрования
const encryptionUtils = require('../utils/encryptionUtils');
const encryptionKey = encryptionUtils.getEncryptionKey();
try {
// Публичные сообщения видны на главной странице пользователя
const targetUserId = userId || currentUserId;
// Если нужен только подсчет
if (countOnly) {
const countResult = await db.getQuery()(
`SELECT COUNT(*) FROM messages WHERE user_id = $1 AND message_type = 'public'`,
[targetUserId]
);
const totalCount = parseInt(countResult.rows[0].count, 10);
return res.json({ success: true, count: totalCount, total: totalCount });
}
const result = await db.getQuery()(
`SELECT m.id, m.user_id, decrypt_text(m.sender_type_encrypted, $2) as sender_type,
decrypt_text(m.content_encrypted, $2) as content,
decrypt_text(m.channel_encrypted, $2) as channel,
decrypt_text(m.role_encrypted, $2) as role,
decrypt_text(m.direction_encrypted, $2) as direction,
m.created_at, m.message_type,
arm.last_read_at
FROM messages m
LEFT JOIN admin_read_messages arm ON arm.user_id = m.user_id AND arm.admin_id = $5
WHERE m.user_id = $1 AND m.message_type = 'public'
ORDER BY m.created_at DESC
LIMIT $3 OFFSET $4`,
[targetUserId, encryptionKey, limit, offset, currentUserId]
);
// Получаем общее количество для пагинации
const countResult = await db.getQuery()(
`SELECT COUNT(*) FROM messages WHERE user_id = $1 AND message_type = 'public'`,
[targetUserId]
);
const totalCount = parseInt(countResult.rows[0].count, 10);
res.json({
success: true,
messages: result.rows,
total: totalCount,
limit,
offset,
hasMore: offset + limit < totalCount
});
} catch (e) {
res.status(500).json({ error: 'DB error', details: e.message });
}
});
// GET /api/messages/private - получить приватные сообщения текущего пользователя
router.get('/private', requireAuth, async (req, res) => {
const currentUserId = req.user.id;
// Параметры пагинации
const limit = parseInt(req.query.limit, 10) || 30;
const offset = parseInt(req.query.offset, 10) || 0;
const countOnly = req.query.count_only === 'true';
// Получаем ключ шифрования
const encryptionUtils = require('../utils/encryptionUtils');
const encryptionKey = encryptionUtils.getEncryptionKey();
try {
// Если нужен только подсчет
if (countOnly) {
const countResult = await db.getQuery()(
`SELECT COUNT(*) FROM messages WHERE user_id = $1 AND message_type = 'private'`,
[currentUserId]
);
const totalCount = parseInt(countResult.rows[0].count, 10);
return res.json({ success: true, count: totalCount, total: totalCount });
}
// Приватные сообщения видны только в личных сообщениях
const result = await db.getQuery()(
`SELECT m.id, m.user_id, decrypt_text(m.sender_type_encrypted, $2) as sender_type,
decrypt_text(m.content_encrypted, $2) as content,
decrypt_text(m.channel_encrypted, $2) as channel,
decrypt_text(m.role_encrypted, $2) as role,
decrypt_text(m.direction_encrypted, $2) as direction,
m.created_at, m.message_type,
arm.last_read_at
FROM messages m
LEFT JOIN admin_read_messages arm ON arm.user_id = m.user_id AND arm.admin_id = $5
WHERE m.user_id = $1 AND m.message_type = 'private'
ORDER BY m.created_at DESC
LIMIT $3 OFFSET $4`,
[currentUserId, encryptionKey, limit, offset, currentUserId]
);
// Получаем общее количество для пагинации
const countResult = await db.getQuery()(
`SELECT COUNT(*) FROM messages WHERE user_id = $1 AND message_type = 'private'`,
[currentUserId]
);
const totalCount = parseInt(countResult.rows[0].count, 10);
res.json({
success: true,
messages: result.rows,
total: totalCount,
limit,
offset,
hasMore: offset + limit < totalCount
});
} catch (e) {
res.status(500).json({ error: 'DB error', details: e.message });
}
});
// GET /api/messages?userId=123 - УСТАРЕВШИЙ эндпоинт, используйте /api/messages/public или /api/messages/private
// Оставлен для обратной совместимости
router.get('/', requireAuth, requirePermission(PERMISSIONS.VIEW_CONTACTS), async (req, res) => {
const userId = req.query.userId;
const conversationId = req.query.conversationId;
@@ -107,28 +232,29 @@ router.get('/', requireAuth, requirePermission(PERMISSIONS.VIEW_CONTACTS), async
return res.json(messages);
}
// Стандартная логика для зарегистрированных пользователей
// Стандартная логика для зарегистрированных пользователей - ТОЛЬКО ПУБЛИЧНЫЕ СООБЩЕНИЯ
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 = 'public'
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 = 'public'
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 = 'public'
ORDER BY created_at ASC`,
[encryptionKey]
);
@@ -139,7 +265,8 @@ router.get('/', requireAuth, requirePermission(PERMISSIONS.VIEW_CONTACTS), async
}
});
// POST /api/messages
// POST /api/messages - УСТАРЕВШИЙ эндпоинт, используйте /api/messages/send
// Оставлен для обратной совместимости, но теперь сохраняет как публичные сообщения
router.post('/', async (req, res) => {
const { user_id, sender_type, content, channel, role, direction, attachment_filename, attachment_mimetype, attachment_size, attachment_data } = req.body;
@@ -197,11 +324,11 @@ router.post('/', async (req, res) => {
} else {
conversation = conversationResult.rows[0];
}
// 3. Сохраняем сообщение с conversation_id
// 3. Сохраняем сообщение с conversation_id и типом 'public' (для обратной совместимости)
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]
`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,$13),encrypt_text($4,$13),encrypt_text($5,$13),encrypt_text($6,$13),encrypt_text($7,$13),$12,NOW(),encrypt_text($8,$13),encrypt_text($9,$13),$10,$11) RETURNING *`,
[user_id, conversation.id, sender_type, content, channel, role, direction, attachment_filename, attachment_mimetype, attachment_size, attachment_data, 'public', encryptionKey]
);
// 4. Если это исходящее сообщение для Telegram — отправляем через бота
if (channel === 'telegram' && direction === 'out') {
@@ -426,7 +553,7 @@ router.post('/broadcast', requireAuth, requirePermission(PERMISSIONS.BROADCAST),
await db.getQuery()(
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, message_type, created_at)
VALUES ($1, $2, encrypt_text($3, $8), encrypt_text($4, $8), encrypt_text($5, $8), encrypt_text($6, $8), encrypt_text($7, $8), $9, NOW())`,
[user_id, conversation.id, 'admin', content, 'email', 'user', 'out', encryptionKey, 'user_chat']
[user_id, conversation.id, 'editor', content, 'email', 'user', 'out', encryptionKey, 'user_chat']
);
results.push({ channel: 'email', status: 'sent' });
sent = true;
@@ -449,7 +576,7 @@ router.post('/broadcast', requireAuth, requirePermission(PERMISSIONS.BROADCAST),
await db.getQuery()(
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, message_type, created_at)
VALUES ($1, $2, encrypt_text($3, $8), encrypt_text($4, $8), encrypt_text($5, $8), encrypt_text($6, $8), encrypt_text($7, $8), $9, NOW())`,
[user_id, conversation.id, 'admin', content, 'telegram', 'user', 'out', encryptionKey, 'user_chat']
[user_id, conversation.id, 'editor', content, 'telegram', 'user', 'out', encryptionKey, 'user_chat']
);
results.push({ channel: 'telegram', status: 'sent' });
sent = true;
@@ -466,9 +593,9 @@ router.post('/broadcast', requireAuth, requirePermission(PERMISSIONS.BROADCAST),
if (wallet) {
// Здесь можно реализовать отправку через web3, если нужно
await db.getQuery()(
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, 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, 'wallet', '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, $9), encrypt_text($4, $9), encrypt_text($5, $9), encrypt_text($6, $9), encrypt_text($7, $9), $8, NOW())`,
[user_id, conversation.id, 'editor', content, 'wallet', 'user', 'out', 'user_chat', encryptionKey]
);
results.push({ channel: 'wallet', status: 'saved' });
sent = true;
@@ -482,6 +609,171 @@ router.post('/broadcast', requireAuth, requirePermission(PERMISSIONS.BROADCAST),
}
});
// POST /api/messages/send - новый эндпоинт для отправки сообщений с проверкой ролей
router.post('/send', requireAuth, async (req, res) => {
const { recipientId, content, messageType = 'public', markAsRead = false } = req.body;
if (!recipientId || !content) {
return res.status(400).json({ error: 'recipientId и content обязательны' });
}
if (!['public', 'private'].includes(messageType)) {
return res.status(400).json({ error: 'messageType должен быть "public" или "private"' });
}
try {
// Получаем информацию об отправителе
const senderId = req.user.id;
const senderRole = req.user.contact_type || req.user.role;
// Получаем информацию о получателе
const recipientResult = await db.getQuery()(
'SELECT id, contact_type FROM users WHERE id = $1',
[recipientId]
);
if (recipientResult.rows.length === 0) {
return res.status(404).json({ error: 'Получатель не найден' });
}
const recipientRole = recipientResult.rows[0].contact_type;
// Проверка прав согласно матрице разрешений
const canSend = (
// Editor может отправлять всем
(senderRole === 'editor') ||
// User и readonly могут отправлять только editor
((senderRole === 'user' || senderRole === 'readonly') && recipientRole === 'editor')
);
if (!canSend) {
return res.status(403).json({
error: 'Недостаточно прав для отправки сообщения этому получателю'
});
}
// Получаем ключ шифрования
const encryptionUtils = require('../utils/encryptionUtils');
const encryptionKey = encryptionUtils.getEncryptionKey();
// Находим или создаем беседу
let conversationResult = await db.getQuery()(
'SELECT id FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC LIMIT 1',
[recipientId]
);
let conversationId;
if (conversationResult.rows.length === 0) {
const title = `Чат с пользователем ${recipientId}`;
const newConv = await db.getQuery()(
'INSERT INTO conversations (user_id, title_encrypted, created_at, updated_at) VALUES ($1, encrypt_text($2, $3), NOW(), NOW()) RETURNING id',
[recipientId, title, encryptionKey]
);
conversationId = newConv.rows[0].id;
} else {
conversationId = conversationResult.rows[0].id;
}
// Сохраняем сообщение с типом
const result = await db.getQuery()(
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, message_type, created_at)
VALUES ($1, $2, encrypt_text($3, $8), encrypt_text($4, $8), encrypt_text($5, $8), encrypt_text($6, $8), encrypt_text($7, $8), $9, NOW()) RETURNING *`,
[recipientId, conversationId, 'editor', content, 'web', 'user', 'out', encryptionKey, messageType]
);
// Отправляем обновление через WebSocket
broadcastMessagesUpdate();
// Если нужно отметить как прочитанное
if (markAsRead) {
try {
const lastReadAt = new Date().toISOString();
await db.getQuery()(
`INSERT INTO admin_read_messages (admin_id, user_id, last_read_at)
VALUES ($1, $2, $3)
ON CONFLICT (admin_id, user_id) DO UPDATE SET last_read_at = EXCLUDED.last_read_at`,
[senderId, recipientId, lastReadAt]
);
} catch (markError) {
console.warn('[WARNING] /send mark-read error:', markError);
// Не прерываем выполнение, если mark-read не удался
}
}
res.json({ success: true, message: result.rows[0] });
} catch (e) {
console.error('[ERROR] /send:', e);
res.status(500).json({ error: 'DB error', details: e.message });
}
});
// GET /api/messages/conversations?userId=123 - получить диалоги пользователя
router.get('/conversations', requireAuth, async (req, res) => {
const userId = req.query.userId;
if (!userId) return res.status(400).json({ error: 'userId required' });
try {
const result = await db.getQuery()(
'SELECT * FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC',
[userId]
);
res.json({ success: true, conversations: result.rows });
} catch (e) {
res.status(500).json({ error: 'DB error', details: e.message });
}
});
// POST /api/messages/conversations - создать диалог для пользователя
router.post('/conversations', requireAuth, async (req, res) => {
const { userId, title } = req.body;
if (!userId) return res.status(400).json({ error: 'userId required' });
// Получаем ключ шифрования через унифицированную утилиту
const encryptionUtils = require('../utils/encryptionUtils');
const encryptionKey = encryptionUtils.getEncryptionKey();
try {
const result = await db.getQuery()(
`INSERT INTO conversations (user_id, title_encrypted, created_at, updated_at)
VALUES ($1, encrypt_text($2, $3), NOW(), NOW()) RETURNING *`,
[userId, title || 'Новый диалог', encryptionKey]
);
res.json({ success: true, conversation: result.rows[0] });
} catch (e) {
res.status(500).json({ error: 'DB error', details: e.message });
}
});
// DELETE /api/messages/delete-history/:userId - удалить историю сообщений пользователя (новый API)
router.delete('/delete-history/:userId', requireAuth, requirePermission(PERMISSIONS.DELETE_MESSAGES), async (req, res) => {
const userId = req.params.userId;
if (!userId) {
return res.status(400).json({ error: 'userId required' });
}
try {
// Проверяем права администратора
if (!req.user || !req.user.userAccessLevel?.hasAccess) {
return res.status(403).json({ error: 'Only administrators can delete message history' });
}
// Удаляем все сообщения пользователя
const result = await db.getQuery()(
'DELETE FROM messages WHERE user_id = $1 RETURNING id',
[userId]
);
res.json({
success: true,
deletedCount: result.rows.length,
message: `Deleted ${result.rows.length} messages for user ${userId}`
});
} catch (e) {
console.error('[ERROR] /delete-history/:userId:', e);
res.status(500).json({ error: 'DB error', details: e.message });
}
});
// DELETE /api/messages/history/:userId - удалить историю сообщений пользователя
// Удаление истории сообщений пользователя
router.delete('/history/:userId', requireAuth, requirePermission(PERMISSIONS.DELETE_MESSAGES), async (req, res) => {

View File

@@ -67,7 +67,7 @@ router.put('/profile', requireAuth, async (req, res) => {
*/
// Получение списка пользователей с фильтрацией (CRM/Контакты)
router.get('/', requireAuth, requirePermission(PERMISSIONS.VIEW_CONTACTS), async (req, res, next) => {
router.get('/', requireAuth, async (req, res, next) => {
try {
const {
tagIds = '',
@@ -79,6 +79,7 @@ router.get('/', requireAuth, requirePermission(PERMISSIONS.VIEW_CONTACTS), async
blocked = 'all'
} = req.query;
const adminId = req.user && req.user.id;
const userRole = req.user.role;
// Получаем ключ шифрования
const fs = require('fs');
@@ -92,6 +93,13 @@ router.get('/', requireAuth, requirePermission(PERMISSIONS.VIEW_CONTACTS), async
const params = [];
let idx = 1;
// Фильтрация для USER - видит только editor админов и себя
if (userRole === 'user') {
const { ROLES } = require('/app/shared/permissions');
where.push(`(u.role = '${ROLES.EDITOR}' OR u.id = $${idx++})`);
params.push(req.user.id);
}
// Фильтр по дате
if (dateFrom) {
where.push(`DATE(u.created_at) >= $${idx++}`);
@@ -148,8 +156,7 @@ router.get('/', requireAuth, requirePermission(PERMISSIONS.VIEW_CONTACTS), async
u.created_at, u.preferred_language, u.is_blocked, u.role,
CASE
WHEN u.role = 'editor' THEN 'editor'
WHEN u.role = 'readonly' THEN 'readonly'
WHEN u.role = 'admin' THEN 'admin'
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,
@@ -368,9 +375,10 @@ router.post('/mark-contact-read', async (req, res) => {
if (req.user?.userAccessLevel) {
// Используем новую систему ролей
if (req.user.userAccessLevel.level === 'readonly') {
const { ROLES } = require('/app/shared/permissions');
if (req.user.userAccessLevel.level === ROLES.READONLY) {
userRole = ROLES.READONLY;
} else if (req.user.userAccessLevel.level === 'editor') {
} else if (req.user.userAccessLevel.level === ROLES.EDITOR) {
userRole = ROLES.EDITOR;
}
} else if (req.user?.id) {
@@ -396,7 +404,7 @@ router.post('/mark-contact-read', async (req, res) => {
// Админ может помечать любого контакта как прочитанного, включая самого себя
} else {
// Для всех остальных ролей (GUEST, USER) - НЕ записываем в БД
console.log('[DEBUG] /mark-contact-read: User role is not admin, not recording in admin_read_contacts. Role:', userRole);
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 }); // Просто возвращаем успех без записи в БД
}
@@ -583,6 +591,37 @@ router.delete('/:id', requireAuth, requirePermission(PERMISSIONS.DELETE_USER_DAT
}
});
// --- Получение ролей из базы данных (созданных через миграции) ---
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) => {
@@ -717,17 +756,17 @@ router.post('/', async (req, res) => {
const { first_name, last_name, preferred_language } = req.body;
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
// Получаем ключ шифрования через унифицированную утилиту
const encryptionUtils = require('../utils/encryptionUtils');
const encryptionKey = encryptionUtils.getEncryptionKey();
// Используем централизованную систему ролей
const { ROLES } = require('/app/shared/permissions');
try {
const result = await db.getQuery()(
`INSERT INTO users (first_name_encrypted, last_name_encrypted, preferred_language, created_at)
VALUES (encrypt_text($1, $4), encrypt_text($2, $4), $3, NOW()) RETURNING *`,
[first_name, last_name, JSON.stringify(preferred_language || []), encryptionKey]
`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] });
@@ -784,8 +823,9 @@ router.post('/import', requireAuth, async (req, res) => {
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, created_at) VALUES (encrypt_text($1, $3), encrypt_text($2, $3), NOW()) RETURNING id', [first_name, last_name, encryptionKey]);
// Создаём нового пользователя с централизованной ролью
const { ROLES } = require('/app/shared/permissions');
const ins = await dbq('INSERT INTO users (first_name_encrypted, last_name_encrypted, role, created_at) VALUES (encrypt_text($1, $4), encrypt_text($2, $4), $3, NOW()) RETURNING id', [first_name, last_name, ROLES.USER, encryptionKey]);
userId = ins.rows[0].id;
added++;
}
@@ -820,4 +860,5 @@ router.post('/import', requireAuth, async (req, res) => {
// DELETE /api/tags/user/:id/tag/:tagId — удалить тег у пользователя
// POST /api/tags/user/:id/multirelations — массовое обновление тегов
module.exports = router;