From bf8eadbc85b05c78c99c0ae14fdc7f3058d960e4 Mon Sep 17 00:00:00 2001 From: Hagernesh Date: Mon, 10 Aug 2026 12:00:08 +0000 Subject: [PATCH 1/4] fix: portal cannot load last-mile confirmation request MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LAST_MILE_REQUESTS URL constants were missing the /api prefix every other endpoint in URLS.ts carries — all five calls (get, submit, contract view/document/sign) 404'd against the deployed API, so the departure notification's confirm link never loaded for the customer. Also widen GET /last-mile-requests/:id from @BookingStaff to @MixedAudience with the same ownership check submit()/sign() already use — the confirm page calls this as its first request, before the customer has done anything else, so it can't be staff-only. Co-Authored-By: Claude Opus 5 (1M context) --- .../last-mile-requests.controller.ts | 10 +++++++--- .../last-mile-requests.service.ts | 16 +++++++++++++++- .../edr-freight-web/portal/src/constants/URLS.ts | 10 +++++----- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/apps/edr-freight-api/src/modules/last-mile-requests/last-mile-requests.controller.ts b/apps/edr-freight-api/src/modules/last-mile-requests/last-mile-requests.controller.ts index c6910c7a7..3d9fa4254 100644 --- a/apps/edr-freight-api/src/modules/last-mile-requests/last-mile-requests.controller.ts +++ b/apps/edr-freight-api/src/modules/last-mile-requests/last-mile-requests.controller.ts @@ -91,11 +91,15 @@ export class LastMileRequestsController { return this.contractService.sign(id, dto, user?.id ?? null); } + // Customer-facing like :id/contract/view — the portal's confirm page opens + // this straight from the departure notification link before the customer + // has done anything else, so it can't be staff-only. Service ownership- + // checks against the resolved company; staff may also open it. @Get(':id') - @BookingStaff(FREIGHT_PERMS.lastMile.requestView) + @MixedAudience(FREIGHT_PERMS.lastMile.requestView) @ApiOperation({ summary: 'Get a last-mile confirmation request by ID' }) - findOne(@Param('id', ParseUUIDPipe) id: string) { - return this.requestsService.findById(id); + findOne(@Param('id', ParseUUIDPipe) id: string, @CurrentUser() user: TCurrentUser) { + return this.requestsService.findById(id, user?.id ?? null); } // No @BookingStaff — the customer (portal) fills this, not backoffice staff. diff --git a/apps/edr-freight-api/src/modules/last-mile-requests/last-mile-requests.service.ts b/apps/edr-freight-api/src/modules/last-mile-requests/last-mile-requests.service.ts index b89f06479..ebdc93746 100644 --- a/apps/edr-freight-api/src/modules/last-mile-requests/last-mile-requests.service.ts +++ b/apps/edr-freight-api/src/modules/last-mile-requests/last-mile-requests.service.ts @@ -199,11 +199,25 @@ export class LastMileRequestsService { }; } - async findById(id: string): Promise { + /** + * `userId` is set only when a portal customer calls this directly (the + * confirm-page deep link from the departure notification, before they've + * submitted or signed anything) — staff and every internal caller pass + * nothing and skip the check, same convention as submit()/sign(). + */ + async findById(id: string, userId?: string | null): Promise { const record = await this.requestsRepository.findById(id, { relations: { booking: { company: true } }, }); if (!record) throw new NotFoundException(`Last-mile request ${id} not found`); + + if (userId) { + const companyId = await this.bookingsService.resolveCustomerCompanyId(userId); + if (companyId && record.booking?.companyId && companyId !== record.booking.companyId) { + throw new BadRequestException('This request does not belong to your company'); + } + } + return record; } diff --git a/apps/edr-freight-web/portal/src/constants/URLS.ts b/apps/edr-freight-web/portal/src/constants/URLS.ts index 153ed5e8b..a0d33f5ba 100644 --- a/apps/edr-freight-web/portal/src/constants/URLS.ts +++ b/apps/edr-freight-web/portal/src/constants/URLS.ts @@ -224,10 +224,10 @@ export const URL_CONSTANTS = { }, LAST_MILE_REQUESTS: { - BY_ID: (id: string) => `/last-mile-requests/${id}`, - SUBMIT: (id: string) => `/last-mile-requests/${id}/submit`, - CONTRACT_VIEW: (id: string) => `/last-mile-requests/${id}/contract/view`, - CONTRACT_DOCUMENT: (id: string) => `/last-mile-requests/${id}/contract/document`, - CONTRACT_SIGN: (id: string) => `/last-mile-requests/${id}/contract/sign`, + BY_ID: (id: string) => `/api/last-mile-requests/${id}`, + SUBMIT: (id: string) => `/api/last-mile-requests/${id}/submit`, + CONTRACT_VIEW: (id: string) => `/api/last-mile-requests/${id}/contract/view`, + CONTRACT_DOCUMENT: (id: string) => `/api/last-mile-requests/${id}/contract/document`, + CONTRACT_SIGN: (id: string) => `/api/last-mile-requests/${id}/contract/sign`, }, }; From 45715d68381fac5a6f841f40c110dba31a815461 Mon Sep 17 00:00:00 2001 From: Hagernesh Date: Mon, 10 Aug 2026 12:02:19 +0000 Subject: [PATCH 2/4] fix: default DataTable status to success so rows render status was required with no default; omitting it left the body blank while the footer (which renders regardless of status) still showed "Showing 1-N of N". Rows to draw is the common case, so default to success instead of requiring every caller to pass it explicitly. Co-Authored-By: Claude Opus 5 (1M context) --- packages/ui-common/src/components/data-table/table.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/ui-common/src/components/data-table/table.tsx b/packages/ui-common/src/components/data-table/table.tsx index 957da3300..3dc2d5786 100644 --- a/packages/ui-common/src/components/data-table/table.tsx +++ b/packages/ui-common/src/components/data-table/table.tsx @@ -13,7 +13,10 @@ import { DataTableFooter } from "./footer"; export function DataTable({ columns, data, - status, + // The body only renders under "success", but the footer renders regardless — + // omitting status gave a blank table under a populated "Showing 1–N of N" + // footer. Having rows to draw is the default case, so default to success. + status = "success", onRowClick, rowStyle, rowClassName, From 1b4a317d927aa6c01527560f9e2bb0f83f428b7c Mon Sep 17 00:00:00 2001 From: Hagernesh Date: Mon, 10 Aug 2026 12:02:31 +0000 Subject: [PATCH 3/4] docs: trim eims.types.ts file header comment Co-Authored-By: Claude Opus 5 (1M context) --- apps/edr-freight-api/src/modules/eims/eims.types.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/edr-freight-api/src/modules/eims/eims.types.ts b/apps/edr-freight-api/src/modules/eims/eims.types.ts index 6e74604bd..8cea86619 100644 --- a/apps/edr-freight-api/src/modules/eims/eims.types.ts +++ b/apps/edr-freight-api/src/modules/eims/eims.types.ts @@ -1,6 +1,5 @@ /** - * Wire types for the MoR EIMS gateway, taken from the supplied Postman collection. - * + * Wire types for the MoR EIMS gateway, * Every protected payload is the same envelope: the business object under `request`, a base64 * RSA-SHA512 signature over the *inner* object only, and the base64 certificate bundle. */ From b701ae3f470b669f149702e1175cf0e82378df85 Mon Sep 17 00:00:00 2001 From: Hagernesh Date: Mon, 10 Aug 2026 12:05:04 +0000 Subject: [PATCH 4/4] Revert "docs: trim eims.types.ts file header comment" This reverts commit f4b8dab58d921791b4b3f69e3c9952b6802835cc. --- apps/edr-freight-api/src/modules/eims/eims.types.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/edr-freight-api/src/modules/eims/eims.types.ts b/apps/edr-freight-api/src/modules/eims/eims.types.ts index 8cea86619..6e74604bd 100644 --- a/apps/edr-freight-api/src/modules/eims/eims.types.ts +++ b/apps/edr-freight-api/src/modules/eims/eims.types.ts @@ -1,5 +1,6 @@ /** - * Wire types for the MoR EIMS gateway, + * Wire types for the MoR EIMS gateway, taken from the supplied Postman collection. + * * Every protected payload is the same envelope: the business object under `request`, a base64 * RSA-SHA512 signature over the *inner* object only, and the base64 certificate bundle. */