Добавление и Удаление сайта

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:
2025-11-14 14:18:26 +07:00
parent 0ed6a6007d
commit 4b13923375
22 changed files with 1823 additions and 57 deletions

View 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)
}