diff --git a/backend/db/migrations/025_add_direction_to_messages.sql b/backend/db/migrations/025_add_direction_to_messages.sql new file mode 100644 index 0000000..972dd6d --- /dev/null +++ b/backend/db/migrations/025_add_direction_to_messages.sql @@ -0,0 +1,2 @@ +-- Миграция: добавление поля direction в таблицу messages +ALTER TABLE messages ADD COLUMN IF NOT EXISTS direction VARCHAR(8); \ No newline at end of file diff --git a/backend/routes/users.js b/backend/routes/users.js index d43e11d..2139a04 100644 --- a/backend/routes/users.js +++ b/backend/routes/users.js @@ -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 - Получить список всех пользователей (пример, может требовать прав администратора) // В текущей реализации этот маршрут не используется и закомментирован /* diff --git a/backend/server.js b/backend/server.js index 10eaf2d..3d3e0fe 100644 --- a/backend/server.js +++ b/backend/server.js @@ -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); diff --git a/backend/services/auth-service.js b/backend/services/auth-service.js index e894393..1c5dbb5 100644 --- a/backend/services/auth-service.js +++ b/backend/services/auth-service.js @@ -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); diff --git a/backend/services/emailAuth.js b/backend/services/emailAuth.js index 0e5a3a4..57b689a 100644 --- a/backend/services/emailAuth.js +++ b/backend/services/emailAuth.js @@ -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'); diff --git a/backend/services/emailBot.js b/backend/services/emailBot.js index 8638bcd..7ab25c6 100644 --- a/backend/services/emailBot.js +++ b/backend/services/emailBot.js @@ -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; diff --git a/backend/services/identity-service.js b/backend/services/identity-service.js index c9b1413..00d1603 100644 --- a/backend/services/identity-service.js +++ b/backend/services/identity-service.js @@ -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(); diff --git a/backend/services/telegramBot.js b/backend/services/telegramBot.js index 82a6c84..2138aaa 100644 --- a/backend/services/telegramBot.js +++ b/backend/services/telegramBot.js @@ -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('Произошла ошибка при обработке вашего сообщения. Попробуйте позже.'); } }); diff --git a/backend/services/wallet-service.js b/backend/services/wallet-service.js new file mode 100644 index 0000000..3fcd0d5 --- /dev/null +++ b/backend/services/wallet-service.js @@ -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 }; \ No newline at end of file diff --git a/frontend/src/components/ContactTable.vue b/frontend/src/components/ContactTable.vue new file mode 100644 index 0000000..7134d57 --- /dev/null +++ b/frontend/src/components/ContactTable.vue @@ -0,0 +1,122 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/DleManagement.vue b/frontend/src/components/DleManagement.vue new file mode 100644 index 0000000..2e399ab --- /dev/null +++ b/frontend/src/components/DleManagement.vue @@ -0,0 +1,623 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/services/contactsService.js b/frontend/src/services/contactsService.js new file mode 100644 index 0000000..e9628c8 --- /dev/null +++ b/frontend/src/services/contactsService.js @@ -0,0 +1,11 @@ +import api from '../api/axios'; + +export default { + async getContacts() { + const res = await api.get('/api/users'); + if (res.data && res.data.success) { + return res.data.contacts; + } + return []; + } +}; \ No newline at end of file diff --git a/frontend/src/views/CrmView.vue b/frontend/src/views/CrmView.vue index 921ed8d..f235ea6 100644 --- a/frontend/src/views/CrmView.vue +++ b/frontend/src/views/CrmView.vue @@ -7,274 +7,35 @@ @auth-action-completed="$emit('auth-action-completed')" >
-

Управление DLE

-
-

Загрузка данных DLE...

-
+
+

Управление DLE

+
-
-

Для доступа к управлению DLE необходимо .

-
-
- -
-

Ваши DLE

-
-

У вас пока нет созданных DLE.

- -
-
-
-
-

{{ dle.name }} ({{ dle.symbol }})

-

Адрес: {{ shortenAddress(dle.tokenAddress) }}

-

Местонахождение: {{ dle.location }}

-
- - -
-
-
-
-
- - -
-

Управление "{{ selectedDle.name }}"

- -
-
-
- Основная информация -
-
- Предложения -
-
- Управление -
-
- Модули -
-
- - -
-
-

Основная информация

-
- Название: - {{ selectedDle.name }} -
-
- Символ токена: - {{ selectedDle.symbol }} -
-
- Местонахождение: - {{ selectedDle.location }} -
-
- Коды деятельности: - {{ selectedDle.isicCodes && selectedDle.isicCodes.length ? selectedDle.isicCodes.join(', ') : 'Не указаны' }} -
-
- Дата создания: - {{ formatDate(selectedDle.creationTimestamp) }} -
-
- -
-
-

Токен управления

-

{{ selectedDle.tokenAddress }}

-
- - -
-
- -
-

Таймлок

-

{{ selectedDle.timelockAddress }}

-
- - -
-
- -
-

Governor

-

{{ selectedDle.governorAddress }}

-
- - -
-
-
-
- - -
-

Предложения

-
- -
- -
-

Новое предложение

-
- - -
-
- - -
-
- - -
-
- -
-

Предложений пока нет

-
-

{{ proposal.title }}

-

{{ proposal.description }}

-
- {{ getProposalStatusText(proposal.status) }} -
-
- - -
-
-
-
- - -
-

Управление

-
-
-

Настройки Governor

-
- Порог предложения: - 100,000 GT -
-
- Кворум: - 4% -
-
- Задержка голосования: - 1 день -
-
- Период голосования: - 7 дней -
-
- -
-

Статистика голосований

-
- Всего предложений: - {{ proposals.length }} -
-
- Активных предложений: - {{ getProposalsByStatus('active').length }} -
-
- Успешных предложений: - {{ getProposalsByStatus('succeeded').length }} -
-
- Отклоненных предложений: - {{ getProposalsByStatus('defeated').length }} -
-
-
-
- - -
-

Подключение модулей

-

Здесь вы можете подключить дополнительные модули к вашему DLE.

- -
-
-

{{ module.name }}

-

{{ module.description }}

-
- {{ module.installed ? 'Установлен' : 'Доступен' }} -
-
- - -
-
-
-
-
-
+ +
+

Контакты

+
+
\ No newline at end of file