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
11 changes: 6 additions & 5 deletions PR_DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
## Description
This PR persists audit rows for state-changing calls on the `/api/impersonate` endpoint, capturing the actor, action, and before/after states.
This PR implements the Idempotency-Key middleware for POST/PATCH endpoints on `/api/reports`, ensuring safe retries.

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

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

Closes #
Closes #123
31 changes: 5 additions & 26 deletions src/middleware/idempotency.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ export async function idempotency(
const originalJson = res.json.bind(res);
const originalSend = res.send.bind(res);

let persisted = false;

function saveIdempotency(bodyToSave: unknown) {
if (persisted) return;
persisted = true;
Expand All @@ -169,7 +171,7 @@ export async function idempotency(
const v = res.getHeader(h);
if (v !== undefined) headers[h] = String(v);
}
const expiresAt = new Date(Date.now() + TTL_MS);
const expiresAt = new Date(Date.now() + IDEMPOTENCY_TTL_MS);
const valBody = typeof bodyToSave === "string" ? { content: bodyToSave } : bodyToSave;
db.insert(idempotencyRecords)
.values({
Expand All @@ -180,35 +182,12 @@ export async function idempotency(
responseHeaders: headers,
expiresAt,
})
.catch((err) => logger.error({ err, key, correlationId }, "idempotency_persist_failed"));
.catch((err) => logger.error({ err, key, correlationId: reqId }, "idempotency_persist_failed"));
}
}

res.json = function (responseBody: unknown) {
const status = res.statusCode;
const headers: Record<string, string> = {};
for (const h of REPLAY_HEADERS) {
const v = res.getHeader(h);
if (v) headers[h] = String(v);
}

// Only cache successful mutations; client / server errors are not stored.
if (status >= 200 && status < 300) {
const expiresAt = new Date(Date.now() + IDEMPOTENCY_TTL_MS);
db.insert(idempotencyRecords)
.values({
key,
fingerprint,
responseStatus: status,
responseBody,
responseHeaders: headers,
expiresAt,
})
.catch((err) =>
logger.error({ err, reqId, key }, "idempotency_persist_failed"),
);
}

saveIdempotency(responseBody);
return originalJson(responseBody);
};

Expand Down
6 changes: 6 additions & 0 deletions src/routes/reports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { Router } from "express";
import { requireAuth } from "../middleware/requireAuth";
import { createPerUserTokenBucketLimiter } from "../middleware/rateLimit";
import { scheduledReportsRouter } from "./reports/scheduled";
import { idempotency } from "../middleware/idempotency";

export interface ReportsRouterOptions {
rateLimit?: {
Expand All @@ -34,6 +35,11 @@ export function createReportsRouter(options: ReportsRouterOptions = {}): Router
}),
);

const mutationMethods = ["POST", "PATCH"];
router.use((req, res, next) =>
mutationMethods.includes(req.method) ? idempotency(req, res, next) : next()
);

router.use("/scheduled", scheduledReportsRouter);

return router;
Expand Down
Loading