mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 11:18:17 +00:00
feat: ( permissions ) enforce specific permission keys across API and backoffice
This commit is contained in:
@@ -12,26 +12,60 @@ import { BlockedSeatsRevenueLossQueryDto, FinanceSummaryQueryDto, GenerateReport
|
||||
import { PassengerStaff } from "../../common/passenger-guards";
|
||||
import { PASSENGER_PERMS } from "../../seed/passenger-permissions.registry";
|
||||
|
||||
const R = PASSENGER_PERMS.reports;
|
||||
|
||||
/**
|
||||
* One report's guard: its own key, the `reports:view` umbrella, or admin.
|
||||
*
|
||||
* The umbrella is kept in every array so the `finance`, `financeManager` and
|
||||
* `director` presets — which hold `reports:view` — keep seeing every report.
|
||||
* Granting only a per-report key hands out that report and nothing else.
|
||||
*
|
||||
* This is deliberately NOT a class-level decorator. Nest requires controller-
|
||||
* level AND route-level guards to both pass, so a class-level
|
||||
* `[reports.view, admin]` plus a per-route key would be an AND and would lock
|
||||
* out everyone holding only `reports:view`.
|
||||
*/
|
||||
const Report = (key: string) => PassengerStaff([key, R.view, PASSENGER_PERMS.admin]);
|
||||
|
||||
/** The schedule picker is shared by five reports, so any report key opens it. */
|
||||
const AnyReport = () =>
|
||||
PassengerStaff([
|
||||
R.overall.view,
|
||||
R.finance.view,
|
||||
R.coachUtilization.view,
|
||||
R.seatStatus.view,
|
||||
R.blockedSeats.view,
|
||||
R.passengers.view,
|
||||
R.boarding.view,
|
||||
R.payments.view,
|
||||
R.catalog.view,
|
||||
R.view,
|
||||
PASSENGER_PERMS.admin,
|
||||
]);
|
||||
|
||||
@ApiTags("Reports")
|
||||
@Controller("reports")
|
||||
@PassengerStaff([PASSENGER_PERMS.reports.view, PASSENGER_PERMS.admin])
|
||||
@ApiBearerAuth("IAM-auth")
|
||||
export class ReportsController {
|
||||
constructor(private service: ReportsService) {}
|
||||
|
||||
@Post("generate")
|
||||
@Report(R.catalog.view)
|
||||
@ApiOperation({ summary: "Generate operational report" })
|
||||
generateReport(@Body() dto: GenerateReportDto) {
|
||||
return this.service.generateReport(dto);
|
||||
}
|
||||
|
||||
@Get('schedules')
|
||||
@AnyReport()
|
||||
@ApiOperation({ summary: 'List schedules for the passengers report picker' })
|
||||
listSchedulesForPicker(@Query('all') all?: string) {
|
||||
return this.service.listSchedulesForPicker(all === 'true');
|
||||
}
|
||||
|
||||
@Get("passengers/list")
|
||||
@Report(R.passengers.view)
|
||||
@ApiOperation({ summary: "Flat passenger list for a specific schedule" })
|
||||
getPassengerList(@Query("scheduleId") scheduleId: string) {
|
||||
return this.service.getPassengerList(scheduleId);
|
||||
@@ -39,6 +73,7 @@ export class ReportsController {
|
||||
|
||||
// Two segments, so `@Get(":reportId")` below cannot shadow it whatever the order.
|
||||
@Get("passengers/overview")
|
||||
@Report(R.passengers.view)
|
||||
@ApiOperation({
|
||||
summary: "Fleet-wide passenger mix across a departure window",
|
||||
description:
|
||||
@@ -56,12 +91,14 @@ export class ReportsController {
|
||||
}
|
||||
|
||||
@Get("passengers")
|
||||
@Report(R.passengers.view)
|
||||
@ApiOperation({ summary: "Passengers report for a specific schedule" })
|
||||
getOccupancyReport(@Query("scheduleId") scheduleId: string) {
|
||||
return this.service.getOccupancyBySchedule(scheduleId);
|
||||
}
|
||||
|
||||
@Get("payment-discrepancy")
|
||||
@Report(R.payments.view)
|
||||
@ApiOperation({ summary: "Payment discrepancy report — bookings where paid amount is less than the fare. Pass `search` to look up a specific PNR or ticket number." })
|
||||
getPaymentDiscrepancy(
|
||||
@Query('from') from?: string,
|
||||
@@ -73,6 +110,7 @@ export class ReportsController {
|
||||
}
|
||||
|
||||
@Get("seat-status")
|
||||
@Report(R.seatStatus.view)
|
||||
@ApiOperation({ summary: "Seat status breakdown for a schedule (paid, unpaid, expired holds, blocked)" })
|
||||
getSeatStatusReport(@Query('scheduleId') scheduleId: string) {
|
||||
return this.service.getSeatStatusReport(scheduleId);
|
||||
@@ -80,6 +118,7 @@ export class ReportsController {
|
||||
|
||||
// Two segments, so `@Get(":reportId")` below cannot shadow it whatever the order.
|
||||
@Get("seat-status/overview")
|
||||
@Report(R.seatStatus.view)
|
||||
@ApiOperation({
|
||||
summary: "Fleet-wide seat status across a departure window",
|
||||
description:
|
||||
@@ -96,18 +135,21 @@ export class ReportsController {
|
||||
}
|
||||
|
||||
@Get("boarding")
|
||||
@Report(R.boarding.view)
|
||||
@ApiOperation({ summary: "Boarding report for a schedule — boarded vs not-boarded passengers" })
|
||||
getBoardingReport(@Query('scheduleId') scheduleId: string) {
|
||||
return this.service.getBoardingReport(scheduleId);
|
||||
}
|
||||
|
||||
@Get("payments")
|
||||
@Report(R.payments.view)
|
||||
@ApiOperation({ summary: "Payments collected for a schedule" })
|
||||
getPaymentsReport(@Query('scheduleId') scheduleId: string) {
|
||||
return this.service.getPaymentsReport(scheduleId);
|
||||
}
|
||||
|
||||
@Get("payments/discrepancy")
|
||||
@Report(R.payments.view)
|
||||
@ApiOperation({ summary: "Payment discrepancy breakdown for a schedule" })
|
||||
getPaymentDiscrepancyBySchedule(
|
||||
@Query('scheduleId') scheduleId: string,
|
||||
@@ -121,6 +163,7 @@ export class ReportsController {
|
||||
// ── Finance Summary ──────────────────────────────────────────────────────
|
||||
|
||||
@Get("finance")
|
||||
@Report(R.finance.view)
|
||||
@ApiOperation({
|
||||
summary: "Finance summary — revenue by period, origin/destination segment, payment method, and currency",
|
||||
description:
|
||||
@@ -138,6 +181,7 @@ export class ReportsController {
|
||||
}
|
||||
|
||||
@Get("finance/export")
|
||||
@Report(R.finance.export)
|
||||
@ApiOperation({ summary: "Finance summary as CSV — one row per period + route + payment method" })
|
||||
@ApiProduces("text/csv")
|
||||
@ApiOkResponse({ description: "CSV export", schema: { type: "string" } })
|
||||
@@ -154,6 +198,7 @@ export class ReportsController {
|
||||
// ── Blocked Seat Revenue Loss ──────────────────────────────────────────────
|
||||
|
||||
@Get("blocked-seats-revenue-loss")
|
||||
@Report(R.blockedSeats.view)
|
||||
@ApiOperation({
|
||||
summary: "Potential revenue lost to blocked seats, per schedule",
|
||||
description:
|
||||
@@ -176,6 +221,7 @@ export class ReportsController {
|
||||
}
|
||||
|
||||
@Get("blocked-seats-revenue-loss/export")
|
||||
@Report(R.blockedSeats.export)
|
||||
@ApiOperation({
|
||||
summary: "Blocked-seat revenue loss as CSV",
|
||||
description:
|
||||
@@ -200,12 +246,14 @@ export class ReportsController {
|
||||
}
|
||||
|
||||
@Get(":reportId")
|
||||
@Report(R.catalog.view)
|
||||
@ApiOperation({ summary: "Get report by ID" })
|
||||
getReport(@Param("reportId") reportId: string) {
|
||||
return this.service.getReport(reportId);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Report(R.catalog.view)
|
||||
@ApiOperation({ summary: "List reports" })
|
||||
listReports(@Query("type") type?: string) {
|
||||
return this.service.listReports(type);
|
||||
|
||||
Reference in New Issue
Block a user