From b6eca169302b4bc00846cb83afe8226525a22c89 Mon Sep 17 00:00:00 2001 From: AriaYu927 Date: Tue, 25 Jul 2023 19:07:39 +0800 Subject: [PATCH 01/37] send email --- src/controllers/taskController.js | 41 +++++++++++++++++++++++++++++++ src/routes/taskRouter.js | 3 +++ 2 files changed, 44 insertions(+) diff --git a/src/controllers/taskController.js b/src/controllers/taskController.js index c7d61c1bd..c6858b213 100644 --- a/src/controllers/taskController.js +++ b/src/controllers/taskController.js @@ -3,6 +3,8 @@ const wbs = require('../models/wbs'); const timeEntryHelper = require('../helpers/timeEntryHelper')(); const taskHelper = require('../helpers/taskHelper')(); const hasPermission = require('../utilities/permissions'); +const emailSender = require('../utilities/emailSender'); +const userProfile = require('../models/userProfile'); const taskController = function (Task) { const getTasks = (req, res) => { @@ -1098,6 +1100,44 @@ const taskController = function (Task) { }); }; + const getReviewReqEmailBody = function (name, taskName) { + const text = `New Task Review Request From ${name}: +

The following task is available to review:

+

${taskName}

+

Thank you,

+

One Community

`; + + return text; + }; + + const getRecipients = async function (myUserId) { + const user = await userProfile.findById(myUserId); + const membership = await userProfile.find({ teams: user.teams, role: ['Administrator', 'Manager', 'Mentor'] }); + const emails = membership.map(a => a.email); + return emails; + }; + + const sendReviewReq = async function (req, res) { + const { + myUserId, name, taskName, + } = req.body; + const emailBody = getReviewReqEmailBody(name, taskName); + const recipients = await getRecipients(myUserId); + + try { + emailSender( + recipients, + `Review Request from ${name}`, + emailBody, + 'highestgoodnetwork@gmail.com', + null, + ); + res.status(200).send('Success'); + } catch (err) { + res.status(500).send('Failed'); + } + }; + return { postTask, getTasks, @@ -1114,6 +1154,7 @@ const taskController = function (Task) { getTasksByUserList, getTasksForTeamsByUser, updateChildrenQty, + sendReviewReq, }; }; diff --git a/src/routes/taskRouter.js b/src/routes/taskRouter.js index 00db03325..e1d460d83 100644 --- a/src/routes/taskRouter.js +++ b/src/routes/taskRouter.js @@ -46,6 +46,9 @@ const routes = function (task, userProfile) { wbsRouter.route('/user/:userId/teams/tasks') .get(controller.getTasksForTeamsByUser); + wbsRouter.route('/tasks/reviewreq/:userId') + .post(controller.sendReviewReq); + return wbsRouter; }; From 24f8faa5d88692bd640d175f0d09659d6aecae6c Mon Sep 17 00:00:00 2001 From: AriaYu927 Date: Wed, 26 Jul 2023 15:15:33 +0800 Subject: [PATCH 02/37] solve the bug occurs when find users by team --- src/controllers/taskController.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/controllers/taskController.js b/src/controllers/taskController.js index c6858b213..26be8a539 100644 --- a/src/controllers/taskController.js +++ b/src/controllers/taskController.js @@ -1111,10 +1111,15 @@ const taskController = function (Task) { }; const getRecipients = async function (myUserId) { + const recipients = []; const user = await userProfile.findById(myUserId); - const membership = await userProfile.find({ teams: user.teams, role: ['Administrator', 'Manager', 'Mentor'] }); - const emails = membership.map(a => a.email); - return emails; + const membership = await userProfile.find({ role: ['Administrator', 'Manager', 'Mentor'] }); + membership.forEach((member) => { + if (member.teams.some(team => user.teams.includes(team))) { + recipients.push(member.email); + } + }); + return recipients; }; const sendReviewReq = async function (req, res) { From 346082b365e833b1439b4ce60777ee4417ca5f58 Mon Sep 17 00:00:00 2001 From: AriaYu927 Date: Tue, 29 Aug 2023 12:14:47 +0800 Subject: [PATCH 03/37] solve conflicts --- src/controllers/userProfileController.js | 1 + src/helpers/reporthelper.js | 3 +++ src/models/userProfile.js | 1 + 3 files changed, 5 insertions(+) diff --git a/src/controllers/userProfileController.js b/src/controllers/userProfileController.js index 9d0e7e63a..6b6cee9b1 100644 --- a/src/controllers/userProfileController.js +++ b/src/controllers/userProfileController.js @@ -284,6 +284,7 @@ const userProfileController = function (UserProfile) { record.totalIntangibleHrs = req.body.totalIntangibleHrs; record.bioPosted = req.body.bioPosted || 'default'; record.isFirstTimelog = req.body.isFirstTimelog; + record.teamCode = req.body.teamCode; // find userData in cache const isUserInCache = cache.hasCache('allusers'); diff --git a/src/helpers/reporthelper.js b/src/helpers/reporthelper.js index 4b66c7d65..317605dd4 100644 --- a/src/helpers/reporthelper.js +++ b/src/helpers/reporthelper.js @@ -112,6 +112,9 @@ const reporthelper = function () { }, }, }, + teamCode: { + $ifNull: ['$teamCode', ''], + }, role: 1, weeklySummaries: { $filter: { diff --git a/src/models/userProfile.js b/src/models/userProfile.js index 79223fbe2..aa728f968 100644 --- a/src/models/userProfile.js +++ b/src/models/userProfile.js @@ -154,6 +154,7 @@ const userProfileSchema = new Schema({ weeklySummaryOption: { type: String }, bioPosted: { type: String, default: 'default' }, isFirstTimelog: { type: Boolean, default: true}, + teamCode: { type: String, default: '' }, infoCollections: [ { areaName:{type: String}, areaContent:{type:String}, From 0b63eac581a39838425c7f698cc3758903c4236c Mon Sep 17 00:00:00 2001 From: AriaYu927 Date: Thu, 31 Aug 2023 15:28:57 +0800 Subject: [PATCH 04/37] add Edit Team Code permission to Owner --- src/utilities/createInitialPermissions.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utilities/createInitialPermissions.js b/src/utilities/createInitialPermissions.js index 9ac0f12b0..e42ca9a15 100644 --- a/src/utilities/createInitialPermissions.js +++ b/src/utilities/createInitialPermissions.js @@ -209,6 +209,7 @@ const permissionsRoles = [ 'getWeeklySummaries', 'getTimeZoneAPIKey', 'checkLeadTeamOfXplus', + 'editTeamCode', ], }, ]; From e40e91cf40e9a7b944f32db310a8c0ee9f676e97 Mon Sep 17 00:00:00 2001 From: Nathan Hoffman Date: Sun, 3 Sep 2023 13:51:44 -0700 Subject: [PATCH 05/37] Revert "Revert "new perms and no removing PMP perms on startup"" This reverts commit 97f97b6639348dabb6993c8226cf4bfee669b8a1. --- src/utilities/createInitialPermissions.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/utilities/createInitialPermissions.js b/src/utilities/createInitialPermissions.js index 9ac0f12b0..ffc655b60 100644 --- a/src/utilities/createInitialPermissions.js +++ b/src/utilities/createInitialPermissions.js @@ -72,7 +72,10 @@ const permissionsRoles = [ }, { roleName: 'Volunteer', - permissions: ['getReporteesLimitRoles'], + permissions: [ + 'getReporteesLimitRoles', + 'suggestTask', + ], }, { roleName: 'Core Team', @@ -105,7 +108,8 @@ const permissionsRoles = [ 'putUserProfile', 'infringementAuthorizer', 'getReporteesLimitRoles', - 'suggestTask', + 'updateTask', + 'putTeam', 'getAllInvInProjectWBS', 'postInvInProjectWBS', 'getAllInvInProject', @@ -239,12 +243,16 @@ const createInitialPermissions = async () => { role.permissions = permissions; role.save(); - // If role exists in db and is not updated, update it - } else if (!roleDataBase.permissions.every(perm => permissions.includes(perm)) || !permissions.every(perm => roleDataBase.permissions.includes(perm))) { + // If role exists in db and does not have every permission, add the missing permissions + } else if (!permissions.every(perm => roleDataBase.permissions.includes(perm))) { const roleId = roleDataBase._id; promises.push(Role.findById(roleId, (_, record) => { - record.permissions = permissions; + permissions.forEach((perm) => { + if (!record.permissions.includes(perm)) { + record.permissions.push(perm); + } + }); record.save(); })); } From 84b3baf05c234f43ba2099f080d855e729490854 Mon Sep 17 00:00:00 2001 From: AriaYu927 Date: Wed, 6 Sep 2023 09:15:41 +0800 Subject: [PATCH 06/37] add teamCode Property to team object --- src/controllers/teamController.js | 1 + src/models/team.js | 1 + 2 files changed, 2 insertions(+) diff --git a/src/controllers/teamController.js b/src/controllers/teamController.js index 1072f1fb4..0ff777870 100644 --- a/src/controllers/teamController.js +++ b/src/controllers/teamController.js @@ -71,6 +71,7 @@ const teamcontroller = function (Team) { } record.teamName = req.body.teamName; record.isActive = req.body.isActive; + record.teamCode = req.body.teamCode; record.createdDatetime = Date.now(); record.modifiedDatetime = Date.now(); diff --git a/src/models/team.js b/src/models/team.js index 8d46db283..a57d7bb27 100644 --- a/src/models/team.js +++ b/src/models/team.js @@ -13,6 +13,7 @@ const team = new Schema({ addDateTime: { type: Date, default: Date.now(), ref: 'userProfile' }, }, ], + teamCode: { type: 'String', default: '' }, }); module.exports = mongoose.model('team', team, 'teams'); From 0b50204c4971a6688be51cb76b9d39b63530979d Mon Sep 17 00:00:00 2001 From: Nathan Hoffman Date: Mon, 25 Sep 2023 17:06:27 -0700 Subject: [PATCH 07/37] Create role preset model/API --- src/controllers/rolePresetsController.js | 79 +++++++++++++++++++++++ src/models/rolePreset.js | 14 ++++ src/routes/rolePresetRouter.js | 20 ++++++ src/startup/routes.js | 5 +- src/utilities/createInitialPermissions.js | 23 +++++++ 5 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 src/controllers/rolePresetsController.js create mode 100644 src/models/rolePreset.js create mode 100644 src/routes/rolePresetRouter.js diff --git a/src/controllers/rolePresetsController.js b/src/controllers/rolePresetsController.js new file mode 100644 index 000000000..63c182577 --- /dev/null +++ b/src/controllers/rolePresetsController.js @@ -0,0 +1,79 @@ +const { hasPermission } = require('../utilities/permissions'); + +const rolePresetsController = function (Preset) { + const getPresetsByRole = async function (req, res) { + if (!await hasPermission(req.body.requestor.role, 'putRole')) { + res.status(403).send('You are not authorized to make changes to roles.'); + return; + } + + const { roleName } = req.params; + Preset.find({ roleName }) + .then((results) => { res.status(200).send(results); }) + .catch((error) => { res.status(400).send(error); }); + }; + + const createNewPreset = async function (req, res) { + if (!await hasPermission(req.body.requestor.role, 'putRole')) { + res.status(403).send('You are not authorized to make changes to roles.'); + return; + } + + if (!req.body.roleName || !req.body.presetName || !req.body.permissions) { + res.status(400).send({ error: 'roleName, presetName, and permissions are mandatory fields.' }); + return; + } + + const preset = new Preset(); + preset.roleName = req.body.roleName; + preset.presetName = req.body.presetName; + preset.permissions = req.body.permissions; + preset.save() + .then(res.status(200).send({ message: 'New preset created' })) + .catch(error => res.status(400).send({ error })); + }; + + const updatePresetById = async function (req, res) { + if (!await hasPermission(req.body.requestor.role, 'putRole')) { + res.status(403).send('You are not authorized to make changes to roles.'); + return; + } + + const { presetId } = req.params; + Preset.findById(presetId) + .then((record) => { + record.roleName = req.body.roleName; + record.presetName = req.body.presetName; + record.permissions = req.body.permissions; + record.save() + .then(results => res.status(201).send(results)) + .catch(errors => res.status(400).send(errors)); + }) + .catch(error => res.status(400).send({ error })); + }; + + const deletePresetById = async function (req, res) { + if (!await hasPermission(req.body.requestor.role, 'putRole')) { + res.status(403).send('You are not authorized to make changes to roles.'); + return; + } + + const { presetId } = req.params; + Preset.findById(presetId) + .then((result) => { + result.remove() + .then(res.status(200).send({ message: 'Deleted preset' })) + .catch(error => res.status(400).send({ error })); + }) + .catch(error => res.status(400).send({ error })); + }; + + return { + getPresetsByRole, + createNewPreset, + updatePresetById, + deletePresetById, + }; +}; + +module.exports = rolePresetsController; diff --git a/src/models/rolePreset.js b/src/models/rolePreset.js new file mode 100644 index 000000000..1bf785e2d --- /dev/null +++ b/src/models/rolePreset.js @@ -0,0 +1,14 @@ +const mongoose = require('mongoose'); + +const { Schema } = mongoose; + + +const RolePermissionPresets = new Schema({ + roleName: { type: String, required: true }, + presetName: { type: String, required: true }, + permissions: [String], +}); + +// RolePermissionPresets.createIndex({ roleName: 1, presetName: 1 }, { unique: true }); + +module.exports = mongoose.model('rolePermissionPresets', RolePermissionPresets, 'rolePermissionPresets'); diff --git a/src/routes/rolePresetRouter.js b/src/routes/rolePresetRouter.js new file mode 100644 index 000000000..8b522c7ee --- /dev/null +++ b/src/routes/rolePresetRouter.js @@ -0,0 +1,20 @@ +const express = require('express'); + +const routes = function (rolePreset) { + const controller = require('../controllers/rolePresetsController')(rolePreset); + const PresetsRouter = express.Router(); + + PresetsRouter.route('/rolePreset') + .post(controller.createNewPreset); + + PresetsRouter.route('/rolePreset/:roleName') + .get(controller.getPresetsByRole); + + PresetsRouter.route('/rolePreset/:presetId') + .put(controller.updatePresetById) + .delete(controller.deletePresetById); + +return PresetsRouter; +}; + +module.exports = routes; diff --git a/src/startup/routes.js b/src/startup/routes.js index 6e000002e..665792ba6 100644 --- a/src/startup/routes.js +++ b/src/startup/routes.js @@ -15,6 +15,7 @@ const badge = require('../models/badge'); const inventoryItem = require('../models/inventoryItem'); const inventoryItemType = require('../models/inventoryItemType'); const role = require('../models/role'); +const rolePreset = require('../models/rolePreset'); const ownerMessage = require('../models/ownerMessage'); const ownerStandardMessage = require('../models/ownerStandardMessage'); const profileInitialSetuptoken = require('../models/profileInitialSetupToken'); @@ -47,6 +48,7 @@ const profileInitialSetupRouter = require('../routes/profileInitialSetupRouter') const taskEditSuggestion = require('../models/taskEditSuggestion'); const taskEditSuggestionRouter = require('../routes/taskEditSuggestionRouter')(taskEditSuggestion); const roleRouter = require('../routes/roleRouter')(role); +const rolePresetRouter = require('../routes/rolePresetRouter')(rolePreset); const ownerMessageRouter = require('../routes/ownerMessageRouter')(ownerMessage); const ownerStandardMessageRouter = require('../routes/ownerStandardMessageRouter')(ownerStandardMessage); @@ -77,9 +79,10 @@ module.exports = function (app) { app.use('/api', timeZoneAPIRouter); app.use('/api', taskEditSuggestionRouter); app.use('/api', roleRouter); + app.use('/api', rolePresetRouter); app.use('/api', ownerMessageRouter); app.use('/api', ownerStandardMessageRouter); - app.use('/api', profileInitialSetupRouter) + app.use('/api', profileInitialSetupRouter); app.use('/api', reasonRouter); app.use('/api', informationRouter); app.use('/api', mouseoverTextRouter); diff --git a/src/utilities/createInitialPermissions.js b/src/utilities/createInitialPermissions.js index 9ac0f12b0..a5c69d87f 100644 --- a/src/utilities/createInitialPermissions.js +++ b/src/utilities/createInitialPermissions.js @@ -1,4 +1,5 @@ const Role = require('../models/role'); +const RolePreset = require('../models/rolePreset'); const User = require('../models/userProfile'); const permissionsRoles = [ @@ -222,6 +223,7 @@ const createInitialPermissions = async () => { // Get Roles From DB const allRoles = await Role.find(); + const allPresets = await RolePreset.find(); const onlyUpdateOwner = false; const promises = []; @@ -249,6 +251,27 @@ const createInitialPermissions = async () => { })); } } + + // Update Default presets + const presetDataBase = allPresets.find(preset => preset.roleName === roleName && preset.presetName === 'default'); + + // If role does not exist in db, create it + if (!presetDataBase) { + const defaultPreset = new RolePreset(); + defaultPreset.roleName = roleName; + defaultPreset.presetName = 'default'; + defaultPreset.permissions = permissions; + defaultPreset.save(); + + // If role exists in db and is not updated, update default + } else if (!presetDataBase.permissions.every(perm => permissions.includes(perm)) || !permissions.every(perm => presetDataBase.permissions.includes(perm))) { + const roleId = presetDataBase._id; + + promises.push(Role.findById(roleId, (_, record) => { + record.permissions = permissions; + record.save(); + })); + } } await Promise.all(promises); }; From 5d732a8a224579bb44af576e9f26a86bfb1d134a Mon Sep 17 00:00:00 2001 From: Nathan Hoffman Date: Mon, 25 Sep 2023 17:29:36 -0700 Subject: [PATCH 08/37] Fix hasPermission() to include individual perms --- src/utilities/permissions.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/utilities/permissions.js b/src/utilities/permissions.js index e00d864f2..2e2ac4194 100644 --- a/src/utilities/permissions.js +++ b/src/utilities/permissions.js @@ -1,15 +1,19 @@ const Role = require('../models/role'); -const UserProfile = require('../models/userProfile'); +const UserProfile = require('../models/userProfile'); -const hasPermission = async (role, action) => Role.findOne({ roleName: role }) - .exec() - .then(({ permissions }) => permissions.includes(action)); + +const hasRolePermission = async (role, action) => Role.findOne({ roleName: role }) + .exec() + .then(({ permissions }) => permissions.includes(action)); const hasIndividualPermission = async (userId, action) => UserProfile.findById(userId) .select('permissions') .exec() .then(({ permissions }) => permissions.frontPermissions.includes(action)); + +const hasPermission = async (requestor, action) => hasRolePermission(requestor.role, action) || hasIndividualPermission(requestor.requestorId, action); + const canRequestorUpdateUser = (requestorId, userId) => { const allowedIds = ['63feae337186de1898fa8f51', // dev jae@onecommunityglobal.org '5baac381e16814009017678c', // dev one.community@me.com @@ -29,4 +33,4 @@ const canRequestorUpdateUser = (requestorId, userId) => { return !(protectedIds.includes(userId) && !allowedIds.includes(requestorId)); }; -module.exports = { hasPermission, hasIndividualPermission, canRequestorUpdateUser }; +module.exports = { hasPermission, canRequestorUpdateUser }; From 7c70e5de5a3655c8c98f19e5b080266793b66c3f Mon Sep 17 00:00:00 2001 From: Nathan Hoffman Date: Mon, 25 Sep 2023 17:30:20 -0700 Subject: [PATCH 09/37] updated hasPermission() calls for updated params --- src/controllers/badgeController.js | 12 +++---- src/controllers/inventoryController.js | 26 ++++++++-------- .../popupEditorBackupController.js | 4 +-- src/controllers/popupEditorController.js | 4 +-- src/controllers/projectController.js | 12 +++---- src/controllers/reportsController.js | 4 +-- src/controllers/rolesController.js | 6 ++-- src/controllers/taskController.js | 14 ++++----- src/controllers/teamController.js | 8 ++--- src/controllers/timeEntryController.js | 6 ++-- src/controllers/timeZoneAPIController.js | 3 +- src/controllers/userProfileController.js | 31 +++++++++---------- src/controllers/wbsController.js | 4 +-- 13 files changed, 66 insertions(+), 68 deletions(-) diff --git a/src/controllers/badgeController.js b/src/controllers/badgeController.js index 6238c381e..62bad6399 100644 --- a/src/controllers/badgeController.js +++ b/src/controllers/badgeController.js @@ -1,11 +1,11 @@ const mongoose = require('mongoose'); const UserProfile = require('../models/userProfile'); -const { hasPermission, hasIndividualPermission } = require('../utilities/permissions'); +const { hasPermission } = require('../utilities/permissions'); const escapeRegex = require('../utilities/escapeRegex'); const badgeController = function (Badge) { const getAllBadges = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'seeBadges') && !await hasIndividualPermission(req.body.requestor.requestorId, 'seeBadges')) { + if (!await hasPermission(req.body.requestor, 'seeBadges')) { res.status(403).send('You are not authorized to view all badge data.'); return; } @@ -26,7 +26,7 @@ const badgeController = function (Badge) { }; const assignBadges = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'assignBadges')) { + if (!await hasPermission(req.body.requestor, 'assignBadges')) { res.status(403).send('You are not authorized to assign badges.'); return; } @@ -57,7 +57,7 @@ const badgeController = function (Badge) { }; const postBadge = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'createBadges')) { + if (!await hasPermission(req.body.requestor, 'createBadges')) { res.status(403).send({ error: 'You are not authorized to create new badges.' }); return; } @@ -91,7 +91,7 @@ const badgeController = function (Badge) { }; const deleteBadge = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'deleteBadges')) { + if (!await hasPermission(req.body.requestor, 'deleteBadges')) { res.status(403).send({ error: 'You are not authorized to delete badges.' }); return; } @@ -112,7 +112,7 @@ const badgeController = function (Badge) { }; const putBadge = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'updateBadges')) { + if (!await hasPermission(req.body.requestor, 'updateBadges')) { res.status(403).send({ error: 'You are not authorized to update badges.' }); return; } diff --git a/src/controllers/inventoryController.js b/src/controllers/inventoryController.js index bc0902aeb..f1e00402d 100644 --- a/src/controllers/inventoryController.js +++ b/src/controllers/inventoryController.js @@ -7,7 +7,7 @@ const escapeRegex = require('../utilities/escapeRegex'); const inventoryController = function (Item, ItemType) { const getAllInvInProjectWBS = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'getAllInvInProjectWBS')) { + if (!await hasPermission(req.body.requestor, 'getAllInvInProjectWBS')) { return res.status(403).send('You are not authorized to view inventory data.'); } // use req.params.projectId and wbsId @@ -40,7 +40,7 @@ const inventoryController = function (Item, ItemType) { }; const postInvInProjectWBS = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'postInvInProjectWBS')) { + if (!await hasPermission(req.body.requestor, 'postInvInProjectWBS')) { return res.status(403).send('You are not authorized to view inventory data.'); } // use req.body.projectId and req.body.wbsId req.body.quantity, @@ -108,7 +108,7 @@ const inventoryController = function (Item, ItemType) { const getAllInvInProject = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'getAllInvInProject')) { + if (!await hasPermission(req.body.requestor, 'getAllInvInProject')) { return res.status(403).send('You are not authorized to view inventory data.'); } // same as getAllInvInProjectWBS but just using only the project to find the items of inventory @@ -140,7 +140,7 @@ const inventoryController = function (Item, ItemType) { }; const postInvInProject = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'postInvInProject')) { + if (!await hasPermission(req.body.requestor, 'postInvInProject')) { return res.status(403).send('You are not authorized to post new inventory data.'); } // same as posting an item inProjectWBS but the WBS is uanassigned(i.e. null) @@ -194,7 +194,7 @@ const inventoryController = function (Item, ItemType) { }; const transferInvById = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'transferInvById')) { + if (!await hasPermission(req.body.requestor, 'transferInvById')) { return res.status(403).send('You are not authorized to transfer inventory data.'); } // This function transfer inventory by id @@ -283,7 +283,7 @@ const inventoryController = function (Item, ItemType) { const delInvById = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'delInvById')) { + if (!await hasPermission(req.body.requestor, 'delInvById')) { return res.status(403).send('You are not authorized to waste inventory.'); } // send result just sending something now to have it work and not break anything @@ -372,7 +372,7 @@ const inventoryController = function (Item, ItemType) { }; const unWasteInvById = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'unWasteInvById')) { + if (!await hasPermission(req.body.requestor, 'unWasteInvById')) { return res.status(403).send('You are not authorized to unwaste inventory.'); } const properUnWaste = await Item.findOne({ _id: req.params.invId, quantity: { $gte: req.body.quantity }, wasted: true }).select('_id').lean(); @@ -453,7 +453,7 @@ const inventoryController = function (Item, ItemType) { }; const getInvIdInfo = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'getInvIdInfo')) { + if (!await hasPermission(req.body.requestor, 'getInvIdInfo')) { return res.status(403).send('You are not authorized to get inventory by id.'); } // req.params.invId @@ -465,7 +465,7 @@ const inventoryController = function (Item, ItemType) { }; const putInvById = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'putInvById')) { + if (!await hasPermission(req.body.requestor, 'putInvById')) { return res.status(403).send('You are not authorized to edit inventory by id.'); } // update the inv by id. @@ -493,7 +493,7 @@ const inventoryController = function (Item, ItemType) { }; const getInvTypeById = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'getInvTypeById')) { + if (!await hasPermission(req.body.requestor, 'getInvTypeById')) { return res.status(403).send('You are not authorized to get inv type by id.'); } // send result just sending something now to have it work and not break anything @@ -504,7 +504,7 @@ const inventoryController = function (Item, ItemType) { }; const putInvType = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'putInvType')) { + if (!await hasPermission(req.body.requestor, 'putInvType')) { return res.status(403).send('You are not authorized to edit an inventory type.'); } const { typeId } = req.params; @@ -527,7 +527,7 @@ const inventoryController = function (Item, ItemType) { }; const getAllInvType = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'getAllInvType')) { + if (!await hasPermission(req.body.requestor, 'getAllInvType')) { return res.status(403).send('You are not authorized to get all inventory.'); } // send result just sending something now to have it work and not break anything @@ -537,7 +537,7 @@ const inventoryController = function (Item, ItemType) { }; const postInvType = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'postInvType')) { + if (!await hasPermission(req.body.requestor, 'postInvType')) { return res.status(403).send('You are not authorized to save an inventory type.'); } return ItemType.find({ name: { $regex: escapeRegex(req.body.name), $options: 'i' } }) diff --git a/src/controllers/popupEditorBackupController.js b/src/controllers/popupEditorBackupController.js index 7b2493909..21eccaf8f 100644 --- a/src/controllers/popupEditorBackupController.js +++ b/src/controllers/popupEditorBackupController.js @@ -20,7 +20,7 @@ const popupEditorBackupController = function (PopupEditorBackups) { const createPopupEditorBackup = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'createPopup')) { + if (!await hasPermission(req.body.requestor, 'createPopup')) { res .status(403) .send({ error: 'You are not authorized to create new popup' }); @@ -46,7 +46,7 @@ const popupEditorBackupController = function (PopupEditorBackups) { }; const updatePopupEditorBackup = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'updatePopup')) { + if (!await hasPermission(req.body.requestor, 'updatePopup')) { res .status(403) .send({ error: 'You are not authorized to create new popup' }); diff --git a/src/controllers/popupEditorController.js b/src/controllers/popupEditorController.js index 25eed80dd..71dcb7b69 100644 --- a/src/controllers/popupEditorController.js +++ b/src/controllers/popupEditorController.js @@ -15,7 +15,7 @@ const popupEditorController = function (PopupEditors) { const createPopupEditor = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'createPopup')) { + if (!await hasPermission(req.body.requestor, 'createPopup')) { res .status(403) .send({ error: 'You are not authorized to create new popup' }); @@ -38,7 +38,7 @@ const popupEditorController = function (PopupEditors) { }; const updatePopupEditor = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'updatePopup')) { + if (!await hasPermission(req.body.requestor, 'updatePopup')) { res .status(403) .send({ error: 'You are not authorized to create new popup' }); diff --git a/src/controllers/projectController.js b/src/controllers/projectController.js index 6bac2124c..a88378985 100644 --- a/src/controllers/projectController.js +++ b/src/controllers/projectController.js @@ -14,9 +14,9 @@ const projectController = function (Project) { .catch(error => res.status(404).send(error)); }; - const deleteProject = function (req, res) { - if (!hasPermission(req.body.requestor.role, 'deleteProject')) { - res.status(403).send({ error: 'You are not authorized to delete projects.' }); + const deleteProject = async function (req, res) { + if (!await hasPermission(req.body.requestor, 'deleteProject')) { + res.status(403).send({ error: 'You are not authorized to delete projects.' }); return; } const { projectId } = req.params; @@ -46,7 +46,7 @@ const projectController = function (Project) { }; const postProject = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'postProject')) { + if (!await hasPermission(req.body.requestor, 'postProject')) { res.status(403).send({ error: 'You are not authorized to create new projects.' }); return; } @@ -77,7 +77,7 @@ const projectController = function (Project) { const putProject = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'putProject')) { + if (!await hasPermission(req.body.requestor, 'putProject')) { res.status(403).send('You are not authorized to make changes in the projects.'); return; } @@ -124,7 +124,7 @@ const projectController = function (Project) { const assignProjectToUsers = async function (req, res) { // verify requestor is administrator, projectId is passed in request params and is valid mongoose objectid, and request body contains an array of users - if (!await hasPermission(req.body.requestor.role, 'assignProjectToUsers')) { + if (!await hasPermission(req.body.requestor, 'assignProjectToUsers')) { res.status(403).send({ error: 'You are not authorized to perform this operation' }); return; } diff --git a/src/controllers/reportsController.js b/src/controllers/reportsController.js index e139924fa..8f4aba87b 100644 --- a/src/controllers/reportsController.js +++ b/src/controllers/reportsController.js @@ -1,9 +1,9 @@ const reporthelper = require('../helpers/reporthelper')(); -const { hasPermission, hasIndividualPermission } = require('../utilities/permissions'); +const { hasPermission } = require('../utilities/permissions'); const reportsController = function () { const getWeeklySummaries = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'getWeeklySummaries') && !await hasIndividualPermission(req.body.requestor.requestorId, 'getWeeklySummaries')) { + if (!await hasPermission(req.body.requestor, 'getWeeklySummaries')) { res.status(403).send('You are not authorized to view all users'); return; } diff --git a/src/controllers/rolesController.js b/src/controllers/rolesController.js index a97e9408f..d3d9f8310 100644 --- a/src/controllers/rolesController.js +++ b/src/controllers/rolesController.js @@ -10,7 +10,7 @@ const rolesController = function (Role) { }; const createNewRole = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'postRole')) { + if (!await hasPermission(req.body.requestor, 'postRole')) { res.status(403).send('You are not authorized to create new roles.'); return; } @@ -39,7 +39,7 @@ const rolesController = function (Role) { const updateRoleById = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'putRole')) { + if (!await hasPermission(req.body.requestor, 'putRole')) { res.status(403).send('You are not authorized to make changes to roles.'); return; } @@ -67,7 +67,7 @@ const rolesController = function (Role) { }; const deleteRoleById = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'deleteRole')) { + if (!await hasPermission(req.body.requestor, 'deleteRole')) { res.status(403).send('You are not authorized to delete roles.'); return; } diff --git a/src/controllers/taskController.js b/src/controllers/taskController.js index 79222ed4b..d5e048537 100644 --- a/src/controllers/taskController.js +++ b/src/controllers/taskController.js @@ -390,7 +390,7 @@ const taskController = function (Task) { }; const importTask = async (req, res) => { - if (!await hasPermission(req.body.requestor.role, 'importTask')) { + if (!await hasPermission(req.body.requestor, 'importTask')) { res .status(403) .send({ error: 'You are not authorized to create new Task.' }); @@ -420,7 +420,7 @@ const taskController = function (Task) { }; const postTask = async (req, res) => { - if (!await hasPermission(req.body.requestor.role, 'postTask')) { + if (!await hasPermission(req.body.requestor, 'postTask')) { res .status(403) .send({ error: 'You are not authorized to create new Task.' }); @@ -456,7 +456,7 @@ const taskController = function (Task) { }; const updateNum = async (req, res) => { - if (!await hasPermission(req.body.requestor.role, 'updateNum')) { + if (!await hasPermission(req.body.requestor, 'updateNum')) { res .status(403) .send({ error: 'You are not authorized to create new projects.' }); @@ -593,7 +593,7 @@ const taskController = function (Task) { }; const deleteTask = async (req, res) => { - if (!await hasPermission(req.body.requestor.role, 'deleteTask')) { + if (!await hasPermission(req.body.requestor, 'deleteTask')) { res .status(403) .send({ error: 'You are not authorized to deleteTasks.' }); @@ -642,7 +642,7 @@ const taskController = function (Task) { }; const deleteTaskByWBS = async (req, res) => { - if (!await hasPermission(req.body.requestor.role, 'deleteTask')) { + if (!await hasPermission(req.body.requestor, 'deleteTask')) { res .status(403) .send({ error: 'You are not authorized to deleteTasks.' }); @@ -673,7 +673,7 @@ const taskController = function (Task) { }; const updateTask = async (req, res) => { - if (!await hasPermission(req.body.requestor.role, 'updateTask')) { + if (!await hasPermission(req.body.requestor, 'updateTask')) { res.status(403).send({ error: 'You are not authorized to update Task.' }); return; } @@ -689,7 +689,7 @@ const taskController = function (Task) { }; const swap = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'swapTask')) { + if (!await hasPermission(req.body.requestor, 'swapTask')) { res .status(403) .send({ error: 'You are not authorized to create new projects.' }); diff --git a/src/controllers/teamController.js b/src/controllers/teamController.js index 1072f1fb4..0486a5df7 100644 --- a/src/controllers/teamController.js +++ b/src/controllers/teamController.js @@ -17,7 +17,7 @@ const teamcontroller = function (Team) { .catch(error => res.send(error).status(404)); }; const postTeam = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'postTeam')) { + if (!await hasPermission(req.body.requestor, 'postTeam')) { res.status(403).send({ error: 'You are not authorized to create teams.' }); return; } @@ -34,7 +34,7 @@ const teamcontroller = function (Team) { .catch(error => res.send(error).status(404)); }; const deleteTeam = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'deleteTeam')) { + if (!await hasPermission(req.body.requestor, 'deleteTeam')) { res.status(403).send({ error: 'You are not authorized to delete teams.' }); return; } @@ -57,7 +57,7 @@ const teamcontroller = function (Team) { }); }; const putTeam = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'putTeam')) { + if (!await hasPermission(req.body.requestor, 'putTeam')) { res.status(403).send('You are not authorized to make changes in the teams.'); return; } @@ -84,7 +84,7 @@ const teamcontroller = function (Team) { const assignTeamToUsers = async function (req, res) { // verify requestor is administrator, teamId is passed in request params and is valid mongoose objectid, and request body contains an array of users - if (!await hasPermission(req.body.requestor.role, 'assignTeamToUsers')) { + if (!await hasPermission(req.body.requestor, 'assignTeamToUsers')) { res.status(403).send({ error: 'You are not authorized to perform this operation' }); return; } diff --git a/src/controllers/timeEntryController.js b/src/controllers/timeEntryController.js index e83a7e31b..6b13d9331 100644 --- a/src/controllers/timeEntryController.js +++ b/src/controllers/timeEntryController.js @@ -106,7 +106,7 @@ const timeEntrycontroller = function (TimeEntry) { return res.status(400).send({ error: `No valid records found for ${req.params.timeEntryId}` }); } - if (!(await hasPermission(req.body.requestor.role, 'editTimeEntry') || timeEntry.personId.toString() === req.body.requestor.requestorId.toString())) { + if (!(await hasPermission(req.body.requestor, 'editTimeEntry') || timeEntry.personId.toString() === req.body.requestor.requestorId.toString())) { return res.status(403).send({ error: 'Unauthorized request' }); } @@ -153,7 +153,7 @@ const timeEntrycontroller = function (TimeEntry) { if (initialSeconds !== totalSeconds && timeEntry.isTangible && req.body.requestor.requestorId === timeEntry.personId.toString() - && !await hasPermission(req.body.requestor.role, 'editTimeEntry') + && !await hasPermission(req.body.requestor, 'editTimeEntry') ) { const requestor = await userProfile.findById(req.body.requestor.requestorId); requestor.timeEntryEditHistory.push({ @@ -463,7 +463,7 @@ const timeEntrycontroller = function (TimeEntry) { if ( record.personId.toString() === req.body.requestor.requestorId.toString() - || await hasPermission(req.body.requestor.role, 'deleteTimeEntry') + || await hasPermission(req.body.requestor, 'deleteTimeEntry') ) { // Revert this tangible timeEntry of related task's hoursLogged if (record.isTangible === true) { diff --git a/src/controllers/timeZoneAPIController.js b/src/controllers/timeZoneAPIController.js index 2ed0792c9..3c4df0a22 100644 --- a/src/controllers/timeZoneAPIController.js +++ b/src/controllers/timeZoneAPIController.js @@ -2,14 +2,13 @@ const { hasPermission } = require('../utilities/permissions'); const timeZoneAPIController = function () { const getTimeZoneAPIKey = async (req, res) => { - const requestorRole = req.body.requestor.role; const premiumKey = process.env.TIMEZONE_PREMIUM_KEY; const commonKey = process.env.TIMEZONE_COMMON_KEY; if (!req.body.requestor.role) { res.status(403).send('Unauthorized Request'); return; } - if (await hasPermission(requestorRole, 'getTimeZoneAPIKey')) { + if (await hasPermission(req.body.requestor, 'getTimeZoneAPIKey')) { res.status(200).send({ userAPIKey: premiumKey }); return; } diff --git a/src/controllers/userProfileController.js b/src/controllers/userProfileController.js index 244e7cf45..b42bf705b 100644 --- a/src/controllers/userProfileController.js +++ b/src/controllers/userProfileController.js @@ -35,7 +35,7 @@ async function ValidatePassword(req, res) { return; } // Verify request is authorized by self or adminsitrator - if (!userId === requestor.requestorId && !await hasPermission(requestor.role, 'updatePassword')) { + if (!userId === requestor.requestorId && !await hasPermission(req.body.requestor, 'updatePassword')) { res.status(403).send({ error: "You are unauthorized to update this user's password", }); @@ -52,7 +52,7 @@ async function ValidatePassword(req, res) { const userProfileController = function (UserProfile) { const getUserProfiles = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'getUserProfiles')) { + if (!await hasPermission(req.body.requestor, 'getUserProfiles')) { res.status(403).send('You are not authorized to view all users'); return; } @@ -82,7 +82,7 @@ const userProfileController = function (UserProfile) { }; const getProjectMembers = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'getProjectMembers')) { + if (!await hasPermission(req.body.requestor, 'getProjectMembers')) { res.status(403).send('You are not authorized to view all users'); return; } @@ -104,12 +104,12 @@ const userProfileController = function (UserProfile) { }; const postUserProfile = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'postUserProfile')) { + if (!await hasPermission(req.body.requestor, 'postUserProfile')) { res.status(403).send('You are not authorized to create new users'); return; } - if (req.body.role === 'Owner' && !await hasPermission(req.body.requestor.role, 'addDeleteEditOwners')) { + if (req.body.role === 'Owner' && !await hasPermission(req.body.requestor, 'addDeleteEditOwners')) { res.status(403).send('You are not authorized to create new owners'); return; } @@ -225,7 +225,7 @@ const userProfileController = function (UserProfile) { const userid = req.params.userId; const isRequestorAuthorized = !!( canRequestorUpdateUser(req.body.requestor.requestorId, userid) && ( - await hasPermission(req.body.requestor.role, 'putUserProfile') + await hasPermission(req.body.requestor, 'putUserProfile') || req.body.requestor.requestorId === userid ) ); @@ -235,7 +235,7 @@ const userProfileController = function (UserProfile) { return; } - if (req.body.role === 'Owner' && !await hasPermission(req.body.requestor.role, 'addDeleteEditOwners')) { + if (req.body.role === 'Owner' && !await hasPermission(req.body.requestor, 'addDeleteEditOwners')) { res.status(403).send('You are not authorized to update this user'); return; } @@ -296,7 +296,7 @@ const userProfileController = function (UserProfile) { userIdx = allUserData.findIndex(users => users._id === userid); userData = allUserData[userIdx]; } - if (await hasPermission(req.body.requestor.role, 'putUserProfileImportantInfo')) { + if (await hasPermission(req.body.requestor, 'putUserProfileImportantInfo')) { record.role = req.body.role; record.isRehireable = req.body.isRehireable; record.isActive = req.body.isActive; @@ -360,7 +360,7 @@ const userProfileController = function (UserProfile) { record.bioPosted = req.body.bioPosted || 'default'; - if (await hasPermission(req.body.requestor.role, 'putUserProfilePermissions')) { + if (await hasPermission(req.body.requestor, 'putUserProfilePermissions')) { record.permissions = req.body.permissions; } @@ -380,7 +380,7 @@ const userProfileController = function (UserProfile) { userData.createdDate = record.createdDate.toISOString(); } } - if (await hasPermission(req.body.requestor.role, 'infringementAuthorizer')) { + if (await hasPermission(req.body.requestor, 'infringementAuthorizer')) { record.infringements = req.body.infringements; } @@ -410,12 +410,12 @@ const userProfileController = function (UserProfile) { const deleteUserProfile = async function (req, res) { const { option, userId } = req.body; - if (!await hasPermission(req.body.requestor.role, 'deleteUserProfile')) { + if (!await hasPermission(req.body.requestor, 'deleteUserProfile')) { res.status(403).send('You are not authorized to delete users'); return; } - if (req.body.role === 'Owner' && !await hasPermission(req.body.requestor.role, 'addDeleteEditOwners')) { + if (req.body.role === 'Owner' && !await hasPermission(req.body.requestor, 'addDeleteEditOwners')) { res.status(403).send('You are not authorized to delete this user'); return; } @@ -586,7 +586,7 @@ const userProfileController = function (UserProfile) { }); } // Verify request is authorized by self or adminsitrator - if (!userId === requestor.requestorId && !await hasPermission(requestor.role, 'updatePassword')) { + if (!userId === requestor.requestorId && !await hasPermission(req.body.requestor, 'updatePassword')) { return res.status(403).send({ error: "You are unauthorized to update this user's password", }); @@ -647,11 +647,10 @@ const userProfileController = function (UserProfile) { } const userid = mongoose.Types.ObjectId(req.params.userId); - const { role } = req.body.requestor; let validroles = ['Volunteer', 'Manager', 'Administrator', 'Core Team', 'Owner', 'Mentor']; - if (await hasPermission(role, 'getReporteesLimitRoles')) { + if (await hasPermission(req.body.requestor, 'getReporteesLimitRoles')) { validroles = ['Volunteer', 'Manager']; } @@ -722,7 +721,7 @@ const userProfileController = function (UserProfile) { }); return; } - if (!await hasPermission(req.body.requestor.role, 'changeUserStatus')) { + if (!await hasPermission(req.body.requestor, 'changeUserStatus')) { res.status(403).send('You are not authorized to change user status'); return; } diff --git a/src/controllers/wbsController.js b/src/controllers/wbsController.js index 48b640061..fa7f4427f 100644 --- a/src/controllers/wbsController.js +++ b/src/controllers/wbsController.js @@ -11,7 +11,7 @@ const wbsController = function (WBS) { }; const postWBS = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'postWbs')) { + if (!await hasPermission(req.body.requestor, 'postWbs')) { res.status(403).send({ error: 'You are not authorized to create new projects.' }); return; } @@ -34,7 +34,7 @@ const wbsController = function (WBS) { }; const deleteWBS = async function (req, res) { - if (!await hasPermission(req.body.requestor.role, 'deleteWbs')) { + if (!await hasPermission(req.body.requestor, 'deleteWbs')) { res.status(403).send({ error: 'You are not authorized to delete projects.' }); return; } From bcf9fd94771bc143d3213fbe86ffa576fcfe308d Mon Sep 17 00:00:00 2001 From: Nathan Hoffman Date: Mon, 25 Sep 2023 17:56:24 -0700 Subject: [PATCH 10/37] Fix await --- src/utilities/permissions.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/utilities/permissions.js b/src/utilities/permissions.js index 2e2ac4194..7b5d4a245 100644 --- a/src/utilities/permissions.js +++ b/src/utilities/permissions.js @@ -11,8 +11,7 @@ const hasIndividualPermission = async (userId, action) => UserProfile.findById(u .exec() .then(({ permissions }) => permissions.frontPermissions.includes(action)); - -const hasPermission = async (requestor, action) => hasRolePermission(requestor.role, action) || hasIndividualPermission(requestor.requestorId, action); +const hasPermission = async (requestor, action) => await hasRolePermission(requestor.role, action) || hasIndividualPermission(requestor.requestorId, action); const canRequestorUpdateUser = (requestorId, userId) => { const allowedIds = ['63feae337186de1898fa8f51', // dev jae@onecommunityglobal.org From dd55a3223ed8d27c2f56cd5b0094e127bee04df8 Mon Sep 17 00:00:00 2001 From: Carl Bebli Date: Wed, 27 Sep 2023 14:38:05 +0000 Subject: [PATCH 11/37] removed suggestionModalData.json file --- src/constants/suggestionModalData.json | 1 - 1 file changed, 1 deletion(-) delete mode 100644 src/constants/suggestionModalData.json diff --git a/src/constants/suggestionModalData.json b/src/constants/suggestionModalData.json deleted file mode 100644 index e5b735091..000000000 --- a/src/constants/suggestionModalData.json +++ /dev/null @@ -1 +0,0 @@ -{"suggestion":["Identify and remedy poor client and/or user service experiences","Identify bright spots and enhance positive service experiences","Make fundamental changes to our programs and/or operations","Inform the development of new programs/projects","Identify where we are less inclusive or equitable across demographic groups","Strengthen relationships with the people we serve","Understand people's needs and how we can help them achieve their goals","Other"],"field":[]} \ No newline at end of file From a075a3697bbf3aa6862ab599ce628fdc2180e290 Mon Sep 17 00:00:00 2001 From: Carl Bebli Date: Wed, 27 Sep 2023 14:40:08 +0000 Subject: [PATCH 12/37] changed all functions to use localized suggestion data --- src/controllers/dashBoardController.js | 109 +++++++++---------------- 1 file changed, 37 insertions(+), 72 deletions(-) diff --git a/src/controllers/dashBoardController.js b/src/controllers/dashBoardController.js index 742bfe4d8..c9cdbd588 100644 --- a/src/controllers/dashBoardController.js +++ b/src/controllers/dashBoardController.js @@ -100,7 +100,7 @@ const dashboardcontroller = function () {

${actual}

Visual Proof (screenshots, videos, text)

${visual}

-

Severity/Priority (How Bad is the Bug?

+

Severity/Priority (How Bad is the Bug?)

${severity}

Thank you,
One Community

`; @@ -146,58 +146,38 @@ const dashboardcontroller = function () { } }; - const getSuggestionFilePath = async () => { - // Define a base path based on the process.cwd() - const basePath = process.cwd(); - console.log(basePath, "basePath"); - - // Define the relative path to the JSON file (adjust as needed) - const relativePath = "src/constants/suggestionModalData.json"; - - // Combine the base path and relative path to create the full path - const fullPath = path.join(basePath, relativePath); - - return fullPath; - }; - - const readSuggestionFile = async () => { - // Specify the absolute path to the JSON file - const filepath = await getSuggestionFilePath(); - - try { - // Check if the file exists - await fs.access(filepath); - - // Read and parse the file - const readfile = await fs.readFile(filepath, "utf8"); - const parsedData = JSON.parse(readfile); - return parsedData; - } catch (err) { - console.error("Error reading suggestionModalData.json:", err); - return null; // Handle the error appropriately - } + const suggestionData = { + suggestion: [ + "Identify and remedy poor client and/or user service experiences", + "Identify bright spots and enhance positive service experiences", + "Make fundamental changes to our programs and/or operations", + "Inform the development of new programs/projects", + "Identify where we are less inclusive or equitable across demographic groups", + "Strengthen relationships with the people we serve", + "Understand people's needs and how we can help them achieve their goals", + "Other", + ], + field: [], }; - // create suggestion emailbody const getsuggestionEmailBody = async (...args) => { - const readfile = await readSuggestionFile(); let fieldaaray = []; - if (readfile.field.length) { - fieldaaray = readfile.field.map( + if (suggestionData.field.length) { + fieldaaray = suggestionData.field.map( (item) => `

${item}

-

${args[3][item]}

` +

${args[3][item]}

` ); } const text = `New Suggestion: -

Suggestion Category:

-

${args[0]}

-

Suggestion:

-

${args[1]}

- ${fieldaaray.length > 0 ? fieldaaray : ""} -

Wants Feedback:

-

${args[2]}

-

Thank you,
- One Community

`; +

Suggestion Category:

+

${args[0]}

+

Suggestion:

+

${args[1]}

+ ${fieldaaray.length > 0 ? fieldaaray : ""} +

Wants Feedback:

+

${args[2]}

+

Thank you,
+ One Community

`; return text; }; @@ -225,54 +205,39 @@ const dashboardcontroller = function () { const getSuggestionOption = async (req, res) => { try { - const readfile = await readSuggestionFile(); - if (readfile) { - res.status(200).send(readfile); + if (suggestionData) { + res.status(200).send(suggestionData); } else { - res.status(404).send("Suggestion file not found."); + res.status(404).send("Suggestion data not found."); } } catch (error) { - console.error("Error reading suggestion file:", error); + console.error("Error getting suggestion data:", error); res.status(500).send("Internal Server Error"); } }; - // add new suggestion category or field const editSuggestionOption = async (req, res) => { try { - // Read the current suggestion data - let readfile = await readSuggestionFile(); - if (req.body.suggestion) { if (req.body.action === "add") { - readfile.suggestion.unshift(req.body.newField); + suggestionData.suggestion.unshift(req.body.newField); } if (req.body.action === "delete") { - readfile = { - ...readfile, - suggestion: readfile.suggestion.filter( - (item, index) => index + 1 !== +req.body.newField - ), - }; + suggestionData.suggestion = suggestionData.suggestion.filter( + (item, index) => index + 1 !== +req.body.newField + ); } } else { if (req.body.action === "add") { - readfile.field.unshift(req.body.newField); + suggestionData.field.unshift(req.body.newField); } if (req.body.action === "delete") { - readfile = { - ...readfile, - field: readfile.field.filter((item) => item !== req.body.newField), - }; + suggestionData.field = suggestionData.field.filter( + (item) => item !== req.body.newField + ); } } - // Get the file path - const filepath = await getSuggestionFilePath(); - - // Write the updated data back to the file - await fs.writeFile(filepath, JSON.stringify(readfile, null, 2)); - res.status(200).send("success"); } catch (error) { console.error("Error editing suggestion option:", error); From 1b9f75a788ab4290efc310d14283246ee118d549 Mon Sep 17 00:00:00 2001 From: Nathan Hoffman Date: Thu, 28 Sep 2023 13:02:47 -0700 Subject: [PATCH 13/37] Change HTTP status codes --- src/controllers/rolePresetsController.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/rolePresetsController.js b/src/controllers/rolePresetsController.js index 63c182577..3642627f7 100644 --- a/src/controllers/rolePresetsController.js +++ b/src/controllers/rolePresetsController.js @@ -29,7 +29,7 @@ const rolePresetsController = function (Preset) { preset.presetName = req.body.presetName; preset.permissions = req.body.permissions; preset.save() - .then(res.status(200).send({ message: 'New preset created' })) + .then(result => res.status(201).send({ newPreset: result, message: 'New preset created' })) .catch(error => res.status(400).send({ error })); }; @@ -46,7 +46,7 @@ const rolePresetsController = function (Preset) { record.presetName = req.body.presetName; record.permissions = req.body.permissions; record.save() - .then(results => res.status(201).send(results)) + .then(results => res.status(200).send(results)) .catch(errors => res.status(400).send(errors)); }) .catch(error => res.status(400).send({ error })); From 0c0373bf3f03c1a73370673df00f36a6ee2e6ff3 Mon Sep 17 00:00:00 2001 From: robertoooc Date: Thu, 28 Sep 2023 16:21:55 -0700 Subject: [PATCH 14/37] added statement to clear user cache if needed so profile page is updated immediately --- src/controllers/teamController.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/controllers/teamController.js b/src/controllers/teamController.js index 1072f1fb4..9fa20ab2b 100644 --- a/src/controllers/teamController.js +++ b/src/controllers/teamController.js @@ -1,6 +1,7 @@ const mongoose = require('mongoose'); const userProfile = require('../models/userProfile'); const { hasPermission } = require('../utilities/permissions'); +const cache = require('../utilities/nodeCache')(); const teamcontroller = function (Team) { const getAllTeams = function (req, res) { @@ -113,6 +114,8 @@ const teamcontroller = function (Team) { users.forEach((element) => { const { userId, operation } = element; + // if user's profile is stored in cache, clear it so when you visit their profile page it will be up to date + if(cache.hasCache(`user-${userId}`)) cache.removeCache(`user-${userId}`); if (operation === 'Assign') { assignlist.push(userId); From 165fc73cb11a963c8afa360a628e4a39a6428517 Mon Sep 17 00:00:00 2001 From: Tim Kent Date: Fri, 29 Sep 2023 13:35:30 -0700 Subject: [PATCH 15/37] add material schema, update item type schema --- src/models/inventoryItemType.js | 21 ++++++++++++--- src/models/inventoryMaterial.js | 46 +++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 src/models/inventoryMaterial.js diff --git a/src/models/inventoryItemType.js b/src/models/inventoryItemType.js index 80e5e0d7b..92139f27d 100644 --- a/src/models/inventoryItemType.js +++ b/src/models/inventoryItemType.js @@ -2,11 +2,24 @@ const mongoose = require('mongoose'); const { Schema } = mongoose; -const InventoryItemType = new Schema({ +// const InventoryItemType = new Schema({ +// name: { type: String, required: true }, +// description: { type: String }, +// imageUrl: { type: String }, +// quantifier: { type: String, default: 'each' }, +// }); + +const InventoryItemType = new Schema({ // creates an item, tracks total amount in organization's stock + type: { type: String, required: true }, // ie Material, Equipment, Tool name: { type: String, required: true }, - description: { type: String }, - imageUrl: { type: String }, - quantifier: { type: String, default: 'each' }, + description: { type: String, required: true, maxLength: 150 }, + uom: { type: String, required: true }, // unit of measurement + totalStock: { type: Number, required: true }, // total amount of all stock acquired + totalAvailable: { type: Number, required: true }, // amount of item available to be checked out + projectsUsing: [String], // ids of projects using the item + imageUrl: { type: String } }); module.exports = mongoose.model('inventoryItemType', InventoryItemType, 'inventoryItemType'); + + diff --git a/src/models/inventoryMaterial.js b/src/models/inventoryMaterial.js new file mode 100644 index 000000000..31b6ce069 --- /dev/null +++ b/src/models/inventoryMaterial.js @@ -0,0 +1,46 @@ +const mongoose = require('mongoose') + +const {Schema} = mongoose + +const InventoryMaterial = new Schema({ + inventoryItemType: { type: mongoose.SchemaTypes.ObjectId, ref: 'inventoryItemType', required: true }, + projectId: {type: Number, required: true }, + stockBought: { type: Number, required: true }, // amount bought for project, affects total stock + stockUsed: { type: Number, required: true }, + stockAvailable: { type: Number, required: true }, + stockHeld: { type: Number, required: true }, + stockWasted: { type: Number, required: true }, + usageRecord: [{ // daily log of amount inventory item used at job site + date: { type: Date, required: true, default: Date.now() }, + createdBy: {type: String, required: true }, + taskId: { type: String, required: true }, + quantityUsed: { type: Number, required: true }, + responsibleUserId: { type: String, required: true } + }], + updateRecord: [{ // incident report affecting quantity/status of inventory item + date: { type: Date, required: true, default: Date.now() }, + createdBy: {type: String, required: true }, + action: { type: String, required: true }, // ex: Add, Reduce, Hold + cause: { type: String, required: true }, // ex: Used, Lost, Wasted, Transfer + quantity: { type: Number, required: true }, + description: { type: String, required: true, maxLength: 150 }, + responsibleUserId: { type: String, required: true }, + imageUrl: { type: String } + }], + purchaseRecord: [{ + date: { type: Date, required: true, default: Date.now() }, + createdBy: {type: String, required: true }, + invoiceId: { type: String, required: true }, + vendor: { type: String, required: true }, + brand: { type: String, required: true }, + amount: { type: Number, required: true }, // amount of item in each unit + quantity: { type: Number, required: true }, // number of units purchased + unitCost: { type: Number, required: true }, + tax: { type: Number, required: true }, + shipping: { type: Number, required: true }, + totalCost: { type: Number, required: true }, + imageUrl: { type: String }, + }] +}) + +module.exports = mongoose.model('inventoryMaterial', InventoryMaterial, 'inventoryMaterial'); From e2540140d889b31aede0656722eed32c5306d75e Mon Sep 17 00:00:00 2001 From: Tim Kent Date: Mon, 2 Oct 2023 11:46:14 -0700 Subject: [PATCH 16/37] add materials route, router, controller --- .../bmdashboard/bmMaterialsController.js | 12 ++++++++++++ src/routes/bmdashboard/bmMaterialsRouter.js | 13 +++++++++++++ src/startup/routes.js | 2 ++ 3 files changed, 27 insertions(+) create mode 100644 src/controllers/bmdashboard/bmMaterialsController.js create mode 100644 src/routes/bmdashboard/bmMaterialsRouter.js diff --git a/src/controllers/bmdashboard/bmMaterialsController.js b/src/controllers/bmdashboard/bmMaterialsController.js new file mode 100644 index 000000000..11aa4a744 --- /dev/null +++ b/src/controllers/bmdashboard/bmMaterialsController.js @@ -0,0 +1,12 @@ +const bmMaterialsController = function () { + const bmMaterialsList = async function _matsList(req, res) { + try { + return res.json({ message: "Hello World" }) + } catch (err) { + res.json(err); + } + }; + return { bmMaterialsList }; +}; + +module.exports = bmMaterialsController; \ No newline at end of file diff --git a/src/routes/bmdashboard/bmMaterialsRouter.js b/src/routes/bmdashboard/bmMaterialsRouter.js new file mode 100644 index 000000000..d8cca1aca --- /dev/null +++ b/src/routes/bmdashboard/bmMaterialsRouter.js @@ -0,0 +1,13 @@ +const express = require('express'); + +const routes = function () { +const materialsRouter = express.Router(); +const controller = require('../../controllers/bmdashboard/bmMaterialsController')(); + +materialsRouter.route('/materials') + .get(controller.bmMaterialsList); + + return materialsRouter; +} + +module.exports = routes; \ No newline at end of file diff --git a/src/startup/routes.js b/src/startup/routes.js index 43cd226bd..e0852dac3 100644 --- a/src/startup/routes.js +++ b/src/startup/routes.js @@ -55,6 +55,7 @@ const mouseoverTextRouter = require('../routes/mouseoverTextRouter')(mouseoverTe // bm dashboard const bmLoginRouter = require('../routes/bmdashboard/bmLoginRouter')(); +const bmMaterialsRouter = require('../routes/bmdashboard/bmMaterialsRouter')(); module.exports = function (app) { @@ -88,4 +89,5 @@ module.exports = function (app) { app.use('/api', mouseoverTextRouter); // bm dashboard app.use('/api/bm', bmLoginRouter); + app.use('/api/bm', bmMaterialsRouter); }; From 9fdcdb8d4930421928d5deb4d825c7f5f9c6d213 Mon Sep 17 00:00:00 2001 From: Tim Kent Date: Mon, 2 Oct 2023 18:09:21 -0700 Subject: [PATCH 17/37] update route, router, controller with updated material schema and new collection fetch --- .../bmdashboard/bmMaterialsController.js | 8 +++- ...ryMaterial.js => inventoryItemMaterial.js} | 38 ++++++++----------- src/routes/bmdashboard/bmMaterialsRouter.js | 4 +- src/startup/routes.js | 4 +- 4 files changed, 25 insertions(+), 29 deletions(-) rename src/models/{inventoryMaterial.js => inventoryItemMaterial.js} (51%) diff --git a/src/controllers/bmdashboard/bmMaterialsController.js b/src/controllers/bmdashboard/bmMaterialsController.js index 11aa4a744..797d5730d 100644 --- a/src/controllers/bmdashboard/bmMaterialsController.js +++ b/src/controllers/bmdashboard/bmMaterialsController.js @@ -1,7 +1,11 @@ -const bmMaterialsController = function () { +const mongoose = require('mongoose') + +const bmMaterialsController = function (ItemMaterial, ItemType) { const bmMaterialsList = async function _matsList(req, res) { try { - return res.json({ message: "Hello World" }) + ItemMaterial.find() + .then(results => res.status(200).send(results)) + .catch(error => res.status(500).send(error)) } catch (err) { res.json(err); } diff --git a/src/models/inventoryMaterial.js b/src/models/inventoryItemMaterial.js similarity index 51% rename from src/models/inventoryMaterial.js rename to src/models/inventoryItemMaterial.js index 31b6ce069..b795d2f63 100644 --- a/src/models/inventoryMaterial.js +++ b/src/models/inventoryItemMaterial.js @@ -1,10 +1,10 @@ -const mongoose = require('mongoose') +const mongoose = require('mongoose'); -const {Schema} = mongoose +const { Schema } = mongoose; -const InventoryMaterial = new Schema({ +const InventoryItemMaterial = new Schema({ inventoryItemType: { type: mongoose.SchemaTypes.ObjectId, ref: 'inventoryItemType', required: true }, - projectId: {type: Number, required: true }, + project: { type: mongoose.SchemaTypes.ObjectId, ref: 'project', required: true }, stockBought: { type: Number, required: true }, // amount bought for project, affects total stock stockUsed: { type: Number, required: true }, stockAvailable: { type: Number, required: true }, @@ -12,35 +12,27 @@ const InventoryMaterial = new Schema({ stockWasted: { type: Number, required: true }, usageRecord: [{ // daily log of amount inventory item used at job site date: { type: Date, required: true, default: Date.now() }, - createdBy: {type: String, required: true }, - taskId: { type: String, required: true }, + createdBy: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile', required: true }, quantityUsed: { type: Number, required: true }, - responsibleUserId: { type: String, required: true } }], updateRecord: [{ // incident report affecting quantity/status of inventory item date: { type: Date, required: true, default: Date.now() }, - createdBy: {type: String, required: true }, - action: { type: String, required: true }, // ex: Add, Reduce, Hold - cause: { type: String, required: true }, // ex: Used, Lost, Wasted, Transfer - quantity: { type: Number, required: true }, + createdBy: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile', required: true }, + action: { type: String, required: true }, // ex: Add, Reduce, Hold (updates stock quantities) + cause: { type: String, required: true }, // ex: Used, Lost, Wasted, Transfer (reason for update) + quantity: { type: Number, required: true }, // amount of material affected description: { type: String, required: true, maxLength: 150 }, - responsibleUserId: { type: String, required: true }, - imageUrl: { type: String } }], purchaseRecord: [{ date: { type: Date, required: true, default: Date.now() }, - createdBy: {type: String, required: true }, - invoiceId: { type: String, required: true }, - vendor: { type: String, required: true }, - brand: { type: String, required: true }, - amount: { type: Number, required: true }, // amount of item in each unit - quantity: { type: Number, required: true }, // number of units purchased - unitCost: { type: Number, required: true }, + createdBy: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile', required: true }, + poId: { type: String, required: true }, + sellerId: { type: String, required: true }, + quantity: { type: Number, required: true }, // adds to stockBought + subtotal: { type: Number, required: true }, tax: { type: Number, required: true }, shipping: { type: Number, required: true }, - totalCost: { type: Number, required: true }, - imageUrl: { type: String }, }] }) -module.exports = mongoose.model('inventoryMaterial', InventoryMaterial, 'inventoryMaterial'); +module.exports = mongoose.model('inventoryItemMaterial', InventoryItemMaterial, 'inventoryMaterial'); diff --git a/src/routes/bmdashboard/bmMaterialsRouter.js b/src/routes/bmdashboard/bmMaterialsRouter.js index d8cca1aca..d3e4518da 100644 --- a/src/routes/bmdashboard/bmMaterialsRouter.js +++ b/src/routes/bmdashboard/bmMaterialsRouter.js @@ -1,8 +1,8 @@ const express = require('express'); -const routes = function () { +const routes = function (itemMaterial, itemType) { const materialsRouter = express.Router(); -const controller = require('../../controllers/bmdashboard/bmMaterialsController')(); +const controller = require('../../controllers/bmdashboard/bmMaterialsController')(itemMaterial, itemType); materialsRouter.route('/materials') .get(controller.bmMaterialsList); diff --git a/src/startup/routes.js b/src/startup/routes.js index e0852dac3..cde07c94d 100644 --- a/src/startup/routes.js +++ b/src/startup/routes.js @@ -20,6 +20,7 @@ const ownerStandardMessage = require('../models/ownerStandardMessage'); const profileInitialSetuptoken = require('../models/profileInitialSetupToken'); const reason = require('../models/reason'); const mouseoverText = require('../models/mouseoverText'); +const inventoryItemMaterial = require('../models/inventoryItemMaterial'); const userProfileRouter = require('../routes/userProfileRouter')(userProfile); const badgeRouter = require('../routes/badgeRouter')(badge); @@ -55,8 +56,7 @@ const mouseoverTextRouter = require('../routes/mouseoverTextRouter')(mouseoverTe // bm dashboard const bmLoginRouter = require('../routes/bmdashboard/bmLoginRouter')(); -const bmMaterialsRouter = require('../routes/bmdashboard/bmMaterialsRouter')(); - +const bmMaterialsRouter = require('../routes/bmdashboard/bmMaterialsRouter')(inventoryItemMaterial, inventoryItemType); module.exports = function (app) { app.use('/api', forgotPwdRouter); From 9da89884167f825caf399c09ae44d0bb6fe35079 Mon Sep 17 00:00:00 2001 From: Tim Kent Date: Mon, 2 Oct 2023 20:12:37 -0700 Subject: [PATCH 18/37] update inventoryitemtype schema --- src/models/inventoryItemType.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/models/inventoryItemType.js b/src/models/inventoryItemType.js index 92139f27d..207ac77b0 100644 --- a/src/models/inventoryItemType.js +++ b/src/models/inventoryItemType.js @@ -2,21 +2,14 @@ const mongoose = require('mongoose'); const { Schema } = mongoose; -// const InventoryItemType = new Schema({ -// name: { type: String, required: true }, -// description: { type: String }, -// imageUrl: { type: String }, -// quantifier: { type: String, default: 'each' }, -// }); - const InventoryItemType = new Schema({ // creates an item, tracks total amount in organization's stock type: { type: String, required: true }, // ie Material, Equipment, Tool name: { type: String, required: true }, description: { type: String, required: true, maxLength: 150 }, uom: { type: String, required: true }, // unit of measurement totalStock: { type: Number, required: true }, // total amount of all stock acquired - totalAvailable: { type: Number, required: true }, // amount of item available to be checked out - projectsUsing: [String], // ids of projects using the item + totalAvailable: { type: Number, required: true }, + projectsUsing: [ {type: mongoose.SchemaTypes.ObjectId, ref: 'project'} ], imageUrl: { type: String } }); From 2360136d78c82b61a263b24dc5265cfc1ad04588 Mon Sep 17 00:00:00 2001 From: Nathan Hoffman Date: Tue, 3 Oct 2023 13:18:29 -0700 Subject: [PATCH 19/37] Fix merge conflict extra bracket --- src/controllers/projectController.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/controllers/projectController.js b/src/controllers/projectController.js index b0239835d..a88378985 100644 --- a/src/controllers/projectController.js +++ b/src/controllers/projectController.js @@ -127,7 +127,6 @@ const projectController = function (Project) { if (!await hasPermission(req.body.requestor, 'assignProjectToUsers')) { res.status(403).send({ error: 'You are not authorized to perform this operation' }); return; - } } if (!req.params.projectId || !mongoose.Types.ObjectId.isValid(req.params.projectId) || !req.body.users || (req.body.users.length === 0)) { From 92059d53fff2bc23a21b249f568f172d83849aca Mon Sep 17 00:00:00 2001 From: Nathan Hoffman Date: Tue, 3 Oct 2023 13:22:50 -0700 Subject: [PATCH 20/37] fix another extra bracket --- src/controllers/userProfileController.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/controllers/userProfileController.js b/src/controllers/userProfileController.js index 0899ab83b..447b14d94 100644 --- a/src/controllers/userProfileController.js +++ b/src/controllers/userProfileController.js @@ -55,7 +55,6 @@ const userProfileController = function (UserProfile) { if (!await hasPermission(req.body.requestor, 'getUserProfiles')) { res.status(403).send('You are not authorized to view all users'); return; - } } if (cache.getCache("allusers")) { From d4e04bd6c970ec6c27a4b21d48ba64b1b3c8fdfc Mon Sep 17 00:00:00 2001 From: Tim Kent Date: Wed, 4 Oct 2023 16:31:43 -0700 Subject: [PATCH 21/37] populate db fetch with project, inventoryItemType objects --- src/controllers/bmdashboard/bmMaterialsController.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/controllers/bmdashboard/bmMaterialsController.js b/src/controllers/bmdashboard/bmMaterialsController.js index 797d5730d..b55b70b7e 100644 --- a/src/controllers/bmdashboard/bmMaterialsController.js +++ b/src/controllers/bmdashboard/bmMaterialsController.js @@ -4,6 +4,14 @@ const bmMaterialsController = function (ItemMaterial, ItemType) { const bmMaterialsList = async function _matsList(req, res) { try { ItemMaterial.find() + .populate({ + path: 'project', + select: '_id projectName' + }) + .populate({ + path: 'inventoryItemType', + select: '_id name uom totalStock' + }) .then(results => res.status(200).send(results)) .catch(error => res.status(500).send(error)) } catch (err) { From 975a858079086a9e3e88249ca0e3f49277d620ae Mon Sep 17 00:00:00 2001 From: Tim Kent Date: Thu, 5 Oct 2023 11:55:09 -0700 Subject: [PATCH 22/37] update populate method --- .../bmdashboard/bmMaterialsController.js | 42 +++++++++++++++---- src/routes/bmdashboard/bmMaterialsRouter.js | 4 +- src/startup/routes.js | 2 +- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/controllers/bmdashboard/bmMaterialsController.js b/src/controllers/bmdashboard/bmMaterialsController.js index b55b70b7e..a31ed460e 100644 --- a/src/controllers/bmdashboard/bmMaterialsController.js +++ b/src/controllers/bmdashboard/bmMaterialsController.js @@ -1,17 +1,41 @@ const mongoose = require('mongoose') -const bmMaterialsController = function (ItemMaterial, ItemType) { +const bmMaterialsController = function (ItemMaterial) { const bmMaterialsList = async function _matsList(req, res) { try { ItemMaterial.find() - .populate({ - path: 'project', - select: '_id projectName' - }) - .populate({ - path: 'inventoryItemType', - select: '_id name uom totalStock' - }) + .populate([ + { + path: 'project', + select: '_id projectName' + }, + { + path: 'inventoryItemType', + select: '_id name uom totalStock totalAvailable' + }, + { + path: 'usageRecord', + populate: { + path: 'createdBy', + select: '_id firstName lastName' + } + }, + { + path: 'updateRecord', + populate: { + path: 'createdBy', + select: '_id firstName lastName' + } + }, + { + path: 'purchaseRecord', + populate: { + path: 'createdBy', + select: '_id firstName lastName' + } + } + ]) + .exec() .then(results => res.status(200).send(results)) .catch(error => res.status(500).send(error)) } catch (err) { diff --git a/src/routes/bmdashboard/bmMaterialsRouter.js b/src/routes/bmdashboard/bmMaterialsRouter.js index d3e4518da..ab8a67388 100644 --- a/src/routes/bmdashboard/bmMaterialsRouter.js +++ b/src/routes/bmdashboard/bmMaterialsRouter.js @@ -1,8 +1,8 @@ const express = require('express'); -const routes = function (itemMaterial, itemType) { +const routes = function (itemMaterial) { const materialsRouter = express.Router(); -const controller = require('../../controllers/bmdashboard/bmMaterialsController')(itemMaterial, itemType); +const controller = require('../../controllers/bmdashboard/bmMaterialsController')(itemMaterial); materialsRouter.route('/materials') .get(controller.bmMaterialsList); diff --git a/src/startup/routes.js b/src/startup/routes.js index cde07c94d..bbd22b530 100644 --- a/src/startup/routes.js +++ b/src/startup/routes.js @@ -56,7 +56,7 @@ const mouseoverTextRouter = require('../routes/mouseoverTextRouter')(mouseoverTe // bm dashboard const bmLoginRouter = require('../routes/bmdashboard/bmLoginRouter')(); -const bmMaterialsRouter = require('../routes/bmdashboard/bmMaterialsRouter')(inventoryItemMaterial, inventoryItemType); +const bmMaterialsRouter = require('../routes/bmdashboard/bmMaterialsRouter')(inventoryItemMaterial); module.exports = function (app) { app.use('/api', forgotPwdRouter); From 61663e93b974ecb87ecea6daab9ff6fa200f011b Mon Sep 17 00:00:00 2001 From: Tim Kent Date: Mon, 9 Oct 2023 14:09:42 -0700 Subject: [PATCH 23/37] update material, itemType schema --- src/models/inventoryItemMaterial.js | 2 ++ src/models/inventoryItemType.js | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/models/inventoryItemMaterial.js b/src/models/inventoryItemMaterial.js index b795d2f63..e6153af45 100644 --- a/src/models/inventoryItemMaterial.js +++ b/src/models/inventoryItemMaterial.js @@ -29,6 +29,8 @@ const InventoryItemMaterial = new Schema({ poId: { type: String, required: true }, sellerId: { type: String, required: true }, quantity: { type: Number, required: true }, // adds to stockBought + unitPrice: {type: Number, required: true}, + currency: { type: String, required: true }, subtotal: { type: Number, required: true }, tax: { type: Number, required: true }, shipping: { type: Number, required: true }, diff --git a/src/models/inventoryItemType.js b/src/models/inventoryItemType.js index 207ac77b0..321038a84 100644 --- a/src/models/inventoryItemType.js +++ b/src/models/inventoryItemType.js @@ -10,7 +10,8 @@ const InventoryItemType = new Schema({ // creates an item, tracks total amount i totalStock: { type: Number, required: true }, // total amount of all stock acquired totalAvailable: { type: Number, required: true }, projectsUsing: [ {type: mongoose.SchemaTypes.ObjectId, ref: 'project'} ], - imageUrl: { type: String } + imageUrl: { type: String }, + link: { type: String} }); module.exports = mongoose.model('inventoryItemType', InventoryItemType, 'inventoryItemType'); From 4d98c53461d9c6150aa306d7a13fa528357181ff Mon Sep 17 00:00:00 2001 From: Nathan Hoffman Date: Sat, 14 Oct 2023 13:41:18 -0700 Subject: [PATCH 24/37] Fix collection name --- src/utilities/createInitialPermissions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utilities/createInitialPermissions.js b/src/utilities/createInitialPermissions.js index a5c69d87f..6e22eb6a1 100644 --- a/src/utilities/createInitialPermissions.js +++ b/src/utilities/createInitialPermissions.js @@ -265,9 +265,9 @@ const createInitialPermissions = async () => { // If role exists in db and is not updated, update default } else if (!presetDataBase.permissions.every(perm => permissions.includes(perm)) || !permissions.every(perm => presetDataBase.permissions.includes(perm))) { - const roleId = presetDataBase._id; + const presetId = presetDataBase._id; - promises.push(Role.findById(roleId, (_, record) => { + promises.push(RolePreset.findById(presetId, (_, record) => { record.permissions = permissions; record.save(); })); From 4c4db04f1e252bb6e65a6fbebed97c8280c16b0e Mon Sep 17 00:00:00 2001 From: abdelmounaim lallouache Date: Tue, 17 Oct 2023 20:29:28 -0500 Subject: [PATCH 25/37] add weeklyCommitedHours to setup user, fix updates issues, fix weekly summaries logged hours bug --- .../profileInitialSetupController.js | 41 +++++++++++++++++-- src/models/profileInitialSetupToken.js | 5 +++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/controllers/profileInitialSetupController.js b/src/controllers/profileInitialSetupController.js index f671e06a3..18cf7376c 100644 --- a/src/controllers/profileInitialSetupController.js +++ b/src/controllers/profileInitialSetupController.js @@ -4,6 +4,7 @@ const moment = require("moment-timezone"); const jwt = require("jsonwebtoken"); const emailSender = require("../utilities/emailSender"); const config = require("../config"); +const cache = require('../utilities/nodeCache')(); // returns the email body that includes the setup link for the recipient. function sendLinkMessage(Link) { @@ -25,7 +26,6 @@ function informManagerMessage(user) {

New User ${user.firstName} ${user.lastName} has completed their part of setup.

These areas need to now be completed by an Admin:

    -
  • Weekly Committed Hours
  • Admin Document
  • Link to Media Files
  • Assign Projects
  • @@ -57,6 +57,10 @@ function informManagerMessage(user) { Job Title: ${user.jobTitle} + + Weekly Commited Hours: + ${user.weeklycommittedHours} + Time Zone: ${user.timeZone} @@ -87,7 +91,7 @@ const profileInitialSetupController = function ( - Generates a link using the token and emails it to the recipient. */ const getSetupToken = async (req, res) => { - let { email, baseUrl } = req.body; + let { email, baseUrl,weeklyCommittedHours } = req.body; email = email.toLowerCase(); const token = uuidv4(); const expiration = moment().tz("America/Los_Angeles").add(1, "week"); @@ -103,6 +107,7 @@ const profileInitialSetupController = function ( const newToken = new ProfileInitialSetupToken({ token, email, + weeklyCommittedHours, expiration: expiration.toDate(), }); @@ -187,7 +192,13 @@ const profileInitialSetupController = function ( newUser.jobTitle = req.body.jobTitle; newUser.phoneNumber = req.body.phoneNumber; newUser.bio = ""; - newUser.weeklycommittedHours = req.body.weeklycommittedHours; + newUser.weeklycommittedHours = foundToken.weeklyCommittedHours; + newUser.weeklycommittedHoursHistory = [ + { + hours: newUser.weeklycommittedHours, + dateChanged: Date.now(), + }, + ]; newUser.personalLinks = []; newUser.adminLinks = []; newUser.teams = Array.from(new Set([])); @@ -201,11 +212,17 @@ const profileInitialSetupController = function ( newUser.collaborationPreference = req.body.collaborationPreference; newUser.timeZone = req.body.timeZone || "America/Los_Angeles"; newUser.location = req.body.location; + newUser.permissions = { + frontPermissions: [], + backPermissions: [] + } newUser.bioPosted = "default"; newUser.privacySettings.email = req.body.privacySettings.email; newUser.privacySettings.phoneNumber = req.body.privacySettings.phoneNumber; newUser.teamCode = ""; + newUser.isFirstTimelog = true; + const savedUser = await newUser.save(); emailSender( @@ -230,6 +247,24 @@ const profileInitialSetupController = function ( const token = jwt.sign(jwtPayload, JWT_SECRET); res.send({ token }).status(200); + + const NewUserCache = { + permissions: savedUser.permissions, + isActive: true, + weeklycommittedHours: savedUser.weeklycommittedHours, + createdDate: savedUser.createdDate.toISOString(), + _id: savedUser._id, + role: savedUser.role, + firstName: savedUser.firstName, + lastName: savedUser.lastName, + email: savedUser.email, + }; + + const allUserCache = JSON.parse(cache.getCache("allusers")); + allUserCache.push(NewUserCache); + cache.setCache("allusers", JSON.stringify(allUserCache)); + + } else { res.status(400).send("Token is expired"); } diff --git a/src/models/profileInitialSetupToken.js b/src/models/profileInitialSetupToken.js index fc21bcad5..48413fb77 100644 --- a/src/models/profileInitialSetupToken.js +++ b/src/models/profileInitialSetupToken.js @@ -10,6 +10,11 @@ const profileInitialSetupTokenSchema = new mongoose.Schema({ type: String, required: true, }, + weeklyCommittedHours : { + type: Number, + required: true, + default: 10, + }, expiration: { type: Date, required: true, From 5ffc3d23442fc46a71544a885b3fd42def4a849b Mon Sep 17 00:00:00 2001 From: wang9hu Date: Tue, 17 Oct 2023 20:53:56 -0700 Subject: [PATCH 26/37] finish new timer basic functionality --- src/models/timer.js | 7 +- src/routes/dashboardRouter.js | 1 - src/websockets/TimerService/clientsHandler.js | 286 ++++++++++++++++ .../TimerService/connectionsHandler.js | 50 +++ src/websockets/TimerService/index.js | 308 ------------------ src/websockets/index.js | 153 ++++----- 6 files changed, 394 insertions(+), 411 deletions(-) create mode 100644 src/websockets/TimerService/clientsHandler.js create mode 100644 src/websockets/TimerService/connectionsHandler.js delete mode 100644 src/websockets/TimerService/index.js diff --git a/src/models/timer.js b/src/models/timer.js index c73dfe6c2..8afbad119 100644 --- a/src/models/timer.js +++ b/src/models/timer.js @@ -5,13 +5,12 @@ const { Schema } = mongoose; const timerSchema = new Schema({ userId: { type: Schema.Types.ObjectId, required: true, ref: "userProfile" }, - lastAccess: { type: Date, default: Date.now }, + startAt: { type: Date, default: Date.now }, time: { type: Number, default: 900000 }, - countdown: { type: Boolean, default: true }, goal: { type: Number, default: 900000 }, - paused: { type: Boolean, default: true }, + paused: { type: Boolean, default: false }, forcedPause: { type: Boolean, default: false }, - stopped: { type: Boolean, default: false }, + started: { type: Boolean, default: false }, }); module.exports = mongoose.model("newTimer", timerSchema, "newTimers"); diff --git a/src/routes/dashboardRouter.js b/src/routes/dashboardRouter.js index 33275597c..664c1c802 100644 --- a/src/routes/dashboardRouter.js +++ b/src/routes/dashboardRouter.js @@ -3,7 +3,6 @@ const express = require('express'); const route = function () { const controller = require('../controllers/dashBoardController')(); - const Dashboardrouter = express.Router(); Dashboardrouter.route('/dashboard/:userId') diff --git a/src/websockets/TimerService/clientsHandler.js b/src/websockets/TimerService/clientsHandler.js new file mode 100644 index 000000000..fdf21b8f4 --- /dev/null +++ b/src/websockets/TimerService/clientsHandler.js @@ -0,0 +1,286 @@ +/* eslint-disable no-multi-assign */ +/* eslint-disable radix */ +const moment = require('moment'); +const Timer = require('../../models/timer'); +const logger = require('../../startup/logger'); + +/** + * Here we get the timer. + * If the timer already exists in memory, we return it. + * If it doesn't exist, we try to get it from MongoDB. + * If it doesn't exist in MongoDB, we create it and save it to MongoDB. + * Then we save it to memory and return it. + */ +export const getClient = async (clients, userId) => { + // In case of there is already a connection that is open for this user + // for example user open a new connection + if (!clients.has(userId)) { + try { + let timer = await Timer.findOne({ userId }); + if (!timer) timer = await Timer.create({ userId }); + clients.set(userId, timer); + } catch (e) { + logger.logException(e); + throw new Error( + 'Something happened when trying to retrieve timer from mongo', + ); + } + } + return clients.get(userId); +}; + +/* + * Save client info to database + * Save under these conditions: + * connection is normally closed (paused and closed); + * connection is forced-paused (timer still on and connection closed) + * message: STOP_TIMER + */ +export const saveClient = async (client) => { + try { + await Timer.findOneAndUpdate({ userId: client.userId }, client); + } catch (e) { + logger.logException(e); + throw new Error( + `Something happened when trying to save user timer to mongo, Error: ${e}`, + ); + } +}; + +/* + * This is the contract between client and server. + * The client can send one of the following messages to the server: + */ +export const action = { + START_TIMER: 'START_TIMER', + 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=', + FORCED_PAUSE: 'FORCED_PAUSE', + ACK_FORCED: 'ACK_FORCED', +}; + +const updatedTimeSinceStart = (client) => { + if (!client.started) return client.goal; + const now = moment.utc(); + const startAt = moment(client.startAt); + const timePassed = moment.duration(now.diff(startAt)).asMilliseconds(); + const updatedTime = client.time - timePassed; + return updatedTime > 0 ? updatedTime : 0; +}; + +/** + * Here we start the timer, if it is not already started. + * We set the last access time to now, and set the paused and stopped flags to false. + * If the timer was paused, we need to check if it was paused by the user or by the server. + * If it was paused by the server, we need to set the forcedPause flag to true. + */ +const startTimer = (client) => { + client.startAt = moment.utc(); + client.paused = false; + if (!client.started) { + client.started = true; + client.time = client.goal; + } + if (client.forcedPause) client.forcedPause = false; +}; + +/** + * Here we pause the timer, if it is not already paused. + * We get the total elapsed time since the last access, and set it as the new time. + * We set the last access time to now, and set the paused flag to true. + * If the timer was paused by the server, we need to set the forcedPause flag to true. + * It'll only be triggered when the user closes the connection sudenlly or lacks of ACKs. + */ +const pauseTimer = (client, forced = false) => { + client.time = updatedTimeSinceStart(client); + client.startAt = moment.invalid(); + client.paused = true; + if (forced) client.forcedPause = true; +}; + +// Here we acknowledge the forced pause. To prevent the modal for beeing displayed again. +const ackForcedPause = (client) => { + client.forcedPause = false; + client.paused = true; + client.startAt = moment.invalid(); +}; + +/** + * Here we stop the timer. + * We pause the timer and set the stopped flag to true. + */ +const stopTimer = (client) => { + client.startAt = moment.invalid(); + client.started = false; + client.pause = false; + client.forcedPause = false; +}; + +/** + * Here we clear the timer. + * We pause the timer and check it's mode to set the time to 0 or the goal. + * Then we set the stopped flag to false. + */ +const clearTimer = (client) => { + stopTimer(client); + client.time = client.goal; +}; + + +// /* +// Here we switch the timer mode. +// We pause the timer and check it's mode to set the time to 0 or the goal. +// */ +// const switchMode = (client) => { +// client.countdown = !client.countdown; +// client.time = client.countdown ? client.goal : 0; +// client.paused = true; +// }; + +// Here we get the goal time from the message. +const getGoal = msg => parseInt(msg.split('=')[1]); + +// Here we set the goal and time to the goal time. +const setGoal = (client, msg) => { + const newGoal = getGoal(msg); + if (!client.started) { + client.goal = newGoal; + client.time = newGoal; + } else { + const passedTime = client.goal - client.time; + if (passedTime >= newGoal) { + client.time = 0; + client.goal = passedTime; + } else { + client.time = newGoal - passedTime; + client.goal = newGoal; + } + } +}; + +/** + * Here we add the goal time. + * Each addition add 15min + * First we get the goal time from the message. + * Then we add it to the current goal time and set it as the new goal time. + * We also add it to the current time and set it as the new time. + */ +const addGoal = (client, msg) => { + const duration = getGoal(msg); + const goalAfterAddition = moment + .duration(client.goal) + .add(duration, 'milliseconds') + .asHours(); + + if (goalAfterAddition > 10) return; + + client.goal = moment + .duration(client.goal) + .add(duration, 'milliseconds') + .asMilliseconds() + .toFixed(); + client.time = moment + .duration(client.time) + .add(duration, 'milliseconds') + .asMilliseconds() + .toFixed(); +}; + +/** + * Here we try to remove a goal time. + * First we get the goal time from the message. + * Then we subtract it from the current goal time and set it as the new goal time. + * We also subtract it from the current time and set it as the new time. + * If the new goal time is less than 15 minutes, we don't do anything. + * If the new time is less than 0, we set it to 0. + */ +const removeGoal = (client, msg) => { + const duration = getGoal(msg); + const goalAfterRemoval = moment + .duration(client.goal) + .subtract(duration, 'milliseconds') + .asMinutes(); + const timeAfterRemoval = moment + .duration(client.time) + .subtract(duration, 'milliseconds') + .asMinutes(); + + if (goalAfterRemoval < 15 || timeAfterRemoval < 0) return; + + client.goal = moment + .duration(client.goal) + .subtract(duration, 'milliseconds') + .asMilliseconds() + .toFixed(); + client.time = moment + .duration(client.time) + .subtract(duration, 'milliseconds') + .asMilliseconds() + .toFixed(); +}; + + +/** + * Here is were we handle the messages. + * First we check if the user is in memory, if not, we throw an error. + * Then we parse the request and check which action it is and call the corresponding function. + * If we don't have a match, we just return an error. + * The only operation that we write to Mongo it's the stop timer. Other operations are just in memory. + * So the slowest part of the app is the save to Mongo. + * Then we update the current client in hash map and return the response. + */ +export const handleMessage = async (msg, clients, userId) => { + if (!clients.has(userId)) { + throw new Error('It should have this user in memory'); + } + + const client = clients.get(userId); + let resp = null; + + const req = msg.toString(); + switch (req) { + case action.START_TIMER: + startTimer(client); + break; + case req.match(/SET_GOAL=/i)?.input: + setGoal(client, req); + break; + case req.match(/ADD_TO_GOAL=/i)?.input: + addGoal(client, req); + break; + case req.match(/REMOVE_FROM_GOAL=/i)?.input: + removeGoal(client, req); + break; + case action.PAUSE_TIMER: + pauseTimer(client); + break; + case action.FORCED_PAUSE: + pauseTimer(client, true); + break; + case action.ACK_FORCED: + ackForcedPause(client); + break; + case action.CLEAR_TIMER: + clearTimer(client); + break; + case action.STOP_TIMER: + stopTimer(client); + break; + + default: + resp = { + ...client, + error: `Unknown operation ${req}, please use one of ${action}`, + }; + break; + } + + await saveClient(client); + clients.set(userId, client); + if (resp === null) resp = client; + return JSON.stringify(resp); +}; diff --git a/src/websockets/TimerService/connectionsHandler.js b/src/websockets/TimerService/connectionsHandler.js new file mode 100644 index 000000000..6658321bf --- /dev/null +++ b/src/websockets/TimerService/connectionsHandler.js @@ -0,0 +1,50 @@ +const WebSocket = require('ws'); + +/** + * Here we insert the new connection to the connections map. + * If the user is not in the map, we create a new entry with the user id as key and the connection as value. + * Else we just push the connection to the array of connections. + */ +export function insertNewUser(connections, userId, wsConn) { + const userConnetions = connections.get(userId); + if (!userConnetions) connections.set(userId, [wsConn]); + else userConnetions.push(wsConn); +} + +/** + *Here we remove the connection from the connections map. + *If the user is not in the map, we do nothing. + *Else we remove the connection from the array of connections. + *If the array is empty, we delete the user from the map. + */ +export function removeConnection(connections, userId, connToRemove) { + const userConnetions = connections.get(userId); + if (!userConnetions) return; + + const newConns = userConnetions.filter(conn => conn !== connToRemove); + if (newConns.length === 0) connections.delete(userId); + else connections.set(userId, newConns); +} + +/** + * Here we broadcast the message to all the connections that are connected to the same user. + * We check if the connection is open before sending the message. + */ +export function broadcastToSameUser(connections, userId, data) { + const userConnetions = connections.get(userId); + if (!userConnetions) return; + userConnetions.forEach((conn) => { + if (conn.readyState === WebSocket.OPEN) conn.send(data); + }); +} + +/** + * Here we check if there is another connection to the same user. + * If there is, we return true. + * Else we return false. + */ +export function hasOtherConn(connections, userId, anotherConn) { + if (!connections.has(userId)) return false; + const userConnections = connections.get(userId); + return userConnections.some(con => con !== anotherConn && con.readyState === WebSocket.OPEN); +} diff --git a/src/websockets/TimerService/index.js b/src/websockets/TimerService/index.js deleted file mode 100644 index 9eac199ce..000000000 --- a/src/websockets/TimerService/index.js +++ /dev/null @@ -1,308 +0,0 @@ -/* eslint-disable no-multi-assign */ -/* eslint-disable radix */ -const moment = require('moment'); -const Timer = require('../../models/timer'); -const logger = require('../../startup/logger'); - -/* -This is the contract between client and server. -The client can send one of the following messages to the server: -*/ -export const action = { - START_TIMER: 'START_TIMER', - PAUSE_TIMER: 'PAUSE_TIMER', - STOP_TIMER: 'STOP_TIMER', - GET_TIMER: 'GET_TIMER', - CLEAR_TIMER: 'CLEAR_TIMER', - SWITCH_MODE: 'SWITCH_MODE', - SET_GOAL: 'SET_GOAL=', - ADD_GOAL: 'ADD_GOAL=', - REMOVE_GOAL: 'REMOVE_GOAL=', - FORCED_PAUSE: 'FORCED_PAUSE', - ACK_FORCED: 'ACK_FORCED', -}; - -/* -Here we get the total elapsed time since the last access. -Since we have two modes for the timer, countdown and stopwatch, -we need to know which one is active to calculate the total elapsed time. -If the timer is in countdown mode, we need to subtract the elapsed time from the total time. -if this total time is less than 0, we set it to 0. -If the timer is in stopwatch mode, -we need to add the elapsed time since the last access to the total time. -we then return the total -*/ -const getTotalElapsedTime = (client) => { - const now = moment(); - const lastAccess = moment(client.lastAccess); - const elapSinceLastAccess = moment.duration(now.diff(lastAccess)); - const time = moment.duration(moment(client.time)); - - let total; - if (client.countdown) { - total = time.subtract(elapSinceLastAccess, 'milliseconds'); - if (total.asMilliseconds() < 0) { - total = moment.duration(0); - } - } else total = elapSinceLastAccess.add(client.time, 'milliseconds'); - - return total; -}; - -/* -Here we start the timer, if it is not already started. -We set the last access time to now, and set the paused and stopped flags to false. -If the timer was paused, we need to check if it was paused by the user or by the server. -If it was paused by the server, we need to set the forcedPause flag to true. -*/ -const startTimer = (client) => { - if (!client.paused) { - client.time = getTotalElapsedTime(client).asMilliseconds().toFixed(); - client.lastAccess = moment(); - } - - if (client.paused) { - client.lastAccess = moment(); - client.stopped = false; - client.paused = false; - if (client.forcedPause) client.forcedPause = false; - } -}; - -/* -Here we pause the timer, if it is not already paused. -We get the total elapsed time since the last access, and set it as the new time. -We set the last access time to now, and set the paused flag to true. -If the timer was paused by the server, we need to set the forcedPause flag to true. -It'll only be triggered when the user closes the connection sudenlly or lacks of ACKs. -*/ -const pauseTimer = (client, forced = false) => { - if (!client.paused) { - client.time = getTotalElapsedTime(client).asMilliseconds().toFixed(); - client.lastAccess = moment(); - client.paused = true; - if (forced) client.forcedPause = true; - } -}; - -// Here we acknowledge the forced pause. To prevent the modal for beeing displayed again. -const ackForcedPause = (client) => { - client.forcedPause = false; -}; - -/* -Here we clear the timer. -We pause the timer and check it's mode to set the time to 0 or the goal. -Then we set the stopped flag to false. -*/ -const clearTimer = (client) => { - pauseTimer(client); - client.time = client.countdown ? client.goal : 0; - client.stopped = false; -}; - -/* -Here we stop the timer. -We pause the timer and set the stopped flag to true. -*/ -const stopTimer = (client) => { - pauseTimer(client); - client.stopped = true; -}; - -/* -Here we switch the timer mode. -We pause the timer and check it's mode to set the time to 0 or the goal. -*/ -const switchMode = (client) => { - client.countdown = !client.countdown; - client.time = client.countdown ? client.goal : 0; - client.paused = true; -}; - -// Here we get the goal time from the message. -const getGoal = msg => parseInt(msg.split('=')[1]); - -// Here we set the goal and time to the goal time. -const setGoal = (client, msg) => { - const goal = getGoal(msg); - client.goal = client.time = goal; -}; - -const goalOver10Hours = (client, time) => { - const goal = moment.duration(client.goal).add(time, 'milliseconds').asHours(); - return goal > 10; -}; - -/* -Here we add the goal time. -First we get the goal time from the message. -Then we add it to the current goal time and set it as the new goal time. -We also add it to the current time and set it as the new time. -*/ -const addGoal = (client, msg) => { - const goal = getGoal(msg); - if (goalOver10Hours(client, goal)) return; - - if (!client.paused) { - client.time = getTotalElapsedTime(client).asMilliseconds().toFixed(); - client.lastAccess = moment(); - } - - client.goal = moment - .duration(client.goal) - .add(goal, 'milliseconds') - .asMilliseconds() - .toFixed(); - client.time = moment - .duration(client.time) - .add(goal, 'milliseconds') - .asMilliseconds() - .toFixed(); -}; - -/* - * Here we check if the goal time is less than 15 minutes. - * */ -const goalLessThan15min = (client, time) => { - const goal = moment - .duration(client.goal) - .subtract(time, 'milliseconds') - .asMinutes(); - return goal < 15; -}; - -/* - * Here we try to remove a goal time. - * First we get the goal time from the message. - * Then we subtract it from the current goal time and set it as the new goal time. - * We also subtract it from the current time and set it as the new time. - * If the new goal time is less than 15 minutes, we don't do anything. - * If the new time is less than 0, we set it to 0. - * */ -const removeGoal = (client, msg) => { - const goal = getGoal(msg); - if (goalLessThan15min(client, goal)) return; - - if (!client.paused) { - client.time = getTotalElapsedTime(client).asMilliseconds().toFixed(); - client.lastAccess = moment(); - } - - client.goal = moment - .duration(client.goal) - .subtract(goal, 'milliseconds') - .asMilliseconds() - .toFixed(); - const time = moment - .duration(client.time) - .subtract(goal, 'milliseconds') - .asMilliseconds() - .toFixed(); - client.time = time < 0 ? 0 : time; -}; - -/* -Here we get the timer. -If the timer already exists in memory, we return it. -If it doesn't exist, we try to get it from MongoDB. -If it doesn't exist in MongoDB, we create it and save it to MongoDB. -Then we save it to memory and return it. -*/ -export const getTimer = async (clientsMap, userId) => { - if (clientsMap.has(userId)) return; - - try { - let timer = await Timer.findOne({ userId }); - if (!timer) timer = await Timer.create({ userId }); - clientsMap.set(userId, timer); - } catch (e) { - logger.logException(e); - throw new Error( - 'Something happened when trying to retrieve timer from mongo', - ); - } -}; - -// Here we just save the timer to MongoDB. -const saveClient = async (client) => { - try { - await Timer.findOneAndUpdate({ userId: client.userId }, client); - } catch (e) { - logger.logException(e); - throw new Error( - 'Something happened when trying to save user timer to mongo', - ); - } -}; - -/* -Here is were we handle the messages. -First we check if the user is in memory, if not, we throw an error. -Then we parse the request and check which action it is and call the corresponding function. -If we don't have a match, we just return an error. -The only operation that we write to Mongo it's the stop timer. Other operations are just in memory. -So the slowest part of the app is the save to Mongo. -Then we update the current client in hash map and return the response. -*/ -export const handleMessage = async (msg, clientsMap, userId) => { - if (!clientsMap.has(userId)) { - throw new Error('It should have this user in memory'); - } - - const client = clientsMap.get(userId); - let resp = null; - - const req = msg.toString(); - switch (req) { - case action.GET_TIMER: - break; - case action.START_TIMER: - startTimer(client); - break; - case action.SWITCH_MODE: - switchMode(client); - break; - case req.match(/SET_GOAL=/i)?.input: - setGoal(client, req); - break; - case req.match(/ADD_GOAL=/i)?.input: - addGoal(client, req); - break; - case req.match(/REMOVE_GOAL=/i)?.input: - removeGoal(client, req); - break; - case action.PAUSE_TIMER: - pauseTimer(client); - break; - case action.FORCED_PAUSE: - pauseTimer(client, true); - break; - case action.ACK_FORCED: - ackForcedPause(client); - break; - case action.CLEAR_TIMER: - clearTimer(client); - break; - case action.STOP_TIMER: - stopTimer(client); - break; - - default: - resp = { - ...client, - error: `Unknown operation ${req}, please use one of ${action}`, - }; - break; - } - - if (req === action.STOP_TIMER) { - await saveClient(client).catch((err) => { - resp = { ...client, error: err }; - }); - } - - clientsMap.set(userId, client); - if (resp === null) resp = client; - return JSON.stringify(resp); -}; diff --git a/src/websockets/index.js b/src/websockets/index.js index a733dff25..5da85f729 100644 --- a/src/websockets/index.js +++ b/src/websockets/index.js @@ -7,15 +7,25 @@ const WebSocket = require("ws"); const moment = require("moment"); const jwt = require("jsonwebtoken"); const config = require("../config"); -const { getTimer, handleMessage, action } = require("./TimerService/"); - -/* -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 { + 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. */ -export const authenticate = (req, res) => { +const authenticate = (req, res) => { const authToken = req.headers?.["sec-websocket-protocol"]; let payload = ""; try { @@ -37,68 +47,13 @@ export const authenticate = (req, res) => { res(null, payload.userid); }; -/* - * Here we insert the new connection to the connections map. - * If the user is not in the map, we create a new entry with the user id as key and the connection as value. - * Else we just push the connection to the array of connections. - */ -const insertNewUser = (connections, userId, wsConn) => { - const userConnetions = connections.get(userId); - if (!userConnetions) connections.set(userId, [wsConn]); - else userConnetions.push(wsConn); -}; - -/* - *Here we remove the connection from the connections map. - *If the user is not in the map, we do nothing. - *Else we remove the connection from the array of connections. - *If the array is empty, we delete the user from the map. - */ -const removeConnection = (connections, userId, connToRemove) => { - const userConnetions = connections.get(userId); - if (!userConnetions) return; - - const newConns = userConnetions.filter(conn => conn !== connToRemove); - if (newConns.length === 0) connections.delete(userId); - else connections.set(userId, newConns); -}; - -/* - * Here we broadcast the message to all the connections that are connected to the same user. - * We check if the connection is open before sending the message. - */ -const broadcastToSameUser = (connections, userId, data) => { - const userConnetions = connections.get(userId); - if (!userConnetions) return; - userConnetions.forEach((conn) => { - if (conn.readyState === WebSocket.OPEN) conn.send(data); - }); -}; - -/* - * Here we check if there is another connection to the same user. - * If there is, we return true. - * Else we return false. - */ -const checkOtherConn = (connections, anotherConn, userId) => { - const userConnetions = connections.get(userId); - if (!userConnetions) return false; - for (const con of userConnetions) { - if (con !== anotherConn && con.readyState === WebSocket.OPEN) return true; - } - return false; -}; - -/* -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 clients = new Map(); - const connections = new Map(); - const wss = new WebSocket.Server({ noServer: true, path: "/timer-service", @@ -118,64 +73,66 @@ export default async (expServer) => { }); }); - /* - For each new connection we start a timer of 5min to check if the connection is alive. - If it is, we then repeat the process. If it is not, we terminate the connection. - */ + const clients = new Map(); // { userId: timerInfo } + const connections = new Map(); // { userId: connections[] } + wss.on("connection", async (ws, req) => { ws.isAlive = true; + const { userId } = req; + ws.on("pong", () => { ws.isAlive = true; }); - const { userId } = req; - insertNewUser(connections, userId, ws); - /* + /** * Here we get the timer from memory or from the database and send it to the client. - * We don't broadcast it */ - await getTimer(clients, userId); - ws.send(await handleMessage(action.GET_TIMER, clients, userId)); + const clientTimer = await getClient(clients, userId); + 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. + /** + * 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 resp = await handleMessage(data, clients, userId); broadcastToSameUser(connections, 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. + /** + * 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 (!checkOtherConn(connections, ws, userId)) { - await handleMessage(action.FORCED_PAUSE, clients, userId); + if (!hasOtherConn(connections, userId, ws)) { + const client = clients.get(userId); + if (client.started && !client.paused) { + await handleMessage(action.FORCED_PAUSE, clients, userId); + } } removeConnection(connections, userId, ws); }); }); - // The function to check if the connection is alive - const interval = setInterval(async () => { - wss.clients.forEach(async (ws) => { - if (ws.isAlive === false) return ws.terminate(); - + // For each new connection we start a time interval of 1min to check if the connection is alive. + // change to 1min before push + const interval = setInterval(() => { + wss.clients.forEach((ws) => { + if (ws.isAlive === false) { + return ws.close(); + } ws.isAlive = false; ws.ping(); }); - }, 3000000); + }, 10000); - // Here we just clear the interval when the server closes - wss.on("close", () => { + wss.on('close', () => { clearInterval(interval); }); From 9a7bfb948da6f7b0c2beeb7cf310e3addeaa72b6 Mon Sep 17 00:00:00 2001 From: AriaYu927 Date: Thu, 19 Oct 2023 10:06:48 +0800 Subject: [PATCH 27/37] add validator of regex and authorization --- src/controllers/teamController.js | 11 ++++++++++- src/models/team.js | 13 ++++++++++++- src/models/userProfile.js | 17 ++++++++++++++--- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/controllers/teamController.js b/src/controllers/teamController.js index 5215b024b..b204875a5 100644 --- a/src/controllers/teamController.js +++ b/src/controllers/teamController.js @@ -70,6 +70,15 @@ const teamcontroller = function (Team) { res.status(400).send('No valid records found'); return; } + + const canEditTeamCode = req.body.requestor.role === 'Owner' + || req.body.requestor.permissions?.frontPermissions.includes('editTeamCode'); + + if (!canEditTeamCode) { + res.status(403).send('You are not authorized to edit team code.'); + return; + } + record.teamName = req.body.teamName; record.isActive = req.body.isActive; record.teamCode = req.body.teamCode; @@ -116,7 +125,7 @@ const teamcontroller = function (Team) { users.forEach((element) => { const { userId, operation } = element; // if user's profile is stored in cache, clear it so when you visit their profile page it will be up to date - if(cache.hasCache(`user-${userId}`)) cache.removeCache(`user-${userId}`); + if (cache.hasCache(`user-${userId}`)) cache.removeCache(`user-${userId}`); if (operation === 'Assign') { assignlist.push(userId); diff --git a/src/models/team.js b/src/models/team.js index a57d7bb27..97f8dc360 100644 --- a/src/models/team.js +++ b/src/models/team.js @@ -13,7 +13,18 @@ const team = new Schema({ addDateTime: { type: Date, default: Date.now(), ref: 'userProfile' }, }, ], - teamCode: { type: 'String', default: '' }, + teamCode: { + type: 'String', + default: '', + validate: { + validator(v) { + const teamCoderegex = /^([a-zA-Z]-[a-zA-Z]{3}|[a-zA-Z]{5})$/; + return teamCoderegex.test(v); + }, + message: + 'Please enter a code in the format of A-AAA or AAAAA', + }, + }, }); module.exports = mongoose.model('team', team, 'teams'); diff --git a/src/models/userProfile.js b/src/models/userProfile.js index a58d1d293..4739a05e7 100644 --- a/src/models/userProfile.js +++ b/src/models/userProfile.js @@ -7,7 +7,7 @@ const bcrypt = require('bcryptjs'); const SALT_ROUNDS = 10; const nextDay = new Date(); -nextDay.setDate(nextDay.getDate()+1); +nextDay.setDate(nextDay.getDate() + 1); const userProfileSchema = new Schema({ password: { @@ -153,8 +153,19 @@ const userProfileSchema = new Schema({ isVisible: { type: Boolean, default: false }, weeklySummaryOption: { type: String }, bioPosted: { type: String, default: 'default' }, - isFirstTimelog: { type: Boolean, default: true}, - teamCode: { type: String, default: '' }, + isFirstTimelog: { type: Boolean, default: true }, + teamCode: { + type: String, + default: '', + validate: { + validator(v) { + const teamCoderegex = /^([a-zA-Z]-[a-zA-Z]{3}|[a-zA-Z]{5})$/; + return teamCoderegex.test(v); + }, + message: + 'Please enter a code in the format of A-AAA or AAAAA', + }, + }, infoCollections: [ { areaName: { type: String }, From f376fa8f049717d07eb12cdfd2eab9bcbf250da2 Mon Sep 17 00:00:00 2001 From: AriaYu927 Date: Thu, 19 Oct 2023 22:37:33 +0800 Subject: [PATCH 28/37] add update task status function --- src/controllers/taskController.js | 15 +++++++++++++-- src/routes/taskRouter.js | 3 +++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/controllers/taskController.js b/src/controllers/taskController.js index 7a19fd853..9bcf071de 100644 --- a/src/controllers/taskController.js +++ b/src/controllers/taskController.js @@ -776,7 +776,6 @@ const taskController = function (Task) { }); res.status(200).send(task); - } catch (error) { // Generic error message, you can adjust as needed res.status(500).send({ error: 'Internal Server Error', details: error.message }); @@ -850,6 +849,17 @@ const taskController = function (Task) { } }; + const updateTaskStatus = async (req, res) => { + const { taskId } = req.params; + + Task.findOneAndUpdate( + { _id: mongoose.Types.ObjectId(taskId) }, + { ...req.body, modifiedDatetime: Date.now() }, + ) + .then(() => res.status(201).send()) + .catch(error => res.status(404).send(error)); + }; + const getReviewReqEmailBody = function (name, taskName) { const text = `New Task Review Request From ${name}:

    The following task is available to review:

    @@ -863,7 +873,7 @@ const taskController = function (Task) { const getRecipients = async function (myUserId) { const recipients = []; const user = await userProfile.findById(myUserId); - const membership = await userProfile.find({ role: ['Administrator', 'Manager', 'Mentor'] }); + const membership = await userProfile.find({ role: { $in: ['Administrator', 'Manager', 'Mentor'] } }); membership.forEach((member) => { if (member.teams.some(team => user.teams.includes(team))) { recipients.push(member.email); @@ -909,6 +919,7 @@ const taskController = function (Task) { moveTask, getTasksByUserList, getTasksForTeamsByUser, + updateTaskStatus, sendReviewReq, }; }; diff --git a/src/routes/taskRouter.js b/src/routes/taskRouter.js index ef972f2cb..46e467c9d 100644 --- a/src/routes/taskRouter.js +++ b/src/routes/taskRouter.js @@ -28,6 +28,9 @@ const routes = function (task, userProfile) { wbsRouter.route('/task/update/:taskId') .put(controller.updateTask); + wbsRouter.route('/task/updateStatus/:taskId') + .put(controller.updateTaskStatus); + wbsRouter.route('/task/updateAllParents/:wbsId/') .put(controller.updateAllParents); From 85f6e2dc0847d2644804acc9227a5fcb1b66fe5c Mon Sep 17 00:00:00 2001 From: Aaron Persaud Date: Fri, 20 Oct 2023 10:13:56 -0400 Subject: [PATCH 29/37] update babel/traverse and mongoose to fix vulnerabilities --- package-lock.json | 211 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 194 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index f9eb87ba6..3fcc98986 100644 --- a/package-lock.json +++ b/package-lock.json @@ -318,6 +318,11 @@ "@babel/types": "^7.16.7" } }, + "@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==" + }, "@babel/helper-validator-identifier": { "version": "7.16.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", @@ -1083,20 +1088,131 @@ } }, "@babel/traverse": { - "version": "7.17.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.3.tgz", - "integrity": "sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw==", - "requires": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.3", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.16.7", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.3", - "@babel/types": "^7.17.0", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "requires": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + } + }, + "@babel/generator": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "requires": { + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==" + }, + "@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "requires": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==" + }, + "@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "requires": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==" + }, + "@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + } + }, + "@babel/types": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "requires": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==" + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + } } }, "@babel/types": { @@ -1194,11 +1310,49 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "dependencies": { + "@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + }, + "dependencies": { + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + } + } + } + } + }, "@jridgewell/resolve-uri": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz", "integrity": "sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew==" }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" + }, "@jridgewell/sourcemap-codec": { "version": "1.4.11", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz", @@ -5382,7 +5536,7 @@ "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, "lodash.merge": { "version": "4.6.2", @@ -5666,15 +5820,15 @@ } }, "mongoose": { - "version": "5.13.15", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.13.15.tgz", - "integrity": "sha512-cxp1Gbb8yUWkaEbajdhspSaKzAvsIvOtRlYD87GN/P2QEUhpd6bIvebi36T6M0tIVAMauNaK9SPA055N3PwF8Q==", + "version": "5.13.21", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.13.21.tgz", + "integrity": "sha512-EvSrXrCBogenxY131qKasFcT1Pj+9Pg5AXj17vQ8S1mOEArK3CpOx965u1wTIrdnQ7DjFC+SRwPxNcqUjMAVyQ==", "requires": { "@types/bson": "1.x || 4.0.x", "@types/mongodb": "^3.5.27", "bson": "^1.1.4", "kareem": "2.3.2", - "mongodb": "3.7.3", + "mongodb": "3.7.4", "mongoose-legacy-pluralize": "1.0.2", "mpath": "0.8.4", "mquery": "3.2.5", @@ -5686,6 +5840,29 @@ "sliced": "1.0.1" }, "dependencies": { + "mongodb": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.7.4.tgz", + "integrity": "sha512-K5q8aBqEXMwWdVNh94UQTwZ6BejVbFhh1uB6c5FKtPE9eUMZPUO3sRZdgIEcHSrAWmxzpG/FeODDKL388sqRmw==", + "requires": { + "bl": "^2.2.1", + "bson": "^1.1.4", + "denque": "^1.4.1", + "optional-require": "^1.1.8", + "safe-buffer": "^5.1.2", + "saslprep": "^1.0.0" + }, + "dependencies": { + "optional-require": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.1.8.tgz", + "integrity": "sha512-jq83qaUb0wNg9Krv1c5OQ+58EK+vHde6aBPzLvPPqJm89UQWsvSuFy9X/OSNJnFeSOKo7btE0n8Nl2+nE+z5nA==", + "requires": { + "require-at": "^1.0.6" + } + } + } + }, "optional-require": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.0.3.tgz", From 36d6d23ceee94b95e1d5cc363457f7fd36922a6a Mon Sep 17 00:00:00 2001 From: wang9hu Date: Mon, 23 Oct 2023 20:06:25 -0700 Subject: [PATCH 30/37] make requested changes from last reviews --- src/websockets/TimerService/clientsHandler.js | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/src/websockets/TimerService/clientsHandler.js b/src/websockets/TimerService/clientsHandler.js index fdf21b8f4..6990ead71 100644 --- a/src/websockets/TimerService/clientsHandler.js +++ b/src/websockets/TimerService/clientsHandler.js @@ -29,7 +29,7 @@ export const getClient = async (clients, userId) => { return clients.get(userId); }; -/* +/** * Save client info to database * Save under these conditions: * connection is normally closed (paused and closed); @@ -47,7 +47,7 @@ export const saveClient = async (client) => { } }; -/* +/** * This is the contract between client and server. * The client can send one of the following messages to the server: */ @@ -63,6 +63,9 @@ export const action = { ACK_FORCED: 'ACK_FORCED', }; +const MAX_HOURS = 5; +const MIN_MINS = 1; + const updatedTimeSinceStart = (client) => { if (!client.started) return client.goal; const now = moment.utc(); @@ -127,26 +130,20 @@ const stopTimer = (client) => { */ const clearTimer = (client) => { stopTimer(client); + client.goal = moment.duration(2, 'hours').asMilliseconds(); client.time = client.goal; }; - -// /* -// Here we switch the timer mode. -// We pause the timer and check it's mode to set the time to 0 or the goal. -// */ -// const switchMode = (client) => { -// client.countdown = !client.countdown; -// client.time = client.countdown ? client.goal : 0; -// client.paused = true; -// }; - -// Here we get the goal time from the message. -const getGoal = msg => parseInt(msg.split('=')[1]); - // Here we set the goal and time to the goal time. +/** + * Here we set the goal. + * if timer has not started, we set both time and goal to the new goal + * if timer has started, we calculate the passed time and remove that from new goal + * and if passed time is greater than new goal, then set time to 0, but this should + * not be prohibited by frontend. + */ const setGoal = (client, msg) => { - const newGoal = getGoal(msg); + const newGoal = parseInt(msg.split('=')[1]); if (!client.started) { client.goal = newGoal; client.time = newGoal; @@ -170,13 +167,13 @@ const setGoal = (client, msg) => { * We also add it to the current time and set it as the new time. */ const addGoal = (client, msg) => { - const duration = getGoal(msg); + const duration = parseInt(msg.split('=')[1]); const goalAfterAddition = moment .duration(client.goal) .add(duration, 'milliseconds') .asHours(); - if (goalAfterAddition > 10) return; + if (goalAfterAddition > MAX_HOURS) return; client.goal = moment .duration(client.goal) @@ -199,7 +196,7 @@ const addGoal = (client, msg) => { * If the new time is less than 0, we set it to 0. */ const removeGoal = (client, msg) => { - const duration = getGoal(msg); + const duration = parseInt(msg.split('=')[1]); const goalAfterRemoval = moment .duration(client.goal) .subtract(duration, 'milliseconds') @@ -209,7 +206,7 @@ const removeGoal = (client, msg) => { .subtract(duration, 'milliseconds') .asMinutes(); - if (goalAfterRemoval < 15 || timeAfterRemoval < 0) return; + if (goalAfterRemoval < MIN_MINS || timeAfterRemoval < 0) return; client.goal = moment .duration(client.goal) @@ -279,7 +276,7 @@ export const handleMessage = async (msg, clients, userId) => { break; } - await saveClient(client); + saveClient(client); clients.set(userId, client); if (resp === null) resp = client; return JSON.stringify(resp); From 701be2501cf6f82da6eb90723c1d4fc55f5a6eed Mon Sep 17 00:00:00 2001 From: wang9hu Date: Wed, 25 Oct 2023 17:19:35 -0700 Subject: [PATCH 31/37] cleanup old timer related code and files --- src/controllers/REAL_TIME_timerController.js | 155 ------------------- src/models/REAL_TIME_timer.js | 13 -- src/models/oldTimer.js | 14 -- src/routes/timerRouter.js | 15 -- src/startup/routes.js | 3 - 5 files changed, 200 deletions(-) delete mode 100644 src/controllers/REAL_TIME_timerController.js delete mode 100644 src/models/REAL_TIME_timer.js delete mode 100644 src/models/oldTimer.js delete mode 100644 src/routes/timerRouter.js diff --git a/src/controllers/REAL_TIME_timerController.js b/src/controllers/REAL_TIME_timerController.js deleted file mode 100644 index 699dcddef..000000000 --- a/src/controllers/REAL_TIME_timerController.js +++ /dev/null @@ -1,155 +0,0 @@ - -const logger = require('../startup/logger'); -const OldTimer = require('../models/oldTimer'); - -const timerController = function (Timer) { - const getTimerFromDatabase = async ({ userId }) => { - try { - const timerObject = await Timer.findOne({ userId }).exec(); - if (!timerObject) { - const newRecord = { - userId, - totalSeconds: 0, - isRunning: false, - isApplicationPaused: false, - isUserPaused: false, - }; - const newTimer = await Timer.create(newRecord); - return newTimer; - } - return timerObject; - } catch (e) { - logger.logException(e); - throw new Error('Issue trying to retrieve timer data from MongoDB'); - } - }; - - const setTimerToDatabase = async ({ - userId, - timerObject: { - totalSeconds, - isRunning, - isUserPaused, - isApplicationPaused, - } = {}, - } = {}) => { - try { - const update = { - $set: { - totalSeconds, - isRunning, - isUserPaused, - isApplicationPaused, - }, - }; - - const options = { - upsert: true, - new: true, - setDefaultsOnInsert: true, - rawResult: true, - }; - - return await Timer.findOneAndUpdate({ userId }, update, options).exec(); - } catch (e) { - logger.logException(e); - throw new Error('Issue trying to set timer data from MongoDB'); - } - }; - - const putTimer = function (req, res) { - const { userId } = req.params; - - const query = { userId }; - const update = { - $set: { - pausedAt: req.body.pausedAt, - isWorking: req.body.isWorking, - started: req.body.isWorking ? Date.now() : null, - lastAccess: Date.now(), - }, - }; - const options = { - upsert: true, new: true, setDefaultsOnInsert: true, rawResult: true, - }; - - OldTimer.findOneAndUpdate(query, update, options, (error, rawResult) => { - if (error) { - return res.status(500).send({ error }); - } - - if (rawResult === null || rawResult.value === undefined || rawResult.value === null - || rawResult.lastErrorObject === null || rawResult.lastErrorObject === undefined - || rawResult.value.length === 0) { - return res.status(500).send('Update/Upsert timer date failed'); - } - - if (rawResult.lastErrorObject.updatedExisting === true) { - return res.status(200).send({ message: 'updated timer data' }); - } - if (rawResult.lastErrorObject.updatedExisting === false - && rawResult.lastErrorObject.upserted !== undefined && rawResult.lastErrorObject.upserted !== null) { - return res.status(201).send({ _id: rawResult.lastErrorObject.upserted }); - } - return res.status(500).send('Update/Upsert timer date failed'); - }); - }; - - const timePassed = (timer) => { - if (!timer.started) { return 0; } - const now = timer.timedOut ? timer.lastAccess : Date.now(); - return Math.floor((now - timer.started) / 1000); - }; - - const adjust = (timer, cb) => { - const oneMin = 60 * 1000; - const fiveMin = 5 * oneMin; - const timeSinceLastAccess = timer.lastAccess ? (Date.now() - timer.lastAccess) : 0; - const setLastAccess = !timer.lastAccess || (timeSinceLastAccess > oneMin); - - timer.timedOut = timer.isWorking && (timeSinceLastAccess > fiveMin); - timer.seconds = timer.pausedAt + timePassed(timer); - - if (timer.timedOut) { - return OldTimer.findOneAndUpdate({ userId: timer.userId }, { - isWorking: false, - pauseAt: timer.seconds, - started: null, - lastAccess: Date.now(), - }).then(() => cb(timer)); - } - if (setLastAccess) { - return OldTimer.findOneAndUpdate({ userId: timer.userId }, { lastAccess: Date.now() }).then(() => cb(timer)); - } - - return cb(timer); - }; - - const getTimer = function (req, res) { - const { userId } = req.params; - - OldTimer.findOne({ userId }).lean().exec((error, record) => { - if (error) { - return res.status(500).send(error); - } - if (record === null) { - if (req.body.requestor.requestorId === userId) { - const newRecord = { - userId, - pausedAt: 0, - isWorking: false, - }; - return OldTimer.create(newRecord).then(result => res.status(200).send(result)).catch(() => res.status(400).send('Timer record not found for the given user ID')); - } - return res.status(400).send('Timer record not found for the given user ID'); - } - return adjust(record, (timer) => { res.status(200).send(timer); }); - }); - }; - - return { - putTimer, getTimer, getTimerFromDatabase, setTimerToDatabase, - }; -}; - -module.exports = timerController; diff --git a/src/models/REAL_TIME_timer.js b/src/models/REAL_TIME_timer.js deleted file mode 100644 index 4e143aeaa..000000000 --- a/src/models/REAL_TIME_timer.js +++ /dev/null @@ -1,13 +0,0 @@ -const mongoose = require('mongoose'); - -const { Schema } = mongoose; - -const timerSchema = new Schema({ - userId: { type: Schema.Types.ObjectId, required: true, ref: 'userProfile' }, - totalSeconds: { type: Number, default: 0 }, - isRunning: { type: Boolean, default: false }, - isUserPaused: { type: Boolean, default: false }, - isApplicationPaused: { type: Boolean, default: false }, -}); - -module.exports = mongoose.model('newTimer', timerSchema, 'newTimers'); diff --git a/src/models/oldTimer.js b/src/models/oldTimer.js deleted file mode 100644 index dca0ade1a..000000000 --- a/src/models/oldTimer.js +++ /dev/null @@ -1,14 +0,0 @@ -const mongoose = require('mongoose'); - -const { Schema } = mongoose; - -const timerSchema = new Schema({ - userId: { type: Schema.Types.ObjectId, required: true, ref: 'userProfile' }, - pausedAt: { type: Number, default: 0 }, - isWorking: { type: Boolean, default: false }, - started: { type: Date }, - lastAccess: { type: Date }, - }); - - -module.exports = mongoose.model('timer', timerSchema, 'timers'); diff --git a/src/routes/timerRouter.js b/src/routes/timerRouter.js deleted file mode 100644 index 094b2ba81..000000000 --- a/src/routes/timerRouter.js +++ /dev/null @@ -1,15 +0,0 @@ -const express = require('express'); - -const routes = function (Timer) { - const TimerRouter = express.Router(); - - const controller = require('../controllers/REAL_TIME_timerController')(Timer); - - TimerRouter.route('/timer/:userId') - .put(controller.putTimer) - .get(controller.getTimer); - - return TimerRouter; -}; - -module.exports = routes; diff --git a/src/startup/routes.js b/src/startup/routes.js index 2fd7337a6..eae746e24 100644 --- a/src/startup/routes.js +++ b/src/startup/routes.js @@ -7,7 +7,6 @@ const actionItem = require('../models/actionItem'); const notification = require('../models/notification'); const wbs = require('../models/wbs'); const task = require('../models/task'); -const timer = require('../models/timer'); const popup = require('../models/popupEditor'); const popupBackup = require('../models/popupEditorBackup'); const taskNotification = require('../models/taskNotification'); @@ -38,7 +37,6 @@ const forcePwdRouter = require('../routes/forcePwdRouter')(userProfile); const reportsRouter = require('../routes/reportsRouter')(); const wbsRouter = require('../routes/wbsRouter')(wbs); const taskRouter = require('../routes/taskRouter')(task); -const timerRouter = require('../routes/timerRouter')(timer); const popupRouter = require('../routes/popupEditorRouter')(popup); const popupBackupRouter = require('../routes/popupEditorBackupRouter')(popupBackup); const taskNotificationRouter = require('../routes/taskNotificationRouter')(taskNotification); @@ -76,7 +74,6 @@ module.exports = function (app) { app.use('/api', reportsRouter); app.use('/api', wbsRouter); app.use('/api', taskRouter); - app.use('/api', timerRouter); app.use('/api', popupRouter); app.use('/api', popupBackupRouter); app.use('/api', taskNotificationRouter); From 467cbb84140103fcdd0ad97c728584d1d6156229 Mon Sep 17 00:00:00 2001 From: wang9hu Date: Wed, 25 Oct 2023 18:31:29 -0700 Subject: [PATCH 32/37] remove 2h default time when clear timer --- src/websockets/TimerService/clientsHandler.js | 77 +------------------ 1 file changed, 1 insertion(+), 76 deletions(-) diff --git a/src/websockets/TimerService/clientsHandler.js b/src/websockets/TimerService/clientsHandler.js index 6990ead71..e9e932fd7 100644 --- a/src/websockets/TimerService/clientsHandler.js +++ b/src/websockets/TimerService/clientsHandler.js @@ -4,13 +4,6 @@ const moment = require('moment'); const Timer = require('../../models/timer'); const logger = require('../../startup/logger'); -/** - * Here we get the timer. - * If the timer already exists in memory, we return it. - * If it doesn't exist, we try to get it from MongoDB. - * If it doesn't exist in MongoDB, we create it and save it to MongoDB. - * Then we save it to memory and return it. - */ export const getClient = async (clients, userId) => { // In case of there is already a connection that is open for this user // for example user open a new connection @@ -29,13 +22,6 @@ export const getClient = async (clients, userId) => { return clients.get(userId); }; -/** - * Save client info to database - * Save under these conditions: - * connection is normally closed (paused and closed); - * connection is forced-paused (timer still on and connection closed) - * message: STOP_TIMER - */ export const saveClient = async (client) => { try { await Timer.findOneAndUpdate({ userId: client.userId }, client); @@ -47,10 +33,6 @@ export const saveClient = async (client) => { } }; -/** - * This is the contract between client and server. - * The client can send one of the following messages to the server: - */ export const action = { START_TIMER: 'START_TIMER', PAUSE_TIMER: 'PAUSE_TIMER', @@ -75,12 +57,6 @@ const updatedTimeSinceStart = (client) => { return updatedTime > 0 ? updatedTime : 0; }; -/** - * Here we start the timer, if it is not already started. - * We set the last access time to now, and set the paused and stopped flags to false. - * If the timer was paused, we need to check if it was paused by the user or by the server. - * If it was paused by the server, we need to set the forcedPause flag to true. - */ const startTimer = (client) => { client.startAt = moment.utc(); client.paused = false; @@ -91,31 +67,19 @@ const startTimer = (client) => { if (client.forcedPause) client.forcedPause = false; }; -/** - * Here we pause the timer, if it is not already paused. - * We get the total elapsed time since the last access, and set it as the new time. - * We set the last access time to now, and set the paused flag to true. - * If the timer was paused by the server, we need to set the forcedPause flag to true. - * It'll only be triggered when the user closes the connection sudenlly or lacks of ACKs. - */ const pauseTimer = (client, forced = false) => { client.time = updatedTimeSinceStart(client); - client.startAt = moment.invalid(); + client.startAt = moment.invalid(); // invalid can not be saved in database client.paused = true; if (forced) client.forcedPause = true; }; -// Here we acknowledge the forced pause. To prevent the modal for beeing displayed again. const ackForcedPause = (client) => { client.forcedPause = false; client.paused = true; client.startAt = moment.invalid(); }; -/** - * Here we stop the timer. - * We pause the timer and set the stopped flag to true. - */ const stopTimer = (client) => { client.startAt = moment.invalid(); client.started = false; @@ -123,25 +87,11 @@ const stopTimer = (client) => { client.forcedPause = false; }; -/** - * Here we clear the timer. - * We pause the timer and check it's mode to set the time to 0 or the goal. - * Then we set the stopped flag to false. - */ const clearTimer = (client) => { stopTimer(client); - client.goal = moment.duration(2, 'hours').asMilliseconds(); client.time = client.goal; }; -// Here we set the goal and time to the goal time. -/** - * Here we set the goal. - * if timer has not started, we set both time and goal to the new goal - * if timer has started, we calculate the passed time and remove that from new goal - * and if passed time is greater than new goal, then set time to 0, but this should - * not be prohibited by frontend. - */ const setGoal = (client, msg) => { const newGoal = parseInt(msg.split('=')[1]); if (!client.started) { @@ -159,13 +109,6 @@ const setGoal = (client, msg) => { } }; -/** - * Here we add the goal time. - * Each addition add 15min - * First we get the goal time from the message. - * Then we add it to the current goal time and set it as the new goal time. - * We also add it to the current time and set it as the new time. - */ const addGoal = (client, msg) => { const duration = parseInt(msg.split('=')[1]); const goalAfterAddition = moment @@ -187,14 +130,6 @@ const addGoal = (client, msg) => { .toFixed(); }; -/** - * Here we try to remove a goal time. - * First we get the goal time from the message. - * Then we subtract it from the current goal time and set it as the new goal time. - * We also subtract it from the current time and set it as the new time. - * If the new goal time is less than 15 minutes, we don't do anything. - * If the new time is less than 0, we set it to 0. - */ const removeGoal = (client, msg) => { const duration = parseInt(msg.split('=')[1]); const goalAfterRemoval = moment @@ -220,16 +155,6 @@ const removeGoal = (client, msg) => { .toFixed(); }; - -/** - * Here is were we handle the messages. - * First we check if the user is in memory, if not, we throw an error. - * Then we parse the request and check which action it is and call the corresponding function. - * If we don't have a match, we just return an error. - * The only operation that we write to Mongo it's the stop timer. Other operations are just in memory. - * So the slowest part of the app is the save to Mongo. - * Then we update the current client in hash map and return the response. - */ export const handleMessage = async (msg, clients, userId) => { if (!clients.has(userId)) { throw new Error('It should have this user in memory'); From 29e9bb2c3fddcf4d1a47607691c0ba2e0a4d0d12 Mon Sep 17 00:00:00 2001 From: wang9hu Date: Wed, 25 Oct 2023 23:36:47 -0700 Subject: [PATCH 33/37] make timer keep remaining time as goal after log time, and add initial goal in timer modal --- src/models/timer.js | 1 + src/websockets/TimerService/clientsHandler.js | 24 +++++++++---------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/models/timer.js b/src/models/timer.js index 8afbad119..09c2f89da 100644 --- a/src/models/timer.js +++ b/src/models/timer.js @@ -8,6 +8,7 @@ const timerSchema = new Schema({ startAt: { type: Date, default: Date.now }, time: { type: Number, default: 900000 }, goal: { type: Number, default: 900000 }, + initialGoal: { type: Number, default: 900000 }, paused: { type: Boolean, default: false }, forcedPause: { type: Boolean, default: false }, started: { type: Boolean, default: false }, diff --git a/src/websockets/TimerService/clientsHandler.js b/src/websockets/TimerService/clientsHandler.js index e9e932fd7..3bb2c358d 100644 --- a/src/websockets/TimerService/clientsHandler.js +++ b/src/websockets/TimerService/clientsHandler.js @@ -81,32 +81,30 @@ const ackForcedPause = (client) => { }; const stopTimer = (client) => { + if (client.started) pauseTimer(client); client.startAt = moment.invalid(); client.started = false; client.pause = false; client.forcedPause = false; + if (client.time === 0) { + client.goal = client.initialGoal; + client.time = client.goal; + } else { + client.goal = client.time; + } }; const clearTimer = (client) => { stopTimer(client); + client.goal = client.initialGoal; client.time = client.goal; }; const setGoal = (client, msg) => { const newGoal = parseInt(msg.split('=')[1]); - if (!client.started) { - client.goal = newGoal; - client.time = newGoal; - } else { - const passedTime = client.goal - client.time; - if (passedTime >= newGoal) { - client.time = 0; - client.goal = passedTime; - } else { - client.time = newGoal - passedTime; - client.goal = newGoal; - } - } + client.goal = newGoal; + client.time = newGoal; + client.initialGoal = newGoal; }; const addGoal = (client, msg) => { From bd8f4de8a7ff455500408c608a32f81ddb4c79a3 Mon Sep 17 00:00:00 2001 From: wang9hu Date: Sun, 29 Oct 2023 14:49:00 -0700 Subject: [PATCH 34/37] make timer chime consistent over all tabs --- src/models/timer.js | 1 + src/websockets/TimerService/clientsHandler.js | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/src/models/timer.js b/src/models/timer.js index 09c2f89da..f50921fb3 100644 --- a/src/models/timer.js +++ b/src/models/timer.js @@ -9,6 +9,7 @@ const timerSchema = new Schema({ time: { type: Number, default: 900000 }, goal: { type: Number, default: 900000 }, initialGoal: { type: Number, default: 900000 }, + chiming: { type: Boolean, default: false }, paused: { type: Boolean, default: false }, forcedPause: { type: Boolean, default: false }, started: { type: Boolean, default: false }, diff --git a/src/websockets/TimerService/clientsHandler.js b/src/websockets/TimerService/clientsHandler.js index 3bb2c358d..60eb33fa4 100644 --- a/src/websockets/TimerService/clientsHandler.js +++ b/src/websockets/TimerService/clientsHandler.js @@ -43,6 +43,7 @@ export const action = { REMOVE_GOAL: 'REMOVE_FROM_GOAL=', FORCED_PAUSE: 'FORCED_PAUSE', ACK_FORCED: 'ACK_FORCED', + START_CHIME: 'START_CHIME', }; const MAX_HOURS = 5; @@ -69,11 +70,17 @@ const startTimer = (client) => { const pauseTimer = (client, forced = false) => { client.time = updatedTimeSinceStart(client); + if (client.time === 0) client.chiming = true; client.startAt = moment.invalid(); // invalid can not be saved in database client.paused = true; if (forced) client.forcedPause = true; }; +const startChime = (client, msg) => { + const state = msg.split('=')[1]; + client.chiming = state === 'true'; +}; + const ackForcedPause = (client) => { client.forcedPause = false; client.paused = true; @@ -86,6 +93,7 @@ const stopTimer = (client) => { client.started = false; client.pause = false; client.forcedPause = false; + if (client.chiming) client.chiming = false; if (client.time === 0) { client.goal = client.initialGoal; client.time = client.goal; @@ -97,6 +105,7 @@ const stopTimer = (client) => { const clearTimer = (client) => { stopTimer(client); client.goal = client.initialGoal; + client.chiming = false; client.time = client.goal; }; @@ -175,6 +184,9 @@ export const handleMessage = async (msg, clients, userId) => { case req.match(/REMOVE_FROM_GOAL=/i)?.input: removeGoal(client, req); break; + case req.match(/START_CHIME=/i)?.input: + startChime(client, req); + break; case action.PAUSE_TIMER: pauseTimer(client); break; From 64f6c4121012e4e10d01568e847a6172562a2512 Mon Sep 17 00:00:00 2001 From: AriaYu927 Date: Tue, 31 Oct 2023 16:04:22 +0800 Subject: [PATCH 35/37] set default value for createdDatetime --- src/models/team.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/team.js b/src/models/team.js index 97f8dc360..00fbaf8e3 100644 --- a/src/models/team.js +++ b/src/models/team.js @@ -5,7 +5,7 @@ const { Schema } = mongoose; const team = new Schema({ teamName: { type: 'String', required: true }, isActive: { type: 'Boolean', required: true, default: true }, - createdDatetime: { type: Date }, + createdDatetime: { type: Date, default: Date.now() }, modifiedDatetime: { type: Date, default: Date.now() }, members: [ { From 4e318bc4046fb42324a9b12b0adc5c1522208f5c Mon Sep 17 00:00:00 2001 From: wang9hu Date: Sat, 4 Nov 2023 17:01:10 -0700 Subject: [PATCH 36/37] add empty string in teamCode validator for creating new user --- src/models/userProfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/userProfile.js b/src/models/userProfile.js index 4739a05e7..3219fec18 100644 --- a/src/models/userProfile.js +++ b/src/models/userProfile.js @@ -159,7 +159,7 @@ const userProfileSchema = new Schema({ default: '', validate: { validator(v) { - const teamCoderegex = /^([a-zA-Z]-[a-zA-Z]{3}|[a-zA-Z]{5})$/; + const teamCoderegex = /^([a-zA-Z]-[a-zA-Z]{3}|[a-zA-Z]{5})$|^$/; return teamCoderegex.test(v); }, message: From dbfc6bef60dbf37e0f9f8da70b55ee947fec4a73 Mon Sep 17 00:00:00 2001 From: wang9hu Date: Tue, 7 Nov 2023 23:05:08 -0800 Subject: [PATCH 37/37] make sure escapeRegex check entire string for existing info name --- src/utilities/escapeRegex.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utilities/escapeRegex.js b/src/utilities/escapeRegex.js index 10fa2e61e..01a65ea50 100644 --- a/src/utilities/escapeRegex.js +++ b/src/utilities/escapeRegex.js @@ -1,6 +1,6 @@ const escapeRegex = function (text) { - return text.replace(/[[\]{}()*+?.\\^$|]/g, '\\$&'); + return `^${text.replace(/[[\]{}()*+?.\\^$|]/g, '\\$&')}&`; }; module.exports = escapeRegex;