diff --git a/Backend/WebServer/acme/acme.go b/Backend/WebServer/acme/acme.go index 50d0669..8823377 100644 --- a/Backend/WebServer/acme/acme.go +++ b/Backend/WebServer/acme/acme.go @@ -488,3 +488,80 @@ func getCertDaysLeft(domain string) int { func getCurrentTimestamp() int64 { return time.Now().Unix() } + +// GetCertInfo получает информацию о сертификате для домена +func GetCertInfo(domain string) CertInfo { + certPath := filepath.Join("WebServer/cert", domain, "certificate.crt") + + info := CertInfo{ + Domain: domain, + HasCert: false, + } + + // Проверяем существует ли сертификат + data, err := os.ReadFile(certPath) + if err != nil { + return info + } + + block, _ := pem.Decode(data) + if block == nil { + return info + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return info + } + + info.HasCert = true + info.Issuer = cert.Issuer.CommonName + info.NotBefore = cert.NotBefore.Format("2006-01-02 15:04:05") + info.NotAfter = cert.NotAfter.Format("2006-01-02 15:04:05") + info.DaysLeft = int(time.Until(cert.NotAfter).Hours() / 24) + info.IsExpired = time.Now().After(cert.NotAfter) + info.DNSNames = cert.DNSNames + + return info +} + +// DeleteCertificate удаляет сертификат для домена +func DeleteCertificate(domain string) error { + certDir := filepath.Join("WebServer/cert", domain) + + // Проверяем существует ли директория + if _, err := os.Stat(certDir); os.IsNotExist(err) { + return fmt.Errorf("сертификат для %s не найден", domain) + } + + // Удаляем директорию с сертификатами + err := os.RemoveAll(certDir) + if err != nil { + return fmt.Errorf("ошибка удаления сертификата: %w", err) + } + + tools.Logs_file(0, "ACME", "🗑️ Сертификат удалён для: "+domain, "logs_acme.log", true) + return nil +} + +// GetAllCertsInfo получает информацию о всех сертификатах +func GetAllCertsInfo() []CertInfo { + certs := make([]CertInfo, 0) + certBaseDir := "WebServer/cert" + + entries, err := os.ReadDir(certBaseDir) + if err != nil { + return certs + } + + for _, entry := range entries { + if entry.IsDir() && entry.Name() != "no_cert" && entry.Name() != ".acme" { + info := GetCertInfo(entry.Name()) + if info.HasCert { + certs = append(certs, info) + } + } + } + + return certs +} diff --git a/Backend/WebServer/acme/types.go b/Backend/WebServer/acme/types.go index 49da6ec..39ec216 100644 --- a/Backend/WebServer/acme/types.go +++ b/Backend/WebServer/acme/types.go @@ -25,12 +25,14 @@ type Manager struct { // CertInfo информация о сертификате type CertInfo struct { - Domain string `json:"domain"` - Issuer string `json:"issuer"` - NotBefore string `json:"not_before"` - NotAfter string `json:"not_after"` - DaysLeft int `json:"days_left"` - AutoCreated bool `json:"auto_created"` + Domain string `json:"domain"` + Issuer string `json:"issuer"` + NotBefore string `json:"not_before"` + NotAfter string `json:"not_after"` + DaysLeft int `json:"days_left"` + IsExpired bool `json:"is_expired"` + HasCert bool `json:"has_cert"` + DNSNames []string `json:"dns_names"` } // ObtainResult результат получения сертификата diff --git a/Backend/admin/frontend/assets/css/components/badges.css b/Backend/admin/frontend/assets/css/components/badges.css index ff76cfa..41b40b5 100644 --- a/Backend/admin/frontend/assets/css/components/badges.css +++ b/Backend/admin/frontend/assets/css/components/badges.css @@ -20,21 +20,18 @@ background: linear-gradient(135deg, rgba(16, 185, 129, 0.25), rgba(16, 185, 129, 0.15)); color: var(--accent-green); border: 1px solid rgba(16, 185, 129, 0.4); - box-shadow: 0 0 12px rgba(16, 185, 129, 0.3); } .badge-offline { background: linear-gradient(135deg, rgba(239, 68, 68, 0.25), rgba(239, 68, 68, 0.15)); color: var(--accent-red); border: 1px solid rgba(239, 68, 68, 0.4); - box-shadow: 0 0 12px rgba(239, 68, 68, 0.3); } .badge-pending { background: linear-gradient(135deg, rgba(245, 158, 11, 0.25), rgba(245, 158, 11, 0.15)); color: var(--accent-yellow); border: 1px solid rgba(245, 158, 11, 0.4); - box-shadow: 0 0 12px rgba(245, 158, 11, 0.3); } /* Yes/No Badges */ @@ -55,7 +52,6 @@ width: 7px; height: 7px; border-radius: var(--radius-full); - box-shadow: 0 0 8px currentColor; } .status-online { @@ -85,3 +81,27 @@ } } +/* Certificate Icons */ +.cert-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + margin-right: 8px; + border-radius: var(--radius-sm); + font-size: 12px; +} + +.cert-valid { + background: rgba(16, 185, 129, 0.2); + color: var(--accent-green); + border: 1px solid rgba(16, 185, 129, 0.4); +} + +.cert-expired { + background: rgba(239, 68, 68, 0.2); + color: var(--accent-red); + border: 1px solid rgba(239, 68, 68, 0.4); +} + diff --git a/Backend/admin/frontend/assets/css/components/buttons.css b/Backend/admin/frontend/assets/css/components/buttons.css index 1697467..cb6b90c 100644 --- a/Backend/admin/frontend/assets/css/components/buttons.css +++ b/Backend/admin/frontend/assets/css/components/buttons.css @@ -71,7 +71,6 @@ &: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); } } @@ -152,7 +151,6 @@ font-weight: var(--font-semibold); cursor: pointer; transition: all var(--transition-base); - box-shadow: var(--shadow-red); &:hover:not(:disabled) { background: linear-gradient(135deg, rgba(239, 68, 68, 0.3), rgba(239, 68, 68, 0.15)); @@ -173,7 +171,6 @@ background: linear-gradient(135deg, rgba(16, 185, 129, 0.2), rgba(16, 185, 129, 0.1)); border-color: rgba(16, 185, 129, 0.4); color: var(--accent-green); - box-shadow: var(--shadow-green); &:hover:not(:disabled) { background: linear-gradient(135deg, rgba(16, 185, 129, 0.3), rgba(16, 185, 129, 0.15)); @@ -211,7 +208,6 @@ background: rgba(16, 185, 129, 0.2); border-color: rgba(16, 185, 129, 0.5); color: var(--accent-green); - box-shadow: 0 0 12px rgba(16, 185, 129, 0.2); } &:last-child.active { @@ -315,7 +311,6 @@ &.active { background: var(--accent-purple); color: white; - box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3); } } diff --git a/Backend/admin/frontend/assets/css/components/forms.css b/Backend/admin/frontend/assets/css/components/forms.css index a2cbcd5..a98e313 100644 --- a/Backend/admin/frontend/assets/css/components/forms.css +++ b/Backend/admin/frontend/assets/css/components/forms.css @@ -24,6 +24,11 @@ gap: var(--space-md); } +/* Form Row (3 columns) */ +.form-row.form-row-3 { + grid-template-columns: 1fr 1fr 1fr; +} + /* Form Label */ .form-label { font-size: 12px; diff --git a/Backend/admin/frontend/assets/css/layout/header.css b/Backend/admin/frontend/assets/css/layout/header.css index 8507421..416b18e 100644 --- a/Backend/admin/frontend/assets/css/layout/header.css +++ b/Backend/admin/frontend/assets/css/layout/header.css @@ -65,7 +65,6 @@ backdrop-filter: var(--backdrop-blur-light); border-radius: 20px; border: 1px solid rgba(16, 185, 129, 0.3); - box-shadow: var(--shadow-green); } .status-text { diff --git a/Backend/admin/frontend/assets/css/pages/vaccess.css b/Backend/admin/frontend/assets/css/pages/vaccess.css index f40c582..ef21f9d 100644 --- a/Backend/admin/frontend/assets/css/pages/vaccess.css +++ b/Backend/admin/frontend/assets/css/pages/vaccess.css @@ -262,3 +262,207 @@ } } +/* ============================================ + Cert Manager + ============================================ */ + +.cert-manager-content { + display: flex; + flex-direction: column; + gap: var(--space-md); +} + +.cert-card { + background: rgba(139, 92, 246, 0.03); + border: 1px solid var(--glass-border); + border-radius: var(--radius-xl); + padding: var(--space-lg); + transition: all var(--transition-base); +} + +.cert-card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--space-md); + padding-bottom: var(--space-md); + border-bottom: 1px solid var(--glass-border); +} + +.cert-card-title { + display: flex; + align-items: center; + gap: var(--space-md); + + i { + font-size: 24px; + color: var(--accent-green); + } + + h3 { + margin: 0; + font-size: var(--text-xl); + font-weight: var(--font-semibold); + color: var(--text-primary); + } +} + +.cert-card-title.expired i { + color: var(--accent-red); +} + +.cert-card-actions { + display: flex; + gap: var(--space-sm); +} + +.cert-info-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: var(--space-md); +} + +.cert-info-item { + padding: var(--space-md); + background: rgba(255, 255, 255, 0.02); + border-radius: var(--radius-md); + + .cert-info-label { + font-size: var(--text-sm); + color: var(--text-muted); + margin-bottom: var(--space-xs); + } + + .cert-info-value { + font-size: var(--text-md); + color: var(--text-primary); + font-weight: var(--font-medium); + } + + .cert-info-value.valid { + color: var(--accent-green); + } + + .cert-info-value.expired { + color: var(--accent-red); + } +} + +.cert-domains-list { + display: flex; + flex-wrap: wrap; + gap: var(--space-sm); + margin-top: var(--space-md); +} + +.cert-domain-tag { + padding: 4px 12px; + background: rgba(139, 92, 246, 0.15); + border-radius: var(--radius-md); + font-size: var(--text-sm); + color: var(--accent-purple-light); + font-family: var(--font-mono); +} + +.cert-empty { + text-align: center; + padding: 60px 40px; + color: var(--text-muted); + + i { + font-size: 48px; + margin-bottom: var(--space-lg); + opacity: 0.3; + } + + h3 { + font-size: var(--text-xl); + font-weight: var(--font-semibold); + color: var(--text-primary); + margin-bottom: var(--space-sm); + } + + p { + font-size: var(--text-md); + } +} + +/* Wildcard Info Block */ +.cert-wildcard-info { + padding: var(--space-md); + background: rgba(139, 92, 246, 0.05); + border-radius: var(--radius-lg); +} + +.cert-wildcard-header { + display: flex; + align-items: center; + gap: var(--space-sm); + margin-bottom: var(--space-xs); + font-size: var(--text-md); + font-weight: var(--font-medium); + color: var(--accent-purple-light); + + i { + color: var(--accent-purple-light); + } +} + +.cert-wildcard-info > p { + margin: 0 0 var(--space-sm) 0; + color: var(--text-muted); + font-size: var(--text-sm); +} + +.cert-wildcard-list { + display: flex; + flex-direction: column; + gap: var(--space-xs); +} + +.cert-wildcard-item { + display: flex; + align-items: center; + gap: var(--space-md); + padding: var(--space-xs) var(--space-sm); + background: rgba(139, 92, 246, 0.08); + border-radius: var(--radius-sm); +} + +.cert-wildcard-item.expired { + opacity: 0.7; +} + +.cert-wildcard-domain { + font-family: var(--font-mono); + font-size: var(--text-sm); + color: var(--text-primary); +} + +.cert-wildcard-status { + font-size: var(--text-sm); + color: var(--accent-green); +} + +.cert-wildcard-item.expired .cert-wildcard-status { + color: var(--accent-red); +} + +.cert-wildcard-issuer { + font-size: var(--text-sm); + color: var(--text-muted); +} + +/* Card Variants */ +.cert-card-wildcard { + /* без особых стилей */ +} + +.cert-card-local { + opacity: 0.6; +} + +.cert-card-empty { + border-style: dashed; +} + diff --git a/Backend/admin/frontend/assets/js/api/config.js b/Backend/admin/frontend/assets/js/api/config.js index 151cc87..3e2e23f 100644 --- a/Backend/admin/frontend/assets/js/api/config.js +++ b/Backend/admin/frontend/assets/js/api/config.js @@ -49,6 +49,24 @@ class ConfigAPI { } } + // Включить ACME Service + async enableACMEService() { + if (!this.available) return; + try { + await window.go.admin.App.EnableACMEService(); + } catch (error) { + } + } + + // Отключить ACME Service + async disableACMEService() { + if (!this.available) return; + try { + await window.go.admin.App.DisableACMEService(); + } catch (error) { + } + } + // Перезапустить все сервисы async restartAllServices() { if (!this.available) return; diff --git a/Backend/admin/frontend/assets/js/api/wails.js b/Backend/admin/frontend/assets/js/api/wails.js index 6afe9f0..49345d5 100644 --- a/Backend/admin/frontend/assets/js/api/wails.js +++ b/Backend/admin/frontend/assets/js/api/wails.js @@ -145,6 +145,46 @@ class WailsAPI { return `Error: ${error.message}`; } } + + // Получить информацию о сертификате для домена + async getCertInfo(domain) { + if (!this.checkAvailability()) return { has_cert: false }; + try { + return await window.go.admin.App.GetCertInfo(domain); + } catch (error) { + return { has_cert: false }; + } + } + + // Получить информацию о всех сертификатах + async getAllCertsInfo() { + if (!this.checkAvailability()) return []; + try { + return await window.go.admin.App.GetAllCertsInfo(); + } catch (error) { + return []; + } + } + + // Удалить сертификат + async deleteCertificate(domain) { + if (!this.checkAvailability()) return 'Error: API недоступен'; + try { + return await window.go.admin.App.DeleteCertificate(domain); + } catch (error) { + return `Error: ${error.message}`; + } + } + + // Получить SSL сертификат через Let's Encrypt + async obtainSSLCertificate(domain) { + if (!this.checkAvailability()) return 'Error: API недоступен'; + try { + return await window.go.admin.App.ObtainSSLCertificate(domain); + } catch (error) { + return `Error: ${error.message}`; + } + } } // Экспортируем единственный экземпляр diff --git a/Backend/admin/frontend/assets/js/components/proxy-creator.js b/Backend/admin/frontend/assets/js/components/proxy-creator.js new file mode 100644 index 0000000..3bd1c57 --- /dev/null +++ b/Backend/admin/frontend/assets/js/components/proxy-creator.js @@ -0,0 +1,311 @@ +/* ============================================ + Proxy 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 ProxyCreator { + constructor() { + this.certificates = { + certificate: null, + privatekey: null, + cabundle: null + }; + } + + open() { + this.hideAllSections(); + show($('sectionAddProxy')); + this.resetForm(); + this.attachEventListeners(); + setTimeout(() => initCustomSelects(), 100); + } + + hideAllSections() { + hide($('sectionServices')); + hide($('sectionSites')); + hide($('sectionProxy')); + hide($('sectionSettings')); + hide($('sectionVAccessEditor')); + hide($('sectionAddSite')); + hide($('sectionAddProxy')); + } + + backToMain() { + this.hideAllSections(); + show($('sectionServices')); + show($('sectionSites')); + show($('sectionProxy')); + } + + resetForm() { + $('newProxyDomain').value = ''; + $('newProxyLocalAddr').value = '127.0.0.1'; + $('newProxyLocalPort').value = ''; + $('newProxyStatus').value = 'enable'; + $('newProxyServiceHTTPS').checked = false; + $('newProxyAutoHTTPS').checked = true; + $('proxyCertMode').value = 'none'; + + this.certificates = { + certificate: null, + privatekey: null, + cabundle: null + }; + + hide($('proxyCertUploadBlock')); + + $('proxyCertFileStatus').innerHTML = ''; + $('proxyKeyFileStatus').innerHTML = ''; + $('proxyCaFileStatus').innerHTML = ''; + + if ($('proxyCertFileName')) $('proxyCertFileName').textContent = 'Выберите файл...'; + if ($('proxyKeyFileName')) $('proxyKeyFileName').textContent = 'Выберите файл...'; + if ($('proxyCaFileName')) $('proxyCaFileName').textContent = 'Выберите файл...'; + + if ($('proxyCertFile')) $('proxyCertFile').value = ''; + if ($('proxyKeyFile')) $('proxyKeyFile').value = ''; + if ($('proxyCaFile')) $('proxyCaFile').value = ''; + + const labels = document.querySelectorAll('#sectionAddProxy .file-upload-btn'); + labels.forEach(label => label.classList.remove('file-uploaded')); + } + + attachEventListeners() { + const createBtn = $('createProxyBtn'); + if (createBtn) { + createBtn.onclick = async () => await this.createProxy(); + } + + this.setupDragAndDrop(); + } + + setupDragAndDrop() { + const fileWrappers = [ + { wrapper: document.querySelector('label[for="proxyCertFile"]')?.parentElement, input: $('proxyCertFile'), type: 'certificate' }, + { wrapper: document.querySelector('label[for="proxyKeyFile"]')?.parentElement, input: $('proxyKeyFile'), type: 'privatekey' }, + { wrapper: document.querySelector('label[for="proxyCaFile"]')?.parentElement, input: $('proxyCaFile'), 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'); + }); + }); + + wrapper.addEventListener('drop', (e) => { + const files = e.dataTransfer.files; + if (files.length > 0) { + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(files[0]); + input.files = dataTransfer.files; + + const event = new Event('change', { bubbles: true }); + input.dispatchEvent(event); + } + }); + }); + } + + toggleCertUpload() { + const mode = $('proxyCertMode')?.value; + const block = $('proxyCertUploadBlock'); + + if (mode === 'upload') { + show(block); + } else { + hide(block); + } + } + + handleCertFile(input, certType) { + const file = input.files[0]; + const statusId = certType === 'certificate' ? 'proxyCertFileStatus' : + certType === 'privatekey' ? 'proxyKeyFileStatus' : 'proxyCaFileStatus'; + const labelId = certType === 'certificate' ? 'proxyCertFileName' : + certType === 'privatekey' ? 'proxyKeyFileName' : 'proxyCaFileName'; + + const statusDiv = $(statusId); + const labelSpan = $(labelId); + const labelBtn = input.nextElementSibling; + + if (!file) { + this.certificates[certType] = null; + statusDiv.innerHTML = ''; + if (labelSpan) labelSpan.textContent = 'Выберите файл...'; + if (labelBtn) labelBtn.classList.remove('file-uploaded'); + return; + } + + 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; + } + + 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; + 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 domain = $('newProxyDomain')?.value.trim(); + const localAddr = $('newProxyLocalAddr')?.value.trim(); + const localPort = $('newProxyLocalPort')?.value.trim(); + const certMode = $('proxyCertMode')?.value; + + if (!domain) { + notification.error('❌ Укажите внешний домен'); + return false; + } + + if (!localAddr) { + notification.error('❌ Укажите локальный адрес'); + return false; + } + + if (!localPort) { + notification.error('❌ Укажите локальный порт'); + 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 createProxy() { + if (!this.validateForm()) { + return; + } + + if (!isWailsAvailable()) { + notification.error('Wails API недоступен'); + return; + } + + const createBtn = $('createProxyBtn'); + const originalText = createBtn.querySelector('span').textContent; + + try { + createBtn.disabled = true; + createBtn.querySelector('span').textContent = 'Создание...'; + + const certMode = $('proxyCertMode').value; + + const proxyData = { + Enable: $('newProxyStatus').value === 'enable', + ExternalDomain: $('newProxyDomain').value.trim(), + LocalAddress: $('newProxyLocalAddr').value.trim(), + LocalPort: $('newProxyLocalPort').value.trim(), + ServiceHTTPSuse: $('newProxyServiceHTTPS').checked, + AutoHTTPS: $('newProxyAutoHTTPS').checked, + AutoCreateSSL: certMode === 'auto' + }; + + const config = await configAPI.getConfig(); + + if (!config.Proxy_Service) { + config.Proxy_Service = []; + } + + config.Proxy_Service.push(proxyData); + + const result = await configAPI.saveConfig(JSON.stringify(config, null, 4)); + + if (result.startsWith('Error')) { + notification.error(result, 3000); + return; + } + + notification.success('✅ Прокси сервис создан!', 1500); + + if (certMode === 'upload') { + createBtn.querySelector('span').textContent = 'Загрузка сертификатов...'; + + if (this.certificates.certificate) { + await api.uploadCertificate(proxyData.ExternalDomain, 'certificate', this.certificates.certificate); + } + + if (this.certificates.privatekey) { + await api.uploadCertificate(proxyData.ExternalDomain, 'privatekey', this.certificates.privatekey); + } + + if (this.certificates.cabundle) { + await api.uploadCertificate(proxyData.ExternalDomain, 'cabundle', this.certificates.cabundle); + } + + notification.success('🔒 Сертификаты загружены!', 1500); + } + + 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.proxyManager) { + window.proxyManager.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/components/proxy.js b/Backend/admin/frontend/assets/js/components/proxy.js index e7284cc..ea75b62 100644 --- a/Backend/admin/frontend/assets/js/components/proxy.js +++ b/Backend/admin/frontend/assets/js/components/proxy.js @@ -11,6 +11,7 @@ import { $ } from '../utils/dom.js'; export class ProxyManager { constructor() { this.proxiesData = []; + this.certsCache = {}; this.mockData = [ { enable: true, @@ -19,6 +20,7 @@ export class ProxyManager { local_port: '3333', service_https_use: false, auto_https: true, + auto_create_ssl: true, status: 'active' }, { @@ -28,6 +30,7 @@ export class ProxyManager { local_port: '8080', service_https_use: true, auto_https: false, + auto_create_ssl: false, status: 'active' }, { @@ -37,22 +40,89 @@ export class ProxyManager { local_port: '5000', service_https_use: false, auto_https: false, + auto_create_ssl: false, status: 'disabled' } ]; + this.mockCerts = { + 'git.example.ru': { has_cert: true, is_expired: false, days_left: 60 } + }; } // Загрузить список прокси async load() { if (isWailsAvailable()) { this.proxiesData = await api.getProxyList(); + await this.loadCertsInfo(); } else { - // Используем тестовые данные если Wails недоступен this.proxiesData = this.mockData; + this.certsCache = this.mockCerts; } this.render(); } + // Загрузить информацию о сертификатах + async loadCertsInfo() { + const allCerts = await api.getAllCertsInfo(); + this.certsCache = {}; + for (const cert of allCerts) { + this.certsCache[cert.domain] = cert; + } + } + + // Проверить соответствие домена wildcard паттерну + matchesWildcard(domain, pattern) { + if (pattern.startsWith('*.')) { + const wildcardBase = pattern.slice(2); + const domainParts = domain.split('.'); + if (domainParts.length >= 2) { + const domainBase = domainParts.slice(1).join('.'); + return domainBase === wildcardBase; + } + } + return domain === pattern; + } + + // Найти сертификат для домена (включая wildcard) + findCertForDomain(domain) { + if (this.certsCache[domain]?.has_cert) { + return this.certsCache[domain]; + } + + const domainParts = domain.split('.'); + if (domainParts.length >= 2) { + const wildcardDomain = '*.' + domainParts.slice(1).join('.'); + if (this.certsCache[wildcardDomain]?.has_cert) { + return this.certsCache[wildcardDomain]; + } + } + + for (const [certDomain, cert] of Object.entries(this.certsCache)) { + if (cert.has_cert && cert.dns_names) { + for (const dnsName of cert.dns_names) { + if (this.matchesWildcard(domain, dnsName)) { + return cert; + } + } + } + } + + return null; + } + + // Получить иконку сертификата для домена + getCertIcon(domain) { + const cert = this.findCertForDomain(domain); + if (cert) { + if (cert.is_expired) { + return ``; + } else { + return ``; + } + } + return ''; + } + // Отрисовать список прокси render() { const tbody = $('proxyTable')?.querySelector('tbody'); @@ -66,15 +136,17 @@ export class ProxyManager { const httpsBadge = proxy.service_https_use ? 'badge-yes">HTTPS' : 'badge-no">HTTP'; const autoHttpsBadge = proxy.auto_https ? 'badge-yes">Да' : 'badge-no">Нет'; const protocol = proxy.auto_https ? 'https' : 'http'; + const certIcon = this.getCertIcon(proxy.external_domain); row.innerHTML = ` - ${proxy.external_domain} + ${certIcon}${proxy.external_domain} ${proxy.local_address}:${proxy.local_port} ${proxy.status} - + + `; @@ -119,6 +191,11 @@ export class ProxyManager { window.editVAccess(host, isProxy); } break; + case 'open-certs': + if (window.openCertManager) { + window.openCertManager(host, isProxy, []); + } + break; case 'edit-proxy': if (window.editProxy) { window.editProxy(index); diff --git a/Backend/admin/frontend/assets/js/components/site-creator.js b/Backend/admin/frontend/assets/js/components/site-creator.js index b0be672..c97cbe3 100644 --- a/Backend/admin/frontend/assets/js/components/site-creator.js +++ b/Backend/admin/frontend/assets/js/components/site-creator.js @@ -47,6 +47,7 @@ export class SiteCreator { hide($('sectionSettings')); hide($('sectionVAccessEditor')); hide($('sectionAddSite')); + hide($('sectionAddProxy')); } // Вернуться на главную @@ -295,6 +296,9 @@ export class SiteCreator { // Парсим aliases из поля ввода this.parseAliases(); + // Определяем режим сертификата + const certMode = $('certMode').value; + // Собираем данные сайта const siteData = { name: $('newSiteName').value.trim(), @@ -302,7 +306,8 @@ export class SiteCreator { alias: this.aliases, status: $('newSiteStatus').value, root_file: $('newSiteRootFile').value, - root_file_routing: $('newSiteRouting').checked + root_file_routing: $('newSiteRouting').checked, + AutoCreateSSL: certMode === 'auto' }; // Создаём сайт @@ -317,7 +322,6 @@ export class SiteCreator { notification.success('✅ Сайт успешно создан!', 1500); // Загружаем сертификаты если нужно - const certMode = $('certMode').value; if (certMode === 'upload') { createBtn.querySelector('span').textContent = 'Загрузка сертификатов...'; diff --git a/Backend/admin/frontend/assets/js/components/sites.js b/Backend/admin/frontend/assets/js/components/sites.js index 230322c..091b535 100644 --- a/Backend/admin/frontend/assets/js/components/sites.js +++ b/Backend/admin/frontend/assets/js/components/sites.js @@ -11,45 +11,120 @@ import { $ } from '../utils/dom.js'; export class SitesManager { constructor() { this.sitesData = []; + this.certsCache = {}; this.mockData = [ + { + name: 'Home Voxsel', + host: 'home.voxsel.ru', + alias: ['home.voxsel.com'], + status: 'active', + root_file: 'index.html', + root_file_routing: true, + auto_create_ssl: false + }, + { + name: 'Finance', + host: 'finance.voxsel.ru', + alias: [], + status: 'active', + root_file: 'index.php', + root_file_routing: false, + auto_create_ssl: true + }, { name: 'Локальный сайт', host: '127.0.0.1', alias: ['localhost'], status: 'active', root_file: 'index.html', - root_file_routing: true - }, - { - name: 'Тестовый проект', - host: 'test.local', - alias: ['*.test.local', 'test.com'], - status: 'active', - root_file: 'index.php', - root_file_routing: false - }, - { - name: 'API сервис', - host: 'api.example.com', - alias: ['*.api.example.com'], - status: 'inactive', - root_file: 'index.php', - root_file_routing: true + root_file_routing: true, + auto_create_ssl: false } ]; + this.mockCerts = { + 'voxsel.ru': { has_cert: true, is_expired: false, days_left: 79, dns_names: ['*.voxsel.com', '*.voxsel.ru', 'voxsel.com', 'voxsel.ru'] }, + 'finance.voxsel.ru': { has_cert: true, is_expired: false, days_left: 89, dns_names: ['finance.voxsel.ru'] } + }; } // Загрузить список сайтов async load() { if (isWailsAvailable()) { this.sitesData = await api.getSitesList(); + await this.loadCertsInfo(); } else { - // Используем тестовые данные если Wails недоступен this.sitesData = this.mockData; + this.certsCache = this.mockCerts; } this.render(); } + // Загрузить информацию о сертификатах + async loadCertsInfo() { + const allCerts = await api.getAllCertsInfo(); + this.certsCache = {}; + for (const cert of allCerts) { + this.certsCache[cert.domain] = cert; + } + } + + // Проверить соответствие домена wildcard паттерну + matchesWildcard(domain, pattern) { + if (pattern.startsWith('*.')) { + const wildcardBase = pattern.slice(2); + const domainParts = domain.split('.'); + if (domainParts.length >= 2) { + const domainBase = domainParts.slice(1).join('.'); + return domainBase === wildcardBase; + } + } + return domain === pattern; + } + + // Найти сертификат для домена (включая wildcard) + findCertForDomain(domain) { + if (this.certsCache[domain]?.has_cert) { + return this.certsCache[domain]; + } + + const domainParts = domain.split('.'); + if (domainParts.length >= 2) { + const wildcardDomain = '*.' + domainParts.slice(1).join('.'); + if (this.certsCache[wildcardDomain]?.has_cert) { + return this.certsCache[wildcardDomain]; + } + } + + for (const [certDomain, cert] of Object.entries(this.certsCache)) { + if (cert.has_cert && cert.dns_names) { + for (const dnsName of cert.dns_names) { + if (this.matchesWildcard(domain, dnsName)) { + return cert; + } + } + } + } + + return null; + } + + // Получить иконку сертификата для домена + getCertIcon(host, aliases = []) { + const allDomains = [host, ...aliases.filter(a => !a.includes('*'))]; + + for (const domain of allDomains) { + const cert = this.findCertForDomain(domain); + if (cert) { + if (cert.is_expired) { + return ``; + } else { + return ``; + } + } + } + return ''; + } + // Отрисовать список сайтов render() { const tbody = $('sitesTable')?.querySelector('tbody'); @@ -61,16 +136,18 @@ export class SitesManager { const row = document.createElement('tr'); const statusBadge = site.status === 'active' ? 'badge-online' : 'badge-offline'; const aliases = site.alias.join(', '); + const certIcon = this.getCertIcon(site.host, site.alias); row.innerHTML = ` - ${site.name} + ${certIcon}${site.name} ${site.host} ${aliases} ${site.status} ${site.root_file} - + + `; @@ -118,6 +195,13 @@ export class SitesManager { window.editVAccess(host, isProxy); } break; + case 'open-certs': + if (window.openCertManager) { + const aliasesStr = btn.getAttribute('data-aliases') || ''; + const aliases = aliasesStr ? aliasesStr.split(',').filter(a => a) : []; + window.openCertManager(host, isProxy, aliases); + } + break; case 'edit-site': if (window.editSite) { window.editSite(index); diff --git a/Backend/admin/frontend/assets/js/main.js b/Backend/admin/frontend/assets/js/main.js index 9e6be38..c0828ad 100644 --- a/Backend/admin/frontend/assets/js/main.js +++ b/Backend/admin/frontend/assets/js/main.js @@ -13,6 +13,7 @@ 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 { ProxyCreator } from './components/proxy-creator.js'; import { api } from './api/wails.js'; import { configAPI } from './api/config.js'; import { initCustomSelects } from './ui/custom-select.js'; @@ -28,6 +29,7 @@ class App { this.proxyManager = new ProxyManager(); this.vAccessManager = new VAccessManager(); this.siteCreator = new SiteCreator(); + this.proxyCreator = new ProxyCreator(); this.isWails = isWailsAvailable(); } @@ -109,6 +111,14 @@ class App { }); } + // Кнопка добавления прокси + const addProxyBtn = $('addProxyBtn'); + if (addProxyBtn) { + addProxyBtn.addEventListener('click', () => { + this.proxyCreator.open(); + }); + } + // Кнопка сохранения настроек const saveSettingsBtn = $('saveSettingsBtn'); if (saveSettingsBtn) { @@ -135,6 +145,22 @@ class App { } }); } + + // Моментальное переключение ACME без перезапуска + const acmeCheckbox = $('acmeEnabled'); + if (acmeCheckbox) { + acmeCheckbox.addEventListener('change', async (e) => { + const isEnabled = e.target.checked; + + if (isEnabled) { + await configAPI.enableACMEService(); + notification.success('Cert Manager включен', 1000); + } else { + await configAPI.disableACMEService(); + notification.success('Cert Manager отключен', 1000); + } + }); + } } // Настроить глобальные обработчики @@ -143,15 +169,29 @@ class App { // Ссылки на менеджеры sitesManager: this.sitesManager, siteCreator: this.siteCreator, + proxyCreator: this.proxyCreator, + proxyManager: this.proxyManager, // SiteCreator backToMainFromAddSite: () => this.siteCreator.backToMain(), toggleCertUpload: () => this.siteCreator.toggleCertUpload(), handleCertFileSelect: (input, certType) => this.siteCreator.handleCertFile(input, certType), + // ProxyCreator + backToMainFromAddProxy: () => this.proxyCreator.backToMain(), + toggleProxyCertUpload: () => this.proxyCreator.toggleCertUpload(), + handleProxyCertFileSelect: (input, certType) => this.proxyCreator.handleCertFile(input, certType), + // vAccess editVAccess: (host, isProxy) => this.vAccessManager.open(host, isProxy), backToMain: () => this.vAccessManager.backToMain(), + + // CertManager + openCertManager: (host, isProxy, aliases) => this.openCertManager(host, isProxy, aliases), + backFromCertManager: () => this.backFromCertManager(), + deleteCertificate: async (domain) => await this.deleteCertificate(domain), + renewCertificate: async (domain) => await this.renewCertificate(domain), + issueCertificate: async (domain) => await this.issueCertificate(domain), switchVAccessTab: (tab) => this.vAccessManager.switchTab(tab), saveVAccessChanges: async () => await this.vAccessManager.save(), addVAccessRule: () => this.vAccessManager.addRule(), @@ -209,6 +249,7 @@ class App { $('phpHost').value = 'localhost'; $('phpPort').value = 8000; $('proxyEnabled').checked = true; + $('acmeEnabled').checked = true; return; } @@ -220,6 +261,7 @@ class App { $('phpHost').value = config.Soft_Settings?.php_host || 'localhost'; $('phpPort').value = config.Soft_Settings?.php_port || 8000; $('proxyEnabled').checked = config.Soft_Settings?.proxy_enabled !== false; + $('acmeEnabled').checked = config.Soft_Settings?.ACME_enabled !== false; } // Сохранить настройки конфигурации @@ -294,6 +336,9 @@ class App { if (editRootFile) editRootFile.value = site.root_file; if (editRouting) editRouting.checked = site.root_file_routing; + const editAutoCreateSSL = $('editAutoCreateSSL'); + if (editAutoCreateSSL) editAutoCreateSSL.checked = site.auto_create_ssl || false; + // Добавляем alias теги const aliasContainer = $('aliasTagsContainer'); if (aliasContainer) { @@ -348,6 +393,9 @@ class App { if (editServiceHTTPS) editServiceHTTPS.checked = proxy.service_https_use; if (editAutoHTTPS) editAutoHTTPS.checked = proxy.auto_https; + const editProxyAutoCreateSSL = $('editProxyAutoCreateSSL'); + if (editProxyAutoCreateSSL) editProxyAutoCreateSSL.checked = proxy.auto_create_ssl || false; + // Привязываем обработчик кнопок статуса document.querySelectorAll('.status-btn').forEach(btn => { btn.onclick = () => this.setModalStatus(btn.dataset.value); @@ -425,7 +473,8 @@ class App { alias: aliases, status: statusBtn ? statusBtn.dataset.value : 'active', root_file: $('editRootFile').value, - root_file_routing: $('editRouting').checked + root_file_routing: $('editRouting').checked, + AutoCreateSSL: $('editAutoCreateSSL')?.checked || false }; const result = await configAPI.saveConfig(JSON.stringify(config, null, 4)); @@ -453,7 +502,8 @@ class App { LocalAddress: $('editLocalAddr').value, LocalPort: $('editLocalPort').value, ServiceHTTPSuse: $('editServiceHTTPS').checked, - AutoHTTPS: $('editAutoHTTPS').checked + AutoHTTPS: $('editAutoHTTPS').checked, + AutoCreateSSL: $('editProxyAutoCreateSSL')?.checked || false }; const result = await configAPI.saveConfig(JSON.stringify(config, null, 4)); @@ -541,6 +591,404 @@ class App { notification.error('Ошибка: ' + error.message, 3000); } } + + // ====== Cert Manager ====== + + certManagerHost = null; + certManagerIsProxy = false; + certManagerAliases = []; + + async openCertManager(host, isProxy = false, aliases = []) { + this.certManagerHost = host; + this.certManagerIsProxy = isProxy; + this.certManagerAliases = aliases.filter(a => !a.includes('*')); + + // Обновляем заголовки + $('certManagerBreadcrumb').textContent = `Сертификаты: ${host}`; + const titleSpan = $('certManagerTitle').querySelector('span'); + if (titleSpan) titleSpan.textContent = host; + $('certManagerSubtitle').textContent = isProxy ? 'Прокси сервис' : 'Веб-сайт'; + + // Скрываем все секции, показываем CertManager + this.hideAllSectionsForCertManager(); + $('sectionCertManager').style.display = 'block'; + + // Загружаем сертификаты + await this.loadCertManagerContent(host, this.certManagerAliases); + } + + hideAllSectionsForCertManager() { + const sections = ['sectionServices', 'sectionSites', 'sectionProxy', 'sectionSettings', 'sectionVAccessEditor', 'sectionAddSite', 'sectionCertManager']; + sections.forEach(id => { + const el = $(id); + if (el) el.style.display = 'none'; + }); + } + + // Тестовые данные для сертификатов (браузерный режим) + mockCertsData = [ + { + domain: 'voxsel.ru', + issuer: 'R13', + not_before: '2026-01-07', + not_after: '2026-04-07', + days_left: 79, + is_expired: false, + has_cert: true, + dns_names: ['*.voxsel.com', '*.voxsel.ru', 'voxsel.com', 'voxsel.ru'] + }, + { + domain: 'finance.voxsel.ru', + issuer: 'E8', + not_before: '2026-01-17', + not_after: '2026-04-17', + days_left: 89, + is_expired: false, + has_cert: true, + dns_names: ['finance.voxsel.ru'] + }, + { + domain: 'test.local', + issuer: "Let's Encrypt", + not_before: '2025-01-01', + not_after: '2025-03-31', + days_left: 73, + is_expired: false, + has_cert: true, + dns_names: ['test.local', '*.test.local', 'test.com'] + }, + { + domain: 'api.example.com', + issuer: "Let's Encrypt", + not_before: '2024-10-01', + not_after: '2024-12-30', + days_left: -18, + is_expired: true, + has_cert: true, + dns_names: ['api.example.com', '*.api.example.com'] + } + ]; + + async loadCertManagerContent(host, aliases = []) { + const container = $('certManagerContent'); + container.innerHTML = '
Загрузка...
'; + + try { + // Получаем сертификаты (реальные или mock) + let allCerts; + if (this.isWails) { + allCerts = await api.getAllCertsInfo(); + } else { + allCerts = this.mockCertsData; + } + + // Все домены для отображения (host + алиасы без wildcard) + const allDomains = [host, ...aliases.filter(a => !a.includes('*'))]; + + // Функция проверки wildcard покрытия + const isWildcardCovering = (domain, cert) => { + const parts = domain.split('.'); + if (parts.length < 2) return false; + const wildcardPattern = '*.' + parts.slice(1).join('.'); + return cert.domain === wildcardPattern || + cert.domain.startsWith('*.') && domain.endsWith(cert.domain.slice(1)) || + cert.dns_names?.some(dns => dns === wildcardPattern || (dns.startsWith('*.') && domain.endsWith(dns.slice(1)))); + }; + + // Функция проверки прямого сертификата + const hasDirectCert = (domain, cert) => { + return cert.domain === domain || cert.dns_names?.includes(domain); + }; + + // Собираем информацию по каждому домену + const domainInfos = allDomains.map(domain => { + const directCert = allCerts.find(cert => hasDirectCert(domain, cert)); + const wildcardCert = allCerts.find(cert => isWildcardCovering(domain, cert)); + return { domain, directCert, wildcardCert, isLocal: this.isLocalDomain(domain) }; + }); + + let html = ''; + + // Карточки для каждого домена + domainInfos.forEach(info => { + if (info.isLocal) { + // Локальный домен - только информация + html += ` +
+
+
+ +

${info.domain}

+
+
+
+
+
Статус
+
Локальный домен
+
+
+
+ `; + } else if (info.directCert) { + // Есть прямой сертификат + html += this.renderCertCard(info.directCert, info.domain); + } else if (info.wildcardCert) { + // Покрыт wildcard - показываем с возможностью выпустить прямой + html += this.renderDomainWithWildcard(info.domain, info.wildcardCert); + } else { + // Нет сертификата - предлагаем выпустить + html += this.renderNoCertCard(info.domain); + } + }); + + if (!html) { + html = ` +
+ +

Нет доменов для отображения

+
+ `; + } + + container.innerHTML = html; + + } catch (error) { + container.innerHTML = `

Ошибка загрузки: ${error.message}

`; + } + } + + renderCertCard(cert, displayDomain = null) { + const isExpired = cert.is_expired; + const statusClass = isExpired ? 'expired' : 'valid'; + const statusText = isExpired ? 'Истёк' : `Активен (${cert.days_left} дн.)`; + const iconClass = isExpired ? 'expired' : ''; + const title = displayDomain || cert.domain; + + const dnsNames = cert.dns_names || [cert.domain]; + const domainTags = dnsNames.map(d => `${d}`).join(''); + + return ` +
+
+
+ +

${title}

+
+
+ + +
+
+ +
+
+
Статус
+
${statusText}
+
+
+
Издатель
+
${cert.issuer || 'Неизвестно'}
+
+
+
Выдан
+
${cert.not_before || '-'}
+
+
+
Истекает
+
${cert.not_after || '-'}
+
+
+ +
+ ${domainTags} +
+
+ `; + } + + isLocalDomain(host) { + const localPatterns = [ + 'localhost', + '127.0.0.1', + '0.0.0.0', + '::1', + '.local', + '.localhost', + '.test', + '.example', + '.invalid' + ]; + + const hostLower = host.toLowerCase(); + return localPatterns.some(pattern => { + if (pattern.startsWith('.')) { + return hostLower.endsWith(pattern) || hostLower === pattern.slice(1); + } + return hostLower === pattern; + }); + } + + renderNoCertCard(host) { + return ` +
+
+
+ +

${host}

+
+
+ +
+
+ +
+
+
Статус
+
Нет сертификата
+
+
+ +
+ ${host} +
+
+ `; + } + + renderDomainWithWildcard(domain, wildcardCert) { + const isExpired = wildcardCert.is_expired; + const statusClass = isExpired ? 'expired' : 'valid'; + const statusText = isExpired ? `Покрыт wildcard (истёк)` : `Покрыт wildcard (${wildcardCert.days_left} дн.)`; + + return ` +
+
+
+ +

${domain}

+
+
+ +
+
+ +
+
+
Статус
+
${statusText}
+
+
+
Wildcard
+
${wildcardCert.domain}
+
+
+ +
+ ${domain} +
+
+ `; + } + + async issueCertificate(domain) { + const confirmed = confirm(`Выпустить сертификат для "${domain}"?\n\nБудет запрошен сертификат Let's Encrypt.`); + if (!confirmed) return; + + try { + notification.show('Запрос сертификата...', 'info', 2000); + + if (this.isWails) { + await api.obtainSSLCertificate(domain); + } else { + // Mock для браузерного режима + await new Promise(r => setTimeout(r, 1500)); + } + + notification.success('Сертификат успешно выпущен!', 2000); + + // Перезагружаем контент + await this.loadCertManagerContent(this.certManagerHost); + + // Обновляем списки сайтов и прокси + await this.sitesManager.load(); + await this.proxyManager.load(); + + } catch (error) { + notification.error('Ошибка: ' + error.message, 3000); + } + } + + backFromCertManager() { + this.hideAllSectionsForCertManager(); + + // Показываем секции Dashboard + const dashboard = ['sectionServices', 'sectionSites', 'sectionProxy']; + dashboard.forEach(id => { + const el = $(id); + if (el) el.style.display = 'block'; + }); + + // Убираем active у всех nav-item и ставим на dashboard + document.querySelectorAll('.nav-item').forEach(item => item.classList.remove('active')); + const dashboardBtn = document.querySelector('.nav-item[data-page="dashboard"]'); + if (dashboardBtn) dashboardBtn.classList.add('active'); + } + + async deleteCertificate(domain) { + const confirmed = confirm(`Удалить сертификат для "${domain}"?\n\nЭто действие необратимо.`); + if (!confirmed) return; + + try { + await api.deleteCertificate(domain); + notification.success('Сертификат удалён', 1500); + + // Перезагружаем контент + await this.loadCertManagerContent(this.certManagerHost); + + // Обновляем списки сайтов и прокси + await this.sitesManager.load(); + await this.proxyManager.load(); + + } catch (error) { + notification.error('Ошибка: ' + error.message, 3000); + } + } + + async renewCertificate(domain) { + const confirmed = confirm(`Перевыпустить сертификат для "${domain}"?\n\nТекущий сертификат будет заменён новым.`); + if (!confirmed) return; + + try { + notification.show('Запрос сертификата...', 'info', 2000); + + if (this.isWails) { + await api.obtainSSLCertificate(domain); + } else { + // Mock для браузерного режима + await new Promise(r => setTimeout(r, 1500)); + } + + notification.success('Сертификат успешно перевыпущен!', 2000); + + // Перезагружаем контент + await this.loadCertManagerContent(this.certManagerHost); + + // Обновляем списки сайтов и прокси + await this.sitesManager.load(); + await this.proxyManager.load(); + + } catch (error) { + notification.error('Ошибка: ' + error.message, 3000); + } + } } // Инициализация приложения при загрузке DOM diff --git a/Backend/admin/frontend/assets/js/ui/navigation.js b/Backend/admin/frontend/assets/js/ui/navigation.js index 6de3de7..99de41a 100644 --- a/Backend/admin/frontend/assets/js/ui/navigation.js +++ b/Backend/admin/frontend/assets/js/ui/navigation.js @@ -8,7 +8,7 @@ import { $, $$, hide, show, removeClass, addClass } from '../utils/dom.js'; // Класс для управления навигацией export class Navigation { constructor() { - this.navItems = $$('.nav-item'); + this.navItems = $$('.nav-item[data-page]'); this.sections = { services: $('sectionServices'), sites: $('sectionSites'), @@ -21,32 +21,35 @@ export class Navigation { } init() { - this.navItems.forEach((item, index) => { - item.addEventListener('click', () => this.navigate(index)); + this.navItems.forEach(item => { + item.addEventListener('click', () => { + const page = item.dataset.page; + this.navigate(page, item); + }); }); } - navigate(index) { + navigate(page, clickedItem) { // Убираем active со всех навигационных элементов this.navItems.forEach(nav => removeClass(nav, 'active')); - addClass(this.navItems[index], 'active'); + addClass(clickedItem, 'active'); // Скрываем все секции this.hideAllSections(); - // Показываем нужные секции - if (index === 0) { - // Главная - всё кроме настроек - show(this.sections.services); - show(this.sections.sites); - show(this.sections.proxy); - } else if (index === 3) { - // Настройки - show(this.sections.settings); - // Загружаем конфигурацию при открытии - if (window.loadConfig) { - window.loadConfig(); - } + // Показываем нужные секции по имени страницы + switch (page) { + case 'dashboard': + show(this.sections.services); + show(this.sections.sites); + show(this.sections.proxy); + break; + case 'settings': + show(this.sections.settings); + if (window.loadConfig) { + window.loadConfig(); + } + break; } } diff --git a/Backend/admin/frontend/index.html b/Backend/admin/frontend/index.html index 9e41a24..b3de709 100644 --- a/Backend/admin/frontend/index.html +++ b/Backend/admin/frontend/index.html @@ -40,16 +40,10 @@