diff --git a/backend/package-lock.json b/backend/package-lock.json
index 034e9e7..4c4a8a1 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -21,7 +21,7 @@
"mongoose": "^8.0.3",
"morgan": "^1.10.0",
"multer": "^1.4.5-lts.1",
- "xss": "^1.0.14"
+ "socket.io": "^4.8.1"
},
"devDependencies": {
"@jest/globals": "^30.2.0",
@@ -622,11 +622,37 @@
"node": ">=8"
}
},
- "node_modules/@istanbuljs/schema": {
- "version": "0.1.3",
- "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
- "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
- "dev": true,
+ "node_modules/@socket.io/component-emitter": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
+ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
+ },
+ "node_modules/@types/cors": {
+ "version": "2.8.19",
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
+ "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "25.0.2",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.2.tgz",
+ "integrity": "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA==",
+ "dependencies": {
+ "undici-types": "~7.16.0"
+ }
+ },
+ "node_modules/@types/webidl-conversions": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
+ "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/whatwg-url": {
+ "version": "11.0.5",
+ "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz",
+ "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==",
"license": "MIT",
"engines": {
"node": ">=8"
@@ -762,58 +788,28 @@
"dev": true,
"license": "MIT",
"dependencies": {
- "@jest/console": "^29.7.0",
- "@jest/reporters": "^29.7.0",
- "@jest/test-result": "^29.7.0",
- "@jest/transform": "^29.7.0",
- "@jest/types": "^29.6.3",
- "@types/node": "*",
- "ansi-escapes": "^4.2.1",
- "chalk": "^4.0.0",
- "ci-info": "^3.2.0",
- "exit": "^0.1.2",
- "graceful-fs": "^4.2.9",
- "jest-changed-files": "^29.7.0",
- "jest-config": "^29.7.0",
- "jest-haste-map": "^29.7.0",
- "jest-message-util": "^29.7.0",
- "jest-regex-util": "^29.6.3",
- "jest-resolve": "^29.7.0",
- "jest-resolve-dependencies": "^29.7.0",
- "jest-runner": "^29.7.0",
- "jest-runtime": "^29.7.0",
- "jest-snapshot": "^29.7.0",
- "jest-util": "^29.7.0",
- "jest-validate": "^29.7.0",
- "jest-watcher": "^29.7.0",
- "micromatch": "^4.0.4",
- "pretty-format": "^29.7.0",
- "slash": "^3.0.0",
- "strip-ansi": "^6.0.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- },
- "peerDependencies": {
- "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
- },
- "peerDependenciesMeta": {
- "node-notifier": {
- "optional": true
- }
+ "sprintf-js": "~1.0.2"
}
},
- "node_modules/@jest/core/node_modules/@jest/expect-utils": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz",
- "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==",
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
- "license": "MIT",
- "dependencies": {
- "jest-get-type": "^29.6.3"
- },
+ "license": "MIT"
+ },
+ "node_modules/base64id": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
+ "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
"engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ "node": "^4.5.0 || >= 5.9"
}
},
"node_modules/@jest/core/node_modules/@jest/schemas": {
@@ -1581,21 +1577,58 @@
"@types/babel__traverse": "*"
}
},
- "node_modules/@types/babel__generator": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
- "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
- "dev": true,
- "license": "MIT",
+ "node_modules/engine.io": {
+ "version": "6.6.4",
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
+ "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
"dependencies": {
- "@babel/types": "^7.0.0"
+ "@types/cors": "^2.8.12",
+ "@types/node": ">=10.0.0",
+ "accepts": "~1.3.4",
+ "base64id": "2.0.0",
+ "cookie": "~0.7.2",
+ "cors": "~2.8.5",
+ "debug": "~4.3.1",
+ "engine.io-parser": "~5.2.1",
+ "ws": "~8.17.1"
+ },
+ "engines": {
+ "node": ">=10.2.0"
}
},
- "node_modules/@types/babel__template": {
- "version": "7.4.4",
- "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
- "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
- "dev": true,
+ "node_modules/engine.io-parser": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
+ "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/engine.io/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/engine.io/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.1.0",
@@ -8085,11 +8118,111 @@
"node": ">=14.18.0"
}
},
- "node_modules/supports-color": {
- "version": "5.5.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
- "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
- "dev": true,
+ "node_modules/socket.io": {
+ "version": "4.8.1",
+ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
+ "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
+ "dependencies": {
+ "accepts": "~1.3.4",
+ "base64id": "~2.0.0",
+ "cors": "~2.8.5",
+ "debug": "~4.3.2",
+ "engine.io": "~6.6.0",
+ "socket.io-adapter": "~2.5.2",
+ "socket.io-parser": "~4.2.4"
+ },
+ "engines": {
+ "node": ">=10.2.0"
+ }
+ },
+ "node_modules/socket.io-adapter": {
+ "version": "2.5.5",
+ "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
+ "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
+ "dependencies": {
+ "debug": "~4.3.4",
+ "ws": "~8.17.1"
+ }
+ },
+ "node_modules/socket.io-adapter/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io-adapter/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/socket.io-parser": {
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
+ "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.3.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/socket.io-parser/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io-parser/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/socket.io/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/sparse-bitfield": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
+ "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
"license": "MIT",
"dependencies": {
"has-flag": "^3.0.0"
@@ -8274,9 +8407,7 @@
"node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
- "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
- "dev": true,
- "license": "MIT"
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="
},
"node_modules/unpipe": {
"version": "1.0.0",
@@ -8398,41 +8529,24 @@
"node": ">=18"
}
},
- "node_modules/which": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
- "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "isexe": "^2.0.0"
- },
- "bin": {
- "node-which": "bin/node-which"
- },
+ "node_modules/ws": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
+ "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"engines": {
- "node": ">= 8"
- }
- },
- "node_modules/wrappy": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/write-file-atomic": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz",
- "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "imurmurhash": "^0.1.4",
- "signal-exit": "^4.0.1"
+ "node": ">=10.0.0"
},
- "engines": {
- "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
}
},
"node_modules/write-file-atomic/node_modules/signal-exit": {
diff --git a/backend/package.json b/backend/package.json
index 0ecceb7..116be93 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -14,23 +14,29 @@
"test:unit": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --testPathIgnorePatterns=integration",
"test:setup": "node -e \"console.log('To set up testing, see tests/QUICK_START.md')\""
},
- "keywords": ["wikipedia", "verification", "api", "express", "mongodb"],
+ "keywords": [
+ "wikipedia",
+ "verification",
+ "api",
+ "express",
+ "mongodb"
+ ],
"author": "",
"license": "MIT",
"dependencies": {
- "express": "^4.18.2",
- "mongoose": "^8.0.3",
"bcryptjs": "^2.4.3",
- "jsonwebtoken": "^9.0.2",
+ "cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
+ "express": "^4.18.2",
+ "express-rate-limit": "^7.1.5",
"express-validator": "^7.0.1",
"helmet": "^7.1.0",
- "express-rate-limit": "^7.1.5",
+ "jsonwebtoken": "^9.0.2",
+ "mongoose": "^8.0.3",
"morgan": "^1.10.0",
- "cookie-parser": "^1.4.6",
"multer": "^1.4.5-lts.1",
- "xss": "^1.0.14"
+ "socket.io": "^4.8.1"
},
"devDependencies": {
"@jest/globals": "^30.2.0",
diff --git a/backend/src/controllers/submissionController.js b/backend/src/controllers/submissionController.js
index 39e49c3..3dad783 100644
--- a/backend/src/controllers/submissionController.js
+++ b/backend/src/controllers/submissionController.js
@@ -3,6 +3,7 @@ import User from '../models/User.js';
import AppError from '../utils/AppError.js';
import { ErrorCodes } from '../utils/errorCodes.js';
+
// @desc Create new submission
// @route POST /api/submissions
// @access Private
@@ -30,6 +31,11 @@ export const createSubmission = async (req, res, next) => {
const populatedSubmission = await Submission.findById(submission._id)
.populate('submitter', 'username country');
+ // Emit real-time notification for new submission
+ if (global.webSocketService) {
+ global.webSocketService.broadcastSubmissionCreated(populatedSubmission);
+ }
+
res.status(201).json({
success: true,
submission: populatedSubmission
@@ -156,12 +162,18 @@ export const updateSubmission = async (req, res, next) => {
const { title, publisher, wikipediaArticle, category } = req.body;
+
submission = await Submission.findByIdAndUpdate(
req.params.id,
{ title, publisher, wikipediaArticle, category },
{ new: true, runValidators: true }
).populate('submitter', 'username country');
+ // Emit real-time notification for submission update
+ if (global.webSocketService) {
+ global.webSocketService.broadcastSubmissionUpdated(submission, req.user);
+ }
+
res.status(200).json({
success: true,
submission
@@ -187,8 +199,14 @@ export const deleteSubmission = async (req, res, next) => {
return next(new AppError('Not authorized to delete this submission', 403, ErrorCodes.UNAUTHORIZED_ACCESS));
}
+
await submission.deleteOne();
+ // Emit real-time notification for submission deletion
+ if (global.webSocketService) {
+ global.webSocketService.broadcastSubmissionDeleted(submission, req.user);
+ }
+
res.status(200).json({
success: true,
message: 'Submission deleted successfully'
@@ -250,10 +268,16 @@ export const verifySubmission = async (req, res, next) => {
$inc: { points: 5 }
});
+
submission = await Submission.findById(submission._id)
.populate('submitter', 'username country')
.populate('verifier', 'username country');
+ // Emit real-time notification for submission verification
+ if (global.webSocketService) {
+ global.webSocketService.broadcastSubmissionVerified(submission, req.user);
+ }
+
res.status(200).json({
success: true,
submission
diff --git a/backend/src/server.js b/backend/src/server.js
index 4ef5e92..f5e31f6 100644
--- a/backend/src/server.js
+++ b/backend/src/server.js
@@ -1,9 +1,14 @@
+
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import morgan from 'morgan';
import rateLimit from 'express-rate-limit';
import cookieParser from 'cookie-parser';
+import rateLimit from 'express-rate-limit';
+import { createServer } from 'http';
+import { Server as SocketIOServer } from 'socket.io';
+
import connectDB from './config/database.js';
import errorHandler from './middleware/errorHandler.js';
import { optionalAuth } from './middleware/auth.js';
@@ -15,6 +20,7 @@ import adminRoutes from './routes/adminRoutes.js';
import countryRoutes from './routes/countryRoutes.js';
import systemRoutes from './routes/systemRoutes.js';
import reportsRoutes from './routes/reportsRoutes.js';
+import WebSocketService from './services/websocket.js';
//importing the config file where all the environment variables are stored and loaded
import config from './config/config.js';
@@ -142,10 +148,41 @@ app.use((req, res) => {
});
});
-const PORT = process.env.PORT || config.port || 5000;
-const server = app.listen(PORT, () => {
+const PORT = process.env.PORT || 5000;
+
+// Create HTTP server
+const httpServer = createServer(app);
+
+// Initialize Socket.IO with CORS configuration
+const io = new SocketIOServer(httpServer, {
+ cors: {
+ origin: (origin, callback) => {
+ // Allow requests with no origin (like mobile apps or curl requests)
+ if (!origin) return callback(null, true);
+
+ if (allowedOrigins.indexOf(origin) !== -1) {
+ callback(null, true);
+ } else {
+ callback(new Error('Not allowed by CORS'));
+ }
+ },
+ credentials: true,
+ methods: ['GET', 'POST']
+ },
+ transports: ['websocket', 'polling']
+});
+
+// Initialize WebSocket Service
+const webSocketService = new WebSocketService(io);
+
+// Make WebSocket service available globally
+global.webSocketService = webSocketService;
+
+
+const server = httpServer.listen(config.port, () => {
console.log(`Server running in ${process.env.NODE_ENV} mode on port ${PORT}`);
+ console.log(`🚀 WebSocket server ready for real-time connections`);
});
// Handle unhandled promise rejections
diff --git a/backend/src/services/websocket.js b/backend/src/services/websocket.js
new file mode 100644
index 0000000..adc5bba
--- /dev/null
+++ b/backend/src/services/websocket.js
@@ -0,0 +1,251 @@
+import jwt from 'jsonwebtoken';
+import User from '../models/User.js';
+
+class WebSocketService {
+ constructor(io) {
+ this.io = io;
+ this.connectedUsers = new Map(); // userId -> socket
+ this.userSockets = new Map(); // socketId -> userId
+
+ this.setupMiddleware();
+ this.setupEventHandlers();
+ }
+
+ setupMiddleware() {
+ // Authentication middleware for WebSocket connections
+ this.io.use(async (socket, next) => {
+ try {
+ const token = socket.handshake.auth.token || socket.handshake.headers.authorization?.replace('Bearer ', '');
+
+ if (!token) {
+ return next(new Error('Authentication token required'));
+ }
+
+ const decoded = jwt.verify(token, process.env.JWT_SECRET);
+ const user = await User.findById(decoded.id).select('-password');
+
+ if (!user) {
+ return next(new Error('User not found'));
+ }
+
+ socket.user = user;
+ next();
+ } catch (error) {
+ next(new Error('Invalid authentication token'));
+ }
+ });
+ }
+
+ setupEventHandlers() {
+ this.io.on('connection', (socket) => {
+ console.log(`🔌 User ${socket.user.username} connected via WebSocket`);
+
+ // Store user connection
+ this.connectedUsers.set(socket.user._id.toString(), socket);
+ this.userSockets.set(socket.id, socket.user._id.toString());
+
+ // Join user-specific room
+ socket.join(`user_${socket.user._id}`);
+
+ // Join role-based rooms
+ socket.join(`role_${socket.user.role}`);
+
+ // Join country-based room if user is verifier or admin
+ if (socket.user.role === 'verifier' || socket.user.role === 'admin') {
+ socket.join(`country_${socket.user.country}`);
+ }
+
+ // Send connection confirmation
+ socket.emit('connected', {
+ message: 'Connected to real-time updates',
+ userId: socket.user._id,
+ role: socket.user.role,
+ country: socket.user.country
+ });
+
+ // Handle disconnection
+ socket.on('disconnect', () => {
+ console.log(`🔌 User ${socket.user.username} disconnected`);
+ this.connectedUsers.delete(socket.user._id.toString());
+ this.userSockets.delete(socket.id);
+ });
+
+ // Handle explicit disconnect
+ socket.on('disconnect_user', () => {
+ socket.disconnect();
+ });
+
+ // Handle ping/pong for connection health
+ socket.on('ping', () => {
+ socket.emit('pong');
+ });
+ });
+ }
+
+ // Emit to specific user
+ emitToUser(userId, event, data) {
+ this.io.to(`user_${userId}`).emit(event, data);
+ }
+
+ // Emit to all users in a role
+ emitToRole(role, event, data) {
+ this.io.to(`role_${role}`).emit(event, data);
+ }
+
+ // Emit to all users in a country
+ emitToCountry(country, event, data) {
+ this.io.to(`country_${country}`).emit(event, data);
+ }
+
+ // Emit to all connected users
+ emitToAll(event, data) {
+ this.io.emit(event, data);
+ }
+
+ // Get connected users count
+ getConnectedUsersCount() {
+ return this.connectedUsers.size;
+ }
+
+ // Check if user is online
+ isUserOnline(userId) {
+ return this.connectedUsers.has(userId.toString());
+ }
+
+ // Broadcast submission events
+ broadcastSubmissionCreated(submission) {
+ const submissionData = {
+ id: submission._id,
+ title: submission.title,
+ publisher: submission.publisher,
+ country: submission.country,
+ category: submission.category,
+ submitter: {
+ id: submission.submitter._id,
+ username: submission.submitter.username,
+ country: submission.submitter.country
+ },
+ createdAt: submission.createdAt,
+ message: `📝 New submission: "${submission.title}" from ${submission.publisher}`
+ };
+
+ // Notify all verifiers and admins
+ this.emitToRole('verifier', 'submission:created', submissionData);
+ this.emitToRole('admin', 'submission:created', submissionData);
+
+ // Notify country-specific verifiers
+ this.emitToCountry(submission.country, 'submission:created', submissionData);
+ }
+
+ broadcastSubmissionVerified(submission, verifier) {
+ const submissionData = {
+ id: submission._id,
+ title: submission.title,
+ publisher: submission.publisher,
+ country: submission.country,
+ category: submission.category,
+ status: submission.status,
+ credibility: submission.credibility,
+ verifierNotes: submission.verifierNotes,
+ verifier: {
+ id: verifier._id,
+ username: verifier.username,
+ country: verifier.country
+ },
+ verifiedAt: submission.verifiedAt,
+ message: submission.status === 'approved'
+ ? `✅ Submission verified: "${submission.title}" marked as ${submission.credibility}`
+ : `❌ Submission rejected: "${submission.title}"`
+ };
+
+ // Notify the submitter
+ this.emitToUser(submission.submitter._id, 'submission:verified', submissionData);
+
+ // Notify admins
+ this.emitToRole('admin', 'submission:verified', submissionData);
+
+ // Notify country verifiers about the action
+ this.emitToCountry(submission.country, 'submission:verified', submissionData);
+ }
+
+ broadcastSubmissionUpdated(submission, updatedBy) {
+ const submissionData = {
+ id: submission._id,
+ title: submission.title,
+ publisher: submission.publisher,
+ country: submission.country,
+ category: submission.category,
+ status: submission.status,
+ updatedBy: {
+ id: updatedBy._id,
+ username: updatedBy.username
+ },
+ updatedAt: submission.updatedAt,
+ message: `📝 Submission updated: "${submission.title}" by ${updatedBy.username}`
+ };
+
+ // Notify admins about updates
+ this.emitToRole('admin', 'submission:updated', submissionData);
+
+ // Notify country verifiers
+ this.emitToCountry(submission.country, 'submission:updated', submissionData);
+ }
+
+ broadcastSubmissionDeleted(submission, deletedBy) {
+ const submissionData = {
+ id: submission._id,
+ title: submission.title,
+ publisher: submission.publisher,
+ country: submission.country,
+ deletedBy: {
+ id: deletedBy._id,
+ username: deletedBy.username
+ },
+ message: `🗑️ Submission deleted: "${submission.title}" by ${deletedBy.username}`
+ };
+
+ // Notify admins
+ this.emitToRole('admin', 'submission:deleted', submissionData);
+
+ // Notify country verifiers
+ this.emitToCountry(submission.country, 'submission:deleted', submissionData);
+ }
+
+ // System-wide notifications
+ broadcastSystemNotification(message, type = 'info') {
+ const notification = {
+ id: Date.now(),
+ message,
+ type,
+ timestamp: new Date().toISOString()
+ };
+
+ this.emitToAll('system:notification', notification);
+ }
+
+ // Get statistics for admins
+ getConnectionStats() {
+ const stats = {
+ totalConnections: this.getConnectedUsersCount(),
+ usersByRole: {},
+ usersByCountry: {}
+ };
+
+ // Count users by role and country
+ for (const [userId, socket] of this.connectedUsers) {
+ const user = socket.user;
+
+ // Count by role
+ stats.usersByRole[user.role] = (stats.usersByRole[user.role] || 0) + 1;
+
+ // Count by country (for verifiers and admins)
+ if (user.role === 'verifier' || user.role === 'admin') {
+ stats.usersByCountry[user.country] = (stats.usersByCountry[user.country] || 0) + 1;
+ }
+ }
+
+ return stats;
+ }
+}
+
+export default WebSocketService;
diff --git a/frontend/package.json b/frontend/package.json
index 28434cf..8067ebe 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -32,6 +32,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "*",
"cmdk": "^1.1.1",
+ "date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.487.0",
@@ -41,8 +42,10 @@
"react-dom": "^18.3.1",
"react-hook-form": "^7.55.0",
"react-resizable-panels": "^2.1.7",
- "react-router-dom": "^7.10.1",
+ "react-router-dom": "^7.9.4",
+ "react-toastify": "^11.0.5",
"recharts": "^2.15.2",
+ "socket.io-client": "^4.8.1",
"sonner": "^2.0.3",
"tailwind-merge": "*",
"tailwindcss-animate": "^1.0.7",
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 5d72933..f1efdd9 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,10 +1,7 @@
-import React, { useEffect } from "react";
-import { Routes, Route, Link } from "react-router-dom";
-
-
-import { AuthProvider } from "./lib/auth-context";
-import { Navigation } from "./components/Navigation";
-
+import React, { useEffect } from 'react';
+import { BrowserRouter as Router, Routes, Route, Link, Navigate } from 'react-router-dom';
+import { AuthProvider } from './lib/auth-context';
+import { Navigation } from './components/Navigation';
import {
LandingPage,
AuthPage,
@@ -15,8 +12,9 @@ import {
CountryPage,
} from "./pages";
import { Toaster } from './components/ui/sonner';
-import { TooltipProvider } from './components/ui/tooltip';
+import NotificationSystem from './components/NotificationSystem';
import { initializeData } from './lib/mock-data';
+import { TooltipProvider } from './components/ui/tooltip'; // only if needed
/* -------------------- 404 Page -------------------- */
function NotFound() {
@@ -50,16 +48,15 @@ function AppContent() {
- A community-driven platform for verifying Wikipedia - references and maintaining source quality standards. + A community-driven platform for verifying Wikipedia references and maintaining source quality standards.