diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 7b92437..51aaa11 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -5,7 +5,7 @@ const db = require('../db'); const logger = require('../utils/logger'); const helmet = require('helmet'); const rateLimit = require('express-rate-limit'); -const { checkRole, requireAuth } = require('../middleware/auth'); +const { checkRole, requireAuth, auth } = require('../middleware/auth'); const { pool } = require('../db'); const authService = require('../services/auth-service'); const { SiweMessage } = require('siwe'); @@ -1746,115 +1746,27 @@ router.post('/wallet', async (req, res) => { } }); -// Обработчик для связывания гостевых сообщений с пользователем +// Маршрут для обработки гостевых сообщений после аутентификации router.post('/link-guest-messages', requireAuth, async (req, res) => { try { - const { userId } = req.session; - - logger.info(`[link-guest-messages] Request for user ${userId}`); - - // Проверка кэша обработанных ID в сессии - if (!req.session.processedGuestIds) { - req.session.processedGuestIds = []; - } - - // Получаем все идентификаторы пользователя - const userIdentitiesResult = await db.query( - `SELECT provider, provider_id FROM user_identities WHERE user_id = $1`, - [userId] - ); - - const userIdentities = userIdentitiesResult.rows; - logger.info(`[link-guest-messages] Found ${userIdentities.length} identities for user ${userId}`); - - // Получаем только гостевые идентификаторы и фильтруем по неообработанным - const guestIdentities = userIdentities - .filter(identity => identity.provider === 'guest') - .filter(identity => !req.session.processedGuestIds.includes(identity.provider_id)); - - // Добавляем текущий guestId из сессии если он еще не обработан - if (req.session.guestId && !req.session.processedGuestIds.includes(req.session.guestId)) { - guestIdentities.push({ provider: 'guest', provider_id: req.session.guestId }); - logger.info(`[link-guest-messages] Added session guestId: ${req.session.guestId}`); - } - - // Добавляем guestId из тела запроса, если есть и еще не обработан - if (req.body.guestId && !req.session.processedGuestIds.includes(req.body.guestId)) { - guestIdentities.push({ provider: 'guest', provider_id: req.body.guestId }); - logger.info(`[link-guest-messages] Added request body guestId: ${req.body.guestId}`); - } - - // Убираем дубликаты - const uniqueGuestIds = [...new Set(guestIdentities.map(i => i.provider_id))]; - - // Если все ID уже обработаны, сразу возвращаем успех - if (uniqueGuestIds.length === 0) { - logger.info('[link-guest-messages] No new guest IDs to process'); - return res.json({ - success: true, - message: 'All guest IDs already processed', - processedIds: req.session.processedGuestIds + const userId = req.user.id; + const { currentGuestId } = req.body; + + if (!currentGuestId) { + return res.status(400).json({ + success: false, + error: 'Guest ID is required' }); } + + const result = await authService.linkGuestMessagesAfterAuth(userId, currentGuestId); - logger.info(`[link-guest-messages] Found ${uniqueGuestIds.length} new guestIds to process`); - uniqueGuestIds.forEach(id => logger.info(`[link-guest-messages] - ${id}`)); - - const results = []; - - // Обрабатываем каждый новый guestId - for (const guestId of uniqueGuestIds) { - // Проверяем наличие гостевых сообщений - try { - const guestMessagesCheck = await db.query( - 'SELECT EXISTS(SELECT 1 FROM guest_messages WHERE guest_id = $1)', - [guestId] - ); - - if (guestMessagesCheck.rows[0].exists) { - logger.info(`[link-guest-messages] Found messages for guest ID ${guestId}, processing`); - try { - // Используем обертку с правильным порядком аргументов - const result = await processGuestMessagesWrapper(guestId, userId); - results.push({ guestId, result }); - - // Добавляем в список обработанных - req.session.processedGuestIds.push(guestId); - - logger.info(`[link-guest-messages] Successfully processed guest ID ${guestId}`); - } catch (error) { - logger.error(`[link-guest-messages] Error processing guest ID ${guestId}:`, error); - results.push({ guestId, error: error.message }); - } - } else { - logger.info(`[link-guest-messages] No guest messages found for guest ID ${guestId}`); - // Всё равно добавляем в обработанные, чтобы не проверять снова - req.session.processedGuestIds.push(guestId); - results.push({ guestId, result: { success: true, message: 'No messages found' } }); - } - } catch (error) { - logger.error(`[link-guest-messages] Error checking guest messages for ${guestId}:`, error); - results.push({ guestId, error: error.message }); - } - } - - // Очищаем текущий guestId из сессии - req.session.guestId = null; - await req.session.save(); - logger.info('[link-guest-messages] Session updated, guestId cleared'); - - return res.json({ - success: true, - message: `Processed ${results.length} guest IDs`, - results, - processedIds: req.session.processedGuestIds - }); + res.json(result); } catch (error) { - logger.error('[link-guest-messages] Error:', error); - return res.status(500).json({ - success: false, - error: 'Internal server error', - details: error.message + logger.error('Error in /link-guest-messages:', error); + res.status(500).json({ + success: false, + error: 'Failed to process guest messages' }); } }); @@ -1883,4 +1795,54 @@ router.get('/check-session', async (req, res) => { } }); +// Маршрут для проверки баланса токенов +router.get('/check-tokens/:address', async (req, res) => { + try { + const { address } = req.params; + + // Проверяем баланс токенов на разных сетях + const balances = { + eth: '0', + bsc: '0', + arbitrum: '0', + polygon: '0' + }; + + try { + balances.eth = await authService.getTokenBalance(address, 'eth'); + } catch (error) { + logger.error(`Error checking ETH balance: ${error.message}`); + } + + try { + balances.bsc = await authService.getTokenBalance(address, 'bsc'); + } catch (error) { + logger.error(`Error checking BSC balance: ${error.message}`); + } + + try { + balances.arbitrum = await authService.getTokenBalance(address, 'arbitrum'); + } catch (error) { + logger.error(`Error checking Arbitrum balance: ${error.message}`); + } + + try { + balances.polygon = await authService.getTokenBalance(address, 'polygon'); + } catch (error) { + logger.error(`Error checking Polygon balance: ${error.message}`); + } + + res.json({ + success: true, + balances + }); + } catch (error) { + logger.error('Error checking token balances:', error); + res.status(500).json({ + success: false, + error: 'Internal server error' + }); + } +}); + module.exports = router; \ No newline at end of file diff --git a/backend/routes/identities.js b/backend/routes/identities.js index 8214628..ebaa02e 100644 --- a/backend/routes/identities.js +++ b/backend/routes/identities.js @@ -45,4 +45,31 @@ router.post('/link', requireAuth, async (req, res) => { } }); +// Получение балансов токенов +router.get('/token-balances', requireAuth, async (req, res) => { + try { + const userId = req.session.userId; + if (!userId) { + return res.status(401).json({ error: 'Unauthorized' }); + } + + // Получаем связанный кошелек + const wallet = await authService.getLinkedWallet(userId); + if (!wallet) { + return res.status(404).json({ error: 'No wallet linked' }); + } + + // Получаем балансы токенов + const balances = await authService.getTokenBalances(wallet); + + res.json({ + success: true, + balances + }); + } catch (error) { + logger.error('Error getting token balances:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + module.exports = router; diff --git a/backend/services/auth-service.js b/backend/services/auth-service.js index 5a38d6c..71f7a0a 100644 --- a/backend/services/auth-service.js +++ b/backend/services/auth-service.js @@ -96,10 +96,11 @@ class AuthService { let foundTokens = false; const balances = {}; - for (const contract of ADMIN_CONTRACTS) { + // Создаем массив промисов для параллельной проверки балансов + const checkPromises = ADMIN_CONTRACTS.map(async (contract) => { try { const provider = this.providers[contract.network]; - if (!provider) continue; + if (!provider) return null; const tokenContract = new ethers.Contract( contract.address, @@ -107,7 +108,14 @@ class AuthService { provider ); - const balance = await tokenContract.balanceOf(address); + // Создаем промис с таймаутом + 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[contract.network] = formattedBalance; @@ -122,6 +130,8 @@ class AuthService { logger.info(`Found admin tokens on ${contract.network}`); foundTokens = true; } + + return { network: contract.network, balance: formattedBalance }; } catch (error) { logger.error(`Error checking balance in ${contract.network}:`, { address, @@ -129,8 +139,12 @@ class AuthService { error: error.message }); balances[contract.network] = 'Error'; + return null; } - } + }); + + // Ждем выполнения всех проверок + await Promise.all(checkPromises); if (foundTokens) { logger.info(`Admin role summary for ${address}:`, { @@ -162,6 +176,7 @@ class AuthService { } const balances = {}; + const timeout = 3000; // 3 секунды таймаут for (const contract of ADMIN_CONTRACTS) { try { @@ -178,12 +193,20 @@ class AuthService { provider ); - const balance = await tokenContract.balanceOf(address); + // Создаем промис с таймаутом + const balancePromise = tokenContract.balanceOf(address); + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('Timeout')), timeout) + ); + + // Ждем первый выполненный промис + const balance = await Promise.race([balancePromise, timeoutPromise]); const formattedBalance = ethers.formatUnits(balance, 18); logger.info(`Token balance for ${address} on ${contract.network}:`, { contract: contract.address, - balance: formattedBalance + balance: formattedBalance, + timestamp: new Date().toISOString() }); balances[contract.network] = formattedBalance; @@ -191,12 +214,18 @@ class AuthService { logger.error(`Error getting balance for ${contract.network}:`, { address, contract: contract.address, - error: error.message + error: error.message || 'Unknown error', + timestamp: new Date().toISOString() }); balances[contract.network] = '0'; } } + logger.info(`Token balances fetched for ${address}:`, { + ...balances, + timestamp: new Date().toISOString() + }); + return balances; } @@ -427,6 +456,161 @@ class AuthService { return isAdmin; } + + /** + * Очистка старых гостевых идентификаторов + * @param {number} userId - ID пользователя + * @returns {Promise} + */ + async cleanupGuestIdentities(userId) { + try { + // Получаем все идентификаторы пользователя + const identities = await this.getUserIdentities(userId); + + // Фильтруем только гостевые идентификаторы + const guestIdentities = identities.filter(id => id.identity_type === 'guest'); + + // Если гостевых идентификаторов больше 3, удаляем старые + if (guestIdentities.length > 3) { + // Сортируем по дате создания (новые первые) + guestIdentities.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); + + // Оставляем только 3 последних идентификатора + const identitiesToDelete = guestIdentities.slice(3); + + // Удаляем старые идентификаторы + for (const identity of identitiesToDelete) { + await db.query( + 'DELETE FROM user_identities WHERE id = $1', + [identity.id] + ); + logger.info(`Deleted old guest identity: ${identity.identity_value}`); + } + } + } catch (error) { + logger.error('Error cleaning up guest identities:', error); + } + } + + /** + * Получение всех идентификаторов пользователя + * @param {number} userId - ID пользователя + * @returns {Promise} - Массив идентификаторов + */ + async getUserIdentities(userId) { + try { + const result = await db.query( + 'SELECT * FROM user_identities WHERE user_id = $1 ORDER BY created_at DESC', + [userId] + ); + return result.rows; + } catch (error) { + logger.error('[getUserIdentities] Error:', error); + throw error; + } + } + + /** + * Проверка баланса токенов в сети Arbitrum с оптимизированным таймаутом + * @param {string} address - Адрес кошелька + * @returns {Promise} - Результат проверки баланса + */ + async checkArbitrumBalance(address) { + const timeout = 2000; // Уменьшаем таймаут до 2 секунд + try { + const balance = await Promise.race([ + this.getTokenBalance(address, ADMIN_CONTRACTS.ARBITRUM), + new Promise((_, reject) => + setTimeout(() => reject(new Error('TIMEOUT')), timeout) + ) + ]); + return { balance, hasTokens: balance > 0 }; + } catch (error) { + logger.warn(`[checkArbitrumBalance] Timeout or error for ${address}:`, error); + return { balance: 0, hasTokens: false, error: error.message }; + } + } + + /** + * Обработка гостевых сообщений после аутентификации + */ + async linkGuestMessagesAfterAuth(userId, currentGuestId, previousGuestId) { + try { + logger.info(`[linkGuestMessagesAfterAuth] Starting for user ${userId} with guestId=${currentGuestId}`); + + // Проверяем, есть ли идентификатор для обработки + if (!currentGuestId) { + logger.debug('[linkGuestMessagesAfterAuth] No guest ID to process'); + return { success: true, message: 'No guest ID to process' }; + } + + // Получаем все гостевые сообщения для этого ID + const guestMessagesResult = await db.query( + 'SELECT * FROM guest_messages WHERE guest_id = $1 ORDER BY created_at ASC', + [currentGuestId] + ); + + if (guestMessagesResult.rows.length === 0) { + logger.info(`[linkGuestMessagesAfterAuth] No messages found for guest ID ${currentGuestId}`); + return { success: true, message: 'No messages found' }; + } + + const guestMessages = guestMessagesResult.rows; + logger.info(`[linkGuestMessagesAfterAuth] Found ${guestMessages.length} messages for guest ID ${currentGuestId}`); + + // Создаем одну беседу для всех сообщений этого гостевого ID + const firstMessage = guestMessages[0]; + const title = firstMessage.content.length > 30 + ? `${firstMessage.content.substring(0, 30)}...` + : firstMessage.content; + + const newConversationResult = await db.query( + 'INSERT INTO conversations (user_id, title) VALUES ($1, $2) RETURNING *', + [userId, title] + ); + + const conversation = newConversationResult.rows[0]; + logger.info(`[linkGuestMessagesAfterAuth] Created conversation ${conversation.id} for user ${userId}`); + + // Переносим все сообщения в новую беседу + for (const guestMessage of guestMessages) { + await db.query( + `INSERT INTO messages + (conversation_id, content, sender_type, role, channel, guest_message_id, created_at) + VALUES + ($1, $2, $3, $4, $5, $6, $7)`, + [ + conversation.id, + guestMessage.content, + guestMessage.is_ai ? 'assistant' : 'user', + guestMessage.is_ai ? 'assistant' : 'user', + 'web', + guestMessage.id, + guestMessage.created_at + ] + ); + } + + // Удаляем гостевой идентификатор после успешной привязки + await db.query( + 'DELETE FROM user_identities WHERE user_id = $1 AND identity_type = $2 AND identity_value = $3', + [userId, 'guest', currentGuestId] + ); + logger.info(`[linkGuestMessagesAfterAuth] Deleted guest identity ${currentGuestId}`); + + return { + success: true, + result: { + conversationId: conversation.id, + message: `Processed ${guestMessages.length} guest messages`, + success: true + } + }; + } catch (error) { + logger.error('[linkGuestMessagesAfterAuth] Error:', error); + throw error; + } + } } // Создаем и экспортируем единственный экземпляр diff --git a/frontend/src/assets/styles/home.css b/frontend/src/assets/styles/home.css index 43fd764..aecc817 100644 --- a/frontend/src/assets/styles/home.css +++ b/frontend/src/assets/styles/home.css @@ -1174,3 +1174,61 @@ input, textarea { .small-button:hover { background-color: #444; } + +/* Стили для блоков информации о пользователе и баланса токенов */ +.user-info, .token-balances { + background: #fff; + border-radius: 8px; + padding: 15px; + margin-bottom: 20px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.user-info h3, .token-balances h3 { + margin: 0 0 15px 0; + font-size: 16px; + color: #333; +} + +.user-info-item, .token-balance { + display: flex; + align-items: center; + margin-bottom: 10px; + font-size: 14px; +} + +.user-info-label, .token-name { + min-width: 80px; + color: #666; +} + +.user-info-value, .token-amount { + flex: 1; + color: #333; + font-family: monospace; +} + +.token-symbol { + margin-left: 5px; + color: #666; + font-size: 12px; +} + +/* Стили для правой панели */ +.right-sidebar { + width: 250px; + padding: 20px; + background: #f5f5f5; + border-left: 1px solid #ddd; + height: 100vh; + position: fixed; + right: 0; + top: 0; + overflow-y: auto; +} + +.right-sidebar.collapsed { + width: 0; + padding: 0; + border-left: none; +} diff --git a/frontend/src/composables/useAuth.js b/frontend/src/composables/useAuth.js index eee3389..b852373 100644 --- a/frontend/src/composables/useAuth.js +++ b/frontend/src/composables/useAuth.js @@ -11,6 +11,7 @@ export function useAuth() { const email = ref(null); const processedGuestIds = ref([]); const identities = ref([]); + const tokenBalances = ref([]); // Функция для обновления списка идентификаторов const updateIdentities = async () => { @@ -27,7 +28,21 @@ export function useAuth() { } }; - const updateAuth = ({ authenticated, authType: newAuthType, userId: newUserId, address: newAddress, telegramId: newTelegramId, isAdmin: newIsAdmin, email: newEmail }) => { + const checkTokenBalances = async (address) => { + try { + const response = await axios.get(`/api/auth/check-tokens/${address}`); + if (response.data.success) { + tokenBalances.value = response.data.balances; + return response.data.balances; + } + return null; + } catch (error) { + console.error('Error checking token balances:', error); + return null; + } + }; + + const updateAuth = async ({ authenticated, authType: newAuthType, userId: newUserId, address: newAddress, telegramId: newTelegramId, isAdmin: newIsAdmin, email: newEmail }) => { const wasAuthenticated = isAuthenticated.value; const previousUserId = userId.value; @@ -50,6 +65,22 @@ export function useAuth() { isAdmin.value = newIsAdmin === true; email.value = newEmail || null; + // Кэшируем данные аутентификации + localStorage.setItem('authData', JSON.stringify({ + authenticated, + authType: newAuthType, + userId: newUserId, + address: newAddress, + telegramId: newTelegramId, + isAdmin: newIsAdmin, + email: newEmail + })); + + // Если аутентификация через кошелек, проверяем баланс токенов только при изменении адреса + if (authenticated && newAuthType === 'wallet' && newAddress && newAddress !== address.value) { + await checkTokenBalances(newAddress); + } + console.log('Auth updated:', { authenticated: isAuthenticated.value, userId: userId.value, @@ -159,7 +190,7 @@ export function useAuth() { const previousAuthType = authType.value; // Обновляем данные авторизации через updateAuth вместо прямого изменения - updateAuth({ + await updateAuth({ authenticated: response.data.authenticated, authType: response.data.authType, userId: response.data.userId, @@ -323,6 +354,7 @@ export function useAuth() { email, identities, processedGuestIds, + tokenBalances, updateAuth, checkAuth, disconnect, diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index 5b3cd98..8f8b8b1 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -172,14 +172,108 @@ + + + + +
+ + + + + + + +
+
+ Код верификации: + {{ telegramVerificationCode }} + Скопировано! +
+ Открыть бота Telegram + +
+ + +
+ {{ telegramError }} + +
+
+ {{ emailError }} + +
+
+ -
-

Баланс:

+
+

Баланс токенов:

ETH: {{ tokenBalances.eth }} {{ TOKEN_CONTRACTS.eth.symbol }} -
+
+
+ BSC: + {{ tokenBalances.bsc }} + {{ TOKEN_CONTRACTS.bsc.symbol }} +
ARB: {{ tokenBalances.arbitrum }} @@ -190,49 +284,6 @@ {{ tokenBalances.polygon }} {{ TOKEN_CONTRACTS.polygon.symbol }}
-
- BNB: - {{ tokenBalances.bsc }} - {{ TOKEN_CONTRACTS.bsc.symbol }} -
-
- - - @@ -584,6 +635,37 @@ const handleTelegramAuth = async () => { } }; +// Функция для обновления балансов +const updateBalances = async () => { + if (auth.isAuthenticated.value && auth.address?.value) { + try { + const balances = await fetchTokenBalances(); + tokenBalances.value = balances; + console.log('Token balances updated:', balances); + } catch (error) { + console.error('Error updating balances:', error); + } + } +}; + +// Функция для периодического обновления балансов +let balanceUpdateInterval = null; + +const startBalanceUpdates = () => { + // Обновляем балансы сразу + updateBalances(); + + // Увеличиваем интервал обновления до 5 минут + balanceUpdateInterval = setInterval(updateBalances, 300000); +}; + +const stopBalanceUpdates = () => { + if (balanceUpdateInterval) { + clearInterval(balanceUpdateInterval); + balanceUpdateInterval = null; + } +}; + // Функция для подключения кошелька - обновленная версия const handleWalletAuth = async () => { if (isConnecting.value || isAuthenticated.value) return; @@ -602,6 +684,9 @@ const handleWalletAuth = async () => { // Обрабатываем загрузку сообщений после успешной аутентификации await handlePostAuthMessageLoading('wallet'); + + // Запускаем обновление балансов + startBalanceUpdates(); } // Добавляем небольшую задержку перед сбросом состояния isConnecting @@ -1064,18 +1149,6 @@ const handleScroll = async () => { } }; -// Функция получения балансов -const updateBalances = async () => { - if (auth.isAuthenticated.value && auth.address?.value) { - try { - const balances = await fetchTokenBalances(); - tokenBalances.value = balances; - } catch (error) { - console.error('Error updating balances:', error); - } - } -}; - // Функция для отмены аутентификации через Telegram const cancelTelegramAuth = () => { // Очищаем интервал проверки @@ -1180,6 +1253,9 @@ const disconnectWallet = async () => { try { console.log('Выполняется выход из системы...'); + // Останавливаем обновление балансов + stopBalanceUpdates(); + // Сохраняем гостевой ID для продолжения работы после выхода const guestId = getFromStorage('guestId') || generateUniqueId(); setToStorage('guestId', guestId); @@ -1401,6 +1477,24 @@ onMounted(async () => { setToStorage('hasUserSentMessage', 'true'); } + // Проверяем аутентификацию только если нет данных в localStorage + const cachedAuth = localStorage.getItem('authData'); + if (!cachedAuth) { + const { data: sessionData } = await api.get('/api/auth/check'); + console.log('Проверка сессии:', sessionData); + + if (sessionData.authenticated && sessionData.authType === 'wallet') { + // Запускаем обновление балансов + startBalanceUpdates(); + } + } else { + // Используем кэшированные данные + const authData = JSON.parse(cachedAuth); + if (authData.authenticated && authData.authType === 'wallet') { + startBalanceUpdates(); + } + } + // Прокручиваем к последнему сообщению scrollToBottom(); }); @@ -1411,5 +1505,8 @@ onBeforeUnmount(() => { messagesContainer.value.removeEventListener('scroll', handleScroll); } window.removeEventListener('load-chat-history', loadChatHistory); + + // Останавливаем обновление балансов + stopBalanceUpdates(); });