Add role control
This commit is contained in:
@@ -1,21 +1,38 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using ROLAC.API.Authorization;
|
||||
using ROLAC.API.DTOs.Expense;
|
||||
using ROLAC.API.Services;
|
||||
|
||||
namespace ROLAC.API.Controllers;
|
||||
|
||||
// Class is [Authorize] only — any authenticated member may submit/view their OWN
|
||||
// reimbursements. Finance-level privileges (view-all, edit-any, approve) are resolved
|
||||
// against the configurable permission matrix on the "Expenses" module.
|
||||
[ApiController]
|
||||
[Route("api/expenses")]
|
||||
[Authorize]
|
||||
public class ExpensesController : ControllerBase
|
||||
{
|
||||
private readonly IExpenseService _svc;
|
||||
public ExpensesController(IExpenseService svc) => _svc = svc;
|
||||
private readonly IExpenseService _svc;
|
||||
private readonly IPermissionService _perms;
|
||||
public ExpensesController(IExpenseService svc, IPermissionService perms)
|
||||
{
|
||||
_svc = svc;
|
||||
_perms = perms;
|
||||
}
|
||||
|
||||
private bool IsFinance() => User.IsInRole("finance") || User.IsInRole("super_admin");
|
||||
private bool CanViewAll() => IsFinance() || User.IsInRole("pastor");
|
||||
private List<string> Roles() => User.FindAll("role").Select(claim => claim.Value).ToList();
|
||||
private bool IsSuperAdmin() => User.IsInRole(PermissionAuthorizationHandler.SuperAdminRole);
|
||||
|
||||
// Can manage any expense (edit/delete/upload on others' records). Maps to Expenses:Write.
|
||||
private async Task<bool> CanManageAsync() =>
|
||||
IsSuperAdmin() || await _perms.HasPermissionAsync(Roles(), Modules.Expenses, PermissionActions.Write);
|
||||
|
||||
// Can view all expenses (not just own). Maps to Expenses:Read (finance + pastor by default).
|
||||
private async Task<bool> CanViewAllAsync() =>
|
||||
IsSuperAdmin() || await _perms.HasPermissionAsync(Roles(), Modules.Expenses, PermissionActions.Read);
|
||||
|
||||
// User id lives in the "sub" claim (NameClaimType="sub"); NameIdentifier is absent at runtime.
|
||||
private string CurrentUserId() =>
|
||||
@@ -28,7 +45,7 @@ public class ExpensesController : ControllerBase
|
||||
[FromQuery] string? status = null, [FromQuery] DateOnly? from = null, [FromQuery] DateOnly? to = null,
|
||||
[FromQuery] int? subCategoryId = null, [FromQuery] string? statuses = null)
|
||||
{
|
||||
if (!CanViewAll()) return Forbid();
|
||||
if (!await CanViewAllAsync()) return Forbid();
|
||||
return Ok(await _svc.GetPagedAsync(page, pageSize, search, ministryId, categoryGroupId, status, from, to, subCategoryId, statuses));
|
||||
}
|
||||
|
||||
@@ -43,21 +60,21 @@ public class ExpensesController : ControllerBase
|
||||
{
|
||||
var dto = await _svc.GetByIdAsync(id);
|
||||
if (dto is null) return NotFound();
|
||||
if (!CanViewAll() && dto.SubmittedBy != CurrentUserId()) return Forbid();
|
||||
if (!await CanViewAllAsync() && dto.SubmittedBy != CurrentUserId()) return Forbid();
|
||||
return Ok(dto);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Create([FromBody] CreateExpenseRequest r)
|
||||
{
|
||||
try { return Ok(new { id = await _svc.CreateAsync(r, IsFinance()) }); }
|
||||
try { return Ok(new { id = await _svc.CreateAsync(r, await CanManageAsync()) }); }
|
||||
catch (InvalidOperationException ex) { return Conflict(new { message = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpPut("{id:int}")]
|
||||
public async Task<IActionResult> Update(int id, [FromBody] UpdateExpenseRequest r)
|
||||
{
|
||||
try { await _svc.UpdateAsync(id, r, IsFinance()); return NoContent(); }
|
||||
try { await _svc.UpdateAsync(id, r, await CanManageAsync()); return NoContent(); }
|
||||
catch (KeyNotFoundException) { return NotFound(); }
|
||||
catch (InvalidOperationException ex) { return Conflict(new { message = ex.Message }); }
|
||||
}
|
||||
@@ -65,7 +82,7 @@ public class ExpensesController : ControllerBase
|
||||
[HttpDelete("{id:int}")]
|
||||
public async Task<IActionResult> Delete(int id)
|
||||
{
|
||||
try { await _svc.DeleteAsync(id, IsFinance()); return NoContent(); }
|
||||
try { await _svc.DeleteAsync(id, await CanManageAsync()); return NoContent(); }
|
||||
catch (KeyNotFoundException) { return NotFound(); }
|
||||
catch (InvalidOperationException ex) { return Conflict(new { message = ex.Message }); }
|
||||
}
|
||||
@@ -79,7 +96,7 @@ public class ExpensesController : ControllerBase
|
||||
}
|
||||
|
||||
[HttpPost("{id:int}/approve")]
|
||||
[Authorize(Roles = "finance,super_admin")]
|
||||
[HasPermission(Modules.Expenses, PermissionActions.Approve)]
|
||||
public async Task<IActionResult> Approve(int id)
|
||||
{
|
||||
try { await _svc.ApproveAsync(id); return NoContent(); }
|
||||
@@ -88,7 +105,7 @@ public class ExpensesController : ControllerBase
|
||||
}
|
||||
|
||||
[HttpPost("{id:int}/reject")]
|
||||
[Authorize(Roles = "finance,super_admin")]
|
||||
[HasPermission(Modules.Expenses, PermissionActions.Approve)]
|
||||
public async Task<IActionResult> Reject(int id, [FromBody] RejectExpenseRequest r)
|
||||
{
|
||||
try { await _svc.RejectAsync(id, r.ReviewNotes); return NoContent(); }
|
||||
@@ -97,7 +114,7 @@ public class ExpensesController : ControllerBase
|
||||
}
|
||||
|
||||
[HttpPost("{id:int}/pay")]
|
||||
[Authorize(Roles = "finance,super_admin")]
|
||||
[HasPermission(Modules.Expenses, PermissionActions.Approve)]
|
||||
public async Task<IActionResult> Pay(int id, [FromBody] PayExpenseRequest r)
|
||||
{
|
||||
try { await _svc.PayAsync(id, r.CheckNumber, r.PaidAt); return NoContent(); }
|
||||
@@ -115,7 +132,7 @@ public class ExpensesController : ControllerBase
|
||||
try
|
||||
{
|
||||
await using var stream = file.OpenReadStream();
|
||||
await _svc.SaveReceiptAsync(id, stream, file.FileName, IsFinance());
|
||||
await _svc.SaveReceiptAsync(id, stream, file.FileName, await CanManageAsync());
|
||||
return NoContent();
|
||||
}
|
||||
catch (KeyNotFoundException) { return NotFound(); }
|
||||
@@ -127,7 +144,7 @@ public class ExpensesController : ControllerBase
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _svc.OpenReceiptAsync(id, IsFinance());
|
||||
var result = await _svc.OpenReceiptAsync(id, await CanManageAsync());
|
||||
if (result is null) return NotFound();
|
||||
return File(result.Value.stream, result.Value.contentType);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user