From 3280a1d9ded89a43f4cf6fc8c9c8787978110529 Mon Sep 17 00:00:00 2001 From: janekhuong Date: Sat, 18 Oct 2025 12:33:57 -0400 Subject: [PATCH 01/11] Update email templates with tentative dates --- assets/email/statusEmail/Accepted.hbs | 4 ++-- assets/email/statusEmail/Applied.hbs | 4 ++-- assets/email/statusEmail/Confirmed.hbs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) 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!

From df47e20266a6dd8170e9984dd903a8b1cfecf3f4 Mon Sep 17 00:00:00 2001 From: janekhuong Date: Sat, 18 Oct 2025 12:36:22 -0400 Subject: [PATCH 02/11] Create service for sending automated status emails --- services/automatedEmails.service.js | 60 +++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 services/automatedEmails.service.js diff --git a/services/automatedEmails.service.js b/services/automatedEmails.service.js new file mode 100644 index 00000000..d950b995 --- /dev/null +++ b/services/automatedEmails.service.js @@ -0,0 +1,60 @@ +"use strict"; + +const Services = { + Email: require("./email.service"), + Hacker: require("./hacker.service"), + Logger: require("./logger.service"), +}; + +const TAG = "[AutomatedEmail.Service]"; + +class AutomatedEmailService { + /** + * Send status emails to all hackers with the given status + * @param {string} status - "Accepted", "Declined" + * @returns {Promise<{success: number, failed: number}>} Number of successful and failed emails + */ + async sendAutomatedStatusEmails(status) { + const results = { success: 0, failed: 0 }; + try { + // Get all hackers with the specified status + const hackers = await Services.Hacker.findByStatus(status); + + // Send emails in parallel + 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(); From 7c8752a5f040be39815eacb668207653fa1b344b Mon Sep 17 00:00:00 2001 From: janekhuong Date: Sat, 18 Oct 2025 12:38:23 -0400 Subject: [PATCH 03/11] Created function to find hacker by their status --- services/hacker.service.js | 53 ++++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/services/hacker.service.js b/services/hacker.service.js index 37531700..8a705b99 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,40 @@ 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 + */ +function findByStatus(status) { + const TAG = `[ Hacker Service # findByStatus ]:`; + const query = { status: status }; + + return logger.logQuery( + TAG, + "hacker", + query, + Hacker.find(query).populate("accountId"), + ); +} + 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 +134,7 @@ async function getStatsAllHackersCached() { */ async function generateQRCode(str) { const response = await QRCode.toDataURL(str, { - scale: 4 + scale: 4, }); return response; } @@ -139,7 +167,7 @@ function getStats(hackers) { dietaryRestrictions: {}, shirtSize: {}, age: {}, - applicationDate: {} + applicationDate: {}, }; hackers.forEach((hacker) => { @@ -213,7 +241,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 +265,9 @@ module.exports = { updateOne: updateOne, findIds: findIds, findByAccountId: findByAccountId, + findByStatus: findByStatus, getStats: getStats, getStatsAllHackersCached: getStatsAllHackersCached, generateQRCode: generateQRCode, - generateHackerViewLink: generateHackerViewLink + generateHackerViewLink: generateHackerViewLink, }; From 22ef0eed8a76119c25eefbef12000115c1675d08 Mon Sep 17 00:00:00 2001 From: janekhuong Date: Sat, 18 Oct 2025 12:39:36 -0400 Subject: [PATCH 04/11] API routing for automated emails --- routes/api/emails.js | 75 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 routes/api/emails.js diff --git a/routes/api/emails.js b/routes/api/emails.js new file mode 100644 index 00000000..a6f77536 --- /dev/null +++ b/routes/api/emails.js @@ -0,0 +1,75 @@ +"use strict"; +const express = require("express"); +const Services = { + AutomatedEmail: require("../../services/automatedEmails.service"), +}; +const Middleware = { + Auth: require("../../middlewares/auth.middleware"), +}; +const PROJECT_CONSTANTS = require("../../constants/general.constant"); +const Constants = { + STATUSES: [ + PROJECT_CONSTANTS.HACKER_STATUS_ACCEPTED, + PROJECT_CONSTANTS.HACKER_STATUS_DECLINED, + ], +}; + +module.exports = { + activate: function (apiRouter) { + const automatedEmailRouter = express.Router(); + + /** + * @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(), + async (req, res) => { + const { status } = req.params; + + if (!Constants.STATUSES.includes(status)) { + return res.status(400).json({ + message: "Invalid status", + data: {}, + }); + } + + try { + const results = + await Services.AutomatedEmail.sendAutomatedStatusEmails( + status, + ); + return res.status(200).json({ + message: "Successfully sent emails", + data: results, + }); + } catch (err) { + return res.status(500).json({ + message: err.message, + data: {}, + }); + } + }, + ); + + apiRouter.use("/email", automatedEmailRouter); + }, +}; From 4bc9f5ffb34e528dd2d6a8cf6c6c24854963e937 Mon Sep 17 00:00:00 2001 From: janekhuong Date: Sat, 18 Oct 2025 12:40:27 -0400 Subject: [PATCH 05/11] Cleaner code --- services/email.service.js | 50 ++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 24 deletions(-) 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 ( From 54a66c44eba1cbc737babddafb367ee0718d5af4 Mon Sep 17 00:00:00 2001 From: janekhuong Date: Sun, 19 Oct 2025 22:44:02 -0400 Subject: [PATCH 06/11] API routing for getStatusCount function --- app.js | 31 +++-- constants/routes.constant.js | 172 +++++++++++++++------------- routes/api/emails.js | 105 ++++++++++++----- services/automatedEmails.service.js | 28 ++++- services/hacker.service.js | 9 +- 5 files changed, 213 insertions(+), 132 deletions(-) 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/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/routes/api/emails.js b/routes/api/emails.js index a6f77536..cd74f5cc 100644 --- a/routes/api/emails.js +++ b/routes/api/emails.js @@ -18,6 +18,53 @@ 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(), + async (req, res) => { + const { status } = req.params; + + if (!Constants.STATUSES.includes(status)) { + return res.status(400).json({ + message: "Invalid status", + data: {}, + }); + } + + try { + const count = + await Services.AutomatedEmail.getStatusCount(status); + return res.status(200).json({ + message: "Successfully retrieved count", + data: { count }, + }); + } catch (err) { + return res.status(500).json({ + message: err.message, + data: {}, + }); + } + }, + ); + /** * @api {post} /email/automated/status/:status Send emails to all hackers with specified status * @apiName sendAutomatedStatusEmails @@ -37,38 +84,36 @@ module.exports = { * } * } */ - automatedEmailRouter - .route("/automated/status/:status") - .post( - Middleware.Auth.ensureAuthenticated(), - Middleware.Auth.ensureAuthorized(), - async (req, res) => { - const { status } = req.params; + automatedEmailRouter.route("/automated/status/:status").post( + Middleware.Auth.ensureAuthenticated(), + // Middleware.Auth.ensureAuthorized(), + async (req, res) => { + const { status } = req.params; - if (!Constants.STATUSES.includes(status)) { - return res.status(400).json({ - message: "Invalid status", - data: {}, - }); - } + if (!Constants.STATUSES.includes(status)) { + return res.status(400).json({ + message: "Invalid status", + data: {}, + }); + } - try { - const results = - await Services.AutomatedEmail.sendAutomatedStatusEmails( - status, - ); - return res.status(200).json({ - message: "Successfully sent emails", - data: results, - }); - } catch (err) { - return res.status(500).json({ - message: err.message, - data: {}, - }); - } - }, - ); + try { + const results = + await Services.AutomatedEmail.sendAutomatedStatusEmails( + status, + ); + return res.status(200).json({ + message: "Successfully sent emails", + data: results, + }); + } catch (err) { + return res.status(500).json({ + message: err.message, + data: {}, + }); + } + }, + ); apiRouter.use("/email", automatedEmailRouter); }, diff --git a/services/automatedEmails.service.js b/services/automatedEmails.service.js index d950b995..815a5534 100644 --- a/services/automatedEmails.service.js +++ b/services/automatedEmails.service.js @@ -9,18 +9,40 @@ const Services = { 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}>} Number of successful and failed emails + * @returns {Promise<{success: number, failed: number}>} */ async sendAutomatedStatusEmails(status) { const results = { success: 0, failed: 0 }; try { - // Get all hackers with the specified status const hackers = await Services.Hacker.findByStatus(status); - // Send emails in parallel + 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) => { diff --git a/services/hacker.service.js b/services/hacker.service.js index 8a705b99..d63e0bc4 100644 --- a/services/hacker.service.js +++ b/services/hacker.service.js @@ -101,16 +101,21 @@ function findByAccountId(accountId) { * @param {string} status - The status to search for (e.g., "Accepted", "Declined") * @return {Promise>} Array of hacker documents with the specified status */ -function findByStatus(status) { +async function findByStatus(status) { const TAG = `[ Hacker Service # findByStatus ]:`; const query = { status: status }; - return logger.logQuery( + 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() { From 050f443bee9647a69941c1433e5d46f76dfe614a Mon Sep 17 00:00:00 2001 From: janekhuong Date: Wed, 29 Oct 2025 20:48:52 -0400 Subject: [PATCH 07/11] Added emails.js logic to middleware and controller files --- controllers/email.controller.js | 39 ++++++++++++++++ middlewares/email.middleware.js | 81 +++++++++++++++++++++++++++++++++ routes/api/emails.js | 70 +++++----------------------- 3 files changed, 131 insertions(+), 59 deletions(-) create mode 100644 controllers/email.controller.js create mode 100644 middlewares/email.middleware.js 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 index cd74f5cc..3e55b5b5 100644 --- a/routes/api/emails.js +++ b/routes/api/emails.js @@ -1,17 +1,13 @@ "use strict"; const express = require("express"); -const Services = { - AutomatedEmail: require("../../services/automatedEmails.service"), -}; + const Middleware = { Auth: require("../../middlewares/auth.middleware"), + Email: require("../../middlewares/email.middleware"), }; -const PROJECT_CONSTANTS = require("../../constants/general.constant"); -const Constants = { - STATUSES: [ - PROJECT_CONSTANTS.HACKER_STATUS_ACCEPTED, - PROJECT_CONSTANTS.HACKER_STATUS_DECLINED, - ], + +const Controllers = { + Email: require("../../controllers/email.controller"), }; module.exports = { @@ -39,30 +35,9 @@ module.exports = { automatedEmailRouter.route("/automated/status/:status/count").get( Middleware.Auth.ensureAuthenticated(), // Middleware.Auth.ensureAuthorized(), - async (req, res) => { - const { status } = req.params; - - if (!Constants.STATUSES.includes(status)) { - return res.status(400).json({ - message: "Invalid status", - data: {}, - }); - } - - try { - const count = - await Services.AutomatedEmail.getStatusCount(status); - return res.status(200).json({ - message: "Successfully retrieved count", - data: { count }, - }); - } catch (err) { - return res.status(500).json({ - message: err.message, - data: {}, - }); - } - }, + Middleware.Email.validateStatus, + Middleware.Email.getStatusCount, + Controllers.Email.getStatusCount, ); /** @@ -87,32 +62,9 @@ module.exports = { automatedEmailRouter.route("/automated/status/:status").post( Middleware.Auth.ensureAuthenticated(), // Middleware.Auth.ensureAuthorized(), - async (req, res) => { - const { status } = req.params; - - if (!Constants.STATUSES.includes(status)) { - return res.status(400).json({ - message: "Invalid status", - data: {}, - }); - } - - try { - const results = - await Services.AutomatedEmail.sendAutomatedStatusEmails( - status, - ); - return res.status(200).json({ - message: "Successfully sent emails", - data: results, - }); - } catch (err) { - return res.status(500).json({ - message: err.message, - data: {}, - }); - } - }, + Middleware.Email.validateStatus, + Middleware.Email.sendAutomatedStatusEmails, + Controllers.Email.sendAutomatedStatusEmails, ); apiRouter.use("/email", automatedEmailRouter); From 67fcb7e67c5564070e2f0cec7766838b801758a0 Mon Sep 17 00:00:00 2001 From: janekhuong Date: Fri, 7 Nov 2025 22:10:23 -0500 Subject: [PATCH 08/11] fixed authorization bug --- routes/api/emails.js | 19 +++++++++++-------- services/auth.service.js | 34 ++++++++++++++++++++++++---------- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/routes/api/emails.js b/routes/api/emails.js index 3e55b5b5..f08eb582 100644 --- a/routes/api/emails.js +++ b/routes/api/emails.js @@ -34,7 +34,7 @@ module.exports = { */ automatedEmailRouter.route("/automated/status/:status/count").get( Middleware.Auth.ensureAuthenticated(), - // Middleware.Auth.ensureAuthorized(), + Middleware.Auth.ensureAuthorized(), Middleware.Email.validateStatus, Middleware.Email.getStatusCount, Controllers.Email.getStatusCount, @@ -59,13 +59,16 @@ module.exports = { * } * } */ - automatedEmailRouter.route("/automated/status/:status").post( - Middleware.Auth.ensureAuthenticated(), - // Middleware.Auth.ensureAuthorized(), - Middleware.Email.validateStatus, - Middleware.Email.sendAutomatedStatusEmails, - Controllers.Email.sendAutomatedStatusEmails, - ); + + 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/auth.service.js b/services/auth.service.js index 5b4799d9..5977e2f7 100644 --- a/services/auth.service.js +++ b/services/auth.service.js @@ -8,9 +8,9 @@ module.exports = { emailAndPassStrategy: new LocalStrategy( { usernameField: "email", - passwordField: "password" + passwordField: "password", }, - function(email, password, done) { + function (email, password, done) { email = email.toLowerCase(); Account.getAccountIfValid(email, password).then( (account) => { @@ -22,29 +22,29 @@ module.exports = { }, (reason) => { done(reason, false); - } + }, ); - } + }, ), /** * Takes as input the id of the user. If the user id exists, it passes the user object to the callback * (done). The two arguments of the callback must be (failureReason, userObject). If there is no user, * then the userObject will be undefined. */ - deserializeUser: function(id, done) { + deserializeUser: function (id, done) { Account.findById(id).then( (user) => { done(null, user); }, (reason) => { done(reason); - } + }, ); }, - serializeUser: function(user, done) { + serializeUser: function (user, done) { done(null, user.id); }, - ensureAuthorized: ensureAuthorized + ensureAuthorized: ensureAuthorized, }; /** @@ -125,10 +125,13 @@ async function ensureAuthorized(req, findByIdFns) { currentlyValid = await verifySelfCase( findByIdFns[findByParamCount], splitPath[i], - req.user.id + req.user.id, ); findByParamCount += 1; break; + case ":status": + currentlyValid = true; + break; default: currentlyValid = false; break; @@ -156,12 +159,23 @@ async function ensureAuthorized(req, findByIdFns) { */ function verifyParamsFunctions(params, idFns) { let numParams = Object.values(params).length; + let validRoute = true; if (numParams === 0) { + // No params - should have no functions or undefined functions validRoute = !idFns || idFns.length === 0; } else { - validRoute = numParams === idFns.length; + // Has params - if no functions provided (undefined), assume general params like :status + if (!idFns) { + console.log( + `[Auth Service] Allowing general parameters (no validation functions needed)`, + ); + validRoute = true; + } else { + // Functions provided - must match param count + validRoute = numParams === idFns.length; + } } return validRoute; From e3146f0c86dcdead6c43e74b931d7453555b2704 Mon Sep 17 00:00:00 2001 From: Mika Vohl Date: Sat, 8 Nov 2025 16:32:57 -0500 Subject: [PATCH 09/11] Added hackboard role support --- constants/general.constant.js | 9 +++++++- constants/role.constant.js | 30 +++++++++++++++++++++++++ middlewares/auth.middleware.js | 5 +++++ services/accountConfirmation.service.js | 2 ++ tests/util/roleBinding.test.util.js | 3 +++ 5 files changed, 48 insertions(+), 1 deletion(-) diff --git a/constants/general.constant.js b/constants/general.constant.js index 5e23d2fe..a3c17064 100644 --- a/constants/general.constant.js +++ b/constants/general.constant.js @@ -72,6 +72,7 @@ const SAMPLE_DIET_RESTRICTIONS = [ const HACKER = "Hacker"; const VOLUNTEER = "Volunteer"; const STAFF = "Staff"; +const HACKBOARD = "Hackboard"; const SPONSOR = "Sponsor"; const SPONSOR_T1 = "SponsorT1"; @@ -109,8 +110,9 @@ POST_ROLES[SPONSOR_T4] = "postSponsor"; POST_ROLES[SPONSOR_T5] = "postSponsor"; POST_ROLES[VOLUNTEER] = "postVolunteer"; POST_ROLES[STAFF] = "postStaff"; +POST_ROLES[HACKBOARD] = "Hackboard"; -const USER_TYPES = [HACKER, VOLUNTEER, STAFF, SPONSOR]; +const USER_TYPES = [HACKER, VOLUNTEER, STAFF, HACKBOARD, SPONSOR]; const SPONSOR_TIERS = [ SPONSOR_T1, SPONSOR_T2, @@ -122,6 +124,7 @@ const EXTENDED_USER_TYPES = [ HACKER, VOLUNTEER, STAFF, + HACKBOARD, SPONSOR_T1, SPONSOR_T2, SPONSOR_T3, @@ -168,6 +171,9 @@ CREATE_ACC_EMAIL_SUBJECTS[ CREATE_ACC_EMAIL_SUBJECTS[ STAFF ] = `You've been invited to create a staff account for ${HACKATHON_NAME}`; +CREATE_ACC_EMAIL_SUBJECTS[ + HACKBOARD +] = `You've been invited to create a hackboard account for ${HACKATHON_NAME}`; const CACHE_TIMEOUT_STATS = 5 * 60 * 1000; const CACHE_KEY_STATS = "hackerStats"; @@ -211,6 +217,7 @@ module.exports = { SPONSOR: SPONSOR, VOLUNTEER: VOLUNTEER, STAFF: STAFF, + HACKBOARD: HACKBOARD, SPONSOR_T1: SPONSOR_T1, SPONSOR_T2: SPONSOR_T2, SPONSOR_T3: SPONSOR_T3, diff --git a/constants/role.constant.js b/constants/role.constant.js index 87cb3eba..ca8047e3 100644 --- a/constants/role.constant.js +++ b/constants/role.constant.js @@ -20,12 +20,31 @@ const accountRole = { ] }; +const hackboardRestrictedRoutes = [ // hackboard permissions is all staff routes minus these routes + Constants.Routes.hackerRoutes.postAnySendWeekOfEmail, + Constants.Routes.hackerRoutes.postSelfSendWeekOfEmail, + Constants.Routes.hackerRoutes.postAnySendDayOfEmail, + Constants.Routes.hackerRoutes.postSelfSendDayOfEmail, + Constants.Routes.hackerRoutes.patchAcceptHackerById, + Constants.Routes.hackerRoutes.patchAcceptHackerByEmail, + Constants.Routes.hackerRoutes.patchAcceptHackerByArrayOfIds, + Constants.Routes.hackerRoutes.patchAnyStatusById, + Constants.Routes.settingsRoutes.getSettings, + Constants.Routes.settingsRoutes.patchSettings +]; + const adminRole = { _id: mongoose.Types.ObjectId.createFromTime(1), name: Constants.General.STAFF, routes: Constants.Routes.listAllRoutes() }; +const hackboardRole = { + _id: mongoose.Types.ObjectId.createFromTime(9), + name: "Hackboard", + routes: createHackboardRoutes() +}; + const hackerRole = { _id: mongoose.Types.ObjectId.createFromTime(2), name: Constants.General.HACKER, @@ -143,6 +162,15 @@ const singularRoles = createAllSingularRoles(); const allRolesObject = createAllRoles(); const allRolesArray = Object.values(allRolesObject); +function createHackboardRoutes() { + const restrictedRouteIds = new Set( + hackboardRestrictedRoutes.map((route) => route._id.toString()) + ); + return Constants.Routes.listAllRoutes().filter((route) => { + return !restrictedRouteIds.has(route._id.toString()); + }); +} + /** * Creates all the roles that are of a specific uri and request type * @return {Role[]} @@ -185,6 +213,7 @@ function createAllRoles() { let allRolesObject = { accountRole: accountRole, adminRole: adminRole, + hackboardRole: hackboardRole, hackerRole: hackerRole, volunteerRole: volunteerRole, sponsorT1Role: sponsorT1Role, @@ -208,6 +237,7 @@ function createAllRoles() { module.exports = { accountRole: accountRole, adminRole: adminRole, + hackboardRole: hackboardRole, hackerRole: hackerRole, volunteerRole: volunteerRole, sponsorT1Role: sponsorT1Role, diff --git a/middlewares/auth.middleware.js b/middlewares/auth.middleware.js index eeb898c7..ce3a8793 100644 --- a/middlewares/auth.middleware.js +++ b/middlewares/auth.middleware.js @@ -477,6 +477,11 @@ async function addCreationRoleBindings(req, res, next) { req.body.account.id, Constants.Role.adminRole.name ); + } else if (req.body.account.accountType === Constants.General.HACKBOARD) { + await Services.RoleBinding.createRoleBindingByRoleName( + req.body.account.id, + Constants.Role.hackboardRole.name + ); } else { // Get the default role for the account type given const roleName = diff --git a/services/accountConfirmation.service.js b/services/accountConfirmation.service.js index f27bff94..028e0615 100644 --- a/services/accountConfirmation.service.js +++ b/services/accountConfirmation.service.js @@ -172,6 +172,8 @@ function generateAccountInvitationEmail(address, receiverEmail, type, token) { emailSubject = Constants.CREATE_ACC_EMAIL_SUBJECTS[Constants.SPONSOR]; } else if (type === Constants.STAFF) { emailSubject = Constants.CREATE_ACC_EMAIL_SUBJECTS[Constants.STAFF]; + } else if (type === Constants.HACKBOARD) { + emailSubject = Constants.CREATE_ACC_EMAIL_SUBJECTS[Constants.HACKBOARD]; } const handlebarPath = path.join( __dirname, diff --git a/tests/util/roleBinding.test.util.js b/tests/util/roleBinding.test.util.js index 56c566d6..93f0c0ba 100644 --- a/tests/util/roleBinding.test.util.js +++ b/tests/util/roleBinding.test.util.js @@ -25,6 +25,9 @@ function createRoleBinding(accountId, accountType = null, specificRoles = []) { case Constants.General.STAFF: roleBinding.roles.push(Constants.Role.adminRole); break; + case Constants.General.HACKBOARD: + roleBinding.roles.push(Constants.Role.hackboardRole); + break; case Constants.General.SPONSOR_T1: roleBinding.roles.push(Constants.Role.sponsorT1Role); break; From 3258fc8f83d87355a83337f2848d5b00f35ec5e6 Mon Sep 17 00:00:00 2001 From: Mika Vohl Date: Sat, 8 Nov 2025 16:37:02 -0500 Subject: [PATCH 10/11] Revert "Added hackboard role support" This reverts commit 481d3033b2f622b924d7d8339d1f191d246ce657. --- constants/general.constant.js | 9 +------- constants/role.constant.js | 30 ------------------------- middlewares/auth.middleware.js | 5 ----- services/accountConfirmation.service.js | 2 -- tests/util/roleBinding.test.util.js | 3 --- 5 files changed, 1 insertion(+), 48 deletions(-) diff --git a/constants/general.constant.js b/constants/general.constant.js index a3c17064..5e23d2fe 100644 --- a/constants/general.constant.js +++ b/constants/general.constant.js @@ -72,7 +72,6 @@ const SAMPLE_DIET_RESTRICTIONS = [ const HACKER = "Hacker"; const VOLUNTEER = "Volunteer"; const STAFF = "Staff"; -const HACKBOARD = "Hackboard"; const SPONSOR = "Sponsor"; const SPONSOR_T1 = "SponsorT1"; @@ -110,9 +109,8 @@ POST_ROLES[SPONSOR_T4] = "postSponsor"; POST_ROLES[SPONSOR_T5] = "postSponsor"; POST_ROLES[VOLUNTEER] = "postVolunteer"; POST_ROLES[STAFF] = "postStaff"; -POST_ROLES[HACKBOARD] = "Hackboard"; -const USER_TYPES = [HACKER, VOLUNTEER, STAFF, HACKBOARD, SPONSOR]; +const USER_TYPES = [HACKER, VOLUNTEER, STAFF, SPONSOR]; const SPONSOR_TIERS = [ SPONSOR_T1, SPONSOR_T2, @@ -124,7 +122,6 @@ const EXTENDED_USER_TYPES = [ HACKER, VOLUNTEER, STAFF, - HACKBOARD, SPONSOR_T1, SPONSOR_T2, SPONSOR_T3, @@ -171,9 +168,6 @@ CREATE_ACC_EMAIL_SUBJECTS[ CREATE_ACC_EMAIL_SUBJECTS[ STAFF ] = `You've been invited to create a staff account for ${HACKATHON_NAME}`; -CREATE_ACC_EMAIL_SUBJECTS[ - HACKBOARD -] = `You've been invited to create a hackboard account for ${HACKATHON_NAME}`; const CACHE_TIMEOUT_STATS = 5 * 60 * 1000; const CACHE_KEY_STATS = "hackerStats"; @@ -217,7 +211,6 @@ module.exports = { SPONSOR: SPONSOR, VOLUNTEER: VOLUNTEER, STAFF: STAFF, - HACKBOARD: HACKBOARD, SPONSOR_T1: SPONSOR_T1, SPONSOR_T2: SPONSOR_T2, SPONSOR_T3: SPONSOR_T3, diff --git a/constants/role.constant.js b/constants/role.constant.js index ca8047e3..87cb3eba 100644 --- a/constants/role.constant.js +++ b/constants/role.constant.js @@ -20,31 +20,12 @@ const accountRole = { ] }; -const hackboardRestrictedRoutes = [ // hackboard permissions is all staff routes minus these routes - Constants.Routes.hackerRoutes.postAnySendWeekOfEmail, - Constants.Routes.hackerRoutes.postSelfSendWeekOfEmail, - Constants.Routes.hackerRoutes.postAnySendDayOfEmail, - Constants.Routes.hackerRoutes.postSelfSendDayOfEmail, - Constants.Routes.hackerRoutes.patchAcceptHackerById, - Constants.Routes.hackerRoutes.patchAcceptHackerByEmail, - Constants.Routes.hackerRoutes.patchAcceptHackerByArrayOfIds, - Constants.Routes.hackerRoutes.patchAnyStatusById, - Constants.Routes.settingsRoutes.getSettings, - Constants.Routes.settingsRoutes.patchSettings -]; - const adminRole = { _id: mongoose.Types.ObjectId.createFromTime(1), name: Constants.General.STAFF, routes: Constants.Routes.listAllRoutes() }; -const hackboardRole = { - _id: mongoose.Types.ObjectId.createFromTime(9), - name: "Hackboard", - routes: createHackboardRoutes() -}; - const hackerRole = { _id: mongoose.Types.ObjectId.createFromTime(2), name: Constants.General.HACKER, @@ -162,15 +143,6 @@ const singularRoles = createAllSingularRoles(); const allRolesObject = createAllRoles(); const allRolesArray = Object.values(allRolesObject); -function createHackboardRoutes() { - const restrictedRouteIds = new Set( - hackboardRestrictedRoutes.map((route) => route._id.toString()) - ); - return Constants.Routes.listAllRoutes().filter((route) => { - return !restrictedRouteIds.has(route._id.toString()); - }); -} - /** * Creates all the roles that are of a specific uri and request type * @return {Role[]} @@ -213,7 +185,6 @@ function createAllRoles() { let allRolesObject = { accountRole: accountRole, adminRole: adminRole, - hackboardRole: hackboardRole, hackerRole: hackerRole, volunteerRole: volunteerRole, sponsorT1Role: sponsorT1Role, @@ -237,7 +208,6 @@ function createAllRoles() { module.exports = { accountRole: accountRole, adminRole: adminRole, - hackboardRole: hackboardRole, hackerRole: hackerRole, volunteerRole: volunteerRole, sponsorT1Role: sponsorT1Role, diff --git a/middlewares/auth.middleware.js b/middlewares/auth.middleware.js index ce3a8793..eeb898c7 100644 --- a/middlewares/auth.middleware.js +++ b/middlewares/auth.middleware.js @@ -477,11 +477,6 @@ async function addCreationRoleBindings(req, res, next) { req.body.account.id, Constants.Role.adminRole.name ); - } else if (req.body.account.accountType === Constants.General.HACKBOARD) { - await Services.RoleBinding.createRoleBindingByRoleName( - req.body.account.id, - Constants.Role.hackboardRole.name - ); } else { // Get the default role for the account type given const roleName = diff --git a/services/accountConfirmation.service.js b/services/accountConfirmation.service.js index 028e0615..f27bff94 100644 --- a/services/accountConfirmation.service.js +++ b/services/accountConfirmation.service.js @@ -172,8 +172,6 @@ function generateAccountInvitationEmail(address, receiverEmail, type, token) { emailSubject = Constants.CREATE_ACC_EMAIL_SUBJECTS[Constants.SPONSOR]; } else if (type === Constants.STAFF) { emailSubject = Constants.CREATE_ACC_EMAIL_SUBJECTS[Constants.STAFF]; - } else if (type === Constants.HACKBOARD) { - emailSubject = Constants.CREATE_ACC_EMAIL_SUBJECTS[Constants.HACKBOARD]; } const handlebarPath = path.join( __dirname, diff --git a/tests/util/roleBinding.test.util.js b/tests/util/roleBinding.test.util.js index 93f0c0ba..56c566d6 100644 --- a/tests/util/roleBinding.test.util.js +++ b/tests/util/roleBinding.test.util.js @@ -25,9 +25,6 @@ function createRoleBinding(accountId, accountType = null, specificRoles = []) { case Constants.General.STAFF: roleBinding.roles.push(Constants.Role.adminRole); break; - case Constants.General.HACKBOARD: - roleBinding.roles.push(Constants.Role.hackboardRole); - break; case Constants.General.SPONSOR_T1: roleBinding.roles.push(Constants.Role.sponsorT1Role); break; From 635a7d5674d17cb4157abbd7530920aea079c56a Mon Sep 17 00:00:00 2001 From: Tavi Pollard Date: Mon, 17 Nov 2025 14:12:40 -0500 Subject: [PATCH 11/11] update email dates --- assets/email/statusEmail/Accepted.hbs | 2 +- assets/email/statusEmail/Applied.hbs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/email/statusEmail/Accepted.hbs b/assets/email/statusEmail/Accepted.hbs index 2ff9ae55..f1ada263 100644 --- a/assets/email/statusEmail/Accepted.hbs +++ b/assets/email/statusEmail/Accepted.hbs @@ -392,7 +392,7 @@

Confirm your attendance on our
hacker - dashboard no later than January 13th at 11:59PM EST. + dashboard no later than December 15th 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 December - 20th at + 1st at 11:59 PM ET.

In the meantime, follow us on