Skip to content
Closed
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
7 changes: 6 additions & 1 deletion src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ThrottlerModule } from '@nestjs/throttler';
import { ScheduleModule } from '@nestjs/schedule';
import { TerminusModule } from '@nestjs/terminus';
import { BullModule } from '@nestjs/bull';
import { APP_INTERCEPTOR, APP_GUARD } from '@nestjs/core';
import { APP_INTERCEPTOR, APP_GUARD, APP_FILTER } from '@nestjs/core';

// Core & Database
import { PrismaModule } from './database/prisma/prisma.module';
Expand All @@ -19,6 +19,7 @@ import { CacheModule } from './common/cache/cache.module';
// Logging
import { LoggingModule } from './common/logging/logging.module';
import { LoggingInterceptor } from './common/logging/logging.interceptor';
import { AllExceptionsFilter } from './common/errors/error.filter';

// Redis
import { RedisModule } from './common/services/redis.module';
Expand Down Expand Up @@ -112,6 +113,10 @@ import { AuthRateLimitMiddleware } from './auth/middleware/auth.middleware';
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
{
provide: APP_FILTER,
useClass: AllExceptionsFilter,
},
],
})
export class AppModule implements NestModule {
Expand Down
2 changes: 1 addition & 1 deletion src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class AuthController {
async login(@Body() loginDto: LoginDto, @Req() req: Request) {
return this.authService.login({
email: loginDto.email,
password: loginDto.password
password: loginDto.password,
});
}

Expand Down
32 changes: 18 additions & 14 deletions src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ export class AuthService {
}
}
}

// Remove refresh token
await this.redisService.del(`refresh_token:${userId}`);
this.logger.logAuth('User logged out successfully', { userId });
Expand Down Expand Up @@ -251,14 +251,14 @@ export class AuthService {
async getActiveSessions(userId: string): Promise<any[]> {
const sessionKeys = await this.redisService.keys(`active_session:${userId}:*`);
const sessions = [];

for (const key of sessionKeys) {
const sessionData = await this.redisService.get(key);
if (sessionData) {
sessions.push(JSON.parse(sessionData));
}
}

return sessions;
}

Expand All @@ -272,7 +272,7 @@ export class AuthService {
return sessions.map(session => ({
...session,
isActive: true,
expiresIn: this.getSessionExpiry(session.createdAt)
expiresIn: this.getSessionExpiry(session.createdAt),
}));
}

Expand Down Expand Up @@ -303,10 +303,10 @@ export class AuthService {

private generateTokens(user: any) {
const jti = uuidv4(); // JWT ID for blacklisting
const payload = {
sub: user.id,
const payload = {
sub: user.id,
email: user.email,
jti: jti
jti,
};

const accessToken = this.jwtService.sign(payload, {
Expand All @@ -320,15 +320,19 @@ export class AuthService {
});

this.redisService.set(`refresh_token:${user.id}`, refreshToken);

// Store active session
const sessionExpiry = this.configService.get<number>('SESSION_TIMEOUT', 3600);
this.redisService.setex(`active_session:${user.id}:${jti}`, sessionExpiry, JSON.stringify({
userId: user.id,
createdAt: new Date().toISOString(),
userAgent: 'unknown', // Would be captured from request in real implementation
ip: 'unknown'
}));
this.redisService.setex(
`active_session:${user.id}:${jti}`,
sessionExpiry,
JSON.stringify({
userId: user.id,
createdAt: new Date().toISOString(),
userAgent: 'unknown', // Would be captured from request in real implementation
ip: 'unknown',
}),
);

this.logger.debug('Generated new tokens for user', { userId: user.id, jti });

Expand Down
6 changes: 3 additions & 3 deletions src/auth/guards/jwt-auth.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ export class JwtAuthGuard extends AuthGuard('jwt') {

async canActivate(context: any): Promise<boolean> {
const result = (await super.canActivate(context)) as boolean;

if (result) {
const request = context.switchToHttp().getRequest();
const user = request.user;

// Check if token is blacklisted
if (user && user.jti) {
const isBlacklisted = await this.authService.isTokenBlacklisted(user.jti);
Expand All @@ -23,7 +23,7 @@ export class JwtAuthGuard extends AuthGuard('jwt') {
}
}
}

return result;
}
}
8 changes: 4 additions & 4 deletions src/auth/guards/login-attempts.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ export class LoginAttemptsGuard extends AuthGuard('local') {

try {
const result = (await super.canActivate(context)) as boolean;

if (result) {
// Successful login - reset attempt counters
await this.resetLoginAttempts(email, ip);
this.logger.logAuth('Successful login', { email, ip });
}

return result;
} catch (error) {
// Failed login - increment attempt counters
Expand Down Expand Up @@ -73,7 +73,7 @@ export class LoginAttemptsGuard extends AuthGuard('local') {

// Increment email attempts
await this.incrementLoginAttempts(`login_attempts:${email}`, lockoutDuration);

// Increment IP attempts
await this.incrementLoginAttempts(`login_attempts:ip:${ip}`, lockoutDuration);
}
Expand All @@ -96,4 +96,4 @@ export class LoginAttemptsGuard extends AuthGuard('local') {
private getClientIp(request: any): string {
return request.ips?.length ? request.ips[0] : request.ip;
}
}
}
2 changes: 1 addition & 1 deletion src/auth/mfa/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from './mfa.service';
export * from './mfa.controller';
export * from './mfa.module';
export * from './mfa.module';
12 changes: 6 additions & 6 deletions src/auth/mfa/mfa.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,16 @@ export class MfaController {
async verifyMfa(@Req() req: Request, @Body('token') token: string) {
const user = req['user'] as any;
const verified = await this.mfaService.verifyMfaSetup(user.id, token);

if (verified) {
// Generate backup codes after successful setup
const backupCodes = await this.mfaService.generateBackupCodes(user.id);
return {
message: 'MFA setup completed successfully',
backupCodes
backupCodes,
};
}

throw new Error('Invalid MFA token');
}

Expand Down Expand Up @@ -82,11 +82,11 @@ export class MfaController {
async verifyBackupCode(@Req() req: Request, @Body('code') code: string) {
const user = req['user'] as any;
const verified = await this.mfaService.verifyBackupCode(user.id, code);

if (!verified) {
throw new Error('Invalid backup code');
}

return { message: 'Backup code verified successfully' };
}
}
}
2 changes: 1 addition & 1 deletion src/auth/mfa/mfa.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ import { MfaController } from './mfa.controller';
providers: [MfaService],
exports: [MfaService],
})
export class MfaModule {}
export class MfaModule {}
30 changes: 15 additions & 15 deletions src/auth/mfa/mfa.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class MfaService {
// Generate a new secret
const secret = speakeasy.generateSecret({
name: `PropChain (${email})`,
issuer: 'PropChain'
issuer: 'PropChain',
});

// Generate QR code for authenticator apps
Expand All @@ -30,25 +30,25 @@ export class MfaService {
await this.redisService.setex(`mfa_setup:${userId}`, expiry, secret.base32);

this.logger.logAuth('MFA secret generated', { userId });

return {
secret: secret.base32,
qrCode
qrCode,
};
}

async verifyMfaSetup(userId: string, token: string): Promise<boolean> {
const secret = await this.redisService.get(`mfa_setup:${userId}`);

if (!secret) {
throw new BadRequestException('MFA setup session expired or not found');
}

const verified = speakeasy.totp.verify({
secret: secret,
secret,
encoding: 'base32',
token: token,
window: 2 // Allow 2 time periods of tolerance
token,
window: 2, // Allow 2 time periods of tolerance
});

if (verified) {
Expand All @@ -65,16 +65,16 @@ export class MfaService {

async verifyMfaToken(userId: string, token: string): Promise<boolean> {
const secret = await this.redisService.get(`mfa_secret:${userId}`);

if (!secret) {
throw new UnauthorizedException('MFA not enabled for this user');
}

const verified = speakeasy.totp.verify({
secret: secret,
secret,
encoding: 'base32',
token: token,
window: 2
token,
window: 2,
});

if (verified) {
Expand Down Expand Up @@ -118,14 +118,14 @@ export class MfaService {

async verifyBackupCode(userId: string, code: string): Promise<boolean> {
const codesData = await this.redisService.get(`mfa_backup_codes:${userId}`);

if (!codesData) {
return false;
}

const codes = JSON.parse(codesData);
const index = codes.indexOf(code.toUpperCase());

if (index !== -1) {
// Remove used code
codes.splice(index, 1);
Expand All @@ -144,7 +144,7 @@ export class MfaService {

return {
enabled,
hasBackupCodes
hasBackupCodes,
};
}
}
}
36 changes: 25 additions & 11 deletions src/common/errors/error.filter.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, Logger, Inject } from '@nestjs/common';
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Request, Response } from 'express';
import { ErrorResponseDto } from './error.dto';
import { ErrorCode, ErrorMessages } from './error.codes';
import { v4 as uuidv4 } from 'uuid';
import { LoggerService } from '../logger/logger.service';
import { StructuredLoggerService } from '../logging/logger.service';
import { ErrorMonitoringService } from '../logging/error-monitoring.service';
import { getCorrelationId, getTraceId } from '../logging/correlation-id';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
private readonly logger = new Logger(AllExceptionsFilter.name);

constructor(
@Inject(ConfigService) private readonly configService?: ConfigService,
@Inject(StructuredLoggerService) private readonly loggerService?: StructuredLoggerService,
private readonly configService: ConfigService,
private readonly loggerService: StructuredLoggerService,
private readonly errorMonitoringService: ErrorMonitoringService,
) {}

catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const requestId = (request.headers['x-request-id'] as string) || uuidv4();
const correlationId = getCorrelationId();
const requestId = correlationId || (request.headers['x-request-id'] as string) || uuidv4();

let errorResponse: ErrorResponseDto;

Expand All @@ -30,14 +31,27 @@ export class AllExceptionsFilter implements ExceptionFilter {
errorResponse = this.handleUnknownException(exception, request, requestId);
}

// Log the error
this.logger.error(`Error occurred: ${errorResponse.errorCode} - ${errorResponse.message}`, {
this.loggerService.error(
`Error occurred: ${errorResponse.errorCode} - ${errorResponse.message}`,
exception instanceof Error ? exception.stack : undefined,
{
requestId,
correlationId,
traceId: getTraceId(),
path: request.url,
method: request.method,
statusCode: errorResponse.statusCode,
details: errorResponse.details,
},
);

this.errorMonitoringService.captureException(exception, {
requestId,
correlationId,
traceId: getTraceId(),
path: request.url,
method: request.method,
statusCode: errorResponse.statusCode,
details: errorResponse.details,
stack: exception instanceof Error ? exception.stack : undefined,
});

response.status(errorResponse.statusCode).json(errorResponse);
Expand Down
Loading
Loading