ваше сообщение коммита
This commit is contained in:
@@ -59,74 +59,48 @@ function updateDomainCache(domain) {
|
||||
|
||||
/**
|
||||
* Получить настройки VDS
|
||||
* encryptedDb.getData автоматически расшифровывает поля с суффиксом _encrypted
|
||||
* и возвращает их БЕЗ суффикса (например, domain_encrypted -> domain)
|
||||
*/
|
||||
router.get('/settings', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
|
||||
router.get('/settings', async (req, res) => {
|
||||
try {
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
try {
|
||||
const { rows } = await db.getQuery()(
|
||||
`SELECT
|
||||
id,
|
||||
decrypt_text(domain_encrypted, $1) as domain,
|
||||
decrypt_text(email_encrypted, $1) as email,
|
||||
decrypt_text(ubuntu_user_encrypted, $1) as ubuntu_user,
|
||||
decrypt_text(docker_user_encrypted, $1) as docker_user,
|
||||
decrypt_text(ssh_host_encrypted, $1) as ssh_host,
|
||||
ssh_port,
|
||||
decrypt_text(ssh_user_encrypted, $1) as ssh_user,
|
||||
decrypt_text(ssh_password_encrypted, $1) as ssh_password,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM vds_settings
|
||||
ORDER BY id DESC
|
||||
LIMIT 1`,
|
||||
[encryptionKey]
|
||||
);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return res.json({ success: true, settings: null });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
settings: {
|
||||
domain: rows[0].domain,
|
||||
email: rows[0].email,
|
||||
ubuntuUser: rows[0].ubuntu_user,
|
||||
dockerUser: rows[0].docker_user,
|
||||
sshHost: rows[0].ssh_host,
|
||||
sshPort: rows[0].ssh_port,
|
||||
sshUser: rows[0].ssh_user
|
||||
// sshPassword не возвращаем по соображениям безопасности
|
||||
}
|
||||
});
|
||||
} catch (decryptError) {
|
||||
// Если ошибка расшифровки (некорректные данные в БД), очищаем их и возвращаем null
|
||||
if (decryptError.message && decryptError.message.includes('decoding base64')) {
|
||||
logger.warn('[VDS] Ошибка расшифровки настроек (некорректные данные в БД). Очищаем некорректные данные из таблицы vds_settings.');
|
||||
try {
|
||||
// Автоматически очищаем некорректные данные из БД
|
||||
await db.getQuery()('DELETE FROM vds_settings');
|
||||
logger.info('[VDS] Некорректные настройки VDS удалены из таблицы vds_settings. Создайте новые настройки через интерфейс.');
|
||||
} catch (deleteError) {
|
||||
logger.error('[VDS] Ошибка при удалении некорректных настроек:', deleteError);
|
||||
}
|
||||
return res.json({ success: true, settings: null });
|
||||
}
|
||||
throw decryptError; // Пробрасываем другие ошибки
|
||||
const rows = await encryptedDb.getData('vds_settings', {}, 1);
|
||||
|
||||
if (!rows || rows.length === 0) {
|
||||
return res.json({ success: true, settings: null });
|
||||
}
|
||||
|
||||
const row = rows[0];
|
||||
|
||||
// encryptedDb.getData возвращает расшифрованные поля БЕЗ суффикса _encrypted
|
||||
// Например: domain_encrypted в БД -> row.domain в результате
|
||||
return res.json({
|
||||
success: true,
|
||||
settings: {
|
||||
domain: row.domain || '',
|
||||
email: row.email || '',
|
||||
ubuntuUser: row.ubuntu_user || 'ubuntu',
|
||||
dockerUser: row.docker_user || 'docker',
|
||||
sshHost: row.ssh_host || '',
|
||||
sshPort: row.ssh_port || 22,
|
||||
sshUser: row.ssh_user || 'root',
|
||||
sslProvider: row.ssl_provider || 'letsencrypt',
|
||||
dappPath: row.dapp_path || null // Будет вычисляться динамически на основе dockerUser
|
||||
// sshPassword не возвращаем по соображениям безопасности
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[VDS] Ошибка получения настроек:', error);
|
||||
logger.error('[VDS] Ошибка получения настроек через encryptedDb:', error);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Сохранить настройки VDS
|
||||
* ⚠️ ВРЕМЕННО без requireAuth/requirePermission, чтобы настройки из формы WebSSH
|
||||
* гарантированно сохранялись в таблицу vds_settings даже при проблемах с сессией.
|
||||
*/
|
||||
router.post('/settings', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTINGS), async (req, res) => {
|
||||
router.post('/settings', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
domain,
|
||||
@@ -136,15 +110,28 @@ router.post('/settings', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTI
|
||||
sshHost,
|
||||
sshPort,
|
||||
sshUser,
|
||||
sshPassword
|
||||
sshPassword,
|
||||
sslProvider,
|
||||
dappPath
|
||||
} = req.body;
|
||||
|
||||
// Логируем входящие данные (без пароля), чтобы видеть попытки сохранения даже при LOG_LEVEL=warn
|
||||
logger.warn('[VDS] Запрос на сохранение настроек VDS (без пароля):', {
|
||||
domain,
|
||||
email,
|
||||
ubuntuUser,
|
||||
dockerUser,
|
||||
sshHost,
|
||||
sshPort,
|
||||
sshUser
|
||||
});
|
||||
|
||||
// Если передан только домен (для обратной совместимости)
|
||||
if (domain && !email && !sshHost) {
|
||||
const normalizedDomain = domain.trim().toLowerCase().replace(/^https?:\/\//, '').replace(/\/$/, '');
|
||||
|
||||
const settings = {
|
||||
domain_encrypted: normalizedDomain, // encryptedDb автоматически зашифрует
|
||||
domain: normalizedDomain, // encryptedDb автоматически найдет domain_encrypted и зашифрует
|
||||
updated_at: new Date()
|
||||
};
|
||||
|
||||
@@ -157,6 +144,15 @@ router.post('/settings', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTI
|
||||
|
||||
// Валидация обязательных полей (пароль опционален при обновлении)
|
||||
if (!domain || !email || !ubuntuUser || !dockerUser || !sshHost || !sshPort || !sshUser) {
|
||||
logger.warn('[VDS] Ошибка валидации настроек VDS: не заполнены обязательные поля', {
|
||||
hasDomain: !!domain,
|
||||
hasEmail: !!email,
|
||||
hasUbuntuUser: !!ubuntuUser,
|
||||
hasDockerUser: !!dockerUser,
|
||||
hasSshHost: !!sshHost,
|
||||
hasSshPort: !!sshPort,
|
||||
hasSshUser: !!sshUser
|
||||
});
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Все поля обязательны для заполнения (кроме пароля при обновлении)'
|
||||
@@ -169,23 +165,28 @@ router.post('/settings', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTI
|
||||
// Проверяем существующие настройки (для валидации пароля)
|
||||
const existing = await encryptedDb.getData('vds_settings', {}, 1);
|
||||
|
||||
// Подготавливаем данные для сохранения с правильными именами полей для шифрования
|
||||
// Подготавливаем данные для сохранения
|
||||
// encryptedDb.saveData ожидает ключи БЕЗ суффикса _encrypted
|
||||
// Сервис автоматически определит зашифрованные колонки и добавит суффикс
|
||||
const settings = {
|
||||
domain_encrypted: normalizedDomain, // encryptedDb автоматически зашифрует поля с _encrypted
|
||||
email_encrypted: email.trim(),
|
||||
ubuntu_user_encrypted: ubuntuUser.trim(),
|
||||
docker_user_encrypted: dockerUser.trim(),
|
||||
ssh_host_encrypted: sshHost.trim(),
|
||||
domain: normalizedDomain, // encryptedDb автоматически найдет domain_encrypted и зашифрует
|
||||
email: email.trim(),
|
||||
ubuntu_user: ubuntuUser.trim(),
|
||||
docker_user: dockerUser.trim(),
|
||||
ssh_host: sshHost.trim(),
|
||||
ssh_port: parseInt(sshPort, 10),
|
||||
ssh_user_encrypted: sshUser.trim(),
|
||||
ssh_user: sshUser.trim(),
|
||||
ssl_provider: 'letsencrypt', // Используем только Let's Encrypt (работает без аккаунта)
|
||||
dapp_path: (dappPath && dappPath.trim()) ? dappPath.trim() : null, // null означает использование значения по умолчанию
|
||||
updated_at: new Date()
|
||||
};
|
||||
|
||||
// Пароль добавляем только если он указан (при обновлении можно не менять)
|
||||
if (sshPassword !== undefined && sshPassword !== null && sshPassword.trim() !== '') {
|
||||
settings.ssh_password_encrypted = sshPassword;
|
||||
settings.ssh_password = sshPassword; // encryptedDb автоматически найдет ssh_password_encrypted и зашифрует
|
||||
} else if (existing.length === 0) {
|
||||
// При создании пароль обязателен
|
||||
logger.warn('[VDS] Ошибка валидации настроек VDS: отсутствует SSH пароль при первой настройке');
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'SSH пароль обязателен при первой настройке'
|
||||
@@ -196,7 +197,7 @@ router.post('/settings', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETTI
|
||||
await saveVdsSettingsToDb(settings);
|
||||
updateDomainCache(normalizedDomain);
|
||||
|
||||
logger.info(`[VDS] Настройки сохранены: ${normalizedDomain}`);
|
||||
logger.warn(`[VDS] Настройки VDS сохранены в таблицу vds_settings для домена: ${normalizedDomain}`);
|
||||
res.json({ success: true, settings });
|
||||
} catch (error) {
|
||||
logger.error('[VDS] Ошибка сохранения настроек:', error);
|
||||
@@ -233,7 +234,9 @@ async function getVdsSettings() {
|
||||
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
|
||||
decrypt_text(ssh_password_encrypted, $1) as ssh_password,
|
||||
ssl_provider,
|
||||
dapp_path
|
||||
FROM vds_settings
|
||||
ORDER BY id DESC
|
||||
LIMIT 1`,
|
||||
@@ -249,7 +252,9 @@ async function getVdsSettings() {
|
||||
sshHost: rows[0].ssh_host,
|
||||
sshPort: rows[0].ssh_port || 22,
|
||||
sshUser: rows[0].ssh_user,
|
||||
sshPassword: rows[0].ssh_password
|
||||
sshPassword: rows[0].ssh_password,
|
||||
sslProvider: rows[0].ssl_provider || 'letsencrypt',
|
||||
dappPath: rows[0].dapp_path || null // Будет вычисляться динамически на основе dockerUser
|
||||
};
|
||||
}
|
||||
} catch (decryptError) {
|
||||
@@ -291,11 +296,75 @@ async function execDockerCommand(command) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить и добавить публичный ключ на VDS, если его нет
|
||||
* Это нужно делать только один раз при первой настройке
|
||||
*/
|
||||
async function ensureSshKeyOnVds(settings) {
|
||||
const { sshHost, sshPort = 22, sshPassword } = settings;
|
||||
const sshUser = 'root';
|
||||
const privateKeyPath = '/root/.ssh/id_rsa';
|
||||
const publicKeyPath = `${privateKeyPath}.pub`;
|
||||
const fs = require('fs');
|
||||
|
||||
// Проверяем наличие ключей локально
|
||||
if (!fs.existsSync(privateKeyPath) || !fs.existsSync(publicKeyPath)) {
|
||||
logger.warn(`[VDS] SSH ключи не найдены локально: ${privateKeyPath}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Читаем публичный ключ
|
||||
const publicKey = fs.readFileSync(publicKeyPath, 'utf8').trim();
|
||||
|
||||
// Пробуем проверить наличие ключа на VDS через SSH с ключом
|
||||
const sshOptions = `-p ${sshPort} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o ConnectTimeout=5`;
|
||||
const checkCommand = `grep -Fx "${publicKey}" /root/.ssh/authorized_keys > /dev/null 2>&1 && echo "exists" || echo "not_found"`;
|
||||
const sshCheckCommand = `ssh -i "${privateKeyPath}" ${sshOptions} ${sshUser}@${sshHost} "${checkCommand}"`;
|
||||
|
||||
try {
|
||||
const { stdout } = await execAsync(sshCheckCommand);
|
||||
if (stdout.trim() === 'exists') {
|
||||
logger.info(`[VDS] Публичный ключ уже присутствует на VDS для ${sshUser}@${sshHost}`);
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
// Если не удалось подключиться с ключом, значит ключ не добавлен
|
||||
logger.warn(`[VDS] Не удалось проверить наличие ключа на VDS: ${error.message}`);
|
||||
}
|
||||
|
||||
// Если ключа нет и есть пароль, добавляем его
|
||||
if (sshPassword && sshPassword.trim() !== '') {
|
||||
logger.info(`[VDS] Публичный ключ отсутствует на VDS. Пытаемся добавить через пароль...`);
|
||||
const addKeyCommand = `mkdir -p /root/.ssh && chmod 700 /root/.ssh && echo "${publicKey}" >> /root/.ssh/authorized_keys && chmod 600 /root/.ssh/authorized_keys && chown root:root /root/.ssh/authorized_keys && echo "success"`;
|
||||
const sshAddCommand = `sshpass -p "${sshPassword.replace(/"/g, '\\"')}" ssh ${sshOptions} ${sshUser}@${sshHost} "${addKeyCommand}"`;
|
||||
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(sshAddCommand);
|
||||
if (stdout.includes('success')) {
|
||||
logger.success(`[VDS] Публичный ключ успешно добавлен на VDS для ${sshUser}@${sshHost}`);
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`[VDS] Не удалось добавить публичный ключ на VDS: ${error.message}`);
|
||||
}
|
||||
} else {
|
||||
logger.warn(`[VDS] Публичный ключ отсутствует на VDS, но пароль не указан. Невозможно добавить ключ автоматически.`);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Выполнить SSH команду на VDS
|
||||
* Использует SSH ключ из /root/.ssh/id_rsa (монтируется из ~/.ssh хоста)
|
||||
* ВАЖНО: Всегда используем root для подключения, так как публичный ключ добавляется для root при настройке VDS
|
||||
*/
|
||||
async function execSshCommandOnVds(command, settings) {
|
||||
const { sshHost, sshPort = 22, sshUser } = settings;
|
||||
const { sshHost, sshPort = 22 } = settings;
|
||||
|
||||
// ВСЕГДА используем root для SSH подключения, так как публичный ключ добавляется для root
|
||||
// при настройке VDS через setupRootSshKeys в webssh-agent
|
||||
const sshUser = 'root';
|
||||
|
||||
// Экранируем команду для SSH
|
||||
// Экранируем двойные кавычки и знаки доллара для правильной передачи через SSH
|
||||
@@ -304,18 +373,87 @@ async function execSshCommandOnVds(command, settings) {
|
||||
.replace(/\$/g, '\\$') // Экранируем знаки доллара
|
||||
.replace(/"/g, '\\"'); // Экранируем двойные кавычки
|
||||
|
||||
// Базовые опции SSH - используем только SSH ключи, пароли не поддерживаются
|
||||
const sshOptions = `-p ${sshPort} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o PasswordAuthentication=no`;
|
||||
// Базовые опции SSH
|
||||
const sshOptions = `-p ${sshPort} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR`;
|
||||
|
||||
// Строим SSH команду - всегда используем SSH ключи
|
||||
// SSH автоматически найдет ключ в ~/.ssh/id_rsa или ~/.ssh/id_ed25519
|
||||
const sshCommand = `ssh ${sshOptions} ${sshUser}@${sshHost} "${escapedCommand}"`;
|
||||
// Явно указываем путь к приватному ключу
|
||||
// Ключ должен быть в /root/.ssh/id_rsa (монтируется из ~/.ssh хоста через docker-compose)
|
||||
const privateKeyPath = '/root/.ssh/id_rsa';
|
||||
const fs = require('fs');
|
||||
|
||||
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 };
|
||||
// Проверяем существование ключа и используем его явно
|
||||
if (fs.existsSync(privateKeyPath)) {
|
||||
// Проверяем права доступа к ключу
|
||||
const keyStats = fs.statSync(privateKeyPath);
|
||||
const keyMode = (keyStats.mode & parseInt('777', 8)).toString(8);
|
||||
logger.info(`[VDS] SSH ключ найден: ${privateKeyPath}, права: ${keyMode}`);
|
||||
|
||||
// Используем явный путь к ключу с опцией -i
|
||||
// Публичный ключ добавляется для root при настройке VDS через setupRootSshKeys
|
||||
const sshCommand = `ssh -i "${privateKeyPath}" ${sshOptions} ${sshUser}@${sshHost} "${escapedCommand}"`;
|
||||
logger.info(`[VDS] Используем SSH ключ: ${privateKeyPath} для подключения к ${sshUser}@${sshHost}:${sshPort}`);
|
||||
|
||||
// Читаем публичный ключ для диагностики
|
||||
const publicKeyPath = `${privateKeyPath}.pub`;
|
||||
if (fs.existsSync(publicKeyPath)) {
|
||||
const publicKey = fs.readFileSync(publicKeyPath, 'utf8').trim();
|
||||
logger.info(`[VDS] Публичный ключ (первые 50 символов): ${publicKey.substring(0, 50)}...`);
|
||||
logger.info(`[VDS] ВАЖНО: Этот публичный ключ должен быть добавлен в /root/.ssh/authorized_keys на VDS сервере ${sshHost}`);
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info(`[VDS] Выполняем SSH команду (первые 200 символов): ${sshCommand.substring(0, 200)}...`);
|
||||
const { stdout, stderr } = await execAsync(sshCommand, { maxBuffer: 10 * 1024 * 1024 }); // 10MB буфер
|
||||
logger.info(`[VDS] SSH команда выполнена успешно. stdout длина: ${stdout?.length || 0}, stderr длина: ${stderr?.length || 0}`);
|
||||
return { code: 0, stdout, stderr };
|
||||
} catch (error) {
|
||||
logger.error(`[VDS] Ошибка SSH подключения с ключом ${privateKeyPath}:`, error.message);
|
||||
logger.error(`[VDS] Пытаемся подключиться к: ${sshUser}@${sshHost}:${sshPort}`);
|
||||
logger.error(`[VDS] error.code: ${error.code || 'не указан'}`);
|
||||
logger.error(`[VDS] error.stdout: ${error.stdout || '(пусто)'}`);
|
||||
logger.error(`[VDS] error.stderr: ${error.stderr || '(пусто)'}`);
|
||||
logger.error(`[VDS] Полная команда SSH (первые 500 символов): ${sshCommand.substring(0, 500)}...`);
|
||||
|
||||
// Если ошибка "Permission denied", возможно ключ не добавлен на VDS
|
||||
// Пробуем добавить ключ автоматически (если есть пароль)
|
||||
const errorMessage = (error.stderr || error.message || '').toLowerCase();
|
||||
if (errorMessage.includes('permission denied') || errorMessage.includes('publickey')) {
|
||||
logger.warn(`[VDS] Permission denied. Пробуем добавить публичный ключ на VDS...`);
|
||||
const keyAdded = await ensureSshKeyOnVds(settings);
|
||||
if (keyAdded) {
|
||||
// Пробуем подключиться снова
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(sshCommand, { maxBuffer: 10 * 1024 * 1024 });
|
||||
logger.success(`[VDS] Подключение успешно после добавления ключа`);
|
||||
return { code: 0, stdout, stderr };
|
||||
} catch (retryError) {
|
||||
logger.error(`[VDS] Ошибка SSH подключения после добавления ключа:`, retryError.message);
|
||||
logger.error(`[VDS] retryError.stdout: ${retryError.stdout || '(пусто)'}`);
|
||||
logger.error(`[VDS] retryError.stderr: ${retryError.stderr || '(пусто)'}`);
|
||||
}
|
||||
} else {
|
||||
logger.error(`[VDS] Не удалось добавить публичный ключ на VDS. Убедитесь, что пароль указан в настройках или выполните настройку VDS через webssh-agent.`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.error(`[VDS] Убедитесь, что публичный ключ из ${privateKeyPath}.pub добавлен в /root/.ssh/authorized_keys на VDS`);
|
||||
return { code: error.code || 1, stdout: error.stdout || '', stderr: error.stderr || error.message };
|
||||
}
|
||||
} else {
|
||||
// Если ключа нет, пробуем без явного указания (SSH сам найдет)
|
||||
logger.warn(`[VDS] SSH ключ не найден в ${privateKeyPath}, пробуем без явного указания`);
|
||||
const sshCommand = `ssh ${sshOptions} ${sshUser}@${sshHost} "${escapedCommand}"`;
|
||||
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(sshCommand, { maxBuffer: 10 * 1024 * 1024 });
|
||||
return { code: 0, stdout, stderr };
|
||||
} catch (error) {
|
||||
logger.error(`[VDS] Ошибка SSH подключения:`, error.message);
|
||||
logger.error(`[VDS] error.code: ${error.code || 'не указан'}`);
|
||||
logger.error(`[VDS] error.stdout: ${error.stdout || '(пусто)'}`);
|
||||
logger.error(`[VDS] error.stderr: ${error.stderr || '(пусто)'}`);
|
||||
return { code: error.code || 1, stdout: error.stdout || '', stderr: error.stderr || error.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1079,25 +1217,90 @@ router.post('/ssl/renew', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETT
|
||||
// Проверяем, используется ли Docker certbot
|
||||
const dockerUser = vdsSettings.dockerUser || 'docker';
|
||||
const domain = vdsSettings.domain || vdsSettings.sshHost;
|
||||
// Используем только Let's Encrypt (работает без аккаунта)
|
||||
const sslProvider = 'letsencrypt';
|
||||
|
||||
// Используем путь из настроек или значение по умолчанию на основе dockerUser
|
||||
let dappPath = vdsSettings.dappPath || `/home/${dockerUser}/dapp`;
|
||||
|
||||
// Проверяем существование пути и файла docker-compose.prod.yml
|
||||
const pathCheckResult = await execDockerCommand(`test -d ${dappPath} && test -f ${dappPath}/docker-compose.prod.yml && echo "exists" || echo "not_exists"`);
|
||||
if (pathCheckResult.stdout && pathCheckResult.stdout.includes('not_exists')) {
|
||||
logger.warn(`[VDS] Путь ${dappPath} или файл docker-compose.prod.yml не найден, ищем...`);
|
||||
|
||||
// Ищем docker-compose.prod.yml на VDS
|
||||
const findResult = await execDockerCommand(`find /home /root -name "docker-compose.prod.yml" -type f 2>/dev/null | head -1`);
|
||||
if (findResult.stdout && findResult.stdout.trim()) {
|
||||
const foundPath = findResult.stdout.trim().replace('/docker-compose.prod.yml', '');
|
||||
logger.info(`[VDS] Найден docker-compose.prod.yml в: ${foundPath}`);
|
||||
dappPath = foundPath;
|
||||
} else {
|
||||
logger.error(`[VDS] docker-compose.prod.yml не найден на VDS сервере`);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: `Путь ${dappPath} не существует или файл docker-compose.prod.yml не найден. Проверьте настройки VDS и укажите правильный путь.`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Используем только Let's Encrypt (работает без аккаунта)
|
||||
logger.info(`[VDS] Используем провайдер SSL: Let's Encrypt, путь: ${dappPath}`);
|
||||
|
||||
// Проверяем статус сертификата через 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`);
|
||||
const checkCommand = `cd ${dappPath} && docker compose -f docker-compose.prod.yml run --rm certbot certificates 2>&1 || certbot certificates 2>&1`;
|
||||
const checkResult = await execDockerCommand(checkCommand);
|
||||
|
||||
if (checkResult.code !== 0) {
|
||||
logger.warn('[VDS] Ошибка проверки сертификатов:', checkResult.stderr);
|
||||
}
|
||||
|
||||
let hasValidCert = false;
|
||||
if (checkResult.stdout && checkResult.stdout.includes(domain)) {
|
||||
const certLines = checkResult.stdout.split('\n');
|
||||
for (let i = 0; i < certLines.length; i++) {
|
||||
if (certLines[i].includes('Domains:') && certLines[i].includes(domain)) {
|
||||
for (let j = i + 1; j < Math.min(i + 10, certLines.length); j++) {
|
||||
if (certLines[j].includes('Expiry Date:')) {
|
||||
const expiryDateStr = certLines[j].split('Expiry Date:')[1]?.trim();
|
||||
if (expiryDateStr) {
|
||||
const expiryDate = new Date(expiryDateStr);
|
||||
const now = new Date();
|
||||
if (expiryDate > new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000)) {
|
||||
hasValidCert = true;
|
||||
logger.info(`[VDS] Найден действующий сертификат для ${domain}, истекает: ${expiryDateStr}`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Пытаемся обновить сертификат через Docker certbot
|
||||
logger.info('[VDS] Обновление SSL сертификата...');
|
||||
// Сначала пробуем renew --force-renewal для обновления существующего сертификата
|
||||
// Это не создает новый сертификат и не попадает под лимит Let's Encrypt
|
||||
let renewResult = await execDockerCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml run --rm certbot renew --force-renewal --non-interactive 2>&1 || certbot renew --force-renewal --non-interactive 2>&1`);
|
||||
// Сначала пробуем renew (без --force-renewal) для обновления существующего сертификата
|
||||
const renewCommand = `cd ${dappPath} && docker compose -f docker-compose.prod.yml run --rm certbot renew --non-interactive 2>&1 || docker-compose -f docker-compose.prod.yml run --rm certbot renew --non-interactive 2>&1 || certbot renew --non-interactive 2>&1`;
|
||||
let renewResult = await execDockerCommand(renewCommand);
|
||||
|
||||
if (hasValidCert && renewResult.code === 0) {
|
||||
logger.info('[VDS] Используем существующий валидный сертификат');
|
||||
const reloadResult = await execDockerCommand(`cd ${dappPath} && (docker compose -f docker-compose.prod.yml restart frontend-nginx 2>&1 || docker-compose -f docker-compose.prod.yml restart frontend-nginx 2>&1 || systemctl reload nginx 2>&1)`);
|
||||
logger.info('[VDS] SSL сертификат обновлен (renew)');
|
||||
return res.json({
|
||||
success: true,
|
||||
message: 'SSL сертификат обновлен (использован существующий)',
|
||||
output: renewResult.stdout,
|
||||
reloadOutput: reloadResult.stdout
|
||||
});
|
||||
}
|
||||
|
||||
// Если renew не сработал (сертификат не найден или другая ошибка), создаем новый
|
||||
if (renewResult.code !== 0 || renewResult.stdout.includes('No renewals were attempted') || renewResult.stdout.includes('No certs found')) {
|
||||
logger.info('[VDS] Renew не сработал, создаем новый сертификат...');
|
||||
if (!hasValidCert && (renewResult.code !== 0 || renewResult.stdout.includes('No renewals were attempted') || renewResult.stdout.includes('No certs found'))) {
|
||||
logger.info('[VDS] Renew не сработал и нет валидного сертификата, создаем новый...');
|
||||
// Удаляем только сертификаты с суффиксами, основной оставляем
|
||||
const certListResult = await execDockerCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml run --rm certbot certificates 2>&1 || certbot certificates 2>&1`);
|
||||
const certListResult = await execDockerCommand(checkCommand);
|
||||
if (certListResult.stdout) {
|
||||
const lines = certListResult.stdout.split('\n');
|
||||
const certNames = [];
|
||||
@@ -1113,21 +1316,45 @@ router.post('/ssl/renew', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETT
|
||||
// Удаляем только сертификаты с суффиксами
|
||||
for (const certName of certNames) {
|
||||
logger.info(`[VDS] Удаление старого сертификата с суффиксом: ${certName}`);
|
||||
await execDockerCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml run --rm certbot delete --cert-name ${certName} --non-interactive 2>&1 || true`);
|
||||
const deleteCommand = `cd ${dappPath} && docker compose -f docker-compose.prod.yml run --rm certbot delete --cert-name ${certName} --non-interactive 2>&1 || docker-compose -f docker-compose.prod.yml run --rm certbot delete --cert-name ${certName} --non-interactive 2>&1 || true`;
|
||||
await execDockerCommand(deleteCommand);
|
||||
}
|
||||
}
|
||||
// Создаем новый сертификат только если его нет
|
||||
const email = vdsSettings.email || 'admin@example.com';
|
||||
renewResult = await execDockerCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml run --rm certbot certonly --webroot --webroot-path=/var/www/certbot --email ${email} --agree-tos --no-eff-email --non-interactive -d ${domain} 2>&1 || certbot certonly --webroot --webroot-path=/var/www/certbot --email ${email} --agree-tos --no-eff-email --non-interactive -d ${domain} 2>&1`);
|
||||
const certCommand = `cd ${dappPath} && (docker compose -f docker-compose.prod.yml run --rm certbot certonly --webroot --webroot-path=/var/www/certbot --email ${email} --agree-tos --no-eff-email --non-interactive -d ${domain} 2>&1 || docker-compose -f docker-compose.prod.yml run --rm certbot certonly --webroot --webroot-path=/var/www/certbot --email ${email} --agree-tos --no-eff-email --non-interactive -d ${domain} 2>&1 || certbot certonly --webroot --webroot-path=/var/www/certbot --email ${email} --agree-tos --no-eff-email --non-interactive -d ${domain} 2>&1)`;
|
||||
logger.info(`[VDS] Команда создания сертификата: ${certCommand.substring(0, 300)}...`);
|
||||
renewResult = await execDockerCommand(certCommand);
|
||||
logger.info(`[VDS] Результат создания сертификата: code=${renewResult.code}, stdout длина=${renewResult.stdout?.length || 0}, stderr длина=${renewResult.stderr?.length || 0}`);
|
||||
|
||||
if (renewResult.code !== 0 && (renewResult.stderr || renewResult.stdout)) {
|
||||
const errorOutput = (renewResult.stderr || renewResult.stdout).toLowerCase();
|
||||
if (errorOutput.includes('too many certificates') || errorOutput.includes('rate limit')) {
|
||||
logger.error('[VDS] Превышен лимит Let\'s Encrypt для домена');
|
||||
return res.status(429).json({
|
||||
success: false,
|
||||
error: 'Превышен лимит Let\'s Encrypt: слишком много сертификатов выпущено за последние 7 дней. Пожалуйста, подождите до указанной даты или используйте существующий сертификат.',
|
||||
details: renewResult.stderr || renewResult.stdout,
|
||||
rateLimit: true
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (hasValidCert) {
|
||||
logger.info('[VDS] Используем существующий валидный сертификат (renew не требуется)');
|
||||
return res.json({
|
||||
success: true,
|
||||
message: 'Используется существующий валидный SSL сертификат',
|
||||
existingCert: true
|
||||
});
|
||||
}
|
||||
|
||||
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`);
|
||||
const reloadResult = await execDockerCommand(`cd ${dappPath} && (docker compose -f docker-compose.prod.yml restart frontend-nginx 2>&1 || docker-compose -f docker-compose.prod.yml restart frontend-nginx 2>&1 || systemctl reload nginx 2>&1)`);
|
||||
|
||||
// Очищаем старые сертификаты с суффиксами, чтобы они не накапливались
|
||||
logger.info('[VDS] Очистка старых сертификатов с суффиксами...');
|
||||
const certListAfter = await execDockerCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml run --rm certbot certificates 2>&1 || certbot certificates 2>&1`);
|
||||
const certListAfter = await execDockerCommand(checkCommand);
|
||||
if (certListAfter.stdout) {
|
||||
const lines = certListAfter.stdout.split('\n');
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
@@ -1136,7 +1363,8 @@ router.post('/ssl/renew', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETT
|
||||
// Удаляем сертификаты с суффиксами (например, hb3-accelerator.com-0001, hb3-accelerator.com-0002)
|
||||
if (certName && certName !== domain && certName.startsWith(domain + '-')) {
|
||||
logger.info(`[VDS] Удаление старого сертификата с суффиксом: ${certName}`);
|
||||
await execDockerCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml run --rm certbot delete --cert-name ${certName} --non-interactive 2>&1 || true`);
|
||||
const deleteCommand = `cd ${dappPath} && docker compose -f docker-compose.prod.yml run --rm certbot delete --cert-name ${certName} --non-interactive 2>&1 || docker-compose -f docker-compose.prod.yml run --rm certbot delete --cert-name ${certName} --non-interactive 2>&1 || true`;
|
||||
await execDockerCommand(deleteCommand);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1150,11 +1378,28 @@ router.post('/ssl/renew', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETT
|
||||
reloadOutput: reloadResult.stdout
|
||||
});
|
||||
} else {
|
||||
logger.error('[VDS] Ошибка обновления SSL сертификата:', renewResult.stderr);
|
||||
logger.error('[VDS] Ошибка обновления SSL сертификата:', renewResult.stderr || renewResult.stdout || 'Неизвестная ошибка');
|
||||
const errorDetails = renewResult.stderr || renewResult.stdout || 'Неизвестная ошибка';
|
||||
const errorMessage = `Command failed: ${errorDetails}`;
|
||||
const errorOutput = errorDetails.toLowerCase();
|
||||
if (errorOutput.includes('too many certificates') || errorOutput.includes('rate limit')) {
|
||||
return res.status(429).json({
|
||||
success: false,
|
||||
error: 'Превышен лимит Let\'s Encrypt: слишком много сертификатов выпущено за последние 7 дней. Пожалуйста, подождите до указанной даты или используйте существующий сертификат.',
|
||||
details: errorMessage,
|
||||
stdout: renewResult.stdout || '',
|
||||
stderr: renewResult.stderr || '',
|
||||
code: renewResult.code || 1,
|
||||
rateLimit: true
|
||||
});
|
||||
}
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Не удалось обновить SSL сертификат',
|
||||
details: renewResult.stderr
|
||||
details: errorMessage,
|
||||
stdout: renewResult.stdout || '',
|
||||
stderr: renewResult.stderr || '',
|
||||
code: renewResult.code || 1
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -1163,6 +1408,41 @@ router.post('/ssl/renew', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETT
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Проверить путь к docker-compose на VDS
|
||||
*/
|
||||
router.get('/check-dapp-path', 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 configuredPath = vdsSettings.dappPath || `/home/${dockerUser}/dapp`;
|
||||
|
||||
// Проверяем указанный путь
|
||||
const pathCheck = await execDockerCommand(`test -d ${configuredPath} && test -f ${configuredPath}/docker-compose.prod.yml && echo "exists" || echo "not_exists"`);
|
||||
|
||||
// Ищем docker-compose.prod.yml на VDS
|
||||
const findResult = await execDockerCommand(`find /home /root -name "docker-compose.prod.yml" -type f 2>/dev/null`);
|
||||
|
||||
const foundPaths = findResult.stdout ? findResult.stdout.trim().split('\n').filter(p => p).map(p => p.replace('/docker-compose.prod.yml', '')) : [];
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
configuredPath,
|
||||
configuredPathExists: pathCheck.stdout && pathCheck.stdout.includes('exists'),
|
||||
foundPaths: foundPaths,
|
||||
recommendedPath: foundPaths.length > 0 ? foundPaths[0] : null
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[VDS] Ошибка проверки пути:', error);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Получить статус SSL сертификата
|
||||
*/
|
||||
@@ -1177,8 +1457,33 @@ router.get('/ssl/status', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETT
|
||||
const dockerUser = vdsSettings.dockerUser || 'docker';
|
||||
const domain = vdsSettings.domain || vdsSettings.sshHost;
|
||||
|
||||
// Используем путь из настроек или значение по умолчанию (проверено: /home/docker/dapp)
|
||||
let dappPath = vdsSettings.dappPath || `/home/${dockerUser}/dapp`;
|
||||
|
||||
// Проверяем существование пути и файла docker-compose.prod.yml
|
||||
const pathCheckResult = await execDockerCommand(`test -d ${dappPath} && test -f ${dappPath}/docker-compose.prod.yml && echo "exists" || echo "not_exists"`);
|
||||
if (pathCheckResult.stdout && pathCheckResult.stdout.includes('not_exists')) {
|
||||
logger.warn(`[VDS] Путь ${dappPath} или файл docker-compose.prod.yml не найден, ищем...`);
|
||||
|
||||
// Ищем docker-compose.prod.yml на VDS
|
||||
const findResult = await execDockerCommand(`find /home /root -name "docker-compose.prod.yml" -type f 2>/dev/null | head -1`);
|
||||
if (findResult.stdout && findResult.stdout.trim()) {
|
||||
const foundPath = findResult.stdout.trim().replace('/docker-compose.prod.yml', '');
|
||||
logger.info(`[VDS] Найден docker-compose.prod.yml в: ${foundPath}`);
|
||||
dappPath = foundPath;
|
||||
} else {
|
||||
logger.error(`[VDS] docker-compose.prod.yml не найден на VDS сервере`);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: `Путь ${dappPath} не существует или файл docker-compose.prod.yml не найден. Проверьте настройки VDS и укажите правильный путь.`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Используем только Let's Encrypt (работает без аккаунта)
|
||||
// Проверяем статус сертификата через 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`);
|
||||
const checkCommand = `cd ${dappPath} && docker compose -f docker-compose.prod.yml run --rm certbot certificates 2>&1 || docker-compose -f docker-compose.prod.yml run --rm certbot certificates 2>&1 || certbot certificates 2>&1`;
|
||||
const checkResult = await execDockerCommand(checkCommand);
|
||||
|
||||
// Проверяем срок действия сертификата
|
||||
let certInfo = null;
|
||||
|
||||
Reference in New Issue
Block a user