Улучшение фронта
This commit is contained in:
@@ -39,7 +39,6 @@ func StartHTTPS() {
|
|||||||
serverName := chi.ServerName
|
serverName := chi.ServerName
|
||||||
|
|
||||||
if serverName == "" {
|
if serverName == "" {
|
||||||
tools.Logs_file(1, "HTTPS", "⚠️ Подключение без SNI (возможно по IP)", "logs_https.log", false)
|
|
||||||
|
|
||||||
} else if cert, ok := certMap[serverName]; ok {
|
} else if cert, ok := certMap[serverName]; ok {
|
||||||
// Найден точный сертификат для домена
|
// Найден точный сертификат для домена
|
||||||
@@ -59,7 +58,6 @@ func StartHTTPS() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if fallbackCert != nil {
|
if fallbackCert != nil {
|
||||||
tools.Logs_file(1, "HTTPS", "⚠️ Используем fallback-сертификат", "logs_https.log", false)
|
|
||||||
return fallbackCert, nil
|
return fallbackCert, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -377,7 +377,6 @@ func PHPHandler(w http.ResponseWriter, r *http.Request, host string, originalURI
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tools.Logs_file(0, "PHP", fmt.Sprintf("✅ FastCGI обработал: %s (порт %d)", phpPath, port), "logs_php.log", false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Streaming чтение FastCGI ответа с поддержкой SSE и chunked transfer
|
// Streaming чтение FastCGI ответа с поддержкой SSE и chunked transfer
|
||||||
|
|||||||
@@ -28,16 +28,9 @@ func isWebSocketRequest(r *http.Request) bool {
|
|||||||
|
|
||||||
// Проксирует WebSocket соединение
|
// Проксирует WebSocket соединение
|
||||||
func handleWebSocketProxy(w http.ResponseWriter, r *http.Request, proxyConfig config.Proxy_Service) bool {
|
func handleWebSocketProxy(w http.ResponseWriter, r *http.Request, proxyConfig config.Proxy_Service) bool {
|
||||||
// Определяем протокол для локального соединения
|
|
||||||
protocol := "ws"
|
|
||||||
networkProtocol := "tcp"
|
networkProtocol := "tcp"
|
||||||
if proxyConfig.ServiceHTTPSuse {
|
|
||||||
protocol = "wss"
|
|
||||||
}
|
|
||||||
|
|
||||||
targetAddr := proxyConfig.LocalAddress + ":" + proxyConfig.LocalPort
|
targetAddr := proxyConfig.LocalAddress + ":" + proxyConfig.LocalPort
|
||||||
|
|
||||||
tools.Logs_file(0, "WS-PROXY", "🔌 WebSocket: "+r.RemoteAddr+" → "+protocol+"://"+targetAddr+r.URL.Path, "logs_proxy.log", false)
|
|
||||||
|
|
||||||
// Захватываем клиентское соединение через Hijacker
|
// Захватываем клиентское соединение через Hijacker
|
||||||
hijacker, ok := w.(http.Hijacker)
|
hijacker, ok := w.(http.Hijacker)
|
||||||
@@ -135,7 +128,6 @@ func handleWebSocketProxy(w http.ResponseWriter, r *http.Request, proxyConfig co
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
tools.Logs_file(0, "WS-PROXY", "✅ WebSocket установлен: "+r.Host+r.URL.Path, "logs_proxy.log", false)
|
|
||||||
|
|
||||||
// Двунаправленное проксирование данных
|
// Двунаправленное проксирование данных
|
||||||
done := make(chan struct{}, 2)
|
done := make(chan struct{}, 2)
|
||||||
@@ -171,7 +163,6 @@ func handleWebSocketProxy(w http.ResponseWriter, r *http.Request, proxyConfig co
|
|||||||
// Ждём завершения одного из направлений
|
// Ждём завершения одного из направлений
|
||||||
<-done
|
<-done
|
||||||
|
|
||||||
tools.Logs_file(0, "WS-PROXY", "🔌 WebSocket закрыт: "+r.Host+r.URL.Path, "logs_proxy.log", false)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -388,7 +388,7 @@ func checkRules(rules []VAccessRule, requestPath string, r *http.Request, checkF
|
|||||||
if errorPage == "" {
|
if errorPage == "" {
|
||||||
errorPage = "404"
|
errorPage = "404"
|
||||||
}
|
}
|
||||||
tools.Logs_file(1, logPrefix, "🚫 Доступ запрещён для "+getClientIP(r)+" к "+requestPath, logFile, false)
|
tools.Logs_file(1, logPrefix, "🚫 Доступ запрещён для "+getClientIP(r)+" к "+r.Host+requestPath, logFile, false)
|
||||||
return false, errorPage
|
return false, errorPage
|
||||||
}
|
}
|
||||||
// Все условия Allow выполнены - разрешаем доступ
|
// Все условия Allow выполнены - разрешаем доступ
|
||||||
@@ -413,7 +413,7 @@ func checkRules(rules []VAccessRule, requestPath string, r *http.Request, checkF
|
|||||||
if errorPage == "" {
|
if errorPage == "" {
|
||||||
errorPage = "404"
|
errorPage = "404"
|
||||||
}
|
}
|
||||||
tools.Logs_file(1, logPrefix, "🚫 Доступ запрещён для "+getClientIP(r)+" к "+requestPath, logFile, false)
|
tools.Logs_file(1, logPrefix, "🚫 Доступ запрещён для "+getClientIP(r)+" к "+r.Host+requestPath, logFile, false)
|
||||||
return false, errorPage
|
return false, errorPage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ func CreateNewSite(siteData SiteInfo) error {
|
|||||||
return fmt.Errorf("ошибка добавления в конфиг: %w", err)
|
return fmt.Errorf("ошибка добавления в конфиг: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tools.Logs_file(0, "SITES", fmt.Sprintf("✅ Новый сайт создан: %s (%s)", siteData.Name, siteData.Host), "logs_config.log", true)
|
tools.Logs_file(0, "SITES", fmt.Sprintf("✅ Новый сайт создан: %s (%s)", siteData.Name, siteData.Host), "logs_error.log", true)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ func CreateSiteFolder(host string) error {
|
|||||||
return fmt.Errorf("не удалось создать папку: %w", err)
|
return fmt.Errorf("не удалось создать папку: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tools.Logs_file(0, "SITES", fmt.Sprintf("📁 Создана папка: %s", folderPath), "logs_config.log", false)
|
tools.Logs_file(0, "SITES", fmt.Sprintf("📁 Создана папка: %s", folderPath), "logs_error.log", false)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ func CreateStarterFile(host, rootFile string) error {
|
|||||||
return fmt.Errorf("не удалось создать файл: %w", err)
|
return fmt.Errorf("не удалось создать файл: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tools.Logs_file(0, "SITES", fmt.Sprintf("📄 Создан стартовый файл: %s", rootFile), "logs_config.log", false)
|
tools.Logs_file(0, "SITES", fmt.Sprintf("📄 Создан стартовый файл: %s", rootFile), "logs_error.log", false)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ func CreateVAccessFile(host string) error {
|
|||||||
return fmt.Errorf("не удалось создать vAccess.conf: %w", err)
|
return fmt.Errorf("не удалось создать vAccess.conf: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tools.Logs_file(0, "SITES", "🔒 Создан vAccess.conf", "logs_config.log", false)
|
tools.Logs_file(0, "SITES", "🔒 Создан vAccess.conf", "logs_error.log", false)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +166,7 @@ func AddSiteToConfig(siteData SiteInfo) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
tools.Logs_file(0, "SITES", "💾 Конфигурация обновлена", "logs_config.log", false)
|
tools.Logs_file(0, "SITES", "💾 Конфигурация обновлена", "logs_error.log", false)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +227,7 @@ func UploadSiteCertificate(host, certType string, certData []byte) error {
|
|||||||
return fmt.Errorf("не удалось сохранить сертификат: %w", err)
|
return fmt.Errorf("не удалось сохранить сертификат: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tools.Logs_file(0, "SITES", fmt.Sprintf("🔒 Загружен сертификат: %s для %s", fileName, host), "logs_config.log", true)
|
tools.Logs_file(0, "SITES", fmt.Sprintf("🔒 Загружен сертификат: %s для %s", fileName, host), "logs_error.log", true)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,7 +251,7 @@ func DeleteSiteCertificates(host string) error {
|
|||||||
return fmt.Errorf("не удалось удалить папку сертификатов: %w", err)
|
return fmt.Errorf("не удалось удалить папку сертификатов: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tools.Logs_file(0, "SITES", fmt.Sprintf("🗑️ Удалены сертификаты для: %s", host), "logs_config.log", true)
|
tools.Logs_file(0, "SITES", fmt.Sprintf("🗑️ Удалены сертификаты для: %s", host), "logs_error.log", true)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,13 +281,13 @@ func DeleteSite(host string) error {
|
|||||||
if err := os.RemoveAll(absSiteDir); err != nil {
|
if err := os.RemoveAll(absSiteDir); err != nil {
|
||||||
return fmt.Errorf("не удалось удалить папку сайта: %w", err)
|
return fmt.Errorf("не удалось удалить папку сайта: %w", err)
|
||||||
}
|
}
|
||||||
tools.Logs_file(0, "SITES", fmt.Sprintf("🗑️ Удалена папка сайта: %s", siteDir), "logs_config.log", false)
|
tools.Logs_file(0, "SITES", fmt.Sprintf("🗑️ Удалена папка сайта: %s", siteDir), "logs_error.log", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Удаляем сертификаты
|
// 3. Удаляем сертификаты
|
||||||
if err := DeleteSiteCertificates(host); err != nil {
|
if err := DeleteSiteCertificates(host); err != nil {
|
||||||
// Логируем ошибку, но продолжаем удаление
|
// Логируем ошибку, но продолжаем удаление
|
||||||
tools.Logs_file(1, "SITES", fmt.Sprintf("Ошибка удаления сертификатов: %v", err), "logs_config.log", false)
|
tools.Logs_file(1, "SITES", fmt.Sprintf("Ошибка удаления сертификатов: %v", err), "logs_error.log", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Удаляем из конфига
|
// 4. Удаляем из конфига
|
||||||
@@ -301,7 +301,7 @@ func DeleteSite(host string) error {
|
|||||||
return fmt.Errorf("ошибка сохранения конфигурации: %w", err)
|
return fmt.Errorf("ошибка сохранения конфигурации: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tools.Logs_file(0, "SITES", fmt.Sprintf("✅ Сайт '%s' полностью удалён", host), "logs_config.log", true)
|
tools.Logs_file(0, "SITES", fmt.Sprintf("✅ Сайт '%s' полностью удалён", host), "logs_error.log", true)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,16 +49,13 @@ func LoadConfig() {
|
|||||||
data, err := os.ReadFile(ConfigPath)
|
data, err := os.ReadFile(ConfigPath)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tools.Logs_file(0, "JSON", "Ошибка загрузки конфигурационного файла", "logs_config.log", true)
|
tools.Logs_file(1, "JSON", "Ошибка загрузки конфигурационного файла", "logs_error.log", true)
|
||||||
} else {
|
return
|
||||||
tools.Logs_file(0, "JSON", "config.json успешно загружен", "logs_config.log", true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.Unmarshal(data, &ConfigData)
|
err = json.Unmarshal(data, &ConfigData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tools.Logs_file(0, "JSON", "Ошибка парсинга конфигурационного файла", "logs_config.log", true)
|
tools.Logs_file(1, "JSON", "Ошибка парсинга конфигурационного файла", "logs_error.log", true)
|
||||||
} else {
|
|
||||||
tools.Logs_file(0, "JSON", "config.json успешно прочитан", "logs_config.log", true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Миграция: добавляем новые поля если их нет
|
// Миграция: добавляем новые поля если их нет
|
||||||
@@ -116,7 +113,7 @@ func migrateConfig(originalData []byte) {
|
|||||||
|
|
||||||
// Если нужно обновить - сохраняем конфиг с новыми полями
|
// Если нужно обновить - сохраняем конфиг с новыми полями
|
||||||
if needsSave {
|
if needsSave {
|
||||||
tools.Logs_file(0, "JSON", "🔄 Миграция конфига: добавляем новые поля", "logs_config.log", true)
|
tools.Logs_file(0, "JSON", "🔄 Миграция конфига: добавляем новые поля", "logs_error.log", true)
|
||||||
saveConfig()
|
saveConfig()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,15 +122,15 @@ func migrateConfig(originalData []byte) {
|
|||||||
func saveConfig() {
|
func saveConfig() {
|
||||||
formattedJSON, err := json.MarshalIndent(ConfigData, "", " ")
|
formattedJSON, err := json.MarshalIndent(ConfigData, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tools.Logs_file(1, "JSON", "Ошибка форматирования конфига: "+err.Error(), "logs_config.log", true)
|
tools.Logs_file(1, "JSON", "Ошибка форматирования конфига: "+err.Error(), "logs_error.log", true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.WriteFile(ConfigPath, formattedJSON, 0644)
|
err = os.WriteFile(ConfigPath, formattedJSON, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tools.Logs_file(1, "JSON", "Ошибка сохранения конфига: "+err.Error(), "logs_config.log", true)
|
tools.Logs_file(1, "JSON", "Ошибка сохранения конфига: "+err.Error(), "logs_error.log", true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tools.Logs_file(0, "JSON", "✅ Конфиг обновлён с новыми полями", "logs_config.log", true)
|
tools.Logs_file(0, "JSON", "✅ Конфиг обновлён с новыми полями", "logs_error.log", true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,12 +82,23 @@ func Logs_file(type_log int, service string, message string, log_file string, co
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Открываем файл для дозаписи, создаём если нет, права на запись.
|
// Открываем файл для дозаписи, создаём если нет, права на запись.
|
||||||
file, err := os.OpenFile(logsDir+"/"+log_files, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
filePath := logsDir + "/" + log_files
|
||||||
|
isNew := false
|
||||||
|
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||||
|
isNew = true
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
|
// UTF-8 BOM для новых файлов (чтобы Windows корректно читал)
|
||||||
|
if isNew {
|
||||||
|
file.Write([]byte{0xEF, 0xBB, 0xBF})
|
||||||
|
}
|
||||||
|
|
||||||
// Пишем строку в файл
|
// Пишем строку в файл
|
||||||
if _, err := file.WriteString(text); err != nil {
|
if _, err := file.WriteString(text); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
|||||||
340
README.md
340
README.md
@@ -1,9 +1,9 @@
|
|||||||
# 🚀 vServer - Функциональный веб-сервер на Go
|
# 🚀 vServer - Функциональный веб-сервер на Go
|
||||||
**🇺🇸 [English version](README_EN.md)**
|
**🇺🇸 [English version](README_EN.md)**
|
||||||
> Функциональный веб-сервер с поддержкой HTTP/HTTPS, MySQL, PHP и GUI-админкой
|
> Функциональный веб-сервер с поддержкой HTTP/HTTPS, MySQL, PHP, Let's Encrypt и GUI-админкой
|
||||||
|
|
||||||
**👨💻 Автор:** Суманеев Роман
|
**👨💻 Автор:** Суманеев Роман
|
||||||
**🌐 Сайт:** [voxsel.ru](https://voxsel.ru)
|
**🌐 Сайт:** [vserf.ru](https://vserf.ru)
|
||||||
**📞 Контакт:** [VK](https://vk.com/felias)
|
**📞 Контакт:** [VK](https://vk.com/felias)
|
||||||
|
|
||||||
## 🎯 Возможности
|
## 🎯 Возможности
|
||||||
@@ -13,287 +13,119 @@
|
|||||||
|
|
||||||
### 🌐 Веб-сервер
|
### 🌐 Веб-сервер
|
||||||
- ✅ **HTTP/HTTPS** сервер с поддержкой SSL сертификатов
|
- ✅ **HTTP/HTTPS** сервер с поддержкой SSL сертификатов
|
||||||
- ✅ **Proxy сервер** для проксирования запросов
|
- ✅ **Let's Encrypt** — автоматический выпуск и обновление SSL
|
||||||
- ✅ **PHP сервер** со встроенной поддержкой PHP 8
|
- ✅ **Proxy сервер** — обратный прокси для локальных сервисов
|
||||||
- ✅ **Статический контент** для размещения веб-сайтов
|
- ✅ **PHP 8** — встроенная поддержка (FastCGI пул)
|
||||||
- ✅ **vAccess** - система контроля доступа для сайтов и прокси
|
- ✅ **MySQL** — встроенный сервер базы данных
|
||||||
|
- ✅ **vAccess** — система контроля доступа
|
||||||
|
- ✅ **Wildcard** — поддержка wildcard-алиасов и сертификатов
|
||||||
|
|
||||||
### 🗄️ База данных
|
### 🎛️ GUI Админка
|
||||||
- ✅ **MySQL сервер** с полной поддержкой
|
- ✅ **Управление сервисами** — запуск/остановка HTTP, HTTPS, MySQL, PHP, Proxy
|
||||||
|
- ✅ **Управление сайтами** — создание, редактирование, удаление
|
||||||
### 🔧 Администрирование
|
- ✅ **Управление прокси** — визуальная настройка reverse proxy
|
||||||
- ✅ **GUI Админка** - Wails desktop приложение с современным интерфейсом
|
- ✅ **SSL менеджер** — выпуск, перевыпуск, загрузка сертификатов
|
||||||
- ✅ **Управление сервисами** - запуск/остановка HTTP, HTTPS, MySQL, PHP, Proxy
|
- ✅ **vAccess редактор** — настройка правил доступа с drag-and-drop
|
||||||
- ✅ **Редактор сайтов и прокси** - визуальное управление конфигурацией
|
- ✅ **Настройки** — конфигурация портов MySQL, PHP, прокси, ACME
|
||||||
- ✅ **vAccess редактор** - настройка правил доступа через интерфейс
|
- ✅ **Тёмная/Светлая тема** и **RU/EN** локализация
|
||||||
|
|
||||||
## 🏗️ Архитектура
|
## 🏗️ Архитектура
|
||||||
|
|
||||||
```
|
```
|
||||||
vServer/
|
vServer/
|
||||||
├── 🎯 main.go # Точка входа основного сервера
|
├── 🎯 main.go # Точка входа (Wails)
|
||||||
│
|
│
|
||||||
├── 🔧 Backend/ # Основная логика
|
├── 🔧 Backend/ # Основная логика
|
||||||
│ │
|
│ ├── admin/
|
||||||
│ ├── admin/ # | 🎛️ GUI Админка (Wails) |
|
│ │ ├── go/ # Go backend админки
|
||||||
│ │ ├── go/ # | Go backend для админки |
|
│ │ └── API.md # Документация API
|
||||||
│ │ └── frontend/ # | Современный UI |
|
│ ├── config/ # Конфигурация Go
|
||||||
│ │
|
│ ├── tools/ # Утилиты и хелперы
|
||||||
│ ├── config/ # | 🔧 Конфигурационные файлы Go |
|
│ └── WebServer/ # Модули веб-сервера
|
||||||
│ ├── tools/ # | 🛠️ Утилиты и хелперы |
|
│ └── acme/ # Let's Encrypt
|
||||||
│ └── WebServer/ # | 🌐 Модули веб-сервера |
|
|
||||||
│
|
│
|
||||||
├── 🌐 WebServer/ # Веб-контент и конфигурация
|
├── 🖥️ front_vue/ # Vue 3 фронтенд админки
|
||||||
│ │
|
│ └── src/
|
||||||
│ ├── cert/ # | 🔐 SSL сертификаты |
|
│ ├── Core/ # API, stores, i18n, router
|
||||||
│ ├── soft/ # | 📦 MySQL и PHP |
|
│ └── Design/ # Компоненты, views, стили
|
||||||
│ ├── tools/ # | 📊 Логи и инструменты |
|
|
||||||
│ └── www/ # | 🌍 Веб-контент |
|
|
||||||
│
|
│
|
||||||
├── 📄 go.mod # Go модули
|
├── 🌐 WebServer/ # Рабочие файлы сервера
|
||||||
├── 🔨 build_admin.ps1 # Сборка GUI админки
|
│ ├── config.json # Конфигурация
|
||||||
└── 🚀 vSerf.exe # GUI админка (после сборки)
|
│ ├── cert/ # SSL сертификаты
|
||||||
|
│ ├── soft/ # MySQL и PHP
|
||||||
|
│ ├── tools/ # Логи, error page, vAccess
|
||||||
|
│ └── www/ # Веб-контент (сайты)
|
||||||
|
│
|
||||||
|
├── 🔨 build_admin.ps1 # Скрипт сборки
|
||||||
|
└── 🚀 vSerf.exe # Готовое приложение
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🚀 Установка и запуск
|
## 🚀 Установка и запуск
|
||||||
|
|
||||||
### 🔨 Сборка основного сервера
|
### Для пользователей
|
||||||
|
|
||||||
|
1. Скачайте последний [релиз](https://github.com/AiVoxel/vServer/releases)
|
||||||
|
2. Распакуйте архив `WebServer/soft/soft.rar` в папку `WebServer/soft/`
|
||||||
|
3. Запустите `vSerf.exe` — откроется GUI админка
|
||||||
|
4. Сервер стартует автоматически, управляйте через интерфейс
|
||||||
|
|
||||||
|
> 🔑 **Пароль MySQL по умолчанию:** `root`
|
||||||
|
|
||||||
|
### Для разработчиков
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
|
# Сборка (проверит/установит Go, Node.js, Wails)
|
||||||
./build_admin.ps1
|
./build_admin.ps1
|
||||||
```
|
```
|
||||||
|
|
||||||
Скрипт автоматически:
|
Скрипт автоматически:
|
||||||
- Проверит/создаст `go.mod`
|
- Проверит зависимости (Go, Node.js, npm) — предложит установить через `winget`
|
||||||
- Установит зависимости (`go mod tidy`)
|
- Установит Go модули и Wails CLI
|
||||||
- Проверит/установит Wails CLI
|
- Соберёт Vue фронтенд
|
||||||
- Соберёт приложение → `vSerf.exe`
|
- Скомпилирует → `vSerf.exe`
|
||||||
|
|
||||||
### 📦 Подготовка компонентов
|
## 🔒 vAccess — Контроль доступа
|
||||||
1. Распакуйте архив `WebServer/soft/soft.rar` в папку `WebServer/soft/`
|
|
||||||
2. Запустите `vServer.exe` - основной сервер
|
|
||||||
3. Запустите `vSerf.exe` - GUI админка для управления
|
|
||||||
|
|
||||||
> 🔑 **Важно:** Пароль MySQL по умолчанию - `root`
|
Гибкая система правил для сайтов и прокси. Настраивается через GUI админку (раздел vAccess).
|
||||||
|
|
||||||
### 📦 Готовый проект для пользователя
|
**Возможности:**
|
||||||
Для работы необходимы:
|
- IP-фильтрация — разрешение/блокировка по IP
|
||||||
- 📄 `vSerf.exe` - GUI админка (опционально)
|
- Контроль путей — ограничение доступа к директориям
|
||||||
- 📁 `WebServer/` - конфигурация и ресурсы
|
- Фильтрация файлов — блокировка по расширениям
|
||||||
|
- Исключения — пути, к которым правило не применяется
|
||||||
> 💡 Папка `Backend/` и файлы `go.mod`, `main.go` нужны только для разработки
|
- Кастомные ошибки — редиректы или страницы ошибок
|
||||||
|
|
||||||
## ⚙️ Конфигурация
|
|
||||||
|
|
||||||
Настройка через `WebServer/config.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"Site_www": [
|
|
||||||
{
|
|
||||||
"name": "Локальный сайт",
|
|
||||||
"host": "127.0.0.1",
|
|
||||||
"alias": ["localhost"],
|
|
||||||
"status": "active",
|
|
||||||
"root_file": "index.html",
|
|
||||||
"root_file_routing": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Proxy_Service": [
|
|
||||||
{
|
|
||||||
"Enable": true,
|
|
||||||
"ExternalDomain": "git.example.ru",
|
|
||||||
"LocalAddress": "127.0.0.1",
|
|
||||||
"LocalPort": "3333",
|
|
||||||
"ServiceHTTPSuse": false,
|
|
||||||
"AutoHTTPS": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Soft_Settings": {
|
|
||||||
"mysql_port": 3306, "mysql_host": "127.0.0.1",
|
|
||||||
"php_port": 8000, "php_host": "localhost",
|
|
||||||
"proxy_enabled": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Основные параметры:**
|
|
||||||
- `Site_www` - настройки веб-сайтов
|
|
||||||
- `Proxy_Service` - конфигурация прокси-сервисов
|
|
||||||
- `Soft_Settings` - порты и хосты сервисов (MySQL, PHP, proxy_enabled)
|
|
||||||
|
|
||||||
### 🌐 Alias с поддержкой Wildcard
|
|
||||||
|
|
||||||
Для сайтов поддерживается wildcard (`*`) в алиасах:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"alias": [
|
|
||||||
"*.test.ru", // Все поддомены voxsel.ru
|
|
||||||
"*.test.com", // Все поддомены voxsel.com
|
|
||||||
"test.com", // Точное совпадение
|
|
||||||
"api.*" // api с любой зоной
|
|
||||||
],
|
|
||||||
"host": "test.ru"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Примеры работы wildcard:**
|
|
||||||
- `*.example.com` → `api.example.com`, `admin.example.com`, `test.example.com` ✅
|
|
||||||
- `example.*` → `example.com`, `example.ru`, `example.org` ✅
|
|
||||||
- `*example.com` → `test-example.com`, `my-example.com` ✅
|
|
||||||
- `*` → любой домен ✅ (осторожно!)
|
|
||||||
- `example.com` → только `example.com` ✅ (без wildcard)
|
|
||||||
|
|
||||||
### 🔄 Прокси-сервер
|
|
||||||
|
|
||||||
Прокси-сервер позволяет перенаправлять внешние запросы на локальные сервисы.
|
|
||||||
|
|
||||||
**Параметры Proxy_Service:**
|
|
||||||
- `Enable` - включить/отключить прокси (true/false)
|
|
||||||
- `ExternalDomain` - внешний домен для перехвата запросов
|
|
||||||
- `LocalAddress` - локальный адрес сервиса
|
|
||||||
- `LocalPort` - порт локального сервиса
|
|
||||||
- `ServiceHTTPSuse` - использовать HTTPS для подключения к локальному сервису (true/false)
|
|
||||||
- `AutoHTTPS` - автоматически перенаправлять HTTP → HTTPS (true/false)
|
|
||||||
|
|
||||||
**Пример множественных прокси:**
|
|
||||||
```json
|
|
||||||
"Proxy_Service": [
|
|
||||||
{
|
|
||||||
"Enable": true,
|
|
||||||
"ExternalDomain": "git.example.com",
|
|
||||||
"LocalAddress": "127.0.0.1",
|
|
||||||
"LocalPort": "3000",
|
|
||||||
"ServiceHTTPSuse": false,
|
|
||||||
"AutoHTTPS": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Enable": false,
|
|
||||||
"ExternalDomain": "api.example.com",
|
|
||||||
"LocalAddress": "127.0.0.1",
|
|
||||||
"LocalPort": "8080",
|
|
||||||
"ServiceHTTPSuse": false,
|
|
||||||
"AutoHTTPS": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 📖 Подробное описание параметров:
|
|
||||||
|
|
||||||
**`ServiceHTTPSuse`** - протокол подключения к локальному сервису:
|
|
||||||
- `false` - vServer подключается к локальному сервису по HTTP (по умолчанию)
|
|
||||||
- `true` - vServer подключается к локальному сервису по HTTPS
|
|
||||||
|
|
||||||
**`AutoHTTPS`** - автоматический редирект на HTTPS:
|
|
||||||
- `true` - все HTTP запросы автоматически перенаправляются на HTTPS (рекомендуется)
|
|
||||||
- `false` - разрешены как HTTP, так и HTTPS запросы
|
|
||||||
|
|
||||||
**Схема работы:**
|
|
||||||
```
|
|
||||||
Клиент (HTTP/HTTPS) → vServer (проверка AutoHTTPS) → Локальный сервис (ServiceHTTPSuse)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Применение изменений:**
|
|
||||||
- Введите команду `config_reload` в консоли для перезагрузки конфигурации
|
|
||||||
- Изменения применятся к новым запросам без перезапуска сервера
|
|
||||||
|
|
||||||
## 🔒 vAccess - Система контроля доступа
|
|
||||||
|
|
||||||
vServer включает гибкую систему контроля доступа **vAccess** для сайтов и прокси-сервисов.
|
|
||||||
|
|
||||||
### 📁 Расположение конфигураций
|
|
||||||
|
|
||||||
**Для сайтов:**
|
|
||||||
```
|
|
||||||
WebServer/www/{host}/vAccess.conf
|
|
||||||
```
|
|
||||||
|
|
||||||
**Для прокси:**
|
|
||||||
```
|
|
||||||
WebServer/tools/Proxy_vAccess/{domain}_vAccess.conf
|
|
||||||
```
|
|
||||||
|
|
||||||
### ⚙️ Основные возможности
|
|
||||||
|
|
||||||
- ✅ **IP-фильтрация** - разрешение/блокировка по IP адресам
|
|
||||||
- ✅ **Контроль путей** - ограничение доступа к определённым директориям
|
|
||||||
- ✅ **Фильтрация файлов** - блокировка по расширениям (*.php, *.exe)
|
|
||||||
- ✅ **Исключения** - гибкие правила с exceptions_dir
|
|
||||||
- ✅ **Кастомные ошибки** - редиректы или страницы ошибок
|
|
||||||
|
|
||||||
### 📝 Пример конфигурации
|
|
||||||
|
|
||||||
```conf
|
|
||||||
# Разрешаем админку только с локальных IP
|
|
||||||
type: Allow
|
|
||||||
path_access: /admin/*, /api/admin/*
|
|
||||||
ip_list: 127.0.0.1, 192.168.1.100
|
|
||||||
url_error: 404
|
|
||||||
|
|
||||||
# Блокируем опасные файлы в uploads
|
|
||||||
type: Disable
|
|
||||||
type_file: *.php, *.exe, *.sh
|
|
||||||
path_access: /uploads/*
|
|
||||||
url_error: 404
|
|
||||||
```
|
|
||||||
|
|
||||||
### 📚 Документация
|
|
||||||
|
|
||||||
Подробная документация по vAccess:
|
|
||||||
- **Для сайтов:** см. `WebServer/www/{host}/vAccess.conf` (примеры в файле)
|
|
||||||
- **Для прокси:** см. `WebServer/tools/Proxy_vAccess/README.md`
|
|
||||||
|
|
||||||
## 📝 Логирование
|
|
||||||
|
|
||||||
Все логи сохраняются в `WebServer/tools/logs/`:
|
|
||||||
|
|
||||||
- 🌐 `logs_http.log` - HTTP запросы (включая прокси P-HTTP)
|
|
||||||
- 🔒 `logs_https.log` - HTTPS запросы (включая прокси P-HTTPS)
|
|
||||||
- 🔄 `logs_proxy.log` - Ошибки прокси-сервера
|
|
||||||
- 🗄️ `logs_mysql.log` - MySQL операции
|
|
||||||
- 🐘 `logs_php.log` - PHP ошибки
|
|
||||||
- ⚙️ `logs_config.log` - Конфигурация
|
|
||||||
- 🔐 `logs_vaccess.log` - Контроль доступа для сайтов
|
|
||||||
- 🔐 `logs_vaccess_proxy.log` - Контроль доступа для прокси
|
|
||||||
|
|
||||||
## 🔐 SSL Сертификаты
|
## 🔐 SSL Сертификаты
|
||||||
|
|
||||||
### Установка сертификата
|
### Автоматически (Let's Encrypt)
|
||||||
|
Включите ACME в настройках → сертификаты выпускаются и обновляются автоматически.
|
||||||
1. Откройте каталог `WebServer/`
|
|
||||||
2. Создайте папку `cert/` (если её нет)
|
|
||||||
3. Создайте папку с именем вашего домена или IP-адреса
|
|
||||||
4. Поместите в неё файлы сертификатов с **точными** именами:
|
|
||||||
```
|
|
||||||
certificate.crt
|
|
||||||
private.key
|
|
||||||
ca_bundle.crt
|
|
||||||
```
|
|
||||||
5. Сертификат будет автоматически загружен при запуске сервера
|
|
||||||
|
|
||||||
### 📁 Структура сертификатов
|
|
||||||
|
|
||||||
|
### Вручную
|
||||||
|
Загрузите через GUI админку (раздел SSL менеджер) или поместите файлы:
|
||||||
```
|
```
|
||||||
WebServer/
|
WebServer/cert/{domain}/
|
||||||
└── cert/
|
├── certificate.crt
|
||||||
├── example.com/ # Основной домен
|
├── private.key
|
||||||
│ ├── certificate.crt
|
└── ca_bundle.crt
|
||||||
│ ├── private.key
|
|
||||||
│ └── ca_bundle.crt
|
|
||||||
│
|
|
||||||
└── sub.example.com/ # Поддомен (опционально)
|
|
||||||
├── certificate.crt
|
|
||||||
├── private.key
|
|
||||||
└── ca_bundle.crt
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 🎯 Работа с поддоменами
|
> 💡 **Wildcard:** один сертификат в папке основного домена покрывает все поддомены.
|
||||||
|
|
||||||
**Важно:** Если для поддомена не создана отдельная папка в `cert/`, то автоматически будет использоваться сертификат родительского домена.
|
## 📝 Логирование
|
||||||
|
|
||||||
**Примеры:**
|
Логи в `WebServer/tools/logs/`:
|
||||||
- ✅ Запрос к `example.com` → использует сертификат из `cert/example.com/`
|
|
||||||
- ✅ Запрос к `sub.example.com` (папка существует) → использует `cert/sub.example.com/`
|
|
||||||
- ✅ Запрос к `sub.example.com` (папка НЕ существует) → использует `cert/example.com/`
|
|
||||||
|
|
||||||
**Это удобно для wildcard-сертификатов:** достаточно одного сертификата в папке основного домена для всех поддоменов! 🌟
|
| Файл | Содержимое |
|
||||||
|
|------|------------|
|
||||||
|
| `logs_http.log` | HTTP запросы |
|
||||||
|
| `logs_https.log` | HTTPS запросы |
|
||||||
|
| `logs_proxy.log` | Ошибки прокси |
|
||||||
|
| `logs_mysql.log` | MySQL операции |
|
||||||
|
| `logs_php.log` | PHP ошибки |
|
||||||
|
| `logs_config.log` | Конфигурация |
|
||||||
|
| `logs_vaccess.log` | Контроль доступа |
|
||||||
|
| `logs_acme.log` | Let's Encrypt |
|
||||||
|
|
||||||
|
## 📡 API
|
||||||
|
|
||||||
|
Документация всех методов API админки: [`Backend/admin/API.md`](Backend/admin/API.md)
|
||||||
|
|||||||
341
README_EN.md
341
README_EN.md
@@ -1,9 +1,9 @@
|
|||||||
# 🚀 vServer - Functional Web Server on Go
|
# 🚀 vServer - Functional Web Server on Go
|
||||||
**🇷🇺 [Русская версия](README.md)**
|
**🇷🇺 [Русская версия](README.md)**
|
||||||
> Full-featured web server with HTTP/HTTPS, MySQL, PHP support and GUI admin panel
|
> Full-featured web server with HTTP/HTTPS, MySQL, PHP, Let's Encrypt and GUI admin panel
|
||||||
|
|
||||||
**👨💻 Author:** Roman Sumaneev
|
**👨💻 Author:** Roman Sumaneev
|
||||||
**🌐 Website:** [voxsel.ru](https://voxsel.ru)
|
**🌐 Website:** [vserf.ru](https://vserf.ru)
|
||||||
**📞 Contact:** [VK](https://vk.com/felias)
|
**📞 Contact:** [VK](https://vk.com/felias)
|
||||||
|
|
||||||
## 🎯 Features
|
## 🎯 Features
|
||||||
@@ -13,288 +13,119 @@
|
|||||||
|
|
||||||
### 🌐 Web Server
|
### 🌐 Web Server
|
||||||
- ✅ **HTTP/HTTPS** server with SSL certificate support
|
- ✅ **HTTP/HTTPS** server with SSL certificate support
|
||||||
- ✅ **Proxy server** for request proxying
|
- ✅ **Let's Encrypt** — automatic SSL issuance and renewal
|
||||||
- ✅ **PHP server** with built-in PHP 8 support
|
- ✅ **Proxy server** — reverse proxy for local services
|
||||||
- ✅ **Static content** for hosting websites
|
- ✅ **PHP 8** — built-in support (FastCGI pool)
|
||||||
- ✅ **vAccess** - access control system for sites and proxies
|
- ✅ **MySQL** — built-in database server
|
||||||
|
- ✅ **vAccess** — access control system
|
||||||
|
- ✅ **Wildcard** — wildcard aliases and certificates support
|
||||||
|
|
||||||
### 🗄️ Database
|
### 🎛️ GUI Admin Panel
|
||||||
- ✅ **MySQL server** with full support
|
- ✅ **Service management** — start/stop HTTP, HTTPS, MySQL, PHP, Proxy
|
||||||
|
- ✅ **Site management** — create, edit, delete
|
||||||
### 🔧 Administration
|
- ✅ **Proxy management** — visual reverse proxy configuration
|
||||||
- ✅ **GUI Admin Panel** - Wails desktop application with modern interface
|
- ✅ **SSL manager** — issue, renew, upload certificates
|
||||||
- ✅ **Service Management** - start/stop HTTP, HTTPS, MySQL, PHP, Proxy
|
- ✅ **vAccess editor** — access rules with drag-and-drop
|
||||||
- ✅ **Site and Proxy Editor** - visual configuration management
|
- ✅ **Settings** — MySQL, PHP, proxy, ACME configuration
|
||||||
- ✅ **vAccess Editor** - access rules configuration through interface
|
- ✅ **Dark/Light theme** and **RU/EN** localization
|
||||||
|
|
||||||
## 🏗️ Architecture
|
## 🏗️ Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
vServer/
|
vServer/
|
||||||
├── 🎯 main.go # Main server entry point
|
├── 🎯 main.go # Entry point (Wails)
|
||||||
│
|
│
|
||||||
├── 🔧 Backend/ # Core logic
|
├── 🔧 Backend/ # Core logic
|
||||||
│ │
|
│ ├── admin/
|
||||||
│ ├── admin/ # | 🎛️ GUI Admin Panel (Wails) |
|
│ │ ├── go/ # Go admin backend
|
||||||
│ │ ├── go/ # | Go backend for admin panel |
|
│ │ └── API.md # API documentation
|
||||||
│ │ └── frontend/ # | Modern UI |
|
│ ├── config/ # Go configuration
|
||||||
│ │
|
│ ├── tools/ # Utilities and helpers
|
||||||
│ ├── config/ # | 🔧 Go configuration files |
|
│ └── WebServer/ # Web server modules
|
||||||
│ ├── tools/ # | 🛠️ Utilities and helpers |
|
│ └── acme/ # Let's Encrypt
|
||||||
│ └── WebServer/ # | 🌐 Web server modules |
|
|
||||||
│
|
│
|
||||||
├── 🌐 WebServer/ # Web content and configuration
|
├── 🖥️ front_vue/ # Vue 3 admin frontend
|
||||||
│ │
|
│ └── src/
|
||||||
│ ├── cert/ # | 🔐 SSL certificates |
|
│ ├── Core/ # API, stores, i18n, router
|
||||||
│ ├── soft/ # | 📦 MySQL and PHP |
|
│ └── Design/ # Components, views, styles
|
||||||
│ ├── tools/ # | 📊 Logs and tools |
|
|
||||||
│ └── www/ # | 🌍 Web content |
|
|
||||||
│
|
│
|
||||||
├── 📄 go.mod # Go modules
|
├── 🌐 WebServer/ # Server working files
|
||||||
├── 🔨 build_admin.ps1 # Build GUI admin panel
|
│ ├── config.json # Configuration
|
||||||
└── 🚀 vSerf.exe # GUI admin panel (after build)
|
│ ├── cert/ # SSL certificates
|
||||||
|
│ ├── soft/ # MySQL and PHP
|
||||||
|
│ ├── tools/ # Logs, error page, vAccess
|
||||||
|
│ └── www/ # Web content (sites)
|
||||||
|
│
|
||||||
|
├── 🔨 build_admin.ps1 # Build script
|
||||||
|
└── 🚀 vSerf.exe # Built application
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🚀 Installation and Launch
|
## 🚀 Installation and Launch
|
||||||
|
|
||||||
### 🔨 Building the Main Server
|
### For Users
|
||||||
|
|
||||||
|
1. Download the latest [release](https://github.com/AiVoxel/vServer/releases)
|
||||||
|
2. Extract `WebServer/soft/soft.rar` to `WebServer/soft/`
|
||||||
|
3. Run `vSerf.exe` — the GUI admin panel will open
|
||||||
|
4. Server starts automatically, manage everything through the interface
|
||||||
|
|
||||||
|
> 🔑 **Default MySQL password:** `root`
|
||||||
|
|
||||||
|
### For Developers
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
|
# Build (checks/installs Go, Node.js, Wails)
|
||||||
./build_admin.ps1
|
./build_admin.ps1
|
||||||
```
|
```
|
||||||
|
|
||||||
The script will automatically:
|
The script automatically:
|
||||||
- Check/create `go.mod`
|
- Checks dependencies (Go, Node.js, npm) — offers to install via `winget`
|
||||||
- Install dependencies (`go mod tidy`)
|
- Installs Go modules and Wails CLI
|
||||||
- Check/install Wails CLI
|
- Builds Vue frontend
|
||||||
- Build the application → `vSerf.exe`
|
- Compiles → `vSerf.exe`
|
||||||
|
|
||||||
### 📦 Component Preparation
|
## 🔒 vAccess — Access Control
|
||||||
1. Extract `WebServer/soft/soft.rar` archive to `WebServer/soft/` folder
|
|
||||||
2. Run `vServer.exe` - main server
|
|
||||||
3. Run `vSerf.exe` - GUI admin panel for management
|
|
||||||
|
|
||||||
> 🔑 **Important:** Default MySQL password is `root`
|
Flexible rules system for sites and proxies. Configured through GUI admin panel (vAccess section).
|
||||||
|
|
||||||
### 📦 Ready Project for Users
|
**Features:**
|
||||||
Required for operation:
|
- IP filtering — allow/block by IP addresses
|
||||||
- 📄 `vSerf.exe` - GUI admin panel (optional)
|
- Path control — restrict access to directories
|
||||||
- 📁 `WebServer/` - configuration and resources
|
- File filtering — block by extensions
|
||||||
|
- Exceptions — paths excluded from rules
|
||||||
> 💡 The `Backend/` folder and `go.mod`, `main.go` files are only needed for development
|
- Custom errors — redirects or error pages
|
||||||
|
|
||||||
## ⚙️ Configuration
|
|
||||||
|
|
||||||
Configuration via `WebServer/config.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"Site_www": [
|
|
||||||
{
|
|
||||||
"name": "Local Site",
|
|
||||||
"host": "127.0.0.1",
|
|
||||||
"alias": ["localhost"],
|
|
||||||
"status": "active",
|
|
||||||
"root_file": "index.html",
|
|
||||||
"root_file_routing": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Proxy_Service": [
|
|
||||||
{
|
|
||||||
"Enable": true,
|
|
||||||
"ExternalDomain": "git.example.com",
|
|
||||||
"LocalAddress": "127.0.0.1",
|
|
||||||
"LocalPort": "3333",
|
|
||||||
"ServiceHTTPSuse": false,
|
|
||||||
"AutoHTTPS": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Soft_Settings": {
|
|
||||||
"mysql_port": 3306, "mysql_host": "127.0.0.1",
|
|
||||||
"php_port": 8000, "php_host": "localhost",
|
|
||||||
"proxy_enabled": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Main Parameters:**
|
|
||||||
- `Site_www` - website settings
|
|
||||||
- `Proxy_Service` - proxy service configuration
|
|
||||||
- `Soft_Settings` - service ports and hosts (MySQL, PHP, proxy_enabled)
|
|
||||||
|
|
||||||
### 🌐 Alias with Wildcard Support
|
|
||||||
|
|
||||||
Wildcard (`*`) support in aliases for sites:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"alias": [
|
|
||||||
"*.test.com", // All subdomains of test.com
|
|
||||||
"*.test.ru", // All subdomains of test.ru
|
|
||||||
"test.com", // Exact match
|
|
||||||
"api.*" // api with any zone
|
|
||||||
],
|
|
||||||
"host": "test.com"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Wildcard Examples:**
|
|
||||||
- `*.example.com` → `api.example.com`, `admin.example.com`, `test.example.com` ✅
|
|
||||||
- `example.*` → `example.com`, `example.ru`, `example.org` ✅
|
|
||||||
- `*example.com` → `test-example.com`, `my-example.com` ✅
|
|
||||||
- `*` → any domain ✅ (use carefully!)
|
|
||||||
- `example.com` → only `example.com` ✅ (without wildcard)
|
|
||||||
|
|
||||||
### 🔄 Proxy Server
|
|
||||||
|
|
||||||
The proxy server allows redirecting external requests to local services.
|
|
||||||
|
|
||||||
**Proxy_Service Parameters:**
|
|
||||||
- `Enable` - enable/disable proxy (true/false)
|
|
||||||
- `ExternalDomain` - external domain for request interception
|
|
||||||
- `LocalAddress` - local service address
|
|
||||||
- `LocalPort` - local service port
|
|
||||||
- `ServiceHTTPSuse` - use HTTPS for connecting to local service (true/false)
|
|
||||||
- `AutoHTTPS` - automatically redirect HTTP → HTTPS (true/false)
|
|
||||||
|
|
||||||
**Multiple Proxy Example:**
|
|
||||||
```json
|
|
||||||
"Proxy_Service": [
|
|
||||||
{
|
|
||||||
"Enable": true,
|
|
||||||
"ExternalDomain": "git.example.com",
|
|
||||||
"LocalAddress": "127.0.0.1",
|
|
||||||
"LocalPort": "3000",
|
|
||||||
"ServiceHTTPSuse": false,
|
|
||||||
"AutoHTTPS": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Enable": false,
|
|
||||||
"ExternalDomain": "api.example.com",
|
|
||||||
"LocalAddress": "127.0.0.1",
|
|
||||||
"LocalPort": "8080",
|
|
||||||
"ServiceHTTPSuse": false,
|
|
||||||
"AutoHTTPS": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 📖 Detailed Parameter Description:
|
|
||||||
|
|
||||||
**`ServiceHTTPSuse`** - protocol for connecting to local service:
|
|
||||||
- `false` - vServer connects to local service via HTTP (default)
|
|
||||||
- `true` - vServer connects to local service via HTTPS
|
|
||||||
|
|
||||||
**`AutoHTTPS`** - automatic HTTPS redirect:
|
|
||||||
- `true` - all HTTP requests are automatically redirected to HTTPS (recommended)
|
|
||||||
- `false` - both HTTP and HTTPS requests are allowed
|
|
||||||
|
|
||||||
**How it Works:**
|
|
||||||
```
|
|
||||||
Client (HTTP/HTTPS) → vServer (AutoHTTPS check) → Local Service (ServiceHTTPSuse)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Applying Changes:**
|
|
||||||
- Enter `config_reload` command in console to reload configuration
|
|
||||||
- Changes will apply to new requests without server restart
|
|
||||||
|
|
||||||
## 🔒 vAccess - Access Control System
|
|
||||||
|
|
||||||
vServer includes a flexible access control system **vAccess** for sites and proxy services.
|
|
||||||
|
|
||||||
### 📁 Configuration Locations
|
|
||||||
|
|
||||||
**For Sites:**
|
|
||||||
```
|
|
||||||
WebServer/www/{host}/vAccess.conf
|
|
||||||
```
|
|
||||||
|
|
||||||
**For Proxy:**
|
|
||||||
```
|
|
||||||
WebServer/tools/Proxy_vAccess/{domain}_vAccess.conf
|
|
||||||
```
|
|
||||||
|
|
||||||
### ⚙️ Main Features
|
|
||||||
|
|
||||||
- ✅ **IP Filtering** - allow/block by IP addresses
|
|
||||||
- ✅ **Path Control** - restrict access to specific directories
|
|
||||||
- ✅ **File Filtering** - block by extensions (*.php, *.exe)
|
|
||||||
- ✅ **Exceptions** - flexible rules with exceptions_dir
|
|
||||||
- ✅ **Custom Errors** - redirects or error pages
|
|
||||||
|
|
||||||
### 📝 Configuration Example
|
|
||||||
|
|
||||||
```conf
|
|
||||||
# Allow admin panel only from local IPs
|
|
||||||
type: Allow
|
|
||||||
path_access: /admin/*, /api/admin/*
|
|
||||||
ip_list: 127.0.0.1, 192.168.1.100
|
|
||||||
url_error: 404
|
|
||||||
|
|
||||||
# Block dangerous files in uploads
|
|
||||||
type: Disable
|
|
||||||
type_file: *.php, *.exe, *.sh
|
|
||||||
path_access: /uploads/*
|
|
||||||
url_error: 404
|
|
||||||
```
|
|
||||||
|
|
||||||
### 📚 Documentation
|
|
||||||
|
|
||||||
Detailed vAccess documentation:
|
|
||||||
- **For Sites:** see `WebServer/www/{host}/vAccess.conf` (examples in file)
|
|
||||||
- **For Proxy:** see `WebServer/tools/Proxy_vAccess/README.md`
|
|
||||||
|
|
||||||
## 📝 Logging
|
|
||||||
|
|
||||||
All logs are saved in `WebServer/tools/logs/`:
|
|
||||||
|
|
||||||
- 🌐 `logs_http.log` - HTTP requests (including proxy P-HTTP)
|
|
||||||
- 🔒 `logs_https.log` - HTTPS requests (including proxy P-HTTPS)
|
|
||||||
- 🔄 `logs_proxy.log` - Proxy server errors
|
|
||||||
- 🗄️ `logs_mysql.log` - MySQL operations
|
|
||||||
- 🐘 `logs_php.log` - PHP errors
|
|
||||||
- ⚙️ `logs_config.log` - Configuration
|
|
||||||
- 🔐 `logs_vaccess.log` - Access control for sites
|
|
||||||
- 🔐 `logs_vaccess_proxy.log` - Access control for proxy
|
|
||||||
|
|
||||||
## 🔐 SSL Certificates
|
## 🔐 SSL Certificates
|
||||||
|
|
||||||
### Certificate Installation
|
### Automatic (Let's Encrypt)
|
||||||
|
Enable ACME in settings → certificates are issued and renewed automatically.
|
||||||
1. Open `WebServer/` directory
|
|
||||||
2. Create `cert/` folder (if it doesn't exist)
|
|
||||||
3. Create a folder with your domain name or IP address
|
|
||||||
4. Place certificate files with **exact** names:
|
|
||||||
```
|
|
||||||
certificate.crt
|
|
||||||
private.key
|
|
||||||
ca_bundle.crt
|
|
||||||
```
|
|
||||||
5. Certificate will be automatically loaded at server startup
|
|
||||||
|
|
||||||
### 📁 Certificate Structure
|
|
||||||
|
|
||||||
|
### Manual
|
||||||
|
Upload through GUI admin panel (SSL Manager section) or place files:
|
||||||
```
|
```
|
||||||
WebServer/
|
WebServer/cert/{domain}/
|
||||||
└── cert/
|
├── certificate.crt
|
||||||
├── example.com/ # Main domain
|
├── private.key
|
||||||
│ ├── certificate.crt
|
└── ca_bundle.crt
|
||||||
│ ├── private.key
|
|
||||||
│ └── ca_bundle.crt
|
|
||||||
│
|
|
||||||
└── sub.example.com/ # Subdomain (optional)
|
|
||||||
├── certificate.crt
|
|
||||||
├── private.key
|
|
||||||
└── ca_bundle.crt
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 🎯 Working with Subdomains
|
> 💡 **Wildcard:** one certificate in the main domain folder covers all subdomains.
|
||||||
|
|
||||||
**Important:** If no separate folder is created in `cert/` for a subdomain, the parent domain's certificate will be used automatically.
|
## 📝 Logging
|
||||||
|
|
||||||
**Examples:**
|
Logs in `WebServer/tools/logs/`:
|
||||||
- ✅ Request to `example.com` → uses certificate from `cert/example.com/`
|
|
||||||
- ✅ Request to `sub.example.com` (folder exists) → uses `cert/sub.example.com/`
|
|
||||||
- ✅ Request to `sub.example.com` (folder does NOT exist) → uses `cert/example.com/`
|
|
||||||
|
|
||||||
**This is convenient for wildcard certificates:** one certificate in the main domain folder is enough for all subdomains! 🌟
|
| File | Contents |
|
||||||
|
|------|----------|
|
||||||
|
| `logs_http.log` | HTTP requests |
|
||||||
|
| `logs_https.log` | HTTPS requests |
|
||||||
|
| `logs_proxy.log` | Proxy errors |
|
||||||
|
| `logs_mysql.log` | MySQL operations |
|
||||||
|
| `logs_php.log` | PHP errors |
|
||||||
|
| `logs_config.log` | Configuration |
|
||||||
|
| `logs_vaccess.log` | Access control |
|
||||||
|
| `logs_acme.log` | Let's Encrypt |
|
||||||
|
|
||||||
|
## 📡 API
|
||||||
|
|
||||||
|
Full admin API documentation: [`Backend/admin/API.md`](Backend/admin/API.md)
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ code {
|
|||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
background: rgba(var(--accent-rgb), 0.1);
|
background: rgba(var(--accent-rgb), 0.1);
|
||||||
padding: 3px 8px;
|
padding: 3px 8px;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius);
|
||||||
font-size: var(--text-sm);
|
font-size: var(--text-sm);
|
||||||
color: var(--accent-purple-light);
|
color: var(--accent-purple-light);
|
||||||
border: 1px solid var(--glass-border);
|
border: 1px solid var(--glass-border);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
.form-section {
|
.form-section {
|
||||||
padding: var(--space-xl);
|
padding: var(--space-xl);
|
||||||
background: rgba(var(--accent-rgb), 0.02);
|
background: rgba(var(--accent-rgb), 0.02);
|
||||||
border-radius: var(--radius-xl);
|
border-radius: var(--radius);
|
||||||
border: 1px solid var(--glass-border);
|
border: 1px solid var(--glass-border);
|
||||||
transition: all var(--transition-base);
|
transition: all var(--transition-base);
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
padding: 10px var(--space-md);
|
padding: 10px var(--space-md);
|
||||||
background: rgba(var(--muted-rgb), 0.1);
|
background: rgba(var(--muted-rgb), 0.1);
|
||||||
border: 1px solid rgba(var(--muted-rgb), 0.3);
|
border: 1px solid rgba(var(--muted-rgb), 0.3);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: var(--text-base);
|
font-size: var(--text-base);
|
||||||
font-weight: var(--font-semibold);
|
font-weight: var(--font-semibold);
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
background: var(--glass-bg-light);
|
background: var(--glass-bg-light);
|
||||||
backdrop-filter: var(--backdrop-blur);
|
backdrop-filter: var(--backdrop-blur);
|
||||||
border: 1px solid var(--glass-border);
|
border: 1px solid var(--glass-border);
|
||||||
border-radius: var(--radius-xl);
|
border-radius: var(--radius);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
width: 22px;
|
width: 22px;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +154,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: var(--btn-icon-bg);
|
background: var(--btn-icon-bg);
|
||||||
border: 1px solid var(--btn-icon-border);
|
border: 1px solid var(--btn-icon-border);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius);
|
||||||
color: var(--btn-icon-color);
|
color: var(--btn-icon-color);
|
||||||
font-size: var(--text-md);
|
font-size: var(--text-md);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -58,9 +58,9 @@
|
|||||||
--table-border: rgba(255, 255, 255, 0.05);
|
--table-border: rgba(255, 255, 255, 0.05);
|
||||||
|
|
||||||
/* Компоненты */
|
/* Компоненты */
|
||||||
--card-hover-shadow: 0 8px 32px rgba(139, 92, 246, 0.3);
|
--card-hover-shadow: 0 4px 16px rgba(139, 92, 246, 0.12);
|
||||||
--card-border-hover: rgba(139, 92, 246, 0.3);
|
--card-border-hover: rgba(139, 92, 246, 0.2);
|
||||||
--service-card-gradient: linear-gradient(90deg, #8b5cf6, #a78bfa, #06b6d4);
|
--service-card-gradient: linear-gradient(90deg, rgba(139, 92, 246, 0.6), rgba(167, 139, 250, 0.6), rgba(6, 182, 212, 0.6));
|
||||||
--btn-icon-bg: rgba(139, 92, 246, 0.1);
|
--btn-icon-bg: rgba(139, 92, 246, 0.1);
|
||||||
--btn-icon-border: rgba(139, 92, 246, 0.3);
|
--btn-icon-border: rgba(139, 92, 246, 0.3);
|
||||||
--btn-icon-color: #a78bfa;
|
--btn-icon-color: #a78bfa;
|
||||||
|
|||||||
@@ -13,10 +13,7 @@
|
|||||||
--space-3xl: 60px;
|
--space-3xl: 60px;
|
||||||
|
|
||||||
/* Border Radius */
|
/* Border Radius */
|
||||||
--radius-sm: 6px;
|
--radius: 8px;
|
||||||
--radius-md: 8px;
|
|
||||||
--radius-lg: 12px;
|
|
||||||
--radius-xl: 16px;
|
|
||||||
--radius-full: 50%;
|
--radius-full: 50%;
|
||||||
|
|
||||||
/* Transitions */
|
/* Transitions */
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const goBack = () => {
|
|||||||
margin-bottom: var(--space-md);
|
margin-bottom: var(--space-md);
|
||||||
padding: var(--space-md) 20px;
|
padding: var(--space-md) 20px;
|
||||||
background: rgba(var(--accent-rgb), 0.05);
|
background: rgba(var(--accent-rgb), 0.05);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius);
|
||||||
border: 1px solid var(--glass-border);
|
border: 1px solid var(--glass-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ const goBack = () => {
|
|||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
padding: var(--space-sm) var(--space-lg);
|
padding: var(--space-sm) var(--space-lg);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all var(--transition-base);
|
transition: all var(--transition-base);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ defineProps({
|
|||||||
margin-bottom: var(--space-md);
|
margin-bottom: var(--space-md);
|
||||||
padding: var(--space-lg);
|
padding: var(--space-lg);
|
||||||
background: rgba(var(--accent-rgb), 0.03);
|
background: rgba(var(--accent-rgb), 0.03);
|
||||||
border-radius: var(--radius-xl);
|
border-radius: var(--radius);
|
||||||
border: 1px solid var(--glass-border);
|
border: 1px solid var(--glass-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ const navigate = (item) => {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius);
|
||||||
color: var(--nav-color);
|
color: var(--nav-color);
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ const windowClose = () => { if (isWails) window.runtime.Quit() }
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-sm);
|
gap: var(--space-sm);
|
||||||
padding: var(--space-xs) var(--space-md);
|
padding: var(--space-xs) var(--space-md);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius);
|
||||||
background: var(--glass-bg);
|
background: var(--glass-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@ const windowClose = () => { if (isWails) window.runtime.Quit() }
|
|||||||
gap: var(--space-sm);
|
gap: var(--space-sm);
|
||||||
padding: var(--space-xs) var(--space-md);
|
padding: var(--space-xs) var(--space-md);
|
||||||
border: 1px solid var(--glass-border);
|
border: 1px solid var(--glass-border);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius);
|
||||||
background: var(--glass-bg);
|
background: var(--glass-bg);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -194,7 +194,7 @@ const windowClose = () => { if (isWails) window.runtime.Quit() }
|
|||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -208,7 +208,7 @@ const windowClose = () => { if (isWails) window.runtime.Quit() }
|
|||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ const findCertForDomain = (domain) => {
|
|||||||
</VBadge>
|
</VBadge>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="icon-btn" :title="t('sites.editVaccess')" @click="router.push(`/vaccess/${proxy.ExternalDomain}`)">
|
<button class="icon-btn" :title="t('sites.editVaccess')" @click="router.push(`/vaccess/${proxy.ExternalDomain}?proxy=true`)">
|
||||||
<i class="fas fa-user-lock"></i>
|
<i class="fas fa-user-lock"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="icon-btn" :title="t('certs.title')" @click="router.push(`/certs/${proxy.ExternalDomain}`)">
|
<button class="icon-btn" :title="t('certs.title')" @click="router.push(`/certs/${proxy.ExternalDomain}`)">
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ const serviceInfoLabel = {
|
|||||||
background: var(--glass-bg-light);
|
background: var(--glass-bg-light);
|
||||||
backdrop-filter: var(--backdrop-blur);
|
backdrop-filter: var(--backdrop-blur);
|
||||||
border: 1px solid var(--glass-border);
|
border: 1px solid var(--glass-border);
|
||||||
border-radius: var(--radius-xl);
|
border-radius: var(--radius);
|
||||||
padding: var(--space-lg);
|
padding: var(--space-lg);
|
||||||
transition: all var(--transition-bounce);
|
transition: all var(--transition-bounce);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ defineEmits(['click'])
|
|||||||
gap: var(--space-sm);
|
gap: var(--space-sm);
|
||||||
padding: var(--space-sm) var(--space-md);
|
padding: var(--space-sm) var(--space-md);
|
||||||
border: 1px solid var(--glass-border);
|
border: 1px solid var(--glass-border);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius);
|
||||||
background: var(--glass-bg);
|
background: var(--glass-bg);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ defineProps({
|
|||||||
.v-card {
|
.v-card {
|
||||||
background: var(--glass-bg);
|
background: var(--glass-bg);
|
||||||
border: 1px solid var(--glass-border);
|
border: 1px solid var(--glass-border);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius);
|
||||||
backdrop-filter: var(--backdrop-blur);
|
backdrop-filter: var(--backdrop-blur);
|
||||||
transition: all var(--transition-base);
|
transition: all var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ const onFileSelect = (event) => {
|
|||||||
padding: var(--space-sm) var(--space-md);
|
padding: var(--space-sm) var(--space-md);
|
||||||
background: var(--glass-bg);
|
background: var(--glass-bg);
|
||||||
border: 1px dashed var(--glass-border);
|
border: 1px dashed var(--glass-border);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius);
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
transition: all var(--transition-fast);
|
transition: all var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ defineEmits(['update:modelValue'])
|
|||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
background: var(--glass-bg-dark);
|
background: var(--glass-bg-dark);
|
||||||
border: 1px solid var(--glass-border);
|
border: 1px solid var(--glass-border);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font-size: var(--text-base);
|
font-size: var(--text-base);
|
||||||
transition: all var(--transition-fast);
|
transition: all var(--transition-fast);
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ const modal = useModal()
|
|||||||
.v-modal-window {
|
.v-modal-window {
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border: 1px solid var(--glass-border);
|
border: 1px solid var(--glass-border);
|
||||||
border-radius: var(--radius-xl);
|
border-radius: var(--radius);
|
||||||
width: 90%;
|
width: 90%;
|
||||||
max-width: 700px;
|
max-width: 700px;
|
||||||
max-height: 85vh;
|
max-height: 85vh;
|
||||||
@@ -79,7 +79,7 @@ const modal = useModal()
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const { notifications, remove } = useNotification()
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-sm);
|
gap: var(--space-sm);
|
||||||
padding: var(--space-sm) var(--space-md);
|
padding: var(--space-sm) var(--space-md);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius);
|
||||||
font-size: var(--text-md);
|
font-size: var(--text-md);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
backdrop-filter: var(--backdrop-blur);
|
backdrop-filter: var(--backdrop-blur);
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ onUnmounted(() => document.removeEventListener('click', onDocClick))
|
|||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
background: var(--glass-bg-dark);
|
background: var(--glass-bg-dark);
|
||||||
border: 1px solid var(--glass-border);
|
border: 1px solid var(--glass-border);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font-size: var(--text-base);
|
font-size: var(--text-base);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -129,7 +129,7 @@ onUnmounted(() => document.removeEventListener('click', onDocClick))
|
|||||||
right: 0;
|
right: 0;
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border: 1px solid rgba(var(--accent-rgb), 0.3);
|
border: 1px solid rgba(var(--accent-rgb), 0.3);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius);
|
||||||
box-shadow: var(--shadow-lg);
|
box-shadow: var(--shadow-lg);
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ defineProps({
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.v-table-container {
|
.v-table-container {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius);
|
||||||
border: 1px solid var(--glass-border);
|
border: 1px solid var(--glass-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const show = ref(false)
|
|||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border: 1px solid rgba(var(--accent-rgb), 0.3);
|
border: 1px solid rgba(var(--accent-rgb), 0.3);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius);
|
||||||
box-shadow: var(--shadow-lg);
|
box-shadow: var(--shadow-lg);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
font-size: var(--text-sm);
|
font-size: var(--text-sm);
|
||||||
|
|||||||
@@ -1 +1,161 @@
|
|||||||
<script setup>`nconst { t } = useI18n()`n</script>`n<template><div>Field Editor</div></template>
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: Array, default: () => [] },
|
||||||
|
placeholder: { type: String, default: '' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
const input = ref('')
|
||||||
|
const editing = ref(false)
|
||||||
|
|
||||||
|
const addItem = () => {
|
||||||
|
const raw = input.value.trim()
|
||||||
|
if (!raw) return
|
||||||
|
const items = raw.split(',').map(s => s.trim()).filter(s => s && !props.modelValue.includes(s))
|
||||||
|
if (items.length) {
|
||||||
|
emit('update:modelValue', [...props.modelValue, ...items])
|
||||||
|
}
|
||||||
|
input.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeItem = (index) => {
|
||||||
|
const updated = [...props.modelValue]
|
||||||
|
updated.splice(index, 1)
|
||||||
|
emit('update:modelValue', updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onKeydown = (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault()
|
||||||
|
addItem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="field-editor" @click="editing = true">
|
||||||
|
<div v-if="!editing && modelValue.length === 0" class="empty-field" @click="editing = true">—</div>
|
||||||
|
<div v-else-if="!editing" class="mini-tags" @click="editing = true">
|
||||||
|
<code v-for="(item, i) in modelValue" :key="i">{{ item }}</code>
|
||||||
|
<span class="edit-hint"><i class="fas fa-pen"></i></span>
|
||||||
|
</div>
|
||||||
|
<div v-else class="field-editor-active" @click.stop>
|
||||||
|
<div class="field-input-row">
|
||||||
|
<input
|
||||||
|
v-model="input"
|
||||||
|
class="field-input"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
@keydown="onKeydown"
|
||||||
|
@blur="input || (editing = false)"
|
||||||
|
>
|
||||||
|
<button v-if="input" class="field-add-btn" @click="addItem"><i class="fas fa-plus"></i></button>
|
||||||
|
</div>
|
||||||
|
<div v-if="modelValue.length" class="field-tags">
|
||||||
|
<span v-for="(item, i) in modelValue" :key="i" class="field-tag">
|
||||||
|
{{ item }}
|
||||||
|
<button class="field-tag-remove" @click="removeItem(i)"><i class="fas fa-times"></i></button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.field-editor {
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-hint {
|
||||||
|
opacity: 0;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
transition: opacity var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-editor:hover .edit-hint {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-editor-active {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
background: rgba(var(--accent-rgb), 0.2);
|
||||||
|
border: 1px solid rgba(var(--accent-rgb), 0.4);
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-tag-remove {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--accent-red);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 9px;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: opacity var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-tag-remove:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-input-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: var(--glass-bg-dark);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
outline: none;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-input:focus {
|
||||||
|
border-color: rgba(var(--accent-rgb), 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-input::placeholder {
|
||||||
|
color: var(--text-muted);
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-add-btn {
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: rgba(var(--accent-rgb), 0.15);
|
||||||
|
border: 1px solid rgba(var(--accent-rgb), 0.3);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
color: var(--accent-purple-light);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-add-btn:hover {
|
||||||
|
background: rgba(var(--accent-rgb), 0.25);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ const deleteCert = async (domain) => {
|
|||||||
.cert-card {
|
.cert-card {
|
||||||
background: rgba(var(--accent-rgb), 0.03);
|
background: rgba(var(--accent-rgb), 0.03);
|
||||||
border: 1px solid var(--glass-border);
|
border: 1px solid var(--glass-border);
|
||||||
border-radius: var(--radius-xl);
|
border-radius: var(--radius);
|
||||||
padding: var(--space-lg);
|
padding: var(--space-lg);
|
||||||
transition: all var(--transition-base);
|
transition: all var(--transition-base);
|
||||||
}
|
}
|
||||||
@@ -196,7 +196,7 @@ const deleteCert = async (domain) => {
|
|||||||
.cert-info-item {
|
.cert-info-item {
|
||||||
padding: var(--space-md);
|
padding: var(--space-md);
|
||||||
background: var(--subtle-overlay);
|
background: var(--subtle-overlay);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cert-info-label {
|
.cert-info-label {
|
||||||
@@ -229,7 +229,7 @@ const deleteCert = async (domain) => {
|
|||||||
.cert-domain-tag {
|
.cert-domain-tag {
|
||||||
padding: 4px 12px;
|
padding: 4px 12px;
|
||||||
background: rgba(var(--accent-rgb), 0.15);
|
background: rgba(var(--accent-rgb), 0.15);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius);
|
||||||
font-size: var(--text-sm);
|
font-size: var(--text-sm);
|
||||||
color: var(--accent-purple-light);
|
color: var(--accent-purple-light);
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ const toggleAcme = async () => {
|
|||||||
background: var(--glass-bg-light);
|
background: var(--glass-bg-light);
|
||||||
backdrop-filter: var(--backdrop-blur);
|
backdrop-filter: var(--backdrop-blur);
|
||||||
border: 1px solid var(--glass-border);
|
border: 1px solid var(--glass-border);
|
||||||
border-radius: var(--radius-xl);
|
border-radius: var(--radius);
|
||||||
padding: var(--space-lg);
|
padding: var(--space-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ const confirmDelete = () => {
|
|||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
background: var(--glass-bg-dark);
|
background: var(--glass-bg-dark);
|
||||||
border: 1px solid var(--glass-border);
|
border: 1px solid var(--glass-border);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font-size: var(--text-base);
|
font-size: var(--text-base);
|
||||||
outline: none;
|
outline: none;
|
||||||
@@ -197,7 +197,7 @@ const confirmDelete = () => {
|
|||||||
padding: var(--space-sm) var(--space-md);
|
padding: var(--space-sm) var(--space-md);
|
||||||
background: rgba(var(--accent-rgb), 0.15);
|
background: rgba(var(--accent-rgb), 0.15);
|
||||||
border: 1px solid rgba(var(--accent-rgb), 0.3);
|
border: 1px solid rgba(var(--accent-rgb), 0.3);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius);
|
||||||
color: var(--accent-purple-light);
|
color: var(--accent-purple-light);
|
||||||
font-size: var(--text-base);
|
font-size: var(--text-base);
|
||||||
font-weight: var(--font-semibold);
|
font-weight: var(--font-semibold);
|
||||||
@@ -222,7 +222,7 @@ const confirmDelete = () => {
|
|||||||
padding: 12px;
|
padding: 12px;
|
||||||
background: var(--glass-bg-dark);
|
background: var(--glass-bg-dark);
|
||||||
border: 1px solid var(--glass-border);
|
border: 1px solid var(--glass-border);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius);
|
||||||
min-height: 48px;
|
min-height: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ const props = defineProps({
|
|||||||
host: { type: String, required: true },
|
host: { type: String, required: true },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isProxy = computed(() => route.query.proxy === 'true')
|
||||||
|
|
||||||
const activeTab = ref('rules')
|
const activeTab = ref('rules')
|
||||||
const rules = ref([])
|
const rules = ref([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
@@ -17,13 +19,13 @@ const loading = ref(true)
|
|||||||
const { dragIndex, dragOverIndex, onDragStart, onDragOver, onDragEnter, onDragLeave, onDrop, onDragEnd } = useDraggable(rules)
|
const { dragIndex, dragOverIndex, onDragStart, onDragOver, onDragEnter, onDragLeave, onDrop, onDragEnd } = useDraggable(rules)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const data = await api.getVAccessRules(props.host, false)
|
const data = await api.getVAccessRules(props.host, isProxy.value)
|
||||||
rules.value = data?.rules || []
|
rules.value = data?.rules || []
|
||||||
loading.value = false
|
loading.value = false
|
||||||
})
|
})
|
||||||
|
|
||||||
const saveRules = async () => {
|
const saveRules = async () => {
|
||||||
await api.saveVAccessRules(props.host, false, JSON.stringify({ rules: rules.value }))
|
await api.saveVAccessRules(props.host, isProxy.value, JSON.stringify({ rules: rules.value }))
|
||||||
success(t('notify.changesSaved'))
|
success(t('notify.changesSaved'))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,12 +86,12 @@ const formatList = (arr) => {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="col-drag"></th>
|
<th class="col-drag"></th>
|
||||||
<th class="col-type">{{ t('vaccess.type') }}</th>
|
<th class="col-type"><span class="th-with-info">{{ t('vaccess.type') }} <VTooltip text="Allow — разрешить доступ, Disable — запретить. Клик для переключения" /></span></th>
|
||||||
<th class="col-files">{{ t('vaccess.files') }}</th>
|
<th class="col-files"><span class="th-with-info">{{ t('vaccess.files') }} <VTooltip text="Расширения файлов через запятую" :items="['*.php', '*.exe', '*.sh', 'no_extension']" /></span></th>
|
||||||
<th class="col-paths">{{ t('vaccess.paths') }}</th>
|
<th class="col-paths"><span class="th-with-info">{{ t('vaccess.paths') }} <VTooltip text="Пути доступа через запятую" :items="['/admin/*', '/api/*', '/uploads/*']" /></span></th>
|
||||||
<th class="col-ips">{{ t('vaccess.ips') }}</th>
|
<th class="col-ips"><span class="th-with-info">{{ t('vaccess.ips') }} <VTooltip text="IP адреса через запятую" :items="['192.168.1.1', '10.0.0.0/24', '127.0.0.1']" /></span></th>
|
||||||
<th class="col-exceptions">{{ t('vaccess.exceptions') }}</th>
|
<th class="col-exceptions"><span class="th-with-info">{{ t('vaccess.exceptions') }} <VTooltip text="Пути-исключения: правило НЕ применяется к ним" :items="['/public/*', '/bot/*', '/api/open/*']" /></span></th>
|
||||||
<th class="col-error">{{ t('vaccess.error') }}</th>
|
<th class="col-error"><span class="th-with-info">{{ t('vaccess.error') }} <VTooltip text="Куда перенаправить при блокировке" :items="['404', 'https://site.com', '/error.html']" /></span></th>
|
||||||
<th class="col-actions"></th>
|
<th class="col-actions"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -108,33 +110,25 @@ const formatList = (arr) => {
|
|||||||
>
|
>
|
||||||
<td class="drag-handle"><i class="fas fa-grip-vertical"></i></td>
|
<td class="drag-handle"><i class="fas fa-grip-vertical"></i></td>
|
||||||
<td>
|
<td>
|
||||||
<VBadge :variant="rule.type === 'Allow' ? 'yes' : 'no'">{{ rule.type }}</VBadge>
|
<VBadge class="type-toggle" :variant="rule.type === 'Allow' ? 'yes' : 'no'" @click="rule.type = rule.type === 'Allow' ? 'Disable' : 'Allow'">
|
||||||
|
{{ rule.type }}
|
||||||
|
</VBadge>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span v-if="rule.type_file?.length" class="mini-tags">
|
<FieldEditor v-model="rule.type_file" placeholder="*.php" />
|
||||||
<code v-for="f in rule.type_file" :key="f">{{ f }}</code>
|
|
||||||
</span>
|
|
||||||
<span v-else class="empty-field">—</span>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span v-if="rule.path_access?.length" class="mini-tags">
|
<FieldEditor v-model="rule.path_access" placeholder="/admin/*" />
|
||||||
<code v-for="p in rule.path_access" :key="p">{{ p }}</code>
|
|
||||||
</span>
|
|
||||||
<span v-else class="empty-field">—</span>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span v-if="rule.ip_list?.length" class="mini-tags">
|
<FieldEditor v-model="rule.ip_list" placeholder="192.168.1.1" />
|
||||||
<code v-for="ip in rule.ip_list" :key="ip">{{ ip }}</code>
|
|
||||||
</span>
|
|
||||||
<span v-else class="empty-field">—</span>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span v-if="rule.exceptions_dir?.length" class="mini-tags">
|
<FieldEditor v-model="rule.exceptions_dir" placeholder="/public/*" />
|
||||||
<code v-for="e in rule.exceptions_dir" :key="e">{{ e }}</code>
|
</td>
|
||||||
</span>
|
<td>
|
||||||
<span v-else class="empty-field">—</span>
|
<input v-model="rule.url_error" class="inline-input" placeholder="404" />
|
||||||
</td>
|
</td>
|
||||||
<td><code>{{ rule.url_error || '—' }}</code></td>
|
|
||||||
<td class="col-actions-cell">
|
<td class="col-actions-cell">
|
||||||
<button class="icon-btn-small" @click="removeRule(index)"><i class="fas fa-trash"></i></button>
|
<button class="icon-btn-small" @click="removeRule(index)"><i class="fas fa-trash"></i></button>
|
||||||
</td>
|
</td>
|
||||||
@@ -235,7 +229,7 @@ const formatList = (arr) => {
|
|||||||
padding: 10px 18px;
|
padding: 10px 18px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: var(--text-base);
|
font-size: var(--text-base);
|
||||||
font-weight: var(--font-medium);
|
font-weight: var(--font-medium);
|
||||||
@@ -302,6 +296,12 @@ const formatList = (arr) => {
|
|||||||
border-bottom: 1px solid rgba(var(--accent-rgb), 0.05);
|
border-bottom: 1px solid rgba(var(--accent-rgb), 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.th-with-info {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.col-drag { width: 3%; min-width: 40px; text-align: center; }
|
.col-drag { width: 3%; min-width: 40px; text-align: center; }
|
||||||
.col-type { width: 8%; min-width: 80px; }
|
.col-type { width: 8%; min-width: 80px; }
|
||||||
.col-files { width: 15%; min-width: 120px; }
|
.col-files { width: 15%; min-width: 120px; }
|
||||||
@@ -345,7 +345,7 @@ const formatList = (arr) => {
|
|||||||
.mini-tags code {
|
.mini-tags code {
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
background: rgba(var(--accent-rgb), 0.15);
|
background: rgba(var(--accent-rgb), 0.15);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius);
|
||||||
font-size: var(--text-sm);
|
font-size: var(--text-sm);
|
||||||
color: var(--accent-purple-light);
|
color: var(--accent-purple-light);
|
||||||
}
|
}
|
||||||
@@ -356,6 +356,36 @@ const formatList = (arr) => {
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.type-toggle {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-toggle:hover {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-input {
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: var(--glass-bg-dark);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
outline: none;
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-input:focus {
|
||||||
|
border-color: rgba(var(--accent-rgb), 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-input::placeholder {
|
||||||
|
color: var(--text-muted);
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
.col-actions-cell {
|
.col-actions-cell {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
@@ -368,7 +398,7 @@ const formatList = (arr) => {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: rgba(var(--danger-rgb), 0.1);
|
background: rgba(var(--danger-rgb), 0.1);
|
||||||
border: 1px solid rgba(var(--danger-rgb), 0.3);
|
border: 1px solid rgba(var(--danger-rgb), 0.3);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius);
|
||||||
color: var(--accent-red);
|
color: var(--accent-red);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -413,7 +443,7 @@ const formatList = (arr) => {
|
|||||||
|
|
||||||
.help-card {
|
.help-card {
|
||||||
background: var(--subtle-overlay);
|
background: var(--subtle-overlay);
|
||||||
border-radius: var(--radius-xl);
|
border-radius: var(--radius);
|
||||||
padding: var(--space-xl);
|
padding: var(--space-xl);
|
||||||
border: 1px solid var(--glass-border);
|
border: 1px solid var(--glass-border);
|
||||||
transition: all var(--transition-slow);
|
transition: all var(--transition-slow);
|
||||||
@@ -459,7 +489,7 @@ const formatList = (arr) => {
|
|||||||
.help-param {
|
.help-param {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background: rgba(var(--accent-rgb), 0.03);
|
background: rgba(var(--accent-rgb), 0.03);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius);
|
||||||
border-left: 3px solid var(--accent-purple);
|
border-left: 3px solid var(--accent-purple);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -479,7 +509,7 @@ const formatList = (arr) => {
|
|||||||
.help-param code {
|
.help-param code {
|
||||||
padding: 3px 8px;
|
padding: 3px 8px;
|
||||||
background: rgba(var(--accent-rgb), 0.15);
|
background: rgba(var(--accent-rgb), 0.15);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius);
|
||||||
font-size: var(--text-base);
|
font-size: var(--text-base);
|
||||||
color: var(--accent-purple-light);
|
color: var(--accent-purple-light);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user