diff --git a/API.md b/API.md index 4e2f91a..c49b3c7 100644 --- a/API.md +++ b/API.md @@ -1,25 +1,87 @@ # API -Endpoints: /api/*. Auth: cookie z51_token. +Все endpoints начинаются с `/api/`. Авторизация через cookie `z51_token` (кроме `/api/auth/*`). -## Auth -| POST | /api/auth/login | {login, password} | -| GET | /api/auth/me | Текущий пользователь | +## Авторизация + +| Метод | URL | Body | Описание | +|-------|-----|------|----------| +| POST | `/api/auth/login` | `{login, password}` | Логин, возвращает user object, ставит cookie | +| POST | `/api/auth/logout` | — | Выход, удаляет cookie | +| GET | `/api/auth/me` | — | Текущий пользователь `{id, login, name, role, projects, theme}` | +| PUT | `/api/auth/preferences` | `{theme}` | Сохранить настройки пользователя | ## Проекты -| GET | /api/projects | Список | -| GET | /api/project/:name | Данные проекта | -| PUT | /api/project/:name/settings | Настройки | + +| Метод | URL | Body | Описание | +|-------|-----|------|----------| +| GET | `/api/projects` | — | Список проектов (фильтруется по user.projects) | +| POST | `/api/projects` | `{name}` | Создать проект | +| GET | `/api/project/:name` | — | Все данные: meta, settings, draft, presets, letters, notes, blockText | +| PUT | `/api/project/:name/block` | `{blockText, sourceName}` | Сохранить block.pug | +| PUT | `/api/project/:name/settings` | `{...settings}` | Сохранить настройки проекта | +| PUT | `/api/project/:name/draft` | `[...blocks]` | Сохранить черновик | +| PUT | `/api/project/:name/presets` | `[...presets]` | Сохранить пресеты | ## Письма -| PUT | /api/project/:name/letter | Сохранить (+createdBy) | -| DELETE | /api/project/:name/letter/:id | Удалить | + +| Метод | URL | Body | Описание | +|-------|-----|------|----------| +| GET | `/api/project/:name/letters` | — | Индекс писем `{list, currentId}` | +| PUT | `/api/project/:name/letters` | `{list, currentId}` | Обновить индекс | +| GET | `/api/project/:name/letter/:id` | — | Данные письма | +| PUT | `/api/project/:name/letter` | `{id, blocks, ...}` | Сохранить (сервер добавляет `createdBy`/`updatedBy`) | +| DELETE | `/api/project/:name/letter/:id` | — | Удалить письмо | +| GET | `/api/project/:name/letter/:id/history` | — | История изменений | +| PUT | `/api/project/:name/letter/:id/history` | `{snapshot}` | Добавить в историю | + +## Заметки + +| Метод | URL | Body | Описание | +|-------|-----|------|----------| +| GET/PUT | `/api/project/:name/notes` | `{list, currentId}` | Индекс заметок | +| GET | `/api/project/:name/note/:id` | — | Данные заметки | +| PUT | `/api/project/:name/note` | `{id, content, ...}` | Сохранить | +| DELETE | `/api/project/:name/note/:id` | — | Удалить | ## Рендер -| POST | /api/project/:name/render-email | Pug→HTML | -## Фиды -| POST | /api/project/:name/auto-assemble | Авто-подбор | +| Метод | URL | Body | Описание | +|-------|-----|------|----------| +| POST | `/api/project/:name/render-email` | `{pugCode, layout}` | Рендер Pug→HTML. Обрабатывает Mindbox/RetailCRM теги для превью | -## Админ -| GET | /api/admin/logs | Аудит-логи | +## Фиды и товары + +| Метод | URL | Body | Описание | +|-------|-----|------|----------| +| POST | `/api/project/:name/feed-refresh` | — | Сбросить кэш фида, вернуть diff | +| POST | `/api/project/:name/feed-lookup` | `{ids}` | Поиск товаров по массиву ID | +| POST | `/api/project/:name/feed-suggest` | `{productId, excludeIds, search}` | Подбор замены (по серии, стране, цене) | +| POST | `/api/project/:name/auto-assemble` | `{type, layout, productType, priceMin, priceMax, excludeMaterials, excludeIds, anchorIds}` | Авто-подбор. Возвращает `{blocks, total}` с `usedDaysAgo` для каждого товара | + +## FTP / Картинки + +| Метод | URL | Body | Описание | +|-------|-----|------|----------| +| POST | `/api/project/:name/ftp/test` | — | Проверить подключение (FTP или локальное) | +| POST | `/api/project/:name/ftp/upload` | `{imageData, fileName, folder}` | Загрузить (base64). Max 30 MB | +| POST | `/api/project/:name/ftp/list` | `{folder}` | Список файлов в папке | + +## Прочее + +| Метод | URL | Описание | +|-------|-----|----------| +| POST | `/api/upload-image` | Локальная загрузка (fallback без FTP) | +| POST | `/api/check-links` | Проверка массива URL на доступность | +| GET/PUT | `/api/config` | Google Sheets конфиг | +| GET | `/api/stats` | Статистика по всем проектам | + +## Админ (role: admin) + +| Метод | URL | Описание | +|-------|-----|----------| +| GET | `/api/admin/users` | Список пользователей | +| POST | `/api/admin/users` | Создать `{login, password, name, role, projects}` | +| PUT | `/api/admin/users/:id` | Обновить | +| DELETE | `/api/admin/users/:id` | Удалить (нельзя удалить себя) | +| GET | `/api/admin/logs?user=&project=&action=&from=&to=&page=&limit=` | Аудит-логи | diff --git a/Architecture.md b/Architecture.md index c92bc20..20b2381 100644 --- a/Architecture.md +++ b/Architecture.md @@ -1,29 +1,117 @@ # Архитектура -## Структура +## Структура проекта + ``` aspekter/ -├── z51-pug-builder/ # Фронтенд + API -│ ├── src/App.svelte # SPA компонент -│ ├── vite.config.js # API middleware -│ └── data/{project}/ # Данные -├── email-gen/ # Pug-шаблоны -├── deploy/email-gen-api/ # Рендер-сервер -├── coin-scout/ # Подбор монет -└── docker-compose*.yml +├── z51-pug-builder/ # Фронтенд + API сервер +│ ├── src/ +│ │ ├── App.svelte # Основной SPA компонент (~7000 строк) +│ │ ├── app.css # Все стили (~3300 строк) +│ │ └── lib/ +│ │ ├── api.js # HTTP-клиент для всех API endpoints +│ │ ├── parsing.js # Парсинг Pug: buildBaseSchema, parseQuotedArgs, etc. +│ │ ├── utils.js # Утилиты: форматирование, ID, экранирование +│ │ └── spellcheck.js # Интеграция Яндекс.Спеллер +│ ├── vite.config.js # API middleware (~2300 строк): CRUD, auth, фиды, FTP, аудит +│ ├── data/ # Данные проектов (Docker volume, persist) +│ │ ├── _system/ +│ │ │ ├── users.json # Пользователи (scrypt хэши) +│ │ │ └── logs/YYYY-MM.jsonl # Аудит-логи +│ │ ├── {project}/ +│ │ │ ├── block.pug # Исходные блоки/шаблоны +│ │ │ ├── settings.json # Настройки: миксины, FTP, цвета, фид, extraFields +│ │ │ ├── draft.json # Текущий черновик +│ │ │ ├── drafts/{uid}.json # Черновики per-user +│ │ │ ├── presets.json # Сохранённые пресеты +│ │ │ ├── letters.json # Индекс писем (общий для всех пользователей) +│ │ │ ├── letters/*.json # Файлы писем + .history.json +│ │ │ └── notes/ # Заметки проекта +│ │ └── images/ # Локальное хранение картинок +│ ├── public/Block.pug # Дефолтный блок для новых проектов +│ └── Dockerfile +├── email-gen/ # Pug-шаблоны и рендерер +│ └── emails/ +│ ├── includes/mixins.pug # Общие миксины (buttonRounded, table, spacer, etc.) +│ └── {project}/ +│ ├── layout/layout.pug # Layout письма (header, footer, meta) +│ ├── css/style.css # CSS проекта (инлайнится через Juice) +│ ├── blocks/ # Блоки проекта +│ └── includes/ # Локальные миксины проекта +├── email-gen-overrides/ # Локальные override файлов email-gen (safe across git pull) +├── deploy/ +│ ├── email-gen-api/ +│ │ ├── server.js # HTTP-сервер рендеринга с валидацией Pug +│ │ └── Dockerfile +│ └── nginx/ +│ ├── prod.conf # app.aspekter.ru +│ └── coins.conf # coins.aspekter.ru +├── coin-scout/ # Отдельный сервис подбора монет +│ ├── server.js +│ └── data/coins.db # SQLite: price_history, coins +├── docker-compose.yml # Базовый compose (сервисы, volumes) +├── docker-compose.dev.yml # Dev: порт 5174, data-dev/, HMR +└── docker-compose.prod.yml # Prod: порт 5175 на 127.0.0.1, volume mounts ``` -## Поток: Pug → HTML -1. Пользователь собирает письмо из блоков -2. POST на email-gen-api с Pug-кодом -3. Валидация → temp-файл → email-templates рендер -4. Juice инлайнит CSS → HTML -5. Превью / копирование +## Поток генерации HTML -## Данные проекта ``` -data/{project}/ -├── block.pug, settings.json -├── draft.json, presets.json -├── letters.json + letters/*.json +Пользователь → собирает письмо из блоков в конструкторе + ↓ + Pug-код собирается из block.content каждого блока + ↓ + POST /api/project/:name/render-email → vite middleware + ↓ + vite пересылает на email-gen-api (порт 8787) + ↓ + email-gen-api: + 1. validatePugSafety() — проверка на инъекции + 2. Создаёт уникальный temp-файл (crypto.randomBytes) + 3. renderWithNode() — запускает email-templates + 4. Juice инлайнит CSS из style.css + 5. Возвращает HTML строку + ↓ + vite middleware: + 1. processMindboxTags() — подставляет данные из фида для превью + 2. Возвращает HTML клиенту + ↓ + Конструктор показывает: + - Превью в iframe (600px) + - HTML-код (копирование) + - Очищенный HTML (без Mindbox-тегов) ``` + +## Модули vite.config.js + +| Модуль | Строки | Описание | +|--------|--------|----------| +| Auth system | 155-310 | Сессии, пароли, cookie, middleware | +| Admin endpoints | 332-450 | CRUD пользователей, аудит-логи | +| Project CRUD | 470-670 | Проекты, письма, заметки, пресеты | +| Image upload | 770-850 | Загрузка картинок (локальная) | +| Render proxy | 850-870 | Пересылка на email-gen-api | +| Link checker | 870-930 | Проверка ссылок в HTML | +| Sheets integration | 930-1000 | Google Sheets | +| Mindbox tags | 1000-1100 | Обработка Mindbox-переменных для превью | +| Feed cache | 1100-1250 | YML-фид: парсинг, кэш 3 часа, поиск | +| Auto-assemble | 1250-1800 | Авто-подбор товаров: фильтры, группировка, scoring | +| FTP/SFTP | 1800-2000 | Загрузка картинок + локальное хранение | +| Project data | 2000-2300 | GET/PUT данных проекта | + +## Решения и компромиссы + +### Парсинг Pug на фронте (parsing.js) +Regex-парсинг вместо AST. Работает для шаблонных блоков с предсказуемой структурой. Полноценный Pug-парсер на фронте — overkill для внутреннего инструмента. Схема полей (schema) строится из content каждого блока + правил `mixinRules` из settings. + +### Один файл App.svelte (7000 строк) +Исторически сложилось. Планируется разбивка на компоненты. Не критично для работоспособности. + +### vite.config.js как бэкенд (2300 строк) +Vite dev server используется как application server в проде. API живёт в middleware. Для внутреннего инструмента с 5 проектами — приемлемо. Следующий шаг при масштабировании — вынос в Express/SvelteKit. + +### processMindboxTags — обработка Mindbox-переменных +НЕ парсер шаблонного языка. Предпросмотрщик: подставляет данные из фида для превью. В финальном HTML теги остаются as-is — Mindbox обрабатывает их при отправке. Если шаблон изменится — сломается только превью, не рассылка. + +### resolveProductProp — маппинг свойств +Маппит Mindbox-пропсы на поля YML-фида. Fallback `product[cfMatch[1]]` подхватывает новые поля автоматически. Ручное добавление нужно только для нетривиального маппинга (sohrannost → condition). diff --git a/Blocks.md b/Blocks.md new file mode 100644 index 0000000..7173e34 --- /dev/null +++ b/Blocks.md @@ -0,0 +1,87 @@ +# Блоки и парсинг + +## Что такое блок + +Блок — фрагмент Pug-кода, который можно добавить в письмо. Хранится в `block.pug` проекта: + +```pug +//Текст +tr + td.padding-wrapper + span.text Здесь текст + +//Кнопка +tr + td(align="center") + +buttonRounded("Текст кнопки", "https://...", 525, 42, "#c9e905", 18, "#000000", 4, "#c9e905") +``` + +Комментарий `//Название` — разделитель и имя блока. + +## Schema (поля редактирования) + +Для каждого блока конструктор автоматически генерирует schema — список редактируемых полей. Типы: + +| Тип | Описание | Пример | +|-----|----------|--------| +| text | Текстовое поле | `span.text Текст здесь` | +| href | URL ссылки | `a(href="https://...")` | +| src | URL картинки | `img(src="https://...")` | +| mixin-text | Текстовый аргумент миксина | `+button("Текст", ...)` | +| mixin-href | URL аргумент миксина | `+button("...", "https://...")` | +| mixin-ids | ID товаров (через запятую) | `+products3inRow("1,2,3")` | +| select | Выбор из вариантов (цветовые темы) | `- var _t = 'синий'` | +| raw | Полный Pug-код блока | — | + +## mixinRules (settings.json) + +Правила маппинга аргументов миксинов на типы полей: + +```json +{ + "mixinRules": [ + { "mixin": "products3inRow", "argIndex": 0, "type": "mixin-ids", "label": "ID товаров" }, + { "mixin": "buttonRounded", "argIndex": 0, "type": "mixin-text", "label": "Текст" }, + { "mixin": "buttonRounded", "argIndex": 1, "type": "mixin-href", "label": "Ссылка" } + ] +} +``` + +## extraFields (цветовые темы) + +Дополнительные поля поверх авто-схемы. Используются для цветовых тем Реаспект: + +```json +{ + "blocks": { + "Текст 100% Ширины": { + "extraFields": [{ + "type": "select", + "lineIndex": 0, + "options": [ + {"label": "Синий", "value": "синий", "color": "#130F33"}, + {"label": "Белый", "value": "белый", "color": "#ffffff"}, + {"label": "Зелёный", "value": "зелёный", "color": "#AAC8C8"} + ] + }] + } + } +} +``` + +В Pug это работает через переменную на первой строке: +```pug +- var _t = 'синий' +- var _bg = _t === 'белый' ? 'background__white' : _t === 'зелёный' ? 'background__green' : 'background__blue' +``` + +## Парсинг (parsing.js) + +`buildBaseSchema(content, blockName, mixinRules)` — главная функция. Алгоритм: +1. Разбивает content на строки +2. Для каждой строки определяет тип: текст, ссылка, картинка, вызов миксина +3. Если есть mixinRules для миксина — использует их +4. Иначе — автоопределение по содержимому аргументов +5. Применяет extraFields из settings + +**Известное ограничение:** Regex-парсинг. Работает для шаблонных блоков. Если изменить структуру миксина — нужно обновить mixinRules. diff --git a/Deploy.md b/Deploy.md index 38c1ac0..27f7852 100644 --- a/Deploy.md +++ b/Deploy.md @@ -1,25 +1,112 @@ # Деплой ## Сервер -IP: 147.45.109.108, Ubuntu 24, Docker 29.2 -## Запуск +| Параметр | Значение | +|----------|----------| +| IP | 147.45.109.108 | +| OS | Ubuntu 24.04 | +| Docker | 29.2, Compose 5.1 | +| RAM | 2 GB | +| Диск | 38 GB SSD | +| Домены | app.aspekter.ru, coins.aspekter.ru | + +## Структура на сервере + +``` +/opt/aspekter/ # Основная папка (git repo) +├── z51-pug-builder/ +│ ├── data/ # Docker volume — данные проектов +│ ├── src/ # Docker volume — исходники (live reload) +│ └── vite.config.js # Docker volume — API +├── email-gen/ # Email шаблоны +├── data/images/ # Локальное хранение картинок +├── coin-scout/data/ # SQLite для Coin Scout +└── docker-compose*.yml + +/etc/nginx/sites-enabled/ +├── emailbro.conf # app.aspekter.ru → 127.0.0.1:5175 +└── coins.conf # coins.aspekter.ru → 127.0.0.1:5180 + +/etc/letsencrypt/live/ +├── app.aspekter.ru/ # SSL сертификат +└── coins.aspekter.ru/ # SSL сертификат +``` + +## Docker Compose + +Три файла: +- `docker-compose.yml` — базовые сервисы (builder, email-gen-api) +- `docker-compose.dev.yml` — dev: порт 5174, data-dev/, coin-scout +- `docker-compose.prod.yml` — prod: порт 5175 на 127.0.0.1, volume mounts для src и vite.config.js + +Prod запуск: ```bash cd /opt/aspekter docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build ``` -## SSL -```bash -certbot certonly --standalone -d app.aspekter.ru -certbot certonly --standalone -d coins.aspekter.ru +Volume mounts в prod позволяют обновлять код без пересборки контейнера: +```yaml +volumes: + - ./z51-pug-builder/data:/app/data + - ./z51-pug-builder/vite.config.js:/app/vite.config.js + - ./z51-pug-builder/src:/app/src + - ./data/images:/app/data/images ``` -## Обновление +## SSL сертификаты + +Let's Encrypt через certbot (standalone mode): ```bash -cd /opt/aspekter && git pull && docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build +systemctl stop nginx # освободить порт 80 +certbot certonly --standalone -d app.aspekter.ru --agree-tos --email admin@aspekter.ru +certbot certonly --standalone -d coins.aspekter.ru --agree-tos --email admin@aspekter.ru +systemctl start nginx ``` -## Картинки -- FTP/SFTP — внешний сервер -- Локально — data/images/ → https://app.aspekter.ru/images/ +Автообновление настроено через certbot systemd timer. + +## Обновление кода + +```bash +cd /opt/aspekter +git pull origin main + +# Если изменился только код (src, vite.config.js) — перезапуск не нужен (live reload) +# Если изменился Dockerfile или package.json: +docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build +``` + +## DNS + +systemd-resolved может перезаписывать `/etc/resolv.conf` при перезапуске docker. Постоянный фикс: + +```bash +mkdir -p /etc/systemd/resolved.conf.d +cat > /etc/systemd/resolved.conf.d/dns.conf << EOF +[Resolve] +DNS=8.8.8.8 1.1.1.1 +FallbackDNS=8.8.4.4 +EOF +systemctl restart systemd-resolved +``` + +## Хранение картинок + +Два режима (настраивается per-project в Настройки → Интеграции): + +| Режим | Где хранятся | Публичный URL | +|-------|-------------|---------------| +| FTP/SFTP | Внешний сервер | Настраивается в ftpConfig.baseUrl | +| Локально | /opt/aspekter/data/images/{project}/{folder}/ | https://app.aspekter.ru/images/{project}/{folder}/file.png | + +Nginx раздаёт локальные картинки статикой (expires 30d, Cache-Control immutable). + +## Мониторинг + +```bash +docker ps # статус контейнеров +docker logs aspekter-builder # логи приложения +docker logs aspekter-email-gen-api # логи рендерера +``` diff --git a/Feeds.md b/Feeds.md new file mode 100644 index 0000000..5e65c88 --- /dev/null +++ b/Feeds.md @@ -0,0 +1,63 @@ +# Товары и фиды + +## Поддерживаемые форматы + +| Проект | Источник | Формат | Кодировка | +|--------|----------|--------|-----------| +| AT | Mindbox | YML (Yandex Market Language) | windows-1251 | +| numizmatRU | Mindbox | YML | windows-1251 | +| КБ | Mindbox | YML | UTF-8 | +| z51 | RetailCRM | API | UTF-8 | + +## Кэш фидов + +- Кэш в памяти vite-сервера, TTL 3 часа +- `feed-refresh` сбрасывает кэш и возвращает diff (добавленные/удалённые) +- При перезапуске контейнера кэш пуст — первый запрос загружает фид + +## Авто-подбор товаров (auto-assemble) + +Алгоритм подбора монет из фида: + +### 1. Фильтрация +- Исключение аксессуаров, альбомов, значков (EXCLUDE_CATS) +- Определение типа: `getProductType()` — монета/банкнота/копия/другое +- Фильтр по цене (любая / своя min-max) +- Исключение материалов (золото, серебро, платина) + +### 2. Группировка +- `getStyleKey()` — ключ стиля: регион + эпоха + материал + цена + диаметр +- Серии имеют приоритет +- Островные монетные дворы группируются по подтипу (wildlife/popculture/fantasy) + +### 3. Подбор блоков +- Layout mode: `[3, 1, 3, 3]` — схема блоков +- Country round-robin — чередование стран +- Scoring по similarity (серия, страна, эпоха, материал, диаметр, цена) +- Проверка конфликтов (дубли по ключу/имени/номиналу+году) + +### 4. Замена одной монеты +Каскадная замена с приоритетами: +1. Та же серия +2. Та же страна + материал + цена ±2x +3. Та же страна +4. Тот же регион + материал +5. Тот же регион +6. Любая с score ≥ 30 + +### 5. Пометка использованных +Сканирует письма за последние 25 дней, извлекает ID из Pug-контента: +- Красный бейдж: 0-15 дней назад +- Жёлтый бейдж: 16-25 дней назад + +## Определение типа товара (getProductType) + +``` +categoryId → BANKNOTE_CATS / COIN_CATS / COPY_CATS / EXCLUDE_CATS + ↓ не найден +name regex → банкнот, купюр, марк, лотерейн, сертификат, ... + ↓ не найден +material/weight check → если пустые → 'other' (не монета) + ↓ есть +return 'coin' +``` diff --git a/Home.md b/Home.md index 40a5fcf..938c853 100644 --- a/Home.md +++ b/Home.md @@ -1,6 +1,6 @@ # ASPEKTER -Визуальный конструктор email-рассылок. +Визуальный конструктор email-рассылок. Позволяет собирать письма из Pug-блоков, рендерить в HTML с инлайнингом CSS, управлять несколькими проектами с разными шаблонами и фидами товаров. ## Ссылки @@ -8,19 +8,61 @@ - **Coin Scout:** [coins.aspekter.ru](https://coins.aspekter.ru) - **Репозиторий:** [git.aspekter.ru/s.zotov/aspekter](https://git.aspekter.ru/s.zotov/aspekter) -## Стек +## Компоненты системы -| Компонент | Описание | Порт | -|-----------|----------|------| -| z51-pug-builder | Svelte 5 SPA — редактор писем | 5173 → 5175 | -| email-gen-api | Рендерер Pug→HTML | 8787 | -| coin-scout | Подбор монет из фидов | 5180 | -| nginx | Reverse proxy + SSL | 80/443 | +| Компонент | Технология | Описание | Порт | +|-----------|-----------|----------|------| +| z51-pug-builder | Svelte 5 + Vite 7 | SPA — визуальный редактор писем + API сервер | 5173 (контейнер) → 5175 (хост) | +| email-gen-api | Node.js 20 | HTTP-обёртка для рендерера Pug→HTML через email-templates + Juice | 8787 | +| coin-scout | Node.js 20 | Отдельный сервис подбора монет из YML-фидов | 5180 | +| nginx | 1.24 (хост) | Reverse proxy, SSL termination, раздача статики (картинки) | 80/443 | -## Документация +## Технологический стек -- [Архитектура](Architecture) -- [Деплой](Deploy) -- [API](API) -- [Безопасность](Security) -- [Пользователи](Users) +- **Frontend:** Svelte 5, Vite 7, моноширинный шрифт для Pug-редактора +- **Backend API:** Vite middleware в `vite.config.js` (CRUD, авторизация, фиды, FTP, аудит) +- **Рендер писем:** Pug → email-templates → Juice CSS inlining → HTML +- **Хранение данных:** JSON-файлы на диске (Docker volume) +- **Авторизация:** Cookie sessions, scrypt, роли admin/user +- **Деплой:** Docker Compose, Nginx на хосте, Let's Encrypt SSL +- **Сервер:** Ubuntu 24, VPS (2 GB RAM, 38 GB SSD) + +## Проекты + +Каждый проект — независимая папка в `data/` со своими блоками, настройками, письмами, фидами: + +| Проект | Описание | Фид товаров | Хранение картинок | +|--------|----------|-------------|-------------------| +| Реаспект | Агентство digital-маркетинга | — | SFTP selcdn.ru | +| AT (numizm.at) | Нумизматика | Mindbox YML (win-1251) | SFTP selcdn.ru | +| numizmatRU | Нумизматика | Mindbox YML (win-1251) | SFTP discobombulator.ru | +| z51 | Игровые кресла/столы | RetailCRM API | SFTP (z51.ru) | +| КБ (coinsbolshov) | Нумизматика | Mindbox YML (UTF-8) | SFTP selcdn.ru | + +## Возможности + +- Сборка писем из Pug-блоков drag-and-drop +- Цветовые темы блоков (синий/белый/зелёный для Реаспект) +- Авто-подбор товаров из YML-фидов с учётом страны, серии, материала, цены +- Пометка недавно использованных товаров (красный 0-15 дней, жёлтый 15-25 дней) +- Типограф Лебедева, Яндекс.Спеллер +- Подсветка двойных пробелов при копировании из Figma +- Превью письма в iframe 600px +- Копирование HTML (as-is и очищенный) +- FTP/SFTP загрузка картинок + локальное хранение +- Пресеты (сохранённые сборки блоков) +- История изменений писем +- Календарь рассылок (план) +- Многопользовательский режим с ролями +- Аудит-логи всех действий +- Интеграция с Google Sheets + +## Страницы wiki + +- [Архитектура](Architecture) — структура проекта, потоки данных, модули +- [Деплой](Deploy) — сервер, Docker, SSL, обновление, DNS +- [API](API) — полный список endpoints +- [Безопасность](Security) — Pug injection, конкурентность, аудит +- [Пользователи](Users) — роли, права, управление, аудит +- [Блоки и парсинг](Blocks) — как работают блоки, schema, миксины +- [Товары и фиды](Feeds) — авто-подбор, замена, кэш diff --git a/Security.md b/Security.md index 3b728ca..8817d17 100644 --- a/Security.md +++ b/Security.md @@ -1,13 +1,75 @@ # Безопасность ## Pug Injection Protection -Валидация перед рендером: запрещены require, process, exec, eval, Function, global, fs, Buffer. -## Конкурентность -Уникальный temp-файл на каждый запрос рендера. +**Проблема (исправлена):** Pug-код рендерится на сервере через Node.js. Вредоносный Pug мог выполнить произвольный код: +```pug +div(data-calc=process.cwd()) test +- let x = require('child_process').execSync('ls -la').toString() +``` -## Auth -scrypt, HttpOnly cookies, порты только 127.0.0.1, HTTPS. +**Решение:** В `email-gen-api/server.js` добавлена функция `validatePugSafety()`, которая проверяет Pug перед рендером. Запрещённые паттерны: -## Аудит -Все мутации логируются в JSONL (кто, когда, что, IP). Ротация 6 мес. +| Паттерн | Что блокирует | +|---------|--------------| +| `require(` | Подключение модулей | +| `process` | Доступ к process | +| `child_process` | Запуск процессов | +| `exec`, `execSync`, `spawn` | Выполнение команд | +| `eval(`, `Function(` | Динамическое выполнение | +| `global` | Глобальный объект | +| `__dirname`, `__filename` | Пути файловой системы | +| `fs.` | Файловые операции | +| `Buffer` | Работа с буферами | + +При обнаружении возвращается `400 Bad Request` с описанием найденного паттерна. + +**Дополнительная защита:** email-gen-api работает в Docker-контейнере без привилегий и без доступа к хостовой файловой системе (кроме email-gen volume). + +## Конкурентность рендера + +**Проблема (исправлена):** `renderWithNode()` писал результат в общий файл `public/index.html`. При одновременных запросах один пользователь мог получить HTML другого. + +**Решение:** Каждый запрос генерирует уникальное имя файла через `crypto.randomBytes(16)`: +``` +render_a1b2c3d4e5f6.html +``` +Файл создаётся, читается, удаляется — атомарно для каждого запроса. + +## Авторизация + +- **Хэширование:** scrypt (salt 16 bytes + hash 64 bytes) +- **Сессии:** Cookie `z51_token`, HttpOnly, SameSite=Lax, TTL 7 дней +- **Очистка:** Просроченные сессии удаляются каждые 30 минут +- **Middleware:** Все `/api/` endpoints (кроме `/api/auth/*`) требуют авторизацию +- **Admin:** `/api/admin/*` доступен только пользователям с `role: admin` +- **Проекты:** `userCanAccessProject()` проверяет `user.projects` (массив или `["*"]`) + +## Аудит-логи + +**Формат:** JSONL файлы `data/_system/logs/YYYY-MM.jsonl`, одна строка = одно действие: +```json +{"ts":1712345678000,"userId":"abc123","login":"admin","action":"letter","project":"Реаспект","details":{},"ip":"172.17.0.1"} +``` + +**Что логируется:** +- Все успешные мутации (POST/PUT/DELETE) — автоматически через обёртку `send()` +- Логин (успешный и неудачный) — явный вызов +- Логаут — явный вызов + +**Фильтрация:** Admin UI (Настройки → Логи) с фильтрами по пользователю, проекту, действию, пагинация. + +**Ротация:** Файлы старше 6 месяцев удаляются при старте сервера. + +## Сетевая безопасность + +- Docker порты проброшены только на `127.0.0.1` (не наружу) +- Nginx — единственная точка входа (80/443) +- HTTP → HTTPS редирект +- TLS 1.2+, strong ciphers +- `client_max_body_size 30m` (для GIF-загрузок) + +## Ограничения + +- Сессии хранятся в памяти Node.js — теряются при перезапуске контейнера (пользователи перелогинятся) +- `MAX_BODY_SIZE = 30 MB` — достаточно для GIF, но ограничивает загрузку очень больших файлов diff --git a/Users.md b/Users.md index c74a58e..9c283a8 100644 --- a/Users.md +++ b/Users.md @@ -1,9 +1,54 @@ # Пользователи ## Роли -| admin | Все проекты, управление пользователями, логи | -| user | Только назначенные проекты | + +| Роль | Проекты | Управление пользователями | Аудит-логи | Настройки | +|------|---------|--------------------------|------------|-----------| +| admin | Все (или `["*"]`) | Да | Да | Все | +| user | Только назначенные | Нет | Нет | Только доступных проектов | + +## Структура пользователя + +```json +{ + "id": "ae579099110bfe21", + "login": "s.zotov", + "passwordHash": "salt:hash", + "name": "Сергей Зотов", + "role": "admin", + "projects": ["*"], + "theme": "light" +} +``` + +Файл: `data/_system/users.json` + +## Управление (Настройки → Пользователи) + +Доступно только admin: +- Создание пользователя: логин, пароль, имя, роль, список проектов +- Изменение роли, проектов +- Сброс пароля +- Удаление (нельзя удалить себя) + +## Первый запуск + +При первом запуске (users.json пуст) создаётся seed-аккаунт: +- Логин: `admin`, Пароль: `admin` +- **Обязательно сменить пароль!** + +## Авторство писем + +При сохранении письма сервер автоматически добавляет: +- `createdBy` — логин создателя (при первом сохранении) +- `updatedBy` — логин последнего редактора (при каждом сохранении) + +Отображается в списке писем рядом с датой. При наведении — тултип с создателем и редактором. + +## Черновики + +Черновики (draft) хранятся per-user: `data/{project}/drafts/{userId}.json`. Каждый пользователь работает со своим черновиком, не мешая другим. ## Письма -- createdBy — создатель -- updatedBy — последний редактор + +Письма — общие для всех пользователей проекта. Индекс `letters.json` и файлы `letters/*.json` — shared. Кто последний сохранил — тот и `updatedBy`.