From 17e41aa7677b1c0427cab27582ea3ef44e5eb1e4 Mon Sep 17 00:00:00 2001 From: ghost2023 Date: Fri, 21 Aug 2026 15:43:26 +0300 Subject: [PATCH] feat(overview): serve GET /overview/layouts filtered by permission MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New catalog endpoint, same shape as GET /reports: returns the overview layouts (key + label) the caller holds the matching edr_freight_app:overview::view permission for, in priority order. Backend enforcement to go with the permission-based frontend resolver (next commit) — a caller can no longer land on a layout their JWT doesn't actually carry the permission for. --- .../overview/dto/overview-layout.dto.ts | 19 +++++++++++++++ .../modules/overview/overview.controller.ts | 24 ++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 apps/edr-freight-api/src/modules/overview/dto/overview-layout.dto.ts diff --git a/apps/edr-freight-api/src/modules/overview/dto/overview-layout.dto.ts b/apps/edr-freight-api/src/modules/overview/dto/overview-layout.dto.ts new file mode 100644 index 000000000..8a125bf81 --- /dev/null +++ b/apps/edr-freight-api/src/modules/overview/dto/overview-layout.dto.ts @@ -0,0 +1,19 @@ +import { ApiProperty } from '@nestjs/swagger'; + +import type { OverviewLayoutKey } from '../../../seed/freight-permissions.registry'; + +/** + * One entry per `GET /overview/layouts` item: a layout the caller holds the + * `edr_freight_app:overview::view` permission for. Mirrors the reports + * module's catalog entry (`ReportCatalogEntry`) — same "server filters by + * permission, frontend just renders what comes back" shape. + */ +export class OverviewLayoutDto { + @ApiProperty({ + enum: ['clearance', 'occ', 'operation', 'marketer', 'finance', 'executive'], + }) + key!: OverviewLayoutKey; + + @ApiProperty() + label!: string; +} diff --git a/apps/edr-freight-api/src/modules/overview/overview.controller.ts b/apps/edr-freight-api/src/modules/overview/overview.controller.ts index fcec82d4d..37d0b77d2 100644 --- a/apps/edr-freight-api/src/modules/overview/overview.controller.ts +++ b/apps/edr-freight-api/src/modules/overview/overview.controller.ts @@ -9,7 +9,13 @@ import { CurrentUser } from '@edr/api-common'; import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type'; import { BookingStaff } from '../../common/booking-guards'; -import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry'; +import { hasFreightPermission } from '../../common/freight-permission.util'; +import { + FREIGHT_PERMS, + OVERVIEW_LAYOUT_KEYS, + OVERVIEW_LAYOUT_LABELS, +} from '../../seed/freight-permissions.registry'; +import { OverviewLayoutDto } from './dto/overview-layout.dto'; import { OverviewQueryDto } from './dto/overview-query.dto'; import { OverviewResponseDto } from './dto/overview-response.dto'; import { @@ -34,6 +40,22 @@ export class OverviewController { private readonly userTradeAccessService: UserTradeAccessService, ) {} + /** + * Layouts the caller has permission to render, in priority order — exactly + * the same "server filters by permission, frontend just renders what comes + * back" shape as GET /reports. A caller lands on exactly one layout, so the + * frontend picks the first entry here rather than rendering the whole list. + */ + @Get('layouts') + @BookingStaff(FREIGHT_PERMS.overview.view) + @ApiOperation({ summary: 'Overview dashboard layouts the caller has permission to render' }) + @ApiOkResponse({ type: OverviewLayoutDto, isArray: true }) + getLayouts(@CurrentUser() user: TCurrentUser): OverviewLayoutDto[] { + return OVERVIEW_LAYOUT_KEYS.filter((key) => + hasFreightPermission(user, FREIGHT_PERMS.overview.layout(key)), + ).map((key) => ({ key, label: OVERVIEW_LAYOUT_LABELS[key] })); + } + @Get() @BookingStaff(FREIGHT_PERMS.overview.view) @ApiOperation({ summary: 'Aggregated dashboard summary for backoffice overview' })