Files
TREK/server/src/nest/app.module.ts
T
Maurice ffb73fac20 Turn protection from opt-in into default-deny
Nothing in src/nest/** was an APP_GUARD. Every protected route said so
itself, 62 times, which means a forgotten @UseGuards was a silent bypass
rather than an error. GlobalAuthGuard inverts that: a route is authenticated
unless it carries @Public() or @OptionalAuth(), or declares its own guard
chain.

Reading the declared chain is what makes this behaviour-neutral. Nest runs
global guards before route guards, so without that check the global guard
would 401 an anonymous request to an addon-gated controller before its
AddonGuard could answer 404 - inverting an order four e2e suites pin, and
leaking which addons are installed. A controller that declared a chain keeps
deciding for itself.

It is a new class rather than JwtAuthGuard gaining a Reflector: that guard is
constructed positionally in its own test with a context stub that has no
getHandler, and the two want opposite behaviour anyway.

@Public() takes a required reason, and the reasons are the diff worth
reading: the invite token IS the credential, help assets are loaded by <img>
which cannot send one, logout has to work with an expired session or nobody
can sign out.

The real deliverable is the inventory. validateRouteGuards runs in buildApp
next to the body-contract gate and refuses to start when a route answers
anonymously without being on a reviewed list of 25 - or when a listed one no
longer does. An allow list nobody prunes stops being a list.

Default-deny found two controllers on its own: SharedController and
OidcController both sit in a file next to a guarded controller, so a
file-level search for unguarded ones missed them. 37 tests went red and named
them. That is the mechanism working before the change shipped, not after.
2026-08-10 13:38:26 +02:00

91 lines
5.9 KiB
TypeScript

import { Module } from '@nestjs/common';
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
import { ZodValidationPipe } from './common/zod-validation.pipe';
import { AppConfigModule } from './app-config/app-config.module';
import { DatabaseModule } from './database/database.module';
import { RealtimeModule } from './realtime/realtime.module';
import { HealthModule } from './health/health.module';
import { GlobalAuthGuard } from './auth/global-auth.guard';
import { WeatherModule } from './weather/weather.module';
import { HelpModule } from './help/help.module';
import { AirportsModule } from './airports/airports.module';
import { ConfigModule } from './config/config.module';
import { SystemNoticesModule } from './system-notices/system-notices.module';
import { MapsModule } from './maps/maps.module';
import { PlaceEnrichmentModule } from './place-enrichment/place-enrichment.module';
import { CategoriesModule } from './categories/categories.module';
import { TagsModule } from './tags/tags.module';
import { NotificationsModule } from './notifications/notifications.module';
import { AtlasModule } from './atlas/atlas.module';
import { VacayModule } from './vacay/vacay.module';
import { PackingModule } from './packing/packing.module';
import { BudgetModule } from './budget/budget.module';
import { ReservationsModule } from './reservations/reservations.module';
import { DaysModule } from './days/days.module';
import { DayNotesModule } from './day-notes/day-notes.module';
import { AccommodationsModule } from './accommodations/accommodations.module';
import { AssignmentsModule } from './assignments/assignments.module';
import { PlacesModule } from './places/places.module';
import { TripsModule } from './trips/trips.module';
import { TodoModule } from './todo/todo.module';
import { CollabModule } from './collab/collab.module';
import { FilesModule } from './files/files.module';
import { PhotosModule } from './photos/photos.module';
import { MemoriesModule } from './memories/memories.module';
import { AirtrailModule } from './integrations/airtrail.module';
import { JourneyModule } from './journey/journey.module';
import { CollectionsModule } from './collections/collections.module';
import { ShareModule } from './share/share.module';
import { TripInviteModule } from './trip-invite/trip-invite.module';
import { TransitModule } from './transit/transit.module';
import { FeedsModule } from './feeds/feeds.module';
import { SettingsModule } from './settings/settings.module';
import { BackupModule } from './backup/backup.module';
import { BookingImportModule } from './booking-import/booking-import.module';
import { LlmParseModule } from './llm-parse/llm-parse.module';
import { AuthModule } from './auth/auth.module';
import { OidcModule } from './oidc/oidc.module';
import { OauthModule } from './oauth/oauth.module';
import { AdminModule } from './admin/admin.module';
import { AddonsModule } from './addons/addons.module';
import { AuditModule } from './audit/audit.module';
import { PermissionsModule } from './permissions/permissions.module';
import { PluginsModule } from './plugins/plugins.module';
import { McpModule } from '@trek/nest-mcp';
import { trekMcpAccessPolicy, trekMcpValidateAccess } from '../mcp/nest-mcp-policy';
import { TrekExceptionFilter } from './common/trek-exception.filter';
import { SpaFallbackFilter } from './platform/spa-fallback.filter';
import { IdempotencyInterceptor } from './common/idempotency.interceptor';
/**
* Root NestJS module for the incremental migration. Domain modules
* (weather, notifications, integrations, ...) get registered here as they are
* migrated.
*/
@Module({
imports: [AppConfigModule, DatabaseModule, RealtimeModule, McpModule.forRoot({ accessPolicy: trekMcpAccessPolicy, validateAccess: trekMcpValidateAccess }), HealthModule, WeatherModule, HelpModule, AirportsModule, ConfigModule, SystemNoticesModule, MapsModule, PlaceEnrichmentModule, CategoriesModule, TagsModule, NotificationsModule, AtlasModule, VacayModule, PackingModule, TodoModule, BudgetModule, ReservationsModule, DaysModule, DayNotesModule, AccommodationsModule, AssignmentsModule, PlacesModule, TripsModule, CollabModule, FilesModule, PhotosModule, MemoriesModule, AirtrailModule, JourneyModule, CollectionsModule, ShareModule, TripInviteModule, TransitModule, FeedsModule, SettingsModule, BackupModule, AuthModule, OidcModule, OauthModule, AdminModule, AddonsModule, AuditModule, PermissionsModule, PluginsModule, BookingImportModule, LlmParseModule],
providers: [
// Default-deny: a route is authenticated unless it carries @Public() or
// @OptionalAuth(), or declares its own @UseGuards chain. Protection used to
// be opt-in, which made a forgotten guard a silent bypass instead of an error.
{ provide: APP_GUARD, useClass: GlobalAuthGuard },
// Global error-envelope normaliser (DI-registered so it also catches
// framework-level exceptions like the not-found handler).
{ provide: APP_FILTER, useClass: TrekExceptionFilter },
// SPA fallback: serves index.html for unmatched GETs in production (the Nest
// equivalent of the legacy Express app.get('*') catch-all). @Catch(NotFoundException)
// is more specific than TrekExceptionFilter, so Nest routes 404s here.
{ provide: APP_FILTER, useClass: SpaFallbackFilter },
// Replays the X-Idempotency-Key the client sends on every write, so retried
// mutations don't double-apply.
{ provide: APP_INTERCEPTOR, useClass: IdempotencyInterceptor },
// Global Zod validation: any parameter typed with a createZodDto class
// (the <domain>.dto.ts wrappers over @trek/shared schemas) is validated;
// everything else passes through untouched. Paired with the boot gate in
// common/validate-body-contracts.ts so unvalidated mutation bodies refuse
// to boot instead of shipping silently.
{ provide: APP_PIPE, useClass: ZodValidationPipe },
],
})
export class AppModule {}