1255 lines
61 KiB
Markdown
1255 lines
61 KiB
Markdown
# 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 |
|
||
| Хранение сессий | 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`, `/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-преобразованиями:
|
||
- Удаление `<?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`:**
|
||
```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
|
||
|
||
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`
|
||
|
||
### Доступ
|
||
|
||
Доступ контролируется двойной проверкой:
|
||
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: {
|
||
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-анализаторе. Два уровня проверки:
|
||
|
||
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 символов
|
||
- Оригинальный пароль никогда не логируется
|
||
|
||
### Сессия
|
||
|
||
- Хранение: MariaDB через `express-mysql-session` (таблица `sessions`)
|
||
- Сессии переживают рестарт PM2 — пользователи остаются залогиненными
|
||
- `httpOnly: true` — недоступно JavaScript
|
||
- `secure: true` в production — только по HTTPS
|
||
- `sameSite: '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`:
|
||
|
||
```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": "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 лимиты.
|