Skip to content

Instantly share code, notes, and snippets.

@devhammed
Last active September 12, 2025 09:42
Show Gist options
  • Select an option

  • Save devhammed/dcfa262d15db2a4540b7e63247fc0dca to your computer and use it in GitHub Desktop.

Select an option

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)
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('*');
}
}
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;
}
}
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();
}
}
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;
}
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[];
}
export interface AuditMetadataOperation {
id: string;
name: string;
action: 'create' | 'read' | 'update' | 'delete';
data: Record<string, unknown>;
}
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;
}
}
export enum AuditAction {
LOGIN = 'login',
GET_CURRENT_USER = 'get_current_user',
LOGOUT = 'logout',
UNKNOWN = 'unknown'
}
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);
}
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,
});
}
}
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