diff --git a/backend/db/migrations/011_verification_codes.sql b/backend/db/migrations/011_verification_codes.sql new file mode 100644 index 0000000..2e9d6ff --- /dev/null +++ b/backend/db/migrations/011_verification_codes.sql @@ -0,0 +1,19 @@ +-- Создаем таблицу для кодов верификации +CREATE TABLE IF NOT EXISTS verification_codes ( + id SERIAL PRIMARY KEY, + code VARCHAR(6) NOT NULL, + provider VARCHAR(50) NOT NULL, -- 'telegram', 'email' + provider_id VARCHAR(255) NOT NULL, -- telegram_id или email + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + expires_at TIMESTAMP NOT NULL, + used BOOLEAN DEFAULT FALSE +); + +-- Индексы для оптимизации +CREATE INDEX IF NOT EXISTS idx_verification_codes_code ON verification_codes(code); +CREATE INDEX IF NOT EXISTS idx_verification_codes_provider ON verification_codes(provider); +CREATE INDEX IF NOT EXISTS idx_verification_codes_expires ON verification_codes(expires_at); + +-- Удаляем старую таблицу email_auth_tokens +DROP TABLE IF EXISTS email_auth_tokens; \ No newline at end of file diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 491f2e6..0420780 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -9,18 +9,20 @@ const { checkRole, requireAuth } = require('../middleware/auth'); const { pool } = require('../db'); const authService = require('../services/auth-service'); const { SiweMessage } = require('siwe'); -const { sendEmail } = require('../services/emailBot'); +const { EmailBotService } = 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 emailAuth = require('../services/emailAuth'); +const verificationService = require('../services/verification-service'); // Создайте лимитер для попыток аутентификации const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 минут - max: 20, // Увеличьте лимит с 5 до 20 + max: 20, standardHeaders: true, legacyHeaders: false, message: { error: 'Слишком много попыток аутентификации. Попробуйте позже.' }, @@ -99,12 +101,51 @@ router.post('/verify', async (req, res) => { `, [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 if (req.session.guestId) { + // Проверяем, есть ли пользователь с текущим guestId + const guestUserResult = await db.query(` + SELECT u.* FROM users u + JOIN user_identities ui ON u.id = ui.user_id + WHERE ui.provider = 'guest' AND ui.provider_id = $1 + `, [req.session.guestId]); + + if (guestUserResult.rows.length > 0) { + // Используем существующего пользователя с guestId + userId = guestUserResult.rows[0].id; + isAdmin = guestUserResult.rows[0].role === 'admin'; + + // Добавляем идентификатор кошелька к существующему пользователю + await db.query( + 'INSERT INTO user_identities (user_id, provider, provider_id) VALUES ($1, $2, $3)', + [userId, 'wallet', address.toLowerCase()] + ); + } 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()] + ); + + // Добавляем идентификатор гостя + await db.query( + 'INSERT INTO user_identities (user_id, provider, provider_id) VALUES ($1, $2, $3)', + [userId, 'guest', req.session.guestId] + ); + } } else { - // Создаем нового пользователя + // Создаем нового пользователя без гостевого ID const newUserResult = await db.query( 'INSERT INTO users (role) VALUES ($1) RETURNING id', ['user'] @@ -215,7 +256,7 @@ router.post('/telegram', async (req, res) => { }); // Маршрут для запроса кода подтверждения по email -router.post('/email/request', async (req, res) => { +router.post('/email/request', authLimiter, async (req, res) => { try { const { email } = req.body; @@ -223,62 +264,37 @@ router.post('/email/request', async (req, res) => { 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'); + // Отправляем email с кодом подтверждения const emailBot = new EmailBotService(process.env.EMAIL_USER, process.env.EMAIL_PASSWORD); - - // Используем новый метод sendVerificationCode вместо sendEmail - const result = await emailBot.sendVerificationCode(email, token); + const result = await emailBot.sendVerificationCode(email, userId); 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 - только для отладки + res.json({ + success: true, + message: 'Код подтверждения отправлен на email' }); } else { - return res.status(500).json({ error: 'Error sending email' }); + res.status(500).json({ + success: false, + error: result.error || 'Ошибка отправки кода' + }); } } catch (error) { - console.error('Error requesting email verification:', error); - res.status(500).json({ error: 'Internal server error' }); + logger.error('Error requesting email code:', error); + res.status(500).json({ error: 'Ошибка сервера' }); } }); @@ -286,80 +302,55 @@ router.post('/email/request', async (req, res) => { 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) { + if (!code) { return res.status(400).json({ success: false, - error: 'Неверный или истекший код подтверждения' + error: 'Код подтверждения обязателен' }); } - const email = verificationData.email; + // Проверяем код через сервис верификации + const result = await verificationService.verifyCode(code, 'email', req.session.pendingEmail); - // Ищем или создаем пользователя с этим email - const result = await db.query( - 'SELECT * FROM find_or_create_user_by_identity($1, $2)', - ['email', email] + if (!result.success) { + return res.status(400).json({ + success: false, + error: result.error || 'Неверный код подтверждения' + }); + } + + const userId = result.userId; + const email = req.session.pendingEmail; + + // Добавляем 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()] ); - 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.email = email.toLowerCase(); 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.pendingEmail; + delete req.session.tempUserId; - // Очищаем данные верификации - delete req.session.emailVerificationData; - - res.json({ + return res.json({ success: true, - authenticated: true, userId, - email, - isAdmin, - hasWallet, - walletAddress, - isNew + email: email.toLowerCase(), + message: 'Аутентификация успешна' }); + } catch (error) { - logger.error(`Error in email verification: ${error.message}`); + logger.error('Error in email verification:', error); res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); } }); @@ -508,94 +499,33 @@ router.post('/link-identity', async (req, res) => { }); // Проверка статуса аутентификации -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 { - // Если нет, ищем в базе данных - 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); - } - } +router.get('/check', (req, res) => { + try { + console.log('Сессия при проверке:', req.session); + + const authenticated = req.session.authenticated || req.session.isAuthenticated || false; + const userId = req.session.userId; + const authType = req.session.authType; + const address = req.session.address; + const telegramId = req.session.telegramId; + const email = req.session.email; + + // Проверяем, является ли пользователь администратором + const isAdmin = req.session.userRole === 'admin'; + + res.json({ + authenticated, + userId, + isAdmin, + authType, + address, + telegramId, + email + }); + } catch (error) { + console.error('Error checking auth:', error); + 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); }); // Выход из системы @@ -624,22 +554,91 @@ router.get('/telegram', (req, res) => { res.json({ authUrl }); }); -// Маршрут для получения кода подтверждения Telegram -router.get('/telegram/code', async (req, res) => { +// Маршрут для верификации Telegram +router.post('/telegram/verify', async (req, res) => { try { - // Генерируем код подтверждения (6 символов) - const verificationCode = Math.random().toString(36).substring(2, 8).toUpperCase(); + const { code } = req.body; - // Сохраняем код в сессии - req.session.telegramVerificationData = { - code: verificationCode, - expires: Date.now() + 10 * 60 * 1000 // 10 минут - }; + // Проверяем код через сервис верификации + const result = await verificationService.verifyCode(code, 'telegram', req.session.guestId || 'temp'); + + if (!result.success) { + return res.status(400).json({ + success: false, + error: result.error || 'Неверный код подтверждения' + }); + } + + const userId = result.userId; + const telegramId = result.providerId; + + // Добавляем Telegram идентификатор в базу данных + 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, 'telegram', telegramId] + ); + + // Устанавливаем аутентификацию пользователя + req.session.authenticated = true; + req.session.userId = userId; + req.session.telegramId = telegramId; + req.session.authType = 'telegram'; + + // Если был временный ID, удаляем его + if (req.session.tempUserId) { + delete req.session.tempUserId; + } + + // Если есть подключенный кошелек, проверяем баланс токенов + if (req.session.address) { + const isAdmin = await checkTokenBalance(req.session.address); + req.session.isAdmin = isAdmin; + } + + return res.json({ + success: true, + userId, + telegramId, + isAdmin: req.session.isAdmin || false, + authenticated: true + }); + + } catch (error) { + logger.error('Error in telegram verification:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// Маршрут для получения кода подтверждения Telegram +router.get('/telegram/code', authLimiter, async (req, res) => { + try { + // Создаем или получаем ID пользователя + let userId; + + if (req.session.authenticated && req.session.userId) { + userId = req.session.userId; + } else { + const userResult = await db.query( + 'INSERT INTO users (created_at) VALUES (NOW()) RETURNING id' + ); + userId = userResult.rows[0].id; + req.session.tempUserId = userId; + } + + // Создаем код через сервис верификации + const code = await verificationService.createVerificationCode( + 'telegram', + req.session.guestId || 'temp', + userId + ); res.json({ success: true, message: 'Отправьте этот код боту @' + process.env.TELEGRAM_BOT_USERNAME, - code: verificationCode, + code, botUsername: process.env.TELEGRAM_BOT_USERNAME || 'YourDAppBot' }); } catch (error) { @@ -695,92 +694,6 @@ 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 = getBot(); - const result = await telegramBot.verifyCode(code); - - console.log('Telegram verification result:', result); - - if (result.success) { - // Проверяем, что у нас есть telegramId - if (!result.telegramId) { - return res.status(400).json({ error: 'Invalid Telegram ID' }); - } - - // Создаем или находим пользователя - const userResult = await pool.query( - `INSERT INTO users (created_at) - VALUES (NOW()) - RETURNING id`, - ); - - const userId = userResult.rows[0].id; - - // Добавляем Telegram идентификатор - await pool.query( - `INSERT INTO user_identities - (user_id, identity_type, identity_value, verified, created_at) - VALUES ($1, 'telegram', $2, true, NOW()) - ON CONFLICT (identity_type, identity_value) - DO UPDATE SET verified = true - RETURNING user_id`, - [userId, result.telegramId] - ); - - // Обновляем сессию - req.session.userId = userId; - req.session.authenticated = true; - req.session.authType = 'telegram'; - req.session.telegramId = result.telegramId; - - // Если есть подключенный кошелек, проверяем баланс токенов - if (req.session.address) { - const isAdmin = await checkTokenBalance(req.session.address); - req.session.isAdmin = isAdmin; - } - - // Сохраняем сессию - await new Promise((resolve, reject) => { - req.session.save(err => { - if (err) reject(err); - else resolve(); - }); - }); - - 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, - telegramId: result.telegramId, - isAdmin: req.session.isAdmin || false, - authenticated: true - }); - } - - res.status(400).json({ error: result.error || 'Invalid verification code' }); - } catch (error) { - console.error('Error in telegram verification:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - // Маршрут для связывания разных идентификаторов router.post('/link-identity', requireAuth, async (req, res) => { try { @@ -1120,48 +1033,29 @@ router.get('/email/auth-status/:token', async (req, res) => { } }); -// Маршрут для проверки кода, введенного пользователем -router.post('/email/verify-code', async (req, res) => { +// Маршрут для проверки кода email +router.post('/email/verify-code', authLimiter, async (req, res) => { try { const { email, code } = req.body; if (!email || !code) { - return res.status(400).json({ success: false, error: 'Email и код обязательны' }); + 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 result = await verificationService.verifyCode(code, 'email', email.toLowerCase()); - // Проверяем код из хранилища - const verificationData = EmailBotService.verificationCodes.get(email.toLowerCase()); - - if (!verificationData) { - return res.status(400).json({ success: false, error: 'Код подтверждения не найден' }); + if (!result.success) { + return res.status(400).json({ + success: false, + error: result.error || 'Неверный код подтверждения' + }); } - if (Date.now() > verificationData.expires) { - EmailBotService.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; + const userId = result.userId; // Добавляем email в базу данных await db.query( @@ -1172,20 +1066,16 @@ router.post('/email/verify-code', async (req, res) => { [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'; - // Удаляем код из хранилища - EmailBotService.verificationCodes.delete(email.toLowerCase()); + // Если был временный ID, удаляем его + if (req.session.tempUserId) { + delete req.session.tempUserId; + } return res.json({ success: true, @@ -1195,8 +1085,11 @@ router.post('/email/verify-code', async (req, res) => { }); } catch (error) { - console.error('Error verifying email code:', error); - return res.status(500).json({ success: false, error: 'Ошибка сервера' }); + logger.error('Error verifying email code:', error); + return res.status(500).json({ + success: false, + error: 'Ошибка сервера' + }); } }); @@ -1220,44 +1113,42 @@ 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); + + if (!verificationCode || !botLink) { + throw new Error('Failed to generate verification code'); } - const { verificationCode, botLink } = await initTelegramAuth(req.session); - res.json({ verificationCode, botLink }); + res.json({ + success: true, + verificationCode, + botLink + }); } catch (error) { - console.error('Error initializing Telegram auth:', error); - res.status(500).json({ error: 'Failed to initialize Telegram auth' }); + logger.error('Error initializing Telegram auth:', error); + + if (error.message === 'Telegram уже привязан к этому аккаунту') { + return res.status(400).json({ + success: false, + error: error.message + }); + } + + res.status(500).json({ + success: false, + 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 }); + const { verificationCode } = await emailAuth.initEmailAuth(req.session); + res.json({ success: true, verificationCode }); } catch (error) { console.error('Error initializing email auth:', error); - res.status(500).json({ error: 'Failed to send verification code' }); + res.status(500).json({ error: 'Internal server error' }); } }); @@ -1269,7 +1160,7 @@ router.post('/email/verify', requireAuth, async (req, res) => { return res.status(400).json({ error: 'Verification code is required' }); } - const result = await verifyEmailCode(code, req.session.userId); + const result = await emailAuth.checkEmailVerification(code); res.json(result); } catch (error) { console.error('Error verifying email code:', error); @@ -1277,4 +1168,70 @@ router.post('/email/verify', requireAuth, async (req, res) => { } }); +// Проверка кода верификации email +router.get('/check-email-verification', async (req, res) => { + try { + const { code } = req.query; + + if (!code) { + return res.status(400).json({ + verified: false, + message: "Код верификации не указан" + }); + } + + const result = await emailAuth.checkEmailVerification(code, req.session); + + if (result.verified) { + // Обновляем сессию + req.session.userId = result.userId; + req.session.authenticated = true; + req.session.authType = 'email'; + req.session.email = result.email; + + // Проверяем роль пользователя + const userRole = await authService.checkUserRole(result.userId); + req.session.userRole = userRole; + + // Сохраняем сессию + await new Promise((resolve, reject) => { + req.session.save(err => { + if (err) reject(err); + else resolve(); + }); + }); + } + + res.json(result); + } catch (error) { + logger.error('Error checking email verification:', error); + res.status(500).json({ + verified: false, + message: "Ошибка при проверке кода" + }); + } +}); + +// Маршрут для имитации отправки email +router.post('/email/send', async (req, res) => { + try { + const { code } = req.body; + + if (!code) { + return res.status(400).json({ error: 'Code is required' }); + } + + const result = await emailAuth.markEmailAsSent(code); + + if (result.success) { + return res.json({ success: true }); + } else { + return res.status(400).json({ error: result.message }); + } + } catch (error) { + console.error('Error marking email as sent:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + module.exports = router; \ No newline at end of file diff --git a/backend/services/auth-service.js b/backend/services/auth-service.js index a40a4ac..8f900bc 100644 --- a/backend/services/auth-service.js +++ b/backend/services/auth-service.js @@ -3,6 +3,7 @@ const logger = require('../utils/logger'); const { ethers } = require('ethers'); const crypto = require('crypto'); const { processMessage } = require('./ai-assistant'); // Используем AI Assistant +const verificationService = require('./verification-service'); // Используем сервис верификации const ADMIN_CONTRACTS = [ { address: "0xd95a45fc46a7300e6022885afec3d618d7d3f27c", network: "eth" }, @@ -219,6 +220,40 @@ class AuthService { return 'user'; } } + + // Проверка верификации Email + async checkEmailVerification(code) { + try { + // Проверяем код через сервис верификации + const result = await verificationService.verifyCode(code, 'email', null); + + if (!result.success) { + return { verified: false }; + } + + const userId = result.userId; + const email = result.providerId; + + // Проверяем, существует ли пользователь с таким email + const userResult = await db.query( + 'SELECT * FROM users WHERE id = $1', + [userId] + ); + + if (userResult.rows.length === 0) { + return { verified: false }; + } + + return { + verified: true, + userId, + email + }; + } catch (error) { + logger.error('Error checking email verification:', error); + return { verified: false }; + } + } } // Создаем и экспортируем единственный экземпляр diff --git a/backend/services/emailAuth.js b/backend/services/emailAuth.js new file mode 100644 index 0000000..f11dd27 --- /dev/null +++ b/backend/services/emailAuth.js @@ -0,0 +1,112 @@ +const logger = require('../utils/logger'); +const db = require('../db'); +const authService = require('./auth-service'); +const verificationService = require('./verification-service'); + +// Инициализация процесса аутентификации по email +async function initEmailAuth(session) { + try { + // Создаем или получаем ID пользователя + let userId; + + if (session.authenticated && session.userId) { + userId = session.userId; + } else { + const userResult = await db.query( + 'INSERT INTO users (created_at) VALUES (NOW()) RETURNING id' + ); + userId = userResult.rows[0].id; + session.tempUserId = userId; + } + + // Создаем код через сервис верификации + const code = await verificationService.createVerificationCode( + 'email', + session.guestId || 'temp', + userId + ); + + logger.info(`Generated verification code: ${code} for Email auth`); + return { verificationCode: code }; + } catch (error) { + logger.error('Error initializing email auth:', error); + throw error; + } +} + +// Проверка кода верификации +async function checkEmailVerification(code, session) { + try { + if (!session?.guestId) { + return { verified: false, message: "Сессия не найдена" }; + } + + // Проверяем код через сервис верификации + const result = await verificationService.verifyCode(code, 'email', session.guestId); + + if (!result.success) { + return { verified: false, message: result.error || "Неверный код верификации" }; + } + + const userId = result.userId; + + // Проверяем, существует ли пользователь + const userResult = await db.query( + 'SELECT * FROM users WHERE id = $1', + [userId] + ); + + if (userResult.rows.length === 0) { + return { verified: false, message: "Пользователь не найден" }; + } + + // Проверяем, есть ли у пользователя связанный email + const emailIdentity = await db.query( + `SELECT * FROM user_identities + WHERE user_id = $1 AND provider = 'email'`, + [userId] + ); + + if (emailIdentity.rows.length === 0) { + // Связываем Email с пользователем + 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, 'email', session.guestId] + ); + } + + // Связываем гостевой ID с пользователем, если его еще нет + const guestIdentity = await db.query( + `SELECT * FROM user_identities + WHERE user_id = $1 AND provider = 'guest' AND provider_id = $2`, + [userId, session.guestId] + ); + + if (guestIdentity.rows.length === 0) { + 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, 'guest', session.guestId] + ); + } + + return { + verified: true, + userId, + email: session.guestId + }; + } catch (error) { + logger.error('Error in Email verification:', error); + return { verified: false, message: "Ошибка при проверке кода" }; + } +} + +module.exports = { + initEmailAuth, + checkEmailVerification +}; \ No newline at end of file diff --git a/backend/services/emailBot.js b/backend/services/emailBot.js index e999332..a23b270 100644 --- a/backend/services/emailBot.js +++ b/backend/services/emailBot.js @@ -5,22 +5,19 @@ 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 } } +const verificationService = require('./verification-service'); // Конфигурация для отправки писем const transporter = nodemailer.createTransport({ host: process.env.EMAIL_SMTP_HOST, port: process.env.EMAIL_SMTP_PORT, - secure: process.env.EMAIL_SMTP_PORT === '465', // true для 465, false для других портов + secure: process.env.EMAIL_SMTP_PORT === '465', auth: { user: process.env.EMAIL_USER, pass: process.env.EMAIL_PASSWORD, }, tls: { - rejectUnauthorized: false // Отключение проверки сертификата + rejectUnauthorized: false } }); @@ -38,63 +35,39 @@ class EmailBotService { constructor(user, password) { this.user = user; this.password = password; - this.transporter = null; - this.imap = null; + this.transporter = transporter; + this.imap = new Imap(imapConfig); this.initialize(); this.listenForReplies(); } 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 для чтения входящих писем - 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) { + async sendVerificationCode(toEmail, userId) { 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 code = await verificationService.createVerificationCode( + 'email', + toEmail.toLowerCase(), + userId + ); // Отправляем письмо с кодом const mailOptions = { from: this.user, to: toEmail, subject: 'Код подтверждения для DApp for Business', - text: `Ваш код подтверждения: ${verificationCode}\n\nДля завершения аутентификации, пожалуйста, ответьте на это письмо, указав только полученный код.\n\nКод действителен в течение 15 минут.`, + text: `Ваш код подтверждения: ${code}\n\nДля завершения аутентификации, пожалуйста, ответьте на это письмо, указав только полученный код.\n\nКод действителен в течение 15 минут.`, html: `

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

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

- ${verificationCode} + ${code}

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

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

@@ -107,7 +80,7 @@ class EmailBotService { const info = await this.transporter.sendMail(mailOptions); logger.info(`Email sent: ${info.messageId}`); - return { success: true, code: verificationCode }; // Код для отладки + return { success: true, code }; } catch (error) { logger.error(`Error sending email: ${error}`); return { success: false, error: error.message }; @@ -225,67 +198,50 @@ class EmailBotService { const code = codeMatch[0]; - // Проверяем, есть ли код для этого email - const verificationData = verificationCodes.get(fromEmail); + // Проверяем код через сервис верификации + const result = await verificationService.verifyCode(code, 'email', 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); - } + if (!result.success) { + // Отправляем сообщение об ошибке + await this.transporter.sendMail({ + from: this.user, + to: fromEmail, + subject: 'Ошибка верификации', + text: 'Неверный или истекший код подтверждения. Пожалуйста, запросите новый код.' + }); + return; } + + // Код верный и актуальный + const userId = result.userId; + + // Добавляем идентификатор 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 this.transporter.sendMail({ + from: this.user, + to: fromEmail, + subject: 'Аутентификация успешна', + text: 'Ваш email успешно связан с аккаунтом DApp for Business.' + }); + } catch (error) { logger.error(`Error processing email: ${error}`); } } + // Метод для проверки кода без IMAP + async verifyCode(email, code) { + return await verificationService.verifyCode(code, 'email', email.toLowerCase()); + } + // Оставляем существующие методы async sendEmail(to, subject, text) { try { @@ -304,110 +260,9 @@ class EmailBotService { return false; } } - - // Метод для проверки кода без 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 -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, - verificationCodes, - initEmailAuth, - verifyEmailCode + transporter }; diff --git a/backend/services/telegramBot.js b/backend/services/telegramBot.js index 1709e0f..34475fe 100644 --- a/backend/services/telegramBot.js +++ b/backend/services/telegramBot.js @@ -2,17 +2,9 @@ const { Telegraf } = require('telegraf'); const logger = require('../utils/logger'); const db = require('../db'); const authService = require('./auth-service'); +const verificationService = require('./verification-service'); let botInstance = null; -const verificationCodes = new Map(); - -// Простая остановка бота -async function stopBot() { - if (botInstance) { - await botInstance.stop(); - botInstance = null; - } -} // Создание и настройка бота async function getBot() { @@ -27,95 +19,96 @@ async function getBot() { // Обработка кодов верификации 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()] + // Получаем код верификации для всех активных кодов с провайдером telegram + const codeResult = await db.query( + `SELECT * FROM verification_codes + WHERE code = $1 + AND provider = 'telegram' + AND used = false + AND expires_at > NOW()`, + [code] ); - 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); + if (codeResult.rows.length === 0) { + ctx.reply('Неверный код подтверждения'); + return; } - // Связываем Telegram с пользователем + const verification = codeResult.rows[0]; + const providerId = verification.provider_id; + let userId = verification.user_id; + + // Отмечаем код как использованный 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()] + 'UPDATE verification_codes SET used = true WHERE id = $1', + [verification.id] ); - logger.info(`User ${userId} successfully linked Telegram account ${ctx.from.id}`); + logger.info('Starting Telegram auth process for code:', code); - // Обновляем сессию - 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'); - } + // Проверяем, существует ли уже пользователь с таким Telegram ID + const existingTelegramUser = await db.query( + `SELECT ui.user_id + FROM user_identities ui + WHERE ui.provider = 'telegram' AND ui.provider_id = $1`, + [ctx.from.id.toString()] + ); - // Отправляем последнее сообщение пользователя - 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]); + if (existingTelegramUser.rows.length > 0) { + // Если пользователь с таким Telegram ID уже существует, + // используем его ID вместо создания нового связывания + const existingUserId = existingTelegramUser.rows[0].user_id; - const lastMessage = messageResult.rows[0]?.content; - logger.info('Found last message:', lastMessage); - if (lastMessage) { - await ctx.reply(`Ваше последнее сообщение: "${lastMessage}"`); + // Связываем гостевой ID с существующим пользователем, если его еще нет + const guestIdentity = await db.query( + `SELECT * FROM user_identities + WHERE user_id = $1 AND provider = 'guest' AND provider_id = $2`, + [existingUserId, providerId] + ); + + if (guestIdentity.rows.length === 0 && providerId) { + await db.query( + `INSERT INTO user_identities + (user_id, provider, provider_id, created_at) + VALUES ($1, $2, $3, NOW()) + ON CONFLICT (provider, provider_id) DO UPDATE SET user_id = $1`, + [existingUserId, 'guest', providerId] + ); } + + userId = existingUserId; + logger.info(`Using existing user ${userId} for Telegram account ${ctx.from.id}`); + } else { + // Связываем Telegram с пользователем + await db.query( + `INSERT INTO user_identities + (user_id, provider, provider_id, created_at) + VALUES ($1, $2, $3, NOW()) + ON CONFLICT (provider, provider_id) DO UPDATE SET user_id = $1`, + [userId, 'telegram', ctx.from.id.toString()] + ); + + logger.info(`User ${userId} successfully linked Telegram account ${ctx.from.id}`); } + // Обновляем сессию в базе данных + await db.query( + `UPDATE session + SET sess = (sess::jsonb || $1::jsonb)::json + WHERE sess::jsonb @> $2::jsonb`, + [ + JSON.stringify({ + userId: userId.toString(), + authenticated: true, + authType: "telegram", + telegramId: ctx.from.id.toString() + }), + JSON.stringify({guestId: providerId}) + ] + ); + // Отправляем сообщение об успешной аутентификации await ctx.reply('Аутентификация успешна! Можете вернуться в приложение.'); @@ -126,31 +119,9 @@ async function getBot() { 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 ctx.reply('Произошла ошибка при аутентификации. Попробуйте позже.'); } }); @@ -161,23 +132,87 @@ async function getBot() { return botInstance; } +// Остановка бота +async function stopBot() { + if (botInstance) { + try { + await botInstance.stop(); + botInstance = null; + logger.info('Telegram bot stopped successfully'); + } catch (error) { + logger.error('Error stopping Telegram bot:', error); + throw error; + } + } +} + // Инициализация процесса аутентификации 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 }; + try { + // Создаем или получаем ID пользователя + let userId; + + if (session.authenticated && session.userId) { + // Если пользователь уже аутентифицирован, используем его ID + userId = session.userId; + } else if (session.guestId) { + // Проверяем, есть ли уже пользователь с этим guestId + const existingUser = await db.query( + `SELECT u.id + FROM users u + JOIN user_identities ui ON u.id = ui.user_id + WHERE ui.provider = 'guest' AND ui.provider_id = $1`, + [session.guestId] + ); + + if (existingUser.rows.length > 0) { + // Используем существующего пользователя + userId = existingUser.rows[0].id; + } else { + // Создаем нового пользователя + const userResult = await db.query( + 'INSERT INTO users (created_at) VALUES (NOW()) RETURNING id' + ); + userId = userResult.rows[0].id; + + // Связываем гостевой ID с пользователем + await db.query( + `INSERT INTO user_identities + (user_id, provider, provider_id, created_at) + VALUES ($1, $2, $3, NOW())`, + [userId, 'guest', session.guestId] + ); + } + + session.tempUserId = userId; + } else { + // Создаем нового пользователя без гостевого ID + const userResult = await db.query( + 'INSERT INTO users (created_at) VALUES (NOW()) RETURNING id' + ); + userId = userResult.rows[0].id; + session.tempUserId = userId; + } + + // Создаем код через сервис верификации + const code = await verificationService.createVerificationCode( + 'telegram', + session.guestId || 'temp', + userId + ); + + return { + verificationCode: code, + botLink: `https://t.me/${process.env.TELEGRAM_BOT_USERNAME}` + }; + } catch (error) { + logger.error('Error initializing Telegram auth:', error); + throw error; + } } module.exports = { getBot, stopBot, - verificationCodes, initTelegramAuth -}; \ No newline at end of file +}; \ No newline at end of file diff --git a/backend/services/verification-service.js b/backend/services/verification-service.js new file mode 100644 index 0000000..2ac90be --- /dev/null +++ b/backend/services/verification-service.js @@ -0,0 +1,83 @@ +const db = require('../db'); +const logger = require('../utils/logger'); + +class VerificationService { + constructor() { + this.codeLength = 6; + this.expirationMinutes = 15; + } + + // Генерация кода + generateCode() { + return Math.random().toString(36).substring(2, 2 + this.codeLength).toUpperCase(); + } + + // Создание кода верификации + async createVerificationCode(provider, providerId, userId) { + const code = this.generateCode(); + const expiresAt = new Date(Date.now() + this.expirationMinutes * 60 * 1000); + + try { + await db.query( + `INSERT INTO verification_codes + (code, provider, provider_id, user_id, expires_at) + VALUES ($1, $2, $3, $4, $5)`, + [code, provider, providerId, userId, expiresAt] + ); + + return code; + } catch (error) { + logger.error('Error creating verification code:', error); + throw error; + } + } + + // Проверка кода + async verifyCode(code, provider, providerId) { + try { + const result = await db.query( + `SELECT * FROM verification_codes + WHERE code = $1 + AND provider = $2 + AND provider_id = $3 + AND used = false + AND expires_at > NOW()`, + [code, provider, providerId] + ); + + if (result.rows.length === 0) { + return { success: false, error: 'Неверный или истекший код' }; + } + + const verification = result.rows[0]; + + // Отмечаем код как использованный + await db.query( + 'UPDATE verification_codes SET used = true WHERE id = $1', + [verification.id] + ); + + return { + success: true, + userId: verification.user_id, + providerId: verification.provider_id + }; + } catch (error) { + logger.error('Error verifying code:', error); + throw error; + } + } + + // Очистка истекших кодов + async cleanupExpiredCodes() { + try { + await db.query( + 'DELETE FROM verification_codes WHERE expires_at <= NOW()' + ); + } catch (error) { + logger.error('Error cleaning up expired codes:', error); + } + } +} + +module.exports = new VerificationService(); \ No newline at end of file diff --git a/frontend/src/composables/useAuth.js b/frontend/src/composables/useAuth.js index 2b7326a..e7bfcf2 100644 --- a/frontend/src/composables/useAuth.js +++ b/frontend/src/composables/useAuth.js @@ -9,6 +9,7 @@ export function useAuth() { const telegramInfo = ref(null); const isAdmin = ref(false); const telegramId = ref(null); + const email = ref(null); const updateAuth = ({ authenticated, authType: newAuthType, userId: newUserId, address: newAddress, telegramId: newTelegramId, isAdmin: newIsAdmin }) => { isAuthenticated.value = authenticated; @@ -23,10 +24,18 @@ export function useAuth() { try { const response = await axios.get('/api/auth/check'); console.log('Auth check response:', response.data); - updateAuth(response.data); + + isAuthenticated.value = response.data.authenticated; + userId.value = response.data.userId; + isAdmin.value = response.data.isAdmin; + authType.value = response.data.authType; + address.value = response.data.address; + telegramId.value = response.data.telegramId; + email.value = response.data.email; + return response.data; } catch (error) { - console.error('Error checking auth status:', error); + console.error('Error checking auth:', error); return { authenticated: false }; } }; @@ -71,6 +80,7 @@ export function useAuth() { telegramInfo, isAdmin, telegramId, + email, updateAuth, checkAuth, disconnect diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index 235a241..2e348b6 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -11,7 +11,7 @@
@@ -19,17 +19,24 @@ Подключение кошелька...
-
- {{ auth.address && auth.address.value ? truncateAddress(auth.address.value) : '' }} -
-
- Telegram: {{ auth.telegramId }} +
+ Telegram: {{ auth.telegramId }} +
+ +
+ Email: {{ auth.email }} +
@@ -51,9 +58,9 @@ -
+
@@ -63,11 +70,11 @@ {{ telegramVerificationCode }}
- 📱 Открыть HB3_Accelerator_Bot + Открыть HB3_Accelerator_Bot
@@ -75,18 +82,62 @@
Код подтверждения: {{ emailVerificationCode }} + Скопировано! +
+ +
+ + Отправить код на info@hb3-accelerator.com + + +
+

Выберите почтовый сервис:

+ +
+

Или скопируйте код и отправьте вручную:

+ +
+
- - ✉️ Открыть почту -
{{ emailError }} +
@@ -107,18 +158,6 @@
- - -
-
- Подключен кошелек: {{ auth.address }} - -
-
- Подключен Telegram: {{ auth.telegramId }} - -
-
@@ -156,13 +195,65 @@ const telegramBotLink = ref(''); const telegramAuthCheckInterval = ref(null); const showEmailVerification = ref(false); const emailVerificationCode = ref(''); -const emailInput = ref(''); +const emailAuthCheckInterval = ref(null); const emailError = ref(''); +const codeCopied = ref(false); +const showEmailAlternatives = ref(false); // Функция для копирования кода const copyCode = (code) => { - navigator.clipboard.writeText(code); - // Можно добавить уведомление о копировании + navigator.clipboard.writeText(code).then(() => { + codeCopied.value = true; + setTimeout(() => { + codeCopied.value = false; + }, 2000); + }); +}; + +// Функция для копирования email и кода +const copyEmailWithCode = () => { + const textToCopy = `Email: info@hb3-accelerator.com\nКод верификации: ${emailVerificationCode.value}`; + navigator.clipboard.writeText(textToCopy).then(() => { + alert('Email и код скопированы в буфер обмена'); + }); +}; + +// Функция для определения URL почтового сервиса +const getEmailServiceUrl = (service) => { + const to = 'info@hb3-accelerator.com'; + const subject = 'Verification Code'; + const body = `Код верификации: ${emailVerificationCode.value}`; + + switch (service) { + case 'gmail': + return `https://mail.google.com/mail/?view=cm&fs=1&to=${to}&su=${subject}&body=${encodeURIComponent(body)}`; + case 'outlook': + return `https://outlook.live.com/mail/0/deeplink/compose?to=${to}&subject=${subject}&body=${encodeURIComponent(body)}`; + case 'yahoo': + return `https://compose.mail.yahoo.com/?to=${to}&subject=${subject}&body=${encodeURIComponent(body)}`; + case 'proton': + return `https://mail.protonmail.com/compose?to=${to}&subject=${subject}&body=${encodeURIComponent(body)}`; + default: + return `mailto:${to}?subject=${subject}&body=${encodeURIComponent(body)}`; + } +}; + +// Функция для обработки клика по email-ссылке +const handleEmailClick = (e) => { + // Определяем, есть ли у пользователя почтовый клиент + const userAgent = navigator.userAgent.toLowerCase(); + const isMobile = /android|webos|iphone|ipad|ipod|blackberry|windows phone/i.test(userAgent); + + // На мобильных устройствах обычно есть почтовые клиенты + if (isMobile) { + // Позволяем стандартному обработчику открыть почтовый клиент + return true; + } + + // На десктопе предлагаем веб-почту + e.preventDefault(); + showEmailAlternatives.value = true; + return false; }; // Функция для показа ошибок @@ -220,24 +311,63 @@ const handleTelegramAuth = async () => { } }; -// Обработчик для Email аутентификации +// Функция для очистки ошибки +const clearEmailError = () => { + emailError.value = ''; +}; + +// Функция для обработки Email аутентификации const handleEmailAuth = async () => { try { - // Запрашиваем email у пользователя - const email = prompt('Введите ваш email:'); - if (!email) return; + clearEmailError(); // Очищаем ошибку перед новой попыткой - const { data } = await axios.post('/api/auth/email/init', { email }); - if (data.success) { + // Инициализируем процесс аутентификации + const response = await axios.post('/api/auth/email/init'); + + if (response.data.success) { showEmailVerification.value = true; - emailInput.value = email; + emailVerificationCode.value = response.data.verificationCode; + + // Запускаем проверку статуса аутентификации + startEmailAuthCheck(); } } catch (error) { - console.error('Error initializing email auth:', error); - emailError.value = error.response?.data?.error || 'Ошибка отправки кода'; + console.error('Error in email auth:', error); + emailError.value = 'Ошибка при подключении Email'; } }; +// Функция для запуска проверки статуса аутентификации по Email +const startEmailAuthCheck = () => { + if (emailAuthCheckInterval.value) { + clearInterval(emailAuthCheckInterval.value); + } + + emailAuthCheckInterval.value = setInterval(async () => { + try { + const response = await axios.get('/api/auth/check-email-verification', { + params: { code: emailVerificationCode.value } + }); + + if (response.data.verified) { + clearInterval(emailAuthCheckInterval.value); + showEmailVerification.value = false; + + // Обновляем состояние аутентификации + await auth.checkAuth(); + + // Перезагружаем страницу для обновления UI + window.location.reload(); + } else if (response.data.message) { + // Показываем сообщение пользователю, если есть + emailError.value = response.data.message; + } + } catch (error) { + console.error('Error checking email verification:', error); + } + }, 5000); // Проверяем каждые 5 секунд +}; + // Функция для сокращения адреса кошелька const truncateAddress = (address) => { if (!address) return ''; @@ -466,10 +596,18 @@ const handleMessage = async (text) => { } }; +// Функция для отключения кошелька/выхода const disconnectWallet = async () => { try { - await auth.disconnect(); - console.log('Wallet disconnected successfully'); + await axios.post('/api/auth/logout'); + auth.isAuthenticated.value = false; + auth.address.value = null; + auth.authType.value = null; + auth.telegramId = null; + auth.email = null; + + // Перезагружаем страницу для сброса состояния + window.location.reload(); } catch (error) { console.error('Error disconnecting wallet:', error); } @@ -516,6 +654,9 @@ onBeforeUnmount(() => { if (telegramAuthCheckInterval.value) { clearInterval(telegramAuthCheckInterval.value); } + if (emailAuthCheckInterval.value) { + clearInterval(emailAuthCheckInterval.value); + } }); @@ -842,9 +983,24 @@ h1 { } .error-message { - color: #D32F2F; - font-size: 0.9rem; - margin-top: 0.5rem; + position: relative; + padding: 10px; + margin: 10px 0; + background-color: #ffebee; + color: #c62828; + border-radius: 4px; + border-left: 4px solid #c62828; +} + +.close-error { + position: absolute; + top: 5px; + right: 5px; + background: none; + border: none; + color: #c62828; + font-size: 16px; + cursor: pointer; } .auth-buttons { @@ -878,81 +1034,45 @@ h1 { color: white; } +.telegram-btn:hover { + background-color: #0077b5; +} + .email-btn { - background-color: #4caf50; + background-color: #f44336; color: white; } -.auth-icon { - margin-right: 8px; +.email-btn:hover { + background-color: #d32f2f; } -.email-form { - margin-top: 10px; - padding: 10px; - border: 1px solid #ccc; - border-radius: 4px; -} - -.email-form input { - width: 100%; - padding: 8px; - margin-bottom: 10px; - border: 1px solid #ccc; - border-radius: 4px; -} - -.email-form button { - padding: 8px 16px; - background-color: #4CAF50; +.disconnect-btn { + background-color: #f44336; color: white; - border: none; - border-radius: 4px; - cursor: pointer; } -.email-form button:disabled { - background-color: #cccccc; - cursor: not-allowed; +.disconnect-btn:hover { + background-color: #d32f2f; } -.auth-form { - margin-top: 10px; - padding: 15px; - border: 1px solid #ddd; - border-radius: 4px; - background: #f9f9f9; -} - -.auth-input { - width: 100%; - padding: 8px 12px; - margin-bottom: 10px; - border: 1px solid #ddd; - border-radius: 4px; - font-size: 14px; -} - -.error-message { - color: #dc3545; - font-size: 14px; - margin-top: 5px; -} - -.load-more-container { +/* Стили для информации о пользователе в header */ +.auth-info-header { display: flex; - justify-content: center; - padding: 10px; - background-color: #f5f5f5; - position: sticky; - top: 0; - z-index: 1; + align-items: center; + gap: 15px; } -.load-more-btn { +.user-id { + font-size: 14px; + color: #333; +} + +/* Общие стили для кнопок */ +.auth-btn { + display: flex; + align-items: center; padding: 8px 16px; - background-color: #4a5568; - color: white; border: none; border-radius: 4px; cursor: pointer; @@ -960,155 +1080,149 @@ h1 { transition: background-color 0.2s; } -.load-more-btn:hover:not(:disabled) { - background-color: #2d3748; +.wallet-btn { + background-color: #4CAF50; + color: white; } -.load-more-btn:disabled { - background-color: #cbd5e0; - cursor: not-allowed; +.wallet-btn:hover { + background-color: #45a049; } -.wallet-section { - margin-top: 20px; - padding: 20px; - border: 1px solid #ccc; - border-radius: 8px; - background-color: #f9f9f9; +.telegram-btn { + background-color: #0088cc; + color: white; } -.wallet-info { - display: flex; - align-items: center; - gap: 1rem; - margin-bottom: 1rem; +.telegram-btn:hover { + background-color: #0077b5; +} + +.email-btn { + background-color: #f44336; + color: white; +} + +.email-btn:hover { + background-color: #d32f2f; } .disconnect-btn { - padding: 0.5rem 1rem; - background-color: #ff4444; + background-color: #f44336; color: white; - border: none; - border-radius: 4px; - cursor: pointer; } .disconnect-btn:hover { - background-color: #cc0000; + background-color: #d32f2f; } -.chat-history { - height: 60vh; - overflow-y: auto; - padding: 1rem; - border: 1px solid #ddd; - border-radius: 4px; - margin-top: 1rem; -} - -/* Добавим индикатор загрузки */ -.loading { - text-align: center; - padding: 1rem; - color: #666; -} - -/* Добавляем отображение кода и ссылки для Telegram */ -.verification-info { - padding: 10px; - background-color: #f9f9f9; - border: 1px solid #ddd; - border-radius: 4px; +.bot-link { + display: inline-block; 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 { + color: #0088cc; text-decoration: underline; + cursor: pointer; +} + +.bot-link:hover { + color: #005580; } .verification-block { - display: flex; - flex-direction: column; - gap: 8px; - padding: 10px; - background: #f5f5f5; - border-radius: 8px; - margin: 8px 0; + margin: 15px 0; + padding: 15px; + background-color: #f5f5f5; + border-radius: 5px; + border-left: 4px solid #0088cc; } .verification-code { display: flex; align-items: center; - gap: 8px; + gap: 10px; + margin-bottom: 10px; } .verification-code code { - background: #fff; - padding: 4px 8px; - border-radius: 4px; + background-color: #e0e0e0; + padding: 5px 10px; + border-radius: 3px; font-family: monospace; cursor: pointer; + user-select: all; +} + +.copied-message { + color: #4CAF50; + font-size: 12px; +} + +.email-options { + display: flex; + flex-direction: column; + gap: 15px; +} + +.email-alternatives { + margin-top: 15px; + padding: 15px; + background-color: #fff; + border-radius: 5px; border: 1px solid #ddd; } -.verification-code code:hover { - background: #f0f0f0; +.email-services { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin: 15px 0; } -.bot-link { - display: flex; +.email-service-btn { + display: inline-flex; align-items: center; justify-content: center; - gap: 8px; - padding: 8px 16px; - background: #0088cc; + padding: 8px 15px; + border-radius: 4px; color: white; text-decoration: none; - border-radius: 4px; - transition: background-color 0.2s; + font-weight: 500; + min-width: 120px; } -.bot-link:hover { - background: #006699; +.gmail { + background-color: #D44638; } -.auth-icon { - font-size: 1.2em; +.outlook { + background-color: #0078D4; } -/* Добавляем новые стили для информации о пользователе */ -.auth-info { - margin-top: 10px; - padding: 10px; - border: 1px solid #ccc; - border-radius: 4px; - background-color: #f9f9f9; +.yahoo { + background-color: #6001D2; } -.auth-info button { - padding: 8px 16px; - background-color: #ff4444; - color: white; - border: none; +.proton { + background-color: #8A6EFF; +} + +.manual-option { + margin-top: 15px; + padding-top: 15px; + border-top: 1px solid #eee; +} + +.copy-button { + background-color: #f0f0f0; + border: 1px solid #ddd; + padding: 8px 15px; border-radius: 4px; cursor: pointer; - margin-top: 10px; + font-size: 14px; + margin-top: 5px; } -.auth-info button:hover { - background-color: #cc0000; +.copy-button:hover { + background-color: #e0e0e0; }