61 KiB
WA Dev Tools — Полная документация проекта
Версия документации: март 2026 Репозиторий:
git.wadevelop.ru/treamz/wa-dev-toolsПродакшн:https://wadevelop.ru
Содержание
- Обзор проекта
- Архитектура
- Инструменты
- Авторизация и роли
- AdminJS панель
- База данных
- API Reference
- Фронтенд архитектура
- Безопасность
- Деплой и инфраструктура
- Мониторинг
- Известные ограничения
- Будущие доработки
1. Обзор проекта
WA Dev Tools — веб-платформа с набором утилит для разработчиков. Доступна по адресу https://wadevelop.ru. Целевая аудитория: веб-разработчики, которым нужны быстрые инструменты для работы с изображениями, видео, PDF, кодом и HTTP-запросами без установки десктопных приложений.
Технологии
| Слой | Технология |
|---|---|
| Среда выполнения | Node.js 20 |
| Веб-фреймворк | Express 5 |
| База данных | MariaDB 10.11 |
| Хранение сессий | express-mysql-session (MariaDB) |
| Очередь задач | better-sqlite3 (SQLite WAL, /mnt/webdata/storage/jobs.db) |
| Обработка изображений | Sharp 0.34 |
| Обработка видео | FFmpeg 5.1.8 (libx264, libvpx, libmp3lame), ffprobe |
| Обработка PDF | pdf-lib, pdf-parse, Ghostscript (системный) |
| Парсинг HTML | @mozilla/readability, jsdom |
| Авторизация | express-session, bcrypt |
| Панель администратора | AdminJS 7 + @adminjs/sequelize |
| Стили | Tailwind CSS 3 (CLI build, vendor/tailwind.min.css) |
| WebSocket | ws (реалтайм прогресс конвертации) |
| Логирование | pino (структурированный JSON) |
| Процесс-менеджер | 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 # Логгер pino: структурированный JSON + logToFile
│ ├── queue.js # Очередь задач на SQLite (better-sqlite3, WAL mode)
│ ├── session.js # Persistent sessions (express-mysql-session → MariaDB)
│ ├── storage.js # Единые пути хранения: /mnt/webdata/storage/{uploads,results}
│ ├── ws.js # WebSocket сервер (ws) — реалтайм прогресс конвертации
│ └── 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,/placeholderPUBLIC_API:/api/settings,/api/tools,/api/content/advantages,/api/content/dashboardPUBLIC_FILES:/shared.css,/shared.js,/landing.htmlPUBLIC_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:
const { setupAdmin } = require('./lib/admin');
await setupAdmin(app);
Функция setupAdmin в lib/admin.js:
- Создаёт отдельный экземпляр Sequelize (параллельно с пулом из
lib/db.js) - Определяет Sequelize-модели для всех таблиц
- Загружает
adminjs,@adminjs/express,@adminjs/sequelizeчерезawait import() - Регистрирует адаптер, создаёт экземпляр AdminJS с ресурсами
- Монтирует 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,gifquality— 1–100 (по умолчанию изCOMPRESS_QUALITYв .env, обычно 80)resize— максимальный размер по длинной стороне в пикселях (0 = без ресайза)
Зависимости: sharp, archiver, multer, geoip-lite
Принцип работы:
- Multer сохраняет файлы во временный
uploads/ - Sharp обрабатывает каждый файл (конвертация + опциональный ресайз)
- Буферы добавляются в ZIP через archiver
- ZIP сохраняется в
downloads/, клиенту возвращается URL - Загруженные файлы удаляются немедленно, ZIP — через 30 минут
- Кириллические имена файлов транслитерируются
Лимиты:
- Размер файла:
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-преобразованиями:
- Удаление
<?xml>, комментариев,<metadata>,<title>,<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:
{
"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:
{
"url": "https://example.com/api",
"method": "POST",
"headers": {"Authorization": "Bearer ..."},
"body": "{\"key\": \"value\"}",
"timeout": 30000
}
Ответ /api/proxy:
{
"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) | Анализ цепочки редиректов |
Параметры:
{
"url": "https://example.com",
"userAgent": "desktop|mobile|googlebot",
"method": "GET|HEAD"
}
Ответ:
{
"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 регистрации
GET /auth/register→ формаregister.htmlPOST /auth/register— валидация email (regexp), пароль ≥ 6 символов, проверка на дубликат- Хеширование пароля
bcrypt.hash(password, 10) - Запись в
users: email (lowercase), password_hash, display_name - Создание сессии
req.session.user = { id, email, name, role: 'user' } - Редирект на
/dashboard
display_name санируется: HTML-теги удаляются, длина ограничена 100 символами.
Flow логина
GET /auth/login→ формаlogin.htmlPOST /auth/login— поиск пользователя по email, проверкаis_blocked,bcrypt.comparedb.updateLastLogin(id)— обновление поляlast_login- Session regeneration (
req.session.regenerate) для защиты от session fixation - Новая сессия с данными пользователя
- Редирект на
returnToили/dashboard
Структура сессии
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/
Конфигурация сессии
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
Rate limit работает per-user: keyGenerator использует req.session.user.id для авторизованных пользователей, req.ip для анонимных. Это предотвращает ситуацию, когда один бот исчерпывает лимит для всех.
15 минут, 10 попыток (применяется к POST /auth/login и POST /auth/register).
5. AdminJS панель
URL: https://wadevelop.ru/admin
Доступ
Доступ контролируется двойной проверкой:
- Основная сессия Express (
req.session.userдолжен существовать) - Роль администратора (
req.session.user.role === 'admin')
Если пользователь не авторизован или не является admin — редирект на /auth/login с returnTo=/admin. Встроенный логин AdminJS не используется.
Управляемые сущности
| Сущность | Навигация | Описание |
|---|---|---|
| Users | Пользователи | Управление аккаунтами, блокировка, смена роли |
| Categories | Контент | Категории инструментов с иконками и цветами |
| Tools | Контент | Список инструментов, включение/отключение |
| ContentBlocks | Контент | Текстовые блоки лендинга (преимущества, контент дашборда) |
| Settings | Настройки | Ключ-значение настройки сайта |
Смена пароля пользователя
В форме редактирования User есть два виртуальных поля: password и password_confirm. Они не хранятся в БД — вместо этого action-hook before обрабатывает их:
- Если
passwordпустой — пропуск (пароль не меняется) - Если
password !== password_confirm— ошибка валидации - Если длина < 6 символов — ошибка
- Иначе:
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 |
Скачивание результата |
| Метод | Путь | Описание |
|---|---|---|
| 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" |
| 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" |
| POST | /pdf/protect |
{fileId, password} → {downloadUrl, size} |
| POST | /pdf/extract-text |
{fileId} → {text, pages, metadata} |
| POST | /pdf/toImages |
`{fileId, format: "png" |
| 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 |
Страница инструмента (шаблон)
<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)
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'", "https://mc.yandex.ru"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https://mc.yandex.ru"],
connectSrc: ["'self'", "wss:", "ws:"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
frameAncestors: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
},
},
crossOriginEmbedderPolicy: false,
})
CSP включён. unsafe-inline необходим для inline <script> и <style> блоков UI. Остальные политики helmet включены: HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy.
Защита от SSRF (lib/ssrf.js)
Применяется в HTTP-клиенте, парсере и redirect-анализаторе. Два уровня проверки:
- Синтаксическая — только
http:иhttps:, блокировкаlocalhost,*.local,*.internal - Диапазоны IP (regexp):
10.0.0.0/8,172.16.0.0/12,192.168.0.0/16— приватные RFC 1918127.0.0.0/8— loopback169.254.0.0/16— link-local::1,fc*/fd*,fe80*— IPv6 приватные
- DNS-резолвинг — hostname разрешается через
dns.resolve4, все IP проверяются на попадание в приватные диапазоны
Загрузка файлов
- MIME-тип валидируется через whitelist (multer
fileFilter) - Проверяется также расширение файла (regexp) как запасной вариант
- Максимальный размер задаётся через переменные окружения
- Временные файлы удаляются немедленно после обработки
Path traversal
Имена файлов при скачивании обрабатываются через path.basename(req.params.filename) — попытки выйти из директории (../) отсекаются.
Пароли
- bcrypt с
saltRounds: 10 - Минимальная длина: 6 символов
- Оригинальный пароль никогда не логируется
Сессия
- Хранение: MariaDB через
express-mysql-session(таблицаsessions) - Сессии переживают рестарт PM2 — пользователи остаются залогиненными
httpOnly: true— недоступно JavaScriptsecure: trueв production — только по HTTPSsameSite: 'lax'— защита от CSRF- Session regeneration при логине — защита от session fixation attack
- Logout:
req.session.destroy() - Автоочистка:
checkExpirationInterval: 900000(15 мин),expiration: 86400000(24ч)
Санитизация ввода
display_name— HTML-теги удаляются regexp'ом, длина обрезается до 100- Email — нормализуется в lowercase, проверяется regexp
AdminJS
Двойная проверка: наличие сессии + role === 'admin'. Статические ресурсы AdminJS (.js, .css, .woff и т.д.) пропускаются без auth-проверки по расширению.
10. Деплой и инфраструктура
PM2
Конфигурация в ecosystem.config.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
Команды управления:
pm2 start ecosystem.config.js # первый запуск
pm2 restart images # перезапуск
pm2 reload images # graceful reload (0-downtime)
pm2 logs images # просмотр логов
pm2 monit # мониторинг в реальном времени
Nginx
wadevelop.ru — основной сайт:
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
Ответ:
{
"overall": "operational",
"uptime": 86400,
"services": [
{"name": "API", "status": "operational"},
{"name": "Database", "status": "operational"}
],
"timestamp": "2026-03-22T10:00:00.000Z"
}
Health check /health
{
"status": "ok",
"uptime": 86400,
"memory": {
"rss": "302MB",
"heap": "86/91MB",
"system": "37%"
},
"cpu": {
"load1m": "0.00",
"load5m": "0.00",
"load15m": "0.05",
"cores": 4
},
"queue": {
"stats": { "done": 5, "processing": 1 },
"active": 1,
"pending": 0
},
"ws": {
"connections": 2
},
"node": "v20.19.5",
"pid": 12345
}
Расширенный health: системная память, CPU load, глубина очереди задач, активные WebSocket подключения.
Логирование
pino (lib/logger.js):
- Формат: структурированный JSON (
{"level":30,"time":...,"msg":"..."}) - Уровни:
error,warn,info,debug(контролируется черезLOG_LEVEL) - API совместим:
log.info(msg, meta),log.error(msg, meta),log.logToFile(text) - PM2 перенаправляет stdout/stderr в файлы логов
logToFile — append-запись в файл для операций сжатия/парсинга:
- Формат:
[ISO timestamp] [RU] 1.2.3.4 compress: file.jpg 800x600 format=webp q=80 (234KB) - Включает страну из GeoIP, IP, имя файла, параметры, размер
12. Известные ограничения
AdminJS: медленная загрузка
ESM-импорт AdminJS при старте занимает ~6 секунд. В течение этого времени /admin возвращает 404. После рестарта PM2 нужно подождать перед открытием панели.
Память (swap)
Swap загружен на ~100% из-за LLM-кластера (llama.cpp + open-webui ~3.8 GB). PM2 перезапустит процесс при превышении 256 MB RSS.
PM2 cluster mode
Несовместим с текущей архитектурой AdminJS (ESM dynamic import + in-memory state). Работает только в fork mode (instances: 1).
Производительность конвертации
Sharp — нативный, быстрый. FFmpeg и Ghostscript — CPU-интенсивные. На Raspberry Pi 5 длинное видео может конвертироваться несколько минут.
PDF-файлы
Загруженные PDF хранятся в in-memory Map с таймаутом 30 мин. При рестарте теряются (в отличие от видео-задач, которые уже в SQLite-очереди).
13. Будущие доработки
OAuth авторизация
Добавить вход через Google и Яндекс. Пакет openid-client уже установлен в зависимостях, passport тоже присутствует — реализация не завершена.
Улучшения инструментов
| Инструмент | Улучшение |
|---|---|
| Конвертер изображений | Quality slider в UI (сейчас фиксируется в .env) |
| Генератор паролей | Режим passphrase (несколько слов через дефис) |
| HTTP-клиент | Поддержка переменных окружения (как в Postman) |
Usage analytics
Логирование использования инструментов в отдельную таблицу БД для аналитики популярности.
PDF-файлы в очередь
Перенести pdfFiles Map в SQLite-очередь (аналогично видео-задачам) для персистентности.
Монетизация
Тарифы, API-ключи, billing usage, multi-tenant лимиты.