From 33907d4ec3a5f9301354f4ce8ece4f45445892f0 Mon Sep 17 00:00:00 2001 From: MD JUBER QURAISHI Date: Thu, 26 Mar 2026 02:08:08 +0530 Subject: [PATCH] feat: implement audit trail system --- backend/src/app.module.ts | 2 ++ backend/src/audit/audit.controller.spec.ts | 18 +++++++++++ backend/src/audit/audit.controller.ts | 12 +++++++ backend/src/audit/audit.entity.ts | 31 ++++++++++++++++++ backend/src/audit/audit.interceptor.ts | 34 ++++++++++++++++++++ backend/src/audit/audit.module.ts | 13 ++++++++ backend/src/audit/audit.service.spec.ts | 18 +++++++++++ backend/src/audit/audit.service.ts | 37 ++++++++++++++++++++++ backend/src/main.ts | 4 +++ 9 files changed, 169 insertions(+) create mode 100644 backend/src/audit/audit.controller.spec.ts create mode 100644 backend/src/audit/audit.controller.ts create mode 100644 backend/src/audit/audit.entity.ts create mode 100644 backend/src/audit/audit.interceptor.ts create mode 100644 backend/src/audit/audit.module.ts create mode 100644 backend/src/audit/audit.service.spec.ts create mode 100644 backend/src/audit/audit.service.ts diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index da24c5f4..b3a2cd96 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -13,6 +13,7 @@ import { cdnConfig } from './config/cdn.config'; import { stellarConfig } from './config/stellar.config'; import { smsConfig } from './config/sms.config'; import { AuthModule } from './auth/auth.module'; +import { AuditModule } from './audit/audit.module'; // Feature Modules import { UsersModule } from './modules/users/users.module'; @@ -105,6 +106,7 @@ import { WebSocketModule } from './websocket/websocket.module'; LostPetsModule, AllergiesModule, ConditionsModule, + AuditModule, VerificationModule, diff --git a/backend/src/audit/audit.controller.spec.ts b/backend/src/audit/audit.controller.spec.ts new file mode 100644 index 00000000..e3548b40 --- /dev/null +++ b/backend/src/audit/audit.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuditController } from './audit.controller'; + +describe('AuditController', () => { + let controller: AuditController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AuditController], + }).compile(); + + controller = module.get(AuditController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/backend/src/audit/audit.controller.ts b/backend/src/audit/audit.controller.ts new file mode 100644 index 00000000..b523177c --- /dev/null +++ b/backend/src/audit/audit.controller.ts @@ -0,0 +1,12 @@ +import { Controller, Get } from '@nestjs/common'; +import { AuditService } from './audit.service'; + +@Controller('audit') +export class AuditController { + constructor(private readonly auditService: AuditService) {} + + @Get() + getLogs() { + return this.auditService.findAll(); + } +} \ No newline at end of file diff --git a/backend/src/audit/audit.entity.ts b/backend/src/audit/audit.entity.ts new file mode 100644 index 00000000..853ca158 --- /dev/null +++ b/backend/src/audit/audit.entity.ts @@ -0,0 +1,31 @@ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm'; + +@Entity('audit_logs') +export class AuditLog { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + action: string; + + @Column({ nullable: true }) + entity: string; + + @Column({ nullable: true }) + entityId: string; + + @Column({ type: 'json', nullable: true }) + oldData: any; + + @Column({ type: 'json', nullable: true }) + newData: any; + + @Column() + userId: string; + + @Column() + hash: string; + + @CreateDateColumn() + createdAt: Date; +} \ No newline at end of file diff --git a/backend/src/audit/audit.interceptor.ts b/backend/src/audit/audit.interceptor.ts new file mode 100644 index 00000000..76c93c07 --- /dev/null +++ b/backend/src/audit/audit.interceptor.ts @@ -0,0 +1,34 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, +} from '@nestjs/common'; +import { tap } from 'rxjs'; +import { AuditService } from './audit.service'; + +@Injectable() +export class AuditInterceptor implements NestInterceptor { + constructor(private readonly auditService: AuditService) {} + + intercept(context: ExecutionContext, next: CallHandler) { + const request = context.switchToHttp().getRequest(); + + const { method, url, body, user } = request; + + return next.handle().pipe( + tap(async (responseData) => { + try { + await this.auditService.logAction({ + action: method, + entity: url, + userId: user?.id || 'anonymous', + newData: body, + }); + } catch (err) { + console.error('Audit log failed:', err); + } + }), + ); + } +} \ No newline at end of file diff --git a/backend/src/audit/audit.module.ts b/backend/src/audit/audit.module.ts new file mode 100644 index 00000000..56667642 --- /dev/null +++ b/backend/src/audit/audit.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AuditLog } from './audit.entity'; +import { AuditService } from './audit.service'; +import { AuditController } from './audit.controller'; + +@Module({ + imports: [TypeOrmModule.forFeature([AuditLog])], + controllers: [AuditController], + providers: [AuditService], + exports: [AuditService], +}) +export class AuditModule {} \ No newline at end of file diff --git a/backend/src/audit/audit.service.spec.ts b/backend/src/audit/audit.service.spec.ts new file mode 100644 index 00000000..fcd49655 --- /dev/null +++ b/backend/src/audit/audit.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuditService } from './audit.service'; + +describe('AuditService', () => { + let service: AuditService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AuditService], + }).compile(); + + service = module.get(AuditService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/audit/audit.service.ts b/backend/src/audit/audit.service.ts new file mode 100644 index 00000000..2f88395c --- /dev/null +++ b/backend/src/audit/audit.service.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { AuditLog } from './audit.entity'; +import { Repository } from 'typeorm'; +import * as crypto from 'crypto'; + +@Injectable() +export class AuditService { + constructor( + @InjectRepository(AuditLog) + private readonly auditRepo: Repository, + ) {} + + generateHash(data: any): string { + return crypto + .createHash('sha256') + .update(JSON.stringify(data)) + .digest('hex'); + } + + async logAction(payload: Partial) { + const hash = this.generateHash(payload); + + const log = this.auditRepo.create({ + ...payload, + hash, + }); + + return this.auditRepo.save(log); + } + + async findAll() { + return this.auditRepo.find({ + order: { createdAt: 'DESC' }, + }); + } +} \ No newline at end of file diff --git a/backend/src/main.ts b/backend/src/main.ts index 98a42eab..dfc04f72 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -4,10 +4,14 @@ import { ConfigService } from '@nestjs/config'; import { NestExpressApplication } from '@nestjs/platform-express'; import helmet from 'helmet'; import { AppModule } from './app.module'; +import { AuditInterceptor } from './audit/audit.interceptor'; +import { AuditService } from './audit/audit.service'; async function bootstrap() { const app = await NestFactory.create(AppModule); const configService = app.get(ConfigService); + const auditService = app.get(AuditService); + app.useGlobalInterceptors(new AuditInterceptor(auditService)); // Trust proxy for correct IP address detection // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access