diff --git a/src/app.module.ts b/src/app.module.ts index 11e12776..91fe4573 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -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'; @@ -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'; @@ -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 { diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index ba32e340..48b03010 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -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, }); } diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index e995958b..e343b102 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -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 }); @@ -251,14 +251,14 @@ export class AuthService { async getActiveSessions(userId: string): Promise { 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; } @@ -272,7 +272,7 @@ export class AuthService { return sessions.map(session => ({ ...session, isActive: true, - expiresIn: this.getSessionExpiry(session.createdAt) + expiresIn: this.getSessionExpiry(session.createdAt), })); } @@ -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, { @@ -320,15 +320,19 @@ export class AuthService { }); this.redisService.set(`refresh_token:${user.id}`, refreshToken); - + // Store active session const sessionExpiry = this.configService.get('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 }); diff --git a/src/auth/guards/jwt-auth.guard.ts b/src/auth/guards/jwt-auth.guard.ts index ea938e2e..93c7b6fd 100644 --- a/src/auth/guards/jwt-auth.guard.ts +++ b/src/auth/guards/jwt-auth.guard.ts @@ -10,11 +10,11 @@ export class JwtAuthGuard extends AuthGuard('jwt') { async canActivate(context: any): Promise { 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); @@ -23,7 +23,7 @@ export class JwtAuthGuard extends AuthGuard('jwt') { } } } - + return result; } } diff --git a/src/auth/guards/login-attempts.guard.ts b/src/auth/guards/login-attempts.guard.ts index fb50db9d..3857a8fd 100644 --- a/src/auth/guards/login-attempts.guard.ts +++ b/src/auth/guards/login-attempts.guard.ts @@ -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 @@ -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); } @@ -96,4 +96,4 @@ export class LoginAttemptsGuard extends AuthGuard('local') { private getClientIp(request: any): string { return request.ips?.length ? request.ips[0] : request.ip; } -} \ No newline at end of file +} diff --git a/src/auth/mfa/index.ts b/src/auth/mfa/index.ts index 61976022..2102a95e 100644 --- a/src/auth/mfa/index.ts +++ b/src/auth/mfa/index.ts @@ -1,3 +1,3 @@ export * from './mfa.service'; export * from './mfa.controller'; -export * from './mfa.module'; \ No newline at end of file +export * from './mfa.module'; diff --git a/src/auth/mfa/mfa.controller.ts b/src/auth/mfa/mfa.controller.ts index 8412f9bc..1adf1889 100644 --- a/src/auth/mfa/mfa.controller.ts +++ b/src/auth/mfa/mfa.controller.ts @@ -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'); } @@ -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' }; } -} \ No newline at end of file +} diff --git a/src/auth/mfa/mfa.module.ts b/src/auth/mfa/mfa.module.ts index 22a53cd3..fdf14606 100644 --- a/src/auth/mfa/mfa.module.ts +++ b/src/auth/mfa/mfa.module.ts @@ -7,4 +7,4 @@ import { MfaController } from './mfa.controller'; providers: [MfaService], exports: [MfaService], }) -export class MfaModule {} \ No newline at end of file +export class MfaModule {} diff --git a/src/auth/mfa/mfa.service.ts b/src/auth/mfa/mfa.service.ts index 6a2b3a5c..83ece6b3 100644 --- a/src/auth/mfa/mfa.service.ts +++ b/src/auth/mfa/mfa.service.ts @@ -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 @@ -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 { 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) { @@ -65,16 +65,16 @@ export class MfaService { async verifyMfaToken(userId: string, token: string): Promise { 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) { @@ -118,14 +118,14 @@ export class MfaService { async verifyBackupCode(userId: string, code: string): Promise { 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); @@ -144,7 +144,7 @@ export class MfaService { return { enabled, - hasBackupCodes + hasBackupCodes, }; } -} \ No newline at end of file +} diff --git a/src/common/errors/error.filter.ts b/src/common/errors/error.filter.ts index 4c950bd4..0fece2ea 100644 --- a/src/common/errors/error.filter.ts +++ b/src/common/errors/error.filter.ts @@ -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(); const request = ctx.getRequest(); - 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; @@ -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); diff --git a/src/common/logging/correlation-id.ts b/src/common/logging/correlation-id.ts index 362dcb81..6c90061e 100644 --- a/src/common/logging/correlation-id.ts +++ b/src/common/logging/correlation-id.ts @@ -1,33 +1,34 @@ import { createNamespace, Namespace } from 'cls-hooked'; -/** - * Manages correlation IDs for request tracking using async_hooks (via cls-hooked) - */ export const CORRELATION_ID_KEY = 'correlationId'; +export const TRACE_ID_KEY = 'traceId'; +export const SPAN_ID_KEY = 'spanId'; -// Create a namespace for correlation IDs const ns: Namespace = createNamespace('propchain-request'); -/** - * Get the correlation ID for the current request context - */ export const getCorrelationId = (): string | undefined => { return ns.get(CORRELATION_ID_KEY); }; -/** - * Run a function within a request context and set the correlation ID - */ +export const getTraceId = (): string | undefined => { + return ns.get(TRACE_ID_KEY); +}; + +export const getSpanId = (): string | undefined => { + return ns.get(SPAN_ID_KEY); +}; + export const withCorrelationId = (fn: () => void, correlationId: string): void => { ns.run(() => { ns.set(CORRELATION_ID_KEY, correlationId); + const existingTraceId = ns.get(TRACE_ID_KEY); + if (!existingTraceId) { + ns.set(TRACE_ID_KEY, correlationId); + } fn(); }); }; -/** - * Get the underlying namespace - */ export const getNamespace = (): Namespace => { return ns; }; diff --git a/src/common/logging/error-monitoring.service.ts b/src/common/logging/error-monitoring.service.ts new file mode 100644 index 00000000..7fe5c8d1 --- /dev/null +++ b/src/common/logging/error-monitoring.service.ts @@ -0,0 +1,36 @@ +import { Injectable } from '@nestjs/common'; +import { StructuredLoggerService } from './logger.service'; +import { MetricsService } from './metrics.service'; + +@Injectable() +export class ErrorMonitoringService { + private totalErrors = 0; + + constructor( + private readonly logger: StructuredLoggerService, + private readonly metricsService: MetricsService, + ) {} + + captureException(exception: unknown, metadata?: Record): void { + this.totalErrors += 1; + + const error = exception instanceof Error ? exception : new Error(String(exception)); + + this.logger.error(error.message, error.stack, { + ...metadata, + name: error.name, + type: 'exception', + }); + + this.metricsService.recordError('exception', { + ...metadata, + name: error.name, + }); + } + + getErrorStats(): Record { + return { + totalErrors: this.totalErrors, + }; + } +} diff --git a/src/common/logging/logger.service.ts b/src/common/logging/logger.service.ts index 172d84e5..6119e462 100644 --- a/src/common/logging/logger.service.ts +++ b/src/common/logging/logger.service.ts @@ -2,7 +2,7 @@ import { Injectable, Scope, LoggerService as NestLoggerService } from '@nestjs/c import * as winston from 'winston'; import { ConfigService } from '@nestjs/config'; import { createWinstonLogger, LOG_CATEGORIES } from './logging.config'; -import { getCorrelationId } from './correlation-id'; +import { getCorrelationId, getTraceId } from './correlation-id'; /** * Structured logging service with Winston @@ -162,13 +162,12 @@ export class StructuredLoggerService implements NestLoggerService { }); } - /** - * Build consistent metadata structure for all logs - */ private buildLogMetadata(metadata?: Record): Record { const correlationId = getCorrelationId(); + const traceId = getTraceId(); return { correlationId, + traceId, context: this.context, timestamp: new Date().toISOString(), ...metadata, diff --git a/src/common/logging/logging.interceptor.ts b/src/common/logging/logging.interceptor.ts index 5f9b6146..89e167dd 100644 --- a/src/common/logging/logging.interceptor.ts +++ b/src/common/logging/logging.interceptor.ts @@ -2,13 +2,14 @@ import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nes import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; import { StructuredLoggerService } from './logger.service'; +import { MetricsService } from './metrics.service'; -/** - * Interceptor for logging incoming requests and outgoing responses - */ @Injectable() export class LoggingInterceptor implements NestInterceptor { - constructor(private readonly logger: StructuredLoggerService) { + constructor( + private readonly logger: StructuredLoggerService, + private readonly metrics: MetricsService, + ) { this.logger.setContext('HTTP'); } @@ -17,7 +18,6 @@ export class LoggingInterceptor implements NestInterceptor { const { method, url, headers, body } = request; const requestStartTime = Date.now(); - // Log incoming request this.logger.logRequest(method, url, { userAgent: headers['user-agent'], body, @@ -31,12 +31,18 @@ export class LoggingInterceptor implements NestInterceptor { const duration = Date.now() - requestStartTime; this.logger.logResponse(method, url, statusCode, duration); + this.metrics.recordHttpRequest(method, url, statusCode, duration); }, error: (error: any) => { const duration = Date.now() - requestStartTime; this.logger.error(`${method} ${url} Failed in ${duration}ms`, error.stack, { error, }); + this.metrics.recordHttpRequest(method, url, 500, duration); + this.metrics.recordError('http_request', { + method, + url, + }); }, }), ); diff --git a/src/common/logging/logging.middleware.ts b/src/common/logging/logging.middleware.ts index 26a90ec7..f4d63b91 100644 --- a/src/common/logging/logging.middleware.ts +++ b/src/common/logging/logging.middleware.ts @@ -3,15 +3,12 @@ import { Request, Response, NextFunction } from 'express'; import { v4 as uuidv4 } from 'uuid'; import { withCorrelationId } from './correlation-id'; -/** - * Middleware to generate and manage correlation IDs for request tracking - * Assigns a unique correlation ID to every incoming request - */ @Injectable() export class LoggingMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { const correlationId = (req.headers['x-correlation-id'] as string) || uuidv4(); res.setHeader('x-correlation-id', correlationId); + res.setHeader('x-trace-id', correlationId); withCorrelationId(() => { next(); diff --git a/src/common/logging/logging.module.ts b/src/common/logging/logging.module.ts index 86c44f1e..820aaa9f 100644 --- a/src/common/logging/logging.module.ts +++ b/src/common/logging/logging.module.ts @@ -2,12 +2,14 @@ import { Module, Global, MiddlewareConsumer, NestModule } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { StructuredLoggerService } from './logger.service'; import { LoggingMiddleware } from './logging.middleware'; +import { MetricsService } from './metrics.service'; +import { ErrorMonitoringService } from './error-monitoring.service'; @Global() @Module({ imports: [ConfigModule], - providers: [StructuredLoggerService], - exports: [StructuredLoggerService], + providers: [StructuredLoggerService, MetricsService, ErrorMonitoringService], + exports: [StructuredLoggerService, MetricsService, ErrorMonitoringService], }) export class LoggingModule implements NestModule { configure(consumer: MiddlewareConsumer) { diff --git a/src/common/logging/metrics.service.ts b/src/common/logging/metrics.service.ts new file mode 100644 index 00000000..e8d96808 --- /dev/null +++ b/src/common/logging/metrics.service.ts @@ -0,0 +1,90 @@ +import { Injectable } from '@nestjs/common'; + +interface HttpRequestMetricKey { + method: string; + path: string; +} + +interface HttpRequestMetricValue { + count: number; + totalDurationMs: number; + minDurationMs: number; + maxDurationMs: number; + statusCounts: Record; +} + +interface ErrorMetricValue { + count: number; +} + +@Injectable() +export class MetricsService { + private httpRequestMetrics = new Map(); + private errorMetrics = new Map(); + + recordHttpRequest(method: string, path: string, statusCode: number, durationMs: number): void { + const key: HttpRequestMetricKey = { method, path }; + const mapKey = this.buildHttpKey(key); + const existing = this.httpRequestMetrics.get(mapKey); + + if (!existing) { + this.httpRequestMetrics.set(mapKey, { + count: 1, + totalDurationMs: durationMs, + minDurationMs: durationMs, + maxDurationMs: durationMs, + statusCounts: { [statusCode]: 1 }, + }); + return; + } + + existing.count += 1; + existing.totalDurationMs += durationMs; + existing.minDurationMs = Math.min(existing.minDurationMs, durationMs); + existing.maxDurationMs = Math.max(existing.maxDurationMs, durationMs); + existing.statusCounts[statusCode] = (existing.statusCounts[statusCode] || 0) + 1; + } + + recordError(type: string, metadata?: Record): void { + const key = type; + const existing = this.errorMetrics.get(key); + + if (!existing) { + this.errorMetrics.set(key, { count: 1 }); + return; + } + + existing.count += 1; + } + + getMetrics(): Record { + const http: Record = {}; + + for (const [key, value] of this.httpRequestMetrics.entries()) { + http[key] = { + count: value.count, + avgDurationMs: value.count > 0 ? value.totalDurationMs / value.count : 0, + minDurationMs: value.minDurationMs, + maxDurationMs: value.maxDurationMs, + statusCounts: value.statusCounts, + }; + } + + const errors: Record = {}; + + for (const [key, value] of this.errorMetrics.entries()) { + errors[key] = { + count: value.count, + }; + } + + return { + http, + errors, + }; + } + + private buildHttpKey(key: HttpRequestMetricKey): string { + return `${key.method.toUpperCase()} ${key.path}`; + } +} diff --git a/src/common/validators/password.validator.ts b/src/common/validators/password.validator.ts index 6845cb66..54f500fb 100644 --- a/src/common/validators/password.validator.ts +++ b/src/common/validators/password.validator.ts @@ -7,7 +7,7 @@ export class PasswordValidator { validatePassword(password: string): { valid: boolean; errors: string[] } { const errors: string[] = []; - + // Length validation const minLength = this.configService.get('PASSWORD_MIN_LENGTH', 12); if (password.length < minLength) { @@ -39,14 +39,7 @@ export class PasswordValidator { } // Common password patterns to avoid - const commonPatterns = [ - /password/i, - /123456/, - /qwerty/, - /abc123/, - /admin/, - /welcome/ - ]; + const commonPatterns = [/password/i, /123456/, /qwerty/, /abc123/, /admin/, /welcome/]; for (const pattern of commonPatterns) { if (pattern.test(password)) { @@ -57,7 +50,7 @@ export class PasswordValidator { return { valid: errors.length === 0, - errors + errors, }; } @@ -70,4 +63,4 @@ export class PasswordValidator { const { errors } = this.validatePassword(password); return errors.join(', ') || 'Password is valid'; } -} \ No newline at end of file +} diff --git a/src/config/interfaces/joi-schema-config.interface.ts b/src/config/interfaces/joi-schema-config.interface.ts index fc62b3fc..a83375b6 100644 --- a/src/config/interfaces/joi-schema-config.interface.ts +++ b/src/config/interfaces/joi-schema-config.interface.ts @@ -71,7 +71,7 @@ export interface JoiSchemaConfig { // Security BCRYPT_ROUNDS: number; SESSION_SECRET: string; - + // Password Security PASSWORD_MIN_LENGTH: number; PASSWORD_REQUIRE_SPECIAL_CHARS: boolean; @@ -79,7 +79,7 @@ export interface JoiSchemaConfig { PASSWORD_REQUIRE_UPPERCASE: boolean; PASSWORD_HISTORY_COUNT: number; PASSWORD_EXPIRY_DAYS: number; - + // Authentication Security JWT_BLACKLIST_ENABLED: boolean; LOGIN_MAX_ATTEMPTS: number; diff --git a/src/health/health.controller.ts b/src/health/health.controller.ts index 40dc0654..db8605d9 100644 --- a/src/health/health.controller.ts +++ b/src/health/health.controller.ts @@ -4,6 +4,7 @@ import { HealthCheck, HealthCheckService, HttpHealthIndicator } from '@nestjs/te import { DatabaseHealthIndicator } from './indicators/database.health'; import { RedisHealthIndicator } from './indicators/redis.health'; import { BlockchainHealthIndicator } from './indicators/blockchain.health'; +import { MetricsService } from '../common/logging/metrics.service'; @ApiTags('health') @Controller('health') @@ -14,6 +15,7 @@ export class HealthController { private dbHealth: DatabaseHealthIndicator, private redisHealth: RedisHealthIndicator, private blockchainHealth: BlockchainHealthIndicator, + private readonly metricsService: MetricsService, ) {} @Get() @@ -61,4 +63,12 @@ export class HealthController { readiness() { return this.health.check([() => this.dbHealth.isHealthy('database'), () => this.redisHealth.isHealthy('redis')]); } + + @Get('metrics') + @HealthCheck() + @ApiOperation({ summary: 'Application performance metrics' }) + @ApiResponse({ status: 200, description: 'Current metrics snapshot' }) + metrics() { + return this.metricsService.getMetrics(); + } } diff --git a/src/main.ts b/src/main.ts index 225759da..1cc6446a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,14 +5,7 @@ import { ConfigService } from '@nestjs/config'; import helmet from 'helmet'; import * as compression from 'compression'; import { AppModule } from './app.module'; - -// --- NEW LOGGING IMPORTS --- import { StructuredLoggerService } from './common/logging/logger.service'; -import { LoggingInterceptor } from './common/logging/logging.interceptor'; -// --------------------------- - -// FIX: Corrected import name from AppExceptionFilter to AllExceptionsFilter -import { AllExceptionsFilter } from './common/errors/error.filter'; import { ResponseInterceptor } from './common/interceptors/response.interceptor'; async function bootstrap() { @@ -22,7 +15,6 @@ async function bootstrap() { const configService = app.get(ConfigService); - // Use our new StructuredLoggerService const logger = await app.resolve(StructuredLoggerService); app.useLogger(logger); @@ -60,18 +52,12 @@ async function bootstrap() { }), ); - // Global filters and interceptors - // FIX: Removed arguments from AllExceptionsFilter because the constructor expects 0 - app.useGlobalFilters(new AllExceptionsFilter()); - - // Using 'as any' to bypass the strict LoggerService interface mismatch - app.useGlobalInterceptors(new ResponseInterceptor(logger as any), new LoggingInterceptor(logger as any)); + app.useGlobalInterceptors(new ResponseInterceptor(logger as any)); // API prefix const apiPrefix = configService.get('API_PREFIX', 'api'); app.setGlobalPrefix(apiPrefix); - // Swagger documentation if (configService.get('SWAGGER_ENABLED', true)) { const config = new DocumentBuilder() .setTitle('PropChain API') @@ -104,7 +90,6 @@ async function bootstrap() { logger.log(`🏠 Environment: ${configService.get('NODE_ENV', 'development')}`); logger.log(`📊 Health check: http://${host}:${port}/${apiPrefix}/health`); - // Graceful shutdown process.on('SIGTERM', async () => { logger.log('SIGTERM signal received: closing HTTP server'); await app.close(); @@ -118,7 +103,7 @@ async function bootstrap() { }); } -bootstrap().catch(async (error) => { +bootstrap().catch(async error => { // Use a temporary logger since the app hasn't started const tempLogger = new (await import('./common/logging/logger.service')).StructuredLoggerService(null); tempLogger.setContext('Main'); diff --git a/src/rbac/rbac.service.ts b/src/rbac/rbac.service.ts index bbb3a568..50caa440 100644 --- a/src/rbac/rbac.service.ts +++ b/src/rbac/rbac.service.ts @@ -70,7 +70,11 @@ export class RbacService { return false; } catch (error) { - this.logger.error('Error checking permission:', error.stack, { userId: userId, resource: resource, action: action }); + this.logger.error('Error checking permission:', error.stack, { + userId, + resource, + action, + }); return false; } } @@ -321,7 +325,11 @@ export class RbacService { return false; } } catch (error) { - this.logger.error('Error validating resource ownership:', error.stack, { userId: userId, resourceType: resourceType, resourceId: resourceId }); + this.logger.error('Error validating resource ownership:', error.stack, { + userId, + resourceType, + resourceId, + }); return false; } } diff --git a/src/users/user.service.ts b/src/users/user.service.ts index a333be97..b40f7e64 100644 --- a/src/users/user.service.ts +++ b/src/users/user.service.ts @@ -1,4 +1,10 @@ -import { Injectable, NotFoundException, ConflictException, UnauthorizedException, BadRequestException } from '@nestjs/common'; +import { + Injectable, + NotFoundException, + ConflictException, + UnauthorizedException, + BadRequestException, +} from '@nestjs/common'; import { PrismaService } from '../database/prisma/prisma.service'; import { CreateUserDto } from './dto/create-user.dto'; import * as bcrypt from 'bcrypt'; @@ -8,7 +14,7 @@ import { PasswordValidator } from '../common/validators/password.validator'; export class UserService { constructor( private prisma: PrismaService, - private readonly passwordValidator: PasswordValidator + private readonly passwordValidator: PasswordValidator, ) {} async create(createUserDto: CreateUserDto) { diff --git a/src/valuation/valuation.service.ts b/src/valuation/valuation.service.ts index 6f5776fd..2f8ca5b6 100644 --- a/src/valuation/valuation.service.ts +++ b/src/valuation/valuation.service.ts @@ -5,11 +5,30 @@ import axios from 'axios'; import { Decimal } from '@prisma/client/runtime/library'; import { CacheService } from '../common/services/cache.service'; import { withResilience } from 'src/common/utils/resilence.util'; -import { PropertyFeatures, ValuationResult } from './valuation.types'; -import { PrismaProperty, PrismaPropertyValuation } from '../types/prisma.types'; -import { isObject, isString, isNumber } from '../types/guards'; +import { getCorrelationId, getTraceId } from 'src/common/logging/correlation-id'; + +export interface PropertyFeatures { + id?: string; + location: string; + bedrooms?: number; + bathrooms?: number; + squareFootage?: number; + yearBuilt?: number; + propertyType?: string; + lotSize?: number; + [key: string]: any; +} -// Remove the inline interfaces since we're importing them from valuation.types.ts +export interface ValuationResult { + propertyId: string; + estimatedValue: number; + confidenceScore: number; + valuationDate: Date; + source: string; + marketTrend?: string; + featuresUsed?: PropertyFeatures; + rawData?: any; +} @Injectable() export class ValuationService { @@ -67,7 +86,7 @@ export class ValuationService { throw new NotFoundException(`Property with ID ${propertyId} not found`); } - const prop = property as PrismaProperty; + const prop = property as any; features = { id: prop.id, location: prop.location, @@ -122,9 +141,8 @@ export class ValuationService { this.logger.log(`Successfully cached valuation for property ${propertyId}`); return savedValuation; - } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; - this.logger.error(`Valuation failed for property ${propertyId}: ${errorMessage}`); + } catch (error) { + this.logger.error(`Valuation failed for property ${propertyId}: ${error.message}`); throw error; } } @@ -140,7 +158,8 @@ export class ValuationService { } try { - // Mock implementation - in real scenario, this would call Zillow's actual API + const correlationId = getCorrelationId(); + const traceId = getTraceId(); const response = await axios.post( `${this.externalApis.zillow.baseUrl}/valuation`, { @@ -154,6 +173,8 @@ export class ValuationService { headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', + 'x-correlation-id': correlationId, + 'x-trace-id': traceId, }, timeout: this.configService.get('valuation.valuation.timeout'), }, @@ -169,9 +190,8 @@ export class ValuationService { featuresUsed: features, rawData: response.data, }; - } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; - this.logger.error(`Zillow API error: ${errorMessage}`); + } catch (error) { + this.logger.error(`Zillow API error: ${error.message}`); return null; } } @@ -187,7 +207,8 @@ export class ValuationService { } try { - // Mock implementation - in real scenario, this would call Redfin's actual API + const correlationId = getCorrelationId(); + const traceId = getTraceId(); const response = await axios.get(`${this.externalApis.redfin.baseUrl}/home-value`, { params: { location: features.location, @@ -197,6 +218,8 @@ export class ValuationService { }, headers: { 'X-API-Key': apiKey, + 'x-correlation-id': correlationId, + 'x-trace-id': traceId, }, timeout: this.configService.get('valuation.valuation.timeout'), }); @@ -228,7 +251,8 @@ export class ValuationService { } try { - // Mock implementation - in real scenario, this would call CoreLogic's actual API + const correlationId = getCorrelationId(); + const traceId = getTraceId(); const response = await axios.post( `${this.externalApis.corelogic.baseUrl}/property-valuations`, { @@ -244,6 +268,8 @@ export class ValuationService { headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', + 'x-correlation-id': correlationId, + 'x-trace-id': traceId, }, timeout: this.configService.get('valuation.valuation.timeout'), }, @@ -340,24 +366,19 @@ export class ValuationService { } // Simple majority vote for market trend - const trendCounts: Record = {}; + const trendCounts = {}; for (const trend of trends) { trendCounts[trend] = (trendCounts[trend] || 0) + 1; } - const trendEntries = Object.entries(trendCounts); - if (trendEntries.length === 0) { - return 'stable'; - } - - return trendEntries.reduce((a, b) => (a[1] > b[1] ? a : b))[0] as 'up' | 'down' | 'stable'; + return Object.keys(trendCounts).reduce((a, b) => (trendCounts[a] > trendCounts[b] ? a : b)); } /** * Save valuation to database */ private async saveValuation(valuation: ValuationResult) { - const saved = await this.prisma.propertyValuation.create({ + const saved = await (this.prisma as any).propertyValuation.create({ data: { propertyId: valuation.propertyId, estimatedValue: new Decimal(valuation.estimatedValue.toString()), @@ -424,7 +445,7 @@ export class ValuationService { this.logger.log(`Cache MISS for property history ${propertyId}, fetching fresh data`); - const valuations = await this.prisma.propertyValuation?.findMany({ + const valuations = await (this.prisma as any).propertyValuation?.findMany({ where: { propertyId }, orderBy: { valuationDate: 'desc' }, }); @@ -454,7 +475,7 @@ export class ValuationService { // This would typically integrate with market analysis APIs // For now, returning mock data - const valuations = await this.prisma.propertyValuation?.findMany({ + const valuations = await (this.prisma as any).propertyValuation?.findMany({ where: { property: { location: { @@ -646,7 +667,7 @@ export class ValuationService { */ private async getRecentValuedProperties(limit: number = 10): Promise> { try { - const recentValuations = await this.prisma.propertyValuation?.findMany({ + const recentValuations = await (this.prisma as any).propertyValuation?.findMany({ orderBy: { valuationDate: 'desc' }, take: limit, select: { @@ -679,7 +700,7 @@ export class ValuationService { throw new NotFoundException(`Property with ID ${propertyId} not found`); } - const prop = property as PrismaProperty; + const prop = property as any; features = { id: prop.id, location: prop.location,