ADR-021: AI Photo Studio — интеграция через cross-origin iframe + postMessage

Контекст

ai-photo-studio — самостоятельный продукт (ai-photo-studio-frontend + ai-photo-studio-backend aka gensvc). На вход — фото блюда + пресет, на выход — webp 1024×1024, сгенерированный AI (Polza.ai / Gemini Flash Image). Нужна интеграция с erp-admin: маркетолог открывает карточку товара → жмёт «Улучшить с помощью AI» → модалка с фотостудией → результат применяется к product.image_url.

Развилки и решения:

1. Хостинг — same-origin или отдельный домен

  • ✗ same-origin под /photo-studio/ за admin-nginx — пришлось бы править admin nginx + Vite BASE_PATH + копировать сертификат
  • отдельные поддомены ai-photostudio-test.nirbi.ru (frontend) и ai-photostudio-api-test.nirbi.ru (backend)

Прод: ai-photostudio.nirbi.ru / ai-photostudio-api.nirbi.ru.

Минусы — cross-origin, требуется CORS на API + targetOrigin в postMessage. Принимаемо.

2. Auth crypto — JWKS RS256 или shared HS256

ERP auth-service подписывает JWT через HS256 (симметричный секрет в app.jwt.secret). AI-backend изначально умел JWKS RS256.

  • ✗ переезд auth-service на RS256 — затрагивает 12 сервисов, ~3-5 дней работы
  • ✗ JWKS endpoint только для gensvc-токенов — отдельная RSA-пара, +код в auth-service
  • gensvc мигрирует под HS256 (коллега со стороны фотостудии). gensvc запускается с AUTH_MODE=local + JWT_LOCAL_SECRET = app.jwt.secret из ERP auth-service

Секрет лежит на VPS в env обоих сервисов, в git не попадает. Серверный, не клиентский — leak risk минимальный.

3. Permissions — endpoint user-service или claim в JWT

По Roles.md в ERP permissions не кладутся в JWT — внутренние сервисы получают через user-service endpoint. Но gensvc — внешний downstream и ходить в user-service не должен.

  • в JWT добавлен claim permissions: [...] — только для внешних потребителей. Внутренние сервисы продолжают использовать endpoint.

JwtService.generateAccessToken принял дополнительный параметр permissions. Все 3 callsites (login, pinLogin, refresh) обновлены.

4. Передача результата из iframe в parent

  • postMessage с явным targetOrigin (parent_origin приходит query-параметром в iframe ?parent_origin=https://erp-test.nirbi.ru). Не * — иначе любой сторонний фрейм может прочитать presigned output_url.
  • Parent фильтрует входящие сообщения по e.origin === VITE_PHOTO_STUDIO_URL.

События:

  • photo-studio:apply{job_id, output_url, preset_id} — родитель скачивает webp по presigned URL и заливает через существующий POST /api/v1/admin/catalog/products/{id}/image (multipart). Reuse уже работающего пути.
  • photo-studio:cancel — родитель закрывает модалку.

5. Embed-mode фотостудии

В iframe не нужны header/nav/«История». Переключение через ?embed=1 в URL → сохраняется в sessionStorage → Shell не рендерит header.

Последствия

Плюсы:

  • gensvc остался самостоятельным продуктом (можно отдать клиенту в чистом виде)
  • erp-admin не зависит от внутренней реализации gensvc
  • postMessage-контракт документирован, расширяем (можно добавить apply-multiple, progress etc.)
  • Auth — никаких новых endpoint’ов в ERP, токен передаётся в iframe через query

Минусы:

  • Нужны отдельные TLS-сертификаты (certbot) и DNS A-records на каждый поддомен — операционная нагрузка на каждом окружении.
  • Cross-origin sessionStorage — admin не может revoked-listить токены фотостудии. Принимаемо: TTL короткий, gensvc валидирует самостоятельно.
  • 24-часовой presigned output_url — если юзер уйдёт на час до клика «Применить», ссылка может истечь. Mitigation: при открытии модалки токен выдан со свежим TTL, на success сразу применяем.

Связанные