From 202763e0d06d76d54a9a83dd0cc9cabc71b50c9c Mon Sep 17 00:00:00 2001 From: nns Date: Sun, 4 Oct 2026 11:32:12 +0500 Subject: [PATCH] =?UTF-8?q?feat(s29):=20SuperAdmin=20advanced-mode=20toggl?= =?UTF-8?q?e=20+=20/api/me.advancedMode=20(=D1=88=D0=B0=D0=B3=202/8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PUT /api/super-admin/organizations/{id}/advanced-mode { enabled: bool } — только SuperAdmin, админ орги → 403 от [Authorize(Roles=...)]. - Idempotent: совпадающее состояние → 200 без записи в аудит; разное → запись SuperAdminAuditLog (EnableAdvancedMode / DisableAdvancedMode) с before/after + IP. Архивную org переключать нельзя (409). - /api/me теперь отдаёт advancedMode (bool) — читается из Organization с IgnoreQueryFilters (SuperAdmin без org → false — работает через /api/super-admin/...). - OrgRow / OrgDetail в SuperAdmin list / detail дополнены IsAdvancedMode для бейджа «Простой/Расширенный» (шаг 6). Co-Authored-By: Claude Opus 4.7 --- docs/simple-mode-progress.md | 12 +++++- .../SuperAdminOrganizationsController.cs | 43 +++++++++++++++++-- src/food-market.api/Program.cs | 16 ++++++- 3 files changed, 64 insertions(+), 7 deletions(-) diff --git a/docs/simple-mode-progress.md b/docs/simple-mode-progress.md index bd1a579..229c2b1 100644 --- a/docs/simple-mode-progress.md +++ b/docs/simple-mode-progress.md @@ -10,7 +10,7 @@ SuperAdmin и только для конкретной организации. ## Чек-лист - [x] 1. Domain + миграция `Organization.IsAdvancedMode` (bool, default false, backfill false) -- [ ] 2. API `PUT /api/superadmin/organizations/{id}/advanced-mode` + аудит + `/api/me.advancedMode`; org-admin → 403 +- [x] 2. API `PUT /api/super-admin/organizations/{id}/advanced-mode` + аудит + `/api/me.advancedMode`; org-admin → 403 - [ ] 3. Серверная защита: `[RequiresAdvancedMode]` → 403 ProblemDetails на скрытых модулях - [ ] 4. Web: меню, маршруты, дашборд фильтруются; прямой переход → страница «Недоступно» - [ ] 5. Упрощённые формы (товар / контрагент / документы) — скрыть «не нужные» поля, значения сохраняются @@ -53,3 +53,13 @@ SuperAdmin и только для конкретной организации. - 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). diff --git a/src/food-market.api/Controllers/SuperAdmin/SuperAdminOrganizationsController.cs b/src/food-market.api/Controllers/SuperAdmin/SuperAdminOrganizationsController.cs index 158fa36..ad1fbb8 100644 --- a/src/food-market.api/Controllers/SuperAdmin/SuperAdminOrganizationsController.cs +++ b/src/food-market.api/Controllers/SuperAdmin/SuperAdminOrganizationsController.cs @@ -31,7 +31,8 @@ public record OrgRow( Guid Id, string Name, string CountryCode, bool IsActive, bool IsArchived, DateTime? ArchivedAt, DateTime CreatedAt, int EmployeeCount, int ProductCount, - DateTime? LastLoginAt); + DateTime? LastLoginAt, + bool IsAdvancedMode); public record OrgDetail( Guid Id, string Name, string CountryCode, string? Bin, string? Address, string? Phone, string? Email, @@ -39,7 +40,8 @@ public record OrgDetail( bool IsActive, bool IsArchived, DateTime? ArchivedAt, Guid? AccountOwnerUserId, string? AccountOwnerName, string? AccountOwnerEmail, DateTime CreatedAt, DateTime? UpdatedAt, - int EmployeeCount, int ProductCount, int CounterpartyCount, int SupplyCountThisMonth); + int EmployeeCount, int ProductCount, int CounterpartyCount, int SupplyCountThisMonth, + bool IsAdvancedMode); public record OrgInput( string Name, string CountryCode, string? Bin, string? Address, string? Phone, string? Email, @@ -53,6 +55,7 @@ public record CreateOrgResult(OrgDetail Organization, string AdminEmail, string public record ArchiveRequest(string ConfirmationName); public record DeleteRequest(string ConfirmationName); public record ChangeOwnerRequest(Guid NewOwnerUserId, string Reason); + public record AdvancedModeRequest(bool Enabled); [HttpGet] public async Task>> List( @@ -77,7 +80,8 @@ public record ChangeOwnerRequest(Guid NewOwnerUserId, string Reason); _db.Employees.IgnoreQueryFilters().Count(e => e.OrganizationId == o.Id), _db.Products.IgnoreQueryFilters().Count(p => p.OrganizationId == o.Id), _db.Users.Where(u => u.OrganizationId == o.Id && u.LastLoginAt != null) - .Max(u => (DateTime?)u.LastLoginAt))) + .Max(u => (DateTime?)u.LastLoginAt), + o.IsAdvancedMode)) .ToListAsync(ct); return new PagedResult { Items = items, Total = total, Page = req.Page, PageSize = req.Take }; } @@ -229,6 +233,36 @@ public async Task Delete(Guid id, [FromBody] DeleteRequest req, C return NoContent(); } + /// Sprint 29 — переключатель расширенного режима организации. + /// По умолчанию false → UI/API видят только «простой» набор функций. + /// Переключать может только SuperAdmin; админ организации — 403 (гейтим + /// ролью на уровне контроллера). Idempotent: если состояние совпадает — + /// вернём 200 + IsAdvancedMode, без записи в аудит. + [HttpPut("{id:guid}/advanced-mode")] + public async Task SetAdvancedMode(Guid id, [FromBody] AdvancedModeRequest req, CancellationToken ct) + { + var o = await _db.Organizations.IgnoreQueryFilters().FirstOrDefaultAsync(x => x.Id == id, ct); + if (o is null) return NotFound(); + if (o.IsArchived) + return Conflict(new { error = "Организация в архиве — сначала восстанови её." }); + + if (o.IsAdvancedMode == req.Enabled) + return Ok(new { id = o.Id, isAdvancedMode = o.IsAdvancedMode, changed = false }); + + var before = o.IsAdvancedMode; + o.IsAdvancedMode = req.Enabled; + await _db.SaveChangesAsync(ct); + + var actionType = req.Enabled ? "EnableAdvancedMode" : "DisableAdvancedMode"; + var description = req.Enabled + ? $"Включен расширенный режим «{o.Name}»" + : $"Выключен расширенный режим «{o.Name}»"; + await LogAsync(actionType, o.Id, description, null, + $"{{\"before\":{before.ToString().ToLower()},\"after\":{req.Enabled.ToString().ToLower()}}}", ct); + + return Ok(new { id = o.Id, isAdvancedMode = o.IsAdvancedMode, changed = true }); + } + [HttpPost("{id:guid}/change-owner")] public async Task ChangeOwner(Guid id, [FromBody] ChangeOwnerRequest req, CancellationToken ct) { @@ -267,7 +301,8 @@ public async Task ChangeOwner(Guid id, [FromBody] ChangeOwnerRequ o.DefaultCurrencyId, o.DefaultCurrency?.Code, o.IsActive, o.IsArchived, o.ArchivedAt, o.AccountOwnerUserId, owner?.FullName, owner?.Email, - o.CreatedAt, o.UpdatedAt, emp, prod, cp, supplies); + o.CreatedAt, o.UpdatedAt, emp, prod, cp, supplies, + o.IsAdvancedMode); } private async Task LogAsync(string actionType, Guid orgId, string description, string? reason, string changesJson, CancellationToken ct) diff --git a/src/food-market.api/Program.cs b/src/food-market.api/Program.cs index dc51d7f..dac798c 100644 --- a/src/food-market.api/Program.cs +++ b/src/food-market.api/Program.cs @@ -632,10 +632,21 @@ [new Microsoft.OpenApi.Models.OpenApiSecurityScheme // получает /no-organization fallback вместо белого экрана на /dashboard. bool hasLiveOrg = false; bool hasActiveEmployee = false; + // Sprint 29 (simple-mode): флаг расширенного режима организации. + // false по умолчанию → UI показывает только «простое» меню. + // Включает только SuperAdmin через /api/super-admin/organizations/{id}/advanced-mode. + bool advancedMode = false; if (Guid.TryParse(orgIdRaw, out var orgId)) { - hasLiveOrg = await db.Organizations.IgnoreQueryFilters() - .AnyAsync(o => o.Id == orgId && !o.IsArchived); + var org = await db.Organizations.IgnoreQueryFilters() + .Where(o => o.Id == orgId) + .Select(o => new { o.IsArchived, o.IsAdvancedMode }) + .FirstOrDefaultAsync(); + if (org is not null) + { + hasLiveOrg = !org.IsArchived; + advancedMode = org.IsAdvancedMode; + } if (Guid.TryParse(subRaw, out var sub)) { hasActiveEmployee = await db.Employees.IgnoreQueryFilters() @@ -652,6 +663,7 @@ [new Microsoft.OpenApi.Models.OpenApiSecurityScheme orgId = orgIdRaw, hasLiveOrg, hasActiveEmployee, + advancedMode, }); }).RequireAuthorization();