diff --git a/src/controllers/profileInitialSetupController.js b/src/controllers/profileInitialSetupController.js index 5b35af9bd..d7a050145 100644 --- a/src/controllers/profileInitialSetupController.js +++ b/src/controllers/profileInitialSetupController.js @@ -7,7 +7,6 @@ const config = require('../config'); const cache = require('../utilities/nodeCache')(); const LOGGER = require('../startup/logger'); - const TOKEN_HAS_SETUP_MESSAGE = 'SETUP_ALREADY_COMPLETED'; const TOKEN_CANCEL_MESSAGE = 'CANCELLED'; const TOKEN_INVALID_MESSAGE = 'INVALID'; @@ -121,7 +120,7 @@ const profileInitialSetupController = function ( ProfileInitialSetupToken, userProfile, Project, - MapLocation + MapLocation, ) { const { JWT_SECRET } = config; @@ -133,8 +132,8 @@ const profileInitialSetupController = function ( return response; } catch (err) { return { - type: "Error", - message: err.message || "An error occurred while saving the location", + type: 'Error', + message: err.message || 'An error occurred while saving the location', }; } }; @@ -156,17 +155,22 @@ const profileInitialSetupController = function ( const expiration = moment().add(3, 'week'); // Wrap multiple db operations in a transaction const session = await startSession(); - + session.startTransaction(); + try { - const existingEmail = await userProfile.findOne({ - email, - }); - + const existingEmail = await userProfile + .findOne({ + email, + }) + .session(session); + if (existingEmail) { + await session.abortTransaction(); + session.endSession(); return res.status(400).send('email already in use'); - } + } - await ProfileInitialSetupToken.findOneAndDelete({ email }); + await ProfileInitialSetupToken.findOneAndDelete({ email }).session(session); const newToken = new ProfileInitialSetupToken({ token, @@ -178,7 +182,7 @@ const profileInitialSetupController = function ( createdDate: Date.now(), }); - const savedToken = await newToken.save(); + const savedToken = await newToken.save({ session }); const link = `${baseUrl}/ProfileInitialSetup/${savedToken.token}`; await session.commitTransaction(); @@ -261,23 +265,23 @@ const profileInitialSetupController = function ( if (existingEmail) { return res.status(400).send('email already in use'); - } + } if (foundToken) { const expirationMoment = moment(foundToken.expiration); if (expirationMoment.isAfter(currentMoment)) { const defaultProject = await Project.findOne({ - projectName: "Orientation and Initial Setup", + projectName: 'Orientation and Initial Setup', }); const newUser = new userProfile(); newUser.password = req.body.password; - newUser.role = "Volunteer"; + newUser.role = 'Volunteer'; newUser.firstName = req.body.firstName; newUser.lastName = req.body.lastName; newUser.jobTitle = req.body.jobTitle; newUser.phoneNumber = req.body.phoneNumber; - newUser.bio = ""; + newUser.bio = ''; newUser.weeklycommittedHours = foundToken.weeklyCommittedHours; newUser.weeklycommittedHoursHistory = [ { @@ -291,33 +295,32 @@ const profileInitialSetupController = function ( newUser.projects = Array.from(new Set([defaultProject])); newUser.createdDate = Date.now(); newUser.email = req.body.email; - newUser.weeklySummaries = [{ summary: "" }]; + newUser.weeklySummaries = [{ summary: '' }]; newUser.weeklySummariesCount = 0; - newUser.weeklySummaryOption = "Required"; - newUser.mediaUrl = ""; + newUser.weeklySummaryOption = 'Required'; + newUser.mediaUrl = ''; newUser.collaborationPreference = req.body.collaborationPreference; - newUser.timeZone = req.body.timeZone || "America/Los_Angeles"; + newUser.timeZone = req.body.timeZone || 'America/Los_Angeles'; newUser.location = req.body.location; newUser.profilePic = req.body.profilePicture; newUser.permissions = { frontPermissions: [], backPermissions: [], }; - newUser.bioPosted = "default"; + newUser.bioPosted = 'default'; newUser.privacySettings.email = req.body.privacySettings.email; - newUser.privacySettings.phoneNumber = - req.body.privacySettings.phoneNumber; - newUser.teamCode = ""; + newUser.privacySettings.phoneNumber = req.body.privacySettings.phoneNumber; + newUser.teamCode = ''; newUser.isFirstTimelog = true; const savedUser = await newUser.save(); emailSender( - process.env.MANAGER_EMAIL || "jae@onecommunityglobal.org", // "jae@onecommunityglobal.org" + process.env.MANAGER_EMAIL || 'jae@onecommunityglobal.org', // "jae@onecommunityglobal.org" `NEW USER REGISTERED: ${savedUser.firstName} ${savedUser.lastName}`, informManagerMessage(savedUser), null, - null + null, ); await ProfileInitialSetupToken.findByIdAndDelete(foundToken._id); @@ -325,16 +328,13 @@ const profileInitialSetupController = function ( userid: savedUser._id, role: savedUser.role, permissions: savedUser.permissions, - expiryTimestamp: moment().add( - config.TOKEN.Lifetime, - config.TOKEN.Units - ), + expiryTimestamp: moment().add(config.TOKEN.Lifetime, config.TOKEN.Units), }; const token = jwt.sign(jwtPayload, JWT_SECRET); const locationData = { - title: "", + title: '', firstName: req.body.firstName, lastName: req.body.lastName, jobTitle: req.body.jobTitle, @@ -345,7 +345,7 @@ const profileInitialSetupController = function ( res.send({ token }).status(200); const mapEntryResult = await setMapLocation(locationData); - if (mapEntryResult.type === "Error") { + if (mapEntryResult.type === 'Error') { console.log(mapEntryResult.message); } @@ -361,9 +361,9 @@ const profileInitialSetupController = function ( email: savedUser.email, }; - const allUserCache = JSON.parse(cache.getCache("allusers")); + const allUserCache = JSON.parse(cache.getCache('allusers')); allUserCache.push(NewUserCache); - cache.setCache("allusers", JSON.stringify(allUserCache)); + cache.setCache('allusers', JSON.stringify(allUserCache)); } else { return res.status(400).send('Token is expired'); } @@ -499,7 +499,7 @@ const profileInitialSetupController = function ( if (foundToken) { return res.status(200).send({ userAPIKey: premiumKey }); - } + } return res.status(403).send('Unauthorized Request'); }; @@ -514,16 +514,11 @@ const profileInitialSetupController = function ( const getTotalCountryCount = async (req, res) => { try { const users = []; - const results = await userProfile.find( - {}, - "location totalTangibleHrs hoursByCategory" - ); + const results = await userProfile.find({}, 'location totalTangibleHrs hoursByCategory'); results.forEach((item) => { if ( - (item.location?.coords.lat && - item.location?.coords.lng && - item.totalTangibleHrs >= 10) || + (item.location?.coords.lat && item.location?.coords.lng && item.totalTangibleHrs >= 10) || (item.location?.coords.lat && item.location?.coords.lng && calculateTotalHours(item.hoursByCategory) >= 10) @@ -531,13 +526,13 @@ const profileInitialSetupController = function ( users.push(item); } }); - const modifiedUsers = users.map(item => ({ + const modifiedUsers = users.map((item) => ({ location: item.location, })); const mapUsers = await MapLocation.find({}); const combined = [...modifiedUsers, ...mapUsers]; - const countries = combined.map(user => user.location.country); + const countries = combined.map((user) => user.location.country); const totalUniqueCountries = [...new Set(countries)].length; return res.status(200).send({ CountryCount: totalUniqueCountries }); } catch (error) { @@ -546,8 +541,6 @@ const profileInitialSetupController = function ( } }; - - /** * Returns a list of setup token in not completed status * @param {*} req HTTP request include requester role information @@ -557,29 +550,36 @@ const profileInitialSetupController = function ( const getSetupInvitation = (req, res) => { const { role } = req.body.requestor; if (role === 'Administrator' || role === 'Owner') { - try{ - ProfileInitialSetupToken - .find({ isSetupCompleted: false }) - .sort({ createdDate: -1 }) - .exec((err, result) => { - // Handle the result - if (err) { - LOGGER.logException(err); - return res.status(500).send('Internal Error: Please retry. If the problem persists, please contact the administrator'); - } - return res.status(200).send(result); - }); - } catch (error) { - LOGGER.logException(error); - return res.status(500).send('Internal Error: Please retry. If the problem persists, please contact the administrator'); - } + try { + ProfileInitialSetupToken.find({ isSetupCompleted: false }) + .sort({ createdDate: -1 }) + .exec((err, result) => { + // Handle the result + if (err) { + LOGGER.logException(err); + return res + .status(500) + .send( + 'Internal Error: Please retry. If the problem persists, please contact the administrator', + ); + } + return res.status(200).send(result); + }); + } catch (error) { + LOGGER.logException(error); + return res + .status(500) + .send( + 'Internal Error: Please retry. If the problem persists, please contact the administrator', + ); + } } else { return res.status(403).send('You are not authorized to get setup history.'); } }; /** - * Cancel the setup token + * Cancel the setup token * @param {*} req HTTP request include requester role information * @param {*} res HTTP response include whether the setup invitation record is successfully cancelled * @returns @@ -589,33 +589,40 @@ const profileInitialSetupController = function ( const { token } = req.body; if (role === 'Administrator' || role === 'Owner') { try { - ProfileInitialSetupToken - .findOneAndUpdate( - { token }, - { isCancelled: true }, - (err, result) => { - if (err) { - LOGGER.logException(err); - return res.status(500).send('Internal Error: Please retry. If the problem persists, please contact the administrator'); - } - sendEmailWithAcknowledgment( - result.email, - 'One Community: Your Profile Setup Link Has Been Deactivated', - sendCancelLinkMessage(), - ); - return res.status(200).send(result); - }, - ); - } catch (error) { + ProfileInitialSetupToken.findOneAndUpdate( + { token }, + { isCancelled: true }, + (err, result) => { + if (err) { + LOGGER.logException(err); + return res + .status(500) + .send( + 'Internal Error: Please retry. If the problem persists, please contact the administrator', + ); + } + sendEmailWithAcknowledgment( + result.email, + 'One Community: Your Profile Setup Link Has Been Deactivated', + sendCancelLinkMessage(), + ); + return res.status(200).send(result); + }, + ); + } catch (error) { LOGGER.logException(error); - return res.status(500).send('Internal Error: Please retry. If the problem persists, please contact the administrator'); + return res + .status(500) + .send( + 'Internal Error: Please retry. If the problem persists, please contact the administrator', + ); } } else { res.status(403).send('You are not authorized to cancel setup invitation.'); } }; - /** - * Update the expired setup token to active status. After refreshing, the expiration date will be extended by 3 weeks. + /** + * Update the expired setup token to active status. After refreshing, the expiration date will be extended by 3 weeks. * @param {*} req HTTP request include requester role information * @param {*} res HTTP response include whether the setup invitation record is successfully refreshed * @returns updated result of the setup invitation record. @@ -626,30 +633,37 @@ const profileInitialSetupController = function ( if (role === 'Administrator' || role === 'Owner') { try { - ProfileInitialSetupToken - .findOneAndUpdate( + ProfileInitialSetupToken.findOneAndUpdate( { token }, { expiration: moment().add(3, 'week'), isCancelled: false, }, ) - .then((result) => { - const { email } = result; - const link = `${baseUrl}/ProfileInitialSetup/${result.token}`; - sendEmailWithAcknowledgment( - email, - 'Invitation Link Refreshed: Complete Your One Community Profile Setup', - sendRefreshedLinkMessage(link), - ); - return res.status(200).send(result); - }) - .catch((err) => { - LOGGER.logException(err); - res.status(500).send('Internal Error: Please retry. If the problem persists, please contact the administrator'); - }); + .then((result) => { + const { email } = result; + const link = `${baseUrl}/ProfileInitialSetup/${result.token}`; + sendEmailWithAcknowledgment( + email, + 'Invitation Link Refreshed: Complete Your One Community Profile Setup', + sendRefreshedLinkMessage(link), + ); + return res.status(200).send(result); + }) + .catch((err) => { + LOGGER.logException(err); + res + .status(500) + .send( + 'Internal Error: Please retry. If the problem persists, please contact the administrator', + ); + }); } catch (error) { - return res.status(500).send('Internal Error: Please retry. If the problem persists, please contact the administrator'); + return res + .status(500) + .send( + 'Internal Error: Please retry. If the problem persists, please contact the administrator', + ); } } else { return res.status(403).send('You are not authorized to refresh setup invitation.'); @@ -689,4 +703,4 @@ const profileInitialSetupController = function ( }; }; -module.exports = profileInitialSetupController; \ No newline at end of file +module.exports = profileInitialSetupController; diff --git a/src/websockets/TimerService/clientsHandler.js b/src/websockets/TimerService/clientsHandler.js index 5a77593b0..32c4168f5 100644 --- a/src/websockets/TimerService/clientsHandler.js +++ b/src/websockets/TimerService/clientsHandler.js @@ -34,9 +34,10 @@ const action = { PAUSE_TIMER: 'PAUSE_TIMER', STOP_TIMER: 'STOP_TIMER', CLEAR_TIMER: 'CLEAR_TIMER', - SET_GOAL: 'SET_GOAL=', - ADD_GOAL: 'ADD_TO_GOAL=', - REMOVE_GOAL: 'REMOVE_FROM_GOAL=', + GET_TIMER: 'GET_TIMER', + SET_GOAL: 'SET_GOAL', + ADD_GOAL: 'ADD_TO_GOAL', + REMOVE_GOAL: 'REMOVE_FROM_GOAL', FORCED_PAUSE: 'FORCED_PAUSE', ACK_FORCED: 'ACK_FORCED', START_CHIME: 'START_CHIME', @@ -66,6 +67,8 @@ const startTimer = (client) => { }; const pauseTimer = (client, forced = false) => { + if (client.paused) return; + client.time = updatedTimeSinceStart(client); if (client.time === 0) client.chiming = true; client.startAt = moment.invalid(); // invalid can not be saved in database @@ -74,8 +77,8 @@ const pauseTimer = (client, forced = false) => { }; const startChime = (client, msg) => { - const state = msg.split('=')[1]; - client.chiming = state === 'true'; + const state = msg.value; + client.chiming = state === true; }; const ackForcedPause = (client) => { @@ -107,14 +110,14 @@ const clearTimer = (client) => { }; const setGoal = (client, msg) => { - const newGoal = parseInt(msg.split('=')[1]); + const newGoal = parseInt(msg.value); client.goal = newGoal; client.time = newGoal; client.initialGoal = newGoal; }; const addGoal = (client, msg) => { - const duration = parseInt(msg.split('=')[1]); + const duration = parseInt(msg.value); const goalAfterAddition = moment.duration(client.goal).add(duration, 'milliseconds').asHours(); if (goalAfterAddition >= MAX_HOURS) { @@ -142,7 +145,7 @@ const addGoal = (client, msg) => { }; const removeGoal = (client, msg) => { - const duration = parseInt(msg.split('=')[1]); + const duration = parseInt(msg.value); const goalAfterRemoval = moment .duration(client.goal) .subtract(duration, 'milliseconds') @@ -167,27 +170,30 @@ const removeGoal = (client, msg) => { }; const handleMessage = async (msg, clients, userId) => { - if (!clients.has(userId)) { - throw new Error('It should have this user in memory'); - } + // if (!clients.has(userId)) { + // throw new Error('It should have this user in memory'); + // } - const client = clients.get(userId); + const client = await getClient(clients, userId); let resp = null; - switch (msg) { + switch (msg.action) { case action.START_TIMER: startTimer(client); break; - case msg.match(/SET_GOAL=/i)?.input: + case action.GET_TIMER: + resp = client; + break; + case action.SET_GOAL: setGoal(client, msg); break; - case msg.match(/ADD_TO_GOAL=/i)?.input: + case action.ADD_GOAL: addGoal(client, msg); break; - case msg.match(/REMOVE_FROM_GOAL=/i)?.input: + case action.REMOVE_GOAL: removeGoal(client, msg); break; - case msg.match(/START_CHIME=/i)?.input: + case action.START_CHIME: startChime(client, msg); break; case action.PAUSE_TIMER: @@ -208,7 +214,7 @@ const handleMessage = async (msg, clients, userId) => { default: resp = { ...client, - error: `Unknown operation ${msg}, please use one from { ${Object.values(action).join(', ')} }`, + error: `Unknown operation ${msg.action}, please use one from { ${Object.values(action).join(', ')} }`, }; break; } diff --git a/src/websockets/index.js b/src/websockets/index.js index a12fa18cb..368f07ba2 100644 --- a/src/websockets/index.js +++ b/src/websockets/index.js @@ -3,35 +3,31 @@ /* eslint-disable consistent-return */ /* eslint-disable quotes */ /* eslint-disable linebreak-style */ -const WebSocket = require("ws"); -const moment = require("moment"); -const jwt = require("jsonwebtoken"); -const config = require("../config"); +const WebSocket = require('ws'); +const moment = require('moment'); +const jwt = require('jsonwebtoken'); +const config = require('../config'); const { - insertNewUser, - removeConnection, - broadcastToSameUser, - hasOtherConn, -} = require("./TimerService/connectionsHandler"); -const { - getClient, - handleMessage, - action, -} = require("./TimerService/clientsHandler"); + insertNewUser, + removeConnection, + broadcastToSameUser, + hasOtherConn, +} = require('./TimerService/connectionsHandler'); +const { getClient, handleMessage, action } = require('./TimerService/clientsHandler'); /** -* Here we authenticate the user. -* We get the token from the headers and try to verify it. -* If it fails, we throw an error. -* Else we check if the token is valid and if it is, we return the user id. -*/ + * Here we authenticate the user. + * We get the token from the headers and try to verify it. + * If it fails, we throw an error. + * Else we check if the token is valid and if it is, we return the user id. + */ const authenticate = (req, res) => { - const authToken = req.headers?.["sec-websocket-protocol"]; - let payload = ""; + const authToken = req.headers?.['sec-websocket-protocol']; + let payload = ''; try { payload = jwt.verify(authToken, config.JWT_SECRET); } catch (error) { - res("401 Unauthorized", null); + res('401 Unauthorized', null); } if ( @@ -41,34 +37,34 @@ const authenticate = (req, res) => { !payload.role || moment().isAfter(payload.expiryTimestamp) ) { - res("401 Unauthorized", null); + res('401 Unauthorized', null); } res(null, payload.userid); }; /** -* Here we start the timer service. -* First we create a map to store the clients and start the Websockets Server. -* Then we set the upgrade event listener to the Express Server, authenticate the user and -* if it is valid, we add the user id to the request and handle the upgrade and emit the connection event. -*/ + * Here we start the timer service. + * First we create a map to store the clients and start the Websockets Server. + * Then we set the upgrade event listener to the Express Server, authenticate the user and + * if it is valid, we add the user id to the request and handle the upgrade and emit the connection event. + */ export default async (expServer) => { const wss = new WebSocket.Server({ noServer: true, - path: "/timer-service", + path: '/timer-service', }); - expServer.on("upgrade", (request, socket, head) => { + expServer.on('upgrade', (request, socket, head) => { authenticate(request, (err, client) => { if (err || !client) { - socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n"); + socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); socket.destroy(); return; } request.userId = client; wss.handleUpgrade(request, socket, head, (websocket) => { - wss.emit("connection", websocket, request); + wss.emit('connection', websocket, request); }); }); }); @@ -76,11 +72,11 @@ export default async (expServer) => { const clients = new Map(); // { userId: timerInfo } const connections = new Map(); // { userId: connections[] } - wss.on("connection", async (ws, req) => { + wss.on('connection', async (ws, req) => { ws.isAlive = true; const { userId } = req; - ws.on("pong", () => { + ws.on('pong', () => { ws.isAlive = true; }); @@ -93,32 +89,33 @@ export default async (expServer) => { ws.send(JSON.stringify(clientTimer)); /** - * Here we handle the messages from the client. - * And we broadcast the response to all the clients that are connected to the same user. - */ - ws.on("message", async (data) => { - const msg = data.toString(); - if (msg === action.HEARTBEAT) { - ws.send(JSON.stringify({ heartbeat: "pong" })); + * Here we handle the messages from the client. + * And we broadcast the response to all the clients that are connected to the same user. + */ + ws.on('message', async (data) => { + const msg = JSON.parse(data.toString()); + if (msg.action === action.HEARTBEAT) { + ws.send(JSON.stringify({ heartbeat: 'pong' })); return; } - const resp = await handleMessage(msg, clients, userId); + const resp = await handleMessage(msg, clients, msg.userId ?? userId); broadcastToSameUser(connections, userId, resp); + if (msg.userId) broadcastToSameUser(connections, msg.userId, resp); }); /** - * Here we handle the close event. - * If there is another connection to the same user, we don't do anything. - * Else he is the last connection and we do a forced pause if need be. - * This may happen if the user closes all the tabs or the browser or he lost connection with - * the service - * We then remove the connection from the connections map. - */ - ws.on("close", async () => { + * Here we handle the close event. + * If there is another connection to the same user, we don't do anything. + * Else he is the last connection and we do a forced pause if need be. + * This may happen if the user closes all the tabs or the browser or he lost connection with + * the service + * We then remove the connection from the connections map. + */ + ws.on('close', async () => { if (!hasOtherConn(connections, userId, ws)) { const client = clients.get(userId); if (client.started && !client.paused) { - await handleMessage(action.FORCED_PAUSE, clients, userId); + await handleMessage({ action: action.FORCED_PAUSE }, clients, userId); } } removeConnection(connections, userId, ws);