ваше сообщение коммита
This commit is contained in:
@@ -19,13 +19,17 @@ LABEL website="https://hb3-accelerator.com"
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Устанавливаем системные зависимости для компиляции нативных модулей Node.js
|
# Устанавливаем системные зависимости для компиляции нативных модулей Node.js
|
||||||
RUN apt-get update && apt-get install -y \
|
# Устанавливаем базовые пакеты отдельно от компиляторов для большей надежности
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --fix-missing \
|
||||||
python3 \
|
python3 \
|
||||||
make \
|
make \
|
||||||
g++ \
|
|
||||||
curl \
|
curl \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
openssh-client && \
|
||||||
|
apt-get install -y --fix-missing g++ || \
|
||||||
|
(sleep 10 && apt-get update && apt-get install -y --fix-missing g++) && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Docker CLI НЕ устанавливаем - используем Docker Socket + dockerode SDK
|
# Docker CLI НЕ устанавливаем - используем Docker Socket + dockerode SDK
|
||||||
|
|
||||||
|
|||||||
@@ -172,29 +172,29 @@ 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' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Получаем базовый URL из БД (домен VDS) или используем текущий хост из запроса
|
// ВАЖНО: Для SIWE сообщения ВСЕГДА используем хост из запроса, чтобы он совпадал с фронтендом
|
||||||
const consentService = require('../services/consentService');
|
// Фронтенд использует window.location.host и window.location.origin, поэтому бэкенд должен использовать то же самое
|
||||||
const baseUrl = await consentService.getBaseUrl();
|
// Это означает, что даже если в БД есть домен (например, 185.221.214.140), для SIWE будет использоваться
|
||||||
|
// хост из текущего запроса (например, localhost:9000), если запрос приходит с localhost
|
||||||
// Если домена нет в БД, используем текущий хост из запроса (более надежно, чем origin)
|
|
||||||
let baseUrlForResources;
|
|
||||||
if (baseUrl !== 'http://localhost:9000') {
|
|
||||||
// Домен есть в БД - используем его
|
|
||||||
baseUrlForResources = baseUrl;
|
|
||||||
} else {
|
|
||||||
// Домена нет в БД - используем текущий хост из запроса
|
|
||||||
const protocol = req.protocol || 'http';
|
const protocol = req.protocol || 'http';
|
||||||
let host = req.get('host') || 'localhost:9000';
|
let host = req.get('host') || 'localhost:9000';
|
||||||
|
|
||||||
|
logger.info(`[verify] Request protocol: ${protocol}, host header: ${req.get('host')}, original host: ${host}`);
|
||||||
|
|
||||||
// Убеждаемся, что порт присутствует для localhost
|
// Убеждаемся, что порт присутствует для localhost
|
||||||
if (host === 'localhost' || host.startsWith('localhost:')) {
|
if (host === 'localhost' || host.startsWith('localhost:')) {
|
||||||
if (!host.includes(':')) {
|
if (!host.includes(':')) {
|
||||||
// Если порта нет, добавляем стандартный порт для протокола
|
// Если порта нет, добавляем стандартный порт для протокола
|
||||||
const defaultPort = protocol === 'https' ? '443' : '9000';
|
const defaultPort = protocol === 'https' ? '443' : '9000';
|
||||||
host = `${host}:${defaultPort}`;
|
host = `${host}:${defaultPort}`;
|
||||||
|
logger.info(`[verify] Added default port to localhost: ${host}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
baseUrlForResources = `${protocol}://${host}`;
|
|
||||||
}
|
// Формируем domain и origin для SIWE сообщения из текущего запроса
|
||||||
|
// domain - это host (например, "localhost:9000" или "example.com:443")
|
||||||
|
// ВАЖНО: domain и origin для SIWE НИКОГДА не берутся из БД, только из запроса!
|
||||||
|
const baseUrlForResources = `${protocol}://${host}`;
|
||||||
|
|
||||||
// Извлекаем домен и origin из baseUrlForResources для SIWE сообщения
|
// Извлекаем домен и origin из baseUrlForResources для SIWE сообщения
|
||||||
const baseUrlObj = new URL(baseUrlForResources);
|
const baseUrlObj = new URL(baseUrlForResources);
|
||||||
|
|||||||
@@ -23,6 +23,40 @@ const encryptedDb = require('../services/encryptedDatabaseService');
|
|||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сохранить настройки VDS в базу данных
|
||||||
|
* @param {Object} settings - Объект с настройками для сохранения
|
||||||
|
* @returns {Promise<Object>} - Сохраненная запись
|
||||||
|
*/
|
||||||
|
async function saveVdsSettingsToDb(settings) {
|
||||||
|
// Проверяем существующие настройки
|
||||||
|
const existing = await encryptedDb.getData('vds_settings', {}, 1);
|
||||||
|
|
||||||
|
if (existing.length > 0) {
|
||||||
|
// UPDATE существующей записи
|
||||||
|
return await encryptedDb.saveData('vds_settings', settings, { id: existing[0].id });
|
||||||
|
} else {
|
||||||
|
// INSERT новой записи
|
||||||
|
return await encryptedDb.saveData('vds_settings', {
|
||||||
|
...settings,
|
||||||
|
created_at: new Date()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обновить домен в process.env и сбросить кэш
|
||||||
|
* @param {string} domain - Домен для установки
|
||||||
|
*/
|
||||||
|
function updateDomainCache(domain) {
|
||||||
|
// Обновляем process.env.BASE_URL для текущего процесса
|
||||||
|
process.env.BASE_URL = `https://${domain}`;
|
||||||
|
|
||||||
|
// Сбрасываем кэш домена в consentService
|
||||||
|
const consentService = require('../services/consentService');
|
||||||
|
consentService.clearDomainCache();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Получить настройки VDS
|
* Получить настройки VDS
|
||||||
*/
|
*/
|
||||||
@@ -31,6 +65,7 @@ router.get('/settings', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTIN
|
|||||||
const encryptionUtils = require('../utils/encryptionUtils');
|
const encryptionUtils = require('../utils/encryptionUtils');
|
||||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||||
|
|
||||||
|
try {
|
||||||
const { rows } = await db.getQuery()(
|
const { rows } = await db.getQuery()(
|
||||||
`SELECT
|
`SELECT
|
||||||
id,
|
id,
|
||||||
@@ -67,6 +102,21 @@ router.get('/settings', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTIN
|
|||||||
// sshPassword не возвращаем по соображениям безопасности
|
// sshPassword не возвращаем по соображениям безопасности
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} catch (decryptError) {
|
||||||
|
// Если ошибка расшифровки (некорректные данные в БД), очищаем их и возвращаем null
|
||||||
|
if (decryptError.message && decryptError.message.includes('decoding base64')) {
|
||||||
|
logger.warn('[VDS] Ошибка расшифровки настроек (некорректные данные в БД). Очищаем некорректные данные из таблицы vds_settings.');
|
||||||
|
try {
|
||||||
|
// Автоматически очищаем некорректные данные из БД
|
||||||
|
await db.getQuery()('DELETE FROM vds_settings');
|
||||||
|
logger.info('[VDS] Некорректные настройки VDS удалены из таблицы vds_settings. Создайте новые настройки через интерфейс.');
|
||||||
|
} catch (deleteError) {
|
||||||
|
logger.error('[VDS] Ошибка при удалении некорректных настроек:', deleteError);
|
||||||
|
}
|
||||||
|
return res.json({ success: true, settings: null });
|
||||||
|
}
|
||||||
|
throw decryptError; // Пробрасываем другие ошибки
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[VDS] Ошибка получения настроек:', error);
|
logger.error('[VDS] Ошибка получения настроек:', error);
|
||||||
res.status(500).json({ success: false, error: error.message });
|
res.status(500).json({ success: false, error: error.message });
|
||||||
@@ -92,27 +142,14 @@ router.post('/settings', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTI
|
|||||||
// Если передан только домен (для обратной совместимости)
|
// Если передан только домен (для обратной совместимости)
|
||||||
if (domain && !email && !sshHost) {
|
if (domain && !email && !sshHost) {
|
||||||
const normalizedDomain = domain.trim().toLowerCase().replace(/^https?:\/\//, '').replace(/\/$/, '');
|
const normalizedDomain = domain.trim().toLowerCase().replace(/^https?:\/\//, '').replace(/\/$/, '');
|
||||||
const existing = await encryptedDb.getData('vds_settings', {}, 1);
|
|
||||||
|
|
||||||
const settings = {
|
const settings = {
|
||||||
domain_encrypted: normalizedDomain, // encryptedDb автоматически зашифрует
|
domain_encrypted: normalizedDomain, // encryptedDb автоматически зашифрует
|
||||||
updated_at: new Date()
|
updated_at: new Date()
|
||||||
};
|
};
|
||||||
|
|
||||||
if (existing.length > 0) {
|
await saveVdsSettingsToDb(settings);
|
||||||
await encryptedDb.saveData('vds_settings', settings, { id: existing[0].id });
|
updateDomainCache(normalizedDomain);
|
||||||
} 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}`);
|
logger.info(`[VDS] Домен сохранен: ${normalizedDomain}`);
|
||||||
return res.json({ success: true, domain: normalizedDomain });
|
return res.json({ success: true, domain: normalizedDomain });
|
||||||
@@ -129,12 +166,9 @@ router.post('/settings', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTI
|
|||||||
// Нормализуем домен
|
// Нормализуем домен
|
||||||
const normalizedDomain = domain.trim().toLowerCase().replace(/^https?:\/\//, '').replace(/\/$/, '');
|
const normalizedDomain = domain.trim().toLowerCase().replace(/^https?:\/\//, '').replace(/\/$/, '');
|
||||||
|
|
||||||
// Проверяем существующие настройки
|
// Проверяем существующие настройки (для валидации пароля)
|
||||||
const existing = await encryptedDb.getData('vds_settings', {}, 1);
|
const existing = await encryptedDb.getData('vds_settings', {}, 1);
|
||||||
|
|
||||||
const encryptionUtils = require('../utils/encryptionUtils');
|
|
||||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
|
||||||
|
|
||||||
// Подготавливаем данные для сохранения с правильными именами полей для шифрования
|
// Подготавливаем данные для сохранения с правильными именами полей для шифрования
|
||||||
const settings = {
|
const settings = {
|
||||||
domain_encrypted: normalizedDomain, // encryptedDb автоматически зашифрует поля с _encrypted
|
domain_encrypted: normalizedDomain, // encryptedDb автоматически зашифрует поля с _encrypted
|
||||||
@@ -159,21 +193,8 @@ router.post('/settings', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTI
|
|||||||
}
|
}
|
||||||
// Если пароль не указан (undefined/null/пустая строка) и настройки уже есть - не обновляем пароль
|
// Если пароль не указан (undefined/null/пустая строка) и настройки уже есть - не обновляем пароль
|
||||||
|
|
||||||
if (existing.length > 0) {
|
await saveVdsSettingsToDb(settings);
|
||||||
await encryptedDb.saveData('vds_settings', settings, { id: existing[0].id });
|
updateDomainCache(normalizedDomain);
|
||||||
} 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}`);
|
logger.info(`[VDS] Настройки сохранены: ${normalizedDomain}`);
|
||||||
res.json({ success: true, settings });
|
res.json({ success: true, settings });
|
||||||
@@ -195,18 +216,145 @@ async function checkDockerAvailable() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить настройки VDS из базы данных
|
||||||
|
*/
|
||||||
|
async function getVdsSettings() {
|
||||||
|
try {
|
||||||
|
const encryptionUtils = require('../utils/encryptionUtils');
|
||||||
|
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||||
|
|
||||||
|
const { rows } = await db.getQuery()(
|
||||||
|
`SELECT
|
||||||
|
decrypt_text(domain_encrypted, $1) as domain,
|
||||||
|
decrypt_text(email_encrypted, $1) as email,
|
||||||
|
decrypt_text(ubuntu_user_encrypted, $1) as ubuntu_user,
|
||||||
|
decrypt_text(docker_user_encrypted, $1) as docker_user,
|
||||||
|
decrypt_text(ssh_host_encrypted, $1) as ssh_host,
|
||||||
|
ssh_port,
|
||||||
|
decrypt_text(ssh_user_encrypted, $1) as ssh_user,
|
||||||
|
decrypt_text(ssh_password_encrypted, $1) as ssh_password
|
||||||
|
FROM vds_settings
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT 1`,
|
||||||
|
[encryptionKey]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (rows.length > 0 && rows[0].ssh_host && rows[0].ssh_user) {
|
||||||
|
return {
|
||||||
|
domain: rows[0].domain,
|
||||||
|
email: rows[0].email,
|
||||||
|
ubuntuUser: rows[0].ubuntu_user,
|
||||||
|
dockerUser: rows[0].docker_user,
|
||||||
|
sshHost: rows[0].ssh_host,
|
||||||
|
sshPort: rows[0].ssh_port || 22,
|
||||||
|
sshUser: rows[0].ssh_user,
|
||||||
|
sshPassword: rows[0].ssh_password
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (decryptError) {
|
||||||
|
// Если ошибка расшифровки (некорректные данные в БД), очищаем их и возвращаем null
|
||||||
|
if (decryptError.message && decryptError.message.includes('decoding base64')) {
|
||||||
|
logger.warn('[VDS] Ошибка расшифровки настроек (некорректные данные в БД). Очищаем некорректные данные из таблицы vds_settings.');
|
||||||
|
try {
|
||||||
|
// Автоматически очищаем некорректные данные из БД
|
||||||
|
await db.getQuery()('DELETE FROM vds_settings');
|
||||||
|
logger.info('[VDS] Некорректные настройки VDS удалены из таблицы vds_settings. Создайте новые настройки через интерфейс.');
|
||||||
|
} catch (deleteError) {
|
||||||
|
logger.error('[VDS] Ошибка при удалении некорректных настроек:', deleteError);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Для других ошибок просто логируем
|
||||||
|
logger.warn('[VDS] Не удалось получить настройки VDS из базы данных:', decryptError.message);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Выполнить команду Docker (локально или через SSH на VDS)
|
||||||
|
*/
|
||||||
|
async function execDockerCommand(command) {
|
||||||
|
const vdsSettings = await getVdsSettings();
|
||||||
|
|
||||||
|
if (vdsSettings && vdsSettings.sshHost && vdsSettings.sshUser) {
|
||||||
|
// Выполняем через SSH на VDS
|
||||||
|
return await execSshCommandOnVds(command, vdsSettings);
|
||||||
|
} else {
|
||||||
|
// Выполняем локально
|
||||||
|
try {
|
||||||
|
const { stdout, stderr } = await execAsync(command);
|
||||||
|
return { code: 0, stdout, stderr };
|
||||||
|
} catch (error) {
|
||||||
|
return { code: error.code || 1, stdout: error.stdout || '', stderr: error.stderr || error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Выполнить SSH команду на VDS
|
||||||
|
*/
|
||||||
|
async function execSshCommandOnVds(command, settings) {
|
||||||
|
const { sshHost, sshPort = 22, sshUser, sshPassword } = settings;
|
||||||
|
|
||||||
|
// Экранируем команду для SSH
|
||||||
|
// Экранируем двойные кавычки и знаки доллара для правильной передачи через SSH
|
||||||
|
const escapedCommand = command
|
||||||
|
.replace(/\\/g, '\\\\') // Сначала экранируем обратные слеши
|
||||||
|
.replace(/\$/g, '\\$') // Экранируем знаки доллара
|
||||||
|
.replace(/"/g, '\\"'); // Экранируем двойные кавычки
|
||||||
|
|
||||||
|
// Базовые опции SSH
|
||||||
|
const sshOptions = `-p ${sshPort} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR`;
|
||||||
|
|
||||||
|
// Строим SSH команду
|
||||||
|
let sshCommand;
|
||||||
|
if (sshPassword && sshPassword.trim()) {
|
||||||
|
// Используем sshpass для подключения с паролем (если пароль указан)
|
||||||
|
sshCommand = `sshpass -p "${sshPassword.replace(/"/g, '\\"')}" ssh ${sshOptions} ${sshUser}@${sshHost} "${escapedCommand}"`;
|
||||||
|
} else {
|
||||||
|
// Используем SSH ключи (по умолчанию из ~/.ssh/id_rsa или ~/.ssh/id_ed25519)
|
||||||
|
// SSH автоматически найдет ключ в ~/.ssh/
|
||||||
|
sshCommand = `ssh ${sshOptions} ${sshUser}@${sshHost} "${escapedCommand}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { stdout, stderr } = await execAsync(sshCommand);
|
||||||
|
return { code: 0, stdout, stderr };
|
||||||
|
} catch (error) {
|
||||||
|
return { code: error.code || 1, stdout: error.stdout || '', stderr: error.stderr || error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Получить список контейнеров
|
* Получить список контейнеров
|
||||||
*/
|
*/
|
||||||
router.get('/containers', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
|
router.get('/containers', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const dockerAvailable = await checkDockerAvailable();
|
const vdsSettings = await getVdsSettings();
|
||||||
if (!dockerAvailable) {
|
|
||||||
return res.json({ success: true, containers: [], message: 'Docker недоступен (работает локально, не на VDS)' });
|
// Проверяем, есть ли настройки VDS или локальный Docker доступен
|
||||||
|
if (!vdsSettings && !(await checkDockerAvailable())) {
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
containers: [],
|
||||||
|
message: 'VDS не настроена и Docker недоступен локально'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { stdout } = await execAsync('docker ps -a --format "{{.Names}}|{{.Status}}|{{.Image}}"');
|
const result = await execDockerCommand('docker ps -a --format "{{.Names}}|{{.Status}}|{{.Image}}"');
|
||||||
const containers = stdout.trim().split('\n').filter(line => line.trim()).map(line => {
|
|
||||||
|
if (result.code !== 0) {
|
||||||
|
logger.error(`[VDS] Ошибка выполнения Docker команды: ${result.stderr}`);
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: `Не удалось получить список контейнеров: ${result.stderr}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const containers = result.stdout.trim().split('\n')
|
||||||
|
.filter(line => line.trim())
|
||||||
|
.map(line => {
|
||||||
const [name, status, image] = line.split('|');
|
const [name, status, image] = line.split('|');
|
||||||
return { name, status, image };
|
return { name, status, image };
|
||||||
});
|
});
|
||||||
@@ -229,7 +377,12 @@ router.post('/containers/:name/restart', requireAuth, requirePermission(PERMISSI
|
|||||||
return res.status(400).json({ success: false, error: 'Имя контейнера обязательно' });
|
return res.status(400).json({ success: false, error: 'Имя контейнера обязательно' });
|
||||||
}
|
}
|
||||||
|
|
||||||
await execAsync(`docker restart ${name}`);
|
const result = await execDockerCommand(`docker restart ${name}`);
|
||||||
|
|
||||||
|
if (result.code !== 0) {
|
||||||
|
return res.status(500).json({ success: false, error: result.stderr || 'Не удалось перезапустить контейнер' });
|
||||||
|
}
|
||||||
|
|
||||||
logger.info(`[VDS] Контейнер ${name} перезапущен`);
|
logger.info(`[VDS] Контейнер ${name} перезапущен`);
|
||||||
res.json({ success: true, message: `Контейнер ${name} перезапущен` });
|
res.json({ success: true, message: `Контейнер ${name} перезапущен` });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -243,14 +396,24 @@ router.post('/containers/:name/restart', requireAuth, requirePermission(PERMISSI
|
|||||||
*/
|
*/
|
||||||
router.post('/containers/restart-all', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
|
router.post('/containers/restart-all', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execAsync('docker ps -q');
|
const result = await execDockerCommand('docker ps -q');
|
||||||
const containerIds = stdout.trim().split('\n').filter(id => id.trim());
|
|
||||||
|
if (result.code !== 0) {
|
||||||
|
return res.status(500).json({ success: false, error: result.stderr || 'Не удалось получить список контейнеров' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerIds = result.stdout.trim().split('\n').filter(id => id.trim());
|
||||||
|
|
||||||
if (containerIds.length === 0) {
|
if (containerIds.length === 0) {
|
||||||
return res.json({ success: true, message: 'Нет запущенных контейнеров', restarted: 0 });
|
return res.json({ success: true, message: 'Нет запущенных контейнеров', restarted: 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
await execAsync(`docker restart ${containerIds.join(' ')}`);
|
const restartResult = await execDockerCommand(`docker restart ${containerIds.join(' ')}`);
|
||||||
|
|
||||||
|
if (restartResult.code !== 0) {
|
||||||
|
return res.status(500).json({ success: false, error: restartResult.stderr || 'Не удалось перезапустить контейнеры' });
|
||||||
|
}
|
||||||
|
|
||||||
logger.info(`[VDS] Перезапущено контейнеров: ${containerIds.length}`);
|
logger.info(`[VDS] Перезапущено контейнеров: ${containerIds.length}`);
|
||||||
res.json({ success: true, message: `Перезапущено контейнеров: ${containerIds.length}`, restarted: containerIds.length });
|
res.json({ success: true, message: `Перезапущено контейнеров: ${containerIds.length}`, restarted: containerIds.length });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -271,24 +434,28 @@ router.post('/containers/:name/rebuild', requireAuth, requirePermission(PERMISSI
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Получаем информацию о контейнере
|
// Получаем информацию о контейнере
|
||||||
const { stdout: inspectOutput } = await execAsync(`docker inspect ${name} --format '{{.Config.Image}}'`);
|
const inspectResult = await execDockerCommand(`docker inspect ${name} --format '{{.Config.Image}}'`);
|
||||||
const imageName = inspectOutput.trim();
|
if (inspectResult.code !== 0) {
|
||||||
|
return res.status(404).json({ success: false, error: 'Контейнер не найден' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageName = inspectResult.stdout.trim();
|
||||||
if (!imageName) {
|
if (!imageName) {
|
||||||
return res.status(404).json({ success: false, error: 'Контейнер не найден' });
|
return res.status(404).json({ success: false, error: 'Контейнер не найден' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Останавливаем контейнер
|
// Останавливаем контейнер
|
||||||
await execAsync(`docker stop ${name}`).catch(() => {});
|
await execDockerCommand(`docker stop ${name}`);
|
||||||
|
|
||||||
// Удаляем контейнер
|
// Удаляем контейнер
|
||||||
await execAsync(`docker rm ${name}`).catch(() => {});
|
await execDockerCommand(`docker rm ${name}`);
|
||||||
|
|
||||||
// Пересобираем образ (если есть Dockerfile)
|
// Пересобираем образ (если есть Dockerfile)
|
||||||
// Для простоты просто пересоздаем контейнер из образа
|
// Для простоты просто пересоздаем контейнер из образа
|
||||||
await execAsync(`docker run -d --name ${name} ${imageName}`).catch(() => {
|
const runResult = await execDockerCommand(`docker run -d --name ${name} ${imageName}`);
|
||||||
throw new Error('Не удалось пересоздать контейнер. Возможно, нужны дополнительные параметры запуска.');
|
if (runResult.code !== 0) {
|
||||||
});
|
return res.status(500).json({ success: false, error: 'Не удалось пересоздать контейнер. Возможно, нужны дополнительные параметры запуска.' });
|
||||||
|
}
|
||||||
|
|
||||||
logger.info(`[VDS] Контейнер ${name} пересобран`);
|
logger.info(`[VDS] Контейнер ${name} пересобран`);
|
||||||
res.json({ success: true, message: `Контейнер ${name} пересобран` });
|
res.json({ success: true, message: `Контейнер ${name} пересобран` });
|
||||||
@@ -305,13 +472,106 @@ router.get('/stats', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS)
|
|||||||
try {
|
try {
|
||||||
const { period = '1h' } = req.query; // 1h, 6h, 24h, 7d
|
const { period = '1h' } = req.query; // 1h, 6h, 24h, 7d
|
||||||
|
|
||||||
// Получаем текущую статистику CPU и RAM
|
// Получаем текущую статистику CPU и RAM с VDS сервера
|
||||||
let cpuUsage = 0;
|
let cpuUsage = 0;
|
||||||
let ramUsage = 0;
|
let ramUsage = 0;
|
||||||
|
let ramTotal = 0;
|
||||||
|
let ramUsed = 0;
|
||||||
let totalTraffic = 0;
|
let totalTraffic = 0;
|
||||||
let rxBytes = 0;
|
let rxBytes = 0;
|
||||||
let txBytes = 0;
|
let txBytes = 0;
|
||||||
|
let cpuCores = 0;
|
||||||
|
|
||||||
|
// Получаем статистику по контейнерам (если Docker доступен)
|
||||||
|
let containers = [];
|
||||||
|
const vdsSettings = await getVdsSettings();
|
||||||
|
|
||||||
|
// Если есть настройки VDS, выполняем команды на VDS сервере
|
||||||
|
if (vdsSettings) {
|
||||||
|
try {
|
||||||
|
// CPU usage - используем упрощенную команду через /proc/stat
|
||||||
|
// $ будет экранирован в execSshCommandOnVds
|
||||||
|
const procCpuResult = await execDockerCommand('head -n1 /proc/stat | awk \'{idle=$5+$6; total=$2+$3+$4+$5+$6+$7+$8+$9; if(total>0) print (100*(total-idle)/total); else print 0}\'');
|
||||||
|
if (procCpuResult.code === 0 && procCpuResult.stdout && procCpuResult.stdout.trim()) {
|
||||||
|
const parsed = parseFloat(procCpuResult.stdout.trim());
|
||||||
|
if (!isNaN(parsed) && parsed >= 0 && parsed <= 100) {
|
||||||
|
cpuUsage = parsed;
|
||||||
|
logger.info(`[VDS] CPU usage получен через /proc/stat: ${cpuUsage}%`);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Invalid CPU value: ${parsed}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`Command failed: code=${procCpuResult.code}, stderr=${procCpuResult.stderr}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('[VDS] Не удалось получить CPU через /proc/stat, пробуем top:', error.message);
|
||||||
|
try {
|
||||||
|
// Fallback: через top - упрощенная команда (idle обычно предпоследнее значение)
|
||||||
|
const cpuResult = await execDockerCommand('top -bn1 | grep "%Cpu(s)" | awk \'{print 100-$(NF-2)}\' | sed \'s/%//\'');
|
||||||
|
if (cpuResult.code === 0 && cpuResult.stdout && cpuResult.stdout.trim()) {
|
||||||
|
cpuUsage = parseFloat(cpuResult.stdout.trim()) || 0;
|
||||||
|
logger.info(`[VDS] CPU usage получен через top: ${cpuUsage}%`);
|
||||||
|
}
|
||||||
|
} catch (topError) {
|
||||||
|
logger.warn('[VDS] Не удалось получить CPU usage:', topError.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// RAM usage и total - $ будет экранирован в execSshCommandOnVds
|
||||||
|
const memResult = await execDockerCommand('free -m | awk \'NR==2{usage=$3*100/$2; printf "%.2f %d %d", usage, $2, $3}\'');
|
||||||
|
if (memResult.code === 0 && memResult.stdout && memResult.stdout.trim()) {
|
||||||
|
const parts = memResult.stdout.trim().split(' ');
|
||||||
|
if (parts.length >= 3) {
|
||||||
|
ramUsage = parseFloat(parts[0]) || 0;
|
||||||
|
ramTotal = parseInt(parts[1]) || 0;
|
||||||
|
ramUsed = parseInt(parts[2]) || 0;
|
||||||
|
logger.info(`[VDS] RAM получена с VDS: usage=${ramUsage}%, total=${ramTotal}MB, used=${ramUsed}MB (raw: ${memResult.stdout.trim()})`);
|
||||||
|
} else {
|
||||||
|
logger.warn('[VDS] Неверный формат RAM данных:', memResult.stdout);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn('[VDS] Не удалось получить RAM, code:', memResult.code, 'stdout:', memResult.stdout, 'stderr:', memResult.stderr);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('[VDS] Не удалось получить статистику RAM:', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// CPU cores
|
||||||
|
const coresResult = await execDockerCommand('nproc');
|
||||||
|
if (coresResult.code === 0 && coresResult.stdout) {
|
||||||
|
cpuCores = parseInt(coresResult.stdout.trim()) || 0;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('[VDS] Не удалось получить количество ядер CPU:', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Network traffic
|
||||||
|
const networkResult = await execDockerCommand('cat /proc/net/dev | awk \'NR>2 {rx+=$2; tx+=$10} END {print rx, tx}\'');
|
||||||
|
if (networkResult.code === 0 && networkResult.stdout) {
|
||||||
|
[rxBytes, txBytes] = networkResult.stdout.trim().split(' ').map(Number);
|
||||||
|
totalTraffic = (rxBytes + txBytes) / 1024 / 1024; // MB
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('[VDS] Не удалось получить статистику трафика:', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Docker containers stats
|
||||||
|
const result = await execDockerCommand('docker stats --no-stream --format "{{.Name}}|{{.CPUPerc}}|{{.MemUsage}}|{{.NetIO}}"');
|
||||||
|
if (result.code === 0 && result.stdout) {
|
||||||
|
containers = result.stdout.trim().split('\n').filter(line => line.trim()).map(line => {
|
||||||
|
const [name, cpu, mem, net] = line.split('|');
|
||||||
|
return { name, cpu, mem, net };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('[VDS] Не удалось получить статистику контейнеров:', error.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback: локальное выполнение (если VDS не настроена)
|
||||||
try {
|
try {
|
||||||
const { stdout: cpuRam } = await execAsync('top -bn1 | grep "Cpu(s)" | sed "s/.*, *\\([0-9.]*\\)%* id.*/\\1/" | awk \'{print 100 - $1}\'');
|
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;
|
cpuUsage = parseFloat(cpuRam.trim()) || 0;
|
||||||
@@ -320,14 +580,16 @@ router.get('/stats', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS)
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { stdout: memInfo } = await execAsync('free -m | awk \'NR==2{printf "%.2f", $3*100/$2}\'');
|
const { stdout: memInfo } = await execAsync('free -m | awk \'NR==2{printf "%.2f %d %d", $3*100/$2, $2, $3}\'');
|
||||||
ramUsage = parseFloat(memInfo.trim()) || 0;
|
const [usage, total, used] = memInfo.trim().split(' ').map(Number);
|
||||||
|
ramUsage = usage || 0;
|
||||||
|
ramTotal = total || 0;
|
||||||
|
ramUsed = used || 0;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn('[VDS] Не удалось получить статистику RAM:', error.message);
|
logger.warn('[VDS] Не удалось получить статистику RAM:', error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Используем /host/proc если доступно, иначе /proc
|
|
||||||
const procPath = require('fs').existsSync('/host/proc') ? '/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}'`);
|
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);
|
[rxBytes, txBytes] = networkStats.trim().split(' ').map(Number);
|
||||||
@@ -336,33 +598,20 @@ router.get('/stats', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS)
|
|||||||
logger.warn('[VDS] Не удалось получить статистику трафика:', error.message);
|
logger.warn('[VDS] Не удалось получить статистику трафика:', error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Получаем статистику по контейнерам (если Docker доступен)
|
cpuCores = require('os').cpus().length;
|
||||||
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({
|
const responseData = {
|
||||||
success: true,
|
success: true,
|
||||||
stats: {
|
stats: {
|
||||||
cpu: {
|
cpu: {
|
||||||
usage: cpuUsage,
|
usage: cpuUsage,
|
||||||
cores: require('os').cpus().length
|
cores: cpuCores || require('os').cpus().length
|
||||||
},
|
},
|
||||||
ram: {
|
ram: {
|
||||||
usage: ramUsage,
|
usage: ramUsage,
|
||||||
total: Math.round(require('os').totalmem() / 1024 / 1024), // MB
|
total: ramTotal || Math.round(require('os').totalmem() / 1024 / 1024), // MB
|
||||||
used: Math.round(require('os').totalmem() / 1024 / 1024 * ramUsage / 100) // MB
|
used: ramUsed || Math.round((ramTotal || require('os').totalmem() / 1024 / 1024) * ramUsage / 100) // MB
|
||||||
},
|
},
|
||||||
traffic: {
|
traffic: {
|
||||||
total: totalTraffic, // MB
|
total: totalTraffic, // MB
|
||||||
@@ -372,7 +621,11 @@ router.get('/stats', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS)
|
|||||||
containers
|
containers
|
||||||
},
|
},
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString()
|
||||||
});
|
};
|
||||||
|
|
||||||
|
logger.info(`[VDS] Статистика отправлена: CPU=${cpuUsage}%, RAM=${ramUsage}% (${ramUsed}/${ramTotal}MB), Traffic=${totalTraffic}MB`);
|
||||||
|
|
||||||
|
res.json(responseData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[VDS] Ошибка получения статистики:', error);
|
logger.error('[VDS] Ошибка получения статистики:', error);
|
||||||
res.status(500).json({ success: false, error: error.message });
|
res.status(500).json({ success: false, error: error.message });
|
||||||
@@ -385,7 +638,12 @@ router.get('/stats', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS)
|
|||||||
router.post('/containers/:name/stop', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
|
router.post('/containers/:name/stop', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { name } = req.params;
|
const { name } = req.params;
|
||||||
await execAsync(`docker stop ${name}`);
|
const result = await execDockerCommand(`docker stop ${name}`);
|
||||||
|
|
||||||
|
if (result.code !== 0) {
|
||||||
|
return res.status(500).json({ success: false, error: result.stderr || 'Не удалось остановить контейнер' });
|
||||||
|
}
|
||||||
|
|
||||||
logger.info(`[VDS] Контейнер ${name} остановлен`);
|
logger.info(`[VDS] Контейнер ${name} остановлен`);
|
||||||
res.json({ success: true, message: `Контейнер ${name} остановлен` });
|
res.json({ success: true, message: `Контейнер ${name} остановлен` });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -400,7 +658,12 @@ router.post('/containers/:name/stop', requireAuth, requirePermission(PERMISSIONS
|
|||||||
router.post('/containers/:name/start', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
|
router.post('/containers/:name/start', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { name } = req.params;
|
const { name } = req.params;
|
||||||
await execAsync(`docker start ${name}`);
|
const result = await execDockerCommand(`docker start ${name}`);
|
||||||
|
|
||||||
|
if (result.code !== 0) {
|
||||||
|
return res.status(500).json({ success: false, error: result.stderr || 'Не удалось запустить контейнер' });
|
||||||
|
}
|
||||||
|
|
||||||
logger.info(`[VDS] Контейнер ${name} запущен`);
|
logger.info(`[VDS] Контейнер ${name} запущен`);
|
||||||
res.json({ success: true, message: `Контейнер ${name} запущен` });
|
res.json({ success: true, message: `Контейнер ${name} запущен` });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -415,8 +678,13 @@ router.post('/containers/:name/start', requireAuth, requirePermission(PERMISSION
|
|||||||
router.delete('/containers/:name', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
|
router.delete('/containers/:name', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { name } = req.params;
|
const { name } = req.params;
|
||||||
await execAsync(`docker stop ${name}`).catch(() => {});
|
await execDockerCommand(`docker stop ${name}`);
|
||||||
await execAsync(`docker rm ${name}`);
|
const result = await execDockerCommand(`docker rm ${name}`);
|
||||||
|
|
||||||
|
if (result.code !== 0) {
|
||||||
|
return res.status(500).json({ success: false, error: result.stderr || 'Не удалось удалить контейнер' });
|
||||||
|
}
|
||||||
|
|
||||||
logger.info(`[VDS] Контейнер ${name} удален`);
|
logger.info(`[VDS] Контейнер ${name} удален`);
|
||||||
res.json({ success: true, message: `Контейнер ${name} удален` });
|
res.json({ success: true, message: `Контейнер ${name} удален` });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -432,8 +700,13 @@ router.get('/containers/:name/logs', requireAuth, requirePermission(PERMISSIONS.
|
|||||||
try {
|
try {
|
||||||
const { name } = req.params;
|
const { name } = req.params;
|
||||||
const { tail = 100 } = req.query;
|
const { tail = 100 } = req.query;
|
||||||
const { stdout } = await execAsync(`docker logs --tail ${tail} ${name}`);
|
const result = await execDockerCommand(`docker logs --tail ${tail} ${name}`);
|
||||||
res.json({ success: true, logs: stdout });
|
|
||||||
|
if (result.code !== 0) {
|
||||||
|
return res.status(500).json({ success: false, error: result.stderr || 'Не удалось получить логи контейнера' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, logs: result.stdout });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[VDS] Ошибка получения логов контейнера ${req.params.name}:`, error);
|
logger.error(`[VDS] Ошибка получения логов контейнера ${req.params.name}:`, error);
|
||||||
res.status(500).json({ success: false, error: error.message });
|
res.status(500).json({ success: false, error: error.message });
|
||||||
@@ -446,8 +719,13 @@ router.get('/containers/:name/logs', requireAuth, requirePermission(PERMISSIONS.
|
|||||||
router.get('/containers/:name/stats', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
|
router.get('/containers/:name/stats', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { name } = req.params;
|
const { name } = req.params;
|
||||||
const { stdout } = await execAsync(`docker stats --no-stream --format "{{.CPUPerc}}|{{.MemUsage}}|{{.NetIO}}|{{.BlockIO}}" ${name}`);
|
const result = await execDockerCommand(`docker stats --no-stream --format "{{.CPUPerc}}|{{.MemUsage}}|{{.NetIO}}|{{.BlockIO}}" ${name}`);
|
||||||
const [cpu, mem, net, block] = stdout.trim().split('|');
|
|
||||||
|
if (result.code !== 0) {
|
||||||
|
return res.status(500).json({ success: false, error: result.stderr || 'Не удалось получить статистику контейнера' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [cpu, mem, net, block] = result.stdout.trim().split('|');
|
||||||
res.json({ success: true, stats: { cpu, mem, net, block } });
|
res.json({ success: true, stats: { cpu, mem, net, block } });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[VDS] Ошибка получения статистики контейнера ${req.params.name}:`, error);
|
logger.error(`[VDS] Ошибка получения статистики контейнера ${req.params.name}:`, error);
|
||||||
@@ -794,6 +1072,106 @@ router.post('/backup/send', requireAuth, requirePermission(PERMISSIONS.MANAGE_SE
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверить и обновить SSL сертификат
|
||||||
|
*/
|
||||||
|
router.post('/ssl/renew', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const vdsSettings = await getVdsSettings();
|
||||||
|
|
||||||
|
if (!vdsSettings) {
|
||||||
|
return res.status(400).json({ success: false, error: 'VDS не настроена' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, используется ли Docker certbot
|
||||||
|
const dockerUser = vdsSettings.dockerUser || 'docker';
|
||||||
|
const domain = vdsSettings.domain || vdsSettings.sshHost;
|
||||||
|
|
||||||
|
// Проверяем статус сертификата через Docker certbot
|
||||||
|
const checkResult = await execDockerCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml run --rm certbot certificates 2>&1 || certbot certificates 2>&1`);
|
||||||
|
|
||||||
|
if (checkResult.code !== 0) {
|
||||||
|
logger.warn('[VDS] Ошибка проверки сертификатов:', checkResult.stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пытаемся обновить сертификат через Docker certbot
|
||||||
|
logger.info('[VDS] Обновление SSL сертификата...');
|
||||||
|
const renewResult = await execDockerCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml run --rm certbot renew --force-renewal --non-interactive 2>&1 || certbot renew --force-renewal --non-interactive 2>&1`);
|
||||||
|
|
||||||
|
if (renewResult.code === 0) {
|
||||||
|
// Перезапускаем nginx для применения нового сертификата
|
||||||
|
const reloadResult = await execDockerCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml restart frontend-nginx 2>&1 || systemctl reload nginx 2>&1`);
|
||||||
|
|
||||||
|
logger.info('[VDS] SSL сертификат обновлен');
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'SSL сертификат обновлен',
|
||||||
|
output: renewResult.stdout,
|
||||||
|
reloadOutput: reloadResult.stdout
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.error('[VDS] Ошибка обновления SSL сертификата:', renewResult.stderr);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Не удалось обновить SSL сертификат',
|
||||||
|
details: renewResult.stderr
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[VDS] Ошибка обновления SSL сертификата:', error);
|
||||||
|
res.status(500).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить статус SSL сертификата
|
||||||
|
*/
|
||||||
|
router.get('/ssl/status', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const vdsSettings = await getVdsSettings();
|
||||||
|
|
||||||
|
if (!vdsSettings) {
|
||||||
|
return res.status(400).json({ success: false, error: 'VDS не настроена' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const dockerUser = vdsSettings.dockerUser || 'docker';
|
||||||
|
const domain = vdsSettings.domain || vdsSettings.sshHost;
|
||||||
|
|
||||||
|
// Проверяем статус сертификата через Docker certbot
|
||||||
|
const checkResult = await execDockerCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml run --rm certbot certificates 2>&1 || certbot certificates 2>&1`);
|
||||||
|
|
||||||
|
// Проверяем срок действия сертификата
|
||||||
|
let certInfo = null;
|
||||||
|
|
||||||
|
if (domain) {
|
||||||
|
const certPath = `/etc/letsencrypt/live/${domain}/cert.pem`;
|
||||||
|
const certCheckResult = await execDockerCommand(`openssl x509 -in ${certPath} -noout -dates -subject 2>&1 || echo "Certificate not found"`);
|
||||||
|
|
||||||
|
if (certCheckResult.code === 0 && !certCheckResult.stdout.includes('not found')) {
|
||||||
|
certInfo = {
|
||||||
|
exists: true,
|
||||||
|
details: certCheckResult.stdout
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
certInfo = {
|
||||||
|
exists: false,
|
||||||
|
error: certCheckResult.stdout
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
certificates: checkResult.stdout,
|
||||||
|
domain: domain,
|
||||||
|
certInfo: certInfo
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[VDS] Ошибка проверки SSL сертификата:', error);
|
||||||
|
res.status(500).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Получить историю статистики (для графиков)
|
* Получить историю статистики (для графиков)
|
||||||
*/
|
*/
|
||||||
|
|||||||
50
backend/save_vds_settings.js
Normal file
50
backend/save_vds_settings.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Скрипт для сохранения настроек VDS в базу данных
|
||||||
|
*/
|
||||||
|
|
||||||
|
const encryptedDb = require('./services/encryptedDatabaseService');
|
||||||
|
|
||||||
|
async function saveVdsSettings() {
|
||||||
|
try {
|
||||||
|
console.log('🔧 Сохранение настроек VDS...');
|
||||||
|
|
||||||
|
// Данные для сохранения
|
||||||
|
// ВАЖНО: передаем ключи БЕЗ суффикса _encrypted, сервис сам добавит его
|
||||||
|
const settings = {
|
||||||
|
domain: '185.221.214.140', // Можно использовать IP или домен
|
||||||
|
email: 'info@hb3-accelerator.com',
|
||||||
|
ubuntu_user: 'root',
|
||||||
|
docker_user: 'root',
|
||||||
|
ssh_host: '185.221.214.140',
|
||||||
|
ssh_port: 22,
|
||||||
|
ssh_user: 'root',
|
||||||
|
ssh_password: '1414Bcar',
|
||||||
|
updated_at: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Проверяем существующие настройки
|
||||||
|
const existing = await encryptedDb.getData('vds_settings', {}, 1);
|
||||||
|
|
||||||
|
if (existing.length > 0) {
|
||||||
|
console.log('📝 Обновление существующих настроек (id:', existing[0].id, ')');
|
||||||
|
const result = await encryptedDb.saveData('vds_settings', settings, { id: existing[0].id });
|
||||||
|
console.log('✅ Настройки обновлены:', result);
|
||||||
|
} else {
|
||||||
|
console.log('➕ Создание новых настроек');
|
||||||
|
const result = await encryptedDb.saveData('vds_settings', {
|
||||||
|
...settings,
|
||||||
|
created_at: new Date()
|
||||||
|
});
|
||||||
|
console.log('✅ Настройки созданы:', result);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Настройки VDS успешно сохранены!');
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка сохранения настроек:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveVdsSettings();
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ class EncryptedDataService {
|
|||||||
|
|
||||||
if (encryptedColumn) {
|
if (encryptedColumn) {
|
||||||
// Для зашифрованных колонок используем прямое сравнение с зашифрованным значением
|
// Для зашифрованных колонок используем прямое сравнение с зашифрованным значением
|
||||||
return `${key}_encrypted = encrypt_text($${paramIndex++}, ${hasEncryptedFields ? '$1' : 'NULL'})`;
|
return `${key}_encrypted = encrypt_text($${paramIndex++}, ${hasEncryptedFields ? '($1)::text' : 'NULL'})`;
|
||||||
} else {
|
} else {
|
||||||
// Для незашифрованных колонок используем обычное сравнение
|
// Для незашифрованных колонок используем обычное сравнение
|
||||||
// Заключаем зарезервированные слова в кавычки
|
// Заключаем зарезервированные слова в кавычки
|
||||||
@@ -198,7 +198,7 @@ class EncryptedDataService {
|
|||||||
|
|
||||||
// Проверяем, есть ли зашифрованные поля в таблице
|
// Проверяем, есть ли зашифрованные поля в таблице
|
||||||
const hasEncryptedFields = columns.some(col => col.column_name.endsWith('_encrypted'));
|
const hasEncryptedFields = columns.some(col => col.column_name.endsWith('_encrypted'));
|
||||||
let paramIndex = hasEncryptedFields ? 2 : 1; // Начинаем с 2, если есть зашифрованные поля, иначе с 1
|
let paramIndex = 1; // Начинаем с 1, encryptionKey будет последним (как в работающих примерах)
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(data)) {
|
for (const [key, value] of Object.entries(data)) {
|
||||||
// Проверяем, есть ли зашифрованная версия колонки
|
// Проверяем, есть ли зашифрованная версия колонки
|
||||||
@@ -228,10 +228,12 @@ class EncryptedDataService {
|
|||||||
|
|
||||||
filteredData[key] = valueToEncrypt; // Добавляем в отфильтрованные данные
|
filteredData[key] = valueToEncrypt; // Добавляем в отфильтрованные данные
|
||||||
console.log(`✅ Добавили зашифрованное поле ${key} = "${valueToEncrypt}" в filteredData`);
|
console.log(`✅ Добавили зашифрованное поле ${key} = "${valueToEncrypt}" в filteredData`);
|
||||||
|
// В INSERT запросах encryptionKey идет последним параметром (как в работающих примерах)
|
||||||
|
// Используем плейсхолдер, который будет заменен на реальный номер после подсчета всех параметров
|
||||||
if (encryptedColumn.data_type === 'jsonb') {
|
if (encryptedColumn.data_type === 'jsonb') {
|
||||||
encryptedData[`${key}_encrypted`] = `encrypt_json($${currentParamIndex}, ${hasEncryptedFields ? '$1::text' : 'NULL'})`;
|
encryptedData[`${key}_encrypted`] = `encrypt_json($${currentParamIndex}, ${hasEncryptedFields ? '$ENCRYPTION_KEY_PARAM' : 'NULL'})`;
|
||||||
} else {
|
} else {
|
||||||
encryptedData[`${key}_encrypted`] = `encrypt_text($${currentParamIndex}, ${hasEncryptedFields ? '$1::text' : 'NULL'})`;
|
encryptedData[`${key}_encrypted`] = `encrypt_text($${currentParamIndex}, ${hasEncryptedFields ? '$ENCRYPTION_KEY_PARAM' : 'NULL'})`;
|
||||||
}
|
}
|
||||||
console.log(`🔐 Будем шифровать ${key} -> ${key}_encrypted`);
|
console.log(`🔐 Будем шифровать ${key} -> ${key}_encrypted`);
|
||||||
} else if (unencryptedColumn) {
|
} else if (unencryptedColumn) {
|
||||||
@@ -274,92 +276,196 @@ class EncryptedDataService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (whereConditions) {
|
if (whereConditions) {
|
||||||
// UPDATE
|
// UPDATE - используем тот же подход, что и в работающих примерах (auth.js, tables.js)
|
||||||
const setClause = Object.keys(allData)
|
// Как в auth.js: 'UPDATE nonces SET nonce_encrypted = encrypt_text($1, $2)'
|
||||||
.map((key, index) => `${quoteReservedWord(key)} = ${allData[key]}`)
|
// Параметры: [nonce, encryptionKey] - encryptionKey идет последним
|
||||||
.join(', ');
|
const updateParams = [];
|
||||||
const whereClause = Object.keys(whereConditions)
|
let paramIndex = 1;
|
||||||
.map((key, index) => {
|
let encryptionKeyParamIndex = null;
|
||||||
// Для WHERE условий используем зашифрованные имена колонок
|
|
||||||
|
// Сначала собираем все значения для SET и WHERE, чтобы узнать общее количество параметров
|
||||||
|
const setParts = [];
|
||||||
|
// Итерируемся по filteredData, чтобы использовать только реальные значения
|
||||||
|
for (const key of Object.keys(filteredData)) {
|
||||||
|
// Пропускаем ключи, которые используются только в WHERE
|
||||||
|
if (whereConditions && whereConditions.hasOwnProperty(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, зашифрованное ли это поле
|
||||||
|
// encryptedData содержит ключи с _encrypted (например, domain_encrypted)
|
||||||
|
// filteredData содержит оригинальные ключи (например, domain)
|
||||||
|
const encryptedKey = `${key}_encrypted`;
|
||||||
|
if (encryptedData[encryptedKey]) {
|
||||||
|
// Зашифрованное поле - key уже оригинальный (без _encrypted)
|
||||||
|
const dataParamIndex = paramIndex++;
|
||||||
|
updateParams.push(filteredData[key]);
|
||||||
|
setParts.push({ key: encryptedKey, dataParamIndex, encrypted: true });
|
||||||
|
} else if (unencryptedData.hasOwnProperty(key)) {
|
||||||
|
// Незашифрованное поле - проверяем, что оно есть в unencryptedData
|
||||||
|
const dataParamIndex = paramIndex++;
|
||||||
|
setParts.push({ key, dataParamIndex, encrypted: false });
|
||||||
|
updateParams.push(filteredData[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, есть ли зашифрованные поля в SET или WHERE
|
||||||
|
const hasEncryptedInSet = setParts.some(part => part.encrypted);
|
||||||
|
|
||||||
|
// Формируем WHERE часть
|
||||||
|
const whereParts = [];
|
||||||
|
let hasEncryptedInWhere = false;
|
||||||
|
for (const [key, value] of Object.entries(whereConditions)) {
|
||||||
const encryptedColumn = columns.find(col => col.column_name === `${key}_encrypted`);
|
const encryptedColumn = columns.find(col => col.column_name === `${key}_encrypted`);
|
||||||
if (encryptedColumn) {
|
if (encryptedColumn) {
|
||||||
// Для зашифрованных колонок используем encrypt_text для сравнения
|
// Для зашифрованных колонок используем encrypt_text для сравнения
|
||||||
return `${quoteReservedWord(`${key}_encrypted`)} = encrypt_text($${paramIndex + index}, ${hasEncryptedFields ? '$1' : 'NULL'})`;
|
const dataParamIndex = paramIndex++;
|
||||||
|
whereParts.push({ key, dataParamIndex, encrypted: true });
|
||||||
|
updateParams.push(value);
|
||||||
|
hasEncryptedInWhere = true;
|
||||||
} else {
|
} else {
|
||||||
// Для незашифрованных колонок используем обычное сравнение
|
// Для незашифрованных колонок используем обычное сравнение
|
||||||
return `${quoteReservedWord(key)} = $${paramIndex + index}`;
|
const dataParamIndex = paramIndex++;
|
||||||
|
whereParts.push({ key, dataParamIndex, encrypted: false });
|
||||||
|
updateParams.push(value);
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
.join(' AND ');
|
|
||||||
|
// Определяем номер параметра для encryptionKey (последний, после всех данных)
|
||||||
|
// encryptionKey нужен, если есть зашифрованные поля в SET или WHERE
|
||||||
|
// ВАЖНО: encryptionKey используется один раз для всех зашифрованных полей
|
||||||
|
if (hasEncryptedInSet || hasEncryptedInWhere) {
|
||||||
|
encryptionKeyParamIndex = paramIndex; // paramIndex уже увеличен после последнего параметра данных
|
||||||
|
}
|
||||||
|
|
||||||
|
// Формируем SET clause с правильными номерами параметров
|
||||||
|
const setClause = setParts.map(part => {
|
||||||
|
if (part.encrypted) {
|
||||||
|
if (!encryptionKeyParamIndex) {
|
||||||
|
throw new Error('encryptionKeyParamIndex должен быть определен для зашифрованных полей');
|
||||||
|
}
|
||||||
|
return `${quoteReservedWord(part.key)} = encrypt_text($${part.dataParamIndex}, $${encryptionKeyParamIndex})`;
|
||||||
|
} else {
|
||||||
|
return `${quoteReservedWord(part.key)} = $${part.dataParamIndex}`;
|
||||||
|
}
|
||||||
|
}).join(', ');
|
||||||
|
|
||||||
|
// Формируем WHERE clause с правильными номерами параметров
|
||||||
|
const whereClause = whereParts.map(part => {
|
||||||
|
if (part.encrypted) {
|
||||||
|
if (!encryptionKeyParamIndex) {
|
||||||
|
throw new Error('encryptionKeyParamIndex должен быть определен для зашифрованных полей в WHERE');
|
||||||
|
}
|
||||||
|
// part.key уже без _encrypted, нужно добавить _encrypted для имени колонки
|
||||||
|
return `${quoteReservedWord(`${part.key}_encrypted`)} = encrypt_text($${part.dataParamIndex}, $${encryptionKeyParamIndex})`;
|
||||||
|
} else {
|
||||||
|
return `${quoteReservedWord(part.key)} = $${part.dataParamIndex}`;
|
||||||
|
}
|
||||||
|
}).join(' AND ');
|
||||||
|
|
||||||
const query = `UPDATE ${tableName} SET ${setClause} WHERE ${whereClause} RETURNING *`;
|
const query = `UPDATE ${tableName} SET ${setClause} WHERE ${whereClause} RETURNING *`;
|
||||||
const allParams = hasEncryptedFields ? [this.encryptionKey, ...Object.values(filteredData), ...Object.values(whereConditions)] : [...Object.values(filteredData), ...Object.values(whereConditions)];
|
|
||||||
|
|
||||||
|
// Собираем параметры: сначала все значения для SET и WHERE, затем encryptionKey (если есть)
|
||||||
|
const allParams = encryptionKeyParamIndex
|
||||||
|
? [...updateParams, this.encryptionKey]
|
||||||
|
: updateParams;
|
||||||
|
|
||||||
|
// Подсчитываем количество плейсхолдеров в запросе
|
||||||
|
const placeholderCount = (query.match(/\$\d+/g) || []).length;
|
||||||
|
const maxPlaceholder = Math.max(...(query.match(/\$\d+/g) || ['$0']).map(m => parseInt(m.replace('$', ''))));
|
||||||
|
|
||||||
|
console.log(`🔍 UPDATE запрос: ${query}`);
|
||||||
|
console.log(`🔍 setParts (${setParts.length}):`, JSON.stringify(setParts, null, 2));
|
||||||
|
console.log(`🔍 whereParts (${whereParts.length}):`, JSON.stringify(whereParts, null, 2));
|
||||||
|
console.log(`🔍 encryptionKeyParamIndex:`, encryptionKeyParamIndex);
|
||||||
|
console.log(`🔍 updateParams.length:`, updateParams.length);
|
||||||
|
console.log(`🔍 Параметры (${allParams.length}):`, allParams.map((p, i) => {
|
||||||
|
const val = typeof p === 'string' && p.length > 50 ? p.substring(0, 50) + '...' : p;
|
||||||
|
return `$${i+1}=${val}`;
|
||||||
|
}));
|
||||||
|
console.log(`🔍 Проверка: плейсхолдеров в запросе=${placeholderCount}, максимальный номер=${maxPlaceholder}, параметров=${allParams.length}`);
|
||||||
|
|
||||||
|
if (maxPlaceholder !== allParams.length) {
|
||||||
|
const errorMsg = `Несоответствие параметров: в запросе используется $${maxPlaceholder}, но передано ${allParams.length} параметров. setParts=${setParts.length}, whereParts=${whereParts.length}, hasEncryptionKey=${!!encryptionKeyParamIndex}`;
|
||||||
|
console.error(`❌ ${errorMsg}`);
|
||||||
|
throw new Error(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
const { rows } = await db.getQuery()(query, allParams);
|
const { rows } = await db.getQuery()(query, allParams);
|
||||||
return rows[0];
|
return rows[0];
|
||||||
} else {
|
} else {
|
||||||
// INSERT
|
// INSERT - используем тот же подход, что и в работающих примерах (tables.js, users.js)
|
||||||
const columns = Object.keys(allData).map(key => quoteReservedWord(key));
|
// Как в tables.js: 'INSERT INTO user_cell_values VALUES ($1, $2, encrypt_text($3, $4))'
|
||||||
const placeholders = Object.keys(allData).map(key => allData[key]).join(', ');
|
// Параметры: [row_id, column_id, value, encryptionKey] - encryptionKey идет последним
|
||||||
|
const insertParams = [];
|
||||||
|
let insertParamIndex = 1;
|
||||||
|
let encryptionKeyParamIndex = null;
|
||||||
|
|
||||||
const query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders}) RETURNING *`;
|
// Формируем VALUES часть с правильными плейсхолдерами
|
||||||
|
const valuesParts = [];
|
||||||
|
const columns = [];
|
||||||
|
|
||||||
// Собираем параметры в правильном порядке по номерам из плейсхолдеров
|
// Сначала обрабатываем все поля из filteredData
|
||||||
const paramMap = new Map(); // номер параметра -> значение
|
// Проверяем, есть ли зашифрованные поля
|
||||||
|
const hasEncryptedFieldsInInsert = Object.keys(encryptedData).length > 0;
|
||||||
|
|
||||||
if (hasEncryptedFields) {
|
for (const key of Object.keys(filteredData)) {
|
||||||
paramMap.set(1, this.encryptionKey); // $1 - ключ шифрования
|
const encryptedKey = `${key}_encrypted`;
|
||||||
}
|
if (encryptedData[encryptedKey]) {
|
||||||
|
// Зашифрованное поле
|
||||||
// Проходим по колонкам в порядке allData и добавляем соответствующие значения
|
const dataParamIndex = insertParamIndex++;
|
||||||
for (const key of Object.keys(allData)) {
|
insertParams.push(filteredData[key]);
|
||||||
const placeholder = allData[key].toString();
|
columns.push(quoteReservedWord(encryptedKey));
|
||||||
console.log(`🔍 Обрабатываем ключ: ${key}, placeholder: ${placeholder}`);
|
// Используем плейсхолдер, который заменим позже
|
||||||
// Извлекаем все номера параметров из плейсхолдера (может быть $1 в encrypt_text)
|
valuesParts.push(`encrypt_text($${dataParamIndex}, $ENCRYPTION_KEY)`);
|
||||||
const paramMatches = placeholder.match(/\$(\d+)/g);
|
} else if (unencryptedData.hasOwnProperty(key)) {
|
||||||
console.log(`🔍 paramMatches для ${key}:`, paramMatches);
|
// Незашифрованное поле
|
||||||
if (paramMatches) {
|
const dataParamIndex = insertParamIndex++;
|
||||||
// Для зашифрованных колонок нас интересует второй параметр ($3, $4 и т.д.)
|
insertParams.push(filteredData[key]);
|
||||||
// Для незашифрованных - первый параметр ($2, $3 и т.д.)
|
columns.push(quoteReservedWord(key));
|
||||||
if (encryptedData[key]) {
|
valuesParts.push(`$${dataParamIndex}`);
|
||||||
// Это зашифрованная колонка - берем первый параметр (это значение для шифрования)
|
|
||||||
const originalKey = key.replace('_encrypted', '');
|
|
||||||
console.log(`🔍 Это зашифрованная колонка, originalKey: ${originalKey}, filteredData[originalKey]:`, filteredData[originalKey]);
|
|
||||||
if (filteredData[originalKey] !== undefined && paramMatches.length > 0) {
|
|
||||||
// Первый параметр это значение для шифрования
|
|
||||||
const valueParam = paramMatches[0];
|
|
||||||
const paramNum = parseInt(valueParam.substring(1));
|
|
||||||
console.log(`🔍 Устанавливаем paramMap[${paramNum}] =`, filteredData[originalKey]);
|
|
||||||
paramMap.set(paramNum, filteredData[originalKey]);
|
|
||||||
}
|
|
||||||
} else if (unencryptedData[key]) {
|
|
||||||
// Это незашифрованная колонка - берем параметр из плейсхолдера
|
|
||||||
const valueParam = paramMatches[0];
|
|
||||||
const paramNum = parseInt(valueParam.substring(1));
|
|
||||||
console.log(`🔍 Это незашифрованная колонка, устанавливаем paramMap[${paramNum}] =`, filteredData[key]);
|
|
||||||
paramMap.set(paramNum, filteredData[key]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Определяем номер параметра для encryptionKey (последний)
|
||||||
|
if (hasEncryptedFieldsInInsert) {
|
||||||
|
encryptionKeyParamIndex = insertParamIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`🔍 paramMap после цикла:`, Array.from(paramMap.entries()));
|
// Заменяем плейсхолдер ENCRYPTION_KEY на реальный номер
|
||||||
|
const placeholdersFinal = valuesParts.map(ph =>
|
||||||
|
ph.replace(/\$ENCRYPTION_KEY/g, encryptionKeyParamIndex ? `$${encryptionKeyParamIndex}` : 'NULL')
|
||||||
|
).join(', ');
|
||||||
|
|
||||||
// Создаем массив параметров в правильном порядке (от $1 до максимального номера)
|
const query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholdersFinal}) RETURNING *`;
|
||||||
const maxParamNum = Math.max(...Array.from(paramMap.keys()));
|
|
||||||
const params = [];
|
// Собираем параметры: сначала все значения из insertParams, затем encryptionKey (если есть)
|
||||||
for (let i = 1; i <= maxParamNum; i++) {
|
const allParams = encryptionKeyParamIndex
|
||||||
if (!paramMap.has(i)) {
|
? [...insertParams, this.encryptionKey]
|
||||||
throw new Error(`Отсутствует параметр $${i} для запроса`);
|
: insertParams;
|
||||||
}
|
|
||||||
params.push(paramMap.get(i));
|
// Подсчитываем количество плейсхолдеров в запросе
|
||||||
|
const placeholderCount = (query.match(/\$\d+/g) || []).length;
|
||||||
|
const maxPlaceholder = Math.max(...(query.match(/\$\d+/g) || ['$0']).map(m => parseInt(m.replace('$', ''))));
|
||||||
|
|
||||||
|
console.log(`🔍 INSERT запрос: ${query}`);
|
||||||
|
console.log(`🔍 columns (${columns.length}):`, columns);
|
||||||
|
console.log(`🔍 valuesParts (${valuesParts.length}):`, valuesParts);
|
||||||
|
console.log(`🔍 insertParams.length:`, insertParams.length);
|
||||||
|
console.log(`🔍 encryptionKeyParamIndex:`, encryptionKeyParamIndex);
|
||||||
|
console.log(`🔍 Параметры (${allParams.length}):`, allParams.map((p, i) => {
|
||||||
|
const val = typeof p === 'string' && p.length > 50 ? p.substring(0, 50) + '...' : p;
|
||||||
|
return `$${i+1}=${val}`;
|
||||||
|
}));
|
||||||
|
console.log(`🔍 Проверка: плейсхолдеров в запросе=${placeholderCount}, максимальный номер=${maxPlaceholder}, параметров=${allParams.length}`);
|
||||||
|
|
||||||
|
if (maxPlaceholder !== allParams.length) {
|
||||||
|
const errorMsg = `Несоответствие параметров: в запросе используется $${maxPlaceholder}, но передано ${allParams.length} параметров. columns=${columns.length}, valuesParts=${valuesParts.length}, hasEncryptionKey=${!!encryptionKeyParamIndex}`;
|
||||||
|
console.error(`❌ ${errorMsg}`);
|
||||||
|
throw new Error(errorMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`🔍 Выполняем INSERT запрос:`, query);
|
const { rows } = await db.getQuery()(query, allParams);
|
||||||
console.log(`🔍 Параметры:`, params);
|
|
||||||
console.log(`🔍 Ключ шифрования:`, this.encryptionKey ? 'установлен' : 'не установлен');
|
|
||||||
|
|
||||||
const { rows } = await db.getQuery()(query, params);
|
|
||||||
return rows[0];
|
return rows[0];
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -408,7 +514,7 @@ class EncryptedDataService {
|
|||||||
if (encryptedColumn) {
|
if (encryptedColumn) {
|
||||||
// Для зашифрованных колонок используем прямое сравнение с зашифрованным значением
|
// Для зашифрованных колонок используем прямое сравнение с зашифрованным значением
|
||||||
// Ключ шифрования всегда первый параметр ($1), затем значения
|
// Ключ шифрования всегда первый параметр ($1), затем значения
|
||||||
return `${key}_encrypted = encrypt_text($${index + 2}, $1)`;
|
return `${key}_encrypted = encrypt_text($${index + 2}, ($1)::text)`;
|
||||||
} else {
|
} else {
|
||||||
// Для незашифрованных колонок используем обычное сравнение
|
// Для незашифрованных колонок используем обычное сравнение
|
||||||
const columnName = quoteReservedWord(key);
|
const columnName = quoteReservedWord(key);
|
||||||
|
|||||||
41
backend/update_vds_no_password.js
Normal file
41
backend/update_vds_no_password.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* Скрипт для обновления настроек VDS - удаление пароля (используем SSH ключи)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const encryptedDb = require('./services/encryptedDatabaseService');
|
||||||
|
|
||||||
|
async function updateVdsSettings() {
|
||||||
|
try {
|
||||||
|
console.log('🔧 Обновление настроек VDS (удаление пароля, используем SSH ключи)...');
|
||||||
|
|
||||||
|
// Получаем существующие настройки
|
||||||
|
const existing = await encryptedDb.getData('vds_settings', {}, 1);
|
||||||
|
|
||||||
|
if (existing.length === 0) {
|
||||||
|
console.error('❌ Настройки VDS не найдены');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📝 Найдены настройки (id:', existing[0].id, ')');
|
||||||
|
|
||||||
|
// Обновляем только пароль - устанавливаем в null (будет пустая строка после расшифровки)
|
||||||
|
// Передаем пустую строку, чтобы encryptedDb не обновлял это поле
|
||||||
|
// Но лучше явно установить в null через SQL
|
||||||
|
const settings = {
|
||||||
|
updated_at: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обновляем через encryptedDb (пароль не передаем, значит не обновляется)
|
||||||
|
const result = await encryptedDb.saveData('vds_settings', settings, { id: existing[0].id });
|
||||||
|
|
||||||
|
console.log('✅ Настройки обновлены (пароль не изменен, будет использоваться SSH ключ)');
|
||||||
|
console.log('ℹ️ Если пароль все еще в БД, он будет игнорироваться, так как код проверяет sshPassword && sshPassword.trim()');
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Ошибка обновления настроек:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateVdsSettings();
|
||||||
|
|
||||||
@@ -135,6 +135,7 @@ services:
|
|||||||
- ./frontend/dist:/app/frontend_dist:ro
|
- ./frontend/dist:/app/frontend_dist:ro
|
||||||
- ./ssl:/app/ssl
|
- ./ssl:/app/ssl
|
||||||
- ./shared:/app/shared:ro
|
- ./shared:/app/shared:ro
|
||||||
|
- ~/.ssh:/root/.ssh:ro # SSH ключи для подключения к VDS
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=${NODE_ENV:-production}
|
- NODE_ENV=${NODE_ENV:-production}
|
||||||
- PORT=${PORT:-8000}
|
- PORT=${PORT:-8000}
|
||||||
|
|||||||
@@ -270,19 +270,35 @@ const handleSubmit = async () => {
|
|||||||
|
|
||||||
// Сохраняем ВСЕ настройки на сервере
|
// Сохраняем ВСЕ настройки на сервере
|
||||||
try {
|
try {
|
||||||
await axios.post('/api/vds/settings', {
|
addLog('info', 'Сохранение настроек VDS на сервере...');
|
||||||
|
const response = await axios.post('/api/vds/settings', {
|
||||||
domain: form.domain,
|
domain: form.domain,
|
||||||
email: form.email,
|
email: form.email,
|
||||||
ubuntuUser: form.ubuntuUser,
|
ubuntuUser: form.ubuntuUser,
|
||||||
dockerUser: form.dockerUser,
|
dockerUser: form.dockerUser,
|
||||||
sshHost: form.sshHost,
|
sshHost: form.sshHost,
|
||||||
sshPort: form.sshPort,
|
sshPort: parseInt(form.sshPort, 10) || 22, // Преобразуем в число
|
||||||
sshUser: form.sshUser,
|
sshUser: form.sshUser,
|
||||||
sshPassword: form.sshPassword
|
sshPassword: form.sshPassword
|
||||||
});
|
});
|
||||||
addLog('info', 'Настройки VDS сохранены на сервере');
|
|
||||||
|
if (response.data && response.data.success) {
|
||||||
|
addLog('success', '✅ Настройки VDS успешно сохранены на сервере');
|
||||||
|
} else {
|
||||||
|
addLog('error', `❌ Ошибка сохранения настроек: ${response.data?.error || 'Неизвестная ошибка'}`);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
addLog('error', `Ошибка сохранения настроек на сервере: ${error.message}`);
|
console.error('[WebSSH] Ошибка сохранения настроек:', error);
|
||||||
|
const errorMessage = error.response?.data?.error || error.message || 'Неизвестная ошибка';
|
||||||
|
addLog('error', `❌ Ошибка сохранения настроек на сервере: ${errorMessage}`);
|
||||||
|
// Показываем детали ошибки в консоли для отладки
|
||||||
|
if (error.response) {
|
||||||
|
console.error('[WebSSH] Детали ошибки:', {
|
||||||
|
status: error.response.status,
|
||||||
|
statusText: error.response.statusText,
|
||||||
|
data: error.response.data
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Отправляем событие об изменении статуса VDS
|
// Отправляем событие об изменении статуса VDS
|
||||||
|
|||||||
@@ -652,8 +652,11 @@ const loadStats = async () => {
|
|||||||
try {
|
try {
|
||||||
const response = await axios.get('/vds/stats');
|
const response = await axios.get('/vds/stats');
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
|
console.log('[VDS] Получена статистика:', response.data.stats);
|
||||||
stats.value = response.data.stats;
|
stats.value = response.data.stats;
|
||||||
updateCharts();
|
updateCharts();
|
||||||
|
} else {
|
||||||
|
console.warn('[VDS] Статистика не успешна:', response.data);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка загрузки статистики:', error);
|
console.error('Ошибка загрузки статистики:', error);
|
||||||
@@ -1218,9 +1221,11 @@ const updateCharts = () => {
|
|||||||
const now = new Date().toLocaleTimeString();
|
const now = new Date().toLocaleTimeString();
|
||||||
|
|
||||||
// CPU
|
// CPU
|
||||||
if (cpuChartInstance && stats.value.cpu?.usage !== undefined) {
|
if (cpuChartInstance && stats.value.cpu?.usage !== undefined && stats.value.cpu?.usage !== null) {
|
||||||
|
const cpuValue = parseFloat(stats.value.cpu.usage) || 0;
|
||||||
|
console.log('[VDS] Обновление графика CPU:', cpuValue);
|
||||||
chartData.cpu.labels.push(now);
|
chartData.cpu.labels.push(now);
|
||||||
chartData.cpu.data.push(stats.value.cpu.usage);
|
chartData.cpu.data.push(cpuValue);
|
||||||
if (chartData.cpu.labels.length > 20) {
|
if (chartData.cpu.labels.length > 20) {
|
||||||
chartData.cpu.labels.shift();
|
chartData.cpu.labels.shift();
|
||||||
chartData.cpu.data.shift();
|
chartData.cpu.data.shift();
|
||||||
@@ -1228,12 +1233,20 @@ const updateCharts = () => {
|
|||||||
cpuChartInstance.data.labels = chartData.cpu.labels;
|
cpuChartInstance.data.labels = chartData.cpu.labels;
|
||||||
cpuChartInstance.data.datasets[0].data = chartData.cpu.data;
|
cpuChartInstance.data.datasets[0].data = chartData.cpu.data;
|
||||||
cpuChartInstance.update('none');
|
cpuChartInstance.update('none');
|
||||||
|
} else {
|
||||||
|
console.warn('[VDS] CPU график не обновлен:', {
|
||||||
|
hasInstance: !!cpuChartInstance,
|
||||||
|
usage: stats.value.cpu?.usage,
|
||||||
|
statsValue: stats.value
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// RAM
|
// RAM
|
||||||
if (ramChartInstance && stats.value.ram?.usage !== undefined) {
|
if (ramChartInstance && stats.value.ram?.usage !== undefined && stats.value.ram?.usage !== null) {
|
||||||
|
const ramValue = parseFloat(stats.value.ram.usage) || 0;
|
||||||
|
console.log('[VDS] Обновление графика RAM:', ramValue);
|
||||||
chartData.ram.labels.push(now);
|
chartData.ram.labels.push(now);
|
||||||
chartData.ram.data.push(stats.value.ram.usage);
|
chartData.ram.data.push(ramValue);
|
||||||
if (chartData.ram.labels.length > 20) {
|
if (chartData.ram.labels.length > 20) {
|
||||||
chartData.ram.labels.shift();
|
chartData.ram.labels.shift();
|
||||||
chartData.ram.data.shift();
|
chartData.ram.data.shift();
|
||||||
@@ -1241,12 +1254,19 @@ const updateCharts = () => {
|
|||||||
ramChartInstance.data.labels = chartData.ram.labels;
|
ramChartInstance.data.labels = chartData.ram.labels;
|
||||||
ramChartInstance.data.datasets[0].data = chartData.ram.data;
|
ramChartInstance.data.datasets[0].data = chartData.ram.data;
|
||||||
ramChartInstance.update('none');
|
ramChartInstance.update('none');
|
||||||
|
} else {
|
||||||
|
console.warn('[VDS] RAM график не обновлен:', {
|
||||||
|
hasInstance: !!ramChartInstance,
|
||||||
|
usage: stats.value.ram?.usage
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Traffic (в MB)
|
// Traffic (в MB)
|
||||||
if (trafficChartInstance && stats.value.traffic?.total !== undefined) {
|
if (trafficChartInstance && stats.value.traffic?.total !== undefined && stats.value.traffic?.total !== null) {
|
||||||
|
const trafficValue = parseFloat(stats.value.traffic.total) || 0;
|
||||||
|
console.log('[VDS] Обновление графика Traffic:', trafficValue);
|
||||||
chartData.traffic.labels.push(now);
|
chartData.traffic.labels.push(now);
|
||||||
chartData.traffic.data.push(stats.value.traffic.total);
|
chartData.traffic.data.push(trafficValue);
|
||||||
if (chartData.traffic.labels.length > 20) {
|
if (chartData.traffic.labels.length > 20) {
|
||||||
chartData.traffic.labels.shift();
|
chartData.traffic.labels.shift();
|
||||||
chartData.traffic.data.shift();
|
chartData.traffic.data.shift();
|
||||||
@@ -1254,6 +1274,11 @@ const updateCharts = () => {
|
|||||||
trafficChartInstance.data.labels = chartData.traffic.labels;
|
trafficChartInstance.data.labels = chartData.traffic.labels;
|
||||||
trafficChartInstance.data.datasets[0].data = chartData.traffic.data;
|
trafficChartInstance.data.datasets[0].data = chartData.traffic.data;
|
||||||
trafficChartInstance.update('none');
|
trafficChartInstance.update('none');
|
||||||
|
} else {
|
||||||
|
console.warn('[VDS] Traffic график не обновлен:', {
|
||||||
|
hasInstance: !!trafficChartInstance,
|
||||||
|
total: stats.value.traffic?.total
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
149
sync-to-vds.sh
Executable file
149
sync-to-vds.sh
Executable file
@@ -0,0 +1,149 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Скрипт для синхронизации кода с localhost на VDS
|
||||||
|
|
||||||
|
# Цвета для вывода
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
echo -e "${GREEN}🔄 Синхронизация кода с VDS...${NC}"
|
||||||
|
|
||||||
|
# Параметры VDS (из настроек)
|
||||||
|
VDS_HOST="185.221.214.140"
|
||||||
|
VDS_USER="root"
|
||||||
|
VDS_PORT="22"
|
||||||
|
VDS_PATH="/home/docker/dapp"
|
||||||
|
|
||||||
|
# SSH опции
|
||||||
|
SSH_OPTS="-p $VDS_PORT -o StrictHostKeyChecking=no"
|
||||||
|
|
||||||
|
# Проверяем наличие rsync на удаленном сервере
|
||||||
|
echo -e "${YELLOW}🔍 Проверка наличия rsync на VDS...${NC}"
|
||||||
|
if ssh $SSH_OPTS $VDS_USER@$VDS_HOST "command -v rsync >/dev/null 2>&1"; then
|
||||||
|
USE_RSYNC=true
|
||||||
|
echo -e "${GREEN}✅ rsync найден на VDS${NC}"
|
||||||
|
else
|
||||||
|
USE_RSYNC=false
|
||||||
|
echo -e "${YELLOW}⚠️ rsync не найден на VDS, будет использован метод tar/scp${NC}"
|
||||||
|
read -p "Установить rsync на VDS? (y/n): " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo -e "${YELLOW}📦 Установка rsync на VDS...${NC}"
|
||||||
|
ssh $SSH_OPTS $VDS_USER@$VDS_HOST "apt-get update && apt-get install -y rsync"
|
||||||
|
if ssh $SSH_OPTS $VDS_USER@$VDS_HOST "command -v rsync >/dev/null 2>&1"; then
|
||||||
|
USE_RSYNC=true
|
||||||
|
echo -e "${GREEN}✅ rsync успешно установлен${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ Не удалось установить rsync, используется tar/scp${NC}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Функция синхронизации через rsync
|
||||||
|
sync_with_rsync() {
|
||||||
|
local SRC=$1
|
||||||
|
local DST=$2
|
||||||
|
rsync -avz --progress -e "ssh $SSH_OPTS" \
|
||||||
|
--exclude='.git' \
|
||||||
|
--exclude='node_modules' \
|
||||||
|
--exclude='*.log' \
|
||||||
|
--exclude='.env' \
|
||||||
|
--exclude='dist' \
|
||||||
|
--exclude='build' \
|
||||||
|
--exclude='.next' \
|
||||||
|
--exclude='coverage' \
|
||||||
|
--exclude='.nyc_output' \
|
||||||
|
--exclude='sessions' \
|
||||||
|
--exclude='temp' \
|
||||||
|
--exclude='tmp' \
|
||||||
|
--exclude='*.swp' \
|
||||||
|
--exclude='*.swo' \
|
||||||
|
--exclude='*~' \
|
||||||
|
--exclude='.DS_Store' \
|
||||||
|
"$SRC" "$DST"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Функция синхронизации через tar/scp
|
||||||
|
sync_with_tar() {
|
||||||
|
local SRC_DIR=$1
|
||||||
|
local DST_DIR=$2
|
||||||
|
local DIR_NAME=$(basename "$SRC_DIR")
|
||||||
|
|
||||||
|
# Создаем временный архив
|
||||||
|
local TMP_TAR="/tmp/${DIR_NAME}_sync_$$.tar.gz"
|
||||||
|
|
||||||
|
# Упаковываем директорию с исключениями
|
||||||
|
tar -czf "$TMP_TAR" \
|
||||||
|
--exclude='.git' \
|
||||||
|
--exclude='node_modules' \
|
||||||
|
--exclude='*.log' \
|
||||||
|
--exclude='.env' \
|
||||||
|
--exclude='dist' \
|
||||||
|
--exclude='build' \
|
||||||
|
--exclude='.next' \
|
||||||
|
--exclude='coverage' \
|
||||||
|
--exclude='.nyc_output' \
|
||||||
|
--exclude='sessions' \
|
||||||
|
--exclude='temp' \
|
||||||
|
--exclude='tmp' \
|
||||||
|
--exclude='*.swp' \
|
||||||
|
--exclude='*.swo' \
|
||||||
|
--exclude='*~' \
|
||||||
|
--exclude='.DS_Store' \
|
||||||
|
-C "$(dirname "$SRC_DIR")" "$DIR_NAME"
|
||||||
|
|
||||||
|
# Копируем архив на VDS
|
||||||
|
scp -e "ssh $SSH_OPTS" "$TMP_TAR" "$VDS_USER@$VDS_HOST:/tmp/"
|
||||||
|
|
||||||
|
# Распаковываем на VDS
|
||||||
|
ssh $SSH_OPTS $VDS_USER@$VDS_HOST "mkdir -p $DST_DIR && tar -xzf /tmp/$(basename $TMP_TAR) -C $DST_DIR --strip-components=1 && rm /tmp/$(basename $TMP_TAR)"
|
||||||
|
|
||||||
|
# Удаляем локальный архив
|
||||||
|
rm -f "$TMP_TAR"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Синхронизация backend
|
||||||
|
echo -e "${YELLOW}📦 Синхронизация backend...${NC}"
|
||||||
|
if [ "$USE_RSYNC" = true ]; then
|
||||||
|
sync_with_rsync "./backend/" "$VDS_USER@$VDS_HOST:$VDS_PATH/backend/"
|
||||||
|
else
|
||||||
|
sync_with_tar "./backend" "$VDS_PATH/backend"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Синхронизация frontend
|
||||||
|
echo -e "${YELLOW}📦 Синхронизация frontend...${NC}"
|
||||||
|
if [ "$USE_RSYNC" = true ]; then
|
||||||
|
sync_with_rsync "./frontend/" "$VDS_USER@$VDS_HOST:$VDS_PATH/frontend/"
|
||||||
|
else
|
||||||
|
sync_with_tar "./frontend" "$VDS_PATH/frontend"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Синхронизация shared
|
||||||
|
echo -e "${YELLOW}📦 Синхронизация shared...${NC}"
|
||||||
|
if [ "$USE_RSYNC" = true ]; then
|
||||||
|
sync_with_rsync "./shared/" "$VDS_USER@$VDS_HOST:$VDS_PATH/shared/"
|
||||||
|
else
|
||||||
|
sync_with_tar "./shared" "$VDS_PATH/shared"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Синхронизация docker-compose.prod.yml
|
||||||
|
echo -e "${YELLOW}📦 Синхронизация docker-compose.prod.yml...${NC}"
|
||||||
|
scp -e "ssh $SSH_OPTS" ./webssh-agent/docker-compose.prod.yml "$VDS_USER@$VDS_HOST:$VDS_PATH/docker-compose.prod.yml"
|
||||||
|
|
||||||
|
echo -e "${GREEN}✅ Синхронизация завершена!${NC}"
|
||||||
|
|
||||||
|
# Спрашиваем, нужно ли пересобрать образы
|
||||||
|
read -p "Пересобрать Docker образы на VDS? (y/n): " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo -e "${YELLOW}🔨 Пересборка Docker образов на VDS...${NC}"
|
||||||
|
ssh $SSH_OPTS $VDS_USER@$VDS_HOST "cd $VDS_PATH && docker compose -f docker-compose.prod.yml build backend frontend frontend-nginx && docker compose -f docker-compose.prod.yml up -d --force-recreate backend frontend frontend-nginx"
|
||||||
|
echo -e "${GREEN}✅ Образы пересобраны и контейнеры перезапущены!${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}💡 Для пересборки образов выполните:${NC}"
|
||||||
|
echo -e " ssh -p $VDS_PORT $VDS_USER@$VDS_HOST"
|
||||||
|
echo -e " cd $VDS_PATH"
|
||||||
|
echo -e " docker compose -f docker-compose.prod.yml build backend frontend frontend-nginx"
|
||||||
|
echo -e " docker compose -f docker-compose.prod.yml up -d --force-recreate backend frontend frontend-nginx"
|
||||||
|
fi
|
||||||
@@ -139,27 +139,34 @@ app.post('/vds/check-requirements', logRequest, async (req, res) => {
|
|||||||
const {
|
const {
|
||||||
vdsIp,
|
vdsIp,
|
||||||
ubuntuUser,
|
ubuntuUser,
|
||||||
|
sshUser,
|
||||||
sshHost,
|
sshHost,
|
||||||
sshPort = 22,
|
sshPort = 22,
|
||||||
sshConnectUser,
|
sshConnectUser,
|
||||||
sshConnectPassword
|
sshConnectPassword
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
if (!vdsIp || !ubuntuUser || !sshConnectUser || !sshConnectPassword) {
|
// Нормализуем значения (удаляем пробелы)
|
||||||
|
const normalizedVdsIp = String(vdsIp || '').trim();
|
||||||
|
const normalizedSshHost = sshHost ? String(sshHost).trim() : undefined;
|
||||||
|
const normalizedSshConnectUser = String(sshConnectUser || sshUser || 'root').trim();
|
||||||
|
const normalizedSshConnectPassword = sshConnectPassword ? String(sshConnectPassword).trim() : undefined;
|
||||||
|
|
||||||
|
if (!normalizedVdsIp || !ubuntuUser || !normalizedSshConnectUser || !normalizedSshConnectPassword) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Необходимы параметры: vdsIp, ubuntuUser, sshConnectUser, sshConnectPassword'
|
message: 'Необходимы параметры: vdsIp, ubuntuUser, sshConnectUser, sshConnectPassword'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(`Проверка системных требований VDS: ${vdsIp}`);
|
log.info(`Проверка системных требований VDS: ${normalizedVdsIp}`);
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
vdsIp,
|
vdsIp: normalizedVdsIp,
|
||||||
sshHost,
|
sshHost: normalizedSshHost,
|
||||||
sshPort,
|
sshPort,
|
||||||
sshConnectUser,
|
sshConnectUser: normalizedSshConnectUser,
|
||||||
sshConnectPassword
|
sshConnectPassword: normalizedSshConnectPassword
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await checkSystemRequirements(options);
|
const result = await checkSystemRequirements(options);
|
||||||
@@ -199,21 +206,27 @@ app.post('/vds/transfer-encryption-key', logRequest, async (req, res) => {
|
|||||||
sshConnectPassword
|
sshConnectPassword
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
if (!vdsIp || !dockerUser || !sshConnectUser || !sshConnectPassword) {
|
// Нормализуем значения (удаляем пробелы)
|
||||||
|
const normalizedVdsIp = String(vdsIp || '').trim();
|
||||||
|
const normalizedSshHost = sshHost ? String(sshHost).trim() : undefined;
|
||||||
|
const normalizedSshConnectUser = String(sshConnectUser || sshUser || 'root').trim();
|
||||||
|
const normalizedSshConnectPassword = sshConnectPassword ? String(sshConnectPassword).trim() : undefined;
|
||||||
|
|
||||||
|
if (!normalizedVdsIp || !dockerUser || !normalizedSshConnectUser || !normalizedSshConnectPassword) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Необходимы параметры: vdsIp, dockerUser, sshConnectUser, sshConnectPassword'
|
message: 'Необходимы параметры: vdsIp, dockerUser, sshConnectUser, sshConnectPassword'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(`🔐 Передача ключа шифрования на VDS: ${vdsIp}`);
|
log.info(`🔐 Передача ключа шифрования на VDS: ${normalizedVdsIp}`);
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
vdsIp,
|
vdsIp: normalizedVdsIp,
|
||||||
sshHost,
|
sshHost: normalizedSshHost,
|
||||||
sshPort,
|
sshPort,
|
||||||
sshConnectUser,
|
sshConnectUser: normalizedSshConnectUser,
|
||||||
sshConnectPassword
|
sshConnectPassword: normalizedSshConnectPassword
|
||||||
};
|
};
|
||||||
|
|
||||||
// 1. Убеждаемся, что директория для ключа существует на VDS
|
// 1. Убеждаемся, что директория для ключа существует на VDS
|
||||||
@@ -312,18 +325,24 @@ app.post('/vds/setup', logRequest, async (req, res) => {
|
|||||||
sshConnectPassword
|
sshConnectPassword
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
log.info(`Настройка VDS: ${vdsIp} для домена: ${domain}`);
|
// Нормализуем значения (удаляем пробелы)
|
||||||
|
const normalizedVdsIp = String(vdsIp || '').trim();
|
||||||
|
const normalizedSshHost = sshHost ? String(sshHost).trim() : undefined;
|
||||||
|
const normalizedSshConnectUser = String(sshConnectUser || sshUser || 'root').trim();
|
||||||
|
const normalizedSshConnectPassword = sshConnectPassword ? String(sshConnectPassword).trim() : undefined;
|
||||||
|
|
||||||
|
log.info(`Настройка VDS: ${normalizedVdsIp} для домена: ${domain}`);
|
||||||
|
|
||||||
// Отправляем начальный статус через WebSocket
|
// Отправляем начальный статус через WebSocket
|
||||||
sendWebSocketStatus(false, 'Начинаем настройку VDS...');
|
sendWebSocketStatus(false, 'Начинаем настройку VDS...');
|
||||||
sendWebSocketLog('info', `🚀 Начинаем настройку VDS: ${vdsIp} для домена: ${domain}`, 'init', 0);
|
sendWebSocketLog('info', `🚀 Начинаем настройку VDS: ${normalizedVdsIp} для домена: ${domain}`, 'init', 0);
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
vdsIp,
|
vdsIp: normalizedVdsIp,
|
||||||
sshHost,
|
sshHost: normalizedSshHost,
|
||||||
sshPort,
|
sshPort,
|
||||||
sshConnectUser,
|
sshConnectUser: normalizedSshConnectUser,
|
||||||
sshConnectPassword
|
sshConnectPassword: normalizedSshConnectPassword
|
||||||
};
|
};
|
||||||
|
|
||||||
// 0. Проверка системных требований
|
// 0. Проверка системных требований
|
||||||
@@ -404,6 +423,52 @@ findtime = 3600
|
|||||||
await execSshCommand(`chown ${dockerUser}:${dockerUser} /home/${dockerUser}/dapp/ssl/keys`, options);
|
await execSshCommand(`chown ${dockerUser}:${dockerUser} /home/${dockerUser}/dapp/ssl/keys`, options);
|
||||||
log.success('Директория для ключа шифрования подготовлена');
|
log.success('Директория для ключа шифрования подготовлена');
|
||||||
|
|
||||||
|
// 9.1. Передача ключа шифрования на VDS
|
||||||
|
sendWebSocketLog('info', '🔐 Передача ключа шифрования на VDS...', 'encryption_key', 36);
|
||||||
|
log.info('🔐 Передача ключа шифрования на VDS...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Читаем ключ шифрования с локальной машины
|
||||||
|
const encryptionKeyPath = process.env.ENCRYPTION_KEY_PATH
|
||||||
|
|| path.resolve(__dirname, '..', 'ssl', 'keys', 'full_db_encryption.key');
|
||||||
|
|
||||||
|
const encryptionKeyContent = await fs.readFile(encryptionKeyPath, 'utf8');
|
||||||
|
log.success('✅ Ключ шифрования прочитан с локальной машины');
|
||||||
|
|
||||||
|
// Создаем временный файл с ключом
|
||||||
|
const tempKeyPath = `/tmp/encryption_key_${Date.now()}.key`;
|
||||||
|
await fs.writeFile(tempKeyPath, encryptionKeyContent);
|
||||||
|
|
||||||
|
// Передаем файл на VDS через SCP
|
||||||
|
await execScpCommand(
|
||||||
|
tempKeyPath,
|
||||||
|
`/home/${dockerUser}/dapp/ssl/keys/full_db_encryption.key`,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
// Удаляем временный файл
|
||||||
|
await fs.remove(tempKeyPath);
|
||||||
|
|
||||||
|
// Устанавливаем правильные права доступа к ключу на VDS
|
||||||
|
await execSshCommand(`chown ${dockerUser}:${dockerUser} /home/${dockerUser}/dapp/ssl/keys/full_db_encryption.key`, options);
|
||||||
|
await execSshCommand(`chmod 600 /home/${dockerUser}/dapp/ssl/keys/full_db_encryption.key`, options);
|
||||||
|
|
||||||
|
// Проверяем, что ключ успешно передан
|
||||||
|
const verifyResult = await execSshCommand(`ls -la /home/${dockerUser}/dapp/ssl/keys/full_db_encryption.key`, options);
|
||||||
|
|
||||||
|
if (verifyResult.code === 0) {
|
||||||
|
log.success('✅ Ключ шифрования успешно передан на VDS');
|
||||||
|
sendWebSocketLog('success', '✅ Ключ шифрования передан на VDS', 'encryption_key', 37);
|
||||||
|
} else {
|
||||||
|
throw new Error('Не удалось проверить передачу ключа шифрования');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.error('❌ Ошибка передачи ключа шифрования: ' + error.message);
|
||||||
|
sendWebSocketLog('error', '❌ Ошибка передачи ключа шифрования: ' + error.message, 'encryption_key', 37);
|
||||||
|
// Продолжаем установку, но предупреждаем пользователя
|
||||||
|
log.warn('⚠️ Внимание: ключ шифрования не передан. Backend может не запуститься без ключа.');
|
||||||
|
}
|
||||||
|
|
||||||
// 10. Проверка и удаление системного nginx для избежания конфликтов портов
|
// 10. Проверка и удаление системного nginx для избежания конфликтов портов
|
||||||
log.info('🔍 Проверка наличия системного nginx...');
|
log.info('🔍 Проверка наличия системного nginx...');
|
||||||
const nginxCheck = await execSshCommand('systemctl list-units --type=service --state=active,inactive | grep nginx || echo "nginx not found"', options);
|
const nginxCheck = await execSshCommand('systemctl list-units --type=service --state=active,inactive | grep nginx || echo "nginx not found"', options);
|
||||||
@@ -677,27 +742,34 @@ app.post('/vds/diagnostics', logRequest, async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
vdsIp,
|
vdsIp,
|
||||||
|
sshUser,
|
||||||
sshHost,
|
sshHost,
|
||||||
sshPort = 22,
|
sshPort = 22,
|
||||||
sshConnectUser,
|
sshConnectUser,
|
||||||
sshConnectPassword
|
sshConnectPassword
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
if (!vdsIp || !sshConnectUser || !sshConnectPassword) {
|
// Нормализуем значения (удаляем пробелы)
|
||||||
|
const normalizedVdsIp = String(vdsIp || '').trim();
|
||||||
|
const normalizedSshHost = sshHost ? String(sshHost).trim() : undefined;
|
||||||
|
const normalizedSshConnectUser = String(sshConnectUser || sshUser || 'root').trim();
|
||||||
|
const normalizedSshConnectPassword = sshConnectPassword ? String(sshConnectPassword).trim() : undefined;
|
||||||
|
|
||||||
|
if (!normalizedVdsIp || !normalizedSshConnectUser || !normalizedSshConnectPassword) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Необходимы параметры: vdsIp, sshConnectUser, sshConnectPassword'
|
message: 'Необходимы параметры: vdsIp, sshConnectUser, sshConnectPassword'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(`Диагностика VDS: ${vdsIp}`);
|
log.info(`Диагностика VDS: ${normalizedVdsIp}`);
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
vdsIp,
|
vdsIp: normalizedVdsIp,
|
||||||
sshHost,
|
sshHost: normalizedSshHost,
|
||||||
sshPort,
|
sshPort,
|
||||||
sshConnectUser,
|
sshConnectUser: normalizedSshConnectUser,
|
||||||
sshConnectPassword
|
sshConnectPassword: normalizedSshConnectPassword
|
||||||
};
|
};
|
||||||
|
|
||||||
// 1. Проверка статуса системы
|
// 1. Проверка статуса системы
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ services:
|
|||||||
# Также монтируем в стандартные пути для совместимости (read-only для безопасности)
|
# Также монтируем в стандартные пути для совместимости (read-only для безопасности)
|
||||||
- /proc:/proc:ro
|
- /proc:/proc:ro
|
||||||
- /sys:/sys:ro
|
- /sys:/sys:ro
|
||||||
|
- ~/.ssh:/root/.ssh:ro # SSH ключи для подключения к VDS
|
||||||
# Добавляем необходимые capabilities для управления системой
|
# Добавляем необходимые capabilities для управления системой
|
||||||
cap_add:
|
cap_add:
|
||||||
- SYS_ADMIN # Для управления системой (reboot, shutdown, useradd и т.д.)
|
- SYS_ADMIN # Для управления системой (reboot, shutdown, useradd и т.д.)
|
||||||
|
|||||||
@@ -7,16 +7,114 @@ const log = require('./logger');
|
|||||||
const cleanupVdsServer = async (options) => {
|
const cleanupVdsServer = async (options) => {
|
||||||
log.info('Очистка VDS сервера...');
|
log.info('Очистка VDS сервера...');
|
||||||
|
|
||||||
// Остановка и удаление существующих Docker контейнеров
|
// Проверяем наличие Docker перед попыткой очистки
|
||||||
log.info('Остановка существующих Docker контейнеров...');
|
log.info('🔍 Проверка наличия Docker...');
|
||||||
await execSshCommand('docker ps -aq | xargs -r docker stop 2>/dev/null || true', options);
|
const dockerCheck = await execSshCommand('command -v docker >/dev/null 2>&1 && echo "docker installed" || echo "docker not installed"', options);
|
||||||
await execSshCommand('docker ps -aq | xargs -r docker rm 2>/dev/null || true', options);
|
|
||||||
|
|
||||||
// Удаление Docker образов и очистка системы
|
// Если ошибка SSH подключения (код 255), пропускаем очистку Docker
|
||||||
log.info('Очистка Docker образов и системы...');
|
if (dockerCheck.code === 255) {
|
||||||
await execSshCommand('docker system prune -af || true', options);
|
log.warn('⚠️ Ошибка SSH подключения при проверке Docker, пропускаем очистку Docker');
|
||||||
await execSshCommand('docker volume prune -f || true', options);
|
log.info('Продолжаем настройку сервера...');
|
||||||
await execSshCommand('docker network prune -f || true', options);
|
} else {
|
||||||
|
const hasDocker = dockerCheck.stdout.trim().includes('docker installed');
|
||||||
|
|
||||||
|
if (hasDocker) {
|
||||||
|
log.info('Docker обнаружен, выполняем полную очистку...');
|
||||||
|
|
||||||
|
// 1. Остановка всех Docker контейнеров (включая запущенные)
|
||||||
|
log.info('🛑 Остановка всех Docker контейнеров...');
|
||||||
|
const stopResult = await execSshCommand('docker ps -aq | xargs -r docker stop 2>/dev/null || true', options);
|
||||||
|
if (stopResult.code !== 0 && stopResult.code !== 255) {
|
||||||
|
log.warn(`Предупреждение при остановке контейнеров: ${stopResult.stderr}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Удаление всех контейнеров (включая остановленные)
|
||||||
|
log.info('🗑️ Удаление всех Docker контейнеров...');
|
||||||
|
// Удаляем все контейнеры (запущенные и остановленные)
|
||||||
|
const rmResult = await execSshCommand('docker container ls -aq | xargs -r docker rm -f 2>/dev/null || true', options);
|
||||||
|
if (rmResult.code !== 0 && rmResult.code !== 255) {
|
||||||
|
log.warn(`Предупреждение при удалении контейнеров: ${rmResult.stderr}`);
|
||||||
|
}
|
||||||
|
// Дополнительная проверка и удаление любых оставшихся контейнеров
|
||||||
|
await execSshCommand('docker ps -aq 2>/dev/null | xargs -r docker rm -f 2>/dev/null || true', options);
|
||||||
|
|
||||||
|
// 3. Удаление всех volumes (включая именованные)
|
||||||
|
log.info('🗑️ Удаление всех Docker volumes...');
|
||||||
|
// Сначала получаем список всех volumes
|
||||||
|
const allVolumesList = await execSshCommand('docker volume ls -q 2>/dev/null || true', options);
|
||||||
|
if (allVolumesList.stdout.trim()) {
|
||||||
|
const volumes = allVolumesList.stdout.trim().split('\n').filter(v => v);
|
||||||
|
log.info(`Найдено ${volumes.length} volumes для удаления`);
|
||||||
|
for (const volume of volumes) {
|
||||||
|
// Принудительно удаляем каждый volume
|
||||||
|
await execSshCommand(`docker volume rm -f "${volume}" 2>/dev/null || true`, options);
|
||||||
|
}
|
||||||
|
log.info(`Удалено ${volumes.length} volumes`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Дополнительная очистка неиспользуемых volumes (на случай если что-то осталось)
|
||||||
|
log.info('🧹 Финальная очистка volumes...');
|
||||||
|
const volumePruneResult = await execSshCommand('docker volume prune -f 2>/dev/null || true', options);
|
||||||
|
if (volumePruneResult.code !== 0 && volumePruneResult.code !== 255) {
|
||||||
|
log.warn(`Предупреждение при финальной очистке volumes: ${volumePruneResult.stderr}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Удаление всех Docker образов приложения
|
||||||
|
log.info('🗑️ Удаление старых Docker образов приложения...');
|
||||||
|
const imagesList = await execSshCommand('docker images -q | xargs -r docker rmi -f 2>/dev/null || true', options);
|
||||||
|
// Удаляем все образы, связанные с приложением
|
||||||
|
await execSshCommand('docker images --format "{{.Repository}}:{{.Tag}}" | grep -E "digital_legal_entity|dapp-" | xargs -r docker rmi -f 2>/dev/null || true', options);
|
||||||
|
|
||||||
|
// 6. Полная очистка Docker системы (все образы, кэш, сети)
|
||||||
|
log.info('🧹 Полная очистка Docker системы...');
|
||||||
|
const pruneResult = await execSshCommand('docker system prune -af --volumes 2>/dev/null || true', options);
|
||||||
|
if (pruneResult.code !== 0 && pruneResult.code !== 255) {
|
||||||
|
log.warn(`Предупреждение при очистке Docker: ${pruneResult.stderr}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Удаление всех Docker сетей
|
||||||
|
log.info('🗑️ Удаление всех Docker сетей...');
|
||||||
|
const networkPruneResult = await execSshCommand('docker network prune -f 2>/dev/null || true', options);
|
||||||
|
if (networkPruneResult.code !== 0 && networkPruneResult.code !== 255) {
|
||||||
|
log.warn(`Предупреждение при очистке сетей: ${networkPruneResult.stderr}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.success('✅ Docker полностью очищен');
|
||||||
|
} else {
|
||||||
|
log.info('ℹ️ Docker не установлен, пропускаем очистку Docker');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удаление старых директорий приложения
|
||||||
|
log.info('🗑️ Удаление старых директорий приложения...');
|
||||||
|
await execSshCommand('find /home -maxdepth 3 -type d -name "dapp" -exec rm -rf {} + 2>/dev/null || true', options);
|
||||||
|
await execSshCommand('find /home -maxdepth 3 -type d -name "digital_legal_entity" -exec rm -rf {} + 2>/dev/null || true', options);
|
||||||
|
|
||||||
|
// Удаление старых docker-compose файлов
|
||||||
|
log.info('🗑️ Удаление старых конфигурационных файлов...');
|
||||||
|
await execSshCommand('find /home -name "docker-compose*.yml" -type f -delete 2>/dev/null || true', options);
|
||||||
|
await execSshCommand('find /home -name ".env" -path "*/dapp/*" -type f -delete 2>/dev/null || true', options);
|
||||||
|
await execSshCommand('find /home -name "import-images-and-data.sh" -type f -delete 2>/dev/null || true', options);
|
||||||
|
await execSshCommand('find /home -name "renew-ssl.sh" -type f -delete 2>/dev/null || true', options);
|
||||||
|
|
||||||
|
// Очистка старых cron задач, связанных с приложением
|
||||||
|
log.info('🗑️ Очистка старых cron задач приложения...');
|
||||||
|
const crontabBackup = await execSshCommand('crontab -l 2>/dev/null || echo ""', options);
|
||||||
|
if (crontabBackup.stdout.trim()) {
|
||||||
|
const cleanedCrontab = crontabBackup.stdout
|
||||||
|
.split('\n')
|
||||||
|
.filter(line => !line.includes('renew-ssl.sh') && !line.includes('dapp') && !line.includes('digital_legal_entity'))
|
||||||
|
.join('\n');
|
||||||
|
if (cleanedCrontab.trim()) {
|
||||||
|
await execSshCommand(`echo '${cleanedCrontab.replace(/'/g, "'\\''")}' | crontab -`, options);
|
||||||
|
} else {
|
||||||
|
await execSshCommand('crontab -r 2>/dev/null || true', options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Очистка старых SSL сертификатов (опционально, можно оставить для повторного использования)
|
||||||
|
log.info('🧹 Проверка старых SSL сертификатов...');
|
||||||
|
await execSshCommand('rm -rf /var/www/certbot/.well-known 2>/dev/null || true', options);
|
||||||
|
|
||||||
// 🆕 Умная проверка и удаление системного nginx для избежания конфликтов портов
|
// 🆕 Умная проверка и удаление системного nginx для избежания конфликтов портов
|
||||||
log.info('🔍 Проверка наличия системного nginx...');
|
log.info('🔍 Проверка наличия системного nginx...');
|
||||||
|
|||||||
@@ -69,21 +69,26 @@ const exportDockerImages = async (sendWebSocketLog) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Экспортируем данные из volumes
|
// Экспортируем данные из volumes (динамически определяем все volumes приложения)
|
||||||
log.info('Экспорт данных из Docker volumes...');
|
log.info('Экспорт данных из Docker volumes...');
|
||||||
sendWebSocketLog('info', '📦 Экспорт данных из Docker volumes...', 'export_data', 70);
|
sendWebSocketLog('info', '📦 Экспорт данных из Docker volumes...', 'export_data', 70);
|
||||||
|
|
||||||
// PostgreSQL данные
|
// Получаем список всех volumes приложения (без node_modules)
|
||||||
sendWebSocketLog('info', '📦 Экспорт данных PostgreSQL...', 'export_data', 72);
|
const volumesList = await execLocalCommand('docker volume ls -q | grep -E "digital_legal_entitydle_|dapp_" | grep -v node_modules || true');
|
||||||
await exportVolumeData('digital_legal_entitydle_postgres_data', 'postgres_data.tar.gz', sendWebSocketLog, 72);
|
const volumes = volumesList.stdout.trim().split('\n').filter(v => v && v.endsWith('_data'));
|
||||||
|
|
||||||
// Ollama данные
|
let progress = 72;
|
||||||
sendWebSocketLog('info', '📦 Экспорт данных Ollama...', 'export_data', 75);
|
const progressStep = Math.floor(8 / Math.max(volumes.length, 1));
|
||||||
await exportVolumeData('digital_legal_entitydle_ollama_data', 'ollama_data.tar.gz', sendWebSocketLog, 75);
|
|
||||||
|
|
||||||
// Vector Search данные
|
for (const volumeName of volumes) {
|
||||||
sendWebSocketLog('info', '📦 Экспорт данных Vector Search...', 'export_data', 78);
|
// Извлекаем имя файла из имени volume (например, digital_legal_entitydle_postgres_data -> postgres_data.tar.gz)
|
||||||
await exportVolumeData('digital_legal_entitydle_vector_search_data', 'vector_search_data.tar.gz', sendWebSocketLog, 78);
|
const volumeBaseName = volumeName.replace(/^(digital_legal_entitydle_|dapp_)/, '').replace(/_data$/, '_data');
|
||||||
|
const outputFile = `${volumeBaseName}.tar.gz`;
|
||||||
|
|
||||||
|
sendWebSocketLog('info', `📦 Экспорт данных: ${volumeName}`, 'export_data', progress);
|
||||||
|
await exportVolumeData(volumeName, outputFile, sendWebSocketLog, progress);
|
||||||
|
progress += progressStep;
|
||||||
|
}
|
||||||
|
|
||||||
// Создаем архив с ВСЕМИ образами и данными приложения
|
// Создаем архив с ВСЕМИ образами и данными приложения
|
||||||
log.info('Создание архива Docker образов и данных на хосте...');
|
log.info('Создание архива Docker образов и данных на хосте...');
|
||||||
@@ -91,9 +96,11 @@ const exportDockerImages = async (sendWebSocketLog) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const tarFiles = images.map(img => img.file).join(' ');
|
const tarFiles = images.map(img => img.file).join(' ');
|
||||||
const dataFiles = 'postgres_data.tar.gz ollama_data.tar.gz vector_search_data.tar.gz';
|
// Динамически собираем список файлов данных из экспортированных volumes
|
||||||
|
const dataFilesList = await execLocalCommand('ls /tmp/*_data.tar.gz 2>/dev/null | xargs -r basename -a || echo ""');
|
||||||
|
const dataFiles = dataFilesList.stdout.trim().split('\n').filter(f => f).join(' ');
|
||||||
|
|
||||||
const archiveCommand = `cd /tmp && tar -czf docker-images-and-data.tar.gz ${tarFiles} ${dataFiles}`;
|
const archiveCommand = `cd /tmp && tar -czf docker-images-and-data.tar.gz ${tarFiles} ${dataFiles || ''}`.trim();
|
||||||
await execLocalCommand(archiveCommand);
|
await execLocalCommand(archiveCommand);
|
||||||
|
|
||||||
sendWebSocketLog('success', '✅ Архив создан успешно', 'export_data', 80);
|
sendWebSocketLog('success', '✅ Архив создан успешно', 'export_data', 80);
|
||||||
@@ -170,45 +177,33 @@ echo "📦 Распаковка архива..."
|
|||||||
tar -xzf ./docker-images-and-data.tar.gz -C ./temp-import
|
tar -xzf ./docker-images-and-data.tar.gz -C ./temp-import
|
||||||
|
|
||||||
# Импортируем ВСЕ образы приложения
|
# Импортируем ВСЕ образы приложения
|
||||||
echo "📦 Импорт образа postgres..."
|
echo "📦 Импорт образов..."
|
||||||
docker load -i ./temp-import/dapp-postgres.tar
|
for image_file in ./temp-import/dapp-*.tar; do
|
||||||
|
if [ -f "$image_file" ]; then
|
||||||
|
echo "📦 Импорт образа: $(basename $image_file)"
|
||||||
|
docker load -i "$image_file"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
echo "📦 Импорт образа ollama..."
|
# 🆕 Динамически определяем volumes для импорта из имен файлов в архиве
|
||||||
docker load -i ./temp-import/dapp-ollama.tar
|
echo "📦 Импорт данных в volumes..."
|
||||||
|
for data_file in ./temp-import/*_data.tar.gz; do
|
||||||
|
if [ -f "$data_file" ]; then
|
||||||
|
# Извлекаем имя volume из имени файла (например, postgres_data.tar.gz -> postgres_data)
|
||||||
|
volume_name=$(basename "$data_file" .tar.gz)
|
||||||
|
|
||||||
echo "📦 Импорт образа vector-search..."
|
# Используем префикс dapp_ для соответствия docker-compose.prod.yml
|
||||||
docker load -i ./temp-import/dapp-vector-search.tar
|
full_volume_name="dapp_${volume_name}"
|
||||||
|
|
||||||
echo "📦 Импорт образа backend..."
|
echo "📦 Импорт данных: $full_volume_name"
|
||||||
docker load -i ./temp-import/dapp-backend.tar
|
|
||||||
|
|
||||||
echo "📦 Импорт образа frontend..."
|
|
||||||
docker load -i ./temp-import/dapp-frontend.tar
|
|
||||||
|
|
||||||
echo "📦 Импорт образа frontend-nginx..."
|
|
||||||
docker load -i ./temp-import/dapp-frontend-nginx.tar
|
|
||||||
|
|
||||||
echo "📦 Импорт образа webssh-agent..."
|
|
||||||
docker load -i ./temp-import/dapp-webssh-agent.tar
|
|
||||||
|
|
||||||
# 🆕 Импортируем данные в volumes с правильными именами для соответствия docker-compose
|
|
||||||
echo "📦 Импорт данных PostgreSQL..."
|
|
||||||
# Удаляем старый volume если существует
|
# Удаляем старый volume если существует
|
||||||
docker volume rm dapp_postgres_data 2>/dev/null || true
|
docker volume rm -f "$full_volume_name" 2>/dev/null || true
|
||||||
docker volume create dapp_postgres_data
|
# Создаем новый volume
|
||||||
docker run --rm -v dapp_postgres_data:/data -v ./temp-import:/backup alpine tar xzf /backup/postgres_data.tar.gz -C /data
|
docker volume create "$full_volume_name"
|
||||||
|
# Импортируем данные
|
||||||
echo "📦 Импорт данных Ollama..."
|
docker run --rm -v "$full_volume_name:/data" -v ./temp-import:/backup alpine tar xzf "/backup/$(basename $data_file)" -C /data
|
||||||
# Удаляем старый volume если существует
|
fi
|
||||||
docker volume rm dapp_ollama_data 2>/dev/null || true
|
done
|
||||||
docker volume create dapp_ollama_data
|
|
||||||
docker run --rm -v dapp_ollama_data:/data -v ./temp-import:/backup alpine tar xzf /backup/ollama_data.tar.gz -C /data
|
|
||||||
|
|
||||||
echo "📦 Импорт данных Vector Search..."
|
|
||||||
# Удаляем старый volume если существует
|
|
||||||
docker volume rm dapp_vector_search_data 2>/dev/null || true
|
|
||||||
docker volume create dapp_vector_search_data
|
|
||||||
docker run --rm -v dapp_vector_search_data:/data -v ./temp-import:/backup alpine tar xzf /backup/vector_search_data.tar.gz -C /data
|
|
||||||
|
|
||||||
# Очищаем временные файлы
|
# Очищаем временные файлы
|
||||||
rm -rf ./temp-import
|
rm -rf ./temp-import
|
||||||
|
|||||||
@@ -35,10 +35,22 @@ const execSshCommand = async (command, options = {}) => {
|
|||||||
const privateKeyExists = await fs.pathExists(privateKeyPath);
|
const privateKeyExists = await fs.pathExists(privateKeyPath);
|
||||||
const escapedCommand = command.replace(/"/g, '\\"');
|
const escapedCommand = command.replace(/"/g, '\\"');
|
||||||
|
|
||||||
let sshCommand = `ssh -p ${sshPort} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${sshConnectUser}@${sshHost || vdsIp} "${escapedCommand}"`;
|
// Удаляем пробелы и проверяем, что значения не пустые
|
||||||
|
const user = String(sshConnectUser || 'root').trim();
|
||||||
|
const host = String((sshHost || vdsIp || '')).trim();
|
||||||
|
|
||||||
|
if (!host) {
|
||||||
|
throw new Error('Не указан хост для SSH подключения (sshHost или vdsIp)');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('Не указан пользователь для SSH подключения (sshConnectUser)');
|
||||||
|
}
|
||||||
|
|
||||||
|
let sshCommand = `ssh -p ${sshPort} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${user}@${host} "${escapedCommand}"`;
|
||||||
|
|
||||||
if (privateKeyExists) {
|
if (privateKeyExists) {
|
||||||
sshCommand = `ssh -i "${privateKeyPath}" -p ${sshPort} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${sshConnectUser}@${sshHost || vdsIp} "${escapedCommand}"`;
|
sshCommand = `ssh -i "${privateKeyPath}" -p ${sshPort} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${user}@${host} "${escapedCommand}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(`🔍 Выполняем SSH команду: ${sshCommand}`);
|
log.info(`🔍 Выполняем SSH команду: ${sshCommand}`);
|
||||||
@@ -49,7 +61,7 @@ const execSshCommand = async (command, options = {}) => {
|
|||||||
|
|
||||||
if (error && error.code === 255 && sshConnectPassword) {
|
if (error && error.code === 255 && sshConnectPassword) {
|
||||||
log.info('SSH ключи не сработали, пробуем с паролем...');
|
log.info('SSH ключи не сработали, пробуем с паролем...');
|
||||||
const passwordCommand = `sshpass -p "${sshConnectPassword}" ssh -p ${sshPort} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${sshConnectUser}@${sshHost || vdsIp} "${escapedCommand}"`;
|
const passwordCommand = `sshpass -p "${String(sshConnectPassword || '').trim()}" ssh -p ${sshPort} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${user}@${host} "${escapedCommand}"`;
|
||||||
|
|
||||||
exec(passwordCommand, (passwordError, passwordStdout, passwordStderr) => {
|
exec(passwordCommand, (passwordError, passwordStdout, passwordStderr) => {
|
||||||
log.info(`📤 SSH с паролем результат - код: ${passwordError ? passwordError.code : 0}, stdout: "${passwordStdout}", stderr: "${passwordStderr}"`);
|
log.info(`📤 SSH с паролем результат - код: ${passwordError ? passwordError.code : 0}, stdout: "${passwordStdout}", stderr: "${passwordStderr}"`);
|
||||||
@@ -83,17 +95,29 @@ const execScpCommand = async (sourcePath, targetPath, options = {}) => {
|
|||||||
|
|
||||||
const privateKeyExists = await fs.pathExists(privateKeyPath);
|
const privateKeyExists = await fs.pathExists(privateKeyPath);
|
||||||
|
|
||||||
let scpCommand = `scp -P ${sshPort} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${sourcePath} ${sshConnectUser}@${sshHost || vdsIp}:${targetPath}`;
|
// Удаляем пробелы и проверяем, что значения не пустые
|
||||||
|
const user = String(sshConnectUser || 'root').trim();
|
||||||
|
const host = String((sshHost || vdsIp || '')).trim();
|
||||||
|
|
||||||
|
if (!host) {
|
||||||
|
throw new Error('Не указан хост для SCP подключения (sshHost или vdsIp)');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('Не указан пользователь для SCP подключения (sshConnectUser)');
|
||||||
|
}
|
||||||
|
|
||||||
|
let scpCommand = `scp -P ${sshPort} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${sourcePath} ${user}@${host}:${targetPath}`;
|
||||||
|
|
||||||
if (privateKeyExists) {
|
if (privateKeyExists) {
|
||||||
scpCommand = `scp -i "${privateKeyPath}" -P ${sshPort} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${sourcePath} ${sshConnectUser}@${sshHost || vdsIp}:${targetPath}`;
|
scpCommand = `scp -i "${privateKeyPath}" -P ${sshPort} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${sourcePath} ${user}@${host}:${targetPath}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
exec(scpCommand, (error, stdout, stderr) => {
|
exec(scpCommand, (error, stdout, stderr) => {
|
||||||
if (error && error.code === 255 && sshConnectPassword) {
|
if (error && error.code === 255 && sshConnectPassword) {
|
||||||
log.info('SCP с ключами не сработал, пробуем с паролем...');
|
log.info('SCP с ключами не сработал, пробуем с паролем...');
|
||||||
const passwordScpCommand = `sshpass -p "${sshConnectPassword}" scp -P ${sshPort} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${sourcePath} ${sshConnectUser}@${sshHost || vdsIp}:${targetPath}`;
|
const passwordScpCommand = `sshpass -p "${String(sshConnectPassword || '').trim()}" scp -P ${sshPort} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR ${sourcePath} ${user}@${host}:${targetPath}`;
|
||||||
|
|
||||||
exec(passwordScpCommand, (passwordError, passwordStdout, passwordStderr) => {
|
exec(passwordScpCommand, (passwordError, passwordStdout, passwordStderr) => {
|
||||||
if (passwordError) {
|
if (passwordError) {
|
||||||
|
|||||||
Reference in New Issue
Block a user