Add "Architecture"

2026-04-12 20:46:34 +00:00
parent b67611ffc9
commit 7e6d509205

238
Architecture.md Normal file

@@ -0,0 +1,238 @@
# Архитектура проекта
## Структура директорий
```
VA.ASPEKTER/
├── z51-pug-builder/ # Основное приложение (название историческое)
│ ├── vite.config.js # ⭐ ВЕСЬ backend API (~1600 строк, Vite middleware)
│ ├── src/
│ │ ├── App.svelte # ⭐ ВЕСЬ frontend UI (~6500 строк, монолит)
│ │ ├── app.css # Стили + темы (light/dark)
│ │ ├── main.js # Entry point Svelte
│ │ └── lib/
│ │ ├── api.js # HTTP-клиент ко всем API эндпоинтам
│ │ ├── parsing.js # Парсинг Pug: блоки, миксины, секции, поля
│ │ ├── spellcheck.js # Извлечение текста из HTML, инъекция меток ошибок
│ │ └── utils.js # Утилиты
│ ├── public/
│ │ ├── Block.pug # Исходные шаблоны всех блоков
│ │ ├── favicon.jpg
│ │ └── login-bg/ # Фоны страницы логина (5 изображений)
│ ├── data/ # ⚠️ НЕ в git — пользовательские данные
│ │ ├── _system/ # users.json, sessions.json
│ │ ├── config.json # Yonote token, URLs
│ │ ├── feed-cache.json # Кэш YML-фида на диске
│ │ ├── render-cache.json # Кэш рендера PUG→HTML
│ │ ├── uploads/ # Загруженные картинки
│ │ └── vipavenue/ # Данные проекта VipAvenue
│ │ ├── settings.json # Все настройки (FTP, фид, gender paths, блоки)
│ │ ├── presets.json # Пресеты блоков
│ │ ├── notes.json # Индекс заметок
│ │ ├── stats.json # Статистика времени сборки
│ │ ├── block.pug # Кастомный шаблон блоков
│ │ ├── drafts/{userId}.json # Черновики по юзерам
│ │ └── letters/{userId}/ # Письма по юзерам
│ │ ├── _index.json # Индекс писем
│ │ ├── {id}.json # Данные письма (блоки, тема, дата)
│ │ └── {id}.history.json # История изменений (макс 20 снэпшотов)
│ ├── Dockerfile
│ └── package.json
├── email-gen/ # Pug-шаблоны писем (репозиторий коллег)
│ └── emails/
│ ├── includes/
│ │ └── mixins.pug # Общие миксины (preheader, spacerLine, buttons и т.д.)
│ ├── layout/
│ │ └── layout.pug # Базовый layout письма
│ └── vipavenue/
│ ├── parts/
│ │ ├── header/ # header-woman.pug, header-man.pug
│ │ └── footer/ # footer-woman.pug, footer-man.pug
│ └── letters/
│ └── let.pug # ⚠️ Генерируется автоматически при рендере
├── deploy/
│ ├── email-gen-api/
│ │ ├── server.js # Микросервис рендера (~200 строк)
│ │ ├── Dockerfile
│ │ └── entrypoint.sh
│ ├── nginx/ # Референсный конфиг nginx
│ └── scripts/
│ └── update-email-gen.sh # Скрипт обновления email-gen
├── docker-compose.yml # DEV окружение (с HMR)
└── docker-compose.prod.yml # PROD окружение (образы)
```
## Цепочка рендера (подробно)
```
1. Пользователь собирает блоки в конструкторе
└── assembledBlocks[] → rebuildOutput() → outputPug (строка с PUG-кодом)
2. Нажимает 🔄 или Ctrl+G
└── renderEmailPreview() → POST /api/project/vipavenue/render-email
Body: { projectSlug: 'vipavenue', pug: outputPug, preheader: '...', gender: 'female' }
3. Backend (vite.config.js):
a. Экранирует #{} и !{} в pug (защита от инъекций)
b. Экранирует переносы строк в preheader
c. Проверяет render cache (MD5 от slug+pug+gender+genderPaths)
d. Если cache miss:
- Записывает pug в email-gen/emails/vipavenue/letters/let.pug
- Генерирует html.pug с gender-specific header/footer
- Отправляет POST к email-gen-api:8787/render
- Получает скомпилированный HTML
e. Параллельно загружает YML-фид (если настроен feedUrl)
f. Обрабатывает Mindbox-теги (подставляет данные из фида)
g. Применяет nowrap (висячие предлоги)
h. Возвращает: { html, previewHtml, unavailableProducts, feedSyncedAt }
4. email-gen-api (server.js):
a. npm install если нет node_modules
b. Записывает let.pug
c. Генерирует html.pug с путями header/footer по гендеру
d. Компилирует через email-templates (Pug → HTML с инлайном CSS)
e. Заменяет #MAILRU_PREHEADER_TAG# → <vk-snippet-end/>
f. Возвращает HTML
5. Frontend получает HTML:
a. Показывает в <iframe srcdoc sandbox="allow-same-origin">
b. Масштабирует под viewport (fitPreviewFrame)
c. Инжектит click detection для quick edit
d. Запускает spell check если активен
```
## Аутентификация (подробно)
### Хеширование паролей
- Алгоритм: **scrypt** (Node.js crypto.scryptSync)
- Ключ: 64 байта
- Соль: 16 случайных байтов (crypto.randomBytes)
- Формат хранения: `salt_hex:hash_hex`
- Сравнение: timing-safe (crypto.timingSafeEqual)
### Сессии
- Токен: 32 случайных байта → hex (64 символа)
- Хранение: Map в памяти + `data/_system/sessions.json` на диске
- TTL: 7 дней
- Cookie: `va_token`, HttpOnly, Secure, SameSite=Strict
### Brute-force защита
- Лимит: 5 попыток с одного IP за 15 минут
- Хранение: Map в памяти `{ip: {count, firstAttempt}}`
- Очистка: setInterval каждые 10 минут
- IP определяется из X-Forwarded-For (первый) или socket.remoteAddress
### CSRF защита
- POST/PUT/DELETE/PATCH запросы проверяют Origin или Referer заголовок
- Должен совпадать с Host
- Если оба заголовка отсутствуют — запрос пропускается (для форм без JS)
- Основная защита — SameSite=Strict на cookie
### Auto-seed
При первом запуске, если `users.json` пуст:
- Создаётся пользователь `admin` с ролью `admin`
- Генерируется случайный 16-символьный hex пароль
- Пароль выводится в console.log
### Роли
- `admin` — полный доступ: настройки, конфигурация, управление пользователями
- `user` — сборка писем, просмотр настроек (без редактирования)
## Хранение данных
Файловая БД — JSON-файлы на диске. Нет SQL/MongoDB/Redis.
### Глобальные данные
| Файл | Описание |
|------|----------|
| `_system/users.json` | Массив `[{id, login, passwordHash, name, role, projects, theme, activePage, previewZoom}]` |
| `_system/sessions.json` | Объект `{token: {userId, expiresAt}}` |
| `config.json` | `{yonote_token, yonote_base_url, upload_base_url}` |
| `feed-cache.json` | `{url, ts, products: {id: {...}}}` — кэш фида |
| `render-cache.json` | `{hash: html}` — кэш рендера (LRU, макс 30) |
### Данные проекта (vipavenue/)
| Файл | Описание |
|------|----------|
| `settings.json` | Все настройки проекта (см. ниже) |
| `block.pug` | Пользовательский шаблон блоков |
| `block-custom.pug` | Экспортированные кастомные блоки |
| `meta.json` | `{sourceName}` — имя источника шаблонов |
| `presets.json` | `[{id, name, savedAt, blocks}]` |
| `notes.json` | `{list: [{id, title, createdAt, updatedAt}], currentId}` |
| `notes/{id}.json` | Содержимое заметки |
| `stats.json` | Массив записей трекинга времени |
### Данные по пользователям
| Путь | Описание |
|------|----------|
| `drafts/{userId}.json` | Черновик текущей сборки |
| `letters/{userId}/_index.json` | Индекс писем `{list, currentId}` |
| `letters/{userId}/{id}.json` | Письмо: `{id, name, title, date, blocks, preheader, assemblyInfo, ...}` |
| `letters/{userId}/{id}.history.json` | Массив снэпшотов (макс 20) |
### settings.json — полная структура
```json
{
"globalSpacing": 40,
"blocks": {
"Заголовок": { "defaultSpacing": 20 },
"БАННЕР": { "defaultSpacing": 0, "template": "..." }
},
"accentColor": "#130F33",
"projectTitle": "vipavenue",
"emailGenProject": "vipavenue",
"productOptions": [
{ "key": "hidePrice", "label": "Скрыть цену", "default": false }
],
"mixinRules": [
{ "mixin": "+products4", "args": [{"index": 0, "type": "mixin-ids", "label": "ID товаров"}] }
],
"quickBlocks": ["Заголовок", "Текст", "БАННЕР"],
"quickBlockColors": { "Заголовок": "default" },
"customBlocks": [
{ "name": "Мой блок", "content": "...", "schema": [...] }
],
"certificate": {
"enabled": false,
"blocks": [...]
},
"sidebarNote": "Текст заметки в сайдбаре",
"yonoteConfig": {
"databaseId": "...",
"statusProperty": "Статус",
"dateProperty": "Дата",
"subjectProperty": "Тема",
"preheaderProperty": "Прехедер",
"idProperty": "ID товаров",
"extraProperties": ["Проект", "Теги"]
},
"ftpConfig": {
"protocol": "sftp",
"host": "212.113.122.5",
"port": 22,
"user": "...",
"password": "...",
"remotePath": "public/newsletter_2026",
"baseUrl": "https://email-files.vipavenue.ru/newsletter_2026"
},
"mailingService": {
"label": "Mindbox",
"url": "https://vipavenue.mindbox.ru/..."
},
"feedUrl": "https://exchange.vipavenue.ru/products/mind_box_by_products.xml",
"genderPaths": {
"headerFemale": "./parts/header/header-woman",
"headerMale": "./parts/header/header-man",
"footerFemale": "./parts/footer/footer-woman",
"footerMale": "./parts/footer/footer-man"
},
"imageBaseUrl": "https://email-files.vipavenue.ru/",
"imageExt": ".png",
"linkTemplate": ""
}
```