diff --git a/backend/db/migrations/011_verification_codes.sql b/backend/db/migrations/012_verification_codes.sql similarity index 90% rename from backend/db/migrations/011_verification_codes.sql rename to backend/db/migrations/012_verification_codes.sql index 2e9d6ff..621f4a9 100644 --- a/backend/db/migrations/011_verification_codes.sql +++ b/backend/db/migrations/012_verification_codes.sql @@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS verification_codes ( code VARCHAR(6) NOT NULL, provider VARCHAR(50) NOT NULL, -- 'telegram', 'email' provider_id VARCHAR(255) NOT NULL, -- telegram_id или email - user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + user_id INTEGER NULL REFERENCES users(id) ON DELETE CASCADE, -- Может быть NULL created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP NOT NULL, used BOOLEAN DEFAULT FALSE diff --git a/backend/db/migrations/013_update_verification_codes.sql b/backend/db/migrations/013_update_verification_codes.sql new file mode 100644 index 0000000..4614628 --- /dev/null +++ b/backend/db/migrations/013_update_verification_codes.sql @@ -0,0 +1,12 @@ +-- Изменяем ограничение для поля user_id в таблице verification_codes +ALTER TABLE verification_codes + ALTER COLUMN user_id DROP NOT NULL; + +-- Обновляем комментарий в информационной схеме +COMMENT ON COLUMN verification_codes.user_id IS 'ID пользователя (может быть NULL для временных кодов)'; + +-- Логирование для отслеживания выполнения миграции +DO $$ +BEGIN + RAISE NOTICE 'Migration 012: Updated verification_codes table to allow NULL values for user_id'; +END $$; \ No newline at end of file diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 430f49e..5af6085 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -17,6 +17,7 @@ const { initTelegramAuth } = require('../services/telegramBot'); const emailAuth = require('../services/emailAuth'); const verificationService = require('../services/verification-service'); const { processGuestMessages } = require('./chat'); // Импортируем функцию обработки гостевых сообщений +const nonceStore = {}; // Создайте лимитер для попыток аутентификации const authLimiter = rateLimit({ @@ -221,138 +222,89 @@ router.post('/verify', async (req, res) => { // Аутентификация через Telegram router.post('/telegram/verify', async (req, res) => { try { - const { code } = req.body; + const { telegramId, verificationCode } = req.body; - logger.info(`[telegram/verify] Verifying code: ${code}`); - - // Проверяем код верификации - const verificationResult = await verifyTelegramCode(code); - - if (!verificationResult.success) { - return res.status(400).json({ - success: false, - error: 'Неверный код подтверждения' + if (!telegramId || !verificationCode) { + return res.status(400).json({ + success: false, + error: 'Missing required fields' }); } - - const { telegramId, authData } = verificationResult; - logger.info(`[telegram/verify] Code verified successfully for telegramId: ${telegramId}`); - - // Сохраняем гостевые ID до проверки - const guestId = req.session.guestId; - - let userId; - - // Если пользователь уже аутентифицирован, добавляем Telegram к существующему аккаунту - if (req.session.authenticated && req.session.userId) { - userId = req.session.userId; - - // Проверяем не связан ли Telegram с другим аккаунтом - const existingTelegram = await db.query(` - SELECT user_id FROM user_identities - WHERE provider = 'telegram' AND provider_id = $1 - `, [telegramId]); - - if (existingTelegram.rows.length > 0) { - return res.status(400).json({ - success: false, - error: 'Этот Telegram аккаунт уже связан с другим пользователем' - }); - } - - // Добавляем Telegram к существующему пользователю - await saveUserIdentity(userId, 'telegram', telegramId, true); - logger.info(`[telegram/verify] Added Telegram identity ${telegramId} to existing user ${userId}`); - - } else { - // Ищем существующего пользователя по Telegram ID - const existingUser = await db.query(` - SELECT u.* FROM users u - JOIN user_identities ui ON u.id = ui.user_id - WHERE ui.provider = 'telegram' AND ui.provider_id = $1 - `, [telegramId]); - - if (existingUser.rows.length > 0) { - // Используем существующего пользователя - userId = existingUser.rows[0].id; - logger.info(`[telegram/verify] Found existing user with ID ${userId} for telegramId ${telegramId}`); - } else { - // Создаем нового пользователя - const newUser = await db.query( - 'INSERT INTO users (role) VALUES ($1) RETURNING id', - ['user'] - ); - userId = newUser.rows[0].id; - - // Добавляем Telegram идентификатор - await saveUserIdentity(userId, 'telegram', telegramId, true); - logger.info(`[telegram/verify] Created new user with ID ${userId} for telegramId ${telegramId}`); - } - } - // Если есть гостевые сообщения, переносим их - if (guestId && !req.session.processedGuestIds?.includes(guestId)) { - await processGuestMessages(userId, guestId); - // Сохраняем обработанный guestId чтобы избежать повторной обработки - if (!req.session.processedGuestIds) { - req.session.processedGuestIds = []; - } - req.session.processedGuestIds.push(guestId); - logger.info(`[telegram/verify] Processed guest messages for user ${userId} with guest ID ${guestId}`); - } + logger.info(`[telegram/verify] Verifying Telegram auth for ID: ${telegramId}`); + + // Сохраняем гостевой ID из текущей сессии + const guestId = req.session.guestId; - // Создаем новую сессию + // Передаем сессию в метод верификации + const verificationResult = await authService.verifyTelegramAuth( + telegramId, + verificationCode, + req.session + ); + + if (!verificationResult.success) { + return res.status(400).json({ + success: false, + error: verificationResult.error || 'Verification failed' + }); + } + + // Создаем новую сессию для этого telegramId req.session.regenerate(async (err) => { if (err) { - logger.error('Error regenerating session:', err); - return res.status(500).json({ success: false, error: 'Server error' }); + logger.error('[telegram/verify] Error regenerating session:', err); + return res.status(500).json({ + success: false, + error: 'Session regeneration failed' + }); } - - // Устанавливаем данные новой сессии - req.session.authenticated = true; - req.session.userId = userId; + + // Устанавливаем данные в новой сессии + req.session.userId = verificationResult.userId; req.session.telegramId = telegramId; req.session.authType = 'telegram'; + req.session.authenticated = true; + req.session.role = verificationResult.role; - // Сохраняем список обработанных гостевых ID - if (req.session.processedGuestIds?.length > 0) { - req.session.processedGuestIds = [...req.session.processedGuestIds]; + // Восстанавливаем гостевой ID, если он был + if (guestId) { + req.session.guestId = guestId; } - - // Удаляем временные данные - delete req.session.tempUserId; - delete req.session.guestId; - delete req.session.pendingTelegramId; - - if (authData.first_name) { - req.session.telegramFirstName = authData.first_name; - } - if (authData.username) { - req.session.telegramUsername = authData.username; - } - + // Сохраняем сессию - req.session.save((err) => { - if (err) { - logger.error('Error saving session:', err); - return res.status(500).json({ success: false, error: 'Server error' }); - } - - res.json({ - success: true, - userId, - telegramId, - authenticated: true, - authType: 'telegram', - username: authData.username, - firstName: authData.first_name + await new Promise((resolve, reject) => { + req.session.save(err => { + if (err) { + logger.error('[telegram/verify] Error saving session:', err); + reject(err); + } else { + logger.info(`[telegram/verify] Session saved for user ${verificationResult.userId}`); + resolve(); + } }); }); + + // Связываем гостевые сообщения только если это новый пользователь + if (verificationResult.isNewUser && guestId) { + const linkResults = await authService.linkGuestMessagesAfterAuth(verificationResult.userId, guestId); + logger.info(`[telegram/verify] Guest messages linking results for new user:`, linkResults); + } + + return res.json({ + success: true, + userId: verificationResult.userId, + role: verificationResult.role, + telegramId, + isNewUser: verificationResult.isNewUser + }); }); - } catch (error) { logger.error('[telegram/verify] Error:', error); - res.status(500).json({ success: false, error: 'Server error' }); + return res.status(500).json({ + success: false, + error: 'Internal server error' + }); } }); @@ -729,29 +681,46 @@ router.get('/check', async (req, res) => { }); // Выход из системы -router.post('/logout', (req, res) => { +router.post('/logout', async (req, res) => { try { - // Сохраняем важные данные перед уничтожением сессии - const guestId = req.session.guestId; - - // Полностью уничтожаем сессию - req.session.destroy((err) => { - if (err) { - logger.error('Error destroying session:', err); - return res.status(500).json({ success: false, error: 'Server error' }); - } - - // Очищаем куки сессии - res.clearCookie('connect.sid'); - - res.json({ - success: true, - guestId // Возвращаем guestId для фронтенда + // Очищаем все идентификаторы сессии + req.session.authenticated = false; + req.session.userId = null; + req.session.address = null; + req.session.telegramId = null; + req.session.email = null; + req.session.isAdmin = false; + req.session.guestId = null; + req.session.previousGuestId = null; + req.session.processedGuestIds = []; + req.session.pendingEmail = null; + req.session.authType = null; + + // Сохраняем изменения в сессии + await new Promise((resolve, reject) => { + req.session.save(err => { + if (err) { + logger.error('[logout] Error saving session:', err); + reject(err); + } else { + logger.info('[logout] Session cleared successfully'); + resolve(); + } }); }); + + // Уничтожаем сессию полностью + req.session.destroy((err) => { + if (err) { + logger.error('[logout] Error destroying session:', err); + return res.status(500).json({ success: false, error: 'Error during logout' }); + } + res.clearCookie('connect.sid'); + res.json({ success: true, message: 'Logged out successfully' }); + }); } catch (error) { - logger.error('Error in logout:', error); - res.status(500).json({ success: false, error: 'Server error' }); + logger.error('[logout] Error:', error); + res.status(500).json({ success: false, error: 'Internal server error during logout' }); } }); @@ -773,30 +742,20 @@ router.get('/telegram', (req, res) => { // Маршрут для получения кода подтверждения Telegram router.get('/telegram/code', authLimiter, async (req, res) => { try { - // Создаем или получаем ID пользователя - let userId; + // Создаем код через сервис телеграм авторизации + const authData = await initTelegramAuth(req.session); - if (req.session.authenticated && req.session.userId) { - userId = req.session.userId; - } else { - const userResult = await db.query( - 'INSERT INTO users (created_at) VALUES (NOW()) RETURNING id' - ); - userId = userResult.rows[0].id; - req.session.tempUserId = userId; + if (!authData.verificationCode) { + return res.status(500).json({ + success: false, + error: 'Failed to generate verification code' + }); } - // Создаем код через сервис верификации - const code = await verificationService.createVerificationCode( - 'telegram', - req.session.guestId || 'temp', - userId - ); - res.json({ success: true, message: 'Отправьте этот код боту @' + process.env.TELEGRAM_BOT_USERNAME, - code, + code: authData.verificationCode, botUsername: process.env.TELEGRAM_BOT_USERNAME || 'YourDAppBot' }); } catch (error) { diff --git a/backend/services/auth-service.js b/backend/services/auth-service.js index 7896dd6..cd52df0 100644 --- a/backend/services/auth-service.js +++ b/backend/services/auth-service.js @@ -393,60 +393,63 @@ class AuthService { /** * Проверка Telegram аутентификации */ - async verifyTelegramAuth(telegramId, verificationCode) { + async verifyTelegramAuth(telegramId, verificationCode, session) { try { - logger.info(`Verifying Telegram auth for ID: ${telegramId} with code: ${verificationCode}`); + logger.info(`[verifyTelegramAuth] Starting for telegramId: ${telegramId}`); - // Находим или создаем пользователя - const userResult = await db.query( - `SELECT u.* FROM users u + // Проверяем, существует ли уже пользователь с таким Telegram ID + const existingUserResult = await db.query( + `SELECT u.*, ui.provider, ui.provider_id + FROM users u JOIN user_identities ui ON u.id = ui.user_id WHERE ui.provider = 'telegram' AND ui.provider_id = $1`, [telegramId] ); - + let userId; - if (userResult.rows.length > 0) { - userId = userResult.rows[0].id; - logger.info(`Found existing user ${userId} for Telegram ID ${telegramId}`); + let isNewUser = false; + + // Если пользователь существует с таким telegramId, используем его + if (existingUserResult.rows.length > 0) { + const existingUser = existingUserResult.rows[0]; + userId = existingUser.id; + logger.info(`[verifyTelegramAuth] Found existing user ${userId} for Telegram ID ${telegramId}`); } else { - // Создаем нового пользователя с ролью user + // Создаем нового пользователя для нового telegramId const newUserResult = await db.query( 'INSERT INTO users (role) VALUES ($1) RETURNING id', ['user'] ); userId = newUserResult.rows[0].id; + isNewUser = true; // Добавляем Telegram идентификатор await db.query( 'INSERT INTO user_identities (user_id, provider, provider_id) VALUES ($1, $2, $3)', [userId, 'telegram', telegramId] ); - logger.info(`Created new user ${userId} for Telegram ID ${telegramId}`); + + logger.info(`[verifyTelegramAuth] Created new user ${userId} for Telegram ID ${telegramId}`); } - - // Проверяем наличие кошелька и определяем роль - const wallet = await this.getLinkedWallet(userId); - let role = 'user'; // Базовая роль для доступа к чату - - if (wallet) { - // Если есть кошелек, проверяем баланс токенов - const isAdmin = await this.checkAdminRole(wallet); - role = isAdmin ? 'admin' : 'user'; - logger.info(`User ${userId} has wallet ${wallet}, role set to ${role}`); - } else { - logger.info(`User ${userId} has no wallet, using basic user role`); + + // Если есть гостевой ID в сессии, сохраняем его для нового пользователя + if (session.guestId && isNewUser) { + await db.query( + 'INSERT INTO user_identities (user_id, provider, provider_id) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING', + [userId, 'guest', session.guestId] + ); + logger.info(`[verifyTelegramAuth] Saved guest ID ${session.guestId} for user ${userId}`); } - + return { success: true, userId, - role, + role: 'user', telegramId, - wallet: wallet || null + isNewUser }; } catch (error) { - logger.error('Error in Telegram auth verification:', error); + logger.error('[verifyTelegramAuth] Error:', error); throw error; } } @@ -574,68 +577,104 @@ class AuthService { 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', + // Проверяем, не привязаны ли уже эти гостевые сообщения к другому пользователю + const existingMessagesCheck = await db.query( + `SELECT DISTINCT user_id + FROM messages + WHERE guest_message_id IN ( + SELECT id FROM guest_messages WHERE guest_id = $1 + )`, [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 + if (existingMessagesCheck.rows.length > 0) { + const existingUserId = existingMessagesCheck.rows[0].user_id; + if (existingUserId !== userId) { + logger.warn(`[linkGuestMessagesAfterAuth] Guest messages for ${currentGuestId} are already linked to user ${existingUserId}`); + return { + success: false, + error: 'Guest messages are already linked to another user' + }; } - }; + } + + // Блокируем таблицу guest_messages для атомарной операции + await db.query('BEGIN'); + + try { + // Получаем все гостевые сообщения для этого ID + const guestMessagesResult = await db.query( + 'SELECT * FROM guest_messages WHERE guest_id = $1 ORDER BY created_at ASC FOR UPDATE', + [currentGuestId] + ); + + if (guestMessagesResult.rows.length === 0) { + await db.query('COMMIT'); + 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, user_id) + VALUES + ($1, $2, $3, $4, $5, $6, $7, $8)`, + [ + conversation.id, + guestMessage.content, + guestMessage.is_ai ? 'assistant' : 'user', + guestMessage.is_ai ? 'assistant' : 'user', + 'web', + guestMessage.id, + guestMessage.created_at, + userId + ] + ); + } + + // Удаляем обработанные гостевые сообщения + await db.query('DELETE FROM guest_messages WHERE guest_id = $1', [currentGuestId]); + + // Удаляем гостевой идентификатор + await db.query( + 'DELETE FROM user_identities WHERE user_id = $1 AND provider = $2 AND provider_id = $3', + [userId, 'guest', currentGuestId] + ); + + await db.query('COMMIT'); + logger.info(`[linkGuestMessagesAfterAuth] Successfully processed guest ID ${currentGuestId}`); + + return { + success: true, + result: { + conversationId: conversation.id, + message: `Processed ${guestMessages.length} guest messages`, + success: true + } + }; + } catch (error) { + await db.query('ROLLBACK'); + throw error; + } } catch (error) { logger.error('[linkGuestMessagesAfterAuth] Error:', error); throw error; diff --git a/backend/services/telegramBot.js b/backend/services/telegramBot.js index 34475fe..5083c80 100644 --- a/backend/services/telegramBot.js +++ b/backend/services/telegramBot.js @@ -3,6 +3,7 @@ const logger = require('../utils/logger'); const db = require('../db'); const authService = require('./auth-service'); const verificationService = require('./verification-service'); +const crypto = require('crypto'); let botInstance = null; @@ -38,7 +39,7 @@ async function getBot() { const verification = codeResult.rows[0]; const providerId = verification.provider_id; - let userId = verification.user_id; + let userId; // Отмечаем код как использованный await db.query( @@ -57,40 +58,37 @@ async function getBot() { ); if (existingTelegramUser.rows.length > 0) { - // Если пользователь с таким Telegram ID уже существует, - // используем его ID вместо создания нового связывания - const existingUserId = existingTelegramUser.rows[0].user_id; + // Если пользователь с таким Telegram ID уже существует, используем его + userId = existingTelegramUser.rows[0].user_id; + logger.info(`Using existing user ${userId} for Telegram account ${ctx.from.id}`); + } else { + // Создаем нового пользователя, если нет существующего с этим Telegram ID + const userResult = await db.query( + 'INSERT INTO users (created_at, role) VALUES (NOW(), $1) RETURNING id', + ['user'] + ); + userId = userResult.rows[0].id; - // Связываем гостевой ID с существующим пользователем, если его еще нет - const guestIdentity = await db.query( - `SELECT * FROM user_identities - WHERE user_id = $1 AND provider = 'guest' AND provider_id = $2`, - [existingUserId, providerId] + // Связываем Telegram с новым пользователем + await db.query( + `INSERT INTO user_identities + (user_id, provider, provider_id, created_at) + VALUES ($1, $2, $3, NOW())`, + [userId, 'telegram', ctx.from.id.toString()] ); - if (guestIdentity.rows.length === 0 && providerId) { + // Если был гостевой ID, связываем его с новым пользователем + if (providerId) { await db.query( `INSERT INTO user_identities (user_id, provider, provider_id, created_at) VALUES ($1, $2, $3, NOW()) - ON CONFLICT (provider, provider_id) DO UPDATE SET user_id = $1`, - [existingUserId, 'guest', providerId] + ON CONFLICT (provider, provider_id) DO NOTHING`, + [userId, 'guest', providerId] ); } - userId = existingUserId; - logger.info(`Using existing user ${userId} for Telegram account ${ctx.from.id}`); - } else { - // Связываем Telegram с пользователем - await db.query( - `INSERT INTO user_identities - (user_id, provider, provider_id, created_at) - VALUES ($1, $2, $3, NOW()) - ON CONFLICT (provider, provider_id) DO UPDATE SET user_id = $1`, - [userId, 'telegram', ctx.from.id.toString()] - ); - - logger.info(`User ${userId} successfully linked Telegram account ${ctx.from.id}`); + logger.info(`Created new user ${userId} with Telegram account ${ctx.from.id}`); } // Обновляем сессию в базе данных @@ -149,58 +147,19 @@ async function stopBot() { // Инициализация процесса аутентификации async function initTelegramAuth(session) { try { - // Создаем или получаем ID пользователя - let userId; + // Используем временный идентификатор для создания кода верификации + // Реальный пользователь будет создан или найден при проверке кода через бота + const tempId = crypto.randomBytes(16).toString('hex'); - if (session.authenticated && session.userId) { - // Если пользователь уже аутентифицирован, используем его ID - userId = session.userId; - } else if (session.guestId) { - // Проверяем, есть ли уже пользователь с этим guestId - const existingUser = await db.query( - `SELECT u.id - FROM users u - JOIN user_identities ui ON u.id = ui.user_id - WHERE ui.provider = 'guest' AND ui.provider_id = $1`, - [session.guestId] - ); - - if (existingUser.rows.length > 0) { - // Используем существующего пользователя - userId = existingUser.rows[0].id; - } else { - // Создаем нового пользователя - const userResult = await db.query( - 'INSERT INTO users (created_at) VALUES (NOW()) RETURNING id' - ); - userId = userResult.rows[0].id; - - // Связываем гостевой ID с пользователем - await db.query( - `INSERT INTO user_identities - (user_id, provider, provider_id, created_at) - VALUES ($1, $2, $3, NOW())`, - [userId, 'guest', session.guestId] - ); - } - - session.tempUserId = userId; - } else { - // Создаем нового пользователя без гостевого ID - const userResult = await db.query( - 'INSERT INTO users (created_at) VALUES (NOW()) RETURNING id' - ); - userId = userResult.rows[0].id; - session.tempUserId = userId; - } - - // Создаем код через сервис верификации + // Создаем код через сервис верификации с временным идентификатором const code = await verificationService.createVerificationCode( 'telegram', - session.guestId || 'temp', - userId + session.guestId || tempId, + null // Не привязываем к конкретному userId на этом этапе ); + logger.info(`[initTelegramAuth] Created verification code for guestId: ${session.guestId || tempId}`); + return { verificationCode: code, botLink: `https://t.me/${process.env.TELEGRAM_BOT_USERNAME}` diff --git a/backend/services/verification-service.js b/backend/services/verification-service.js index 9715367..dcded02 100644 --- a/backend/services/verification-service.js +++ b/backend/services/verification-service.js @@ -20,14 +20,24 @@ class VerificationService { const expiresAt = new Date(Date.now() + this.expirationMinutes * 60 * 1000); try { - logger.info(`Creating verification code for ${provider}:${providerId}, userId: ${userId}`); + logger.info(`Creating verification code for ${provider}:${providerId}, userId: ${userId || 'null'}`); - await db.query( - `INSERT INTO verification_codes - (code, provider, provider_id, user_id, expires_at) - VALUES ($1, $2, $3, $4, $5)`, - [code, provider, providerId, userId, expiresAt] - ); + // Если userId не указан, добавляем запись без ссылки на пользователя + if (userId === null || userId === undefined) { + await db.query( + `INSERT INTO verification_codes + (code, provider, provider_id, expires_at) + VALUES ($1, $2, $3, $4)`, + [code, provider, providerId, expiresAt] + ); + } else { + await db.query( + `INSERT INTO verification_codes + (code, provider, provider_id, user_id, expires_at) + VALUES ($1, $2, $3, $4, $5)`, + [code, provider, providerId, userId, expiresAt] + ); + } logger.info(`Verification code created successfully for ${provider}:${providerId}`); return code; diff --git a/backend/update_verification_table.js b/backend/update_verification_table.js new file mode 100644 index 0000000..746d754 --- /dev/null +++ b/backend/update_verification_table.js @@ -0,0 +1,77 @@ +// Скрипт для обновления таблицы verification_codes +const { Pool } = require('pg'); +require('dotenv').config(); + +// Создаем подключение к базе данных +const pool = new Pool({ + host: process.env.POSTGRES_HOST || 'localhost', + port: process.env.POSTGRES_PORT || 5432, + database: process.env.POSTGRES_DB || 'dapp_business', + user: process.env.POSTGRES_USER || 'postgres', + password: process.env.POSTGRES_PASSWORD || 'postgres', +}); + +async function updateVerificationTable() { + try { + console.log('Начинаем обновление таблицы verification_codes...'); + + // Проверяем, существует ли таблица + const checkTableResult = await pool.query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = 'verification_codes' + ); + `); + + const tableExists = checkTableResult.rows[0].exists; + + if (!tableExists) { + console.log('Таблица verification_codes не существует. Пропускаем обновление.'); + return; + } + + // Проверяем, разрешает ли уже колонка null значения + const checkColumnResult = await pool.query(` + SELECT is_nullable + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'verification_codes' + AND column_name = 'user_id'; + `); + + if (checkColumnResult.rows.length > 0 && checkColumnResult.rows[0].is_nullable === 'YES') { + console.log('Колонка user_id уже разрешает NULL значения. Пропускаем обновление.'); + return; + } + + // Начинаем транзакцию + await pool.query('BEGIN'); + + // Изменяем ограничение для поля user_id + await pool.query(` + ALTER TABLE verification_codes + ALTER COLUMN user_id DROP NOT NULL; + `); + + // Добавляем комментарий к колонке + await pool.query(` + COMMENT ON COLUMN verification_codes.user_id IS 'ID пользователя (может быть NULL для временных кодов)'; + `); + + // Фиксируем транзакцию + await pool.query('COMMIT'); + + console.log('Таблица verification_codes успешно обновлена!'); + } catch (error) { + // Откатываем транзакцию в случае ошибки + await pool.query('ROLLBACK'); + console.error('Ошибка при обновлении таблицы verification_codes:', error); + } finally { + // Закрываем соединение с базой данных + await pool.end(); + } +} + +// Выполняем обновление +updateVerificationTable(); \ No newline at end of file diff --git a/frontend/src/assets/styles/home.css b/frontend/src/assets/styles/home.css index aecc817..de9e657 100644 --- a/frontend/src/assets/styles/home.css +++ b/frontend/src/assets/styles/home.css @@ -3,6 +3,13 @@ font-family: 'Roboto', 'Helvetica Neue', Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + background-color: #fff; } /* Стили для монопространственных шрифтов (код, верификация) */ @@ -35,10 +42,9 @@ input, textarea { .app-container { display: flex; - height: 100vh; - width: 100%; - background-color: #f5f5f5; - position: relative; + flex-direction: column; + min-height: 100vh; + background-color: #fff; } /* Стили для боковой панели */ @@ -160,14 +166,10 @@ input, textarea { flex: 1; display: flex; flex-direction: column; - margin-left: 190px; /* 40px + 110px (sidebar) + 40px (button) */ - margin-right: 190px; /* 40px + 110px (sidebar) + 40px (button) */ - transition: margin 0.3s ease; max-width: 1200px; + margin: 0 auto; padding: 0 20px; - height: 100vh; - position: relative; - box-sizing: border-box; + width: 100%; } .sidebar-expanded ~ .main-content { @@ -188,27 +190,18 @@ input, textarea { flex: 1; display: flex; flex-direction: column; - width: 100%; - height: calc(100vh - 140px); - position: relative; - box-sizing: border-box; + margin: 20px 0; + min-height: 0; } .chat-messages { flex: 1; overflow-y: auto; - background-color: white; - border-radius: 8px; padding: 20px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); + background: #fff; + border-radius: 8px; + border: 1px solid #e0e0e0; margin-bottom: 20px; - width: 100%; - box-sizing: border-box; - position: absolute; - top: 0; - bottom: 120px; /* Увеличиваем отступ для возможного расширения chat-input */ - left: 0; - right: 0; } .message { @@ -282,57 +275,45 @@ input, textarea { border: 1px solid #F44336; } +/* Стили для ввода сообщений */ .chat-input { display: flex; - background-color: white; + gap: 10px; + padding: 20px; + background: #fff; border-radius: 8px; - padding: 15px; - width: 100%; - box-sizing: border-box; - position: absolute; - bottom: 40px; - left: 0; - right: 0; - min-height: 70px; - max-height: 200px; /* Максимальная высота */ - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); + border: 1px solid #e0e0e0; } .chat-input textarea { flex: 1; + padding: 12px; border: 1px solid #e0e0e0; - resize: vertical; /* Разрешаем вертикальное изменение размера */ - padding: 10px; - min-height: 40px; - max-height: 170px; /* Максимальная высота минус padding */ - font-family: inherit; border-radius: 4px; - background-color: white; - line-height: 1.4; + resize: none; + font-size: 14px; + line-height: 1.5; + min-height: 60px; + max-height: 200px; } .chat-input button { - background-color: white; - color: #333; - border: 1px solid #333; - border-radius: 4px; padding: 0 20px; + background: #4CAF50; + color: white; + border: none; + border-radius: 4px; cursor: pointer; - height: 40px; - margin-left: 10px; - align-self: flex-start; - transition: background-color 0.2s; font-size: 14px; + transition: background-color 0.3s; } .chat-input button:hover:not(:disabled) { - background-color: #f0f0f0; + background: #45a049; } .chat-input button:disabled { - background-color: #f5f5f5; - color: #999; - border-color: #ddd; + background: #ccc; cursor: not-allowed; } @@ -744,43 +725,63 @@ input, textarea { word-break: break-all; } -/* Медиа-запрос для узких экранов */ -@media (max-width: 1300px) { - .wallet-sidebar { - width: 286px; - min-width: 286px; - padding: 15px; +/* Медиа-запросы для адаптивности */ +@media (max-width: 1200px) { + .header-content, + .main-content { + max-width: 100%; + padding: 0 15px; } - - .verification-code code { - font-size: 14px; - padding: 6px 10px; +} + +/* Медиа-запросы для мобильных устройств */ +@media screen and (max-width: 768px) { + .header-content { + flex-direction: row; + align-items: center; + justify-content: space-between; + padding: 10px; } - - .auth-btn, .bot-link, .cancel-btn { - padding: 10px 8px; - font-size: 13px; + + .header-text { + flex: 1; + margin-right: 10px; + min-width: 0; /* Важно для корректной работы text-overflow */ } - - .verify-btn, .send-email-btn { - padding: 0 10px; - font-size: 13px; + + .title { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: 1.2rem; + margin: 0; } - - .main-content:not(.no-right-sidebar) { - margin-right: 286px; + + .subtitle { + white-space: normal; + font-size: 0.9rem; + margin: 0; + line-height: 1.2; } - - .close-wallet-sidebar { - width: 26px; - height: 26px; - font-size: 18px; - top: 8px; - right: 8px; + + .header-wallet-btn { + flex-shrink: 0; + margin-left: 10px; } - - .wallet-buttons { - margin-top: 35px; +} + +/* Дополнительные стили для очень маленьких экранов */ +@media screen and (max-width: 480px) { + .header-content { + padding: 8px; + } + + .title { + font-size: 1.1rem; + } + + .subtitle { + font-size: 0.8rem; } } @@ -999,43 +1000,119 @@ input, textarea { } .header { + background: #fff; + border-bottom: 1px solid #e0e0e0; padding: 20px 0; - margin-bottom: 24px; /* Уменьшенный отступ после заголовка */ - width: 100%; + position: sticky; + top: 0; + z-index: 100; +} + +.header-content { + max-width: 1200px; + margin: 0 auto; + padding: 0 20px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.header-text { + flex: 1; } .title { font-size: 24px; - font-weight: bold; - margin: 0; - line-height: 1.2; + font-weight: 500; + color: #333; + margin-bottom: 5px; } .subtitle { - font-size: 14px; + font-size: 16px; color: #666; - margin: 5px 0 0 0; } -/* Стили для правой панели */ -.wallet-sidebar { - position: fixed; - top: 0; - right: 0; - width: 300px; - height: 100vh; - background: white; - box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1); - padding: 20px; - overflow-y: auto; - z-index: 1000; +.header-wallet-btn { + margin-left: 20px; + padding: 10px; + background: transparent; + color: #333; + border: none; + cursor: pointer; + display: flex; + align-items: center; + gap: 10px; + transition: background-color 0.3s; + position: relative; + width: 40px; + height: 40px; + justify-content: center; } -/* Стили для основного контента */ -.content-container { - padding: 20px 15px; - margin-right: 40px; /* Одинаковый отступ справа */ - margin-left: 40px; /* Одинаковый отступ слева */ +.header-wallet-btn:hover { + background: rgba(0, 0, 0, 0.05); +} + +.header-wallet-btn .nav-btn-number { + display: none; +} + +.header-wallet-btn .nav-btn-text { + display: none; +} + +.header-wallet-btn::before, +.header-wallet-btn::after, +.header-wallet-btn .hamburger-line { + content: ''; + position: absolute; + width: 24px; + height: 2px; + background-color: #333; + transition: all 0.3s ease; +} + +.header-wallet-btn::before { + top: 12px; +} + +.header-wallet-btn::after { + bottom: 12px; +} + +.header-wallet-btn .hamburger-line { + top: 50%; + transform: translateY(-50%); +} + +/* Анимация при наведении */ +.header-wallet-btn:hover::before { + top: 11px; +} + +.header-wallet-btn:hover::after { + bottom: 11px; +} + +/* Анимация при активном состоянии */ +.header-wallet-btn.active::before { + transform: rotate(45deg); + top: 50%; +} + +.header-wallet-btn.active::after { + transform: rotate(-45deg); + bottom: 50%; +} + +.header-wallet-btn.active .hamburger-line { + opacity: 0; +} + +.header-wallet-btn .hamburger-line { + top: 50%; + transform: translateY(-50%); } .footer { @@ -1232,3 +1309,42 @@ input, textarea { padding: 0; border-left: none; } + +/* Стили для кнопок в чате */ +.chat-buttons { + display: flex; + gap: 10px; + margin-top: 10px; +} + +.chat-buttons button { + padding: 8px 16px; + border-radius: 4px; + border: none; + cursor: pointer; + font-size: 14px; + transition: background-color 0.3s; +} + +.chat-buttons button:first-child { + background-color: #4CAF50; + color: white; +} + +.chat-buttons button:first-child:hover:not(:disabled) { + background-color: #45a049; +} + +.chat-buttons .clear-btn { + background-color: #f44336; + color: white; +} + +.chat-buttons .clear-btn:hover:not(:disabled) { + background-color: #da190b; +} + +.chat-buttons button:disabled { + background-color: #cccccc; + cursor: not-allowed; +} diff --git a/frontend/src/composables/useAuth.js b/frontend/src/composables/useAuth.js index e44ce9d..357b914 100644 --- a/frontend/src/composables/useAuth.js +++ b/frontend/src/composables/useAuth.js @@ -274,11 +274,7 @@ export function useAuth() { const disconnect = async () => { try { - // Сохраняем текущий guestId перед выходом - const newGuestId = crypto.randomUUID(); - localStorage.setItem('guestId', newGuestId); - console.log('Created new guestId for future session:', newGuestId); - + // Удаляем все идентификаторы перед выходом await axios.post('/api/auth/logout'); // Обновляем состояние в памяти @@ -297,17 +293,22 @@ export function useAuth() { // Очищаем списки идентификаторов identities.value = []; + processedGuestIds.value = []; - // Очищаем localStorage кроме guestId + // Очищаем localStorage полностью localStorage.removeItem('isAuthenticated'); localStorage.removeItem('userId'); localStorage.removeItem('address'); localStorage.removeItem('isAdmin'); + localStorage.removeItem('guestId'); + localStorage.removeItem('guestMessages'); + localStorage.removeItem('telegramId'); + localStorage.removeItem('email'); // Удаляем класс подключенного кошелька document.body.classList.remove('wallet-connected'); - console.log('User disconnected successfully'); + console.log('User disconnected successfully and all identifiers cleared'); return { success: true }; } catch (error) { diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index 5fb4c64..f4e5fa0 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -1,51 +1,21 @@ @@ -371,6 +334,7 @@ const isLoadingMore = ref(false); const hasMoreMessages = ref(false); const offset = ref(0); const limit = ref(30); +const isMessageLoadingInProgress = ref(false); // Добавляем флаг для отслеживания процесса загрузки // Состояния для верификации const showTelegramVerification = ref(false); @@ -400,8 +364,7 @@ const successMessage = ref(''); const showSuccessMessage = ref(false); // Состояния для сайдбара -const showSidebar = ref(false); -const currentPage = ref('home'); +const showWalletSidebar = ref(false); // Добавляем состояние для балансов const tokenBalances = ref({ @@ -411,8 +374,8 @@ const tokenBalances = ref({ polygon: '0' }); -// Состояние для отображения правой панели -const showWalletSidebar = ref(false); +// Добавляем состояние для отслеживания привязки гостевых сообщений +const isLinkingGuestMessages = ref(false); // Вычисленное свойство для фильтрации идентификаторов const filteredIdentities = computed(() => { @@ -444,17 +407,6 @@ function formatIdentityProvider(provider) { return providers[provider] || provider; } -// Функция для управления сайдбаром -const toggleSidebar = () => { - showSidebar.value = !showSidebar.value; - document.querySelector('.app-container').classList.toggle('menu-open'); -}; - -const navigateTo = (page) => { - currentPage.value = page; - console.log(`Навигация на страницу: ${page}`); -}; - // Функция для переключения отображения правой панели const toggleWalletSidebar = () => { showWalletSidebar.value = !showWalletSidebar.value; @@ -560,38 +512,26 @@ const sendEmailVerification = async () => { // Функция для обработки загрузки сообщений после аутентификации const handlePostAuthMessageLoading = async (authType) => { try { - console.log(`Обработка загрузки сообщений после аутентификации через ${authType}`); + isMessageLoadingInProgress.value = true; - // Сохраняем текущее количество сообщений для отслеживания - 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; + // Загружаем историю сообщений напрямую через API + const response = await axios.get('/api/chat/history'); + if (response.data.success) { + messages.value = response.data.messages || []; + console.log(`Загружено ${messages.value.length} сообщений после аутентификации`); - // Настраиваем отслеживание только если есть разница в количестве сообщений - if (newCount !== currentMessageCount) { - console.log(`Количество сообщений изменилось: ${currentMessageCount} -> ${newCount}`); - setupMessagePolling(newCount); - } else { - console.log('Количество сообщений не изменилось, отслеживание не требуется'); - } + // Очищаем локальное хранилище гостевых сообщений + localStorage.removeItem('guestMessages'); + localStorage.removeItem('guestId'); + + // Прокручиваем к последнему сообщению + await nextTick(); + scrollToBottom(); } } catch (error) { console.error(`Ошибка при обработке сообщений после аутентификации через ${authType}:`, error); + } finally { + isMessageLoadingInProgress.value = false; } }; @@ -864,38 +804,40 @@ const clearGuestMessages = () => { // Метод для загрузки истории чата const loadChatHistory = async () => { - // Сбрасываем текущие сообщения и загружаем новую историю isLoading.value = true; try { - // Сначала получаем общее количество сообщений + // Если пользователь аутентифицирован и есть гостевые сообщения, + // но привязка уже выполняется - ждем её завершения + if (auth.isAuthenticated.value && isLinkingGuestMessages.value) { + await new Promise(resolve => { + const checkInterval = setInterval(() => { + if (!isLinkingGuestMessages.value) { + clearInterval(checkInterval); + resolve(); + } + }, 100); + }); + } + + // Получаем общее количество сообщений const countResponse = await axios.get('/api/chat/history?count_only=true'); if (countResponse.data.success) { const messageCount = countResponse.data.count; console.log(`История содержит ${messageCount} сообщений`); - // Рассчитываем смещение для получения последних сообщений const effectiveOffset = Math.max(0, messageCount - limit.value); - - // Загружаем историю сообщений 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 = response.data.messages; console.log(`Загружено ${messages.value.length} сообщений из истории`); - // Отправляем событие об обновлении сообщений window.dispatchEvent(new CustomEvent('messages-updated', { detail: { count: messages.value.length } })); - // Прокручиваем к последнему сообщению await nextTick(); scrollToBottom(); } @@ -1258,10 +1200,6 @@ const disconnectWallet = async () => { // Останавливаем обновление балансов stopBalanceUpdates(); - // Сохраняем гостевой ID для продолжения работы после выхода - const guestId = getFromStorage('guestId') || generateUniqueId(); - setToStorage('guestId', guestId); - // Отправляем запрос на выход await axios.post('/api/auth/logout'); @@ -1275,25 +1213,14 @@ const disconnectWallet = async () => { // Обновляем отображение 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); - } + // Очищаем localStorage от всех сообщений + localStorage.removeItem('guestMessages'); + localStorage.removeItem('hasUserSentMessage'); console.log('Выход из системы выполнен успешно'); } catch (error) {