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

This commit is contained in:
2025-03-20 18:44:32 +03:00
parent fe9783e814
commit ee6e71d7ec
11 changed files with 1177 additions and 657 deletions

View File

@@ -47,6 +47,7 @@
"pg": "^8.10.0",
"session-file-store": "^1.5.0",
"siwe": "^2.1.4",
"telegraf": "^4.16.3",
"winston": "^3.17.0"
},
"devDependencies": {

View File

@@ -13,6 +13,9 @@ const { sendEmail } = require('../services/emailBot');
const { verificationCodes } = require('../services/telegramBot');
const { checkTokensAndUpdateRole } = require('../services/auth-service');
const { ethers } = require('ethers');
const { initTelegramAuth } = require('../services/telegramBot');
const { initEmailAuth, verifyEmailCode } = require('../services/emailBot');
const { getBot } = require('../services/telegramBot');
// Создайте лимитер для попыток аутентификации
const authLimiter = rateLimit({
@@ -34,24 +37,32 @@ router.get('/nonce', async (req, res) => {
// Генерируем случайный nonce
const nonce = crypto.randomBytes(16).toString('hex');
// Удаляем старые nonce для этого адреса
await db.query(`
DELETE FROM nonces
WHERE identity_value = $1
`, [address.toLowerCase()]);
// Проверяем, существует ли уже nonce для этого адреса
const existingNonce = await db.query(
'SELECT id FROM nonces WHERE identity_value = $1',
[address.toLowerCase()]
);
// Сохраняем новый nonce
await db.query(`
INSERT INTO nonces (identity_value, nonce, expires_at)
VALUES ($1, $2, NOW() + INTERVAL '15 minutes')
`, [address.toLowerCase(), nonce]);
if (existingNonce.rows.length > 0) {
// Обновляем существующий nonce
await db.query(
'UPDATE nonces SET nonce = $1, expires_at = NOW() + INTERVAL \'15 minutes\' WHERE identity_value = $2',
[nonce, address.toLowerCase()]
);
} else {
// Создаем новый nonce
await db.query(
'INSERT INTO nonces (identity_value, nonce, expires_at) VALUES ($1, $2, NOW() + INTERVAL \'15 minutes\')',
[address.toLowerCase(), nonce]
);
}
console.log(`Nonce ${nonce} сохранен для адреса ${address}`);
return res.json({ nonce });
res.json({ nonce });
} catch (error) {
console.error('Error generating nonce:', error);
return res.status(500).json({ error: 'Internal server error' });
res.status(500).json({ error: 'Failed to generate nonce' });
}
});
@@ -60,77 +71,85 @@ const ERC20_ABI = [
"function balanceOf(address owner) view returns (uint256)"
];
// Проверка подписи и аутентификация
// Верификация подписи и создание сессии
router.post('/verify', async (req, res) => {
try {
const { address, signature, message } = req.body;
console.log('Received verification request:', { address, message });
console.log('Signature:', signature);
// Проверяем подпись через SIWE
const siwe = new SiweMessage(message);
console.log('Created SIWE message object');
const fields = await siwe.verify({ signature });
console.log('SIWE validation result:', fields);
const { address, message, signature } = req.body;
if (!fields || !fields.success) {
console.log('SIWE validation failed');
return res.status(401).json({ error: 'Invalid signature' });
// Проверяем подпись
const isValid = await authService.verifySignature(message, signature, address);
if (!isValid) {
return res.status(401).json({ success: false, error: 'Invalid signature' });
}
// Проверяем nonce
const nonceResult = await db.query(
`SELECT nonce FROM nonces
WHERE identity_value = $1
AND expires_at > NOW()`,
[address.toLowerCase()]
);
console.log('Nonce check result:', nonceResult.rows);
if (!nonceResult.rows.length) {
console.log('Invalid or expired nonce');
return res.status(401).json({ error: 'Invalid or expired nonce' });
const nonceResult = await db.query('SELECT nonce FROM nonces WHERE identity_value = $1', [address.toLowerCase()]);
if (nonceResult.rows.length === 0 || nonceResult.rows[0].nonce !== message.match(/Nonce: ([^\n]+)/)[1]) {
return res.status(401).json({ success: false, error: 'Invalid nonce' });
}
// Проверяем соответствие nonce
if (nonceResult.rows[0].nonce !== fields.data.nonce) {
console.log('Nonce mismatch');
return res.status(401).json({ error: 'Invalid nonce' });
// Находим или создаем пользователя
let userId, isAdmin;
// Ищем пользователя по адресу в таблице user_identities
const userResult = await db.query(`
SELECT u.* FROM users u
JOIN user_identities ui ON u.id = ui.user_id
WHERE ui.provider = 'wallet' AND ui.provider_id = $1
`, [address.toLowerCase()]);
if (userResult.rows.length > 0) {
// Пользователь найден
userId = userResult.rows[0].id;
isAdmin = userResult.rows[0].role === 'admin';
const address = userResult.rows[0].address;
} else {
// Создаем нового пользователя
const newUserResult = await db.query(
'INSERT INTO users (role) VALUES ($1) RETURNING id',
['user']
);
userId = newUserResult.rows[0].id;
isAdmin = false;
// Добавляем идентификатор кошелька
await db.query(
'INSERT INTO user_identities (user_id, provider, provider_id) VALUES ($1, $2, $3)',
[userId, 'wallet', address.toLowerCase()]
);
}
// Получаем или создаем пользователя
const userResult = await db.query(
`INSERT INTO users (address, created_at, updated_at)
VALUES ($1, NOW(), NOW())
ON CONFLICT (address) DO UPDATE
SET updated_at = NOW()
RETURNING id, role = 'admin' as is_admin`,
[address]
);
const userId = userResult.rows[0].id;
const isAdmin = false; // Будет обновлено в createSession
// Используем централизованный сервис
await authService.createSession(req, {
// Обновляем сессию
req.session.userId = userId;
req.session.authenticated = true;
req.session.authType = 'wallet';
req.session.isAdmin = isAdmin;
req.session.address = address.toLowerCase();
// Сохраняем сессию
await new Promise((resolve, reject) => {
req.session.save((err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
// Возвращаем успешный ответ
return res.json({
success: true,
userId,
address,
isAdmin,
authType: 'wallet',
guestId: req.session.guestId
});
res.json({
authenticated: true,
userId,
address,
isAdmin,
authType: 'wallet'
authenticated: true
});
} catch (error) {
console.error('Error:', error);
res.status(500).json({ error: 'Internal server error' });
console.error('Error in /verify:', error);
res.status(500).json({ success: false, error: 'Server error' });
}
});
@@ -488,54 +507,106 @@ router.post('/link-identity', async (req, res) => {
}
});
// Проверка аутентификации
router.get('/check', (req, res) => {
try {
console.log('Сессия при проверке:', req.session);
if (req.session && req.session.authenticated) {
return res.json({
authenticated: true,
userId: req.session.userId,
address: req.session.address,
isAdmin: req.session.isAdmin,
authType: req.session.authType || 'wallet'
});
// Проверка статуса аутентификации
router.get('/check', async (req, res) => {
console.log('Сессия при проверке:', req.session);
let telegramId = null;
if (req.session.userId && req.session.authType === 'telegram') {
// Проверяем, есть ли telegramId в сессии
if (req.session.telegramId) {
telegramId = req.session.telegramId;
console.log('Telegram ID from session:', telegramId);
// Проверяем, есть ли запись в базе данных
try {
const result = await db.query(
'SELECT provider_id FROM user_identities WHERE user_id = $1 AND provider = $2',
[req.session.userId, 'telegram']
);
console.log('Telegram ID query result:', result.rows);
if (result.rows.length === 0) {
// Если нет, добавляем запись
try {
await db.query(
'INSERT INTO user_identities (user_id, provider, provider_id, created_at) VALUES ($1, $2, $3, NOW()) ON CONFLICT (provider, provider_id) DO NOTHING',
[req.session.userId, 'telegram', telegramId]
);
console.log('Added Telegram ID to database:', telegramId);
} catch (error) {
console.error('Error adding Telegram ID to database:', error);
}
}
} catch (error) {
console.error('Error checking Telegram ID in database:', error);
}
} else {
return res.json({ authenticated: false });
// Если нет, ищем в базе данных
try {
const result = await db.query(
'SELECT provider_id FROM user_identities WHERE user_id = $1 AND provider = $2',
[req.session.userId, 'telegram']
);
console.log('Telegram ID query result:', result.rows);
if (result.rows.length > 0) {
telegramId = result.rows[0].provider_id;
console.log('Telegram ID from database:', telegramId);
// Сохраняем в сессию для будущих запросов
req.session.telegramId = telegramId;
} else {
// Если нет в базе данных, используем фиксированное значение
telegramId = 'Telegram User';
console.log('Using fixed Telegram ID:', telegramId);
// Сохраняем в сессию для будущих запросов
req.session.telegramId = telegramId;
// Добавляем запись в базу данных
try {
await db.query(
'INSERT INTO user_identities (user_id, provider, provider_id, created_at) VALUES ($1, $2, $3, NOW()) ON CONFLICT (provider, provider_id) DO NOTHING',
[req.session.userId, 'telegram', telegramId]
);
console.log('Added Telegram ID to database:', telegramId);
} catch (error) {
console.error('Error adding Telegram ID to database:', error);
}
}
} catch (error) {
console.error('Error fetching Telegram ID:', error);
}
}
} catch (error) {
console.error('Ошибка при проверке аутентификации:', error);
return res.status(500).json({ error: 'Internal server error' });
}
const response = {
authenticated: !!req.session.authenticated,
userId: req.session.userId,
isAdmin: !!req.session.isAdmin,
authType: req.session.authType,
address: req.session.address,
telegramId: telegramId
};
console.log('Auth check response:', response);
res.json(response);
});
// Выход из системы
router.post('/logout', async (req, res) => {
try {
// Сохраняем ID сессии до уничтожения
const sessionID = req.sessionID;
// Уничтожаем сессию
req.session.destroy();
// Удаляем сессию из базы данных
try {
const { pool } = require('../db');
await pool.query('DELETE FROM session WHERE sid = $1', [sessionID]);
console.log(`Сессия ${sessionID} удалена из базы данных`);
} catch (dbError) {
console.error('Ошибка при удалении сессии из базы данных:', dbError);
router.post('/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
console.error('Error destroying session:', err);
return res.status(500).json({ error: 'Failed to logout' });
}
// Очищаем куки
res.clearCookie('connect.sid');
res.json({ success: true });
} catch (error) {
console.error('Ошибка при выходе из системы:', error);
res.status(500).json({ error: 'Ошибка сервера' });
}
});
});
// Маршрут для авторизации через Telegram
@@ -626,12 +697,16 @@ async function checkTokenBalance(address) {
// Маршрут для верификации Telegram
router.post('/telegram/verify', async (req, res) => {
console.log('Telegram verification request body:', req.body);
const { code } = req.body;
try {
const telegramBot = require('../services/telegramBot');
const telegramBot = getBot();
const result = await telegramBot.verifyCode(code);
console.log('Telegram verification result:', result);
if (result.success) {
// Проверяем, что у нас есть telegramId
if (!result.telegramId) {
@@ -678,6 +753,18 @@ router.post('/telegram/verify', async (req, res) => {
});
});
console.log('Telegram ID saved in session:', req.session.telegramId);
// Сохраняем идентификатор Telegram в базе данных
try {
await db.query(
'INSERT INTO user_identities (user_id, provider, provider_id, created_at) VALUES ($1, $2, $3, NOW()) ON CONFLICT (provider, provider_id) DO UPDATE SET user_id = $1',
[userId, 'telegram', result.telegramId]
);
} catch (error) {
console.error('Error saving Telegram ID to database:', error);
}
return res.json({
success: true,
userId: userId,
@@ -1130,4 +1217,64 @@ router.post('/clear-session', async (req, res) => {
}
});
// Инициализация Telegram аутентификации
router.post('/telegram/init', async (req, res) => {
try {
// Проверяем, есть ли уже привязанный Telegram
if (req.session?.userId) {
const existingTelegram = await db.query(
`SELECT provider_id
FROM user_identities
WHERE user_id = $1
AND provider = 'telegram'`,
[req.session.userId]
);
if (existingTelegram.rows.length > 0) {
return res.status(400).json({
error: 'Telegram already linked to this account'
});
}
}
const { verificationCode, botLink } = await initTelegramAuth(req.session);
res.json({ verificationCode, botLink });
} catch (error) {
console.error('Error initializing Telegram auth:', error);
res.status(500).json({ error: 'Failed to initialize Telegram auth' });
}
});
// Инициализация Email аутентификации
router.post('/email/init', async (req, res) => {
try {
const { email } = req.body;
if (!email) {
return res.status(400).json({ error: 'Email is required' });
}
await initEmailAuth(email);
res.json({ success: true });
} catch (error) {
console.error('Error initializing email auth:', error);
res.status(500).json({ error: 'Failed to send verification code' });
}
});
// Проверка кода подтверждения email
router.post('/email/verify', requireAuth, async (req, res) => {
try {
const { code } = req.body;
if (!code) {
return res.status(400).json({ error: 'Verification code is required' });
}
const result = await verifyEmailCode(code, req.session.userId);
res.json(result);
} catch (error) {
console.error('Error verifying email code:', error);
res.status(400).json({ error: error.message });
}
});
module.exports = router;

View File

@@ -10,7 +10,7 @@ const authRouter = require('./routes/auth');
const identitiesRouter = require('./routes/identities');
const { pool } = require('./db');
const helmet = require('helmet');
const TelegramBotService = require('./services/telegramBot');
const { getBot, stopBot } = require('./services/telegramBot');
const pgSession = require('connect-pg-simple')(session);
const authService = require('./services/auth-service');
const logger = require('./utils/logger');
@@ -25,13 +25,34 @@ console.log('Используемый порт:', process.env.PORT || 8000);
async function initServices() {
try {
console.log('Инициализация сервисов...');
if (process.env.TELEGRAM_BOT_TOKEN) {
const telegramBot = new TelegramBotService(process.env.TELEGRAM_BOT_TOKEN);
global.telegramBot = telegramBot; // Сохраняем экземпляр глобально
console.log('Telegram бот инициализирован');
// Останавливаем предыдущий экземпляр бота
await stopBot();
// Добавляем обработку ошибок при запуске бота
try {
await getBot(); // getBot теперь асинхронный и сам запускает бота
console.log('Telegram bot started');
// Добавляем graceful shutdown
process.once('SIGINT', async () => {
await stopBot();
process.exit(0);
});
process.once('SIGTERM', async () => {
await stopBot();
process.exit(0);
});
} catch (error) {
if (error.code === 409) {
logger.warn('Another instance of Telegram bot is running. This is normal during development with nodemon');
// Просто логируем ошибку и продолжаем работу
// Бот будет запущен при следующем перезапуске
} else {
logger.error('Error launching Telegram bot:', error);
}
}
console.log('Все сервисы успешно инициализированы');
} catch (error) {
console.error('Ошибка при инициализации сервисов:', error);

View File

@@ -44,39 +44,41 @@ class AuthService {
*/
async findOrCreateUser(address) {
try {
const existingUser = await db.query(
`SELECT u.id
FROM users u
JOIN user_identities ui ON u.id = ui.user_id
WHERE ui.provider = 'wallet'
AND LOWER(ui.provider_id) = LOWER($1)`,
[address]
);
if (existingUser.rows.length > 0) {
const userId = existingUser.rows[0].id;
const isAdmin = await this.checkAdminRole(address);
return { userId, isAdmin };
// Нормализуем адрес
address = ethers.getAddress(address);
// Ищем пользователя по адресу в таблице user_identities
const userResult = await db.query(`
SELECT u.* FROM users u
JOIN user_identities ui ON u.id = ui.user_id
WHERE ui.provider = 'wallet' AND ui.provider_id = $1
`, [address]);
if (userResult.rows.length > 0) {
const user = userResult.rows[0];
return {
userId: user.id,
isAdmin: user.role === 'admin'
};
}
// Создание нового пользователя
const result = await db.query(
'INSERT INTO users DEFAULT VALUES RETURNING id',
[]
// Если пользователь не найден, создаем нового
const newUserResult = await db.query(
'INSERT INTO users (role) VALUES ($1) RETURNING id',
['user']
);
const userId = result.rows[0].id;
const userId = newUserResult.rows[0].id;
// Добавляем идентификатор кошелька
await db.query(
`INSERT INTO user_identities
(user_id, provider, provider_id, identity_type, identity_value)
VALUES ($1, 'wallet', $2, 'wallet', $2)`,
[userId, address.toLowerCase()]
'INSERT INTO user_identities (user_id, provider, provider_id) VALUES ($1, $2, $3)',
[userId, 'wallet', address]
);
const isAdmin = await this.checkAdminRole(address);
return { userId, isAdmin };
return { userId, isAdmin: false };
} catch (error) {
console.error('Error in findOrCreateUser:', error);
console.error('Error finding or creating user:', error);
throw error;
}
}
@@ -143,41 +145,80 @@ class AuthService {
}
// Создание сессии с проверкой роли
async createSession(req, { userId, address, authType, guestId }) {
let isAdmin = false;
if (address) {
isAdmin = await this.checkAdminRole(address);
} else if (userId) {
const linkedWallet = await this.getLinkedWallet(userId);
if (linkedWallet) {
isAdmin = await this.checkAdminRole(linkedWallet);
async createSession(session, { userId, authenticated, authType, guestId, address }) {
try {
// Обновляем данные сессии
session.userId = userId;
session.authenticated = authenticated;
session.authType = authType;
session.guestId = guestId;
if (address) {
session.address = address;
}
// Сохраняем сессию в БД
const result = await db.query(
`UPDATE session
SET sess = $1
WHERE sid = $2`,
[JSON.stringify({
userId,
authenticated,
authType,
guestId,
address,
cookie: session.cookie
}), session.id]
);
return true;
} catch (error) {
logger.error('Error creating session:', error);
return false;
}
}
req.session.userId = userId;
req.session.address = address;
req.session.isAdmin = isAdmin;
req.session.authenticated = true;
req.session.authType = authType;
if (guestId) {
req.session.guestId = guestId;
async getSession(sessionId) {
try {
const result = await db.query('SELECT * FROM session WHERE sid = $1', [sessionId]);
return result.rows[0];
} catch (error) {
console.error('Error getting session:', error);
throw error;
}
await req.session.save();
}
// Получение связанного кошелька
async getLinkedWallet(userId) {
const result = await db.query(
`SELECT identity_value as address
`SELECT provider_id as address
FROM user_identities
WHERE user_id = $1 AND identity_type = 'wallet'`,
WHERE user_id = $1 AND provider = 'wallet'`,
[userId]
);
return result.rows[0]?.address;
}
/**
* Проверяет роль пользователя Telegram
* @param {number} userId - ID пользователя
* @returns {Promise<string>} - Роль пользователя
*/
async checkUserRole(userId) {
try {
// Проверяем наличие связанного кошелька
const wallet = await this.getLinkedWallet(userId);
if (wallet) {
// Если есть кошелек, проверяем админские токены
const isAdmin = await this.checkAdminRole(wallet);
return isAdmin ? 'admin' : 'user';
}
return 'user';
} catch (error) {
logger.error('Error checking user role:', error);
return 'user';
}
}
}
// Создаем и экспортируем единственный экземпляр

View File

@@ -5,6 +5,7 @@ const simpleParser = require('mailparser').simpleParser;
const { processMessage } = require('./ai-assistant');
const { inspect } = require('util');
const logger = require('../utils/logger');
const { generateVerificationCode, addUserIdentity } = require('../utils/helpers');
// Хранилище кодов подтверждения
const verificationCodes = new Map(); // { email: { code, token, expires } }
@@ -334,6 +335,79 @@ class EmailBotService {
}
}
// Инициализация процесса аутентификации по email
async function initEmailAuth(email) {
const code = generateVerificationCode();
// Сохраняем код на 15 минут
verificationCodes.set(code, {
email,
timestamp: Date.now(),
verified: false
});
// Отправляем код на email
try {
await transporter.sendMail({
from: process.env.SMTP_FROM,
to: email,
subject: 'Код подтверждения для HB3 Accelerator',
text: `Ваш код подтверждения: ${code}\n\nВведите его на сайте для завершения аутентификации.`,
html: `
<h2>Код подтверждения для HB3 Accelerator</h2>
<p>Ваш код подтверждения: <strong>${code}</strong></p>
<p>Введите его на сайте для завершения аутентификации.</p>
`
});
logger.info(`Verification code sent to email: ${email}`);
return { success: true };
} catch (error) {
logger.error('Error sending verification email:', error);
throw new Error('Failed to send verification email');
}
}
// Проверка кода подтверждения
async function verifyEmailCode(code, userId) {
const verification = verificationCodes.get(code);
if (!verification) {
logger.warn(`Invalid verification code attempt: ${code}`);
throw new Error('Неверный код');
}
if (Date.now() - verification.timestamp > 15 * 60 * 1000) {
verificationCodes.delete(code);
logger.warn(`Expired verification code: ${code}`);
throw new Error('Код устарел');
}
try {
// Сохраняем связь пользователя с email
const success = await addUserIdentity(
userId,
'email',
verification.email
);
if (success) {
verificationCodes.delete(code);
logger.info(`User ${userId} successfully linked email ${verification.email}`);
return { success: true };
} else {
throw new Error('Этот email уже привязан к другому пользователю');
}
} catch (error) {
logger.error('Error saving email identity:', error);
throw error;
}
}
// Экспортируем класс и хранилище кодов
module.exports = EmailBotService;
module.exports.verificationCodes = verificationCodes;
module.exports = {
EmailBotService,
verificationCodes,
initEmailAuth,
verifyEmailCode
};

View File

@@ -1,144 +1,183 @@
const TelegramBot = require('node-telegram-bot-api');
const { Telegraf } = require('telegraf');
const logger = require('../utils/logger');
const db = require('../db');
const authService = require('./auth-service');
class TelegramBotService {
constructor(token) {
this.bot = new TelegramBot(token, {
polling: true,
request: {
timeout: 30000 // 30 секунд таймаут
}
});
this.verificationCodes = new Map();
this.setupHandlers();
logger.info('TelegramBot service initialized');
}
let botInstance = null;
const verificationCodes = new Map();
setupHandlers() {
this.bot.on('message', this.handleMessage.bind(this));
this.bot.on('callback_query', this.handleCallbackQuery.bind(this));
// Обработка ошибок
this.bot.on('polling_error', (error) => {
logger.error('Telegram polling error:', error);
});
this.bot.on('error', (error) => {
logger.error('Telegram bot error:', error);
});
}
async handleMessage(msg) {
try {
const chatId = msg.chat.id;
const text = msg.text;
logger.info(`Received message from ${chatId}: ${text}`);
if (text.startsWith('/start')) {
await this.handleStart(msg);
} else if (this.verificationCodes.has(chatId)) {
await this.handleVerificationCode(msg);
}
} catch (error) {
logger.error('Error handling message:', error);
}
}
async handleCallbackQuery(query) {
try {
const chatId = query.message.chat.id;
await this.bot.answerCallbackQuery(query.id);
logger.info(`Handled callback query from ${chatId}`);
} catch (error) {
logger.error('Error handling callback query:', error);
}
}
async handleStart(msg) {
const chatId = msg.chat.id;
try {
await this.bot.sendMessage(
chatId,
'Добро пожаловать! Используйте этого бота для аутентификации в приложении.'
);
logger.info(`Sent welcome message to ${chatId}`);
} catch (error) {
logger.error(`Error sending welcome message to ${chatId}:`, error);
}
}
async handleVerificationCode(msg) {
const chatId = msg.chat.id;
const code = msg.text.trim();
try {
const verificationData = this.verificationCodes.get(chatId);
if (!verificationData) {
await this.bot.sendMessage(chatId, 'Нет активного кода подтверждения.');
return;
}
if (Date.now() > verificationData.expires) {
this.verificationCodes.delete(chatId);
await this.bot.sendMessage(chatId, 'Код подтверждения истек. Запросите новый.');
return;
}
if (verificationData.code === code) {
await this.bot.sendMessage(chatId, 'Код подтвержден успешно!');
this.verificationCodes.delete(chatId);
} else {
await this.bot.sendMessage(chatId, 'Неверный код. Попробуйте еще раз.');
}
} catch (error) {
logger.error(`Error handling verification code for ${chatId}:`, error);
}
}
async sendVerificationCode(chatId, code) {
try {
// Сохраняем код с временем истечения (15 минут)
this.verificationCodes.set(chatId, {
code,
expires: Date.now() + 15 * 60 * 1000
});
await this.bot.sendMessage(
chatId,
`Ваш код подтверждения: ${code}\nВведите его в приложении.`
);
logger.info(`Sent verification code to ${chatId}`);
return true;
} catch (error) {
logger.error(`Error sending verification code to ${chatId}:`, error);
return false;
}
}
async verifyCode(code) {
try {
for (const [chatId, data] of this.verificationCodes.entries()) {
if (data.code === code) {
if (Date.now() > data.expires) {
this.verificationCodes.delete(chatId);
return { success: false, error: 'Код истек' };
}
this.verificationCodes.delete(chatId);
return { success: true, telegramId: chatId.toString() };
}
}
return { success: false, error: 'Неверный код' };
} catch (error) {
logger.error('Error verifying code:', error);
return { success: false, error: 'Внутренняя ошибка' };
}
// Простая остановка бота
async function stopBot() {
if (botInstance) {
await botInstance.stop();
botInstance = null;
}
}
module.exports = TelegramBotService;
// Создание и настройка бота
async function getBot() {
if (!botInstance) {
botInstance = new Telegraf(process.env.TELEGRAM_BOT_TOKEN);
// Обработка команды /start
botInstance.command('start', (ctx) => {
ctx.reply('Добро пожаловать! Отправьте код подтверждения для аутентификации.');
});
// Обработка кодов верификации
botInstance.on('text', async (ctx) => {
const code = ctx.message.text.trim();
const verification = verificationCodes.get(code);
if (!verification) {
ctx.reply('Неверный код подтверждения');
return;
}
try {
logger.info('Starting Telegram auth process for code:', code);
logger.info('Verification data:', verification);
// Сначала проверяем, существует ли пользователь с этим Telegram ID
let userId;
const existingUser = await db.query(
`SELECT u.id
FROM users u
JOIN user_identities ui ON u.id = ui.user_id
WHERE ui.provider = $1
AND ui.provider_id = $2`,
['telegram', ctx.from.id.toString()]
);
if (existingUser.rows.length > 0) {
userId = existingUser.rows[0].id;
logger.info('Found existing user with ID:', userId);
} else {
// Создаем нового пользователя
const result = await db.query(
`INSERT INTO users (created_at, updated_at)
VALUES (NOW(), NOW())
RETURNING id`,
[]
);
userId = result.rows[0].id;
logger.info('Created new user with ID:', userId);
}
// Связываем Telegram с пользователем
await db.query(
`INSERT INTO user_identities
(user_id, provider, provider_id, created_at)
VALUES ($1, $2, $3, NOW())
ON CONFLICT (provider, provider_id) DO UPDATE SET user_id = $1`,
[userId, 'telegram', ctx.from.id.toString()]
);
logger.info(`User ${userId} successfully linked Telegram account ${ctx.from.id}`);
// Обновляем сессию
if (verification?.session) {
logger.info('Creating session with data:', verification.session);
// Обновляем данные сессии напрямую
verification.session.userId = userId;
verification.session.authenticated = true;
verification.session.authType = 'telegram';
verification.session.telegramId = ctx.from.id.toString(); // Добавляем идентификатор Telegram в сессию
// Проверяем роль пользователя
const userRole = await authService.checkUserRole(userId);
verification.session.userRole = userRole;
await new Promise((resolve, reject) => {
verification.session.save(err => {
if (err) reject(err);
else resolve();
});
});
logger.info('Session created successfully');
}
// Отправляем последнее сообщение пользователя
if (verification.session.guestId) {
logger.info('Fetching last guest message for guestId:', verification.session.guestId);
const messageResult = await db.query(`
SELECT content FROM guest_messages
WHERE guest_id = $1
ORDER BY created_at DESC
LIMIT 1
`, [verification.session.guestId]);
const lastMessage = messageResult.rows[0]?.content;
logger.info('Found last message:', lastMessage);
if (lastMessage) {
await ctx.reply(`Ваше последнее сообщение: "${lastMessage}"`);
}
}
// Отправляем сообщение об успешной аутентификации
await ctx.reply('Аутентификация успешна! Можете вернуться в приложение.');
// Удаляем сообщение с кодом
try {
await ctx.deleteMessage(ctx.message.message_id);
} catch (error) {
logger.warn('Could not delete code message:', error);
}
// Удаляем код верификации
verificationCodes.delete(code);
} catch (error) {
logger.error('Error in Telegram auth:', error);
// Более информативные сообщения об ошибках
let errorMessage = 'Произошла ошибка при сохранении. Попробуйте позже.';
if (error.code === '42P01') {
errorMessage = 'Ошибка сессии. Пожалуйста, обновите страницу и попробуйте снова.';
} else if (error.code === '42703') {
errorMessage = 'Ошибка структуры данных. Обратитесь к администратору.';
}
if (error.code) {
logger.error('Database error code:', error.code);
}
if (error.detail) {
logger.error('Error detail:', error.detail);
}
if (error.stack) {
logger.error('Error stack:', error.stack);
}
await ctx.reply(errorMessage);
}
});
// Запускаем бота
await botInstance.launch();
}
return botInstance;
}
// Инициализация процесса аутентификации
async function initTelegramAuth(session) {
const code = Math.random().toString(36).substring(2, 8).toUpperCase();
const botLink = `https://t.me/${process.env.TELEGRAM_BOT_USERNAME}`;
verificationCodes.set(code, {
timestamp: Date.now(),
session: session
});
logger.info(`Generated verification code: ${code} for Telegram auth`);
return { verificationCode: code, botLink };
}
module.exports = {
getBot,
stopBot,
verificationCodes,
initTelegramAuth
};

View File

@@ -1,3 +1,5 @@
const db = require('../db');
// Функция для создания задержки
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
@@ -9,7 +11,43 @@ function isValidEmail(email) {
return emailRegex.test(email);
}
// Генерация кода подтверждения
function generateVerificationCode(length = 6) {
return Math.random()
.toString(36)
.substring(2, 2 + length)
.toUpperCase();
}
// Проверка существования идентификатора пользователя
async function checkUserIdentity(userId, provider, providerId) {
const result = await db.query(
'SELECT * FROM user_identities WHERE user_id = $1 AND provider = $2 AND provider_id = $3',
[userId, provider, providerId]
);
return result.rows.length > 0;
}
// Добавление новой идентификации
async function addUserIdentity(userId, provider, providerId) {
try {
await db.query(
'INSERT INTO user_identities (user_id, provider, provider_id) VALUES ($1, $2, $3)',
[userId, provider, providerId]
);
return true;
} catch (error) {
if (error.code === '23505') { // Уникальное ограничение нарушено
return false;
}
throw error;
}
}
module.exports = {
sleep,
isValidEmail,
generateVerificationCode,
checkUserIdentity,
addUserIdentity
};

View File

@@ -1079,6 +1079,11 @@
resolved "https://registry.npmjs.org/@stablelib/wipe/-/wipe-1.0.1.tgz"
integrity sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg==
"@telegraf/types@^7.1.0":
version "7.1.0"
resolved "https://registry.yarnpkg.com/@telegraf/types/-/types-7.1.0.tgz#d8bd9b2f5070b4de46971416e890338cd89fc23d"
integrity sha512-kGevOIbpMcIlCDeorKGpwZmdH7kHbqlk/Yj6dEpJMKEQw5lk0KVQY0OLXaCswy8GqlIVLd5625OB+rAntP9xVw==
"@tsconfig/node10@^1.0.7":
version "1.0.11"
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2"
@@ -1806,6 +1811,24 @@ bs58check@^2.1.2:
create-hash "^1.1.0"
safe-buffer "^5.1.2"
buffer-alloc-unsafe@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0"
integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==
buffer-alloc@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec"
integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==
dependencies:
buffer-alloc-unsafe "^1.1.0"
buffer-fill "^1.0.0"
buffer-fill@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
integrity sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==
buffer-from@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
@@ -2288,7 +2311,7 @@ debug@2.6.9:
dependencies:
ms "2.0.0"
debug@4, debug@^4, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.5:
debug@4, debug@^4, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5:
version "4.4.0"
resolved "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz"
integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
@@ -4678,6 +4701,11 @@ mocha@^10.0.0, mocha@^10.2.0:
yargs-parser "^20.2.9"
yargs-unparser "^2.0.0"
mri@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz"
@@ -4753,7 +4781,7 @@ node-emoji@^1.10.0:
dependencies:
lodash "^4.17.21"
node-fetch@^2.6.7:
node-fetch@^2.6.7, node-fetch@^2.7.0:
version "2.7.0"
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz"
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
@@ -5011,6 +5039,11 @@ p-timeout@^3.2.0:
dependencies:
p-finally "^1.0.0"
p-timeout@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-4.1.0.tgz#788253c0452ab0ffecf18a62dff94ff1bd09ca0a"
integrity sha512-+/wmHtzJuWii1sXn3HCuH/FTwGhrp4tmJTxSKJbfS+vkipci6osxXM5mY0jUiRzWKMTgUT8l7HFbeSwZAynqHw==
parent-module@^1.0.0:
version "1.0.1"
resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz"
@@ -5555,6 +5588,13 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1:
resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
safe-compare@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/safe-compare/-/safe-compare-1.1.4.tgz#5e0128538a82820e2e9250cd78e45da6786ba593"
integrity sha512-b9wZ986HHCo/HbKrRpBJb2kqXMK9CEWIE1egeEvZsYn69ay3kdfl9nG3RyOcR+jInTDf7a86WQ1d4VJX7goSSQ==
dependencies:
buffer-alloc "^1.2.0"
safe-push-apply@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz"
@@ -5582,6 +5622,11 @@ safe-stable-stringify@^2.3.1:
resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
sandwich-stream@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/sandwich-stream/-/sandwich-stream-2.0.2.tgz#6d1feb6cf7e9fe9fadb41513459a72c2e84000fa"
integrity sha512-jLYV0DORrzY3xaz/S9ydJL6Iz7essZeAfnAavsJ+zsJGZ1MOnsS52yRjU3uF3pJa/lla7+wisp//fxOwOH8SKQ==
sc-istanbul@^0.4.5:
version "0.4.6"
resolved "https://registry.yarnpkg.com/sc-istanbul/-/sc-istanbul-0.4.6.tgz#cf6784355ff2076f92d70d59047d71c13703e839"
@@ -6153,6 +6198,20 @@ table@^6.8.0:
string-width "^4.2.3"
strip-ansi "^6.0.1"
telegraf@^4.16.3:
version "4.16.3"
resolved "https://registry.yarnpkg.com/telegraf/-/telegraf-4.16.3.tgz#f03fa30482b540a7f9895af8f13ec8f432840a66"
integrity sha512-yjEu2NwkHlXu0OARWoNhJlIjX09dRktiMQFsM678BAH/PEPVwctzL67+tvXqLCRQQvm3SDtki2saGO9hLlz68w==
dependencies:
"@telegraf/types" "^7.1.0"
abort-controller "^3.0.0"
debug "^4.3.4"
mri "^1.2.0"
node-fetch "^2.7.0"
p-timeout "^4.1.0"
safe-compare "^1.1.4"
sandwich-stream "^2.0.2"
text-hex@1.0.x:
version "1.0.0"
resolved "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz"