diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 0de2a1e..43a7078 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -934,12 +934,12 @@ router.get('/check-tokens/:address', async (req, res) => { try { const { address } = req.params; - // Получаем балансы токенов на всех сетях - const balances = await authService.getTokenBalances(address); + // Получаем балансы токенов с минимальными балансами + const balances = await authService.getUserTokenBalances(address); res.json({ success: true, - balances, + data: balances, }); } catch (error) { logger.error('Error checking token balances:', error); diff --git a/backend/routes/blockchain.js b/backend/routes/blockchain.js index fd9e11e..77db039 100644 --- a/backend/routes/blockchain.js +++ b/backend/routes/blockchain.js @@ -108,4 +108,300 @@ router.post('/read-dle-info', async (req, res) => { } }); +// Получение поддерживаемых сетей из смарт-контракта +router.post('/get-supported-chains', async (req, res) => { + try { + const { dleAddress } = req.body; + + if (!dleAddress) { + return res.status(400).json({ + success: false, + error: 'Адрес DLE обязателен' + }); + } + + console.log(`[Blockchain] Получение поддерживаемых сетей для DLE: ${dleAddress}`); + + // Получаем RPC URL для Sepolia + const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: 'RPC URL для Sepolia не найден' + }); + } + + const provider = new ethers.JsonRpcProvider(rpcUrl); + + // ABI для проверки поддерживаемых сетей + const dleAbi = [ + "function isChainSupported(uint256 _chainId) external view returns (bool)", + "function getCurrentChainId() external view returns (uint256)" + ]; + + const dle = new ethers.Contract(dleAddress, dleAbi, provider); + + // Список всех возможных сетей для проверки + const allChains = [ + { chainId: 1, name: 'Ethereum', description: 'Основная сеть Ethereum' }, + { chainId: 137, name: 'Polygon', description: 'Сеть Polygon' }, + { chainId: 56, name: 'BSC', description: 'Binance Smart Chain' }, + { chainId: 42161, name: 'Arbitrum', description: 'Arbitrum One' }, + { chainId: 10, name: 'Optimism', description: 'Optimism' }, + { chainId: 8453, name: 'Base', description: 'Base' }, + { chainId: 43114, name: 'Avalanche', description: 'Avalanche C-Chain' }, + { chainId: 250, name: 'Fantom', description: 'Fantom Opera' }, + { chainId: 11155111, name: 'Sepolia', description: 'Ethereum Testnet Sepolia' }, + { chainId: 80001, name: 'Mumbai', description: 'Polygon Testnet Mumbai' }, + { chainId: 97, name: 'BSC Testnet', description: 'Binance Smart Chain Testnet' }, + { chainId: 421613, name: 'Arbitrum Goerli', description: 'Arbitrum Testnet Goerli' } + ]; + + const supportedChains = []; + + // Проверяем каждую сеть через смарт-контракт + for (const chain of allChains) { + try { + const isSupported = await dle.isChainSupported(chain.chainId); + if (isSupported) { + supportedChains.push(chain); + } + } catch (error) { + console.log(`[Blockchain] Ошибка при проверке сети ${chain.chainId}:`, error.message); + // Продолжаем проверку других сетей + } + } + + console.log(`[Blockchain] Найдено поддерживаемых сетей: ${supportedChains.length}`); + + res.json({ + success: true, + data: { + chains: supportedChains, + totalCount: supportedChains.length + } + }); + + } catch (error) { + console.error('[Blockchain] Ошибка при получении поддерживаемых сетей:', error); + res.status(500).json({ + success: false, + error: 'Ошибка при получении поддерживаемых сетей: ' + error.message + }); + } +}); + +// Получение списка всех предложений +router.post('/get-proposals', async (req, res) => { + try { + const { dleAddress } = req.body; + + if (!dleAddress) { + return res.status(400).json({ + success: false, + error: 'Адрес DLE обязателен' + }); + } + + console.log(`[Blockchain] Получение списка предложений для DLE: ${dleAddress}`); + + // Получаем RPC URL для Sepolia + const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: 'RPC URL для Sepolia не найден' + }); + } + + const provider = new ethers.JsonRpcProvider(rpcUrl); + + // ABI для чтения предложений (только читаемые поля) + const dleAbi = [ + "function proposals(uint256) external view returns (uint256 id, string description, uint256 forVotes, uint256 againstVotes, bool executed, uint256 deadline, address initiator, bytes operation)", + "function checkProposalResult(uint256 _proposalId) external view returns (bool)", + "event ProposalCreated(uint256 proposalId, address initiator, string description)" + ]; + + const dle = new ethers.Contract(dleAddress, dleAbi, provider); + + // Получаем события ProposalCreated для определения количества предложений + const currentBlock = await provider.getBlockNumber(); + const fromBlock = Math.max(0, currentBlock - 10000); // Последние 10000 блоков + + const events = await dle.queryFilter('ProposalCreated', fromBlock, currentBlock); + + console.log(`[Blockchain] Найдено событий ProposalCreated: ${events.length}`); + console.log(`[Blockchain] Диапазон блоков: ${fromBlock} - ${currentBlock}`); + + const proposals = []; + + // Читаем информацию о каждом предложении + for (let i = 0; i < events.length; i++) { + try { + const proposalId = events[i].args.proposalId; + console.log(`[Blockchain] Читаем предложение ID: ${proposalId}`); + + // Пробуем несколько раз для новых предложений + let proposal, isPassed; + let retryCount = 0; + const maxRetries = 3; + + while (retryCount < maxRetries) { + try { + proposal = await dle.proposals(proposalId); + isPassed = await dle.checkProposalResult(proposalId); + break; // Успешно прочитали + } catch (error) { + retryCount++; + console.log(`[Blockchain] Попытка ${retryCount} чтения предложения ${proposalId} не удалась:`, error.message); + if (retryCount < maxRetries) { + await new Promise(resolve => setTimeout(resolve, 2000)); // Ждем 2 секунды + } else { + throw error; // Превышено количество попыток + } + } + } + + // governanceChainId не сохраняется в предложении, используем текущую цепочку + const governanceChainId = 11155111; // Sepolia chain ID + + console.log(`[Blockchain] Данные предложения ${proposalId}:`, { + id: Number(proposal.id), + description: proposal.description, + forVotes: Number(proposal.forVotes), + againstVotes: Number(proposal.againstVotes), + executed: proposal.executed, + deadline: Number(proposal.deadline), + initiator: proposal.initiator, + operation: proposal.operation, + governanceChainId: Number(governanceChainId) + }); + + const proposalInfo = { + id: Number(proposal.id), + description: proposal.description, + forVotes: Number(proposal.forVotes), + againstVotes: Number(proposal.againstVotes), + executed: proposal.executed, + deadline: Number(proposal.deadline), + initiator: proposal.initiator, + operation: proposal.operation, + governanceChainId: Number(governanceChainId), + isPassed: isPassed, + blockNumber: events[i].blockNumber + }; + + proposals.push(proposalInfo); + } catch (error) { + console.log(`[Blockchain] Ошибка при чтении предложения ${i}:`, error.message); + + // Если это ошибка декодирования, возможно предложение еще не полностью записано + if (error.message.includes('could not decode result data')) { + console.log(`[Blockchain] Предложение ${i} еще не полностью синхронизировано, пропускаем`); + continue; + } + + // Продолжаем с следующим предложением + } + } + + // Сортируем по ID предложения (новые сверху) + proposals.sort((a, b) => b.id - a.id); + + console.log(`[Blockchain] Найдено предложений: ${proposals.length}`); + + res.json({ + success: true, + data: { + proposals: proposals, + totalCount: proposals.length + } + }); + + } catch (error) { + console.error('[Blockchain] Ошибка при получении списка предложений:', error); + res.status(500).json({ + success: false, + error: 'Ошибка при получении списка предложений: ' + error.message + }); + } +}); + +// Получение информации о предложении +router.post('/get-proposal-info', async (req, res) => { + try { + const { dleAddress, proposalId } = req.body; + + if (!dleAddress || proposalId === undefined) { + return res.status(400).json({ + success: false, + error: 'Все поля обязательны: dleAddress, proposalId' + }); + } + + console.log(`[Blockchain] Получение информации о предложении ${proposalId} в DLE: ${dleAddress}`); + + // Получаем RPC URL для Sepolia + const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: 'RPC URL для Sepolia не найден' + }); + } + + const provider = new ethers.JsonRpcProvider(rpcUrl); + + // ABI для чтения информации о предложении + const dleAbi = [ + "function proposals(uint256) external view returns (tuple(string description, uint256 duration, bytes operation, uint256 governanceChainId, uint256 startTime, bool executed, uint256 forVotes, uint256 againstVotes))", + "function checkProposalResult(uint256 _proposalId) external view returns (bool)" + ]; + + const dle = new ethers.Contract(dleAddress, dleAbi, provider); + + // Читаем информацию о предложении + const proposal = await dle.proposals(proposalId); + const isPassed = await dle.checkProposalResult(proposalId); + + // governanceChainId не сохраняется в предложении, используем текущую цепочку + const governanceChainId = 11155111; // Sepolia chain ID + + const proposalInfo = { + description: proposal.description, + duration: Number(proposal.duration), + operation: proposal.operation, + governanceChainId: Number(proposal.governanceChainId), + startTime: Number(proposal.startTime), + executed: proposal.executed, + forVotes: Number(proposal.forVotes), + againstVotes: Number(proposal.againstVotes), + isPassed: isPassed + }; + + console.log(`[Blockchain] Информация о предложении получена:`, proposalInfo); + + res.json({ + success: true, + data: proposalInfo + }); + + } catch (error) { + console.error('[Blockchain] Ошибка при получении информации о предложении:', error); + res.status(500).json({ + success: false, + error: 'Ошибка при получении информации о предложении: ' + error.message + }); + } +}); + + + + + +// Импортируем WebSocket функции из wsHub +const { broadcastProposalCreated, broadcastProposalVoted, broadcastProposalExecuted } = require('../wsHub'); + +// Экспортируем router как основной экспорт module.exports = router; \ No newline at end of file diff --git a/backend/routes/dleV2.js b/backend/routes/dleV2.js index 5819322..d1aa4f1 100644 --- a/backend/routes/dleV2.js +++ b/backend/routes/dleV2.js @@ -71,9 +71,9 @@ router.post('/', auth.requireAuth, auth.requireAdmin, async (req, res, next) => /** * @route GET /api/dle-v2 * @desc Получить список всех DLE v2 - * @access Private (только для авторизованных пользователей) + * @access Public (доступно всем пользователям) */ -router.get('/', auth.requireAuth, async (req, res, next) => { +router.get('/', async (req, res, next) => { try { const dles = dleV2Service.getAllDLEs(); diff --git a/backend/server.js b/backend/server.js index e2c1491..41f52d7 100644 --- a/backend/server.js +++ b/backend/server.js @@ -62,6 +62,8 @@ async function initServices() { const server = http.createServer(app); initWSS(server); +// WebSocket уже инициализирован в wsHub.js + async function startServer() { await initDbPool(); // Дождаться пересоздания пула! await seedAIAssistantSettings(); // Инициализация ассистента после загрузки модели Ollama diff --git a/backend/services/auth-service.js b/backend/services/auth-service.js index 97ba204..d7f4d47 100644 --- a/backend/services/auth-service.js +++ b/backend/services/auth-service.js @@ -890,6 +890,7 @@ class AuthService { tokenName: token.name, symbol: token.symbol || '', balance, + minBalance: token.min_balance, }); } return results; diff --git a/backend/services/authTokenService.js b/backend/services/authTokenService.js index ea36881..1688832 100644 --- a/backend/services/authTokenService.js +++ b/backend/services/authTokenService.js @@ -62,7 +62,14 @@ async function upsertAuthToken(token) { } async function deleteAuthToken(address, network) { - await encryptedDb.deleteData('auth_tokens', { address, network }); + console.log(`[AuthTokenService] deleteAuthToken: address=${address}, network=${network}`); + try { + await encryptedDb.deleteData('auth_tokens', { address, network }); + console.log(`[AuthTokenService] Токен успешно удален`); + } catch (error) { + console.error(`[AuthTokenService] Ошибка при удалении токена:`, error); + throw error; + } } module.exports = { getAllAuthTokens, saveAllAuthTokens, upsertAuthToken, deleteAuthToken }; \ No newline at end of file diff --git a/backend/services/encryptedDatabaseService.js b/backend/services/encryptedDatabaseService.js index ba9c8b5..d6fd113 100644 --- a/backend/services/encryptedDatabaseService.js +++ b/backend/services/encryptedDatabaseService.js @@ -323,48 +323,73 @@ class EncryptedDataService { */ async deleteData(tableName, conditions) { try { + console.log(`[EncryptedDataService] deleteData: tableName=${tableName}, conditions=`, conditions); + + // Проверяем, включено ли шифрование + if (!this.isEncryptionEnabled) { + return await this.executeUnencryptedQuery(tableName, conditions); + } + + // Получаем информацию о колонках + const { rows: columns } = await db.getQuery()(` + SELECT column_name, data_type + FROM information_schema.columns + WHERE table_name = $1 + AND table_schema = 'public' + ORDER BY ordinal_position + `, [tableName]); + + console.log(`[EncryptedDataService] Columns for ${tableName}:`, columns.map(c => c.column_name)); + // Функция для заключения зарезервированных слов в кавычки const quoteReservedWord = (word) => { const reservedWords = ['order', 'group', 'user', 'index', 'table', 'column', 'key', 'foreign', 'primary', 'unique', 'check', 'constraint', 'default', 'null', 'not', 'and', 'or', 'as', 'on', 'in', 'is', 'like', 'between', 'exists', 'all', 'any', 'some', 'distinct', 'case', 'when', 'then', 'else', 'end', 'limit', 'offset', 'having', 'union', 'intersect', 'except', 'with', 'recursive']; return reservedWords.includes(word.toLowerCase()) ? `"${word}"` : word; }; - // Проверяем, включено ли шифрование - if (!this.isEncryptionEnabled) { - let query = `DELETE FROM ${tableName}`; - const params = []; - let paramIndex = 1; - - if (Object.keys(conditions).length > 0) { - const whereClause = Object.keys(conditions) - .map(key => `${quoteReservedWord(key)} = $${paramIndex++}`) - .join(' AND '); - query += ` WHERE ${whereClause}`; - params.push(...Object.values(conditions)); - } - - const { rows } = await db.getQuery()(query, params); - return rows; - } - - // Для зашифрованных таблиц - пока используем обычный DELETE - // TODO: Добавить логику для зашифрованных условий WHERE let query = `DELETE FROM ${tableName}`; const params = []; let paramIndex = 1; if (Object.keys(conditions).length > 0) { const whereClause = Object.keys(conditions) - .map(key => `${quoteReservedWord(key)} = $${paramIndex++}`) + .map((key, index) => { + const value = conditions[key]; + + // Проверяем, есть ли зашифрованная версия колонки + const encryptedColumn = columns.find(col => col.column_name === `${key}_encrypted`); + + if (encryptedColumn) { + // Для зашифрованных колонок используем прямое сравнение с зашифрованным значением + // Ключ шифрования всегда первый параметр ($1), затем значения + return `${key}_encrypted = encrypt_text($${index + 2}, $1)`; + } else { + // Для незашифрованных колонок используем обычное сравнение + const columnName = quoteReservedWord(key); + return `${columnName} = $${index + 1}`; + } + }) .join(' AND '); query += ` WHERE ${whereClause}`; - params.push(...Object.values(conditions)); + + // Добавляем параметры + const paramsToAdd = Object.values(conditions); + params.push(...paramsToAdd); } + // Добавляем ключ шифрования в начало, если есть зашифрованные поля + const hasEncryptedFields = columns.some(col => col.column_name.endsWith('_encrypted')); + if (hasEncryptedFields) { + params.unshift(this.encryptionKey); + } + + console.log(`[EncryptedDataService] DELETE query: ${query}`); + console.log(`[EncryptedDataService] DELETE params:`, params); + const result = await db.getQuery()(query, params); return result.rows; } catch (error) { - // console.error(`❌ Ошибка удаления данных из ${tableName}:`, error); + console.error(`[EncryptedDataService] ❌ Ошибка удаления данных из ${tableName}:`, error); throw error; } } diff --git a/backend/wsHub.js b/backend/wsHub.js index 46e3725..b79f3b1 100644 --- a/backend/wsHub.js +++ b/backend/wsHub.js @@ -313,6 +313,58 @@ function broadcastAIStatus(status) { } } +// Blockchain уведомления +function broadcastProposalCreated(dleAddress, proposalId, txHash) { + const message = JSON.stringify({ + type: 'proposal_created', + data: { + dleAddress: dleAddress, + proposalId: proposalId, + txHash: txHash + }, + timestamp: Date.now() + }); + + broadcastToAllClients(message); +} + +function broadcastProposalVoted(dleAddress, proposalId, support, txHash) { + const message = JSON.stringify({ + type: 'proposal_voted', + data: { + dleAddress: dleAddress, + proposalId: proposalId, + support: support, + txHash: txHash + }, + timestamp: Date.now() + }); + + broadcastToAllClients(message); +} + +function broadcastProposalExecuted(dleAddress, proposalId, txHash) { + const message = JSON.stringify({ + type: 'proposal_executed', + data: { + dleAddress: dleAddress, + proposalId: proposalId, + txHash: txHash + }, + timestamp: Date.now() + }); + + broadcastToAllClients(message); +} + +function broadcastToAllClients(message) { + wss.clients.forEach((client) => { + if (client.readyState === WebSocket.OPEN) { + client.send(message); + } + }); +} + module.exports = { initWSS, broadcastContactsUpdate, @@ -323,6 +375,9 @@ module.exports = { broadcastTableRelationsUpdate, broadcastTagsUpdate, broadcastAIStatus, + broadcastProposalCreated, + broadcastProposalVoted, + broadcastProposalExecuted, getConnectedUsers, getStats }; \ No newline at end of file diff --git a/frontend/nginx-tunnel.conf b/frontend/nginx-tunnel.conf index f7013bf..1df15ca 100644 --- a/frontend/nginx-tunnel.conf +++ b/frontend/nginx-tunnel.conf @@ -12,10 +12,24 @@ server { return 403; } - # Блокировка очень старых браузеров - if ($http_user_agent ~* "MSIE [1-8]\.") { + # Блокировка старых браузеров и подозрительных User-Agent + if ($http_user_agent ~* "Chrome/[1-7][0-9]\.") { return 403; } + + if ($http_user_agent ~* "Safari/[1-5][0-9][0-9]\.") { + return 403; + } + + # Блокировка подозрительных поддоменов + if ($host !~* "^(hb3-accelerator\.com|www\.hb3-accelerator\.com|localhost|127\.0\.0\.1)$") { + return 404; + } + + # Блокировка сканирования резервных копий и архивов + if ($request_uri ~* "(backup|backups|bak|old|restore|www\.tar|website\.tar|\.tar\.gz|\.gz|\.sql\.tar|public_html\.tar|sftp-config\.json)") { + return 404; + } # Блокировка опасных файлов (НЕ блокируем .js, .css) if ($request_uri ~* "\.(php|asp|aspx|jsp|cgi|pl|py|sh|bash|exe|bat|cmd|com|pif|scr|vbs|vbe|jar|war|ear|dll|so|dylib|bin|sys|ini|log|bak|old|tmp|temp|swp|swo|~)$") { @@ -37,6 +51,16 @@ server { return 403; } + # Блокировка HEAD запросов к подозрительным файлам + if ($request_method = "HEAD" && $request_uri ~* "(backup|backups|bak|old|restore|\.tar|\.gz|\.sql|config\.js|sftp-config\.json)") { + return 404; + } + + # Блокировка всех запросов к конфигурационным файлам + if ($request_uri ~* "(config\.js|sftp-config\.json|\.config\.|\.conf\.|\.ini\.|\.env\.|\.json\.)") { + return 404; + } + # Основной location location / { try_files $uri $uri/ /index.html =404; diff --git a/frontend/src/components/BaseLayout.vue b/frontend/src/components/BaseLayout.vue index 019c78e..3a85cad 100644 --- a/frontend/src/components/BaseLayout.vue +++ b/frontend/src/components/BaseLayout.vue @@ -49,7 +49,7 @@ - - \ No newline at end of file diff --git a/frontend/src/views/smartcontracts/DleProposalsView.vue b/frontend/src/views/smartcontracts/DleProposalsView.vue index 5ab1d9d..535a01d 100644 --- a/frontend/src/views/smartcontracts/DleProposalsView.vue +++ b/frontend/src/views/smartcontracts/DleProposalsView.vue @@ -33,17 +33,160 @@ DLE не выбран - + + - -
Предложений пока нет
Подключите кошелек в сайдбаре для создания новых предложений
+Предложений пока нет
-Установка, настройка и управление модулями
-{{ module.description }}
-Нет установленных модулей
-{{ module.description }}
-Адрес: {{ formatAddress(module.address) }}
-Версия: {{ module.version }}
-Создание, подписание и выполнение предложений
-Нет активных предложений
-Описание: {{ proposal.description }}
-Инициатор: {{ formatAddress(proposal.initiator) }}
-Таймлок: {{ formatTimestamp(proposal.timelock) }}
-Подписи: {{ proposal.signaturesCount }} / {{ proposal.quorumRequired }}
-