From b073e99ca7a8e9eb43b85798380c255c63feccd0 Mon Sep 17 00:00:00 2001 From: nns <278048682+nurdotnet@users.noreply.github.com> Date: Wed, 6 May 2026 11:15:47 +0500 Subject: [PATCH] =?UTF-8?q?feat(roles):=20=D1=82=D1=80=D0=B8=20=D1=81?= =?UTF-8?q?=D0=B8=D1=81=D1=82=D0=B5=D0=BC=D0=BD=D1=8B=D0=B5=20=D1=80=D0=BE?= =?UTF-8?q?=D0=BB=D0=B8=20=E2=80=94=20Admin/Cashier/Storekeeper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Менеджер/Закупщик/Бухгалтер сидились как кастомные шаблоны вместе с организацией, но при этом числились системными в DevDataSeeder (IsSystem=false), что путало UI (где-то нельзя было менять, где-то можно было). Юзер хочет: при создании новой орги только три системные роли (Admin, Storekeeper, Cashier), все остальные роли администратор создаёт сам. — SystemRoles.Manager убран. Identity-роли сидируются: SuperAdmin, Admin, Cashier, Storekeeper. — EmployeeRoles tenant-сидер создаёт только три записи (все IsSystem=true, все три не редактируются и не удаляются обычным юзером — это правило уже работало для Админа/Кассира, теперь покрывает Кладовщика). — Authorize(Roles = ".. Manager ..") убрано из всех контроллеров (13 файлов): Sales/RetailSales, Catalog/{Products,ProductImages,ProductGroups, Counterparties,UnitsOfMeasure,RetailPoints,PriceTypes,Stores}, Purchases/Supplies, Organizations/{Employees,EmployeeRoles, OrganizationSettings}. Существующие организации с уже созданными «Менеджер/Закупщик/ Бухгалтер» записями НЕ затрагиваются — сидер пропускает org если в ней уже есть роли (anyRole short-circuit). При желании админ может удалить эти кастомные роли через UI. --- .../Catalog/CounterpartiesController.cs | 4 +- .../Catalog/PriceTypesController.cs | 4 +- .../Catalog/ProductGroupsController.cs | 4 +- .../Catalog/ProductImagesController.cs | 6 +- .../Controllers/Catalog/ProductsController.cs | 10 ++-- .../Catalog/RetailPointsController.cs | 4 +- .../Controllers/Catalog/StoresController.cs | 4 +- .../Catalog/UnitsOfMeasureController.cs | 4 +- .../Organizations/EmployeeRolesController.cs | 4 +- .../Organizations/EmployeesController.cs | 4 +- .../OrganizationSettingsController.cs | 2 +- .../Purchases/SuppliesController.cs | 10 ++-- .../Sales/RetailSalesController.cs | 10 ++-- src/food-market.api/Seed/DevDataSeeder.cs | 60 +++---------------- .../Identity/Role.cs | 5 +- 15 files changed, 48 insertions(+), 87 deletions(-) diff --git a/src/food-market.api/Controllers/Catalog/CounterpartiesController.cs b/src/food-market.api/Controllers/Catalog/CounterpartiesController.cs index 7c9aa6c..8edfe19 100644 --- a/src/food-market.api/Controllers/Catalog/CounterpartiesController.cs +++ b/src/food-market.api/Controllers/Catalog/CounterpartiesController.cs @@ -69,7 +69,7 @@ public async Task> Get(Guid id, CancellationToken c.BankName, c.BankAccount, c.Bik, c.ContactPerson, c.Notes); } - [HttpPost, Authorize(Roles = "Admin,Manager,Storekeeper")] + [HttpPost, Authorize(Roles = "Admin,Storekeeper")] public async Task> Create([FromBody] CounterpartyInput input, CancellationToken ct) { var e = Apply(new Counterparty(), input); @@ -78,7 +78,7 @@ public async Task> Create([FromBody] CounterpartyI return CreatedAtAction(nameof(Get), new { id = e.Id }, await ProjectAsync(e.Id, ct)); } - [HttpPut("{id:guid}"), Authorize(Roles = "Admin,Manager,Storekeeper")] + [HttpPut("{id:guid}"), Authorize(Roles = "Admin,Storekeeper")] public async Task Update(Guid id, [FromBody] CounterpartyInput input, CancellationToken ct) { var e = await _db.Counterparties.FirstOrDefaultAsync(x => x.Id == id, ct); diff --git a/src/food-market.api/Controllers/Catalog/PriceTypesController.cs b/src/food-market.api/Controllers/Catalog/PriceTypesController.cs index 6b66d8b..4d5150f 100644 --- a/src/food-market.api/Controllers/Catalog/PriceTypesController.cs +++ b/src/food-market.api/Controllers/Catalog/PriceTypesController.cs @@ -49,7 +49,7 @@ public async Task> Get(Guid id, CancellationToken ct) return p is null ? NotFound() : new PriceTypeDto(p.Id, p.Name, p.IsRequired, p.IsSystem, p.IsRetail, p.SortOrder); } - [HttpPost, Authorize(Roles = "Admin,Manager")] + [HttpPost, Authorize(Roles = "Admin")] public async Task> Create([FromBody] PriceTypeInput input, CancellationToken ct) { if (input.IsRetail) @@ -72,7 +72,7 @@ await _db.PriceTypes.Where(p => p.IsRetail) new PriceTypeDto(e.Id, e.Name, e.IsRequired, e.IsSystem, e.IsRetail, e.SortOrder)); } - [HttpPut("{id:guid}"), Authorize(Roles = "Admin,Manager")] + [HttpPut("{id:guid}"), Authorize(Roles = "Admin")] public async Task Update(Guid id, [FromBody] PriceTypeInput input, CancellationToken ct) { var e = await _db.PriceTypes.FirstOrDefaultAsync(x => x.Id == id, ct); diff --git a/src/food-market.api/Controllers/Catalog/ProductGroupsController.cs b/src/food-market.api/Controllers/Catalog/ProductGroupsController.cs index d54d406..0b9ecc8 100644 --- a/src/food-market.api/Controllers/Catalog/ProductGroupsController.cs +++ b/src/food-market.api/Controllers/Catalog/ProductGroupsController.cs @@ -56,7 +56,7 @@ public async Task> Get(Guid id, CancellationToken return g is null ? NotFound() : new ProductGroupDto(g.Id, g.Name, g.ParentId, g.Path, g.SortOrder, g.MarkupPercent, g.OrganizationId); } - [HttpPost, Authorize(Roles = "Admin,Manager")] + [HttpPost, Authorize(Roles = "Admin")] public async Task> Create([FromBody] ProductGroupInput input, CancellationToken ct) { var path = await BuildPathAsync(input.ParentId, input.Name, ct); @@ -72,7 +72,7 @@ public async Task> Create([FromBody] ProductGroupI new ProductGroupDto(e.Id, e.Name, e.ParentId, e.Path, e.SortOrder, e.MarkupPercent, e.OrganizationId)); } - [HttpPut("{id:guid}"), Authorize(Roles = "Admin,Manager,SuperAdmin")] + [HttpPut("{id:guid}"), Authorize(Roles = "Admin,SuperAdmin")] public async Task Update(Guid id, [FromBody] ProductGroupInput input, CancellationToken ct) { var e = await _db.ProductGroups.FirstOrDefaultAsync(x => x.Id == id, ct); diff --git a/src/food-market.api/Controllers/Catalog/ProductImagesController.cs b/src/food-market.api/Controllers/Catalog/ProductImagesController.cs index 4408fa2..aaccdd9 100644 --- a/src/food-market.api/Controllers/Catalog/ProductImagesController.cs +++ b/src/food-market.api/Controllers/Catalog/ProductImagesController.cs @@ -49,7 +49,7 @@ public async Task>> List(Guid productId, Ca return images; } - [HttpPost, Authorize(Roles = "Admin,Manager,Storekeeper")] + [HttpPost, Authorize(Roles = "Admin,Storekeeper")] [RequestSizeLimit(MaxBytes)] public async Task> Upload(Guid productId, IFormFile file, CancellationToken ct) { @@ -90,7 +90,7 @@ public async Task> Upload(Guid productId, IFormFile file, return new ImageDto(entity.Id, entity.Url, entity.IsMain, entity.SortOrder); } - [HttpDelete("{imageId:guid}"), Authorize(Roles = "Admin,Manager,Storekeeper")] + [HttpDelete("{imageId:guid}"), Authorize(Roles = "Admin,Storekeeper")] public async Task Delete(Guid productId, Guid imageId, CancellationToken ct) { var image = await _db.ProductImages.FirstOrDefaultAsync(i => i.Id == imageId && i.ProductId == productId, ct); @@ -128,7 +128,7 @@ public async Task Delete(Guid productId, Guid imageId, Cancellati return NoContent(); } - [HttpPost("{imageId:guid}/main"), Authorize(Roles = "Admin,Manager,Storekeeper")] + [HttpPost("{imageId:guid}/main"), Authorize(Roles = "Admin,Storekeeper")] public async Task SetMain(Guid productId, Guid imageId, CancellationToken ct) { var image = await _db.ProductImages.FirstOrDefaultAsync(i => i.Id == imageId && i.ProductId == productId, ct); diff --git a/src/food-market.api/Controllers/Catalog/ProductsController.cs b/src/food-market.api/Controllers/Catalog/ProductsController.cs index 1248014..10d42dd 100644 --- a/src/food-market.api/Controllers/Catalog/ProductsController.cs +++ b/src/food-market.api/Controllers/Catalog/ProductsController.cs @@ -195,7 +195,7 @@ public async Task> Get(Guid id, CancellationToken ct) return p is null ? NotFound() : p; } - [HttpPost, Authorize(Roles = "Admin,Manager,Storekeeper")] + [HttpPost, Authorize(Roles = "Admin,Storekeeper")] public async Task> Create([FromBody] ProductInput input, CancellationToken ct) { if (input.Barcodes is null || input.Barcodes.Count == 0) @@ -232,7 +232,7 @@ public async Task> Create([FromBody] ProductInput input return CreatedAtAction(nameof(Get), new { id = e.Id }, dto); } - [HttpPut("{id:guid}"), Authorize(Roles = "Admin,Manager,Storekeeper")] + [HttpPut("{id:guid}"), Authorize(Roles = "Admin,Storekeeper")] public async Task Update(Guid id, [FromBody] ProductInput input, CancellationToken ct) { if (input.Barcodes is null || input.Barcodes.Count == 0) @@ -312,7 +312,7 @@ public async Task Update(Guid id, [FromBody] ProductInput input, /// «Привести розничную к себестоимости»: ставит дефолтную /// розничную цену = ceil(Cost * (1 + Group.MarkupPercent/100)). Если у /// группы товара не задан MarkupPercent — 400 с подсказкой. - [HttpPost("{id:guid}/recalc-retail"), Authorize(Roles = "Admin,Manager,Storekeeper")] + [HttpPost("{id:guid}/recalc-retail"), Authorize(Roles = "Admin,Storekeeper")] public async Task RecalcRetail(Guid id, CancellationToken ct) { var p = await _db.Products @@ -361,7 +361,7 @@ public async Task RecalcRetail(Guid id, CancellationToken ct) return Ok(new { retail = newRetail }); } - [HttpDelete("{id:guid}"), Authorize(Roles = "Admin,Manager")] + [HttpDelete("{id:guid}"), Authorize(Roles = "Admin")] public async Task Delete(Guid id, CancellationToken ct) { var e = await _db.Products.FirstOrDefaultAsync(x => x.Id == id, ct); @@ -378,7 +378,7 @@ public record DuplicateProductRef(Guid ProductId, string ProductName, string? Ar /// организации. Уникальный индекс это запрещает в новых записях, но реальная /// БД может содержать исторические дубли (например, после ручной правки). /// Используется UI чистки (/admin/cleanup) и отчётом MoySklad-импорта. - [HttpGet("barcode-duplicates"), Authorize(Roles = "Admin,Manager")] + [HttpGet("barcode-duplicates"), Authorize(Roles = "Admin")] public async Task>> BarcodeDuplicates(CancellationToken ct) { var rows = await _db.ProductBarcodes diff --git a/src/food-market.api/Controllers/Catalog/RetailPointsController.cs b/src/food-market.api/Controllers/Catalog/RetailPointsController.cs index edbeb16..839f907 100644 --- a/src/food-market.api/Controllers/Catalog/RetailPointsController.cs +++ b/src/food-market.api/Controllers/Catalog/RetailPointsController.cs @@ -56,7 +56,7 @@ public async Task> Get(Guid id, CancellationToken c r.Address, r.Phone, r.FiscalSerial, r.FiscalRegNumber, r.IsActive); } - [HttpPost, Authorize(Roles = "Admin,Manager")] + [HttpPost, Authorize(Roles = "Admin")] public async Task> Create([FromBody] RetailPointInput input, CancellationToken ct) { var store = await _db.Stores.FirstOrDefaultAsync(s => s.Id == input.StoreId, ct); @@ -76,7 +76,7 @@ public async Task> Create([FromBody] RetailPointInp e.Address, e.Phone, e.FiscalSerial, e.FiscalRegNumber, e.IsActive)); } - [HttpPut("{id:guid}"), Authorize(Roles = "Admin,Manager")] + [HttpPut("{id:guid}"), Authorize(Roles = "Admin")] public async Task Update(Guid id, [FromBody] RetailPointInput input, CancellationToken ct) { var e = await _db.RetailPoints.FirstOrDefaultAsync(x => x.Id == id, ct); diff --git a/src/food-market.api/Controllers/Catalog/StoresController.cs b/src/food-market.api/Controllers/Catalog/StoresController.cs index a464bdb..3f0e98d 100644 --- a/src/food-market.api/Controllers/Catalog/StoresController.cs +++ b/src/food-market.api/Controllers/Catalog/StoresController.cs @@ -55,7 +55,7 @@ public async Task> Get(Guid id, CancellationToken ct) return x is null ? NotFound() : new StoreDto(x.Id, x.Name, x.Code, x.Address, x.Phone, x.ManagerName, x.IsMain, x.IsActive); } - [HttpPost, Authorize(Roles = "Admin,Manager")] + [HttpPost, Authorize(Roles = "Admin")] public async Task> Create([FromBody] StoreInput input, CancellationToken ct) { if (input.IsMain) @@ -73,7 +73,7 @@ public async Task> Create([FromBody] StoreInput input, Ca new StoreDto(e.Id, e.Name, e.Code, e.Address, e.Phone, e.ManagerName, e.IsMain, e.IsActive)); } - [HttpPut("{id:guid}"), Authorize(Roles = "Admin,Manager")] + [HttpPut("{id:guid}"), Authorize(Roles = "Admin")] public async Task Update(Guid id, [FromBody] StoreInput input, CancellationToken ct) { var e = await _db.Stores.FirstOrDefaultAsync(x => x.Id == id, ct); diff --git a/src/food-market.api/Controllers/Catalog/UnitsOfMeasureController.cs b/src/food-market.api/Controllers/Catalog/UnitsOfMeasureController.cs index 93fb608..372cf98 100644 --- a/src/food-market.api/Controllers/Catalog/UnitsOfMeasureController.cs +++ b/src/food-market.api/Controllers/Catalog/UnitsOfMeasureController.cs @@ -48,7 +48,7 @@ public async Task> Get(Guid id, CancellationToken return u is null ? NotFound() : new UnitOfMeasureDto(u.Id, u.Code, u.Name, u.Description, u.OrganizationId); } - [HttpPost, Authorize(Roles = "Admin,Manager")] + [HttpPost, Authorize(Roles = "Admin")] public async Task> Create([FromBody] UnitOfMeasureInput input, CancellationToken ct) { var e = new UnitOfMeasure @@ -63,7 +63,7 @@ public async Task> Create([FromBody] UnitOfMeasur new UnitOfMeasureDto(e.Id, e.Code, e.Name, e.Description, e.OrganizationId)); } - [HttpPut("{id:guid}"), Authorize(Roles = "Admin,Manager,SuperAdmin")] + [HttpPut("{id:guid}"), Authorize(Roles = "Admin,SuperAdmin")] public async Task Update(Guid id, [FromBody] UnitOfMeasureInput input, CancellationToken ct) { var e = await _db.UnitsOfMeasure.FirstOrDefaultAsync(x => x.Id == id, ct); diff --git a/src/food-market.api/Controllers/Organizations/EmployeeRolesController.cs b/src/food-market.api/Controllers/Organizations/EmployeeRolesController.cs index 18abcde..91b80d6 100644 --- a/src/food-market.api/Controllers/Organizations/EmployeeRolesController.cs +++ b/src/food-market.api/Controllers/Organizations/EmployeeRolesController.cs @@ -55,7 +55,7 @@ public async Task> Get(Guid id, CancellationToken return r is null ? NotFound() : new EmployeeRoleDto(r.Id, r.Name, r.Description, r.IsSystem, r.SortOrder, r.Permissions); } - [HttpPost, Authorize(Roles = "SuperAdmin,Admin,Manager")] + [HttpPost, Authorize(Roles = "SuperAdmin,Admin")] public async Task> Create([FromBody] EmployeeRoleInput input, CancellationToken ct) { var role = new EmployeeRole @@ -72,7 +72,7 @@ public async Task> Create([FromBody] EmployeeRoleI new EmployeeRoleDto(role.Id, role.Name, role.Description, role.IsSystem, role.SortOrder, role.Permissions)); } - [HttpPut("{id:guid}"), Authorize(Roles = "SuperAdmin,Admin,Manager")] + [HttpPut("{id:guid}"), Authorize(Roles = "SuperAdmin,Admin")] public async Task Update(Guid id, [FromBody] EmployeeRoleInput input, CancellationToken ct) { var r = await _db.EmployeeRoles.FirstOrDefaultAsync(x => x.Id == id, ct); diff --git a/src/food-market.api/Controllers/Organizations/EmployeesController.cs b/src/food-market.api/Controllers/Organizations/EmployeesController.cs index e2a6659..299a8cf 100644 --- a/src/food-market.api/Controllers/Organizations/EmployeesController.cs +++ b/src/food-market.api/Controllers/Organizations/EmployeesController.cs @@ -94,7 +94,7 @@ public async Task> Get(Guid id, CancellationToken ct) return dto is null ? NotFound() : Ok(dto); } - [HttpPost, Authorize(Roles = "SuperAdmin,Admin,Manager")] + [HttpPost, Authorize(Roles = "SuperAdmin,Admin")] public async Task> Create([FromBody] EmployeeInput input, CancellationToken ct) { var orgId = _tenant.OrganizationId ?? throw new InvalidOperationException("No tenant."); @@ -148,7 +148,7 @@ public async Task> Create([FromBody] Employee return new EmployeeCreateResult(dto!, tempPassword); } - [HttpPut("{id:guid}"), Authorize(Roles = "SuperAdmin,Admin,Manager")] + [HttpPut("{id:guid}"), Authorize(Roles = "SuperAdmin,Admin")] public async Task Update(Guid id, [FromBody] EmployeeInput input, CancellationToken ct) { var orgId = _tenant.OrganizationId ?? throw new InvalidOperationException("No tenant."); diff --git a/src/food-market.api/Controllers/Organizations/OrganizationSettingsController.cs b/src/food-market.api/Controllers/Organizations/OrganizationSettingsController.cs index 9fb072b..4f0f6d3 100644 --- a/src/food-market.api/Controllers/Organizations/OrganizationSettingsController.cs +++ b/src/food-market.api/Controllers/Organizations/OrganizationSettingsController.cs @@ -65,7 +65,7 @@ public async Task> Get(CancellationToken ct) return Project(o, vat); } - [HttpPut("settings"), Authorize(Roles = "Admin,Manager")] + [HttpPut("settings"), Authorize(Roles = "Admin")] public async Task> Update([FromBody] OrgSettingsInput input, CancellationToken ct) { var orgId = _tenant.OrganizationId ?? throw new InvalidOperationException("No tenant."); diff --git a/src/food-market.api/Controllers/Purchases/SuppliesController.cs b/src/food-market.api/Controllers/Purchases/SuppliesController.cs index 9af1577..e3fd24d 100644 --- a/src/food-market.api/Controllers/Purchases/SuppliesController.cs +++ b/src/food-market.api/Controllers/Purchases/SuppliesController.cs @@ -121,7 +121,7 @@ public async Task> Get(Guid id, CancellationToken ct) return dto is null ? NotFound() : Ok(dto); } - [HttpPost, Authorize(Roles = "Admin,Manager,Storekeeper")] + [HttpPost, Authorize(Roles = "Admin,Storekeeper")] public async Task> Create([FromBody] SupplyInput input, CancellationToken ct) { if (input.Lines is null || input.Lines.Count == 0) @@ -164,7 +164,7 @@ public async Task> Create([FromBody] SupplyInput input, return CreatedAtAction(nameof(Get), new { id = supply.Id }, dto); } - [HttpPut("{id:guid}"), Authorize(Roles = "Admin,Manager,Storekeeper")] + [HttpPut("{id:guid}"), Authorize(Roles = "Admin,Storekeeper")] public async Task Update(Guid id, [FromBody] SupplyInput input, CancellationToken ct) { if (input.Lines is null || input.Lines.Count == 0) @@ -208,7 +208,7 @@ public async Task Update(Guid id, [FromBody] SupplyInput input, C return NoContent(); } - [HttpDelete("{id:guid}"), Authorize(Roles = "Admin,Manager,Storekeeper")] + [HttpDelete("{id:guid}"), Authorize(Roles = "Admin,Storekeeper")] public async Task Delete(Guid id, CancellationToken ct) { var supply = await _db.Supplies.FirstOrDefaultAsync(s => s.Id == id, ct); @@ -220,7 +220,7 @@ public async Task Delete(Guid id, CancellationToken ct) return NoContent(); } - [HttpPost("{id:guid}/post"), Authorize(Roles = "Admin,Manager,Storekeeper")] + [HttpPost("{id:guid}/post"), Authorize(Roles = "Admin,Storekeeper")] public async Task Post(Guid id, CancellationToken ct) { var supply = await _db.Supplies.Include(s => s.Lines).FirstOrDefaultAsync(s => s.Id == id, ct); @@ -322,7 +322,7 @@ private void SetDefaultRetail(foodmarket.Domain.Catalog.Product p, decimal value } } - [HttpPost("{id:guid}/unpost"), Authorize(Roles = "Admin,Manager,Storekeeper")] + [HttpPost("{id:guid}/unpost"), Authorize(Roles = "Admin,Storekeeper")] public async Task Unpost(Guid id, CancellationToken ct) { var supply = await _db.Supplies.Include(s => s.Lines).FirstOrDefaultAsync(s => s.Id == id, ct); diff --git a/src/food-market.api/Controllers/Sales/RetailSalesController.cs b/src/food-market.api/Controllers/Sales/RetailSalesController.cs index 087292f..279b337 100644 --- a/src/food-market.api/Controllers/Sales/RetailSalesController.cs +++ b/src/food-market.api/Controllers/Sales/RetailSalesController.cs @@ -194,7 +194,7 @@ public async Task> Get(Guid id, CancellationToken ct return dto is null ? NotFound() : Ok(dto); } - [HttpPost, Authorize(Roles = "Admin,Manager,Cashier")] + [HttpPost, Authorize(Roles = "Admin,Cashier")] public async Task> Create([FromBody] RetailSaleInput input, CancellationToken ct) { var number = await GenerateNumberAsync(input.Date, ct); @@ -221,7 +221,7 @@ public async Task> Create([FromBody] RetailSaleInput return CreatedAtAction(nameof(Get), new { id = sale.Id }, dto); } - [HttpPut("{id:guid}"), Authorize(Roles = "Admin,Manager,Cashier")] + [HttpPut("{id:guid}"), Authorize(Roles = "Admin,Cashier")] public async Task Update(Guid id, [FromBody] RetailSaleInput input, CancellationToken ct) { var sale = await _db.RetailSales.Include(s => s.Lines).FirstOrDefaultAsync(s => s.Id == id, ct); @@ -249,7 +249,7 @@ public async Task Update(Guid id, [FromBody] RetailSaleInput inpu return NoContent(); } - [HttpDelete("{id:guid}"), Authorize(Roles = "Admin,Manager")] + [HttpDelete("{id:guid}"), Authorize(Roles = "Admin")] public async Task Delete(Guid id, CancellationToken ct) { var sale = await _db.RetailSales.FirstOrDefaultAsync(s => s.Id == id, ct); @@ -261,7 +261,7 @@ public async Task Delete(Guid id, CancellationToken ct) return NoContent(); } - [HttpPost("{id:guid}/post"), Authorize(Roles = "Admin,Manager,Cashier")] + [HttpPost("{id:guid}/post"), Authorize(Roles = "Admin,Cashier")] public async Task Post(Guid id, CancellationToken ct) { var sale = await _db.RetailSales.Include(s => s.Lines).FirstOrDefaultAsync(s => s.Id == id, ct); @@ -289,7 +289,7 @@ public async Task Post(Guid id, CancellationToken ct) return NoContent(); } - [HttpPost("{id:guid}/unpost"), Authorize(Roles = "Admin,Manager")] + [HttpPost("{id:guid}/unpost"), Authorize(Roles = "Admin")] public async Task Unpost(Guid id, CancellationToken ct) { var sale = await _db.RetailSales.Include(s => s.Lines).FirstOrDefaultAsync(s => s.Id == id, ct); diff --git a/src/food-market.api/Seed/DevDataSeeder.cs b/src/food-market.api/Seed/DevDataSeeder.cs index 403c262..eb2caca 100644 --- a/src/food-market.api/Seed/DevDataSeeder.cs +++ b/src/food-market.api/Seed/DevDataSeeder.cs @@ -30,7 +30,7 @@ public async Task StartAsync(CancellationToken ct) var userMgr = scope.ServiceProvider.GetRequiredService>(); var roleMgr = scope.ServiceProvider.GetRequiredService>(); - foreach (var role in new[] { SystemRoles.SuperAdmin, SystemRoles.Admin, SystemRoles.Manager, SystemRoles.Cashier, SystemRoles.Storekeeper }) + foreach (var role in new[] { SystemRoles.SuperAdmin, SystemRoles.Admin, SystemRoles.Cashier, SystemRoles.Storekeeper }) { if (!await roleMgr.RoleExistsAsync(role)) { @@ -222,10 +222,11 @@ public static async Task SeedTenantReferencesAsync(AppDbContext db, Guid orgId, await SeedEmployeeRolesAsync(db, orgId, ct); } - /// Системные роли (IsSystem=true): Администратор / Менеджер / - /// Кладовщик / Кассир / Закупщик / Бухгалтер. Сидируется один раз - /// per организацию; обновлять не пытаемся, чтобы не сбросить кастомные - /// правки галок которые админ мог сделать. + /// Системные роли (IsSystem=true): только Администратор / Кладовщик / + /// Кассир. Менеджеры/Закупщики/Бухгалтеры и пр. — это кастомные роли, + /// которые создаёт администратор орги сам через UI «Настроить права ролей». + /// Системные нельзя редактировать или удалить из UI; только просмотр. + /// Сидируется один раз per организацию. private static async Task SeedEmployeeRolesAsync(AppDbContext db, Guid orgId, CancellationToken ct) { var anyRole = await db.EmployeeRoles.IgnoreQueryFilters().AnyAsync(r => r.OrganizationId == orgId, ct); @@ -239,28 +240,12 @@ private static async Task SeedEmployeeRolesAsync(AppDbContext db, Guid orgId, Ca IsSystem = true, SortOrder = 0, Permissions = RolePermissions.All(), }; - // Менеджер/Кладовщик/Закупщик/Бухгалтер — кастомные шаблоны (IsSystem=false), - // юзер может удалить или подкрутить под себя. Системные только Администратор + Кассир. - var manager = new EmployeeRole - { - OrganizationId = orgId, - Name = "Менеджер", - Description = "Управление каталогом, документами и контрагентами", - IsSystem = false, SortOrder = 10, - Permissions = new RolePermissions - { - ProductsView = true, ProductsEdit = true, ProductGroupsManage = true, PriceTypesManage = true, - SuppliesView = true, SuppliesEdit = true, SuppliesPost = true, - CounterpartiesView = true, CounterpartiesEdit = true, - ReportsView = true, StocksView = true, - }, - }; var keeper = new EmployeeRole { OrganizationId = orgId, Name = "Кладовщик", Description = "Приёмки, инвентаризация, остатки", - IsSystem = false, SortOrder = 20, + IsSystem = true, SortOrder = 10, Permissions = new RolePermissions { ProductsView = true, @@ -273,7 +258,7 @@ private static async Task SeedEmployeeRolesAsync(AppDbContext db, Guid orgId, Ca OrganizationId = orgId, Name = "Кассир", Description = "Только работа на кассе. Без доступа к веб-админке.", - IsSystem = true, SortOrder = 30, + IsSystem = true, SortOrder = 20, Permissions = new RolePermissions { ProductsView = true, @@ -282,35 +267,8 @@ private static async Task SeedEmployeeRolesAsync(AppDbContext db, Guid orgId, Ca // RetailSalesRefund по умолчанию false — админ включит при необходимости }, }; - var buyer = new EmployeeRole - { - OrganizationId = orgId, - Name = "Закупщик", - Description = "Заказы поставщикам и приёмка товара", - IsSystem = false, SortOrder = 40, - Permissions = new RolePermissions - { - ProductsView = true, - SuppliesView = true, SuppliesEdit = true, - CounterpartiesView = true, CounterpartiesEdit = true, - }, - }; - var accountant = new EmployeeRole - { - OrganizationId = orgId, - Name = "Бухгалтер", - Description = "Просмотр всех данных и отчётов, без редактирования", - IsSystem = false, SortOrder = 50, - Permissions = new RolePermissions - { - ProductsView = true, - SuppliesView = true, - CounterpartiesView = true, - ReportsView = true, StocksView = true, - }, - }; - db.EmployeeRoles.AddRange(admin, manager, keeper, cashier, buyer, accountant); + db.EmployeeRoles.AddRange(admin, keeper, cashier); await db.SaveChangesAsync(ct); } diff --git a/src/food-market.infrastructure/Identity/Role.cs b/src/food-market.infrastructure/Identity/Role.cs index 29b6c06..93b2bdb 100644 --- a/src/food-market.infrastructure/Identity/Role.cs +++ b/src/food-market.infrastructure/Identity/Role.cs @@ -10,9 +10,12 @@ public class Role : IdentityRole public static class SystemRoles { + // Tenant-scoped: только три системные роли. Менеджер/Закупщик/Бухгалтер + // и любые другие — это кастомные роли, создаются администратором орги + // вручную (требование юзера, см. коммит). public const string Admin = "Admin"; - public const string Manager = "Manager"; public const string Cashier = "Cashier"; public const string Storekeeper = "Storekeeper"; + // Platform-level: только SuperAdmin платформы. public const string SuperAdmin = "SuperAdmin"; }