66using Microsoft . AspNetCore . Mvc ;
77using Resgrid . Model . Reporting ;
88using Resgrid . Model . Services ;
9+ using Resgrid . Providers . Claims ;
910using Resgrid . Web . Services . Helpers ;
1011using Resgrid . Web . Services . Models . v4 . Reporting ;
1112
@@ -18,6 +19,7 @@ namespace Resgrid.Web.Services.Controllers.v4
1819 ///
1920 /// SECURITY: every endpoint is hard-scoped to the authenticated user's claim DepartmentId — there is
2021 /// deliberately no departmentId parameter, so a client can never request another department's data.
22+ /// Every endpoint also requires the caller's Reports/View permission (Reports_View policy).
2123 /// System-wide (cross-department) reporting is intentionally NOT exposed over HTTP; it is available
2224 /// only to the in-process BackOffice, which resolves IPlatformReportingService directly and calls it
2325 /// with departmentId = null.
@@ -29,6 +31,7 @@ public class ReportingController : V4AuthenticatedApiControllerbase
2931 {
3032 private const int MaxDayWindow = 366 ;
3133 private const int MaxMonthWindowDays = 366 * 5 ;
34+ private const int MaxTopN = 50 ;
3235
3336 private readonly IPlatformReportingService _reportingService ;
3437
@@ -44,14 +47,15 @@ public ReportingController(IPlatformReportingService reportingService)
4447 /// <param name="from">Window start (UTC).</param>
4548 /// <param name="to">Window end (UTC).</param>
4649 /// <param name="granularity">Series bucketing: 0 = day, 1 = month.</param>
47- /// <param name="topN">Max slices per breakdown before an "Other" bucket.</param>
50+ /// <param name="topN">Max slices per breakdown before an "Other" bucket (clamped to 1–50) .</param>
4851 [ HttpGet ( "GetDashboard" ) ]
4952 [ ProducesResponseType ( StatusCodes . Status200OK ) ]
50- [ Authorize ]
53+ [ Authorize ( Policy = ResgridResources . Reports_View ) ]
5154 public async Task < ActionResult < DashboardReportResult > > GetDashboard ( DateTime from , DateTime to ,
5255 int granularity = 0 , int topN = 5 , CancellationToken cancellationToken = default )
5356 {
5457 var gran = granularity == 1 ? ReportGranularity . Month : ReportGranularity . Day ;
58+ topN = Math . Clamp ( topN , 1 , MaxTopN ) ;
5559 var ( startUtc , endUtc ) = NormalizeWindow ( from , to , gran == ReportGranularity . Month ? MaxMonthWindowDays : MaxDayWindow ) ;
5660
5761 var report = await _reportingService . GetDashboardReportAsync ( DepartmentId , startUtc , endUtc , gran , topN , false , cancellationToken ) ;
@@ -65,7 +69,7 @@ public async Task<ActionResult<DashboardReportResult>> GetDashboard(DateTime fro
6569 /// <summary>Response-time / NFPA analytics (alarm handling, turnout, travel, total response).</summary>
6670 [ HttpGet ( "GetResponseTimes" ) ]
6771 [ ProducesResponseType ( StatusCodes . Status200OK ) ]
68- [ Authorize ]
72+ [ Authorize ( Policy = ResgridResources . Reports_View ) ]
6973 public async Task < ActionResult < ResponseTimeReportResult > > GetResponseTimes ( DateTime from , DateTime to ,
7074 CancellationToken cancellationToken = default )
7175 {
@@ -81,7 +85,7 @@ public async Task<ActionResult<ResponseTimeReportResult>> GetResponseTimes(DateT
8185 /// <summary>Unit Hour Utilization and workload analytics.</summary>
8286 [ HttpGet ( "GetUtilization" ) ]
8387 [ ProducesResponseType ( StatusCodes . Status200OK ) ]
84- [ Authorize ]
88+ [ Authorize ( Policy = ResgridResources . Reports_View ) ]
8589 public async Task < ActionResult < UtilizationReportResult > > GetUtilization ( DateTime from , DateTime to ,
8690 CancellationToken cancellationToken = default )
8791 {
@@ -97,7 +101,7 @@ public async Task<ActionResult<UtilizationReportResult>> GetUtilization(DateTime
97101 /// <summary>Personnel participation and certification-compliance analytics.</summary>
98102 [ HttpGet ( "GetParticipation" ) ]
99103 [ ProducesResponseType ( StatusCodes . Status200OK ) ]
100- [ Authorize ]
104+ [ Authorize ( Policy = ResgridResources . Reports_View ) ]
101105 public async Task < ActionResult < ParticipationReportResult > > GetParticipation ( DateTime from , DateTime to ,
102106 CancellationToken cancellationToken = default )
103107 {
@@ -116,7 +120,7 @@ public async Task<ActionResult<ParticipationReportResult>> GetParticipation(Date
116120 /// </summary>
117121 [ HttpGet ( "ExportIncidents" ) ]
118122 [ ProducesResponseType ( StatusCodes . Status200OK ) ]
119- [ Authorize ]
123+ [ Authorize ( Policy = ResgridResources . Reports_View ) ]
120124 public async Task < IActionResult > ExportIncidents ( DateTime from , DateTime to , int profile = 0 ,
121125 CancellationToken cancellationToken = default )
122126 {
@@ -134,7 +138,7 @@ public async Task<IActionResult> ExportIncidents(DateTime from, DateTime to, int
134138 /// </summary>
135139 [ HttpGet ( "GetExportGaps" ) ]
136140 [ ProducesResponseType ( StatusCodes . Status200OK ) ]
137- [ Authorize ]
141+ [ Authorize ( Policy = ResgridResources . Reports_View ) ]
138142 public ActionResult < ExportGapReportResult > GetExportGaps ( int profile = 0 )
139143 {
140144 var exportProfile = Enum . IsDefined ( typeof ( ExportProfile ) , profile ) ? ( ExportProfile ) profile : ExportProfile . Generic ;
0 commit comments