ваше сообщение коммита
This commit is contained in:
@@ -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;
|
||||
|
||||
25
setup.sh
25
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 запущен
|
||||
|
||||
@@ -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 образов и данных...');
|
||||
|
||||
Reference in New Issue
Block a user