Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 checkinRouter = require("./routes/api/checkin");
const emailsRouter = require("./routes/api/emails");

const app = express();
Expand Down Expand Up @@ -115,8 +116,12 @@ settingsRouter.activate(apiRouter);
Services.log.info("Settings router activated");
roleRouter.activate(apiRouter);
Services.log.info("Role router activated");
checkinRouter.activate(apiRouter);
Services.log.info("Checkin router activated");
emailsRouter.activate(apiRouter);
Services.log.info("Emails router activated");
checkinRouter.activate(apiRouter);
Services.log.info("Checkin router activated");

app.use("/", indexRouter);
app.use("/api", apiRouter);
Expand Down
37 changes: 37 additions & 0 deletions constants/checkin-options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Team checkin form constants for Mchacks 13
"use strict";

const PRIZE_CATEGORIES = [
"Best Beginner Hack",
"Best Design",
"Chaotic Evil",
"Best Use of AI or AI Agents",
];

const SPONSOR_CHALLENGES = [
"HoloRay",
"Athena AI",
"Gumloop",
"National Bank",
"Tail'ed",
"BassiliChat AI",
"Dobson Center",
"Desjardins",
"NOVA",
"CSUS"
];

const MLH_CHALLENGES = [
"Best Use of ElevenLabs",
"Best Use of Gemini API",
"Best Use of MongoDB Atlas",
"Best Use of DigitalOcean",
"Best Use of Solana",
"Best Use of Auth0"
];

module.exports = {
PRIZE_CATEGORIES,
SPONSOR_CHALLENGES,
MLH_CHALLENGES
};
101 changes: 101 additions & 0 deletions controllers/checkin.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"use strict";

const Services = {
Sheets: require('../services/sheets.service'),
Hacker: require('../services/hacker.service'),
Team: require('../services/team.service'),
Account: require('../services/account.service')
};

/**
* @function submitCheckin
* @param {{body: {formData: Object}, user: {id: string}}} req
* @param {*} res
* @return {JSON} Success status
* @description Handles the check-in form submission and adds data to Google Sheets
* Automatically fetches team member emails from the logged-in user's team
*/
async function submitCheckin(req, res) {
try {
// Get logged-in hacker
const hacker = await Services.Hacker.findByAccountId(req.user.id);

if (!hacker) {
return res.status(404).json({
message: "Hacker not found",
data: {}
});
}

// Check hacker has a team
if (!hacker.teamId) {
return res.status(400).json({
message: "You must be part of a team to submit check-in",
data: {}
});
}

// Fetch team data
const team = await Services.Team.findById(hacker.teamId);

if (!team) {
return res.status(404).json({
message: "Team not found",
data: {}
});
}

// Fetch all team member emails
const teamMemberEmails = [];
for (const memberId of team.members) {
const memberHacker = await Services.Hacker.findById(memberId);
if (memberHacker) {
const memberAccount = await Services.Account.findById(memberHacker.accountId);
if (memberAccount) {
teamMemberEmails.push(memberAccount.email);
}
}
}

// Update team's devpostURL in the database if provided
if (req.body.formData.devpostLink) {
await Services.Team.updateOne(hacker.teamId, {
devpostURL: req.body.formData.devpostLink
});
}

// Prepare data for Google Sheets with team member emails
const teamIdString = team._id ? team._id.toString() : hacker.teamId.toString();

const checkinData = {
teamMember1: teamMemberEmails[0] || '',
teamMember2: teamMemberEmails[1] || '',
teamMember3: teamMemberEmails[2] || '',
teamMember4: teamMemberEmails[3] || '',
prizeCategories: req.body.formData.prizeCategories,
sponsorChallenges: req.body.formData.sponsorChallenges,
mlhChallenges: req.body.formData.mlhChallenges,
// workshopsAttended: req.body.formData.workshopsAttended,
discordTag: req.body.formData.discordTag,
devpostLink: req.body.formData.devpostLink,
teamId: teamIdString
};

await Services.Sheets.appendCheckinData(checkinData);

return res.status(200).json({
message: "Check-in data successfully submitted",
data: {}
});
} catch (error) {
console.error('Checkin submission error:', error);
return res.status(500).json({
message: "Error submitting check-in data",
data: {}
});
}
}

module.exports = {
submitCheckin
};
3 changes: 3 additions & 0 deletions cookies.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
31 changes: 31 additions & 0 deletions middlewares/settings.middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ async function updateSettings(req, res, next) {
* @description Confirms that openTime < closeTime < confirmTime
*/
function confirmValidPatch(req, res, next) {
if (!req.body.settingsDetails.openTime &&
!req.body.settingsDetails.closeTime &&
!req.body.settingsDetails.confirmTime) {
return next();
}
const openTime = new Date(req.body.settingsDetails.openTime);
const closeTime = new Date(req.body.settingsDetails.closeTime);
const confirmTime = new Date(req.body.settingsDetails.confirmTime);
Expand Down Expand Up @@ -126,9 +131,35 @@ async function confirmAppsOpen(req, res, next) {
}
}

/**
* @function confirmCheckinOpen
* @param {*} req
* @param {*} res
* @param {*} next
* @description Only succeeds if check-in is currently open
*/
async function confirmCheckinOpen(req, res, next) {
const settings = await Services.Settings.getSettings();
if (!settings) {
return next({
status: 500,
message: Constants.Error.GENERIC_500_MESSAGE
});
}
if (settings.checkinOpen) {
return next();
}

return next({
status: 403,
message: Constants.Error.SETTINGS_403_MESSAGE
});
}

module.exports = {
parsePatch: parsePatch,
confirmValidPatch: confirmValidPatch,
confirmCheckinOpen: Middleware.Util.asyncMiddleware(confirmCheckinOpen),
confirmAppsOpen: Middleware.Util.asyncMiddleware(confirmAppsOpen),
updateSettings: Middleware.Util.asyncMiddleware(updateSettings),
getSettings: Middleware.Util.asyncMiddleware(getSettings)
Expand Down
58 changes: 58 additions & 0 deletions middlewares/validators/checkin.validator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"use strict";

const { body } = require('express-validator');
const {
PRIZE_CATEGORIES,
SPONSOR_CHALLENGES,
MLH_CHALLENGES
} = require('../../constants/checkin-options');

/**
* Validator for check-in form submission
*/
const checkinValidator = [
body('formData.prizeCategories')
.isArray()
.withMessage('Prize categories must be an array')
.custom((values) =>
Array.isArray(values) &&
values.every((value) => typeof value === 'string' && PRIZE_CATEGORIES.includes(value))
)
.withMessage('Prize categories contain invalid selections'),
body('formData.sponsorChallenges')
.isArray()
.withMessage('Sponsor challenges must be an array')
.custom((values) =>
Array.isArray(values) &&
values.every((value) => typeof value === 'string' && SPONSOR_CHALLENGES.includes(value))
)
.withMessage('Sponsor challenges contain invalid selections'),
body('formData.mlhChallenges')
.isArray()
.withMessage('MLH challenges must be an array')
.custom((values) =>
Array.isArray(values) &&
values.every((value) => typeof value === 'string' && MLH_CHALLENGES.includes(value))
)
.withMessage('MLH challenges contain invalid selections'),
// body('formData.workshopsAttended').isArray().withMessage('Workshops attended must be an array'),
body('formData.discordTag').notEmpty().withMessage('Discord tag is required'),
body('formData.devpostLink')
.notEmpty()
.withMessage('Devpost link is required')
.bail()
.isURL({ require_protocol: true, protocols: ['http', 'https'] })
.withMessage('Devpost link must be a valid URL')
.bail()
.custom((value) => {
try {
const url = new URL(value);
return url.hostname === 'devpost.com' || url.hostname.endsWith('.devpost.com');
} catch (error) {
return false;
}
})
.withMessage('Devpost link must be a devpost.com URL')
];

module.exports = checkinValidator;
3 changes: 2 additions & 1 deletion middlewares/validators/settings.validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module.exports = {
VALIDATOR.dateValidator("body", "openTime", true),
VALIDATOR.dateValidator("body", "closeTime", true),
VALIDATOR.dateValidator("body", "confirmTime", true),
VALIDATOR.booleanValidator("body", "isRemote", true)
VALIDATOR.booleanValidator("body", "isRemote", true),
VALIDATOR.booleanValidator("body", "checkinOpen", true),
]
};
4 changes: 4 additions & 0 deletions models/settings.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ const settings = new mongoose.Schema({
isRemote: {
type: Boolean,
default: false
},
checkinOpen: {
type: Boolean,
default: false
}
});

Expand Down
Loading
Loading