Add role control

This commit is contained in:
Chris Chen
2026-06-23 07:19:08 -07:00
parent deff2264a6
commit 870eeec82a
45 changed files with 1923 additions and 165 deletions
+31 -14
View File
@@ -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);
}