From 9d2a829cd654fa9bdfaad2cc0cd5d63212f95409 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 20 Mar 2025 10:16:45 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/middleware/auth.js | 46 ++- backend/routes/auth.js | 34 +- backend/services/auth-service.js | 534 +++++++------------------------ frontend/src/views/HomeView.vue | 1 + 4 files changed, 136 insertions(+), 479 deletions(-) diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js index 2b35c72..2cff23a 100644 --- a/backend/middleware/auth.js +++ b/backend/middleware/auth.js @@ -3,6 +3,7 @@ const authService = require('../services/auth-service'); const logger = require('../utils/logger'); const { USER_ROLES } = require('../utils/constants'); const db = require('../db'); +const { checkAdminTokens } = require('../services/auth-service'); /** * Middleware для проверки аутентификации @@ -164,32 +165,27 @@ function requireRole(role) { * Проверяет роль пользователя * @param {string} role - Роль для проверки */ -function checkRole(role) { - return async (req, res, next) => { - try { - // Если пользователь не аутентифицирован, просто продолжаем - if (!req.session || !req.session.authenticated) { - req.hasRole = false; - return next(); - } - - // Проверка через ID пользователя - if (req.session.userId) { - const userResult = await db.query('SELECT role FROM users WHERE id = $1', [req.session.userId]); - if (userResult.rows.length > 0 && userResult.rows[0].role === role) { - req.hasRole = true; - return next(); - } - } - - req.hasRole = false; - next(); - } catch (error) { - logger.error(`Error in checkRole middleware: ${error.message}`); - req.hasRole = false; - next(); +async function checkRole(req, res, next) { + try { + if (!req.session.authenticated) { + return res.status(401).json({ error: 'Unauthorized' }); } - }; + + // Если есть адрес кошелька - проверяем токены + if (req.session.address) { + req.session.isAdmin = await checkAdminTokens(req.session.address); + await req.session.save(); + } + + if (!req.session.isAdmin) { + return res.status(403).json({ error: 'Access denied' }); + } + + next(); + } catch (error) { + console.error('Error in checkRole middleware:', error); + res.status(500).json({ error: 'Internal server error' }); + } } module.exports = { diff --git a/backend/routes/auth.js b/backend/routes/auth.js index a80cb25..f0f7f9f 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -12,6 +12,7 @@ const { SiweMessage } = require('siwe'); const { sendEmail } = require('../services/emailBot'); const { verificationCodes } = require('../services/telegramBot'); const { checkTokensAndUpdateRole } = require('../services/auth-service'); +const { ethers } = require('ethers'); // Создайте лимитер для попыток аутентификации const authLimiter = rateLimit({ @@ -54,33 +55,10 @@ router.get('/nonce', async (req, res) => { } }); -// Функция для проверки роли пользователя -async function checkUserRole(address, req) { - try { - const lowerCaseAddress = address.toLowerCase(); - - // Проверяем наличие токена доступа в базе данных - const result = await db.query( - 'SELECT role FROM access_tokens WHERE LOWER(wallet_address) = $1 AND expires_at > NOW()', - [lowerCaseAddress] - ); - - if (result.rows.length > 0) { - // Если есть активный токен, проверяем роль - const role = result.rows[0].role; - return role === 'ADMIN'; - } - - // Если нет токена, проверяем адрес администратора из переменных окружения - const adminAddresses = (process.env.ADMIN_ADDRESSES || '') - .split(',') - .map((a) => a.toLowerCase()); - return adminAddresses.includes(lowerCaseAddress); - } catch (error) { - console.error('Ошибка при проверке роли пользователя:', error); - return false; - } -} +// Минимальный ABI для проверки баланса ERC20 +const ERC20_ABI = [ + "function balanceOf(address owner) view returns (uint256)" +]; // Проверка подписи и аутентификация router.post('/verify', async (req, res) => { @@ -132,7 +110,7 @@ router.post('/verify', async (req, res) => { ); const userId = userResult.rows[0].id; - const isAdmin = userResult.rows[0].is_admin; + const isAdmin = false; // Будет обновлено в createSession // Используем централизованный сервис await authService.createSession(req, { diff --git a/backend/services/auth-service.js b/backend/services/auth-service.js index 7e62daf..1525a19 100644 --- a/backend/services/auth-service.js +++ b/backend/services/auth-service.js @@ -4,107 +4,35 @@ const { ethers } = require('ethers'); const crypto = require('crypto'); const { processMessage } = require('./ai-assistant'); // Используем AI Assistant +const ADMIN_CONTRACTS = [ + { address: "0xd95a45fc46a7300e6022885afec3d618d7d3f27c", network: "eth" }, + { address: "0x1d47f12ffA279BFE59Ab16d56fBb10d89AECdD5D", network: "bsc" }, + { address: "0xdce769b847a0a697239777d0b1c7dd33b6012ba0", network: "arbitrum" }, + { address: "0x351f59de4fedbdf7601f5592b93db3b9330c1c1d", network: "polygon" } +]; + +const ERC20_ABI = [ + "function balanceOf(address owner) view returns (uint256)" +]; + class AuthService { constructor() { - // Инициализация провайдеров для разных сетей this.providers = { eth: new ethers.JsonRpcProvider(process.env.RPC_URL_ETH), polygon: new ethers.JsonRpcProvider(process.env.RPC_URL_POLYGON), bsc: new ethers.JsonRpcProvider(process.env.RPC_URL_BSC), arbitrum: new ethers.JsonRpcProvider(process.env.RPC_URL_ARBITRUM) }; - - // Конфигурация токенов для разных сетей - this.tokenContracts = [ - { address: "0xd95a45fc46a7300e6022885afec3d618d7d3f27c", network: "eth" }, // Ethereum - { address: "0x1d47f12ffA279BFE59Ab16d56fBb10d89AECdD5D", network: "bsc" }, // Binance Smart Chain - { address: "0xdce769b847a0a697239777d0b1c7dd33b6012ba0", network: "arbitrum" }, // Arbitrum - { address: "0x351f59de4fedbdf7601f5592b93db3b9330c1c1d", network: "polygon" } // Polygon - ]; - - this.MIN_BALANCE = ethers.parseUnits("1000000.0", 18); // 1,000,000 токенов для роли админа } // Проверка подписи async verifySignature(message, signature, address) { try { - logger.info('Verifying signature:', { - message: message.substring(0, 100) + '...', - signature: signature.substring(0, 10) + '...', - address - }); - - if (!message || !signature || !address) { - logger.error('Missing parameters for signature verification'); - return false; - } - - try { - // Восстанавливаем адрес из подписи через ethers - const recoveredAddress = ethers.verifyMessage(message, signature); - return ethers.getAddress(recoveredAddress) === ethers.getAddress(address); - } catch (error) { - logger.error('Error in signature verification:', error); - return false; - } + if (!message || !signature || !address) return false; + const recoveredAddress = ethers.verifyMessage(message, signature); + return ethers.getAddress(recoveredAddress) === ethers.getAddress(address); } catch (error) { - logger.error('Error in verifySignature:', error); - return false; - } - } - - /** - * Проверяет наличие токенов на кошельке - * @param {string} walletAddress - Адрес кошелька - * @returns {Promise} - Имеет ли кошелек токены - */ - async checkAdminTokens(walletAddress) { - try { - for (const contract of this.tokenContracts) { - try { - const provider = this.providers[contract.network]; - const tokenContract = new ethers.Contract( - contract.address, - ['function balanceOf(address) view returns (uint256)'], - provider - ); - - const balance = await tokenContract.balanceOf(walletAddress); - logger.info(`Balance for ${walletAddress} on ${contract.network}: ${balance}`); - - if (balance >= this.MIN_BALANCE) { - logger.info(`Admin token found on ${contract.network} for ${walletAddress}`); - return true; - } - } catch (error) { - logger.error(`Error checking balance on ${contract.network}:`, error); - } - } - - logger.info(`No admin tokens found for ${walletAddress}`); - return false; - } catch (error) { - logger.error('Error in checkAdminTokens:', error); - return false; - } - } - - /** - * Проверяет баланс токенов и обновляет роль пользователя - * @param {string} address - Адрес кошелька - * @returns {Promise} - Является ли пользователь админом - */ - async checkTokensAndUpdateRole(address) { - try { - const isAdmin = await this.checkAdminTokens(address); - - // Обновляем роль в базе данных - await this.updateUserRole(address, isAdmin); - - logger.info(`Updated role for user with address ${address}: admin=${isAdmin}`); - return isAdmin; - } catch (error) { - logger.error('Error in checkTokensAndUpdateRole:', error); + logger.error('Error in signature verification:', error); return false; } } @@ -117,27 +45,27 @@ class AuthService { async findOrCreateUser(address) { try { const existingUser = await db.query( - `SELECT u.id, - (u.role = 'admin') as is_admin - FROM users u - JOIN user_identities ui ON u.id = ui.user_id - WHERE ui.provider = 'wallet' - AND LOWER(ui.provider_id) = LOWER($1)`, + `SELECT u.id + FROM users u + JOIN user_identities ui ON u.id = ui.user_id + WHERE ui.provider = 'wallet' + AND LOWER(ui.provider_id) = LOWER($1)`, [address] ); if (existingUser.rows.length > 0) { - return existingUser.rows[0]; + const userId = existingUser.rows[0].id; + const isAdmin = await this.checkAdminRole(address); + return { userId, isAdmin }; } - // Если пользователь не найден, создаем нового + // Создание нового пользователя const result = await db.query( 'INSERT INTO users DEFAULT VALUES RETURNING id', [] ); const userId = result.rows[0].id; - // Добавляем wallet identity await db.query( `INSERT INTO user_identities (user_id, provider, provider_id, identity_type, identity_value) @@ -145,13 +73,8 @@ class AuthService { [userId, address.toLowerCase()] ); - // Проверяем роль админа - const isAdmin = await this.checkAdminRole(userId); - - return { - userId, - isAdmin - }; + const isAdmin = await this.checkAdminRole(address); + return { userId, isAdmin }; } catch (error) { console.error('Error in findOrCreateUser:', error); throw error; @@ -159,342 +82,101 @@ class AuthService { } /** - * Обновляет роль пользователя и связанные данные + * Основной метод проверки роли админа + * @param {string} address - Адрес кошелька + * @returns {Promise} - Является ли пользователь админом */ - async updateUserRole(address, isAdmin) { - try { - const result = await db.query(` - UPDATE users u - SET - role = $2::user_role - FROM user_identities ui - WHERE u.id = ui.user_id - AND ui.provider = 'wallet' - AND LOWER(ui.provider_id) = LOWER($1) - RETURNING u.id - `, [ - address, - isAdmin ? 'admin' : 'user' - ]); - - if (result.rows.length > 0) { - logger.info(`Updated role for user ${result.rows[0].id} to ${isAdmin ? 'admin' : 'user'}`); - } - } catch (error) { - logger.error('Error updating user role:', error); - } - } - - // Связывание идентификаторов (из identity-linker.js) - async linkIdentity(userId, type, value) { - try { - // Проверяем, не связан ли идентификатор с другим пользователем - const existingResult = await db.query( - `SELECT user_id - FROM user_identities - WHERE identity_type = $1 - AND LOWER(identity_value) = LOWER($2)`, - [type, value] - ); - - if (existingResult.rows.length > 0 && existingResult.rows[0].user_id !== userId) { - throw new Error('Identity already linked to another user'); - } - - // Добавляем или обновляем идентификатор - await db.query( - `INSERT INTO user_identities - (user_id, identity_type, identity_value, verified, created_at) - VALUES ($1, $2, $3, true, NOW()) - ON CONFLICT (identity_type, identity_value) - DO UPDATE SET user_id = $1, verified = true`, - [userId, type, value] - ); - - // Если это кошелек, проверяем права админа - if (type === 'wallet') { - await this.checkTokensAndUpdateRole(value); - } - - return true; - } catch (error) { - logger.error('Error linking identity:', error); - throw error; - } - } - - // Получение всех идентификаторов пользователя - async getUserIdentities(userId) { - try { - const result = await db.query( - `SELECT identity_type, identity_value, verified, created_at - FROM user_identities - WHERE user_id = $1`, - [userId] - ); - return result.rows; - } catch (error) { - logger.error('Error getting user identities:', error); - return []; - } - } - - // Проверка роли админа - async isAdmin(userId) { - try { - const result = await db.query( - 'SELECT is_admin FROM users WHERE id = $1', - [userId] - ); - return result.rows.length > 0 && result.rows[0].is_admin; - } catch (error) { - logger.error('Error checking admin status:', error); - return false; - } - } - - /** - * Получает все токены доступа - * @returns {Promise} - Список токенов доступа - */ - async getAllTokens() { - try { - const result = await db.query(` - SELECT * FROM access_tokens - ORDER BY created_at DESC - `); - - return result.rows.map(token => ({ - id: token.id, - walletAddress: token.wallet_address, - role: token.role, - createdAt: token.created_at, - expiresAt: token.expires_at, - })); - } catch (error) { - logger.error(`Error getting all tokens: ${error.message}`); - return []; - } - } - - /** - * Получает ID пользователя по идентификатору - * @param {string} identityType - Тип идентификатора ('wallet', 'email', 'telegram') - * @param {string} identityValue - Значение идентификатора - * @returns {Promise} - ID пользователя или null, если пользователь не найден - */ - async getUserIdByIdentity(identityType, identityValue) { - try { - // Нормализуем значение идентификатора - const normalizedValue = identityType === 'wallet' - ? identityValue.toLowerCase() - : identityValue; - - // Получаем ID пользователя - const result = await db.query(` - SELECT u.id FROM users u - JOIN user_identities ui ON u.id = ui.user_id - WHERE ui.identity_type = $1 AND LOWER(ui.identity_value) = LOWER($2) - `, [identityType, normalizedValue]); - - if (result.rows.length === 0) { - return null; - } - - return result.rows[0].id; - } catch (error) { - logger.error(`Ошибка при получении ID пользователя по идентификатору: ${error.message}`); - return null; - } - } - - /** - * Обрабатывает гостевые сообщения после аутентификации - */ - async processGuestMessages(userId, guestId) { - try { - logger.info(`Processing guest messages for user ${userId} with guestId ${guestId}`); - - // Сначала обновляем user_id для всех бесед с гостевыми сообщениями - await db.query( - `UPDATE conversations c - SET user_id = $1 - WHERE id IN ( - SELECT DISTINCT conversation_id - FROM messages m - WHERE m.metadata->>'guest_id' = $2 - )`, - [userId, guestId] - ); - - // Получаем все гостевые сообщения без ответов - const guestMessages = await db.query( - `SELECT m.id, m.content, m.conversation_id, m.metadata, m.created_at - FROM messages m - WHERE m.metadata->>'guest_id' = $1 - AND NOT EXISTS ( - SELECT 1 FROM messages - WHERE conversation_id = m.conversation_id - AND sender_type = 'assistant' - ) - ORDER BY m.created_at ASC`, - [guestId] - ); - - logger.info(`Found ${guestMessages.rows.length} unprocessed guest messages`); - - // Обрабатываем каждое гостевое сообщение - for (const msg of guestMessages.rows) { - logger.info(`Processing guest message ${msg.id}: ${msg.content}`); + async checkAdminRole(address) { + if (!address) return false; + + logger.info(`Checking admin role for address: ${address}`); + let foundTokens = false; + const balances = {}; + + for (const contract of ADMIN_CONTRACTS) { + try { + const provider = this.providers[contract.network]; + if (!provider) continue; - // Получаем язык из метаданных - const metadata = typeof msg.metadata === 'string' ? JSON.parse(msg.metadata) : msg.metadata; - const language = metadata?.language || 'ru'; - - // Используем AI Assistant для обработки сообщения - const aiResponse = await processMessage(userId, msg.content, language); - - // Сохраняем ответ AI в ту же беседу - await db.query( - `INSERT INTO messages - (conversation_id, sender_type, content, channel, created_at) - VALUES ($1, 'assistant', $2, 'chat', NOW())`, - [msg.conversation_id, aiResponse] + const tokenContract = new ethers.Contract( + contract.address, + ERC20_ABI, + provider ); + + const balance = await tokenContract.balanceOf(address); + const formattedBalance = ethers.formatUnits(balance, 18); + balances[contract.network] = formattedBalance; + + logger.info(`Token balance on ${contract.network}:`, { + address, + contract: contract.address, + balance: formattedBalance, + hasTokens: balance > 0 + }); - logger.info(`Saved AI response for message ${msg.id}`); + if (balance > 0) { + logger.info(`Found admin tokens on ${contract.network}`); + foundTokens = true; + } + } catch (error) { + logger.error(`Error checking balance in ${contract.network}:`, { + address, + contract: contract.address, + error: error.message + }); + balances[contract.network] = 'Error'; } - - // Обновляем метаданные сообщений, чтобы показать, что они обработаны - await db.query( - `UPDATE messages m - SET metadata = jsonb_set( - CASE - WHEN m.metadata IS NULL THEN '{}'::jsonb - ELSE m.metadata::jsonb - END, - '{processed}', - 'true' - ) - WHERE m.metadata->>'guest_id' = $1`, - [guestId] - ); - - logger.info(`Successfully processed all guest messages for user ${userId}`); - return true; - } catch (error) { - logger.error('Error processing guest messages:', error); - return false; } - } - - async disconnect() { - try { - // Очищаем состояние аутентификации - this.isAuthenticated = false; - this.userId = null; - this.address = null; - this.isAdmin = false; - this.authType = null; - - // Очищаем сессию - localStorage.removeItem('auth'); - - // Очищаем guestId - localStorage.removeItem('guestId'); - - return true; - } catch (error) { - logger.error('Error during disconnect:', error); - return false; - } - } - - async createSession(req, userData) { - // Сохраняем существующие данные сессии - const existingData = { ...req.session }; - req.session.userId = userData.userId; - req.session.address = userData.address; - req.session.isAdmin = userData.isAdmin; + if (foundTokens) { + logger.info(`Admin role summary for ${address}:`, { + networks: Object.keys(balances).filter(net => balances[net] > 0), + balances + }); + logger.info(`Admin role granted for ${address}`); + return true; + } + + logger.info(`Admin role denied - no tokens found for ${address}`); + return false; + } + + // Создание сессии с проверкой роли + async createSession(req, { userId, address, authType, guestId }) { + let isAdmin = false; + + if (address) { + isAdmin = await this.checkAdminRole(address); + } else if (userId) { + const linkedWallet = await this.getLinkedWallet(userId); + if (linkedWallet) { + isAdmin = await this.checkAdminRole(linkedWallet); + } + } + + req.session.userId = userId; + req.session.address = address; + req.session.isAdmin = isAdmin; req.session.authenticated = true; - req.session.authType = userData.authType; + req.session.authType = authType; - // Если есть гостевые сообщения в существующей сессии - if (existingData.guestId) { - req.session.guestId = existingData.guestId; - // Связываем сообщения сразу здесь - await this.linkGuestMessages(req, { - userId: userData.userId, - guestId: existingData.guestId - }); + if (guestId) { + req.session.guestId = guestId; } - - return new Promise((resolve, reject) => { - req.session.save((err) => { - if (err) reject(err); - else resolve(); - }); - }); + + await req.session.save(); } - async linkGuestMessages(req, userData) { - if (!userData.guestId) return; - - const { rows } = await db.query( - 'SELECT EXISTS(SELECT 1 FROM guest_messages WHERE guest_id = $1)', - [userData.guestId] + // Получение связанного кошелька + async getLinkedWallet(userId) { + const result = await db.query( + `SELECT identity_value as address + FROM user_identities + WHERE user_id = $1 AND identity_type = 'wallet'`, + [userId] ); - - if (rows[0].exists) { - // Сначала связываем сообщения - await db.query('SELECT link_guest_messages($1, $2)', - [userData.userId, userData.guestId] - ); - // Только после успешного связывания удаляем guestId - delete req.session.guestId; - } - } - - async checkAdminRole(userId) { - try { - // Получаем все идентификаторы пользователя - const identities = await db.query( - `SELECT provider, provider_id - FROM user_identities - WHERE user_id = $1`, - [userId] - ); - - // Ищем wallet среди идентификаторов - const wallet = identities.rows.find(i => i.provider === 'wallet'); - if (!wallet) return false; - - // Проверяем баланс токенов - const hasTokens = await this.checkAdminTokens(wallet.provider_id); - if (!hasTokens) return false; - - // Обновляем роль пользователя - await db.query( - `UPDATE users SET role = 'admin' WHERE id = $1`, - [userId] - ); - - return true; - } catch (error) { - console.error('Error checking admin role:', error); - return false; - } - } - - // Проверка при каждой аутентификации - async verifyIdentity(type, value) { - const userId = await this.getUserIdByIdentity(type, value); - if (!userId) return false; - - // Проверяем роль только если есть связанный кошелек - await this.checkAdminRole(userId); - return true; + return result.rows[0]?.address; } } diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index 1829357..b585150 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -5,6 +5,7 @@

Венчурный фонд и поставщик программного обеспечения

+