diff --git a/.vscode/settings.json b/.vscode/settings.json index 3eb39ee5..74276cc5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,5 +5,6 @@ "this":false, "bad_property":false }, - "search.usePCRE2": true + "search.usePCRE2": true, + "editor.formatOnSave": true } \ No newline at end of file diff --git a/assets/email/Ticket.hbs b/assets/email/Ticket.hbs new file mode 100644 index 00000000..d9d006ff --- /dev/null +++ b/assets/email/Ticket.hbs @@ -0,0 +1 @@ +
{{firstName}}
\ No newline at end of file diff --git a/constants/general.constant.js b/constants/general.constant.js index dd0385b9..01267922 100644 --- a/constants/general.constant.js +++ b/constants/general.constant.js @@ -89,6 +89,8 @@ const ANY_REGEX = /^.+$/; const MAX_TEAM_SIZE = 4; +const WEEK_OF = 'Week Of'; + const EMAIL_SUBJECTS = {}; EMAIL_SUBJECTS[HACKER_STATUS_NONE] = `Application for ${HACKATHON_NAME} incomplete`; EMAIL_SUBJECTS[HACKER_STATUS_APPLIED] = `Thanks for applying to ${HACKATHON_NAME}`; @@ -98,6 +100,8 @@ EMAIL_SUBJECTS[HACKER_STATUS_CONFIRMED] = `Thanks for confirming your attendance EMAIL_SUBJECTS[HACKER_STATUS_CANCELLED] = "Sorry to see you go"; EMAIL_SUBJECTS[HACKER_STATUS_CHECKED_IN] = `Welcome to ${HACKATHON_NAME}`; +EMAIL_SUBJECTS[WEEK_OF] = `Welcome to ${HACKATHON_NAME}`; + const CONFIRM_ACC_EMAIL_SUBJECT = `Please complete your hacker application for ${HACKATHON_NAME}`; const CREATE_ACC_EMAIL_SUBJECTS = {}; CREATE_ACC_EMAIL_SUBJECTS[HACKER] = `You've been invited to create a hacker account for ${HACKATHON_NAME}`; @@ -145,5 +149,6 @@ module.exports = { CACHE_TIMEOUT_STATS: CACHE_TIMEOUT_STATS, CACHE_KEY_STATS: CACHE_KEY_STATS, MAX_TEAM_SIZE: MAX_TEAM_SIZE, + WEEK_OF: WEEK_OF, SAMPLE_DIET_RESTRICTIONS: SAMPLE_DIET_RESTRICTIONS, }; \ No newline at end of file diff --git a/constants/routes.constant.js b/constants/routes.constant.js index f1622d5e..d7b4f799 100644 --- a/constants/routes.constant.js +++ b/constants/routes.constant.js @@ -124,6 +124,14 @@ const hackerRoutes = { requestType: Constants.REQUEST_TYPES.PATCH, uri: "/api/hacker/confirmation/" + Constants.ROLE_CATEGORIES.SELF, }, + "postAnySendWeekOfEmail": { + requestType: Constants.REQUEST_TYPES.POST, + uri: "/api/hacker/email/weekOf/" + Constants.ROLE_CATEGORIES.ALL, + }, + "postSelfSendWeekOfEmail": { + requestType: Constants.REQUEST_TYPES.POST, + uri: "/api/hacker/email/weekOf/" + Constants.ROLE_CATEGORIES.SELF, + }, }; const sponsorRoutes = { diff --git a/constants/success.constant.js b/constants/success.constant.js index 0d473a25..acebf1d8 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_SENT_WEEK_OF = "Hacker week-of email sent." const RESUME_UPLOAD = "Resume upload successful."; const RESUME_DOWNLOAD = "Resume download successful."; @@ -67,6 +68,8 @@ module.exports = { HACKER_CREATE: HACKER_CREATE, HACKER_UPDATE: HACKER_UPDATE, + HACKER_SENT_WEEK_OF: HACKER_SENT_WEEK_OF, + RESUME_UPLOAD: RESUME_UPLOAD, RESUME_DOWNLOAD: RESUME_DOWNLOAD, diff --git a/controllers/hacker.controller.js b/controllers/hacker.controller.js index abf4d60d..daf49bf1 100644 --- a/controllers/hacker.controller.js +++ b/controllers/hacker.controller.js @@ -79,6 +79,13 @@ function gotStats(req, res) { } +function sentWeekOfEmail(req, res) { + return res.status(200).json({ + message: Constants.Success.HACKER_SENT_WEEK_OF, + data: {} + }); +} + module.exports = { updatedHacker: updatedHacker, createdHacker: createdHacker, @@ -86,4 +93,5 @@ module.exports = { downloadedResume: downloadedResume, showHacker: showHacker, gotStats: gotStats, + sentWeekOfEmail: sentWeekOfEmail, }; \ No newline at end of file diff --git a/docs/api/api_data.js b/docs/api/api_data.js index 93150e27..932e9940 100644 --- a/docs/api/api_data.js +++ b/docs/api/api_data.js @@ -1869,6 +1869,58 @@ define({ "url": "https://api.mchacks.ca/api/hacker/resume/:id" }] }, + { + "type": "post", + "url": "/hacker/email/weekOf/:id", + "title": "", + "description": "

Sends a hacker the week-of email, along with the HackPass QR code to view their hacker profile (for checkin purposes). Hackers must be eitherconfirmed, or checked in.

", + "name": "postHackerSendWeekOfEmail", + "group": "Hacker", + "version": "0.0.9", + "parameter": { + "fields": { + "param": [{ + "group": "param", + "type": "string", + "optional": true, + "field": "status", + "description": "

The hacker ID

" + }] + } + }, + "success": { + "fields": { + "Success 200": [{ + "group": "Success 200", + "type": "string", + "optional": false, + "field": "message", + "description": "

Success message

" + }, + { + "group": "Success 200", + "type": "object", + "optional": false, + "field": "data", + "description": "

empty

" + } + ] + }, + "examples": [{ + "title": "Success-Response: ", + "content": "{\n \"message\": \"Hacker week-of email sent.\", \n \"data\": {}\n}", + "type": "object" + }] + }, + "permission": [{ + "name": "Administrator" + }], + "filename": "routes/api/hacker.js", + "groupTitle": "Hacker", + "sampleRequest": [{ + "url": "https://api.mchacks.ca/api/hacker/email/weekOf/:id" + }] + }, { "type": "get", "url": "/sponsor/self", diff --git a/docs/api/api_data.json b/docs/api/api_data.json index fab71f01..9406fb6b 100644 --- a/docs/api/api_data.json +++ b/docs/api/api_data.json @@ -1868,6 +1868,58 @@ "url": "https://api.mchacks.ca/api/hacker/resume/:id" }] }, + { + "type": "post", + "url": "/hacker/email/weekOf/:id", + "title": "", + "description": "

Sends a hacker the week-of email, along with the HackPass QR code to view their hacker profile (for checkin purposes). Hackers must be eitherconfirmed, or checked in.

", + "name": "postHackerSendWeekOfEmail", + "group": "Hacker", + "version": "0.0.9", + "parameter": { + "fields": { + "param": [{ + "group": "param", + "type": "string", + "optional": true, + "field": "status", + "description": "

The hacker ID

" + }] + } + }, + "success": { + "fields": { + "Success 200": [{ + "group": "Success 200", + "type": "string", + "optional": false, + "field": "message", + "description": "

Success message

" + }, + { + "group": "Success 200", + "type": "object", + "optional": false, + "field": "data", + "description": "

empty

" + } + ] + }, + "examples": [{ + "title": "Success-Response: ", + "content": "{\n \"message\": \"Hacker week-of email sent.\", \n \"data\": {}\n}", + "type": "object" + }] + }, + "permission": [{ + "name": "Administrator" + }], + "filename": "routes/api/hacker.js", + "groupTitle": "Hacker", + "sampleRequest": [{ + "url": "https://api.mchacks.ca/api/hacker/email/weekOf/:id" + }] + }, { "type": "get", "url": "/sponsor/self", diff --git a/docs/api/api_project.js b/docs/api/api_project.js index 59d642d6..8730e140 100644 --- a/docs/api/api_project.js +++ b/docs/api/api_project.js @@ -9,7 +9,7 @@ define({ "apidoc": "0.3.0", "generator": { "name": "apidoc", - "time": "2019-01-21T04:13:36.482Z", + "time": "2019-01-23T23:37:14.991Z", "url": "http://apidocjs.com", "version": "0.17.7" } diff --git a/docs/api/api_project.json b/docs/api/api_project.json index 8d421e55..75fa20a2 100644 --- a/docs/api/api_project.json +++ b/docs/api/api_project.json @@ -9,7 +9,7 @@ "apidoc": "0.3.0", "generator": { "name": "apidoc", - "time": "2019-01-21T04:13:36.482Z", + "time": "2019-01-23T23:37:14.991Z", "url": "http://apidocjs.com", "version": "0.17.7" } diff --git a/middlewares/hacker.middleware.js b/middlewares/hacker.middleware.js index 22e982ec..c061fa03 100644 --- a/middlewares/hacker.middleware.js +++ b/middlewares/hacker.middleware.js @@ -7,6 +7,7 @@ const Services = { Storage: require("../services/storage.service"), Email: require("../services/email.service"), Account: require("../services/account.service"), + Env: require("../services/env.service"), }; const Middleware = { Util: require("./util.middleware") @@ -308,6 +309,29 @@ async function sendAppliedStatusEmail(req, res, next) { Services.Email.sendStatusUpdate(account.firstName, account.email, Constants.General.HACKER_STATUS_APPLIED, 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 + * @param {*} res + * @param {(err?:*)=>void} next + */ +async function sendTicketEmail(req, res, next) { + const hacker = req.body.hacker; + const address = Services.Env.isProduction() ? process.env.FRONTEND_ADDRESS_DEPLOY : process.env.FRONTEND_ADDRESS_DEV; + const httpOrHttps = (address.includes("localhost")) ? "http" : "https"; + const singleHackerViewLink = Services.Hacker.generateHackerViewLink(httpOrHttps, address, hacker._id.toString()); + const ticketSVG = await Services.Hacker.generateQRCode(singleHackerViewLink); + const account = await Services.Account.findById(hacker.accountId); + if (!account || !ticketSVG) { + return next({ + status: 500, + message: Constants.Error.GENERIC_500_MESSAGE, + error: {} + }); + } + Services.Email.sendTicketEmail(account.firstName, account.email, ticketSVG, next); +} + /** * If the current hacker's status is Constants.HACKER_STATUS_NONE, and the hacker's application is completed, * then it will change the status of the hacker to Constants.General.HACKER_STATUS_APPLIED, and then email the hacker to @@ -520,6 +544,7 @@ module.exports = { ensureAccountLinkedToHacker: ensureAccountLinkedToHacker, uploadResume: Middleware.Util.asyncMiddleware(uploadResume), downloadResume: Middleware.Util.asyncMiddleware(downloadResume), + sendTicketEmail: Middleware.Util.asyncMiddleware(sendTicketEmail), sendStatusUpdateEmail: Middleware.Util.asyncMiddleware(sendStatusUpdateEmail), sendAppliedStatusEmail: Middleware.Util.asyncMiddleware(sendAppliedStatusEmail), updateHacker: Middleware.Util.asyncMiddleware(updateHacker), diff --git a/package-lock.json b/package-lock.json index e3a011d2..2df93898 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1685,6 +1685,14 @@ } } }, + "can-promise": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/can-promise/-/can-promise-0.0.1.tgz", + "integrity": "sha512-gzVrHyyrvgt0YpDm7pn04MQt8gjh0ZAhN4ZDyCRtGl6YnuuK6b4aiUTD7G52r9l4YNmxfTtEscb92vxtAlL6XQ==", + "requires": { + "window-or-global": "^1.0.1" + } + }, "capture-stack-trace": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz", @@ -2147,7 +2155,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "dev": true, "requires": { "lru-cache": "^4.0.1", "shebang-command": "^1.2.0", @@ -2373,6 +2380,11 @@ "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.4.tgz", "integrity": "sha512-Uv3SW8bmH9nAtHKaKSanOQmj2DnlH65fUpcrMdfdaOxUG02QQ4YGZ8AE7kKOMisF7UqvOlGKVYWRvezdncW9lg==" }, + "dijkstrajs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.1.tgz", + "integrity": "sha1-082BIh4+pAdCz83lVtTpnpjdxxs=" + }, "dir-glob": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.0.0.tgz", @@ -2620,7 +2632,6 @@ "version": "0.7.0", "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", - "dev": true, "requires": { "cross-spawn": "^5.0.1", "get-stream": "^3.0.0", @@ -3731,6 +3742,11 @@ } } }, + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==" + }, "get-func-name": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", @@ -3745,8 +3761,7 @@ "get-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" }, "get-value": { "version": "2.0.6", @@ -4893,8 +4908,7 @@ "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" }, "is-stream-ended": { "version": "0.1.4", @@ -4924,8 +4938,7 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, "isobject": { "version": "3.0.1", @@ -5191,6 +5204,22 @@ } } }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "dependencies": { + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + } + } + }, "lodash": { "version": "4.17.10", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", @@ -5297,7 +5326,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.2.tgz", "integrity": "sha512-wgeVXhrDwAWnIF/yZARsFnMBtdFXOg1b8RIrhilp+0iDYN4mdQcNZElDZ0e4B64BhaxeQ5zN7PMyvu7we1kPeQ==", - "dev": true, "requires": { "pseudomap": "^1.0.2", "yallist": "^2.1.2" @@ -5367,6 +5395,14 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, + "mem": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", + "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", + "requires": { + "mimic-fn": "^1.0.0" + } + }, "memory-cache": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/memory-cache/-/memory-cache-0.2.0.tgz", @@ -5465,6 +5501,11 @@ "mime-db": "~1.33.0" } }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -5778,7 +5819,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, "requires": { "path-key": "^2.0.0" } @@ -5904,8 +5944,28 @@ "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" }, "package-json": { "version": "4.0.1", @@ -5986,8 +6046,7 @@ "path-key": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" }, "path-to-regexp": { "version": "0.1.7", @@ -6045,6 +6104,11 @@ "pinkie": "^2.0.0" } }, + "pngjs": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.3.3.tgz", + "integrity": "sha512-1n3Z4p3IOxArEs1VRXnZ/RXdfEniAUS9jb68g58FIXMNkPJeZd+Qh4Uq7/e0LVxAQGos1eIUrqrt4FpjdnEd+Q==" + }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -6268,6 +6332,143 @@ "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" }, + "qrcode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.3.2.tgz", + "integrity": "sha512-sYo+Tf7nx14YZSNPqnyEWefVrjUEpM4/NxOrxOk+jcQ9NhM2LCNuzGmQctDhGTQdO2YJorKY55dypx+hvVo5jw==", + "requires": { + "can-promise": "0.0.1", + "dijkstrajs": "^1.0.1", + "isarray": "^2.0.1", + "pngjs": "^3.3.0", + "yargs": "^8.0.2" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "requires": { + "locate-path": "^2.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "isarray": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.4.tgz", + "integrity": "sha512-GMxXOiUirWg1xTKRipM0Ek07rX+ubx4nNVElTJdNLYmNO/2YrDkgJGw9CljXn+r4EWiDQg/8lsRdHyg2PJuUaA==" + }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + } + }, + "os-locale": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", + "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", + "requires": { + "execa": "^0.7.0", + "lcid": "^1.0.0", + "mem": "^1.1.0" + } + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "requires": { + "pify": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "requires": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" + }, + "yargs": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-8.0.2.tgz", + "integrity": "sha1-YpmpBVsc78lp/355wdkY3Osiw2A=", + "requires": { + "camelcase": "^4.1.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^2.0.0", + "read-pkg-up": "^2.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^7.0.0" + } + } + } + }, "qs": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", @@ -6489,6 +6690,16 @@ "uuid": "^3.1.0" } }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" + }, "require_optional": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", @@ -6612,6 +6823,11 @@ "send": "0.16.2" } }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, "set-immediate-shim": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", @@ -6648,7 +6864,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, "requires": { "shebang-regex": "^1.0.0" } @@ -6656,8 +6871,7 @@ "shebang-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" }, "shelljs": { "version": "0.3.0", @@ -7043,8 +7257,7 @@ "strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" }, "strip-indent": { "version": "1.0.1", @@ -7584,11 +7797,15 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", - "dev": true, "requires": { "isexe": "^2.0.0" } }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, "widest-line": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.0.tgz", @@ -7631,6 +7848,11 @@ } } }, + "window-or-global": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/window-or-global/-/window-or-global-1.0.1.tgz", + "integrity": "sha1-2+RboqKRqrxW1iz2bEW3+jIpRt4=" + }, "window-size": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz", @@ -7728,6 +7950,21 @@ "window-size": "^0.1.4", "y18n": "^3.2.0" } + }, + "yargs-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz", + "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=", + "requires": { + "camelcase": "^4.1.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + } + } } } } diff --git a/package.json b/package.json index 6483660f..93508266 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "passport": "^0.4.0", "passport-local": "^1.0.0", "q": "^1.5.1", + "qrcode": "^1.3.2", "winston": "^2.4.4" }, "devDependencies": { diff --git a/routes/api/hacker.js b/routes/api/hacker.js index d7f690d8..e714e0eb 100644 --- a/routes/api/hacker.js +++ b/routes/api/hacker.js @@ -585,6 +585,36 @@ module.exports = { Middleware.Hacker.sendStatusUpdateEmail, Controllers.Hacker.updatedHacker ); + + /** + * @api {post} /hacker/email/weekOf/:id + * @apiDescription Sends a hacker the week-of email, along with the HackPass QR code to view their hacker profile (for checkin purposes). Hackers must be eitherconfirmed, or checked in. + * @apiName postHackerSendWeekOfEmail + * @apiGroup Hacker + * @apiVersion 0.0.9 + * + * @apiParam (param) {string} [status] The hacker ID + * @apiSuccess {string} message Success message + * @apiSuccess {object} data empty + * @apiSuccessExample {object} Success-Response: + * { + * "message": "Hacker week-of email sent.", + * "data": {} + * } + * @apiPermission Administrator + */ + hackerRouter.route("/email/weekOf/:id").post( + Middleware.Auth.ensureAuthenticated(), + Middleware.Auth.ensureAuthorized([Services.Hacker.findById]), + + Middleware.Validator.RouteParam.idValidator, + Middleware.parseBody.middleware, + Middleware.Hacker.findById, + Middleware.Hacker.checkStatus([CONSTANTS.HACKER_STATUS_CONFIRMED, CONSTANTS.HACKER_STATUS_CHECKED_IN]), + Middleware.Hacker.sendTicketEmail, + Controllers.Hacker.sentWeekOfEmail + ) + apiRouter.use("/hacker", hackerRouter); } }; \ No newline at end of file diff --git a/services/email.service.js b/services/email.service.js index f0a8ed55..ccde0da8 100644 --- a/services/email.service.js +++ b/services/email.service.js @@ -51,6 +51,35 @@ class EmailService { } }); } + /** + * Send email with ticket. + * @param {string} firstName the recipient's first name + * @param {string} recipient the recipient's email address + * @param {string} ticket the ticket image (must be base-64 string) + * @param {(err?)=>void} callback + */ + sendTicketEmail(firstName, recipient, ticket, callback) { + const handlebarsPath = path.join(__dirname, `../assets/email/Ticket.hbs`); + const html = this.renderEmail(handlebarsPath, { + firstName: firstName, + ticket: ticket + }); + const mailData = { + to: recipient, + from: process.env.NO_REPLY_EMAIL, + subject: Constants.EMAIL_SUBJECTS[Constants.WEEK_OF], + html: html + }; + this.send(mailData).then( + (response) => { + if (response[0].statusCode >= 200 && response[0].statusCode < 300) { + callback(); + } else { + callback(response[0]); + } + }, callback); + + } sendStatusUpdate(firstName, recipient, status, callback) { const handlebarsPath = path.join(__dirname, `../assets/email/statusEmail/${status}.hbs`); diff --git a/services/hacker.service.js b/services/hacker.service.js index ff0adb60..24259f21 100644 --- a/services/hacker.service.js +++ b/services/hacker.service.js @@ -5,6 +5,9 @@ const logger = require("./logger.service"); const cache = require("memory-cache"); const Constants = require("../constants/general.constant"); + +const QRCode = require("qrcode"); + /** * @function createHacker * @param {{_id: ObjectId, accountId: ObjectId, school: string, gender: string, needsBus: boolean, application: {Object}}} hackerDetails @@ -94,6 +97,28 @@ async function getStatsAllHackersCached() { return getStats(allHackers); } +/** + * Generate a QR code for the hacker. + * @param {string} str The string to be encoded in the QR code. + */ +async function generateQRCode(str) { + const response = await QRCode.toDataURL(str, { + scale: 3 + }); + return response; +} + +/** + * Generate the link for the single hacker view page on frontend. + * @param {string} httpOrHttps either HTTP or HTTPs + * @param {string} domain The domain of the frontend site + * @param {string} id The ID of the hacker to view + */ +function generateHackerViewLink(httpOrHttps, domain, id) { + const link = `${httpOrHttps}://${domain}/application/view/${id}`; + return link; +} + function getStats(hackers) { const TAG = `[ hacker Service # getStats ]`; const stats = { @@ -151,4 +176,6 @@ module.exports = { findByAccountId: findByAccountId, getStats: getStats, getStatsAllHackersCached: getStatsAllHackersCached, + generateQRCode: generateQRCode, + generateHackerViewLink: generateHackerViewLink, }; \ No newline at end of file diff --git a/tests/hacker.test.js b/tests/hacker.test.js index db317b33..76948a78 100644 --- a/tests/hacker.test.js +++ b/tests/hacker.test.js @@ -910,4 +910,56 @@ describe("GET Hacker stats", function () { done(); }); }); +}); + +describe("POST send week-of email", function () { + it("It should FAIL to send the week-of email due to invalid Authentication", function (done) { + //this takes a lot of time for some reason + chai.request(server.app) + .post(`/api/hacker/email/weekOf/${noTeamHacker0._id}`) + .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); + res.body.should.have.property("data"); + done(); + }); + }); + it("It should FAIL to send the week-of email due to invalid Authorization", function (done) { + //this takes a lot of time for some reason + util.auth.login(agent, noTeamHacker0, (error) => { + if (error) { + return done(error); + } + return agent + .post(`/api/hacker/email/weekOf/${noTeamHacker0._id}`) + .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("It should SUCCEED to send the week-of email", function (done) { + //this takes a lot of time for some reason + util.auth.login(agent, Admin0, (error) => { + if (error) { + return done(error); + } + return agent + .post(`/api/hacker/email/weekOf/${TeamHacker0._id}`) + .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_SENT_WEEK_OF); + res.body.should.have.property("data"); + done(); + }); + }); + }); }); \ No newline at end of file