Улучшен фронт
1. Добавлен функционал в интерфейс по управлению сертификатами и службой редактирования сертификатов. 2. Добавлена кнопка для добавления прокси и экран редактирования прокси.
This commit is contained in:
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);
|
||||
|
||||
Reference in New Issue
Block a user