feat: новая функция

This commit is contained in:
2025-10-22 15:17:08 +03:00
parent 0cbc31812a
commit b2e0795e8a
15 changed files with 724 additions and 36 deletions

View File

@@ -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

View File

@@ -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
);

View File

@@ -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
);
-- Создаем индексы после создания таблицы

View File

@@ -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

View File

@@ -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()
);

View File

@@ -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)
);
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Дефолтные данные заполняются через db_init_helper.sh

View File

@@ -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,

View File

@@ -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;

View File

@@ -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]);

View File

@@ -106,9 +106,53 @@ async function getAllBotsSettings() {
}
}
/**
* Удалить настройки бота
* @param {string} botType - Тип бота (telegram, email)
* @returns {Promise<void>}
*/
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
};

View File

@@ -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');