From 9aa842d23856611ebcf03ce79b64cadebaaac91e Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 22 May 2025 16:24:39 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B2=D0=B0=D1=88=D0=B5=20=D1=81=D0=BE=D0=BE?= =?UTF-8?q?=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BA=D0=BE=D0=BC=D0=BC?= =?UTF-8?q?=D0=B8=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 +- backend/app.js | 25 ++- backend/config/session.js | 59 +++-- backend/db.js | 160 ++++++-------- backend/db/index.js | 21 -- .../migrations/020_create_email_settings.sql | 17 ++ .../021_create_telegram_settings.sql | 12 ++ .../db/migrations/022_create_db_settings.sql | 15 ++ .../023_create_ai_providers_settings.sql | 14 ++ backend/middleware/errorHandler.js | 12 +- backend/package.json | 3 + backend/routes/admin.js | 28 +-- backend/routes/auth.js | 2 +- backend/routes/chat.js | 38 ++-- backend/routes/dle.js | 32 +-- backend/routes/identities.js | 146 ++++++++++++- backend/routes/isic.js | 18 +- backend/routes/settings.js | 104 +++++++-- backend/routes/tokens.js | 4 +- backend/routes/users.js | 23 +- backend/scripts/run-migrations.js | 3 +- backend/services/aiProviderSettingsService.js | 105 +++++++++ backend/services/authTokenService.js | 2 +- backend/services/emailAuth.js | 6 +- backend/services/emailBot.js | 114 ++++------ backend/services/identity-service.js | 22 +- backend/services/rpcProviderService.js | 2 +- backend/services/session-service.js | 2 +- backend/services/telegramBot.js | 52 +++-- backend/services/verification-service.js | 15 +- backend/utils/helpers.js | 4 +- backend/yarn.lock | 142 +++++++++++- frontend/src/components/Sidebar.vue | 2 +- .../src/components/identity/EmailConnect.vue | 116 +++++----- frontend/src/composables/useAuth.js | 60 +----- .../src/views/settings/AIProviderSettings.vue | 203 ++++++++++++++++++ .../src/views/settings/AiSettingsView.vue | 85 +++++++- .../views/settings/DatabaseSettingsView.vue | 171 +++++++++++++++ .../src/views/settings/EmailSettingsView.vue | 126 ++++++++++- .../src/views/settings/OllamaSettingsView.vue | 63 ++++++ .../views/settings/TelegramSettingsView.vue | 95 +++++++- 41 files changed, 1621 insertions(+), 507 deletions(-) delete mode 100644 backend/db/index.js create mode 100644 backend/db/migrations/020_create_email_settings.sql create mode 100644 backend/db/migrations/021_create_telegram_settings.sql create mode 100644 backend/db/migrations/022_create_db_settings.sql create mode 100644 backend/db/migrations/023_create_ai_providers_settings.sql create mode 100644 backend/services/aiProviderSettingsService.js create mode 100644 frontend/src/views/settings/AIProviderSettings.vue create mode 100644 frontend/src/views/settings/DatabaseSettingsView.vue create mode 100644 frontend/src/views/settings/OllamaSettingsView.vue 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: ` -
-

Код подтверждения

-

Ваш код подтверждения:

-
- ${code} -
-

Код действителен в течение 15 минут.

-
- `, + html: `

Код подтверждения

Ваш код подтверждения:

${code}

Код действителен в течение 15 минут.

`, }; - - await this.transporter.sendMail(mailOptions); + await transporter.sendMail(mailOptions); logger.info(`Verification code sent to ${email}`); } catch (error) { logger.error('Error sending verification code:', error); @@ -187,14 +165,15 @@ class EmailBotService { // Метод для отправки email async sendEmail(to, subject, text) { try { + const settings = await this.getSettingsFromDb(); + const transporter = await this.getTransporter(); const mailOptions = { - from: process.env.EMAIL_USER, + from: settings.from_email, to, subject, text, }; - - await this.transporter.sendMail(mailOptions); + await transporter.sendMail(mailOptions); logger.info(`Email sent to ${to}`); return true; } catch (error) { @@ -204,5 +183,4 @@ class EmailBotService { } } -// Экспортируем singleton instance -module.exports = new EmailBotService(); +module.exports = EmailBotService; diff --git a/backend/services/identity-service.js b/backend/services/identity-service.js index ca6cf61..c9b1413 100644 --- a/backend/services/identity-service.js +++ b/backend/services/identity-service.js @@ -62,7 +62,7 @@ class IdentityService { ); try { - 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, normalizedProviderId] ); @@ -91,7 +91,7 @@ class IdentityService { ); // Проверяем, существует ли уже такой идентификатор - const existingResult = await db.query( + const existingResult = await db.getQuery()( `SELECT user_id FROM user_identities WHERE provider = $1 AND provider_id = $2`, [normalizedProvider, normalizedProviderId] ); @@ -116,7 +116,7 @@ class IdentityService { } } else { // Создаем новую запись - await db.query( + await db.getQuery()( `INSERT INTO user_identities (user_id, provider, provider_id) VALUES ($1, $2, $3)`, [userId, normalizedProvider, normalizedProviderId] @@ -148,7 +148,7 @@ class IdentityService { return []; } - const result = await db.query( + const result = await db.getQuery()( `SELECT provider, provider_id FROM user_identities WHERE user_id = $1`, [userId] ); @@ -174,7 +174,7 @@ class IdentityService { return []; } - const result = await db.query( + const result = await db.getQuery()( `SELECT provider_id FROM user_identities WHERE user_id = $1 AND provider = $2`, [userId, provider] ); @@ -211,7 +211,7 @@ class IdentityService { const { provider: normalizedProvider, providerId: normalizedProviderId } = this.normalizeIdentity(provider, providerId); - const result = await db.query( + const result = await db.getQuery()( `SELECT u.id, u.role FROM users u JOIN user_identities ui ON u.id = ui.user_id WHERE ui.provider = $1 AND ui.provider_id = $2`, @@ -255,7 +255,7 @@ class IdentityService { // Нормализуем провайдера const normalizedProvider = provider.toLowerCase(); - const result = await db.query( + const result = await db.getQuery()( `SELECT provider, provider_id, created_at, updated_at FROM user_identities WHERE user_id = $1 AND provider = $2 @@ -320,7 +320,7 @@ class IdentityService { // Сохраняем гостевые идентификаторы в guest_user_mapping if (session.guestId) { try { - 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, session.guestId] ); @@ -333,7 +333,7 @@ class IdentityService { if (session.previousGuestId && session.previousGuestId !== session.guestId) { try { - 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, session.previousGuestId] ); @@ -479,7 +479,7 @@ class IdentityService { for (const [provider, providerId] of Object.entries(identities)) { if (!providerId) continue; - const result = await db.query( + const result = await db.getQuery()( `SELECT DISTINCT user_id FROM user_identities WHERE provider = $1 AND provider_id = $2`, @@ -510,7 +510,7 @@ class IdentityService { return { success: false, error: 'Missing required parameters' }; } const { provider: normalizedProvider, providerId: normalizedProviderId } = this.normalizeIdentity(provider, providerId); - const result = await db.query( + const result = await db.getQuery()( `DELETE FROM user_identities WHERE user_id = $1 AND provider = $2 AND provider_id = $3`, [userId, normalizedProvider, normalizedProviderId] ); diff --git a/backend/services/rpcProviderService.js b/backend/services/rpcProviderService.js index 3ab3974..3fb410c 100644 --- a/backend/services/rpcProviderService.js +++ b/backend/services/rpcProviderService.js @@ -1,7 +1,7 @@ const db = require('../db'); async function getAllRpcProviders() { - const { rows } = await db.query('SELECT * FROM rpc_providers ORDER BY id'); + const { rows } = await db.getQuery()('SELECT * FROM rpc_providers ORDER BY id'); return rows; } diff --git a/backend/services/session-service.js b/backend/services/session-service.js index d5962e8..077f352 100644 --- a/backend/services/session-service.js +++ b/backend/services/session-service.js @@ -51,7 +51,7 @@ class SessionService { } // Получаем все гостевые ID для текущего пользователя из новой таблицы - const guestIdsResult = await db.query( + const guestIdsResult = await db.getQuery()( 'SELECT guest_id FROM guest_user_mapping WHERE user_id = $1', [userId] ); diff --git a/backend/services/telegramBot.js b/backend/services/telegramBot.js index dce7506..82a6c84 100644 --- a/backend/services/telegramBot.js +++ b/backend/services/telegramBot.js @@ -6,11 +6,21 @@ const verificationService = require('./verification-service'); const crypto = require('crypto'); let botInstance = null; +let telegramSettingsCache = null; + +async function getTelegramSettings() { + if (telegramSettingsCache) return telegramSettingsCache; + const { rows } = await db.getQuery()('SELECT * FROM telegram_settings ORDER BY id LIMIT 1'); + if (!rows.length) throw new Error('Telegram settings not found in DB'); + telegramSettingsCache = rows[0]; + return telegramSettingsCache; +} // Создание и настройка бота async function getBot() { if (!botInstance) { - botInstance = new Telegraf(process.env.TELEGRAM_BOT_TOKEN); + const settings = await getTelegramSettings(); + botInstance = new Telegraf(settings.bot_token); // Обработка команды /start botInstance.command('start', (ctx) => { @@ -23,7 +33,7 @@ async function getBot() { try { // Получаем код верификации для всех активных кодов с провайдером telegram - const codeResult = await db.query( + const codeResult = await db.getQuery()( `SELECT * FROM verification_codes WHERE code = $1 AND provider = 'telegram' @@ -44,14 +54,14 @@ async function getBot() { let userRole = 'user'; // Роль по умолчанию // Отмечаем код как использованный - await db.query('UPDATE verification_codes SET used = true WHERE id = $1', [ + 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.query( + const existingTelegramUser = await db.getQuery()( `SELECT ui.user_id FROM user_identities ui WHERE ui.provider = 'telegram' AND ui.provider_id = $1`, @@ -68,7 +78,7 @@ async function getBot() { // Используем userId из кода верификации userId = linkedUserId; // Связываем Telegram с этим пользователем - await db.query( + await db.getQuery()( `INSERT INTO user_identities (user_id, provider, provider_id, created_at) VALUES ($1, $2, $3, NOW())`, @@ -81,7 +91,7 @@ async function getBot() { // Проверяем, есть ли пользователь, связанный с гостевым идентификатором let existingUserWithGuestId = null; if (providerId) { - const guestUserResult = await db.query( + const guestUserResult = await db.getQuery()( `SELECT user_id FROM guest_user_mapping WHERE guest_id = $1`, [providerId] ); @@ -96,7 +106,7 @@ async function getBot() { if (existingUserWithGuestId) { // Используем существующего пользователя и добавляем ему Telegram идентификатор userId = existingUserWithGuestId; - await db.query( + await db.getQuery()( `INSERT INTO user_identities (user_id, provider, provider_id, created_at) VALUES ($1, $2, $3, NOW())`, @@ -105,14 +115,14 @@ async function getBot() { logger.info(`Linked Telegram account ${ctx.from.id} to existing user ${userId}`); } else { // Создаем нового пользователя, если не нашли существующего - const userResult = await db.query( + 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.query( + await db.getQuery()( `INSERT INTO user_identities (user_id, provider, provider_id, created_at) VALUES ($1, $2, $3, NOW())`, @@ -121,7 +131,7 @@ async function getBot() { // Если был гостевой ID, связываем его с новым пользователем if (providerId) { - await db.query( + await db.getQuery()( `INSERT INTO guest_user_mapping (user_id, guest_id) VALUES ($1, $2) @@ -147,15 +157,15 @@ async function getBot() { logger.info(`[TelegramBot] Role for user ${userId} determined as: ${userRole}`); // Опционально: Обновить роль в таблице users - const currentUser = await db.query('SELECT role FROM users WHERE id = $1', [userId]); + 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.query('UPDATE users SET role = $1 WHERE id = $2', [userRole, userId]); + 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.query('SELECT role FROM users WHERE id = $1', [userId]); + const currentUser = await db.getQuery()('SELECT role FROM users WHERE id = $1', [userId]); if (currentUser.rows.length > 0) { userRole = currentUser.rows[0].role; } @@ -164,7 +174,7 @@ async function getBot() { logger.error(`[TelegramBot] Error checking admin role for user ${userId}:`, roleCheckError); // В случае ошибки берем роль из базы или оставляем 'user' try { - const currentUser = await db.query('SELECT role FROM users WHERE id = $1', [userId]); + 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 */ } } @@ -181,7 +191,7 @@ async function getBot() { try { // Ищем сессию, где есть userId и она не истекла (проверка expires_at) // Сортируем по expires_at DESC чтобы взять самую "свежую", если их несколько - const sessionResult = await db.query( + const sessionResult = await db.getQuery()( `SELECT sid FROM session WHERE sess ->> 'userId' = $1 AND expire > NOW() @@ -195,7 +205,7 @@ async function getBot() { logger.info(`[telegramBot] Found active session ID ${activeSessionId} for user ${userId}`); // Обновляем найденную сессию в базе данных, добавляя/перезаписывая данные Telegram - const updateResult = await db.query( + const updateResult = await db.getQuery()( `UPDATE session SET sess = (sess::jsonb || $1::jsonb)::json WHERE sid = $2`, @@ -275,7 +285,7 @@ async function initTelegramAuth(session) { const guestId = session.guestId || tempId; // Связываем гостевой ID с текущим пользователем - 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`, @@ -298,9 +308,10 @@ async function initTelegramAuth(session) { `[initTelegramAuth] Created verification code for guestId: ${session.guestId || tempId}${session.authenticated ? `, userId: ${session.userId}` : ''}` ); + const settings = await getTelegramSettings(); return { verificationCode: code, - botLink: `https://t.me/${process.env.TELEGRAM_BOT_USERNAME}`, + botLink: `https://t.me/${settings.bot_username}`, }; } catch (error) { logger.error('Error initializing Telegram auth:', error); @@ -308,8 +319,13 @@ async function initTelegramAuth(session) { } } +function clearSettingsCache() { + telegramSettingsCache = null; +} + module.exports = { getBot, stopBot, initTelegramAuth, + clearSettingsCache, }; diff --git a/backend/services/verification-service.js b/backend/services/verification-service.js index 914c586..e309d63 100644 --- a/backend/services/verification-service.js +++ b/backend/services/verification-service.js @@ -29,14 +29,14 @@ class VerificationService { // Если userId не указан, добавляем запись без ссылки на пользователя if (userId === null || userId === undefined) { - await db.query( + await db.getQuery()( `INSERT INTO verification_codes (code, provider, provider_id, expires_at) VALUES ($1, $2, $3, $4)`, [code, provider, providerId, expiresAt] ); } else { - await db.query( + await db.getQuery()( `INSERT INTO verification_codes (code, provider, provider_id, user_id, expires_at) VALUES ($1, $2, $3, $4, $5)`, @@ -67,7 +67,7 @@ class VerificationService { logger.info(`Normalized code: ${normalizedCode}`); // Проверим, есть ли такой код в базе (для отладки) - const checkResult = await db.query( + const checkResult = await db.getQuery()( `SELECT code FROM verification_codes WHERE provider = $1 AND provider_id = $2 @@ -84,7 +84,7 @@ class VerificationService { logger.warn(`No active codes found for ${provider}:${providerId}`); } - const result = await db.query( + const result = await db.getQuery()( `SELECT * FROM verification_codes WHERE code = $1 AND provider = $2 @@ -104,7 +104,10 @@ class VerificationService { const verification = result.rows[0]; // Отмечаем код как использованный - await db.query('UPDATE verification_codes SET used = true WHERE id = $1', [verification.id]); + await db.getQuery()( + 'UPDATE verification_codes SET used = true WHERE id = $1', + [verification.id] + ); logger.info(`Code verified successfully for ${provider}:${providerId}`); return { @@ -126,7 +129,7 @@ class VerificationService { // Очистка истекших кодов async cleanupExpiredCodes() { try { - const result = await db.query( + const result = await db.getQuery()( 'DELETE FROM verification_codes WHERE expires_at <= NOW() RETURNING id' ); logger.info(`Cleaned up ${result.rowCount} expired verification codes`); diff --git a/backend/utils/helpers.js b/backend/utils/helpers.js index c74b365..cac5b7c 100644 --- a/backend/utils/helpers.js +++ b/backend/utils/helpers.js @@ -21,7 +21,7 @@ function generateVerificationCode(length = 6) { // Проверка существования идентификатора пользователя async function checkUserIdentity(userId, provider, providerId) { - const result = await db.query( + const result = await db.getQuery()( 'SELECT * FROM user_identities WHERE user_id = $1 AND provider = $2 AND provider_id = $3', [userId, provider, providerId] ); @@ -31,7 +31,7 @@ async function checkUserIdentity(userId, provider, providerId) { // Добавление новой идентификации async function addUserIdentity(userId, provider, providerId) { try { - await db.query( + await db.getQuery()( 'INSERT INTO user_identities (user_id, provider, provider_id) VALUES ($1, $2, $3)', [userId, provider, providerId] ); diff --git a/backend/yarn.lock b/backend/yarn.lock index 546fb98..fa8aae1 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -12,6 +12,11 @@ resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz#42cc67c5baa407ac25059fcd7d405cc5ecdb0c33" integrity sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg== +"@anthropic-ai/sdk@^0.51.0": + version "0.51.0" + resolved "https://registry.yarnpkg.com/@anthropic-ai/sdk/-/sdk-0.51.0.tgz#2cd022c47e0eb6f4d645d8e8ed9ee0f3c5745ea8" + integrity sha512-fAFC/uHhyzfw7rs65EPVV+scXDytGNm5BjttxHf6rP/YGvaBRKEvp2lwyuMigTwMI95neeG4bzrZigz7KCikjw== + "@colors/colors@1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" @@ -354,6 +359,16 @@ resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d" integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA== +"@google/genai@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@google/genai/-/genai-1.0.1.tgz#ccef337f733f27fdc2e5bf741af51889e2847082" + integrity sha512-qf8sq9vpuKUeBKukAn43z2eC1I/Jw63b9wo6O+1x3EIroF3oDouJOtW1AzwvfO+9gzCPfLjuCUONhMKiBC8vkQ== + dependencies: + google-auth-library "^9.14.2" + ws "^8.18.0" + zod "^3.22.4" + zod-to-json-schema "^3.22.4" + "@humanfs/core@^0.19.1": version "0.19.1" resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77" @@ -1223,6 +1238,11 @@ agent-base@6: dependencies: debug "4" +agent-base@^7.1.2: + version "7.1.3" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.3.tgz#29435eb821bc4194633a5b89e5bc4703bafc25a1" + integrity sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw== + agentkeepalive@^4.2.1: version "4.6.0" resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.6.0.tgz#35f73e94b3f40bf65f105219c623ad19c136ea6a" @@ -1504,7 +1524,7 @@ base-x@^3.0.2: dependencies: safe-buffer "^5.0.1" -base64-js@^1.5.1: +base64-js@^1.3.0, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -1516,6 +1536,11 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +bignumber.js@^9.0.0: + version "9.3.0" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.3.0.tgz#bdba7e2a4c1a2eba08290e8dcad4f36393c92acd" + integrity sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA== + binary-extensions@^2.0.0, binary-extensions@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" @@ -1671,6 +1696,11 @@ buffer-alloc@^1.2.0: buffer-alloc-unsafe "^1.1.0" buffer-fill "^1.0.0" +buffer-equal-constant-time@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + buffer-fill@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" @@ -2309,6 +2339,13 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" +ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -2842,7 +2879,7 @@ express@^4.21.2: utils-merge "1.0.1" vary "~1.1.2" -extend@~3.0.2: +extend@^3.0.2, extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -3117,6 +3154,26 @@ functions-have-names@^1.2.3: resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== +gaxios@^6.0.0, gaxios@^6.1.1: + version "6.7.1" + resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-6.7.1.tgz#ebd9f7093ede3ba502685e73390248bb5b7f71fb" + integrity sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ== + dependencies: + extend "^3.0.2" + https-proxy-agent "^7.0.1" + is-stream "^2.0.0" + node-fetch "^2.6.9" + uuid "^9.0.1" + +gcp-metadata@^6.1.0: + version "6.1.1" + resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-6.1.1.tgz#f65aa69f546bc56e116061d137d3f5f90bdec494" + integrity sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A== + dependencies: + gaxios "^6.1.1" + google-logging-utils "^0.0.2" + json-bigint "^1.0.0" + get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -3295,6 +3352,23 @@ globby@^10.0.1: merge2 "^1.2.3" slash "^3.0.0" +google-auth-library@^9.14.2: + version "9.15.1" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-9.15.1.tgz#0c5d84ed1890b2375f1cd74f03ac7b806b392928" + integrity sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng== + dependencies: + base64-js "^1.3.0" + ecdsa-sig-formatter "^1.0.11" + gaxios "^6.1.1" + gcp-metadata "^6.1.0" + gtoken "^7.0.0" + jws "^4.0.0" + +google-logging-utils@^0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/google-logging-utils/-/google-logging-utils-0.0.2.tgz#5fd837e06fa334da450433b9e3e1870c1594466a" + integrity sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ== + gopd@^1.0.1, gopd@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" @@ -3305,6 +3379,14 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== +gtoken@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-7.1.0.tgz#d61b4ebd10132222817f7222b1e6064bd463fc26" + integrity sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw== + dependencies: + gaxios "^6.0.0" + jws "^4.0.0" + handlebars@^4.0.1: version "4.7.8" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.8.tgz#41c42c18b1be2365439188c77c6afae71c0cd9e9" @@ -3550,6 +3632,14 @@ https-proxy-agent@^5.0.0: agent-base "6" debug "4" +https-proxy-agent@^7.0.1: + version "7.0.6" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9" + integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== + dependencies: + agent-base "^7.1.2" + debug "4" + humanize-ms@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" @@ -3951,6 +4041,13 @@ jsbn@~0.1.0: resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== +json-bigint@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" + integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== + dependencies: + bignumber.js "^9.0.0" + json-buffer@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" @@ -4027,6 +4124,23 @@ jsprim@^2.0.2: json-schema "0.4.0" verror "1.10.0" +jwa@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.1.tgz#bf8176d1ad0cd72e0f3f58338595a13e110bc804" + integrity sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg== + dependencies: + buffer-equal-constant-time "^1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4" + integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg== + dependencies: + jwa "^2.0.0" + safe-buffer "^5.0.1" + keccak@^3.0.0, keccak@^3.0.2: version "3.0.4" resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.4.tgz#edc09b89e633c0549da444432ecf062ffadee86d" @@ -4520,7 +4634,7 @@ node-emoji@^1.10.0: dependencies: lodash "^4.17.21" -node-fetch@^2.6.7, node-fetch@^2.7.0: +node-fetch@^2.6.7, node-fetch@^2.6.9, node-fetch@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== @@ -4663,6 +4777,19 @@ one-time@^1.0.0: dependencies: fn.name "1.x.x" +openai@^4.102.0: + version "4.102.0" + resolved "https://registry.yarnpkg.com/openai/-/openai-4.102.0.tgz#fcf09d2ad2b33eb399fe79bb43df1872bf2a15c8" + integrity sha512-CWk15CMhPSHNZnjz+6rwVYV551xaC8CwOd7/zxImrC1btEo37dX/Ii5tBKWfqqxqyzpJ6p3Y4bICzzKhW03WhQ== + dependencies: + "@types/node" "^18.11.18" + "@types/node-fetch" "^2.6.4" + abort-controller "^3.0.0" + agentkeepalive "^4.2.1" + form-data-encoder "1.7.2" + formdata-node "^4.3.2" + node-fetch "^2.6.7" + openai@^4.93.0: version "4.96.0" resolved "https://registry.yarnpkg.com/openai/-/openai-4.96.0.tgz#d1a821e99949ac2c55709f4e28e18bb1d9fd8ef9" @@ -6355,6 +6482,11 @@ uuid@^10.0.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294" integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ== +uuid@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== + v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" @@ -6595,7 +6727,7 @@ write-file-atomic@3.0.3: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" -ws@8.17.1, ws@8.18.1, ws@^7.4.6, ws@^8.18.1: +ws@8.17.1, ws@8.18.1, ws@^7.4.6, ws@^8.18.0, ws@^8.18.1: version "8.18.1" resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.1.tgz#ea131d3784e1dfdff91adb0a4a116b127515e3cb" integrity sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w== @@ -6653,7 +6785,7 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -zod-to-json-schema@^3.22.3, zod-to-json-schema@^3.22.5, zod-to-json-schema@^3.24.1: +zod-to-json-schema@^3.22.3, zod-to-json-schema@^3.22.4, zod-to-json-schema@^3.22.5, zod-to-json-schema@^3.24.1: version "3.24.5" resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz#d1095440b147fb7c2093812a53c54df8d5df50a3" integrity sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g== diff --git a/frontend/src/components/Sidebar.vue b/frontend/src/components/Sidebar.vue index 7e01257..e1d4192 100644 --- a/frontend/src/components/Sidebar.vue +++ b/frontend/src/components/Sidebar.vue @@ -142,7 +142,7 @@ const props = defineProps({ isLoadingTokens: Boolean }); -const emit = defineEmits(['update:modelValue', 'wallet-auth', 'disconnect-wallet', 'telegram-auth', 'email-auth']); +const emit = defineEmits(['update:modelValue', 'wallet-auth', 'disconnect-wallet', 'telegram-auth', 'email-auth', 'cancel-email-auth']); const { deleteIdentity } = useAuth(); diff --git a/frontend/src/components/identity/EmailConnect.vue b/frontend/src/components/identity/EmailConnect.vue index 3344b77..e4023c4 100644 --- a/frontend/src/components/identity/EmailConnect.vue +++ b/frontend/src/components/identity/EmailConnect.vue @@ -7,22 +7,22 @@
{{ showVerification ? 'Шаг 2 из 2' : 'Шаг 1 из 2' }}
-
+ -
+ {{ isLoading ? 'Отправка...' : 'Получить код' }} + +
+

Код отправлен на {{ email }}

Проверьте почту и введите код из письма
+ {{ isLoading ? 'Проверка...' : 'Подтвердить' }} +
{{ error }}
@@ -30,74 +30,74 @@ -
+ \ No newline at end of file diff --git a/frontend/src/views/settings/AiSettingsView.vue b/frontend/src/views/settings/AiSettingsView.vue index 9df1557..829d005 100644 --- a/frontend/src/views/settings/AiSettingsView.vue +++ b/frontend/src/views/settings/AiSettingsView.vue @@ -1,26 +1,97 @@ \ No newline at end of file diff --git a/frontend/src/views/settings/EmailSettingsView.vue b/frontend/src/views/settings/EmailSettingsView.vue index f30f925..cd9d90b 100644 --- a/frontend/src/views/settings/EmailSettingsView.vue +++ b/frontend/src/views/settings/EmailSettingsView.vue @@ -1,40 +1,115 @@ @@ -69,4 +144,43 @@ const saveEmailSettings = async () => { .save-btn:hover { background: var(--color-primary-dark); } +.cancel-btn { + background: #eee; + color: #333; + border: none; + border-radius: 6px; + padding: 0.5rem 1.5rem; + cursor: pointer; + font-size: 1rem; + margin-left: 1rem; +} +.settings-view { + display: flex; + flex-direction: column; + gap: 1.2rem; +} +.view-row { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 1rem; + background: #f8f8f8; + border-radius: 4px; + padding: 0.5rem 1rem; +} +.edit-btn { + background: var(--color-primary); + color: #fff; + border: none; + border-radius: 6px; + padding: 0.5rem 1.5rem; + cursor: pointer; + font-size: 1rem; + align-self: flex-end; + margin-top: 1.5rem; + transition: background 0.2s; +} +.edit-btn:hover { + background: var(--color-primary-dark); +} \ No newline at end of file diff --git a/frontend/src/views/settings/OllamaSettingsView.vue b/frontend/src/views/settings/OllamaSettingsView.vue new file mode 100644 index 0000000..5574879 --- /dev/null +++ b/frontend/src/views/settings/OllamaSettingsView.vue @@ -0,0 +1,63 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/settings/TelegramSettingsView.vue b/frontend/src/views/settings/TelegramSettingsView.vue index ed61063..b31ff25 100644 --- a/frontend/src/views/settings/TelegramSettingsView.vue +++ b/frontend/src/views/settings/TelegramSettingsView.vue @@ -1,7 +1,7 @@ @@ -59,4 +105,43 @@ const saveTelegramSettings = async () => { .save-btn:hover { background: var(--color-primary-dark); } +.cancel-btn { + background: #eee; + color: #333; + border: none; + border-radius: 6px; + padding: 0.5rem 1.5rem; + cursor: pointer; + font-size: 1rem; + margin-left: 1rem; +} +.settings-view { + display: flex; + flex-direction: column; + gap: 1.2rem; +} +.view-row { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 1rem; + background: #f8f8f8; + border-radius: 4px; + padding: 0.5rem 1rem; +} +.edit-btn { + background: var(--color-primary); + color: #fff; + border: none; + border-radius: 6px; + padding: 0.5rem 1.5rem; + cursor: pointer; + font-size: 1rem; + align-self: flex-end; + margin-top: 1.5rem; + transition: background 0.2s; +} +.edit-btn:hover { + background: var(--color-primary-dark); +} \ No newline at end of file