From 6ae3ce2922b23e793d7ccedf436b0aae22145ab6 Mon Sep 17 00:00:00 2001 From: Pierre Theo Klein Date: Sun, 26 Jul 2020 17:36:21 -0400 Subject: [PATCH 1/3] feat: fix non-determinism of npm run seed (Bugfix/651) (#652) * fix: hasOwnProperty vulnerability (see https://eslint.org/docs/rules/no-prototype-builtins) * feat: _id value to all route objects, starting at 100 and moving upwards. Add warning at top of file to maintain naming standard. * feat: update role.constant.js to use the new _id attribute so that ids are deterministic between two runs of npm run seed. * feat: update architecture documentation --- constants/role.constant.js | 13 +- constants/routes.constant.js | 224 ++++++++++++++++++++++++----------- docs/architecture.md | 49 +++++++- 3 files changed, 207 insertions(+), 79 deletions(-) diff --git a/constants/role.constant.js b/constants/role.constant.js index 9aa1a707..dfee5c3e 100644 --- a/constants/role.constant.js +++ b/constants/role.constant.js @@ -151,27 +151,26 @@ function createAllSingularRoles() { const allRoutes = Constants.Routes.allRoutes; let roles = []; - // i is unique integer so that objectId is constant - var i = 1000000; for (let routeGroupKey in allRoutes) { - if (!allRoutes.hasOwnProperty(routeGroupKey)) { + if (!Object.prototype.hasOwnProperty.call(allRoutes, routeGroupKey)) { continue; } const routeGroup = allRoutes[routeGroupKey]; for (let routeKey in routeGroup) { - if (!routeGroup.hasOwnProperty(routeKey)) { + // Iterating through the attributes in the routeGroup object + if (!Object.prototype.hasOwnProperty.call(routeGroup, routeKey)) { + // Avoid all prototype attributes continue; } let role = { - _id: mongoose.Types.ObjectId(i), + _id: routeGroup[routeKey]._id, name: routeKey + routeGroupKey, routes: routeGroup[routeKey] }; let roleName = role.name; roles[roleName] = role; - i -= 1; } } @@ -197,7 +196,7 @@ function createAllRoles() { const singularRoles = createAllSingularRoles(); for (let role in singularRoles) { - if (!singularRoles.hasOwnProperty(role)) { + if (!Object.prototype.hasOwnProperty.call(singularRoles, role)) { continue; } allRolesObject[role] = singularRoles[role]; diff --git a/constants/routes.constant.js b/constants/routes.constant.js index e9c21ed0..9a0dda56 100644 --- a/constants/routes.constant.js +++ b/constants/routes.constant.js @@ -1,296 +1,376 @@ "use strict"; + +/** + * ===***===***===***===***===***===***===***===***=== + * ===***=== PLEASE READ BEFORE EDITING ===***=== + * ===***===***===***===***===***===***===***===***=== + * + * If you are adding a route to this list, update this number + * next avaiable createFromTime value: 165 + * + * If you are deleting a route from this list, please add the ID to the list of 'reserved' IDs, + * so that we don't accidentally assign someone to a given ID. + * reserved createFromTime values: + */ + const Constants = require("./general.constant"); +const mongoose = require("mongoose"); const authRoutes = { login: { requestType: Constants.REQUEST_TYPES.POST, - uri: "/api/auth/login" + uri: "/api/auth/login", + _id: mongoose.Types.ObjectId.createFromTime(100) }, logout: { requestType: Constants.REQUEST_TYPES.POST, - uri: "/api/auth/logout" + uri: "/api/auth/logout", + _id: mongoose.Types.ObjectId.createFromTime(101) }, getSelfRoleBindindings: { requestType: Constants.REQUEST_TYPES.GET, - uri: "/api/auth/rolebindings/" + Constants.ROLE_CATEGORIES.SELF + uri: "/api/auth/rolebindings/" + Constants.ROLE_CATEGORIES.SELF, + _id: mongoose.Types.ObjectId.createFromTime(102) }, getAnyRoleBindings: { requestType: Constants.REQUEST_TYPES.GET, - uri: "/api/auth/rolebindings/" + Constants.ROLE_CATEGORIES.ALL + uri: "/api/auth/rolebindings/" + Constants.ROLE_CATEGORIES.ALL, + _id: mongoose.Types.ObjectId.createFromTime(103) }, changePassword: { requestType: Constants.REQUEST_TYPES.PATCH, - uri: "/api/auth/password/change" + uri: "/api/auth/password/change", + _id: mongoose.Types.ObjectId.createFromTime(104) } }; const accountRoutes = { getSelf: { requestType: Constants.REQUEST_TYPES.GET, - uri: "/api/account/self" + uri: "/api/account/self", + _id: mongoose.Types.ObjectId.createFromTime(105) }, getSelfById: { requestType: Constants.REQUEST_TYPES.GET, - uri: "/api/account/" + Constants.ROLE_CATEGORIES.SELF + uri: "/api/account/" + Constants.ROLE_CATEGORIES.SELF, + _id: mongoose.Types.ObjectId.createFromTime(106) }, getAnyById: { requestType: Constants.REQUEST_TYPES.GET, - uri: "/api/account/" + Constants.ROLE_CATEGORIES.ALL + uri: "/api/account/" + Constants.ROLE_CATEGORIES.ALL, + _id: mongoose.Types.ObjectId.createFromTime(107) }, post: { requestType: Constants.REQUEST_TYPES.POST, - uri: "/api/account/" + uri: "/api/account/", + _id: mongoose.Types.ObjectId.createFromTime(108) }, patchSelfById: { requestType: Constants.REQUEST_TYPES.PATCH, - uri: "/api/account/" + Constants.ROLE_CATEGORIES.SELF + uri: "/api/account/" + Constants.ROLE_CATEGORIES.SELF, + _id: mongoose.Types.ObjectId.createFromTime(109) }, patchAnyById: { requestType: Constants.REQUEST_TYPES.PATCH, - uri: "/api/account/" + Constants.ROLE_CATEGORIES.ALL + uri: "/api/account/" + Constants.ROLE_CATEGORIES.ALL, + _id: mongoose.Types.ObjectId.createFromTime(110) }, inviteAccount: { requestType: Constants.REQUEST_TYPES.POST, - uri: "/api/account/invite" + uri: "/api/account/invite", + _id: mongoose.Types.ObjectId.createFromTime(111) } }; const hackerRoutes = { getSelf: { requestType: Constants.REQUEST_TYPES.GET, - uri: "/api/hacker/self/" + uri: "/api/hacker/self/", + _id: mongoose.Types.ObjectId.createFromTime(112) }, getSelfById: { requestType: Constants.REQUEST_TYPES.GET, - uri: "/api/hacker/" + Constants.ROLE_CATEGORIES.SELF + uri: "/api/hacker/" + Constants.ROLE_CATEGORIES.SELF, + _id: mongoose.Types.ObjectId.createFromTime(113) }, getAnyById: { requestType: Constants.REQUEST_TYPES.GET, - uri: "/api/hacker/" + Constants.ROLE_CATEGORIES.ALL + uri: "/api/hacker/" + Constants.ROLE_CATEGORIES.ALL, + _id: mongoose.Types.ObjectId.createFromTime(114) }, getSelfByEmail: { requestType: Constants.REQUEST_TYPES.GET, - uri: "/api/hacker/email/" + Constants.ROLE_CATEGORIES.SELF + uri: "/api/hacker/email/" + Constants.ROLE_CATEGORIES.SELF, + _id: mongoose.Types.ObjectId.createFromTime(115) }, getAnyByEmail: { requestType: Constants.REQUEST_TYPES.GET, - uri: "/api/hacker/email/" + Constants.ROLE_CATEGORIES.ALL + uri: "/api/hacker/email/" + Constants.ROLE_CATEGORIES.ALL, + _id: mongoose.Types.ObjectId.createFromTime(116) }, getSelfResumeById: { requestType: Constants.REQUEST_TYPES.GET, - uri: "/api/hacker/resume/" + Constants.ROLE_CATEGORIES.SELF + uri: "/api/hacker/resume/" + Constants.ROLE_CATEGORIES.SELF, + _id: mongoose.Types.ObjectId.createFromTime(117) }, getAnyResumeById: { requestType: Constants.REQUEST_TYPES.GET, - uri: "/api/hacker/resume/" + Constants.ROLE_CATEGORIES.ALL + uri: "/api/hacker/resume/" + Constants.ROLE_CATEGORIES.ALL, + _id: mongoose.Types.ObjectId.createFromTime(118) }, post: { requestType: Constants.REQUEST_TYPES.POST, - uri: "/api/hacker/" + uri: "/api/hacker/", + _id: mongoose.Types.ObjectId.createFromTime(119) }, postSelfResumeById: { requestType: Constants.REQUEST_TYPES.POST, - uri: "/api/hacker/resume/" + Constants.ROLE_CATEGORIES.SELF + uri: "/api/hacker/resume/" + Constants.ROLE_CATEGORIES.SELF, + _id: mongoose.Types.ObjectId.createFromTime(120) }, postAnyResumeById: { requestType: Constants.REQUEST_TYPES.POST, - uri: "/api/hacker/resume/" + Constants.ROLE_CATEGORIES.ALL + uri: "/api/hacker/resume/" + Constants.ROLE_CATEGORIES.ALL, + _id: mongoose.Types.ObjectId.createFromTime(121) }, patchSelfById: { requestType: Constants.REQUEST_TYPES.PATCH, - uri: "/api/hacker/" + Constants.ROLE_CATEGORIES.SELF + uri: "/api/hacker/" + Constants.ROLE_CATEGORIES.SELF, + _id: mongoose.Types.ObjectId.createFromTime(122) }, patchAnyById: { requestType: Constants.REQUEST_TYPES.PATCH, - uri: "/api/hacker/" + Constants.ROLE_CATEGORIES.ALL + uri: "/api/hacker/" + Constants.ROLE_CATEGORIES.ALL, + _id: mongoose.Types.ObjectId.createFromTime(123) }, patchAnyStatusById: { requestType: Constants.REQUEST_TYPES.PATCH, - uri: "/api/hacker/status/" + Constants.ROLE_CATEGORIES.ALL + uri: "/api/hacker/status/" + Constants.ROLE_CATEGORIES.ALL, + _id: mongoose.Types.ObjectId.createFromTime(124) }, patchSelfStatusById: { requestType: Constants.REQUEST_TYPES.PATCH, - uri: "/api/hacker/status/" + Constants.ROLE_CATEGORIES.SELF + uri: "/api/hacker/status/" + Constants.ROLE_CATEGORIES.SELF, + _id: mongoose.Types.ObjectId.createFromTime(125) }, patchSelfCheckInById: { requestType: Constants.REQUEST_TYPES.PATCH, - uri: "/api/hacker/checkin/" + Constants.ROLE_CATEGORIES.SELF + uri: "/api/hacker/checkin/" + Constants.ROLE_CATEGORIES.SELF, + _id: mongoose.Types.ObjectId.createFromTime(126) }, patchAnyCheckInById: { requestType: Constants.REQUEST_TYPES.PATCH, - uri: "/api/hacker/checkin/" + Constants.ROLE_CATEGORIES.ALL + uri: "/api/hacker/checkin/" + Constants.ROLE_CATEGORIES.ALL, + _id: mongoose.Types.ObjectId.createFromTime(127) }, patchSelfConfirmationById: { requestType: Constants.REQUEST_TYPES.PATCH, - uri: "/api/hacker/confirmation/" + Constants.ROLE_CATEGORIES.SELF + uri: "/api/hacker/confirmation/" + Constants.ROLE_CATEGORIES.SELF, + _id: mongoose.Types.ObjectId.createFromTime(128) }, patchAcceptHackerById: { requestType: Constants.REQUEST_TYPES.PATCH, uri: "/api/hacker/accept/" + Constants.ROLE_CATEGORIES.ALL, + _id: mongoose.Types.ObjectId.createFromTime(129) }, patchAcceptHackerByEmail: { requestType: Constants.REQUEST_TYPES.PATCH, uri: "/api/hacker/acceptEmail/" + Constants.ROLE_CATEGORIES.ALL, + _id: mongoose.Types.ObjectId.createFromTime(130) }, postAnySendWeekOfEmail: { requestType: Constants.REQUEST_TYPES.POST, - uri: "/api/hacker/email/weekOf/" + Constants.ROLE_CATEGORIES.ALL + uri: "/api/hacker/email/weekOf/" + Constants.ROLE_CATEGORIES.ALL, + _id: mongoose.Types.ObjectId.createFromTime(131) }, postSelfSendWeekOfEmail: { requestType: Constants.REQUEST_TYPES.POST, - uri: "/api/hacker/email/weekOf/" + Constants.ROLE_CATEGORIES.SELF + uri: "/api/hacker/email/weekOf/" + Constants.ROLE_CATEGORIES.SELF, + _id: mongoose.Types.ObjectId.createFromTime(132) }, postAnySendDayOfEmail: { requestType: Constants.REQUEST_TYPES.POST, - uri: "/api/hacker/email/dayOf/" + Constants.ROLE_CATEGORIES.ALL + uri: "/api/hacker/email/dayOf/" + Constants.ROLE_CATEGORIES.ALL, + _id: mongoose.Types.ObjectId.createFromTime(133) }, postSelfSendDayOfEmail: { requestType: Constants.REQUEST_TYPES.POST, - uri: "/api/hacker/email/dayOf/" + Constants.ROLE_CATEGORIES.SELF + uri: "/api/hacker/email/dayOf/" + Constants.ROLE_CATEGORIES.SELF, + _id: mongoose.Types.ObjectId.createFromTime(134) } }; const travelRoutes = { getSelf: { requestType: Constants.REQUEST_TYPES.GET, - uri: "/api/travel/self/" + uri: "/api/travel/self/", + _id: mongoose.Types.ObjectId.createFromTime(135) }, getSelfById: { requestType: Constants.REQUEST_TYPES.GET, - uri: "/api/travel/" + Constants.ROLE_CATEGORIES.SELF + uri: "/api/travel/" + Constants.ROLE_CATEGORIES.SELF, + _id: mongoose.Types.ObjectId.createFromTime(136) }, getAnyById: { requestType: Constants.REQUEST_TYPES.GET, - uri: "/api/travel/" + Constants.ROLE_CATEGORIES.ALL + uri: "/api/travel/" + Constants.ROLE_CATEGORIES.ALL, + _id: mongoose.Types.ObjectId.createFromTime(137) }, getSelfByEmail: { requestType: Constants.REQUEST_TYPES.GET, - uri: "/api/travel/email/" + Constants.ROLE_CATEGORIES.SELF + uri: "/api/travel/email/" + Constants.ROLE_CATEGORIES.SELF, + _id: mongoose.Types.ObjectId.createFromTime(138) }, getAnyByEmail: { requestType: Constants.REQUEST_TYPES.GET, - uri: "/api/travel/email/" + Constants.ROLE_CATEGORIES.ALL + uri: "/api/travel/email/" + Constants.ROLE_CATEGORIES.ALL, + _id: mongoose.Types.ObjectId.createFromTime(139) }, post: { requestType: Constants.REQUEST_TYPES.POST, - uri: "/api/travel/" + uri: "/api/travel/", + _id: mongoose.Types.ObjectId.createFromTime(140) }, patchAnyStatusById: { requestType: Constants.REQUEST_TYPES.PATCH, - uri: "/api/travel/status/" + Constants.ROLE_CATEGORIES.ALL + uri: "/api/travel/status/" + Constants.ROLE_CATEGORIES.ALL, + _id: mongoose.Types.ObjectId.createFromTime(141) }, patchAnyOfferById: { requestType: Constants.REQUEST_TYPES.PATCH, - uri: "/api/travel/offer/" + Constants.ROLE_CATEGORIES.ALL + uri: "/api/travel/offer/" + Constants.ROLE_CATEGORIES.ALL, + _id: mongoose.Types.ObjectId.createFromTime(142) } -} +}; const sponsorRoutes = { getSelf: { requestType: Constants.REQUEST_TYPES.GET, - uri: "/api/sponsor/self/" + uri: "/api/sponsor/self/", + _id: mongoose.Types.ObjectId.createFromTime(143) }, getSelfById: { requestType: Constants.REQUEST_TYPES.GET, - uri: "/api/sponsor/" + Constants.ROLE_CATEGORIES.SELF + uri: "/api/sponsor/" + Constants.ROLE_CATEGORIES.SELF, + _id: mongoose.Types.ObjectId.createFromTime(144) }, getAnyById: { requestType: Constants.REQUEST_TYPES.GET, - uri: "/api/sponsor/" + Constants.ROLE_CATEGORIES.ALL + uri: "/api/sponsor/" + Constants.ROLE_CATEGORIES.ALL, + _id: mongoose.Types.ObjectId.createFromTime(145) }, post: { requestType: Constants.REQUEST_TYPES.POST, - uri: "/api/sponsor/" + uri: "/api/sponsor/", + _id: mongoose.Types.ObjectId.createFromTime(146) }, patchSelfById: { requestType: Constants.REQUEST_TYPES.PATCH, - uri: "/api/sponsor/" + Constants.ROLE_CATEGORIES.SELF + uri: "/api/sponsor/" + Constants.ROLE_CATEGORIES.SELF, + _id: mongoose.Types.ObjectId.createFromTime(147) }, patchAnyById: { requestType: Constants.REQUEST_TYPES.PATCH, - uri: "/api/sponsor/" + Constants.ROLE_CATEGORIES.ALL + uri: "/api/sponsor/" + Constants.ROLE_CATEGORIES.ALL, + _id: mongoose.Types.ObjectId.createFromTime(148) } }; const teamRoutes = { get: { requestType: Constants.REQUEST_TYPES.GET, - uri: "/api/team/" + Constants.ROLE_CATEGORIES.ALL + uri: "/api/team/" + Constants.ROLE_CATEGORIES.ALL, + _id: mongoose.Types.ObjectId.createFromTime(149) }, post: { requestType: Constants.REQUEST_TYPES.POST, - uri: "/api/team/" + uri: "/api/team/", + _id: mongoose.Types.ObjectId.createFromTime(150) }, join: { requestType: Constants.REQUEST_TYPES.PATCH, - uri: "/api/team/join/" + uri: "/api/team/join/", + _id: mongoose.Types.ObjectId.createFromTime(151) }, patchSelfById: { requestType: Constants.REQUEST_TYPES.PATCH, - uri: "/api/team/" + Constants.ROLE_CATEGORIES.SELF + uri: "/api/team/" + Constants.ROLE_CATEGORIES.SELF, + _id: mongoose.Types.ObjectId.createFromTime(152) }, patchAnyById: { requestType: Constants.REQUEST_TYPES.PATCH, - uri: "/api/team/" + Constants.ROLE_CATEGORIES.ALL + uri: "/api/team/" + Constants.ROLE_CATEGORIES.ALL, + _id: mongoose.Types.ObjectId.createFromTime(153) }, leave: { requestType: Constants.REQUEST_TYPES.PATCH, - uri: "/api/team/leave/" + uri: "/api/team/leave/", + _id: mongoose.Types.ObjectId.createFromTime(154) } }; const volunteerRoutes = { getSelfById: { requestType: Constants.REQUEST_TYPES.GET, - uri: "/api/volunteer/" + Constants.ROLE_CATEGORIES.SELF + uri: "/api/volunteer/" + Constants.ROLE_CATEGORIES.SELF, + _id: mongoose.Types.ObjectId.createFromTime(155) }, getAnyById: { requestType: Constants.REQUEST_TYPES.GET, - uri: "/api/volunteer/" + Constants.ROLE_CATEGORIES.ALL + uri: "/api/volunteer/" + Constants.ROLE_CATEGORIES.ALL, + _id: mongoose.Types.ObjectId.createFromTime(156) }, post: { requestType: Constants.REQUEST_TYPES.POST, - uri: "/api/volunteer/" + uri: "/api/volunteer/", + _id: mongoose.Types.ObjectId.createFromTime(157) } }; const roleRoutes = { post: { requestType: Constants.REQUEST_TYPES.POST, - uri: "/api/role/" + uri: "/api/role/", + _id: mongoose.Types.ObjectId.createFromTime(158) } }; const searchRoutes = { get: { requestType: Constants.REQUEST_TYPES.GET, - uri: "/api/search/" + uri: "/api/search/", + _id: mongoose.Types.ObjectId.createFromTime(159) } }; const staffRoutes = { hackerStats: { requestType: Constants.REQUEST_TYPES.GET, - uri: "/api/hacker/stats" + uri: "/api/hacker/stats", + _id: mongoose.Types.ObjectId.createFromTime(160) }, postInvite: { requestType: Constants.REQUEST_TYPES.POST, - uri: "/api/account/invite" + uri: "/api/account/invite", + _id: mongoose.Types.ObjectId.createFromTime(161) }, getInvite: { requestType: Constants.REQUEST_TYPES.GET, - uri: "/api/account/invite" + uri: "/api/account/invite", + _id: mongoose.Types.ObjectId.createFromTime(162) } }; const settingsRoutes = { getSettings: { requestType: Constants.REQUEST_TYPES.GET, - uri: "/api/settings" + uri: "/api/settings", + _id: mongoose.Types.ObjectId.createFromTime(163) }, patchSettings: { requestType: Constants.REQUEST_TYPES.PATCH, - uri: "/api/settings" + uri: "/api/settings", + _id: mongoose.Types.ObjectId.createFromTime(164) } }; @@ -310,23 +390,29 @@ const allRoutes = { /** * returns all the routes as a list - * @return {{requestType: string, uri: string}[]} + * @param {boolean} includeId whether to include _id in the returned route object. + * @return {{requestType: string, uri: string, id?: ObjectId}[]} */ -function listAllRoutes() { +function listAllRoutes(includeId = true) { let routes = []; for (let routeGroupKey in allRoutes) { - if (!allRoutes.hasOwnProperty(routeGroupKey)) { + if (!Object.prototype.hasOwnProperty.call(allRoutes, routeGroupKey)) { continue; } const routeGroup = allRoutes[routeGroupKey]; for (let routeKey in routeGroup) { - if (!routeGroup.hasOwnProperty(routeKey)) { + if (!Object.prototype.hasOwnProperty.call(routeGroup, routeKey)) { continue; } - const route = routeGroup[routeKey]; + const route = {}; + // copy only over the attributes that we care about + route.requestType = routeGroup[routeKey].requestType; + route.uri = routeGroup[routeKey].uri; + if (includeId) route._id = routeGroup[routeKey]._id; + routes.push(route); } } diff --git a/docs/architecture.md b/docs/architecture.md index 952ee097..4cf2bbb2 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -28,12 +28,13 @@ We use a custom implementation for authorization, which relies heavily on the de #### `Route` -A `Route`, which is a component of a `Role`, is defined by its uri path, its query parameters in the uri path, and the HTTP verb used to access it: +A `Route`, which is a component of a `Role`, is defined by its uri path, its query parameters in the uri path, the HTTP verb used to access it, and and `_id`: ```json { "uri": "/api/sponsor/", "requestType": "POST", + "_id": "000000010000000000000000" } ``` @@ -53,6 +54,45 @@ A `Role` is a collection of `Routes`, and a unique name (such as `hacker`, or `s _Note here that the parameter `routes` is a list of `Route` objects described in the previous section._ +##### In our code-base, we have a auto-generated list of `single roles` and a custom list of `user roles` + +`single roles` are roles that give permissions for one specific URI and request type. Every route has a `single role`. + +```json +{ + "_id": "000000010000000000000000", + "name": "postAccount", + "routes": [{ + "uri": "/api/account/", + "requestType": "POST", + "_id": "000000010000000000000000", + }] +} +``` + +`user roles` are syntactically the same as custom roles that give permissions for a collection of URIs and request types. These are essentially collections of routes that a given user needs access to in order to properly function with the API. + +```json +{ + "_id": "000000020000000000000000", + "name": "Account", + "routes": [{ + "uri": "/api/account/", + "requestType": "POST", + "_id": "000000010000000000000000", + }, + { + "uri": "/api/account/:SELF", + "requestType": "GET", + "_id": "000000030000000000000000", + }, + // etc. + ] +} +``` + +Note that for each route in the `user role`, you have access to the `single role` document's `_id`! + #### `RoleBinding` A `RoleBinding` is a mapping between an `account` and a `Role`. In this way, we can say that an account has a certain set of allowed actions, defined by the `Role`. An `account` can have a `RoleBinding` to multiple `Roles`. @@ -174,15 +214,18 @@ The http request that this would translate to is: ``` ### Error Codes and Messages + Error messages are in the error.constant.js file. The error constants are of the for TYPE_HTTPCODE_MESSAGE. For example, HACKER_404_MESSAGE. When creating a response, use an existing error message, or create a new one. An example: -``` + +```js next({ status: 422, message: Constants.Error.ACCOUNT_DUPLICATE_422_MESSAGE, error: {...} }); ``` -Note that the error status and the HTTPCODE in the error constant name are the same. + +Note that the error status and the HTTPCODE in the error constant name are the same. Error codes currently in use are: 401 - Invalid authentication From 28e074f294a190fe2c176056111fc379b220c0e7 Mon Sep 17 00:00:00 2001 From: ManethKulatunge <43322057+ManethKulatunge@users.noreply.github.com> Date: Sun, 26 Jul 2020 21:58:36 -0400 Subject: [PATCH 2/3] feat: add batchAccept route to accept multiple hackers at once (#629) * feat: Add HACKER_UPDATE_BATCH success message * feat: Add promise.allsettled so that we can wait for many promises to complete * feat: Add findByHackerId function to Account * feat: Add updatedHackerBatch controller function, generalize validation function, fix router * fix: resolve bug where requests hang when response is null * feat: set route apiVersion number to be 3.0.0 * feat: create parseAcceptBatch Co-authored-by: Pierre Theo Klein --- constants/routes.constant.js | 7 +- constants/success.constant.js | 3 + controllers/hacker.controller.js | 11 + middlewares/hacker.middleware.js | 268 ++++++++++++++++----- middlewares/validators/hacker.validator.js | 3 + package-lock.json | 258 ++++++++++++++------ package.json | 3 +- routes/api/hacker.js | 59 ++++- services/account.service.js | 26 ++ services/email.service.js | 15 +- tests/hacker.test.js | 124 +++++++++- 11 files changed, 623 insertions(+), 154 deletions(-) diff --git a/constants/routes.constant.js b/constants/routes.constant.js index 9a0dda56..79d4c460 100644 --- a/constants/routes.constant.js +++ b/constants/routes.constant.js @@ -6,7 +6,7 @@ * ===***===***===***===***===***===***===***===***=== * * If you are adding a route to this list, update this number - * next avaiable createFromTime value: 165 + * next avaiable createFromTime value: 166 * * If you are deleting a route from this list, please add the ID to the list of 'reserved' IDs, * so that we don't accidentally assign someone to a given ID. @@ -178,6 +178,11 @@ const hackerRoutes = { uri: "/api/hacker/acceptEmail/" + Constants.ROLE_CATEGORIES.ALL, _id: mongoose.Types.ObjectId.createFromTime(130) }, + patchAcceptHackerByArrayOfIds: { + requestType: Constants.REQUEST_TYPES.PATCH, + uri: "/api/hacker/batchAccept", + _id: mongoose.Types.ObjectId.createFromTime(165) + }, postAnySendWeekOfEmail: { requestType: Constants.REQUEST_TYPES.POST, uri: "/api/hacker/email/weekOf/" + Constants.ROLE_CATEGORIES.ALL, diff --git a/constants/success.constant.js b/constants/success.constant.js index e908f651..13e63959 100644 --- a/constants/success.constant.js +++ b/constants/success.constant.js @@ -21,6 +21,7 @@ const HACKER_GET_BY_ID = "Hacker found by id."; const HACKER_READ = "Hacker retrieval successful."; const HACKER_CREATE = "Hacker creation successful."; const HACKER_UPDATE = "Hacker update successful."; +const HACKER_UPDATE_BATCH = "Hacker batch update successful."; const HACKER_SENT_WEEK_OF = "Hacker week-of email sent."; const HACKER_SENT_DAY_OF = "Hacker day-of email sent."; @@ -76,6 +77,8 @@ module.exports = { HACKER_CREATE: HACKER_CREATE, HACKER_UPDATE: HACKER_UPDATE, + HACKER_UPDATE_BATCH: HACKER_UPDATE_BATCH, + HACKER_SENT_WEEK_OF: HACKER_SENT_WEEK_OF, HACKER_SENT_DAY_OF: HACKER_SENT_DAY_OF, diff --git a/controllers/hacker.controller.js b/controllers/hacker.controller.js index bbcdd3bd..25b2a635 100644 --- a/controllers/hacker.controller.js +++ b/controllers/hacker.controller.js @@ -92,8 +92,19 @@ function sentDayOfEmail(req, res) { }); } +function updatedHackerBatch(req, res) { + return res.status(200).json({ + message: Constants.Success.HACKER_UPDATE_BATCH, + data: { + success_ids: req.body.ids, + errors: req.errors || [] + } + }); +} + module.exports = { updatedHacker: updatedHacker, + updatedHackerBatch: updatedHackerBatch, createdHacker: createdHacker, uploadedResume: uploadedResume, downloadedResume: downloadedResume, diff --git a/middlewares/hacker.middleware.js b/middlewares/hacker.middleware.js index 5228f51d..1c70829c 100644 --- a/middlewares/hacker.middleware.js +++ b/middlewares/hacker.middleware.js @@ -1,7 +1,12 @@ +/* eslint-disable require-atomic-updates */ "use strict"; const TAG = `[ HACKER.MIDDLEWARE.js ]`; const mongoose = require("mongoose"); + +// shim to allow us to use Promise.allSettled +require("promise.allsettled").shim(); + const Services = { Hacker: require("../services/hacker.service"), Storage: require("../services/storage.service"), @@ -111,31 +116,52 @@ function addDefaultStatus(req, res, next) { } /** - * Helper function that validates if account is confirmed and is of proper type + * Helper function that validates if account is confirmed and is of proper type. * @param account account object containing the information for an account - * @param {(err?) => void} next + * @returns {{status: number, message: string, error: Object}| null} returns error message if invalid, or null if valid. */ -async function validateConfirmedStatus(account, next) { +function validateConfirmedStatus(account) { if (!account) { - return next({ + return { status: 404, message: Constants.Error.ACCOUNT_404_MESSAGE, - error: {} - }); + data: { account: account } + }; } else if (!account.confirmed) { - return next({ + return { status: 403, message: Constants.Error.ACCOUNT_403_MESSAGE, - error: {} - }); + data: { account: { id: account.id, confirmed: account.confirmed } } + }; } else if (account.accountType !== Constants.General.HACKER) { - return next({ + return { status: 409, - message: Constants.Error.ACCOUNT_TYPE_409_MESSAGE - }); + message: Constants.Error.ACCOUNT_TYPE_409_MESSAGE, + data: { + account: { id: account.id, accountType: account.accountType } + } + }; } else { - return next(); + return; + } +} + +/** + * Verifies that account is confirmed and of proper type from the hacker ID + * @param {string} id + * @returns {string} the id if it is confirmed. + * @throws ACCOUNT_404_MESSAGE if hacker / and or the account does not exist + * @throws ACCOUNT_403_MESSAGE if account is not confirmed + * @throws ACCOUNT_409_MESSAGE if account is not hacker + */ +async function hackerHasConfirmedAccount(id) { + const account = await Services.Account.findByHackerId(id); + const error = validateConfirmedStatus(account); + if (error) { + error.data.hacker_id = id; + throw error; } + return id; } /** @@ -146,7 +172,7 @@ async function validateConfirmedStatus(account, next) { */ async function validateConfirmedStatusFromAccountId(req, res, next) { const account = await Services.Account.findById(req.body.accountId); - return validateConfirmedStatus(account, next); + return next(validateConfirmedStatus(account)); } /** @@ -156,16 +182,42 @@ async function validateConfirmedStatusFromAccountId(req, res, next) { * @param {(err?) => void} next */ async function validateConfirmedStatusFromHackerId(req, res, next) { - const hacker = await Services.Hacker.findById(req.params.id); - if (hacker == null) { + // Throws error if not confirmed. + await hackerHasConfirmedAccount(req.params.id); + next(); +} + +/** + * Verifies that account is confirmed and of proper type from the hacker ID passed in req.body.ids. + * It will remove all ids that are not valid. + * @param {{body: {ids: ObjectId[]}}} req + * @param {*} res + * @param {(err?) => void} next + */ +async function validateConfirmedStatusFromArrayofHackerIds(req, res, next) { + if (!req.body.ids) { return next({ status: 404, - message: Constants.Error.HACKER_404_MESSAGE, - data: req.body.hackerId + message: Constants.Error.HACKER_404_MESSAGE }); } - const account = await Services.Account.findById(hacker.accountId); - return validateConfirmedStatus(account, next); + let confirmedStatusPromises = []; + for (const id of req.body.ids) { + confirmedStatusPromises.push(hackerHasConfirmedAccount(id)); + } + const results = await Promise.allSettled(confirmedStatusPromises); + req.body.ids = []; + req.errors = req.errors ? req.errors : []; + // Iterate through results and split by errors and IDs + for (const result of results) { + if (result.status === "rejected") { + req.errors.push(result.reason); + } else { + // hackerHasConfirmedAccount will return ID + req.body.ids.push(result.value); + } + } + next(); } /** @@ -175,7 +227,7 @@ async function validateConfirmedStatusFromHackerId(req, res, next) { * @param {(err?) => void} next */ async function validateConfirmedStatusFromObject(req, res, next) { - return validateConfirmedStatus(req.body.account, next); + next(validateConfirmedStatus(req.body.account)); } /** @@ -236,7 +288,7 @@ function ensureAccountLinkedToHacker(req, res, next) { hacker && req.user && String.toString(hacker.accountId) === - String.toString(req.user.id) + String.toString(req.user.id) ) { return next(); } else { @@ -295,6 +347,36 @@ async function downloadResume(req, res, next) { } return next(); } + +/** + * Sends the status for a given hacker. + * @returns {Promise} Returns a promise, which resolves into the ID, or rejects with the reason. + * @param {string} id + * @param {string} status + */ +async function sendStatusUpdateEmailHelper(id, status) { + // send it to the hacker that is being updated. + const account = await Services.Account.findByHackerId(id); + if (!account) { + throw { + status: 500, + message: Constants.Error.GENERIC_500_MESSAGE, + id: id + }; + } + // Promisify sendStatusUpdate :/ + return new Promise((resolve, reject) => { + Services.Email.sendStatusUpdate( + account.firstName, + account.email, + status, + (err) => { + err ? reject(err) : resolve(id); + } + ); + }); +} + /** * Sends a preset email to a user if a status change occured. * @param {{body: {status?: string}, params: {id: string}}} req @@ -306,29 +388,42 @@ async function sendStatusUpdateEmail(req, res, next) { if (!req.body.status) { return next(); } else { - // send it to the hacker that is being updated. - const hacker = await Services.Hacker.findById(req.params.id); - const account = await Services.Account.findById(hacker.accountId); - if (!hacker) { - return next({ - status: 404, - message: Constants.Error.HACKER_404_MESSAGE - }); - } else if (!account) { - return next({ - status: 500, - message: Constants.Error.GENERIC_500_MESSAGE - }); - } - Services.Email.sendStatusUpdate( - account.firstName, - account.email, - req.body.status, - next - ); + await sendStatusUpdateEmailHelper(req.params.id, req.body.status); + next(); } } +/** + * Sends a preset email to a user if a status change occured. + * @param {{body: {status?: string, ids: string[]}} req + * @param {*} res + * @param {(err?:*)=>void} next + */ +async function sendStatusUpdateEmailForMultipleIds(req, res, next) { + //skip if the status doesn't exist + if (!req.body.status) { + return next(); + } else { + const statusUpdatePromises = []; + for (const id of req.body.ids) { + statusUpdatePromises.push( + sendStatusUpdateEmailHelper(id, req.body.status) + ); + } + const results = await Promise.allSettled(statusUpdatePromises); + req.body.ids = []; + req.errors = req.errors ? req.errors : []; + for (const result of results) { + if (result.status === "rejected") { + req.errors.push(result.reason); + } else { + // result.value will be the hacker's ID. + req.body.ids.push(result.value); + } + } + next(); + } +} /** * Sends a preset email to a user if a status change occured with email params. * @param {{body: {status?: string}, params: {email: string}}} req @@ -497,7 +592,7 @@ function checkStatus(statuses) { return Middleware.Util.asyncMiddleware(async (req, res, next) => { let hacker = await Services.Hacker.findById(req.params.id); - if (!!hacker) { + if (hacker) { const status = hacker.status; // makes sure the hacker's status is in the accepted statuses list if (statuses.indexOf(status) === -1) { @@ -550,7 +645,9 @@ async function updateHacker(req, res, next) { // If this hacker has a travel account associated with it, then update request to reflect amount wanted for travel const travel = await Services.Travel.findByHackerId(hacker.id); if (travel) { - await Services.Travel.updateOne(travel.id, { "request": hacker.application.accommodation.travel }); + await Services.Travel.updateOne(travel.id, { + request: hacker.application.accommodation.travel + }); } return next(); } else { @@ -599,10 +696,39 @@ async function obtainEmailByHackerId(req, res, next) { } /** - * Sets req.body.status to Accepted for next middleware. - * @param {{params:{id: string}, body: *}} req - * @param {*} res - * @param {*} next + * Updates a list of hacker that is specified by req.body.ids. + * Some hackers may fail, and that will be stored in req.errors. + * Filters req.body.ids to only the successful processes. + * @param {{body:{ids: String[]}}} req + * @param {*} res + * @param {*} next + */ +async function updateBatchHacker(req, res, next) { + let eachHackerPromise = []; + for (const id of req.body.ids) { + eachHackerPromise.push(Services.Hacker.updateOne(id, req.body)); + } + const updateResults = await Promise.allSettled(eachHackerPromise); + // clear req.body.ids so that we store only the good ones. + req.body.ids = []; + req.errors = req.errors ? req.errors : []; + for (const result of updateResults) { + // Some error happened when trying to update a hacker. + if (result.status === "rejected") { + req.errors.push(result.reason); + } else { + // result.value will be the hacker object. + req.body.ids.push(result.value._id); + } + } + next(); +} + +/** + * Sets req.body.status to Accepted for next middleware, and store req.params.id as req.hackerId + * @param {{params:{id: string}, body: *}} req + * @param {*} res + * @param {*} next */ function parseAccept(req, res, next) { req.body.status = Constants.General.HACKER_STATUS_ACCEPTED; @@ -612,9 +738,9 @@ function parseAccept(req, res, next) { /** * Sets req.body.hacker.status to Accepted for next middleware. - * @param {{params:{email: string}, body: *}} req - * @param {*} res - * @param {*} next + * @param {{params:{email: string}, body: *}} req + * @param {*} res + * @param {*} next */ function parseAcceptEmail(req, res, next) { req.body.hacker.status = Constants.General.HACKER_STATUS_ACCEPTED; @@ -622,6 +748,27 @@ function parseAcceptEmail(req, res, next) { next(); } +/** + * Sets req.body.status to Accepted for next middleware. + * @param {{body: *}} req + * @param {*} res + * @param {*} next + */ +function parseAcceptBatch(req, res, next) { + req.body.status = Constants.General.HACKER_STATUS_ACCEPTED; + next(); +} + +/** + * Sets req.body.status to Accepted for next middleware. + * @param {{params:{id: string}, body: *}} req + * @param {*} res + * @param {*} next + */ +function parseBatchAccept(req, res, next) { + req.body.status = Constants.General.HACKER_STATUS_ACCEPTED; + next(); +} /** * @function createHacker @@ -649,7 +796,7 @@ async function createHacker(req, res, next) { }); } const hacker = await Services.Hacker.createHacker(hackerDetails); - if (!!hacker) { + if (hacker) { req.body.hacker = hacker; return next(); } else { @@ -704,7 +851,7 @@ async function findSelf(req, res, next) { const hacker = await Services.Hacker.findByAccountId(req.user.id); - if (!!hacker) { + if (hacker) { req.body.hacker = hacker; return next(); } else { @@ -736,19 +883,27 @@ module.exports = { sendStatusUpdateEmail: Middleware.Util.asyncMiddleware( sendStatusUpdateEmail ), + sendStatusUpdateEmailForMultipleIds: Middleware.Util.asyncMiddleware( + sendStatusUpdateEmailForMultipleIds + ), sendAppliedStatusEmail: Middleware.Util.asyncMiddleware( sendAppliedStatusEmail ), updateHacker: Middleware.Util.asyncMiddleware(updateHacker), - obtainEmailByHackerId: Middleware.Util.asyncMiddleware(obtainEmailByHackerId), + updateBatchHacker: Middleware.Util.asyncMiddleware(updateBatchHacker), parseAccept: parseAccept, + parseAcceptBatch: parseAcceptBatch, parseAcceptEmail: parseAcceptEmail, + parseBatch: parseBatchAccept, validateConfirmedStatusFromAccountId: Middleware.Util.asyncMiddleware( validateConfirmedStatusFromAccountId ), validateConfirmedStatusFromHackerId: Middleware.Util.asyncMiddleware( validateConfirmedStatusFromHackerId ), + validateConfirmedStatusFromArrayofHackerIds: Middleware.Util.asyncMiddleware( + validateConfirmedStatusFromArrayofHackerIds + ), validateConfirmedStatusFromObject: Middleware.Util.asyncMiddleware( validateConfirmedStatusFromObject ), @@ -768,5 +923,8 @@ module.exports = { findSelf: Middleware.Util.asyncMiddleware(findSelf), getStats: Middleware.Util.asyncMiddleware(getStats), findById: Middleware.Util.asyncMiddleware(findById), - findByEmail: Middleware.Util.asyncMiddleware(findByEmail) + findByEmail: Middleware.Util.asyncMiddleware(findByEmail), + obtainEmailByHackerId: Middleware.Util.asyncMiddleware( + obtainEmailByHackerId + ) }; diff --git a/middlewares/validators/hacker.validator.js b/middlewares/validators/hacker.validator.js index 8bc7c931..44004afd 100644 --- a/middlewares/validators/hacker.validator.js +++ b/middlewares/validators/hacker.validator.js @@ -264,5 +264,8 @@ module.exports = { statsValidator: [ VALIDATOR.searchModelValidator("query", "model", false), VALIDATOR.searchValidator("query", "q") + ], + batchUpdateValidator: [ + VALIDATOR.mongoIdArrayValidator("body", "ids", false) ] }; diff --git a/package-lock.json b/package-lock.json index 4a3ab0cc..ec4aeda2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "hackerAPI", - "version": "2.4.0", + "version": "2.5.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1047,6 +1047,17 @@ "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", "dev": true }, + "array.prototype.map": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array.prototype.map/-/array.prototype.map-1.0.2.tgz", + "integrity": "sha512-Az3OYxgsa1g7xDYp86l0nnN4bcmuEITGe1rbdEBVkrqkzMgDcbdQ2R7r41pNzti+4NMces3H8gMmuioZUilLgw==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "es-array-method-boxes-properly": "^1.0.0", + "is-string": "^1.0.4" + } + }, "arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", @@ -2056,7 +2067,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, "requires": { "object-keys": "^1.0.12" } @@ -2282,28 +2292,53 @@ "dev": true }, "es-abstract": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.16.3.tgz", - "integrity": "sha512-WtY7Fx5LiOnSYgF5eg/1T+GONaGmpvpPdCpSnYij+U2gDTL0UPfWrhDw7b2IYb+9NQJsYpCA0wOQvZfsd6YwRw==", - "dev": true, + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", "requires": { "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", "has": "^1.0.3", "has-symbols": "^1.0.1", - "is-callable": "^1.1.4", - "is-regex": "^1.0.4", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", "object-inspect": "^1.7.0", "object-keys": "^1.1.1", - "string.prototype.trimleft": "^2.1.0", - "string.prototype.trimright": "^2.1.0" + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-array-method-boxes-properly": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", + "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==" + }, + "es-get-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.0.tgz", + "integrity": "sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==", + "requires": { + "es-abstract": "^1.17.4", + "has-symbols": "^1.0.1", + "is-arguments": "^1.0.4", + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-string": "^1.0.5", + "isarray": "^2.0.5" + }, + "dependencies": { + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + } } }, "es-to-primitive": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, "requires": { "is-callable": "^1.1.4", "is-date-object": "^1.0.1", @@ -3640,8 +3675,7 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "functional-red-black-tree": { "version": "1.0.1", @@ -4583,7 +4617,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -4601,8 +4634,7 @@ "has-symbols": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", - "dev": true + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" }, "has-unicode": { "version": "2.0.1", @@ -4958,6 +4990,11 @@ } } }, + "is-arguments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", + "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==" + }, "is-binary-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", @@ -4974,10 +5011,9 @@ "dev": true }, "is-callable": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", - "dev": true + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", + "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==" }, "is-ci": { "version": "1.2.1", @@ -5009,10 +5045,9 @@ } }, "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", - "dev": true + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==" }, "is-descriptor": { "version": "0.1.6", @@ -5081,6 +5116,11 @@ "ip-regex": "^2.0.0" } }, + "is-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.1.tgz", + "integrity": "sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==" + }, "is-npm": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", @@ -5144,12 +5184,11 @@ "dev": true }, "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", + "integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==", "requires": { - "has": "^1.0.1" + "has-symbols": "^1.0.1" } }, "is-retry-allowed": { @@ -5158,6 +5197,11 @@ "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", "dev": true }, + "is-set": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.1.tgz", + "integrity": "sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==" + }, "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", @@ -5169,11 +5213,15 @@ "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==" }, + "is-string": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==" + }, "is-symbol": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", - "dev": true, "requires": { "has-symbols": "^1.0.1" } @@ -5211,6 +5259,20 @@ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, + "iterate-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/iterate-iterator/-/iterate-iterator-1.0.1.tgz", + "integrity": "sha512-3Q6tudGN05kbkDQDI4CqjaBf4qf85w6W6GnuZDtUVYwKgtC1q8yxYX7CZed7N+tLzQqS6roujWvszf13T+n9aw==" + }, + "iterate-value": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/iterate-value/-/iterate-value-1.0.2.tgz", + "integrity": "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ==", + "requires": { + "es-get-iterator": "^1.0.2", + "iterate-iterator": "^1.0.1" + } + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5785,9 +5847,9 @@ } }, "mocha": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.2.2.tgz", - "integrity": "sha512-FgDS9Re79yU1xz5d+C4rv1G7QagNGHZ+iXF81hO8zY35YZZcLEsJVfFolfsqKFWunATEvNzMK0r/CwWd/szO9A==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.2.3.tgz", + "integrity": "sha512-0R/3FvjIGH3eEuG17ccFPk117XL2rWxatr81a57D+r/x2uTYZRbdZ4oVidEUMh2W2TJDa7MdAb12Lm2/qrKajg==", "dev": true, "requires": { "ansi-colors": "3.2.3", @@ -5802,7 +5864,7 @@ "js-yaml": "3.13.1", "log-symbols": "2.2.0", "minimatch": "3.0.4", - "mkdirp": "0.5.1", + "mkdirp": "0.5.4", "ms": "2.1.1", "node-environment-flags": "1.0.5", "object.assign": "4.1.0", @@ -5810,8 +5872,8 @@ "supports-color": "6.0.0", "which": "1.3.1", "wide-align": "1.1.3", - "yargs": "13.3.0", - "yargs-parser": "13.1.1", + "yargs": "13.3.2", + "yargs-parser": "13.1.2", "yargs-unparser": "1.6.0" }, "dependencies": { @@ -5830,6 +5892,12 @@ "color-convert": "^1.9.0" } }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, "cliui": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", @@ -5870,6 +5938,21 @@ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", "dev": true }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "mkdirp": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.4.tgz", + "integrity": "sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, "ms": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", @@ -5923,9 +6006,9 @@ "dev": true }, "yargs": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", - "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", "dev": true, "requires": { "cliui": "^5.0.0", @@ -5937,7 +6020,17 @@ "string-width": "^3.0.0", "which-module": "^2.0.0", "y18n": "^4.0.0", - "yargs-parser": "^13.1.1" + "yargs-parser": "^13.1.2" + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" } } } @@ -6326,16 +6419,14 @@ } }, "object-inspect": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", - "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", - "dev": true + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==" }, "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" }, "object-visit": { "version": "1.0.1", @@ -6350,7 +6441,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, "requires": { "define-properties": "^1.1.2", "function-bind": "^1.1.1", @@ -6359,13 +6449,13 @@ } }, "object.getownpropertydescriptors": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", - "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz", + "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==", "dev": true, "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.5.1" + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" } }, "object.pick": { @@ -6651,6 +6741,18 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, + "promise.allsettled": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.2.tgz", + "integrity": "sha512-UpcYW5S1RaNKT6pd+s9jp9K9rlQge1UXKskec0j6Mmuq7UJCvlS2J2/s/yuPN8ehftf9HXMxWlKiPbGGUzpoRg==", + "requires": { + "array.prototype.map": "^1.0.1", + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1", + "iterate-value": "^1.0.0" + } + }, "protobufjs": { "version": "6.8.8", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz", @@ -7597,24 +7699,22 @@ "strip-ansi": "^3.0.0" } }, - "string.prototype.trimleft": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz", - "integrity": "sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==", - "dev": true, + "string.prototype.trimend": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", + "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", "requires": { "define-properties": "^1.1.3", - "function-bind": "^1.1.1" + "es-abstract": "^1.17.5" } }, - "string.prototype.trimright": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz", - "integrity": "sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==", - "dev": true, + "string.prototype.trimstart": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", + "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", "requires": { "define-properties": "^1.1.3", - "function-bind": "^1.1.1" + "es-abstract": "^1.17.5" } }, "string_decoder": { @@ -8511,6 +8611,12 @@ "color-convert": "^1.9.0" } }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, "cliui": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", @@ -8528,12 +8634,6 @@ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", "dev": true }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - }, "string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", @@ -8572,9 +8672,9 @@ "dev": true }, "yargs": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", - "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", "dev": true, "requires": { "cliui": "^5.0.0", @@ -8586,7 +8686,17 @@ "string-width": "^3.0.0", "which-module": "^2.0.0", "y18n": "^4.0.0", - "yargs-parser": "^13.1.1" + "yargs-parser": "^13.1.2" + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" } } } diff --git a/package.json b/package.json index d6100ee5..c44f0159 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "multer": "^1.4.2", "passport": "^0.4.0", "passport-local": "^1.0.0", + "promise.allsettled": "^1.0.2", "q": "^1.5.1", "qrcode": "^1.4.4", "winston": "^2.4.4" @@ -52,7 +53,7 @@ "eslint": "6.7.2", "eslint-config-prettier": "6.7.0", "eslint-plugin-prettier": "3.1.1", - "mocha": "^6.2.2", + "mocha": "^6.2.3", "nodemon": "^1.19.4", "prettier": "1.19.1" } diff --git a/routes/api/hacker.js b/routes/api/hacker.js index 0360772e..038ed869 100644 --- a/routes/api/hacker.js +++ b/routes/api/hacker.js @@ -307,7 +307,7 @@ module.exports = { Middleware.Hacker.sendStatusUpdateEmail, Controllers.Hacker.updatedHacker ); - + /** * @api {patch} /hacker/acceptEmail/:email accept a Hacker by email * @apiName acceptHacker @@ -326,19 +326,52 @@ module.exports = { * @apiPermission Administrator */ hackerRouter - .route("/acceptEmail/:email") - .patch( - Middleware.Auth.ensureAuthenticated(), - Middleware.Auth.ensureAuthorized([Services.Hacker.findByEmail]), - Middleware.Validator.RouteParam.emailValidator, - Middleware.parseBody.middleware, - Middleware.Hacker.findByEmail, - Middleware.Hacker.parseAcceptEmail, - Middleware.Hacker.obtainEmailByHackerId, - Middleware.Hacker.completeStatusUpdateEmail, - Controllers.Hacker.updatedHacker - ); + .route("/acceptEmail/:email") + .patch( + Middleware.Auth.ensureAuthenticated(), + Middleware.Auth.ensureAuthorized([Services.Hacker.findByEmail]), + Middleware.Validator.RouteParam.emailValidator, + Middleware.parseBody.middleware, + Middleware.Hacker.findByEmail, + Middleware.Hacker.parseAcceptEmail, + Middleware.Hacker.obtainEmailByHackerId, + Middleware.Hacker.completeStatusUpdateEmail, + Controllers.Hacker.updatedHacker + ); + /** + * @api {patch} /hacker/batchAccept/ accept array of Hackers + * @apiName acceptHacker + * @apiGroup Hacker + * @apiVersion 3.0.0 + * + * @apiParam (body) {{ids: ObjectId[]}} Array of id(s) that needed to be accepted + * + * @apiSuccess {string} message Success message + * @apiSuccess {object} data success_ids array and errors array. Errors array will contain a detailed error for why the batch update for a given ID did not work + * @apiSuccessExample {object} Success-Response: + * { + * "message": "Hacker batch update successful.", + * "data": { + * "success_ids": ["id1", "id2"] + * "errors": [{status: 404, message: "ACCOUNT_NOT_FOUND", account: null, hacker_id: "id3"}] + * } + * } + * @apiPermission Administrator + */ + hackerRouter + .route("/batchAccept") + .patch( + Middleware.Auth.ensureAuthenticated(), + Middleware.Auth.ensureAuthorized([]), + Middleware.Validator.Hacker.batchUpdateValidator, + Middleware.parseBody.middleware, + Middleware.Hacker.validateConfirmedStatusFromArrayofHackerIds, + Middleware.Hacker.parseAcceptBatch, + Middleware.Hacker.updateBatchHacker, + Middleware.Hacker.sendStatusUpdateEmailForMultipleIds, + Controllers.Hacker.updatedHackerBatch + ); /** * @api {patch} /hacker/checkin/:id update a hacker's status to be 'Checked-in'. Note that the Hacker must eitehr be Accepted or Confirmed. * @apiName checkinHacker diff --git a/services/account.service.js b/services/account.service.js index b11bd79f..8c7a74b3 100644 --- a/services/account.service.js +++ b/services/account.service.js @@ -1,4 +1,5 @@ "use strict"; +const Hacker = require("../models/hacker.model"); const Account = require("../models/account.model"); const logger = require("./logger.service"); const bcrypt = require("bcrypt"); @@ -20,6 +21,30 @@ function findById(id) { ); } +/** + * @function findByHackerId + * @param {ObjectId} id the Hacker's ID + * @returns {Promise} The account of the hacker, minus the password. Returns null if the hacker does not exist, or if the hacker is not associated with an account. + * Get the account by using the hacker's ID. + */ +async function findByHackerId(id) { + const TAG = `[Account Service # findByHackerId]:`; + const query = { + _id: id + }; + const hacker = await Hacker.findById( + query, + logger.queryCallbackFactory(TAG, "account", query) + ).populate({ + path: "accountId", + select: " -password" + }); + if (!hacker || !hacker.accountId) { + return null; + } + return hacker.accountId; +} + /** * @function findByEmail * @param {String} email @@ -123,6 +148,7 @@ function updatePassword(id, newPassword) { module.exports = { findOne: findOne, findById: findById, + findByHackerId: findByHackerId, findByEmail: findByEmail, addOneAccount: addOneAccount, getAccountIfValid: getAccountIfValid, diff --git a/services/email.service.js b/services/email.service.js index dcefe34f..246f245a 100644 --- a/services/email.service.js +++ b/services/email.service.js @@ -74,7 +74,10 @@ class EmailService { html: html }; this.send(mailData).then((response) => { - if (response[0].statusCode >= 200 && response[0].statusCode < 300) { + if ( + !response || + (response[0].statusCode >= 200 && response[0].statusCode < 300) + ) { callback(); } else { callback(response[0]); @@ -102,7 +105,10 @@ class EmailService { html: html }; this.send(mailData).then((response) => { - if (response[0].statusCode >= 200 && response[0].statusCode < 300) { + if ( + !response || + (response[0].statusCode >= 200 && response[0].statusCode < 300) + ) { callback(); } else { callback(response[0]); @@ -124,7 +130,10 @@ class EmailService { }) }; this.send(mailData).then((response) => { - if (response[0].statusCode >= 200 && response[0].statusCode < 300) { + if ( + !response || + (response[0].statusCode >= 200 && response[0].statusCode < 300) + ) { callback(); } else { callback(response[0]); diff --git a/tests/hacker.test.js b/tests/hacker.test.js index d76547b6..89b82f08 100644 --- a/tests/hacker.test.js +++ b/tests/hacker.test.js @@ -4,7 +4,7 @@ const chaiHttp = require("chai-http"); chai.use(chaiHttp); const server = require("../app"); const agent = chai.request.agent(server.app); -const should = chai.should(); +chai.should(); const Hacker = require("../models/hacker.model"); const fs = require("fs"); const path = require("path"); @@ -50,6 +50,17 @@ const unconfirmedHacker1 = util.hacker.unconfirmedAccountHacker1; const invalidHacker1 = util.hacker.invalidHacker1; +const BatchAcceptHackerArrayValid = [ + util.hacker.TeamHacker0._id, + util.hacker.TeamHacker1._id, + util.hacker.NoTeamHacker0._id +]; + +const BatchAcceptHackerArrayInvalid = [ + invalidHacker1._id, + unconfirmedHacker1._id +]; + describe("GET hacker", function() { // fail on authentication it("should FAIL to list a hacker's information on /api/hacker/:id GET due to authentication", function(done) { @@ -621,6 +632,106 @@ describe("POST create hacker", function() { }); }); +describe("PATCH update multiple hackers", function() { + it("should FAIL input validation on /api/hacker/batchAccept as an Admin", function(done) { + util.auth.login(agent, Admin0, (error) => { + if (error) { + agent.close(); + return done(error); + } + return agent + .patch(`/api/hacker/batchAccept/`) + .type("application/json") + .send() + .end(function(err, res) { + res.should.have.status(422); + res.should.be.json; + res.body.should.have.property("message"); + res.body.message.should.equal( + Constants.Error.VALIDATION_422_MESSAGE + ); + done(); + }); + }); + }); + it("should FAIL authorization on /api/hacker/batchAccept as a non-Admin", function(done) { + util.auth.login(agent, noTeamHackerAccount0, (error) => { + if (error) { + agent.close(); + return done(error); + } + return agent + .patch(`/api/hacker/batchAccept/`) + .type("application/json") + .send() + .end(function(err, res) { + res.should.have.status(403); + res.should.be.json; + res.body.should.have.property("message"); + res.body.message.should.equal( + Constants.Error.AUTH_403_MESSAGE + ); + done(); + }); + }); + }); + it("should SUCCEED and accept 2 hackers on /api/hacker/batchAccept as an Admin", function(done) { + util.auth.login(agent, Admin0, (error) => { + if (error) { + agent.close(); + return done(error); + } + return agent + .patch(`/api/hacker/batchAccept/`) + .type("application/json") + .send({ ids: BatchAcceptHackerArrayValid }) + .end(function(err, res) { + res.should.have.status(200); + res.should.be.json; + res.body.should.have.property("message"); + res.body.message.should.equal( + Constants.Success.HACKER_UPDATE_BATCH + ); + res.body.should.have.property("data"); + chai.assert.equal( + JSON.stringify(res.body.data), + JSON.stringify({ + success_ids: BatchAcceptHackerArrayValid, + errors: [] + }) + ); + done(); + }); + }); + }); + it("should SUCCEED and accept 0 out of 2 hackers on /api/hacker/batchAccept as an Admin", function(done) { + util.auth.login(agent, Admin0, (error) => { + if (error) { + agent.close(); + return done(error); + } + return agent + .patch(`/api/hacker/batchAccept/`) + .type("application/json") + .send({ ids: BatchAcceptHackerArrayInvalid }) + .end(function(err, res) { + res.should.have.status(200); + res.should.be.json; + res.body.should.have.property("message"); + res.body.message.should.equal( + Constants.Success.HACKER_UPDATE_BATCH + ); + res.body.should.have.property("data"); + res.body.data.should.have.property("success_ids"); + chai.assert.equal(res.body.data.success_ids.length, 0); + res.body.data.should.have.property("errors"); + chai.assert.equal(res.body.data.errors.length, 2); + done(); + }); + }); + }); +}); + describe("PATCH update one hacker", function() { // fail on authentication it("should FAIL to update a hacker on /api/hacker/:id GET due to authentication", function(done) { @@ -694,7 +805,7 @@ describe("PATCH update one hacker", function() { res.should.be.json; res.body.should.have.property("message"); res.body.message.should.equal( - Constants.Error.HACKER_404_MESSAGE + Constants.Error.ACCOUNT_404_MESSAGE ); res.body.should.have.property("data"); @@ -778,7 +889,9 @@ describe("PATCH update one hacker", function() { return done(error); } return agent - .patch(`/api/hacker/acceptEmail/${invalidHackerAccount0[0].email}`) + .patch( + `/api/hacker/acceptEmail/${invalidHackerAccount0[0].email}` + ) .type("application/json") .send() .end(function(err, res) { @@ -813,10 +926,7 @@ describe("PATCH update one hacker", function() { Constants.Success.HACKER_UPDATE ); res.body.should.have.property("data"); - chai.assert.equal( - res.body.data.hacker.status, - "Accepted" - ); + chai.assert.equal(res.body.data.hacker.status, "Accepted"); done(); }); }); From ec1c284d75c4f940a9019bec14065d29e821b8ed Mon Sep 17 00:00:00 2001 From: loreina Date: Thu, 30 Jul 2020 17:55:28 -0400 Subject: [PATCH 3/3] release: bump to v3.0.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index ec4aeda2..541951e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "hackerAPI", - "version": "2.5.0", + "version": "3.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index c44f0159..f37b7ae3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hackerAPI", - "version": "2.5.0", + "version": "3.0.0", "private": true, "scripts": { "start": "DEBUG=hackboard:* NODE_ENV=development nodemon --ignore gcp_creds.json ./bin/www.js",