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.
This commit is contained in:
parent
7a5cc9f028
commit
1323600978
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<number>("RATE_LIMIT_TTL", 60),
|
||||
limit: configService.get<number>("RATE_LIMIT_LIMIT", 100),
|
||||
},
|
||||
{
|
||||
name: "auth",
|
||||
ttl: configService.get<number>("AUTH_RATE_LIMIT_TTL", 900),
|
||||
limit: configService.get<number>("AUTH_RATE_LIMIT_LIMIT", 3),
|
||||
},
|
||||
{
|
||||
name: "auth-refresh",
|
||||
ttl: configService.get<number>("AUTH_REFRESH_RATE_LIMIT_TTL", 300),
|
||||
limit: configService.get<number>("AUTH_REFRESH_RATE_LIMIT_LIMIT", 10),
|
||||
},
|
||||
];
|
||||
4
apps/bff/src/core/rate-limiting/index.ts
Normal file
4
apps/bff/src/core/rate-limiting/index.ts
Normal file
@ -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";
|
||||
|
||||
44
apps/bff/src/core/rate-limiting/rate-limit.decorator.ts
Normal file
44
apps/bff/src/core/rate-limiting/rate-limit.decorator.ts
Normal file
@ -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);
|
||||
|
||||
166
apps/bff/src/core/rate-limiting/rate-limit.guard.ts
Normal file
166
apps/bff/src/core/rate-limiting/rate-limit.guard.ts
Normal file
@ -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<string, RateLimiterRedis>();
|
||||
|
||||
constructor(
|
||||
private readonly reflector: Reflector,
|
||||
@Inject("REDIS_CLIENT") private readonly redis: Redis,
|
||||
@Inject(Logger) private readonly logger: Logger
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
// Check for route-level options first, then controller-level
|
||||
const options =
|
||||
this.reflector.get<RateLimitOptions>(RATE_LIMIT_KEY, context.getHandler()) ??
|
||||
this.reflector.get<RateLimitOptions>(RATE_LIMIT_KEY, context.getClass());
|
||||
|
||||
// No rate limit configured or explicitly skipped
|
||||
if (!options || options.skip) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const response = context.switchToHttp().getResponse<Response>();
|
||||
|
||||
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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
29
apps/bff/src/core/rate-limiting/rate-limit.module.ts
Normal file
29
apps/bff/src/core/rate-limiting/rate-limit.module.ts
Normal file
@ -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 {}
|
||||
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 } },
|
||||
|
||||
@ -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<string> {
|
||||
// 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}`;
|
||||
}
|
||||
}
|
||||
@ -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<SimCatalogCollection> {
|
||||
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<VpnCatalogProduct[]> {
|
||||
return this.vpnCatalog.getPlans();
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
84
pnpm-lock.yaml
generated
84
pnpm-lock.yaml
generated
@ -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: {}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user