Files
DLE/backend/routes/vds.js

1312 lines
56 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
* All rights reserved.
*
* This software is proprietary and confidential.
* Unauthorized copying, modification, or distribution is prohibited.
*
* For licensing inquiries: info@hb3-accelerator.com
* Website: https://hb3-accelerator.com
* GitHub: https://github.com/VC-HB3-Accelerator
*/
const express = require('express');
const router = express.Router();
const { exec } = require('child_process');
const { promisify } = require('util');
const logger = require('../utils/logger');
const { requireAuth } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permissions');
const { PERMISSIONS } = require('../shared/permissions');
const db = require('../db');
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
*/
router.get('/settings', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
const encryptionUtils = require('../utils/encryptionUtils');
const encryptionKey = encryptionUtils.getEncryptionKey();
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 });
}
});
/**
* Сохранить настройки VDS
*/
router.post('/settings', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
const {
domain,
email,
ubuntuUser,
dockerUser,
sshHost,
sshPort,
sshUser,
sshPassword
} = req.body;
// Если передан только домен (для обратной совместимости)
if (domain && !email && !sshHost) {
const normalizedDomain = domain.trim().toLowerCase().replace(/^https?:\/\//, '').replace(/\/$/, '');
const settings = {
domain_encrypted: normalizedDomain, // encryptedDb автоматически зашифрует
updated_at: new Date()
};
await saveVdsSettingsToDb(settings);
updateDomainCache(normalizedDomain);
logger.info(`[VDS] Домен сохранен: ${normalizedDomain}`);
return res.json({ success: true, domain: normalizedDomain });
}
// Валидация обязательных полей (пароль опционален при обновлении)
if (!domain || !email || !ubuntuUser || !dockerUser || !sshHost || !sshPort || !sshUser) {
return res.status(400).json({
success: false,
error: 'Все поля обязательны для заполнения (кроме пароля при обновлении)'
});
}
// Нормализуем домен
const normalizedDomain = domain.trim().toLowerCase().replace(/^https?:\/\//, '').replace(/\/$/, '');
// Проверяем существующие настройки (для валидации пароля)
const existing = await encryptedDb.getData('vds_settings', {}, 1);
// Подготавливаем данные для сохранения с правильными именами полей для шифрования
const settings = {
domain_encrypted: normalizedDomain, // encryptedDb автоматически зашифрует поля с _encrypted
email_encrypted: email.trim(),
ubuntu_user_encrypted: ubuntuUser.trim(),
docker_user_encrypted: dockerUser.trim(),
ssh_host_encrypted: sshHost.trim(),
ssh_port: parseInt(sshPort, 10),
ssh_user_encrypted: sshUser.trim(),
updated_at: new Date()
};
// Пароль добавляем только если он указан (при обновлении можно не менять)
if (sshPassword !== undefined && sshPassword !== null && sshPassword.trim() !== '') {
settings.ssh_password_encrypted = sshPassword;
} else if (existing.length === 0) {
// При создании пароль обязателен
return res.status(400).json({
success: false,
error: 'SSH пароль обязателен при первой настройке'
});
}
// Если пароль не указан (undefined/null/пустая строка) и настройки уже есть - не обновляем пароль
await saveVdsSettingsToDb(settings);
updateDomainCache(normalizedDomain);
logger.info(`[VDS] Настройки сохранены: ${normalizedDomain}`);
res.json({ success: true, settings });
} catch (error) {
logger.error('[VDS] Ошибка сохранения настроек:', error);
res.status(500).json({ success: false, error: error.message });
}
});
/**
* Проверка доступности Docker
*/
async function checkDockerAvailable() {
try {
await execAsync('docker --version');
return true;
} catch (error) {
return false;
}
}
/**
* Получить настройки 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 } = settings;
// Экранируем команду для SSH
// Экранируем двойные кавычки и знаки доллара для правильной передачи через SSH
const escapedCommand = command
.replace(/\\/g, '\\\\') // Сначала экранируем обратные слеши
.replace(/\$/g, '\\$') // Экранируем знаки доллара
.replace(/"/g, '\\"'); // Экранируем двойные кавычки
// Базовые опции SSH - используем только SSH ключи, пароли не поддерживаются
const sshOptions = `-p ${sshPort} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o PasswordAuthentication=no`;
// Строим SSH команду - всегда используем SSH ключи
// SSH автоматически найдет ключ в ~/.ssh/id_rsa или ~/.ssh/id_ed25519
const 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 vdsSettings = await getVdsSettings();
// Проверяем, есть ли настройки VDS или локальный Docker доступен
if (!vdsSettings && !(await checkDockerAvailable())) {
return res.json({
success: true,
containers: [],
message: 'VDS не настроена и Docker недоступен локально'
});
}
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) {
logger.error('[VDS] Ошибка получения списка контейнеров:', error);
res.status(500).json({ success: false, error: error.message });
}
});
/**
* Перезапустить контейнер
*/
router.post('/containers/:name/restart', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
const { name } = req.params;
if (!name) {
return res.status(400).json({ success: false, error: 'Имя контейнера обязательно' });
}
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) {
logger.error(`[VDS] Ошибка перезапуска контейнера ${req.params.name}:`, error);
res.status(500).json({ success: false, error: error.message });
}
});
/**
* Перезапустить все контейнеры
*/
router.post('/containers/restart-all', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
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 });
}
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) {
logger.error('[VDS] Ошибка перезапуска всех контейнеров:', error);
res.status(500).json({ success: false, error: error.message });
}
});
/**
* Пересобрать контейнер
*/
router.post('/containers/:name/rebuild', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
const { name } = req.params;
if (!name) {
return res.status(400).json({ success: false, error: 'Имя контейнера обязательно' });
}
// Получаем информацию о контейнере
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 execDockerCommand(`docker stop ${name}`);
// Удаляем контейнер
await execDockerCommand(`docker rm ${name}`);
// Пересобираем образ (если есть Dockerfile)
// Для простоты просто пересоздаем контейнер из образа
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} пересобран` });
} catch (error) {
logger.error(`[VDS] Ошибка пересборки контейнера ${req.params.name}:`, error);
res.status(500).json({ success: false, error: error.message });
}
});
/**
* Получить статистику системы (CPU, RAM, трафик)
*/
router.get('/stats', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
const { period = '1h' } = req.query; // 1h, 6h, 24h, 7d
// Получаем текущую статистику CPU и RAM с VDS сервера
let cpuUsage = 0;
let ramUsage = 0;
let ramTotal = 0;
let ramUsed = 0;
let totalTraffic = 0;
let rxBytes = 0;
let txBytes = 0;
let cpuCores = 0;
// Получаем статистику по контейнерам (если Docker доступен)
let containers = [];
const vdsSettings = await getVdsSettings();
// Если есть настройки VDS, выполняем команды на VDS сервере
if (vdsSettings) {
try {
// 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;
}
const responseData = {
success: true,
stats: {
cpu: {
usage: cpuUsage,
cores: cpuCores || require('os').cpus().length
},
ram: {
usage: ramUsage,
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
rx: rxBytes / 1024 / 1024, // MB
tx: txBytes / 1024 / 1024 // MB
},
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 });
}
});
/**
* Остановить контейнер
*/
router.post('/containers/:name/stop', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
const { name } = req.params;
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) {
logger.error(`[VDS] Ошибка остановки контейнера ${req.params.name}:`, error);
res.status(500).json({ success: false, error: error.message });
}
});
/**
* Запустить контейнер
*/
router.post('/containers/:name/start', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
const { name } = req.params;
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) {
logger.error(`[VDS] Ошибка запуска контейнера ${req.params.name}:`, error);
res.status(500).json({ success: false, error: error.message });
}
});
/**
* Удалить контейнер
*/
router.delete('/containers/:name', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
const { name } = req.params;
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) {
logger.error(`[VDS] Ошибка удаления контейнера ${req.params.name}:`, error);
res.status(500).json({ success: false, error: error.message });
}
});
/**
* Получить логи контейнера
*/
router.get('/containers/:name/logs', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
const { name } = req.params;
const { tail = 100 } = req.query;
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 });
}
});
/**
* Получить статистику контейнера
*/
router.get('/containers/:name/stats', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
const { name } = req.params;
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);
res.status(500).json({ success: false, error: error.message });
}
});
/**
* Очистить Docker (удалить неиспользуемые образы, контейнеры, сети)
*/
router.post('/docker/cleanup', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
const { type = 'all' } = req.body; // all, images, containers, volumes, networks
let command;
switch (type) {
case 'images':
command = 'docker image prune -a -f';
break;
case 'containers':
command = 'docker container prune -f';
break;
case 'volumes':
command = 'docker volume prune -f';
break;
case 'networks':
command = 'docker network prune -f';
break;
default:
command = 'docker system prune -a -f';
}
const { stdout } = await execAsync(command);
logger.info(`[VDS] Docker очистка выполнена: ${type}`);
res.json({ success: true, message: `Очистка Docker (${type}) выполнена`, output: stdout });
} catch (error) {
logger.error('[VDS] Ошибка очистки Docker:', error);
res.status(500).json({ success: false, error: error.message });
}
});
/**
* Перезагрузить сервер
*/
router.post('/server/reboot', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
res.json({ success: true, message: 'Сервер будет перезагружен через 5 секунд' });
setTimeout(() => {
execAsync('reboot').catch(err => logger.error('[VDS] Ошибка перезагрузки:', err));
}, 5000);
} catch (error) {
logger.error('[VDS] Ошибка перезагрузки сервера:', error);
res.status(500).json({ success: false, error: error.message });
}
});
/**
* Выключить сервер
*/
router.post('/server/shutdown', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
res.json({ success: true, message: 'Сервер будет выключен через 5 секунд' });
setTimeout(() => {
execAsync('shutdown -h now').catch(err => logger.error('[VDS] Ошибка выключения:', err));
}, 5000);
} catch (error) {
logger.error('[VDS] Ошибка выключения сервера:', error);
res.status(500).json({ success: false, error: error.message });
}
});
/**
* Обновить систему
*/
router.post('/server/update', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
const { stdout } = await execAsync('apt update && apt upgrade -y');
logger.info('[VDS] Система обновлена');
res.json({ success: true, message: 'Система обновлена', output: stdout });
} catch (error) {
logger.error('[VDS] Ошибка обновления системы:', error);
res.status(500).json({ success: false, error: error.message });
}
});
/**
* Получить системные логи
*/
router.get('/server/logs', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
const { type = 'syslog', lines = 100 } = req.query; // syslog, journalctl, auth
let command;
switch (type) {
case 'journalctl':
command = `journalctl -n ${lines} --no-pager`;
break;
case 'auth':
command = `tail -n ${lines} /var/log/auth.log`;
break;
default:
command = `tail -n ${lines} /var/log/syslog`;
}
const { stdout } = await execAsync(command);
res.json({ success: true, logs: stdout, type });
} catch (error) {
logger.error('[VDS] Ошибка получения системных логов:', error);
res.status(500).json({ success: false, error: error.message });
}
});
/**
* Получить информацию о диске
*/
router.get('/server/disk', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
const { stdout: df } = await execAsync('df -h');
const { stdout: du } = await execAsync('du -sh / 2>/dev/null || echo "N/A"');
res.json({ success: true, disk: { df, du } });
} catch (error) {
logger.error('[VDS] Ошибка получения информации о диске:', error);
res.status(500).json({ success: false, error: error.message });
}
});
/**
* Получить список процессов
*/
router.get('/server/processes', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
const { stdout } = await execAsync('ps aux --sort=-%cpu | head -20');
res.json({ success: true, processes: stdout });
} catch (error) {
logger.error('[VDS] Ошибка получения списка процессов:', error);
res.status(500).json({ success: false, error: error.message });
}
});
/**
* Создать пользователя
*/
router.post('/users/create', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
const { username, password, addToDocker = false } = req.body;
if (!username || !password) {
return res.status(400).json({ success: false, error: 'Имя пользователя и пароль обязательны' });
}
let command = `useradd -m -s /bin/bash ${username}`;
if (addToDocker) {
command += ` && usermod -aG docker ${username}`;
}
await execAsync(command);
await execAsync(`echo "${username}:${password}" | chpasswd`);
logger.info(`[VDS] Пользователь ${username} создан`);
res.json({ success: true, message: `Пользователь ${username} создан` });
} catch (error) {
logger.error('[VDS] Ошибка создания пользователя:', error);
res.status(500).json({ success: false, error: error.message });
}
});
/**
* Удалить пользователя
*/
router.delete('/users/:username', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
const { username } = req.params;
const { removeHome = false } = req.query;
const command = removeHome ? `userdel -r ${username}` : `userdel ${username}`;
await execAsync(command);
logger.info(`[VDS] Пользователь ${username} удален`);
res.json({ success: true, message: `Пользователь ${username} удален` });
} catch (error) {
logger.error(`[VDS] Ошибка удаления пользователя ${req.params.username}:`, error);
res.status(500).json({ success: false, error: error.message });
}
});
/**
* Изменить пароль пользователя
*/
router.post('/users/:username/password', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
const { username } = req.params;
const { password } = req.body;
if (!password) {
return res.status(400).json({ success: false, error: 'Пароль обязателен' });
}
await execAsync(`echo "${username}:${password}" | chpasswd`);
logger.info(`[VDS] Пароль пользователя ${username} изменен`);
res.json({ success: true, message: `Пароль пользователя ${username} изменен` });
} catch (error) {
logger.error(`[VDS] Ошибка изменения пароля пользователя ${req.params.username}:`, error);
res.status(500).json({ success: false, error: error.message });
}
});
/**
* Получить список пользователей
*/
router.get('/users', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
const { stdout } = await execAsync('cat /etc/passwd | awk -F: \'{print $1, $3, $7}\'');
const users = stdout.trim().split('\n').map(line => {
const [username, uid, shell] = line.split(' ');
return { username, uid, shell };
});
res.json({ success: true, users });
} catch (error) {
logger.error('[VDS] Ошибка получения списка пользователей:', error);
res.status(500).json({ success: false, error: error.message });
}
});
/**
* Получить SSH ключи пользователя
*/
router.get('/users/:username/ssh-keys', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
const { username } = req.params;
const { stdout } = await execAsync(`cat /home/${username}/.ssh/authorized_keys 2>/dev/null || echo ""`);
const keys = stdout.trim().split('\n').filter(k => k.trim()).map((key, index) => ({
id: index + 1,
key: key.trim()
}));
res.json({ success: true, keys });
} catch (error) {
logger.error(`[VDS] Ошибка получения SSH ключей пользователя ${req.params.username}:`, error);
res.status(500).json({ success: false, error: error.message });
}
});
/**
* Добавить SSH ключ пользователю
*/
router.post('/users/:username/ssh-keys', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
const { username } = req.params;
const { key } = req.body;
if (!key) {
return res.status(400).json({ success: false, error: 'SSH ключ обязателен' });
}
await execAsync(`mkdir -p /home/${username}/.ssh`);
await execAsync(`chmod 700 /home/${username}/.ssh`);
await execAsync(`echo "${key}" >> /home/${username}/.ssh/authorized_keys`);
await execAsync(`chmod 600 /home/${username}/.ssh/authorized_keys`);
await execAsync(`chown -R ${username}:${username} /home/${username}/.ssh`);
logger.info(`[VDS] SSH ключ добавлен пользователю ${username}`);
res.json({ success: true, message: `SSH ключ добавлен пользователю ${username}` });
} catch (error) {
logger.error(`[VDS] Ошибка добавления SSH ключа пользователю ${req.params.username}:`, error);
res.status(500).json({ success: false, error: error.message });
}
});
/**
* Удалить SSH ключ пользователя
*/
router.delete('/users/:username/ssh-keys/:keyId', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
const { username, keyId } = req.params;
const keyIndex = parseInt(keyId, 10) - 1;
const { stdout } = await execAsync(`cat /home/${username}/.ssh/authorized_keys`);
const keys = stdout.trim().split('\n').filter(k => k.trim());
if (keyIndex < 0 || keyIndex >= keys.length) {
return res.status(400).json({ success: false, error: 'Неверный ID ключа' });
}
keys.splice(keyIndex, 1);
await execAsync(`echo "${keys.join('\n')}" > /home/${username}/.ssh/authorized_keys`);
await execAsync(`chmod 600 /home/${username}/.ssh/authorized_keys`);
logger.info(`[VDS] SSH ключ удален у пользователя ${username}`);
res.json({ success: true, message: `SSH ключ удален у пользователя ${username}` });
} catch (error) {
logger.error(`[VDS] Ошибка удаления SSH ключа пользователя ${req.params.username}:`, error);
res.status(500).json({ success: false, error: error.message });
}
});
/**
* Создать бэкап базы данных
*/
router.post('/backup/create', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupFile = `/tmp/backup-${timestamp}.sql`;
// Получаем настройки БД из переменных окружения или конфига
const dbName = process.env.DB_NAME || 'dapp_db';
const dbUser = process.env.DB_USER || 'dapp_user';
await execAsync(`PGPASSWORD=${process.env.DB_PASSWORD || ''} pg_dump -U ${dbUser} -d ${dbName} > ${backupFile}`);
logger.info(`[VDS] Бэкап создан: ${backupFile}`);
res.json({ success: true, message: 'Бэкап создан', file: backupFile });
} catch (error) {
logger.error('[VDS] Ошибка создания бэкапа:', error);
res.status(500).json({ success: false, error: error.message });
}
});
/**
* Отправить бэкап на локальную машину
*/
router.post('/backup/send', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
const { file, localHost, localUser, localPath, sshKeyPath } = req.body;
if (!file || !localHost || !localUser || !localPath) {
return res.status(400).json({
success: false,
error: 'Файл, локальный хост, пользователь и путь обязательны'
});
}
let command;
if (sshKeyPath) {
command = `scp -i ${sshKeyPath} ${file} ${localUser}@${localHost}:${localPath}`;
} else {
command = `scp ${file} ${localUser}@${localHost}:${localPath}`;
}
const { stdout } = await execAsync(command);
logger.info(`[VDS] Бэкап отправлен на ${localHost}:${localPath}`);
res.json({ success: true, message: 'Бэкап отправлен', output: stdout });
} catch (error) {
logger.error('[VDS] Ошибка отправки бэкапа:', error);
res.status(500).json({ success: false, error: error.message });
}
});
/**
* Проверить и обновить 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 сертификата...');
// Сначала пробуем renew --force-renewal для обновления существующего сертификата
// Это не создает новый сертификат и не попадает под лимит Let's Encrypt
let 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`);
// Если renew не сработал (сертификат не найден или другая ошибка), создаем новый
if (renewResult.code !== 0 || renewResult.stdout.includes('No renewals were attempted') || renewResult.stdout.includes('No certs found')) {
logger.info('[VDS] Renew не сработал, создаем новый сертификат...');
// Удаляем только сертификаты с суффиксами, основной оставляем
const certListResult = await execDockerCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml run --rm certbot certificates 2>&1 || certbot certificates 2>&1`);
if (certListResult.stdout) {
const lines = certListResult.stdout.split('\n');
const certNames = [];
for (let i = 0; i < lines.length; i++) {
if (lines[i].includes('Certificate Name:')) {
const certName = lines[i].split('Certificate Name:')[1]?.trim();
// Удаляем только сертификаты с суффиксами (например, hb3-accelerator.com-0001, hb3-accelerator.com-0002)
if (certName && certName !== domain && certName.startsWith(domain + '-')) {
certNames.push(certName);
}
}
}
// Удаляем только сертификаты с суффиксами
for (const certName of certNames) {
logger.info(`[VDS] Удаление старого сертификата с суффиксом: ${certName}`);
await execDockerCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml run --rm certbot delete --cert-name ${certName} --non-interactive 2>&1 || true`);
}
}
// Создаем новый сертификат только если его нет
const email = vdsSettings.email || 'admin@example.com';
renewResult = await execDockerCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml run --rm certbot certonly --webroot --webroot-path=/var/www/certbot --email ${email} --agree-tos --no-eff-email --non-interactive -d ${domain} 2>&1 || certbot certonly --webroot --webroot-path=/var/www/certbot --email ${email} --agree-tos --no-eff-email --non-interactive -d ${domain} 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] Очистка старых сертификатов с суффиксами...');
const certListAfter = await execDockerCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml run --rm certbot certificates 2>&1 || certbot certificates 2>&1`);
if (certListAfter.stdout) {
const lines = certListAfter.stdout.split('\n');
for (let i = 0; i < lines.length; i++) {
if (lines[i].includes('Certificate Name:')) {
const certName = lines[i].split('Certificate Name:')[1]?.trim();
// Удаляем сертификаты с суффиксами (например, hb3-accelerator.com-0001, hb3-accelerator.com-0002)
if (certName && certName !== domain && certName.startsWith(domain + '-')) {
logger.info(`[VDS] Удаление старого сертификата с суффиксом: ${certName}`);
await execDockerCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml run --rm certbot delete --cert-name ${certName} --non-interactive 2>&1 || true`);
}
}
}
}
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 && checkResult.stdout) {
// Парсим вывод certbot certificates для поиска сертификата по домену
// Ищем строку с "Domains:" содержащую наш домен, затем ищем "Certificate Path:"
const certLines = checkResult.stdout.split('\n');
let certPath = null;
let certName = null;
for (let i = 0; i < certLines.length; i++) {
const line = certLines[i];
// Ищем сертификат по домену
if (line.includes('Domains:') && line.includes(domain)) {
// Нашли сертификат для нашего домена, ищем путь в следующих строках
for (let j = i + 1; j < Math.min(i + 10, certLines.length); j++) {
if (certLines[j].includes('Certificate Path:')) {
certPath = certLines[j].split('Certificate Path:')[1]?.trim();
// Заменяем fullchain.pem на cert.pem для проверки
certPath = certPath.replace('/fullchain.pem', '/cert.pem');
break;
}
if (certLines[j].includes('Certificate Name:')) {
certName = certLines[j].split('Certificate Name:')[1]?.trim();
}
}
break;
}
}
// Если нашли путь, проверяем сертификат
if (certPath) {
const certCheckResult = await execDockerCommand(`openssl x509 -in ${certPath} -noout -dates -subject -issuer 2>&1 || echo "Certificate not found"`);
if (certCheckResult.code === 0 && !certCheckResult.stdout.includes('not found') && !certCheckResult.stdout.includes('No such file')) {
certInfo = {
exists: true,
details: certCheckResult.stdout,
certName: certName,
certPath: certPath
};
} else {
certInfo = {
exists: false,
error: certCheckResult.stdout || 'Сертификат не найден'
};
}
} else {
certInfo = {
exists: false,
error: 'Сертификат не найден для домена ' + domain + ' в выводе certbot certificates'
};
}
}
// Парсим все сертификаты из вывода certbot certificates
const allCertificates = [];
if (checkResult.stdout) {
const lines = checkResult.stdout.split('\n');
let currentCert = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.includes('Certificate Name:')) {
if (currentCert) {
allCertificates.push(currentCert);
}
currentCert = {
name: line.split('Certificate Name:')[1]?.trim(),
domains: [],
expiryDate: null,
certPath: null,
keyPath: null
};
} else if (currentCert) {
if (line.includes('Domains:')) {
currentCert.domains = line.split('Domains:')[1]?.trim().split(/\s+/);
} else if (line.includes('Expiry Date:')) {
currentCert.expiryDate = line.split('Expiry Date:')[1]?.trim();
} else if (line.includes('Certificate Path:')) {
currentCert.certPath = line.split('Certificate Path:')[1]?.trim();
} else if (line.includes('Private Key Path:')) {
currentCert.keyPath = line.split('Private Key Path:')[1]?.trim();
}
}
}
if (currentCert) {
allCertificates.push(currentCert);
}
}
res.json({
success: true,
certificates: checkResult.stdout,
allCertificates: allCertificates,
domain: domain,
certInfo: certInfo
});
} catch (error) {
logger.error('[VDS] Ошибка проверки SSL сертификата:', error);
res.status(500).json({ success: false, error: error.message });
}
});
/**
* Получить историю статистики (для графиков)
*/
router.get('/stats/history', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
try {
const { period = '1h' } = req.query; // 1h, 6h, 24h, 7d
// Пока возвращаем пустую историю, так как нужно настроить сбор данных
// В будущем можно использовать временную БД или файлы для хранения истории
res.json({
success: true,
history: {
cpu: [],
ram: [],
traffic: []
},
period
});
} catch (error) {
logger.error('[VDS] Ошибка получения истории статистики:', error);
res.status(500).json({ success: false, error: error.message });
}
});
module.exports = router;