Отчёт о проделанной работе
Обзор¶
Этот документ — исчерпывающий отчёт обо всей работе, выполненной при проектировании, разработке, аудите безопасности и выводе в производство платформы 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:
Без этой настройки 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. В режиме разработки эти порты доступны напрямую для отладки.
Пайплайн обработки задач¶
Когда встроенный виджет клиента инициирует запрос примерки:
- Виджет (
tryon-embed.js) захватывает изображения товара и человека, отправляетPOST /api/v1/tryonс API-ключом - Admin API валидирует ключ, проверяет квоту генераций клиента, обрабатывает изображения (изменение размера, снятие EXIF, конвертация в WebP), сохраняет в хранилище, пушит JSON-payload задачи в Redis
- ML Worker подхватывает задачу через
BRPOP(блокирующий pop), вызывает fal.ai FASHN v1.5 - fal.ai выполняет инференс (виртуальная примерка), возвращает URL результата
- Worker обновляет статус задачи в PostgreSQL и вызывает зарегистрированные вебхуки
- Клиент поллит
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, необходимо явно приводить к строке:
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 публикует 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/ — ручная настройка после деплоя не требуется:
- Platform Overview — частота запросов, throughput задач, частота ошибок
- Client Usage — количество генераций на клиента, утилизация квоты
- 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 скрипты валидируют секреты до любых изменений контейнеров