Добавление и Удаление сайта
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:
@@ -42,28 +42,15 @@ func matchWildcardAlias(pattern, host string) bool {
|
||||
}
|
||||
|
||||
// Поддержка wildcard: *.example.com, example.*, *example*, *
|
||||
// Заменяем * на регулярное выражение
|
||||
pattern = strings.ReplaceAll(pattern, ".", "\\.")
|
||||
pattern = strings.ReplaceAll(pattern, "*", ".*")
|
||||
pattern = "^" + pattern + "$"
|
||||
// Паттерн *.example.com -> должен совпадать с sub.example.com
|
||||
|
||||
// Простая проверка без regexp (более быстрая)
|
||||
return matchSimplePattern(pattern, host)
|
||||
}
|
||||
|
||||
// Простая проверка паттерна (без использования regexp для скорости)
|
||||
func matchSimplePattern(pattern, host string) bool {
|
||||
// Убираем ^ и $ добавленные выше
|
||||
pattern = strings.TrimPrefix(pattern, "^")
|
||||
pattern = strings.TrimSuffix(pattern, "$")
|
||||
|
||||
// Если паттерн = .* (любой хост)
|
||||
if pattern == ".*" {
|
||||
// Если паттерн = * (любой хост)
|
||||
if pattern == "*" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Разбиваем паттерн на части по .*
|
||||
parts := strings.Split(pattern, ".*")
|
||||
// Разбиваем паттерн на части по звёздочке
|
||||
parts := strings.Split(pattern, "*")
|
||||
|
||||
// Проверяем каждую часть
|
||||
currentPos := 0
|
||||
@@ -79,14 +66,14 @@ func matchSimplePattern(pattern, host string) bool {
|
||||
}
|
||||
|
||||
// Для первой части проверяем что она в начале (если паттерн не начинается с *)
|
||||
if i == 0 && !strings.HasPrefix(pattern, ".*") {
|
||||
if i == 0 && !strings.HasPrefix(pattern, "*") {
|
||||
if idx != 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Для последней части проверяем что она в конце (если паттерн не кончается на *)
|
||||
if i == len(parts)-1 && !strings.HasSuffix(pattern, ".*") {
|
||||
if i == len(parts)-1 && !strings.HasSuffix(pattern, "*") {
|
||||
if currentPos+idx+len(part) != len(host) {
|
||||
return false
|
||||
}
|
||||
@@ -98,40 +85,41 @@ func matchSimplePattern(pattern, host string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func Alias_check(r *http.Request) (alias_found bool, host string) {
|
||||
|
||||
alias_found = false
|
||||
|
||||
for _, site := range config.ConfigData.Site_www {
|
||||
|
||||
for _, alias := range site.Alias {
|
||||
|
||||
// Поддержка wildcard паттернов
|
||||
if matchWildcardAlias(alias, r.Host) {
|
||||
alias_found = true
|
||||
return alias_found, site.Host
|
||||
} else {
|
||||
alias_found = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return alias_found, ""
|
||||
|
||||
}
|
||||
|
||||
func Alias_Run(r *http.Request) (rhost string) {
|
||||
requestHost := r.Host
|
||||
|
||||
var host string
|
||||
host = r.Host
|
||||
|
||||
alias_check, alias := Alias_check(r)
|
||||
|
||||
if alias_check {
|
||||
host = alias
|
||||
// Убираем порт если есть (например :80 или :443)
|
||||
if colonIndex := strings.Index(requestHost, ":"); colonIndex != -1 {
|
||||
requestHost = requestHost[:colonIndex]
|
||||
}
|
||||
|
||||
return host
|
||||
// Приоритет 1: Проверяем точное совпадение с site.Host
|
||||
for _, site := range config.ConfigData.Site_www {
|
||||
if site.Host == requestHost {
|
||||
return site.Host
|
||||
}
|
||||
}
|
||||
|
||||
// Приоритет 2: Проверяем точные alias (без wildcard)
|
||||
for _, site := range config.ConfigData.Site_www {
|
||||
for _, alias := range site.Alias {
|
||||
if !strings.Contains(alias, "*") && alias == requestHost {
|
||||
return site.Host
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Приоритет 3: Проверяем wildcard alias
|
||||
for _, site := range config.ConfigData.Site_www {
|
||||
for _, alias := range site.Alias {
|
||||
if strings.Contains(alias, "*") && matchWildcardAlias(alias, requestHost) {
|
||||
return site.Host
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Не нашли совпадений - возвращаем как есть
|
||||
return requestHost
|
||||
}
|
||||
|
||||
// Получает список root_file для сайта из конфигурации
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -318,3 +318,49 @@ func (a *App) UpdateSiteCache() string {
|
||||
webserver.UpdateSiteStatusCache()
|
||||
return "Cache updated"
|
||||
}
|
||||
|
||||
func (a *App) CreateNewSite(siteJSON string) string {
|
||||
var siteData sites.SiteInfo
|
||||
err := json.Unmarshal([]byte(siteJSON), &siteData)
|
||||
if err != nil {
|
||||
return "Error: Invalid JSON - " + err.Error()
|
||||
}
|
||||
|
||||
err = sites.CreateNewSite(siteData)
|
||||
if err != nil {
|
||||
return "Error: " + err.Error()
|
||||
}
|
||||
|
||||
config.LoadConfig()
|
||||
return "Site created successfully"
|
||||
}
|
||||
|
||||
func (a *App) UploadCertificate(host, certType, certDataBase64 string) string {
|
||||
certData, err := tools.DecodeBase64(certDataBase64)
|
||||
if err != nil {
|
||||
return "Error: Invalid base64 data - " + err.Error()
|
||||
}
|
||||
|
||||
err = sites.UploadSiteCertificate(host, certType, certData)
|
||||
if err != nil {
|
||||
return "Error: " + err.Error()
|
||||
}
|
||||
|
||||
webserver.ReloadCertificates()
|
||||
return "Certificate uploaded successfully"
|
||||
}
|
||||
|
||||
func (a *App) ReloadSSLCertificates() string {
|
||||
webserver.ReloadCertificates()
|
||||
return "SSL certificates reloaded"
|
||||
}
|
||||
|
||||
func (a *App) DeleteSite(host string) string {
|
||||
err := sites.DeleteSite(host)
|
||||
if err != nil {
|
||||
return "Error: " + err.Error()
|
||||
}
|
||||
|
||||
config.LoadConfig()
|
||||
return "Site deleted successfully"
|
||||
}
|
||||
312
Backend/admin/go/sites/methods.go
Normal file
312
Backend/admin/go/sites/methods.go
Normal file
@@ -0,0 +1,312 @@
|
||||
package sites
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
config "vServer/Backend/config"
|
||||
tools "vServer/Backend/tools"
|
||||
)
|
||||
|
||||
//go:embed templates/index.tmpl
|
||||
var indexTemplate string
|
||||
|
||||
// CreateNewSite создаёт новый сайт со всей необходимой структурой
|
||||
func CreateNewSite(siteData SiteInfo) error {
|
||||
// 1. Валидация данных
|
||||
if err := ValidateSiteData(siteData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. Создание структуры папок
|
||||
if err := CreateSiteFolder(siteData.Host); err != nil {
|
||||
return fmt.Errorf("ошибка создания папок: %w", err)
|
||||
}
|
||||
|
||||
// 3. Создание стартового файла
|
||||
if err := CreateStarterFile(siteData.Host, siteData.RootFile); err != nil {
|
||||
return fmt.Errorf("ошибка создания стартового файла: %w", err)
|
||||
}
|
||||
|
||||
// 4. Создание пустого vAccess.conf
|
||||
if err := CreateVAccessFile(siteData.Host); err != nil {
|
||||
return fmt.Errorf("ошибка создания vAccess.conf: %w", err)
|
||||
}
|
||||
|
||||
// 5. Добавление сайта в конфиг
|
||||
if err := AddSiteToConfig(siteData); err != nil {
|
||||
return fmt.Errorf("ошибка добавления в конфиг: %w", err)
|
||||
}
|
||||
|
||||
tools.Logs_file(0, "SITES", fmt.Sprintf("✅ Новый сайт создан: %s (%s)", siteData.Name, siteData.Host), "logs_config.log", true)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateSiteData проверяет данные нового сайта
|
||||
func ValidateSiteData(siteData SiteInfo) error {
|
||||
// Проверка обязательных полей
|
||||
if strings.TrimSpace(siteData.Host) == "" {
|
||||
return errors.New("поле Host обязательно для заполнения")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(siteData.Name) == "" {
|
||||
return errors.New("поле Name обязательно для заполнения")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(siteData.RootFile) == "" {
|
||||
return errors.New("поле RootFile обязательно для заполнения")
|
||||
}
|
||||
|
||||
// Проверка уникальности host
|
||||
for _, site := range config.ConfigData.Site_www {
|
||||
if strings.EqualFold(site.Host, siteData.Host) {
|
||||
return fmt.Errorf("сайт с host '%s' уже существует", siteData.Host)
|
||||
}
|
||||
}
|
||||
|
||||
// Проверка валидности status
|
||||
if siteData.Status != "active" && siteData.Status != "inactive" {
|
||||
return errors.New("status должен быть 'active' или 'inactive'")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateSiteFolder создаёт структуру папок для нового сайта
|
||||
func CreateSiteFolder(host string) error {
|
||||
// Создаём путь: WebServer/www/{host}/public_www/
|
||||
folderPath := filepath.Join("WebServer", "www", host, "public_www")
|
||||
|
||||
absPath, err := tools.AbsPath(folderPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Создаём все необходимые папки
|
||||
if err := os.MkdirAll(absPath, 0755); err != nil {
|
||||
return fmt.Errorf("не удалось создать папку: %w", err)
|
||||
}
|
||||
|
||||
tools.Logs_file(0, "SITES", fmt.Sprintf("📁 Создана папка: %s", folderPath), "logs_config.log", false)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateStarterFile создаёт стартовый файл (index.html или index.php)
|
||||
func CreateStarterFile(host, rootFile string) error {
|
||||
filePath := filepath.Join("WebServer", "www", host, "public_www", rootFile)
|
||||
|
||||
// Получаем абсолютный путь БЕЗ проверки существования
|
||||
absPath, err := filepath.Abs(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка получения абсолютного пути: %w", err)
|
||||
}
|
||||
|
||||
// Генерируем контент из шаблона
|
||||
content := generateTemplate(host, rootFile)
|
||||
|
||||
// Записываем файл
|
||||
if err := os.WriteFile(absPath, []byte(content), 0644); err != nil {
|
||||
return fmt.Errorf("не удалось создать файл: %w", err)
|
||||
}
|
||||
|
||||
tools.Logs_file(0, "SITES", fmt.Sprintf("📄 Создан стартовый файл: %s", rootFile), "logs_config.log", false)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateVAccessFile создаёт пустой конфиг vAccess
|
||||
func CreateVAccessFile(host string) error {
|
||||
filePath := filepath.Join("WebServer", "www", host, "vAccess.conf")
|
||||
|
||||
// Получаем абсолютный путь БЕЗ проверки существования
|
||||
absPath, err := filepath.Abs(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка получения абсолютного пути: %w", err)
|
||||
}
|
||||
|
||||
content := `# vAccess Configuration
|
||||
# Правила применяются сверху вниз
|
||||
|
||||
# Пример правила (закомментировано):
|
||||
# type: Disable
|
||||
# type_file: *.php
|
||||
# path_access: /uploads/*
|
||||
# url_error: 404
|
||||
|
||||
`
|
||||
|
||||
if err := os.WriteFile(absPath, []byte(content), 0644); err != nil {
|
||||
return fmt.Errorf("не удалось создать vAccess.conf: %w", err)
|
||||
}
|
||||
|
||||
tools.Logs_file(0, "SITES", "🔒 Создан vAccess.conf", "logs_config.log", false)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddSiteToConfig добавляет новый сайт в config.json
|
||||
func AddSiteToConfig(siteData SiteInfo) error {
|
||||
// Создаём новую запись
|
||||
newSite := config.Site_www{
|
||||
Name: siteData.Name,
|
||||
Host: siteData.Host,
|
||||
Alias: siteData.Alias,
|
||||
Status: siteData.Status,
|
||||
Root_file: siteData.RootFile,
|
||||
Root_file_routing: siteData.RootFileRouting,
|
||||
}
|
||||
|
||||
// Добавляем в массив
|
||||
config.ConfigData.Site_www = append(config.ConfigData.Site_www, newSite)
|
||||
|
||||
// Сохраняем конфиг в файл
|
||||
if err := SaveConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tools.Logs_file(0, "SITES", "💾 Конфигурация обновлена", "logs_config.log", false)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveConfig сохраняет текущую конфигурацию в файл
|
||||
func SaveConfig() error {
|
||||
// Форматируем JSON с отступами
|
||||
jsonData, err := json.MarshalIndent(config.ConfigData, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка форматирования JSON: %w", err)
|
||||
}
|
||||
|
||||
// Получаем абсолютный путь к файлу конфига
|
||||
absPath, err := tools.AbsPath(config.ConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Записываем в файл
|
||||
if err := os.WriteFile(absPath, jsonData, 0644); err != nil {
|
||||
return fmt.Errorf("ошибка записи файла: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UploadSiteCertificate загружает SSL сертификат для сайта
|
||||
func UploadSiteCertificate(host, certType string, certData []byte) error {
|
||||
// Создаём папку для сертификатов
|
||||
certDir := filepath.Join("WebServer", "cert", host)
|
||||
|
||||
absCertDir, err := tools.AbsPath(certDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(absCertDir, 0755); err != nil {
|
||||
return fmt.Errorf("не удалось создать папку для сертификатов: %w", err)
|
||||
}
|
||||
|
||||
// Определяем имя файла
|
||||
var fileName string
|
||||
switch certType {
|
||||
case "certificate":
|
||||
fileName = "certificate.crt"
|
||||
case "privatekey":
|
||||
fileName = "private.key"
|
||||
case "cabundle":
|
||||
fileName = "ca_bundle.crt"
|
||||
default:
|
||||
return fmt.Errorf("неизвестный тип сертификата: %s", certType)
|
||||
}
|
||||
|
||||
// Путь к файлу сертификата
|
||||
certFilePath := filepath.Join(absCertDir, fileName)
|
||||
|
||||
// Записываем файл
|
||||
if err := os.WriteFile(certFilePath, certData, 0644); err != nil {
|
||||
return fmt.Errorf("не удалось сохранить сертификат: %w", err)
|
||||
}
|
||||
|
||||
tools.Logs_file(0, "SITES", fmt.Sprintf("🔒 Загружен сертификат: %s для %s", fileName, host), "logs_config.log", true)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteSiteCertificates удаляет сертификаты сайта
|
||||
func DeleteSiteCertificates(host string) error {
|
||||
certDir := filepath.Join("WebServer", "cert", host)
|
||||
|
||||
// Получаем абсолютный путь к папке сертификатов
|
||||
absCertDir, err := filepath.Abs(certDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка получения пути: %w", err)
|
||||
}
|
||||
|
||||
// Проверяем, существует ли папка
|
||||
if _, err := os.Stat(absCertDir); os.IsNotExist(err) {
|
||||
return nil // Папки нет - ничего удалять не нужно
|
||||
}
|
||||
|
||||
// Удаляем папку со всем содержимым
|
||||
if err := os.RemoveAll(absCertDir); err != nil {
|
||||
return fmt.Errorf("не удалось удалить папку сертификатов: %w", err)
|
||||
}
|
||||
|
||||
tools.Logs_file(0, "SITES", fmt.Sprintf("🗑️ Удалены сертификаты для: %s", host), "logs_config.log", true)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteSite полностью удаляет сайт
|
||||
func DeleteSite(host string) error {
|
||||
// 1. Проверяем, существует ли сайт в конфиге
|
||||
siteIndex := -1
|
||||
for i, site := range config.ConfigData.Site_www {
|
||||
if site.Host == host {
|
||||
siteIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if siteIndex == -1 {
|
||||
return fmt.Errorf("сайт с host '%s' не найден в конфигурации", host)
|
||||
}
|
||||
|
||||
// 2. Удаляем папку сайта
|
||||
siteDir := filepath.Join("WebServer", "www", host)
|
||||
absSiteDir, err := filepath.Abs(siteDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ошибка получения пути: %w", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(absSiteDir); err == nil {
|
||||
if err := os.RemoveAll(absSiteDir); err != nil {
|
||||
return fmt.Errorf("не удалось удалить папку сайта: %w", err)
|
||||
}
|
||||
tools.Logs_file(0, "SITES", fmt.Sprintf("🗑️ Удалена папка сайта: %s", siteDir), "logs_config.log", false)
|
||||
}
|
||||
|
||||
// 3. Удаляем сертификаты
|
||||
if err := DeleteSiteCertificates(host); err != nil {
|
||||
// Логируем ошибку, но продолжаем удаление
|
||||
tools.Logs_file(1, "SITES", fmt.Sprintf("Ошибка удаления сертификатов: %v", err), "logs_config.log", false)
|
||||
}
|
||||
|
||||
// 4. Удаляем из конфига
|
||||
config.ConfigData.Site_www = append(
|
||||
config.ConfigData.Site_www[:siteIndex],
|
||||
config.ConfigData.Site_www[siteIndex+1:]...,
|
||||
)
|
||||
|
||||
// 5. Сохраняем конфиг
|
||||
if err := SaveConfig(); err != nil {
|
||||
return fmt.Errorf("ошибка сохранения конфигурации: %w", err)
|
||||
}
|
||||
|
||||
tools.Logs_file(0, "SITES", fmt.Sprintf("✅ Сайт '%s' полностью удалён", host), "logs_config.log", true)
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateTemplate генерирует шаблон для нового сайта
|
||||
func generateTemplate(host, rootFile string) string {
|
||||
// Для всех типов файлов используем один HTML шаблон
|
||||
return strings.ReplaceAll(indexTemplate, "{{.Host}}", host)
|
||||
}
|
||||
209
Backend/admin/go/sites/templates/index.tmpl
Normal file
209
Backend/admin/go/sites/templates/index.tmpl
Normal file
@@ -0,0 +1,209 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{.Host}} - Добро пожаловать!</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #1a252f 0%, #2c3e50 50%, #34495e 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.container {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
background: rgba(26, 37, 47, 0.95);
|
||||
border-radius: 16px;
|
||||
backdrop-filter: blur(15px);
|
||||
border: 1px solid rgba(52, 152, 219, 0.2);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(52, 152, 219, 0.1);
|
||||
max-width: 580px;
|
||||
animation: fadeIn 1s ease-in-out;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, #3498db, #2ecc71, #e74c3c, #f39c12);
|
||||
background-size: 400% 400%;
|
||||
animation: gradientShift 4s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes gradientShift {
|
||||
0%, 100% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 3.8rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 1.5rem;
|
||||
background: linear-gradient(135deg, #3498db, #2ecc71);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
letter-spacing: 3px;
|
||||
text-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.welcome-text {
|
||||
font-size: 1.9rem;
|
||||
margin-bottom: 1.5rem;
|
||||
font-weight: 400;
|
||||
color: #ecf0f1;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 1.15rem;
|
||||
margin-bottom: 2.5rem;
|
||||
line-height: 1.8;
|
||||
color: #bdc3c7;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
background: linear-gradient(135deg, rgba(46, 204, 113, 0.15), rgba(39, 174, 96, 0.25));
|
||||
padding: 15px 30px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(46, 204, 113, 0.4);
|
||||
margin-bottom: 1.5rem;
|
||||
box-shadow: 0 8px 16px rgba(46, 204, 113, 0.1);
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: #2ecc71;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s infinite;
|
||||
box-shadow: 0 0 10px rgba(46, 204, 113, 0.5);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(46, 204, 113, 0.7), 0 0 10px rgba(46, 204, 113, 0.5);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 10px rgba(46, 204, 113, 0), 0 0 10px rgba(46, 204, 113, 0.5);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(46, 204, 113, 0), 0 0 10px rgba(46, 204, 113, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.status-text {
|
||||
color: #ecf0f1;
|
||||
font-weight: 600;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 1.5rem;
|
||||
font-size: 0.95rem;
|
||||
opacity: 0.8;
|
||||
color: #95a5a6;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: #3498db;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.footer a:hover {
|
||||
color: #2ecc71;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.floating-particles {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.particle {
|
||||
position: absolute;
|
||||
background: rgba(52, 152, 219, 0.15);
|
||||
border-radius: 50%;
|
||||
animation: float 8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0px) rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-20px) rotate(180deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="floating-particles">
|
||||
<div class="particle" style="width: 18px; height: 18px; top: 15%; left: 10%; animation-delay: 0s;"></div>
|
||||
<div class="particle" style="width: 14px; height: 14px; top: 65%; left: 85%; animation-delay: 3s;"></div>
|
||||
<div class="particle" style="width: 10px; height: 10px; top: 85%; left: 20%; animation-delay: 6s;"></div>
|
||||
<div class="particle" style="width: 22px; height: 22px; top: 25%; left: 75%; animation-delay: 2s;"></div>
|
||||
<div class="particle" style="width: 12px; height: 12px; top: 75%; left: 50%; animation-delay: 4s;"></div>
|
||||
<div class="particle" style="width: 16px; height: 16px; top: 40%; left: 15%; animation-delay: 1s;"></div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="logo">{{.Host}}</div>
|
||||
<div class="welcome-text">Добро пожаловать!</div>
|
||||
<div class="description">
|
||||
Ваш сайт успешно создан и готов к работе.<br>
|
||||
Начните разработку, заменив этот файл своим контентом.
|
||||
</div>
|
||||
<div class="status">
|
||||
<div class="status-dot"></div>
|
||||
<span class="status-text">Сайт работает</span>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
Powered by vServer
|
||||
<br>
|
||||
Сайт разработчика: <a href="https://voxsel.ru" target="_blank">voxsel.ru</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
tools "vServer/Backend/tools"
|
||||
)
|
||||
|
||||
func GetVAccessPath(host string, isProxy bool) string {
|
||||
@@ -18,8 +18,13 @@ func GetVAccessPath(host string, isProxy bool) string {
|
||||
func GetVAccessConfig(host string, isProxy bool) (*VAccessConfig, error) {
|
||||
filePath := GetVAccessPath(host, isProxy)
|
||||
|
||||
// Получаем абсолютный путь БЕЗ проверки существования
|
||||
absPath, err := filepath.Abs(filePath)
|
||||
if err != nil {
|
||||
return &VAccessConfig{Rules: []VAccessRule{}}, nil
|
||||
}
|
||||
|
||||
// Проверяем существование файла
|
||||
absPath, _ := tools.AbsPath(filePath)
|
||||
if _, err := os.Stat(absPath); os.IsNotExist(err) {
|
||||
// Файл не существует - возвращаем пустую конфигурацию
|
||||
return &VAccessConfig{Rules: []VAccessRule{}}, nil
|
||||
@@ -104,11 +109,14 @@ func SaveVAccessConfig(host string, isProxy bool, config *VAccessConfig) error {
|
||||
dir = fmt.Sprintf("WebServer/www/%s", host)
|
||||
}
|
||||
|
||||
absDir, _ := tools.AbsPath(dir)
|
||||
absDir, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.MkdirAll(absDir, 0755)
|
||||
|
||||
// Получаем абсолютный путь к файлу
|
||||
absPath, err := tools.AbsPath(filePath)
|
||||
absPath, err := filepath.Abs(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 495 KiB |
@@ -1,6 +1,7 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
@@ -74,6 +75,11 @@ func Error_check(err error, message string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// DecodeBase64 декодирует строку из base64
|
||||
func DecodeBase64(encoded string) ([]byte, error) {
|
||||
decoded, err := base64.StdEncoding.DecodeString(encoded)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ошибка декодирования base64: %w", err)
|
||||
}
|
||||
return decoded, nil
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ Write-Success "Зависимости установлены"
|
||||
Write-ProgressBar 50
|
||||
Write-Host ""
|
||||
|
||||
Write-Step 3 4 "Проверка Wails CLI..."
|
||||
Write-Step 3 5 "Проверка Wails CLI..."
|
||||
$null = wails version 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Info "Установка Wails CLI..."
|
||||
@@ -70,10 +70,17 @@ if ($LASTEXITCODE -ne 0) {
|
||||
} else {
|
||||
Write-Success "Найден"
|
||||
}
|
||||
Write-ProgressBar 75
|
||||
Write-ProgressBar 60
|
||||
Write-Host ""
|
||||
|
||||
Write-Step 4 4 "Сборка приложения..."
|
||||
Write-Step 4 5 "Генерация биндингов..."
|
||||
Write-Info "Создание TypeScript/JS биндингов для Go методов..."
|
||||
wails generate module 2>&1 | Out-Null
|
||||
Write-Success "Биндинги сгенерированы"
|
||||
Write-ProgressBar 80
|
||||
Write-Host ""
|
||||
|
||||
Write-Step 5 5 "Сборка приложения..."
|
||||
Write-Info "Компиляция (может занять ~10 сек)..."
|
||||
|
||||
wails build -f admin.go 2>&1 | Out-Null
|
||||
|
||||
Reference in New Issue
Block a user