30 KiB
WA Dev Tools
Веб-платформа с набором инструментов для разработчиков. Включает 15 утилит для работы с изображениями, видео, PDF, кодом и веб-ресурсами — всё в одном защищённом интерфейсе с авторизацией.
Продакшн: wadevelop.ru / images.wadevelop.ru
Содержание
- Быстрый старт
- Архитектура
- Инструменты
- Авторизация
- Админ-панель
- API Reference
- Безопасность
- Конфигурация
- Деплой
- Мониторинг
- Технологии
- База данных
Быстрый старт
Требования
- Node.js 18+
- MariaDB 10.6+
- FFmpeg (для видеоконвертера)
- Ghostscript (для сжатия и конвертации PDF)
Установка
# Клонировать репозиторий
git clone <repo-url>
cd wa-dev-tools
# Установить зависимости
npm install
# Создать файл конфигурации
cp .env.example .env
# Заполнить переменные (см. раздел Конфигурация)
# Создать базу данных
mysql -u root -p < schema.sql
# Запустить в режиме разработки
node server.js
# Или через PM2 в продакшне
pm2 start ecosystem.config.js
Сервер стартует на http://localhost:3000 (порт задаётся через PORT в .env).
Архитектура
Приложение построено на Express 5 с модульной маршрутизацией. Каждый инструмент изолирован в отдельном файле роутера. Общая библиотека (lib/) предоставляет auth middleware, подключение к БД, логгер и SSRF-защиту.
wa-dev-tools/
├── server.js # Точка входа: Express, сессии, middleware, AdminJS
├── ecosystem.config.js # PM2-конфигурация
├── lib/
│ ├── auth.js # Middleware авторизации (список публичных путей)
│ ├── admin.js # AdminJS: Sequelize-модели, маунт /admin
│ ├── db.js # Пул соединений mysql2, CRUD-функции для users
│ ├── logger.js # Структурированный логгер (stdout + файл)
│ └── ssrf.js # Валидация URL, блокировка приватных IP и диапазонов
├── routes/
│ ├── api.js # Публичный REST API (settings, tools, content)
│ ├── auth.js # Регистрация, вход, выход, /auth/me
│ ├── compress.js # Сжатие изображений (Sharp + Archiver)
│ ├── placeholder.js # Генератор placeholder-изображений
│ ├── svgeditor.js # SVG-оптимизация и AI-генерация иконок
│ ├── video.js # Конвертер видео (FFmpeg, асинхронные задачи)
│ ├── pdf.js # PDF-инструменты (pdf-lib, Ghostscript)
│ ├── parser.js # Парсер статей (Readability, JSDOM, кеш)
│ ├── httpclient.js # HTTP-клиент (прокси-запросы, история сессии)
│ ├── redirects.js # Анализатор цепочек редиректов
│ ├── pages.js # HTML-страницы инструментов (formatter, md и др.)
│ ├── status.js # Страница статуса сервисов
│ └── logs.js # Просмотр логов (защищённый)
└── public/
├── shared.js # Сайдбар, темы, список инструментов (клиент)
├── shared.css # Базовые стили
├── landing.html # Публичная лендинг-страница
├── login.html # Страница входа
├── register.html # Страница регистрации
└── *.html # Страницы инструментов
Порядок middleware в server.js
helmet (security headers)
↓
express-session
↓
express.static (public/) — всегда публично
↓
/auth/* — публично (вход/регистрация)
↓
/api/settings, /api/tools, /api/content/* — публично (лендинг)
↓
/status — публично
↓
authMiddleware — всё ниже требует сессии
↓
/api/*, /compress/*, /video/*, /pdf/*, /parser, ...
Инструменты
Изображения
| Инструмент | Путь | Описание |
|---|---|---|
| Конвертер изображений | /compress |
Пакетное сжатие и конвертация до 50 файлов. Форматы: JPEG, PNG, WebP, AVIF, TIFF, GIF. Настройка качества и максимальной стороны. Результат — ZIP-архив. |
| Placeholder | /placeholder |
Генератор заглушек для макетов. API-эндпоинт /placeholder/:size/:bg/:fg/:text возвращает PNG/JPEG/WebP без авторизации. |
| SVG-редактор | /svgeditor |
Оптимизация SVG (удаление комментариев, metadata, пустых групп, обрезка чисел). Генерация простых иконок по ключевому слову (20 встроенных шаблонов). |
| Фоторедактор | /editor |
Клиентский редактор изображений. |
| Видеоконвертер | /video |
Конвертация, сжатие, извлечение аудио и создание GIF через FFmpeg. Асинхронная обработка с поллингом прогресса. |
Код
| Инструмент | Путь | Описание |
|---|---|---|
| Code Formatter | /formatter |
Форматирование кода на стороне клиента. |
| HTML Sanitizer | /sanitizer |
Очистка HTML от потенциально опасных тегов и атрибутов. |
| Конвертер форматов | /converter |
Конвертация между форматами данных (JSON, YAML, TOML и др.). |
Веб
| Инструмент | Путь | Описание |
|---|---|---|
| Парсер статей | /parser |
Извлечение текста и метаданных из URL через Mozilla Readability. Поддержка кодировок (iconv-lite), кеш 50 записей / 10 мин, автоматическая смена кодировки. |
| HTTP-клиент | /httpclient |
Прокси-запросы к внешним URL: GET/POST/PUT/DELETE, произвольные заголовки, тело запроса, таймаут до 60 сек. История запросов в сессии (до 30 записей). |
| Redirect-анализатор | /redirects |
Трассировка цепочки редиректов до 15 шагов. Три User-Agent профиля: desktop, mobile, Googlebot. Детектирует длинные цепочки, смешанный протокол HTTP→HTTPS, временные 302-редиректы. |
Утилиты
| Инструмент | Путь | Описание |
|---|---|---|
| Генератор паролей | /password |
Клиентская генерация паролей с настройкой сложности. |
| Markdown Viewer | /md |
Рендер Markdown в HTML. |
| PDF-инструменты | /pdf |
Merge, Split, Rotate, Delete pages, Reorder, Watermark, Page numbers, Compress (Ghostscript), Protect (пароль), Extract text, PDF→Images, Images→PDF. |
Авторизация
Используется локальная авторизация на основе email + пароль.
Технический стек: bcrypt (10 rounds), express-session, MariaDB.
Процесс входа
- Пользователь отправляет
POST /auth/loginсemailиpassword. - Пароль сравнивается с
password_hashв таблицеusersчерезbcrypt.compare. - При успехе сессия перегенерируется (
req.session.regenerate) для защиты от session fixation. - В сессии сохраняется объект
{ id, email, name, role }.
Процесс регистрации
POST /auth/register
Body: { email, password, password2, name }
Валидация: формат email, минимум 6 символов пароля, совпадение паролей, уникальность email. Имя пользователя очищается от HTML-тегов.
Сессия
// Конфигурация сессии (server.js)
{
secret: SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 86400000, // 24 часа
httpOnly: true,
secure: true, // только в production
sameSite: 'lax',
}
}
Роли
| Роль | Доступ |
|---|---|
user |
Все инструменты платформы |
admin |
Все инструменты + ссылка на /admin в сайдбаре + доступ к AdminJS |
Заблокированные аккаунты (is_blocked = true) получают 403 при попытке входа.
Публичные пути (без авторизации)
/— лендинг/auth/*— вход, регистрация, выход/status— страница статуса/placeholder,/placeholder-img/*— API заглушек/api/settings,/api/tools,/api/content/advantages,/api/content/dashboard— данные для лендинга/health— health check
Админ-панель
Доступна по адресу /admin. Требует роль admin в основной сессии сайта — отдельная авторизация AdminJS не используется.
Построена на AdminJS 7 + @adminjs/sequelize. Модели подключены через Sequelize (параллельно с основным пулом mysql2).
Управляемые сущности
| Раздел | Модель | Таблица | Что можно делать |
|---|---|---|---|
| Пользователи | User |
users |
Просмотр, создание, редактирование, блокировка, смена роли и пароля |
| Контент | Category |
categories |
CRUD категорий инструментов |
| Контент | Tool |
tools |
CRUD инструментов: включение/отключение, порядок сортировки, иконки |
| Контент | ContentBlock |
content_blocks |
Редактирование текстовых блоков лендинга (richtext) |
| Настройки | Setting |
settings |
Редактирование значений настроек приложения |
Смена пароля через AdminJS: пароль хешируется bcrypt'ом в before-хуке действия edit / new. Поле password_hash скрыто из интерфейса.
API Reference
Публичные эндпоинты
| Метод | Путь | Описание |
|---|---|---|
GET |
/api/settings |
Все настройки как объект { key: value } |
GET |
/api/tools |
Категории с вложенными активными инструментами |
GET |
/api/content/advantages |
Блоки контента секции advantages |
GET |
/api/content/dashboard |
Блоки контента секции dashboard |
GET |
/placeholder/:size/:bg/:fg |
Изображение-заглушка |
GET |
/placeholder/:size/:bg/:fg/:text |
Изображение-заглушка с текстом |
GET |
/status/api |
Статус сервисов |
GET |
/health |
Uptime, память, версия Node |
Пример запроса к placeholder API:
GET /placeholder/800x600/0054e6/ffffff/Hello
Параметры: size = WxH, bg и fg = hex-цвет без #, text = текст на изображении. Формат: ?format=png|jpg|webp.
Защищённые эндпоинты (требуют сессию)
Авторизация:
| Метод | Путь | Описание |
|---|---|---|
POST |
/auth/login |
Вход. Body: { email, password } |
POST |
/auth/register |
Регистрация. Body: { email, password, password2, name } |
GET |
/auth/logout |
Выход, уничтожение сессии |
GET |
/auth/me |
Текущий пользователь { id, email, name, role } |
Изображения:
| Метод | Путь | Описание |
|---|---|---|
POST |
/compress |
Загрузка файлов (multipart/form-data, поле images[]). Параметры: format, quality, resize. Ответ: { downloadUrl, stats[] } |
GET |
/download/:filename |
Скачать ZIP с результатом |
Видео:
| Метод | Путь | Описание |
|---|---|---|
POST |
/video/upload |
Загрузка видео (до 200MB). Ответ: { jobId, info } |
POST |
/video/convert |
Запуск обработки. Body: { jobId, mode, format, quality, startTime, endTime } |
GET |
/video/progress/:jobId |
Прогресс задачи. Ответ: { status, progress, downloadUrl?, savings? } |
GET |
/video/download/:filename |
Скачать результат |
Режимы конвертации (mode): convert, compress, audio, gif.
PDF:
| Метод | Путь | Описание |
|---|---|---|
POST |
/pdf/upload |
Загрузка PDF/изображений (до 50MB, до 10 файлов). Ответ: { files: [{ id, name, size, pages }] } |
GET |
/pdf/info/:fileId |
Метаданные PDF (страницы, автор, создатель) |
POST |
/pdf/merge |
Объединение. Body: { fileIds: [] } |
POST |
/pdf/split |
Разбивка страниц. Body: { fileId, ranges: "1-3,5" } |
POST |
/pdf/rotate |
Поворот. Body: { fileId, pages: "all" или "1,3", angle: 90 } |
POST |
/pdf/delete |
Удаление страниц. Body: { fileId, pages: "2,4" } |
POST |
/pdf/reorder |
Переупорядочивание. Body: { fileId, order: [3,1,2] } |
POST |
/pdf/watermark |
Водяной знак. Body: { fileId, text, fontSize, opacity, color } |
POST |
/pdf/pagenumbers |
Нумерация страниц. Body: { fileId, position, startFrom } |
POST |
/pdf/compress |
Сжатие через Ghostscript. Body: `{ fileId, quality: "screen" |
POST |
/pdf/protect |
Защита паролем. Body: { fileId, password } |
POST |
/pdf/extract-text |
Извлечение текста. Body: { fileId } |
POST |
/pdf/toImages |
PDF → ZIP с PNG/JPEG. Body: { fileId, format, dpi } |
POST |
/pdf/fromImages |
Изображения → PDF (multipart/form-data, поле images[]) |
GET |
/pdf/download/:filename |
Скачать результат |
Парсер статей:
| Метод | Путь | Описание |
|---|---|---|
GET |
/parse?url= |
Полный разбор статьи: заголовок, автор, текст, HTML, изображения, ссылки |
GET |
/metadata?url= |
Только метаданные (OG, description, дата) |
GET |
/text?url= |
Только текст (text/plain) |
GET |
/preview?url= |
Карточка превью: заголовок, описание, изображение |
HTTP-клиент:
| Метод | Путь | Описание |
|---|---|---|
POST |
/api/proxy |
Прокси-запрос. Body: { url, method, headers, body, timeout } |
GET |
/api/history |
История запросов текущей сессии |
POST |
/api/history/add |
Добавить запись в историю |
DELETE |
/api/history |
Очистить историю |
Redirect-анализатор:
| Метод | Путь | Описание |
|---|---|---|
POST |
/api/redirect-analyze |
Трассировка. Body: `{ url, userAgent: "desktop" |
SVG-редактор:
| Метод | Путь | Описание |
|---|---|---|
POST |
/api/svg-optimize |
Оптимизация SVG. Body: { svg: "..." }. Ответ: { svg, saved: % } |
POST |
/api/svg-ai |
Генерация иконки. Body: `{ keyword, style: "outline" |
Безопасность
HTTP-заголовки
Helmet добавляет стандартные security headers. Политика CSP отключена (используется Tailwind CDN с inline-скриптами).
Rate Limiting (express-rate-limit)
| Эндпоинт | Лимит |
|---|---|
/auth/login, /auth/register |
10 запросов / 15 мин |
/compress |
Настраивается через RATE_LIMIT_MAX / RATE_LIMIT_WINDOW_MS (по умолчанию 30 / 60 сек) |
/parse, /metadata, /text, /preview |
20 запросов / 60 сек |
/api/proxy |
Настраивается через PROXY_RATE_LIMIT_MAX (по умолчанию 60 / 60 сек) |
/api/redirect-analyze |
20 запросов / 60 сек |
/video/upload |
10 запросов / 60 сек |
/pdf/* |
30 запросов / 60 сек |
SSRF-защита
Все эндпоинты, выполняющие server-side запросы к внешним URL (парсер, HTTP-клиент, redirect-анализатор), проходят валидацию через lib/ssrf.js:
- Разрешены только протоколы
http:иhttps: - Блокируются прямые обращения к приватным IP-диапазонам:
10.x,172.16-31.x,192.168.x,127.x,169.254.x, IPv6 loopback и link-local - Блокируются hostname'ы:
localhost,*.local,*.internal - DNS-резолвинг: все A-записи домена проверяются на принадлежность к приватным диапазонам
Прочее
- Сессионные cookie:
httpOnly,secure(в production),sameSite: lax - Session fixation: при входе сессия перегенерируется через
req.session.regenerate - Загружаемые файлы изображений валидируются по MIME-типу и расширению
- Временные файлы автоматически удаляются: загрузки через 10 мин, результаты через 30 мин
- Имена файлов транслитерируются и очищаются от спецсимволов перед записью в архив
Конфигурация
Файл .env в корне проекта. Пример структуры:
# Сервер
NODE_ENV=production
PORT=3000
# База данных (MariaDB)
DB_HOST=localhost
DB_USER=wa_tools
DB_PASSWORD=секрет
DB_NAME=wa_tools
# Сессия
SESSION_SECRET=длинная-случайная-строка
# Логирование
LOG_LEVEL=info # error | warn | info | debug
# Загрузка файлов (изображения)
MAX_FILE_SIZE_MB=20 # максимальный размер одного файла
MAX_FILES=50 # максимальное количество файлов в одном запросе
# Сжатие изображений
COMPRESS_QUALITY=80 # качество по умолчанию (1-100)
# Rate limiting (изображения)
RATE_LIMIT_WINDOW_MS=60000
RATE_LIMIT_MAX=30
# Rate limiting (HTTP-клиент)
PROXY_RATE_LIMIT_MAX=60
| Переменная | Описание | По умолчанию |
|---|---|---|
NODE_ENV |
Режим работы. В production cookie становятся secure |
development |
PORT |
Порт Express-сервера | 3000 |
DB_HOST |
Хост MariaDB | localhost |
DB_USER |
Пользователь БД | wa_tools |
DB_PASSWORD |
Пароль БД | — |
DB_NAME |
Имя базы данных | wa_tools |
SESSION_SECRET |
Секрет для подписи сессионных cookie | change-me-in-env |
LOG_LEVEL |
Уровень логирования | info |
MAX_FILE_SIZE_MB |
Лимит размера изображения | 20 |
MAX_FILES |
Лимит файлов в пакетной загрузке | 50 |
COMPRESS_QUALITY |
Качество сжатия по умолчанию | 80 |
RATE_LIMIT_WINDOW_MS |
Окно rate limiting (мс) | 60000 |
RATE_LIMIT_MAX |
Лимит запросов в окне | 30 |
PROXY_RATE_LIMIT_MAX |
Лимит для HTTP-клиента | 60 |
Важно: Никогда не коммитьте
.envв репозиторий. ЗначениеSESSION_SECRETдолжно быть случайной строкой длиной не менее 32 символов.
Деплой
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,
env: { NODE_ENV: 'production' }
}
# Запуск
pm2 start ecosystem.config.js
# Перезапуск после обновления
pm2 restart images
# Логи
pm2 logs images
Graceful shutdown
Сервер перехватывает SIGTERM и SIGINT: закрывает HTTP-сервер, ждёт завершения активных соединений до 10 секунд, затем завершается принудительно.
Nginx
Пример конфигурации виртуального хоста:
server {
listen 443 ssl;
server_name images.wadevelop.ru;
ssl_certificate /etc/letsencrypt/live/images.wadevelop.ru/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/images.wadevelop.ru/privkey.pem;
client_max_body_size 210M; # чуть больше MAX видео (200MB)
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 120s;
}
}
В server.js установлено app.set('trust proxy', 1) — необходимо для корректной работы rate limiting и логирования IP за nginx.
SSL
certbot --nginx -d images.wadevelop.ru
Мониторинг
Страница статуса
URL: /status — публичная HTML-страница в стиле status.claude.com.
API: GET /status/api — возвращает статус пяти сервисов:
{
"overall": "operational",
"uptime": 86400,
"services": [
{ "name": "API", "status": "operational" },
{ "name": "Database", "status": "operational" },
{ "name": "Image Processing", "status": "operational" },
{ "name": "Video Processing", "status": "operational" },
{ "name": "Authentication", "status": "operational" }
],
"timestamp": "2026-03-22T10:00:00.000Z"
}
Статус Database проверяется реальным запросом SELECT 1 к MariaDB.
Health Endpoint
URL: GET /health — расширенная информация о процессе (не требует авторизации):
{
"status": "ok",
"uptime": 3600,
"memory": {
"rss": "128MB",
"heap": "64/128MB"
},
"node": "v22.0.0",
"pid": 12345
}
Логи
Структурированный вывод в stdout (формат: [ISO timestamp] [LEVEL] message {meta}). PM2 пишет stdout в /home/treamz/.pm2/logs/images-out.log, stderr — в images-error.log.
Операции сжатия изображений дополнительно пишутся в файл compress.log в корне проекта (через log.logToFile).
Технологии
| Компонент | Технология | Назначение |
|---|---|---|
| Runtime | Node.js 18+ | Серверная среда выполнения |
| Framework | Express 5 | HTTP-сервер, маршрутизация |
| База данных | MariaDB 10.6+ | Хранение пользователей, настроек, контента |
| ORM | Sequelize 6 | Модели для AdminJS |
| DB Driver | mysql2 | Прямые SQL-запросы (пул соединений) |
| Авторизация | bcrypt + express-session | Хеширование паролей, серверные сессии |
| Админ-панель | AdminJS 7 + @adminjs/sequelize | Веб-интерфейс управления данными |
| Изображения | Sharp | Конвертация, сжатие, генерация placeholder |
| Видео | FFmpeg + ffprobe | Конвертация, сжатие, извлечение аудио, GIF |
| pdf-lib | Merge, split, rotate, watermark, protect | |
| pdf-parse | Извлечение текста и метаданных | |
| Ghostscript | Сжатие, конвертация PDF в изображения | |
| Парсер | @mozilla/readability + JSDOM | Извлечение контента статей |
| Кодировки | iconv-lite | Декодирование страниц в non-UTF-8 |
| Загрузка файлов | Multer | Обработка multipart/form-data |
| Архивация | Archiver | ZIP-архивы с результатами |
| Геолокация | geoip-lite | Определение страны по IP в логах |
| Безопасность | Helmet | HTTP security headers |
| Rate Limiting | express-rate-limit | Ограничение частоты запросов |
| Шрифты SVG | opentype.js | Работа с шрифтами (вспомогательная) |
| UI | Tailwind CSS (CDN) | Стилизация интерфейса |
| Шрифты UI | JetBrains Mono, Manrope | Типографика |
| Процесс | PM2 | Управление процессом в production |
| Веб-сервер | Nginx | Reverse proxy, SSL termination |
| SSL | Let's Encrypt (certbot) | TLS-сертификаты |
База данных
Схема таблиц
users — учётные записи пользователей
| Поле | Тип | Описание |
|---|---|---|
id |
INT PK AI | Идентификатор |
email |
VARCHAR(255) UNIQUE | Email-адрес |
password_hash |
VARCHAR(255) | bcrypt-хеш пароля |
display_name |
VARCHAR(100) | Отображаемое имя |
role |
ENUM('user','admin') | Роль, по умолчанию user |
is_blocked |
BOOLEAN | Блокировка аккаунта |
created_at |
DATETIME | Дата регистрации |
last_login |
DATETIME | Дата последнего входа |
settings — настройки приложения (key-value)
| Поле | Тип | Описание |
|---|---|---|
id |
INT PK AI | Идентификатор |
setting_key |
VARCHAR(100) UNIQUE | Ключ настройки |
setting_value |
TEXT | Значение |
description |
VARCHAR(255) | Описание для AdminJS |
updated_at |
DATETIME | Дата обновления |
categories — категории инструментов
| Поле | Тип | Описание |
|---|---|---|
id |
INT PK AI | Идентификатор |
slug |
VARCHAR(50) UNIQUE | URL-идентификатор |
title |
VARCHAR(100) | Название |
description |
VARCHAR(255) | Описание |
icon_svg |
TEXT | SVG-иконка категории |
color |
VARCHAR(20) | Акцентный цвет (hex) |
sort_order |
INT | Порядок отображения |
tools — инструменты платформы
| Поле | Тип | Описание |
|---|---|---|
id |
INT PK AI | Идентификатор |
slug |
VARCHAR(50) UNIQUE | URL-идентификатор |
title |
VARCHAR(100) | Название |
description |
VARCHAR(255) | Описание |
path |
VARCHAR(100) | URL-путь инструмента |
icon_svg |
TEXT | SVG-иконка |
category_id |
INT FK | Ссылка на categories.id |
sort_order |
INT | Порядок в категории |
is_enabled |
BOOLEAN | Включён/отключён |
content_blocks — текстовые блоки лендинга
| Поле | Тип | Описание |
|---|---|---|
id |
INT PK AI | Идентификатор |
block_key |
VARCHAR(100) UNIQUE | Уникальный ключ блока |
title |
VARCHAR(255) | Заголовок |
body |
TEXT | HTML-содержимое (richtext) |
section |
VARCHAR(50) | Секция лендинга (advantages, dashboard и др.) |
sort_order |
INT | Порядок в секции |
is_visible |
BOOLEAN | Отображать/скрыть |
updated_at |
DATETIME | Дата последнего изменения |