Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
21 changes: 10 additions & 11 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
7 changes: 6 additions & 1 deletion src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
File renamed without changes.
94 changes: 94 additions & 0 deletions src/common/filter/all-exceptions.filter.ts
Original file line number Diff line number Diff line change
@@ -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<Request>()
const response = ctx.getResponse<Response>()

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<string, unknown>).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,
})
}
}
1 change: 1 addition & 0 deletions src/common/filter/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './all-exceptions.filter'
13 changes: 0 additions & 13 deletions src/infra/prisma/audit.helper.ts

This file was deleted.

1 change: 1 addition & 0 deletions src/infra/prisma/extension/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './prisma.extension'
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
import { PrismaClient, type Prisma } from 'generated/prisma/client'
import { AlsService } from '../als/als.service'
import { getAuditContext } from './audit.helper'
import { AlsService } from 'src/infra/als/als.service'
import { getAuditContext } from '../util'

type PrismaJsonValue =
| Prisma.InputJsonValue
| Prisma.NullableJsonNullValueInput
| undefined

const action = {
const auditLogType = {
CREATE: 'CREATE',
UPDATA: 'UPDATE',
DELETE: 'DELETE',
ERROR: 'ERROR',
}

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)

Expand All @@ -39,17 +34,21 @@ 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,
data: {
old: null,
new: newData,
},
} as Prisma.InputJsonValue,
},
})

return result
})
},
// update
// UPDATE
async update({ model, args, query }) {
if (model === 'AuditLog') return query(args)

Expand All @@ -74,10 +73,14 @@ 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,
data: {
old: oldData,
new: newData,
},
} as Prisma.InputJsonValue,
},
})

Expand Down
2 changes: 1 addition & 1 deletion src/infra/prisma/prisma.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
13 changes: 13 additions & 0 deletions src/infra/prisma/util/audit.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { AlsService } from 'src/infra/als/als.service'

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,
userIpAddress: store?.get('ip') as string | undefined,
}
}
1 change: 1 addition & 0 deletions src/infra/prisma/util/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './audit.util'
1 change: 1 addition & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ async function bootstrap() {

await app.listen(process.env.PORT ?? 3000)
}

void bootstrap()
Loading