Files
vServer/Backend/admin/frontend/assets/js/components/site-creator.js
Falknat 4b13923375 Добавление и Удаление сайта
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 Секция добавления сайта теперь скрывается при навигации
2025-11-14 14:18:26 +07:00

394 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ============================================
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;
}
}
}