diff --git a/README.md b/README.md index 3921636..2fb6c0d 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,9 @@ nano frontend/.env 4. Выполните миграции изнутри контейнера backend: ``` -docker exec dapp-backend yarn migrate -``` + docker exec -e NODE_ENV=migration dapp-backend yarn migrate + + ``` Скрипт автоматически: - Проверит наличие Docker и Docker Compose diff --git a/backend/app.js b/backend/app.js index b670ff8..ebae5af 100644 --- a/backend/app.js +++ b/backend/app.js @@ -2,12 +2,12 @@ const express = require('express'); const helmet = require('helmet'); const cors = require('cors'); const session = require('express-session'); -const { sessionMiddleware } = require('./config/session'); +const sessionConfig = require('./config/session'); const logger = require('./utils/logger'); // const csurf = require('csurf'); // Закомментировано, так как не используется -const { errorHandler } = require('./middleware/errorHandler'); +const errorHandler = require('./middleware/errorHandler'); // const { version } = require('./package.json'); // Закомментировано, так как не используется -const pool = require('./db'); // Добавляем импорт pool +const db = require('./db'); // Добавляем импорт db const aiAssistant = require('./services/ai-assistant'); // Добавляем импорт aiAssistant const fs = require('fs'); const path = require('path'); @@ -48,6 +48,9 @@ const ensureDirectoriesExist = () => { // Вызываем функцию проверки директорий при запуске сервера ensureDirectoriesExist(); +// Регистрируем коллбек для пересоздания session middleware при смене пула +db.setPoolChangeCallback(sessionConfig.reloadSessionMiddleware); + // Импорт маршрутов const authRoutes = require('./routes/auth'); const usersRoutes = require('./routes/users'); @@ -79,8 +82,8 @@ app.use( }) ); -// Настройка сессии (ИСПОЛЬЗУЕМ ИМПОРТИРОВАННОЕ MIDDLEWARE) -app.use(sessionMiddleware); +// Настройка сессии (используем геттер, чтобы всегда был актуальный middleware) +app.use((req, res, next) => sessionConfig.sessionMiddleware(req, res, next)); // Добавим middleware для проверки сессии app.use(async (req, res, next) => { @@ -89,7 +92,7 @@ app.use(async (req, res, next) => { // Проверяем сессию в базе данных if (req.sessionID) { - const result = await pool.query('SELECT sess FROM session WHERE sid = $1', [req.sessionID]); + const result = await db.getQuery()('SELECT sess FROM session WHERE sid = $1', [req.sessionID]); console.log('Session from DB:', result.rows[0]?.sess); } @@ -104,7 +107,7 @@ app.use(async (req, res, next) => { const token = authHeader.split(' ')[1]; try { // Находим пользователя по токену - const { rows } = await pool.query( + const { rows } = await db.getQuery( ` SELECT u.id, (u.role = 'admin') as is_admin, @@ -152,7 +155,7 @@ app.use((req, res, next) => { // Маршруты API app.use('/api/auth', authRoutes); app.use('/api/users', usersRoutes); -app.use('/api/identities', identitiesRoutes); +app.use('/api', identitiesRoutes); app.use('/api/chat', chatRoutes); app.use('/api/admin', adminRoutes); app.use('/api/tokens', tokensRouter); @@ -181,6 +184,8 @@ console.log('OPENAI_API_KEY:', redactedValue); console.log('EMAIL_USER:', process.env.EMAIL_USER); console.log('EMAIL_PASSWORD:', redactedValue); +console.log('typeof errorHandler:', typeof errorHandler, errorHandler.name); + // Добавляем обработчик ошибок последним app.use(errorHandler); @@ -188,7 +193,7 @@ app.use(errorHandler); app.get('/api/health', async (req, res) => { try { // Проверяем подключение к БД - await pool.query('SELECT NOW()'); + await db.getQuery('SELECT NOW()'); // Проверяем AI сервис const aiStatus = await aiAssistant.checkHealth(); @@ -212,7 +217,7 @@ app.get('/api/health', async (req, res) => { setInterval( async () => { try { - await pool.query('DELETE FROM session WHERE expire < NOW()'); + await db.getQuery('DELETE FROM session WHERE expire < NOW()'); } catch (error) { console.error('Error cleaning old sessions:', error); } diff --git a/backend/config/session.js b/backend/config/session.js index aa9929b..92719a1 100644 --- a/backend/config/session.js +++ b/backend/config/session.js @@ -1,25 +1,46 @@ const session = require('express-session'); const pgSession = require('connect-pg-simple')(session); -const { pool } = require('../db'); +const db = require('../db'); -const sessionConfig = { - store: new pgSession({ - pool, - tableName: 'session', - }), - secret: process.env.SESSION_SECRET || 'hb3atoken', - name: 'sessionId', - resave: false, - saveUninitialized: true, - cookie: { - maxAge: 30 * 24 * 60 * 60 * 1000, - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'lax', - path: '/', - }, -}; +let onPoolChangeCallback = null; + +function setPoolChangeCallback(cb) { + onPoolChangeCallback = cb; +} + +let sessionMiddleware = createSessionMiddleware(); + +function createSessionMiddleware() { + return session({ + store: new pgSession({ + pool: db.getPool(), + tableName: 'session', + }), + secret: process.env.SESSION_SECRET || 'hb3atoken', + name: 'sessionId', + resave: false, + saveUninitialized: true, + cookie: { + maxAge: 30 * 24 * 60 * 60 * 1000, + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + path: '/', + }, + }); +} + +function reloadSessionMiddleware() { + sessionMiddleware = createSessionMiddleware(); + if (onPoolChangeCallback) { + onPoolChangeCallback(); + } +} module.exports = { - sessionMiddleware: session(sessionConfig), + get sessionMiddleware() { + return sessionMiddleware; + }, + reloadSessionMiddleware, + setPoolChangeCallback, }; diff --git a/backend/db.js b/backend/db.js index ae6c5e2..5f306c1 100644 --- a/backend/db.js +++ b/backend/db.js @@ -9,10 +9,14 @@ console.log('DB_PORT:', process.env.DB_PORT); console.log('DB_NAME:', process.env.DB_NAME); console.log('DB_USER:', process.env.DB_USER); -// Создаем пул соединений с базой данных -const pool = new Pool({ - connectionString: process.env.DATABASE_URL, - ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false, +// Первичное подключение по дефолтным значениям +let pool = new Pool({ + host: process.env.DB_HOST || 'postgres', + port: parseInt(process.env.DB_PORT || '5432'), + database: process.env.DB_NAME || 'dapp_db', + user: process.env.DB_USER || 'dapp_user', + password: process.env.DB_PASSWORD, + ssl: false, }); // Проверяем подключение к базе данных @@ -21,36 +25,59 @@ pool.query('SELECT NOW()') console.log('Успешное подключение к базе данных:', res.rows[0]); }) .catch(err => { - console.error('Failed to connect to the database using DATABASE_URL:', err); - console.log('Attempting alternative database connection...'); - - // Пробуем альтернативное подключение - const altPool = new Pool({ - host: process.env.DB_HOST || 'localhost', - port: parseInt(process.env.DB_PORT || '5432'), - database: process.env.DB_NAME || 'dapp_db', - user: process.env.DB_USER || 'dapp_user', - password: process.env.DB_PASSWORD, - }); - - altPool.query('SELECT NOW()') - .then(altRes => { - console.log('Альтернативное подключение успешно:', altRes.rows[0]); - // Заменяем основной пул на альтернативный - module.exports.pool = altPool; - module.exports.query = (text, params) => altPool.query(text, params); - }) - .catch(altErr => { - console.error('Альтернативное подключение тоже не удалось:', altErr); - console.log('Переключение на временное хранилище данных в памяти...'); - module.exports = createInMemoryStorage(); - }); + console.error('Ошибка подключения к базе данных:', err); }); -// Функция для выполнения SQL-запросов -const query = (text, params) => { - return pool.query(text, params); -}; +console.log('Пул создан:', pool.options || pool); + +function getPool() { + return pool; +} + +function getQuery() { + return pool.query.bind(pool); +} + +let poolChangeCallback = null; + +function setPoolChangeCallback(cb) { + poolChangeCallback = cb; +} + +// Функция для пересоздания пула из db_settings +async function reinitPoolFromDbSettings() { + try { + const res = await pool.query('SELECT * FROM db_settings ORDER BY id LIMIT 1'); + if (!res.rows.length) throw new Error('DB settings not found'); + const settings = res.rows[0]; + // Закрываем старый пул + await pool.end(); + // Создаём новый пул + pool = new Pool({ + host: settings.db_host, + port: parseInt(settings.db_port), + database: settings.db_name, + user: settings.db_user, + password: settings.db_password, + ssl: false, + }); + // Пересоздаём session middleware + if (poolChangeCallback) { + poolChangeCallback(); + } + console.log('Пул пересоздан с новыми параметрами:', settings); + } catch (err) { + console.error('Ошибка пересоздания пула:', err); + throw err; + } +} + +// При старте приложения — сразу пробуем инициализировать из db_settings +if (process.env.NODE_ENV !== 'migration') { + reinitPoolFromDbSettings(); +} + +const query = (text, params) => pool.query(text, params); // Функция для сохранения гостевого сообщения в базе данных async function saveGuestMessageToDatabase(message, language, guestId) { @@ -71,70 +98,9 @@ async function saveGuestMessageToDatabase(message, language, guestId) { // Экспортируем функции для работы с базой данных module.exports = { - query, - pool, + getPool, + getQuery, + reinitPoolFromDbSettings, saveGuestMessageToDatabase, + setPoolChangeCallback, }; - -// Функция для создания временного хранилища данных в памяти -function createInMemoryStorage() { - console.log('Используется временное хранилище данных в памяти'); - - const users = []; - let userId = 1; - - // Эмуляция функции query для работы с пользователями - const inMemoryQuery = async (text, params) => { - console.log('SQL query (in-memory):', text, 'Params:', params); - - // Эмуляция запроса SELECT * FROM users WHERE address = $1 - if (text.includes('SELECT * FROM users WHERE address = $1')) { - const address = params[0]; - const user = users.find((u) => u.address === address); - return { rows: user ? [user] : [] }; - } - - // Эмуляция запроса SELECT * FROM users WHERE email = $1 - if (text.includes('SELECT * FROM users WHERE email = $1')) { - const email = params[0]; - const user = users.find((u) => u.email === email); - return { rows: user ? [user] : [] }; - } - - // Эмуляция запроса INSERT INTO users - if (text.includes('INSERT INTO users')) { - let newUser; - - if (text.includes('address')) { - newUser = { id: userId++, address: params[0], created_at: new Date(), is_admin: false }; - } else if (text.includes('email')) { - newUser = { id: userId++, email: params[0], created_at: new Date(), is_admin: false }; - } - - if (newUser) { - users.push(newUser); - return { rows: [newUser] }; - } - } - - return { rows: [] }; - }; - - return { - query: inMemoryQuery, - pool: { - query: (text, params, callback) => { - if (callback) { - try { - const result = inMemoryQuery(text, params); - callback(null, result); - } catch (err) { - callback(err); - } - } else { - return inMemoryQuery(text, params); - } - }, - }, - }; -} diff --git a/backend/db/index.js b/backend/db/index.js deleted file mode 100644 index eff32a0..0000000 --- a/backend/db/index.js +++ /dev/null @@ -1,21 +0,0 @@ -const { Pool } = require('pg'); -const logger = require('../utils/logger'); - -const pool = new Pool({ - user: process.env.DB_USER || 'dapp_user', - host: process.env.DB_HOST || 'localhost', - database: process.env.DB_NAME || 'dapp_db', - password: process.env.DB_PASSWORD, - port: process.env.DB_PORT || 5432, -}); - -// Проверка подключения -pool.query('SELECT NOW()', (err, res) => { - if (err) { - logger.error('Error connecting to database:', err); - } else { - logger.info('Успешное подключение к базе данных:', res.rows[0]); - } -}); - -module.exports = { pool }; diff --git a/backend/db/migrations/020_create_email_settings.sql b/backend/db/migrations/020_create_email_settings.sql new file mode 100644 index 0000000..3cfadcb --- /dev/null +++ b/backend/db/migrations/020_create_email_settings.sql @@ -0,0 +1,17 @@ +CREATE TABLE IF NOT EXISTS email_settings ( + id SERIAL PRIMARY KEY, + smtp_host VARCHAR(255) NOT NULL, + smtp_port INTEGER NOT NULL, + smtp_user VARCHAR(255) NOT NULL, + smtp_password VARCHAR(255) NOT NULL, + imap_host VARCHAR(255), + imap_port INTEGER, + from_email VARCHAR(255) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW() +); + +-- Для простоты предполагаем, что настройки всегда одни (id=1) +INSERT INTO email_settings (smtp_host, smtp_port, smtp_user, smtp_password, imap_host, imap_port, from_email) +VALUES ('smtp.example.com', 465, 'user@example.com', 'password', 'imap.example.com', 993, 'noreply@example.com') +ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/backend/db/migrations/021_create_telegram_settings.sql b/backend/db/migrations/021_create_telegram_settings.sql new file mode 100644 index 0000000..ad43212 --- /dev/null +++ b/backend/db/migrations/021_create_telegram_settings.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS telegram_settings ( + id SERIAL PRIMARY KEY, + bot_token VARCHAR(255) NOT NULL, + bot_username VARCHAR(255) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW() +); + +-- Для простоты предполагаем, что настройки всегда одни (id=1) +INSERT INTO telegram_settings (bot_token, bot_username) +VALUES ('your-telegram-bot-token', 'your_bot_username') +ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/backend/db/migrations/022_create_db_settings.sql b/backend/db/migrations/022_create_db_settings.sql new file mode 100644 index 0000000..3a3016f --- /dev/null +++ b/backend/db/migrations/022_create_db_settings.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS db_settings ( + id SERIAL PRIMARY KEY, + db_host VARCHAR(255) NOT NULL, + db_port INTEGER NOT NULL, + db_name VARCHAR(255) NOT NULL, + db_user VARCHAR(255) NOT NULL, + db_password VARCHAR(255) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW() +); + +-- Для простоты предполагаем, что настройки всегда одни (id=1) +INSERT INTO db_settings (db_host, db_port, db_name, db_user, db_password) +VALUES ('localhost', 5432, 'dapp_db', 'dapp_user', 'dapp_password') +ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/backend/db/migrations/023_create_ai_providers_settings.sql b/backend/db/migrations/023_create_ai_providers_settings.sql new file mode 100644 index 0000000..4163f33 --- /dev/null +++ b/backend/db/migrations/023_create_ai_providers_settings.sql @@ -0,0 +1,14 @@ +CREATE TABLE IF NOT EXISTS ai_providers_settings ( + id SERIAL PRIMARY KEY, + provider VARCHAR(32) NOT NULL UNIQUE, -- openai, anthropic, google, ollama + api_key VARCHAR(255), + base_url VARCHAR(255), + selected_model VARCHAR(128), + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW() +); + +-- Пример заполнения для Ollama (без ключа) +INSERT INTO ai_providers_settings (provider, base_url, selected_model) +VALUES ('ollama', 'http://localhost:11434', 'qwen2.5') +ON CONFLICT (provider) DO NOTHING; \ No newline at end of file diff --git a/backend/middleware/errorHandler.js b/backend/middleware/errorHandler.js index 813c2db..0644c5d 100644 --- a/backend/middleware/errorHandler.js +++ b/backend/middleware/errorHandler.js @@ -7,6 +7,11 @@ const { ERROR_CODES } = require('../utils/constants'); */ // eslint-disable-next-line no-unused-vars const errorHandler = (err, req, res, /* next */) => { + console.error('errorHandler: err =', err); + console.error('errorHandler: typeof err =', typeof err); + console.error('errorHandler: stack =', err && err.stack); + console.log('errorHandler called, typeof res:', typeof res, 'res:', res); + console.log('typeof res:', typeof res, 'isFunction:', typeof res === 'function'); // Логируем ошибку logger.error(`Error: ${err.message}`, { stack: err.stack, @@ -65,7 +70,6 @@ function createError(message, status) { return error; } -module.exports = { - errorHandler, - createError, -}; +module.exports = errorHandler; +// Если нужен createError для других файлов: +// module.exports.createError = createError; diff --git a/backend/package.json b/backend/package.json index bba6db8..e368c1c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -23,6 +23,8 @@ "fix-duplicates": "node scripts/fix-duplicate-identities.js" }, "dependencies": { + "@anthropic-ai/sdk": "^0.51.0", + "@google/genai": "^1.0.1", "@langchain/community": "^0.3.34", "@langchain/core": "0.3.0", "@langchain/ollama": "^0.2.0", @@ -47,6 +49,7 @@ "node-cron": "^3.0.3", "node-telegram-bot-api": "^0.66.0", "nodemailer": "^6.10.0", + "openai": "^4.102.0", "pg": "^8.10.0", "semver": "^7.7.1", "session-file-store": "^1.5.0", diff --git a/backend/routes/admin.js b/backend/routes/admin.js index 02fa185..f21c0c1 100644 --- a/backend/routes/admin.js +++ b/backend/routes/admin.js @@ -6,49 +6,49 @@ const authService = require('../services/auth-service'); const logger = require('../utils/logger'); // Роли -router.get('/roles', requireAdmin, async (req, res) => { +router.get('/roles', requireAdmin, async (req, res, next) => { try { const roles = await authService.getAllRoles(); res.json({ success: true, roles }); } catch (error) { logger.error('Error getting roles:', error); - res.status(500).json({ error: 'Internal server error' }); + next(error); } }); -router.post('/roles', requireAdmin, async (req, res) => { +router.post('/roles', requireAdmin, async (req, res, next) => { try { const { name, permissions } = req.body; const role = await authService.createRole(name, permissions); res.json({ success: true, role }); } catch (error) { logger.error('Error creating role:', error); - res.status(500).json({ error: 'Internal server error' }); + next(error); } }); // Админ функции -router.get('/users', requireAdmin, async (req, res) => { +router.get('/users', requireAdmin, async (req, res, next) => { try { const users = await authService.getAllUsers(); res.json({ success: true, users }); } catch (error) { logger.error('Error getting users:', error); - res.status(500).json({ error: 'Internal server error' }); + next(error); } }); // Маршрут для получения статистики (защищен middleware requireAdmin) -router.get('/stats', requireAdmin, async (req, res) => { +router.get('/stats', requireAdmin, async (req, res, next) => { try { // Получаем количество пользователей - const usersCount = await db.query('SELECT COUNT(*) FROM users'); + const usersCount = await db.getQuery()('SELECT COUNT(*) FROM users'); // Получаем количество досок - const boardsCount = await db.query('SELECT COUNT(*) FROM kanban_boards'); + const boardsCount = await db.getQuery()('SELECT COUNT(*) FROM kanban_boards'); // Получаем количество задач - const tasksCount = await db.query('SELECT COUNT(*) FROM kanban_tasks'); + const tasksCount = await db.getQuery()('SELECT COUNT(*) FROM kanban_tasks'); res.json({ userCount: parseInt(usersCount.rows[0].count), @@ -57,18 +57,18 @@ router.get('/stats', requireAdmin, async (req, res) => { }); } catch (error) { console.error('Ошибка при получении статистики:', error); - res.status(500).json({ error: 'Internal server error' }); + next(error); } }); // Маршрут для получения логов -router.get('/logs', requireAdmin, async (req, res) => { +router.get('/logs', requireAdmin, async (req, res, next) => { try { - const result = await db.query('SELECT * FROM logs ORDER BY created_at DESC LIMIT 100'); + const result = await db.getQuery()('SELECT * FROM logs ORDER BY created_at DESC LIMIT 100'); res.json(result.rows); } catch (error) { console.error('Ошибка при получении логов:', error); - res.status(500).json({ error: 'Internal server error' }); + next(error); } }); diff --git a/backend/routes/auth.js b/backend/routes/auth.js index ea1a21a..6d6ae04 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -34,7 +34,7 @@ router.get('/nonce', async (req, res) => { const nonce = crypto.randomBytes(16).toString('hex'); // Проверяем, существует ли уже nonce для этого адреса - const existingNonce = await db.query('SELECT id FROM nonces WHERE identity_value = $1', [ + const existingNonce = await db.getQuery()('SELECT id FROM nonces WHERE identity_value = $1', [ address.toLowerCase(), ]); diff --git a/backend/routes/chat.js b/backend/routes/chat.js index cdb7258..b854168 100644 --- a/backend/routes/chat.js +++ b/backend/routes/chat.js @@ -17,7 +17,7 @@ async function processGuestMessages(userId, guestId) { logger.info(`Processing guest messages for user ${userId} with guest ID ${guestId}`); // Проверяем, обрабатывались ли уже эти сообщения - const mappingCheck = await db.query( + const mappingCheck = await db.getQuery()( 'SELECT processed FROM guest_user_mapping WHERE guest_id = $1', [guestId] ); @@ -30,7 +30,7 @@ async function processGuestMessages(userId, guestId) { // Проверяем наличие mapping записи и создаем если нет if (mappingCheck.rows.length === 0) { - await db.query( + 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, guestId] ); @@ -38,7 +38,7 @@ async function processGuestMessages(userId, guestId) { } // Получаем все гостевые сообщения со всеми новыми полями - const guestMessagesResult = await db.query( + const guestMessagesResult = await db.getQuery()( `SELECT id, guest_id, content, language, is_ai, created_at, attachment_filename, attachment_mimetype, attachment_size, attachment_data @@ -48,9 +48,9 @@ async function processGuestMessages(userId, guestId) { if (guestMessagesResult.rows.length === 0) { logger.info(`No guest messages found for guest ID ${guestId}`); - const checkResult = await db.query('SELECT 1 FROM guest_user_mapping WHERE guest_id = $1', [guestId]); + const checkResult = await db.getQuery()('SELECT 1 FROM guest_user_mapping WHERE guest_id = $1', [guestId]); if (checkResult.rows.length > 0) { - await db.query('UPDATE guest_user_mapping SET processed = true WHERE guest_id = $1', [guestId]); + await db.getQuery()('UPDATE guest_user_mapping SET processed = true WHERE guest_id = $1', [guestId]); logger.info(`Marked guest mapping as processed (no messages found) for guest ID ${guestId}`); } else { logger.warn(`Attempted to mark non-existent guest mapping as processed for guest ID ${guestId}`); @@ -67,7 +67,7 @@ async function processGuestMessages(userId, guestId) { ? (firstMessage.content.length > 30 ? `${firstMessage.content.substring(0, 30)}...` : firstMessage.content) : (firstMessage.attachment_filename ? `Файл: ${firstMessage.attachment_filename}` : 'Новый диалог'); - const newConversationResult = await db.query( + const newConversationResult = await db.getQuery()( 'INSERT INTO conversations (user_id, title) VALUES ($1, $2) RETURNING *', [userId, title] ); @@ -84,7 +84,7 @@ async function processGuestMessages(userId, guestId) { try { // Сохраняем сообщение пользователя в таблицу messages, включая данные файла - const userMessageResult = await db.query( + const userMessageResult = await db.getQuery()( `INSERT INTO messages (conversation_id, content, sender_type, role, channel, created_at, user_id, attachment_filename, attachment_mimetype, attachment_size, attachment_data) @@ -118,7 +118,7 @@ async function processGuestMessages(userId, guestId) { if (aiResponseContent) { // Сохраняем ответ от ИИ (у него нет вложений) - const aiMessageResult = await db.query( + const aiMessageResult = await db.getQuery()( `INSERT INTO messages (conversation_id, content, sender_type, role, channel, created_at, user_id) VALUES @@ -144,20 +144,20 @@ async function processGuestMessages(userId, guestId) { // Удаляем только успешно обработанные гостевые сообщения if (savedMessageIds.length > 0) { - await db.query('DELETE FROM guest_messages WHERE id = ANY($1::int[])', [savedMessageIds]); + await db.getQuery()('DELETE FROM guest_messages WHERE id = ANY($1::int[])', [savedMessageIds]); logger.info( `Deleted ${savedMessageIds.length} processed guest messages for guest ID ${guestId}` ); // Помечаем гостевой ID как обработанный - await db.query('UPDATE guest_user_mapping SET processed = true WHERE guest_id = $1', [ + await db.getQuery()('UPDATE guest_user_mapping SET processed = true WHERE guest_id = $1', [ guestId, ]); logger.info(`Marked guest mapping as processed for guest ID ${guestId}`); } else { logger.warn(`No guest messages were successfully processed, skipping deletion for guest ID ${guestId}`); // Если не было успешных, все равно пометим как обработанные, чтобы не пытаться снова - await db.query('UPDATE guest_user_mapping SET processed = true WHERE guest_id = $1', [guestId]); + await db.getQuery()('UPDATE guest_user_mapping SET processed = true WHERE guest_id = $1', [guestId]); logger.info(`Marked guest mapping as processed (no successful messages) for guest ID ${guestId}`); } @@ -221,7 +221,7 @@ router.post('/guest-message', upload.array('attachments'), async (req, res) => { }); // Сохраняем сообщение пользователя с текстом или файлом - const result = await db.query( + const result = await db.getQuery()( `INSERT INTO guest_messages (guest_id, content, language, is_ai, attachment_filename, attachment_mimetype, attachment_size, attachment_data) @@ -293,7 +293,7 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re try { // Найти или создать диалог if (conversationId) { - const convResult = await db.query( + const convResult = await db.getQuery()( 'SELECT * FROM conversations WHERE id = $1 AND user_id = $2', [conversationId, userId] ); @@ -308,7 +308,7 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re ? (message.length > 50 ? `${message.substring(0, 50)}...` : message) : (file ? `Файл: ${file.originalname}` : 'Новый диалог'); - const newConvResult = await db.query( + const newConvResult = await db.getQuery()( 'INSERT INTO conversations (user_id, title) VALUES ($1, $2) RETURNING *', [userId, title] ); @@ -325,7 +325,7 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re const attachmentData = file ? file.buffer : null; // Сохраняем сообщение пользователя - const userMessageResult = await db.query( + const userMessageResult = await db.getQuery()( `INSERT INTO messages (conversation_id, user_id, content, sender_type, role, channel, attachment_filename, attachment_mimetype, attachment_size, attachment_data) @@ -354,7 +354,7 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re logger.info('AI response received' + (aiResponseContent ? '' : ' (empty)'), { conversationId }); if (aiResponseContent) { - const aiMessageResult = await db.query( + const aiMessageResult = await db.getQuery()( `INSERT INTO messages (conversation_id, user_id, content, sender_type, role, channel) VALUES ($1, $2, $3, 'assistant', 'assistant', 'web') @@ -443,7 +443,7 @@ router.get('/history', requireAuth, async (req, res) => { countQuery += ' AND conversation_id = $2'; countParams.push(conversationId); } - const countResult = await db.query(countQuery, countParams); + const countResult = await db.getQuery()(countQuery, countParams); const totalCount = parseInt(countResult.rows[0].count, 10); return res.json({ success: true, count: totalCount }); } @@ -481,7 +481,7 @@ router.get('/history', requireAuth, async (req, res) => { logger.debug('Executing history query:', { query, params }); - const result = await db.query(query, params); + const result = await db.getQuery()(query, params); // Обрабатываем результаты для фронтенда const messages = result.rows.map(msg => { @@ -522,7 +522,7 @@ router.get('/history', requireAuth, async (req, res) => { totalCountQuery += ' AND conversation_id = $2'; totalCountParams.push(conversationId); } - const totalCountResult = await db.query(totalCountQuery, totalCountParams); + const totalCountResult = await db.getQuery()(totalCountQuery, totalCountParams); const totalMessages = parseInt(totalCountResult.rows[0].count, 10); logger.info(`Returning message history for user ${userId}`, { count: messages.length, offset, limit, total: totalMessages }); diff --git a/backend/routes/dle.js b/backend/routes/dle.js index 37d39c0..276e528 100644 --- a/backend/routes/dle.js +++ b/backend/routes/dle.js @@ -11,7 +11,7 @@ const fs = require('fs'); * @desc Создать новое DLE (Digital Legal Entity) * @access Private (только для авторизованных пользователей с ролью admin) */ -router.post('/', auth.requireAuth, auth.requireAdmin, async (req, res) => { +router.post('/', auth.requireAuth, auth.requireAdmin, async (req, res, next) => { try { const dleParams = req.body; logger.info('Получен запрос на создание DLE:', dleParams); @@ -44,11 +44,7 @@ router.post('/', auth.requireAuth, auth.requireAdmin, async (req, res) => { }); } catch (error) { logger.error('Ошибка при создании DLE:', error); - res.status(500).json({ - success: false, - message: error.message || 'Произошла ошибка при создании DLE', - error: process.env.NODE_ENV === 'development' ? error.stack : undefined - }); + next(error); } }); @@ -57,7 +53,7 @@ router.post('/', auth.requireAuth, auth.requireAdmin, async (req, res) => { * @desc Получить список всех DLE * @access Private (только для авторизованных пользователей) */ -router.get('/', auth.requireAuth, async (req, res) => { +router.get('/', auth.requireAuth, async (req, res, next) => { try { const dles = await dleService.getAllDLEs(); res.json({ @@ -66,11 +62,7 @@ router.get('/', auth.requireAuth, async (req, res) => { }); } catch (error) { logger.error('Ошибка при получении списка DLE:', error); - res.status(500).json({ - success: false, - message: error.message || 'Произошла ошибка при получении списка DLE', - error: process.env.NODE_ENV === 'development' ? error.stack : undefined - }); + next(error); } }); @@ -99,7 +91,7 @@ router.get('/settings', auth.requireAuth, (req, res) => { * @desc Удалить DLE по адресу токена * @access Private (только для авторизованных пользователей с ролью admin) */ -router.delete('/:tokenAddress', auth.requireAuth, auth.requireAdmin, async (req, res) => { +router.delete('/:tokenAddress', auth.requireAuth, auth.requireAdmin, async (req, res, next) => { try { const { tokenAddress } = req.params; logger.info(`Получен запрос на удаление DLE с адресом токена: ${tokenAddress}`); @@ -142,11 +134,7 @@ router.delete('/:tokenAddress', auth.requireAuth, auth.requireAdmin, async (req, }); } catch (error) { logger.error('Ошибка при удалении DLE:', error); - res.status(500).json({ - success: false, - message: error.message || 'Произошла ошибка при удалении DLE', - error: process.env.NODE_ENV === 'development' ? error.stack : undefined - }); + next(error); } }); @@ -155,7 +143,7 @@ router.delete('/:tokenAddress', auth.requireAuth, auth.requireAdmin, async (req, * @desc Удалить пустое DLE по имени файла * @access Private (только для авторизованных пользователей с ролью admin) */ -router.delete('/empty/:fileName', auth.requireAuth, auth.requireAdmin, async (req, res) => { +router.delete('/empty/:fileName', auth.requireAuth, auth.requireAdmin, async (req, res, next) => { try { const { fileName } = req.params; logger.info(`Получен запрос на удаление пустого DLE с именем файла: ${fileName}`); @@ -180,11 +168,7 @@ router.delete('/empty/:fileName', auth.requireAuth, auth.requireAdmin, async (re }); } catch (error) { logger.error('Ошибка при удалении пустого DLE:', error); - res.status(500).json({ - success: false, - message: error.message || 'Произошла ошибка при удалении пустого DLE', - error: process.env.NODE_ENV === 'development' ? error.stack : undefined - }); + next(error); } }); diff --git a/backend/routes/identities.js b/backend/routes/identities.js index b84e336..0046d33 100644 --- a/backend/routes/identities.js +++ b/backend/routes/identities.js @@ -6,19 +6,19 @@ const logger = require('../utils/logger'); const db = require('../db'); // Получение всех идентификаторов пользователя -router.get('/', requireAuth, async (req, res) => { +router.get('/', requireAuth, async (req, res, next) => { try { const userId = req.session.userId; const identities = await authService.getUserIdentities(userId); res.json({ success: true, identities }); } catch (error) { logger.error('Error getting identities:', error); - res.status(500).json({ error: 'Internal server error' }); + next(error); } }); // Связывание нового идентификатора -router.post('/link', requireAuth, async (req, res) => { +router.post('/link', requireAuth, async (req, res, next) => { try { const { type, value } = req.body; const userId = req.session.userId; @@ -28,7 +28,7 @@ router.post('/link', requireAuth, async (req, res) => { const normalizedWallet = value.toLowerCase(); // Проверяем, существует ли уже такой кошелек - const existingCheck = await db.query( + const existingCheck = await db.getQuery()( `SELECT user_id FROM user_identities WHERE provider = 'wallet' AND provider_id = $1`, [normalizedWallet] @@ -73,12 +73,12 @@ router.post('/link', requireAuth, async (req, res) => { }); } - res.status(500).json({ error: error.message || 'Internal server error' }); + next(error); } }); // Получение балансов токенов -router.get('/token-balances', requireAuth, async (req, res) => { +router.get('/token-balances', requireAuth, async (req, res, next) => { try { const userId = req.session.userId; if (!userId) { @@ -103,12 +103,12 @@ router.get('/token-balances', requireAuth, async (req, res) => { }); } catch (error) { logger.error('Error getting token balances:', error); - res.status(500).json({ error: 'Internal server error' }); + next(error); } }); // Удаление идентификатора пользователя -router.delete('/:provider/:providerId', requireAuth, async (req, res) => { +router.delete('/:provider/:providerId', requireAuth, async (req, res, next) => { try { const userId = req.session.userId; const { provider, providerId } = req.params; @@ -120,7 +120,135 @@ router.delete('/:provider/:providerId', requireAuth, async (req, res) => { } } catch (error) { logger.error('Error deleting identity:', error); - res.status(500).json({ error: 'Internal server error' }); + next(error); + } +}); + +// Получение email-настроек +router.get('/email-settings', requireAuth, async (req, res, next) => { + try { + const { rows } = await db.getQuery()('SELECT * FROM email_settings ORDER BY id LIMIT 1'); + if (!rows.length) return res.status(404).json({ success: false, error: 'Not found' }); + const settings = rows[0]; + delete settings.smtp_password; // не возвращаем пароль + res.json({ success: true, settings }); + } catch (error) { + logger.error('Error getting email settings:', error, error && error.stack); + next(error); + } +}); + +// Обновление email-настроек +router.put('/email-settings', requireAuth, async (req, res, next) => { + try { + const { smtp_host, smtp_port, smtp_user, smtp_password, imap_host, imap_port, from_email } = req.body; + if (!smtp_host || !smtp_port || !smtp_user || !from_email) { + return res.status(400).json({ success: false, error: 'Missing required fields' }); + } + const { rows } = await db.getQuery()('SELECT id FROM email_settings ORDER BY id LIMIT 1'); + if (rows.length) { + // Обновляем существующую запись + await db.getQuery()( + `UPDATE email_settings SET smtp_host=$1, smtp_port=$2, smtp_user=$3, smtp_password=COALESCE($4, smtp_password), imap_host=$5, imap_port=$6, from_email=$7, updated_at=NOW() WHERE id=$8`, + [smtp_host, smtp_port, smtp_user, smtp_password, imap_host, imap_port, from_email, rows[0].id] + ); + } else { + // Вставляем новую + await db.getQuery()( + `INSERT INTO email_settings (smtp_host, smtp_port, smtp_user, smtp_password, imap_host, imap_port, from_email) VALUES ($1,$2,$3,$4,$5,$6,$7)`, + [smtp_host, smtp_port, smtp_user, smtp_password, imap_host, imap_port, from_email] + ); + } + res.json({ success: true }); + } catch (error) { + logger.error('Error updating email settings:', error); + next(error); + } +}); + +// Получение telegram-настроек +router.get('/telegram-settings', requireAuth, async (req, res, next) => { + try { + const { rows } = await db.getQuery()('SELECT * FROM telegram_settings ORDER BY id LIMIT 1'); + if (!rows.length) return res.status(404).json({ success: false, error: 'Not found' }); + const settings = rows[0]; + delete settings.bot_token; // не возвращаем токен + res.json({ success: true, settings }); + } catch (error) { + logger.error('Error getting telegram settings:', error, error && error.stack); + next(error); + } +}); + +// Обновление telegram-настроек +router.put('/telegram-settings', requireAuth, async (req, res, next) => { + try { + const { bot_token, bot_username } = req.body; + if (!bot_token || !bot_username) { + return res.status(400).json({ success: false, error: 'Missing required fields' }); + } + const { rows } = await db.getQuery()('SELECT id FROM telegram_settings ORDER BY id LIMIT 1'); + if (rows.length) { + // Обновляем существующую запись + await db.getQuery()( + `UPDATE telegram_settings SET bot_token=$1, bot_username=$2, updated_at=NOW() WHERE id=$3`, + [bot_token, bot_username, rows[0].id] + ); + } else { + // Вставляем новую + await db.getQuery()( + `INSERT INTO telegram_settings (bot_token, bot_username) VALUES ($1,$2)` , + [bot_token, bot_username] + ); + } + res.json({ success: true }); + } catch (error) { + logger.error('Error updating telegram settings:', error); + next(error); + } +}); + +// Получение db-настроек +router.get('/db-settings', requireAuth, async (req, res, next) => { + try { + const { rows } = await db.getQuery()('SELECT * FROM db_settings ORDER BY id LIMIT 1'); + if (!rows.length) return res.status(404).json({ success: false, error: 'Not found' }); + const settings = rows[0]; + delete settings.db_password; // не возвращаем пароль + res.json({ success: true, settings }); + } catch (error) { + logger.error('Error getting db settings:', error, error && error.stack); + next(error); + } +}); + +// Обновление db-настроек +router.put('/db-settings', requireAuth, async (req, res, next) => { + try { + const { db_host, db_port, db_name, db_user, db_password } = req.body; + if (!db_host || !db_port || !db_name || !db_user) { + return res.status(400).json({ success: false, error: 'Missing required fields' }); + } + const { rows } = await db.getQuery()('SELECT id FROM db_settings ORDER BY id LIMIT 1'); + if (rows.length) { + // Обновляем существующую запись + await db.getQuery()( + `UPDATE db_settings SET db_host=$1, db_port=$2, db_name=$3, db_user=$4, db_password=COALESCE($5, db_password), updated_at=NOW() WHERE id=$6`, + [db_host, db_port, db_name, db_user, db_password, rows[0].id] + ); + } else { + // Вставляем новую + await db.getQuery()( + `INSERT INTO db_settings (db_host, db_port, db_name, db_user, db_password) VALUES ($1,$2,$3,$4,$5)` , + [db_host, db_port, db_name, db_user, db_password] + ); + } + // Пересоздаём пул соединений с новыми настройками + await db.reinitPoolFromDbSettings(); + res.json({ success: true }); + } catch (error) { + logger.error('Error updating db settings:', error); + next(error); } }); diff --git a/backend/routes/isic.js b/backend/routes/isic.js index bf58135..d7ffb71 100644 --- a/backend/routes/isic.js +++ b/backend/routes/isic.js @@ -1,6 +1,6 @@ const express = require('express'); const router = express.Router(); -const { pool } = require('../db'); // Убедитесь, что путь к вашему db-коннектору правильный +const db = require('../db'); const logger = require('../utils/logger'); // Если используете логгер /** @@ -98,7 +98,7 @@ router.get('/codes', async (req, res) => { if (parent_code) { try { - const parentResult = await pool.query('SELECT code_level FROM isic_rev4_codes WHERE code = $1', [parent_code]); + const parentResult = await db.getQuery()('SELECT code_level FROM isic_rev4_codes WHERE code = $1', [parent_code]); if (parentResult.rows.length > 0) { const parentLevel = parentResult.rows[0].code_level; if (parentLevel >= 1 && parentLevel < 6) { @@ -146,7 +146,7 @@ router.get('/codes', async (req, res) => { } if (parent_code) { // Предполагаем, что parent_code уже добавлен в countQueryParams - const parentLevelResult = await pool.query('SELECT code_level FROM isic_rev4_codes WHERE code = $1', [parent_code]); // Нужно будет передать parent_code в countQueryParams + const parentLevelResult = await db.getQuery()('SELECT code_level FROM isic_rev4_codes WHERE code = $1', [parent_code]); // Нужно будет передать parent_code в countQueryParams if (parentLevelResult.rows.length > 0) { const parentLevel = parentLevelResult.rows[0].code_level; if (parentLevel >=1 && parentLevel < 6) { @@ -174,7 +174,7 @@ router.get('/codes', async (req, res) => { const queryWhereConditions = []; if (level) queryWhereConditions.push(`c.code_level = $${currentQueryParamIndex++}`); if (parent_code) { - const parentLevelResult = await pool.query('SELECT code_level FROM isic_rev4_codes WHERE code = $1', [parent_code]); // Это дублирование, лучше получить parentLevel один раз + const parentLevelResult = await db.getQuery()('SELECT code_level FROM isic_rev4_codes WHERE code = $1', [parent_code]); // Это дублирование, лучше получить parentLevel один раз if (parentLevelResult.rows.length > 0) { const parentLevel = parentLevelResult.rows[0].code_level; if (parentLevel >=1 && parentLevel < 6) { @@ -193,12 +193,12 @@ router.get('/codes', async (req, res) => { try { logger.debug('Executing count query:', finalCountQuery, 'Params:', countQueryParams); - const totalItemsResult = await pool.query(finalCountQuery, countQueryParams); + const totalItemsResult = await db.getQuery()(finalCountQuery, countQueryParams); const totalItems = parseInt(totalItemsResult.rows[0].total, 10); // Параметры для основного запроса - это все, что в queryParams (включая limit и offset) logger.debug('Executing data query:', finalQuery, 'Params:', queryParams); - const result = await pool.query(finalQuery, queryParams); + const result = await db.getQuery()(finalQuery, queryParams); res.json({ totalItems, @@ -253,13 +253,13 @@ router.get('/tree', async (req, res) => { try { let items; if (!root_code) { // Если нет root_code, возвращаем секции (уровень 1) - const result = await pool.query( + const result = await db.getQuery()( "SELECT code, description, code_level FROM isic_rev4_codes WHERE code_level = 1 ORDER BY sort_order, code" ); items = result.rows.map(row => ({ ...row, children: [] })); // Добавляем пустой массив children } else { // Получаем сам root_code - const rootResult = await pool.query( + const rootResult = await db.getQuery()( "SELECT code, description, code_level FROM isic_rev4_codes WHERE code = $1", [root_code] ); @@ -281,7 +281,7 @@ router.get('/tree', async (req, res) => { if (childrenQuery) { - const childrenResult = await pool.query(childrenQuery, childrenParams); + const childrenResult = await db.getQuery()(childrenQuery, childrenParams); rootNode.children = childrenResult.rows.map(row => ({ ...row, children: [] })); } items = [rootNode]; diff --git a/backend/routes/settings.js b/backend/routes/settings.js index 520733f..cf21e4c 100644 --- a/backend/routes/settings.js +++ b/backend/routes/settings.js @@ -5,23 +5,25 @@ const logger = require('../utils/logger'); const { ethers } = require('ethers'); const rpcProviderService = require('../services/rpcProviderService'); const authTokenService = require('../services/authTokenService'); +const aiProviderSettingsService = require('../services/aiProviderSettingsService'); +const aiAssistant = require('../services/ai-assistant'); // Логируем версию ethers для отладки logger.info(`Ethers version: ${ethers.version || 'unknown'}`); // Получение RPC настроек -router.get('/rpc', requireAdmin, async (req, res) => { +router.get('/rpc', requireAdmin, async (req, res, next) => { try { const rpcConfigs = await rpcProviderService.getAllRpcProviders(); res.json({ success: true, data: rpcConfigs }); } catch (error) { logger.error('Ошибка при получении RPC настроек:', error); - res.status(500).json({ success: false, error: 'Ошибка сервера при получении настроек RPC' }); + next(error); } }); // Добавление/обновление одного или нескольких RPC -router.post('/rpc', requireAdmin, async (req, res) => { +router.post('/rpc', requireAdmin, async (req, res, next) => { try { // Если пришёл массив rpcConfigs — bulk-режим if (Array.isArray(req.body.rpcConfigs)) { @@ -41,35 +43,35 @@ router.post('/rpc', requireAdmin, async (req, res) => { res.json({ success: true, message: 'RPC провайдер сохранён' }); } catch (error) { logger.error('Ошибка при сохранении RPC:', error); - res.status(500).json({ success: false, error: 'Ошибка сервера при сохранении RPC' }); + next(error); } }); // Удаление одного RPC -router.delete('/rpc/:networkId', requireAdmin, async (req, res) => { +router.delete('/rpc/:networkId', requireAdmin, async (req, res, next) => { try { const { networkId } = req.params; await rpcProviderService.deleteRpcProvider(networkId); res.json({ success: true, message: 'RPC провайдер удалён' }); } catch (error) { logger.error('Ошибка при удалении RPC:', error); - res.status(500).json({ success: false, error: 'Ошибка сервера при удалении RPC' }); + next(error); } }); // Получение токенов для аутентификации -router.get('/auth-tokens', requireAdmin, async (req, res) => { +router.get('/auth-tokens', requireAdmin, async (req, res, next) => { try { const authTokens = await authTokenService.getAllAuthTokens(); res.json({ success: true, data: authTokens }); } catch (error) { logger.error('Ошибка при получении токенов аутентификации:', error); - res.status(500).json({ success: false, error: 'Ошибка сервера при получении токенов аутентификации' }); + next(error); } }); // Сохранение токенов для аутентификации -router.post('/auth-tokens', requireAdmin, async (req, res) => { +router.post('/auth-tokens', requireAdmin, async (req, res, next) => { try { const { authTokens } = req.body; if (!Array.isArray(authTokens)) { @@ -79,12 +81,12 @@ router.post('/auth-tokens', requireAdmin, async (req, res) => { res.json({ success: true, message: 'Токены аутентификации успешно сохранены' }); } catch (error) { logger.error('Ошибка при сохранении токенов аутентификации:', error); - res.status(500).json({ success: false, error: 'Ошибка сервера при сохранении токенов аутентификации' }); + next(error); } }); // Добавление/обновление одного токена -router.post('/auth-token', requireAdmin, async (req, res) => { +router.post('/auth-token', requireAdmin, async (req, res, next) => { try { const { name, address, network, minBalance } = req.body; if (!name || !address || !network) { @@ -94,24 +96,24 @@ router.post('/auth-token', requireAdmin, async (req, res) => { res.json({ success: true, message: 'Токен аутентификации сохранён' }); } catch (error) { logger.error('Ошибка при сохранении токена аутентификации:', error); - res.status(500).json({ success: false, error: 'Ошибка сервера при сохранении токена' }); + next(error); } }); // Удаление одного токена -router.delete('/auth-token/:address/:network', requireAdmin, async (req, res) => { +router.delete('/auth-token/:address/:network', requireAdmin, async (req, res, next) => { try { const { address, network } = req.params; await authTokenService.deleteAuthToken(address, network); res.json({ success: true, message: 'Токен аутентификации удалён' }); } catch (error) { logger.error('Ошибка при удалении токена аутентификации:', error); - res.status(500).json({ success: false, error: 'Ошибка сервера при удалении токена' }); + next(error); } }); // Тестирование RPC соединения -router.post('/rpc-test', requireAdmin, async (req, res) => { +router.post('/rpc-test', requireAdmin, async (req, res, next) => { try { const { rpcUrl, networkId } = req.body; @@ -164,4 +166,76 @@ router.post('/rpc-test', requireAdmin, async (req, res) => { } }); +// Получить настройки AI-провайдера +router.get('/ai-settings/:provider', requireAdmin, async (req, res, next) => { + try { + const { provider } = req.params; + const settings = await aiProviderSettingsService.getProviderSettings(provider); + res.json({ success: true, settings }); + } catch (error) { + logger.error('Ошибка при получении AI-настроек:', error); + next(error); + } +}); + +// Сохранить/обновить настройки AI-провайдера +router.put('/ai-settings/:provider', requireAdmin, async (req, res, next) => { + try { + const { provider } = req.params; + const { api_key, base_url, selected_model } = req.body; + const updated = await aiProviderSettingsService.upsertProviderSettings({ provider, api_key, base_url, selected_model }); + res.json({ success: true, settings: updated }); + } catch (error) { + logger.error('Ошибка при сохранении AI-настроек:', error); + next(error); + } +}); + +// Удалить настройки AI-провайдера +router.delete('/ai-settings/:provider', requireAdmin, async (req, res, next) => { + try { + const { provider } = req.params; + await aiProviderSettingsService.deleteProviderSettings(provider); + res.json({ success: true }); + } catch (error) { + logger.error('Ошибка при удалении AI-настроек:', error); + next(error); + } +}); + +// Получить список моделей для провайдера +router.get('/ai-settings/:provider/models', requireAdmin, async (req, res, next) => { + try { + const { provider } = req.params; + const settings = await aiProviderSettingsService.getProviderSettings(provider); + let models = []; + if (provider === 'ollama') { + models = await aiAssistant.getAvailableModels(); + } else { + models = await aiProviderSettingsService.getProviderModels(provider, settings || {}); + } + res.json({ success: true, models }); + } catch (error) { + logger.error('Ошибка при получении моделей AI:', error); + res.status(500).json({ success: false, error: error.message }); + } +}); + +// Проверить валидность ключа (verify) +router.post('/ai-settings/:provider/verify', requireAdmin, async (req, res, next) => { + try { + const { provider } = req.params; + const { api_key, base_url } = req.body; + const result = await aiProviderSettingsService.verifyProviderKey(provider, { api_key, base_url }); + if (result.success) { + res.json({ success: true }); + } else { + res.status(400).json({ success: false, error: result.error }); + } + } catch (error) { + logger.error('Ошибка при проверке AI-ключа:', error); + res.status(500).json({ success: false, error: error.message }); + } +}); + module.exports = router; \ No newline at end of file diff --git a/backend/routes/tokens.js b/backend/routes/tokens.js index cf33025..ae0b972 100644 --- a/backend/routes/tokens.js +++ b/backend/routes/tokens.js @@ -4,7 +4,7 @@ const logger = require('../utils/logger'); const authService = require('../services/auth-service'); // Получение балансов токенов пользователя по токенам из базы -router.get('/balances', async (req, res) => { +router.get('/balances', async (req, res, next) => { try { const address = req.query.address; if (!address) { @@ -15,7 +15,7 @@ router.get('/balances', async (req, res) => { res.json({ success: true, data: balances }); } catch (error) { logger.error('Error fetching token balances:', error); - res.status(500).json({ success: false, error: 'Failed to fetch token balances' }); + next(error); } }); diff --git a/backend/routes/users.js b/backend/routes/users.js index 0107454..d43e11d 100644 --- a/backend/routes/users.js +++ b/backend/routes/users.js @@ -20,53 +20,42 @@ router.get('/:address', (req, res) => { }); // Маршрут для обновления языка пользователя -router.post('/update-language', requireAuth, async (req, res) => { +router.post('/update-language', requireAuth, async (req, res, next) => { try { const { language } = req.body; const userId = req.session.userId; - - // Проверка валидности языка const validLanguages = ['ru', 'en']; if (!validLanguages.includes(language)) { return res.status(400).json({ error: 'Неподдерживаемый язык' }); } - - // Обновление языка в базе данных - await db.query('UPDATE users SET preferred_language = $1 WHERE id = $2', [language, userId]); - + await db.getQuery()('UPDATE users SET preferred_language = $1 WHERE id = $2', [language, userId]); res.json({ success: true }); } catch (error) { logger.error('Error updating language:', error); - res.status(500).json({ error: 'Внутренняя ошибка сервера' }); + next(error); } }); // Маршрут для обновления имени и фамилии пользователя -router.post('/update-profile', requireAuth, async (req, res) => { +router.post('/update-profile', requireAuth, async (req, res, next) => { try { const { firstName, lastName } = req.body; const userId = req.session.userId; - - // Проверка валидности данных if (firstName && firstName.length > 255) { return res.status(400).json({ error: 'Имя слишком длинное (максимум 255 символов)' }); } - if (lastName && lastName.length > 255) { return res.status(400).json({ error: 'Фамилия слишком длинная (максимум 255 символов)' }); } - - // Обновление имени и фамилии в базе данных - await db.query('UPDATE users SET first_name = $1, last_name = $2 WHERE id = $3', [ + await db.getQuery()('UPDATE users SET first_name = $1, last_name = $2 WHERE id = $3', [ firstName || null, lastName || null, userId, ]); - res.json({ success: true }); } catch (error) { logger.error('Error updating user profile:', error); - res.status(500).json({ error: 'Внутренняя ошибка сервера' }); + next(error); } }); diff --git a/backend/scripts/run-migrations.js b/backend/scripts/run-migrations.js index f1eb10f..d2f2cff 100644 --- a/backend/scripts/run-migrations.js +++ b/backend/scripts/run-migrations.js @@ -1,7 +1,8 @@ const fs = require('fs').promises; const path = require('path'); require('dotenv').config(); -const { pool } = require('../db'); +const { getPool } = require('../db'); +const pool = getPool(); const logger = require('../utils/logger'); async function runMigrations() { diff --git a/backend/services/aiProviderSettingsService.js b/backend/services/aiProviderSettingsService.js new file mode 100644 index 0000000..080c25e --- /dev/null +++ b/backend/services/aiProviderSettingsService.js @@ -0,0 +1,105 @@ +const db = require('../db'); +const OpenAI = require('openai'); +const Anthropic = require('@anthropic-ai/sdk'); +const { GoogleGenAI } = require('@google/genai'); + +const TABLE = 'ai_providers_settings'; + +async function getProviderSettings(provider) { + const { rows } = await db.getQuery()( + `SELECT * FROM ${TABLE} WHERE provider = $1 LIMIT 1`, + [provider] + ); + return rows[0] || null; +} + +async function upsertProviderSettings({ provider, api_key, base_url, selected_model }) { + const { rows } = await db.getQuery()( + `INSERT INTO ${TABLE} (provider, api_key, base_url, selected_model, updated_at) + VALUES ($1, $2, $3, $4, NOW()) + ON CONFLICT (provider) DO UPDATE SET + api_key = EXCLUDED.api_key, + base_url = EXCLUDED.base_url, + selected_model = EXCLUDED.selected_model, + updated_at = NOW() + RETURNING *`, + [provider, api_key, base_url, selected_model] + ); + return rows[0]; +} + +async function deleteProviderSettings(provider) { + await db.getQuery()( + `DELETE FROM ${TABLE} WHERE provider = $1`, + [provider] + ); +} + +async function getProviderModels(provider, { api_key, base_url } = {}) { + try { + if (provider === 'openai') { + const client = new OpenAI({ apiKey: api_key, baseURL: base_url }); + const res = await client.models.list(); + return res.data ? res.data.map(m => ({ id: m.id, ...m })) : []; + } + if (provider === 'anthropic') { + const client = new Anthropic({ apiKey: api_key, baseURL: base_url }); + const res = await client.models.list(); + return res.data ? res.data.map(m => ({ id: m.id, ...m })) : []; + } + if (provider === 'google') { + const ai = new GoogleGenAI({ apiKey: api_key, baseUrl: base_url }); + const pager = await ai.models.list(); + const models = []; + for await (const model of pager) { + models.push(model); + } + return models; + } + if (provider === 'ollama') { + // Для Ollama — через ai-assistant.js + return []; + } + return []; + } catch (error) { + return []; + } +} + +async function verifyProviderKey(provider, { api_key, base_url } = {}) { + try { + if (provider === 'openai') { + const client = new OpenAI({ apiKey: api_key, baseURL: base_url }); + await client.models.list(); + return { success: true }; + } + if (provider === 'anthropic') { + const client = new Anthropic({ apiKey: api_key, baseURL: base_url }); + await client.models.list(); + return { success: true }; + } + if (provider === 'google') { + const ai = new GoogleGenAI({ apiKey: api_key, baseUrl: base_url }); + const pager = await ai.models.list(); + for await (const _ of pager) { + break; + } + return { success: true }; + } + if (provider === 'ollama') { + // Для Ollama — всегда true (локальный) + return { success: true }; + } + return { success: false, error: 'Unknown provider' }; + } catch (error) { + return { success: false, error: error.message }; + } +} + +module.exports = { + getProviderSettings, + upsertProviderSettings, + deleteProviderSettings, + getProviderModels, + verifyProviderKey, +}; \ No newline at end of file diff --git a/backend/services/authTokenService.js b/backend/services/authTokenService.js index d559353..06644d1 100644 --- a/backend/services/authTokenService.js +++ b/backend/services/authTokenService.js @@ -1,7 +1,7 @@ const db = require('../db'); async function getAllAuthTokens() { - const { rows } = await db.query('SELECT * FROM auth_tokens ORDER BY id'); + const { rows } = await db.getQuery()('SELECT * FROM auth_tokens ORDER BY id'); return rows; } diff --git a/backend/services/emailAuth.js b/backend/services/emailAuth.js index 63f57f4..128352b 100644 --- a/backend/services/emailAuth.js +++ b/backend/services/emailAuth.js @@ -1,13 +1,13 @@ const { pool } = require('../db'); const verificationService = require('./verification-service'); const logger = require('../utils/logger'); -const emailBot = require('./emailBot'); +const EmailBotService = require('./emailBot'); const db = require('../db'); const authService = require('./auth-service'); class EmailAuth { constructor() { - this.emailBot = emailBot; + this.emailBot = new EmailBotService(); } async initEmailAuth(session, email) { @@ -17,7 +17,7 @@ class EmailAuth { } // Проверяем, существует ли пользователь с таким email - const existingEmailUser = await db.query( + const existingEmailUser = await db.getQuery()( `SELECT u.id FROM users u JOIN user_identities i ON u.id = i.user_id WHERE i.provider = 'email' AND i.provider_id = $1`, diff --git a/backend/services/emailBot.js b/backend/services/emailBot.js index 7adf63b..8638bcd 100644 --- a/backend/services/emailBot.js +++ b/backend/services/emailBot.js @@ -1,4 +1,4 @@ -const { pool } = require('../db'); +const db = require('../db'); const nodemailer = require('nodemailer'); const Imap = require('imap'); const simpleParser = require('mailparser').simpleParser; @@ -6,61 +6,47 @@ const { processMessage } = require('./ai-assistant'); const { inspect } = require('util'); const logger = require('../utils/logger'); -// Конфигурация для отправки писем -const transporter = nodemailer.createTransport({ - host: process.env.EMAIL_SMTP_HOST || 'smtp.hostland.ru', - port: process.env.EMAIL_SMTP_PORT || 465, - secure: true, - auth: { - user: process.env.EMAIL_USER, - pass: process.env.EMAIL_PASSWORD, - }, - pool: true, - maxConnections: 3, - maxMessages: 5, - tls: { - rejectUnauthorized: false, - }, -}); - -// Конфигурация для получения писем -const imapConfig = { - user: process.env.EMAIL_USER, - password: process.env.EMAIL_PASSWORD, - host: process.env.EMAIL_IMAP_HOST, - port: process.env.EMAIL_IMAP_PORT, - tls: true, - tlsOptions: { rejectUnauthorized: false }, - keepalive: { - interval: 10000, - idleInterval: 300000, - forceNoop: true, - }, -}; - class EmailBotService { - constructor() { - this.transporter = transporter; - this.imap = new Imap(imapConfig); - this.initialize(); + async getSettingsFromDb() { + const { rows } = await db.getQuery()('SELECT * FROM email_settings ORDER BY id LIMIT 1'); + if (!rows.length) throw new Error('Email settings not found in DB'); + return rows[0]; } - initialize() { - this.imap.once('error', (err) => { - logger.error(`IMAP connection error: ${err.message}`); - setTimeout(() => { - try { - if (this.imap.state !== 'connected') { - this.imap = new Imap(imapConfig); - this.initialize(); - } - } catch (e) { - logger.error(`Error reconnecting IMAP: ${e.message}`); - } - }, 60000); + async getTransporter() { + const settings = await this.getSettingsFromDb(); + return nodemailer.createTransport({ + host: settings.smtp_host, + port: settings.smtp_port, + secure: true, + auth: { + user: settings.smtp_user, + pass: settings.smtp_password, + }, + pool: true, + maxConnections: 3, + maxMessages: 5, + tls: { rejectUnauthorized: false }, }); } + async getImapConfig() { + const settings = await this.getSettingsFromDb(); + return { + user: settings.smtp_user, + password: settings.smtp_password, + host: settings.imap_host, + port: settings.imap_port, + tls: true, + tlsOptions: { rejectUnauthorized: false }, + keepalive: { + interval: 10000, + idleInterval: 300000, + forceNoop: true, + }, + }; + } + // Метод для инициализации email верификации async initEmailVerification(email, userId, code) { try { @@ -77,24 +63,16 @@ class EmailBotService { // Отправка кода верификации async sendVerificationCode(email, code) { try { + const settings = await this.getSettingsFromDb(); + const transporter = await this.getTransporter(); const mailOptions = { - from: process.env.EMAIL_USER, + from: settings.from_email, to: email, subject: 'Код подтверждения', text: `Ваш код подтверждения: ${code}\n\nКод действителен в течение 15 минут.`, - html: ` -
Ваш код подтверждения:
-Код действителен в течение 15 минут.
-Ваш код подтверждения:
Код действителен в течение 15 минут.
Код отправлен на {{ email }}
Интеграция с OpenAI (GPT-4, GPT-3.5 и др.).
+ +Интеграция с Anthropic Claude (Claude 3 и др.).
+ +Интеграция с Google Gemini (Gemini 1.5, 1.0 и др.).
+ +Локальные open-source модели через Ollama.
+ +Интеграция с Telegram-ботом для уведомлений и авторизации.
- +Интеграция с Email для отправки писем и уведомлений.
- +