diff --git a/frontend/src/views/VdsManagementView.vue b/frontend/src/views/VdsManagementView.vue index 0125b1e..0fc92f1 100644 --- a/frontend/src/views/VdsManagementView.vue +++ b/frontend/src/views/VdsManagementView.vue @@ -1125,6 +1125,75 @@ const sendBackup = async () => { } }; +// SSL Сертификаты +const loadSslStatus = async () => { + if (!isEditor.value) { + alert('Только пользователи с ролью "Редактор" могут проверять SSL сертификаты'); + return; + } + isLoadingSsl.value = true; + try { + const response = await axios.get('/vds/ssl/status'); + if (response.data.success) { + sslStatus.value = response.data; + } else { + alert('Ошибка получения статуса SSL сертификата'); + } + } catch (error) { + console.error('Ошибка получения статуса SSL:', error); + alert(error.response?.data?.error || 'Ошибка получения статуса SSL сертификата'); + } finally { + isLoadingSsl.value = false; + } +}; + +const renewSslCertificate = async () => { + if (!isEditor.value) { + alert('Только пользователи с ролью "Редактор" могут получать SSL сертификаты'); + return; + } + if (!confirm('Получить/обновить SSL сертификат от Let\'s Encrypt? Это может занять некоторое время.')) return; + isLoading.value = true; + try { + const response = await axios.post('/vds/ssl/renew'); + if (response.data.success) { + alert('SSL сертификат успешно получен/обновлен'); + await loadSslStatus(); + } else { + alert('Ошибка получения SSL сертификата: ' + (response.data.error || 'Неизвестная ошибка')); + } + } catch (error) { + console.error('Ошибка получения SSL сертификата:', error); + alert(error.response?.data?.error || 'Ошибка получения SSL сертификата'); + } finally { + isLoading.value = false; + } +}; + +const isCertExpiringSoon = (expiryDate) => { + if (!expiryDate) return false; + const expiry = new Date(expiryDate); + const now = new Date(); + const daysUntilExpiry = (expiry - now) / (1000 * 60 * 60 * 24); + return daysUntilExpiry < 30; // Истекает в течение 30 дней +}; + +const formatDate = (dateString) => { + if (!dateString) return 'Не указан'; + try { + const date = new Date(dateString); + return date.toLocaleString('ru-RU', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + } catch (error) { + return dateString; + } +}; + // Форматирование байтов const formatBytes = (bytes) => { if (!bytes || bytes === 0) return '0 B'; @@ -1292,6 +1361,7 @@ onMounted(async () => { // Загружаем пользователей только для редакторов if (isEditor.value) { await loadUsers(); + await loadSslStatus(); } // Обновляем статистику каждые 5 секунд @@ -1985,6 +2055,101 @@ onUnmounted(() => { background: #138496; } +/* Стили для SSL сертификатов */ +.ssl-section { + margin-bottom: 30px; + background: white; + border: 1px solid #e9ecef; + border-radius: var(--radius-lg, 12px); + padding: 24px; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); +} + +.ssl-status { + margin-bottom: 20px; + padding: 16px; + background: #f8f9fa; + border-radius: var(--radius-sm, 8px); + border: 1px solid #e9ecef; +} + +.ssl-info { + display: flex; + flex-direction: column; + gap: 12px; +} + +.ssl-info-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 0; + border-bottom: 1px solid #e9ecef; +} + +.ssl-info-item:last-child { + border-bottom: none; +} + +.ssl-info-item label { + font-weight: 600; + color: var(--color-primary); + min-width: 150px; +} + +.ssl-info-item span { + color: var(--color-dark, #333); + text-align: right; +} + +.ssl-info-item span.expiring-soon { + color: #dc3545; + font-weight: 600; +} + +.ssl-info-item span.self-signed { + color: #ff9800; + font-weight: 600; +} + +.ssl-no-cert { + text-align: center; + padding: 20px; + color: #856404; + background: #fff3cd; + border: 1px solid #ffc107; + border-radius: var(--radius-sm, 8px); +} + +.ssl-actions-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 12px; +} + +.ssl-btn { + padding: 12px 20px; + font-size: 0.95rem; +} + +.ssl-btn.renew { + background: #28a745; + color: white; +} + +.ssl-btn.renew:hover:not(:disabled) { + background: #218838; +} + +.ssl-btn.status { + background: #17a2b8; + color: white; +} + +.ssl-btn.status:hover:not(:disabled) { + background: #138496; +} + /* Модальные окна */ .modal-overlay { position: fixed; diff --git a/setup.sh b/setup.sh index 60aae4a..57b2600 100755 --- a/setup.sh +++ b/setup.sh @@ -45,22 +45,33 @@ check_curl() { print_green "✅ curl установлен" } +# Установка Docker +install_docker() { + print_blue "📦 Установка Docker..." + if curl -fsSL https://get.docker.com | bash; then + print_green "✅ Docker установлен" + systemctl enable docker 2>/dev/null || true + systemctl start docker 2>/dev/null || true + sleep 2 + else + print_red "❌ Ошибка установки Docker" + exit 1 + fi +} + # Проверка Docker check_docker() { print_blue "🔍 Проверка Docker..." if ! command -v docker &> /dev/null; then - print_red "❌ Docker не установлен!" - print_yellow "Установите Docker: https://docs.docker.com/get-docker/" - exit 1 + print_yellow "⚠️ Docker не установлен. Начинаем установку..." + install_docker fi if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then - print_red "❌ Docker Compose не установлен!" - print_yellow "Установите Docker Compose: https://docs.docker.com/compose/install/" - exit 1 + print_yellow "⚠️ Docker Compose не найден, но это нормально - используется встроенный docker compose" fi - print_green "✅ Docker и Docker Compose установлены" + print_green "✅ Docker установлен и готов к работе" } # Проверка Docker запущен diff --git a/webssh-agent/utils/dockerUtils.js b/webssh-agent/utils/dockerUtils.js index 1307d17..b833625 100644 --- a/webssh-agent/utils/dockerUtils.js +++ b/webssh-agent/utils/dockerUtils.js @@ -187,10 +187,22 @@ done # 🆕 Динамически определяем volumes для импорта из имен файлов в архиве echo "📦 Импорт данных в volumes..." +# Включаем nullglob для безопасной обработки пустых glob-паттернов +shopt -s nullglob +# Инициализируем переменные +volume_name="" +full_volume_name="" 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) + volume_name=$(basename "$data_file" .tar.gz 2>/dev/null || echo "") + + # Проверяем, что volume_name не пустой и не содержит только пробелы + if [ -z "${volume_name:-}" ] || [ -z "$(echo "${volume_name}" | tr -d '[:space:]')" ]; then + echo "⚠️ Предупреждение: не удалось извлечь имя volume из файла: $data_file" + volume_name="" + continue + fi # Используем префикс dapp_ для соответствия docker-compose.prod.yml full_volume_name="dapp_${volume_name}" @@ -199,11 +211,19 @@ for data_file in ./temp-import/*_data.tar.gz; do # Удаляем старый volume если существует docker volume rm -f "$full_volume_name" 2>/dev/null || true # Создаем новый volume - docker volume create "$full_volume_name" + if ! docker volume create "$full_volume_name"; then + echo "❌ Ошибка создания volume: $full_volume_name" + continue + fi # Импортируем данные - docker run --rm -v "$full_volume_name:/data" -v ./temp-import:/backup alpine tar xzf "/backup/$(basename $data_file)" -C /data + if ! docker run --rm -v "$full_volume_name:/data" -v "$(pwd)/temp-import:/backup" alpine tar xzf "/backup/$(basename "$data_file")" -C /data; then + echo "❌ Ошибка импорта данных в volume: $full_volume_name" + continue + fi + echo "✅ Данные успешно импортированы в volume: $full_volume_name" fi done +shopt -u nullglob # Очищаем временные файлы rm -rf ./temp-import @@ -214,8 +234,17 @@ docker images | grep -E "digital_legal_entitydle|postgres" echo "📋 Доступные volumes:" docker volume ls | grep dapp_`; - await execSshCommand(`echo '${importScript}' | tee /home/${dockerUser}/dapp/import-images-and-data.sh`, options); - await execSshCommand(`chmod +x /home/${dockerUser}/dapp/import-images-and-data.sh`, options); + // Записываем скрипт в файл локально и передаем через SCP для избежания проблем с экранированием + const tempScriptPath = `/tmp/import-images-and-data-${Date.now()}.sh`; + await fs.writeFile(tempScriptPath, importScript, { mode: 0o755 }); + + try { + await execScpCommand(tempScriptPath, `/home/${dockerUser}/dapp/import-images-and-data.sh`, options); + await execSshCommand(`chmod +x /home/${dockerUser}/dapp/import-images-and-data.sh`, options); + } finally { + // Удаляем временный файл + await fs.remove(tempScriptPath).catch(() => {}); + } // Импортируем образы и данные log.info('Импорт Docker образов и данных...');