Skip to content

Commit e076a82

Browse files
Merge pull request #865 from Glam26/feature/reports-idempotency
feat: idempotency-key middleware for POST/PATCH on /api/reports
2 parents 2f4b484 + 73a36c0 commit e076a82

3 files changed

Lines changed: 17 additions & 31 deletions

File tree

PR_DESCRIPTION.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
## Description
2-
This PR persists audit rows for state-changing calls on the `/api/impersonate` endpoint, capturing the actor, action, and before/after states.
2+
This PR implements the Idempotency-Key middleware for POST/PATCH endpoints on `/api/reports`, ensuring safe retries.
33

4-
- Enriched `createAuditLog` calls in `src/routes/admin/users/impersonate.ts` to log `beforeState: null` and `afterState: { targetAddress, role: "user" }`.
5-
- Restored missing `getCircuitBreaker` utility function in `src/lib/circuitBreaker.ts` to correctly mock/track circuit breakers.
6-
- Updated `tests/adminImpersonate.test.ts` and `tests/impersonateCircuitBreaker.test.ts` to strictly assert the new payload format.
4+
- Fixed a bug where `persisted` flag was missing in the global idempotency middleware, throwing a ReferenceError.
5+
- Fixed a bug where `res.json` manually inserted records causing duplicate database inserts.
6+
- Addressed undefined variables `TTL_MS` and `correlationId` in `idempotency.ts`.
7+
- Explicitly mounted the `idempotency` middleware inside `createReportsRouter` (in `src/routes/reports.ts`) for all `POST` and `PATCH` methods.
78

89
## API / Visible Changes
910
- Global `audit_logs` will now contain detailed states outlining what token roles were issued when impersonating.
1011

11-
Closes #
12+
Closes #123

src/middleware/idempotency.ts

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,8 @@ export async function idempotency(
159159
const originalJson = res.json.bind(res);
160160
const originalSend = res.send.bind(res);
161161

162+
let persisted = false;
163+
162164
function saveIdempotency(bodyToSave: unknown) {
163165
if (persisted) return;
164166
persisted = true;
@@ -169,7 +171,7 @@ export async function idempotency(
169171
const v = res.getHeader(h);
170172
if (v !== undefined) headers[h] = String(v);
171173
}
172-
const expiresAt = new Date(Date.now() + TTL_MS);
174+
const expiresAt = new Date(Date.now() + IDEMPOTENCY_TTL_MS);
173175
const valBody = typeof bodyToSave === "string" ? { content: bodyToSave } : bodyToSave;
174176
db.insert(idempotencyRecords)
175177
.values({
@@ -180,35 +182,12 @@ export async function idempotency(
180182
responseHeaders: headers,
181183
expiresAt,
182184
})
183-
.catch((err) => logger.error({ err, key, correlationId }, "idempotency_persist_failed"));
185+
.catch((err) => logger.error({ err, key, correlationId: reqId }, "idempotency_persist_failed"));
184186
}
185187
}
186188

187189
res.json = function (responseBody: unknown) {
188-
const status = res.statusCode;
189-
const headers: Record<string, string> = {};
190-
for (const h of REPLAY_HEADERS) {
191-
const v = res.getHeader(h);
192-
if (v) headers[h] = String(v);
193-
}
194-
195-
// Only cache successful mutations; client / server errors are not stored.
196-
if (status >= 200 && status < 300) {
197-
const expiresAt = new Date(Date.now() + IDEMPOTENCY_TTL_MS);
198-
db.insert(idempotencyRecords)
199-
.values({
200-
key,
201-
fingerprint,
202-
responseStatus: status,
203-
responseBody,
204-
responseHeaders: headers,
205-
expiresAt,
206-
})
207-
.catch((err) =>
208-
logger.error({ err, reqId, key }, "idempotency_persist_failed"),
209-
);
210-
}
211-
190+
saveIdempotency(responseBody);
212191
return originalJson(responseBody);
213192
};
214193

src/routes/reports.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { Router } from "express";
1515
import { requireAuth } from "../middleware/requireAuth";
1616
import { createPerUserTokenBucketLimiter } from "../middleware/rateLimit";
1717
import { scheduledReportsRouter } from "./reports/scheduled";
18+
import { idempotency } from "../middleware/idempotency";
1819

1920
export interface ReportsRouterOptions {
2021
rateLimit?: {
@@ -34,6 +35,11 @@ export function createReportsRouter(options: ReportsRouterOptions = {}): Router
3435
}),
3536
);
3637

38+
const mutationMethods = ["POST", "PATCH"];
39+
router.use((req, res, next) =>
40+
mutationMethods.includes(req.method) ? idempotency(req, res, next) : next()
41+
);
42+
3743
router.use("/scheduled", scheduledReportsRouter);
3844

3945
return router;

0 commit comments

Comments
 (0)