diff --git a/server/scripts/import-boundaries-baseline.json b/server/scripts/import-boundaries-baseline.json index d9d0ba6e6..a2d580d42 100644 --- a/server/scripts/import-boundaries-baseline.json +++ b/server/scripts/import-boundaries-baseline.json @@ -2,69 +2,44 @@ "fileCycles": [], "domainCycles": [ "accommodations -> assignments", - "accommodations -> mcp-shared", "accommodations -> places", - "accommodations -> realtime", "addons -> memories", "addons -> transit", "assignments -> days", "assignments -> journey", - "assignments -> mcp-shared", - "assignments -> realtime", "atlas -> addons", "auth -> plugins", "budget -> addons", - "budget -> mcp-shared", - "budget -> realtime", "days -> accommodations", "days -> assignments", - "days -> mcp-shared", "days -> places", - "days -> realtime", - "files -> mcp-shared", - "files -> realtime", "geo -> maps", "integrations -> addons", "integrations -> assignments", "integrations -> atlas", "integrations -> journey", - "integrations -> mcp-shared", "integrations -> places", - "integrations -> realtime", "integrations -> reservations", "journey -> addons", "journey -> atlas", - "journey -> files", "journey -> memories", "journey -> photos", - "journey -> realtime", "maps -> geo", - "mcp-shared -> realtime", "memories -> addons", - "memories -> notifications", "memories -> photos", - "memories -> realtime", - "notifications -> realtime", "photos -> memories", "places -> accommodations", "places -> assignments", "places -> atlas", "places -> journey", - "places -> mcp-shared", - "places -> realtime", "plugins -> trips", "plugins -> vacay", - "realtime -> journey", "reservations -> accommodations", "reservations -> assignments", "reservations -> budget", "reservations -> days", "reservations -> integrations", - "reservations -> mcp-shared", - "reservations -> notifications", - "reservations -> realtime", "transit -> days", - "transit -> mcp-shared", "transit -> reservations", "trip-members -> auth", "trip-members -> trips", diff --git a/server/src/nest/app.module.ts b/server/src/nest/app.module.ts index de24eba89..cd23980d2 100644 --- a/server/src/nest/app.module.ts +++ b/server/src/nest/app.module.ts @@ -64,6 +64,7 @@ import { SpaFallbackFilter } from './platform/spa-fallback.filter'; import { PluginsModule } from './plugins/plugins.module'; import { PublicApiModule } from './public-api/public-api.module'; import { RealtimeGatewayModule } from './realtime/realtime-gateway.module'; +import { JourneyAccessModule } from './journey/journey-access.module'; import { RealtimeModule } from './realtime/realtime.module'; import { ReceiptScanModule } from './receipt-scan/receipt-scan.module'; import { ReservationImportModule } from './reservation-import/reservation-import.module'; @@ -100,6 +101,7 @@ import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'; DemoModule, RealtimeModule, RealtimeGatewayModule, + JourneyAccessModule, SchedulingModule, McpModule.forRoot({ accessPolicy: trekMcpAccessPolicy, diff --git a/server/src/nest/journey/journey-access.module.ts b/server/src/nest/journey/journey-access.module.ts new file mode 100644 index 000000000..ca9655f3e --- /dev/null +++ b/server/src/nest/journey/journey-access.module.ts @@ -0,0 +1,17 @@ +import { Global, Module } from '@nestjs/common'; +import { JOURNEY_ACCESS } from '../realtime/journey-access.types'; +import { JourneyDomainModule } from './journey-domain.module'; +import { JourneyDomainService } from './journey-domain.service'; + +/** + * Binds the realtime gateway's JOURNEY_ACCESS port to JourneyDomainService. + * Global so RealtimeGatewayModule resolves it without importing the journey + * domain; AppModule imports it next to the gateway module. + */ +@Global() +@Module({ + imports: [JourneyDomainModule], + providers: [{ provide: JOURNEY_ACCESS, useExisting: JourneyDomainService }], + exports: [JOURNEY_ACCESS], +}) +export class JourneyAccessModule {} diff --git a/server/src/nest/realtime/journey-access.types.ts b/server/src/nest/realtime/journey-access.types.ts new file mode 100644 index 000000000..10bcac304 --- /dev/null +++ b/server/src/nest/realtime/journey-access.types.ts @@ -0,0 +1,15 @@ +/** + * What the gateway asks before it lets a socket into a journey's book room. + * + * A port rather than JourneyDomainService itself: realtime is imported by + * nearly every domain for the broadcast facade, so depending on journey from + * here put realtime, journey and everything between them on one import cycle. + * The journey domain binds the token (JourneyAccessModule), and the answer is + * still the one the REST routes get. + */ +export const JOURNEY_ACCESS = Symbol('JOURNEY_ACCESS'); + +export interface JourneyAccess { + /** Truthy when the user may open the journey. */ + canAccessJourney(journeyId: number, userId: number): Promise; +} diff --git a/server/src/nest/realtime/realtime-gateway.module.ts b/server/src/nest/realtime/realtime-gateway.module.ts index 9681b7511..6b860cd2b 100644 --- a/server/src/nest/realtime/realtime-gateway.module.ts +++ b/server/src/nest/realtime/realtime-gateway.module.ts @@ -2,7 +2,6 @@ import { Module, type OnModuleDestroy } from '@nestjs/common'; import { MikroOrmModule } from '@mikro-orm/nestjs'; import { RealtimeGateway } from './realtime.gateway'; import { EphemeralTokenModule } from '../auth-core/ephemeral-token.module'; -import { JourneyDomainModule } from '../journey/journey-domain.module'; import { AppSettings } from '../../db/entities/AppSettings.entity'; import { Users } from '../../db/entities/Users.entity'; import { Trips } from '../../db/entities/Trips.entity'; @@ -23,13 +22,13 @@ import { processRooms, RoomRegistry, roomsSlot } from './ws-state'; * TrekWsAdapter already registered. */ @Module({ - // JourneyDomainModule for the book rooms: who may open a journey is asked - // of the same service the REST routes ask. Users/AppSettings: Plan 4 Task + // The book rooms ask JOURNEY_ACCESS, bound globally by JourneyAccessModule + // to the same service the REST routes ask. Users/AppSettings: Plan 4 Task // 1 — the handshake's password-version and require_mfa reads, moved off // DatabaseService onto UsersRepository/AppSettingsRepository. Trips: Plan 4 // Task 2 — handleJoin's own canAccessTrip delegate, now TripsRepository // directly. - imports: [EphemeralTokenModule, JourneyDomainModule, MikroOrmModule.forFeature([Users, AppSettings, Trips])], + imports: [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 aac4bec3f..2f561f277 100644 --- a/server/src/nest/realtime/realtime.gateway.ts +++ b/server/src/nest/realtime/realtime.gateway.ts @@ -1,4 +1,4 @@ -import { Injectable, OnModuleDestroy } from '@nestjs/common'; +import { Inject, Injectable, OnModuleDestroy } from '@nestjs/common'; import { ConnectedSocket, MessageBody, @@ -30,7 +30,7 @@ import { userOf, type TrekWebSocket, } from './ws-state'; -import { JourneyDomainService } from '../journey/journey-domain.service'; +import { JOURNEY_ACCESS, type JourneyAccess } from './journey-access.types'; import { readAppSetting } from '../common/app-settings.registry'; import { runningVersion } from '../../app-config'; @@ -71,9 +71,10 @@ export class RealtimeGateway /* * For the book rooms, and injected rather than reimplemented: who may open * a journey is one question with one answer, and a second copy of it here - * is a second thing to keep in step with the REST routes. + * is a second thing to keep in step with the REST routes. Behind a port + * (journey-access.types.ts) so realtime does not import the journey domain. */ - private readonly journeys: JourneyDomainService, + @Inject(JOURNEY_ACCESS) private readonly journeys: JourneyAccess, @InjectRepository(Users) private readonly users: UsersRepository, @InjectRepository(AppSettings) private readonly appSettings: AppSettingsRepository, // Room membership, behind its port. The container's provider, which