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() { } /> } /> } /> - } - /> - {/* 404 */} + {/* Proper 404 handling */} } /> + {/* Real-time Notification System */} + + {/* Footer */} @@ -69,29 +66,16 @@ function AppContent() {

About WikiSourceVerifier

- 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.

Quick Links

    -
  • - - Browse Directory - -
  • -
  • - - Submit Reference - -
  • -
  • - - Login / Register - -
  • +
  • Browse Directory
  • +
  • Submit Reference
  • +
  • Login / Register
@@ -106,34 +90,8 @@ function AppContent() {
- © 2025 WikiSourceVerifier. Built for the Wikipedia community. -
- - -
- - } /> - } /> - } /> - } /> - } /> - } /> - } /> - -
- - - - {/* Footer */} -