From ffe965f421521476cba7df5cae4ac6b30f65f840 Mon Sep 17 00:00:00 2001 From: Divine Ifediorah Date: Sat, 4 Oct 2025 16:03:48 +0100 Subject: [PATCH] audit logging --- backend/src/app.module.ts | 4 ++- .../src/audit-logs/audit-logs.controller.ts | 13 ++++++++ .../src/audit-logs/audit-logs.interceptor.ts | 32 +++++++++++++++++++ backend/src/audit-logs/audit-logs.module.ts | 9 ++++++ backend/src/audit-logs/audit-logs.service.ts | 22 +++++++++++++ .../audit-logs/dto/create-audit-log.dto.ts | 1 + .../audit-logs/dto/update-audit-log.dto.ts | 4 +++ .../audit-logs/entities/audit-log.entity.ts | 26 +++++++++++++++ backend/src/main.ts | 15 ++++++--- 9 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 backend/src/audit-logs/audit-logs.controller.ts create mode 100644 backend/src/audit-logs/audit-logs.interceptor.ts create mode 100644 backend/src/audit-logs/audit-logs.module.ts create mode 100644 backend/src/audit-logs/audit-logs.service.ts create mode 100644 backend/src/audit-logs/dto/create-audit-log.dto.ts create mode 100644 backend/src/audit-logs/dto/update-audit-log.dto.ts create mode 100644 backend/src/audit-logs/entities/audit-log.entity.ts diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 80ef124..bfad93b 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -12,6 +12,7 @@ import { JwtAuthGuard } from './auth/guards/jwt.guard'; import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler'; import { AuditsModule } from './audits/audits.module'; import { CostCentersModule } from './cost-centers/cost-centers.module'; +import { AuditLogsModule } from './audit-logs/audit-logs.module'; @Module({ imports: [ @@ -64,7 +65,8 @@ import { CostCentersModule } from './cost-centers/cost-centers.module'; EmailModule, NewsletterModule, AuditsModule, - CostCentersModule, + CostCentersModule, + AuditLogsModule, ], controllers: [AppController], providers: [ diff --git a/backend/src/audit-logs/audit-logs.controller.ts b/backend/src/audit-logs/audit-logs.controller.ts new file mode 100644 index 0000000..b2e9678 --- /dev/null +++ b/backend/src/audit-logs/audit-logs.controller.ts @@ -0,0 +1,13 @@ +/* eslint-disable prettier/prettier */ +import { Controller, Get, Param } from '@nestjs/common'; +import { AuditLogsService } from './audit-logs.service'; + +@Controller('audit-logs') +export class AuditLogsController { + constructor(private readonly auditLogsService: AuditLogsService) {} + + @Get(':userId') + getLogs(@Param('userId') userId: string) { + return this.auditLogsService.findByUser(userId); + } +} diff --git a/backend/src/audit-logs/audit-logs.interceptor.ts b/backend/src/audit-logs/audit-logs.interceptor.ts new file mode 100644 index 0000000..1371a2f --- /dev/null +++ b/backend/src/audit-logs/audit-logs.interceptor.ts @@ -0,0 +1,32 @@ +/* eslint-disable prettier/prettier */ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, +} from '@nestjs/common'; +import { Observable, tap } from 'rxjs'; +import { AuditLogsService } from './audit-logs.service'; + +@Injectable() +export class AuditLogsInterceptor implements NestInterceptor { + constructor(private readonly auditLogsService: AuditLogsService) {} + + intercept(context: ExecutionContext, next: CallHandler): Observable { + const req = context.switchToHttp().getRequest(); + const user = req.user; // requires auth + const { method, originalUrl, body, params, query } = req; + + return next.handle().pipe( + tap(async (data) => { + await this.auditLogsService.createLog({ + action: `${method} ${originalUrl}`, + entity: params?.id ? originalUrl.split('/')[1] : null, + entityId: params?.id, + userId: user?.id || 'anonymous', + details: { body, query, response: data }, + }); + }), + ); + } +} diff --git a/backend/src/audit-logs/audit-logs.module.ts b/backend/src/audit-logs/audit-logs.module.ts new file mode 100644 index 0000000..4afb160 --- /dev/null +++ b/backend/src/audit-logs/audit-logs.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { AuditLogsService } from './audit-logs.service'; +import { AuditLogsController } from './audit-logs.controller'; + +@Module({ + controllers: [AuditLogsController], + providers: [AuditLogsService], +}) +export class AuditLogsModule {} diff --git a/backend/src/audit-logs/audit-logs.service.ts b/backend/src/audit-logs/audit-logs.service.ts new file mode 100644 index 0000000..c6e1995 --- /dev/null +++ b/backend/src/audit-logs/audit-logs.service.ts @@ -0,0 +1,22 @@ +/* eslint-disable prettier/prettier */ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { AuditLog } from './entities/audit-log.entity'; + +@Injectable() +export class AuditLogsService { + constructor( + @InjectRepository(AuditLog) + private readonly auditLogsRepo: Repository, + ) {} + + async createLog(log: Partial) { + const auditLog = this.auditLogsRepo.create(log); + return this.auditLogsRepo.save(auditLog); + } + + async findByUser(userId: string) { + return this.auditLogsRepo.find({ where: { userId }, order: { timestamp: 'DESC' } }); + } +} diff --git a/backend/src/audit-logs/dto/create-audit-log.dto.ts b/backend/src/audit-logs/dto/create-audit-log.dto.ts new file mode 100644 index 0000000..59769e2 --- /dev/null +++ b/backend/src/audit-logs/dto/create-audit-log.dto.ts @@ -0,0 +1 @@ +export class CreateAuditLogDto {} diff --git a/backend/src/audit-logs/dto/update-audit-log.dto.ts b/backend/src/audit-logs/dto/update-audit-log.dto.ts new file mode 100644 index 0000000..9981879 --- /dev/null +++ b/backend/src/audit-logs/dto/update-audit-log.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateAuditLogDto } from './create-audit-log.dto'; + +export class UpdateAuditLogDto extends PartialType(CreateAuditLogDto) {} diff --git a/backend/src/audit-logs/entities/audit-log.entity.ts b/backend/src/audit-logs/entities/audit-log.entity.ts new file mode 100644 index 0000000..1b0738c --- /dev/null +++ b/backend/src/audit-logs/entities/audit-log.entity.ts @@ -0,0 +1,26 @@ +/* eslint-disable prettier/prettier */ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm'; + +@Entity('audit_logs') +export class AuditLog { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + action: string; // e.g. LOGIN, CREATE_USER, UPDATE_ASSET + + @Column({ nullable: true }) + entity: string; // e.g. User, Asset + + @Column({ nullable: true }) + entityId: string; // e.g. the affected entity's ID + + @Column() + userId: string; // who performed the action + + @Column({ type: 'json', nullable: true }) + details: any; // extra payload + + @CreateDateColumn() + timestamp: Date; +} diff --git a/backend/src/main.ts b/backend/src/main.ts index e9c770a..429736a 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -1,3 +1,4 @@ +/* eslint-disable prettier/prettier */ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; @@ -6,11 +7,13 @@ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { ValidationPipe, ClassSerializerInterceptor } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; +import { AuditLogsInterceptor } from './audit-logs/audit-logs.interceptor'; +import { AuditLogsService } from './audit-logs/audit-logs.service'; async function bootstrap() { const app = await NestFactory.create(AppModule); - //GLOBAL VALIDATION + // GLOBAL VALIDATION app.useGlobalPipes( new ValidationPipe({ transform: true, @@ -19,10 +22,14 @@ async function bootstrap() { }), ); - //GLOBAL SERIALIZATION + // GLOBAL SERIALIZATION app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector))); - //ENABLE CORS + // GLOBAL AUDIT LOGGING INTERCEPTOR + const auditLogsService = app.get(AuditLogsService); + app.useGlobalInterceptors(new AuditLogsInterceptor(auditLogsService)); + + // ENABLE CORS app.enableCors({ origin: process.env.NODE_ENV === 'production' @@ -51,4 +58,4 @@ async function bootstrap() { await app.listen(process.env.PORT ?? 3000, '0.0.0.0'); console.log(`Server is listening at: ${await app.getUrl()}`); } -bootstrap(); \ No newline at end of file +bootstrap();