feat(roles): три системные роли — Admin/Cashier/Storekeeper
Менеджер/Закупщик/Бухгалтер сидились как кастомные шаблоны вместе с
организацией, но при этом числились системными в 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.
This commit is contained in:
parent
8eceff0bb5
commit
b073e99ca7
|
|
@ -69,7 +69,7 @@ public async Task<ActionResult<CounterpartyDto>> Get(Guid id, CancellationToken
|
||||||
c.BankName, c.BankAccount, c.Bik, c.ContactPerson, c.Notes);
|
c.BankName, c.BankAccount, c.Bik, c.ContactPerson, c.Notes);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost, Authorize(Roles = "Admin,Manager,Storekeeper")]
|
[HttpPost, Authorize(Roles = "Admin,Storekeeper")]
|
||||||
public async Task<ActionResult<CounterpartyDto>> Create([FromBody] CounterpartyInput input, CancellationToken ct)
|
public async Task<ActionResult<CounterpartyDto>> Create([FromBody] CounterpartyInput input, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var e = Apply(new Counterparty(), input);
|
var e = Apply(new Counterparty(), input);
|
||||||
|
|
@ -78,7 +78,7 @@ public async Task<ActionResult<CounterpartyDto>> Create([FromBody] CounterpartyI
|
||||||
return CreatedAtAction(nameof(Get), new { id = e.Id }, await ProjectAsync(e.Id, ct));
|
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<IActionResult> Update(Guid id, [FromBody] CounterpartyInput input, CancellationToken ct)
|
public async Task<IActionResult> Update(Guid id, [FromBody] CounterpartyInput input, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var e = await _db.Counterparties.FirstOrDefaultAsync(x => x.Id == id, ct);
|
var e = await _db.Counterparties.FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ public async Task<ActionResult<PriceTypeDto>> Get(Guid id, CancellationToken ct)
|
||||||
return p is null ? NotFound() : new PriceTypeDto(p.Id, p.Name, p.IsRequired, p.IsSystem, p.IsRetail, p.SortOrder);
|
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<ActionResult<PriceTypeDto>> Create([FromBody] PriceTypeInput input, CancellationToken ct)
|
public async Task<ActionResult<PriceTypeDto>> Create([FromBody] PriceTypeInput input, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (input.IsRetail)
|
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));
|
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<IActionResult> Update(Guid id, [FromBody] PriceTypeInput input, CancellationToken ct)
|
public async Task<IActionResult> Update(Guid id, [FromBody] PriceTypeInput input, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var e = await _db.PriceTypes.FirstOrDefaultAsync(x => x.Id == id, ct);
|
var e = await _db.PriceTypes.FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ public async Task<ActionResult<ProductGroupDto>> 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);
|
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<ActionResult<ProductGroupDto>> Create([FromBody] ProductGroupInput input, CancellationToken ct)
|
public async Task<ActionResult<ProductGroupDto>> Create([FromBody] ProductGroupInput input, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var path = await BuildPathAsync(input.ParentId, input.Name, ct);
|
var path = await BuildPathAsync(input.ParentId, input.Name, ct);
|
||||||
|
|
@ -72,7 +72,7 @@ public async Task<ActionResult<ProductGroupDto>> Create([FromBody] ProductGroupI
|
||||||
new ProductGroupDto(e.Id, e.Name, e.ParentId, e.Path, e.SortOrder, e.MarkupPercent, e.OrganizationId));
|
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<IActionResult> Update(Guid id, [FromBody] ProductGroupInput input, CancellationToken ct)
|
public async Task<IActionResult> Update(Guid id, [FromBody] ProductGroupInput input, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var e = await _db.ProductGroups.FirstOrDefaultAsync(x => x.Id == id, ct);
|
var e = await _db.ProductGroups.FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ public async Task<ActionResult<IReadOnlyList<ImageDto>>> List(Guid productId, Ca
|
||||||
return images;
|
return images;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost, Authorize(Roles = "Admin,Manager,Storekeeper")]
|
[HttpPost, Authorize(Roles = "Admin,Storekeeper")]
|
||||||
[RequestSizeLimit(MaxBytes)]
|
[RequestSizeLimit(MaxBytes)]
|
||||||
public async Task<ActionResult<ImageDto>> Upload(Guid productId, IFormFile file, CancellationToken ct)
|
public async Task<ActionResult<ImageDto>> Upload(Guid productId, IFormFile file, CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
|
@ -90,7 +90,7 @@ public async Task<ActionResult<ImageDto>> Upload(Guid productId, IFormFile file,
|
||||||
return new ImageDto(entity.Id, entity.Url, entity.IsMain, entity.SortOrder);
|
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<IActionResult> Delete(Guid productId, Guid imageId, CancellationToken ct)
|
public async Task<IActionResult> Delete(Guid productId, Guid imageId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var image = await _db.ProductImages.FirstOrDefaultAsync(i => i.Id == imageId && i.ProductId == productId, ct);
|
var image = await _db.ProductImages.FirstOrDefaultAsync(i => i.Id == imageId && i.ProductId == productId, ct);
|
||||||
|
|
@ -128,7 +128,7 @@ public async Task<IActionResult> Delete(Guid productId, Guid imageId, Cancellati
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{imageId:guid}/main"), Authorize(Roles = "Admin,Manager,Storekeeper")]
|
[HttpPost("{imageId:guid}/main"), Authorize(Roles = "Admin,Storekeeper")]
|
||||||
public async Task<IActionResult> SetMain(Guid productId, Guid imageId, CancellationToken ct)
|
public async Task<IActionResult> SetMain(Guid productId, Guid imageId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var image = await _db.ProductImages.FirstOrDefaultAsync(i => i.Id == imageId && i.ProductId == productId, ct);
|
var image = await _db.ProductImages.FirstOrDefaultAsync(i => i.Id == imageId && i.ProductId == productId, ct);
|
||||||
|
|
|
||||||
|
|
@ -195,7 +195,7 @@ public async Task<ActionResult<ProductDto>> Get(Guid id, CancellationToken ct)
|
||||||
return p is null ? NotFound() : p;
|
return p is null ? NotFound() : p;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost, Authorize(Roles = "Admin,Manager,Storekeeper")]
|
[HttpPost, Authorize(Roles = "Admin,Storekeeper")]
|
||||||
public async Task<ActionResult<ProductDto>> Create([FromBody] ProductInput input, CancellationToken ct)
|
public async Task<ActionResult<ProductDto>> Create([FromBody] ProductInput input, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (input.Barcodes is null || input.Barcodes.Count == 0)
|
if (input.Barcodes is null || input.Barcodes.Count == 0)
|
||||||
|
|
@ -232,7 +232,7 @@ public async Task<ActionResult<ProductDto>> Create([FromBody] ProductInput input
|
||||||
return CreatedAtAction(nameof(Get), new { id = e.Id }, dto);
|
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<IActionResult> Update(Guid id, [FromBody] ProductInput input, CancellationToken ct)
|
public async Task<IActionResult> Update(Guid id, [FromBody] ProductInput input, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (input.Barcodes is null || input.Barcodes.Count == 0)
|
if (input.Barcodes is null || input.Barcodes.Count == 0)
|
||||||
|
|
@ -312,7 +312,7 @@ public async Task<IActionResult> Update(Guid id, [FromBody] ProductInput input,
|
||||||
/// <summary>«Привести розничную к себестоимости»: ставит дефолтную
|
/// <summary>«Привести розничную к себестоимости»: ставит дефолтную
|
||||||
/// розничную цену = ceil(Cost * (1 + Group.MarkupPercent/100)). Если у
|
/// розничную цену = ceil(Cost * (1 + Group.MarkupPercent/100)). Если у
|
||||||
/// группы товара не задан MarkupPercent — 400 с подсказкой.</summary>
|
/// группы товара не задан MarkupPercent — 400 с подсказкой.</summary>
|
||||||
[HttpPost("{id:guid}/recalc-retail"), Authorize(Roles = "Admin,Manager,Storekeeper")]
|
[HttpPost("{id:guid}/recalc-retail"), Authorize(Roles = "Admin,Storekeeper")]
|
||||||
public async Task<IActionResult> RecalcRetail(Guid id, CancellationToken ct)
|
public async Task<IActionResult> RecalcRetail(Guid id, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var p = await _db.Products
|
var p = await _db.Products
|
||||||
|
|
@ -361,7 +361,7 @@ public async Task<IActionResult> RecalcRetail(Guid id, CancellationToken ct)
|
||||||
return Ok(new { retail = newRetail });
|
return Ok(new { retail = newRetail });
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{id:guid}"), Authorize(Roles = "Admin,Manager")]
|
[HttpDelete("{id:guid}"), Authorize(Roles = "Admin")]
|
||||||
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var e = await _db.Products.FirstOrDefaultAsync(x => x.Id == id, 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-импорта.</summary>
|
/// Используется UI чистки (/admin/cleanup) и отчётом MoySklad-импорта.</summary>
|
||||||
[HttpGet("barcode-duplicates"), Authorize(Roles = "Admin,Manager")]
|
[HttpGet("barcode-duplicates"), Authorize(Roles = "Admin")]
|
||||||
public async Task<ActionResult<IReadOnlyList<BarcodeDuplicate>>> BarcodeDuplicates(CancellationToken ct)
|
public async Task<ActionResult<IReadOnlyList<BarcodeDuplicate>>> BarcodeDuplicates(CancellationToken ct)
|
||||||
{
|
{
|
||||||
var rows = await _db.ProductBarcodes
|
var rows = await _db.ProductBarcodes
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ public async Task<ActionResult<RetailPointDto>> Get(Guid id, CancellationToken c
|
||||||
r.Address, r.Phone, r.FiscalSerial, r.FiscalRegNumber, r.IsActive);
|
r.Address, r.Phone, r.FiscalSerial, r.FiscalRegNumber, r.IsActive);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost, Authorize(Roles = "Admin,Manager")]
|
[HttpPost, Authorize(Roles = "Admin")]
|
||||||
public async Task<ActionResult<RetailPointDto>> Create([FromBody] RetailPointInput input, CancellationToken ct)
|
public async Task<ActionResult<RetailPointDto>> Create([FromBody] RetailPointInput input, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var store = await _db.Stores.FirstOrDefaultAsync(s => s.Id == input.StoreId, ct);
|
var store = await _db.Stores.FirstOrDefaultAsync(s => s.Id == input.StoreId, ct);
|
||||||
|
|
@ -76,7 +76,7 @@ public async Task<ActionResult<RetailPointDto>> Create([FromBody] RetailPointInp
|
||||||
e.Address, e.Phone, e.FiscalSerial, e.FiscalRegNumber, e.IsActive));
|
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<IActionResult> Update(Guid id, [FromBody] RetailPointInput input, CancellationToken ct)
|
public async Task<IActionResult> Update(Guid id, [FromBody] RetailPointInput input, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var e = await _db.RetailPoints.FirstOrDefaultAsync(x => x.Id == id, ct);
|
var e = await _db.RetailPoints.FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ public async Task<ActionResult<StoreDto>> 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);
|
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<ActionResult<StoreDto>> Create([FromBody] StoreInput input, CancellationToken ct)
|
public async Task<ActionResult<StoreDto>> Create([FromBody] StoreInput input, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (input.IsMain)
|
if (input.IsMain)
|
||||||
|
|
@ -73,7 +73,7 @@ public async Task<ActionResult<StoreDto>> Create([FromBody] StoreInput input, Ca
|
||||||
new StoreDto(e.Id, e.Name, e.Code, e.Address, e.Phone, e.ManagerName, e.IsMain, e.IsActive));
|
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<IActionResult> Update(Guid id, [FromBody] StoreInput input, CancellationToken ct)
|
public async Task<IActionResult> Update(Guid id, [FromBody] StoreInput input, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var e = await _db.Stores.FirstOrDefaultAsync(x => x.Id == id, ct);
|
var e = await _db.Stores.FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ public async Task<ActionResult<UnitOfMeasureDto>> Get(Guid id, CancellationToken
|
||||||
return u is null ? NotFound() : new UnitOfMeasureDto(u.Id, u.Code, u.Name, u.Description, u.OrganizationId);
|
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<ActionResult<UnitOfMeasureDto>> Create([FromBody] UnitOfMeasureInput input, CancellationToken ct)
|
public async Task<ActionResult<UnitOfMeasureDto>> Create([FromBody] UnitOfMeasureInput input, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var e = new UnitOfMeasure
|
var e = new UnitOfMeasure
|
||||||
|
|
@ -63,7 +63,7 @@ public async Task<ActionResult<UnitOfMeasureDto>> Create([FromBody] UnitOfMeasur
|
||||||
new UnitOfMeasureDto(e.Id, e.Code, e.Name, e.Description, e.OrganizationId));
|
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<IActionResult> Update(Guid id, [FromBody] UnitOfMeasureInput input, CancellationToken ct)
|
public async Task<IActionResult> Update(Guid id, [FromBody] UnitOfMeasureInput input, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var e = await _db.UnitsOfMeasure.FirstOrDefaultAsync(x => x.Id == id, ct);
|
var e = await _db.UnitsOfMeasure.FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ public async Task<ActionResult<EmployeeRoleDto>> Get(Guid id, CancellationToken
|
||||||
return r is null ? NotFound() : new EmployeeRoleDto(r.Id, r.Name, r.Description, r.IsSystem, r.SortOrder, r.Permissions);
|
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<ActionResult<EmployeeRoleDto>> Create([FromBody] EmployeeRoleInput input, CancellationToken ct)
|
public async Task<ActionResult<EmployeeRoleDto>> Create([FromBody] EmployeeRoleInput input, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var role = new EmployeeRole
|
var role = new EmployeeRole
|
||||||
|
|
@ -72,7 +72,7 @@ public async Task<ActionResult<EmployeeRoleDto>> Create([FromBody] EmployeeRoleI
|
||||||
new EmployeeRoleDto(role.Id, role.Name, role.Description, role.IsSystem, role.SortOrder, role.Permissions));
|
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<IActionResult> Update(Guid id, [FromBody] EmployeeRoleInput input, CancellationToken ct)
|
public async Task<IActionResult> Update(Guid id, [FromBody] EmployeeRoleInput input, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var r = await _db.EmployeeRoles.FirstOrDefaultAsync(x => x.Id == id, ct);
|
var r = await _db.EmployeeRoles.FirstOrDefaultAsync(x => x.Id == id, ct);
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ public async Task<ActionResult<EmployeeDto>> Get(Guid id, CancellationToken ct)
|
||||||
return dto is null ? NotFound() : Ok(dto);
|
return dto is null ? NotFound() : Ok(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost, Authorize(Roles = "SuperAdmin,Admin,Manager")]
|
[HttpPost, Authorize(Roles = "SuperAdmin,Admin")]
|
||||||
public async Task<ActionResult<EmployeeCreateResult>> Create([FromBody] EmployeeInput input, CancellationToken ct)
|
public async Task<ActionResult<EmployeeCreateResult>> Create([FromBody] EmployeeInput input, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var orgId = _tenant.OrganizationId ?? throw new InvalidOperationException("No tenant.");
|
var orgId = _tenant.OrganizationId ?? throw new InvalidOperationException("No tenant.");
|
||||||
|
|
@ -148,7 +148,7 @@ public async Task<ActionResult<EmployeeCreateResult>> Create([FromBody] Employee
|
||||||
return new EmployeeCreateResult(dto!, tempPassword);
|
return new EmployeeCreateResult(dto!, tempPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{id:guid}"), Authorize(Roles = "SuperAdmin,Admin,Manager")]
|
[HttpPut("{id:guid}"), Authorize(Roles = "SuperAdmin,Admin")]
|
||||||
public async Task<IActionResult> Update(Guid id, [FromBody] EmployeeInput input, CancellationToken ct)
|
public async Task<IActionResult> Update(Guid id, [FromBody] EmployeeInput input, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var orgId = _tenant.OrganizationId ?? throw new InvalidOperationException("No tenant.");
|
var orgId = _tenant.OrganizationId ?? throw new InvalidOperationException("No tenant.");
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ public async Task<ActionResult<OrgSettingsDto>> Get(CancellationToken ct)
|
||||||
return Project(o, vat);
|
return Project(o, vat);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("settings"), Authorize(Roles = "Admin,Manager")]
|
[HttpPut("settings"), Authorize(Roles = "Admin")]
|
||||||
public async Task<ActionResult<OrgSettingsDto>> Update([FromBody] OrgSettingsInput input, CancellationToken ct)
|
public async Task<ActionResult<OrgSettingsDto>> Update([FromBody] OrgSettingsInput input, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var orgId = _tenant.OrganizationId ?? throw new InvalidOperationException("No tenant.");
|
var orgId = _tenant.OrganizationId ?? throw new InvalidOperationException("No tenant.");
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@ public async Task<ActionResult<SupplyDto>> Get(Guid id, CancellationToken ct)
|
||||||
return dto is null ? NotFound() : Ok(dto);
|
return dto is null ? NotFound() : Ok(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost, Authorize(Roles = "Admin,Manager,Storekeeper")]
|
[HttpPost, Authorize(Roles = "Admin,Storekeeper")]
|
||||||
public async Task<ActionResult<SupplyDto>> Create([FromBody] SupplyInput input, CancellationToken ct)
|
public async Task<ActionResult<SupplyDto>> Create([FromBody] SupplyInput input, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (input.Lines is null || input.Lines.Count == 0)
|
if (input.Lines is null || input.Lines.Count == 0)
|
||||||
|
|
@ -164,7 +164,7 @@ public async Task<ActionResult<SupplyDto>> Create([FromBody] SupplyInput input,
|
||||||
return CreatedAtAction(nameof(Get), new { id = supply.Id }, dto);
|
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<IActionResult> Update(Guid id, [FromBody] SupplyInput input, CancellationToken ct)
|
public async Task<IActionResult> Update(Guid id, [FromBody] SupplyInput input, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (input.Lines is null || input.Lines.Count == 0)
|
if (input.Lines is null || input.Lines.Count == 0)
|
||||||
|
|
@ -208,7 +208,7 @@ public async Task<IActionResult> Update(Guid id, [FromBody] SupplyInput input, C
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{id:guid}"), Authorize(Roles = "Admin,Manager,Storekeeper")]
|
[HttpDelete("{id:guid}"), Authorize(Roles = "Admin,Storekeeper")]
|
||||||
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var supply = await _db.Supplies.FirstOrDefaultAsync(s => s.Id == id, ct);
|
var supply = await _db.Supplies.FirstOrDefaultAsync(s => s.Id == id, ct);
|
||||||
|
|
@ -220,7 +220,7 @@ public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id:guid}/post"), Authorize(Roles = "Admin,Manager,Storekeeper")]
|
[HttpPost("{id:guid}/post"), Authorize(Roles = "Admin,Storekeeper")]
|
||||||
public async Task<IActionResult> Post(Guid id, CancellationToken ct)
|
public async Task<IActionResult> Post(Guid id, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var supply = await _db.Supplies.Include(s => s.Lines).FirstOrDefaultAsync(s => s.Id == id, 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<IActionResult> Unpost(Guid id, CancellationToken ct)
|
public async Task<IActionResult> Unpost(Guid id, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var supply = await _db.Supplies.Include(s => s.Lines).FirstOrDefaultAsync(s => s.Id == id, ct);
|
var supply = await _db.Supplies.Include(s => s.Lines).FirstOrDefaultAsync(s => s.Id == id, ct);
|
||||||
|
|
|
||||||
|
|
@ -194,7 +194,7 @@ public async Task<ActionResult<RetailSaleDto>> Get(Guid id, CancellationToken ct
|
||||||
return dto is null ? NotFound() : Ok(dto);
|
return dto is null ? NotFound() : Ok(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost, Authorize(Roles = "Admin,Manager,Cashier")]
|
[HttpPost, Authorize(Roles = "Admin,Cashier")]
|
||||||
public async Task<ActionResult<RetailSaleDto>> Create([FromBody] RetailSaleInput input, CancellationToken ct)
|
public async Task<ActionResult<RetailSaleDto>> Create([FromBody] RetailSaleInput input, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var number = await GenerateNumberAsync(input.Date, ct);
|
var number = await GenerateNumberAsync(input.Date, ct);
|
||||||
|
|
@ -221,7 +221,7 @@ public async Task<ActionResult<RetailSaleDto>> Create([FromBody] RetailSaleInput
|
||||||
return CreatedAtAction(nameof(Get), new { id = sale.Id }, dto);
|
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<IActionResult> Update(Guid id, [FromBody] RetailSaleInput input, CancellationToken ct)
|
public async Task<IActionResult> Update(Guid id, [FromBody] RetailSaleInput input, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var sale = await _db.RetailSales.Include(s => s.Lines).FirstOrDefaultAsync(s => s.Id == id, ct);
|
var sale = await _db.RetailSales.Include(s => s.Lines).FirstOrDefaultAsync(s => s.Id == id, ct);
|
||||||
|
|
@ -249,7 +249,7 @@ public async Task<IActionResult> Update(Guid id, [FromBody] RetailSaleInput inpu
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{id:guid}"), Authorize(Roles = "Admin,Manager")]
|
[HttpDelete("{id:guid}"), Authorize(Roles = "Admin")]
|
||||||
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var sale = await _db.RetailSales.FirstOrDefaultAsync(s => s.Id == id, ct);
|
var sale = await _db.RetailSales.FirstOrDefaultAsync(s => s.Id == id, ct);
|
||||||
|
|
@ -261,7 +261,7 @@ public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id:guid}/post"), Authorize(Roles = "Admin,Manager,Cashier")]
|
[HttpPost("{id:guid}/post"), Authorize(Roles = "Admin,Cashier")]
|
||||||
public async Task<IActionResult> Post(Guid id, CancellationToken ct)
|
public async Task<IActionResult> Post(Guid id, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var sale = await _db.RetailSales.Include(s => s.Lines).FirstOrDefaultAsync(s => s.Id == id, ct);
|
var sale = await _db.RetailSales.Include(s => s.Lines).FirstOrDefaultAsync(s => s.Id == id, ct);
|
||||||
|
|
@ -289,7 +289,7 @@ public async Task<IActionResult> Post(Guid id, CancellationToken ct)
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id:guid}/unpost"), Authorize(Roles = "Admin,Manager")]
|
[HttpPost("{id:guid}/unpost"), Authorize(Roles = "Admin")]
|
||||||
public async Task<IActionResult> Unpost(Guid id, CancellationToken ct)
|
public async Task<IActionResult> Unpost(Guid id, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var sale = await _db.RetailSales.Include(s => s.Lines).FirstOrDefaultAsync(s => s.Id == id, ct);
|
var sale = await _db.RetailSales.Include(s => s.Lines).FirstOrDefaultAsync(s => s.Id == id, ct);
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ public async Task StartAsync(CancellationToken ct)
|
||||||
var userMgr = scope.ServiceProvider.GetRequiredService<UserManager<User>>();
|
var userMgr = scope.ServiceProvider.GetRequiredService<UserManager<User>>();
|
||||||
var roleMgr = scope.ServiceProvider.GetRequiredService<RoleManager<Role>>();
|
var roleMgr = scope.ServiceProvider.GetRequiredService<RoleManager<Role>>();
|
||||||
|
|
||||||
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))
|
if (!await roleMgr.RoleExistsAsync(role))
|
||||||
{
|
{
|
||||||
|
|
@ -222,10 +222,11 @@ public static async Task SeedTenantReferencesAsync(AppDbContext db, Guid orgId,
|
||||||
await SeedEmployeeRolesAsync(db, orgId, ct);
|
await SeedEmployeeRolesAsync(db, orgId, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Системные роли (IsSystem=true): Администратор / Менеджер /
|
/// <summary>Системные роли (IsSystem=true): только Администратор / Кладовщик /
|
||||||
/// Кладовщик / Кассир / Закупщик / Бухгалтер. Сидируется один раз
|
/// Кассир. Менеджеры/Закупщики/Бухгалтеры и пр. — это кастомные роли,
|
||||||
/// per организацию; обновлять не пытаемся, чтобы не сбросить кастомные
|
/// которые создаёт администратор орги сам через UI «Настроить права ролей».
|
||||||
/// правки галок которые админ мог сделать.</summary>
|
/// Системные нельзя редактировать или удалить из UI; только просмотр.
|
||||||
|
/// Сидируется один раз per организацию.</summary>
|
||||||
private static async Task SeedEmployeeRolesAsync(AppDbContext db, Guid orgId, CancellationToken ct)
|
private static async Task SeedEmployeeRolesAsync(AppDbContext db, Guid orgId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var anyRole = await db.EmployeeRoles.IgnoreQueryFilters().AnyAsync(r => r.OrganizationId == orgId, 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,
|
IsSystem = true, SortOrder = 0,
|
||||||
Permissions = RolePermissions.All(),
|
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
|
var keeper = new EmployeeRole
|
||||||
{
|
{
|
||||||
OrganizationId = orgId,
|
OrganizationId = orgId,
|
||||||
Name = "Кладовщик",
|
Name = "Кладовщик",
|
||||||
Description = "Приёмки, инвентаризация, остатки",
|
Description = "Приёмки, инвентаризация, остатки",
|
||||||
IsSystem = false, SortOrder = 20,
|
IsSystem = true, SortOrder = 10,
|
||||||
Permissions = new RolePermissions
|
Permissions = new RolePermissions
|
||||||
{
|
{
|
||||||
ProductsView = true,
|
ProductsView = true,
|
||||||
|
|
@ -273,7 +258,7 @@ private static async Task SeedEmployeeRolesAsync(AppDbContext db, Guid orgId, Ca
|
||||||
OrganizationId = orgId,
|
OrganizationId = orgId,
|
||||||
Name = "Кассир",
|
Name = "Кассир",
|
||||||
Description = "Только работа на кассе. Без доступа к веб-админке.",
|
Description = "Только работа на кассе. Без доступа к веб-админке.",
|
||||||
IsSystem = true, SortOrder = 30,
|
IsSystem = true, SortOrder = 20,
|
||||||
Permissions = new RolePermissions
|
Permissions = new RolePermissions
|
||||||
{
|
{
|
||||||
ProductsView = true,
|
ProductsView = true,
|
||||||
|
|
@ -282,35 +267,8 @@ private static async Task SeedEmployeeRolesAsync(AppDbContext db, Guid orgId, Ca
|
||||||
// RetailSalesRefund по умолчанию false — админ включит при необходимости
|
// 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);
|
await db.SaveChangesAsync(ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,12 @@ public class Role : IdentityRole<Guid>
|
||||||
|
|
||||||
public static class SystemRoles
|
public static class SystemRoles
|
||||||
{
|
{
|
||||||
|
// Tenant-scoped: только три системные роли. Менеджер/Закупщик/Бухгалтер
|
||||||
|
// и любые другие — это кастомные роли, создаются администратором орги
|
||||||
|
// вручную (требование юзера, см. коммит).
|
||||||
public const string Admin = "Admin";
|
public const string Admin = "Admin";
|
||||||
public const string Manager = "Manager";
|
|
||||||
public const string Cashier = "Cashier";
|
public const string Cashier = "Cashier";
|
||||||
public const string Storekeeper = "Storekeeper";
|
public const string Storekeeper = "Storekeeper";
|
||||||
|
// Platform-level: только SuperAdmin платформы.
|
||||||
public const string SuperAdmin = "SuperAdmin";
|
public const string SuperAdmin = "SuperAdmin";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue