Skip to content
Open
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
15 changes: 11 additions & 4 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Module } from '@nestjs/common';
import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
Expand All @@ -13,6 +13,7 @@ import { APP_GUARD } from '@nestjs/core';
import { HealthModule } from './health/health.module';
import { LoggerModule } from './logger/logger.module';
import { QueueModule } from './queue/queue.module';
import { LoggerMiddleware } from './common/middleware/logger.middleware';

@Module({
imports: [
Expand All @@ -21,8 +22,8 @@ import { QueueModule } from './queue/queue.module';
}),
ThrottlerModule.forRoot([
{
ttl: 60000, // 60 seconds in milliseconds
limit: 100, // 100 requests per 60 seconds
ttl: 60000,
limit: 100,
},
]),
LoggerModule,
Expand All @@ -44,4 +45,10 @@ import { QueueModule } from './queue/queue.module';
},
],
})
export class AppModule {}
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes({ path: '*', method: RequestMethod.ALL });
}
}
97 changes: 97 additions & 0 deletions backend/src/common/middleware/logger.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import {
Injectable,
NestMiddleware,
LoggerService,
} from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { WinstonLogger } from '../logger/winston.logger';

/* ---------- sensitive field redaction ---------- */

const SENSITIVE_FIELDS = [
'password',
'password_confirmation',
'currentPassword',
'newPassword',
'token',
'accessToken',
'refreshToken',
'apiKey',
'secret',
'authorization',
'creditCard',
'ssn',
];

function redactSensitive(obj: Record<string, unknown>): Record<string, unknown> {
if (!obj || typeof obj !== 'object') return obj;
const redacted = { ...obj };
for (const key of Object.keys(redacted)) {
const lowerKey = key.toLowerCase();
if (SENSITIVE_FIELDS.some((s) => lowerKey.includes(s.toLowerCase()))) {
(redacted as any)[key] = '[REDACTED]';
} else if (typeof (redacted as any)[key] === 'object' && (redacted as any)[key] !== null) {
(redacted as any)[key] = redactSensitive((redacted as any)[key] as Record<string, unknown>);
}
}
return redacted;
}

/* ---------- middleware ---------- */

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
private logger: WinstonLogger;

constructor() {
this.logger = new WinstonLogger('HTTP');
}

use(req: Request, res: Response, next: NextFunction): void {
const start = Date.now();
const { method, originalUrl, ip, headers, body } = req;

// Capture response finish
res.on('finish', () => {
const duration = Date.now() - start;
const statusCode = res.statusCode;
const contentLength = res.getHeader('content-length');

// Build safe log entry
const logData: Record<string, unknown> = {
method,
url: originalUrl,
ip: this.extractIp(req),
statusCode,
durationMs: duration,
contentLength: contentLength ?? 0,
userAgent: headers['user-agent']?.substring(0, 120),
};

// Include body for non-GET/DELETE requests (redacted)
if (!['GET', 'HEAD', 'OPTIONS'].includes(method) && body && Object.keys(body).length > 0) {
logData.body = redactSensitive(body as Record<string, unknown>);
}

// Log level based on status code
const level = statusCode >= 500 ? 'error' : statusCode >= 400 ? 'warn' : 'info';
this.logger[level]?.(
`${method} ${originalUrl} - ${statusCode} - ${duration}ms`,
'HTTP',
);
});

next();
}

/**
* Extract client IP, respecting reverse proxy headers.
*/
private extractIp(req: Request): string {
const forwarded = req.headers['x-forwarded-for'];
if (typeof forwarded === 'string') {
return forwarded.split(',')[0].trim();
}
return req.socket.remoteAddress || req.ip || 'unknown';
}
}