ваше сообщение коммита

This commit is contained in:
2025-11-26 18:51:50 +03:00
parent 9dfe264ed4
commit 6d158c3952
3 changed files with 217 additions and 12 deletions

View File

@@ -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;

View File

@@ -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 запущен

View File

@@ -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 образов и данных...');