Skip to content

Commit 645dec1

Browse files
multiple shi
1 parent cdf1c41 commit 645dec1

File tree

5 files changed

+86
-19
lines changed

5 files changed

+86
-19
lines changed

backend/middlewares/idempotency.middleware.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { getCache, setCache } from "../utils/redisClient.js";
44
* Idempotency middleware to prevent duplicate requests
55
* Clients should send an `Idempotency-Key` header with a unique UUID
66
* If the same key is used within the TTL window, returns the cached response
7-
*
7+
*
88
* @param {number} ttlSeconds - Time to live for the cached response (default: 86400 = 24 hours)
99
* @param {boolean} strict - If true, requires Idempotency-Key header (default: false)
1010
*/
@@ -19,9 +19,9 @@ export const idempotencyMiddleware = (ttlSeconds = 86400, strict = false) => {
1919

2020
// If strict mode is enabled and no idempotency key is provided, reject the request
2121
if (strict && !idempotencyKey) {
22-
return res.status(400).json({
22+
return res.status(400).json({
2323
message: "Idempotency-Key header is required for this operation",
24-
hint: "Include a unique UUID in the 'Idempotency-Key' header to prevent duplicate requests"
24+
hint: "Include a unique UUID in the 'Idempotency-Key' header to prevent duplicate requests",
2525
});
2626
}
2727

backend/routes/report.routes.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,28 @@ router.get("/user/:userId", isAuthenticated, adminOnly, getReportsByUserId);
3434
router.get("/:id", isAuthenticated, getReportById);
3535

3636
// Update a report (with idempotency)
37-
router.patch("/:id", isAuthenticated, idempotencyMiddleware(3600), updateReport);
37+
router.patch(
38+
"/:id",
39+
isAuthenticated,
40+
idempotencyMiddleware(3600),
41+
updateReport,
42+
);
3843

3944
// Delete a report (with idempotency)
40-
router.delete("/:id", isAuthenticated, idempotencyMiddleware(3600), deleteReport);
45+
router.delete(
46+
"/:id",
47+
isAuthenticated,
48+
idempotencyMiddleware(3600),
49+
deleteReport,
50+
);
4151

4252
// Update report status (admin only, with idempotency)
43-
router.patch("/:id/status", isAuthenticated, adminOnly, idempotencyMiddleware(3600), updateReportStatus);
53+
router.patch(
54+
"/:id/status",
55+
isAuthenticated,
56+
adminOnly,
57+
idempotencyMiddleware(3600),
58+
updateReportStatus,
59+
);
4460

4561
export default router;

backend/routes/user.routes.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@ router.post(
2929
claimItem,
3030
);
3131
router.get("/my-claims", isAuthenticated, myClaims);
32-
router.delete("/my-claims/:claimId", isAuthenticated, idempotencyMiddleware(3600), deleteClaim);
32+
router.delete(
33+
"/my-claims/:claimId",
34+
isAuthenticated,
35+
idempotencyMiddleware(3600),
36+
deleteClaim,
37+
);
3338
router.get("/profile", isAuthenticated, getProfile);
3439
router.patch(
3540
"/profile",

frontend/src/pages/admin.jsx

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ const Admin = () => {
5959
});
6060

6161
const [remarkText, setRemarkText] = useState('');
62+
const [submitting, setSubmitting] = useState(false);
6263

6364
useEffect(() => {
6465
if (activeTab === 'items') {
@@ -160,47 +161,62 @@ const Admin = () => {
160161

161162
const handleCreateItem = async (e) => {
162163
e.preventDefault();
164+
if (submitting) return; // Prevent double submission
165+
166+
setSubmitting(true);
163167
try {
164168
// Don't send itemId - it's auto-generated by backend
165169
// eslint-disable-next-line no-unused-vars
166170
const { itemId, ...itemData } = formData;
167171
await adminApi.createItem(itemData);
168-
toast.success('Item created successfully');
172+
toast.success('Item created successfully!');
169173
setShowModal(false);
170174
resetForm();
171175
fetchItems();
172176
} catch (error) {
173177
toast.error(error.response?.data?.message || 'Failed to create item');
178+
} finally {
179+
setSubmitting(false);
174180
}
175181
};
176182

177183
const handleUpdateItem = async (e) => {
178184
e.preventDefault();
185+
if (submitting) return; // Prevent double submission
186+
187+
setSubmitting(true);
179188
try {
180189
await adminApi.updateItem(selectedItem._id, formData);
181-
toast.success('Item updated successfully');
190+
toast.success('Item updated successfully!');
182191
setShowModal(false);
183192
resetForm();
184193
fetchItems();
185194
} catch (error) {
186195
toast.error(error.response?.data?.message || 'Failed to update item');
196+
} finally {
197+
setSubmitting(false);
187198
}
188199
};
189200

190201
const handleDeleteItem = async () => {
202+
if (submitting) return; // Prevent double submission
203+
204+
setSubmitting(true);
191205
try {
192206
await adminApi.deleteItem(selectedItem._id);
193-
toast.success('Item deleted successfully');
207+
toast.success('Item deleted successfully!');
194208
setShowModal(false);
195209
fetchItems();
196210
} catch (error) {
197211
toast.error(error.response?.data?.message || 'Failed to delete item');
212+
} finally {
213+
setSubmitting(false);
198214
}
199215
};
200216

201217
const handleApproveClaim = async (claimId) => {
202218
try {
203-
const response = await adminApi.approveClaim(claimId, remarkText);
219+
await adminApi.approveClaim(claimId, remarkText);
204220
toast.success('✅ Claim approved successfully!');
205221
setShowModal(false);
206222
setRemarkText('');
@@ -223,7 +239,7 @@ const Admin = () => {
223239

224240
const handleRejectClaim = async (claimId) => {
225241
try {
226-
const response = await adminApi.rejectClaim(claimId, remarkText);
242+
await adminApi.rejectClaim(claimId, remarkText);
227243
toast.success('❌ Claim rejected successfully!');
228244
setShowModal(false);
229245
setRemarkText('');
@@ -243,12 +259,14 @@ const Admin = () => {
243259

244260
const openCreateModal = () => {
245261
resetForm();
262+
setSubmitting(false);
246263
setModalType('create');
247264
setShowModal(true);
248265
};
249266

250267
const openEditModal = (item) => {
251268
setSelectedItem(item);
269+
setSubmitting(false);
252270
setFormData({
253271
itemId: item.itemId,
254272
name: item.name,
@@ -263,6 +281,7 @@ const Admin = () => {
263281

264282
const openDeleteModal = (item) => {
265283
setSelectedItem(item);
284+
setSubmitting(false);
266285
setModalType('delete');
267286
setShowModal(true);
268287
};
@@ -299,6 +318,7 @@ const Admin = () => {
299318
description: ''
300319
});
301320
setSelectedItem(null);
321+
setSubmitting(false);
302322
if (formControls && typeof formControls.clear === 'function') formControls.clear();
303323
};
304324

@@ -1145,14 +1165,26 @@ const Admin = () => {
11451165
<div className="flex gap-3 mt-6">
11461166
<button
11471167
type="submit"
1148-
className="flex-1 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors"
1168+
disabled={submitting}
1169+
className={`flex-1 px-4 py-2 bg-indigo-600 text-white rounded-lg transition-colors flex items-center justify-center gap-2 ${
1170+
submitting ? 'opacity-70 cursor-not-allowed' : 'hover:bg-indigo-700'
1171+
}`}
11491172
>
1150-
{modalType === 'create' ? 'Create Item' : 'Update Item'}
1173+
{submitting && (
1174+
<svg className="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
1175+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
1176+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
1177+
</svg>
1178+
)}
1179+
{submitting ? 'Submitting...' : (modalType === 'create' ? 'Create Item' : 'Update Item')}
11511180
</button>
11521181
<button
11531182
type="button"
11541183
onClick={() => setShowModal(false)}
1184+
disabled={submitting}
11551185
className={`px-4 py-2 border rounded-lg transition-colors ${
1186+
submitting ? 'opacity-50 cursor-not-allowed' : ''
1187+
} ${
11561188
darkMode ? 'border-gray-600 text-gray-300 hover:bg-gray-700' : 'border-gray-300 text-gray-700 hover:bg-gray-50'
11571189
}`}
11581190
>
@@ -1172,13 +1204,25 @@ const Admin = () => {
11721204
<div className="flex gap-3">
11731205
<button
11741206
onClick={handleDeleteItem}
1175-
className="flex-1 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
1207+
disabled={submitting}
1208+
className={`flex-1 px-4 py-2 bg-red-600 text-white rounded-lg transition-colors flex items-center justify-center gap-2 ${
1209+
submitting ? 'opacity-70 cursor-not-allowed' : 'hover:bg-red-700'
1210+
}`}
11761211
>
1177-
Delete
1212+
{submitting && (
1213+
<svg className="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
1214+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
1215+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
1216+
</svg>
1217+
)}
1218+
{submitting ? 'Deleting...' : 'Delete'}
11781219
</button>
11791220
<button
11801221
onClick={() => setShowModal(false)}
1181-
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
1222+
disabled={submitting}
1223+
className={`px-4 py-2 border border-gray-300 text-gray-700 rounded-lg transition-colors ${
1224+
submitting ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-50'
1225+
}`}
11821226
>
11831227
Cancel
11841228
</button>

frontend/src/utils/api.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@ api.interceptors.request.use(
3535
config.headers.Authorization = `Bearer ${token}`;
3636
}
3737

38-
// Add idempotency key for POST, PUT, PATCH requests
39-
if (["post", "put", "patch"].includes(config.method?.toLowerCase())) {
38+
// Add idempotency key for POST, PUT, PATCH, DELETE requests
39+
if (
40+
["post", "put", "patch", "delete"].includes(config.method?.toLowerCase())
41+
) {
4042
config.headers["Idempotency-Key"] = generateUUID();
4143
}
4244

0 commit comments

Comments
 (0)