Описание изменений

This commit is contained in:
2025-03-14 16:13:56 +03:00
parent 7667161131
commit 7fc14e42b9
11 changed files with 1702 additions and 663 deletions

View File

@@ -14,40 +14,54 @@ const requireAuth = async (req, res, next) => {
console.log('Authorization header:', req.headers.authorization); 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(); return next();
} }
// Проверяем заголовок авторизации // Проверяем заголовок авторизации
const authHeader = req.headers.authorization; const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) { if (authHeader && authHeader.startsWith('Bearer ')) {
const address = authHeader.split(' ')[1]; const token = authHeader.split(' ')[1];
console.log('Found address in Authorization header:', address);
try { // Проверяем, это адрес кошелька или JWT-токен
// Находим пользователя по адресу if (token.startsWith('0x')) {
const { pool } = require('../db'); // Это адрес кошелька
console.log('Querying database for user with address:', address); const address = token;
const result = await pool.query('SELECT * FROM users WHERE LOWER(address) = LOWER($1)', [address]); console.log('Found address in Authorization header:', address);
console.log('Database query result:', result.rows);
if (result.rows.length > 0) { try {
const user = result.rows[0]; // Проверяем, существует ли пользователь с таким адресом
console.log('Found user by address:', user); 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]);
// Устанавливаем данные пользователя в запросе if (result.rows.length > 0) {
req.user = { const user = result.rows[0];
userId: user.id, req.user = {
address: address, userId: user.id,
isAdmin: user.is_admin address: address,
}; isAdmin: user.is_admin,
authType: 'wallet'
return next(); };
} else { return next();
console.log('No user found with address:', address); }
} catch (error) {
console.error('Error finding user by address:', error);
} }
} catch (error) { } else {
console.error('Error finding user by address:', error); // Здесь можно добавить логику проверки JWT, если используется
} }
} }

View File

@@ -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;

View File

@@ -11,6 +11,7 @@ const { pool } = require('../db');
const { verifySignature, checkAccess, findOrCreateUser } = require('../utils/auth'); const { verifySignature, checkAccess, findOrCreateUser } = require('../utils/auth');
const authService = require('../services/auth-service'); const authService = require('../services/auth-service');
const { SiweMessage } = require('siwe'); const { SiweMessage } = require('siwe');
const { sendEmail } = require('../services/emailBot');
// Создайте лимитер для попыток аутентификации // Создайте лимитер для попыток аутентификации
const authLimiter = rateLimit({ 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) { 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 // Аутентификация через Email
router.post('/email', async (req, res) => { router.post('/email', async (req, res) => {
try { 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]);
// Если есть связанный кошелек, проверяем токены const identities = identitiesResult.rows;
if (identities.wallet) {
await authService.checkTokensAndUpdateRole(identities.wallet); // Формируем объект с идентификаторами по типам
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.userId = userId;
req.session.email = email; req.session.email = email;
req.session.authType = 'email'; req.session.authType = 'email';
req.session.authenticated = true; req.session.authenticated = true;
req.session.isAdmin = isAdmin;
res.json({ if (identitiesMap.wallet) {
authenticated: true, 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,
userId, userId,
isAdmin, isAdmin,
authType: 'email', authType: 'email',
identities identities: identitiesMap
}); });
} catch (error) { } catch (error) {
logger.error(`Email auth error: ${error.message}`); 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); const isAdmin = await authService.isAdmin(req.session.userId);
@@ -383,103 +580,186 @@ router.get('/telegram', (req, res) => {
res.json({ authUrl }); res.json({ authUrl });
}); });
// Маршрут для авторизации через Email // Маршрут для получения кода подтверждения Telegram
router.post('/email', async (req, res) => { router.get('/telegram/code', async (req, res) => {
try { try {
const { email } = req.body; // Генерируем код подтверждения (6 символов)
const verificationCode = Math.random().toString(36).substring(2, 8).toUpperCase();
if (!email) {
return res.status(400).json({ error: 'Email is required' });
}
// Генерируем код подтверждения
const verificationCode = Math.floor(100000 + Math.random() * 900000).toString();
// Сохраняем код в сессии // Сохраняем код в сессии
req.session.emailVerificationCode = verificationCode; req.session.telegramVerificationData = {
req.session.pendingEmail = email; code: verificationCode,
expires: Date.now() + 10 * 60 * 1000 // 10 минут
};
// В реальном приложении здесь нужно отправить email с кодом подтверждения res.json({
// Удалите или закомментируйте эти логи success: true,
// console.log(`Verification code for ${email}: ${verificationCode}`); message: 'Отправьте этот код боту @' + process.env.TELEGRAM_BOT_USERNAME,
code: verificationCode,
res.json({ success: true, message: 'Verification code sent' }); botUsername: process.env.TELEGRAM_BOT_USERNAME || 'YourDAppBot'
});
} catch (error) { } catch (error) {
// Удалите или закомментируйте эти логи logger.error(`Error in telegram code request: ${error.message}`);
// console.error('Error sending verification code:', error); res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' });
logger.error('Error sending verification code:', error);
res.status(500).json({ error: 'Failed to send verification code' });
} }
}); });
// Маршрут для проверки кода подтверждения Email // Маршрут для верификации Telegram
router.post('/email/verify', async (req, res) => { router.post('/telegram/verify', async (req, res) => {
try { try {
const { email, code } = req.body; const { telegramId, code } = req.body;
const verificationData = req.session.telegramVerificationData;
if (!email || !code) { // Проверяем, что код существует и не истек
return res.status(400).json({ error: 'Email and code are required' }); if (!verificationData ||
verificationData.code !== code ||
Date.now() > verificationData.expires) {
return res.status(400).json({
success: false,
error: 'Неверный или истекший код подтверждения'
});
} }
// Получаем код из сессии // Ищем или создаем пользователя с этим Telegram ID
const verificationCode = req.session.emailVerificationCode; const result = await db.query(
const pendingEmail = req.session.pendingEmail; 'SELECT * FROM find_or_create_user_by_identity($1, $2)',
['telegram', telegramId]
);
if (!verificationCode || !pendingEmail) { const userId = result.rows[0].user_id;
return res.status(400).json({ error: 'No pending verification' }); const isNew = result.rows[0].is_new;
}
// Проверяем, что email совпадает с тем, для которого был сгенерирован код // Проверяем, есть ли у пользователя связанный кошелек
if (pendingEmail !== email) { const walletResult = await db.query(`
return res.status(400).json({ error: 'Email mismatch' }); SELECT identity_value
} FROM user_identities ui
WHERE ui.user_id = $1 AND ui.identity_type = 'wallet'
`, [userId]);
// Проверяем код const hasWallet = walletResult.rows.length > 0;
if (verificationCode !== code) { let walletAddress = null;
return res.status(400).json({ error: 'Invalid verification code' });
}
// Проверяем, существует ли пользователь в базе данных
const user = await db.query('SELECT * FROM users WHERE email = $1', [email]);
let userId;
let isAdmin = false; let isAdmin = false;
if (user.rows.length === 0) { // Если есть кошелек, проверяем наличие токенов
// Если пользователь не существует, создаем его if (hasWallet) {
const newUser = await db.query( walletAddress = walletResult.rows[0].identity_value;
'INSERT INTO users (email, created_at) VALUES ($1, NOW()) RETURNING id', const userResult = await db.query('SELECT is_admin FROM users WHERE id = $1', [userId]);
[email] isAdmin = userResult.rows[0].is_admin;
);
userId = newUser.rows[0].id;
} else {
userId = user.rows[0].id;
isAdmin = user.rows[0].is_admin || false;
} }
// Устанавливаем состояние аутентификации в сессии // Устанавливаем сессию
req.session.isAuthenticated = true;
req.session.authenticated = true; req.session.authenticated = true;
req.session.address = email;
req.session.userId = userId; req.session.userId = userId;
req.session.authType = 'telegram';
req.session.telegramId = telegramId;
req.session.isAdmin = isAdmin; req.session.isAdmin = isAdmin;
req.session.authType = 'email'; if (walletAddress) {
req.session.address = walletAddress;
}
// Удаляем код из сессии // Сохраняем сессию
delete req.session.emailVerificationCode; await new Promise((resolve, reject) => {
delete req.session.pendingEmail; req.session.save(err => {
if (err) reject(err);
else resolve();
});
});
// Очищаем данные верификации
delete req.session.telegramVerificationData;
res.json({ res.json({
success: true,
authenticated: true, authenticated: true,
address: email, userId,
telegramId,
isAdmin, isAdmin,
authType: 'email', hasWallet,
walletAddress,
isNew
}); });
} catch (error) { } catch (error) {
// Удалите или закомментируйте эти логи logger.error(`Error in telegram verification: ${error.message}`);
// console.error('Error verifying email code:', error); res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' });
logger.error('Error verifying email code:', error); }
res.status(500).json({ error: 'Failed to verify email code' }); });
// Маршрут для связывания разных идентификаторов
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; module.exports = router;

View File

@@ -33,7 +33,7 @@ const messagesRouter = require('./routes/messages');
const sessionMiddleware = require('./middleware/session'); const sessionMiddleware = require('./middleware/session');
// Импорт сервисов // Импорт сервисов
const { initTelegramBot } = require('./services/telegram-service'); const telegramService = require('./services/telegramBot');
const PORT = process.env.PORT || 8000; const PORT = process.env.PORT || 8000;
@@ -199,7 +199,7 @@ async function initServices() {
// Инициализируем ботов, если они нужны // Инициализируем ботов, если они нужны
if (process.env.TELEGRAM_BOT_TOKEN) { 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 бот инициализирован'); console.log('Telegram бот инициализирован');
} }
@@ -683,7 +683,7 @@ process.on('unhandledRejection', (reason, promise) => {
}); });
// Инициализация Telegram бота // Инициализация Telegram бота
initTelegramBot(); telegramService.initTelegramBot();
// Добавьте после других маршрутов // Добавьте после других маршрутов
const chatRouter = require('./routes/chat'); const chatRouter = require('./routes/chat');

View File

@@ -206,6 +206,46 @@ class AuthService {
return null; return null;
} }
} }
/**
* Получает все идентификаторы пользователя
* @param {number} userId - ID пользователя
* @returns {Promise<Array>} - Список идентификаторов
*/
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<boolean>} - Является ли пользователь администратором
*/
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(); module.exports = new AuthService();

View File

@@ -3,6 +3,11 @@ const nodemailer = require('nodemailer');
const Imap = require('imap'); const Imap = require('imap');
const simpleParser = require('mailparser').simpleParser; const simpleParser = require('mailparser').simpleParser;
const { processMessage } = require('./ai-assistant'); 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({ const transporter = nodemailer.createTransport({
@@ -13,6 +18,9 @@ const transporter = nodemailer.createTransport({
user: process.env.EMAIL_USER, user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASSWORD, pass: process.env.EMAIL_PASSWORD,
}, },
tls: {
rejectUnauthorized: false // Отключение проверки сертификата
}
}); });
// Конфигурация для получения писем // Конфигурация для получения писем
@@ -25,222 +33,307 @@ const imapConfig = {
tlsOptions: { rejectUnauthorized: false }, tlsOptions: { rejectUnauthorized: false },
}; };
/** class EmailBotService {
* Инициализация сервиса электронной почты constructor(user, password) {
*/ this.user = user;
function initEmailBot() { this.password = password;
if (!process.env.EMAIL_USER || !process.env.EMAIL_PASSWORD) { this.transporter = null;
console.warn('EMAIL_USER or EMAIL_PASSWORD not set, Email integration disabled'); this.imap = null;
return null; this.initialize();
this.listenForReplies();
} }
console.log('Email bot initialized'); initialize() {
// Настройка транспорта
// Запуск проверки почты каждые 5 минут this.transporter = nodemailer.createTransport({
const checkInterval = 5 * 60 * 1000; // 5 минут host: process.env.EMAIL_SMTP_HOST,
setInterval(checkEmails, checkInterval); port: process.env.EMAIL_SMTP_PORT,
secure: true,
// Первая проверка при запуске auth: {
checkEmails(); user: this.user,
pass: this.password
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;
} }
});
// Поиск непрочитанных писем // Настройка IMAP для чтения входящих писем
imap.search(['UNSEEN'], (err, results) => { this.imap = new Imap({
if (err) { user: this.user,
console.error('Error searching emails:', err); password: this.password,
return; 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: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #ddd; border-radius: 5px;">
<h2 style="color: #333;">Код подтверждения для DApp for Business</h2>
<p>Ваш код подтверждения:</p>
<div style="font-size: 24px; font-weight: bold; padding: 15px; background-color: #f5f5f5; border-radius: 5px; text-align: center; margin: 20px 0;">
${verificationCode}
</div>
<p>Для завершения аутентификации, пожалуйста, ответьте на это письмо, указав только полученный код.</p>
<p>Код действителен в течение 15 минут.</p>
<hr style="margin: 20px 0; border: none; border-top: 1px solid #ddd;">
<p style="font-size: 12px; color: #777;">Это автоматическое сообщение, пожалуйста, не отвечайте на него.</p>
</div>
`
};
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) { this.imap.once('ready', () => {
console.log('No new emails'); this.imap.openBox('INBOX', false, (err, box) => {
imap.end(); if (err) {
return; logger.error(`Error opening inbox: ${err}`);
} this.imap.end();
return;
}
console.log(`Found ${results.length} new emails`); // Ищем непрочитанные письма
this.imap.search(['UNSEEN'], (err, results) => {
if (err) {
logger.error(`Error searching messages: ${err}`);
this.imap.end();
return;
}
const f = imap.fetch(results, { bodies: '' }); if (!results || results.length === 0) {
logger.info('No new messages found');
this.imap.end();
return;
}
f.on('message', (msg, seqno) => { // Защищаемся от пустых результатов
msg.on('body', (stream, info) => { try {
simpleParser(stream, async (err, parsed) => { const f = this.imap.fetch(results, { bodies: '' });
if (err) {
console.error('Error parsing email:', err);
return;
}
try { f.on('message', (msg, seqno) => {
// Обработка письма msg.on('body', (stream, info) => {
await processEmail(parsed); simpleParser(stream, async (err, parsed) => {
if (err) {
logger.error(`Error parsing message: ${err}`);
return;
}
// Пометить как прочитанное // Обработка входящего письма
imap.setFlags(results, ['\\Seen'], (err) => { try {
if (err) { await this.processEmail(parsed);
console.error('Error marking email as read:', err); } 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) => { this.imap.connect();
console.error('IMAP error:', err); } catch (error) {
}); logger.error(`Global error checking emails: ${error.message}`);
// Обеспечиваем корректное завершение IMAP сессии
try {
this.imap.end();
} catch (e) {
// Игнорируем ошибки при закрытии
}
}
}
imap.connect(); async processEmail(email) {
} try {
const fromEmail = email.from.value[0].address.toLowerCase();
const subject = email.subject;
const text = email.text;
/** // Ищем код в тексте письма
* Обработка полученного письма const codeMatch = text.match(/\b\d{6}\b/);
* @param {Object} email - Распарсенное письмо if (!codeMatch) return;
*/
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}`); const code = codeMatch[0];
// Поиск пользователя по email // Проверяем, есть ли код для этого email
const userResult = await pool.query( const verificationData = verificationCodes.get(fromEmail);
`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) { if (verificationData && verificationData.code === code) {
console.log(`No verified user found for email ${from}`); // Проверяем срок действия
// Отправка ответа о необходимости регистрации if (Date.now() > verificationData.expires) {
await sendEmail( // Код истек
from, this.transporter.sendMail({
'Регистрация в системе', from: this.user,
'Для использования ИИ-ассистента через email, пожалуйста, зарегистрируйтесь на нашем сайте и подтвердите свой email.' to: fromEmail,
); subject: 'Срок действия кода истек',
return; 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}`);
}
}
// Оставляем существующие методы
async sendEmail(to, subject, text) {
try {
const mailOptions = {
from: this.user,
to,
subject,
text
};
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;
}
}
// Метод для проверки кода без IMAP
verifyCode(email, code) {
email = email.toLowerCase();
const data = verificationCodes.get(email);
if (!data) {
return { success: false, error: 'Код не найден' };
} }
const user = userResult.rows[0]; if (Date.now() > data.expires) {
verificationCodes.delete(email);
// Получение или создание диалога return { success: false, error: 'Срок действия кода истек' };
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;
} }
// Сохранение сообщения пользователя if (data.code !== code) {
await pool.query( return { success: false, error: 'Неверный код' };
`INSERT INTO messages (conversation_id, sender_type, sender_id, content, channel) }
VALUES ($1, $2, $3, $4, $5)`,
[conversationId, 'user', user.id, text, 'email']
);
// Обработка сообщения ИИ-ассистентом return {
const aiResponse = await processMessage(user.id, text, user.language || 'ru'); success: true,
token: data.token
};
}
// Сохранение ответа ИИ // Метод для удаления кода после проверки
await pool.query( removeCode(email) {
`INSERT INTO messages (conversation_id, sender_type, sender_id, content, channel) verificationCodes.delete(email.toLowerCase());
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;
} }
} }
/** // Экспортируем класс и хранилище кодов
* Отправка email module.exports = EmailBotService;
* @param {string} to - Адрес получателя module.exports.verificationCodes = verificationCodes;
* @param {string} subject - Тема письма
* @param {string} text - Текст письма
* @returns {Promise<Object>} - Результат отправки
*/
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,
};

View File

@@ -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<Object>} - Информация о пользователе
*/
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<Object>} - Информация о диалоге
*/
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<Object>} - Информация о сообщении
*/
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<boolean>} - Успешность отправки
*/
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,
};

View File

@@ -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' +
`<code>${verificationCode}</code>\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' +
`<code>${verificationCode}</code>\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<Object>} - Результат операции
*/
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
};

View File

@@ -0,0 +1,195 @@
<template>
<div class="telegram-auth">
<div v-if="!isAuthenticating">
<a :href="telegramBotLink" target="_blank" class="telegram-btn" @click="startAuth">
<span class="auth-icon">📱</span> Подключить Telegram
</a>
</div>
<div v-else class="auth-progress">
<p>Для завершения авторизации:</p>
<ol>
<li>Перейдите в Telegram-бота <strong>@{{ botUsername }}</strong></li>
<li>Если бот не открылся автоматически, скопируйте и отправьте ему команду:</li>
</ol>
<div class="auth-code">
/auth {{ authToken }}
</div>
<button class="copy-btn" @click="copyAuthCommand">Копировать команду</button>
<div class="auth-actions">
<button class="cancel-btn" @click="cancelAuth">Отмена</button>
<button class="check-btn" @click="checkAuthStatus">Проверить статус</button>
</div>
<div v-if="errorMessage" class="error-message">
{{ errorMessage }}
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import { useAuthStore } from '../stores/auth';
const auth = useAuthStore();
const isAuthenticating = ref(false);
const authToken = ref('');
const botUsername = ref(process.env.VUE_APP_TELEGRAM_BOT_USERNAME || 'HB3_Accelerator_Bot');
const errorMessage = ref('');
const checkInterval = ref(null);
// Формируем ссылку на бота с параметром авторизации
const telegramBotLink = computed(() => {
// Возвращаем ссылку только если есть токен
if (!authToken.value) return `https://t.me/${botUsername.value}`;
return `https://t.me/${botUsername.value}?start=auth_${authToken.value}`;
});
async function startAuth() {
try {
// Сначала запрашиваем токен
const response = await auth.createTelegramAuthToken();
if (response.success) {
authToken.value = response.token;
// Теперь можно включить режим авторизации
isAuthenticating.value = true;
// И запустить проверку
checkInterval.value = setInterval(checkAuthStatus, 3000);
// Открываем Telegram
console.log(`Открывается ссылка на Telegram: ${telegramBotLink.value}`);
window.open(telegramBotLink.value, '_blank');
} else {
errorMessage.value = response.error || 'Не удалось начать авторизацию';
}
} catch (error) {
console.error('Error starting Telegram auth:', error);
errorMessage.value = 'Ошибка при инициализации авторизации';
}
}
async function checkAuthStatus() {
try {
const response = await auth.checkTelegramAuthStatus(authToken.value);
if (response.success && response.authenticated) {
// Авторизация успешна, очищаем интервал и состояние
clearInterval(checkInterval.value);
isAuthenticating.value = false;
// Здесь можно добавить дополнительные действия после успешной авторизации
}
} catch (error) {
console.error('Error checking auth status:', error);
}
}
function cancelAuth() {
clearInterval(checkInterval.value);
isAuthenticating.value = false;
authToken.value = '';
errorMessage.value = '';
}
function copyAuthCommand() {
const command = `/auth ${authToken.value}`;
navigator.clipboard.writeText(command);
// Можно добавить уведомление о копировании
}
</script>
<style scoped>
.telegram-auth {
margin-bottom: 15px;
}
.telegram-btn {
display: flex;
align-items: center;
justify-content: center;
background-color: #0088cc;
color: white;
border: none;
border-radius: 4px;
padding: 8px 16px;
font-weight: bold;
cursor: pointer;
width: 100%;
}
.auth-icon {
margin-right: 8px;
}
.auth-progress {
background-color: #f8f8f8;
border-radius: 8px;
padding: 16px;
margin-top: 10px;
}
.auth-actions {
display: flex;
justify-content: space-between;
margin-top: 15px;
}
.cancel-btn {
background-color: #f5f5f5;
color: #333;
border: 1px solid #ddd;
border-radius: 4px;
padding: 8px;
cursor: pointer;
}
.check-btn {
background-color: #0088cc;
color: white;
border: none;
border-radius: 4px;
padding: 8px;
cursor: pointer;
font-weight: bold;
}
.error-message {
color: #ff4d4f;
margin-top: 10px;
font-size: 14px;
}
.auth-code {
font-family: monospace;
font-size: 16px;
padding: 12px;
background-color: #f1f1f1;
border-radius: 4px;
margin: 15px 0;
white-space: nowrap;
overflow-x: auto;
}
.copy-btn {
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
padding: 8px 12px;
cursor: pointer;
font-weight: bold;
display: block;
margin-bottom: 15px;
}
.copy-btn:hover {
background-color: #45a049;
}
</style>

View File

@@ -260,6 +260,167 @@ export const useAuthStore = defineStore('auth', {
} }
return false; 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: 'Ошибка проверки статуса' };
}
} }
} }
}); });

View File

@@ -30,22 +30,42 @@
</div> </div>
<div class="auth-option"> <div class="auth-option">
<button class="auth-btn telegram-btn" @click="connectTelegram"> <TelegramConnect />
<span class="auth-icon">📱</span> Подключить Telegram
</button>
</div> </div>
<div class="auth-option email-option"> <!-- Email аутентификация: первый шаг - запрос кода -->
<div v-if="!showEmailVerification" class="auth-option email-option">
<input <input
type="email" type="email"
v-model="email" v-model="email"
placeholder="Введите ваш email" placeholder="Введите ваш email"
class="email-input" class="email-input"
/> />
<button class="auth-btn email-btn" @click="connectEmail" :disabled="!isValidEmail"> <button class="auth-btn email-btn" @click="requestEmailCode" :disabled="!isValidEmail">
<span class="auth-icon"></span> Подключить Email <span class="auth-icon"></span> Подключить Email
</button> </button>
</div> </div>
<!-- Email аутентификация: второй шаг - ввод кода -->
<div v-else class="auth-option email-verification">
<p>Код подтверждения отправлен на {{ email }}</p>
<input
type="text"
v-model="emailVerificationCode"
placeholder="Введите код подтверждения"
class="verification-input"
/>
<div class="email-verification-actions">
<button class="auth-btn email-btn" @click="verifyEmailCode">
<span class="auth-icon"></span> Подтвердить
</button>
<button class="auth-btn cancel-btn" @click="cancelEmailVerification">
Отмена
</button>
</div>
</div>
<div v-if="emailErrorMessage" class="error-message">{{ emailErrorMessage }}</div>
</div> </div>
<div class="message-time"> <div class="message-time">
@@ -73,6 +93,7 @@
import { ref, computed, onMounted, watch, nextTick } from 'vue'; import { ref, computed, onMounted, watch, nextTick } from 'vue';
import { useAuthStore } from '../stores/auth'; import { useAuthStore } from '../stores/auth';
import WalletConnection from '../components/WalletConnection.vue'; import WalletConnection from '../components/WalletConnection.vue';
import TelegramConnect from '../components/TelegramConnect.vue';
import axios from '../api/axios'; import axios from '../api/axios';
console.log('HomeView.vue: Version with chat loaded'); console.log('HomeView.vue: Version with chat loaded');
@@ -89,6 +110,11 @@ const hasShownAuthMessage = ref(false);
const guestMessages = ref([]); const guestMessages = ref([]);
const hasShownAuthOptions = ref(false); const hasShownAuthOptions = ref(false);
// Email аутентификация
const emailVerificationCode = ref('');
const showEmailVerification = ref(false);
const emailErrorMessage = ref('');
// Простая функция для выхода // Простая функция для выхода
const logout = async () => { const logout = async () => {
await auth.logout(); await auth.logout();
@@ -320,95 +346,66 @@ async function connectTelegram() {
} }
} }
// Функция для подключения через Email // Запрос кода подтверждения по email
async function connectEmail() { async function requestEmailCode() {
if (!isValidEmail.value) return; emailErrorMessage.value = '';
try { try {
messages.value.push({ const response = await auth.requestEmailVerification(email.value);
sender: 'ai',
text: `Отправляем код подтверждения на ${email.value}...`,
timestamp: new Date(),
});
// Отправляем запрос на отправку кода подтверждения if (response.success) {
const response = await axios.post('/api/auth/email', { showEmailVerification.value = true;
email: email.value // Временно для тестирования
}, { if (response.verificationCode) {
withCredentials: true emailErrorMessage.value = `Код для тестирования: ${response.verificationCode}`;
});
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(),
});
} }
} else {
emailErrorMessage.value = response.error || 'Ошибка запроса кода подтверждения';
} }
} catch (error) { } catch (error) {
console.error('Error connecting with email:', error); console.error('Error requesting email verification:', error);
emailErrorMessage.value = 'Ошибка запроса кода подтверждения';
messages.value.push({
sender: 'ai',
text: 'Извините, произошла ошибка при подключении Email. Пожалуйста, попробуйте позже.',
timestamp: new Date(),
});
} }
} }
// Подтверждение кода подтверждения по 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 = '';
}
// Добавьте эту функцию в <script setup> // Добавьте эту функцию в <script setup>
const formatAddress = (address) => { const formatAddress = (address) => {
if (!address) return ''; if (!address) return '';
@@ -856,4 +853,14 @@ h1 {
background-color: #4caf50; background-color: #4caf50;
color: white; color: white;
} }
.cancel-btn {
background-color: #999;
}
.error-message {
color: #D32F2F;
font-size: 0.9rem;
margin-top: 0.5rem;
}
</style> </style>