feat: новая функция
This commit is contained in:
@@ -3,6 +3,8 @@ CREATE TABLE IF NOT EXISTS users (
|
|||||||
username VARCHAR(255),
|
username VARCHAR(255),
|
||||||
email VARCHAR(255) UNIQUE,
|
email VARCHAR(255) UNIQUE,
|
||||||
address VARCHAR(255) UNIQUE,
|
address VARCHAR(255) UNIQUE,
|
||||||
|
first_name_encrypted TEXT,
|
||||||
|
last_name_encrypted TEXT,
|
||||||
status VARCHAR(50) DEFAULT 'active',
|
status VARCHAR(50) DEFAULT 'active',
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
CREATE TABLE IF NOT EXISTS roles (
|
CREATE TABLE IF NOT EXISTS roles (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
name VARCHAR(50) NOT NULL UNIQUE,
|
name_encrypted TEXT NOT NULL,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
CREATE TABLE IF NOT EXISTS user_identities (
|
CREATE TABLE IF NOT EXISTS user_identities (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||||
provider VARCHAR(50) NOT NULL,
|
provider_encrypted TEXT NOT NULL,
|
||||||
provider_id VARCHAR(255) NOT NULL,
|
provider_id_encrypted TEXT NOT NULL,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
UNIQUE(provider, provider_id)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Создаем индексы после создания таблицы
|
-- Создаем индексы после создания таблицы
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
CREATE TABLE IF NOT EXISTS messages (
|
CREATE TABLE IF NOT EXISTS messages (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
conversation_id INTEGER REFERENCES conversations(id) ON DELETE CASCADE,
|
conversation_id INTEGER REFERENCES conversations(id) ON DELETE CASCADE,
|
||||||
sender_type VARCHAR(20) NOT NULL,
|
sender_type_encrypted TEXT NOT NULL,
|
||||||
sender_id INTEGER,
|
sender_id INTEGER,
|
||||||
content TEXT,
|
content_encrypted TEXT,
|
||||||
channel VARCHAR(20) NOT NULL,
|
channel_encrypted TEXT NOT NULL,
|
||||||
|
role_encrypted TEXT NOT NULL DEFAULT 'user',
|
||||||
|
direction_encrypted TEXT,
|
||||||
metadata JSONB,
|
metadata JSONB,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
role VARCHAR(20) NOT NULL DEFAULT 'user',
|
|
||||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||||
tokens_used INTEGER DEFAULT 0,
|
tokens_used INTEGER DEFAULT 0,
|
||||||
is_processed BOOLEAN DEFAULT FALSE
|
is_processed BOOLEAN DEFAULT FALSE
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
CREATE TABLE IF NOT EXISTS nonces (
|
CREATE TABLE IF NOT EXISTS nonces (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
identity_value VARCHAR(255) NOT NULL,
|
identity_value_encrypted TEXT NOT NULL,
|
||||||
nonce VARCHAR(255) NOT NULL,
|
nonce_encrypted TEXT NOT NULL,
|
||||||
expires_at TIMESTAMP NOT NULL,
|
expires_at TIMESTAMP NOT NULL,
|
||||||
created_at TIMESTAMP DEFAULT NOW()
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
-- Таблица RPC провайдеров
|
-- Таблица RPC провайдеров
|
||||||
CREATE TABLE IF NOT EXISTS rpc_providers (
|
CREATE TABLE IF NOT EXISTS rpc_providers (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
network_id VARCHAR(64) NOT NULL UNIQUE,
|
network_id_encrypted TEXT NOT NULL,
|
||||||
rpc_url TEXT NOT NULL,
|
rpc_url_encrypted TEXT NOT NULL,
|
||||||
chain_id INTEGER,
|
chain_id INTEGER,
|
||||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_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 (
|
CREATE TABLE IF NOT EXISTS auth_tokens (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
name VARCHAR(255) NOT NULL,
|
name_encrypted TEXT NOT NULL,
|
||||||
address VARCHAR(64) NOT NULL,
|
address_encrypted TEXT NOT NULL,
|
||||||
network VARCHAR(64) NOT NULL,
|
network_encrypted TEXT NOT NULL,
|
||||||
min_balance NUMERIC(36, 18) 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,
|
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||||
CONSTRAINT auth_tokens_address_network_unique UNIQUE (address, network)
|
);
|
||||||
);
|
|
||||||
|
-- Дефолтные данные заполняются через db_init_helper.sh
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS user_tables (
|
CREATE TABLE IF NOT EXISTS user_tables (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
name VARCHAR(255) NOT NULL,
|
name_encrypted TEXT NOT NULL,
|
||||||
description TEXT,
|
description_encrypted TEXT,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_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 (
|
CREATE TABLE IF NOT EXISTS user_columns (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
table_id INTEGER NOT NULL REFERENCES user_tables(id) ON DELETE CASCADE,
|
table_id INTEGER NOT NULL REFERENCES user_tables(id) ON DELETE CASCADE,
|
||||||
name VARCHAR(255) NOT NULL,
|
name_encrypted TEXT NOT NULL,
|
||||||
type VARCHAR(50) NOT NULL, -- text, number, select, multiselect, date, etc.
|
type_encrypted TEXT NOT NULL, -- text, number, select, multiselect, date, etc.
|
||||||
|
placeholder_encrypted TEXT,
|
||||||
options JSONB DEFAULT NULL, -- для select/multiselect
|
options JSONB DEFAULT NULL, -- для select/multiselect
|
||||||
"order" INTEGER DEFAULT 0,
|
"order" INTEGER DEFAULT 0,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|||||||
@@ -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
|
// Обновить настройки Email
|
||||||
router.put('/email-settings', requireAdmin, async (req, res, next) => {
|
router.put('/email-settings', requireAdmin, async (req, res, next) => {
|
||||||
try {
|
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-бота
|
// Обновить настройки Telegram-бота
|
||||||
router.put('/telegram-settings', requireAdmin, async (req, res, next) => {
|
router.put('/telegram-settings', requireAdmin, async (req, res, next) => {
|
||||||
try {
|
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;
|
module.exports = router;
|
||||||
@@ -17,10 +17,26 @@ const { getPool } = require('../db');
|
|||||||
const pool = getPool();
|
const pool = getPool();
|
||||||
const logger = require('../utils/logger');
|
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() {
|
async function runMigrations() {
|
||||||
try {
|
try {
|
||||||
console.log('Запуск миграций...');
|
console.log('Запуск миграций...');
|
||||||
|
|
||||||
|
// Читаем ключ шифрования
|
||||||
|
const encryptionKey = await getEncryptionKey();
|
||||||
|
console.log('Ключ шифрования загружен');
|
||||||
|
|
||||||
// Создаем таблицу для отслеживания миграций, если её нет
|
// Создаем таблицу для отслеживания миграций, если её нет
|
||||||
await pool.query(`
|
await pool.query(`
|
||||||
CREATE TABLE IF NOT EXISTS migrations (
|
CREATE TABLE IF NOT EXISTS migrations (
|
||||||
@@ -85,6 +101,16 @@ async function runMigrations() {
|
|||||||
logger.info(`Executing UP migration from ${file}...`);
|
logger.info(`Executing UP migration from ${file}...`);
|
||||||
await pool.query('BEGIN');
|
await pool.query('BEGIN');
|
||||||
try {
|
try {
|
||||||
|
// Создаем функцию для получения ключа шифрования
|
||||||
|
await pool.query(`
|
||||||
|
CREATE OR REPLACE FUNCTION get_encryption_key()
|
||||||
|
RETURNS TEXT AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN '${encryptionKey}';
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
`);
|
||||||
|
|
||||||
// Выполняем только извлеченный UP SQL
|
// Выполняем только извлеченный UP SQL
|
||||||
await pool.query(sqlToExecute);
|
await pool.query(sqlToExecute);
|
||||||
await pool.query('INSERT INTO migrations (name) VALUES ($1)', [file]);
|
await pool.query('INSERT INTO migrations (name) VALUES ($1)', [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 = {
|
module.exports = {
|
||||||
getBotSettings,
|
getBotSettings,
|
||||||
saveBotSettings,
|
saveBotSettings,
|
||||||
getAllBotsSettings
|
getAllBotsSettings,
|
||||||
|
deleteBotSettings
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,14 @@ function getEncryptionKey() {
|
|||||||
return cachedKey;
|
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
|
// В Docker контейнере путь /app/ssl/keys/full_db_encryption.key
|
||||||
// В локальной разработке ../../ssl/keys/full_db_encryption.key
|
// В локальной разработке ../../ssl/keys/full_db_encryption.key
|
||||||
const keyPath = fs.existsSync('/app/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] Ключ шифрования не найден ни в файле, ни в переменной окружения!');
|
logger.error('[EncryptionUtils] Ключ шифрования не найден ни в файле, ни в переменной окружения!');
|
||||||
throw new Error('Encryption key not found');
|
throw new Error('Encryption key not found');
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ services:
|
|||||||
- ./backend/uploads:/app/uploads
|
- ./backend/uploads:/app/uploads
|
||||||
- backend_node_modules:/app/node_modules
|
- backend_node_modules:/app/node_modules
|
||||||
- ./frontend/dist:/app/frontend_dist:ro
|
- ./frontend/dist:/app/frontend_dist:ro
|
||||||
- ./ssl:/app/ssl:ro
|
- ./ssl:/app/ssl
|
||||||
- ./shared:/app/shared:ro
|
- ./shared:/app/shared:ro
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=${NODE_ENV:-development}
|
- NODE_ENV=${NODE_ENV:-development}
|
||||||
@@ -222,7 +222,7 @@ services:
|
|||||||
- 8.8.8.8 # Google (надежность, fallback)
|
- 8.8.8.8 # Google (надежность, fallback)
|
||||||
volumes:
|
volumes:
|
||||||
- ./scripts/ssh-key-server.js:/app/ssh-key-server.js:ro
|
- ./scripts/ssh-key-server.js:/app/ssh-key-server.js:ro
|
||||||
- ./ssl:/app/ssl:ro
|
- ./ssl:/app/ssl
|
||||||
- ~/.ssh:/root/.ssh:rw
|
- ~/.ssh:/root/.ssh:rw
|
||||||
ports:
|
ports:
|
||||||
- '3001:3001'
|
- '3001:3001'
|
||||||
@@ -248,7 +248,7 @@ services:
|
|||||||
- ~/.ssh:/root/.ssh:rw
|
- ~/.ssh:/root/.ssh:rw
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro # Только чтение для безопасности
|
- /var/run/docker.sock:/var/run/docker.sock:ro # Только чтение для безопасности
|
||||||
- /tmp:/tmp # для временных файлов
|
- /tmp:/tmp # для временных файлов
|
||||||
- ./ssl:/app/ssl:ro # для доступа к ключу шифрования
|
- ./ssl:/app/ssl # для доступа к ключу шифрования
|
||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true # Запрет повышения привилегий
|
- no-new-privileges:true # Запрет повышения привилегий
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -48,6 +48,23 @@
|
|||||||
<div class="view-row"><span>Database:</span> <b>{{ form.dbName }}</b> <span class="readonly-badge">(неизменяемо)</span></div>
|
<div class="view-row"><span>Database:</span> <b>{{ form.dbName }}</b> <span class="readonly-badge">(неизменяемо)</span></div>
|
||||||
<div class="view-row"><span>User:</span> <b>{{ form.dbUser }}</b></div>
|
<div class="view-row"><span>User:</span> <b>{{ form.dbUser }}</b></div>
|
||||||
<div class="view-row"><span>Password:</span> <b>••••••••••••••••••••••••••••••••</b></div>
|
<div class="view-row"><span>Password:</span> <b>••••••••••••••••••••••••••••••••</b></div>
|
||||||
|
<div class="view-row encryption-key-row">
|
||||||
|
<span>Ключ шифрования:</span>
|
||||||
|
<div class="encryption-key-inline">
|
||||||
|
<div class="encryption-key-field">
|
||||||
|
<span class="key-display">{{ displayKey }}</span>
|
||||||
|
<button type="button" class="eye-btn" @click="toggleKeyVisibility" v-if="encryptionKeyState.exists">
|
||||||
|
{{ showKey ? '👁️' : '👁️🗨️' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<span class="key-status" :class="keyStatusClass">
|
||||||
|
{{ keyStatus }}
|
||||||
|
</span>
|
||||||
|
<button type="button" class="generate-key-btn" @click="generateNewEncryptionKey">
|
||||||
|
{{ buttonText }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button type="button" class="edit-btn" @click="editMode = true">Изменить</button>
|
<button type="button" class="edit-btn" @click="editMode = true">Изменить</button>
|
||||||
<button type="button" class="cancel-btn" @click="goBack">Закрыть</button>
|
<button type="button" class="cancel-btn" @click="goBack">Закрыть</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -59,7 +76,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import BaseLayout from '@/components/BaseLayout.vue';
|
import BaseLayout from '@/components/BaseLayout.vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { reactive, ref, onMounted } from 'vue';
|
import { reactive, ref, onMounted, nextTick, computed, watch } from 'vue';
|
||||||
import api from '@/api/axios';
|
import api from '@/api/axios';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -74,6 +91,36 @@ const form = reactive({
|
|||||||
});
|
});
|
||||||
const original = reactive({});
|
const original = reactive({});
|
||||||
const editMode = ref(false);
|
const editMode = ref(false);
|
||||||
|
const encryptionKeyState = reactive({ exists: false, key: null });
|
||||||
|
const showKey = ref(false);
|
||||||
|
|
||||||
|
// Computed свойство для отображения статуса ключа
|
||||||
|
const keyStatus = computed(() => {
|
||||||
|
return encryptionKeyState.exists ? 'Настроен' : 'Не настроен';
|
||||||
|
});
|
||||||
|
|
||||||
|
const keyStatusClass = computed(() => {
|
||||||
|
return encryptionKeyState.exists ? 'key-exists' : 'key-missing';
|
||||||
|
});
|
||||||
|
|
||||||
|
const buttonText = computed(() => {
|
||||||
|
return encryptionKeyState.exists ? 'Сгенерировать новый' : 'Сгенерировать ключ';
|
||||||
|
});
|
||||||
|
|
||||||
|
const displayKey = computed(() => {
|
||||||
|
if (!encryptionKeyState.exists) return 'Ключ не найден';
|
||||||
|
if (!encryptionKeyState.key) return 'Ключ не загружен';
|
||||||
|
return showKey.value ? encryptionKeyState.key : '••••••••••••••••••••••••••••••••';
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleKeyVisibility = () => {
|
||||||
|
showKey.value = !showKey.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Watch для отслеживания изменений состояния ключа
|
||||||
|
watch(() => encryptionKeyState.exists, (newValue, oldValue) => {
|
||||||
|
console.log('encryptionKeyState.exists changed from', oldValue, 'to', newValue);
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
const loadDbSettings = async () => {
|
const loadDbSettings = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -92,8 +139,49 @@ const loadDbSettings = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const checkEncryptionKey = async () => {
|
||||||
|
try {
|
||||||
|
const res = await api.get('/settings/encryption-key/status');
|
||||||
|
console.log('Encryption key status response:', res.data);
|
||||||
|
encryptionKeyState.exists = res.data.exists;
|
||||||
|
encryptionKeyState.key = res.data.key; // Сохраняем ключ из API
|
||||||
|
console.log('encryptionKeyState.exists updated to:', encryptionKeyState.exists);
|
||||||
|
console.log('encryptionKeyState.key updated to:', encryptionKeyState.key);
|
||||||
|
console.log('encryptionKeyState.exists type:', typeof encryptionKeyState.exists);
|
||||||
|
console.log('encryptionKeyState.exists === true:', encryptionKeyState.exists === true);
|
||||||
|
|
||||||
|
// Принудительно обновляем DOM
|
||||||
|
await nextTick();
|
||||||
|
console.log('DOM updated after nextTick');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Ошибка проверки ключа шифрования:', e);
|
||||||
|
encryptionKeyState.exists = false;
|
||||||
|
encryptionKeyState.key = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateNewEncryptionKey = async () => {
|
||||||
|
try {
|
||||||
|
const confirmRotate = confirm('Сгенерировать новый ключ шифрования? Все зашифрованные данные будут безопасно перешифрованы новым ключом.');
|
||||||
|
if (!confirmRotate) return;
|
||||||
|
|
||||||
|
// Безопасная смена ключа (работает как для первой генерации, так и для смены)
|
||||||
|
const res = await api.post('/settings/encryption-key/rotate');
|
||||||
|
if (res.data.success) {
|
||||||
|
alert(res.data.message);
|
||||||
|
await checkEncryptionKey();
|
||||||
|
} else {
|
||||||
|
alert('Ошибка смены ключа шифрования');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Ошибка генерации ключа шифрования:', e);
|
||||||
|
alert('Ошибка генерации ключа шифрования');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadDbSettings();
|
await loadDbSettings();
|
||||||
|
await checkEncryptionKey();
|
||||||
editMode.value = false;
|
editMode.value = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -250,4 +338,86 @@ h2 {
|
|||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.encryption-key-row {
|
||||||
|
align-items: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.encryption-key-inline {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.encryption-key-field {
|
||||||
|
background: #f8f8f8;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
word-break: break-all;
|
||||||
|
max-width: 300px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-display {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eye-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.2em;
|
||||||
|
padding: 0.2rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eye-btn:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-status {
|
||||||
|
padding: 0.3rem 0.8rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-exists {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-missing {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-key-btn {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9em;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-key-btn:hover {
|
||||||
|
background: var(--color-primary-dark);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -66,6 +66,7 @@
|
|||||||
<div class="view-row"><span>IMAP Password:</span> <b>{{ form.imapPassword ? '••••••••' : 'Не установлен' }}</b></div>
|
<div class="view-row"><span>IMAP Password:</span> <b>{{ form.imapPassword ? '••••••••' : 'Не установлен' }}</b></div>
|
||||||
<div class="view-row"><span>From Email:</span> <b>{{ form.fromEmail }}</b></div>
|
<div class="view-row"><span>From Email:</span> <b>{{ form.fromEmail }}</b></div>
|
||||||
<button type="button" class="edit-btn" @click="editMode = true">Изменить</button>
|
<button type="button" class="edit-btn" @click="editMode = true">Изменить</button>
|
||||||
|
<button type="button" class="clear-btn" @click="clearEmailSettings">Очистить</button>
|
||||||
<button type="button" class="cancel-btn" @click="goBack">Закрыть</button>
|
<button type="button" class="cancel-btn" @click="goBack">Закрыть</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -180,6 +181,32 @@ const cancelEdit = () => {
|
|||||||
form.imapPassword = '';
|
form.imapPassword = '';
|
||||||
editMode.value = false;
|
editMode.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const clearEmailSettings = async () => {
|
||||||
|
const confirmClear = confirm('Внимание! Это действие полностью удалит все настройки Email из базы данных. Продолжить?');
|
||||||
|
if (!confirmClear) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.delete('/settings/email-settings');
|
||||||
|
alert('Настройки Email полностью удалены');
|
||||||
|
|
||||||
|
// Очищаем форму
|
||||||
|
form.smtpHost = '';
|
||||||
|
form.smtpPort = 465;
|
||||||
|
form.smtpUser = '';
|
||||||
|
form.smtpPassword = '';
|
||||||
|
form.imapHost = '';
|
||||||
|
form.imapPort = 993;
|
||||||
|
form.imapUser = '';
|
||||||
|
form.imapPassword = '';
|
||||||
|
form.fromEmail = '';
|
||||||
|
Object.assign(original, JSON.parse(JSON.stringify(form)));
|
||||||
|
editMode.value = false;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Ошибка удаления настроек Email:', e);
|
||||||
|
alert('Ошибка удаления настроек Email');
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -242,6 +269,22 @@ h2 {
|
|||||||
.save-btn:hover {
|
.save-btn:hover {
|
||||||
background: var(--color-primary-dark);
|
background: var(--color-primary-dark);
|
||||||
}
|
}
|
||||||
|
.clear-btn {
|
||||||
|
background: #dc3545;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-left: 1rem;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-btn:hover {
|
||||||
|
background: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
.cancel-btn {
|
.cancel-btn {
|
||||||
background: #eee;
|
background: #eee;
|
||||||
color: #333;
|
color: #333;
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
<div class="view-row"><span>Bot Token:</span> <b>••••••••••••••••••••••••••••••••••••••••</b></div>
|
<div class="view-row"><span>Bot Token:</span> <b>••••••••••••••••••••••••••••••••••••••••</b></div>
|
||||||
<div class="view-row"><span>Bot Username:</span> <b>{{ form.botUsername }}</b></div>
|
<div class="view-row"><span>Bot Username:</span> <b>{{ form.botUsername }}</b></div>
|
||||||
<button type="button" class="edit-btn" @click="editMode = true">Изменить</button>
|
<button type="button" class="edit-btn" @click="editMode = true">Изменить</button>
|
||||||
|
<button type="button" class="clear-btn" @click="clearTelegramSettings">Очистить</button>
|
||||||
<button type="button" class="cancel-btn" @click="goBack">Закрыть</button>
|
<button type="button" class="cancel-btn" @click="goBack">Закрыть</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -123,6 +124,25 @@ const cancelEdit = () => {
|
|||||||
form.botToken = '';
|
form.botToken = '';
|
||||||
editMode.value = false;
|
editMode.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const clearTelegramSettings = async () => {
|
||||||
|
const confirmClear = confirm('Внимание! Это действие полностью удалит все настройки Telegram из базы данных. Продолжить?');
|
||||||
|
if (!confirmClear) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.delete('/settings/telegram-settings');
|
||||||
|
alert('Настройки Telegram полностью удалены');
|
||||||
|
|
||||||
|
// Очищаем форму
|
||||||
|
form.botToken = '';
|
||||||
|
form.botUsername = '';
|
||||||
|
Object.assign(original, JSON.parse(JSON.stringify(form)));
|
||||||
|
editMode.value = false;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Ошибка удаления настроек Telegram:', e);
|
||||||
|
alert('Ошибка удаления настроек Telegram');
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -185,6 +205,22 @@ h2 {
|
|||||||
.save-btn:hover {
|
.save-btn:hover {
|
||||||
background: var(--color-primary-dark);
|
background: var(--color-primary-dark);
|
||||||
}
|
}
|
||||||
|
.clear-btn {
|
||||||
|
background: #dc3545;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-left: 1rem;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-btn:hover {
|
||||||
|
background: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
.cancel-btn {
|
.cancel-btn {
|
||||||
background: #eee;
|
background: #eee;
|
||||||
color: #333;
|
color: #333;
|
||||||
|
|||||||
Reference in New Issue
Block a user