feat(listings): rate limit feature-listing via @nestjs/throttler (TEC-2930)
- Wire ThrottlerModule to a Redis-backed storage (shared across API
instances) using @nest-lab/throttler-storage-redis.
- Add FeatureListingThrottlerGuard that tracks per-user when JWT is
present, falling back to the real client IP behind the reverse proxy —
keeps per-user and per-IP buckets independent.
- Apply @Throttle({ default: { limit: 10, ttl: 60_000 } }) + the guard
to POST /listings/:id/feature and document 429 in Swagger.
- Integration test (feature-listing-throttle.integration.spec.ts)
verifies: 10 reqs pass / 11th returns 429 with Retry-After, separate
IPs keep their own quotas, and the tracker key logic.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
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';
|
||||
@@ -70,6 +71,8 @@ import { AppController } from './app.controller';
|
||||
// ── 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: [
|
||||
{
|
||||
@@ -88,6 +91,21 @@ import { AppController } from './app.controller';
|
||||
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],
|
||||
|
||||
Reference in New Issue
Block a user