import { ThrottlerStorageRedisService } from '@nest-lab/throttler-storage-redis'; import { BullModule } from '@nestjs/bullmq'; import { type MiddlewareConsumer, Module, type NestModule, RequestMethod } from '@nestjs/common'; import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'; import { CqrsModule } from '@nestjs/cqrs'; import { ScheduleModule } from '@nestjs/schedule'; import { ThrottlerModule } from '@nestjs/throttler'; import { SentryGlobalFilter, SentryModule } from '@sentry/nestjs/setup'; import { AdminModule } from '@modules/admin'; import { AgentsModule } from '@modules/agents'; import { AnalyticsModule } from '@modules/analytics'; import { AuthModule } from '@modules/auth'; import { FavoritesModule } from '@modules/favorites'; import { HealthModule } from '@modules/health'; import { IndustrialModule } from '@modules/industrial'; import { InquiriesModule } from '@modules/inquiries'; import { LeadsModule } from '@modules/leads'; import { ListingsModule } from '@modules/listings'; import { McpIntegrationModule } from '@modules/mcp'; import { MessagingModule } from '@modules/messaging'; import { HttpMetricsInterceptor, MetricsModule } from '@modules/metrics'; import { NotificationsModule } from '@modules/notifications'; import { PaymentsModule } from '@modules/payments'; import { ProjectsModule } from '@modules/projects'; import { ReportsModule } from '@modules/reports'; import { ReviewsModule } from '@modules/reviews'; import { SearchModule } from '@modules/search'; import { SharedModule } from '@modules/shared'; import { ThrottlerBehindProxyGuard } from '@modules/shared/infrastructure/guards/throttler-behind-proxy.guard'; import { CsrfMiddleware } from '@modules/shared/infrastructure/middleware/csrf.middleware'; import { SanitizeInputMiddleware } from '@modules/shared/infrastructure/middleware/sanitize-input.middleware'; import { SubscriptionsModule } from '@modules/subscriptions'; import { TransferModule } from '@modules/transfer'; import { AppController } from './app.controller'; @Module({ imports: [ SentryModule.forRoot(), BullModule.forRoot({ connection: { host: process.env['REDIS_HOST'] ?? 'localhost', port: Number(process.env['REDIS_PORT'] ?? 6379), password: process.env['REDIS_PASSWORD'] ?? undefined, }, }), CqrsModule.forRoot(), ScheduleModule.forRoot(), SharedModule, HealthModule, AuthModule, AgentsModule, InquiriesModule, LeadsModule, ListingsModule, ReviewsModule, FavoritesModule, SearchModule, NotificationsModule, PaymentsModule, SubscriptionsModule, AdminModule, AnalyticsModule, MetricsModule, McpIntegrationModule, MessagingModule, ReportsModule, ProjectsModule, IndustrialModule, TransferModule, // ── Rate Limiting ── // Default: 60 requests per 60 seconds per IP // Override per-route with @Throttle() decorator // Storage: Redis-backed sliding window so limits are shared across // every API instance (required for TEC-2930 feature-listing throttling). ThrottlerModule.forRoot({ throttlers: [ { name: 'default', ttl: 60_000, limit: process.env['NODE_ENV'] === 'test' || process.env['NODE_ENV'] === 'development' ? 10_000 : 60, }, { name: 'auth', ttl: 60_000, limit: process.env['NODE_ENV'] === 'test' || process.env['NODE_ENV'] === 'development' ? 10_000 : 10, }, { name: 'payment-callback', ttl: 60_000, limit: process.env['NODE_ENV'] === 'test' || process.env['NODE_ENV'] === 'development' ? 10_000 : 20, }, ], storage: new ThrottlerStorageRedisService({ host: process.env['REDIS_HOST'] ?? 'localhost', port: Number(process.env['REDIS_PORT'] ?? 6379), password: process.env['REDIS_PASSWORD'] ?? undefined, // Single retry per command + bounded reconnect backoff so a // transient Redis blip cannot stall the request path. Behaviour // matches RedisService for consistency. maxRetriesPerRequest: 1, enableReadyCheck: false, lazyConnect: true, retryStrategy(times: number): number { return Math.min(times * 1000, 5000); }, keyPrefix: 'throttler:', }), }), ], controllers: [AppController], providers: [ { provide: APP_FILTER, useClass: SentryGlobalFilter, }, { provide: APP_GUARD, useClass: ThrottlerBehindProxyGuard, }, { provide: APP_INTERCEPTOR, useClass: HttpMetricsInterceptor, }, ], }) export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer): void { // Sanitize all incoming request strings to prevent stored XSS consumer .apply(SanitizeInputMiddleware) .forRoutes('*'); // CSRF double-submit cookie (sets on GET, validates on state-changing methods) // Exclude health endpoints — they must remain accessible without cookies // Skip entirely in test mode so E2E / API tests can POST without a CSRF cookie if (process.env['NODE_ENV'] !== 'test') { consumer .apply(CsrfMiddleware) .exclude( { path: 'health', method: RequestMethod.GET }, { path: 'health/(.*)', method: RequestMethod.GET }, { path: 'api/v1/web-vitals', method: RequestMethod.POST }, // sendBeacon cannot send CSRF headers { path: 'web-vitals', method: RequestMethod.POST }, // middleware exclude uses controller-relative path ) .forRoutes('*'); } } }