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} ${proxy.external_domain} ${proxy.local_address}:${proxy.local_port}${site.host} ${aliases}${site.root_file}Ошибка загрузки: ${error.message}