diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 6c6e9c99..64767079 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -36,6 +36,7 @@ import { parseDuration, randomBase32Secret, randomToken, + redactEmail, sanitizeUser, verifyBackupCode, verifyTotpCode, diff --git a/src/auth/login-rate-limit.service.ts b/src/auth/login-rate-limit.service.ts index ecda175e..4ed28bbe 100644 --- a/src/auth/login-rate-limit.service.ts +++ b/src/auth/login-rate-limit.service.ts @@ -2,6 +2,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { PrismaService } from '../database/prisma.service'; +import { redactEmail } from './security.utils'; export interface LoginAttemptConfig { maxAttempts: number; @@ -85,7 +86,7 @@ export class LoginRateLimitService { if (shouldLock) { this.logger.warn( - `Account locked due to too many failed login attempts: ${email} (IP: ${ipAddress || 'unknown'})`, + `Account locked due to too many failed login attempts: ${redactEmail(email)} (IP: ${ipAddress || 'unknown'})`, ); } @@ -110,7 +111,7 @@ export class LoginRateLimitService { }, }); - this.logger.log(`Successful login: ${email} (IP: ${ipAddress || 'unknown'})`); + this.logger.log(`Successful login: ${redactEmail(email)} (IP: ${ipAddress || 'unknown'})`); } /** @@ -153,7 +154,7 @@ export class LoginRateLimitService { }, }); - this.logger.log(`Account manually unlocked: ${email}`); + this.logger.log(`Account manually unlocked: ${redactEmail(email)}`); } /** diff --git a/src/auth/security.utils.ts b/src/auth/security.utils.ts index 119bb0ce..e3b1bd5d 100644 --- a/src/auth/security.utils.ts +++ b/src/auth/security.utils.ts @@ -33,6 +33,10 @@ export function sanitizeUser>(user: T) { return safeUser; } +export function redactEmail(email: string): string { + return createSha256(email).slice(0, 8); +} + export function createSha256(input: string): string { return createHash('sha256').update(input).digest('hex'); } diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index d8647610..cf83b676 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -39,10 +39,13 @@ import { DeactivateAccountDto, ReactivateAccountDto } from './dto/deactivation.d import { UpdateProfileDto } from './dto/update-profile.dto'; const UNAUTHORIZED_ACTION_MESSAGE = 'You are not authorized to perform this action'; +const REACTIVATE_LIMIT = 5; +const REACTIVATE_WINDOW_MS = 60 * 60 * 1000; @Controller('users') export class UsersController { private readonly downloadRateLimitMap = new Map(); + private readonly reactivateRateLimitMap = new Map(); private static readonly DOWNLOAD_LIMIT = 10; private static readonly DOWNLOAD_WINDOW_MS = 60 * 60 * 1000; @@ -243,6 +246,22 @@ export class UsersController { @CurrentUser() user: AuthUserPayload, @Body() reactivateDto: ReactivateAccountDto, ) { + const emailLower = user.email.toLowerCase(); + const now = Date.now(); + const entry = this.reactivateRateLimitMap.get(emailLower); + + if (entry && now < entry.resetAt) { + if (entry.count >= REACTIVATE_LIMIT) { + throw new HttpException('Too many reactivation attempts. Try again later.', HttpStatus.TOO_MANY_REQUESTS); + } + entry.count++; + } else { + this.reactivateRateLimitMap.set(emailLower, { + count: 1, + resetAt: now + REACTIVATE_WINDOW_MS, + }); + } + return this.usersService.reactivate(user.sub, reactivateDto); } diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 01de3cd7..8b3d5ee2 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -477,7 +477,7 @@ export class UsersService implements OnModuleInit { }); this.logger.log( - `User ${userId} (${user.email}) deactivated. Scheduled deletion: ${scheduledDeletionAt ? scheduledDeletionAt.toISOString() : 'None'}`, + `User ${userId} (${redactEmail(user.email)}) deactivated. Scheduled deletion: ${scheduledDeletionAt ? scheduledDeletionAt.toISOString() : 'None'}`, ); await this.sessionsService.revokeAllSessions(userId); @@ -577,7 +577,17 @@ export class UsersService implements OnModuleInit { }, }); - this.logger.log(`User ${userId} (${user.email}) reactivated`); + await this.prisma.activityLog.create({ + data: { + userId, + action: 'REACTIVATE', + entityType: 'USER', + entityId: userId, + description: `Account reactivated`, + }, + }); + + this.logger.log(`User ${userId} (${redactEmail(user.email)}) reactivated`); return updatedUser; }