- RequiresAdvancedModeAttribute (TypeFilterAttribute → IAsyncAuthorizationFilter): читает Organization.IsAdvancedMode по org_id-claim через AppDbContext, кэш 30 с, инвалидация после переключения. SuperAdmin bypass. На отказ возвращает 403 ProblemDetails с code=advanced-mode-required. - Атрибут навешен на контроллеры: Sales/DemandsController, Inventory/TransfersController, Loyalty/LoyaltyCardsController + LoyaltyProgramsController, Promotions/PromotionsController, Reports/AbcReportController, Admin/OrgAuditLogController, Telegram/TelegramBindingController, Common/UserPresetsController. - И на отдельные действия: Catalog/PriceTypesController POST/PUT/DELETE (GET доступен — товары используют retail-прайс), Catalog/ProductsController POST /bulk-update. - SuperAdmin toggle (шаг 2) инвалидирует кэш фильтра после SaveChangesAsync. - SSO (Auth/ExternalAuthController) гейтится UI, а не атрибутом: endpoint инициируется анонимом без org_id в токене. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
89 lines
7.3 KiB
Markdown
89 lines
7.3 KiB
Markdown
# Simple Mode — прогресс
|
||
|
||
Задача: по умолчанию у всех организаций «Простой режим» — только базовые
|
||
функции розничного учёта. Включить «Расширенный режим» может только
|
||
SuperAdmin и только для конкретной организации. Данные при переключении
|
||
не удаляются; при повторном включении всё видно.
|
||
|
||
Старт: 2026-10-04.
|
||
|
||
## Чек-лист
|
||
|
||
- [x] 1. Domain + миграция `Organization.IsAdvancedMode` (bool, default false, backfill false)
|
||
- [x] 2. API `PUT /api/super-admin/organizations/{id}/advanced-mode` + аудит + `/api/me.advancedMode`; org-admin → 403
|
||
- [x] 3. Серверная защита: `[RequiresAdvancedMode]` → 403 ProblemDetails на скрытых модулях
|
||
- [ ] 4. Web: меню, маршруты, дашборд фильтруются; прямой переход → страница «Недоступно»
|
||
- [ ] 5. Упрощённые формы (товар / контрагент / документы) — скрыть «не нужные» поля, значения сохраняются
|
||
- [ ] 6. SuperAdmin UI — бейдж «Простой/Расширенный» + переключатель с подтверждением
|
||
- [ ] 7. Тесты (unit + integration + e2e); патч quality-watchdog при необходимости
|
||
- [ ] 8. Деплой стейджа + `docs/simple-mode.md`
|
||
|
||
## Простой режим — что видно
|
||
|
||
- Главная: выручка за сегодня, что заканчивается, последние продажи
|
||
- Товары и группы товаров
|
||
- Контрагенты (единый список — поставщики и покупатели)
|
||
- Склад: Приёмка, Оприходование, Списание, Инвентаризация, Возврат поставщику, Остатки
|
||
- Продажи: Продажи, Возвраты покупателей
|
||
- Отчёты: Продажи, Прибыль, Остатки на дату
|
||
- Сотрудники и роли, Настройки магазина (включая ОФД), Импорт товаров
|
||
- 2FA в профиле — остаётся
|
||
|
||
## Расширенный режим — что добавляется
|
||
|
||
- Оптовые отгрузки (Demand)
|
||
- Перемещения между точками
|
||
- Лояльность
|
||
- Промокоды / акции
|
||
- ABC-анализ
|
||
- Аудит организации
|
||
- Несколько типов цен (в простом — одна цена продажи)
|
||
- Bulk-операции и сохранённые пресеты в таблицах
|
||
- Telegram-бот владельца
|
||
- SSO
|
||
- Переключатель языка (в простом только русский)
|
||
- Продвинутые виджеты дашборда
|
||
|
||
## Журнал
|
||
|
||
### 2026-10-04 — Шаг 1 ✓
|
||
|
||
- `Organization.IsAdvancedMode` (bool, default false) + XML-док, что переключает только SuperAdmin.
|
||
- Миграция `20261004100000_Phase29a_IsAdvancedMode` — идемпотентный `ADD COLUMN IF NOT EXISTS` с `DEFAULT false`; все существующие орги получают `false`.
|
||
- Snapshot обновлён (`AppDbContextModelSnapshot.cs`).
|
||
- Применено на dev-БД: 5/5 организаций `IsAdvancedMode = false`.
|
||
- `dotnet build food-market.api` — ✓ (0 error, warnings — старые, не наши).
|
||
|
||
### 2026-10-04 — Шаг 2 ✓
|
||
|
||
- `PUT /api/super-admin/organizations/{id}/advanced-mode { "enabled": bool }` в `SuperAdminOrganizationsController` — `[Authorize(Roles="SuperAdmin")]`. Админ орги получает 403 от стандартного Authorize-фильтра (роль Admin ≠ SuperAdmin); в контроллере сверху уже `[Authorize(Roles="SuperAdmin")]` — не нужно отдельного `Forbid()`.
|
||
- Запись в `SuperAdminAuditLog`: ActionType `EnableAdvancedMode` / `DisableAdvancedMode`, Description, Changes `{"before":…,"after":…}`, IpAddress. Идемпотентный переключатель: если состояние совпадает — 200 без аудит-записи.
|
||
- Архивную орг переключать нельзя — 409.
|
||
- `/api/me` отдаёт `advancedMode` (bool). SuperAdmin без org → false (это ОК — он работает через `/api/super-admin/...`).
|
||
- `OrgRow` / `OrgDetail` в SuperAdmin list / detail — добавлено `IsAdvancedMode`.
|
||
- Маршрут: `/api/super-admin/...` (с дефисом), так как существующий контроллер уже использует этот стиль; задача упоминала `superadmin` без дефиса — выбрал существующий стиль.
|
||
- `dotnet build food-market.api` — ✓ (0 error).
|
||
|
||
### 2026-10-04 — Шаг 3 ✓
|
||
|
||
- `src/food-market.api/Infrastructure/Authorization/RequiresAdvancedModeAttribute.cs`:
|
||
- `[RequiresAdvancedMode]` — `TypeFilterAttribute` вокруг `AdvancedModeFilter : IAsyncAuthorizationFilter`.
|
||
- Читает `IsAdvancedMode` по `org_id`-claim через `AppDbContext` (без доверия к клиентским заголовкам). Кэш in-memory 30 с, инвалидация после переключения.
|
||
- SuperAdmin bypass (чтобы он мог проверять скрытые модули через «войти как»).
|
||
- Отказ — `403 ProblemDetails` с `type`, `title`, `detail` на русском и `extensions.code = "advanced-mode-required"` — фронт различает это 403 от обычного «нет прав».
|
||
- Атрибут навешен на (контроллеры):
|
||
- `Sales/DemandsController` (оптовые отгрузки)
|
||
- `Inventory/TransfersController` (перемещения)
|
||
- `Loyalty/LoyaltyCardsController`, `Loyalty/LoyaltyProgramsController`
|
||
- `Promotions/PromotionsController`
|
||
- `Reports/AbcReportController`
|
||
- `Admin/OrgAuditLogController`
|
||
- `Telegram/TelegramBindingController`
|
||
- `Common/UserPresetsController` (пресеты фильтров)
|
||
- И на отдельные действия:
|
||
- `Catalog/PriceTypesController` — POST / PUT / DELETE (чтение остаётся — товары используют системный retail-прайс)
|
||
- `Catalog/ProductsController.BulkUpdate` (POST `/products/bulk-update`)
|
||
- `Auth/ExternalAuthController` (SSO) — атрибут НЕ навешен: endpoint инициирует OAuth-челлендж для анонима, `org_id`-claim ещё не выставлен. Контроль — на уровне UI (не показывать кнопку «Войти через Google/Microsoft» в простом режиме). Также SSO в простом режиме не ломается при включении — просто кнопка появляется.
|
||
- `SuperAdminOrganizationsController.SetAdvancedMode` вызывает `AdvancedModeFilter.InvalidateCache(orgId)` после `SaveChangesAsync`, чтобы следующий запрос этой org сразу увидел новое значение.
|
||
- `dotnet build food-market.api` — ✓ (0 error).
|