Skip to content

Commit 3e54f09

Browse files
authored
Merge pull request #52 from Aayush0966/develop
fix(audit): resolve middleware execution issues and prevent recursion
2 parents efb9623 + beacb01 commit 3e54f09

3 files changed

Lines changed: 174 additions & 32 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { ErrorResponse, SuccessResponse } from "@/dtos";
2+
import auditService from "@/services/audit.service"
3+
import { HTTP } from "@/utils/constants";
4+
import type { Request, Response } from "express";
5+
6+
class AuditController {
7+
async getAudits(_: Request, res: Response) {
8+
try {
9+
const result = await auditService.getAllAudits();
10+
if (!result.success) return res.status(HTTP.BAD_REQUEST).json(ErrorResponse(HTTP.BAD_REQUEST, result.error))
11+
return res.status(HTTP.OK).json(SuccessResponse(HTTP.OK, "Successfully fetched.", result.data))
12+
} catch (error) {
13+
console.log("Error while fetching auditsLogs: ", error)
14+
return res.status(HTTP.INTERNAL).json(ErrorResponse(HTTP.INTERNAL, "Internal Server Error"))
15+
16+
}
17+
}
18+
};
19+
20+
const auditController = new AuditController();
21+
export default auditController;

src/middleware/audit.middleware.ts

Lines changed: 129 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,157 @@
11
import type { ActionType, Models } from "@prisma/client";
22
import type { PrismaClient } from "@prisma/client/extension";
3+
import { requestContext } from "@/context/request.context";
34

5+
type AuditOperation = "create" | "update" | "delete";
46

5-
import type { Prisma } from "@prisma/client";
6-
import { requestContext } from "@/context/request.context";
7+
type QueryContext = {
8+
model: string;
9+
operation: string;
10+
args: any;
11+
query: (args: any) => Promise<any>;
12+
};
13+
14+
// List of operations
15+
const actionsToAudit: AuditOperation[] = ["create", "update", "delete"];
16+
17+
// Map model names to their values
18+
const modelToEnumMap: Record<string, Models> = {
19+
User: "USER",
20+
user: "USER",
21+
member: "MEMBER",
22+
Member: "MEMBER",
23+
Events: "EVENT",
24+
events: "EVENT",
25+
EventSchedule: "EVENTSCHEDULE",
26+
eventSchedule: "EVENTSCHEDULE",
27+
Project: "PROJECT",
28+
project: "PROJECT",
29+
ProjectContributors: "PROJECTCONTRIBUTORS",
30+
projectContributors: "PROJECTCONTRIBUTORS",
31+
Contributor: "CONTRIBUTOR",
32+
contributor: "CONTRIBUTOR",
33+
Tag: "TAG",
34+
tag: "TAG",
35+
};
36+
37+
const resolveDelegate = (client: PrismaClient, model: string) => {
38+
// Checking if the provided model exist on the prisma client
39+
const exact = (client as any)[model];
40+
if (exact) {
41+
// if the model exist, then return it
42+
return exact;
43+
}
44+
// if the model name doesn't exit, try using the camelCase version.
45+
const camel = model.charAt(0).toLowerCase() + model.slice(1);
46+
return (client as any)[camel];
47+
};
48+
49+
// finds the enum value for the given model name by capitalizing the first letter of the model name
50+
// or converting the entire model name to upper case
51+
const resolveEntity = (model: string): Models | undefined => {
52+
return modelToEnumMap[model] ?? modelToEnumMap[model.charAt(0).toUpperCase() + model.slice(1)] ?? (model.toUpperCase() as Models);
53+
};
754

55+
56+
const shouldDebug = () => process.env.AUDIT_DEBUG === "true" || process.env.NODE_ENV === "development";
57+
58+
// Middleware to handle auditing
859
export const auditMiddleware = (prisma: PrismaClient) => {
960
return prisma.$extends({
10-
model: {
61+
query: {
1162
$allModels: {
12-
async execute(args: any, next: (args: any) => Promise<any>) {
13-
const actionsToAudit = ["create", "update", "delete"];
63+
async $allOperations({ model, operation, args, query }: QueryContext) {
64+
const op = operation as AuditOperation;
65+
66+
// Skip if db transaction is about AuditLogs or models which doesnt need to be audited
67+
if (model === "AuditLogs" || !actionsToAudit.includes(op)) {
68+
return query(args);
69+
}
1470

15-
// Only track the defined actions.
16-
if (!actionsToAudit.includes(args.action)) return next(args);
71+
const debug = shouldDebug();
72+
if (debug) {
73+
console.debug(`[AUDIT] ${model}.${operation} intercepted`);
74+
}
1775

18-
let before: any = null;
76+
let before: Record<string, unknown> | null = null;
77+
const delegate = resolveDelegate(prisma, model);
1978

20-
if (args.action === "update" || args.action === "delete") {
21-
before = await next({ ...args, action: "findUnique" })
79+
// Fetch the current state of the entity which will be needed when returing before and after
80+
if ((op === "update" || op === "delete") && delegate && args?.where) {
81+
try {
82+
before = await delegate.findUnique({ where: args.where });
83+
if (debug) {
84+
console.debug(`[AUDIT] Loaded before state for ${model}.${operation}`);
85+
}
86+
} catch (error) {
87+
if (debug) {
88+
console.debug(`[AUDIT] Failed to load before state for ${model}.${operation}`, error);
89+
}
90+
}
2291
}
2392

24-
let result: any = null;
2593
const userId = requestContext.getStore()?.userId ?? "UNKNOWN";
2694

95+
let result: any;
2796
try {
28-
const result = await next(args)
29-
} catch (error: any) {
30-
throw new Error("Error while performing database query: ", error)
97+
// Executing the original query
98+
result = await query(args);
99+
if (debug) {
100+
console.debug(`[AUDIT] ${model}.${operation} executed successfully (id: ${result?.id ?? "n/a"})`);
101+
}
102+
} catch (error) {
103+
if (debug) {
104+
console.error(`[AUDIT] ${model}.${operation} failed`, error);
105+
}
106+
throw error;
31107
}
32108

33-
let changes: Record<string, { before: any, after: any }> = {};
34-
if (args.action === "update" && before) {
35-
for (const key in args.data) {
36-
if (before[key] !== result[key]) {
37-
changes[key] = { before: before[key], after: result[key] }
109+
// On update operaiton, tracking changes of which field changed and what changed.
110+
const changes: Record<string, { before: unknown; after: unknown }> = {};
111+
if (op === "update" && before && args?.data) {
112+
for (const key of Object.keys(args.data)) {
113+
const beforeValue = (before as any)[key];
114+
const afterValue = result?.[key];
115+
if (!Object.is(beforeValue, afterValue)) {
116+
changes[key] = {
117+
before: beforeValue,
118+
after: afterValue,
119+
};
38120
}
39121
}
122+
if (debug) {
123+
console.debug(`[AUDIT] Change set for ${model}.${operation}:`, changes);
124+
}
40125
}
41126

127+
128+
const entity = resolveEntity(model);
42129

130+
if (!entity) {
131+
if (debug) {
132+
console.debug(`[AUDIT] Skipping audit log for model ${model}; no enum mapping found.`);
133+
}
134+
return result;
135+
}
136+
137+
// Create an audit log
43138
await prisma.auditLogs.create({
44139
data: {
45-
action: args.action.toUpperCase() as ActionType,
46-
userId: userId,
47-
entity: args.model.toUpperCase() as Models,
48-
entitiyId: result.id,
49-
changes: Object.keys(changes).length ? changes : null
140+
action: op.toUpperCase() as ActionType,
141+
userId,
142+
entity,
143+
entityId: result?.id ?? args?.where?.id ?? "UNKNOWN",
144+
changes: Object.keys(changes).length ? changes : null,
145+
},
146+
});
50147

51-
}
52-
})
148+
if (debug) {
149+
console.debug(`[AUDIT] Audit log written for ${model}.${operation}`);
150+
}
53151

54152
return result;
55-
56-
}
57-
}
58-
}
59-
})
60-
}
153+
},
154+
},
155+
},
156+
});
157+
};

src/services/audit.service.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import prisma from "@/db/prisma";
2+
import { prismaSafe } from "@/lib/prismaSafe";
3+
4+
5+
class AuditService {
6+
async getAllAudits() {
7+
try {
8+
const [error, audits] = await prismaSafe(
9+
prisma.auditLogs.findMany({
10+
take: 10
11+
})
12+
)
13+
if (error) return { success: false, error }
14+
if (!audits) return { success: false, error: "No audits found" }
15+
return { success: true, data: audits }
16+
} catch (error: any) {
17+
console.log("Error while fetching auditLogs: ", error)
18+
return { success: false, error: "Internal Server Error" }
19+
}
20+
}
21+
}
22+
23+
const auditService = new AuditService();
24+
export default auditService;

0 commit comments

Comments
 (0)