|
// src/modules/security/application/services/security.service.ts |
|
import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; |
|
import { InjectRepository } from '@nestjs/typeorm'; |
|
import { Repository } from 'typeorm'; |
|
import { IpRateLimit } from '../../infrastructure/persistence/entities/ip-rate-limit.entity'; |
|
import { OtpRequestTracking } from '../../infrastructure/persistence/entities/otp-request-tracking.entity'; |
|
import { ConfigService } from '@nestjs/config'; |
|
import { Request } from 'express'; |
|
|
|
interface TrackOtpRequestParams { |
|
phoneNumber: string; |
|
ipAddress: string; |
|
userAgent?: string; |
|
success: boolean; |
|
} |
|
|
|
interface ValidateOtpRequestParams { |
|
phoneNumber: string; |
|
ipAddress: string; |
|
endpoint: string; |
|
} |
|
|
|
@Injectable() |
|
export class SecurityService { |
|
constructor( |
|
@InjectRepository(IpRateLimit) |
|
private ipRateLimitRepository: Repository<IpRateLimit>, |
|
@InjectRepository(OtpRequestTracking) |
|
private otpRequestTrackingRepository: Repository<OtpRequestTracking>, |
|
private configService: ConfigService, |
|
) {} |
|
|
|
/** |
|
* Advanced check for suspicious OTP request patterns |
|
*/ |
|
async validateOtpRequest(params: ValidateOtpRequestParams): Promise<boolean> { |
|
const { phoneNumber, ipAddress, endpoint } = params; |
|
|
|
// 1. Check suspicious patterns - multiple phone numbers from same IP |
|
const phoneNumbersFromIp = await this.otpRequestTrackingRepository |
|
.createQueryBuilder('tracking') |
|
.select('DISTINCT tracking.phoneNumber') |
|
.where('tracking.ipAddress = :ipAddress', { ipAddress }) |
|
.andWhere('tracking.createdAt > :date', { |
|
date: new Date(Date.now() - 24 * 60 * 60 * 1000) // last 24 hours |
|
}) |
|
.getCount(); |
|
|
|
const maxPhonesPerIp = this.configService.get<number>('security.maxPhonesPerIp', 3); |
|
if (phoneNumbersFromIp >= maxPhonesPerIp) { |
|
// Block this IP for suspicious activity |
|
await this.blockIpAddress(ipAddress, endpoint); |
|
throw new HttpException( |
|
'Security check failed: Too many different phone numbers from this IP', |
|
HttpStatus.TOO_MANY_REQUESTS, |
|
); |
|
} |
|
|
|
// 2. Check for verification success rate (fraud prevention) |
|
const totalRequests = await this.otpRequestTrackingRepository.count({ |
|
where: { |
|
ipAddress, |
|
createdAt: new Date(Date.now() - 24 * 60 * 60 * 1000), // last 24 hours |
|
}, |
|
}); |
|
|
|
const verifiedRequests = await this.otpRequestTrackingRepository.count({ |
|
where: { |
|
ipAddress, |
|
otpVerified: true, |
|
createdAt: new Date(Date.now() - 24 * 60 * 60 * 1000), // last 24 hours |
|
}, |
|
}); |
|
|
|
// If more than 10 requests and less than 10% verification rate, it's likely spam |
|
if (totalRequests > 10 && verifiedRequests / totalRequests < 0.1) { |
|
await this.blockIpAddress(ipAddress, endpoint); |
|
throw new HttpException( |
|
'Security check failed: Unusual activity detected', |
|
HttpStatus.TOO_MANY_REQUESTS, |
|
); |
|
} |
|
|
|
return true; |
|
} |
|
|
|
/** |
|
* Track OTP request for analysis and fraud prevention |
|
*/ |
|
async trackOtpRequest(params: TrackOtpRequestParams): Promise<void> { |
|
const { phoneNumber, ipAddress, userAgent, success } = params; |
|
|
|
await this.otpRequestTrackingRepository.save({ |
|
phoneNumber, |
|
ipAddress, |
|
userAgent, |
|
requestSuccess: success, |
|
otpVerified: false, // will be updated when/if OTP is verified |
|
}); |
|
} |
|
|
|
/** |
|
* Mark an OTP as verified for a specific phone number |
|
*/ |
|
async markOtpVerified(phoneNumber: string): Promise<void> { |
|
// Find the most recent OTP request for this phone number |
|
const latestRequest = await this.otpRequestTrackingRepository.findOne({ |
|
where: { phoneNumber }, |
|
order: { createdAt: 'DESC' }, |
|
}); |
|
|
|
if (latestRequest) { |
|
latestRequest.otpVerified = true; |
|
await this.otpRequestTrackingRepository.save(latestRequest); |
|
} |
|
} |
|
|
|
/** |
|
* Extract IP and client info from request |
|
*/ |
|
getClientInfo(req: Request): { ipAddress: string; userAgent: string } { |
|
// Get IP from various headers that might be set by proxies |
|
const forwarded = req.headers['x-forwarded-for']; |
|
let ipAddress: string; |
|
|
|
if (forwarded) { |
|
ipAddress = Array.isArray(forwarded) ? forwarded[0] : forwarded.split(/, /)[0]; |
|
} else { |
|
const realIp = req.headers['x-real-ip']; |
|
if (realIp) { |
|
ipAddress = Array.isArray(realIp) ? realIp[0] : realIp; |
|
} else { |
|
ipAddress = req.ip || '0.0.0.0'; |
|
} |
|
} |
|
|
|
const userAgent = req.headers['user-agent'] || ''; |
|
|
|
return { ipAddress, userAgent: userAgent.toString() }; |
|
} |
|
|
|
/** |
|
* Block an IP address for a specified endpoint |
|
*/ |
|
private async blockIpAddress(ipAddress: string, endpoint: string, durationHours = 24): Promise<void> { |
|
let rateLimit = await this.ipRateLimitRepository.findOne({ |
|
where: { ipAddress, endpoint }, |
|
}); |
|
|
|
const now = new Date(); |
|
|
|
if (!rateLimit) { |
|
rateLimit = this.ipRateLimitRepository.create({ |
|
ipAddress, |
|
endpoint, |
|
requestCount: 999, // Arbitrary high number |
|
firstRequestAt: now, |
|
lastRequestAt: now, |
|
}); |
|
} |
|
|
|
rateLimit.blockedUntil = new Date(now.getTime() + durationHours * 60 * 60 * 1000); |
|
await this.ipRateLimitRepository.save(rateLimit); |
|
} |
|
|
|
/** |
|
* Get detailed rate limiting stats for an IP |
|
*/ |
|
async getIpStats(ipAddress: string): Promise<any> { |
|
const rateLimits = await this.ipRateLimitRepository.find({ |
|
where: { ipAddress }, |
|
}); |
|
|
|
const otpStats = await this.otpRequestTrackingRepository |
|
.createQueryBuilder('tracking') |
|
.select('COUNT(*)', 'totalRequests') |
|
.addSelect('SUM(CASE WHEN tracking.otpVerified = true THEN 1 ELSE 0 END)', 'verifiedRequests') |
|
.where('tracking.ipAddress = :ipAddress', { ipAddress }) |
|
.andWhere('tracking.createdAt > :date', { |
|
date: new Date(Date.now() - 24 * 60 * 60 * 1000), // last 24 hours |
|
}) |
|
.getRawOne(); |
|
|
|
return { |
|
rateLimits, |
|
otpStats, |
|
}; |
|
} |
|
} |