ваше сообщение коммита
This commit is contained in:
@@ -22,12 +22,32 @@ const errorHandler = require('./middleware/errorHandler');
|
|||||||
// const { version } = require('./package.json'); // Закомментировано, так как не используется
|
// const { version } = require('./package.json'); // Закомментировано, так как не используется
|
||||||
const db = require('./db'); // Добавляем импорт db
|
const db = require('./db'); // Добавляем импорт db
|
||||||
const aiAssistant = require('./services/ai-assistant'); // Добавляем импорт aiAssistant
|
const aiAssistant = require('./services/ai-assistant'); // Добавляем импорт aiAssistant
|
||||||
|
const encryptedDb = require('./services/encryptedDatabaseService'); // Добавляем импорт encryptedDb
|
||||||
|
|
||||||
// Инициализация AI Assistant из БД
|
// Инициализация AI Assistant из БД
|
||||||
aiAssistant.initPromise.catch(error => {
|
aiAssistant.initPromise.catch(error => {
|
||||||
logger.error('[app.js] AI Assistant не инициализирован:', error.message);
|
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 deploymentWebSocketService = require('./services/deploymentWebSocketService'); // WebSocket для деплоя
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
@@ -109,13 +129,16 @@ const compileRoutes = require('./routes/compile'); // Компиляция ко
|
|||||||
const { router: dleHistoryRoutes } = require('./routes/dleHistory'); // Расширенная история
|
const { router: dleHistoryRoutes } = require('./routes/dleHistory'); // Расширенная история
|
||||||
const systemRoutes = require('./routes/system'); // Добавляем импорт маршрутов системного мониторинга
|
const systemRoutes = require('./routes/system'); // Добавляем импорт маршрутов системного мониторинга
|
||||||
const consentRoutes = require('./routes/consent'); // Добавляем импорт маршрутов согласий
|
const consentRoutes = require('./routes/consent'); // Добавляем импорт маршрутов согласий
|
||||||
|
const vdsRoutes = require('./routes/vds'); // Добавляем импорт маршрутов VDS управления
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
// Указываем хост явно
|
// Указываем хост явно
|
||||||
app.set('host', '0.0.0.0');
|
app.set('host', '0.0.0.0');
|
||||||
app.set('port', process.env.PORT || 8000);
|
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
|
// Настройка CORS
|
||||||
const corsOrigins = process.env.NODE_ENV === 'production'
|
const corsOrigins = process.env.NODE_ENV === 'production'
|
||||||
@@ -181,8 +204,7 @@ app.use((req, res, next) => {
|
|||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.urlencoded({ extended: true }));
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
// Определяем режим работы
|
// Режим работы уже определен выше (при настройке trust proxy)
|
||||||
const isProduction = process.env.NODE_ENV === 'production';
|
|
||||||
|
|
||||||
// Rate limiting
|
// Rate limiting
|
||||||
const limiter = rateLimit({
|
const limiter = rateLimit({
|
||||||
@@ -194,7 +216,8 @@ const limiter = rateLimit({
|
|||||||
},
|
},
|
||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
legacyHeaders: false,
|
legacyHeaders: false,
|
||||||
trustProxy: true, // Доверяем nginx proxy
|
// Настраиваем trust proxy правильно: 1 означает доверять одному прокси (nginx)
|
||||||
|
trustProxy: isProduction ? 1 : false, // В продакшне доверяем nginx, в dev - нет
|
||||||
});
|
});
|
||||||
|
|
||||||
// Применяем rate limiting ко всем запросам (временно отключено для тестирования)
|
// Применяем rate limiting ко всем запросам (временно отключено для тестирования)
|
||||||
@@ -210,7 +233,8 @@ const strictLimiter = rateLimit({
|
|||||||
},
|
},
|
||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
legacyHeaders: false,
|
legacyHeaders: false,
|
||||||
trustProxy: true, // Доверяем nginx proxy
|
// Настраиваем trust proxy правильно: 1 означает доверять одному прокси (nginx)
|
||||||
|
trustProxy: isProduction ? 1 : false, // В продакшне доверяем nginx, в dev - нет
|
||||||
});
|
});
|
||||||
|
|
||||||
// Мягкий rate limiting для RPC настроек (часто запрашиваемых данных)
|
// Мягкий rate limiting для RPC настроек (часто запрашиваемых данных)
|
||||||
@@ -223,7 +247,8 @@ const rpcSettingsLimiter = rateLimit({
|
|||||||
},
|
},
|
||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
legacyHeaders: false,
|
legacyHeaders: false,
|
||||||
trustProxy: true,
|
// Настраиваем trust proxy правильно: 1 означает доверять одному прокси (nginx)
|
||||||
|
trustProxy: isProduction ? 1 : false, // В продакшне доверяем nginx, в dev - нет
|
||||||
});
|
});
|
||||||
|
|
||||||
// Статическая раздача загруженных файлов (для dev и prod)
|
// Статическая раздача загруженных файлов (для dev и prod)
|
||||||
@@ -287,6 +312,7 @@ app.use('/api/monitoring', monitoringRoutes);
|
|||||||
app.use('/api/pages', pagesRoutes); // Подключаем роутер страниц
|
app.use('/api/pages', pagesRoutes); // Подключаем роутер страниц
|
||||||
app.use('/api/consent', consentRoutes); // Добавляем маршрут согласий
|
app.use('/api/consent', consentRoutes); // Добавляем маршрут согласий
|
||||||
app.use('/api/system', systemRoutes); // Добавляем маршрут системного мониторинга
|
app.use('/api/system', systemRoutes); // Добавляем маршрут системного мониторинга
|
||||||
|
app.use('/api/vds', vdsRoutes); // Добавляем маршрут VDS управления
|
||||||
app.use('/api/uploads', uploadsRoutes); // Загрузка файлов (логотипы)
|
app.use('/api/uploads', uploadsRoutes); // Загрузка файлов (логотипы)
|
||||||
app.use('/api/ens', ensRoutes); // ENS utilities
|
app.use('/api/ens', ensRoutes); // ENS utilities
|
||||||
app.use('/api', sshRoutes); // SSH роуты
|
app.use('/api', sshRoutes); // SSH роуты
|
||||||
|
|||||||
@@ -172,9 +172,47 @@ router.post('/verify', async (req, res) => {
|
|||||||
return res.status(401).json({ success: false, error: 'Invalid nonce' });
|
return res.status(401).json({ success: false, error: 'Invalid nonce' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создаем SIWE сообщение для проверки подписи
|
// Получаем базовый URL из БД (домен VDS) или используем текущий хост из запроса
|
||||||
const origin = req.get('origin') || 'http://localhost:5173';
|
const consentService = require('../services/consentService');
|
||||||
const domain = new URL(origin).host; // Извлекаем домен из origin
|
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
|
// Получаем список документов для подписания и добавляем их в resources
|
||||||
const documentTitles = Object.keys(DOCUMENT_CONSENT_MAP);
|
const documentTitles = Object.keys(DOCUMENT_CONSENT_MAP);
|
||||||
@@ -183,7 +221,7 @@ router.post('/verify', async (req, res) => {
|
|||||||
`SELECT to_regclass($1) as exists`, [tableName]
|
`SELECT to_regclass($1) as exists`, [tableName]
|
||||||
);
|
);
|
||||||
|
|
||||||
let resources = [`${origin}/api/auth/verify`];
|
let resources = [`${baseUrlForResources}/api/auth/verify`];
|
||||||
if (tableExistsRes.rows[0].exists) {
|
if (tableExistsRes.rows[0].exists) {
|
||||||
const { rows: documents } = await db.getQuery()(`
|
const { rows: documents } = await db.getQuery()(`
|
||||||
SELECT id FROM ${tableName}
|
SELECT id FROM ${tableName}
|
||||||
@@ -192,9 +230,9 @@ router.post('/verify', async (req, res) => {
|
|||||||
AND title = ANY($1)
|
AND title = ANY($1)
|
||||||
`, [documentTitles]);
|
`, [documentTitles]);
|
||||||
|
|
||||||
// Добавляем ссылки на документы в resources
|
// Добавляем ссылки на документы в resources (используем домен из БД)
|
||||||
documents.forEach(doc => {
|
documents.forEach(doc => {
|
||||||
resources.push(`${origin}/content/published/${doc.id}`);
|
resources.push(`${baseUrlForResources}/content/published/${doc.id}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
822
backend/routes/vds.js
Normal file
822
backend/routes/vds.js
Normal 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;
|
||||||
|
|
||||||
@@ -565,8 +565,7 @@ class UniversalGuestService {
|
|||||||
const consentSystemMessage = await consentService.getConsentSystemMessage({
|
const consentSystemMessage = await consentService.getConsentSystemMessage({
|
||||||
userId: null,
|
userId: null,
|
||||||
walletAddress,
|
walletAddress,
|
||||||
channel: channel === 'web' ? 'web' : channel,
|
channel: channel === 'web' ? 'web' : channel
|
||||||
baseUrl: process.env.BASE_URL || 'http://localhost:9000'
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (consentSystemMessage && consentSystemMessage.consentRequired) {
|
if (consentSystemMessage && consentSystemMessage.consentRequired) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
const db = require('../db');
|
const db = require('../db');
|
||||||
const logger = require('../utils/logger');
|
const logger = require('../utils/logger');
|
||||||
|
const encryptedDb = require('./encryptedDatabaseService');
|
||||||
|
|
||||||
// Маппинг названий документов на типы согласий
|
// Маппинг названий документов на типы согласий
|
||||||
const DOCUMENT_CONSENT_MAP = {
|
const DOCUMENT_CONSENT_MAP = {
|
||||||
@@ -20,6 +21,61 @@ const DOCUMENT_CONSENT_MAP = {
|
|||||||
'Согласие на обработку персональных данных': 'personal_data_processing'
|
'Согласие на обработку персональных данных': '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 - Параметры проверки
|
* @param {Object} params - Параметры проверки
|
||||||
@@ -224,11 +280,16 @@ function formatConsentMessage({ channel = 'web', missingConsents = [], consentDo
|
|||||||
* @param {number|null} params.userId - ID пользователя
|
* @param {number|null} params.userId - ID пользователя
|
||||||
* @param {string|null} params.walletAddress - Адрес кошелька или guest_ID
|
* @param {string|null} params.walletAddress - Адрес кошелька или guest_ID
|
||||||
* @param {string} params.channel - Канал (web/telegram/email)
|
* @param {string} params.channel - Канал (web/telegram/email)
|
||||||
* @param {string} params.baseUrl - Базовый URL сайта
|
* @param {string} params.baseUrl - Базовый URL сайта (опционально, если не указан - загружается из БД)
|
||||||
* @returns {Promise<Object|null>} - Системное сообщение или null, если согласия есть
|
* @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 {
|
try {
|
||||||
|
// Если baseUrl не указан, загружаем из БД
|
||||||
|
if (!baseUrl) {
|
||||||
|
baseUrl = await getBaseUrl();
|
||||||
|
}
|
||||||
|
|
||||||
// Проверяем согласия
|
// Проверяем согласия
|
||||||
const consentCheck = await checkConsents({ userId, walletAddress });
|
const consentCheck = await checkConsents({ userId, walletAddress });
|
||||||
|
|
||||||
@@ -257,6 +318,8 @@ module.exports = {
|
|||||||
getConsentDocuments,
|
getConsentDocuments,
|
||||||
formatConsentMessage,
|
formatConsentMessage,
|
||||||
getConsentSystemMessage,
|
getConsentSystemMessage,
|
||||||
|
getBaseUrl,
|
||||||
|
clearDomainCache,
|
||||||
DOCUMENT_CONSENT_MAP
|
DOCUMENT_CONSENT_MAP
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -385,8 +385,7 @@ async function processMessage(messageData) {
|
|||||||
const consentSystemMessage = await consentService.getConsentSystemMessage({
|
const consentSystemMessage = await consentService.getConsentSystemMessage({
|
||||||
userId,
|
userId,
|
||||||
walletAddress: walletIdentity?.provider_id || null,
|
walletAddress: walletIdentity?.provider_id || null,
|
||||||
channel: channel === 'web' ? 'web' : channel,
|
channel: channel === 'web' ? 'web' : channel
|
||||||
baseUrl: process.env.BASE_URL || 'http://localhost:9000'
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Формируем финальный ответ ИИ с системным сообщением, если нужно
|
// Формируем финальный ответ ИИ с системным сообщением, если нужно
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
|
"chart.js": "^4.5.1",
|
||||||
"connect-pg-simple": "^10.0.0",
|
"connect-pg-simple": "^10.0.0",
|
||||||
"dompurify": "^3.2.4",
|
"dompurify": "^3.2.4",
|
||||||
"element-plus": "^2.9.11",
|
"element-plus": "^2.9.11",
|
||||||
|
|||||||
@@ -141,9 +141,12 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue';
|
import { ref, reactive, onMounted } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
import { useWebSshService } from '../services/webSshService';
|
import { useWebSshService } from '../services/webSshService';
|
||||||
import { useWebSshLogs } from '../composables/useWebSshLogs';
|
import { useWebSshLogs } from '../composables/useWebSshLogs';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
const webSshService = useWebSshService();
|
const webSshService = useWebSshService();
|
||||||
|
|
||||||
const encodeDomainForRequest = (domain) => {
|
const encodeDomainForRequest = (domain) => {
|
||||||
@@ -265,10 +268,33 @@ const handleSubmit = async () => {
|
|||||||
domain: form.domain
|
domain: form.domain
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Сохраняем ВСЕ настройки на сервере
|
||||||
|
try {
|
||||||
|
await axios.post('/api/vds/settings', {
|
||||||
|
domain: form.domain,
|
||||||
|
email: form.email,
|
||||||
|
ubuntuUser: form.ubuntuUser,
|
||||||
|
dockerUser: form.dockerUser,
|
||||||
|
sshHost: form.sshHost,
|
||||||
|
sshPort: form.sshPort,
|
||||||
|
sshUser: form.sshUser,
|
||||||
|
sshPassword: form.sshPassword
|
||||||
|
});
|
||||||
|
addLog('info', 'Настройки VDS сохранены на сервере');
|
||||||
|
} catch (error) {
|
||||||
|
addLog('error', `Ошибка сохранения настроек на сервере: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Отправляем событие об изменении статуса VDS
|
// Отправляем событие об изменении статуса VDS
|
||||||
window.dispatchEvent(new CustomEvent('vds-status-changed', {
|
window.dispatchEvent(new CustomEvent('vds-status-changed', {
|
||||||
detail: { isConfigured: true }
|
detail: { isConfigured: true }
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Перенаправляем на страницу управления VDS через 3 секунды
|
||||||
|
addLog('info', 'Перенаправление на страницу управления VDS через 3 секунды...');
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push({ name: 'vds-management' });
|
||||||
|
}, 3000);
|
||||||
} else {
|
} else {
|
||||||
addLog('error', result.message || 'Ошибка при настройке VDS');
|
addLog('error', result.message || 'Ошибка при настройке VDS');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -296,9 +296,10 @@ const routes = [
|
|||||||
component: () => import('../views/smartcontracts/SettingsView.vue')
|
component: () => import('../views/smartcontracts/SettingsView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/vds-mock',
|
path: '/vds',
|
||||||
name: 'vds-mock',
|
name: 'vds-management',
|
||||||
component: () => import('../views/VdsMockView.vue')
|
component: () => import('../views/VdsManagementView.vue'),
|
||||||
|
meta: { permission: PERMISSIONS.MANAGE_SETTINGS }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/connect-wallet',
|
path: '/connect-wallet',
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ function goToManagement() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function goToWeb3App() {
|
function goToWeb3App() {
|
||||||
router.push({ name: 'vds-mock' });
|
router.push({ name: 'vds-management' });
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToAcceleratorRegistration() {
|
function goToAcceleratorRegistration() {
|
||||||
|
|||||||
2034
frontend/src/views/VdsManagementView.vue
Normal file
2034
frontend/src/views/VdsManagementView.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,477 +0,0 @@
|
|||||||
<!--
|
|
||||||
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
|
|
||||||
-->
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<BaseLayout
|
|
||||||
:is-authenticated="isAuthenticated"
|
|
||||||
:identities="identities"
|
|
||||||
:token-balances="tokenBalances"
|
|
||||||
:is-loading-tokens="isLoadingTokens"
|
|
||||||
@auth-action-completed="$emit('auth-action-completed')"
|
|
||||||
>
|
|
||||||
<div class="vds-mock-container">
|
|
||||||
<div class="mock-header">
|
|
||||||
<h1 v-if="vdsConfigured">VDS Сервер - Настроен</h1>
|
|
||||||
<h1 v-else>VDS Сервер - Не настроен</h1>
|
|
||||||
<div class="mock-status">
|
|
||||||
<div class="status-indicator" :class="vdsConfigured ? 'online' : 'offline'"></div>
|
|
||||||
<span v-if="vdsConfigured">Онлайн</span>
|
|
||||||
<span v-else>Офлайн</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Информация о домене -->
|
|
||||||
<div v-if="vdsConfigured && vdsDomain" class="domain-info">
|
|
||||||
<h3>🌐 Ваше приложение доступно по адресу:</h3>
|
|
||||||
<a :href="`https://${vdsDomain}`" target="_blank" class="domain-link">
|
|
||||||
https://{{ vdsDomain }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Мок интерфейс -->
|
|
||||||
<div class="mock-content">
|
|
||||||
<div class="mock-card">
|
|
||||||
<h2>Статус сервера</h2>
|
|
||||||
<div class="mock-metrics">
|
|
||||||
<div class="mock-metric">
|
|
||||||
<span class="label">CPU:</span>
|
|
||||||
<span class="value mock">--%</span>
|
|
||||||
</div>
|
|
||||||
<div class="mock-metric">
|
|
||||||
<span class="label">RAM:</span>
|
|
||||||
<span class="value mock">--%</span>
|
|
||||||
</div>
|
|
||||||
<div class="mock-metric">
|
|
||||||
<span class="label">Диск:</span>
|
|
||||||
<span class="value mock">--%</span>
|
|
||||||
</div>
|
|
||||||
<div class="mock-metric">
|
|
||||||
<span class="label">Uptime:</span>
|
|
||||||
<span class="value mock">--</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mock-card">
|
|
||||||
<h2>Управление сервисами</h2>
|
|
||||||
<div class="mock-services">
|
|
||||||
<div class="mock-service">
|
|
||||||
<span class="service-name">DLE Application</span>
|
|
||||||
<span class="service-status mock">Недоступно</span>
|
|
||||||
</div>
|
|
||||||
<div class="mock-service">
|
|
||||||
<span class="service-name">PostgreSQL</span>
|
|
||||||
<span class="service-status mock">Недоступно</span>
|
|
||||||
</div>
|
|
||||||
<div class="mock-service">
|
|
||||||
<span class="service-name">Nginx</span>
|
|
||||||
<span class="service-status mock">Недоступно</span>
|
|
||||||
</div>
|
|
||||||
<div class="mock-service">
|
|
||||||
<span class="service-name">Docker</span>
|
|
||||||
<span class="service-status mock">Недоступно</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mock-card">
|
|
||||||
<h2>Логи системы</h2>
|
|
||||||
<div class="mock-logs">
|
|
||||||
<pre>VDS сервер не настроен
|
|
||||||
Для активации перейдите в настройки и настройте VDS сервер</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mock-card">
|
|
||||||
<h2>Деплой приложения</h2>
|
|
||||||
<div class="mock-deploy">
|
|
||||||
<p>Деплой недоступен - VDS сервер не настроен</p>
|
|
||||||
<button class="mock-btn" disabled>Деплой недоступен</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mock-card">
|
|
||||||
<h2>Управление бэкапами</h2>
|
|
||||||
<div class="mock-backups">
|
|
||||||
<p>Бэкапы недоступны - VDS сервер не настроен</p>
|
|
||||||
<div class="mock-backup-list">
|
|
||||||
<div class="mock-backup-item">
|
|
||||||
<span class="backup-name">Нет бэкапов</span>
|
|
||||||
<span class="backup-date">--</span>
|
|
||||||
<span class="backup-size">--</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Призыв к действию -->
|
|
||||||
<div class="call-to-action">
|
|
||||||
<h2>Настройте VDS сервер</h2>
|
|
||||||
<p>Для использования всех функций управления VDS сервером необходимо его настроить.</p>
|
|
||||||
<button
|
|
||||||
class="setup-btn"
|
|
||||||
@click="canManageSettings ? goToSetup() : null"
|
|
||||||
:disabled="!canManageSettings"
|
|
||||||
>
|
|
||||||
Перейти к настройке VDS
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</BaseLayout>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { defineProps, defineEmits, ref, onMounted } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import BaseLayout from '../components/BaseLayout.vue';
|
|
||||||
import { usePermissions } from '@/composables/usePermissions';
|
|
||||||
|
|
||||||
// Props
|
|
||||||
const props = defineProps({
|
|
||||||
isAuthenticated: Boolean,
|
|
||||||
identities: Array,
|
|
||||||
tokenBalances: Object,
|
|
||||||
isLoadingTokens: Boolean
|
|
||||||
});
|
|
||||||
|
|
||||||
// Emits
|
|
||||||
const emit = defineEmits(['auth-action-completed']);
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { canManageSettings } = usePermissions();
|
|
||||||
|
|
||||||
// Состояние VDS
|
|
||||||
const vdsConfigured = ref(false);
|
|
||||||
const vdsDomain = ref(null);
|
|
||||||
|
|
||||||
// Проверка статуса VDS
|
|
||||||
const checkVdsStatus = () => {
|
|
||||||
try {
|
|
||||||
const vdsConfig = localStorage.getItem('vds-config');
|
|
||||||
if (vdsConfig) {
|
|
||||||
const config = JSON.parse(vdsConfig);
|
|
||||||
vdsConfigured.value = config.isConfigured || false;
|
|
||||||
vdsDomain.value = config.domain || null;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка при проверке статуса VDS:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const goToSetup = () => {
|
|
||||||
router.push({ name: 'webssh-settings' });
|
|
||||||
};
|
|
||||||
|
|
||||||
// Жизненный цикл
|
|
||||||
onMounted(() => {
|
|
||||||
checkVdsStatus();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.vds-mock-container {
|
|
||||||
padding: 20px;
|
|
||||||
background-color: var(--color-white);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
||||||
margin-top: 20px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mock-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
padding-bottom: 20px;
|
|
||||||
border-bottom: 2px solid #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mock-header h1 {
|
|
||||||
margin: 0;
|
|
||||||
color: #6c757d;
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mock-status {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
background: #f8d7da;
|
|
||||||
color: #721c24;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-indicator {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: currentColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-indicator.offline {
|
|
||||||
background: #dc3545;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-indicator.online {
|
|
||||||
background: #28a745;
|
|
||||||
}
|
|
||||||
|
|
||||||
.domain-info {
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
color: white;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 12px;
|
|
||||||
margin: 20px 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.domain-info h3 {
|
|
||||||
margin: 0 0 15px 0;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.domain-link {
|
|
||||||
color: white;
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
background: rgba(255, 255, 255, 0.2);
|
|
||||||
padding: 12px 24px;
|
|
||||||
border-radius: 8px;
|
|
||||||
display: inline-block;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.domain-link:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.3);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mock-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mock-card {
|
|
||||||
background: #f5f5f5;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 24px;
|
|
||||||
border: 2px solid #dee2e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mock-card h2 {
|
|
||||||
margin: 0 0 20px 0;
|
|
||||||
font-size: 1.4rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #6c757d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mock-metrics {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mock-metric {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 12px;
|
|
||||||
background: #e9ecef;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid #dee2e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mock-metric .label {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #6c757d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mock-metric .value.mock {
|
|
||||||
font-weight: 700;
|
|
||||||
color: #6c757d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mock-services {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mock-service {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 16px;
|
|
||||||
background: #e9ecef;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid #dee2e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-name {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #6c757d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-status.mock {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 500;
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: #f8d7da;
|
|
||||||
color: #721c24;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mock-logs {
|
|
||||||
background: #2d3748;
|
|
||||||
color: #6c757d;
|
|
||||||
padding: 16px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
max-height: 200px;
|
|
||||||
overflow-y: auto;
|
|
||||||
border: 1px solid #dee2e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mock-deploy {
|
|
||||||
text-align: center;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mock-deploy p {
|
|
||||||
color: #6c757d;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mock-btn {
|
|
||||||
padding: 12px 24px;
|
|
||||||
background: #6c757d;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: not-allowed;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mock-backups {
|
|
||||||
text-align: center;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mock-backups p {
|
|
||||||
color: #6c757d;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mock-backup-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mock-backup-item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 16px;
|
|
||||||
background: #e9ecef;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid #dee2e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.backup-name {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #6c757d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.backup-date, .backup-size {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: #6c757d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.call-to-action {
|
|
||||||
background: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 32px;
|
|
||||||
text-align: center;
|
|
||||||
border: 2px solid #bbdefb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.call-to-action h2 {
|
|
||||||
margin: 0 0 16px 0;
|
|
||||||
color: var(--color-primary);
|
|
||||||
font-size: 1.6rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.call-to-action p {
|
|
||||||
color: #6c757d;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setup-btn {
|
|
||||||
padding: 16px 32px;
|
|
||||||
background: var(--color-primary);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setup-btn:hover:not(:disabled) {
|
|
||||||
background: var(--color-primary-dark);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.setup-btn:disabled {
|
|
||||||
background: #e0e0e0 !important;
|
|
||||||
color: #aaa !important;
|
|
||||||
cursor: not-allowed !important;
|
|
||||||
transform: none !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Адаптивность */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.mock-header {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mock-metrics {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mock-service, .mock-backup-item {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -151,7 +151,7 @@ const handleSubmit = async () => {
|
|||||||
// Перенаправляем на страницу VDS Mock через 3 секунды
|
// Перенаправляем на страницу VDS Mock через 3 секунды
|
||||||
addLog('info', 'Перенаправление на страницу управления VDS через 3 секунды...');
|
addLog('info', 'Перенаправление на страницу управления VDS через 3 секунды...');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
router.push({ name: 'vds-mock' });
|
router.push({ name: 'vds-management' });
|
||||||
}, 3000);
|
}, 3000);
|
||||||
} else {
|
} else {
|
||||||
addLog('error', result.message || 'Ошибка при настройке VDS');
|
addLog('error', result.message || 'Ошибка при настройке VDS');
|
||||||
|
|||||||
@@ -328,6 +328,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz#7358043433b2e5da569aa02cbc4c121da3af27d7"
|
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz#7358043433b2e5da569aa02cbc4c121da3af27d7"
|
||||||
integrity sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==
|
integrity sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==
|
||||||
|
|
||||||
|
"@kurkle/color@^0.3.0":
|
||||||
|
version "0.3.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.4.tgz#4d4ff677e1609214fc71c580125ddddd86abcabf"
|
||||||
|
integrity sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==
|
||||||
|
|
||||||
"@noble/curves@1.2.0":
|
"@noble/curves@1.2.0":
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35"
|
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35"
|
||||||
@@ -890,6 +895,13 @@ chalk@^4.0.0:
|
|||||||
ansi-styles "^4.1.0"
|
ansi-styles "^4.1.0"
|
||||||
supports-color "^7.1.0"
|
supports-color "^7.1.0"
|
||||||
|
|
||||||
|
chart.js@^4.5.1:
|
||||||
|
version "4.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.5.1.tgz#19dd1a9a386a3f6397691672231cb5fc9c052c35"
|
||||||
|
integrity sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==
|
||||||
|
dependencies:
|
||||||
|
"@kurkle/color" "^0.3.0"
|
||||||
|
|
||||||
color-convert@^2.0.1:
|
color-convert@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
|
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
|
||||||
|
|||||||
@@ -128,6 +128,19 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- backend_node_modules:/app/node_modules
|
- backend_node_modules:/app/node_modules
|
||||||
- ./ssl:/app/ssl:ro
|
- ./ssl:/app/ssl:ro
|
||||||
|
# Доступ к Docker socket для управления контейнерами на VDS
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
# Доступ к системным файловым системам для мониторинга и управления
|
||||||
|
# Монтируем напрямую для работы команд top, free, ps и т.д.
|
||||||
|
- /proc:/host/proc:ro
|
||||||
|
- /sys:/host/sys:ro
|
||||||
|
# Также монтируем в стандартные пути для совместимости (read-only для безопасности)
|
||||||
|
- /proc:/proc:ro
|
||||||
|
- /sys:/sys:ro
|
||||||
|
# Добавляем необходимые capabilities для управления системой
|
||||||
|
cap_add:
|
||||||
|
- SYS_ADMIN # Для управления системой (reboot, shutdown, useradd и т.д.)
|
||||||
|
- SYS_TIME # Для управления временем системы
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
- PORT=8000
|
- PORT=8000
|
||||||
|
|||||||
Reference in New Issue
Block a user