Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
parseDuration,
randomBase32Secret,
randomToken,
redactEmail,
sanitizeUser,
verifyBackupCode,
verifyTotpCode,
Expand Down
7 changes: 4 additions & 3 deletions src/auth/login-rate-limit.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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'})`,
);
}

Expand All @@ -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'})`);
}

/**
Expand Down Expand Up @@ -153,7 +154,7 @@ export class LoginRateLimitService {
},
});

this.logger.log(`Account manually unlocked: ${email}`);
this.logger.log(`Account manually unlocked: ${redactEmail(email)}`);
}

/**
Expand Down
4 changes: 4 additions & 0 deletions src/auth/security.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ export function sanitizeUser<T extends Record<string, unknown>>(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');
}
Expand Down
19 changes: 19 additions & 0 deletions src/users/users.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, { count: number; resetAt: number }>();
private readonly reactivateRateLimitMap = new Map<string, { count: number; resetAt: number }>();
private static readonly DOWNLOAD_LIMIT = 10;
private static readonly DOWNLOAD_WINDOW_MS = 60 * 60 * 1000;

Expand Down Expand Up @@ -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);
}

Expand Down
14 changes: 12 additions & 2 deletions src/users/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down
Loading