forked from Shared/esg
initial commit
This commit is contained in:
36
src/rate-limit/rate-limit.module.ts
Normal file
36
src/rate-limit/rate-limit.module.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import { UsersModule } from '../users/users.module';
|
||||
import { UserRateLimitService } from './user-rate-limit.service';
|
||||
import { UserRateLimitGuard } from './user-rate-limit.guard';
|
||||
|
||||
/**
|
||||
* Global rate limiting via @nestjs/throttler plus per-user limits for inquiry routes.
|
||||
*/
|
||||
@Module({
|
||||
imports: [
|
||||
UsersModule,
|
||||
ThrottlerModule.forRootAsync({
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => [
|
||||
{
|
||||
ttl: config.get<number>('throttle.ttl', 60) * 1000,
|
||||
limit: config.get<number>('throttle.limit', 100),
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
providers: [
|
||||
UserRateLimitService,
|
||||
UserRateLimitGuard,
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: ThrottlerGuard,
|
||||
},
|
||||
],
|
||||
/** Re-export UsersModule so importers (e.g. InquiryModule) can resolve UserRateLimitGuard deps. */
|
||||
exports: [UserRateLimitService, UserRateLimitGuard, UsersModule],
|
||||
})
|
||||
export class RateLimitModule {}
|
||||
45
src/rate-limit/user-rate-limit.guard.ts
Normal file
45
src/rate-limit/user-rate-limit.guard.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Injectable,
|
||||
} from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import { AuthenticatedUser } from '../auth/interfaces/authenticated-user.interface';
|
||||
import { UsersService } from '../users/users.service';
|
||||
import { UserRateLimitService } from './user-rate-limit.service';
|
||||
|
||||
/**
|
||||
* Per-user rate limits from the user document (requestLimitPerMinute / requestLimitPerDay).
|
||||
* Complements global ThrottlerGuard on inquiry routes.
|
||||
*/
|
||||
@Injectable()
|
||||
export class UserRateLimitGuard implements CanActivate {
|
||||
constructor(
|
||||
private readonly rateLimitService: UserRateLimitService,
|
||||
private readonly usersService: UsersService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request & { user?: AuthenticatedUser }>();
|
||||
const user = request.user;
|
||||
|
||||
if (!user) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const allowed = this.rateLimitService.checkAndIncrement(
|
||||
user.id,
|
||||
user.requestLimitPerMinute,
|
||||
user.requestLimitPerDay,
|
||||
);
|
||||
|
||||
if (!allowed) {
|
||||
throw new HttpException('User rate limit exceeded', HttpStatus.TOO_MANY_REQUESTS);
|
||||
}
|
||||
|
||||
void this.usersService.incrementTotalRequests(user.id);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
70
src/rate-limit/user-rate-limit.service.ts
Normal file
70
src/rate-limit/user-rate-limit.service.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Injectable, OnModuleDestroy } from '@nestjs/common';
|
||||
|
||||
interface RateBucket {
|
||||
minuteCount: number;
|
||||
minuteResetAt: number;
|
||||
dayCount: number;
|
||||
dayResetAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* In-memory per-user rate counters.
|
||||
* Production: replace with Redis / sliding-window store for horizontal scaling.
|
||||
*/
|
||||
@Injectable()
|
||||
export class UserRateLimitService implements OnModuleDestroy {
|
||||
private readonly buckets = new Map<string, RateBucket>();
|
||||
private cleanupInterval: ReturnType<typeof setInterval>;
|
||||
|
||||
constructor() {
|
||||
this.cleanupInterval = setInterval(() => this.cleanup(), 60_000);
|
||||
}
|
||||
|
||||
onModuleDestroy(): void {
|
||||
clearInterval(this.cleanupInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the request is allowed; increments counters when allowed.
|
||||
*/
|
||||
checkAndIncrement(userId: string, limitPerMinute: number, limitPerDay: number): boolean {
|
||||
const now = Date.now();
|
||||
let bucket = this.buckets.get(userId);
|
||||
|
||||
if (!bucket) {
|
||||
bucket = {
|
||||
minuteCount: 0,
|
||||
minuteResetAt: now + 60_000,
|
||||
dayCount: 0,
|
||||
dayResetAt: now + 86_400_000,
|
||||
};
|
||||
this.buckets.set(userId, bucket);
|
||||
}
|
||||
|
||||
if (now >= bucket.minuteResetAt) {
|
||||
bucket.minuteCount = 0;
|
||||
bucket.minuteResetAt = now + 60_000;
|
||||
}
|
||||
if (now >= bucket.dayResetAt) {
|
||||
bucket.dayCount = 0;
|
||||
bucket.dayResetAt = now + 86_400_000;
|
||||
}
|
||||
|
||||
if (bucket.minuteCount >= limitPerMinute || bucket.dayCount >= limitPerDay) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bucket.minuteCount += 1;
|
||||
bucket.dayCount += 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
private cleanup(): void {
|
||||
const now = Date.now();
|
||||
for (const [userId, bucket] of this.buckets.entries()) {
|
||||
if (now > bucket.dayResetAt + 86_400_000) {
|
||||
this.buckets.delete(userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user