From 927d174f66e267e502dcd1430e232a08d7008ab4 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 16 Oct 2025 18:44:30 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BD=D0=BE=D0=B2=D0=B0=D1=8F=20=D1=84?= =?UTF-8?q?=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DEPLOYMENT_FIXES.md | 40 -- backend/db/init.js | 6 +- backend/db/migrations/010_cleanup_roles.sql | 2 +- backend/middleware/auth.js | 16 +- backend/routes/ai-queue.js | 6 +- backend/routes/auth.js | 75 ++-- backend/routes/messages.js | 328 +++++++++++++++- backend/routes/users.js | 69 +++- backend/server.js | 10 + backend/services/IdentityLinkService.js | 4 +- backend/services/admin-role.js | 138 ------- backend/services/adminLogicService.js | 6 +- backend/services/auth-service.js | 270 ++++++------- backend/services/authTokenService.js | 8 +- backend/services/emailAuth.js | 12 +- backend/services/identity-service.js | 14 +- backend/services/tokenBalanceService.js | 12 +- backend/services/unifiedMessageProcessor.js | 4 +- frontend/src/components/BaseLayout.vue | 14 +- frontend/src/components/ContactTable.vue | 238 +++++++++-- frontend/src/composables/useAuth.js | 8 +- frontend/src/composables/useChat.js | 31 +- .../src/composables/useContactsWebSocket.js | 41 +- frontend/src/composables/useRoles.js | 104 +++++ frontend/src/router/index.js | 5 + frontend/src/services/adminChatService.js | 6 +- frontend/src/services/messagesService.js | 60 ++- frontend/src/views/ContactsView.vue | 209 +--------- frontend/src/views/CrmView.vue | 30 ++ frontend/src/views/PersonalMessagesView.vue | 43 +- .../AcceleratorRegistrationView.vue | 368 ++++++++++++++++++ .../src/views/contacts/ContactDetailsView.vue | 15 +- shared/permissions.js | 2 + 33 files changed, 1494 insertions(+), 700 deletions(-) delete mode 100644 DEPLOYMENT_FIXES.md delete mode 100644 backend/services/admin-role.js create mode 100644 frontend/src/composables/useRoles.js create mode 100644 frontend/src/views/accelerator/AcceleratorRegistrationView.vue diff --git a/DEPLOYMENT_FIXES.md b/DEPLOYMENT_FIXES.md deleted file mode 100644 index 34b21ae..0000000 --- a/DEPLOYMENT_FIXES.md +++ /dev/null @@ -1,40 +0,0 @@ -# Исправления ошибок деплоя DLE - -## Проблема -Ошибки `Cannot access 'chainId' before initialization` в скриптах деплоя из-за неправильного использования переменных. - -## Источник данных -**База данных `deploy_params`:** -- `supported_chain_ids`: `[421614, 84532, 11155111, 17000]` (числовые chainId) -- `rpc_urls`: `["https://sepolia-rollup.arbitrum.io/rpc", ...]` (строки URL) - -## Исправления - -### 1. deploy-multichain.js -- **Функция `deployInNetwork(chainId, ...)`** -- **Было:** `const networkChainId = Number(net.chainId)` + использование `networkChainId` -- **Стало:** Использование параметра `chainId` напрямую -- **Причина:** `chainId` уже приходит как числовой параметр из `supportedChainIds` базы данных - -### 2. deploy-modules.js -- **Функция `deployModuleInNetwork(rpcUrl, ...)`** -- **Было:** `createRPCConnection(rpcUrl, ...)` - неправильно -- **Стало:** Получение `chainId` из RPC URL + `createRPCConnection(chainId, ...)` -- **Функция `deployAllModulesInNetwork(chainId, ...)`** -- **Было:** Создание `networkChainId` + использование его -- **Стало:** Использование параметра `chainId` напрямую - -### 3. DleDeployFormView.vue -- **Было:** `adminTokenCheck` использовался в watcher до объявления -- **Стало:** Объявление `adminTokenCheck` перед watcher'ом - -## Логика работы -1. База данных → `params.supportedChainIds` (числовые chainId) -2. `createMultipleRPCConnections(supportedChainIds, ...)` -3. `connection.network.chainId` возвращает тот же числовой chainId -4. `deployInNetwork(chainId, ...)` получает числовой chainId как параметр -5. **Внутри функций используем `chainId` (параметр), НЕ создаем `networkChainId`** - -## Результат -✅ Все ошибки инициализации переменных исправлены -✅ Система готова к работе без ошибок `Cannot access before initialization` diff --git a/backend/db/init.js b/backend/db/init.js index 9feb83a..cf88cf7 100644 --- a/backend/db/init.js +++ b/backend/db/init.js @@ -41,7 +41,7 @@ async function initRoles() { await pool.query(` INSERT INTO roles (id, name, description) VALUES (3, 'user', 'Обычный пользователь'), - (4, 'admin', 'Администратор с полным доступом'); + (4, 'editor', 'Администратор с полным доступом'); `); // console.log('Таблица roles создана и заполнена'); @@ -57,7 +57,7 @@ async function initRoles() { `SELECT EXISTS (SELECT FROM roles WHERE name = 'user');` ); const adminRoleExists = await pool.query( - `SELECT EXISTS (SELECT FROM roles WHERE name = 'admin');` + `SELECT EXISTS (SELECT FROM roles WHERE name = 'editor');` ); if (!userRoleExists.rows[0].exists) { @@ -70,7 +70,7 @@ async function initRoles() { if (!adminRoleExists.rows[0].exists) { await pool.query(` INSERT INTO roles (id, name, description) VALUES - (4, 'admin', 'Администратор с полным доступом'); + (4, 'editor', 'Администратор с полным доступом'); `); } diff --git a/backend/db/migrations/010_cleanup_roles.sql b/backend/db/migrations/010_cleanup_roles.sql index a5e8b2e..ae6309c 100644 --- a/backend/db/migrations/010_cleanup_roles.sql +++ b/backend/db/migrations/010_cleanup_roles.sql @@ -2,7 +2,7 @@ DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'user_role') THEN - CREATE TYPE user_role AS ENUM ('user', 'admin'); + CREATE TYPE user_role AS ENUM ('user', 'readonly', 'editor'); END IF; END $$; diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js index 2eb07bc..53e2674 100644 --- a/backend/middleware/auth.js +++ b/backend/middleware/auth.js @@ -18,7 +18,6 @@ const logger = require('../utils/logger'); // НОВАЯ СИСТЕМА РОЛЕЙ: используем shared/permissions.js const { hasPermission, ROLES, PERMISSIONS } = require('/app/shared/permissions'); const db = require('../db'); -const { checkAdminTokens } = require('../services/auth-service'); // Получаем ключ шифрования const encryptionUtils = require('../utils/encryptionUtils'); @@ -29,10 +28,21 @@ const encryptionKey = encryptionUtils.getEncryptionKey(); */ const requireAuth = async (req, res, next) => { // console.log('[DIAG][requireAuth] session:', req.session); - if (!req.session || !req.session.authenticated) { + + // Проверяем аутентификацию через сессию + if (!req.session) { return res.status(401).json({ error: 'Требуется аутентификация' }); } - // Можно добавить проверку isAdmin здесь, если нужно + + // Проверяем различные способы аутентификации + const isAuthenticated = req.session.authenticated || + (req.session.userId && req.session.authType) || + (req.session.address && req.session.authType === 'wallet'); + + if (!isAuthenticated) { + return res.status(401).json({ error: 'Требуется аутентификация' }); + } + next(); }; diff --git a/backend/routes/ai-queue.js b/backend/routes/ai-queue.js index a41bb6d..21ce1f9 100644 --- a/backend/routes/ai-queue.js +++ b/backend/routes/ai-queue.js @@ -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({ diff --git a/backend/routes/auth.js b/backend/routes/auth.js index f1f4c19..dcb552e 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -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'); diff --git a/backend/routes/messages.js b/backend/routes/messages.js index a44e917..8b41439 100644 --- a/backend/routes/messages.js +++ b/backend/routes/messages.js @@ -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) => { diff --git a/backend/routes/users.js b/backend/routes/users.js index 8f27986..9e2a446 100644 --- a/backend/routes/users.js +++ b/backend/routes/users.js @@ -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; diff --git a/backend/server.js b/backend/server.js index 88dc306..037adb9 100644 --- a/backend/server.js +++ b/backend/server.js @@ -117,6 +117,16 @@ async function startServer() { ragService.startQueueWorker(); console.log('[Server] ✅ AI Queue Worker запущен'); } + + // ✨ Запускаем периодическую проверку ролей пользователей + const authService = require('./services/auth-service'); + authService.startRoleUpdateInterval(); + console.log('[Server] ✅ Периодическая проверка ролей запущена'); + + // ✨ Выполняем немедленную проверку ролей при запуске + authService.updateUserRolesPeriodically().catch(error => { + console.error('[Server] ❌ Ошибка при первоначальной проверке ролей:', error); + }); }) .catch(error => { console.error('[Server] ❌ Ошибка инициализации:', error.message); diff --git a/backend/services/IdentityLinkService.js b/backend/services/IdentityLinkService.js index d34c594..5af4054 100644 --- a/backend/services/IdentityLinkService.js +++ b/backend/services/IdentityLinkService.js @@ -218,7 +218,7 @@ class IdentityLinkService { `UPDATE users SET role = $1 WHERE id = $2`, ['editor', userId] ); - logger.info(`[IdentityLinkService] Пользователь ${userId} получил роль admin`); + logger.info(`[IdentityLinkService] Пользователь ${userId} получил роль editor`); } // 7. Создаем identifier для миграции @@ -235,7 +235,7 @@ class IdentityLinkService { userId, identifier, provider: tokenData.source_provider, - role: userAccessLevel.hasAccess ? 'admin' : 'user' + role: userAccessLevel.hasAccess ? 'editor' : 'user' }; } catch (error) { diff --git a/backend/services/admin-role.js b/backend/services/admin-role.js deleted file mode 100644 index d0bbdf3..0000000 --- a/backend/services/admin-role.js +++ /dev/null @@ -1,138 +0,0 @@ -/** - * 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 { ethers } = require('ethers'); -const logger = require('../utils/logger'); -const db = require('../db'); -const authTokenService = require('./authTokenService'); -const rpcProviderService = require('./rpcProviderService'); - -// Минимальный ABI для ERC20 -const ERC20_ABI = [ - 'function balanceOf(address owner) view returns (uint256)' -]; - -/** - * Основной метод проверки роли админа - * @param {string} address - Адрес кошелька - * @returns {Promise} - Является ли пользователь админом - */ -async function checkAdminRole(address) { - if (!address) return false; - logger.info(`Checking admin role for address: ${address}`); - - try { - let foundTokens = false; - let errorCount = 0; - const balances = {}; - // Получаем ключ шифрования через унифицированную утилиту - const encryptionUtils = require('../utils/encryptionUtils'); - const encryptionKey = encryptionUtils.getEncryptionKey(); - - // Получаем токены и RPC из базы с расшифровкой - const tokensResult = await db.getQuery()( - 'SELECT id, min_balance, created_at, updated_at, decrypt_text(name_encrypted, $1) as name, decrypt_text(address_encrypted, $1) as address, decrypt_text(network_encrypted, $1) as network FROM auth_tokens', - [encryptionKey] - ); - const tokens = tokensResult.rows; - - const rpcProvidersResult = await db.getQuery()( - 'SELECT id, chain_id, created_at, updated_at, decrypt_text(network_id_encrypted, $1) as network_id, decrypt_text(rpc_url_encrypted, $1) as rpc_url FROM rpc_providers', - [encryptionKey] - ); - const rpcProviders = rpcProvidersResult.rows; - - logger.info(`Retrieved ${tokens.length} tokens and ${rpcProviders.length} RPC providers`); - logger.info('Tokens:', JSON.stringify(tokens, null, 2)); - logger.info('RPC Providers:', JSON.stringify(rpcProviders, null, 2)); - const rpcMap = {}; - for (const rpc of rpcProviders) { - rpcMap[rpc.network_id] = rpc.rpc_url; - } - const checkPromises = tokens.map(async (token) => { - try { - const rpcUrl = rpcMap[token.network]; - if (!rpcUrl) { - logger.error(`No RPC URL for network ${token.network}`); - balances[token.network] = 'Error: No RPC URL'; - errorCount++; - return null; - } - const provider = new ethers.JsonRpcProvider(await rpcService.getRpcUrlByChainId(chainId)); - // Проверяем доступность сети с таймаутом - try { - const networkCheckPromise = provider.getNetwork(); - const timeoutPromise = new Promise((_, reject) => - setTimeout(() => reject(new Error('Network check timeout')), 3000) - ); - await Promise.race([networkCheckPromise, timeoutPromise]); - } catch (networkError) { - logger.error(`Provider for ${token.network} is not available: ${networkError.message}`); - balances[token.network] = 'Error: Network unavailable'; - errorCount++; - return null; - } - const tokenContract = new ethers.Contract(token.address, ERC20_ABI, provider); - const balancePromise = tokenContract.balanceOf(address); - const timeoutPromise = new Promise((_, reject) => - setTimeout(() => reject(new Error('Timeout')), 3000) - ); - const balance = await Promise.race([balancePromise, timeoutPromise]); - const formattedBalance = ethers.formatUnits(balance, 18); - balances[token.network] = formattedBalance; - logger.info(`Token balance on ${token.network}:`, { - address, - contract: token.address, - balance: formattedBalance, - minBalance: token.min_balance, - hasTokens: parseFloat(formattedBalance) >= parseFloat(token.min_balance), - }); - if (parseFloat(formattedBalance) >= parseFloat(token.min_balance)) { - logger.info(`Found admin tokens on ${token.network}`); - foundTokens = true; - } - return { network: token.network, balance: formattedBalance }; - } catch (error) { - logger.error(`Error checking balance in ${token.network}:`, { - address, - contract: token.address, - error: error.message || 'Unknown error', - }); - balances[token.network] = 'Error'; - errorCount++; - return null; - } - }); - await Promise.all(checkPromises); - if (errorCount === tokens.length) { - logger.error(`All network checks for ${address} failed. Cannot verify admin status.`); - return false; - } - if (foundTokens) { - logger.info(`Admin role summary for ${address}:`, { - networks: Object.keys(balances).filter( - (net) => parseFloat(balances[net]) > 0 && balances[net] !== 'Error' - ), - balances, - }); - logger.info(`Admin role granted for ${address}`); - return true; - } - logger.info(`Admin role denied - no tokens found for ${address}`); - return false; - } catch (error) { - logger.error(`Error in checkAdminRole for ${address}:`, error); - return false; - } -} - -module.exports = { checkAdminRole }; \ No newline at end of file diff --git a/backend/services/adminLogicService.js b/backend/services/adminLogicService.js index b93fd2b..f06a73b 100644 --- a/backend/services/adminLogicService.js +++ b/backend/services/adminLogicService.js @@ -30,7 +30,7 @@ function shouldGenerateAiReply(params) { const { senderType, userId, recipientId } = params; // Обычные пользователи всегда получают AI ответ - if (senderType !== 'admin') { + if (senderType !== 'editor') { return true; } @@ -103,7 +103,7 @@ function canPerformAdminAction(params) { } // editor может все (и свои действия, и readonly действия) - if (role === 'editor') { + if (role === 'editor' || role === 'readonly') { return editorOnlyActions.includes(action) || readonlyActions.includes(action); } @@ -120,7 +120,7 @@ function getAdminSettings(params) { const { role } = params; // Editor - полные права - if (role === 'editor') { + if (role === 'editor' || role === 'readonly') { return { role: 'editor', roleDisplay: 'Редактор', diff --git a/backend/services/auth-service.js b/backend/services/auth-service.js index 13ebdef..f44e22c 100644 --- a/backend/services/auth-service.js +++ b/backend/services/auth-service.js @@ -18,11 +18,14 @@ const crypto = require('crypto'); const { processMessage } = require('./ai-assistant'); // Используем AI Assistant const verificationService = require('./verification-service'); // Используем сервис верификации const identityService = require('./identity-service'); // <-- ДОБАВЛЕН ИМПОРТ +const { ROLES } = require('/app/shared/permissions'); // Централизованная система ролей const authTokenService = require('./authTokenService'); const rpcProviderService = require('./rpcProviderService'); const tokenBalanceService = require('./tokenBalanceService'); const { getLinkedWallet } = require('./wallet-service'); -const { checkAdminRole } = require('./admin-role'); + +// Периодическая проверка и обновление ролей пользователей +let roleUpdateInterval = null; const { broadcastContactsUpdate } = require('../wsHub'); const ERC20_ABI = ['function balanceOf(address owner) view returns (uint256)']; @@ -84,12 +87,14 @@ class AuthService { const currentAccessLevel = userAccessLevel !== null ? userAccessLevel : await this.getUserAccessLevel(normalizedAddress); // Если уровень доступа изменился, обновляем роль в базе данных - const currentRole = userData.role === 'admin' ? 'editor' : 'user'; - const newRole = currentAccessLevel.hasAccess ? 'admin' : 'user'; + const currentRole = userData.role; + + // Используем роль из currentAccessLevel, которая уже правильно определена с учетом порогов + const newRole = currentAccessLevel.level; if (currentRole !== newRole) { await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', [newRole, userData.id]); - logger.info(`Updated user ${userData.id} role to ${newRole} (access level changed)`); + logger.info(`Updated user ${userData.id} role to ${newRole} (access level changed, tokens: ${currentAccessLevel.tokenCount})`); } return { @@ -100,7 +105,15 @@ class AuthService { // Если пользователь не найден, создаем нового с правильной ролью const currentAccessLevel = userAccessLevel !== null ? userAccessLevel : await this.getUserAccessLevel(normalizedAddress); - const initialRole = currentAccessLevel.hasAccess ? 'admin' : 'user'; + logger.info(`[verify] Creating user - userAccessLevel: ${userAccessLevel ? JSON.stringify(userAccessLevel) : 'null'}`); + logger.info(`[verify] Creating user - currentAccessLevel: ${JSON.stringify(currentAccessLevel)}`); + + let initialRole = ROLES.USER; + if (currentAccessLevel.hasAccess) { + initialRole = currentAccessLevel.level; + } + + logger.info(`[verify] Creating user - initialRole determined: ${initialRole}`); const newUserResult = await db.getQuery()('INSERT INTO users (role) VALUES ($1) RETURNING id', [initialRole]); const userId = newUserResult.rows[0].id; @@ -219,7 +232,7 @@ class AuthService { session.userId = userId; session.authenticated = authenticated; session.authType = authType; - session.userAccessLevel = userAccessLevel || { level: 'user', tokenCount: 0, hasAccess: false }; + session.userAccessLevel = userAccessLevel || { level: ROLES.USER, tokenCount: 0, hasAccess: false }; // Сохраняем адрес кошелька если есть if (address) { @@ -237,7 +250,7 @@ class AuthService { authenticated, authType, address, - userAccessLevel: userAccessLevel || { level: 'user', tokenCount: 0, hasAccess: false }, + userAccessLevel: userAccessLevel || { level: ROLES.USER, tokenCount: 0, hasAccess: false }, cookie: session.cookie, }), session.id, @@ -301,18 +314,21 @@ class AuthService { // с базовым доступом к чату и истории сообщений if (!wallet) { logger.info(`No wallet linked for user ${userId}, assigning basic user role`); - return 'user'; + return ROLES.USER; } // Если есть кошелек, проверяем уровень доступа const userAccessLevel = await this.getUserAccessLevel(wallet); logger.info( - `Role check for user ${userId} with wallet ${wallet}: ${userAccessLevel.hasAccess ? 'admin' : 'user'}` + `Role check for user ${userId} with wallet ${wallet}: ${userAccessLevel.hasAccess ? 'editor/readonly' : 'user'}` ); - return userAccessLevel.hasAccess ? 'admin' : 'user'; + if (userAccessLevel.hasAccess) { + return userAccessLevel.level; + } + return ROLES.USER; } catch (error) { logger.error('Error checking user role:', error); - return 'user'; + return ROLES.USER; } } @@ -338,12 +354,12 @@ class AuthService { // Проверяем наличие кошелька и определяем роль const wallet = await getLinkedWallet(userId); - let role = 'user'; // Базовая роль для доступа к чату + let role = ROLES.USER; // Базовая роль для доступа к чату if (wallet) { // Если есть кошелек, проверяем уровень доступа const userAccessLevel = await this.getUserAccessLevel(wallet); - role = userAccessLevel.hasAccess ? 'admin' : 'user'; + role = userAccessLevel.hasAccess ? userAccessLevel.level : ROLES.USER; logger.info(`User ${userId} has wallet ${wallet}, role set to ${role}`); } else { logger.info(`User ${userId} has no wallet, using basic user role`); @@ -386,7 +402,7 @@ class AuthService { return { success: true, userId, - role: session.userAccessLevel?.hasAccess ? 'admin' : 'user', + role: session.userAccessLevel?.level || ROLES.USER, telegramId, isNewUser: false, }; @@ -412,7 +428,7 @@ class AuthService { } else { // Создаем нового пользователя для нового telegramId const newUserResult = await db.getQuery()('INSERT INTO users (role) VALUES ($1) RETURNING id', [ - 'user', + ROLES.USER, ]); userId = newUserResult.rows[0].id; isNewUser = true; @@ -445,7 +461,7 @@ class AuthService { return { success: true, userId, - role: 'user', + role: ROLES.USER, telegramId, isNewUser, }; @@ -463,13 +479,13 @@ class AuthService { */ async getUserAccessLevel(address) { if (!address) { - return { level: 'user', tokenCount: 0, hasAccess: false }; + return { level: ROLES.USER, tokenCount: 0, hasAccess: false }; } logger.info(`Checking access level for address: ${address}`); try { - // Получаем токены из базы данных напрямую (как в checkAdminRole) + // Получаем токены из базы данных напрямую const fs = require('fs'); const path = require('path'); let encryptionKey = 'default-key'; @@ -542,8 +558,8 @@ class AuthService { symbol: '', balance, minBalance: token.min_balance, - readonlyThreshold: token.readonly_threshold || 1, - editorThreshold: token.editor_threshold || 2, + readonlyThreshold: token.readonly_threshold, + editorThreshold: token.editor_threshold, }); logger.info(`[getUserAccessLevel] Token balance for ${token.name} (${token.address}): ${balance}`); @@ -557,15 +573,15 @@ class AuthService { symbol: '', balance: '0', minBalance: token.min_balance, - readonlyThreshold: token.readonly_threshold || 1, - editorThreshold: token.editor_threshold || 2, + readonlyThreshold: token.readonly_threshold, + editorThreshold: token.editor_threshold, }); } } if (!tokenBalances || !Array.isArray(tokenBalances)) { logger.warn(`No token balances found for address: ${address}`); - return { level: 'user', tokenCount: 0, hasAccess: false }; + return { level: ROLES.USER, tokenCount: 0, hasAccess: false }; } // Подсчитываем сумму токенов с достаточным балансом @@ -594,7 +610,7 @@ class AuthService { }); // Определяем уровень доступа на основе настроек токенов - let accessLevel = 'user'; + let accessLevel = ROLES.USER; let hasAccess = false; // Получаем настройки порогов из токенов (используем самые низкие требования для максимального доступа) @@ -604,8 +620,8 @@ class AuthService { if (tokenBalances.length > 0) { // Находим самые низкие пороги среди всех токенов for (const token of tokenBalances) { - const tokenReadonlyThreshold = token.readonlyThreshold || 1; - const tokenEditorThreshold = token.editorThreshold || 2; + const tokenReadonlyThreshold = token.readonlyThreshold; + const tokenEditorThreshold = token.editorThreshold; if (tokenReadonlyThreshold < readonlyThreshold) { readonlyThreshold = tokenReadonlyThreshold; @@ -615,27 +631,25 @@ class AuthService { } } - // Если не нашли токены с порогами, используем дефолтные значения - if (readonlyThreshold === Infinity) readonlyThreshold = 1; - if (editorThreshold === Infinity) editorThreshold = 2; - logger.info(`[AuthService] Определены пороги доступа: readonly=${readonlyThreshold}, editor=${editorThreshold} (из ${tokenBalances.length} токенов)`); } else { - readonlyThreshold = 1; - editorThreshold = 2; + // Если токенов нет, пользователь не имеет доступа + logger.warn(`[AuthService] Токены не найдены, пользователь не имеет доступа`); + readonlyThreshold = Infinity; + editorThreshold = Infinity; } if (validTokenCount >= editorThreshold) { // Достаточно токенов для полных прав редактора - accessLevel = 'editor'; + accessLevel = ROLES.EDITOR; hasAccess = true; } else if (validTokenCount > 0) { // Есть токены, но недостаточно для редактора - права только на чтение - accessLevel = 'readonly'; + accessLevel = ROLES.READONLY; hasAccess = true; } else { // Нет токенов - обычный пользователь - accessLevel = 'user'; + accessLevel = ROLES.USER; hasAccess = false; } @@ -649,106 +663,17 @@ class AuthService { }; } catch (error) { logger.error(`Error in getUserAccessLevel: ${error.message}`); - return { level: 'user', tokenCount: 0, hasAccess: false }; + return { level: ROLES.USER, tokenCount: 0, hasAccess: false }; } } - // Добавляем псевдоним функции checkAdminRole для обратной совместимости - async checkAdminTokens(address) { - if (!address) return false; - - logger.info(`Checking admin tokens for address: ${address}`); - - try { - // Используем новую функцию для определения уровня доступа - const accessLevel = await this.getUserAccessLevel(address); - const hasAccess = accessLevel.hasAccess; // Любой доступ выше 'user' считается админским - - // Обновляем роль пользователя в базе данных - if (hasAccess) { - try { - // Получаем ключ шифрования - 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); - } - - // Находим userId по адресу - const userResult = await db.getQuery()( - ` - SELECT u.id FROM users u - JOIN user_identities ui ON u.id = ui.user_id - WHERE ui.provider_encrypted = encrypt_text('wallet', $2) AND ui.provider_id_encrypted = encrypt_text($1, $2)`, - [address.toLowerCase(), encryptionKey] - ); - - if (userResult.rows.length > 0) { - const userId = userResult.rows[0].id; - // Обновляем роль пользователя с учетом уровня доступа - const role = accessLevel.level; - await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', [role, userId]); - logger.info(`Updated user ${userId} role to ${role} based on token holdings (${accessLevel.tokenCount} tokens)`); - } - } catch (error) { - logger.error('Error updating user role:', error); - // Продолжаем выполнение, даже если обновление роли не удалось - } - } else { - // Если пользователь не имеет доступа, сбрасываем роль на "user" - try { - // Получаем ключ шифрования - 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); - } - - const userResult = await db.getQuery()( - ` - SELECT u.id, u.role FROM users u - JOIN user_identities ui ON u.id = ui.user_id - WHERE ui.provider_encrypted = encrypt_text('wallet', $2) AND ui.provider_id_encrypted = encrypt_text($1, $2)`, - [address.toLowerCase(), encryptionKey] - ); - - if (userResult.rows.length > 0 && userResult.rows[0].role !== 'user') { - const userId = userResult.rows[0].id; - await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', ['user', userId]); - logger.info(`Reset user ${userId} role to user (no valid tokens found)`); - } - } catch (error) { - logger.error('Error updating user role:', error); - } - } - - return hasAccess; - } catch (error) { - logger.error(`Error in checkAdminTokens: ${error.message}`); - return false; // При любой ошибке считаем, что пользователь не админ - } - } /** - * Перепроверяет админский статус ВСЕХ пользователей с кошельками + * Перепроверяет роли ВСЕХ пользователей с кошельками * @returns {Promise} */ async recheckAllUsersAdminStatus() { - logger.info('Starting recheck of admin status for all users with wallets'); + logger.info('Starting recheck of user roles for all users with wallets'); try { // Получаем ключ шифрования через унифицированную утилиту @@ -963,8 +888,9 @@ class AuthService { // Обновляем роль пользователя в базе данных, если нужно if (userAccessLevel.hasAccess) { - await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', ['admin', userId]); - logger.info(`[AuthService] Updated user ${userId} role to admin based on token holdings`); + const role = userAccessLevel.level; + await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', [role, userId]); + logger.info(`[AuthService] Updated user ${userId} role to ${role} based on token holdings (${userAccessLevel.tokenCount} tokens)`); } } @@ -1011,13 +937,13 @@ class AuthService { userId = session.tempUserId; logger.info(`[handleEmailVerification] Using temporary user ${userId}`); } else { - // Создаем нового пользователя + // Создаем нового пользователя с ролью по умолчанию const newUserResult = await db.getQuery()('INSERT INTO users (role) VALUES ($1) RETURNING id', [ - 'user', + ROLES.USER, ]); userId = newUserResult.rows[0].id; isNewUser = true; - logger.info(`[handleEmailVerification] Created new user ${userId}`); + logger.info(`[handleEmailVerification] Created new user ${userId} with role ${ROLES.USER}`); } } @@ -1039,7 +965,11 @@ class AuthService { if (linkedWallet && linkedWallet.provider_id) { logger.info(`[handleEmailVerification] Found linked wallet ${linkedWallet.provider_id}. Checking role...`); const userAccessLevel = await this.getUserAccessLevel(linkedWallet.provider_id); - userRole = userAccessLevel.hasAccess ? 'admin' : 'user'; + if (userAccessLevel.hasAccess) { + userRole = userAccessLevel.level; + } else { + userRole = ROLES.USER; + } logger.info(`[handleEmailVerification] Role determined as: ${userRole}`); // Опционально: Обновить роль в таблице users @@ -1057,7 +987,7 @@ class AuthService { } } } catch (roleCheckError) { - logger.error(`[handleEmailVerification] Error checking admin role:`, roleCheckError); + logger.error(`[handleEmailVerification] Error checking user role:`, roleCheckError); // В случае ошибки берем текущую роль из базы или оставляем 'user' try { const currentUser = await db.getQuery()('SELECT role FROM users WHERE id = $1', [userId]); @@ -1130,6 +1060,78 @@ class AuthService { return false; } } + + // Периодическая проверка и обновление ролей пользователей + async updateUserRolesPeriodically() { + try { + logger.info('[AuthService] Начинаем периодическую проверку ролей пользователей'); + + const encryptionUtils = require('../utils/encryptionUtils'); + const encryptionKey = encryptionUtils.getEncryptionKey(); + + // Получаем всех пользователей с привязанными кошельками + const usersResult = await db.getQuery()(` + SELECT DISTINCT u.id, u.role, decrypt_text(ui.provider_id_encrypted, $1) as wallet_address + FROM users u + JOIN user_identities ui ON u.id = ui.user_id + WHERE ui.provider_encrypted = encrypt_text('wallet', $1) + `, [encryptionKey]); + + let updatedCount = 0; + + for (const user of usersResult.rows) { + try { + const userAccessLevel = await this.getUserAccessLevel(user.wallet_address); + const expectedRole = userAccessLevel.hasAccess ? userAccessLevel.level : ROLES.USER; + + if (user.role !== expectedRole) { + logger.info(`[AuthService] Обновляем роль пользователя ${user.id} с ${user.role} на ${expectedRole} (токены: ${userAccessLevel.tokenCount})`); + const updateResult = await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2 RETURNING role', [expectedRole, user.id]); + logger.info(`[AuthService] SQL UPDATE выполнен для пользователя ${user.id}, результат:`, updateResult.rows[0]); + logger.info(`[AuthService] Обновлена роль пользователя ${user.id} с ${user.role} на ${expectedRole} (токены: ${userAccessLevel.tokenCount})`); + updatedCount++; + } else { + logger.info(`[AuthService] Роль пользователя ${user.id} не требует обновления: ${user.role} (токены: ${userAccessLevel.tokenCount})`); + } + } catch (error) { + logger.error(`[AuthService] Ошибка при обновлении роли пользователя ${user.id}:`, error); + } + } + + logger.info(`[AuthService] Периодическая проверка завершена. Обновлено ролей: ${updatedCount}`); + + if (updatedCount > 0) { + // Уведомляем frontend об обновлении контактов + broadcastContactsUpdate(); + } + + } catch (error) { + logger.error('[AuthService] Ошибка при периодической проверке ролей:', error); + } + } + + // Запуск периодической проверки ролей + startRoleUpdateInterval() { + if (roleUpdateInterval) { + clearInterval(roleUpdateInterval); + } + + // Проверяем роли каждые 5 минут + roleUpdateInterval = setInterval(() => { + this.updateUserRolesPeriodically(); + }, 5 * 60 * 1000); + + logger.info('[AuthService] Запущена периодическая проверка ролей (каждые 5 минут)'); + } + + // Остановка периодической проверки + stopRoleUpdateInterval() { + if (roleUpdateInterval) { + clearInterval(roleUpdateInterval); + roleUpdateInterval = null; + logger.info('[AuthService] Остановлена периодическая проверка ролей'); + } + } } // Создаем и экспортируем единственный экземпляр diff --git a/backend/services/authTokenService.js b/backend/services/authTokenService.js index 611cc29..7ac953e 100644 --- a/backend/services/authTokenService.js +++ b/backend/services/authTokenService.js @@ -28,8 +28,8 @@ async function saveAllAuthTokens(authTokens) { address: token.address, network: token.network, min_balance: token.minBalance == null ? 0 : Number(token.minBalance), - readonly_threshold: token.readonlyThreshold == null ? 1 : Number(token.readonlyThreshold), - editor_threshold: token.editorThreshold == null ? 2 : Number(token.editorThreshold) + readonly_threshold: token.readonlyThreshold == null ? null : Number(token.readonlyThreshold), + editor_threshold: token.editorThreshold == null ? null : Number(token.editorThreshold) }); } } @@ -40,8 +40,8 @@ async function upsertAuthToken(token) { console.log('[AuthTokenService] token.editorThreshold:', token.editorThreshold, 'тип:', typeof token.editorThreshold); const minBalance = token.minBalance == null ? 0 : Number(token.minBalance); - const readonlyThreshold = (token.readonlyThreshold === null || token.readonlyThreshold === undefined || token.readonlyThreshold === '') ? 1 : Number(token.readonlyThreshold); - const editorThreshold = (token.editorThreshold === null || token.editorThreshold === undefined || token.editorThreshold === '') ? 2 : Number(token.editorThreshold); + const readonlyThreshold = (token.readonlyThreshold === null || token.readonlyThreshold === undefined || token.readonlyThreshold === '') ? null : Number(token.readonlyThreshold); + const editorThreshold = (token.editorThreshold === null || token.editorThreshold === undefined || token.editorThreshold === '') ? null : Number(token.editorThreshold); // Валидация порогов доступа if (readonlyThreshold >= editorThreshold) { diff --git a/backend/services/emailAuth.js b/backend/services/emailAuth.js index 1aa54ce..4066936 100644 --- a/backend/services/emailAuth.js +++ b/backend/services/emailAuth.js @@ -15,7 +15,6 @@ const verificationService = require('./verification-service'); const logger = require('../utils/logger'); const encryptedDb = require('./encryptedDatabaseService'); const authService = require('./auth-service'); -const { checkAdminRole } = require('./admin-role'); const { broadcastContactsUpdate } = require('../wsHub'); const nodemailer = require('nodemailer'); const db = require('../db'); @@ -114,8 +113,9 @@ class EmailAuth { logger.info(`[initEmailAuth] Found existing user ${userId} with email ${email}`); } else { // Создаем временного пользователя, если нужно будет создать нового + const { ROLES } = require('/app/shared/permissions'); const newUser = await encryptedDb.saveData('users', { - role: 'user' + role: ROLES.USER }); userId = newUser.id; session.tempUserId = userId; @@ -243,10 +243,12 @@ class EmailAuth { try { const linkedWallet = await authService.getLinkedWallet(finalUserId); if (linkedWallet) { - logger.info(`[checkEmailVerification] Found linked wallet ${linkedWallet} for user ${finalUserId}. Checking admin role...`); + logger.info(`[checkEmailVerification] Found linked wallet ${linkedWallet} for user ${finalUserId}. Checking user role...`); const authService = require('./auth-service'); const userAccessLevel = await authService.getUserAccessLevel(linkedWallet); - userRole = userAccessLevel.hasAccess ? 'admin' : 'user'; + const { ROLES } = require('/app/shared/permissions'); + // Используем роль из userAccessLevel, которая уже правильно определена с учетом порогов + userRole = userAccessLevel.level; logger.info(`[checkEmailVerification] Role for user ${finalUserId} determined as: ${userRole}`); // Опционально: Обновить роль в таблице users, если она отличается @@ -259,7 +261,7 @@ class EmailAuth { logger.info(`[checkEmailVerification] No linked wallet found for user ${finalUserId}. Role remains 'user'.`); } } catch (roleCheckError) { - logger.error(`[checkEmailVerification] Error checking admin role for user ${finalUserId}:`, roleCheckError); + logger.error(`[checkEmailVerification] Error checking user role for user ${finalUserId}:`, roleCheckError); // В случае ошибки оставляем роль 'user' } // ----> КОНЕЦ: Проверка роли diff --git a/backend/services/identity-service.js b/backend/services/identity-service.js index cb29ca4..61bbe7b 100644 --- a/backend/services/identity-service.js +++ b/backend/services/identity-service.js @@ -16,7 +16,6 @@ const encryptedDb = require('./encryptedDatabaseService'); const db = require('../db'); const logger = require('../utils/logger'); const { getLinkedWallet } = require('./wallet-service'); -const { checkAdminRole } = require('./admin-role'); const { broadcastContactsUpdate } = require('../wsHub'); /** @@ -506,23 +505,26 @@ class IdentityService { let user = await this.findUserByIdentity(provider, providerId); let isNew = false; if (!user) { - // Создаем пользователя + // Создаем пользователя с централизованной ролью + const { ROLES } = require('/app/shared/permissions'); const newUser = await encryptedDb.saveData('users', { - role: 'user' + role: ROLES.USER }); const userId = newUser.id; await this.saveIdentity(userId, provider, providerId, true); - user = { id: userId, role: 'user' }; + user = { id: userId, role: ROLES.USER }; isNew = true; logger.info('[WS] broadcastContactsUpdate after new user created'); broadcastContactsUpdate(); } // Проверяем связь с кошельком const wallet = await getLinkedWallet(user.id); - let role = 'user'; + const { ROLES } = require('/app/shared/permissions'); + let role = ROLES.USER; if (wallet) { const userAccessLevel = await authService.getUserAccessLevel(wallet); - role = userAccessLevel.hasAccess ? 'admin' : 'user'; + // Используем роль из userAccessLevel, которая уже правильно определена с учетом порогов + role = userAccessLevel.level; // Обновляем роль в users, если изменилась if (user.role !== role) { await encryptedDb.saveData('users', { diff --git a/backend/services/tokenBalanceService.js b/backend/services/tokenBalanceService.js index 6f3b012..c320efc 100644 --- a/backend/services/tokenBalanceService.js +++ b/backend/services/tokenBalanceService.js @@ -70,8 +70,8 @@ async function getUserTokenBalances(address) { symbol: token.symbol || '', balance: '0', minBalance: token.min_balance, - readonlyThreshold: token.readonly_threshold || 1, - editorThreshold: token.editor_threshold || 2, + readonlyThreshold: token.readonly_threshold, + editorThreshold: token.editor_threshold, error: 'RPC URL не настроен' }); continue; @@ -133,8 +133,8 @@ async function getUserTokenBalances(address) { symbol: token.symbol || '', balance, minBalance: token.min_balance, - readonlyThreshold: token.readonly_threshold || 1, - editorThreshold: token.editor_threshold || 2, + readonlyThreshold: token.readonly_threshold, + editorThreshold: token.editor_threshold, error: errorType, errorDetails: errorMessage }); @@ -149,8 +149,8 @@ async function getUserTokenBalances(address) { symbol: token.symbol || '', balance, minBalance: token.min_balance, - readonlyThreshold: token.readonly_threshold || 1, - editorThreshold: token.editor_threshold || 2, + readonlyThreshold: token.readonly_threshold, + editorThreshold: token.editor_threshold, }); } diff --git a/backend/services/unifiedMessageProcessor.js b/backend/services/unifiedMessageProcessor.js index 887c468..85861be 100644 --- a/backend/services/unifiedMessageProcessor.js +++ b/backend/services/unifiedMessageProcessor.js @@ -94,7 +94,7 @@ async function processMessage(messageData) { // 4. Определяем нужно ли генерировать AI ответ const shouldGenerateAi = adminLogicService.shouldGenerateAiReply({ - senderType: isAdmin ? 'admin' : 'user', + senderType: isAdmin ? 'editor' : 'user', userId: userId, recipientId: recipientId || userId, channel: channel @@ -161,7 +161,7 @@ async function processMessage(messageData) { [ userId, conversationId, - isAdmin ? 'admin' : 'user', + isAdmin ? 'editor' : 'user', content, channel, 'user', diff --git a/frontend/src/components/BaseLayout.vue b/frontend/src/components/BaseLayout.vue index b0743e4..03379af 100644 --- a/frontend/src/components/BaseLayout.vue +++ b/frontend/src/components/BaseLayout.vue @@ -217,11 +217,15 @@ const handleWalletAuth = async () => { const disconnectWallet = async () => { // console.log('[BaseLayout] Выполняется выход из системы...'); try { - await api.post('/auth/logout'); - showSuccessMessage('Вы успешно вышли из системы'); - removeFromStorage('guestMessages'); - removeFromStorage('hasUserSentMessage'); - emit('auth-action-completed'); + // Используем централизованную функцию disconnect из useAuth + const result = await auth.disconnect(); + + if (result.success) { + showSuccessMessage('Вы успешно вышли из системы'); + emit('auth-action-completed'); + } else { + showErrorMessage(result.error || 'Произошла ошибка при выходе из системы'); + } } catch (error) { // console.error('[BaseLayout] Ошибка при выходе из системы:', error); showErrorMessage('Произошла ошибка при выходе из системы'); diff --git a/frontend/src/components/ContactTable.vue b/frontend/src/components/ContactTable.vue index b40ff1d..7d91e8e 100644 --- a/frontend/src/components/ContactTable.vue +++ b/frontend/src/components/ContactTable.vue @@ -13,9 +13,10 @@ @@ -150,137 +94,4 @@ function goBack() { margin-left: 7px; } -/* Стили для таблицы-заглушки */ -.contact-table-placeholder { - background: #fff; - border-radius: 16px; - box-shadow: 0 4px 32px rgba(0,0,0,0.12); - padding: 32px 24px 24px 24px; - width: 100%; - margin-top: 40px; - position: relative; - overflow-x: auto; -} - -.contact-table-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 24px; -} - -.contact-table-header h2 { - margin: 0; - color: #333; -} - -.close-btn { - position: absolute; - top: 18px; - right: 18px; - background: none; - border: none; - font-size: 2rem; - cursor: pointer; - color: #bbb; - transition: color 0.2s; -} - -.close-btn:hover { - color: #333; -} - -.filters-form-placeholder { - margin-bottom: 24px; - padding: 16px; - background: #f8f9fa; - border-radius: 8px; -} - -.form-row { - display: flex; - flex-wrap: wrap; - gap: 16px; - align-items: end; -} - -.form-item { - display: flex; - flex-direction: column; - min-width: 150px; -} - -.form-item label { - font-size: 0.9rem; - color: #666; - margin-bottom: 4px; -} - -.form-item input, -.form-item select { - padding: 8px 12px; - border: 1px solid #dee2e6; - border-radius: 4px; - font-size: 0.9rem; - background: #f8f9fa; - color: #6c757d; - cursor: not-allowed; -} - -.btn-disabled { - padding: 8px 16px; - border: 1px solid #dee2e6; - border-radius: 4px; - background: #f8f9fa; - color: #6c757d; - cursor: not-allowed; - height: fit-content; -} - -.contact-table-masked { - width: 100%; - border-collapse: collapse; - font-size: 0.9rem; -} - -.contact-table-masked th, -.contact-table-masked td { - padding: 12px 8px; - text-align: left; - border-bottom: 1px solid #e9ecef; -} - -.contact-table-masked th { - background: #f8f9fa; - font-weight: 600; - color: #495057; -} - -.contact-table-masked td { - color: #adb5bd; - font-family: monospace; -} - -.details-btn-disabled { - padding: 6px 12px; - border: 1px solid #dee2e6; - border-radius: 4px; - background: #f8f9fa; - color: #6c757d; - font-size: 0.8rem; - cursor: not-allowed; -} - -.access-notice { - margin-top: 20px; - padding: 12px 16px; - background: #e3f2fd; - border: 1px solid #bbdefb; - border-radius: 8px; - color: #1976d2; - display: flex; - align-items: center; - gap: 8px; - font-size: 0.9rem; -} \ No newline at end of file diff --git a/frontend/src/views/CrmView.vue b/frontend/src/views/CrmView.vue index b2f6731..da758c7 100644 --- a/frontend/src/views/CrmView.vue +++ b/frontend/src/views/CrmView.vue @@ -53,6 +53,13 @@ Подробнее + +
+

Акселератор

+ +
@@ -242,6 +249,10 @@ function goToManagement() { function goToWeb3App() { router.push({ name: 'vds-mock' }); } + +function goToAcceleratorRegistration() { + router.push({ name: 'accelerator-registration' }); +} \ No newline at end of file diff --git a/frontend/src/views/PersonalMessagesView.vue b/frontend/src/views/PersonalMessagesView.vue index ec3c75e..ade6368 100644 --- a/frontend/src/views/PersonalMessagesView.vue +++ b/frontend/src/views/PersonalMessagesView.vue @@ -51,6 +51,7 @@ import { useRouter, useRoute } from 'vue-router'; import BaseLayout from '../components/BaseLayout.vue'; import adminChatService from '../services/adminChatService.js'; import { usePermissions } from '@/composables/usePermissions'; +import { getPrivateMessages } from '../services/messagesService'; const router = useRouter(); const route = useRoute(); @@ -65,15 +66,41 @@ let ws = null; async function fetchPersonalMessages() { try { isLoading.value = true; - console.log('[PersonalMessagesView] Загружаем личные сообщения...'); - const response = await adminChatService.getAdminContacts(); - console.log('[PersonalMessagesView] API ответ:', response); - personalMessages.value = response?.contacts || []; - console.log('[PersonalMessagesView] Загружено бесед:', personalMessages.value.length); - console.log('[PersonalMessagesView] Беседы:', personalMessages.value); - newMessagesCount.value = personalMessages.value.length; // Simplified for now + console.log('[PersonalMessagesView] Загружаем приватные сообщения...'); + + // Загружаем приватные сообщения через новый API с пагинацией + const response = await getPrivateMessages({ limit: 100, offset: 0 }); + console.log('[PersonalMessagesView] Загружено приватных сообщений:', response.messages?.length || 0); + + const privateMessages = response.success && response.messages ? response.messages : []; + + // Группируем сообщения по отправителям для отображения списка бесед + const messageGroups = {}; + privateMessages.forEach(msg => { + const senderId = msg.sender_id || 'unknown'; + if (!messageGroups[senderId]) { + messageGroups[senderId] = { + id: senderId, + name: `Админ ${senderId}`, + last_message: msg.content, + last_message_at: msg.created_at, + messages: [] + }; + } + messageGroups[senderId].messages.push(msg); + // Обновляем последнее сообщение + if (new Date(msg.created_at) > new Date(messageGroups[senderId].last_message_at)) { + messageGroups[senderId].last_message = msg.content; + messageGroups[senderId].last_message_at = msg.created_at; + } + }); + + personalMessages.value = Object.values(messageGroups); + newMessagesCount.value = personalMessages.value.length; + + console.log('[PersonalMessagesView] Сформировано бесед:', personalMessages.value.length); } catch (error) { - console.error('[PersonalMessagesView] Ошибка загрузки личных сообщений:', error); + console.error('[PersonalMessagesView] Ошибка загрузки приватных сообщений:', error); personalMessages.value = []; } finally { isLoading.value = false; diff --git a/frontend/src/views/accelerator/AcceleratorRegistrationView.vue b/frontend/src/views/accelerator/AcceleratorRegistrationView.vue new file mode 100644 index 0000000..15a40c9 --- /dev/null +++ b/frontend/src/views/accelerator/AcceleratorRegistrationView.vue @@ -0,0 +1,368 @@ + + + + + + + diff --git a/frontend/src/views/contacts/ContactDetailsView.vue b/frontend/src/views/contacts/ContactDetailsView.vue index 4c85016..d692656 100644 --- a/frontend/src/views/contacts/ContactDetailsView.vue +++ b/frontend/src/views/contacts/ContactDetailsView.vue @@ -160,6 +160,7 @@ import Message from '../../components/Message.vue'; import ChatInterface from '../../components/ChatInterface.vue'; import contactsService from '../../services/contactsService.js'; import messagesService from '../../services/messagesService.js'; +import { getPublicMessages, getConversationByUserId } from '../../services/messagesService.js'; import { useAuthContext } from '@/composables/useAuth'; import { usePermissions } from '@/composables/usePermissions'; import { useContactsAndMessagesWebSocket } from '@/composables/useContactsWebSocket'; @@ -401,11 +402,15 @@ async function loadMessages() { console.log('[ContactDetailsView] 📥 loadMessages START for:', contact.value.id); isLoadingMessages.value = true; try { - // Загружаем ВСЕ публичные сообщения этого пользователя (как на главной странице) - const loadedMessages = await messagesService.getMessagesByUserId(contact.value.id); - console.log('[ContactDetailsView] 📩 Loaded messages:', loadedMessages.length, 'for', contact.value.id); + // Загружаем только публичные сообщения этого пользователя с пагинацией + const response = await getPublicMessages(contact.value.id, { limit: 50, offset: 0 }); + console.log('[ContactDetailsView] 📩 Loaded messages:', response.messages?.length || 0, 'for', contact.value.id); - messages.value = loadedMessages; + if (response.success && response.messages) { + messages.value = response.messages; + } else { + messages.value = []; + } if (messages.value.length > 0) { lastMessageDate.value = messages.value[messages.value.length - 1].created_at; @@ -417,7 +422,7 @@ async function loadMessages() { // Гости не имеют conversations if (!String(contact.value.id).startsWith('guest_')) { try { - const conv = await messagesService.getConversationByUserId(contact.value.id); + const conv = await getConversationByUserId(contact.value.id); conversationId.value = conv?.id || null; } catch (convError) { console.warn('[ContactDetailsView] Не удалось загрузить conversationId:', convError.message); diff --git a/shared/permissions.js b/shared/permissions.js index ef9de53..0277c0f 100644 --- a/shared/permissions.js +++ b/shared/permissions.js @@ -76,6 +76,8 @@ const PERMISSIONS_MAP = { PERMISSIONS.VIEW_HOME, PERMISSIONS.CHAT_AI, PERMISSIONS.RECEIVE_MESSAGES, + PERMISSIONS.VIEW_CONTACTS, // Пользователи могут видеть контакты для выбора + PERMISSIONS.SEND_TO_USERS, // Пользователи могут отправлять сообщения PERMISSIONS.CHAT_WITH_ADMINS // Авторизованные пользователи могут видеть личные сообщения ],