823 lines
32 KiB
JavaScript
823 lines
32 KiB
JavaScript
/**
|
||
* 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
|
||
*/
|
||
router.get('/settings', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
|
||
try {
|
||
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 не возвращаем по соображениям безопасности
|
||
}
|
||
});
|
||
} 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 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();
|
||
|
||
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 encryptionUtils = require('../utils/encryptionUtils');
|
||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||
|
||
// Подготавливаем данные для сохранения с правильными именами полей для шифрования
|
||
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/пустая строка) и настройки уже есть - не обновляем пароль
|
||
|
||
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();
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Получить список контейнеров
|
||
*/
|
||
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 { 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 };
|
||
});
|
||
|
||
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: 'Имя контейнера обязательно' });
|
||
}
|
||
|
||
await execAsync(`docker restart ${name}`);
|
||
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 { stdout } = await execAsync('docker ps -q');
|
||
const containerIds = 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(' ')}`);
|
||
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 { stdout: inspectOutput } = await execAsync(`docker inspect ${name} --format '{{.Config.Image}}'`);
|
||
const imageName = inspectOutput.trim();
|
||
|
||
if (!imageName) {
|
||
return res.status(404).json({ success: false, error: 'Контейнер не найден' });
|
||
}
|
||
|
||
// Останавливаем контейнер
|
||
await execAsync(`docker stop ${name}`).catch(() => {});
|
||
|
||
// Удаляем контейнер
|
||
await execAsync(`docker rm ${name}`).catch(() => {});
|
||
|
||
// Пересобираем образ (если есть Dockerfile)
|
||
// Для простоты просто пересоздаем контейнер из образа
|
||
await execAsync(`docker run -d --name ${name} ${imageName}`).catch(() => {
|
||
throw new 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
|
||
let cpuUsage = 0;
|
||
let ramUsage = 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);
|
||
}
|
||
|
||
// Получаем статистику по контейнерам (если Docker доступен)
|
||
let containers = [];
|
||
const dockerAvailable = await checkDockerAvailable();
|
||
if (dockerAvailable) {
|
||
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 };
|
||
});
|
||
} catch (error) {
|
||
logger.warn('[VDS] Не удалось получить статистику контейнеров:', error.message);
|
||
// Продолжаем без статистики контейнеров
|
||
}
|
||
}
|
||
|
||
res.json({
|
||
success: true,
|
||
stats: {
|
||
cpu: {
|
||
usage: cpuUsage,
|
||
cores: 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
|
||
},
|
||
traffic: {
|
||
total: totalTraffic, // MB
|
||
rx: rxBytes / 1024 / 1024, // MB
|
||
tx: txBytes / 1024 / 1024 // MB
|
||
},
|
||
containers
|
||
},
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
} 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;
|
||
await execAsync(`docker stop ${name}`);
|
||
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;
|
||
await execAsync(`docker start ${name}`);
|
||
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 execAsync(`docker stop ${name}`).catch(() => {});
|
||
await execAsync(`docker rm ${name}`);
|
||
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 { stdout } = await execAsync(`docker logs --tail ${tail} ${name}`);
|
||
res.json({ success: true, logs: 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 { stdout } = await execAsync(`docker stats --no-stream --format "{{.CPUPerc}}|{{.MemUsage}}|{{.NetIO}}|{{.BlockIO}}" ${name}`);
|
||
const [cpu, mem, net, block] = 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 });
|
||
}
|
||
});
|
||
|
||
/**
|
||
* Получить историю статистики (для графиков)
|
||
*/
|
||
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;
|
||
|