diff --git a/Backend/WebServer/handler.go b/Backend/WebServer/handler.go index f7a402a..4cef8a1 100644 --- a/Backend/WebServer/handler.go +++ b/Backend/WebServer/handler.go @@ -42,28 +42,15 @@ func matchWildcardAlias(pattern, host string) bool { } // Поддержка wildcard: *.example.com, example.*, *example*, * - // Заменяем * на регулярное выражение - pattern = strings.ReplaceAll(pattern, ".", "\\.") - pattern = strings.ReplaceAll(pattern, "*", ".*") - pattern = "^" + pattern + "$" + // Паттерн *.example.com -> должен совпадать с sub.example.com - // Простая проверка без regexp (более быстрая) - return matchSimplePattern(pattern, host) -} - -// Простая проверка паттерна (без использования regexp для скорости) -func matchSimplePattern(pattern, host string) bool { - // Убираем ^ и $ добавленные выше - pattern = strings.TrimPrefix(pattern, "^") - pattern = strings.TrimSuffix(pattern, "$") - - // Если паттерн = .* (любой хост) - if pattern == ".*" { + // Если паттерн = * (любой хост) + if pattern == "*" { return true } - // Разбиваем паттерн на части по .* - parts := strings.Split(pattern, ".*") + // Разбиваем паттерн на части по звёздочке + parts := strings.Split(pattern, "*") // Проверяем каждую часть currentPos := 0 @@ -79,14 +66,14 @@ func matchSimplePattern(pattern, host string) bool { } // Для первой части проверяем что она в начале (если паттерн не начинается с *) - if i == 0 && !strings.HasPrefix(pattern, ".*") { + if i == 0 && !strings.HasPrefix(pattern, "*") { if idx != 0 { return false } } // Для последней части проверяем что она в конце (если паттерн не кончается на *) - if i == len(parts)-1 && !strings.HasSuffix(pattern, ".*") { + if i == len(parts)-1 && !strings.HasSuffix(pattern, "*") { if currentPos+idx+len(part) != len(host) { return false } @@ -98,40 +85,41 @@ func matchSimplePattern(pattern, host string) bool { return true } -func Alias_check(r *http.Request) (alias_found bool, host string) { +func Alias_Run(r *http.Request) (rhost string) { + requestHost := r.Host - alias_found = false + // Убираем порт если есть (например :80 или :443) + if colonIndex := strings.Index(requestHost, ":"); colonIndex != -1 { + requestHost = requestHost[:colonIndex] + } + // Приоритет 1: Проверяем точное совпадение с site.Host for _, site := range config.ConfigData.Site_www { + if site.Host == requestHost { + return site.Host + } + } + // Приоритет 2: Проверяем точные alias (без wildcard) + for _, site := range config.ConfigData.Site_www { for _, alias := range site.Alias { - - // Поддержка wildcard паттернов - if matchWildcardAlias(alias, r.Host) { - alias_found = true - return alias_found, site.Host - } else { - alias_found = false + if !strings.Contains(alias, "*") && alias == requestHost { + return site.Host } } } - return alias_found, "" - -} - -func Alias_Run(r *http.Request) (rhost string) { - - var host string - host = r.Host - - alias_check, alias := Alias_check(r) - - if alias_check { - host = alias + // Приоритет 3: Проверяем wildcard alias + for _, site := range config.ConfigData.Site_www { + for _, alias := range site.Alias { + if strings.Contains(alias, "*") && matchWildcardAlias(alias, requestHost) { + return site.Host + } + } } - return host + // Не нашли совпадений - возвращаем как есть + return requestHost } // Получает список root_file для сайта из конфигурации diff --git a/Backend/admin/frontend/assets/css/components/buttons.css b/Backend/admin/frontend/assets/css/components/buttons.css index 35bd3e7..1697467 100644 --- a/Backend/admin/frontend/assets/css/components/buttons.css +++ b/Backend/admin/frontend/assets/css/components/buttons.css @@ -62,6 +62,19 @@ } } +/* Delete Button Variant */ +.delete-btn { + background: rgba(239, 68, 68, 0.15); + border-color: rgba(239, 68, 68, 0.3); + color: var(--accent-red); + + &:hover:not(:disabled) { + background: rgba(239, 68, 68, 0.25); + border-color: rgba(239, 68, 68, 0.5); + box-shadow: 0 4px 12px rgba(239, 68, 68, 0.2); + } +} + /* Icon Button - Квадратная кнопка с иконкой */ .icon-btn { width: 32px; diff --git a/Backend/admin/frontend/assets/css/components/modals.css b/Backend/admin/frontend/assets/css/components/modals.css index 23986c1..9e36285 100644 --- a/Backend/admin/frontend/assets/css/components/modals.css +++ b/Backend/admin/frontend/assets/css/components/modals.css @@ -91,11 +91,15 @@ .modal-footer { display: flex; justify-content: flex-end; - gap: var(--space-lg); + gap: 8px; padding: 20px var(--space-lg); border-top: 1px solid var(--glass-border); } +.modal-footer .action-btn { + margin: 0; +} + /* Notification Modal */ .notification { position: fixed; diff --git a/Backend/admin/frontend/assets/css/layout/container.css b/Backend/admin/frontend/assets/css/layout/container.css index f591d30..e68c91a 100644 --- a/Backend/admin/frontend/assets/css/layout/container.css +++ b/Backend/admin/frontend/assets/css/layout/container.css @@ -48,7 +48,7 @@ /* Footer */ .footer { - margin-top: var(--space-3xl); + margin-top: var(--space-lg); padding: 20px; text-align: center; color: var(--text-muted); diff --git a/Backend/admin/frontend/assets/css/main.css b/Backend/admin/frontend/assets/css/main.css index 2d062a2..b996de6 100644 --- a/Backend/admin/frontend/assets/css/main.css +++ b/Backend/admin/frontend/assets/css/main.css @@ -23,4 +23,5 @@ /* 4. Pages */ @import 'pages/dashboard.css'; @import 'pages/vaccess.css'; +@import 'pages/site-creator.css'; diff --git a/Backend/admin/frontend/assets/css/pages/site-creator.css b/Backend/admin/frontend/assets/css/pages/site-creator.css new file mode 100644 index 0000000..53725c7 --- /dev/null +++ b/Backend/admin/frontend/assets/css/pages/site-creator.css @@ -0,0 +1,272 @@ +/* ============================================ + Site Creator Page + Страница создания нового сайта + ============================================ */ + +/* Form Section */ +.form-section { + padding: var(--space-xl); + background: rgba(139, 92, 246, 0.02); + border-radius: var(--radius-xl); + border: 1px solid var(--glass-border); + transition: all var(--transition-base); +} + +.form-section:hover { + background: rgba(139, 92, 246, 0.04); + border-color: rgba(139, 92, 246, 0.2); +} + +/* Subsection Title (внутри формы) */ +.form-subsection-title { + font-size: var(--text-lg); + font-weight: var(--font-semibold); + color: var(--text-primary); + margin: 0 0 var(--space-lg) 0; + display: flex; + align-items: center; + gap: var(--space-md); + padding-bottom: var(--space-sm); + border-bottom: 1px solid rgba(139, 92, 246, 0.1); + + i { + color: var(--accent-purple-light); + font-size: 16px; + } +} + +/* Второй и последующие подзаголовки - добавляем отступ сверху */ +.form-subsection-title:not(:first-of-type) { + margin-top: var(--space-xl); +} + +/* Custom Select Styling */ +.form-input[type="text"], +.form-input[type="number"] { + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + background-image: none; +} + +/* Кастомный Select */ +.custom-select { + position: relative; + width: 100%; +} + +.custom-select-trigger { + padding: 10px 40px 10px 14px; + background: var(--glass-bg-dark); + border: 1px solid var(--glass-border); + border-radius: var(--radius-md); + color: var(--text-primary); + font-size: var(--text-base); + cursor: pointer; + transition: all var(--transition-base); + display: flex; + align-items: center; + justify-content: space-between; + user-select: none; +} + +.custom-select-trigger:hover { + border-color: rgba(139, 92, 246, 0.4); + background-color: rgba(139, 92, 246, 0.05); +} + +.custom-select.open .custom-select-trigger { + border-color: rgba(139, 92, 246, 0.6); + box-shadow: 0 0 16px rgba(139, 92, 246, 0.2); + background-color: rgba(139, 92, 246, 0.03); +} + +.custom-select-value { + flex: 1; +} + +.custom-select-arrow { + color: var(--accent-purple-light); + font-size: 12px; + transition: transform var(--transition-base); +} + +.custom-select.open .custom-select-arrow { + transform: rotate(180deg); +} + +/* Dropdown */ +.custom-select-dropdown { + position: absolute; + top: calc(100% + 4px); + left: 0; + right: 0; + background: #1a1d2e; + border: 1px solid rgba(139, 92, 246, 0.3); + border-radius: var(--radius-md); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); + max-height: 0; + overflow: hidden; + opacity: 0; + transform: translateY(-10px); + transition: all var(--transition-base); + z-index: 1000; +} + +.custom-select.open .custom-select-dropdown { + max-height: 300px; + overflow-y: auto; + opacity: 1; + transform: translateY(0); +} + +/* Scrollbar для dropdown */ +.custom-select-dropdown::-webkit-scrollbar { + width: 6px; +} + +.custom-select-dropdown::-webkit-scrollbar-track { + background: transparent; +} + +.custom-select-dropdown::-webkit-scrollbar-thumb { + background: rgba(139, 92, 246, 0.3); + border-radius: 3px; +} + +.custom-select-dropdown::-webkit-scrollbar-thumb:hover { + background: rgba(139, 92, 246, 0.5); +} + +/* Option */ +.custom-select-option { + padding: 10px 14px; + color: var(--text-secondary); + cursor: pointer; + transition: all var(--transition-base); + font-size: var(--text-base); +} + +.custom-select-option:hover { + background: rgba(139, 92, 246, 0.1); + color: var(--text-primary); +} + +.custom-select-option.selected { + background: rgba(139, 92, 246, 0.2); + color: var(--accent-purple-light); + font-weight: var(--font-semibold); +} + +.custom-select-option.selected::before { + content: '✓ '; + margin-right: 8px; +} + +/* File Upload Styling */ +.file-upload-wrapper { + position: relative; + display: inline-block; + width: 100%; +} + +.file-input { + position: absolute; + opacity: 0; + width: 0; + height: 0; + pointer-events: none; +} + +.file-upload-btn { + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-md); + padding: 12px 20px; + background: var(--glass-bg-dark); + border: 2px dashed var(--glass-border); + border-radius: var(--radius-md); + color: var(--text-secondary); + font-size: var(--text-base); + font-weight: var(--font-medium); + cursor: pointer; + transition: all var(--transition-base); + text-align: center; + + i { + color: var(--accent-purple-light); + font-size: 18px; + } + + &:hover { + background: rgba(139, 92, 246, 0.05); + border-color: rgba(139, 92, 246, 0.4); + color: var(--text-primary); + transform: translateY(-1px); + } + + &:active { + transform: translateY(0); + } +} + +.file-input:focus + .file-upload-btn { + border-color: rgba(139, 92, 246, 0.6); + box-shadow: 0 0 16px rgba(139, 92, 246, 0.2); +} + +/* Drag over state */ +.file-upload-wrapper.drag-over .file-upload-btn { + background: rgba(139, 92, 246, 0.15); + border-color: rgba(139, 92, 246, 0.7); + border-style: solid; + color: var(--text-primary); + transform: scale(1.02); + box-shadow: 0 0 20px rgba(139, 92, 246, 0.3); +} + +.file-upload-wrapper.drag-over .file-upload-btn i { + animation: bounce 0.6s ease infinite; +} + +@keyframes bounce { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-4px); + } +} + +/* File uploaded state */ +.file-uploaded { + border-style: solid; + border-color: rgba(16, 185, 129, 0.5); + background: rgba(16, 185, 129, 0.05); + + &:hover { + border-color: rgba(16, 185, 129, 0.6); + background: rgba(16, 185, 129, 0.1); + } + + i { + color: var(--accent-green); + } +} + +/* File status */ +#certFileStatus, +#keyFileStatus, +#caFileStatus { + font-size: var(--text-sm); + font-weight: var(--font-medium); + display: flex; + align-items: center; + gap: var(--space-sm); + + i { + font-size: 14px; + } +} + diff --git a/Backend/admin/frontend/assets/js/api/wails.js b/Backend/admin/frontend/assets/js/api/wails.js index 4e98181..1cd4ed3 100644 --- a/Backend/admin/frontend/assets/js/api/wails.js +++ b/Backend/admin/frontend/assets/js/api/wails.js @@ -136,6 +136,58 @@ class WailsAPI { log(`Ошибка открытия папки: ${error.message}`, 'error'); } } + + /** + * Создать новый сайт + */ + async createNewSite(siteJSON) { + if (!this.checkAvailability()) return 'Error: API недоступен'; + try { + return await window.go.admin.App.CreateNewSite(siteJSON); + } catch (error) { + log(`Ошибка создания сайта: ${error.message}`, 'error'); + return `Error: ${error.message}`; + } + } + + /** + * Загрузить сертификат для сайта + */ + async uploadCertificate(host, certType, certDataBase64) { + if (!this.checkAvailability()) return 'Error: API недоступен'; + try { + return await window.go.admin.App.UploadCertificate(host, certType, certDataBase64); + } catch (error) { + log(`Ошибка загрузки сертификата: ${error.message}`, 'error'); + return `Error: ${error.message}`; + } + } + + /** + * Перезагрузить SSL сертификаты + */ + async reloadSSLCertificates() { + if (!this.checkAvailability()) return 'Error: API недоступен'; + try { + return await window.go.admin.App.ReloadSSLCertificates(); + } catch (error) { + log(`Ошибка перезагрузки сертификатов: ${error.message}`, 'error'); + return `Error: ${error.message}`; + } + } + + /** + * Удалить сайт + */ + async deleteSite(host) { + if (!this.checkAvailability()) return 'Error: API недоступен'; + try { + return await window.go.admin.App.DeleteSite(host); + } catch (error) { + log(`Ошибка удаления сайта: ${error.message}`, 'error'); + return `Error: ${error.message}`; + } + } } // Экспортируем единственный экземпляр diff --git a/Backend/admin/frontend/assets/js/components/site-creator.js b/Backend/admin/frontend/assets/js/components/site-creator.js new file mode 100644 index 0000000..039fee1 --- /dev/null +++ b/Backend/admin/frontend/assets/js/components/site-creator.js @@ -0,0 +1,393 @@ +/* ============================================ + Site Creator Component + Управление созданием новых сайтов + ============================================ */ + +import { api } from '../api/wails.js'; +import { configAPI } from '../api/config.js'; +import { $, hide, show } from '../utils/dom.js'; +import { notification } from '../ui/notification.js'; +import { isWailsAvailable } from '../utils/helpers.js'; +import { initCustomSelects } from '../ui/custom-select.js'; + +/** + * Класс для создания новых сайтов + */ +export class SiteCreator { + constructor() { + this.aliases = []; + this.certificates = { + certificate: null, + privatekey: null, + cabundle: null + }; + } + + /** + * Открыть страницу создания сайта + */ + open() { + // Скрываем все секции + this.hideAllSections(); + + // Показываем страницу создания + show($('sectionAddSite')); + + // Очищаем форму + this.resetForm(); + + // Привязываем обработчики + this.attachEventListeners(); + + // Инициализируем кастомные select'ы + setTimeout(() => initCustomSelects(), 100); + } + + /** + * Скрыть все секции + */ + hideAllSections() { + hide($('sectionServices')); + hide($('sectionSites')); + hide($('sectionProxy')); + hide($('sectionSettings')); + hide($('sectionVAccessEditor')); + hide($('sectionAddSite')); + } + + /** + * Вернуться на главную + */ + backToMain() { + this.hideAllSections(); + show($('sectionServices')); + show($('sectionSites')); + show($('sectionProxy')); + } + + /** + * Очистить форму + */ + resetForm() { + $('newSiteName').value = ''; + $('newSiteHost').value = ''; + $('newSiteAliasInput').value = ''; + $('newSiteRootFile').value = 'index.html'; + $('newSiteStatus').value = 'active'; + $('newSiteRouting').checked = true; + $('certMode').value = 'none'; + + this.aliases = []; + this.certificates = { + certificate: null, + privatekey: null, + cabundle: null + }; + + // Скрываем блок загрузки сертификатов + hide($('certUploadBlock')); + + // Очищаем статусы файлов + $('certFileStatus').innerHTML = ''; + $('keyFileStatus').innerHTML = ''; + $('caFileStatus').innerHTML = ''; + + // Очищаем labels файлов + if ($('certFileName')) $('certFileName').textContent = 'Выберите файл...'; + if ($('keyFileName')) $('keyFileName').textContent = 'Выберите файл...'; + if ($('caFileName')) $('caFileName').textContent = 'Выберите файл...'; + + // Очищаем input файлов + if ($('certFile')) $('certFile').value = ''; + if ($('keyFile')) $('keyFile').value = ''; + if ($('caFile')) $('caFile').value = ''; + + // Убираем класс uploaded + const labels = document.querySelectorAll('.file-upload-btn'); + labels.forEach(label => label.classList.remove('file-uploaded')); + } + + /** + * Привязать обработчики событий + */ + attachEventListeners() { + const createBtn = $('createSiteBtn'); + if (createBtn) { + createBtn.onclick = async () => await this.createSite(); + } + + // Drag & Drop для файлов сертификатов + this.setupDragAndDrop(); + } + + /** + * Настроить Drag & Drop для файлов + */ + setupDragAndDrop() { + const fileWrappers = [ + { wrapper: document.querySelector('label[for="certFile"]')?.parentElement, input: $('certFile'), type: 'certificate' }, + { wrapper: document.querySelector('label[for="keyFile"]')?.parentElement, input: $('keyFile'), type: 'privatekey' }, + { wrapper: document.querySelector('label[for="caFile"]')?.parentElement, input: $('caFile'), type: 'cabundle' } + ]; + + fileWrappers.forEach(({ wrapper, input, type }) => { + if (!wrapper || !input) return; + + // Предотвращаем стандартное поведение + ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { + wrapper.addEventListener(eventName, (e) => { + e.preventDefault(); + e.stopPropagation(); + }); + }); + + // Подсветка при наведении файла + ['dragenter', 'dragover'].forEach(eventName => { + wrapper.addEventListener(eventName, () => { + wrapper.classList.add('drag-over'); + }); + }); + + ['dragleave', 'drop'].forEach(eventName => { + wrapper.addEventListener(eventName, () => { + wrapper.classList.remove('drag-over'); + }); + }); + + // Обработка dropped файла + wrapper.addEventListener('drop', (e) => { + const files = e.dataTransfer.files; + if (files.length > 0) { + // Создаём объект DataTransfer и присваиваем файлы input'у + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(files[0]); + input.files = dataTransfer.files; + + // Триггерим событие change + const event = new Event('change', { bubbles: true }); + input.dispatchEvent(event); + } + }); + }); + } + + /** + * Парсить aliases из строки (через запятую) + */ + parseAliases() { + const input = $('newSiteAliasInput'); + const value = input?.value.trim(); + + if (!value) { + this.aliases = []; + return; + } + + // Разделяем по запятой и очищаем + this.aliases = value + .split(',') + .map(alias => alias.trim()) + .filter(alias => alias.length > 0); + } + + /** + * Переключить видимость блока загрузки сертификатов + */ + toggleCertUpload() { + const mode = $('certMode')?.value; + const block = $('certUploadBlock'); + + if (mode === 'upload') { + show(block); + } else { + hide(block); + } + } + + /** + * Обработать выбор файла сертификата + */ + handleCertFile(input, certType) { + const file = input.files[0]; + const statusId = certType === 'certificate' ? 'certFileStatus' : + certType === 'privatekey' ? 'keyFileStatus' : 'caFileStatus'; + const labelId = certType === 'certificate' ? 'certFileName' : + certType === 'privatekey' ? 'keyFileName' : 'caFileName'; + + const statusDiv = $(statusId); + const labelSpan = $(labelId); + const labelBtn = input.nextElementSibling; // label элемент + + if (!file) { + this.certificates[certType] = null; + statusDiv.innerHTML = ''; + if (labelSpan) labelSpan.textContent = 'Выберите файл...'; + if (labelBtn) labelBtn.classList.remove('file-uploaded'); + return; + } + + // Проверяем размер файла (макс 1MB) + if (file.size > 1024 * 1024) { + statusDiv.innerHTML = ' Файл слишком большой (макс 1MB)'; + this.certificates[certType] = null; + input.value = ''; + if (labelSpan) labelSpan.textContent = 'Выберите файл...'; + if (labelBtn) labelBtn.classList.remove('file-uploaded'); + return; + } + + // Обновляем UI + if (labelSpan) labelSpan.textContent = file.name; + if (labelBtn) labelBtn.classList.add('file-uploaded'); + + // Читаем файл + const reader = new FileReader(); + reader.onload = (e) => { + const content = e.target.result; + // Сохраняем как base64 + this.certificates[certType] = btoa(content); + statusDiv.innerHTML = ` Загружен успешно`; + }; + reader.onerror = () => { + statusDiv.innerHTML = ' Ошибка чтения файла'; + this.certificates[certType] = null; + if (labelSpan) labelSpan.textContent = 'Выберите файл...'; + if (labelBtn) labelBtn.classList.remove('file-uploaded'); + }; + reader.readAsText(file); + } + + /** + * Валидация формы + */ + validateForm() { + const name = $('newSiteName')?.value.trim(); + const host = $('newSiteHost')?.value.trim(); + const rootFile = $('newSiteRootFile')?.value; + const certMode = $('certMode')?.value; + + if (!name) { + notification.error('❌ Укажите название сайта'); + return false; + } + + if (!host) { + notification.error('❌ Укажите host (домен)'); + return false; + } + + if (!rootFile) { + notification.error('❌ Укажите root файл'); + return false; + } + + // Проверка сертификатов если режим загрузки + if (certMode === 'upload') { + if (!this.certificates.certificate) { + notification.error('❌ Загрузите файл certificate.crt'); + return false; + } + if (!this.certificates.privatekey) { + notification.error('❌ Загрузите файл private.key'); + return false; + } + } + + return true; + } + + /** + * Создать сайт + */ + async createSite() { + if (!this.validateForm()) { + return; + } + + if (!isWailsAvailable()) { + notification.error('Wails API недоступен'); + return; + } + + const createBtn = $('createSiteBtn'); + const originalText = createBtn.querySelector('span').textContent; + + try { + createBtn.disabled = true; + createBtn.querySelector('span').textContent = 'Создание...'; + + // Парсим aliases из поля ввода + this.parseAliases(); + + // Собираем данные сайта + const siteData = { + name: $('newSiteName').value.trim(), + host: $('newSiteHost').value.trim(), + alias: this.aliases, + status: $('newSiteStatus').value, + root_file: $('newSiteRootFile').value, + root_file_routing: $('newSiteRouting').checked + }; + + // Создаём сайт + const siteJSON = JSON.stringify(siteData); + const result = await api.createNewSite(siteJSON); + + if (result.startsWith('Error')) { + notification.error(result, 3000); + return; + } + + notification.success('✅ Сайт успешно создан!', 1500); + + // Загружаем сертификаты если нужно + const certMode = $('certMode').value; + if (certMode === 'upload') { + createBtn.querySelector('span').textContent = 'Загрузка сертификатов...'; + + // Загружаем certificate + if (this.certificates.certificate) { + await api.uploadCertificate(siteData.host, 'certificate', this.certificates.certificate); + } + + // Загружаем private key + if (this.certificates.privatekey) { + await api.uploadCertificate(siteData.host, 'privatekey', this.certificates.privatekey); + } + + // Загружаем ca bundle если есть + if (this.certificates.cabundle) { + await api.uploadCertificate(siteData.host, 'cabundle', this.certificates.cabundle); + } + + notification.success('🔒 Сертификаты загружены!', 1500); + } + + // Перезапускаем HTTP/HTTPS + createBtn.querySelector('span').textContent = 'Перезапуск серверов...'; + await configAPI.stopHTTPService(); + await configAPI.stopHTTPSService(); + await new Promise(resolve => setTimeout(resolve, 500)); + await configAPI.startHTTPService(); + await configAPI.startHTTPSService(); + + notification.success('🚀 Серверы перезапущены! Сайт готов к работе!', 2000); + + // Возвращаемся на главную + setTimeout(() => { + this.backToMain(); + // Перезагружаем список сайтов + if (window.sitesManager) { + window.sitesManager.load(); + } + }, 1000); + + } catch (error) { + notification.error('Ошибка: ' + error.message, 3000); + } finally { + createBtn.disabled = false; + createBtn.querySelector('span').textContent = originalText; + } + } +} + diff --git a/Backend/admin/frontend/assets/js/main.js b/Backend/admin/frontend/assets/js/main.js index 45b0d84..79239be 100644 --- a/Backend/admin/frontend/assets/js/main.js +++ b/Backend/admin/frontend/assets/js/main.js @@ -12,7 +12,10 @@ import { ServicesManager } from './components/services.js'; import { SitesManager } from './components/sites.js'; import { ProxyManager } from './components/proxy.js'; import { VAccessManager } from './components/vaccess.js'; +import { SiteCreator } from './components/site-creator.js'; +import { api } from './api/wails.js'; import { configAPI } from './api/config.js'; +import { initCustomSelects } from './ui/custom-select.js'; import { $ } from './utils/dom.js'; /** @@ -26,6 +29,7 @@ class App { this.sitesManager = new SitesManager(); this.proxyManager = new ProxyManager(); this.vAccessManager = new VAccessManager(); + this.siteCreator = new SiteCreator(); this.isWails = isWailsAvailable(); @@ -69,6 +73,9 @@ class App { // Привязываем кнопки this.setupButtons(); + // Инициализируем кастомные select'ы + initCustomSelects(); + log('Приложение запущено'); } @@ -96,6 +103,14 @@ class App { * Привязать кнопки */ setupButtons() { + // Кнопка добавления сайта + const addSiteBtn = $('addSiteBtn'); + if (addSiteBtn) { + addSiteBtn.addEventListener('click', () => { + this.siteCreator.open(); + }); + } + // Кнопка сохранения настроек const saveSettingsBtn = $('saveSettingsBtn'); if (saveSettingsBtn) { @@ -128,6 +143,23 @@ class App { * Настроить глобальные обработчики */ setupGlobalHandlers() { + // Глобальная ссылка на sitesManager + window.sitesManager = this.sitesManager; + window.siteCreator = this.siteCreator; + + // Для SiteCreator + window.backToMainFromAddSite = () => { + this.siteCreator.backToMain(); + }; + + window.toggleCertUpload = () => { + this.siteCreator.toggleCertUpload(); + }; + + window.handleCertFileSelect = (input, certType) => { + this.siteCreator.handleCertFile(input, certType); + }; + // Для vAccess window.editVAccess = (host, isProxy) => { this.vAccessManager.open(host, isProxy); @@ -248,6 +280,10 @@ class App { window.openSiteFolder = async (host) => { await this.sitesManager.handleAction('open-folder', { getAttribute: () => host }); }; + + window.deleteSiteConfirm = async () => { + await this.deleteSiteConfirm(); + }; } /** @@ -382,6 +418,9 @@ class App { modal.open('Редактировать сайт', content); window.currentEditType = 'site'; window.currentEditIndex = index; + + // Добавляем кнопку удаления в футер модального окна + this.addDeleteButtonToModal(); } /** @@ -446,6 +485,9 @@ class App { modal.open('Редактировать прокси', content); window.currentEditType = 'proxy'; window.currentEditIndex = index; + + // Убираем кнопку удаления (для прокси не нужна) + this.removeDeleteButtonFromModal(); } /** @@ -574,6 +616,94 @@ class App { modal.close(); } } + + /** + * Добавить кнопку удаления в модальное окно + */ + addDeleteButtonToModal() { + const footer = document.querySelector('.modal-footer'); + if (!footer) return; + + // Удаляем старую кнопку удаления если есть + const oldDeleteBtn = footer.querySelector('#modalDeleteBtn'); + if (oldDeleteBtn) oldDeleteBtn.remove(); + + // Создаём кнопку удаления + const deleteBtn = document.createElement('button'); + deleteBtn.className = 'action-btn delete-btn'; + deleteBtn.id = 'modalDeleteBtn'; + deleteBtn.innerHTML = ` + + Удалить сайт + `; + deleteBtn.onclick = () => this.deleteSiteConfirm(); + + // Вставляем перед кнопкой "Отмена" + const cancelBtn = footer.querySelector('#modalCancelBtn'); + if (cancelBtn) { + footer.insertBefore(deleteBtn, cancelBtn); + } + } + + /** + * Удалить кнопку удаления из модального окна + */ + removeDeleteButtonFromModal() { + const deleteBtn = document.querySelector('#modalDeleteBtn'); + if (deleteBtn) deleteBtn.remove(); + } + + /** + * Подтверждение удаления сайта + */ + async deleteSiteConfirm() { + const index = window.currentEditIndex; + const site = this.sitesManager.sitesData[index]; + if (!site) return; + + // Подтверждение + const confirmed = confirm( + `⚠️ ВНИМАНИЕ!\n\n` + + `Вы действительно хотите удалить сайт "${site.name}" (${site.host})?\n\n` + + `Будут удалены:\n` + + `• Папка сайта: WebServer/www/${site.host}/\n` + + `• SSL сертификаты (если есть)\n` + + `• Запись в конфигурации\n\n` + + `Это действие НЕОБРАТИМО!` + ); + + if (!confirmed) return; + + try { + notification.show('Удаление сайта...', 'info', 1000); + + const result = await api.deleteSite(site.host); + + if (result.startsWith('Error')) { + notification.error(result, 3000); + return; + } + + notification.success('✅ Сайт успешно удалён!', 1500); + + // Перезапускаем HTTP/HTTPS + notification.show('Перезапуск серверов...', 'success', 800); + await configAPI.stopHTTPService(); + await configAPI.stopHTTPSService(); + await sleep(500); + await configAPI.startHTTPService(); + await configAPI.startHTTPSService(); + + notification.success('🚀 Серверы перезапущены!', 1000); + + // Закрываем модальное окно и обновляем список + modal.close(); + await this.sitesManager.load(); + + } catch (error) { + notification.error('Ошибка: ' + error.message, 3000); + } + } } // Инициализация приложения при загрузке DOM diff --git a/Backend/admin/frontend/assets/js/ui/custom-select.js b/Backend/admin/frontend/assets/js/ui/custom-select.js new file mode 100644 index 0000000..1dc5cdf --- /dev/null +++ b/Backend/admin/frontend/assets/js/ui/custom-select.js @@ -0,0 +1,140 @@ +/* ============================================ + Custom Select Component + Кастомные выпадающие списки + ============================================ */ + +import { $ } from '../utils/dom.js'; + +/** + * Инициализация всех кастомных select'ов на странице + */ +export function initCustomSelects() { + const selects = document.querySelectorAll('select.form-input'); + selects.forEach(select => { + if (!select.dataset.customized) { + createCustomSelect(select); + } + }); +} + +/** + * Создать кастомный select из нативного + */ +function createCustomSelect(selectElement) { + // Помечаем как обработанный + selectElement.dataset.customized = 'true'; + + // Создаём контейнер + const wrapper = document.createElement('div'); + wrapper.className = 'custom-select'; + + // Получаем выбранное значение + const selectedOption = selectElement.options[selectElement.selectedIndex]; + const selectedText = selectedOption ? selectedOption.text : ''; + + // Создаём кнопку (видимая часть) + const button = document.createElement('div'); + button.className = 'custom-select-trigger'; + button.innerHTML = ` + ${selectedText} + + `; + + // Создаём выпадающий список + const dropdown = document.createElement('div'); + dropdown.className = 'custom-select-dropdown'; + + // Заполняем опции + Array.from(selectElement.options).forEach((option, index) => { + const item = document.createElement('div'); + item.className = 'custom-select-option'; + item.textContent = option.text; + item.dataset.value = option.value; + item.dataset.index = index; + + if (option.selected) { + item.classList.add('selected'); + } + + // Клик по опции + item.addEventListener('click', () => { + selectOption(selectElement, wrapper, item, index); + }); + + dropdown.appendChild(item); + }); + + // Клик по кнопке - открыть/закрыть + button.addEventListener('click', (e) => { + e.stopPropagation(); + toggleDropdown(wrapper); + }); + + // Собираем вместе + wrapper.appendChild(button); + wrapper.appendChild(dropdown); + + // Скрываем оригинальный select + selectElement.style.display = 'none'; + + // Вставляем кастомный select после оригинального + selectElement.parentNode.insertBefore(wrapper, selectElement.nextSibling); + + // Закрываем при клике вне + document.addEventListener('click', (e) => { + if (!wrapper.contains(e.target)) { + closeDropdown(wrapper); + } + }); +} + +/** + * Открыть/закрыть dropdown + */ +function toggleDropdown(wrapper) { + const isOpen = wrapper.classList.contains('open'); + + // Закрываем все открытые + document.querySelectorAll('.custom-select.open').forEach(el => { + el.classList.remove('open'); + }); + + if (!isOpen) { + wrapper.classList.add('open'); + } +} + +/** + * Закрыть dropdown + */ +function closeDropdown(wrapper) { + wrapper.classList.remove('open'); +} + +/** + * Выбрать опцию + */ +function selectOption(selectElement, wrapper, optionElement, index) { + // Обновляем оригинальный select + selectElement.selectedIndex = index; + + // Триггерим событие change + const event = new Event('change', { bubbles: true }); + selectElement.dispatchEvent(event); + + // Обновляем UI + const valueSpan = wrapper.querySelector('.custom-select-value'); + valueSpan.textContent = optionElement.textContent; + + // Убираем selected у всех опций + wrapper.querySelectorAll('.custom-select-option').forEach(opt => { + opt.classList.remove('selected'); + }); + + // Добавляем selected к выбранной + optionElement.classList.add('selected'); + + // Закрываем dropdown + closeDropdown(wrapper); +} + diff --git a/Backend/admin/frontend/assets/js/ui/navigation.js b/Backend/admin/frontend/assets/js/ui/navigation.js index 3b3e478..df80919 100644 --- a/Backend/admin/frontend/assets/js/ui/navigation.js +++ b/Backend/admin/frontend/assets/js/ui/navigation.js @@ -16,7 +16,8 @@ export class Navigation { sites: $('sectionSites'), proxy: $('sectionProxy'), settings: $('sectionSettings'), - vaccess: $('sectionVAccessEditor') + vaccess: $('sectionVAccessEditor'), + addSite: $('sectionAddSite') }; this.init(); } diff --git a/Backend/admin/frontend/index.html b/Backend/admin/frontend/index.html index 34ad86c..df23220 100644 --- a/Backend/admin/frontend/index.html +++ b/Backend/admin/frontend/index.html @@ -129,7 +129,13 @@
-

Список сайтов

+
+

Список сайтов

+ +
@@ -481,6 +487,160 @@ + +
diff --git a/Backend/admin/frontend/wailsjs/wailsjs/go/admin/App.d.ts b/Backend/admin/frontend/wailsjs/wailsjs/go/admin/App.d.ts index 5317185..47deaef 100644 --- a/Backend/admin/frontend/wailsjs/wailsjs/go/admin/App.d.ts +++ b/Backend/admin/frontend/wailsjs/wailsjs/go/admin/App.d.ts @@ -7,6 +7,10 @@ import {vaccess} from '../models'; export function CheckServicesReady():Promise; +export function CreateNewSite(arg1:string):Promise; + +export function DeleteSite(arg1:string):Promise; + export function DisableProxyService():Promise; export function EnableProxyService():Promise; @@ -25,6 +29,8 @@ export function OpenSiteFolder(arg1:string):Promise; export function ReloadConfig():Promise; +export function ReloadSSLCertificates():Promise; + export function RestartAllServices():Promise; export function SaveConfig(arg1:string):Promise; @@ -52,3 +58,5 @@ export function StopPHPService():Promise; export function StopServer():Promise; export function UpdateSiteCache():Promise; + +export function UploadCertificate(arg1:string,arg2:string,arg3:string):Promise; diff --git a/Backend/admin/frontend/wailsjs/wailsjs/go/admin/App.js b/Backend/admin/frontend/wailsjs/wailsjs/go/admin/App.js index b83a9b7..6e0e4cb 100644 --- a/Backend/admin/frontend/wailsjs/wailsjs/go/admin/App.js +++ b/Backend/admin/frontend/wailsjs/wailsjs/go/admin/App.js @@ -6,6 +6,14 @@ export function CheckServicesReady() { return window['go']['admin']['App']['CheckServicesReady'](); } +export function CreateNewSite(arg1) { + return window['go']['admin']['App']['CreateNewSite'](arg1); +} + +export function DeleteSite(arg1) { + return window['go']['admin']['App']['DeleteSite'](arg1); +} + export function DisableProxyService() { return window['go']['admin']['App']['DisableProxyService'](); } @@ -42,6 +50,10 @@ export function ReloadConfig() { return window['go']['admin']['App']['ReloadConfig'](); } +export function ReloadSSLCertificates() { + return window['go']['admin']['App']['ReloadSSLCertificates'](); +} + export function RestartAllServices() { return window['go']['admin']['App']['RestartAllServices'](); } @@ -97,3 +109,7 @@ export function StopServer() { export function UpdateSiteCache() { return window['go']['admin']['App']['UpdateSiteCache'](); } + +export function UploadCertificate(arg1, arg2, arg3) { + return window['go']['admin']['App']['UploadCertificate'](arg1, arg2, arg3); +} diff --git a/Backend/admin/go/admin.go b/Backend/admin/go/admin.go index 0cd1c23..3346463 100644 --- a/Backend/admin/go/admin.go +++ b/Backend/admin/go/admin.go @@ -318,3 +318,49 @@ func (a *App) UpdateSiteCache() string { webserver.UpdateSiteStatusCache() return "Cache updated" } + +func (a *App) CreateNewSite(siteJSON string) string { + var siteData sites.SiteInfo + err := json.Unmarshal([]byte(siteJSON), &siteData) + if err != nil { + return "Error: Invalid JSON - " + err.Error() + } + + err = sites.CreateNewSite(siteData) + if err != nil { + return "Error: " + err.Error() + } + + config.LoadConfig() + return "Site created successfully" +} + +func (a *App) UploadCertificate(host, certType, certDataBase64 string) string { + certData, err := tools.DecodeBase64(certDataBase64) + if err != nil { + return "Error: Invalid base64 data - " + err.Error() + } + + err = sites.UploadSiteCertificate(host, certType, certData) + if err != nil { + return "Error: " + err.Error() + } + + webserver.ReloadCertificates() + return "Certificate uploaded successfully" +} + +func (a *App) ReloadSSLCertificates() string { + webserver.ReloadCertificates() + return "SSL certificates reloaded" +} + +func (a *App) DeleteSite(host string) string { + err := sites.DeleteSite(host) + if err != nil { + return "Error: " + err.Error() + } + + config.LoadConfig() + return "Site deleted successfully" +} \ No newline at end of file diff --git a/Backend/admin/go/sites/methods.go b/Backend/admin/go/sites/methods.go new file mode 100644 index 0000000..5d99a05 --- /dev/null +++ b/Backend/admin/go/sites/methods.go @@ -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) +} diff --git a/Backend/admin/go/sites/templates/index.tmpl b/Backend/admin/go/sites/templates/index.tmpl new file mode 100644 index 0000000..c05cad3 --- /dev/null +++ b/Backend/admin/go/sites/templates/index.tmpl @@ -0,0 +1,209 @@ + + + + + + {{.Host}} - Добро пожаловать! + + + +
+
+
+
+
+
+
+
+ +
+ +
Добро пожаловать!
+
+ Ваш сайт успешно создан и готов к работе.
+ Начните разработку, заменив этот файл своим контентом. +
+
+
+ Сайт работает +
+ + +
+ + diff --git a/Backend/admin/go/vaccess/vaccess.go b/Backend/admin/go/vaccess/vaccess.go index 25c61f7..247ccb7 100644 --- a/Backend/admin/go/vaccess/vaccess.go +++ b/Backend/admin/go/vaccess/vaccess.go @@ -4,8 +4,8 @@ import ( "bufio" "fmt" "os" + "path/filepath" "strings" - tools "vServer/Backend/tools" ) func GetVAccessPath(host string, isProxy bool) string { @@ -18,8 +18,13 @@ func GetVAccessPath(host string, isProxy bool) string { func GetVAccessConfig(host string, isProxy bool) (*VAccessConfig, error) { filePath := GetVAccessPath(host, isProxy) + // Получаем абсолютный путь БЕЗ проверки существования + absPath, err := filepath.Abs(filePath) + if err != nil { + return &VAccessConfig{Rules: []VAccessRule{}}, nil + } + // Проверяем существование файла - absPath, _ := tools.AbsPath(filePath) if _, err := os.Stat(absPath); os.IsNotExist(err) { // Файл не существует - возвращаем пустую конфигурацию return &VAccessConfig{Rules: []VAccessRule{}}, nil @@ -104,11 +109,14 @@ func SaveVAccessConfig(host string, isProxy bool, config *VAccessConfig) error { dir = fmt.Sprintf("WebServer/www/%s", host) } - absDir, _ := tools.AbsPath(dir) + absDir, err := filepath.Abs(dir) + if err != nil { + return err + } os.MkdirAll(absDir, 0755) // Получаем абсолютный путь к файлу - absPath, err := tools.AbsPath(filePath) + absPath, err := filepath.Abs(filePath) if err != nil { return err } diff --git a/Backend/admin/icon_exe.png b/Backend/admin/icon_exe.png deleted file mode 100644 index b98ab7f..0000000 Binary files a/Backend/admin/icon_exe.png and /dev/null differ diff --git a/Backend/tools/FunctionAll.go b/Backend/tools/FunctionAll.go index f0e1200..0347c9f 100644 --- a/Backend/tools/FunctionAll.go +++ b/Backend/tools/FunctionAll.go @@ -1,6 +1,7 @@ package tools import ( + "encoding/base64" "fmt" "net" "time" @@ -74,6 +75,11 @@ func Error_check(err error, message string) bool { return true } - - - +// DecodeBase64 декодирует строку из base64 +func DecodeBase64(encoded string) ([]byte, error) { + decoded, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + return nil, fmt.Errorf("ошибка декодирования base64: %w", err) + } + return decoded, nil +} diff --git a/build_admin.ps1 b/build_admin.ps1 index 9ac64e6..4208740 100644 --- a/build_admin.ps1 +++ b/build_admin.ps1 @@ -61,7 +61,7 @@ Write-Success "Зависимости установлены" Write-ProgressBar 50 Write-Host "" -Write-Step 3 4 "Проверка Wails CLI..." +Write-Step 3 5 "Проверка Wails CLI..." $null = wails version 2>&1 if ($LASTEXITCODE -ne 0) { Write-Info "Установка Wails CLI..." @@ -70,10 +70,17 @@ if ($LASTEXITCODE -ne 0) { } else { Write-Success "Найден" } -Write-ProgressBar 75 +Write-ProgressBar 60 Write-Host "" -Write-Step 4 4 "Сборка приложения..." +Write-Step 4 5 "Генерация биндингов..." +Write-Info "Создание TypeScript/JS биндингов для Go методов..." +wails generate module 2>&1 | Out-Null +Write-Success "Биндинги сгенерированы" +Write-ProgressBar 80 +Write-Host "" + +Write-Step 5 5 "Сборка приложения..." Write-Info "Компиляция (может занять ~10 сек)..." wails build -f admin.go 2>&1 | Out-Null diff --git a/vSerf.exe b/vSerf.exe index 843a5ff..b5ebb5f 100644 Binary files a/vSerf.exe and b/vSerf.exe differ