Add "Architecture"
238
Architecture.md
Normal file
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": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user