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на сервере (login→closeloop,finishRegistration→wallet) сохраняет обратную совместимость со старыми клиентами, но это компромисс: новый клиент обязан слать свой реальный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