From 0114673120df9cf0726aff6693519d0fd3a39ca9 Mon Sep 17 00:00:00 2001 From: Nathnael Date: Fri, 7 Aug 2026 07:31:23 +0000 Subject: [PATCH] feat(auth): gate and regate freight API controllers Gates the previously open support-agent, procurement, compliance, facilities, list-users and trade-access controllers, separates customer from staff routes across bookings, contracts, companies, billing, warehouses, files and train scheduling, and moves billing, overview, reports and the settings controllers onto their own keys instead of the blanket admin key. Drops the demo-permissions module and the untested notification test route. --- apps/edr-freight-api/src/app.module.ts | 2 - .../src/modules/ai/ai.controller.ts | 8 +-- .../src/modules/auth/list-users.controller.ts | 5 +- .../backoffice/backoffice.controller.ts | 8 ++- .../src/modules/billing/billing.controller.ts | 7 ++- .../billing/portal-billing.controller.ts | 2 + .../modules/bookings/bookings.controller.ts | 61 +++++++++++++++++-- .../modules/companies/companies.controller.ts | 36 +++++++++-- .../compliance/compliance.controller.ts | 6 ++ .../contract-templates.controller.ts | 13 ++-- .../modules/contracts/contracts.controller.ts | 54 +++++++++++----- .../demo-permissions.controller.ts | 22 ------- .../demo-permissions.module.ts | 8 --- .../dropdown-settings.controller.ts | 17 +++--- .../exchange-settings.controller.ts | 7 ++- .../facilities/facilities.controller.ts | 6 ++ .../file-upload-settings.controller.ts | 17 +++--- .../src/modules/files/files.controller.ts | 2 + .../last-mile-requests.controller.ts | 6 +- .../notification-inbox.controller.ts | 24 -------- .../notification-inbox.service.ts | 27 -------- .../modules/overview/overview.controller.ts | 17 +++--- .../src/modules/payment/payment.controller.ts | 4 +- .../procurement/procurement.controller.ts | 11 ++++ .../src/modules/reports/reports.controller.ts | 5 +- .../controllers/rates.controller.ts | 4 +- .../support-attachment.controller.ts | 3 + .../support-chat-agent.controller.ts | 8 +++ .../support-chat/support-chat.controller.ts | 2 + .../train-scheduling.controller.ts | 17 ++++-- .../user-trade-access.controller.ts | 5 +- .../warehouse-inventory.controller.ts | 24 ++++---- .../warehouse-invoice.controller.ts | 12 ++-- 33 files changed, 271 insertions(+), 179 deletions(-) delete mode 100644 apps/edr-freight-api/src/modules/demo-permissions/demo-permissions.controller.ts delete mode 100644 apps/edr-freight-api/src/modules/demo-permissions/demo-permissions.module.ts diff --git a/apps/edr-freight-api/src/app.module.ts b/apps/edr-freight-api/src/app.module.ts index d41b77dce..0375df6df 100644 --- a/apps/edr-freight-api/src/app.module.ts +++ b/apps/edr-freight-api/src/app.module.ts @@ -53,7 +53,6 @@ import { OtpModule } from "./modules/otp/otp.module"; import { HealthModule } from "./modules/health/health.module"; import { RuleEngineModule } from "./modules/rule-engine/rule-engine.module"; import { BackofficeModule } from "./modules/backoffice/backoffice.module"; -import { DemoPermissionsModule } from "./modules/demo-permissions/demo-permissions.module"; import { FreightAuthModule } from "./modules/auth/freight-auth.module"; import { EDR_FREIGHT_APPLICATION, @@ -219,7 +218,6 @@ if (!process.env.APPLICATION_NAME) { HealthModule, RuleEngineModule, BackofficeModule, - DemoPermissionsModule, FreightAuthModule, PaymentModule, //New Modules diff --git a/apps/edr-freight-api/src/modules/ai/ai.controller.ts b/apps/edr-freight-api/src/modules/ai/ai.controller.ts index eb87fe97d..d71795066 100644 --- a/apps/edr-freight-api/src/modules/ai/ai.controller.ts +++ b/apps/edr-freight-api/src/modules/ai/ai.controller.ts @@ -1,15 +1,13 @@ import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; -import { Public } from '@edr/api-common'; +import { BookingStaff } from '../../common/booking-guards'; import { AiBookingRequestDto } from './dto/ai-booking-request.dto'; import { AiBookingResult } from './types/ai-booking-result.type'; import { MockAiService } from './mock-ai.service'; +import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry'; -// @Public() — TODO: swap for real guard when this leaves dev/testing. -// Safe while public: extracts + validates text only, never creates or -// dispatches anything. -@Public() +@BookingStaff(FREIGHT_PERMS.bookings.view) @ApiTags('AI Assistant (mock)') @Controller('ai') export class AiController { diff --git a/apps/edr-freight-api/src/modules/auth/list-users.controller.ts b/apps/edr-freight-api/src/modules/auth/list-users.controller.ts index e7fbfd771..e2f06f065 100644 --- a/apps/edr-freight-api/src/modules/auth/list-users.controller.ts +++ b/apps/edr-freight-api/src/modules/auth/list-users.controller.ts @@ -3,7 +3,8 @@ import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import { ListUsersQueryDto } from './dto/list-users-query.dto'; import { ListUsersService } from './list-users.service'; -import { StaffReference } from '../../common/booking-guards'; +import { BookingStaff } from '../../common/booking-guards'; +import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry'; @ApiTags('auth') @Controller('staff/users') @@ -12,7 +13,7 @@ export class ListUsersController { constructor(private readonly service: ListUsersService) {} @Get() - @StaffReference() + @BookingStaff(FREIGHT_PERMS.staff.users.view) @ApiOperation({ summary: 'List IAM users (paginated) for backoffice pickers', }) diff --git a/apps/edr-freight-api/src/modules/backoffice/backoffice.controller.ts b/apps/edr-freight-api/src/modules/backoffice/backoffice.controller.ts index b3305ba68..d25fb74ba 100644 --- a/apps/edr-freight-api/src/modules/backoffice/backoffice.controller.ts +++ b/apps/edr-freight-api/src/modules/backoffice/backoffice.controller.ts @@ -10,18 +10,19 @@ import { } from "@nestjs/common"; import { ApiOperation, ApiTags } from "@nestjs/swagger"; -import { FreightAdmin } from "../../common/booking-guards"; +import { BookingStaff } from "../../common/booking-guards"; +import { FREIGHT_PERMS } from "../../seed/freight-permissions.registry"; import { BackofficeService } from "./backoffice.service"; import { CreateOrganizationUserDto } from "./dto/create-organization-user.dto"; import { UpdateEmployeeUserRolesDto } from "./dto/update-employee-user-roles.dto"; @ApiTags("backoffice") @Controller("backoffice") -@FreightAdmin() export class BackofficeController { constructor(private readonly backofficeService: BackofficeService) {} @Post("organizations/:orgId/users") + @BookingStaff([FREIGHT_PERMS.staff.employeeRegistration.create, FREIGHT_PERMS.admin]) @ApiOperation({ summary: "Create an organization user without assigning positions" }) createOrganizationUser( @Param("orgId", ParseUUIDPipe) organizationId: string, @@ -31,6 +32,7 @@ export class BackofficeController { } @Get("organizations/:orgId/employees") + @BookingStaff([FREIGHT_PERMS.staff.roleAssignment.view, FREIGHT_PERMS.admin]) @ApiOperation({ summary: "Get deduplicated organization employees for backoffice" }) getOrganizationEmployees( @Param("orgId", ParseUUIDPipe) organizationId: string, @@ -44,6 +46,7 @@ export class BackofficeController { } @Get("organizations/:orgId/employee-users/:userId/roles") + @BookingStaff([FREIGHT_PERMS.staff.roleAssignment.view, FREIGHT_PERMS.admin]) @ApiOperation({ summary: "Get org-scoped roles assigned to an employee user" }) getEmployeeUserRoles( @Param("orgId", ParseUUIDPipe) organizationId: string, @@ -53,6 +56,7 @@ export class BackofficeController { } @Put("organizations/:orgId/employee-users/:userId/roles") + @BookingStaff([FREIGHT_PERMS.staff.roleAssignment.replace, FREIGHT_PERMS.admin]) @ApiOperation({ summary: "Replace org-scoped roles assigned to an employee user" }) replaceEmployeeUserRoles( @Param("orgId", ParseUUIDPipe) organizationId: string, diff --git a/apps/edr-freight-api/src/modules/billing/billing.controller.ts b/apps/edr-freight-api/src/modules/billing/billing.controller.ts index 49772316a..d72528310 100644 --- a/apps/edr-freight-api/src/modules/billing/billing.controller.ts +++ b/apps/edr-freight-api/src/modules/billing/billing.controller.ts @@ -12,14 +12,15 @@ import type { Response } from "express"; import { CurrentUser } from "@edr/api-common"; import type { TCurrentUser } from "@tria-plc/api-common/modules/auth/types/current-user.type"; -import { BookingView } from "../../common/booking-guards"; +import { BookingStaff } from "../../common/booking-guards"; +import { FREIGHT_PERMS } from "../../seed/freight-permissions.registry"; import { UserTradeAccessService } from "../user-trade-access/user-trade-access.service"; import { BillingService } from "./billing.service"; import { FilterInvoiceDto } from "./dto/filter-invoice.dto"; @ApiTags("billing") @Controller("billing") -@BookingView() +@BookingStaff(FREIGHT_PERMS.invoices.view) @ApiBearerAuth() export class BillingController { constructor( @@ -51,6 +52,7 @@ export class BillingController { } @Get("invoices/:id/document") + @BookingStaff(FREIGHT_PERMS.invoices.export) @ApiOperation({ summary: "Download the sealed invoice PDF" }) async document(@Param("id", ParseUUIDPipe) id: string, @Res() res: Response) { const { filename, buffer } = await this.billingService.document(id); @@ -58,6 +60,7 @@ export class BillingController { } @Get("invoices/:id/receipt") + @BookingStaff(FREIGHT_PERMS.invoices.export) @ApiOperation({ summary: "Download the sealed payment receipt PDF" }) async receipt(@Param("id", ParseUUIDPipe) id: string, @Res() res: Response) { const { filename, buffer } = await this.billingService.receipt(id); diff --git a/apps/edr-freight-api/src/modules/billing/portal-billing.controller.ts b/apps/edr-freight-api/src/modules/billing/portal-billing.controller.ts index 981233df0..90f6a32a4 100644 --- a/apps/edr-freight-api/src/modules/billing/portal-billing.controller.ts +++ b/apps/edr-freight-api/src/modules/billing/portal-billing.controller.ts @@ -12,6 +12,7 @@ import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger"; import type { Response } from "express"; import { CurrentUser } from "@edr/api-common"; +import { PortalCustomer } from "../../common/booking-guards"; import { type AuthUserPayload, resolveAuthUserId, @@ -28,6 +29,7 @@ import { ConfirmOtpDto, PayInvoiceDto } from "./dto/pay-invoice.dto"; @ApiTags("billing") @ApiBearerAuth() @Controller("billing") +@PortalCustomer() export class PortalBillingController { constructor(private readonly billingService: BillingService) {} diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.controller.ts b/apps/edr-freight-api/src/modules/bookings/bookings.controller.ts index 837d0d801..c36f90eed 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.controller.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.controller.ts @@ -15,13 +15,11 @@ import { UnauthorizedException, UploadedFile, UploadedFiles, - UseGuards, UseInterceptors, } from '@nestjs/common'; import { CurrentUser } from '@edr/api-common'; import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type'; -import { JwtGuard } from '@tria-plc/api-common/modules/auth/services/jwt.guard'; -import { BookingStaff, BookingView } from '../../common/booking-guards'; +import { BookingStaff, BookingView, MixedAudience, PortalCustomer } from '../../common/booking-guards'; import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry'; import { AnyFilesInterceptor, FileInterceptor } from '@nestjs/platform-express'; import { @@ -156,6 +154,7 @@ export class BookingsController { ) {} @Post() + @MixedAudience([]) @UseInterceptors(AnyFilesInterceptor()) @ApiConsumes("multipart/form-data") @ApiOperation({ summary: "Create a new freight booking (DRAFT)" }) @@ -196,6 +195,7 @@ export class BookingsController { } @Patch(":id") + @MixedAudience([]) @UseInterceptors(AnyFilesInterceptor()) @ApiConsumes("multipart/form-data") @ApiOperation({ @@ -212,6 +212,7 @@ export class BookingsController { } @Get() + @MixedAudience([]) @ApiOperation({ summary: "List freight bookings (paginated)" }) async findAll( @Query() filter: FilterBookingDto, @@ -287,6 +288,7 @@ export class BookingsController { } @Get("my") + @PortalCustomer() @ApiOperation({ summary: "List the current customer's bookings ready for payment", description: @@ -317,6 +319,7 @@ export class BookingsController { } @Get("reference-data") + @MixedAudience([]) @ApiOperation({ summary: "Booking form catalog" }) @ApiOkResponse({ type: BookingReferenceDataDto }) getReferenceData(): Promise { @@ -324,6 +327,7 @@ export class BookingsController { } @Get("by-reference/:reference") + @MixedAudience(FREIGHT_PERMS.bookings.view) @ApiOperation({ summary: "Get booking by reference" }) async findByReference( @Param("reference") reference: string, @@ -341,6 +345,7 @@ export class BookingsController { } @Get(":id") + @MixedAudience(FREIGHT_PERMS.bookings.view) @ApiOperation({ summary: "Get booking by ID" }) async findOne( @Param("id", ParseUUIDPipe) id: string, @@ -362,6 +367,7 @@ export class BookingsController { } @Get(':id/available-days') + @MixedAudience([]) @ApiOperation({ summary: 'Days bookable for THIS booking (cargo-aware wagon-TYPE gate; days only, no capacity counts)', @@ -384,6 +390,7 @@ export class BookingsController { } @Get(':id/day-availability') + @MixedAudience([]) @ApiOperation({ summary: 'Advisory free-wagon count for a shipment day (planning hint, not enforced). ' + @@ -409,6 +416,7 @@ export class BookingsController { } @Get(':id/mile-summary') + @MixedAudience(FREIGHT_PERMS.bookings.view) @ApiOperation({ summary: 'First/last-mile operational summary for a booking (customer-safe)', }) @@ -436,6 +444,7 @@ export class BookingsController { } @Post(':id/customer-truck-assignment') + @PortalCustomer() @ApiOperation({ summary: 'Customer assigns external truck and driver for terminal pickup' }) async assignCustomerTruck( @Param('id', ParseUUIDPipe) id: string, @@ -451,6 +460,7 @@ export class BookingsController { } @Get(':id/customer-truck-assignment/freight-order') + @MixedAudience(FREIGHT_PERMS.bookings.view) @ApiOperation({ summary: 'Download freight order copies. The 2 gate copies always print; ?copies=1,2,8 adds waybill-style copies (catalog indexes 1-8).', @@ -477,6 +487,7 @@ export class BookingsController { } @Get(':id/carriage-acceptance-sheet') + @MixedAudience(FREIGHT_PERMS.bookings.view) @ApiOperation({ summary: 'Download the carriage acceptance sheet (one per booking, lists every allocated wagon)', @@ -497,6 +508,11 @@ export class BookingsController { } @Get(':id/customer-trucks') + @MixedAudience([ + FREIGHT_PERMS.bookings.view, + FREIGHT_PERMS.bookings.operations, + FREIGHT_PERMS.warehouseInventory.view, + ]) @ApiOperation({ summary: 'List customer self-haul trucks (multi-truck) for a booking' }) async listCustomerTrucks( @Param('id', ParseUUIDPipe) id: string, @@ -510,6 +526,7 @@ export class BookingsController { } @Post(':id/customer-trucks') + @PortalCustomer() @ApiOperation({ summary: 'Add a customer self-haul truck carrying 1–2 of the booking containers' }) async addCustomerTruck( @Param('id', ParseUUIDPipe) id: string, @@ -524,6 +541,7 @@ export class BookingsController { } @Post(':id/customer-trucks/bulk') + @PortalCustomer() @ApiOperation({ summary: 'Bulk add customer trucks from array payload (Excel parsed)' }) async bulkAddCustomerTrucks( @Param('id', ParseUUIDPipe) id: string, @@ -538,6 +556,7 @@ export class BookingsController { } @Patch(':id/customer-trucks/:assignmentId') + @PortalCustomer() @ApiOperation({ summary: 'Edit a not-yet-arrived customer truck (plate/driver/type + containers)' }) async updateCustomerTruck( @Param('id', ParseUUIDPipe) id: string, @@ -553,6 +572,7 @@ export class BookingsController { } @Delete(':id/customer-trucks/:assignmentId') + @PortalCustomer() @ApiOperation({ summary: 'Remove a not-yet-arrived customer truck from a booking' }) async removeCustomerTruck( @Param('id', ParseUUIDPipe) id: string, @@ -567,6 +587,7 @@ export class BookingsController { } @Get(':id/customer-trucks/loadable-containers') + @MixedAudience(FREIGHT_PERMS.bookings.view) @ApiOperation({ summary: 'Booking containers not yet loaded onto a truck' }) async loadableContainers( @Param('id', ParseUUIDPipe) id: string, @@ -580,6 +601,7 @@ export class BookingsController { } @Post(':id/customer-trucks/:assignmentId/load') + @BookingStaff(FREIGHT_PERMS.bookings.operations) @ApiOperation({ summary: 'Truck_dispatch: load selected containers onto a truck (staff)' }) async loadCustomerTruck( @Param('id', ParseUUIDPipe) id: string, @@ -594,6 +616,7 @@ export class BookingsController { } @Post(':id/customer-trucks/:assignmentId/depart') + @BookingStaff(FREIGHT_PERMS.bookings.operations) @ApiOperation({ summary: 'Register an import truck leaving: containers loaded + weighed gross (staff)', }) @@ -611,6 +634,7 @@ export class BookingsController { } @Get(':id/received-pending-grn') + @MixedAudience(FREIGHT_PERMS.bookings.view) @ApiOperation({ summary: 'Containers received into port but not yet on a GRN' }) async receivedPendingGrn( @Param('id', ParseUUIDPipe) id: string, @@ -624,6 +648,7 @@ export class BookingsController { } @Post(':id/generate-grn') + @BookingStaff(FREIGHT_PERMS.bookings.operations) @ApiOperation({ summary: 'Generate a GRN over the received containers (all received, or a subset) — one GRN per batch', @@ -641,6 +666,7 @@ export class BookingsController { } @Get(':id/tracking') + @MixedAudience(FREIGHT_PERMS.bookings.view) @ApiOperation({ summary: "Shipment tracking timeline for a booking", description: @@ -663,6 +689,7 @@ export class BookingsController { } @Delete(":id") + @MixedAudience([]) @HttpCode(204) @ApiOperation({ summary: "Soft-delete DRAFT booking" }) remove(@Param("id", ParseUUIDPipe) id: string) { @@ -670,6 +697,7 @@ export class BookingsController { } @Post(":id/documents") + @MixedAudience([]) @UseInterceptors(AnyFilesInterceptor()) @ApiConsumes("multipart/form-data") @ApiOperation({ summary: "Upload documents for a booking (DRAFT only)" }) @@ -682,6 +710,7 @@ export class BookingsController { } @Post(":id/generate-price") + @MixedAudience([]) @ApiOperation({ summary: "Generate price preview (DRAFT or CHANGES_REQUESTED)", description: @@ -693,6 +722,7 @@ export class BookingsController { } @Post(":id/submit") + @MixedAudience([]) @ApiOperation({ summary: "Customer submit booking", description: @@ -704,6 +734,7 @@ export class BookingsController { } @Post(":id/confirm-submit") + @MixedAudience([]) @ApiOperation({ summary: "Confirm submit after price change", description: @@ -715,6 +746,7 @@ export class BookingsController { } @Post(":id/reject") + @PortalCustomer() @ApiOperation({ summary: "Customer reject price estimate", description: @@ -745,6 +777,7 @@ export class BookingsController { } @Get(':id/clearance') + @MixedAudience([FREIGHT_PERMS.bookings.clearanceView, FREIGHT_PERMS.bookings.reviewDocuments]) @ApiOperation({ summary: "Document-clearance grid (required docs + upload + GL review status)", @@ -754,6 +787,7 @@ export class BookingsController { } @Post(":id/clearance/documents") + @PortalCustomer() @UseInterceptors(AnyFilesInterceptor()) @ApiConsumes("multipart/form-data") @ApiOperation({ @@ -770,7 +804,10 @@ export class BookingsController { return this.transitionService.enrichBookingResponse(booking); } + // Customer requests the operation; GL ET also resubmits here on the + // customer's behalf after operations requests changes (BookingChangesRequestedAlert). @Post(":id/clearance/proceed") + @MixedAudience(FREIGHT_PERMS.contracts.createBooking) @ApiOperation({ summary: "Customer requests operation with a schedule day " + @@ -789,6 +826,7 @@ export class BookingsController { } @Get(":id/export-trains") + @MixedAudience([]) @ApiOperation({ summary: "Export train picker: the day's export trains on the booking's corridor " + @@ -990,6 +1028,7 @@ export class BookingsController { } @Post(':id/clearance/draft-declaration/accept') + @PortalCustomer() @ApiOperation({ summary: 'Customer accepts the draft customs declaration — unlocks the real customs declaration step for GL Ethiopia', @@ -1000,6 +1039,7 @@ export class BookingsController { } @Post(':id/clearance/draft-declaration/change') + @PortalCustomer() @ApiOperation({ summary: 'Customer requests a change to the draft customs declaration with a reason — GL Ethiopia sends a corrected draft (repeatable)', @@ -1026,6 +1066,7 @@ export class BookingsController { } @Post(':id/clearance/duty-slip') + @PortalCustomer() @UseInterceptors(FileInterceptor('file')) @ApiConsumes('multipart/form-data') @ApiOperation({ summary: 'Customer uploads duty/tax payment slip on booking' }) @@ -1177,7 +1218,7 @@ export class BookingsController { } @Post(":id/government-expedite") - @BookingStaff(FREIGHT_PERMS.bookings.staffAccept) + @BookingStaff(FREIGHT_PERMS.bookings.governmentExpedite) @ApiOperation({ summary: "Expedite government booking to PAID / ELIGIBLE for scheduling", }) @@ -1201,6 +1242,7 @@ export class BookingsController { } @Get(":id/contract/view") + @MixedAudience(FREIGHT_PERMS.bookings.view) @ApiOkResponse({ type: ContractViewDto }) @ApiOperation({ summary: "Contract HTML view for portal and backoffice" }) getContractView( @@ -1212,6 +1254,7 @@ export class BookingsController { } @Get(":id/contract/document") + @MixedAudience(FREIGHT_PERMS.bookings.view) @ApiOperation({ summary: "Download contract PDF" }) async downloadContractDocument( @Param("id", ParseUUIDPipe) id: string, @@ -1227,6 +1270,7 @@ export class BookingsController { } @Get(":id/contract") + @MixedAudience(FREIGHT_PERMS.bookings.view) @ApiOperation({ summary: "Download contract file (alias)" }) async downloadContract( @Param("id", ParseUUIDPipe) id: string, @@ -1236,7 +1280,7 @@ export class BookingsController { } @Post(":id/contract/sign") - @UseGuards(JwtGuard) + @MixedAudience(FREIGHT_PERMS.bookings.signStaff) @ApiOperation({ summary: "Apply digital signature (customer or staff)" }) async signContract( @Param("id", ParseUUIDPipe) id: string, @@ -1257,18 +1301,21 @@ export class BookingsController { } @Get(":id/contract/signatures") + @MixedAudience(FREIGHT_PERMS.bookings.view) @ApiOperation({ summary: "List contract signatures" }) getContractSignatures(@Param("id", ParseUUIDPipe) id: string) { return this.contractService.getSignatures(id); } @Get(":id/summary") + @MixedAudience(FREIGHT_PERMS.bookings.view) @ApiOperation({ summary: "Contract summary string for dashboard" }) getSummary(@Param("id", ParseUUIDPipe) id: string) { return this.contractService.getSummary(id); } @Post(":id/customer/sign") + @PortalCustomer() @ApiOperation({ summary: "Customer digital signature (deprecated — use POST contract/sign)", }) @@ -1336,6 +1383,7 @@ export class BookingsController { } @Post(":id/cancel-hold") + @PortalCustomer() @ApiOperation({ summary: "Customer cancels an unpaid hold (SELECTED_FOR_BATCH → CANCELLED); " + @@ -1350,18 +1398,21 @@ export class BookingsController { } @Post(":id/consolidation") + @PortalCustomer() @ApiOperation({ summary: "Request freight consolidation" }) requestConsolidation(@Param("id", ParseUUIDPipe) id: string) { return this.bookingsService.requestConsolidation(id); } @Delete(":id/consolidation") + @PortalCustomer() @ApiOperation({ summary: "Remove consolidation pairing" }) removeConsolidation(@Param("id", ParseUUIDPipe) id: string) { return this.bookingsService.removeConsolidation(id); } @Get(":id/consolidation") + @MixedAudience(FREIGHT_PERMS.bookings.view) @ApiOperation({ summary: "Get consolidation details" }) getConsolidationDetails(@Param("id", ParseUUIDPipe) id: string) { return this.bookingsService.getConsolidationDetails(id); diff --git a/apps/edr-freight-api/src/modules/companies/companies.controller.ts b/apps/edr-freight-api/src/modules/companies/companies.controller.ts index 1c9d75487..810d4d46a 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.controller.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.controller.ts @@ -11,7 +11,6 @@ import { HttpCode, HttpStatus, UseInterceptors, - UseGuards, UploadedFiles, BadRequestException, NotFoundException, @@ -20,8 +19,7 @@ import { AnyFilesInterceptor } from "@nestjs/platform-express"; import { ApiOperation, ApiTags, ApiConsumes } from "@nestjs/swagger"; import { CurrentUser } from "@edr/api-common"; import type { TCurrentUser } from "@tria-plc/api-common/modules/auth/types/current-user.type"; -import { JwtGuard } from "@tria-plc/api-common/modules/auth/services/jwt.guard"; -import { BookingStaff } from "../../common/booking-guards"; +import { BookingStaff, MixedAudience, PortalCustomer } from "../../common/booking-guards"; import { assertFreightPermission, hasFreightPermission, @@ -118,6 +116,7 @@ export class CompaniesController { } @Get("getInfo") + @PortalCustomer() @ApiOperation({ summary: "Get company info for the current user" }) async getInfo( @CurrentUser() user: CurrentIamUser, @@ -131,6 +130,7 @@ export class CompaniesController { } @Get("profile") + @PortalCustomer() @ApiOperation({ summary: "Get flattened profile for the settings page" }) async getProfile( @CurrentUser() user: CurrentIamUser, @@ -146,6 +146,7 @@ export class CompaniesController { } @Get("profile/change-request") + @PortalCustomer() @ApiOperation({ summary: "Current user's open profile change request (pending/rejected)", }) @@ -161,6 +162,7 @@ export class CompaniesController { } @Post("company-profiles/:profileId/reapply") + @PortalCustomer() @ApiOperation({ summary: "Resubmit a rejected operational role for approval (→ pending)", }) @@ -176,6 +178,7 @@ export class CompaniesController { } @Get("dashboard") + @PortalCustomer() @ApiOperation({ summary: "Get portal dashboard KPIs (delivered, spend, freight volume) for the current user", @@ -191,6 +194,7 @@ export class CompaniesController { } @Post("fetch-etrade-info") + @PortalCustomer() @ApiOperation({ summary: "Fetch company info from eTrade by TIN" }) async fetchETradeInfo( @CurrentUser() user: CurrentIamUser, @@ -211,6 +215,7 @@ export class CompaniesController { } @Patch("profile") + @PortalCustomer() @ApiOperation({ summary: "Update profile (flattened settings page)" }) async updateProfile( @CurrentUser() user: CurrentIamUser, @@ -220,6 +225,7 @@ export class CompaniesController { } @Post("company-profiles") + @PortalCustomer() @ApiOperation({ summary: "Add operational profile(s) (importer/exporter/forwarder) to the current user's company", @@ -236,6 +242,7 @@ export class CompaniesController { } @Post("onboarding/start") + @PortalCustomer() @ApiOperation({ summary: "Begin onboarding: create a draft company + profile + role(s) so later steps can save incrementally", @@ -261,6 +268,7 @@ export class CompaniesController { } @Post("company-profile") + @PortalCustomer() @ApiOperation({ summary: "Create a single operational profile for the current user's company. The role starts pending and does not become the active mode", @@ -278,6 +286,7 @@ export class CompaniesController { } @Post("company-profiles/:profileId/license") + @PortalCustomer() @UseInterceptors(AnyFilesInterceptor()) @ApiConsumes("multipart/form-data") @ApiOperation({ @@ -298,6 +307,7 @@ export class CompaniesController { } @Post("company-profiles/:profileId/license/:fileId/replace") + @PortalCustomer() @UseInterceptors(AnyFilesInterceptor()) @ApiConsumes("multipart/form-data") @ApiOperation({ @@ -324,6 +334,7 @@ export class CompaniesController { } @Delete("company-profiles/:profileId/license/:fileId") + @PortalCustomer() @ApiOperation({ summary: "Remove a business-license file (staged for review on an approved company).", @@ -341,6 +352,7 @@ export class CompaniesController { } @Get("company-profiles/:profileId/license") + @PortalCustomer() @ApiOperation({ summary: "List business-license documents (with review state) for a profile", }) @@ -352,6 +364,7 @@ export class CompaniesController { } @Get("poa-delegation") + @PortalCustomer() @ApiOperation({ summary: "List the Power of Attorney delegation letter (with review state) for the current user's company", @@ -363,6 +376,7 @@ export class CompaniesController { } @Post("poa-delegation") + @PortalCustomer() @UseInterceptors(AnyFilesInterceptor()) @ApiConsumes("multipart/form-data") @ApiOperation({ @@ -383,6 +397,7 @@ export class CompaniesController { } @Delete("poa-delegation/:fileId") + @PortalCustomer() @ApiOperation({ summary: "Remove the Power of Attorney delegation letter (staged for review on an approved company).", @@ -395,6 +410,7 @@ export class CompaniesController { } @Post("identity/fayda/complete") + @PortalCustomer() @ApiOperation({ summary: "Bind a completed Fayda verification to the company's owner or Power of Attorney. " + @@ -409,6 +425,7 @@ export class CompaniesController { } @Post("identity/gm/same-as-owner") + @PortalCustomer() @ApiOperation({ summary: "Declare the General Manager is the company's owner, copying the owner's verified identity across. " + @@ -421,6 +438,7 @@ export class CompaniesController { } @Delete("identity/gm") + @PortalCustomer() @ApiOperation({ summary: "Clear the General Manager's identity — the \"same as owner\" declaration or a verification, and the details either wrote. " + @@ -433,6 +451,7 @@ export class CompaniesController { } @Delete("identity/fayda/poa") + @PortalCustomer() @ApiOperation({ summary: "Remove the company's Power of Attorney — the verified identity, its details and the delegation paper together. " + @@ -445,6 +464,7 @@ export class CompaniesController { } @Patch("onboarding-step") + @PortalCustomer() @ApiOperation({ summary: "Persist the user's current onboarding wizard step" }) @HttpCode(HttpStatus.NO_CONTENT) async setOnboardingStep( @@ -455,6 +475,7 @@ export class CompaniesController { } @Get("onboarding/requirements") + @PortalCustomer() @ApiOperation({ summary: "What the current user's company still needs to finish onboarding (server-driven documents + outstanding items)", @@ -466,6 +487,7 @@ export class CompaniesController { } @Post("onboarding/complete") + @PortalCustomer() @ApiOperation({ summary: "Mark the current user's onboarding as complete" }) async completeOnboarding( @CurrentUser() user: CurrentIamUser, @@ -477,6 +499,7 @@ export class CompaniesController { // Used by portal @Post("create") + @PortalCustomer() @ApiOperation({ summary: "Create a company with its associated external profile (onboarding)", @@ -591,7 +614,11 @@ export class CompaniesController { * permission still needs the applicant's documents. */ @Get(":companyId/documents") - @UseGuards(JwtGuard) + @MixedAudience([ + FREIGHT_PERMS.customers.view, + FREIGHT_PERMS.contracts.view, + FREIGHT_PERMS.bookings.view, + ]) @ApiOperation({ summary: "List documents uploaded for a company" }) async listDocuments( @Param("companyId", ParseUUIDPipe) companyId: string, @@ -661,6 +688,7 @@ export class CompaniesController { } @Post(":companyId/documents") + @MixedAudience(FREIGHT_PERMS.customers.update) @UseInterceptors(AnyFilesInterceptor()) @ApiConsumes("multipart/form-data") @ApiOperation({ summary: "Upload documents for a company (onboarding)" }) diff --git a/apps/edr-freight-api/src/modules/compliance/compliance.controller.ts b/apps/edr-freight-api/src/modules/compliance/compliance.controller.ts index 2a5715647..0e5949300 100644 --- a/apps/edr-freight-api/src/modules/compliance/compliance.controller.ts +++ b/apps/edr-freight-api/src/modules/compliance/compliance.controller.ts @@ -1,5 +1,7 @@ import { Controller, Post, Get, Patch, Delete, Body, Param, Query } from '@nestjs/common'; import { ApiTags, ApiOperation } from '@nestjs/swagger'; +import { BookingStaff } from '../../common/booking-guards'; +import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry'; import { ComplianceService } from './compliance.service'; import { CreateComplianceRecordDto, @@ -9,10 +11,12 @@ import { ComplianceType } from './entities/compliance-record.entity'; @ApiTags('Vehicle Compliance') @Controller('compliance') +@BookingStaff(FREIGHT_PERMS.compliance.view) export class ComplianceController { constructor(private readonly complianceService: ComplianceService) {} @Post() + @BookingStaff(FREIGHT_PERMS.compliance.manage) @ApiOperation({ summary: 'Create a compliance record' }) create(@Body() dto: CreateComplianceRecordDto) { return this.complianceService.create(dto); @@ -40,12 +44,14 @@ export class ComplianceController { } @Patch(':id') + @BookingStaff(FREIGHT_PERMS.compliance.manage) @ApiOperation({ summary: 'Update a compliance record' }) update(@Param('id') id: string, @Body() dto: UpdateComplianceRecordDto) { return this.complianceService.update(id, dto); } @Delete(':id') + @BookingStaff(FREIGHT_PERMS.compliance.manage) @ApiOperation({ summary: 'Soft-delete a compliance record' }) remove(@Param('id') id: string) { return this.complianceService.remove(id); diff --git a/apps/edr-freight-api/src/modules/contract-templates/contract-templates.controller.ts b/apps/edr-freight-api/src/modules/contract-templates/contract-templates.controller.ts index 8cf1c5671..67f6ededc 100644 --- a/apps/edr-freight-api/src/modules/contract-templates/contract-templates.controller.ts +++ b/apps/edr-freight-api/src/modules/contract-templates/contract-templates.controller.ts @@ -10,7 +10,8 @@ import { } from "@nestjs/common"; import { ApiOperation, ApiTags } from "@nestjs/swagger"; -import { FreightAdmin } from "../../common/booking-guards"; +import { BookingStaff } from "../../common/booking-guards"; +import { FREIGHT_PERMS } from "../../seed/freight-permissions.registry"; import { ContractTemplatesService } from "./contract-templates.service"; import { CreateArticleDto, @@ -41,7 +42,7 @@ export class ContractTemplatesController { } @Patch(":code") - @FreightAdmin() + @BookingStaff([FREIGHT_PERMS.settings.contractTemplates.manage, FREIGHT_PERMS.admin]) @ApiOperation({ summary: "Update template metadata (name, title, recitals, active flag)" }) update(@Param("code") code: string, @Body() dto: UpdateContractTemplateDto) { return this.service.update(code, dto); @@ -61,21 +62,21 @@ export class ContractTemplatesController { /* ------------------------- article routes ------------------------- */ @Put(":code/articles") - @FreightAdmin() + @BookingStaff([FREIGHT_PERMS.settings.contractTemplates.manage, FREIGHT_PERMS.admin]) @ApiOperation({ summary: "Replace the full ordered article list (used for reorder)" }) replaceArticles(@Param("code") code: string, @Body() dto: ReplaceArticlesDto) { return this.service.replaceArticles(code, dto.articles); } @Post(":code/articles") - @FreightAdmin() + @BookingStaff([FREIGHT_PERMS.settings.contractTemplates.manage, FREIGHT_PERMS.admin]) @ApiOperation({ summary: "Add an article to the template" }) addArticle(@Param("code") code: string, @Body() dto: CreateArticleDto) { return this.service.addArticle(code, dto); } @Patch(":code/articles/:articleId") - @FreightAdmin() + @BookingStaff([FREIGHT_PERMS.settings.contractTemplates.manage, FREIGHT_PERMS.admin]) @ApiOperation({ summary: "Update an article's title or body" }) updateArticle( @Param("code") code: string, @@ -86,7 +87,7 @@ export class ContractTemplatesController { } @Delete(":code/articles/:articleId") - @FreightAdmin() + @BookingStaff([FREIGHT_PERMS.settings.contractTemplates.manage, FREIGHT_PERMS.admin]) @ApiOperation({ summary: "Remove an article from the template" }) removeArticle( @Param("code") code: string, diff --git a/apps/edr-freight-api/src/modules/contracts/contracts.controller.ts b/apps/edr-freight-api/src/modules/contracts/contracts.controller.ts index ec61fdd4a..10a1f42d2 100644 --- a/apps/edr-freight-api/src/modules/contracts/contracts.controller.ts +++ b/apps/edr-freight-api/src/modules/contracts/contracts.controller.ts @@ -14,12 +14,10 @@ import { UnauthorizedException, UploadedFiles, UploadedFile, - UseGuards, UseInterceptors, } from '@nestjs/common'; import { CurrentUser } from '@edr/api-common'; import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type'; -import { JwtGuard } from '@tria-plc/api-common/modules/auth/services/jwt.guard'; import { AnyFilesInterceptor, FileInterceptor } from '@nestjs/platform-express'; import type { Response } from 'express'; import { @@ -32,7 +30,7 @@ import { } from '@nestjs/swagger'; import { actorLabel } from '../warehouses/current-actor.util'; -import { BookingStaff } from '../../common/booking-guards'; +import { BookingStaff, MixedAudience, PortalCustomer } from '../../common/booking-guards'; import { ContractDocumentHistoryService } from './contract-document-history.service'; import { FREIGHT_PERMS, @@ -127,6 +125,7 @@ export class ContractsController { } @Get('booking-requests/:reqId') + @MixedAudience(FREIGHT_PERMS.contracts.view) @ApiOperation({ summary: 'A single shipment request' }) getBookingRequest(@Param('reqId', ParseUUIDPipe) reqId: string) { return this.bookingRequestService.findOne(reqId); @@ -159,6 +158,7 @@ export class ContractsController { } @Post('booking-requests/:reqId/cancel') + @PortalCustomer() @ApiOperation({ summary: 'Customer cancels their own pending shipment request' }) cancelBookingRequest( @Param('reqId', ParseUUIDPipe) reqId: string, @@ -168,6 +168,7 @@ export class ContractsController { } @Post(':id/booking-requests') + @PortalCustomer() @ApiOperation({ summary: 'Customer submits a shipment request on a GENERAL customs contract' }) submitBookingRequest( @Param('id', ParseUUIDPipe) id: string, @@ -178,12 +179,14 @@ export class ContractsController { } @Get(':id/booking-requests') + @MixedAudience(FREIGHT_PERMS.contracts.view) @ApiOperation({ summary: 'List the shipment requests on a contract' }) listBookingRequests(@Param('id', ParseUUIDPipe) id: string) { return this.bookingRequestService.listForContract(id); } @Post() + @MixedAudience([]) @UseInterceptors(AnyFilesInterceptor()) @ApiConsumes('multipart/form-data') @ApiOperation({ summary: 'Create a new contract (DRAFT) with routes + cargo scope' }) @@ -203,6 +206,7 @@ export class ContractsController { } @Get() + @MixedAudience([]) @ApiOperation({ summary: 'List contracts (paginated)' }) async findAll( @Query() filter: FilterContractDto, @@ -247,6 +251,7 @@ export class ContractsController { } @Get('my') + @PortalCustomer() @ApiOperation({ summary: "List the current customer's contracts" }) async findMy( @CurrentUser() user: AuthUserPayload, @@ -301,6 +306,7 @@ export class ContractsController { } @Get(':id') + @MixedAudience(FREIGHT_PERMS.contracts.view) @ApiOperation({ summary: 'Get contract by ID (routes, cargo scope, unit rates)' }) async findOne( @Param('id', ParseUUIDPipe) id: string, @@ -319,6 +325,7 @@ export class ContractsController { } @Patch(':id') + @MixedAudience([]) @UseInterceptors(AnyFilesInterceptor()) @ApiConsumes('multipart/form-data') @ApiOperation({ @@ -337,6 +344,7 @@ export class ContractsController { } @Delete(':id') + @MixedAudience([]) @HttpCode(204) @ApiOperation({ summary: 'Soft-delete DRAFT contract' }) remove(@Param('id', ParseUUIDPipe) id: string) { @@ -344,6 +352,7 @@ export class ContractsController { } @Post(':id/documents') + @MixedAudience([]) @UseInterceptors(AnyFilesInterceptor()) @ApiConsumes('multipart/form-data') @ApiOperation({ summary: 'Upload intake documents for a contract (DRAFT only)' }) @@ -355,18 +364,21 @@ export class ContractsController { } @Post(':id/generate-price') + @MixedAudience([]) @ApiOperation({ summary: 'Generate unit-rate breakdown (no totals at contract phase)' }) generatePrice(@Param('id', ParseUUIDPipe) id: string) { return this.pricingService.generatePrice(id); } @Post(':id/submit') + @MixedAudience([]) @ApiOperation({ summary: 'Customer submit contract (freezes contract_rate_snapshots)' }) submit(@Param('id', ParseUUIDPipe) id: string) { return this.transitionService.submit(id); } @Post(':id/confirm-submit') + @MixedAudience([]) @ApiOperation({ summary: 'Confirm submit after a price change' }) confirmSubmit(@Param('id', ParseUUIDPipe) id: string) { return this.transitionService.confirmSubmit(id); @@ -427,10 +439,7 @@ export class ContractsController { // real boundary: it admits only the approver whose step is currently pending // (edit rights hand off down the chain on each approval). @Put(':id/document/articles') - @BookingStaff([ - FREIGHT_PERMS.contracts.view, - ...bothFreightTypes(FREIGHT_PERMS.contracts.staffAccept), - ]) + @BookingStaff(FREIGHT_PERMS.contracts.editDocument) @ApiOperation({ summary: 'Edit this contract\'s document articles only (per-contract; never touches the six shared templates)', @@ -515,6 +524,7 @@ export class ContractsController { } @Post(':id/cancel') + @PortalCustomer() @ApiOperation({ summary: 'Customer cancels their own contract (blocked while a booking is live)', }) @@ -591,6 +601,7 @@ export class ContractsController { } @Get(':id/contract/view') + @MixedAudience(FREIGHT_PERMS.contracts.view) @ApiOperation({ summary: 'Contract PDF view-model + rendered HTML for signing' }) async getContractView( @Param('id', ParseUUIDPipe) id: string, @@ -630,6 +641,7 @@ export class ContractsController { } @Get(':id/contract/document') + @MixedAudience(FREIGHT_PERMS.contracts.view) @ApiOperation({ summary: 'Download contract PDF' }) async downloadContractDocument( @Param('id', ParseUUIDPipe) id: string, @@ -653,7 +665,7 @@ export class ContractsController { } @Post(':id/contract/send-signing-otp') - @UseGuards(JwtGuard) + @MixedAudience(bothFreightTypes(FREIGHT_PERMS.contracts.signStaff)) @ApiOperation({ summary: "Send the sudo-mode signing OTP to the contract company's registered phone (server picks the number)", @@ -666,7 +678,7 @@ export class ContractsController { } @Post(':id/contract/sign') - @UseGuards(JwtGuard) + @MixedAudience(bothFreightTypes(FREIGHT_PERMS.contracts.signStaff)) @ApiOperation({ summary: 'Apply digital signature (customer or staff/director/ceo)' }) async signContract( @Param('id', ParseUUIDPipe) id: string, @@ -691,6 +703,7 @@ export class ContractsController { } @Post(':id/renew') + @PortalCustomer() @ApiOperation({ summary: 'Create a renewal draft linked via renewalOfId' }) async renew( @Param('id', ParseUUIDPipe) id: string, @@ -712,12 +725,14 @@ export class ContractsController { // ── Pre-booking clearance (Path B, doc §15.2.1) ──────────────────────────── @Get(':id/clearance') + @MixedAudience(FREIGHT_PERMS.contracts.view) @ApiOperation({ summary: 'Pre-booking clearance document grid on the contract' }) getClearance(@Param('id', ParseUUIDPipe) id: string) { return this.clearanceService.getClearanceView(id); } @Post(':id/clearance/documents') + @PortalCustomer() @UseInterceptors(AnyFilesInterceptor()) @ApiConsumes('multipart/form-data') @ApiOperation({ summary: 'Customer uploads clearance documents (fieldname = document key)' }) @@ -901,6 +916,7 @@ export class ContractsController { } @Post(':id/clearance/duty/dispute') + @PortalCustomer() @ApiOperation({ summary: 'Customer disputes the advised duty/tax with a reason — reopens the step so GL Ethiopia can re-advise (repeatable)', @@ -914,6 +930,7 @@ export class ContractsController { } @Post(':id/clearance/duty-slip') + @PortalCustomer() @UseInterceptors(FileInterceptor('file')) @ApiConsumes('multipart/form-data') @ApiOperation({ summary: 'Customer uploads duty/tax payment slip on contract' }) @@ -1057,6 +1074,7 @@ export class ContractsController { // ── Booking under contract (Path A customer / Path B GL ET) ──────────────── @Post(':id/bookings') + @BookingStaff(FREIGHT_PERMS.contracts.createBooking) @ApiOperation({ summary: 'Create a shipment booking under a contract — Path A (customer) or Path B (GL Ethiopia).', @@ -1078,6 +1096,7 @@ export class ContractsController { } @Post(':id/bookings/initiate') + @BookingStaff(FREIGHT_PERMS.contracts.createBooking) @ApiOperation({ summary: 'Initiate a bare booking instance under an import/export contract (ONE_TIME or GENERAL) — no cargo, no date; enters per-booking clearance (AWAITING_DOCUMENTS). ONE_TIME customs instances are opened by the customer (or GL); GENERAL customs comes from a shipment request.', @@ -1096,6 +1115,7 @@ export class ContractsController { } @Post(':id/bookings/:bookingId/complete') + @BookingStaff(FREIGHT_PERMS.contracts.createBooking) @ApiOperation({ summary: 'Complete an initiated booking after Operations finalized its clearance — cargo + binding day, window and departure checks, pricing and invoicing.', @@ -1117,6 +1137,7 @@ export class ContractsController { } @Post(':id/validate-shipment') + @MixedAudience([FREIGHT_PERMS.contracts.createBooking, FREIGHT_PERMS.contracts.view]) @ApiOperation({ summary: 'Pre-create validation + authoritative price preview: full booking price breakdown (rail, first/last mile, surcharges), overweight lines and 20ft weight-pairing errors for a shipment payload (no booking created).', @@ -1129,6 +1150,7 @@ export class ContractsController { } @Get(':id/capacity') + @MixedAudience(FREIGHT_PERMS.contracts.view) @ApiOperation({ summary: 'Remaining bookable quantity per cargo line (GENERAL draw-down cap, or the outstanding remainder of a split ONE_TIME contract)', @@ -1141,12 +1163,14 @@ export class ContractsController { // ── Clearance milestones (doc §11.3, §12.2) ──────────────────────────────── @Get(':id/milestones') + @MixedAudience(FREIGHT_PERMS.contracts.view) @ApiOperation({ summary: 'Pre-booking clearance milestones for a contract cycle' }) listContractMilestones(@Param('id', ParseUUIDPipe) id: string) { return this.milestoneService.listForContract(id); } @Get('bookings/:bookingId/milestones') + @MixedAudience(FREIGHT_PERMS.contracts.view) @ApiOperation({ summary: 'Post-booking clearance milestones for a shipment booking' }) listBookingMilestones(@Param('bookingId', ParseUUIDPipe) bookingId: string) { return this.milestoneService.listForBooking(bookingId); @@ -1280,7 +1304,7 @@ export class ContractsController { } @Post('bookings/:bookingId/final-invoice') - @BookingStaff(FREIGHT_PERMS.contracts.clearanceDjActions) + @BookingStaff(FREIGHT_PERMS.contracts.finalInvoiceRaise) @UseInterceptors(FileInterceptor('file')) @ApiConsumes('multipart/form-data') @ApiOperation({ @@ -1307,6 +1331,7 @@ export class ContractsController { } @Post('bookings/:bookingId/final-invoice/approve') + @PortalCustomer() @ApiOperation({ summary: 'Customer approves the drafted final invoice — unlocks the payment slip', }) @@ -1321,6 +1346,7 @@ export class ContractsController { } @Post('bookings/:bookingId/final-invoice-slip') + @PortalCustomer() @UseInterceptors(FileInterceptor('file')) @ApiConsumes('multipart/form-data') @ApiOperation({ summary: 'Customer attaches the payment slip for the final invoice' }) @@ -1332,10 +1358,7 @@ export class ContractsController { } @Post('bookings/:bookingId/final-invoice/confirm') - @BookingStaff([ - FREIGHT_PERMS.contracts.clearanceDjActions, - FREIGHT_PERMS.contracts.clearanceEtActions, - ]) + @BookingStaff(FREIGHT_PERMS.contracts.finalInvoiceConfirm) @ApiOperation({ summary: 'GL (ET or DJ) confirms the payment slip — settles the final invoice' }) confirmFinalInvoicePaid( @Param('bookingId', ParseUUIDPipe) bookingId: string, @@ -1378,6 +1401,7 @@ export class ContractsController { } @Post('bookings/:bookingId/second-duty-slip') + @PortalCustomer() @UseInterceptors(FileInterceptor('file')) @ApiConsumes('multipart/form-data') @ApiOperation({ summary: 'Customer attaches the additional duty/tax payment slip' }) @@ -1403,6 +1427,7 @@ export class ContractsController { } @Post('bookings/:bookingId/duty-slip') + @PortalCustomer() @UseInterceptors(AnyFilesInterceptor()) @ApiConsumes('multipart/form-data') @ApiOperation({ summary: 'Customer uploads the duty/tax payment slip' }) @@ -1419,6 +1444,7 @@ export class ContractsController { } @Get('bookings/:bookingId/incidents') + @MixedAudience(FREIGHT_PERMS.contracts.view) @ApiOperation({ summary: 'List cargo exception/damage reports for a shipment' }) listIncidents(@Param('bookingId', ParseUUIDPipe) bookingId: string) { return this.glOperationsService.listIncidents(bookingId); diff --git a/apps/edr-freight-api/src/modules/demo-permissions/demo-permissions.controller.ts b/apps/edr-freight-api/src/modules/demo-permissions/demo-permissions.controller.ts deleted file mode 100644 index 6f425e1bb..000000000 --- a/apps/edr-freight-api/src/modules/demo-permissions/demo-permissions.controller.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Controller, Get, UseGuards } from "@nestjs/common"; -import { ApiOperation, ApiTags } from "@nestjs/swagger"; - -import { PermissionGuard } from "@tria-plc/api-common/modules/auth/services/permission.guard"; - -@ApiTags("demo-permissions") -@Controller() -export class DemoPermissionsController { - @Get("test_user1") - @ApiOperation({ summary: "Permission demo (can:demo:user1)" }) - @UseGuards(PermissionGuard(["can:demo:user1"])) - testUser1() { - return { ok: true, permission: "can:demo:user1" }; - } - - @Get("test_user2") - @ApiOperation({ summary: "Permission demo (can:demo:user2)" }) - @UseGuards(PermissionGuard(["can:demo:user2"])) - testUser2() { - return { ok: true, permission: "can:demo:user2" }; - } -} diff --git a/apps/edr-freight-api/src/modules/demo-permissions/demo-permissions.module.ts b/apps/edr-freight-api/src/modules/demo-permissions/demo-permissions.module.ts deleted file mode 100644 index db73ed728..000000000 --- a/apps/edr-freight-api/src/modules/demo-permissions/demo-permissions.module.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Module } from "@nestjs/common"; - -import { DemoPermissionsController } from "./demo-permissions.controller"; - -@Module({ - controllers: [DemoPermissionsController], -}) -export class DemoPermissionsModule {} diff --git a/apps/edr-freight-api/src/modules/dropdown-settings/dropdown-settings.controller.ts b/apps/edr-freight-api/src/modules/dropdown-settings/dropdown-settings.controller.ts index aad904557..aaf552f40 100644 --- a/apps/edr-freight-api/src/modules/dropdown-settings/dropdown-settings.controller.ts +++ b/apps/edr-freight-api/src/modules/dropdown-settings/dropdown-settings.controller.ts @@ -14,7 +14,8 @@ import { } from "@nestjs/common"; import { ApiOperation, ApiTags } from "@nestjs/swagger"; -import { FreightAdmin } from "../../common/booking-guards"; +import { BookingStaff } from "../../common/booking-guards"; +import { FREIGHT_PERMS } from "../../seed/freight-permissions.registry"; import { CreateDropdownOptionDto } from "./dto/create-dropdown-option.dto"; import { CreateDropdownSettingDto } from "./dto/create-dropdown-setting.dto"; import { ListDropdownSettingsQueryDto } from "./dto/list-dropdown-settings-query.dto"; @@ -58,14 +59,14 @@ export class DropdownSettingsController { } @Post() - @FreightAdmin() + @BookingStaff([FREIGHT_PERMS.settings.dropdown.manage, FREIGHT_PERMS.admin]) @ApiOperation({ summary: "Create a new dropdown setting" }) create(@Body() dto: CreateDropdownSettingDto) { return this.service.create(dto); } @Patch(":id") - @FreightAdmin() + @BookingStaff([FREIGHT_PERMS.settings.dropdown.manage, FREIGHT_PERMS.admin]) @ApiOperation({ summary: "Update a dropdown setting's metadata" }) update( @Param("id", ParseUUIDPipe) id: string, @@ -75,7 +76,7 @@ export class DropdownSettingsController { } @Delete(":id") - @FreightAdmin() + @BookingStaff([FREIGHT_PERMS.settings.dropdown.manage, FREIGHT_PERMS.admin]) @ApiOperation({ summary: "Soft-delete a dropdown setting" }) @HttpCode(HttpStatus.NO_CONTENT) remove(@Param("id", ParseUUIDPipe) id: string) { @@ -85,7 +86,7 @@ export class DropdownSettingsController { /* ------------------------- option routes ------------------------- */ @Put(":id/options") - @FreightAdmin() + @BookingStaff([FREIGHT_PERMS.settings.dropdown.manage, FREIGHT_PERMS.admin]) @ApiOperation({ summary: "Replace the full option list for a setting" }) replaceOptions( @Param("id", ParseUUIDPipe) id: string, @@ -95,7 +96,7 @@ export class DropdownSettingsController { } @Post(":id/options") - @FreightAdmin() + @BookingStaff([FREIGHT_PERMS.settings.dropdown.manage, FREIGHT_PERMS.admin]) @ApiOperation({ summary: "Append a single option to a setting" }) addOption( @Param("id", ParseUUIDPipe) id: string, @@ -105,7 +106,7 @@ export class DropdownSettingsController { } @Patch("options/:optionId") - @FreightAdmin() + @BookingStaff([FREIGHT_PERMS.settings.dropdown.manage, FREIGHT_PERMS.admin]) @ApiOperation({ summary: "Update a single option" }) updateOption( @Param("optionId", ParseUUIDPipe) optionId: string, @@ -115,7 +116,7 @@ export class DropdownSettingsController { } @Delete("options/:optionId") - @FreightAdmin() + @BookingStaff([FREIGHT_PERMS.settings.dropdown.manage, FREIGHT_PERMS.admin]) @ApiOperation({ summary: "Soft-delete a single option" }) @HttpCode(HttpStatus.NO_CONTENT) removeOption(@Param("optionId", ParseUUIDPipe) optionId: string) { diff --git a/apps/edr-freight-api/src/modules/exchange-settings/exchange-settings.controller.ts b/apps/edr-freight-api/src/modules/exchange-settings/exchange-settings.controller.ts index a6c08a317..001fc90d2 100644 --- a/apps/edr-freight-api/src/modules/exchange-settings/exchange-settings.controller.ts +++ b/apps/edr-freight-api/src/modules/exchange-settings/exchange-settings.controller.ts @@ -3,7 +3,8 @@ import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger"; import { CurrentUser } from "@edr/api-common"; import type { TCurrentUser } from "@tria-plc/api-common/modules/auth/types/current-user.type"; -import { FreightAdmin } from "../../common/booking-guards"; +import { BookingStaff } from "../../common/booking-guards"; +import { FREIGHT_PERMS } from "../../seed/freight-permissions.registry"; import { UpdateExchangeSettingDto } from "./dto/update-exchange-setting.dto"; import { ExchangeSettingsService } from "./exchange-settings.service"; @@ -14,7 +15,7 @@ export class ExchangeSettingsController { constructor(private readonly service: ExchangeSettingsService) {} @Get() - @FreightAdmin() + @BookingStaff([FREIGHT_PERMS.settings.exchangeRate.view, FREIGHT_PERMS.admin]) @ApiOperation({ summary: "Current USD→ETB fallback rate and CBE feed health", }) @@ -32,7 +33,7 @@ export class ExchangeSettingsController { } @Patch() - @FreightAdmin() + @BookingStaff([FREIGHT_PERMS.settings.exchangeRate.manage, FREIGHT_PERMS.admin]) @ApiOperation({ summary: "Set the USD→ETB fallback by hand (used only while CBE is unreachable)", diff --git a/apps/edr-freight-api/src/modules/facilities/facilities.controller.ts b/apps/edr-freight-api/src/modules/facilities/facilities.controller.ts index 25fbbc365..451c4d03b 100644 --- a/apps/edr-freight-api/src/modules/facilities/facilities.controller.ts +++ b/apps/edr-freight-api/src/modules/facilities/facilities.controller.ts @@ -1,6 +1,8 @@ import { Body, Controller, Delete, Get, HttpCode, Param, Patch, Post } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { BookingStaff } from '../../common/booking-guards'; +import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry'; import { CreateFacilityDto } from './dto/create-facility.dto'; import { UpdateFacilityDto } from './dto/update-facility.dto'; import { Facility } from './entities/facility.entity'; @@ -8,10 +10,12 @@ import { FacilitiesService } from './facilities.service'; @ApiTags('Facilities') @Controller('facilities') +@BookingStaff(FREIGHT_PERMS.facilities.view) export class FacilitiesController { constructor(private readonly facilitiesService: FacilitiesService) {} @Post() + @BookingStaff(FREIGHT_PERMS.facilities.manage) @ApiOperation({ summary: 'Create a new facility' }) async create(@Body() createFacilityDto: CreateFacilityDto): Promise { return this.facilitiesService.create(createFacilityDto); @@ -30,6 +34,7 @@ export class FacilitiesController { } @Patch(':id') + @BookingStaff(FREIGHT_PERMS.facilities.manage) @ApiOperation({ summary: 'Update a facility' }) async update(@Param('id') id: string, @Body() updateFacilityDto: UpdateFacilityDto): Promise { return this.facilitiesService.update(id, updateFacilityDto); @@ -37,6 +42,7 @@ export class FacilitiesController { @Delete(':id') @HttpCode(204) + @BookingStaff(FREIGHT_PERMS.facilities.manage) @ApiOperation({ summary: 'Delete a facility (soft delete)' }) async remove(@Param('id') id: string): Promise { return this.facilitiesService.remove(id); diff --git a/apps/edr-freight-api/src/modules/file-upload-settings/file-upload-settings.controller.ts b/apps/edr-freight-api/src/modules/file-upload-settings/file-upload-settings.controller.ts index 661339902..ab4ae98f0 100644 --- a/apps/edr-freight-api/src/modules/file-upload-settings/file-upload-settings.controller.ts +++ b/apps/edr-freight-api/src/modules/file-upload-settings/file-upload-settings.controller.ts @@ -13,7 +13,8 @@ import { } from "@nestjs/common"; import { ApiOperation, ApiTags } from "@nestjs/swagger"; -import { FreightAdmin } from "../../common/booking-guards"; +import { BookingStaff } from "../../common/booking-guards"; +import { FREIGHT_PERMS } from "../../seed/freight-permissions.registry"; import { CreateFileUploadFieldDto } from "./dto/create-file-upload-field.dto"; import { CreateFileUploadSettingDto } from "./dto/create-file-upload-setting.dto"; import { UpdateFileUploadFieldDto } from "./dto/update-file-upload-field.dto"; @@ -53,14 +54,14 @@ export class FileUploadSettingsController { } @Post() - @FreightAdmin() + @BookingStaff([FREIGHT_PERMS.settings.fileUpload.manage, FREIGHT_PERMS.admin]) @ApiOperation({ summary: "Create a new file upload setting" }) create(@Body() dto: CreateFileUploadSettingDto) { return this.service.create(dto); } @Patch(":id") - @FreightAdmin() + @BookingStaff([FREIGHT_PERMS.settings.fileUpload.manage, FREIGHT_PERMS.admin]) @ApiOperation({ summary: "Update a file upload setting's metadata" }) update( @Param("id", ParseUUIDPipe) id: string, @@ -70,7 +71,7 @@ export class FileUploadSettingsController { } @Delete(":id") - @FreightAdmin() + @BookingStaff([FREIGHT_PERMS.settings.fileUpload.manage, FREIGHT_PERMS.admin]) @ApiOperation({ summary: "Soft-delete a file upload setting" }) @HttpCode(HttpStatus.NO_CONTENT) remove(@Param("id", ParseUUIDPipe) id: string) { @@ -80,7 +81,7 @@ export class FileUploadSettingsController { /* ------------------------- field routes ------------------------- */ @Put(":id/fields") - @FreightAdmin() + @BookingStaff([FREIGHT_PERMS.settings.fileUpload.manage, FREIGHT_PERMS.admin]) @ApiOperation({ summary: "Replace the full field list for a setting" }) replaceFields( @Param("id", ParseUUIDPipe) id: string, @@ -90,7 +91,7 @@ export class FileUploadSettingsController { } @Post(":id/fields") - @FreightAdmin() + @BookingStaff([FREIGHT_PERMS.settings.fileUpload.manage, FREIGHT_PERMS.admin]) @ApiOperation({ summary: "Append a single field to a setting" }) addField( @Param("id", ParseUUIDPipe) id: string, @@ -100,7 +101,7 @@ export class FileUploadSettingsController { } @Patch("fields/:fieldId") - @FreightAdmin() + @BookingStaff([FREIGHT_PERMS.settings.fileUpload.manage, FREIGHT_PERMS.admin]) @ApiOperation({ summary: "Update a single field" }) updateField( @Param("fieldId", ParseUUIDPipe) fieldId: string, @@ -110,7 +111,7 @@ export class FileUploadSettingsController { } @Delete("fields/:fieldId") - @FreightAdmin() + @BookingStaff([FREIGHT_PERMS.settings.fileUpload.manage, FREIGHT_PERMS.admin]) @ApiOperation({ summary: "Soft-delete a single field" }) @HttpCode(HttpStatus.NO_CONTENT) removeField(@Param("fieldId", ParseUUIDPipe) fieldId: string) { diff --git a/apps/edr-freight-api/src/modules/files/files.controller.ts b/apps/edr-freight-api/src/modules/files/files.controller.ts index fc58a731c..139da9fbd 100644 --- a/apps/edr-freight-api/src/modules/files/files.controller.ts +++ b/apps/edr-freight-api/src/modules/files/files.controller.ts @@ -16,6 +16,7 @@ import { } from "@nestjs/swagger"; import { Response } from "express"; +import { MixedAudience } from "../../common/booking-guards"; import { FilesService } from "./files.service"; @ApiTags("files") @@ -25,6 +26,7 @@ export class FilesController { constructor(private readonly filesService: FilesService) {} @Get(":fileId") + @MixedAudience([]) // Authenticated: no @Public, so the global JwtGuard applies. Unguessable file // UUIDs are obscurity, not authorization — raw byte streams must require auth. // Browser inline previews (/