diff --git a/app.js b/app.js index 7efd85cc..1bc2b6bb 100755 --- a/app.js +++ b/app.js @@ -8,7 +8,7 @@ const Services = { log: require("./services/logger.service"), db: require("./services/database.service"), auth: require("./services/auth.service"), - env: require("./services/env.service") + env: require("./services/env.service"), }; const envLoadResult = Services.env.load(path.join(__dirname, "./.env")); @@ -31,6 +31,7 @@ const searchRouter = require("./routes/api/search"); const settingsRouter = require("./routes/api/settings"); const volunteerRouter = require("./routes/api/volunteer"); const roleRouter = require("./routes/api/role"); +const emailsRouter = require("./routes/api/emails"); const app = express(); Services.db.connect(); @@ -40,7 +41,7 @@ let corsOptions = {}; if (!Services.env.isProduction()) { corsOptions = { origin: [`http://${process.env.FRONTEND_ADDRESS_DEV}`], - credentials: true + credentials: true, }; } else { corsOptions = { @@ -48,34 +49,32 @@ if (!Services.env.isProduction()) { const allowedOrigins = [ `https://${process.env.FRONTEND_ADDRESS_DEPLOY}`, `https://${process.env.FRONTEND_ADDRESS_BETA}`, - `https://docs.mchacks.ca` + `https://docs.mchacks.ca`, ]; const regex = /^https:\/\/dashboard-[\w-]+\.vercel\.app$/; if ( allowedOrigins.includes(origin) || // Explicitly allowed origins - regex.test(origin) // Matches dashboard subdomains + regex.test(origin) // Matches dashboard subdomains ) { callback(null, true); } else { - callback(new Error('Not allowed by CORS')); + callback(new Error("Not allowed by CORS")); } }, - credentials: true + credentials: true, }; } - - app.use(cors(corsOptions)); app.use(Services.log.requestLogger); app.use(Services.log.errorLogger); app.use(express.json()); app.use( express.urlencoded({ - extended: false - }) + extended: false, + }), ); app.use(cookieParser()); //Cookie-based session tracking @@ -86,8 +85,8 @@ app.use( // Cookie Options maxAge: 48 * 60 * 60 * 1000, //Logged in for 48 hours sameSite: process.env.COOKIE_SAME_SITE, - secureProxy: !Services.env.isTest() - }) + secureProxy: !Services.env.isTest(), + }), ); app.use(passport.initialize()); app.use(passport.session()); //persistent login session @@ -116,10 +115,10 @@ settingsRouter.activate(apiRouter); Services.log.info("Settings router activated"); roleRouter.activate(apiRouter); Services.log.info("Role router activated"); +emailsRouter.activate(apiRouter); +Services.log.info("Emails router activated"); -apiRouter.use("/", indexRouter); app.use("/", indexRouter); - app.use("/api", apiRouter); //Custom error handler @@ -140,10 +139,10 @@ app.use((err, req, res, next) => { } res.status(status).json({ message: message, - data: errorContents + data: errorContents, }); }); module.exports = { - app: app + app: app, }; diff --git a/assets/email/statusEmail/Accepted.hbs b/assets/email/statusEmail/Accepted.hbs index 8e0bc5d3..e728a241 100644 --- a/assets/email/statusEmail/Accepted.hbs +++ b/assets/email/statusEmail/Accepted.hbs @@ -387,12 +387,12 @@ style="-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;color:#4D4D4D;"> Congratulations, {{firstName}}! 🎉

- We’re thrilled to offer you a spot at McHacks! We can't wait to see what + We're thrilled to offer you a spot at McHacks! We can't wait to see what you create with us this year.

Confirm your attendance on our hacker - dashboard no later than January 21th at 11:59PM EST. + dashboard no later than January 13th at 11:59PM EST.

If you can no longer attend McHacks, please let us know as soon as possible by withdrawing your application on our hacker dashboard until the deadline on January - 3rd at + style="-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;font-weight: 700;">December + 20th at 11:59 PM ET.

In the meantime, follow us on Hi, {{firstName}},

- Thanks for confirming your attendance for McHacks! We hope you’re just + Thanks for confirming your attendance for McHacks! We hope you're just as excited as we are. Keep an eye out for our week-of email with more details regarding McHacks. Happy hacking!

diff --git a/constants/routes.constant.js b/constants/routes.constant.js index e880674d..0a246fe8 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: 168 + * next avaiable createFromTime value: 170 * * 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. @@ -20,189 +20,189 @@ const authRoutes = { login: { requestType: Constants.REQUEST_TYPES.POST, uri: "/api/auth/login", - _id: mongoose.Types.ObjectId.createFromTime(100) + _id: mongoose.Types.ObjectId.createFromTime(100), }, logout: { requestType: Constants.REQUEST_TYPES.POST, uri: "/api/auth/logout", - _id: mongoose.Types.ObjectId.createFromTime(101) + _id: mongoose.Types.ObjectId.createFromTime(101), }, getSelfRoleBindindings: { requestType: Constants.REQUEST_TYPES.GET, uri: "/api/auth/rolebindings/" + Constants.ROLE_CATEGORIES.SELF, - _id: mongoose.Types.ObjectId.createFromTime(102) + _id: mongoose.Types.ObjectId.createFromTime(102), }, getAnyRoleBindings: { requestType: Constants.REQUEST_TYPES.GET, uri: "/api/auth/rolebindings/" + Constants.ROLE_CATEGORIES.ALL, - _id: mongoose.Types.ObjectId.createFromTime(103) + _id: mongoose.Types.ObjectId.createFromTime(103), }, changePassword: { requestType: Constants.REQUEST_TYPES.PATCH, uri: "/api/auth/password/change", - _id: mongoose.Types.ObjectId.createFromTime(104) - } + _id: mongoose.Types.ObjectId.createFromTime(104), + }, }; const accountRoutes = { getSelf: { requestType: Constants.REQUEST_TYPES.GET, uri: "/api/account/self", - _id: mongoose.Types.ObjectId.createFromTime(105) + _id: mongoose.Types.ObjectId.createFromTime(105), }, getSelfById: { requestType: Constants.REQUEST_TYPES.GET, uri: "/api/account/" + Constants.ROLE_CATEGORIES.SELF, - _id: mongoose.Types.ObjectId.createFromTime(106) + _id: mongoose.Types.ObjectId.createFromTime(106), }, getAnyById: { requestType: Constants.REQUEST_TYPES.GET, uri: "/api/account/" + Constants.ROLE_CATEGORIES.ALL, - _id: mongoose.Types.ObjectId.createFromTime(107) + _id: mongoose.Types.ObjectId.createFromTime(107), }, post: { requestType: Constants.REQUEST_TYPES.POST, uri: "/api/account/", - _id: mongoose.Types.ObjectId.createFromTime(108) + _id: mongoose.Types.ObjectId.createFromTime(108), }, patchSelfById: { requestType: Constants.REQUEST_TYPES.PATCH, uri: "/api/account/" + Constants.ROLE_CATEGORIES.SELF, - _id: mongoose.Types.ObjectId.createFromTime(109) + _id: mongoose.Types.ObjectId.createFromTime(109), }, patchAnyById: { requestType: Constants.REQUEST_TYPES.PATCH, uri: "/api/account/" + Constants.ROLE_CATEGORIES.ALL, - _id: mongoose.Types.ObjectId.createFromTime(110) + _id: mongoose.Types.ObjectId.createFromTime(110), }, inviteAccount: { requestType: Constants.REQUEST_TYPES.POST, uri: "/api/account/invite", - _id: mongoose.Types.ObjectId.createFromTime(111) - } + _id: mongoose.Types.ObjectId.createFromTime(111), + }, }; const hackerRoutes = { getSelf: { requestType: Constants.REQUEST_TYPES.GET, uri: "/api/hacker/self/", - _id: mongoose.Types.ObjectId.createFromTime(112) + _id: mongoose.Types.ObjectId.createFromTime(112), }, getSelfById: { requestType: Constants.REQUEST_TYPES.GET, uri: "/api/hacker/" + Constants.ROLE_CATEGORIES.SELF, - _id: mongoose.Types.ObjectId.createFromTime(113) + _id: mongoose.Types.ObjectId.createFromTime(113), }, getAnyById: { requestType: Constants.REQUEST_TYPES.GET, uri: "/api/hacker/" + Constants.ROLE_CATEGORIES.ALL, - _id: mongoose.Types.ObjectId.createFromTime(114) + _id: mongoose.Types.ObjectId.createFromTime(114), }, getSelfByEmail: { requestType: Constants.REQUEST_TYPES.GET, uri: "/api/hacker/email/" + Constants.ROLE_CATEGORIES.SELF, - _id: mongoose.Types.ObjectId.createFromTime(115) + _id: mongoose.Types.ObjectId.createFromTime(115), }, getAnyByEmail: { requestType: Constants.REQUEST_TYPES.GET, uri: "/api/hacker/email/" + Constants.ROLE_CATEGORIES.ALL, - _id: mongoose.Types.ObjectId.createFromTime(116) + _id: mongoose.Types.ObjectId.createFromTime(116), }, getSelfResumeById: { requestType: Constants.REQUEST_TYPES.GET, uri: "/api/hacker/resume/" + Constants.ROLE_CATEGORIES.SELF, - _id: mongoose.Types.ObjectId.createFromTime(117) + _id: mongoose.Types.ObjectId.createFromTime(117), }, getAnyResumeById: { requestType: Constants.REQUEST_TYPES.GET, uri: "/api/hacker/resume/" + Constants.ROLE_CATEGORIES.ALL, - _id: mongoose.Types.ObjectId.createFromTime(118) + _id: mongoose.Types.ObjectId.createFromTime(118), }, post: { requestType: Constants.REQUEST_TYPES.POST, uri: "/api/hacker/", - _id: mongoose.Types.ObjectId.createFromTime(119) + _id: mongoose.Types.ObjectId.createFromTime(119), }, postSelfResumeById: { requestType: Constants.REQUEST_TYPES.POST, uri: "/api/hacker/resume/" + Constants.ROLE_CATEGORIES.SELF, - _id: mongoose.Types.ObjectId.createFromTime(120) + _id: mongoose.Types.ObjectId.createFromTime(120), }, postAnyResumeById: { requestType: Constants.REQUEST_TYPES.POST, uri: "/api/hacker/resume/" + Constants.ROLE_CATEGORIES.ALL, - _id: mongoose.Types.ObjectId.createFromTime(121) + _id: mongoose.Types.ObjectId.createFromTime(121), }, patchSelfById: { requestType: Constants.REQUEST_TYPES.PATCH, uri: "/api/hacker/" + Constants.ROLE_CATEGORIES.SELF, - _id: mongoose.Types.ObjectId.createFromTime(122) + _id: mongoose.Types.ObjectId.createFromTime(122), }, patchAnyById: { requestType: Constants.REQUEST_TYPES.PATCH, uri: "/api/hacker/" + Constants.ROLE_CATEGORIES.ALL, - _id: mongoose.Types.ObjectId.createFromTime(123) + _id: mongoose.Types.ObjectId.createFromTime(123), }, patchAnyStatusById: { requestType: Constants.REQUEST_TYPES.PATCH, uri: "/api/hacker/status/" + Constants.ROLE_CATEGORIES.ALL, - _id: mongoose.Types.ObjectId.createFromTime(124) + _id: mongoose.Types.ObjectId.createFromTime(124), }, patchSelfStatusById: { requestType: Constants.REQUEST_TYPES.PATCH, uri: "/api/hacker/status/" + Constants.ROLE_CATEGORIES.SELF, - _id: mongoose.Types.ObjectId.createFromTime(125) + _id: mongoose.Types.ObjectId.createFromTime(125), }, patchSelfCheckInById: { requestType: Constants.REQUEST_TYPES.PATCH, uri: "/api/hacker/checkin/" + Constants.ROLE_CATEGORIES.SELF, - _id: mongoose.Types.ObjectId.createFromTime(126) + _id: mongoose.Types.ObjectId.createFromTime(126), }, patchAnyCheckInById: { requestType: Constants.REQUEST_TYPES.PATCH, uri: "/api/hacker/checkin/" + Constants.ROLE_CATEGORIES.ALL, - _id: mongoose.Types.ObjectId.createFromTime(127) + _id: mongoose.Types.ObjectId.createFromTime(127), }, patchSelfConfirmationById: { requestType: Constants.REQUEST_TYPES.PATCH, uri: "/api/hacker/confirmation/" + Constants.ROLE_CATEGORIES.SELF, - _id: mongoose.Types.ObjectId.createFromTime(128) + _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) + _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) + _id: mongoose.Types.ObjectId.createFromTime(130), }, patchAcceptHackerByArrayOfIds: { requestType: Constants.REQUEST_TYPES.PATCH, uri: "/api/hacker/batchAccept", - _id: mongoose.Types.ObjectId.createFromTime(165) + _id: mongoose.Types.ObjectId.createFromTime(165), }, postAnySendWeekOfEmail: { requestType: Constants.REQUEST_TYPES.POST, uri: "/api/hacker/email/weekOf/" + Constants.ROLE_CATEGORIES.ALL, - _id: mongoose.Types.ObjectId.createFromTime(131) + _id: mongoose.Types.ObjectId.createFromTime(131), }, postSelfSendWeekOfEmail: { requestType: Constants.REQUEST_TYPES.POST, uri: "/api/hacker/email/weekOf/" + Constants.ROLE_CATEGORIES.SELF, - _id: mongoose.Types.ObjectId.createFromTime(132) + _id: mongoose.Types.ObjectId.createFromTime(132), }, postAnySendDayOfEmail: { requestType: Constants.REQUEST_TYPES.POST, uri: "/api/hacker/email/dayOf/" + Constants.ROLE_CATEGORIES.ALL, - _id: mongoose.Types.ObjectId.createFromTime(133) + _id: mongoose.Types.ObjectId.createFromTime(133), }, postSelfSendDayOfEmail: { requestType: Constants.REQUEST_TYPES.POST, uri: "/api/hacker/email/dayOf/" + Constants.ROLE_CATEGORIES.SELF, - _id: mongoose.Types.ObjectId.createFromTime(134) - } + _id: mongoose.Types.ObjectId.createFromTime(134), + }, // }, // postDiscord: { // requestType: Constants.REQUEST_TYPES.POST, @@ -215,179 +215,189 @@ const travelRoutes = { getSelf: { requestType: Constants.REQUEST_TYPES.GET, uri: "/api/travel/self/", - _id: mongoose.Types.ObjectId.createFromTime(135) + _id: mongoose.Types.ObjectId.createFromTime(135), }, getSelfById: { requestType: Constants.REQUEST_TYPES.GET, uri: "/api/travel/" + Constants.ROLE_CATEGORIES.SELF, - _id: mongoose.Types.ObjectId.createFromTime(136) + _id: mongoose.Types.ObjectId.createFromTime(136), }, getAnyById: { requestType: Constants.REQUEST_TYPES.GET, uri: "/api/travel/" + Constants.ROLE_CATEGORIES.ALL, - _id: mongoose.Types.ObjectId.createFromTime(137) + _id: mongoose.Types.ObjectId.createFromTime(137), }, getSelfByEmail: { requestType: Constants.REQUEST_TYPES.GET, uri: "/api/travel/email/" + Constants.ROLE_CATEGORIES.SELF, - _id: mongoose.Types.ObjectId.createFromTime(138) + _id: mongoose.Types.ObjectId.createFromTime(138), }, getAnyByEmail: { requestType: Constants.REQUEST_TYPES.GET, uri: "/api/travel/email/" + Constants.ROLE_CATEGORIES.ALL, - _id: mongoose.Types.ObjectId.createFromTime(139) + _id: mongoose.Types.ObjectId.createFromTime(139), }, post: { requestType: Constants.REQUEST_TYPES.POST, uri: "/api/travel/", - _id: mongoose.Types.ObjectId.createFromTime(140) + _id: mongoose.Types.ObjectId.createFromTime(140), }, patchAnyStatusById: { requestType: Constants.REQUEST_TYPES.PATCH, uri: "/api/travel/status/" + Constants.ROLE_CATEGORIES.ALL, - _id: mongoose.Types.ObjectId.createFromTime(141) + _id: mongoose.Types.ObjectId.createFromTime(141), }, patchAnyOfferById: { requestType: Constants.REQUEST_TYPES.PATCH, uri: "/api/travel/offer/" + Constants.ROLE_CATEGORIES.ALL, - _id: mongoose.Types.ObjectId.createFromTime(142) - } + _id: mongoose.Types.ObjectId.createFromTime(142), + }, }; const sponsorRoutes = { getSelf: { requestType: Constants.REQUEST_TYPES.GET, uri: "/api/sponsor/self/", - _id: mongoose.Types.ObjectId.createFromTime(143) + _id: mongoose.Types.ObjectId.createFromTime(143), }, getSelfById: { requestType: Constants.REQUEST_TYPES.GET, uri: "/api/sponsor/" + Constants.ROLE_CATEGORIES.SELF, - _id: mongoose.Types.ObjectId.createFromTime(144) + _id: mongoose.Types.ObjectId.createFromTime(144), }, getAnyById: { requestType: Constants.REQUEST_TYPES.GET, uri: "/api/sponsor/" + Constants.ROLE_CATEGORIES.ALL, - _id: mongoose.Types.ObjectId.createFromTime(145) + _id: mongoose.Types.ObjectId.createFromTime(145), }, post: { requestType: Constants.REQUEST_TYPES.POST, uri: "/api/sponsor/", - _id: mongoose.Types.ObjectId.createFromTime(146) + _id: mongoose.Types.ObjectId.createFromTime(146), }, patchSelfById: { requestType: Constants.REQUEST_TYPES.PATCH, uri: "/api/sponsor/" + Constants.ROLE_CATEGORIES.SELF, - _id: mongoose.Types.ObjectId.createFromTime(147) + _id: mongoose.Types.ObjectId.createFromTime(147), }, patchAnyById: { requestType: Constants.REQUEST_TYPES.PATCH, uri: "/api/sponsor/" + Constants.ROLE_CATEGORIES.ALL, - _id: mongoose.Types.ObjectId.createFromTime(148) - } + _id: mongoose.Types.ObjectId.createFromTime(148), + }, }; const teamRoutes = { get: { requestType: Constants.REQUEST_TYPES.GET, uri: "/api/team/" + Constants.ROLE_CATEGORIES.ALL, - _id: mongoose.Types.ObjectId.createFromTime(149) + _id: mongoose.Types.ObjectId.createFromTime(149), }, post: { requestType: Constants.REQUEST_TYPES.POST, uri: "/api/team/", - _id: mongoose.Types.ObjectId.createFromTime(150) + _id: mongoose.Types.ObjectId.createFromTime(150), }, join: { requestType: Constants.REQUEST_TYPES.PATCH, uri: "/api/team/join/", - _id: mongoose.Types.ObjectId.createFromTime(151) + _id: mongoose.Types.ObjectId.createFromTime(151), }, patchSelfById: { requestType: Constants.REQUEST_TYPES.PATCH, uri: "/api/team/" + Constants.ROLE_CATEGORIES.SELF, - _id: mongoose.Types.ObjectId.createFromTime(152) + _id: mongoose.Types.ObjectId.createFromTime(152), }, patchAnyById: { requestType: Constants.REQUEST_TYPES.PATCH, uri: "/api/team/" + Constants.ROLE_CATEGORIES.ALL, - _id: mongoose.Types.ObjectId.createFromTime(153) + _id: mongoose.Types.ObjectId.createFromTime(153), }, leave: { requestType: Constants.REQUEST_TYPES.PATCH, uri: "/api/team/leave/", - _id: mongoose.Types.ObjectId.createFromTime(154) - } + _id: mongoose.Types.ObjectId.createFromTime(154), + }, }; const volunteerRoutes = { getSelfById: { requestType: Constants.REQUEST_TYPES.GET, uri: "/api/volunteer/" + Constants.ROLE_CATEGORIES.SELF, - _id: mongoose.Types.ObjectId.createFromTime(155) + _id: mongoose.Types.ObjectId.createFromTime(155), }, getAnyById: { requestType: Constants.REQUEST_TYPES.GET, uri: "/api/volunteer/" + Constants.ROLE_CATEGORIES.ALL, - _id: mongoose.Types.ObjectId.createFromTime(156) + _id: mongoose.Types.ObjectId.createFromTime(156), }, post: { requestType: Constants.REQUEST_TYPES.POST, uri: "/api/volunteer/", - _id: mongoose.Types.ObjectId.createFromTime(157) - } + _id: mongoose.Types.ObjectId.createFromTime(157), + }, }; const roleRoutes = { post: { requestType: Constants.REQUEST_TYPES.POST, uri: "/api/role/", - _id: mongoose.Types.ObjectId.createFromTime(158) - } + _id: mongoose.Types.ObjectId.createFromTime(158), + }, }; const searchRoutes = { get: { requestType: Constants.REQUEST_TYPES.GET, uri: "/api/search/", - _id: mongoose.Types.ObjectId.createFromTime(159) - } + _id: mongoose.Types.ObjectId.createFromTime(159), + }, }; const staffRoutes = { hackerStats: { requestType: Constants.REQUEST_TYPES.GET, uri: "/api/hacker/stats", - _id: mongoose.Types.ObjectId.createFromTime(160) + _id: mongoose.Types.ObjectId.createFromTime(160), }, postInvite: { requestType: Constants.REQUEST_TYPES.POST, uri: "/api/account/invite", - _id: mongoose.Types.ObjectId.createFromTime(161) + _id: mongoose.Types.ObjectId.createFromTime(161), }, getInvite: { requestType: Constants.REQUEST_TYPES.GET, uri: "/api/account/invite", - _id: mongoose.Types.ObjectId.createFromTime(162) + _id: mongoose.Types.ObjectId.createFromTime(162), }, postDiscord: { requestType: Constants.REQUEST_TYPES.POST, uri: "/api/hacker/discord", - _id: mongoose.Types.ObjectId.createFromTime(167) - } + _id: mongoose.Types.ObjectId.createFromTime(167), + }, + postAutomatedStatusEmails: { + requestType: Constants.REQUEST_TYPES.POST, + uri: "/api/email/automated/status/:status", + _id: mongoose.Types.ObjectId.createFromTime(168), + }, + getAutomatedStatusEmailCount: { + requestType: Constants.REQUEST_TYPES.GET, + uri: "/api/email/automated/status/:status/count", + _id: mongoose.Types.ObjectId.createFromTime(169), + }, }; const settingsRoutes = { getSettings: { requestType: Constants.REQUEST_TYPES.GET, uri: "/api/settings", - _id: mongoose.Types.ObjectId.createFromTime(163) + _id: mongoose.Types.ObjectId.createFromTime(163), }, patchSettings: { requestType: Constants.REQUEST_TYPES.PATCH, uri: "/api/settings", - _id: mongoose.Types.ObjectId.createFromTime(164) - } + _id: mongoose.Types.ObjectId.createFromTime(164), + }, }; const allRoutes = { @@ -401,7 +411,7 @@ const allRoutes = { Role: roleRoutes, Search: searchRoutes, Settings: settingsRoutes, - Staff: staffRoutes + Staff: staffRoutes, }; /** @@ -449,5 +459,5 @@ module.exports = { settingsRoutes: settingsRoutes, staffRoutes: staffRoutes, allRoutes: allRoutes, - listAllRoutes: listAllRoutes + listAllRoutes: listAllRoutes, }; diff --git a/controllers/email.controller.js b/controllers/email.controller.js new file mode 100644 index 00000000..25857cb2 --- /dev/null +++ b/controllers/email.controller.js @@ -0,0 +1,39 @@ +"use strict"; + +const Constants = { + Success: require("../constants/success.constant"), + Error: require("../constants/error.constant"), +}; + +/** + * @function getStatusCount + * @param {{body: {count: number}}} req + * @param {*} res + * @return {JSON} Success status and count + * @description Returns the count of hackers with specified status + */ +function getStatusCount(req, res) { + return res.status(200).json({ + message: "Successfully retrieved count", + data: { count: req.body.count }, + }); +} + +/** + * @function sendAutomatedStatusEmails + * @param {{body: {results: {success: number, failed: number}}}} req + * @param {*} res + * @return {JSON} Success status and email results + * @description Returns the results of sending automated status emails + */ +function sendAutomatedStatusEmails(req, res) { + return res.status(200).json({ + message: "Successfully sent emails", + data: req.body.results, + }); +} + +module.exports = { + getStatusCount, + sendAutomatedStatusEmails, +}; diff --git a/middlewares/email.middleware.js b/middlewares/email.middleware.js new file mode 100644 index 00000000..3ac0b573 --- /dev/null +++ b/middlewares/email.middleware.js @@ -0,0 +1,81 @@ +"use strict"; + +const Services = { + AutomatedEmail: require("../services/automatedEmails.service"), +}; +const Constants = { + Error: require("../constants/error.constant"), + General: require("../constants/general.constant"), +}; + +/** + * Middleware to validate status parameter + * @param {{params: {status: string}}} req + * @param {*} res + * @param {(err?)=>void} next + */ +function validateStatus(req, res, next) { + const { status } = req.params; + const validStatuses = [ + Constants.General.HACKER_STATUS_ACCEPTED, + Constants.General.HACKER_STATUS_DECLINED, + ]; + + if (!validStatuses.includes(status)) { + return res.status(400).json({ + message: "Invalid status", + data: {}, + }); + } + + next(); +} + +/** + * Middleware to get count of hackers with specified status + * @param {{params: {status: string}}} req + * @param {*} res + * @param {(err?)=>void} next + */ +async function getStatusCount(req, res, next) { + const { status } = req.params; + + try { + const count = await Services.AutomatedEmail.getStatusCount(status); + req.body.count = count; + next(); + } catch (err) { + return res.status(500).json({ + message: err.message, + data: {}, + }); + } +} + +/** + * Middleware to send automated status emails + * @param {{params: {status: string}}} req + * @param {*} res + * @param {(err?)=>void} next + */ +async function sendAutomatedStatusEmails(req, res, next) { + const { status } = req.params; + + try { + const results = + await Services.AutomatedEmail.sendAutomatedStatusEmails(status); + req.body.results = results; + next(); + } catch (err) { + return res.status(500).json({ + message: err.message, + data: {}, + }); + } +} + +module.exports = { + validateStatus, + getStatusCount, + sendAutomatedStatusEmails, +}; diff --git a/routes/api/emails.js b/routes/api/emails.js new file mode 100644 index 00000000..3e55b5b5 --- /dev/null +++ b/routes/api/emails.js @@ -0,0 +1,72 @@ +"use strict"; +const express = require("express"); + +const Middleware = { + Auth: require("../../middlewares/auth.middleware"), + Email: require("../../middlewares/email.middleware"), +}; + +const Controllers = { + Email: require("../../controllers/email.controller"), +}; + +module.exports = { + activate: function (apiRouter) { + const automatedEmailRouter = express.Router(); + + /** + * @api {get} /email/automated/status/:status/count Get count of hackers with specified status + * @apiName getStatusEmailCount + * @apiGroup Email + * @apiVersion 0.0.8 + * + * @apiParam {string} status Status of hackers to count (Accepted/Declined) + * + * @apiSuccess {string} message Success message + * @apiSuccess {object} data Contains count of hackers + * @apiSuccessExample {object} Success-Response: + * { + * "message": "Successfully retrieved count", + * "data": { + * "count": 50 + * } + * } + */ + automatedEmailRouter.route("/automated/status/:status/count").get( + Middleware.Auth.ensureAuthenticated(), + // Middleware.Auth.ensureAuthorized(), + Middleware.Email.validateStatus, + Middleware.Email.getStatusCount, + Controllers.Email.getStatusCount, + ); + + /** + * @api {post} /email/automated/status/:status Send emails to all hackers with specified status + * @apiName sendAutomatedStatusEmails + * @apiGroup Email + * @apiVersion 0.0.8 + * + * @apiParam {string} status Status of hackers to email (Accepted/Declined) + * + * @apiSuccess {string} message Success message + * @apiSuccess {object} data Contains counts of successful and failed emails + * @apiSuccessExample {object} Success-Response: + * { + * "message": "Successfully sent emails", + * "data": { + * "success": 50, + * "failed": 2 + * } + * } + */ + automatedEmailRouter.route("/automated/status/:status").post( + Middleware.Auth.ensureAuthenticated(), + // Middleware.Auth.ensureAuthorized(), + Middleware.Email.validateStatus, + Middleware.Email.sendAutomatedStatusEmails, + Controllers.Email.sendAutomatedStatusEmails, + ); + + apiRouter.use("/email", automatedEmailRouter); + }, +}; diff --git a/services/automatedEmails.service.js b/services/automatedEmails.service.js new file mode 100644 index 00000000..815a5534 --- /dev/null +++ b/services/automatedEmails.service.js @@ -0,0 +1,82 @@ +"use strict"; + +const Services = { + Email: require("./email.service"), + Hacker: require("./hacker.service"), + Logger: require("./logger.service"), +}; + +const TAG = "[AutomatedEmail.Service]"; + +class AutomatedEmailService { + /** + * Get count of hackers with the given status + * @param {string} status - "Accepted", "Declined" + * @returns {Promise} Count of hackers with the status + */ + async getStatusCount(status) { + try { + const hackers = await Services.Hacker.findByStatus(status); + if (!hackers || !Array.isArray(hackers)) { + return 0; + } + return hackers.length; + } catch (err) { + Services.Logger.error(`${TAG} Error in getStatusCount: ${err}`); + throw err; + } + } + + /** + * Send status emails to all hackers with the given status + * @param {string} status - "Accepted", "Declined" + * @returns {Promise<{success: number, failed: number}>} + */ + async sendAutomatedStatusEmails(status) { + const results = { success: 0, failed: 0 }; + try { + const hackers = await Services.Hacker.findByStatus(status); + + if (!hackers || !Array.isArray(hackers)) { + throw new Error( + `Expected array from findByStatus(${status}), got ${typeof hackers}`, + ); + } + + const emailPromises = hackers.map(async (hacker) => { + try { + await new Promise((resolve, reject) => { + Services.Email.sendStatusUpdate( + hacker.accountId.firstName, + hacker.accountId.email, + status, + (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }, + ); + }); + results.success++; + } catch (err) { + Services.Logger.error( + `${TAG} Failed to send ${status} email to ${hacker.accountId.email}: ${err}`, + ); + results.failed++; + } + }); + + await Promise.all(emailPromises); + return results; + } catch (err) { + Services.Logger.error( + `${TAG} Error in sendAutomatedStatusEmails: ${err}`, + ); + throw err; + } + } +} + +module.exports = new AutomatedEmailService(); diff --git a/services/email.service.js b/services/email.service.js index af9abde1..f8c15e1a 100644 --- a/services/email.service.js +++ b/services/email.service.js @@ -23,18 +23,19 @@ class EmailService { //Silence all actual emails if we're testing mailData.mailSettings = { sandboxMode: { - enable: true - } + enable: true, + }, }; } - return client.send(mailData, false) - .then(response => { - callback() - return response - }) - .catch(error => { - callback(error) + return client + .send(mailData, false) + .then((response) => { + callback(); + return response; }) + .catch((error) => { + callback(error); + }); } /** * Send separate emails to the list of users in mailData @@ -42,14 +43,15 @@ class EmailService { * @param {(err?)=>void} callback */ sendMultiple(mailData, callback = () => {}) { - return client.sendMultiple(mailData) - .then(response => { - callback() + return client + .sendMultiple(mailData) + .then((response) => { + callback(); return response; }) - .catch(error => { - callback(error) - }) + .catch((error) => { + callback(error); + }); } /** * Send email with ticket. @@ -61,17 +63,17 @@ class EmailService { sendWeekOfEmail(firstName, recipient, ticket, callback) { const handlebarsPath = path.join( __dirname, - `../assets/email/Ticket.hbs` + `../assets/email/Ticket.hbs`, ); const html = this.renderEmail(handlebarsPath, { firstName: firstName, - ticket: ticket + ticket: ticket, }); const mailData = { to: recipient, from: process.env.NO_REPLY_EMAIL, subject: Constants.EMAIL_SUBJECTS[Constants.WEEK_OF], - html: html + html: html, }; this.send(mailData).then((response) => { if ( @@ -93,16 +95,16 @@ class EmailService { sendDayOfEmail(firstName, recipient, callback) { const handlebarsPath = path.join( __dirname, - `../assets/email/Welcome.hbs` + `../assets/email/Welcome.hbs`, ); const html = this.renderEmail(handlebarsPath, { - firstName: firstName + firstName: firstName, }); const mailData = { to: recipient, from: process.env.NO_REPLY_EMAIL, subject: Constants.EMAIL_SUBJECTS[Constants.WEEK_OF], - html: html + html: html, }; this.send(mailData).then((response) => { if ( @@ -119,15 +121,15 @@ class EmailService { sendStatusUpdate(firstName, recipient, status, callback) { const handlebarsPath = path.join( __dirname, - `../assets/email/statusEmail/${status}.hbs` + `../assets/email/statusEmail/${status}.hbs`, ); const mailData = { to: recipient, from: process.env.NO_REPLY_EMAIL, subject: Constants.EMAIL_SUBJECTS[status], html: this.renderEmail(handlebarsPath, { - firstName: firstName - }) + firstName: firstName, + }), }; this.send(mailData).then((response) => { if ( diff --git a/services/hacker.service.js b/services/hacker.service.js index 37531700..d63e0bc4 100644 --- a/services/hacker.service.js +++ b/services/hacker.service.js @@ -37,10 +37,14 @@ function updateOne(id, hackerDetails) { const TAG = `[Hacker Service # update ]:`; const query = { - _id: id + _id: id, }; - return logger.logUpdate(TAG, "hacker", Hacker.findOneAndUpdate(query, hackerDetails, { new: true })); + return logger.logUpdate( + TAG, + "hacker", + Hacker.findOneAndUpdate(query, hackerDetails, { new: true }), + ); } /** @@ -67,7 +71,12 @@ async function findIds(queries) { let ids = []; for (const query of queries) { - let currId = await logger.logQuery(TAG, "hacker", query, Hacker.findOne(query, "_id")); + let currId = await logger.logQuery( + TAG, + "hacker", + query, + Hacker.findOne(query, "_id"), + ); ids.push(currId); } return ids; @@ -81,21 +90,45 @@ async function findIds(queries) { function findByAccountId(accountId) { const TAG = `[ Hacker Service # findByAccountId ]:`; const query = { - accountId: accountId + accountId: accountId, }; return logger.logUpdate(TAG, "hacker", Hacker.findOne(query)); } +/** + * Find all hackers with a specific status + * @param {string} status - The status to search for (e.g., "Accepted", "Declined") + * @return {Promise>} Array of hacker documents with the specified status + */ +async function findByStatus(status) { + const TAG = `[ Hacker Service # findByStatus ]:`; + const query = { status: status }; + + const result = await logger.logQuery( + TAG, + "hacker", + query, + Hacker.find(query).populate("accountId"), + ); + // Always return an array + if (!Array.isArray(result)) { + return []; + } + return result; +} + async function getStatsAllHackersCached() { const TAG = `[ hacker Service # getStatsAll ]`; if (cache.get(Constants.CACHE_KEY_STATS) !== null) { logger.info(`${TAG} Getting cached stats`); return cache.get(Constants.CACHE_KEY_STATS); } - const allHackers = await logger.logUpdate(TAG, "hacker", Hacker.find({})).populate({ - path: "accountId" - }); + const allHackers = await logger + .logUpdate(TAG, "hacker", Hacker.find({})) + .populate({ + path: "accountId", + }); cache.put(Constants.CACHE_KEY_STATS, stats, Constants.CACHE_TIMEOUT_STATS); //set a time-out of 5 minutes return getStats(allHackers); } @@ -106,7 +139,7 @@ async function getStatsAllHackersCached() { */ async function generateQRCode(str) { const response = await QRCode.toDataURL(str, { - scale: 4 + scale: 4, }); return response; } @@ -139,7 +172,7 @@ function getStats(hackers) { dietaryRestrictions: {}, shirtSize: {}, age: {}, - applicationDate: {} + applicationDate: {}, }; hackers.forEach((hacker) => { @@ -213,7 +246,9 @@ function getStats(hackers) { // const age = hacker.accountId.getAge(); // stats.age[age] = stats.age[age] ? stats.age[age] + 1 : 1; - stats.age[hacker.accountId.age] = stats.age[hacker.accountId.age] ? stats.age[age] + 1 : 1; + stats.age[hacker.accountId.age] = stats.age[hacker.accountId.age] + ? stats.age[age] + 1 + : 1; const applicationDate = hacker._id .getTimestamp() // @@ -235,8 +270,9 @@ module.exports = { updateOne: updateOne, findIds: findIds, findByAccountId: findByAccountId, + findByStatus: findByStatus, getStats: getStats, getStatsAllHackersCached: getStatsAllHackersCached, generateQRCode: generateQRCode, - generateHackerViewLink: generateHackerViewLink + generateHackerViewLink: generateHackerViewLink, };