Repository navigation
Als ontwikkelaar wil ik logging van beveiligingsrelevante gebeurtenissen toevoegen, zodat verdachte activiteiten traceerbaar zijn. #20
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -2,6 +2,7 @@ import { type IUserRepository } from '@/domain/user/user-repository.interface'; | |||||
| import { IAuthService } from '@/application/ports/auth.port'; | ||||||
| import { verify, type Options } from '@node-rs/argon2'; | ||||||
| import { Inject, Injectable } from '@nestjs/common'; | ||||||
| import { LoggerService } from '@/common/logger.service'; | ||||||
| import { fail, succeed } from '@/result'; | ||||||
|
|
||||||
| @Injectable() | ||||||
|
|
@@ -14,18 +15,34 @@ export class AuthService implements IAuthService { | |||||
| timeCost: 2, | ||||||
| }; | ||||||
|
|
||||||
| constructor(@Inject('REPO.USER') _userRepository: IUserRepository) { | ||||||
| constructor( | ||||||
| @Inject('REPO.USER') _userRepository: IUserRepository, | ||||||
| private readonly logger?: LoggerService, | ||||||
| ) { | ||||||
| this.userRepository = _userRepository; | ||||||
| } | ||||||
|
|
||||||
| async login(email: string, password: string) { | ||||||
| const result = await this.userRepository.findByEmail(email); | ||||||
| if (result._tag === 'Failure') return fail(result.error); | ||||||
| if (result._tag === 'Failure') { | ||||||
| // Don't log repository-specific "User not found" to avoid exposing user existence. | ||||||
| const repoMsg = result.error?.message ?? 'Unknown error'; | ||||||
| const logMsg = | ||||||
| repoMsg === 'User not found' ? 'Credentials not correct' : repoMsg; | ||||||
| this.logger?.warn( | ||||||
| `Failed login attempt for ${email}: ${logMsg}`, | ||||||
|
||||||
| 'AuthService', | ||||||
| ); | ||||||
| return fail(result.error); | ||||||
| } | ||||||
|
|
||||||
| const user = result.data; | ||||||
|
|
||||||
| const isMatch = await verify(user.hashedPassword, password); | ||||||
| if (!isMatch) return fail(new Error('Invalid credentials')); | ||||||
| if (!isMatch) { | ||||||
| this.logger?.warn(`Invalid credentials for ${email}`, 'AuthService'); | ||||||
|
||||||
| this.logger?.warn(`Invalid credentials for ${email}`, 'AuthService'); | |
| this.logger?.warn('Invalid credentials', 'AuthService'); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,53 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Injectable, LoggerService as NestLoggerService } from '@nestjs/common'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type LogLevel = 'log' | 'error' | 'warn' | 'debug' | 'verbose'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Injectable() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export class LoggerService implements NestLoggerService { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private name = 'App'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setContext(name: string) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.name = name; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private format(level: LogLevel, message: any, context?: string, meta?: any) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const payload: Record<string, any> = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| timestamp: new Date().toISOString(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| level, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| context: context ?? this.name, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+7
to
+17
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private name = 'App'; | |
| setContext(name: string) { | |
| this.name = name; | |
| } | |
| private format(level: LogLevel, message: any, context?: string, meta?: any) { | |
| const payload: Record<string, any> = { | |
| timestamp: new Date().toISOString(), | |
| level, | |
| context: context ?? this.name, | |
| private name?: string; | |
| setContext(name: string) { | |
| this.name = name; | |
| } | |
| private format(level: LogLevel, message: any, context?: string, meta?: any) { | |
| const resolvedContext = context ?? this.name; | |
| if (!resolvedContext) { | |
| throw new Error('LoggerService context is not set. Please set a context using setContext() or provide one when logging.'); | |
| } | |
| const payload: Record<string, any> = { | |
| timestamp: new Date().toISOString(), | |
| level, | |
| context: resolvedContext, |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,23 +2,91 @@ import 'dotenv/config'; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { PrismaMariaDb } from '@prisma/adapter-mariadb'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { PrismaClient } from '@/infrastructure/database/generated/prisma/client'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { LoggerService } from '@/common/logger.service'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Injectable() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export class PrismaService | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| extends PrismaClient | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| implements OnModuleInit, OnModuleDestroy | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| constructor() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const adapter = new PrismaMariaDb(process.env.DATABASE_URL!); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| constructor(private readonly logger: LoggerService) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Parse DATABASE_URL and create mariadb pool config | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const dbUrl = new URL(process.env.DATABASE_URL!); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const adapter = new PrismaMariaDb({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| host: dbUrl.hostname, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| port: parseInt(dbUrl.port || '3306', 10), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| user: dbUrl.username, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| password: dbUrl.password, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| database: dbUrl.pathname.slice(1), // remove leading / | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| connectionLimit: 20, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| connectTimeout: 30000, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| super({ adapter }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Forward Prisma client events to our logger | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Prisma client's $on typing may be strict depending on generated client. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Cast to `any` to attach listeners at runtime while keeping TypeScript happy. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (this as any).$on('info', (e: any) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.logger.log(`${e.message}`, 'Prisma'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (this as any).$on('warn', (e: any) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.logger.warn(`${e.message}`, 'Prisma'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (this as any).$on('error', (e: any) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // e is an object with message and perhaps stack | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.logger.error( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `${e.message}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (e as any).stack ?? undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+28
to
+42
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Prisma client's $on typing may be strict depending on generated client. | |
| // Cast to `any` to attach listeners at runtime while keeping TypeScript happy. | |
| (this as any).$on('info', (e: any) => { | |
| this.logger.log(`${e.message}`, 'Prisma'); | |
| }); | |
| (this as any).$on('warn', (e: any) => { | |
| this.logger.warn(`${e.message}`, 'Prisma'); | |
| }); | |
| (this as any).$on('error', (e: any) => { | |
| // e is an object with message and perhaps stack | |
| this.logger.error( | |
| `${e.message}`, | |
| (e as any).stack ?? undefined, | |
| // Define event interfaces for Prisma events | |
| interface PrismaLogEvent { | |
| message: string; | |
| target?: string; | |
| timestamp?: Date; | |
| } | |
| interface PrismaErrorEvent extends PrismaLogEvent { | |
| stack?: string; | |
| } | |
| // Cast only `this` to any if needed, but type event parameter | |
| (this as any).$on('info', (e: PrismaLogEvent) => { | |
| this.logger.log(`${e.message}`, 'Prisma'); | |
| }); | |
| (this as any).$on('warn', (e: PrismaLogEvent) => { | |
| this.logger.warn(`${e.message}`, 'Prisma'); | |
| }); | |
| (this as any).$on('error', (e: PrismaErrorEvent) => { | |
| // e is an object with message and perhaps stack | |
| this.logger.error( | |
| `${e.message}`, | |
| e.stack ?? undefined, |
Copilot
AI
Dec 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Silently catching and ignoring exceptions without logging makes debugging difficult if event registration actually fails. Consider logging this failure at debug level to aid troubleshooting.
| // Listening may fail in some runtime builds; safely ignore | |
| // Listening may fail in some runtime builds; log at debug level for troubleshooting | |
| this.logger.debug( | |
| `Failed to register Prisma event listeners: ${e instanceof Error ? e.message : String(e)}`, | |
| (e instanceof Error && e.stack) ? e.stack : undefined, | |
| 'PrismaService', | |
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import { Injectable, NestMiddleware } from '@nestjs/common'; | ||
| import { Request, Response, NextFunction } from 'express'; | ||
| import { LoggerService } from '../../common/logger.service'; | ||
|
|
||
| @Injectable() | ||
| export class RequestLoggingMiddleware implements NestMiddleware { | ||
| constructor(private readonly logger: LoggerService) {} | ||
|
|
||
| use(req: Request, res: Response, next: NextFunction) { | ||
| const start = Date.now(); | ||
| const { method, originalUrl } = req; | ||
|
|
||
| res.on('finish', () => { | ||
| const duration = Date.now() - start; | ||
| const message = `${method} ${originalUrl} ${res.statusCode} ${duration}ms`; | ||
| this.logger.log(message, 'HTTP'); | ||
| }); | ||
|
|
||
| next(); | ||
| } | ||
| } | ||
|
|
||
| export default RequestLoggingMiddleware; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
loggerparameter is marked as optional, but the code uses optional chaining throughout. Consider making it required to ensure consistent logging behavior across all authentication operations.