ваше сообщение коммита
This commit is contained in:
2
backend/db/migrations/025_add_direction_to_messages.sql
Normal file
2
backend/db/migrations/025_add_direction_to_messages.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- Миграция: добавление поля direction в таблицу messages
|
||||
ALTER TABLE messages ADD COLUMN IF NOT EXISTS direction VARCHAR(8);
|
||||
@@ -6,9 +6,9 @@ const { requireAuth } = require('../middleware/auth');
|
||||
// const userService = require('../services/userService');
|
||||
|
||||
// Получение списка пользователей
|
||||
router.get('/', (req, res) => {
|
||||
res.json({ message: 'Users API endpoint' });
|
||||
});
|
||||
// router.get('/', (req, res) => {
|
||||
// res.json({ message: 'Users API endpoint' });
|
||||
// });
|
||||
|
||||
// Получение информации о пользователе
|
||||
router.get('/:address', (req, res) => {
|
||||
@@ -92,6 +92,36 @@ router.put('/profile', requireAuth, async (req, res) => {
|
||||
});
|
||||
*/
|
||||
|
||||
// Получение списка пользователей с контактами
|
||||
router.get('/', async (req, res, next) => {
|
||||
try {
|
||||
const usersResult = await db.getQuery()('SELECT id, first_name, last_name, created_at FROM users ORDER BY id');
|
||||
const users = usersResult.rows;
|
||||
// Получаем все user_identities разом
|
||||
const identitiesResult = await db.getQuery()('SELECT user_id, provider, provider_id FROM user_identities');
|
||||
const identities = identitiesResult.rows;
|
||||
// Группируем идентификаторы по user_id
|
||||
const identityMap = {};
|
||||
for (const id of identities) {
|
||||
if (!identityMap[id.user_id]) identityMap[id.user_id] = {};
|
||||
if (!identityMap[id.user_id][id.provider]) identityMap[id.user_id][id.provider] = id.provider_id;
|
||||
}
|
||||
// Собираем контакты
|
||||
const contacts = users.map(u => ({
|
||||
id: u.id,
|
||||
name: [u.first_name, u.last_name].filter(Boolean).join(' ') || null,
|
||||
email: identityMap[u.id]?.email || null,
|
||||
telegram: identityMap[u.id]?.telegram || null,
|
||||
wallet: identityMap[u.id]?.wallet || null,
|
||||
created_at: u.created_at
|
||||
}));
|
||||
res.json({ success: true, contacts });
|
||||
} catch (error) {
|
||||
logger.error('Error fetching contacts:', error);
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/users - Получить список всех пользователей (пример, может требовать прав администратора)
|
||||
// В текущей реализации этот маршрут не используется и закомментирован
|
||||
/*
|
||||
|
||||
@@ -2,7 +2,6 @@ require('dotenv').config();
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const { ethers } = require('ethers');
|
||||
const emailBot = require('./services/emailBot');
|
||||
const session = require('express-session');
|
||||
const { app, nonceStore } = require('./app');
|
||||
const usersRouter = require('./routes/users');
|
||||
@@ -15,6 +14,7 @@ const { getBot, stopBot } = require('./services/telegramBot');
|
||||
const pgSession = require('connect-pg-simple')(session);
|
||||
const authService = require('./services/auth-service');
|
||||
const logger = require('./utils/logger');
|
||||
const EmailBotService = require('./services/emailBot.js');
|
||||
|
||||
const PORT = process.env.PORT || 8000;
|
||||
|
||||
@@ -28,12 +28,20 @@ async function initServices() {
|
||||
console.log('Инициализация сервисов...');
|
||||
|
||||
// Останавливаем предыдущий экземпляр бота
|
||||
console.log('Перед stopBot');
|
||||
await stopBot();
|
||||
console.log('После stopBot, перед getBot');
|
||||
getBot();
|
||||
console.log('После getBot, перед созданием EmailBotService');
|
||||
|
||||
// Добавляем обработку ошибок при запуске бота
|
||||
try {
|
||||
await getBot(); // getBot теперь асинхронный и сам запускает бота
|
||||
console.log('Telegram bot started');
|
||||
console.log('Пробуем создать экземпляр EmailBotService');
|
||||
|
||||
// Запуск email-бота
|
||||
console.log('Создаём экземпляр EmailBotService');
|
||||
// const emailBot = new EmailBotService();
|
||||
// await emailBot.start();
|
||||
|
||||
// Добавляем graceful shutdown
|
||||
process.once('SIGINT', async () => {
|
||||
@@ -53,6 +61,7 @@ async function initServices() {
|
||||
// Бот будет запущен при следующем перезапуске
|
||||
} else {
|
||||
logger.error('Error launching Telegram bot:', error);
|
||||
console.error('Ошибка при запуске Telegram-бота:', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,12 +100,13 @@ app.get('/api/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// Запуск сервера
|
||||
const host = app.get('host');
|
||||
app.listen(PORT, host, async () => {
|
||||
// Для отладки
|
||||
// const host = app.get('host');
|
||||
// console.log('host:', host);
|
||||
app.listen(PORT, async () => {
|
||||
try {
|
||||
await initServices();
|
||||
console.log(`Server is running on http://${host}:${PORT}`);
|
||||
console.log(`Server is running on port ${PORT}`);
|
||||
} catch (error) {
|
||||
console.error('Error starting server:', error);
|
||||
process.exit(1);
|
||||
|
||||
@@ -7,6 +7,7 @@ const verificationService = require('./verification-service'); // Использ
|
||||
const identityService = require('./identity-service'); // <-- ДОБАВЛЕН ИМПОРТ
|
||||
const authTokenService = require('./authTokenService');
|
||||
const rpcProviderService = require('./rpcProviderService');
|
||||
const { getLinkedWallet } = require('./wallet-service');
|
||||
|
||||
const ERC20_ABI = ['function balanceOf(address owner) view returns (uint256)'];
|
||||
|
||||
@@ -359,26 +360,6 @@ class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
// Получение связанного кошелька
|
||||
async getLinkedWallet(userId) {
|
||||
logger.info(`[getLinkedWallet] Called with userId: ${userId} (Type: ${typeof userId})`);
|
||||
try {
|
||||
const result = await db.getQuery()(
|
||||
`SELECT provider_id as address
|
||||
FROM user_identities
|
||||
WHERE user_id = $1 AND provider = 'wallet'`,
|
||||
[userId]
|
||||
);
|
||||
logger.info(`[getLinkedWallet] DB query result for userId ${userId}:`, result.rows);
|
||||
const address = result.rows[0]?.address;
|
||||
logger.info(`[getLinkedWallet] Returning address: ${address} for userId ${userId}`);
|
||||
return address;
|
||||
} catch (error) {
|
||||
logger.error(`[getLinkedWallet] Error fetching linked wallet for userId ${userId}:`, error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет роль пользователя Telegram
|
||||
* @param {number} userId - ID пользователя
|
||||
@@ -387,7 +368,7 @@ class AuthService {
|
||||
async checkUserRole(userId) {
|
||||
try {
|
||||
// Проверяем наличие связанного кошелька
|
||||
const wallet = await this.getLinkedWallet(userId);
|
||||
const wallet = await getLinkedWallet(userId);
|
||||
|
||||
// Если кошелек не привязан, пользователь получает роль user
|
||||
// с базовым доступом к чату и истории сообщений
|
||||
@@ -429,7 +410,7 @@ class AuthService {
|
||||
}
|
||||
|
||||
// Проверяем наличие кошелька и определяем роль
|
||||
const wallet = await this.getLinkedWallet(userId);
|
||||
const wallet = await getLinkedWallet(userId);
|
||||
let role = 'user'; // Базовая роль для доступа к чату
|
||||
|
||||
if (wallet) {
|
||||
@@ -814,7 +795,7 @@ class AuthService {
|
||||
|
||||
// 4. Проверить роль на основе привязанного кошелька
|
||||
try {
|
||||
const linkedWallet = await this.getLinkedWallet(userId);
|
||||
const linkedWallet = await getLinkedWallet(userId);
|
||||
if (linkedWallet && linkedWallet.provider_id) {
|
||||
logger.info(`[handleEmailVerification] Found linked wallet ${linkedWallet.provider_id}. Checking role...`);
|
||||
const isAdmin = await this.checkAdminRole(linkedWallet.provider_id);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const { pool } = require('../db');
|
||||
const verificationService = require('./verification-service');
|
||||
const logger = require('../utils/logger');
|
||||
const EmailBotService = require('./emailBot');
|
||||
const EmailBotService = require('./emailBot.js');
|
||||
const db = require('../db');
|
||||
const authService = require('./auth-service');
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ const simpleParser = require('mailparser').simpleParser;
|
||||
const { processMessage } = require('./ai-assistant');
|
||||
const { inspect } = require('util');
|
||||
const logger = require('../utils/logger');
|
||||
const identityService = require('./identity-service');
|
||||
const aiAssistant = require('./ai-assistant');
|
||||
|
||||
class EmailBotService {
|
||||
async getSettingsFromDb() {
|
||||
@@ -124,6 +126,49 @@ class EmailBotService {
|
||||
logger.error(`Error parsing message: ${err}`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const fromEmail = parsed.from?.value?.[0]?.address;
|
||||
const subject = parsed.subject || '';
|
||||
const text = parsed.text || '';
|
||||
const html = parsed.html || '';
|
||||
// 1. Найти или создать пользователя
|
||||
const { userId, role } = await identityService.findOrCreateUserWithRole('email', fromEmail);
|
||||
// 2. Сохранить письмо и вложения в messages
|
||||
let hasAttachments = parsed.attachments && parsed.attachments.length > 0;
|
||||
if (hasAttachments) {
|
||||
for (const att of parsed.attachments) {
|
||||
await db.getQuery()(
|
||||
`INSERT INTO messages (user_id, sender_type, content, channel, role, direction, created_at, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, NOW(), $7, $8, $9, $10, $11)`,
|
||||
[userId, 'user', text, 'email', role, 'in',
|
||||
att.filename,
|
||||
att.contentType,
|
||||
att.size,
|
||||
att.content,
|
||||
JSON.stringify({ subject, html })
|
||||
]
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await db.getQuery()(
|
||||
`INSERT INTO messages (user_id, sender_type, content, channel, role, direction, created_at, metadata)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, NOW(), $7)`,
|
||||
[userId, 'user', text, 'email', role, 'in', JSON.stringify({ subject, html })]
|
||||
);
|
||||
}
|
||||
// 3. Получить ответ от ИИ
|
||||
const aiResponse = await aiAssistant.getResponse(text, 'auto');
|
||||
// 4. Сохранить ответ в БД
|
||||
await db.getQuery()(
|
||||
`INSERT INTO messages (user_id, sender_type, content, channel, role, direction, created_at, metadata)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, NOW(), $7)`,
|
||||
[userId, 'assistant', aiResponse, 'email', role, 'out', JSON.stringify({ subject, html })]
|
||||
);
|
||||
// 5. Отправить ответ на email
|
||||
await this.sendEmail(fromEmail, 'Re: ' + subject, aiResponse);
|
||||
} catch (processErr) {
|
||||
logger.error('Error processing incoming email:', processErr);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -181,6 +226,45 @@ class EmailBotService {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async start() {
|
||||
logger.info('[EmailBot] start() called');
|
||||
const imapConfig = await this.getImapConfig();
|
||||
// Логируем IMAP-конфиг (без пароля)
|
||||
const safeConfig = { ...imapConfig };
|
||||
if (safeConfig.password) safeConfig.password = '***';
|
||||
logger.info('[EmailBot] IMAP config:', safeConfig);
|
||||
let attempt = 0;
|
||||
const maxAttempts = 3;
|
||||
const tryConnect = () => {
|
||||
attempt++;
|
||||
logger.info(`[EmailBot] IMAP connect attempt ${attempt}`);
|
||||
this.imap = new Imap(imapConfig);
|
||||
this.imap.once('ready', () => {
|
||||
logger.info('[EmailBot] IMAP connection ready');
|
||||
this.imap.openBox('INBOX', false, (err, box) => {
|
||||
if (err) {
|
||||
logger.error(`[EmailBot] Error opening INBOX: ${err.message}`);
|
||||
this.imap.end();
|
||||
return;
|
||||
}
|
||||
logger.info('[EmailBot] INBOX opened successfully');
|
||||
});
|
||||
// После успешного подключения — обычная логика
|
||||
this.checkEmails();
|
||||
logger.info('[EmailBot] Email bot started and IMAP connection initiated');
|
||||
});
|
||||
this.imap.once('error', (err) => {
|
||||
logger.error(`[EmailBot] IMAP connection error: ${err.message}`);
|
||||
if (err.message && err.message.toLowerCase().includes('timed out') && attempt < maxAttempts) {
|
||||
logger.warn(`[EmailBot] IMAP reconnecting in 10 seconds (attempt ${attempt + 1})...`);
|
||||
setTimeout(tryConnect, 10000);
|
||||
}
|
||||
});
|
||||
this.imap.connect();
|
||||
};
|
||||
tryConnect();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = EmailBotService;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const db = require('../db');
|
||||
const logger = require('../utils/logger');
|
||||
const { getLinkedWallet } = require('./wallet-service');
|
||||
|
||||
/**
|
||||
* Сервис для работы с идентификаторами пользователей
|
||||
@@ -521,6 +522,38 @@ class IdentityService {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Универсальная функция: найти или создать пользователя по идентификатору, привязать идентификатор, проверить роль
|
||||
* @param {string} provider - Тип идентификатора ('email' | 'telegram')
|
||||
* @param {string} providerId - Значение идентификатора
|
||||
* @param {object} [options] - Дополнительные опции
|
||||
* @returns {Promise<{userId: number, role: string, isNew: boolean}>}
|
||||
*/
|
||||
async findOrCreateUserWithRole(provider, providerId, options = {}) {
|
||||
let user = await this.findUserByIdentity(provider, providerId);
|
||||
let isNew = false;
|
||||
if (!user) {
|
||||
// Создаем пользователя
|
||||
const newUserResult = await db.getQuery()('INSERT INTO users (role) VALUES ($1) RETURNING id', ['user']);
|
||||
const userId = newUserResult.rows[0].id;
|
||||
await this.saveIdentity(userId, provider, providerId, true);
|
||||
user = { id: userId, role: 'user' };
|
||||
isNew = true;
|
||||
}
|
||||
// Проверяем связь с кошельком
|
||||
const wallet = await getLinkedWallet(user.id);
|
||||
let role = 'user';
|
||||
if (wallet) {
|
||||
const isAdmin = await authService.checkAdminRole(wallet);
|
||||
role = isAdmin ? 'admin' : 'user';
|
||||
// Обновляем роль в users, если изменилась
|
||||
if (user.role !== role) {
|
||||
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', [role, user.id]);
|
||||
}
|
||||
}
|
||||
return { userId: user.id, role, isNew };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new IdentityService();
|
||||
|
||||
@@ -4,6 +4,8 @@ const db = require('../db');
|
||||
const authService = require('./auth-service');
|
||||
const verificationService = require('./verification-service');
|
||||
const crypto = require('crypto');
|
||||
const identityService = require('./identity-service');
|
||||
const aiAssistant = require('./ai-assistant');
|
||||
|
||||
let botInstance = null;
|
||||
let telegramSettingsCache = null;
|
||||
@@ -27,227 +29,305 @@ async function getBot() {
|
||||
ctx.reply('Добро пожаловать! Отправьте код подтверждения для аутентификации.');
|
||||
});
|
||||
|
||||
// Обработка кодов верификации
|
||||
// Универсальный обработчик текстовых сообщений
|
||||
botInstance.on('text', async (ctx) => {
|
||||
const code = ctx.message.text.trim();
|
||||
|
||||
try {
|
||||
// Получаем код верификации для всех активных кодов с провайдером telegram
|
||||
const codeResult = await db.getQuery()(
|
||||
`SELECT * FROM verification_codes
|
||||
WHERE code = $1
|
||||
AND provider = 'telegram'
|
||||
AND used = false
|
||||
AND expires_at > NOW()`,
|
||||
[code]
|
||||
);
|
||||
|
||||
if (codeResult.rows.length === 0) {
|
||||
ctx.reply('Неверный код подтверждения');
|
||||
return;
|
||||
}
|
||||
|
||||
const verification = codeResult.rows[0];
|
||||
const providerId = verification.provider_id;
|
||||
const linkedUserId = verification.user_id; // Получаем связанный userId если он есть
|
||||
let userId;
|
||||
let userRole = 'user'; // Роль по умолчанию
|
||||
|
||||
// Отмечаем код как использованный
|
||||
await db.getQuery()('UPDATE verification_codes SET used = true WHERE id = $1', [
|
||||
verification.id,
|
||||
]);
|
||||
|
||||
logger.info('Starting Telegram auth process for code:', code);
|
||||
|
||||
// Проверяем, существует ли уже пользователь с таким Telegram ID
|
||||
const existingTelegramUser = await db.getQuery()(
|
||||
`SELECT ui.user_id
|
||||
FROM user_identities ui
|
||||
WHERE ui.provider = 'telegram' AND ui.provider_id = $1`,
|
||||
[ctx.from.id.toString()]
|
||||
);
|
||||
|
||||
if (existingTelegramUser.rows.length > 0) {
|
||||
// Если пользователь с таким Telegram ID уже существует, используем его
|
||||
userId = existingTelegramUser.rows[0].user_id;
|
||||
logger.info(`Using existing user ${userId} for Telegram account ${ctx.from.id}`);
|
||||
} else {
|
||||
// Если код верификации был связан с существующим пользователем, используем его
|
||||
if (linkedUserId) {
|
||||
// Используем userId из кода верификации
|
||||
userId = linkedUserId;
|
||||
// Связываем Telegram с этим пользователем
|
||||
await db.getQuery()(
|
||||
`INSERT INTO user_identities
|
||||
(user_id, provider, provider_id, created_at)
|
||||
VALUES ($1, $2, $3, NOW())`,
|
||||
[userId, 'telegram', ctx.from.id.toString()]
|
||||
);
|
||||
logger.info(
|
||||
`Linked Telegram account ${ctx.from.id} to pre-authenticated user ${userId}`
|
||||
);
|
||||
} else {
|
||||
// Проверяем, есть ли пользователь, связанный с гостевым идентификатором
|
||||
let existingUserWithGuestId = null;
|
||||
if (providerId) {
|
||||
const guestUserResult = await db.getQuery()(
|
||||
`SELECT user_id FROM guest_user_mapping WHERE guest_id = $1`,
|
||||
[providerId]
|
||||
);
|
||||
if (guestUserResult.rows.length > 0) {
|
||||
existingUserWithGuestId = guestUserResult.rows[0].user_id;
|
||||
logger.info(
|
||||
`Found existing user ${existingUserWithGuestId} by guest ID ${providerId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (existingUserWithGuestId) {
|
||||
// Используем существующего пользователя и добавляем ему Telegram идентификатор
|
||||
userId = existingUserWithGuestId;
|
||||
await db.getQuery()(
|
||||
`INSERT INTO user_identities
|
||||
(user_id, provider, provider_id, created_at)
|
||||
VALUES ($1, $2, $3, NOW())`,
|
||||
[userId, 'telegram', ctx.from.id.toString()]
|
||||
);
|
||||
logger.info(`Linked Telegram account ${ctx.from.id} to existing user ${userId}`);
|
||||
} else {
|
||||
// Создаем нового пользователя, если не нашли существующего
|
||||
const userResult = await db.getQuery()(
|
||||
'INSERT INTO users (created_at, role) VALUES (NOW(), $1) RETURNING id',
|
||||
['user']
|
||||
);
|
||||
userId = userResult.rows[0].id;
|
||||
|
||||
// Связываем Telegram с новым пользователем
|
||||
await db.getQuery()(
|
||||
`INSERT INTO user_identities
|
||||
(user_id, provider, provider_id, created_at)
|
||||
VALUES ($1, $2, $3, NOW())`,
|
||||
[userId, 'telegram', ctx.from.id.toString()]
|
||||
);
|
||||
|
||||
// Если был гостевой ID, связываем его с новым пользователем
|
||||
if (providerId) {
|
||||
await db.getQuery()(
|
||||
`INSERT INTO guest_user_mapping
|
||||
(user_id, guest_id)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (guest_id) DO UPDATE SET user_id = $1`,
|
||||
[userId, providerId]
|
||||
);
|
||||
}
|
||||
|
||||
logger.info(`Created new user ${userId} with Telegram account ${ctx.from.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----> НАЧАЛО: Проверка роли на основе привязанного кошелька <----
|
||||
if (userId) { // Убедимся, что userId определен
|
||||
logger.info(`[TelegramBot] Checking linked wallet for determined userId: ${userId} (Type: ${typeof userId})`);
|
||||
try {
|
||||
const linkedWallet = await authService.getLinkedWallet(userId);
|
||||
if (linkedWallet) {
|
||||
logger.info(`[TelegramBot] Found linked wallet ${linkedWallet} for user ${userId}. Checking role...`);
|
||||
const isAdmin = await authService.checkAdminRole(linkedWallet);
|
||||
userRole = isAdmin ? 'admin' : 'user';
|
||||
logger.info(`[TelegramBot] Role for user ${userId} determined as: ${userRole}`);
|
||||
|
||||
// Опционально: Обновить роль в таблице users
|
||||
const currentUser = await db.getQuery()('SELECT role FROM users WHERE id = $1', [userId]);
|
||||
if (currentUser.rows.length > 0 && currentUser.rows[0].role !== userRole) {
|
||||
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', [userRole, userId]);
|
||||
logger.info(`[TelegramBot] Updated user role in DB to ${userRole}`);
|
||||
}
|
||||
} else {
|
||||
logger.info(`[TelegramBot] No linked wallet found for user ${userId}. Checking current DB role.`);
|
||||
// Если кошелька нет, берем текущую роль из базы
|
||||
const currentUser = await db.getQuery()('SELECT role FROM users WHERE id = $1', [userId]);
|
||||
if (currentUser.rows.length > 0) {
|
||||
userRole = currentUser.rows[0].role;
|
||||
}
|
||||
}
|
||||
} catch (roleCheckError) {
|
||||
logger.error(`[TelegramBot] Error checking admin role for user ${userId}:`, roleCheckError);
|
||||
// В случае ошибки берем роль из базы или оставляем 'user'
|
||||
try {
|
||||
const currentUser = await db.getQuery()('SELECT role FROM users WHERE id = $1', [userId]);
|
||||
if (currentUser.rows.length > 0) { userRole = currentUser.rows[0].role; }
|
||||
} catch (dbError) { /* ignore */ }
|
||||
}
|
||||
} else {
|
||||
logger.error('[TelegramBot] Cannot check role because userId is undefined!');
|
||||
}
|
||||
// ----> КОНЕЦ: Проверка роли <----
|
||||
|
||||
// Логируем userId перед обновлением сессии
|
||||
logger.info(`[telegramBot] Attempting to update session for userId: ${userId}`);
|
||||
|
||||
// Находим последнюю активную сессию для данного userId
|
||||
let activeSessionId = null;
|
||||
const text = ctx.message.text.trim();
|
||||
// 1. Если команда — пропустить
|
||||
if (text.startsWith('/')) return;
|
||||
// 2. Проверка: это потенциальный код?
|
||||
const isPotentialCode = (str) => /^[A-Z0-9]{6}$/i.test(str);
|
||||
if (isPotentialCode(text)) {
|
||||
try {
|
||||
// Ищем сессию, где есть userId и она не истекла (проверка expires_at)
|
||||
// Сортируем по expires_at DESC чтобы взять самую "свежую", если их несколько
|
||||
const sessionResult = await db.getQuery()(
|
||||
`SELECT sid FROM session
|
||||
WHERE sess ->> 'userId' = $1
|
||||
AND expire > NOW()
|
||||
ORDER BY expire DESC
|
||||
LIMIT 1`,
|
||||
[userId?.toString()] // Используем optional chaining и преобразуем в строку
|
||||
// Получаем код верификации для всех активных кодов с провайдером telegram
|
||||
const codeResult = await db.getQuery()(
|
||||
`SELECT * FROM verification_codes
|
||||
WHERE code = $1
|
||||
AND provider = 'telegram'
|
||||
AND used = false
|
||||
AND expires_at > NOW()`,
|
||||
[text.toUpperCase()]
|
||||
);
|
||||
|
||||
if (sessionResult.rows.length > 0) {
|
||||
activeSessionId = sessionResult.rows[0].sid;
|
||||
logger.info(`[telegramBot] Found active session ID ${activeSessionId} for user ${userId}`);
|
||||
|
||||
// Обновляем найденную сессию в базе данных, добавляя/перезаписывая данные Telegram
|
||||
const updateResult = await db.getQuery()(
|
||||
`UPDATE session
|
||||
SET sess = (sess::jsonb || $1::jsonb)::json
|
||||
WHERE sid = $2`,
|
||||
[
|
||||
JSON.stringify({
|
||||
// authenticated: true, // Не перезаписываем, т.к. сессия уже должна быть аутентифицирована
|
||||
authType: 'telegram', // Обновляем тип аутентификации
|
||||
telegramId: ctx.from.id.toString(),
|
||||
telegramUsername: ctx.from.username,
|
||||
telegramFirstName: ctx.from.first_name,
|
||||
role: userRole, // Записываем определенную роль
|
||||
// userId: userId?.toString() // userId уже должен быть в сессии
|
||||
}),
|
||||
activeSessionId // Обновляем по найденному session ID
|
||||
]
|
||||
if (codeResult.rows.length === 0) {
|
||||
ctx.reply('Неверный код подтверждения');
|
||||
return;
|
||||
}
|
||||
|
||||
const verification = codeResult.rows[0];
|
||||
const providerId = verification.provider_id;
|
||||
const linkedUserId = verification.user_id; // Получаем связанный userId если он есть
|
||||
let userId;
|
||||
let userRole = 'user'; // Роль по умолчанию
|
||||
|
||||
// Отмечаем код как использованный
|
||||
await db.getQuery()('UPDATE verification_codes SET used = true WHERE id = $1', [
|
||||
verification.id,
|
||||
]);
|
||||
|
||||
logger.info('Starting Telegram auth process for code:', text);
|
||||
|
||||
// Проверяем, существует ли уже пользователь с таким Telegram ID
|
||||
const existingTelegramUser = await db.getQuery()(
|
||||
`SELECT ui.user_id
|
||||
FROM user_identities ui
|
||||
WHERE ui.provider = 'telegram' AND ui.provider_id = $1`,
|
||||
[ctx.from.id.toString()]
|
||||
);
|
||||
|
||||
if (existingTelegramUser.rows.length > 0) {
|
||||
// Если пользователь с таким Telegram ID уже существует, используем его
|
||||
userId = existingTelegramUser.rows[0].user_id;
|
||||
logger.info(`Using existing user ${userId} for Telegram account ${ctx.from.id}`);
|
||||
} else {
|
||||
// Если код верификации был связан с существующим пользователем, используем его
|
||||
if (linkedUserId) {
|
||||
// Используем userId из кода верификации
|
||||
userId = linkedUserId;
|
||||
// Связываем Telegram с этим пользователем
|
||||
await db.getQuery()(
|
||||
`INSERT INTO user_identities
|
||||
(user_id, provider, provider_id, created_at)
|
||||
VALUES ($1, $2, $3, NOW())`,
|
||||
[userId, 'telegram', ctx.from.id.toString()]
|
||||
);
|
||||
logger.info(
|
||||
`Linked Telegram account ${ctx.from.id} to pre-authenticated user ${userId}`
|
||||
);
|
||||
} else {
|
||||
// Проверяем, есть ли пользователь, связанный с гостевым идентификатором
|
||||
let existingUserWithGuestId = null;
|
||||
if (providerId) {
|
||||
const guestUserResult = await db.getQuery()(
|
||||
`SELECT user_id FROM guest_user_mapping WHERE guest_id = $1`,
|
||||
[providerId]
|
||||
);
|
||||
if (guestUserResult.rows.length > 0) {
|
||||
existingUserWithGuestId = guestUserResult.rows[0].user_id;
|
||||
logger.info(
|
||||
`Found existing user ${existingUserWithGuestId} by guest ID ${providerId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (existingUserWithGuestId) {
|
||||
// Используем существующего пользователя и добавляем ему Telegram идентификатор
|
||||
userId = existingUserWithGuestId;
|
||||
await db.getQuery()(
|
||||
`INSERT INTO user_identities
|
||||
(user_id, provider, provider_id, created_at)
|
||||
VALUES ($1, $2, $3, NOW())`,
|
||||
[userId, 'telegram', ctx.from.id.toString()]
|
||||
);
|
||||
logger.info(`Linked Telegram account ${ctx.from.id} to existing user ${userId}`);
|
||||
} else {
|
||||
// Создаем нового пользователя, если не нашли существующего
|
||||
const userResult = await db.getQuery()(
|
||||
'INSERT INTO users (created_at, role) VALUES (NOW(), $1) RETURNING id',
|
||||
['user']
|
||||
);
|
||||
userId = userResult.rows[0].id;
|
||||
|
||||
// Связываем Telegram с новым пользователем
|
||||
await db.getQuery()(
|
||||
`INSERT INTO user_identities
|
||||
(user_id, provider, provider_id, created_at)
|
||||
VALUES ($1, $2, $3, NOW())`,
|
||||
[userId, 'telegram', ctx.from.id.toString()]
|
||||
);
|
||||
|
||||
// Если был гостевой ID, связываем его с новым пользователем
|
||||
if (providerId) {
|
||||
await db.getQuery()(
|
||||
`INSERT INTO guest_user_mapping
|
||||
(user_id, guest_id)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (guest_id) DO UPDATE SET user_id = $1`,
|
||||
[userId, providerId]
|
||||
);
|
||||
}
|
||||
|
||||
logger.info(`Created new user ${userId} with Telegram account ${ctx.from.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----> НАЧАЛО: Проверка роли на основе привязанного кошелька <----
|
||||
if (userId) { // Убедимся, что userId определен
|
||||
logger.info(`[TelegramBot] Checking linked wallet for determined userId: ${userId} (Type: ${typeof userId})`);
|
||||
try {
|
||||
const linkedWallet = await authService.getLinkedWallet(userId);
|
||||
if (linkedWallet) {
|
||||
logger.info(`[TelegramBot] Found linked wallet ${linkedWallet} for user ${userId}. Checking role...`);
|
||||
const isAdmin = await authService.checkAdminRole(linkedWallet);
|
||||
userRole = isAdmin ? 'admin' : 'user';
|
||||
logger.info(`[TelegramBot] Role for user ${userId} determined as: ${userRole}`);
|
||||
|
||||
// Опционально: Обновить роль в таблице users
|
||||
const currentUser = await db.getQuery()('SELECT role FROM users WHERE id = $1', [userId]);
|
||||
if (currentUser.rows.length > 0 && currentUser.rows[0].role !== userRole) {
|
||||
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', [userRole, userId]);
|
||||
logger.info(`[TelegramBot] Updated user role in DB to ${userRole}`);
|
||||
}
|
||||
} else {
|
||||
logger.info(`[TelegramBot] No linked wallet found for user ${userId}. Checking current DB role.`);
|
||||
// Если кошелька нет, берем текущую роль из базы
|
||||
const currentUser = await db.getQuery()('SELECT role FROM users WHERE id = $1', [userId]);
|
||||
if (currentUser.rows.length > 0) {
|
||||
userRole = currentUser.rows[0].role;
|
||||
}
|
||||
}
|
||||
} catch (roleCheckError) {
|
||||
logger.error(`[TelegramBot] Error checking admin role for user ${userId}:`, roleCheckError);
|
||||
// В случае ошибки берем роль из базы или оставляем 'user'
|
||||
try {
|
||||
const currentUser = await db.getQuery()('SELECT role FROM users WHERE id = $1', [userId]);
|
||||
if (currentUser.rows.length > 0) { userRole = currentUser.rows[0].role; }
|
||||
} catch (dbError) { /* ignore */ }
|
||||
}
|
||||
} else {
|
||||
logger.error('[TelegramBot] Cannot check role because userId is undefined!');
|
||||
}
|
||||
// ----> КОНЕЦ: Проверка роли <----
|
||||
|
||||
// Логируем userId перед обновлением сессии
|
||||
logger.info(`[telegramBot] Attempting to update session for userId: ${userId}`);
|
||||
|
||||
// Находим последнюю активную сессию для данного userId
|
||||
let activeSessionId = null;
|
||||
try {
|
||||
// Ищем сессию, где есть userId и она не истекла (проверка expires_at)
|
||||
// Сортируем по expires_at DESC чтобы взять самую "свежую", если их несколько
|
||||
const sessionResult = await db.getQuery()(
|
||||
`SELECT sid FROM session
|
||||
WHERE sess ->> 'userId' = $1
|
||||
AND expire > NOW()
|
||||
ORDER BY expire DESC
|
||||
LIMIT 1`,
|
||||
[userId?.toString()] // Используем optional chaining и преобразуем в строку
|
||||
);
|
||||
|
||||
if (updateResult.rowCount > 0) {
|
||||
logger.info(`[telegramBot] Session ${activeSessionId} updated successfully with Telegram data for user ${userId}`);
|
||||
if (sessionResult.rows.length > 0) {
|
||||
activeSessionId = sessionResult.rows[0].sid;
|
||||
logger.info(`[telegramBot] Found active session ID ${activeSessionId} for user ${userId}`);
|
||||
|
||||
// Обновляем найденную сессию в базе данных, добавляя/перезаписывая данные Telegram
|
||||
const updateResult = await db.getQuery()(
|
||||
`UPDATE session
|
||||
SET sess = (sess::jsonb || $1::jsonb)::json
|
||||
WHERE sid = $2`,
|
||||
[
|
||||
JSON.stringify({
|
||||
// authenticated: true, // Не перезаписываем, т.к. сессия уже должна быть аутентифицирована
|
||||
authType: 'telegram', // Обновляем тип аутентификации
|
||||
telegramId: ctx.from.id.toString(),
|
||||
telegramUsername: ctx.from.username,
|
||||
telegramFirstName: ctx.from.first_name,
|
||||
role: userRole, // Записываем определенную роль
|
||||
// userId: userId?.toString() // userId уже должен быть в сессии
|
||||
}),
|
||||
activeSessionId // Обновляем по найденному session ID
|
||||
]
|
||||
);
|
||||
|
||||
if (updateResult.rowCount > 0) {
|
||||
logger.info(`[telegramBot] Session ${activeSessionId} updated successfully with Telegram data for user ${userId}`);
|
||||
} else {
|
||||
logger.warn(`[telegramBot] Session update query executed but did not update rows for sid: ${activeSessionId}. This might indicate a concurrency issue or incorrect sid.`);
|
||||
}
|
||||
|
||||
} else {
|
||||
logger.warn(`[telegramBot] Session update query executed but did not update rows for sid: ${activeSessionId}. This might indicate a concurrency issue or incorrect sid.`);
|
||||
logger.warn(`[telegramBot] No active web session found for userId: ${userId}. Telegram is linked, but the user might need to refresh their browser session.`);
|
||||
}
|
||||
|
||||
} else {
|
||||
logger.warn(`[telegramBot] No active web session found for userId: ${userId}. Telegram is linked, but the user might need to refresh their browser session.`);
|
||||
} catch(sessionError) {
|
||||
logger.error(`[telegramBot] Error finding or updating session for userId ${userId}:`, sessionError);
|
||||
}
|
||||
} catch(sessionError) {
|
||||
logger.error(`[telegramBot] Error finding or updating session for userId ${userId}:`, sessionError);
|
||||
}
|
||||
|
||||
// Отправляем сообщение об успешной аутентификации
|
||||
await ctx.reply('Аутентификация успешна! Можете вернуться в приложение.');
|
||||
// Отправляем сообщение об успешной аутентификации
|
||||
await ctx.reply('Аутентификация успешна! Можете вернуться в приложение.');
|
||||
|
||||
// Удаляем сообщение с кодом
|
||||
try {
|
||||
await ctx.deleteMessage(ctx.message.message_id);
|
||||
// Удаляем сообщение с кодом
|
||||
try {
|
||||
await ctx.deleteMessage(ctx.message.message_id);
|
||||
} catch (error) {
|
||||
logger.warn('Could not delete code message:', error);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('Could not delete code message:', error);
|
||||
logger.error('Error in Telegram auth:', error);
|
||||
await ctx.reply('Произошла ошибка при аутентификации. Попробуйте позже.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 3. Всё остальное — чат с ИИ-ассистентом
|
||||
try {
|
||||
const telegramId = ctx.from.id.toString();
|
||||
// 1. Найти или создать пользователя
|
||||
const { userId, role } = await identityService.findOrCreateUserWithRole('telegram', telegramId);
|
||||
|
||||
// 2. Сохранить входящее сообщение в messages
|
||||
let content = text;
|
||||
let attachmentMeta = {};
|
||||
// Проверяем вложения (фото, документ, аудио, видео)
|
||||
let fileId, fileName, mimeType, fileSize, attachmentBuffer;
|
||||
if (ctx.message.document) {
|
||||
fileId = ctx.message.document.file_id;
|
||||
fileName = ctx.message.document.file_name;
|
||||
mimeType = ctx.message.document.mime_type;
|
||||
fileSize = ctx.message.document.file_size;
|
||||
} else if (ctx.message.photo && ctx.message.photo.length > 0) {
|
||||
// Берём самое большое фото
|
||||
const photo = ctx.message.photo[ctx.message.photo.length - 1];
|
||||
fileId = photo.file_id;
|
||||
fileName = 'photo.jpg';
|
||||
mimeType = 'image/jpeg';
|
||||
fileSize = photo.file_size;
|
||||
} else if (ctx.message.audio) {
|
||||
fileId = ctx.message.audio.file_id;
|
||||
fileName = ctx.message.audio.file_name || 'audio.ogg';
|
||||
mimeType = ctx.message.audio.mime_type || 'audio/ogg';
|
||||
fileSize = ctx.message.audio.file_size;
|
||||
} else if (ctx.message.video) {
|
||||
fileId = ctx.message.video.file_id;
|
||||
fileName = ctx.message.video.file_name || 'video.mp4';
|
||||
mimeType = ctx.message.video.mime_type || 'video/mp4';
|
||||
fileSize = ctx.message.video.file_size;
|
||||
}
|
||||
if (fileId) {
|
||||
// Скачиваем файл
|
||||
const fileLink = await ctx.telegram.getFileLink(fileId);
|
||||
const res = await fetch(fileLink.href);
|
||||
attachmentBuffer = await res.buffer();
|
||||
attachmentMeta = {
|
||||
attachment_filename: fileName,
|
||||
attachment_mimetype: mimeType,
|
||||
attachment_size: fileSize,
|
||||
attachment_data: attachmentBuffer
|
||||
};
|
||||
}
|
||||
// Сохраняем сообщение в БД
|
||||
await db.getQuery()(
|
||||
`INSERT INTO messages (user_id, sender_type, content, channel, role, direction, created_at, attachment_filename, attachment_mimetype, attachment_size, attachment_data)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, NOW(), $7, $8, $9, $10)`,
|
||||
[userId, 'user', content, 'telegram', role, 'in',
|
||||
attachmentMeta.attachment_filename || null,
|
||||
attachmentMeta.attachment_mimetype || null,
|
||||
attachmentMeta.attachment_size || null,
|
||||
attachmentMeta.attachment_data || null
|
||||
]
|
||||
);
|
||||
|
||||
// 3. Получить ответ от ИИ
|
||||
const aiResponse = await aiAssistant.getResponse(content, 'auto');
|
||||
// 4. Сохранить ответ в БД
|
||||
await db.getQuery()(
|
||||
`INSERT INTO messages (user_id, sender_type, content, channel, role, direction, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, NOW())`,
|
||||
[userId, 'assistant', aiResponse, 'telegram', role, 'out']
|
||||
);
|
||||
// 5. Отправить ответ пользователю
|
||||
await ctx.reply(aiResponse);
|
||||
} catch (error) {
|
||||
logger.error('Error in Telegram auth:', error);
|
||||
await ctx.reply('Произошла ошибка при аутентификации. Попробуйте позже.');
|
||||
logger.error('[TelegramBot] Ошибка при обработке сообщения:', error);
|
||||
await ctx.reply('Произошла ошибка при обработке вашего сообщения. Попробуйте позже.');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
24
backend/services/wallet-service.js
Normal file
24
backend/services/wallet-service.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const db = require('../db');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
// Получение связанного кошелька
|
||||
async function getLinkedWallet(userId) {
|
||||
logger.info(`[getLinkedWallet] Called with userId: ${userId} (Type: ${typeof userId})`);
|
||||
try {
|
||||
const result = await db.getQuery()(
|
||||
`SELECT provider_id as address
|
||||
FROM user_identities
|
||||
WHERE user_id = $1 AND provider = 'wallet'`,
|
||||
[userId]
|
||||
);
|
||||
logger.info(`[getLinkedWallet] DB query result for userId ${userId}:`, result.rows);
|
||||
const address = result.rows[0]?.address;
|
||||
logger.info(`[getLinkedWallet] Returning address: ${address} for userId ${userId}`);
|
||||
return address;
|
||||
} catch (error) {
|
||||
logger.error(`[getLinkedWallet] Error fetching linked wallet for userId ${userId}:`, error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { getLinkedWallet };
|
||||
Reference in New Issue
Block a user