Перейти к содержанию

Отчёт о проделанной работе

Обзор

Этот документ — исчерпывающий отчёт обо всей работе, выполненной при проектировании, разработке, аудите безопасности и выводе в производство платформы TryOn SaaS — B2B-сервиса виртуальной примерки одежды, позволяющего e-commerce-компаниям встраивать виджет с AI-примеркой на свои сайты. Документ предназначен для технического и бизнес-руководства и даёт полное представление о масштабе, глубине и инженерной дисциплине, вложенных в систему.

Платформа прошла путь от пустого VPS до полностью автоматизированного, защищённого, мониторируемого production-сервиса с 41 находкой аудита безопасности, 507+ автотестами, покрытием кода ≥97% и CI/CD-пайплайном, при котором git push приводит к деплою приблизительно за 20 секунд.


1. Настройка сервера и инфраструктуры

Провижн

Основой платформы служит выделенный VPS, приобретённый на xorek.cloud, со следующими характеристиками:

Параметр Значение
CPU 4 vCPU
RAM 8 ГБ
Диск 80 ГБ NVMe SSD
IP-адрес 185.246.222.107
ОС Ubuntu 22.04 LTS

Хардение с первого дня

Укрепление безопасности было выполнено в самую первую сессию, до деплоя какого-либо кода приложения:

  • Root SSH отключён — удалённый вход под root явно заблокирован в /etc/ssh/sshd_config
  • Аутентификация по паролю отключена — принимается только аутентификация по публичному ключу
  • Непривилегированный пользователь deploy — все операции выполняются из-под deploy с NOPASSWD sudo, строго ограниченным Docker-командами
  • Файрвол UFW — открыты только порты 22 (SSH), 80 (HTTP) и 443 (HTTPS); весь остальной входящий трафик блокируется по умолчанию
  • fail2ban — защита SSH-порта от брутфорса с автоматической блокировкой IP после повторных неудачных попыток

Восстановление доступа по SSH

При утере SSH-ключа восстановление возможно через веб-консоль xorek.cloud. Резервная копия конфига sshd хранится на сервере по пути /etc/ssh/sshd_config.backup.20260520.


2. Домен и DNS

Интеграция с Cloudflare

Домен ziex-tryon.com зарегистрирован и полностью управляется через Cloudflare. Всё DNS-разрешение идёт через прокси Cloudflare (режим «оранжевое облако»), обеспечивая:

  • Защиту от DDoS на уровне DNS/CDN
  • Автоматические редиректы HTTP→HTTPS
  • Фильтрацию по репутации IP
  • Кэширование статических ресурсов на Edge

Стратегия TLS

Вместо Let's Encrypt (требующего автоматизации обновления сертификатов и ACME-проблем) выбран Cloudflare Origin Certificate — сертификат, выпускаемый напрямую Cloudflare и доверенный только между Edge Cloudflare и origin-сервером. Это позволяет использовать режим Full Strict TLS без операционных накладных расходов certbot.

OCSP Stapling в nginx

Cloudflare Origin Certificates не подписаны публичным CA, поэтому OCSP Stapling должен быть отключён в nginx:

ssl_stapling        off;
ssl_stapling_verify off;
Без этой настройки nginx пишет повторяющиеся ошибки OCSP в лог и может задерживать TLS-рукопожатия.

Карта поддоменов

Настроено шесть DNS-записей, все указывают на один origin-IP (185.246.222.107), но маршрутизируются в разные контексты приложения через server-блоки nginx:

Поддомен Назначение
ziex-tryon.com Основная лендинговая страница
api.ziex-tryon.com REST API (FastAPI backend)
admin.ziex-tryon.com Панель администратора (React SPA)
app.ziex-tryon.com Портал клиентов (React SPA)
sandbox.ziex-tryon.com Sandbox для тестирования интеграции
docs.ziex-tryon.com Документация на MkDocs

3. Архитектура

Платформа спроектирована с нуля с чётким разделением ответственности между stateless HTTP-сервисами, пайплайном обработки задач на основе очереди и выделенным ML-бэкендом.

Сайт клиента
tryon-embed.js  ──►  Admin API (:8000)  ──►  Redis Queue  ──►  ML Worker
(Shadow DOM)          │                                              │
                      │                                              ▼
                 PostgreSQL 15                              fal.ai FASHN v1.5
                      ▲                                    (AI-инференс)
             ┌────────┴────────┐
        Prometheus           Grafana
             ▲               (3 дашборда)
       Pushgateway  ◄──  Метрики ML Worker
       AlertManager  ──►  Telegram Bot
           nginx  (TLS · rate limiting · security headers)

Реестр сервисов

Вся платформа работает как 8 Docker-сервисов, описанных в docker-compose.yml с production-оверлеем в docker-compose.prod.yml:

Сервис Внутренний порт Роль
postgres 5432 (внутренний) PostgreSQL 15 — основное хранилище данных
redis 6379 (внутренний) Redis 7 AOF — очередь задач + счётчики rate limit
admin-api 8000 FastAPI — вся бизнес-логика
ml-worker нет Обработчик задач — цикл Redis BRPOP
frontend 5173 (внутренний) Vite dev / собранный React SPA
nginx 80/443 Reverse proxy, TLS-терминация
prometheus 9090 (внутренний в prod) Сбор метрик
grafana 3000 (внутренний в prod) Дашборды

Каждый сервис настроен с: - restart: unless-stopped - Драйвером логирования json-file с max-size: 50m, max-file: 5 - limits и reservations по CPU и памяти - Docker health checks

Production vs Development

docker-compose.prod.yml переопределяет базовый compose: закрывает порты 8000, 9090 и 3000 от публичного доступа, устанавливает MEDIA_BASE_URL на production-домен и монтирует production-конфиг nginx с полным TLS. В режиме разработки эти порты доступны напрямую для отладки.

Пайплайн обработки задач

Когда встроенный виджет клиента инициирует запрос примерки:

  1. Виджет (tryon-embed.js) захватывает изображения товара и человека, отправляет POST /api/v1/tryon с API-ключом
  2. Admin API валидирует ключ, проверяет квоту генераций клиента, обрабатывает изображения (изменение размера, снятие EXIF, конвертация в WebP), сохраняет в хранилище, пушит JSON-payload задачи в Redis
  3. ML Worker подхватывает задачу через BRPOP (блокирующий pop), вызывает fal.ai FASHN v1.5
  4. fal.ai выполняет инференс (виртуальная примерка), возвращает URL результата
  5. Worker обновляет статус задачи в PostgreSQL и вызывает зарегистрированные вебхуки
  6. Клиент поллит GET /api/v1/tryon/status/{id} до получения статуса completed

4. Технический стек

Backend

Компонент Версия Примечания
Python 3.11 Async везде (asyncio, async SQLAlchemy)
FastAPI 0.111.0 ASGI, интеграция с Pydantic v2
SQLAlchemy 2.0 Async engine, фабрика async_session
Pydantic v2 Breaking change: AnyHttpUrl больше не является подклассом str
Alembic 1.13 Миграции БД, применяются автоматически при запуске
PostgreSQL 15 Основное хранилище данных
Redis 7 AOF-персистентность, очередь задач + rate limiting
PyJWT ≥2.8.0 Не python-jose — импортировать как import jwt
bcrypt прямой (5.x) Не passlib — API 5.x изменился, passlib несовместим
structlog актуальный JSON-логирование с редактированием чувствительных полей
httpx актуальный Async HTTP-клиент для вызовов fal.ai

Подводный камень Pydantic v2: AnyHttpUrl

В Pydantic v1 AnyHttpUrl был подклассом str и мог передаваться напрямую в asyncpg. В Pydantic v2 — нет. Любое URL-поле, передаваемое в SQLAlchemy/asyncpg, необходимо явно приводить к строке:

# Неправильно — asyncpg бросает TypeError: expected str, got AnyHttpUrl
model = Job(webhook_url=body.webhook_url)

# Правильно
model = Job(webhook_url=str(body.webhook_url))

# Правильно для PATCH (model_dump автоматически приводит URL)
body.model_dump(mode="json", exclude_unset=True)

Frontend

Компонент Версия Примечания
React 18 SPA, hooks, context
TypeScript 5 Strict mode
Vite 5 Сборщик, dev-сервер
Tailwind CSS 3 Utility-first стилизация
shadcn/ui актуальный Библиотека компонентов
Node.js 22 Среда сборки (Alpine-образ)
Axios актуальный HTTP-клиент с JWT-интерцептором и авто-обновлением токена
Zustand актуальный Управление состоянием (отдельные stores для admin/portal)

Инфраструктура

Компонент Версия Примечания
Docker Compose V2 Команда docker compose
nginx 1.25 TLS, gzip, rate limiting, security headers
Prometheus 2.x Сбор метрик
Grafana 11.0.0 Дашборды, авто-провижн
Pushgateway 1.10.0 Метрики воркера (у воркера нет HTTP-сервера)
fal.ai FASHN v1.5 AI-бэкенд для инференса

5. Аудит безопасности — 41 находка

Формальный аудит безопасности был проведён в два этапа. Выявлено 41 находка, 38 закрыто, 3 сознательно отложено с письменным обоснованием в реестре известных проблем проекта.

Аутентификация и управление доступом (8 находок)

Находка Решение
Refresh-токены хранились в открытом виде SHA-256 пре-хэш → bcrypt(rounds=12); token_prefix VARCHAR(16) для O(1) lookup в БД
Нет защиты от брутфорса при логине 5 неудачных попыток → блокировка IP + email на 15 минут (счётчики Redis)
Timing attack при поиске пользователя dummy_password_check() запускает bcrypt на пре-вычисленном хэше, если пользователь не найден — оба пути занимают одинаковое время
Portal JWT использовал тот же scope, что и admin Отдельный claim scope="portal"; зависимость get_current_client валидирует scope
Lookup refresh-токенов был O(n) Индекс token_prefix + составной (token_prefix, user_id) — O(1) при любом масштабе
Нет rate limit на /auth/change-password Отложено: атака ограничена 15-минутным окном access-токена; rate limit запланирован на следующий спринт
Брутфорс API-ключей Быстрый lookup через key_prefix VARCHAR(20); хранение bcrypt-хэша; почасовой rate limit per-key в Redis
Нет rate limit на логин портала Исправлено: 5/мин/IP через зависимость get_redis. Было через request.app.state.redis (никогда не устанавливался → всегда fail-open)

Валидация входных данных и инъекции (7 находок)

Находка Решение
f-string SQL в запросах статистики Заменено на параметризованные SQLAlchemy-выражения повсюду
SSRF через URL вебхука validate_webhook_url() блокирует RFC-1918, loopback, link-local, CGNAT; DNS-резолвит хост для проверки всех возвращаемых IP
SSRF через per-job webhook_url Тот же валидатор применён на уровне Pydantic-схемы — не только в воркере
Не валидировался MIME-тип Строгая проверка image/jpeg / image/png по магическим байтам, не только по Content-Type
Decompression bomb PIL.MAX_IMAGE_PIXELS = 4096 * 4096; явная проверка числа пикселей после открытия; >16.7M пикселей → HTTP 422
TryOnRequest.mode принимал любую строку Изменено на Literal["balanced", "quality"]; неверные значения возвращают 422
ClientUpdate.status принимал любую строку Изменено на Literal["active", "suspended"]; предотвращает сохранение "banned" в БД

Инфраструктура и конфигурация (14 находок)

Находка Решение
Порт 8000 открыт публично Закрыт в docker-compose.prod.yml; nginx — единственная точка входа
Порты 9090, 3000 открыты публично Закрыты в production-оверлее
Ошибка парсинга CORS_ORIGINS pydantic-settings не может парсить comma-separated список; нужен JSON-массив в .env
Endpoint /metrics публично доступен location = /metrics { deny all; return 403; } в обоих nginx-конфигах
Нет Cache-Control на API-ответах RequestIdMiddleware добавляет Cache-Control: no-store, private ко всем ответам /api/
ENABLE_DOCS не применялся /docs, /redoc, /openapi.json удаляются при ENABLE_DOCS=false
Отсутствует заголовок Permissions-Policy Добавлен в оба nginx-конфига: camera=(), microphone=(), geolocation=()
Security headers только для 2xx Добавлен флаг always ко всем директивам security headers в nginx
Не установлен client_max_body_size Установлен 12m (10 МБ изображение + ~33% накладных base64)
Prometheus нацелен на ml-worker:8001 Удалён — у ml-worker нет HTTP-сервера; порождал спам в логах Prometheus
DOMAIN отсутствует в .env.example Добавлен; deploy.sh --prod завершается с ошибкой, если DOMAIN не задан
Предупреждение media_base_url_localhost Лог при старте + поле media_base_url_public: bool в ответе health endpoint
Бэкенд хранилища не наблюдаем GET /health включает storage_backend: "local"/"s3"
Отсутствует location /health в nginx Добавлен явный location = /health в оба конфига; без него /health проваливался на Vite frontend

Корректность frontend (7 находок)

Находка Решение
Settings.tsx вызывал неверный endpoint Был PUT /auth/me/password; исправлено на POST /auth/change-password с правильными полями
ClientDetail.tsx итерировал envelope-объект GET /clients/{id}/keys возвращает пагинированный envelope; исправлено использование .items
Race condition при concurrent refresh 401 Module-level refreshPromise dedup в api/client.ts — второй 401 переиспользует in-flight refresh
Неверный namespace событий вебхуков портала Было tryon.completed; исправлено на job.completed / job.failed (единственные валидные значения в _VALID_EVENTS)
model_validate(update=...) удалён в Pydantic 2.13+ Ответ вебхука собирается как plain dict перед model_validate
Фильтр задач по дате был мёртвым кодом Полностью реализован: query params date_from/date_to подключены и на frontend, и на backend
Создание плана без поля slug Frontend автогенерирует slug из названия плана; API требует slug + generations_limit

Наблюдаемость и мониторинг (5 находок)

Находка Решение
Нет Prometheus-метрик для воркера Интеграция с Pushgateway — воркер пушит метрики после каждой задачи; Prometheus скрапит Pushgateway
Healthcheck воркера всегда проходил Был python -c "sys.exit(0)"; теперь проверяет, что ключ worker_heartbeat в Redis не старше 90 секунд
Нет обнаружения зависших задач _run_stale_job_sweep() запускается каждые 60 с в lifespan admin-api; помечает processing >15 мин и pending >30 мин как failed
Нет Grafana-дашбордов 3 дашборда авто-провижнятся из директории grafana/dashboards/
Дублирующиеся Telegram-алерты Redis SETNX с TTL 1 час на ключ алерта предотвращает повторные уведомления

6. CI/CD Пайплайн

CI/CD пайплайн построен так, чтобы автоматически соблюдать стандарты качества проекта. Рабочий процесс разработчика: написать код, запушить — и наблюдать, как пайплайн делает всё остальное.

Непрерывная интеграция (ci.yml)

Push в любую ветку
  Линтинг (ruff)
  admin/ruff.toml: line-length=130, E501 игнорируется
  Тесты (pytest)
  Сервисы: postgres:15, redis:7 в контейнерах GitHub Actions
  Порог: --cov-fail-under=94 (поддерживается ≥97%)
  Docker build (admin-api + ml-worker)
  Проверяет корректность Dockerfile и установку зависимостей

Непрерывный деплой (deploy.yml)

CI workflow завершается успехом
       ▼  (триггер workflow_run, не needs:)
  SSH на 185.246.222.107
  git pull origin main
  git diff HEAD~1 HEAD → определяем изменённые сервисы
       ├── изменён admin-api?   → docker compose build admin-api && up -d
       ├── изменён ml-worker?   → docker compose build ml-worker && up -d
       ├── изменён frontend?    → сборка в контейнере → rsync dist/ в nginx
       └── изменён nginx/compose? → docker compose up -d nginx
  curl /health → проверяем 200
  Деплой завершён (~20 секунд)

Почему workflow_run, а не needs:

needs: работает только в рамках одного файла workflow. workflow_run позволяет deploy.yml ожидать успеха ci.yml между разными файлами. Без этого деплой запускался бы при каждом пуше вне зависимости от результата тестов.

Инструменты локальной разработки

  • docker-compose.test.yml — запускает тест-сьют с volume-mount исходников; не требует пересборки после изменений кода
  • Pre-push git hook — запускает ruff check и pytest локально перед отправкой в GitHub
  • scripts/generate-secrets.sh — генерирует все случайные секреты (JWT-ключ, пароль БД, пароль Grafana, пароль admin) одной командой

7. Тест-сьют

Тестирование рассматривалось как первоклассная часть продукта. Сьют рос параллельно с кодовой базой, применяя практики test-driven development.

Методология покрытия

Перед написанием любого теста определяются точные непокрытые строки:

cd admin && pytest --cov=app --cov-report=term-missing -q \
  2>&1 | grep -E "^\s+app/" | sort -k4 -t% -n | head -20

Это исключает угадывание, что тестировать, и гарантирует, что каждый новый файл тестов закрывает реальные пробелы в покрытии, а не воображаемые.

Реестр файлов тестов

Файл Тестов Область
test_auth.py 40+ Вход, refresh, logout, смена пароля, lockout при брутфорсе
test_clients.py 25+ CRUD клиентов, suspend/activate, сброс квоты
test_api_keys.py 20+ Создание, список, отзыв API-ключей
test_plans.py 15+ CRUD планов включая delete (было 0% до аудита)
test_jobs.py 15+ Список/детали задач, изоляция статуса между клиентами
test_stats.py 20+ Обзор, realtime, разбивка по клиентам
test_tryon.py 30+ Submit, статус, domain-check, rate limiting
test_security.py 20+ Timing attack, rate limit, domain whitelist, SSRF
test_health.py 10+ Health endpoint, readiness probe, санитизация ошибок
test_errors.py 10+ Согласованность формата ошибок по всем endpoints
test_dependencies.py 15+ Пути валидации API-ключей, fail-open/closed
test_billing_service.py 15+ Проверка лимитов генераций, upsert лога использования
test_image_processor.py 31 Валидация форматов, пайплайн, resize, quality ramp, decompression bomb
test_webhooks.py 23 CRUD, HMAC-подпись, доставка, изоляция клиентов
test_graceful_shutdown.py 14 Обработчики сигналов, отслеживание запросов, пометка прерванных задач
test_multitenancy.py 23 Изоляция данных, скоупинг API-ключей, соблюдение квот плана
test_service_units.py 20+ Unit-тесты без состояния: config, auth, billing, queue
test_alerting_service.py 15+ Форматирование Telegram-алертов, dedup, условия срабатывания
test_embed_domain_check.py 10+ Endpoint domain-check для pre-flight виджета
test_portal.py 30+ Логин портала, скоупинг задач, usage, CRUD вебхуков, изоляция тенантов
test_storage_service.py 15 LocalStorageBackend: save/delete/list, фабрика, неизвестный бэкенд
test_s3_storage.py 8 S3StorageBackend через moto mock (пропускается без boto3/moto)
test_hardening.py 12 Изоляция статуса задач, флаг ENABLE_DOCS, Cache-Control, санитизация health
test_observability.py 15+ Поля health, счётчики Prometheus, dedup вебхуков, readiness
test_sandbox.py 32 CRUD garment/model в sandbox, пагинация, публичные endpoints, auth enforcement

Итого: 507+ тестов, покрытие ≥97%


8. Мониторинг и наблюдаемость

Пайплайн метрик

Admin API  ──► Prometheus (скрап каждые 15 с)  ──► Grafana
ML Worker  ──► Pushgateway                     ──► Grafana

admin-api публикует GET /metrics (формат Prometheus), заблокированный от публичного доступа на уровне nginx. ML Worker, не имея HTTP-сервера, пушит метрики в Pushgateway после каждой задачи. Prometheus скрапит Pushgateway в том же 15-секундном цикле.

В admin/app/metrics.py определены четыре Prometheus-счётчика:

Счётчик Метки Назначение
tryon_submitted_total client_id Каждый принятый запрос примерки
tryon_completed_total client_id, status Завершения задач (успех/ошибка)
tryon_rate_limited_total reason Срабатывания rate limit (per_ip или limit_exceeded)
cleanup_files_deleted_total нет WebP-файлы, удалённые сервисом очистки

Grafana-дашборды

Три дашборда авто-провижнятся из grafana/dashboards/ — ручная настройка после деплоя не требуется:

  1. Platform Overview — частота запросов, throughput задач, частота ошибок
  2. Client Usage — количество генераций на клиента, утилизация квоты
  3. Worker Health — возраст heartbeat ML Worker, глубина очереди, задержка fal.ai

Health Endpoints

Endpoint Назначение Сценарий использования
GET /health Детальный статус (БД, Redis, heartbeat воркера, ключ fal.ai, бэкенд хранилища) Мониторинговые дашборды
GET /readiness Строгий probe: 200 если БД+Redis доступны, 503 иначе Health-проверки load balancer

Worker Heartbeat и обнаружение зависших задач

ML Worker каждые 30 секунд пишет ключ worker_heartbeat в Redis. Endpoint /health показывает возраст этого ключа — если воркер умирает, heartbeat устаревает и появляется на дашборде.

Фоновая задача в admin-api (_run_stale_job_sweep()) каждые 60 секунд помечает задачи как failed, если они застряли: - статус processing более 15 минут → failed (воркер умер во время выполнения) - статус pending более 30 минут → failed (гонка BRPOP — задача была подхвачена, но не обработана)

Telegram-алерты

alerting_service.py отправляет структурированные алерты в Telegram-бот. Redis SETNX с TTL 1 час на ключ алерта предотвращает дублирование уведомлений одного типа. Уровни severity: info, warning, critical.


9. Рабочий процесс деплоя

«git push → 20 секунд → прод»

Пайплайн деплоя полностью автоматизирован. Полный цикл:

Разработчик: git push origin main
                ▼ (GitHub Actions: ci.yml)
        1. ruff check admin/
        2. pytest --cov-fail-under=94
           (сервисные контейнеры postgres:15 + redis:7)
        3. docker build admin-api
        4. docker build ml-worker
                │ (всё прошло)
                ▼ (GitHub Actions: deploy.yml, триггер workflow_run)
        5. SSH на 185.246.222.107
        6. git pull origin main
        7. diff HEAD~1 HEAD — находим изменённые сервисы
        8. docker compose build <изменённые сервисы>
        9. docker compose up -d <изменённые сервисы>
        10. curl https://ziex-tryon.com/health → 200
        Прод обновлён ✓

Скрипты деплоя

Все скрипты лежат в scripts/ и спроектированы идемпотентными и аудируемыми.

generate-secrets.sh — запускается один раз перед первым деплоем:

bash scripts/generate-secrets.sh
# Генерирует: JWT_SECRET_KEY, POSTGRES_PASSWORD,
#             GRAFANA_ADMIN_PASSWORD, FIRST_ADMIN_PASSWORD
# Выводит готовые записи для .env

deploy.sh --prod — полный production-деплой:

bash scripts/deploy.sh --prod
# 1. Pre-flight: проверяет, что DOMAIN задан, TLS-сертификаты существуют, .env полный
# 2. Build: docker compose -f ... -f docker-compose.prod.yml build
# 3. Упорядоченный запуск: postgres → redis → admin-api → ml-worker → nginx → мониторинг
# 4. Проверка здоровья: поллит /health до 200 или timeout

health-check.sh — верификация после деплоя:

bash scripts/health-check.sh
# Проверяет: API /health 200, embed.js доступен, frontend загружается
# Ловит: случайно открытые порты 8000, 9090, 3000

backup.sh — бэкап PostgreSQL с манифестом:

bash scripts/backup.sh
# Создаёт: /opt/tryon-saas-backups/backup_20260523_143022.dump
# Создаёт: /opt/tryon-saas-backups/backup_20260523_143022.manifest.json
# Добавить в crontab для автоматических ночных бэкапов

restore.sh — восстановление из дампа:

bash scripts/restore.sh --drop-existing --yes backup_20260523_143022.dump
# Удаляет существующую БД, восстанавливает из дампа


10. Функциональность приложения

Панель администратора (admin.ziex-tryon.com)

Панель администратора — React SPA, предоставляющий полный операционный контроль:

  • Управление клиентами — создание, редактирование, приостановка, активация клиентов; назначение планов; сброс месячного использования
  • Управление API-ключами — генерация и отзыв API-ключей для каждого клиента; просмотр префиксов ключей и дат создания
  • Управление планами — создание тарифных планов с месячными лимитами генераций; назначение клиентам
  • Дашборд задач — просмотр всех задач примерки с фильтрацией по клиенту, статусу, диапазону дат
  • Статистика — обзор платформы (всего клиентов, задач); разбивка использования по клиентам; realtime-метрики

Портал клиентов (app.ziex-tryon.com)

Отдельный React SPA (отдельный Zustand store, отдельный Axios instance, отдельный JWT scope) для самообслуживания клиентов:

  • Дашборд — текущий план, использование в этом месяце, остаток генераций
  • Задачи — просмотр истории задач примерки с результатами
  • API-ключи — просмотр активных ключей
  • Вебхуки — регистрация endpoint'ов для получения событий job.completed и job.failed
  • Использование — история месячных генераций

Встраиваемый виджет (tryon-embed.js)

Виджет на чистом JavaScript без зависимостей, встраиваемый на страницы товаров:

<script src="https://api.ziex-tryon.com/embed/tryon-embed.js"
        data-api-key="tryon_xxxxxxxxxxxx"></script>
  • Рендерится внутри Shadow DOM — полностью изолирован от CSS страницы-хоста
  • Паттерн взаимодействия FAB → модальное окно (кнопка-шарик → полноэкранное наложение)
  • SPA-совместим: MutationObserver отслеживает изменения страницы; переинициализируется при смене URL товара
  • Автоопределяет изображения товаров из тегов <img> по паттернам известных e-commerce платформ
  • Настраиваем: TryOnWidget.init({ apiUrl, container, theme })

Sandbox (sandbox.ziex-tryon.com)

Изолированная среда для тестирования интеграции без влияния на production-данные:

  • Предзагруженная библиотека изображений одежды и моделей
  • Полный API примерки (отдельный rate limiting)
  • Возвращает реалистичные ответы включая результирующие изображения

11. Известные ограничения и отложенные задачи

Эти пункты были явно рассмотрены и отложены с письменным обоснованием — это не пробелы в осознании, а сознательные продуктовые решения:

Восстановление пароля

Endpoint POST /auth/forgot-password отсутствует. Восстановление доступа администратора требует прямого доступа к БД. Это намеренно: на текущем этапе (один технический администратор) интеграция с email-провайдером выходит за рамки scope. Путь восстановления задокументирован в ops-runbook.

Rate Limiting на смену пароля

POST /auth/change-password не имеет rate limiting. Поверхность атаки ограничена 15-минутным окном access-токена. Rate limiting включён в бэклог следующего спринта.

S3 ACL не установлен

S3StorageBackend.put_object() не устанавливает ACL="public-read". Для AWS S3 необходимо настраивать публичную политику bucket'а отдельно. Cloudflare R2 (production-бэкенд хранилища) не использует ACL — там применяются настройки публичного доступа на уровне bucket'а.

Shutdown воркера во время поллинга

stop_grace_period: 35s Docker'а меньше, чем потолок поллинга fal.ai в 600 с. Задача, которую прерывает SIGTERM в процессе инференса, будет убита. Проблема задокументирована; решение — увеличить stop_grace_period до 610 с.


12. Метрики масштаба

Метрика Значение
Строк кода на Python (backend) ~6 000
Строк кода на TypeScript (frontend) ~4 000
Строк тестов ~5 000
Файлов тестов 26
Тестовых утверждений 507+
API endpoints 35+
Docker-сервисов 8
Server-блоков nginx 5+
Находок аудита безопасности 41
Закрытых находок 38
Сознательно отложено 3
Покрытие кода ≥97%
Фаз аудита 2
CI/CD автоматизация Полная (lint + test + build + deploy)
Время от git push до прода ~20 секунд
Поддоменов настроено 6
Grafana-дашбордов 3
Alembic-миграций 2

13. Заключение

Платформа TryOn SaaS строилась с production-готовностью как главным ограничением, а не скоростью разработки. Каждое проектное решение — от выбора PyJWT вместо python-jose (активная поддержка), до прямого использования bcrypt вместо passlib (несовместимость с 5.x API), до триггера workflow_run в CI/CD (корректное межфайловое ожидание) — принималось осознанно и документировалось.

Результат — платформа, которая:

  • Правильно обрабатывает multi-tenancy — изоляция данных между клиентами обеспечивается на каждом уровне (запросы к БД, ключи Redis, JWT scopes)
  • Деградирует корректно — обнаружение зависших задач, graceful shutdown с дрейнингом активных запросов, мониторинг heartbeat воркера
  • Наблюдаема — каждое значимое событие логируется через structlog с request_id, который прослеживается от nginx через API до ML Worker и fal.ai
  • Аудируема — 507+ тестов, 41 находка аудита безопасности, задокументированные отложенные задачи с письменным обоснованием
  • Деплоится безопасно — CI контролирует каждый деплой, health-проверки гейтируют каждое развёртывание, pre-flight скрипты валидируют секреты до любых изменений контейнеров