From 3502334cedf26538a2b82332cade83290664aed9 Mon Sep 17 00:00:00 2001
From: Andreas Bigger
Date: Tue, 18 Aug 2020 08:39:30 -0700
Subject: [PATCH 001/129] [WIP] migrating rest api to vercel serverless
functions
---
lib/utils.ts | 119 ++++++++++++++++++++
pages/api/admin.ts | 224 ++++++++++++++++++++++++++++++++++++++
pages/api/admin/review.ts | 0
3 files changed, 343 insertions(+)
create mode 100644 lib/utils.ts
create mode 100644 pages/api/admin.ts
create mode 100644 pages/api/admin/review.ts
diff --git a/lib/utils.ts b/lib/utils.ts
new file mode 100644
index 00000000..55db386c
--- /dev/null
+++ b/lib/utils.ts
@@ -0,0 +1,119 @@
+const models = require("./models");
+const Sentry = require("@sentry/node");
+
+module.exports = {
+ authMiddleware: function(req, res, next) {
+ if (req.user) {
+ return next();
+ }
+ res.status(400).send("Unauthorized");
+ },
+ preprocessRequest: function(req, res, next) {
+ delete req.body.status;
+ delete req.body.userId;
+ delete req.body.email;
+ delete req.body.role;
+ return next();
+ },
+ requireAdmin: function(req, res, next) {
+ /* Read user from database, and make sure it is in fact an admin */
+ try {
+ models.HackerProfile.findAll({
+ where: {
+ userId: req.user.id
+ }
+ }).then(hackerProfiles => {
+ if (hackerProfiles.length >= 1) {
+ if (hackerProfiles[0].get("role") === "admin") {
+ return next();
+ } else {
+ res.status(400).send("Unauthorized: Incorrect role");
+ }
+ } else {
+ res.status(400).send("Unauthorized; profile not found");
+ }
+ });
+ } catch (e) {
+ Sentry.captureException(e);
+ res.status(400).send("Unauthorized");
+ }
+ },
+ requireVolunteer: function(req, res, next) {
+ /* Read user from database, and make sure it is in fact an volunteer */
+ try {
+ models.HackerProfile.findAll({
+ where: {
+ userId: req.user.id
+ }
+ }).then(hackerProfiles => {
+ if (hackerProfiles.length >= 1) {
+ if (hackerProfiles[0].get("role") === "volunteer") {
+ return next();
+ } else {
+ res.status(400).send("Unauthorized: Incorrect role");
+ }
+ } else {
+ res.status(400).send("Unauthorized; profile not found");
+ }
+ });
+ } catch (e) {
+ Sentry.captureException(e);
+ res.status(400).send("Unauthorized");
+ }
+ },
+ requireSponsor: function(req, res, next) {
+ /* Read user from database, and make sure it is in fact an sponsor */
+ try {
+ models.HackerProfile.findAll({
+ where: {
+ userId: req.user.id
+ }
+ }).then(hackerProfiles => {
+ if (hackerProfiles.length >= 1) {
+ if (hackerProfiles[0].get("role") === "sponsor") {
+ return next();
+ } else {
+ res.status(400).send("Unauthorized: Incorrect role");
+ }
+ } else {
+ res.status(400).send("Unauthorized; profile not found");
+ }
+ });
+ } catch (e) {
+ Sentry.captureException(e);
+ res.status(400).send("Unauthorized");
+ }
+ },
+ requireNonHacker: function(req, res, next) {
+ /* Read user from database, and make sure it is in fact not a hacker */
+ try {
+ models.HackerProfile.findAll({
+ where: {
+ userId: req.user.id
+ }
+ }).then(hackerProfiles => {
+ if (hackerProfiles.length >= 1) {
+ if (hackerProfiles[0].get("role") !== "hacker") {
+ // Add role to be used
+ req.user.role = hackerProfiles[0].get("role");
+ return next();
+ } else {
+ res.status(400).send("Unauthorized: Incorrect role");
+ }
+ } else {
+ res.status(400).send("Unauthorized; profile not found");
+ }
+ });
+ } catch (e) {
+ Sentry.captureException(e);
+ res.status(400).send("Unauthorized");
+ }
+ },
+
+ requireDevelopmentEnv: function(req, res, next) {
+ if (process.env.NODE_ENV == "production") {
+ return res.redirect("/");
+ }
+ return next();
+ }
+};
diff --git a/pages/api/admin.ts b/pages/api/admin.ts
new file mode 100644
index 00000000..d4b64b8b
--- /dev/null
+++ b/pages/api/admin.ts
@@ -0,0 +1,224 @@
+let express = require("express");
+const models = require("./models");
+const utils = require("./utils");
+const router = express.Router();
+const sequelize = require("sequelize");
+const Sentry = require("@sentry/node");
+
+/* This router supports superuser routes, such as updating the status of users */
+
+router.use(utils.authMiddleware);
+router.use(utils.requireAdmin);
+
+// Full write access to a user's hackerProfile
+// Variable parameters in the foremost endpoint path restricts use to just this route.
+// Consider changing route as to not use up all single-path put requests
+router.put("/:email", async (req, res) => {
+ const updatedhackerProfile = await models.HackerProfile.update(req.body, {
+ where: {
+ email: req.params.email
+ }
+ });
+ return res.json({ hackerProfile: newHackerProfile });
+});
+
+// TODO: use the new client fetcher api
+router.get("/profiles", async (req, res) => {
+ const Op = sequelize.Op;
+ const { query } = req.query;
+ const flexQuery = "%" + query + "%";
+ try {
+ const profiles = await models.HackerProfile.findAll({
+ where: {
+ [Op.or]: [
+ {
+ email: {
+ [Op.like]: flexQuery
+ }
+ },
+ {
+ firstName: {
+ [Op.like]: flexQuery
+ }
+ },
+ {
+ lastName: {
+ [Op.like]: flexQuery
+ }
+ }
+ ]
+ },
+ limit: 50
+ });
+ return res.json({
+ profiles
+ });
+ } catch (e) {
+ return res.status(500).json({ error: e.message });
+ }
+});
+
+router.post("/updateRole", async (req, res) => {
+ try {
+ const { email, role } = req.body;
+ const result = await models.HackerProfile.update(
+ {
+ role: role
+ },
+ {
+ where: {
+ email: email
+ }
+ }
+ );
+
+ return res.json({ success: result });
+ } catch (e) {
+ return res.status(500).json({ error: e.message });
+ }
+});
+
+router.get("/reviews", async (req, res) => {
+ try {
+ const reviews = await models.HackerReview.findAll();
+ return res.json({ reviews: reviews });
+ } catch (e) {
+ return res.status(500).json({ err: e });
+ }
+});
+
+router.get("/reviewHistory", async (req, res) => {
+ try {
+ const reviews = await models.HackerReview.findAll({
+ where: {
+ createdBy: req.user.id
+ }
+ });
+ return res.json({ reviews: reviews });
+ } catch (e) {
+ return res.status(500).json({ err: e });
+ }
+});
+
+router.put("/review/:id", async (req, res) => {
+ const requestId = req.params.id;
+ const allowedFields = new Set([
+ "scoreOne",
+ "scoreTwo",
+ "scoreThree",
+ "comments"
+ ]);
+ const formInput = req.body;
+
+ for (let key of Object.keys(formInput)) {
+ if (!allowedFields.has(key)) {
+ return res.status(400).json({
+ error: `${key} is not a supported field`
+ });
+ }
+ }
+
+ try {
+ const result = await models.HackerReview.update(req.body, {
+ where: {
+ id: requestId
+ }
+ });
+
+ return res.json({ update: result });
+ } catch (e) {
+ return res.status(500).json({
+ error: e
+ });
+ }
+});
+
+router.get("/eligibleProfiles", async (req, res) => {
+ try {
+ const allProfiles = await models.HackerProfile.findAll({
+ where: {
+ submittedAt: {
+ [sequelize.Op.not]: null
+ }
+ },
+ include: [
+ {
+ model: models.HackerReview
+ }
+ ]
+ });
+ filteredProfiles = allProfiles.filter(profile => {
+ const reviewsByCurrUser = profile.HackerReviews.filter(review => {
+ return review.dataValues.createdBy === req.user.id;
+ });
+ return reviewsByCurrUser.length === 0 && profile.HackerReviews.length < 1;
+ });
+ return res.json({
+ eligibleReviews: filteredProfiles
+ });
+ } catch (e) {
+ return res.status(500).json({ error: e.message });
+ }
+});
+
+router.post("/review", async (req, res) => {
+ try {
+ const formBody = req.body;
+ const newReview = await models.HackerReview.create({
+ hackerId: formBody.userId,
+ createdBy: req.user.id,
+ scoreOne: formBody.scoreOne,
+ scoreTwo: formBody.scoreTwo,
+ scoreThree: formBody.scoreThree,
+ comments: formBody.comments
+ });
+
+ return res.json({ newReview: newReview });
+ } catch (e) {
+ return res.status(500).json({ error: e.message });
+ }
+});
+
+router.get("/review", async (req, res) => {
+ try {
+ const profilesWCount = await models.HackerProfile.findAll({
+ attributes: {
+ include: [
+ [
+ sequelize.fn("COUNT", sequelize.col("HackerReviews.id")),
+ "reviewCount"
+ ]
+ ]
+ },
+ include: [
+ {
+ model: models.HackerReview,
+ attributes: []
+ }
+ ],
+ group: ["HackerProfile.userId"]
+ });
+
+ const acceptableProfile = profilesWCount.find(profile => {
+ return profile.dataValues.reviewCount < 1;
+ });
+ if (acceptableProfile) {
+ const newReview = await models.HackerReview.create({
+ hackerId: acceptableProfile.dataValues.userId,
+ createdBy: req.user.id
+ });
+
+ return res.json({
+ review: newReview,
+ profile: acceptableProfile
+ });
+ } else {
+ return res.json({ review: null, profile: null }); // Returns empty when there are no more profiles
+ }
+ } catch (e) {
+ console.log(e);
+ return res.status(500).json({ err: e });
+ }
+});
+
+module.exports = router;
diff --git a/pages/api/admin/review.ts b/pages/api/admin/review.ts
new file mode 100644
index 00000000..e69de29b
From 219bc326dd5d2ee57f16265e5b9a631c44c6b4e1 Mon Sep 17 00:00:00 2001
From: Andreas Bigger
Date: Thu, 20 Aug 2020 09:33:35 -0700
Subject: [PATCH 002/129] temp
---
lib/database/Readme.md | 2 +
lib/database/config.ts | 27 ++++
.../20191010225527-create-hacker-profile.js | 51 ++++++
.../20191028071621-update-hackerProfile.js | 76 +++++++++
.../migrations/20191029183843-add-role.js | 13 ++
...0191031003446-profile-additional-fields.js | 73 +++++++++
.../20191101021240-mlh-app-requirements.js | 21 +++
.../20191104062818-cleanup-profile.js | 62 ++++++++
.../20191111203354-add-promo-codes.js | 33 ++++
.../migrations/20191114212136-teams.js | 47 ++++++
.../20191115010855-add-review-table.js | 58 +++++++
.../20191126000713-teams-add-dates.js | 23 +++
.../20191126043728-create-task-table.js | 38 +++++
.../20191203052918-add-person-table.js | 29 ++++
.../20191203062141-add-contribution-table.js | 42 +++++
.../20191209225538-add-houses-table.js | 46 ++++++
.../20191223005530-confirmation-fields.js | 70 ++++++++
.../migrations/20191224053622-no-bus-check.js | 15 ++
.../20191226014741-confirmed-at-field.js | 21 +++
.../20200115053213-add-task-groupings.js | 43 +++++
.../20200115054722-add-multipliers-table.js | 42 +++++
.../20200115061612-add-actions-table.js | 34 ++++
.../20200115235853-add-travel-column.js | 22 +++
.../20200118201352-add-task-type.js | 23 +++
.../migrations/20200120005508-create-prize.js | 30 ++++
.../20200120005653-create-projectTeam.js | 33 ++++
...120005958-associate-projectTeams-prizes.js | 32 ++++
...20011322-associate-projectTeams-persons.js | 17 ++
...4192357-augment-contrib-with-multiplier.js | 13 ++
.../20200124194744-add-schedule-table.js | 40 +++++
.../20200126211953-add-scanner-id-column.js | 17 ++
...0200126213315-adds-timestamps-to-prizes.js | 23 +++
.../20200126213646-add-is-group-task-flag.js | 13 ++
.../20200126215010-add-unlockables-table.js | 38 +++++
...000435-modify-role-superadmin-volunteer.js | 19 +++
.../20200127004044-add-is-active-field.js | 13 ++
...0127012812-add-qrcode-to-hacker-profile.js | 14 ++
...00201211804-add-sponsor-and-isPast-flag.js | 23 +++
.../20200810155744-give_id_default_value.js | 23 +++
lib/database/models/action.ts | 14 ++
lib/database/models/contribution.ts | 27 ++++
lib/database/models/event.ts | 17 ++
lib/database/models/groupings.ts | 16 ++
lib/database/models/hackerProfile.ts | 150 ++++++++++++++++++
lib/database/models/hackerReview.ts | 24 +++
lib/database/models/house.ts | 21 +++
lib/database/models/index.ts | 33 ++++
lib/database/models/multiplier.ts | 18 +++
lib/database/models/person.ts | 49 ++++++
lib/database/models/prize.ts | 21 +++
lib/database/models/projectTeam.ts | 30 ++++
lib/database/models/projectTeamPrize.ts | 9 ++
lib/database/models/task.ts | 25 +++
lib/database/models/team.ts | 34 ++++
lib/database/models/unlockable.ts | 15 ++
lib/{ => database}/utils.ts | 2 +-
lib/db.ts | 20 +++
package.json | 3 +
pages/api/admin/review.ts | 41 +++++
yarn.lock | 30 +++-
60 files changed, 1856 insertions(+), 2 deletions(-)
create mode 100644 lib/database/Readme.md
create mode 100644 lib/database/config.ts
create mode 100644 lib/database/migrations/20191010225527-create-hacker-profile.js
create mode 100644 lib/database/migrations/20191028071621-update-hackerProfile.js
create mode 100644 lib/database/migrations/20191029183843-add-role.js
create mode 100644 lib/database/migrations/20191031003446-profile-additional-fields.js
create mode 100644 lib/database/migrations/20191101021240-mlh-app-requirements.js
create mode 100644 lib/database/migrations/20191104062818-cleanup-profile.js
create mode 100644 lib/database/migrations/20191111203354-add-promo-codes.js
create mode 100644 lib/database/migrations/20191114212136-teams.js
create mode 100644 lib/database/migrations/20191115010855-add-review-table.js
create mode 100644 lib/database/migrations/20191126000713-teams-add-dates.js
create mode 100644 lib/database/migrations/20191126043728-create-task-table.js
create mode 100644 lib/database/migrations/20191203052918-add-person-table.js
create mode 100644 lib/database/migrations/20191203062141-add-contribution-table.js
create mode 100644 lib/database/migrations/20191209225538-add-houses-table.js
create mode 100644 lib/database/migrations/20191223005530-confirmation-fields.js
create mode 100644 lib/database/migrations/20191224053622-no-bus-check.js
create mode 100644 lib/database/migrations/20191226014741-confirmed-at-field.js
create mode 100644 lib/database/migrations/20200115053213-add-task-groupings.js
create mode 100644 lib/database/migrations/20200115054722-add-multipliers-table.js
create mode 100644 lib/database/migrations/20200115061612-add-actions-table.js
create mode 100644 lib/database/migrations/20200115235853-add-travel-column.js
create mode 100644 lib/database/migrations/20200118201352-add-task-type.js
create mode 100644 lib/database/migrations/20200120005508-create-prize.js
create mode 100644 lib/database/migrations/20200120005653-create-projectTeam.js
create mode 100644 lib/database/migrations/20200120005958-associate-projectTeams-prizes.js
create mode 100644 lib/database/migrations/20200120011322-associate-projectTeams-persons.js
create mode 100644 lib/database/migrations/20200124192357-augment-contrib-with-multiplier.js
create mode 100644 lib/database/migrations/20200124194744-add-schedule-table.js
create mode 100644 lib/database/migrations/20200126211953-add-scanner-id-column.js
create mode 100644 lib/database/migrations/20200126213315-adds-timestamps-to-prizes.js
create mode 100644 lib/database/migrations/20200126213646-add-is-group-task-flag.js
create mode 100644 lib/database/migrations/20200126215010-add-unlockables-table.js
create mode 100644 lib/database/migrations/20200127000435-modify-role-superadmin-volunteer.js
create mode 100644 lib/database/migrations/20200127004044-add-is-active-field.js
create mode 100644 lib/database/migrations/20200127012812-add-qrcode-to-hacker-profile.js
create mode 100644 lib/database/migrations/20200201211804-add-sponsor-and-isPast-flag.js
create mode 100644 lib/database/migrations/20200810155744-give_id_default_value.js
create mode 100644 lib/database/models/action.ts
create mode 100644 lib/database/models/contribution.ts
create mode 100644 lib/database/models/event.ts
create mode 100644 lib/database/models/groupings.ts
create mode 100644 lib/database/models/hackerProfile.ts
create mode 100644 lib/database/models/hackerReview.ts
create mode 100644 lib/database/models/house.ts
create mode 100644 lib/database/models/index.ts
create mode 100644 lib/database/models/multiplier.ts
create mode 100644 lib/database/models/person.ts
create mode 100644 lib/database/models/prize.ts
create mode 100644 lib/database/models/projectTeam.ts
create mode 100644 lib/database/models/projectTeamPrize.ts
create mode 100644 lib/database/models/task.ts
create mode 100644 lib/database/models/team.ts
create mode 100644 lib/database/models/unlockable.ts
rename lib/{ => database}/utils.ts (98%)
create mode 100644 lib/db.ts
diff --git a/lib/database/Readme.md b/lib/database/Readme.md
new file mode 100644
index 00000000..58606528
--- /dev/null
+++ b/lib/database/Readme.md
@@ -0,0 +1,2 @@
+Directory that contains all the legacy SQL logic.
+Temporary holding place as the nodejs express rest api is migrated to Vercel Serverless Functions.
diff --git a/lib/database/config.ts b/lib/database/config.ts
new file mode 100644
index 00000000..87e5159f
--- /dev/null
+++ b/lib/database/config.ts
@@ -0,0 +1,27 @@
+const config = {
+ development: {
+ username: process.env.DEV_DB_USERNAME,
+ password: process.env.DEV_DB_PASSWORD,
+ database: process.env.DEV_DB_NAME,
+ host: process.env.DEV_DB_HOSTNAME,
+ logging: false,
+ dialect: "mysql"
+ },
+ test: {
+ username: process.env.TEST_DB_USERNAME,
+ password: process.env.TEST_DB_PASSWORD,
+ database: process.env.TEST_DB_NAME,
+ host: process.env.TEST_DB_HOSTNAME,
+ logging: false,
+ dialect: "mysql"
+ },
+ production: {
+ username: process.env.PROD_DB_USERNAME,
+ password: process.env.PROD_DB_PASSWORD,
+ database: process.env.PROD_DB_NAME,
+ host: process.env.PROD_DB_HOSTNAME,
+ dialect: "mysql"
+ }
+};
+
+export default config;
diff --git a/lib/database/migrations/20191010225527-create-hacker-profile.js b/lib/database/migrations/20191010225527-create-hacker-profile.js
new file mode 100644
index 00000000..b79b6079
--- /dev/null
+++ b/lib/database/migrations/20191010225527-create-hacker-profile.js
@@ -0,0 +1,51 @@
+export default {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable("HackerProfiles", {
+ id: {
+ allowNull: false,
+ type: Sequelize.INTEGER
+ },
+ userId: {
+ primaryKey: true,
+ type: Sequelize.STRING,
+ allowNull: false
+ },
+ gender: {
+ type: Sequelize.ENUM,
+ values: ["male", "female", "other"]
+ },
+ email: {
+ type: Sequelize.STRING
+ },
+ ethnicity: {
+ type: Sequelize.STRING
+ },
+ major: {
+ type: Sequelize.STRING
+ },
+ minor: {
+ type: Sequelize.STRING
+ },
+ resume: {
+ type: Sequelize.STRING
+ },
+ skills: {
+ type: Sequelize.STRING
+ },
+ interests: {
+ type: Sequelize.STRING
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ });
+ },
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable("HackerProfiles");
+ }
+};
diff --git a/lib/database/migrations/20191028071621-update-hackerProfile.js b/lib/database/migrations/20191028071621-update-hackerProfile.js
new file mode 100644
index 00000000..e1be8a70
--- /dev/null
+++ b/lib/database/migrations/20191028071621-update-hackerProfile.js
@@ -0,0 +1,76 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.addColumn("HackerProfiles", "applicationSubmittedAt", {
+ type: Sequelize.DATE
+ }),
+ queryInterface.addColumn("HackerProfiles", "profileSubmittedAt", {
+ type: Sequelize.DATE
+ }),
+ queryInterface.addColumn("HackerProfiles", "status", {
+ type: Sequelize.ENUM,
+ values: [
+ "unverified",
+ "verified",
+ "profileSubmitted",
+ "applicationSubmitted",
+ "accepted",
+ "waitlisted",
+ "rejected",
+ "confirmed",
+ "checkedIn"
+ ],
+ defaultValue: "unverified",
+ allowNull: false
+ }),
+ queryInterface.addColumn("HackerProfiles", "firstName", {
+ type: Sequelize.STRING
+ }),
+ queryInterface.addColumn("HackerProfiles", "lastName", {
+ type: Sequelize.STRING
+ }),
+ queryInterface.addColumn("HackerProfiles", "phoneNumber", {
+ type: Sequelize.STRING
+ }),
+ queryInterface.addColumn("HackerProfiles", "school", {
+ type: Sequelize.STRING
+ }),
+ queryInterface.addColumn("HackerProfiles", "year", {
+ type: Sequelize.ENUM,
+ values: ["Freshman", "Sophomore", "Junior", "Senior", "Graduate"]
+ }),
+ queryInterface.addColumn("HackerProfiles", "skillLevel", {
+ type: Sequelize.ENUM,
+ values: ["Beginner", "Intermediate", "Advanced"]
+ }),
+ queryInterface.addColumn("HackerProfiles", "questionOne", {
+ type: Sequelize.STRING(1000)
+ }),
+ queryInterface.addColumn("HackerProfiles", "questionTwo", {
+ type: Sequelize.STRING(1000)
+ }),
+ queryInterface.addColumn("HackerProfiles", "questionThree", {
+ type: Sequelize.STRING(1000)
+ })
+ ]);
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.removeColumn("HackerProfiles", "applicationSubmittedAt"),
+ queryInterface.removeColumn("HackerProfiles", "profileSubmittedAt"),
+ queryInterface.removeColumn("HackerProfiles", "status"),
+ queryInterface.removeColumn("HackerProfiles", "firstName"),
+ queryInterface.removeColumn("HackerProfiles", "lastName"),
+ queryInterface.removeColumn("HackerProfiles", "phoneNumber"),
+ queryInterface.removeColumn("HackerProfiles", "school"),
+ queryInterface.removeColumn("HackerProfiles", "year"),
+ queryInterface.removeColumn("HackerProfiles", "skillLevel"),
+ queryInterface.removeColumn("HackerProfiles", "questionOne"),
+ queryInterface.removeColumn("HackerProfiles", "questionTwo"),
+ queryInterface.removeColumn("HackerProfiles", "questionThree")
+ ]);
+ }
+};
diff --git a/lib/database/migrations/20191029183843-add-role.js b/lib/database/migrations/20191029183843-add-role.js
new file mode 100644
index 00000000..46401b49
--- /dev/null
+++ b/lib/database/migrations/20191029183843-add-role.js
@@ -0,0 +1,13 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.addColumn("HackerProfiles", "role", {
+ type: Sequelize.ENUM,
+ values: ["hacker", "admin", "sponsor", "superadmin"]
+ });
+ },
+ down: (queryInterface, Sequelize) => {
+ queryInterface.removeColumn("HackerProfiles", "role");
+ }
+};
diff --git a/lib/database/migrations/20191031003446-profile-additional-fields.js b/lib/database/migrations/20191031003446-profile-additional-fields.js
new file mode 100644
index 00000000..e39204f1
--- /dev/null
+++ b/lib/database/migrations/20191031003446-profile-additional-fields.js
@@ -0,0 +1,73 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.addColumn("HackerProfiles", "graduationDate", {
+ type: Sequelize.ENUM,
+ values: [
+ "spring-2020",
+ "fall-2020",
+ "spring-2021",
+ "fall-2021",
+ "spring-2022",
+ "fall-2022",
+ "spring-2023",
+ "fall-2023",
+ "other"
+ ]
+ }),
+ queryInterface.addColumn("HackerProfiles", "over18", {
+ type: Sequelize.BOOLEAN
+ }),
+ queryInterface.addColumn("HackerProfiles", "needBus", {
+ type: Sequelize.BOOLEAN
+ }),
+ queryInterface.addColumn("HackerProfiles", "links", {
+ type: Sequelize.STRING(1000)
+ }),
+ queryInterface.changeColumn("HackerProfiles", "role", {
+ type: Sequelize.ENUM,
+ values: ["hacker", "admin", "sponsor", "superadmin"],
+ defaultValue: "hacker"
+ }),
+ queryInterface.changeColumn("HackerProfiles", "year", {
+ type: Sequelize.ENUM,
+ values: ["freshman", "sophomore", "junior", "senior", "graduate"]
+ }),
+ queryInterface.changeColumn("HackerProfiles", "gender", {
+ type: Sequelize.ENUM,
+ values: ["male", "female", "non-binary", "other", "no-say"]
+ }),
+ queryInterface.changeColumn("HackerProfiles", "skillLevel", {
+ type: Sequelize.ENUM,
+ values: ["beginner", "intermediate", "advanced"]
+ })
+ ]);
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.removeColumn("HackerProfiles", "graduationDate"),
+ queryInterface.removeColumn("HackerProfiles", "over18"),
+ queryInterface.removeColumn("HackerProfiles", "needBus"),
+ queryInterface.removeColumn("HackerProfiles", "links"),
+ queryInterface.changeColumn("HackerProfiles", "role", {
+ type: Sequelize.ENUM,
+ values: ["hacker", "admin", "sponsor", "superadmin"]
+ }),
+ queryInterface.changeColumn("HackerProfiles", "year", {
+ type: Sequelize.ENUM,
+ values: ["Freshman", "Sophomore", "Junior", "Senior", "Graduate"]
+ }),
+ queryInterface.changeColumn("HackerProfiles", "gender", {
+ type: Sequelize.ENUM,
+ values: ["male", "female", "other"]
+ }),
+ queryInterface.changeColumn("HackerProfiles", "skillLevel", {
+ type: Sequelize.ENUM,
+ values: ["Beginner", "Intermediate", "Advanced"]
+ })
+ ]);
+ }
+};
diff --git a/lib/database/migrations/20191101021240-mlh-app-requirements.js b/lib/database/migrations/20191101021240-mlh-app-requirements.js
new file mode 100644
index 00000000..50c92c07
--- /dev/null
+++ b/lib/database/migrations/20191101021240-mlh-app-requirements.js
@@ -0,0 +1,21 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.addColumn("HackerProfiles", "codeOfConduct", {
+ type: Sequelize.BOOLEAN
+ }),
+ queryInterface.addColumn("HackerProfiles", "authorize", {
+ type: Sequelize.BOOLEAN
+ })
+ ]);
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.removeColumn("HackerProfiles", "codeOfConduct"),
+ queryInterface.removeColumn("HackerProfiles", "authorize")
+ ]);
+ }
+};
diff --git a/lib/database/migrations/20191104062818-cleanup-profile.js b/lib/database/migrations/20191104062818-cleanup-profile.js
new file mode 100644
index 00000000..f0f7e20e
--- /dev/null
+++ b/lib/database/migrations/20191104062818-cleanup-profile.js
@@ -0,0 +1,62 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.removeColumn("HackerProfiles", "applicationSubmittedAt"),
+ queryInterface.changeColumn("HackerProfiles", "status", {
+ type: Sequelize.ENUM,
+ values: [
+ "unverified",
+ "verified",
+ "submitted",
+ "accepted",
+ "waitlisted",
+ "rejected",
+ "confirmed",
+ "checkedIn"
+ ],
+ defaultValue: "unverified",
+ allowNull: false
+ }),
+ queryInterface.addColumn("HackerProfiles", "marketing", {
+ type: Sequelize.STRING(100)
+ }),
+ queryInterface.renameColumn(
+ "HackerProfiles",
+ "profileSubmittedAt",
+ "submittedAt"
+ )
+ ]);
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.addColumn("HackerProfiles", "applicationSubmittedAt", {
+ type: Sequelize.DATE
+ }),
+ queryInterface.changeColumn("HackerProfiles", "status", {
+ type: Sequelize.ENUM,
+ values: [
+ "unverified",
+ "verified",
+ "profileSubmitted",
+ "applicationSubmitted",
+ "accepted",
+ "waitlisted",
+ "rejected",
+ "confirmed",
+ "checkedIn"
+ ],
+ defaultValue: "unverified",
+ allowNull: false
+ }),
+ queryInterface.removeColumn("HackerProfiles", "marketing"),
+ queryInterface.renameColumn(
+ "HackerProfiles",
+ "submittedAt",
+ "profileSubmittedAt"
+ )
+ ]);
+ }
+};
diff --git a/lib/database/migrations/20191111203354-add-promo-codes.js b/lib/database/migrations/20191111203354-add-promo-codes.js
new file mode 100644
index 00000000..653c5d84
--- /dev/null
+++ b/lib/database/migrations/20191111203354-add-promo-codes.js
@@ -0,0 +1,33 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface
+ .addColumn("HackerProfiles", "promoCode", {
+ type: Sequelize.STRING(100),
+ unique: true
+ })
+ .then(function() {
+ return queryInterface.sequelize.query(`
+ UPDATE HackerProfiles
+ SET promoCode = (
+ SELECT substring(MD5(RAND()), -8)
+ )
+ WHERE promoCode IS NULL;
+
+ `);
+ }),
+ queryInterface.addColumn("HackerProfiles", "referrerCode", {
+ type: Sequelize.STRING(100)
+ })
+ ]);
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.removeColumn("HackerProfiles", "promoCode"),
+ queryInterface.removeColumn("HackerProfiles", "referrerCode")
+ ]);
+ }
+};
diff --git a/lib/database/migrations/20191114212136-teams.js b/lib/database/migrations/20191114212136-teams.js
new file mode 100644
index 00000000..7c1d50aa
--- /dev/null
+++ b/lib/database/migrations/20191114212136-teams.js
@@ -0,0 +1,47 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface
+ .createTable("Teams", {
+ id: {
+ allowNull: false,
+ type: Sequelize.INTEGER,
+ autoIncrement: true,
+ primaryKey: true
+ },
+ name: {
+ type: Sequelize.STRING(150),
+ allowNull: false
+ },
+ teamCode: {
+ type: Sequelize.STRING(4),
+ allowNull: false,
+ unique: true
+ },
+ ownerId: {
+ type: Sequelize.STRING,
+ references: {
+ model: "HackerProfiles",
+ key: "userId"
+ }
+ }
+ })
+ .then(() => {
+ return queryInterface.addColumn("HackerProfiles", "teamId", {
+ type: Sequelize.INTEGER,
+ references: {
+ model: "Teams",
+ key: "id"
+ }
+ });
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.removeColumn("HackerProfiles", "teamId"),
+ queryInterface.dropTable("Teams")
+ ]);
+ }
+};
diff --git a/lib/database/migrations/20191115010855-add-review-table.js b/lib/database/migrations/20191115010855-add-review-table.js
new file mode 100644
index 00000000..3fb6bbf5
--- /dev/null
+++ b/lib/database/migrations/20191115010855-add-review-table.js
@@ -0,0 +1,58 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable("HackerReviews", {
+ id: {
+ allowNull: false,
+ primaryKey: true,
+ autoIncrement: true,
+ type: Sequelize.INTEGER
+ },
+ hackerId: {
+ type: Sequelize.STRING,
+ references: {
+ model: "HackerProfiles",
+ key: "userId"
+ }
+ },
+ createdBy: {
+ // Should reference admin who created this review
+ allowNull: false,
+ type: Sequelize.STRING,
+ references: {
+ model: "HackerProfiles",
+ key: "userId"
+ }
+ },
+ scoreOne: {
+ allowNull: true,
+ type: Sequelize.INTEGER
+ },
+ scoreTwo: {
+ allowNull: true,
+ type: Sequelize.INTEGER
+ },
+ scoreThree: {
+ allowNull: true,
+ type: Sequelize.INTEGER
+ },
+ comments: {
+ allowNull: true,
+ type: Sequelize.STRING
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable("HackerReviews");
+ }
+};
diff --git a/lib/database/migrations/20191126000713-teams-add-dates.js b/lib/database/migrations/20191126000713-teams-add-dates.js
new file mode 100644
index 00000000..68f2b98f
--- /dev/null
+++ b/lib/database/migrations/20191126000713-teams-add-dates.js
@@ -0,0 +1,23 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.addColumn("Teams", "createdAt", {
+ type: Sequelize.DATE,
+ allowNull: false
+ }),
+ queryInterface.addColumn("Teams", "updatedAt", {
+ type: Sequelize.DATE,
+ allowNull: false
+ })
+ ]);
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.removeColumn("Teams", "createdAt"),
+ queryInterface.removeColumn("Teams", "updatedAt")
+ ]);
+ }
+};
diff --git a/lib/database/migrations/20191126043728-create-task-table.js b/lib/database/migrations/20191126043728-create-task-table.js
new file mode 100644
index 00000000..c343a000
--- /dev/null
+++ b/lib/database/migrations/20191126043728-create-task-table.js
@@ -0,0 +1,38 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable("Tasks", {
+ id: {
+ allowNull: false,
+ primaryKey: true,
+ autoIncrement: true,
+ type: Sequelize.INTEGER
+ },
+ points: {
+ allowNull: false,
+ type: Sequelize.INTEGER
+ },
+ description: {
+ allowNull: true,
+ type: Sequelize.STRING
+ },
+ blocking: {
+ allowNull: false,
+ type: Sequelize.BOOLEAN
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable("Tasks");
+ }
+};
diff --git a/lib/database/migrations/20191203052918-add-person-table.js b/lib/database/migrations/20191203052918-add-person-table.js
new file mode 100644
index 00000000..d462b0ae
--- /dev/null
+++ b/lib/database/migrations/20191203052918-add-person-table.js
@@ -0,0 +1,29 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable("persons", {
+ identityId: {
+ allowNull: false,
+ primaryKey: true,
+ type: Sequelize.STRING
+ },
+ isBattlepassComplete: {
+ allowNull: false,
+ type: Sequelize.BOOLEAN
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable("persons");
+ }
+};
diff --git a/lib/database/migrations/20191203062141-add-contribution-table.js b/lib/database/migrations/20191203062141-add-contribution-table.js
new file mode 100644
index 00000000..42a44bb6
--- /dev/null
+++ b/lib/database/migrations/20191203062141-add-contribution-table.js
@@ -0,0 +1,42 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable("Contributions", {
+ id: {
+ allowNull: false,
+ primaryKey: true,
+ autoIncrement: true,
+ type: Sequelize.INTEGER
+ },
+ personId: {
+ allowNull: false,
+ type: Sequelize.STRING,
+ references: {
+ model: "persons",
+ key: "identityId"
+ }
+ },
+ taskId: {
+ allowNull: false,
+ type: Sequelize.INTEGER,
+ references: {
+ model: "Tasks",
+ key: "id"
+ }
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable("Contributions");
+ }
+};
diff --git a/lib/database/migrations/20191209225538-add-houses-table.js b/lib/database/migrations/20191209225538-add-houses-table.js
new file mode 100644
index 00000000..f29a163a
--- /dev/null
+++ b/lib/database/migrations/20191209225538-add-houses-table.js
@@ -0,0 +1,46 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface
+ .createTable("Houses", {
+ id: {
+ type: Sequelize.INTEGER,
+ primaryKey: true,
+ autoIncrement: true,
+ allowNull: false
+ },
+ name: {
+ type: Sequelize.STRING,
+ allowNull: false
+ },
+ color: {
+ type: Sequelize.STRING,
+ allowNull: false
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ })
+ .then(() => {
+ return queryInterface.addColumn("persons", "houseId", {
+ type: Sequelize.INTEGER,
+ allowNull: true,
+ references: {
+ model: "Houses",
+ key: "id"
+ }
+ });
+ });
+ },
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.removeColumn("persons", "houseId").then(() => {
+ return queryInterface.dropTable("Houses");
+ });
+ }
+};
diff --git a/lib/database/migrations/20191223005530-confirmation-fields.js b/lib/database/migrations/20191223005530-confirmation-fields.js
new file mode 100644
index 00000000..68031975
--- /dev/null
+++ b/lib/database/migrations/20191223005530-confirmation-fields.js
@@ -0,0 +1,70 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.addColumn("HackerProfiles", "travelOrigin", {
+ type: Sequelize.STRING(500)
+ }),
+ queryInterface.addColumn("HackerProfiles", "travelMethod", {
+ type: Sequelize.ENUM,
+ values: ["driving", "bus", "flying", "usc", "other"]
+ }),
+ queryInterface.addColumn("HackerProfiles", "travelPlan", {
+ type: Sequelize.STRING(500)
+ }),
+ queryInterface.addColumn("HackerProfiles", "dietaryRestrictions", {
+ type: Sequelize.STRING(1000)
+ }),
+ queryInterface.addColumn("HackerProfiles", "confirmCodeOfConduct", {
+ type: Sequelize.BOOLEAN
+ }),
+ queryInterface.addColumn("HackerProfiles", "shirtSize", {
+ type: Sequelize.ENUM,
+ values: ["xs", "s", "m", "l", "xl"]
+ }),
+ queryInterface.changeColumn("HackerProfiles", "status", {
+ type: Sequelize.ENUM,
+ values: [
+ "unverified",
+ "verified",
+ "submitted",
+ "accepted",
+ "waitlisted",
+ "rejected",
+ "confirmed",
+ "declined",
+ "checkedIn"
+ ],
+ defaultValue: "unverified",
+ allowNull: false
+ })
+ ]);
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.removeColumn("HackerProfiles", "travelOrigin"),
+ queryInterface.removeColumn("HackerProfiles", "travelMethod"),
+ queryInterface.removeColumn("HackerProfiles", "travelPlan"),
+ queryInterface.removeColumn("HackerProfiles", "dietaryRestrictions"),
+ queryInterface.removeColumn("HackerProfiles", "shirtSize"),
+ queryInterface.removeColumn("HackerProfiles", "confirmCodeOfConduct"),
+ queryInterface.changeColumn("HackerProfiles", "status", {
+ type: Sequelize.ENUM,
+ values: [
+ "unverified",
+ "verified",
+ "submitted",
+ "accepted",
+ "waitlisted",
+ "rejected",
+ "confirmed",
+ "checkedIn"
+ ],
+ defaultValue: "unverified",
+ allowNull: false
+ })
+ ]);
+ }
+};
diff --git a/lib/database/migrations/20191224053622-no-bus-check.js b/lib/database/migrations/20191224053622-no-bus-check.js
new file mode 100644
index 00000000..851d9cc6
--- /dev/null
+++ b/lib/database/migrations/20191224053622-no-bus-check.js
@@ -0,0 +1,15 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.addColumn("HackerProfiles", "noBusCheck", {
+ type: Sequelize.BOOLEAN
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.removeColumn("HackerProfiles", "noBusCheck", {
+ type: Sequelize.BOOLEAN
+ });
+ }
+};
diff --git a/lib/database/migrations/20191226014741-confirmed-at-field.js b/lib/database/migrations/20191226014741-confirmed-at-field.js
new file mode 100644
index 00000000..0288f9b7
--- /dev/null
+++ b/lib/database/migrations/20191226014741-confirmed-at-field.js
@@ -0,0 +1,21 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.addColumn("HackerProfiles", "confirmedAt", {
+ type: Sequelize.DATE
+ }),
+ queryInterface.addColumn("HackerProfiles", "declinedAt", {
+ type: Sequelize.DATE
+ })
+ ]);
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.removeColumn("HackerProfiles", "confirmedAt"),
+ queryInterface.removeColumn("HackerProfiles", "declinedAt")
+ ]);
+ }
+};
diff --git a/lib/database/migrations/20200115053213-add-task-groupings.js b/lib/database/migrations/20200115053213-add-task-groupings.js
new file mode 100644
index 00000000..2910a2ac
--- /dev/null
+++ b/lib/database/migrations/20200115053213-add-task-groupings.js
@@ -0,0 +1,43 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface
+ .createTable("Groupings", {
+ id: {
+ type: Sequelize.INTEGER,
+ primaryKey: true,
+ autoIncrement: true,
+ allowNull: false
+ },
+ name: {
+ type: Sequelize.STRING,
+ allowNull: false
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ })
+ .then(() => {
+ return queryInterface.addColumn("Tasks", "groupingId", {
+ type: Sequelize.INTEGER,
+ allowNull: true,
+ references: {
+ model: "Groupings",
+ key: "id"
+ }
+ });
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.removeColumn("Tasks", "groupingId").then(() => {
+ return queryInterface.dropTable("Groupings");
+ });
+ }
+};
diff --git a/lib/database/migrations/20200115054722-add-multipliers-table.js b/lib/database/migrations/20200115054722-add-multipliers-table.js
new file mode 100644
index 00000000..23e54a6d
--- /dev/null
+++ b/lib/database/migrations/20200115054722-add-multipliers-table.js
@@ -0,0 +1,42 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable("Multipliers", {
+ id: {
+ type: Sequelize.INTEGER,
+ primaryKey: true,
+ autoIncrement: true,
+ allowNull: false
+ },
+ name: {
+ type: Sequelize.STRING,
+ allowNull: false
+ },
+ multiplierValue: {
+ type: Sequelize.INTEGER,
+ allowNull: false
+ },
+ groupingId: {
+ type: Sequelize.INTEGER,
+ allowNull: false,
+ references: {
+ model: "Groupings",
+ key: "id"
+ }
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable("Multipliers");
+ }
+};
diff --git a/lib/database/migrations/20200115061612-add-actions-table.js b/lib/database/migrations/20200115061612-add-actions-table.js
new file mode 100644
index 00000000..8bf32f4b
--- /dev/null
+++ b/lib/database/migrations/20200115061612-add-actions-table.js
@@ -0,0 +1,34 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable("Actions", {
+ id: {
+ type: Sequelize.INTEGER,
+ primaryKey: true,
+ autoIncrement: true,
+ allowNull: false
+ },
+ name: {
+ type: Sequelize.STRING,
+ allowNull: false
+ },
+ role: {
+ type: Sequelize.STRING,
+ allowNull: false
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable("Actions");
+ }
+};
diff --git a/lib/database/migrations/20200115235853-add-travel-column.js b/lib/database/migrations/20200115235853-add-travel-column.js
new file mode 100644
index 00000000..b03c91d0
--- /dev/null
+++ b/lib/database/migrations/20200115235853-add-travel-column.js
@@ -0,0 +1,22 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.addColumn("HackerProfiles", "travelStatus", {
+ type: Sequelize.ENUM,
+ values: [
+ "ineligible",
+ "needed",
+ "unneeded",
+ "declined",
+ "unknown",
+ "submitted",
+ "reimbursed"
+ ]
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.removeColumn("HackerProfiles", "travelStatus");
+ }
+};
diff --git a/lib/database/migrations/20200118201352-add-task-type.js b/lib/database/migrations/20200118201352-add-task-type.js
new file mode 100644
index 00000000..4631e091
--- /dev/null
+++ b/lib/database/migrations/20200118201352-add-task-type.js
@@ -0,0 +1,23 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.addColumn("Tasks", "type", {
+ type: Sequelize.STRING,
+ allowNull: false
+ }),
+ queryInterface.addColumn("Tasks", "name", {
+ type: Sequelize.STRING,
+ allowNull: false
+ })
+ ]);
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.removeColumn("Tasks", "type"),
+ queryInterface.removeColumn("Tasks", "name")
+ ]);
+ }
+};
diff --git a/lib/database/migrations/20200120005508-create-prize.js b/lib/database/migrations/20200120005508-create-prize.js
new file mode 100644
index 00000000..fdb67388
--- /dev/null
+++ b/lib/database/migrations/20200120005508-create-prize.js
@@ -0,0 +1,30 @@
+"use strict";
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable("Prizes", {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ title: {
+ type: Sequelize.STRING
+ },
+ description: {
+ type: Sequelize.STRING
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ });
+ },
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable("Prizes");
+ }
+};
diff --git a/lib/database/migrations/20200120005653-create-projectTeam.js b/lib/database/migrations/20200120005653-create-projectTeam.js
new file mode 100644
index 00000000..2b8bbc18
--- /dev/null
+++ b/lib/database/migrations/20200120005653-create-projectTeam.js
@@ -0,0 +1,33 @@
+"use strict";
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable("ProjectTeams", {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ name: {
+ type: Sequelize.STRING
+ },
+ devpostLink: {
+ type: Sequelize.STRING
+ },
+ githubLink: {
+ type: Sequelize.STRING
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ });
+ },
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable("ProjectTeams");
+ }
+};
diff --git a/lib/database/migrations/20200120005958-associate-projectTeams-prizes.js b/lib/database/migrations/20200120005958-associate-projectTeams-prizes.js
new file mode 100644
index 00000000..5b34c925
--- /dev/null
+++ b/lib/database/migrations/20200120005958-associate-projectTeams-prizes.js
@@ -0,0 +1,32 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable("ProjectTeamPrizes", {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ projectTeam: {
+ type: Sequelize.INTEGER,
+ references: {
+ model: "ProjectTeams",
+ key: "id"
+ }
+ },
+ prize: {
+ type: Sequelize.INTEGER,
+ references: {
+ model: "Prizes",
+ key: "id"
+ }
+ }
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable("ProjectTeamPrizes");
+ }
+};
diff --git a/lib/database/migrations/20200120011322-associate-projectTeams-persons.js b/lib/database/migrations/20200120011322-associate-projectTeams-persons.js
new file mode 100644
index 00000000..7d6e8a83
--- /dev/null
+++ b/lib/database/migrations/20200120011322-associate-projectTeams-persons.js
@@ -0,0 +1,17 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.addColumn("persons", "projectTeamId", {
+ type: Sequelize.INTEGER,
+ references: {
+ model: "ProjectTeams",
+ key: "id"
+ }
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.removeColumn("persons", "projectTeamId");
+ }
+};
diff --git a/lib/database/migrations/20200124192357-augment-contrib-with-multiplier.js b/lib/database/migrations/20200124192357-augment-contrib-with-multiplier.js
new file mode 100644
index 00000000..b5587e74
--- /dev/null
+++ b/lib/database/migrations/20200124192357-augment-contrib-with-multiplier.js
@@ -0,0 +1,13 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.addColumn("Contributions", "multiplier", {
+ type: Sequelize.INTEGER
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.removeColumn("Contributions", "multiplier");
+ }
+};
diff --git a/lib/database/migrations/20200124194744-add-schedule-table.js b/lib/database/migrations/20200124194744-add-schedule-table.js
new file mode 100644
index 00000000..fe76211d
--- /dev/null
+++ b/lib/database/migrations/20200124194744-add-schedule-table.js
@@ -0,0 +1,40 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable("Events", {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ name: {
+ type: Sequelize.STRING
+ },
+ description: {
+ type: Sequelize.STRING
+ },
+ startsAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ endsAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable("Events");
+ }
+};
diff --git a/lib/database/migrations/20200126211953-add-scanner-id-column.js b/lib/database/migrations/20200126211953-add-scanner-id-column.js
new file mode 100644
index 00000000..b6b737d5
--- /dev/null
+++ b/lib/database/migrations/20200126211953-add-scanner-id-column.js
@@ -0,0 +1,17 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.addColumn("Contributions", "scannerId", {
+ type: Sequelize.STRING,
+ references: {
+ model: "HackerProfiles",
+ key: "userId"
+ }
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.removeColumn("Contributions", "scannerId");
+ }
+};
diff --git a/lib/database/migrations/20200126213315-adds-timestamps-to-prizes.js b/lib/database/migrations/20200126213315-adds-timestamps-to-prizes.js
new file mode 100644
index 00000000..f737e921
--- /dev/null
+++ b/lib/database/migrations/20200126213315-adds-timestamps-to-prizes.js
@@ -0,0 +1,23 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.addColumn("ProjectTeamPrizes", "createdAt", {
+ type: Sequelize.DATE,
+ allowNull: false
+ }),
+ queryInterface.addColumn("ProjectTeamPrizes", "updatedAt", {
+ type: Sequelize.DATE,
+ allowNull: false
+ })
+ ]);
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.removeColumn("ProjectTeamPrizes", "createdAt"),
+ queryInterface.removeColumn("ProjectTeamPrizes", "updatedAt")
+ ]);
+ }
+};
diff --git a/lib/database/migrations/20200126213646-add-is-group-task-flag.js b/lib/database/migrations/20200126213646-add-is-group-task-flag.js
new file mode 100644
index 00000000..1bba995b
--- /dev/null
+++ b/lib/database/migrations/20200126213646-add-is-group-task-flag.js
@@ -0,0 +1,13 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.addColumn("Tasks", "isGroupTask", {
+ type: Sequelize.BOOLEAN
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.removeColumn("Tasks", "isGroupTask");
+ }
+};
diff --git a/lib/database/migrations/20200126215010-add-unlockables-table.js b/lib/database/migrations/20200126215010-add-unlockables-table.js
new file mode 100644
index 00000000..4e71b1e6
--- /dev/null
+++ b/lib/database/migrations/20200126215010-add-unlockables-table.js
@@ -0,0 +1,38 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable("Unlockables", {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ tier: {
+ allowNull: false,
+ type: Sequelize.INTEGER
+ },
+ pointThreshold: {
+ allowNull: false,
+ type: Sequelize.INTEGER
+ },
+ isPremium: {
+ allowNull: false,
+ type: Sequelize.BOOLEAN
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable("Unlockables");
+ }
+};
diff --git a/lib/database/migrations/20200127000435-modify-role-superadmin-volunteer.js b/lib/database/migrations/20200127000435-modify-role-superadmin-volunteer.js
new file mode 100644
index 00000000..f7b35e27
--- /dev/null
+++ b/lib/database/migrations/20200127000435-modify-role-superadmin-volunteer.js
@@ -0,0 +1,19 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.changeColumn("HackerProfiles", "role", {
+ type: Sequelize.ENUM,
+ values: ["hacker", "admin", "sponsor", "volunteer"],
+ defaultValue: "hacker"
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.changeColumn("HackerProfiles", "role", {
+ type: Sequelize.ENUM,
+ values: ["hacker", "admin", "sponsor", "superadmin"],
+ defaultValue: "hacker"
+ });
+ }
+};
diff --git a/lib/database/migrations/20200127004044-add-is-active-field.js b/lib/database/migrations/20200127004044-add-is-active-field.js
new file mode 100644
index 00000000..4cbe848c
--- /dev/null
+++ b/lib/database/migrations/20200127004044-add-is-active-field.js
@@ -0,0 +1,13 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.addColumn("Tasks", "isActive", {
+ type: Sequelize.BOOLEAN
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.removeColumn("Tasks", "isActive");
+ }
+};
diff --git a/lib/database/migrations/20200127012812-add-qrcode-to-hacker-profile.js b/lib/database/migrations/20200127012812-add-qrcode-to-hacker-profile.js
new file mode 100644
index 00000000..37579e5a
--- /dev/null
+++ b/lib/database/migrations/20200127012812-add-qrcode-to-hacker-profile.js
@@ -0,0 +1,14 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.addColumn("HackerProfiles", "qrCodeId", {
+ type: Sequelize.STRING,
+ unique: true
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.removeColumn("HackerProfiles", "qrCodeId");
+ }
+};
diff --git a/lib/database/migrations/20200201211804-add-sponsor-and-isPast-flag.js b/lib/database/migrations/20200201211804-add-sponsor-and-isPast-flag.js
new file mode 100644
index 00000000..157171a1
--- /dev/null
+++ b/lib/database/migrations/20200201211804-add-sponsor-and-isPast-flag.js
@@ -0,0 +1,23 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.addColumn("Tasks", "sponsor", {
+ type: Sequelize.STRING,
+ allowNull: true
+ }),
+ queryInterface.addColumn("Tasks", "isPast", {
+ type: Sequelize.BOOLEAN,
+ allowNull: false
+ })
+ ]);
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.removeColumn("Tasks", "sponsor"),
+ queryInterface.removeColumn("Tasks", "isPast")
+ ]);
+ }
+};
diff --git a/lib/database/migrations/20200810155744-give_id_default_value.js b/lib/database/migrations/20200810155744-give_id_default_value.js
new file mode 100644
index 00000000..a06b994d
--- /dev/null
+++ b/lib/database/migrations/20200810155744-give_id_default_value.js
@@ -0,0 +1,23 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.changeColumn("HackerProfiles", "id", {
+ type: Sequelize.STRING,
+ allowNull: false,
+ defaultValue: "hacker_id"
+ })
+ ]);
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.changeColumn("HackerProfiles", "id", {
+ type: Sequelize.STRING,
+ allowNull: false,
+ defaultValue: ""
+ })
+ ]);
+ }
+};
diff --git a/lib/database/models/action.ts b/lib/database/models/action.ts
new file mode 100644
index 00000000..b5c066b7
--- /dev/null
+++ b/lib/database/models/action.ts
@@ -0,0 +1,14 @@
+const action = (sequelize, DataTypes) => {
+ const Action = sequelize.define(
+ "Action",
+ {
+ role: DataTypes.STRING(100),
+ name: DataTypes.STRING(100)
+ },
+ { tableName: "Actions" }
+ );
+ Action.associate = models => {};
+ return Action;
+};
+
+export default action;
diff --git a/lib/database/models/contribution.ts b/lib/database/models/contribution.ts
new file mode 100644
index 00000000..6f162301
--- /dev/null
+++ b/lib/database/models/contribution.ts
@@ -0,0 +1,27 @@
+const contribution = (sequelize, DataTypes) => {
+ const Contribution = sequelize.define(
+ "Contribution",
+ {
+ personId: DataTypes.STRING(100),
+ multiplier: DataTypes.INTEGER,
+ scannerId: DataTypes.STRING(100),
+ taskId: DataTypes.INTEGER
+ },
+ {
+ tableName: "Contributions"
+ }
+ );
+ Contribution.associate = function(models) {
+ Contribution.belongsTo(models.Person, {
+ foreignKey: "personId",
+ targetKey: "identityId"
+ });
+ Contribution.belongsTo(models.Task, { foreignKey: "taskId" });
+ Contribution.addScope("defaultScope", {
+ include: [{ model: models.Task }]
+ });
+ };
+ return Contribution;
+};
+
+export default contribution;
diff --git a/lib/database/models/event.ts b/lib/database/models/event.ts
new file mode 100644
index 00000000..affacbc1
--- /dev/null
+++ b/lib/database/models/event.ts
@@ -0,0 +1,17 @@
+const event = (sequelize, DataTypes) => {
+ const Event = sequelize.define(
+ "Event",
+ {
+ name: DataTypes.STRING(100),
+ description: DataTypes.STRING(500),
+ startsAt: DataTypes.DATE,
+ endsAt: DataTypes.DATE
+ },
+ {}
+ );
+
+ Event.associate = models => {};
+ return Event;
+};
+
+export default event;
diff --git a/lib/database/models/groupings.ts b/lib/database/models/groupings.ts
new file mode 100644
index 00000000..b6121fc0
--- /dev/null
+++ b/lib/database/models/groupings.ts
@@ -0,0 +1,16 @@
+const groupings = (sequelize, DataTypes) => {
+ const Grouping = sequelize.define(
+ "Grouping",
+ {
+ name: DataTypes.STRING(100)
+ },
+ { tableName: "Groupings" }
+ );
+ Grouping.associate = models => {
+ models.Task.belongsTo(models.Grouping, { foreignKey: "groupingId" });
+ Grouping.hasMany(models.Task, { foreignKey: "groupingId" });
+ };
+ return Grouping;
+};
+
+export default groupings;
diff --git a/lib/database/models/hackerProfile.ts b/lib/database/models/hackerProfile.ts
new file mode 100644
index 00000000..7b8e75ca
--- /dev/null
+++ b/lib/database/models/hackerProfile.ts
@@ -0,0 +1,150 @@
+// If you make any changes to HackerProfile, make sure you do the following:
+// 1) Generate a Sequelize migration that adds/removes columns as needed
+// 2) Update the Profile type definition in odyssey.d.ts
+const profile = (sequelize, DataTypes) => {
+ const HackerProfile = sequelize.define(
+ "HackerProfile",
+ {
+ id: {
+ type: DataTypes.STRING,
+ defaultValue: "hacker_id",
+ allowNull: false
+ },
+ gender: {
+ type: DataTypes.ENUM,
+ values: ["male", "female", "non-binary", "other", "no-say"]
+ },
+ userId: {
+ type: DataTypes.STRING,
+ primaryKey: true,
+ allowNull: false
+ },
+ travelStatus: {
+ type: DataTypes.ENUM,
+ values: [
+ "ineligible",
+ "needed",
+ "unneeded",
+ "declined",
+ "unknown",
+ "submitted",
+ "reimbursed"
+ ]
+ },
+ ethnicity: DataTypes.STRING,
+ email: DataTypes.STRING,
+ major: DataTypes.STRING,
+ minor: DataTypes.STRING,
+ resume: DataTypes.STRING,
+ skills: DataTypes.STRING,
+ interests: DataTypes.STRING,
+ submittedAt: DataTypes.DATE,
+ status: {
+ type: DataTypes.ENUM,
+ values: [
+ "unverified",
+ "verified",
+ "submitted",
+ "accepted",
+ "waitlisted",
+ "rejected",
+ "confirmed",
+ "declined",
+ "checkedIn"
+ ],
+ defaultValue: "unverified",
+ allowNull: false
+ },
+ firstName: DataTypes.STRING,
+ lastName: DataTypes.STRING,
+ phoneNumber: DataTypes.STRING,
+ school: DataTypes.STRING,
+ year: {
+ type: DataTypes.ENUM,
+ values: ["freshman", "sophomore", "junior", "senior", "graduate"]
+ },
+ skillLevel: {
+ type: DataTypes.ENUM,
+ values: ["beginner", "intermediate", "advanced"]
+ },
+ questionOne: DataTypes.STRING(1000),
+ questionTwo: DataTypes.STRING(1000),
+ questionThree: DataTypes.STRING(1000),
+ role: {
+ type: DataTypes.ENUM,
+ values: ["hacker", "admin", "sponsor", "volunteer"],
+ defaultValue: "hacker"
+ },
+ graduationDate: {
+ type: DataTypes.ENUM,
+ values: [
+ "spring-2020",
+ "fall-2020",
+ "spring-2021",
+ "fall-2021",
+ "spring-2022",
+ "fall-2022",
+ "spring-2023",
+ "fall-2023",
+ "other"
+ ]
+ },
+ over18: DataTypes.BOOLEAN,
+ needBus: DataTypes.BOOLEAN,
+ links: DataTypes.STRING(1000),
+ codeOfConduct: DataTypes.BOOLEAN,
+ authorize: DataTypes.BOOLEAN,
+ marketing: DataTypes.STRING(100),
+ promoCode: {
+ type: DataTypes.STRING(100),
+ defaultValue: function() {
+ //Random string of length 8
+ return (Math.random().toString(36) + "00000000000000000").slice(
+ 2,
+ 10
+ );
+ }
+ },
+ referrerCode: DataTypes.STRING(100),
+ referred: DataTypes.VIRTUAL,
+ travelOrigin: DataTypes.STRING(500),
+ travelMethod: {
+ type: DataTypes.ENUM,
+ values: ["driving", "bus", "flying", "usc", "other"]
+ },
+ shirtSize: {
+ type: DataTypes.ENUM,
+ values: ["xs", "s", "m", "l", "xl"]
+ },
+ travelPlan: DataTypes.STRING(500),
+ dietaryRestrictions: DataTypes.STRING(1000),
+ confirmCodeOfConduct: DataTypes.BOOLEAN,
+ noBusCheck: DataTypes.BOOLEAN,
+ confirmedAt: DataTypes.DATE,
+ declinedAt: DataTypes.DATE,
+ qrCodeId: {
+ type: DataTypes.STRING,
+ unique: true
+ }
+ },
+ {}
+ );
+
+ HackerProfile.prototype.getReferred = () => {
+ return sequelize.models.HackerProfile.findAll({
+ where: {
+ referrerCode: this.promoCode
+ }
+ });
+ };
+ HackerProfile.associate = models => {
+ HackerProfile.belongsTo(models.Team, {
+ as: "team",
+ foreignKey: "teamId",
+ constraints: false
+ });
+ };
+ return HackerProfile;
+};
+
+export default profile;
diff --git a/lib/database/models/hackerReview.ts b/lib/database/models/hackerReview.ts
new file mode 100644
index 00000000..0086f918
--- /dev/null
+++ b/lib/database/models/hackerReview.ts
@@ -0,0 +1,24 @@
+const review = (sequelize, DataTypes) => {
+ const HackerReview = sequelize.define(
+ "HackerReview",
+ {
+ hackerId: DataTypes.STRING(100),
+ createdBy: DataTypes.STRING(100),
+ scoreOne: DataTypes.INTEGER,
+ scoreTwo: DataTypes.INTEGER,
+ scoreThree: DataTypes.INTEGER,
+ comments: DataTypes.STRING
+ },
+ {}
+ );
+ HackerReview.associate = models => {
+ // associations can be defined here
+ models.HackerProfile.hasMany(HackerReview, { foreignKey: "hackerId" });
+ models.HackerReview.belongsTo(models.HackerProfile, {
+ foreignKey: "createdBy"
+ });
+ };
+ return HackerReview;
+};
+
+export default review;
diff --git a/lib/database/models/house.ts b/lib/database/models/house.ts
new file mode 100644
index 00000000..3cf3dffa
--- /dev/null
+++ b/lib/database/models/house.ts
@@ -0,0 +1,21 @@
+const house = (sequelize, DataTypes) => {
+ const House = sequelize.define(
+ "House",
+ {
+ name: DataTypes.STRING(100),
+ color: DataTypes.STRING(100)
+ },
+ { tableName: "Houses" }
+ );
+ House.associate = models => {
+ House.hasMany(models.Person, {
+ foreignKey: "houseId",
+ as: "HouseMembers",
+ constraints: false
+ });
+ };
+
+ return House;
+};
+
+export default house;
diff --git a/lib/database/models/index.ts b/lib/database/models/index.ts
new file mode 100644
index 00000000..73f59e79
--- /dev/null
+++ b/lib/database/models/index.ts
@@ -0,0 +1,33 @@
+const Sequelize = require("sequelize");
+const env = process.env.NODE_ENV || "test";
+
+import config from "../config";
+
+let env_config = config[env];
+
+let db = {
+ sequelize: null,
+ Sequelize: null
+};
+
+let sequelize = new Sequelize(
+ env_config.database,
+ env_config.username,
+ env_config.password,
+ env_config
+);
+
+let model;
+model = sequelize["import"]("./action");
+db[model.name] = model;
+
+Object.keys(db).forEach(modelName => {
+ if (db[modelName].associate) {
+ db[modelName].associate(db);
+ }
+});
+
+db.sequelize = sequelize;
+db.Sequelize = Sequelize;
+
+export default db;
diff --git a/lib/database/models/multiplier.ts b/lib/database/models/multiplier.ts
new file mode 100644
index 00000000..b284f5c2
--- /dev/null
+++ b/lib/database/models/multiplier.ts
@@ -0,0 +1,18 @@
+const multiplier = (sequelize, DataTypes) => {
+ const Multiplier = sequelize.define(
+ "Multiplier",
+ {
+ name: DataTypes.STRING,
+ multiplierValue: DataTypes.INTEGER,
+ groupingId: DataTypes.INTEGER
+ },
+ { tableName: "Multipliers" }
+ );
+ Multiplier.associate = function(models) {
+ Multiplier.belongsTo(models.Grouping);
+ models.Grouping.hasMany(Multiplier, { foreignKey: "groupingId" });
+ };
+ return Multiplier;
+};
+
+export default multiplier;
diff --git a/lib/database/models/person.ts b/lib/database/models/person.ts
new file mode 100644
index 00000000..fc161703
--- /dev/null
+++ b/lib/database/models/person.ts
@@ -0,0 +1,49 @@
+const person = (sequelize, DataTypes) => {
+ const Person = sequelize.define(
+ "Person",
+ {
+ identityId: {
+ type: DataTypes.STRING(100),
+ primaryKey: true
+ },
+ isBattlepassComplete: DataTypes.BOOLEAN,
+ ProjectTeamId: DataTypes.NUMBER
+ },
+ {
+ tableName: "persons",
+ defaultScope: {
+ include: [
+ { model: sequelize.models.HackerProfile, as: "Profile" },
+ { model: sequelize.models.Contribution, as: "Contributions" },
+ { model: sequelize.models.House, as: "Home" }
+ ]
+ }
+ }
+ );
+
+ Person.associate = models => {
+ Person.belongsTo(models.House, {
+ foreignKey: "houseId",
+ as: "Home",
+ constraints: false
+ });
+ Person.belongsTo(models.ProjectTeam);
+ Person.belongsTo(models.HackerProfile, {
+ foreignKey: "identityId",
+ targetKey: "userId",
+ constraints: false,
+ as: "Profile"
+ });
+ Person.hasMany(models.Contribution, { foreignKey: "personId" });
+ Person.addScope("hideProfile", {
+ include: [
+ { model: sequelize.models.Contribution, as: "Contributions" },
+ { model: sequelize.models.House, as: "Home" }
+ ]
+ });
+ };
+
+ return Person;
+};
+
+export default person;
diff --git a/lib/database/models/prize.ts b/lib/database/models/prize.ts
new file mode 100644
index 00000000..bb1bae95
--- /dev/null
+++ b/lib/database/models/prize.ts
@@ -0,0 +1,21 @@
+const prize = (sequelize, DataTypes) => {
+ const Prize = sequelize.define(
+ "Prize",
+ {
+ title: DataTypes.STRING,
+ description: DataTypes.STRING
+ },
+ {}
+ );
+ Prize.associate = models => {
+ Prize.belongsToMany(models.ProjectTeam, {
+ through: "ProjectTeamPrizes",
+ foreignKey: "prize",
+ as: "Prizes",
+ otherKey: "projectTeam"
+ });
+ };
+ return Prize;
+};
+
+export default prize;
diff --git a/lib/database/models/projectTeam.ts b/lib/database/models/projectTeam.ts
new file mode 100644
index 00000000..b6a07256
--- /dev/null
+++ b/lib/database/models/projectTeam.ts
@@ -0,0 +1,30 @@
+const team = (sequelize, DataTypes) => {
+ const ProjectTeam = sequelize.define(
+ "ProjectTeam",
+ {
+ name: DataTypes.STRING,
+ devpostLink: DataTypes.STRING,
+ githubLink: DataTypes.STRING
+ },
+ {
+ defaultScope: {
+ include: [
+ { model: sequelize.models.Prize, as: "Prizes" },
+ { model: sequelize.models.Person, as: "Members" }
+ ]
+ }
+ }
+ );
+ ProjectTeam.associate = models => {
+ ProjectTeam.hasMany(models.Person, { as: "Members" });
+ ProjectTeam.belongsToMany(models.Prize, {
+ through: "ProjectTeamPrizes",
+ foreignKey: "projectTeam",
+ as: "Prizes",
+ otherKey: "prize"
+ });
+ };
+ return ProjectTeam;
+};
+
+export default team;
diff --git a/lib/database/models/projectTeamPrize.ts b/lib/database/models/projectTeamPrize.ts
new file mode 100644
index 00000000..9ef6aeeb
--- /dev/null
+++ b/lib/database/models/projectTeamPrize.ts
@@ -0,0 +1,9 @@
+const teamPrize = (sequelize, DataTypes) => {
+ const ProjectTeamPrizes = sequelize.define("ProjectTeamPrizes", {
+ ProjectTeam: DataTypes.NUMBER,
+ Prize: DataTypes.NUMBER
+ });
+ return ProjectTeamPrizes;
+};
+
+export default teamPrize;
diff --git a/lib/database/models/task.ts b/lib/database/models/task.ts
new file mode 100644
index 00000000..a0af6390
--- /dev/null
+++ b/lib/database/models/task.ts
@@ -0,0 +1,25 @@
+const task = (sequelize, DataTypes) => {
+ const Task = sequelize.define(
+ "Task",
+ {
+ points: DataTypes.INTEGER,
+ description: DataTypes.STRING(100),
+ blocking: DataTypes.BOOLEAN,
+ type: DataTypes.STRING(100),
+ isGroupTask: DataTypes.BOOLEAN,
+ isActive: DataTypes.BOOLEAN,
+ sponsor: DataTypes.STRING,
+ isPast: DataTypes.BOOLEAN,
+ name: DataTypes.STRING(100)
+ },
+ {}
+ );
+ Task.associate = models => {
+ Task.hasMany(models.Contribution, { foreignKey: "taskId" });
+ // associations can be defined here
+ };
+
+ return Task;
+};
+
+export default task;
diff --git a/lib/database/models/team.ts b/lib/database/models/team.ts
new file mode 100644
index 00000000..5d1cfa38
--- /dev/null
+++ b/lib/database/models/team.ts
@@ -0,0 +1,34 @@
+const team = (sequelize, DataTypes) => {
+ const Team = sequelize.define(
+ "Team",
+ {
+ name: DataTypes.STRING(150),
+ teamCode: {
+ type: DataTypes.STRING(4),
+ unique: true
+ },
+ ownerId: {
+ type: DataTypes.STRING,
+ references: {
+ model: "HackerProfiles",
+ key: "userId"
+ }
+ }
+ },
+ {}
+ );
+ Team.associate = function(models) {
+ Team.belongsTo(models.HackerProfile, {
+ as: "owner",
+ foreignKey: "ownerId",
+ constraints: false
+ });
+ Team.hasMany(models.HackerProfile, {
+ foreignKey: "teamId",
+ constraints: false
+ });
+ };
+ return Team;
+};
+
+export default team;
diff --git a/lib/database/models/unlockable.ts b/lib/database/models/unlockable.ts
new file mode 100644
index 00000000..3c7cea7f
--- /dev/null
+++ b/lib/database/models/unlockable.ts
@@ -0,0 +1,15 @@
+const unlockable = (sequelize, DataTypes) => {
+ const Unlockable = sequelize.define(
+ "Unlockable",
+ {
+ tier: DataTypes.INTEGER,
+ pointThreshold: DataTypes.INTEGER,
+ isPremium: DataTypes.BOOLEAN
+ },
+ {}
+ );
+ Unlockable.associate = models => {};
+ return Unlockable;
+};
+
+export default unlockable;
diff --git a/lib/utils.ts b/lib/database/utils.ts
similarity index 98%
rename from lib/utils.ts
rename to lib/database/utils.ts
index 55db386c..253109f4 100644
--- a/lib/utils.ts
+++ b/lib/database/utils.ts
@@ -1,4 +1,4 @@
-const models = require("./models");
+import models from "./models";
const Sentry = require("@sentry/node");
module.exports = {
diff --git a/lib/db.ts b/lib/db.ts
new file mode 100644
index 00000000..518488cf
--- /dev/null
+++ b/lib/db.ts
@@ -0,0 +1,20 @@
+const mysql = require("serverless-mysql");
+
+const db = mysql({
+ config: {
+ host: process.env.MYSQL_HOST,
+ database: process.env.MYSQL_DATABASE,
+ user: process.env.MYSQL_USER,
+ password: process.env.MYSQL_PASSWORD
+ }
+});
+
+exports.query = async query => {
+ try {
+ const results = await db.query(query);
+ await db.end();
+ return results;
+ } catch (error) {
+ return { error };
+ }
+};
diff --git a/package.json b/package.json
index 236bf587..4f6b80c2 100644
--- a/package.json
+++ b/package.json
@@ -55,6 +55,7 @@
"js-cookie": "^2.2.1",
"jsqr": "^1.2.0",
"jszip": "^3.2.2",
+ "list-react-files": "^0.2.0",
"moment": "^2.24.0",
"mysql": "^2.17.1",
"mysql2": "^1.7.0",
@@ -83,7 +84,9 @@
"react-use": "^13.8.1",
"sequelize": "^5.21.1",
"serialize-javascript": "^3.1.0",
+ "serverless-mysql": "^1.5.4",
"simplex-noise": "^2.4.0",
+ "sql-template-strings": "^2.2.2",
"start-server-and-test": "^1.10.6",
"styled-components": "^5.0.0",
"styled-reset": "^4.0.2",
diff --git a/pages/api/admin/review.ts b/pages/api/admin/review.ts
index e69de29b..544056a8 100644
--- a/pages/api/admin/review.ts
+++ b/pages/api/admin/review.ts
@@ -0,0 +1,41 @@
+module.exports = async (req, res) => {
+ try {
+ const profilesWCount = await models.HackerProfile.findAll({
+ attributes: {
+ include: [
+ [
+ sequelize.fn("COUNT", sequelize.col("HackerReviews.id")),
+ "reviewCount"
+ ]
+ ]
+ },
+ include: [
+ {
+ model: models.HackerReview,
+ attributes: []
+ }
+ ],
+ group: ["HackerProfile.userId"]
+ });
+
+ const acceptableProfile = profilesWCount.find(profile => {
+ return profile.dataValues.reviewCount < 1;
+ });
+ if (acceptableProfile) {
+ const newReview = await models.HackerReview.create({
+ hackerId: acceptableProfile.dataValues.userId,
+ createdBy: req.user.id
+ });
+
+ return res.json({
+ review: newReview,
+ profile: acceptableProfile
+ });
+ } else {
+ return res.json({ review: null, profile: null }); // Returns empty when there are no more profiles
+ }
+ } catch (e) {
+ console.log(e);
+ return res.status(500).json({ err: e });
+ }
+};
diff --git a/yarn.lock b/yarn.lock
index bfa21a69..8ba24a87 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1925,6 +1925,13 @@
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
+"@types/mysql@^2.15.6":
+ version "2.15.15"
+ resolved "https://registry.yarnpkg.com/@types/mysql/-/mysql-2.15.15.tgz#af2223d2841091a5a819eabee6dff19567f1cf1f"
+ integrity sha512-1GJnq7RwuFPRicMHdT53vza5v39nep9OKIbozxNUpFXP04CydcdWrqpZQ+MlVdlLFCisWnnt09xughajjWpFsw==
+ dependencies:
+ "@types/node" "*"
+
"@types/node@*":
version "14.6.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.6.0.tgz#7d4411bf5157339337d7cff864d9ff45f177b499"
@@ -7395,6 +7402,13 @@ lines-and-columns@^1.1.6:
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
+list-react-files@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/list-react-files/-/list-react-files-0.2.0.tgz#5d5102d42270328e085dfee6b90338bbc4cbe234"
+ integrity sha512-ZbBO+Ch76klfcD+bf/0FCBzCMZ16moT598cUo5yn0PbdQzQp4C11hZiHXAUnm3OZUXfV/ze1mog6wzwH4AGdKQ==
+ dependencies:
+ glob "^7.1.2"
+
listr-silent-renderer@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e"
@@ -8051,7 +8065,7 @@ mysql2@^1.7.0:
seq-queue "^0.0.5"
sqlstring "^2.3.1"
-mysql@^2.17.1:
+mysql@^2.17.1, mysql@^2.18.1:
version "2.18.1"
resolved "https://registry.yarnpkg.com/mysql/-/mysql-2.18.1.tgz#2254143855c5a8c73825e4522baf2ea021766717"
integrity sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==
@@ -10335,6 +10349,15 @@ serve-static@1.14.1:
parseurl "~1.3.3"
send "0.17.1"
+serverless-mysql@^1.5.4:
+ version "1.5.4"
+ resolved "https://registry.yarnpkg.com/serverless-mysql/-/serverless-mysql-1.5.4.tgz#c8562777e42440d6ebed0f96e17278bafef1f2d6"
+ integrity sha512-q7hJh8NivO2g4CcZ7wy3KTctsFpqx/P4zrVJTwsJJoV9v9QouGv0IFLKXW0rkOqbHSQRvth/eTbf7noYBJzPiQ==
+ dependencies:
+ mysql "^2.18.1"
+ optionalDependencies:
+ "@types/mysql" "^2.15.6"
+
set-blocking@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
@@ -10624,6 +10647,11 @@ sprintf-js@~1.0.2:
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
+sql-template-strings@^2.2.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/sql-template-strings/-/sql-template-strings-2.2.2.tgz#3f11508a25addfce217a3042a9d300c3193b96ff"
+ integrity sha1-PxFQiiWt384hejBCqdMAwxk7lv8=
+
sqlstring@2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.1.tgz#475393ff9e91479aea62dcaf0ca3d14983a7fb40"
From 6285ce1918b2dac20aa68728a93b3a1dc89edf2c Mon Sep 17 00:00:00 2001
From: Andreas Bigger
Date: Tue, 1 Sep 2020 17:06:39 -0700
Subject: [PATCH 003/129] migrated admin and contribution to typescript vercel
serverless
---
pages/api/admin.ts | 8 ++++----
pages/api/contribution.ts | 38 ++++++++++++++++++++++++++++++++++++++
2 files changed, 42 insertions(+), 4 deletions(-)
create mode 100644 pages/api/contribution.ts
diff --git a/pages/api/admin.ts b/pages/api/admin.ts
index d4b64b8b..cfea2394 100644
--- a/pages/api/admin.ts
+++ b/pages/api/admin.ts
@@ -1,4 +1,4 @@
-let express = require("express");
+const express = require("express");
const models = require("./models");
const utils = require("./utils");
const router = express.Router();
@@ -19,7 +19,7 @@ router.put("/:email", async (req, res) => {
email: req.params.email
}
});
- return res.json({ hackerProfile: newHackerProfile });
+ return res.json({ hackerProfile: updatedhackerProfile });
});
// TODO: use the new client fetcher api
@@ -147,7 +147,7 @@ router.get("/eligibleProfiles", async (req, res) => {
}
]
});
- filteredProfiles = allProfiles.filter(profile => {
+ let filteredProfiles = allProfiles.filter(profile => {
const reviewsByCurrUser = profile.HackerReviews.filter(review => {
return review.dataValues.createdBy === req.user.id;
});
@@ -221,4 +221,4 @@ router.get("/review", async (req, res) => {
}
});
-module.exports = router;
+export { router };
diff --git a/pages/api/contribution.ts b/pages/api/contribution.ts
new file mode 100644
index 00000000..8fbe6c10
--- /dev/null
+++ b/pages/api/contribution.ts
@@ -0,0 +1,38 @@
+const express = require("express");
+const models = require("./models");
+const utils = require("./utils");
+const router = express.Router();
+const sequelize = require("sequelize");
+const Sentry = require("@sentry/node");
+
+router.use(utils.authMiddleware);
+
+router.get("/all", async (req, res) => {
+ const contributions = await models.Contribution.findAll();
+ return res.json({ success: contributions });
+});
+
+router.get("/owned", async (req, res) => {
+ const contributions = await models.Contribution.findAll({
+ where: {
+ personId: req.user.id
+ }
+ });
+ return res.json({ success: contributions });
+});
+
+router.post("/create", async (req, res) => {
+ const input = req.body;
+
+ if (!input.taskId) {
+ return res.status(400).json({ error: "Missing TaskID" });
+ } else {
+ const result = await models.Contribution.build({
+ personId: req.user.id,
+ taskId: input.taskId
+ }).save();
+ return res.json({ success: result });
+ }
+});
+
+export { router };
From 5e33b954efe487f0c560052c5e56c19d40fe16e3 Mon Sep 17 00:00:00 2001
From: Andreas Bigger
Date: Tue, 8 Sep 2020 18:48:58 -0700
Subject: [PATCH 004/129] local testing
---
.env.example | 2 ++
..._value.js => 20200903012115-give_id_default_value.js} | 4 ++--
api/models/hackerProfile.js | 4 ++--
components/TaskBreakdown.tsx | 2 +-
lib/database/models/index.ts | 2 +-
lib/database/models/types/db.ts | 9 +++++++++
odyssey.d.ts | 2 +-
server.js | 2 +-
8 files changed, 19 insertions(+), 8 deletions(-)
rename api/database/migrations/{20200810155744-give_id_default_value.js => 20200903012115-give_id_default_value.js} (86%)
create mode 100644 lib/database/models/types/db.ts
diff --git a/.env.example b/.env.example
index 15d32ed2..06a988a0 100644
--- a/.env.example
+++ b/.env.example
@@ -20,6 +20,8 @@ PROD_DB_HOSTNAME=
URL_BASE=
+SENTRY_DSN=
+
S3_ACCESS_KEY=
S3_SECRET=
diff --git a/api/database/migrations/20200810155744-give_id_default_value.js b/api/database/migrations/20200903012115-give_id_default_value.js
similarity index 86%
rename from api/database/migrations/20200810155744-give_id_default_value.js
rename to api/database/migrations/20200903012115-give_id_default_value.js
index a06b994d..0db39e79 100644
--- a/api/database/migrations/20200810155744-give_id_default_value.js
+++ b/api/database/migrations/20200903012115-give_id_default_value.js
@@ -4,9 +4,9 @@ module.exports = {
up: (queryInterface, Sequelize) => {
return Promise.all([
queryInterface.changeColumn("HackerProfiles", "id", {
- type: Sequelize.STRING,
+ type: Sequelize.INTEGER,
allowNull: false,
- defaultValue: "hacker_id"
+ defaultValue: 0
})
]);
},
diff --git a/api/models/hackerProfile.js b/api/models/hackerProfile.js
index 59dd39a2..b622feb6 100644
--- a/api/models/hackerProfile.js
+++ b/api/models/hackerProfile.js
@@ -6,8 +6,8 @@ module.exports = (sequelize, DataTypes) => {
"HackerProfile",
{
id: {
- type: DataTypes.STRING,
- defaultValue: "hacker_id",
+ type: DataTypes.INTEGER,
+ defaultValue: 0,
allowNull: false
},
gender: {
diff --git a/components/TaskBreakdown.tsx b/components/TaskBreakdown.tsx
index 03857ab7..e0e406f3 100644
--- a/components/TaskBreakdown.tsx
+++ b/components/TaskBreakdown.tsx
@@ -73,7 +73,7 @@ const content = [
const TaskBreakdown = () => (
{content.map(task => (
-
+
{task.title}
{task.description}
diff --git a/lib/database/models/index.ts b/lib/database/models/index.ts
index 73f59e79..5a7b63cd 100644
--- a/lib/database/models/index.ts
+++ b/lib/database/models/index.ts
@@ -5,7 +5,7 @@ import config from "../config";
let env_config = config[env];
-let db = {
+let db: DBType = {
sequelize: null,
Sequelize: null
};
diff --git a/lib/database/models/types/db.ts b/lib/database/models/types/db.ts
new file mode 100644
index 00000000..796dca5b
--- /dev/null
+++ b/lib/database/models/types/db.ts
@@ -0,0 +1,9 @@
+interface DBType {
+ sequelize: any;
+ Sequelize: any;
+ HackerProfile;
+}
+
+module.exports = {
+ DBType
+};
diff --git a/odyssey.d.ts b/odyssey.d.ts
index 12e844e3..df6d8afc 100644
--- a/odyssey.d.ts
+++ b/odyssey.d.ts
@@ -13,7 +13,7 @@ declare type FormStep = {
};
declare type Profile = {
- id: string;
+ id: number;
gender: "male" | "female" | "non-binary" | "other" | "no-say";
userId: ResourceID;
ethnicity: string;
diff --git a/server.js b/server.js
index c54ab576..f796ce56 100644
--- a/server.js
+++ b/server.js
@@ -22,7 +22,7 @@ if (!dev) {
}
Sentry.init({
- dsn: "https://1a18ac7b9aa94cb5b2a8c9fc2f7e4fc8@sentry.io/1801129",
+ dsn: process.env.SENTRY_DSN,
environment: dev ? "dev" : process.env.NODE_ENV,
release: "odyssey@" + process.env.npm_package_version
});
From 12fcd1d00aeda738c2c5e57c75d7508bd437ef4e Mon Sep 17 00:00:00 2001
From: Andreas Bigger
Date: Tue, 8 Sep 2020 23:32:59 -0700
Subject: [PATCH 005/129] Testing CircleCI Slack notifications... this is
definitely a bad idea
From a0384e74d4f9ab320549cd687e67a6cacc707c03 Mon Sep 17 00:00:00 2001
From: Andreas Bigger
Date: Tue, 8 Sep 2020 23:40:48 -0700
Subject: [PATCH 006/129] bump .circleci/config.yml
---
.circleci/config.yml | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/.circleci/config.yml b/.circleci/config.yml
index f5515222..c222678d 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -1,4 +1,6 @@
-version: 2
+version: 2.1
+orbs:
+ slack: circleci/slack@3.4.2
jobs:
E2ETest:
docker:
@@ -24,6 +26,12 @@ jobs:
- checkout
- run: yarn install
- run: yarn build
+ - slack/notify:
+ channel: G01B137TAP2
+ color: "#FEDA22"
+ mentions: ""
+ message: circleci build pending
+ webhook: webhook
workflows:
version: 2
e2e_tests:
From 94ada7ce81a0e2723d1f06b80638a07433ccb9f2 Mon Sep 17 00:00:00 2001
From: Andreas Bigger
Date: Tue, 8 Sep 2020 23:45:07 -0700
Subject: [PATCH 007/129] send on circleci message on fail_only
---
.circleci/config.yml | 11 +++++------
1 file changed, 5 insertions(+), 6 deletions(-)
diff --git a/.circleci/config.yml b/.circleci/config.yml
index c222678d..9874e019 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -26,12 +26,11 @@ jobs:
- checkout
- run: yarn install
- run: yarn build
- - slack/notify:
- channel: G01B137TAP2
- color: "#FEDA22"
- mentions: ""
- message: circleci build pending
- webhook: webhook
+ - slack/status:
+ fail_only: true
+ mentions: ""
+ only_for_branches: "staging"
+ webhook: webhook
workflows:
version: 2
e2e_tests:
From fc639f995f6838b1c700db93030815396c8c3ca2 Mon Sep 17 00:00:00 2001
From: Andreas Bigger
Date: Tue, 8 Sep 2020 23:53:10 -0700
Subject: [PATCH 008/129] send on circleci message on fail_only
---
.circleci/config.yml | 1 -
1 file changed, 1 deletion(-)
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 9874e019..a6d9fa7e 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -30,7 +30,6 @@ jobs:
fail_only: true
mentions: ""
only_for_branches: "staging"
- webhook: webhook
workflows:
version: 2
e2e_tests:
From ea25a98eac376943de455bbf080787d5ba50917f Mon Sep 17 00:00:00 2001
From: Andreas Bigger
Date: Wed, 9 Sep 2020 07:39:39 -0700
Subject: [PATCH 009/129] test circleci more
---
.circleci/config.yml | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/.circleci/config.yml b/.circleci/config.yml
index a6d9fa7e..162c4f36 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -26,10 +26,13 @@ jobs:
- checkout
- run: yarn install
- run: yarn build
+ - slack/notify:
+ color: "#42e2f4"
+ mentions: "abigger87,"
+ message: "Circleci notification here"
- slack/status:
fail_only: true
- mentions: ""
- only_for_branches: "staging"
+ mentions: "abigger87"
workflows:
version: 2
e2e_tests:
From 148fc11d5247882a6a90be006e5c50022f8ec15d Mon Sep 17 00:00:00 2001
From: Andreas Bigger
Date: Wed, 9 Sep 2020 07:53:31 -0700
Subject: [PATCH 010/129] test circleci more
---
.circleci/config.yml | 28 ++++++++++++++++++++--------
1 file changed, 20 insertions(+), 8 deletions(-)
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 162c4f36..774a4676 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -19,25 +19,37 @@ jobs:
command: "yarn run E2ETest"
- store_artifacts:
path: cypress/videos/odysseyTests
+ slackNotify:
+ docker:
+ - image: circleci/node
+ steps:
+ - run: exit 0
+ - slack/status:
+ fail_only: true
+ mentions: ""
+ only_for_branches: "staging"
+ webhook: "${SLACK_WEBHOOK}"
build:
docker:
- image: circleci/node
steps:
- checkout
- - run: yarn install
- - run: yarn build
- - slack/notify:
- color: "#42e2f4"
- mentions: "abigger87,"
- message: "Circleci notification here"
+ - run:
+ name: "Installing"
+ command: yarn install
+ - run:
+ name: "Building"
+ command: yarn build
- slack/status:
fail_only: true
- mentions: "abigger87"
workflows:
- version: 2
+ version: 2.1
e2e_tests:
jobs:
- E2ETest
+ slackNotify:
+ jobs:
+ - slackNotify
build:
jobs:
- build
From 9ee550ca2effd7902088586bdf8bfaff6723fe0d Mon Sep 17 00:00:00 2001
From: Andreas Bigger
Date: Wed, 9 Sep 2020 07:56:30 -0700
Subject: [PATCH 011/129] custom build name?
---
.circleci/config.yml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 774a4676..2d1ea2e9 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -29,7 +29,7 @@ jobs:
mentions: ""
only_for_branches: "staging"
webhook: "${SLACK_WEBHOOK}"
- build:
+ customBuild:
docker:
- image: circleci/node
steps:
@@ -50,6 +50,6 @@ workflows:
slackNotify:
jobs:
- slackNotify
- build:
+ customBuild:
jobs:
- - build
+ - customBuild
From 7c9c2cd4b9b31d713820fc7af841783de8575b6b Mon Sep 17 00:00:00 2001
From: Andreas Bigger
Date: Wed, 9 Sep 2020 07:59:04 -0700
Subject: [PATCH 012/129] OMG just bad indentation :(
---
.circleci/config.yml | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 2d1ea2e9..48df4233 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -35,13 +35,13 @@ jobs:
steps:
- checkout
- run:
- name: "Installing"
- command: yarn install
+ name: "Installing"
+ command: yarn install
- run:
- name: "Building"
- command: yarn build
+ name: "Building"
+ command: yarn build
- slack/status:
- fail_only: true
+ fail_only: true
workflows:
version: 2.1
e2e_tests:
From 2fa49bae1b8bbc8c06060a61b732fc484968e4b8 Mon Sep 17 00:00:00 2001
From: Andreas Bigger
Date: Wed, 9 Sep 2020 08:07:15 -0700
Subject: [PATCH 013/129] added success and failure messages
---
.circleci/config.yml | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 48df4233..e9286e06 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -41,7 +41,10 @@ jobs:
name: "Building"
command: yarn build
- slack/status:
- fail_only: true
+ fail_only: false
+ failure_message: ":red_circle: A $CIRCLE_JOB job has failed!"
+ only_for_branches: "staging,develop"
+ success_message: ":tada: A $CIRCLE_JOB job has succeeded!"
workflows:
version: 2.1
e2e_tests:
From 4ef8afcd70273593d4336398457deae41a3efcfa Mon Sep 17 00:00:00 2001
From: Andreas Bigger
Date: Wed, 9 Sep 2020 08:14:50 -0700
Subject: [PATCH 014/129] added success and failure messages
---
.circleci/config.yml | 23 +++++------------------
1 file changed, 5 insertions(+), 18 deletions(-)
diff --git a/.circleci/config.yml b/.circleci/config.yml
index e9286e06..5e95f2b1 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -19,17 +19,7 @@ jobs:
command: "yarn run E2ETest"
- store_artifacts:
path: cypress/videos/odysseyTests
- slackNotify:
- docker:
- - image: circleci/node
- steps:
- - run: exit 0
- - slack/status:
- fail_only: true
- mentions: ""
- only_for_branches: "staging"
- webhook: "${SLACK_WEBHOOK}"
- customBuild:
+ build:
docker:
- image: circleci/node
steps:
@@ -42,17 +32,14 @@ jobs:
command: yarn build
- slack/status:
fail_only: false
- failure_message: ":red_circle: A $CIRCLE_JOB job has failed!"
+ failure_message: ":red_circle: A $CIRCLE_JOB job has failed for $CIRCLE_PROJECT_REPONAME on the $CIRCLE_BRANCH branch! Triggered by: $CIRCLE_USERNAME"
only_for_branches: "staging,develop"
- success_message: ":tada: A $CIRCLE_JOB job has succeeded!"
+ success_message: ":tada: A $CIRCLE_JOB job has succeeded for $CIRCLE_PROJECT_REPONAME on the $CIRCLE_BRANCH branch! Triggered by: $CIRCLE_USERNAME"
workflows:
version: 2.1
e2e_tests:
jobs:
- E2ETest
- slackNotify:
- jobs:
- - slackNotify
- customBuild:
+ build:
jobs:
- - customBuild
+ - build
From 492a81e5a06dd49b4d56087928ec2bf6f4938b26 Mon Sep 17 00:00:00 2001
From: Andreas Bigger
Date: Wed, 16 Sep 2020 19:43:31 -0700
Subject: [PATCH 015/129] Serverless ambitions (#355)
* [WIP] migrating rest api to vercel serverless functions
* temp
* migrated admin and contribution to typescript vercel serverless
* local testing
* migrated serverless functions into pages/api/ so routes should be good to go. Now we need to transition all api calls to the website itself
* :sparkles: ready to merge into staging for testing
Co-authored-by: Andreas Bigger
---
.circleci/config.yml | 2 +-
.env.example | 2 +
.../20200903012115-give_id_default_value.js | 23 +
api/models/hackerProfile.js | 4 +-
components/TaskBreakdown.tsx | 2 +-
lib/database/Readme.md | 2 +
lib/database/config.ts | 27 ++
.../20191010225527-create-hacker-profile.js | 51 ++
.../20191028071621-update-hackerProfile.js | 76 +++
.../migrations/20191029183843-add-role.js | 13 +
...0191031003446-profile-additional-fields.js | 73 +++
.../20191101021240-mlh-app-requirements.js | 21 +
.../20191104062818-cleanup-profile.js | 62 +++
.../20191111203354-add-promo-codes.js | 33 ++
.../migrations/20191114212136-teams.js | 47 ++
.../20191115010855-add-review-table.js | 58 +++
.../20191126000713-teams-add-dates.js | 23 +
.../20191126043728-create-task-table.js | 38 ++
.../20191203052918-add-person-table.js | 29 ++
.../20191203062141-add-contribution-table.js | 42 ++
.../20191209225538-add-houses-table.js | 46 ++
.../20191223005530-confirmation-fields.js | 70 +++
.../migrations/20191224053622-no-bus-check.js | 15 +
.../20191226014741-confirmed-at-field.js | 21 +
.../20200115053213-add-task-groupings.js | 43 ++
.../20200115054722-add-multipliers-table.js | 42 ++
.../20200115061612-add-actions-table.js | 34 ++
.../20200115235853-add-travel-column.js | 22 +
.../20200118201352-add-task-type.js | 23 +
.../migrations/20200120005508-create-prize.js | 30 ++
.../20200120005653-create-projectTeam.js | 33 ++
...120005958-associate-projectTeams-prizes.js | 32 ++
...20011322-associate-projectTeams-persons.js | 17 +
...4192357-augment-contrib-with-multiplier.js | 13 +
.../20200124194744-add-schedule-table.js | 40 ++
.../20200126211953-add-scanner-id-column.js | 17 +
...0200126213315-adds-timestamps-to-prizes.js | 23 +
.../20200126213646-add-is-group-task-flag.js | 13 +
.../20200126215010-add-unlockables-table.js | 38 ++
...000435-modify-role-superadmin-volunteer.js | 19 +
.../20200127004044-add-is-active-field.js | 13 +
...0127012812-add-qrcode-to-hacker-profile.js | 14 +
...00201211804-add-sponsor-and-isPast-flag.js | 23 +
.../20200810155744-give_id_default_value.js | 0
lib/database/models/action.ts | 14 +
lib/database/models/contribution.ts | 27 ++
lib/database/models/event.ts | 17 +
lib/database/models/groupings.ts | 16 +
lib/database/models/hackerProfile.ts | 150 ++++++
lib/database/models/hackerReview.ts | 24 +
lib/database/models/house.ts | 21 +
lib/database/models/index.ts | 34 ++
lib/database/models/multiplier.ts | 18 +
lib/database/models/person.ts | 49 ++
lib/database/models/prize.ts | 21 +
lib/database/models/projectTeam.ts | 30 ++
lib/database/models/projectTeamPrize.ts | 9 +
lib/database/models/task.ts | 25 +
lib/database/models/team.ts | 34 ++
lib/database/models/types/db.ts | 7 +
lib/database/models/types/index.ts | 1 +
lib/database/models/unlockable.ts | 15 +
lib/database/utils.ts | 119 +++++
lib/db.ts | 22 +
odyssey.d.ts | 2 +-
package.json | 3 +
pages/api/admin.ts | 224 +++++++++
pages/api/admin/review.ts | 44 ++
pages/api/config.ts | 28 ++
pages/api/contribution.ts | 38 ++
pages/api/event.ts | 52 ++
pages/api/hackerLive.ts | 282 +++++++++++
pages/api/hackerProfile.ts | 348 ++++++++++++++
pages/api/live.ts | 446 ++++++++++++++++++
pages/api/login.ts | 97 ++++
pages/api/models/action.ts | 14 +
pages/api/models/contribution.ts | 27 ++
pages/api/models/event.ts | 17 +
pages/api/models/groupings.ts | 16 +
pages/api/models/hackerProfile.ts | 150 ++++++
pages/api/models/hackerReview.ts | 24 +
pages/api/models/house.ts | 21 +
pages/api/models/index.ts | 34 ++
pages/api/models/multiplier.ts | 18 +
pages/api/models/person.ts | 49 ++
pages/api/models/prize.ts | 21 +
pages/api/models/projectTeam.ts | 30 ++
pages/api/models/projectTeamPrize.ts | 9 +
pages/api/models/task.ts | 25 +
pages/api/models/team.ts | 34 ++
pages/api/models/types/db.ts | 7 +
pages/api/models/types/index.ts | 1 +
pages/api/models/unlockable.ts | 15 +
pages/api/people.ts | 60 +++
pages/api/prizes.ts | 13 +
pages/api/projectTeam.ts | 148 ++++++
pages/api/public.ts | 15 +
pages/api/tasks.ts | 88 ++++
pages/api/team.ts | 192 ++++++++
pages/api/unlockable.ts | 61 +++
pages/api/{users.js => users.ts} | 1 +
pages/api/utils.ts | 132 ++++++
server.js | 2 +-
yarn.lock | 30 +-
104 files changed, 4733 insertions(+), 7 deletions(-)
create mode 100644 api/database/migrations/20200903012115-give_id_default_value.js
create mode 100644 lib/database/Readme.md
create mode 100644 lib/database/config.ts
create mode 100644 lib/database/migrations/20191010225527-create-hacker-profile.js
create mode 100644 lib/database/migrations/20191028071621-update-hackerProfile.js
create mode 100644 lib/database/migrations/20191029183843-add-role.js
create mode 100644 lib/database/migrations/20191031003446-profile-additional-fields.js
create mode 100644 lib/database/migrations/20191101021240-mlh-app-requirements.js
create mode 100644 lib/database/migrations/20191104062818-cleanup-profile.js
create mode 100644 lib/database/migrations/20191111203354-add-promo-codes.js
create mode 100644 lib/database/migrations/20191114212136-teams.js
create mode 100644 lib/database/migrations/20191115010855-add-review-table.js
create mode 100644 lib/database/migrations/20191126000713-teams-add-dates.js
create mode 100644 lib/database/migrations/20191126043728-create-task-table.js
create mode 100644 lib/database/migrations/20191203052918-add-person-table.js
create mode 100644 lib/database/migrations/20191203062141-add-contribution-table.js
create mode 100644 lib/database/migrations/20191209225538-add-houses-table.js
create mode 100644 lib/database/migrations/20191223005530-confirmation-fields.js
create mode 100644 lib/database/migrations/20191224053622-no-bus-check.js
create mode 100644 lib/database/migrations/20191226014741-confirmed-at-field.js
create mode 100644 lib/database/migrations/20200115053213-add-task-groupings.js
create mode 100644 lib/database/migrations/20200115054722-add-multipliers-table.js
create mode 100644 lib/database/migrations/20200115061612-add-actions-table.js
create mode 100644 lib/database/migrations/20200115235853-add-travel-column.js
create mode 100644 lib/database/migrations/20200118201352-add-task-type.js
create mode 100644 lib/database/migrations/20200120005508-create-prize.js
create mode 100644 lib/database/migrations/20200120005653-create-projectTeam.js
create mode 100644 lib/database/migrations/20200120005958-associate-projectTeams-prizes.js
create mode 100644 lib/database/migrations/20200120011322-associate-projectTeams-persons.js
create mode 100644 lib/database/migrations/20200124192357-augment-contrib-with-multiplier.js
create mode 100644 lib/database/migrations/20200124194744-add-schedule-table.js
create mode 100644 lib/database/migrations/20200126211953-add-scanner-id-column.js
create mode 100644 lib/database/migrations/20200126213315-adds-timestamps-to-prizes.js
create mode 100644 lib/database/migrations/20200126213646-add-is-group-task-flag.js
create mode 100644 lib/database/migrations/20200126215010-add-unlockables-table.js
create mode 100644 lib/database/migrations/20200127000435-modify-role-superadmin-volunteer.js
create mode 100644 lib/database/migrations/20200127004044-add-is-active-field.js
create mode 100644 lib/database/migrations/20200127012812-add-qrcode-to-hacker-profile.js
create mode 100644 lib/database/migrations/20200201211804-add-sponsor-and-isPast-flag.js
rename {api => lib}/database/migrations/20200810155744-give_id_default_value.js (100%)
create mode 100644 lib/database/models/action.ts
create mode 100644 lib/database/models/contribution.ts
create mode 100644 lib/database/models/event.ts
create mode 100644 lib/database/models/groupings.ts
create mode 100644 lib/database/models/hackerProfile.ts
create mode 100644 lib/database/models/hackerReview.ts
create mode 100644 lib/database/models/house.ts
create mode 100644 lib/database/models/index.ts
create mode 100644 lib/database/models/multiplier.ts
create mode 100644 lib/database/models/person.ts
create mode 100644 lib/database/models/prize.ts
create mode 100644 lib/database/models/projectTeam.ts
create mode 100644 lib/database/models/projectTeamPrize.ts
create mode 100644 lib/database/models/task.ts
create mode 100644 lib/database/models/team.ts
create mode 100644 lib/database/models/types/db.ts
create mode 100644 lib/database/models/types/index.ts
create mode 100644 lib/database/models/unlockable.ts
create mode 100644 lib/database/utils.ts
create mode 100644 lib/db.ts
create mode 100644 pages/api/admin.ts
create mode 100644 pages/api/admin/review.ts
create mode 100644 pages/api/config.ts
create mode 100644 pages/api/contribution.ts
create mode 100644 pages/api/event.ts
create mode 100644 pages/api/hackerLive.ts
create mode 100644 pages/api/hackerProfile.ts
create mode 100644 pages/api/live.ts
create mode 100644 pages/api/login.ts
create mode 100644 pages/api/models/action.ts
create mode 100644 pages/api/models/contribution.ts
create mode 100644 pages/api/models/event.ts
create mode 100644 pages/api/models/groupings.ts
create mode 100644 pages/api/models/hackerProfile.ts
create mode 100644 pages/api/models/hackerReview.ts
create mode 100644 pages/api/models/house.ts
create mode 100644 pages/api/models/index.ts
create mode 100644 pages/api/models/multiplier.ts
create mode 100644 pages/api/models/person.ts
create mode 100644 pages/api/models/prize.ts
create mode 100644 pages/api/models/projectTeam.ts
create mode 100644 pages/api/models/projectTeamPrize.ts
create mode 100644 pages/api/models/task.ts
create mode 100644 pages/api/models/team.ts
create mode 100644 pages/api/models/types/db.ts
create mode 100644 pages/api/models/types/index.ts
create mode 100644 pages/api/models/unlockable.ts
create mode 100644 pages/api/people.ts
create mode 100644 pages/api/prizes.ts
create mode 100644 pages/api/projectTeam.ts
create mode 100644 pages/api/public.ts
create mode 100644 pages/api/tasks.ts
create mode 100644 pages/api/team.ts
create mode 100644 pages/api/unlockable.ts
rename pages/api/{users.js => users.ts} (94%)
create mode 100644 pages/api/utils.ts
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 5e95f2b1..a7b309eb 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -34,7 +34,7 @@ jobs:
fail_only: false
failure_message: ":red_circle: A $CIRCLE_JOB job has failed for $CIRCLE_PROJECT_REPONAME on the $CIRCLE_BRANCH branch! Triggered by: $CIRCLE_USERNAME"
only_for_branches: "staging,develop"
- success_message: ":tada: A $CIRCLE_JOB job has succeeded for $CIRCLE_PROJECT_REPONAME on the $CIRCLE_BRANCH branch! Triggered by: $CIRCLE_USERNAME"
+ success_message: ":tada: A $CIRCLE_JOB job has succeeded for $CIRCLE_PROJECT_REPONAME on the $CIRCLE_BRANCH branch! Triggered by: $CIRCLE_USERNAME :rocket:"
workflows:
version: 2.1
e2e_tests:
diff --git a/.env.example b/.env.example
index 15d32ed2..06a988a0 100644
--- a/.env.example
+++ b/.env.example
@@ -20,6 +20,8 @@ PROD_DB_HOSTNAME=
URL_BASE=
+SENTRY_DSN=
+
S3_ACCESS_KEY=
S3_SECRET=
diff --git a/api/database/migrations/20200903012115-give_id_default_value.js b/api/database/migrations/20200903012115-give_id_default_value.js
new file mode 100644
index 00000000..0db39e79
--- /dev/null
+++ b/api/database/migrations/20200903012115-give_id_default_value.js
@@ -0,0 +1,23 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.changeColumn("HackerProfiles", "id", {
+ type: Sequelize.INTEGER,
+ allowNull: false,
+ defaultValue: 0
+ })
+ ]);
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.changeColumn("HackerProfiles", "id", {
+ type: Sequelize.STRING,
+ allowNull: false,
+ defaultValue: ""
+ })
+ ]);
+ }
+};
diff --git a/api/models/hackerProfile.js b/api/models/hackerProfile.js
index 59dd39a2..b622feb6 100644
--- a/api/models/hackerProfile.js
+++ b/api/models/hackerProfile.js
@@ -6,8 +6,8 @@ module.exports = (sequelize, DataTypes) => {
"HackerProfile",
{
id: {
- type: DataTypes.STRING,
- defaultValue: "hacker_id",
+ type: DataTypes.INTEGER,
+ defaultValue: 0,
allowNull: false
},
gender: {
diff --git a/components/TaskBreakdown.tsx b/components/TaskBreakdown.tsx
index 03857ab7..e0e406f3 100644
--- a/components/TaskBreakdown.tsx
+++ b/components/TaskBreakdown.tsx
@@ -73,7 +73,7 @@ const content = [
const TaskBreakdown = () => (
{content.map(task => (
-
+
{task.title}
{task.description}
diff --git a/lib/database/Readme.md b/lib/database/Readme.md
new file mode 100644
index 00000000..58606528
--- /dev/null
+++ b/lib/database/Readme.md
@@ -0,0 +1,2 @@
+Directory that contains all the legacy SQL logic.
+Temporary holding place as the nodejs express rest api is migrated to Vercel Serverless Functions.
diff --git a/lib/database/config.ts b/lib/database/config.ts
new file mode 100644
index 00000000..87e5159f
--- /dev/null
+++ b/lib/database/config.ts
@@ -0,0 +1,27 @@
+const config = {
+ development: {
+ username: process.env.DEV_DB_USERNAME,
+ password: process.env.DEV_DB_PASSWORD,
+ database: process.env.DEV_DB_NAME,
+ host: process.env.DEV_DB_HOSTNAME,
+ logging: false,
+ dialect: "mysql"
+ },
+ test: {
+ username: process.env.TEST_DB_USERNAME,
+ password: process.env.TEST_DB_PASSWORD,
+ database: process.env.TEST_DB_NAME,
+ host: process.env.TEST_DB_HOSTNAME,
+ logging: false,
+ dialect: "mysql"
+ },
+ production: {
+ username: process.env.PROD_DB_USERNAME,
+ password: process.env.PROD_DB_PASSWORD,
+ database: process.env.PROD_DB_NAME,
+ host: process.env.PROD_DB_HOSTNAME,
+ dialect: "mysql"
+ }
+};
+
+export default config;
diff --git a/lib/database/migrations/20191010225527-create-hacker-profile.js b/lib/database/migrations/20191010225527-create-hacker-profile.js
new file mode 100644
index 00000000..b79b6079
--- /dev/null
+++ b/lib/database/migrations/20191010225527-create-hacker-profile.js
@@ -0,0 +1,51 @@
+export default {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable("HackerProfiles", {
+ id: {
+ allowNull: false,
+ type: Sequelize.INTEGER
+ },
+ userId: {
+ primaryKey: true,
+ type: Sequelize.STRING,
+ allowNull: false
+ },
+ gender: {
+ type: Sequelize.ENUM,
+ values: ["male", "female", "other"]
+ },
+ email: {
+ type: Sequelize.STRING
+ },
+ ethnicity: {
+ type: Sequelize.STRING
+ },
+ major: {
+ type: Sequelize.STRING
+ },
+ minor: {
+ type: Sequelize.STRING
+ },
+ resume: {
+ type: Sequelize.STRING
+ },
+ skills: {
+ type: Sequelize.STRING
+ },
+ interests: {
+ type: Sequelize.STRING
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ });
+ },
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable("HackerProfiles");
+ }
+};
diff --git a/lib/database/migrations/20191028071621-update-hackerProfile.js b/lib/database/migrations/20191028071621-update-hackerProfile.js
new file mode 100644
index 00000000..e1be8a70
--- /dev/null
+++ b/lib/database/migrations/20191028071621-update-hackerProfile.js
@@ -0,0 +1,76 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.addColumn("HackerProfiles", "applicationSubmittedAt", {
+ type: Sequelize.DATE
+ }),
+ queryInterface.addColumn("HackerProfiles", "profileSubmittedAt", {
+ type: Sequelize.DATE
+ }),
+ queryInterface.addColumn("HackerProfiles", "status", {
+ type: Sequelize.ENUM,
+ values: [
+ "unverified",
+ "verified",
+ "profileSubmitted",
+ "applicationSubmitted",
+ "accepted",
+ "waitlisted",
+ "rejected",
+ "confirmed",
+ "checkedIn"
+ ],
+ defaultValue: "unverified",
+ allowNull: false
+ }),
+ queryInterface.addColumn("HackerProfiles", "firstName", {
+ type: Sequelize.STRING
+ }),
+ queryInterface.addColumn("HackerProfiles", "lastName", {
+ type: Sequelize.STRING
+ }),
+ queryInterface.addColumn("HackerProfiles", "phoneNumber", {
+ type: Sequelize.STRING
+ }),
+ queryInterface.addColumn("HackerProfiles", "school", {
+ type: Sequelize.STRING
+ }),
+ queryInterface.addColumn("HackerProfiles", "year", {
+ type: Sequelize.ENUM,
+ values: ["Freshman", "Sophomore", "Junior", "Senior", "Graduate"]
+ }),
+ queryInterface.addColumn("HackerProfiles", "skillLevel", {
+ type: Sequelize.ENUM,
+ values: ["Beginner", "Intermediate", "Advanced"]
+ }),
+ queryInterface.addColumn("HackerProfiles", "questionOne", {
+ type: Sequelize.STRING(1000)
+ }),
+ queryInterface.addColumn("HackerProfiles", "questionTwo", {
+ type: Sequelize.STRING(1000)
+ }),
+ queryInterface.addColumn("HackerProfiles", "questionThree", {
+ type: Sequelize.STRING(1000)
+ })
+ ]);
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.removeColumn("HackerProfiles", "applicationSubmittedAt"),
+ queryInterface.removeColumn("HackerProfiles", "profileSubmittedAt"),
+ queryInterface.removeColumn("HackerProfiles", "status"),
+ queryInterface.removeColumn("HackerProfiles", "firstName"),
+ queryInterface.removeColumn("HackerProfiles", "lastName"),
+ queryInterface.removeColumn("HackerProfiles", "phoneNumber"),
+ queryInterface.removeColumn("HackerProfiles", "school"),
+ queryInterface.removeColumn("HackerProfiles", "year"),
+ queryInterface.removeColumn("HackerProfiles", "skillLevel"),
+ queryInterface.removeColumn("HackerProfiles", "questionOne"),
+ queryInterface.removeColumn("HackerProfiles", "questionTwo"),
+ queryInterface.removeColumn("HackerProfiles", "questionThree")
+ ]);
+ }
+};
diff --git a/lib/database/migrations/20191029183843-add-role.js b/lib/database/migrations/20191029183843-add-role.js
new file mode 100644
index 00000000..46401b49
--- /dev/null
+++ b/lib/database/migrations/20191029183843-add-role.js
@@ -0,0 +1,13 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.addColumn("HackerProfiles", "role", {
+ type: Sequelize.ENUM,
+ values: ["hacker", "admin", "sponsor", "superadmin"]
+ });
+ },
+ down: (queryInterface, Sequelize) => {
+ queryInterface.removeColumn("HackerProfiles", "role");
+ }
+};
diff --git a/lib/database/migrations/20191031003446-profile-additional-fields.js b/lib/database/migrations/20191031003446-profile-additional-fields.js
new file mode 100644
index 00000000..e39204f1
--- /dev/null
+++ b/lib/database/migrations/20191031003446-profile-additional-fields.js
@@ -0,0 +1,73 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.addColumn("HackerProfiles", "graduationDate", {
+ type: Sequelize.ENUM,
+ values: [
+ "spring-2020",
+ "fall-2020",
+ "spring-2021",
+ "fall-2021",
+ "spring-2022",
+ "fall-2022",
+ "spring-2023",
+ "fall-2023",
+ "other"
+ ]
+ }),
+ queryInterface.addColumn("HackerProfiles", "over18", {
+ type: Sequelize.BOOLEAN
+ }),
+ queryInterface.addColumn("HackerProfiles", "needBus", {
+ type: Sequelize.BOOLEAN
+ }),
+ queryInterface.addColumn("HackerProfiles", "links", {
+ type: Sequelize.STRING(1000)
+ }),
+ queryInterface.changeColumn("HackerProfiles", "role", {
+ type: Sequelize.ENUM,
+ values: ["hacker", "admin", "sponsor", "superadmin"],
+ defaultValue: "hacker"
+ }),
+ queryInterface.changeColumn("HackerProfiles", "year", {
+ type: Sequelize.ENUM,
+ values: ["freshman", "sophomore", "junior", "senior", "graduate"]
+ }),
+ queryInterface.changeColumn("HackerProfiles", "gender", {
+ type: Sequelize.ENUM,
+ values: ["male", "female", "non-binary", "other", "no-say"]
+ }),
+ queryInterface.changeColumn("HackerProfiles", "skillLevel", {
+ type: Sequelize.ENUM,
+ values: ["beginner", "intermediate", "advanced"]
+ })
+ ]);
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.removeColumn("HackerProfiles", "graduationDate"),
+ queryInterface.removeColumn("HackerProfiles", "over18"),
+ queryInterface.removeColumn("HackerProfiles", "needBus"),
+ queryInterface.removeColumn("HackerProfiles", "links"),
+ queryInterface.changeColumn("HackerProfiles", "role", {
+ type: Sequelize.ENUM,
+ values: ["hacker", "admin", "sponsor", "superadmin"]
+ }),
+ queryInterface.changeColumn("HackerProfiles", "year", {
+ type: Sequelize.ENUM,
+ values: ["Freshman", "Sophomore", "Junior", "Senior", "Graduate"]
+ }),
+ queryInterface.changeColumn("HackerProfiles", "gender", {
+ type: Sequelize.ENUM,
+ values: ["male", "female", "other"]
+ }),
+ queryInterface.changeColumn("HackerProfiles", "skillLevel", {
+ type: Sequelize.ENUM,
+ values: ["Beginner", "Intermediate", "Advanced"]
+ })
+ ]);
+ }
+};
diff --git a/lib/database/migrations/20191101021240-mlh-app-requirements.js b/lib/database/migrations/20191101021240-mlh-app-requirements.js
new file mode 100644
index 00000000..50c92c07
--- /dev/null
+++ b/lib/database/migrations/20191101021240-mlh-app-requirements.js
@@ -0,0 +1,21 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.addColumn("HackerProfiles", "codeOfConduct", {
+ type: Sequelize.BOOLEAN
+ }),
+ queryInterface.addColumn("HackerProfiles", "authorize", {
+ type: Sequelize.BOOLEAN
+ })
+ ]);
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.removeColumn("HackerProfiles", "codeOfConduct"),
+ queryInterface.removeColumn("HackerProfiles", "authorize")
+ ]);
+ }
+};
diff --git a/lib/database/migrations/20191104062818-cleanup-profile.js b/lib/database/migrations/20191104062818-cleanup-profile.js
new file mode 100644
index 00000000..f0f7e20e
--- /dev/null
+++ b/lib/database/migrations/20191104062818-cleanup-profile.js
@@ -0,0 +1,62 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.removeColumn("HackerProfiles", "applicationSubmittedAt"),
+ queryInterface.changeColumn("HackerProfiles", "status", {
+ type: Sequelize.ENUM,
+ values: [
+ "unverified",
+ "verified",
+ "submitted",
+ "accepted",
+ "waitlisted",
+ "rejected",
+ "confirmed",
+ "checkedIn"
+ ],
+ defaultValue: "unverified",
+ allowNull: false
+ }),
+ queryInterface.addColumn("HackerProfiles", "marketing", {
+ type: Sequelize.STRING(100)
+ }),
+ queryInterface.renameColumn(
+ "HackerProfiles",
+ "profileSubmittedAt",
+ "submittedAt"
+ )
+ ]);
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.addColumn("HackerProfiles", "applicationSubmittedAt", {
+ type: Sequelize.DATE
+ }),
+ queryInterface.changeColumn("HackerProfiles", "status", {
+ type: Sequelize.ENUM,
+ values: [
+ "unverified",
+ "verified",
+ "profileSubmitted",
+ "applicationSubmitted",
+ "accepted",
+ "waitlisted",
+ "rejected",
+ "confirmed",
+ "checkedIn"
+ ],
+ defaultValue: "unverified",
+ allowNull: false
+ }),
+ queryInterface.removeColumn("HackerProfiles", "marketing"),
+ queryInterface.renameColumn(
+ "HackerProfiles",
+ "submittedAt",
+ "profileSubmittedAt"
+ )
+ ]);
+ }
+};
diff --git a/lib/database/migrations/20191111203354-add-promo-codes.js b/lib/database/migrations/20191111203354-add-promo-codes.js
new file mode 100644
index 00000000..653c5d84
--- /dev/null
+++ b/lib/database/migrations/20191111203354-add-promo-codes.js
@@ -0,0 +1,33 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface
+ .addColumn("HackerProfiles", "promoCode", {
+ type: Sequelize.STRING(100),
+ unique: true
+ })
+ .then(function() {
+ return queryInterface.sequelize.query(`
+ UPDATE HackerProfiles
+ SET promoCode = (
+ SELECT substring(MD5(RAND()), -8)
+ )
+ WHERE promoCode IS NULL;
+
+ `);
+ }),
+ queryInterface.addColumn("HackerProfiles", "referrerCode", {
+ type: Sequelize.STRING(100)
+ })
+ ]);
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.removeColumn("HackerProfiles", "promoCode"),
+ queryInterface.removeColumn("HackerProfiles", "referrerCode")
+ ]);
+ }
+};
diff --git a/lib/database/migrations/20191114212136-teams.js b/lib/database/migrations/20191114212136-teams.js
new file mode 100644
index 00000000..7c1d50aa
--- /dev/null
+++ b/lib/database/migrations/20191114212136-teams.js
@@ -0,0 +1,47 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface
+ .createTable("Teams", {
+ id: {
+ allowNull: false,
+ type: Sequelize.INTEGER,
+ autoIncrement: true,
+ primaryKey: true
+ },
+ name: {
+ type: Sequelize.STRING(150),
+ allowNull: false
+ },
+ teamCode: {
+ type: Sequelize.STRING(4),
+ allowNull: false,
+ unique: true
+ },
+ ownerId: {
+ type: Sequelize.STRING,
+ references: {
+ model: "HackerProfiles",
+ key: "userId"
+ }
+ }
+ })
+ .then(() => {
+ return queryInterface.addColumn("HackerProfiles", "teamId", {
+ type: Sequelize.INTEGER,
+ references: {
+ model: "Teams",
+ key: "id"
+ }
+ });
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.removeColumn("HackerProfiles", "teamId"),
+ queryInterface.dropTable("Teams")
+ ]);
+ }
+};
diff --git a/lib/database/migrations/20191115010855-add-review-table.js b/lib/database/migrations/20191115010855-add-review-table.js
new file mode 100644
index 00000000..3fb6bbf5
--- /dev/null
+++ b/lib/database/migrations/20191115010855-add-review-table.js
@@ -0,0 +1,58 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable("HackerReviews", {
+ id: {
+ allowNull: false,
+ primaryKey: true,
+ autoIncrement: true,
+ type: Sequelize.INTEGER
+ },
+ hackerId: {
+ type: Sequelize.STRING,
+ references: {
+ model: "HackerProfiles",
+ key: "userId"
+ }
+ },
+ createdBy: {
+ // Should reference admin who created this review
+ allowNull: false,
+ type: Sequelize.STRING,
+ references: {
+ model: "HackerProfiles",
+ key: "userId"
+ }
+ },
+ scoreOne: {
+ allowNull: true,
+ type: Sequelize.INTEGER
+ },
+ scoreTwo: {
+ allowNull: true,
+ type: Sequelize.INTEGER
+ },
+ scoreThree: {
+ allowNull: true,
+ type: Sequelize.INTEGER
+ },
+ comments: {
+ allowNull: true,
+ type: Sequelize.STRING
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable("HackerReviews");
+ }
+};
diff --git a/lib/database/migrations/20191126000713-teams-add-dates.js b/lib/database/migrations/20191126000713-teams-add-dates.js
new file mode 100644
index 00000000..68f2b98f
--- /dev/null
+++ b/lib/database/migrations/20191126000713-teams-add-dates.js
@@ -0,0 +1,23 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.addColumn("Teams", "createdAt", {
+ type: Sequelize.DATE,
+ allowNull: false
+ }),
+ queryInterface.addColumn("Teams", "updatedAt", {
+ type: Sequelize.DATE,
+ allowNull: false
+ })
+ ]);
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.removeColumn("Teams", "createdAt"),
+ queryInterface.removeColumn("Teams", "updatedAt")
+ ]);
+ }
+};
diff --git a/lib/database/migrations/20191126043728-create-task-table.js b/lib/database/migrations/20191126043728-create-task-table.js
new file mode 100644
index 00000000..c343a000
--- /dev/null
+++ b/lib/database/migrations/20191126043728-create-task-table.js
@@ -0,0 +1,38 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable("Tasks", {
+ id: {
+ allowNull: false,
+ primaryKey: true,
+ autoIncrement: true,
+ type: Sequelize.INTEGER
+ },
+ points: {
+ allowNull: false,
+ type: Sequelize.INTEGER
+ },
+ description: {
+ allowNull: true,
+ type: Sequelize.STRING
+ },
+ blocking: {
+ allowNull: false,
+ type: Sequelize.BOOLEAN
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable("Tasks");
+ }
+};
diff --git a/lib/database/migrations/20191203052918-add-person-table.js b/lib/database/migrations/20191203052918-add-person-table.js
new file mode 100644
index 00000000..d462b0ae
--- /dev/null
+++ b/lib/database/migrations/20191203052918-add-person-table.js
@@ -0,0 +1,29 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable("persons", {
+ identityId: {
+ allowNull: false,
+ primaryKey: true,
+ type: Sequelize.STRING
+ },
+ isBattlepassComplete: {
+ allowNull: false,
+ type: Sequelize.BOOLEAN
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable("persons");
+ }
+};
diff --git a/lib/database/migrations/20191203062141-add-contribution-table.js b/lib/database/migrations/20191203062141-add-contribution-table.js
new file mode 100644
index 00000000..42a44bb6
--- /dev/null
+++ b/lib/database/migrations/20191203062141-add-contribution-table.js
@@ -0,0 +1,42 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable("Contributions", {
+ id: {
+ allowNull: false,
+ primaryKey: true,
+ autoIncrement: true,
+ type: Sequelize.INTEGER
+ },
+ personId: {
+ allowNull: false,
+ type: Sequelize.STRING,
+ references: {
+ model: "persons",
+ key: "identityId"
+ }
+ },
+ taskId: {
+ allowNull: false,
+ type: Sequelize.INTEGER,
+ references: {
+ model: "Tasks",
+ key: "id"
+ }
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable("Contributions");
+ }
+};
diff --git a/lib/database/migrations/20191209225538-add-houses-table.js b/lib/database/migrations/20191209225538-add-houses-table.js
new file mode 100644
index 00000000..f29a163a
--- /dev/null
+++ b/lib/database/migrations/20191209225538-add-houses-table.js
@@ -0,0 +1,46 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface
+ .createTable("Houses", {
+ id: {
+ type: Sequelize.INTEGER,
+ primaryKey: true,
+ autoIncrement: true,
+ allowNull: false
+ },
+ name: {
+ type: Sequelize.STRING,
+ allowNull: false
+ },
+ color: {
+ type: Sequelize.STRING,
+ allowNull: false
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ })
+ .then(() => {
+ return queryInterface.addColumn("persons", "houseId", {
+ type: Sequelize.INTEGER,
+ allowNull: true,
+ references: {
+ model: "Houses",
+ key: "id"
+ }
+ });
+ });
+ },
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.removeColumn("persons", "houseId").then(() => {
+ return queryInterface.dropTable("Houses");
+ });
+ }
+};
diff --git a/lib/database/migrations/20191223005530-confirmation-fields.js b/lib/database/migrations/20191223005530-confirmation-fields.js
new file mode 100644
index 00000000..68031975
--- /dev/null
+++ b/lib/database/migrations/20191223005530-confirmation-fields.js
@@ -0,0 +1,70 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.addColumn("HackerProfiles", "travelOrigin", {
+ type: Sequelize.STRING(500)
+ }),
+ queryInterface.addColumn("HackerProfiles", "travelMethod", {
+ type: Sequelize.ENUM,
+ values: ["driving", "bus", "flying", "usc", "other"]
+ }),
+ queryInterface.addColumn("HackerProfiles", "travelPlan", {
+ type: Sequelize.STRING(500)
+ }),
+ queryInterface.addColumn("HackerProfiles", "dietaryRestrictions", {
+ type: Sequelize.STRING(1000)
+ }),
+ queryInterface.addColumn("HackerProfiles", "confirmCodeOfConduct", {
+ type: Sequelize.BOOLEAN
+ }),
+ queryInterface.addColumn("HackerProfiles", "shirtSize", {
+ type: Sequelize.ENUM,
+ values: ["xs", "s", "m", "l", "xl"]
+ }),
+ queryInterface.changeColumn("HackerProfiles", "status", {
+ type: Sequelize.ENUM,
+ values: [
+ "unverified",
+ "verified",
+ "submitted",
+ "accepted",
+ "waitlisted",
+ "rejected",
+ "confirmed",
+ "declined",
+ "checkedIn"
+ ],
+ defaultValue: "unverified",
+ allowNull: false
+ })
+ ]);
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.removeColumn("HackerProfiles", "travelOrigin"),
+ queryInterface.removeColumn("HackerProfiles", "travelMethod"),
+ queryInterface.removeColumn("HackerProfiles", "travelPlan"),
+ queryInterface.removeColumn("HackerProfiles", "dietaryRestrictions"),
+ queryInterface.removeColumn("HackerProfiles", "shirtSize"),
+ queryInterface.removeColumn("HackerProfiles", "confirmCodeOfConduct"),
+ queryInterface.changeColumn("HackerProfiles", "status", {
+ type: Sequelize.ENUM,
+ values: [
+ "unverified",
+ "verified",
+ "submitted",
+ "accepted",
+ "waitlisted",
+ "rejected",
+ "confirmed",
+ "checkedIn"
+ ],
+ defaultValue: "unverified",
+ allowNull: false
+ })
+ ]);
+ }
+};
diff --git a/lib/database/migrations/20191224053622-no-bus-check.js b/lib/database/migrations/20191224053622-no-bus-check.js
new file mode 100644
index 00000000..851d9cc6
--- /dev/null
+++ b/lib/database/migrations/20191224053622-no-bus-check.js
@@ -0,0 +1,15 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.addColumn("HackerProfiles", "noBusCheck", {
+ type: Sequelize.BOOLEAN
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.removeColumn("HackerProfiles", "noBusCheck", {
+ type: Sequelize.BOOLEAN
+ });
+ }
+};
diff --git a/lib/database/migrations/20191226014741-confirmed-at-field.js b/lib/database/migrations/20191226014741-confirmed-at-field.js
new file mode 100644
index 00000000..0288f9b7
--- /dev/null
+++ b/lib/database/migrations/20191226014741-confirmed-at-field.js
@@ -0,0 +1,21 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.addColumn("HackerProfiles", "confirmedAt", {
+ type: Sequelize.DATE
+ }),
+ queryInterface.addColumn("HackerProfiles", "declinedAt", {
+ type: Sequelize.DATE
+ })
+ ]);
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.removeColumn("HackerProfiles", "confirmedAt"),
+ queryInterface.removeColumn("HackerProfiles", "declinedAt")
+ ]);
+ }
+};
diff --git a/lib/database/migrations/20200115053213-add-task-groupings.js b/lib/database/migrations/20200115053213-add-task-groupings.js
new file mode 100644
index 00000000..2910a2ac
--- /dev/null
+++ b/lib/database/migrations/20200115053213-add-task-groupings.js
@@ -0,0 +1,43 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface
+ .createTable("Groupings", {
+ id: {
+ type: Sequelize.INTEGER,
+ primaryKey: true,
+ autoIncrement: true,
+ allowNull: false
+ },
+ name: {
+ type: Sequelize.STRING,
+ allowNull: false
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ })
+ .then(() => {
+ return queryInterface.addColumn("Tasks", "groupingId", {
+ type: Sequelize.INTEGER,
+ allowNull: true,
+ references: {
+ model: "Groupings",
+ key: "id"
+ }
+ });
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.removeColumn("Tasks", "groupingId").then(() => {
+ return queryInterface.dropTable("Groupings");
+ });
+ }
+};
diff --git a/lib/database/migrations/20200115054722-add-multipliers-table.js b/lib/database/migrations/20200115054722-add-multipliers-table.js
new file mode 100644
index 00000000..23e54a6d
--- /dev/null
+++ b/lib/database/migrations/20200115054722-add-multipliers-table.js
@@ -0,0 +1,42 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable("Multipliers", {
+ id: {
+ type: Sequelize.INTEGER,
+ primaryKey: true,
+ autoIncrement: true,
+ allowNull: false
+ },
+ name: {
+ type: Sequelize.STRING,
+ allowNull: false
+ },
+ multiplierValue: {
+ type: Sequelize.INTEGER,
+ allowNull: false
+ },
+ groupingId: {
+ type: Sequelize.INTEGER,
+ allowNull: false,
+ references: {
+ model: "Groupings",
+ key: "id"
+ }
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable("Multipliers");
+ }
+};
diff --git a/lib/database/migrations/20200115061612-add-actions-table.js b/lib/database/migrations/20200115061612-add-actions-table.js
new file mode 100644
index 00000000..8bf32f4b
--- /dev/null
+++ b/lib/database/migrations/20200115061612-add-actions-table.js
@@ -0,0 +1,34 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable("Actions", {
+ id: {
+ type: Sequelize.INTEGER,
+ primaryKey: true,
+ autoIncrement: true,
+ allowNull: false
+ },
+ name: {
+ type: Sequelize.STRING,
+ allowNull: false
+ },
+ role: {
+ type: Sequelize.STRING,
+ allowNull: false
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable("Actions");
+ }
+};
diff --git a/lib/database/migrations/20200115235853-add-travel-column.js b/lib/database/migrations/20200115235853-add-travel-column.js
new file mode 100644
index 00000000..b03c91d0
--- /dev/null
+++ b/lib/database/migrations/20200115235853-add-travel-column.js
@@ -0,0 +1,22 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.addColumn("HackerProfiles", "travelStatus", {
+ type: Sequelize.ENUM,
+ values: [
+ "ineligible",
+ "needed",
+ "unneeded",
+ "declined",
+ "unknown",
+ "submitted",
+ "reimbursed"
+ ]
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.removeColumn("HackerProfiles", "travelStatus");
+ }
+};
diff --git a/lib/database/migrations/20200118201352-add-task-type.js b/lib/database/migrations/20200118201352-add-task-type.js
new file mode 100644
index 00000000..4631e091
--- /dev/null
+++ b/lib/database/migrations/20200118201352-add-task-type.js
@@ -0,0 +1,23 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.addColumn("Tasks", "type", {
+ type: Sequelize.STRING,
+ allowNull: false
+ }),
+ queryInterface.addColumn("Tasks", "name", {
+ type: Sequelize.STRING,
+ allowNull: false
+ })
+ ]);
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.removeColumn("Tasks", "type"),
+ queryInterface.removeColumn("Tasks", "name")
+ ]);
+ }
+};
diff --git a/lib/database/migrations/20200120005508-create-prize.js b/lib/database/migrations/20200120005508-create-prize.js
new file mode 100644
index 00000000..fdb67388
--- /dev/null
+++ b/lib/database/migrations/20200120005508-create-prize.js
@@ -0,0 +1,30 @@
+"use strict";
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable("Prizes", {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ title: {
+ type: Sequelize.STRING
+ },
+ description: {
+ type: Sequelize.STRING
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ });
+ },
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable("Prizes");
+ }
+};
diff --git a/lib/database/migrations/20200120005653-create-projectTeam.js b/lib/database/migrations/20200120005653-create-projectTeam.js
new file mode 100644
index 00000000..2b8bbc18
--- /dev/null
+++ b/lib/database/migrations/20200120005653-create-projectTeam.js
@@ -0,0 +1,33 @@
+"use strict";
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable("ProjectTeams", {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ name: {
+ type: Sequelize.STRING
+ },
+ devpostLink: {
+ type: Sequelize.STRING
+ },
+ githubLink: {
+ type: Sequelize.STRING
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ });
+ },
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable("ProjectTeams");
+ }
+};
diff --git a/lib/database/migrations/20200120005958-associate-projectTeams-prizes.js b/lib/database/migrations/20200120005958-associate-projectTeams-prizes.js
new file mode 100644
index 00000000..5b34c925
--- /dev/null
+++ b/lib/database/migrations/20200120005958-associate-projectTeams-prizes.js
@@ -0,0 +1,32 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable("ProjectTeamPrizes", {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ projectTeam: {
+ type: Sequelize.INTEGER,
+ references: {
+ model: "ProjectTeams",
+ key: "id"
+ }
+ },
+ prize: {
+ type: Sequelize.INTEGER,
+ references: {
+ model: "Prizes",
+ key: "id"
+ }
+ }
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable("ProjectTeamPrizes");
+ }
+};
diff --git a/lib/database/migrations/20200120011322-associate-projectTeams-persons.js b/lib/database/migrations/20200120011322-associate-projectTeams-persons.js
new file mode 100644
index 00000000..7d6e8a83
--- /dev/null
+++ b/lib/database/migrations/20200120011322-associate-projectTeams-persons.js
@@ -0,0 +1,17 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.addColumn("persons", "projectTeamId", {
+ type: Sequelize.INTEGER,
+ references: {
+ model: "ProjectTeams",
+ key: "id"
+ }
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.removeColumn("persons", "projectTeamId");
+ }
+};
diff --git a/lib/database/migrations/20200124192357-augment-contrib-with-multiplier.js b/lib/database/migrations/20200124192357-augment-contrib-with-multiplier.js
new file mode 100644
index 00000000..b5587e74
--- /dev/null
+++ b/lib/database/migrations/20200124192357-augment-contrib-with-multiplier.js
@@ -0,0 +1,13 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.addColumn("Contributions", "multiplier", {
+ type: Sequelize.INTEGER
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.removeColumn("Contributions", "multiplier");
+ }
+};
diff --git a/lib/database/migrations/20200124194744-add-schedule-table.js b/lib/database/migrations/20200124194744-add-schedule-table.js
new file mode 100644
index 00000000..fe76211d
--- /dev/null
+++ b/lib/database/migrations/20200124194744-add-schedule-table.js
@@ -0,0 +1,40 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable("Events", {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ name: {
+ type: Sequelize.STRING
+ },
+ description: {
+ type: Sequelize.STRING
+ },
+ startsAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ endsAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable("Events");
+ }
+};
diff --git a/lib/database/migrations/20200126211953-add-scanner-id-column.js b/lib/database/migrations/20200126211953-add-scanner-id-column.js
new file mode 100644
index 00000000..b6b737d5
--- /dev/null
+++ b/lib/database/migrations/20200126211953-add-scanner-id-column.js
@@ -0,0 +1,17 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.addColumn("Contributions", "scannerId", {
+ type: Sequelize.STRING,
+ references: {
+ model: "HackerProfiles",
+ key: "userId"
+ }
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.removeColumn("Contributions", "scannerId");
+ }
+};
diff --git a/lib/database/migrations/20200126213315-adds-timestamps-to-prizes.js b/lib/database/migrations/20200126213315-adds-timestamps-to-prizes.js
new file mode 100644
index 00000000..f737e921
--- /dev/null
+++ b/lib/database/migrations/20200126213315-adds-timestamps-to-prizes.js
@@ -0,0 +1,23 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.addColumn("ProjectTeamPrizes", "createdAt", {
+ type: Sequelize.DATE,
+ allowNull: false
+ }),
+ queryInterface.addColumn("ProjectTeamPrizes", "updatedAt", {
+ type: Sequelize.DATE,
+ allowNull: false
+ })
+ ]);
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.removeColumn("ProjectTeamPrizes", "createdAt"),
+ queryInterface.removeColumn("ProjectTeamPrizes", "updatedAt")
+ ]);
+ }
+};
diff --git a/lib/database/migrations/20200126213646-add-is-group-task-flag.js b/lib/database/migrations/20200126213646-add-is-group-task-flag.js
new file mode 100644
index 00000000..1bba995b
--- /dev/null
+++ b/lib/database/migrations/20200126213646-add-is-group-task-flag.js
@@ -0,0 +1,13 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.addColumn("Tasks", "isGroupTask", {
+ type: Sequelize.BOOLEAN
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.removeColumn("Tasks", "isGroupTask");
+ }
+};
diff --git a/lib/database/migrations/20200126215010-add-unlockables-table.js b/lib/database/migrations/20200126215010-add-unlockables-table.js
new file mode 100644
index 00000000..4e71b1e6
--- /dev/null
+++ b/lib/database/migrations/20200126215010-add-unlockables-table.js
@@ -0,0 +1,38 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.createTable("Unlockables", {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ tier: {
+ allowNull: false,
+ type: Sequelize.INTEGER
+ },
+ pointThreshold: {
+ allowNull: false,
+ type: Sequelize.INTEGER
+ },
+ isPremium: {
+ allowNull: false,
+ type: Sequelize.BOOLEAN
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.dropTable("Unlockables");
+ }
+};
diff --git a/lib/database/migrations/20200127000435-modify-role-superadmin-volunteer.js b/lib/database/migrations/20200127000435-modify-role-superadmin-volunteer.js
new file mode 100644
index 00000000..f7b35e27
--- /dev/null
+++ b/lib/database/migrations/20200127000435-modify-role-superadmin-volunteer.js
@@ -0,0 +1,19 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.changeColumn("HackerProfiles", "role", {
+ type: Sequelize.ENUM,
+ values: ["hacker", "admin", "sponsor", "volunteer"],
+ defaultValue: "hacker"
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.changeColumn("HackerProfiles", "role", {
+ type: Sequelize.ENUM,
+ values: ["hacker", "admin", "sponsor", "superadmin"],
+ defaultValue: "hacker"
+ });
+ }
+};
diff --git a/lib/database/migrations/20200127004044-add-is-active-field.js b/lib/database/migrations/20200127004044-add-is-active-field.js
new file mode 100644
index 00000000..4cbe848c
--- /dev/null
+++ b/lib/database/migrations/20200127004044-add-is-active-field.js
@@ -0,0 +1,13 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.addColumn("Tasks", "isActive", {
+ type: Sequelize.BOOLEAN
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.removeColumn("Tasks", "isActive");
+ }
+};
diff --git a/lib/database/migrations/20200127012812-add-qrcode-to-hacker-profile.js b/lib/database/migrations/20200127012812-add-qrcode-to-hacker-profile.js
new file mode 100644
index 00000000..37579e5a
--- /dev/null
+++ b/lib/database/migrations/20200127012812-add-qrcode-to-hacker-profile.js
@@ -0,0 +1,14 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return queryInterface.addColumn("HackerProfiles", "qrCodeId", {
+ type: Sequelize.STRING,
+ unique: true
+ });
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return queryInterface.removeColumn("HackerProfiles", "qrCodeId");
+ }
+};
diff --git a/lib/database/migrations/20200201211804-add-sponsor-and-isPast-flag.js b/lib/database/migrations/20200201211804-add-sponsor-and-isPast-flag.js
new file mode 100644
index 00000000..157171a1
--- /dev/null
+++ b/lib/database/migrations/20200201211804-add-sponsor-and-isPast-flag.js
@@ -0,0 +1,23 @@
+"use strict";
+
+module.exports = {
+ up: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.addColumn("Tasks", "sponsor", {
+ type: Sequelize.STRING,
+ allowNull: true
+ }),
+ queryInterface.addColumn("Tasks", "isPast", {
+ type: Sequelize.BOOLEAN,
+ allowNull: false
+ })
+ ]);
+ },
+
+ down: (queryInterface, Sequelize) => {
+ return Promise.all([
+ queryInterface.removeColumn("Tasks", "sponsor"),
+ queryInterface.removeColumn("Tasks", "isPast")
+ ]);
+ }
+};
diff --git a/api/database/migrations/20200810155744-give_id_default_value.js b/lib/database/migrations/20200810155744-give_id_default_value.js
similarity index 100%
rename from api/database/migrations/20200810155744-give_id_default_value.js
rename to lib/database/migrations/20200810155744-give_id_default_value.js
diff --git a/lib/database/models/action.ts b/lib/database/models/action.ts
new file mode 100644
index 00000000..b5c066b7
--- /dev/null
+++ b/lib/database/models/action.ts
@@ -0,0 +1,14 @@
+const action = (sequelize, DataTypes) => {
+ const Action = sequelize.define(
+ "Action",
+ {
+ role: DataTypes.STRING(100),
+ name: DataTypes.STRING(100)
+ },
+ { tableName: "Actions" }
+ );
+ Action.associate = models => {};
+ return Action;
+};
+
+export default action;
diff --git a/lib/database/models/contribution.ts b/lib/database/models/contribution.ts
new file mode 100644
index 00000000..6f162301
--- /dev/null
+++ b/lib/database/models/contribution.ts
@@ -0,0 +1,27 @@
+const contribution = (sequelize, DataTypes) => {
+ const Contribution = sequelize.define(
+ "Contribution",
+ {
+ personId: DataTypes.STRING(100),
+ multiplier: DataTypes.INTEGER,
+ scannerId: DataTypes.STRING(100),
+ taskId: DataTypes.INTEGER
+ },
+ {
+ tableName: "Contributions"
+ }
+ );
+ Contribution.associate = function(models) {
+ Contribution.belongsTo(models.Person, {
+ foreignKey: "personId",
+ targetKey: "identityId"
+ });
+ Contribution.belongsTo(models.Task, { foreignKey: "taskId" });
+ Contribution.addScope("defaultScope", {
+ include: [{ model: models.Task }]
+ });
+ };
+ return Contribution;
+};
+
+export default contribution;
diff --git a/lib/database/models/event.ts b/lib/database/models/event.ts
new file mode 100644
index 00000000..affacbc1
--- /dev/null
+++ b/lib/database/models/event.ts
@@ -0,0 +1,17 @@
+const event = (sequelize, DataTypes) => {
+ const Event = sequelize.define(
+ "Event",
+ {
+ name: DataTypes.STRING(100),
+ description: DataTypes.STRING(500),
+ startsAt: DataTypes.DATE,
+ endsAt: DataTypes.DATE
+ },
+ {}
+ );
+
+ Event.associate = models => {};
+ return Event;
+};
+
+export default event;
diff --git a/lib/database/models/groupings.ts b/lib/database/models/groupings.ts
new file mode 100644
index 00000000..b6121fc0
--- /dev/null
+++ b/lib/database/models/groupings.ts
@@ -0,0 +1,16 @@
+const groupings = (sequelize, DataTypes) => {
+ const Grouping = sequelize.define(
+ "Grouping",
+ {
+ name: DataTypes.STRING(100)
+ },
+ { tableName: "Groupings" }
+ );
+ Grouping.associate = models => {
+ models.Task.belongsTo(models.Grouping, { foreignKey: "groupingId" });
+ Grouping.hasMany(models.Task, { foreignKey: "groupingId" });
+ };
+ return Grouping;
+};
+
+export default groupings;
diff --git a/lib/database/models/hackerProfile.ts b/lib/database/models/hackerProfile.ts
new file mode 100644
index 00000000..7b8e75ca
--- /dev/null
+++ b/lib/database/models/hackerProfile.ts
@@ -0,0 +1,150 @@
+// If you make any changes to HackerProfile, make sure you do the following:
+// 1) Generate a Sequelize migration that adds/removes columns as needed
+// 2) Update the Profile type definition in odyssey.d.ts
+const profile = (sequelize, DataTypes) => {
+ const HackerProfile = sequelize.define(
+ "HackerProfile",
+ {
+ id: {
+ type: DataTypes.STRING,
+ defaultValue: "hacker_id",
+ allowNull: false
+ },
+ gender: {
+ type: DataTypes.ENUM,
+ values: ["male", "female", "non-binary", "other", "no-say"]
+ },
+ userId: {
+ type: DataTypes.STRING,
+ primaryKey: true,
+ allowNull: false
+ },
+ travelStatus: {
+ type: DataTypes.ENUM,
+ values: [
+ "ineligible",
+ "needed",
+ "unneeded",
+ "declined",
+ "unknown",
+ "submitted",
+ "reimbursed"
+ ]
+ },
+ ethnicity: DataTypes.STRING,
+ email: DataTypes.STRING,
+ major: DataTypes.STRING,
+ minor: DataTypes.STRING,
+ resume: DataTypes.STRING,
+ skills: DataTypes.STRING,
+ interests: DataTypes.STRING,
+ submittedAt: DataTypes.DATE,
+ status: {
+ type: DataTypes.ENUM,
+ values: [
+ "unverified",
+ "verified",
+ "submitted",
+ "accepted",
+ "waitlisted",
+ "rejected",
+ "confirmed",
+ "declined",
+ "checkedIn"
+ ],
+ defaultValue: "unverified",
+ allowNull: false
+ },
+ firstName: DataTypes.STRING,
+ lastName: DataTypes.STRING,
+ phoneNumber: DataTypes.STRING,
+ school: DataTypes.STRING,
+ year: {
+ type: DataTypes.ENUM,
+ values: ["freshman", "sophomore", "junior", "senior", "graduate"]
+ },
+ skillLevel: {
+ type: DataTypes.ENUM,
+ values: ["beginner", "intermediate", "advanced"]
+ },
+ questionOne: DataTypes.STRING(1000),
+ questionTwo: DataTypes.STRING(1000),
+ questionThree: DataTypes.STRING(1000),
+ role: {
+ type: DataTypes.ENUM,
+ values: ["hacker", "admin", "sponsor", "volunteer"],
+ defaultValue: "hacker"
+ },
+ graduationDate: {
+ type: DataTypes.ENUM,
+ values: [
+ "spring-2020",
+ "fall-2020",
+ "spring-2021",
+ "fall-2021",
+ "spring-2022",
+ "fall-2022",
+ "spring-2023",
+ "fall-2023",
+ "other"
+ ]
+ },
+ over18: DataTypes.BOOLEAN,
+ needBus: DataTypes.BOOLEAN,
+ links: DataTypes.STRING(1000),
+ codeOfConduct: DataTypes.BOOLEAN,
+ authorize: DataTypes.BOOLEAN,
+ marketing: DataTypes.STRING(100),
+ promoCode: {
+ type: DataTypes.STRING(100),
+ defaultValue: function() {
+ //Random string of length 8
+ return (Math.random().toString(36) + "00000000000000000").slice(
+ 2,
+ 10
+ );
+ }
+ },
+ referrerCode: DataTypes.STRING(100),
+ referred: DataTypes.VIRTUAL,
+ travelOrigin: DataTypes.STRING(500),
+ travelMethod: {
+ type: DataTypes.ENUM,
+ values: ["driving", "bus", "flying", "usc", "other"]
+ },
+ shirtSize: {
+ type: DataTypes.ENUM,
+ values: ["xs", "s", "m", "l", "xl"]
+ },
+ travelPlan: DataTypes.STRING(500),
+ dietaryRestrictions: DataTypes.STRING(1000),
+ confirmCodeOfConduct: DataTypes.BOOLEAN,
+ noBusCheck: DataTypes.BOOLEAN,
+ confirmedAt: DataTypes.DATE,
+ declinedAt: DataTypes.DATE,
+ qrCodeId: {
+ type: DataTypes.STRING,
+ unique: true
+ }
+ },
+ {}
+ );
+
+ HackerProfile.prototype.getReferred = () => {
+ return sequelize.models.HackerProfile.findAll({
+ where: {
+ referrerCode: this.promoCode
+ }
+ });
+ };
+ HackerProfile.associate = models => {
+ HackerProfile.belongsTo(models.Team, {
+ as: "team",
+ foreignKey: "teamId",
+ constraints: false
+ });
+ };
+ return HackerProfile;
+};
+
+export default profile;
diff --git a/lib/database/models/hackerReview.ts b/lib/database/models/hackerReview.ts
new file mode 100644
index 00000000..0086f918
--- /dev/null
+++ b/lib/database/models/hackerReview.ts
@@ -0,0 +1,24 @@
+const review = (sequelize, DataTypes) => {
+ const HackerReview = sequelize.define(
+ "HackerReview",
+ {
+ hackerId: DataTypes.STRING(100),
+ createdBy: DataTypes.STRING(100),
+ scoreOne: DataTypes.INTEGER,
+ scoreTwo: DataTypes.INTEGER,
+ scoreThree: DataTypes.INTEGER,
+ comments: DataTypes.STRING
+ },
+ {}
+ );
+ HackerReview.associate = models => {
+ // associations can be defined here
+ models.HackerProfile.hasMany(HackerReview, { foreignKey: "hackerId" });
+ models.HackerReview.belongsTo(models.HackerProfile, {
+ foreignKey: "createdBy"
+ });
+ };
+ return HackerReview;
+};
+
+export default review;
diff --git a/lib/database/models/house.ts b/lib/database/models/house.ts
new file mode 100644
index 00000000..3cf3dffa
--- /dev/null
+++ b/lib/database/models/house.ts
@@ -0,0 +1,21 @@
+const house = (sequelize, DataTypes) => {
+ const House = sequelize.define(
+ "House",
+ {
+ name: DataTypes.STRING(100),
+ color: DataTypes.STRING(100)
+ },
+ { tableName: "Houses" }
+ );
+ House.associate = models => {
+ House.hasMany(models.Person, {
+ foreignKey: "houseId",
+ as: "HouseMembers",
+ constraints: false
+ });
+ };
+
+ return House;
+};
+
+export default house;
diff --git a/lib/database/models/index.ts b/lib/database/models/index.ts
new file mode 100644
index 00000000..bb1e4e1d
--- /dev/null
+++ b/lib/database/models/index.ts
@@ -0,0 +1,34 @@
+const Sequelize = require("sequelize");
+const env = process.env.NODE_ENV || "test";
+
+import config from "../config";
+import { DBType } from "./types";
+
+let env_config = config[env];
+
+let db: DBType = {
+ sequelize: null,
+ Sequelize: null
+};
+
+let sequelize = new Sequelize(
+ env_config.database,
+ env_config.username,
+ env_config.password,
+ env_config
+);
+
+let model;
+model = sequelize["import"]("./action");
+db[model.name] = model;
+
+Object.keys(db).forEach(modelName => {
+ if (db[modelName].associate) {
+ db[modelName].associate(db);
+ }
+});
+
+db.sequelize = sequelize;
+db.Sequelize = Sequelize;
+
+export default db;
diff --git a/lib/database/models/multiplier.ts b/lib/database/models/multiplier.ts
new file mode 100644
index 00000000..b284f5c2
--- /dev/null
+++ b/lib/database/models/multiplier.ts
@@ -0,0 +1,18 @@
+const multiplier = (sequelize, DataTypes) => {
+ const Multiplier = sequelize.define(
+ "Multiplier",
+ {
+ name: DataTypes.STRING,
+ multiplierValue: DataTypes.INTEGER,
+ groupingId: DataTypes.INTEGER
+ },
+ { tableName: "Multipliers" }
+ );
+ Multiplier.associate = function(models) {
+ Multiplier.belongsTo(models.Grouping);
+ models.Grouping.hasMany(Multiplier, { foreignKey: "groupingId" });
+ };
+ return Multiplier;
+};
+
+export default multiplier;
diff --git a/lib/database/models/person.ts b/lib/database/models/person.ts
new file mode 100644
index 00000000..fc161703
--- /dev/null
+++ b/lib/database/models/person.ts
@@ -0,0 +1,49 @@
+const person = (sequelize, DataTypes) => {
+ const Person = sequelize.define(
+ "Person",
+ {
+ identityId: {
+ type: DataTypes.STRING(100),
+ primaryKey: true
+ },
+ isBattlepassComplete: DataTypes.BOOLEAN,
+ ProjectTeamId: DataTypes.NUMBER
+ },
+ {
+ tableName: "persons",
+ defaultScope: {
+ include: [
+ { model: sequelize.models.HackerProfile, as: "Profile" },
+ { model: sequelize.models.Contribution, as: "Contributions" },
+ { model: sequelize.models.House, as: "Home" }
+ ]
+ }
+ }
+ );
+
+ Person.associate = models => {
+ Person.belongsTo(models.House, {
+ foreignKey: "houseId",
+ as: "Home",
+ constraints: false
+ });
+ Person.belongsTo(models.ProjectTeam);
+ Person.belongsTo(models.HackerProfile, {
+ foreignKey: "identityId",
+ targetKey: "userId",
+ constraints: false,
+ as: "Profile"
+ });
+ Person.hasMany(models.Contribution, { foreignKey: "personId" });
+ Person.addScope("hideProfile", {
+ include: [
+ { model: sequelize.models.Contribution, as: "Contributions" },
+ { model: sequelize.models.House, as: "Home" }
+ ]
+ });
+ };
+
+ return Person;
+};
+
+export default person;
diff --git a/lib/database/models/prize.ts b/lib/database/models/prize.ts
new file mode 100644
index 00000000..bb1bae95
--- /dev/null
+++ b/lib/database/models/prize.ts
@@ -0,0 +1,21 @@
+const prize = (sequelize, DataTypes) => {
+ const Prize = sequelize.define(
+ "Prize",
+ {
+ title: DataTypes.STRING,
+ description: DataTypes.STRING
+ },
+ {}
+ );
+ Prize.associate = models => {
+ Prize.belongsToMany(models.ProjectTeam, {
+ through: "ProjectTeamPrizes",
+ foreignKey: "prize",
+ as: "Prizes",
+ otherKey: "projectTeam"
+ });
+ };
+ return Prize;
+};
+
+export default prize;
diff --git a/lib/database/models/projectTeam.ts b/lib/database/models/projectTeam.ts
new file mode 100644
index 00000000..b6a07256
--- /dev/null
+++ b/lib/database/models/projectTeam.ts
@@ -0,0 +1,30 @@
+const team = (sequelize, DataTypes) => {
+ const ProjectTeam = sequelize.define(
+ "ProjectTeam",
+ {
+ name: DataTypes.STRING,
+ devpostLink: DataTypes.STRING,
+ githubLink: DataTypes.STRING
+ },
+ {
+ defaultScope: {
+ include: [
+ { model: sequelize.models.Prize, as: "Prizes" },
+ { model: sequelize.models.Person, as: "Members" }
+ ]
+ }
+ }
+ );
+ ProjectTeam.associate = models => {
+ ProjectTeam.hasMany(models.Person, { as: "Members" });
+ ProjectTeam.belongsToMany(models.Prize, {
+ through: "ProjectTeamPrizes",
+ foreignKey: "projectTeam",
+ as: "Prizes",
+ otherKey: "prize"
+ });
+ };
+ return ProjectTeam;
+};
+
+export default team;
diff --git a/lib/database/models/projectTeamPrize.ts b/lib/database/models/projectTeamPrize.ts
new file mode 100644
index 00000000..9ef6aeeb
--- /dev/null
+++ b/lib/database/models/projectTeamPrize.ts
@@ -0,0 +1,9 @@
+const teamPrize = (sequelize, DataTypes) => {
+ const ProjectTeamPrizes = sequelize.define("ProjectTeamPrizes", {
+ ProjectTeam: DataTypes.NUMBER,
+ Prize: DataTypes.NUMBER
+ });
+ return ProjectTeamPrizes;
+};
+
+export default teamPrize;
diff --git a/lib/database/models/task.ts b/lib/database/models/task.ts
new file mode 100644
index 00000000..a0af6390
--- /dev/null
+++ b/lib/database/models/task.ts
@@ -0,0 +1,25 @@
+const task = (sequelize, DataTypes) => {
+ const Task = sequelize.define(
+ "Task",
+ {
+ points: DataTypes.INTEGER,
+ description: DataTypes.STRING(100),
+ blocking: DataTypes.BOOLEAN,
+ type: DataTypes.STRING(100),
+ isGroupTask: DataTypes.BOOLEAN,
+ isActive: DataTypes.BOOLEAN,
+ sponsor: DataTypes.STRING,
+ isPast: DataTypes.BOOLEAN,
+ name: DataTypes.STRING(100)
+ },
+ {}
+ );
+ Task.associate = models => {
+ Task.hasMany(models.Contribution, { foreignKey: "taskId" });
+ // associations can be defined here
+ };
+
+ return Task;
+};
+
+export default task;
diff --git a/lib/database/models/team.ts b/lib/database/models/team.ts
new file mode 100644
index 00000000..5d1cfa38
--- /dev/null
+++ b/lib/database/models/team.ts
@@ -0,0 +1,34 @@
+const team = (sequelize, DataTypes) => {
+ const Team = sequelize.define(
+ "Team",
+ {
+ name: DataTypes.STRING(150),
+ teamCode: {
+ type: DataTypes.STRING(4),
+ unique: true
+ },
+ ownerId: {
+ type: DataTypes.STRING,
+ references: {
+ model: "HackerProfiles",
+ key: "userId"
+ }
+ }
+ },
+ {}
+ );
+ Team.associate = function(models) {
+ Team.belongsTo(models.HackerProfile, {
+ as: "owner",
+ foreignKey: "ownerId",
+ constraints: false
+ });
+ Team.hasMany(models.HackerProfile, {
+ foreignKey: "teamId",
+ constraints: false
+ });
+ };
+ return Team;
+};
+
+export default team;
diff --git a/lib/database/models/types/db.ts b/lib/database/models/types/db.ts
new file mode 100644
index 00000000..de1d9d68
--- /dev/null
+++ b/lib/database/models/types/db.ts
@@ -0,0 +1,7 @@
+export default interface DBType {
+ sequelize?: any;
+ Sequelize?: any;
+ HackerProfile?: any;
+}
+
+module.exports = {};
diff --git a/lib/database/models/types/index.ts b/lib/database/models/types/index.ts
new file mode 100644
index 00000000..37799355
--- /dev/null
+++ b/lib/database/models/types/index.ts
@@ -0,0 +1 @@
+export type { default as DBType } from './db';
diff --git a/lib/database/models/unlockable.ts b/lib/database/models/unlockable.ts
new file mode 100644
index 00000000..3c7cea7f
--- /dev/null
+++ b/lib/database/models/unlockable.ts
@@ -0,0 +1,15 @@
+const unlockable = (sequelize, DataTypes) => {
+ const Unlockable = sequelize.define(
+ "Unlockable",
+ {
+ tier: DataTypes.INTEGER,
+ pointThreshold: DataTypes.INTEGER,
+ isPremium: DataTypes.BOOLEAN
+ },
+ {}
+ );
+ Unlockable.associate = models => {};
+ return Unlockable;
+};
+
+export default unlockable;
diff --git a/lib/database/utils.ts b/lib/database/utils.ts
new file mode 100644
index 00000000..253109f4
--- /dev/null
+++ b/lib/database/utils.ts
@@ -0,0 +1,119 @@
+import models from "./models";
+const Sentry = require("@sentry/node");
+
+module.exports = {
+ authMiddleware: function(req, res, next) {
+ if (req.user) {
+ return next();
+ }
+ res.status(400).send("Unauthorized");
+ },
+ preprocessRequest: function(req, res, next) {
+ delete req.body.status;
+ delete req.body.userId;
+ delete req.body.email;
+ delete req.body.role;
+ return next();
+ },
+ requireAdmin: function(req, res, next) {
+ /* Read user from database, and make sure it is in fact an admin */
+ try {
+ models.HackerProfile.findAll({
+ where: {
+ userId: req.user.id
+ }
+ }).then(hackerProfiles => {
+ if (hackerProfiles.length >= 1) {
+ if (hackerProfiles[0].get("role") === "admin") {
+ return next();
+ } else {
+ res.status(400).send("Unauthorized: Incorrect role");
+ }
+ } else {
+ res.status(400).send("Unauthorized; profile not found");
+ }
+ });
+ } catch (e) {
+ Sentry.captureException(e);
+ res.status(400).send("Unauthorized");
+ }
+ },
+ requireVolunteer: function(req, res, next) {
+ /* Read user from database, and make sure it is in fact an volunteer */
+ try {
+ models.HackerProfile.findAll({
+ where: {
+ userId: req.user.id
+ }
+ }).then(hackerProfiles => {
+ if (hackerProfiles.length >= 1) {
+ if (hackerProfiles[0].get("role") === "volunteer") {
+ return next();
+ } else {
+ res.status(400).send("Unauthorized: Incorrect role");
+ }
+ } else {
+ res.status(400).send("Unauthorized; profile not found");
+ }
+ });
+ } catch (e) {
+ Sentry.captureException(e);
+ res.status(400).send("Unauthorized");
+ }
+ },
+ requireSponsor: function(req, res, next) {
+ /* Read user from database, and make sure it is in fact an sponsor */
+ try {
+ models.HackerProfile.findAll({
+ where: {
+ userId: req.user.id
+ }
+ }).then(hackerProfiles => {
+ if (hackerProfiles.length >= 1) {
+ if (hackerProfiles[0].get("role") === "sponsor") {
+ return next();
+ } else {
+ res.status(400).send("Unauthorized: Incorrect role");
+ }
+ } else {
+ res.status(400).send("Unauthorized; profile not found");
+ }
+ });
+ } catch (e) {
+ Sentry.captureException(e);
+ res.status(400).send("Unauthorized");
+ }
+ },
+ requireNonHacker: function(req, res, next) {
+ /* Read user from database, and make sure it is in fact not a hacker */
+ try {
+ models.HackerProfile.findAll({
+ where: {
+ userId: req.user.id
+ }
+ }).then(hackerProfiles => {
+ if (hackerProfiles.length >= 1) {
+ if (hackerProfiles[0].get("role") !== "hacker") {
+ // Add role to be used
+ req.user.role = hackerProfiles[0].get("role");
+ return next();
+ } else {
+ res.status(400).send("Unauthorized: Incorrect role");
+ }
+ } else {
+ res.status(400).send("Unauthorized; profile not found");
+ }
+ });
+ } catch (e) {
+ Sentry.captureException(e);
+ res.status(400).send("Unauthorized");
+ }
+ },
+
+ requireDevelopmentEnv: function(req, res, next) {
+ if (process.env.NODE_ENV == "production") {
+ return res.redirect("/");
+ }
+ return next();
+ }
+};
diff --git a/lib/db.ts b/lib/db.ts
new file mode 100644
index 00000000..c9c5996b
--- /dev/null
+++ b/lib/db.ts
@@ -0,0 +1,22 @@
+const mysql = require("serverless-mysql");
+
+const db = mysql({
+ config: {
+ host: process.env.MYSQL_HOST,
+ database: process.env.MYSQL_DATABASE,
+ user: process.env.MYSQL_USER,
+ password: process.env.MYSQL_PASSWORD
+ }
+});
+
+exports.query = async query => {
+ try {
+ const results = await db.query(query);
+ await db.end();
+ return results;
+ } catch (error) {
+ return { error };
+ }
+};
+
+export default db
\ No newline at end of file
diff --git a/odyssey.d.ts b/odyssey.d.ts
index 12e844e3..df6d8afc 100644
--- a/odyssey.d.ts
+++ b/odyssey.d.ts
@@ -13,7 +13,7 @@ declare type FormStep = {
};
declare type Profile = {
- id: string;
+ id: number;
gender: "male" | "female" | "non-binary" | "other" | "no-say";
userId: ResourceID;
ethnicity: string;
diff --git a/package.json b/package.json
index c0a02efb..c4cd2074 100644
--- a/package.json
+++ b/package.json
@@ -55,6 +55,7 @@
"js-cookie": "^2.2.1",
"jsqr": "^1.2.0",
"jszip": "^3.2.2",
+ "list-react-files": "^0.2.0",
"moment": "^2.24.0",
"mysql": "^2.17.1",
"mysql2": "^1.7.0",
@@ -84,7 +85,9 @@
"react-use": "^13.8.1",
"sequelize": "^5.21.1",
"serialize-javascript": "^3.1.0",
+ "serverless-mysql": "^1.5.4",
"simplex-noise": "^2.4.0",
+ "sql-template-strings": "^2.2.2",
"start-server-and-test": "^1.10.6",
"styled-components": "^5.0.0",
"styled-reset": "^4.0.2",
diff --git a/pages/api/admin.ts b/pages/api/admin.ts
new file mode 100644
index 00000000..cfea2394
--- /dev/null
+++ b/pages/api/admin.ts
@@ -0,0 +1,224 @@
+const express = require("express");
+const models = require("./models");
+const utils = require("./utils");
+const router = express.Router();
+const sequelize = require("sequelize");
+const Sentry = require("@sentry/node");
+
+/* This router supports superuser routes, such as updating the status of users */
+
+router.use(utils.authMiddleware);
+router.use(utils.requireAdmin);
+
+// Full write access to a user's hackerProfile
+// Variable parameters in the foremost endpoint path restricts use to just this route.
+// Consider changing route as to not use up all single-path put requests
+router.put("/:email", async (req, res) => {
+ const updatedhackerProfile = await models.HackerProfile.update(req.body, {
+ where: {
+ email: req.params.email
+ }
+ });
+ return res.json({ hackerProfile: updatedhackerProfile });
+});
+
+// TODO: use the new client fetcher api
+router.get("/profiles", async (req, res) => {
+ const Op = sequelize.Op;
+ const { query } = req.query;
+ const flexQuery = "%" + query + "%";
+ try {
+ const profiles = await models.HackerProfile.findAll({
+ where: {
+ [Op.or]: [
+ {
+ email: {
+ [Op.like]: flexQuery
+ }
+ },
+ {
+ firstName: {
+ [Op.like]: flexQuery
+ }
+ },
+ {
+ lastName: {
+ [Op.like]: flexQuery
+ }
+ }
+ ]
+ },
+ limit: 50
+ });
+ return res.json({
+ profiles
+ });
+ } catch (e) {
+ return res.status(500).json({ error: e.message });
+ }
+});
+
+router.post("/updateRole", async (req, res) => {
+ try {
+ const { email, role } = req.body;
+ const result = await models.HackerProfile.update(
+ {
+ role: role
+ },
+ {
+ where: {
+ email: email
+ }
+ }
+ );
+
+ return res.json({ success: result });
+ } catch (e) {
+ return res.status(500).json({ error: e.message });
+ }
+});
+
+router.get("/reviews", async (req, res) => {
+ try {
+ const reviews = await models.HackerReview.findAll();
+ return res.json({ reviews: reviews });
+ } catch (e) {
+ return res.status(500).json({ err: e });
+ }
+});
+
+router.get("/reviewHistory", async (req, res) => {
+ try {
+ const reviews = await models.HackerReview.findAll({
+ where: {
+ createdBy: req.user.id
+ }
+ });
+ return res.json({ reviews: reviews });
+ } catch (e) {
+ return res.status(500).json({ err: e });
+ }
+});
+
+router.put("/review/:id", async (req, res) => {
+ const requestId = req.params.id;
+ const allowedFields = new Set([
+ "scoreOne",
+ "scoreTwo",
+ "scoreThree",
+ "comments"
+ ]);
+ const formInput = req.body;
+
+ for (let key of Object.keys(formInput)) {
+ if (!allowedFields.has(key)) {
+ return res.status(400).json({
+ error: `${key} is not a supported field`
+ });
+ }
+ }
+
+ try {
+ const result = await models.HackerReview.update(req.body, {
+ where: {
+ id: requestId
+ }
+ });
+
+ return res.json({ update: result });
+ } catch (e) {
+ return res.status(500).json({
+ error: e
+ });
+ }
+});
+
+router.get("/eligibleProfiles", async (req, res) => {
+ try {
+ const allProfiles = await models.HackerProfile.findAll({
+ where: {
+ submittedAt: {
+ [sequelize.Op.not]: null
+ }
+ },
+ include: [
+ {
+ model: models.HackerReview
+ }
+ ]
+ });
+ let filteredProfiles = allProfiles.filter(profile => {
+ const reviewsByCurrUser = profile.HackerReviews.filter(review => {
+ return review.dataValues.createdBy === req.user.id;
+ });
+ return reviewsByCurrUser.length === 0 && profile.HackerReviews.length < 1;
+ });
+ return res.json({
+ eligibleReviews: filteredProfiles
+ });
+ } catch (e) {
+ return res.status(500).json({ error: e.message });
+ }
+});
+
+router.post("/review", async (req, res) => {
+ try {
+ const formBody = req.body;
+ const newReview = await models.HackerReview.create({
+ hackerId: formBody.userId,
+ createdBy: req.user.id,
+ scoreOne: formBody.scoreOne,
+ scoreTwo: formBody.scoreTwo,
+ scoreThree: formBody.scoreThree,
+ comments: formBody.comments
+ });
+
+ return res.json({ newReview: newReview });
+ } catch (e) {
+ return res.status(500).json({ error: e.message });
+ }
+});
+
+router.get("/review", async (req, res) => {
+ try {
+ const profilesWCount = await models.HackerProfile.findAll({
+ attributes: {
+ include: [
+ [
+ sequelize.fn("COUNT", sequelize.col("HackerReviews.id")),
+ "reviewCount"
+ ]
+ ]
+ },
+ include: [
+ {
+ model: models.HackerReview,
+ attributes: []
+ }
+ ],
+ group: ["HackerProfile.userId"]
+ });
+
+ const acceptableProfile = profilesWCount.find(profile => {
+ return profile.dataValues.reviewCount < 1;
+ });
+ if (acceptableProfile) {
+ const newReview = await models.HackerReview.create({
+ hackerId: acceptableProfile.dataValues.userId,
+ createdBy: req.user.id
+ });
+
+ return res.json({
+ review: newReview,
+ profile: acceptableProfile
+ });
+ } else {
+ return res.json({ review: null, profile: null }); // Returns empty when there are no more profiles
+ }
+ } catch (e) {
+ console.log(e);
+ return res.status(500).json({ err: e });
+ }
+});
+
+export { router };
diff --git a/pages/api/admin/review.ts b/pages/api/admin/review.ts
new file mode 100644
index 00000000..a8ab7dbd
--- /dev/null
+++ b/pages/api/admin/review.ts
@@ -0,0 +1,44 @@
+// @ts-nocheck
+module.exports = async (req, res) => {
+ try {
+ const profilesWCount = await models.HackerProfile.findAll({
+ attributes: {
+ include: [
+ [
+ sequelize.fn("COUNT", sequelize.col("HackerReviews.id")),
+ "reviewCount"
+ ]
+ ]
+ },
+ include: [
+ {
+ model: models.HackerReview,
+ attributes: []
+ }
+ ],
+ group: ["HackerProfile.userId"]
+ });
+
+ const acceptableProfile = profilesWCount.find(profile => {
+ return profile.dataValues.reviewCount < 1;
+ });
+ if (acceptableProfile) {
+ const newReview = await models.HackerReview.create({
+ hackerId: acceptableProfile.dataValues.userId,
+ createdBy: req.user.id
+ });
+
+ return res.json({
+ review: newReview,
+ profile: acceptableProfile
+ });
+ } else {
+ return res.json({ review: null, profile: null }); // Returns empty when there are no more profiles
+ }
+ } catch (e) {
+ console.log(e);
+ return res.status(500).json({ err: e });
+ }
+};
+
+export default {}
\ No newline at end of file
diff --git a/pages/api/config.ts b/pages/api/config.ts
new file mode 100644
index 00000000..9b93d538
--- /dev/null
+++ b/pages/api/config.ts
@@ -0,0 +1,28 @@
+const config = {
+ development: {
+ username: process.env.DEV_DB_USERNAME,
+ password: process.env.DEV_DB_PASSWORD,
+ database: process.env.DEV_DB_NAME,
+ host: process.env.DEV_DB_HOSTNAME,
+ logging: false,
+ dialect: "mysql"
+ },
+ test: {
+ username: process.env.TEST_DB_USERNAME,
+ password: process.env.TEST_DB_PASSWORD,
+ database: process.env.TEST_DB_NAME,
+ host: process.env.TEST_DB_HOSTNAME,
+ logging: false,
+ dialect: "mysql"
+ },
+ production: {
+ username: process.env.PROD_DB_USERNAME,
+ password: process.env.PROD_DB_PASSWORD,
+ database: process.env.PROD_DB_NAME,
+ host: process.env.PROD_DB_HOSTNAME,
+ dialect: "mysql"
+ }
+};
+
+export default config;
+
\ No newline at end of file
diff --git a/pages/api/contribution.ts b/pages/api/contribution.ts
new file mode 100644
index 00000000..8fbe6c10
--- /dev/null
+++ b/pages/api/contribution.ts
@@ -0,0 +1,38 @@
+const express = require("express");
+const models = require("./models");
+const utils = require("./utils");
+const router = express.Router();
+const sequelize = require("sequelize");
+const Sentry = require("@sentry/node");
+
+router.use(utils.authMiddleware);
+
+router.get("/all", async (req, res) => {
+ const contributions = await models.Contribution.findAll();
+ return res.json({ success: contributions });
+});
+
+router.get("/owned", async (req, res) => {
+ const contributions = await models.Contribution.findAll({
+ where: {
+ personId: req.user.id
+ }
+ });
+ return res.json({ success: contributions });
+});
+
+router.post("/create", async (req, res) => {
+ const input = req.body;
+
+ if (!input.taskId) {
+ return res.status(400).json({ error: "Missing TaskID" });
+ } else {
+ const result = await models.Contribution.build({
+ personId: req.user.id,
+ taskId: input.taskId
+ }).save();
+ return res.json({ success: result });
+ }
+});
+
+export { router };
diff --git a/pages/api/event.ts b/pages/api/event.ts
new file mode 100644
index 00000000..1fc90d15
--- /dev/null
+++ b/pages/api/event.ts
@@ -0,0 +1,52 @@
+const express = require("express");
+const models = require("./models");
+const utils = require("./utils");
+const router = express.Router();
+const sequelize = require("sequelize");
+const Sentry = require("@sentry/node");
+
+/* This router supports superuser routes, such as updating the status of users */
+
+router.use(utils.authMiddleware);
+router.use(utils.requireAdmin);
+
+router.get("/", async (req, res) => {
+ try {
+ const events = await models.Event.findAll();
+ return res.json({ events });
+ } catch (e) {
+ return res.json({ err: e });
+ }
+});
+
+router.post("/", async (req, res) => {
+ try {
+ // @ts-ignore
+ const { startsAt, endsAt, name, description } = { ...req.body };
+ const newEvent = await models.Event.create({
+ name: name,
+ description: description,
+ startsAt: startsAt,
+ endsAt: endsAt
+ });
+ return res.json({ newEvent });
+ } catch (e) {
+ return res.json({ err: e });
+ }
+});
+
+router.delete("/:id", async (req, res) => {
+ try {
+ const id = req.params.id;
+ await models.Event.destroy({
+ where: {
+ id: id
+ }
+ });
+ return res.status(200).json({ result: "success" });
+ } catch (e) {
+ return res.status(400).json({ err: e.message });
+ }
+});
+
+export { router };
diff --git a/pages/api/hackerLive.ts b/pages/api/hackerLive.ts
new file mode 100644
index 00000000..821a80ea
--- /dev/null
+++ b/pages/api/hackerLive.ts
@@ -0,0 +1,282 @@
+// Contains routes the correspond to day of operations
+
+const express = require("express");
+const models = require("./models");
+const utils = require("./utils");
+const router = express.Router();
+const sequelize = require("sequelize");
+
+router.use(utils.authMiddleware);
+
+router.get("/battlepass", async (req, res) => {
+ return res.json({
+ success: [
+ {
+ id: "1",
+ isPremium: false,
+ pointValue: 6000,
+ prizeName: "10 Raffle Tickets"
+ },
+ {
+ id: "2",
+ isPremium: false,
+ pointValue: 6000,
+ prizeName: "HackSC T-Shirt"
+ },
+ {
+ id: "3",
+ isPremium: false,
+ pointValue: 6000,
+ prizeName: "10 Raffle Tickets"
+ },
+ {
+ id: "4",
+ isPremium: false,
+ pointValue: 6000,
+ prizeName: "10 Raffle Tickets"
+ },
+ {
+ id: "5",
+ isPremium: false,
+ pointValue: 6000,
+ prizeName: "10 Raffle Tickets"
+ },
+ {
+ id: "6",
+ isPremium: false,
+ pointValue: 6000,
+ prizeName: "10 Raffle Tickets"
+ },
+ {
+ id: "7",
+ isPremium: false,
+ pointValue: 6000,
+ prizeName: "10 Raffle Tickets"
+ },
+ {
+ id: "8",
+ isPremium: false,
+ pointValue: 6000,
+ prizeName: "HackSC Stickers"
+ },
+ {
+ id: "9",
+ isPremium: false,
+ pointValue: 6000,
+ prizeName: "10 Raffle Tickets"
+ },
+ {
+ id: "10",
+ isPremium: false,
+ pointValue: 6000,
+ prizeName: "Drawstring Bag"
+ },
+ {
+ id: "11",
+ isPremium: false,
+ pointValue: 6000,
+ prizeName: "100 Raffle Tickets"
+ },
+ {
+ id: "12",
+ isPremium: true,
+ pointValue: 6000,
+ prizeName: "100 Raffle Tickets & Travel Reimbursement!"
+ },
+ {
+ id: "13",
+ isPremium: true,
+ pointValue: 6000,
+ prizeName: "100 Raffle Tickets"
+ },
+ {
+ id: "14",
+ isPremium: true,
+ pointValue: 6000,
+ prizeName: "100 Raffle Tickets"
+ },
+ {
+ id: "15",
+ isPremium: true,
+ pointValue: 6000,
+ prizeName: "100 Raffle Tickets"
+ },
+ {
+ id: "16",
+ isPremium: true,
+ pointValue: 6000,
+ prizeName: "100 Raffle Tickets"
+ },
+ {
+ id: "17",
+ isPremium: true,
+ pointValue: 6000,
+ prizeName: "100 Raffle Tickets"
+ },
+ {
+ id: "18",
+ isPremium: true,
+ pointValue: 6000,
+ prizeName: "100 Raffle Tickets"
+ },
+ {
+ id: "19",
+ isPremium: true,
+ pointValue: 6000,
+ prizeName: "100 Raffle Tickets"
+ },
+ {
+ id: "20",
+ isPremium: true,
+ pointValue: 6000,
+ prizeName: "100 Raffle Tickets & Hacker Socks"
+ },
+ {
+ id: "21",
+ isPremium: true,
+ pointValue: 6000,
+ prizeName: "100 Raffle Tickets"
+ },
+ {
+ id: "22",
+ isPremium: true,
+ pointValue: 6000,
+ prizeName: "100 Raffle Tickets & Hacker Hat"
+ }
+ ]
+ });
+});
+
+//TODO: Figure out why this route exists
+router.get("/personInfo", async (req, res) => {
+ const person = await models.Person.findByPk(req.user.id);
+ return res.json({ success: person });
+});
+
+router.get("/tasks", async (req, res) => {
+ const tasks = await models.Task.findAll();
+ return res.json({ success: tasks });
+});
+
+router.get("/event/list", async (req, res) => {
+ try {
+ const events = await models.Event.findAll();
+ return res.json({ success: events });
+ } catch (e) {
+ return res.json({ error: e });
+ }
+});
+
+router.get("/houseInfo/list", async (req, res) => {
+ const houses = await models.House.findAll({
+ include: [
+ {
+ model: models.Person.scope("hideProfile"),
+ as: "HouseMembers",
+ required: false
+ }
+ ]
+ });
+ return res.json({ success: houses });
+});
+
+router.get("/houseInfo/:id", async (req, res) => {
+ const houseId = req.params.id;
+ const house = await models.House.findByPk(houseId, {
+ include: [
+ {
+ model: models.Person,
+ as: "HouseMembers"
+ }
+ ]
+ });
+ return res.json({ success: house });
+});
+
+router.get("/incompleteTasks", async (req, res) => {
+ const allTasks = await models.Task.findAll();
+ const completedTasks = await models.Contribution.findAll({
+ where: {
+ personId: req.user.id
+ }
+ });
+ if (!completedTasks) {
+ return res.json({ success: allTasks });
+ }
+ const completeTaskIds = completedTasks.map(x => {
+ return x.get("Task").get("id");
+ });
+
+ const incompleteTasks = allTasks.filter(x => {
+ const taskId = x.get("id");
+ return !completeTaskIds.includes(taskId);
+ });
+
+ return res.json({ success: incompleteTasks });
+});
+
+router.get("/rafflePoints", async (req, res) => {
+ const contributions = await models.Contribution.findAll({
+ where: {
+ personId: req.user.id
+ },
+ attributes: [
+ [sequelize.fn("SUM", sequelize.col("Task.points")), "totalPoints"]
+ ],
+ include: [{ model: models.Task, required: true }]
+ });
+
+ const person = await models.Person.findByPk(req.user.id);
+
+ if (!person) {
+ return res.status(404).json({ error: "Hacker Person Profile Not Found" });
+ }
+
+ if (!contributions) {
+ return res.status(404).json({ error: "Hacker Points Not Found" });
+ } else {
+ const totalPoints = parseInt(contributions[0].get("totalPoints"));
+ const isPersonBPComplete = person.get("isBattlepassComplete") || 0;
+
+ const houseTier = Math.min(Math.floor(totalPoints / 6000), 10);
+ const tierPoints = [10, 10, 20, 30, 40, 50, 60, 60, 70, 70, 170];
+ const premiumTierPoints = [
+ 100,
+ 200,
+ 300,
+ 400,
+ 500,
+ 600,
+ 700,
+ 800,
+ 900,
+ 1000,
+ 11000
+ ];
+
+ let totalRafflePoints =
+ isPersonBPComplete == 0
+ ? tierPoints[houseTier]
+ : tierPoints[houseTier] + premiumTierPoints[houseTier];
+
+ const houseId = person.houseId || 0;
+
+ if (houseId === 6) {
+ totalRafflePoints += 1000;
+ } else if (houseId === 5) {
+ totalRafflePoints += 500;
+ } else if (houseId === 4) {
+ totalRafflePoints += 250;
+ } else if (houseId === 3) {
+ totalRafflePoints += 100;
+ }
+
+ return res.json({
+ success: {
+ totalRafflePoints
+ }
+ });
+ }
+});
+
+export { router };
diff --git a/pages/api/hackerProfile.ts b/pages/api/hackerProfile.ts
new file mode 100644
index 00000000..3b169e5b
--- /dev/null
+++ b/pages/api/hackerProfile.ts
@@ -0,0 +1,348 @@
+const express = require("express");
+const models = require("./models");
+const utils = require("./utils");
+const router = express.Router();
+const Sentry = require("@sentry/node");
+const Busboy = require("busboy");
+const AWS = require("aws-sdk");
+
+router.use(utils.authMiddleware);
+router.use(utils.preprocessRequest);
+
+router.get("/", async (req, res) => {
+ const [hackerProfile] = await models.HackerProfile.findOrCreate({
+ where: {
+ userId: req.user.id
+ },
+ defaults: {
+ email: req.user._json.email,
+ status: req.user._json.email_verified ? "verified" : "unverified"
+ },
+ include: [
+ {
+ model: models.Team,
+ as: "team"
+ }
+ ]
+ });
+
+ hackerProfile.referred = await hackerProfile.getReferred();
+
+ if (hackerProfile.status === "unverified" && req.user._json.email_verified) {
+ // Update hacker profile
+ try {
+ hackerProfile.status = "verified";
+ await hackerProfile.save();
+ return res.json({ hackerProfile });
+ } catch (exception) {
+ return res.status(500).json({
+ message: "Error trying to save profile",
+ exception
+ });
+ }
+ }
+
+ return res.json({ hackerProfile });
+});
+
+router.put("/", async (req, res) => {
+ // Get the users current hacker profile
+ const currentHackerProfile = await models.HackerProfile.findOne({
+ where: { userId: req.user.id }
+ });
+
+ const formInput = req.body;
+
+ // If the user is saving a profile, make sure that they have not already submitted one before
+ if (currentHackerProfile.submittedAt !== null) {
+ return res.status(400).json({
+ error: "You have already submitted an application"
+ });
+ }
+
+ // Only allow certain fields for form input
+ const allowedFields = new Set([
+ "gender",
+ "ethnicity",
+ "email",
+ "major",
+ "minor",
+ "skills",
+ "interests",
+ "firstName",
+ "resume",
+ "lastName",
+ "phoneNumber",
+ "school",
+ "year",
+ "skillLevel",
+ "graduationDate",
+ "over18",
+ "needBus",
+ "links",
+ "questionOne",
+ "questionTwo",
+ "questionThree",
+ "codeOfConduct",
+ "authorize",
+ "marketing",
+ "submit",
+ "referrerCode"
+ ]);
+
+ for (let key of Object.keys(formInput)) {
+ if (!allowedFields.has(key)) {
+ return res.status(400).json({
+ error: `${key} is not a supported field`
+ });
+ }
+ }
+
+ // TODO: Validate inputs
+
+ const updatedProfileFields = {
+ ...formInput
+ };
+
+ /*
+ Advance the user in the application process if they meet the following conditions
+ - Have previously not submitted a profile
+ - Have filled out all of the required fields
+ */
+
+ if (formInput.submit) {
+ if (currentHackerProfile.submittedAt === null) {
+ if (
+ formInput.gender &&
+ formInput.ethnicity &&
+ formInput.major &&
+ formInput.firstName &&
+ formInput.lastName &&
+ formInput.phoneNumber &&
+ formInput.school &&
+ formInput.skillLevel &&
+ formInput.graduationDate &&
+ formInput.over18 &&
+ formInput.questionOne &&
+ formInput.questionTwo &&
+ formInput.questionThree &&
+ formInput.codeOfConduct &&
+ formInput.authorize
+ ) {
+ updatedProfileFields.submittedAt = new Date();
+ updatedProfileFields.status = "submitted";
+ } else {
+ return res.status(400).json({
+ error: "Not all required fields are filled out"
+ });
+ }
+ }
+ }
+
+ // Update, then re-retrieve the updated hacker profile
+ await models.HackerProfile.update(updatedProfileFields, {
+ where: {
+ userId: req.user.id
+ }
+ });
+
+ const updatedHackerProfile = await models.HackerProfile.findOne({
+ where: { userId: req.user.id }
+ });
+
+ return res.json({ hackerProfile: updatedHackerProfile });
+});
+
+// Direct patch the referrerCode if there isn't one
+router.put("/referrerCode", async (req, res) => {
+ const referrerCode = req.body.referrerCode;
+
+ const currentHackerProfile = await models.HackerProfile.findOne({
+ where: { userId: req.user.id }
+ });
+
+ const currentReferrerCode = currentHackerProfile.referrerCode;
+
+ if (!currentReferrerCode || currentReferrerCode == "") {
+ const updatedFields = { referrerCode };
+
+ await models.HackerProfile.update(updatedFields, {
+ where: {
+ userId: req.user.id
+ }
+ });
+ }
+
+ return res.send();
+});
+
+router.post("/resume", utils.authMiddleware, async (req, res) => {
+ const user = req.user;
+ if (req.files) {
+ const file = req.files.file;
+ const s3 = new AWS.S3({
+ accessKeyId: process.env.S3_ACCESS_KEY,
+ secretAccessKey: process.env.S3_SECRET
+ });
+
+ const params = {
+ Bucket: "hacksc-odyssey",
+ Key: user.id,
+ Body: file.data
+ };
+
+ s3.upload(params, function(err, data) {
+ if (!err) {
+ models.HackerProfile.update(
+ { resume: data.Location },
+ {
+ where: {
+ userId: req.user.id
+ }
+ }
+ )
+ .then(updatedProfile => {
+ res.json({ hackerProfileUpdate: updatedProfile });
+ })
+ .catch(e => {
+ res.status(500).json({ error: e });
+ });
+ } else {
+ res.json(500, { message: "Failed to upload Resume" });
+ }
+ });
+ }
+});
+
+// Confirmation and declination routes
+router.post("/confirm", (req, res) => {
+ const { body, files, user } = req;
+
+ if (
+ body["travelMethod"] &&
+ files &&
+ files["travelPlan"] &&
+ body["shirtSize"] &&
+ body["codeOfConduct"]
+ ) {
+ // All required fields are in the request body
+ const s3 = new AWS.S3({
+ accessKeyId: process.env.S3_ACCESS_KEY,
+ secretAccessKey: process.env.S3_SECRET
+ });
+
+ const file = files.travelPlan;
+ const fileExtension = file.name.split(".").slice(-1)[0];
+
+ const params = {
+ Bucket: "hacksc-odyssey",
+ Key: "confirmation-proof/" + user.id + "." + fileExtension,
+ Body: file.data
+ };
+
+ s3.upload(params, function(err, data) {
+ if (!err) {
+ // Create dietary restrictions string
+ const dietaryRestrictions = [];
+ if (body["dietaryRestrictions.vegetarian"] === "true")
+ dietaryRestrictions.push("vegetarian");
+ if (body["dietaryRestrictions.vegan"] === "true")
+ dietaryRestrictions.push("vegan");
+ if (body["dietaryRestrictions.halal"] === "true")
+ dietaryRestrictions.push("halal");
+ if (body["dietaryRestrictions.kosher"] === "true")
+ dietaryRestrictions.push("kosher");
+ if (body["dietaryRestrictions.nutAllergy"] === "true")
+ dietaryRestrictions.push("nutAllergy");
+ if (body["dietaryRestrictions.lactoseIntolerant"] === "true")
+ dietaryRestrictions.push("lactoseIntolerant");
+ if (body["dietaryRestrictions.glutenFree"] === "true")
+ dietaryRestrictions.push("glutenFree");
+ if (
+ body["dietaryRestrictions.other"] &&
+ body["dietaryRestrictions.other"].trim() !== ""
+ )
+ dietaryRestrictions.push(body["dietaryRestrictions.other"]);
+
+ models.HackerProfile.update(
+ {
+ travelOrigin: body["travelOrigin"] || null,
+ travelMethod: body["travelMethod"],
+ shirtSize: body["shirtSize"],
+ travelPlan: data.Location,
+ dietaryRestrictions: dietaryRestrictions.join(" "),
+ confirmCodeOfConduct:
+ body["codeOfConduct"] === "true" ? true : false,
+ status: "confirmed",
+ noBusCheck: body["noBusCheck"] === "true" ? true : false,
+ confirmedAt: new Date()
+ },
+ {
+ where: {
+ userId: req.user.id,
+ status: "accepted"
+ }
+ }
+ )
+ .then(updatedProfile => {
+ res.json({ hackerProfileUpdate: updatedProfile });
+ })
+ .catch(e => {
+ res.status(500).json({ error: e });
+ });
+ } else {
+ res.json(500, { message: "Failed to upload confirmation proof" });
+ }
+ });
+ } else {
+ return res.json(500, {
+ message: "Failed to confirm attendance, missing fields"
+ });
+ }
+});
+
+router.post("/decline", async (req, res) => {
+ await models.HackerProfile.update(
+ {
+ status: "declined",
+ declinedAt: new Date()
+ },
+ {
+ where: {
+ userId: req.user.id,
+ status: "accepted"
+ }
+ }
+ );
+
+ return res.json({
+ message: "Successfully processed request to decline HackSC 2020 acceptance"
+ });
+});
+
+router.post("/undecline", async (req, res) => {
+ await models.HackerProfile.update(
+ {
+ status: "accepted",
+ declinedAt: null
+ },
+ {
+ where: {
+ userId: req.user.id,
+ status: "declined"
+ }
+ }
+ );
+
+ return res.json({
+ message:
+ "Successfully processed request to un-decline HackSC 2020 acceptance"
+ });
+});
+
+router.get("/list", utils.requireDevelopmentEnv, async (req, res) => {
+ const profiles = await models.HackerProfile.findAll({});
+ return res.json({ profiles });
+});
+
+export { router };
diff --git a/pages/api/live.ts b/pages/api/live.ts
new file mode 100644
index 00000000..c33885c4
--- /dev/null
+++ b/pages/api/live.ts
@@ -0,0 +1,446 @@
+// Contains routes the correspond to day of operations
+
+const express = require("express");
+const models = require("./models");
+const utils = require("./utils");
+const router = express.Router();
+const sequelize = require("sequelize");
+
+router.use(utils.authMiddleware);
+router.use(utils.requireNonHacker);
+
+const actions = {
+ CHECKIN: "checkin",
+ CONTRIB: "contrib",
+ GROUP_CONTRIB: "groupContrib",
+ EMAIL_CONTRIB: "emailContrib",
+ IDENTIFY: "identify",
+ SUBMIT: "submit",
+ JUDGE: "judge"
+};
+
+router.post("/dispatch", async (req, res) => {
+ // @ts-ignore
+ const { qrCodeId, actionId } = { ...req.body };
+
+ const hackerProfile = await models.HackerProfile.findOne({
+ where: { qrCodeId: qrCodeId }
+ });
+
+ if (hackerProfile === null) {
+ return res.status(404).json({
+ error: "No user has been assigned this QR code"
+ });
+ }
+ const userId = hackerProfile.get("userId");
+
+ //TODO: Add sentry logging at the dispatch level
+ switch (actionId) {
+ case actions.CHECKIN:
+ return await handleCheckin(userId, req, res);
+ case actions.CONTRIB:
+ return await handleContrib(userId, req, res);
+ case actions.GROUP_CONTRIB:
+ return await handleGroupContrib(userId, req, res);
+ case actions.EMAIL_CONTRIB:
+ return await handleEmailContrib(userId, req, res);
+ case actions.IDENTIFY:
+ return await handleIdentify(userId, req, res);
+ case actions.SUBMIT:
+ return await handleSubmit(userId, req, res);
+ case actions.JUDGE:
+ return await handleJudge(userId, req, res);
+ }
+});
+
+/*
+----- Action Dispatchers below, register your action above and implement the appropriate handler below -----
+*/
+
+async function handleSubmit(userId, req, res) {
+ const person = await models.Person.findByPk(userId);
+ if (!person) {
+ return res
+ .status(400)
+ .json({ error: "Couldn't find hacker points profile" });
+ }
+ person.isBattlepassComplete = true;
+ await person.save();
+
+ return res.json({ success: person });
+}
+
+async function handleGroupContrib(userId, req, res) {
+ try {
+ if (!req.body.taskId) {
+ return res.status(400).json({ error: "Bad Request, taskId not found" });
+ }
+ const result = await models.Person.findOne({
+ where: {
+ identityId: userId
+ },
+ include: [
+ {
+ model: models.ProjectTeam,
+ required: false,
+ include: [
+ {
+ model: models.Person,
+ required: false
+ }
+ ]
+ }
+ ]
+ });
+ const teammates = result.get("ProjectTeam").get("People");
+ const taskId = req.body.taskId;
+ const taskMultiplier = getMultiplierForTask(taskId);
+ const teammateContribs = teammates.map(tm => {
+ const tmId = tm.dataValues.identityId;
+ return models.Contribution.create({
+ personId: tmId,
+ multiplier: taskMultiplier,
+ scannerId: req.user.id,
+ taskId: taskId
+ });
+ });
+
+ await Promise.all(teammateContribs);
+ return res.json({ success: teammates });
+ } catch (e) {
+ return res.status(400).json({ error: e.message });
+ }
+}
+
+async function handleContrib(userId, req, res) {
+ const input = req.body;
+
+ const profile = await models.HackerProfile.findByPk(userId);
+
+ if (!input.taskId) {
+ return res.status(400).json({ error: "Invalid request" });
+ }
+ const taskMultiplier = getMultiplierForTask(input.taskId);
+ const [result, isCreated] = await models.Contribution.findOrCreate({
+ defaults: {
+ multiplier: taskMultiplier,
+ scannerId: req.user.id
+ },
+ where: {
+ personId: userId,
+ taskId: input.taskId
+ }
+ });
+ if (!isCreated) {
+ return res.status(400).json({
+ error: "User already has completed this task"
+ });
+ }
+ return res.json({
+ success: {
+ contribution: result,
+ message: `Successfully created a task contribution for ${profile.firstName} ${profile.lastName}`
+ }
+ });
+}
+
+async function getMultiplierForTask(taskId) {
+ try {
+ const result = await models.Task.findOne({
+ where: {
+ id: taskId
+ },
+
+ include: [
+ {
+ model: models.Grouping,
+ required: false,
+ include: [
+ {
+ model: models.Multiplier,
+ required: false
+ }
+ ]
+ }
+ ]
+ });
+ const multipliers = result.get("Grouping").get("Multipliers");
+ return multipliers.reduce((x, y) => x + y.dataValues.multiplierValue, 0);
+ } catch (e) {
+ return 1;
+ }
+}
+
+async function handleEmailContrib(userEmail, req, res) {
+ const input = req.body;
+ if (!input.taskId) {
+ return res.status(400).json({ error: "Requires specified taskId" });
+ } else {
+ // Try to find a model by email
+ const profile = await models.HackerProfile.findOne({
+ where: {
+ email: userEmail
+ }
+ });
+ const userId = profile.get("userId");
+ const taskMultiplier = getMultiplierForTask(input.taskId);
+ const [result, isCreated] = await models.Contribution.findOrCreate({
+ defaults: {
+ multiplier: taskMultiplier,
+ scannerId: req.user.id
+ },
+ where: {
+ personId: userId,
+ // @ts-ignore
+ taskId: taskId
+ }
+ });
+ if (!isCreated) {
+ return res.status(400).json({
+ error: "User already has completed this task"
+ });
+ }
+ return res.json({ success: result });
+ }
+}
+
+async function handleCheckin(userId, req, res) {
+ const profile = await models.HackerProfile.findOne({
+ where: { userId: userId }
+ });
+ // const profileStatus = profile.get("status");
+
+ // const invalidStatuses = [
+ // "unverified",
+ // "verified",
+ // "rejected",
+ // "submitted",
+ // "checkedIn"
+ // ];
+ // if (invalidStatuses.includes(profileStatus)) {
+ // return res.status(400).json({ error: `User has status ${profileStatus}` });
+ // }
+
+ const [pointsProfile, isCreated] = await models.Person.findOrCreate({
+ where: { identityId: userId },
+ defaults: { isBattlepassComplete: false }
+ });
+
+ if (!isCreated) {
+ return res
+ .status(400)
+ .json({ error: "Hacker has already had a person profile created" });
+ }
+
+ const minHouse = await models.House.findOne({
+ attributes: [
+ ["id", "id"],
+ ["name", "name"],
+ [
+ sequelize.literal(
+ "(SELECT COUNT(*) FROM persons where persons.houseId = House.id)"
+ ),
+ "personCount"
+ ]
+ ],
+ order: [[sequelize.literal("personCount"), "ASC"]]
+ });
+
+ profile.status = "checkedIn";
+ await profile.save();
+
+ pointsProfile.houseId = minHouse.id;
+ await pointsProfile.save();
+
+ return res.json({ success: pointsProfile });
+}
+
+async function handleIdentify(userId, req, res) {
+ const profile = await models.HackerProfile.findOne({
+ where: { userId: userId }
+ });
+
+ const person = await models.Person.findByPk(userId);
+
+ if (profile) {
+ const returnProfile = {
+ userId: profile.userId,
+ firstName: profile.firstName,
+ lastName: profile.lastName,
+ email: profile.email,
+ isBattlepassComplete: person.isBattlepassComplete
+ };
+
+ return res.json({ success: returnProfile });
+ } else {
+ return res.status(404).json({ error: "Could not find user" });
+ }
+}
+
+async function handleJudge(userId, req, res) {
+ const Op = sequelize.Op;
+
+ const person = await models.Person.findByPk(userId, {
+ include: [{ model: models.ProjectTeam, required: false }]
+ });
+ let memberIds = [];
+ let memberProfiles = [];
+
+ if (!person) {
+ return res.status(404).json({ error: "Could not find user" });
+ } else if (!person.ProjectTeam && !person.isBattlepassComplete) {
+ // Enable single hacker for premium
+ memberProfiles.push(person.Profile);
+ memberIds.push(person.identityId);
+ } else if (person.ProjectTeam) {
+ // enable everyone's
+ const { Members } = person.ProjectTeam;
+ for (let i in Members) {
+ let p = Members[i];
+ if (!p.isBattlepassComplete) {
+ memberProfiles.push(p.Profile);
+ memberIds.push(p.identityId);
+ }
+ }
+ }
+
+ if (memberIds.length < 1) {
+ return res.status(400).json({
+ error: "User & Team has already been confirmed for submission"
+ });
+ }
+ const contributionAdditions = memberIds.map(pId => {
+ return models.Contribution.create({
+ personId: pId,
+ taskId: 53, //Hardcoded ID of the project submission task
+ multiplier: 0,
+ scannerId: req.user.id
+ });
+ });
+
+ // Adds contributions
+ await Promise.all(contributionAdditions);
+
+ await models.Person.update(
+ {
+ isBattlepassComplete: true
+ },
+ {
+ where: {
+ identityId: {
+ [Op.in]: memberIds
+ }
+ }
+ }
+ );
+
+ return res.json({ success: memberProfiles });
+}
+
+router.get("/lookup", async (req, res) => {
+ const lookupFilter = {};
+
+ const { firstName, lastName, email } = req.query;
+
+ if (!!firstName) {
+ lookupFilter["firstName"] = firstName;
+ }
+
+ if (!!lastName) {
+ lookupFilter["lastName"] = lastName;
+ }
+
+ if (!!email) {
+ lookupFilter["email"] = email;
+ }
+
+ const profiles = await models.HackerProfile.findAll({
+ where: lookupFilter
+ });
+
+ return res.json({
+ success: profiles
+ });
+});
+
+router.post("/assign-qr", async (req, res) => {
+ const { userId, qrCodeId } = req.body;
+
+ if (!!userId && !!qrCodeId) {
+ await models.HackerProfile.update(
+ {
+ qrCodeId: qrCodeId.trim().toUpperCase()
+ },
+ {
+ where: {
+ userId
+ }
+ }
+ );
+
+ return res.json({
+ success: {
+ message: `Updated profile with user ID ${userId} to have the QR Code Id ${qrCodeId}`
+ }
+ });
+ } else {
+ return res.status(400).json({
+ error: "Missing data, need both userId and qrCodeId"
+ });
+ }
+});
+
+router.get("/identity-check/:userId", async (req, res) => {
+ if (req.params.userId) {
+ const userId = req.params.userId;
+ const profile = await models.HackerProfile.findOne({
+ where: { userId: userId }
+ });
+
+ if (profile) {
+ if (["checkedIn", "confirmed"].includes(profile.status)) {
+ return res.json({
+ success: {
+ firstName: profile.firstName,
+ lastName: profile.lastName
+ }
+ });
+ } else {
+ return res.status(400).json({
+ error: "user cannot be scanned! neither confirmed nor checkedIn"
+ });
+ }
+ } else {
+ return res
+ .status(404)
+ .json({ error: "could not find a profile with that userId" });
+ }
+ } else {
+ return res.status(400).json({ error: "missing user ID" });
+ }
+});
+
+router.get("/hacker/:qrCodeId", async (req, res) => {
+ const qrCodeId = req.params.qrCodeId;
+ const result = await models.HackerProfile.findOne({
+ where: {
+ qrCodeId: qrCodeId
+ }
+ });
+ if (!result) {
+ return res.status(404).json({ error: "Hacker not Found" });
+ }
+ const userId = result.get("userId");
+ const contributions = await models.Contribution.findAll({
+ where: {
+ personId: userId
+ },
+ attributes: [
+ [sequelize.fn("SUM", sequelize.col("Task.points")), "totalPoints"]
+ ],
+ include: [{ model: models.Task, required: true }]
+ });
+ return res.json({ success: contributions });
+});
+
+export { router };
diff --git a/pages/api/login.ts b/pages/api/login.ts
new file mode 100644
index 00000000..05b7588f
--- /dev/null
+++ b/pages/api/login.ts
@@ -0,0 +1,97 @@
+var express = require("express");
+var passport = require("passport");
+var util = require("util");
+var url = require("url");
+var querystring = require("querystring");
+var utils = require("./utils");
+var router = express.Router();
+
+var secured = function(req, res, next) {
+ if (req.user) {
+ return next();
+ }
+ req.session.returnTo = req.originalUrl;
+ res.redirect("/");
+};
+
+router.get("/devlogin", utils.requireDevelopmentEnv, function(req, res) {
+ req.logout();
+ const { id, role } = req.query;
+ const user = {
+ id,
+ role,
+ _json: { email: "", email_verified: true }
+ };
+
+ req.logIn(user, function(err) {
+ if (err) {
+ res.status(400).send(err);
+ }
+ res.json(user);
+ });
+});
+
+router.get(
+ "/login",
+ passport.authenticate("auth0", {
+ scope: "openid email profile"
+ }),
+ function(req, res) {
+ res.redirect("/");
+ }
+);
+
+router.get("/callback", function(req, res, next) {
+ passport.authenticate("auth0", function(err, user, info) {
+ if (err) {
+ return next(err);
+ }
+ if (!user) {
+ return res.redirect("/");
+ }
+ req.logIn(user, function(err) {
+ if (err) {
+ return next(err);
+ }
+ res.redirect("/dashboard");
+ });
+ })(req, res, next);
+});
+
+router.get("/logout", (req, res) => {
+ req.logout();
+ var returnTo = req.protocol + "://" + req.hostname;
+ const port = req.connection.localPort;
+ if (
+ port !== undefined &&
+ req.hostname == "localhost" &&
+ port !== 80 &&
+ port !== 443
+ ) {
+ returnTo += ":" + port;
+ }
+ var logoutURL = new url.URL(
+ util.format("https://%s/logout", process.env.AUTH0_DOMAIN)
+ );
+ var searchString = querystring.stringify({
+ client_id: process.env.AUTH0_CLIENT_ID,
+ returnTo: returnTo
+ });
+ logoutURL.search = searchString;
+
+ res.redirect(logoutURL);
+});
+
+router.get("/needAuth", secured, function(req, res, next) {
+ res.send("Hey, you're authenticated!");
+});
+
+router.get("/profile", secured, function(req, res, next) {
+ if (!req.user) {
+ res.json(403, { message: "No User is logged in" });
+ } else {
+ res.json(req.user);
+ }
+});
+
+export {router};
diff --git a/pages/api/models/action.ts b/pages/api/models/action.ts
new file mode 100644
index 00000000..b5c066b7
--- /dev/null
+++ b/pages/api/models/action.ts
@@ -0,0 +1,14 @@
+const action = (sequelize, DataTypes) => {
+ const Action = sequelize.define(
+ "Action",
+ {
+ role: DataTypes.STRING(100),
+ name: DataTypes.STRING(100)
+ },
+ { tableName: "Actions" }
+ );
+ Action.associate = models => {};
+ return Action;
+};
+
+export default action;
diff --git a/pages/api/models/contribution.ts b/pages/api/models/contribution.ts
new file mode 100644
index 00000000..6f162301
--- /dev/null
+++ b/pages/api/models/contribution.ts
@@ -0,0 +1,27 @@
+const contribution = (sequelize, DataTypes) => {
+ const Contribution = sequelize.define(
+ "Contribution",
+ {
+ personId: DataTypes.STRING(100),
+ multiplier: DataTypes.INTEGER,
+ scannerId: DataTypes.STRING(100),
+ taskId: DataTypes.INTEGER
+ },
+ {
+ tableName: "Contributions"
+ }
+ );
+ Contribution.associate = function(models) {
+ Contribution.belongsTo(models.Person, {
+ foreignKey: "personId",
+ targetKey: "identityId"
+ });
+ Contribution.belongsTo(models.Task, { foreignKey: "taskId" });
+ Contribution.addScope("defaultScope", {
+ include: [{ model: models.Task }]
+ });
+ };
+ return Contribution;
+};
+
+export default contribution;
diff --git a/pages/api/models/event.ts b/pages/api/models/event.ts
new file mode 100644
index 00000000..affacbc1
--- /dev/null
+++ b/pages/api/models/event.ts
@@ -0,0 +1,17 @@
+const event = (sequelize, DataTypes) => {
+ const Event = sequelize.define(
+ "Event",
+ {
+ name: DataTypes.STRING(100),
+ description: DataTypes.STRING(500),
+ startsAt: DataTypes.DATE,
+ endsAt: DataTypes.DATE
+ },
+ {}
+ );
+
+ Event.associate = models => {};
+ return Event;
+};
+
+export default event;
diff --git a/pages/api/models/groupings.ts b/pages/api/models/groupings.ts
new file mode 100644
index 00000000..b6121fc0
--- /dev/null
+++ b/pages/api/models/groupings.ts
@@ -0,0 +1,16 @@
+const groupings = (sequelize, DataTypes) => {
+ const Grouping = sequelize.define(
+ "Grouping",
+ {
+ name: DataTypes.STRING(100)
+ },
+ { tableName: "Groupings" }
+ );
+ Grouping.associate = models => {
+ models.Task.belongsTo(models.Grouping, { foreignKey: "groupingId" });
+ Grouping.hasMany(models.Task, { foreignKey: "groupingId" });
+ };
+ return Grouping;
+};
+
+export default groupings;
diff --git a/pages/api/models/hackerProfile.ts b/pages/api/models/hackerProfile.ts
new file mode 100644
index 00000000..7b8e75ca
--- /dev/null
+++ b/pages/api/models/hackerProfile.ts
@@ -0,0 +1,150 @@
+// If you make any changes to HackerProfile, make sure you do the following:
+// 1) Generate a Sequelize migration that adds/removes columns as needed
+// 2) Update the Profile type definition in odyssey.d.ts
+const profile = (sequelize, DataTypes) => {
+ const HackerProfile = sequelize.define(
+ "HackerProfile",
+ {
+ id: {
+ type: DataTypes.STRING,
+ defaultValue: "hacker_id",
+ allowNull: false
+ },
+ gender: {
+ type: DataTypes.ENUM,
+ values: ["male", "female", "non-binary", "other", "no-say"]
+ },
+ userId: {
+ type: DataTypes.STRING,
+ primaryKey: true,
+ allowNull: false
+ },
+ travelStatus: {
+ type: DataTypes.ENUM,
+ values: [
+ "ineligible",
+ "needed",
+ "unneeded",
+ "declined",
+ "unknown",
+ "submitted",
+ "reimbursed"
+ ]
+ },
+ ethnicity: DataTypes.STRING,
+ email: DataTypes.STRING,
+ major: DataTypes.STRING,
+ minor: DataTypes.STRING,
+ resume: DataTypes.STRING,
+ skills: DataTypes.STRING,
+ interests: DataTypes.STRING,
+ submittedAt: DataTypes.DATE,
+ status: {
+ type: DataTypes.ENUM,
+ values: [
+ "unverified",
+ "verified",
+ "submitted",
+ "accepted",
+ "waitlisted",
+ "rejected",
+ "confirmed",
+ "declined",
+ "checkedIn"
+ ],
+ defaultValue: "unverified",
+ allowNull: false
+ },
+ firstName: DataTypes.STRING,
+ lastName: DataTypes.STRING,
+ phoneNumber: DataTypes.STRING,
+ school: DataTypes.STRING,
+ year: {
+ type: DataTypes.ENUM,
+ values: ["freshman", "sophomore", "junior", "senior", "graduate"]
+ },
+ skillLevel: {
+ type: DataTypes.ENUM,
+ values: ["beginner", "intermediate", "advanced"]
+ },
+ questionOne: DataTypes.STRING(1000),
+ questionTwo: DataTypes.STRING(1000),
+ questionThree: DataTypes.STRING(1000),
+ role: {
+ type: DataTypes.ENUM,
+ values: ["hacker", "admin", "sponsor", "volunteer"],
+ defaultValue: "hacker"
+ },
+ graduationDate: {
+ type: DataTypes.ENUM,
+ values: [
+ "spring-2020",
+ "fall-2020",
+ "spring-2021",
+ "fall-2021",
+ "spring-2022",
+ "fall-2022",
+ "spring-2023",
+ "fall-2023",
+ "other"
+ ]
+ },
+ over18: DataTypes.BOOLEAN,
+ needBus: DataTypes.BOOLEAN,
+ links: DataTypes.STRING(1000),
+ codeOfConduct: DataTypes.BOOLEAN,
+ authorize: DataTypes.BOOLEAN,
+ marketing: DataTypes.STRING(100),
+ promoCode: {
+ type: DataTypes.STRING(100),
+ defaultValue: function() {
+ //Random string of length 8
+ return (Math.random().toString(36) + "00000000000000000").slice(
+ 2,
+ 10
+ );
+ }
+ },
+ referrerCode: DataTypes.STRING(100),
+ referred: DataTypes.VIRTUAL,
+ travelOrigin: DataTypes.STRING(500),
+ travelMethod: {
+ type: DataTypes.ENUM,
+ values: ["driving", "bus", "flying", "usc", "other"]
+ },
+ shirtSize: {
+ type: DataTypes.ENUM,
+ values: ["xs", "s", "m", "l", "xl"]
+ },
+ travelPlan: DataTypes.STRING(500),
+ dietaryRestrictions: DataTypes.STRING(1000),
+ confirmCodeOfConduct: DataTypes.BOOLEAN,
+ noBusCheck: DataTypes.BOOLEAN,
+ confirmedAt: DataTypes.DATE,
+ declinedAt: DataTypes.DATE,
+ qrCodeId: {
+ type: DataTypes.STRING,
+ unique: true
+ }
+ },
+ {}
+ );
+
+ HackerProfile.prototype.getReferred = () => {
+ return sequelize.models.HackerProfile.findAll({
+ where: {
+ referrerCode: this.promoCode
+ }
+ });
+ };
+ HackerProfile.associate = models => {
+ HackerProfile.belongsTo(models.Team, {
+ as: "team",
+ foreignKey: "teamId",
+ constraints: false
+ });
+ };
+ return HackerProfile;
+};
+
+export default profile;
diff --git a/pages/api/models/hackerReview.ts b/pages/api/models/hackerReview.ts
new file mode 100644
index 00000000..0086f918
--- /dev/null
+++ b/pages/api/models/hackerReview.ts
@@ -0,0 +1,24 @@
+const review = (sequelize, DataTypes) => {
+ const HackerReview = sequelize.define(
+ "HackerReview",
+ {
+ hackerId: DataTypes.STRING(100),
+ createdBy: DataTypes.STRING(100),
+ scoreOne: DataTypes.INTEGER,
+ scoreTwo: DataTypes.INTEGER,
+ scoreThree: DataTypes.INTEGER,
+ comments: DataTypes.STRING
+ },
+ {}
+ );
+ HackerReview.associate = models => {
+ // associations can be defined here
+ models.HackerProfile.hasMany(HackerReview, { foreignKey: "hackerId" });
+ models.HackerReview.belongsTo(models.HackerProfile, {
+ foreignKey: "createdBy"
+ });
+ };
+ return HackerReview;
+};
+
+export default review;
diff --git a/pages/api/models/house.ts b/pages/api/models/house.ts
new file mode 100644
index 00000000..3cf3dffa
--- /dev/null
+++ b/pages/api/models/house.ts
@@ -0,0 +1,21 @@
+const house = (sequelize, DataTypes) => {
+ const House = sequelize.define(
+ "House",
+ {
+ name: DataTypes.STRING(100),
+ color: DataTypes.STRING(100)
+ },
+ { tableName: "Houses" }
+ );
+ House.associate = models => {
+ House.hasMany(models.Person, {
+ foreignKey: "houseId",
+ as: "HouseMembers",
+ constraints: false
+ });
+ };
+
+ return House;
+};
+
+export default house;
diff --git a/pages/api/models/index.ts b/pages/api/models/index.ts
new file mode 100644
index 00000000..b8632ce5
--- /dev/null
+++ b/pages/api/models/index.ts
@@ -0,0 +1,34 @@
+const Sequelize = require("sequelize");
+const env = process.env.NODE_ENV || "test";
+
+import config from "../config";
+import { DBType } from './types';
+
+let env_config = config[env];
+
+let db: DBType = {
+ sequelize: null,
+ Sequelize: null
+};
+
+let sequelize = new Sequelize(
+ env_config.database,
+ env_config.username,
+ env_config.password,
+ env_config
+);
+
+let model;
+model = sequelize["import"]("./action");
+db[model.name] = model;
+
+Object.keys(db).forEach(modelName => {
+ if (db[modelName].associate) {
+ db[modelName].associate(db);
+ }
+});
+
+db.sequelize = sequelize;
+db.Sequelize = Sequelize;
+
+export default db;
diff --git a/pages/api/models/multiplier.ts b/pages/api/models/multiplier.ts
new file mode 100644
index 00000000..b284f5c2
--- /dev/null
+++ b/pages/api/models/multiplier.ts
@@ -0,0 +1,18 @@
+const multiplier = (sequelize, DataTypes) => {
+ const Multiplier = sequelize.define(
+ "Multiplier",
+ {
+ name: DataTypes.STRING,
+ multiplierValue: DataTypes.INTEGER,
+ groupingId: DataTypes.INTEGER
+ },
+ { tableName: "Multipliers" }
+ );
+ Multiplier.associate = function(models) {
+ Multiplier.belongsTo(models.Grouping);
+ models.Grouping.hasMany(Multiplier, { foreignKey: "groupingId" });
+ };
+ return Multiplier;
+};
+
+export default multiplier;
diff --git a/pages/api/models/person.ts b/pages/api/models/person.ts
new file mode 100644
index 00000000..fc161703
--- /dev/null
+++ b/pages/api/models/person.ts
@@ -0,0 +1,49 @@
+const person = (sequelize, DataTypes) => {
+ const Person = sequelize.define(
+ "Person",
+ {
+ identityId: {
+ type: DataTypes.STRING(100),
+ primaryKey: true
+ },
+ isBattlepassComplete: DataTypes.BOOLEAN,
+ ProjectTeamId: DataTypes.NUMBER
+ },
+ {
+ tableName: "persons",
+ defaultScope: {
+ include: [
+ { model: sequelize.models.HackerProfile, as: "Profile" },
+ { model: sequelize.models.Contribution, as: "Contributions" },
+ { model: sequelize.models.House, as: "Home" }
+ ]
+ }
+ }
+ );
+
+ Person.associate = models => {
+ Person.belongsTo(models.House, {
+ foreignKey: "houseId",
+ as: "Home",
+ constraints: false
+ });
+ Person.belongsTo(models.ProjectTeam);
+ Person.belongsTo(models.HackerProfile, {
+ foreignKey: "identityId",
+ targetKey: "userId",
+ constraints: false,
+ as: "Profile"
+ });
+ Person.hasMany(models.Contribution, { foreignKey: "personId" });
+ Person.addScope("hideProfile", {
+ include: [
+ { model: sequelize.models.Contribution, as: "Contributions" },
+ { model: sequelize.models.House, as: "Home" }
+ ]
+ });
+ };
+
+ return Person;
+};
+
+export default person;
diff --git a/pages/api/models/prize.ts b/pages/api/models/prize.ts
new file mode 100644
index 00000000..bb1bae95
--- /dev/null
+++ b/pages/api/models/prize.ts
@@ -0,0 +1,21 @@
+const prize = (sequelize, DataTypes) => {
+ const Prize = sequelize.define(
+ "Prize",
+ {
+ title: DataTypes.STRING,
+ description: DataTypes.STRING
+ },
+ {}
+ );
+ Prize.associate = models => {
+ Prize.belongsToMany(models.ProjectTeam, {
+ through: "ProjectTeamPrizes",
+ foreignKey: "prize",
+ as: "Prizes",
+ otherKey: "projectTeam"
+ });
+ };
+ return Prize;
+};
+
+export default prize;
diff --git a/pages/api/models/projectTeam.ts b/pages/api/models/projectTeam.ts
new file mode 100644
index 00000000..b6a07256
--- /dev/null
+++ b/pages/api/models/projectTeam.ts
@@ -0,0 +1,30 @@
+const team = (sequelize, DataTypes) => {
+ const ProjectTeam = sequelize.define(
+ "ProjectTeam",
+ {
+ name: DataTypes.STRING,
+ devpostLink: DataTypes.STRING,
+ githubLink: DataTypes.STRING
+ },
+ {
+ defaultScope: {
+ include: [
+ { model: sequelize.models.Prize, as: "Prizes" },
+ { model: sequelize.models.Person, as: "Members" }
+ ]
+ }
+ }
+ );
+ ProjectTeam.associate = models => {
+ ProjectTeam.hasMany(models.Person, { as: "Members" });
+ ProjectTeam.belongsToMany(models.Prize, {
+ through: "ProjectTeamPrizes",
+ foreignKey: "projectTeam",
+ as: "Prizes",
+ otherKey: "prize"
+ });
+ };
+ return ProjectTeam;
+};
+
+export default team;
diff --git a/pages/api/models/projectTeamPrize.ts b/pages/api/models/projectTeamPrize.ts
new file mode 100644
index 00000000..9ef6aeeb
--- /dev/null
+++ b/pages/api/models/projectTeamPrize.ts
@@ -0,0 +1,9 @@
+const teamPrize = (sequelize, DataTypes) => {
+ const ProjectTeamPrizes = sequelize.define("ProjectTeamPrizes", {
+ ProjectTeam: DataTypes.NUMBER,
+ Prize: DataTypes.NUMBER
+ });
+ return ProjectTeamPrizes;
+};
+
+export default teamPrize;
diff --git a/pages/api/models/task.ts b/pages/api/models/task.ts
new file mode 100644
index 00000000..a0af6390
--- /dev/null
+++ b/pages/api/models/task.ts
@@ -0,0 +1,25 @@
+const task = (sequelize, DataTypes) => {
+ const Task = sequelize.define(
+ "Task",
+ {
+ points: DataTypes.INTEGER,
+ description: DataTypes.STRING(100),
+ blocking: DataTypes.BOOLEAN,
+ type: DataTypes.STRING(100),
+ isGroupTask: DataTypes.BOOLEAN,
+ isActive: DataTypes.BOOLEAN,
+ sponsor: DataTypes.STRING,
+ isPast: DataTypes.BOOLEAN,
+ name: DataTypes.STRING(100)
+ },
+ {}
+ );
+ Task.associate = models => {
+ Task.hasMany(models.Contribution, { foreignKey: "taskId" });
+ // associations can be defined here
+ };
+
+ return Task;
+};
+
+export default task;
diff --git a/pages/api/models/team.ts b/pages/api/models/team.ts
new file mode 100644
index 00000000..5d1cfa38
--- /dev/null
+++ b/pages/api/models/team.ts
@@ -0,0 +1,34 @@
+const team = (sequelize, DataTypes) => {
+ const Team = sequelize.define(
+ "Team",
+ {
+ name: DataTypes.STRING(150),
+ teamCode: {
+ type: DataTypes.STRING(4),
+ unique: true
+ },
+ ownerId: {
+ type: DataTypes.STRING,
+ references: {
+ model: "HackerProfiles",
+ key: "userId"
+ }
+ }
+ },
+ {}
+ );
+ Team.associate = function(models) {
+ Team.belongsTo(models.HackerProfile, {
+ as: "owner",
+ foreignKey: "ownerId",
+ constraints: false
+ });
+ Team.hasMany(models.HackerProfile, {
+ foreignKey: "teamId",
+ constraints: false
+ });
+ };
+ return Team;
+};
+
+export default team;
diff --git a/pages/api/models/types/db.ts b/pages/api/models/types/db.ts
new file mode 100644
index 00000000..d6dff63e
--- /dev/null
+++ b/pages/api/models/types/db.ts
@@ -0,0 +1,7 @@
+export default interface DBType {
+ sequelize?: any;
+ Sequelize?: any;
+ HackerProfile?: any;
+}
+
+module.exports = {}
\ No newline at end of file
diff --git a/pages/api/models/types/index.ts b/pages/api/models/types/index.ts
new file mode 100644
index 00000000..37799355
--- /dev/null
+++ b/pages/api/models/types/index.ts
@@ -0,0 +1 @@
+export type { default as DBType } from './db';
diff --git a/pages/api/models/unlockable.ts b/pages/api/models/unlockable.ts
new file mode 100644
index 00000000..3c7cea7f
--- /dev/null
+++ b/pages/api/models/unlockable.ts
@@ -0,0 +1,15 @@
+const unlockable = (sequelize, DataTypes) => {
+ const Unlockable = sequelize.define(
+ "Unlockable",
+ {
+ tier: DataTypes.INTEGER,
+ pointThreshold: DataTypes.INTEGER,
+ isPremium: DataTypes.BOOLEAN
+ },
+ {}
+ );
+ Unlockable.associate = models => {};
+ return Unlockable;
+};
+
+export default unlockable;
diff --git a/pages/api/people.ts b/pages/api/people.ts
new file mode 100644
index 00000000..74368136
--- /dev/null
+++ b/pages/api/people.ts
@@ -0,0 +1,60 @@
+const express = require("express");
+const models = require("./models");
+const utils = require("./utils");
+const router = express.Router();
+const sequelize = require("sequelize");
+const Sentry = require("@sentry/node");
+
+router.use(utils.authMiddleware);
+
+router.get("/self", async (req, res) => {
+ try {
+ const [person, isCreated] = await models.Person.findOrCreate({
+ where: {
+ identityId: req.user.id
+ },
+ defaults: { isBattlepassComplete: false }
+ });
+
+ return res.json({ person });
+ } catch (e) {
+ return res.status(500).json({ err: e.message });
+ }
+});
+
+router.get("/houses", async (req, res) => {
+ try {
+ const houses = await models.House.findAll();
+ return res.json({ houses: houses });
+ } catch (e) {
+ return res.status(400).json({ err: e.message });
+ }
+});
+
+router.post("/houses", async (req, res) => {
+ try {
+ const postValues = req.body;
+ // @ts-ignore
+ const { name, color } = { ...postValues };
+ const house = await models.House.create({ name: name, color: color });
+ return res.json({ newHouse: house });
+ } catch (e) {
+ return res.status(400).json({ err: e.message });
+ }
+});
+
+router.put("/houses", async (req, res) => {
+ try {
+ const values = req.body;
+ const houseId = values.id;
+ const result = await models.House.update(
+ { ...values },
+ { where: { id: houseId } }
+ );
+ return res.json({ result: result });
+ } catch (e) {
+ return res.status(400).json({ err: e.message });
+ }
+});
+
+export { router };
diff --git a/pages/api/prizes.ts b/pages/api/prizes.ts
new file mode 100644
index 00000000..8a68252f
--- /dev/null
+++ b/pages/api/prizes.ts
@@ -0,0 +1,13 @@
+const express = require("express");
+const models = require("./models");
+const router = express.Router();
+const utils = require("./utils");
+
+router.use(utils.authMiddleware);
+
+router.get("/", async (req, res) => {
+ const prizes = await models.Prize.findAll();
+ return res.json({ success: prizes });
+});
+
+export { router };
diff --git a/pages/api/projectTeam.ts b/pages/api/projectTeam.ts
new file mode 100644
index 00000000..c28926a7
--- /dev/null
+++ b/pages/api/projectTeam.ts
@@ -0,0 +1,148 @@
+const express = require("express");
+const models = require("./models");
+const utils = require("./utils");
+const router = express.Router();
+
+router.use(utils.authMiddleware);
+router.use(utils.preprocessRequest);
+
+const getProjectTeamForSelf = async req => {
+ const { id } = req.user;
+
+ const person = await models.Person.findByPk(id, {
+ include: [{ model: models.ProjectTeam, required: false }]
+ });
+ if (person) {
+ return person.ProjectTeam;
+ }
+ return person;
+};
+
+const getPersonForQRID = async qrCodeId => {
+ const hackerProfile = await models.HackerProfile.findOne({
+ where: {
+ qrCodeId: qrCodeId
+ }
+ });
+ const userId = hackerProfile.get("userId");
+ const person = await models.Person.findByPk(userId);
+ return person;
+};
+
+router.get("/self", async (req, res) => {
+ const ProjectTeam = await getProjectTeamForSelf(req);
+ return res.json({ success: ProjectTeam });
+});
+
+router.put("/join/:name", async (req, res) => {
+ const { name } = req.params;
+ const projectTeam = await models.ProjectTeam.findOne({
+ where: { name }
+ });
+
+ if (!projectTeam) {
+ return res.status(400).json({
+ error: `Team: ${name} not found`
+ });
+ }
+
+ if (projectTeam.Members.length >= 4) {
+ return res.status(400).json({ error: "Team is full" });
+ }
+ await projectTeam.addMember(req.user.id);
+ await projectTeam.reload();
+
+ return res.json({ success: projectTeam });
+});
+
+router.get("/list", async (req, res) => {
+ const projectTeams = await models.ProjectTeam.findAll();
+ return res.json({ success: projectTeams });
+});
+
+router.get("/:id", async (req, res) => {
+ const { id } = req.params.id;
+ const projectTeam = await models.ProjectTeam.findByPk(id);
+ return res.json({ success: projectTeam });
+});
+
+router.post("/self", async (req, res) => {
+ const userId = req.user.id;
+ const { body } = req;
+
+ const person = await models.Person.findByPk(userId);
+
+ if (person.teamId) {
+ return res.status(400).json({ error: "Already has a team" });
+ }
+
+ const result = await models.sequelize.transaction(async t => {
+ const projectTeam = await models.ProjectTeam.create(body, {
+ transaction: t
+ });
+
+ await person.setProjectTeam(projectTeam, { transaction: t });
+ });
+
+ const projectTeam = await getProjectTeamForSelf(req);
+
+ return res.json({ success: projectTeam });
+});
+
+router.put("/self", async (req, res) => {
+ const { body } = req;
+
+ const keys = ["devpostLink", "githubLink", "name"];
+
+ const updateObject = keys.reduce((obj, key) => {
+ obj[key] = body[key];
+ return obj;
+ }, {});
+
+ const projectTeam = await getProjectTeamForSelf(req);
+ await projectTeam.update(updateObject);
+ await projectTeam.reload();
+ return res.json({ success: projectTeam });
+});
+
+router.post("/self/addPrize", async (req, res) => {
+ const { id } = req.body;
+ const projectTeam = await getProjectTeamForSelf(req);
+ await projectTeam.addPrize(id);
+ await projectTeam.reload();
+ return res.json({ success: projectTeam });
+});
+
+router.put("/self/addMember", async (req, res) => {
+ const { memberId } = req.body;
+ const projectTeam = await getProjectTeamForSelf(req);
+ const newTeammate = await getPersonForQRID(memberId);
+ projectTeam.addMember(newTeammate);
+ await projectTeam.save();
+
+ return res.json({ success: projectTeam });
+});
+
+router.delete("/self/deletePrize/:prizeID", async (req, res) => {
+ const { prizeID } = req.params;
+ const projectTeam = await getProjectTeamForSelf(req);
+ await projectTeam.removePrize(prizeID);
+ await projectTeam.reload();
+ return res.json({ success: projectTeam });
+});
+
+router.delete("/self/deleteMember/:memberId", async (req, res) => {
+ const { memberId } = req.params;
+ const projectTeam = await getProjectTeamForSelf(req);
+
+ await projectTeam.removeMember(memberId);
+
+ //If you remove yourself
+ if (req.user.id == memberId) {
+ return res.json({ success: null });
+ }
+ await projectTeam.reload();
+ return res.json({ success: projectTeam });
+});
+
+export { router };
diff --git a/pages/api/public.ts b/pages/api/public.ts
new file mode 100644
index 00000000..e7f30c31
--- /dev/null
+++ b/pages/api/public.ts
@@ -0,0 +1,15 @@
+const express = require("express");
+const cors = require("cors");
+const models = require("./models");
+const router = express.Router();
+
+router.get("/events/list", cors(), async (req, res) => {
+ try {
+ const events = await models.Event.findAll();
+ return res.json({ events });
+ } catch (e) {
+ return res.json({ err: e });
+ }
+});
+
+export { router };
diff --git a/pages/api/tasks.ts b/pages/api/tasks.ts
new file mode 100644
index 00000000..3375074e
--- /dev/null
+++ b/pages/api/tasks.ts
@@ -0,0 +1,88 @@
+const express = require("express");
+const models = require("./models");
+const utils = require("./utils");
+const router = express.Router();
+const sequelize = require("sequelize");
+const Sentry = require("@sentry/node");
+
+/* This router supports superuser routes, such as updating the status of users */
+
+router.use(utils.authMiddleware);
+
+router.get("/list", utils.requireNonHacker, async (req, res) => {
+ const Op = sequelize.Op
+ let filter = []
+
+ if (req.user.role && req.user.role === "admin") {
+ filter.push({
+ type: "admin"
+ })
+ filter.push({
+ type: "volunteer"
+ })
+ filter.push({
+ type: "sponsor"
+ })
+ } else if (req.user.role && req.user.role === "volunteer") {
+ filter.push({
+ type: "volunteer"
+ })
+ filter.push({
+ type: "sponsor"
+ })
+ } else if (req.user.role && req.user.role === "sponsor") {
+ filter.push({
+ type: "sponsor"
+ })
+ }
+
+ const tasks = await models.Task.findAll({
+ where: {
+ [Op.or]: filter
+ }
+ });
+
+ return res.json({ success: tasks });
+});
+
+router.post("/create", utils.requireAdmin, async (req, res) => {
+ const allowedFields = new Set([
+ "blocking",
+ "description",
+ "points",
+ "name",
+ "isGroupTask",
+ "isActive"
+ ]);
+ const formInput = req.body;
+
+ for (let key of Object.keys(formInput)) {
+ if (!allowedFields.has(key)) {
+ return res.status(400).json({
+ error: `${key} is not a supported field`
+ });
+ }
+ }
+ const result = await models.Task.create(req.body);
+ return res.status(200).json({ success: result });
+});
+
+router.put("/:id", utils.requireAdmin, async (req, res) => {
+ const updatedObj = req.body;
+ const taskId = updatedObj.id;
+ delete updatedObj.id;
+ await models.Task.update({ ...updatedObj }, { where: { id: taskId } });
+ return res.status(200);
+});
+
+router.delete("/:id", utils.requireAdmin, async (req, res) => {
+ const id = req.params.id;
+ await models.Task.destroy({
+ where: {
+ id: id
+ }
+ });
+ return res.json({ success: null });
+});
+
+export { router };
diff --git a/pages/api/team.ts b/pages/api/team.ts
new file mode 100644
index 00000000..481fc77c
--- /dev/null
+++ b/pages/api/team.ts
@@ -0,0 +1,192 @@
+const express = require("express");
+const models = require("./models");
+const utils = require("./utils");
+const router = express.Router();
+
+router.use(utils.authMiddleware);
+router.use(utils.preprocessRequest);
+
+// GET /api/team
+// - If a hacker is on a team, get that team info
+router.get("/", async (req, res) => {
+ const hackerProfile = await models.HackerProfile.findOne({
+ where: { userId: req.user.id }
+ });
+
+ const team = await hackerProfile.getTeam({
+ include: [
+ {
+ model: models.HackerProfile,
+ attributes: ["firstName", "lastName", "status", "email", "userId"]
+ }
+ ]
+ });
+
+ if (team) {
+ return res.json({ team });
+ } else {
+ return res.json({
+ message: "User does not currently belong to a team"
+ });
+ }
+});
+
+// GET /api/team/:code
+// - If provided a team code, retrieve it
+router.get("/:code", async (req, res) => {
+ // Try to find a team with the provided code
+ const team = await models.Team.findOne({
+ where: { teamCode: req.params.code || "" }
+ });
+ if (!team) {
+ return res
+ .status(400)
+ .json({ message: "Could not find a team with that code" });
+ } else {
+ return res.json({ team });
+ }
+});
+
+// POST /api/team
+// - If a hacker is not on a team, create a team
+router.post("/", async (req, res) => {
+ const hackerProfile = await models.HackerProfile.findOne({
+ where: { userId: req.user.id }
+ });
+
+ let team = await hackerProfile.getTeam();
+ if (team) {
+ return res.status(500).json({ message: "User already belongs on a team" });
+ }
+
+ let generatedCode = Math.random()
+ .toString(36)
+ .slice(4, 8)
+ .toUpperCase();
+
+ while (
+ await models.Team.findOne({
+ where: { teamCode: generatedCode }
+ })
+ ) {
+ // Regenerate code
+ generatedCode = Math.random()
+ .toString(36)
+ .slice(4, 8)
+ .toUpperCase();
+ }
+
+ team = await models.Team.create({
+ name: req.body.name,
+ teamCode: generatedCode,
+ ownerId: req.user.id
+ });
+
+ await hackerProfile.update({
+ teamId: team.id
+ });
+
+ return res.json({
+ team
+ });
+});
+
+// DELETE /api/team
+// - Attempts to delete a user's current team
+router.delete("/", async (req, res) => {
+ const hackerProfile = await models.HackerProfile.findOne({
+ where: { userId: req.user.id }
+ });
+
+ // Can't join a team if you're already on one!
+ let team = await hackerProfile.getTeam();
+ if (!team) {
+ return res.status(400).json({ message: "User does not belong on a team" });
+ }
+
+ if (team.ownerId === req.user.id) {
+ // Allow deletion
+ await models.HackerProfile.update(
+ { teamId: null },
+ { where: { teamId: team.id } }
+ );
+ await team.destroy();
+ return res.status(200).json({ message: "Team successfully deleted" });
+ } else {
+ return res
+ .status(400)
+ .json({ message: "You cannot delete a team you don't own" });
+ }
+});
+
+// POST /api/team/join/:code
+// - If a hacker is not on a team, attempt to join a team
+router.post("/join/:code", async (req, res) => {
+ const hackerProfile = await models.HackerProfile.findOne({
+ where: { userId: req.user.id }
+ });
+
+ // Can't join a team if you're already on one!
+ let team = await hackerProfile.getTeam();
+ if (team) {
+ return res.status(400).json({ message: "User already belongs on a team" });
+ }
+
+ // Try to find a team with the provided code
+ team = await models.Team.findOne({
+ where: { teamCode: req.params.code || "" }
+ });
+ if (!team) {
+ return res
+ .status(400)
+ .json({ message: "Could not find a team with that code" });
+ }
+
+ // See if there is still space in the team
+ const teamMembers = await team.getHackerProfiles();
+
+ if (teamMembers.length + 1 > 4) {
+ return res.status(400).json({ message: "This team is full!" });
+ }
+
+ // If we're still here, we can join the team :)
+ await hackerProfile.setTeam(team);
+
+ return res.status(200).json({
+ message: "Successfully joined team"
+ });
+});
+
+// POST /api/team/leave
+// - If a hacker is on a team, attempt to leave that team
+router.post("/leave", async (req, res) => {
+ const hackerProfile = await models.HackerProfile.findOne({
+ where: { userId: req.user.id }
+ });
+
+ // Can't leave a team if you're not in one!
+ let team = await hackerProfile.getTeam();
+ if (!team) {
+ return res
+ .status(400)
+ .json({ message: "User does not currently belong to a team" });
+ }
+
+ // Can't leave a team if you own it
+ if (team.ownerId === req.user.id) {
+ return res
+ .status(400)
+ .json({ message: "You cannot leave this team, you created it" });
+ }
+
+ // If we're still here, we can leave the team :)
+ await hackerProfile.update({
+ teamId: null
+ });
+
+ return res.status(200).json({
+ message: "Successfully left team"
+ });
+});
+
+export { router };
diff --git a/pages/api/unlockable.ts b/pages/api/unlockable.ts
new file mode 100644
index 00000000..e824a174
--- /dev/null
+++ b/pages/api/unlockable.ts
@@ -0,0 +1,61 @@
+const express = require("express");
+const models = require("./models");
+const utils = require("./utils");
+const router = express.Router();
+const sequelize = require("sequelize");
+const Sentry = require("@sentry/node");
+
+/* This router supports superuser routes, such as updating the status of users */
+
+router.use(utils.authMiddleware);
+router.use(utils.requireAdmin);
+
+router.get("/list", async (req, res) => {
+ try {
+ const unlockables = await models.Unlockable.findAll();
+ return res.json({ unlockables });
+ } catch (e) {
+ return res.json({ err: e.message });
+ }
+});
+
+router.post("/", async (req, res) => {
+ try {
+ const unlockable = await models.Unlockable.create({
+ tier: parseInt(req.body.tier),
+ isPremium: req.body.isPremium,
+ pointThreshold: parseInt(req.body.pointThreshold)
+ });
+ return res.json({ unlockable });
+ } catch (e) {
+ return res.status(400).json({ err: e.message });
+ }
+});
+
+router.put("/", async (req, res) => {
+ try {
+ const unlockableId = req.body.id;
+ delete req.body.id;
+ const unlockable = await models.Unlockable.update(
+ {
+ ...req.body
+ },
+ { where: { id: unlockableId } }
+ );
+ return res.json({ unlockable });
+ } catch (e) {
+ return res.json({ err: e.message });
+ }
+});
+
+router.delete("/:id", async (req, res) => {
+ try {
+ const unlockableId = req.params.id;
+ await models.Unlockable.destroy({ where: { id: unlockableId } });
+ return res.status(200);
+ } catch (e) {
+ return res.json({ err: e.message });
+ }
+});
+
+export { router };
diff --git a/pages/api/users.js b/pages/api/users.ts
similarity index 94%
rename from pages/api/users.js
rename to pages/api/users.ts
index 7e535774..d5705635 100644
--- a/pages/api/users.js
+++ b/pages/api/users.ts
@@ -13,3 +13,4 @@ var secured = function(req, res, next) {
res.redirect("/login");
};
+export { secured };
\ No newline at end of file
diff --git a/pages/api/utils.ts b/pages/api/utils.ts
new file mode 100644
index 00000000..e838adfb
--- /dev/null
+++ b/pages/api/utils.ts
@@ -0,0 +1,132 @@
+const models = require("./models");
+const Sentry = require("@sentry/node");
+
+const authMiddleware = function(req, res, next) {
+ if (req.user) {
+ return next();
+ }
+ res.status(400).send("Unauthorized");
+}
+
+const preprocessRequest = function(req, res, next) {
+ delete req.body.status;
+ delete req.body.userId;
+ delete req.body.email;
+ delete req.body.role;
+ return next();
+};
+
+const requireAdmin = function(req, res, next) {
+ /* Read user from database, and make sure it is in fact an admin */
+ try {
+ models.HackerProfile.findAll({
+ where: {
+ userId: req.user.id
+ }
+ }).then(hackerProfiles => {
+ if (hackerProfiles.length >= 1) {
+ if (hackerProfiles[0].get("role") === "admin") {
+ return next();
+ } else {
+ res.status(400).send("Unauthorized: Incorrect role");
+ }
+ } else {
+ res.status(400).send("Unauthorized; profile not found");
+ }
+ });
+ } catch (e) {
+ Sentry.captureException(e);
+ res.status(400).send("Unauthorized");
+ }
+};
+
+const requireVolunteer = function(req, res, next) {
+ /* Read user from database, and make sure it is in fact an volunteer */
+ try {
+ models.HackerProfile.findAll({
+ where: {
+ userId: req.user.id
+ }
+ }).then(hackerProfiles => {
+ if (hackerProfiles.length >= 1) {
+ if (hackerProfiles[0].get("role") === "volunteer") {
+ return next();
+ } else {
+ res.status(400).send("Unauthorized: Incorrect role");
+ }
+ } else {
+ res.status(400).send("Unauthorized; profile not found");
+ }
+ });
+ } catch (e) {
+ Sentry.captureException(e);
+ res.status(400).send("Unauthorized");
+ }
+};
+
+const requireSponsor = function(req, res, next) {
+ /* Read user from database, and make sure it is in fact an sponsor */
+ try {
+ models.HackerProfile.findAll({
+ where: {
+ userId: req.user.id
+ }
+ }).then(hackerProfiles => {
+ if (hackerProfiles.length >= 1) {
+ if (hackerProfiles[0].get("role") === "sponsor") {
+ return next();
+ } else {
+ res.status(400).send("Unauthorized: Incorrect role");
+ }
+ } else {
+ res.status(400).send("Unauthorized; profile not found");
+ }
+ });
+ } catch (e) {
+ Sentry.captureException(e);
+ res.status(400).send("Unauthorized");
+ }
+};
+
+const requireNonHacker = function(req, res, next) {
+ /* Read user from database, and make sure it is in fact not a hacker */
+ try {
+ models.HackerProfile.findAll({
+ where: {
+ userId: req.user.id
+ }
+ }).then(hackerProfiles => {
+ if (hackerProfiles.length >= 1) {
+ if (hackerProfiles[0].get("role") !== "hacker") {
+ // Add role to be used
+ req.user.role = hackerProfiles[0].get("role");
+ return next();
+ } else {
+ res.status(400).send("Unauthorized: Incorrect role");
+ }
+ } else {
+ res.status(400).send("Unauthorized; profile not found");
+ }
+ });
+ } catch (e) {
+ Sentry.captureException(e);
+ res.status(400).send("Unauthorized");
+ }
+};
+
+const requireDevelopmentEnv = function(req, res, next) {
+ if (process.env.NODE_ENV == "production") {
+ return res.redirect("/");
+ }
+ return next();
+};
+
+export {
+ authMiddleware,
+ preprocessRequest,
+ requireAdmin,
+ requireVolunteer,
+ requireSponsor,
+ requireNonHacker,
+ requireDevelopmentEnv
+};
diff --git a/server.js b/server.js
index c54ab576..f796ce56 100644
--- a/server.js
+++ b/server.js
@@ -22,7 +22,7 @@ if (!dev) {
}
Sentry.init({
- dsn: "https://1a18ac7b9aa94cb5b2a8c9fc2f7e4fc8@sentry.io/1801129",
+ dsn: process.env.SENTRY_DSN,
environment: dev ? "dev" : process.env.NODE_ENV,
release: "odyssey@" + process.env.npm_package_version
});
diff --git a/yarn.lock b/yarn.lock
index 972969b0..eef8c9dc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1925,6 +1925,13 @@
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
+"@types/mysql@^2.15.6":
+ version "2.15.15"
+ resolved "https://registry.yarnpkg.com/@types/mysql/-/mysql-2.15.15.tgz#af2223d2841091a5a819eabee6dff19567f1cf1f"
+ integrity sha512-1GJnq7RwuFPRicMHdT53vza5v39nep9OKIbozxNUpFXP04CydcdWrqpZQ+MlVdlLFCisWnnt09xughajjWpFsw==
+ dependencies:
+ "@types/node" "*"
+
"@types/node@*":
version "14.6.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.6.0.tgz#7d4411bf5157339337d7cff864d9ff45f177b499"
@@ -7400,6 +7407,13 @@ lines-and-columns@^1.1.6:
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
+list-react-files@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/list-react-files/-/list-react-files-0.2.0.tgz#5d5102d42270328e085dfee6b90338bbc4cbe234"
+ integrity sha512-ZbBO+Ch76klfcD+bf/0FCBzCMZ16moT598cUo5yn0PbdQzQp4C11hZiHXAUnm3OZUXfV/ze1mog6wzwH4AGdKQ==
+ dependencies:
+ glob "^7.1.2"
+
listr-silent-renderer@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e"
@@ -8056,7 +8070,7 @@ mysql2@^1.7.0:
seq-queue "^0.0.5"
sqlstring "^2.3.1"
-mysql@^2.17.1:
+mysql@^2.17.1, mysql@^2.18.1:
version "2.18.1"
resolved "https://registry.yarnpkg.com/mysql/-/mysql-2.18.1.tgz#2254143855c5a8c73825e4522baf2ea021766717"
integrity sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==
@@ -10424,6 +10438,15 @@ serve-static@1.14.1:
parseurl "~1.3.3"
send "0.17.1"
+serverless-mysql@^1.5.4:
+ version "1.5.4"
+ resolved "https://registry.yarnpkg.com/serverless-mysql/-/serverless-mysql-1.5.4.tgz#c8562777e42440d6ebed0f96e17278bafef1f2d6"
+ integrity sha512-q7hJh8NivO2g4CcZ7wy3KTctsFpqx/P4zrVJTwsJJoV9v9QouGv0IFLKXW0rkOqbHSQRvth/eTbf7noYBJzPiQ==
+ dependencies:
+ mysql "^2.18.1"
+ optionalDependencies:
+ "@types/mysql" "^2.15.6"
+
set-blocking@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
@@ -10720,6 +10743,11 @@ sprintf-js@~1.0.2:
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
+sql-template-strings@^2.2.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/sql-template-strings/-/sql-template-strings-2.2.2.tgz#3f11508a25addfce217a3042a9d300c3193b96ff"
+ integrity sha1-PxFQiiWt384hejBCqdMAwxk7lv8=
+
sqlstring@2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.1.tgz#475393ff9e91479aea62dcaf0ca3d14983a7fb40"
From 77dc3bb60fe4c8880355e613bc7ff04f89eb65fe Mon Sep 17 00:00:00 2001
From: Andreas Bigger
Date: Thu, 17 Sep 2020 16:45:52 -0700
Subject: [PATCH 016/129] deepscan?
---
components/steps/Status.tsx | 4 +++-
pages/api/models/types/index.ts | 2 +-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/components/steps/Status.tsx b/components/steps/Status.tsx
index 6f464078..1c9c6090 100644
--- a/components/steps/Status.tsx
+++ b/components/steps/Status.tsx
@@ -18,7 +18,9 @@ type Props = {
};
const getStatusLabel = (profile: Profile): string => {
- const { status, submittedAt } = profile;
+ const { status, submittedAt } = profile
+ ? profile
+ : { status: null, submittedAt: null };
if (!status || status === "unverified") {
return "Unverified";
}
diff --git a/pages/api/models/types/index.ts b/pages/api/models/types/index.ts
index 37799355..c7e0ee0e 100644
--- a/pages/api/models/types/index.ts
+++ b/pages/api/models/types/index.ts
@@ -1 +1 @@
-export type { default as DBType } from './db';
+export type { default as DBType } from './db';
\ No newline at end of file
From f4ced10071ce57e1015c72ba604b8993b5b29af6 Mon Sep 17 00:00:00 2001
From: Andreas Bigger
Date: Thu, 17 Sep 2020 18:41:31 -0700
Subject: [PATCH 017/129] test push to main
---
.circleci/config.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.circleci/config.yml b/.circleci/config.yml
index eefe5c1c..a7b309eb 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -42,4 +42,4 @@ workflows:
- E2ETest
build:
jobs:
- - build
\ No newline at end of file
+ - build
From 9d3cb51b94ae321279ad0df2564a7e5cda64d9bb Mon Sep 17 00:00:00 2001
From: Andreas Bigger
Date: Thu, 17 Sep 2020 18:43:41 -0700
Subject: [PATCH 018/129] test push to staging
From 4f4a7fda4011622c4acc4d3980d2904567111214 Mon Sep 17 00:00:00 2001
From: Jason Silberman
Date: Fri, 18 Sep 2020 12:15:06 -0700
Subject: [PATCH 019/129] Update banner to use current year (#374)
---
components/Countdown.tsx | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/components/Countdown.tsx b/components/Countdown.tsx
index e3081317..ddb22451 100644
--- a/components/Countdown.tsx
+++ b/components/Countdown.tsx
@@ -8,7 +8,10 @@ const Countdown = () => {
- Hacking has concluded! Thanks for coming to HackSC 2020
+
+ Hacking has concluded! Thanks for coming to HackSC{" "}
+ {new Date().getFullYear()}
+
From 50eb64d0f6448c592977f5ef777dee8fd7bafd10 Mon Sep 17 00:00:00 2001
From: Andreas Bigger
Date: Fri, 18 Sep 2020 12:18:58 -0700
Subject: [PATCH 020/129] dynamically render date (#377)
---
pages/admin.tsx | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/pages/admin.tsx b/pages/admin.tsx
index 1ece6b36..abd5b7e1 100644
--- a/pages/admin.tsx
+++ b/pages/admin.tsx
@@ -23,7 +23,7 @@ const Admin = ({ profile }) => {
Hello there -- welcome to the admin dashboard. Here you can access
actions to help organize and run HackSC. If you have any questions
or find any errors, hit up the engineers in{" "}
- #2020-engineering
+ #{new Date().getFullYear()}-engineering
@@ -37,10 +37,10 @@ const Admin = ({ profile }) => {
*/}
Check In Hackers
-
+
Manage Roles
-
+
Manage Available Tasks
From 396deddfed747c1dbb6750fc51f708f93f9b95f8 Mon Sep 17 00:00:00 2001
From: Andreas Bigger
Date: Fri, 18 Sep 2020 12:30:07 -0700
Subject: [PATCH 021/129] Allow team owners to kick other members (#375)
* :sparkles: allow team members to kick other people
* Update api/team.js
Co-authored-by: Jason Silberman
* Update team.js
Co-authored-by: Jason Silberman
---
api/team.js | 45 ++++++++++++++++++++++++++++++++++++++++++++
components/Team.tsx | 46 +++++++++++++++++++++++++++++++++++++++++++--
2 files changed, 89 insertions(+), 2 deletions(-)
diff --git a/api/team.js b/api/team.js
index f80e3550..0d263a69 100644
--- a/api/team.js
+++ b/api/team.js
@@ -157,6 +157,51 @@ router.post("/join/:code", async (req, res) => {
});
});
+// POST /api/team/kick/:userid
+// - If a hacker is not on a team, attempt to join a team
+router.post("/kick/:userid", async (req, res) => {
+ const hackerProfile = await models.HackerProfile.findOne({
+ where: { userId: req.user.id }
+ });
+
+ // Can't kick someone else from a team if you are not on a team!
+ let team = await hackerProfile.getTeam();
+ if (!team) {
+ return res.status(400).json({ message: "User does not belong on a team" });
+ }
+
+ if (team.ownerId === req.params.userid) {
+ return res
+ .status(400)
+ .json({
+ message: `Not allowed to kick yourself. Delete the team instead.`
+ });
+ }
+
+ if (team.ownerId === req.user.id) {
+ // Allow kicking
+ const kickProfile = await models.HackerProfile.findOne({
+ where: { userId: req.params.userid }
+ });
+
+ let kicked_team = await hackerProfile.getTeam();
+
+ if (kicked_team.teamCode == team.teamCode) {
+ // In the same team, we can kick
+ await kickProfile.setTeam(null);
+ return res
+ .status(200)
+ .json({ message: `User ${req.params.userid} successfully kicked.` });
+ }
+ }
+
+ return res
+ .status(400)
+ .json({
+ message: `Could not kick member with userid ${req.params.userid}.`
+ });
+});
+
// POST /api/team/leave
// - If a hacker is on a team, attempt to leave that team
router.post("/leave", async (req, res) => {
diff --git a/components/Team.tsx b/components/Team.tsx
index db7313f0..d2d137ea 100644
--- a/components/Team.tsx
+++ b/components/Team.tsx
@@ -52,6 +52,29 @@ const Team = ({ team, profile }: Props) => {
}
}, []);
+ const handleKick = useCallback(async member => {
+ const confirm = window.confirm(
+ `Are you sure you want to kick ${member.firstName} ${member.lastName}, ${member.email}?`
+ );
+
+ if (!confirm) {
+ return;
+ }
+
+ const res = await fetch("/api/team/kick/" + member.userId, {
+ method: "POST"
+ });
+ const data = await res.json();
+
+ if (res.status === 200) {
+ setError(null);
+ await Router.push("/team");
+ window.scrollTo(0, 0);
+ } else {
+ setError(data.message);
+ }
+ }, []);
+
return (
@@ -74,7 +97,7 @@ const Team = ({ team, profile }: Props) => {
{team.HackerProfiles.map((member: any) => (
-
+
{member.firstName && member.lastName
? member.firstName + " " + member.lastName
@@ -84,7 +107,14 @@ const Team = ({ team, profile }: Props) => {
{member.userId === team.ownerId && ", Team Owner"})
{member.status}
-
+
+ {profile.userId === team.ownerId ? (
+ handleKick(member)}>
+ Kick
+
+ ) : (
+ <>>
+ )}
))}
@@ -106,6 +136,16 @@ const Team = ({ team, profile }: Props) => {
);
};
+const PaddedP = styled.p`
+ padding-right: 3rem;
+`;
+
+const KickButton = styled(Button)`
+ margin: auto;
+ min-width: 150px;
+ align-self: end;
+`;
+
const TeamSection = styled.div`
border-radius: 4px;
background-color: ${({ theme }) => theme.colors.white};
@@ -145,6 +185,8 @@ const Members = styled.ul`
`;
const Member = styled.li`
+ display: flex;
+ flex-direction: row;
padding: 16px 0;
border-bottom: 1px solid ${({ theme }) => theme.colors.gray5};
`;
From f9842566ad99fc2638f47e369454044c4b2e3cdd Mon Sep 17 00:00:00 2001
From: Andreas Bigger
Date: Fri, 18 Sep 2020 12:43:04 -0700
Subject: [PATCH 022/129] fix footer HackSC copyright (#372)
* fix footer HackSC copyright
* replace hackers@hacksc.com with team@hacksc.com
* Update components/Footer.tsx
Co-authored-by: Jason Silberman
Co-authored-by: Jason Silberman
---
components/Footer.tsx | 10 +++++-----
components/results/Confirmed.tsx | 3 +--
components/results/Declined.tsx | 2 +-
components/steps/Confirmed.tsx | 2 +-
components/steps/Status.tsx | 4 ++--
5 files changed, 10 insertions(+), 11 deletions(-)
diff --git a/components/Footer.tsx b/components/Footer.tsx
index c3f6994c..c2e4bdc5 100644
--- a/components/Footer.tsx
+++ b/components/Footer.tsx
@@ -16,7 +16,7 @@ const Footer = () => {
-
+
@@ -32,7 +32,7 @@ const Footer = () => {
- HackSC 2019 ©
+ HackSC {(new Date()).getFullYear()} ©
@@ -53,9 +53,9 @@ const Footer = () => {
Contact us
Reach out to us at{" "}
- hackers@hacksc.com or on
- social media if you have any questions, want more information, or
- want to talk about sponsorship!
+ team@hacksc.com or on social
+ media if you have any questions, want more information, or want to
+ talk about sponsorship!
diff --git a/components/results/Confirmed.tsx b/components/results/Confirmed.tsx
index 91a3ca01..ce9f348a 100644
--- a/components/results/Confirmed.tsx
+++ b/components/results/Confirmed.tsx
@@ -19,8 +19,7 @@ const Confirmed: React.FunctionComponent = props => {
2020. We have a lot of exciting things planned and we can't wait for
you to be a part of it. Be on the lookout for future updates and
communications from us. If you have any updates or questions, please
- contact us at{" "}
- hackers@hacksc.com
+ contact us at team@hacksc.com
diff --git a/components/results/Declined.tsx b/components/results/Declined.tsx
index 8eb9751e..fb58c331 100644
--- a/components/results/Declined.tsx
+++ b/components/results/Declined.tsx
@@ -37,7 +37,7 @@ const Declined: React.FunctionComponent = props => {
If you have any additional comments or questions, please let us know
- at hackers@hacksc.com
+ at team@hacksc.com
diff --git a/components/steps/Confirmed.tsx b/components/steps/Confirmed.tsx
index f5885a69..b117c3e8 100644
--- a/components/steps/Confirmed.tsx
+++ b/components/steps/Confirmed.tsx
@@ -14,7 +14,7 @@ type Props = {
// hacking before being checked-in.`;
const TITLE = `Congrats, you're confirmed for HackSC!`;
-const INSTRUCTIONS = `We're excited to have you at HackSC 2020! Be on the look out for future communications from us in the days leading up to the hackathon. If you have any questions or comments, don't hesitate to reach out to us via hackers@hacksc.com or ask in the HackSC 2020 Slack org`;
+const INSTRUCTIONS = `We're excited to have you at HackSC 2020! Be on the look out for future communications from us in the days leading up to the hackathon. If you have any questions or comments, don't hesitate to reach out to us via team@hacksc.com or ask in the HackSC 2020 Slack org`;
const ConfirmedStep: React.FunctionComponent = props => {
const { profile } = props;
diff --git a/components/steps/Status.tsx b/components/steps/Status.tsx
index 1c9c6090..90b4182d 100644
--- a/components/steps/Status.tsx
+++ b/components/steps/Status.tsx
@@ -101,7 +101,7 @@ const StatusStep: React.FunctionComponent = props => {
If you would like to un-decline , please do so on{" "}
the results page. If you have any additional
questions or comments, please let us know at{" "}
- hackers@hacksc.com
+ team@hacksc.com
)}
@@ -110,7 +110,7 @@ const StatusStep: React.FunctionComponent = props => {
We're excited to have you at HackSC 2020! Be on the lookout for
future updates and communications from us. If you have any updates
or questions, please let us know at{" "}
- hackers@hacksc.com
+ team@hacksc.com
)}
From 7411d44016b206cab37854515a5aa15272342596 Mon Sep 17 00:00:00 2001
From: Andreas Bigger
Date: Fri, 18 Sep 2020 12:46:19 -0700
Subject: [PATCH 023/129] fix missing application page (#362)
* fix missing application page
* fix deepscan issues
---
components/steps/Status.tsx | 2 +-
pages/appform.tsx | 49 +++++++++++++++++++++++++++++++++++++
2 files changed, 50 insertions(+), 1 deletion(-)
create mode 100644 pages/appform.tsx
diff --git a/components/steps/Status.tsx b/components/steps/Status.tsx
index 90b4182d..16d21c34 100644
--- a/components/steps/Status.tsx
+++ b/components/steps/Status.tsx
@@ -151,7 +151,7 @@ const StatusStep: React.FunctionComponent = props => {
{getStage(profile) === 2 && (
- navigateTo("application")}>
+ navigateTo("appform")}>
Fill out application
)}
diff --git a/pages/appform.tsx b/pages/appform.tsx
new file mode 100644
index 00000000..d2248dab
--- /dev/null
+++ b/pages/appform.tsx
@@ -0,0 +1,49 @@
+import React from "react";
+
+import { handleLoginRedirect, getProfile } from "../lib/authenticate";
+import { getReferrerCode } from "../lib/referrerCode";
+
+import Head from "../components/Head";
+import Navbar from "../components/Navbar";
+import Footer from "../components/Footer";
+
+import { Background, Container } from "../styles";
+
+import Step from "../components/steps/Profile";
+
+const appform = ({ profile }) => {
+ return (
+ <>
+
+
+
+ {profile && }
+
+
+ >
+ );
+};
+
+appform.getInitialProps = async ctx => {
+ const { req } = ctx;
+
+ const profile = await getProfile(req);
+
+ // Null profile means user is not logged in
+ if (!profile) {
+ handleLoginRedirect(req);
+ } else {
+ //Referrer Code Special Case Handling
+ profile.referrerCode = getReferrerCode(ctx, profile);
+ }
+
+ return {
+ profile
+ };
+};
+
+export default appform;
From 206781be173885d9b73c4898161b8430de55377e Mon Sep 17 00:00:00 2001
From: Andreas Bigger
Date: Sun, 20 Sep 2020 20:36:31 -0700
Subject: [PATCH 024/129] update hacksc header logo (#380)
---
assets/header_logo_21.png | Bin 0 -> 116148 bytes
assets/header_logo_21_transparent.png | Bin 0 -> 96975 bytes
components/Navbar.tsx | 4 ++--
3 files changed, 2 insertions(+), 2 deletions(-)
create mode 100644 assets/header_logo_21.png
create mode 100644 assets/header_logo_21_transparent.png
diff --git a/assets/header_logo_21.png b/assets/header_logo_21.png
new file mode 100644
index 0000000000000000000000000000000000000000..be58d2ce7c9c92f3fb4d89f966911d672b161f06
GIT binary patch
literal 116148
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