tmp_27.06.2026
Различия
Показаны различия между двумя версиями страницы.
| Следующая версия | Предыдущая версия | ||
| tmp_27.06.2026 [2026/06/27 10:43] – создано VladPolskiy | tmp_27.06.2026 [2026/06/27 15:35] (текущий) – VladPolskiy | ||
|---|---|---|---|
| Строка 1: | Строка 1: | ||
| - | ====== Файл менеджер ====== | + | // assets/js/app.js |
| - | ===== 🗺️ Общая архитектурная схема проекта ===== | + | |
| - | < | + | |
| - | [ ПОЛЬЗОВАТЕЛЬ | + | |
| - | │ | + | |
| - | ▼ | + | |
| - | ┌────────────────────────────────────────────────────────┐ | + | |
| - | │ 1. index.php (Главный шлюз приложения) | + | |
| - | │ - Проверяет сессию | + | |
| - | │ - Если первый запуск ➔ перенаправляет на установку │ | + | |
| - | └──────────────────┬─────────────────────────────────────┘ | + | |
| - | │ | + | |
| - | ▼ | + | |
| - | ┌────────────────────────────────────────────────────────┐ | + | |
| - | │ 2. backend/core/initializer.php (Динамический корень) | + | |
| - | │ - Вычисляет APP_ROOT через __DIR__ | + | |
| - | │ - Создает папки: config/, storage/, system_logs/ | + | |
| - | └──────────────────┬─────────────────────────────────────┘ | + | |
| - | │ | + | |
| - | ▼ | + | |
| - | ┌────────────────────────────────────────────────────────┐ | + | |
| - | │ 3. backend/ | + | |
| - | │ - Сверяет IP с config/ | + | |
| - | │ - Бан на месте: die() │ | + | |
| - | └──────────────────┬─────────────────────────────────────┘ | + | |
| - | │ | + | |
| - | | + | |
| - | ▼ (Если не авторизован) | + | |
| - | ┌────────────────────────────────────────┐ ┌──────────────────────────────────────┐ | + | |
| - | │ 4. ЭКРАН АВТОРИЗАЦИИ / ИНИЦИАЛИЗАЦИИ | + | |
| - | │ - Поля: Логин / Пароль | + | |
| - | │ - Скрытое поле: Honeypot (Ловушка) | + | |
| - | │ - Восстановление: | + | |
| - | │ │ │ │ [Имя / Путь] [Релизы ▼] [ZIP] │ │ | + | |
| - | │ backend/ | + | |
| - | │ - Вырезает XSS / Path Traversal | + | |
| - | │ │ │ │ (Имя | Примечание | Дата/ | + | |
| - | │ backend/ | + | |
| - | │ - Хэширует пароли и текст книги | + | |
| - | │ - Генерирует случайный 8-значный ID │ │ │ НИЖНЕЕ ОКНО | + | |
| - | │ - Сортирует и перетасовывает базу | + | |
| - | │ │ │ │ [Лог действий (для админа)] | + | |
| - | │ │ │ ├──────────────────────────────────┤ │ | + | |
| - | │ │ │ │ Динамический текст (Fetch API) │ │ | + | |
| - | │ │ │ └──────────────────────────────────┘ │ | + | |
| - | └────────────────────────────────────────┘ └──────────────────┬───────────────────┘ | + | |
| - | │ | + | |
| - | ▼ (Любое действие) | + | |
| - | | + | |
| - | │ 6. backend/ | + | |
| - | | + | |
| - | | + | |
| - | | + | |
| - | │ | + | |
| - | ┌───────────────────┴───────────────────┐ | + | |
| - | ▼ (Действия Админа) | + | |
| - | | + | |
| - | │ backend/ | + | |
| - | │ - list.php | + | |
| - | │ - mkdir.php | + | |
| - | │ - delete.php | + | |
| - | | + | |
| - | </code> | + | document.addEventListener(' |
| + | // --- НАДЕЖНОЕ УПРАВЛЕНИЕ МОДАЛКОЙ С ДЕБАГ-МАЯКОМ --- | ||
| + | // --- УПРАВЛЕНИЕ ЗАГРУЗЧИКОМ ПРОЕКТОВ --- | ||
| + | const btnCreateProject | ||
| + | const projectModal | ||
| + | const btnModalCancel | ||
| + | const btnModalSubmit | ||
| + | const newProjectName | ||
| + | const modalErrorBox | ||
| + | |||
| + | const folderInput | ||
| + | const btnSelectFolder | ||
| + | const folderStatus | ||
| - | ===== 🧩 Карта маскировки | + | if (btnCreateProject && projectModal) { |
| - | Поскольку мы закладываем параноидальную защиту, вот схема того, как скрыты наши | + | |
| + | // Функция проверки условий для активации кнопки "Загрузить" | ||
| + | function checkInputsValidity() { | ||
| + | const nameVal = newProjectName.value.trim(); | ||
| + | const hasFiles = folderInput.files && folderInput.files.length > 0; | ||
| + | |||
| + | if (nameVal !== "" | ||
| + | btnModalSubmit.disabled = false; // Включаем кнопку | ||
| + | btnModalSubmit.style.opacity = " | ||
| + | } else { | ||
| + | btnModalSubmit.disabled = true; // Выключаем кнопку | ||
| + | btnModalSubmit.style.opacity = " | ||
| + | } | ||
| + | } | ||
| - | < | + | |
| - | backend/config/ | + | |
| - | │ | + | |
| - | ├── salt.php | + | |
| - | │ | + | |
| - | │ (Хакер | + | |
| - | │ | + | |
| - | ├── email.php | + | |
| - | │ | + | |
| - | │ | + | |
| - | ├── ip_blacklist.txt | + | |
| - | │ | + | |
| - | └── users.json | + | |
| - | ┌──────────────────────────────────────────────┐ | + | |
| - | │ ID: 85910432 | + | |
| - | │ Пароль: | + | |
| - | │ Роль: x7R9wQ2pM4zL1vK8 (Случайный шум) | + | |
| - | │ Книга: $2y$10$... (Хэш контрольной строки) | + | |
| - | └──────────────────────────────────────────────┘ | + | |
| - | </code> | + | |
| + | btnSelectFolder.addEventListener(' | ||
| + | folderInput.click(); | ||
| + | }); | ||
| - | ===== 📂 Карта песочницы на диске (storage/) ===== | + | // Следим за тем, что пользователь выбрал папку на ПК |
| - | Абсолютно безликая структура. Никаких названий компаний и имен. Только цифровые идентификаторы, сгенерированные нашей системой тасования. | + | folderInput.addEventListener(' |
| - | <code> | + | if (this.files && this.files.length |
| - | storage/ | + | // Берем имя корневой |
| - | │ | + | const firstFile = this.files[0]; |
| - | ├── 85910432/ ───► Личная папка Супер-админа (или Главного Админа) | + | const rootFolderName = firstFile.webkitRelativePath.split('/' |
| - | │ | + | |
| - | │ | + | } else { |
| - | │ │ | + | |
| - | │ | + | } |
| - | │ | + | checkInputsValidity(); |
| - | │ | + | |
| - | │ | + | |
| - | │ | + | |
| - | │ | + | |
| - | │ │ | + | |
| - | │ | + | |
| - | │ | + | |
| - | ├── 10294811/ | + | |
| - | │ | + | |
| - | │ | + | |
| - | └── system_logs/ | + | |
| - | | + | |
| - | | + | |
| - | </code> | + | // Открытие модалки |
| + | btnCreateProject.addEventListener(' | ||
| + | newProjectName.value = ''; | ||
| + | folderInput.value = ''; | ||
| + | folderStatus.textContent = " | ||
| + | if (modalErrorBox) modalErrorBox.classList.add(' | ||
| + | btnModalSubmit.disabled = true; | ||
| + | btnModalSubmit.style.opacity = " | ||
| + | projectModal.classList.remove(' | ||
| + | newProjectName.focus(); | ||
| + | }); | ||
| + | |||
| + | // Отмена | ||
| + | btnModalCancel.addEventListener(' | ||
| + | projectModal.classList.add(' | ||
| + | }); | ||
| + | |||
| + | // Нажатие на кнопку " | ||
| + | btnModalSubmit.addEventListener(' | ||
| + | const projectName = newProjectName.value.trim(); | ||
| + | if (modalErrorBox) modalErrorBox.classList.add(' | ||
| + | |||
| + | const formData = new FormData(); | ||
| + | formData.append(' | ||
| + | |||
| + | // КРИТИЧЕСКИ ВАЖНО: Пакуем файлы вместе с их путями вложения | ||
| + | for (let i = 0; i < folderInput.files.length; | ||
| + | let file = folderInput.files[i]; | ||
| + | formData.append(' | ||
| + | | ||
| + | formData.append(' | ||
| + | } | ||
| + | |||
| + | // Меняем статус кнопки на время тяжелой загрузки | ||
| + | btnModalSubmit.disabled = true; | ||
| + | btnModalSubmit.textContent = " | ||
| + | |||
| + | try { | ||
| + | const response = await fetch(' | ||
| + | method: ' | ||
| + | body: formData | ||
| + | }); | ||
| + | const result = await response.json(); | ||
| + | |||
| + | if (result.success) { | ||
| + | projectModal.classList.add(' | ||
| + | btnModalSubmit.textContent = " | ||
| + | location.reload(); | ||
| + | } else { | ||
| + | btnModalSubmit.disabled = false; | ||
| + | btnModalSubmit.textContent = " | ||
| + | if (modalErrorBox) { | ||
| + | modalErrorBox.textContent = result.error; | ||
| + | modalErrorBox.classList.remove(' | ||
| + | } | ||
| + | } | ||
| + | } catch (error) { | ||
| + | // Возвращаем кнопку в исходное рабочее состояние | ||
| + | btnModalSubmit.disabled = false; | ||
| + | btnModalSubmit.textContent = " | ||
| + | |||
| + | // КРАСИВО ВЫВОДИМ ОШИБКУ СЕТИ В ТВОЮ ПЛАШКУ БЕЗ ВСЯКИХ АЛЕРТОВ | ||
| + | if (modalErrorBox) { | ||
| + | modalErrorBox.textContent = " | ||
| + | modalErrorBox.classList.remove(' | ||
| + | } | ||
| + | } | ||
| + | }); | ||
| + | } | ||
| + | |||
| + | |||
| + | |||
| + | |||
| + | |||
| + | |||
| + | |||
| + | // Локальное хранилище для текстов текущего выбранного релиза, | ||
| + | // чтобы переключать вкладки мгновенно без повторных запросов к серверу | ||
| + | let currentReleaseTexts = { | ||
| + | readme: '', | ||
| + | license: '', | ||
| + | comment: '', | ||
| + | log: ' | ||
| + | }; | ||
| + | |||
| + | /** | ||
| + | * Отрисовка таблицы файлов в верхнем окне | ||
| + | * @param {Array} files Массив файлов из JSON | ||
| + | */ | ||
| + | function renderFilesTable(files) { | ||
| + | if (!files || files.length === 0) { | ||
| + | filesList.innerHTML = `<tr><td colspan=" | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | let html = ''; | ||
| + | files.forEach(file => { | ||
| + | const icon = file.is_dir ? ' | ||
| + | html += ` | ||
| + | < | ||
| + | < | ||
| + | <td class=" | ||
| + | <td class=" | ||
| + | <td style=" | ||
| + | </ | ||
| + | `; | ||
| + | }); | ||
| + | filesList.innerHTML = html; | ||
| + | } | ||
| + | |||
| + | // Вспомогательная функция защиты от XSS при отрисовке имен файлов | ||
| + | function escapeHtml(string) { | ||
| + | return String(string).replace(/&/ | ||
| + | } | ||
| + | |||
| + | // 1. СЛУШАЕМ ВЫБОР РЕЛИЗА В ВЫПАДАЮЩЕМ СПИСКЕ | ||
| + | releasesDropdown.addEventListener(' | ||
| + | const selectedRelease = this.value; | ||
| + | |||
| + | if (!selectedRelease) { | ||
| + | // Если сбросили выбор — возвращаем интерфейс в исходное состояние | ||
| + | btnDownloadZip.disabled = true; | ||
| + | filesList.innerHTML = `< | ||
| + | textEditor.value = ' | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | filesList.innerHTML = `< | ||
| + | |||
| + | // Дергаем наш апи-модуль | ||
| + | const result = await Api.getReleaseData(selectedRelease); | ||
| + | |||
| + | if (result.success) { | ||
| + | // Активируем кнопку ZIP | ||
| + | btnDownloadZip.disabled = false; | ||
| + | |||
| + | // Сохраняем тексты в память для вкладок | ||
| + | currentReleaseTexts.readme = result.texts.readme || ' | ||
| + | currentReleaseTexts.license = result.texts.license || ' | ||
| + | currentReleaseTexts.comment = result.texts.comment || ' | ||
| + | |||
| + | // Отрисовываем файлы в таблице верхнего окна | ||
| + | renderFilesTable(result.files); | ||
| + | |||
| + | // Автоматически показываем текст той вкладки, | ||
| + | const activeTab = document.querySelector(' | ||
| + | textEditor.value = currentReleaseTexts[activeTab]; | ||
| + | } else { | ||
| + | alert(" | ||
| + | filesList.innerHTML = `< | ||
| + | } | ||
| + | }); | ||
| + | |||
| + | // 2. СЛУШАЕМ ПЕРЕКЛЮЧЕНИЕ ВКЛАДОК НИЖНЕГО ОКНА | ||
| + | tabButtons.forEach(button => { | ||
| + | button.addEventListener(' | ||
| + | tabButtons.forEach(btn => btn.classList.remove(' | ||
| + | this.classList.add(' | ||
| + | |||
| + | const targetTab = this.getAttribute(' | ||
| + | |||
| + | // Вытаскиваем текст из памяти без лишних запросов к PHP | ||
| + | textEditor.value = currentReleaseTexts[targetTab] || `Текст для вкладки ${targetTab.toUpperCase()} отсутствует.`; | ||
| + | }); | ||
| + | }); | ||
| + | }); | ||
| - | ===== Создаем файл backend/ | ||
| - | Этот файл использует магические константы PHP и глобальные массивы сервера, | ||
| - | < | ||
| - | </ | ||
| - | ===== backend/ | ||
| - | Этот файл будет создаваться автоматически при самом первом запуске приложения. Он сгенерирует случайные 16-значные строки для ролей, которые заменят стандартные понятия super_admin и admin. Ни хакер, ни сторонний наблюдатель, | ||
| - | Шаг 1. Серверный генератор солей (backend/ | ||
tmp_27.06.2026.1782546208.txt.gz · Последнее изменение: — VladPolskiy
