Добавление и Удаление сайта
Backend (Go): - Добавлен полный функционал создания сайтов - Добавлен функционал удаления сайтов - Новые API методы в admin.go: - Добавлен шаблон стартовой страницы - Добавлена функция DecodeBase64 Исправления критических ошибок: - Исправлена работа wildcard алиасов (*.domain.com) в handler.go - Исправлены ошибки "файл не найден" при создании файлов Frontend (JavaScript + HTML + CSS): - Добавлена страница создания сайта - Добавлена кнопка "Удалить сайт" в редактировании - Мелкие доработки стилей Build: - Обновлён build_admin.ps1 - добавлен шаг генерации биндингов (wails generate module) Fixes: - #fix Wildcard алиасы (*.domain.com) теперь работают корректно - #fix Удалён порт из host при проверке алиасов - #fix Приоритет точных доменов над wildcard - #fix Ошибки "файл не найден" при создании сайтов/vAccess - #fix Секция добавления сайта теперь скрывается при навигации
This commit is contained in:
312
Backend/admin/go/sites/methods.go
Normal file
312
Backend/admin/go/sites/methods.go
Normal file
@@ -0,0 +1,312 @@
|
||||
package sites
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
config "vServer/Backend/config"
|
||||
tools "vServer/Backend/tools"
|
||||
)
|
||||
|
||||
//go:embed templates/index.tmpl
|
||||
var indexTemplate string
|
||||
|
||||
// CreateNewSite создаёт новый сайт со всей необходимой структурой
|
||||
func CreateNewSite(siteData SiteInfo) error {
|
||||
// 1. Валидация данных
|
||||
if err := ValidateSiteData(siteData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. Создание структуры папок
|
||||
if err := CreateSiteFolder(siteData.Host); err != nil {
|
||||
return fmt.Errorf("ошибка создания папок: %w", err)
|
||||
}
|
||||
|
||||
// 3. Создание стартового файла
|
||||
if err := CreateStarterFile(siteData.Host, siteData.RootFile); err != nil {
|
||||
return fmt.Errorf("ошибка создания стартового файла: %w", err)
|
||||
}
|
||||
|
||||
// 4. Создание пустого vAccess.conf
|
||||
if err := CreateVAccessFile(siteData.Host); err != nil {
|
||||
return fmt.Errorf("ошибка создания vAccess.conf: %w", err)
|
||||
}
|
||||
|
||||
// 5. Добавление сайта в конфиг
|
||||
if err := AddSiteToConfig(siteData); err != nil {
|
||||
return fmt.Errorf("ошибка добавления в конфиг: %w", err)
|
||||
}
|
||||
|
||||
tools.Logs_file(0, "SITES", fmt.Sprintf("✅ Новый сайт создан: %s (%s)", siteData.Name, siteData.Host), "logs_config.log", true)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateSiteData проверяет данные нового сайта
|
||||
func ValidateSiteData(siteData SiteInfo) error {
|
||||
// Проверка обязательных полей
|
||||
if strings.TrimSpace(siteData.Host) == "" {
|
||||
return errors.New("поле Host обязательно для заполнения")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(siteData.Name) == "" {
|
||||
return errors.New("поле Name обязательно для заполнения")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(siteData.RootFile) == "" {
|
||||
return errors.New("поле RootFile обязательно для заполнения")
|
||||
}
|
||||
|
||||
// Проверка уникальности host
|
||||
for _, site := range config.ConfigData.Site_www {
|
||||
if strings.EqualFold(site.Host, siteData.Host) {
|
||||
return fmt.Errorf("сайт с host '%s' уже существует", siteData.Host)
|
||||
}
|
||||
}
|
||||
|
||||
// Проверка валидности status
|
||||
if siteData.Status != "active" && siteData.Status != "inactive" {
|
||||
return errors.New("status должен быть 'active' или 'inactive'")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateSiteFolder создаёт структуру папок для нового сайта
|
||||
func CreateSiteFolder(host string) error {
|
||||
// Создаём путь: WebServer/www/{host}/public_www/
|
||||
folderPath := filepath.Join("WebServer", "www", host, "public_www")
|
||||
|
||||
absPath, err := tools.AbsPath(folderPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Создаём все необходимые папки
|
||||
if err := os.MkdirAll(absPath, 0755); err != nil {
|
||||
return fmt.Errorf("не удалось создать папку: %w", err)
|
||||
}
|
||||
|
||||
tools.Logs_file(0, "SITES", fmt.Sprintf("📁 Создана папка: %s", folderPath), "logs_config.log", false)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateStarterFile создаёт стартовый файл (index.html или index.php)
|
||||
func CreateStarterFile(host, rootFile string) error {
|
||||
filePath := filepath.Join("WebServer", "www", host, "public_www", rootFile)
|
||||
|
||||
// Получаем абсолютный путь БЕЗ проверки существования
|
||||
absPath, err := filepath.Abs(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка получения абсолютного пути: %w", err)
|
||||
}
|
||||
|
||||
// Генерируем контент из шаблона
|
||||
content := generateTemplate(host, rootFile)
|
||||
|
||||
// Записываем файл
|
||||
if err := os.WriteFile(absPath, []byte(content), 0644); err != nil {
|
||||
return fmt.Errorf("не удалось создать файл: %w", err)
|
||||
}
|
||||
|
||||
tools.Logs_file(0, "SITES", fmt.Sprintf("📄 Создан стартовый файл: %s", rootFile), "logs_config.log", false)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateVAccessFile создаёт пустой конфиг vAccess
|
||||
func CreateVAccessFile(host string) error {
|
||||
filePath := filepath.Join("WebServer", "www", host, "vAccess.conf")
|
||||
|
||||
// Получаем абсолютный путь БЕЗ проверки существования
|
||||
absPath, err := filepath.Abs(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка получения абсолютного пути: %w", err)
|
||||
}
|
||||
|
||||
content := `# vAccess Configuration
|
||||
# Правила применяются сверху вниз
|
||||
|
||||
# Пример правила (закомментировано):
|
||||
# type: Disable
|
||||
# type_file: *.php
|
||||
# path_access: /uploads/*
|
||||
# url_error: 404
|
||||
|
||||
`
|
||||
|
||||
if err := os.WriteFile(absPath, []byte(content), 0644); err != nil {
|
||||
return fmt.Errorf("не удалось создать vAccess.conf: %w", err)
|
||||
}
|
||||
|
||||
tools.Logs_file(0, "SITES", "🔒 Создан vAccess.conf", "logs_config.log", false)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddSiteToConfig добавляет новый сайт в config.json
|
||||
func AddSiteToConfig(siteData SiteInfo) error {
|
||||
// Создаём новую запись
|
||||
newSite := config.Site_www{
|
||||
Name: siteData.Name,
|
||||
Host: siteData.Host,
|
||||
Alias: siteData.Alias,
|
||||
Status: siteData.Status,
|
||||
Root_file: siteData.RootFile,
|
||||
Root_file_routing: siteData.RootFileRouting,
|
||||
}
|
||||
|
||||
// Добавляем в массив
|
||||
config.ConfigData.Site_www = append(config.ConfigData.Site_www, newSite)
|
||||
|
||||
// Сохраняем конфиг в файл
|
||||
if err := SaveConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tools.Logs_file(0, "SITES", "💾 Конфигурация обновлена", "logs_config.log", false)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveConfig сохраняет текущую конфигурацию в файл
|
||||
func SaveConfig() error {
|
||||
// Форматируем JSON с отступами
|
||||
jsonData, err := json.MarshalIndent(config.ConfigData, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка форматирования JSON: %w", err)
|
||||
}
|
||||
|
||||
// Получаем абсолютный путь к файлу конфига
|
||||
absPath, err := tools.AbsPath(config.ConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Записываем в файл
|
||||
if err := os.WriteFile(absPath, jsonData, 0644); err != nil {
|
||||
return fmt.Errorf("ошибка записи файла: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UploadSiteCertificate загружает SSL сертификат для сайта
|
||||
func UploadSiteCertificate(host, certType string, certData []byte) error {
|
||||
// Создаём папку для сертификатов
|
||||
certDir := filepath.Join("WebServer", "cert", host)
|
||||
|
||||
absCertDir, err := tools.AbsPath(certDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(absCertDir, 0755); err != nil {
|
||||
return fmt.Errorf("не удалось создать папку для сертификатов: %w", err)
|
||||
}
|
||||
|
||||
// Определяем имя файла
|
||||
var fileName string
|
||||
switch certType {
|
||||
case "certificate":
|
||||
fileName = "certificate.crt"
|
||||
case "privatekey":
|
||||
fileName = "private.key"
|
||||
case "cabundle":
|
||||
fileName = "ca_bundle.crt"
|
||||
default:
|
||||
return fmt.Errorf("неизвестный тип сертификата: %s", certType)
|
||||
}
|
||||
|
||||
// Путь к файлу сертификата
|
||||
certFilePath := filepath.Join(absCertDir, fileName)
|
||||
|
||||
// Записываем файл
|
||||
if err := os.WriteFile(certFilePath, certData, 0644); err != nil {
|
||||
return fmt.Errorf("не удалось сохранить сертификат: %w", err)
|
||||
}
|
||||
|
||||
tools.Logs_file(0, "SITES", fmt.Sprintf("🔒 Загружен сертификат: %s для %s", fileName, host), "logs_config.log", true)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteSiteCertificates удаляет сертификаты сайта
|
||||
func DeleteSiteCertificates(host string) error {
|
||||
certDir := filepath.Join("WebServer", "cert", host)
|
||||
|
||||
// Получаем абсолютный путь к папке сертификатов
|
||||
absCertDir, err := filepath.Abs(certDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка получения пути: %w", err)
|
||||
}
|
||||
|
||||
// Проверяем, существует ли папка
|
||||
if _, err := os.Stat(absCertDir); os.IsNotExist(err) {
|
||||
return nil // Папки нет - ничего удалять не нужно
|
||||
}
|
||||
|
||||
// Удаляем папку со всем содержимым
|
||||
if err := os.RemoveAll(absCertDir); err != nil {
|
||||
return fmt.Errorf("не удалось удалить папку сертификатов: %w", err)
|
||||
}
|
||||
|
||||
tools.Logs_file(0, "SITES", fmt.Sprintf("🗑️ Удалены сертификаты для: %s", host), "logs_config.log", true)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteSite полностью удаляет сайт
|
||||
func DeleteSite(host string) error {
|
||||
// 1. Проверяем, существует ли сайт в конфиге
|
||||
siteIndex := -1
|
||||
for i, site := range config.ConfigData.Site_www {
|
||||
if site.Host == host {
|
||||
siteIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if siteIndex == -1 {
|
||||
return fmt.Errorf("сайт с host '%s' не найден в конфигурации", host)
|
||||
}
|
||||
|
||||
// 2. Удаляем папку сайта
|
||||
siteDir := filepath.Join("WebServer", "www", host)
|
||||
absSiteDir, err := filepath.Abs(siteDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка получения пути: %w", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(absSiteDir); err == nil {
|
||||
if err := os.RemoveAll(absSiteDir); err != nil {
|
||||
return fmt.Errorf("не удалось удалить папку сайта: %w", err)
|
||||
}
|
||||
tools.Logs_file(0, "SITES", fmt.Sprintf("🗑️ Удалена папка сайта: %s", siteDir), "logs_config.log", false)
|
||||
}
|
||||
|
||||
// 3. Удаляем сертификаты
|
||||
if err := DeleteSiteCertificates(host); err != nil {
|
||||
// Логируем ошибку, но продолжаем удаление
|
||||
tools.Logs_file(1, "SITES", fmt.Sprintf("Ошибка удаления сертификатов: %v", err), "logs_config.log", false)
|
||||
}
|
||||
|
||||
// 4. Удаляем из конфига
|
||||
config.ConfigData.Site_www = append(
|
||||
config.ConfigData.Site_www[:siteIndex],
|
||||
config.ConfigData.Site_www[siteIndex+1:]...,
|
||||
)
|
||||
|
||||
// 5. Сохраняем конфиг
|
||||
if err := SaveConfig(); err != nil {
|
||||
return fmt.Errorf("ошибка сохранения конфигурации: %w", err)
|
||||
}
|
||||
|
||||
tools.Logs_file(0, "SITES", fmt.Sprintf("✅ Сайт '%s' полностью удалён", host), "logs_config.log", true)
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateTemplate генерирует шаблон для нового сайта
|
||||
func generateTemplate(host, rootFile string) string {
|
||||
// Для всех типов файлов используем один HTML шаблон
|
||||
return strings.ReplaceAll(indexTemplate, "{{.Host}}", host)
|
||||
}
|
||||
209
Backend/admin/go/sites/templates/index.tmpl
Normal file
209
Backend/admin/go/sites/templates/index.tmpl
Normal file
@@ -0,0 +1,209 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{.Host}} - Добро пожаловать!</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #1a252f 0%, #2c3e50 50%, #34495e 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.container {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
background: rgba(26, 37, 47, 0.95);
|
||||
border-radius: 16px;
|
||||
backdrop-filter: blur(15px);
|
||||
border: 1px solid rgba(52, 152, 219, 0.2);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(52, 152, 219, 0.1);
|
||||
max-width: 580px;
|
||||
animation: fadeIn 1s ease-in-out;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, #3498db, #2ecc71, #e74c3c, #f39c12);
|
||||
background-size: 400% 400%;
|
||||
animation: gradientShift 4s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes gradientShift {
|
||||
0%, 100% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 3.8rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 1.5rem;
|
||||
background: linear-gradient(135deg, #3498db, #2ecc71);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
letter-spacing: 3px;
|
||||
text-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.welcome-text {
|
||||
font-size: 1.9rem;
|
||||
margin-bottom: 1.5rem;
|
||||
font-weight: 400;
|
||||
color: #ecf0f1;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 1.15rem;
|
||||
margin-bottom: 2.5rem;
|
||||
line-height: 1.8;
|
||||
color: #bdc3c7;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
background: linear-gradient(135deg, rgba(46, 204, 113, 0.15), rgba(39, 174, 96, 0.25));
|
||||
padding: 15px 30px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(46, 204, 113, 0.4);
|
||||
margin-bottom: 1.5rem;
|
||||
box-shadow: 0 8px 16px rgba(46, 204, 113, 0.1);
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: #2ecc71;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s infinite;
|
||||
box-shadow: 0 0 10px rgba(46, 204, 113, 0.5);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(46, 204, 113, 0.7), 0 0 10px rgba(46, 204, 113, 0.5);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 10px rgba(46, 204, 113, 0), 0 0 10px rgba(46, 204, 113, 0.5);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(46, 204, 113, 0), 0 0 10px rgba(46, 204, 113, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.status-text {
|
||||
color: #ecf0f1;
|
||||
font-weight: 600;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 1.5rem;
|
||||
font-size: 0.95rem;
|
||||
opacity: 0.8;
|
||||
color: #95a5a6;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: #3498db;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.footer a:hover {
|
||||
color: #2ecc71;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.floating-particles {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.particle {
|
||||
position: absolute;
|
||||
background: rgba(52, 152, 219, 0.15);
|
||||
border-radius: 50%;
|
||||
animation: float 8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0px) rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-20px) rotate(180deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="floating-particles">
|
||||
<div class="particle" style="width: 18px; height: 18px; top: 15%; left: 10%; animation-delay: 0s;"></div>
|
||||
<div class="particle" style="width: 14px; height: 14px; top: 65%; left: 85%; animation-delay: 3s;"></div>
|
||||
<div class="particle" style="width: 10px; height: 10px; top: 85%; left: 20%; animation-delay: 6s;"></div>
|
||||
<div class="particle" style="width: 22px; height: 22px; top: 25%; left: 75%; animation-delay: 2s;"></div>
|
||||
<div class="particle" style="width: 12px; height: 12px; top: 75%; left: 50%; animation-delay: 4s;"></div>
|
||||
<div class="particle" style="width: 16px; height: 16px; top: 40%; left: 15%; animation-delay: 1s;"></div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="logo">{{.Host}}</div>
|
||||
<div class="welcome-text">Добро пожаловать!</div>
|
||||
<div class="description">
|
||||
Ваш сайт успешно создан и готов к работе.<br>
|
||||
Начните разработку, заменив этот файл своим контентом.
|
||||
</div>
|
||||
<div class="status">
|
||||
<div class="status-dot"></div>
|
||||
<span class="status-text">Сайт работает</span>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
Powered by vServer
|
||||
<br>
|
||||
Сайт разработчика: <a href="https://voxsel.ru" target="_blank">voxsel.ru</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user