@@ -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 = sshP assword;
settings . ssh _password = sshPassword ; // encryptedDb автоматически найдет ssh_p assword_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 . c ode || 1 , stdout : error . stdout || '' , stderr : error . stderr || error . message } ;
// Проверяем существование ключа и используем е г о явно
if ( fs . existsSync ( privateKeyPath ) ) {
// Проверяем права доступа к ключу
const keyStats = fs . statSync ( privateKeyPath ) ;
const keyMode = ( keyStats . m ode & 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 execDocker Command ( ` 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 execDocker Command( ` cd /home/ ${ dockerUser } /dapp && docker compose -f docker-compose.prod.yml run --rm certbot delete --cert-name ${ certName } --non-interactive 2>&1 || true ` ) ;
const delete Command = ` 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 execDock erCommand( ` 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 c ert Command = ` 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 execDocker Command( ` cd /home/ ${ dockerUser } /dapp && docker compose -f docker-compose.prod.yml run --rm certbot delete --cert-name ${ certName } --non-interactive 2>&1 || true ` ) ;
const delete Command = ` 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 execDocker Command ( ` 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 ;