From 75d6c186aa8901a397f64bff4e29da8460db59bf Mon Sep 17 00:00:00 2001 From: treamz Date: Sun, 22 Mar 2026 01:19:23 +0300 Subject: [PATCH] Add project documentation (README.md) --- README.md | 652 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 652 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..56039a2 --- /dev/null +++ b/README.md @@ -0,0 +1,652 @@ +# 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 отключена (используется 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` в корне проекта. Пример структуры: + +```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 (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 | Дата последнего изменения |