diff --git a/package-lock.json b/package-lock.json index 6f27140..9683cf8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "pg": "^8.9.0", "pg-hstore": "^2.3.4", "sequelize": "^6.29.0", + "socket.io": "^4.7.5", "source-map": "^0.7.4", "swagger-ui-express": "^4.6.1" }, @@ -2319,18 +2320,36 @@ "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", "dev": true }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + }, "node_modules/@types/chai": { "version": "4.3.12", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.12.tgz", "integrity": "sha512-zNKDHG/1yxm8Il6uCCVsm+dRdEsJlFoDu73X17y09bId6UwoYww+vFBsAcRzl8knM1sab3Dp1VRikFQwDOtDDw==", "dev": true }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, "node_modules/@types/cookiejar": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", "dev": true }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -2711,6 +2730,14 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "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": "^4.5.0 || >= 5.9" + } + }, "node_modules/base64url": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", @@ -3657,6 +3684,42 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", + "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", + "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/es-abstract": { "version": "1.22.5", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.5.tgz", @@ -8624,6 +8687,44 @@ "node": ">=6" } }, + "node_modules/socket.io": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", + "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.5.2", + "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.4", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.4.tgz", + "integrity": "sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.11.0" + } + }, + "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/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -9636,6 +9737,26 @@ "typedarray-to-buffer": "^3.1.5" } }, + "node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 6c70f1c..af7527a 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "pg": "^8.9.0", "pg-hstore": "^2.3.4", "sequelize": "^6.29.0", + "socket.io": "^4.7.5", "source-map": "^0.7.4", "swagger-ui-express": "^4.6.1" }, diff --git a/src/app.js b/src/app.js index 301f868..99a3039 100644 --- a/src/app.js +++ b/src/app.js @@ -1,5 +1,6 @@ /* eslint-disable no-undef */ import express from "express"; +import * as http from "http"; import session from "express-session"; import dotenv from "dotenv"; import cors from "cors"; @@ -12,12 +13,16 @@ import { listenToUserProjectAssigned, listenToLeadProjectAssigned, } from "./system/utils/listenToEvent"; +import { SocketUtil } from "./system/utils/SocketUtil"; import cronJob from "./system/utils/cronjob"; + dotenv.config(); const PORT = process.env.PORT || 4000; const app = express(); +const server = http.createServer(app); +SocketUtil.config(server); cronJob(); app.use(express.urlencoded({ extended: false })); app.use(cors()); @@ -51,7 +56,7 @@ const initializeDatabase = async () => { const start = () => { try { initializeDatabase(); - app.listen({ port: PORT }, () => + server.listen({ port: PORT }, () => process.stdout.write(`http://localhost:${PORT} \n`) ); } catch (error) { diff --git a/src/documentation/notification/index.js b/src/documentation/notification/index.js index 7d28917..c3446e4 100644 --- a/src/documentation/notification/index.js +++ b/src/documentation/notification/index.js @@ -3,7 +3,7 @@ import responses from "../responses"; const notification = { "/notification": { get: { - tags: ["Notification"], + tags: ["Notifications"], security: [{ JWT: [] }], summary: "get notifications", parameters: [ @@ -29,6 +29,28 @@ const notification = { responses, }, }, + + "/notification/{notificationId}": { + patch: { + tags: ["Notifications"], + security: [{ JWT: [] }], + summary: "Update User notifications", + parameters: [ + { + name: "notificationId", + in: "path", + description: "Id of the notification to be updated", + required: true, + schema: { + type: "string", + format: "uuid", + }, + }, + ], + consumes: ["application/json"], + responses, + }, + }, }; export default notification; diff --git a/src/restful/controllers/NotificationController.js b/src/restful/controllers/NotificationController.js index 24608de..be4147b 100644 --- a/src/restful/controllers/NotificationController.js +++ b/src/restful/controllers/NotificationController.js @@ -1,4 +1,7 @@ import NotificationService from "../../services/notificationService"; +import { knownEvents, subscribe } from "../../system/utils/event.util"; + +// subscribe(knownEvents.) export default class NotificationController { static async getNotifications(req, res) { @@ -19,4 +22,22 @@ export default class NotificationController { }); } } + + static async markOneNotification(req, res) { + try { + await NotificationService.updateNotifications( + { read: true }, + { id: req.params.notificationId, userId: req.user.id } + ); + + return res + .status(200) + .json({ message: "Marked one notification as read" }); + } catch (err) { + return res.status(500).json({ + error: err.message, + message: "Failed to update the notification", + }); + } + } } diff --git a/src/restful/middlewares/notificationMiddleware.js b/src/restful/middlewares/notificationMiddleware.js index fde2658..7476fbc 100644 --- a/src/restful/middlewares/notificationMiddleware.js +++ b/src/restful/middlewares/notificationMiddleware.js @@ -1,3 +1,5 @@ +import NotificationService from "../../services/notificationService"; + export const receivedPaginationFormat = async (req, res, next) => { req.query = { limit: req.query["limit"] || "10", @@ -5,3 +7,17 @@ export const receivedPaginationFormat = async (req, res, next) => { }; next(); }; + +export const checkIfHasNotificationId = async (req, res, next) => { + const notification = await NotificationService.getNotifications( + { + id: req.params.notificationId, + }, + null, + null + ); + if (notification.rows.length === 0) { + return res.status(404).json({ message: "Notification not found" }); + } + next(); +}; diff --git a/src/restful/routes/notificationRoutes.js b/src/restful/routes/notificationRoutes.js index 18ef0a8..a3919c7 100644 --- a/src/restful/routes/notificationRoutes.js +++ b/src/restful/routes/notificationRoutes.js @@ -3,7 +3,10 @@ import NotificationController from "../controllers/NotificationController"; import protect from "../middlewares"; import { validatePagination } from "../../validations/notification.validation"; -import { receivedPaginationFormat } from "../middlewares/notificationMiddleware"; +import { + checkIfHasNotificationId, + receivedPaginationFormat, +} from "../middlewares/notificationMiddleware"; const notificationRouter = express.Router(); @@ -15,4 +18,10 @@ notificationRouter.get( NotificationController.getNotifications ); +notificationRouter.patch( + "/:notificationId", + protect, + checkIfHasNotificationId, + NotificationController.markOneNotification +); export default notificationRouter; diff --git a/src/services/notificationService.js b/src/services/notificationService.js index a3ac0a4..f4331d3 100644 --- a/src/services/notificationService.js +++ b/src/services/notificationService.js @@ -20,6 +20,10 @@ export default class NotificationService { } } + static async updateNotifications(field, query) { + return await Notification.update(field, { where: query }); + } + static async getNotifications(query, limit, page) { const offset = (page - 1) * limit; diff --git a/src/system/utils/SocketUtil.js b/src/system/utils/SocketUtil.js new file mode 100644 index 0000000..4fbf9e0 --- /dev/null +++ b/src/system/utils/SocketUtil.js @@ -0,0 +1,23 @@ +import { Server } from "socket.io"; + +export class SocketUtil { + static io; + static socketEmit(key, data) { + SocketUtil.io.sockets.emit(key, data); + } + + static config(server) { + SocketUtil.io = new Server(server, { cors: { origin: "*" } }); + + SocketUtil.io.on("connection", (socket) => { + console.log("Connected", socket.id); + socket.on("join_room", (data) => { + socket.join(data); + }); + }); + } +} + +export const knownSockets = { + notification: "notification", +}; diff --git a/src/system/utils/listenToEvent.js b/src/system/utils/listenToEvent.js index fb83d05..81a3249 100644 --- a/src/system/utils/listenToEvent.js +++ b/src/system/utils/listenToEvent.js @@ -1,6 +1,7 @@ import { knownEvents, subscribe } from "./event.util"; import sendEmail from "../../services/emailService"; import NotificationService from "../../services/notificationService"; +import { knownSockets, SocketUtil } from "./SocketUtil"; const { createNotification } = NotificationService; @@ -18,7 +19,12 @@ export const listenToUserProjectAssigned = subscribe( userId: user.id, }; - await createNotification(userNotification); + const notification = await createNotification(userNotification); + + SocketUtil.socketEmit( + `${knownSockets.notification}.${user.id}`, + notification + ); sendEmail( email, @@ -42,7 +48,14 @@ export const listenToLeadProjectAssigned = subscribe( url: `${process.env.FRONTEND_URL}/projects/${projectId}`, userId: leadUserId, }; - await createNotification(leadNotification); + + const notification = await createNotification(leadNotification); + + SocketUtil.socketEmit( + `${knownSockets.notification}.${leadUserId}`, + notification + ); + sendEmail( email, "Added to the project as lead!",