From 808f2594c626be6d6ed906ba9abd68abe1061efe Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 3 Apr 2025 17:25:38 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B2=D0=B0=D1=88=D0=B5=20=D1=81=D0=BE=D0=BE?= =?UTF-8?q?=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BA=D0=BE=D0=BC=D0=BC?= =?UTF-8?q?=D0=B8=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/routes/auth.js | 284 +++--- backend/routes/chat.js | 14 +- backend/services/identity-service.js | 200 +++++ backend/services/session-service.js | 169 ++++ frontend/src/assets/styles/home.css | 159 ++++ frontend/src/composables/useAuth.js | 122 ++- frontend/src/utils/wallet.js | 148 ++++ frontend/src/views/HomeView.vue | 1195 +++++++++++++++++--------- 8 files changed, 1743 insertions(+), 548 deletions(-) create mode 100644 backend/services/identity-service.js create mode 100644 backend/services/session-service.js create mode 100644 frontend/src/utils/wallet.js diff --git a/backend/routes/auth.js b/backend/routes/auth.js index a1f5fab..7b92437 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -534,51 +534,133 @@ router.post('/link-identity', async (req, res) => { // Проверка статуса аутентификации router.get('/check', async (req, res) => { try { - console.log('Check auth, session data:', { - userId: req.session.userId, - authenticated: req.session.authenticated, - authType: req.session.authType, - telegramId: req.session.telegramId - }); + logger.info(`[session/check] Checking session: ${req.sessionID}`); - // Если пользователь не аутентифицирован - if (!req.session.authenticated || !req.session.userId) { - return res.json({ - authenticated: false + const authenticated = req.session.authenticated || false; + const authType = req.session.authType || null; + + // Подробное логирование для отладки восстановления сессии + logger.info(`[session/check] Session state: authenticated=${authenticated}, authType=${authType}, userId=${req.session.userId || 'none'}`); + + // Проверяем наличие идентификаторов в сессии + const sessionIdentities = []; + if (req.session.userId) sessionIdentities.push(`userId:${req.session.userId}`); + if (req.session.email) sessionIdentities.push(`email:${req.session.email}`); + if (req.session.address) sessionIdentities.push(`address:${req.session.address}`); + if (req.session.telegramId) sessionIdentities.push(`telegramId:${req.session.telegramId}`); + + logger.info(`[session/check] Identities in session: ${sessionIdentities.join(', ')}`); + + let identities = []; + let isAdmin = false; + + if (authenticated && req.session.userId) { + // Если пользователь аутентифицирован, получаем его идентификаторы из БД + try { + const identitiesResult = await db.query( + `SELECT provider, provider_id FROM user_identities WHERE user_id = $1`, + [req.session.userId] + ); + + identities = identitiesResult.rows; + logger.info(`[session/check] Found ${identities.length} identities in database for user ${req.session.userId}`); + + // Проверяем роль пользователя + const roleResult = await db.query( + 'SELECT role FROM users WHERE id = $1', + [req.session.userId] + ); + + if (roleResult.rows.length > 0) { + isAdmin = roleResult.rows[0].role === 'admin'; + req.session.isAdmin = isAdmin; + } + } catch (error) { + logger.error(`[session/check] Error fetching identities: ${error.message}`); + } + } + + // Проверяем, нужно ли создать новый гостевой ID + if (!authenticated && !req.session.guestId) { + req.session.guestId = crypto.randomBytes(16).toString('hex'); + logger.info(`[session/check] Created new guest ID: ${req.session.guestId}`); + + // Сохраняем сессию с новым гостевым ID + await new Promise((resolve, reject) => { + req.session.save(err => { + if (err) { + logger.error('[session/check] Error saving session with new guest ID:', err); + reject(err); + } else { + logger.info('[session/check] Session with new guest ID saved successfully'); + resolve(); + } + }); }); } - // Возвращаем полную информацию об аутентифицированном пользователе - const userData = { - authenticated: true, - userId: req.session.userId, - authType: req.session.authType, - isAdmin: req.session.isAdmin || false + // Формируем ответ + const response = { + success: true, + authenticated, + userId: req.session.userId || null, + guestId: req.session.guestId || null, + authType, + identitiesCount: identities.length, + isAdmin: isAdmin || false }; - // Добавляем идентификаторы, если они есть в сессии - if (req.session.address) userData.address = req.session.address; - if (req.session.telegramId) userData.telegramId = req.session.telegramId; - if (req.session.email) userData.email = req.session.email; + // Добавляем специфические поля в зависимости от типа аутентификации + if (authType === 'wallet') { + response.address = req.session.address || null; + } else if (authType === 'email') { + response.email = req.session.email || null; + } else if (authType === 'telegram') { + response.telegramId = req.session.telegramId || null; + if (req.session.telegramUsername) { + response.telegramUsername = req.session.telegramUsername; + } + if (req.session.telegramFirstName) { + response.telegramFirstName = req.session.telegramFirstName; + } + } - console.log('Returning auth data:', userData); - - res.json(userData); + logger.info(`[session/check] Session check complete: authenticated=${authenticated}, authType=${authType}`); + return res.json(response); } catch (error) { - console.error('Error checking auth status:', error); - res.status(500).json({ error: 'Internal server error' }); + logger.error('[session/check] Error:', error); + return res.status(500).json({ + success: false, + error: 'Internal server error' + }); } }); // Выход из системы -router.post('/logout', (req, res) => { - req.session.destroy((err) => { - if (err) { - console.error('Error destroying session:', err); - return res.status(500).json({ error: 'Failed to logout' }); +router.post('/logout', async (req, res) => { + try { + logger.info('[logout] Logout request received'); + + const sessionService = require('../services/session-service'); + const result = await sessionService.logout(req.session); + + if (result) { + logger.info('[logout] User successfully logged out'); + return res.json({ success: true }); + } else { + logger.warn('[logout] Error during logout process'); + return res.status(500).json({ + success: false, + error: 'Error during logout process' + }); } - res.json({ success: true }); - }); + } catch (error) { + logger.error('[logout] Error:', error); + return res.status(500).json({ + success: false, + error: 'Internal server error' + }); + } }); // Маршрут для авторизации через Telegram @@ -1455,29 +1537,10 @@ async function linkGuestMessagesAfterAuth(session, userId) { return { success: true, message: 'No guest IDs to process' }; } - // Получаем список постоянных идентификаторов пользователя - const permanentIdentitiesResult = await db.query( - `SELECT provider, provider_id FROM user_identities WHERE user_id = $1 AND provider IN ('wallet', 'email', 'telegram')`, - [userId] - ); - - // Логирование найденных постоянных идентификаторов - logger.info(`[linkGuestMessagesAfterAuth] Found ${permanentIdentitiesResult.rows.length} permanent identities for user ${userId}`); - permanentIdentitiesResult.rows.forEach(identity => { - logger.info(`[linkGuestMessagesAfterAuth] - ${identity.provider}:${identity.provider_id}`); - }); - - // Сохраняем все постоянные идентификаторы из сессии - if (session.email) { - await saveUserIdentity(userId, 'email', session.email, true); - } - - if (session.address) { - await saveUserIdentity(userId, 'wallet', session.address, true); - } - - if (session.telegramId) { - await saveUserIdentity(userId, 'telegram', session.telegramId, true); + // Инициализируем массив обработанных ID, если он не существует + if (!session.processedGuestIds) { + session.processedGuestIds = []; + logger.info(`[linkGuestMessagesAfterAuth] Initialized processedGuestIds array for session`); } let results = []; @@ -1487,12 +1550,13 @@ async function linkGuestMessagesAfterAuth(session, userId) { logger.info(`[linkGuestMessagesAfterAuth] Processing current guest ID ${guestId} for user ${userId}`); try { - // Сохраняем гостевой ID как идентификатор пользователя - await saveUserIdentity(userId, 'guest', guestId, true); - // Связываем сообщения с пользователем через обертку с правильным порядком аргументов const result = await processGuestMessagesWrapper(guestId, userId); results.push({ guestId, result }); + + // Добавляем в список обработанных + session.processedGuestIds.push(guestId); + logger.info(`[linkGuestMessagesAfterAuth] Successfully processed guest ID ${guestId}`); } catch (error) { logger.error(`[linkGuestMessagesAfterAuth] Error processing guest ID ${guestId}:`, error); @@ -1505,12 +1569,13 @@ async function linkGuestMessagesAfterAuth(session, userId) { logger.info(`[linkGuestMessagesAfterAuth] Processing previous guest ID ${previousGuestId} for user ${userId}`); try { - // Сохраняем предыдущий гостевой ID как идентификатор пользователя - await saveUserIdentity(userId, 'guest', previousGuestId, true); - // Связываем сообщения с пользователем через обертку с правильным порядком аргументов const result = await processGuestMessagesWrapper(previousGuestId, userId); results.push({ guestId: previousGuestId, result }); + + // Добавляем в список обработанных + session.processedGuestIds.push(previousGuestId); + logger.info(`[linkGuestMessagesAfterAuth] Successfully processed previous guest ID ${previousGuestId}`); } catch (error) { logger.error(`[linkGuestMessagesAfterAuth] Error processing previous guest ID ${previousGuestId}:`, error); @@ -1688,6 +1753,11 @@ router.post('/link-guest-messages', requireAuth, async (req, res) => { 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`, @@ -1696,66 +1766,44 @@ router.post('/link-guest-messages', requireAuth, async (req, res) => { const userIdentities = userIdentitiesResult.rows; logger.info(`[link-guest-messages] Found ${userIdentities.length} identities for user ${userId}`); - userIdentities.forEach(identity => { - logger.info(`[link-guest-messages] - ${identity.provider}:${identity.provider_id}`); - }); - // Собираем все guestId, связанные с идентификаторами пользователя - let guestIds = []; + // Получаем только гостевые идентификаторы и фильтруем по неообработанным + const guestIdentities = userIdentities + .filter(identity => identity.provider === 'guest') + .filter(identity => !req.session.processedGuestIds.includes(identity.provider_id)); - // Добавляем текущий guestId из сессии - if (req.session.guestId) { - guestIds.push(req.session.guestId); + // Добавляем текущий 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) { - guestIds.push(req.body.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}`); } - // Добавляем массив guestIds из тела запроса, если есть - if (req.body.guestIds && Array.isArray(req.body.guestIds)) { - logger.info(`[link-guest-messages] Adding ${req.body.guestIds.length} guestIds from request body`); - guestIds = [...guestIds, ...req.body.guestIds]; - } - - // Добавляем guestId из идентификаторов пользователя - const guestIdentities = userIdentities.filter(identity => identity.provider === 'guest'); - if (guestIdentities.length > 0) { - logger.info(`[link-guest-messages] Adding ${guestIdentities.length} guest identities from user_identities`); - guestIds = [...guestIds, ...guestIdentities.map(identity => identity.provider_id)]; - } - // Убираем дубликаты - guestIds = [...new Set(guestIds)]; + const uniqueGuestIds = [...new Set(guestIdentities.map(i => i.provider_id))]; - logger.info(`[link-guest-messages] Final list of unique guestIds to process: ${guestIds.length}`); - guestIds.forEach(id => logger.info(`[link-guest-messages] - ${id}`)); - - if (guestIds.length === 0) { - logger.info('[link-guest-messages] No guest IDs found for user'); - return res.json({ success: true, message: 'No guest messages to link' }); + // Если все 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 + }); } + 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 guestIds) { - // Пропускаем пустые или невалидные ID - if (!guestId || typeof guestId !== 'string') { - logger.warn(`[link-guest-messages] Skipping invalid guest ID: ${guestId}`); - continue; - } - - // Проверяем, не обрабатывали ли мы уже этот ID - if (req.session.processedGuestIds && req.session.processedGuestIds.includes(guestId)) { - logger.info(`[link-guest-messages] Guest ID ${guestId} already processed, skipping`); - results.push({ guestId, result: { success: true, message: 'Already processed' } }); - continue; - } - + // Обрабатываем каждый новый guestId + for (const guestId of uniqueGuestIds) { // Проверяем наличие гостевых сообщений try { const guestMessagesCheck = await db.query( @@ -1769,6 +1817,10 @@ router.post('/link-guest-messages', requireAuth, async (req, res) => { // Используем обертку с правильным порядком аргументов 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); @@ -1776,6 +1828,8 @@ router.post('/link-guest-messages', requireAuth, async (req, res) => { } } 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) { @@ -1784,19 +1838,6 @@ router.post('/link-guest-messages', requireAuth, async (req, res) => { } } - // Сохраняем все обработанные guestId, чтобы не обрабатывать их снова - if (!req.session.processedGuestIds) { - req.session.processedGuestIds = []; - } - - // Добавляем только успешно обработанные ID - const successfulGuestIds = results - .filter(r => r.result && r.result.success) - .map(r => r.guestId); - - req.session.processedGuestIds = [...new Set([...req.session.processedGuestIds, ...successfulGuestIds])]; - logger.info(`[link-guest-messages] Updated processed guest IDs: ${req.session.processedGuestIds.join(', ')}`); - // Очищаем текущий guestId из сессии req.session.guestId = null; await req.session.save(); @@ -1805,7 +1846,8 @@ router.post('/link-guest-messages', requireAuth, async (req, res) => { return res.json({ success: true, message: `Processed ${results.length} guest IDs`, - results + results, + processedIds: req.session.processedGuestIds }); } catch (error) { logger.error('[link-guest-messages] Error:', error); diff --git a/backend/routes/chat.js b/backend/routes/chat.js index 81f7506..559a237 100644 --- a/backend/routes/chat.js +++ b/backend/routes/chat.js @@ -142,19 +142,25 @@ async function processGuestMessages(userId, guestId) { // Обработчик для гостевых сообщений router.post('/guest-message', async (req, res) => { try { - const { message, language } = req.body; - const guestId = req.session.guestId || crypto.randomBytes(16).toString('hex'); + const { content, language, guestId: requestGuestId } = req.body; + + if (!content) { + return res.status(400).json({ success: false, error: 'Content is required' }); + } + + // Используем гостевой ID из запроса или из сессии, или генерируем новый + const guestId = requestGuestId || req.session.guestId || crypto.randomBytes(16).toString('hex'); // Сохраняем ID гостя в сессии req.session.guestId = guestId; await req.session.save(); - console.log('Saving guest message:', { guestId, message }); + console.log('Saving guest message:', { guestId, content }); // Сохраняем сообщение пользователя const result = await db.query( 'INSERT INTO guest_messages (guest_id, content, language, is_ai) VALUES ($1, $2, $3, false) RETURNING id', - [guestId, message, language] + [guestId, content, language || 'auto'] ); console.log('Guest message saved:', result.rows[0]); diff --git a/backend/services/identity-service.js b/backend/services/identity-service.js new file mode 100644 index 0000000..fb81787 --- /dev/null +++ b/backend/services/identity-service.js @@ -0,0 +1,200 @@ +const db = require('../db'); +const logger = require('../utils/logger'); + +/** + * Сервис для работы с идентификаторами пользователей + */ +class IdentityService { + /** + * Сохраняет идентификатор пользователя в базу данных + * @param {number} userId - ID пользователя + * @param {string} provider - Тип идентификатора (wallet, email, telegram, guest) + * @param {string} providerId - Значение идентификатора + * @param {boolean} verified - Флаг верификации идентификатора + * @returns {Promise} - Результат операции + */ + async saveIdentity(userId, provider, providerId, verified = true) { + try { + if (!userId || !provider || !providerId) { + logger.warn(`[IdentityService] Missing required parameters: userId=${userId}, provider=${provider}, providerId=${providerId}`); + return { + success: false, + error: 'Missing required parameters' + }; + } + + logger.info(`[IdentityService] Saving identity for user ${userId}: ${provider}:${providerId}`); + + // Проверяем, существует ли уже такой идентификатор + const existingResult = await db.query( + `SELECT user_id FROM user_identities WHERE provider = $1 AND provider_id = $2`, + [provider, providerId] + ); + + if (existingResult.rows.length > 0) { + const existingUserId = existingResult.rows[0].user_id; + + // Если идентификатор уже принадлежит этому пользователю, ничего не делаем + if (existingUserId === userId) { + logger.info(`[IdentityService] Identity ${provider}:${providerId} already exists for user ${userId}`); + } else { + // Если идентификатор принадлежит другому пользователю, логируем это + logger.warn(`[IdentityService] Identity ${provider}:${providerId} already belongs to user ${existingUserId}, not user ${userId}`); + return { + success: false, + error: `Identity already belongs to another user (${existingUserId})` + }; + } + } else { + // Создаем новую запись + await db.query( + `INSERT INTO user_identities (user_id, provider, provider_id) + VALUES ($1, $2, $3)`, + [userId, provider, providerId] + ); + logger.info(`[IdentityService] Created new identity ${provider}:${providerId} for user ${userId}`); + } + + return { success: true }; + } catch (error) { + logger.error(`[IdentityService] Error saving identity ${provider}:${providerId} for user ${userId}:`, error); + return { success: false, error: error.message }; + } + } + + /** + * Получает все идентификаторы пользователя + * @param {number} userId - ID пользователя + * @returns {Promise} - Массив идентификаторов + */ + async getUserIdentities(userId) { + try { + if (!userId) { + logger.warn('[IdentityService] Missing userId parameter'); + return []; + } + + const result = await db.query( + `SELECT provider, provider_id FROM user_identities WHERE user_id = $1`, + [userId] + ); + + logger.info(`[IdentityService] Found ${result.rows.length} identities for user ${userId}`); + return result.rows; + } catch (error) { + logger.error(`[IdentityService] Error getting identities for user ${userId}:`, error); + return []; + } + } + + /** + * Получает все идентификаторы пользователя определенного типа + * @param {number} userId - ID пользователя + * @param {string} provider - Тип идентификатора + * @returns {Promise} - Массив идентификаторов + */ + async getUserIdentitiesByProvider(userId, provider) { + try { + if (!userId || !provider) { + logger.warn(`[IdentityService] Missing parameters: userId=${userId}, provider=${provider}`); + return []; + } + + const result = await db.query( + `SELECT provider_id FROM user_identities WHERE user_id = $1 AND provider = $2`, + [userId, provider] + ); + + logger.info(`[IdentityService] Found ${result.rows.length} ${provider} identities for user ${userId}`); + return result.rows.map(row => row.provider_id); + } catch (error) { + logger.error(`[IdentityService] Error getting ${provider} identities for user ${userId}:`, error); + return []; + } + } + + /** + * Находит пользователя по идентификатору + * @param {string} provider - Тип идентификатора + * @param {string} providerId - Значение идентификатора + * @returns {Promise} - Информация о пользователе или null + */ + async findUserByIdentity(provider, providerId) { + try { + if (!provider || !providerId) { + logger.warn(`[IdentityService] Missing parameters: provider=${provider}, providerId=${providerId}`); + return null; + } + + const result = await db.query( + `SELECT u.id, u.role FROM users u + JOIN user_identities ui ON u.id = ui.user_id + WHERE ui.provider = $1 AND ui.provider_id = $2`, + [provider, providerId] + ); + + if (result.rows.length === 0) { + logger.info(`[IdentityService] No user found with identity ${provider}:${providerId}`); + return null; + } + + logger.info(`[IdentityService] Found user ${result.rows[0].id} with identity ${provider}:${providerId}`); + return result.rows[0]; + } catch (error) { + logger.error(`[IdentityService] Error finding user by identity ${provider}:${providerId}:`, error); + return null; + } + } + + /** + * Сохраняет идентификаторы из сессии для пользователя + * @param {object} session - Объект сессии + * @param {number} userId - ID пользователя + * @returns {Promise} - Результат операции + */ + async saveIdentitiesFromSession(session, userId) { + try { + if (!session || !userId) { + logger.warn(`[IdentityService] Missing parameters: session=${!!session}, userId=${userId}`); + return { success: false, error: 'Missing required parameters' }; + } + + const results = []; + + // Сохраняем все постоянные идентификаторы из сессии + if (session.email) { + const emailResult = await this.saveIdentity(userId, 'email', session.email.toLowerCase(), true); + results.push({ type: 'email', result: emailResult }); + } + + if (session.address) { + const walletResult = await this.saveIdentity(userId, 'wallet', session.address.toLowerCase(), true); + results.push({ type: 'wallet', result: walletResult }); + } + + if (session.telegramId) { + const telegramResult = await this.saveIdentity(userId, 'telegram', session.telegramId, true); + results.push({ type: 'telegram', result: telegramResult }); + } + + // Сохраняем гостевые идентификаторы + if (session.guestId) { + const guestResult = await this.saveIdentity(userId, 'guest', session.guestId, true); + results.push({ type: 'guest', result: guestResult }); + } + + if (session.previousGuestId && session.previousGuestId !== session.guestId) { + const prevGuestResult = await this.saveIdentity(userId, 'guest', session.previousGuestId, true); + results.push({ type: 'previousGuest', result: prevGuestResult }); + } + + logger.info(`[IdentityService] Saved ${results.length} identities from session for user ${userId}`); + return { success: true, results }; + } catch (error) { + logger.error(`[IdentityService] Error saving identities from session for user ${userId}:`, error); + return { success: false, error: error.message }; + } + } +} + +module.exports = new IdentityService(); \ No newline at end of file diff --git a/backend/services/session-service.js b/backend/services/session-service.js new file mode 100644 index 0000000..ca19836 --- /dev/null +++ b/backend/services/session-service.js @@ -0,0 +1,169 @@ +const logger = require('../utils/logger'); +const db = require('../db'); + +/** + * Сервис для работы с сессиями пользователей + */ +class SessionService { + /** + * Сохраняет сессию с обработкой ошибок + * @param {object} session - Объект сессии + * @param {string} context - Контекст для логирования + * @returns {Promise} - Результат операции + */ + async saveSession(session, context = '') { + if (!session) { + logger.warn(`[SessionService${context ? '/' + context : ''}] Cannot save null session`); + return false; + } + + try { + return await new Promise((resolve, reject) => { + session.save(err => { + if (err) { + logger.error(`[SessionService${context ? '/' + context : ''}] Error saving session:`, err); + reject(err); + } else { + logger.info(`[SessionService${context ? '/' + context : ''}] Session saved successfully`); + resolve(true); + } + }); + }); + } catch (error) { + logger.error(`[SessionService${context ? '/' + context : ''}] Error in saveSession:`, error); + return false; + } + } + + /** + * Восстанавливает сессию из базы данных по ID + * @param {string} sessionId - ID сессии + * @returns {Promise} - Данные сессии или null + */ + async getSessionData(sessionId) { + try { + if (!sessionId) { + logger.warn('[SessionService] Cannot restore session without sessionId'); + return null; + } + + logger.info(`[SessionService] Attempting to retrieve session ${sessionId}`); + + const result = await db.query( + 'SELECT sess FROM session WHERE sid = $1', + [sessionId] + ); + + if (result.rows.length === 0) { + logger.info(`[SessionService] No session found with ID ${sessionId}`); + return null; + } + + const sessionData = result.rows[0].sess; + logger.info(`[SessionService] Retrieved session data for ${sessionId}`); + + return sessionData; + } catch (error) { + logger.error(`[SessionService] Error retrieving session ${sessionId}:`, error); + return null; + } + } + + /** + * Обновляет данные аутентификации в сессии + * @param {object} session - Объект сессии + * @param {object} authData - Данные аутентификации + * @returns {Promise} - Результат операции + */ + async updateAuthData(session, authData) { + try { + if (!session || !authData) { + logger.warn('[SessionService] Missing parameters for updateAuthData'); + return false; + } + + const { userId, authType, isAdmin, ...otherData } = authData; + + if (!userId || !authType) { + logger.warn('[SessionService] Missing userId or authType in authData'); + return false; + } + + // Обновляем основные поля аутентификации + session.userId = userId; + session.authType = authType; + session.authenticated = true; + + if (isAdmin !== undefined) { + session.isAdmin = isAdmin; + } + + // Обновляем дополнительные данные в зависимости от типа аутентификации + if (authType === 'wallet' && otherData.address) { + session.address = otherData.address.toLowerCase(); + } else if (authType === 'email' && otherData.email) { + session.email = otherData.email.toLowerCase(); + } else if (authType === 'telegram') { + if (otherData.telegramId) session.telegramId = otherData.telegramId; + if (otherData.telegramUsername) session.telegramUsername = otherData.telegramUsername; + if (otherData.telegramFirstName) session.telegramFirstName = otherData.telegramFirstName; + } + + // Сохраняем гостевые ID, если они предоставлены и не были ранее в сессии + if (otherData.guestId && !session.guestId) { + session.guestId = otherData.guestId; + } + + if (otherData.previousGuestId && !session.previousGuestId) { + session.previousGuestId = otherData.previousGuestId; + } + + // Сохраняем обновленную сессию + return await this.saveSession(session, 'updateAuthData'); + } catch (error) { + logger.error('[SessionService] Error updating auth data:', error); + return false; + } + } + + /** + * Очищает данные аутентификации в сессии + * @param {object} session - Объект сессии + * @returns {Promise} - Результат операции + */ + async logout(session) { + try { + if (!session) { + logger.warn('[SessionService] Cannot logout null session'); + return false; + } + + // Сохраняем гостевые ID перед очисткой + const guestId = session.guestId; + + // Удаляем данные аутентификации + delete session.userId; + delete session.authenticated; + delete session.authType; + delete session.isAdmin; + delete session.address; + delete session.email; + delete session.telegramId; + delete session.telegramUsername; + delete session.telegramFirstName; + + // Восстанавливаем гостевой ID для продолжения работы + if (guestId) { + session.guestId = guestId; + } + + // Сохраняем обновленную сессию + return await this.saveSession(session, 'logout'); + } catch (error) { + logger.error('[SessionService] Error during logout:', error); + return false; + } + } +} + +module.exports = new SessionService(); \ No newline at end of file diff --git a/frontend/src/assets/styles/home.css b/frontend/src/assets/styles/home.css index 6e3a20d..43fd764 100644 --- a/frontend/src/assets/styles/home.css +++ b/frontend/src/assets/styles/home.css @@ -216,6 +216,7 @@ input, textarea { padding: 10px 15px; border-radius: 8px; max-width: 80%; + position: relative; } .user-message { @@ -229,18 +230,58 @@ input, textarea { align-self: flex-start; } +.system-message { + background-color: #FFF3E0; + align-self: center; + margin-left: auto; + margin-right: auto; + font-style: italic; + color: #FF5722; + text-align: center; + max-width: 90%; +} + .message-content { margin-bottom: 5px; white-space: pre-wrap; word-break: break-word; } +.message-meta { + display: flex; + justify-content: space-between; + align-items: center; +} + .message-time { font-size: 12px; color: #777; text-align: right; } +.message-status { + font-size: 12px; + color: #777; +} + +.sending-indicator { + color: #2196F3; + font-style: italic; +} + +.error-indicator { + color: #F44336; + font-weight: bold; +} + +.is-local { + opacity: 0.7; +} + +.has-error { + border: 1px solid #F44336; +} + .chat-input { display: flex; background-color: white; @@ -1015,3 +1056,121 @@ input, textarea { color: #666; font-size: 14px; } + +/* Стили для отображения подключенного кошелька */ +.wallet-connected .wallet-button { + background-color: #4CAF50 !important; + color: white !important; +} + +.wallet-connected #auth-display { + display: inline-block; + padding: 8px 12px; + background-color: rgba(76, 175, 80, 0.1); + border: 1px solid #4CAF50; + border-radius: 4px; + margin-right: 10px; + color: #4CAF50; + font-weight: 500; +} + +/* Индикатор подключения */ +.connection-indicator { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 50%; + margin-right: 8px; + background-color: #ccc; +} + +.wallet-connected .connection-indicator { + background-color: #4CAF50; +} + +/* Стили для кнопок авторизации */ +#auth-buttons { + display: flex; + gap: 10px; + margin-bottom: 20px; +} + +#logout-button { + display: none; + background-color: #f44336; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + font-weight: 500; +} + +#logout-button:hover { + background-color: #d32f2f; +} + +/* Анимация для индикации подключения */ +@keyframes pulse { + 0% { + box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.7); + } + 70% { + box-shadow: 0 0 0 10px rgba(76, 175, 80, 0); + } + 100% { + box-shadow: 0 0 0 0 rgba(76, 175, 80, 0); + } +} + +.wallet-connected .connection-indicator { + animation: pulse 2s infinite; +} + +/* Стили для отладочной информации */ +.debug-info { + margin-top: 20px; + padding: 10px; + background-color: #f5f5f5; + border-radius: 8px; + font-size: 0.9em; +} + +.debug-info h4 { + margin-top: 0; + margin-bottom: 8px; + color: #666; +} + +.debug-item { + margin-bottom: 8px; + word-break: break-all; +} + +.debug-item code { + background-color: #e0e0e0; + padding: 2px 4px; + border-radius: 4px; + font-family: monospace; + color: #333; +} + +.debug-buttons { + display: flex; + gap: 8px; + margin-top: 8px; +} + +.small-button { + padding: 5px 10px; + background-color: #5e5e5e; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.8em; +} + +.small-button:hover { + background-color: #444; +} diff --git a/frontend/src/composables/useAuth.js b/frontend/src/composables/useAuth.js index b9b48b3..eee3389 100644 --- a/frontend/src/composables/useAuth.js +++ b/frontend/src/composables/useAuth.js @@ -74,9 +74,23 @@ export function useAuth() { if (isAuthenticated.value) { console.log('Linking messages after authentication'); + // Проверка, есть ли гостевой ID для обработки + const localGuestId = localStorage.getItem('guestId'); + + // Если гостевого ID нет или он уже был обработан, пропускаем запрос + if (!localGuestId || processedGuestIds.value.includes(localGuestId)) { + console.log('No new guest IDs to process or already processed'); + return { + success: true, + message: 'No new guest IDs to process', + processedIds: processedGuestIds.value + }; + } + // Создаем объект с идентификаторами для передачи на сервер const identifiersData = { - userId: userId.value + userId: userId.value, + guestId: localGuestId }; // Добавляем все доступные идентификаторы @@ -84,17 +98,6 @@ export function useAuth() { if (email.value) identifiersData.email = email.value; if (telegramId.value) identifiersData.telegramId = telegramId.value; - // Сохраняем предыдущий guestId из localStorage, если есть - const localGuestId = localStorage.getItem('guestId'); - if (localGuestId && !processedGuestIds.value.includes(localGuestId)) { - console.log('Found local guestId:', localGuestId); - // Добавляем guestId в идентификаторы - identifiersData.guestId = localGuestId; - // Добавляем guestId в список обработанных - processedGuestIds.value.push(localGuestId); - } - - // Логируем попытку связывания сообщений console.log('Sending link-guest-messages request with data:', identifiersData); try { @@ -104,15 +107,20 @@ export function useAuth() { if (response.data.success) { console.log('Messages linked successfully:', response.data); - // Если в ответе есть обработанные guestIds, добавляем их в список - if (response.data.results && Array.isArray(response.data.results)) { + // Обновляем список обработанных guestIds из ответа сервера + if (response.data.processedIds && Array.isArray(response.data.processedIds)) { + processedGuestIds.value = [...response.data.processedIds]; + console.log('Updated processed guest IDs from server:', processedGuestIds.value); + } + // В качестве запасного варианта также обрабатываем старый формат ответа + else if (response.data.results && Array.isArray(response.data.results)) { const newProcessedIds = response.data.results .filter(result => result.guestId) .map(result => result.guestId); if (newProcessedIds.length > 0) { processedGuestIds.value = [...new Set([...processedGuestIds.value, ...newProcessedIds])]; - console.log('Updated processed guest IDs:', processedGuestIds.value); + console.log('Updated processed guest IDs from results:', processedGuestIds.value); } } @@ -148,6 +156,7 @@ export function useAuth() { const wasAuthenticated = isAuthenticated.value; const previousUserId = userId.value; + const previousAuthType = authType.value; // Обновляем данные авторизации через updateAuth вместо прямого изменения updateAuth({ @@ -165,18 +174,34 @@ export function useAuth() { // Сначала обновляем идентификаторы, чтобы иметь актуальные данные await updateIdentities(); - // Если пользователь только что аутентифицировался или сменил аккаунт, + // Если пользователь только что аутентифицировался или сменил аккаунт, // связываем гостевые сообщения с его аккаунтом if (!wasAuthenticated || (previousUserId && previousUserId !== response.data.userId)) { // Немедленно связываем сообщения const linkResult = await linkMessages(); console.log('Link messages result on auth change:', linkResult); + + // Если пользователь только что аутентифицировался через Telegram, + // обновляем историю чата без перезагрузки страницы + if (response.data.authType === 'telegram' && previousAuthType !== 'telegram') { + console.log('Telegram auth detected, loading message history'); + // Отправляем событие для загрузки истории чата + window.dispatchEvent(new CustomEvent('load-chat-history')); + } } + + // Обновляем отображение подключенного состояния в UI + updateConnectionDisplay(true, response.data.authType, response.data); + } else { + // Обновляем отображение отключенного состояния + updateConnectionDisplay(false); } return response.data; } catch (error) { console.error('Error checking auth:', error); + // В случае ошибки сбрасываем состояние аутентификации + updateConnectionDisplay(false); return { authenticated: false }; } }; @@ -189,6 +214,8 @@ export function useAuth() { console.log('Created new guestId for future session:', newGuestId); await axios.post('/api/auth/logout'); + + // Обновляем состояние в памяти updateAuth({ authenticated: false, authType: null, @@ -199,6 +226,9 @@ export function useAuth() { isAdmin: false }); + // Обновляем отображение отключенного состояния + updateConnectionDisplay(false); + // Очищаем списки идентификаторов identities.value = []; @@ -208,6 +238,11 @@ export function useAuth() { localStorage.removeItem('address'); localStorage.removeItem('isAdmin'); + // Удаляем класс подключенного кошелька + document.body.classList.remove('wallet-connected'); + + console.log('User disconnected successfully'); + return { success: true }; } catch (error) { console.error('Error disconnecting:', error); @@ -222,6 +257,58 @@ export function useAuth() { } }; + // Функция для обновления отображения подключения в UI + const updateConnectionDisplay = (isConnected, authType, authData = {}) => { + try { + console.log('Updating connection display:', { isConnected, authType, authData }); + + if (isConnected) { + document.body.classList.add('wallet-connected'); + + const authDisplayEl = document.getElementById('auth-display'); + if (authDisplayEl) { + let displayText = 'Подключено'; + + if (authType === 'wallet' && authData.address) { + const shortAddress = `${authData.address.substring(0, 6)}...${authData.address.substring(authData.address.length - 4)}`; + displayText = `Кошелек: ${shortAddress}`; + } else if (authType === 'email' && authData.email) { + displayText = `Email: ${authData.email}`; + } else if (authType === 'telegram' && authData.telegramId) { + displayText = `Telegram: ${authData.telegramUsername || authData.telegramId}`; + } + + authDisplayEl.innerHTML = displayText; + authDisplayEl.style.display = 'inline-block'; + } + + // Скрываем кнопки авторизации и показываем кнопку выхода + const authButtonsEl = document.getElementById('auth-buttons'); + const logoutButtonEl = document.getElementById('logout-button'); + + if (authButtonsEl) authButtonsEl.style.display = 'none'; + if (logoutButtonEl) logoutButtonEl.style.display = 'inline-block'; + } else { + document.body.classList.remove('wallet-connected'); + + // Скрываем отображение аутентификации + const authDisplayEl = document.getElementById('auth-display'); + if (authDisplayEl) { + authDisplayEl.style.display = 'none'; + } + + // Показываем кнопки авторизации и скрываем кнопку выхода + const authButtonsEl = document.getElementById('auth-buttons'); + const logoutButtonEl = document.getElementById('logout-button'); + + if (authButtonsEl) authButtonsEl.style.display = 'flex'; + if (logoutButtonEl) logoutButtonEl.style.display = 'none'; + } + } catch (error) { + console.error('Error updating connection display:', error); + } + }; + onMounted(async () => { await checkAuth(); }); @@ -241,6 +328,7 @@ export function useAuth() { disconnect, linkMessages, updateIdentities, - updateProcessedGuestIds + updateProcessedGuestIds, + updateConnectionDisplay }; } \ No newline at end of file diff --git a/frontend/src/utils/wallet.js b/frontend/src/utils/wallet.js new file mode 100644 index 0000000..cd08791 --- /dev/null +++ b/frontend/src/utils/wallet.js @@ -0,0 +1,148 @@ +import axios from '../api/axios'; +import { ethers } from 'ethers'; +import { SiweMessage } from 'siwe'; + +export const connectWallet = async () => { + try { + console.log('Starting wallet connection...'); + + // Проверяем наличие MetaMask или другого Ethereum провайдера + if (!window.ethereum) { + console.error('No Ethereum provider (like MetaMask) detected!'); + return { + success: false, + error: 'Не найден кошелек MetaMask или другой Ethereum провайдер. Пожалуйста, установите расширение MetaMask.' + }; + } + + console.log('MetaMask detected, requesting accounts...'); + + // Запрашиваем доступ к аккаунтам + const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' }); + console.log('Got accounts:', accounts); + + if (!accounts || accounts.length === 0) { + return { + success: false, + error: 'Не удалось получить доступ к аккаунтам. Пожалуйста, разрешите доступ в MetaMask.' + }; + } + + // Берем первый аккаунт в списке + const address = accounts[0]; + // Нормализуем адрес (приводим к нижнему регистру для последующих сравнений) + const normalizedAddress = ethers.utils.getAddress(address); + console.log('Normalized address:', normalizedAddress); + + // Запрашиваем nonce с сервера + console.log('Requesting nonce...'); + const nonceResponse = await axios.get(`/api/auth/nonce?address=${normalizedAddress}`); + const nonce = nonceResponse.data.nonce; + console.log('Got nonce:', nonce); + + if (!nonce) { + return { + success: false, + error: 'Не удалось получить nonce от сервера.' + }; + } + + // Создаем провайдер Ethers + const provider = new ethers.providers.Web3Provider(window.ethereum); + const signer = provider.getSigner(); + + // Создаем сообщение для подписи + const domain = window.location.host; + const origin = window.location.origin; + + // Создаем SIWE сообщение + const message = new SiweMessage({ + domain, + address: normalizedAddress, + statement: 'Sign in with Ethereum to the app.', + uri: origin, + version: '1', + chainId: 1, // Ethereum mainnet + nonce: nonce, + issuedAt: new Date().toISOString(), + resources: [`${origin}/api/auth/verify`] + }); + + // Получаем строку сообщения для подписи + const messageToSign = message.prepareMessage(); + console.log('SIWE message:', messageToSign); + + // Запрашиваем подпись + console.log('Requesting signature...'); + const signature = await signer.signMessage(messageToSign); + + if (!signature) { + return { + success: false, + error: 'Подпись не была получена. Пожалуйста, подпишите сообщение в MetaMask.' + }; + } + + console.log('Got signature:', signature); + + // Отправляем верификацию на сервер + console.log('Sending verification request...'); + const verifyResponse = await axios.post('/api/auth/verify', { + address: normalizedAddress, + signature, + nonce + }); + + // Обновляем интерфейс для отображения подключенного состояния + document.body.classList.add('wallet-connected'); + + // Обновляем отображение адреса кошелька в UI + const authDisplayEl = document.getElementById('auth-display'); + if (authDisplayEl) { + const shortAddress = `${normalizedAddress.substring(0, 6)}...${normalizedAddress.substring(normalizedAddress.length - 4)}`; + authDisplayEl.innerHTML = `Кошелек: ${shortAddress}`; + authDisplayEl.style.display = 'inline-block'; + } + + // Скрываем кнопки авторизации и показываем кнопку выхода + const authButtonsEl = document.getElementById('auth-buttons'); + const logoutButtonEl = document.getElementById('logout-button'); + + if (authButtonsEl) authButtonsEl.style.display = 'none'; + if (logoutButtonEl) logoutButtonEl.style.display = 'inline-block'; + + console.log('Verification response:', verifyResponse.data); + + if (verifyResponse.data.success) { + return { + success: true, + address: normalizedAddress, + userId: verifyResponse.data.userId, + isAdmin: verifyResponse.data.isAdmin + }; + } else { + return { + success: false, + error: verifyResponse.data.error || 'Ошибка верификации на сервере.' + }; + } + } catch (error) { + console.error('Error connecting wallet:', error); + + // Формируем понятное сообщение об ошибке + let errorMessage = 'Произошла ошибка при подключении кошелька.'; + + if (error.code === 4001) { + errorMessage = 'Вы отклонили запрос на подпись в MetaMask.'; + } else if (error.response && error.response.data && error.response.data.error) { + errorMessage = error.response.data.error; + } else if (error.message) { + errorMessage = error.message; + } + + return { + success: false, + error: errorMessage + }; + } +}; \ No newline at end of file diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index 044ebbf..5b3cd98 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -49,18 +49,33 @@
+ :class="['message', + message.sender_type === 'assistant' || message.role === 'assistant' ? 'ai-message' : + message.sender_type === 'system' || message.role === 'system' ? 'system-message' : 'user-message', + message.isLocal ? 'is-local' : '', + message.hasError ? 'has-error' : '']">
-
{{ formatTime(message.timestamp || message.created_at) }}
+
+
{{ formatTime(message.timestamp || message.created_at) }}
+
+ Отправка... +
+
+ Ошибка отправки +
+
+
-
- +
+ +
+ {{ telegramError }} + +
+
+ + +
+

Гостевой идентификатор:

+
+ {{ guestIdValue || 'Не задан' }} +
+
+ + +
+
@@ -230,11 +263,57 @@ import { fetchTokenBalances, TOKEN_CONTRACTS } from '../services/tokens'; console.log('HomeView.vue: Version with chat loaded'); +// Вспомогательные функции для работы с localStorage +const isLocalStorageAvailable = () => { + try { + const test = 'test'; + window.localStorage.setItem(test, test); + window.localStorage.removeItem(test); + return true; + } catch (e) { + console.error('localStorage is not available:', e); + return false; + } +}; + +const getFromStorage = (key, defaultValue = null) => { + if (!isLocalStorageAvailable()) return defaultValue; + try { + return window.localStorage.getItem(key) || defaultValue; + } catch (e) { + console.error(`Error getting ${key} from localStorage:`, e); + return defaultValue; + } +}; + +const setToStorage = (key, value) => { + if (!isLocalStorageAvailable()) return false; + try { + window.localStorage.setItem(key, value); + return true; + } catch (e) { + console.error(`Error setting ${key} in localStorage:`, e); + return false; + } +}; + +const removeFromStorage = (key) => { + if (!isLocalStorageAvailable()) return false; + try { + window.localStorage.removeItem(key); + return true; + } catch (e) { + console.error(`Error removing ${key} from localStorage:`, e); + return false; + } +}; + +// Константы const auth = useAuth(); const isAuthenticated = computed(() => auth.isAuthenticated.value); const isConnecting = ref(false); const messages = ref([]); -const hasUserSentMessage = ref(localStorage.getItem('hasUserSentMessage') === 'true'); +const hasUserSentMessage = ref(getFromStorage('hasUserSentMessage') === 'true'); const newMessage = ref(''); const isLoading = ref(false); const messagesContainer = ref(null); @@ -251,6 +330,7 @@ const showTelegramVerification = ref(false); const telegramVerificationCode = ref(''); const telegramBotLink = ref(''); const telegramAuthCheckInterval = ref(null); +const telegramError = ref(''); const showEmailVerification = ref(false); const emailVerificationCode = ref(''); const emailError = ref(''); @@ -289,29 +369,33 @@ const showWalletSidebar = ref(false); // Вычисленное свойство для фильтрации идентификаторов const filteredIdentities = computed(() => { - if (!auth.identities?.value) return []; + if (!auth.identities || !auth.identities.value) return []; - // Фильтруем и оставляем только постоянные идентификаторы - return auth.identities.value.filter(identity => - identity.provider !== 'guest' && // Исключаем guest идентификаторы - // Также исключаем дубликаты, если идентификатор уже показан выше - !(identity.provider === 'wallet' && auth.address?.value) && - !(identity.provider === 'telegram' && auth.telegramId?.value) && - !(identity.provider === 'email' && auth.email?.value) - ); + // Фильтруем идентификаторы, чтобы не показывать те, которые уже отображены в основных полях + return auth.identities.value.filter(identity => { + if (identity.provider === 'wallet' && auth.address?.value === identity.provider_id) { + return false; + } + if (identity.provider === 'telegram' && auth.telegramId?.value === identity.provider_id) { + return false; + } + if (identity.provider === 'email' && auth.email?.value === identity.provider_id) { + return false; + } + return true; + }); }); -// Функция для форматирования названий провайдеров -const formatIdentityProvider = (provider) => { +// Функция для форматирования названий провайдеров +function formatIdentityProvider(provider) { const providers = { 'wallet': 'Кошелек', - 'telegram': 'Telegram', 'email': 'Email', + 'telegram': 'Telegram', 'guest': 'Гость' }; - return providers[provider] || provider; -}; +} // Функция для управления сайдбаром const toggleSidebar = () => { @@ -328,7 +412,7 @@ const navigateTo = (page) => { const toggleWalletSidebar = () => { showWalletSidebar.value = !showWalletSidebar.value; // Сохраняем в localStorage предпочтение пользователя - localStorage.setItem('showWalletSidebar', showWalletSidebar.value); + setToStorage('showWalletSidebar', showWalletSidebar.value.toString()); }; // Функция для копирования кода @@ -420,7 +504,122 @@ const sendEmailVerification = async () => { } }; -// Функция для проверки введенного кода +// Функция для обработки загрузки сообщений после аутентификации +const handlePostAuthMessageLoading = async (authType) => { + try { + console.log(`Обработка загрузки сообщений после аутентификации через ${authType}`); + + // Сохраняем текущее количество сообщений для отслеживания + let currentMessageCount = 0; + try { + const countResponse = await axios.get('/api/chat/history?count_only=true'); + if (countResponse.data.success) { + currentMessageCount = countResponse.data.count || 0; + console.log(`Текущее количество сообщений перед обработкой: ${currentMessageCount}`); + } + } catch (error) { + console.warn('Ошибка при получении текущего количества сообщений:', error); + } + + // Загружаем историю сообщений после успешной авторизации + await loadChatHistory(); + + // Получаем новое количество сообщений для отслеживания новых ответов + const newCountResponse = await axios.get('/api/chat/history?count_only=true'); + if (newCountResponse.data.success) { + const newCount = newCountResponse.data.count || 0; + + // Настраиваем отслеживание только если есть разница в количестве сообщений + if (newCount !== currentMessageCount) { + console.log(`Количество сообщений изменилось: ${currentMessageCount} -> ${newCount}`); + setupMessagePolling(newCount); + } else { + console.log('Количество сообщений не изменилось, отслеживание не требуется'); + } + } + } catch (error) { + console.error(`Ошибка при обработке сообщений после аутентификации через ${authType}:`, error); + } +}; + +// Обработчик для Telegram аутентификации +const handleTelegramAuth = async () => { + try { + showTelegramVerification.value = true; + telegramError.value = ''; + + // Инициализируем процесс аутентификации через Telegram + const response = await axios.post('/api/auth/telegram/init'); + + if (response.data.success) { + // Используем правильное имя свойства из ответа API + telegramVerificationCode.value = response.data.verificationCode; + telegramBotLink.value = response.data.botLink; + + // Создаем интервал для проверки состояния авторизации + telegramAuthCheckInterval.value = setInterval(async () => { + try { + const checkResponse = await auth.checkAuth(); + if (checkResponse.authenticated && checkResponse.authType === 'telegram') { + console.log('Telegram аутентификация успешна'); + clearTelegramInterval(); + showTelegramVerification.value = false; + telegramVerificationCode.value = ''; + + // Обрабатываем загрузку сообщений после успешной аутентификации + await handlePostAuthMessageLoading('telegram'); + } + } catch (error) { + console.error('Ошибка при проверке аутентификации:', error); + } + }, 2000); // Проверяем каждые 2 секунды + } else { + telegramError.value = response.data.error || 'Ошибка при инициализации авторизации Telegram'; + showTelegramVerification.value = false; + } + } catch (error) { + console.error('Ошибка инициализации Telegram аутентификации:', error); + telegramError.value = 'Произошла ошибка при инициализации аутентификации через Telegram'; + showTelegramVerification.value = false; + } +}; + +// Функция для подключения кошелька - обновленная версия +const handleWalletAuth = async () => { + if (isConnecting.value || isAuthenticated.value) return; + + isConnecting.value = true; + try { + const result = await connectWithWallet(); + console.log('Результат подключения кошелька:', result); + + if (result.success) { + // Обновляем состояние авторизации + const authResponse = await auth.checkAuth(); + + if (authResponse.authenticated && authResponse.authType === 'wallet') { + console.log('Кошелёк успешно подключен и аутентифицирован'); + + // Обрабатываем загрузку сообщений после успешной аутентификации + await handlePostAuthMessageLoading('wallet'); + } + + // Добавляем небольшую задержку перед сбросом состояния isConnecting + setTimeout(() => { + isConnecting.value = false; + }, 500); + return; + } else { + console.error('Не удалось подключить кошелёк:', result.error); + } + } catch (error) { + console.error('Ошибка при подключении кошелька:', error); + } + + isConnecting.value = false; +}; + +// Функция для проверки кода верификации email - обновленная версия const verifyEmailCode = async () => { try { // Очищаем сообщение об ошибке @@ -455,12 +654,14 @@ const verifyEmailCode = async () => { }, 3000); // Обновляем состояние аутентификации - await auth.checkAuth(); + const authResponse = await auth.checkAuth(); - // Перезагружаем страницу для обновления UI через 1 секунду - setTimeout(() => { - window.location.reload(); - }, 1000); + if (authResponse.authenticated && authResponse.authType === 'email') { + console.log('Email успешно подтвержден и аутентифицирован'); + + // Обрабатываем загрузку сообщений после успешной аутентификации + await handlePostAuthMessageLoading('email'); + } } else { emailError.value = response.data.message || 'Неверный код верификации'; } @@ -477,118 +678,380 @@ const verifyEmailCode = async () => { } }; -// Функция для отмены Email аутентификации -const cancelEmailAuth = () => { - showEmailForm.value = false; - showEmailVerificationInput.value = false; - showEmailVerification.value = false; - emailInput.value = ''; - emailVerificationCode.value = ''; - emailError.value = ''; - emailFormatError.value = false; +// Улучшенная функция для отслеживания изменений аутентификации +const watchAuthChanges = () => { + // Отслеживаем изменения в состоянии аутентификации + watch(() => auth.isAuthenticated.value, async (newValue, oldValue) => { + const authChanged = { + from: oldValue, + to: newValue, + userId: auth.userId.value, + authType: auth.authType.value + }; + + console.log('Состояние аутентификации изменилось:', authChanged); + + // Обновляем отображение аутентификации + updateAuthDisplay({ + isAuthenticated: auth.isAuthenticated.value, + authType: auth.authType.value, + address: auth.address.value, + email: auth.email.value, + telegramId: auth.telegramId.value, + telegramUsername: auth.telegramUsername + }); + + if (newValue && !oldValue) { + // Пользователь только что аутентифицировался + console.log(`Пользователь аутентифицирован через ${auth.authType.value}, загружаем историю и проверяем новые сообщения`); + + // Обрабатываем загрузку сообщений после аутентификации + await handlePostAuthMessageLoading(auth.authType.value); + } else if (!newValue && oldValue) { + // Пользователь вышел из системы + console.log('Пользователь вышел, сбрасываем сообщения'); + messages.value = messages.value.filter(msg => msg.isGuest); + offset.value = 0; + hasMoreMessages.value = true; + } + }, { immediate: true }); }; -// Функция для отправки сообщения -const handleMessage = async (text) => { +// Функция для форматирования времени +const formatTime = (timestamp) => { + if (!timestamp) return ''; + try { - const messageContent = text.trim(); - if (!messageContent) return; + const date = new Date(timestamp); - // Показываем правую панель только если пользователь не аутентифицирован - if (!isAuthenticated.value) { - showWalletSidebar.value = true; + // Проверяем, является ли дата валидной + if (isNaN(date.getTime())) { + console.warn('Invalid timestamp:', timestamp); + return ''; } - newMessage.value = ''; - isLoading.value = true; + // Форматируем дату с указанием дня, месяца, года и времени + return date.toLocaleString([], { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + } catch (error) { + console.error('Error formatting time:', error, timestamp); + return ''; + } +}; + +// Форматирование сообщения с поддержкой markdown +const formatMessage = (text) => { + if (!text) return ''; + const rawHtml = marked.parse(text); + return DOMPurify.sanitize(rawHtml); +}; + +// Функция для сокращения адреса кошелька (первые 6 и последние 4 символа) +function truncateAddress(address) { + if (!address) return ''; + return `${address.substring(0, 6)}...${address.substring(address.length - 4)}`; +} + +// Вычисляемое свойство для гостевого ID +const guestIdValue = computed(() => { + return getFromStorage('guestId', ''); +}); + +// Функции для отладки гостевых идентификаторов +const refreshGuestId = () => { + const newGuestId = generateUniqueId(); + setToStorage('guestId', newGuestId); + console.log('Guest ID refreshed:', newGuestId); +}; + +const clearGuestMessages = () => { + removeFromStorage('guestMessages'); + console.log('Guest messages cleared'); + messages.value = messages.value.filter(m => !m.isGuest); +}; + +// Метод для загрузки истории чата +const loadChatHistory = async () => { + // Сбрасываем текущие сообщения и загружаем новую историю + isLoading.value = true; + + try { + // Сначала получаем общее количество сообщений + const countResponse = await axios.get('/api/chat/history?count_only=true'); - if (!isAuthenticated.value) { - // Сохраняем в таблицу guest_messages - console.log('Sending guest message:', messageContent); - const response = await api.post('/api/chat/guest-message', { - message: messageContent, - language: userLanguage.value - }); + if (countResponse.data.success) { + const messageCount = countResponse.data.count; + console.log(`История содержит ${messageCount} сообщений`); - if (response.data.success) { - console.log('Guest message sent:', response.data); - const userMessage = { - id: response.data.messageId, - content: messageContent, - sender_type: 'user', - role: 'user', - timestamp: new Date().toISOString() - }; - messages.value.push(userMessage); - - // Показываем сообщение с просьбой авторизоваться - messages.value.push({ - id: Date.now() + 1, - content: 'Для получения ответа от ассистента, пожалуйста, авторизуйтесь одним из способов в правой панели.', - sender_type: 'assistant', - role: 'assistant', - timestamp: new Date().toISOString() - }); - - // Прокручиваем к последнему сообщению - await nextTick(); - scrollToBottom(); - } else { - throw new Error(response.data.error || 'Ошибка при отправке сообщения'); - } - } else { - // Отправляем сообщение аутентифицированного пользователя - console.log('Sending authenticated message:', messageContent); - const response = await api.post('/api/chat/message', { - message: messageContent, - language: userLanguage.value - }); + // Рассчитываем смещение для получения последних сообщений + const effectiveOffset = Math.max(0, messageCount - limit.value); - if (response.data.success) { - console.log('Authenticated message sent:', response.data); - // Добавляем сообщение пользователя - messages.value.push({ - id: response.data.userMessage.id, - content: response.data.userMessage.content, - sender_type: 'user', - role: 'user', - timestamp: response.data.userMessage.created_at - }); + // Загружаем историю сообщений + const response = await axios.get(`/api/chat/history?offset=${effectiveOffset}&limit=${limit.value}`); + + if (response && response.data.success) { + // Очищаем локальные гостевые сообщения при успешной загрузке истории аутентифицированного пользователя + if (auth.isAuthenticated.value) { + removeFromStorage('guestMessages'); + } - // Добавляем ответ ассистента - messages.value.push({ - id: response.data.aiMessage.id, - content: response.data.aiMessage.content, - sender_type: 'assistant', - role: 'assistant', - timestamp: response.data.aiMessage.created_at - }); + messages.value = response.data.messages; + console.log(`Загружено ${messages.value.length} сообщений из истории`); + + // Отправляем событие об обновлении сообщений + window.dispatchEvent(new CustomEvent('messages-updated', { + detail: { count: messages.value.length } + })); // Прокручиваем к последнему сообщению await nextTick(); scrollToBottom(); - } else { - throw new Error(response.data.error || 'Ошибка при отправке сообщения'); } } } catch (error) { - console.error('Error sending message:', error); - messages.value.push({ - id: Date.now() + 1, - content: 'Произошла ошибка при отправке сообщения. Пожалуйста, попробуйте позже.', - sender_type: 'assistant', - role: 'assistant', - timestamp: new Date().toISOString() - }); - - // Прокручиваем к последнему сообщению - await nextTick(); - scrollToBottom(); + console.error('Ошибка загрузки истории чата:', error); } finally { isLoading.value = false; } }; +// Метод для очистки интервала проверки авторизации +const clearTelegramInterval = () => { + if (telegramAuthCheckInterval.value) { + clearInterval(telegramAuthCheckInterval.value); + telegramAuthCheckInterval.value = null; + console.log('Интервал проверки Telegram авторизации очищен'); + } +}; + +// Метод для обновления отображения аутентификации +const updateAuthDisplay = (state) => { + console.log('Updating auth display:', state); + + // Обновляем отображение в зависимости от типа аутентификации + if (state.isAuthenticated) { + const authTypeLabels = { + 'wallet': 'Кошелек', + 'email': 'Email', + 'telegram': 'Telegram' + }; + + let authLabel = authTypeLabels[state.authType] || 'Аккаунт'; + let authValue = ''; + + if (state.authType === 'wallet' && state.address) { + authValue = truncateAddress(state.address); + // Добавляем класс для отображения подключенного состояния + document.body.classList.add('wallet-connected'); + } else if (state.authType === 'email' && state.email) { + authValue = state.email; + } else if (state.authType === 'telegram' && state.telegramUsername) { + authValue = state.telegramUsername; + } else if (state.authType === 'telegram' && state.telegramId) { + authValue = `ID: ${state.telegramId}`; + } + + // Обновляем текст в интерфейсе + const authDisplayEl = document.getElementById('auth-display'); + if (authDisplayEl) { + authDisplayEl.innerHTML = `${authLabel}: ${authValue}`; + authDisplayEl.style.display = 'inline-block'; + } + + // Скрываем кнопки авторизации + const authButtonsEl = document.getElementById('auth-buttons'); + if (authButtonsEl) { + authButtonsEl.style.display = 'none'; + } + + // Показываем кнопку выхода + const logoutButtonEl = document.getElementById('logout-button'); + if (logoutButtonEl) { + logoutButtonEl.style.display = 'inline-block'; + } + } else { + // Сбрасываем классы при отсутствии аутентификации + document.body.classList.remove('wallet-connected'); + + // Скрываем отображение аутентификации + const authDisplayEl = document.getElementById('auth-display'); + if (authDisplayEl) { + authDisplayEl.style.display = 'none'; + } + + // Показываем кнопки авторизации + const authButtonsEl = document.getElementById('auth-buttons'); + if (authButtonsEl) { + authButtonsEl.style.display = 'inline-block'; + } + + // Скрываем кнопку выхода + const logoutButtonEl = document.getElementById('logout-button'); + if (logoutButtonEl) { + logoutButtonEl.style.display = 'none'; + } + } +} + +// Генерация уникального ID с помощью времени и случайного числа +function generateUniqueId() { + return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; +} + +// Функция для отправки сообщения +const handleMessage = async (text) => { + if (!text.trim()) return; + + try { + // Добавляем сообщение в список + const userMessageContent = text.trim(); + const tempId = generateUniqueId(); + + const userMessage = { + id: tempId, + content: userMessageContent, + sender_type: 'user', + role: 'user', + isLocal: true, + isGuest: !auth.isAuthenticated.value, + timestamp: new Date().toISOString() + }; + + messages.value.push(userMessage); + + // Очищаем поле ввода + newMessage.value = ''; + + // Прокручиваем к последнему сообщению + scrollToBottom(); + + // Устанавливаем состояние загрузки + isLoading.value = true; + + try { + if (auth.isAuthenticated.value) { + // Отправляем сообщение как авторизованный пользователь + const response = await axios.post('/api/chat/message', { + message: userMessageContent, + language: userLanguage.value + }); + + if (response.data.success) { + // Обновляем ID сообщения пользователя + const userMsgIndex = messages.value.findIndex(m => m.id === tempId); + if (userMsgIndex !== -1) { + messages.value[userMsgIndex].id = response.data.userMessage.id; + messages.value[userMsgIndex].isLocal = false; + } + + // Добавляем ответ ИИ + messages.value.push({ + id: response.data.aiMessage.id, + content: response.data.aiMessage.content, + sender_type: 'assistant', + role: 'assistant', + timestamp: response.data.aiMessage.created_at + }); + + // Прокручиваем к последнему сообщению + scrollToBottom(); + } + } else { + // Отправляем сообщение как гость + console.log('Sending guest message:', userMessageContent); + + // Получаем или создаем идентификатор гостя + let guestId = getFromStorage('guestId'); + if (!guestId) { + guestId = generateUniqueId(); + setToStorage('guestId', guestId); + } + + const response = await axios.post('/api/chat/guest-message', { + content: userMessageContent, + guestId, + language: userLanguage.value + }); + + if (response.data.success) { + console.log('Guest message sent:', response.data); + + // Обновляем ID сообщения пользователя + const userMsgIndex = messages.value.findIndex(m => m.id === tempId); + if (userMsgIndex !== -1) { + messages.value[userMsgIndex].id = response.data.messageId; + messages.value[userMsgIndex].isLocal = false; + } + + // Сохраняем сообщение в localStorage + try { + const storedMessages = JSON.parse(getFromStorage('guestMessages', '[]')); + storedMessages.push({ + id: response.data.messageId, + content: userMessageContent, + sender_type: 'user', + role: 'user', + isGuest: true, + timestamp: new Date().toISOString() + }); + setToStorage('guestMessages', JSON.stringify(storedMessages)); + setToStorage('hasUserSentMessage', 'true'); + hasUserSentMessage.value = true; + } catch (storageError) { + console.error('Error saving message to localStorage:', storageError); + } + + // Показываем правую панель, если она скрыта + if (!showWalletSidebar.value) { + showWalletSidebar.value = true; + try { + setToStorage('showWalletSidebar', 'true'); + } catch (storageError) { + console.error('Error saving sidebar state to localStorage:', storageError); + } + } + } + } + } catch (error) { + console.error('Error sending message:', error); + // Помечаем сообщение как ошибочное + const userMsgIndex = messages.value.findIndex(m => m.id === tempId); + if (userMsgIndex !== -1) { + messages.value[userMsgIndex].hasError = true; + } + + // Добавляем сообщение об ошибке в чат + messages.value.push({ + id: `error-${Date.now()}`, + content: 'Произошла ошибка при отправке сообщения. Пожалуйста, попробуйте еще раз.', + sender_type: 'system', + role: 'system', + timestamp: new Date().toISOString() + }); + scrollToBottom(); + } finally { + isLoading.value = false; + } + } catch (error) { + console.error('Unexpected error in handleMessage:', error); + isLoading.value = false; + } +}; + +// Функция для прокрутки к последнему сообщению +const scrollToBottom = () => { + if (messagesContainer.value) { + setTimeout(() => { + messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight; + }, 100); + } +}; + // Обработка прокрутки const handleScroll = async () => { const element = messagesContainer.value; @@ -613,66 +1076,150 @@ const updateBalances = async () => { } }; -// Функция отмены Telegram аутентификации +// Функция для отмены аутентификации через Telegram const cancelTelegramAuth = () => { + // Очищаем интервал проверки + clearTelegramInterval(); + + // Сбрасываем состояния интерфейса showTelegramVerification.value = false; - if (telegramAuthCheckInterval.value) { - clearInterval(telegramAuthCheckInterval.value); - telegramAuthCheckInterval.value = null; - } + telegramVerificationCode.value = ''; + telegramError.value = ''; + + console.log('Аутентификация Telegram отменена пользователем'); }; // Вычисленное свойство для определения, нужно ли загружать историю const shouldLoadHistory = computed(() => { // Загружаем историю только если пользователь авторизован или есть гостевой ID return isAuthenticated.value || - (localStorage.getItem('guestId') && localStorage.getItem('guestId').length > 0); + (getFromStorage('guestId') && getFromStorage('guestId').length > 0); }); -// Следим за изменением авторизации -watch(() => auth.isAuthenticated.value, async (newValue, oldValue) => { - console.log('Auth state changed:', { - from: oldValue, - to: newValue, - userId: auth.userId.value - }); +// Метод для периодической проверки новых сообщений +const setupMessagePolling = (initialCount) => { + console.log(`Настраиваю отслеживание сообщений с начальным количеством: ${initialCount}`); + let messageCheckInterval; - if (newValue === true) { - // Если пользователь только что авторизовался - console.log('User authenticated, loading message history'); - - // Сначала связываем сообщения, затем загружаем историю - try { - // Сбрасываем текущие сообщения - messages.value = []; - offset.value = 0; - - // Сначала связываем гостевые сообщения - await auth.linkMessages(); - - // Затем загружаем историю сообщений после небольшой задержки, - // чтобы сервер успел обработать связанные сообщения - setTimeout(async () => { - await loadMoreMessages(true); - }, 1000); - } catch (error) { - console.error('Error loading history after auth:', error); + const clearMessagePolling = () => { + if (messageCheckInterval) { + clearInterval(messageCheckInterval); + messageCheckInterval = null; + console.log('Отслеживание сообщений остановлено'); } + }; + + // Очищаем предыдущий интервал, если он существует + clearMessagePolling(); + + // Создаем новый интервал для проверки сообщений + messageCheckInterval = setInterval(async () => { + try { + // Проверяем, есть ли новые сообщения + const newCountResponse = await axios.get('/api/chat/history?count_only=true'); + + if (newCountResponse.data.success) { + const newCount = newCountResponse.data.count; + console.log(`Проверка новых сообщений: ${newCount} / ${initialCount}`); + + if (newCount > initialCount) { + console.log(`Обнаружены новые сообщения: ${newCount} > ${initialCount}`); + + // Рассчитываем смещение для получения последних сообщений + const effectiveOffset = Math.max(0, newCount - limit.value); + + // Загружаем обновленную историю + const newResponse = await axios.get(`/api/chat/history?offset=${effectiveOffset}&limit=${limit.value}`); + + if (newResponse && newResponse.data.success) { + messages.value = newResponse.data.messages; + console.log('Сообщения обновлены с новыми ответами'); + + // Прокручиваем к последнему сообщению + await nextTick(); + scrollToBottom(); + + // Останавливаем интервал после получения ответа + clearMessagePolling(); + } + } + } + } catch (error) { + console.error('Ошибка при проверке новых сообщений:', error); + clearMessagePolling(); + } + }, 2000); // Проверяем каждые 2 секунды + + // Останавливаем интервал после 30 секунд в любом случае + setTimeout(() => { + clearMessagePolling(); + }, 30000); + + return clearMessagePolling; +}; + +// Отслеживаем изменения в сообщениях +watch(() => messages.value.length, (newLength, oldLength) => { + if (newLength > 0) { + // Сортируем сообщения по дате/времени + messages.value.sort((a, b) => { + const dateA = new Date(a.timestamp || a.created_at); + const dateB = new Date(b.timestamp || b.created_at); + return dateA - dateB; + }); + + // Прокручиваем к последнему сообщению + nextTick(() => { + scrollToBottom(); + }); } }); -// Функция для сокращения адреса кошелька -const truncateAddress = (address) => { - if (!address) return ''; - return `${address.slice(0, 6)}...${address.slice(-4)}`; -}; - -// Функция прокрутки к последнему сообщению -const scrollToBottom = () => { - if (messagesContainer.value) { - setTimeout(() => { - messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight; - }, 100); +// Функция для отключения кошелька/выхода +const disconnectWallet = async () => { + try { + console.log('Выполняется выход из системы...'); + + // Сохраняем гостевой ID для продолжения работы после выхода + const guestId = getFromStorage('guestId') || generateUniqueId(); + setToStorage('guestId', guestId); + + // Отправляем запрос на выход + await axios.post('/api/auth/logout'); + + // Обновляем состояние аутентификации локально + auth.isAuthenticated.value = false; + auth.address.value = null; + auth.authType.value = null; + auth.telegramId.value = null; + auth.email.value = null; + + // Обновляем отображение UI + document.body.classList.remove('wallet-connected'); + + // Очищаем историю сообщений, кроме гостевых + messages.value = []; + offset.value = 0; + hasMoreMessages.value = true; + + // Загружаем только гостевые сообщения после выхода + try { + // Проверяем наличие сообщений в localStorage + const storedMessages = getFromStorage('guestMessages'); + if (storedMessages) { + const parsedMessages = JSON.parse(storedMessages); + if (parsedMessages.length > 0) { + console.log(`Найдено ${parsedMessages.length} сохраненных гостевых сообщений`); + messages.value = [...messages.value, ...parsedMessages]; + } + } + } catch (e) { + console.error('Ошибка загрузки сообщений из localStorage:', e); + } + + console.log('Выход из системы выполнен успешно'); + } catch (error) { + console.error('Ошибка при выходе из системы:', error); } }; @@ -753,7 +1300,7 @@ const loadMoreMessages = async (silent = false) => { // После загрузки первой порции сообщений считаем, что пользователь уже отправлял сообщения if (messages.value.length > 0) { hasUserSentMessage.value = true; - localStorage.setItem('hasUserSentMessage', 'true'); + window.localStorage.setItem('hasUserSentMessage', 'true'); } // Прокручиваем контейнер с сообщениями вниз @@ -767,266 +1314,102 @@ const loadMoreMessages = async (silent = false) => { } }; -// Функция для подключения кошелька -const handleWalletAuth = async () => { - if (isConnecting.value || isAuthenticated.value) return; - - isConnecting.value = true; - try { - const result = await connectWithWallet(); - console.log('Wallet connection result:', result); - - if (result.success) { - // Обновляем состояние авторизации - await auth.checkAuth(); - - // Добавляем небольшую задержку перед сбросом состояния isConnecting - setTimeout(() => { - isConnecting.value = false; - }, 500); - return; - } else { - console.error('Failed to connect wallet:', result.error); - } - } catch (error) { - console.error('Error connecting wallet:', error); - } - - isConnecting.value = false; +// Функция для отмены Email аутентификации +const cancelEmailAuth = () => { + showEmailForm.value = false; + showEmailVerificationInput.value = false; + showEmailVerification.value = false; + emailInput.value = ''; + emailVerificationCode.value = ''; + emailError.value = ''; + emailFormatError.value = false; }; -// Функция для отключения кошелька/выхода -const disconnectWallet = async () => { - try { - await axios.post('/api/auth/logout'); - auth.isAuthenticated.value = false; - auth.address.value = null; - auth.authType.value = null; - auth.telegramId = null; - auth.email = null; - - // Загружаем только гостевые сообщения после выхода - messages.value = []; - offset.value = 0; - hasMoreMessages.value = true; - await loadMoreMessages(); - - // НЕ перезагружаем страницу, чтобы не потерять историю сообщений - // window.location.reload(); - } catch (error) { - console.error('Error disconnecting wallet:', error); - } -}; - -// Форматирование времени -const formatTime = (timestamp) => { - if (!timestamp) return ''; - - try { - const date = new Date(timestamp); - - // Проверяем, является ли дата валидной - if (isNaN(date.getTime())) { - console.warn('Invalid timestamp:', timestamp); - return ''; - } - - // Форматируем дату с указанием дня, месяца, года и времени - return date.toLocaleString([], { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - }); - } catch (error) { - console.error('Error formatting time:', error, timestamp); - return ''; - } -}; - -// Форматирование сообщения с поддержкой markdown -const formatMessage = (text) => { - if (!text) return ''; - const rawHtml = marked.parse(text); - return DOMPurify.sanitize(rawHtml); -}; - -// Проверяет наличие гостевых сообщений -const checkGuestMessages = async () => { - try { - // Проверяем сессию через auth/check вместо chat/check-session - const sessionCheck = await axios.get('/api/auth/check'); - console.log('Session auth check response:', sessionCheck.data); - - // Проверяем наличие сообщений в localStorage - const storedMessages = localStorage.getItem('guestMessages'); - if (storedMessages) { - const parsedMessages = JSON.parse(storedMessages); - if (Array.isArray(parsedMessages) && parsedMessages.length > 0) { - // Если есть сообщения и пользователь не аутентифицирован, показываем их - if (!auth.isAuthenticated.value) { - console.log('Found guest messages in localStorage:', parsedMessages); - messages.value = [...messages.value, ...parsedMessages]; - hasUserSentMessage.value = true; - localStorage.setItem('hasUserSentMessage', 'true'); - } else { - // Если пользователь аутентифицирован, удаляем гостевые сообщения из localStorage - console.log('User is authenticated, removing guest messages from localStorage'); - localStorage.removeItem('guestMessages'); - } - } - } - } catch (error) { - console.error('Error checking guest messages:', error); - } -}; - -// Инициализация состояния при загрузке +// Функции жизненного цикла onMounted(async () => { - // Загружаем состояние правой панели из localStorage - const savedSidebarState = localStorage.getItem('showWalletSidebar'); - if (savedSidebarState !== null) { - showWalletSidebar.value = savedSidebarState === 'true'; - } else { - // По умолчанию правая панель скрыта - showWalletSidebar.value = false; - } + console.log('HomeView.vue: компонент загружен'); - // Добавляем слушатель прокрутки - if (messagesContainer.value) { - messagesContainer.value.addEventListener('scroll', handleScroll); - } - - console.log('Auth state on mount:', { + // Проверяем состояние аутентификации + console.log('Состояние аутентификации при загрузке:', { isAuthenticated: auth.isAuthenticated.value, authType: auth.authType.value, telegramId: auth.telegramId.value }); - - // Проверяем статус авторизации - await auth.checkAuth(); - - // Проверяем наличие гостевых сообщений и инициализируем сессию - await checkGuestMessages(); - - // Обновляем баланс при монтировании если авторизован - if (auth.isAuthenticated.value) { - updateBalances(); - } -}); -watch(() => auth.isAuthenticated.value, async (newValue) => { - if (newValue) { - await updateBalances(); + // Загружаем сохраненное состояние боковой панели + const savedSidebarState = getFromStorage('showWalletSidebar'); + if (savedSidebarState !== null) { + showWalletSidebar.value = savedSidebarState === 'true'; } else { - tokenBalances.value = { - eth: '0', - bsc: '0', - arbitrum: '0', - polygon: '0' - }; - } -}); - -// Отслеживаем изменения в идентификаторах -watch(() => auth.telegramId?.value, (newValue) => { - if (newValue) { - console.log('Telegram ID изменился:', newValue); - } -}); - -// Отслеживаем успешную авторизацию через Telegram -watch(() => auth.authType?.value, (newValue) => { - if (newValue === 'telegram') { - console.log('Авторизация через Telegram завершена, authType:', newValue); - console.log('Текущий telegramId:', auth.telegramId?.value); + // По умолчанию показываем панель, если не указано иное + showWalletSidebar.value = true; + setToStorage('showWalletSidebar', 'true'); } + + // Запускаем отслеживание изменений + watchAuthChanges(); + + // Установка обработчика скролла + if (messagesContainer.value) { + messagesContainer.value.addEventListener('scroll', handleScroll); + } + + // Загружаем историю сообщений + if (shouldLoadHistory.value) { + // Получаем сессию пользователя + const { data: sessionData } = await api.get('/api/auth/check'); + console.log('Проверка сессии:', sessionData); + + if (!isAuthenticated.value && !sessionData.authenticated) { + // Пробуем загрузить локальные сообщения + try { + // Проверяем наличие сообщений в localStorage + const storedMessages = getFromStorage('guestMessages'); + if (storedMessages) { + const parsedMessages = JSON.parse(storedMessages); + if (parsedMessages.length > 0) { + console.log(`Найдено ${parsedMessages.length} сохраненных гостевых сообщений`); + + // Если пользователь не аутентифицирован, добавляем гостевые сообщения + if (!isAuthenticated.value) { + messages.value = [...messages.value, ...parsedMessages]; + hasUserSentMessage.value = true; + setToStorage('hasUserSentMessage', 'true'); + } else { + // Если пользователь аутентифицирован, удаляем гостевые сообщения из localStorage + console.log('Пользователь аутентифицирован, удаляем гостевые сообщения из localStorage'); + removeFromStorage('guestMessages'); + } + } + } + } catch (e) { + console.error('Ошибка загрузки сообщений из localStorage:', e); + } + } + + if (isAuthenticated.value) { + await loadMoreMessages(); + } + } + + // Добавляем слушатель события для загрузки истории чата + window.addEventListener('load-chat-history', loadChatHistory); + + // Установка статуса отправленных сообщений + if (messages.value.length > 0) { + hasUserSentMessage.value = true; + setToStorage('hasUserSentMessage', 'true'); + } + + // Прокручиваем к последнему сообщению + scrollToBottom(); }); +// Очистка слушателей событий при размонтировании компонента onBeforeUnmount(() => { - // Удаляем слушатель if (messagesContainer.value) { messagesContainer.value.removeEventListener('scroll', handleScroll); } - if (telegramAuthCheckInterval.value) { - clearInterval(telegramAuthCheckInterval.value); - } - document.querySelector('.app-container')?.classList.remove('menu-open'); -}); - -// Обработчик для Telegram аутентификации -const handleTelegramAuth = async () => { - try { - const { data } = await axios.post('/api/auth/telegram/init'); - const { verificationCode, botLink } = data; - - // Показываем код верификации - showTelegramVerification.value = true; - telegramVerificationCode.value = verificationCode; - telegramBotLink.value = botLink; - - // Запускаем проверку статуса аутентификации - telegramAuthCheckInterval.value = setInterval(async () => { - try { - const response = await axios.get('/api/auth/check'); - console.log('Проверка авторизации:', response.data); - - if (response.data.authenticated && response.data.telegramId) { - clearInterval(telegramAuthCheckInterval.value); - telegramAuthCheckInterval.value = null; - showTelegramVerification.value = false; - - console.log('Telegram ID получен:', response.data.telegramId); - - // Обновляем информацию об авторизации через метод updateAuth - auth.updateAuth({ - authenticated: true, - authType: response.data.authType, - userId: response.data.userId, - telegramId: response.data.telegramId, - isAdmin: response.data.isAdmin - }); - - // Сначала обновляем идентификаторы, затем связываем сообщения и обновляем историю - await auth.checkAuth(); - - // Весь процесс загрузки истории будет выполнен через watch на isAuthenticated - } - } catch (error) { - console.error('Error checking auth status:', error); - } - }, 2000); - - // Очищаем интервал через 5 минут - setTimeout(() => { - if (telegramAuthCheckInterval.value) { - clearInterval(telegramAuthCheckInterval.value); - telegramAuthCheckInterval.value = null; - showTelegramVerification.value = false; - } - }, 5 * 60 * 1000); - - } catch (error) { - console.error('Error initializing Telegram auth:', error); - alert('Ошибка при инициализации Telegram аутентификации'); - } -}; - -// Отслеживаем изменения в сообщениях -watch(() => messages.value.length, (newLength, oldLength) => { - if (newLength > 0) { - // Сортируем сообщения по дате/времени - messages.value.sort((a, b) => { - const dateA = new Date(a.timestamp || a.created_at); - const dateB = new Date(b.timestamp || b.created_at); - return dateA - dateB; - }); - - // Прокручиваем к последнему сообщению - nextTick(() => { - scrollToBottom(); - }); - } + window.removeEventListener('load-chat-history', loadChatHistory); });