3
Architecture
s.zotov edited this page 2026-04-12 20:46:34 +00:00
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": ""
}