From 95fa37ebdfa86dd40f11ca1662b8879cb5bb9766 Mon Sep 17 00:00:00 2001 From: Chris Chen Date: Fri, 29 May 2026 19:14:18 -0700 Subject: [PATCH] fix(expense): open category read to all authed users; statement lookups via FirstOrDefaultAsync Final-review findings: - ExpenseCategoriesController was finance-only at the class level, but the member self-service reimbursement form reads the category list to populate its dropdown, so members got 403 and could not submit. Open GET to any authenticated user; keep group/subcategory writes finance-only (mirrors MinistriesController). Verified live with a member-role account: reads 200, writes 403, self-submit 200. - MonthlyStatementService Update/Finalize now use FirstOrDefaultAsync for convention consistency with the rest of the service layer. Co-Authored-By: Claude Opus 4.8 --- API/ROLAC.API/Controllers/ExpenseCategoriesController.cs | 9 ++++++++- API/ROLAC.API/Services/MonthlyStatementService.cs | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/API/ROLAC.API/Controllers/ExpenseCategoriesController.cs b/API/ROLAC.API/Controllers/ExpenseCategoriesController.cs index 7e9f327..e039375 100644 --- a/API/ROLAC.API/Controllers/ExpenseCategoriesController.cs +++ b/API/ROLAC.API/Controllers/ExpenseCategoriesController.cs @@ -7,7 +7,8 @@ namespace ROLAC.API.Controllers; [ApiController] [Route("api/expense-categories")] -[Authorize(Roles = "finance,super_admin")] +[Authorize] // read (GetAll) is open to any authenticated user — the member self-service + // reimbursement form needs the category list. Write actions are finance-only below. public class ExpenseCategoriesController : ControllerBase { private readonly IExpenseCategoryService _svc; @@ -18,26 +19,32 @@ public class ExpenseCategoriesController : ControllerBase => Ok(await _svc.GetAllAsync(includeInactive)); [HttpPost("groups")] + [Authorize(Roles = "finance,super_admin")] public async Task CreateGroup([FromBody] CreateExpenseGroupRequest r) => Ok(new { id = await _svc.CreateGroupAsync(r) }); [HttpPut("groups/{id:int}")] + [Authorize(Roles = "finance,super_admin")] public async Task UpdateGroup(int id, [FromBody] UpdateExpenseGroupRequest r) { try { await _svc.UpdateGroupAsync(id, r); return NoContent(); } catch (KeyNotFoundException) { return NotFound(); } } [HttpDelete("groups/{id:int}")] + [Authorize(Roles = "finance,super_admin")] public async Task DeactivateGroup(int id) { try { await _svc.DeactivateGroupAsync(id); return NoContent(); } catch (KeyNotFoundException) { return NotFound(); } } [HttpPost("subcategories")] + [Authorize(Roles = "finance,super_admin")] public async Task CreateSub([FromBody] CreateExpenseSubCategoryRequest r) { try { return Ok(new { id = await _svc.CreateSubCategoryAsync(r) }); } catch (KeyNotFoundException) { return NotFound(); } } [HttpPut("subcategories/{id:int}")] + [Authorize(Roles = "finance,super_admin")] public async Task UpdateSub(int id, [FromBody] UpdateExpenseSubCategoryRequest r) { try { await _svc.UpdateSubCategoryAsync(id, r); return NoContent(); } catch (KeyNotFoundException) { return NotFound(); } } [HttpDelete("subcategories/{id:int}")] + [Authorize(Roles = "finance,super_admin")] public async Task DeactivateSub(int id) { try { await _svc.DeactivateSubCategoryAsync(id); return NoContent(); } catch (KeyNotFoundException) { return NotFound(); } } } diff --git a/API/ROLAC.API/Services/MonthlyStatementService.cs b/API/ROLAC.API/Services/MonthlyStatementService.cs index 4ba1669..2392936 100644 --- a/API/ROLAC.API/Services/MonthlyStatementService.cs +++ b/API/ROLAC.API/Services/MonthlyStatementService.cs @@ -51,7 +51,7 @@ public class MonthlyStatementService : IMonthlyStatementService public async Task UpdateAsync(int id, UpdateMonthlyStatementRequest r) { - var s = await _db.MonthlyStatements.FindAsync(id) + var s = await _db.MonthlyStatements.FirstOrDefaultAsync(x => x.Id == id) ?? throw new KeyNotFoundException($"MonthlyStatement {id} not found."); if (s.IsFinalized) throw new InvalidOperationException("Statement is finalized and cannot be modified."); s.OpeningBalance = r.OpeningBalance; s.TotalOtherIncome = r.TotalOtherIncome; @@ -62,7 +62,7 @@ public class MonthlyStatementService : IMonthlyStatementService public async Task FinalizeAsync(int id) { - var s = await _db.MonthlyStatements.FindAsync(id) + var s = await _db.MonthlyStatements.FirstOrDefaultAsync(x => x.Id == id) ?? throw new KeyNotFoundException($"MonthlyStatement {id} not found."); s.IsFinalized = true; s.FinalizedAt = DateTimeOffset.UtcNow; s.FinalizedBy = CurrentUserId; await _db.SaveChangesAsync();