### Короткий план единой системы доступа (policy-based authorization) - Суть: выносим все проверки вида packId/admin/подписка/покупка/enable в единый слой Authorization Policies, а в хендлерах используем декларативные «гарды»/хелперы. В итоге в эндпоинтах остаётся одна строка вида `await access.require(PackAction.view, packId);`. ВАЖНО: Можно удалить v1 ручки!!! ## 1) Модель прав и ресурсов - **Resource**: `Pack`, `Card`, `Test`, `Purchase`, `AdminArea`, … - **Actions** на ресурсы: - `PackAction`: `list`, `view`, `cards`, `tests`, `edit`, `delete`, `buy` - `AdminAction`: `access` - **AccessContext**: `user`, `request`, `query`, `path`, `now`, флаги окружения. - **AccessResult**: `allowed | denied(reason)`. Минимум ENUM’ы и типы в `lib/api/authorize/acl_types.dart`. ## 2) Политики (policy layer) - **PackAccessPolicy** единственное место истины для всех проверок паков: - `isAdmin` → allow all pack actions - публичные правила (например, preview pack id=10 для `view`) - `enabled`/наличие в базе → deny с маскировкой 404 для приватных - доступ по покупке/подписке для `view/cards/tests` - редактирование/удаление только админам - Аналогично при необходимости: `CardPolicy`, `TestPolicy`, `AdminPolicy`. Реализация в `lib/api/authorize/policies/pack_policy.dart` и пр. ## 3) Сервис авторизации - **AccessService** координирует политики: роутит ресурс+действие → нужную политику. - API: - `Future require(ResourceAction action, {resourceId})` - `Future can(ResourceAction action, {resourceId})` - Варианты `requirePack(PackAction action, String packId)` и т.п. Файл: `lib/api/authorize/access_service.dart`. ## 4) Единая загрузка моделей (resource loader) - Вынести загрузку моделей и кэширование в **ResourceLoader**: - `getPackModel(packId)` (с кэшем на запрос) + флаги `exists/enabled`. - Позволяет политикам не дергать базу по 3 раза. - Хранить в `Request.context` для reuse по всему запросу. Файл: `lib/api/authorize/resource_loader.dart`. ## 5) Middleware - Уже есть auth middlewares (`appAuthorize`, `authorizeV2`). Добавляем: - `accessMiddleware`: кладёт `AccessService` и `ResourceLoader` в `Request.context`. - Оборачиваем и v1, и v2 пайплайны в `mnemo_shelf.dart`. Пример вставки рядом с авторизацией: - v1: `.addMiddleware(accessMiddleware(getIt.get()))` - v2: после `authorizeV2(...)` — чтобы `user` уже был в `request`. ## 6) Декларативные гарды/хелперы для роутов - Набор хелперов, чтобы минимизировать код в хендлерах: - `withPack(String packId, Future Function(PackModel) fn)` — загрузка/404. - `guardPack(String packId, PackAction action)` — `await access.require(...);` - Комбо-хелпер: `withPackGuarded(packId, PackAction.view, (pack) => ...)` Файл: `lib/api/authorize/route_guards.dart`. ## 7) Миграция эндпоинтов (пошагово) - V2 сначала (меньше риска, уже структурированы): - `GET /api/v2/packs/`: заменить все `if (packId == '10') ...` и проверки подписки на `await access.requirePack(PackAction.view, packId);`, а логику «показывать purchased» — оставить как дополнение к ответу. - `GET /api/v2/packs//cards`: `await access.requirePack(PackAction.cards, packId);` - `GET /api/v2/packs//tests`: `await access.requirePack(PackAction.tests, packId);` - V1 затем: - `lib/packs/packs_api.dart`: убрать `request.user == null`/`packId != '10'`/`admin` проверки; заменить на `access.require...`. - Редактирование/удаление: `await access.require(AdminAction.access)` или `await access.requirePack(PackAction.edit/delete, packId)`. Важно: ошибки политики маппятся централизованно: - `denied(unauthenticated)` → 401 - `denied(not_found_mask)` → 404 - `denied(forbidden)` → 403 - JSON ответ единообразный. ## 8) Единая конфигурация правил - Конфиг для публичных паков и особых кейсов: - Например, `publicPackIds = {'10'}`. - Флаги: «разрешить изображения без авторизации для enabled pack». - Файл: `lib/api/authorize/access_config.dart`. ## 9) Трассировка и аудит - Лёгкий логгер в `AccessService`: кто/что/результат. - Опционально: счётчики Prometheus (allowed/denied per policy). ## 10) Тесты - Unit-тесты для `PackAccessPolicy` с таблицей кейсов: - anon vs user vs admin; enabled vs disabled; purchase vs subscription; public id=10. - Интеграционные для ключевых эндпоинтов v2. ## 11) Гайд по использованию для разработчиков - «В хендлерах не пишем `if (user == null || ...)` — только `access.require(...)`». - Если нужен объект — `withPackGuarded(packId, action, (pack) => ...)`. - Любые изменения правил — только в Policy, нигде больше. ## Короткий пример (идея API) ```dart // handler @Route.get('/packs//cards') Future getPackCards(Request request, String packId) async { final access = request.access; // из middleware await access.requirePack(PackAction.cards, packId); return withPack(packId, (pack) async { final cards = await loadCards(pack); // бизнес-логика без проверок return ok({'items': cards}); }); } ``` ## Что это даст - Единая точка правил: меньше багов, проще изменять доступ. - Читабельные хендлеры без разрозненных `if`. - Предсказуемые коды ошибок и ответы. - Уменьшение дублирования загрузки моделей.