Улучшен фронт
1. Добавлен функционал в интерфейс по управлению сертификатами и службой редактирования сертификатов. 2. Добавлена кнопка для добавления прокси и экран редактирования прокси.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Экспортируем единственный экземпляр
|
||||
|
||||
311
Backend/admin/frontend/assets/js/components/proxy-creator.js
Normal file
311
Backend/admin/frontend/assets/js/components/proxy-creator.js
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 `<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() {
|
||||
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 = `
|
||||
<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><span class="badge ${httpsBadge}</span></td>
|
||||
<td><span class="badge ${autoHttpsBadge}</span></td>
|
||||
<td><span class="badge ${statusBadge}">${proxy.status}</span></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>
|
||||
</td>
|
||||
`;
|
||||
@@ -119,6 +191,11 @@ export class ProxyManager {
|
||||
window.editVAccess(host, isProxy);
|
||||
}
|
||||
break;
|
||||
case 'open-certs':
|
||||
if (window.openCertManager) {
|
||||
window.openCertManager(host, isProxy, []);
|
||||
}
|
||||
break;
|
||||
case 'edit-proxy':
|
||||
if (window.editProxy) {
|
||||
window.editProxy(index);
|
||||
|
||||
@@ -47,6 +47,7 @@ export class SiteCreator {
|
||||
hide($('sectionSettings'));
|
||||
hide($('sectionVAccessEditor'));
|
||||
hide($('sectionAddSite'));
|
||||
hide($('sectionAddProxy'));
|
||||
}
|
||||
|
||||
// Вернуться на главную
|
||||
@@ -295,6 +296,9 @@ export class SiteCreator {
|
||||
// Парсим aliases из поля ввода
|
||||
this.parseAliases();
|
||||
|
||||
// Определяем режим сертификата
|
||||
const certMode = $('certMode').value;
|
||||
|
||||
// Собираем данные сайта
|
||||
const siteData = {
|
||||
name: $('newSiteName').value.trim(),
|
||||
@@ -302,7 +306,8 @@ export class SiteCreator {
|
||||
alias: this.aliases,
|
||||
status: $('newSiteStatus').value,
|
||||
root_file: $('newSiteRootFile').value,
|
||||
root_file_routing: $('newSiteRouting').checked
|
||||
root_file_routing: $('newSiteRouting').checked,
|
||||
AutoCreateSSL: certMode === 'auto'
|
||||
};
|
||||
|
||||
// Создаём сайт
|
||||
@@ -317,7 +322,6 @@ export class SiteCreator {
|
||||
notification.success('✅ Сайт успешно создан!', 1500);
|
||||
|
||||
// Загружаем сертификаты если нужно
|
||||
const certMode = $('certMode').value;
|
||||
if (certMode === 'upload') {
|
||||
createBtn.querySelector('span').textContent = 'Загрузка сертификатов...';
|
||||
|
||||
|
||||
@@ -11,45 +11,120 @@ import { $ } from '../utils/dom.js';
|
||||
export class SitesManager {
|
||||
constructor() {
|
||||
this.sitesData = [];
|
||||
this.certsCache = {};
|
||||
this.mockData = [
|
||||
{
|
||||
name: 'Home Voxsel',
|
||||
host: 'home.voxsel.ru',
|
||||
alias: ['home.voxsel.com'],
|
||||
status: 'active',
|
||||
root_file: 'index.html',
|
||||
root_file_routing: true,
|
||||
auto_create_ssl: false
|
||||
},
|
||||
{
|
||||
name: 'Finance',
|
||||
host: 'finance.voxsel.ru',
|
||||
alias: [],
|
||||
status: 'active',
|
||||
root_file: 'index.php',
|
||||
root_file_routing: false,
|
||||
auto_create_ssl: true
|
||||
},
|
||||
{
|
||||
name: 'Локальный сайт',
|
||||
host: '127.0.0.1',
|
||||
alias: ['localhost'],
|
||||
status: 'active',
|
||||
root_file: 'index.html',
|
||||
root_file_routing: true
|
||||
},
|
||||
{
|
||||
name: 'Тестовый проект',
|
||||
host: 'test.local',
|
||||
alias: ['*.test.local', 'test.com'],
|
||||
status: 'active',
|
||||
root_file: 'index.php',
|
||||
root_file_routing: false
|
||||
},
|
||||
{
|
||||
name: 'API сервис',
|
||||
host: 'api.example.com',
|
||||
alias: ['*.api.example.com'],
|
||||
status: 'inactive',
|
||||
root_file: 'index.php',
|
||||
root_file_routing: true
|
||||
root_file_routing: true,
|
||||
auto_create_ssl: false
|
||||
}
|
||||
];
|
||||
this.mockCerts = {
|
||||
'voxsel.ru': { has_cert: true, is_expired: false, days_left: 79, dns_names: ['*.voxsel.com', '*.voxsel.ru', 'voxsel.com', 'voxsel.ru'] },
|
||||
'finance.voxsel.ru': { has_cert: true, is_expired: false, days_left: 89, dns_names: ['finance.voxsel.ru'] }
|
||||
};
|
||||
}
|
||||
|
||||
// Загрузить список сайтов
|
||||
async load() {
|
||||
if (isWailsAvailable()) {
|
||||
this.sitesData = await api.getSitesList();
|
||||
await this.loadCertsInfo();
|
||||
} else {
|
||||
// Используем тестовые данные если Wails недоступен
|
||||
this.sitesData = this.mockData;
|
||||
this.certsCache = this.mockCerts;
|
||||
}
|
||||
this.render();
|
||||
}
|
||||
|
||||
// Загрузить информацию о сертификатах
|
||||
async loadCertsInfo() {
|
||||
const allCerts = await api.getAllCertsInfo();
|
||||
this.certsCache = {};
|
||||
for (const cert of allCerts) {
|
||||
this.certsCache[cert.domain] = cert;
|
||||
}
|
||||
}
|
||||
|
||||
// Проверить соответствие домена wildcard паттерну
|
||||
matchesWildcard(domain, pattern) {
|
||||
if (pattern.startsWith('*.')) {
|
||||
const wildcardBase = pattern.slice(2);
|
||||
const domainParts = domain.split('.');
|
||||
if (domainParts.length >= 2) {
|
||||
const domainBase = domainParts.slice(1).join('.');
|
||||
return domainBase === wildcardBase;
|
||||
}
|
||||
}
|
||||
return domain === pattern;
|
||||
}
|
||||
|
||||
// Найти сертификат для домена (включая wildcard)
|
||||
findCertForDomain(domain) {
|
||||
if (this.certsCache[domain]?.has_cert) {
|
||||
return this.certsCache[domain];
|
||||
}
|
||||
|
||||
const domainParts = domain.split('.');
|
||||
if (domainParts.length >= 2) {
|
||||
const wildcardDomain = '*.' + domainParts.slice(1).join('.');
|
||||
if (this.certsCache[wildcardDomain]?.has_cert) {
|
||||
return this.certsCache[wildcardDomain];
|
||||
}
|
||||
}
|
||||
|
||||
for (const [certDomain, cert] of Object.entries(this.certsCache)) {
|
||||
if (cert.has_cert && cert.dns_names) {
|
||||
for (const dnsName of cert.dns_names) {
|
||||
if (this.matchesWildcard(domain, dnsName)) {
|
||||
return cert;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Получить иконку сертификата для домена
|
||||
getCertIcon(host, aliases = []) {
|
||||
const allDomains = [host, ...aliases.filter(a => !a.includes('*'))];
|
||||
|
||||
for (const domain of allDomains) {
|
||||
const cert = this.findCertForDomain(domain);
|
||||
if (cert) {
|
||||
if (cert.is_expired) {
|
||||
return `<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() {
|
||||
const tbody = $('sitesTable')?.querySelector('tbody');
|
||||
@@ -61,16 +136,18 @@ export class SitesManager {
|
||||
const row = document.createElement('tr');
|
||||
const statusBadge = site.status === 'active' ? 'badge-online' : 'badge-offline';
|
||||
const aliases = site.alias.join(', ');
|
||||
const certIcon = this.getCertIcon(site.host, site.alias);
|
||||
|
||||
row.innerHTML = `
|
||||
<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>${aliases}</code></td>
|
||||
<td><span class="badge ${statusBadge}">${site.status}</span></td>
|
||||
<td><code>${site.root_file}</code></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="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>
|
||||
</td>
|
||||
`;
|
||||
@@ -118,6 +195,13 @@ export class SitesManager {
|
||||
window.editVAccess(host, isProxy);
|
||||
}
|
||||
break;
|
||||
case 'open-certs':
|
||||
if (window.openCertManager) {
|
||||
const aliasesStr = btn.getAttribute('data-aliases') || '';
|
||||
const aliases = aliasesStr ? aliasesStr.split(',').filter(a => a) : [];
|
||||
window.openCertManager(host, isProxy, aliases);
|
||||
}
|
||||
break;
|
||||
case 'edit-site':
|
||||
if (window.editSite) {
|
||||
window.editSite(index);
|
||||
|
||||
@@ -13,6 +13,7 @@ import { SitesManager } from './components/sites.js';
|
||||
import { ProxyManager } from './components/proxy.js';
|
||||
import { VAccessManager } from './components/vaccess.js';
|
||||
import { SiteCreator } from './components/site-creator.js';
|
||||
import { ProxyCreator } from './components/proxy-creator.js';
|
||||
import { api } from './api/wails.js';
|
||||
import { configAPI } from './api/config.js';
|
||||
import { initCustomSelects } from './ui/custom-select.js';
|
||||
@@ -28,6 +29,7 @@ class App {
|
||||
this.proxyManager = new ProxyManager();
|
||||
this.vAccessManager = new VAccessManager();
|
||||
this.siteCreator = new SiteCreator();
|
||||
this.proxyCreator = new ProxyCreator();
|
||||
|
||||
this.isWails = isWailsAvailable();
|
||||
}
|
||||
@@ -109,6 +111,14 @@ class App {
|
||||
});
|
||||
}
|
||||
|
||||
// Кнопка добавления прокси
|
||||
const addProxyBtn = $('addProxyBtn');
|
||||
if (addProxyBtn) {
|
||||
addProxyBtn.addEventListener('click', () => {
|
||||
this.proxyCreator.open();
|
||||
});
|
||||
}
|
||||
|
||||
// Кнопка сохранения настроек
|
||||
const saveSettingsBtn = $('saveSettingsBtn');
|
||||
if (saveSettingsBtn) {
|
||||
@@ -135,6 +145,22 @@ class App {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Моментальное переключение ACME без перезапуска
|
||||
const acmeCheckbox = $('acmeEnabled');
|
||||
if (acmeCheckbox) {
|
||||
acmeCheckbox.addEventListener('change', async (e) => {
|
||||
const isEnabled = e.target.checked;
|
||||
|
||||
if (isEnabled) {
|
||||
await configAPI.enableACMEService();
|
||||
notification.success('Cert Manager включен', 1000);
|
||||
} else {
|
||||
await configAPI.disableACMEService();
|
||||
notification.success('Cert Manager отключен', 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Настроить глобальные обработчики
|
||||
@@ -143,15 +169,29 @@ class App {
|
||||
// Ссылки на менеджеры
|
||||
sitesManager: this.sitesManager,
|
||||
siteCreator: this.siteCreator,
|
||||
proxyCreator: this.proxyCreator,
|
||||
proxyManager: this.proxyManager,
|
||||
|
||||
// SiteCreator
|
||||
backToMainFromAddSite: () => this.siteCreator.backToMain(),
|
||||
toggleCertUpload: () => this.siteCreator.toggleCertUpload(),
|
||||
handleCertFileSelect: (input, certType) => this.siteCreator.handleCertFile(input, certType),
|
||||
|
||||
// ProxyCreator
|
||||
backToMainFromAddProxy: () => this.proxyCreator.backToMain(),
|
||||
toggleProxyCertUpload: () => this.proxyCreator.toggleCertUpload(),
|
||||
handleProxyCertFileSelect: (input, certType) => this.proxyCreator.handleCertFile(input, certType),
|
||||
|
||||
// vAccess
|
||||
editVAccess: (host, isProxy) => this.vAccessManager.open(host, isProxy),
|
||||
backToMain: () => this.vAccessManager.backToMain(),
|
||||
|
||||
// CertManager
|
||||
openCertManager: (host, isProxy, aliases) => this.openCertManager(host, isProxy, aliases),
|
||||
backFromCertManager: () => this.backFromCertManager(),
|
||||
deleteCertificate: async (domain) => await this.deleteCertificate(domain),
|
||||
renewCertificate: async (domain) => await this.renewCertificate(domain),
|
||||
issueCertificate: async (domain) => await this.issueCertificate(domain),
|
||||
switchVAccessTab: (tab) => this.vAccessManager.switchTab(tab),
|
||||
saveVAccessChanges: async () => await this.vAccessManager.save(),
|
||||
addVAccessRule: () => this.vAccessManager.addRule(),
|
||||
@@ -209,6 +249,7 @@ class App {
|
||||
$('phpHost').value = 'localhost';
|
||||
$('phpPort').value = 8000;
|
||||
$('proxyEnabled').checked = true;
|
||||
$('acmeEnabled').checked = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -220,6 +261,7 @@ class App {
|
||||
$('phpHost').value = config.Soft_Settings?.php_host || 'localhost';
|
||||
$('phpPort').value = config.Soft_Settings?.php_port || 8000;
|
||||
$('proxyEnabled').checked = config.Soft_Settings?.proxy_enabled !== false;
|
||||
$('acmeEnabled').checked = config.Soft_Settings?.ACME_enabled !== false;
|
||||
}
|
||||
|
||||
// Сохранить настройки конфигурации
|
||||
@@ -294,6 +336,9 @@ class App {
|
||||
if (editRootFile) editRootFile.value = site.root_file;
|
||||
if (editRouting) editRouting.checked = site.root_file_routing;
|
||||
|
||||
const editAutoCreateSSL = $('editAutoCreateSSL');
|
||||
if (editAutoCreateSSL) editAutoCreateSSL.checked = site.auto_create_ssl || false;
|
||||
|
||||
// Добавляем alias теги
|
||||
const aliasContainer = $('aliasTagsContainer');
|
||||
if (aliasContainer) {
|
||||
@@ -348,6 +393,9 @@ class App {
|
||||
if (editServiceHTTPS) editServiceHTTPS.checked = proxy.service_https_use;
|
||||
if (editAutoHTTPS) editAutoHTTPS.checked = proxy.auto_https;
|
||||
|
||||
const editProxyAutoCreateSSL = $('editProxyAutoCreateSSL');
|
||||
if (editProxyAutoCreateSSL) editProxyAutoCreateSSL.checked = proxy.auto_create_ssl || false;
|
||||
|
||||
// Привязываем обработчик кнопок статуса
|
||||
document.querySelectorAll('.status-btn').forEach(btn => {
|
||||
btn.onclick = () => this.setModalStatus(btn.dataset.value);
|
||||
@@ -425,7 +473,8 @@ class App {
|
||||
alias: aliases,
|
||||
status: statusBtn ? statusBtn.dataset.value : 'active',
|
||||
root_file: $('editRootFile').value,
|
||||
root_file_routing: $('editRouting').checked
|
||||
root_file_routing: $('editRouting').checked,
|
||||
AutoCreateSSL: $('editAutoCreateSSL')?.checked || false
|
||||
};
|
||||
|
||||
const result = await configAPI.saveConfig(JSON.stringify(config, null, 4));
|
||||
@@ -453,7 +502,8 @@ class App {
|
||||
LocalAddress: $('editLocalAddr').value,
|
||||
LocalPort: $('editLocalPort').value,
|
||||
ServiceHTTPSuse: $('editServiceHTTPS').checked,
|
||||
AutoHTTPS: $('editAutoHTTPS').checked
|
||||
AutoHTTPS: $('editAutoHTTPS').checked,
|
||||
AutoCreateSSL: $('editProxyAutoCreateSSL')?.checked || false
|
||||
};
|
||||
|
||||
const result = await configAPI.saveConfig(JSON.stringify(config, null, 4));
|
||||
@@ -541,6 +591,404 @@ class App {
|
||||
notification.error('Ошибка: ' + error.message, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
// ====== Cert Manager ======
|
||||
|
||||
certManagerHost = null;
|
||||
certManagerIsProxy = false;
|
||||
certManagerAliases = [];
|
||||
|
||||
async openCertManager(host, isProxy = false, aliases = []) {
|
||||
this.certManagerHost = host;
|
||||
this.certManagerIsProxy = isProxy;
|
||||
this.certManagerAliases = aliases.filter(a => !a.includes('*'));
|
||||
|
||||
// Обновляем заголовки
|
||||
$('certManagerBreadcrumb').textContent = `Сертификаты: ${host}`;
|
||||
const titleSpan = $('certManagerTitle').querySelector('span');
|
||||
if (titleSpan) titleSpan.textContent = host;
|
||||
$('certManagerSubtitle').textContent = isProxy ? 'Прокси сервис' : 'Веб-сайт';
|
||||
|
||||
// Скрываем все секции, показываем CertManager
|
||||
this.hideAllSectionsForCertManager();
|
||||
$('sectionCertManager').style.display = 'block';
|
||||
|
||||
// Загружаем сертификаты
|
||||
await this.loadCertManagerContent(host, this.certManagerAliases);
|
||||
}
|
||||
|
||||
hideAllSectionsForCertManager() {
|
||||
const sections = ['sectionServices', 'sectionSites', 'sectionProxy', 'sectionSettings', 'sectionVAccessEditor', 'sectionAddSite', 'sectionCertManager'];
|
||||
sections.forEach(id => {
|
||||
const el = $(id);
|
||||
if (el) el.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// Тестовые данные для сертификатов (браузерный режим)
|
||||
mockCertsData = [
|
||||
{
|
||||
domain: 'voxsel.ru',
|
||||
issuer: 'R13',
|
||||
not_before: '2026-01-07',
|
||||
not_after: '2026-04-07',
|
||||
days_left: 79,
|
||||
is_expired: false,
|
||||
has_cert: true,
|
||||
dns_names: ['*.voxsel.com', '*.voxsel.ru', 'voxsel.com', 'voxsel.ru']
|
||||
},
|
||||
{
|
||||
domain: 'finance.voxsel.ru',
|
||||
issuer: 'E8',
|
||||
not_before: '2026-01-17',
|
||||
not_after: '2026-04-17',
|
||||
days_left: 89,
|
||||
is_expired: false,
|
||||
has_cert: true,
|
||||
dns_names: ['finance.voxsel.ru']
|
||||
},
|
||||
{
|
||||
domain: 'test.local',
|
||||
issuer: "Let's Encrypt",
|
||||
not_before: '2025-01-01',
|
||||
not_after: '2025-03-31',
|
||||
days_left: 73,
|
||||
is_expired: false,
|
||||
has_cert: true,
|
||||
dns_names: ['test.local', '*.test.local', 'test.com']
|
||||
},
|
||||
{
|
||||
domain: 'api.example.com',
|
||||
issuer: "Let's Encrypt",
|
||||
not_before: '2024-10-01',
|
||||
not_after: '2024-12-30',
|
||||
days_left: -18,
|
||||
is_expired: true,
|
||||
has_cert: true,
|
||||
dns_names: ['api.example.com', '*.api.example.com']
|
||||
}
|
||||
];
|
||||
|
||||
async loadCertManagerContent(host, aliases = []) {
|
||||
const container = $('certManagerContent');
|
||||
container.innerHTML = '<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
|
||||
|
||||
@@ -8,7 +8,7 @@ import { $, $$, hide, show, removeClass, addClass } from '../utils/dom.js';
|
||||
// Класс для управления навигацией
|
||||
export class Navigation {
|
||||
constructor() {
|
||||
this.navItems = $$('.nav-item');
|
||||
this.navItems = $$('.nav-item[data-page]');
|
||||
this.sections = {
|
||||
services: $('sectionServices'),
|
||||
sites: $('sectionSites'),
|
||||
@@ -21,32 +21,35 @@ export class Navigation {
|
||||
}
|
||||
|
||||
init() {
|
||||
this.navItems.forEach((item, index) => {
|
||||
item.addEventListener('click', () => this.navigate(index));
|
||||
this.navItems.forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
const page = item.dataset.page;
|
||||
this.navigate(page, item);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
navigate(index) {
|
||||
navigate(page, clickedItem) {
|
||||
// Убираем active со всех навигационных элементов
|
||||
this.navItems.forEach(nav => removeClass(nav, 'active'));
|
||||
addClass(this.navItems[index], 'active');
|
||||
addClass(clickedItem, 'active');
|
||||
|
||||
// Скрываем все секции
|
||||
this.hideAllSections();
|
||||
|
||||
// Показываем нужные секции
|
||||
if (index === 0) {
|
||||
// Главная - всё кроме настроек
|
||||
show(this.sections.services);
|
||||
show(this.sections.sites);
|
||||
show(this.sections.proxy);
|
||||
} else if (index === 3) {
|
||||
// Настройки
|
||||
show(this.sections.settings);
|
||||
// Загружаем конфигурацию при открытии
|
||||
if (window.loadConfig) {
|
||||
window.loadConfig();
|
||||
}
|
||||
// Показываем нужные секции по имени страницы
|
||||
switch (page) {
|
||||
case 'dashboard':
|
||||
show(this.sections.services);
|
||||
show(this.sections.sites);
|
||||
show(this.sections.proxy);
|
||||
break;
|
||||
case 'settings':
|
||||
show(this.sections.settings);
|
||||
if (window.loadConfig) {
|
||||
window.loadConfig();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,16 +40,10 @@
|
||||
|
||||
<aside class="sidebar">
|
||||
<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>
|
||||
</button>
|
||||
<button class="nav-item" 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="Настройки">
|
||||
<button class="nav-item" data-page="settings" title="Настройки">
|
||||
<i class="fas fa-cog"></i>
|
||||
</button>
|
||||
</nav>
|
||||
@@ -191,7 +185,13 @@
|
||||
</section>
|
||||
|
||||
<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">
|
||||
<table class="data-table" id="proxyTable">
|
||||
<thead>
|
||||
@@ -299,6 +299,25 @@
|
||||
</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>
|
||||
</section>
|
||||
|
||||
@@ -589,6 +608,7 @@
|
||||
<label class="form-label">Режим сертификата:</label>
|
||||
<select class="form-input" id="certMode" onchange="toggleCertUpload()">
|
||||
<option value="none">Без сертификата (fallback)</option>
|
||||
<option value="auto">Автоматическое создание сертификата</option>
|
||||
<option value="upload">Загрузить файлы сертификата</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -641,6 +661,199 @@
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<footer class="footer">
|
||||
|
||||
@@ -28,11 +28,11 @@
|
||||
</div>
|
||||
<div class="tags-container" id="aliasTagsContainer"></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-group">
|
||||
<label class="form-label">Root файл:</label>
|
||||
<input type="text" class="form-input" id="editRootFile">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Роутинг:</label>
|
||||
<div class="toggle-wrapper">
|
||||
@@ -43,6 +43,16 @@
|
||||
<span class="toggle-label">Включён</span>
|
||||
</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>
|
||||
</template>
|
||||
@@ -75,7 +85,7 @@
|
||||
<input type="text" class="form-input" id="editLocalPort">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-row form-row-3">
|
||||
<div class="form-group">
|
||||
<label class="form-label">HTTPS к сервису:</label>
|
||||
<div class="toggle-wrapper">
|
||||
@@ -96,6 +106,16 @@
|
||||
<span class="toggle-label">Включён</span>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -295,6 +295,26 @@ func (a *App) DisableProxyService() string {
|
||||
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 {
|
||||
folderPath := "WebServer/www/" + host
|
||||
|
||||
@@ -422,4 +442,26 @@ func (a *App) ObtainAllSSLCertificates() string {
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
@@ -20,6 +20,7 @@ func GetProxyList() []ProxyInfo {
|
||||
LocalPort: proxyConfig.LocalPort,
|
||||
ServiceHTTPSuse: proxyConfig.ServiceHTTPSuse,
|
||||
AutoHTTPS: proxyConfig.AutoHTTPS,
|
||||
AutoCreateSSL: proxyConfig.AutoCreateSSL,
|
||||
Status: status,
|
||||
}
|
||||
proxies = append(proxies, proxyInfo)
|
||||
|
||||
@@ -7,7 +7,7 @@ type ProxyInfo struct {
|
||||
LocalPort string `json:"local_port"`
|
||||
ServiceHTTPSuse bool `json:"service_https_use"`
|
||||
AutoHTTPS bool `json:"auto_https"`
|
||||
AutoCreateSSL bool `json:"AutoCreateSSL"`
|
||||
AutoCreateSSL bool `json:"auto_create_ssl"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ func GetSitesList() []SiteInfo {
|
||||
Status: site.Status,
|
||||
RootFile: site.Root_file,
|
||||
RootFileRouting: site.Root_file_routing,
|
||||
AutoCreateSSL: site.AutoCreateSSL,
|
||||
}
|
||||
sites = append(sites, siteInfo)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,6 @@ type SiteInfo struct {
|
||||
Status string `json:"status"`
|
||||
RootFile string `json:"root_file"`
|
||||
RootFileRouting bool `json:"root_file_routing"`
|
||||
AutoCreateSSL bool `json:"AutoCreateSSL"`
|
||||
AutoCreateSSL bool `json:"auto_create_ssl"`
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user