ваше сообщение коммита

This commit is contained in:
2025-11-18 22:48:13 +03:00
parent 794cf1dcee
commit 970b53e5ba
15 changed files with 1297 additions and 337 deletions

View File

@@ -19,13 +19,17 @@ LABEL website="https://hb3-accelerator.com"
WORKDIR /app
# Устанавливаем системные зависимости для компиляции нативных модулей Node.js
RUN apt-get update && apt-get install -y \
python3 \
make \
g++ \
curl \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Устанавливаем базовые пакеты отдельно от компиляторов для большей надежности
RUN apt-get update && \
apt-get install -y --fix-missing \
python3 \
make \
curl \
ca-certificates \
openssh-client && \
apt-get install -y --fix-missing g++ || \
(sleep 10 && apt-get update && apt-get install -y --fix-missing g++) && \
rm -rf /var/lib/apt/lists/*
# Docker CLI НЕ устанавливаем - используем Docker Socket + dockerode SDK

View File

@@ -172,29 +172,29 @@ router.post('/verify', async (req, res) => {
return res.status(401).json({ success: false, error: 'Invalid nonce' });
}
// Получаем базовый URL из БД (домен VDS) или используем текущий хост из запроса
const consentService = require('../services/consentService');
const baseUrl = await consentService.getBaseUrl();
// Если домена нет в БД, используем текущий хост из запроса (более надежно, чем origin)
let baseUrlForResources;
if (baseUrl !== 'http://localhost:9000') {
// Домен есть в БД - используем его
baseUrlForResources = baseUrl;
} else {
// Домена нет в БД - используем текущий хост из запроса
const protocol = req.protocol || 'http';
let host = req.get('host') || 'localhost:9000';
// Убеждаемся, что порт присутствует для localhost
if (host === 'localhost' || host.startsWith('localhost:')) {
if (!host.includes(':')) {
// Если порта нет, добавляем стандартный порт для протокола
const defaultPort = protocol === 'https' ? '443' : '9000';
host = `${host}:${defaultPort}`;
}
// ВАЖНО: Для SIWE сообщения ВСЕГДА используем хост из запроса, чтобы он совпадал с фронтендом
// Фронтенд использует window.location.host и window.location.origin, поэтому бэкенд должен использовать то же самое
// Это означает, что даже если в БД есть домен (например, 185.221.214.140), для SIWE будет использоваться
// хост из текущего запроса (например, localhost:9000), если запрос приходит с localhost
const protocol = req.protocol || 'http';
let host = req.get('host') || 'localhost:9000';
logger.info(`[verify] Request protocol: ${protocol}, host header: ${req.get('host')}, original host: ${host}`);
// Убеждаемся, что порт присутствует для localhost
if (host === 'localhost' || host.startsWith('localhost:')) {
if (!host.includes(':')) {
// Если порта нет, добавляем стандартный порт для протокола
const defaultPort = protocol === 'https' ? '443' : '9000';
host = `${host}:${defaultPort}`;
logger.info(`[verify] Added default port to localhost: ${host}`);
}
baseUrlForResources = `${protocol}://${host}`;
}
// Формируем domain и origin для SIWE сообщения из текущего запроса
// domain - это host (например, "localhost:9000" или "example.com:443")
// ВАЖНО: domain и origin для SIWE НИКОГДА не берутся из БД, только из запроса!
const baseUrlForResources = `${protocol}://${host}`;
// Извлекаем домен и origin из baseUrlForResources для SIWE сообщения
const baseUrlObj = new URL(baseUrlForResources);

View File

@@ -23,6 +23,40 @@ const encryptedDb = require('../services/encryptedDatabaseService');
const execAsync = promisify(exec);
/**
* Сохранить настройки VDS в базу данных
* @param {Object} settings - Объект с настройками для сохранения
* @returns {Promise<Object>} - Сохраненная запись
*/
async function saveVdsSettingsToDb(settings) {
// Проверяем существующие настройки
const existing = await encryptedDb.getData('vds_settings', {}, 1);
if (existing.length > 0) {
// UPDATE существующей записи
return await encryptedDb.saveData('vds_settings', settings, { id: existing[0].id });
} else {
// INSERT новой записи
return await encryptedDb.saveData('vds_settings', {
...settings,
created_at: new Date()
});
}
}
/**
* Обновить домен в process.env и сбросить кэш
* @param {string} domain - Домен для установки
*/
function updateDomainCache(domain) {
// Обновляем process.env.BASE_URL для текущего процесса
process.env.BASE_URL = `https://${domain}`;
// Сбрасываем кэш домена в consentService
const consentService = require('../services/consentService');
consentService.clearDomainCache();
}
/**
* Получить настройки VDS
*/
@@ -31,42 +65,58 @@ router.get('/settings', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTIN
const encryptionUtils = require('../utils/encryptionUtils');
const encryptionKey = encryptionUtils.getEncryptionKey();
const { rows } = await db.getQuery()(
`SELECT
id,
decrypt_text(domain_encrypted, $1) as domain,
decrypt_text(email_encrypted, $1) as email,
decrypt_text(ubuntu_user_encrypted, $1) as ubuntu_user,
decrypt_text(docker_user_encrypted, $1) as docker_user,
decrypt_text(ssh_host_encrypted, $1) as ssh_host,
ssh_port,
decrypt_text(ssh_user_encrypted, $1) as ssh_user,
decrypt_text(ssh_password_encrypted, $1) as ssh_password,
created_at,
updated_at
FROM vds_settings
ORDER BY id DESC
LIMIT 1`,
[encryptionKey]
);
if (rows.length === 0) {
return res.json({ success: true, settings: null });
}
res.json({
success: true,
settings: {
domain: rows[0].domain,
email: rows[0].email,
ubuntuUser: rows[0].ubuntu_user,
dockerUser: rows[0].docker_user,
sshHost: rows[0].ssh_host,
sshPort: rows[0].ssh_port,
sshUser: rows[0].ssh_user
// sshPassword не возвращаем по соображениям безопасности
try {
const { rows } = await db.getQuery()(
`SELECT
id,
decrypt_text(domain_encrypted, $1) as domain,
decrypt_text(email_encrypted, $1) as email,
decrypt_text(ubuntu_user_encrypted, $1) as ubuntu_user,
decrypt_text(docker_user_encrypted, $1) as docker_user,
decrypt_text(ssh_host_encrypted, $1) as ssh_host,
ssh_port,
decrypt_text(ssh_user_encrypted, $1) as ssh_user,
decrypt_text(ssh_password_encrypted, $1) as ssh_password,
created_at,
updated_at
FROM vds_settings
ORDER BY id DESC
LIMIT 1`,
[encryptionKey]
);
if (rows.length === 0) {
return res.json({ success: true, settings: null });
}
});
res.json({
success: true,
settings: {
domain: rows[0].domain,
email: rows[0].email,
ubuntuUser: rows[0].ubuntu_user,
dockerUser: rows[0].docker_user,
sshHost: rows[0].ssh_host,
sshPort: rows[0].ssh_port,
sshUser: rows[0].ssh_user
// sshPassword не возвращаем по соображениям безопасности
}
});
} catch (decryptError) {
// Если ошибка расшифровки (некорректные данные в БД), очищаем их и возвращаем null
if (decryptError.message && decryptError.message.includes('decoding base64')) {
logger.warn('[VDS] Ошибка расшифровки настроек (некорректные данные в БД). Очищаем некорректные данные из таблицы vds_settings.');
try {
// Автоматически очищаем некорректные данные из БД
await db.getQuery()('DELETE FROM vds_settings');
logger.info('[VDS] Некорректные настройки VDS удалены из таблицы vds_settings. Создайте новые настройки через интерфейс.');
} catch (deleteError) {
logger.error('[VDS] Ошибка при удалении некорректных настроек:', deleteError);
}
return res.json({ success: true, settings: null });
}
throw decryptError; // Пробрасываем другие ошибки
}
} catch (error) {
logger.error('[VDS] Ошибка получения настроек:', error);
res.status(500).json({ success: false, error: error.message });
@@ -92,27 +142,14 @@ router.post('/settings', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTI
// Если передан только домен (для обратной совместимости)
if (domain && !email && !sshHost) {
const normalizedDomain = domain.trim().toLowerCase().replace(/^https?:\/\//, '').replace(/\/$/, '');
const existing = await encryptedDb.getData('vds_settings', {}, 1);
const settings = {
domain_encrypted: normalizedDomain, // encryptedDb автоматически зашифрует
updated_at: new Date()
};
if (existing.length > 0) {
await encryptedDb.saveData('vds_settings', settings, { id: existing[0].id });
} else {
await encryptedDb.saveData('vds_settings', {
...settings,
created_at: new Date()
});
}
process.env.BASE_URL = `https://${normalizedDomain}`;
// Сбрасываем кэш домена в consentService
const consentService = require('../services/consentService');
consentService.clearDomainCache();
await saveVdsSettingsToDb(settings);
updateDomainCache(normalizedDomain);
logger.info(`[VDS] Домен сохранен: ${normalizedDomain}`);
return res.json({ success: true, domain: normalizedDomain });
@@ -129,12 +166,9 @@ router.post('/settings', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTI
// Нормализуем домен
const normalizedDomain = domain.trim().toLowerCase().replace(/^https?:\/\//, '').replace(/\/$/, '');
// Проверяем существующие настройки
// Проверяем существующие настройки (для валидации пароля)
const existing = await encryptedDb.getData('vds_settings', {}, 1);
const encryptionUtils = require('../utils/encryptionUtils');
const encryptionKey = encryptionUtils.getEncryptionKey();
// Подготавливаем данные для сохранения с правильными именами полей для шифрования
const settings = {
domain_encrypted: normalizedDomain, // encryptedDb автоматически зашифрует поля с _encrypted
@@ -159,21 +193,8 @@ router.post('/settings', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTI
}
// Если пароль не указан (undefined/null/пустая строка) и настройки уже есть - не обновляем пароль
if (existing.length > 0) {
await encryptedDb.saveData('vds_settings', settings, { id: existing[0].id });
} else {
await encryptedDb.saveData('vds_settings', {
...settings,
created_at: new Date()
});
}
// Обновляем process.env.BASE_URL для текущего процесса
process.env.BASE_URL = `https://${normalizedDomain}`;
// Сбрасываем кэш домена в consentService
const consentService = require('../services/consentService');
consentService.clearDomainCache();
await saveVdsSettingsToDb(settings);
updateDomainCache(normalizedDomain);
logger.info(`[VDS] Настройки сохранены: ${normalizedDomain}`);
res.json({ success: true, settings });
@@ -195,21 +216,148 @@ async function checkDockerAvailable() {
}
}
/**
* Получить настройки VDS из базы данных
*/
async function getVdsSettings() {
try {
const encryptionUtils = require('../utils/encryptionUtils');
const encryptionKey = encryptionUtils.getEncryptionKey();
const { rows } = await db.getQuery()(
`SELECT
decrypt_text(domain_encrypted, $1) as domain,
decrypt_text(email_encrypted, $1) as email,
decrypt_text(ubuntu_user_encrypted, $1) as ubuntu_user,
decrypt_text(docker_user_encrypted, $1) as docker_user,
decrypt_text(ssh_host_encrypted, $1) as ssh_host,
ssh_port,
decrypt_text(ssh_user_encrypted, $1) as ssh_user,
decrypt_text(ssh_password_encrypted, $1) as ssh_password
FROM vds_settings
ORDER BY id DESC
LIMIT 1`,
[encryptionKey]
);
if (rows.length > 0 && rows[0].ssh_host && rows[0].ssh_user) {
return {
domain: rows[0].domain,
email: rows[0].email,
ubuntuUser: rows[0].ubuntu_user,
dockerUser: rows[0].docker_user,
sshHost: rows[0].ssh_host,
sshPort: rows[0].ssh_port || 22,
sshUser: rows[0].ssh_user,
sshPassword: rows[0].ssh_password
};
}
} catch (decryptError) {
// Если ошибка расшифровки (некорректные данные в БД), очищаем их и возвращаем null
if (decryptError.message && decryptError.message.includes('decoding base64')) {
logger.warn('[VDS] Ошибка расшифровки настроек (некорректные данные в БД). Очищаем некорректные данные из таблицы vds_settings.');
try {
// Автоматически очищаем некорректные данные из БД
await db.getQuery()('DELETE FROM vds_settings');
logger.info('[VDS] Некорректные настройки VDS удалены из таблицы vds_settings. Создайте новые настройки через интерфейс.');
} catch (deleteError) {
logger.error('[VDS] Ошибка при удалении некорректных настроек:', deleteError);
}
return null;
}
// Для других ошибок просто логируем
logger.warn('[VDS] Не удалось получить настройки VDS из базы данных:', decryptError.message);
}
return null;
}
/**
* Выполнить команду Docker (локально или через SSH на VDS)
*/
async function execDockerCommand(command) {
const vdsSettings = await getVdsSettings();
if (vdsSettings && vdsSettings.sshHost && vdsSettings.sshUser) {
// Выполняем через SSH на VDS
return await execSshCommandOnVds(command, vdsSettings);
} else {
// Выполняем локально
try {
const { stdout, stderr } = await execAsync(command);
return { code: 0, stdout, stderr };
} catch (error) {
return { code: error.code || 1, stdout: error.stdout || '', stderr: error.stderr || error.message };
}
}
}
/**
* Выполнить SSH команду на VDS
*/
async function execSshCommandOnVds(command, settings) {
const { sshHost, sshPort = 22, sshUser, sshPassword } = settings;
// Экранируем команду для SSH
// Экранируем двойные кавычки и знаки доллара для правильной передачи через SSH
const escapedCommand = command
.replace(/\\/g, '\\\\') // Сначала экранируем обратные слеши
.replace(/\$/g, '\\$') // Экранируем знаки доллара
.replace(/"/g, '\\"'); // Экранируем двойные кавычки
// Базовые опции SSH
const sshOptions = `-p ${sshPort} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR`;
// Строим SSH команду
let sshCommand;
if (sshPassword && sshPassword.trim()) {
// Используем sshpass для подключения с паролем (если пароль указан)
sshCommand = `sshpass -p "${sshPassword.replace(/"/g, '\\"')}" ssh ${sshOptions} ${sshUser}@${sshHost} "${escapedCommand}"`;
} else {
// Используем SSH ключи (по умолчанию из ~/.ssh/id_rsa или ~/.ssh/id_ed25519)
// SSH автоматически найдет ключ в ~/.ssh/
sshCommand = `ssh ${sshOptions} ${sshUser}@${sshHost} "${escapedCommand}"`;
}
try {
const { stdout, stderr } = await execAsync(sshCommand);
return { code: 0, stdout, stderr };
} catch (error) {
return { code: error.code || 1, stdout: error.stdout || '', stderr: error.stderr || error.message };
}
}
/**
* Получить список контейнеров
*/
router.get('/containers', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
const dockerAvailable = await checkDockerAvailable();
if (!dockerAvailable) {
return res.json({ success: true, containers: [], message: 'Docker недоступен (работает локально, не на VDS)' });
const vdsSettings = await getVdsSettings();
// Проверяем, есть ли настройки VDS или локальный Docker доступен
if (!vdsSettings && !(await checkDockerAvailable())) {
return res.json({
success: true,
containers: [],
message: 'VDS не настроена и Docker недоступен локально'
});
}
const { stdout } = await execAsync('docker ps -a --format "{{.Names}}|{{.Status}}|{{.Image}}"');
const containers = stdout.trim().split('\n').filter(line => line.trim()).map(line => {
const [name, status, image] = line.split('|');
return { name, status, image };
});
const result = await execDockerCommand('docker ps -a --format "{{.Names}}|{{.Status}}|{{.Image}}"');
if (result.code !== 0) {
logger.error(`[VDS] Ошибка выполнения Docker команды: ${result.stderr}`);
return res.status(500).json({
success: false,
error: `Не удалось получить список контейнеров: ${result.stderr}`
});
}
const containers = result.stdout.trim().split('\n')
.filter(line => line.trim())
.map(line => {
const [name, status, image] = line.split('|');
return { name, status, image };
});
res.json({ success: true, containers });
} catch (error) {
@@ -229,7 +377,12 @@ router.post('/containers/:name/restart', requireAuth, requirePermission(PERMISSI
return res.status(400).json({ success: false, error: 'Имя контейнера обязательно' });
}
await execAsync(`docker restart ${name}`);
const result = await execDockerCommand(`docker restart ${name}`);
if (result.code !== 0) {
return res.status(500).json({ success: false, error: result.stderr || 'Не удалось перезапустить контейнер' });
}
logger.info(`[VDS] Контейнер ${name} перезапущен`);
res.json({ success: true, message: `Контейнер ${name} перезапущен` });
} catch (error) {
@@ -243,14 +396,24 @@ router.post('/containers/:name/restart', requireAuth, requirePermission(PERMISSI
*/
router.post('/containers/restart-all', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
const { stdout } = await execAsync('docker ps -q');
const containerIds = stdout.trim().split('\n').filter(id => id.trim());
const result = await execDockerCommand('docker ps -q');
if (result.code !== 0) {
return res.status(500).json({ success: false, error: result.stderr || 'Не удалось получить список контейнеров' });
}
const containerIds = result.stdout.trim().split('\n').filter(id => id.trim());
if (containerIds.length === 0) {
return res.json({ success: true, message: 'Нет запущенных контейнеров', restarted: 0 });
}
await execAsync(`docker restart ${containerIds.join(' ')}`);
const restartResult = await execDockerCommand(`docker restart ${containerIds.join(' ')}`);
if (restartResult.code !== 0) {
return res.status(500).json({ success: false, error: restartResult.stderr || 'Не удалось перезапустить контейнеры' });
}
logger.info(`[VDS] Перезапущено контейнеров: ${containerIds.length}`);
res.json({ success: true, message: `Перезапущено контейнеров: ${containerIds.length}`, restarted: containerIds.length });
} catch (error) {
@@ -271,24 +434,28 @@ router.post('/containers/:name/rebuild', requireAuth, requirePermission(PERMISSI
}
// Получаем информацию о контейнере
const { stdout: inspectOutput } = await execAsync(`docker inspect ${name} --format '{{.Config.Image}}'`);
const imageName = inspectOutput.trim();
const inspectResult = await execDockerCommand(`docker inspect ${name} --format '{{.Config.Image}}'`);
if (inspectResult.code !== 0) {
return res.status(404).json({ success: false, error: 'Контейнер не найден' });
}
const imageName = inspectResult.stdout.trim();
if (!imageName) {
return res.status(404).json({ success: false, error: 'Контейнер не найден' });
}
// Останавливаем контейнер
await execAsync(`docker stop ${name}`).catch(() => {});
await execDockerCommand(`docker stop ${name}`);
// Удаляем контейнер
await execAsync(`docker rm ${name}`).catch(() => {});
await execDockerCommand(`docker rm ${name}`);
// Пересобираем образ (если есть Dockerfile)
// Для простоты просто пересоздаем контейнер из образа
await execAsync(`docker run -d --name ${name} ${imageName}`).catch(() => {
throw new Error('Не удалось пересоздать контейнер. Возможно, нужны дополнительные параметры запуска.');
});
const runResult = await execDockerCommand(`docker run -d --name ${name} ${imageName}`);
if (runResult.code !== 0) {
return res.status(500).json({ success: false, error: 'Не удалось пересоздать контейнер. Возможно, нужны дополнительные параметры запуска.' });
}
logger.info(`[VDS] Контейнер ${name} пересобран`);
res.json({ success: true, message: `Контейнер ${name} пересобран` });
@@ -305,64 +472,146 @@ router.get('/stats', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS)
try {
const { period = '1h' } = req.query; // 1h, 6h, 24h, 7d
// Получаем текущую статистику CPU и RAM
// Получаем текущую статистику CPU и RAM с VDS сервера
let cpuUsage = 0;
let ramUsage = 0;
let ramTotal = 0;
let ramUsed = 0;
let totalTraffic = 0;
let rxBytes = 0;
let txBytes = 0;
try {
const { stdout: cpuRam } = await execAsync('top -bn1 | grep "Cpu(s)" | sed "s/.*, *\\([0-9.]*\\)%* id.*/\\1/" | awk \'{print 100 - $1}\'');
cpuUsage = parseFloat(cpuRam.trim()) || 0;
} catch (error) {
logger.warn('[VDS] Не удалось получить статистику CPU:', error.message);
}
try {
const { stdout: memInfo } = await execAsync('free -m | awk \'NR==2{printf "%.2f", $3*100/$2}\'');
ramUsage = parseFloat(memInfo.trim()) || 0;
} catch (error) {
logger.warn('[VDS] Не удалось получить статистику RAM:', error.message);
}
try {
// Используем /host/proc если доступно, иначе /proc
const procPath = require('fs').existsSync('/host/proc') ? '/host/proc' : '/proc';
const { stdout: networkStats } = await execAsync(`cat ${procPath}/net/dev | awk 'NR>2 {rx+=$2; tx+=$10} END {print rx, tx}'`);
[rxBytes, txBytes] = networkStats.trim().split(' ').map(Number);
totalTraffic = (rxBytes + txBytes) / 1024 / 1024; // MB
} catch (error) {
logger.warn('[VDS] Не удалось получить статистику трафика:', error.message);
}
let cpuCores = 0;
// Получаем статистику по контейнерам (если Docker доступен)
let containers = [];
const dockerAvailable = await checkDockerAvailable();
if (dockerAvailable) {
const vdsSettings = await getVdsSettings();
// Если есть настройки VDS, выполняем команды на VDS сервере
if (vdsSettings) {
try {
const { stdout: containerStats } = await execAsync('docker stats --no-stream --format "{{.Name}}|{{.CPUPerc}}|{{.MemUsage}}|{{.NetIO}}"');
containers = containerStats.trim().split('\n').filter(line => line.trim()).map(line => {
const [name, cpu, mem, net] = line.split('|');
return { name, cpu, mem, net };
});
// CPU usage - используем упрощенную команду через /proc/stat
// $ будет экранирован в execSshCommandOnVds
const procCpuResult = await execDockerCommand('head -n1 /proc/stat | awk \'{idle=$5+$6; total=$2+$3+$4+$5+$6+$7+$8+$9; if(total>0) print (100*(total-idle)/total); else print 0}\'');
if (procCpuResult.code === 0 && procCpuResult.stdout && procCpuResult.stdout.trim()) {
const parsed = parseFloat(procCpuResult.stdout.trim());
if (!isNaN(parsed) && parsed >= 0 && parsed <= 100) {
cpuUsage = parsed;
logger.info(`[VDS] CPU usage получен через /proc/stat: ${cpuUsage}%`);
} else {
throw new Error(`Invalid CPU value: ${parsed}`);
}
} else {
throw new Error(`Command failed: code=${procCpuResult.code}, stderr=${procCpuResult.stderr}`);
}
} catch (error) {
logger.warn('[VDS] Не удалось получить CPU через /proc/stat, пробуем top:', error.message);
try {
// Fallback: через top - упрощенная команда (idle обычно предпоследнее значение)
const cpuResult = await execDockerCommand('top -bn1 | grep "%Cpu(s)" | awk \'{print 100-$(NF-2)}\' | sed \'s/%//\'');
if (cpuResult.code === 0 && cpuResult.stdout && cpuResult.stdout.trim()) {
cpuUsage = parseFloat(cpuResult.stdout.trim()) || 0;
logger.info(`[VDS] CPU usage получен через top: ${cpuUsage}%`);
}
} catch (topError) {
logger.warn('[VDS] Не удалось получить CPU usage:', topError.message);
}
}
try {
// RAM usage и total - $ будет экранирован в execSshCommandOnVds
const memResult = await execDockerCommand('free -m | awk \'NR==2{usage=$3*100/$2; printf "%.2f %d %d", usage, $2, $3}\'');
if (memResult.code === 0 && memResult.stdout && memResult.stdout.trim()) {
const parts = memResult.stdout.trim().split(' ');
if (parts.length >= 3) {
ramUsage = parseFloat(parts[0]) || 0;
ramTotal = parseInt(parts[1]) || 0;
ramUsed = parseInt(parts[2]) || 0;
logger.info(`[VDS] RAM получена с VDS: usage=${ramUsage}%, total=${ramTotal}MB, used=${ramUsed}MB (raw: ${memResult.stdout.trim()})`);
} else {
logger.warn('[VDS] Неверный формат RAM данных:', memResult.stdout);
}
} else {
logger.warn('[VDS] Не удалось получить RAM, code:', memResult.code, 'stdout:', memResult.stdout, 'stderr:', memResult.stderr);
}
} catch (error) {
logger.warn('[VDS] Не удалось получить статистику RAM:', error.message);
}
try {
// CPU cores
const coresResult = await execDockerCommand('nproc');
if (coresResult.code === 0 && coresResult.stdout) {
cpuCores = parseInt(coresResult.stdout.trim()) || 0;
}
} catch (error) {
logger.warn('[VDS] Не удалось получить количество ядер CPU:', error.message);
}
try {
// Network traffic
const networkResult = await execDockerCommand('cat /proc/net/dev | awk \'NR>2 {rx+=$2; tx+=$10} END {print rx, tx}\'');
if (networkResult.code === 0 && networkResult.stdout) {
[rxBytes, txBytes] = networkResult.stdout.trim().split(' ').map(Number);
totalTraffic = (rxBytes + txBytes) / 1024 / 1024; // MB
}
} catch (error) {
logger.warn('[VDS] Не удалось получить статистику трафика:', error.message);
}
try {
// Docker containers stats
const result = await execDockerCommand('docker stats --no-stream --format "{{.Name}}|{{.CPUPerc}}|{{.MemUsage}}|{{.NetIO}}"');
if (result.code === 0 && result.stdout) {
containers = result.stdout.trim().split('\n').filter(line => line.trim()).map(line => {
const [name, cpu, mem, net] = line.split('|');
return { name, cpu, mem, net };
});
}
} catch (error) {
logger.warn('[VDS] Не удалось получить статистику контейнеров:', error.message);
// Продолжаем без статистики контейнеров
}
} else {
// Fallback: локальное выполнение (если VDS не настроена)
try {
const { stdout: cpuRam } = await execAsync('top -bn1 | grep "Cpu(s)" | sed "s/.*, *\\([0-9.]*\\)%* id.*/\\1/" | awk \'{print 100 - $1}\'');
cpuUsage = parseFloat(cpuRam.trim()) || 0;
} catch (error) {
logger.warn('[VDS] Не удалось получить статистику CPU:', error.message);
}
try {
const { stdout: memInfo } = await execAsync('free -m | awk \'NR==2{printf "%.2f %d %d", $3*100/$2, $2, $3}\'');
const [usage, total, used] = memInfo.trim().split(' ').map(Number);
ramUsage = usage || 0;
ramTotal = total || 0;
ramUsed = used || 0;
} catch (error) {
logger.warn('[VDS] Не удалось получить статистику RAM:', error.message);
}
try {
const procPath = require('fs').existsSync('/host/proc') ? '/host/proc' : '/proc';
const { stdout: networkStats } = await execAsync(`cat ${procPath}/net/dev | awk 'NR>2 {rx+=$2; tx+=$10} END {print rx, tx}'`);
[rxBytes, txBytes] = networkStats.trim().split(' ').map(Number);
totalTraffic = (rxBytes + txBytes) / 1024 / 1024; // MB
} catch (error) {
logger.warn('[VDS] Не удалось получить статистику трафика:', error.message);
}
cpuCores = require('os').cpus().length;
}
res.json({
const responseData = {
success: true,
stats: {
cpu: {
usage: cpuUsage,
cores: require('os').cpus().length
cores: cpuCores || require('os').cpus().length
},
ram: {
usage: ramUsage,
total: Math.round(require('os').totalmem() / 1024 / 1024), // MB
used: Math.round(require('os').totalmem() / 1024 / 1024 * ramUsage / 100) // MB
total: ramTotal || Math.round(require('os').totalmem() / 1024 / 1024), // MB
used: ramUsed || Math.round((ramTotal || require('os').totalmem() / 1024 / 1024) * ramUsage / 100) // MB
},
traffic: {
total: totalTraffic, // MB
@@ -372,7 +621,11 @@ router.get('/stats', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS)
containers
},
timestamp: new Date().toISOString()
});
};
logger.info(`[VDS] Статистика отправлена: CPU=${cpuUsage}%, RAM=${ramUsage}% (${ramUsed}/${ramTotal}MB), Traffic=${totalTraffic}MB`);
res.json(responseData);
} catch (error) {
logger.error('[VDS] Ошибка получения статистики:', error);
res.status(500).json({ success: false, error: error.message });
@@ -385,7 +638,12 @@ router.get('/stats', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS)
router.post('/containers/:name/stop', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
const { name } = req.params;
await execAsync(`docker stop ${name}`);
const result = await execDockerCommand(`docker stop ${name}`);
if (result.code !== 0) {
return res.status(500).json({ success: false, error: result.stderr || 'Не удалось остановить контейнер' });
}
logger.info(`[VDS] Контейнер ${name} остановлен`);
res.json({ success: true, message: `Контейнер ${name} остановлен` });
} catch (error) {
@@ -400,7 +658,12 @@ router.post('/containers/:name/stop', requireAuth, requirePermission(PERMISSIONS
router.post('/containers/:name/start', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
const { name } = req.params;
await execAsync(`docker start ${name}`);
const result = await execDockerCommand(`docker start ${name}`);
if (result.code !== 0) {
return res.status(500).json({ success: false, error: result.stderr || 'Не удалось запустить контейнер' });
}
logger.info(`[VDS] Контейнер ${name} запущен`);
res.json({ success: true, message: `Контейнер ${name} запущен` });
} catch (error) {
@@ -415,8 +678,13 @@ router.post('/containers/:name/start', requireAuth, requirePermission(PERMISSION
router.delete('/containers/:name', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
const { name } = req.params;
await execAsync(`docker stop ${name}`).catch(() => {});
await execAsync(`docker rm ${name}`);
await execDockerCommand(`docker stop ${name}`);
const result = await execDockerCommand(`docker rm ${name}`);
if (result.code !== 0) {
return res.status(500).json({ success: false, error: result.stderr || 'Не удалось удалить контейнер' });
}
logger.info(`[VDS] Контейнер ${name} удален`);
res.json({ success: true, message: `Контейнер ${name} удален` });
} catch (error) {
@@ -432,8 +700,13 @@ router.get('/containers/:name/logs', requireAuth, requirePermission(PERMISSIONS.
try {
const { name } = req.params;
const { tail = 100 } = req.query;
const { stdout } = await execAsync(`docker logs --tail ${tail} ${name}`);
res.json({ success: true, logs: stdout });
const result = await execDockerCommand(`docker logs --tail ${tail} ${name}`);
if (result.code !== 0) {
return res.status(500).json({ success: false, error: result.stderr || 'Не удалось получить логи контейнера' });
}
res.json({ success: true, logs: result.stdout });
} catch (error) {
logger.error(`[VDS] Ошибка получения логов контейнера ${req.params.name}:`, error);
res.status(500).json({ success: false, error: error.message });
@@ -446,8 +719,13 @@ router.get('/containers/:name/logs', requireAuth, requirePermission(PERMISSIONS.
router.get('/containers/:name/stats', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
const { name } = req.params;
const { stdout } = await execAsync(`docker stats --no-stream --format "{{.CPUPerc}}|{{.MemUsage}}|{{.NetIO}}|{{.BlockIO}}" ${name}`);
const [cpu, mem, net, block] = stdout.trim().split('|');
const result = await execDockerCommand(`docker stats --no-stream --format "{{.CPUPerc}}|{{.MemUsage}}|{{.NetIO}}|{{.BlockIO}}" ${name}`);
if (result.code !== 0) {
return res.status(500).json({ success: false, error: result.stderr || 'Не удалось получить статистику контейнера' });
}
const [cpu, mem, net, block] = result.stdout.trim().split('|');
res.json({ success: true, stats: { cpu, mem, net, block } });
} catch (error) {
logger.error(`[VDS] Ошибка получения статистики контейнера ${req.params.name}:`, error);
@@ -794,6 +1072,106 @@ router.post('/backup/send', requireAuth, requirePermission(PERMISSIONS.MANAGE_SE
}
});
/**
* Проверить и обновить SSL сертификат
*/
router.post('/ssl/renew', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
const vdsSettings = await getVdsSettings();
if (!vdsSettings) {
return res.status(400).json({ success: false, error: 'VDS не настроена' });
}
// Проверяем, используется ли Docker certbot
const dockerUser = vdsSettings.dockerUser || 'docker';
const domain = vdsSettings.domain || vdsSettings.sshHost;
// Проверяем статус сертификата через Docker certbot
const checkResult = await execDockerCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml run --rm certbot certificates 2>&1 || certbot certificates 2>&1`);
if (checkResult.code !== 0) {
logger.warn('[VDS] Ошибка проверки сертификатов:', checkResult.stderr);
}
// Пытаемся обновить сертификат через Docker certbot
logger.info('[VDS] Обновление SSL сертификата...');
const renewResult = await execDockerCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml run --rm certbot renew --force-renewal --non-interactive 2>&1 || certbot renew --force-renewal --non-interactive 2>&1`);
if (renewResult.code === 0) {
// Перезапускаем nginx для применения нового сертификата
const reloadResult = await execDockerCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml restart frontend-nginx 2>&1 || systemctl reload nginx 2>&1`);
logger.info('[VDS] SSL сертификат обновлен');
res.json({
success: true,
message: 'SSL сертификат обновлен',
output: renewResult.stdout,
reloadOutput: reloadResult.stdout
});
} else {
logger.error('[VDS] Ошибка обновления SSL сертификата:', renewResult.stderr);
res.status(500).json({
success: false,
error: 'Не удалось обновить SSL сертификат',
details: renewResult.stderr
});
}
} catch (error) {
logger.error('[VDS] Ошибка обновления SSL сертификата:', error);
res.status(500).json({ success: false, error: error.message });
}
});
/**
* Получить статус SSL сертификата
*/
router.get('/ssl/status', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
const vdsSettings = await getVdsSettings();
if (!vdsSettings) {
return res.status(400).json({ success: false, error: 'VDS не настроена' });
}
const dockerUser = vdsSettings.dockerUser || 'docker';
const domain = vdsSettings.domain || vdsSettings.sshHost;
// Проверяем статус сертификата через Docker certbot
const checkResult = await execDockerCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml run --rm certbot certificates 2>&1 || certbot certificates 2>&1`);
// Проверяем срок действия сертификата
let certInfo = null;
if (domain) {
const certPath = `/etc/letsencrypt/live/${domain}/cert.pem`;
const certCheckResult = await execDockerCommand(`openssl x509 -in ${certPath} -noout -dates -subject 2>&1 || echo "Certificate not found"`);
if (certCheckResult.code === 0 && !certCheckResult.stdout.includes('not found')) {
certInfo = {
exists: true,
details: certCheckResult.stdout
};
} else {
certInfo = {
exists: false,
error: certCheckResult.stdout
};
}
}
res.json({
success: true,
certificates: checkResult.stdout,
domain: domain,
certInfo: certInfo
});
} catch (error) {
logger.error('[VDS] Ошибка проверки SSL сертификата:', error);
res.status(500).json({ success: false, error: error.message });
}
});
/**
* Получить историю статистики (для графиков)
*/

View File

@@ -0,0 +1,50 @@
/**
* Скрипт для сохранения настроек VDS в базу данных
*/
const encryptedDb = require('./services/encryptedDatabaseService');
async function saveVdsSettings() {
try {
console.log('🔧 Сохранение настроек VDS...');
// Данные для сохранения
// ВАЖНО: передаем ключи БЕЗ суффикса _encrypted, сервис сам добавит его
const settings = {
domain: '185.221.214.140', // Можно использовать IP или домен
email: 'info@hb3-accelerator.com',
ubuntu_user: 'root',
docker_user: 'root',
ssh_host: '185.221.214.140',
ssh_port: 22,
ssh_user: 'root',
ssh_password: '1414Bcar',
updated_at: new Date()
};
// Проверяем существующие настройки
const existing = await encryptedDb.getData('vds_settings', {}, 1);
if (existing.length > 0) {
console.log('📝 Обновление существующих настроек (id:', existing[0].id, ')');
const result = await encryptedDb.saveData('vds_settings', settings, { id: existing[0].id });
console.log('✅ Настройки обновлены:', result);
} else {
console.log(' Создание новых настроек');
const result = await encryptedDb.saveData('vds_settings', {
...settings,
created_at: new Date()
});
console.log('✅ Настройки созданы:', result);
}
console.log('✅ Настройки VDS успешно сохранены!');
process.exit(0);
} catch (error) {
console.error('❌ Ошибка сохранения настроек:', error);
process.exit(1);
}
}
saveVdsSettings();

View File

@@ -113,7 +113,7 @@ class EncryptedDataService {
if (encryptedColumn) {
// Для зашифрованных колонок используем прямое сравнение с зашифрованным значением
return `${key}_encrypted = encrypt_text($${paramIndex++}, ${hasEncryptedFields ? '$1' : 'NULL'})`;
return `${key}_encrypted = encrypt_text($${paramIndex++}, ${hasEncryptedFields ? '($1)::text' : 'NULL'})`;
} else {
// Для незашифрованных колонок используем обычное сравнение
// Заключаем зарезервированные слова в кавычки
@@ -198,7 +198,7 @@ class EncryptedDataService {
// Проверяем, есть ли зашифрованные поля в таблице
const hasEncryptedFields = columns.some(col => col.column_name.endsWith('_encrypted'));
let paramIndex = hasEncryptedFields ? 2 : 1; // Начинаем с 2, если есть зашифрованные поля, иначе с 1
let paramIndex = 1; // Начинаем с 1, encryptionKey будет последним (как в работающих примерах)
for (const [key, value] of Object.entries(data)) {
// Проверяем, есть ли зашифрованная версия колонки
@@ -228,10 +228,12 @@ class EncryptedDataService {
filteredData[key] = valueToEncrypt; // Добавляем в отфильтрованные данные
console.log(`✅ Добавили зашифрованное поле ${key} = "${valueToEncrypt}" в filteredData`);
// В INSERT запросах encryptionKey идет последним параметром (как в работающих примерах)
// Используем плейсхолдер, который будет заменен на реальный номер после подсчета всех параметров
if (encryptedColumn.data_type === 'jsonb') {
encryptedData[`${key}_encrypted`] = `encrypt_json($${currentParamIndex}, ${hasEncryptedFields ? '$1::text' : 'NULL'})`;
encryptedData[`${key}_encrypted`] = `encrypt_json($${currentParamIndex}, ${hasEncryptedFields ? '$ENCRYPTION_KEY_PARAM' : 'NULL'})`;
} else {
encryptedData[`${key}_encrypted`] = `encrypt_text($${currentParamIndex}, ${hasEncryptedFields ? '$1::text' : 'NULL'})`;
encryptedData[`${key}_encrypted`] = `encrypt_text($${currentParamIndex}, ${hasEncryptedFields ? '$ENCRYPTION_KEY_PARAM' : 'NULL'})`;
}
console.log(`🔐 Будем шифровать ${key} -> ${key}_encrypted`);
} else if (unencryptedColumn) {
@@ -274,92 +276,196 @@ class EncryptedDataService {
};
if (whereConditions) {
// UPDATE
const setClause = Object.keys(allData)
.map((key, index) => `${quoteReservedWord(key)} = ${allData[key]}`)
.join(', ');
const whereClause = Object.keys(whereConditions)
.map((key, index) => {
// Для WHERE условий используем зашифрованные имена колонок
const encryptedColumn = columns.find(col => col.column_name === `${key}_encrypted`);
if (encryptedColumn) {
// Для зашифрованных колонок используем encrypt_text для сравнения
return `${quoteReservedWord(`${key}_encrypted`)} = encrypt_text($${paramIndex + index}, ${hasEncryptedFields ? '$1' : 'NULL'})`;
} else {
// Для незашифрованных колонок используем обычное сравнение
return `${quoteReservedWord(key)} = $${paramIndex + index}`;
// UPDATE - используем тот же подход, что и в работающих примерах (auth.js, tables.js)
// Как в auth.js: 'UPDATE nonces SET nonce_encrypted = encrypt_text($1, $2)'
// Параметры: [nonce, encryptionKey] - encryptionKey идет последним
const updateParams = [];
let paramIndex = 1;
let encryptionKeyParamIndex = null;
// Сначала собираем все значения для SET и WHERE, чтобы узнать общее количество параметров
const setParts = [];
// Итерируемся по filteredData, чтобы использовать только реальные значения
for (const key of Object.keys(filteredData)) {
// Пропускаем ключи, которые используются только в WHERE
if (whereConditions && whereConditions.hasOwnProperty(key)) {
continue;
}
// Проверяем, зашифрованное ли это поле
// encryptedData содержит ключи с _encrypted (например, domain_encrypted)
// filteredData содержит оригинальные ключи (например, domain)
const encryptedKey = `${key}_encrypted`;
if (encryptedData[encryptedKey]) {
// Зашифрованное поле - key уже оригинальный (без _encrypted)
const dataParamIndex = paramIndex++;
updateParams.push(filteredData[key]);
setParts.push({ key: encryptedKey, dataParamIndex, encrypted: true });
} else if (unencryptedData.hasOwnProperty(key)) {
// Незашифрованное поле - проверяем, что оно есть в unencryptedData
const dataParamIndex = paramIndex++;
setParts.push({ key, dataParamIndex, encrypted: false });
updateParams.push(filteredData[key]);
}
}
// Проверяем, есть ли зашифрованные поля в SET или WHERE
const hasEncryptedInSet = setParts.some(part => part.encrypted);
// Формируем WHERE часть
const whereParts = [];
let hasEncryptedInWhere = false;
for (const [key, value] of Object.entries(whereConditions)) {
const encryptedColumn = columns.find(col => col.column_name === `${key}_encrypted`);
if (encryptedColumn) {
// Для зашифрованных колонок используем encrypt_text для сравнения
const dataParamIndex = paramIndex++;
whereParts.push({ key, dataParamIndex, encrypted: true });
updateParams.push(value);
hasEncryptedInWhere = true;
} else {
// Для незашифрованных колонок используем обычное сравнение
const dataParamIndex = paramIndex++;
whereParts.push({ key, dataParamIndex, encrypted: false });
updateParams.push(value);
}
}
// Определяем номер параметра для encryptionKey (последний, после всех данных)
// encryptionKey нужен, если есть зашифрованные поля в SET или WHERE
// ВАЖНО: encryptionKey используется один раз для всех зашифрованных полей
if (hasEncryptedInSet || hasEncryptedInWhere) {
encryptionKeyParamIndex = paramIndex; // paramIndex уже увеличен после последнего параметра данных
}
// Формируем SET clause с правильными номерами параметров
const setClause = setParts.map(part => {
if (part.encrypted) {
if (!encryptionKeyParamIndex) {
throw new Error('encryptionKeyParamIndex должен быть определен для зашифрованных полей');
}
})
.join(' AND ');
return `${quoteReservedWord(part.key)} = encrypt_text($${part.dataParamIndex}, $${encryptionKeyParamIndex})`;
} else {
return `${quoteReservedWord(part.key)} = $${part.dataParamIndex}`;
}
}).join(', ');
// Формируем WHERE clause с правильными номерами параметров
const whereClause = whereParts.map(part => {
if (part.encrypted) {
if (!encryptionKeyParamIndex) {
throw new Error('encryptionKeyParamIndex должен быть определен для зашифрованных полей в WHERE');
}
// part.key уже без _encrypted, нужно добавить _encrypted для имени колонки
return `${quoteReservedWord(`${part.key}_encrypted`)} = encrypt_text($${part.dataParamIndex}, $${encryptionKeyParamIndex})`;
} else {
return `${quoteReservedWord(part.key)} = $${part.dataParamIndex}`;
}
}).join(' AND ');
const query = `UPDATE ${tableName} SET ${setClause} WHERE ${whereClause} RETURNING *`;
const allParams = hasEncryptedFields ? [this.encryptionKey, ...Object.values(filteredData), ...Object.values(whereConditions)] : [...Object.values(filteredData), ...Object.values(whereConditions)];
// Собираем параметры: сначала все значения для SET и WHERE, затем encryptionKey (если есть)
const allParams = encryptionKeyParamIndex
? [...updateParams, this.encryptionKey]
: updateParams;
// Подсчитываем количество плейсхолдеров в запросе
const placeholderCount = (query.match(/\$\d+/g) || []).length;
const maxPlaceholder = Math.max(...(query.match(/\$\d+/g) || ['$0']).map(m => parseInt(m.replace('$', ''))));
console.log(`🔍 UPDATE запрос: ${query}`);
console.log(`🔍 setParts (${setParts.length}):`, JSON.stringify(setParts, null, 2));
console.log(`🔍 whereParts (${whereParts.length}):`, JSON.stringify(whereParts, null, 2));
console.log(`🔍 encryptionKeyParamIndex:`, encryptionKeyParamIndex);
console.log(`🔍 updateParams.length:`, updateParams.length);
console.log(`🔍 Параметры (${allParams.length}):`, allParams.map((p, i) => {
const val = typeof p === 'string' && p.length > 50 ? p.substring(0, 50) + '...' : p;
return `$${i+1}=${val}`;
}));
console.log(`🔍 Проверка: плейсхолдеров в запросе=${placeholderCount}, максимальный номер=${maxPlaceholder}, параметров=${allParams.length}`);
if (maxPlaceholder !== allParams.length) {
const errorMsg = `Несоответствие параметров: в запросе используется $${maxPlaceholder}, но передано ${allParams.length} параметров. setParts=${setParts.length}, whereParts=${whereParts.length}, hasEncryptionKey=${!!encryptionKeyParamIndex}`;
console.error(`${errorMsg}`);
throw new Error(errorMsg);
}
const { rows } = await db.getQuery()(query, allParams);
return rows[0];
} else {
// INSERT
const columns = Object.keys(allData).map(key => quoteReservedWord(key));
const placeholders = Object.keys(allData).map(key => allData[key]).join(', ');
const query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders}) RETURNING *`;
// INSERT - используем тот же подход, что и в работающих примерах (tables.js, users.js)
// Как в tables.js: 'INSERT INTO user_cell_values VALUES ($1, $2, encrypt_text($3, $4))'
// Параметры: [row_id, column_id, value, encryptionKey] - encryptionKey идет последним
const insertParams = [];
let insertParamIndex = 1;
let encryptionKeyParamIndex = null;
// Собираем параметры в правильном порядке по номерам из плейсхолдеров
const paramMap = new Map(); // номер параметра -> значение
// Формируем VALUES часть с правильными плейсхолдерами
const valuesParts = [];
const columns = [];
if (hasEncryptedFields) {
paramMap.set(1, this.encryptionKey); // $1 - ключ шифрования
}
// Сначала обрабатываем все поля из filteredData
// Проверяем, есть ли зашифрованные поля
const hasEncryptedFieldsInInsert = Object.keys(encryptedData).length > 0;
// Проходим по колонкам в порядке allData и добавляем соответствующие значения
for (const key of Object.keys(allData)) {
const placeholder = allData[key].toString();
console.log(`🔍 Обрабатываем ключ: ${key}, placeholder: ${placeholder}`);
// Извлекаем все номера параметров из плейсхолдера (может быть $1 в encrypt_text)
const paramMatches = placeholder.match(/\$(\d+)/g);
console.log(`🔍 paramMatches для ${key}:`, paramMatches);
if (paramMatches) {
// Для зашифрованных колонок нас интересует второй параметр ($3, $4 и т.д.)
// Для незашифрованных - первый параметр ($2, $3 и т.д.)
if (encryptedData[key]) {
// Это зашифрованная колонка - берем первый параметр (это значение для шифрования)
const originalKey = key.replace('_encrypted', '');
console.log(`🔍 Это зашифрованная колонка, originalKey: ${originalKey}, filteredData[originalKey]:`, filteredData[originalKey]);
if (filteredData[originalKey] !== undefined && paramMatches.length > 0) {
// Первый параметр это значение для шифрования
const valueParam = paramMatches[0];
const paramNum = parseInt(valueParam.substring(1));
console.log(`🔍 Устанавливаем paramMap[${paramNum}] =`, filteredData[originalKey]);
paramMap.set(paramNum, filteredData[originalKey]);
}
} else if (unencryptedData[key]) {
// Это незашифрованная колонка - берем параметр из плейсхолдера
const valueParam = paramMatches[0];
const paramNum = parseInt(valueParam.substring(1));
console.log(`🔍 Это незашифрованная колонка, устанавливаем paramMap[${paramNum}] =`, filteredData[key]);
paramMap.set(paramNum, filteredData[key]);
}
for (const key of Object.keys(filteredData)) {
const encryptedKey = `${key}_encrypted`;
if (encryptedData[encryptedKey]) {
// Зашифрованное поле
const dataParamIndex = insertParamIndex++;
insertParams.push(filteredData[key]);
columns.push(quoteReservedWord(encryptedKey));
// Используем плейсхолдер, который заменим позже
valuesParts.push(`encrypt_text($${dataParamIndex}, $ENCRYPTION_KEY)`);
} else if (unencryptedData.hasOwnProperty(key)) {
// Незашифрованное поле
const dataParamIndex = insertParamIndex++;
insertParams.push(filteredData[key]);
columns.push(quoteReservedWord(key));
valuesParts.push(`$${dataParamIndex}`);
}
}
console.log(`🔍 paramMap после цикла:`, Array.from(paramMap.entries()));
// Определяем номер параметра для encryptionKey (последний)
if (hasEncryptedFieldsInInsert) {
encryptionKeyParamIndex = insertParamIndex;
}
// Создаем массив параметров в правильном порядке (от $1 до максимального номера)
const maxParamNum = Math.max(...Array.from(paramMap.keys()));
const params = [];
for (let i = 1; i <= maxParamNum; i++) {
if (!paramMap.has(i)) {
throw new Error(`Отсутствует параметр $${i} для запроса`);
}
params.push(paramMap.get(i));
// Заменяем плейсхолдер ENCRYPTION_KEY на реальный номер
const placeholdersFinal = valuesParts.map(ph =>
ph.replace(/\$ENCRYPTION_KEY/g, encryptionKeyParamIndex ? `$${encryptionKeyParamIndex}` : 'NULL')
).join(', ');
const query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholdersFinal}) RETURNING *`;
// Собираем параметры: сначала все значения из insertParams, затем encryptionKey (если есть)
const allParams = encryptionKeyParamIndex
? [...insertParams, this.encryptionKey]
: insertParams;
// Подсчитываем количество плейсхолдеров в запросе
const placeholderCount = (query.match(/\$\d+/g) || []).length;
const maxPlaceholder = Math.max(...(query.match(/\$\d+/g) || ['$0']).map(m => parseInt(m.replace('$', ''))));
console.log(`🔍 INSERT запрос: ${query}`);
console.log(`🔍 columns (${columns.length}):`, columns);
console.log(`🔍 valuesParts (${valuesParts.length}):`, valuesParts);
console.log(`🔍 insertParams.length:`, insertParams.length);
console.log(`🔍 encryptionKeyParamIndex:`, encryptionKeyParamIndex);
console.log(`🔍 Параметры (${allParams.length}):`, allParams.map((p, i) => {
const val = typeof p === 'string' && p.length > 50 ? p.substring(0, 50) + '...' : p;
return `$${i+1}=${val}`;
}));
console.log(`🔍 Проверка: плейсхолдеров в запросе=${placeholderCount}, максимальный номер=${maxPlaceholder}, параметров=${allParams.length}`);
if (maxPlaceholder !== allParams.length) {
const errorMsg = `Несоответствие параметров: в запросе используется $${maxPlaceholder}, но передано ${allParams.length} параметров. columns=${columns.length}, valuesParts=${valuesParts.length}, hasEncryptionKey=${!!encryptionKeyParamIndex}`;
console.error(`${errorMsg}`);
throw new Error(errorMsg);
}
console.log(`🔍 Выполняем INSERT запрос:`, query);
console.log(`🔍 Параметры:`, params);
console.log(`🔍 Ключ шифрования:`, this.encryptionKey ? 'установлен' : 'не установлен');
const { rows } = await db.getQuery()(query, params);
const { rows } = await db.getQuery()(query, allParams);
return rows[0];
}
} catch (error) {
@@ -408,7 +514,7 @@ class EncryptedDataService {
if (encryptedColumn) {
// Для зашифрованных колонок используем прямое сравнение с зашифрованным значением
// Ключ шифрования всегда первый параметр ($1), затем значения
return `${key}_encrypted = encrypt_text($${index + 2}, $1)`;
return `${key}_encrypted = encrypt_text($${index + 2}, ($1)::text)`;
} else {
// Для незашифрованных колонок используем обычное сравнение
const columnName = quoteReservedWord(key);

View File

@@ -0,0 +1,41 @@
/**
* Скрипт для обновления настроек VDS - удаление пароля (используем SSH ключи)
*/
const encryptedDb = require('./services/encryptedDatabaseService');
async function updateVdsSettings() {
try {
console.log('🔧 Обновление настроек VDS (удаление пароля, используем SSH ключи)...');
// Получаем существующие настройки
const existing = await encryptedDb.getData('vds_settings', {}, 1);
if (existing.length === 0) {
console.error('❌ Настройки VDS не найдены');
process.exit(1);
}
console.log('📝 Найдены настройки (id:', existing[0].id, ')');
// Обновляем только пароль - устанавливаем в null (будет пустая строка после расшифровки)
// Передаем пустую строку, чтобы encryptedDb не обновлял это поле
// Но лучше явно установить в null через SQL
const settings = {
updated_at: new Date()
};
// Обновляем через encryptedDb (пароль не передаем, значит не обновляется)
const result = await encryptedDb.saveData('vds_settings', settings, { id: existing[0].id });
console.log('✅ Настройки обновлены (пароль не изменен, будет использоваться SSH ключ)');
console.log(' Если пароль все еще в БД, он будет игнорироваться, так как код проверяет sshPassword && sshPassword.trim()');
process.exit(0);
} catch (error) {
console.error('❌ Ошибка обновления настроек:', error);
process.exit(1);
}
}
updateVdsSettings();