diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 7fed6b7d..093a4222 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.2.0](https://github.com/hackmcgill/hackerapi/tree/2.2.0) - 2020-01-12 + +### Added + +- Add route to accept hacker by email + ## [2.1.3](https://github.com/hackmcgill/hackerapi/tree/2.1.3) - 2020-01-11 ### Changed diff --git a/VERSION b/VERSION index ac2cdeba..ccbccc3d 100755 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.1.3 +2.2.0 diff --git a/constants/routes.constant.js b/constants/routes.constant.js index e22e1d28..3e141cc4 100644 --- a/constants/routes.constant.js +++ b/constants/routes.constant.js @@ -128,6 +128,10 @@ const hackerRoutes = { requestType: Constants.REQUEST_TYPES.PATCH, uri: "/api/hacker/accept/" + Constants.ROLE_CATEGORIES.ALL, }, + patchAcceptHackerByEmail: { + requestType: Constants.REQUEST_TYPES.PATCH, + uri: "/api/hacker/acceptEmail/" + Constants.ROLE_CATEGORIES.ALL, + }, postAnySendWeekOfEmail: { requestType: Constants.REQUEST_TYPES.POST, uri: "/api/hacker/email/weekOf/" + Constants.ROLE_CATEGORIES.ALL diff --git a/middlewares/hacker.middleware.js b/middlewares/hacker.middleware.js index affede09..06b65ce6 100644 --- a/middlewares/hacker.middleware.js +++ b/middlewares/hacker.middleware.js @@ -328,6 +328,40 @@ async function sendStatusUpdateEmail(req, res, next) { } } +/** + * Sends a preset email to a user if a status change occured with email params. + * @param {{body: {status?: string}, params: {email: string}}} req + * @param {*} res + * @param {(err?:*)=>void} next + */ +async function completeStatusUpdateEmail(req, res, next) { + //skip if the status doesn't exist + if (!req.body.hacker.status) { + return next(); + } else { + // send it to the hacker that is being updated. + const hacker = await Services.Hacker.findById(req.body.hacker._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.hacker.status, + next + ); + } +} + /** * Sends an email telling the user that they have applied. This is used exclusively when we POST a hacker. * @param {{body: {hacker: {accountId: string}}}} req @@ -523,6 +557,40 @@ async function updateHacker(req, res, next) { } } +/** + * Updates a hacker that is specified by req.body.hacker._id, and then sets req.email + * to the email of the hacker, found in Account. + * @param {{params:{_id: string}, body: *}} req + * @param {*} res + * @param {*} next + */ +async function obtainEmailByHackerId(req, res, next) { + const hacker = await Services.Hacker.findById(req.body.hacker._id); + if (hacker) { + const acct = await Services.Account.findById(hacker.accountId); + if (!acct) { + return next({ + status: 500, + message: Constants.Error.HACKER_UPDATE_500_MESSAGE, + data: { + hackerId: hacker.id, + accountId: hacker.accountId + } + }); + } + req.email = acct.email; + return next(); + } else { + return next({ + status: 404, + message: Constants.Error.HACKER_404_MESSAGE, + data: { + id: req.params.id + } + }); + } +} + /** * Sets req.body.status to Accepted for next middleware. * @param {{params:{id: string}, body: *}} req @@ -531,6 +599,19 @@ async function updateHacker(req, res, next) { */ function parseAccept(req, res, next) { req.body.status = Constants.General.HACKER_STATUS_ACCEPTED; + req.hackerId = req.params.id; + next(); +} + +/** + * Sets req.body.hacker.status to Accepted for next middleware. + * @param {{params:{email: string}, body: *}} req + * @param {*} res + * @param {*} next + */ +function parseAcceptEmail(req, res, next) { + req.body.hacker.status = Constants.General.HACKER_STATUS_ACCEPTED; + req.hackerId = req.body.hacker._id; next(); } @@ -652,7 +733,9 @@ module.exports = { sendAppliedStatusEmail ), updateHacker: Middleware.Util.asyncMiddleware(updateHacker), + obtainEmailByHackerId: Middleware.Util.asyncMiddleware(obtainEmailByHackerId), parseAccept: parseAccept, + parseAcceptEmail: parseAcceptEmail, validateConfirmedStatusFromAccountId: Middleware.Util.asyncMiddleware( validateConfirmedStatusFromAccountId ), @@ -662,6 +745,9 @@ module.exports = { validateConfirmedStatusFromObject: Middleware.Util.asyncMiddleware( validateConfirmedStatusFromObject ), + completeStatusUpdateEmail: Middleware.Util.asyncMiddleware( + completeStatusUpdateEmail + ), checkDuplicateAccountLinks: Middleware.Util.asyncMiddleware( checkDuplicateAccountLinks ), diff --git a/package-lock.json b/package-lock.json index 145e4339..b14debea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "hackerAPI", - "version": "2.1.3", + "version": "2.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 65906e9c..2246c794 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hackerAPI", - "version": "2.1.3", + "version": "2.2.0", "private": true, "scripts": { "start": "DEBUG=hackboard:* NODE_ENV=development nodemon --ignore gcp_creds.json ./bin/www.js", diff --git a/routes/api/hacker.js b/routes/api/hacker.js index c84bbe2a..0360772e 100644 --- a/routes/api/hacker.js +++ b/routes/api/hacker.js @@ -307,6 +307,37 @@ module.exports = { Middleware.Hacker.sendStatusUpdateEmail, Controllers.Hacker.updatedHacker ); + + /** + * @api {patch} /hacker/acceptEmail/:email accept a Hacker by email + * @apiName acceptHacker + * @apiGroup Hacker + * @apiVersion 2.0.0 + * + * @apiSuccess {string} message Success message + * @apiSuccess {object} data Hacker object + * @apiSuccessExample {object} Success-Response: + * { + * "message": "Changed hacker information", + * "data": { + * "status": "Accepted" + * } + * } + * @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 + ); /** * @api {patch} /hacker/checkin/:id update a hacker's status to be 'Checked-in'. Note that the Hacker must eitehr be Accepted or Confirmed. diff --git a/tests/hacker.test.js b/tests/hacker.test.js index 15e8b9fb..d76547b6 100644 --- a/tests/hacker.test.js +++ b/tests/hacker.test.js @@ -28,6 +28,7 @@ const volunteerAccount0 = util.account.volunteerAccounts.stored[0]; const newHackerAccount0 = util.account.hackerAccounts.new[0]; const newHacker0 = util.hacker.newHacker0; +const invalidHackerAccount0 = util.account.hackerAccounts.invalid; const invalidHacker0 = util.hacker.invalidHacker0; const invalidHacker2 = util.hacker.invalidHacker2; const newHacker1 = util.hacker.newHacker1; @@ -638,6 +639,7 @@ describe("PATCH update one hacker", function() { }); }); + //should FAIL on authentication it("should FAIL to accept a hacker on /api/hacker/accept/:id due to authentication", function(done) { chai.request(server.app) .patch(`/api/hacker/accept/${TeamHacker0._id}`) @@ -730,6 +732,96 @@ describe("PATCH update one hacker", function() { }); }); + it("should FAIL to accept a hacker on /api/hacker/acceptEmail/:email due to authentication", function(done) { + chai.request(server.app) + .patch(`/api/hacker/acceptEmail/${teamHackerAccount0.email}`) + .type("application/json") + .send() + .end(function(err, res) { + res.should.have.status(401); + res.should.be.json; + res.body.should.have.property("message"); + res.body.message.should.equal(Constants.Error.AUTH_401_MESSAGE); + done(); + }); + }); + + // should FAIL due to authorization + it("should FAIL to accept hacker info due to lack of authorization on /api/hacker/acceptEmail/:email", function(done) { + util.auth.login(agent, noTeamHackerAccount0, (error) => { + if (error) { + agent.close(); + return done(error); + } + return agent + .patch(`/api/hacker/acceptEmail/${teamHackerAccount0.email}`) + .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 + ); + res.body.should.have.property("data"); + + done(); + }); + }); + }); + + it("should FAIL to accept an invalid hacker's info on /api/hacker/acceptEmail/:email", function(done) { + util.auth.login(agent, Admin0, (error) => { + if (error) { + agent.close(); + return done(error); + } + return agent + .patch(`/api/hacker/acceptEmail/${invalidHackerAccount0[0].email}`) + .type("application/json") + .send() + .end(function(err, res) { + res.should.have.status(404); + res.should.be.json; + res.body.should.have.property("message"); + res.body.message.should.equal( + Constants.Error.ACCOUNT_404_MESSAGE + ); + res.body.should.have.property("data"); + + done(); + }); + }); + }); + + it("should SUCCEED and accept a hacker on /api/hacker/acceptEmail/:email as an Admin", function(done) { + util.auth.login(agent, Admin0, (error) => { + if (error) { + agent.close(); + return done(error); + } + return agent + .patch(`/api/hacker/acceptEmail/${teamHackerAccount0.email}`) + .type("application/json") + .send() + .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 + ); + res.body.should.have.property("data"); + chai.assert.equal( + res.body.data.hacker.status, + "Accepted" + ); + done(); + }); + }); + }); + // should succeed on admin case it("should SUCCEED and update a hacker using admin power", function(done) { util.auth.login(agent, Admin0, (error) => {