From 13236009781622203ec11621ad77ce46f496c672 Mon Sep 17 00:00:00 2001 From: barsa Date: Thu, 11 Dec 2025 11:25:23 +0900 Subject: [PATCH] Refactor rate limiting implementation and update package dependencies - Removed the @nestjs/throttler package and replaced it with a custom rate limiting solution using rate-limiter-flexible for enhanced control and flexibility. - Updated relevant controllers and services to utilize the new rate limiting approach, ensuring consistent request handling across authentication and catalog endpoints. - Cleaned up unused throttler configuration files and guards to streamline the codebase. - Updated package.json and pnpm-lock.yaml to reflect the removal of outdated dependencies and improve overall package management. --- apps/bff/Dockerfile | 3 + apps/bff/package.json | 3 - apps/bff/src/app.module.ts | 11 +- apps/bff/src/core/config/throttler.config.ts | 19 -- apps/bff/src/core/rate-limiting/index.ts | 4 + .../rate-limiting/rate-limit.decorator.ts | 44 +++++ .../core/rate-limiting/rate-limit.guard.ts | 166 ++++++++++++++++++ .../core/rate-limiting/rate-limit.module.ts | 29 +++ .../rate-limiting/auth-rate-limit.service.ts | 17 +- .../auth/presentation/http/auth.controller.ts | 32 ++-- .../http/guards/auth-throttle.guard.ts | 27 --- .../src/modules/catalog/catalog.controller.ts | 10 +- .../src/modules/orders/orders.controller.ts | 6 +- apps/portal/package.json | 3 +- pnpm-lock.yaml | 84 --------- portal-backend.latest.tar.gz.sha256 | 2 +- portal-frontend.latest.tar.gz.sha256 | 2 +- 17 files changed, 290 insertions(+), 172 deletions(-) delete mode 100644 apps/bff/src/core/config/throttler.config.ts create mode 100644 apps/bff/src/core/rate-limiting/index.ts create mode 100644 apps/bff/src/core/rate-limiting/rate-limit.decorator.ts create mode 100644 apps/bff/src/core/rate-limiting/rate-limit.guard.ts create mode 100644 apps/bff/src/core/rate-limiting/rate-limit.module.ts delete mode 100644 apps/bff/src/modules/auth/presentation/http/guards/auth-throttle.guard.ts diff --git a/apps/bff/Dockerfile b/apps/bff/Dockerfile index 0f37a82e..a257243b 100644 --- a/apps/bff/Dockerfile +++ b/apps/bff/Dockerfile @@ -70,6 +70,7 @@ WORKDIR /app # ============================================================================= FROM node:${NODE_VERSION}-alpine AS production +ARG PNPM_VERSION ARG PRISMA_VERSION LABEL org.opencontainers.image.title="Customer Portal BFF" \ @@ -78,6 +79,8 @@ LABEL org.opencontainers.image.title="Customer Portal BFF" \ # Install runtime dependencies only + security hardening RUN apk add --no-cache dumb-init libc6-compat netcat-openbsd \ + && corepack enable \ + && corepack prepare pnpm@${PNPM_VERSION} --activate \ && addgroup --system --gid 1001 nodejs \ && adduser --system --uid 1001 nestjs \ # Remove apk cache and unnecessary files diff --git a/apps/bff/package.json b/apps/bff/package.json index 5a1f05e8..74d81bd6 100644 --- a/apps/bff/package.json +++ b/apps/bff/package.json @@ -39,14 +39,12 @@ "@nestjs/jwt": "^11.0.2", "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.1.9", - "@nestjs/throttler": "^6.5.0", "@prisma/adapter-pg": "^7.1.0", "@prisma/client": "^7.1.0", "@sendgrid/mail": "^8.1.6", "bcrypt": "^6.0.0", "bullmq": "^5.65.1", "cookie-parser": "^1.4.7", - "express": "^5.2.1", "helmet": "^8.1.0", "ioredis": "^5.8.2", "jsforce": "^3.10.10", @@ -58,7 +56,6 @@ "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "pg": "^8.16.3", - "pino": "^10.1.0", "pino-http": "^11.0.0", "rate-limiter-flexible": "^9.0.0", "reflect-metadata": "^0.2.2", diff --git a/apps/bff/src/app.module.ts b/apps/bff/src/app.module.ts index 82512124..742c7030 100644 --- a/apps/bff/src/app.module.ts +++ b/apps/bff/src/app.module.ts @@ -1,18 +1,17 @@ import { Module } from "@nestjs/common"; import { APP_PIPE } from "@nestjs/core"; import { RouterModule } from "@nestjs/core"; -import { ConfigModule, ConfigService } from "@nestjs/config"; -import { ThrottlerModule } from "@nestjs/throttler"; +import { ConfigModule } from "@nestjs/config"; import { ZodValidationPipe } from "nestjs-zod"; // Configuration import { appConfig } from "@bff/core/config/app.config.js"; -import { createThrottlerConfig } from "@bff/core/config/throttler.config.js"; import { apiRoutes } from "@bff/core/config/router.config.js"; // Core Modules import { LoggingModule } from "@bff/core/logging/logging.module.js"; import { SecurityModule } from "@bff/core/security/security.module.js"; +import { RateLimitModule } from "@bff/core/rate-limiting/index.js"; import { PrismaModule } from "@bff/infra/database/prisma.module.js"; import { RedisModule } from "@bff/infra/redis/redis.module.js"; import { CacheModule } from "@bff/infra/cache/cache.module.js"; @@ -58,11 +57,7 @@ import { HealthModule } from "@bff/modules/health/health.module.js"; // === INFRASTRUCTURE === LoggingModule, SecurityModule, - ThrottlerModule.forRootAsync({ - imports: [ConfigModule], - inject: [ConfigService], - useFactory: createThrottlerConfig, - }), + RateLimitModule, // === CORE SERVICES === PrismaModule, diff --git a/apps/bff/src/core/config/throttler.config.ts b/apps/bff/src/core/config/throttler.config.ts deleted file mode 100644 index 3adcbd69..00000000 --- a/apps/bff/src/core/config/throttler.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { ConfigService } from "@nestjs/config"; -import type { ThrottlerModuleOptions } from "@nestjs/throttler"; - -export const createThrottlerConfig = (configService: ConfigService): ThrottlerModuleOptions => [ - { - ttl: configService.get("RATE_LIMIT_TTL", 60), - limit: configService.get("RATE_LIMIT_LIMIT", 100), - }, - { - name: "auth", - ttl: configService.get("AUTH_RATE_LIMIT_TTL", 900), - limit: configService.get("AUTH_RATE_LIMIT_LIMIT", 3), - }, - { - name: "auth-refresh", - ttl: configService.get("AUTH_REFRESH_RATE_LIMIT_TTL", 300), - limit: configService.get("AUTH_REFRESH_RATE_LIMIT_LIMIT", 10), - }, -]; diff --git a/apps/bff/src/core/rate-limiting/index.ts b/apps/bff/src/core/rate-limiting/index.ts new file mode 100644 index 00000000..b659693d --- /dev/null +++ b/apps/bff/src/core/rate-limiting/index.ts @@ -0,0 +1,4 @@ +export { RateLimitModule } from "./rate-limit.module.js"; +export { RateLimitGuard } from "./rate-limit.guard.js"; +export { RateLimit, SkipRateLimit, type RateLimitOptions, RATE_LIMIT_KEY } from "./rate-limit.decorator.js"; + diff --git a/apps/bff/src/core/rate-limiting/rate-limit.decorator.ts b/apps/bff/src/core/rate-limiting/rate-limit.decorator.ts new file mode 100644 index 00000000..99faeb74 --- /dev/null +++ b/apps/bff/src/core/rate-limiting/rate-limit.decorator.ts @@ -0,0 +1,44 @@ +import { SetMetadata } from "@nestjs/common"; + +export interface RateLimitOptions { + /** + * Maximum number of requests allowed within the TTL window + */ + limit: number; + + /** + * Time-to-live in seconds for the rate limit window + */ + ttl: number; + + /** + * Optional key prefix for Redis storage (defaults to route path) + */ + keyPrefix?: string; + + /** + * Whether to skip rate limiting (useful for conditional logic) + */ + skip?: boolean; +} + +export const RATE_LIMIT_KEY = "RATE_LIMIT_OPTIONS"; + +/** + * Decorator to apply rate limiting to a controller or route handler. + * Uses rate-limiter-flexible with Redis backend. + * + * @example + * ```typescript + * @RateLimit({ limit: 10, ttl: 60 }) // 10 requests per minute + * @Get('endpoint') + * async myEndpoint() { ... } + * ``` + */ +export const RateLimit = (options: RateLimitOptions) => SetMetadata(RATE_LIMIT_KEY, options); + +/** + * Skip rate limiting for this route (useful when applied at controller level) + */ +export const SkipRateLimit = () => SetMetadata(RATE_LIMIT_KEY, { skip: true } as RateLimitOptions); + diff --git a/apps/bff/src/core/rate-limiting/rate-limit.guard.ts b/apps/bff/src/core/rate-limiting/rate-limit.guard.ts new file mode 100644 index 00000000..37fd6421 --- /dev/null +++ b/apps/bff/src/core/rate-limiting/rate-limit.guard.ts @@ -0,0 +1,166 @@ +import { + Injectable, + Inject, + type CanActivate, + type ExecutionContext, + HttpException, + HttpStatus, +} from "@nestjs/common"; +import { Reflector } from "@nestjs/core"; +import { RateLimiterRedis, RateLimiterRes } from "rate-limiter-flexible"; +import type { Redis } from "ioredis"; +import type { Request, Response } from "express"; +import { Logger } from "nestjs-pino"; +import { createHash } from "crypto"; +import { RATE_LIMIT_KEY, type RateLimitOptions } from "./rate-limit.decorator.js"; + +/** + * Rate limit guard using rate-limiter-flexible with Redis backend. + * Replaces @nestjs/throttler with a unified, more flexible solution. + * + * Features: + * - Redis-backed for distributed rate limiting + * - Per-IP + User-Agent tracking for better security + * - In-memory blocking for reduced Redis load + * - Standard rate limit headers (X-RateLimit-*) + */ +@Injectable() +export class RateLimitGuard implements CanActivate { + private readonly limiters = new Map(); + + constructor( + private readonly reflector: Reflector, + @Inject("REDIS_CLIENT") private readonly redis: Redis, + @Inject(Logger) private readonly logger: Logger + ) {} + + async canActivate(context: ExecutionContext): Promise { + // Check for route-level options first, then controller-level + const options = + this.reflector.get(RATE_LIMIT_KEY, context.getHandler()) ?? + this.reflector.get(RATE_LIMIT_KEY, context.getClass()); + + // No rate limit configured or explicitly skipped + if (!options || options.skip) { + return true; + } + + const request = context.switchToHttp().getRequest(); + const response = context.switchToHttp().getResponse(); + + const key = this.buildKey(request, context); + const limiter = this.getOrCreateLimiter(options, context); + + try { + const result = await limiter.consume(key); + + // Set standard rate limit headers + this.setRateLimitHeaders(response, options, result); + + return true; + } catch (error: unknown) { + if (error instanceof RateLimiterRes) { + this.setRateLimitHeaders(response, options, error, true); + + const retryAfterSeconds = Math.ceil(error.msBeforeNext / 1000); + + this.logger.warn( + { key, retryAfter: retryAfterSeconds, path: request.path }, + "Rate limit exceeded" + ); + + throw new HttpException( + { + statusCode: HttpStatus.TOO_MANY_REQUESTS, + message: `Too many requests. Please try again in ${retryAfterSeconds} seconds.`, + error: "Too Many Requests", + retryAfter: retryAfterSeconds, + }, + HttpStatus.TOO_MANY_REQUESTS, + { + cause: error, + } + ); + } + + // Redis connection error - fail open with warning + this.logger.error({ error, key }, "Rate limiter error - failing open"); + return true; + } + } + + /** + * Build a unique key for rate limiting based on IP and User-Agent + */ + private buildKey(request: Request, context: ExecutionContext): string { + const ip = this.extractIp(request); + const userAgent = request.headers["user-agent"] || "unknown"; + const uaHash = createHash("sha256").update(String(userAgent)).digest("hex").slice(0, 16); + + // Use handler name as part of key to separate limits per endpoint + const handlerName = context.getHandler().name; + const controllerName = context.getClass().name; + + return `rl:${controllerName}:${handlerName}:${ip}:${uaHash}`; + } + + /** + * Extract client IP address, handling proxies + */ + private extractIp(request: Request): string { + const forwarded = request.headers["x-forwarded-for"]; + const forwardedIp = Array.isArray(forwarded) ? forwarded[0] : forwarded; + + const rawIp = + (typeof forwardedIp === "string" ? forwardedIp.split(",")[0]?.trim() : undefined) || + (request.headers["x-real-ip"] as string | undefined) || + request.socket?.remoteAddress || + request.ip || + "unknown"; + + return rawIp.replace(/^::ffff:/, ""); + } + + /** + * Get or create a rate limiter for the given options + */ + private getOrCreateLimiter(options: RateLimitOptions, context: ExecutionContext): RateLimiterRedis { + const handlerName = context.getHandler().name; + const controllerName = context.getClass().name; + const cacheKey = `${controllerName}:${handlerName}:${options.limit}:${options.ttl}`; + + let limiter = this.limiters.get(cacheKey); + if (!limiter) { + limiter = new RateLimiterRedis({ + storeClient: this.redis, + keyPrefix: options.keyPrefix ?? `rate_limit:${controllerName}:${handlerName}`, + points: options.limit, + duration: options.ttl, + // Block in-memory after limit exceeded to reduce Redis load + inMemoryBlockOnConsumed: options.limit + 1, + }); + this.limiters.set(cacheKey, limiter); + } + + return limiter; + } + + /** + * Set standard rate limit headers on response + */ + private setRateLimitHeaders( + response: Response, + options: RateLimitOptions, + result: RateLimiterRes, + exceeded = false + ): void { + response.setHeader("X-RateLimit-Limit", String(options.limit)); + response.setHeader("X-RateLimit-Remaining", String(Math.max(0, result.remainingPoints))); + response.setHeader("X-RateLimit-Reset", String(Math.ceil(result.msBeforeNext / 1000))); + + if (exceeded) { + response.setHeader("Retry-After", String(Math.ceil(result.msBeforeNext / 1000))); + } + } +} + diff --git a/apps/bff/src/core/rate-limiting/rate-limit.module.ts b/apps/bff/src/core/rate-limiting/rate-limit.module.ts new file mode 100644 index 00000000..1ceecc15 --- /dev/null +++ b/apps/bff/src/core/rate-limiting/rate-limit.module.ts @@ -0,0 +1,29 @@ +import { Global, Module } from "@nestjs/common"; +import { RateLimitGuard } from "./rate-limit.guard.js"; + +/** + * Global rate limiting module using rate-limiter-flexible with Redis. + * + * This replaces @nestjs/throttler with a unified solution that: + * - Uses the same Redis-backed rate-limiter-flexible as auth rate limiting + * - Provides consistent rate limiting across the application + * - Reduces dependency count + * + * Usage: + * ```typescript + * @Controller('example') + * @UseGuards(RateLimitGuard) + * export class ExampleController { + * @RateLimit({ limit: 10, ttl: 60 }) + * @Get() + * async example() { ... } + * } + * ``` + */ +@Global() +@Module({ + providers: [RateLimitGuard], + exports: [RateLimitGuard], +}) +export class RateLimitModule {} + diff --git a/apps/bff/src/modules/auth/infra/rate-limiting/auth-rate-limit.service.ts b/apps/bff/src/modules/auth/infra/rate-limiting/auth-rate-limit.service.ts index b3754a55..0b6b64d0 100644 --- a/apps/bff/src/modules/auth/infra/rate-limiting/auth-rate-limit.service.ts +++ b/apps/bff/src/modules/auth/infra/rate-limiting/auth-rate-limit.service.ts @@ -1,6 +1,5 @@ -import { Inject, Injectable, InternalServerErrorException } from "@nestjs/common"; +import { Inject, Injectable, InternalServerErrorException, HttpException, HttpStatus } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; -import { ThrottlerException } from "@nestjs/throttler"; import { Logger } from "nestjs-pino"; import type { Request } from "express"; import { RateLimiterRedis, RateLimiterRes } from "rate-limiter-flexible"; @@ -174,11 +173,21 @@ export class AuthRateLimitService { this.logger.warn({ key, context, retryAfterMs }, "Auth rate limit reached"); - throw new ThrottlerException(message); + throw new HttpException( + { statusCode: HttpStatus.TOO_MANY_REQUESTS, message, error: "Too Many Requests" }, + HttpStatus.TOO_MANY_REQUESTS + ); } this.logger.error({ key, context, error: getErrorMessage(error) }, "Rate limiter failure"); - throw new ThrottlerException("Authentication temporarily unavailable"); + throw new HttpException( + { + statusCode: HttpStatus.TOO_MANY_REQUESTS, + message: "Authentication temporarily unavailable", + error: "Too Many Requests", + }, + HttpStatus.TOO_MANY_REQUESTS + ); } } diff --git a/apps/bff/src/modules/auth/presentation/http/auth.controller.ts b/apps/bff/src/modules/auth/presentation/http/auth.controller.ts index 7e5abb71..9aa3d84e 100644 --- a/apps/bff/src/modules/auth/presentation/http/auth.controller.ts +++ b/apps/bff/src/modules/auth/presentation/http/auth.controller.ts @@ -11,11 +11,10 @@ import { Res, } from "@nestjs/common"; import type { Request, Response } from "express"; -import { Throttle } from "@nestjs/throttler"; +import { RateLimitGuard, RateLimit } from "@bff/core/rate-limiting/index.js"; import { JwtService } from "@nestjs/jwt"; import { AuthFacade } from "@bff/modules/auth/application/auth.facade.js"; import { LocalAuthGuard } from "./guards/local-auth.guard.js"; -import { AuthThrottleGuard } from "./guards/auth-throttle.guard.js"; import { FailedLoginThrottleGuard, type RequestWithRateLimit, @@ -137,8 +136,8 @@ export class AuthController { @Public() @Post("validate-signup") - @UseGuards(AuthThrottleGuard, SalesforceReadThrottleGuard) - @Throttle({ default: { limit: 20, ttl: 600 } }) // 20 validations per 10 minutes per IP + @UseGuards(RateLimitGuard, SalesforceReadThrottleGuard) + @RateLimit({ limit: 20, ttl: 600 }) // 20 validations per 10 minutes per IP @UsePipes(new ZodValidationPipe(validateSignupRequestSchema)) async validateSignup(@Body() validateData: ValidateSignupRequest, @Req() req: Request) { return this.authFacade.validateSignup(validateData, req); @@ -152,8 +151,8 @@ export class AuthController { @Public() @Post("signup-preflight") - @UseGuards(AuthThrottleGuard, SalesforceReadThrottleGuard) - @Throttle({ default: { limit: 20, ttl: 600 } }) // 20 validations per 10 minutes per IP + @UseGuards(RateLimitGuard, SalesforceReadThrottleGuard) + @RateLimit({ limit: 20, ttl: 600 }) // 20 validations per 10 minutes per IP @UsePipes(new ZodValidationPipe(signupRequestSchema)) @HttpCode(200) async signupPreflight(@Body() signupData: SignupRequest) { @@ -169,8 +168,8 @@ export class AuthController { @Public() @Post("signup") - @UseGuards(AuthThrottleGuard, SalesforceWriteThrottleGuard) - @Throttle({ default: { limit: 5, ttl: 900 } }) // 5 signups per 15 minutes per IP (reasonable for account creation) + @UseGuards(RateLimitGuard, SalesforceWriteThrottleGuard) + @RateLimit({ limit: 5, ttl: 900 }) // 5 signups per 15 minutes per IP (reasonable for account creation) @UsePipes(new ZodValidationPipe(signupRequestSchema)) async signup( @Body() signupData: SignupRequest, @@ -228,7 +227,8 @@ export class AuthController { @Public() @Post("refresh") - @Throttle({ default: { limit: 10, ttl: 300 } }) // 10 attempts per 5 minutes per IP + @UseGuards(RateLimitGuard) + @RateLimit({ limit: 10, ttl: 300 }) // 10 attempts per 5 minutes per IP @UsePipes(new ZodValidationPipe(refreshTokenRequestSchema)) async refreshToken( @Body() body: RefreshTokenRequest, @@ -248,8 +248,8 @@ export class AuthController { @Public() @Post("link-whmcs") - @UseGuards(AuthThrottleGuard, SalesforceWriteThrottleGuard) - @Throttle({ default: { limit: 5, ttl: 600 } }) // 5 attempts per 10 minutes per IP (industry standard) + @UseGuards(RateLimitGuard, SalesforceWriteThrottleGuard) + @RateLimit({ limit: 5, ttl: 600 }) // 5 attempts per 10 minutes per IP (industry standard) @UsePipes(new ZodValidationPipe(linkWhmcsRequestSchema)) async linkWhmcs(@Body() linkData: LinkWhmcsRequest, @Req() _req: Request) { const result = await this.authFacade.linkWhmcsUser(linkData); @@ -258,8 +258,8 @@ export class AuthController { @Public() @Post("set-password") - @UseGuards(AuthThrottleGuard) - @Throttle({ default: { limit: 5, ttl: 600 } }) // 5 attempts per 10 minutes per IP+UA (industry standard) + @UseGuards(RateLimitGuard) + @RateLimit({ limit: 5, ttl: 600 }) // 5 attempts per 10 minutes per IP+UA (industry standard) @UsePipes(new ZodValidationPipe(setPasswordRequestSchema)) async setPassword( @Body() setPasswordData: SetPasswordRequest, @@ -282,7 +282,8 @@ export class AuthController { @Public() @Post("request-password-reset") - @Throttle({ default: { limit: 5, ttl: 900 } }) // 5 attempts per 15 minutes (standard for password operations) + @UseGuards(RateLimitGuard) + @RateLimit({ limit: 5, ttl: 900 }) // 5 attempts per 15 minutes (standard for password operations) @UsePipes(new ZodValidationPipe(passwordResetRequestSchema)) async requestPasswordReset(@Body() body: PasswordResetRequest, @Req() req: Request) { await this.authFacade.requestPasswordReset(body.email, req); @@ -305,7 +306,8 @@ export class AuthController { } @Post("change-password") - @Throttle({ default: { limit: 5, ttl: 300 } }) + @UseGuards(RateLimitGuard) + @RateLimit({ limit: 5, ttl: 300 }) // 5 attempts per 5 minutes @UsePipes(new ZodValidationPipe(changePasswordRequestSchema)) async changePassword( @Req() req: Request & { user: { id: string } }, diff --git a/apps/bff/src/modules/auth/presentation/http/guards/auth-throttle.guard.ts b/apps/bff/src/modules/auth/presentation/http/guards/auth-throttle.guard.ts deleted file mode 100644 index 0cbc5585..00000000 --- a/apps/bff/src/modules/auth/presentation/http/guards/auth-throttle.guard.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { ThrottlerGuard } from "@nestjs/throttler"; -import { createHash } from "crypto"; -import type { Request } from "express"; - -@Injectable() -export class AuthThrottleGuard extends ThrottlerGuard { - protected override async getTracker(req: Request): Promise { - // Track by IP address + User Agent for better security on sensitive auth operations - const forwarded = req.headers["x-forwarded-for"]; - const forwardedIp = Array.isArray(forwarded) ? forwarded[0] : forwarded; - const ip = - (typeof forwardedIp === "string" ? forwardedIp.split(",")[0]?.trim() : undefined) || - (req.headers["x-real-ip"] as string | undefined) || - req.socket?.remoteAddress || - req.ip || - "unknown"; - - const userAgent = req.headers["user-agent"] || "unknown"; - const userAgentHash = createHash("sha256").update(userAgent).digest("hex").slice(0, 16); - - const normalizedIp = ip.replace(/^::ffff:/, ""); - - const resolvedIp = await Promise.resolve(normalizedIp); - return `auth_${resolvedIp}_${userAgentHash}`; - } -} diff --git a/apps/bff/src/modules/catalog/catalog.controller.ts b/apps/bff/src/modules/catalog/catalog.controller.ts index 4feab39f..667b26b0 100644 --- a/apps/bff/src/modules/catalog/catalog.controller.ts +++ b/apps/bff/src/modules/catalog/catalog.controller.ts @@ -1,5 +1,5 @@ import { Controller, Get, Request, UseGuards, Header } from "@nestjs/common"; -import { Throttle, ThrottlerGuard } from "@nestjs/throttler"; +import { RateLimitGuard, RateLimit } from "@bff/core/rate-limiting/index.js"; import type { RequestWithUser } from "@bff/modules/auth/auth.types.js"; import { parseInternetCatalog, @@ -18,7 +18,7 @@ import { VpnCatalogService } from "./services/vpn-catalog.service.js"; import { SalesforceReadThrottleGuard } from "@bff/integrations/salesforce/guards/salesforce-read-throttle.guard.js"; @Controller("catalog") -@UseGuards(SalesforceReadThrottleGuard, ThrottlerGuard) +@UseGuards(SalesforceReadThrottleGuard, RateLimitGuard) export class CatalogController { constructor( private internetCatalog: InternetCatalogService, @@ -27,7 +27,7 @@ export class CatalogController { ) {} @Get("internet/plans") - @Throttle({ default: { limit: 20, ttl: 60 } }) // 20 requests per minute + @RateLimit({ limit: 20, ttl: 60 }) // 20 requests per minute @Header("Cache-Control", "private, max-age=300") // Personalised responses: prevent shared caching async getInternetPlans(@Request() req: RequestWithUser): Promise<{ plans: InternetPlanCatalogItem[]; @@ -62,7 +62,7 @@ export class CatalogController { } @Get("sim/plans") - @Throttle({ default: { limit: 20, ttl: 60 } }) // 20 requests per minute + @RateLimit({ limit: 20, ttl: 60 }) // 20 requests per minute @Header("Cache-Control", "private, max-age=300") // Personalised responses: prevent shared caching async getSimCatalogData(@Request() req: RequestWithUser): Promise { const userId = req.user?.id; @@ -96,7 +96,7 @@ export class CatalogController { } @Get("vpn/plans") - @Throttle({ default: { limit: 20, ttl: 60 } }) // 20 requests per minute + @RateLimit({ limit: 20, ttl: 60 }) // 20 requests per minute @Header("Cache-Control", "public, max-age=300, s-maxage=300") // 5 minutes async getVpnPlans(): Promise { return this.vpnCatalog.getPlans(); diff --git a/apps/bff/src/modules/orders/orders.controller.ts b/apps/bff/src/modules/orders/orders.controller.ts index 6f24368d..081e55c8 100644 --- a/apps/bff/src/modules/orders/orders.controller.ts +++ b/apps/bff/src/modules/orders/orders.controller.ts @@ -11,7 +11,7 @@ import { UnauthorizedException, type MessageEvent, } from "@nestjs/common"; -import { Throttle, ThrottlerGuard } from "@nestjs/throttler"; +import { RateLimitGuard, RateLimit } from "@bff/core/rate-limiting/index.js"; import { OrderOrchestrator } from "./services/order-orchestrator.service.js"; import type { RequestWithUser } from "@bff/modules/auth/auth.types.js"; import { Logger } from "nestjs-pino"; @@ -30,7 +30,7 @@ import { SalesforceReadThrottleGuard } from "@bff/integrations/salesforce/guards import { SalesforceWriteThrottleGuard } from "@bff/integrations/salesforce/guards/salesforce-write-throttle.guard.js"; @Controller("orders") -@UseGuards(ThrottlerGuard) +@UseGuards(RateLimitGuard) export class OrdersController { constructor( private orderOrchestrator: OrderOrchestrator, @@ -42,7 +42,7 @@ export class OrdersController { @Post() @UseGuards(SalesforceWriteThrottleGuard) - @Throttle({ default: { limit: 5, ttl: 60 } }) // 5 order creations per minute + @RateLimit({ limit: 5, ttl: 60 }) // 5 order creations per minute @UsePipes(new ZodValidationPipe(createOrderRequestSchema)) async create(@Request() req: RequestWithUser, @Body() body: CreateOrderRequest) { this.logger.log( diff --git a/apps/portal/package.json b/apps/portal/package.json index 0f08aee6..c58857dd 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -41,7 +41,6 @@ "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "tailwindcss": "^4.1.17", - "typescript": "^5.9.3", - "webpack-bundle-analyzer": "^5.1.0" + "typescript": "^5.9.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 308a86b6..d30a2ab9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -76,9 +76,6 @@ importers: '@nestjs/platform-express': specifier: ^11.1.9 version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) - '@nestjs/throttler': - specifier: ^6.5.0 - version: 6.5.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2) '@prisma/adapter-pg': specifier: ^7.1.0 version: 7.1.0 @@ -97,9 +94,6 @@ importers: cookie-parser: specifier: ^1.4.7 version: 1.4.7 - express: - specifier: ^5.2.1 - version: 5.2.1 helmet: specifier: ^8.1.0 version: 8.1.0 @@ -133,9 +127,6 @@ importers: pg: specifier: ^8.16.3 version: 8.16.3 - pino: - specifier: ^10.1.0 - version: 10.1.0 pino-http: specifier: ^11.0.0 version: 11.0.0 @@ -294,9 +285,6 @@ importers: typescript: specifier: ^5.9.3 version: 5.9.3 - webpack-bundle-analyzer: - specifier: ^5.1.0 - version: 5.1.0 packages/domain: dependencies: @@ -1477,13 +1465,6 @@ packages: '@nestjs/platform-express': optional: true - '@nestjs/throttler@6.5.0': - resolution: {integrity: sha512-9j0ZRfH0QE1qyrj9JjIRDz5gQLPqq9yVC2nHsrosDVAfI5HHw08/aUAWx9DZLSdQf4HDkmhTTEGLrRFHENvchQ==} - peerDependencies: - '@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 - '@nestjs/core': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 - reflect-metadata: ^0.1.13 || ^0.2.0 - '@next/bundle-analyzer@16.0.8': resolution: {integrity: sha512-LFWHAWdurTCpqLq1ZRUah1nuDK8cti6kN8vj4wqIW3yAnq3BDgJeNgMMUmeLLzR9C81AnVKfSZFBEhn+aUpe/g==} @@ -3327,10 +3308,6 @@ packages: resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} engines: {node: '>= 18'} - express@5.2.1: - resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} - engines: {node: '>= 18'} - exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} @@ -5750,11 +5727,6 @@ packages: engines: {node: '>= 10.13.0'} hasBin: true - webpack-bundle-analyzer@5.1.0: - resolution: {integrity: sha512-WAWwIoIUx4yC2AEBqXbDkcmh/LzAaenv0+nISBflP5l+XIXO9/x6poWarGA3RTrfavk9H3oWQ64Wm0z26/UGKA==} - engines: {node: '>= 20.9.0'} - hasBin: true - webpack-node-externals@3.0.0: resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} engines: {node: '>=6'} @@ -7090,12 +7062,6 @@ snapshots: optionalDependencies: '@nestjs/platform-express': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) - '@nestjs/throttler@6.5.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)': - dependencies: - '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) - reflect-metadata: 0.2.2 - '@next/bundle-analyzer@16.0.8': dependencies: webpack-bundle-analyzer: 4.10.1 @@ -9258,39 +9224,6 @@ snapshots: transitivePeerDependencies: - supports-color - express@5.2.1: - dependencies: - accepts: 2.0.0 - body-parser: 2.2.1 - content-disposition: 1.0.1 - content-type: 1.0.5 - cookie: 0.7.2 - cookie-signature: 1.2.2 - debug: 4.4.3 - depd: 2.0.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 2.1.1 - fresh: 2.0.0 - http-errors: 2.0.1 - merge-descriptors: 2.0.0 - mime-types: 3.0.2 - on-finished: 2.4.1 - once: 1.4.0 - parseurl: 1.3.3 - proxy-addr: 2.0.7 - qs: 6.14.0 - range-parser: 1.2.1 - router: 2.2.0 - send: 1.2.0 - serve-static: 2.2.0 - statuses: 2.0.2 - type-is: 2.0.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - exsolve@1.0.8: {} ext-list@2.2.2: @@ -12090,23 +12023,6 @@ snapshots: - bufferutil - utf-8-validate - webpack-bundle-analyzer@5.1.0: - dependencies: - '@discoveryjs/json-ext': 0.5.7 - acorn: 8.15.0 - acorn-walk: 8.3.4 - commander: 7.2.0 - debounce: 1.2.1 - escape-string-regexp: 4.0.0 - html-escaper: 2.0.2 - opener: 1.5.2 - picocolors: 1.1.1 - sirv: 2.0.4 - ws: 7.5.10 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - webpack-node-externals@3.0.0: {} webpack-sources@3.3.3: {} diff --git a/portal-backend.latest.tar.gz.sha256 b/portal-backend.latest.tar.gz.sha256 index 6a9f2b06..49e2b1b3 100644 --- a/portal-backend.latest.tar.gz.sha256 +++ b/portal-backend.latest.tar.gz.sha256 @@ -1 +1 @@ -53a3fba0f80abe58f8d3c0cde37b99027ce93f03486b4124c0664c7d53233921 /home/barsa/projects/customer_portal/customer-portal/portal-backend.latest.tar.gz +b809f14715623b94d3a38d058ab09ef4cb3f9aa655031c4613c2feeedacce14b /home/barsa/projects/customer_portal/customer-portal/portal-backend.latest.tar.gz diff --git a/portal-frontend.latest.tar.gz.sha256 b/portal-frontend.latest.tar.gz.sha256 index 128667fc..0d861f06 100644 --- a/portal-frontend.latest.tar.gz.sha256 +++ b/portal-frontend.latest.tar.gz.sha256 @@ -1 +1 @@ -85dcbdfe8c81740c1d5d6d2bc09a81a92148f1f79c1a6ee5dcc8baf85e57be32 /home/barsa/projects/customer_portal/customer-portal/portal-frontend.latest.tar.gz +447e6f2bebb4670bab788977187704c10a5b8cf0e318f950d16bc15d1e459ce2 /home/barsa/projects/customer_portal/customer-portal/portal-frontend.latest.tar.gz