# WA Dev Tools Веб-платформа с набором инструментов для разработчиков. Включает 15 утилит для работы с изображениями, видео, PDF, кодом и веб-ресурсами — всё в одном защищённом интерфейсе с авторизацией. **Продакшн:** [wadevelop.ru](https://wadevelop.ru) / [images.wadevelop.ru](https://images.wadevelop.ru) --- ## Содержание 1. [Быстрый старт](#быстрый-старт) 2. [Архитектура](#архитектура) 3. [Инструменты](#инструменты) 4. [Авторизация](#авторизация) 5. [Админ-панель](#админ-панель) 6. [API Reference](#api-reference) 7. [Безопасность](#безопасность) 8. [Конфигурация](#конфигурация) 9. [Деплой](#деплой) 10. [Мониторинг](#мониторинг) 11. [Технологии](#технологии) 12. [База данных](#база-данных) --- ## Быстрый старт ### Требования - Node.js 18+ - MariaDB 10.6+ - FFmpeg (для видеоконвертера) - Ghostscript (для сжатия и конвертации PDF) ### Установка ```bash # Клонировать репозиторий git clone 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. ### Процесс входа 1. Пользователь отправляет `POST /auth/login` с `email` и `password`. 2. Пароль сравнивается с `password_hash` в таблице `users` через `bcrypt.compare`. 3. При успехе сессия перегенерируется (`req.session.regenerate`) для защиты от session fixation. 4. В сессии сохраняется объект `{ id, email, name, role }`. ### Процесс регистрации ``` POST /auth/register Body: { email, password, password2, name } ``` Валидация: формат email, минимум 6 символов пароля, совпадение паролей, уникальность email. Имя пользователя очищается от HTML-тегов. ### Сессия ```javascript // Конфигурация сессии (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"|"ebook"|"printer" }` | | `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"|"mobile"|"googlebot", method }` | **SVG-редактор:** | Метод | Путь | Описание | |---|---|---| | `POST` | `/api/svg-optimize` | Оптимизация SVG. Body: `{ svg: "..." }`. Ответ: `{ svg, saved: % }` | | `POST` | `/api/svg-ai` | Генерация иконки. Body: `{ keyword, style: "outline"|"filled"|"duotone", size, color }` | --- ## Безопасность ### HTTP-заголовки Helmet добавляет security headers + CSP (default-src self, object-src none, frame-ancestors none). Tailwind собирается локально через CLI. ### 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` в корне проекта. Пример структуры: ```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`: ```javascript { 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' } } ``` ```bash # Запуск pm2 start ecosystem.config.js # Перезапуск после обновления pm2 restart images # Логи pm2 logs images ``` ### Graceful shutdown Сервер перехватывает `SIGTERM` и `SIGINT`: закрывает HTTP-сервер, ждёт завершения активных соединений до 10 секунд, затем завершается принудительно. ### Nginx Пример конфигурации виртуального хоста: ```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 ```bash certbot --nginx -d images.wadevelop.ru ``` --- ## Мониторинг ### Страница статуса **URL:** `/status` — публичная HTML-страница в стиле status.claude.com. **API:** `GET /status/api` — возвращает статус пяти сервисов: ```json { "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` — расширенная информация о процессе (не требует авторизации): ```json { "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 | pdf-lib | Merge, split, rotate, watermark, protect | | PDF | pdf-parse | Извлечение текста и метаданных | | PDF | 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 3 (CLI build) | Стилизация интерфейса | | Шрифты 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 | Дата последнего изменения |