mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
fix(auth): stop class guards shadowing route permission keys
Nest runs class and method guards together, so a class-level view key ANDs with every action key below it. Staff granted only an action were denied before their key was ever checked: OCC could not fulfil wagon transfers, dispatchers could not create a yard, and track staff could not assign first/last-mile vehicles. Reads now carry the view key themselves, and the warehouses baseline lists every key its routes use.
This commit is contained in:
@@ -28,7 +28,9 @@ import { FirstMileInvoiceService } from './first-mile-invoice.service';
|
|||||||
@ApiTags('first-mile')
|
@ApiTags('first-mile')
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@Controller('first-mile')
|
@Controller('first-mile')
|
||||||
@BookingStaff(FREIGHT_PERMS.firstMile.view)
|
// No class-level key: Nest stacks class and method guards, so a class-level
|
||||||
|
// `view` would AND with every action key below and lock out staff granted only
|
||||||
|
// an action (e.g. assign_vehicles). Each route carries its own key instead.
|
||||||
export class FirstMileController {
|
export class FirstMileController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly firstMileService: FirstMileService,
|
private readonly firstMileService: FirstMileService,
|
||||||
@@ -36,6 +38,7 @@ export class FirstMileController {
|
|||||||
) { }
|
) { }
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@BookingStaff(FREIGHT_PERMS.firstMile.view)
|
||||||
@ApiOperation({ summary: 'List first-mile legs' })
|
@ApiOperation({ summary: 'List first-mile legs' })
|
||||||
findAll(
|
findAll(
|
||||||
@Query('status') status?: string,
|
@Query('status') status?: string,
|
||||||
@@ -58,6 +61,7 @@ export class FirstMileController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
|
@BookingStaff(FREIGHT_PERMS.firstMile.view)
|
||||||
@ApiOperation({ summary: 'Get a first-mile leg by ID' })
|
@ApiOperation({ summary: 'Get a first-mile leg by ID' })
|
||||||
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
return this.firstMileService.findById(id);
|
return this.firstMileService.findById(id);
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ import { LastMileInvoiceService } from './last-mile-invoice.service';
|
|||||||
@ApiTags('last-mile')
|
@ApiTags('last-mile')
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@Controller('last-mile')
|
@Controller('last-mile')
|
||||||
@BookingStaff(FREIGHT_PERMS.lastMile.view)
|
// No class-level key: Nest stacks class and method guards, so a class-level
|
||||||
|
// `view` would AND with every action key below and lock out staff granted only
|
||||||
|
// an action (e.g. assign_vehicles). Each route carries its own key instead.
|
||||||
export class LastMileController {
|
export class LastMileController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly lastMileService: LastMileService,
|
private readonly lastMileService: LastMileService,
|
||||||
@@ -42,6 +44,7 @@ export class LastMileController {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@BookingStaff(FREIGHT_PERMS.lastMile.view)
|
||||||
@ApiOperation({ summary: 'List last-mile legs' })
|
@ApiOperation({ summary: 'List last-mile legs' })
|
||||||
findAll(
|
findAll(
|
||||||
@Query('status') status?: string,
|
@Query('status') status?: string,
|
||||||
@@ -64,18 +67,21 @@ export class LastMileController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
|
@BookingStaff(FREIGHT_PERMS.lastMile.view)
|
||||||
@ApiOperation({ summary: 'Get a last-mile leg by ID' })
|
@ApiOperation({ summary: 'Get a last-mile leg by ID' })
|
||||||
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
return this.lastMileService.findById(id);
|
return this.lastMileService.findById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('booking/:bookingId/arrival-trucks')
|
@Get('booking/:bookingId/arrival-trucks')
|
||||||
|
@BookingStaff(FREIGHT_PERMS.lastMile.view)
|
||||||
@ApiOperation({ summary: "Assigned EDR last-mile trucks for a booking (arrival/exit weighing prefill)" })
|
@ApiOperation({ summary: "Assigned EDR last-mile trucks for a booking (arrival/exit weighing prefill)" })
|
||||||
arrivalTrucks(@Param('bookingId', ParseUUIDPipe) bookingId: string) {
|
arrivalTrucks(@Param('bookingId', ParseUUIDPipe) bookingId: string) {
|
||||||
return this.lastMileService.arrivalTrucksForBooking(bookingId);
|
return this.lastMileService.arrivalTrucksForBooking(bookingId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('booking/:bookingId/remaining-tons')
|
@Get('booking/:bookingId/remaining-tons')
|
||||||
|
@BookingStaff(FREIGHT_PERMS.lastMile.view)
|
||||||
@ApiOperation({ summary: 'Bulk drawdown: tonnage still to be hauled (total − departed trucks)' })
|
@ApiOperation({ summary: 'Bulk drawdown: tonnage still to be hauled (total − departed trucks)' })
|
||||||
remainingTons(@Param('bookingId', ParseUUIDPipe) bookingId: string) {
|
remainingTons(@Param('bookingId', ParseUUIDPipe) bookingId: string) {
|
||||||
return this.lastMileService.remainingTonsForBooking(bookingId);
|
return this.lastMileService.remainingTonsForBooking(bookingId);
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ const toInt = (value?: string): number | undefined => {
|
|||||||
*/
|
*/
|
||||||
@ApiTags('wagon-transfer-requests')
|
@ApiTags('wagon-transfer-requests')
|
||||||
@Controller('wagon-transfer-requests')
|
@Controller('wagon-transfer-requests')
|
||||||
@WagonTransferView()
|
// No class-level key: Nest stacks class and method guards, so a class-level
|
||||||
|
// `transfer_view` would AND with every action key below and lock out the OCC
|
||||||
|
// staff granted only `transfer_fulfill`. Reads carry the view key themselves.
|
||||||
export class WagonTransferRequestsController {
|
export class WagonTransferRequestsController {
|
||||||
constructor(private readonly service: WagonTransferRequestsService) {}
|
constructor(private readonly service: WagonTransferRequestsService) {}
|
||||||
|
|
||||||
@@ -56,6 +58,7 @@ export class WagonTransferRequestsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@WagonTransferView()
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary:
|
summary:
|
||||||
'Transfer desk list — paginated, filterable by status (comma-separated), yards and wagon type',
|
'Transfer desk list — paginated, filterable by status (comma-separated), yards and wagon type',
|
||||||
@@ -84,6 +87,7 @@ export class WagonTransferRequestsController {
|
|||||||
// matches in declaration order, so `/history` would otherwise be captured by
|
// matches in declaration order, so `/history` would otherwise be captured by
|
||||||
// the `:id` param route (and rejected by ParseUUIDPipe).
|
// the `:id` param route (and rejected by ParseUUIDPipe).
|
||||||
@Get('history')
|
@Get('history')
|
||||||
|
@WagonTransferView()
|
||||||
@ApiQuery({ name: 'page', required: false })
|
@ApiQuery({ name: 'page', required: false })
|
||||||
@ApiQuery({ name: 'pageSize', required: false })
|
@ApiQuery({ name: 'pageSize', required: false })
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
@@ -129,6 +133,7 @@ export class WagonTransferRequestsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
|
@WagonTransferView()
|
||||||
@ApiOperation({ summary: 'Get one transfer request' })
|
@ApiOperation({ summary: 'Get one transfer request' })
|
||||||
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
return this.service.findById(id);
|
return this.service.findById(id);
|
||||||
|
|||||||
@@ -14,13 +14,19 @@ import { WarehousesService } from './warehouses.service';
|
|||||||
@ApiTags('warehouses')
|
@ApiTags('warehouses')
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
// Baseline read: warehouse reference data is consumed by inventory/dashboard
|
// Baseline read: warehouse reference data is consumed by inventory/dashboard
|
||||||
// flows too, so any of the three view permissions grants reads. Writes stack
|
// flows too, so any of the view permissions grants reads. Writes stack their
|
||||||
// their specific create/update permission per route on top.
|
// specific create/update permission per route on top — which means every key
|
||||||
|
// used by a route below must also appear here, or the class guard denies
|
||||||
|
// before the route's own key is ever consulted (Nest ANDs the two).
|
||||||
@Controller('warehouses')
|
@Controller('warehouses')
|
||||||
@BookingStaff([
|
@BookingStaff([
|
||||||
FREIGHT_PERMS.warehouses.view,
|
FREIGHT_PERMS.warehouses.view,
|
||||||
FREIGHT_PERMS.warehouseInventory.view,
|
FREIGHT_PERMS.warehouseInventory.view,
|
||||||
FREIGHT_PERMS.warehouseDashboard.view,
|
FREIGHT_PERMS.warehouseDashboard.view,
|
||||||
|
FREIGHT_PERMS.warehouses.create,
|
||||||
|
FREIGHT_PERMS.warehouses.update,
|
||||||
|
FREIGHT_PERMS.warehouseYards.view,
|
||||||
|
FREIGHT_PERMS.warehouseYards.create,
|
||||||
])
|
])
|
||||||
export class WarehousesController {
|
export class WarehousesController {
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
Reference in New Issue
Block a user