Полная документация ASPEKTER
90
API.md
90
API.md
@@ -1,25 +1,87 @@
|
|||||||
# API
|
# 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 | Данные проекта |
|
| Метод | URL | Body | Описание |
|
||||||
| PUT | /api/project/:name/settings | Настройки |
|
|-------|-----|------|----------|
|
||||||
|
| 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 |
|
|
||||||
|
|
||||||
## Фиды
|
| Метод | URL | Body | Описание |
|
||||||
| POST | /api/project/:name/auto-assemble | Авто-подбор |
|
|-------|-----|------|----------|
|
||||||
|
| 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=` | Аудит-логи |
|
||||||
|
|||||||
128
Architecture.md
128
Architecture.md
@@ -1,29 +1,117 @@
|
|||||||
# Архитектура
|
# Архитектура
|
||||||
|
|
||||||
## Структура
|
## Структура проекта
|
||||||
|
|
||||||
```
|
```
|
||||||
aspekter/
|
aspekter/
|
||||||
├── z51-pug-builder/ # Фронтенд + API
|
├── z51-pug-builder/ # Фронтенд + API сервер
|
||||||
│ ├── src/App.svelte # SPA компонент
|
│ ├── src/
|
||||||
│ ├── vite.config.js # API middleware
|
│ │ ├── App.svelte # Основной SPA компонент (~7000 строк)
|
||||||
│ └── data/{project}/ # Данные
|
│ │ ├── app.css # Все стили (~3300 строк)
|
||||||
├── email-gen/ # Pug-шаблоны
|
│ │ └── lib/
|
||||||
├── deploy/email-gen-api/ # Рендер-сервер
|
│ │ ├── api.js # HTTP-клиент для всех API endpoints
|
||||||
├── coin-scout/ # Подбор монет
|
│ │ ├── parsing.js # Парсинг Pug: buildBaseSchema, parseQuotedArgs, etc.
|
||||||
└── docker-compose*.yml
|
│ │ ├── 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
|
## Поток генерации HTML
|
||||||
1. Пользователь собирает письмо из блоков
|
|
||||||
2. POST на email-gen-api с Pug-кодом
|
|
||||||
3. Валидация → temp-файл → email-templates рендер
|
|
||||||
4. Juice инлайнит CSS → HTML
|
|
||||||
5. Превью / копирование
|
|
||||||
|
|
||||||
## Данные проекта
|
|
||||||
```
|
```
|
||||||
data/{project}/
|
Пользователь → собирает письмо из блоков в конструкторе
|
||||||
├── block.pug, settings.json
|
↓
|
||||||
├── draft.json, presets.json
|
Pug-код собирается из block.content каждого блока
|
||||||
├── letters.json + letters/*.json
|
↓
|
||||||
|
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
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
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
|
```bash
|
||||||
cd /opt/aspekter
|
cd /opt/aspekter
|
||||||
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
|
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
## SSL
|
Volume mounts в prod позволяют обновлять код без пересборки контейнера:
|
||||||
```bash
|
```yaml
|
||||||
certbot certonly --standalone -d app.aspekter.ru
|
volumes:
|
||||||
certbot certonly --standalone -d coins.aspekter.ru
|
- ./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
|
```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
|
||||||
```
|
```
|
||||||
|
|
||||||
## Картинки
|
Автообновление настроено через certbot systemd timer.
|
||||||
- FTP/SFTP — внешний сервер
|
|
||||||
- Локально — data/images/ → https://app.aspekter.ru/images/
|
## Обновление кода
|
||||||
|
|
||||||
|
```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
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
70
Home.md
@@ -1,6 +1,6 @@
|
|||||||
# ASPEKTER
|
# ASPEKTER
|
||||||
|
|
||||||
Визуальный конструктор email-рассылок.
|
Визуальный конструктор email-рассылок. Позволяет собирать письма из Pug-блоков, рендерить в HTML с инлайнингом CSS, управлять несколькими проектами с разными шаблонами и фидами товаров.
|
||||||
|
|
||||||
## Ссылки
|
## Ссылки
|
||||||
|
|
||||||
@@ -8,19 +8,61 @@
|
|||||||
- **Coin Scout:** [coins.aspekter.ru](https://coins.aspekter.ru)
|
- **Coin Scout:** [coins.aspekter.ru](https://coins.aspekter.ru)
|
||||||
- **Репозиторий:** [git.aspekter.ru/s.zotov/aspekter](https://git.aspekter.ru/s.zotov/aspekter)
|
- **Репозиторий:** [git.aspekter.ru/s.zotov/aspekter](https://git.aspekter.ru/s.zotov/aspekter)
|
||||||
|
|
||||||
## Стек
|
## Компоненты системы
|
||||||
|
|
||||||
| Компонент | Описание | Порт |
|
| Компонент | Технология | Описание | Порт |
|
||||||
|-----------|----------|------|
|
|-----------|-----------|----------|------|
|
||||||
| z51-pug-builder | Svelte 5 SPA — редактор писем | 5173 → 5175 |
|
| z51-pug-builder | Svelte 5 + Vite 7 | SPA — визуальный редактор писем + API сервер | 5173 (контейнер) → 5175 (хост) |
|
||||||
| email-gen-api | Рендерер Pug→HTML | 8787 |
|
| email-gen-api | Node.js 20 | HTTP-обёртка для рендерера Pug→HTML через email-templates + Juice | 8787 |
|
||||||
| coin-scout | Подбор монет из фидов | 5180 |
|
| coin-scout | Node.js 20 | Отдельный сервис подбора монет из YML-фидов | 5180 |
|
||||||
| nginx | Reverse proxy + SSL | 80/443 |
|
| nginx | 1.24 (хост) | Reverse proxy, SSL termination, раздача статики (картинки) | 80/443 |
|
||||||
|
|
||||||
## Документация
|
## Технологический стек
|
||||||
|
|
||||||
- [Архитектура](Architecture)
|
- **Frontend:** Svelte 5, Vite 7, моноширинный шрифт для Pug-редактора
|
||||||
- [Деплой](Deploy)
|
- **Backend API:** Vite middleware в `vite.config.js` (CRUD, авторизация, фиды, FTP, аудит)
|
||||||
- [API](API)
|
- **Рендер писем:** Pug → email-templates → Juice CSS inlining → HTML
|
||||||
- [Безопасность](Security)
|
- **Хранение данных:** JSON-файлы на диске (Docker volume)
|
||||||
- [Пользователи](Users)
|
- **Авторизация:** 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) — авто-подбор, замена, кэш
|
||||||
|
|||||||
76
Security.md
76
Security.md
@@ -1,13 +1,75 @@
|
|||||||
# Безопасность
|
# Безопасность
|
||||||
|
|
||||||
## Pug Injection Protection
|
## Pug Injection Protection
|
||||||
Валидация перед рендером: запрещены require, process, exec, eval, Function, global, fs, Buffer.
|
|
||||||
|
|
||||||
## Конкурентность
|
**Проблема (исправлена):** Pug-код рендерится на сервере через Node.js. Вредоносный Pug мог выполнить произвольный код:
|
||||||
Уникальный temp-файл на каждый запрос рендера.
|
```pug
|
||||||
|
div(data-calc=process.cwd()) test
|
||||||
|
- let x = require('child_process').execSync('ls -la').toString()
|
||||||
|
```
|
||||||
|
|
||||||
## Auth
|
**Решение:** В `email-gen-api/server.js` добавлена функция `validatePugSafety()`, которая проверяет Pug перед рендером. Запрещённые паттерны:
|
||||||
scrypt, HttpOnly cookies, порты только 127.0.0.1, HTTPS.
|
|
||||||
|
|
||||||
## Аудит
|
| Паттерн | Что блокирует |
|
||||||
Все мутации логируются в 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, но ограничивает загрузку очень больших файлов
|
||||||
|
|||||||
53
Users.md
53
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`.
|
||||||
|
|||||||
Reference in New Issue
Block a user