diff --git a/backend/package.json b/backend/package.json index 3b71c27..3448389 100644 --- a/backend/package.json +++ b/backend/package.json @@ -47,6 +47,7 @@ "pg": "^8.10.0", "session-file-store": "^1.5.0", "siwe": "^2.1.4", + "telegraf": "^4.16.3", "winston": "^3.17.0" }, "devDependencies": { diff --git a/backend/routes/auth.js b/backend/routes/auth.js index f0f7f9f..491f2e6 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -13,6 +13,9 @@ const { sendEmail } = require('../services/emailBot'); const { verificationCodes } = require('../services/telegramBot'); const { checkTokensAndUpdateRole } = require('../services/auth-service'); const { ethers } = require('ethers'); +const { initTelegramAuth } = require('../services/telegramBot'); +const { initEmailAuth, verifyEmailCode } = require('../services/emailBot'); +const { getBot } = require('../services/telegramBot'); // Создайте лимитер для попыток аутентификации const authLimiter = rateLimit({ @@ -34,24 +37,32 @@ router.get('/nonce', async (req, res) => { // Генерируем случайный nonce const nonce = crypto.randomBytes(16).toString('hex'); - // Удаляем старые nonce для этого адреса - await db.query(` - DELETE FROM nonces - WHERE identity_value = $1 - `, [address.toLowerCase()]); + // Проверяем, существует ли уже nonce для этого адреса + const existingNonce = await db.query( + 'SELECT id FROM nonces WHERE identity_value = $1', + [address.toLowerCase()] + ); - // Сохраняем новый nonce - await db.query(` - INSERT INTO nonces (identity_value, nonce, expires_at) - VALUES ($1, $2, NOW() + INTERVAL '15 minutes') - `, [address.toLowerCase(), nonce]); + if (existingNonce.rows.length > 0) { + // Обновляем существующий nonce + await db.query( + 'UPDATE nonces SET nonce = $1, expires_at = NOW() + INTERVAL \'15 minutes\' WHERE identity_value = $2', + [nonce, address.toLowerCase()] + ); + } else { + // Создаем новый nonce + await db.query( + 'INSERT INTO nonces (identity_value, nonce, expires_at) VALUES ($1, $2, NOW() + INTERVAL \'15 minutes\')', + [address.toLowerCase(), nonce] + ); + } console.log(`Nonce ${nonce} сохранен для адреса ${address}`); - return res.json({ nonce }); + res.json({ nonce }); } catch (error) { console.error('Error generating nonce:', error); - return res.status(500).json({ error: 'Internal server error' }); + res.status(500).json({ error: 'Failed to generate nonce' }); } }); @@ -60,77 +71,85 @@ const ERC20_ABI = [ "function balanceOf(address owner) view returns (uint256)" ]; -// Проверка подписи и аутентификация +// Верификация подписи и создание сессии router.post('/verify', async (req, res) => { try { - const { address, signature, message } = req.body; - console.log('Received verification request:', { address, message }); - console.log('Signature:', signature); - - // Проверяем подпись через SIWE - const siwe = new SiweMessage(message); - console.log('Created SIWE message object'); - - const fields = await siwe.verify({ signature }); - console.log('SIWE validation result:', fields); + const { address, message, signature } = req.body; - if (!fields || !fields.success) { - console.log('SIWE validation failed'); - return res.status(401).json({ error: 'Invalid signature' }); + // Проверяем подпись + const isValid = await authService.verifySignature(message, signature, address); + if (!isValid) { + return res.status(401).json({ success: false, error: 'Invalid signature' }); } - + // Проверяем nonce - const nonceResult = await db.query( - `SELECT nonce FROM nonces - WHERE identity_value = $1 - AND expires_at > NOW()`, - [address.toLowerCase()] - ); - console.log('Nonce check result:', nonceResult.rows); - - if (!nonceResult.rows.length) { - console.log('Invalid or expired nonce'); - return res.status(401).json({ error: 'Invalid or expired nonce' }); + const nonceResult = await db.query('SELECT nonce FROM nonces WHERE identity_value = $1', [address.toLowerCase()]); + if (nonceResult.rows.length === 0 || nonceResult.rows[0].nonce !== message.match(/Nonce: ([^\n]+)/)[1]) { + return res.status(401).json({ success: false, error: 'Invalid nonce' }); } - - // Проверяем соответствие nonce - if (nonceResult.rows[0].nonce !== fields.data.nonce) { - console.log('Nonce mismatch'); - return res.status(401).json({ error: 'Invalid nonce' }); + + // Находим или создаем пользователя + let userId, isAdmin; + + // Ищем пользователя по адресу в таблице user_identities + const userResult = await db.query(` + SELECT u.* FROM users u + JOIN user_identities ui ON u.id = ui.user_id + WHERE ui.provider = 'wallet' AND ui.provider_id = $1 + `, [address.toLowerCase()]); + + if (userResult.rows.length > 0) { + // Пользователь найден + userId = userResult.rows[0].id; + isAdmin = userResult.rows[0].role === 'admin'; + const address = userResult.rows[0].address; + } else { + // Создаем нового пользователя + const newUserResult = await db.query( + 'INSERT INTO users (role) VALUES ($1) RETURNING id', + ['user'] + ); + + userId = newUserResult.rows[0].id; + isAdmin = false; + + // Добавляем идентификатор кошелька + await db.query( + 'INSERT INTO user_identities (user_id, provider, provider_id) VALUES ($1, $2, $3)', + [userId, 'wallet', address.toLowerCase()] + ); } - - // Получаем или создаем пользователя - const userResult = await db.query( - `INSERT INTO users (address, created_at, updated_at) - VALUES ($1, NOW(), NOW()) - ON CONFLICT (address) DO UPDATE - SET updated_at = NOW() - RETURNING id, role = 'admin' as is_admin`, - [address] - ); - - const userId = userResult.rows[0].id; - const isAdmin = false; // Будет обновлено в createSession - - // Используем централизованный сервис - await authService.createSession(req, { + + // Обновляем сессию + req.session.userId = userId; + req.session.authenticated = true; + req.session.authType = 'wallet'; + req.session.isAdmin = isAdmin; + req.session.address = address.toLowerCase(); + + // Сохраняем сессию + await new Promise((resolve, reject) => { + req.session.save((err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + + // Возвращаем успешный ответ + return res.json({ + success: true, userId, address, isAdmin, - authType: 'wallet', - guestId: req.session.guestId - }); - - res.json({ - authenticated: true, - userId, - address, - isAdmin, - authType: 'wallet' + authenticated: true }); + } catch (error) { - console.error('Error:', error); - res.status(500).json({ error: 'Internal server error' }); + console.error('Error in /verify:', error); + res.status(500).json({ success: false, error: 'Server error' }); } }); @@ -488,54 +507,106 @@ router.post('/link-identity', async (req, res) => { } }); -// Проверка аутентификации -router.get('/check', (req, res) => { - try { - console.log('Сессия при проверке:', req.session); - - if (req.session && req.session.authenticated) { - return res.json({ - authenticated: true, - userId: req.session.userId, - address: req.session.address, - isAdmin: req.session.isAdmin, - authType: req.session.authType || 'wallet' - }); +// Проверка статуса аутентификации +router.get('/check', async (req, res) => { + console.log('Сессия при проверке:', req.session); + + let telegramId = null; + + if (req.session.userId && req.session.authType === 'telegram') { + // Проверяем, есть ли telegramId в сессии + if (req.session.telegramId) { + telegramId = req.session.telegramId; + console.log('Telegram ID from session:', telegramId); + + // Проверяем, есть ли запись в базе данных + try { + const result = await db.query( + 'SELECT provider_id FROM user_identities WHERE user_id = $1 AND provider = $2', + [req.session.userId, 'telegram'] + ); + + console.log('Telegram ID query result:', result.rows); + + if (result.rows.length === 0) { + // Если нет, добавляем запись + try { + 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 NOTHING', + [req.session.userId, 'telegram', telegramId] + ); + console.log('Added Telegram ID to database:', telegramId); + } catch (error) { + console.error('Error adding Telegram ID to database:', error); + } + } + } catch (error) { + console.error('Error checking Telegram ID in database:', error); + } } else { - return res.json({ authenticated: false }); + // Если нет, ищем в базе данных + try { + const result = await db.query( + 'SELECT provider_id FROM user_identities WHERE user_id = $1 AND provider = $2', + [req.session.userId, 'telegram'] + ); + + console.log('Telegram ID query result:', result.rows); + + if (result.rows.length > 0) { + telegramId = result.rows[0].provider_id; + console.log('Telegram ID from database:', telegramId); + + // Сохраняем в сессию для будущих запросов + req.session.telegramId = telegramId; + } else { + // Если нет в базе данных, используем фиксированное значение + telegramId = 'Telegram User'; + console.log('Using fixed Telegram ID:', telegramId); + + // Сохраняем в сессию для будущих запросов + req.session.telegramId = telegramId; + + // Добавляем запись в базу данных + try { + 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 NOTHING', + [req.session.userId, 'telegram', telegramId] + ); + console.log('Added Telegram ID to database:', telegramId); + } catch (error) { + console.error('Error adding Telegram ID to database:', error); + } + } + } catch (error) { + console.error('Error fetching Telegram ID:', error); + } } - } catch (error) { - console.error('Ошибка при проверке аутентификации:', error); - return res.status(500).json({ error: 'Internal server error' }); } + + const response = { + authenticated: !!req.session.authenticated, + userId: req.session.userId, + isAdmin: !!req.session.isAdmin, + authType: req.session.authType, + address: req.session.address, + telegramId: telegramId + }; + + console.log('Auth check response:', response); + + res.json(response); }); // Выход из системы -router.post('/logout', async (req, res) => { - try { - // Сохраняем ID сессии до уничтожения - const sessionID = req.sessionID; - - // Уничтожаем сессию - req.session.destroy(); - - // Удаляем сессию из базы данных - try { - const { pool } = require('../db'); - await pool.query('DELETE FROM session WHERE sid = $1', [sessionID]); - console.log(`Сессия ${sessionID} удалена из базы данных`); - } catch (dbError) { - console.error('Ошибка при удалении сессии из базы данных:', dbError); +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' }); } - - // Очищаем куки - res.clearCookie('connect.sid'); - res.json({ success: true }); - } catch (error) { - console.error('Ошибка при выходе из системы:', error); - res.status(500).json({ error: 'Ошибка сервера' }); - } + }); }); // Маршрут для авторизации через Telegram @@ -626,12 +697,16 @@ async function checkTokenBalance(address) { // Маршрут для верификации Telegram router.post('/telegram/verify', async (req, res) => { + console.log('Telegram verification request body:', req.body); + const { code } = req.body; try { - const telegramBot = require('../services/telegramBot'); + const telegramBot = getBot(); const result = await telegramBot.verifyCode(code); + console.log('Telegram verification result:', result); + if (result.success) { // Проверяем, что у нас есть telegramId if (!result.telegramId) { @@ -678,6 +753,18 @@ router.post('/telegram/verify', async (req, res) => { }); }); + console.log('Telegram ID saved in session:', req.session.telegramId); + + // Сохраняем идентификатор Telegram в базе данных + try { + 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', result.telegramId] + ); + } catch (error) { + console.error('Error saving Telegram ID to database:', error); + } + return res.json({ success: true, userId: userId, @@ -1130,4 +1217,64 @@ router.post('/clear-session', async (req, res) => { } }); +// Инициализация Telegram аутентификации +router.post('/telegram/init', async (req, res) => { + try { + // Проверяем, есть ли уже привязанный Telegram + if (req.session?.userId) { + const existingTelegram = await db.query( + `SELECT provider_id + FROM user_identities + WHERE user_id = $1 + AND provider = 'telegram'`, + [req.session.userId] + ); + + if (existingTelegram.rows.length > 0) { + return res.status(400).json({ + error: 'Telegram already linked to this account' + }); + } + } + + const { verificationCode, botLink } = await initTelegramAuth(req.session); + res.json({ verificationCode, botLink }); + } catch (error) { + console.error('Error initializing Telegram auth:', error); + res.status(500).json({ error: 'Failed to initialize Telegram auth' }); + } +}); + +// Инициализация Email аутентификации +router.post('/email/init', async (req, res) => { + try { + const { email } = req.body; + if (!email) { + return res.status(400).json({ error: 'Email is required' }); + } + + await initEmailAuth(email); + res.json({ success: true }); + } catch (error) { + console.error('Error initializing email auth:', error); + res.status(500).json({ error: 'Failed to send verification code' }); + } +}); + +// Проверка кода подтверждения email +router.post('/email/verify', requireAuth, async (req, res) => { + try { + const { code } = req.body; + if (!code) { + return res.status(400).json({ error: 'Verification code is required' }); + } + + const result = await verifyEmailCode(code, req.session.userId); + res.json(result); + } catch (error) { + console.error('Error verifying email code:', error); + res.status(400).json({ error: error.message }); + } +}); + module.exports = router; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index 75a7968..98344a4 100644 --- a/backend/server.js +++ b/backend/server.js @@ -10,7 +10,7 @@ const authRouter = require('./routes/auth'); const identitiesRouter = require('./routes/identities'); const { pool } = require('./db'); const helmet = require('helmet'); -const TelegramBotService = require('./services/telegramBot'); +const { getBot, stopBot } = require('./services/telegramBot'); const pgSession = require('connect-pg-simple')(session); const authService = require('./services/auth-service'); const logger = require('./utils/logger'); @@ -25,13 +25,34 @@ console.log('Используемый порт:', process.env.PORT || 8000); async function initServices() { try { console.log('Инициализация сервисов...'); - - if (process.env.TELEGRAM_BOT_TOKEN) { - const telegramBot = new TelegramBotService(process.env.TELEGRAM_BOT_TOKEN); - global.telegramBot = telegramBot; // Сохраняем экземпляр глобально - console.log('Telegram бот инициализирован'); + + // Останавливаем предыдущий экземпляр бота + await stopBot(); + + // Добавляем обработку ошибок при запуске бота + try { + await getBot(); // getBot теперь асинхронный и сам запускает бота + console.log('Telegram bot started'); + + // Добавляем graceful shutdown + process.once('SIGINT', async () => { + await stopBot(); + process.exit(0); + }); + process.once('SIGTERM', async () => { + await stopBot(); + process.exit(0); + }); + } catch (error) { + if (error.code === 409) { + logger.warn('Another instance of Telegram bot is running. This is normal during development with nodemon'); + // Просто логируем ошибку и продолжаем работу + // Бот будет запущен при следующем перезапуске + } else { + logger.error('Error launching Telegram bot:', error); + } } - + console.log('Все сервисы успешно инициализированы'); } catch (error) { console.error('Ошибка при инициализации сервисов:', error); diff --git a/backend/services/auth-service.js b/backend/services/auth-service.js index 1525a19..a40a4ac 100644 --- a/backend/services/auth-service.js +++ b/backend/services/auth-service.js @@ -44,39 +44,41 @@ class AuthService { */ async findOrCreateUser(address) { try { - const existingUser = await db.query( - `SELECT u.id - FROM users u - JOIN user_identities ui ON u.id = ui.user_id - WHERE ui.provider = 'wallet' - AND LOWER(ui.provider_id) = LOWER($1)`, - [address] - ); - - if (existingUser.rows.length > 0) { - const userId = existingUser.rows[0].id; - const isAdmin = await this.checkAdminRole(address); - return { userId, isAdmin }; + // Нормализуем адрес + address = ethers.getAddress(address); + + // Ищем пользователя по адресу в таблице user_identities + const userResult = await db.query(` + SELECT u.* FROM users u + JOIN user_identities ui ON u.id = ui.user_id + WHERE ui.provider = 'wallet' AND ui.provider_id = $1 + `, [address]); + + if (userResult.rows.length > 0) { + const user = userResult.rows[0]; + return { + userId: user.id, + isAdmin: user.role === 'admin' + }; } - - // Создание нового пользователя - const result = await db.query( - 'INSERT INTO users DEFAULT VALUES RETURNING id', - [] + + // Если пользователь не найден, создаем нового + const newUserResult = await db.query( + 'INSERT INTO users (role) VALUES ($1) RETURNING id', + ['user'] ); - const userId = result.rows[0].id; - + + const userId = newUserResult.rows[0].id; + + // Добавляем идентификатор кошелька await db.query( - `INSERT INTO user_identities - (user_id, provider, provider_id, identity_type, identity_value) - VALUES ($1, 'wallet', $2, 'wallet', $2)`, - [userId, address.toLowerCase()] + 'INSERT INTO user_identities (user_id, provider, provider_id) VALUES ($1, $2, $3)', + [userId, 'wallet', address] ); - - const isAdmin = await this.checkAdminRole(address); - return { userId, isAdmin }; + + return { userId, isAdmin: false }; } catch (error) { - console.error('Error in findOrCreateUser:', error); + console.error('Error finding or creating user:', error); throw error; } } @@ -143,41 +145,80 @@ class AuthService { } // Создание сессии с проверкой роли - async createSession(req, { userId, address, authType, guestId }) { - let isAdmin = false; - - if (address) { - isAdmin = await this.checkAdminRole(address); - } else if (userId) { - const linkedWallet = await this.getLinkedWallet(userId); - if (linkedWallet) { - isAdmin = await this.checkAdminRole(linkedWallet); + async createSession(session, { userId, authenticated, authType, guestId, address }) { + try { + // Обновляем данные сессии + session.userId = userId; + session.authenticated = authenticated; + session.authType = authType; + session.guestId = guestId; + if (address) { + session.address = address; } + + // Сохраняем сессию в БД + const result = await db.query( + `UPDATE session + SET sess = $1 + WHERE sid = $2`, + [JSON.stringify({ + userId, + authenticated, + authType, + guestId, + address, + cookie: session.cookie + }), session.id] + ); + + return true; + } catch (error) { + logger.error('Error creating session:', error); + return false; } + } - req.session.userId = userId; - req.session.address = address; - req.session.isAdmin = isAdmin; - req.session.authenticated = true; - req.session.authType = authType; - - if (guestId) { - req.session.guestId = guestId; + async getSession(sessionId) { + try { + const result = await db.query('SELECT * FROM session WHERE sid = $1', [sessionId]); + return result.rows[0]; + } catch (error) { + console.error('Error getting session:', error); + throw error; } - - await req.session.save(); } // Получение связанного кошелька async getLinkedWallet(userId) { const result = await db.query( - `SELECT identity_value as address + `SELECT provider_id as address FROM user_identities - WHERE user_id = $1 AND identity_type = 'wallet'`, + WHERE user_id = $1 AND provider = 'wallet'`, [userId] ); return result.rows[0]?.address; } + + /** + * Проверяет роль пользователя Telegram + * @param {number} userId - ID пользователя + * @returns {Promise} - Роль пользователя + */ + async checkUserRole(userId) { + try { + // Проверяем наличие связанного кошелька + const wallet = await this.getLinkedWallet(userId); + if (wallet) { + // Если есть кошелек, проверяем админские токены + const isAdmin = await this.checkAdminRole(wallet); + return isAdmin ? 'admin' : 'user'; + } + return 'user'; + } catch (error) { + logger.error('Error checking user role:', error); + return 'user'; + } + } } // Создаем и экспортируем единственный экземпляр diff --git a/backend/services/emailBot.js b/backend/services/emailBot.js index afd763f..e999332 100644 --- a/backend/services/emailBot.js +++ b/backend/services/emailBot.js @@ -5,6 +5,7 @@ const simpleParser = require('mailparser').simpleParser; const { processMessage } = require('./ai-assistant'); const { inspect } = require('util'); const logger = require('../utils/logger'); +const { generateVerificationCode, addUserIdentity } = require('../utils/helpers'); // Хранилище кодов подтверждения const verificationCodes = new Map(); // { email: { code, token, expires } } @@ -334,6 +335,79 @@ class EmailBotService { } } +// Инициализация процесса аутентификации по email +async function initEmailAuth(email) { + const code = generateVerificationCode(); + + // Сохраняем код на 15 минут + verificationCodes.set(code, { + email, + timestamp: Date.now(), + verified: false + }); + + // Отправляем код на email + try { + await transporter.sendMail({ + from: process.env.SMTP_FROM, + to: email, + subject: 'Код подтверждения для HB3 Accelerator', + text: `Ваш код подтверждения: ${code}\n\nВведите его на сайте для завершения аутентификации.`, + html: ` +

Код подтверждения для HB3 Accelerator

+

Ваш код подтверждения: ${code}

+

Введите его на сайте для завершения аутентификации.

+ ` + }); + + logger.info(`Verification code sent to email: ${email}`); + return { success: true }; + } catch (error) { + logger.error('Error sending verification email:', error); + throw new Error('Failed to send verification email'); + } +} + +// Проверка кода подтверждения +async function verifyEmailCode(code, userId) { + const verification = verificationCodes.get(code); + + if (!verification) { + logger.warn(`Invalid verification code attempt: ${code}`); + throw new Error('Неверный код'); + } + + if (Date.now() - verification.timestamp > 15 * 60 * 1000) { + verificationCodes.delete(code); + logger.warn(`Expired verification code: ${code}`); + throw new Error('Код устарел'); + } + + try { + // Сохраняем связь пользователя с email + const success = await addUserIdentity( + userId, + 'email', + verification.email + ); + + if (success) { + verificationCodes.delete(code); + logger.info(`User ${userId} successfully linked email ${verification.email}`); + return { success: true }; + } else { + throw new Error('Этот email уже привязан к другому пользователю'); + } + } catch (error) { + logger.error('Error saving email identity:', error); + throw error; + } +} + // Экспортируем класс и хранилище кодов -module.exports = EmailBotService; -module.exports.verificationCodes = verificationCodes; +module.exports = { + EmailBotService, + verificationCodes, + initEmailAuth, + verifyEmailCode +}; diff --git a/backend/services/telegramBot.js b/backend/services/telegramBot.js index 6b6b2a1..1709e0f 100644 --- a/backend/services/telegramBot.js +++ b/backend/services/telegramBot.js @@ -1,144 +1,183 @@ -const TelegramBot = require('node-telegram-bot-api'); +const { Telegraf } = require('telegraf'); const logger = require('../utils/logger'); +const db = require('../db'); +const authService = require('./auth-service'); -class TelegramBotService { - constructor(token) { - this.bot = new TelegramBot(token, { - polling: true, - request: { - timeout: 30000 // 30 секунд таймаут - } - }); - this.verificationCodes = new Map(); - this.setupHandlers(); - - logger.info('TelegramBot service initialized'); - } +let botInstance = null; +const verificationCodes = new Map(); - setupHandlers() { - this.bot.on('message', this.handleMessage.bind(this)); - this.bot.on('callback_query', this.handleCallbackQuery.bind(this)); - - // Обработка ошибок - this.bot.on('polling_error', (error) => { - logger.error('Telegram polling error:', error); - }); - - this.bot.on('error', (error) => { - logger.error('Telegram bot error:', error); - }); - } - - async handleMessage(msg) { - try { - const chatId = msg.chat.id; - const text = msg.text; - - logger.info(`Received message from ${chatId}: ${text}`); - - if (text.startsWith('/start')) { - await this.handleStart(msg); - } else if (this.verificationCodes.has(chatId)) { - await this.handleVerificationCode(msg); - } - } catch (error) { - logger.error('Error handling message:', error); - } - } - - async handleCallbackQuery(query) { - try { - const chatId = query.message.chat.id; - await this.bot.answerCallbackQuery(query.id); - - logger.info(`Handled callback query from ${chatId}`); - } catch (error) { - logger.error('Error handling callback query:', error); - } - } - - async handleStart(msg) { - const chatId = msg.chat.id; - try { - await this.bot.sendMessage( - chatId, - 'Добро пожаловать! Используйте этого бота для аутентификации в приложении.' - ); - logger.info(`Sent welcome message to ${chatId}`); - } catch (error) { - logger.error(`Error sending welcome message to ${chatId}:`, error); - } - - } - - async handleVerificationCode(msg) { - const chatId = msg.chat.id; - const code = msg.text.trim(); - - try { - const verificationData = this.verificationCodes.get(chatId); - - if (!verificationData) { - await this.bot.sendMessage(chatId, 'Нет активного кода подтверждения.'); - return; - } - - if (Date.now() > verificationData.expires) { - this.verificationCodes.delete(chatId); - await this.bot.sendMessage(chatId, 'Код подтверждения истек. Запросите новый.'); - return; - } - - if (verificationData.code === code) { - await this.bot.sendMessage(chatId, 'Код подтвержден успешно!'); - this.verificationCodes.delete(chatId); - } else { - await this.bot.sendMessage(chatId, 'Неверный код. Попробуйте еще раз.'); - } - } catch (error) { - logger.error(`Error handling verification code for ${chatId}:`, error); - } - } - - async sendVerificationCode(chatId, code) { - try { - // Сохраняем код с временем истечения (15 минут) - this.verificationCodes.set(chatId, { - code, - expires: Date.now() + 15 * 60 * 1000 - }); - - await this.bot.sendMessage( - chatId, - `Ваш код подтверждения: ${code}\nВведите его в приложении.` - ); - - logger.info(`Sent verification code to ${chatId}`); - return true; - } catch (error) { - logger.error(`Error sending verification code to ${chatId}:`, error); - return false; - } - } - - async verifyCode(code) { - try { - for (const [chatId, data] of this.verificationCodes.entries()) { - if (data.code === code) { - if (Date.now() > data.expires) { - this.verificationCodes.delete(chatId); - return { success: false, error: 'Код истек' }; - } - this.verificationCodes.delete(chatId); - return { success: true, telegramId: chatId.toString() }; - } - } - return { success: false, error: 'Неверный код' }; - } catch (error) { - logger.error('Error verifying code:', error); - return { success: false, error: 'Внутренняя ошибка' }; - } +// Простая остановка бота +async function stopBot() { + if (botInstance) { + await botInstance.stop(); + botInstance = null; } } -module.exports = TelegramBotService; \ No newline at end of file +// Создание и настройка бота +async function getBot() { + if (!botInstance) { + botInstance = new Telegraf(process.env.TELEGRAM_BOT_TOKEN); + + // Обработка команды /start + botInstance.command('start', (ctx) => { + ctx.reply('Добро пожаловать! Отправьте код подтверждения для аутентификации.'); + }); + + // Обработка кодов верификации + botInstance.on('text', async (ctx) => { + const code = ctx.message.text.trim(); + const verification = verificationCodes.get(code); + + if (!verification) { + ctx.reply('Неверный код подтверждения'); + return; + } + + try { + logger.info('Starting Telegram auth process for code:', code); + logger.info('Verification data:', verification); + + // Сначала проверяем, существует ли пользователь с этим Telegram ID + let userId; + const existingUser = await db.query( + `SELECT u.id + FROM users u + JOIN user_identities ui ON u.id = ui.user_id + WHERE ui.provider = $1 + AND ui.provider_id = $2`, + ['telegram', ctx.from.id.toString()] + ); + + if (existingUser.rows.length > 0) { + userId = existingUser.rows[0].id; + logger.info('Found existing user with ID:', userId); + } else { + // Создаем нового пользователя + const result = await db.query( + `INSERT INTO users (created_at, updated_at) + VALUES (NOW(), NOW()) + RETURNING id`, + [] + ); + userId = result.rows[0].id; + logger.info('Created new user with ID:', userId); + } + + // Связываем 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}`); + + // Обновляем сессию + if (verification?.session) { + logger.info('Creating session with data:', verification.session); + + // Обновляем данные сессии напрямую + verification.session.userId = userId; + verification.session.authenticated = true; + verification.session.authType = 'telegram'; + verification.session.telegramId = ctx.from.id.toString(); // Добавляем идентификатор Telegram в сессию + + // Проверяем роль пользователя + const userRole = await authService.checkUserRole(userId); + verification.session.userRole = userRole; + + await new Promise((resolve, reject) => { + verification.session.save(err => { + if (err) reject(err); + else resolve(); + }); + }); + + logger.info('Session created successfully'); + } + + // Отправляем последнее сообщение пользователя + if (verification.session.guestId) { + logger.info('Fetching last guest message for guestId:', verification.session.guestId); + const messageResult = await db.query(` + SELECT content FROM guest_messages + WHERE guest_id = $1 + ORDER BY created_at DESC + LIMIT 1 + `, [verification.session.guestId]); + + const lastMessage = messageResult.rows[0]?.content; + logger.info('Found last message:', lastMessage); + if (lastMessage) { + await ctx.reply(`Ваше последнее сообщение: "${lastMessage}"`); + } + } + + // Отправляем сообщение об успешной аутентификации + await ctx.reply('Аутентификация успешна! Можете вернуться в приложение.'); + + // Удаляем сообщение с кодом + try { + await ctx.deleteMessage(ctx.message.message_id); + } catch (error) { + logger.warn('Could not delete code message:', error); + } + + // Удаляем код верификации + verificationCodes.delete(code); + + } catch (error) { + logger.error('Error in Telegram auth:', error); + + // Более информативные сообщения об ошибках + let errorMessage = 'Произошла ошибка при сохранении. Попробуйте позже.'; + + if (error.code === '42P01') { + errorMessage = 'Ошибка сессии. Пожалуйста, обновите страницу и попробуйте снова.'; + } else if (error.code === '42703') { + errorMessage = 'Ошибка структуры данных. Обратитесь к администратору.'; + } + + if (error.code) { + logger.error('Database error code:', error.code); + } + if (error.detail) { + logger.error('Error detail:', error.detail); + } + if (error.stack) { + logger.error('Error stack:', error.stack); + } + await ctx.reply(errorMessage); + } + }); + + // Запускаем бота + await botInstance.launch(); + } + + return botInstance; +} + +// Инициализация процесса аутентификации +async function initTelegramAuth(session) { + const code = Math.random().toString(36).substring(2, 8).toUpperCase(); + const botLink = `https://t.me/${process.env.TELEGRAM_BOT_USERNAME}`; + + verificationCodes.set(code, { + timestamp: Date.now(), + session: session + }); + + logger.info(`Generated verification code: ${code} for Telegram auth`); + return { verificationCode: code, botLink }; +} + +module.exports = { + getBot, + stopBot, + verificationCodes, + initTelegramAuth +}; \ No newline at end of file diff --git a/backend/utils/helpers.js b/backend/utils/helpers.js index e6fdecd..edc66d0 100644 --- a/backend/utils/helpers.js +++ b/backend/utils/helpers.js @@ -1,3 +1,5 @@ +const db = require('../db'); + // Функция для создания задержки function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); @@ -9,7 +11,43 @@ function isValidEmail(email) { return emailRegex.test(email); } +// Генерация кода подтверждения +function generateVerificationCode(length = 6) { + return Math.random() + .toString(36) + .substring(2, 2 + length) + .toUpperCase(); +} + +// Проверка существования идентификатора пользователя +async function checkUserIdentity(userId, provider, providerId) { + const result = await db.query( + 'SELECT * FROM user_identities WHERE user_id = $1 AND provider = $2 AND provider_id = $3', + [userId, provider, providerId] + ); + return result.rows.length > 0; +} + +// Добавление новой идентификации +async function addUserIdentity(userId, provider, providerId) { + try { + await db.query( + 'INSERT INTO user_identities (user_id, provider, provider_id) VALUES ($1, $2, $3)', + [userId, provider, providerId] + ); + return true; + } catch (error) { + if (error.code === '23505') { // Уникальное ограничение нарушено + return false; + } + throw error; + } +} + module.exports = { sleep, isValidEmail, + generateVerificationCode, + checkUserIdentity, + addUserIdentity }; diff --git a/backend/yarn.lock b/backend/yarn.lock index 7523588..2643668 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -1079,6 +1079,11 @@ resolved "https://registry.npmjs.org/@stablelib/wipe/-/wipe-1.0.1.tgz" integrity sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg== +"@telegraf/types@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@telegraf/types/-/types-7.1.0.tgz#d8bd9b2f5070b4de46971416e890338cd89fc23d" + integrity sha512-kGevOIbpMcIlCDeorKGpwZmdH7kHbqlk/Yj6dEpJMKEQw5lk0KVQY0OLXaCswy8GqlIVLd5625OB+rAntP9xVw== + "@tsconfig/node10@^1.0.7": version "1.0.11" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2" @@ -1806,6 +1811,24 @@ bs58check@^2.1.2: create-hash "^1.1.0" safe-buffer "^5.1.2" +buffer-alloc-unsafe@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" + integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg== + +buffer-alloc@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec" + integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow== + dependencies: + buffer-alloc-unsafe "^1.1.0" + buffer-fill "^1.0.0" + +buffer-fill@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" + integrity sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ== + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -2288,7 +2311,7 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@4, debug@^4, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.5: +debug@4, debug@^4, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5: version "4.4.0" resolved "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz" integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== @@ -4678,6 +4701,11 @@ mocha@^10.0.0, mocha@^10.2.0: yargs-parser "^20.2.9" yargs-unparser "^2.0.0" +mri@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" + integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== + ms@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" @@ -4753,7 +4781,7 @@ node-emoji@^1.10.0: dependencies: lodash "^4.17.21" -node-fetch@^2.6.7: +node-fetch@^2.6.7, node-fetch@^2.7.0: version "2.7.0" resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== @@ -5011,6 +5039,11 @@ p-timeout@^3.2.0: dependencies: p-finally "^1.0.0" +p-timeout@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-4.1.0.tgz#788253c0452ab0ffecf18a62dff94ff1bd09ca0a" + integrity sha512-+/wmHtzJuWii1sXn3HCuH/FTwGhrp4tmJTxSKJbfS+vkipci6osxXM5mY0jUiRzWKMTgUT8l7HFbeSwZAynqHw== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" @@ -5555,6 +5588,13 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +safe-compare@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/safe-compare/-/safe-compare-1.1.4.tgz#5e0128538a82820e2e9250cd78e45da6786ba593" + integrity sha512-b9wZ986HHCo/HbKrRpBJb2kqXMK9CEWIE1egeEvZsYn69ay3kdfl9nG3RyOcR+jInTDf7a86WQ1d4VJX7goSSQ== + dependencies: + buffer-alloc "^1.2.0" + safe-push-apply@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz" @@ -5582,6 +5622,11 @@ safe-stable-stringify@^2.3.1: resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +sandwich-stream@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/sandwich-stream/-/sandwich-stream-2.0.2.tgz#6d1feb6cf7e9fe9fadb41513459a72c2e84000fa" + integrity sha512-jLYV0DORrzY3xaz/S9ydJL6Iz7essZeAfnAavsJ+zsJGZ1MOnsS52yRjU3uF3pJa/lla7+wisp//fxOwOH8SKQ== + sc-istanbul@^0.4.5: version "0.4.6" resolved "https://registry.yarnpkg.com/sc-istanbul/-/sc-istanbul-0.4.6.tgz#cf6784355ff2076f92d70d59047d71c13703e839" @@ -6153,6 +6198,20 @@ table@^6.8.0: string-width "^4.2.3" strip-ansi "^6.0.1" +telegraf@^4.16.3: + version "4.16.3" + resolved "https://registry.yarnpkg.com/telegraf/-/telegraf-4.16.3.tgz#f03fa30482b540a7f9895af8f13ec8f432840a66" + integrity sha512-yjEu2NwkHlXu0OARWoNhJlIjX09dRktiMQFsM678BAH/PEPVwctzL67+tvXqLCRQQvm3SDtki2saGO9hLlz68w== + dependencies: + "@telegraf/types" "^7.1.0" + abort-controller "^3.0.0" + debug "^4.3.4" + mri "^1.2.0" + node-fetch "^2.7.0" + p-timeout "^4.1.0" + safe-compare "^1.1.4" + sandwich-stream "^2.0.2" + text-hex@1.0.x: version "1.0.0" resolved "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz" diff --git a/frontend/src/composables/useAuth.js b/frontend/src/composables/useAuth.js new file mode 100644 index 0000000..2b7326a --- /dev/null +++ b/frontend/src/composables/useAuth.js @@ -0,0 +1,78 @@ +import { ref, onMounted } from 'vue'; +import axios from '../api/axios'; + +export function useAuth() { + const isAuthenticated = ref(false); + const authType = ref(null); + const userId = ref(null); + const address = ref(null); + const telegramInfo = ref(null); + const isAdmin = ref(false); + const telegramId = ref(null); + + const updateAuth = ({ authenticated, authType: newAuthType, userId: newUserId, address: newAddress, telegramId: newTelegramId, isAdmin: newIsAdmin }) => { + isAuthenticated.value = authenticated; + authType.value = newAuthType; + userId.value = newUserId; + address.value = newAddress; + telegramId.value = newTelegramId; + isAdmin.value = newIsAdmin; + }; + + const checkAuth = async () => { + try { + const response = await axios.get('/api/auth/check'); + console.log('Auth check response:', response.data); + updateAuth(response.data); + return response.data; + } catch (error) { + console.error('Error checking auth status:', error); + return { authenticated: false }; + } + }; + + const disconnect = async () => { + try { + await axios.post('/api/auth/logout'); + updateAuth({ + authenticated: false, + authType: null, + userId: null, + address: null, + telegramId: null, + isAdmin: false + }); + + // Очищаем localStorage + localStorage.removeItem('isAuthenticated'); + localStorage.removeItem('userId'); + localStorage.removeItem('address'); + localStorage.removeItem('isAdmin'); + + // Перезагружаем страницу + window.location.reload(); + + return { success: true }; + } catch (error) { + console.error('Error disconnecting:', error); + return { success: false, error: error.message }; + } + }; + + onMounted(async () => { + await checkAuth(); + }); + + return { + isAuthenticated, + authType, + userId, + address, + telegramInfo, + isAdmin, + telegramId, + updateAuth, + checkAuth, + disconnect + }; +} \ No newline at end of file diff --git a/frontend/src/services/wallet.js b/frontend/src/services/wallet.js index f376d4b..b1f24d2 100644 --- a/frontend/src/services/wallet.js +++ b/frontend/src/services/wallet.js @@ -1,77 +1,90 @@ import { ethers } from 'ethers'; -import axios from 'axios'; +import axios from '../api/axios'; +import { SiweMessage } from 'siwe'; export async function connectWithWallet() { + console.log('Starting wallet connection...'); + try { - console.log('Starting wallet connection...'); // Проверяем наличие MetaMask if (!window.ethereum) { - throw new Error('MetaMask не установлен. Пожалуйста, установите MetaMask'); + throw new Error('MetaMask not detected. Please install MetaMask.'); } - + console.log('MetaMask detected, requesting accounts...'); - const accounts = await window.ethereum.request({ - method: 'eth_requestAccounts' - }); - + + // Запрашиваем доступ к аккаунтам + const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' }); + console.log('Got accounts:', accounts); + if (!accounts || accounts.length === 0) { - throw new Error('Нет доступных аккаунтов. Пожалуйста, разблокируйте MetaMask'); + throw new Error('No accounts found. Please unlock MetaMask.'); } - + + // Берем первый аккаунт const address = ethers.getAddress(accounts[0]); console.log('Normalized address:', address); - + + // Запрашиваем nonce с сервера console.log('Requesting nonce...'); - const { data: { nonce } } = await axios.get('/api/auth/nonce', { - params: { address } - }); + const nonceResponse = await axios.get(`/api/auth/nonce?address=${address}`); + const nonce = nonceResponse.data.nonce; console.log('Got nonce:', nonce); - - // Формируем сообщение в формате SIWE (Sign-In with Ethereum) + + // Создаем сообщение для подписи const domain = window.location.host; const origin = window.location.origin; - const statement = "Sign in with Ethereum to the app."; - const message = [ - `${domain} wants you to sign in with your Ethereum account:`, + const statement = 'Sign in with Ethereum to the app.'; + + const siweMessage = new SiweMessage({ + domain, address, - "", statement, - "", - `URI: ${origin}`, - "Version: 1", - "Chain ID: 1", - `Nonce: ${nonce}`, - `Issued At: ${new Date().toISOString()}`, - "Resources:", - `- ${origin}/api/auth/verify` - ].join("\n"); - + uri: origin, + version: '1', + chainId: 1, + nonce, + resources: [`${origin}/api/auth/verify`] + }); + + const message = siweMessage.prepareMessage(); console.log('SIWE message:', message); - + + // Запрашиваем подпись console.log('Requesting signature...'); const signature = await window.ethereum.request({ method: 'personal_sign', params: [message, address] }); + console.log('Got signature:', signature); - + + // Отправляем подпись на сервер для верификации console.log('Sending verification request...'); - const response = await axios.post('/api/auth/verify', { - address, + const verificationResponse = await axios.post('/api/auth/verify', { + message, signature, - message + address }); - console.log('Verification response:', response.data); - - const provider = new ethers.BrowserProvider(window.ethereum); - const signer = await provider.getSigner(); - - return { address, signer }; + + console.log('Verification response:', verificationResponse.data); + + // Обновляем состояние аутентификации + if (verificationResponse.data.success) { + // Обновляем состояние аутентификации в localStorage + localStorage.setItem('isAuthenticated', 'true'); + localStorage.setItem('userId', verificationResponse.data.userId); + localStorage.setItem('address', verificationResponse.data.address); + localStorage.setItem('isAdmin', verificationResponse.data.isAdmin); + + // Перезагружаем страницу для обновления состояния + window.location.reload(); + } + + return verificationResponse.data; } catch (error) { - // Форматируем ошибку для пользователя - const message = error.message || 'Ошибка подключения кошелька'; - console.error('Error connecting wallet:', message); - throw new Error(message); + console.error('Error connecting wallet:', error); + throw error; } } \ No newline at end of file diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index 2074cc1..235a241 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -6,19 +6,30 @@

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

-
-
+
-
- {{ truncateAddress(auth.address.value) }} - +
+ +
+ Telegram: {{ auth.telegramId }} +
@@ -44,52 +55,35 @@ - - -
- - -
- - -
- - -
- -
- - -
- - -
{{ emailError }} @@ -113,6 +107,18 @@
+ + +
+
+ Подключен кошелек: {{ auth.address }} + +
+
+ Подключен Telegram: {{ auth.telegramId }} + +
+
@@ -123,50 +129,114 @@ import TelegramConnect from '../components/identity/TelegramConnect.vue'; import EmailConnect from '../components/identity/EmailConnect.vue'; import api from '../api/axios'; import { connectWithWallet } from '../services/wallet'; +import axios from 'axios'; +import { useAuth } from '../composables/useAuth'; console.log('HomeView.vue: Version with chat loaded'); -const auth = inject('auth'); +const auth = useAuth(); const isAuthenticated = computed(() => auth.isAuthenticated.value); -const authType = ref(null); +const isConnecting = ref(false); const messages = ref([]); -const guestMessages = ref([]); const newMessage = ref(''); const isLoading = ref(false); const messagesContainer = ref(null); const userLanguage = ref('ru'); -const email = ref(''); -const isValidEmail = ref(true); -const hasShownAuthMessage = ref(false); -const hasShownAuthOptions = ref(false); - -// Email аутентификация -const emailVerificationCode = ref(''); -const showEmailVerification = ref(false); -const emailErrorMessage = ref(''); - -// Добавляем состояния для форм верификации -const showTelegramVerification = ref(false); -const showEmailForm = ref(false); -const telegramCode = ref(''); -const emailInput = ref(''); -const emailCode = ref(''); -const emailError = ref(''); // Добавляем состояния для пагинации -const PAGE_SIZE = 2; // Показываем только последнее сообщение и ответ -const allMessages = ref([]); // Все загруженные сообщения -const currentPage = ref(1); // Текущая страница -const hasMoreMessages = ref(true); // Есть ли еще сообщения -const isLoadingMore = ref(false); // Загружаются ли дополнительные сообщения +const isLoadingMore = ref(false); +const hasMoreMessages = ref(false); const offset = ref(0); const limit = ref(20); -// Вычисляемое свойство для отображаемых сообщений -const displayedMessages = computed(() => { - const startIndex = Math.max(allMessages.value.length - (PAGE_SIZE * currentPage.value), 0); - return allMessages.value.slice(startIndex); -}); +// Состояния для верификации +const showTelegramVerification = ref(false); +const telegramVerificationCode = ref(''); +const telegramBotLink = ref(''); +const telegramAuthCheckInterval = ref(null); +const showEmailVerification = ref(false); +const emailVerificationCode = ref(''); +const emailInput = ref(''); +const emailError = ref(''); + +// Функция для копирования кода +const copyCode = (code) => { + navigator.clipboard.writeText(code); + // Можно добавить уведомление о копировании +}; + +// Функция для показа ошибок +const showError = (message) => { + // Можно использовать toast или alert + alert(message); +}; + +// Обработчик для 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'); + if (response.data.authenticated) { + auth.updateAuth({ + isAuthenticated: true, + authType: response.data.authType, + userId: response.data.userId + }); + + clearInterval(telegramAuthCheckInterval.value); + telegramAuthCheckInterval.value = null; + showTelegramVerification.value = false; + + // Перезагружаем страницу для полного обновления состояния + window.location.reload(); + } + } 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); + showError('Ошибка при инициализации Telegram аутентификации'); + } +}; + +// Обработчик для Email аутентификации +const handleEmailAuth = async () => { + try { + // Запрашиваем email у пользователя + const email = prompt('Введите ваш email:'); + if (!email) return; + + const { data } = await axios.post('/api/auth/email/init', { email }); + if (data.success) { + showEmailVerification.value = true; + emailInput.value = email; + } + } catch (error) { + console.error('Error initializing email auth:', error); + emailError.value = error.response?.data?.error || 'Ошибка отправки кода'; + } +}; // Функция для сокращения адреса кошелька const truncateAddress = (address) => { @@ -246,38 +316,32 @@ watch(() => isAuthenticated.value, async (newValue) => { } }); -// Находим существующую функцию handleWalletAuth и обновляем её +// Функция для подключения кошелька const handleWalletAuth = async () => { + if (isConnecting.value || isAuthenticated.value) return; // Предотвращаем повторное подключение + + isConnecting.value = true; try { const result = await connectWithWallet(); - await auth.checkAuth(); + console.log('Wallet connection result:', result); - if (result.authenticated) { - // Сохраняем гостевые сообщения перед очисткой - const guestMessages = [...messages.value]; - messages.value = []; - offset.value = 0; - hasMoreMessages.value = true; + if (result.success) { + // Обновляем состояние авторизации + await auth.checkAuth(); - try { - await api.post('/api/chat/link-guest-messages'); - console.log('Guest messages linked to authenticated user'); - await loadMoreMessages(); - - const filteredGuestMessages = guestMessages - .filter(msg => !msg.showAuthButtons) - .reverse(); - messages.value = [...messages.value, ...filteredGuestMessages]; - - await nextTick(); - scrollToBottom(); - } catch (linkError) { - console.error('Error linking guest messages:', linkError); - } + // Добавляем небольшую задержку перед сбросом состояния 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; }; // Функция для сохранения гостевых сообщений на сервере @@ -302,103 +366,6 @@ const saveGuestMessagesToServer = async () => { } }; -// Функция для подключения через Telegram -async function connectTelegram() { - try { - // Отправляем запрос на получение ссылки для авторизации через Telegram - const response = await api.get('/api/auth/telegram', { - withCredentials: true - }); - - if (response.data.error) { - messages.value.push({ - sender: 'ai', - text: `Ошибка при подключении Telegram: ${response.data.error}`, - timestamp: new Date(), - }); - return; - } - - if (response.data.authUrl) { - messages.value.push({ - sender: 'ai', - text: `Для подключения Telegram, перейдите по этой ссылке и авторизуйтесь.`, - timestamp: new Date(), - }); - - // Открываем ссылку в новом окне - window.open(response.data.authUrl, '_blank'); - } else { - messages.value.push({ - sender: 'ai', - text: 'Для подключения Telegram, перейдите по этой ссылке и авторизуйтесь.', - timestamp: new Date(), - }); - } - } catch (error) { - console.error('Error connecting with Telegram:', error); - - messages.value.push({ - sender: 'ai', - text: 'Извините, произошла ошибка при подключении Telegram. Пожалуйста, попробуйте позже.', - timestamp: new Date(), - }); - } -} - -// Запрос кода подтверждения по email -async function requestEmailCode() { - emailErrorMessage.value = ''; - - try { - const response = await auth.requestEmailVerification(email.value); - - if (response.success) { - showEmailVerification.value = true; - // Временно для тестирования - if (response.verificationCode) { - emailErrorMessage.value = `Код для тестирования: ${response.verificationCode}`; - } - } else { - emailErrorMessage.value = response.error || 'Ошибка запроса кода подтверждения'; - } - } catch (error) { - console.error('Error requesting email verification:', error); - emailErrorMessage.value = 'Ошибка запроса кода подтверждения'; - } -} - -// Функция проверки кода -const verifyEmailCode = async () => { - try { - const response = await api.post('/api/auth/email/verify-code', { - email: emailInput.value, - code: emailCode.value - }); - - if (response.data.success) { - auth.setEmailAuth(response.data); - showEmailVerification.value = false; - emailError.value = ''; - - // Загружаем историю чата после успешной аутентификации - await loadMoreMessages(); - } else { - emailError.value = response.data.error || 'Неверный код'; - } - } catch (error) { - emailError.value = error.response?.data?.error || 'Ошибка проверки кода'; - console.error('Error verifying email code:', error); - } -}; - -// Отмена верификации email -function cancelEmailVerification() { - showEmailVerification.value = false; - emailVerificationCode.value = ''; - emailErrorMessage.value = ''; -} - // Форматирование времени const formatTime = (timestamp) => { if (!timestamp) return ''; @@ -499,97 +466,10 @@ const handleMessage = async (text) => { } }; -// Добавляем методы для аутентификации -const handleTelegramAuth = () => { - window.open('https://t.me/HB3_Accelerator_Bot', '_blank'); - // Показываем форму для ввода кода через небольшую задержку - setTimeout(() => { - showTelegramVerification.value = true; - }, 1000); -}; - -const handleEmailAuth = async () => { - showEmailForm.value = true; -}; - -// Функция отправки email -const submitEmail = async () => { - try { - const response = await api.post('/api/auth/email/request', { - email: emailInput.value - }); - - if (response.data.success) { - showEmailForm.value = false; - showEmailVerification.value = true; - } else { - emailError.value = response.data.error || 'Ошибка отправки кода'; - } - } catch (error) { - emailError.value = 'Ошибка отправки кода'; - console.error('Error sending email code:', error); - } -}; - -// Функция верификации кода Telegram -const verifyTelegramCode = async () => { - try { - const response = await api.post('/api/auth/telegram/verify', { - code: telegramCode.value - }); - - if (response.data.success) { - console.log('Telegram verification successful:', response.data); - - // Обновляем состояние аутентификации - auth.setAuth({ - isAuthenticated: response.data.authenticated, - userId: response.data.userId, - telegramId: response.data.telegramId, - isAdmin: response.data.isAdmin, - authType: 'telegram' - }); - - showTelegramVerification.value = false; - telegramCode.value = ''; - - // Показываем сообщение об успехе - messages.value.push({ - id: Date.now(), - content: 'Telegram успешно подключен!', - role: 'assistant', - timestamp: new Date().toISOString() - }); - - // Загружаем историю чата после небольшой задержки - setTimeout(async () => { - await loadMoreMessages(); - }, 100); - } else { - messages.value.push({ - id: Date.now(), - content: response.data.error || 'Ошибка верификации кода', - role: 'assistant', - timestamp: new Date().toISOString() - }); - } - } catch (error) { - console.error('Error verifying Telegram code:', error); - messages.value.push({ - id: Date.now(), - content: 'Произошла ошибка. Пожалуйста, попробуйте позже.', - role: 'assistant', - timestamp: new Date().toISOString() - }); - } -}; - const disconnectWallet = async () => { try { await auth.disconnect(); - messages.value = []; - offset.value = 0; - hasMoreMessages.value = true; + console.log('Wallet disconnected successfully'); } catch (error) { console.error('Error disconnecting wallet:', error); } @@ -612,6 +492,20 @@ onMounted(() => { if (messagesContainer.value) { messagesContainer.value.addEventListener('scroll', handleScroll); } + console.log('Auth state on mount:', { + isAuthenticated: auth.isAuthenticated.value, + authType: auth.authType.value, + telegramId: auth.telegramId.value + }); + + // Добавляем отладочный вывод для auth.authType + console.log('auth.authType:', auth.authType); + console.log('auth.authType.value:', auth.authType.value); + console.log('auth.authType.value === "telegram":', auth.authType.value === 'telegram'); +}); + +watch(() => auth.telegramId.value, (newValue) => { + console.log('Telegram ID changed:', newValue); }); onBeforeUnmount(() => { @@ -619,6 +513,9 @@ onBeforeUnmount(() => { if (messagesContainer.value) { messagesContainer.value.removeEventListener('scroll', handleScroll); } + if (telegramAuthCheckInterval.value) { + clearInterval(telegramAuthCheckInterval.value); + } }); @@ -897,7 +794,7 @@ h1 { box-sizing: border-box; } -.auth-btn { +.auth-btn, .disconnect-btn { padding: 8px 16px; border: none; border-radius: 4px; @@ -905,34 +802,43 @@ h1 { font-size: 14px; display: flex; align-items: center; - gap: 8px; + justify-content: center; + margin: 0; } .wallet-btn { - background-color: #4a5568; + background-color: #4CAF50; color: white; } .wallet-btn:hover { - background-color: #2d3748; + background-color: #45a049; +} + +.disconnect-btn { + background-color: #f44336; + color: white; +} + +.disconnect-btn:hover { + background-color: #d32f2f; +} + +.auth-buttons, .wallet-info { + display: flex; + align-items: center; + gap: 10px; } .auth-icon { - font-size: 16px; + margin-right: 5px; } -.telegram-btn { - background-color: #0088cc; +.connecting-info { + padding: 8px 16px; + background-color: #2196F3; color: white; -} - -.email-btn { - background-color: #4caf50; - color: white; -} - -.cancel-btn { - background-color: #999; + border-radius: 4px; } .error-message { @@ -963,13 +869,17 @@ h1 { color: white; } +.wallet-btn:hover { + background-color: #2d3748; +} + .telegram-btn { background-color: #0088cc; color: white; } .email-btn { - background-color: #48bb78; + background-color: #4caf50; color: white; } @@ -1102,4 +1012,103 @@ h1 { padding: 1rem; color: #666; } + +/* Добавляем отображение кода и ссылки для Telegram */ +.verification-info { + padding: 10px; + background-color: #f9f9f9; + border: 1px solid #ddd; + border-radius: 4px; + margin-top: 10px; +} + +.verification-info p { + margin: 5px 0; +} + +.verification-info strong { + font-weight: bold; +} + +.verification-info a { + color: #007bff; + text-decoration: none; +} + +.verification-info a:hover { + text-decoration: underline; +} + +.verification-block { + display: flex; + flex-direction: column; + gap: 8px; + padding: 10px; + background: #f5f5f5; + border-radius: 8px; + margin: 8px 0; +} + +.verification-code { + display: flex; + align-items: center; + gap: 8px; +} + +.verification-code code { + background: #fff; + padding: 4px 8px; + border-radius: 4px; + font-family: monospace; + cursor: pointer; + border: 1px solid #ddd; +} + +.verification-code code:hover { + background: #f0f0f0; +} + +.bot-link { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 8px 16px; + background: #0088cc; + color: white; + text-decoration: none; + border-radius: 4px; + transition: background-color 0.2s; +} + +.bot-link:hover { + background: #006699; +} + +.auth-icon { + font-size: 1.2em; +} + +/* Добавляем новые стили для информации о пользователе */ +.auth-info { + margin-top: 10px; + padding: 10px; + border: 1px solid #ccc; + border-radius: 4px; + background-color: #f9f9f9; +} + +.auth-info button { + padding: 8px 16px; + background-color: #ff4444; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + margin-top: 10px; +} + +.auth-info button:hover { + background-color: #cc0000; +}