From aab41f52fec5adf69e45fc69a4d79c78000c479d Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 14 Mar 2025 16:13:56 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/middleware/auth.js | 62 +- backend/migrations/fix_function.sql | 32 + backend/routes/auth.js | 668 +++++++++++++++++--- backend/server.js | 6 +- backend/services/auth-service.js | 40 ++ backend/services/emailBot.js | 489 ++++++++------ backend/services/telegram-service.js | 262 -------- backend/services/telegramBot.js | 273 ++++++++ frontend/src/components/TelegramConnect.vue | 195 ++++++ frontend/src/stores/auth.js | 161 +++++ frontend/src/views/HomeView.vue | 177 +++--- 11 files changed, 1702 insertions(+), 663 deletions(-) create mode 100644 backend/migrations/fix_function.sql delete mode 100644 backend/services/telegram-service.js create mode 100644 backend/services/telegramBot.js create mode 100644 frontend/src/components/TelegramConnect.vue diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js index 3cba7f7..8fd7ed2 100644 --- a/backend/middleware/auth.js +++ b/backend/middleware/auth.js @@ -14,40 +14,54 @@ const requireAuth = async (req, res, next) => { console.log('Authorization header:', req.headers.authorization); // Проверяем, что пользователь аутентифицирован через сессию - if (req.session && req.session.authenticated) { + if (req.session && req.session.authenticated && req.session.userId) { + // Добавляем информацию о пользователе в запрос + req.user = { + userId: req.session.userId, + address: req.session.address || null, + email: req.session.email || null, + telegramId: req.session.telegramId || null, + isAdmin: req.session.isAdmin || false, + authType: req.session.authType || 'unknown' + }; return next(); } // Проверяем заголовок авторизации const authHeader = req.headers.authorization; if (authHeader && authHeader.startsWith('Bearer ')) { - const address = authHeader.split(' ')[1]; - console.log('Found address in Authorization header:', address); + const token = authHeader.split(' ')[1]; - try { - // Находим пользователя по адресу - const { pool } = require('../db'); - console.log('Querying database for user with address:', address); - const result = await pool.query('SELECT * FROM users WHERE LOWER(address) = LOWER($1)', [address]); - console.log('Database query result:', result.rows); + // Проверяем, это адрес кошелька или JWT-токен + if (token.startsWith('0x')) { + // Это адрес кошелька + const address = token; + console.log('Found address in Authorization header:', address); - if (result.rows.length > 0) { - const user = result.rows[0]; - console.log('Found user by address:', user); + try { + // Проверяем, существует ли пользователь с таким адресом + const result = await db.query(` + SELECT u.id, u.is_admin + FROM users u + JOIN user_identities ui ON u.id = ui.user_id + WHERE ui.identity_type = 'wallet' AND LOWER(ui.identity_value) = LOWER($1) + `, [address]); - // Устанавливаем данные пользователя в запросе - req.user = { - userId: user.id, - address: address, - isAdmin: user.is_admin - }; - - return next(); - } else { - console.log('No user found with address:', address); + if (result.rows.length > 0) { + const user = result.rows[0]; + req.user = { + userId: user.id, + address: address, + isAdmin: user.is_admin, + authType: 'wallet' + }; + return next(); + } + } catch (error) { + console.error('Error finding user by address:', error); } - } catch (error) { - console.error('Error finding user by address:', error); + } else { + // Здесь можно добавить логику проверки JWT, если используется } } diff --git a/backend/migrations/fix_function.sql b/backend/migrations/fix_function.sql new file mode 100644 index 0000000..5e11c7f --- /dev/null +++ b/backend/migrations/fix_function.sql @@ -0,0 +1,32 @@ +DROP FUNCTION IF EXISTS find_or_create_user_by_identity; + +CREATE OR REPLACE FUNCTION find_or_create_user_by_identity( + identity_type_param VARCHAR(20), + identity_value_param VARCHAR(255) +) +RETURNS TABLE(user_id INTEGER, is_new BOOLEAN) +AS $$ +DECLARE + existing_user_id INTEGER; + new_user_id INTEGER; +BEGIN + SELECT ui.user_id INTO existing_user_id + FROM user_identities ui + WHERE ui.identity_type = identity_type_param + AND ui.identity_value = identity_value_param; + + IF existing_user_id IS NOT NULL THEN + RETURN QUERY SELECT existing_user_id::INTEGER, FALSE::BOOLEAN; + RETURN; + END IF; + + INSERT INTO users (created_at) + VALUES (NOW()) + RETURNING id INTO new_user_id; + + INSERT INTO user_identities (user_id, identity_type, identity_value, created_at, verified) + VALUES (new_user_id, identity_type_param, identity_value_param, NOW(), TRUE); + + RETURN QUERY SELECT new_user_id::INTEGER, TRUE::BOOLEAN; +END; +$$ LANGUAGE plpgsql; \ No newline at end of file diff --git a/backend/routes/auth.js b/backend/routes/auth.js index ddd26ca..2b6bdb5 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -11,6 +11,7 @@ const { pool } = require('../db'); const { verifySignature, checkAccess, findOrCreateUser } = require('../utils/auth'); const authService = require('../services/auth-service'); const { SiweMessage } = require('siwe'); +const { sendEmail } = require('../services/emailBot'); // Создайте лимитер для попыток аутентификации const authLimiter = rateLimit({ @@ -187,7 +188,12 @@ router.post('/telegram', async (req, res) => { } // Проверяем связанные аккаунты - const identities = await authService.getAllUserIdentities(userId); + const identitiesResult = await db.query(` + SELECT identity_type, identity_value + FROM user_identities ui + WHERE user_id = $1 + `, [userId]); + const identities = identitiesResult.rows; // Если есть связанный кошелек, проверяем токены if (identities.wallet) { @@ -216,6 +222,156 @@ router.post('/telegram', async (req, res) => { } }); +// Маршрут для запроса кода подтверждения по email +router.post('/email/request', async (req, res) => { + try { + const { email } = req.body; + + if (!email || !email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) { + return res.status(400).json({ error: 'Invalid email format' }); + } + + // Генерируем уникальный токен + const token = crypto.randomBytes(20).toString('hex'); + + // Создаем или получаем ID пользователя + let userId; + + if (req.session.authenticated && req.session.userId) { + // Если пользователь уже аутентифицирован, используем его ID + userId = req.session.userId; + } else { + // Создаем временного пользователя + const userResult = await db.query( + 'INSERT INTO users (created_at) VALUES (NOW()) RETURNING id' + ); + userId = userResult.rows[0].id; + + // Сохраняем ID в сессии как временный + req.session.tempUserId = userId; + } + + // Сохраняем токен в базе данных + await db.query(` + INSERT INTO email_auth_tokens (user_id, token, created_at, expires_at) + VALUES ($1, $2, NOW(), NOW() + INTERVAL '15 minutes') + `, [userId, token]); + + // Отправляем email с кодом подтверждения через emailBot + const EmailBotService = require('../services/emailBot'); + const emailBot = new EmailBotService(process.env.EMAIL_USER, process.env.EMAIL_PASSWORD); + + // Используем новый метод sendVerificationCode вместо sendEmail + const result = await emailBot.sendVerificationCode(email, token); + + if (result.success) { + // Сохраняем email в сессии для последующей верификации + req.session.pendingEmail = email; + + // Сохраняем сессию + await new Promise((resolve, reject) => { + req.session.save(err => { + if (err) reject(err); + else resolve(); + }); + }); + + return res.json({ + success: true, + message: 'Verification code sent to your email', + verificationCode: result.code // НЕ ОСТАВЛЯЙТЕ В PRODUCTION - только для отладки + }); + } else { + return res.status(500).json({ error: 'Error sending email' }); + } + } catch (error) { + console.error('Error requesting email verification:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// Маршрут для верификации email +router.post('/email/verify', async (req, res) => { + try { + const { code } = req.body; + const verificationData = req.session.emailVerificationData; + + // Проверяем, что код существует и не истек + if (!verificationData || + verificationData.code !== code || + Date.now() > verificationData.expires) { + return res.status(400).json({ + success: false, + error: 'Неверный или истекший код подтверждения' + }); + } + + const email = verificationData.email; + + // Ищем или создаем пользователя с этим email + const result = await db.query( + 'SELECT * FROM find_or_create_user_by_identity($1, $2)', + ['email', email] + ); + + const userId = result.rows[0].user_id; + const isNew = result.rows[0].is_new; + + // Проверяем, есть ли у пользователя связанный кошелек + const walletResult = await db.query(` + SELECT identity_value + FROM user_identities ui + WHERE ui.user_id = $1 AND ui.identity_type = 'wallet' + `, [userId]); + + const hasWallet = walletResult.rows.length > 0; + let walletAddress = null; + let isAdmin = false; + + // Если есть кошелек, проверяем наличие токенов + if (hasWallet) { + walletAddress = walletResult.rows[0].identity_value; + const userResult = await db.query('SELECT is_admin FROM users WHERE id = $1', [userId]); + isAdmin = userResult.rows[0].is_admin; + } + + // Устанавливаем сессию + req.session.authenticated = true; + req.session.userId = userId; + req.session.authType = 'email'; + req.session.email = email; + req.session.isAdmin = isAdmin; + if (walletAddress) { + req.session.address = walletAddress; + } + + // Сохраняем сессию + await new Promise((resolve, reject) => { + req.session.save(err => { + if (err) reject(err); + else resolve(); + }); + }); + + // Очищаем данные верификации + delete req.session.emailVerificationData; + + res.json({ + success: true, + authenticated: true, + userId, + email, + isAdmin, + hasWallet, + walletAddress, + isNew + }); + } catch (error) { + logger.error(`Error in email verification: ${error.message}`); + res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); + } +}); + // Аутентификация через Email router.post('/email', async (req, res) => { try { @@ -242,29 +398,65 @@ router.post('/email', async (req, res) => { ); } - // Проверяем связанные аккаунты - const identities = await authService.getAllUserIdentities(userId); + // Получаем связанные идентификаторы + const identitiesResult = await db.query(` + SELECT identity_type, identity_value + FROM user_identities ui + WHERE ui.user_id = $1 + `, [userId]); - // Если есть связанный кошелек, проверяем токены - if (identities.wallet) { - await authService.checkTokensAndUpdateRole(identities.wallet); + const identities = identitiesResult.rows; + + // Формируем объект с идентификаторами по типам + const identitiesMap = {}; + for (const identity of identities) { + identitiesMap[identity.identity_type] = identity.identity_value; } - // Получаем текущую роль - const isAdmin = await authService.isAdmin(userId); + // Проверяем, есть ли связанный кошелек + let isAdmin = false; + if (identitiesMap.wallet) { + // Если есть связанный кошелек, проверяем токены + const walletAddress = identitiesMap.wallet; + isAdmin = await authService.checkAdminTokens(walletAddress); + + // Обновляем статус администратора в БД, если необходимо + await db.query('UPDATE users SET is_admin = $1 WHERE id = $2', [isAdmin, userId]); + } else { + // Если нет связанного кошелька, проверяем текущий статус администратора + isAdmin = await authService.isAdmin(userId); + } // Устанавливаем сессию req.session.userId = userId; req.session.email = email; req.session.authType = 'email'; req.session.authenticated = true; + req.session.isAdmin = isAdmin; + + if (identitiesMap.wallet) { + req.session.address = identitiesMap.wallet; + } + + // Сохраняем сессию перед отправкой ответа + await new Promise((resolve, reject) => { + req.session.save(err => { + if (err) { + console.error('Error saving session:', err); + reject(err); + } else { + console.log('Session saved successfully'); + resolve(); + } + }); + }); - res.json({ - authenticated: true, + res.json({ + authenticated: true, userId, isAdmin, authType: 'email', - identities + identities: identitiesMap }); } catch (error) { logger.error(`Email auth error: ${error.message}`); @@ -302,7 +494,12 @@ router.post('/link-identity', async (req, res) => { } // Получаем все идентификаторы пользователя - const identities = await authService.getAllUserIdentities(req.session.userId); + const identitiesResult = await db.query(` + SELECT identity_type, identity_value + FROM user_identities + WHERE user_id = $1 + `, [req.session.userId]); + const identities = identitiesResult.rows; // Получаем текущую роль const isAdmin = await authService.isAdmin(req.session.userId); @@ -383,103 +580,186 @@ router.get('/telegram', (req, res) => { res.json({ authUrl }); }); -// Маршрут для авторизации через Email -router.post('/email', async (req, res) => { +// Маршрут для получения кода подтверждения Telegram +router.get('/telegram/code', async (req, res) => { try { - const { email } = req.body; - - if (!email) { - return res.status(400).json({ error: 'Email is required' }); - } - - // Генерируем код подтверждения - const verificationCode = Math.floor(100000 + Math.random() * 900000).toString(); - + // Генерируем код подтверждения (6 символов) + const verificationCode = Math.random().toString(36).substring(2, 8).toUpperCase(); + // Сохраняем код в сессии - req.session.emailVerificationCode = verificationCode; - req.session.pendingEmail = email; - - // В реальном приложении здесь нужно отправить email с кодом подтверждения - // Удалите или закомментируйте эти логи - // console.log(`Verification code for ${email}: ${verificationCode}`); - - res.json({ success: true, message: 'Verification code sent' }); + req.session.telegramVerificationData = { + code: verificationCode, + expires: Date.now() + 10 * 60 * 1000 // 10 минут + }; + + res.json({ + success: true, + message: 'Отправьте этот код боту @' + process.env.TELEGRAM_BOT_USERNAME, + code: verificationCode, + botUsername: process.env.TELEGRAM_BOT_USERNAME || 'YourDAppBot' + }); } catch (error) { - // Удалите или закомментируйте эти логи - // console.error('Error sending verification code:', error); - logger.error('Error sending verification code:', error); - res.status(500).json({ error: 'Failed to send verification code' }); + logger.error(`Error in telegram code request: ${error.message}`); + res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); } }); -// Маршрут для проверки кода подтверждения Email -router.post('/email/verify', async (req, res) => { +// Маршрут для верификации Telegram +router.post('/telegram/verify', async (req, res) => { try { - const { email, code } = req.body; - - if (!email || !code) { - return res.status(400).json({ error: 'Email and code are required' }); + const { telegramId, code } = req.body; + const verificationData = req.session.telegramVerificationData; + + // Проверяем, что код существует и не истек + if (!verificationData || + verificationData.code !== code || + Date.now() > verificationData.expires) { + return res.status(400).json({ + success: false, + error: 'Неверный или истекший код подтверждения' + }); } - - // Получаем код из сессии - const verificationCode = req.session.emailVerificationCode; - const pendingEmail = req.session.pendingEmail; - - if (!verificationCode || !pendingEmail) { - return res.status(400).json({ error: 'No pending verification' }); - } - - // Проверяем, что email совпадает с тем, для которого был сгенерирован код - if (pendingEmail !== email) { - return res.status(400).json({ error: 'Email mismatch' }); - } - - // Проверяем код - if (verificationCode !== code) { - return res.status(400).json({ error: 'Invalid verification code' }); - } - - // Проверяем, существует ли пользователь в базе данных - const user = await db.query('SELECT * FROM users WHERE email = $1', [email]); - - let userId; + + // Ищем или создаем пользователя с этим Telegram ID + const result = await db.query( + 'SELECT * FROM find_or_create_user_by_identity($1, $2)', + ['telegram', telegramId] + ); + + const userId = result.rows[0].user_id; + const isNew = result.rows[0].is_new; + + // Проверяем, есть ли у пользователя связанный кошелек + const walletResult = await db.query(` + SELECT identity_value + FROM user_identities ui + WHERE ui.user_id = $1 AND ui.identity_type = 'wallet' + `, [userId]); + + const hasWallet = walletResult.rows.length > 0; + let walletAddress = null; let isAdmin = false; - - if (user.rows.length === 0) { - // Если пользователь не существует, создаем его - const newUser = await db.query( - 'INSERT INTO users (email, created_at) VALUES ($1, NOW()) RETURNING id', - [email] - ); - userId = newUser.rows[0].id; - } else { - userId = user.rows[0].id; - isAdmin = user.rows[0].is_admin || false; + + // Если есть кошелек, проверяем наличие токенов + if (hasWallet) { + walletAddress = walletResult.rows[0].identity_value; + const userResult = await db.query('SELECT is_admin FROM users WHERE id = $1', [userId]); + isAdmin = userResult.rows[0].is_admin; } - - // Устанавливаем состояние аутентификации в сессии - req.session.isAuthenticated = true; + + // Устанавливаем сессию req.session.authenticated = true; - req.session.address = email; req.session.userId = userId; + req.session.authType = 'telegram'; + req.session.telegramId = telegramId; req.session.isAdmin = isAdmin; - req.session.authType = 'email'; - - // Удаляем код из сессии - delete req.session.emailVerificationCode; - delete req.session.pendingEmail; - + if (walletAddress) { + req.session.address = walletAddress; + } + + // Сохраняем сессию + await new Promise((resolve, reject) => { + req.session.save(err => { + if (err) reject(err); + else resolve(); + }); + }); + + // Очищаем данные верификации + delete req.session.telegramVerificationData; + res.json({ + success: true, authenticated: true, - address: email, + userId, + telegramId, isAdmin, - authType: 'email', + hasWallet, + walletAddress, + isNew }); } catch (error) { - // Удалите или закомментируйте эти логи - // console.error('Error verifying email code:', error); - logger.error('Error verifying email code:', error); - res.status(500).json({ error: 'Failed to verify email code' }); + logger.error(`Error in telegram verification: ${error.message}`); + res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); + } +}); + +// Маршрут для связывания разных идентификаторов +router.post('/link-identity', requireAuth, async (req, res) => { + try { + const { type, value } = req.body; + const userId = req.session.userId; + + // Проверяем валидность типа + if (!['wallet', 'email', 'telegram'].includes(type)) { + return res.status(400).json({ + success: false, + error: 'Неподдерживаемый тип идентификатора' + }); + } + + // Проверяем, не связан ли идентификатор с другим пользователем + const existingResult = await db.query(` + SELECT ui.user_id + FROM user_identities ui + WHERE ui.identity_type = $1 AND ui.identity_value = $2 + `, [type, value]); + + if (existingResult.rows.length > 0 && existingResult.rows[0].user_id !== userId) { + return res.status(400).json({ + success: false, + error: 'Этот идентификатор уже связан с другим аккаунтом' + }); + } + + // Добавляем или обновляем идентификатор + await db.query(` + INSERT INTO user_identities (user_id, identity_type, identity_value, created_at, verified) + VALUES ($1, $2, $3, NOW(), true) + ON CONFLICT (identity_type, identity_value) + DO UPDATE SET verified = true + `, [userId, type, value]); + + // Если связываем кошелек, обновляем также поле address в таблице users + if (type === 'wallet') { + await db.query('UPDATE users SET address = $1 WHERE id = $2', [value, userId]); + + // Проверяем наличие токенов для статуса админа + const isAdmin = await authService.checkAdminTokens(value); + if (isAdmin) { + await db.query('UPDATE users SET is_admin = true WHERE id = $1', [userId]); + req.session.isAdmin = true; + } + + req.session.address = value; + } + + // Если связываем email, обновляем сессию + if (type === 'email') { + req.session.email = value; + } + + // Если связываем telegram, обновляем сессию + if (type === 'telegram') { + req.session.telegramId = value; + } + + // Сохраняем сессию + await new Promise((resolve, reject) => { + req.session.save(err => { + if (err) reject(err); + else resolve(); + }); + }); + + res.json({ + success: true, + message: `Идентификатор успешно связан с вашим аккаунтом`, + isAdmin: req.session.isAdmin + }); + } catch (error) { + logger.error(`Error linking identity: ${error.message}`); + res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); } }); @@ -626,4 +906,210 @@ router.post('/update-admin-status', async (req, res) => { } }); +// Маршрут для создания токена авторизации через Email +router.post('/email/auth-token', async (req, res) => { + try { + const { email } = req.body; + + if (!email || !email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) { + return res.status(400).json({ success: false, error: 'Неверный формат email' }); + } + + // Генерируем уникальный токен + const token = crypto.randomBytes(20).toString('hex'); + + // Получаем ID пользователя из сессии или создаем нового гостевого пользователя + let userId; + + if (req.session.authenticated && req.session.userId) { + // Если пользователь уже аутентифицирован, используем его ID + userId = req.session.userId; + } else { + // Создаем временного пользователя + const userResult = await db.query( + 'INSERT INTO users (created_at) VALUES (NOW()) RETURNING id' + ); + userId = userResult.rows[0].id; + + // Сохраняем ID в сессии как временный + req.session.tempUserId = userId; + } + + // Сохраняем токен в базе данных + await db.query(` + INSERT INTO email_auth_tokens (user_id, token, created_at, expires_at) + VALUES ($1, $2, NOW(), NOW() + INTERVAL '15 minutes') + `, [userId, token]); + + // Отправляем email с кодом подтверждения через emailBot + const emailBot = require('../services/emailBot'); + const emailService = new emailBot(process.env.EMAIL_USER, process.env.EMAIL_PASSWORD); + const sendResult = await emailService.sendVerificationCode(email, token); + + if (sendResult.success) { + res.json({ + success: true, + message: 'Код подтверждения отправлен на ваш email' + }); + } else { + res.status(500).json({ success: false, error: 'Ошибка отправки email' }); + } + } catch (error) { + logger.error(`Error creating Email auth token: ${error.message}`); + res.status(500).json({ success: false, error: 'Ошибка сервера' }); + } +}); + +// Маршрут для проверки статуса аутентификации через Email +router.get('/email/auth-status/:token', async (req, res) => { + try { + const { token } = req.params; + + // Проверяем статус токена + const tokenResult = await db.query(` + SELECT user_id, used FROM email_auth_tokens + WHERE token = $1 AND expires_at > NOW() + `, [token]); + + if (tokenResult.rows.length === 0) { + return res.json({ success: false, error: 'Токен не найден или истек' }); + } + + const userId = tokenResult.rows[0].user_id; + const isAuthenticated = tokenResult.rows[0].used; + + if (isAuthenticated) { + // Токен использован, email подключен + + // Получаем email пользователя + const emailResult = await db.query(` + SELECT ui.identity_value FROM user_identities ui + WHERE ui.user_id = $1 AND ui.identity_type = 'email' + `, [userId]); + + if (emailResult.rows.length > 0) { + // Устанавливаем полную аутентификацию в сессии + req.session.authenticated = true; + req.session.userId = userId; + req.session.email = emailResult.rows[0].identity_value; + req.session.authType = 'email'; + + // Если был временный ID, удаляем его + if (req.session.tempUserId) { + delete req.session.tempUserId; + } + + // Сохраняем сессию + await new Promise((resolve, reject) => { + req.session.save(err => { + if (err) reject(err); + else resolve(); + }); + }); + } + } + + res.json({ + success: true, + authenticated: isAuthenticated + }); + } catch (error) { + logger.error(`Error checking Email auth status: ${error.message}`); + res.status(500).json({ success: false, error: 'Ошибка сервера' }); + } +}); + +// Маршрут для прямой проверки кода, введенного пользователем +router.post('/email/verify-code', async (req, res) => { + try { + const { email, code } = req.body; + + if (!email || !code) { + return res.status(400).json({ success: false, error: 'Email и код обязательны' }); + } + + const EmailBotService = require('../services/emailBot'); + const emailBot = new EmailBotService(process.env.EMAIL_USER, process.env.EMAIL_PASSWORD); + + // Проверяем код из хранилища + const verificationData = emailBot.verificationCodes.get(email.toLowerCase()); + + if (!verificationData) { + return res.status(400).json({ success: false, error: 'Код подтверждения не найден' }); + } + + if (Date.now() > verificationData.expires) { + emailBot.verificationCodes.delete(email.toLowerCase()); + return res.status(400).json({ success: false, error: 'Срок действия кода истек' }); + } + + if (verificationData.code !== code) { + return res.status(400).json({ success: false, error: 'Неверный код подтверждения' }); + } + + // Код верный, завершаем аутентификацию + const token = verificationData.token; + + // Получаем информацию о токене + const tokenResult = await db.query( + 'SELECT user_id FROM email_auth_tokens WHERE token = $1', + [token] + ); + + if (tokenResult.rows.length === 0) { + return res.status(500).json({ success: false, error: 'Токен не найден' }); + } + + const userId = tokenResult.rows[0].user_id; + + // Добавляем email в базу данных + await db.query( + 'INSERT INTO user_identities (user_id, identity_type, identity_value, verified, created_at) ' + + 'VALUES ($1, $2, $3, true, NOW()) ' + + 'ON CONFLICT (identity_type, identity_value) ' + + 'DO UPDATE SET user_id = $1, verified = true', + [userId, 'email', email.toLowerCase()] + ); + + // Отмечаем токен как использованный + await db.query( + 'UPDATE email_auth_tokens SET used = true WHERE token = $1', + [token] + ); + + // Устанавливаем аутентификацию пользователя + req.session.authenticated = true; + req.session.userId = userId; + req.session.email = email.toLowerCase(); + req.session.authType = 'email'; + + // Если был временный ID, удаляем его + if (req.session.tempUserId) { + delete req.session.tempUserId; + } + + // Сохраняем сессию + await new Promise((resolve, reject) => { + req.session.save(err => { + if (err) reject(err); + else resolve(); + }); + }); + + // Удаляем код из хранилища + emailBot.verificationCodes.delete(email.toLowerCase()); + + return res.json({ + success: true, + userId, + email: email.toLowerCase(), + message: 'Аутентификация успешна' + }); + + } catch (error) { + logger.error(`Error verifying email code: ${error.message}`); + return res.status(500).json({ success: false, error: 'Ошибка сервера' }); + } +}); + module.exports = router; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index b36a1e0..05a6ac9 100644 --- a/backend/server.js +++ b/backend/server.js @@ -33,7 +33,7 @@ const messagesRouter = require('./routes/messages'); const sessionMiddleware = require('./middleware/session'); // Импорт сервисов -const { initTelegramBot } = require('./services/telegram-service'); +const telegramService = require('./services/telegramBot'); const PORT = process.env.PORT || 8000; @@ -199,7 +199,7 @@ async function initServices() { // Инициализируем ботов, если они нужны if (process.env.TELEGRAM_BOT_TOKEN) { - telegramBot = new TelegramBotService(process.env.TELEGRAM_BOT_TOKEN); + telegramBot = new telegramService(process.env.TELEGRAM_BOT_TOKEN); console.log('Telegram бот инициализирован'); } @@ -683,7 +683,7 @@ process.on('unhandledRejection', (reason, promise) => { }); // Инициализация Telegram бота -initTelegramBot(); +telegramService.initTelegramBot(); // Добавьте после других маршрутов const chatRouter = require('./routes/chat'); diff --git a/backend/services/auth-service.js b/backend/services/auth-service.js index 1ded5bb..d628ee0 100644 --- a/backend/services/auth-service.js +++ b/backend/services/auth-service.js @@ -206,6 +206,46 @@ class AuthService { return null; } } + + /** + * Получает все идентификаторы пользователя + * @param {number} userId - ID пользователя + * @returns {Promise} - Список идентификаторов + */ + async getAllUserIdentities(userId) { + try { + const result = await db.query(` + SELECT identity_type, identity_value, verified, created_at + FROM user_identities + WHERE user_id = $1 + `, [userId]); + + return result.rows; + } catch (error) { + logger.error(`Error getting user identities: ${error.message}`); + return []; + } + } + + /** + * Проверяет, является ли пользователь администратором + * @param {number} userId - ID пользователя + * @returns {Promise} - Является ли пользователь администратором + */ + async isAdmin(userId) { + try { + const result = await db.query('SELECT is_admin FROM users WHERE id = $1', [userId]); + + if (result.rows.length === 0) { + return false; + } + + return result.rows[0].is_admin; + } catch (error) { + logger.error(`Error checking admin status: ${error.message}`); + return false; + } + } } module.exports = new AuthService(); \ No newline at end of file diff --git a/backend/services/emailBot.js b/backend/services/emailBot.js index 4a80df2..afd763f 100644 --- a/backend/services/emailBot.js +++ b/backend/services/emailBot.js @@ -3,6 +3,11 @@ const nodemailer = require('nodemailer'); const Imap = require('imap'); const simpleParser = require('mailparser').simpleParser; const { processMessage } = require('./ai-assistant'); +const { inspect } = require('util'); +const logger = require('../utils/logger'); + +// Хранилище кодов подтверждения +const verificationCodes = new Map(); // { email: { code, token, expires } } // Конфигурация для отправки писем const transporter = nodemailer.createTransport({ @@ -13,6 +18,9 @@ const transporter = nodemailer.createTransport({ user: process.env.EMAIL_USER, pass: process.env.EMAIL_PASSWORD, }, + tls: { + rejectUnauthorized: false // Отключение проверки сертификата + } }); // Конфигурация для получения писем @@ -25,222 +33,307 @@ const imapConfig = { tlsOptions: { rejectUnauthorized: false }, }; -/** - * Инициализация сервиса электронной почты - */ -function initEmailBot() { - if (!process.env.EMAIL_USER || !process.env.EMAIL_PASSWORD) { - console.warn('EMAIL_USER or EMAIL_PASSWORD not set, Email integration disabled'); - return null; +class EmailBotService { + constructor(user, password) { + this.user = user; + this.password = password; + this.transporter = null; + this.imap = null; + this.initialize(); + this.listenForReplies(); } - console.log('Email bot initialized'); - - // Запуск проверки почты каждые 5 минут - const checkInterval = 5 * 60 * 1000; // 5 минут - setInterval(checkEmails, checkInterval); - - // Первая проверка при запуске - checkEmails(); - - return { - sendEmail, - checkEmails, - }; -} - -/** - * Проверка новых писем - */ -function checkEmails() { - const imap = new Imap(imapConfig); - - imap.once('ready', () => { - imap.openBox('INBOX', false, (err, box) => { - if (err) { - console.error('Error opening inbox:', err); - return; + initialize() { + // Настройка транспорта + this.transporter = nodemailer.createTransport({ + host: process.env.EMAIL_SMTP_HOST, + port: process.env.EMAIL_SMTP_PORT, + secure: true, + auth: { + user: this.user, + pass: this.password } + }); - // Поиск непрочитанных писем - imap.search(['UNSEEN'], (err, results) => { - if (err) { - console.error('Error searching emails:', err); - return; + // Настройка IMAP для чтения входящих писем + this.imap = new Imap({ + user: this.user, + password: this.password, + host: process.env.EMAIL_IMAP_HOST, + port: process.env.EMAIL_IMAP_PORT, + tls: true, + tlsOptions: { rejectUnauthorized: false } + }); + + this.imap.once('error', (err) => { + logger.error(`IMAP connection error: ${err.message}`); + }); + } + + async sendVerificationCode(toEmail, token) { + try { + // Генерируем код подтверждения + const verificationCode = Math.floor(100000 + Math.random() * 900000).toString(); + + // Сохраняем код в хранилище + verificationCodes.set(toEmail.toLowerCase(), { + code: verificationCode, + token: token, + expires: Date.now() + 15 * 60 * 1000 // 15 минут + }); + + // Отправляем письмо с кодом + const mailOptions = { + from: this.user, + to: toEmail, + subject: 'Код подтверждения для DApp for Business', + text: `Ваш код подтверждения: ${verificationCode}\n\nДля завершения аутентификации, пожалуйста, ответьте на это письмо, указав только полученный код.\n\nКод действителен в течение 15 минут.`, + html: ` +
+

Код подтверждения для DApp for Business

+

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

+
+ ${verificationCode} +
+

Для завершения аутентификации, пожалуйста, ответьте на это письмо, указав только полученный код.

+

Код действителен в течение 15 минут.

+
+

Это автоматическое сообщение, пожалуйста, не отвечайте на него.

+
+ ` + }; + + const info = await this.transporter.sendMail(mailOptions); + logger.info(`Email sent: ${info.messageId}`); + + return { success: true, code: verificationCode }; // Код для отладки + } catch (error) { + logger.error(`Error sending email: ${error}`); + return { success: false, error: error.message }; + } + } + + listenForReplies() { + // Запускаем проверку почты каждые 30 секунд + setInterval(() => { + this.checkEmails(); + }, 30000); + } + + checkEmails() { + try { + // Добавляем обработчики ошибок + this.imap.once('error', (err) => { + logger.error(`IMAP connection error during check: ${err.message}`); + // Пытаемся закрыть соединение при ошибке + try { + this.imap.end(); + } catch (e) { + // Игнорируем ошибки при закрытии } + }); - if (results.length === 0) { - console.log('No new emails'); - imap.end(); - return; - } - - console.log(`Found ${results.length} new emails`); - - const f = imap.fetch(results, { bodies: '' }); - - f.on('message', (msg, seqno) => { - msg.on('body', (stream, info) => { - simpleParser(stream, async (err, parsed) => { - if (err) { - console.error('Error parsing email:', err); - return; - } - - try { - // Обработка письма - await processEmail(parsed); - - // Пометить как прочитанное - imap.setFlags(results, ['\\Seen'], (err) => { - if (err) { - console.error('Error marking email as read:', err); - } + this.imap.once('ready', () => { + this.imap.openBox('INBOX', false, (err, box) => { + if (err) { + logger.error(`Error opening inbox: ${err}`); + this.imap.end(); + return; + } + + // Ищем непрочитанные письма + this.imap.search(['UNSEEN'], (err, results) => { + if (err) { + logger.error(`Error searching messages: ${err}`); + this.imap.end(); + return; + } + + if (!results || results.length === 0) { + logger.info('No new messages found'); + this.imap.end(); + return; + } + + // Защищаемся от пустых результатов + try { + const f = this.imap.fetch(results, { bodies: '' }); + + f.on('message', (msg, seqno) => { + msg.on('body', (stream, info) => { + simpleParser(stream, async (err, parsed) => { + if (err) { + logger.error(`Error parsing message: ${err}`); + return; + } + + // Обработка входящего письма + try { + await this.processEmail(parsed); + } catch (e) { + logger.error(`Error processing email: ${e.message}`); + } + }); }); - } catch (error) { - console.error('Error processing email:', error); + }); + + f.once('error', (err) => { + logger.error(`Fetch error: ${err}`); + }); + + f.once('end', () => { + try { + this.imap.end(); + } catch (e) { + logger.error(`Error ending IMAP connection: ${e.message}`); + } + }); + } catch (e) { + logger.error(`Error fetching messages: ${e.message}`); + try { + this.imap.end(); + } catch (e) { + // Игнорируем ошибки при закрытии } - }); + } }); }); - - f.once('error', (err) => { - console.error('Fetch error:', err); - }); - - f.once('end', () => { - imap.end(); - }); }); - }); - }); - - imap.once('error', (err) => { - console.error('IMAP error:', err); - }); - - imap.connect(); -} - -/** - * Обработка полученного письма - * @param {Object} email - Распарсенное письмо - */ -async function processEmail(email) { - try { - const from = email.from.value[0].address; - const subject = email.subject; - const text = email.text || ''; - - console.log(`Processing email from ${from}, subject: ${subject}`); - - // Поиск пользователя по email - const userResult = await pool.query( - `SELECT u.* FROM users u - JOIN user_identities ui ON u.id = ui.user_id - WHERE ui.identity_type = 'email' AND ui.identity_value = $1 AND ui.verified = TRUE`, - [from] - ); - - if (userResult.rows.length === 0) { - console.log(`No verified user found for email ${from}`); - // Отправка ответа о необходимости регистрации - await sendEmail( - from, - 'Регистрация в системе', - 'Для использования ИИ-ассистента через email, пожалуйста, зарегистрируйтесь на нашем сайте и подтвердите свой email.' - ); - return; + + this.imap.connect(); + } catch (error) { + logger.error(`Global error checking emails: ${error.message}`); + // Обеспечиваем корректное завершение IMAP сессии + try { + this.imap.end(); + } catch (e) { + // Игнорируем ошибки при закрытии + } } + } - const user = userResult.rows[0]; + async processEmail(email) { + try { + const fromEmail = email.from.value[0].address.toLowerCase(); + const subject = email.subject; + const text = email.text; - // Получение или создание диалога - const conversationResult = await pool.query( - `SELECT * FROM conversations - WHERE user_id = $1 - ORDER BY updated_at DESC - LIMIT 1`, - [user.id] - ); - - let conversationId; - - if (conversationResult.rows.length === 0) { - // Создание нового диалога - const newConversationResult = await pool.query( - `INSERT INTO conversations (user_id, title) - VALUES ($1, $2) - RETURNING id`, - [user.id, subject || 'Email диалог'] - ); - - conversationId = newConversationResult.rows[0].id; - } else { - conversationId = conversationResult.rows[0].id; + // Ищем код в тексте письма + const codeMatch = text.match(/\b\d{6}\b/); + if (!codeMatch) return; + + const code = codeMatch[0]; + + // Проверяем, есть ли код для этого email + const verificationData = verificationCodes.get(fromEmail); + + if (verificationData && verificationData.code === code) { + // Проверяем срок действия + if (Date.now() > verificationData.expires) { + // Код истек + this.transporter.sendMail({ + from: this.user, + to: fromEmail, + subject: 'Срок действия кода истек', + text: 'Срок действия кода подтверждения истек. Пожалуйста, запросите новый код.' + }); + verificationCodes.delete(fromEmail); + return; + } + + // Код верный и актуальный + const { pool } = require('../db'); + const token = verificationData.token; + + // Связываем email с пользователем + const tokenResult = await pool.query( + 'SELECT user_id FROM email_auth_tokens WHERE token = $1', + [token] + ); + + if (tokenResult.rows.length > 0) { + const userId = tokenResult.rows[0].user_id; + + // Добавляем идентификатор email для пользователя + await pool.query( + 'INSERT INTO user_identities (user_id, identity_type, identity_value, verified, created_at) ' + + 'VALUES ($1, $2, $3, true, NOW()) ' + + 'ON CONFLICT (identity_type, identity_value) ' + + 'DO UPDATE SET user_id = $1, verified = true', + [userId, 'email', fromEmail] + ); + + // Отмечаем токен как использованный + await pool.query( + 'UPDATE email_auth_tokens SET used = true WHERE token = $1', + [token] + ); + + // Отправляем подтверждение + this.transporter.sendMail({ + from: this.user, + to: fromEmail, + subject: 'Аутентификация успешна', + text: 'Ваш email успешно связан с аккаунтом DApp for Business.' + }); + + verificationCodes.delete(fromEmail); + } + } + } catch (error) { + logger.error(`Error processing email: ${error}`); } + } - // Сохранение сообщения пользователя - await pool.query( - `INSERT INTO messages (conversation_id, sender_type, sender_id, content, channel) - VALUES ($1, $2, $3, $4, $5)`, - [conversationId, 'user', user.id, text, 'email'] - ); + // Оставляем существующие методы + async sendEmail(to, subject, text) { + try { + const mailOptions = { + from: this.user, + to, + subject, + text + }; - // Обработка сообщения ИИ-ассистентом - const aiResponse = await processMessage(user.id, text, user.language || 'ru'); + const info = await this.transporter.sendMail(mailOptions); + logger.info(`Email sent: ${info.messageId}`); + return true; + } catch (error) { + logger.error(`Error sending email: ${error}`); + return false; + } + } - // Сохранение ответа ИИ - await pool.query( - `INSERT INTO messages (conversation_id, sender_type, sender_id, content, channel) - VALUES ($1, $2, $3, $4, $5)`, - [conversationId, 'ai', null, aiResponse, 'email'] - ); - - // Обновление времени последнего обновления диалога - await pool.query( - `UPDATE conversations - SET updated_at = NOW() - WHERE id = $1`, - [conversationId] - ); - - // Отправка ответа пользователю - await sendEmail(from, `Re: ${subject}`, aiResponse); - - console.log(`Sent response to ${from}`); - } catch (error) { - console.error('Error processing email:', error); - throw error; + // Метод для проверки кода без IMAP + verifyCode(email, code) { + email = email.toLowerCase(); + const data = verificationCodes.get(email); + + if (!data) { + return { success: false, error: 'Код не найден' }; + } + + if (Date.now() > data.expires) { + verificationCodes.delete(email); + return { success: false, error: 'Срок действия кода истек' }; + } + + if (data.code !== code) { + return { success: false, error: 'Неверный код' }; + } + + return { + success: true, + token: data.token + }; + } + + // Метод для удаления кода после проверки + removeCode(email) { + verificationCodes.delete(email.toLowerCase()); } } -/** - * Отправка email - * @param {string} to - Адрес получателя - * @param {string} subject - Тема письма - * @param {string} text - Текст письма - * @returns {Promise} - Результат отправки - */ -async function sendEmail(to, subject, text) { - try { - const info = await transporter.sendMail({ - from: process.env.EMAIL_USER, - to, - subject, - text, - }); - - console.log('Email sent:', info.messageId); - return info; - } catch (error) { - console.error('Error sending email:', error); - throw error; - } -} - -module.exports = { - initEmailBot, - sendEmail, - checkEmails, -}; +// Экспортируем класс и хранилище кодов +module.exports = EmailBotService; +module.exports.verificationCodes = verificationCodes; diff --git a/backend/services/telegram-service.js b/backend/services/telegram-service.js deleted file mode 100644 index 385b821..0000000 --- a/backend/services/telegram-service.js +++ /dev/null @@ -1,262 +0,0 @@ -const TelegramBot = require('node-telegram-bot-api'); -const { pool } = require('../db'); -const { processMessage } = require('./ai-assistant'); - -// Инициализация бота -const token = process.env.TELEGRAM_BOT_TOKEN; -let bot = null; - -if (token) { - bot = new TelegramBot(token, { polling: true }); - console.log('Telegram bot initialized'); -} else { - console.warn('TELEGRAM_BOT_TOKEN not set, Telegram integration disabled'); -} - -/** - * Инициализация Telegram бота - */ -function initTelegramBot() { - if (!bot) return; - - // Обработка команды /start - bot.onText(/\/start/, async (msg) => { - const chatId = msg.chat.id; - const userId = msg.from.id; - const username = - msg.from.username || `${msg.from.first_name} ${msg.from.last_name || ''}`.trim(); - - try { - // Проверка существования пользователя - const user = await findOrCreateUser(userId, username, chatId); - - // Приветственное сообщение - bot.sendMessage(chatId, `Привет, ${username}! Я ИИ-ассистент. Чем могу помочь?`); - } catch (error) { - console.error('Error handling /start command:', error); - bot.sendMessage( - chatId, - 'Произошла ошибка при обработке команды. Пожалуйста, попробуйте позже.' - ); - } - }); - - // Обработка текстовых сообщений - bot.on('message', async (msg) => { - if (!msg.text || msg.text.startsWith('/')) return; - - const chatId = msg.chat.id; - const userId = msg.from.id; - const username = - msg.from.username || `${msg.from.first_name} ${msg.from.last_name || ''}`.trim(); - - try { - // Проверка существования пользователя - const user = await findOrCreateUser(userId, username, chatId); - - // Получение или создание диалога - const conversation = await getOrCreateConversation(user.id); - - // Сохранение сообщения пользователя - await saveMessage(conversation.id, 'user', user.id, msg.text, 'telegram'); - - // Обработка сообщения ИИ-ассистентом - const aiResponse = await processMessage(user.id, msg.text, user.language || 'ru'); - - // Сохранение ответа ИИ - await saveMessage(conversation.id, 'ai', null, aiResponse, 'telegram'); - - // Отправка ответа - bot.sendMessage(chatId, aiResponse); - } catch (error) { - console.error('Error processing message:', error); - bot.sendMessage( - chatId, - 'Произошла ошибка при обработке сообщения. Пожалуйста, попробуйте позже.' - ); - } - }); - - console.log('Telegram bot handlers registered'); -} - -/** - * Поиск или создание пользователя по Telegram ID - * @param {number} telegramId - Telegram ID пользователя - * @param {string} username - Имя пользователя - * @param {number} chatId - ID чата - * @returns {Promise} - Информация о пользователе - */ -async function findOrCreateUser(telegramId, username, chatId) { - try { - // Поиск пользователя по Telegram ID - const userIdResult = await pool.query( - `SELECT user_id FROM user_identities - WHERE identity_type = 'telegram' AND identity_value = $1`, - [telegramId.toString()] - ); - - if (userIdResult.rows.length > 0) { - // Пользователь найден - const userId = userIdResult.rows[0].user_id; - - // Получение информации о пользователе - const userResult = await pool.query('SELECT * FROM users WHERE id = $1', [userId]); - - return userResult.rows[0]; - } else { - // Создание нового пользователя - const userResult = await pool.query( - `INSERT INTO users ( - username, - role_id, - is_admin, - language, - address - ) VALUES ( - $1, - (SELECT id FROM roles WHERE name = 'user'), - FALSE, - 'ru', - '0x' || encode(gen_random_bytes(20), 'hex') - ) RETURNING *`, - [username] - ); - - const newUser = userResult.rows[0]; - - // Добавление идентификатора Telegram - await pool.query( - `INSERT INTO user_identities ( - user_id, - identity_type, - identity_value, - verified - ) VALUES ($1, 'telegram', $2, TRUE)`, - [newUser.id, telegramId.toString()] - ); - - // Сохранение метаданных Telegram - await pool.query( - `INSERT INTO user_preferences ( - user_id, - preference_key, - preference_value - ) VALUES ($1, 'telegram_chat_id', $2)`, - [newUser.id, chatId.toString()] - ); - - return newUser; - } - } catch (error) { - console.error('Error finding or creating user:', error); - throw error; - } -} - -/** - * Получение или создание диалога для пользователя - * @param {number} userId - ID пользователя - * @returns {Promise} - Информация о диалоге - */ -async function getOrCreateConversation(userId) { - try { - // Поиск активного диалога - const conversationResult = await pool.query( - `SELECT * FROM conversations - WHERE user_id = $1 - ORDER BY updated_at DESC - LIMIT 1`, - [userId] - ); - - if (conversationResult.rows.length > 0) { - // Обновление времени последней активности - await pool.query('UPDATE conversations SET updated_at = NOW() WHERE id = $1', [ - conversationResult.rows[0].id, - ]); - - return conversationResult.rows[0]; - } else { - // Создание нового диалога - const newConversationResult = await pool.query( - `INSERT INTO conversations (user_id, title) - VALUES ($1, $2) - RETURNING *`, - [userId, 'Диалог в Telegram'] - ); - - return newConversationResult.rows[0]; - } - } catch (error) { - console.error('Error getting or creating conversation:', error); - throw error; - } -} - -/** - * Сохранение сообщения - * @param {number} conversationId - ID диалога - * @param {string} senderType - Тип отправителя ('user', 'ai') - * @param {number|null} senderId - ID отправителя - * @param {string} content - Текст сообщения - * @param {string} channel - Канал ('telegram') - * @returns {Promise} - Информация о сообщении - */ -async function saveMessage(conversationId, senderType, senderId, content, channel) { - try { - const messageResult = await pool.query( - `INSERT INTO messages ( - conversation_id, - sender_type, - sender_id, - content, - channel - ) VALUES ($1, $2, $3, $4, $5) - RETURNING *`, - [conversationId, senderType, senderId, content, channel] - ); - - return messageResult.rows[0]; - } catch (error) { - console.error('Error saving message:', error); - throw error; - } -} - -/** - * Отправка сообщения пользователю через Telegram - * @param {number} userId - ID пользователя - * @param {string} message - Текст сообщения - * @returns {Promise} - Успешность отправки - */ -async function sendMessageToUser(userId, message) { - if (!bot) return false; - - try { - // Получение Telegram chat ID пользователя - const chatIdResult = await pool.query( - `SELECT preference_value FROM user_preferences - WHERE user_id = $1 AND preference_key = 'telegram_chat_id'`, - [userId] - ); - - if (chatIdResult.rows.length === 0) { - return false; - } - - const chatId = chatIdResult.rows[0].preference_value; - - // Отправка сообщения - await bot.sendMessage(chatId, message); - return true; - } catch (error) { - console.error('Error sending message to user:', error); - return false; - } -} - -module.exports = { - initTelegramBot, - sendMessageToUser, -}; diff --git a/backend/services/telegramBot.js b/backend/services/telegramBot.js new file mode 100644 index 0000000..bc02ae9 --- /dev/null +++ b/backend/services/telegramBot.js @@ -0,0 +1,273 @@ +const TelegramBot = require('node-telegram-bot-api'); +const logger = require('../utils/logger'); + +// Создаем бота +const token = process.env.TELEGRAM_BOT_TOKEN; +let bot = null; + +// Добавим хранилище для кодов подтверждения +const verificationCodes = new Map(); // Формат: { telegramId: { code: '123456', token: 'auth_token', expires: timestamp } } + +/** + * Инициализация Telegram бота + * @returns {Object|null} - Объект с методами для работы с ботом или null, если инициализация не удалась + */ +function initTelegramBot() { + if (!token) { + console.warn('TELEGRAM_BOT_TOKEN not set, Telegram integration disabled'); + return null; + } + + try { + // Создаем бота с опцией polling + bot = new TelegramBot(token, { polling: true }); + console.log('Telegram bot initialized'); + + // Регистрируем обработчики событий + registerHandlers(); + + return { + bot, + sendMessage: (chatId, text) => bot.sendMessage(chatId, text) + }; + } catch (error) { + console.error('Error initializing Telegram bot:', error); + return null; + } +} + +/** + * Регистрация обработчиков событий для бота + */ +function registerHandlers() { + // Обработчик /start + bot.onText(/\/start(.*)/, async (msg, match) => { + const chatId = msg.chat.id; + const param = match[1] ? match[1].trim() : ''; + + console.log(`Получена команда /start с параметром: "${param}" от пользователя ${chatId}`); + + if (param.startsWith('auth_')) { + // Это токен авторизации через deep link + const authToken = param.replace('auth_', ''); + console.log(`Обработка токена авторизации: ${authToken}`); + + try { + // Проверяем, существует ли токен + const { pool } = require('../db'); + const tokenResult = await pool.query( + 'SELECT user_id, expires_at FROM telegram_auth_tokens WHERE token = $1', + [authToken] + ); + + if (tokenResult.rows.length === 0 || new Date(tokenResult.rows[0].expires_at) < new Date()) { + bot.sendMessage(chatId, '❌ Недействительный или истекший токен авторизации.'); + return; + } + + // Генерируем код подтверждения + const verificationCode = Math.floor(100000 + Math.random() * 900000).toString(); // 6-значный код + + // Сохраняем в хранилище + verificationCodes.set(chatId.toString(), { + code: verificationCode, + token: authToken, + expires: Date.now() + 5 * 60 * 1000 // Срок действия 5 минут + }); + + // Отправляем код пользователю + bot.sendMessage(chatId, + '🔐 Для завершения связывания аккаунта, пожалуйста, введите этот код:\n\n' + + `${verificationCode}\n\n` + + 'Код действителен в течение 5 минут.', + { parse_mode: 'HTML' } + ); + } catch (error) { + console.error('Error processing auth token:', error); + bot.sendMessage(chatId, '❌ Произошла ошибка при обработке запроса авторизации.'); + } + } else { + // Получаем последний активный токен для этого чата, если есть + const { pool } = require('../db'); + try { + const lastTokenResult = await pool.query(` + SELECT token FROM telegram_auth_tokens + WHERE expires_at > NOW() AND used = FALSE + ORDER BY created_at DESC LIMIT 1 + `); + + if (lastTokenResult.rows.length > 0) { + const authToken = lastTokenResult.rows[0].token; + + // Генерируем код подтверждения + const verificationCode = Math.floor(100000 + Math.random() * 900000).toString(); // 6-значный код + + // Сохраняем в хранилище + verificationCodes.set(chatId.toString(), { + code: verificationCode, + token: authToken, + expires: Date.now() + 5 * 60 * 1000 // Срок действия 5 минут + }); + + // Отправляем код пользователю + bot.sendMessage(chatId, + '🔐 Для завершения связывания аккаунта, пожалуйста, введите этот код:\n\n' + + `${verificationCode}\n\n` + + 'Код действителен в течение 5 минут.', + { parse_mode: 'HTML' } + ); + return; + } + } catch (error) { + console.error('Error checking last token:', error); + } + + // Если нет активного токена, отправляем стандартное сообщение + bot.sendMessage(chatId, + 'Привет! Я бот для аутентификации в DApp for Business.\n\n' + + 'Для связи с вашим аккаунтом используйте кнопку на сайте.' + ); + } + }); + + // Обработчик для проверки кода подтверждения + bot.on('message', async (msg) => { + const chatId = msg.chat.id; + const text = msg.text; + + // Игнорируем команды + if (text && text.startsWith('/')) return; + + // Проверяем, есть ли ожидающая верификация для этого чата + const verificationData = verificationCodes.get(chatId.toString()); + + if (verificationData && text === verificationData.code) { + // Код верный, проверяем срок действия + if (Date.now() > verificationData.expires) { + bot.sendMessage(chatId, '❌ Срок действия кода истек. Пожалуйста, начните процесс заново.'); + verificationCodes.delete(chatId.toString()); + return; + } + + // Код верный и актуальный, завершаем аутентификацию + try { + const result = await linkTelegramAccount(chatId.toString(), verificationData.token); + + if (result.success) { + bot.sendMessage(chatId, + '✅ Аутентификация успешна!\n\n' + + 'Ваш Telegram аккаунт связан с DApp for Business.\n' + + 'Теперь вы можете использовать бота для общения с системой.' + ); + } else { + bot.sendMessage(chatId, + '❌ Ошибка аутентификации: ' + (result.error || 'неизвестная ошибка') + ); + } + + // Удаляем данные верификации + verificationCodes.delete(chatId.toString()); + } catch (error) { + console.error('Error completing authentication:', error); + bot.sendMessage(chatId, '❌ Произошла ошибка при завершении аутентификации.'); + } + } else if (verificationData) { + // Есть ожидающая верификация, но код неверный + bot.sendMessage(chatId, '❌ Неверный код. Пожалуйста, попробуйте еще раз.'); + } else { + // Нет ожидающей верификации + bot.sendMessage(chatId, 'Я могу помочь с аутентификацией. Используйте кнопку на сайте для начала процесса.'); + } + }); + + // Добавить обработку прямых команд аутентификации + bot.onText(/\/auth (.+)/, async (msg, match) => { + const chatId = msg.chat.id; + const authToken = match[1].trim(); + + console.log(`Получена прямая команда авторизации с токеном: ${authToken}`); + + try { + // Связываем Telegram ID с аккаунтом по токену + const result = await linkTelegramAccount(chatId.toString(), authToken); + console.log(`Результат связывания: ${JSON.stringify(result)}`); + + if (result.success) { + bot.sendMessage(chatId, + '✅ Аутентификация успешна!\n\n' + + 'Ваш Telegram аккаунт связан с DApp for Business.\n' + + 'Теперь вы можете использовать бота для общения с системой.' + ); + } else { + bot.sendMessage(chatId, + '❌ Ошибка аутентификации: ' + (result.error || 'неизвестная ошибка') + ); + } + } catch (error) { + console.error('Error linking telegram account:', error); + bot.sendMessage(chatId, '❌ Произошла ошибка при связывании аккаунта.'); + } + }); + + // Обработка ошибок + bot.on('polling_error', (error) => { + logger.error(`[polling_error] ${JSON.stringify(error)}`); + }); + + console.log('Telegram bot handlers registered'); +} + +/** + * Связывание Telegram ID с аккаунтом пользователя + * @param {string} telegramId - ID пользователя в Telegram + * @param {string} authToken - Токен авторизации + * @returns {Promise} - Результат операции + */ +async function linkTelegramAccount(telegramId, authToken) { + try { + console.log(`Попытка связать Telegram ID ${telegramId} с токеном ${authToken}`); + + // Здесь должен быть код для связывания через API или напрямую с БД + const { pool } = require('../db'); + + // Проверяем токен авторизации + const tokenResult = await pool.query( + 'SELECT user_id, expires_at FROM telegram_auth_tokens WHERE token = $1', + [authToken] + ); + + console.log(`Результат запроса токена: ${JSON.stringify(tokenResult.rows)}`); + + if (tokenResult.rows.length === 0 || new Date(tokenResult.rows[0].expires_at) < new Date()) { + console.log('Токен не найден или истек'); + return { success: false, error: 'Недействительный или истекший токен' }; + } + + const userId = tokenResult.rows[0].user_id; + console.log(`Найден пользователь с ID: ${userId}`); + + // Добавляем идентификатор Telegram для пользователя + await pool.query( + 'INSERT INTO user_identities (user_id, identity_type, identity_value, verified, created_at) ' + + 'VALUES ($1, $2, $3, true, NOW()) ' + + 'ON CONFLICT (identity_type, identity_value) ' + + 'DO UPDATE SET user_id = $1, verified = true', + [userId, 'telegram', telegramId] + ); + + // Отмечаем токен как использованный + await pool.query( + 'UPDATE telegram_auth_tokens SET used = true WHERE token = $1', + [authToken] + ); + + return { success: true }; + } catch (error) { + console.error('Error in linkTelegramAccount:', error); + return { success: false, error: 'Внутренняя ошибка сервера' }; + } +} + +module.exports = { + initTelegramBot +}; \ No newline at end of file diff --git a/frontend/src/components/TelegramConnect.vue b/frontend/src/components/TelegramConnect.vue new file mode 100644 index 0000000..5150f89 --- /dev/null +++ b/frontend/src/components/TelegramConnect.vue @@ -0,0 +1,195 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/stores/auth.js b/frontend/src/stores/auth.js index b1aebea..6ad609a 100644 --- a/frontend/src/stores/auth.js +++ b/frontend/src/stores/auth.js @@ -260,6 +260,167 @@ export const useAuthStore = defineStore('auth', { } return false; + }, + + async requestEmailVerification(email) { + this.loading = true; + this.error = null; + + try { + const response = await axios.post('/api/auth/email/request', { email }); + + console.log('Email verification code response:', response.data); + + return { + success: true, + message: response.data.message, + verificationCode: response.data.verificationCode // Для разработки + }; + } catch (error) { + this.error = error.response?.data?.error || 'Ошибка запроса кода'; + return { success: false, error: this.error }; + } finally { + this.loading = false; + } + }, + + async verifyEmail(code) { + this.loading = true; + this.error = null; + + try { + const response = await axios.post('/api/auth/email/verify', { code }); + + if (response.data.success) { + this.isAuthenticated = true; + this.user = { + id: response.data.userId, + email: response.data.email + }; + + if (response.data.walletAddress) { + this.user.address = response.data.walletAddress; + this.address = response.data.walletAddress; + } + + this.isAdmin = response.data.isAdmin; + this.authType = 'email'; + } + + return response.data; + } catch (error) { + this.error = error.response?.data?.error || 'Ошибка верификации'; + return { success: false, error: this.error }; + } finally { + this.loading = false; + } + }, + + async requestTelegramCode() { + this.loading = true; + this.error = null; + + try { + const response = await axios.get('/api/auth/telegram/code'); + return response.data; + } catch (error) { + this.error = error.response?.data?.error || 'Ошибка запроса кода'; + return { success: false, error: this.error }; + } finally { + this.loading = false; + } + }, + + async verifyTelegram(telegramId, code) { + this.loading = true; + this.error = null; + + try { + const response = await axios.post('/api/auth/telegram/verify', { telegramId, code }); + + if (response.data.success) { + this.isAuthenticated = true; + this.user = { + id: response.data.userId, + telegramId: response.data.telegramId + }; + + if (response.data.walletAddress) { + this.user.address = response.data.walletAddress; + this.address = response.data.walletAddress; + } + + this.isAdmin = response.data.isAdmin; + this.authType = 'telegram'; + } + + return response.data; + } catch (error) { + this.error = error.response?.data?.error || 'Ошибка верификации'; + return { success: false, error: this.error }; + } finally { + this.loading = false; + } + }, + + async linkIdentity(type, value) { + this.loading = true; + this.error = null; + + try { + const response = await axios.post('/api/auth/link-identity', { type, value }); + + if (response.data.success) { + if (type === 'wallet') { + this.user.address = value; + this.address = value; + } else if (type === 'email') { + this.user.email = value; + } else if (type === 'telegram') { + this.user.telegramId = value; + } + + this.isAdmin = response.data.isAdmin; + } + + return response.data; + } catch (error) { + this.error = error.response?.data?.error || 'Ошибка связывания аккаунта'; + return { success: false, error: this.error }; + } finally { + this.loading = false; + } + }, + + async createTelegramAuthToken() { + this.loading = true; + this.error = null; + + try { + const response = await axios.post('/api/auth/telegram/auth-token'); + return response.data; + } catch (error) { + this.error = error.response?.data?.error || 'Ошибка создания токена'; + return { success: false, error: this.error }; + } finally { + this.loading = false; + } + }, + + async checkTelegramAuthStatus(token) { + try { + const response = await axios.get(`/api/auth/telegram/auth-status/${token}`); + + if (response.data.success && response.data.authenticated) { + // Обновляем состояние аутентификации + await this.checkAuth(); + } + + return response.data; + } catch (error) { + console.error('Error checking Telegram auth status:', error); + return { success: false, error: 'Ошибка проверки статуса' }; + } } } }); diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index acaeb1c..6db0f7e 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -30,22 +30,42 @@
- +
-
@@ -73,6 +93,7 @@ import { ref, computed, onMounted, watch, nextTick } from 'vue'; import { useAuthStore } from '../stores/auth'; import WalletConnection from '../components/WalletConnection.vue'; +import TelegramConnect from '../components/TelegramConnect.vue'; import axios from '../api/axios'; console.log('HomeView.vue: Version with chat loaded'); @@ -89,6 +110,11 @@ const hasShownAuthMessage = ref(false); const guestMessages = ref([]); const hasShownAuthOptions = ref(false); +// Email аутентификация +const emailVerificationCode = ref(''); +const showEmailVerification = ref(false); +const emailErrorMessage = ref(''); + // Простая функция для выхода const logout = async () => { await auth.logout(); @@ -320,95 +346,66 @@ async function connectTelegram() { } } -// Функция для подключения через Email -async function connectEmail() { - if (!isValidEmail.value) return; +// Запрос кода подтверждения по email +async function requestEmailCode() { + emailErrorMessage.value = ''; try { - messages.value.push({ - sender: 'ai', - text: `Отправляем код подтверждения на ${email.value}...`, - timestamp: new Date(), - }); + const response = await auth.requestEmailVerification(email.value); - // Отправляем запрос на отправку кода подтверждения - const response = await axios.post('/api/auth/email', { - email: email.value - }, { - withCredentials: true - }); - - if (response.data.error) { - messages.value.push({ - sender: 'ai', - text: `Ошибка: ${response.data.error}`, - timestamp: new Date(), - }); - return; - } - - messages.value.push({ - sender: 'ai', - text: `На ваш email ${email.value} отправлено письмо с кодом подтверждения. Пожалуйста, введите код:`, - timestamp: new Date(), - }); - - // Добавляем поле для ввода кода - const verificationCode = prompt('Введите код подтверждения:'); - - if (verificationCode) { - try { - // Отправляем запрос на проверку кода - const verifyResponse = await axios.post('/api/auth/email/verify', { - email: email.value, - code: verificationCode - }, { - withCredentials: true - }); - - if (verifyResponse.data.error) { - messages.value.push({ - sender: 'ai', - text: `Ошибка: ${verifyResponse.data.error}`, - timestamp: new Date(), - }); - return; - } - - messages.value.push({ - sender: 'ai', - text: 'Email успешно подтвержден! Теперь вы можете использовать все функции чата.', - timestamp: new Date(), - }); - - // Обновляем состояние аутентификации - auth.isAuthenticated = true; - auth.user = { email: email.value }; - auth.authType = 'email'; - - // Сбрасываем флаг показа сообщения с опциями авторизации - hasShownAuthMessage.value = false; - } catch (error) { - console.error('Error verifying email code:', error); - - messages.value.push({ - sender: 'ai', - text: 'Произошла ошибка при проверке кода. Пожалуйста, попробуйте позже.', - timestamp: new Date(), - }); + if (response.success) { + showEmailVerification.value = true; + // Временно для тестирования + if (response.verificationCode) { + emailErrorMessage.value = `Код для тестирования: ${response.verificationCode}`; } + } else { + emailErrorMessage.value = response.error || 'Ошибка запроса кода подтверждения'; } } catch (error) { - console.error('Error connecting with email:', error); - - messages.value.push({ - sender: 'ai', - text: 'Извините, произошла ошибка при подключении Email. Пожалуйста, попробуйте позже.', - timestamp: new Date(), - }); + console.error('Error requesting email verification:', error); + emailErrorMessage.value = 'Ошибка запроса кода подтверждения'; } } +// Подтверждение кода подтверждения по email +async function verifyEmailCode() { + emailErrorMessage.value = ''; + + try { + const response = await auth.verifyEmail(emailVerificationCode.value); + + if (response.success) { + // Успешная верификация + showEmailVerification.value = false; + emailVerificationCode.value = ''; + + // Связываем гостевые сообщения с аутентифицированным пользователем + try { + await axios.post('/api/chat/link-guest-messages'); + console.log('Guest messages linked to authenticated user'); + } catch (linkError) { + console.error('Error linking guest messages:', linkError); + } + + // Загружаем историю сообщений + await loadChatHistory(); + } else { + emailErrorMessage.value = response.error || 'Неверный код подтверждения'; + } + } catch (error) { + console.error('Error verifying email code:', error); + emailErrorMessage.value = 'Ошибка верификации'; + } +} + +// Отмена верификации email +function cancelEmailVerification() { + showEmailVerification.value = false; + emailVerificationCode.value = ''; + emailErrorMessage.value = ''; +} + // Добавьте эту функцию в