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.
This commit is contained in:
Maurice
2026-10-10 13:11:10 +02:00
parent 734cbc2a94
commit fe35e26313
70 changed files with 219 additions and 195 deletions
@@ -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]),
],
@@ -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,
@@ -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,
@@ -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,
+3 -5
View File
@@ -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,
@@ -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],
@@ -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,
+2 -1
View File
@@ -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],
+3 -4
View File
@@ -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,
+2 -4
View File
@@ -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,
+2 -1
View File
@@ -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
@@ -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<number>, 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,
) {}
@@ -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<number>, 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,
) {}
@@ -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')
@@ -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')
@@ -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')
@@ -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,
) {}
@@ -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,
@@ -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,
) {}
@@ -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<number>): TripCa
export class TripCardContributionsController {
constructor(
private readonly hooks: PluginHooks,
@InjectRepository(Trips) private readonly trips: TripsRepository,
private readonly trips: TripAccessService,
) {}
@Get()
@@ -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')
@@ -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({
@@ -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')
@@ -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.
+3 -4
View File
@@ -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
@@ -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,
) {}
@@ -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 {}
@@ -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,
) {}
@@ -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],
})
+2 -4
View File
@@ -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) })
@@ -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,
@@ -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<GoogleRoutePreview> {
@@ -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.',
@@ -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,
@@ -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,
) {}
+2 -4
View File
@@ -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,
) {}
+2 -1
View File
@@ -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,
+2 -1
View File
@@ -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],
+4 -4
View File
@@ -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,
+3 -5
View File
@@ -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
+2 -1
View File
@@ -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],
@@ -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,
@@ -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<TripAccess | undefined> {
return this.trips.findAccessible(tripId, userId);
}
}
@@ -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 {}