# WA Dev Tools — Полная документация проекта > Версия документации: март 2026 > Репозиторий: `git.wadevelop.ru/treamz/wa-dev-tools` > Продакшн: `https://wadevelop.ru` --- ## Содержание 1. [Обзор проекта](#1-обзор-проекта) 2. [Архитектура](#2-архитектура) 3. [Инструменты](#3-инструменты) 4. [Авторизация и роли](#4-авторизация-и-роли) 5. [AdminJS панель](#5-adminjs-панель) 6. [База данных](#6-база-данных) 7. [API Reference](#7-api-reference) 8. [Фронтенд архитектура](#8-фронтенд-архитектура) 9. [Безопасность](#9-безопасность) 10. [Деплой и инфраструктура](#10-деплой-и-инфраструктура) 11. [Мониторинг](#11-мониторинг) 12. [Известные ограничения](#12-известные-ограничения) 13. [Будущие доработки](#13-будущие-доработки) --- ## 1. Обзор проекта **WA Dev Tools** — веб-платформа с набором утилит для разработчиков. Доступна по адресу `https://wadevelop.ru`. Целевая аудитория: веб-разработчики, которым нужны быстрые инструменты для работы с изображениями, видео, PDF, кодом и HTTP-запросами без установки десктопных приложений. ### Технологии | Слой | Технология | |------|-----------| | Среда выполнения | Node.js 20 | | Веб-фреймворк | Express 5 | | База данных | MariaDB 10.11 | | Обработка изображений | Sharp 0.34 | | Обработка видео | FFmpeg (системный), ffprobe | | Обработка PDF | pdf-lib, pdf-parse, Ghostscript (системный) | | Парсинг HTML | @mozilla/readability, jsdom | | Авторизация | express-session, bcrypt | | Панель администратора | AdminJS 7 + @adminjs/sequelize | | Стили | Tailwind CSS (CDN), кастомный shared.css | | Процесс-менеджер | PM2 | ### Сервер - Raspberry Pi 5, 8 GB RAM, Debian Bookworm - Nginx как реверс-прокси с SSL от Let's Encrypt - Файлы проекта: `/mnt/webdata/www/images.wadevelop.ru/` --- ## 2. Архитектура ### Файловая структура ``` /mnt/webdata/www/images.wadevelop.ru/ │ ├── server.js # Точка входа: Express app, middleware pipeline, запуск сервера ├── package.json # Зависимости (name: "tinypng" — историческое название) ├── ecosystem.config.js # PM2 конфигурация ├── .env # Переменные окружения (не в git) ├── compress.log # Лог операций сжатия (пишется через lib/logger.logToFile) │ ├── lib/ │ ├── auth.js # Middleware авторизации: список публичных путей, редиректы │ ├── admin.js # Настройка AdminJS: Sequelize модели, ресурсы, монтирование │ ├── db.js # Пул соединений MariaDB (mysql2/promise), хелперы для users │ ├── logger.js # Логгер: stdout + запись в compress.log через logToFile │ └── ssrf.js # Защита от SSRF: проверка IP-диапазонов, DNS-резолвинг │ ├── routes/ │ ├── api.js # Публичный API: /api/settings, /api/tools, /api/content/:section │ ├── auth.js # Авторизация: login, register, logout, /auth/me │ ├── compress.js # Конвертер изображений: сжатие, ресайз, архивирование │ ├── video.js # Видео конвертер: загрузка, конвертация через FFmpeg, прогресс │ ├── pdf.js # PDF инструменты: merge, split, rotate, watermark и др. │ ├── parser.js # Парсер статей: Readability + jsdom, кэш, несколько эндпоинтов │ ├── httpclient.js # HTTP-клиент: прокси-запросы, история в сессии │ ├── redirects.js # Redirect-анализатор: пошаговый обход цепочки редиректов │ ├── placeholder.js # Placeholder-изображения: генерация через Sharp + SVG │ ├── svgeditor.js # SVG редактор: страница + оптимизация + AI-иконки (lookup) │ ├── status.js # Страница статуса: проверка DB, сервисов, uptime │ ├── pages.js # Статические страницы: landing, dashboard, md, editor и др. │ └── logs.js # [вспомогательный] логирование (не экспортирует маршруты) │ ├── public/ │ ├── shared.css # Все общие стили: переменные, sidebar, top-header, компоненты │ ├── shared.js # Tailwind config, WA_TOOLS, WA_CATEGORIES, sidebar, тема │ ├── landing.html # Лендинг (публичный) │ ├── dashboard.html # Главная для авторизованных │ ├── login.html # Форма входа │ ├── register.html # Форма регистрации │ ├── status.html # Страница статуса сервисов │ ├── compress.html # Инструмент: конвертер изображений │ ├── video.html # Инструмент: видео конвертер │ ├── pdf.html # Инструмент: PDF инструменты │ ├── parser.html # Инструмент: парсер статей │ ├── httpclient.html # Инструмент: HTTP-клиент │ ├── redirects.html # Инструмент: redirect-анализатор │ ├── placeholder.html # Инструмент: placeholder изображения │ ├── svgeditor.html # Инструмент: SVG редактор (full-screen) │ ├── editor.html # Инструмент: фоторедактор (full-screen) │ ├── formatter.html # Инструмент: code formatter (только фронтенд) │ ├── sanitizer.html # Инструмент: HTML sanitizer (только фронтенд) │ ├── converter.html # Инструмент: format converter (только фронтенд) │ ├── password.html # Инструмент: генератор паролей (только фронтенд) │ ├── md.html # Инструмент: markdown viewer (только фронтенд) │ └── vendor/ # Локальные копии: tailwind.js, fonts.css │ ├── uploads/ # Временные загруженные файлы (автоочистка 10 мин) └── downloads/ # Результаты обработки (автоочистка 30 мин) ``` ### Middleware pipeline Порядок регистрации middleware в `server.js` критичен для корректной работы авторизации: ``` Запрос │ ├─ helmet() # Security headers (CSP отключен) ├─ session() # express-session: cookie maxAge 24ч ├─ express.static('public') # Статика — всегда публична ├─ /auth/* # Auth routes — до auth middleware ├─ GET /api/settings # Публичные API для лендинга ├─ GET /api/tools # ├─ GET /api/content/advantages # ├─ GET /api/content/dashboard # ├─ /status/* # Статус — публичный │ ├─ authMiddleware() # ← ГРАНИЦА АВТОРИЗАЦИИ │ ├─ /api/* # Защищённый API ├─ /health # Health check ├─ /compress # Инструменты (защищены) ├─ /download/:filename # Скачивание файлов (защищено) ├─ /parser, /placeholder # ... ├─ /httpclient, /redirects # ... ├─ /svgeditor, /video, /pdf # ... ├─ /admin/* # AdminJS (доп. проверка role=admin) └─ pages router # Страницы: /, /dashboard, /md и т.д. ``` ### Как работает авторизация `lib/auth.js` реализует middleware-функцию, которая пропускает запросы по белому списку без проверки: **Публичные пути** (без авторизации): - `PUBLIC_PATHS`: `/`, `/health`, `/favicon.ico`, `/status`, `/placeholder` - `PUBLIC_API`: `/api/settings`, `/api/tools`, `/api/content/advantages`, `/api/content/dashboard` - `PUBLIC_FILES`: `/shared.css`, `/shared.js`, `/landing.html` - `PUBLIC_PREFIXES`: `/auth/`, `/vendor/`, `/placeholder-img/`, `/status/` Всё остальное требует `req.session.user`. При отсутствии сессии: - GET-запросы — редирект на `/auth/login` с сохранением `returnTo` - API-запросы — `401 JSON` ### Как работает AdminJS AdminJS v7 — ESM-only пакет. Поскольку основное приложение использует CommonJS (`"type": "commonjs"`), AdminJS загружается через динамический импорт внутри async IIFE в `server.js`: ```js const { setupAdmin } = require('./lib/admin'); await setupAdmin(app); ``` Функция `setupAdmin` в `lib/admin.js`: 1. Создаёт отдельный экземпляр Sequelize (параллельно с пулом из `lib/db.js`) 2. Определяет Sequelize-модели для всех таблиц 3. Загружает `adminjs`, `@adminjs/express`, `@adminjs/sequelize` через `await import()` 4. Регистрирует адаптер, создаёт экземпляр AdminJS с ресурсами 5. Монтирует router через `app.use('/admin', authCheck, adminRouter)` Авторизация в AdminJS **не использует** встроенный механизм логина — вместо этого проверяется основная сессия Express (`req.session.user.role === 'admin'`). Статические ресурсы AdminJS (`.js`, `.css`, `.woff`) пропускаются без проверки. --- ## 3. Инструменты Платформа содержит **16 инструментов** в 4 категориях. 5 инструментов работают полностью на фронтенде без серверных вызовов. --- ### Категория: Изображения #### Конвертер изображений (`/compress`) **Что делает:** массовая конвертация и сжатие изображений с изменением формата и размера. Результат упаковывается в ZIP-архив. **Бэкенд:** `routes/compress.js` | Эндпоинт | Метод | Описание | |----------|-------|----------| | `GET /compress` | GET | Страница инструмента | | `POST /compress` | POST | Загрузка файлов, обработка, возврат ссылки на ZIP | | `GET /download/:filename` | GET | Скачивание ZIP (монтируется в server.js) | **Параметры POST-запроса (multipart/form-data):** - `images[]` — файлы (до 50 штук) - `format` — `original`, `webp`, `jpeg`, `png`, `avif`, `tiff`, `gif` - `quality` — 1–100 (по умолчанию из `COMPRESS_QUALITY` в .env, обычно 80) - `resize` — максимальный размер по длинной стороне в пикселях (0 = без ресайза) **Зависимости:** `sharp`, `archiver`, `multer`, `geoip-lite` **Принцип работы:** 1. Multer сохраняет файлы во временный `uploads/` 2. Sharp обрабатывает каждый файл (конвертация + опциональный ресайз) 3. Буферы добавляются в ZIP через archiver 4. ZIP сохраняется в `downloads/`, клиенту возвращается URL 5. Загруженные файлы удаляются немедленно, ZIP — через 30 минут 6. Кириллические имена файлов транслитерируются **Лимиты:** - Размер файла: `MAX_FILE_SIZE_MB` (по умолчанию 20 MB) - Количество файлов: `MAX_FILES` (по умолчанию 50) - Rate limit: `RATE_LIMIT_MAX` запросов за `RATE_LIMIT_WINDOW_MS` (по умолчанию 30/мин) **Поддерживаемые форматы входа:** JPEG, PNG, WebP, AVIF, TIFF, GIF, BMP, SVG **Фронтенд:** `public/compress.html` — drag & drop загрузка, превью файлов, прогресс-бар, таблица статистики сжатия. --- #### Placeholder (`/placeholder`) **Что делает:** генерирует placeholder-изображения по URL-шаблону. Используется в разработке вместо реальных картинок. Работает **без авторизации**. **Бэкенд:** `routes/placeholder.js` | Эндпоинт | Метод | Описание | |----------|-------|----------| | `GET /placeholder` | GET | Страница с документацией | | `GET /placeholder/:size/:bg/:fg` | GET | Генерация изображения | | `GET /placeholder/:size/:bg/:fg/:text` | GET | С кастомным текстом | | `GET /placeholder-img/:size/:bg/:fg` | GET | Алиас (тот же роутер) | **Параметры URL:** - `size` — `300x200`, `1920x1080` и т.д. (макс. 4000x4000) - `bg` — цвет фона в hex без `#` (например `cccccc`) - `fg` — цвет текста - `text` — текст поверх изображения (опционально) - `?format=png|jpg|webp` — формат вывода (по умолчанию png) - `?fontsize=N` — размер шрифта (по умолчанию автоматически) **Пример:** `GET /placeholder/800x400/0054e6/ffffff/Preview` **Зависимости:** `sharp` (SVG → растровое) **Фронтенд:** `public/placeholder.html` — конструктор URL, предпросмотр, код для вставки. --- #### SVG-редактор (`/svgeditor`) **Что делает:** редактирование SVG-кода с предпросмотром, оптимизацией и генерацией простых иконок по ключевому слову. **Бэкенд:** `routes/svgeditor.js` | Эндпоинт | Метод | Описание | |----------|-------|----------| | `GET /svgeditor` | GET | Страница редактора (full-screen) | | `POST /api/svg-optimize` | POST | Оптимизация SVG (до 5 MB) | | `POST /api/svg-ai` | POST | Генерация иконки по ключевому слову | **Оптимизация** (`/api/svg-optimize`) выполняется на сервере regexp-преобразованиями: - Удаление ``, комментариев, ``, ``, `<desc>` - Удаление пустых `<g>` и `data-*` атрибутов - Округление чисел до 2 знаков, сжатие пробелов **AI-генерация** (`/api/svg-ai`) — это lookup-таблица из ~20 предопределённых SVG-путей (home, user, search, heart, star, и т.д.) в стилях outline/filled/duotone. Не использует внешний AI. **Параметры `/api/svg-ai`:** `{ keyword, style: "outline"|"filled"|"duotone", size, color }` **Фронтенд:** `public/svgeditor.html` — полноэкранный редактор, split-панель код/превью. --- #### Фоторедактор (`/editor`) **Что делает:** браузерный редактор изображений на Canvas API. Работает **полностью на фронтенде** — серверных вызовов нет. **Бэкенд:** `routes/pages.js` — только отдаёт `editor.html` **Фронтенд:** `public/editor.html` + `public/editor.js` — полноэкранный редактор. Возможности (реализованы на Canvas): обрезка, яркость/контраст/насыщенность, фильтры, вращение, отражение, сохранение в JPEG/PNG. --- #### Видео конвертер (`/video`) **Что делает:** конвертация видеофайлов между форматами, сжатие, извлечение аудио, создание GIF. Обработка асинхронная с polling-прогресса. **Бэкенд:** `routes/video.js` | Эндпоинт | Метод | Описание | |----------|-------|----------| | `GET /video` | GET | Страница инструмента | | `POST /video/upload` | POST | Загрузка файла, получение `jobId` и метаданных | | `POST /video/convert` | POST (JSON) | Запуск конвертации, возвращает немедленно | | `GET /video/progress/:jobId` | GET | Статус и прогресс задачи (0–100%) | | `GET /video/download/:filename` | GET | Скачивание результата | **Режимы конвертации (`mode`):** - `convert` — смена формата (mp4, webm, avi, mkv) - `compress` — сжатие с выбором качества (high/medium/low) - `audio` — извлечение аудио (mp3, aac) - `gif` — конвертация в анимированный GIF (макс. ширина 480px, 12 fps) **Зависимости:** FFmpeg, ffprobe (системные), `multer` **Лимиты:** - Обычный пользователь: 200 MB - Администратор: 1 GB - Таймаут FFmpeg: 10 минут - Rate limit: 10 запросов/мин **Хранение задач:** in-memory Map `jobs`, автоочистка через 30 минут. При рестарте сервера все задачи теряются. **Прогресс:** FFmpeg пишет в stderr строки вида `time=00:01:23`, роут парсит их и обновляет `job.progress`. **Фронтенд:** `public/video.html` — загрузка, выбор режима и параметров, polling прогресса, скачивание. --- ### Категория: Код #### Code Formatter (`/formatter`) **Что делает:** форматирование CSS и JavaScript с правильными отступами. Работает **полностью на фронтенде**. **Бэкенд:** только `GET /formatter` → `formatter.html` **Алгоритм форматирования** реализован в `formatter.html` через regexp: - Строки и комментарии сохраняются в массив (не затрагиваются) - Расставляются переносы строк вокруг `{` и `}` - Применяется отступ 2 пробела с счётчиком вложенности Нет зависимости от сторонних библиотек (prettier, babel и т.д.). **Фронтенд:** `public/formatter.html` — две textarea (input/output), переключатель CSS/JS, кнопки копировать/очистить, счётчик символов. --- #### HTML Sanitizer (`/sanitizer`) **Что делает:** очистка HTML от потенциально опасных тегов и атрибутов. Работает **полностью на фронтенде** через браузерный DOM. **Бэкенд:** только `GET /sanitizer` → `sanitizer.html` **Фронтенд:** `public/sanitizer.html` --- #### Format Converter (`/converter`) **Что делает:** конвертация данных между форматами (JSON, YAML, CSV и т.д.). Работает **полностью на фронтенде**. **Бэкенд:** только `GET /converter` → `converter.html` **Фронтенд:** `public/converter.html` --- ### Категория: Веб #### Парсер статей (`/parser`) **Что делает:** извлекает основной текст статьи из URL с помощью алгоритма Readability (тот же, что использует Firefox Reader View). Возвращает заголовок, автора, текст, HTML, изображения, ссылки. **Бэкенд:** `routes/parser.js` | Эндпоинт | Метод | Описание | |----------|-------|----------| | `GET /parser` | GET | Страница инструмента | | `GET /parse?url=` | GET | Полный разбор статьи | | `GET /metadata?url=` | GET | Только OG/мета-теги | | `GET /text?url=` | GET | Только текст (text/plain) | | `GET /preview?url=` | GET | Карточка предпросмотра | **Зависимости:** `@mozilla/readability`, `jsdom`, `iconv-lite`, `geoip-lite` **Особенности:** - SSRF-защита через `lib/ssrf.js` на всех эндпоинтах - In-memory кэш: 50 записей, TTL 10 минут - Определение кодировки из `Content-Type` и `<meta charset>` - Лимит страницы: 2 MB - Таймаут запроса: 10 секунд - Rate limit: 20 запросов/мин **Ответ `/parse`:** ```json { "url": "...", "title": "...", "author": "...", "site": "...", "date_published": "...", "excerpt": "...", "content_html": "...", "content_text": "...", "lead_image": "...", "images": [...], "links": [{"text": "...", "href": "..."}], "word_count": 1234, "lang": "ru" } ``` **Фронтенд:** `public/parser.html` — поле URL, вкладки с результатами (текст, HTML, метаданные, изображения). --- #### HTTP-клиент (`/httpclient`) **Что делает:** выполняет произвольные HTTP-запросы с сервера (полезно для тестирования API, недоступных из браузера из-за CORS). История сохраняется в сессии. **Бэкенд:** `routes/httpclient.js` | Эндпоинт | Метод | Описание | |----------|-------|----------| | `GET /httpclient` | GET | Страница инструмента | | `POST /api/proxy` | POST (JSON) | Выполнение HTTP-запроса | | `GET /api/history` | GET | История запросов сессии (до 30) | | `POST /api/history/add` | POST | Добавление записи в историю | | `DELETE /api/history` | DELETE | Очистка истории | **Параметры `/api/proxy`:** ```json { "url": "https://example.com/api", "method": "POST", "headers": {"Authorization": "Bearer ..."}, "body": "{\"key\": \"value\"}", "timeout": 30000 } ``` **Ответ `/api/proxy`:** ```json { "status": 200, "statusText": "OK", "headers": {...}, "body": "...", "time": 234, "size": 1024, "truncated": false, "url": "..." } ``` **Ограничения:** - Тело ответа обрезается до 10 MB - Таймаут: до 60 секунд (параметр `timeout`, но не более 60000 мс) - SSRF-защита: приватные IP и localhost заблокированы - Хедеры `host`, `connection`, `transfer-encoding` фильтруются из запроса - Rate limit прокси: `PROXY_RATE_LIMIT_MAX` (по умолчанию 60/мин) **Фронтенд:** `public/httpclient.html` — Postman-подобный интерфейс: метод, URL, заголовки, тело, подсветка ответа, история. --- #### Redirect-анализатор (`/redirects`) **Что делает:** пошагово обходит цепочку HTTP-редиректов, показывает каждый шаг (статус, Location, время), выявляет проблемы (длинная цепочка, смешанный HTTP/HTTPS, 302 вместо 301). **Бэкенд:** `routes/redirects.js` | Эндпоинт | Метод | Описание | |----------|-------|----------| | `GET /redirects` | GET | Страница инструмента | | `POST /api/redirect-analyze` | POST (JSON) | Анализ цепочки редиректов | **Параметры:** ```json { "url": "https://example.com", "userAgent": "desktop|mobile|googlebot", "method": "GET|HEAD" } ``` **Ответ:** ```json { "chain": [ {"url": "...", "status": 301, "statusText": "Moved Permanently", "location": "...", "time": 123, "headers": {...}}, {"url": "...", "status": 200, "statusText": "OK", "location": null, "time": 456, "headers": {...}} ], "final_url": "...", "total_time": 579, "loop_detected": false, "issues": ["long_chain", "302_not_301"] } ``` **Ограничения:** - Максимум 15 шагов в цепочке - Таймаут на каждый шаг: 10 секунд - SSRF-защита - Rate limit: 20 запросов/мин **Фронтенд:** `public/redirects.html` — визуальная цепочка шагов, выделение ошибок, подсказки. --- ### Категория: Утилиты #### Генератор паролей (`/password`) **Что делает:** генерация случайных паролей с настройкой длины и набора символов. Работает **полностью на фронтенде** через `crypto.getRandomValues()`. **Бэкенд:** только `GET /password` → `password.html` **Фронтенд:** `public/password.html` — слайдер длины, чекбоксы наборов символов, индикатор сложности, кнопка копирования. --- #### Markdown Viewer (`/md`) **Что делает:** рендеринг Markdown в HTML с предпросмотром. Работает **полностью на фронтенде**. **Бэкенд:** только `GET /md` → `md.html` **Фронтенд:** `public/md.html` — split-панель редактор/превью, библиотека marked.js. --- #### PDF инструменты (`/pdf`) **Что делает:** полный набор операций с PDF-файлами: объединение, разделение, поворот, удаление страниц, перестановка, водяной знак, нумерация, сжатие через Ghostscript, защита паролем, конвертация PDF → изображения и изображений → PDF. **Бэкенд:** `routes/pdf.js` | Эндпоинт | Метод | Описание | |----------|-------|----------| | `GET /pdf` | GET | Страница инструмента | | `POST /pdf/upload` | POST | Загрузка PDF/изображений, возврат `fileId` | | `GET /pdf/preview/:fileId` | GET | Превью первой страницы (PNG, Ghostscript) | | `GET /pdf/preview/:fileId/:page` | GET | Превью конкретной страницы | | `GET /pdf/thumbnails/:fileId` | GET | Список URL превью всех страниц | | `GET /pdf/info/:fileId` | GET | Метаданные PDF (страницы, автор, заголовок) | | `POST /pdf/merge` | POST (JSON) | Объединение нескольких PDF | | `POST /pdf/split` | POST (JSON) | Извлечение страниц по диапазону | | `POST /pdf/rotate` | POST (JSON) | Поворот страниц | | `POST /pdf/delete` | POST (JSON) | Удаление страниц | | `POST /pdf/reorder` | POST (JSON) | Перестановка страниц | | `POST /pdf/watermark` | POST (JSON) | Добавление текстового водяного знака | | `POST /pdf/pagenumbers` | POST (JSON) | Нумерация страниц | | `POST /pdf/compress` | POST (JSON) | Сжатие через Ghostscript | | `POST /pdf/protect` | POST (JSON) | Защита паролем (pdf-lib encrypt) | | `POST /pdf/extract-text` | POST (JSON) | Извлечение текста | | `POST /pdf/toImages` | POST (JSON) | PDF → изображения ZIP (Ghostscript) | | `POST /pdf/fromImages` | POST | Изображения → PDF (pdf-lib) | | `GET /pdf/download/:filename` | GET | Скачивание результата | **Зависимости:** `pdf-lib` (ESM, через dynamic import), `pdf-parse`, Ghostscript (системный, команда `gs`) **Хранение файлов:** in-memory Map `pdfFiles` с `fileId`, автоочистка через 30 минут. **Превью** кэшируется на диске в `downloads/preview_<fileId>_p<N>.png`, удаляется через 30 минут. **Лимиты:** - Размер файла: 50 MB - До 10 файлов за раз при загрузке - До 50 изображений при конвертации из картинок **Фронтенд:** `public/pdf.html` — drag & drop загрузка, визуальная сетка миниатюр страниц, панель операций. --- ## 4. Авторизация и роли ### Flow регистрации 1. `GET /auth/register` → форма `register.html` 2. `POST /auth/register` — валидация email (regexp), пароль ≥ 6 символов, проверка на дубликат 3. Хеширование пароля `bcrypt.hash(password, 10)` 4. Запись в `users`: email (lowercase), password_hash, display_name 5. Создание сессии `req.session.user = { id, email, name, role: 'user' }` 6. Редирект на `/dashboard` `display_name` санируется: HTML-теги удаляются, длина ограничена 100 символами. ### Flow логина 1. `GET /auth/login` → форма `login.html` 2. `POST /auth/login` — поиск пользователя по email, проверка `is_blocked`, `bcrypt.compare` 3. `db.updateLastLogin(id)` — обновление поля `last_login` 4. **Session regeneration** (`req.session.regenerate`) для защиты от session fixation 5. Новая сессия с данными пользователя 6. Редирект на `returnTo` или `/dashboard` ### Структура сессии ```js req.session.user = { id: 42, email: "user@example.com", name: "Иван", role: "user" // или "admin" } ``` Дополнительные поля сессии: - `req.session.returnTo` — URL для редиректа после логина - `req.session.httpHistory` — история HTTP-клиента (до 30 записей) ### Роли | Возможность | `user` | `admin` | |-------------|--------|---------| | Доступ к инструментам | да | да | | Лимит загрузки видео | 200 MB | 1 GB | | Ссылка «Админ» в сайдбаре | нет | да | | Доступ к `/admin` | нет | да | ### Публичные маршруты (без авторизации) - Лендинг `/` - Страницы: `/auth/login`, `/auth/register` - API лендинга: `/api/settings`, `/api/tools`, `/api/content/advantages`, `/api/content/dashboard` - Статус: `/status`, `/status/api` - Placeholder: `/placeholder`, `/placeholder-img/*` - Статика: `shared.css`, `shared.js`, `landing.html`, всё из `/vendor/` ### Конфигурация сессии ```js session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { maxAge: 24 * 60 * 60 * 1000, // 1 день httpOnly: true, // недоступно JS secure: NODE_ENV === 'production', // только HTTPS sameSite: 'lax', // защита от CSRF } }) ``` Сессии хранятся in-memory (MemoryStore Express). При рестарте PM2 все сессии теряются — пользователям нужно войти заново. ### Rate limiting для auth 15 минут, 10 попыток (применяется к `POST /auth/login` и `POST /auth/register`). --- ## 5. AdminJS панель **URL:** `https://wadevelop.ru/admin` ### Доступ Доступ контролируется двойной проверкой: 1. Основная сессия Express (`req.session.user` должен существовать) 2. Роль администратора (`req.session.user.role === 'admin'`) Если пользователь не авторизован или не является admin — редирект на `/auth/login` с `returnTo=/admin`. Встроенный логин AdminJS не используется. ### Управляемые сущности | Сущность | Навигация | Описание | |----------|-----------|----------| | Users | Пользователи | Управление аккаунтами, блокировка, смена роли | | Categories | Контент | Категории инструментов с иконками и цветами | | Tools | Контент | Список инструментов, включение/отключение | | ContentBlocks | Контент | Текстовые блоки лендинга (преимущества, контент дашборда) | | Settings | Настройки | Ключ-значение настройки сайта | ### Смена пароля пользователя В форме редактирования User есть два виртуальных поля: `password` и `password_confirm`. Они не хранятся в БД — вместо этого action-hook `before` обрабатывает их: 1. Если `password` пустой — пропуск (пароль не меняется) 2. Если `password !== password_confirm` — ошибка валидации 3. Если длина < 6 символов — ошибка 4. Иначе: `bcrypt.hash(password, 10)` → запись в `password_hash` Поле `password_hash` скрыто в интерфейсе (`isVisible: false`). ### Особенности AdminJS - Загрузка занимает ~6 секунд при старте сервера (ESM import + инициализация Sequelize) - В первые секунды после рестарта PM2 `/admin` может возвращать 502 - Sequelize использует отдельное соединение, независимое от пула `lib/db.js` --- ## 6. База данных **СУБД:** MariaDB 10.11 **База:** `wa_tools` **Пользователь:** `wa_tools` **Кодировка:** utf8mb4 ### Таблица `users` | Поле | Тип | Описание | |------|-----|----------| | `id` | INT AUTO_INCREMENT PK | Идентификатор | | `email` | VARCHAR(255) UNIQUE NOT NULL | Email (хранится в lowercase) | | `password_hash` | VARCHAR(255) NOT NULL | bcrypt-хеш пароля | | `display_name` | VARCHAR(100) | Отображаемое имя | | `role` | ENUM('user', 'admin') | Роль, по умолчанию 'user' | | `is_blocked` | BOOLEAN | Заблокирован ли аккаунт | | `created_at` | DATETIME | Дата регистрации | | `last_login` | DATETIME | Дата последнего входа | ### Таблица `settings` Ключ-значение для настроек сайта. Отдаётся через `/api/settings` на лендинг. | Поле | Тип | Описание | |------|-----|----------| | `id` | INT AUTO_INCREMENT PK | Идентификатор | | `setting_key` | VARCHAR(100) UNIQUE NOT NULL | Ключ настройки | | `setting_value` | TEXT | Значение | | `description` | VARCHAR(255) | Описание (для AdminJS) | | `updated_at` | DATETIME | Дата последнего изменения | ### Таблица `categories` Категории инструментов для лендинга и дашборда. | Поле | Тип | Описание | |------|-----|----------| | `id` | INT AUTO_INCREMENT PK | Идентификатор | | `slug` | VARCHAR(50) UNIQUE NOT NULL | URL-идентификатор (images, code, web, utils) | | `title` | VARCHAR(100) NOT NULL | Название | | `description` | VARCHAR(255) | Краткое описание | | `icon_svg` | TEXT | SVG-путь иконки | | `color` | VARCHAR(20) | Цвет акцента (по умолчанию '#0054e6') | | `sort_order` | INT | Порядок сортировки | ### Таблица `tools` Список инструментов платформы. | Поле | Тип | Описание | |------|-----|----------| | `id` | INT AUTO_INCREMENT PK | Идентификатор | | `slug` | VARCHAR(50) UNIQUE NOT NULL | URL-идентификатор | | `title` | VARCHAR(100) NOT NULL | Название | | `description` | VARCHAR(255) | Описание | | `path` | VARCHAR(100) NOT NULL | URL-путь (например `/compress`) | | `icon_svg` | TEXT | SVG-путь иконки | | `category_id` | INT FK → categories.id | Категория | | `sort_order` | INT | Порядок сортировки | | `is_enabled` | BOOLEAN | Отображается ли инструмент | ### Таблица `content_blocks` Редактируемый контент для лендинга. | Поле | Тип | Описание | |------|-----|----------| | `id` | INT AUTO_INCREMENT PK | Идентификатор | | `block_key` | VARCHAR(100) UNIQUE NOT NULL | Ключ блока | | `title` | VARCHAR(255) | Заголовок блока | | `body` | TEXT | Содержимое (HTML, richtext в AdminJS) | | `section` | VARCHAR(50) | Секция: `advantages`, `dashboard` | | `sort_order` | INT | Порядок | | `is_visible` | BOOLEAN | Отображается ли блок | | `updated_at` | DATETIME | Дата последнего изменения | ### Соединение с БД `lib/db.js` использует `mysql2/promise` с connection pool (лимит 5 соединений). Предоставляет 4 хелпера: `findUserByEmail`, `findUserById`, `createUser`, `updateLastLogin`, и экспортирует `pool` для прямых запросов из роутов. --- ## 7. API Reference ### Публичные API (без авторизации) | Метод | Путь | Описание | |-------|------|----------| | GET | `/api/settings` | Все настройки как `{key: value}` | | GET | `/api/tools` | Категории с вложенными инструментами | | GET | `/api/content/advantages` | Блоки секции advantages | | GET | `/api/content/dashboard` | Блоки секции dashboard | ### Auth API | Метод | Путь | Описание | |-------|------|----------| | GET | `/auth/login` | Страница логина | | GET | `/auth/register` | Страница регистрации | | POST | `/auth/login` | Вход: `{email, password}` → `{ok, redirect}` | | POST | `/auth/register` | Регистрация: `{email, password, password2, name}` → `{ok, redirect}` | | GET | `/auth/logout` | Выход, разрушение сессии | | GET | `/auth/me` | Данные текущего пользователя → `{id, email, name, role}` | ### Tool APIs (защищены) #### Изображения | Метод | Путь | Описание | |-------|------|----------| | GET | `/compress` | Страница инструмента | | POST | `/compress` | `multipart: images[], format, quality, resize` → `{downloadUrl, stats}` | | GET | `/download/:filename` | Скачивание ZIP | | GET | `/placeholder/:size/:bg/:fg` | Генерация placeholder (публично) | | GET | `/placeholder/:size/:bg/:fg/:text` | С текстом (публично) | | POST | `/api/svg-optimize` | `{svg}` → `{svg, saved}` | | POST | `/api/svg-ai` | `{keyword, style, size, color}` → `{svg, keyword, available}` | #### Видео | Метод | Путь | Описание | |-------|------|----------| | GET | `/video` | Страница | | POST | `/video/upload` | `multipart: video` → `{jobId, info, originalName, size}` | | POST | `/video/convert` | `{jobId, mode, format, quality, startTime, endTime}` → `{status}` | | GET | `/video/progress/:jobId` | → `{status, progress, downloadUrl?, outputSize?, savings?, error?}` | | GET | `/video/download/:filename` | Скачивание результата | #### PDF | Метод | Путь | Описание | |-------|------|----------| | GET | `/pdf` | Страница | | POST | `/pdf/upload` | `multipart: files[]` → `{files: [{id, name, size, pages}]}` | | GET | `/pdf/preview/:fileId[/:page]` | PNG превью страницы | | GET | `/pdf/thumbnails/:fileId` | `{pages, thumbnails: [...urls]}` | | GET | `/pdf/info/:fileId` | `{pages, title, author, creator, size}` | | POST | `/pdf/merge` | `{fileIds: [...]}` → `{downloadUrl, size}` | | POST | `/pdf/split` | `{fileId, ranges: "1-3,5"}` → `{downloadUrl, size, pages}` | | POST | `/pdf/rotate` | `{fileId, pages: "all"|"1,3", angle: 90}` → `{downloadUrl, size}` | | POST | `/pdf/delete` | `{fileId, pages: "2,4"}` → `{downloadUrl, size, pages}` | | POST | `/pdf/reorder` | `{fileId, order: [3,1,2]}` → `{downloadUrl, size}` | | POST | `/pdf/watermark` | `{fileId, text, fontSize, opacity, color}` → `{downloadUrl, size}` | | POST | `/pdf/pagenumbers` | `{fileId, position, startFrom}` → `{downloadUrl, size}` | | POST | `/pdf/compress` | `{fileId, quality: "screen"|"ebook"|"printer"}` → `{downloadUrl, size, savings}` | | POST | `/pdf/protect` | `{fileId, password}` → `{downloadUrl, size}` | | POST | `/pdf/extract-text` | `{fileId}` → `{text, pages, metadata}` | | POST | `/pdf/toImages` | `{fileId, format: "png"|"jpg", dpi: 150}` → `{downloadUrl, size}` | | POST | `/pdf/fromImages` | `multipart: images[]` → `{downloadUrl, size, pages}` | | GET | `/pdf/download/:filename` | Скачивание результата | #### Веб-инструменты | Метод | Путь | Описание | |-------|------|----------| | GET | `/parse?url=` | Полный разбор статьи | | GET | `/metadata?url=` | Только метаданные | | GET | `/text?url=` | Только текст | | GET | `/preview?url=` | Карточка предпросмотра | | POST | `/api/proxy` | HTTP-прокси запрос | | GET | `/api/history` | История HTTP-клиента | | POST | `/api/history/add` | Добавить в историю | | DELETE | `/api/history` | Очистить историю | | POST | `/api/redirect-analyze` | Анализ цепочки редиректов | ### System API | Метод | Путь | Auth | Описание | |-------|------|------|----------| | GET | `/health` | нет | `{status, uptime, memory, node, pid}` | | GET | `/status` | нет | Страница статуса | | GET | `/status/api` | нет | `{overall, uptime, services, timestamp}` | --- ## 8. Фронтенд архитектура ### shared.css Единый файл стилей для всех страниц. Структура: **CSS-переменные** (`:root` и `.dark`): - `--bg`, `--bg-grad1/2` — фон страницы с радиальными градиентами - `--surface-800/700/600` — уровни поверхностей (фон карточек, бордеры) - `--text-primary/secondary/muted` — уровни текста - `--accent`, `--accent-dim`, `--accent-bright`, `--accent-bg` — акцентный синий (#0054e6) **Компоненты:** - `.sidebar` — фиксированная боковая панель 56px (на мобильных: нижняя панель 52px) - `.sidebar-link` — кнопка инструмента 36x36px с активным состоянием и индикатором - `.sidebar-avatar` — аватар пользователя (первая буква имени) - `.theme-toggle` — переключатель темы - `.top-header` — верхняя шапка для публичных страниц - `.tool-container` — контейнер содержимого с padding - `.wa-input`, `.wa-btn`, `.wa-card` — базовые элементы форм - `.wa-modal-overlay`, `.wa-modal` — модальное окно с анимацией ### shared.js Загружается на всех страницах с sidebar. Содержит: **`WA_CATEGORIES`** — массив категорий: `id`, `title`, `description`, `icon` (SVG-path), `tools` (массив slug-ов). **`WA_TOOLS`** — массив всех инструментов: `id`, `path`, `title`, `category`, `icon`, `adminOnly`. Определяет полный список сайдбара. Добавить новый инструмент = добавить объект сюда. **`initSidebar(activeId)`** — вставляет `<nav class="sidebar">` в `document.body.prepend()`. Автоматически определяет активный инструмент по `window.WA_TOOL_ID` (устанавливается в каждом HTML) или по URL. Группирует ссылки по категориям с разделителями. **`_loadUser()`** — вызывает `/auth/me`, заполняет аватар инициалами имени. Для admin-пользователей делает видимыми элементы `.sidebar-admin-link` (ссылку «Админ»). **`_showLogoutModal(userName)`** — модальное окно подтверждения выхода. **`initTheme()`** — читает `localStorage.theme`, устанавливает класс `.dark` на `<html>`. Переключение через клик на `#themeToggle`. Вызывается немедленно (до DOMContentLoaded) для предотвращения мигания. **Tailwind config** — тёмный режим через `class`, кастомные цвета `surface.*`, `accent.*`, шрифты Manrope и JetBrains Mono. ### Типы страниц | Тип | Примеры | Layout | |-----|---------|--------| | Tool pages | `/compress`, `/pdf`, `/video` | Sidebar слева, `<div class="main-content">` | | Public pages | `/`, `/auth/login`, `/status` | `top-header` сверху, без sidebar | | Full-screen | `/editor`, `/svgeditor` | Без sidebar и top-header, 100vw/100vh | ### Страница инструмента (шаблон) ```html <script>window.WA_TOOL_ID = 'compress';</script> <!-- устанавливает активный элемент sidebar --> <script src="/shared.js?v=3"></script> <link href="/shared.css" rel="stylesheet"> <body> <!-- sidebar вставляется shared.js автоматически --> <div class="main-content"> <div class="tool-container"> <!-- содержимое инструмента --> </div> </div> </body> ``` ### Лендинг (`/`) `public/landing.html` — публичная страница, загружает данные динамически: - `/api/settings` — настройки (заголовок, описание сайта) - `/api/tools` — список инструментов для секции категорий - `/api/content/advantages` — блоки преимуществ Структура: hero с CTA → секция категорий → секция преимуществ → footer. ### Дашборд (`/dashboard`) `public/dashboard.html` — главная страница для авторизованных пользователей. Загружает `/auth/me` для персонализации приветствия, `/api/tools` для отображения категорий с иконками. ### Яндекс Метрика Счётчик `108185982` подключён на всех страницах инструментов (в конце `<body>`). --- ## 9. Безопасность ### HTTP-заголовки (helmet) ```js helmet({ contentSecurityPolicy: false, // отключено — Tailwind CDN требует inline-скриптов crossOriginEmbedderPolicy: false, }) ``` Остальные политики helmet включены: HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy. ### Защита от SSRF (`lib/ssrf.js`) Применяется в HTTP-клиенте, парсере и redirect-анализаторе. Два уровня проверки: 1. **Синтаксическая** — только `http:` и `https:`, блокировка `localhost`, `*.local`, `*.internal` 2. **Диапазоны IP** (regexp): - `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16` — приватные RFC 1918 - `127.0.0.0/8` — loopback - `169.254.0.0/16` — link-local - `::1`, `fc*/fd*`, `fe80*` — IPv6 приватные 3. **DNS-резолвинг** — hostname разрешается через `dns.resolve4`, все IP проверяются на попадание в приватные диапазоны ### Загрузка файлов - MIME-тип валидируется через whitelist (multer `fileFilter`) - Проверяется также расширение файла (regexp) как запасной вариант - Максимальный размер задаётся через переменные окружения - Временные файлы удаляются немедленно после обработки ### Path traversal Имена файлов при скачивании обрабатываются через `path.basename(req.params.filename)` — попытки выйти из директории (`../`) отсекаются. ### Пароли - bcrypt с `saltRounds: 10` - Минимальная длина: 6 символов - Оригинальный пароль никогда не логируется ### Сессия - `httpOnly: true` — недоступно JavaScript - `secure: true` в production — только по HTTPS - `sameSite: 'lax'` — защита от CSRF - Session regeneration при логине — защита от session fixation attack - Logout: `req.session.destroy()` ### Санитизация ввода - `display_name` — HTML-теги удаляются regexp'ом, длина обрезается до 100 - Email — нормализуется в lowercase, проверяется regexp ### AdminJS Двойная проверка: наличие сессии + role === 'admin'. Статические ресурсы AdminJS (`.js`, `.css`, `.woff` и т.д.) пропускаются без auth-проверки по расширению. --- ## 10. Деплой и инфраструктура ### PM2 Конфигурация в `ecosystem.config.js`: ```js { name: 'images', script: 'server.js', cwd: '/mnt/webdata/www/images.wadevelop.ru', instances: 1, exec_mode: 'fork', max_memory_restart: '256M', restart_delay: 3000, max_restarts: 10, min_uptime: 5000, } ``` Логи: - stdout: `~/.pm2/logs/images-out.log` - stderr: `~/.pm2/logs/images-error.log` - Формат времени: `YYYY-MM-DD HH:mm:ss` Команды управления: ```bash pm2 start ecosystem.config.js # первый запуск pm2 restart images # перезапуск pm2 reload images # graceful reload (0-downtime) pm2 logs images # просмотр логов pm2 monit # мониторинг в реальном времени ``` ### Nginx **wadevelop.ru** — основной сайт: ```nginx server { server_name wadevelop.ru www.wadevelop.ru; listen 443 ssl; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; client_max_body_size 200M; # для загрузки видео } } # HTTP → HTTPS редирект управляется certbot ``` **images.wadevelop.ru** — 301 редирект на wadevelop.ru (историческое доменное имя). Логи nginx: - `/var/log/nginx/wadevelop.access.log` - `/var/log/nginx/wadevelop.error.log` ### SSL Let's Encrypt через certbot. Сертификат: - `/etc/letsencrypt/live/wadevelop.ru/fullchain.pem` - `/etc/letsencrypt/live/wadevelop.ru/privkey.pem` Автообновление через certbot systemd timer. ### Переменные окружения (`.env`) | Переменная | Описание | По умолчанию | |-----------|----------|--------------| | `PORT` | Порт Express | 3000 | | `NODE_ENV` | Окружение (`production`/`development`) | — | | `SESSION_SECRET` | Секрет сессий (обязательно сменить) | 'change-me-in-env' | | `DB_HOST` | Хост MariaDB | localhost | | `DB_USER` | Пользователь БД | wa_tools | | `DB_PASSWORD` | Пароль БД | — | | `DB_NAME` | Имя базы данных | wa_tools | | `COMPRESS_QUALITY` | Качество сжатия изображений по умолчанию | 60 | | `MAX_FILE_SIZE_MB` | Максимальный размер файла для конвертера изображений (MB) | 20 | | `MAX_FILES` | Максимальное количество файлов за раз | 50 | | `RATE_LIMIT_WINDOW_MS` | Окно rate limit (мс) | 60000 | | `RATE_LIMIT_MAX` | Максимум запросов за окно (compress) | 30 | | `PROXY_RATE_LIMIT_MAX` | Максимум запросов за окно (http proxy) | 60 | | `LOG_LEVEL` | Уровень логирования (`error`/`warn`/`info`/`debug`) | info | --- ## 11. Мониторинг ### Страница статуса `/status` Публичная страница, доступная без авторизации. API: `GET /status/api`. Проверяемые сервисы: - **API** — всегда `operational` (если сервер отвечает) - **Database** — `pool.execute('SELECT 1')`, `operational` или `outage` - **Image Processing** — всегда `operational` - **Video Processing** — всегда `operational` - **Authentication** — всегда `operational` Ответ: ```json { "overall": "operational", "uptime": 86400, "services": [ {"name": "API", "status": "operational"}, {"name": "Database", "status": "operational"} ], "timestamp": "2026-03-22T10:00:00.000Z" } ``` ### Health check `/health` ```json { "status": "ok", "uptime": 86400, "memory": { "rss": "124MB", "heap": "67/128MB" }, "node": "v20.18.0", "pid": 12345 } ``` Uptime считается от момента запуска процесса Node.js (не PM2). ### Логирование **stdout/stderr** (`lib/logger.js`): - Формат: `[ISO timestamp] [LEVEL] message {meta JSON}` - Уровни: `error`, `warn`, `info`, `debug` - Уровень контролируется через `LOG_LEVEL` в `.env` - PM2 перенаправляет в файлы логов **compress.log** — отдельный файл для операций сжатия изображений и парсинга: - Формат: `[ISO timestamp] [RU] 1.2.3.4 compress: file.jpg 800x600 format=webp q=80 (234KB)` - Включает страну из GeoIP, IP, имя файла, параметры, размер --- ## 12. Известные ограничения ### FFmpeg: нет libx264 На сервере (Raspberry Pi 5, Debian Bookworm) FFmpeg скомпилирован без `libx264` (требует отдельной лицензии). Видео конвертируется через `mpeg4` software encoder (`-c:v mpeg4 -q:v 5`). Это означает: - Выходной mp4 технически корректен, но использует MPEG-4 Part 2 вместо H.264 - Совместимость чуть ниже, чем у H.264 - WebM-конвертация реализована как fallback в AVI-контейнере (`-f avi`), а не настоящий VP8/VP9 ### AdminJS: медленная загрузка ESM-импорт AdminJS при старте занимает ~6 секунд. В течение этого времени `/admin` возвращает 404 (маршрут ещё не зарегистрирован). После рестарта PM2 нужно подождать перед открытием панели. ### In-memory сессии Сессии хранятся в памяти Node.js процесса. При `pm2 restart` все пользователи разлогиниваются. Аналогично теряются: история HTTP-клиента, задачи видеоконвертера (`jobs Map`), загруженные PDF-файлы (`pdfFiles Map`). ### Память (swap) На сервере swap загружен на ~100%. llama.cpp и open-webui занимают ~1.5 GB RAM. При высокой нагрузке на видеоконвертер (FFmpeg) возможна конкуренция за память. PM2 перезапустит процесс при превышении 256 MB RSS. ### CSP отключён `contentSecurityPolicy: false` в helmet. Причина: Tailwind CSS подключается через CDN и требует inline-скриптов. Это снижает защиту от XSS. Решение — собрать Tailwind локально и убрать зависимость от CDN. ### Производительность конвертации Sharp работает нативно и быстро. FFmpeg и Ghostscript — CPU-интенсивные операции. На Raspberry Pi 5 конвертация длинного видео может занять несколько минут. --- ## 13. Будущие доработки ### OAuth авторизация Добавить вход через Google и Яндекс. Пакет `openid-client` уже установлен в зависимостях, `passport` тоже присутствует — реализация не завершена. ### Persistent sessions Перенести хранение сессий из памяти в MariaDB (пакет `express-mysql-session` или аналог). Позволит пользователям оставаться залогиненными после перезапуска сервера. ### Улучшения инструментов | Инструмент | Улучшение | |-----------|-----------| | Конвертер изображений | Quality slider в UI (сейчас фиксируется в .env) | | Генератор паролей | Режим passphrase (несколько слов через дефис) | | HTTP-клиент | Поддержка переменных окружения (как в Postman) | | Видео конвертер | Установить libx264 и переключить encoder | ### Usage analytics Логирование использования инструментов в отдельную таблицу БД для аналитики популярности. ### CSP настройка Перейти на локальную сборку Tailwind CSS, убрать CDN, включить Content-Security-Policy. ### Persistent job storage Видео задачи (`jobs Map`) и PDF файлы (`pdfFiles Map`) хранить в БД или на диске с восстановлением после рестарта.