Улучшен фронт

1. Добавлен функционал в интерфейс по управлению сертификатами и службой редактирования сертификатов.

2. Добавлена кнопка для добавления прокси и экран редактирования прокси.
This commit is contained in:
2026-01-17 11:57:57 +07:00
parent 9a788800b5
commit 05ddb1e796
22 changed files with 1641 additions and 77 deletions

View File

@@ -488,3 +488,80 @@ func getCertDaysLeft(domain string) int {
func getCurrentTimestamp() int64 { func getCurrentTimestamp() int64 {
return time.Now().Unix() 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
}

View File

@@ -25,12 +25,14 @@ type Manager struct {
// CertInfo информация о сертификате // CertInfo информация о сертификате
type CertInfo struct { type CertInfo struct {
Domain string `json:"domain"` Domain string `json:"domain"`
Issuer string `json:"issuer"` Issuer string `json:"issuer"`
NotBefore string `json:"not_before"` NotBefore string `json:"not_before"`
NotAfter string `json:"not_after"` NotAfter string `json:"not_after"`
DaysLeft int `json:"days_left"` DaysLeft int `json:"days_left"`
AutoCreated bool `json:"auto_created"` IsExpired bool `json:"is_expired"`
HasCert bool `json:"has_cert"`
DNSNames []string `json:"dns_names"`
} }
// ObtainResult результат получения сертификата // ObtainResult результат получения сертификата

View File

@@ -20,21 +20,18 @@
background: linear-gradient(135deg, rgba(16, 185, 129, 0.25), rgba(16, 185, 129, 0.15)); background: linear-gradient(135deg, rgba(16, 185, 129, 0.25), rgba(16, 185, 129, 0.15));
color: var(--accent-green); color: var(--accent-green);
border: 1px solid rgba(16, 185, 129, 0.4); border: 1px solid rgba(16, 185, 129, 0.4);
box-shadow: 0 0 12px rgba(16, 185, 129, 0.3);
} }
.badge-offline { .badge-offline {
background: linear-gradient(135deg, rgba(239, 68, 68, 0.25), rgba(239, 68, 68, 0.15)); background: linear-gradient(135deg, rgba(239, 68, 68, 0.25), rgba(239, 68, 68, 0.15));
color: var(--accent-red); color: var(--accent-red);
border: 1px solid rgba(239, 68, 68, 0.4); border: 1px solid rgba(239, 68, 68, 0.4);
box-shadow: 0 0 12px rgba(239, 68, 68, 0.3);
} }
.badge-pending { .badge-pending {
background: linear-gradient(135deg, rgba(245, 158, 11, 0.25), rgba(245, 158, 11, 0.15)); background: linear-gradient(135deg, rgba(245, 158, 11, 0.25), rgba(245, 158, 11, 0.15));
color: var(--accent-yellow); color: var(--accent-yellow);
border: 1px solid rgba(245, 158, 11, 0.4); border: 1px solid rgba(245, 158, 11, 0.4);
box-shadow: 0 0 12px rgba(245, 158, 11, 0.3);
} }
/* Yes/No Badges */ /* Yes/No Badges */
@@ -55,7 +52,6 @@
width: 7px; width: 7px;
height: 7px; height: 7px;
border-radius: var(--radius-full); border-radius: var(--radius-full);
box-shadow: 0 0 8px currentColor;
} }
.status-online { .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);
}

View File

@@ -71,7 +71,6 @@
&:hover:not(:disabled) { &:hover:not(:disabled) {
background: rgba(239, 68, 68, 0.25); background: rgba(239, 68, 68, 0.25);
border-color: rgba(239, 68, 68, 0.5); 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); font-weight: var(--font-semibold);
cursor: pointer; cursor: pointer;
transition: all var(--transition-base); transition: all var(--transition-base);
box-shadow: var(--shadow-red);
&:hover:not(:disabled) { &:hover:not(:disabled) {
background: linear-gradient(135deg, rgba(239, 68, 68, 0.3), rgba(239, 68, 68, 0.15)); 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)); background: linear-gradient(135deg, rgba(16, 185, 129, 0.2), rgba(16, 185, 129, 0.1));
border-color: rgba(16, 185, 129, 0.4); border-color: rgba(16, 185, 129, 0.4);
color: var(--accent-green); color: var(--accent-green);
box-shadow: var(--shadow-green);
&:hover:not(:disabled) { &:hover:not(:disabled) {
background: linear-gradient(135deg, rgba(16, 185, 129, 0.3), rgba(16, 185, 129, 0.15)); 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); background: rgba(16, 185, 129, 0.2);
border-color: rgba(16, 185, 129, 0.5); border-color: rgba(16, 185, 129, 0.5);
color: var(--accent-green); color: var(--accent-green);
box-shadow: 0 0 12px rgba(16, 185, 129, 0.2);
} }
&:last-child.active { &:last-child.active {
@@ -315,7 +311,6 @@
&.active { &.active {
background: var(--accent-purple); background: var(--accent-purple);
color: white; color: white;
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
} }
} }

View File

@@ -24,6 +24,11 @@
gap: var(--space-md); gap: var(--space-md);
} }
/* Form Row (3 columns) */
.form-row.form-row-3 {
grid-template-columns: 1fr 1fr 1fr;
}
/* Form Label */ /* Form Label */
.form-label { .form-label {
font-size: 12px; font-size: 12px;

View File

@@ -65,7 +65,6 @@
backdrop-filter: var(--backdrop-blur-light); backdrop-filter: var(--backdrop-blur-light);
border-radius: 20px; border-radius: 20px;
border: 1px solid rgba(16, 185, 129, 0.3); border: 1px solid rgba(16, 185, 129, 0.3);
box-shadow: var(--shadow-green);
} }
.status-text { .status-text {

View File

@@ -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;
}

View File

@@ -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() { async restartAllServices() {
if (!this.available) return; if (!this.available) return;

View File

@@ -145,6 +145,46 @@ class WailsAPI {
return `Error: ${error.message}`; 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}`;
}
}
} }
// Экспортируем единственный экземпляр // Экспортируем единственный экземпляр

View File

@@ -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 = '<span style="color: #e74c3c;"><i class="fas fa-times-circle"></i> Файл слишком большой (макс 1MB)</span>';
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 = `<span style="color: #2ecc71;"><i class="fas fa-check-circle"></i> Загружен успешно</span>`;
};
reader.onerror = () => {
statusDiv.innerHTML = '<span style="color: #e74c3c;"><i class="fas fa-times-circle"></i> Ошибка чтения файла</span>';
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;
}
}
}

View File

@@ -11,6 +11,7 @@ import { $ } from '../utils/dom.js';
export class ProxyManager { export class ProxyManager {
constructor() { constructor() {
this.proxiesData = []; this.proxiesData = [];
this.certsCache = {};
this.mockData = [ this.mockData = [
{ {
enable: true, enable: true,
@@ -19,6 +20,7 @@ export class ProxyManager {
local_port: '3333', local_port: '3333',
service_https_use: false, service_https_use: false,
auto_https: true, auto_https: true,
auto_create_ssl: true,
status: 'active' status: 'active'
}, },
{ {
@@ -28,6 +30,7 @@ export class ProxyManager {
local_port: '8080', local_port: '8080',
service_https_use: true, service_https_use: true,
auto_https: false, auto_https: false,
auto_create_ssl: false,
status: 'active' status: 'active'
}, },
{ {
@@ -37,22 +40,89 @@ export class ProxyManager {
local_port: '5000', local_port: '5000',
service_https_use: false, service_https_use: false,
auto_https: false, auto_https: false,
auto_create_ssl: false,
status: 'disabled' status: 'disabled'
} }
]; ];
this.mockCerts = {
'git.example.ru': { has_cert: true, is_expired: false, days_left: 60 }
};
} }
// Загрузить список прокси // Загрузить список прокси
async load() { async load() {
if (isWailsAvailable()) { if (isWailsAvailable()) {
this.proxiesData = await api.getProxyList(); this.proxiesData = await api.getProxyList();
await this.loadCertsInfo();
} else { } else {
// Используем тестовые данные если Wails недоступен
this.proxiesData = this.mockData; this.proxiesData = this.mockData;
this.certsCache = this.mockCerts;
} }
this.render(); 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 `<span class="cert-icon cert-expired" title="SSL сертификат истёк"><i class="fas fa-shield-alt"></i></span>`;
} else {
return `<span class="cert-icon cert-valid" title="SSL активен (${cert.days_left} дн.)"><i class="fas fa-shield-alt"></i></span>`;
}
}
return '';
}
// Отрисовать список прокси // Отрисовать список прокси
render() { render() {
const tbody = $('proxyTable')?.querySelector('tbody'); 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 httpsBadge = proxy.service_https_use ? 'badge-yes">HTTPS' : 'badge-no">HTTP';
const autoHttpsBadge = proxy.auto_https ? 'badge-yes">Да' : 'badge-no">Нет'; const autoHttpsBadge = proxy.auto_https ? 'badge-yes">Да' : 'badge-no">Нет';
const protocol = proxy.auto_https ? 'https' : 'http'; const protocol = proxy.auto_https ? 'https' : 'http';
const certIcon = this.getCertIcon(proxy.external_domain);
row.innerHTML = ` row.innerHTML = `
<td><code class="clickable-link" data-url="${protocol}://${proxy.external_domain}">${proxy.external_domain} <i class="fas fa-external-link-alt"></i></code></td> <td>${certIcon}<code class="clickable-link" data-url="${protocol}://${proxy.external_domain}">${proxy.external_domain} <i class="fas fa-external-link-alt"></i></code></td>
<td><code>${proxy.local_address}:${proxy.local_port}</code></td> <td><code>${proxy.local_address}:${proxy.local_port}</code></td>
<td><span class="badge ${httpsBadge}</span></td> <td><span class="badge ${httpsBadge}</span></td>
<td><span class="badge ${autoHttpsBadge}</span></td> <td><span class="badge ${autoHttpsBadge}</span></td>
<td><span class="badge ${statusBadge}">${proxy.status}</span></td> <td><span class="badge ${statusBadge}">${proxy.status}</span></td>
<td> <td>
<button class="icon-btn" data-action="edit-vaccess" data-host="${proxy.external_domain}" data-is-proxy="true" title="vAccess"><i class="fas fa-shield-alt"></i></button> <button class="icon-btn" data-action="edit-vaccess" data-host="${proxy.external_domain}" data-is-proxy="true" title="vAccess"><i class="fas fa-user-lock"></i></button>
<button class="icon-btn" data-action="open-certs" data-host="${proxy.external_domain}" data-is-proxy="true" title="SSL сертификаты"><i class="fas fa-shield-alt"></i></button>
<button class="icon-btn" data-action="edit-proxy" data-index="${index}" title="Редактировать"><i class="fas fa-edit"></i></button> <button class="icon-btn" data-action="edit-proxy" data-index="${index}" title="Редактировать"><i class="fas fa-edit"></i></button>
</td> </td>
`; `;
@@ -119,6 +191,11 @@ export class ProxyManager {
window.editVAccess(host, isProxy); window.editVAccess(host, isProxy);
} }
break; break;
case 'open-certs':
if (window.openCertManager) {
window.openCertManager(host, isProxy, []);
}
break;
case 'edit-proxy': case 'edit-proxy':
if (window.editProxy) { if (window.editProxy) {
window.editProxy(index); window.editProxy(index);

View File

@@ -47,6 +47,7 @@ export class SiteCreator {
hide($('sectionSettings')); hide($('sectionSettings'));
hide($('sectionVAccessEditor')); hide($('sectionVAccessEditor'));
hide($('sectionAddSite')); hide($('sectionAddSite'));
hide($('sectionAddProxy'));
} }
// Вернуться на главную // Вернуться на главную
@@ -295,6 +296,9 @@ export class SiteCreator {
// Парсим aliases из поля ввода // Парсим aliases из поля ввода
this.parseAliases(); this.parseAliases();
// Определяем режим сертификата
const certMode = $('certMode').value;
// Собираем данные сайта // Собираем данные сайта
const siteData = { const siteData = {
name: $('newSiteName').value.trim(), name: $('newSiteName').value.trim(),
@@ -302,7 +306,8 @@ export class SiteCreator {
alias: this.aliases, alias: this.aliases,
status: $('newSiteStatus').value, status: $('newSiteStatus').value,
root_file: $('newSiteRootFile').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); notification.success('✅ Сайт успешно создан!', 1500);
// Загружаем сертификаты если нужно // Загружаем сертификаты если нужно
const certMode = $('certMode').value;
if (certMode === 'upload') { if (certMode === 'upload') {
createBtn.querySelector('span').textContent = 'Загрузка сертификатов...'; createBtn.querySelector('span').textContent = 'Загрузка сертификатов...';

View File

@@ -11,45 +11,120 @@ import { $ } from '../utils/dom.js';
export class SitesManager { export class SitesManager {
constructor() { constructor() {
this.sitesData = []; this.sitesData = [];
this.certsCache = {};
this.mockData = [ 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: 'Локальный сайт', name: 'Локальный сайт',
host: '127.0.0.1', host: '127.0.0.1',
alias: ['localhost'], alias: ['localhost'],
status: 'active', status: 'active',
root_file: 'index.html', root_file: 'index.html',
root_file_routing: true root_file_routing: true,
}, auto_create_ssl: false
{
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
} }
]; ];
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() { async load() {
if (isWailsAvailable()) { if (isWailsAvailable()) {
this.sitesData = await api.getSitesList(); this.sitesData = await api.getSitesList();
await this.loadCertsInfo();
} else { } else {
// Используем тестовые данные если Wails недоступен
this.sitesData = this.mockData; this.sitesData = this.mockData;
this.certsCache = this.mockCerts;
} }
this.render(); 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 `<span class="cert-icon cert-expired" title="SSL сертификат истёк"><i class="fas fa-shield-alt"></i></span>`;
} else {
return `<span class="cert-icon cert-valid" title="SSL активен (${cert.days_left} дн.)"><i class="fas fa-shield-alt"></i></span>`;
}
}
}
return '';
}
// Отрисовать список сайтов // Отрисовать список сайтов
render() { render() {
const tbody = $('sitesTable')?.querySelector('tbody'); const tbody = $('sitesTable')?.querySelector('tbody');
@@ -61,16 +136,18 @@ export class SitesManager {
const row = document.createElement('tr'); const row = document.createElement('tr');
const statusBadge = site.status === 'active' ? 'badge-online' : 'badge-offline'; const statusBadge = site.status === 'active' ? 'badge-online' : 'badge-offline';
const aliases = site.alias.join(', '); const aliases = site.alias.join(', ');
const certIcon = this.getCertIcon(site.host, site.alias);
row.innerHTML = ` row.innerHTML = `
<td>${site.name}</td> <td>${certIcon}${site.name}</td>
<td><code class="clickable-link" data-url="http://${site.host}">${site.host} <i class="fas fa-external-link-alt"></i></code></td> <td><code class="clickable-link" data-url="http://${site.host}">${site.host} <i class="fas fa-external-link-alt"></i></code></td>
<td><code>${aliases}</code></td> <td><code>${aliases}</code></td>
<td><span class="badge ${statusBadge}">${site.status}</span></td> <td><span class="badge ${statusBadge}">${site.status}</span></td>
<td><code>${site.root_file}</code></td> <td><code>${site.root_file}</code></td>
<td> <td>
<button class="icon-btn" data-action="open-folder" data-host="${site.host}" title="Открыть папку"><i class="fas fa-folder-open"></i></button> <button class="icon-btn" data-action="open-folder" data-host="${site.host}" title="Открыть папку"><i class="fas fa-folder-open"></i></button>
<button class="icon-btn" data-action="edit-vaccess" data-host="${site.host}" data-is-proxy="false" title="vAccess"><i class="fas fa-shield-alt"></i></button> <button class="icon-btn" data-action="edit-vaccess" data-host="${site.host}" data-is-proxy="false" title="vAccess"><i class="fas fa-user-lock"></i></button>
<button class="icon-btn" data-action="open-certs" data-host="${site.host}" data-aliases="${site.alias.join(',')}" data-is-proxy="false" title="SSL сертификаты"><i class="fas fa-shield-alt"></i></button>
<button class="icon-btn" data-action="edit-site" data-index="${index}" title="Редактировать"><i class="fas fa-edit"></i></button> <button class="icon-btn" data-action="edit-site" data-index="${index}" title="Редактировать"><i class="fas fa-edit"></i></button>
</td> </td>
`; `;
@@ -118,6 +195,13 @@ export class SitesManager {
window.editVAccess(host, isProxy); window.editVAccess(host, isProxy);
} }
break; 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': case 'edit-site':
if (window.editSite) { if (window.editSite) {
window.editSite(index); window.editSite(index);

View File

@@ -13,6 +13,7 @@ import { SitesManager } from './components/sites.js';
import { ProxyManager } from './components/proxy.js'; import { ProxyManager } from './components/proxy.js';
import { VAccessManager } from './components/vaccess.js'; import { VAccessManager } from './components/vaccess.js';
import { SiteCreator } from './components/site-creator.js'; import { SiteCreator } from './components/site-creator.js';
import { ProxyCreator } from './components/proxy-creator.js';
import { api } from './api/wails.js'; import { api } from './api/wails.js';
import { configAPI } from './api/config.js'; import { configAPI } from './api/config.js';
import { initCustomSelects } from './ui/custom-select.js'; import { initCustomSelects } from './ui/custom-select.js';
@@ -28,6 +29,7 @@ class App {
this.proxyManager = new ProxyManager(); this.proxyManager = new ProxyManager();
this.vAccessManager = new VAccessManager(); this.vAccessManager = new VAccessManager();
this.siteCreator = new SiteCreator(); this.siteCreator = new SiteCreator();
this.proxyCreator = new ProxyCreator();
this.isWails = isWailsAvailable(); this.isWails = isWailsAvailable();
} }
@@ -109,6 +111,14 @@ class App {
}); });
} }
// Кнопка добавления прокси
const addProxyBtn = $('addProxyBtn');
if (addProxyBtn) {
addProxyBtn.addEventListener('click', () => {
this.proxyCreator.open();
});
}
// Кнопка сохранения настроек // Кнопка сохранения настроек
const saveSettingsBtn = $('saveSettingsBtn'); const saveSettingsBtn = $('saveSettingsBtn');
if (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, sitesManager: this.sitesManager,
siteCreator: this.siteCreator, siteCreator: this.siteCreator,
proxyCreator: this.proxyCreator,
proxyManager: this.proxyManager,
// SiteCreator // SiteCreator
backToMainFromAddSite: () => this.siteCreator.backToMain(), backToMainFromAddSite: () => this.siteCreator.backToMain(),
toggleCertUpload: () => this.siteCreator.toggleCertUpload(), toggleCertUpload: () => this.siteCreator.toggleCertUpload(),
handleCertFileSelect: (input, certType) => this.siteCreator.handleCertFile(input, certType), 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 // vAccess
editVAccess: (host, isProxy) => this.vAccessManager.open(host, isProxy), editVAccess: (host, isProxy) => this.vAccessManager.open(host, isProxy),
backToMain: () => this.vAccessManager.backToMain(), 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), switchVAccessTab: (tab) => this.vAccessManager.switchTab(tab),
saveVAccessChanges: async () => await this.vAccessManager.save(), saveVAccessChanges: async () => await this.vAccessManager.save(),
addVAccessRule: () => this.vAccessManager.addRule(), addVAccessRule: () => this.vAccessManager.addRule(),
@@ -209,6 +249,7 @@ class App {
$('phpHost').value = 'localhost'; $('phpHost').value = 'localhost';
$('phpPort').value = 8000; $('phpPort').value = 8000;
$('proxyEnabled').checked = true; $('proxyEnabled').checked = true;
$('acmeEnabled').checked = true;
return; return;
} }
@@ -220,6 +261,7 @@ class App {
$('phpHost').value = config.Soft_Settings?.php_host || 'localhost'; $('phpHost').value = config.Soft_Settings?.php_host || 'localhost';
$('phpPort').value = config.Soft_Settings?.php_port || 8000; $('phpPort').value = config.Soft_Settings?.php_port || 8000;
$('proxyEnabled').checked = config.Soft_Settings?.proxy_enabled !== false; $('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 (editRootFile) editRootFile.value = site.root_file;
if (editRouting) editRouting.checked = site.root_file_routing; if (editRouting) editRouting.checked = site.root_file_routing;
const editAutoCreateSSL = $('editAutoCreateSSL');
if (editAutoCreateSSL) editAutoCreateSSL.checked = site.auto_create_ssl || false;
// Добавляем alias теги // Добавляем alias теги
const aliasContainer = $('aliasTagsContainer'); const aliasContainer = $('aliasTagsContainer');
if (aliasContainer) { if (aliasContainer) {
@@ -348,6 +393,9 @@ class App {
if (editServiceHTTPS) editServiceHTTPS.checked = proxy.service_https_use; if (editServiceHTTPS) editServiceHTTPS.checked = proxy.service_https_use;
if (editAutoHTTPS) editAutoHTTPS.checked = proxy.auto_https; 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 => { document.querySelectorAll('.status-btn').forEach(btn => {
btn.onclick = () => this.setModalStatus(btn.dataset.value); btn.onclick = () => this.setModalStatus(btn.dataset.value);
@@ -425,7 +473,8 @@ class App {
alias: aliases, alias: aliases,
status: statusBtn ? statusBtn.dataset.value : 'active', status: statusBtn ? statusBtn.dataset.value : 'active',
root_file: $('editRootFile').value, 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)); const result = await configAPI.saveConfig(JSON.stringify(config, null, 4));
@@ -453,7 +502,8 @@ class App {
LocalAddress: $('editLocalAddr').value, LocalAddress: $('editLocalAddr').value,
LocalPort: $('editLocalPort').value, LocalPort: $('editLocalPort').value,
ServiceHTTPSuse: $('editServiceHTTPS').checked, ServiceHTTPSuse: $('editServiceHTTPS').checked,
AutoHTTPS: $('editAutoHTTPS').checked AutoHTTPS: $('editAutoHTTPS').checked,
AutoCreateSSL: $('editProxyAutoCreateSSL')?.checked || false
}; };
const result = await configAPI.saveConfig(JSON.stringify(config, null, 4)); const result = await configAPI.saveConfig(JSON.stringify(config, null, 4));
@@ -541,6 +591,404 @@ class App {
notification.error('Ошибка: ' + error.message, 3000); 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 = '<div style="text-align: center; padding: 40px; color: var(--text-muted);"><i class="fas fa-spinner fa-spin"></i> Загрузка...</div>';
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 += `
<div class="cert-card cert-card-local">
<div class="cert-card-header">
<div class="cert-card-title">
<i class="fas fa-home" style="opacity: 0.4"></i>
<h3>${info.domain}</h3>
</div>
</div>
<div class="cert-info-grid">
<div class="cert-info-item">
<div class="cert-info-label">Статус</div>
<div class="cert-info-value" style="opacity: 0.6">Локальный домен</div>
</div>
</div>
</div>
`;
} 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 = `
<div class="cert-empty">
<i class="fas fa-shield-alt"></i>
<h3>Нет доменов для отображения</h3>
</div>
`;
}
container.innerHTML = html;
} catch (error) {
container.innerHTML = `<div class="cert-empty"><p>Ошибка загрузки: ${error.message}</p></div>`;
}
}
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 => `<span class="cert-domain-tag">${d}</span>`).join('');
return `
<div class="cert-card">
<div class="cert-card-header">
<div class="cert-card-title ${iconClass}">
<i class="fas fa-shield-alt"></i>
<h3>${title}</h3>
</div>
<div class="cert-card-actions">
<button class="action-btn" onclick="renewCertificate('${title}')">
<i class="fas fa-sync-alt"></i> Перевыпустить
</button>
<button class="action-btn delete-btn" onclick="deleteCertificate('${cert.domain}')">
<i class="fas fa-trash"></i> Удалить
</button>
</div>
</div>
<div class="cert-info-grid">
<div class="cert-info-item">
<div class="cert-info-label">Статус</div>
<div class="cert-info-value ${statusClass}">${statusText}</div>
</div>
<div class="cert-info-item">
<div class="cert-info-label">Издатель</div>
<div class="cert-info-value">${cert.issuer || 'Неизвестно'}</div>
</div>
<div class="cert-info-item">
<div class="cert-info-label">Выдан</div>
<div class="cert-info-value">${cert.not_before || '-'}</div>
</div>
<div class="cert-info-item">
<div class="cert-info-label">Истекает</div>
<div class="cert-info-value ${statusClass}">${cert.not_after || '-'}</div>
</div>
</div>
<div class="cert-domains-list">
${domainTags}
</div>
</div>
`;
}
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 `
<div class="cert-card cert-card-empty">
<div class="cert-card-header">
<div class="cert-card-title">
<i class="fas fa-shield-alt" style="opacity: 0.4"></i>
<h3>${host}</h3>
</div>
<div class="cert-card-actions">
<button class="action-btn btn-success" onclick="issueCertificate('${host}')">
<i class="fas fa-plus"></i> Выпустить сертификат
</button>
</div>
</div>
<div class="cert-info-grid">
<div class="cert-info-item">
<div class="cert-info-label">Статус</div>
<div class="cert-info-value" style="opacity: 0.6">Нет сертификата</div>
</div>
</div>
<div class="cert-domains-list">
<span class="cert-domain-tag">${host}</span>
</div>
</div>
`;
}
renderDomainWithWildcard(domain, wildcardCert) {
const isExpired = wildcardCert.is_expired;
const statusClass = isExpired ? 'expired' : 'valid';
const statusText = isExpired ? `Покрыт wildcard (истёк)` : `Покрыт wildcard (${wildcardCert.days_left} дн.)`;
return `
<div class="cert-card cert-card-wildcard">
<div class="cert-card-header">
<div class="cert-card-title ${isExpired ? 'expired' : ''}">
<i class="fas fa-shield-alt"></i>
<h3>${domain}</h3>
</div>
<div class="cert-card-actions">
<button class="action-btn btn-success" onclick="issueCertificate('${domain}')">
<i class="fas fa-plus"></i> Выпустить прямой
</button>
</div>
</div>
<div class="cert-info-grid">
<div class="cert-info-item">
<div class="cert-info-label">Статус</div>
<div class="cert-info-value ${statusClass}">${statusText}</div>
</div>
<div class="cert-info-item">
<div class="cert-info-label">Wildcard</div>
<div class="cert-info-value">${wildcardCert.domain}</div>
</div>
</div>
<div class="cert-domains-list">
<span class="cert-domain-tag">${domain}</span>
</div>
</div>
`;
}
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 // Инициализация приложения при загрузке DOM

View File

@@ -8,7 +8,7 @@ import { $, $$, hide, show, removeClass, addClass } from '../utils/dom.js';
// Класс для управления навигацией // Класс для управления навигацией
export class Navigation { export class Navigation {
constructor() { constructor() {
this.navItems = $$('.nav-item'); this.navItems = $$('.nav-item[data-page]');
this.sections = { this.sections = {
services: $('sectionServices'), services: $('sectionServices'),
sites: $('sectionSites'), sites: $('sectionSites'),
@@ -21,32 +21,35 @@ export class Navigation {
} }
init() { init() {
this.navItems.forEach((item, index) => { this.navItems.forEach(item => {
item.addEventListener('click', () => this.navigate(index)); item.addEventListener('click', () => {
const page = item.dataset.page;
this.navigate(page, item);
});
}); });
} }
navigate(index) { navigate(page, clickedItem) {
// Убираем active со всех навигационных элементов // Убираем active со всех навигационных элементов
this.navItems.forEach(nav => removeClass(nav, 'active')); this.navItems.forEach(nav => removeClass(nav, 'active'));
addClass(this.navItems[index], 'active'); addClass(clickedItem, 'active');
// Скрываем все секции // Скрываем все секции
this.hideAllSections(); this.hideAllSections();
// Показываем нужные секции // Показываем нужные секции по имени страницы
if (index === 0) { switch (page) {
// Главная - всё кроме настроек case 'dashboard':
show(this.sections.services); show(this.sections.services);
show(this.sections.sites); show(this.sections.sites);
show(this.sections.proxy); show(this.sections.proxy);
} else if (index === 3) { break;
// Настройки case 'settings':
show(this.sections.settings); show(this.sections.settings);
// Загружаем конфигурацию при открытии if (window.loadConfig) {
if (window.loadConfig) { window.loadConfig();
window.loadConfig(); }
} break;
} }
} }

View File

@@ -40,16 +40,10 @@
<aside class="sidebar"> <aside class="sidebar">
<nav class="sidebar-nav"> <nav class="sidebar-nav">
<button class="nav-item active" title="Главная"> <button class="nav-item active" data-page="dashboard" title="Главная">
<i class="fas fa-home"></i> <i class="fas fa-home"></i>
</button> </button>
<button class="nav-item" title="Статистика"> <button class="nav-item" data-page="settings" title="Настройки">
<i class="fas fa-chart-bar"></i>
</button>
<button class="nav-item" title="Сервисы">
<i class="fas fa-server"></i>
</button>
<button class="nav-item" title="Настройки">
<i class="fas fa-cog"></i> <i class="fas fa-cog"></i>
</button> </button>
</nav> </nav>
@@ -191,7 +185,13 @@
</section> </section>
<section class="section" id="sectionProxy"> <section class="section" id="sectionProxy">
<h2 class="section-title">Прокси сервисы</h2> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
<h2 class="section-title" style="margin-bottom: 0;">Прокси сервисы</h2>
<button class="action-btn" id="addProxyBtn">
<i class="fas fa-plus"></i>
<span>Добавить прокси</span>
</button>
</div>
<div class="table-container"> <div class="table-container">
<table class="data-table" id="proxyTable"> <table class="data-table" id="proxyTable">
<thead> <thead>
@@ -299,6 +299,25 @@
</div> </div>
</div> </div>
</div> </div>
<div class="settings-card">
<h3 class="settings-card-title"><i class="fas fa-certificate"></i> Cert Manager</h3>
<div class="settings-form">
<div class="form-group">
<label class="form-label">Статус:</label>
<div class="toggle-wrapper">
<label class="toggle-switch">
<input type="checkbox" id="acmeEnabled">
<span class="toggle-slider"></span>
</label>
<span class="toggle-label">Cert Manager</span>
</div>
</div>
<div class="form-info">
🔐 Автоматическое получение SSL сертификатов от Let's Encrypt для доменов с включённым "Авто SSL".
</div>
</div>
</div>
</div> </div>
</section> </section>
@@ -589,6 +608,7 @@
<label class="form-label">Режим сертификата:</label> <label class="form-label">Режим сертификата:</label>
<select class="form-input" id="certMode" onchange="toggleCertUpload()"> <select class="form-input" id="certMode" onchange="toggleCertUpload()">
<option value="none">Без сертификата (fallback)</option> <option value="none">Без сертификата (fallback)</option>
<option value="auto">Автоматическое создание сертификата</option>
<option value="upload">Загрузить файлы сертификата</option> <option value="upload">Загрузить файлы сертификата</option>
</select> </select>
</div> </div>
@@ -641,6 +661,199 @@
</div> </div>
</div> </div>
</section> </section>
<section class="section" id="sectionAddProxy" style="display: none;">
<div class="vaccess-page">
<!-- Хлебные крошки -->
<div class="breadcrumbs">
<div class="breadcrumbs-left">
<button class="breadcrumb-item" onclick="backToMainFromAddProxy()">
<i class="fas fa-arrow-left"></i> Назад
</button>
<span class="breadcrumb-separator">/</span>
<span class="breadcrumb-item active">Добавить прокси сервис</span>
</div>
</div>
<!-- Заголовок -->
<div class="vaccess-header">
<div class="vaccess-title-block">
<h2 class="vaccess-title">
<i class="fas fa-plus-circle"></i>
<span>Создание прокси сервиса</span>
</h2>
<p class="vaccess-subtitle">Настройте проксирование внешнего домена на локальный сервис</p>
</div>
<div class="vaccess-actions">
<button class="action-btn save-btn" id="createProxyBtn">
<i class="fas fa-check"></i>
<span>Создать прокси</span>
</button>
</div>
</div>
<!-- Форма создания прокси -->
<div class="vaccess-tab-content">
<div class="site-creator-form">
<!-- Единая форма -->
<div class="form-section">
<div class="settings-form">
<!-- Основная информация -->
<h3 class="form-subsection-title"><i class="fas fa-info-circle"></i> Основная информация</h3>
<div class="form-group">
<label class="form-label">Внешний домен: <span style="color: #e74c3c;">*</span></label>
<input type="text" class="form-input" id="newProxyDomain" placeholder="example.com">
<small style="color: #95a5a6; display: block; margin-top: 5px;">
<i class="fas fa-info-circle"></i> Домен, на который будут приходить запросы (например: git.example.ru)
</small>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Локальный адрес: <span style="color: #e74c3c;">*</span></label>
<input type="text" class="form-input" id="newProxyLocalAddr" placeholder="127.0.0.1" value="127.0.0.1">
</div>
<div class="form-group">
<label class="form-label">Локальный порт: <span style="color: #e74c3c;">*</span></label>
<input type="text" class="form-input" id="newProxyLocalPort" placeholder="3000">
</div>
</div>
<div class="form-group">
<label class="form-label">Статус:</label>
<select class="form-input" id="newProxyStatus">
<option value="enable">Включён</option>
<option value="disable">Отключён</option>
</select>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">HTTPS к сервису:</label>
<div class="toggle-wrapper">
<label class="toggle-switch">
<input type="checkbox" id="newProxyServiceHTTPS">
<span class="toggle-slider"></span>
</label>
<span class="toggle-label">Включён</span>
</div>
<small style="color: #95a5a6; display: block; margin-top: 5px;">
<i class="fas fa-info-circle"></i> Использовать HTTPS при подключении к локальному сервису
</small>
</div>
<div class="form-group">
<label class="form-label">Авто HTTPS:</label>
<div class="toggle-wrapper">
<label class="toggle-switch">
<input type="checkbox" id="newProxyAutoHTTPS" checked>
<span class="toggle-slider"></span>
</label>
<span class="toggle-label">Включён</span>
</div>
<small style="color: #95a5a6; display: block; margin-top: 5px;">
<i class="fas fa-info-circle"></i> Автоматически перенаправлять HTTP запросы на HTTPS
</small>
</div>
</div>
<!-- SSL Сертификаты -->
<h3 class="form-subsection-title"><i class="fas fa-lock"></i> SSL Сертификаты (опционально)</h3>
<div class="form-group">
<label class="form-label">Режим сертификата:</label>
<select class="form-input" id="proxyCertMode" onchange="toggleProxyCertUpload()">
<option value="none">Без сертификата (fallback)</option>
<option value="auto">Автоматическое создание сертификата</option>
<option value="upload">Загрузить файлы сертификата</option>
</select>
</div>
<div id="proxyCertUploadBlock" style="display: none;">
<div class="form-group">
<label class="form-label">Certificate (*.crt): <span style="color: #e74c3c;">*</span></label>
<div class="file-upload-wrapper">
<input type="file" class="file-input" id="proxyCertFile" accept=".crt,.pem" onchange="handleProxyCertFileSelect(this, 'certificate')">
<label for="proxyCertFile" class="file-upload-btn">
<i class="fas fa-file-upload"></i>
<span id="proxyCertFileName">Выберите файл...</span>
</label>
</div>
<div id="proxyCertFileStatus" style="margin-top: 8px;"></div>
</div>
<div class="form-group">
<label class="form-label">Private Key (*.key): <span style="color: #e74c3c;">*</span></label>
<div class="file-upload-wrapper">
<input type="file" class="file-input" id="proxyKeyFile" accept=".key,.pem" onchange="handleProxyCertFileSelect(this, 'privatekey')">
<label for="proxyKeyFile" class="file-upload-btn">
<i class="fas fa-file-upload"></i>
<span id="proxyKeyFileName">Выберите файл...</span>
</label>
</div>
<div id="proxyKeyFileStatus" style="margin-top: 8px;"></div>
</div>
<div class="form-group">
<label class="form-label">CA Bundle (*.crt):</label>
<div class="file-upload-wrapper">
<input type="file" class="file-input" id="proxyCaFile" accept=".crt,.pem" onchange="handleProxyCertFileSelect(this, 'cabundle')">
<label for="proxyCaFile" class="file-upload-btn">
<i class="fas fa-file-upload"></i>
<span id="proxyCaFileName">Выберите файл...</span>
</label>
</div>
<div id="proxyCaFileStatus" style="margin-top: 8px;"></div>
<small style="color: #95a5a6; display: block; margin-top: 5px;">
<i class="fas fa-info-circle"></i> CA Bundle опционален, но рекомендуется для полной цепочки сертификации
</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="section" id="sectionCertManager" style="display: none;">
<div class="vaccess-page">
<!-- Хлебные крошки -->
<div class="breadcrumbs">
<div class="breadcrumbs-left">
<button class="breadcrumb-item" onclick="backFromCertManager()">
<i class="fas fa-arrow-left"></i> Назад
</button>
<span class="breadcrumb-separator">/</span>
<span class="breadcrumb-item active" id="certManagerBreadcrumb">Сертификаты</span>
</div>
</div>
<!-- Заголовок -->
<div class="vaccess-header">
<div class="vaccess-title-block">
<h2 class="vaccess-title" id="certManagerTitle">
<i class="fas fa-shield-alt"></i>
<span>Управление сертификатами</span>
</h2>
<p class="vaccess-subtitle" id="certManagerSubtitle">Просмотр и управление SSL сертификатами для домена</p>
</div>
</div>
<!-- Контент -->
<div class="vaccess-tab-content">
<div class="cert-manager-content" id="certManagerContent">
<!-- Сертификаты будут загружены сюда -->
</div>
</div>
</div>
</section>
</main> </main>
<footer class="footer"> <footer class="footer">

View File

@@ -28,11 +28,11 @@
</div> </div>
<div class="tags-container" id="aliasTagsContainer"></div> <div class="tags-container" id="aliasTagsContainer"></div>
</div> </div>
<div class="form-group">
<label class="form-label">Root файл:</label>
<input type="text" class="form-input" id="editRootFile">
</div>
<div class="form-row"> <div class="form-row">
<div class="form-group">
<label class="form-label">Root файл:</label>
<input type="text" class="form-input" id="editRootFile">
</div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Роутинг:</label> <label class="form-label">Роутинг:</label>
<div class="toggle-wrapper"> <div class="toggle-wrapper">
@@ -43,6 +43,16 @@
<span class="toggle-label">Включён</span> <span class="toggle-label">Включён</span>
</div> </div>
</div> </div>
<div class="form-group">
<label class="form-label">Авто SSL:</label>
<div class="toggle-wrapper">
<label class="toggle-switch">
<input type="checkbox" id="editAutoCreateSSL">
<span class="toggle-slider"></span>
</label>
<span class="toggle-label">Включён</span>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -75,7 +85,7 @@
<input type="text" class="form-input" id="editLocalPort"> <input type="text" class="form-input" id="editLocalPort">
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row form-row-3">
<div class="form-group"> <div class="form-group">
<label class="form-label">HTTPS к сервису:</label> <label class="form-label">HTTPS к сервису:</label>
<div class="toggle-wrapper"> <div class="toggle-wrapper">
@@ -96,6 +106,16 @@
<span class="toggle-label">Включён</span> <span class="toggle-label">Включён</span>
</div> </div>
</div> </div>
<div class="form-group">
<label class="form-label">Авто SSL:</label>
<div class="toggle-wrapper">
<label class="toggle-switch">
<input type="checkbox" id="editProxyAutoCreateSSL">
<span class="toggle-slider"></span>
</label>
<span class="toggle-label">Включён</span>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -295,6 +295,26 @@ func (a *App) DisableProxyService() string {
return "Proxy disabled" return "Proxy disabled"
} }
func (a *App) EnableACMEService() string {
config.ConfigData.Soft_Settings.ACME_enabled = true
// Сохраняем в файл
configJSON, _ := json.MarshalIndent(config.ConfigData, "", " ")
os.WriteFile(config.ConfigPath, configJSON, 0644)
return "ACME enabled"
}
func (a *App) DisableACMEService() string {
config.ConfigData.Soft_Settings.ACME_enabled = false
// Сохраняем в файл
configJSON, _ := json.MarshalIndent(config.ConfigData, "", " ")
os.WriteFile(config.ConfigPath, configJSON, 0644)
return "ACME disabled"
}
func (a *App) OpenSiteFolder(host string) string { func (a *App) OpenSiteFolder(host string) string {
folderPath := "WebServer/www/" + host folderPath := "WebServer/www/" + host
@@ -422,4 +442,26 @@ func (a *App) ObtainAllSSLCertificates() string {
} }
return fmt.Sprintf("Completed: %d success, %d errors", successCount, errorCount) return fmt.Sprintf("Completed: %d success, %d errors", successCount, errorCount)
}
// GetCertInfo получает информацию о сертификате для домена
func (a *App) GetCertInfo(domain string) acme.CertInfo {
return acme.GetCertInfo(domain)
}
// GetAllCertsInfo получает информацию о всех сертификатах
func (a *App) GetAllCertsInfo() []acme.CertInfo {
return acme.GetAllCertsInfo()
}
// DeleteCertificate удаляет сертификат для домена
func (a *App) DeleteCertificate(domain string) string {
err := acme.DeleteCertificate(domain)
if err != nil {
return "Error: " + err.Error()
}
// Перезагружаем сертификаты после удаления
webserver.ReloadCertificates()
return "Certificate deleted successfully"
} }

View File

@@ -20,6 +20,7 @@ func GetProxyList() []ProxyInfo {
LocalPort: proxyConfig.LocalPort, LocalPort: proxyConfig.LocalPort,
ServiceHTTPSuse: proxyConfig.ServiceHTTPSuse, ServiceHTTPSuse: proxyConfig.ServiceHTTPSuse,
AutoHTTPS: proxyConfig.AutoHTTPS, AutoHTTPS: proxyConfig.AutoHTTPS,
AutoCreateSSL: proxyConfig.AutoCreateSSL,
Status: status, Status: status,
} }
proxies = append(proxies, proxyInfo) proxies = append(proxies, proxyInfo)

View File

@@ -7,7 +7,7 @@ type ProxyInfo struct {
LocalPort string `json:"local_port"` LocalPort string `json:"local_port"`
ServiceHTTPSuse bool `json:"service_https_use"` ServiceHTTPSuse bool `json:"service_https_use"`
AutoHTTPS bool `json:"auto_https"` AutoHTTPS bool `json:"auto_https"`
AutoCreateSSL bool `json:"AutoCreateSSL"` AutoCreateSSL bool `json:"auto_create_ssl"`
Status string `json:"status"` Status string `json:"status"`
} }

View File

@@ -15,6 +15,7 @@ func GetSitesList() []SiteInfo {
Status: site.Status, Status: site.Status,
RootFile: site.Root_file, RootFile: site.Root_file,
RootFileRouting: site.Root_file_routing, RootFileRouting: site.Root_file_routing,
AutoCreateSSL: site.AutoCreateSSL,
} }
sites = append(sites, siteInfo) sites = append(sites, siteInfo)
} }

View File

@@ -7,6 +7,6 @@ type SiteInfo struct {
Status string `json:"status"` Status string `json:"status"`
RootFile string `json:"root_file"` RootFile string `json:"root_file"`
RootFileRouting bool `json:"root_file_routing"` RootFileRouting bool `json:"root_file_routing"`
AutoCreateSSL bool `json:"AutoCreateSSL"` AutoCreateSSL bool `json:"auto_create_ssl"`
} }