From f109aa7b84cde7f79c5ea05c339c2803e2623b89 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Jun 2026 09:46:29 +0000 Subject: [PATCH 1/4] Initial plan From a0d94564e837dbe0665ff1ee22ccad77fc624ab5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Jun 2026 09:48:20 +0000 Subject: [PATCH 2/4] Plan minimal fix for restore route --- frontend/package-lock.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4bc59a3..3e886e9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1509,6 +1509,14 @@ "@types/react": "^18.0.0" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", From 74e092e3f565079f72bd5e223c7a0fc856c06beb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Jun 2026 09:49:24 +0000 Subject: [PATCH 3/4] Fix restore route to include fileId for access checks --- Readme.md | 5 ++--- backend/controllers/versionController.js | 2 +- backend/routes/fileRoutes.js | 2 +- frontend/src/components/VersionHistory.jsx | 2 +- frontend/src/services/fileApi.js | 4 ++-- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Readme.md b/Readme.md index 7571a07..4091ad0 100644 --- a/Readme.md +++ b/Readme.md @@ -502,7 +502,7 @@ CodeSync/ | `DELETE` | `/:id` | Delete file or folder (recursive) | Editor | | `POST` | `/:fileId/version` | Save version snapshot | Editor | | `GET` | `/:fileId/history` | Get version history | Viewer | -| `POST` | `/restore/:versionId` | Restore a file version | Editor | +| `POST` | `/restore/:fileId/:versionId` | Restore a file version | Editor | --- @@ -666,7 +666,7 @@ Editor clicks "Save Version" icon │ ▼ User clicks "Restore" on a past version - │ POST /api/files/restore/:versionId + │ POST /api/files/restore/:fileId/:versionId ▼ File.content overwritten with snapshot content (Socket will pick up and sync to all clients) @@ -806,4 +806,3 @@ This project is licensed under the **MIT License** — see the [LICENSE](LICENSE Built with ❤️ Alok Kumar Sharma

- diff --git a/backend/controllers/versionController.js b/backend/controllers/versionController.js index 0fe79b5..70fc65f 100644 --- a/backend/controllers/versionController.js +++ b/backend/controllers/versionController.js @@ -40,7 +40,7 @@ export const getHistory = async (req, res) => { } }; -// ─── POST /api/files/restore/:versionId ────────────────────────────────────── +// ─── POST /api/files/restore/:fileId/:versionId ────────────────────────────── export const restoreVersion = async (req, res) => { try { const { versionId } = req.params; diff --git a/backend/routes/fileRoutes.js b/backend/routes/fileRoutes.js index f7d7b5b..b07c4ef 100644 --- a/backend/routes/fileRoutes.js +++ b/backend/routes/fileRoutes.js @@ -38,6 +38,6 @@ router.delete('/:id', verifyFileAccess, checkRole('editor'), deleteFile); // Version history router.post('/:fileId/version', verifyFileAccess, checkRole('editor'), saveVersion); router.get('/:fileId/history', verifyFileAccess, getHistory); -router.post('/restore/:versionId', verifyFileAccess, checkRole('editor'), restoreVersion); +router.post('/restore/:fileId/:versionId', verifyFileAccess, checkRole('editor'), restoreVersion); export default router; diff --git a/frontend/src/components/VersionHistory.jsx b/frontend/src/components/VersionHistory.jsx index 43adf82..5344c8f 100644 --- a/frontend/src/components/VersionHistory.jsx +++ b/frontend/src/components/VersionHistory.jsx @@ -35,7 +35,7 @@ const VersionHistory = ({ fileId, onRestore, canEdit }) => { const handleRestore = async (versionId) => { if (!window.confirm('Restore this version? current changes will be overwritten.')) return; try { - const data = await restoreVersion(versionId); + const data = await restoreVersion(fileId, versionId); onRestore(data.file.content); toast.success('Version restored'); } catch (err) { diff --git a/frontend/src/services/fileApi.js b/frontend/src/services/fileApi.js index ef230aa..ac5325f 100644 --- a/frontend/src/services/fileApi.js +++ b/frontend/src/services/fileApi.js @@ -40,7 +40,7 @@ export const fetchHistory = async (fileId) => { return res.data; }; -export const restoreVersion = async (versionId) => { - const res = await api.post(`/api/files/restore/${versionId}`); +export const restoreVersion = async (fileId, versionId) => { + const res = await api.post(`/api/files/restore/${fileId}/${versionId}`); return res.data; }; From 3f0381173ecec6ded1f27b3ba5e2898f05eb2451 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Jun 2026 09:53:02 +0000 Subject: [PATCH 4/4] Add restore endpoint rate limiting --- backend/package-lock.json | 28 ++++++++++++++++++++++++++++ backend/package.json | 1 + backend/routes/fileRoutes.js | 10 +++++++++- 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 32f23b4..64f5674 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -13,6 +13,7 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.18.2", + "express-rate-limit": "^8.5.2", "express-session": "^1.19.0", "jsonwebtoken": "^9.0.2", "mongoose": "^8.1.1", @@ -743,6 +744,24 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/express-session": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.19.0.tgz", @@ -1133,6 +1152,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", diff --git a/backend/package.json b/backend/package.json index 1f07ada..aeb7e7c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -14,6 +14,7 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.18.2", + "express-rate-limit": "^8.5.2", "express-session": "^1.19.0", "jsonwebtoken": "^9.0.2", "mongoose": "^8.1.1", diff --git a/backend/routes/fileRoutes.js b/backend/routes/fileRoutes.js index b07c4ef..b08196d 100644 --- a/backend/routes/fileRoutes.js +++ b/backend/routes/fileRoutes.js @@ -1,4 +1,5 @@ import express from 'express'; +import rateLimit from 'express-rate-limit'; import authMiddleware from '../middleware/authMiddleware.js'; import { isEditor, requireRole, checkRole } from '../middleware/permissionMiddleware.js'; import { verifyFileAccess } from '../middleware/filePermission.js'; @@ -16,6 +17,13 @@ import { } from '../controllers/versionController.js'; const router = express.Router(); +const restoreVersionRateLimit = rateLimit({ + windowMs: 60 * 1000, + max: 30, + standardHeaders: true, + legacyHeaders: false, + message: { message: 'Too many requests. Please try again later.' } +}); router.use(authMiddleware); @@ -38,6 +46,6 @@ router.delete('/:id', verifyFileAccess, checkRole('editor'), deleteFile); // Version history router.post('/:fileId/version', verifyFileAccess, checkRole('editor'), saveVersion); router.get('/:fileId/history', verifyFileAccess, getHistory); -router.post('/restore/:fileId/:versionId', verifyFileAccess, checkRole('editor'), restoreVersion); +router.post('/restore/:fileId/:versionId', restoreVersionRateLimit, verifyFileAccess, checkRole('editor'), restoreVersion); export default router;