Last active
September 12, 2025 09:42
-
-
Save devhammed/dcfa262d15db2a4540b7e63247fc0dca to your computer and use it in GitHub Desktop.
Nest.js Audit Module (request with all database operations that happened in the request-response flow)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { | |
| forwardRef, | |
| MiddlewareConsumer, | |
| Module, | |
| NestModule, | |
| } from '@nestjs/common'; | |
| import { TypeOrmModule } from '@nestjs/typeorm'; | |
| import { Audit } from '@/audit/entities/audit.entity'; | |
| import { AuditMiddleware } from '@/audit/middlewares/audit.middleware'; | |
| import { APP_GUARD } from '@nestjs/core'; | |
| import { AuditGuard } from '@/audit/guards/audit.guard'; | |
| import { AuditService } from '@/audit/services/audit.service'; | |
| import { EncryptionModule } from '@/encryption/encryption.module'; | |
| import { AuditController } from '@/audit/controllers/audit.controller'; | |
| import { AuditSubscriber } from '@/audit/subscribers/audit.subscriber'; | |
| @Module({ | |
| imports: [ | |
| TypeOrmModule.forFeature([Audit]), | |
| forwardRef(() => EncryptionModule), | |
| ], | |
| controllers: [AuditController], | |
| providers: [ | |
| { | |
| provide: APP_GUARD, | |
| useClass: AuditGuard, | |
| }, | |
| AuditMiddleware, | |
| AuditGuard, | |
| AuditService, | |
| AuditSubscriber, | |
| ], | |
| exports: [AuditService], | |
| }) | |
| export class AuditModule implements NestModule { | |
| configure(consumer: MiddlewareConsumer) { | |
| consumer.apply(AuditMiddleware).forRoutes('*'); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { Injectable, Logger, NotFoundException } from '@nestjs/common'; | |
| import { Audit } from '@/audit/entities/audit.entity'; | |
| import { CreateAuditDto } from '@/audit/dtos/create-audit.dto'; | |
| import { InjectRepository } from '@nestjs/typeorm'; | |
| import { Repository } from 'typeorm'; | |
| import { EncryptionService } from '@/encryption/services/encryption.service'; | |
| import { validate as isUuid } from 'uuid'; | |
| @Injectable() | |
| export class AuditService { | |
| private readonly logger = new Logger(AuditService.name); | |
| constructor( | |
| @InjectRepository(Audit) | |
| private readonly auditRepository: Repository<Audit>, | |
| private readonly encryptionService: EncryptionService, | |
| ) {} | |
| async create(data: CreateAuditDto): Promise<Audit> { | |
| const audit = this.auditRepository.create({ | |
| id: data.id, | |
| action: data.action, | |
| userId: data.userId, | |
| metadata: this.encryptionService.mask(data.metadata), | |
| }); | |
| await this.auditRepository.save(audit); | |
| return audit; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { Injectable, NestMiddleware } from '@nestjs/common'; | |
| import { Request, Response, NextFunction } from 'express'; | |
| import { v4 as uuidv4 } from 'uuid'; | |
| import { AuditService } from '@/audit/services/audit.service'; | |
| import { AuditMetadata } from '@/audit/interfaces/audit-metadata.interface'; | |
| import { AuditAction } from '@/audit/enums/audit-action.enum'; | |
| @Injectable() | |
| export class AuditMiddleware implements NestMiddleware { | |
| constructor(private readonly auditService: AuditService) {} | |
| use(req: Request, res: Response, next: NextFunction) { | |
| req.auditId = uuidv4(); | |
| req.auditOperations = []; | |
| res.originalSend = res.send.bind(res); | |
| res.send = (body?: unknown) => { | |
| try { | |
| res.body = JSON.parse(body as string); | |
| } catch { | |
| res.body = body; | |
| } | |
| return res.originalSend!(body); | |
| }; | |
| res.setHeader('X-Audit-Id', req.auditId); | |
| res.on('finish', async () => { | |
| const action = req.auditAction; | |
| const ip = req.ip; | |
| const user = req.user; | |
| const pathname = req.path; | |
| const query = req.query; | |
| const params = req.params; | |
| const agent = req.headers['user-agent']; | |
| await this.auditService.create({ | |
| id: req.auditId, | |
| userId: user?.id, | |
| action: action ?? AuditAction.UNKNOWN, | |
| metadata: { | |
| ip, | |
| agent, | |
| params, | |
| pathname, | |
| query, | |
| body: req.body as AuditMetadata['body'], | |
| response: res.body as AuditMetadata['response'], | |
| operations: req.auditOperations, | |
| }, | |
| }); | |
| }); | |
| next(); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { User } from '@/user/entities/user.entity'; | |
| import { AuditAction } from '@/audit/enums/audit-action.enum'; | |
| import { AuditMetadata } from '@/audit/interfaces/audit-metadata.interface'; | |
| import { ApiProperty } from '@nestjs/swagger'; | |
| import { | |
| Column, | |
| CreateDateColumn, | |
| Entity, | |
| ManyToOne, | |
| PrimaryGeneratedColumn, | |
| UpdateDateColumn, | |
| } from 'typeorm'; | |
| @Entity() | |
| export class Audit { | |
| @ApiProperty({ | |
| description: 'The unique identifier of the audit.', | |
| example: '123e4567-e89b-12d3-a456-426614174000', | |
| format: 'uuid', | |
| }) | |
| @PrimaryGeneratedColumn('uuid') | |
| id: string; | |
| @ApiProperty({ | |
| description: 'The unique identifier of the user.', | |
| example: '123e4567-e89b-12d3-a456-426614174000', | |
| format: 'uuid', | |
| }) | |
| @Column({ | |
| type: 'uuid', | |
| nullable: true, | |
| }) | |
| userId: string | null; | |
| @ApiProperty({ | |
| description: 'The action of the audit.', | |
| enum: AuditAction, | |
| example: AuditAction.LOGIN, | |
| }) | |
| @Column({ | |
| type: 'varchar', | |
| }) | |
| action: AuditAction; | |
| @ApiProperty({ | |
| type: 'object', | |
| description: 'The metadata of the audit.', | |
| additionalProperties: { | |
| type: 'string', | |
| }, | |
| example: { | |
| ip: '127.0.0.1', | |
| url: 'https://example.com/api/v1/users/123', | |
| agent: | |
| 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', | |
| response: { | |
| message: 'User fetched successfully.', | |
| data: { | |
| id: '123e4567-e89b-12d3-a456-426614174000', | |
| email: '[email protected]', | |
| role: 'admin', | |
| createdAt: '2021-01-01T00:00:00.000Z', | |
| updatedAt: '2021-01-01T00:00:00.000Z', | |
| }, | |
| }, | |
| operations: [ | |
| { | |
| id: '123e4567-e89b-12d3-a456-426614174000', | |
| name: 'User', | |
| action: 'read', | |
| data: { | |
| id: '123e4567-e89b-12d3-a456-426614174000', | |
| email: '[email protected]', | |
| role: 'admin', | |
| password: '********', | |
| createdAt: '2021-01-01T00:00:00.000Z', | |
| updatedAt: '2021-01-01T00:00:00.000Z', | |
| }, | |
| }, | |
| ], | |
| }, | |
| }) | |
| @Column({ | |
| type: 'jsonb', | |
| default: {}, | |
| }) | |
| metadata: AuditMetadata; | |
| @ApiProperty({ | |
| description: 'The user that performed the action.', | |
| type: () => User, | |
| nullable: true, | |
| required: false, | |
| }) | |
| @ManyToOne(() => User, (user) => user.audits, { | |
| nullable: true, | |
| onUpdate: 'CASCADE', | |
| onDelete: 'SET NULL', | |
| }) | |
| user?: User | null; | |
| @ApiProperty({ | |
| description: 'When the audit was created.', | |
| type: 'string', | |
| example: '2021-01-01T00:00:00.000Z', | |
| format: 'date-time', | |
| }) | |
| @CreateDateColumn() | |
| createdAt: Date; | |
| @ApiProperty({ | |
| description: 'When the audit was last updated.', | |
| type: 'string', | |
| example: '2021-01-01T00:00:00.000Z', | |
| format: 'date-time', | |
| }) | |
| @UpdateDateColumn() | |
| updatedAt: Date; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { AuditMetadataOperation } from '@/audit/interfaces/audit-metadata-operation.interface'; | |
| export interface AuditMetadata { | |
| ip?: string | null; | |
| pathname?: string | null; | |
| agent?: string | null; | |
| body?: Record<string, unknown> | string | null; | |
| params?: Record<string, unknown> | null; | |
| query?: Record<string, unknown> | null; | |
| response?: | |
| | Record<string, unknown> | |
| | Record<string, unknown>[] | |
| | string | |
| | null; | |
| operations?: AuditMetadataOperation[]; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| export interface AuditMetadataOperation { | |
| id: string; | |
| name: string; | |
| action: 'create' | 'read' | 'update' | 'delete'; | |
| data: Record<string, unknown>; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; | |
| import { Reflector } from '@nestjs/core'; | |
| import { Request } from 'express'; | |
| import { AuditAction } from '@/audit/enums/audit-action.enum'; | |
| import { RECORD_AUDIT_KEY } from '@/audit/decorators/record-audit.decorator'; | |
| @Injectable() | |
| export class AuditGuard implements CanActivate { | |
| constructor(private reflector: Reflector) {} | |
| canActivate(context: ExecutionContext): boolean { | |
| const req = context.switchToHttp().getRequest<Request>(); | |
| const action = this.reflector.get<AuditAction | undefined>( | |
| RECORD_AUDIT_KEY, | |
| context.getHandler(), | |
| ); | |
| if (action) { | |
| req.auditAction = action; | |
| } | |
| return true; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| export enum AuditAction { | |
| LOGIN = 'login', | |
| GET_CURRENT_USER = 'get_current_user', | |
| LOGOUT = 'logout', | |
| UNKNOWN = 'unknown' | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { SetMetadata } from '@nestjs/common'; | |
| import { AuditAction } from '@/audit/enums/audit-action.enum'; | |
| export const RECORD_AUDIT_KEY = 'record-audit'; | |
| export function RecordAudit(action: AuditAction): MethodDecorator { | |
| return SetMetadata(RECORD_AUDIT_KEY, action); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { Audit } from '@/audit/entities/audit.entity'; | |
| import { AuditedEntity } from '@/audit/interfaces/audited-entity.interface'; | |
| import { | |
| DataSource, | |
| EntitySubscriberInterface, | |
| EventSubscriber, | |
| InsertEvent, | |
| RemoveEvent, | |
| UpdateEvent, | |
| } from 'typeorm'; | |
| import { ClsService } from 'nestjs-cls'; | |
| @EventSubscriber() | |
| export class AuditSubscriber | |
| implements EntitySubscriberInterface<AuditedEntity> | |
| { | |
| constructor( | |
| private readonly clsService: ClsService, | |
| private readonly dataSource: DataSource, | |
| ) { | |
| this.dataSource.subscribers.push(this); | |
| } | |
| afterInsert(event: InsertEvent<AuditedEntity>) { | |
| const type = event.metadata.targetName; | |
| if (type === Audit.name) { | |
| return; | |
| } | |
| const after = event.entity; | |
| if (!after) { | |
| return; | |
| } | |
| const req = this.clsService.get('req'); | |
| if (!req) { | |
| return; | |
| } | |
| req.auditOperations?.push({ | |
| id: after.id, | |
| action: 'create', | |
| name: type, | |
| data: after, | |
| }); | |
| } | |
| afterLoad(entity: AuditedEntity) { | |
| const type = entity.constructor.name; | |
| if (type === Audit.name) { | |
| return; | |
| } | |
| const req = this.clsService.get('req'); | |
| if (!req) { | |
| return; | |
| } | |
| req.auditOperations?.push({ | |
| id: entity.id, | |
| action: 'read', | |
| name: type, | |
| data: entity, | |
| }); | |
| } | |
| afterUpdate(event: UpdateEvent<AuditedEntity>) { | |
| const type = event.metadata.targetName; | |
| if (type === Audit.name) { | |
| return; | |
| } | |
| const after = event.entity as AuditedEntity; | |
| if (!after) { | |
| return; | |
| } | |
| const before = event.databaseEntity; | |
| if (!before) { | |
| return; | |
| } | |
| const req = this.clsService.get('req'); | |
| if (!req) { | |
| return; | |
| } | |
| req.auditOperations?.push({ | |
| id: after.id, | |
| action: 'update', | |
| name: type, | |
| data: { | |
| before, | |
| after, | |
| }, | |
| }); | |
| } | |
| afterRemove(event: RemoveEvent<AuditedEntity>) { | |
| const type = event.metadata.targetName; | |
| if (type === Audit.name) { | |
| return; | |
| } | |
| const before = event.databaseEntity || event.entity; | |
| if (!before) { | |
| return; | |
| } | |
| const req = this.clsService.get('req'); | |
| if (!req) { | |
| return; | |
| } | |
| req.auditOperations?.push({ | |
| id: before.id, | |
| action: 'delete', | |
| name: type, | |
| data: before, | |
| }); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| export interface AuditedEntity { | |
| id: string; | |
| [key: string]: unknown; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment