fix(#24): Implement idempotency protection for critical write operations
Some checks are pending
Docker Test / test (push) Waiting to run
Some checks are pending
Docker Test / test (push) Waiting to run
This commit is contained in:
parent
6c25464369
commit
b44e7bf46c
6 changed files with 281 additions and 4 deletions
70
backend/src/middleware/idempotency.js
Normal file
70
backend/src/middleware/idempotency.js
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { pool } from '../db/connection.js';
|
||||
|
||||
// In-memory cache for idempotency keys (in production, use Redis or similar)
|
||||
const idempotencyCache = new Map();
|
||||
|
||||
export const requireIdempotencyKey = async (req, res, next) => {
|
||||
// Only apply to POST requests
|
||||
if (req.method !== 'POST') {
|
||||
return next();
|
||||
}
|
||||
|
||||
const idempotencyKey = req.headers['x-idempotency-key'];
|
||||
|
||||
// If no key provided, proceed normally (non-idempotent request)
|
||||
if (!idempotencyKey) {
|
||||
return next();
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if this key was used before
|
||||
const [existing] = await pool.query(
|
||||
'SELECT id, response_body, created_at FROM idempotency_keys WHERE key = ?',
|
||||
[idempotencyKey]
|
||||
);
|
||||
|
||||
if (existing.length > 0) {
|
||||
// Key already exists, return cached response
|
||||
const cachedResponse = JSON.parse(existing[0].response_body);
|
||||
return res.status(cachedResponse.status).json(cachedResponse.body);
|
||||
}
|
||||
|
||||
// Store the key with a placeholder for response
|
||||
await pool.query(
|
||||
'INSERT INTO idempotency_keys (key, created_at) VALUES (?, NOW())',
|
||||
[idempotencyKey]
|
||||
);
|
||||
|
||||
// Add a function to cache the response
|
||||
req.cacheIdempotentResponse = async (status, body) => {
|
||||
const responseBody = JSON.stringify({ status, body });
|
||||
await pool.query(
|
||||
'UPDATE idempotency_keys SET response_body = ? WHERE key = ?',
|
||||
[responseBody, idempotencyKey]
|
||||
);
|
||||
};
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('Error in idempotency middleware:', error);
|
||||
// If database fails, proceed without idempotency
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to create a unique key for a request
|
||||
export const generateIdempotencyKey = (req) => {
|
||||
// Create a simple hash of the request data
|
||||
const { method, url, body } = req;
|
||||
const keyString = `${method}-${url}-${JSON.stringify(body)}`;
|
||||
|
||||
// Simple hash function (in production, use proper hashing)
|
||||
let hash = 0;
|
||||
for (let i = 0; i < keyString.length; i++) {
|
||||
const char = keyString.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash |= 0; // Convert to 32bit integer
|
||||
}
|
||||
|
||||
return `idempotency-${Math.abs(hash)}`;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue