Files
goodgo-platform/apps/api/src/modules/shared/shared.module.ts
2026-05-04 17:27:08 +07:00

133 lines
5.6 KiB
TypeScript

import { Global, type MiddlewareConsumer, Module, type NestModule, RequestMethod } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { PrometheusModule, makeCounterProvider } from '@willsoto/nestjs-prometheus';
import {
CacheService,
CACHE_HIT_TOTAL,
CACHE_MISS_TOTAL,
CACHE_DEGRADATION_TOTAL,
} from './infrastructure/cache.service';
// RFC-004 Phase 0 (GOO-172) — durable async messaging backbone is parked
// while the `@goodgo/contracts-events` workspace package is wired up.
// `event-bus/` and `outbox/` subdirectories are excluded from typecheck via
// apps/api/tsconfig.json. Re-enable the imports + providers below once the
// missing types land.
// import { EVENT_BUS, RedisStreamsEventBus } from './infrastructure/event-bus';
import { EventBusService } from './infrastructure/event-bus.service';
import { FieldEncryptionService } from './infrastructure/field-encryption.service';
import { GlobalExceptionFilter } from './infrastructure/filters/global-exception.filter';
import { GeoLookupService } from './infrastructure/geo-lookup.service';
import { DeprecationInterceptor, VersionInterceptor } from './infrastructure/interceptors';
import { LoggerService } from './infrastructure/logger.service';
import { CorrelationIdMiddleware } from './infrastructure/middleware/correlation-id.middleware';
import { CsrfMiddleware } from './infrastructure/middleware/csrf.middleware';
import { RequestLoggingMiddleware } from './infrastructure/middleware/request-logging.middleware';
import { SanitizeInputMiddleware } from './infrastructure/middleware/sanitize-input.middleware';
// import { OutboxRelay, OutboxService } from './infrastructure/outbox';
import { PrismaService } from './infrastructure/prisma.service';
import { RedisService } from './infrastructure/redis.service';
import { TypesenseClientService } from './infrastructure/typesense-client.service';
@Global()
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
EventEmitterModule.forRoot(),
PrometheusModule.register({ path: '/metrics', defaultMetrics: { enabled: true } }),
],
providers: [
LoggerService,
FieldEncryptionService,
PrismaService,
RedisService,
CacheService,
EventBusService,
GeoLookupService,
// RFC-004 Phase 0 (GOO-172) — see import comment above.
// { provide: EVENT_BUS, useClass: RedisStreamsEventBus },
// OutboxService,
// OutboxRelay,
TypesenseClientService,
makeCounterProvider({
name: CACHE_HIT_TOTAL,
help: 'Total number of cache hits',
labelNames: ['resource'],
}),
makeCounterProvider({
name: CACHE_MISS_TOTAL,
help: 'Total number of cache misses',
labelNames: ['resource'],
}),
makeCounterProvider({
name: CACHE_DEGRADATION_TOTAL,
help: 'Total number of cache degradation events',
labelNames: ['resource', 'operation'],
}),
{
provide: APP_FILTER,
useClass: GlobalExceptionFilter,
},
// RFC-001 Phase 1 (GOO-170) — order matters: VersionInterceptor first
// populates req.apiVersion; DeprecationInterceptor reads from it.
{
provide: APP_INTERCEPTOR,
useClass: VersionInterceptor,
},
{
provide: APP_INTERCEPTOR,
useClass: DeprecationInterceptor,
},
],
exports: [
PrismaService,
RedisService,
CacheService,
LoggerService,
EventBusService,
FieldEncryptionService,
GeoLookupService,
TypesenseClientService,
PrometheusModule,
],
})
export class SharedModule implements NestModule {
configure(consumer: MiddlewareConsumer): void {
consumer
.apply(CorrelationIdMiddleware, SanitizeInputMiddleware, RequestLoggingMiddleware)
.forRoutes('*');
if (process.env['NODE_ENV'] !== 'test') {
consumer
.apply(CsrfMiddleware)
.exclude(
// NOTE: Nest 11 + path-to-regexp v8 matches `forRoutes('*')`
// middleware exclude paths against the FULL request URL — i.e.
// including the global prefix `api/v1`. Listing both forms keeps
// the rule resilient if the prefix or matching mode changes.
{ path: 'api/v1/payments/callback/*path', method: RequestMethod.POST },
{ path: 'api/v1/auth/login', method: RequestMethod.POST },
{ path: 'api/v1/auth/register', method: RequestMethod.POST },
{ path: 'api/v1/auth/refresh', method: RequestMethod.POST },
{ path: 'api/v1/auth/exchange-token', method: RequestMethod.POST },
{ path: 'api/v1/auth/logout', method: RequestMethod.POST },
{ path: 'api/v1/auth/forgot-password', method: RequestMethod.POST },
{ path: 'api/v1/auth/reset-password', method: RequestMethod.POST },
{ path: 'api/v1/web-vitals', method: RequestMethod.POST }, // sendBeacon cannot send CSRF headers
// Legacy controller-relative forms (kept for older path-matching modes).
{ path: 'auth/login', method: RequestMethod.POST },
{ path: 'auth/register', method: RequestMethod.POST },
{ path: 'auth/refresh', method: RequestMethod.POST },
{ path: 'auth/exchange-token', method: RequestMethod.POST },
{ path: 'auth/logout', method: RequestMethod.POST },
{ path: 'auth/forgot-password', method: RequestMethod.POST },
{ path: 'auth/reset-password', method: RequestMethod.POST },
{ path: 'web-vitals', method: RequestMethod.POST },
{ path: 'payments/callback/*path', method: RequestMethod.POST },
)
.forRoutes('*');
}
}
}