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
+39 -7
View File
@@ -1,6 +1,9 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using ROLAC.API.DTOs.Auth;
using ROLAC.API.Entities;
using ROLAC.API.Services;
namespace ROLAC.API.Controllers;
@@ -13,11 +16,14 @@ public class AuthController : ControllerBase
private const int CookieMaxAge = 30 * 24 * 60 * 60; // 30 days in seconds
private readonly IAuthService _authService;
private readonly UserManager<AppUser> _userManager;
private readonly IWebHostEnvironment _env;
public AuthController(IAuthService authService, IWebHostEnvironment env)
public AuthController(
IAuthService authService, UserManager<AppUser> userManager, IWebHostEnvironment env)
{
_authService = authService;
_userManager = userManager;
_env = env;
}
@@ -79,17 +85,43 @@ public class AuthController : ControllerBase
}
// -------------------------------------------------------------------------
// GET /api/auth/me (dev-only diagnostic — remove before production)
// GET /api/auth/me
// -------------------------------------------------------------------------
/// <summary>
/// Returns the claims ASP.NET Core parsed from the Bearer token.
/// Use this to debug 401 vs 403: if you get 200 here, the JWT validates
/// fine; if you then get 403 on /api/users the role claim isn't matching.
/// Returns the current user's identity, roles, and effective permissions.
/// The SPA calls this on startup and after an admin edits the permission matrix
/// to refresh what the UI shows — without forcing a re-login.
/// </summary>
[HttpGet("me")]
[Authorize] // no role restriction — just needs a valid JWT
public IActionResult GetMe()
[Authorize]
[ProducesResponseType(typeof(UserInfo), StatusCodes.Status200OK)]
public async Task<IActionResult> GetMe()
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub");
if (string.IsNullOrEmpty(userId))
return Unauthorized();
var user = await _userManager.FindByIdAsync(userId);
if (user is null || !user.IsActive)
return Unauthorized();
var roles = await _userManager.GetRolesAsync(user);
return Ok(await _authService.BuildUserInfoAsync(user, roles));
}
// -------------------------------------------------------------------------
// GET /api/auth/claims (dev-only diagnostic)
// -------------------------------------------------------------------------
/// <summary>
/// Returns the raw claims ASP.NET Core parsed from the Bearer token.
/// Use this to debug 401 vs 403: if you get 200 here, the JWT validates fine;
/// if you then get 403 on a protected endpoint the role/permission isn't matching.
/// </summary>
[HttpGet("claims")]
[Authorize]
public IActionResult GetClaims()
{
var claims = User.Claims
.Select(c => new { c.Type, c.Value })
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using ROLAC.API.Authorization;
using ROLAC.API.DTOs.Disbursement;
using ROLAC.API.Services;
@@ -7,16 +8,18 @@ namespace ROLAC.API.Controllers;
[ApiController]
[Route("api/church-profile")]
[Authorize(Roles = "finance,super_admin")]
[Authorize]
public class ChurchProfileController : ControllerBase
{
private readonly IChurchProfileService _svc;
public ChurchProfileController(IChurchProfileService svc) => _svc = svc;
[HttpGet]
[HasPermission(Modules.ChurchProfile, PermissionActions.Read)]
public async Task<IActionResult> Get() => Ok(await _svc.GetAsync());
[HttpPut]
[HasPermission(Modules.ChurchProfile, PermissionActions.Write)]
public async Task<IActionResult> Update([FromBody] UpdateChurchProfileRequest r)
{
await _svc.UpdateAsync(r);
@@ -1,6 +1,7 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using ROLAC.API.Authorization;
using ROLAC.API.DTOs.Disbursement;
using ROLAC.API.Services;
@@ -8,17 +9,19 @@ namespace ROLAC.API.Controllers;
[ApiController]
[Route("api/disbursements")]
[Authorize(Roles = "finance,super_admin")]
[Authorize]
public class DisbursementsController : ControllerBase
{
private readonly IDisbursementService _svc;
public DisbursementsController(IDisbursementService svc) => _svc = svc;
[HttpGet("approved-unpaid")]
[HasPermission(Modules.Disbursements, PermissionActions.Read)]
public async Task<IActionResult> GetApprovedUnpaid()
=> Ok(await _svc.GetApprovedUnpaidGroupedAsync());
[HttpPost("issue")]
[HasPermission(Modules.Disbursements, PermissionActions.Write)]
public async Task<IActionResult> Issue([FromBody] IssueChecksRequest r)
{
try { return Ok(await _svc.IssueChecksAsync(r)); }
@@ -27,12 +30,14 @@ public class DisbursementsController : ControllerBase
}
[HttpGet("checks")]
[HasPermission(Modules.Disbursements, PermissionActions.Read)]
public async Task<IActionResult> GetRegister(
[FromQuery] int page = 1, [FromQuery] int pageSize = 20, [FromQuery] string? status = null,
[FromQuery] string? search = null, [FromQuery] DateOnly? from = null, [FromQuery] DateOnly? to = null)
=> Ok(await _svc.GetRegisterAsync(page, pageSize, status, search, from, to));
[HttpGet("checks/{id:int}")]
[HasPermission(Modules.Disbursements, PermissionActions.Read)]
public async Task<IActionResult> GetById(int id)
{
var dto = await _svc.GetByIdAsync(id);
@@ -40,6 +45,7 @@ public class DisbursementsController : ControllerBase
}
[HttpPost("checks/{id:int}/void")]
[HasPermission(Modules.Disbursements, PermissionActions.Delete)]
public async Task<IActionResult> Void(int id, [FromBody] VoidCheckRequest r)
{
try { await _svc.VoidAsync(id, r.Reason); return NoContent(); }
@@ -48,6 +54,7 @@ public class DisbursementsController : ControllerBase
}
[HttpGet("checks/{id:int}/pdf")]
[HasPermission(Modules.Disbursements, PermissionActions.Read)]
public async Task<IActionResult> GetPdf(int id)
{
var result = await _svc.RenderPdfAsync(id);
@@ -56,6 +63,7 @@ public class DisbursementsController : ControllerBase
}
[HttpGet("checks/{id:int}/receipt-pdf")]
[HasPermission(Modules.Disbursements, PermissionActions.Read)]
public async Task<IActionResult> GetReceiptPdf(int id)
{
var result = await _svc.RenderReceiptPdfAsync(id);
@@ -64,6 +72,7 @@ public class DisbursementsController : ControllerBase
}
[HttpPost("checks/{id:int}/acknowledge")]
[HasPermission(Modules.Disbursements, PermissionActions.Approve)]
[RequestSizeLimit(5_242_880)]
public async Task<IActionResult> Acknowledge(int id, [FromForm] IFormFile signature, [FromForm] string signedName)
{
@@ -82,6 +91,7 @@ public class DisbursementsController : ControllerBase
}
[HttpGet("checks/{id:int}/signature")]
[HasPermission(Modules.Disbursements, PermissionActions.Read)]
public async Task<IActionResult> GetSignature(int id)
{
try
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using ROLAC.API.Authorization;
using ROLAC.API.DTOs.Expense;
using ROLAC.API.Services;
@@ -19,32 +20,32 @@ public class ExpenseCategoriesController : ControllerBase
=> Ok(await _svc.GetAllAsync(includeInactive));
[HttpPost("groups")]
[Authorize(Roles = "finance,super_admin")]
[HasPermission(Modules.ExpenseCategories, PermissionActions.Write)]
public async Task<IActionResult> CreateGroup([FromBody] CreateExpenseGroupRequest r)
=> Ok(new { id = await _svc.CreateGroupAsync(r) });
[HttpPut("groups/{id:int}")]
[Authorize(Roles = "finance,super_admin")]
[HasPermission(Modules.ExpenseCategories, PermissionActions.Write)]
public async Task<IActionResult> 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")]
[HasPermission(Modules.ExpenseCategories, PermissionActions.Delete)]
public async Task<IActionResult> DeactivateGroup(int id)
{ try { await _svc.DeactivateGroupAsync(id); return NoContent(); } catch (KeyNotFoundException) { return NotFound(); } }
[HttpPost("subcategories")]
[Authorize(Roles = "finance,super_admin")]
[HasPermission(Modules.ExpenseCategories, PermissionActions.Write)]
public async Task<IActionResult> 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")]
[HasPermission(Modules.ExpenseCategories, PermissionActions.Write)]
public async Task<IActionResult> 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")]
[HasPermission(Modules.ExpenseCategories, PermissionActions.Delete)]
public async Task<IActionResult> DeactivateSub(int id)
{ try { await _svc.DeactivateSubCategoryAsync(id); return NoContent(); } catch (KeyNotFoundException) { return NotFound(); } }
}
+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);
}
@@ -1,12 +1,13 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using ROLAC.API.Authorization;
using ROLAC.API.Services;
namespace ROLAC.API.Controllers;
[ApiController]
[Route("api/finance-dashboard")]
[Authorize(Roles = "finance,super_admin")]
[HasPermission(Modules.FinanceDashboard, PermissionActions.Read)]
public class FinanceDashboardController : ControllerBase
{
private readonly IFinanceDashboardService _svc;
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using ROLAC.API.Authorization;
using ROLAC.API.DTOs.Giving;
using ROLAC.API.Services;
@@ -7,17 +8,19 @@ namespace ROLAC.API.Controllers;
[ApiController]
[Route("api/giving-categories")]
[Authorize(Roles = "finance,super_admin")]
[Authorize]
public class GivingCategoriesController : ControllerBase
{
private readonly IGivingCategoryService _svc;
public GivingCategoriesController(IGivingCategoryService svc) => _svc = svc;
[HttpGet]
[HasPermission(Modules.GivingCategories, PermissionActions.Read)]
public async Task<IActionResult> GetAll([FromQuery] bool includeInactive = false)
=> Ok(await _svc.GetAllAsync(includeInactive));
[HttpPost]
[HasPermission(Modules.GivingCategories, PermissionActions.Write)]
public async Task<IActionResult> Create([FromBody] CreateGivingCategoryRequest request)
{
var id = await _svc.CreateAsync(request);
@@ -25,6 +28,7 @@ public class GivingCategoriesController : ControllerBase
}
[HttpPut("{id:int}")]
[HasPermission(Modules.GivingCategories, PermissionActions.Write)]
public async Task<IActionResult> Update(int id, [FromBody] UpdateGivingCategoryRequest request)
{
try { await _svc.UpdateAsync(id, request); return NoContent(); }
@@ -32,6 +36,7 @@ public class GivingCategoriesController : ControllerBase
}
[HttpDelete("{id:int}")]
[HasPermission(Modules.GivingCategories, PermissionActions.Delete)]
public async Task<IActionResult> Deactivate(int id)
{
try { await _svc.DeactivateAsync(id); return NoContent(); }
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using ROLAC.API.Authorization;
using ROLAC.API.DTOs.Giving;
using ROLAC.API.Services;
@@ -7,13 +8,14 @@ namespace ROLAC.API.Controllers;
[ApiController]
[Route("api/givings")]
[Authorize(Roles = "finance,super_admin")]
[Authorize]
public class GivingsController : ControllerBase
{
private readonly IGivingService _svc;
public GivingsController(IGivingService svc) => _svc = svc;
[HttpGet]
[HasPermission(Modules.Givings, PermissionActions.Read)]
public async Task<IActionResult> GetPaged(
[FromQuery] int page = 1, [FromQuery] int pageSize = 20,
[FromQuery] string? search = null, [FromQuery] int? categoryId = null,
@@ -21,6 +23,7 @@ public class GivingsController : ControllerBase
=> Ok(await _svc.GetPagedAsync(page, pageSize, search, categoryId, from, to));
[HttpGet("{id:int}")]
[HasPermission(Modules.Givings, PermissionActions.Read)]
public async Task<IActionResult> GetById(int id)
{
var dto = await _svc.GetByIdAsync(id);
@@ -28,6 +31,7 @@ public class GivingsController : ControllerBase
}
[HttpPost]
[HasPermission(Modules.Givings, PermissionActions.Write)]
public async Task<IActionResult> Create([FromBody] CreateGivingRequest request)
{
var id = await _svc.CreateAsync(request);
@@ -35,6 +39,7 @@ public class GivingsController : ControllerBase
}
[HttpPut("{id:int}")]
[HasPermission(Modules.Givings, PermissionActions.Write)]
public async Task<IActionResult> Update(int id, [FromBody] UpdateGivingRequest request)
{
try { await _svc.UpdateAsync(id, request); return NoContent(); }
@@ -43,6 +48,7 @@ public class GivingsController : ControllerBase
}
[HttpDelete("{id:int}")]
[HasPermission(Modules.Givings, PermissionActions.Delete)]
public async Task<IActionResult> Delete(int id)
{
try { await _svc.DeleteAsync(id); return NoContent(); }
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using ROLAC.API.Authorization;
using ROLAC.API.DTOs.Members;
using ROLAC.API.Services;
@@ -15,7 +16,7 @@ public class MembersController : ControllerBase
/// <summary>GET /api/members?page=1&pageSize=20&search=Chen&status=Member&hasUser=false</summary>
[HttpGet]
[Authorize(Roles = "super_admin,secretary,pastor")]
[HasPermission(Modules.Members, PermissionActions.Read)]
public async Task<IActionResult> GetPaged(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20,
@@ -26,7 +27,7 @@ public class MembersController : ControllerBase
/// <summary>GET /api/members/{id}</summary>
[HttpGet("{id:int}")]
[Authorize(Roles = "super_admin,secretary,pastor")]
[HasPermission(Modules.Members, PermissionActions.Read)]
public async Task<IActionResult> GetById(int id)
{
var dto = await _members.GetByIdAsync(id);
@@ -35,7 +36,7 @@ public class MembersController : ControllerBase
/// <summary>POST /api/members</summary>
[HttpPost]
[Authorize(Roles = "super_admin,secretary")]
[HasPermission(Modules.Members, PermissionActions.Write)]
public async Task<IActionResult> Create([FromBody] CreateMemberRequest request)
{
var id = await _members.CreateAsync(request);
@@ -44,7 +45,7 @@ public class MembersController : ControllerBase
/// <summary>PUT /api/members/{id}</summary>
[HttpPut("{id:int}")]
[Authorize(Roles = "super_admin,secretary")]
[HasPermission(Modules.Members, PermissionActions.Write)]
public async Task<IActionResult> Update(int id, [FromBody] UpdateMemberRequest request)
{
try { await _members.UpdateAsync(id, request); return NoContent(); }
@@ -53,7 +54,7 @@ public class MembersController : ControllerBase
/// <summary>DELETE /api/members/{id} — soft delete</summary>
[HttpDelete("{id:int}")]
[Authorize(Roles = "super_admin,secretary")]
[HasPermission(Modules.Members, PermissionActions.Delete)]
public async Task<IActionResult> Delete(int id)
{
try { await _members.DeleteAsync(id); return NoContent(); }
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using ROLAC.API.Authorization;
using ROLAC.API.DTOs.Expense;
using ROLAC.API.Services;
@@ -7,17 +8,19 @@ namespace ROLAC.API.Controllers;
[ApiController]
[Route("api/monthly-statements")]
[Authorize(Roles = "finance,super_admin")]
[Authorize]
public class MonthlyStatementsController : ControllerBase
{
private readonly IMonthlyStatementService _svc;
public MonthlyStatementsController(IMonthlyStatementService svc) => _svc = svc;
[HttpGet]
[HasPermission(Modules.MonthlyStatements, PermissionActions.Read)]
public async Task<IActionResult> GetAll([FromQuery] int? year = null)
=> Ok(await _svc.GetAllAsync(year));
[HttpGet("{id:int}")]
[HasPermission(Modules.MonthlyStatements, PermissionActions.Read)]
public async Task<IActionResult> GetById(int id)
{
var dto = await _svc.GetByIdAsync(id);
@@ -25,6 +28,7 @@ public class MonthlyStatementsController : ControllerBase
}
[HttpPost]
[HasPermission(Modules.MonthlyStatements, PermissionActions.Write)]
public async Task<IActionResult> Create([FromBody] CreateMonthlyStatementRequest r)
{
try { return Ok(new { id = await _svc.CreateAsync(r) }); }
@@ -32,6 +36,7 @@ public class MonthlyStatementsController : ControllerBase
}
[HttpPut("{id:int}")]
[HasPermission(Modules.MonthlyStatements, PermissionActions.Write)]
public async Task<IActionResult> Update(int id, [FromBody] UpdateMonthlyStatementRequest r)
{
try { await _svc.UpdateAsync(id, r); return NoContent(); }
@@ -40,6 +45,7 @@ public class MonthlyStatementsController : ControllerBase
}
[HttpPost("{id:int}/finalize")]
[HasPermission(Modules.MonthlyStatements, PermissionActions.Approve)]
public async Task<IActionResult> Finalize(int id)
{
try { await _svc.FinalizeAsync(id); return NoContent(); }
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using ROLAC.API.Authorization;
using ROLAC.API.DTOs.Giving;
using ROLAC.API.Services;
@@ -7,23 +8,26 @@ namespace ROLAC.API.Controllers;
[ApiController]
[Route("api/offering-sessions")]
[Authorize(Roles = "finance,super_admin")]
[Authorize]
public class OfferingSessionsController : ControllerBase
{
private readonly IOfferingSessionService _svc;
public OfferingSessionsController(IOfferingSessionService svc) => _svc = svc;
[HttpGet]
[HasPermission(Modules.OfferingSessions, PermissionActions.Read)]
public async Task<IActionResult> GetPaged(
[FromQuery] int page = 1, [FromQuery] int pageSize = 20,
[FromQuery] DateOnly? from = null, [FromQuery] DateOnly? to = null)
=> Ok(await _svc.GetPagedAsync(page, pageSize, from, to));
[HttpGet("check-date")]
[HasPermission(Modules.OfferingSessions, PermissionActions.Read)]
public async Task<IActionResult> CheckDate([FromQuery] DateOnly date)
=> Ok(new { exists = await _svc.DateExistsAsync(date) });
[HttpGet("{id:int}")]
[HasPermission(Modules.OfferingSessions, PermissionActions.Read)]
public async Task<IActionResult> GetById(int id)
{
var dto = await _svc.GetByIdAsync(id);
@@ -31,6 +35,7 @@ public class OfferingSessionsController : ControllerBase
}
[HttpPost]
[HasPermission(Modules.OfferingSessions, PermissionActions.Write)]
public async Task<IActionResult> Create([FromBody] CreateOfferingSessionRequest request)
{
try
@@ -42,6 +47,7 @@ public class OfferingSessionsController : ControllerBase
}
[HttpPost("{id:int}/reopen")]
[HasPermission(Modules.OfferingSessions, PermissionActions.Approve)]
public async Task<IActionResult> Reopen(int id)
{
try { await _svc.ReopenAsync(id); return NoContent(); }
@@ -50,6 +56,7 @@ public class OfferingSessionsController : ControllerBase
}
[HttpPut("{id:int}")]
[HasPermission(Modules.OfferingSessions, PermissionActions.Write)]
public async Task<IActionResult> Replace(int id, [FromBody] CreateOfferingSessionRequest request)
{
try { await _svc.ReplaceAsync(id, request); return NoContent(); }
@@ -60,6 +67,7 @@ public class OfferingSessionsController : ControllerBase
// ── Paper-proof PDF (merged client-side, one file per session) ───────────
[HttpPost("{id:int}/proof")]
[HasPermission(Modules.OfferingSessions, PermissionActions.Write)]
[RequestSizeLimit(52_428_800)] // 50 MB — a merged multi-image PDF is larger than one receipt
public async Task<IActionResult> UploadProof(int id, IFormFile file)
{
@@ -75,6 +83,7 @@ public class OfferingSessionsController : ControllerBase
}
[HttpGet("{id:int}/proof")]
[HasPermission(Modules.OfferingSessions, PermissionActions.Read)]
public async Task<IActionResult> GetProof(int id)
{
try
@@ -87,6 +96,7 @@ public class OfferingSessionsController : ControllerBase
}
[HttpDelete("{id:int}/proof")]
[HasPermission(Modules.OfferingSessions, PermissionActions.Delete)]
public async Task<IActionResult> DeleteProof(int id)
{
try { await _svc.DeleteProofAsync(id); return NoContent(); }
@@ -0,0 +1,45 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using ROLAC.API.Authorization;
using ROLAC.API.DTOs.Permissions;
using ROLAC.API.Services;
namespace ROLAC.API.Controllers;
/// <summary>
/// Admin surface for the configurable RBAC matrix. Restricted to super_admin —
/// the role that governs who governs everyone else.
/// </summary>
[ApiController]
[Route("api/permissions")]
[Authorize(Roles = "super_admin")]
public class PermissionsController : ControllerBase
{
private readonly IPermissionService _permissions;
public PermissionsController(IPermissionService permissions) => _permissions = permissions;
/// <summary>GET /api/permissions — the full role × module matrix.</summary>
[HttpGet]
public async Task<IActionResult> GetMatrix() => Ok(await _permissions.GetMatrixAsync());
/// <summary>GET /api/permissions/catalog — module + action names for the grid.</summary>
[HttpGet("catalog")]
public IActionResult GetCatalog() => Ok(new PermissionCatalogDto
{
Modules = Modules.All,
Actions = PermissionActions.All,
});
/// <summary>PUT /api/permissions/{roleName} — replaces a role's grants.</summary>
[HttpPut("{roleName}")]
public async Task<IActionResult> UpdateRole(string roleName, [FromBody] UpdateRolePermissionsRequest request)
{
try
{
await _permissions.UpsertRoleAsync(roleName, request.Modules);
return NoContent();
}
catch (KeyNotFoundException) { return NotFound(); }
catch (InvalidOperationException ex) { return BadRequest(new { message = ex.Message }); }
}
}
+8 -1
View File
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using ROLAC.API.Authorization;
using ROLAC.API.DTOs.Users;
using ROLAC.API.Services;
@@ -7,7 +8,7 @@ namespace ROLAC.API.Controllers;
[ApiController]
[Route("api/users")]
[Authorize(Roles = "super_admin")]
[Authorize]
public class UsersController : ControllerBase
{
private readonly IUserManagementService _users;
@@ -15,6 +16,7 @@ public class UsersController : ControllerBase
/// <summary>GET /api/users?page=1&pageSize=20&search=Chris</summary>
[HttpGet]
[HasPermission(Modules.Users, PermissionActions.Read)]
public async Task<IActionResult> GetPaged(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20,
@@ -23,6 +25,7 @@ public class UsersController : ControllerBase
/// <summary>GET /api/users/{id}</summary>
[HttpGet("{id}")]
[HasPermission(Modules.Users, PermissionActions.Read)]
public async Task<IActionResult> GetById(string id)
{
var dto = await _users.GetByIdAsync(id);
@@ -34,6 +37,7 @@ public class UsersController : ControllerBase
/// TempPassword is returned ONCE — show it to the admin and never log it.
/// </summary>
[HttpPost]
[HasPermission(Modules.Users, PermissionActions.Write)]
public async Task<IActionResult> Create([FromBody] CreateUserRequest request)
{
try
@@ -49,6 +53,7 @@ public class UsersController : ControllerBase
/// <summary>PUT /api/users/{id} — update email, roles, IsActive</summary>
[HttpPut("{id}")]
[HasPermission(Modules.Users, PermissionActions.Write)]
public async Task<IActionResult> Update(string id, [FromBody] UpdateUserRequest request)
{
try { await _users.UpdateAsync(id, request); return NoContent(); }
@@ -58,6 +63,7 @@ public class UsersController : ControllerBase
/// <summary>DELETE /api/users/{id} — deactivates account (IsActive=false), does not delete</summary>
[HttpDelete("{id}")]
[HasPermission(Modules.Users, PermissionActions.Delete)]
public async Task<IActionResult> Deactivate(string id)
{
try { await _users.DeactivateAsync(id); return NoContent(); }
@@ -66,6 +72,7 @@ public class UsersController : ControllerBase
/// <summary>POST /api/users/{id}/reset-password — returns new temp password</summary>
[HttpPost("{id}/reset-password")]
[HasPermission(Modules.Users, PermissionActions.Write)]
public async Task<IActionResult> ResetPassword(string id)
{
try