From 0ab7579e38b158cfb9d0cf97c76c4ae8de47f3f7 Mon Sep 17 00:00:00 2001 From: Ramya Non-IU Date: Thu, 1 Feb 2024 15:17:47 -0800 Subject: [PATCH 01/10] Initial commit --- src/controllers/teamController.js | 66 ++++++++++++++++++++++++++++++ src/helpers/dashboardhelper.js | 12 +++++- src/models/team.js | 1 + src/routes/teamRouter.js | 4 +- src/utilities/addMembersToTeams.js | 2 +- 5 files changed, 82 insertions(+), 3 deletions(-) diff --git a/src/controllers/teamController.js b/src/controllers/teamController.js index 9cd98036e..14e2e9bac 100644 --- a/src/controllers/teamController.js +++ b/src/controllers/teamController.js @@ -182,6 +182,71 @@ const teamcontroller = function (Team) { .then(result => res.status(200).send(result)) .catch(error => res.status(500).send(error)); }; + const updateTeamVisibility = async (req, res) => { + console.log("==============> 9 "); + + const { visibility, teamId, userId } = req.body; + + try { + Team.findById(teamId, (error, team) => { + if (error || team === null) { + res.status(400).send('No valid records found'); + return; + } + + const memberIndex = team.members.findIndex(member => member.userId.toString() === userId); + if (memberIndex === -1) { + res.status(400).send('Member not found in the team.'); + return; + } + + team.members[memberIndex].visible = visibility; + team.modifiedDatetime = Date.now(); + + team.save() + .then(updatedTeam => { + // Additional operations after team.save() + const assignlist = []; + const unassignlist = []; + team.members.forEach(member => { + if (member.userId.toString() === userId) { + // Current user, no need to process further + return; + } + + if (visibility) { + assignlist.push(member.userId); + } else { + console.log("Visiblity set to false so removing it"); + unassignlist.push(member.userId); + } + }); + + const addTeamToUserProfile = userProfile + .updateMany({ _id: { $in: assignlist } }, { $addToSet: { teams: teamId } }) + .exec(); + const removeTeamFromUserProfile = userProfile + .updateMany({ _id: { $in: unassignlist } }, { $pull: { teams: teamId } }) + .exec(); + + Promise.all([addTeamToUserProfile, removeTeamFromUserProfile]) + .then(() => { + res.status(200).send({ result: 'Done' }); + }) + .catch((error) => { + res.status(500).send({ error }); + }); + }) + .catch(errors => { + console.error('Error saving team:', errors); + res.status(400).send(errors); + }); + + }); + } catch (error) { + res.status(500).send('Error updating team visibility: ' + error.message); + } + }; return { getAllTeams, @@ -191,6 +256,7 @@ const teamcontroller = function (Team) { putTeam, assignTeamToUsers, getTeamMembership, + updateTeamVisibility, }; }; diff --git a/src/helpers/dashboardhelper.js b/src/helpers/dashboardhelper.js index 89cecbab9..edc24745c 100644 --- a/src/helpers/dashboardhelper.js +++ b/src/helpers/dashboardhelper.js @@ -193,10 +193,20 @@ const dashboardhelper = function () { const teamsResult = await team .find({ 'members.userId': { $in: [userid] } }, { members: 1 }); + console.log(teamsResult); teamsResult.forEach((_myTeam) => { + let isUserVisible = false; _myTeam.members.forEach((teamMember) => { - if (!teamMember.userId.equals(userid)) teamMemberIds.push(teamMember.userId); + if (teamMember.userId.equals(userid) && teamMember.visible) isUserVisible = true; }); + if(isUserVisible) + { + _myTeam.members.forEach((teamMember) => { + if (!teamMember.userId.equals(userid)) + teamMemberIds.push(teamMember.userId); + }); + } + }); teamMembers = await userProfile diff --git a/src/models/team.js b/src/models/team.js index 1df50b95a..94d666b06 100644 --- a/src/models/team.js +++ b/src/models/team.js @@ -11,6 +11,7 @@ const team = new Schema({ { userId: { type: mongoose.SchemaTypes.ObjectId, required: true }, addDateTime: { type: Date, default: Date.now(), ref: 'userProfile' }, + visible: { type : 'Boolean', default:true}, }, ], teamCode: { diff --git a/src/routes/teamRouter.js b/src/routes/teamRouter.js index dd940504c..1ea5b7611 100644 --- a/src/routes/teamRouter.js +++ b/src/routes/teamRouter.js @@ -7,7 +7,9 @@ const router = function (team) { teamRouter.route('/team') .get(controller.getAllTeams) - .post(controller.postTeam); + .post(controller.postTeam) + .put(controller.updateTeamVisibility); + teamRouter.route('/team/:teamId') .get(controller.getTeamById) diff --git a/src/utilities/addMembersToTeams.js b/src/utilities/addMembersToTeams.js index b637fa2c1..431722470 100644 --- a/src/utilities/addMembersToTeams.js +++ b/src/utilities/addMembersToTeams.js @@ -17,7 +17,7 @@ const addMembersField = async () => { const updateOperations = allUsers .map((user) => { const { _id, teams, createdDate } = user; - return teams.map(team => Teams.updateOne({ _id: team }, { $addToSet: { members: { userId: _id, addDateTime: createdDate } } })); + return teams.map(team => Teams.updateOne({ _id: team }, { $addToSet: { members: { userId: _id, addDateTime: createdDate, visibility: true } } })); }) .flat(); From e8c5b23faed4d0ee0e740ca22cc2af61214bf795 Mon Sep 17 00:00:00 2001 From: Shengwei Peng Date: Sun, 21 Apr 2024 07:47:05 -0400 Subject: [PATCH 02/10] Fix Sentry Error: Object captured as exception with keys --- src/startup/logger.js | 3 +++ src/utilities/emailSender.js | 26 ++++++++++++++++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/startup/logger.js b/src/startup/logger.js index 52f07708f..7cf216d0b 100644 --- a/src/startup/logger.js +++ b/src/startup/logger.js @@ -59,6 +59,9 @@ exports.init = function () { Sentry.setTag('app_name', 'hgn-backend'); }; +/** + * @param {String} message message to be logged to Sentry + */ exports.logInfo = function (message) { if (process.env.NODE_ENV === 'local' || !process.env.NODE_ENV) { // Do not log to Sentry in local environment diff --git a/src/utilities/emailSender.js b/src/utilities/emailSender.js index 393d3b602..ef3e32580 100644 --- a/src/utilities/emailSender.js +++ b/src/utilities/emailSender.js @@ -35,8 +35,14 @@ const closure = () => { if (!nextItem) return; const { - recipient, subject, message, cc, bcc, replyTo, acknowledgingReceipt, -} = nextItem; + recipient, + subject, + message, + cc, + bcc, + replyTo, + acknowledgingReceipt, + } = nextItem; try { // Generate the accessToken on the fly @@ -62,12 +68,24 @@ const closure = () => { if (typeof acknowledgingReceipt === 'function') { acknowledgingReceipt(null, result); } - logger.logInfo(result); + // Prevent logging email in production + // Why? + // Could create a security risk + // Could create heavy loads on the server if the email is sent to many people + // Contain not much useful info: + // result format : {"accepted":["emailAddr"],"rejected":[],"envelopeTime":209,"messageTime":566,"messageSize":317,"response":"250 2.0.0 OK 17***69 p11-2***322qvd.85 - gsmtp","envelope":{"from":"emailAddr", "to":"emailAddr"}} + if (process.env.NODE_ENV === 'local' || !process.env.NODE_ENV) { + logger.logInfo(`Email sent: ${JSON.stringify(result)}`); + } } catch (error) { if (typeof acknowledgingReceipt === 'function') { acknowledgingReceipt(error, null); } - logger.logException(error, `Error sending email: from ${CLIENT_EMAIL} to ${recipient}`, `Extra Data: cc ${cc} bcc ${bcc} subject ${subject}`); + logger.logException( + error, + `Error sending email: from ${CLIENT_EMAIL} to ${recipient}`, + `Extra Data: cc ${cc} bcc ${bcc} subject ${subject}`, + ); } }, process.env.MAIL_QUEUE_INTERVAL || 1000); From e77625c0c3f6530d7d8a46bf71aadb2040f82b95 Mon Sep 17 00:00:00 2001 From: Shengwei Peng Date: Sun, 21 Apr 2024 10:55:23 -0400 Subject: [PATCH 03/10] Fix unhandled exception and implement new custom error handler - Fix uncaught error from express-validator in userProfileRouter - Implement new global custom error handler middleware - Implement new custom error classes - Enhance Sentry log readability with a new error tracking ID --- src/routes/userProfileRouter.js | 25 ++++++++++---- src/server.js | 5 +++ src/startup/logger.js | 2 +- src/utilities/customError.js | 45 ++++++++++++++++++++++++ src/utilities/customErrorHandler.js | 53 +++++++++++++++++++++++++++++ 5 files changed, 123 insertions(+), 7 deletions(-) create mode 100644 src/utilities/customError.js create mode 100644 src/utilities/customErrorHandler.js diff --git a/src/routes/userProfileRouter.js b/src/routes/userProfileRouter.js index 66b8976db..92670da32 100644 --- a/src/routes/userProfileRouter.js +++ b/src/routes/userProfileRouter.js @@ -1,6 +1,7 @@ import { body } from 'express-validator'; const express = require('express'); +const { ValidationError } = require('../utilities/customError'); const routes = function (userProfile) { const controller = require('../controllers/userProfileController')( @@ -13,8 +14,14 @@ const routes = function (userProfile) { .route('/userProfile') .get(controller.getUserProfiles) .post( - body('firstName').customSanitizer((value) => value.trim()), - body('lastName').customSanitizer((value) => value.trim()), + body('firstName').customSanitizer((value) => { + if (!value) throw new ValidationError('First Name is required'); + return value.trim(); + }), + body('lastName').customSanitizer((value) => { + if (!value) throw new ValidationError('Last Name is required'); + return value.trim(); + }), controller.postUserProfile, ); @@ -22,8 +29,14 @@ const routes = function (userProfile) { .route('/userProfile/:userId') .get(controller.getUserById) .put( - body('firstName').customSanitizer((value) => value.trim()), - body('lastName').customSanitizer((value) => value.trim()), + body('firstName').customSanitizer((value) => { + if (!value) throw new ValidationError('First Name is required'); + return value.trim(); + }), + body('lastName').customSanitizer((value) => { + if (!value) throw new ValidationError('Last Name is required'); + return value.trim(); + }), body('personalLinks').customSanitizer((value) => value.map((link) => { if (link.Name.replace(/\s/g, '') || link.Link.replace(/\s/g, '')) { return { @@ -32,7 +45,7 @@ const routes = function (userProfile) { Link: link.Link.replace(/\s/g, ''), }; } - throw new Error('Url not valid'); + throw new ValidationError('personalLinks not valid'); })), body('adminLinks').customSanitizer((value) => value.map((link) => { if (link.Name.replace(/\s/g, '') || link.Link.replace(/\s/g, '')) { @@ -42,7 +55,7 @@ const routes = function (userProfile) { Link: link.Link.replace(/\s/g, ''), }; } - throw new Error('Url not valid'); + throw new ValidationError('adminLinks not valid'); })), controller.putUserProfile, ) diff --git a/src/server.js b/src/server.js index 77dc5be81..8cd363ac4 100644 --- a/src/server.js +++ b/src/server.js @@ -7,6 +7,7 @@ const websockets = require("./websockets").default; const app = express(); const logger = require("./startup/logger"); +const globalErrorHandler = require("./utilities/customErrorHandler").default; logger.init(); // The request handler must be the first middleware on the app @@ -20,6 +21,10 @@ require("./startup/routes")(app); // The error handler must be before any other error middleware and after all controllers app.use(Sentry.Handlers.errorHandler()); + +// Make it the last middleware since it returns a response and do not call next() +app.use(globalErrorHandler); + const port = process.env.PORT || 4500; const server = app.listen(port, () => { diff --git a/src/startup/logger.js b/src/startup/logger.js index 7cf216d0b..1ddd7bda6 100644 --- a/src/startup/logger.js +++ b/src/startup/logger.js @@ -16,7 +16,7 @@ exports.init = function () { return null; } - if (event.modules) { + if (event.lastmodules) { // Don't send a list of modules with the event delete event.modules; } diff --git a/src/utilities/customError.js b/src/utilities/customError.js new file mode 100644 index 000000000..893cdf080 --- /dev/null +++ b/src/utilities/customError.js @@ -0,0 +1,45 @@ +/* eslint-disable max-classes-per-file */ +class CustomError extends Error { + statusCode; + + constructor(message, statusCode) { + super(message); + this.statusCode = statusCode; + this.name = this.constructor.name; + } +} + +class ValidationError extends CustomError { + constructor(message) { + super(message, 400); + } +} + +class AuthenticationError extends CustomError { + constructor(message) { + super(message, 401); + } +} + +class AuthorizationError extends CustomError { + constructor(message) { + super(message, 403); + } +} + +class RuntimeError extends CustomError { + constructor(message) { + super(message, 500); + } +} + +// Define other error classes here... + +module.exports = { + CustomError, + ValidationError, + AuthenticationError, + AuthorizationError, + RuntimeError, + // Export other error classes here... +}; diff --git a/src/utilities/customErrorHandler.js b/src/utilities/customErrorHandler.js new file mode 100644 index 000000000..1216f150f --- /dev/null +++ b/src/utilities/customErrorHandler.js @@ -0,0 +1,53 @@ +/* eslint-disable no-console */ +const { v4: uuidv4 } = require('uuid'); +const { CustomError } = require('./customError'); +const Logger = require('../startup/logger'); + +/** + * Custom error handler middleware for global errors. Make it the last middleware since it returns a response and do not call next(). +*/ +function globalErrorHandler(err, req, res, next) { + /** + * Notes: + * 1. We will need to implement a global distributed eventId for tracking errors + * if move to microservices artechtecture or with replicated services + * 2. Developer will use the eventId (Searchable) to trace the error in the Sentry.io + */ + const eventId = uuidv4(); + const tracking = `An internal error has occurred. If the issue persists, please contact the administrator and provide the trakcing ID: ${eventId}`; + let transactionName = ''; + const requestData = req.body && req.method ? JSON.stringify(req.body) : null; + + if (req.method) { + transactionName = transactionName.concat(req.method); + } + if (req.url) { + transactionName = transactionName.concat(' ', req.originalUrl); + } + + transactionName = transactionName.concat(' ', 'Tracking ID: ', eventId); + + // Log the error to Sentry if not in local environment + if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'production') { + Logger.logException( + err, + transactionName, + requestData, + ); + } else { + console.log(`An error occurred. Event ID: ${eventId} \nRequest Data: ${requestData}`); + console.error(err); + } + + // If the error is an instance of CustomError, return the error message and status code + if (err instanceof CustomError) { + return res.status(err.statusCode).json({ error: err.message, tracking }); + } + + // else return generic error message and status code 500 + return res.status(500).json({ + tracking, + }); +} + +export default globalErrorHandler; From 1404504d3acf7b550953786ae862d7e5b3a0e411 Mon Sep 17 00:00:00 2001 From: Shengwei Peng Date: Tue, 23 Apr 2024 12:53:52 -0400 Subject: [PATCH 04/10] Fix Non-Error exception captured error and update logger and global error handler --- src/helpers/userHelper.js | 29 ++++++++++++++++++----------- src/startup/logger.js | 11 ++++++++--- src/utilities/emailSender.js | 12 ++++++------ src/utilities/globalErrorHandler.js | 7 +++++-- 4 files changed, 37 insertions(+), 22 deletions(-) diff --git a/src/helpers/userHelper.js b/src/helpers/userHelper.js index fc1b89fea..dc1977a84 100644 --- a/src/helpers/userHelper.js +++ b/src/helpers/userHelper.js @@ -769,7 +769,8 @@ const userHelper = function () { }, ); - logger.logInfo(results); + logger.logInfo(`Job deleting blue squares older than 1 year finished + at ${moment().tz('America/Los_Angeles').format()} \nReulst: ${JSON.stringify(results)}`); } catch (err) { logger.logException(err); } @@ -1546,13 +1547,19 @@ const userHelper = function () { const { endDate } = user; endDate.setHours(endDate.getHours() + 7); if (moment().isAfter(moment(endDate).add(1, 'days'))) { - await userProfile.findByIdAndUpdate( - user._id, - user.set({ - isActive: false, - }), - { new: true }, - ); + try { + await userProfile.findByIdAndUpdate( + user._id, + user.set({ + isActive: false, + }), + { new: true }, + ); + } catch (err) { + // Log the error and continue to the next user + logger.logException(err, `Error in deActivateUser. Failed to update User ${user._id}`); + continue; + } const id = user._id; const person = await userProfile.findById(id); @@ -1575,7 +1582,7 @@ const userHelper = function () { } } } catch (err) { - logger.logException(err); + logger.logException(err, 'Unexpected rrror in deActivateUser'); } }; @@ -1585,7 +1592,7 @@ const userHelper = function () { try { await token.deleteMany({ expiration: { $lt: currentDate } }); } catch (error) { - logger.logException(error); + logger.logException(error, `Error in deleteExpiredTokens. Date ${currentDate}`); } }; @@ -1596,7 +1603,7 @@ const userHelper = function () { try { await timeOffRequest.deleteMany({ endingDate: { $lte: utcEndMoment } }); } catch (error) { - console.error('Error deleting expired time off requests:', error); + logger.logException(error, `Error deleting expired time-off requests: utcEndMoment ${utcEndMoment}`); } }; diff --git a/src/startup/logger.js b/src/startup/logger.js index 9511ea9be..9c1ea3cf0 100644 --- a/src/startup/logger.js +++ b/src/startup/logger.js @@ -62,6 +62,8 @@ exports.init = function () { }; /** + * Send message to sentry if in production or development environment. Otherwise, log to console. + * Please use JSON.stringify() to convert Object to String first. * @param {String} message message to be logged to Sentry */ exports.logInfo = function (message) { @@ -74,11 +76,12 @@ exports.logInfo = function (message) { }; /** - * + * Send log message to Sentry if in production or development environment. Otherwise, log to console. + * * @param {Error} error error object to be logged to Sentry * @param {String} transactionName (Optional) name assigned to a transaction. Seachable in Sentry (e.g. error in Function/Service/Operation/Job name) * @param {*} extraData (Optional) extra data to be logged to Sentry (e.g. request body, params, message, etc.) - * @param {String} trackingId (Optional) unique id to track the error in Sentry. Search by tag 'tacking_id:some_value' + * @param {String} trackingId (Optional) unique id to track the error in Sentry. Search by tag 'tacking_id' */ exports.logException = function (error, transactionName = null, extraData = null, trackingId = null) { if (process.env.NODE_ENV === 'local' || !process.env.NODE_ENV) { @@ -95,7 +98,9 @@ exports.logException = function (error, transactionName = null, extraData = null if (extraData !== null) { scope.setExtra('extraData', extraData); } - scope.setTag('tracking_id', trackingId); + if(trackingId !== null){ + scope.setTag('tracking_id', trackingId); + } return scope; }); } diff --git a/src/utilities/emailSender.js b/src/utilities/emailSender.js index c0c5ab5fb..eb8eca3de 100644 --- a/src/utilities/emailSender.js +++ b/src/utilities/emailSender.js @@ -58,11 +58,11 @@ const closure = () => { } // Prevent logging email in production // Why? - // Could create a security risk - // Could create heavy loads on the server if the email is sent to many people - // Contain not much useful info: + // 1. Could create a security risk + // 2. Could create heavy loads on the server if emails are sent to many people + // 3. Contain limited useful info: // result format : {"accepted":["emailAddr"],"rejected":[],"envelopeTime":209,"messageTime":566,"messageSize":317,"response":"250 2.0.0 OK 17***69 p11-2***322qvd.85 - gsmtp","envelope":{"from":"emailAddr", "to":"emailAddr"}} - if (process.env.NODE_ENV === 'local' || !process.env.NODE_ENV) { + if (process.env.NODE_ENV === 'local') { logger.logInfo(`Email sent: ${JSON.stringify(result)}`); } } catch (error) { @@ -71,8 +71,8 @@ const closure = () => { } logger.logException( error, - `Error sending email: from ${CLIENT_EMAIL} to ${recipient}`, - `Extra Data: cc ${cc} bcc ${bcc} subject ${subject}`, + `Error sending email: from ${CLIENT_EMAIL} to ${recipient} subject ${subject}`, + `Extra Data: cc ${cc} bcc ${bcc}`, ); } }, process.env.MAIL_QUEUE_INTERVAL || 1000); diff --git a/src/utilities/globalErrorHandler.js b/src/utilities/globalErrorHandler.js index 9913aa2a2..8c33ad40e 100644 --- a/src/utilities/globalErrorHandler.js +++ b/src/utilities/globalErrorHandler.js @@ -15,6 +15,7 @@ function globalErrorHandler(err, req, res, next) { */ const trackingId = uuidv4(); const errorMessage = `An internal error has occurred. If the issue persists, please contact the administrator and provide the trakcing ID: ${trackingId}`; + let transactionName = ''; const requestData = req.body && req.method ? JSON.stringify(req.body) : null; @@ -26,7 +27,9 @@ function globalErrorHandler(err, req, res, next) { } // transactionName = transactionName.concat(' ', 'Tracking ID: ', eventId); - + if(!err){ + transactionName = 'Critical: err parameter is missing. This is probably due to an improper error handling in the code.'; + } // Log the error to Sentry if not in local environment if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'production') { Logger.logException( @@ -36,7 +39,7 @@ function globalErrorHandler(err, req, res, next) { trackingId ); } else { - console.log(`An error occurred. Event ID: ${trackingId} \nRequest Data: ${requestData}`); + console.log(`An error occurred. Transaction: ${transactionName} \nRequest Data: ${requestData}`); console.error(err); } From ac334f4dd2ac3870ecdee195178200c04d44849e Mon Sep 17 00:00:00 2001 From: Shengwei Peng Date: Tue, 23 Apr 2024 13:25:52 -0400 Subject: [PATCH 05/10] Fix minor issues - Lint issue in multiple files - Typo in logger.js --- src/app.js | 5 ++++- src/server.js | 4 ++-- src/startup/logger.js | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/app.js b/src/app.js index d4f5aecdf..5c76ea0c9 100644 --- a/src/app.js +++ b/src/app.js @@ -3,15 +3,18 @@ const Sentry = require('@sentry/node'); const app = express(); const logger = require('./startup/logger'); -const globalErrorHandler = require("./utilities/globalErrorHandler").default; +const globalErrorHandler = require('./utilities/globalErrorHandler').default; logger.init(); + // The request handler must be the first middleware on the app app.use(Sentry.Handlers.requestHandler()); + require('./startup/cors')(app); require('./startup/bodyParser')(app); require('./startup/middleware')(app); require('./startup/routes')(app); + // The error handler must be before any other error middleware and after all controllers app.use(Sentry.Handlers.errorHandler()); diff --git a/src/server.js b/src/server.js index c5f64a9da..e53949703 100644 --- a/src/server.js +++ b/src/server.js @@ -4,8 +4,8 @@ require('dotenv').load(); const { app, logger } = require('./app'); const websockets = require('./websockets').default; -require("./startup/db")(); -require("./cronjobs/userProfileJobs")(); +require('./startup/db')(); +require('./cronjobs/userProfileJobs')(); const port = process.env.PORT || 4500; diff --git a/src/startup/logger.js b/src/startup/logger.js index 9c1ea3cf0..8081b0512 100644 --- a/src/startup/logger.js +++ b/src/startup/logger.js @@ -19,7 +19,7 @@ exports.init = function () { return null; } - if (event.lastmodules) { + if (event.modules) { // Don't send a list of modules with the event delete event.modules; } From f68c4ef72047f76bf984d1b2d00f42709bccce90 Mon Sep 17 00:00:00 2001 From: Shengwei Peng Date: Mon, 29 Apr 2024 15:24:38 -0400 Subject: [PATCH 06/10] Update README.md to include new env variable NODE_ENV --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 90890f72a..8c2da0c91 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ SMTPUser= TOKEN_LIFETIME= TOKEN_LIFETIME_UNITS= JWT_SECRET= - +NODE_ENV= prod | devolpment | local To make the process easy create a .env file and put the above text in the file and replace values with the correct values, which you can get from your teammates. Then do an npm run-script build followed by an npm start. By default, the services will start on port 4500 and you can http://localhost:4500/api/ to access the methods. A tools like Postman will be your best friend here, you will need to have an auth token placed in the 'Authorization' header which you can get through the networking tab of the local frontend when you login. - `npm run lint` -- fix lint From 398d3e34ef2986dca51d74207e192783943dacc6 Mon Sep 17 00:00:00 2001 From: Shengwei Peng Date: Mon, 29 Apr 2024 15:25:58 -0400 Subject: [PATCH 07/10] Update README.md styling --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8c2da0c91..01c9bc373 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,9 @@ SMTPUser= TOKEN_LIFETIME= TOKEN_LIFETIME_UNITS= JWT_SECRET= -NODE_ENV= prod | devolpment | local +NODE_ENV= + + To make the process easy create a .env file and put the above text in the file and replace values with the correct values, which you can get from your teammates. Then do an npm run-script build followed by an npm start. By default, the services will start on port 4500 and you can http://localhost:4500/api/ to access the methods. A tools like Postman will be your best friend here, you will need to have an auth token placed in the 'Authorization' header which you can get through the networking tab of the local frontend when you login. - `npm run lint` -- fix lint From d49d1a1a6898b73f24d724e0f0d15c108d3a378d Mon Sep 17 00:00:00 2001 From: Shengwei Peng <69882989+ptrpengdev@users.noreply.github.com> Date: Mon, 29 Apr 2024 15:31:07 -0400 Subject: [PATCH 08/10] Update README.md for the new env variable NODE_ENV --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 01c9bc373..bbea44b69 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,8 @@ SMTPPort= SMTPUser= TOKEN_LIFETIME= TOKEN_LIFETIME_UNITS= +NODE_ENV= `local` | `development` | `production`
JWT_SECRET= -NODE_ENV= To make the process easy create a .env file and put the above text in the file and replace values with the correct values, which you can get from your teammates. Then do an npm run-script build followed by an npm start. By default, the services will start on port 4500 and you can http://localhost:4500/api/ to access the methods. A tools like Postman will be your best friend here, you will need to have an auth token placed in the 'Authorization' header which you can get through the networking tab of the local frontend when you login. From aacf0b175f23f48df4214908342a563b472fe45f Mon Sep 17 00:00:00 2001 From: Shengwei Peng Date: Fri, 10 May 2024 20:08:37 -0400 Subject: [PATCH 09/10] Update Logger to automatically assign tacking id to all error event --- src/app.js | 2 +- src/routes/userProfileRouter.js | 2 +- src/startup/logger.js | 18 +++++-- src/utilities/customError.js | 45 ----------------- src/utilities/errorHandling/customError.js | 48 +++++++++++++++++++ .../{ => errorHandling}/globalErrorHandler.js | 22 ++++----- src/utilities/exceptionHandler.js | 17 ------- 7 files changed, 73 insertions(+), 81 deletions(-) delete mode 100644 src/utilities/customError.js create mode 100644 src/utilities/errorHandling/customError.js rename src/utilities/{ => errorHandling}/globalErrorHandler.js (81%) delete mode 100644 src/utilities/exceptionHandler.js diff --git a/src/app.js b/src/app.js index 5c76ea0c9..359db89c2 100644 --- a/src/app.js +++ b/src/app.js @@ -3,7 +3,7 @@ const Sentry = require('@sentry/node'); const app = express(); const logger = require('./startup/logger'); -const globalErrorHandler = require('./utilities/globalErrorHandler').default; +const globalErrorHandler = require('./utilities/errorHandling/globalErrorHandler').default; logger.init(); diff --git a/src/routes/userProfileRouter.js b/src/routes/userProfileRouter.js index e04f6e101..8fedb2e81 100644 --- a/src/routes/userProfileRouter.js +++ b/src/routes/userProfileRouter.js @@ -1,7 +1,7 @@ const { body } = require('express-validator'); const express = require('express'); -const { ValidationError } = require('../utilities/customError'); +const { ValidationError } = require('../utilities/errorHandling/customError'); const routes = function (userProfile) { const controller = require('../controllers/userProfileController')(userProfile); diff --git a/src/startup/logger.js b/src/startup/logger.js index 8081b0512..8a68ae737 100644 --- a/src/startup/logger.js +++ b/src/startup/logger.js @@ -1,6 +1,7 @@ /* eslint-disable no-console */ const Sentry = require('@sentry/node'); const { extraErrorDataIntegration } = require('@sentry/integrations'); +const { v4: uuidv4 } = require('uuid'); // Read more about intergration plugins here: https://docs.sentry.io/platforms/node/configuration/integrations/pluggable-integrations/ exports.init = function () { @@ -77,13 +78,18 @@ exports.logInfo = function (message) { /** * Send log message to Sentry if in production or development environment. Otherwise, log to console. - * + * * @param {Error} error error object to be logged to Sentry * @param {String} transactionName (Optional) name assigned to a transaction. Seachable in Sentry (e.g. error in Function/Service/Operation/Job name) * @param {*} extraData (Optional) extra data to be logged to Sentry (e.g. request body, params, message, etc.) * @param {String} trackingId (Optional) unique id to track the error in Sentry. Search by tag 'tacking_id' */ -exports.logException = function (error, transactionName = null, extraData = null, trackingId = null) { +exports.logException = function ( + error, + transactionName = null, + extraData = null, + trackingId = null, +) { if (process.env.NODE_ENV === 'local' || !process.env.NODE_ENV) { // Do not log to Sentry in local environment console.error(error); @@ -91,6 +97,9 @@ exports.logException = function (error, transactionName = null, extraData = null `Additional info \ntransactionName : ${transactionName} \nextraData: ${JSON.stringify(extraData)}`, ); } else { + if (trackingId == null) { + trackingId = uuidv4(); + } Sentry.captureException(error, (scope) => { if (transactionName !== null) { scope.setTransactionName(transactionName); @@ -98,10 +107,9 @@ exports.logException = function (error, transactionName = null, extraData = null if (extraData !== null) { scope.setExtra('extraData', extraData); } - if(trackingId !== null){ - scope.setTag('tracking_id', trackingId); - } + scope.setTag('tracking_id', trackingId); return scope; }); } + return trackingId; }; diff --git a/src/utilities/customError.js b/src/utilities/customError.js deleted file mode 100644 index 893cdf080..000000000 --- a/src/utilities/customError.js +++ /dev/null @@ -1,45 +0,0 @@ -/* eslint-disable max-classes-per-file */ -class CustomError extends Error { - statusCode; - - constructor(message, statusCode) { - super(message); - this.statusCode = statusCode; - this.name = this.constructor.name; - } -} - -class ValidationError extends CustomError { - constructor(message) { - super(message, 400); - } -} - -class AuthenticationError extends CustomError { - constructor(message) { - super(message, 401); - } -} - -class AuthorizationError extends CustomError { - constructor(message) { - super(message, 403); - } -} - -class RuntimeError extends CustomError { - constructor(message) { - super(message, 500); - } -} - -// Define other error classes here... - -module.exports = { - CustomError, - ValidationError, - AuthenticationError, - AuthorizationError, - RuntimeError, - // Export other error classes here... -}; diff --git a/src/utilities/errorHandling/customError.js b/src/utilities/errorHandling/customError.js new file mode 100644 index 000000000..81e38f083 --- /dev/null +++ b/src/utilities/errorHandling/customError.js @@ -0,0 +1,48 @@ +/* eslint-disable max-classes-per-file */ +/** + * By throwing an instance of this class, the global error handler middleware will return the error message and status code. + */ +class CustomError extends Error { + statusCode; + + constructor(message, statusCode) { + super(message); + this.statusCode = statusCode; + this.name = this.constructor.name; + } +} + +class ValidationError extends CustomError { + constructor(message) { + super(message, 400); + } +} + +class AuthenticationError extends CustomError { + constructor(message) { + super(message, 401); + } +} + +class AuthorizationError extends CustomError { + constructor(message) { + super(message, 403); + } +} + +class RuntimeError extends CustomError { + constructor(message) { + super(message, 500); + } +} + +// Define other error classes here... + +module.exports = { + CustomError, + ValidationError, + AuthenticationError, + AuthorizationError, + RuntimeError, + // Export other error classes here... +}; diff --git a/src/utilities/globalErrorHandler.js b/src/utilities/errorHandling/globalErrorHandler.js similarity index 81% rename from src/utilities/globalErrorHandler.js rename to src/utilities/errorHandling/globalErrorHandler.js index 8c33ad40e..90d3774f8 100644 --- a/src/utilities/globalErrorHandler.js +++ b/src/utilities/errorHandling/globalErrorHandler.js @@ -1,11 +1,11 @@ /* eslint-disable no-console */ const { v4: uuidv4 } = require('uuid'); const { CustomError } = require('./customError'); -const Logger = require('../startup/logger'); +const Logger = require('../../startup/logger'); /** * Custom error handler middleware for global unhandled errors. Make it the last middleware since it returns a response and do not call next(). -*/ + */ function globalErrorHandler(err, req, res, next) { /** * Notes: @@ -15,7 +15,7 @@ function globalErrorHandler(err, req, res, next) { */ const trackingId = uuidv4(); const errorMessage = `An internal error has occurred. If the issue persists, please contact the administrator and provide the trakcing ID: ${trackingId}`; - + let transactionName = ''; const requestData = req.body && req.method ? JSON.stringify(req.body) : null; @@ -27,19 +27,17 @@ function globalErrorHandler(err, req, res, next) { } // transactionName = transactionName.concat(' ', 'Tracking ID: ', eventId); - if(!err){ - transactionName = 'Critical: err parameter is missing. This is probably due to an improper error handling in the code.'; + if (!err) { + transactionName = + 'Critical: err parameter is missing. This is probably due to an improper error handling in the code.'; } // Log the error to Sentry if not in local environment if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'production') { - Logger.logException( - err, - transactionName, - requestData, - trackingId - ); + Logger.logException(err, transactionName, requestData, trackingId); } else { - console.log(`An error occurred. Transaction: ${transactionName} \nRequest Data: ${requestData}`); + console.log( + `An error occurred. Transaction: ${transactionName} \nRequest Data: ${requestData}`, + ); console.error(err); } diff --git a/src/utilities/exceptionHandler.js b/src/utilities/exceptionHandler.js deleted file mode 100644 index 9669f362a..000000000 --- a/src/utilities/exceptionHandler.js +++ /dev/null @@ -1,17 +0,0 @@ -const logger = require('../startup/logger'); - -const exceptionHandler = (err, req, res, next) => { - logger.logException(err); - - const errStatus = err.statusCode || 500; - const errMsg = err.message || 'Internal Server Error. Please try again later. If the problem persists, please contact support ID.'; - res.status(errStatus).json({ - success: false, - status: errStatus, - message: errMsg, - stack: !process.env.NODE_ENV || process.env.NODE_ENV === 'local' ? err.stack : {}, - }); - next(); -}; - -export default exceptionHandler; From b51b7338b6e6a4e7318966c15fc530166acab65c Mon Sep 17 00:00:00 2001 From: wang9hu Date: Mon, 15 Jul 2024 15:08:27 -0700 Subject: [PATCH 10/10] feat: allow user to max-out 5 hour goal limit --- src/websockets/TimerService/clientsHandler.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/websockets/TimerService/clientsHandler.js b/src/websockets/TimerService/clientsHandler.js index 4fed5334c..5a77593b0 100644 --- a/src/websockets/TimerService/clientsHandler.js +++ b/src/websockets/TimerService/clientsHandler.js @@ -117,7 +117,17 @@ const addGoal = (client, msg) => { const duration = parseInt(msg.split('=')[1]); const goalAfterAddition = moment.duration(client.goal).add(duration, 'milliseconds').asHours(); - if (goalAfterAddition > MAX_HOURS) return; + if (goalAfterAddition >= MAX_HOURS) { + const oldGoal = client.goal; + client.goal = MAX_HOURS * 60 * 60 * 1000; + client.time = moment + .duration(client.time) + .add(client.goal - oldGoal, 'milliseconds') + .asMilliseconds() + .toFixed(); + + return; + } client.goal = moment .duration(client.goal)