Добавление и Удаление сайта
Backend (Go): - Добавлен полный функционал создания сайтов - Добавлен функционал удаления сайтов - Новые API методы в admin.go: - Добавлен шаблон стартовой страницы - Добавлена функция DecodeBase64 Исправления критических ошибок: - Исправлена работа wildcard алиасов (*.domain.com) в handler.go - Исправлены ошибки "файл не найден" при создании файлов Frontend (JavaScript + HTML + CSS): - Добавлена страница создания сайта - Добавлена кнопка "Удалить сайт" в редактировании - Мелкие доработки стилей Build: - Обновлён build_admin.ps1 - добавлен шаг генерации биндингов (wails generate module) Fixes: - #fix Wildcard алиасы (*.domain.com) теперь работают корректно - #fix Удалён порт из host при проверке алиасов - #fix Приоритет точных доменов над wildcard - #fix Ошибки "файл не найден" при создании сайтов/vAccess - #fix Секция добавления сайта теперь скрывается при навигации
This commit is contained in:
@@ -62,6 +62,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Delete Button Variant */
|
||||
.delete-btn {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
border-color: rgba(239, 68, 68, 0.3);
|
||||
color: var(--accent-red);
|
||||
|
||||
&: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);
|
||||
}
|
||||
}
|
||||
|
||||
/* Icon Button - Квадратная кнопка с иконкой */
|
||||
.icon-btn {
|
||||
width: 32px;
|
||||
|
||||
@@ -91,11 +91,15 @@
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-lg);
|
||||
gap: 8px;
|
||||
padding: 20px var(--space-lg);
|
||||
border-top: 1px solid var(--glass-border);
|
||||
}
|
||||
|
||||
.modal-footer .action-btn {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Notification Modal */
|
||||
.notification {
|
||||
position: fixed;
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
margin-top: var(--space-3xl);
|
||||
margin-top: var(--space-lg);
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
|
||||
@@ -23,4 +23,5 @@
|
||||
/* 4. Pages */
|
||||
@import 'pages/dashboard.css';
|
||||
@import 'pages/vaccess.css';
|
||||
@import 'pages/site-creator.css';
|
||||
|
||||
|
||||
272
Backend/admin/frontend/assets/css/pages/site-creator.css
Normal file
272
Backend/admin/frontend/assets/css/pages/site-creator.css
Normal file
@@ -0,0 +1,272 @@
|
||||
/* ============================================
|
||||
Site Creator Page
|
||||
Страница создания нового сайта
|
||||
============================================ */
|
||||
|
||||
/* Form Section */
|
||||
.form-section {
|
||||
padding: var(--space-xl);
|
||||
background: rgba(139, 92, 246, 0.02);
|
||||
border-radius: var(--radius-xl);
|
||||
border: 1px solid var(--glass-border);
|
||||
transition: all var(--transition-base);
|
||||
}
|
||||
|
||||
.form-section:hover {
|
||||
background: rgba(139, 92, 246, 0.04);
|
||||
border-color: rgba(139, 92, 246, 0.2);
|
||||
}
|
||||
|
||||
/* Subsection Title (внутри формы) */
|
||||
.form-subsection-title {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 var(--space-lg) 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-md);
|
||||
padding-bottom: var(--space-sm);
|
||||
border-bottom: 1px solid rgba(139, 92, 246, 0.1);
|
||||
|
||||
i {
|
||||
color: var(--accent-purple-light);
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Второй и последующие подзаголовки - добавляем отступ сверху */
|
||||
.form-subsection-title:not(:first-of-type) {
|
||||
margin-top: var(--space-xl);
|
||||
}
|
||||
|
||||
/* Custom Select Styling */
|
||||
.form-input[type="text"],
|
||||
.form-input[type="number"] {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
/* Кастомный Select */
|
||||
.custom-select {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.custom-select-trigger {
|
||||
padding: 10px 40px 10px 14px;
|
||||
background: var(--glass-bg-dark);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-base);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-base);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.custom-select-trigger:hover {
|
||||
border-color: rgba(139, 92, 246, 0.4);
|
||||
background-color: rgba(139, 92, 246, 0.05);
|
||||
}
|
||||
|
||||
.custom-select.open .custom-select-trigger {
|
||||
border-color: rgba(139, 92, 246, 0.6);
|
||||
box-shadow: 0 0 16px rgba(139, 92, 246, 0.2);
|
||||
background-color: rgba(139, 92, 246, 0.03);
|
||||
}
|
||||
|
||||
.custom-select-value {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.custom-select-arrow {
|
||||
color: var(--accent-purple-light);
|
||||
font-size: 12px;
|
||||
transition: transform var(--transition-base);
|
||||
}
|
||||
|
||||
.custom-select.open .custom-select-arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Dropdown */
|
||||
.custom-select-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #1a1d2e;
|
||||
border: 1px solid rgba(139, 92, 246, 0.3);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
transition: all var(--transition-base);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.custom-select.open .custom-select-dropdown {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Scrollbar для dropdown */
|
||||
.custom-select-dropdown::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.custom-select-dropdown::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.custom-select-dropdown::-webkit-scrollbar-thumb {
|
||||
background: rgba(139, 92, 246, 0.3);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.custom-select-dropdown::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(139, 92, 246, 0.5);
|
||||
}
|
||||
|
||||
/* Option */
|
||||
.custom-select-option {
|
||||
padding: 10px 14px;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-base);
|
||||
font-size: var(--text-base);
|
||||
}
|
||||
|
||||
.custom-select-option:hover {
|
||||
background: rgba(139, 92, 246, 0.1);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.custom-select-option.selected {
|
||||
background: rgba(139, 92, 246, 0.2);
|
||||
color: var(--accent-purple-light);
|
||||
font-weight: var(--font-semibold);
|
||||
}
|
||||
|
||||
.custom-select-option.selected::before {
|
||||
content: '✓ ';
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* File Upload Styling */
|
||||
.file-upload-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.file-upload-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-md);
|
||||
padding: 12px 20px;
|
||||
background: var(--glass-bg-dark);
|
||||
border: 2px dashed var(--glass-border);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-base);
|
||||
font-weight: var(--font-medium);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-base);
|
||||
text-align: center;
|
||||
|
||||
i {
|
||||
color: var(--accent-purple-light);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(139, 92, 246, 0.05);
|
||||
border-color: rgba(139, 92, 246, 0.4);
|
||||
color: var(--text-primary);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.file-input:focus + .file-upload-btn {
|
||||
border-color: rgba(139, 92, 246, 0.6);
|
||||
box-shadow: 0 0 16px rgba(139, 92, 246, 0.2);
|
||||
}
|
||||
|
||||
/* Drag over state */
|
||||
.file-upload-wrapper.drag-over .file-upload-btn {
|
||||
background: rgba(139, 92, 246, 0.15);
|
||||
border-color: rgba(139, 92, 246, 0.7);
|
||||
border-style: solid;
|
||||
color: var(--text-primary);
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 0 20px rgba(139, 92, 246, 0.3);
|
||||
}
|
||||
|
||||
.file-upload-wrapper.drag-over .file-upload-btn i {
|
||||
animation: bounce 0.6s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
}
|
||||
|
||||
/* File uploaded state */
|
||||
.file-uploaded {
|
||||
border-style: solid;
|
||||
border-color: rgba(16, 185, 129, 0.5);
|
||||
background: rgba(16, 185, 129, 0.05);
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(16, 185, 129, 0.6);
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
}
|
||||
|
||||
i {
|
||||
color: var(--accent-green);
|
||||
}
|
||||
}
|
||||
|
||||
/* File status */
|
||||
#certFileStatus,
|
||||
#keyFileStatus,
|
||||
#caFileStatus {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
|
||||
i {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +136,58 @@ class WailsAPI {
|
||||
log(`Ошибка открытия папки: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Создать новый сайт
|
||||
*/
|
||||
async createNewSite(siteJSON) {
|
||||
if (!this.checkAvailability()) return 'Error: API недоступен';
|
||||
try {
|
||||
return await window.go.admin.App.CreateNewSite(siteJSON);
|
||||
} catch (error) {
|
||||
log(`Ошибка создания сайта: ${error.message}`, 'error');
|
||||
return `Error: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Загрузить сертификат для сайта
|
||||
*/
|
||||
async uploadCertificate(host, certType, certDataBase64) {
|
||||
if (!this.checkAvailability()) return 'Error: API недоступен';
|
||||
try {
|
||||
return await window.go.admin.App.UploadCertificate(host, certType, certDataBase64);
|
||||
} catch (error) {
|
||||
log(`Ошибка загрузки сертификата: ${error.message}`, 'error');
|
||||
return `Error: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Перезагрузить SSL сертификаты
|
||||
*/
|
||||
async reloadSSLCertificates() {
|
||||
if (!this.checkAvailability()) return 'Error: API недоступен';
|
||||
try {
|
||||
return await window.go.admin.App.ReloadSSLCertificates();
|
||||
} catch (error) {
|
||||
log(`Ошибка перезагрузки сертификатов: ${error.message}`, 'error');
|
||||
return `Error: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Удалить сайт
|
||||
*/
|
||||
async deleteSite(host) {
|
||||
if (!this.checkAvailability()) return 'Error: API недоступен';
|
||||
try {
|
||||
return await window.go.admin.App.DeleteSite(host);
|
||||
} catch (error) {
|
||||
log(`Ошибка удаления сайта: ${error.message}`, 'error');
|
||||
return `Error: ${error.message}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Экспортируем единственный экземпляр
|
||||
|
||||
393
Backend/admin/frontend/assets/js/components/site-creator.js
Normal file
393
Backend/admin/frontend/assets/js/components/site-creator.js
Normal file
@@ -0,0 +1,393 @@
|
||||
/* ============================================
|
||||
Site 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 SiteCreator {
|
||||
constructor() {
|
||||
this.aliases = [];
|
||||
this.certificates = {
|
||||
certificate: null,
|
||||
privatekey: null,
|
||||
cabundle: null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Открыть страницу создания сайта
|
||||
*/
|
||||
open() {
|
||||
// Скрываем все секции
|
||||
this.hideAllSections();
|
||||
|
||||
// Показываем страницу создания
|
||||
show($('sectionAddSite'));
|
||||
|
||||
// Очищаем форму
|
||||
this.resetForm();
|
||||
|
||||
// Привязываем обработчики
|
||||
this.attachEventListeners();
|
||||
|
||||
// Инициализируем кастомные select'ы
|
||||
setTimeout(() => initCustomSelects(), 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Скрыть все секции
|
||||
*/
|
||||
hideAllSections() {
|
||||
hide($('sectionServices'));
|
||||
hide($('sectionSites'));
|
||||
hide($('sectionProxy'));
|
||||
hide($('sectionSettings'));
|
||||
hide($('sectionVAccessEditor'));
|
||||
hide($('sectionAddSite'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Вернуться на главную
|
||||
*/
|
||||
backToMain() {
|
||||
this.hideAllSections();
|
||||
show($('sectionServices'));
|
||||
show($('sectionSites'));
|
||||
show($('sectionProxy'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистить форму
|
||||
*/
|
||||
resetForm() {
|
||||
$('newSiteName').value = '';
|
||||
$('newSiteHost').value = '';
|
||||
$('newSiteAliasInput').value = '';
|
||||
$('newSiteRootFile').value = 'index.html';
|
||||
$('newSiteStatus').value = 'active';
|
||||
$('newSiteRouting').checked = true;
|
||||
$('certMode').value = 'none';
|
||||
|
||||
this.aliases = [];
|
||||
this.certificates = {
|
||||
certificate: null,
|
||||
privatekey: null,
|
||||
cabundle: null
|
||||
};
|
||||
|
||||
// Скрываем блок загрузки сертификатов
|
||||
hide($('certUploadBlock'));
|
||||
|
||||
// Очищаем статусы файлов
|
||||
$('certFileStatus').innerHTML = '';
|
||||
$('keyFileStatus').innerHTML = '';
|
||||
$('caFileStatus').innerHTML = '';
|
||||
|
||||
// Очищаем labels файлов
|
||||
if ($('certFileName')) $('certFileName').textContent = 'Выберите файл...';
|
||||
if ($('keyFileName')) $('keyFileName').textContent = 'Выберите файл...';
|
||||
if ($('caFileName')) $('caFileName').textContent = 'Выберите файл...';
|
||||
|
||||
// Очищаем input файлов
|
||||
if ($('certFile')) $('certFile').value = '';
|
||||
if ($('keyFile')) $('keyFile').value = '';
|
||||
if ($('caFile')) $('caFile').value = '';
|
||||
|
||||
// Убираем класс uploaded
|
||||
const labels = document.querySelectorAll('.file-upload-btn');
|
||||
labels.forEach(label => label.classList.remove('file-uploaded'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Привязать обработчики событий
|
||||
*/
|
||||
attachEventListeners() {
|
||||
const createBtn = $('createSiteBtn');
|
||||
if (createBtn) {
|
||||
createBtn.onclick = async () => await this.createSite();
|
||||
}
|
||||
|
||||
// Drag & Drop для файлов сертификатов
|
||||
this.setupDragAndDrop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Настроить Drag & Drop для файлов
|
||||
*/
|
||||
setupDragAndDrop() {
|
||||
const fileWrappers = [
|
||||
{ wrapper: document.querySelector('label[for="certFile"]')?.parentElement, input: $('certFile'), type: 'certificate' },
|
||||
{ wrapper: document.querySelector('label[for="keyFile"]')?.parentElement, input: $('keyFile'), type: 'privatekey' },
|
||||
{ wrapper: document.querySelector('label[for="caFile"]')?.parentElement, input: $('caFile'), 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');
|
||||
});
|
||||
});
|
||||
|
||||
// Обработка dropped файла
|
||||
wrapper.addEventListener('drop', (e) => {
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
// Создаём объект DataTransfer и присваиваем файлы input'у
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(files[0]);
|
||||
input.files = dataTransfer.files;
|
||||
|
||||
// Триггерим событие change
|
||||
const event = new Event('change', { bubbles: true });
|
||||
input.dispatchEvent(event);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Парсить aliases из строки (через запятую)
|
||||
*/
|
||||
parseAliases() {
|
||||
const input = $('newSiteAliasInput');
|
||||
const value = input?.value.trim();
|
||||
|
||||
if (!value) {
|
||||
this.aliases = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// Разделяем по запятой и очищаем
|
||||
this.aliases = value
|
||||
.split(',')
|
||||
.map(alias => alias.trim())
|
||||
.filter(alias => alias.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Переключить видимость блока загрузки сертификатов
|
||||
*/
|
||||
toggleCertUpload() {
|
||||
const mode = $('certMode')?.value;
|
||||
const block = $('certUploadBlock');
|
||||
|
||||
if (mode === 'upload') {
|
||||
show(block);
|
||||
} else {
|
||||
hide(block);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработать выбор файла сертификата
|
||||
*/
|
||||
handleCertFile(input, certType) {
|
||||
const file = input.files[0];
|
||||
const statusId = certType === 'certificate' ? 'certFileStatus' :
|
||||
certType === 'privatekey' ? 'keyFileStatus' : 'caFileStatus';
|
||||
const labelId = certType === 'certificate' ? 'certFileName' :
|
||||
certType === 'privatekey' ? 'keyFileName' : 'caFileName';
|
||||
|
||||
const statusDiv = $(statusId);
|
||||
const labelSpan = $(labelId);
|
||||
const labelBtn = input.nextElementSibling; // label элемент
|
||||
|
||||
if (!file) {
|
||||
this.certificates[certType] = null;
|
||||
statusDiv.innerHTML = '';
|
||||
if (labelSpan) labelSpan.textContent = 'Выберите файл...';
|
||||
if (labelBtn) labelBtn.classList.remove('file-uploaded');
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем размер файла (макс 1MB)
|
||||
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;
|
||||
}
|
||||
|
||||
// Обновляем UI
|
||||
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;
|
||||
// Сохраняем как base64
|
||||
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 name = $('newSiteName')?.value.trim();
|
||||
const host = $('newSiteHost')?.value.trim();
|
||||
const rootFile = $('newSiteRootFile')?.value;
|
||||
const certMode = $('certMode')?.value;
|
||||
|
||||
if (!name) {
|
||||
notification.error('❌ Укажите название сайта');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!host) {
|
||||
notification.error('❌ Укажите host (домен)');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!rootFile) {
|
||||
notification.error('❌ Укажите root файл');
|
||||
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 createSite() {
|
||||
if (!this.validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isWailsAvailable()) {
|
||||
notification.error('Wails API недоступен');
|
||||
return;
|
||||
}
|
||||
|
||||
const createBtn = $('createSiteBtn');
|
||||
const originalText = createBtn.querySelector('span').textContent;
|
||||
|
||||
try {
|
||||
createBtn.disabled = true;
|
||||
createBtn.querySelector('span').textContent = 'Создание...';
|
||||
|
||||
// Парсим aliases из поля ввода
|
||||
this.parseAliases();
|
||||
|
||||
// Собираем данные сайта
|
||||
const siteData = {
|
||||
name: $('newSiteName').value.trim(),
|
||||
host: $('newSiteHost').value.trim(),
|
||||
alias: this.aliases,
|
||||
status: $('newSiteStatus').value,
|
||||
root_file: $('newSiteRootFile').value,
|
||||
root_file_routing: $('newSiteRouting').checked
|
||||
};
|
||||
|
||||
// Создаём сайт
|
||||
const siteJSON = JSON.stringify(siteData);
|
||||
const result = await api.createNewSite(siteJSON);
|
||||
|
||||
if (result.startsWith('Error')) {
|
||||
notification.error(result, 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
notification.success('✅ Сайт успешно создан!', 1500);
|
||||
|
||||
// Загружаем сертификаты если нужно
|
||||
const certMode = $('certMode').value;
|
||||
if (certMode === 'upload') {
|
||||
createBtn.querySelector('span').textContent = 'Загрузка сертификатов...';
|
||||
|
||||
// Загружаем certificate
|
||||
if (this.certificates.certificate) {
|
||||
await api.uploadCertificate(siteData.host, 'certificate', this.certificates.certificate);
|
||||
}
|
||||
|
||||
// Загружаем private key
|
||||
if (this.certificates.privatekey) {
|
||||
await api.uploadCertificate(siteData.host, 'privatekey', this.certificates.privatekey);
|
||||
}
|
||||
|
||||
// Загружаем ca bundle если есть
|
||||
if (this.certificates.cabundle) {
|
||||
await api.uploadCertificate(siteData.host, 'cabundle', this.certificates.cabundle);
|
||||
}
|
||||
|
||||
notification.success('🔒 Сертификаты загружены!', 1500);
|
||||
}
|
||||
|
||||
// Перезапускаем HTTP/HTTPS
|
||||
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.sitesManager) {
|
||||
window.sitesManager.load();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
notification.error('Ошибка: ' + error.message, 3000);
|
||||
} finally {
|
||||
createBtn.disabled = false;
|
||||
createBtn.querySelector('span').textContent = originalText;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,10 @@ import { ServicesManager } from './components/services.js';
|
||||
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 { api } from './api/wails.js';
|
||||
import { configAPI } from './api/config.js';
|
||||
import { initCustomSelects } from './ui/custom-select.js';
|
||||
import { $ } from './utils/dom.js';
|
||||
|
||||
/**
|
||||
@@ -26,6 +29,7 @@ class App {
|
||||
this.sitesManager = new SitesManager();
|
||||
this.proxyManager = new ProxyManager();
|
||||
this.vAccessManager = new VAccessManager();
|
||||
this.siteCreator = new SiteCreator();
|
||||
|
||||
this.isWails = isWailsAvailable();
|
||||
|
||||
@@ -69,6 +73,9 @@ class App {
|
||||
// Привязываем кнопки
|
||||
this.setupButtons();
|
||||
|
||||
// Инициализируем кастомные select'ы
|
||||
initCustomSelects();
|
||||
|
||||
log('Приложение запущено');
|
||||
}
|
||||
|
||||
@@ -96,6 +103,14 @@ class App {
|
||||
* Привязать кнопки
|
||||
*/
|
||||
setupButtons() {
|
||||
// Кнопка добавления сайта
|
||||
const addSiteBtn = $('addSiteBtn');
|
||||
if (addSiteBtn) {
|
||||
addSiteBtn.addEventListener('click', () => {
|
||||
this.siteCreator.open();
|
||||
});
|
||||
}
|
||||
|
||||
// Кнопка сохранения настроек
|
||||
const saveSettingsBtn = $('saveSettingsBtn');
|
||||
if (saveSettingsBtn) {
|
||||
@@ -128,6 +143,23 @@ class App {
|
||||
* Настроить глобальные обработчики
|
||||
*/
|
||||
setupGlobalHandlers() {
|
||||
// Глобальная ссылка на sitesManager
|
||||
window.sitesManager = this.sitesManager;
|
||||
window.siteCreator = this.siteCreator;
|
||||
|
||||
// Для SiteCreator
|
||||
window.backToMainFromAddSite = () => {
|
||||
this.siteCreator.backToMain();
|
||||
};
|
||||
|
||||
window.toggleCertUpload = () => {
|
||||
this.siteCreator.toggleCertUpload();
|
||||
};
|
||||
|
||||
window.handleCertFileSelect = (input, certType) => {
|
||||
this.siteCreator.handleCertFile(input, certType);
|
||||
};
|
||||
|
||||
// Для vAccess
|
||||
window.editVAccess = (host, isProxy) => {
|
||||
this.vAccessManager.open(host, isProxy);
|
||||
@@ -248,6 +280,10 @@ class App {
|
||||
window.openSiteFolder = async (host) => {
|
||||
await this.sitesManager.handleAction('open-folder', { getAttribute: () => host });
|
||||
};
|
||||
|
||||
window.deleteSiteConfirm = async () => {
|
||||
await this.deleteSiteConfirm();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -382,6 +418,9 @@ class App {
|
||||
modal.open('Редактировать сайт', content);
|
||||
window.currentEditType = 'site';
|
||||
window.currentEditIndex = index;
|
||||
|
||||
// Добавляем кнопку удаления в футер модального окна
|
||||
this.addDeleteButtonToModal();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -446,6 +485,9 @@ class App {
|
||||
modal.open('Редактировать прокси', content);
|
||||
window.currentEditType = 'proxy';
|
||||
window.currentEditIndex = index;
|
||||
|
||||
// Убираем кнопку удаления (для прокси не нужна)
|
||||
this.removeDeleteButtonFromModal();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -574,6 +616,94 @@ class App {
|
||||
modal.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавить кнопку удаления в модальное окно
|
||||
*/
|
||||
addDeleteButtonToModal() {
|
||||
const footer = document.querySelector('.modal-footer');
|
||||
if (!footer) return;
|
||||
|
||||
// Удаляем старую кнопку удаления если есть
|
||||
const oldDeleteBtn = footer.querySelector('#modalDeleteBtn');
|
||||
if (oldDeleteBtn) oldDeleteBtn.remove();
|
||||
|
||||
// Создаём кнопку удаления
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.className = 'action-btn delete-btn';
|
||||
deleteBtn.id = 'modalDeleteBtn';
|
||||
deleteBtn.innerHTML = `
|
||||
<i class="fas fa-trash"></i>
|
||||
<span>Удалить сайт</span>
|
||||
`;
|
||||
deleteBtn.onclick = () => this.deleteSiteConfirm();
|
||||
|
||||
// Вставляем перед кнопкой "Отмена"
|
||||
const cancelBtn = footer.querySelector('#modalCancelBtn');
|
||||
if (cancelBtn) {
|
||||
footer.insertBefore(deleteBtn, cancelBtn);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Удалить кнопку удаления из модального окна
|
||||
*/
|
||||
removeDeleteButtonFromModal() {
|
||||
const deleteBtn = document.querySelector('#modalDeleteBtn');
|
||||
if (deleteBtn) deleteBtn.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Подтверждение удаления сайта
|
||||
*/
|
||||
async deleteSiteConfirm() {
|
||||
const index = window.currentEditIndex;
|
||||
const site = this.sitesManager.sitesData[index];
|
||||
if (!site) return;
|
||||
|
||||
// Подтверждение
|
||||
const confirmed = confirm(
|
||||
`⚠️ ВНИМАНИЕ!\n\n` +
|
||||
`Вы действительно хотите удалить сайт "${site.name}" (${site.host})?\n\n` +
|
||||
`Будут удалены:\n` +
|
||||
`• Папка сайта: WebServer/www/${site.host}/\n` +
|
||||
`• SSL сертификаты (если есть)\n` +
|
||||
`• Запись в конфигурации\n\n` +
|
||||
`Это действие НЕОБРАТИМО!`
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
notification.show('Удаление сайта...', 'info', 1000);
|
||||
|
||||
const result = await api.deleteSite(site.host);
|
||||
|
||||
if (result.startsWith('Error')) {
|
||||
notification.error(result, 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
notification.success('✅ Сайт успешно удалён!', 1500);
|
||||
|
||||
// Перезапускаем HTTP/HTTPS
|
||||
notification.show('Перезапуск серверов...', 'success', 800);
|
||||
await configAPI.stopHTTPService();
|
||||
await configAPI.stopHTTPSService();
|
||||
await sleep(500);
|
||||
await configAPI.startHTTPService();
|
||||
await configAPI.startHTTPSService();
|
||||
|
||||
notification.success('🚀 Серверы перезапущены!', 1000);
|
||||
|
||||
// Закрываем модальное окно и обновляем список
|
||||
modal.close();
|
||||
await this.sitesManager.load();
|
||||
|
||||
} catch (error) {
|
||||
notification.error('Ошибка: ' + error.message, 3000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализация приложения при загрузке DOM
|
||||
|
||||
140
Backend/admin/frontend/assets/js/ui/custom-select.js
Normal file
140
Backend/admin/frontend/assets/js/ui/custom-select.js
Normal file
@@ -0,0 +1,140 @@
|
||||
/* ============================================
|
||||
Custom Select Component
|
||||
Кастомные выпадающие списки
|
||||
============================================ */
|
||||
|
||||
import { $ } from '../utils/dom.js';
|
||||
|
||||
/**
|
||||
* Инициализация всех кастомных select'ов на странице
|
||||
*/
|
||||
export function initCustomSelects() {
|
||||
const selects = document.querySelectorAll('select.form-input');
|
||||
selects.forEach(select => {
|
||||
if (!select.dataset.customized) {
|
||||
createCustomSelect(select);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Создать кастомный select из нативного
|
||||
*/
|
||||
function createCustomSelect(selectElement) {
|
||||
// Помечаем как обработанный
|
||||
selectElement.dataset.customized = 'true';
|
||||
|
||||
// Создаём контейнер
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'custom-select';
|
||||
|
||||
// Получаем выбранное значение
|
||||
const selectedOption = selectElement.options[selectElement.selectedIndex];
|
||||
const selectedText = selectedOption ? selectedOption.text : '';
|
||||
|
||||
// Создаём кнопку (видимая часть)
|
||||
const button = document.createElement('div');
|
||||
button.className = 'custom-select-trigger';
|
||||
button.innerHTML = `
|
||||
<span class="custom-select-value">${selectedText}</span>
|
||||
<i class="fas fa-chevron-down custom-select-arrow"></i>
|
||||
`;
|
||||
|
||||
// Создаём выпадающий список
|
||||
const dropdown = document.createElement('div');
|
||||
dropdown.className = 'custom-select-dropdown';
|
||||
|
||||
// Заполняем опции
|
||||
Array.from(selectElement.options).forEach((option, index) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'custom-select-option';
|
||||
item.textContent = option.text;
|
||||
item.dataset.value = option.value;
|
||||
item.dataset.index = index;
|
||||
|
||||
if (option.selected) {
|
||||
item.classList.add('selected');
|
||||
}
|
||||
|
||||
// Клик по опции
|
||||
item.addEventListener('click', () => {
|
||||
selectOption(selectElement, wrapper, item, index);
|
||||
});
|
||||
|
||||
dropdown.appendChild(item);
|
||||
});
|
||||
|
||||
// Клик по кнопке - открыть/закрыть
|
||||
button.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
toggleDropdown(wrapper);
|
||||
});
|
||||
|
||||
// Собираем вместе
|
||||
wrapper.appendChild(button);
|
||||
wrapper.appendChild(dropdown);
|
||||
|
||||
// Скрываем оригинальный select
|
||||
selectElement.style.display = 'none';
|
||||
|
||||
// Вставляем кастомный select после оригинального
|
||||
selectElement.parentNode.insertBefore(wrapper, selectElement.nextSibling);
|
||||
|
||||
// Закрываем при клике вне
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!wrapper.contains(e.target)) {
|
||||
closeDropdown(wrapper);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Открыть/закрыть dropdown
|
||||
*/
|
||||
function toggleDropdown(wrapper) {
|
||||
const isOpen = wrapper.classList.contains('open');
|
||||
|
||||
// Закрываем все открытые
|
||||
document.querySelectorAll('.custom-select.open').forEach(el => {
|
||||
el.classList.remove('open');
|
||||
});
|
||||
|
||||
if (!isOpen) {
|
||||
wrapper.classList.add('open');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Закрыть dropdown
|
||||
*/
|
||||
function closeDropdown(wrapper) {
|
||||
wrapper.classList.remove('open');
|
||||
}
|
||||
|
||||
/**
|
||||
* Выбрать опцию
|
||||
*/
|
||||
function selectOption(selectElement, wrapper, optionElement, index) {
|
||||
// Обновляем оригинальный select
|
||||
selectElement.selectedIndex = index;
|
||||
|
||||
// Триггерим событие change
|
||||
const event = new Event('change', { bubbles: true });
|
||||
selectElement.dispatchEvent(event);
|
||||
|
||||
// Обновляем UI
|
||||
const valueSpan = wrapper.querySelector('.custom-select-value');
|
||||
valueSpan.textContent = optionElement.textContent;
|
||||
|
||||
// Убираем selected у всех опций
|
||||
wrapper.querySelectorAll('.custom-select-option').forEach(opt => {
|
||||
opt.classList.remove('selected');
|
||||
});
|
||||
|
||||
// Добавляем selected к выбранной
|
||||
optionElement.classList.add('selected');
|
||||
|
||||
// Закрываем dropdown
|
||||
closeDropdown(wrapper);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@ export class Navigation {
|
||||
sites: $('sectionSites'),
|
||||
proxy: $('sectionProxy'),
|
||||
settings: $('sectionSettings'),
|
||||
vaccess: $('sectionVAccessEditor')
|
||||
vaccess: $('sectionVAccessEditor'),
|
||||
addSite: $('sectionAddSite')
|
||||
};
|
||||
this.init();
|
||||
}
|
||||
|
||||
@@ -129,7 +129,13 @@
|
||||
</section>
|
||||
|
||||
<section class="section" id="sectionSites">
|
||||
<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="addSiteBtn">
|
||||
<i class="fas fa-plus"></i>
|
||||
<span>Добавить сайт</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table class="data-table" id="sitesTable">
|
||||
<thead>
|
||||
@@ -481,6 +487,160 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="sectionAddSite" style="display: none;">
|
||||
<div class="vaccess-page">
|
||||
<!-- Хлебные крошки -->
|
||||
<div class="breadcrumbs">
|
||||
<div class="breadcrumbs-left">
|
||||
<button class="breadcrumb-item" onclick="backToMainFromAddSite()">
|
||||
<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">Заполните информацию о сайте и при необходимости загрузите SSL сертификаты</p>
|
||||
</div>
|
||||
<div class="vaccess-actions">
|
||||
<button class="action-btn save-btn" id="createSiteBtn">
|
||||
<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="newSiteName" placeholder="Мой новый сайт">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Host (домен): <span style="color: #e74c3c;">*</span></label>
|
||||
<input type="text" class="form-input" id="newSiteHost" placeholder="example.com">
|
||||
<small style="color: #95a5a6; display: block; margin-top: 5px;">
|
||||
<i class="fas fa-info-circle"></i> Введите домен без протокола (например: example.com или 192.168.1.100)
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Alias (псевдонимы):</label>
|
||||
<input type="text" class="form-input" id="newSiteAliasInput" placeholder="*.example.com, www.example.com, alias.com">
|
||||
<small style="color: #95a5a6; display: block; margin-top: 5px;">
|
||||
<i class="fas fa-info-circle"></i> Введите псевдонимы через запятую
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Root файл: <span style="color: #e74c3c;">*</span></label>
|
||||
<select class="form-input" id="newSiteRootFile">
|
||||
<option value="index.html">index.html</option>
|
||||
<option value="index.php">index.php</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Статус:</label>
|
||||
<select class="form-input" id="newSiteStatus">
|
||||
<option value="active">Active (Активен)</option>
|
||||
<option value="inactive">Inactive (Отключен)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Root file routing:</label>
|
||||
<div class="toggle-wrapper">
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="newSiteRouting" 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> Если включено, все запросы к несуществующим файлам будут перенаправляться на root файл
|
||||
</small>
|
||||
</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="certMode" onchange="toggleCertUpload()">
|
||||
<option value="none">Без сертификата (fallback)</option>
|
||||
<option value="upload">Загрузить файлы сертификата</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="certUploadBlock" 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="certFile" accept=".crt,.pem" onchange="handleCertFileSelect(this, 'certificate')">
|
||||
<label for="certFile" class="file-upload-btn">
|
||||
<i class="fas fa-file-upload"></i>
|
||||
<span id="certFileName">Выберите файл...</span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="certFileStatus" 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="keyFile" accept=".key,.pem" onchange="handleCertFileSelect(this, 'privatekey')">
|
||||
<label for="keyFile" class="file-upload-btn">
|
||||
<i class="fas fa-file-upload"></i>
|
||||
<span id="keyFileName">Выберите файл...</span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="keyFileStatus" 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="caFile" accept=".crt,.pem" onchange="handleCertFileSelect(this, 'cabundle')">
|
||||
<label for="caFile" class="file-upload-btn">
|
||||
<i class="fas fa-file-upload"></i>
|
||||
<span id="caFileName">Выберите файл...</span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="caFileStatus" 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>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
|
||||
@@ -7,6 +7,10 @@ import {vaccess} from '../models';
|
||||
|
||||
export function CheckServicesReady():Promise<boolean>;
|
||||
|
||||
export function CreateNewSite(arg1:string):Promise<string>;
|
||||
|
||||
export function DeleteSite(arg1:string):Promise<string>;
|
||||
|
||||
export function DisableProxyService():Promise<string>;
|
||||
|
||||
export function EnableProxyService():Promise<string>;
|
||||
@@ -25,6 +29,8 @@ export function OpenSiteFolder(arg1:string):Promise<string>;
|
||||
|
||||
export function ReloadConfig():Promise<string>;
|
||||
|
||||
export function ReloadSSLCertificates():Promise<string>;
|
||||
|
||||
export function RestartAllServices():Promise<string>;
|
||||
|
||||
export function SaveConfig(arg1:string):Promise<string>;
|
||||
@@ -52,3 +58,5 @@ export function StopPHPService():Promise<string>;
|
||||
export function StopServer():Promise<string>;
|
||||
|
||||
export function UpdateSiteCache():Promise<string>;
|
||||
|
||||
export function UploadCertificate(arg1:string,arg2:string,arg3:string):Promise<string>;
|
||||
|
||||
@@ -6,6 +6,14 @@ export function CheckServicesReady() {
|
||||
return window['go']['admin']['App']['CheckServicesReady']();
|
||||
}
|
||||
|
||||
export function CreateNewSite(arg1) {
|
||||
return window['go']['admin']['App']['CreateNewSite'](arg1);
|
||||
}
|
||||
|
||||
export function DeleteSite(arg1) {
|
||||
return window['go']['admin']['App']['DeleteSite'](arg1);
|
||||
}
|
||||
|
||||
export function DisableProxyService() {
|
||||
return window['go']['admin']['App']['DisableProxyService']();
|
||||
}
|
||||
@@ -42,6 +50,10 @@ export function ReloadConfig() {
|
||||
return window['go']['admin']['App']['ReloadConfig']();
|
||||
}
|
||||
|
||||
export function ReloadSSLCertificates() {
|
||||
return window['go']['admin']['App']['ReloadSSLCertificates']();
|
||||
}
|
||||
|
||||
export function RestartAllServices() {
|
||||
return window['go']['admin']['App']['RestartAllServices']();
|
||||
}
|
||||
@@ -97,3 +109,7 @@ export function StopServer() {
|
||||
export function UpdateSiteCache() {
|
||||
return window['go']['admin']['App']['UpdateSiteCache']();
|
||||
}
|
||||
|
||||
export function UploadCertificate(arg1, arg2, arg3) {
|
||||
return window['go']['admin']['App']['UploadCertificate'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user