Полная документация ASPEKTER

2026-04-13 12:25:01 +05:00
parent 48cc5ae564
commit e3467cbcaf
8 changed files with 606 additions and 70 deletions

90
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=` | Аудит-логи |

@@ -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).

87
Blocks.md Normal file

@@ -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.

109
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 # логи рендерера
```

63
Feeds.md Normal file

@@ -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'
```

70
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) — авто-подбор, замена, кэш

@@ -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, но ограничивает загрузку очень больших файлов

@@ -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`.