initial commit

This commit is contained in:
2026-06-09 14:07:37 +03:30
parent 30ac533800
commit 996a4fcda7
121 changed files with 20557 additions and 3 deletions

View 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 {}

View 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;
}
}

View 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);
}
}
}
}