From fe35e26313dbaa65372b59669173a2f86e331fbb Mon Sep 17 00:00:00 2001 From: Maurice Date: Sat, 10 Oct 2026 13:11:10 +0200 Subject: [PATCH] refactor(trip-membership): domains ask TripAccessService who may see a trip Thirty services, controllers, MCP tools and RPC handlers outside the trips domain injected TripsRepository for one call, findAccessible. They now inject TripAccessService from the trip-membership leaf, in the same constructor slot under the same field name; it answers with the same access row (or undefined) and binds the id exactly as given, so each caller keeps its own coercion and its own 404, 403 or tool error. --- .../accommodations-domain.module.ts | 3 +- .../accommodations/accommodations.service.ts | 8 ++--- .../assignments/assignments-domain.module.ts | 3 +- .../nest/assignments/assignments.service.ts | 7 ++-- server/src/nest/budget/costs.rpc.ts | 8 ++--- server/src/nest/day-notes/day-notes.module.ts | 3 +- .../src/nest/day-notes/day-notes.service.ts | 7 ++-- server/src/nest/files/files.module.ts | 3 +- server/src/nest/files/files.service.ts | 7 ++-- server/src/nest/places/places.mcp.ts | 6 ++-- server/src/nest/places/places.module.ts | 3 +- .../contributions/day-schedule.controller.ts | 5 ++- .../contributions/day-tints.controller.ts | 5 ++- .../contributions/map-layers.controller.ts | 6 ++-- .../contributions/map-markers.controller.ts | 6 ++-- .../contributions/pdf-sections.controller.ts | 6 ++-- .../contributions/place-details.controller.ts | 5 ++- .../plugin-contributions.module.ts | 3 +- .../contributions/plugin-routes.controller.ts | 5 ++- .../trip-card-contributions.controller.ts | 6 ++-- .../contributions/trip-warnings.controller.ts | 6 ++-- .../contributions/trip-warnings.mcp.ts | 6 ++-- .../view-contributions.controller.ts | 6 ++-- .../nest/realtime/realtime-gateway.module.ts | 3 +- server/src/nest/realtime/realtime.gateway.ts | 7 ++-- .../receipt-scan/receipt-scan.controller.ts | 6 ++-- .../nest/receipt-scan/receipt-scan.module.ts | 3 +- .../reservation-import.mcp.ts | 8 ++--- .../reservation-import.module.ts | 3 +- server/src/nest/roadtrip/charging.mcp.ts | 6 ++-- .../src/nest/roadtrip/day-boundaries.mcp.ts | 6 ++-- .../src/nest/roadtrip/google-route.service.ts | 5 ++- .../src/nest/roadtrip/roadtrip-hazards.mcp.ts | 6 ++-- .../nest/roadtrip/roadtrip-plan.service.ts | 5 ++- .../nest/roadtrip/roadtrip-preferences.mcp.ts | 6 ++-- server/src/nest/roadtrip/roadtrip.mcp.ts | 6 ++-- server/src/nest/roadtrip/roadtrip.module.ts | 3 +- server/src/nest/todo/todo.module.ts | 3 +- server/src/nest/todo/todo.service.ts | 8 ++--- server/src/nest/transit/transit.mcp.ts | 8 ++--- server/src/nest/transit/transit.module.ts | 3 +- .../nest/trip-invite/trip-invite.service.ts | 7 ++-- .../trip-membership/trip-access.service.ts | 27 +++++++++++++++ .../trip-membership/trip-membership.module.ts | 10 ++++-- .../tests/helpers/accommodations-service.ts | 5 +-- server/tests/helpers/mcp-test-controllers.ts | 25 +++++++------- server/tests/helpers/plugin-host.ts | 13 ++++---- server/tests/unit/budget/costs.rpc.test.ts | 3 +- .../unit/nest/assignments.service.test.ts | 3 +- .../tests/unit/nest/day-notes.service.test.ts | 5 +-- .../unit/nest/day-removal.service.test.ts | 5 +-- server/tests/unit/nest/files.service.test.ts | 3 +- .../tests/unit/nest/realtime.gateway.test.ts | 33 ++++++++++--------- .../unit/nest/receipt-scan.controller.test.ts | 3 +- .../unit/nest/roadtrip-plan.service.test.ts | 3 +- server/tests/unit/nest/todo.service.test.ts | 5 +-- .../unit/nest/trip-invite.service.test.ts | 3 +- .../unit/nest/trip-read-model.service.test.ts | 5 +-- server/tests/unit/nest/trips.service.test.ts | 5 +-- .../unit/plugins/addon-gates.rpc.test.ts | 3 +- .../plugins/day-schedule.controller.test.ts | 3 +- .../unit/plugins/day-tints.controller.test.ts | 3 +- .../plugins/map-layers.controller.test.ts | 3 +- .../plugins/map-markers.controller.test.ts | 3 +- .../plugins/pdf-sections.controller.test.ts | 3 +- .../plugins/place-details.controller.test.ts | 3 +- .../plugins/plugin-routes.controller.test.ts | 3 +- ...trip-card-contributions.controller.test.ts | 3 +- .../plugins/trip-warnings.controller.test.ts | 3 +- .../view-contributions.controller.test.ts | 3 +- 70 files changed, 219 insertions(+), 195 deletions(-) create mode 100644 server/src/nest/trip-membership/trip-access.service.ts diff --git a/server/src/nest/accommodations/accommodations-domain.module.ts b/server/src/nest/accommodations/accommodations-domain.module.ts index 66b5a59bd..3e8ccdbc6 100644 --- a/server/src/nest/accommodations/accommodations-domain.module.ts +++ b/server/src/nest/accommodations/accommodations-domain.module.ts @@ -12,6 +12,7 @@ import { Days } from '../../db/entities/Days.entity'; import { RoadtripVias } from '../../db/entities/RoadtripVias.entity'; import { Reservations } from '../../db/entities/Reservations.entity'; import { Trips } from '../../db/entities/Trips.entity'; +import { TripMembershipModule } from '../trip-membership/trip-membership.module'; /** * The accommodations SERVICE, split from the controller/MCP/RPC surfaces (the @@ -38,7 +39,7 @@ import { Trips } from '../../db/entities/Trips.entity'; * no entity added here for that. */ @Module({ - imports: [ + imports: [TripMembershipModule, PermissionsModule, RealtimeModule, AssignmentsDomainModule, MikroOrmModule.forFeature([DayAccommodations, DayAssignments, Places, Days, RoadtripVias, Reservations, BudgetItems, Trips]), ], diff --git a/server/src/nest/accommodations/accommodations.service.ts b/server/src/nest/accommodations/accommodations.service.ts index f3310d553..e6175a00d 100644 --- a/server/src/nest/accommodations/accommodations.service.ts +++ b/server/src/nest/accommodations/accommodations.service.ts @@ -14,8 +14,7 @@ import { Places } from '../../db/entities/Places.entity'; import type { PlacesRepository, PlaceWithTagsRow as PlaceWithTags } from '../../db/repositories/Places.repository'; import { Days } from '../../db/entities/Days.entity'; import type { DaysRepository } from '../../db/repositories/Days.repository'; -import { Trips } from '../../db/entities/Trips.entity'; -import type { TripsRepository, TripAccess } from '../../db/repositories/Trips.repository'; +import type { TripAccess } from '../../db/repositories/Trips.repository'; import { RoadtripVias } from '../../db/entities/RoadtripVias.entity'; import type { RoadtripViasRepository } from '../../db/repositories/RoadtripVias.repository'; import { Reservations } from '../../db/entities/Reservations.entity'; @@ -26,6 +25,7 @@ import { carryViasWith, locatedStopIdsWith, reseatOwnStopWith, seatHolds, seatIn import type { User } from '../../types'; import { RESERVATION_METADATA } from '../../db/json-columns'; import { decodeJson, encodeJson } from '../../utils/json-column'; +import { TripAccessService } from '../trip-membership/trip-access.service'; type Trip = TripAccess; @@ -115,8 +115,8 @@ export class AccommodationsService { private readonly assignments: AssignmentsService, private readonly uow: UnitOfWork, // Plan 4 Task 2 — canAccessTrip's own DatabaseService delegation is gone: - // this injects TripsRepository directly and calls findAccessible. - @InjectRepository(Trips) private readonly trips: TripsRepository, + // this injects TripAccessService (trip-membership) and calls findAccessible. + private readonly trips: TripAccessService, @InjectRepository(DayAccommodations) private readonly dayAccommodationsRepo: DayAccommodationsRepository, @InjectRepository(DayAssignments) private readonly dayAssignmentsRepo: DayAssignmentsRepository, @InjectRepository(Places) private readonly placesRepo: PlacesRepository, diff --git a/server/src/nest/assignments/assignments-domain.module.ts b/server/src/nest/assignments/assignments-domain.module.ts index 1ebeb34f2..6c04f9aa3 100644 --- a/server/src/nest/assignments/assignments-domain.module.ts +++ b/server/src/nest/assignments/assignments-domain.module.ts @@ -13,6 +13,7 @@ import { TripMembers } from '../../db/entities/TripMembers.entity'; import { RoadtripVias } from '../../db/entities/RoadtripVias.entity'; import { Trips } from '../../db/entities/Trips.entity'; import { Tours } from '../../db/entities/Tours.entity'; +import { TripMembershipModule } from '../trip-membership/trip-membership.module'; /** * The assignments SERVICE, split from the controller/MCP/RPC surfaces (the @@ -35,7 +36,7 @@ import { Tours } from '../../db/entities/Tours.entity'; * one-Tour-per-day check create and move share. */ @Module({ - imports: [ + imports: [TripMembershipModule, PermissionsModule, QueryHelpersModule, JourneyDomainModule, diff --git a/server/src/nest/assignments/assignments.service.ts b/server/src/nest/assignments/assignments.service.ts index e923644a7..82d077dc3 100644 --- a/server/src/nest/assignments/assignments.service.ts +++ b/server/src/nest/assignments/assignments.service.ts @@ -17,8 +17,6 @@ import { AssignmentParticipants } from '../../db/entities/AssignmentParticipants import type { AssignmentParticipantsRepository } from '../../db/repositories/AssignmentParticipants.repository'; import { Days } from '../../db/entities/Days.entity'; import type { DaysRepository } from '../../db/repositories/Days.repository'; -import { Trips } from '../../db/entities/Trips.entity'; -import type { TripsRepository } from '../../db/repositories/Trips.repository'; import { Places } from '../../db/entities/Places.entity'; import type { PlacesRepository } from '../../db/repositories/Places.repository'; import { TripMembers } from '../../db/entities/TripMembers.entity'; @@ -27,6 +25,7 @@ import { RoadtripVias } from '../../db/entities/RoadtripVias.entity'; import type { RoadtripViasRepository } from '../../db/repositories/RoadtripVias.repository'; import { Tours } from '../../db/entities/Tours.entity'; import type { ToursRepository } from '../../db/repositories/Tours.repository'; +import { TripAccessService } from '../trip-membership/trip-access.service'; type Trip = TripAccess; @@ -100,8 +99,8 @@ function sortMinutes(time: string | null): number | null { export class AssignmentsService { constructor( // Plan 4 Task 2 — canAccessTrip's own DatabaseService delegation is gone: - // this injects TripsRepository directly and calls findAccessible. - @InjectRepository(Trips) private readonly trips: TripsRepository, + // this injects TripAccessService (trip-membership) and calls findAccessible. + private readonly trips: TripAccessService, private readonly permissions: PermissionsService, private readonly realtime: RealtimeService, private readonly queryHelpers: QueryHelpersService, diff --git a/server/src/nest/budget/costs.rpc.ts b/server/src/nest/budget/costs.rpc.ts index f00d307ea..f91e3183b 100644 --- a/server/src/nest/budget/costs.rpc.ts +++ b/server/src/nest/budget/costs.rpc.ts @@ -4,13 +4,11 @@ import { PluginGuards } from '../../nest-rpc/plugin-guards.service'; import { BadParams, ForbiddenResource } from '../../nest-rpc/rpc-errors'; import { num, schemaMessage } from '../../nest-rpc/rpc-params'; import type { PluginRpcContext } from '../../nest-rpc/rpc-kit/types'; -import { InjectRepository } from '@mikro-orm/nestjs'; import { RealtimeService } from '../realtime/realtime.service'; -import { Trips } from '../../db/entities/Trips.entity'; -import type { TripsRepository } from '../../db/repositories/Trips.repository'; import { TripMembershipService } from '../trip-membership/trip-membership.service'; import { ADDON_IDS } from '../../addons'; import { BudgetService } from './budget.service'; +import { TripAccessService } from '../trip-membership/trip-access.service'; /** Costs are budget items, and the app edits them under 'budget_edit'. */ const BUDGET_EDIT_ACTION = 'budget_edit'; @@ -33,8 +31,8 @@ export class CostsRpc { constructor( private readonly budget: BudgetService, // Plan 4 Task 2 — canAccessTrip's own DatabaseService delegation is gone: - // this injects TripsRepository directly and calls findAccessible. - @InjectRepository(Trips) private readonly trips: TripsRepository, + // this injects TripAccessService (trip-membership) and calls findAccessible. + private readonly trips: TripAccessService, private readonly realtime: RealtimeService, private readonly guards: PluginGuards, private readonly membership: TripMembershipService, diff --git a/server/src/nest/day-notes/day-notes.module.ts b/server/src/nest/day-notes/day-notes.module.ts index 57bc46d0e..2ea5547f5 100644 --- a/server/src/nest/day-notes/day-notes.module.ts +++ b/server/src/nest/day-notes/day-notes.module.ts @@ -11,6 +11,7 @@ import { McpSharedModule } from '../mcp-shared/mcp-shared.module'; import { DayNotes } from '../../db/entities/DayNotes.entity'; import { Days } from '../../db/entities/Days.entity'; import { Trips } from '../../db/entities/Trips.entity'; +import { TripMembershipModule } from '../trip-membership/trip-membership.module'; /** * Day notes. Its own domain rather than a second file set inside days/, which @@ -28,7 +29,7 @@ import { Trips } from '../../db/entities/Trips.entity'; @Module({ // DayNotes/Days: Plan 4 Task 1 — DayNotesService's own DayNotesRepository/ // DaysRepository.existsInTrip, replacing its raw `this.dbs.all/get/run`. - imports: [McpSharedModule, PermissionsModule, RealtimeModule, PluginGuardsModule, MikroOrmModule.forFeature([DayNotes, Days, Trips])], + imports: [TripMembershipModule, McpSharedModule, PermissionsModule, RealtimeModule, PluginGuardsModule, MikroOrmModule.forFeature([DayNotes, Days, Trips])], controllers: [DayNotesController], providers: [DayNotesService, DayNotesMcp, DayNotesRpc], exports: [DayNotesService], diff --git a/server/src/nest/day-notes/day-notes.service.ts b/server/src/nest/day-notes/day-notes.service.ts index 407aac4b7..dd6e2b6d3 100644 --- a/server/src/nest/day-notes/day-notes.service.ts +++ b/server/src/nest/day-notes/day-notes.service.ts @@ -9,8 +9,7 @@ import { DayNotes } from '../../db/entities/DayNotes.entity'; import type { DayNotesRepository } from '../../db/repositories/DayNotes.repository'; import { Days } from '../../db/entities/Days.entity'; import type { DaysRepository } from '../../db/repositories/Days.repository'; -import { Trips } from '../../db/entities/Trips.entity'; -import type { TripsRepository } from '../../db/repositories/Trips.repository'; +import { TripAccessService } from '../trip-membership/trip-access.service'; /** * Day-notes domain service — the legacy dayNoteService SQL folded in over @@ -42,9 +41,9 @@ export function normalizeNoteColor(color: string | null | undefined): string | n export class DayNotesService { constructor( // Plan 4 Task 2 — canAccessTrip's own DatabaseService delegation is gone: - // this injects TripsRepository directly (same constructor slot) and + // this injects TripAccessService (trip-membership) (same constructor slot) and // calls findAccessible. - @InjectRepository(Trips) private readonly trips: TripsRepository, + private readonly trips: TripAccessService, private readonly permissions: PermissionsService, private readonly realtime: RealtimeService, @InjectRepository(DayNotes) private readonly dayNotes: DayNotesRepository, diff --git a/server/src/nest/files/files.module.ts b/server/src/nest/files/files.module.ts index c7de94759..9b7ee55c7 100644 --- a/server/src/nest/files/files.module.ts +++ b/server/src/nest/files/files.module.ts @@ -27,9 +27,10 @@ import { DayAssignments } from '../../db/entities/DayAssignments.entity'; import { BudgetItems } from '../../db/entities/BudgetItems.entity'; import { Users } from '../../db/entities/Users.entity'; import { Trips } from '../../db/entities/Trips.entity'; +import { TripMembershipModule } from '../trip-membership/trip-membership.module'; @Module({ - imports: [ + imports: [TripMembershipModule, MulterModule.registerAsync({ imports: [StorageModule, AllowedFileTypesModule], inject: [StorageService, AllowedFileTypesService], diff --git a/server/src/nest/files/files.service.ts b/server/src/nest/files/files.service.ts index 65c6abe18..3303da289 100644 --- a/server/src/nest/files/files.service.ts +++ b/server/src/nest/files/files.service.ts @@ -15,8 +15,6 @@ import { Users } from '../../db/entities/Users.entity'; import { UserSessions } from '../../db/entities/UserSessions.entity'; import type { User } from '../../types'; import type { TripAccess } from '../../db/repositories/Trips.repository'; -import { Trips } from '../../db/entities/Trips.entity'; -import type { TripsRepository } from '../../db/repositories/Trips.repository'; import { UnitOfWork } from '../database/unit-of-work'; import { toRowId } from '../common/row-id'; import { StorageService } from '../storage/storage.service'; @@ -33,6 +31,7 @@ import { DayAssignments } from '../../db/entities/DayAssignments.entity'; import type { DayAssignmentsRepository } from '../../db/repositories/DayAssignments.repository'; import { BudgetItems } from '../../db/entities/BudgetItems.entity'; import type { BudgetItemsRepository } from '../../db/repositories/BudgetItems.repository'; +import { TripAccessService } from '../trip-membership/trip-access.service'; type Trip = TripAccess; type FilePermission = 'file_upload' | 'file_edit' | 'file_delete'; @@ -119,9 +118,9 @@ function coerceLinkId(value: string | number | null | undefined): number | null export class FilesService { constructor( // Plan 4 Task 2 — canAccessTrip's own DatabaseService delegation is gone: - // this injects TripsRepository directly (same constructor slot) and + // this injects TripAccessService (trip-membership) (same constructor slot) and // calls findAccessible. - @InjectRepository(Trips) private readonly trips: TripsRepository, + private readonly trips: TripAccessService, private readonly permissions: PermissionsService, private readonly realtime: RealtimeService, private readonly tokens: EphemeralTokenService, diff --git a/server/src/nest/places/places.mcp.ts b/server/src/nest/places/places.mcp.ts index 240883d10..c75dc2e57 100644 --- a/server/src/nest/places/places.mcp.ts +++ b/server/src/nest/places/places.mcp.ts @@ -4,7 +4,6 @@ import { TOOL_ANNOTATIONS_DELETE, TOOL_ANNOTATIONS_NON_IDEMPOTENT, errorResult, ok, } from '../../nest-mcp'; -import { InjectRepository } from '@mikro-orm/nestjs'; import { McpToolGuardsService } from '../mcp-shared/mcp-tool-guards.service'; import { mapsSearchRequestSchema, @@ -28,8 +27,7 @@ import { MapsService } from '../maps/maps.service'; import { PlacesService } from './places.service'; import { isUpdateConflict } from '../common/conflictResult'; import { isDirectionsUrl } from './maps-dir.helpers'; -import { Trips } from '../../db/entities/Trips.entity'; -import type { TripsRepository } from '../../db/repositories/Trips.repository'; +import { TripAccessService } from '../trip-membership/trip-access.service'; function parseId(value: string | string[]): number | null { const n = Number(Array.isArray(value) ? value[0] : value); @@ -61,7 +59,7 @@ export class PlacesMcp { constructor( private readonly places: PlacesService, private readonly maps: MapsService, - @InjectRepository(Trips) private readonly tripsRepo: TripsRepository, + private readonly tripsRepo: TripAccessService, private readonly journey: JourneyDomainService, private readonly assignments: AssignmentsService, private readonly guards: McpToolGuardsService, diff --git a/server/src/nest/places/places.module.ts b/server/src/nest/places/places.module.ts index 4d498f6d3..3d237b557 100644 --- a/server/src/nest/places/places.module.ts +++ b/server/src/nest/places/places.module.ts @@ -30,6 +30,7 @@ import { StorageModule } from '../storage/storage.module'; import { StorageService } from '../storage/storage.service'; import { buildStorageUploadOptions } from '../storage/storage-upload.factory'; import { MAX_PLACE_IMAGE_SIZE } from '../common/place-image-upload'; +import { TripMembershipModule } from '../trip-membership/trip-membership.module'; /** * Places domain (S8 — Phase 2 trip sub-domain). Depends on L4 Categories + L5 @@ -40,7 +41,7 @@ import { MAX_PLACE_IMAGE_SIZE } from '../common/place-image-upload'; * is no places.bridge.ts: nothing outside the container consumes this domain. */ @Module({ - imports: [ + imports: [TripMembershipModule, // Module-level options deliberately carry NO fileFilter: the GPX/KML import // route keeps its inline memoryStorage config and would inherit a // module-level filter via the interceptor's shallow merge — the image diff --git a/server/src/nest/plugins/contributions/day-schedule.controller.ts b/server/src/nest/plugins/contributions/day-schedule.controller.ts index f489247f5..e5f185815 100644 --- a/server/src/nest/plugins/contributions/day-schedule.controller.ts +++ b/server/src/nest/plugins/contributions/day-schedule.controller.ts @@ -1,14 +1,13 @@ import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common'; import type { Request } from 'express'; import { InjectRepository } from '@mikro-orm/nestjs'; -import { Trips } from '../../../db/entities/Trips.entity'; -import type { TripsRepository } from '../../../db/repositories/Trips.repository'; import { Days } from '../../../db/entities/Days.entity'; import type { DaysRepository } from '../../../db/repositories/Days.repository'; import { JwtAuthGuard } from '../../auth-core/jwt-auth.guard'; import { pluginsEnabled } from '../kill-switch'; import { PluginHooks } from '../plugin-hooks.service'; import { stripEmoji } from '../text-sanitize'; +import { TripAccessService } from '../../trip-membership/trip-access.service'; /** * GET /api/day-schedule/:tripId — bounded time contributions plugins attach to the @@ -84,7 +83,7 @@ function normalize(pluginId: string, tripDayIds: ReadonlySet, raw: unkno export class DayScheduleController { constructor( private readonly hooks: PluginHooks, - @InjectRepository(Trips) private readonly trips: TripsRepository, + private readonly trips: TripAccessService, // CT1 (Plan 3j Task 5) — the trip's day-id set, converted onto Days.repository.ts. @InjectRepository(Days) private readonly days: DaysRepository, ) {} diff --git a/server/src/nest/plugins/contributions/day-tints.controller.ts b/server/src/nest/plugins/contributions/day-tints.controller.ts index 7ed2ecbd1..336452ed5 100644 --- a/server/src/nest/plugins/contributions/day-tints.controller.ts +++ b/server/src/nest/plugins/contributions/day-tints.controller.ts @@ -1,14 +1,13 @@ import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common'; import type { Request } from 'express'; import { InjectRepository } from '@mikro-orm/nestjs'; -import { Trips } from '../../../db/entities/Trips.entity'; -import type { TripsRepository } from '../../../db/repositories/Trips.repository'; import { Days } from '../../../db/entities/Days.entity'; import type { DaysRepository } from '../../../db/repositories/Days.repository'; import { JwtAuthGuard } from '../../auth-core/jwt-auth.guard'; import { pluginsEnabled } from '../kill-switch'; import { PluginHooks } from '../plugin-hooks.service'; import { stripEmoji } from '../text-sanitize'; +import { TripAccessService } from '../../trip-membership/trip-access.service'; /** * GET /api/day-tints/:tripId — the colours the planner paints into a day card in the @@ -161,7 +160,7 @@ function normalize(pluginId: string, tripDayIds: ReadonlySet, raw: unkno export class DayTintsController { constructor( private readonly hooks: PluginHooks, - @InjectRepository(Trips) private readonly trips: TripsRepository, + private readonly trips: TripAccessService, // CT2 (Plan 3j Task 5) — the trip's day-id set, converted onto Days.repository.ts. @InjectRepository(Days) private readonly days: DaysRepository, ) {} diff --git a/server/src/nest/plugins/contributions/map-layers.controller.ts b/server/src/nest/plugins/contributions/map-layers.controller.ts index 93ac85b74..958a7314d 100644 --- a/server/src/nest/plugins/contributions/map-layers.controller.ts +++ b/server/src/nest/plugins/contributions/map-layers.controller.ts @@ -1,12 +1,10 @@ import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common'; import type { Request } from 'express'; -import { InjectRepository } from '@mikro-orm/nestjs'; -import { Trips } from '../../../db/entities/Trips.entity'; -import type { TripsRepository } from '../../../db/repositories/Trips.repository'; import { JwtAuthGuard } from '../../auth-core/jwt-auth.guard'; import { pluginsEnabled } from '../kill-switch'; import { PluginHooks } from '../plugin-hooks.service'; import { stripEmoji } from '../text-sanitize'; +import { TripAccessService } from '../../trip-membership/trip-access.service'; /** * GET /api/map-layers/:tripId — bounded vector overlays plugins draw on the trip map @@ -149,7 +147,7 @@ function normalize(pluginId: string, raw: unknown): MapLayer[] { export class MapLayersController { constructor( private readonly hooks: PluginHooks, - @InjectRepository(Trips) private readonly trips: TripsRepository, + private readonly trips: TripAccessService, ) {} @Get(':tripId') diff --git a/server/src/nest/plugins/contributions/map-markers.controller.ts b/server/src/nest/plugins/contributions/map-markers.controller.ts index 7e0297f9f..52a4963bf 100644 --- a/server/src/nest/plugins/contributions/map-markers.controller.ts +++ b/server/src/nest/plugins/contributions/map-markers.controller.ts @@ -1,12 +1,10 @@ import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common'; import type { Request } from 'express'; -import { InjectRepository } from '@mikro-orm/nestjs'; -import { Trips } from '../../../db/entities/Trips.entity'; -import type { TripsRepository } from '../../../db/repositories/Trips.repository'; import { JwtAuthGuard } from '../../auth-core/jwt-auth.guard'; import { pluginsEnabled } from '../kill-switch'; import { PluginHooks } from '../plugin-hooks.service'; import { stripEmoji } from '../text-sanitize'; +import { TripAccessService } from '../../trip-membership/trip-access.service'; /** * GET /api/map-markers/:tripId — bounded markers plugins overlay on the trip map @@ -82,7 +80,7 @@ function normalize(pluginId: string, raw: unknown): MapMarker[] { export class MapMarkersController { constructor( private readonly hooks: PluginHooks, - @InjectRepository(Trips) private readonly trips: TripsRepository, + private readonly trips: TripAccessService, ) {} @Get(':tripId') diff --git a/server/src/nest/plugins/contributions/pdf-sections.controller.ts b/server/src/nest/plugins/contributions/pdf-sections.controller.ts index 398a63a19..b149b20b8 100644 --- a/server/src/nest/plugins/contributions/pdf-sections.controller.ts +++ b/server/src/nest/plugins/contributions/pdf-sections.controller.ts @@ -1,12 +1,10 @@ import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common'; import type { Request } from 'express'; -import { InjectRepository } from '@mikro-orm/nestjs'; -import { Trips } from '../../../db/entities/Trips.entity'; -import type { TripsRepository } from '../../../db/repositories/Trips.repository'; import { JwtAuthGuard } from '../../auth-core/jwt-auth.guard'; import { pluginsEnabled } from '../kill-switch'; import { PluginHooks } from '../plugin-hooks.service'; import { stripEmoji } from '../text-sanitize'; +import { TripAccessService } from '../../trip-membership/trip-access.service'; /** * GET /api/pdf-sections/:tripId — text-only sections plugins append to a trip's @@ -78,7 +76,7 @@ function normalize(pluginId: string, raw: unknown): PdfSection[] { export class PdfSectionsController { constructor( private readonly hooks: PluginHooks, - @InjectRepository(Trips) private readonly trips: TripsRepository, + private readonly trips: TripAccessService, ) {} @Get(':tripId') diff --git a/server/src/nest/plugins/contributions/place-details.controller.ts b/server/src/nest/plugins/contributions/place-details.controller.ts index d41854fb2..8141ca1fc 100644 --- a/server/src/nest/plugins/contributions/place-details.controller.ts +++ b/server/src/nest/plugins/contributions/place-details.controller.ts @@ -1,14 +1,13 @@ import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common'; import type { Request } from 'express'; import { InjectRepository } from '@mikro-orm/nestjs'; -import { Trips } from '../../../db/entities/Trips.entity'; -import type { TripsRepository } from '../../../db/repositories/Trips.repository'; import { Places } from '../../../db/entities/Places.entity'; import type { PlacesRepository } from '../../../db/repositories/Places.repository'; import { JwtAuthGuard } from '../../auth-core/jwt-auth.guard'; import { pluginsEnabled } from '../kill-switch'; import { PluginHooks } from '../plugin-hooks.service'; import { stripEmoji } from '../text-sanitize'; +import { TripAccessService } from '../../trip-membership/trip-access.service'; /** * GET /api/place-details/:placeId — extra info for a place, contributed by plugins @@ -67,7 +66,7 @@ function normalize(raw: unknown): DetailItem[] { export class PlaceDetailsController { constructor( private readonly hooks: PluginHooks, - @InjectRepository(Trips) private readonly trips: TripsRepository, + private readonly trips: TripAccessService, // CT7 (Plan 3j Task 5) — the place's owning trip id, converted onto Places.repository.ts. @InjectRepository(Places) private readonly places: PlacesRepository, ) {} diff --git a/server/src/nest/plugins/contributions/plugin-contributions.module.ts b/server/src/nest/plugins/contributions/plugin-contributions.module.ts index 8fcf93de9..87820afce 100644 --- a/server/src/nest/plugins/contributions/plugin-contributions.module.ts +++ b/server/src/nest/plugins/contributions/plugin-contributions.module.ts @@ -30,6 +30,7 @@ import { PdfSectionsController } from './pdf-sections.controller'; import { AtlasLayersController } from './atlas-layers.controller'; import { JournalEntryRowsController } from './journal-entry-rows.controller'; import { DemoModule } from '../../common/demo.module'; +import { TripMembershipModule } from '../../trip-membership/trip-membership.module'; /** * The read-only surface plugins contribute to the app: photos, calendar events, @@ -63,7 +64,7 @@ import { DemoModule } from '../../common/demo.module'; * dependency unresolved. */ @Module({ - imports: [PluginsRuntimeModule, AddonsModule, JourneyDomainModule, DemoModule, MikroOrmModule.forFeature([JourneyEntries, Plugins, Days, Places, Trips])], + imports: [TripMembershipModule, PluginsRuntimeModule, AddonsModule, JourneyDomainModule, DemoModule, MikroOrmModule.forFeature([JourneyEntries, Plugins, Days, Places, Trips])], controllers: [ PlaceDetailsController, PluginSearchController, diff --git a/server/src/nest/plugins/contributions/plugin-routes.controller.ts b/server/src/nest/plugins/contributions/plugin-routes.controller.ts index 6cfc31656..52059b3f8 100644 --- a/server/src/nest/plugins/contributions/plugin-routes.controller.ts +++ b/server/src/nest/plugins/contributions/plugin-routes.controller.ts @@ -1,8 +1,6 @@ import { Body, Controller, Param, Post, Req, UseGuards } from '@nestjs/common'; import type { Request } from 'express'; import { InjectRepository } from '@mikro-orm/nestjs'; -import { Trips } from '../../../db/entities/Trips.entity'; -import type { TripsRepository } from '../../../db/repositories/Trips.repository'; import { Plugins } from '../../../db/entities/Plugins.entity'; import type { PluginsRepository } from '../../../db/repositories/Plugins.repository'; import { JwtAuthGuard } from '../../auth-core/jwt-auth.guard'; @@ -10,6 +8,7 @@ import { pluginsEnabled } from '../kill-switch'; import { PluginHooks } from '../plugin-hooks.service'; import { PluginRouteDto } from '../plugins.dto'; import { normalize, declaredProfiles, readWaypoints, PROFILE_RE, type PluginRouteOut } from './plugin-route-normalize'; +import { TripAccessService } from '../../trip-membership/trip-access.service'; export type { PluginRouteOut } from './plugin-route-normalize'; /** @@ -36,7 +35,7 @@ export type { PluginRouteOut } from './plugin-route-normalize'; export class PluginRoutesController { constructor( private readonly hooks: PluginHooks, - @InjectRepository(Trips) private readonly trips: TripsRepository, + private readonly trips: TripAccessService, @InjectRepository(Plugins) private readonly plugins: PluginsRepository, ) {} diff --git a/server/src/nest/plugins/contributions/trip-card-contributions.controller.ts b/server/src/nest/plugins/contributions/trip-card-contributions.controller.ts index 56ffc7ad5..926f655ac 100644 --- a/server/src/nest/plugins/contributions/trip-card-contributions.controller.ts +++ b/server/src/nest/plugins/contributions/trip-card-contributions.controller.ts @@ -1,12 +1,10 @@ import { Controller, Get, Query, Req, UseGuards } from '@nestjs/common'; import type { Request } from 'express'; -import { InjectRepository } from '@mikro-orm/nestjs'; -import { Trips } from '../../../db/entities/Trips.entity'; -import type { TripsRepository } from '../../../db/repositories/Trips.repository'; import { JwtAuthGuard } from '../../auth-core/jwt-auth.guard'; import { pluginsEnabled } from '../kill-switch'; import { PluginHooks } from '../plugin-hooks.service'; import { stripEmoji } from '../text-sanitize'; +import { TripAccessService } from '../../trip-membership/trip-access.service'; /** * GET /api/trip-card-contributions?tripIds=1,2,3 — host-rendered badges that plugins @@ -86,7 +84,7 @@ function normalize(pluginId: string, raw: unknown, allowed: Set): TripCa export class TripCardContributionsController { constructor( private readonly hooks: PluginHooks, - @InjectRepository(Trips) private readonly trips: TripsRepository, + private readonly trips: TripAccessService, ) {} @Get() diff --git a/server/src/nest/plugins/contributions/trip-warnings.controller.ts b/server/src/nest/plugins/contributions/trip-warnings.controller.ts index b870a9ae3..0acd70c83 100644 --- a/server/src/nest/plugins/contributions/trip-warnings.controller.ts +++ b/server/src/nest/plugins/contributions/trip-warnings.controller.ts @@ -1,12 +1,10 @@ import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common'; import type { Request } from 'express'; -import { InjectRepository } from '@mikro-orm/nestjs'; -import { Trips } from '../../../db/entities/Trips.entity'; -import type { TripsRepository } from '../../../db/repositories/Trips.repository'; import { JwtAuthGuard } from '../../auth-core/jwt-auth.guard'; import { pluginsEnabled } from '../kill-switch'; import { PluginHooks } from '../plugin-hooks.service'; import { stripEmoji } from '../text-sanitize'; +import { TripAccessService } from '../../trip-membership/trip-access.service'; /** * GET /api/trip-warnings/:tripId — validation/warning contributions from plugins @@ -31,7 +29,7 @@ const MESSAGE_MAX = 300; export class TripWarningsController { constructor( private readonly hooks: PluginHooks, - @InjectRepository(Trips) private readonly trips: TripsRepository, + private readonly trips: TripAccessService, ) {} @Get(':tripId') diff --git a/server/src/nest/plugins/contributions/trip-warnings.mcp.ts b/server/src/nest/plugins/contributions/trip-warnings.mcp.ts index 651362da6..10c18d704 100644 --- a/server/src/nest/plugins/contributions/trip-warnings.mcp.ts +++ b/server/src/nest/plugins/contributions/trip-warnings.mcp.ts @@ -1,12 +1,10 @@ import { z } from 'zod'; import { McpController, Tool, TOOL_ANNOTATIONS_READONLY, ok, type McpContext, type McpTextResult } from '../../../nest-mcp'; import { noAccess } from '../../../mcp/tools/_shared'; -import { InjectRepository } from '@mikro-orm/nestjs'; -import { Trips } from '../../../db/entities/Trips.entity'; -import type { TripsRepository } from '../../../db/repositories/Trips.repository'; import { pluginsEnabled } from '../kill-switch'; import { PluginHooks } from '../plugin-hooks.service'; import { stripEmoji } from '../text-sanitize'; +import { TripAccessService } from '../../trip-membership/trip-access.service'; /** * The MCP half of GET /api/trip-warnings/:tripId (#1429). @@ -42,7 +40,7 @@ const MESSAGE_MAX = 300; export class TripWarningsMcp { constructor( private readonly hooks: PluginHooks, - @InjectRepository(Trips) private readonly trips: TripsRepository, + private readonly trips: TripAccessService, ) {} @Tool({ diff --git a/server/src/nest/plugins/contributions/view-contributions.controller.ts b/server/src/nest/plugins/contributions/view-contributions.controller.ts index 31526cd76..bbdea361f 100644 --- a/server/src/nest/plugins/contributions/view-contributions.controller.ts +++ b/server/src/nest/plugins/contributions/view-contributions.controller.ts @@ -1,12 +1,10 @@ import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common'; import type { Request } from 'express'; -import { InjectRepository } from '@mikro-orm/nestjs'; -import { Trips } from '../../../db/entities/Trips.entity'; -import type { TripsRepository } from '../../../db/repositories/Trips.repository'; import { JwtAuthGuard } from '../../auth-core/jwt-auth.guard'; import { pluginsEnabled } from '../kill-switch'; import { PluginHooks } from '../plugin-hooks.service'; import { stripEmoji } from '../text-sanitize'; +import { TripAccessService } from '../../trip-membership/trip-access.service'; /** * GET /api/view-contributions/:view/:tripId — host-rendered columns/actions that @@ -106,7 +104,7 @@ function normalize(pluginId: string, raw: unknown): Contribution[] { export class ViewContributionsController { constructor( private readonly hooks: PluginHooks, - @InjectRepository(Trips) private readonly trips: TripsRepository, + private readonly trips: TripAccessService, ) {} @Get(':view/:tripId') diff --git a/server/src/nest/realtime/realtime-gateway.module.ts b/server/src/nest/realtime/realtime-gateway.module.ts index 6b860cd2b..6992d4ab4 100644 --- a/server/src/nest/realtime/realtime-gateway.module.ts +++ b/server/src/nest/realtime/realtime-gateway.module.ts @@ -6,6 +6,7 @@ import { AppSettings } from '../../db/entities/AppSettings.entity'; import { Users } from '../../db/entities/Users.entity'; import { Trips } from '../../db/entities/Trips.entity'; import { processRooms, RoomRegistry, roomsSlot } from './ws-state'; +import { TripMembershipModule } from '../trip-membership/trip-membership.module'; /** * The transport, kept out of RealtimeModule on purpose. @@ -28,7 +29,7 @@ import { processRooms, RoomRegistry, roomsSlot } from './ws-state'; // DatabaseService onto UsersRepository/AppSettingsRepository. Trips: Plan 4 // Task 2 — handleJoin's own canAccessTrip delegate, now TripsRepository // directly. - imports: [EphemeralTokenModule, MikroOrmModule.forFeature([Users, AppSettings, Trips])], + imports: [TripMembershipModule, EphemeralTokenModule, MikroOrmModule.forFeature([Users, AppSettings, Trips])], // The room registry is the process-wide in-memory one; the constructor // below hands whichever one the container resolved to the broadcast // functions, so the gateway and the broadcasts never use two registries. diff --git a/server/src/nest/realtime/realtime.gateway.ts b/server/src/nest/realtime/realtime.gateway.ts index 2f561f277..166f039c2 100644 --- a/server/src/nest/realtime/realtime.gateway.ts +++ b/server/src/nest/realtime/realtime.gateway.ts @@ -16,8 +16,6 @@ import { AppSettings } from '../../db/entities/AppSettings.entity'; import type { AppSettingsRepository } from '../../db/repositories/AppSettings.repository'; import { Users } from '../../db/entities/Users.entity'; import type { UsersRepository } from '../../db/repositories/Users.repository'; -import { Trips } from '../../db/entities/Trips.entity'; -import type { TripsRepository } from '../../db/repositories/Trips.repository'; import { User } from '../../types'; import { logError } from '../audit/audit-log.logger'; import { @@ -33,6 +31,7 @@ import { import { JOURNEY_ACCESS, type JourneyAccess } from './journey-access.types'; import { readAppSetting } from '../common/app-settings.registry'; import { runningVersion } from '../../app-config'; +import { TripAccessService } from '../trip-membership/trip-access.service'; const HEARTBEAT_INTERVAL = 30_000; @@ -64,9 +63,9 @@ export class RealtimeGateway constructor( // Plan 4 Task 2 — canAccessTrip's own DatabaseService delegation is gone: - // this injects TripsRepository directly (same constructor slot) and + // this injects TripAccessService (trip-membership) (same constructor slot) and // calls findAccessible from `handleJoin`. - @InjectRepository(Trips) private readonly trips: TripsRepository, + private readonly trips: TripAccessService, private readonly tokens: EphemeralTokenService, /* * For the book rooms, and injected rather than reimplemented: who may open diff --git a/server/src/nest/receipt-scan/receipt-scan.controller.ts b/server/src/nest/receipt-scan/receipt-scan.controller.ts index 3849ec653..075be93e1 100644 --- a/server/src/nest/receipt-scan/receipt-scan.controller.ts +++ b/server/src/nest/receipt-scan/receipt-scan.controller.ts @@ -1,6 +1,5 @@ import { Controller, HttpException, Param, Post, UploadedFile, UseGuards, UseInterceptors } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; -import { InjectRepository } from '@mikro-orm/nestjs'; import { memoryStorage } from 'multer'; import type { ReceiptScanStartResponse } from '@trek/shared'; import type { User } from '../../types'; @@ -8,12 +7,11 @@ import { ADDON_IDS } from '../../addons'; import { AddonsService } from '../addons/addons.service'; import { JwtAuthGuard } from '../auth-core/jwt-auth.guard'; import { CurrentUser } from '../auth-core/current-user.decorator'; -import { Trips } from '../../db/entities/Trips.entity'; -import type { TripsRepository } from '../../db/repositories/Trips.repository'; import { PermissionsService } from '../permissions/permissions.service'; import { ImportJobsService } from '../booking-import/import-jobs.service'; import { LlmParseService } from '../llm-parse/llm-parse.service'; import { imageMimeType } from '../llm-parse/image-input'; +import { TripAccessService } from '../trip-membership/trip-access.service'; const MAX_FILE_BYTES = 10 * 1024 * 1024; @@ -40,7 +38,7 @@ export class ReceiptScanController { constructor( private readonly importJobs: ImportJobsService, private readonly llmParse: LlmParseService, - @InjectRepository(Trips) private readonly trips: TripsRepository, + private readonly trips: TripAccessService, private readonly permissions: PermissionsService, private readonly addons: AddonsService, ) {} diff --git a/server/src/nest/receipt-scan/receipt-scan.module.ts b/server/src/nest/receipt-scan/receipt-scan.module.ts index 4398f2a03..c57d101d5 100644 --- a/server/src/nest/receipt-scan/receipt-scan.module.ts +++ b/server/src/nest/receipt-scan/receipt-scan.module.ts @@ -6,10 +6,11 @@ import { BookingImportModule } from '../booking-import/booking-import.module'; import { LlmParseModule } from '../llm-parse/llm-parse.module'; import { AddonsModule } from '../addons/addons.module'; import { PermissionsModule } from '../permissions/permissions.module'; +import { TripMembershipModule } from '../trip-membership/trip-membership.module'; /** The Costs tab's receipt scan: an HTTP surface over the import job queue. */ @Module({ - imports: [MikroOrmModule.forFeature([Trips]), BookingImportModule, LlmParseModule, AddonsModule, PermissionsModule], + imports: [TripMembershipModule, MikroOrmModule.forFeature([Trips]), BookingImportModule, LlmParseModule, AddonsModule, PermissionsModule], controllers: [ReceiptScanController], }) export class ReceiptScanModule {} diff --git a/server/src/nest/reservation-import/reservation-import.mcp.ts b/server/src/nest/reservation-import/reservation-import.mcp.ts index 4efea644b..fd51306e3 100644 --- a/server/src/nest/reservation-import/reservation-import.mcp.ts +++ b/server/src/nest/reservation-import/reservation-import.mcp.ts @@ -8,12 +8,10 @@ import { airtrailImportSchema } from '@trek/shared'; import { ADDON_IDS } from '../../addons'; import { addonGate } from '../addons/addon-gate'; import { AddonsService } from '../addons/addons.service'; -import { InjectRepository } from '@mikro-orm/nestjs'; -import { Trips } from '../../db/entities/Trips.entity'; -import type { TripsRepository } from '../../db/repositories/Trips.repository'; import { McpToolGuardsService } from '../mcp-shared/mcp-tool-guards.service'; import { noAccess, permissionDenied } from '../../mcp/tools/_shared'; import { AirtrailImportService } from '../integrations/airtrail-import.service'; +import { TripAccessService } from '../trip-membership/trip-access.service'; /** The handler's own @RequireAddon(ADDON_IDS.AIRTRAIL), as an availability gate. */ const airtrailAddonOn = addonGate(ADDON_IDS.AIRTRAIL); @@ -44,9 +42,9 @@ export class ReservationImportMcp { constructor( private readonly airtrailImport: AirtrailImportService, // Plan 4 Task 2 — canAccessTrip's own DatabaseService delegation is gone: - // this injects TripsRepository directly (same constructor slot) and + // this injects TripAccessService (trip-membership) (same constructor slot) and // calls findAccessible. - @InjectRepository(Trips) private readonly trips: TripsRepository, + private readonly trips: TripAccessService, private readonly guards: McpToolGuardsService, readonly addons: AddonsService, ) {} diff --git a/server/src/nest/reservation-import/reservation-import.module.ts b/server/src/nest/reservation-import/reservation-import.module.ts index 7da91b946..9e7c753c7 100644 --- a/server/src/nest/reservation-import/reservation-import.module.ts +++ b/server/src/nest/reservation-import/reservation-import.module.ts @@ -8,6 +8,7 @@ import { AddonsModule } from '../addons/addons.module'; import { PermissionsModule } from '../permissions/permissions.module'; import { McpSharedModule } from '../mcp-shared/mcp-shared.module'; import { Trips } from '../../db/entities/Trips.entity'; +import { TripMembershipModule } from '../trip-membership/trip-membership.module'; /** * The one route prefix that turns something external into reservations. @@ -22,7 +23,7 @@ import { Trips } from '../../db/entities/Trips.entity'; * @Global, so it is named here. */ @Module({ - imports: [BookingImportModule, AirtrailModule, AddonsModule, PermissionsModule, McpSharedModule, MikroOrmModule.forFeature([Trips])], + imports: [TripMembershipModule, BookingImportModule, AirtrailModule, AddonsModule, PermissionsModule, McpSharedModule, MikroOrmModule.forFeature([Trips])], controllers: [ReservationImportController], providers: [ReservationImportMcp], }) diff --git a/server/src/nest/roadtrip/charging.mcp.ts b/server/src/nest/roadtrip/charging.mcp.ts index 478642ed5..5f0ae4892 100644 --- a/server/src/nest/roadtrip/charging.mcp.ts +++ b/server/src/nest/roadtrip/charging.mcp.ts @@ -5,15 +5,13 @@ import { noAccess } from '../../mcp/tools/_shared'; import { McpController, Tool, TOOL_ANNOTATIONS_READONLY, ok, type McpContext } from '../../nest-mcp'; import { addonGate } from '../addons/addon-gate'; import { AddonsService } from '../addons/addons.service'; -import { InjectRepository } from '@mikro-orm/nestjs'; -import { Trips } from '../../db/entities/Trips.entity'; -import type { TripsRepository } from '../../db/repositories/Trips.repository'; import { ChargingService } from './charging.service'; import { answeringRefusals } from './roadtrip-mcp.helpers'; +import { TripAccessService } from '../trip-membership/trip-access.service'; @McpController() export class ChargingMcp { - constructor(private readonly charging: ChargingService, @InjectRepository(Trips) private readonly tripsRepo: TripsRepository, readonly addons: AddonsService) {} + constructor(private readonly charging: ChargingService, private readonly tripsRepo: TripAccessService, readonly addons: AddonsService) {} @Tool({ name: 'get_roadtrip_charging_info', description: 'Read open charging availability and published tariffs for a saved charging stop. No API key required. Coverage varies by region and operator. Unknown or stale status is not available capacity. Prices can have conditions and extra fees and are not personalized roaming quotes.', inputSchema: { tripId: z.number().int().positive(), placeId: z.number().int().positive() }, annotations: TOOL_ANNOTATIONS_READONLY, access: { group: 'trips', mode: 'read' }, when: addonGate(ADDON_IDS.ROADTRIP) }) diff --git a/server/src/nest/roadtrip/day-boundaries.mcp.ts b/server/src/nest/roadtrip/day-boundaries.mcp.ts index 316374d54..e5a2a5839 100644 --- a/server/src/nest/roadtrip/day-boundaries.mcp.ts +++ b/server/src/nest/roadtrip/day-boundaries.mcp.ts @@ -2,9 +2,6 @@ import { z } from 'zod'; import { MAX_TRIP_DAYS, roadtripDayBoundarySchema, type RoadtripDayBoundary } from '@trek/shared'; import { McpController, Tool, TOOL_ANNOTATIONS_READONLY, TOOL_ANNOTATIONS_WRITE, ok, type McpContext } from '../../nest-mcp'; import { noAccess, permissionDenied } from '../../mcp/tools/_shared'; -import { InjectRepository } from '@mikro-orm/nestjs'; -import { Trips } from '../../db/entities/Trips.entity'; -import type { TripsRepository } from '../../db/repositories/Trips.repository'; import { McpToolGuardsService } from '../mcp-shared/mcp-tool-guards.service'; import { RealtimeService } from '../realtime/realtime.service'; import { AddonsService } from '../addons/addons.service'; @@ -12,6 +9,7 @@ import { addonGate } from '../addons/addon-gate'; import { ADDON_IDS } from '../../addons'; import { DayBoundariesService } from './day-boundaries.service'; import { answeringRefusals } from './roadtrip-mcp.helpers'; +import { TripAccessService } from '../trip-membership/trip-access.service'; const when = addonGate(ADDON_IDS.ROADTRIP); @@ -19,7 +17,7 @@ const when = addonGate(ADDON_IDS.ROADTRIP); export class DayBoundariesMcp { constructor( private readonly boundaries: DayBoundariesService, - @InjectRepository(Trips) private readonly tripsRepo: TripsRepository, + private readonly tripsRepo: TripAccessService, private readonly guards: McpToolGuardsService, private readonly realtime: RealtimeService, readonly addons: AddonsService, diff --git a/server/src/nest/roadtrip/google-route.service.ts b/server/src/nest/roadtrip/google-route.service.ts index 08814228d..463c80966 100644 --- a/server/src/nest/roadtrip/google-route.service.ts +++ b/server/src/nest/roadtrip/google-route.service.ts @@ -8,10 +8,9 @@ import { UnitOfWork } from '../database/unit-of-work'; import { PlacesService } from '../places/places.service'; import { AssignmentsService } from '../assignments/assignments.service'; import { PermissionsService } from '../permissions/permissions.service'; -import { Trips } from '../../db/entities/Trips.entity'; -import type { TripsRepository } from '../../db/repositories/Trips.repository'; import { Users } from '../../db/entities/Users.entity'; import type { UsersRepository } from '../../db/repositories/Users.repository'; +import { TripAccessService } from '../trip-membership/trip-access.service'; @Injectable() export class GoogleRouteService { @@ -19,7 +18,7 @@ export class GoogleRouteService { private readonly places: PlacesService, private readonly assignments: AssignmentsService, private readonly permissions: PermissionsService, private readonly uow: UnitOfWork, - @InjectRepository(Trips) private readonly tripsRepo: TripsRepository, + private readonly tripsRepo: TripAccessService, @InjectRepository(Users) private readonly usersRepo: UsersRepository) {} async preview(raw: string): Promise { diff --git a/server/src/nest/roadtrip/roadtrip-hazards.mcp.ts b/server/src/nest/roadtrip/roadtrip-hazards.mcp.ts index 2bb093532..cb97a2931 100644 --- a/server/src/nest/roadtrip/roadtrip-hazards.mcp.ts +++ b/server/src/nest/roadtrip/roadtrip-hazards.mcp.ts @@ -4,14 +4,12 @@ import { noAccess } from '../../mcp/tools/_shared'; import { McpController, Tool, TOOL_ANNOTATIONS_READONLY, ok, type McpContext } from '../../nest-mcp'; import { addonGate } from '../addons/addon-gate'; import { AddonsService } from '../addons/addons.service'; -import { InjectRepository } from '@mikro-orm/nestjs'; -import { Trips } from '../../db/entities/Trips.entity'; -import type { TripsRepository } from '../../db/repositories/Trips.repository'; import { RoadtripHazardsService } from './roadtrip-hazards.service'; +import { TripAccessService } from '../trip-membership/trip-access.service'; @McpController() export class RoadtripHazardsMcp { - constructor(private readonly hazards: RoadtripHazardsService, @InjectRepository(Trips) private readonly tripsRepo: TripsRepository, readonly addons: AddonsService) {} + constructor(private readonly hazards: RoadtripHazardsService, private readonly tripsRepo: TripAccessService, readonly addons: AddonsService) {} @Tool({ name: 'get_roadtrip_hazards', description: 'Read current DWD and GDACS hazard notices, geometry, source timestamps and feed availability. These are current notices, not forecasts for the trip dates or confirmed road closures. A point means no affected-area polygon is available. Coverage can be incomplete. This does not change routes. The shared roadtrip_show_hazards preference controls map display.', diff --git a/server/src/nest/roadtrip/roadtrip-plan.service.ts b/server/src/nest/roadtrip/roadtrip-plan.service.ts index 2c1a283dc..e660e73d9 100644 --- a/server/src/nest/roadtrip/roadtrip-plan.service.ts +++ b/server/src/nest/roadtrip/roadtrip-plan.service.ts @@ -6,8 +6,6 @@ import { RoadtripRouterService } from './roadtrip-router.service'; import { RoadtripService } from './roadtrip.service'; import { Injectable, HttpException } from '@nestjs/common'; import { type RoadtripPreferences } from '@trek/shared'; -import { Trips } from '../../db/entities/Trips.entity'; -import type { TripsRepository } from '../../db/repositories/Trips.repository'; import { Days } from '../../db/entities/Days.entity'; import type { DaysRepository } from '../../db/repositories/Days.repository'; import { DayAssignments } from '../../db/entities/DayAssignments.entity'; @@ -48,6 +46,7 @@ import { type RouteAvoidClass, standsAsDay, } from '@trek/shared/roadtrip'; +import { TripAccessService } from '../trip-membership/trip-access.service'; const stopKey = (s: RoadtripStop) => `${s.lat.toFixed(5)},${s.lng.toFixed(5)},${s.legMode ?? ''},${s.incomingLegMode ?? ''}`; @@ -60,7 +59,7 @@ export class RoadtripPlanService { private readonly router: RoadtripRouterService, private readonly roadtrip: RoadtripService, private readonly boundaries: DayBoundariesService, - @InjectRepository(Trips) private readonly tripsRepo: TripsRepository, + private readonly tripsRepo: TripAccessService, @InjectRepository(Days) private readonly daysRepo: DaysRepository, @InjectRepository(DayAssignments) private readonly dayAssignmentsRepo: DayAssignmentsRepository, @InjectRepository(DayAccommodations) private readonly dayAccommodationsRepo: DayAccommodationsRepository, diff --git a/server/src/nest/roadtrip/roadtrip-preferences.mcp.ts b/server/src/nest/roadtrip/roadtrip-preferences.mcp.ts index c44777023..196ade17e 100644 --- a/server/src/nest/roadtrip/roadtrip-preferences.mcp.ts +++ b/server/src/nest/roadtrip/roadtrip-preferences.mcp.ts @@ -10,15 +10,13 @@ import { } from '../../nest-mcp'; import { addonGate } from '../addons/addon-gate'; import { AddonsService } from '../addons/addons.service'; -import { InjectRepository } from '@mikro-orm/nestjs'; -import { Trips } from '../../db/entities/Trips.entity'; -import type { TripsRepository } from '../../db/repositories/Trips.repository'; import { McpToolGuardsService } from '../mcp-shared/mcp-tool-guards.service'; import { RoadtripPreferencesService } from './roadtrip-preferences.service'; import { answeringRefusals } from './roadtrip-mcp.helpers'; import { roadtripPreferencesUpdateSchema, type RoadtripPreferences } from '@trek/shared'; import { z } from 'zod'; +import { TripAccessService } from '../trip-membership/trip-access.service'; const when = addonGate(ADDON_IDS.ROADTRIP); @@ -27,7 +25,7 @@ export class RoadtripPreferencesMcp { constructor( private readonly preferences: RoadtripPreferencesService, readonly addons: AddonsService, - @InjectRepository(Trips) private readonly tripsRepo: TripsRepository, + private readonly tripsRepo: TripAccessService, private readonly guards: McpToolGuardsService, ) {} diff --git a/server/src/nest/roadtrip/roadtrip.mcp.ts b/server/src/nest/roadtrip/roadtrip.mcp.ts index ca7e0b620..f6a64c5c1 100644 --- a/server/src/nest/roadtrip/roadtrip.mcp.ts +++ b/server/src/nest/roadtrip/roadtrip.mcp.ts @@ -1,15 +1,13 @@ import { McpController, Tool, TOOL_ANNOTATIONS_READONLY, TOOL_ANNOTATIONS_NON_IDEMPOTENT, ok, type McpContext } from '../../nest-mcp'; import { z } from 'zod'; -import { InjectRepository } from '@mikro-orm/nestjs'; import { RoadtripService } from './roadtrip.service'; -import { Trips } from '../../db/entities/Trips.entity'; -import type { TripsRepository } from '../../db/repositories/Trips.repository'; import { McpToolGuardsService } from '../mcp-shared/mcp-tool-guards.service'; import { noAccess, permissionDenied } from '../../mcp/tools/_shared'; import { ADDON_IDS } from '../../addons'; import { addonGate } from '../addons/addon-gate'; import { AddonsService } from '../addons/addons.service'; import { roadtripViaUpdateRequestSchema, type RoadtripViaUpdateRequest } from '@trek/shared'; +import { TripAccessService } from '../trip-membership/trip-access.service'; /** * The whole surface rides the road trip addon, the same way the controller does @@ -40,7 +38,7 @@ const BOOKEND_VIA_NOTE = export class RoadtripMcp { constructor( private readonly roadtrip: RoadtripService, - @InjectRepository(Trips) private readonly tripsRepo: TripsRepository, + private readonly tripsRepo: TripAccessService, private readonly guards: McpToolGuardsService, readonly addons: AddonsService, ) {} diff --git a/server/src/nest/roadtrip/roadtrip.module.ts b/server/src/nest/roadtrip/roadtrip.module.ts index 9082492b1..9e927fc9d 100644 --- a/server/src/nest/roadtrip/roadtrip.module.ts +++ b/server/src/nest/roadtrip/roadtrip.module.ts @@ -47,6 +47,7 @@ import { DayAccommodations } from '../../db/entities/DayAccommodations.entity'; import { Reservations } from '../../db/entities/Reservations.entity'; import { ReservationEndpoints } from '../../db/entities/ReservationEndpoints.entity'; import { ReservationDayPositions } from '../../db/entities/ReservationDayPositions.entity'; +import { TripMembershipModule } from '../trip-membership/trip-membership.module'; /** Road trip domain (#1797): the points a drive is routed through. Registered in AppModule. */ @Module({ @@ -71,7 +72,7 @@ import { ReservationDayPositions } from '../../db/entities/ReservationDayPositio // `ReservationDayPositions`: `RoadtripPlanService`'s RPL3-RPL5 reads (the // booked nights and the carrier bookings that seam the drive) plus the RPL6 // terminal and RS19 day-position reads it joins onto them. - imports: [ + imports: [TripMembershipModule, McpSharedModule, PermissionsModule, diff --git a/server/src/nest/todo/todo.module.ts b/server/src/nest/todo/todo.module.ts index 8c112fb21..55bfe4798 100644 --- a/server/src/nest/todo/todo.module.ts +++ b/server/src/nest/todo/todo.module.ts @@ -13,6 +13,7 @@ import { TodoItems } from '../../db/entities/TodoItems.entity'; import { TodoCategoryAssignees } from '../../db/entities/TodoCategoryAssignees.entity'; import { Trips } from '../../db/entities/Trips.entity'; import { TripMembers } from '../../db/entities/TripMembers.entity'; +import { TripMembershipModule } from '../trip-membership/trip-membership.module'; /** To-do domain (S3 — Phase 2 trip sub-domain). Registered in AppModule. * Exports TodoService for in-container consumers (TripsService bundle). @@ -23,7 +24,7 @@ import { TripMembers } from '../../db/entities/TripMembers.entity'; * (Plan 4 Task 2): `verifyTripAccess`'s own canAccessTrip delegate, now * TripsRepository directly. */ @Module({ - imports: [MikroOrmModule.forFeature([TodoItems, TodoCategoryAssignees, Trips, TripMembers]), McpSharedModule, PermissionsModule, RealtimeModule, PluginGuardsModule, AddonsModule], + imports: [TripMembershipModule, MikroOrmModule.forFeature([TodoItems, TodoCategoryAssignees, Trips, TripMembers]), McpSharedModule, PermissionsModule, RealtimeModule, PluginGuardsModule, AddonsModule], controllers: [TodoController], providers: [TodoService, TodoMcp, TodoRpc], exports: [TodoService], diff --git a/server/src/nest/todo/todo.service.ts b/server/src/nest/todo/todo.service.ts index da07ba2ba..69901ee0d 100644 --- a/server/src/nest/todo/todo.service.ts +++ b/server/src/nest/todo/todo.service.ts @@ -9,10 +9,10 @@ import { TodoItems } from '../../db/entities/TodoItems.entity'; import type { TodoItemsRepository } from '../../db/repositories/TodoItems.repository'; import { TodoCategoryAssignees } from '../../db/entities/TodoCategoryAssignees.entity'; import type { TodoCategoryAssigneesRepository } from '../../db/repositories/TodoCategoryAssignees.repository'; -import { Trips } from '../../db/entities/Trips.entity'; -import type { TripsRepository, TripAccess } from '../../db/repositories/Trips.repository'; +import type { TripAccess } from '../../db/repositories/Trips.repository'; import { TripMembers } from '../../db/entities/TripMembers.entity'; import type { TripMembersRepository } from '../../db/repositories/TripMembers.repository'; +import { TripAccessService } from '../trip-membership/trip-access.service'; type Trip = TripAccess; @@ -38,8 +38,8 @@ export class TodoService { @InjectRepository(TodoItems) private readonly todoItemsRepo: TodoItemsRepository, @InjectRepository(TodoCategoryAssignees) private readonly todoCategoryAssigneesRepo: TodoCategoryAssigneesRepository, // Plan 4 Task 2 — canAccessTrip's own DatabaseService delegation is - // gone: this injects TripsRepository directly. - @InjectRepository(Trips) private readonly tripsRepo: TripsRepository, + // gone: this injects TripAccessService (trip-membership). + private readonly tripsRepo: TripAccessService, // Plan 4 Task 3 — DatabaseService.rosterUserIds inlined onto // TripMembersRepository.rosterUserIds directly. @InjectRepository(TripMembers) private readonly tripMembersRepo: TripMembersRepository, diff --git a/server/src/nest/transit/transit.mcp.ts b/server/src/nest/transit/transit.mcp.ts index 06dfb1ba8..c2ca7f568 100644 --- a/server/src/nest/transit/transit.mcp.ts +++ b/server/src/nest/transit/transit.mcp.ts @@ -15,13 +15,11 @@ import { } from './transit-itinerary.helpers'; import { noAccess, permissionDenied } from '../../mcp/tools/_shared'; import { RateLimitService } from '../common/rate-limit.service'; -import { InjectRepository } from '@mikro-orm/nestjs'; -import { Trips } from '../../db/entities/Trips.entity'; -import type { TripsRepository } from '../../db/repositories/Trips.repository'; import { DaysService } from '../days/days.service'; import { ReservationsService } from '../reservations/reservations.service'; import { SCHEDULED_TRANSIT_MODES, type TransitItinerary } from './transit.helpers'; import { TransitService } from './transit.service'; +import { TripAccessService } from '../trip-membership/trip-access.service'; const TRANSIT_RATE_WINDOW = 15 * 60 * 1000; @@ -65,9 +63,9 @@ export class TransitMcp { private readonly days: DaysService, private readonly reservations: ReservationsService, // Plan 4 Task 2 — canAccessTrip's own DatabaseService delegation is gone: - // this injects TripsRepository directly (same constructor slot) and + // this injects TripAccessService (trip-membership) (same constructor slot) and // calls findAccessible. - @InjectRepository(Trips) private readonly trips: TripsRepository, + private readonly trips: TripAccessService, private readonly guards: McpToolGuardsService, // The limiter RateLimitModule provides, so its store is the one every // other caller counts in. The MCP buckets (mcp_transit_*) are keyed by diff --git a/server/src/nest/transit/transit.module.ts b/server/src/nest/transit/transit.module.ts index 16538d382..c227d061d 100644 --- a/server/src/nest/transit/transit.module.ts +++ b/server/src/nest/transit/transit.module.ts @@ -13,6 +13,7 @@ import { Users } from '../../db/entities/Users.entity'; import { Trips } from '../../db/entities/Trips.entity'; import { GoogleQuotaModule } from '../google-quota/google-quota.module'; import { AppConfigModule } from '../app-config/app-config.module'; +import { TripMembershipModule } from '../trip-membership/trip-membership.module'; /** * Transit domain (#1065) — the Transitous/MOTIS proxy, with the optional @@ -32,7 +33,7 @@ import { AppConfigModule } from '../app-config/app-config.module'; */ @Module({ // DaysModule + ReservationsModule: TransitMcp's create_transit_journey injects both. - imports: [AppConfigModule, McpSharedModule, RateLimitModule, DaysModule, ReservationsModule, MikroOrmModule.forFeature([AppSettings, Users, Trips]), GoogleQuotaModule], + imports: [TripMembershipModule, AppConfigModule, McpSharedModule, RateLimitModule, DaysModule, ReservationsModule, MikroOrmModule.forFeature([AppSettings, Users, Trips]), GoogleQuotaModule], controllers: [TransitController], providers: [TransitService, TransitMcp, GoogleTransitProvider], exports: [TransitService], diff --git a/server/src/nest/trip-invite/trip-invite.service.ts b/server/src/nest/trip-invite/trip-invite.service.ts index cab317d82..af8f8619e 100644 --- a/server/src/nest/trip-invite/trip-invite.service.ts +++ b/server/src/nest/trip-invite/trip-invite.service.ts @@ -3,13 +3,12 @@ import { InjectRepository } from '@mikro-orm/nestjs'; import crypto from 'crypto'; import { UnitOfWork } from '../database/unit-of-work'; import type { TripAccess } from '../../db/repositories/Trips.repository'; -import { Trips } from '../../db/entities/Trips.entity'; -import type { TripsRepository } from '../../db/repositories/Trips.repository'; import { PermissionsService } from '../permissions/permissions.service'; import { TripMembershipService } from '../trip-membership/trip-membership.service'; import { TripInviteTokens } from '../../db/entities/TripInviteTokens.entity'; import type { TripInviteTokensRepository } from '../../db/repositories/TripInviteTokens.repository'; import type { User } from '../../types'; +import { TripAccessService } from '../trip-membership/trip-access.service'; type Trip = TripAccess; @@ -41,9 +40,9 @@ export interface TripInviteInfo { export class TripInviteService { constructor( // Plan 4 Task 2 — canAccessTrip's own DatabaseService delegation is gone: - // this injects TripsRepository directly (same constructor slot) and + // this injects TripAccessService (trip-membership) (same constructor slot) and // calls findAccessible. - @InjectRepository(Trips) private readonly trips: TripsRepository, + private readonly trips: TripAccessService, private readonly permissions: PermissionsService, private readonly membership: TripMembershipService, private readonly uow: UnitOfWork, diff --git a/server/src/nest/trip-membership/trip-access.service.ts b/server/src/nest/trip-membership/trip-access.service.ts new file mode 100644 index 000000000..fcb27483d --- /dev/null +++ b/server/src/nest/trip-membership/trip-access.service.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@mikro-orm/nestjs'; +import { Trips } from '../../db/entities/Trips.entity'; +import type { TripAccess, TripsRepository } from '../../db/repositories/Trips.repository'; + +/** + * "May this user see this trip?" for every domain that is not the trips domain. + * + * It answers with the trip's access row (id, owner, currency) when the user + * owns the trip or is a member, and undefined otherwise. It deliberately makes + * no HTTP decision: a REST route turns undefined into its 404, an MCP tool into + * its "not found or access denied" result, a plugin RPC into its error, and + * each caller keeps the one it had. + * + * Before this the domains injected TripsRepository themselves for this one + * read, so the trips table had a reader in thirty files outside its domain. + * The id is bound exactly as given (TripsRepository.findAccessible explains + * why it is never coerced here); a caller that coerced before still does. + */ +@Injectable() +export class TripAccessService { + constructor(@InjectRepository(Trips) private readonly trips: TripsRepository) {} + + findAccessible(tripId: number | string, userId: number): Promise { + return this.trips.findAccessible(tripId, userId); + } +} diff --git a/server/src/nest/trip-membership/trip-membership.module.ts b/server/src/nest/trip-membership/trip-membership.module.ts index 094ddd42e..a8733d1f9 100644 --- a/server/src/nest/trip-membership/trip-membership.module.ts +++ b/server/src/nest/trip-membership/trip-membership.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { MikroOrmModule } from '@mikro-orm/nestjs'; import { TripMembershipService } from './trip-membership.service'; +import { TripAccessService } from './trip-access.service'; import { Trips } from '../../db/entities/Trips.entity'; import { TripMembers } from '../../db/entities/TripMembers.entity'; @@ -13,10 +14,13 @@ import { TripMembers } from '../../db/entities/TripMembers.entity'; * (Plan 3c Task 1 ruling): five modules import THIS module, so a new service * import here would close a cycle. `EntityManager`/repository resolution * comes entirely from the ORM's own `@Global()` core module + this - * `forFeature` registration, keeping this module a leaf. */ + * `forFeature` registration, keeping this module a leaf. + * + * TripAccessService is the trip-visibility read every other domain asks + * instead of injecting TripsRepository for it. */ @Module({ imports: [MikroOrmModule.forFeature([Trips, TripMembers])], - providers: [TripMembershipService], - exports: [TripMembershipService], + providers: [TripMembershipService, TripAccessService], + exports: [TripMembershipService, TripAccessService], }) export class TripMembershipModule {} diff --git a/server/tests/helpers/accommodations-service.ts b/server/tests/helpers/accommodations-service.ts index 5c78967d2..68429c98f 100644 --- a/server/tests/helpers/accommodations-service.ts +++ b/server/tests/helpers/accommodations-service.ts @@ -20,6 +20,7 @@ import { createTestJourneyPhotosRepo, createTestJourneyEntryPhotosRepo, } from './journey-repos'; import { createTestToursRepo } from './tours-repos'; +import { TripAccessService } from '../../src/nest/trip-membership/trip-access.service'; /** * AccommodationsService over a test connection. @@ -51,7 +52,7 @@ export async function accommodationsOver(conn: Database): Promise { await createTestRoadtripViasRepo(db), await createTestRoadtripDayBoundariesRepo(db), ); - const todoService = new TodoService(permissionsService, realtimeService, await createTestUnitOfWork(db), await createTestTodoItemsRepo(db), await createTestTodoCategoryAssigneesRepo(db), await createTestTripsRepo(db), await createTestTripMembersRepo(db)); + const todoService = new TodoService(permissionsService, realtimeService, await createTestUnitOfWork(db), await createTestTodoItemsRepo(db), await createTestTodoCategoryAssigneesRepo(db), new TripAccessService(await createTestTripsRepo(db)), await createTestTripMembersRepo(db)); const packingService = new PackingService( permissionsService, realtimeService, notificationsStub(), await createTestUnitOfWork(db), await createTestPackingItemsRepo(db), await createTestPackingItemContributorsRepo(db), await createTestPackingBagsRepo(db), @@ -266,7 +267,7 @@ export async function createMcpTestRegistry(): Promise { // One instance, four consumers: AssignmentsMcp, ReservationsMcp, PlacesMcp and // AccommodationsService, which writes the day stop a booked night implies. const assignmentsService = new AssignmentsService( - await createTestTripsRepo(db), permissionsService, realtimeService, queryHelpersService, journeyDomain, await createTestUnitOfWork(db), + new TripAccessService(await createTestTripsRepo(db)), permissionsService, realtimeService, queryHelpersService, journeyDomain, await createTestUnitOfWork(db), await createTestDayAssignmentsRepo(db), await createTestAssignmentParticipantsRepo(db), await createTestDaysRepo(db), @@ -277,7 +278,7 @@ export async function createMcpTestRegistry(): Promise { ); const accommodationsService = new AccommodationsService( permissionsService, realtimeService, assignmentsService, await createTestUnitOfWork(db), - await createTestTripsRepo(db), + new TripAccessService(await createTestTripsRepo(db)), await createTestDayAccommodationsRepo(db), await createTestDayAssignmentsRepo(db), await createTestPlacesRepo(db), @@ -338,7 +339,7 @@ export async function createMcpTestRegistry(): Promise { // once and shared by both consumers below (readModelService, FilesMcp) // rather than reconstructed twice with the same seven-repository tail. const filesService = new FilesService( - await createTestTripsRepo(db), permissionsService, realtimeService, new EphemeralTokenService(), generalStorage, mcpOrm.em, + new TripAccessService(await createTestTripsRepo(db)), permissionsService, realtimeService, new EphemeralTokenService(), generalStorage, mcpOrm.em, await createTestUnitOfWork(db), await createTestTripFilesRepo(db), await createTestFileLinksRepo(db), @@ -388,7 +389,7 @@ export async function createMcpTestRegistry(): Promise { new PackingMcp(packingService, addonsService, guards), new BudgetMcp(budgetService, exchangeRatesService, new RuntimeEnvService(), new TripMembershipService(await createTestTripsRepo(db), await createTestTripMembersRepo(db)), addonsService, guards, await createTestUnitOfWork(db), await createTestPlacesRepo(db), await createTestTripsRepo(db), await createTestTripMembersRepo(db)), new ReservationsMcp(reservationsService, daysService, budgetService, assignmentsService, guards), - new DayNotesMcp(new DayNotesService(await createTestTripsRepo(db), permissionsService, realtimeService, await createTestDayNotesRepo(db), await createTestDaysRepo(db)), guards), + new DayNotesMcp(new DayNotesService(new TripAccessService(await createTestTripsRepo(db)), permissionsService, realtimeService, await createTestDayNotesRepo(db), await createTestDaysRepo(db)), guards), new DaysMcp(daysService, guards, dayRemovalService), new RoadtripMcp( new RoadtripService( @@ -399,13 +400,13 @@ export async function createMcpTestRegistry(): Promise { await createTestRoadtripViasRepo(db), await createTestRoadtripDayTracksRepo(db), ), - await createTestTripsRepo(db), + new TripAccessService(await createTestTripsRepo(db)), guards, addonsService, ), new RoadtripPreferencesMcp( new RoadtripPreferencesService(realtimeService, await createTestUnitOfWork(db), await createTestRoadtripPreferencesRepo(db)), - addonsService, await createTestTripsRepo(db), guards, + addonsService, new TripAccessService(await createTestTripsRepo(db)), guards, ), new FilesMcp(filesService, guards, new AllowedFileTypesService(appSettings)), new AccommodationsMcp(accommodationsService, placesService, guards, await createTestUnitOfWork(db)), @@ -434,9 +435,9 @@ export async function createMcpTestRegistry(): Promise { new TripPromptsMcp(tripsService, readModelService, packingService, addonsService), new ShareMcp(new ShareService(new SettingsService(await createTestUnitOfWork(db), appSettings, await createTestSettingsRepo(db)), permissionsService, queryHelpersService, placePhotoCache, await createTestUnitOfWork(db), ...(await shareServiceRepoArgs(db))), guards), new FeedsMcp(new FeedsService(await createTestTripsRepo(db), usersRepo, calendarService), await createTestTripsRepo(db), new RuntimeEnvService(), guards), - new TripInviteMcp(new TripInviteService(await createTestTripsRepo(db), permissionsService, new TripMembershipService(await createTestTripsRepo(db), await createTestTripMembersRepo(db)), await createTestUnitOfWork(db), await createTestTripInviteTokensRepo(db)), new RuntimeEnvService(), guards, new AuditService(auditLogRepo, usersRepo)), + new TripInviteMcp(new TripInviteService(new TripAccessService(await createTestTripsRepo(db)), permissionsService, new TripMembershipService(await createTestTripsRepo(db), await createTestTripMembersRepo(db)), await createTestUnitOfWork(db), await createTestTripInviteTokensRepo(db)), new RuntimeEnvService(), guards, new AuditService(auditLogRepo, usersRepo)), new MapsMcp(mapsService), - new PlacesMcp(placesService, mapsService, await createTestTripsRepo(db), journeyDomain, assignmentsService, guards, await createTestUnitOfWork(db)), + new PlacesMcp(placesService, mapsService, new TripAccessService(await createTestTripsRepo(db)), journeyDomain, assignmentsService, guards, await createTestUnitOfWork(db)), new CollectionsMcp( new CollectionsService( permissionsService, realtimeService, notificationsStub(), generalStorage, await createTestUnitOfWork(db), @@ -460,7 +461,7 @@ export async function createMcpTestRegistry(): Promise { // (`findUsernameEmail`, UM11's precedent) added in its place. usersRepo, addonsService, ), - new TransitMcp(new TransitService(new GoogleTransitProvider(appSettings, usersRepo, noGoogleQuota), transitConfig()), daysService, reservationsService, await createTestTripsRepo(db), guards), + new TransitMcp(new TransitService(new GoogleTransitProvider(appSettings, usersRepo, noGoogleQuota), transitConfig()), daysService, reservationsService, new TripAccessService(await createTestTripsRepo(db)), guards), new AtlasMcp(new AtlasService( await createTestBucketListRepo(db), await createTestHiddenCountriesRepo(db), await createTestHiddenRegionsRepo(db), await createTestVisitedCountriesRepo(db), @@ -488,10 +489,10 @@ export async function createMcpTestRegistry(): Promise { await createTestReservationsRepo(db), await createTestReservationEndpointsRepo(db), await createTestDaysRepo(db), realtimeService, reservationsService, new AirtrailClient(), new AirtrailService(await createTestUserAirtrailRepo(db), new AuditService(auditLogRepo, usersRepo), new AirtrailClient()), await createTestUnitOfWork(db), - ), await createTestTripsRepo(db), guards, addonsService), + ), new TripAccessService(await createTestTripsRepo(db)), guards, addonsService), new SettingsMcp(new SettingsService(await createTestUnitOfWork(db), appSettings, await createTestSettingsRepo(db))), new HelpMcp(), new AddonsMcp(addonsService), - new TripWarningsMcp(new PluginHooks({ providersOf: () => [], invokeHook: async () => [] } as unknown as PluginRuntimeService), await createTestTripsRepo(db)), + new TripWarningsMcp(new PluginHooks({ providersOf: () => [], invokeHook: async () => [] } as unknown as PluginRuntimeService), new TripAccessService(await createTestTripsRepo(db))), new PluginSearchMcp(new PluginHooks({ providersOf: () => [], invokeHook: async () => [] } as unknown as PluginRuntimeService)), new PluginPoisMcp(new PluginPoisService(new PluginHooks({ providersOf: () => [], invokeHook: async () => [] } as unknown as PluginRuntimeService), await createTestPluginsRepo(db))), ], diff --git a/server/tests/helpers/plugin-host.ts b/server/tests/helpers/plugin-host.ts index 26b082c66..6bef8580d 100644 --- a/server/tests/helpers/plugin-host.ts +++ b/server/tests/helpers/plugin-host.ts @@ -143,6 +143,7 @@ import { noGoogleQuota } from './google-quota'; import { createTestToursRepo } from './tours-repos'; import { createTestBudgetSettlementsRepo } from './budget-repos'; import { MaintenanceRepository } from '../../src/db/repositories/MaintenanceRepository'; +import { TripAccessService } from '../../src/nest/trip-membership/trip-access.service'; /** * Hand-wired counterpart of the PluginsModule DI graph for no-Nest tests @@ -178,7 +179,7 @@ export async function createPluginRpcHostParts( const budget = new BudgetService(permissions, exchangeRates, realtime, await createTestUnitOfWork(db), ...(await budgetRepoArgs(db))); const addons = await createTestAddonsService(db); const queryHelpers = new QueryHelpersService(await createTestTagsRepo(db), await createTestPlaceRatingsRepo(db), await createTestAssignmentParticipantsRepo(db)); - const todos = new TodoService(permissions, realtime, await createTestUnitOfWork(db), await createTestTodoItemsRepo(db), await createTestTodoCategoryAssigneesRepo(db), await createTestTripsRepo(db), await createTestTripMembersRepo(db)); + const todos = new TodoService(permissions, realtime, await createTestUnitOfWork(db), await createTestTodoItemsRepo(db), await createTestTodoCategoryAssigneesRepo(db), new TripAccessService(await createTestTripsRepo(db)), await createTestTripMembersRepo(db)); const packing = new PackingService( permissions, realtime, notificationsStub(), await createTestUnitOfWork(db), await createTestPackingItemsRepo(db), await createTestPackingItemContributorsRepo(db), await createTestPackingBagsRepo(db), @@ -188,7 +189,7 @@ export async function createPluginRpcHostParts( // Plan 3e Task 1 (files): FilesService now also takes uow + the repositories // its R2 transactions and R12 cross-object trip-scoping guard need. const files = new FilesService( - await createTestTripsRepo(db), permissions, realtime, new EphemeralTokenService(), generalStorage, (await sharedTestOrm(db)).em, + new TripAccessService(await createTestTripsRepo(db)), permissions, realtime, new EphemeralTokenService(), generalStorage, (await sharedTestOrm(db)).em, await createTestUnitOfWork(db), await createTestTripFilesRepo(db), await createTestFileLinksRepo(db), @@ -267,9 +268,9 @@ export async function createPluginRpcHostParts( await createTestTripsRepo(db), await createTestPlacesRepo(db), await createTestReservationEndpointsRepo(db), await createTestUnitOfWork(db), (await sharedTestOrm(db)).orm, ); - const dayNotes = new DayNotesService(await createTestTripsRepo(db), permissions, realtime, await createTestDayNotesRepo(db), await createTestDaysRepo(db)); + const dayNotes = new DayNotesService(new TripAccessService(await createTestTripsRepo(db)), permissions, realtime, await createTestDayNotesRepo(db), await createTestDaysRepo(db)); const assignments = new AssignmentsService( - await createTestTripsRepo(db), permissions, realtime, queryHelpers, journey, await createTestUnitOfWork(db), + new TripAccessService(await createTestTripsRepo(db)), permissions, realtime, queryHelpers, journey, await createTestUnitOfWork(db), await createTestDayAssignmentsRepo(db), await createTestAssignmentParticipantsRepo(db), await createTestDaysRepo(db), @@ -287,7 +288,7 @@ export async function createPluginRpcHostParts( const oauth = new PluginOAuthService(pluginOrm.repo(Plugins), pluginOrm.repo(PluginOauthTokens), pluginOrm.repo(PluginOauthState), pluginOrm.repo(PluginSettingsFields), await createTestUnitOfWork(db)); const accommodations = new AccommodationsService( permissions, realtime, assignments, await createTestUnitOfWork(db), - await createTestTripsRepo(db), + new TripAccessService(await createTestTripsRepo(db)), await createTestDayAccommodationsRepo(db), await createTestDayAssignmentsRepo(db), await createTestPlacesRepo(db), @@ -339,7 +340,7 @@ export async function createPluginRpcHostParts( new AccommodationsRpc(accommodations, realtime, guards), new ItineraryRpc(assignments, realtime, guards), new TripsRpc(trips, reservations, days, membership, realtime, guards, accommodations, members, (await sharedTestOrm(db)).em), - new CostsRpc(budget, await createTestTripsRepo(db), realtime, guards, membership), + new CostsRpc(budget, new TripAccessService(await createTestTripsRepo(db)), realtime, guards, membership), new ReservationsRpc(reservations, realtime, guards), new CollabRpc(collab, realtime, guards), new AtlasRpc(atlas, guards), diff --git a/server/tests/unit/budget/costs.rpc.test.ts b/server/tests/unit/budget/costs.rpc.test.ts index ae7ee9076..7239ac12f 100644 --- a/server/tests/unit/budget/costs.rpc.test.ts +++ b/server/tests/unit/budget/costs.rpc.test.ts @@ -21,6 +21,7 @@ import type { UsersRepository } from '../../../src/db/repositories/Users.reposit import type { TripMembershipService } from '../../../src/nest/trip-membership/trip-membership.service'; import type { RpcRequest, RpcError } from '../../../src/nest/plugins/protocol/envelope'; import { makeDeps } from '../../helpers/rpc-host-deps'; +import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service'; const req = (method: string, params: Record = {}): RpcRequest => ({ k: 'req', id: 'x', method, params }); @@ -55,7 +56,7 @@ function build(opts: { addonOn?: boolean; canEdit?: boolean; missing?: boolean; ); // The leaf membership read replaced the deleted trips.bridge for listMine. const membership = { listAccessibleTripIds: vi.fn(() => [1, 2]) } as unknown as TripMembershipService; - const rpc = new CostsRpc(budget, db, realtime, guards, membership); + const rpc = new CostsRpc(budget, new TripAccessService(db), realtime, guards, membership); const host = (...grants: string[]) => new PluginRpcHost('p', new Set(grants.length ? grants : ['db:read:costs', 'db:write:costs']), makeDeps(), createTestPluginRegistry([rpc])); return { budget, realtime, host }; diff --git a/server/tests/unit/nest/assignments.service.test.ts b/server/tests/unit/nest/assignments.service.test.ts index 304dd8132..d9e9b1298 100644 --- a/server/tests/unit/nest/assignments.service.test.ts +++ b/server/tests/unit/nest/assignments.service.test.ts @@ -60,6 +60,7 @@ import { Days } from '../../../src/db/entities/Days.entity'; import { Places } from '../../../src/db/entities/Places.entity'; import { RoadtripVias } from '../../../src/db/entities/RoadtripVias.entity'; import { Users } from '../../../src/db/entities/Users.entity'; +import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service'; let svc: AssignmentsService; beforeAll(async () => { @@ -67,7 +68,7 @@ beforeAll(async () => { svc = new AssignmentsService( // Plan 4 Task 2 — AssignmentsService's own canAccessTrip delegate is now // TripsRepository.findAccessible, in the same constructor slot. - await createTestTripsRepo(testDb), + new TripAccessService(await createTestTripsRepo(testDb)), new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), realtime, new QueryHelpersService(await createTestTagsRepo(testDb), await createTestPlaceRatingsRepo(testDb), await createTestAssignmentParticipantsRepo(testDb)), diff --git a/server/tests/unit/nest/day-notes.service.test.ts b/server/tests/unit/nest/day-notes.service.test.ts index 57ace8500..89dfedb58 100644 --- a/server/tests/unit/nest/day-notes.service.test.ts +++ b/server/tests/unit/nest/day-notes.service.test.ts @@ -39,13 +39,14 @@ import { createTestUnitOfWork, createTestAppSettingsRepo, createTestDayNotesRepo import { sharedTestOrm } from '../../helpers/test-uow'; import { findRow } from '../../helpers/factories/rows'; import { DayNotes } from '../../../src/db/entities/DayNotes.entity'; +import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service'; let svc: DayNotesService; beforeAll(async () => { // Plan 4 Task 2 — DayNotesService's own canAccessTrip delegate is now // TripsRepository.findAccessible, in the same constructor slot. svc = new DayNotesService( - await createTestTripsRepo(testDb), + new TripAccessService(await createTestTripsRepo(testDb)), new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), new RealtimeService(), await createTestDayNotesRepo(testDb), @@ -279,7 +280,7 @@ describe('DayNotesService.canEdit', () => { it('DAYNOTE-SVC-090 asks for day_edit and flags a non-owner as shared', async () => { const checkPermission = vi.fn(() => true); const permissions = { checkPermission } as unknown as PermissionsService; - const withStub = new DayNotesService(await createTestTripsRepo(testDb), permissions, new RealtimeService(), await createTestDayNotesRepo(testDb), await createTestDaysRepo(testDb)); + const withStub = new DayNotesService(new TripAccessService(await createTestTripsRepo(testDb)), permissions, new RealtimeService(), await createTestDayNotesRepo(testDb), await createTestDaysRepo(testDb)); const trip = { id: 1, user_id: 1 } as never; expect(await withStub.canEdit(trip, { id: 1, role: 'user' } as never)).toBe(true); diff --git a/server/tests/unit/nest/day-removal.service.test.ts b/server/tests/unit/nest/day-removal.service.test.ts index 06467dd4f..ff6bde21b 100644 --- a/server/tests/unit/nest/day-removal.service.test.ts +++ b/server/tests/unit/nest/day-removal.service.test.ts @@ -62,6 +62,7 @@ import { ReservationEndpoints } from '../../../src/db/entities/ReservationEndpoi import { RoadtripDayBoundaries } from '../../../src/db/entities/RoadtripDayBoundaries.entity'; import { Trips } from '../../../src/db/entities/Trips.entity'; import { legacyBoundIntegerText } from '../../../src/nest/common/row-id'; +import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service'; let days: DaysService; let journey: JourneyDomainService; @@ -96,7 +97,7 @@ beforeAll(async () => { await createTestJourneyPhotosRepo(testDb), await createTestJourneyEntryPhotosRepo(testDb), await createTestPlacesRepo(testDb), ); assignments = new AssignmentsService( - await createTestTripsRepo(testDb), permissions, realtime, queryHelpers, journey, + new TripAccessService(await createTestTripsRepo(testDb)), permissions, realtime, queryHelpers, journey, await createTestUnitOfWork(testDb), await createTestDayAssignmentsRepo(testDb), await createTestAssignmentParticipantsRepo(testDb), @@ -108,7 +109,7 @@ beforeAll(async () => { ); accommodations = new AccommodationsService( permissions, realtime, assignments, await createTestUnitOfWork(testDb), - await createTestTripsRepo(testDb), + new TripAccessService(await createTestTripsRepo(testDb)), await createTestDayAccommodationsRepo(testDb), await createTestDayAssignmentsRepo(testDb), await createTestPlacesRepo(testDb), diff --git a/server/tests/unit/nest/files.service.test.ts b/server/tests/unit/nest/files.service.test.ts index f7121364e..055b65642 100644 --- a/server/tests/unit/nest/files.service.test.ts +++ b/server/tests/unit/nest/files.service.test.ts @@ -79,6 +79,7 @@ import { EphemeralTokenService } from '../../../src/nest/auth-core/ephemeral-tok import type { EntityManager } from '@mikro-orm/core'; import { Users } from '../../../src/db/entities/Users.entity'; import { UserSessions } from '../../../src/db/entities/UserSessions.entity'; +import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service'; const storageDelete = vi.fn(); const storageStub = { delete: storageDelete } as unknown as import('../../../src/nest/storage/storage.service').StorageService; @@ -114,7 +115,7 @@ beforeAll(async () => { tripFilesRepo = await createTestTripFilesRepo(testDb); fileLinksRepo = await createTestFileLinksRepo(testDb); svc = new FilesService( - await createTestTripsRepo(testDb), + new TripAccessService(await createTestTripsRepo(testDb)), permissionsStub, new RealtimeService(), new EphemeralTokenService(), diff --git a/server/tests/unit/nest/realtime.gateway.test.ts b/server/tests/unit/nest/realtime.gateway.test.ts index 52a0b686b..05844010f 100644 --- a/server/tests/unit/nest/realtime.gateway.test.ts +++ b/server/tests/unit/nest/realtime.gateway.test.ts @@ -43,6 +43,7 @@ import type { JourneyDomainService } from '../../../src/nest/journey/journey-dom import type { UsersRepository } from '../../../src/db/repositories/Users.repository'; import type { AppSettingsRepository } from '../../../src/db/repositories/AppSettings.repository'; import type { User } from '../../../src/types'; +import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service'; interface FakeSocket extends TrekWebSocket { sent: string[]; @@ -88,7 +89,7 @@ const canAccessJourney = vi.fn((journeyId: number) => (journeyId === 4 ? null : const journeys = { canAccessJourney } as unknown as JourneyDomainService; async function connect(url: string) { - const gw = new RealtimeGateway(db, tokens, journeys, users, appSettings); + const gw = new RealtimeGateway(new TripAccessService(db), tokens, journeys, users, appSettings); const ws = socket(); await gw.handleConnection(ws, { url } as never); return { gw, ws }; @@ -158,7 +159,7 @@ describe('RealtimeGateway handshake', () => { }); it('WSGW-007b: survives an upgrade request with no url, rather than throwing at it', async () => { - const gw = new RealtimeGateway(db, tokens, journeys, users, appSettings); + const gw = new RealtimeGateway(new TripAccessService(db), tokens, journeys, users, appSettings); const ws = socket(); await expect(gw.handleConnection(ws, {} as never)).resolves.not.toThrow(); expect(ws.closedWith).toEqual([4001, 'Authentication required']); @@ -198,7 +199,7 @@ describe('RealtimeGateway handshake', () => { const throwingUsers = { findForWsHandshake: () => { throw new Error('db exploded'); }, } as unknown as UsersRepository; - const gw = new RealtimeGateway(db, tokens, journeys, throwingUsers, appSettings); + const gw = new RealtimeGateway(new TripAccessService(db), tokens, journeys, throwingUsers, appSettings); const ws = socket(); await gw.handleConnection(ws, { url: '/ws?token=x' } as never); @@ -211,7 +212,7 @@ describe('RealtimeGateway handshake', () => { const throwingUsers = { findForWsHandshake: () => { throw 'db string'; }, // non-Error throw, pinning the ternary's String(err) fallback } as unknown as UsersRepository; - const gw = new RealtimeGateway(db, tokens, journeys, throwingUsers, appSettings); + const gw = new RealtimeGateway(new TripAccessService(db), tokens, journeys, throwingUsers, appSettings); const ws = socket(); await gw.handleConnection(ws, { url: '/ws?token=x' } as never); @@ -248,7 +249,7 @@ describe('RealtimeGateway heartbeat', () => { alive.isAlive = true; const stale = socket(); stale.isAlive = false; - const gw = new RealtimeGateway(db, tokens, journeys, users, appSettings); + const gw = new RealtimeGateway(new TripAccessService(db), tokens, journeys, users, appSettings); gw.afterInit({ clients: new Set([alive, stale]) } as never); vi.advanceTimersByTime(30_000); @@ -269,7 +270,7 @@ describe('RealtimeGateway heartbeat', () => { try { const ws = socket(); ws.isAlive = true; - const gw = new RealtimeGateway(db, tokens, journeys, users, appSettings); + const gw = new RealtimeGateway(new TripAccessService(db), tokens, journeys, users, appSettings); gw.afterInit({ clients: new Set([ws]) } as never); gw.onModuleDestroy(); @@ -378,7 +379,7 @@ describe('book rooms', () => { let nextJourney = 100; async function joined(journeyId: number) { - const gw = new RealtimeGateway(db, tokens, journeys, users, appSettings); + const gw = new RealtimeGateway(new TripAccessService(db), tokens, journeys, users, appSettings); const ws = socket(); registerSocket(ws, { id: 3, username: 'm' } as User); const reply = await gw.handleBookJoin({ journeyId }, ws); @@ -394,7 +395,7 @@ describe('book rooms', () => { /* Same shape as the trip room's refusal, and for the same reason. */ it('WSGW-BOOK-002: refuses a journey the user cannot see, and adds nobody', async () => { - const gw = new RealtimeGateway(db, tokens, journeys, users, appSettings); + const gw = new RealtimeGateway(new TripAccessService(db), tokens, journeys, users, appSettings); const ws = socket(); registerSocket(ws, { id: 3, username: 'm' } as User); @@ -445,7 +446,7 @@ describe('book pointers', () => { async function pair() { const journeyId = nextJourney++; - const gw = new RealtimeGateway(db, tokens, journeys, users, appSettings); + const gw = new RealtimeGateway(new TripAccessService(db), tokens, journeys, users, appSettings); const mine = socket(); const theirs = socket(); registerSocket(mine, { id: 3, username: 'm' } as User); @@ -510,7 +511,7 @@ describe('book messages that are refused', () => { let nextJourney = 300; it('WSGW-BOOK-006: a join without a journey id is ignored', async () => { - const gw = new RealtimeGateway(db, tokens, journeys, users, appSettings); + const gw = new RealtimeGateway(new TripAccessService(db), tokens, journeys, users, appSettings); const ws = socket(); registerSocket(ws, { id: 3, username: 'm' } as User); @@ -520,7 +521,7 @@ describe('book messages that are refused', () => { /* An unauthenticated socket never got as far as the registry. */ it('WSGW-BOOK-007: a join from a socket with no user is ignored', async () => { - const gw = new RealtimeGateway(db, tokens, journeys, users, appSettings); + const gw = new RealtimeGateway(new TripAccessService(db), tokens, journeys, users, appSettings); const j = nextJourney++; expect(await gw.handleBookJoin({ journeyId: j }, socket())).toBeUndefined(); @@ -528,7 +529,7 @@ describe('book messages that are refused', () => { }); it('WSGW-BOOK-008: a journey id that is not a number is refused, not joined', async () => { - const gw = new RealtimeGateway(db, tokens, journeys, users, appSettings); + const gw = new RealtimeGateway(new TripAccessService(db), tokens, journeys, users, appSettings); const ws = socket(); registerSocket(ws, { id: 3, username: 'm' } as User); @@ -537,7 +538,7 @@ describe('book messages that are refused', () => { }); it('WSGW-BOOK-009: a leave without a journey id is ignored', () => { - const gw = new RealtimeGateway(db, tokens, journeys, users, appSettings); + const gw = new RealtimeGateway(new TripAccessService(db), tokens, journeys, users, appSettings); const ws = socket(); registerSocket(ws, { id: 3, username: 'm' } as User); @@ -546,7 +547,7 @@ describe('book messages that are refused', () => { /* Leaving a room nobody is in must not create one on the way out. */ it('WSGW-BOOK-010: leaving a book that was never joined does nothing', () => { - const gw = new RealtimeGateway(db, tokens, journeys, users, appSettings); + const gw = new RealtimeGateway(new TripAccessService(db), tokens, journeys, users, appSettings); const ws = socket(); registerSocket(ws, { id: 3, username: 'm' } as User); const j = nextJourney++; @@ -556,7 +557,7 @@ describe('book messages that are refused', () => { }); it('WSGW-CUR-005: a pointer without a journey id reaches nobody', async () => { - const gw = new RealtimeGateway(db, tokens, journeys, users, appSettings); + const gw = new RealtimeGateway(new TripAccessService(db), tokens, journeys, users, appSettings); const mine = socket(); const theirs = socket(); registerSocket(mine, { id: 3, username: 'm' } as User); @@ -571,7 +572,7 @@ describe('book messages that are refused', () => { }); it('WSGW-CUR-006: a pointer from a socket with no user reaches nobody', async () => { - const gw = new RealtimeGateway(db, tokens, journeys, users, appSettings); + const gw = new RealtimeGateway(new TripAccessService(db), tokens, journeys, users, appSettings); const theirs = socket(); registerSocket(theirs, { id: 4, username: 'o' } as User); const j = nextJourney++; diff --git a/server/tests/unit/nest/receipt-scan.controller.test.ts b/server/tests/unit/nest/receipt-scan.controller.test.ts index 331c864ea..946b5dcab 100644 --- a/server/tests/unit/nest/receipt-scan.controller.test.ts +++ b/server/tests/unit/nest/receipt-scan.controller.test.ts @@ -7,6 +7,7 @@ import type { TripsRepository } from '../../../src/db/repositories/Trips.reposit import type { PermissionsService } from '../../../src/nest/permissions/permissions.service'; import type { AddonsService } from '../../../src/nest/addons/addons.service'; import type { User } from '../../../src/types'; +import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service'; const user = { id: 3, role: 'user' } as User; const photo = (name = 'r.jpg') => ({ originalname: name, buffer: Buffer.from('x') } as Express.Multer.File); @@ -17,7 +18,7 @@ function make(over: { readsImages?: boolean; trip?: unknown; addon?: boolean; al const c = new ReceiptScanController( { startReceipt } as unknown as ImportJobsService, { readsImages: vi.fn(async () => over.readsImages ?? true) } as unknown as LlmParseService, - { findAccessible: vi.fn(async () => ('trip' in over ? over.trip : { user_id: 5 })) } as unknown as TripsRepository, + new TripAccessService({ findAccessible: vi.fn(async () => ('trip' in over ? over.trip : { user_id: 5 })) } as unknown as TripsRepository), { checkPermission } as unknown as PermissionsService, { isAddonEnabled: vi.fn(async () => over.addon ?? true) } as unknown as AddonsService, ); diff --git a/server/tests/unit/nest/roadtrip-plan.service.test.ts b/server/tests/unit/nest/roadtrip-plan.service.test.ts index a6d756c68..a76ca7d29 100644 --- a/server/tests/unit/nest/roadtrip-plan.service.test.ts +++ b/server/tests/unit/nest/roadtrip-plan.service.test.ts @@ -31,6 +31,7 @@ import { bookendAssignmentId, type RoadtripStop } from '@trek/shared/roadtrip'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { legacyBoundIntegerText } from '../../../src/nest/common/row-id'; +import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service'; /** Every leg an hour and 60 km, whatever it joins. */ function hourlyRouter() { @@ -61,7 +62,7 @@ function hourlyRouter() { /** The repositories the service reads through, all on the worker's one test handle. */ async function planRepos() { return [ - await createTestTripsRepo(db), + new TripAccessService(await createTestTripsRepo(db)), await createTestDaysRepo(db), await createTestDayAssignmentsRepo(db), await createTestDayAccommodationsRepo(db), diff --git a/server/tests/unit/nest/todo.service.test.ts b/server/tests/unit/nest/todo.service.test.ts index 945e21bf0..8ee70233d 100644 --- a/server/tests/unit/nest/todo.service.test.ts +++ b/server/tests/unit/nest/todo.service.test.ts @@ -37,6 +37,7 @@ import { TodoItems } from '../../../src/db/entities/TodoItems.entity'; import { TodoCategoryAssignees } from '../../../src/db/entities/TodoCategoryAssignees.entity'; import { Users } from '../../../src/db/entities/Users.entity'; import { createTestTodoItemsRepo, createTestTodoCategoryAssigneesRepo } from '../../helpers/todo-repos'; +import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service'; let svc: TodoService; let todoItemsRepoDirect: Awaited>; @@ -48,7 +49,7 @@ beforeAll(async () => { todoItemsRepoDirect, await createTestTodoCategoryAssigneesRepo(testDb), // Plan 4 Task 2 — TodoService's own canAccessTrip delegate is now // TripsRepository.findAccessible, a new trailing constructor param. - await createTestTripsRepo(testDb), + new TripAccessService(await createTestTripsRepo(testDb)), // Plan 4 Task 3 — DatabaseService.rosterUserIds inlined onto // TripMembersRepository.rosterUserIds directly. await createTestTripMembersRepo(testDb), @@ -404,7 +405,7 @@ describe('TodoService.canEdit', () => { const withStub = new TodoService( permissions, new RealtimeService(), await createTestUnitOfWork(testDb), await createTestTodoItemsRepo(testDb), await createTestTodoCategoryAssigneesRepo(testDb), - await createTestTripsRepo(testDb), await createTestTripMembersRepo(testDb), + new TripAccessService(await createTestTripsRepo(testDb)), await createTestTripMembersRepo(testDb), ); const trip = { id: 1, user_id: 1 } as never; diff --git a/server/tests/unit/nest/trip-invite.service.test.ts b/server/tests/unit/nest/trip-invite.service.test.ts index 303c0b422..eb0bfa2a5 100644 --- a/server/tests/unit/nest/trip-invite.service.test.ts +++ b/server/tests/unit/nest/trip-invite.service.test.ts @@ -36,6 +36,7 @@ import { createTestUnitOfWork, createTestAppSettingsRepo, createTestTripsRepo, c import { sharedTestOrm } from '../../helpers/test-uow'; import { countRows, updateRows } from '../../helpers/factories/rows'; import { TripInviteTokens } from '../../../src/db/entities/TripInviteTokens.entity'; +import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service'; let svc: TripInviteService; beforeAll(async () => { @@ -43,7 +44,7 @@ beforeAll(async () => { svc = new TripInviteService( // Plan 4 Task 2 — TripInviteService's own canAccessTrip delegate is now // TripsRepository.findAccessible, in the same constructor slot. - await createTestTripsRepo(testDb), + new TripAccessService(await createTestTripsRepo(testDb)), new PermissionsService(await createTestAppSettingsRepo(testDb), uow), new TripMembershipService(await createTestTripsRepo(testDb), await createTestTripMembersRepo(testDb)), uow, diff --git a/server/tests/unit/nest/trip-read-model.service.test.ts b/server/tests/unit/nest/trip-read-model.service.test.ts index 40d578070..dd47f0ada 100644 --- a/server/tests/unit/nest/trip-read-model.service.test.ts +++ b/server/tests/unit/nest/trip-read-model.service.test.ts @@ -108,6 +108,7 @@ import { createTestCollectionPlacesRepo } from '../../helpers/test-uow'; import { noGoogleQuota } from '../../helpers/google-quota'; import { createTestBudgetSettlementsRepo } from '../../helpers/budget-repos'; import { MaintenanceRepository } from '../../../src/db/repositories/MaintenanceRepository'; +import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service'; // Real sibling services over the same in-memory DB — the aggregation runs the // actual SQL of every domain it fans out to, so a shape change downstream shows @@ -200,7 +201,7 @@ const buildReadModel = async (tripsRepo: TripsRepository, roster: TripMembersSer await createTestTodoItemsRepo(testDb), await createTestTodoCategoryAssigneesRepo(testDb), // Plan 4 Task 2 — TodoService's own canAccessTrip delegate is now // TripsRepository.findAccessible, a new trailing constructor param. - await createTestTripsRepo(testDb), + new TripAccessService(await createTestTripsRepo(testDb)), // Plan 4 Task 3 — DatabaseService.rosterUserIds inlined onto // TripMembersRepository.rosterUserIds directly. await createTestTripMembersRepo(testDb), @@ -208,7 +209,7 @@ const buildReadModel = async (tripsRepo: TripsRepository, roster: TripMembersSer new FilesService( // Plan 4 Task 2 — FilesService's own canAccessTrip delegate is now // TripsRepository.findAccessible, in the same constructor slot. - await createTestTripsRepo(testDb), + new TripAccessService(await createTestTripsRepo(testDb)), new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), new RealtimeService(), new EphemeralTokenService(), diff --git a/server/tests/unit/nest/trips.service.test.ts b/server/tests/unit/nest/trips.service.test.ts index 0e8cb68e3..5788d2ddb 100644 --- a/server/tests/unit/nest/trips.service.test.ts +++ b/server/tests/unit/nest/trips.service.test.ts @@ -153,6 +153,7 @@ import { TripMembers } from '../../../src/db/entities/TripMembers.entity'; import { Trips } from '../../../src/db/entities/Trips.entity'; import { Users } from '../../../src/db/entities/Users.entity'; import { legacyBoundIntegerText } from '../../../src/nest/common/row-id'; +import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service'; const orm = () => sharedTestOrm(testDb); @@ -322,13 +323,13 @@ beforeAll(async () => { new TodoService( new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), new RealtimeService(), await createTestUnitOfWork(testDb), await createTestTodoItemsRepo(testDb), await createTestTodoCategoryAssigneesRepo(testDb), - await createTestTripsRepo(testDb), + new TripAccessService(await createTestTripsRepo(testDb)), await createTestTripMembersRepo(testDb), ), new FilesService( // Plan 4 Task 2 — FilesService's own canAccessTrip delegate is now // TripsRepository.findAccessible, in the same constructor slot. - await createTestTripsRepo(testDb), + new TripAccessService(await createTestTripsRepo(testDb)), new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), new RealtimeService(), new EphemeralTokenService(), diff --git a/server/tests/unit/plugins/addon-gates.rpc.test.ts b/server/tests/unit/plugins/addon-gates.rpc.test.ts index afe50f2d2..e3fb25073 100644 --- a/server/tests/unit/plugins/addon-gates.rpc.test.ts +++ b/server/tests/unit/plugins/addon-gates.rpc.test.ts @@ -26,6 +26,7 @@ import type { AddonsService } from '../../../src/nest/addons/addons.service'; import type { UsersRepository } from '../../../src/db/repositories/Users.repository'; import type { RpcRequest, RpcError } from '../../../src/nest/plugins/protocol/envelope'; import { makeDeps } from '../../helpers/rpc-host-deps'; +import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service'; const req = (method: string, params: Record = {}): RpcRequest => ({ k: 'req', id: 'x', method, params }); @@ -81,7 +82,7 @@ function build(addonOn: boolean) { // SV8 — Plan 3i: DemoService.isDemoUserId, the shared demo-gate primitive. { isDemoUserId: async () => false } as never), new CollectionsRpc(spyService(calls, 'collections'), guards), - new CostsRpc(spyService(calls, 'budget'), db, { broadcast: vi.fn() } as never, guards, spyService(calls, 'membership')), + new CostsRpc(spyService(calls, 'budget'), new TripAccessService(db), { broadcast: vi.fn() } as never, guards, spyService(calls, 'membership')), ]); const granted = new Set([ 'db:read:collab', 'db:write:collab', 'db:read:journal', 'db:write:journal', diff --git a/server/tests/unit/plugins/day-schedule.controller.test.ts b/server/tests/unit/plugins/day-schedule.controller.test.ts index 2e06a7fe6..507c1d15d 100644 --- a/server/tests/unit/plugins/day-schedule.controller.test.ts +++ b/server/tests/unit/plugins/day-schedule.controller.test.ts @@ -22,6 +22,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled })); import { DayScheduleController } from '../../../src/nest/plugins/contributions/day-schedule.controller'; import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service'; import type { DaysRepository } from '../../../src/db/repositories/Days.repository'; +import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any; @@ -32,7 +33,7 @@ function controller(invoke: (id: string) => unknown, providers = ['p1']) { } as unknown as PluginHooks; // CT1 (Plan 3j Task 5) — the day-id-set read is now DaysRepository.listIdsByTrip. const days = { listIdsByTrip: vi.fn(async () => tripDays.value.map((d) => d.id)) } as unknown as DaysRepository; - return { c: new DayScheduleController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository, days), runtime }; + return { c: new DayScheduleController(runtime, new TripAccessService({ findAccessible: canAccessTrip } as unknown as TripsRepository), days), runtime }; } const item = (over: Record = {}) => ({ id: 's1', dayId: 10, label: 'Charging', ...over }); diff --git a/server/tests/unit/plugins/day-tints.controller.test.ts b/server/tests/unit/plugins/day-tints.controller.test.ts index b03acc37c..2bacd85c9 100644 --- a/server/tests/unit/plugins/day-tints.controller.test.ts +++ b/server/tests/unit/plugins/day-tints.controller.test.ts @@ -22,6 +22,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled })); import { DayTintsController } from '../../../src/nest/plugins/contributions/day-tints.controller'; import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service'; import type { DaysRepository } from '../../../src/db/repositories/Days.repository'; +import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any; @@ -32,7 +33,7 @@ function controller(invoke: (id: string) => unknown, providers = ['p1']) { } as unknown as PluginHooks; // CT2 (Plan 3j Task 5) — the day-id-set read is now DaysRepository.listIdsByTrip. const days = { listIdsByTrip: vi.fn(async () => tripDays.value.map((d) => d.id)) } as unknown as DaysRepository; - return { c: new DayTintsController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository, days), runtime }; + return { c: new DayTintsController(runtime, new TripAccessService({ findAccessible: canAccessTrip } as unknown as TripsRepository), days), runtime }; } const tint = (over: Record = {}) => ({ dayId: 10, tone: 'success', ...over }); diff --git a/server/tests/unit/plugins/map-layers.controller.test.ts b/server/tests/unit/plugins/map-layers.controller.test.ts index 5c0ae045c..2c3d9e533 100644 --- a/server/tests/unit/plugins/map-layers.controller.test.ts +++ b/server/tests/unit/plugins/map-layers.controller.test.ts @@ -17,6 +17,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled })); import { MapLayersController } from '../../../src/nest/plugins/contributions/map-layers.controller'; import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service'; import type { TripsRepository } from '../../../src/db/repositories/Trips.repository'; +import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any; @@ -25,7 +26,7 @@ function controller(invoke: (id: string) => unknown, providers = ['p1']) { providersOf: vi.fn(() => providers), mapLayers: vi.fn(async (id: string) => invoke(id)), } as unknown as PluginHooks; - return { c: new MapLayersController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository), runtime }; + return { c: new MapLayersController(runtime, new TripAccessService({ findAccessible: canAccessTrip } as unknown as TripsRepository)), runtime }; } const line = (n = 3, over: Record = {}) => ({ type: 'polyline', diff --git a/server/tests/unit/plugins/map-markers.controller.test.ts b/server/tests/unit/plugins/map-markers.controller.test.ts index 3cf0f8a48..b8455679f 100644 --- a/server/tests/unit/plugins/map-markers.controller.test.ts +++ b/server/tests/unit/plugins/map-markers.controller.test.ts @@ -16,6 +16,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled })); import { MapMarkersController } from '../../../src/nest/plugins/contributions/map-markers.controller'; import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service'; import type { TripsRepository } from '../../../src/db/repositories/Trips.repository'; +import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any; @@ -24,7 +25,7 @@ function controller(invoke: (id: string) => unknown, providers = ['p1']) { providersOf: vi.fn(() => providers), mapMarkers: vi.fn(async (id: string) => invoke(id)), } as unknown as PluginHooks; - return { c: new MapMarkersController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository), runtime }; + return { c: new MapMarkersController(runtime, new TripAccessService({ findAccessible: canAccessTrip } as unknown as TripsRepository)), runtime }; } const mk = (over: Record = {}) => ({ id: 'm1', lat: 48.85, lng: 2.35, ...over }); diff --git a/server/tests/unit/plugins/pdf-sections.controller.test.ts b/server/tests/unit/plugins/pdf-sections.controller.test.ts index 65b7ed4f3..bd0004864 100644 --- a/server/tests/unit/plugins/pdf-sections.controller.test.ts +++ b/server/tests/unit/plugins/pdf-sections.controller.test.ts @@ -17,6 +17,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled })); import { PdfSectionsController } from '../../../src/nest/plugins/contributions/pdf-sections.controller'; import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service'; import type { TripsRepository } from '../../../src/db/repositories/Trips.repository'; +import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any; @@ -25,7 +26,7 @@ function controller(invoke: (id: string) => unknown, providers = ['p1']) { providersOf: vi.fn(() => providers), pdfSections: vi.fn(async (id: string) => invoke(id)), } as unknown as PluginHooks; - return { c: new PdfSectionsController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository), runtime }; + return { c: new PdfSectionsController(runtime, new TripAccessService({ findAccessible: canAccessTrip } as unknown as TripsRepository)), runtime }; } const sec = (over: Record = {}) => ({ title: 'Weather', ...over }); diff --git a/server/tests/unit/plugins/place-details.controller.test.ts b/server/tests/unit/plugins/place-details.controller.test.ts index 9b312df76..7da1743c8 100644 --- a/server/tests/unit/plugins/place-details.controller.test.ts +++ b/server/tests/unit/plugins/place-details.controller.test.ts @@ -15,6 +15,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled })); import { PlaceDetailsController } from '../../../src/nest/plugins/contributions/place-details.controller'; import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service'; import type { PlacesRepository } from '../../../src/db/repositories/Places.repository'; +import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any; @@ -26,7 +27,7 @@ function controller(over: Partial = {}) { } as unknown as PluginHooks; // CT7 (Plan 3j Task 5) — the place's owning trip id is now Places.repository.ts#findTripId. const places = { findTripId: vi.fn(async (placeId: number) => placeTrip(placeId)?.trip_id) } as unknown as PlacesRepository; - return { c: new PlaceDetailsController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository, places), runtime }; + return { c: new PlaceDetailsController(runtime, new TripAccessService({ findAccessible: canAccessTrip } as unknown as TripsRepository), places), runtime }; } describe('PlaceDetailsController', () => { diff --git a/server/tests/unit/plugins/plugin-routes.controller.test.ts b/server/tests/unit/plugins/plugin-routes.controller.test.ts index b84d1c260..81b68561f 100644 --- a/server/tests/unit/plugins/plugin-routes.controller.test.ts +++ b/server/tests/unit/plugins/plugin-routes.controller.test.ts @@ -22,6 +22,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled })); import { PluginRoutesController } from '../../../src/nest/plugins/contributions/plugin-routes.controller'; import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service'; import type { PluginsRepository } from '../../../src/db/repositories/Plugins.repository'; +import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any; @@ -36,7 +37,7 @@ function controller(invoke: () => unknown, providers = ['ev-plug']) { const plugins = { findCapabilities: vi.fn(async () => (capabilitiesRow.value === undefined ? null : capabilitiesRow.value)), } as unknown as PluginsRepository; - return { c: new PluginRoutesController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository, plugins), runtime }; + return { c: new PluginRoutesController(runtime, new TripAccessService({ findAccessible: canAccessTrip } as unknown as TripsRepository), plugins), runtime }; } const wp = (n = 3) => Array.from({ length: n }, (_, i) => ({ lat: 48 + i * 0.1, lng: 2 + i * 0.1 })); const goodRoute = (n = 3, over: Record = {}) => ({ diff --git a/server/tests/unit/plugins/trip-card-contributions.controller.test.ts b/server/tests/unit/plugins/trip-card-contributions.controller.test.ts index d09696b00..b1f8a8735 100644 --- a/server/tests/unit/plugins/trip-card-contributions.controller.test.ts +++ b/server/tests/unit/plugins/trip-card-contributions.controller.test.ts @@ -18,6 +18,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled })); import { TripCardContributionsController } from '../../../src/nest/plugins/contributions/trip-card-contributions.controller'; import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service'; import type { TripsRepository } from '../../../src/db/repositories/Trips.repository'; +import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any; @@ -26,7 +27,7 @@ function controller(invoke: (id: string, args: unknown[]) => unknown, providers providersOf: vi.fn(() => providers), tripCards: vi.fn(async (id: string, _h: string, _fn: string, args: unknown[]) => invoke(id, args)), } as unknown as PluginHooks; - return { c: new TripCardContributionsController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository), runtime }; + return { c: new TripCardContributionsController(runtime, new TripAccessService({ findAccessible: canAccessTrip } as unknown as TripsRepository)), runtime }; } const badge = (over: Record = {}) => ({ tripId: 1, id: 'b1', label: 'Visa', ...over }); diff --git a/server/tests/unit/plugins/trip-warnings.controller.test.ts b/server/tests/unit/plugins/trip-warnings.controller.test.ts index 7422509eb..8579508ff 100644 --- a/server/tests/unit/plugins/trip-warnings.controller.test.ts +++ b/server/tests/unit/plugins/trip-warnings.controller.test.ts @@ -10,6 +10,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled })); import { TripWarningsController } from '../../../src/nest/plugins/contributions/trip-warnings.controller'; import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service'; import type { TripsRepository } from '../../../src/db/repositories/Trips.repository'; +import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any; @@ -20,7 +21,7 @@ function controller(over: Partial = {}) { id === 'p2' ? [{ level: 'error', message: 'Day 3 is overpacked', dayId: 3 }] : [{ level: 'warning', message: 'Museum closed Mon', placeId: 7 }]), ...over, } as unknown as PluginHooks; - return { c: new TripWarningsController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository), runtime }; + return { c: new TripWarningsController(runtime, new TripAccessService({ findAccessible: canAccessTrip } as unknown as TripsRepository)), runtime }; } describe('TripWarningsController', () => { diff --git a/server/tests/unit/plugins/view-contributions.controller.test.ts b/server/tests/unit/plugins/view-contributions.controller.test.ts index 15a7b2c86..81844976d 100644 --- a/server/tests/unit/plugins/view-contributions.controller.test.ts +++ b/server/tests/unit/plugins/view-contributions.controller.test.ts @@ -17,6 +17,7 @@ vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled })); import { ViewContributionsController } from '../../../src/nest/plugins/contributions/view-contributions.controller'; import type { PluginHooks } from '../../../src/nest/plugins/plugin-hooks.service'; import type { TripsRepository } from '../../../src/db/repositories/Trips.repository'; +import { TripAccessService } from '../../../src/nest/trip-membership/trip-access.service'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any; @@ -25,7 +26,7 @@ function controller(invoke: (id: string) => unknown, providers = ['p1']) { providersOf: vi.fn(() => providers), tableContributions: vi.fn(async (id: string) => invoke(id)), } as unknown as PluginHooks; - return { c: new ViewContributionsController(runtime, { findAccessible: canAccessTrip } as unknown as TripsRepository), runtime }; + return { c: new ViewContributionsController(runtime, new TripAccessService({ findAccessible: canAccessTrip } as unknown as TripsRepository)), runtime }; } const col = (over: Record = {}) => ({ kind: 'column', entityId: 1, id: 'c1', label: 'X', ...over }); const act = (over: Record = {}) => ({ kind: 'action', entityId: 1, id: 'a1', label: 'Go', target: { kind: 'frame', sub: '/ui' }, ...over });