Skip to content

Commit fa78058

Browse files
authored
Merge pull request #41 from AbuJulaybeeb/fix/issue-13-audit-log-dispute-resolve
fix: attach auditLog middleware to JWT admin dispute endpoints
2 parents 1ac6c3c + 4050e03 commit fa78058

3 files changed

Lines changed: 60 additions & 0 deletions

File tree

src/__tests__/loanDispute.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,4 +280,61 @@ describe("Loan Dispute/Appeal Mechanism", () => {
280280
expect(res.status).toBe(200);
281281
expect(res.body.success).toBe(true);
282282
});
283+
284+
it("should allow admin to resolve dispute via JWT and log audit", async () => {
285+
const adminToken = jwt.sign(
286+
{ publicKey: TEST_PUBLIC_KEY, role: "admin", scopes: [] },
287+
process.env.JWT_SECRET!,
288+
{ algorithm: "HS256", expiresIn: "1h" },
289+
);
290+
291+
mockQuery
292+
.mockResolvedValueOnce(dbOk()) // [1] INSERT audit_logs (middleware runs first)
293+
.mockResolvedValueOnce(
294+
dbRows([
295+
{
296+
id: disputeId,
297+
loan_id: LOAN_ID,
298+
borrower: TEST_PUBLIC_KEY,
299+
status: "open",
300+
},
301+
]),
302+
) // [2] SELECT dispute
303+
.mockResolvedValueOnce(dbOk("UPDATE")) // [3] UPDATE dispute
304+
.mockResolvedValueOnce(dbOk()); // [4] INSERT contract_events
305+
306+
const res = await request(app)
307+
.post(`/api/admin/disputes/${disputeId}/resolve`)
308+
.set("Authorization", `Bearer ${adminToken}`)
309+
.send({
310+
action: "confirm",
311+
resolution: "JWT valid default.",
312+
token: "super_secret_token",
313+
});
314+
315+
expect(res.status).toBe(200);
316+
expect(res.body.success).toBe(true);
317+
318+
// Wait for the async audit log query to fire
319+
await new Promise((resolve) => setTimeout(resolve, 50));
320+
321+
const calls = mockQuery.mock.calls;
322+
const auditLogCall = calls.find(
323+
(call: any[]) =>
324+
typeof call[0] === "string" &&
325+
call[0].includes("INSERT INTO audit_logs"),
326+
);
327+
328+
expect(auditLogCall).toBeDefined();
329+
if (auditLogCall) {
330+
expect(auditLogCall[1][0]).toBe(TEST_PUBLIC_KEY);
331+
expect(auditLogCall[1][1]).toContain("POST /disputes");
332+
expect(auditLogCall[1][2]).toBe(`DisputeID:${disputeId}`);
333+
334+
const payloadString = auditLogCall[1][3];
335+
expect(payloadString).toContain("JWT valid default.");
336+
expect(payloadString).toContain("[REDACTED]");
337+
expect(payloadString).not.toContain("super_secret_token");
338+
}
339+
});
283340
});

src/middleware/auditLog.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ function extractTarget(req: Request): string | undefined {
3636
// Check common path parameters
3737
if (req.params.id) return `ID:${req.params.id}`;
3838
if (req.params.loanId) return `LoanID:${req.params.loanId}`;
39+
if (req.params.disputeId) return `DisputeID:${req.params.disputeId}`;
3940
if (req.params.address) return `Address:${req.params.address}`;
4041
if (req.params.userId) return `UserID:${req.params.userId}`;
4142
if (req.params.borrower) return `Borrower:${req.params.borrower}`;

src/routes/adminRoutes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,12 +106,14 @@ router.post(
106106
"/disputes/:disputeId/resolve",
107107
requireJwtAuth,
108108
requireRoles("admin"),
109+
auditLog,
109110
resolveLoanDispute,
110111
);
111112
router.post(
112113
"/disputes/:disputeId/reject",
113114
requireJwtAuth,
114115
requireRoles("admin"),
116+
auditLog,
115117
rejectLoanDispute,
116118
);
117119

0 commit comments

Comments
 (0)