From 821af8b0ff733c7dfab2b7f83fcbdb1814f3ae16 Mon Sep 17 00:00:00 2001 From: Bijin Krishn Date: Mon, 4 May 2026 12:37:53 +0530 Subject: [PATCH 1/2] feat: update audit_logs table model Update for including auditLogs for error logs --- .../migration.sql | 26 +++++++++++++++++++ prisma/schema.prisma | 21 +++++++-------- src/infra/prisma/audit.helper.ts | 6 ++--- src/infra/prisma/prisma.extension.ts | 23 +++++++++------- 4 files changed, 53 insertions(+), 23 deletions(-) create mode 100644 prisma/migrations/20260504065239_update_audit_log_model/migration.sql diff --git a/prisma/migrations/20260504065239_update_audit_log_model/migration.sql b/prisma/migrations/20260504065239_update_audit_log_model/migration.sql new file mode 100644 index 0000000..26382c8 --- /dev/null +++ b/prisma/migrations/20260504065239_update_audit_log_model/migration.sql @@ -0,0 +1,26 @@ +/* + Warnings: + + - You are about to drop the column `action` on the `audit_logs` table. All the data in the column will be lost. + - You are about to drop the column `entity` on the `audit_logs` table. All the data in the column will be lost. + - You are about to drop the column `ip_address` on the `audit_logs` table. All the data in the column will be lost. + - You are about to drop the column `method` on the `audit_logs` table. All the data in the column will be lost. + - You are about to drop the column `new_data` on the `audit_logs` table. All the data in the column will be lost. + - You are about to drop the column `old_data` on the `audit_logs` table. All the data in the column will be lost. + - You are about to drop the column `url` on the `audit_logs` table. All the data in the column will be lost. + - Added the required column `type` to the `audit_logs` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "audit_logs" DROP COLUMN "action", +DROP COLUMN "entity", +DROP COLUMN "ip_address", +DROP COLUMN "method", +DROP COLUMN "new_data", +DROP COLUMN "old_data", +DROP COLUMN "url", +ADD COLUMN "details" JSONB, +ADD COLUMN "request_method" TEXT, +ADD COLUMN "request_url" TEXT, +ADD COLUMN "type" TEXT NOT NULL, +ADD COLUMN "user_ip_address" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 320af93..6de79f9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -50,19 +50,18 @@ model RefreshToken { } model AuditLog { - id String @default(uuid()) - createdAt DateTime @default(now()) @map("created_at") @db.Timestamp() + id String @default(uuid()) + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp() + // + type String // e.g., "CREATE", "UPDATE", "DELETE", "ERROR" + details Json? // - action String // e.g., "CREATE", "UPDATE", "DELETE" - entity String // e.g., "User", "Product" - oldData Json? @map("old_data") - newData Json? @map("new_data") + requestUrl String? @map("request_url") + requestMethod String? @map("request_method") // api request method // - method String? // api request method - url String? - userId Int? @map("user_id") // ID of the user who performed the action - userEmail String? @map("user_email") // Email of the user who performed the action - ipAddress String? @map("ip_address") + userId Int? @map("user_id") // ID of the user who performed the action + userEmail String? @map("user_email") // Email of the user who performed the action + userIpAddress String? @map("user_ip_address") @@id([id, createdAt]) @@index([userId], map: "idx_audit_logs_user_id") diff --git a/src/infra/prisma/audit.helper.ts b/src/infra/prisma/audit.helper.ts index cb30874..f1e7319 100644 --- a/src/infra/prisma/audit.helper.ts +++ b/src/infra/prisma/audit.helper.ts @@ -4,10 +4,10 @@ export function getAuditContext(als: AlsService) { const store = als.getStore() return { + requestUrl: store?.get('url') as string | undefined, + requestMethod: store?.get('method') as string | undefined, userId: store?.get('userId') as number | undefined, userEmail: store?.get('userEmail') as string | undefined, - ipAddress: store?.get('ip') as string | undefined, - url: store?.get('url') as string | undefined, - method: store?.get('method') as string | undefined, + userIpAddress: store?.get('ip') as string | undefined, } } diff --git a/src/infra/prisma/prisma.extension.ts b/src/infra/prisma/prisma.extension.ts index 5a9ca0c..e56d532 100644 --- a/src/infra/prisma/prisma.extension.ts +++ b/src/infra/prisma/prisma.extension.ts @@ -7,10 +7,11 @@ type PrismaJsonValue = | Prisma.NullableJsonNullValueInput | undefined -const action = { +const auditLogType = { CREATE: 'CREATE', UPDATA: 'UPDATE', DELETE: 'DELETE', + ERROR: 'ERROR', } export const auditLogExtension = (client: PrismaClient, als: AlsService) => { @@ -39,10 +40,12 @@ export const auditLogExtension = (client: PrismaClient, als: AlsService) => { await tx.auditLog.create({ data: { ...context, - action: action.CREATE, - entity: model, - oldData: undefined, - newData: newData as PrismaJsonValue, + type: auditLogType.CREATE, + details: { + entity: model, + oldData: null, + newData, + } as PrismaJsonValue, }, }) @@ -74,10 +77,12 @@ export const auditLogExtension = (client: PrismaClient, als: AlsService) => { await tx.auditLog.create({ data: { ...context, - action: action.UPDATA, - entity: model, - oldData: oldData as PrismaJsonValue, - newData: newData as PrismaJsonValue, + type: auditLogType.UPDATA, + details: { + entity: model, + oldData, + newData, + } as PrismaJsonValue, }, }) From af5105631e1297b531a02525f777b5fc28593187 Mon Sep 17 00:00:00 2001 From: Bijin Krishn Date: Tue, 5 May 2026 15:04:07 +0530 Subject: [PATCH 2/2] feat: add centralized error handling --- src/app.module.ts | 7 +- src/auth/auth.service.ts | 2 +- .../exception}/index.ts | 0 .../user-already-exists.exception.ts | 0 src/common/filter/all-exceptions.filter.ts | 94 +++++++++++++++++++ src/common/filter/index.ts | 1 + src/infra/prisma/extension/index.ts | 1 + .../{ => extension}/prisma.extension.ts | 30 +++--- src/infra/prisma/prisma.service.ts | 2 +- .../{audit.helper.ts => util/audit.util.ts} | 2 +- src/infra/prisma/util/index.ts | 1 + src/main.ts | 1 + 12 files changed, 121 insertions(+), 20 deletions(-) rename src/{custom-exceptions => common/exception}/index.ts (100%) rename src/{custom-exceptions => common/exception}/user-already-exists.exception.ts (100%) create mode 100644 src/common/filter/all-exceptions.filter.ts create mode 100644 src/common/filter/index.ts create mode 100644 src/infra/prisma/extension/index.ts rename src/infra/prisma/{ => extension}/prisma.extension.ts (83%) rename src/infra/prisma/{audit.helper.ts => util/audit.util.ts} (87%) create mode 100644 src/infra/prisma/util/index.ts diff --git a/src/app.module.ts b/src/app.module.ts index 53f2f11..3c48508 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -6,13 +6,14 @@ import { RequestMethod, } from '@nestjs/common' import { ConfigModule, ConfigService } from '@nestjs/config' -import { APP_GUARD } from '@nestjs/core' +import { APP_FILTER, APP_GUARD } from '@nestjs/core' import { ScheduleModule } from '@nestjs/schedule' import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler' import { AppController } from './app.controller' import { AppService } from './app.service' import { AuthInfrastructureModule } from './auth/auth-infrastructure.module' import { AuthModule } from './auth/auth.module' +import { AllExceptionsFilter } from './common/filter' import { RequestContextMiddleware } from './common/middleware' import envValidation from './config/env.validation' import { AlsModule } from './infra/als/als.module' @@ -54,6 +55,10 @@ import { UserModule } from './user/user.module' provide: APP_GUARD, useClass: ThrottlerGuard, }, + { + provide: APP_FILTER, + useClass: AllExceptionsFilter, + }, ], }) export class AppModule implements NestModule { diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 0e56a56..c0ff957 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -5,7 +5,7 @@ import { NotFoundException, } from '@nestjs/common' import * as argon from 'argon2' -import { UserAlreadyExistsException } from 'src/custom-exceptions' +import { UserAlreadyExistsException } from 'src/common/exception' import { PrismaService } from 'src/infra/prisma/prisma.service' import { OtpService } from 'src/otp/otp.service' import { TokenService } from 'src/token/token.service' diff --git a/src/custom-exceptions/index.ts b/src/common/exception/index.ts similarity index 100% rename from src/custom-exceptions/index.ts rename to src/common/exception/index.ts diff --git a/src/custom-exceptions/user-already-exists.exception.ts b/src/common/exception/user-already-exists.exception.ts similarity index 100% rename from src/custom-exceptions/user-already-exists.exception.ts rename to src/common/exception/user-already-exists.exception.ts diff --git a/src/common/filter/all-exceptions.filter.ts b/src/common/filter/all-exceptions.filter.ts new file mode 100644 index 0000000..28f3943 --- /dev/null +++ b/src/common/filter/all-exceptions.filter.ts @@ -0,0 +1,94 @@ +import { + ArgumentsHost, + Catch, + ExceptionFilter, + HttpException, + HttpStatus, +} from '@nestjs/common' +import { Request, Response } from 'express' +import { REQUEST_USER_KEY } from 'src/auth/guard' +import { PrismaService } from 'src/infra/prisma/prisma.service' + +const auditLogType = { + CREATE: 'CREATE', + UPDATA: 'UPDATE', + ERROR: 'ERROR', +} + +@Catch() // Leaving this empty catches EVERYTHING +export class AllExceptionsFilter implements ExceptionFilter { + constructor(private readonly prisma: PrismaService) {} + async catch(exception: unknown, host: ArgumentsHost) { + const ctx = host.switchToHttp() + const request = ctx.getRequest() + const response = ctx.getResponse() + + const reqUser = request[REQUEST_USER_KEY] as { + sub: number + email: string + } + + let traceId: string | undefined + const timestamp = new Date().toISOString() + const status = + exception instanceof HttpException + ? exception.getStatus() + : HttpStatus.INTERNAL_SERVER_ERROR + let originalRes = + exception instanceof HttpException + ? exception.getResponse() + : 'Internal server error' + + if (typeof originalRes === 'string') originalRes = { error: originalRes } + if (typeof originalRes === 'object') { + originalRes = Object.fromEntries( + Object.entries(originalRes as Record).filter( + ([key]) => key !== 'statusCode', + ), + ) + } + + const reqContext = { + requestUrl: request?.url, + requestMethod: request?.method, + userIpAddress: request?.ip, + userId: reqUser?.sub || undefined, + userEmail: reqUser?.email || undefined, + } + const details = { + ...originalRes, + // errorCode: 'NOT_FOUND', + // errorType: 'BUSINESS_ERROR', + timestamp, + } + + try { + const auditLogRes = await this.prisma.auditLog.create({ + data: { + ...reqContext, + details, + type: auditLogType.ERROR, + }, + }) + traceId = auditLogRes.id + } catch (auditError) { + // Log to console if DB is down, so we don't lose the original error + console.error('Failed to save audit log:', auditError) + } + + response.status(status).json({ + success: false, + statusCode: status, + // errorCode: 'NOT_FOUND', + // errorType: 'BUSINESS_ERROR', + errorCode: + (originalRes as { error: string })?.error?.toUpperCase() || null, + name: (originalRes as { name: string })?.name || null, + message: (originalRes as { message: string })?.message || null, + details: originalRes, + path: request?.url, + timestamp, + traceId, + }) + } +} diff --git a/src/common/filter/index.ts b/src/common/filter/index.ts new file mode 100644 index 0000000..c1fa799 --- /dev/null +++ b/src/common/filter/index.ts @@ -0,0 +1 @@ +export * from './all-exceptions.filter' diff --git a/src/infra/prisma/extension/index.ts b/src/infra/prisma/extension/index.ts new file mode 100644 index 0000000..1f93beb --- /dev/null +++ b/src/infra/prisma/extension/index.ts @@ -0,0 +1 @@ +export * from './prisma.extension' diff --git a/src/infra/prisma/prisma.extension.ts b/src/infra/prisma/extension/prisma.extension.ts similarity index 83% rename from src/infra/prisma/prisma.extension.ts rename to src/infra/prisma/extension/prisma.extension.ts index e56d532..662d2d9 100644 --- a/src/infra/prisma/prisma.extension.ts +++ b/src/infra/prisma/extension/prisma.extension.ts @@ -1,16 +1,10 @@ import { PrismaClient, type Prisma } from 'generated/prisma/client' -import { AlsService } from '../als/als.service' -import { getAuditContext } from './audit.helper' - -type PrismaJsonValue = - | Prisma.InputJsonValue - | Prisma.NullableJsonNullValueInput - | undefined +import { AlsService } from 'src/infra/als/als.service' +import { getAuditContext } from '../util' const auditLogType = { CREATE: 'CREATE', UPDATA: 'UPDATE', - DELETE: 'DELETE', ERROR: 'ERROR', } @@ -18,7 +12,7 @@ export const auditLogExtension = (client: PrismaClient, als: AlsService) => { return client.$extends({ query: { $allModels: { - // create + // CREATE async create({ model, args, query }) { if (model === 'AuditLog') return query(args) @@ -43,16 +37,18 @@ export const auditLogExtension = (client: PrismaClient, als: AlsService) => { type: auditLogType.CREATE, details: { entity: model, - oldData: null, - newData, - } as PrismaJsonValue, + data: { + old: null, + new: newData, + }, + } as Prisma.InputJsonValue, }, }) return result }) }, - // update + // UPDATE async update({ model, args, query }) { if (model === 'AuditLog') return query(args) @@ -80,9 +76,11 @@ export const auditLogExtension = (client: PrismaClient, als: AlsService) => { type: auditLogType.UPDATA, details: { entity: model, - oldData, - newData, - } as PrismaJsonValue, + data: { + old: oldData, + new: newData, + }, + } as Prisma.InputJsonValue, }, }) diff --git a/src/infra/prisma/prisma.service.ts b/src/infra/prisma/prisma.service.ts index 78d3210..c4a9993 100644 --- a/src/infra/prisma/prisma.service.ts +++ b/src/infra/prisma/prisma.service.ts @@ -3,7 +3,7 @@ import { ConfigService } from '@nestjs/config' import { PrismaPg } from '@prisma/adapter-pg' import { PrismaClient } from 'generated/prisma/client' import { AlsService } from '../als/als.service' -import { AuditLogPrismaClient, auditLogExtension } from './prisma.extension' +import { AuditLogPrismaClient, auditLogExtension } from './extension' @Injectable() export class PrismaService extends PrismaClient { diff --git a/src/infra/prisma/audit.helper.ts b/src/infra/prisma/util/audit.util.ts similarity index 87% rename from src/infra/prisma/audit.helper.ts rename to src/infra/prisma/util/audit.util.ts index f1e7319..efe8c65 100644 --- a/src/infra/prisma/audit.helper.ts +++ b/src/infra/prisma/util/audit.util.ts @@ -1,4 +1,4 @@ -import { AlsService } from '../als/als.service' +import { AlsService } from 'src/infra/als/als.service' export function getAuditContext(als: AlsService) { const store = als.getStore() diff --git a/src/infra/prisma/util/index.ts b/src/infra/prisma/util/index.ts new file mode 100644 index 0000000..e1d3440 --- /dev/null +++ b/src/infra/prisma/util/index.ts @@ -0,0 +1 @@ +export * from './audit.util' diff --git a/src/main.ts b/src/main.ts index 5591ced..14dcb1e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -27,4 +27,5 @@ async function bootstrap() { await app.listen(process.env.PORT ?? 3000) } + void bootstrap()