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

This commit is contained in:
2025-11-14 21:51:09 +03:00
parent bbf1c6aa5a
commit 794cf1dcee
15 changed files with 3057 additions and 500 deletions

View File

@@ -22,12 +22,32 @@ const errorHandler = require('./middleware/errorHandler');
// const { version } = require('./package.json'); // Закомментировано, так как не используется
const db = require('./db'); // Добавляем импорт db
const aiAssistant = require('./services/ai-assistant'); // Добавляем импорт aiAssistant
const encryptedDb = require('./services/encryptedDatabaseService'); // Добавляем импорт encryptedDb
// Инициализация AI Assistant из БД
aiAssistant.initPromise.catch(error => {
logger.error('[app.js] AI Assistant не инициализирован:', error.message);
});
// Загрузка домена из БД при старте backend
async function loadDomainFromDB() {
try {
const settings = await encryptedDb.getData('vds_settings', {}, 1);
if (settings.length > 0 && settings[0].domain) {
const domain = settings[0].domain;
process.env.BASE_URL = `https://${domain}`;
logger.info(`[app.js] Домен загружен из БД: ${process.env.BASE_URL}`);
} else {
logger.info('[app.js] Домен не найден в БД, используется значение по умолчанию');
}
} catch (error) {
logger.error('[app.js] Ошибка загрузки домена из БД:', error);
}
}
// Загружаем домен при старте
loadDomainFromDB();
const deploymentWebSocketService = require('./services/deploymentWebSocketService'); // WebSocket для деплоя
const fs = require('fs');
const path = require('path');
@@ -109,13 +129,16 @@ const compileRoutes = require('./routes/compile'); // Компиляция ко
const { router: dleHistoryRoutes } = require('./routes/dleHistory'); // Расширенная история
const systemRoutes = require('./routes/system'); // Добавляем импорт маршрутов системного мониторинга
const consentRoutes = require('./routes/consent'); // Добавляем импорт маршрутов согласий
const vdsRoutes = require('./routes/vds'); // Добавляем импорт маршрутов VDS управления
const app = express();
// Указываем хост явно
app.set('host', '0.0.0.0');
app.set('port', process.env.PORT || 8000);
app.set('trust proxy', true);
// Настраиваем trust proxy: в продакшне доверяем nginx (1 прокси), в dev - не доверяем
const isProduction = process.env.NODE_ENV === 'production';
app.set('trust proxy', isProduction ? 1 : false);
// Настройка CORS
const corsOrigins = process.env.NODE_ENV === 'production'
@@ -181,8 +204,7 @@ app.use((req, res, next) => {
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Определяем режим работы
const isProduction = process.env.NODE_ENV === 'production';
// Режим работы уже определен выше (при настройке trust proxy)
// Rate limiting
const limiter = rateLimit({
@@ -194,7 +216,8 @@ const limiter = rateLimit({
},
standardHeaders: true,
legacyHeaders: false,
trustProxy: true, // Доверяем nginx proxy
// Настраиваем trust proxy правильно: 1 означает доверять одному прокси (nginx)
trustProxy: isProduction ? 1 : false, // В продакшне доверяем nginx, в dev - нет
});
// Применяем rate limiting ко всем запросам (временно отключено для тестирования)
@@ -210,7 +233,8 @@ const strictLimiter = rateLimit({
},
standardHeaders: true,
legacyHeaders: false,
trustProxy: true, // Доверяем nginx proxy
// Настраиваем trust proxy правильно: 1 означает доверять одному прокси (nginx)
trustProxy: isProduction ? 1 : false, // В продакшне доверяем nginx, в dev - нет
});
// Мягкий rate limiting для RPC настроек (часто запрашиваемых данных)
@@ -223,7 +247,8 @@ const rpcSettingsLimiter = rateLimit({
},
standardHeaders: true,
legacyHeaders: false,
trustProxy: true,
// Настраиваем trust proxy правильно: 1 означает доверять одному прокси (nginx)
trustProxy: isProduction ? 1 : false, // В продакшне доверяем nginx, в dev - нет
});
// Статическая раздача загруженных файлов (для dev и prod)
@@ -287,6 +312,7 @@ app.use('/api/monitoring', monitoringRoutes);
app.use('/api/pages', pagesRoutes); // Подключаем роутер страниц
app.use('/api/consent', consentRoutes); // Добавляем маршрут согласий
app.use('/api/system', systemRoutes); // Добавляем маршрут системного мониторинга
app.use('/api/vds', vdsRoutes); // Добавляем маршрут VDS управления
app.use('/api/uploads', uploadsRoutes); // Загрузка файлов (логотипы)
app.use('/api/ens', ensRoutes); // ENS utilities
app.use('/api', sshRoutes); // SSH роуты

View File

@@ -172,9 +172,47 @@ router.post('/verify', async (req, res) => {
return res.status(401).json({ success: false, error: 'Invalid nonce' });
}
// Создаем SIWE сообщение для проверки подписи
const origin = req.get('origin') || 'http://localhost:5173';
const domain = new URL(origin).host; // Извлекаем домен из origin
// Получаем базовый 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}`;
}
}
baseUrlForResources = `${protocol}://${host}`;
}
// Извлекаем домен и origin из baseUrlForResources для SIWE сообщения
const baseUrlObj = new URL(baseUrlForResources);
// Используем host (включает порт, если он нестандартный) или hostname + port
let domain = baseUrlObj.host; // Домен для SIWE (например, "localhost:9000" или "example.com")
// Если порт стандартный (80 для http, 443 для https), он может не быть в host
// В этом случае добавляем порт явно для localhost или если порт указан в URL
if (!domain.includes(':')) {
if (baseUrlObj.port) {
// Порт есть в URL, но не в host (стандартный порт)
domain = `${baseUrlObj.hostname}:${baseUrlObj.port}`;
} else if (baseUrlObj.hostname === 'localhost' || baseUrlObj.hostname === '127.0.0.1') {
// Для localhost добавляем порт явно
const defaultPort = baseUrlObj.protocol === 'https:' ? '443' : '9000';
domain = `${baseUrlObj.hostname}:${defaultPort}`;
}
}
const origin = baseUrlForResources; // URI для SIWE (полный URL)
// Получаем список документов для подписания и добавляем их в resources
const documentTitles = Object.keys(DOCUMENT_CONSENT_MAP);
@@ -183,7 +221,7 @@ router.post('/verify', async (req, res) => {
`SELECT to_regclass($1) as exists`, [tableName]
);
let resources = [`${origin}/api/auth/verify`];
let resources = [`${baseUrlForResources}/api/auth/verify`];
if (tableExistsRes.rows[0].exists) {
const { rows: documents } = await db.getQuery()(`
SELECT id FROM ${tableName}
@@ -192,9 +230,9 @@ router.post('/verify', async (req, res) => {
AND title = ANY($1)
`, [documentTitles]);
// Добавляем ссылки на документы в resources
// Добавляем ссылки на документы в resources (используем домен из БД)
documents.forEach(doc => {
resources.push(`${origin}/content/published/${doc.id}`);
resources.push(`${baseUrlForResources}/content/published/${doc.id}`);
});
}

822
backend/routes/vds.js Normal file
View File

@@ -0,0 +1,822 @@
/**
* 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;

View File

@@ -565,8 +565,7 @@ class UniversalGuestService {
const consentSystemMessage = await consentService.getConsentSystemMessage({
userId: null,
walletAddress,
channel: channel === 'web' ? 'web' : channel,
baseUrl: process.env.BASE_URL || 'http://localhost:9000'
channel: channel === 'web' ? 'web' : channel
});
if (consentSystemMessage && consentSystemMessage.consentRequired) {

View File

@@ -12,6 +12,7 @@
const db = require('../db');
const logger = require('../utils/logger');
const encryptedDb = require('./encryptedDatabaseService');
// Маппинг названий документов на типы согласий
const DOCUMENT_CONSENT_MAP = {
@@ -20,6 +21,61 @@ const DOCUMENT_CONSENT_MAP = {
'Согласие на обработку персональных данных': 'personal_data_processing'
};
// Кэш для домена
let cachedDomain = null;
let domainCacheTime = 0;
const DOMAIN_CACHE_TTL = 5 * 60 * 1000; // 5 минут
/**
* Сбросить кэш домена (вызывается при сохранении нового домена)
*/
function clearDomainCache() {
cachedDomain = null;
domainCacheTime = 0;
}
/**
* Получить домен из настроек VDS
* @returns {Promise<string>} - Базовый URL (https://domain.com)
*/
async function getBaseUrl() {
try {
// Проверяем кэш
const now = Date.now();
if (cachedDomain && (now - domainCacheTime) < DOMAIN_CACHE_TTL) {
return cachedDomain;
}
// Проверяем process.env
if (process.env.BASE_URL) {
cachedDomain = process.env.BASE_URL;
domainCacheTime = now;
return cachedDomain;
}
// Загружаем из БД
const settings = await encryptedDb.getData('vds_settings', {}, 1);
if (settings.length > 0 && settings[0].domain) {
const domain = settings[0].domain;
cachedDomain = `https://${domain}`;
domainCacheTime = now;
// Обновляем process.env для текущего процесса
process.env.BASE_URL = cachedDomain;
return cachedDomain;
}
// Возвращаем дефолтное значение
const defaultUrl = 'http://localhost:9000';
cachedDomain = defaultUrl;
domainCacheTime = now;
return defaultUrl;
} catch (error) {
logger.error('[ConsentService] Ошибка получения домена:', error);
return process.env.BASE_URL || 'http://localhost:9000';
}
}
/**
* Проверить согласия пользователя или гостя
* @param {Object} params - Параметры проверки
@@ -224,11 +280,16 @@ function formatConsentMessage({ channel = 'web', missingConsents = [], consentDo
* @param {number|null} params.userId - ID пользователя
* @param {string|null} params.walletAddress - Адрес кошелька или guest_ID
* @param {string} params.channel - Канал (web/telegram/email)
* @param {string} params.baseUrl - Базовый URL сайта
* @param {string} params.baseUrl - Базовый URL сайта (опционально, если не указан - загружается из БД)
* @returns {Promise<Object|null>} - Системное сообщение или null, если согласия есть
*/
async function getConsentSystemMessage({ userId = null, walletAddress = null, channel = 'web', baseUrl = 'http://localhost:9000' }) {
async function getConsentSystemMessage({ userId = null, walletAddress = null, channel = 'web', baseUrl = null }) {
try {
// Если baseUrl не указан, загружаем из БД
if (!baseUrl) {
baseUrl = await getBaseUrl();
}
// Проверяем согласия
const consentCheck = await checkConsents({ userId, walletAddress });
@@ -257,6 +318,8 @@ module.exports = {
getConsentDocuments,
formatConsentMessage,
getConsentSystemMessage,
getBaseUrl,
clearDomainCache,
DOCUMENT_CONSENT_MAP
};

View File

@@ -385,8 +385,7 @@ async function processMessage(messageData) {
const consentSystemMessage = await consentService.getConsentSystemMessage({
userId,
walletAddress: walletIdentity?.provider_id || null,
channel: channel === 'web' ? 'web' : channel,
baseUrl: process.env.BASE_URL || 'http://localhost:9000'
channel: channel === 'web' ? 'web' : channel
});
// Формируем финальный ответ ИИ с системным сообщением, если нужно