mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
feat(user-management): implement staff reference permissions for various controllers and update sidebar permissions
This commit is contained in:
@@ -14,6 +14,13 @@ export const BookingStaff = (permission: string | string[]) =>
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Read-only reference data (yard dropdowns, search filters): any signed-in
|
||||
* staff. Menu/page visibility stays permission-gated in the frontend — this
|
||||
* only lets forms populate their lookups.
|
||||
*/
|
||||
export const StaffReference = () => applyDecorators(UseGuards(JwtGuard));
|
||||
|
||||
export const BookingView = () => BookingStaff(FREIGHT_PERMS.bookings.view);
|
||||
|
||||
export const TrainSchedulingView = () =>
|
||||
|
||||
@@ -3,7 +3,8 @@ import {
|
||||
Param, ParseUUIDPipe, Patch, Post, Query,
|
||||
} from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { RuleEngineManage, RuleEngineView } from '../../../common/rule-engine-guards';
|
||||
import { RuleEngineManage } from '../../../common/rule-engine-guards';
|
||||
import { StaffReference } from '../../../common/booking-guards';
|
||||
import { CreateCargoTypeDto } from '../dto/create-cargo-type.dto';
|
||||
import { ListCargoTypesQueryDto } from '../dto/list-rule-engine-query.dto';
|
||||
import { MoveOrderDto } from '../dto/move-order.dto';
|
||||
@@ -18,7 +19,7 @@ export class CargoTypesController {
|
||||
constructor(private readonly service: CargoTypesService) {}
|
||||
|
||||
@Get()
|
||||
@RuleEngineView('cargo-types')
|
||||
@StaffReference()
|
||||
@ApiOperation({ summary: 'List cargo types' })
|
||||
findAll(@Query() query: ListCargoTypesQueryDto) {
|
||||
return this.service.findAll(query);
|
||||
@@ -41,7 +42,7 @@ export class CargoTypesController {
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@RuleEngineView('cargo-types')
|
||||
@StaffReference()
|
||||
@ApiOperation({ summary: 'Get a cargo type by ID' })
|
||||
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.service.findById(id);
|
||||
|
||||
@@ -3,7 +3,8 @@ import {
|
||||
Param, ParseUUIDPipe, Patch, Post, Query,
|
||||
} from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { RuleEngineManage, RuleEngineView } from '../../../common/rule-engine-guards';
|
||||
import { RuleEngineManage } from '../../../common/rule-engine-guards';
|
||||
import { StaffReference } from '../../../common/booking-guards';
|
||||
import { CreateContainerTypeDto } from '../dto/create-container-type.dto';
|
||||
import { ListContainerTypesQueryDto } from '../dto/list-rule-engine-query.dto';
|
||||
import { MoveOrderDto } from '../dto/move-order.dto';
|
||||
@@ -18,7 +19,7 @@ export class ContainerTypesController {
|
||||
constructor(private readonly service: ContainerTypesService) {}
|
||||
|
||||
@Get()
|
||||
@RuleEngineView('container-types')
|
||||
@StaffReference()
|
||||
@ApiOperation({ summary: 'List container types' })
|
||||
findAll(@Query() query: ListContainerTypesQueryDto) {
|
||||
return this.service.findAll(query);
|
||||
@@ -41,7 +42,7 @@ export class ContainerTypesController {
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@RuleEngineView('container-types')
|
||||
@StaffReference()
|
||||
@ApiOperation({ summary: 'Get a container type by ID' })
|
||||
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.service.findById(id);
|
||||
|
||||
@@ -2,7 +2,8 @@ import {
|
||||
Body, Controller, Delete, Get, HttpCode, HttpStatus,
|
||||
Param, ParseUUIDPipe, Patch, Post, Query,
|
||||
} from '@nestjs/common';
|
||||
import { RuleEngineManage, RuleEngineView } from '../../../common/rule-engine-guards';
|
||||
import { RuleEngineManage } from '../../../common/rule-engine-guards';
|
||||
import { StaffReference } from '../../../common/booking-guards';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { CreateServiceTypeDto } from '../dto/create-service-type.dto';
|
||||
import { ListServiceTypesQueryDto } from '../dto/list-rule-engine-query.dto';
|
||||
@@ -18,7 +19,7 @@ export class ServiceTypesController {
|
||||
constructor(private readonly service: ServiceTypesService) {}
|
||||
|
||||
@Get()
|
||||
@RuleEngineView('service-types')
|
||||
@StaffReference()
|
||||
@ApiOperation({ summary: 'List service types' })
|
||||
findAll(@Query() query: ListServiceTypesQueryDto) {
|
||||
return this.service.findAll(query);
|
||||
@@ -41,7 +42,7 @@ export class ServiceTypesController {
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@RuleEngineView('service-types')
|
||||
@StaffReference()
|
||||
@ApiOperation({ summary: 'Get a service type by ID' })
|
||||
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.service.findById(id);
|
||||
|
||||
@@ -2,7 +2,8 @@ import {
|
||||
Body, Controller, Delete, Get, HttpCode, HttpStatus,
|
||||
Param, ParseUUIDPipe, Patch, Post, Query,
|
||||
} from '@nestjs/common';
|
||||
import { RuleEngineManage, RuleEngineView } from '../../../common/rule-engine-guards';
|
||||
import { RuleEngineManage } from '../../../common/rule-engine-guards';
|
||||
import { StaffReference } from '../../../common/booking-guards';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { CreateShippingLineDto } from '../dto/create-shipping-line.dto';
|
||||
import { ListRuleEngineQueryDto } from '../dto/list-rule-engine-query.dto';
|
||||
@@ -16,14 +17,14 @@ export class ShippingLinesController {
|
||||
constructor(private readonly service: ShippingLinesService) {}
|
||||
|
||||
@Get()
|
||||
@RuleEngineView('shipping-lines')
|
||||
@StaffReference()
|
||||
@ApiOperation({ summary: 'List shipping lines' })
|
||||
findAll(@Query() query: ListRuleEngineQueryDto) {
|
||||
return this.service.findAll(query);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@RuleEngineView('shipping-lines')
|
||||
@StaffReference()
|
||||
@ApiOperation({ summary: 'Get a shipping line by ID' })
|
||||
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.service.findById(id);
|
||||
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { RuleEngineManage, RuleEngineView } from '../../../common/rule-engine-guards';
|
||||
import { RuleEngineManage } from '../../../common/rule-engine-guards';
|
||||
import { StaffReference } from '../../../common/booking-guards';
|
||||
import { CreateYardDistanceDto } from '../dto/create-yard-distance.dto';
|
||||
import { ListYardDistancesQueryDto } from '../dto/list-rule-engine-query.dto';
|
||||
import { UpdateYardDistanceDto } from '../dto/update-yard-distance.dto';
|
||||
@@ -25,14 +26,14 @@ export class YardDistancesController {
|
||||
constructor(private readonly service: YardDistancesService) {}
|
||||
|
||||
@Get()
|
||||
@RuleEngineView('yard-distances')
|
||||
@StaffReference()
|
||||
@ApiOperation({ summary: 'List yard distances' })
|
||||
findAll(@Query() query: ListYardDistancesQueryDto) {
|
||||
return this.service.findAll(query);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@RuleEngineView('yard-distances')
|
||||
@StaffReference()
|
||||
@ApiOperation({ summary: 'Get a yard distance by ID' })
|
||||
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.service.findById(id);
|
||||
|
||||
@@ -2,7 +2,8 @@ import {
|
||||
Body, Controller, Delete, Get, HttpCode, HttpStatus,
|
||||
Param, ParseUUIDPipe, Patch, Post, Query,
|
||||
} from '@nestjs/common';
|
||||
import { RuleEngineManage, RuleEngineView } from '../../../common/rule-engine-guards';
|
||||
import { RuleEngineManage } from '../../../common/rule-engine-guards';
|
||||
import { StaffReference } from '../../../common/booking-guards';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { CreateYardDto } from '../dto/create-yard.dto';
|
||||
import { ListYardsQueryDto } from '../dto/list-rule-engine-query.dto';
|
||||
@@ -18,7 +19,9 @@ export class YardsController {
|
||||
constructor(private readonly service: YardsService) {}
|
||||
|
||||
@Get()
|
||||
@RuleEngineView('yards')
|
||||
// Reference read: every staff form/search needs the yard list (origin /
|
||||
// destination pickers), so login is the only requirement.
|
||||
@StaffReference()
|
||||
@ApiOperation({ summary: 'List yards' })
|
||||
findAll(@Query() query: ListYardsQueryDto) {
|
||||
return this.service.findAll(query);
|
||||
@@ -41,7 +44,7 @@ export class YardsController {
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@RuleEngineView('yards')
|
||||
@StaffReference()
|
||||
@ApiOperation({ summary: 'Get a yard by ID' })
|
||||
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.service.findById(id);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Body, Controller, Get, Param, ParseUUIDPipe, Patch, Post } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
|
||||
import { BookingStaff } from '../../common/booking-guards';
|
||||
import { BookingStaff, StaffReference } from '../../common/booking-guards';
|
||||
import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
|
||||
import { CreateWarehouseZoneDto } from './dto/create-warehouse-zone.dto';
|
||||
import { UpdateWarehouseYardDto } from './dto/update-warehouse-yard.dto';
|
||||
@@ -10,8 +10,9 @@ import { WarehouseZonesService } from './warehouse-zones.service';
|
||||
|
||||
@ApiTags('warehouse-yards')
|
||||
@ApiBearerAuth()
|
||||
// No class-level guard: the two reference GETs are open to any signed-in
|
||||
// staff (StaffReference), every other route carries its own permission.
|
||||
@Controller('warehouse-yards')
|
||||
@BookingStaff(FREIGHT_PERMS.warehouseYards.view)
|
||||
export class WarehouseYardsController {
|
||||
constructor(
|
||||
private readonly yardsService: WarehouseYardsService,
|
||||
@@ -19,12 +20,14 @@ export class WarehouseYardsController {
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@StaffReference()
|
||||
@ApiOperation({ summary: 'List all warehouse yards' })
|
||||
findAll() {
|
||||
return this.yardsService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@StaffReference()
|
||||
@ApiOperation({ summary: 'Get warehouse yard by ID' })
|
||||
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.yardsService.findById(id);
|
||||
|
||||
@@ -151,6 +151,7 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
|
||||
label: "Customers",
|
||||
href: "/dashboard/customers",
|
||||
icon: <Building2 />,
|
||||
permission: FREIGHT_PERMS.customers.view,
|
||||
},
|
||||
{
|
||||
label: "Contracts",
|
||||
@@ -162,6 +163,7 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
|
||||
label: "Bookings",
|
||||
href: "/dashboard/booking-requests",
|
||||
icon: <FileText />,
|
||||
permission: FREIGHT_PERMS.bookings.view,
|
||||
},
|
||||
// Operations hub: clearance-document review for contracts WITHOUT
|
||||
// customs clearing (contract-level for one-time, per-booking for general).
|
||||
@@ -375,6 +377,7 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
|
||||
label: "Imports",
|
||||
href: "/dashboard/import-warehouse",
|
||||
icon: <PackageOpen />,
|
||||
permission: FREIGHT_PERMS.warehouseInventory.view,
|
||||
children: [
|
||||
{
|
||||
label: "Import Overview",
|
||||
@@ -407,6 +410,7 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
|
||||
label: "Exports",
|
||||
href: "/dashboard/export-warehouse",
|
||||
icon: <Truck />,
|
||||
permission: FREIGHT_PERMS.warehouseInventory.view,
|
||||
children: [
|
||||
{
|
||||
label: "Export Overview",
|
||||
@@ -449,6 +453,7 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
|
||||
label: "Intercity",
|
||||
href: "/dashboard/intercity",
|
||||
icon: <TrainFront />,
|
||||
permission: FREIGHT_PERMS.trainScheduling.view,
|
||||
children: [
|
||||
{
|
||||
label: "Intercity Cargo",
|
||||
@@ -465,6 +470,7 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
|
||||
label: "Warehouse Dashboard",
|
||||
href: "/dashboard/warehouse-dashboard",
|
||||
icon: <LayoutDashboard />,
|
||||
permission: FREIGHT_PERMS.warehouseDashboard.view,
|
||||
},
|
||||
{
|
||||
// Yard-wide, not per-direction: the gate sees import and export
|
||||
@@ -472,21 +478,25 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
|
||||
label: "Trucks on Site",
|
||||
href: "/dashboard/trucks-on-site",
|
||||
icon: <Truck />,
|
||||
permission: FREIGHT_PERMS.warehouseInventory.view,
|
||||
},
|
||||
{
|
||||
label: "Warehouses",
|
||||
href: "/dashboard/warehouses",
|
||||
icon: <Container />,
|
||||
permission: FREIGHT_PERMS.warehouses.view,
|
||||
},
|
||||
{
|
||||
label: "Allocation & Fees",
|
||||
href: "/dashboard/warehouse-rules",
|
||||
icon: <SlidersHorizontal />,
|
||||
permission: FREIGHT_PERMS.warehouseAllocationRules.view,
|
||||
},
|
||||
{
|
||||
label: "Fee Invoices",
|
||||
href: "/dashboard/warehouse-fee-invoices",
|
||||
icon: <Wallet />,
|
||||
permission: FREIGHT_PERMS.warehouseFeeInvoices.view,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -523,10 +533,12 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
|
||||
{
|
||||
label: "Contract validity",
|
||||
href: "/dashboard/configuration/contract-validity-periods",
|
||||
permission: FREIGHT_PERMS.config.contractValidity.view,
|
||||
},
|
||||
{
|
||||
label: "Train scheduling rules",
|
||||
href: "/dashboard/configuration/train-scheduling-rules",
|
||||
permission: FREIGHT_PERMS.trainScheduling.rulesManage,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -541,6 +553,7 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
|
||||
label: "Staff",
|
||||
href: "/user-management",
|
||||
icon: <Users />,
|
||||
permission: FREIGHT_PERMS.admin,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -596,10 +609,22 @@ const filterSidebarByPermission = (
|
||||
return permissionAllowed(item);
|
||||
};
|
||||
|
||||
// Recursive: a group's own permission gates the whole subtree, leaves are
|
||||
// checked individually, and a group with no surviving children disappears.
|
||||
const filterItems = (items: SidebarItem[]): SidebarItem[] =>
|
||||
items.flatMap((item) => {
|
||||
if (item.children?.length) {
|
||||
if (item.permission && !permissionAllowed(item)) return [];
|
||||
const children = filterItems(item.children);
|
||||
return children.length ? [{ ...item, children }] : [];
|
||||
}
|
||||
return itemAllowed(item) ? [item] : [];
|
||||
});
|
||||
|
||||
return sections
|
||||
.map((section) => ({
|
||||
...section,
|
||||
items: section.items.filter(itemAllowed),
|
||||
items: filterItems(section.items),
|
||||
}))
|
||||
.filter((section) => section.items.length > 0);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { SidebarItem } from "@/components/layout/types";
|
||||
import { ruleEngineViewKey } from "@/lib/permissions";
|
||||
import type { RuleEngineResourceSlug } from "@/types/rule-engine";
|
||||
|
||||
export type RuleEngineNavCategory = "configuration" | "rules";
|
||||
@@ -787,6 +788,7 @@ export const getCategorySidebarChildren = (
|
||||
RULE_ENGINE_RESOURCES.filter((r) => r.category === category).map((r) => ({
|
||||
label: r.label,
|
||||
href: ruleEngineResourcePath(r.slug),
|
||||
permission: ruleEngineViewKey(r.slug),
|
||||
}));
|
||||
|
||||
export const DEFAULT_CONFIGURATION_SLUG: RuleEngineResourceSlug = "cargo-types";
|
||||
|
||||
Reference in New Issue
Block a user