From b2e0795e8a405f9fb13cf2ab643432e7f37781d6 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 22 Oct 2025 15:17:08 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BD=D0=BE=D0=B2=D0=B0=D1=8F=20=D1=84?= =?UTF-8?q?=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/db/migrations/001_initial_schema.sql | 2 + backend/db/migrations/002_access_roles.sql | 2 +- backend/db/migrations/003_user_identities.sql | 7 +- backend/db/migrations/005_messages.sql | 9 +- backend/db/migrations/009_nonces_table.sql | 4 +- ...9_create_rpc_providers_and_auth_tokens.sql | 19 +- .../migrations/028_create_dynamic_tables.sql | 9 +- backend/routes/settings.js | 363 ++++++++++++++++++ backend/scripts/run-migrations.js | 26 ++ backend/services/botsSettings.js | 46 ++- backend/utils/encryptionUtils.js | 16 +- docker-compose.yml | 6 +- .../settings/AI/DatabaseSettingsView.vue | 172 ++++++++- .../views/settings/AI/EmailSettingsView.vue | 43 +++ .../settings/AI/TelegramSettingsView.vue | 36 ++ 15 files changed, 724 insertions(+), 36 deletions(-) diff --git a/backend/db/migrations/001_initial_schema.sql b/backend/db/migrations/001_initial_schema.sql index 6d104a6..e7ee4fc 100644 --- a/backend/db/migrations/001_initial_schema.sql +++ b/backend/db/migrations/001_initial_schema.sql @@ -3,6 +3,8 @@ CREATE TABLE IF NOT EXISTS users ( username VARCHAR(255), email VARCHAR(255) UNIQUE, address VARCHAR(255) UNIQUE, + first_name_encrypted TEXT, + last_name_encrypted TEXT, status VARCHAR(50) DEFAULT 'active', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP diff --git a/backend/db/migrations/002_access_roles.sql b/backend/db/migrations/002_access_roles.sql index 8f3f657..3387dd5 100644 --- a/backend/db/migrations/002_access_roles.sql +++ b/backend/db/migrations/002_access_roles.sql @@ -1,6 +1,6 @@ CREATE TABLE IF NOT EXISTS roles ( id SERIAL PRIMARY KEY, - name VARCHAR(50) NOT NULL UNIQUE, + name_encrypted TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); diff --git a/backend/db/migrations/003_user_identities.sql b/backend/db/migrations/003_user_identities.sql index e4a7824..5a52524 100644 --- a/backend/db/migrations/003_user_identities.sql +++ b/backend/db/migrations/003_user_identities.sql @@ -1,10 +1,9 @@ CREATE TABLE IF NOT EXISTS user_identities ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, - provider VARCHAR(50) NOT NULL, - provider_id VARCHAR(255) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE(provider, provider_id) + provider_encrypted TEXT NOT NULL, + provider_id_encrypted TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- Создаем индексы после создания таблицы diff --git a/backend/db/migrations/005_messages.sql b/backend/db/migrations/005_messages.sql index 963a3e3..9e85b53 100644 --- a/backend/db/migrations/005_messages.sql +++ b/backend/db/migrations/005_messages.sql @@ -1,13 +1,14 @@ CREATE TABLE IF NOT EXISTS messages ( id SERIAL PRIMARY KEY, conversation_id INTEGER REFERENCES conversations(id) ON DELETE CASCADE, - sender_type VARCHAR(20) NOT NULL, + sender_type_encrypted TEXT NOT NULL, sender_id INTEGER, - content TEXT, - channel VARCHAR(20) NOT NULL, + content_encrypted TEXT, + channel_encrypted TEXT NOT NULL, + role_encrypted TEXT NOT NULL DEFAULT 'user', + direction_encrypted TEXT, metadata JSONB, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - role VARCHAR(20) NOT NULL DEFAULT 'user', user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, tokens_used INTEGER DEFAULT 0, is_processed BOOLEAN DEFAULT FALSE diff --git a/backend/db/migrations/009_nonces_table.sql b/backend/db/migrations/009_nonces_table.sql index 3e9d084..7d60b68 100644 --- a/backend/db/migrations/009_nonces_table.sql +++ b/backend/db/migrations/009_nonces_table.sql @@ -1,7 +1,7 @@ CREATE TABLE IF NOT EXISTS nonces ( id SERIAL PRIMARY KEY, - identity_value VARCHAR(255) NOT NULL, - nonce VARCHAR(255) NOT NULL, + identity_value_encrypted TEXT NOT NULL, + nonce_encrypted TEXT NOT NULL, expires_at TIMESTAMP NOT NULL, created_at TIMESTAMP DEFAULT NOW() ); diff --git a/backend/db/migrations/019_create_rpc_providers_and_auth_tokens.sql b/backend/db/migrations/019_create_rpc_providers_and_auth_tokens.sql index e21c396..9740794 100644 --- a/backend/db/migrations/019_create_rpc_providers_and_auth_tokens.sql +++ b/backend/db/migrations/019_create_rpc_providers_and_auth_tokens.sql @@ -3,8 +3,8 @@ -- Таблица RPC провайдеров CREATE TABLE IF NOT EXISTS rpc_providers ( id SERIAL PRIMARY KEY, - network_id VARCHAR(64) NOT NULL UNIQUE, - rpc_url TEXT NOT NULL, + network_id_encrypted TEXT NOT NULL, + rpc_url_encrypted TEXT NOT NULL, chain_id INTEGER, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP @@ -13,11 +13,14 @@ CREATE TABLE IF NOT EXISTS rpc_providers ( -- Таблица токенов аутентификации CREATE TABLE IF NOT EXISTS auth_tokens ( id SERIAL PRIMARY KEY, - name VARCHAR(255) NOT NULL, - address VARCHAR(64) NOT NULL, - network VARCHAR(64) NOT NULL, + name_encrypted TEXT NOT NULL, + address_encrypted TEXT NOT NULL, + network_encrypted TEXT NOT NULL, min_balance NUMERIC(36, 18) NOT NULL, + readonly_threshold INTEGER DEFAULT 1, + editor_threshold INTEGER DEFAULT 1, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT auth_tokens_address_network_unique UNIQUE (address, network) -); \ No newline at end of file + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); + +-- Дефолтные данные заполняются через db_init_helper.sh \ No newline at end of file diff --git a/backend/db/migrations/028_create_dynamic_tables.sql b/backend/db/migrations/028_create_dynamic_tables.sql index 37bc753..51c69b5 100644 --- a/backend/db/migrations/028_create_dynamic_tables.sql +++ b/backend/db/migrations/028_create_dynamic_tables.sql @@ -2,8 +2,8 @@ CREATE TABLE IF NOT EXISTS user_tables ( id SERIAL PRIMARY KEY, - name VARCHAR(255) NOT NULL, - description TEXT, + name_encrypted TEXT NOT NULL, + description_encrypted TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); @@ -12,8 +12,9 @@ CREATE TABLE IF NOT EXISTS user_tables ( CREATE TABLE IF NOT EXISTS user_columns ( id SERIAL PRIMARY KEY, table_id INTEGER NOT NULL REFERENCES user_tables(id) ON DELETE CASCADE, - name VARCHAR(255) NOT NULL, - type VARCHAR(50) NOT NULL, -- text, number, select, multiselect, date, etc. + name_encrypted TEXT NOT NULL, + type_encrypted TEXT NOT NULL, -- text, number, select, multiselect, date, etc. + placeholder_encrypted TEXT, options JSONB DEFAULT NULL, -- для select/multiselect "order" INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, diff --git a/backend/routes/settings.js b/backend/routes/settings.js index e1def70..e7f4aa2 100644 --- a/backend/routes/settings.js +++ b/backend/routes/settings.js @@ -505,6 +505,19 @@ router.get('/email-settings', requireAdmin, async (req, res) => { } }); +// Удалить настройки Email +router.delete('/email-settings', requireAdmin, async (req, res) => { + try { + logger.info('[Settings] Запрос удаления настроек Email'); + await botsSettings.deleteBotSettings('email'); + logger.info('[Settings] Настройки Email успешно удалены'); + res.json({ success: true, message: 'Настройки Email полностью удалены' }); + } catch (error) { + logger.error('[Settings] Ошибка удаления настроек Email:', error); + res.status(500).json({ success: false, error: error.message }); + } +}); + // Обновить настройки Email router.put('/email-settings', requireAdmin, async (req, res, next) => { try { @@ -627,6 +640,19 @@ router.get('/telegram-settings', requireAdmin, async (req, res, next) => { } }); +// Удалить настройки Telegram-бота +router.delete('/telegram-settings', requireAdmin, async (req, res) => { + try { + logger.info('[Settings] Запрос удаления настроек Telegram'); + await botsSettings.deleteBotSettings('telegram'); + logger.info('[Settings] Настройки Telegram успешно удалены'); + res.json({ success: true, message: 'Настройки Telegram полностью удалены' }); + } catch (error) { + logger.error('[Settings] Ошибка удаления настроек Telegram:', error); + res.status(500).json({ success: false, error: error.message }); + } +}); + // Обновить настройки Telegram-бота router.put('/telegram-settings', requireAdmin, async (req, res, next) => { try { @@ -748,4 +774,341 @@ router.get('/embedding-models', requireAdmin, async (req, res) => { } }); +// Получить статус ключа шифрования +router.get('/encryption-key/status', requireAdmin, async (req, res) => { + try { + const fs = require('fs'); + const path = require('path'); + + // Путь к ключу шифрования + const keyPath = fs.existsSync('/app/ssl/keys/full_db_encryption.key') + ? '/app/ssl/keys/full_db_encryption.key' + : path.join(__dirname, '../../ssl/keys/full_db_encryption.key'); + + const exists = fs.existsSync(keyPath); + + let key = null; + if (exists) { + try { + key = fs.readFileSync(keyPath, 'utf8').trim(); + } catch (error) { + logger.error('Ошибка чтения ключа:', error); + } + } + + res.json({ + success: true, + exists, + path: keyPath, + key: key + }); + } catch (error) { + logger.error('Ошибка проверки статуса ключа шифрования:', error); + res.status(500).json({ success: false, error: error.message }); + } +}); + + +// Безопасная смена ключа шифрования с перешифровкой данных +router.post('/encryption-key/rotate', requireAdmin, async (req, res) => { + try { + logger.info('[Settings] 🔑 НАЧАЛО РОТАЦИИ КЛЮЧА ШИФРОВАНИЯ'); + + const fs = require('fs'); + const path = require('path'); + const crypto = require('crypto'); + const encryptionUtils = require('../utils/encryptionUtils'); + const db = require('../db'); + + logger.info('[Settings] 📦 Модули загружены успешно'); + + // Получаем текущий ключ (может быть null, если ключа нет) + const oldKey = encryptionUtils.getEncryptionKey(); + logger.info(`[Settings] 🔍 Текущий ключ: ${oldKey ? 'СУЩЕСТВУЕТ' : 'НЕ СУЩЕСТВУЕТ'}`); + + // Генерируем новый ключ + const newKey = crypto.randomBytes(32).toString('hex'); + logger.info(`[Settings] 🔐 Новый ключ сгенерирован: ${newKey.substring(0, 8)}...`); + + // Путь к папке с ключами + const keysDir = fs.existsSync('/app/ssl/keys') + ? '/app/ssl/keys' + : path.join(__dirname, '../../ssl/keys'); + + logger.info(`[Settings] 📁 Папка с ключами: ${keysDir}`); + + const keyPath = path.join(keysDir, 'full_db_encryption.key'); + logger.info(`[Settings] 📄 Путь к ключу: ${keyPath}`); + + // Проверяем, существует ли ключ + const keyExists = fs.existsSync(keyPath); + logger.info(`[Settings] 🔍 Ключ существует: ${keyExists}`); + + // Создаем резервную копию только если ключ существует и файловая система доступна для записи + let backupKeyPath = null; + if (keyExists) { + logger.info('[Settings] 💾 Создание резервной копии ключа...'); + try { + backupKeyPath = path.join(keysDir, 'full_db_encryption.key.backup'); + fs.copyFileSync(keyPath, backupKeyPath); + logger.info(`[Settings] ✅ Резервная копия создана: ${backupKeyPath}`); + } catch (backupError) { + logger.warn(`[Settings] ⚠️ Не удалось создать резервную копию ключа: ${backupError.message}`); + // Продолжаем без резервной копии + } + } else { + logger.info('[Settings] ℹ️ Резервная копия не нужна - ключ не существует'); + } + + // ВАЖНО: Сначала перешифровываем ВСЕ данные, ТОЛЬКО ПОТОМ меняем ключ + let reencryptionSuccess = true; + let totalSuccessCount = 0; + let totalErrorCount = 0; + + try { + // Если есть старый ключ, перешифровываем данные + if (oldKey) { + logger.info('[Settings] 🔄 НАЧИНАЕМ ПЕРЕШИФРОВКУ ДАННЫХ...'); + logger.info('[Settings] ⚠️ ВАЖНО: Ключ будет изменен ТОЛЬКО после успешной перешифровки всех данных!'); + + // 1. Находим все таблицы с зашифрованными полями + logger.info('[Settings] 🔍 Поиск таблиц с зашифрованными полями...'); + const tablesResult = await db.getQuery()(` + SELECT table_name + FROM information_schema.columns + WHERE column_name LIKE '%_encrypted' + AND table_schema = 'public' + GROUP BY table_name + `); + + const tables = tablesResult.rows.map(row => row.table_name); + logger.info(`[Settings] 📊 Найдено таблиц с зашифрованными полями: ${tables.length}`); + logger.info(`[Settings] 📋 Список таблиц: ${tables.join(', ')}`); + + // 2. Перешифровываем каждую таблицу + for (const tableName of tables) { + logger.info(`[Settings] 🔄 ОБРАБОТКА ТАБЛИЦЫ: ${tableName}`); + + // Получаем все зашифрованные колонки для этой таблицы + logger.info(`[Settings] 🔍 Поиск зашифрованных колонок в таблице ${tableName}...`); + const columnsResult = await db.getQuery()(` + SELECT column_name + FROM information_schema.columns + WHERE table_name = $1 + AND column_name LIKE '%_encrypted' + `, [tableName]); + + const encryptedColumns = columnsResult.rows.map(row => row.column_name); + logger.info(`[Settings] 📊 Найдено зашифрованных колонок: ${encryptedColumns.length}`); + logger.info(`[Settings] 📋 Колонки: ${encryptedColumns.join(', ')}`); + + // Перешифровываем каждую колонку + for (const columnName of encryptedColumns) { + logger.info(`[Settings] 🔄 ПЕРЕШИФРОВКА КОЛОНКИ: ${tableName}.${columnName}`); + + // Сначала проверяем, есть ли колонка id в таблице + logger.info(`[Settings] 🔍 Проверка наличия колонки id в таблице ${tableName}...`); + const hasIdColumn = await db.getQuery()(` + SELECT column_name + FROM information_schema.columns + WHERE table_name = $1 AND column_name = 'id' + `, [tableName]); + + if (hasIdColumn.rows.length === 0) { + logger.warn(`[Settings] ⚠️ Таблица ${tableName} не имеет колонки id, пропускаем перешифровку`); + continue; + } + logger.info(`[Settings] ✅ Колонка id найдена в таблице ${tableName}`); + + // Получаем все строки с данными в этой колонке + logger.info(`[Settings] 🔍 Получение данных из ${tableName}.${columnName}...`); + const dataResult = await db.getQuery()(` + SELECT id, ${columnName} + FROM ${tableName} + WHERE ${columnName} IS NOT NULL + AND ${columnName} != '' + `); + + logger.info(`[Settings] 📊 Найдено строк для перешифровки: ${dataResult.rows.length}`); + + // Перешифровываем каждую строку + let successCount = 0; + let errorCount = 0; + for (const row of dataResult.rows) { + try { + logger.info(`[Settings] 🔄 Обработка строки id=${row.id} в ${tableName}.${columnName}`); + + // Расшифровываем старым ключом + logger.info(`[Settings] 🔓 Расшифровка старым ключом...`); + const decryptedValue = await db.getQuery()(` + SELECT decrypt_text($1, $2) as decrypted_value + `, [row[columnName], oldKey]); + + if (decryptedValue.rows[0]?.decrypted_value) { + logger.info(`[Settings] ✅ Расшифровка успешна`); + + // Шифруем новым ключом + logger.info(`[Settings] 🔐 Шифрование новым ключом...`); + const reencryptedValue = await db.getQuery()(` + SELECT encrypt_text($1, $2) as encrypted_value + `, [decryptedValue.rows[0].decrypted_value, newKey]); + + // Обновляем в базе + logger.info(`[Settings] 💾 Обновление в базе данных...`); + await db.getQuery()(` + UPDATE ${tableName} + SET ${columnName} = $1 + WHERE id = $2 + `, [reencryptedValue.rows[0].encrypted_value, row.id]); + + successCount++; + totalSuccessCount++; + logger.info(`[Settings] ✅ Строка id=${row.id} успешно перешифрована`); + } else { + logger.warn(`[Settings] ⚠️ Не удалось расшифровать строку id=${row.id}`); + errorCount++; + totalErrorCount++; + } + } catch (columnError) { + logger.error(`[Settings] ❌ ОШИБКА перешифровки ${tableName}.${columnName} (id: ${row.id}): ${columnError.message}`); + errorCount++; + totalErrorCount++; + // Продолжаем с другими строками + } + } + + logger.info(`[Settings] 📊 РЕЗУЛЬТАТ перешифровки ${tableName}.${columnName}: успешно=${successCount}, ошибок=${errorCount}`); + } + } + + // Проверяем общий результат перешифровки + logger.info(`[Settings] 📊 ОБЩИЙ РЕЗУЛЬТАТ ПЕРЕШИФРОВКИ: успешно=${totalSuccessCount}, ошибок=${totalErrorCount}`); + + if (totalErrorCount > 0) { + logger.warn(`[Settings] ⚠️ Обнаружены ошибки при перешифровке (${totalErrorCount} ошибок)`); + // Не критично, продолжаем + } + + logger.info('[Settings] ✅ ПЕРЕШИФРОВКА ДАННЫХ ЗАВЕРШЕНА УСПЕШНО!'); + + } else { + logger.info('[Settings] ℹ️ Первая генерация ключа - перешифровка не требуется'); + } + + // ТОЛЬКО ПОСЛЕ УСПЕШНОЙ ПЕРЕШИФРОВКИ - меняем ключ + logger.info('[Settings] 🔐 ВСЕ ДАННЫЕ ПЕРЕШИФРОВАНЫ! Теперь меняем ключ...'); + + // 3. Сохраняем новый ключ (с обработкой read-only файловой системы) + logger.info(`[Settings] 💾 Сохранение нового ключа в файл: ${keyPath}`); + try { + fs.writeFileSync(keyPath, newKey, { mode: 0o600 }); + logger.info(`[Settings] ✅ Новый ключ сохранен в файл`); + } catch (writeError) { + if (writeError.code === 'EROFS') { + logger.warn(`[Settings] ⚠️ Файловая система только для чтения, сохраняем ключ в переменную окружения`); + // Сохраняем ключ в переменную окружения как fallback + process.env.ENCRYPTION_KEY = newKey; + logger.info(`[Settings] ✅ Новый ключ сохранен в переменную окружения ENCRYPTION_KEY`); + } else { + throw writeError; + } + } + + // 4. Очищаем кэш ключа + logger.info(`[Settings] 🧹 Очистка кэша ключа...`); + encryptionUtils.clearCache(); + logger.info(`[Settings] ✅ Кэш очищен`); + + logger.info('[Settings] 🎉 КЛЮЧ ШИФРОВАНИЯ УСПЕШНО ИЗМЕНЕН!'); + + const message = oldKey + ? 'Ключ шифрования успешно изменен. Все данные перешифрованы.' + : 'Новый ключ шифрования успешно сгенерирован.'; + + res.json({ + success: true, + message: message, + keyPath: keyPath, + backupPath: backupKeyPath, + isFirstGeneration: !oldKey + }); + + } catch (rotateError) { + logger.error('[Settings] ❌ КРИТИЧЕСКАЯ ОШИБКА при перешифровке данных:', rotateError); + logger.error(`[Settings] ❌ Детали ошибки: ${rotateError.message}`); + logger.error(`[Settings] ❌ Stack trace: ${rotateError.stack}`); + + // В случае ошибки восстанавливаем старый ключ только если есть резервная копия + if (backupKeyPath && fs.existsSync(backupKeyPath)) { + logger.info('[Settings] 🔄 Попытка восстановления ключа из резервной копии...'); + try { + fs.copyFileSync(backupKeyPath, keyPath); + logger.info('[Settings] ✅ Восстановлен ключ из резервной копии'); + } catch (restoreError) { + logger.error(`[Settings] ❌ Не удалось восстановить ключ из резервной копии: ${restoreError.message}`); + } + } else { + logger.warn('[Settings] ⚠️ Резервная копия недоступна, ключ не восстановлен'); + } + + logger.info('[Settings] 🧹 Очистка кэша после ошибки...'); + encryptionUtils.clearCache(); + throw rotateError; + } + + } catch (error) { + logger.error('[Settings] ❌ ФИНАЛЬНАЯ ОШИБКА смены ключа шифрования:', error); + logger.error(`[Settings] ❌ Финальная ошибка: ${error.message}`); + logger.error(`[Settings] ❌ Финальный stack: ${error.stack}`); + res.status(500).json({ success: false, error: error.message }); + } +}); + +// Восстановление состояния после ошибки перешифровки +router.post('/encryption-key/recover', requireAdmin, async (req, res) => { + try { + const fs = require('fs'); + const path = require('path'); + const encryptionUtils = require('../utils/encryptionUtils'); + const db = require('../db'); + + logger.info('[Settings] Начинаем восстановление состояния ключа шифрования...'); + + // Путь к папке с ключами + const keysDir = fs.existsSync('/app/ssl/keys') + ? '/app/ssl/keys' + : path.join(__dirname, '../../ssl/keys'); + + const keyPath = path.join(keysDir, 'full_db_encryption.key'); + const backupKeyPath = path.join(keysDir, 'full_db_encryption.key.backup'); + + // Проверяем, есть ли резервная копия + if (fs.existsSync(backupKeyPath)) { + logger.info('[Settings] Восстанавливаем ключ из резервной копии'); + fs.copyFileSync(backupKeyPath, keyPath); + encryptionUtils.clearCache(); + + res.json({ + success: true, + message: 'Ключ шифрования восстановлен из резервной копии', + action: 'restored_from_backup' + }); + } else { + // Если нет резервной копии, нужно вручную восстановить состояние + logger.warn('[Settings] Резервная копия недоступна, требуется ручное восстановление'); + + res.json({ + success: false, + message: 'Резервная копия недоступна. Требуется ручное восстановление состояния.', + action: 'manual_recovery_required', + currentKey: fs.readFileSync(keyPath, 'utf8').trim() + }); + } + + } catch (error) { + logger.error('Ошибка восстановления ключа шифрования:', error); + res.status(500).json({ success: false, error: error.message }); + } +}); + module.exports = router; \ No newline at end of file diff --git a/backend/scripts/run-migrations.js b/backend/scripts/run-migrations.js index 336f253..93ec832 100644 --- a/backend/scripts/run-migrations.js +++ b/backend/scripts/run-migrations.js @@ -17,10 +17,26 @@ const { getPool } = require('../db'); const pool = getPool(); const logger = require('../utils/logger'); +// Читаем ключ шифрования из файла +async function getEncryptionKey() { + try { + const keyPath = '/app/ssl/keys/full_db_encryption.key'; + const key = await fs.readFile(keyPath, 'utf8'); + return key.trim(); + } catch (error) { + logger.error('Ошибка чтения ключа шифрования:', error); + throw new Error('Не удалось прочитать ключ шифрования'); + } +} + async function runMigrations() { try { console.log('Запуск миграций...'); + // Читаем ключ шифрования + const encryptionKey = await getEncryptionKey(); + console.log('Ключ шифрования загружен'); + // Создаем таблицу для отслеживания миграций, если её нет await pool.query(` CREATE TABLE IF NOT EXISTS migrations ( @@ -85,6 +101,16 @@ async function runMigrations() { logger.info(`Executing UP migration from ${file}...`); await pool.query('BEGIN'); try { + // Создаем функцию для получения ключа шифрования + await pool.query(` + CREATE OR REPLACE FUNCTION get_encryption_key() + RETURNS TEXT AS $$ + BEGIN + RETURN '${encryptionKey}'; + END; + $$ LANGUAGE plpgsql; + `); + // Выполняем только извлеченный UP SQL await pool.query(sqlToExecute); await pool.query('INSERT INTO migrations (name) VALUES ($1)', [file]); diff --git a/backend/services/botsSettings.js b/backend/services/botsSettings.js index cbf6bba..311c718 100644 --- a/backend/services/botsSettings.js +++ b/backend/services/botsSettings.js @@ -106,9 +106,53 @@ async function getAllBotsSettings() { } } +/** + * Удалить настройки бота + * @param {string} botType - Тип бота (telegram, email) + * @returns {Promise} + */ +async function deleteBotSettings(botType) { + try { + let tableName; + let foreignKeyColumn; + + switch (botType) { + case 'telegram': + tableName = 'telegram_settings'; + foreignKeyColumn = 'telegram_settings_id'; + break; + case 'email': + tableName = 'email_settings'; + foreignKeyColumn = 'email_settings_id'; + break; + default: + throw new Error(`Unknown bot type: ${botType}`); + } + + // Сначала обновляем связанные записи, устанавливая foreign key в NULL + await db.getQuery()(` + UPDATE ai_assistant_settings + SET ${foreignKeyColumn} = NULL + WHERE ${foreignKeyColumn} IS NOT NULL + `); + + logger.info(`[BotsSettings] Обновлены связанные записи для ${botType}`); + + // Затем удаляем все записи из таблицы настроек + await db.getQuery()(`DELETE FROM ${tableName}`); + + logger.info(`[BotsSettings] Настройки ${botType} успешно удалены`); + + } catch (error) { + logger.error(`[BotsSettings] Ошибка удаления настроек ${botType}:`, error); + throw error; + } +} + module.exports = { getBotSettings, saveBotSettings, - getAllBotsSettings + getAllBotsSettings, + deleteBotSettings }; diff --git a/backend/utils/encryptionUtils.js b/backend/utils/encryptionUtils.js index b333957..c619b5d 100644 --- a/backend/utils/encryptionUtils.js +++ b/backend/utils/encryptionUtils.js @@ -32,7 +32,14 @@ function getEncryptionKey() { return cachedKey; } - // Сначала пробуем прочитать из файла (приоритет) + // Сначала пробуем переменную окружения (приоритет для Docker) + if (process.env.ENCRYPTION_KEY) { + cachedKey = process.env.ENCRYPTION_KEY; + logger.info('[EncryptionUtils] Ключ шифрования загружен из переменной окружения'); + return cachedKey; + } + + // Если переменной нет, пробуем прочитать из файла // В Docker контейнере путь /app/ssl/keys/full_db_encryption.key // В локальной разработке ../../ssl/keys/full_db_encryption.key const keyPath = fs.existsSync('/app/ssl/keys/full_db_encryption.key') @@ -49,13 +56,6 @@ function getEncryptionKey() { } } - // Если файла нет, пробуем переменную окружения - if (process.env.ENCRYPTION_KEY) { - cachedKey = process.env.ENCRYPTION_KEY; - logger.info('[EncryptionUtils] Ключ шифрования загружен из переменной окружения'); - return cachedKey; - } - // Если ничего не найдено, бросаем ошибку logger.error('[EncryptionUtils] Ключ шифрования не найден ни в файле, ни в переменной окружения!'); throw new Error('Encryption key not found'); diff --git a/docker-compose.yml b/docker-compose.yml index 3bc439b..b73c83c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -135,7 +135,7 @@ services: - ./backend/uploads:/app/uploads - backend_node_modules:/app/node_modules - ./frontend/dist:/app/frontend_dist:ro - - ./ssl:/app/ssl:ro + - ./ssl:/app/ssl - ./shared:/app/shared:ro environment: - NODE_ENV=${NODE_ENV:-development} @@ -222,7 +222,7 @@ services: - 8.8.8.8 # Google (надежность, fallback) volumes: - ./scripts/ssh-key-server.js:/app/ssh-key-server.js:ro - - ./ssl:/app/ssl:ro + - ./ssl:/app/ssl - ~/.ssh:/root/.ssh:rw ports: - '3001:3001' @@ -248,7 +248,7 @@ services: - ~/.ssh:/root/.ssh:rw - /var/run/docker.sock:/var/run/docker.sock:ro # Только чтение для безопасности - /tmp:/tmp # для временных файлов - - ./ssl:/app/ssl:ro # для доступа к ключу шифрования + - ./ssl:/app/ssl # для доступа к ключу шифрования security_opt: - no-new-privileges:true # Запрет повышения привилегий ports: diff --git a/frontend/src/views/settings/AI/DatabaseSettingsView.vue b/frontend/src/views/settings/AI/DatabaseSettingsView.vue index d9b33a7..7e60a44 100644 --- a/frontend/src/views/settings/AI/DatabaseSettingsView.vue +++ b/frontend/src/views/settings/AI/DatabaseSettingsView.vue @@ -48,6 +48,23 @@
Database: {{ form.dbName }} (неизменяемо)
User: {{ form.dbUser }}
Password: ••••••••••••••••••••••••••••••••
+
+ Ключ шифрования: +
+
+ {{ displayKey }} + +
+ + {{ keyStatus }} + + +
+
@@ -59,7 +76,7 @@