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

ADR 0006: Отдельные клиентские приложения и фильтрация контента по appId

Дата: 2026-06-06 Статус: Accepted

На какие вопросы отвечает

  • Почему consumer и merchant — это два разных Flutter-приложения, а не одно с переключателем роли?
  • Что такое appId и какие значения он принимает (wallet, closeloop, merchant)?
  • Как один пользователь/контент привязывается к приложению — где сервер фильтрует каталог мини-аппов и ленту блога по appId?
  • Почему merchant-приложение не может само завершить регистрацию?
  • Что будет, если мини-апп таргетирован только на closeloop, а его открывают из merchant-приложения?

Контекст

В экосистеме OneWallet есть несколько Flutter-клиентов с разной аудиторией и набором функций: потребительский кошелёк, closeloop-приложение (замкнутый контур) и merchant-приложение (POS). Все они используют один и тот же Serverpod-клиент и общий UI-пакет, но обращаются к одному Auth Center.

Альтернатива «одно приложение со всеми ролями» отвергнута: у consumer и merchant разные политики онбординга (merchant/agent создаются только администратором, без self-registration), разный UX (POS vs кошелёк) и разный набор разрешённых эндпоинтов. Смешение ролей в одной сборке усложняет аудит доступа и публикацию в сторах.

Решение: каждое приложение объявляет константу appId, которую сервер использует и для контроля доступа (какой accountType допустим), и для фильтрации контента (каталог мини-аппов, лента блога).

Решение

Значения appId и матрица доступа

appId передаётся клиентом в запросах к Auth Center. Допустимые типы аккаунта на приложение заданы в _appAccountTypes (onewallet_base_server/lib/src/auth/email_idp_endpoint.dart):

appId Приложение Разрешённые accountType
wallet onewallet_base_flutter (основной потребительский кошелёк) consumer, wallet
closeloop one_loop_app (замкнутый контур) consumer, agent
merchant one_merchant_app (POS) merchant

При login сервер вызывает _assertAppAllowed(accountType, appId) до проверки пароля — если тип аккаунта не входит в список для данного appId, вход отклоняется. При finishRegistration самостоятельную регистрацию могут завершить только приложения, у которых в списке есть consumer; для merchant это запрещено (аккаунты merchant/agent заводит администратор).

Фильтрация мини-аппов по appId

Каталог мини-аппов фильтруется строго по полю targetApps приложения (miniapp_endpoint.dart, метод _matchesTargetApp):

  • targetApps == null или пустой список → мини-апп виден во всех клиентах;
  • иначе — виден только если appId входит в JSON-массив targetApps.

Та же проверка применяется и при выдаче LAUNCH-JWT для запуска конкретного мини-аппа: если appId клиента не разрешён для мини-аппа, запуск не выдаётся.

Фильтрация ленты блога

Фильтрация по appId/targetApps реализована только для каталога мини-аппов (см. выше); для блога она пока не реализована.

BlogEndpoint.getCatalog принимает appId ('wallet' | 'closeloop' | 'merchant') как параметр, но не использует его при формировании ленты: значение не передаётся в _queryFeed, а SQL-фильтр (WHERE над view v_blog_post) работает только по locale, категории, локации и тегам пользователя. В таблице blog.post_groups нет колонки targetApps, поэтому привязать пост к конкретному приложению сейчас нечем.

То есть appId для блога — принимается, но фильтрация ленты по нему не работает; это потенциальный задел на будущее (по аналогии с _matchesTargetApp для мини-аппов), а не действующее поведение.

flowchart TD
  L[one_loop_app<br/>appId=closeloop] -->|login/getCatalog| AC[Auth Center]
  M[one_merchant_app<br/>appId=merchant] -->|login/getCatalog| AC
  AC -->|_assertAppAllowed| ACC{accountType<br/>допустим?}
  ACC -->|нет| ERR[NotAuthorized]
  ACC -->|да| F[фильтр контента по appId]
  F -->|_matchesTargetApp| MINI[каталог мини-аппов]
  F -.->|appId принят, но НЕ фильтрует| BLOG[лента блога]

Пример

Мини-апп с targetApps = ["closeloop"]:

  • открытие из one_loop_app (appId=closeloop) → _matchesTargetApp вернёт true, мини-апп показан, LAUNCH-JWT выдан;
  • открытие из one_merchant_app (appId=merchant) → false, мини-аппа нет в каталоге и запуск не выдаётся.

Попытка login мерчант-аккаунтом из one_loop_app (appId=closeloop): тип merchant не входит в ['consumer','agent'] → отказ ещё до проверки пароля.

Последствия

  • Контроль доступа и фильтрация контента централизованы на сервере: клиент не может «обмануть» каталог, подменив UI — решает appId-логика Auth Center.
  • Новое приложение = новая запись в _appAccountTypes плюс (при необходимости) значение в targetApps мини-аппов; забыли добавить appId → вход и каталог работать не будут.
  • Дефолт appId на сервере (logincloseloop, finishRegistrationwallet) сохраняет обратную совместимость со старыми клиентами, но это компромисс: новый клиент обязан слать свой реальный appId.
  • Текущая проверка appId применяется на login/registration и при выдаче каталога/JWT; per-request enforcement на каждый эндпоинт помечен в коде как TODO(Phase 2) (хранить appId в AppSession и валидировать в requireAccountTypeAndApp).
  • targetApps без значения = «виден всем» — это сознательный дефолт; для закрытых мини-аппов поле нужно заполнять явно.

Ссылки

  • Реализация: onewallet_base_server/lib/src/auth/email_idp_endpoint.dart (_appAccountTypes, _assertAppAllowed), onewallet_base_server/lib/src/endpoints/miniapp_endpoint.dart (_matchesTargetApp), onewallet_base_server/lib/src/endpoints/blog_endpoint.dart (getCatalog)
  • Бизнес-документация: ../business/03-apps.md
  • Dev-документация: ../dev/02-services.md
  • Смежные ADR: 0002 единый HMAC