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