Page:
Architecture
Clone
Table of Contents
This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Архитектура проекта
Структура директорий
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 — полная структура
{
"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": ""
}