From 2460bebcb1e1b436a9ef915d57ae449e7073158f Mon Sep 17 00:00:00 2001 From: leandre Date: Sun, 24 Mar 2024 13:36:16 +0200 Subject: [PATCH 1/5] ft(email): Migrate email service to sendgrid --- package-lock.json | 91 ++++++++++++++++++++++++++++++++++++ package.json | 1 + src/services/emailService.js | 37 ++++++++------- 3 files changed, 112 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7ddc88a..7849cda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@babel/cli": "^7.20.7", + "@sendgrid/mail": "^8.1.1", "cors": "^2.8.5", "coveralls": "^3.1.1", "cron": "^2.3.0", @@ -2208,6 +2209,41 @@ "node": ">=14" } }, + "node_modules/@sendgrid/client": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-8.1.1.tgz", + "integrity": "sha512-pg0gYhAdyQil3Aga7/xHVcZFpvDAjAQMNBgMy5njTSkjACoWHmpSi1nWBZM7nIH/ptcRNMpnBbm9B5EvQ8fX2w==", + "dependencies": { + "@sendgrid/helpers": "^8.0.0", + "axios": "^1.6.4" + }, + "engines": { + "node": ">=12.*" + } + }, + "node_modules/@sendgrid/helpers": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-8.0.0.tgz", + "integrity": "sha512-Ze7WuW2Xzy5GT5WRx+yEv89fsg/pgy3T1E3FS0QEx0/VvRmigMZ5qyVGhJz4SxomegDkzXv/i0aFPpHKN8qdAA==", + "dependencies": { + "deepmerge": "^4.2.2" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/@sendgrid/mail": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-8.1.1.tgz", + "integrity": "sha512-tNtmgWLtBA7ZxKtPuEGOaIdEZP1vZSXsj5zg9iuoDBPVj/fNz+7LWzndvTcKumHk5eaDrS0UPXJqBm61m3+H1A==", + "dependencies": { + "@sendgrid/client": "^8.1.1", + "@sendgrid/helpers": "^8.0.0" + }, + "engines": { + "node": ">=12.*" + } + }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", @@ -2577,6 +2613,29 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" }, + "node_modules/axios": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", + "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.4.8", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.8.tgz", @@ -3296,6 +3355,14 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/default-require-extensions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", @@ -4429,6 +4496,25 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -7551,6 +7637,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", diff --git a/package.json b/package.json index 53cb6b7..50206d7 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "homepage": "https://github.com/denislohan/codehills-be#readme", "dependencies": { "@babel/cli": "^7.20.7", + "@sendgrid/mail": "^8.1.1", "cors": "^2.8.5", "coveralls": "^3.1.1", "cron": "^2.3.0", diff --git a/src/services/emailService.js b/src/services/emailService.js index 0310fbb..f217b59 100644 --- a/src/services/emailService.js +++ b/src/services/emailService.js @@ -1,19 +1,10 @@ -const nodemailer = require("nodemailer"); +import sgMail from "@sendgrid/mail"; const ejs = require("ejs"); const path = require("path"); -const { EMAIL_SERVICE, EMAIL_HOST, EMAIL_PORT, SENDER_EMAIL, EMAIL_PASSWORD } = - process.env; -let transporter = nodemailer.createTransport({ - service: EMAIL_SERVICE, - host: EMAIL_HOST, - port: EMAIL_PORT, - secure: false, // true for 465, false for other ports - auth: { - user: SENDER_EMAIL, - pass: EMAIL_PASSWORD, - }, -}); +const { SEND_GRID_EMAIL, SENDGRID_API_KEY } = process.env; + +sgMail.setApiKey(SENDGRID_API_KEY); export default async function sendEmail(to, subject, body, url) { try { @@ -22,14 +13,26 @@ export default async function sendEmail(to, subject, body, url) { { subject, body, url } ); - let info = await transporter.sendMail({ - from: SENDER_EMAIL, + let message = { to: to, + from: { + name: "CODEHILLS HR APP", + email: SEND_GRID_EMAIL, + }, subject: subject, html: html, - }); + }; - console.log("Message sent: %s", info.messageId); + return sgMail + .send(message) + .then((res) => { + console.log("email sent..."); + return res; + }) + .catch((error) => { + console.error(error.message); + throw error; + }); } catch (error) { console.error("Error sending email:", error); throw Error(error); From 467b0ddafe117713055b578eb310a335c3a1a1dd Mon Sep 17 00:00:00 2001 From: leandre Date: Mon, 25 Mar 2024 16:03:31 +0200 Subject: [PATCH 2/5] ft(improve): Add event handling while creating a project for email sending --- src/app.js | 4 ++ src/restful/controllers/projectController.js | 42 ++++------------ src/system/utils/event.util.js | 17 +++++++ src/system/utils/listenToEvent.js | 53 ++++++++++++++++++++ 4 files changed, 85 insertions(+), 31 deletions(-) create mode 100644 src/system/utils/event.util.js create mode 100644 src/system/utils/listenToEvent.js diff --git a/src/app.js b/src/app.js index f0d6cc4..301f868 100644 --- a/src/app.js +++ b/src/app.js @@ -8,6 +8,10 @@ import router from "./restful/routes/index"; import fileUploader from "express-fileupload"; import { associate } from "./database/relationships"; import { passport } from "./restful/routes/authRouters"; +import { + listenToUserProjectAssigned, + listenToLeadProjectAssigned, +} from "./system/utils/listenToEvent"; import cronJob from "./system/utils/cronjob"; dotenv.config(); diff --git a/src/restful/controllers/projectController.js b/src/restful/controllers/projectController.js index 931a728..2d94c6f 100644 --- a/src/restful/controllers/projectController.js +++ b/src/restful/controllers/projectController.js @@ -3,6 +3,7 @@ import sendEmail from "../../services/emailService"; import notificationService from "../../services/notificationService"; import projectService from "../../services/projectService"; import UserService from "../../services/userService"; +import { eventEmit, knownEvents } from "../../system/utils/event.util"; const { Project, UserProject } = db; const { @@ -120,22 +121,10 @@ export default class projectController { } await project.addUsers(user); - //send emails and notifications to assigned users - - const userNotification = { - title: "Added to the project!", - description: `You have been added to the ${project.name} project`, - url: `${process.env.FRONTEND_URL}/projects/${project.id}`, - userId: user.id, - }; - - await createNotification(userNotification); - sendEmail( - user.email, - "Added to the project", - `Hello ${user.firstName}, You have been added to the "${project.name}" project`, - `${process.env.FRONTEND_URL}/projects/${project.id}` - ); + eventEmit(knownEvents.addUserToProject, { + user, + project, + }); const adminNotification = { title: "Added to the project!", @@ -144,7 +133,7 @@ export default class projectController { userId: user.id, }; await createNotification(adminNotification); - await sendEmail( + sendEmail( admin.email, "Added a user to the project", `Hello admin, You have added ${user.firstName} to the ${project.name} project`, @@ -382,19 +371,10 @@ export default class projectController { await project.update({ projectLeadId: leadUser.id }); - const leadNotification = { - title: "Added to the project as lead!", - description: `You have been added to the project "${project.name}" as the project lead`, - url: `${process.env.FRONTEND_URL}/projects/${project.id}`, - userId: leadUser.id, - }; - await createNotification(leadNotification); - await sendEmail( - leadUser.email, - "Added to the project as lead!", - `Hello ${leadUser.displayName}, You have been added to the project "${project.name}" as the project lead.`, - `${process.env.FRONTEND_URL}/projects/${project.id}` - ); + eventEmit(knownEvents.addLeadToProject, { + leadUser, + project, + }); const adminNotification = { title: "Project lead updated", @@ -403,7 +383,7 @@ export default class projectController { userId: admin.id, }; await createNotification(adminNotification); - await sendEmail( + sendEmail( admin.email, "Project lead updated!", `Hello admin, You have added "${leadUser.displayName}" as the project lead for "${project.name}" project.`, diff --git a/src/system/utils/event.util.js b/src/system/utils/event.util.js new file mode 100644 index 0000000..a13d3a5 --- /dev/null +++ b/src/system/utils/event.util.js @@ -0,0 +1,17 @@ +import EventEmitter from "events"; + +const myEmmiter = new EventEmitter(); + +export const knownEvents = { + projectCreated: "projectCreated", + addUserToProject: "addUserToProject", + addLeadToProject: "addLeadToProject", +}; + +export const subscribe = (eventName, callback) => { + myEmmiter.on(eventName, callback); +}; + +export const eventEmit = (eventName, data) => { + myEmmiter.emit(eventName, data); +}; diff --git a/src/system/utils/listenToEvent.js b/src/system/utils/listenToEvent.js new file mode 100644 index 0000000..275f7fd --- /dev/null +++ b/src/system/utils/listenToEvent.js @@ -0,0 +1,53 @@ +import { knownEvents, subscribe } from "./event.util"; +import sendEmail from "../../services/emailService"; +import notificationService from "../../services/notificationService"; + +const { createNotification } = notificationService; + +export const listenToUserProjectAssigned = subscribe( + knownEvents.addUserToProject, + async (data) => { + const { user, project } = data; + const { email, firstName } = user.dataValues; + const { name, id: projectId } = project.dataValues; + + const userNotification = { + title: "Added to the project!", + description: `You have been added to the ${name} project`, + url: `${process.env.FRONTEND_URL}/projects/${projectId}`, + userId: user.id, + }; + + await createNotification(userNotification); + + sendEmail( + email, + "Added to the project", + `Hello ${firstName}, You have been added to the "${name}" project`, + `${process.env.FRONTEND_URL}/projects/${projectId}` + ); + } +); + +export const listenToLeadProjectAssigned = subscribe( + knownEvents.addLeadToProject, + async (data) => { + const { leadUser, project } = data; + const { email, displayName, id: leadUserId } = leadUser.dataValues; + const { name: projectName, id: projectId } = project.dataValues; + + const leadNotification = { + title: "Added to the project as lead!", + description: `You have been added to the project "${projectName}" as the project lead`, + url: `${process.env.FRONTEND_URL}/projects/${projectId}`, + userId: leadUserId, + }; + await createNotification(leadNotification); + sendEmail( + email, + "Added to the project as lead!", + `Hello ${displayName}, You have been added to the project "${projectName}" as the project lead.`, + `${process.env.FRONTEND_URL}/projects/${projectId}` + ); + } +); From 2ec4021ecb8ff2acd0e6c1c2c79a4db3b05836e5 Mon Sep 17 00:00:00 2001 From: leandre Date: Tue, 26 Mar 2024 15:41:57 +0200 Subject: [PATCH 3/5] ft(fetch-notification): Add endpoint for fethcing notification and fix Notification service class --- package-lock.json | 44 +++++++++++++++++++ package.json | 1 + src/database/relationships/index.js | 4 ++ src/documentation/index.js | 2 + src/documentation/notification/index.js | 34 ++++++++++++++ .../controllers/NotificationController.js | 22 ++++++++++ src/restful/controllers/projectController.js | 4 +- src/restful/controllers/userControllers.js | 4 +- .../middlewares/notificationMiddleware.js | 7 +++ src/restful/routes/index.js | 2 + src/restful/routes/notificationRoutes.js | 18 ++++++++ src/services/notificationService.js | 33 +++++++++++++- src/system/utils/listenToEvent.js | 4 +- src/validations/notification.validation.js | 16 +++++++ 14 files changed, 187 insertions(+), 8 deletions(-) create mode 100644 src/documentation/notification/index.js create mode 100644 src/restful/controllers/NotificationController.js create mode 100644 src/restful/middlewares/notificationMiddleware.js create mode 100644 src/restful/routes/notificationRoutes.js create mode 100644 src/validations/notification.validation.js diff --git a/package-lock.json b/package-lock.json index 7849cda..cf900f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "express": "^4.18.2", "express-fileupload": "^1.4.0", "express-session": "^1.17.3", + "joi": "^17.12.2", "jsonwebtoken": "^9.0.0", "nodemailer": "^6.9.1", "nyc": "^15.1.0", @@ -1901,6 +1902,19 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -2244,6 +2258,24 @@ "node": ">=12.*" } }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" + }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", @@ -5754,6 +5786,18 @@ "node": ">=8" } }, + "node_modules/joi": { + "version": "17.12.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.2.tgz", + "integrity": "sha512-RonXAIzCiHLc8ss3Ibuz45u28GOsWE1UpfDXLbN/9NKbL4tCJf8TWYVKsoYuuh+sAUt7fsSNpA+r2+TBA6Wjmw==", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, "node_modules/js-beautify": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz", diff --git a/package.json b/package.json index 50206d7..d6de5f2 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "express": "^4.18.2", "express-fileupload": "^1.4.0", "express-session": "^1.17.3", + "joi": "^17.12.2", "jsonwebtoken": "^9.0.0", "nodemailer": "^6.9.1", "nyc": "^15.1.0", diff --git a/src/database/relationships/index.js b/src/database/relationships/index.js index 0afad71..dc2a673 100644 --- a/src/database/relationships/index.js +++ b/src/database/relationships/index.js @@ -102,6 +102,10 @@ export const associate = () => { DB.FieldReview.belongsTo(DB.RatingField, { foreignKey: "ratingFieldId", as: "ratingField", + }); + DB.Notification.belongsTo(DB.User, { + foreignKey: "userId", + as: "user", onDelete: "CASCADE", }); }; diff --git a/src/documentation/index.js b/src/documentation/index.js index b7da801..8bce869 100644 --- a/src/documentation/index.js +++ b/src/documentation/index.js @@ -9,6 +9,7 @@ import search from "./search"; import dashboard from "./auth/dashboard"; import ratingCategories from "./ratingCategory"; import ratingFields from "./ratingField"; +import notification from "./notification"; const defaults = swaggerDoc.paths; @@ -30,6 +31,7 @@ const paths = { ...search, ...ratingCategories, ...ratingFields, + ...notification, }; const config = { diff --git a/src/documentation/notification/index.js b/src/documentation/notification/index.js new file mode 100644 index 0000000..7d28917 --- /dev/null +++ b/src/documentation/notification/index.js @@ -0,0 +1,34 @@ +import responses from "../responses"; + +const notification = { + "/notification": { + get: { + tags: ["Notification"], + security: [{ JWT: [] }], + summary: "get notifications", + parameters: [ + { + in: "query", + name: "page", + required: false, + schema: { + example: "", + }, + }, + { + in: "query", + name: "limit", + required: false, + description: "limit can be any positive number greater than 0", + schema: { + example: "", + }, + }, + ], + consumes: ["application/json"], + responses, + }, + }, +}; + +export default notification; diff --git a/src/restful/controllers/NotificationController.js b/src/restful/controllers/NotificationController.js new file mode 100644 index 0000000..24608de --- /dev/null +++ b/src/restful/controllers/NotificationController.js @@ -0,0 +1,22 @@ +import NotificationService from "../../services/notificationService"; + +export default class NotificationController { + static async getNotifications(req, res) { + try { + const { limit, page } = req.query; + const notifications = await NotificationService.getNotifications( + { userId: req.user.id }, + limit, + page + ); + return res + .status(200) + .json({ message: "Fetched all notifications", notifications }); + } catch (err) { + return res.status(500).json({ + error: err.message, + message: "Failed to fetch notifications", + }); + } + } +} diff --git a/src/restful/controllers/projectController.js b/src/restful/controllers/projectController.js index 2d94c6f..c5c8f68 100644 --- a/src/restful/controllers/projectController.js +++ b/src/restful/controllers/projectController.js @@ -1,6 +1,6 @@ import db from "../../database"; import sendEmail from "../../services/emailService"; -import notificationService from "../../services/notificationService"; +import NotificationService from "../../services/notificationService"; import projectService from "../../services/projectService"; import UserService from "../../services/userService"; import { eventEmit, knownEvents } from "../../system/utils/event.util"; @@ -17,7 +17,7 @@ const { } = projectService; const { findOneUser } = UserService; -const { createNotification } = notificationService; +const { createNotification } = NotificationService; export default class projectController { static async createProject(req, res) { diff --git a/src/restful/controllers/userControllers.js b/src/restful/controllers/userControllers.js index 378693a..1b794d8 100644 --- a/src/restful/controllers/userControllers.js +++ b/src/restful/controllers/userControllers.js @@ -2,10 +2,10 @@ import Response from "./../../system/helpers/Response"; import DB from "./../../database"; import UserService from "../../services/userService"; import { fileUploader } from "../../system/fileUploader"; -import notificationService from "../../services/notificationService"; +import NotificationService from "../../services/notificationService"; import sendEmail from "../../services/emailService"; const { User } = DB; -const { createNotification } = notificationService; +const { createNotification } = NotificationService; export default class UserControllers { static async addUser(req, res) { diff --git a/src/restful/middlewares/notificationMiddleware.js b/src/restful/middlewares/notificationMiddleware.js new file mode 100644 index 0000000..fde2658 --- /dev/null +++ b/src/restful/middlewares/notificationMiddleware.js @@ -0,0 +1,7 @@ +export const receivedPaginationFormat = async (req, res, next) => { + req.query = { + limit: req.query["limit"] || "10", + page: req.query["page"] || "1", + }; + next(); +}; diff --git a/src/restful/routes/index.js b/src/restful/routes/index.js index 606cc49..3550ab5 100644 --- a/src/restful/routes/index.js +++ b/src/restful/routes/index.js @@ -12,6 +12,7 @@ import searchRouter from "./searchRouter"; import dashboardRouter from "./dashboard"; import ratingCategoryRouter from "./ratingCategoryRoutes"; import ratingFieldRouter from "./ratingFieldRoutes"; +import notificationRouter from "./notificationRoutes"; const API_VERSION = process.env.API_VERSION || "v1"; const url = `/api/${API_VERSION}`; @@ -29,6 +30,7 @@ router.use(`${url}/search`, searchRouter); router.use(`${url}/dashboard`, dashboardRouter); router.use(`${url}/ratingCategories`, ratingCategoryRouter); router.use(`${url}/ratingFields`, ratingFieldRouter); +router.use(`${url}/notification`, notificationRouter); router.all(`${url}/`, (req, res) => { return res.status(200).json({ message: "Welcome to codehills backend!" }); diff --git a/src/restful/routes/notificationRoutes.js b/src/restful/routes/notificationRoutes.js new file mode 100644 index 0000000..18ef0a8 --- /dev/null +++ b/src/restful/routes/notificationRoutes.js @@ -0,0 +1,18 @@ +import express from "express"; +import NotificationController from "../controllers/NotificationController"; + +import protect from "../middlewares"; +import { validatePagination } from "../../validations/notification.validation"; +import { receivedPaginationFormat } from "../middlewares/notificationMiddleware"; + +const notificationRouter = express.Router(); + +notificationRouter.get( + "/", + protect, + validatePagination, + receivedPaginationFormat, + NotificationController.getNotifications +); + +export default notificationRouter; diff --git a/src/services/notificationService.js b/src/services/notificationService.js index 8fa2407..a3ac0a4 100644 --- a/src/services/notificationService.js +++ b/src/services/notificationService.js @@ -2,9 +2,9 @@ /* eslint-disable no-useless-catch */ /* eslint-disable no-unused-vars */ import db from "./../database"; -const { Notification } = db; +const { Notification, User } = db; -export default class notificationService { +export default class NotificationService { /** * Creates a new message. * @param {object} param details of a message. @@ -19,4 +19,33 @@ export default class notificationService { throw error; } } + + static async getNotifications(query, limit, page) { + const offset = (page - 1) * limit; + + const { count, rows } = await Notification.findAndCountAll({ + limit: limit, + offset: offset, + where: query, + order: [["createdAt", "DESC"]], + include: { + model: User, + as: "user", + attributes: { + exclude: ["bank", "microsoftId", "address"], + }, + }, + }); + + const totalPages = Math.ceil(count / limit); + const currentPage = page; + const totalItems = count; + return { totalPages, currentPage, totalItems, rows }; + } } + +export const knownNotificationType = { + projectCreated: "projectCreated", + projectAssigned: "projectAssigned", + projectCompleted: "projectCompleted", +}; diff --git a/src/system/utils/listenToEvent.js b/src/system/utils/listenToEvent.js index 275f7fd..fb83d05 100644 --- a/src/system/utils/listenToEvent.js +++ b/src/system/utils/listenToEvent.js @@ -1,8 +1,8 @@ import { knownEvents, subscribe } from "./event.util"; import sendEmail from "../../services/emailService"; -import notificationService from "../../services/notificationService"; +import NotificationService from "../../services/notificationService"; -const { createNotification } = notificationService; +const { createNotification } = NotificationService; export const listenToUserProjectAssigned = subscribe( knownEvents.addUserToProject, diff --git a/src/validations/notification.validation.js b/src/validations/notification.validation.js new file mode 100644 index 0000000..a5dc63d --- /dev/null +++ b/src/validations/notification.validation.js @@ -0,0 +1,16 @@ +import Joi from "joi"; + +export const validatePagination = async (req, res, next) => { + const registerSchema = Joi.object().keys({ + limit: Joi.number().integer().min(1), + page: Joi.number().integer().min(1), + }); + + const { error } = registerSchema.validate(req.query); + if (error) { + return res.status(400).json({ + error: error.details[0].message.replace(/[^a-zA-Z0-9 ]/g, ""), + }); + } + next(); +}; From 50367686c791d0ad1789d726164789027e294b5f Mon Sep 17 00:00:00 2001 From: leandre Date: Wed, 3 Apr 2024 12:32:39 +0200 Subject: [PATCH 4/5] ft(socket):Add socket to handle realtime notification --- package-lock.json | 121 ++++++++++++++++++ package.json | 1 + src/app.js | 7 +- src/documentation/notification/index.js | 24 +++- .../controllers/NotificationController.js | 18 +++ .../middlewares/notificationMiddleware.js | 16 +++ src/restful/routes/notificationRoutes.js | 11 +- src/services/notificationService.js | 4 + src/system/utils/SocketUtil.js | 23 ++++ src/system/utils/listenToEvent.js | 17 ++- 10 files changed, 237 insertions(+), 5 deletions(-) create mode 100644 src/system/utils/SocketUtil.js diff --git a/package-lock.json b/package-lock.json index cf900f9..707ae58 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", "valibot": "^0.30.0" @@ -2320,18 +2321,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", @@ -2712,6 +2731,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", @@ -3658,6 +3685,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", @@ -8625,6 +8688,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", @@ -9642,6 +9743,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 d6de5f2..253ebbb 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", "valibot": "^0.30.0" 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..7601129 100644 --- a/src/restful/controllers/NotificationController.js +++ b/src/restful/controllers/NotificationController.js @@ -19,4 +19,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!", From 2050ec1ccc1cdab7e1d87b8e9e56dee59d6ceac2 Mon Sep 17 00:00:00 2001 From: leandre Date: Wed, 19 Jun 2024 19:50:21 +0200 Subject: [PATCH 5/5] ft(improve): Replace Joi validator with valibot --- package-lock.json | 44 ------------------- package.json | 1 - .../middlewares/validateRequestBody.js | 16 ++++++- src/restful/routes/notificationRoutes.js | 6 +-- src/system/validators/index.js | 1 + src/system/validators/pagination.js | 8 ++++ src/validations/notification.validation.js | 16 ------- 7 files changed, 27 insertions(+), 65 deletions(-) create mode 100644 src/system/validators/pagination.js delete mode 100644 src/validations/notification.validation.js diff --git a/package-lock.json b/package-lock.json index 707ae58..d78d4cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,6 @@ "express": "^4.18.2", "express-fileupload": "^1.4.0", "express-session": "^1.17.3", - "joi": "^17.12.2", "jsonwebtoken": "^9.0.0", "nodemailer": "^6.9.1", "nyc": "^15.1.0", @@ -1903,19 +1902,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" - }, - "node_modules/@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -2259,24 +2245,6 @@ "node": ">=12.*" } }, - "node_modules/@sideway/address": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", - "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@sideway/formula": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", - "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" - }, - "node_modules/@sideway/pinpoint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" - }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", @@ -5849,18 +5817,6 @@ "node": ">=8" } }, - "node_modules/joi": { - "version": "17.12.2", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.2.tgz", - "integrity": "sha512-RonXAIzCiHLc8ss3Ibuz45u28GOsWE1UpfDXLbN/9NKbL4tCJf8TWYVKsoYuuh+sAUt7fsSNpA+r2+TBA6Wjmw==", - "dependencies": { - "@hapi/hoek": "^9.3.0", - "@hapi/topo": "^5.1.0", - "@sideway/address": "^4.1.5", - "@sideway/formula": "^3.0.1", - "@sideway/pinpoint": "^2.0.0" - } - }, "node_modules/js-beautify": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz", diff --git a/package.json b/package.json index 253ebbb..0f00b9b 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,6 @@ "express": "^4.18.2", "express-fileupload": "^1.4.0", "express-session": "^1.17.3", - "joi": "^17.12.2", "jsonwebtoken": "^9.0.0", "nodemailer": "^6.9.1", "nyc": "^15.1.0", diff --git a/src/restful/middlewares/validateRequestBody.js b/src/restful/middlewares/validateRequestBody.js index aa0814f..a51273e 100644 --- a/src/restful/middlewares/validateRequestBody.js +++ b/src/restful/middlewares/validateRequestBody.js @@ -1,6 +1,6 @@ import * as v from "valibot"; -import { filterValidationError } from "../../system/utils"; import Response from "../../system/helpers/Response"; +import { filterValidationError } from "../../system/utils"; export function validateRequestBody(validationSchema) { return (req, res, next) => { @@ -23,3 +23,17 @@ export function validateRequestBody(validationSchema) { return next(); }; } + +export function validateQueryParams(validationSchema) { + return (req, res, next) => { + const results = v.safeParse(validationSchema, req.query); + if (!results.success) { + return Response.error(res, 400, { + message: "Please correct your inputs", + errors: filterValidationError(results.issues), + }); + } + req.query = results.output; + return next(); + }; +} diff --git a/src/restful/routes/notificationRoutes.js b/src/restful/routes/notificationRoutes.js index a3919c7..d4dfa62 100644 --- a/src/restful/routes/notificationRoutes.js +++ b/src/restful/routes/notificationRoutes.js @@ -1,19 +1,19 @@ import express from "express"; +import { paginationSchema } from "../../system/validators"; import NotificationController from "../controllers/NotificationController"; - import protect from "../middlewares"; -import { validatePagination } from "../../validations/notification.validation"; import { checkIfHasNotificationId, receivedPaginationFormat, } from "../middlewares/notificationMiddleware"; +import { validateQueryParams } from "../middlewares/validateRequestBody"; const notificationRouter = express.Router(); notificationRouter.get( "/", protect, - validatePagination, + validateQueryParams(paginationSchema), receivedPaginationFormat, NotificationController.getNotifications ); diff --git a/src/system/validators/index.js b/src/system/validators/index.js index 161d7cd..4602d35 100644 --- a/src/system/validators/index.js +++ b/src/system/validators/index.js @@ -1 +1,2 @@ +export * from "./pagination"; export * from "./rating"; diff --git a/src/system/validators/pagination.js b/src/system/validators/pagination.js new file mode 100644 index 0000000..62c6fe9 --- /dev/null +++ b/src/system/validators/pagination.js @@ -0,0 +1,8 @@ +import * as v from "valibot"; + +export const paginationSchema = v.object({ + limit: v.optional( + v.string([v.minValue(1, "Limit should be greater than 0.")]) + ), + page: v.optional(v.string([v.minValue(1, "Page should be greater than 0.")])), +}); diff --git a/src/validations/notification.validation.js b/src/validations/notification.validation.js deleted file mode 100644 index a5dc63d..0000000 --- a/src/validations/notification.validation.js +++ /dev/null @@ -1,16 +0,0 @@ -import Joi from "joi"; - -export const validatePagination = async (req, res, next) => { - const registerSchema = Joi.object().keys({ - limit: Joi.number().integer().min(1), - page: Joi.number().integer().min(1), - }); - - const { error } = registerSchema.validate(req.query); - if (error) { - return res.status(400).json({ - error: error.details[0].message.replace(/[^a-zA-Z0-9 ]/g, ""), - }); - } - next(); -};