From 99cdadfbd9ebbf512115e1e1a4cf0f8fb1c247d5 Mon Sep 17 00:00:00 2001 From: Ehsan Asdar Date: Sun, 8 Oct 2017 14:54:06 -0400 Subject: [PATCH 001/194] Cleanup the repo From 7e7c445c5ea315007a3da11b557e7816f36936e5 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Mon, 9 Oct 2017 00:55:57 -0400 Subject: [PATCH 002/194] Add search result data, user login methods and admin status, team name, and form item labels to GraphQL API --- api.graphql | 27 +++++++- server/routes/api/graphql.ts | 125 +++++++++++++++++++++++++---------- 2 files changed, 116 insertions(+), 36 deletions(-) diff --git a/api.graphql b/api.graphql index cbd29767..17f107c3 100644 --- a/api.graphql +++ b/api.graphql @@ -13,7 +13,7 @@ type Query { # seen from the latest page retrieved, if you want the first page leave this out. users(pagination_token: ID, n: Int!, filter: UserFilter): [User!]! # Search through a user's name and email through regex - search_user(search: String!, use_regex: Boolean = false, offset: Int!, n: Int!, filter: UserFilter): [User!]! + search_user(search: String!, use_regex: Boolean = false, offset: Int!, n: Int!, filter: UserFilter): SearchResult! # All possible application question branches application_branches: [String!]! # All possible confirmation question branches @@ -24,6 +24,18 @@ type Query { question_names(branch: String): [String!] } +# A search result containing the results and associated data +type SearchResult { + # The offset passed to the search + offset: Int! + # The number of users returned by this query (limited by n) + count: Int! + # The total number of users that match this query (but not necessarily returned) + total: Int! + # The array of matching users + users: [User!]! +} + # Registration info about the user type User { # User ID, valid across the entire system @@ -35,6 +47,11 @@ type User { email: String! # If the user's email is a verified email email_verified: Boolean! + # Login method(s) this user uses (merged by email address) + login_methods: [String!]! + + # If the user has admin privileges + admin: Boolean! # If the user has applied to the event applied: Boolean! @@ -100,12 +117,16 @@ type Branch { type Team { # ID of the Team id: ID! + # Name of the Team + name: String! } # Entries to various forms (application, confirmation, etc.) type FormItem { - # Name of the question / form item + # Name (basically the ID) of the question / form item name: String! + # Label of form item + label: String! # Type of form item (textbox, checkbox, phone no.) type: String! # Value (if just one string) @@ -128,5 +149,7 @@ type File { path: String! # The size of the file in bytes size: Int! + # The formatted size of the file in human-readable units + size_formatted: String! } diff --git a/server/routes/api/graphql.ts b/server/routes/api/graphql.ts index 8553d8cc..2c70304c 100644 --- a/server/routes/api/graphql.ts +++ b/server/routes/api/graphql.ts @@ -6,9 +6,10 @@ import * as express from "express"; import { graphqlExpress, graphiqlExpress } from "graphql-server-express"; import { makeExecutableSchema } from "graphql-tools"; import { isAdmin, authenticateWithRedirect } from "../../middleware"; -import { User, IUser, IFormItem, QuestionBranchConfig } from "../../schema"; -import { Branches, Tags, AllTags } from "../../branch"; +import { User, IUser, Team, IFormItem, QuestionBranchConfig } from "../../schema"; +import { Branches, Tags, AllTags, BranchConfig, ApplicationBranch, ConfirmationBranch, NoopBranch } from "../../branch"; import { schema as types } from "./api.graphql.types"; +import { formatSize } from "../../common"; const typeDefs = fs.readFileSync(path.resolve(__dirname, "../../../api.graphql"), "utf8"); @@ -30,7 +31,7 @@ const resolvers: IResolver = { user: async (prev, args, request) => { const id = args.id || (request.user as IUser).uuid; const user = await User.findOne({uuid: id}); - return user ? userRecordToGraphql(user) : undefined; + return user ? await userRecordToGraphql(user) : undefined; }, users: async (prev, args) => { const lastIdQuery = args.pagination_token ? { @@ -45,7 +46,7 @@ const resolvers: IResolver = { }) .limit(args.n); - return allUsers.map(userRecordToGraphql); + return await Promise.all(allUsers.map(userRecordToGraphql)); }, search_user: async (prev, args) => { let escapedQuery: string = args.search; @@ -53,26 +54,35 @@ const resolvers: IResolver = { escapedQuery = escapedQuery.trim().replace(/[|\\{()[^$+*?.-]/g, "\\$&"); } const queryRegExp = new RegExp(escapedQuery, "i"); - + const query = [ + { + name: { + $regex: queryRegExp + } + }, + { + email: { + $regex: queryRegExp + } + } + ]; + const total = await User.find(userFilterToMongo(args.filter)) + .or(query) + .count(); const results = await User .find(userFilterToMongo(args.filter)) - .or([ - { - name: { - $regex: queryRegExp - } - }, - { - email: { - $regex: queryRegExp - } - } - ]) + .or(query) + .collation({ "locale": "en" }).sort({ name: "asc" }) .skip(args.offset) .limit(args.n) .exec(); - return results.map(userRecordToGraphql); + return { + offset: args.offset, + count: results.length, + total, + users: await Promise.all(results.map(userRecordToGraphql)) + }; }, question_branches: () => { return Branches; @@ -115,18 +125,33 @@ async function findQuestions( args: { names: string[] } ): Promise[]> { const user = await User.findOne({uuid: target.id}); - if (!user) return []; + if (!user) { + return []; + } const names = new Set(args.names); - return user.confirmationData.concat(user.applicationData) - .reduce((results, question) => { - if (names.has(question.name)) { - results.push(question); - } - return results; - }, [] as IFormItem[]) - .map(recordToFormItem); + function questionFilter (results: IFormItem[], question: IFormItem): IFormItem[] { + if (names.has(question.name)) { + results.push(question); + } + return results; + } + + let items: types.FormItem[] = []; + if (user.accepted) { + items = items.concat(await Promise.all(user.applicationData + .reduce(questionFilter, []) + .map(item => recordToFormItem(item, user.applicationBranch)) + )); + } + if (user.attending) { + items = items.concat(await Promise.all(user.confirmationData + .reduce(questionFilter, []) + .map(item => recordToFormItem(item, user.confirmationBranch)) + )); + } + return items; } export const schema = makeExecutableSchema({ @@ -179,16 +204,26 @@ function userFilterToMongo(filter: types.UserFilter | undefined) { return query; } -function recordToFormItem(item: IFormItem): types.FormItem { +let cachedBranches: { + [name: string]: NoopBranch | ApplicationBranch | ConfirmationBranch; +} = {}; +async function recordToFormItem(item: IFormItem, branchName: string): Promise> { + if (!cachedBranches[branchName]) { + cachedBranches[branchName] = await BranchConfig.loadBranchFromDB(branchName); + } + let label: string = cachedBranches[branchName].questionLabels[item.name] || item.name; + if (!item.value) { return { name: item.name, + label, type: item.type }; } else if (typeof item.value === "string") { return { name: item.name, + label, type: item.type, value: item.value }; @@ -196,6 +231,7 @@ function recordToFormItem(item: IFormItem): types.FormItem { else if (item.value instanceof Array) { return { name: item.name, + label, type: item.type, values: item.value }; @@ -205,22 +241,24 @@ function recordToFormItem(item: IFormItem): types.FormItem { const file = item.value as Express.Multer.File; return { name: item.name, + label, type: item.type, file: { original_name: file.originalname, encoding: file.encoding, mimetype: file.mimetype, path: file.path, - size: file.size + size: file.size, + size_formatted: formatSize(file.size) } }; } } -function userRecordToGraphql(user: IUser): types.User { +async function userRecordToGraphql(user: IUser): Promise> { const application: types.Branch | undefined = user.applied ? { type: user.applicationBranch, - data: user.applicationData.map(recordToFormItem), + data: await Promise.all(user.applicationData.map(item => recordToFormItem(item, user.applicationBranch))), start_time: user.applicationStartTime && user.applicationStartTime.toDateString(), submit_time: user.applicationSubmitTime && @@ -229,19 +267,37 @@ function userRecordToGraphql(user: IUser): types.User { const confirmation: types.Branch | undefined = user.attending ? { type: user.confirmationBranch, - data: user.confirmationData.map(recordToFormItem), + data: await Promise.all(user.confirmationData.map(item => recordToFormItem(item, user.confirmationBranch))), start_time: user.confirmationStartTime && user.confirmationStartTime.toDateString(), submit_time: user.confirmationSubmitTime && user.confirmationSubmitTime.toDateString() } : undefined; + let loginMethods: string[] = []; + if (user.githubData && user.githubData.id) { + loginMethods.push("GitHub"); + } + if (user.googleData && user.googleData.id) { + loginMethods.push("Google"); + } + if (user.facebookData && user.facebookData.id) { + loginMethods.push("Facebook"); + } + if (user.localData && user.localData.hash) { + loginMethods.push("Local"); + } + + let team = user.teamId ? await Team.findById(user.teamId) : null; + return { id: user.uuid, - name: user.name, + name: user.name || "", email: user.email, email_verified: !!user.verifiedEmail, + admin: !!user.admin, + login_methods: loginMethods, applied: !!user.applied, accepted: !!user.accepted, @@ -254,7 +310,8 @@ function userRecordToGraphql(user: IUser): types.User { // Will be filled in child resolver. questions: [], team: user.teamId && { - id: user.teamId.toHexString() + id: user.teamId.toHexString(), + name: team ? team.teamName : "(Missing team)" }, pagination_token: user._id.toHexString() From 8b9def920b45fa835c9f0982160878f4d244b1ba Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Mon, 9 Oct 2017 01:42:43 -0400 Subject: [PATCH 003/194] Add UI for searching --- client/admin.html | 7 ++++++- client/css/admin.css | 10 ++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/client/admin.html b/client/admin.html index a49b3801..0d203ad1 100644 --- a/client/admin.html +++ b/client/admin.html @@ -138,9 +138,14 @@

Users

Applicants

- Export for check in
+
+ + + + +
diff --git a/client/css/admin.css b/client/css/admin.css index e0387075..0d93589d 100644 --- a/client/css/admin.css +++ b/client/css/admin.css @@ -21,6 +21,16 @@ table td:last-child { width: 50px; text-align: center; } +#applicants > .search > * { + padding: 10px; +} +#applicants > .search > label { + min-width: 60px; + text-align: right; +} +#applicants > .search > button { + min-width: 100px; +} #applicants table td:last-child { width: 140px; } From e6ea76b66b55051d17df8352bed62e55a38b4413 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Mon, 9 Oct 2017 02:51:13 -0400 Subject: [PATCH 004/194] Remove unnecessary ES6 promise polyfill --- client/admin.html | 1 - client/application.html | 1 - client/confirmation.html | 1 - client/team.html | 1 - 4 files changed, 4 deletions(-) diff --git a/client/admin.html b/client/admin.html index 0d203ad1..0e2fe2cf 100644 --- a/client/admin.html +++ b/client/admin.html @@ -11,7 +11,6 @@ - diff --git a/client/application.html b/client/application.html index 310d7fbc..7d06b9a8 100644 --- a/client/application.html +++ b/client/application.html @@ -10,7 +10,6 @@ - diff --git a/client/confirmation.html b/client/confirmation.html index 9d6b55d2..a2deafef 100644 --- a/client/confirmation.html +++ b/client/confirmation.html @@ -10,7 +10,6 @@ - diff --git a/client/team.html b/client/team.html index ff8e8fab..6f5542ee 100644 --- a/client/team.html +++ b/client/team.html @@ -15,7 +15,6 @@ text-align: center; } - From d6f561c503d9861f0a3ccb038a7d20fa6a9c7371 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Mon, 9 Oct 2017 02:53:26 -0400 Subject: [PATCH 005/194] Fix bug in setIf() where false attributes wouldn't be set --- server/routes/api/graphql.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/routes/api/graphql.ts b/server/routes/api/graphql.ts index 2c70304c..7940102e 100644 --- a/server/routes/api/graphql.ts +++ b/server/routes/api/graphql.ts @@ -195,7 +195,12 @@ function userFilterToMongo(filter: types.UserFilter | undefined) { return {}; } const query: { [name: string]: any } = {}; - const setIf = (key: string, val: any) => val ? query[key] = val : undefined; + + function setIf(key: string, val: any): void { + if (val !== null && val !== undefined) { + query[key] = val; + } + } setIf("applied", filter.applied); setIf("accepted", filter.accepted); setIf("attending", filter.attending); From b11bb200dd8144746e6a477278956ca0b5afbfb3 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Mon, 9 Oct 2017 02:54:14 -0400 Subject: [PATCH 006/194] Implement applicant searching and loading via GraphQL API --- client/admin.html | 17 +++- client/js/admin.ts | 182 ++++++++++++++++++++++++++++------- server/routes/api/graphql.ts | 4 +- server/routes/templates.ts | 1 - server/schema.ts | 1 - 5 files changed, 162 insertions(+), 43 deletions(-) diff --git a/client/admin.html b/client/admin.html index 0e2fe2cf..4d8a82f0 100644 --- a/client/admin.html +++ b/client/admin.html @@ -139,7 +139,7 @@

Applicants

-
+
-
Name diff --git a/client/js/admin.ts b/client/js/admin.ts index 24290c94..ad94c690 100644 --- a/client/js/admin.ts +++ b/client/js/admin.ts @@ -141,14 +141,17 @@ class UserEntries { } class ApplicantEntries { - private static readonly NODE_COUNT = 100; + private static readonly NODE_COUNT = 10; private static generalNodes: HTMLTableRowElement[] = []; private static detailsNodes: HTMLTableRowElement[] = []; private static offset: number = 0; + private static readonly searchButton = document.getElementById("applicant-search-execute") as HTMLButtonElement; private static readonly previousButton = document.getElementById("applicants-entries-previous") as HTMLButtonElement; private static readonly nextButton = document.getElementById("applicants-entries-next") as HTMLButtonElement; private static readonly branchFilter = document.getElementById("branch-filter") as HTMLInputElement; private static readonly statusFilter = document.getElementById("status-filter") as HTMLInputElement; + private static readonly searchBox = document.getElementById("applicant-search") as HTMLInputElement; + private static readonly searchRegex = document.getElementById("applicant-search-regex") as HTMLInputElement; private static filter: any = {}; private static instantiate() { @@ -182,12 +185,34 @@ class ApplicantEntries { } private static updateFilter() { this.filter = {}; + if (this.branchFilter.value !== "*") { - this.filter.branch = this.branchFilter.value; + let [, type, branchName] = this.branchFilter.value.match(/^(application|confirmation)-(.*)$/)!; + if (type === "application") { + this.filter.application_branch = branchName; + } + else if (type === "confirmation") { + this.filter.confirmation_branch = branchName; + } } - if (this.statusFilter.value !== "*") { - this.filter.status = this.statusFilter.value; + + switch (this.statusFilter.value) { + case "no-decision": + this.filter.accepted = false; + break; + case "accepted": + this.filter.accepted = true; + break; + case "not-confirmed": + this.filter.accepted = true; + this.filter.attending = false; + break; + case "confirmed": + this.filter.accepted = true; + this.filter.attending = true; + break; } + this.offset = 0; this.load(); } @@ -195,40 +220,91 @@ class ApplicantEntries { const status = document.getElementById("applicants-entries-status") as HTMLParagraphElement; status.textContent = "Loading..."; - let query: { [index: string]: any } = { + let query = ` + query($search: String!, $useRegex: Boolean!, $offset: Int!, $count: Int!, $filter: UserFilter!) { + search_user(search: $search, use_regex: $useRegex, offset: $offset, n: $count, filter: $filter) { + offset, + count, + total, + users { + id, + name, + email, + email_verified, + admin, + team { + id, + name + }, + accepted, + attending, + application { + type, + data { + name, + label, + value, + values, + file { + mimetype, + size_formatted, + path + } + } + }, + confirmation { + type + } + } + } + }`; + let variables = { + search: this.searchBox.value, + useRegex: this.searchRegex.checked, offset: this.offset, count: this.NODE_COUNT, - applied: true, - ...this.filter + filter: { + applied: true, + ...this.filter + } }; - let params = Object.keys(query) - .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(query[key])}`) - .join("&") - .replace(/%20/g, "+"); - fetch(`/api/admin/users?${params}`, { + + fetch(`/graphql`, { credentials: "same-origin", - method: "GET" + method: "POST", + headers: new Headers({ + "Content-Type": "application/json" + }), + body: JSON.stringify({ + query, + variables + }) }).then(checkStatus).then(parseJSON).then((response: { - offset: number; - count: number; - total: number; - data: any[]; + data: { + search_user: { + offset: number; + count: number; + total: number; + users: any[]; + }; + }; }) => { + let res = response.data.search_user; for (let i = 0; i < this.NODE_COUNT; i++) { let generalNode = this.generalNodes[i]; let detailsNode = this.detailsNodes[i]; - let user = response.data[i]; + let user = res.users[i]; if (user) { generalNode.style.display = "table-row"; detailsNode.style.display = "table-row"; - generalNode.dataset.id = user.uuid; + generalNode.dataset.id = user.id; generalNode.querySelector("td.name")!.textContent = user.name; generalNode.querySelector("td.team")!.textContent = ""; - if (user.teamName) { + if (user.team) { let teamContainer = document.createElement("b"); - teamContainer.textContent = user.teamName; + teamContainer.textContent = user.team.name; generalNode.querySelector("td.team")!.appendChild(teamContainer); } else { @@ -236,7 +312,7 @@ class ApplicantEntries { } generalNode.querySelector("td.email > span")!.textContent = user.email; generalNode.querySelector("td.email")!.classList.remove("verified", "notverified", "admin"); - if (user.verifiedEmail) { + if (user.email_verified) { generalNode.querySelector("td.email")!.classList.add("verified"); } else { @@ -245,7 +321,7 @@ class ApplicantEntries { if (user.admin) { generalNode.querySelector("td.email")!.classList.add("admin"); } - generalNode.querySelector("td.branch")!.textContent = user.applicationBranch; + generalNode.querySelector("td.branch")!.textContent = user.application.type; let statusSelect = generalNode.querySelector("select.status") as HTMLSelectElement; statusSelect.value = user.accepted ? "accepted" : "no-decision"; @@ -253,16 +329,42 @@ class ApplicantEntries { while (dataSection.hasChildNodes()) { dataSection.removeChild(dataSection.lastChild!); } - for (let answer of user.applicationDataFormatted as { label: string; value: string; filename?: string }[]) { + + interface IFormItem { + name: string; + label: string; + type: string; + value?: string; + values?: string[]; + file?: { + original_name: string; + encoding: string; + mimetype: string; + path: string; + size: number; + size_formatted: string; + }; + } + for (let answer of user.application.data as IFormItem[]) { let row = document.createElement("p"); let label = document.createElement("b"); label.innerHTML = answer.label; row.appendChild(label); - row.appendChild(document.createTextNode(` → ${answer.value}`)); - if (answer.filename) { + let value = ""; + if (answer.value) { + value = answer.value; + } + else if (answer.values) { + value = answer.values.join(", "); + } + else if (answer.file) { + value = `[${answer.file.mimetype} | ${answer.file.size_formatted}]: ${answer.file.path}`; + } + row.appendChild(document.createTextNode(` → ${value}`)); + if (answer.file) { row.appendChild(document.createTextNode(" (")); let link = document.createElement("a"); - link.setAttribute("href", `/uploads/${answer.filename}`); + link.setAttribute("href", `/${answer.file.path}`); link.textContent = "Download"; row.appendChild(link); row.appendChild(document.createTextNode(")")); @@ -276,25 +378,25 @@ class ApplicantEntries { } } - if (response.offset <= 0) { + if (res.offset <= 0) { this.previousButton.disabled = true; } else { this.previousButton.disabled = false; } - let upperBound = response.offset + response.count; - if (upperBound >= response.total) { - upperBound = response.total; + let upperBound = res.offset + res.count; + if (upperBound >= res.total) { + upperBound = res.total; this.nextButton.disabled = true; } else { this.nextButton.disabled = false; } - let lowerBound = response.offset + 1; - if (response.data.length <= 0) { + let lowerBound = res.offset + 1; + if (res.users.length <= 0) { lowerBound = 0; } - status.textContent = `${lowerBound} – ${upperBound} of ${response.total.toLocaleString()}`; + status.textContent = `${lowerBound} – ${upperBound} of ${res.total.toLocaleString()}`; }).catch(async err => { console.error(err); await sweetAlert("Oh no!", err.message, "error"); @@ -305,7 +407,15 @@ class ApplicantEntries { this.generalNodes = []; this.instantiate(); this.offset = 0; - this.load(); + this.updateFilter(); + this.searchButton.addEventListener("click", () => { + this.updateFilter(); + }); + this.searchBox.addEventListener("keydown", event => { + if (event.key === "Enter") { + this.updateFilter(); + } + }); this.previousButton.addEventListener("click", () => { this.previous(); }); @@ -320,10 +430,12 @@ class ApplicantEntries { }); } public static next() { + document.querySelector("#applicants > table")!.scrollIntoView(); this.offset += this.NODE_COUNT; this.load(); } public static previous() { + document.querySelector("#applicants > table")!.scrollIntoView(); this.offset -= this.NODE_COUNT; if (this.offset < 0) { this.offset = 0; diff --git a/server/routes/api/graphql.ts b/server/routes/api/graphql.ts index 7940102e..f0c694f4 100644 --- a/server/routes/api/graphql.ts +++ b/server/routes/api/graphql.ts @@ -131,7 +131,7 @@ async function findQuestions( const names = new Set(args.names); - function questionFilter (results: IFormItem[], question: IFormItem): IFormItem[] { + function questionFilter(results: IFormItem[], question: IFormItem): IFormItem[] { if (names.has(question.name)) { results.push(question); } @@ -195,7 +195,7 @@ function userFilterToMongo(filter: types.UserFilter | undefined) { return {}; } const query: { [name: string]: any } = {}; - + function setIf(key: string, val: any): void { if (val !== null && val !== undefined) { query[key] = val; diff --git a/server/routes/templates.ts b/server/routes/templates.ts index 8daa1b5b..f22ecee8 100644 --- a/server/routes/templates.ts +++ b/server/routes/templates.ts @@ -548,7 +548,6 @@ templateRoutes.route("/admin").get(authenticateWithRedirect, async (request, res let templateData: IAdminTemplate = { siteTitle: config.eventName, user, - branchNames: await Branches.BranchConfig.getNames(), applicationStatistics: { totalUsers: await User.find().count(), appliedUsers: await User.find({ "applied": true }).count(), diff --git a/server/schema.ts b/server/schema.ts index ae99b543..03fb0245 100644 --- a/server/schema.ts +++ b/server/schema.ts @@ -326,7 +326,6 @@ export interface StatisticEntry { "responses": ResponseCount[]; } export interface IAdminTemplate extends ICommonTemplate { - branchNames: string[]; applicationStatistics: { totalUsers: number; appliedUsers: number; From e8692dd768a1fc96c8afd2d8eda50c60839bbc5d Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Mon, 9 Oct 2017 18:10:34 -0400 Subject: [PATCH 007/194] Fix crash when S3 file can't be found once and for all --- server/routes/uploads.ts | 7 ++++--- server/storage.ts | 17 ++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/server/routes/uploads.ts b/server/routes/uploads.ts index d7735296..cda80db7 100644 --- a/server/routes/uploads.ts +++ b/server/routes/uploads.ts @@ -5,10 +5,11 @@ import { isAdmin } from "../middleware"; export let uploadsRoutes = express.Router(); -uploadsRoutes.route("/:file").get(isAdmin, (request, response) => { - response.attachment(request.params.file); +uploadsRoutes.route("/:file").get(isAdmin, async (request, response) => { try { - STORAGE_ENGINE.readFile(request.params.file).pipe(response); + let stream = await STORAGE_ENGINE.readFile(request.params.file); + response.attachment(request.params.file); + stream.pipe(response); } catch { response.status(404).send("The requested file could not be found"); diff --git a/server/storage.ts b/server/storage.ts index 63501440..558eb438 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -13,7 +13,7 @@ import * as AWS from "aws-sdk"; export interface IStorageEngine { uploadRoot: string; saveFile(currentPath: string, name: string): Promise; - readFile(name: string): Readable; + readFile(name: string): Promise; } interface ICommonOptions { uploadDirectory: string; @@ -56,7 +56,7 @@ class DiskStorageEngine implements IStorageEngine { readStream.pipe(writeStream); }); } - public readFile(name: string): Readable { + public async readFile(name: string): Promise { return fs.createReadStream(path.join(this.options.uploadDirectory, name)); } } @@ -101,7 +101,7 @@ class S3StorageEngine implements IStorageEngine { }).catch(reject); }); } - public readFile(name: string): Readable { + public async readFile(name: string): Promise { AWS.config.update({ region: this.options.region, credentials: new AWS.Credentials({ @@ -110,14 +110,13 @@ class S3StorageEngine implements IStorageEngine { }) }); let s3 = new AWS.S3(); - let stream = s3.getObject({ + const object = { Bucket: this.options.bucket, Key: name - }).createReadStream(); - stream.on("error", err => { - throw err; - }); - return stream; + }; + // Will throw if the object does not exist + await s3.headObject(object).promise(); + return s3.getObject(object).createReadStream(); } } From 1220a02f81cc8a64a9879dbf3109b6732fdeaa14 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Mon, 9 Oct 2017 18:11:10 -0400 Subject: [PATCH 008/194] Display application and confirmation data in applicants list --- client/js/admin.ts | 118 ++++++++++++++++++++++++------------- server/app.ts | 2 - server/routes/api/admin.ts | 115 ------------------------------------ 3 files changed, 76 insertions(+), 159 deletions(-) delete mode 100644 server/routes/api/admin.ts diff --git a/client/js/admin.ts b/client/js/admin.ts index ad94c690..640ea6dd 100644 --- a/client/js/admin.ts +++ b/client/js/admin.ts @@ -253,7 +253,18 @@ class ApplicantEntries { } }, confirmation { - type + type, + data { + name, + label, + value, + values, + file { + mimetype, + size_formatted, + path + } + } } } } @@ -290,6 +301,56 @@ class ApplicantEntries { }; }) => { let res = response.data.search_user; + + interface IFormItem { + name: string; + label: string; + type: string; + value?: string; + values?: string[]; + file?: { + original_name: string; + encoding: string; + mimetype: string; + path: string; + size: number; + size_formatted: string; + }; + } + function addApplicationData(dataSection: HTMLDivElement, applicationData: IFormItem[]): void { + for (let answer of applicationData as IFormItem[]) { + let row = document.createElement("p"); + let label = document.createElement("b"); + label.innerHTML = answer.label; + row.appendChild(label); + let value = ""; + if (answer.value) { + value = answer.value; + } + else if (answer.values) { + value = answer.values.join(", "); + } + else if (answer.file) { + value = `[${answer.file.mimetype} | ${answer.file.size_formatted}]: ${answer.file.path}`; + } + row.appendChild(document.createTextNode(` → ${value}`)); + if (answer.file) { + row.appendChild(document.createTextNode(" (")); + let link = document.createElement("a"); + link.setAttribute("href", `/${answer.file.path}`); + link.textContent = "Download"; + row.appendChild(link); + row.appendChild(document.createTextNode(")")); + } + dataSection.appendChild(row); + } + } + function addApplicationDataHeader(dataSection: HTMLDivElement, content: string): void { + let header = document.createElement("h4"); + header.textContent = content; + dataSection.appendChild(header); + } + for (let i = 0; i < this.NODE_COUNT; i++) { let generalNode = this.generalNodes[i]; let detailsNode = this.detailsNodes[i]; @@ -330,46 +391,19 @@ class ApplicantEntries { dataSection.removeChild(dataSection.lastChild!); } - interface IFormItem { - name: string; - label: string; - type: string; - value?: string; - values?: string[]; - file?: { - original_name: string; - encoding: string; - mimetype: string; - path: string; - size: number; - size_formatted: string; - }; + if (user.application) { + addApplicationDataHeader(dataSection, `Application data (${user.application.type})`); + addApplicationData(dataSection, user.application.data); } - for (let answer of user.application.data as IFormItem[]) { - let row = document.createElement("p"); - let label = document.createElement("b"); - label.innerHTML = answer.label; - row.appendChild(label); - let value = ""; - if (answer.value) { - value = answer.value; - } - else if (answer.values) { - value = answer.values.join(", "); - } - else if (answer.file) { - value = `[${answer.file.mimetype} | ${answer.file.size_formatted}]: ${answer.file.path}`; - } - row.appendChild(document.createTextNode(` → ${value}`)); - if (answer.file) { - row.appendChild(document.createTextNode(" (")); - let link = document.createElement("a"); - link.setAttribute("href", `/${answer.file.path}`); - link.textContent = "Download"; - row.appendChild(link); - row.appendChild(document.createTextNode(")")); - } - dataSection.appendChild(row); + else { + addApplicationDataHeader(dataSection, "No application data"); + } + if (user.confirmation) { + addApplicationDataHeader(dataSection, `Confirmation data (${user.confirmation.type})`); + addApplicationData(dataSection, user.confirmation.data); + } + else { + addApplicationDataHeader(dataSection, "No confirmation data"); } } else { @@ -678,8 +712,8 @@ declare let data: { declare const Chart: any; // Get the text color and use that for graphs -const header = document.querySelector("#sidebar > h1") as HTMLHeadingElement; -const color = window.getComputedStyle(header).getPropertyValue("color"); +const pageHeader = document.querySelector("#sidebar > h1") as HTMLHeadingElement; +const color = window.getComputedStyle(pageHeader).getPropertyValue("color"); for (let i = 0; i < data.length; i++) { let context = document.getElementById(`chart-${i}`) as HTMLCanvasElement | null; diff --git a/server/app.ts b/server/app.ts index f6a51b16..83d74b0e 100644 --- a/server/app.ts +++ b/server/app.ts @@ -102,8 +102,6 @@ let apiRouter = express.Router(); // API routes go here import {userRoutes} from "./routes/api/user"; apiRouter.use("/user/:uuid", userRoutes); -import {adminRoutes} from "./routes/api/admin"; -apiRouter.use("/admin", adminRoutes); import {settingsRoutes} from "./routes/api/settings"; apiRouter.use("/settings", settingsRoutes); diff --git a/server/routes/api/admin.ts b/server/routes/api/admin.ts deleted file mode 100644 index 3f25ff24..00000000 --- a/server/routes/api/admin.ts +++ /dev/null @@ -1,115 +0,0 @@ -import * as express from "express"; - -import { - formatSize -} from "../../common"; -import { - isAdmin -} from "../../middleware"; -import { - User, Team -} from "../../schema"; -import * as Branches from "../../branch"; - -export let adminRoutes = express.Router(); - -adminRoutes.route("/users").get(isAdmin, async (request, response) => { - let offset = parseInt(request.query.offset, 10); - if (isNaN(offset) || offset < 0) { - offset = 0; - } - let count = parseInt(request.query.count, 10); - if (isNaN(count) || count <= 0) { - count = 10; - } - let filter: any = {}; - if (request.query.applied === "true") { - filter.applied = true; - } - if (request.query.branch) { - // tslint:disable-next-line:no-string-literal - filter["$or"] = [{ "applicationBranch": request.query.branch }, { "confirmationBranch": request.query.branch }]; - } - if (request.query.status === "no-decision") { - filter.applied = true; - filter.accepted = false; - } - if (request.query.status === "accepted") { - filter.applied = true; - filter.accepted = true; - } - let branches = await Branches.BranchConfig.loadAllBranches(); - - let teamIDNameMap: { - [id: string]: string; - } = {}; - (await Team.find()).forEach(team => { - teamIDNameMap[team._id.toString()] = team.teamName; - }); - - let total = await User.count(filter); - let results = (await User.find(filter).collation({ "locale": "en" }).sort({ name: "asc" }).skip(offset).limit(count).exec()).map(user => { - let loginMethods: string[] = []; - if (user.githubData && user.githubData.id) { - loginMethods.push("GitHub"); - } - if (user.googleData && user.googleData.id) { - loginMethods.push("Google"); - } - if (user.facebookData && user.facebookData.id) { - loginMethods.push("Facebook"); - } - if (user.localData && user.localData.hash) { - loginMethods.push("Local"); - } - let status: string = "Signed up"; - if (user.applied) { - status = `Applied (${user.applicationBranch})`; - } - if (user.accepted) { - status = `Accepted (${user.applicationBranch})`; - } - if (user.attending) { - status = `Accepted (${user.applicationBranch}) / Attending (${user.confirmationBranch})`; - } - let questionsFromBranch = branches.find(branch => branch.name === user.applicationBranch); - let applicationDataFormatted: {"label": string; "value": string}[] = []; - if (questionsFromBranch) { - applicationDataFormatted = user.applicationData.map(question => { - let rawQuestion = questionsFromBranch!.questions.find(q => q.name === question.name); - // If there isn't schema information for this question return the raw name as the label - let label: string = rawQuestion ? rawQuestion.label : question.name; - - if (typeof question.value === "string") { - return { label, value: question.value }; - } - else if (Array.isArray(question.value)) { - return { label, value: question.value.join(", ") }; - } - else if (question.value === null) { - return { label, value: "N/A" }; - } - else { - // Multer file - let file = question.value; - let formattedSize = formatSize(file.size); - return { label, value: `[${file.mimetype} | ${formattedSize}]: ${file.path}`, filename: file.filename }; - } - }); - } - - return { - ...user.toObject(), - "status": status, - "loginMethods": loginMethods.join(", "), - "applicationDataFormatted": applicationDataFormatted, - "teamName": user.teamId ? teamIDNameMap[user.teamId.toString()] : null - }; - }); - response.json({ - offset, - count, - total, - data: results - }); -}); From 8b833cd687233e88c2be4e1b9716beb00b0ec3be Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Mon, 9 Oct 2017 19:15:53 -0400 Subject: [PATCH 009/194] Switch users list to GraphQL API too --- client/js/admin.ts | 93 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 70 insertions(+), 23 deletions(-) diff --git a/client/js/admin.ts b/client/js/admin.ts index 640ea6dd..1ab64560 100644 --- a/client/js/admin.ts +++ b/client/js/admin.ts @@ -47,33 +47,69 @@ class UserEntries { const status = document.getElementById("users-entries-status") as HTMLParagraphElement; status.textContent = "Loading..."; - let query: { [index: string]: any } = { + let query = ` + query($offset: Int!, $count: Int!) { + search_user(search: "", offset: $offset, n: $count) { + offset, + count, + total, + users { + id, + name, + email, + email_verified, + admin, + login_methods, + + application { + type + }, + confirmation { + type + }, + applied, + accepted, + attending + } + } + }`; + let variables = { offset: this.offset, count: this.NODE_COUNT }; - let params = Object.keys(query) - .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(query[key])}`) - .join("&") - .replace(/%20/g, "+"); - fetch(`/api/admin/users?${params}`, { + + fetch("/graphql", { credentials: "same-origin", - method: "GET" + method: "POST", + headers: new Headers({ + "Content-Type": "application/json" + }), + body: JSON.stringify({ + query, + variables + }) }).then(checkStatus).then(parseJSON).then((response: { - offset: number; - count: number; - total: number; - data: any[]; + data: { + search_user: { + offset: number; + count: number; + total: number; + users: any[]; + }; + }; }) => { + let res = response.data.search_user; + for (let i = 0; i < this.NODE_COUNT; i++) { let node = this.nodes[i]; - let user = response.data[i]; + let user = res.users[i]; if (user) { node.style.display = "table-row"; node.querySelector("td.name")!.textContent = user.name; node.querySelector("td.email > span")!.textContent = user.email; node.querySelector("td.email")!.classList.remove("verified", "notverified", "admin"); - if (user.verifiedEmail) { + if (user.email_verified) { node.querySelector("td.email")!.classList.add("verified"); } else { @@ -82,33 +118,44 @@ class UserEntries { if (user.admin) { node.querySelector("td.email")!.classList.add("admin"); } - node.querySelector("td.status")!.textContent = user.status; - node.querySelector("td.login-method")!.textContent = user.loginMethods; + + let userStatus = "Signed up"; + if (user.applied) { + userStatus = `Applied (${user.application.type})`; + } + if (user.applied && user.accepted) { + userStatus = `Accepted (${user.application.type})`; + } + if (user.applied && user.accepted && user.attending) { + userStatus = `Accepted (${user.application.type}) / Confirmed (${user.confirmation.type})`; + } + node.querySelector("td.status")!.textContent = userStatus; + node.querySelector("td.login-method")!.textContent = user.login_methods.join(", "); } else { node.style.display = "none"; } } - if (response.offset <= 0) { + if (res.offset <= 0) { this.previousButton.disabled = true; } else { this.previousButton.disabled = false; } - let upperBound = response.offset + response.count; - if (upperBound >= response.total) { - upperBound = response.total; + let upperBound = res.offset + res.count; + if (upperBound >= res.total) { + upperBound = res.total; this.nextButton.disabled = true; } else { this.nextButton.disabled = false; } - let lowerBound = response.offset + 1; - if (response.data.length <= 0) { + let lowerBound = res.offset + 1; + if (res.users.length <= 0) { lowerBound = 0; } - status.textContent = `${lowerBound} – ${upperBound} of ${response.total.toLocaleString()}`; + status.textContent = `${lowerBound} – ${upperBound} of ${res.total.toLocaleString()}`; }).catch(async err => { console.error(err); await sweetAlert("Oh no!", err.message, "error"); @@ -280,7 +327,7 @@ class ApplicantEntries { } }; - fetch(`/graphql`, { + fetch("/graphql", { credentials: "same-origin", method: "POST", headers: new Headers({ From f93e73b33ce427b5dec97761d3d29da7686e3a74 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Mon, 9 Oct 2017 19:18:39 -0400 Subject: [PATCH 010/194] Bump version to 1.12.0 --- package-lock.json | 28 +++++++++++++++++++++------- package.json | 2 +- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6fd3a8ce..12de7779 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "1.11.7", + "version": "1.12.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -212,8 +212,7 @@ "@types/node": { "version": "8.0.32", "resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.32.tgz", - "integrity": "sha512-n1zzgeQehndikZc/8N4rGSZc99cO6Tb3OInKzvWYniJsT/jet3m57buaBFa5cMeVNFosN4PKZ2LM1y16CFD7Rg==", - "dev": true + "integrity": "sha512-n1zzgeQehndikZc/8N4rGSZc99cO6Tb3OInKzvWYniJsT/jet3m57buaBFa5cMeVNFosN4PKZ2LM1y16CFD7Rg==" }, "@types/nodemailer": { "version": "1.3.33", @@ -319,6 +318,14 @@ "integrity": "sha1-EHPEvIJHVK49EM+riKsCN7qWTk0=", "dev": true }, + "@types/uuid": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.2.tgz", + "integrity": "sha512-fXrYo81BZgbz6KhHU3+dNUSdca35FwbfTMQvSnIIX6qOCsymSmFAjPDSfr4Q/Pt9abjt04kBi4+7FinVPdyEpg==", + "requires": { + "@types/node": "8.0.32" + } + }, "@types/whatwg-fetch": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/@types/whatwg-fetch/-/whatwg-fetch-0.0.33.tgz", @@ -499,6 +506,13 @@ "uuid": "3.0.1", "xml2js": "0.4.17", "xmlbuilder": "4.2.1" + }, + "dependencies": { + "uuid": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz", + "integrity": "sha1-ZUS7ot/ajBzxfmKaOjBeK7H+5sE=" + } } }, "babel-code-frame": { @@ -1419,7 +1433,7 @@ "requires": { "@types/graphql": "0.9.4", "deprecated-decorator": "0.1.6", - "uuid": "3.0.1" + "uuid": "3.1.0" } }, "graphql-typewriter": { @@ -2818,9 +2832,9 @@ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, "uuid": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz", - "integrity": "sha1-ZUS7ot/ajBzxfmKaOjBeK7H+5sE=" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" }, "validator": { "version": "8.2.0", diff --git a/package.json b/package.json index b93581e7..c8d7ad84 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "1.11.7", + "version": "1.12.0", "description": "TBD", "main": "server/app.js", "scripts": { From ee750cf218e5110df325aca2bfccdbed67351ad6 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Mon, 9 Oct 2017 23:58:38 -0400 Subject: [PATCH 011/194] Add simplified user search to GraphQL API to facilitate forwarding from checkin (#187) * Add simplified user search to GraphQL API to facilitate forwarding from checkin * Bump version to 1.12.1 --- api.graphql | 4 +- package-lock.json | 2 +- package.json | 2 +- server/routes/api/graphql.ts | 81 ++++++++++++++++++++---------------- 4 files changed, 51 insertions(+), 38 deletions(-) diff --git a/api.graphql b/api.graphql index 17f107c3..a347a1f8 100644 --- a/api.graphql +++ b/api.graphql @@ -12,8 +12,10 @@ type Query { # This is paginated, n is the number of results, and last_id is the last ID # seen from the latest page retrieved, if you want the first page leave this out. users(pagination_token: ID, n: Int!, filter: UserFilter): [User!]! - # Search through a user's name and email through regex + # Search for users by name and email search_user(search: String!, use_regex: Boolean = false, offset: Int!, n: Int!, filter: UserFilter): SearchResult! + # Simplified search_user that can be forwarded correctly from checkin2 + search_user_simple(search: String!, use_regex: Boolean = false, offset: Int!, n: Int!, filter: UserFilter): [User!]! # All possible application question branches application_branches: [String!]! # All possible confirmation question branches diff --git a/package-lock.json b/package-lock.json index 12de7779..33de876d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "1.12.0", + "version": "1.12.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index c8d7ad84..c9bcb99f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "1.12.0", + "version": "1.12.1", "description": "TBD", "main": "server/app.js", "scripts": { diff --git a/server/routes/api/graphql.ts b/server/routes/api/graphql.ts index f0c694f4..f1dbc7aa 100644 --- a/server/routes/api/graphql.ts +++ b/server/routes/api/graphql.ts @@ -48,41 +48,9 @@ const resolvers: IResolver = { return await Promise.all(allUsers.map(userRecordToGraphql)); }, - search_user: async (prev, args) => { - let escapedQuery: string = args.search; - if (!args.use_regex) { - escapedQuery = escapedQuery.trim().replace(/[|\\{()[^$+*?.-]/g, "\\$&"); - } - const queryRegExp = new RegExp(escapedQuery, "i"); - const query = [ - { - name: { - $regex: queryRegExp - } - }, - { - email: { - $regex: queryRegExp - } - } - ]; - const total = await User.find(userFilterToMongo(args.filter)) - .or(query) - .count(); - const results = await User - .find(userFilterToMongo(args.filter)) - .or(query) - .collation({ "locale": "en" }).sort({ name: "asc" }) - .skip(args.offset) - .limit(args.n) - .exec(); - - return { - offset: args.offset, - count: results.length, - total, - users: await Promise.all(results.map(userRecordToGraphql)) - }; + search_user: searchUser, + search_user_simple: async (prev, args) => { + return (await searchUser(prev, args)).users; }, question_branches: () => { return Branches; @@ -120,6 +88,49 @@ const resolvers: IResolver = { } }; +async function searchUser(prev: any, args: { + search: string; + use_regex: boolean; + offset: number; + n: number; + filter: types.UserFilter; +}) { + let escapedQuery: string = args.search; + if (!args.use_regex) { + escapedQuery = escapedQuery.trim().replace(/[|\\{()[^$+*?.-]/g, "\\$&"); + } + const queryRegExp = new RegExp(escapedQuery, "i"); + const query = [ + { + name: { + $regex: queryRegExp + } + }, + { + email: { + $regex: queryRegExp + } + } + ]; + const total = await User.find(userFilterToMongo(args.filter)) + .or(query) + .count(); + const results = await User + .find(userFilterToMongo(args.filter)) + .or(query) + .collation({ "locale": "en" }).sort({ name: "asc" }) + .skip(args.offset) + .limit(args.n) + .exec(); + + return { + offset: args.offset, + count: results.length, + total, + users: await Promise.all(results.map(userRecordToGraphql)) + }; +} + async function findQuestions( target: types.User, args: { names: string[] } From bacc8fac06857ad4a415d727069e5fc0c7c0e2fa Mon Sep 17 00:00:00 2001 From: Anthony Liu Date: Wed, 1 Nov 2017 11:38:58 -0400 Subject: [PATCH 012/194] Fix question population for non-accepted applicants (#192) * fix question population for non-accepted applicants * Bump version to 1.12.2 --- package-lock.json | 2 +- package.json | 2 +- server/routes/api/graphql.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 33de876d..507ffbc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "1.12.1", + "version": "1.12.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index c9bcb99f..f180ca3e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "1.12.1", + "version": "1.12.2", "description": "TBD", "main": "server/app.js", "scripts": { diff --git a/server/routes/api/graphql.ts b/server/routes/api/graphql.ts index f1dbc7aa..43c74f12 100644 --- a/server/routes/api/graphql.ts +++ b/server/routes/api/graphql.ts @@ -150,7 +150,7 @@ async function findQuestions( } let items: types.FormItem[] = []; - if (user.accepted) { + if (user.applied) { items = items.concat(await Promise.all(user.applicationData .reduce(questionFilter, []) .map(item => recordToFormItem(item, user.applicationBranch)) From 9c1fc528b20b9fd5d63a4de94e43a0f6f0bc287d Mon Sep 17 00:00:00 2001 From: Ehsan Asdar Date: Mon, 8 Jan 2018 08:36:33 -0800 Subject: [PATCH 013/194] Bump marked version due to CVE-2017-17461 and CVE-2017-1000427 (#197) --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 507ffbc8..aa48815c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1860,9 +1860,9 @@ } }, "marked": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.6.tgz", - "integrity": "sha1-ssbGGPzOzk74bE/Gy4p8v1rtqNc=" + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.9.tgz", + "integrity": "sha512-nW5u0dxpXxHfkHzzrveY45gCbi+R4PaO4WRZYqZNl+vB0hVGeqlFn0aOg1c8AKL63TrNFn9Bm2UP4AdiZ9TPLw==" }, "media-typer": { "version": "0.3.0", diff --git a/package.json b/package.json index f180ca3e..8798113f 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "handlebars": "^4.0.6", "json-schema-to-typescript": "^4.4.0", "json2csv": "^3.7.3", - "marked": "^0.3.6", + "marked": "^0.3.9", "moment": "^2.18.1", "moment-timezone": "^0.5.13", "mongoose": "^4.10.3", From 3777b83a1c583ff9558b9de3017d8fb660b905f0 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Wed, 28 Feb 2018 12:24:26 -0500 Subject: [PATCH 014/194] Switch to simpler and more secure SendGrid API (#200) * Switch to simpler and more secure SendGrid API * Deployment fixes * Remove EMAIL_USERNAME --- README.md | 5 +- deployment.yaml | 5 +- package-lock.json | 377 ++++++++++++++++++++++++++++++++++++++++------ package.json | 5 +- server/common.ts | 52 ++----- server/schema.ts | 5 +- 6 files changed, 352 insertions(+), 97 deletions(-) diff --git a/README.md b/README.md index dcd486b5..db6e7462 100644 --- a/README.md +++ b/README.md @@ -110,10 +110,7 @@ GOOGLE_CLIENT_SECRET | OAuth client secret for Google *required* FACEBOOK_CLIENT_ID | OAuth client ID for Facebook *required* FACEBOOK_CLIENT_SECRET | OAuth client secret for Facebook *required* EMAIL_FROM | The `From` header for sent emails (default: `HackGT Team `) -EMAIL_HOST | The SMTP email server's hostname (default: *none*) -EMAIL_PORT | The SMTP email server's port (default: `465`) -EMAIL_USERNAME | The username for the SMTP email server (default: *none*) -EMAIL_PASSWORD | The password for the SMTP email server (default: *none*) +EMAIL_KEY | The SendGrid API key for sending emails (default: *none*) ADMIN_EMAILS | A JSON array of the emails of the users that you want promoted to admin status when they create their account (default: none) EVENT_NAME | The current event's name which affects rendered templates and sent emails (default: `Untitled Event`) STORAGE_ENGINE | The name of the storage engine that handles file uploads as defined in [storage.ts](server/storage.ts) (default: `disk`) diff --git a/deployment.yaml b/deployment.yaml index df171364..c864cd59 100644 --- a/deployment.yaml +++ b/deployment.yaml @@ -10,8 +10,7 @@ health: secrets: - ADMIN_KEY_SECRET - SESSION_SECRET - - EMAIL_USERNAME - - EMAIL_PASSWORD + - EMAIL_KEY - GOOGLE_CLIENT_ID - GOOGLE_CLIENT_SECRET - GITHUB_CLIENT_ID @@ -24,8 +23,6 @@ env: STORAGE_ENGINE: s3 COOKIE_SECURE_ONLY: true EMAIL_FROM: "HackGT Team " - EMAIL_HOST: smtp.sendgrid.net - EMAIL_PORT: 465 PRODUCTION: true ADMIN_EMAILS: - petschekr@gmail.com diff --git a/package-lock.json b/package-lock.json index aa48815c..163a851c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,9 +1,36 @@ { "name": "registration", - "version": "1.12.2", + "version": "1.12.3", "lockfileVersion": 1, "requires": true, "dependencies": { + "@sendgrid/client": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-6.2.1.tgz", + "integrity": "sha512-FLqoh2UqmFs5R/92xzF1jYMLGU89rTgLK6XX+VA02YcfQW8rGjbMrj7zsSCQ7SLkeiWekmUU2+naeIO9L4dqxA==", + "requires": { + "@sendgrid/helpers": "6.2.1", + "@types/request": "2.47.0", + "request": "2.83.0" + } + }, + "@sendgrid/helpers": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-6.2.1.tgz", + "integrity": "sha512-WnQ4TV51Xln/X70lk6J1/3tfRHW3K4zagz19vlJrtQUtX1wwghOj926OqcMm5nOiBHEh+la3cvdzHENb09FsIA==", + "requires": { + "chalk": "2.1.0" + } + }, + "@sendgrid/mail": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-6.2.1.tgz", + "integrity": "sha512-gTd8gMp4JVLGZhXb/DkyrjByTfIR1OvtpPpQLwO11Vz72x3JdPl4tJTtWA/svAFfN5UXnZtAomAvjJCdcd4lzw==", + "requires": { + "@sendgrid/client": "6.2.1", + "@sendgrid/helpers": "6.2.1" + } + }, "@types/archiver": { "version": "0.15.37", "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-0.15.37.tgz", @@ -41,6 +68,11 @@ "@types/node": "8.0.32" } }, + "@types/caseless": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.1.tgz", + "integrity": "sha512-FhlMa34NHp9K5MY1Uz8yb+ZvuX0pnvn3jScRSNAb75KHGB8d3rEU6hqMs3Z2vjuytcMfRg6c5CHMc3wtYyD2/A==" + }, "@types/chai": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-3.5.2.tgz", @@ -133,6 +165,14 @@ "@types/node": "8.0.32" } }, + "@types/form-data": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz", + "integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==", + "requires": { + "@types/node": "8.0.32" + } + }, "@types/graphql": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/@types/graphql/-/graphql-0.9.4.tgz", @@ -214,36 +254,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.32.tgz", "integrity": "sha512-n1zzgeQehndikZc/8N4rGSZc99cO6Tb3OInKzvWYniJsT/jet3m57buaBFa5cMeVNFosN4PKZ2LM1y16CFD7Rg==" }, - "@types/nodemailer": { - "version": "1.3.33", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-1.3.33.tgz", - "integrity": "sha512-PONEJf/LwNcqgU/GpMIAquSBFdq+kCdpYI9TdoeGcTfLCsXzWunKzv4bUQs8zfKGz97CLymgoL0fMLYpOu+/1A==", - "dev": true, - "requires": { - "@types/node": "8.0.32", - "@types/nodemailer-direct-transport": "1.0.30", - "@types/nodemailer-smtp-transport": "2.7.3" - } - }, - "@types/nodemailer-direct-transport": { - "version": "1.0.30", - "resolved": "https://registry.npmjs.org/@types/nodemailer-direct-transport/-/nodemailer-direct-transport-1.0.30.tgz", - "integrity": "sha512-gH49BNkXM8EZb/UgI4hUwWwTW3izRx5L+0VyohKkbVijvfUIhn7RALSpBjCUyXzEj0XZSNmQMFVc97Lj0z8UIw==", - "dev": true, - "requires": { - "@types/nodemailer": "1.3.33" - } - }, - "@types/nodemailer-smtp-transport": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/@types/nodemailer-smtp-transport/-/nodemailer-smtp-transport-2.7.3.tgz", - "integrity": "sha512-HxKPBErWelYVIWiKkUl06IaG4ojEMDtH6cAlojKgjsqwF8UQun4QeahYCWLCkA8/vKOX0G6VV1Vu2Z4x4ovqLQ==", - "dev": true, - "requires": { - "@types/node": "8.0.32", - "@types/nodemailer": "1.3.33" - } - }, "@types/passport": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/@types/passport/-/passport-0.3.4.tgz", @@ -284,6 +294,17 @@ "@types/passport": "0.3.4" } }, + "@types/request": { + "version": "2.47.0", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.47.0.tgz", + "integrity": "sha512-/KXM5oev+nNCLIgBjkwbk8VqxmzI56woD4VUxn95O+YeQ8hJzcSmIZ1IN3WexiqBb6srzDo2bdMbsXxgXNkz5Q==", + "requires": { + "@types/caseless": "0.12.1", + "@types/form-data": "2.2.1", + "@types/node": "8.0.32", + "@types/tough-cookie": "2.3.2" + } + }, "@types/serve-static": { "version": "1.7.32", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.7.32.tgz", @@ -318,6 +339,11 @@ "integrity": "sha1-EHPEvIJHVK49EM+riKsCN7qWTk0=", "dev": true }, + "@types/tough-cookie": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.2.tgz", + "integrity": "sha512-vOVmaruQG5EatOU/jM6yU2uCp3Lz6mK1P5Ztu4iJjfM4SVHU9XYktPUQtKlIXuahqXHdEyUarMrBEwg5Cwu+bA==" + }, "@types/uuid": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.2.tgz", @@ -471,6 +497,16 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, "assertion-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", @@ -488,8 +524,7 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "aws-sdk": { "version": "2.126.0", @@ -515,6 +550,16 @@ } } }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" + }, "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", @@ -571,6 +616,15 @@ "safe-buffer": "5.1.1" } }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, "bl": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.1.tgz", @@ -606,6 +660,14 @@ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" }, + "boom": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", + "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", + "requires": { + "hoek": "4.2.1" + } + }, "bowser": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/bowser/-/bowser-1.7.3.tgz", @@ -699,6 +761,11 @@ "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", "optional": true }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, "center-align": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", @@ -819,7 +886,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", - "dev": true, "requires": { "delayed-stream": "1.0.0" } @@ -951,6 +1017,24 @@ "readable-stream": "2.3.3" } }, + "cryptiles": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", + "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", + "requires": { + "boom": "5.2.0" + }, + "dependencies": { + "boom": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", + "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", + "requires": { + "hoek": "4.2.1" + } + } + } + }, "crypto-browserify": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-1.0.9.tgz", @@ -980,6 +1064,14 @@ "es5-ext": "0.10.30" } }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "1.0.0" + } + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -1006,8 +1098,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, "depd": { "version": "1.1.1", @@ -1100,6 +1191,15 @@ "domelementtype": "1.3.0" } }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1277,8 +1377,12 @@ "extend": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", - "dev": true + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, "fast-deep-equal": { "version": "1.0.0", @@ -1307,11 +1411,15 @@ "is-buffer": "1.1.5" } }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, "form-data": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", - "dev": true, "requires": { "asynckit": "0.4.0", "combined-stream": "1.0.5", @@ -1359,6 +1467,14 @@ "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", "dev": true }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "1.0.0" + } + }, "git-rev-sync": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/git-rev-sync/-/git-rev-sync-1.9.1.tgz", @@ -1471,6 +1587,20 @@ } } }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "requires": { + "ajv": "5.2.3", + "har-schema": "2.0.0" + } + }, "has-ansi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", @@ -1485,12 +1615,28 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" }, + "hawk": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", + "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", + "requires": { + "boom": "4.3.1", + "cryptiles": "3.1.2", + "hoek": "4.2.1", + "sntp": "2.1.0" + } + }, "he": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", "dev": true }, + "hoek": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", + "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==" + }, "hooks-fixed": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hooks-fixed/-/hooks-fixed-2.0.0.tgz", @@ -1520,6 +1666,16 @@ "statuses": "1.3.1" } }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "1.0.0", + "jsprim": "1.4.1", + "sshpk": "1.13.1" + } + }, "iconv-lite": { "version": "0.4.19", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", @@ -1564,11 +1720,21 @@ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, "iterall": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.1.3.tgz", @@ -1594,6 +1760,17 @@ "esprima": "4.0.0" } }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, "json-schema-ref-parser": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/json-schema-ref-parser/-/json-schema-ref-parser-3.3.1.tgz", @@ -1707,6 +1884,17 @@ "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, "kareem": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/kareem/-/kareem-1.5.0.tgz", @@ -2178,11 +2366,6 @@ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" }, - "nodemailer": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-3.1.8.tgz", - "integrity": "sha1-/r+sy0vSc2eEc6MJxstLSi88SOM=" - }, "normalize-path": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", @@ -2204,6 +2387,11 @@ "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", "integrity": "sha1-vR/vr2hslrdUda7VGWQS/2DPucE=" }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2355,6 +2543,11 @@ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, "process-nextick-args": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", @@ -2447,6 +2640,35 @@ "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" }, + "request": { + "version": "2.83.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", + "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", + "requires": { + "aws-sign2": "0.7.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.3.1", + "har-validator": "5.0.3", + "hawk": "6.0.2", + "http-signature": "1.2.0", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.17", + "oauth-sign": "0.8.2", + "performance-now": "2.1.0", + "qs": "6.5.1", + "safe-buffer": "5.1.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.4", + "tunnel-agent": "0.6.0", + "uuid": "3.1.0" + } + }, "require_optional": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", @@ -2544,6 +2766,14 @@ "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" }, + "sntp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", + "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", + "requires": { + "hoek": "4.2.1" + } + }, "source-map": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", @@ -2557,6 +2787,21 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, + "sshpk": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", + "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + } + }, "statuses": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", @@ -2580,6 +2825,11 @@ "safe-buffer": "5.1.1" } }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" + }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -2696,6 +2946,21 @@ "os-tmpdir": "1.0.2" } }, + "tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "requires": { + "punycode": "1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + } + } + }, "tslib": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.7.1.tgz", @@ -2743,6 +3008,20 @@ "tslib": "1.7.1" } }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, "type-detect": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.3.tgz", @@ -2846,6 +3125,16 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + } + }, "walkdir": { "version": "0.0.11", "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.0.11.tgz", diff --git a/package.json b/package.json index 8798113f..09906dc6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "1.12.2", + "version": "1.12.3", "description": "TBD", "main": "server/app.js", "scripts": { @@ -26,6 +26,7 @@ "homepage": "https://github.com/HackGT/registration#readme", "private": true, "dependencies": { + "@sendgrid/mail": "^6.2.1", "@types/uuid": "^3.4.2", "ajv": "^5.1.6", "archiver": "^1.3.0", @@ -55,7 +56,6 @@ "mongoose": "^4.10.3", "morgan": "^1.8.2", "multer": "^1.3.0", - "nodemailer": "^3.1.7", "passport": "^0.3.2", "passport-facebook": "^2.1.1", "passport-github2": "^0.1.10", @@ -90,7 +90,6 @@ "@types/morgan": "^1.7.32", "@types/multer": "^1.3.2", "@types/node": "^8.0.8", - "@types/nodemailer": "^1.3.32", "@types/passport": "^0.3.3", "@types/passport-facebook": "^2.1.3", "@types/passport-local": "^1.0.29", diff --git a/server/common.ts b/server/common.ts index 08871ab7..5ff57a5d 100644 --- a/server/common.ts +++ b/server/common.ts @@ -28,10 +28,7 @@ class Config implements IConfig.Main { }; public email: IConfig.Email = { from: "HackGT Team ", - host: "", - username: "", - password: "", - port: 465 + key: "" }; public server: IConfig.Server = { isProduction: false, @@ -159,20 +156,8 @@ class Config implements IConfig.Main { if (process.env.EMAIL_FROM) { this.email.from = process.env.EMAIL_FROM!; } - if (process.env.EMAIL_HOST) { - this.email.host = process.env.EMAIL_HOST!; - } - if (process.env.EMAIL_USERNAME) { - this.email.username = process.env.EMAIL_USERNAME!; - } - if (process.env.EMAIL_PASSWORD) { - this.email.password = process.env.EMAIL_PASSWORD!; - } - if (process.env.EMAIL_PORT) { - let port = parseInt(process.env.EMAIL_PORT!, 10); - if (!isNaN(port) && port > 0) { - this.email.port = port; - } + if (process.env.EMAIL_KEY) { + this.email.key = process.env.EMAIL_KEY!; } // Server if (process.env.PRODUCTION && process.env.PRODUCTION!.toLowerCase() === "true") { @@ -384,31 +369,22 @@ export function unbase64File(filename: string): string { // // Email // -import * as nodemailer from "nodemailer"; +import * as sendgrid from "@sendgrid/mail"; +sendgrid.setApiKey(config.email.key); import * as marked from "marked"; // tslint:disable-next-line:no-var-requires const striptags = require("striptags"); import { IUser, Team, IFormItem } from "./schema"; -export let emailTransporter = nodemailer.createTransport({ - host: config.email.host, - port: config.email.port, - secure: true, - auth: { - user: config.email.username, - pass: config.email.password - } -}); -export async function sendMailAsync(mail: nodemailer.SendMailOptions): Promise { - return new Promise((resolve, reject) => { - emailTransporter.sendMail(mail, (err, info) => { - if (err) { - reject(err); - return; - } - resolve(info); - }); - }); +interface IMailObject { + to: string; + from: string; + subject: string; + html: string; + text: string; +} +export async function sendMailAsync(mail: IMailObject): Promise { + await sendgrid.send(mail); } export function sanitize(input: string): string { if (typeof input !== "string") { diff --git a/server/schema.ts b/server/schema.ts index 03fb0245..65f74cc4 100644 --- a/server/schema.ts +++ b/server/schema.ts @@ -24,10 +24,7 @@ export namespace IConfig { } export interface Email { from: string; - host: string; - username: string; - password: string; - port: number; + key: string; } export interface Server { isProduction: boolean; From 57755ebc33117a79d6104d88c99530770ea4fa23 Mon Sep 17 00:00:00 2001 From: Ehsan Asdar Date: Fri, 2 Mar 2018 11:55:52 -0500 Subject: [PATCH 015/194] Metrics Event Updates (#201) * Add events for confirmation submissions, expose branch name in metrics events * Bump version to 1.12.4 --- package-lock.json | 2 +- package.json | 2 +- server/routes/api/user.ts | 14 +++++++++++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 163a851c..59ec5049 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "1.12.3", + "version": "1.12.4", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 09906dc6..f8b03340 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "1.12.3", + "version": "1.12.4", "description": "TBD", "main": "server/app.js", "scripts": { diff --git a/server/routes/api/user.ts b/server/routes/api/user.ts index 02e4ad7a..85aeed95 100644 --- a/server/routes/api/user.ts +++ b/server/routes/api/user.ts @@ -207,7 +207,9 @@ async function postApplicationBranchHandler(request: express.Request, response: user.applicationSubmitTime = new Date(); // Generate tags for metrics support - let tags: {[index: string]: string} = {}; + let tags: {[index: string]: string} = { + branch: questionBranch.name + }; for (let ele of data) { if (ele && ele.name && ele.value) { tags[ele.name.toString()] = ele.value.toString(); @@ -230,6 +232,16 @@ async function postApplicationBranchHandler(request: express.Request, response: user.confirmationData = data; user.markModified("confirmationData"); user.confirmationSubmitTime = new Date(); + + let tags: {[index: string]: string} = { + branch: questionBranch.name + }; + for (let ele of data) { + if (ele && ele.name && ele.value) { + tags[ele.name.toString()] = ele.value.toString(); + } + } + trackEvent("submitted confirmation", request, user.email, tags); } await user.save(); From 0a2be53bf6203984345059fb48e060024d1b06a6 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Sun, 4 Mar 2018 21:59:57 -0500 Subject: [PATCH 016/194] Fix morgan status code coloring bug (#189) Previously, all status codes would be marked green because the conditionals should have been `else if`s instead of plain `if`s --- server/app.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/app.ts b/server/app.ts index 83d74b0e..6d317193 100644 --- a/server/app.ts +++ b/server/app.ts @@ -43,13 +43,13 @@ morgan.format("hackgt", (tokens, request, response) => { if (response.statusCode >= 500) { statusColorizer = chalk.red; } - if (response.statusCode >= 400) { + else if (response.statusCode >= 400) { statusColorizer = chalk.yellow; } - if (response.statusCode >= 300) { + else if (response.statusCode >= 300) { statusColorizer = chalk.cyan; } - if (response.statusCode >= 200) { + else if (response.statusCode >= 200) { statusColorizer = chalk.green; } From a42c88f46ef5880b05e467cfa7df35b2c5c1a264 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Sun, 4 Mar 2018 22:58:26 -0500 Subject: [PATCH 017/194] Implement loading and saving custom email subjects --- client/admin.html | 3 +++ client/js/admin.ts | 7 +++++-- server/common.ts | 5 +++++ server/routes/api/settings.ts | 30 ++++++++++++++++++++++++++---- 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/client/admin.html b/client/admin.html index 4d8a82f0..0cf93689 100644 --- a/client/admin.html +++ b/client/admin.html @@ -304,6 +304,9 @@

Edit email content

{{/each}} + + +
Rendered HTML and text:
diff --git a/client/js/admin.ts b/client/js/admin.ts index 1ab64560..48335902 100644 --- a/client/js/admin.ts +++ b/client/js/admin.ts @@ -570,6 +570,7 @@ sendAcceptancesButton.addEventListener("click", async e => { declare let SimpleMDE: any; const emailTypeSelect = document.getElementById("email-type") as HTMLSelectElement; +const emailSubject = document.getElementById("email-subject") as HTMLInputElement; let emailRenderedArea: HTMLElement | ShadowRoot = document.getElementById("email-rendered") as HTMLElement; if (document.head.attachShadow) { // Browser supports Shadow DOM @@ -616,8 +617,9 @@ async function emailTypeChange(): Promise { // Load editor content via AJAX try { - let content = (await fetch(`/api/settings/email_content/${emailTypeSelect.value}`, { credentials: "same-origin" }).then(checkStatus).then(parseJSON)).content as string; - markdownEditor.value(content); + let emailSettings: { subject: string; content: string } = await fetch(`/api/settings/email_content/${emailTypeSelect.value}`, { credentials: "same-origin" }).then(checkStatus).then(parseJSON); + emailSubject.value = emailSettings.subject; + markdownEditor.value(emailSettings.content); } catch { markdownEditor.value("Couldn't retrieve email content"); @@ -700,6 +702,7 @@ settingsUpdateButton.addEventListener("click", e => { } let emailContentData = new FormData(); + emailContentData.append("subject", emailSubject.value); emailContentData.append("content", markdownEditor.value()); const defaultOptions: RequestInit = { diff --git a/server/common.ts b/server/common.ts index 5ff57a5d..c1070d45 100644 --- a/server/common.ts +++ b/server/common.ts @@ -376,6 +376,11 @@ import * as marked from "marked"; const striptags = require("striptags"); import { IUser, Team, IFormItem } from "./schema"; +export const defaultEmailSubjects = { + apply: `[${config.eventName}] - Thank you for applying!`, + accept: `[${config.eventName}] - You've been accepted!`, + attend: `[${config.eventName}] - Thank you for RSVPing!` +}; interface IMailObject { to: string; from: string; diff --git a/server/routes/api/settings.ts b/server/routes/api/settings.ts index f0d2a111..66c239f4 100644 --- a/server/routes/api/settings.ts +++ b/server/routes/api/settings.ts @@ -1,7 +1,7 @@ import * as express from "express"; import { - getSetting, updateSetting, setDefaultSettings, renderEmailHTML, renderEmailText + getSetting, updateSetting, setDefaultSettings, renderEmailHTML, renderEmailText, defaultEmailSubjects } from "../../common"; import { isAdmin, uploadHandler @@ -180,19 +180,41 @@ settingsRoutes.route("/branch_roles") settingsRoutes.route("/email_content/:type") .get(isAdmin, async (request, response) => { let content: string; + let subject: string; try { content = await getSetting(`${request.params.type}-email`, false); } - catch (err) { + catch { // Content not set yet content = ""; } + try { + subject = await getSetting(`${request.params.type}-email-subject`, false); + } + catch { + // Subject not set yet + let type: string = request.params.type; + if (type.match(/-apply$/)) { + subject = defaultEmailSubjects.apply; + } + else if (type.match(/-accept$/)) { + subject = defaultEmailSubjects.accept; + } + else if (type.match(/-attend$/)) { + subject = defaultEmailSubjects.attend; + } + else { + subject = ""; + } + } - response.json({ content }); + response.json({ subject, content }); }) .put(isAdmin, uploadHandler.any(), async (request, response) => { - let content = request.body.content as string; + let subject: string = request.body.subject; + let content: string = request.body.content; try { + await updateSetting(`${request.params.type}-email-subject`, subject); await updateSetting(`${request.params.type}-email`, content); response.json({ "success": true From 06b86e50ce199a485272ed636085a56e8691eab7 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Sun, 4 Mar 2018 22:59:09 -0500 Subject: [PATCH 018/194] Send emails with the custom subject --- server/routes/api/user.ts | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/server/routes/api/user.ts b/server/routes/api/user.ts index 85aeed95..9811c849 100644 --- a/server/routes/api/user.ts +++ b/server/routes/api/user.ts @@ -7,7 +7,7 @@ import * as moment from "moment-timezone"; import { STORAGE_ENGINE, formatSize, - config, getSetting, renderEmailHTML, renderEmailText, sendMailAsync + config, getSetting, renderEmailHTML, renderEmailText, sendMailAsync, defaultEmailSubjects } from "../../common"; import { MAX_FILE_SIZE, postParser, uploadHandler, @@ -177,12 +177,19 @@ async function postApplicationBranchHandler(request: express.Request, response: return item; }); // Email the applicant to confirm + let type = requestType === ApplicationType.Application ? "apply" : "attend"; + let emailSubject: string | null; + try { + emailSubject = await getSetting(`${questionBranch.name}-${type}-email-subject`, false); + } + catch { + emailSubject = null; + } let emailMarkdown: string; try { - let type = requestType === ApplicationType.Application ? "apply" : "attend"; emailMarkdown = await getSetting(`${questionBranch.name}-${type}-email`, false); } - catch (err) { + catch { // Content not set yet emailMarkdown = ""; } @@ -195,7 +202,7 @@ async function postApplicationBranchHandler(request: express.Request, response: await sendMailAsync({ from: config.email.from, to: user.email, - subject: `[${config.eventName}] - Thank you for applying!`, + subject: emailSubject || defaultEmailSubjects.apply, html: emailHTML, text: emailText }); @@ -222,7 +229,7 @@ async function postApplicationBranchHandler(request: express.Request, response: await sendMailAsync({ from: config.email.from, to: user.email, - subject: `[${config.eventName}] - Thank you for RSVPing!`, + subject: emailSubject || defaultEmailSubjects.attend, html: emailHTML, text: emailText }); @@ -337,11 +344,18 @@ userRoutes.route("/send_acceptances").post(isAdmin, async (request, response): P let users = await User.find({ "accepted": true, "acceptedEmailSent": { $ne: true } }); for (let user of users) { // Email the applicant about their acceptance + let emailSubject: string | null; + try { + emailSubject = await getSetting(`${user.applicationBranch}-accept-email-subject`, false); + } + catch { + emailSubject = null; + } let emailMarkdown: string; try { emailMarkdown = await getSetting(`${user.applicationBranch}-accept-email`, false); } - catch (err) { + catch { // Content not set yet emailMarkdown = ""; } @@ -352,7 +366,7 @@ userRoutes.route("/send_acceptances").post(isAdmin, async (request, response): P await sendMailAsync({ from: config.email.from, to: user.email, - subject: `[${config.eventName}] - You've been accepted!`, + subject: emailSubject || defaultEmailSubjects.accept, html: emailHTML, text: emailText }); From 2eb9274cb27426193e4a9fd320bfffb04ea2a4ac Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Sat, 3 Mar 2018 15:38:45 -0500 Subject: [PATCH 019/194] Add a second admin settings save button at the top of the page --- client/admin.html | 6 +++++- client/js/admin.ts | 19 ++++++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/client/admin.html b/client/admin.html index 0cf93689..4b814941 100644 --- a/client/admin.html +++ b/client/admin.html @@ -220,6 +220,10 @@

Applicants

Settings

+
+ +
+
@@ -364,7 +368,7 @@

config.json options

- +
diff --git a/client/js/admin.ts b/client/js/admin.ts index 48335902..ee80b8f7 100644 --- a/client/js/admin.ts +++ b/client/js/admin.ts @@ -647,14 +647,23 @@ function parseDateTime(dateTime: string) { let digits = dateTime.split(/\D+/).map(num => parseInt(num, 10)); return new Date(digits[0], digits[1] - 1, digits[2], digits[3], digits[4], digits[5] || 0, digits[6] || 0); } -let settingsUpdateButton = document.querySelector("#settings input[type=submit]") as HTMLInputElement; +let settingsUpdateButtons = document.querySelectorAll("#settings input[type=submit]") as NodeListOf; let settingsForm = document.querySelector("#settings form") as HTMLFormElement; -settingsUpdateButton.addEventListener("click", e => { +for (let i = 0; i < settingsUpdateButtons.length; i++) { + settingsUpdateButtons[i].addEventListener("click", settingsUpdate); +} +function settingsUpdateButtonDisabled(disabled: boolean) { + for (let i = 0; i < settingsUpdateButtons.length; i++) { + settingsUpdateButtons[i].disabled = disabled; + } +} + +function settingsUpdate(e: MouseEvent) { if (!settingsForm.checkValidity() || !settingsForm.dataset.action) { return; } e.preventDefault(); - settingsUpdateButton.disabled = true; + settingsUpdateButtonDisabled(true); let teamsEnabledData = new FormData(); teamsEnabledData.append("enabled", (document.getElementById("teams-enabled") as HTMLInputElement).checked ? "true" : "false"); @@ -742,9 +751,9 @@ settingsUpdateButton.addEventListener("click", e => { window.location.reload(); }).catch(async (err: Error) => { await sweetAlert("Oh no!", err.message, "error"); - settingsUpdateButton.disabled = false; + settingsUpdateButtonDisabled(false); }); -}); +} // // Graphs From 9d6c0ee93acf2e7b9771790196871cf231aa2e60 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Sun, 4 Mar 2018 23:11:26 -0500 Subject: [PATCH 020/194] Bump version to 1.12.5 and year to 2018 --- LICENSE | 2 +- README.md | 2 +- package-lock.json | 2 +- package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/LICENSE b/LICENSE index 3df23cf0..2a71b63b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017 HackGT +Copyright (c) 2018 HackGT Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index db6e7462..ecd817d6 100644 --- a/README.md +++ b/README.md @@ -140,4 +140,4 @@ If you have some time and want to help us out with development, thank you! You c ## License -Copyright © 2017 HackGT. Released under the MIT license. See [LICENSE](LICENSE) for more information. +Copyright © 2018 HackGT. Released under the MIT license. See [LICENSE](LICENSE) for more information. diff --git a/package-lock.json b/package-lock.json index 59ec5049..491b75c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "1.12.4", + "version": "1.12.5", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index f8b03340..7b5b6cef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "1.12.4", + "version": "1.12.5", "description": "TBD", "main": "server/app.js", "scripts": { From ce23fb3ef891072296dd7e48b3b55587277de55f Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Tue, 6 Mar 2018 22:54:09 -0500 Subject: [PATCH 021/194] Bump moment version to avoid possible ReDoS issue (CVE-2017-18214) --- package-lock.json | 10 +++++----- package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 491b75c8..62a56fa6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -208,7 +208,7 @@ "integrity": "sha512-HG4pUK/fTrGY3FerMlINxK74MxdAxkCRYrp5AM+oJ2jLcK0jWUi64ZV15JKwDR4TYLIxrT3y9SVnEWcLPbC/YA==", "dev": true, "requires": { - "moment": "2.18.1" + "moment": "2.21.0" } }, "@types/mongodb": { @@ -2198,16 +2198,16 @@ } }, "moment": { - "version": "2.18.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.18.1.tgz", - "integrity": "sha1-w2GT3Tzhwu7SrbfIAtu8d6gbHA8=" + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.21.0.tgz", + "integrity": "sha512-TCZ36BjURTeFTM/CwRcViQlfkMvL1/vFISuNLO5GkcVm1+QHfbSiNqZuWeMFjj1/3+uAjXswgRk30j1kkLYJBQ==" }, "moment-timezone": { "version": "0.5.13", "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.13.tgz", "integrity": "sha1-mc5cfYJyYusPH3AgRBd/YHRde5A=", "requires": { - "moment": "2.18.1" + "moment": "2.21.0" } }, "mongodb": { diff --git a/package.json b/package.json index 7b5b6cef..fcb3704d 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "json-schema-to-typescript": "^4.4.0", "json2csv": "^3.7.3", "marked": "^0.3.9", - "moment": "^2.18.1", + "moment": "^2.21.0", "moment-timezone": "^0.5.13", "mongoose": "^4.10.3", "morgan": "^1.8.2", From c3df883be7b476b2ee1d0a65f4f89ac53c53d7d6 Mon Sep 17 00:00:00 2001 From: Andrew Dai Date: Mon, 5 Mar 2018 08:48:50 -0500 Subject: [PATCH 022/194] Remove request url matching and use types instead --- server/routes/api/user.ts | 42 +++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/server/routes/api/user.ts b/server/routes/api/user.ts index 9811c849..6e79b41b 100644 --- a/server/routes/api/user.ts +++ b/server/routes/api/user.ts @@ -76,26 +76,33 @@ userRoutes.route("/confirmation/:branch") .delete(isUserOrAdmin, applicationTimeRestriction, deleteApplicationBranchHandler); async function postApplicationBranchHandler(request: express.Request, response: express.Response): Promise { - let requestType: ApplicationType = request.url.match(/\/application\//) ? ApplicationType.Application : ApplicationType.Confirmation; - let user = await User.findOne({uuid: request.params.uuid}) as IUserMongoose; let branchName = request.params.branch as string; - if (requestType === ApplicationType.Application && user.applied && branchName.toLowerCase() !== user.applicationBranch.toLowerCase()) { - response.status(400).json({ - "error": "You can only edit the application branch that you originally submitted" - }); - return; - } - else if (requestType === ApplicationType.Confirmation && user.attending && branchName.toLowerCase() !== user.confirmationBranch.toLowerCase()) { + + // TODO embed branchname in the form so we don't have to do this + let questionBranch = (await Branches.BranchConfig.loadAllBranches()).find(branch => branch.name.toLowerCase() === branchName.toLowerCase()); + if (!questionBranch) { response.status(400).json({ - "error": "You can only edit the confirmation branch that you originally submitted" + "error": "Invalid application branch" }); return; } - // TODO embed branchname in the form so we don't have to do this - let questionBranch = (await Branches.BranchConfig.loadAllBranches()).find(branch => branch.name.toLowerCase() === branchName.toLowerCase()); - if (!questionBranch) { + if (questionBranch instanceof Branches.ApplicationBranch) { + if (user.applied && branchName.toLowerCase() !== user.applicationBranch.toLowerCase()) { + response.status(400).json({ + "error": "You can only edit the application branch that you originally submitted" + }); + return; + } + } else if (questionBranch instanceof Branches.ConfirmationBranch) { + if (user.attending && branchName.toLowerCase() !== user.confirmationBranch.toLowerCase()) { + response.status(400).json({ + "error": "You can only edit the confirmation branch that you originally submitted" + }); + return; + } + } else { response.status(400).json({ "error": "Invalid application branch" }); @@ -177,7 +184,7 @@ async function postApplicationBranchHandler(request: express.Request, response: return item; }); // Email the applicant to confirm - let type = requestType === ApplicationType.Application ? "apply" : "attend"; + let type = questionBranch instanceof Branches.ApplicationBranch ? "apply" : "attend"; let emailSubject: string | null; try { emailSubject = await getSetting(`${questionBranch.name}-${type}-email-subject`, false); @@ -197,7 +204,7 @@ async function postApplicationBranchHandler(request: express.Request, response: let emailHTML = await renderEmailHTML(emailMarkdown, user); let emailText = await renderEmailText(emailHTML, user, true); - if (requestType === ApplicationType.Application) { + if (questionBranch instanceof Branches.ApplicationBranch) { if (!user.applied) { await sendMailAsync({ from: config.email.from, @@ -223,8 +230,8 @@ async function postApplicationBranchHandler(request: express.Request, response: } } trackEvent("submitted application", request, user.email, tags); - } - else if (requestType === ApplicationType.Confirmation) { + + } else if (questionBranch instanceof Branches.ConfirmationBranch) { if (!user.attending) { await sendMailAsync({ from: config.email.from, @@ -333,6 +340,7 @@ async function updateUserStatus(user: IUserMongoose, status: ("accepted" | "no-d } else if (status === "accepted") { user.accepted = true; let applicationBranch = (await Branches.BranchConfig.loadBranchFromDB(user.applicationBranch)) as Branches.ApplicationBranch; + // TODO andrew: add no confirmation branch support here user.confirmationDeadlines = ((await Branches.BranchConfig.loadAllBranches("Confirmation")) as Branches.ConfirmationBranch[]) .filter(c => c.usesRollingDeadline) .filter(c => applicationBranch.confirmationBranches.indexOf(c.name) > -1); From d68a23f7706a297f8ba522b59bfc924c5fac238c Mon Sep 17 00:00:00 2001 From: Andrew Dai Date: Mon, 5 Mar 2018 15:16:02 -0500 Subject: [PATCH 023/194] Implement auto-accept for after application submitted --- server/branch.ts | 5 +++++ server/routes/api/user.ts | 12 ++++++++++++ server/schema.ts | 4 ++++ 3 files changed, 21 insertions(+) diff --git a/server/branch.ts b/server/branch.ts index c08fa322..89110a6b 100644 --- a/server/branch.ts +++ b/server/branch.ts @@ -177,11 +177,16 @@ abstract class TimedBranch extends NoopBranch { export class ApplicationBranch extends TimedBranch { public readonly type: keyof QuestionBranchTypes = "Application"; + public autoAccept: boolean; + + public noConfirmation: boolean; public confirmationBranches: string[]; protected async loadSettings(): Promise { await super.loadSettings(); let branchConfig = await QuestionBranchConfig.findOne({ "name": this.name }); + this.autoAccept = branchConfig && branchConfig.settings && branchConfig.settings.autoAccept || false; + this.noConfirmation = branchConfig && branchConfig.settings && branchConfig.settings.noConfirmation || false; this.confirmationBranches = branchConfig && branchConfig.settings && branchConfig.settings.confirmationBranches || []; } protected serializeSettings(): QuestionBranchSettings { diff --git a/server/routes/api/user.ts b/server/routes/api/user.ts index 6e79b41b..dfcb5ecb 100644 --- a/server/routes/api/user.ts +++ b/server/routes/api/user.ts @@ -160,6 +160,7 @@ async function postApplicationBranchHandler(request: express.Request, response: if (errored) { return; } + try { let data = rawData as IFormItem[]; // Nulls are only inserted when an error has occurred // Move files to permanent, requested location @@ -231,6 +232,10 @@ async function postApplicationBranchHandler(request: express.Request, response: } trackEvent("submitted application", request, user.email, tags); + if (questionBranch.autoAccept) { + await updateUserStatus(user, "accepted"); + } + } else if (questionBranch instanceof Branches.ConfirmationBranch) { if (!user.attending) { await sendMailAsync({ @@ -340,6 +345,13 @@ async function updateUserStatus(user: IUserMongoose, status: ("accepted" | "no-d } else if (status === "accepted") { user.accepted = true; let applicationBranch = (await Branches.BranchConfig.loadBranchFromDB(user.applicationBranch)) as Branches.ApplicationBranch; + + // Do not send "you are accepted" emails to auto-accept branches + // Admins should add the information into the "post-apply" email + if (applicationBranch.autoAccept) { + user.acceptedEmailSent = true; + } + // TODO andrew: add no confirmation branch support here user.confirmationDeadlines = ((await Branches.BranchConfig.loadAllBranches("Confirmation")) as Branches.ConfirmationBranch[]) .filter(c => c.usesRollingDeadline) diff --git a/server/schema.ts b/server/schema.ts index 65f74cc4..15622e09 100644 --- a/server/schema.ts +++ b/server/schema.ts @@ -231,6 +231,8 @@ type QuestionBranchType = "Application" | "Confirmation" | "Noop"; export interface QuestionBranchSettings { open?: Date; // Used by all except noop close?: Date; // Used by all except noop + autoAccept?: boolean; // Used by application branch + noConfirmation?: boolean; // Used by application branch confirmationBranches?: string[]; // Used by application branch usesRollingDeadline?: boolean; // Used by confirmation branch } @@ -253,6 +255,8 @@ export const QuestionBranchConfig = mongoose.model Date: Mon, 5 Mar 2018 15:41:20 -0500 Subject: [PATCH 024/194] Implement no-confirmation workflow after applicant is accepted --- client/js/admin.ts | 3 +++ server/routes/api/user.ts | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/client/js/admin.ts b/client/js/admin.ts index ee80b8f7..19ae48b7 100644 --- a/client/js/admin.ts +++ b/client/js/admin.ts @@ -127,6 +127,9 @@ class UserEntries { userStatus = `Accepted (${user.application.type})`; } if (user.applied && user.accepted && user.attending) { + userStatus = `Accepted (${user.application.type}) / Confirmed`; + } + if (user.applied && user.accepted && user.attending && user.confirmation) { userStatus = `Accepted (${user.application.type}) / Confirmed (${user.confirmation.type})`; } node.querySelector("td.status")!.textContent = userStatus; diff --git a/server/routes/api/user.ts b/server/routes/api/user.ts index dfcb5ecb..b6f13103 100644 --- a/server/routes/api/user.ts +++ b/server/routes/api/user.ts @@ -352,7 +352,11 @@ async function updateUserStatus(user: IUserMongoose, status: ("accepted" | "no-d user.acceptedEmailSent = true; } - // TODO andrew: add no confirmation branch support here + // Automatically mark user as "attending" if application branch skips confirmation + if (applicationBranch.noConfirmation) { + user.attending = true; + } + user.confirmationDeadlines = ((await Branches.BranchConfig.loadAllBranches("Confirmation")) as Branches.ConfirmationBranch[]) .filter(c => c.usesRollingDeadline) .filter(c => applicationBranch.confirmationBranches.indexOf(c.name) > -1); From f9b0461d9bf2bf0fc845349903fe382cc8ef4e22 Mon Sep 17 00:00:00 2001 From: Andrew Dai Date: Tue, 6 Mar 2018 01:02:59 -0500 Subject: [PATCH 025/194] Start refactor of branch (apply/confirm) redirect middleware --- server/middleware.ts | 26 ++++++++++++++++++++++++++ server/routes/templates.ts | 35 ++++++++++++++++------------------- 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/server/middleware.ts b/server/middleware.ts index df3e6823..afb02f8a 100644 --- a/server/middleware.ts +++ b/server/middleware.ts @@ -130,6 +130,32 @@ import { ICommonTemplate } from "./schema"; export enum ApplicationType { Application, Confirmation } + +export function branchRedirector(requestType: ApplicationType): (request: express.Request, response: express.Response, next: express.NextFunction) => Promise { + return async (request: express.Request, response: express.Response, next: express.NextFunction) => { + // TODO: fix branch names so they have a machine ID and human label + let user = request.user as IUser; + if (requestType === ApplicationType.Application && user.accepted) { + // Do not redirect of application branch has "no confirmation" enabled + // ^ This is inferred from a user with `attending=true` and empty string for `confirmationBranch` + if (!(user.attending && !user.confirmationBranch)) { + response.redirect("/confirm"); + return; + } + } + if (requestType === ApplicationType.Confirmation && (!user.accepted || !user.applied)) { + response.redirect("/apply"); + return; + } + if (requestType === ApplicationType.Confirmation && user.attending && !user.confirmationBranch) { + response.redirect("/apply"); + return; + } + + next(); + }; +} + export async function timeLimited(request: express.Request, response: express.Response, next: express.NextFunction) { let requestType: ApplicationType = request.url.match(/^\/apply/) ? ApplicationType.Application : ApplicationType.Confirmation; diff --git a/server/routes/templates.ts b/server/routes/templates.ts index f22ecee8..6d9aaaf5 100644 --- a/server/routes/templates.ts +++ b/server/routes/templates.ts @@ -11,7 +11,7 @@ import { } from "../common"; import { authenticateWithRedirect, - timeLimited, ApplicationType + branchRedirector, timeLimited, ApplicationType } from "../middleware"; import { IUser, IUserMongoose, User, @@ -311,30 +311,23 @@ templateRoutes.route("/team").get(authenticateWithRedirect, async (request, resp templateRoutes.route("/apply").get( authenticateWithRedirect, + branchRedirector(ApplicationType.Application), timeLimited, applicationHandler(ApplicationType.Application) ); templateRoutes.route("/confirm").get( authenticateWithRedirect, + branchRedirector(ApplicationType.Confirmation), timeLimited, applicationHandler(ApplicationType.Confirmation) ); function applicationHandler(requestType: ApplicationType): (request: express.Request, response: express.Response) => Promise { return async (request: express.Request, response: express.Response) => { - // TODO: fix branch names so they have a machine ID and human label let user = request.user as IUser; - if (requestType === ApplicationType.Application && user.accepted) { - response.redirect("/confirm"); - return; - } - if (requestType === ApplicationType.Confirmation && !user.accepted) { - response.redirect("/apply"); - return; - } + // TODO: integrate this logic with `middleware.branchRedirector` and `middleware.timeLimited` let questionBranches: string[] = []; - // Filter to only show application / confirmation branches // NOTE: this assumes the user is still able to apply as this type at this point if (requestType === ApplicationType.Application) { @@ -392,20 +385,24 @@ function applicationHandler(requestType: ApplicationType): (request: express.Req }; } -templateRoutes.route("/apply/:branch").get(authenticateWithRedirect, timeLimited, applicationBranchHandler); -templateRoutes.route("/confirm/:branch").get(authenticateWithRedirect, timeLimited, applicationBranchHandler); +templateRoutes.route("/apply/:branch").get( + authenticateWithRedirect, + branchRedirector(ApplicationType.Application), + timeLimited, + applicationBranchHandler +); +templateRoutes.route("/confirm/:branch").get( + authenticateWithRedirect, + branchRedirector(ApplicationType.Confirmation), + timeLimited, + applicationBranchHandler +); async function applicationBranchHandler(request: express.Request, response: express.Response) { let requestType: ApplicationType = request.url.match(/^\/apply/) ? ApplicationType.Application : ApplicationType.Confirmation; let user = request.user as IUser; - // Redirect to application screen if confirmation was requested and user has not applied/been accepted - if (requestType === ApplicationType.Confirmation && (!user.accepted || !user.applied)) { - response.redirect("/apply"); - return; - } - // Redirect directly to branch if there is an existing application or confirmation let branchName = request.params.branch as string; if (requestType === ApplicationType.Application && user.applied && branchName.toLowerCase() !== user.applicationBranch.toLowerCase()) { From ed73b2cfc6743069a9b0acaca66560c903224a4c Mon Sep 17 00:00:00 2001 From: Andrew Dai Date: Tue, 6 Mar 2018 01:03:44 -0500 Subject: [PATCH 026/194] Hide and display proper options for users depending on new branch options --- client/index.html | 2 ++ client/partials/sidebar.html | 19 +++++++++++++------ server/routes/templates.ts | 4 +++- server/schema.ts | 1 + 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/client/index.html b/client/index.html index 44a44f97..91dfdf39 100644 --- a/client/index.html +++ b/client/index.html @@ -43,6 +43,7 @@

Incomplete

{{#if user.attending}}

You're all set!

Application type: {{user.applicationBranch}}

+ {{#unless skipConfirmation}}

Confirmation type: {{user.confirmationBranch}}

{{#if confirmationStatus.areOpen}}

Feel free to edit your RSVP at any time. However, once RSVPing closes on {{confirmationClose}}, you will not be able to edit it anymore.

@@ -50,6 +51,7 @@

Incomplete

{{else}}

RSVPing closed on {{confirmationClose}}.

{{/if}} + {{/unless}}

We look forward to seeing you!

{{#if settings.qrEnabled}} diff --git a/client/partials/sidebar.html b/client/partials/sidebar.html index f25e7e30..21576c2c 100644 --- a/client/partials/sidebar.html +++ b/client/partials/sidebar.html @@ -6,23 +6,30 @@

{{siteTitle}}

{{#if user.accepted}} - Confirmation - + {{#if user.attending}} + {{#if user.confirmation}} + Confirmation + {{else}} + Application + {{/if}} + {{else}} + Confirmation + {{/if}} {{else}} Application - {{/if}} - + + {{#if settings.teamsEnabled}} Team {{/if}} - + {{#if user.admin}} Admin {{/if}} - + Log out

{{user.name}} ({{user.email}})

diff --git a/server/routes/templates.ts b/server/routes/templates.ts index 6d9aaaf5..64c2ab9a 100644 --- a/server/routes/templates.ts +++ b/server/routes/templates.ts @@ -127,8 +127,10 @@ templateRoutes.route("/").get(authenticateWithRedirect, async (request, response let user = request.user as IUser; let applyBranches: Branches.ApplicationBranch[]; + let skipConfirmation = false; if (user.applicationBranch) { applyBranches = [(await Branches.BranchConfig.loadBranchFromDB(user.applicationBranch))] as Branches.ApplicationBranch[]; + skipConfirmation = applyBranches[0].noConfirmation; } else { applyBranches = (await Branches.BranchConfig.loadAllBranches("Application") as Branches.ApplicationBranch[]); } @@ -207,7 +209,6 @@ templateRoutes.route("/").get(authenticateWithRedirect, async (request, response teamsEnabled: await getSetting("teamsEnabled"), qrEnabled: await getSetting("qrEnabled") }, - applicationOpen: formatMoment(applicationOpenDate), applicationClose: formatMoment(applicationCloseDate), applicationStatus: { @@ -215,6 +216,7 @@ templateRoutes.route("/").get(authenticateWithRedirect, async (request, response beforeOpen: applicationOpenDate ? moment().isBefore(applicationOpenDate) : true, afterClose: applicationCloseDate ? moment().isAfter(applicationCloseDate) : false }, + skipConfirmation, confirmationOpen: formatMoment(confirmationOpenDate), confirmationClose: formatMoment(confirmationCloseDate), confirmationStatus: { diff --git a/server/schema.ts b/server/schema.ts index 15622e09..d5a85591 100644 --- a/server/schema.ts +++ b/server/schema.ts @@ -280,6 +280,7 @@ export interface IIndexTemplate extends ICommonTemplate { beforeOpen: boolean; afterClose: boolean; }; + skipConfirmation: boolean; confirmationOpen: string; confirmationClose: string; confirmationStatus: { From 720cc2dcd7ec45e06db0aeaff595aa637d7b16bf Mon Sep 17 00:00:00 2001 From: Andrew Dai Date: Tue, 6 Mar 2018 01:04:55 -0500 Subject: [PATCH 027/194] Clear user `accepted` and `attending` values when deleting application --- server/routes/api/user.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/routes/api/user.ts b/server/routes/api/user.ts index b6f13103..c5d8f7b4 100644 --- a/server/routes/api/user.ts +++ b/server/routes/api/user.ts @@ -282,6 +282,8 @@ async function deleteApplicationBranchHandler(request: express.Request, response let user = await User.findOne({uuid: request.params.uuid}) as IUserMongoose; if (requestType === ApplicationType.Application) { user.applied = false; + user.accepted = false; + user.attending = false; user.applicationBranch = ""; user.applicationData = []; user.markModified("applicationData"); From 1fdbd6007ea28456a9e48ca34442fd61ebe4e45a Mon Sep 17 00:00:00 2001 From: Andrew Dai Date: Tue, 6 Mar 2018 11:57:18 -0500 Subject: [PATCH 028/194] Add auto-accept and skip-confirmation options in admin panel --- client/admin.html | 25 +++++++++++++----- client/js/admin.ts | 50 +++++++++++++++++++++++++++++++---- server/branch.ts | 6 +++-- server/routes/api/settings.ts | 4 ++- server/routes/templates.ts | 4 ++- server/schema.ts | 2 ++ 6 files changed, 76 insertions(+), 15 deletions(-) diff --git a/client/admin.html b/client/admin.html index 4b814941..e49ab149 100644 --- a/client/admin.html +++ b/client/admin.html @@ -243,13 +243,24 @@

{{this.name}}

+ +
+
+ + +
+
+ + +
+
{{#each ../settings.branches.confirmation}} -
- - -
+
+ + +
{{/each}}
@@ -298,8 +309,10 @@

Edit email content

+
+ + +
diff --git a/client/js/admin.ts b/client/js/admin.ts index 9c0dac32..64ce58b0 100644 --- a/client/js/admin.ts +++ b/client/js/admin.ts @@ -662,15 +662,30 @@ for (let input of Array.from(skipConfirmationToggles)) { }; } -function uncheckSkipConfirmation(applicationBranch: string) { - (document.querySelector(`.branch-role[data-name="${applicationBranch}"] input[type="checkbox"].noConfirmation`) as HTMLInputElement).checked = false; +// Uncheck "skip confirmation" option when a confirmation branch is selected +function setClickSkipConfirmation(applicationBranch: string, checked: boolean) { + let checkbox = (document.querySelector(`.branch-role[data-name="${applicationBranch}"] input[type="checkbox"].noConfirmation`) as HTMLInputElement); + if (checkbox.checked !== checked) { + checkbox.click(); + } } let availableConfirmationBranchCheckboxes = document.querySelectorAll(".branch-role fieldset.availableConfirmationBranches input[type=\"checkbox\"]"); for (let input of Array.from(availableConfirmationBranchCheckboxes)) { let checkbox = input as HTMLInputElement; checkbox.onclick = () => { if (checkbox.checked && checkbox.dataset.branchName !== undefined) { - uncheckSkipConfirmation((checkbox.dataset.branchName as string)); + setClickSkipConfirmation((checkbox.dataset.branchName as string), false); + } + }; +} + +// Select "skip confirmation" option when "allow anonymous" option is selected +let allowAnonymousCheckboxes = document.querySelectorAll(".branch-role input[type=\"checkbox\"].allowAnonymous"); +for (let input of Array.from(allowAnonymousCheckboxes)) { + let checkbox = input as HTMLInputElement; + checkbox.onclick = () => { + if (checkbox.checked && checkbox.dataset.branchName !== undefined) { + setClickSkipConfirmation((checkbox.dataset.branchName as string), true); } }; } @@ -720,6 +735,7 @@ function settingsUpdate(e: MouseEvent) { confirmationBranches?: string[]; noConfirmation?: boolean; autoAccept?: boolean; + allowAnonymous?: boolean; } = {role: branchRole}; // TODO this should probably be typed (not just strings) if (branchRole !== "Noop") { @@ -738,10 +754,13 @@ function settingsUpdate(e: MouseEvent) { } branchData.confirmationBranches = allowedConfirmationBranches; + // This operation is all or nothing because it will only error if a branch was just made into an Application branch try { + branchData.allowAnonymous = (branchRoles[i].querySelector("fieldset.applicationBranchOptions input[type=\"checkbox\"].allowAnonymous") as HTMLInputElement).checked; branchData.autoAccept = (branchRoles[i].querySelector("fieldset.applicationBranchOptions input[type=\"checkbox\"].autoAccept") as HTMLInputElement).checked; branchData.noConfirmation = (branchRoles[i].querySelector("fieldset.applicationBranchOptions input[type=\"checkbox\"].noConfirmation") as HTMLInputElement).checked; } catch { + branchData.allowAnonymous = false; branchData.autoAccept = false; branchData.noConfirmation = false; } diff --git a/server/branch.ts b/server/branch.ts index 80132d88..073c0264 100644 --- a/server/branch.ts +++ b/server/branch.ts @@ -177,6 +177,8 @@ export abstract class TimedBranch extends NoopBranch { export class ApplicationBranch extends TimedBranch { public readonly type: keyof QuestionBranchTypes = "Application"; + public allowAnonymous: boolean; + public autoAccept: boolean; public noConfirmation: boolean; @@ -185,6 +187,7 @@ export class ApplicationBranch extends TimedBranch { protected async loadSettings(): Promise { await super.loadSettings(); let branchConfig = await QuestionBranchConfig.findOne({ "name": this.name }); + this.allowAnonymous = branchConfig && branchConfig.settings && branchConfig.settings.allowAnonymous || false; this.autoAccept = branchConfig && branchConfig.settings && branchConfig.settings.autoAccept || false; this.noConfirmation = branchConfig && branchConfig.settings && branchConfig.settings.noConfirmation || false; this.confirmationBranches = branchConfig && branchConfig.settings && branchConfig.settings.confirmationBranches || []; @@ -192,9 +195,10 @@ export class ApplicationBranch extends TimedBranch { protected serializeSettings(): QuestionBranchSettings { return { ...super.serializeSettings(), - confirmationBranches: this.confirmationBranches, + allowAnonymous: this.allowAnonymous, autoAccept: this.autoAccept, - noConfirmation: this.noConfirmation + noConfirmation: this.noConfirmation, + confirmationBranches: this.confirmationBranches }; } } diff --git a/server/routes/api/settings.ts b/server/routes/api/settings.ts index 0eee46d3..4e3a4fc3 100644 --- a/server/routes/api/settings.ts +++ b/server/routes/api/settings.ts @@ -157,6 +157,7 @@ settingsRoutes.route("/branch_roles") // Set available confirmation branches (if application branch) if (branch instanceof Branches.ApplicationBranch) { branch.confirmationBranches = branchData.confirmationBranches || []; + branch.allowAnonymous = branchData.allowAnonymous || false; branch.autoAccept = branchData.autoAccept || false; branch.noConfirmation = branchData.noConfirmation || false; } diff --git a/server/routes/templates.ts b/server/routes/templates.ts index 5d42de61..c2562126 100644 --- a/server/routes/templates.ts +++ b/server/routes/templates.ts @@ -582,9 +582,10 @@ templateRoutes.route("/admin").get(authenticateWithRedirect, async (request, res name: branch.name, open: branch.open.toISOString(), close: branch.close.toISOString(), - confirmationBranches: branch.confirmationBranches, + allowAnonymous: branch.allowAnonymous, autoAccept: branch.autoAccept, - noConfirmation: branch.noConfirmation + noConfirmation: branch.noConfirmation, + confirmationBranches: branch.confirmationBranches }; }), confirmation: confirmationBranches.map((branch: Branches.ConfirmationBranch) => { diff --git a/server/schema.ts b/server/schema.ts index db534c46..6dff2587 100644 --- a/server/schema.ts +++ b/server/schema.ts @@ -231,6 +231,7 @@ type QuestionBranchType = "Application" | "Confirmation" | "Noop"; export interface QuestionBranchSettings { open?: Date; // Used by all except noop close?: Date; // Used by all except noop + allowAnonymous?: boolean; // Used by application branch autoAccept?: boolean; // Used by application branch noConfirmation?: boolean; // Used by application branch confirmationBranches?: string[]; // Used by application branch @@ -255,6 +256,7 @@ export const QuestionBranchConfig = mongoose.model Date: Tue, 6 Mar 2018 14:26:17 -0500 Subject: [PATCH 030/194] Refactor branch redirection middleware --- server/middleware.ts | 68 ++++++++++++++++++++++++++++++++------ server/routes/templates.ts | 33 +----------------- 2 files changed, 58 insertions(+), 43 deletions(-) diff --git a/server/middleware.ts b/server/middleware.ts index afb02f8a..25858b9a 100644 --- a/server/middleware.ts +++ b/server/middleware.ts @@ -135,21 +135,67 @@ export function branchRedirector(requestType: ApplicationType): (request: expres return async (request: express.Request, response: express.Response, next: express.NextFunction) => { // TODO: fix branch names so they have a machine ID and human label let user = request.user as IUser; - if (requestType === ApplicationType.Application && user.accepted) { - // Do not redirect of application branch has "no confirmation" enabled - // ^ This is inferred from a user with `attending=true` and empty string for `confirmationBranch` - if (!(user.attending && !user.confirmationBranch)) { - response.redirect("/confirm"); + if (requestType === ApplicationType.Application) { + if (user.accepted) { + // Do not redirect of application branch has "no confirmation" enabled + // ^ This is inferred from a user with `attending=true` and empty string for `confirmationBranch` + if (!(user.attending && !user.confirmationBranch)) { + response.redirect("/confirm"); + return; + } + } + } + + if (requestType === ApplicationType.Confirmation) { + if (!user.accepted || !user.applied) { + response.redirect("/apply"); + return; + } + if (user.attending && !user.confirmationBranch) { + response.redirect("/apply"); return; } } - if (requestType === ApplicationType.Confirmation && (!user.accepted || !user.applied)) { - response.redirect("/apply"); - return; + + let questionBranches: string[]; + if (requestType === ApplicationType.Application) { + questionBranches = (await BranchConfig.getOpenBranches("Application")).map(branch => branch.name.toLowerCase()); + } else { + questionBranches = ((await BranchConfig.loadBranchFromDB(user.applicationBranch)) as ApplicationBranch).confirmationBranches.map(branchName => branchName.toLowerCase()); } - if (requestType === ApplicationType.Confirmation && user.attending && !user.confirmationBranch) { - response.redirect("/apply"); - return; + + if (request.params.branch) { + let branchName = request.params.branch as string; + if (requestType === ApplicationType.Application) { + // Redirect directly to branch if there is an existing application or confirmation + if (user.applied && branchName.toLowerCase() !== user.applicationBranch.toLowerCase()) { + response.redirect(`/apply/${encodeURIComponent(user.applicationBranch.toLowerCase())}`); + return; + } + let questionBranch = questionBranches.find(branch => branch === branchName.toLowerCase())!; + if (!questionBranch) { + response.redirect("/apply"); + return; + } + } + if (requestType === ApplicationType.Confirmation) { + // Redirect directly to branch if there is an existing application or confirmation + if (user.attending && branchName.toLowerCase() !== user.confirmationBranch.toLowerCase()) { + response.redirect(`/confirm/${encodeURIComponent(user.confirmationBranch.toLowerCase())}`); + return; + } + if (questionBranches.indexOf(branchName.toLowerCase()) === -1 && !user.attending) { + response.redirect("/confirm"); + return; + } + } + } else { + if (questionBranches.length === 1) { + const uriBranch = encodeURIComponent(questionBranches[0]); + const redirPath = requestType === ApplicationType.Application ? "apply" : "confirm"; + response.redirect(`/${redirPath}/${uriBranch}`); + return; + } } next(); diff --git a/server/routes/templates.ts b/server/routes/templates.ts index c2562126..a661023a 100644 --- a/server/routes/templates.ts +++ b/server/routes/templates.ts @@ -361,13 +361,6 @@ function applicationHandler(requestType: ApplicationType): (request: express.Req } } - // If there's only one path, redirect to that - if (questionBranches.length === 1) { - const uriBranch = encodeURIComponent(questionBranches[0]); - const redirPath = requestType === ApplicationType.Application ? "apply" : "confirm"; - response.redirect(`/${redirPath}/${uriBranch}`); - return; - } let templateData: IRegisterBranchChoiceTemplate = { siteTitle: config.eventName, user, @@ -405,35 +398,11 @@ async function applicationBranchHandler(request: express.Request, response: expr let user = request.user as IUser; - // Redirect directly to branch if there is an existing application or confirmation let branchName = request.params.branch as string; - if (requestType === ApplicationType.Application && user.applied && branchName.toLowerCase() !== user.applicationBranch.toLowerCase()) { - response.redirect(`/apply/${encodeURIComponent(user.applicationBranch.toLowerCase())}`); - return; - } - else if (requestType === ApplicationType.Confirmation && user.attending && branchName.toLowerCase() !== user.confirmationBranch.toLowerCase()) { - response.redirect(`/confirm/${encodeURIComponent(user.confirmationBranch.toLowerCase())}`); - return; - } - - // Redirect to confirmation selection screen if no match is found - if (requestType === ApplicationType.Confirmation) { - // We know that `user.applicationBranch` exists because the user has applied and was accepted - let allowedBranches = ((await Branches.BranchConfig.loadBranchFromDB(user.applicationBranch)) as Branches.ApplicationBranch).confirmationBranches; - allowedBranches = allowedBranches.map(allowedBranchName => allowedBranchName.toLowerCase()); - if (allowedBranches.indexOf(branchName.toLowerCase()) === -1 && !user.attending) { - response.redirect("/confirm"); - return; - } - } let questionBranches = await Branches.BranchConfig.loadAllBranches(); - let questionBranch = questionBranches.find(branch => branch.name.toLowerCase() === branchName.toLowerCase())!; - if (!questionBranch) { - response.status(400).send("Invalid application branch"); - return; - } + // tslint:disable:no-string-literal let questionData = await Promise.all(questionBranch.questions.map(async question => { let savedValue = user[requestType === ApplicationType.Application ? "applicationData" : "confirmationData"].find(item => item.name === question.name); From b676ed5e9929ea2c9342e18e9deb3a821977809c Mon Sep 17 00:00:00 2001 From: Andrew Dai Date: Wed, 7 Mar 2018 00:43:23 -0500 Subject: [PATCH 031/194] Use `addEventListener` when setting click handlers --- client/js/admin.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/js/admin.ts b/client/js/admin.ts index 64ce58b0..61a7468e 100644 --- a/client/js/admin.ts +++ b/client/js/admin.ts @@ -655,11 +655,11 @@ function uncheckConfirmationBranches(applicationBranch: string) { let skipConfirmationToggles = document.querySelectorAll(".branch-role input[type=\"checkbox\"].noConfirmation"); for (let input of Array.from(skipConfirmationToggles)) { let checkbox = input as HTMLInputElement; - checkbox.onclick = () => { + checkbox.addEventListener("click", () => { if (checkbox.checked && checkbox.dataset.branchName !== undefined) { uncheckConfirmationBranches((checkbox.dataset.branchName as string)); } - }; + }); } // Uncheck "skip confirmation" option when a confirmation branch is selected @@ -672,11 +672,11 @@ function setClickSkipConfirmation(applicationBranch: string, checked: boolean) { let availableConfirmationBranchCheckboxes = document.querySelectorAll(".branch-role fieldset.availableConfirmationBranches input[type=\"checkbox\"]"); for (let input of Array.from(availableConfirmationBranchCheckboxes)) { let checkbox = input as HTMLInputElement; - checkbox.onclick = () => { + checkbox.addEventListener("click", () => { if (checkbox.checked && checkbox.dataset.branchName !== undefined) { setClickSkipConfirmation((checkbox.dataset.branchName as string), false); } - }; + }); } // Select "skip confirmation" option when "allow anonymous" option is selected From 92107a065f071174f14b7bc45b52093d439be17f Mon Sep 17 00:00:00 2001 From: Andrew Dai Date: Wed, 7 Mar 2018 01:25:09 -0500 Subject: [PATCH 032/194] Implement anonymous registration --- client/application.html | 11 ++ client/confirmation.html | 3 + client/js/application.ts | 14 ++- client/partials/sidebar.html | 2 + server/app.ts | 3 +- server/middleware.ts | 73 ++++++++++++-- server/routes/api/user.ts | 108 +++++++++++++------- server/routes/templates.ts | 188 ++++++++++++++++++++--------------- server/schema.ts | 1 + 9 files changed, 274 insertions(+), 129 deletions(-) diff --git a/client/application.html b/client/application.html index 7d06b9a8..a20d0962 100644 --- a/client/application.html +++ b/client/application.html @@ -23,7 +23,14 @@

Apply: {{branch}}

Set data-action and not action so that verification of the form still occurs but the form is actually submitted via an XHR. Running event.preventDefault() in the submit button's click handler also disables validation for some reason --> + {{#unless unauthenticated}}
+ {{else}} + + + + {{/unless}} + {{#each questionData}} {{#if this.textContent}} {{{this.textContent}}} @@ -85,5 +92,9 @@

Apply: {{branch}}

{{/sidebar}} + diff --git a/client/confirmation.html b/client/confirmation.html index a2deafef..c0890844 100644 --- a/client/confirmation.html +++ b/client/confirmation.html @@ -85,5 +85,8 @@

RSVP: {{branch}}

{{/sidebar}} + diff --git a/client/js/application.ts b/client/js/application.ts index eea62066..1a9f8e1e 100644 --- a/client/js/application.ts +++ b/client/js/application.ts @@ -2,7 +2,10 @@ enum FormType { Application, Confirmation } -let formType = window.location.pathname.match(/^\/apply/) ? FormType.Application : FormType.Confirmation; +declare let formTypeString: keyof typeof FormType; +let formType = FormType[formTypeString]; + +declare let unauthenticated: (boolean | undefined); let form = document.querySelector("form") as HTMLFormElement | null; let submitButton = document.querySelector("form input[type=submit]") as HTMLInputElement; @@ -19,11 +22,16 @@ submitButton.addEventListener("click", e => { body: new FormData(form) }).then(checkStatus).then(parseJSON).then(async () => { let successMessage: string = formType === FormType.Application ? - "Your application has been saved. Feel free to come back here and edit it at any time." : + "Your application has been saved." + (!unauthenticated ? "Feel free to come back here and edit it at any time." : "") : "Your RSVP has been saved. Feel free to come back here and edit it at any time. We look forward to seeing you!"; await sweetAlert("Awesome!", successMessage, "success"); - window.location.assign("/"); + + if (unauthenticated) { + (document.querySelector("form") as HTMLFormElement).reset(); + } else { + window.location.assign("/"); + } }).catch(async (err: Error) => { await sweetAlert("Oh no!", err.message, "error"); submitButton.disabled = false; diff --git a/client/partials/sidebar.html b/client/partials/sidebar.html index 21576c2c..0683ae7c 100644 --- a/client/partials/sidebar.html +++ b/client/partials/sidebar.html @@ -1,6 +1,7 @@
{{> @partial-block }} diff --git a/server/app.ts b/server/app.ts index 6d317193..9f1d5ca9 100644 --- a/server/app.ts +++ b/server/app.ts @@ -100,8 +100,9 @@ app.use((request, response, next) => { let apiRouter = express.Router(); // API routes go here -import {userRoutes} from "./routes/api/user"; +import {userRoutes, registrationRoutes} from "./routes/api/user"; apiRouter.use("/user/:uuid", userRoutes); +apiRouter.use("/registration", registrationRoutes); import {settingsRoutes} from "./routes/api/settings"; apiRouter.use("/settings", settingsRoutes); diff --git a/server/middleware.ts b/server/middleware.ts index 25858b9a..94fe8c80 100644 --- a/server/middleware.ts +++ b/server/middleware.ts @@ -2,10 +2,11 @@ import * as fs from "fs"; import * as moment from "moment-timezone"; import * as path from "path"; import * as os from "os"; +import * as uuid from "uuid/v4"; import { config, getSetting, readFileAsync, STATIC_ROOT } from "./common"; import { getOpenConfirmationBranches, BranchConfig, ApplicationBranch, ConfirmationBranch } from "./branch"; -import { IUser } from "./schema"; +import { IUser, User } from "./schema"; // // Express middleware @@ -131,8 +132,32 @@ export enum ApplicationType { Application, Confirmation } +export async function onlyAllowAnonymousBranch(request: express.Request, response: express.Response, next: express.NextFunction) { + let branchName = request.params.branch as string; + console.log(branchName); + let questionBranches = (await BranchConfig.getOpenBranches("Application")).filter(br => { + + console.log(br.name.toLowerCase()); + return br.name.toLowerCase() === branchName.toLowerCase(); + }); + console.log(questionBranches); + if (questionBranches.length !== 1) { + response.redirect("/apply"); + return; + } + + let branch = questionBranches[0] as ApplicationBranch; + if (!branch.allowAnonymous) { + response.redirect("/apply"); + return; + } + + next(); +} + export function branchRedirector(requestType: ApplicationType): (request: express.Request, response: express.Response, next: express.NextFunction) => Promise { return async (request: express.Request, response: express.Response, next: express.NextFunction) => { + // TODO refactor redirection code and consolidate here (#206) // TODO: fix branch names so they have a machine ID and human label let user = request.user as IUser; if (requestType === ApplicationType.Application) { @@ -165,7 +190,7 @@ export function branchRedirector(requestType: ApplicationType): (request: expres } if (request.params.branch) { - let branchName = request.params.branch as string; + let branchName = (request.params.branch as string).toLowerCase(); if (requestType === ApplicationType.Application) { // Redirect directly to branch if there is an existing application or confirmation if (user.applied && branchName.toLowerCase() !== user.applicationBranch.toLowerCase()) { @@ -190,6 +215,20 @@ export function branchRedirector(requestType: ApplicationType): (request: expres } } } else { + if (requestType === ApplicationType.Application && user.applied && user.applicationBranch) { + questionBranches = [user.applicationBranch]; + } + + // Redirect for applications on "skip confirmation" branches + if (requestType === ApplicationType.Confirmation && user.attending && !user.confirmationBranch && user.applicationBranch) { + questionBranches = [user.applicationBranch]; + requestType = ApplicationType.Application; + } + + if (requestType === ApplicationType.Confirmation && user.attending && user.confirmationBranch) { + questionBranches = [user.confirmationBranch]; + } + if (questionBranches.length === 1) { const uriBranch = encodeURIComponent(questionBranches[0]); const redirPath = requestType === ApplicationType.Application ? "apply" : "confirm"; @@ -205,19 +244,33 @@ export function branchRedirector(requestType: ApplicationType): (request: expres export async function timeLimited(request: express.Request, response: express.Response, next: express.NextFunction) { let requestType: ApplicationType = request.url.match(/^\/apply/) ? ApplicationType.Application : ApplicationType.Confirmation; - let user = request.user as IUser; + let user: IUser; + if (request.isAuthenticated()) { + user = request.user as IUser; + } else { + user = new User({ + uuid: uuid(), + email: "" + }); + } let openBranches: (ApplicationBranch | ConfirmationBranch)[]; if (requestType === ApplicationType.Application) { openBranches = await BranchConfig.getOpenBranches("Application"); - if (user.applied) { - openBranches = openBranches.filter((b => b.name === user.applicationBranch)); + if (request.isAuthenticated() && user.applied && user.applicationBranch) { + let applicationBranch = user.applicationBranch; + openBranches = openBranches.filter((b => b.name === applicationBranch)); } - } - else { - openBranches = await getOpenConfirmationBranches(request.user as IUser); - if (user.attending) { - openBranches = openBranches.filter((b => b.name === user.confirmationBranch)); + } else { + if (request.isAuthenticated()) { + openBranches = await getOpenConfirmationBranches(user); + } else { + openBranches = await BranchConfig.getOpenBranches("Confirmation"); + } + + if (request.isAuthenticated() && user.attending && user.confirmationBranch) { + let confirmationBranch = user.confirmationBranch; + openBranches = openBranches.filter((b => b.name === confirmationBranch)); } } diff --git a/server/routes/api/user.ts b/server/routes/api/user.ts index c5d8f7b4..c75f16b8 100644 --- a/server/routes/api/user.ts +++ b/server/routes/api/user.ts @@ -3,6 +3,7 @@ import * as express from "express"; import * as json2csv from "json2csv"; import * as archiver from "archiver"; import * as moment from "moment-timezone"; +import * as uuid from "uuid/v4"; import { STORAGE_ENGINE, @@ -22,6 +23,7 @@ import { import * as Branches from "../../branch"; export let userRoutes = express.Router({ "mergeParams": true }); +export let registrationRoutes = express.Router({ "mergeParams": true }); let postApplicationBranchErrorHandler: express.ErrorRequestHandler = (err, request, response, next) => { if (err.code === "LIMIT_FILE_SIZE") { @@ -34,49 +36,85 @@ let postApplicationBranchErrorHandler: express.ErrorRequestHandler = (err, reque } }; -let applicationTimeRestriction: express.RequestHandler = async (request, response, next) => { - let requestType: ApplicationType = request.url.match(/\/application\//) ? ApplicationType.Application : ApplicationType.Confirmation; - let branchName = request.params.branch as string; - let branch = (await Branches.BranchConfig.loadAllBranches()).find(b => b.name.toLowerCase() === branchName.toLowerCase()) as (Branches.ApplicationBranch | Branches.ConfirmationBranch); - if (!branch) { - response.status(400).json({ +// TODO what is the difference between this and `middleware.timeLimited`? - related to #206 +function applicationTimeRestriction(requestType: ApplicationType): express.RequestHandler { + return async (request, response, next) => { + let branchName = request.params.branch as string; + let branch = (await Branches.BranchConfig.loadAllBranches()).find(b => b.name.toLowerCase() === branchName.toLowerCase()) as (Branches.ApplicationBranch | Branches.ConfirmationBranch); + if (!branch) { + response.status(400).json({ "error": "Invalid application branch" - }); - return; - } + }); + return; + } - let user = request.user as IUserMongoose; + let user = request.user as IUserMongoose; - let openDate = branch.open; - let closeDate = branch.close; - if (requestType === ApplicationType.Confirmation && user.confirmationDeadlines) { - let times = user.confirmationDeadlines.find((d) => d.name === branch.name); - if (times) { - openDate = times.open; - closeDate = times.close; + let openDate = branch.open; + let closeDate = branch.close; + if (requestType === ApplicationType.Confirmation && user.confirmationDeadlines) { + let times = user.confirmationDeadlines.find((d) => d.name === branch.name); + if (times) { + openDate = times.open; + closeDate = times.close; + } } - } - if (moment().isBetween(openDate, closeDate) || request.user.isAdmin) { - next(); - } - else { - response.status(408).json({ - "error": `${requestType === ApplicationType.Application ? "Applications" : "Confirmations"} are currently closed` - }); - return; - } -}; + if (moment().isBetween(openDate, closeDate) || request.user.isAdmin) { + next(); + } + else { + response.status(408).json({ + "error": `${requestType === ApplicationType.Application ? "Applications" : "Confirmations"} are currently closed` + }); + return; + } + }; +} -userRoutes.route("/application/:branch") - .post(isUserOrAdmin, applicationTimeRestriction, postParser, uploadHandler.any(), postApplicationBranchErrorHandler, postApplicationBranchHandler) - .delete(isUserOrAdmin, applicationTimeRestriction, deleteApplicationBranchHandler); -userRoutes.route("/confirmation/:branch") - .post(isUserOrAdmin, applicationTimeRestriction, postParser, uploadHandler.any(), postApplicationBranchErrorHandler, postApplicationBranchHandler) - .delete(isUserOrAdmin, applicationTimeRestriction, deleteApplicationBranchHandler); +registrationRoutes.route("/:branch").post( + applicationTimeRestriction(ApplicationType.Application), + postParser, + uploadHandler.any(), + postApplicationBranchErrorHandler, + postApplicationBranchHandler +); + +userRoutes.route("/application/:branch").post( + isUserOrAdmin, + applicationTimeRestriction(ApplicationType.Application), + postParser, + uploadHandler.any(), + postApplicationBranchErrorHandler, + postApplicationBranchHandler +).delete( + isUserOrAdmin, + applicationTimeRestriction, + deleteApplicationBranchHandler); +userRoutes.route("/confirmation/:branch").post( + isUserOrAdmin, + applicationTimeRestriction(ApplicationType.Confirmation), + postParser, + uploadHandler.any(), + postApplicationBranchErrorHandler, + postApplicationBranchHandler +).delete( + isUserOrAdmin, + applicationTimeRestriction, + deleteApplicationBranchHandler +); async function postApplicationBranchHandler(request: express.Request, response: express.Response): Promise { - let user = await User.findOne({uuid: request.params.uuid}) as IUserMongoose; + let user: IUserMongoose; + if (request.isAuthenticated()) { + user = await User.findOne({uuid: request.params.uuid}) as IUserMongoose; + } else { + user = new User({ + uuid: uuid(), + email: request.body["anonymous-registration-email"] + }) as IUserMongoose; + } + let branchName = request.params.branch as string; // TODO embed branchname in the form so we don't have to do this diff --git a/server/routes/templates.ts b/server/routes/templates.ts index a661023a..e9e678d0 100644 --- a/server/routes/templates.ts +++ b/server/routes/templates.ts @@ -4,6 +4,7 @@ import * as express from "express"; import * as Handlebars from "handlebars"; import * as moment from "moment-timezone"; import * as bowser from "bowser"; +import * as uuid from "uuid/v4"; import { STATIC_ROOT, STORAGE_ENGINE, @@ -11,13 +12,14 @@ import { } from "../common"; import { authenticateWithRedirect, - branchRedirector, timeLimited, ApplicationType + onlyAllowAnonymousBranch, branchRedirector, timeLimited, ApplicationType } from "../middleware"; import { IUser, IUserMongoose, User, ITeamMongoose, Team, IIndexTemplate, ILoginTemplate, IAdminTemplate, ITeamTemplate, - IRegisterBranchChoiceTemplate, IRegisterTemplate, StatisticEntry + IRegisterBranchChoiceTemplate, IRegisterTemplate, StatisticEntry, + IFormItem } from "../schema"; import * as Branches from "../branch"; @@ -380,115 +382,141 @@ function applicationHandler(requestType: ApplicationType): (request: express.Req }; } +templateRoutes.route("/register/:branch").get( + onlyAllowAnonymousBranch, + timeLimited, + applicationBranchHandler(ApplicationType.Application) +); + templateRoutes.route("/apply/:branch").get( authenticateWithRedirect, branchRedirector(ApplicationType.Application), timeLimited, - applicationBranchHandler + applicationBranchHandler(ApplicationType.Application) ); templateRoutes.route("/confirm/:branch").get( authenticateWithRedirect, branchRedirector(ApplicationType.Confirmation), timeLimited, - applicationBranchHandler + applicationBranchHandler(ApplicationType.Confirmation) ); -async function applicationBranchHandler(request: express.Request, response: express.Response) { - let requestType: ApplicationType = request.url.match(/^\/apply/) ? ApplicationType.Application : ApplicationType.Confirmation; +function applicationBranchHandler(requestType: ApplicationType): (request: express.Request, response: express.Response) => Promise { + return async (request: express.Request, response: express.Response) => { + let user: IUser; + if (request.user) { + user = request.user as IUser; + } else { + user = new User({ + uuid: uuid(), + email: "" + }); + } - let user = request.user as IUser; + let branchName = request.params.branch as string; - let branchName = request.params.branch as string; + let questionBranches = await Branches.BranchConfig.loadAllBranches(); + let questionBranch = questionBranches.find(branch => branch.name.toLowerCase() === branchName.toLowerCase())!; - let questionBranches = await Branches.BranchConfig.loadAllBranches(); - let questionBranch = questionBranches.find(branch => branch.name.toLowerCase() === branchName.toLowerCase())!; + // tslint:disable:no-string-literal + let questionData = await Promise.all(questionBranch.questions.map(async question => { + let savedValue: IFormItem | undefined; + if (user) { + savedValue = user[requestType === ApplicationType.Application ? "applicationData" : "confirmationData"].find(item => item.name === question.name); + } - // tslint:disable:no-string-literal - let questionData = await Promise.all(questionBranch.questions.map(async question => { - let savedValue = user[requestType === ApplicationType.Application ? "applicationData" : "confirmationData"].find(item => item.name === question.name); - if (question.type === "checkbox" || question.type === "radio" || question.type === "select") { - question["multi"] = true; - question["selected"] = question.options.map(option => { - if (savedValue && Array.isArray(savedValue.value)) { - return savedValue.value.indexOf(option) !== -1; - } - else if (savedValue !== undefined) { - return option === savedValue.value; - } - return false; - }); - if (question.hasOther && savedValue) { - if (!Array.isArray(savedValue.value)) { - // Select / radio buttons - if (savedValue.value !== null && question.options.indexOf(savedValue.value as string) === -1) { - question["selected"][question.options.length - 1] = true; // The "Other" pushed earlier - question["otherSelected"] = true; - question["otherValue"] = savedValue.value; + if (question.type === "checkbox" || question.type === "radio" || question.type === "select") { + question["multi"] = true; + question["selected"] = question.options.map(option => { + if (savedValue && Array.isArray(savedValue.value)) { + return savedValue.value.indexOf(option) !== -1; } - } - else { - // Checkboxes - for (let value of savedValue.value as string[]) { - if (question.options.indexOf(value) === -1) { + else if (savedValue !== undefined) { + return option === savedValue.value; + } + return false; + }); + if (question.hasOther && savedValue) { + if (!Array.isArray(savedValue.value)) { + // Select / radio buttons + if (savedValue.value !== null && question.options.indexOf(savedValue.value as string) === -1) { question["selected"][question.options.length - 1] = true; // The "Other" pushed earlier question["otherSelected"] = true; - question["otherValue"] = value; + question["otherValue"] = savedValue.value; + } + } + else { + // Checkboxes + for (let value of savedValue.value as string[]) { + if (question.options.indexOf(value) === -1) { + question["selected"][question.options.length - 1] = true; // The "Other" pushed earlier + question["otherSelected"] = true; + question["otherValue"] = value; + } } } } } - } - else { - question["multi"] = false; - } - if (savedValue && question.type === "file" && savedValue.value) { - savedValue = { + else { + question["multi"] = false; + } + if (savedValue && question.type === "file" && savedValue.value) { + savedValue = { ...savedValue, - value: (savedValue.value as Express.Multer.File).originalname - }; - } - question["value"] = savedValue ? savedValue.value : ""; + value: (savedValue.value as Express.Multer.File).originalname + }; + } + question["value"] = savedValue ? savedValue.value : ""; + if (questionBranch.textBlocks) { + let textContent: string = (await Promise.all(questionBranch.textBlocks.filter(text => text.for === question.name).map(async text => { + return `<${text.type}>${await renderMarkdown(text.content, { sanitize: true }, true)}`; + }))).join("\n"); + question["textContent"] = textContent; + } + + return question; + })); + // tslint:enable:no-string-literal + + let endText: string = ""; if (questionBranch.textBlocks) { - let textContent: string = (await Promise.all(questionBranch.textBlocks.filter(text => text.for === question.name).map(async text => { - return `<${text.type}>${await renderMarkdown(text.content, { sanitize: true }, true)}`; + endText = (await Promise.all(questionBranch.textBlocks.filter(text => text.for === "end").map(async text => { + return `<${text.type} style="font-size: 90%; text-align: center;">${await renderMarkdown(text.content, { sanitize: true }, true)}`; }))).join("\n"); - question["textContent"] = textContent; } - return question; - })); - // tslint:enable:no-string-literal - - let endText: string = ""; - if (questionBranch.textBlocks) { - endText = (await Promise.all(questionBranch.textBlocks.filter(text => text.for === "end").map(async text => { - return `<${text.type} style="font-size: 90%; text-align: center;">${await renderMarkdown(text.content, { sanitize: true }, true)}`; - }))).join("\n"); - } + if (request.isAuthenticated()) { + let thisUser = await User.findById(user._id) as IUserMongoose; + // TODO this is a bug - dates are wrong + if (requestType === ApplicationType.Application && !thisUser.applicationStartTime) { + thisUser.applicationStartTime = new Date(); + } + else if (requestType === ApplicationType.Confirmation && !thisUser.confirmationStartTime) { + thisUser.confirmationStartTime = new Date(); + } + await thisUser.save(); + } - let thisUser = await User.findById(user._id) as IUserMongoose; - if (requestType === ApplicationType.Application) { - thisUser.applicationStartTime = new Date(); - } - else if (requestType === ApplicationType.Confirmation) { - thisUser.confirmationStartTime = new Date(); - } - await thisUser.save(); + let templateData: IRegisterTemplate = { + siteTitle: config.eventName, + unauthenticated: !request.isAuthenticated(), + user: request.user, + settings: { + teamsEnabled: await getSetting("teamsEnabled"), + qrEnabled: await getSetting("qrEnabled") + }, + branch: questionBranch.name, + questionData, + endText + }; - let templateData: IRegisterTemplate = { - siteTitle: config.eventName, - user: request.user, - settings: { - teamsEnabled: await getSetting("teamsEnabled"), - qrEnabled: await getSetting("qrEnabled") - }, - branch: questionBranch.name, - questionData, - endText + if (requestType === ApplicationType.Application) { + response.send(registerTemplate(templateData)); + } else if (requestType === ApplicationType.Confirmation) { + response.send(confirmTemplate(templateData)); + } }; - - response.send(requestType === ApplicationType.Application ? registerTemplate(templateData) : confirmTemplate(templateData)); } templateRoutes.route("/admin").get(authenticateWithRedirect, async (request, response) => { diff --git a/server/schema.ts b/server/schema.ts index 6dff2587..e1619641 100644 --- a/server/schema.ts +++ b/server/schema.ts @@ -319,6 +319,7 @@ export interface IRegisterTemplate extends ICommonTemplate { branch: string; questionData: Questions; endText: string; + unauthenticated: boolean; } export interface ResponseCount { "response": string; From d13d6db5f585b890776a5e39fd3b00755ff75337 Mon Sep 17 00:00:00 2001 From: Andrew Dai Date: Wed, 7 Mar 2018 01:47:34 -0500 Subject: [PATCH 033/194] Display registration link in admin panel for public branches --- client/admin.html | 14 ++++++++++---- client/js/admin.ts | 9 +++++++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/client/admin.html b/client/admin.html index 1d54a55f..b3e24097 100644 --- a/client/admin.html +++ b/client/admin.html @@ -245,10 +245,10 @@

{{this.name}}

-
- - -
+
+ + +
@@ -274,6 +274,12 @@

{{this.name}}

+
{{/each}} {{#each settings.branches.confirmation}} diff --git a/client/js/admin.ts b/client/js/admin.ts index 61a7468e..2e4b3a63 100644 --- a/client/js/admin.ts +++ b/client/js/admin.ts @@ -680,12 +680,17 @@ for (let input of Array.from(availableConfirmationBranchCheckboxes)) { } // Select "skip confirmation" option when "allow anonymous" option is selected +// Hide/show public link when "allow anonymous" is clicked let allowAnonymousCheckboxes = document.querySelectorAll(".branch-role input[type=\"checkbox\"].allowAnonymous"); for (let input of Array.from(allowAnonymousCheckboxes)) { let checkbox = input as HTMLInputElement; checkbox.onclick = () => { - if (checkbox.checked && checkbox.dataset.branchName !== undefined) { - setClickSkipConfirmation((checkbox.dataset.branchName as string), true); + if (checkbox.dataset.branchName !== undefined) { + let branchName = checkbox.dataset.branchName as string; + if (checkbox.checked) { + setClickSkipConfirmation(branchName, true); + } + (document.querySelector(`.branch-role[data-name="${branchName}"] .public-link`) as HTMLDivElement).hidden = !checkbox.checked; } }; } From 3963457ecf0fed2f69a0bed0ad47f2dfded5ef91 Mon Sep 17 00:00:00 2001 From: Andrew Dai Date: Wed, 7 Mar 2018 01:48:02 -0500 Subject: [PATCH 034/194] Clarify location of config file --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ecd817d6..868eeeaa 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ Visit `http://localhost:3000` and you're good to go! A [Dockerfile](Dockerfile) is provided for convenience. -Configuration should normally be done by editing the `config.json` file. Environment variables take precedence over `config.json` and should be used when those options need to be overridden or `config.json` can't be used for some reason (e.g. certain deployment scenarios). +Configuration should normally be done by editing the `server/config/config.json` file. Environment variables take precedence over `config.json` and should be used when those options need to be overridden or `config.json` can't be used for some reason (e.g. certain deployment scenarios). ### OAuth IDs and secrets From 6462123b832fbb52afc65413818295b630a5c345 Mon Sep 17 00:00:00 2001 From: Andrew Dai Date: Thu, 8 Mar 2018 18:37:45 -0500 Subject: [PATCH 035/194] Encode URLs in admin screen --- client/admin.html | 2 +- server/routes/templates.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/client/admin.html b/client/admin.html index b3e24097..4d06b710 100644 --- a/client/admin.html +++ b/client/admin.html @@ -277,7 +277,7 @@

{{this.name}}

diff --git a/server/routes/templates.ts b/server/routes/templates.ts index e9e678d0..b8730ec2 100644 --- a/server/routes/templates.ts +++ b/server/routes/templates.ts @@ -122,6 +122,7 @@ Handlebars.registerHelper("toJSONString", (stat: StatisticEntry): string => { Handlebars.registerHelper("removeSpaces", (input: string): string => { return input.replace(/ /g, "-"); }); +Handlebars.registerHelper("encodeURI", encodeURI); Handlebars.registerPartial("sidebar", fs.readFileSync(path.resolve(STATIC_ROOT, "partials", "sidebar.html"), "utf8")); templateRoutes.route("/dashboard").get((request, response) => response.redirect("/")); From a0669b1f8683607f17c42515be8dcff351ba8618 Mon Sep 17 00:00:00 2001 From: Andrew Dai Date: Thu, 8 Mar 2018 19:10:11 -0500 Subject: [PATCH 036/194] Protect walkin/anonymous registration by enforcing admin user --- client/application.html | 5 + server/middleware.ts | 2 - server/routes/api/user.ts | 369 +++++++++++++++++++------------------ server/routes/templates.ts | 21 ++- 4 files changed, 201 insertions(+), 196 deletions(-) diff --git a/client/application.html b/client/application.html index a20d0962..70567a6f 100644 --- a/client/application.html +++ b/client/application.html @@ -83,12 +83,17 @@

Apply: {{branch}}

{{/each}} {{{endText}}}
+ {{#unless unauthenticated}} {{#if user.applied}} {{else}} {{/if}} + {{else}} + + {{/unless}} +
{{/sidebar}} diff --git a/server/middleware.ts b/server/middleware.ts index 94fe8c80..6efc56c8 100644 --- a/server/middleware.ts +++ b/server/middleware.ts @@ -134,13 +134,11 @@ export enum ApplicationType { export async function onlyAllowAnonymousBranch(request: express.Request, response: express.Response, next: express.NextFunction) { let branchName = request.params.branch as string; - console.log(branchName); let questionBranches = (await BranchConfig.getOpenBranches("Application")).filter(br => { console.log(br.name.toLowerCase()); return br.name.toLowerCase() === branchName.toLowerCase(); }); - console.log(questionBranches); if (questionBranches.length !== 1) { response.redirect("/apply"); return; diff --git a/server/routes/api/user.ts b/server/routes/api/user.ts index c75f16b8..f11705ba 100644 --- a/server/routes/api/user.ts +++ b/server/routes/api/user.ts @@ -73,11 +73,12 @@ function applicationTimeRestriction(requestType: ApplicationType): express.Reque } registrationRoutes.route("/:branch").post( + isAdmin, applicationTimeRestriction(ApplicationType.Application), postParser, uploadHandler.any(), postApplicationBranchErrorHandler, - postApplicationBranchHandler + postApplicationBranchHandler(true) ); userRoutes.route("/application/:branch").post( @@ -86,7 +87,7 @@ userRoutes.route("/application/:branch").post( postParser, uploadHandler.any(), postApplicationBranchErrorHandler, - postApplicationBranchHandler + postApplicationBranchHandler(false) ).delete( isUserOrAdmin, applicationTimeRestriction, @@ -97,223 +98,223 @@ userRoutes.route("/confirmation/:branch").post( postParser, uploadHandler.any(), postApplicationBranchErrorHandler, - postApplicationBranchHandler + postApplicationBranchHandler(false) ).delete( isUserOrAdmin, applicationTimeRestriction, deleteApplicationBranchHandler ); +function postApplicationBranchHandler(anonymous: boolean): express.Handler { + return async (request: express.Request, response: express.Response): Promise => { + let user: IUserMongoose; + if (anonymous) { + user = new User({ + uuid: uuid(), + email: request.body["anonymous-registration-email"] + }) as IUserMongoose; + } else { + user = await User.findOne({uuid: request.params.uuid}) as IUserMongoose; + } -async function postApplicationBranchHandler(request: express.Request, response: express.Response): Promise { - let user: IUserMongoose; - if (request.isAuthenticated()) { - user = await User.findOne({uuid: request.params.uuid}) as IUserMongoose; - } else { - user = new User({ - uuid: uuid(), - email: request.body["anonymous-registration-email"] - }) as IUserMongoose; - } - - let branchName = request.params.branch as string; - - // TODO embed branchname in the form so we don't have to do this - let questionBranch = (await Branches.BranchConfig.loadAllBranches()).find(branch => branch.name.toLowerCase() === branchName.toLowerCase()); - if (!questionBranch) { - response.status(400).json({ - "error": "Invalid application branch" - }); - return; - } + let branchName = request.params.branch as string; - if (questionBranch instanceof Branches.ApplicationBranch) { - if (user.applied && branchName.toLowerCase() !== user.applicationBranch.toLowerCase()) { + // TODO embed branchname in the form so we don't have to do this + let questionBranch = (await Branches.BranchConfig.loadAllBranches()).find(branch => branch.name.toLowerCase() === branchName.toLowerCase()); + if (!questionBranch) { response.status(400).json({ - "error": "You can only edit the application branch that you originally submitted" + "error": "Invalid application branch" }); return; } - } else if (questionBranch instanceof Branches.ConfirmationBranch) { - if (user.attending && branchName.toLowerCase() !== user.confirmationBranch.toLowerCase()) { + + if (questionBranch instanceof Branches.ApplicationBranch) { + if (user.applied && branchName.toLowerCase() !== user.applicationBranch.toLowerCase()) { + response.status(400).json({ + "error": "You can only edit the application branch that you originally submitted" + }); + return; + } + } else if (questionBranch instanceof Branches.ConfirmationBranch) { + if (user.attending && branchName.toLowerCase() !== user.confirmationBranch.toLowerCase()) { + response.status(400).json({ + "error": "You can only edit the confirmation branch that you originally submitted" + }); + return; + } + } else { response.status(400).json({ - "error": "You can only edit the confirmation branch that you originally submitted" + "error": "Invalid application branch" }); return; } - } else { - response.status(400).json({ - "error": "Invalid application branch" - }); - return; - } - let unchangedFiles: string[] = []; - let errored: boolean = false; // Used because .map() can't be broken out of - let rawData: (IFormItem | null)[] = questionBranch.questions.map(question => { - if (errored) { - return null; - } - let files = request.files as Express.Multer.File[]; - let preexistingFile: boolean = question.type === "file" && user.applicationData && user.applicationData.some(entry => entry.name === question.name && !!entry.value); - - if (question.required && !request.body[question.name] && !files.find(file => file.fieldname === question.name)) { - // Required field not filled in - if (preexistingFile) { - let previousValue = user.applicationData.find(entry => entry.name === question.name && !!entry.value)!.value as Express.Multer.File; - unchangedFiles.push(previousValue.filename); - return { - "name": question.name, - "type": "file", - "value": previousValue - }; - } - else { - errored = true; - response.status(400).json({ - "error": `'${question.label}' is a required field` - }); + let unchangedFiles: string[] = []; + let errored: boolean = false; // Used because .map() can't be broken out of + let rawData: (IFormItem | null)[] = questionBranch.questions.map(question => { + if (errored) { return null; } - } - if ((question.type === "select" || question.type === "radio") && Array.isArray(request.body[question.name]) && question.hasOther) { - // "Other" option selected - request.body[question.name] = request.body[question.name].pop(); - } - else if (question.type === "checkbox" && question.hasOther) { - if (!request.body[question.name]) { - request.body[question.name] = []; + let files = request.files as Express.Multer.File[]; + let preexistingFile: boolean = question.type === "file" && user.applicationData && user.applicationData.some(entry => entry.name === question.name && !!entry.value); + + if (question.required && !request.body[question.name] && !files.find(file => file.fieldname === question.name)) { + // Required field not filled in + if (preexistingFile) { + let previousValue = user.applicationData.find(entry => entry.name === question.name && !!entry.value)!.value as Express.Multer.File; + unchangedFiles.push(previousValue.filename); + return { + "name": question.name, + "type": "file", + "value": previousValue + }; + } + else { + errored = true; + response.status(400).json({ + "error": `'${question.label}' is a required field` + }); + return null; + } } - if (!Array.isArray(request.body[question.name])) { - request.body[question.name] = [request.body[question.name]]; + if ((question.type === "select" || question.type === "radio") && Array.isArray(request.body[question.name]) && question.hasOther) { + // "Other" option selected + request.body[question.name] = request.body[question.name].pop(); } - // Filter out "other" option - request.body[question.name] = (request.body[question.name] as string[]).filter(value => value !== "Other"); - } - return { - "name": question.name, - "type": question.type, - "value": request.body[question.name] || files.find(file => file.fieldname === question.name) - }; - }); - if (errored) { - return; - } - - try { - let data = rawData as IFormItem[]; // Nulls are only inserted when an error has occurred - // Move files to permanent, requested location - await Promise.all(data - .map(item => item.value) - .filter(possibleFile => possibleFile !== null && typeof possibleFile === "object" && !Array.isArray(possibleFile)) - .map((file: Express.Multer.File): Promise => { - if (unchangedFiles.indexOf(file.filename) === -1) { - return STORAGE_ENGINE.saveFile(file.path, file.filename); + else if (question.type === "checkbox" && question.hasOther) { + if (!request.body[question.name]) { + request.body[question.name] = []; } - else { - return Promise.resolve(); + if (!Array.isArray(request.body[question.name])) { + request.body[question.name] = [request.body[question.name]]; } - }) - ); - // Set the proper file locations in the data object - data = data.map(item => { - if (item.value !== null && typeof item.value === "object" && !Array.isArray(item.value)) { - item.value.destination = STORAGE_ENGINE.uploadRoot; - item.value.path = path.join(STORAGE_ENGINE.uploadRoot, item.value.filename); + // Filter out "other" option + request.body[question.name] = (request.body[question.name] as string[]).filter(value => value !== "Other"); } - return item; + return { + "name": question.name, + "type": question.type, + "value": request.body[question.name] || files.find(file => file.fieldname === question.name) + }; }); - // Email the applicant to confirm - let type = questionBranch instanceof Branches.ApplicationBranch ? "apply" : "attend"; - let emailSubject: string | null; - try { - emailSubject = await getSetting(`${questionBranch.name}-${type}-email-subject`, false); - } - catch { - emailSubject = null; - } - let emailMarkdown: string; - try { - emailMarkdown = await getSetting(`${questionBranch.name}-${type}-email`, false); - } - catch { - // Content not set yet - emailMarkdown = ""; + if (errored) { + return; } - let emailHTML = await renderEmailHTML(emailMarkdown, user); - let emailText = await renderEmailText(emailHTML, user, true); - - if (questionBranch instanceof Branches.ApplicationBranch) { - if (!user.applied) { - await sendMailAsync({ - from: config.email.from, - to: user.email, - subject: emailSubject || defaultEmailSubjects.apply, - html: emailHTML, - text: emailText - }); - } - user.applied = true; - user.applicationBranch = questionBranch.name; - user.applicationData = data; - user.markModified("applicationData"); - user.applicationSubmitTime = new Date(); - - // Generate tags for metrics support - let tags: {[index: string]: string} = { - branch: questionBranch.name - }; - for (let ele of data) { - if (ele && ele.name && ele.value) { - tags[ele.name.toString()] = ele.value.toString(); + try { + let data = rawData as IFormItem[]; // Nulls are only inserted when an error has occurred + // Move files to permanent, requested location + await Promise.all(data + .map(item => item.value) + .filter(possibleFile => possibleFile !== null && typeof possibleFile === "object" && !Array.isArray(possibleFile)) + .map((file: Express.Multer.File): Promise => { + if (unchangedFiles.indexOf(file.filename) === -1) { + return STORAGE_ENGINE.saveFile(file.path, file.filename); + } + else { + return Promise.resolve(); + } + }) + ); + // Set the proper file locations in the data object + data = data.map(item => { + if (item.value !== null && typeof item.value === "object" && !Array.isArray(item.value)) { + item.value.destination = STORAGE_ENGINE.uploadRoot; + item.value.path = path.join(STORAGE_ENGINE.uploadRoot, item.value.filename); } + return item; + }); + // Email the applicant to confirm + let type = questionBranch instanceof Branches.ApplicationBranch ? "apply" : "attend"; + let emailSubject: string | null; + try { + emailSubject = await getSetting(`${questionBranch.name}-${type}-email-subject`, false); } - trackEvent("submitted application", request, user.email, tags); - - if (questionBranch.autoAccept) { - await updateUserStatus(user, "accepted"); + catch { + emailSubject = null; } - - } else if (questionBranch instanceof Branches.ConfirmationBranch) { - if (!user.attending) { - await sendMailAsync({ - from: config.email.from, - to: user.email, - subject: emailSubject || defaultEmailSubjects.attend, - html: emailHTML, - text: emailText - }); + let emailMarkdown: string; + try { + emailMarkdown = await getSetting(`${questionBranch.name}-${type}-email`, false); + } + catch { + // Content not set yet + emailMarkdown = ""; } - user.attending = true; - user.confirmationBranch = questionBranch.name; - user.confirmationData = data; - user.markModified("confirmationData"); - user.confirmationSubmitTime = new Date(); - let tags: {[index: string]: string} = { - branch: questionBranch.name - }; - for (let ele of data) { - if (ele && ele.name && ele.value) { - tags[ele.name.toString()] = ele.value.toString(); + let emailHTML = await renderEmailHTML(emailMarkdown, user); + let emailText = await renderEmailText(emailHTML, user, true); + + if (questionBranch instanceof Branches.ApplicationBranch) { + if (!user.applied) { + await sendMailAsync({ + from: config.email.from, + to: user.email, + subject: emailSubject || defaultEmailSubjects.apply, + html: emailHTML, + text: emailText + }); } + user.applied = true; + user.applicationBranch = questionBranch.name; + user.applicationData = data; + user.markModified("applicationData"); + user.applicationSubmitTime = new Date(); + + // Generate tags for metrics support + let tags: {[index: string]: string} = { + branch: questionBranch.name + }; + for (let ele of data) { + if (ele && ele.name && ele.value) { + tags[ele.name.toString()] = ele.value.toString(); + } + } + trackEvent("submitted application", request, user.email, tags); + + if (questionBranch.autoAccept) { + await updateUserStatus(user, "accepted"); + } + + } else if (questionBranch instanceof Branches.ConfirmationBranch) { + if (!user.attending) { + await sendMailAsync({ + from: config.email.from, + to: user.email, + subject: emailSubject || defaultEmailSubjects.attend, + html: emailHTML, + text: emailText + }); + } + user.attending = true; + user.confirmationBranch = questionBranch.name; + user.confirmationData = data; + user.markModified("confirmationData"); + user.confirmationSubmitTime = new Date(); + + let tags: {[index: string]: string} = { + branch: questionBranch.name + }; + for (let ele of data) { + if (ele && ele.name && ele.value) { + tags[ele.name.toString()] = ele.value.toString(); + } + } + trackEvent("submitted confirmation", request, user.email, tags); } - trackEvent("submitted confirmation", request, user.email, tags); - } - await user.save(); - response.status(200).json({ - "success": true - }); - } - catch (err) { - console.error(err); - response.status(500).json({ - "error": "An error occurred while saving your application" - }); - } + await user.save(); + response.status(200).json({ + "success": true + }); + } + catch (err) { + console.error(err); + response.status(500).json({ + "error": "An error occurred while saving your application" + }); + } + }; } - async function deleteApplicationBranchHandler(request: express.Request, response: express.Response) { let requestType: ApplicationType = request.url.match(/\/application\//) ? ApplicationType.Application : ApplicationType.Confirmation; diff --git a/server/routes/templates.ts b/server/routes/templates.ts index b8730ec2..372fb566 100644 --- a/server/routes/templates.ts +++ b/server/routes/templates.ts @@ -11,7 +11,7 @@ import { config, getSetting, renderMarkdown } from "../common"; import { - authenticateWithRedirect, + authenticateWithRedirect, isAdmin, onlyAllowAnonymousBranch, branchRedirector, timeLimited, ApplicationType } from "../middleware"; import { @@ -384,34 +384,35 @@ function applicationHandler(requestType: ApplicationType): (request: express.Req } templateRoutes.route("/register/:branch").get( + isAdmin, onlyAllowAnonymousBranch, timeLimited, - applicationBranchHandler(ApplicationType.Application) + applicationBranchHandler(ApplicationType.Application, true) ); templateRoutes.route("/apply/:branch").get( authenticateWithRedirect, branchRedirector(ApplicationType.Application), timeLimited, - applicationBranchHandler(ApplicationType.Application) + applicationBranchHandler(ApplicationType.Application, false) ); templateRoutes.route("/confirm/:branch").get( authenticateWithRedirect, branchRedirector(ApplicationType.Confirmation), timeLimited, - applicationBranchHandler(ApplicationType.Confirmation) + applicationBranchHandler(ApplicationType.Confirmation, false) ); -function applicationBranchHandler(requestType: ApplicationType): (request: express.Request, response: express.Response) => Promise { +function applicationBranchHandler(requestType: ApplicationType, anonymous: boolean): (request: express.Request, response: express.Response) => Promise { return async (request: express.Request, response: express.Response) => { let user: IUser; - if (request.user) { - user = request.user as IUser; - } else { + if (anonymous) { user = new User({ uuid: uuid(), email: "" }); + } else { + user = request.user as IUser; } let branchName = request.params.branch as string; @@ -487,7 +488,7 @@ function applicationBranchHandler(requestType: ApplicationType): (request: expre }))).join("\n"); } - if (request.isAuthenticated()) { + if (!anonymous) { let thisUser = await User.findById(user._id) as IUserMongoose; // TODO this is a bug - dates are wrong if (requestType === ApplicationType.Application && !thisUser.applicationStartTime) { @@ -501,7 +502,7 @@ function applicationBranchHandler(requestType: ApplicationType): (request: expre let templateData: IRegisterTemplate = { siteTitle: config.eventName, - unauthenticated: !request.isAuthenticated(), + unauthenticated: anonymous, user: request.user, settings: { teamsEnabled: await getSetting("teamsEnabled"), From 737bd479416d54bc089999644096ee74b77ad212 Mon Sep 17 00:00:00 2001 From: Andrew Dai Date: Thu, 8 Mar 2018 22:23:12 -0500 Subject: [PATCH 037/194] Uncheck allow-anonymous open when skip confirmation is disabled --- client/js/admin.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/client/js/admin.ts b/client/js/admin.ts index 2e4b3a63..5beee51a 100644 --- a/client/js/admin.ts +++ b/client/js/admin.ts @@ -656,8 +656,13 @@ let skipConfirmationToggles = document.querySelectorAll(".branch-role input[type for (let input of Array.from(skipConfirmationToggles)) { let checkbox = input as HTMLInputElement; checkbox.addEventListener("click", () => { - if (checkbox.checked && checkbox.dataset.branchName !== undefined) { - uncheckConfirmationBranches((checkbox.dataset.branchName as string)); + if (checkbox.dataset.branchName !== undefined) { + let branchName = checkbox.dataset.branchName as string; + if (checkbox.checked) { + uncheckConfirmationBranches(branchName); + } else { + (document.querySelector(`.branch-role[data-name="${branchName}"] input[type="checkbox"].allowAnonymous`) as HTMLInputElement).checked = false; + } } }); } From 224668c2c4a1591c0dfd5a331f997fd234519fc0 Mon Sep 17 00:00:00 2001 From: Andrew Dai Date: Thu, 8 Mar 2018 22:25:48 -0500 Subject: [PATCH 038/194] Fix typos --- server/middleware.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/server/middleware.ts b/server/middleware.ts index 6efc56c8..01338f7f 100644 --- a/server/middleware.ts +++ b/server/middleware.ts @@ -135,8 +135,6 @@ export enum ApplicationType { export async function onlyAllowAnonymousBranch(request: express.Request, response: express.Response, next: express.NextFunction) { let branchName = request.params.branch as string; let questionBranches = (await BranchConfig.getOpenBranches("Application")).filter(br => { - - console.log(br.name.toLowerCase()); return br.name.toLowerCase() === branchName.toLowerCase(); }); if (questionBranches.length !== 1) { @@ -160,7 +158,7 @@ export function branchRedirector(requestType: ApplicationType): (request: expres let user = request.user as IUser; if (requestType === ApplicationType.Application) { if (user.accepted) { - // Do not redirect of application branch has "no confirmation" enabled + // Do not redirect to application branch has "no confirmation" enabled // ^ This is inferred from a user with `attending=true` and empty string for `confirmationBranch` if (!(user.attending && !user.confirmationBranch)) { response.redirect("/confirm"); From 04dc6612f778da51e6e151363154f94001468845 Mon Sep 17 00:00:00 2001 From: Andrew Dai Date: Thu, 8 Mar 2018 22:52:00 -0500 Subject: [PATCH 039/194] Prevent anonymous registration with existing email --- server/routes/api/user.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/server/routes/api/user.ts b/server/routes/api/user.ts index f11705ba..ba73058d 100644 --- a/server/routes/api/user.ts +++ b/server/routes/api/user.ts @@ -108,9 +108,16 @@ function postApplicationBranchHandler(anonymous: boolean): express.Handler { return async (request: express.Request, response: express.Response): Promise => { let user: IUserMongoose; if (anonymous) { + let email = request.body["anonymous-registration-email"] as string; + if (await User.findOne({email})) { + response.status(400).json({ + "error": `User with email "${email}" already exists` + }); + return; + } user = new User({ uuid: uuid(), - email: request.body["anonymous-registration-email"] + email }) as IUserMongoose; } else { user = await User.findOne({uuid: request.params.uuid}) as IUserMongoose; From 2ae170854e77ee7871878a34cfe5f99d8be445a3 Mon Sep 17 00:00:00 2001 From: Andrew Dai Date: Fri, 9 Mar 2018 15:18:40 -0500 Subject: [PATCH 040/194] Small fixes from PR feedback --- client/js/admin.ts | 19 ++++++++++--------- server/routes/api/user.ts | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/client/js/admin.ts b/client/js/admin.ts index 5beee51a..a61d965f 100644 --- a/client/js/admin.ts +++ b/client/js/admin.ts @@ -647,12 +647,12 @@ for (let i = 0; i < timeInputs.length; i++) { // Uncheck available confirmation branches for application branch when "skip confirmation" option is selected function uncheckConfirmationBranches(applicationBranch: string) { - let checkboxes = document.querySelectorAll(`.branch-role[data-name="${applicationBranch}"] .availableConfirmationBranches input[type="checkbox"]`); + let checkboxes = document.querySelectorAll(`.branch-role[data-name="${applicationBranch}"] .availableConfirmationBranches input[type="checkbox"]`) as NodeListOf; for (let input of Array.from(checkboxes)) { (input as HTMLInputElement).checked = false; } } -let skipConfirmationToggles = document.querySelectorAll(".branch-role input[type=\"checkbox\"].noConfirmation"); +let skipConfirmationToggles = document.querySelectorAll(".branch-role input[type=\"checkbox\"].noConfirmation") as NodeListOf; for (let input of Array.from(skipConfirmationToggles)) { let checkbox = input as HTMLInputElement; checkbox.addEventListener("click", () => { @@ -674,9 +674,9 @@ function setClickSkipConfirmation(applicationBranch: string, checked: boolean) { checkbox.click(); } } -let availableConfirmationBranchCheckboxes = document.querySelectorAll(".branch-role fieldset.availableConfirmationBranches input[type=\"checkbox\"]"); +let availableConfirmationBranchCheckboxes = document.querySelectorAll(".branch-role fieldset.availableConfirmationBranches input[type=\"checkbox\"]") as NodeListOf; for (let input of Array.from(availableConfirmationBranchCheckboxes)) { - let checkbox = input as HTMLInputElement; + let checkbox = input; checkbox.addEventListener("click", () => { if (checkbox.checked && checkbox.dataset.branchName !== undefined) { setClickSkipConfirmation((checkbox.dataset.branchName as string), false); @@ -686,9 +686,9 @@ for (let input of Array.from(availableConfirmationBranchCheckboxes)) { // Select "skip confirmation" option when "allow anonymous" option is selected // Hide/show public link when "allow anonymous" is clicked -let allowAnonymousCheckboxes = document.querySelectorAll(".branch-role input[type=\"checkbox\"].allowAnonymous"); +let allowAnonymousCheckboxes = document.querySelectorAll(".branch-role input[type=\"checkbox\"].allowAnonymous") as NodeListOf; for (let input of Array.from(allowAnonymousCheckboxes)) { - let checkbox = input as HTMLInputElement; + let checkbox = input; checkbox.onclick = () => { if (checkbox.dataset.branchName !== undefined) { let branchName = checkbox.dataset.branchName as string; @@ -766,9 +766,10 @@ function settingsUpdate(e: MouseEvent) { // This operation is all or nothing because it will only error if a branch was just made into an Application branch try { - branchData.allowAnonymous = (branchRoles[i].querySelector("fieldset.applicationBranchOptions input[type=\"checkbox\"].allowAnonymous") as HTMLInputElement).checked; - branchData.autoAccept = (branchRoles[i].querySelector("fieldset.applicationBranchOptions input[type=\"checkbox\"].autoAccept") as HTMLInputElement).checked; - branchData.noConfirmation = (branchRoles[i].querySelector("fieldset.applicationBranchOptions input[type=\"checkbox\"].noConfirmation") as HTMLInputElement).checked; + let applicationBranchOptions = branchRoles[i].querySelector("fieldset.applicationBranchOptions") as Element; + branchData.allowAnonymous = (applicationBranchOptions.querySelector("input[type=\"checkbox\"].allowAnonymous") as HTMLInputElement).checked; + branchData.autoAccept = (applicationBranchOptions.querySelector("input[type=\"checkbox\"].autoAccept") as HTMLInputElement).checked; + branchData.noConfirmation = (applicationBranchOptions.querySelector("input[type=\"checkbox\"].noConfirmation") as HTMLInputElement).checked; } catch { branchData.allowAnonymous = false; branchData.autoAccept = false; diff --git a/server/routes/api/user.ts b/server/routes/api/user.ts index ba73058d..6e26da77 100644 --- a/server/routes/api/user.ts +++ b/server/routes/api/user.ts @@ -64,7 +64,7 @@ function applicationTimeRestriction(requestType: ApplicationType): express.Reque next(); } else { - response.status(408).json({ + response.status(400).json({ "error": `${requestType === ApplicationType.Application ? "Applications" : "Confirmations"} are currently closed` }); return; From 9fdce6788a2db94ae4bc8cb4ba8d437a007c38d4 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Fri, 9 Mar 2018 23:17:57 -0500 Subject: [PATCH 041/194] Small type cast fix --- client/js/application.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/js/application.ts b/client/js/application.ts index 1a9f8e1e..799460c3 100644 --- a/client/js/application.ts +++ b/client/js/application.ts @@ -28,7 +28,7 @@ submitButton.addEventListener("click", e => { await sweetAlert("Awesome!", successMessage, "success"); if (unauthenticated) { - (document.querySelector("form") as HTMLFormElement).reset(); + document.querySelector("form")!.reset(); } else { window.location.assign("/"); } From 06699a2aac09a278d61478a18e671c2cd499ea3e Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Fri, 9 Mar 2018 23:19:38 -0500 Subject: [PATCH 042/194] Bump version to 1.13.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 62a56fa6..7e9510c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "1.12.5", + "version": "1.13.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index fcb3704d..b52ec99e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "1.12.5", + "version": "1.13.0", "description": "TBD", "main": "server/app.js", "scripts": { From 2669fcd6d0fc96c73cec4672e4fa492f7f9bb22a Mon Sep 17 00:00:00 2001 From: Ehsan Asdar Date: Mon, 7 May 2018 20:45:09 -0500 Subject: [PATCH 043/194] Various fixes for walk-up registration, including #209/#208, and simplification of redirection logic #206 --- client/js/application.ts | 7 +- server/common.ts | 28 +++++++- server/middleware.ts | 134 ++++++++----------------------------- server/routes/api/user.ts | 54 +++------------ server/routes/templates.ts | 7 +- 5 files changed, 69 insertions(+), 161 deletions(-) diff --git a/client/js/application.ts b/client/js/application.ts index 799460c3..cd68032c 100644 --- a/client/js/application.ts +++ b/client/js/application.ts @@ -29,6 +29,7 @@ submitButton.addEventListener("click", e => { if (unauthenticated) { document.querySelector("form")!.reset(); + submitButton.disabled = false; } else { window.location.assign("/"); } @@ -71,11 +72,7 @@ if (deleteButton) { credentials: "same-origin", method: "DELETE" }).then(checkStatus).then(parseJSON).then(async () => { - if (formType === FormType.Application) { - window.location.assign("/apply"); - } else { - window.location.assign("/confirm"); - } + window.location.assign("/"); }).catch(async (err: Error) => { await sweetAlert("Oh no!", err.message, "error"); submitButton.disabled = false; diff --git a/server/common.ts b/server/common.ts index c1070d45..bdd2ec52 100644 --- a/server/common.ts +++ b/server/common.ts @@ -492,7 +492,7 @@ export async function renderEmailText(markdown: string, user: IUser, markdownRen } // Verify and load questions -import { BranchConfig } from "./branch"; +import { BranchConfig, ApplicationBranch, ConfirmationBranch } from "./branch"; BranchConfig.verifyConfig().then(good => { if (good) { console.log(`Question branches loaded from ${config.questionsLocation} to DB successfully`); @@ -503,3 +503,29 @@ BranchConfig.verifyConfig().then(good => { }).catch(err => { throw err; }); + +import {ApplicationType} from "./middleware"; +import * as moment from "moment-timezone"; + +export async function isBranchOpen(branchName: string, user : IUser, requestType: ApplicationType) { + let branch = (await BranchConfig.loadAllBranches()).find(b => b.name.toLowerCase() === branchName.toLowerCase()) as (ApplicationBranch | ConfirmationBranch); + if (!branch) { + return false; + } + + let openDate = branch.open; + let closeDate = branch.close; + if (requestType === ApplicationType.Confirmation && user.confirmationDeadlines) { + let times = user.confirmationDeadlines.find((d) => d.name === branch.name); + if (times) { + openDate = times.open; + closeDate = times.close; + } + } + + if (moment().isBetween(openDate, closeDate)) { + return true; + } + + return false; +} diff --git a/server/middleware.ts b/server/middleware.ts index 01338f7f..046b3656 100644 --- a/server/middleware.ts +++ b/server/middleware.ts @@ -2,11 +2,10 @@ import * as fs from "fs"; import * as moment from "moment-timezone"; import * as path from "path"; import * as os from "os"; -import * as uuid from "uuid/v4"; -import { config, getSetting, readFileAsync, STATIC_ROOT } from "./common"; -import { getOpenConfirmationBranches, BranchConfig, ApplicationBranch, ConfirmationBranch } from "./branch"; -import { IUser, User } from "./schema"; +import { config, isBranchOpen } from "./common"; +import { BranchConfig, ApplicationBranch } from "./branch"; +import { IUser } from "./schema"; // // Express middleware @@ -126,25 +125,23 @@ export function authenticateWithRedirect(request: express.Request, response: exp } } -import * as Handlebars from "handlebars"; -import { ICommonTemplate } from "./schema"; export enum ApplicationType { Application, Confirmation } export async function onlyAllowAnonymousBranch(request: express.Request, response: express.Response, next: express.NextFunction) { let branchName = request.params.branch as string; - let questionBranches = (await BranchConfig.getOpenBranches("Application")).filter(br => { + let questionBranches = (await BranchConfig.loadAllBranches()).filter(br => { return br.name.toLowerCase() === branchName.toLowerCase(); }); if (questionBranches.length !== 1) { - response.redirect("/apply"); + response.redirect("/"); return; } let branch = questionBranches[0] as ApplicationBranch; if (!branch.allowAnonymous) { - response.redirect("/apply"); + response.redirect("/"); return; } @@ -153,27 +150,20 @@ export async function onlyAllowAnonymousBranch(request: express.Request, respons export function branchRedirector(requestType: ApplicationType): (request: express.Request, response: express.Response, next: express.NextFunction) => Promise { return async (request: express.Request, response: express.Response, next: express.NextFunction) => { - // TODO refactor redirection code and consolidate here (#206) // TODO: fix branch names so they have a machine ID and human label let user = request.user as IUser; - if (requestType === ApplicationType.Application) { - if (user.accepted) { - // Do not redirect to application branch has "no confirmation" enabled - // ^ This is inferred from a user with `attending=true` and empty string for `confirmationBranch` - if (!(user.attending && !user.confirmationBranch)) { - response.redirect("/confirm"); - return; - } - } + + if (requestType === ApplicationType.Application && user.accepted) { + response.redirect("/"); } if (requestType === ApplicationType.Confirmation) { if (!user.accepted || !user.applied) { - response.redirect("/apply"); + response.redirect("/"); return; } if (user.attending && !user.confirmationBranch) { - response.redirect("/apply"); + response.redirect("/"); return; } } @@ -183,10 +173,20 @@ export function branchRedirector(requestType: ApplicationType): (request: expres questionBranches = (await BranchConfig.getOpenBranches("Application")).map(branch => branch.name.toLowerCase()); } else { questionBranches = ((await BranchConfig.loadBranchFromDB(user.applicationBranch)) as ApplicationBranch).confirmationBranches.map(branchName => branchName.toLowerCase()); + + // Check for open branches based on user's individual confirmation deadlines + questionBranches.filter((branch) => { + return isBranchOpen(branch, user, ApplicationType.Confirmation); + }) } if (request.params.branch) { let branchName = (request.params.branch as string).toLowerCase(); + + if (!isBranchOpen(branchName, user, requestType)) { + response.redirect("/") + } + if (requestType === ApplicationType.Application) { // Redirect directly to branch if there is an existing application or confirmation if (user.applied && branchName.toLowerCase() !== user.applicationBranch.toLowerCase()) { @@ -195,7 +195,7 @@ export function branchRedirector(requestType: ApplicationType): (request: expres } let questionBranch = questionBranches.find(branch => branch === branchName.toLowerCase())!; if (!questionBranch) { - response.redirect("/apply"); + response.redirect("/"); return; } } @@ -205,8 +205,9 @@ export function branchRedirector(requestType: ApplicationType): (request: expres response.redirect(`/confirm/${encodeURIComponent(user.confirmationBranch.toLowerCase())}`); return; } + //TODO why is this !user.attending? if (questionBranches.indexOf(branchName.toLowerCase()) === -1 && !user.attending) { - response.redirect("/confirm"); + response.redirect("/"); return; } } @@ -231,94 +232,17 @@ export function branchRedirector(requestType: ApplicationType): (request: expres response.redirect(`/${redirPath}/${uriBranch}`); return; } + + // If there are no valid branches, redirect to main page. + if (questionBranches.length === 0) { + response.redirect("/"); + } } next(); }; } -export async function timeLimited(request: express.Request, response: express.Response, next: express.NextFunction) { - let requestType: ApplicationType = request.url.match(/^\/apply/) ? ApplicationType.Application : ApplicationType.Confirmation; - - let user: IUser; - if (request.isAuthenticated()) { - user = request.user as IUser; - } else { - user = new User({ - uuid: uuid(), - email: "" - }); - } - - let openBranches: (ApplicationBranch | ConfirmationBranch)[]; - if (requestType === ApplicationType.Application) { - openBranches = await BranchConfig.getOpenBranches("Application"); - if (request.isAuthenticated() && user.applied && user.applicationBranch) { - let applicationBranch = user.applicationBranch; - openBranches = openBranches.filter((b => b.name === applicationBranch)); - } - } else { - if (request.isAuthenticated()) { - openBranches = await getOpenConfirmationBranches(user); - } else { - openBranches = await BranchConfig.getOpenBranches("Confirmation"); - } - - if (request.isAuthenticated() && user.attending && user.confirmationBranch) { - let confirmationBranch = user.confirmationBranch; - openBranches = openBranches.filter((b => b.name === confirmationBranch)); - } - } - - if (openBranches.length > 0) { - next(); - return; - } - - // TODO reimplement open and close times - /* - const TIME_FORMAT = "dddd, MMMM Do YYYY [at] h:mm a z"; - */ - interface IClosedTemplate extends ICommonTemplate { - type: string; - /* - open: { - time: string; - verb: string; - }; - close: { - time: string; - verb: string; - }; - */ - contactEmail: string; - } - let template = Handlebars.compile(await readFileAsync(path.resolve(STATIC_ROOT, "closed.html"))); - let emailParsed = config.email.from.match(/<(.*?)>/); - let templateData: IClosedTemplate = { - siteTitle: config.eventName, - user: request.user, - settings: { - teamsEnabled: await getSetting("teamsEnabled"), - qrEnabled: await getSetting("qrEnabled") - }, - - type: requestType === ApplicationType.Application ? "Application" : "Confirmation", - /* - open: { - time: openDate.tz(moment.tz.guess()).format(TIME_FORMAT), - verb: moment().isBefore(openDate) ? "will open" : "opened" - }, - close: { - time: closeDate.tz(moment.tz.guess()).format(TIME_FORMAT), - verb: moment().isBefore(closeDate) ? "will close" : "closed" - }, - */ - contactEmail: emailParsed ? emailParsed[1] : config.email.from - }; - response.send(template(templateData)); -} - import { DataLog, HackGTMetrics } from "./schema"; export function trackEvent(action: string, request: express.Request, user?: string, data?: object) { let thisEvent: DataLog = { diff --git a/server/routes/api/user.ts b/server/routes/api/user.ts index 6e26da77..bfbfe2a1 100644 --- a/server/routes/api/user.ts +++ b/server/routes/api/user.ts @@ -2,13 +2,12 @@ import * as path from "path"; import * as express from "express"; import * as json2csv from "json2csv"; import * as archiver from "archiver"; -import * as moment from "moment-timezone"; import * as uuid from "uuid/v4"; import { STORAGE_ENGINE, formatSize, - config, getSetting, renderEmailHTML, renderEmailText, sendMailAsync, defaultEmailSubjects + config, getSetting, renderEmailHTML, renderEmailText, sendMailAsync, defaultEmailSubjects, isBranchOpen } from "../../common"; import { MAX_FILE_SIZE, postParser, uploadHandler, @@ -36,45 +35,8 @@ let postApplicationBranchErrorHandler: express.ErrorRequestHandler = (err, reque } }; -// TODO what is the difference between this and `middleware.timeLimited`? - related to #206 -function applicationTimeRestriction(requestType: ApplicationType): express.RequestHandler { - return async (request, response, next) => { - let branchName = request.params.branch as string; - let branch = (await Branches.BranchConfig.loadAllBranches()).find(b => b.name.toLowerCase() === branchName.toLowerCase()) as (Branches.ApplicationBranch | Branches.ConfirmationBranch); - if (!branch) { - response.status(400).json({ - "error": "Invalid application branch" - }); - return; - } - - let user = request.user as IUserMongoose; - - let openDate = branch.open; - let closeDate = branch.close; - if (requestType === ApplicationType.Confirmation && user.confirmationDeadlines) { - let times = user.confirmationDeadlines.find((d) => d.name === branch.name); - if (times) { - openDate = times.open; - closeDate = times.close; - } - } - - if (moment().isBetween(openDate, closeDate) || request.user.isAdmin) { - next(); - } - else { - response.status(400).json({ - "error": `${requestType === ApplicationType.Application ? "Applications" : "Confirmations"} are currently closed` - }); - return; - } - }; -} - registrationRoutes.route("/:branch").post( isAdmin, - applicationTimeRestriction(ApplicationType.Application), postParser, uploadHandler.any(), postApplicationBranchErrorHandler, @@ -83,25 +45,21 @@ registrationRoutes.route("/:branch").post( userRoutes.route("/application/:branch").post( isUserOrAdmin, - applicationTimeRestriction(ApplicationType.Application), postParser, uploadHandler.any(), postApplicationBranchErrorHandler, postApplicationBranchHandler(false) ).delete( isUserOrAdmin, - applicationTimeRestriction, deleteApplicationBranchHandler); userRoutes.route("/confirmation/:branch").post( isUserOrAdmin, - applicationTimeRestriction(ApplicationType.Confirmation), postParser, uploadHandler.any(), postApplicationBranchErrorHandler, postApplicationBranchHandler(false) ).delete( isUserOrAdmin, - applicationTimeRestriction, deleteApplicationBranchHandler ); function postApplicationBranchHandler(anonymous: boolean): express.Handler { @@ -129,11 +87,19 @@ function postApplicationBranchHandler(anonymous: boolean): express.Handler { let questionBranch = (await Branches.BranchConfig.loadAllBranches()).find(branch => branch.name.toLowerCase() === branchName.toLowerCase()); if (!questionBranch) { response.status(400).json({ - "error": "Invalid application branch" + "error": "Invalid branch" }); return; } + // Allow admin to submit an already closed branch (for anonymous submission purposes) + if (!user.admin && !isBranchOpen(request.params.branch, user, questionBranch instanceof Branches.ApplicationBranch ? ApplicationType.Application : ApplicationType.Confirmation)) { + response.status(400).json({ + "error": "Branch is closed" + }); + return; + } + if (questionBranch instanceof Branches.ApplicationBranch) { if (user.applied && branchName.toLowerCase() !== user.applicationBranch.toLowerCase()) { response.status(400).json({ diff --git a/server/routes/templates.ts b/server/routes/templates.ts index 372fb566..53ec3b1b 100644 --- a/server/routes/templates.ts +++ b/server/routes/templates.ts @@ -12,7 +12,7 @@ import { } from "../common"; import { authenticateWithRedirect, isAdmin, - onlyAllowAnonymousBranch, branchRedirector, timeLimited, ApplicationType + onlyAllowAnonymousBranch, branchRedirector, ApplicationType } from "../middleware"; import { IUser, IUserMongoose, User, @@ -317,13 +317,11 @@ templateRoutes.route("/team").get(authenticateWithRedirect, async (request, resp templateRoutes.route("/apply").get( authenticateWithRedirect, branchRedirector(ApplicationType.Application), - timeLimited, applicationHandler(ApplicationType.Application) ); templateRoutes.route("/confirm").get( authenticateWithRedirect, branchRedirector(ApplicationType.Confirmation), - timeLimited, applicationHandler(ApplicationType.Confirmation) ); @@ -386,20 +384,17 @@ function applicationHandler(requestType: ApplicationType): (request: express.Req templateRoutes.route("/register/:branch").get( isAdmin, onlyAllowAnonymousBranch, - timeLimited, applicationBranchHandler(ApplicationType.Application, true) ); templateRoutes.route("/apply/:branch").get( authenticateWithRedirect, branchRedirector(ApplicationType.Application), - timeLimited, applicationBranchHandler(ApplicationType.Application, false) ); templateRoutes.route("/confirm/:branch").get( authenticateWithRedirect, branchRedirector(ApplicationType.Confirmation), - timeLimited, applicationBranchHandler(ApplicationType.Confirmation, false) ); From 566736231f5e9806f126ac640ed254d412b61e4c Mon Sep 17 00:00:00 2001 From: Ehsan Asdar Date: Mon, 7 May 2018 20:55:58 -0500 Subject: [PATCH 044/194] TSLint fixes --- server/common.ts | 2 +- server/middleware.ts | 14 +++++++------- server/routes/api/user.ts | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/server/common.ts b/server/common.ts index bdd2ec52..515bca5b 100644 --- a/server/common.ts +++ b/server/common.ts @@ -507,7 +507,7 @@ BranchConfig.verifyConfig().then(good => { import {ApplicationType} from "./middleware"; import * as moment from "moment-timezone"; -export async function isBranchOpen(branchName: string, user : IUser, requestType: ApplicationType) { +export async function isBranchOpen(branchName: string, user: IUser, requestType: ApplicationType) { let branch = (await BranchConfig.loadAllBranches()).find(b => b.name.toLowerCase() === branchName.toLowerCase()) as (ApplicationBranch | ConfirmationBranch); if (!branch) { return false; diff --git a/server/middleware.ts b/server/middleware.ts index 046b3656..2c0fb568 100644 --- a/server/middleware.ts +++ b/server/middleware.ts @@ -173,20 +173,20 @@ export function branchRedirector(requestType: ApplicationType): (request: expres questionBranches = (await BranchConfig.getOpenBranches("Application")).map(branch => branch.name.toLowerCase()); } else { questionBranches = ((await BranchConfig.loadBranchFromDB(user.applicationBranch)) as ApplicationBranch).confirmationBranches.map(branchName => branchName.toLowerCase()); - + // Check for open branches based on user's individual confirmation deadlines questionBranches.filter((branch) => { return isBranchOpen(branch, user, ApplicationType.Confirmation); - }) + }); } if (request.params.branch) { let branchName = (request.params.branch as string).toLowerCase(); - + if (!isBranchOpen(branchName, user, requestType)) { - response.redirect("/") + response.redirect("/"); } - + if (requestType === ApplicationType.Application) { // Redirect directly to branch if there is an existing application or confirmation if (user.applied && branchName.toLowerCase() !== user.applicationBranch.toLowerCase()) { @@ -205,7 +205,7 @@ export function branchRedirector(requestType: ApplicationType): (request: expres response.redirect(`/confirm/${encodeURIComponent(user.confirmationBranch.toLowerCase())}`); return; } - //TODO why is this !user.attending? + // TODO why is this !user.attending? if (questionBranches.indexOf(branchName.toLowerCase()) === -1 && !user.attending) { response.redirect("/"); return; @@ -232,7 +232,7 @@ export function branchRedirector(requestType: ApplicationType): (request: expres response.redirect(`/${redirPath}/${uriBranch}`); return; } - + // If there are no valid branches, redirect to main page. if (questionBranches.length === 0) { response.redirect("/"); diff --git a/server/routes/api/user.ts b/server/routes/api/user.ts index bfbfe2a1..6d1f5f86 100644 --- a/server/routes/api/user.ts +++ b/server/routes/api/user.ts @@ -98,7 +98,7 @@ function postApplicationBranchHandler(anonymous: boolean): express.Handler { "error": "Branch is closed" }); return; - } + } if (questionBranch instanceof Branches.ApplicationBranch) { if (user.applied && branchName.toLowerCase() !== user.applicationBranch.toLowerCase()) { From 718422eaa51ad88f6ecdea1167376b5b11978328 Mon Sep 17 00:00:00 2001 From: Ehsan Asdar Date: Fri, 11 May 2018 16:14:50 -0500 Subject: [PATCH 045/194] Rewrite of core confirmation logic #211, #209, #117 #110 When we accept a user we assign them to a specific confirmation branch (and only one confirmation branch). Each confirmation branch is an option in the state dropdown. Instead of sending an acceptance email for the application branch, we send a pre-confirm email for the confirmation branch. This allows us to alter the message text for each branch. Waitlist/Rejected edge cases: Waitlist can operate as a normal confirmation branch if we have people confirm their spot on the waitlist. Rejected can be implemented by a simple no-op confirmation branch that does not require any user action (this is also necessary for supporting the walk-up feature). A flag on each branch can indicate if this confirmation represents "success" or "failure", for the purposes of ensuring we only check-in accepted students. The only reduction in functionality here is that users can no longer choose from multiple confirmation branch. In my opinion, this isn't necessary for us and only lead to confusion when different confirmation branches had different deadlines. --- api.graphql | 6 +- client/admin.html | 63 +- client/confirmation.html | 2 +- client/index.html | 42 +- client/js/admin.ts | 121 +-- client/js/application.ts | 2 +- client/partials/sidebar.html | 12 +- server/branch.ts | 28 +- server/common.ts | 17 +- server/config/questions.json | 1708 +-------------------------------- server/middleware.ts | 120 ++- server/routes/api/graphql.ts | 15 +- server/routes/api/settings.ts | 10 +- server/routes/api/user.ts | 130 +-- server/routes/auth.ts | 2 +- server/routes/templates.ts | 84 +- server/schema.ts | 37 +- 17 files changed, 319 insertions(+), 2080 deletions(-) diff --git a/api.graphql b/api.graphql index a347a1f8..2226b0fc 100644 --- a/api.graphql +++ b/api.graphql @@ -61,8 +61,10 @@ type User { accepted: Boolean! # If the user has been accepted and notified of his or her acceptance accepted_and_notified: Boolean! - # If the user has indicated that he or she is attending - attending: Boolean! + # If the user has submitted a confirmation + confirmed: Boolean! + # A users assigned confirmation branch + confirmationBranch: String # A users application phase answers # null if user has not filled out this phase diff --git a/client/admin.html b/client/admin.html index 4d06b710..e174c517 100644 --- a/client/admin.html +++ b/client/admin.html @@ -166,16 +166,6 @@

Applicants

- -
@@ -203,8 +193,9 @@

Applicants

@@ -230,7 +221,7 @@

Settings

- +
@@ -250,23 +241,16 @@

{{this.name}}

- - -
-
- - -
- - -
- {{#each ../settings.branches.confirmation}} -
- - + +
- {{/each}}
+
@@ -290,14 +274,27 @@

{{this.name}}

+ +
+
+ + +
+
+ + +
+
+ + +
+
- -
{{/each}} @@ -320,14 +317,14 @@

Edit email content

{{#each settings.branches.application}} - {{#unless this.autoAccept}} - - {{/unless}} {{/each}} {{#each settings.branches.confirmation}} + + {{#unless this.autoConfirm}} + {{/unless}} {{/each}} diff --git a/client/confirmation.html b/client/confirmation.html index c0890844..b28b7c6d 100644 --- a/client/confirmation.html +++ b/client/confirmation.html @@ -76,7 +76,7 @@

RSVP: {{branch}}

{{/each}} {{{endText}}}
- {{#if user.attending}} + {{#if user.confirmed}} {{else}} diff --git a/client/index.html b/client/index.html index 91dfdf39..e23d2afb 100644 --- a/client/index.html +++ b/client/index.html @@ -10,7 +10,7 @@ - {{#if user.attending}} + {{#if user.confirmed}} {{#if settings.qrEnabled}} @@ -27,41 +27,34 @@

Dashboard

Your status:

- {{#if user.attending}} -

Attending

- {{else if user.accepted}} -

Accepted

- {{else if user.applied}} -

Submitted

- {{else}} -

Incomplete

- {{/if}} + {{status}}

Welcome back, {{user.name}}!

- {{#if user.attending}} -

You're all set!

+ {{#if user.confirmed}}

Application type: {{user.applicationBranch}}

- {{#unless skipConfirmation}}

Confirmation type: {{user.confirmationBranch}}

- {{#if confirmationStatus.areOpen}} -

Feel free to edit your RSVP at any time. However, once RSVPing closes on {{confirmationClose}}, you will not be able to edit it anymore.

- Edit your confirmation - {{else}} -

RSVPing closed on {{confirmationClose}}.

- {{/if}} + {{#unless autoConfirm}} + {{#if confirmationStatus.areOpen}} +

Feel free to edit your RSVP at any time. However, once RSVPing closes on {{confirmationClose}}, you will not be able to edit it anymore.

+ Edit your confirmation + {{else}} +

RSVPing closed on {{confirmationClose}}.

+ {{/if}} {{/unless}} -

We look forward to seeing you!

{{#if settings.qrEnabled}} -

Show your QR code to check in when you get here.

-
+ {{#if user.accepted}} +

Show your QR code to check in when you get here.

+
+ {{/if}} {{/if}} - {{else if user.accepted}} + {{else if user.confirmationBranch}} {{#if confirmationStatus.areOpen}} -

You've been accepted but you still need to RSVP!

+

Please confirm your application decision.

Application type: {{user.applicationBranch}}

+

Confirmation type: {{user.confirmationBranch}}

{{#ifCond allConfirmationTimes.length 1}}

If you do not RSVP before {{confirmationClose}}, you will not be able to attend {{siteTitle}}!

{{else}} @@ -74,7 +67,6 @@

Incomplete

{{/ifCond}} Confirm your attendance {{else}} -

You've been accepted!

Application type: {{user.applicationBranch}}

{{#if confirmationStatus.beforeOpen}} {{#ifCond allConfirmationTimes.length 1}} diff --git a/client/js/admin.ts b/client/js/admin.ts index a61d965f..c66676ed 100644 --- a/client/js/admin.ts +++ b/client/js/admin.ts @@ -69,7 +69,7 @@ class UserEntries { }, applied, accepted, - attending + confirmed } } }`; @@ -126,10 +126,10 @@ class UserEntries { if (user.applied && user.accepted) { userStatus = `Accepted (${user.application.type})`; } - if (user.applied && user.accepted && user.attending) { + if (user.applied && user.accepted && user.confirmed) { userStatus = `Accepted (${user.application.type}) / Confirmed`; } - if (user.applied && user.accepted && user.attending && user.confirmation) { + if (user.applied && user.accepted && user.confirmed && user.confirmation) { userStatus = `Accepted (${user.application.type}) / Confirmed (${user.confirmation.type})`; } node.querySelector("td.status")!.textContent = userStatus; @@ -199,7 +199,6 @@ class ApplicantEntries { private static readonly previousButton = document.getElementById("applicants-entries-previous") as HTMLButtonElement; private static readonly nextButton = document.getElementById("applicants-entries-next") as HTMLButtonElement; private static readonly branchFilter = document.getElementById("branch-filter") as HTMLInputElement; - private static readonly statusFilter = document.getElementById("status-filter") as HTMLInputElement; private static readonly searchBox = document.getElementById("applicant-search") as HTMLInputElement; private static readonly searchRegex = document.getElementById("applicant-search-regex") as HTMLInputElement; private static filter: any = {}; @@ -246,23 +245,6 @@ class ApplicantEntries { } } - switch (this.statusFilter.value) { - case "no-decision": - this.filter.accepted = false; - break; - case "accepted": - this.filter.accepted = true; - break; - case "not-confirmed": - this.filter.accepted = true; - this.filter.attending = false; - break; - case "confirmed": - this.filter.accepted = true; - this.filter.attending = true; - break; - } - this.offset = 0; this.load(); } @@ -287,7 +269,8 @@ class ApplicantEntries { name }, accepted, - attending, + confirmed, + confirmationBranch, application { type, data { @@ -434,7 +417,7 @@ class ApplicantEntries { } generalNode.querySelector("td.branch")!.textContent = user.application.type; let statusSelect = generalNode.querySelector("select.status") as HTMLSelectElement; - statusSelect.value = user.accepted ? "accepted" : "no-decision"; + statusSelect.value = user.confirmationBranch ? user.confirmationBranch : "no-decision"; let dataSection = detailsNode.querySelector("div.applicantData") as HTMLDivElement; while (dataSection.hasChildNodes()) { @@ -509,9 +492,6 @@ class ApplicantEntries { this.branchFilter.addEventListener("change", e => { this.updateFilter(); }); - this.statusFilter.addEventListener("change", e => { - this.updateFilter(); - }); } public static next() { document.querySelector("#applicants > table")!.scrollIntoView(); @@ -645,61 +625,6 @@ for (let i = 0; i < timeInputs.length; i++) { timeInputs[i].value = moment(new Date(timeInputs[i].dataset.rawValue || "")).format("Y-MM-DDTHH:mm:00"); } -// Uncheck available confirmation branches for application branch when "skip confirmation" option is selected -function uncheckConfirmationBranches(applicationBranch: string) { - let checkboxes = document.querySelectorAll(`.branch-role[data-name="${applicationBranch}"] .availableConfirmationBranches input[type="checkbox"]`) as NodeListOf; - for (let input of Array.from(checkboxes)) { - (input as HTMLInputElement).checked = false; - } -} -let skipConfirmationToggles = document.querySelectorAll(".branch-role input[type=\"checkbox\"].noConfirmation") as NodeListOf; -for (let input of Array.from(skipConfirmationToggles)) { - let checkbox = input as HTMLInputElement; - checkbox.addEventListener("click", () => { - if (checkbox.dataset.branchName !== undefined) { - let branchName = checkbox.dataset.branchName as string; - if (checkbox.checked) { - uncheckConfirmationBranches(branchName); - } else { - (document.querySelector(`.branch-role[data-name="${branchName}"] input[type="checkbox"].allowAnonymous`) as HTMLInputElement).checked = false; - } - } - }); -} - -// Uncheck "skip confirmation" option when a confirmation branch is selected -function setClickSkipConfirmation(applicationBranch: string, checked: boolean) { - let checkbox = (document.querySelector(`.branch-role[data-name="${applicationBranch}"] input[type="checkbox"].noConfirmation`) as HTMLInputElement); - if (checkbox.checked !== checked) { - checkbox.click(); - } -} -let availableConfirmationBranchCheckboxes = document.querySelectorAll(".branch-role fieldset.availableConfirmationBranches input[type=\"checkbox\"]") as NodeListOf; -for (let input of Array.from(availableConfirmationBranchCheckboxes)) { - let checkbox = input; - checkbox.addEventListener("click", () => { - if (checkbox.checked && checkbox.dataset.branchName !== undefined) { - setClickSkipConfirmation((checkbox.dataset.branchName as string), false); - } - }); -} - -// Select "skip confirmation" option when "allow anonymous" option is selected -// Hide/show public link when "allow anonymous" is clicked -let allowAnonymousCheckboxes = document.querySelectorAll(".branch-role input[type=\"checkbox\"].allowAnonymous") as NodeListOf; -for (let input of Array.from(allowAnonymousCheckboxes)) { - let checkbox = input; - checkbox.onclick = () => { - if (checkbox.dataset.branchName !== undefined) { - let branchName = checkbox.dataset.branchName as string; - if (checkbox.checked) { - setClickSkipConfirmation(branchName, true); - } - (document.querySelector(`.branch-role[data-name="${branchName}"] .public-link`) as HTMLDivElement).hidden = !checkbox.checked; - } - }; -} - // Settings update function parseDateTime(dateTime: string) { let digits = dateTime.split(/\D+/).map(num => parseInt(num, 10)); @@ -742,9 +667,9 @@ function settingsUpdate(e: MouseEvent) { open?: Date; close?: Date; usesRollingDeadline?: boolean; - confirmationBranches?: string[]; - noConfirmation?: boolean; - autoAccept?: boolean; + isAcceptance?: boolean; + autoConfirm?: boolean; + autoAccept?: string; allowAnonymous?: boolean; } = {role: branchRole}; // TODO this should probably be typed (not just strings) @@ -755,30 +680,28 @@ function settingsUpdate(e: MouseEvent) { branchData.close = closeInputElem ? new Date(closeInputElem.value) : new Date(); } if (branchRole === "Application") { - let checkboxes = branchRoles[i].querySelectorAll("fieldset.availableConfirmationBranches input") as NodeListOf; - let allowedConfirmationBranches: string[] = []; - for (let j = 0; j < checkboxes.length; j++) { - if (checkboxes[j].checked) { - allowedConfirmationBranches.push(checkboxes[j].dataset.confirmation!); - } - } - branchData.confirmationBranches = allowedConfirmationBranches; - // This operation is all or nothing because it will only error if a branch was just made into an Application branch try { let applicationBranchOptions = branchRoles[i].querySelector("fieldset.applicationBranchOptions") as Element; branchData.allowAnonymous = (applicationBranchOptions.querySelector("input[type=\"checkbox\"].allowAnonymous") as HTMLInputElement).checked; - branchData.autoAccept = (applicationBranchOptions.querySelector("input[type=\"checkbox\"].autoAccept") as HTMLInputElement).checked; - branchData.noConfirmation = (applicationBranchOptions.querySelector("input[type=\"checkbox\"].noConfirmation") as HTMLInputElement).checked; + branchData.autoAccept = (applicationBranchOptions.querySelector("select.autoAccept") as HTMLInputElement).value; } catch { branchData.allowAnonymous = false; - branchData.autoAccept = false; - branchData.noConfirmation = false; + branchData.autoAccept = "disabled"; } } if (branchRole === "Confirmation") { - let usesRollingDeadlineCheckbox = (branchRoles[i].querySelectorAll("input.usesRollingDeadline") as NodeListOf); - branchData.usesRollingDeadline = usesRollingDeadlineCheckbox.length > 0 ? usesRollingDeadlineCheckbox[0].checked : false; + let confirmationBranchOptions = branchRoles[i].querySelector("fieldset.confirmationBranchOptions") as Element; + try { + branchData.usesRollingDeadline = (confirmationBranchOptions.querySelector("input[type=\"checkbox\"].usesRollingDeadline") as HTMLInputElement).checked; + branchData.isAcceptance = (confirmationBranchOptions.querySelector("input[type=\"checkbox\"].isAcceptance") as HTMLInputElement).checked; + branchData.autoConfirm = (confirmationBranchOptions.querySelector("input[type=\"checkbox\"].autoConfirm") as HTMLInputElement).checked; + } catch { + branchData.usesRollingDeadline = false; + branchData.isAcceptance = true; + branchData.autoConfirm = false; + } + } branchRoleData.append(branchName, JSON.stringify(branchData)); } diff --git a/client/js/application.ts b/client/js/application.ts index cd68032c..ebb650f3 100644 --- a/client/js/application.ts +++ b/client/js/application.ts @@ -5,7 +5,7 @@ enum FormType { declare let formTypeString: keyof typeof FormType; let formType = FormType[formTypeString]; -declare let unauthenticated: (boolean | undefined); +let unauthenticated: (boolean | null) = null; let form = document.querySelector("form") as HTMLFormElement | null; let submitButton = document.querySelector("form input[type=submit]") as HTMLInputElement; diff --git a/client/partials/sidebar.html b/client/partials/sidebar.html index 0683ae7c..86c4ae26 100644 --- a/client/partials/sidebar.html +++ b/client/partials/sidebar.html @@ -6,16 +6,8 @@

{{siteTitle}}

Dashboard - {{#if user.accepted}} - {{#if user.attending}} - {{#if user.confirmation}} - Confirmation - {{else}} - Application - {{/if}} - {{else}} - Confirmation - {{/if}} + {{#if user.confirmationBranch}} + Confirmation {{else}} Application {{/if}} diff --git a/server/branch.ts b/server/branch.ts index 073c0264..6ffd47da 100644 --- a/server/branch.ts +++ b/server/branch.ts @@ -179,26 +179,19 @@ export class ApplicationBranch extends TimedBranch { public allowAnonymous: boolean; - public autoAccept: boolean; - - public noConfirmation: boolean; - public confirmationBranches: string[]; + public autoAccept: string; protected async loadSettings(): Promise { await super.loadSettings(); let branchConfig = await QuestionBranchConfig.findOne({ "name": this.name }); this.allowAnonymous = branchConfig && branchConfig.settings && branchConfig.settings.allowAnonymous || false; - this.autoAccept = branchConfig && branchConfig.settings && branchConfig.settings.autoAccept || false; - this.noConfirmation = branchConfig && branchConfig.settings && branchConfig.settings.noConfirmation || false; - this.confirmationBranches = branchConfig && branchConfig.settings && branchConfig.settings.confirmationBranches || []; + this.autoAccept = branchConfig && branchConfig.settings && branchConfig.settings.autoAccept || "disabled"; } protected serializeSettings(): QuestionBranchSettings { return { ...super.serializeSettings(), allowAnonymous: this.allowAnonymous, - autoAccept: this.autoAccept, - noConfirmation: this.noConfirmation, - confirmationBranches: this.confirmationBranches + autoAccept: this.autoAccept }; } } @@ -207,15 +200,21 @@ export class ConfirmationBranch extends TimedBranch { public readonly type: keyof QuestionBranchTypes = "Confirmation"; public usesRollingDeadline: boolean; + public isAcceptance: boolean; + public autoConfirm: boolean; protected async loadSettings(): Promise { await super.loadSettings(); let branchConfig = await QuestionBranchConfig.findOne({ "name": this.name }); this.usesRollingDeadline = branchConfig && branchConfig.settings && branchConfig.settings.usesRollingDeadline || false; + this.isAcceptance = branchConfig && branchConfig.settings && branchConfig.settings.isAcceptance || false; + this.autoConfirm = branchConfig && branchConfig.settings && branchConfig.settings.autoConfirm || false; } protected serializeSettings(): QuestionBranchSettings { return { ...super.serializeSettings(), + isAcceptance: this.isAcceptance, + autoConfirm: this.autoConfirm, usesRollingDeadline: this.usesRollingDeadline }; } @@ -293,10 +292,11 @@ export async function getOpenConfirmationBranches(user: IUser): Promise { - map[data.name] = data; - return map; - }, {} as DeadlineMap); + + let deadlines = {} as DeadlineMap; + if (user.confirmationDeadline) { + deadlines[user.confirmationDeadline.name] = user.confirmationDeadline; + } let branches = await (BranchConfig.loadAllBranches("Confirmation")) as ConfirmationBranch[]; diff --git a/server/common.ts b/server/common.ts index 515bca5b..3fd56658 100644 --- a/server/common.ts +++ b/server/common.ts @@ -378,7 +378,7 @@ import { IUser, Team, IFormItem } from "./schema"; export const defaultEmailSubjects = { apply: `[${config.eventName}] - Thank you for applying!`, - accept: `[${config.eventName}] - You've been accepted!`, + preConfirm: `[${config.eventName}] - Application Update`, attend: `[${config.eventName}] - Thank you for RSVPing!` }; interface IMailObject { @@ -454,7 +454,7 @@ export async function renderEmailHTML(markdown: string, user: IUser): Promise { let question = user.applicationData.find(data => data.name === name); return formatFormItem(question); @@ -515,12 +515,13 @@ export async function isBranchOpen(branchName: string, user: IUser, requestType: let openDate = branch.open; let closeDate = branch.close; - if (requestType === ApplicationType.Confirmation && user.confirmationDeadlines) { - let times = user.confirmationDeadlines.find((d) => d.name === branch.name); - if (times) { - openDate = times.open; - closeDate = times.close; - } + if (requestType === ApplicationType.Confirmation && user.confirmationDeadline && user.confirmationDeadline.name.toLowerCase() === branchName.toLowerCase()) { + openDate = user.confirmationDeadline.open; + closeDate = user.confirmationDeadline.close; + } + + if (branch instanceof ConfirmationBranch && branch.autoConfirm) { + return false; } if (moment().isBetween(openDate, closeDate)) { diff --git a/server/config/questions.json b/server/config/questions.json index f5f1ffde..f48fe7eb 100644 --- a/server/config/questions.json +++ b/server/config/questions.json @@ -15,328 +15,6 @@ "label": "Full name", "required": true }, - { - "name": "school", - "type": "select", - "label": "What college or university do you currently attend?", - "options": [ - "Georgia Institute of Technology", - "Abraham Baldwin Agricultural College", - "Albany Medical College", - "Albany State University", - "Arizona State University", - "Armstrong State University", - "Atlanta Metropolitan State College", - "Auburn University", - "Augusta University", - "Bainbridge State College", - "Baylor College of Medicine", - "Baylor University", - "Binghamton University", - "Boston College", - "Boston University", - "Bowling Green State University", - "Brandeis University", - "Brigham Young University", - "Brown University", - "California Institute of Technology", - "Carnegie Mellon University", - "Case Western Reserve University", - "City College of New York", - "Clark Atlanta University", - "Clarkson University", - "Clayton State University", - "Clemson University", - "College of Coastal Georgia", - "College of William and Mary", - "Colorado School of Mines", - "Colorado State University", - "Columbia University", - "Columbus State University", - "Cornell University", - "Creighton University", - "Dalton State College", - "Dartmouth College", - "Darton State College", - "Drexel University", - "Duke University", - "East Carolina University", - "East Georgia State College", - "Emory University", - "Florida Atlantic University", - "Florida Institute of Technology", - "Florida International University", - "Florida State University", - "Fort Valley State University", - "George Mason University", - "George Washington University", - "Georgetown University", - "Georgia College & State University", - "Georgia Gwinnett College", - "Georgia Highlands College", - "Georgia Southern University", - "Georgia Southwestern State University", - "Georgia State University", - "Gordon State College", - "Harvard University", - "Howard University", - "Hunter College", - "Icahn School of Medicine at Mount Sinai", - "Illinois Institute of Technology", - "Indiana University - Bloomington", - "Indiana University-Purdue University Indianapolis", - "Iowa State University", - "Johns Hopkins University", - "Kansas State University", - "Kennesaw State University", - "Kent State University", - "Lehigh University", - "Loma Linda University", - "Louisiana State University", - "Louisiana Tech University", - "Loyola University Chicago", - "Marquette University", - "Massachusetts Institute of Technology", - "Medical College of Wisconsin", - "Medical University of South Carolina", - "Miami University", - "Michigan State University", - "Michigan Technological University", - "Middle Georgia State University", - "Mississippi State University", - "Missouri University of Science and Technology", - "Montana State University", - "Morehouse College", - "New Jersey Institute of Technology", - "New Mexico State University", - "New York Medical College", - "New York University", - "North Carolina State University", - "North Dakota State University", - "Northeastern University", - "Northern Arizona University", - "Northern Illinois University", - "Northwestern University", - "Oakland University", - "Ohio State University", - "Ohio University", - "Oklahoma State University", - "Old Dominion University", - "Oregon Health & Science University", - "Oregon State University", - "Pennsylvania State University", - "Portland State University", - "Princeton University", - "Purdue University", - "Queens College, City University of New York", - "Rensselaer Polytechnic Institute", - "Rice University", - "Rochester Institute of Technology", - "Rockefeller University", - "Rush University", - "Rutgers University", - "Saint Louis University", - "San Diego State University", - "San Francisco State University", - "Savannah State University", - "South Georgia State College", - "Southern Illinois University Carbondale", - "Southern Methodist University", - "Spelman College", - "Stanford University", - "Stony Brook University", - "Syracuse University", - "Temple University", - "Texas A&M University", - "Texas Tech University", - "The Catholic University of America", - "Thomas Jefferson University", - "Tufts University", - "Tulane University", - "Uniformed Services University of the Health Sciences", - "University at Albany, SUNY", - "University at Buffalo", - "University of Akron", - "University of Alabama - Tuscaloosa", - "University of Alabama at Birmingham", - "University of Alabama in Huntsville", - "University of Alaska Fairbanks", - "University of Arizona", - "University of Arkansas - Fayetteville", - "University of California, Berkeley", - "University of California, Davis", - "University of California, Irvine", - "University of California, Los Angeles", - "University of California, Merced", - "University of California, Riverside", - "University of California, San Diego", - "University of California, San Francisco", - "University of California, Santa Barbara", - "University of California, Santa Cruz", - "University of Central Florida", - "University of Chicago", - "University of Cincinnati", - "University of Colorado Boulder", - "University of Connecticut", - "University of Dayton", - "University of Delaware", - "University of Denver", - "University of Florida", - "University of Georgia", - "University of Hawaii at Manoa", - "University of Houston", - "University of Idaho", - "University of Illinois at Chicago", - "University of Illinois at Urbana–Champaign", - "University of Iowa", - "University of Kansas", - "University of Kentucky", - "University of Louisville", - "University of Maine", - "University of Maryland, Baltimore", - "University of Maryland, Baltimore County", - "University of Maryland, College Park", - "University of Massachusetts Amherst", - "University of Massachusetts Boston", - "University of Massachusetts Lowell", - "University of Memphis", - "University of Miami", - "University of Michigan", - "University of Minnesota", - "University of Mississippi", - "University of Missouri–Columbia", - "University of Missouri–Kansas City", - "University of Missouri–St. Louis", - "University of Montana", - "University of Nebraska–Lincoln", - "University of Nevada, Las Vegas", - "University of Nevada, Reno", - "University of New Hampshire", - "University of New Mexico", - "University of New Orleans", - "University of North Carolina at Chapel Hill", - "University of North Carolina at Charlotte", - "University of North Carolina at Greensboro", - "University of North Dakota", - "University of North Georgia", - "University of North Texas", - "University of Notre Dame", - "University of Oklahoma", - "University of Oregon", - "University of Pennsylvania", - "University of Pittsburgh", - "University of Rhode Island", - "University of Rochester", - "University of South Alabama", - "University of South Carolina", - "University of South Florida", - "University of Southern California", - "University of Southern Mississippi", - "University of Tennessee, Knoxville", - "University of Texas MD Anderson Cancer Center", - "University of Texas at Arlington", - "University of Texas at Austin", - "University of Texas at Dallas", - "University of Texas at El Paso", - "University of Texas at San Antonio", - "University of Toledo", - "University of Toronto", - "University of Utah", - "University of Vermont", - "University of Virginia", - "University of Washington", - "University of Waterloo", - "University of West Georgia", - "University of Wisconsin–Madison", - "University of Wisconsin–Milwaukee", - "University of Wyoming", - "Utah State University", - "Valdosta State University", - "Vanderbilt University", - "Virginia Commonwealth University", - "Virginia Polytechnic Institute and State University", - "Wake Forest University", - "Washington State University", - "Washington University in St. Louis", - "Wayne State University", - "Wesleyan University", - "West Virginia University", - "Wright State University", - "Yale University", - "Yeshiva University" - ], - "hasOther": true, - "required": true - }, - { - "name": "school-year", - "type": "select", - "label": "What year of studies are you currently in?", - "options": [ - "First year", - "Second year", - "Third year", - "Fourth year", - "Fifth year+", - "Master's", - "PhD" - ], - "required": true - }, - { - "name": "major", - "label": "What is your major?", - "type": "select", - "options": [ - "", - "Accounting", - "Aerospace Engineering", - "Applied Physics", - "Architecture", - "Astronomy or Astrophysics", - "Biochemistry", - "Biology", - "Biomedical Engineering", - "Business", - "Chemical Engineering", - "Chemistry", - "Civil Engineering", - "Computational Media", - "Computer Engineering", - "Computer Science", - "Discrete Mathematics", - "Drama or Theater", - "Earth and Atmospheric Sciences", - "Economics", - "Electrical Engineering", - "English or Literature, Media, and Communication", - "Environmental Engineering", - "Film and Media Studies", - "Finance", - "Global Studies and Languages", - "History", - "Humanities", - "Industrial Design", - "Industrial Engineering", - "International Affairs or International Relations", - "Linguistics", - "Materials Science and Engineering", - "Mathematics", - "Mechanical Engineering", - "Music", - "Music Technology", - "Neuroscience", - "Nuclear and Radiological Engineering", - "Philosophy", - "Physics", - "Political Science", - "Psychology", - "Public Policy", - "Urban Planning" - ], - "hasOther": true, - "required": true - }, { "name": "gender", "label": "Gender", @@ -384,45 +62,6 @@ "label": "What do you hope to learn at HackGT?", "type": "textarea", "required": true - }, - { - "name": "resources", - "label": "Help us help you: what resources would you like to see at HackGT?", - "type": "textarea", - "required": true - }, - { - "name": "tshirt-size", - "label": "What is your T-shirt size?", - "type": "radio", - "options": [ - "Extra small", - "Small", - "Medium", - "Large", - "Extra large", - "2XL", - "3XL" - ], - "required": true - }, - { - "name": "dietary-restrictions", - "label": "Do you have any dietary restrictions?", - "type": "checkbox", - "options": [ - "Vegetarian", - "Vegan", - "Gluten-free" - ], - "hasOther": true, - "required": false - }, - { - "name": "other", - "label": "Anything else you want us to know?", - "type": "textarea", - "required": false } ] }, @@ -471,313 +110,6 @@ ], "required": true }, - { - "name": "school-if-student", - "label": "If so, what college or university do you attend?", - "type": "select", - "options": [ - "Georgia Institute of Technology", - "Abraham Baldwin Agricultural College", - "Albany Medical College", - "Albany State University", - "Arizona State University", - "Armstrong State University", - "Atlanta Metropolitan State College", - "Auburn University", - "Augusta University", - "Bainbridge State College", - "Baylor College of Medicine", - "Baylor University", - "Binghamton University", - "Boston College", - "Boston University", - "Bowling Green State University", - "Brandeis University", - "Brigham Young University", - "Brown University", - "California Institute of Technology", - "Carnegie Mellon University", - "Case Western Reserve University", - "City College of New York", - "Clark Atlanta University", - "Clarkson University", - "Clayton State University", - "Clemson University", - "College of Coastal Georgia", - "College of William and Mary", - "Colorado School of Mines", - "Colorado State University", - "Columbia University", - "Columbus State University", - "Cornell University", - "Creighton University", - "Dalton State College", - "Dartmouth College", - "Darton State College", - "Drexel University", - "Duke University", - "East Carolina University", - "East Georgia State College", - "Emory University", - "Florida Atlantic University", - "Florida Institute of Technology", - "Florida International University", - "Florida State University", - "Fort Valley State University", - "George Mason University", - "George Washington University", - "Georgetown University", - "Georgia College & State University", - "Georgia Gwinnett College", - "Georgia Highlands College", - "Georgia Southern University", - "Georgia Southwestern State University", - "Georgia State University", - "Gordon State College", - "Harvard University", - "Howard University", - "Hunter College", - "Icahn School of Medicine at Mount Sinai", - "Illinois Institute of Technology", - "Indiana University - Bloomington", - "Indiana University-Purdue University Indianapolis", - "Iowa State University", - "Johns Hopkins University", - "Kansas State University", - "Kennesaw State University", - "Kent State University", - "Lehigh University", - "Loma Linda University", - "Louisiana State University", - "Louisiana Tech University", - "Loyola University Chicago", - "Marquette University", - "Massachusetts Institute of Technology", - "Medical College of Wisconsin", - "Medical University of South Carolina", - "Miami University", - "Michigan State University", - "Michigan Technological University", - "Middle Georgia State University", - "Mississippi State University", - "Missouri University of Science and Technology", - "Montana State University", - "Morehouse College", - "New Jersey Institute of Technology", - "New Mexico State University", - "New York Medical College", - "New York University", - "North Carolina State University", - "North Dakota State University", - "Northeastern University", - "Northern Arizona University", - "Northern Illinois University", - "Northwestern University", - "Oakland University", - "Ohio State University", - "Ohio University", - "Oklahoma State University", - "Old Dominion University", - "Oregon Health & Science University", - "Oregon State University", - "Pennsylvania State University", - "Portland State University", - "Princeton University", - "Purdue University", - "Queens College, City University of New York", - "Rensselaer Polytechnic Institute", - "Rice University", - "Rochester Institute of Technology", - "Rockefeller University", - "Rush University", - "Rutgers University", - "Saint Louis University", - "San Diego State University", - "San Francisco State University", - "Savannah State University", - "South Georgia State College", - "Southern Illinois University Carbondale", - "Southern Methodist University", - "Spelman College", - "Stanford University", - "Stony Brook University", - "Syracuse University", - "Temple University", - "Texas A&M University", - "Texas Tech University", - "The Catholic University of America", - "Thomas Jefferson University", - "Tufts University", - "Tulane University", - "Uniformed Services University of the Health Sciences", - "University at Albany, SUNY", - "University at Buffalo", - "University of Akron", - "University of Alabama - Tuscaloosa", - "University of Alabama at Birmingham", - "University of Alabama in Huntsville", - "University of Alaska Fairbanks", - "University of Arizona", - "University of Arkansas - Fayetteville", - "University of California, Berkeley", - "University of California, Davis", - "University of California, Irvine", - "University of California, Los Angeles", - "University of California, Merced", - "University of California, Riverside", - "University of California, San Diego", - "University of California, San Francisco", - "University of California, Santa Barbara", - "University of California, Santa Cruz", - "University of Central Florida", - "University of Chicago", - "University of Cincinnati", - "University of Colorado Boulder", - "University of Connecticut", - "University of Dayton", - "University of Delaware", - "University of Denver", - "University of Florida", - "University of Georgia", - "University of Hawaii at Manoa", - "University of Houston", - "University of Idaho", - "University of Illinois at Chicago", - "University of Illinois at Urbana–Champaign", - "University of Iowa", - "University of Kansas", - "University of Kentucky", - "University of Louisville", - "University of Maine", - "University of Maryland, Baltimore", - "University of Maryland, Baltimore County", - "University of Maryland, College Park", - "University of Massachusetts Amherst", - "University of Massachusetts Boston", - "University of Massachusetts Lowell", - "University of Memphis", - "University of Miami", - "University of Michigan", - "University of Minnesota", - "University of Mississippi", - "University of Missouri–Columbia", - "University of Missouri–Kansas City", - "University of Missouri–St. Louis", - "University of Montana", - "University of Nebraska–Lincoln", - "University of Nevada, Las Vegas", - "University of Nevada, Reno", - "University of New Hampshire", - "University of New Mexico", - "University of New Orleans", - "University of North Carolina at Chapel Hill", - "University of North Carolina at Charlotte", - "University of North Carolina at Greensboro", - "University of North Dakota", - "University of North Georgia", - "University of North Texas", - "University of Notre Dame", - "University of Oklahoma", - "University of Oregon", - "University of Pennsylvania", - "University of Pittsburgh", - "University of Rhode Island", - "University of Rochester", - "University of South Alabama", - "University of South Carolina", - "University of South Florida", - "University of Southern California", - "University of Southern Mississippi", - "University of Tennessee, Knoxville", - "University of Texas MD Anderson Cancer Center", - "University of Texas at Arlington", - "University of Texas at Austin", - "University of Texas at Dallas", - "University of Texas at El Paso", - "University of Texas at San Antonio", - "University of Toledo", - "University of Toronto", - "University of Utah", - "University of Vermont", - "University of Virginia", - "University of Washington", - "University of Waterloo", - "University of West Georgia", - "University of Wisconsin–Madison", - "University of Wisconsin–Milwaukee", - "University of Wyoming", - "Utah State University", - "Valdosta State University", - "Vanderbilt University", - "Virginia Commonwealth University", - "Virginia Polytechnic Institute and State University", - "Wake Forest University", - "Washington State University", - "Washington University in St. Louis", - "Wayne State University", - "Wesleyan University", - "West Virginia University", - "Wright State University", - "Yale University", - "Yeshiva University" - ], - "hasOther": true, - "required": false - }, - { - "name": "major", - "label": "If so, what is your major?", - "type": "select", - "options": [ - "", - "Accounting", - "Aerospace Engineering", - "Applied Physics", - "Architecture", - "Astronomy or Astrophysics", - "Biochemistry", - "Biology", - "Biomedical Engineering", - "Business", - "Chemical Engineering", - "Chemistry", - "Civil Engineering", - "Computational Media", - "Computer Engineering", - "Computer Science", - "Discrete Mathematics", - "Drama or Theater", - "Earth and Atmospheric Sciences", - "Economics", - "Electrical Engineering", - "English or Literature, Media, and Communication", - "Environmental Engineering", - "Film and Media Studies", - "Finance", - "Global Studies and Languages", - "History", - "Humanities", - "Industrial Design", - "Industrial Engineering", - "International Affairs or International Relations", - "Linguistics", - "Materials Science and Engineering", - "Mathematics", - "Mechanical Engineering", - "Music", - "Music Technology", - "Neuroscience", - "Nuclear and Radiological Engineering", - "Philosophy", - "Physics", - "Political Science", - "Psychology", - "Public Policy", - "Urban Planning" - ], - "hasOther": true, - "required": false - }, { "name": "job-if-not-student", "label": "If not, what do you do?", @@ -878,1053 +210,27 @@ ] }, { - "name": "Volunteer", + "name": "Waitlist", "text": [ { - "for": "volunteer-role", - "type": "h3", - "content": "Volunteer Type" - }, - { - "for": "volunteer-role", - "type": "p", - "content": "We have two types of volunteering positions you may apply for: Core Volunteers and General Volunteers." - }, - { - "for": "volunteer-role", - "type": "h5", - "content": "Core Volunteer" - }, - { - "for": "volunteer-role", - "type": "p", - "content": "Core Volunteer responsibilities include being assigned to a major role during the event, leading other volunteers, and liaisoning between general volunteers and volunteer coordinators. Becoming a Core Volunteer requires a greater time commitment: you'll be expected to attend two training sessions prior to the event and be present for the majority of the event." - }, - { - "for": "volunteer-role", + "for": "confirmation", "type": "h5", - "content": "General Volunteer" - }, - { - "for": "volunteer-role", - "type": "p", - "content": "General Volunteer responsibilities might include setting up and tearing down the venue, managing registration, setting up and cleaning up meals, and working a help desk. Becoming a General Volunteer requires less of a time commitment: you'll be expected to attend one training session prior to the event and work at least 2 4-hour shifts during the event." + "content": "We will notify you by 9 PM on the day the event starts" } ], "questions": [ - { - "name": "name", - "type": "text", - "label": "Full name", - "required": true - }, { "name": "phone", - "type": "tel", - "label": "Phone number", - "required": true - }, - { - "name": "gender", - "label": "Gender", - "type": "radio", - "options": ["Male", "Female", "Prefer not to answer"], - "hasOther": true, - "required": true - }, - { - "name": "race", - "label": "Race / ethnicity", - "type": "checkbox", - "options": [ - "Non-Hispanic White", - "Black or African American", - "Latino, Latina, or Hispanic American", - "South Asian or Indian American", - "East Asian or Asian American", - "Middle Eastern or Arab American", - "Native American or Alaskan Native", - "Hawaiian or Pacific Islander" - ], - "hasOther": true, - "required": false - }, - { - "name": "school-year", - "type": "select", - "label": "What year of studies are you currently in?", - "options": [ - "First year", - "Second year", - "Third year", - "Fourth year", - "Fifth year+", - "Master's", - "PhD" - ], - "required": true - }, - { - "name": "major", - "label": "What is your major?", - "type": "select", - "options": [ - "", - "Accounting", - "Aerospace Engineering", - "Applied Physics", - "Architecture", - "Astronomy or Astrophysics", - "Biochemistry", - "Biology", - "Biomedical Engineering", - "Business", - "Chemical Engineering", - "Chemistry", - "Civil Engineering", - "Computational Media", - "Computer Engineering", - "Computer Science", - "Discrete Mathematics", - "Drama or Theater", - "Earth and Atmospheric Sciences", - "Economics", - "Electrical Engineering", - "English or Literature, Media, and Communication", - "Environmental Engineering", - "Film and Media Studies", - "Finance", - "Global Studies and Languages", - "History", - "Humanities", - "Industrial Design", - "Industrial Engineering", - "International Affairs or International Relations", - "Linguistics", - "Materials Science and Engineering", - "Mathematics", - "Mechanical Engineering", - "Music", - "Music Technology", - "Neuroscience", - "Nuclear and Radiological Engineering", - "Philosophy", - "Physics", - "Political Science", - "Psychology", - "Public Policy", - "Urban Planning" - ], - "hasOther": true, - "required": true - }, - { - "name": "career-aspirations", - "label": "What are your career aspirations within that field?", - "type": "textarea", - "required": true - }, - { - "name": "how-know-about-hackgt", - "label": "How did you hear about HackGT?", - "type": "textarea", - "required": true - }, - { - "name": "hackathon-attendance", - "label": "Have you attended HackGT or any other hackathons in the past?", - "type": "radio", - "options": [ - "Yes, HackGT and other hackathons", - "Yes, only HackGT", - "Yes, other hackathons but not HackGT", - "No" - ], - "required": true - }, - { - "name": "hackathon-experiences", - "label": "If so, tell us about your experience(s)", - "type": "textarea", - "required": false - }, - { - "name": "volunteer-role", - "label": "What role do you want to be as a volunteer?", - "type": "radio", - "options": [ - "Core", - "General" - ], - "required": true - }, - { - "name": "why-you", - "label": "What traits do you possess that will make you a good volunteer?", - "type": "textarea", - "required": true - }, - { - "name": "example-experience", - "label": "Describe a time when you have worked with someone different than yourself and what you learned during that experience", - "type": "textarea", - "required": true - }, - { - "name": "example-disagreement", - "label": "Describe a situation when you did not agree with a decision being made by someone leading you and how you handled or responded to that situation", - "type": "textarea", - "required": true - }, - { - "name": "hope-to-gain", - "label": "What do you hope to gain by volunteering at HackGT?", - "type": "textarea", - "required": true - }, - { - "name": "tshirt-size", - "label": "What is your T-shirt size?", - "type": "radio", - "options": [ - "Extra small", - "Small", - "Medium", - "Large", - "Extra large", - "2XL", - "3XL" - ], + "label": "Please give your phone number to confirm spot on the waitlist.", + "type": "text", "required": true - }, - { - "name": "dietary-restrictions", - "label": "Do you have any dietary restrictions?", - "type": "checkbox", - "options": [ - "Vegetarian", - "Vegan", - "Gluten-free" - ], - "hasOther": true, - "required": false - }, - { - "name": "other", - "label": "Anything else you want us to know?", - "type": "textarea", - "required": false } ] }, { - "name": "Alumni Mentor", + "name": "Rejected", "text": [], - "questions": [ - { - "name": "name", - "type": "text", - "label": "Full name", - "required": true - }, - { - "name": "gender", - "label": "Gender", - "type": "radio", - "options": ["Male", "Female", "Prefer not to answer"], - "hasOther": true, - "required": true - }, - { - "name": "race", - "label": "Race / ethnicity", - "type": "checkbox", - "options": [ - "Non-Hispanic White", - "Black or African American", - "Latino, Latina, or Hispanic American", - "South Asian or Indian American", - "East Asian or Asian American", - "Middle Eastern or Arab American", - "Native American or Alaskan Native", - "Hawaiian or Pacific Islander" - ], - "hasOther": true, - "required": false - }, - { - "name": "school", - "type": "select", - "label": "What college or university did you attend?", - "options": [ - "Georgia Institute of Technology", - "Abraham Baldwin Agricultural College", - "Albany Medical College", - "Albany State University", - "Arizona State University", - "Armstrong State University", - "Atlanta Metropolitan State College", - "Auburn University", - "Augusta University", - "Bainbridge State College", - "Baylor College of Medicine", - "Baylor University", - "Binghamton University", - "Boston College", - "Boston University", - "Bowling Green State University", - "Brandeis University", - "Brigham Young University", - "Brown University", - "California Institute of Technology", - "Carnegie Mellon University", - "Case Western Reserve University", - "City College of New York", - "Clark Atlanta University", - "Clarkson University", - "Clayton State University", - "Clemson University", - "College of Coastal Georgia", - "College of William and Mary", - "Colorado School of Mines", - "Colorado State University", - "Columbia University", - "Columbus State University", - "Cornell University", - "Creighton University", - "Dalton State College", - "Dartmouth College", - "Darton State College", - "Drexel University", - "Duke University", - "East Carolina University", - "East Georgia State College", - "Emory University", - "Florida Atlantic University", - "Florida Institute of Technology", - "Florida International University", - "Florida State University", - "Fort Valley State University", - "George Mason University", - "George Washington University", - "Georgetown University", - "Georgia College & State University", - "Georgia Gwinnett College", - "Georgia Highlands College", - "Georgia Southern University", - "Georgia Southwestern State University", - "Georgia State University", - "Gordon State College", - "Harvard University", - "Howard University", - "Hunter College", - "Icahn School of Medicine at Mount Sinai", - "Illinois Institute of Technology", - "Indiana University - Bloomington", - "Indiana University-Purdue University Indianapolis", - "Iowa State University", - "Johns Hopkins University", - "Kansas State University", - "Kennesaw State University", - "Kent State University", - "Lehigh University", - "Loma Linda University", - "Louisiana State University", - "Louisiana Tech University", - "Loyola University Chicago", - "Marquette University", - "Massachusetts Institute of Technology", - "Medical College of Wisconsin", - "Medical University of South Carolina", - "Miami University", - "Michigan State University", - "Michigan Technological University", - "Middle Georgia State University", - "Mississippi State University", - "Missouri University of Science and Technology", - "Montana State University", - "Morehouse College", - "New Jersey Institute of Technology", - "New Mexico State University", - "New York Medical College", - "New York University", - "North Carolina State University", - "North Dakota State University", - "Northeastern University", - "Northern Arizona University", - "Northern Illinois University", - "Northwestern University", - "Oakland University", - "Ohio State University", - "Ohio University", - "Oklahoma State University", - "Old Dominion University", - "Oregon Health & Science University", - "Oregon State University", - "Pennsylvania State University", - "Portland State University", - "Princeton University", - "Purdue University", - "Queens College, City University of New York", - "Rensselaer Polytechnic Institute", - "Rice University", - "Rochester Institute of Technology", - "Rockefeller University", - "Rush University", - "Rutgers University", - "Saint Louis University", - "San Diego State University", - "San Francisco State University", - "Savannah State University", - "South Georgia State College", - "Southern Illinois University Carbondale", - "Southern Methodist University", - "Spelman College", - "Stanford University", - "Stony Brook University", - "Syracuse University", - "Temple University", - "Texas A&M University", - "Texas Tech University", - "The Catholic University of America", - "Thomas Jefferson University", - "Tufts University", - "Tulane University", - "Uniformed Services University of the Health Sciences", - "University at Albany, SUNY", - "University at Buffalo", - "University of Akron", - "University of Alabama - Tuscaloosa", - "University of Alabama at Birmingham", - "University of Alabama in Huntsville", - "University of Alaska Fairbanks", - "University of Arizona", - "University of Arkansas - Fayetteville", - "University of California, Berkeley", - "University of California, Davis", - "University of California, Irvine", - "University of California, Los Angeles", - "University of California, Merced", - "University of California, Riverside", - "University of California, San Diego", - "University of California, San Francisco", - "University of California, Santa Barbara", - "University of California, Santa Cruz", - "University of Central Florida", - "University of Chicago", - "University of Cincinnati", - "University of Colorado Boulder", - "University of Connecticut", - "University of Dayton", - "University of Delaware", - "University of Denver", - "University of Florida", - "University of Georgia", - "University of Hawaii at Manoa", - "University of Houston", - "University of Idaho", - "University of Illinois at Chicago", - "University of Illinois at Urbana–Champaign", - "University of Iowa", - "University of Kansas", - "University of Kentucky", - "University of Louisville", - "University of Maine", - "University of Maryland, Baltimore", - "University of Maryland, Baltimore County", - "University of Maryland, College Park", - "University of Massachusetts Amherst", - "University of Massachusetts Boston", - "University of Massachusetts Lowell", - "University of Memphis", - "University of Miami", - "University of Michigan", - "University of Minnesota", - "University of Mississippi", - "University of Missouri–Columbia", - "University of Missouri–Kansas City", - "University of Missouri–St. Louis", - "University of Montana", - "University of Nebraska–Lincoln", - "University of Nevada, Las Vegas", - "University of Nevada, Reno", - "University of New Hampshire", - "University of New Mexico", - "University of New Orleans", - "University of North Carolina at Chapel Hill", - "University of North Carolina at Charlotte", - "University of North Carolina at Greensboro", - "University of North Dakota", - "University of North Georgia", - "University of North Texas", - "University of Notre Dame", - "University of Oklahoma", - "University of Oregon", - "University of Pennsylvania", - "University of Pittsburgh", - "University of Rhode Island", - "University of Rochester", - "University of South Alabama", - "University of South Carolina", - "University of South Florida", - "University of Southern California", - "University of Southern Mississippi", - "University of Tennessee, Knoxville", - "University of Texas MD Anderson Cancer Center", - "University of Texas at Arlington", - "University of Texas at Austin", - "University of Texas at Dallas", - "University of Texas at El Paso", - "University of Texas at San Antonio", - "University of Toledo", - "University of Toronto", - "University of Utah", - "University of Vermont", - "University of Virginia", - "University of Washington", - "University of Waterloo", - "University of West Georgia", - "University of Wisconsin–Madison", - "University of Wisconsin–Milwaukee", - "University of Wyoming", - "Utah State University", - "Valdosta State University", - "Vanderbilt University", - "Virginia Commonwealth University", - "Virginia Polytechnic Institute and State University", - "Wake Forest University", - "Washington State University", - "Washington University in St. Louis", - "Wayne State University", - "Wesleyan University", - "West Virginia University", - "Wright State University", - "Yale University", - "Yeshiva University" - ], - "hasOther": true, - "required": true - }, - { - "name": "graduation-year", - "label": "What year did you graduate?", - "type": "text", - "required": true - }, - { - "name": "current-gig", - "label": "What are you up to now? (job, graduate studies, side hustles)", - "type": "text", - "required": true - }, - { - "name": "hackathon-history", - "label": "What are hackathons to you? What are some things you've done in the hackathon space?", - "type": "textarea", - "required": true - }, - { - "name": "mentor-topics", - "label": "What kind of projects can you see yourself providing mentorship for?", - "type": "textarea", - "required": true - }, - { - "name": "current-location", - "label": "Where are you current located? (for transportation purposes)", - "type": "text", - "required": true - }, - { - "name": "what-can-mentor", - "type": "textarea", - "label": "What kind of projects do you see yourself providing mentorship for?", - "required": true - }, - { - "name": "can-hold-workshop", - "type": "radio", - "label": "Would you be interested in giving a tech talk/workshop at HackGT? (Does not need to be technical)", - "options": [ - "Yes", - "No" - ], - "required": true - }, - { - "name": "workshop-elaborate", - "type": "textarea", - "label": "If you already have ideas for a tech talk/workshop, please share.", - "required": false - }, - { - "name": "tshirt-size", - "label": "What is your T-shirt size?", - "type": "radio", - "options": [ - "Extra small", - "Small", - "Medium", - "Large", - "Extra large", - "2XL", - "3XL" - ], - "required": true - }, - { - "name": "dietary-restrictions", - "label": "Do you have any dietary restrictions?", - "type": "checkbox", - "options": [ - "Vegetarian", - "Vegan", - "Gluten-free" - ], - "hasOther": true, - "required": false - }, - { - "name": "other", - "label": "Anything else? Drop us some links (GitHub, LinkedIn, personal site, etc.)", - "type": "textarea", - "required": false - } - ] - }, - { - "name": "Need Reimbursement", - "text": [ - { - "for": "confirmation", - "type": "h5", - "content": "Note that opening ceremonies will begin at approximately 7pm on Friday, October 13th and closing ceremonies will end at approximately 2pm on Sunday, October 15th" - }, - { - "for": "receipt1", - "type": "h5", - "content": "Please upload your flight / travel receipts below" - }, - { - "for": "receipt1", - "type": "p", - "content": "(**Note**: We will not accept receipts submitted after September 15th at 8pm EST)" - }, - { - "for": "receipt-confirmation", - "type": "h5", - "content": "Please confirm some things" - } - ], - "questions": [ - { - "name": "confirmation", - "label": "Will you be attending HackGT this year on October 13th to 15th?", - "type": "radio", - "options": [ - "Yes, I'll be there!", - "Sorry, I can't make it" - ], - "hasOther": false, - "required": true - }, - { - "name": "receipt1", - "label": "Flight / Travel receipt", - "type": "file", - "required": true - }, - { - "name": "receipt-confirmation", - "label": "I understand that my travel reimbursement will only be awarded after HackGT has verified that I submitted something to HackGT.", - "type": "checkbox", - "options": [ - "Yes, I understand" - ], - "hasOther": false, - "required": true - }, - { - "name": "required-items", - "label": "Please confirm the following:", - "type": "checkbox", - "options": [ - "I have read and agree to adhere to the [MLH Code of Conduct](https://static.mlh.io/docs/mlh-code-of-conduct.pdf)", - "I agree to the terms of the [MLH Contest Terms and Conditions](https://github.com/MLH/mlh-policies/blob/master/prize-terms-and-conditions/contest-terms.md)", - "I agree to the terms of the [MLH Privacy Policy](https://mlh.io/privacy)", - "I agree to the [HackGT Liability Waiver](https://drive.google.com/file/d/0B2pRUVzz8hWeUE9oaGxXNUxRTjg/view?usp=sharing)" - ], - "hasOther": false, - "required": true - }, - { - "name": "optional-items", - "label": "Please confirm the following (optional):", - "type": "checkbox", - "options": [ - "I agree to the [HackGT Photo Release](https://drive.google.com/file/d/0B2pRUVzz8hWebXQ1WllYSmI5TGc/view?usp=sharing)" - ], - "hasOther": false, - "required": false - } - ] - }, - { - "name": "Don't Need Reimbursement", - "text": [ - { - "for": "confirmation", - "type": "h5", - "content": "Note that opening ceremonies will begin at approximately 7pm on Friday and closing ceremonies will end at approximately 2pm on Sunday" - }, - { - "for": "required-items", - "type": "h5", - "content": "Please confirm some things" - } - ], - "questions": [ - { - "name": "confirmation", - "label": "Will you be attending HackGT this year on October 13th to 15th?", - "type": "radio", - "options": [ - "Yes, I'll be there!", - "Sorry, I can't make it" - ], - "hasOther": false, - "required": true - }, - { - "name": "required-items", - "label": "Please confirm the following:", - "type": "checkbox", - "options": [ - "I have read and agree to adhere to the [MLH Code of Conduct](https://static.mlh.io/docs/mlh-code-of-conduct.pdf)", - "I agree to the terms of the [MLH Contest Terms and Conditions](https://github.com/MLH/mlh-policies/blob/master/prize-terms-and-conditions/contest-terms.md)", - "I agree to the terms of the [MLH Privacy Policy](https://mlh.io/privacy)", - "I agree to the [HackGT Liability Waiver](https://drive.google.com/file/d/0B2pRUVzz8hWeUE9oaGxXNUxRTjg/view?usp=sharing)" - ], - "hasOther": false, - "required": true - }, - { - "name": "optional-items", - "label": "Please confirm the following (optional):", - "type": "checkbox", - "options": [ - "I agree to the [HackGT Photo Release](https://drive.google.com/file/d/0B2pRUVzz8hWebXQ1WllYSmI5TGc/view?usp=sharing)" - ], - "hasOther": false, - "required": false - } - ] - }, - { - "name": "UF Bus", - "text": [ - { - "for": "confirmation", - "type": "h5", - "content": "Note that opening ceremonies will begin at approximately 7pm on Friday and closing ceremonies will end at approximately 2pm on Sunday. Information about bus logistics will be released shortly. In the meantime, join the [Facebook event](https://www.facebook.com/events/680399305489636)!" - }, - { - "for": "required-items", - "type": "h5", - "content": "Please confirm some things" - } - ], - "questions": [ - { - "name": "confirmation", - "label": "Will you be attending HackGT this year on October 13th to 15th?", - "type": "radio", - "options": [ - "Yes, I'll be there!", - "Sorry, I can't make it" - ], - "hasOther": false, - "required": true - }, - { - "name": "required-items", - "label": "Please confirm the following:", - "type": "checkbox", - "options": [ - "I have read and agree to adhere to the [MLH Code of Conduct](https://static.mlh.io/docs/mlh-code-of-conduct.pdf)", - "I agree to the terms of the [MLH Contest Terms and Conditions](https://github.com/MLH/mlh-policies/blob/master/prize-terms-and-conditions/contest-terms.md)", - "I agree to the terms of the [MLH Privacy Policy](https://mlh.io/privacy)", - "I agree to the [HackGT Liability Waiver](https://drive.google.com/file/d/0B2pRUVzz8hWeUE9oaGxXNUxRTjg/view?usp=sharing)" - ], - "hasOther": false, - "required": true - }, - { - "name": "optional-items", - "label": "Please confirm the following (optional):", - "type": "checkbox", - "options": [ - "I agree to the [HackGT Photo Release](https://drive.google.com/file/d/0B2pRUVzz8hWebXQ1WllYSmI5TGc/view?usp=sharing)" - ], - "hasOther": false, - "required": false - } - ] - }, - { - "name": "UIUC Bus", - "text": [ - { - "for": "confirmation", - "type": "h5", - "content": "Note that opening ceremonies will begin at approximately 7pm on Friday and closing ceremonies will end at approximately 2pm on Sunday. Information about bus logistics will be released shortly. In the meantime, join the [Facebook event](https://www.facebook.com/events/144787056112670)!" - }, - { - "for": "required-items", - "type": "h5", - "content": "Please confirm some things" - } - ], - "questions": [ - { - "name": "confirmation", - "label": "Will you be attending HackGT this year on October 13th to 15th?", - "type": "radio", - "options": [ - "Yes, I'll be there!", - "Sorry, I can't make it" - ], - "hasOther": false, - "required": true - }, - { - "name": "required-items", - "label": "Please confirm the following:", - "type": "checkbox", - "options": [ - "I have read and agree to adhere to the [MLH Code of Conduct](https://static.mlh.io/docs/mlh-code-of-conduct.pdf)", - "I agree to the terms of the [MLH Contest Terms and Conditions](https://github.com/MLH/mlh-policies/blob/master/prize-terms-and-conditions/contest-terms.md)", - "I agree to the terms of the [MLH Privacy Policy](https://mlh.io/privacy)", - "I agree to the [HackGT Liability Waiver](https://drive.google.com/file/d/0B2pRUVzz8hWeUE9oaGxXNUxRTjg/view?usp=sharing)" - ], - "hasOther": false, - "required": true - }, - { - "name": "optional-items", - "label": "Please confirm the following (optional):", - "type": "checkbox", - "options": [ - "I agree to the [HackGT Photo Release](https://drive.google.com/file/d/0B2pRUVzz8hWebXQ1WllYSmI5TGc/view?usp=sharing)" - ], - "hasOther": false, - "required": false - } - ] - }, - { - "name": "UCF/FIU Bus", - "text": [ - { - "for": "confirmation", - "type": "h5", - "content": "Note that opening ceremonies will begin at approximately 7pm on Friday and closing ceremonies will end at approximately 2pm on Sunday. Information about bus logistics will be released shortly. In the meantime, join the [Facebook event](https://www.facebook.com/events/1986575941356390)!" - }, - { - "for": "required-items", - "type": "h5", - "content": "Please confirm some things" - } - ], - "questions": [ - { - "name": "confirmation", - "label": "Will you be attending HackGT this year on October 13th to 15th?", - "type": "radio", - "options": [ - "Yes, I'll be there!", - "Sorry, I can't make it" - ], - "hasOther": false, - "required": true - }, - { - "name": "required-items", - "label": "Please confirm the following:", - "type": "checkbox", - "options": [ - "I have read and agree to adhere to the [MLH Code of Conduct](https://static.mlh.io/docs/mlh-code-of-conduct.pdf)", - "I agree to the terms of the [MLH Contest Terms and Conditions](https://github.com/MLH/mlh-policies/blob/master/prize-terms-and-conditions/contest-terms.md)", - "I agree to the terms of the [MLH Privacy Policy](https://mlh.io/privacy)", - "I agree to the [HackGT Liability Waiver](https://drive.google.com/file/d/0B2pRUVzz8hWeUE9oaGxXNUxRTjg/view?usp=sharing)" - ], - "hasOther": false, - "required": true - }, - { - "name": "optional-items", - "label": "Please confirm the following (optional):", - "type": "checkbox", - "options": [ - "I agree to the [HackGT Photo Release](https://drive.google.com/file/d/0B2pRUVzz8hWebXQ1WllYSmI5TGc/view?usp=sharing)" - ], - "hasOther": false, - "required": false - } - ] - }, - { - "name": "WUStL/Vanderbilt University Bus", - "text": [ - { - "for": "confirmation", - "type": "h5", - "content": "Note that opening ceremonies will begin at approximately 7pm on Friday and closing ceremonies will end at approximately 2pm on Sunday. Information about bus logistics will be released shortly. In the meantime, join the [Facebook event](https://www.facebook.com/events/877088149116702)!" - }, - { - "for": "required-items", - "type": "h5", - "content": "Please confirm some things" - } - ], - "questions": [ - { - "name": "confirmation", - "label": "Will you be attending HackGT this year on October 13th to 15th?", - "type": "radio", - "options": [ - "Yes, I'll be there!", - "Sorry, I can't make it" - ], - "hasOther": false, - "required": true - }, - { - "name": "required-items", - "label": "Please confirm the following:", - "type": "checkbox", - "options": [ - "I have read and agree to adhere to the [MLH Code of Conduct](https://static.mlh.io/docs/mlh-code-of-conduct.pdf)", - "I agree to the terms of the [MLH Contest Terms and Conditions](https://github.com/MLH/mlh-policies/blob/master/prize-terms-and-conditions/contest-terms.md)", - "I agree to the terms of the [MLH Privacy Policy](https://mlh.io/privacy)", - "I agree to the [HackGT Liability Waiver](https://drive.google.com/file/d/0B2pRUVzz8hWeUE9oaGxXNUxRTjg/view?usp=sharing)" - ], - "hasOther": false, - "required": true - }, - { - "name": "optional-items", - "label": "Please confirm the following (optional):", - "type": "checkbox", - "options": [ - "I agree to the [HackGT Photo Release](https://drive.google.com/file/d/0B2pRUVzz8hWebXQ1WllYSmI5TGc/view?usp=sharing)" - ], - "hasOther": false, - "required": false - } - ] - }, - { - "name": "Michigan (Ann Arbor) Bus", - "text": [ - { - "for": "confirmation", - "type": "h5", - "content": "Note that opening ceremonies will begin at approximately 7pm on Friday and closing ceremonies will end at approximately 2pm on Sunday. Information about bus logistics will be released shortly. In the meantime, join the [Facebook event](https://www.facebook.com/events/1926672534329321)" - }, - { - "for": "required-items", - "type": "h5", - "content": "Please confirm some things" - } - ], - "questions": [ - { - "name": "confirmation", - "label": "Will you be attending HackGT this year on October 13th to 15th?", - "type": "radio", - "options": [ - "Yes, I'll be there!", - "Sorry, I can't make it" - ], - "hasOther": false, - "required": true - }, - { - "name": "required-items", - "label": "Please confirm the following:", - "type": "checkbox", - "options": [ - "I have read and agree to adhere to the [MLH Code of Conduct](https://static.mlh.io/docs/mlh-code-of-conduct.pdf)", - "I agree to the terms of the [MLH Contest Terms and Conditions](https://github.com/MLH/mlh-policies/blob/master/prize-terms-and-conditions/contest-terms.md)", - "I agree to the terms of the [MLH Privacy Policy](https://mlh.io/privacy)", - "I agree to the [HackGT Liability Waiver](https://drive.google.com/file/d/0B2pRUVzz8hWeUE9oaGxXNUxRTjg/view?usp=sharing)" - ], - "hasOther": false, - "required": true - }, - { - "name": "optional-items", - "label": "Please confirm the following (optional):", - "type": "checkbox", - "options": [ - "I agree to the [HackGT Photo Release](https://drive.google.com/file/d/0B2pRUVzz8hWebXQ1WllYSmI5TGc/view?usp=sharing)" - ], - "hasOther": false, - "required": false - } - ] - }, - { - "name": "Duke/UNC Chapel Hill Bus", - "text": [ - { - "for": "confirmation", - "type": "h5", - "content": "Note that opening ceremonies will begin at approximately 7pm on Friday and closing ceremonies will end at approximately 2pm on Sunday. Information about bus logistics will be released shortly. In the meantime, join the [Facebook event](https://www.facebook.com/events/1247848358660200)!" - }, - { - "for": "required-items", - "type": "h5", - "content": "Please confirm some things" - } - ], - "questions": [ - { - "name": "confirmation", - "label": "Will you be attending HackGT this year on October 13th to 15th?", - "type": "radio", - "options": [ - "Yes, I'll be there!", - "Sorry, I can't make it" - ], - "hasOther": false, - "required": true - }, - { - "name": "required-items", - "label": "Please confirm the following:", - "type": "checkbox", - "options": [ - "I have read and agree to adhere to the [MLH Code of Conduct](https://static.mlh.io/docs/mlh-code-of-conduct.pdf)", - "I agree to the terms of the [MLH Contest Terms and Conditions](https://github.com/MLH/mlh-policies/blob/master/prize-terms-and-conditions/contest-terms.md)", - "I agree to the terms of the [MLH Privacy Policy](https://mlh.io/privacy)", - "I agree to the [HackGT Liability Waiver](https://drive.google.com/file/d/0B2pRUVzz8hWeUE9oaGxXNUxRTjg/view?usp=sharing)" - ], - "hasOther": false, - "required": true - }, - { - "name": "optional-items", - "label": "Please confirm the following (optional):", - "type": "checkbox", - "options": [ - "I agree to the [HackGT Photo Release](https://drive.google.com/file/d/0B2pRUVzz8hWebXQ1WllYSmI5TGc/view?usp=sharing)" - ], - "hasOther": false, - "required": false - } - ] + "questions": [] }, { "name": "General Confirmation", diff --git a/server/middleware.ts b/server/middleware.ts index 2c0fb568..3fd55f2f 100644 --- a/server/middleware.ts +++ b/server/middleware.ts @@ -5,7 +5,8 @@ import * as os from "os"; import { config, isBranchOpen } from "./common"; import { BranchConfig, ApplicationBranch } from "./branch"; -import { IUser } from "./schema"; +import { User, IUser } from "./schema"; +import * as Branches from "./branch"; // // Express middleware @@ -147,44 +148,78 @@ export async function onlyAllowAnonymousBranch(request: express.Request, respons next(); } +export async function canUserModify(request: express.Request, response: express.Response, next: express.NextFunction) { + let user = await User.findOne({uuid: request.params.uuid}) as IUser; + let branchName = request.params.branch as string; + let questionBranch = (await Branches.BranchConfig.loadAllBranches()).find(branch => branch.name.toLowerCase() === branchName.toLowerCase()); + + if (!(await isBranchOpen(request.params.branch, user, questionBranch instanceof Branches.ApplicationBranch ? ApplicationType.Application : ApplicationType.Confirmation))) { + response.status(400).json({ + "error": "Branch is closed" + }); + return; + } + if (questionBranch instanceof Branches.ApplicationBranch) { + // Don't allow user to modify application if we assigned them a confirmation branch + if (user.confirmationBranch) { + response.status(400).json({ + "error": "You can no longer edit this application" + }); + return; + } + if (user.applied && branchName.toLowerCase() !== user.applicationBranch.toLowerCase()) { + response.status(400).json({ + "error": "You can only edit the application branch that you originally submitted" + }); + return; + } + } else if (questionBranch instanceof Branches.ConfirmationBranch) { + if (!user.confirmationBranch) { + response.status(400).json({ + "error": "You can't confirm for that branch" + }); + } else if (user.confirmationBranch && branchName.toLowerCase() !== user.confirmationBranch.toLowerCase()) { + response.status(400).json({ + "error": "You can only submit the confirmation branch you were assigned" + }); + return; + } + } else { + response.status(400).json({ + "error": "Invalid application branch" + }); + return; + } + + next(); +} export function branchRedirector(requestType: ApplicationType): (request: express.Request, response: express.Response, next: express.NextFunction) => Promise { return async (request: express.Request, response: express.Response, next: express.NextFunction) => { // TODO: fix branch names so they have a machine ID and human label let user = request.user as IUser; - if (requestType === ApplicationType.Application && user.accepted) { + if (requestType === ApplicationType.Application && user.confirmationBranch) { response.redirect("/"); + return; } - if (requestType === ApplicationType.Confirmation) { - if (!user.accepted || !user.applied) { - response.redirect("/"); - return; - } - if (user.attending && !user.confirmationBranch) { - response.redirect("/"); - return; - } - } - - let questionBranches: string[]; - if (requestType === ApplicationType.Application) { - questionBranches = (await BranchConfig.getOpenBranches("Application")).map(branch => branch.name.toLowerCase()); - } else { - questionBranches = ((await BranchConfig.loadBranchFromDB(user.applicationBranch)) as ApplicationBranch).confirmationBranches.map(branchName => branchName.toLowerCase()); - - // Check for open branches based on user's individual confirmation deadlines - questionBranches.filter((branch) => { - return isBranchOpen(branch, user, ApplicationType.Confirmation); - }); + if (requestType === ApplicationType.Confirmation && !user.confirmationBranch) { + response.redirect("/"); + return; } if (request.params.branch) { let branchName = (request.params.branch as string).toLowerCase(); + let branch = (await BranchConfig.loadBranchFromDB(request.params.branch as string)); + if ((branch.type === "Application" && requestType !== ApplicationType.Application) || (branch.type === "Confirmation" && requestType !== ApplicationType.Confirmation)) { + response.redirect("/"); + return; + } - if (!isBranchOpen(branchName, user, requestType)) { + if (!(await isBranchOpen(branchName, user, requestType))) { response.redirect("/"); + return; } if (requestType === ApplicationType.Application) { @@ -193,49 +228,32 @@ export function branchRedirector(requestType: ApplicationType): (request: expres response.redirect(`/apply/${encodeURIComponent(user.applicationBranch.toLowerCase())}`); return; } - let questionBranch = questionBranches.find(branch => branch === branchName.toLowerCase())!; - if (!questionBranch) { - response.redirect("/"); - return; - } } if (requestType === ApplicationType.Confirmation) { - // Redirect directly to branch if there is an existing application or confirmation - if (user.attending && branchName.toLowerCase() !== user.confirmationBranch.toLowerCase()) { - response.redirect(`/confirm/${encodeURIComponent(user.confirmationBranch.toLowerCase())}`); - return; - } - // TODO why is this !user.attending? - if (questionBranches.indexOf(branchName.toLowerCase()) === -1 && !user.attending) { + if (request.params.branch !== user.confirmationBranch ) { response.redirect("/"); return; } } } else { + let targetBranch: string | undefined; if (requestType === ApplicationType.Application && user.applied && user.applicationBranch) { - questionBranches = [user.applicationBranch]; + targetBranch = user.applicationBranch; + } else if (requestType === ApplicationType.Confirmation) { + targetBranch = user.confirmationBranch; } - // Redirect for applications on "skip confirmation" branches - if (requestType === ApplicationType.Confirmation && user.attending && !user.confirmationBranch && user.applicationBranch) { - questionBranches = [user.applicationBranch]; - requestType = ApplicationType.Application; - } - - if (requestType === ApplicationType.Confirmation && user.attending && user.confirmationBranch) { - questionBranches = [user.confirmationBranch]; - } - - if (questionBranches.length === 1) { - const uriBranch = encodeURIComponent(questionBranches[0]); + if (targetBranch) { + const uriBranch = encodeURIComponent(targetBranch); const redirPath = requestType === ApplicationType.Application ? "apply" : "confirm"; response.redirect(`/${redirPath}/${uriBranch}`); return; } - // If there are no valid branches, redirect to main page. - if (questionBranches.length === 0) { + // If there are no open application branches, redirect to main page. + if ((await BranchConfig.getOpenBranches("Application")).length === 0) { response.redirect("/"); + return; } } diff --git a/server/routes/api/graphql.ts b/server/routes/api/graphql.ts index 43c74f12..096cbe2e 100644 --- a/server/routes/api/graphql.ts +++ b/server/routes/api/graphql.ts @@ -156,10 +156,10 @@ async function findQuestions( .map(item => recordToFormItem(item, user.applicationBranch)) )); } - if (user.attending) { + if (user.confirmed) { items = items.concat(await Promise.all(user.confirmationData .reduce(questionFilter, []) - .map(item => recordToFormItem(item, user.confirmationBranch)) + .map(item => recordToFormItem(item, user.confirmationBranch!)) )); } return items; @@ -281,9 +281,9 @@ async function userRecordToGraphql(user: IUser): Promise> { user.applicationSubmitTime.toDateString() } : undefined; - const confirmation: types.Branch | undefined = user.attending ? { - type: user.confirmationBranch, - data: await Promise.all(user.confirmationData.map(item => recordToFormItem(item, user.confirmationBranch))), + const confirmation: types.Branch | undefined = user.confirmed ? { + type: user.confirmationBranch!, + data: await Promise.all(user.confirmationData.map(item => recordToFormItem(item, user.confirmationBranch!))), start_time: user.confirmationStartTime && user.confirmationStartTime.toDateString(), submit_time: user.confirmationSubmitTime && @@ -317,8 +317,9 @@ async function userRecordToGraphql(user: IUser): Promise> { applied: !!user.applied, accepted: !!user.accepted, - accepted_and_notified: !!user.acceptedEmailSent, - attending: !!user.attending, + accepted_and_notified: !!user.preConfirmEmailSent, + confirmed: !!user.confirmed, + confirmationBranch: user.confirmationBranch, application, confirmation, diff --git a/server/routes/api/settings.ts b/server/routes/api/settings.ts index 4e3a4fc3..dec94139 100644 --- a/server/routes/api/settings.ts +++ b/server/routes/api/settings.ts @@ -156,14 +156,14 @@ settingsRoutes.route("/branch_roles") } // Set available confirmation branches (if application branch) if (branch instanceof Branches.ApplicationBranch) { - branch.confirmationBranches = branchData.confirmationBranches || []; branch.allowAnonymous = branchData.allowAnonymous || false; - branch.autoAccept = branchData.autoAccept || false; - branch.noConfirmation = branchData.noConfirmation || false; + branch.autoAccept = branchData.autoAccept || "disabled"; } // Set rolling deadline flag (if confirmation branch) if (branch instanceof Branches.ConfirmationBranch) { branch.usesRollingDeadline = branchData.usesRollingDeadline || false; + branch.isAcceptance = branchData.isAcceptance || false; + branch.autoConfirm = branchData.autoConfirm || false; } await branch.save(); @@ -200,8 +200,8 @@ settingsRoutes.route("/email_content/:type") if (type.match(/-apply$/)) { subject = defaultEmailSubjects.apply; } - else if (type.match(/-accept$/)) { - subject = defaultEmailSubjects.accept; + else if (type.match(/-pre-confirm$/)) { + subject = defaultEmailSubjects.preConfirm; } else if (type.match(/-attend$/)) { subject = defaultEmailSubjects.attend; diff --git a/server/routes/api/user.ts b/server/routes/api/user.ts index 6d1f5f86..8bf2fd92 100644 --- a/server/routes/api/user.ts +++ b/server/routes/api/user.ts @@ -7,12 +7,12 @@ import * as uuid from "uuid/v4"; import { STORAGE_ENGINE, formatSize, - config, getSetting, renderEmailHTML, renderEmailText, sendMailAsync, defaultEmailSubjects, isBranchOpen + config, getSetting, renderEmailHTML, renderEmailText, sendMailAsync, defaultEmailSubjects } from "../../common"; import { MAX_FILE_SIZE, postParser, uploadHandler, isAdmin, isUserOrAdmin, ApplicationType, - trackEvent + trackEvent, canUserModify } from "../../middleware"; import { IFormItem, @@ -35,6 +35,7 @@ let postApplicationBranchErrorHandler: express.ErrorRequestHandler = (err, reque } }; +// We don't use canUserModify here, instead check for admin registrationRoutes.route("/:branch").post( isAdmin, postParser, @@ -45,21 +46,25 @@ registrationRoutes.route("/:branch").post( userRoutes.route("/application/:branch").post( isUserOrAdmin, + canUserModify, postParser, uploadHandler.any(), postApplicationBranchErrorHandler, postApplicationBranchHandler(false) ).delete( isUserOrAdmin, + canUserModify, deleteApplicationBranchHandler); userRoutes.route("/confirmation/:branch").post( isUserOrAdmin, + canUserModify, postParser, uploadHandler.any(), postApplicationBranchErrorHandler, postApplicationBranchHandler(false) ).delete( isUserOrAdmin, + canUserModify, deleteApplicationBranchHandler ); function postApplicationBranchHandler(anonymous: boolean): express.Handler { @@ -92,35 +97,6 @@ function postApplicationBranchHandler(anonymous: boolean): express.Handler { return; } - // Allow admin to submit an already closed branch (for anonymous submission purposes) - if (!user.admin && !isBranchOpen(request.params.branch, user, questionBranch instanceof Branches.ApplicationBranch ? ApplicationType.Application : ApplicationType.Confirmation)) { - response.status(400).json({ - "error": "Branch is closed" - }); - return; - } - - if (questionBranch instanceof Branches.ApplicationBranch) { - if (user.applied && branchName.toLowerCase() !== user.applicationBranch.toLowerCase()) { - response.status(400).json({ - "error": "You can only edit the application branch that you originally submitted" - }); - return; - } - } else if (questionBranch instanceof Branches.ConfirmationBranch) { - if (user.attending && branchName.toLowerCase() !== user.confirmationBranch.toLowerCase()) { - response.status(400).json({ - "error": "You can only edit the confirmation branch that you originally submitted" - }); - return; - } - } else { - response.status(400).json({ - "error": "Invalid application branch" - }); - return; - } - let unchangedFiles: string[] = []; let errored: boolean = false; // Used because .map() can't be broken out of let rawData: (IFormItem | null)[] = questionBranch.questions.map(question => { @@ -244,12 +220,12 @@ function postApplicationBranchHandler(anonymous: boolean): express.Handler { } trackEvent("submitted application", request, user.email, tags); - if (questionBranch.autoAccept) { - await updateUserStatus(user, "accepted"); + if (questionBranch.autoAccept && questionBranch.autoAccept !== "disabled") { + await updateUserStatus(user, questionBranch.autoAccept); } } else if (questionBranch instanceof Branches.ConfirmationBranch) { - if (!user.attending) { + if (!user.confirmed) { await sendMailAsync({ from: config.email.from, to: user.email, @@ -258,7 +234,7 @@ function postApplicationBranchHandler(anonymous: boolean): express.Handler { text: emailText }); } - user.attending = true; + user.confirmed = true; user.confirmationBranch = questionBranch.name; user.confirmationData = data; user.markModified("confirmationData"); @@ -295,7 +271,7 @@ async function deleteApplicationBranchHandler(request: express.Request, response if (requestType === ApplicationType.Application) { user.applied = false; user.accepted = false; - user.attending = false; + user.confirmed = false; user.applicationBranch = ""; user.applicationData = []; user.markModified("applicationData"); @@ -303,8 +279,7 @@ async function deleteApplicationBranchHandler(request: express.Request, response user.applicationStartTime = undefined; } else if (requestType === ApplicationType.Confirmation) { - user.attending = false; - user.confirmationBranch = ""; + user.confirmed = false; user.confirmationData = []; user.markModified("confirmationData"); user.confirmationSubmitTime = undefined; @@ -327,7 +302,7 @@ async function deleteApplicationBranchHandler(request: express.Request, response userRoutes.route("/status").post(isAdmin, uploadHandler.any(), async (request, response): Promise => { let user = await User.findOne({uuid: request.params.uuid}); - let status = request.body.status as ("accepted" | "no-decision"); + let status = request.body.status as string; if (!user) { response.status(400).json({ @@ -336,9 +311,8 @@ userRoutes.route("/status").post(isAdmin, uploadHandler.any(), async (request, r return; } - await updateUserStatus(user, status); - try { + await updateUserStatus(user, status); await user.save(); response.status(200).json({ "success": true @@ -347,51 +321,77 @@ userRoutes.route("/status").post(isAdmin, uploadHandler.any(), async (request, r catch (err) { console.error(err); response.status(500).json({ - "error": "An error occurred while accepting or rejecting the user" + "error": "An error occurred while changing user status" }); } }); -async function updateUserStatus(user: IUserMongoose, status: ("accepted" | "no-decision")): Promise { +async function updateUserStatus(user: IUserMongoose, status: string): Promise { if (status === "no-decision") { + // Clear all confirmation data + user.confirmationBranch = undefined; + user.confirmationData = []; + user.confirmationDeadline = undefined; + user.confirmationStartTime = undefined; + user.confirmationSubmitTime = undefined; + user.confirmed = false; user.accepted = false; - user.confirmationDeadlines = []; - } else if (status === "accepted") { - user.accepted = true; - let applicationBranch = (await Branches.BranchConfig.loadBranchFromDB(user.applicationBranch)) as Branches.ApplicationBranch; - - // Do not send "you are accepted" emails to auto-accept branches - // Admins should add the information into the "post-apply" email - if (applicationBranch.autoAccept) { - user.acceptedEmailSent = true; - } + user.preConfirmEmailSent = false; + } else { + let confirmationBranch = await Branches.BranchConfig.loadBranchFromDB(status) as Branches.ConfirmationBranch; + + if (confirmationBranch) { + // Clear all confirmation data + user.confirmationData = []; + user.confirmationDeadline = undefined; + user.confirmationStartTime = undefined; + user.confirmationSubmitTime = undefined; + user.confirmed = false; + user.accepted = false; + user.preConfirmEmailSent = false; + + // Update branch + user.confirmationBranch = status; + + // Handle rolling deadline + if (confirmationBranch.usesRollingDeadline) { + user.confirmationDeadline = { + name: confirmationBranch.name, + open: confirmationBranch.open, + close: confirmationBranch.close + }; + } - // Automatically mark user as "attending" if application branch skips confirmation - if (applicationBranch.noConfirmation) { - user.attending = true; - } + // Handle accptance + if (confirmationBranch.isAcceptance) { + user.accepted = true; + } - user.confirmationDeadlines = ((await Branches.BranchConfig.loadAllBranches("Confirmation")) as Branches.ConfirmationBranch[]) - .filter(c => c.usesRollingDeadline) - .filter(c => applicationBranch.confirmationBranches.indexOf(c.name) > -1); + // Handle no-op confirmation + if (confirmationBranch.autoConfirm) { + user.confirmed = true; + } + } else { + throw new Error("Confirmation branch not valid!"); + } } } userRoutes.route("/send_acceptances").post(isAdmin, async (request, response): Promise => { try { - let users = await User.find({ "accepted": true, "acceptedEmailSent": { $ne: true } }); + let users = await User.find({ "confirmationBranch": {$exists: true}, "preConfirmEmailSent": { $ne: true } }); for (let user of users) { // Email the applicant about their acceptance let emailSubject: string | null; try { - emailSubject = await getSetting(`${user.applicationBranch}-accept-email-subject`, false); + emailSubject = await getSetting(`${user.confirmationBranch}-pre-confirm-email-subject`, false); } catch { emailSubject = null; } let emailMarkdown: string; try { - emailMarkdown = await getSetting(`${user.applicationBranch}-accept-email`, false); + emailMarkdown = await getSetting(`${user.confirmationBranch}-pre-confirm-email`, false); } catch { // Content not set yet @@ -404,12 +404,12 @@ userRoutes.route("/send_acceptances").post(isAdmin, async (request, response): P await sendMailAsync({ from: config.email.from, to: user.email, - subject: emailSubject || defaultEmailSubjects.accept, + subject: emailSubject || defaultEmailSubjects.preConfirm, html: emailHTML, text: emailText }); - user.acceptedEmailSent = true; + user.preConfirmEmailSent = true; await user.save(); } diff --git a/server/routes/auth.ts b/server/routes/auth.ts index 3e89867b..c157e465 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -232,7 +232,7 @@ passport.use(new LocalStrategy({ "applied": false, "accepted": false, - "acceptedEmailSent": false, + "preConfirmEmailSent": false, "attending": false, "applicationData": [], "applicationStartTime": undefined, diff --git a/server/routes/templates.ts b/server/routes/templates.ts index 53ec3b1b..a2ef5bc4 100644 --- a/server/routes/templates.ts +++ b/server/routes/templates.ts @@ -96,9 +96,6 @@ Handlebars.registerHelper("checked", (selected: boolean[], index: number) => { // Adds the "checked" form attribute if the element was checked previously return selected[index] ? "checked" : ""; }); -Handlebars.registerHelper("branchChecked", (selectedBranches: string[], confirmationBranch: string) => { - return (selectedBranches.indexOf(confirmationBranch) !== -1) ? "checked" : ""; -}); Handlebars.registerHelper("selected", (selected: boolean[], index: number) => { // Adds the "selected" form attribute if the element was selected previously return selected[index] ? "selected" : ""; @@ -130,27 +127,17 @@ templateRoutes.route("/").get(authenticateWithRedirect, async (request, response let user = request.user as IUser; let applyBranches: Branches.ApplicationBranch[]; - let skipConfirmation = false; + if (user.applicationBranch) { applyBranches = [(await Branches.BranchConfig.loadBranchFromDB(user.applicationBranch))] as Branches.ApplicationBranch[]; - skipConfirmation = applyBranches[0].noConfirmation; } else { applyBranches = (await Branches.BranchConfig.loadAllBranches("Application") as Branches.ApplicationBranch[]); } - let confirmBranches: Branches.ConfirmationBranch[]; - if (user.confirmationBranch) { - confirmBranches = [(await Branches.BranchConfig.loadBranchFromDB(user.confirmationBranch))] as Branches.ConfirmationBranch[]; - } else { - confirmBranches = (await Branches.BranchConfig.loadAllBranches("Confirmation")) as Branches.ConfirmationBranch[]; - } - // Filter out branches user does not have access to based on apply branch - if (user.applicationBranch) { - let appliedBranch = applyBranches[0]; - confirmBranches = confirmBranches.filter((branch) => { - // TODO, verify template looks reasonable - return appliedBranch.confirmationBranches && appliedBranch.confirmationBranches.indexOf(branch.name) > -1; - }); + let confirmBranches: Branches.ConfirmationBranch[] = []; + + if (user.confirmationBranch) { + confirmBranches.push(await Branches.BranchConfig.loadBranchFromDB(user.confirmationBranch) as Branches.ConfirmationBranch); } interface IBranchOpenClose { @@ -166,8 +153,9 @@ templateRoutes.route("/").get(authenticateWithRedirect, async (request, response map[branch.name] = branch; return map; }, {} as IDeadlineMap); - for (let branchTimes of user.confirmationDeadlines) { - confirmTimes[branchTimes.name] = branchTimes; + + if (user.confirmationDeadline) { + confirmTimes[user.confirmationDeadline.name] = user.confirmationDeadline; } let confirmTimesArr: IBranchOpenClose[] = Object.keys(confirmTimes).map(name => confirmTimes[name]); @@ -204,9 +192,40 @@ templateRoutes.route("/").get(authenticateWithRedirect, async (request, response close: closeString }; } + let status = ""; + + // Block of logic to dermine status: + if (!user.applied) { + status = "Incomplete"; + } else if (user.applied && !user.confirmationBranch) { + status = "Pending Decision"; + } else if (user.applied && user.confirmationBranch) { + // After confirmation - they either confirmed in time, did not, or branch did not require confirmation + if (user.confirmed) { + if (user.accepted) { + status = "Attending - " + user.confirmationBranch; + } else { + // For confirmation branches that do not accept such as Rejected/Waitlist + status = user.confirmationBranch; + } + } else if (moment().isAfter(confirmTimesArr[0].close)) { + status = "Confirmation Incomplete - " + user.confirmationBranch; + } else if (moment().isBefore(confirmTimesArr[0].open)) { + status = "Confirmation Opens Soon - " + user.confirmationBranch; + } else { + status = "Please Confirm - " + user.confirmationBranch; + } + } + + let autoConfirm = false; + if (user.confirmationBranch) { + autoConfirm = confirmBranches[0].autoConfirm; + } let templateData: IIndexTemplate = { siteTitle: config.eventName, + status, + autoConfirm, user, settings: { teamsEnabled: await getSetting("teamsEnabled"), @@ -219,7 +238,6 @@ templateRoutes.route("/").get(authenticateWithRedirect, async (request, response beforeOpen: applicationOpenDate ? moment().isBefore(applicationOpenDate) : true, afterClose: applicationCloseDate ? moment().isAfter(applicationCloseDate) : false }, - skipConfirmation, confirmationOpen: formatMoment(confirmationOpenDate), confirmationClose: formatMoment(confirmationCloseDate), confirmationStatus: { @@ -344,21 +362,10 @@ function applicationHandler(requestType: ApplicationType): (request: express.Req } // Additionally selectively allow confirmation branches based on what the user applied as else if (requestType === ApplicationType.Confirmation) { - if (user.attending) { + if (user.confirmationBranch) { questionBranches = [user.confirmationBranch.toLowerCase()]; - } - else { - const branches = await Branches.getOpenConfirmationBranches(user); - questionBranches = branches.map(branch => branch.name.toLowerCase()); - - let appliedBranch = (await Branches.BranchConfig.loadBranchFromDB(user.applicationBranch)) as Branches.ApplicationBranch; - if (appliedBranch) { - questionBranches = questionBranches.filter(branch => { - return !!appliedBranch.confirmationBranches.find(confirm => { - return confirm.toLowerCase() === branch; - }); - }); - } + } else { + response.redirect("/"); } } @@ -577,9 +584,7 @@ templateRoutes.route("/admin").get(authenticateWithRedirect, async (request, res open: branch.open.toISOString(), close: branch.close.toISOString(), allowAnonymous: branch.allowAnonymous, - autoAccept: branch.autoAccept, - noConfirmation: branch.noConfirmation, - confirmationBranches: branch.confirmationBranches + autoAccept: branch.autoAccept }; }), confirmation: confirmationBranches.map((branch: Branches.ConfirmationBranch) => { @@ -588,7 +593,8 @@ templateRoutes.route("/admin").get(authenticateWithRedirect, async (request, res open: branch.open.toISOString(), close: branch.close.toISOString(), usesRollingDeadline: branch.usesRollingDeadline, - usesRollingDeadlineChecked: branch.usesRollingDeadline ? "checked" : "" + autoConfirm: branch.autoConfirm, + isAcceptance: branch.isAcceptance }; }) } diff --git a/server/schema.ts b/server/schema.ts index e1619641..6f99bbc0 100644 --- a/server/schema.ts +++ b/server/schema.ts @@ -114,20 +114,20 @@ export interface IUser { applied: boolean; accepted: boolean; - acceptedEmailSent: boolean; - attending: boolean; + preConfirmEmailSent: boolean; + confirmed: boolean; applicationBranch: string; applicationData: IFormItem[]; applicationStartTime?: Date; applicationSubmitTime?: Date; - confirmationDeadlines: { + confirmationDeadline?: { name: string; open: Date; close: Date; - }[]; + }; - confirmationBranch: string; + confirmationBranch?: string; confirmationData: IFormItem[]; confirmationStartTime?: Date; confirmationSubmitTime?: Date; @@ -179,18 +179,18 @@ export const User = mongoose.model("User", new mongoose.Schema({ applied: Boolean, accepted: Boolean, - acceptedEmailSent: Boolean, - attending: Boolean, + preConfirmEmailSent: Boolean, + confirmed: Boolean, applicationBranch: String, applicationData: [mongoose.Schema.Types.Mixed], applicationStartTime: Date, applicationSubmitTime: Date, - confirmationDeadlines: [{ + confirmationDeadline: { name: String, open: Date, close: Date - }], + }, confirmationBranch: String, confirmationData: [mongoose.Schema.Types.Mixed], @@ -232,10 +232,11 @@ export interface QuestionBranchSettings { open?: Date; // Used by all except noop close?: Date; // Used by all except noop allowAnonymous?: boolean; // Used by application branch - autoAccept?: boolean; // Used by application branch - noConfirmation?: boolean; // Used by application branch + autoAccept?: string; // Used by application branch confirmationBranches?: string[]; // Used by application branch usesRollingDeadline?: boolean; // Used by confirmation branch + isAcceptance?: boolean; // Used by confirmation branch + autoConfirm?: boolean; // Used by confirmation branch } export interface IQuestionBranchConfig { _id: mongoose.Types.ObjectId; @@ -257,10 +258,11 @@ export const QuestionBranchConfig = mongoose.model Date: Sat, 12 May 2018 14:02:18 -0500 Subject: [PATCH 046/194] Remove batch accept (#213) * Remove batch accept (#166) * Bump version to 1.14.0 --- client/admin.html | 11 ----------- client/js/admin.ts | 24 +----------------------- package-lock.json | 2 +- package.json | 2 +- server/routes/api/user.ts | 38 -------------------------------------- 5 files changed, 3 insertions(+), 74 deletions(-) diff --git a/client/admin.html b/client/admin.html index 4d06b710..c6e3fb2b 100644 --- a/client/admin.html +++ b/client/admin.html @@ -35,7 +35,6 @@

Admin Panel

Statistics Users Applicants - Batch Accept Settings @@ -395,16 +394,6 @@

config.json options

- -
-

Batch Accept - BE CAREFUL

-

TODO: remove this "feature" in the future

-

Please list the user ids of applicants that you want to accept ONE PER LINE. BE CAREFUL.

-
- - -
-
{{/sidebar}} diff --git a/client/js/admin.ts b/client/js/admin.ts index a61d965f..22a47af5 100644 --- a/client/js/admin.ts +++ b/client/js/admin.ts @@ -25,7 +25,7 @@ class State { this.sectionElement.style.display = "block"; } } -const states: State[] = ["statistics", "users", "applicants", "batch-accept", "settings"].map(id => new State(id)); +const states: State[] = ["statistics", "users", "applicants", "settings"].map(id => new State(id)); class UserEntries { private static readonly NODE_COUNT = 20; @@ -894,25 +894,3 @@ for (let i = 0; i < data.length; i++) { } }); } - -// -// Batch Accept TODO: DELETE THIS "FEATURE" -// - -let batchAcceptButton = document.querySelector("#batch-accept input[type=submit]") as HTMLInputElement; -let batchAcceptForm = document.querySelector("#batch-accept form") as HTMLFormElement; -batchAcceptButton.addEventListener("click", async e => { - e.preventDefault(); - if (!batchAcceptForm.checkValidity()) { - await sweetAlert("Fail!", "Need to list user id's one per line", "error"); - } - let userIds = (batchAcceptForm["batch-accept-ids"]).value.split("\n").map((id: string) => id.trim()); - let formData = new FormData(); - formData.append("userIds", userIds); - let acceptedCount: number = (await fetch("/api/user/all/batch_accept", { - credentials: "same-origin", - method: "POST", - body: formData - }).then(checkStatus).then(parseJSON)).count; - await sweetAlert("Success!", `Batch accepted (${acceptedCount} applicants in all).`, "success"); -}); diff --git a/package-lock.json b/package-lock.json index 7e9510c4..1003b908 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "1.13.0", + "version": "1.14.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index b52ec99e..f4b148a9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "1.13.0", + "version": "1.14.0", "description": "TBD", "main": "server/app.js", "scripts": { diff --git a/server/routes/api/user.ts b/server/routes/api/user.ts index 6e26da77..af966f3a 100644 --- a/server/routes/api/user.ts +++ b/server/routes/api/user.ts @@ -460,44 +460,6 @@ userRoutes.route("/send_acceptances").post(isAdmin, async (request, response): P } }); -userRoutes.route("/batch_accept").post(isAdmin, postParser, uploadHandler.any(), async (request, response): Promise => { - try { - let userIds = request.body.userIds.split(','); - let acceptedCount = 0; - let notAccepted = []; - for (let userId of userIds) { - let user; - try { - user = await User.findById(userId); - } catch { - notAccepted.push(userId); - } - if (user && user.applied && !user.accepted) { - try { - await updateUserStatus(user, "accepted"); // Assume that this succeeds - await user.save(); - acceptedCount += 1; - } catch { - notAccepted.push(userId); - } - } else { - notAccepted.push(userId); - } - } - response.json({ - "success": true, - "count": acceptedCount, - "notAccepted": notAccepted - }); - } - catch (err) { - console.error(err); - response.status(500).json({ - "error": "An error occurred while batch accepting applicants" - }); - } -}); - userRoutes.route("/export").get(isAdmin, async (request, response): Promise => { try { let archive = archiver("zip", { From 2e2bd7d5f42faeb21d262005ec719ffb9cbbe5cd Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Mon, 4 Jun 2018 15:18:20 -0400 Subject: [PATCH 047/194] Changed some instances of attending -> confirmed --- api.graphql | 2 +- client/admin.html | 4 ++-- server/routes/api/graphql.ts | 2 +- server/routes/auth.ts | 4 ++-- server/routes/templates.ts | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/api.graphql b/api.graphql index 2226b0fc..ab31beab 100644 --- a/api.graphql +++ b/api.graphql @@ -97,7 +97,7 @@ input UserFilter { # If the user has been accepted to the event accepted: Boolean # If the user has indicated that he or she is attending - attending: Boolean + confirmed: Boolean # The type of application a user filled out (e.g. Mentor, Participant) application_branch: String # The type of confirmation a user filled out (e.g. Needs Reimbursement) diff --git a/client/admin.html b/client/admin.html index e174c517..8070ac6b 100644 --- a/client/admin.html +++ b/client/admin.html @@ -59,11 +59,11 @@

Statistics

{{numberFormat applicationStatistics.admittedUsers}} - Attending users + Confirmed users {{numberFormat applicationStatistics.attendingUsers}} - Declined users + Non-confirmed users {{numberFormat applicationStatistics.declinedUsers}} Applications by branch diff --git a/server/routes/api/graphql.ts b/server/routes/api/graphql.ts index 096cbe2e..040f2d30 100644 --- a/server/routes/api/graphql.ts +++ b/server/routes/api/graphql.ts @@ -214,7 +214,7 @@ function userFilterToMongo(filter: types.UserFilter | undefined) { } setIf("applied", filter.applied); setIf("accepted", filter.accepted); - setIf("attending", filter.attending); + setIf("confirmed", filter.confirmed); setIf("applicationBranch", filter.application_branch); setIf("confirmationBranch", filter.confirmation_branch); return query; diff --git a/server/routes/auth.ts b/server/routes/auth.ts index c157e465..fc5f4aef 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -123,7 +123,7 @@ function useLoginStrategy(strategy: any, dataFieldName: "githubData" | "googleDa "applied": false, "accepted": false, - "attending": false, + "confirmed": false, "applicationData": [], "applicationStartTime": undefined, "applicationSubmitTime": undefined, @@ -233,7 +233,7 @@ passport.use(new LocalStrategy({ "applied": false, "accepted": false, "preConfirmEmailSent": false, - "attending": false, + "confirmed": false, "applicationData": [], "applicationStartTime": undefined, "applicationSubmitTime": undefined, diff --git a/server/routes/templates.ts b/server/routes/templates.ts index a2ef5bc4..87595944 100644 --- a/server/routes/templates.ts +++ b/server/routes/templates.ts @@ -552,8 +552,8 @@ templateRoutes.route("/admin").get(authenticateWithRedirect, async (request, res totalUsers: await User.find().count(), appliedUsers: await User.find({ "applied": true }).count(), admittedUsers: await User.find({ "accepted": true }).count(), - attendingUsers: await User.find({ "attending": true }).count(), - declinedUsers: await User.find({ "accepted": true, "attending": false }).count(), + attendingUsers: await User.find({ "confirmed": true }).count(), + declinedUsers: await User.find({ "accepted": true, "confirmed": false }).count(), applicationBranches: await Promise.all(applicationBranches.map(async branch => { return { "name": branch.name, From b9d4a0f8689d984f07f02cb3b3b5beebc09252f4 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Mon, 4 Jun 2018 16:58:38 -0400 Subject: [PATCH 048/194] Update TSLint and fix related errors --- client/index.html | 2 +- client/js/admin.ts | 3 +- client/js/qr.ts | 6 +- package-lock.json | 1036 ++++++++++++++++++---------------- package.json | 4 +- server/branch.ts | 2 +- server/common.ts | 4 +- server/middleware.ts | 14 +- server/routes/api/graphql.ts | 6 +- tslint.json | 16 +- 10 files changed, 577 insertions(+), 516 deletions(-) diff --git a/client/index.html b/client/index.html index e23d2afb..e00dea96 100644 --- a/client/index.html +++ b/client/index.html @@ -47,7 +47,7 @@

Your status:

{{#if settings.qrEnabled}} {{#if user.accepted}}

Show your QR code to check in when you get here.

-
+
{{/if}} {{/if}} {{else if user.confirmationBranch}} diff --git a/client/js/admin.ts b/client/js/admin.ts index c66676ed..8f6c158b 100644 --- a/client/js/admin.ts +++ b/client/js/admin.ts @@ -1,6 +1,6 @@ class State { public id: string; - private sectionElement: HTMLElement; + private readonly sectionElement: HTMLElement; public static hideAll() { // tslint:disable-next-line:no-use-before-declare @@ -472,6 +472,7 @@ class ApplicantEntries { public static setup() { this.generalNodes = []; + this.detailsNodes = []; this.instantiate(); this.offset = 0; this.updateFilter(); diff --git a/client/js/qr.ts b/client/js/qr.ts index 2007127b..c241637e 100644 --- a/client/js/qr.ts +++ b/client/js/qr.ts @@ -14,7 +14,11 @@ let errorCorrectionLevel = "H"; let qr = qrcode(typeNumber, errorCorrectionLevel); let qrElement = document.getElementById("qrCode") as HTMLElement; -qr.addData(qrElement.attributes.getNamedItem("data").value); +let encoded = qrElement.dataset.encoded; +if (!encoded) { + throw new Error("Data for QR code missing"); +} +qr.addData(encoded); qr.make(); qrElement.innerHTML = qr.createSvgTag(); diff --git a/package-lock.json b/package-lock.json index 7e9510c4..22b77536 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,9 @@ "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-6.2.1.tgz", "integrity": "sha512-FLqoh2UqmFs5R/92xzF1jYMLGU89rTgLK6XX+VA02YcfQW8rGjbMrj7zsSCQ7SLkeiWekmUU2+naeIO9L4dqxA==", "requires": { - "@sendgrid/helpers": "6.2.1", - "@types/request": "2.47.0", - "request": "2.83.0" + "@sendgrid/helpers": "^6.2.1", + "@types/request": "^2.0.3", + "request": "^2.81.0" } }, "@sendgrid/helpers": { @@ -19,7 +19,7 @@ "resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-6.2.1.tgz", "integrity": "sha512-WnQ4TV51Xln/X70lk6J1/3tfRHW3K4zagz19vlJrtQUtX1wwghOj926OqcMm5nOiBHEh+la3cvdzHENb09FsIA==", "requires": { - "chalk": "2.1.0" + "chalk": "^2.0.1" } }, "@sendgrid/mail": { @@ -27,8 +27,8 @@ "resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-6.2.1.tgz", "integrity": "sha512-gTd8gMp4JVLGZhXb/DkyrjByTfIR1OvtpPpQLwO11Vz72x3JdPl4tJTtWA/svAFfN5UXnZtAomAvjJCdcd4lzw==", "requires": { - "@sendgrid/client": "6.2.1", - "@sendgrid/helpers": "6.2.1" + "@sendgrid/client": "^6.2.1", + "@sendgrid/helpers": "^6.2.1" } }, "@types/archiver": { @@ -37,7 +37,7 @@ "integrity": "sha1-5cgLrWyaiY/AlK6UWy3DITx47A4=", "dev": true, "requires": { - "@types/node": "8.0.32" + "@types/node": "*" } }, "@types/aws-sdk": { @@ -46,7 +46,7 @@ "integrity": "sha1-g1iLPRTr3KHUzl4CM4dXdWjOgvM=", "dev": true, "requires": { - "aws-sdk": "2.126.0" + "aws-sdk": "*" } }, "@types/body-parser": { @@ -55,8 +55,8 @@ "integrity": "sha1-mOndlmSn+oFP7CxQyVu/ntrPXwU=", "dev": true, "requires": { - "@types/express": "4.0.37", - "@types/node": "8.0.32" + "@types/express": "*", + "@types/node": "*" } }, "@types/bson": { @@ -65,7 +65,7 @@ "integrity": "sha512-/nysVvxwup1WniGHIM31UZXM+6727h4FAa2tZpFSQBooBcl2Bh1N9oQmVVg8QYnjchN/DOGi7UvVN0jpzWL6sw==", "dev": true, "requires": { - "@types/node": "8.0.32" + "@types/node": "*" } }, "@types/caseless": { @@ -97,7 +97,7 @@ "integrity": "sha1-ldxzOiM5qoRjgdfxN3eS0lU9wn0=", "dev": true, "requires": { - "@types/express": "4.0.37" + "@types/express": "*" } }, "@types/connect-flash": { @@ -106,7 +106,7 @@ "integrity": "sha1-hrjj2tEwfi0QXYNopxO/kPjLwFo=", "dev": true, "requires": { - "@types/express": "4.0.37" + "@types/express": "*" } }, "@types/connect-mongo": { @@ -115,10 +115,10 @@ "integrity": "sha1-k5ynCFE0gsfT8yo4xjfUnSrH0JA=", "dev": true, "requires": { - "@types/express": "4.0.37", - "@types/express-session": "0.0.32", - "@types/mongodb": "2.2.12", - "@types/mongoose": "4.7.23" + "@types/express": "*", + "@types/express-session": "*", + "@types/mongodb": "*", + "@types/mongoose": "*" } }, "@types/cookie-parser": { @@ -127,7 +127,7 @@ "integrity": "sha512-iJY6B3ZGufLiDf2OCAgiAAQuj1sMKC/wz/7XCEjZ+/MDuultfFJuSwrBKcLSmJ5iYApLzCCYBYJZs0Ws8GPmwA==", "dev": true, "requires": { - "@types/express": "4.0.37" + "@types/express": "*" } }, "@types/cookie-signature": { @@ -142,8 +142,8 @@ "integrity": "sha512-tIULTLzQpFFs5/PKnFIAFOsXQxss76glppbVKR3/jddPK26SBsD5HF5grn5G2jOGtpRWSBvYmDYoduVv+3wOXg==", "dev": true, "requires": { - "@types/express-serve-static-core": "4.0.53", - "@types/serve-static": "1.7.32" + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" } }, "@types/express-serve-static-core": { @@ -152,7 +152,7 @@ "integrity": "sha512-zaGeOpEYp5G2EhjaUFdVwysDrfEYc6Q6iPhd3Kl4ip30x0tvVv7SuJvY3yzCUSuFlzAG8N5KsyY6BJg93/cn+Q==", "dev": true, "requires": { - "@types/node": "8.0.32" + "@types/node": "*" } }, "@types/express-session": { @@ -161,8 +161,8 @@ "integrity": "sha1-gvnmoCjrYSWkEtuV8OYal9GUXuA=", "dev": true, "requires": { - "@types/express": "4.0.37", - "@types/node": "8.0.32" + "@types/express": "*", + "@types/node": "*" } }, "@types/form-data": { @@ -170,7 +170,7 @@ "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz", "integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==", "requires": { - "@types/node": "8.0.32" + "@types/node": "*" } }, "@types/graphql": { @@ -208,7 +208,7 @@ "integrity": "sha512-HG4pUK/fTrGY3FerMlINxK74MxdAxkCRYrp5AM+oJ2jLcK0jWUi64ZV15JKwDR4TYLIxrT3y9SVnEWcLPbC/YA==", "dev": true, "requires": { - "moment": "2.21.0" + "moment": ">=2.14.0" } }, "@types/mongodb": { @@ -217,8 +217,8 @@ "integrity": "sha512-nJLPAQro3U0nwhXnglmH+4DMUBVDa4tsPnwEFtzyv4x+eT0UynlrPm2rGR8UPDw0VcTF3UGI5UEqyjcnq/ukgA==", "dev": true, "requires": { - "@types/bson": "1.0.4", - "@types/node": "8.0.32" + "@types/bson": "*", + "@types/node": "*" } }, "@types/mongoose": { @@ -227,8 +227,8 @@ "integrity": "sha512-13UfisRIfRCP/+FV85e99vofsc7JB0K6KeevN4Bq6UjyXHwFGRDnHvc9iAB591fQI+MRQyNvrFHJHrNou16Dww==", "dev": true, "requires": { - "@types/mongodb": "2.2.12", - "@types/node": "8.0.32" + "@types/mongodb": "*", + "@types/node": "*" } }, "@types/morgan": { @@ -237,7 +237,7 @@ "integrity": "sha512-HBsWVjFJWDbH79Aug/Pyxsc5KWyZs2vkxn7qbo+9a7w2jVur6egGsyJeacDW5Pb5cO+fUl+X5kZaDct8asYp1w==", "dev": true, "requires": { - "@types/express": "4.0.37" + "@types/express": "*" } }, "@types/multer": { @@ -246,7 +246,7 @@ "integrity": "sha512-a4vEHvs9MEyvHwb2/rw3Cg0MaPrp1jbEEWrhBL9xf457AYS79bOajcfkYCEICH+1NExZkqxxtZKydKIX5UTbmg==", "dev": true, "requires": { - "@types/express": "4.0.37" + "@types/express": "*" } }, "@types/node": { @@ -260,7 +260,7 @@ "integrity": "sha512-P+eK/+A7KX2Ngtbr4wPO2dV4HxvHpT9cf76iFXnkQ/BuPGqkWPYBL7gElDlcO/XksMCmmwZRv3rUmiBRNkpmjA==", "dev": true, "requires": { - "@types/express": "4.0.37" + "@types/express": "*" } }, "@types/passport-facebook": { @@ -269,8 +269,8 @@ "integrity": "sha512-T3toYvfb9Qy0KcQYwp/fngNvG45VVdvpl4JzjPCN4/RjCCpQ2u4WWL3OrkxKtRWS7iWgeYyY6HcvZspHwSA/xg==", "dev": true, "requires": { - "@types/express": "4.0.37", - "@types/passport": "0.3.4" + "@types/express": "*", + "@types/passport": "*" } }, "@types/passport-local": { @@ -279,9 +279,9 @@ "integrity": "sha512-edWrCkVkgrUUbTohZoXeo1hxbguNwpbx1VasicxqLGAO6N/3P8d1rKhlx8KhkVLWFH+gL5PIzejdYQSt1I30/g==", "dev": true, "requires": { - "@types/express": "4.0.37", - "@types/passport": "0.3.4", - "@types/passport-strategy": "0.2.31" + "@types/express": "*", + "@types/passport": "*", + "@types/passport-strategy": "*" } }, "@types/passport-strategy": { @@ -290,8 +290,8 @@ "integrity": "sha512-YCxa7Xowz0dQ3AOIE7UMGBgMpNWHtxtsv+vmRH7rc6lo3zcDHiKjh12lFyiGpbwyu1digyP70XtuDYtI0jbobQ==", "dev": true, "requires": { - "@types/express": "4.0.37", - "@types/passport": "0.3.4" + "@types/express": "*", + "@types/passport": "*" } }, "@types/request": { @@ -299,10 +299,10 @@ "resolved": "https://registry.npmjs.org/@types/request/-/request-2.47.0.tgz", "integrity": "sha512-/KXM5oev+nNCLIgBjkwbk8VqxmzI56woD4VUxn95O+YeQ8hJzcSmIZ1IN3WexiqBb6srzDo2bdMbsXxgXNkz5Q==", "requires": { - "@types/caseless": "0.12.1", - "@types/form-data": "2.2.1", - "@types/node": "8.0.32", - "@types/tough-cookie": "2.3.2" + "@types/caseless": "*", + "@types/form-data": "*", + "@types/node": "*", + "@types/tough-cookie": "*" } }, "@types/serve-static": { @@ -311,8 +311,8 @@ "integrity": "sha512-WpI0g7M1FiOmJ/a97Qrjafq2I938tjAZ3hZr9O7sXyA6oUhH3bqUNZIt7r1KZg8TQAKxcvxt6JjQ5XuLfIBFvg==", "dev": true, "requires": { - "@types/express-serve-static-core": "4.0.53", - "@types/mime": "2.0.0" + "@types/express-serve-static-core": "*", + "@types/mime": "*" } }, "@types/superagent": { @@ -321,7 +321,7 @@ "integrity": "sha512-yGiVkRbB1qtIkRCpEJIxlHazBoILmu33xbbu4IiwxTJjwDi/EudiPYAD7QwWe035jkE40yQgTVXZsAePFtleww==", "dev": true, "requires": { - "@types/node": "8.0.32" + "@types/node": "*" } }, "@types/supertest": { @@ -330,7 +330,7 @@ "integrity": "sha512-QxgjuDhQEq4tPJXTOnGoN4P/BWwvftvkaCZhXMp1C1Otk9kGXb7l/wA7kwz3MwtEMnovdYikS+ZtvaQ4RbhF4g==", "dev": true, "requires": { - "@types/superagent": "3.5.6" + "@types/superagent": "*" } }, "@types/tmp": { @@ -349,7 +349,7 @@ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.2.tgz", "integrity": "sha512-fXrYo81BZgbz6KhHU3+dNUSdca35FwbfTMQvSnIIX6qOCsymSmFAjPDSfr4Q/Pt9abjt04kBi4+7FinVPdyEpg==", "requires": { - "@types/node": "8.0.32" + "@types/node": "*" } }, "@types/whatwg-fetch": { @@ -358,7 +358,7 @@ "integrity": "sha1-GcDShjyMsjgPIaHHNrecv3iVuxM=", "dev": true, "requires": { - "@types/whatwg-streams": "0.0.4" + "@types/whatwg-streams": "*" } }, "@types/whatwg-streams": { @@ -372,7 +372,7 @@ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=", "requires": { - "mime-types": "2.1.17", + "mime-types": "~2.1.16", "negotiator": "0.6.1" } }, @@ -381,10 +381,10 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.2.3.tgz", "integrity": "sha1-wG9Zh3jETGsWGrr+NGa4GtGBTtI=", "requires": { - "co": "4.6.0", - "fast-deep-equal": "1.0.0", - "json-schema-traverse": "0.3.1", - "json-stable-stringify": "1.0.1" + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "json-schema-traverse": "^0.3.0", + "json-stable-stringify": "^1.0.1" } }, "align-text": { @@ -392,9 +392,9 @@ "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", "requires": { - "kind-of": "3.2.2", - "longest": "1.0.1", - "repeat-string": "1.6.1" + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" } }, "amdefine": { @@ -412,7 +412,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", "requires": { - "color-convert": "1.9.0" + "color-convert": "^1.9.0" } }, "any-promise": { @@ -425,7 +425,7 @@ "resolved": "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-1.1.0.tgz", "integrity": "sha1-dMO/Q5ThTq56tgsdmZo8W4qpTpo=", "requires": { - "apollo-tracing": "0.0.7" + "apollo-tracing": "^0.0.7" } }, "apollo-server-express": { @@ -433,8 +433,8 @@ "resolved": "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-1.1.2.tgz", "integrity": "sha1-aTPHf+XfuafzDdOTI5rZlTphPNk=", "requires": { - "apollo-server-core": "1.1.0", - "apollo-server-module-graphiql": "1.1.2" + "apollo-server-core": "^1.1.0", + "apollo-server-module-graphiql": "^1.1.2" } }, "apollo-server-module-graphiql": { @@ -447,7 +447,7 @@ "resolved": "https://registry.npmjs.org/apollo-tracing/-/apollo-tracing-0.0.7.tgz", "integrity": "sha512-jvjNmOOb3M2QiBEuz9Vjp6HiXtZuDwRvHxqBZQ+TE0UoODRnJoQu5LF1uvPI2ooOHiPC1ce4SAKNNIU9y02EeA==", "requires": { - "graphql-tools": "1.2.3" + "graphql-tools": "^1.1.0" } }, "append-field": { @@ -460,15 +460,15 @@ "resolved": "https://registry.npmjs.org/archiver/-/archiver-1.3.0.tgz", "integrity": "sha1-TyGU1tj5nfP1MeaIHxTxXVX6ryI=", "requires": { - "archiver-utils": "1.3.0", - "async": "2.5.0", - "buffer-crc32": "0.2.13", - "glob": "7.1.2", - "lodash": "4.17.4", - "readable-stream": "2.3.3", - "tar-stream": "1.5.4", - "walkdir": "0.0.11", - "zip-stream": "1.2.0" + "archiver-utils": "^1.3.0", + "async": "^2.0.0", + "buffer-crc32": "^0.2.1", + "glob": "^7.0.0", + "lodash": "^4.8.0", + "readable-stream": "^2.0.0", + "tar-stream": "^1.5.0", + "walkdir": "^0.0.11", + "zip-stream": "^1.1.0" } }, "archiver-utils": { @@ -476,12 +476,12 @@ "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-1.3.0.tgz", "integrity": "sha1-5QtMCccL89aA4y/xt5lOn52JUXQ=", "requires": { - "glob": "7.1.2", - "graceful-fs": "4.1.11", - "lazystream": "1.0.0", - "lodash": "4.17.4", - "normalize-path": "2.1.1", - "readable-stream": "2.3.3" + "glob": "^7.0.0", + "graceful-fs": "^4.1.0", + "lazystream": "^1.0.0", + "lodash": "^4.8.0", + "normalize-path": "^2.0.0", + "readable-stream": "^2.0.0" } }, "argparse": { @@ -489,7 +489,7 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", "requires": { - "sprintf-js": "1.0.3" + "sprintf-js": "~1.0.2" } }, "array-flatten": { @@ -518,7 +518,7 @@ "resolved": "https://registry.npmjs.org/async/-/async-2.5.0.tgz", "integrity": "sha512-e+lJAJeNWuPCNyxZKOBdaJGyLGHugXVQtrAwtuAe2vhxTYxFTKE73p8JuTmdH0qdQZtDvI4dhJwjZc5zsfIsYw==", "requires": { - "lodash": "4.17.4" + "lodash": "^4.14.0" } }, "asynckit": { @@ -533,7 +533,7 @@ "requires": { "buffer": "4.9.1", "crypto-browserify": "1.0.9", - "events": "1.1.1", + "events": "^1.1.1", "jmespath": "0.15.0", "querystring": "0.2.0", "sax": "1.2.1", @@ -566,9 +566,9 @@ "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", "dev": true, "requires": { - "chalk": "1.1.3", - "esutils": "2.0.2", - "js-tokens": "3.0.2" + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" }, "dependencies": { "ansi-styles": { @@ -583,11 +583,11 @@ "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" } }, "supports-color": { @@ -622,7 +622,7 @@ "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", "optional": true, "requires": { - "tweetnacl": "0.14.5" + "tweetnacl": "^0.14.3" } }, "bl": { @@ -630,7 +630,7 @@ "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.1.tgz", "integrity": "sha1-ysMo977kVzDUBLaSID/LWQ4XLV4=", "requires": { - "readable-stream": "2.3.3" + "readable-stream": "^2.0.5" } }, "bluebird": { @@ -644,15 +644,15 @@ "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", "requires": { "bytes": "3.0.0", - "content-type": "1.0.4", + "content-type": "~1.0.4", "debug": "2.6.9", - "depd": "1.1.1", - "http-errors": "1.6.2", + "depd": "~1.1.1", + "http-errors": "~1.6.2", "iconv-lite": "0.4.19", - "on-finished": "2.3.0", + "on-finished": "~2.3.0", "qs": "6.5.1", "raw-body": "2.3.2", - "type-is": "1.6.15" + "type-is": "~1.6.15" } }, "boolbase": { @@ -665,7 +665,7 @@ "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", "requires": { - "hoek": "4.2.1" + "hoek": "4.x.x" } }, "bowser": { @@ -678,7 +678,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", "requires": { - "balanced-match": "1.0.0", + "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, @@ -698,9 +698,9 @@ "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", "requires": { - "base64-js": "1.2.1", - "ieee754": "1.1.8", - "isarray": "1.0.0" + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" } }, "buffer-crc32": { @@ -713,13 +713,19 @@ "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=" }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, "busboy": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", "requires": { "dicer": "0.2.5", - "readable-stream": "1.1.14" + "readable-stream": "1.1.x" }, "dependencies": { "isarray": { @@ -732,10 +738,10 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", "isarray": "0.0.1", - "string_decoder": "0.10.31" + "string_decoder": "~0.10.x" } }, "string_decoder": { @@ -772,8 +778,8 @@ "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", "optional": true, "requires": { - "align-text": "0.1.4", - "lazy-cache": "1.0.4" + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" } }, "chai": { @@ -782,12 +788,12 @@ "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", "dev": true, "requires": { - "assertion-error": "1.0.2", - "check-error": "1.0.2", - "deep-eql": "3.0.1", - "get-func-name": "2.0.0", - "pathval": "1.1.0", - "type-detect": "4.0.3" + "assertion-error": "^1.0.1", + "check-error": "^1.0.1", + "deep-eql": "^3.0.0", + "get-func-name": "^2.0.0", + "pathval": "^1.0.0", + "type-detect": "^4.0.0" } }, "chalk": { @@ -795,9 +801,9 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.1.0.tgz", "integrity": "sha512-LUHGS/dge4ujbXMJrnihYMcL4AoOweGnw9Tp3kQuqy1Kx5c1qKjqvMJZ6nVJPMWJtKCTN72ZogH3oeSO9g9rXQ==", "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "4.4.0" + "ansi-styles": "^3.1.0", + "escape-string-regexp": "^1.0.5", + "supports-color": "^4.0.0" } }, "check-error": { @@ -811,12 +817,12 @@ "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.2.tgz", "integrity": "sha1-S59TqBsn5NXawxwP/Qz6A8xoMNs=", "requires": { - "css-select": "1.2.0", - "dom-serializer": "0.1.0", - "entities": "1.1.1", - "htmlparser2": "3.9.2", - "lodash": "4.17.4", - "parse5": "3.0.2" + "css-select": "~1.2.0", + "dom-serializer": "~0.1.0", + "entities": "~1.1.1", + "htmlparser2": "^3.9.1", + "lodash": "^4.15.0", + "parse5": "^3.0.1" } }, "cli-color": { @@ -824,12 +830,12 @@ "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-1.2.0.tgz", "integrity": "sha1-OlrnT9drYmevZm5p4q+70B3vNNE=", "requires": { - "ansi-regex": "2.1.1", - "d": "1.0.0", - "es5-ext": "0.10.30", - "es6-iterator": "2.0.1", - "memoizee": "0.4.11", - "timers-ext": "0.1.2" + "ansi-regex": "^2.1.1", + "d": "1", + "es5-ext": "^0.10.12", + "es6-iterator": "2", + "memoizee": "^0.4.3", + "timers-ext": "0.1" } }, "cli-table": { @@ -846,8 +852,8 @@ "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", "optional": true, "requires": { - "center-align": "0.1.3", - "right-align": "0.1.3", + "center-align": "^0.1.1", + "right-align": "^0.1.1", "wordwrap": "0.0.2" }, "dependencies": { @@ -869,7 +875,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.0.tgz", "integrity": "sha1-Gsz5fdc5uYO/mU1W/sj5WFNkG3o=", "requires": { - "color-name": "1.1.3" + "color-name": "^1.1.1" } }, "color-name": { @@ -887,7 +893,7 @@ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", "requires": { - "delayed-stream": "1.0.0" + "delayed-stream": "~1.0.0" } }, "commander": { @@ -906,10 +912,10 @@ "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-1.2.0.tgz", "integrity": "sha1-WFhwku8g03y1i68AARLJJ4/3O58=", "requires": { - "buffer-crc32": "0.2.13", - "crc32-stream": "2.0.0", - "normalize-path": "2.1.1", - "readable-stream": "2.3.3" + "buffer-crc32": "^0.2.1", + "crc32-stream": "^2.0.0", + "normalize-path": "^2.0.0", + "readable-stream": "^2.0.0" } }, "compressible": { @@ -917,7 +923,7 @@ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.11.tgz", "integrity": "sha1-FnGKdd4oPtjmBAQWJaIGRYZ5fYo=", "requires": { - "mime-db": "1.30.0" + "mime-db": ">= 1.29.0 < 2" } }, "compression": { @@ -925,13 +931,13 @@ "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.1.tgz", "integrity": "sha1-7/JgPvwuIs+G810uuTWJ+YdTc9s=", "requires": { - "accepts": "1.3.4", + "accepts": "~1.3.4", "bytes": "3.0.0", - "compressible": "2.0.11", + "compressible": "~2.0.11", "debug": "2.6.9", - "on-headers": "1.0.1", + "on-headers": "~1.0.1", "safe-buffer": "5.1.1", - "vary": "1.1.2" + "vary": "~1.1.2" } }, "concat-map": { @@ -944,9 +950,9 @@ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", "requires": { - "inherits": "2.0.3", - "readable-stream": "2.3.3", - "typedarray": "0.0.6" + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" } }, "connect-flash": { @@ -959,8 +965,8 @@ "resolved": "https://registry.npmjs.org/connect-mongo/-/connect-mongo-1.3.2.tgz", "integrity": "sha1-fL9Y3/8mdg5eAOAX0KhbS8kLnTc=", "requires": { - "bluebird": "3.5.0", - "mongodb": "2.2.31" + "bluebird": "^3.0", + "mongodb": ">= 1.2.0 <3.0.0" } }, "content-disposition": { @@ -1013,8 +1019,8 @@ "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-2.0.0.tgz", "integrity": "sha1-483TtN8xaN10494/u8t7KX/pCPQ=", "requires": { - "crc": "3.5.0", - "readable-stream": "2.3.3" + "crc": "^3.4.4", + "readable-stream": "^2.0.0" } }, "cryptiles": { @@ -1022,7 +1028,7 @@ "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", "requires": { - "boom": "5.2.0" + "boom": "5.x.x" }, "dependencies": { "boom": { @@ -1030,7 +1036,7 @@ "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", "requires": { - "hoek": "4.2.1" + "hoek": "4.x.x" } } } @@ -1045,10 +1051,10 @@ "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", "requires": { - "boolbase": "1.0.0", - "css-what": "2.1.0", + "boolbase": "~1.0.0", + "css-what": "2.1", "domutils": "1.5.1", - "nth-check": "1.0.1" + "nth-check": "~1.0.1" } }, "css-what": { @@ -1061,7 +1067,7 @@ "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", "requires": { - "es5-ext": "0.10.30" + "es5-ext": "^0.10.9" } }, "dashdash": { @@ -1069,7 +1075,7 @@ "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", "requires": { - "assert-plus": "1.0.0" + "assert-plus": "^1.0.0" } }, "debug": { @@ -1092,7 +1098,7 @@ "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", "dev": true, "requires": { - "type-detect": "4.0.3" + "type-detect": "^4.0.0" } }, "delayed-stream": { @@ -1120,7 +1126,7 @@ "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", "requires": { - "readable-stream": "1.1.14", + "readable-stream": "1.1.x", "streamsearch": "0.1.2" }, "dependencies": { @@ -1134,10 +1140,10 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", "isarray": "0.0.1", - "string_decoder": "0.10.31" + "string_decoder": "~0.10.x" } }, "string_decoder": { @@ -1158,8 +1164,8 @@ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", "requires": { - "domelementtype": "1.1.3", - "entities": "1.1.1" + "domelementtype": "~1.1.1", + "entities": "~1.1.1" }, "dependencies": { "domelementtype": { @@ -1179,7 +1185,7 @@ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.1.tgz", "integrity": "sha1-iS5HAAqZvlW783dP/qBWHYh5wlk=", "requires": { - "domelementtype": "1.3.0" + "domelementtype": "1" } }, "domutils": { @@ -1187,8 +1193,8 @@ "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", "requires": { - "dom-serializer": "0.1.0", - "domelementtype": "1.3.0" + "dom-serializer": "0", + "domelementtype": "1" } }, "ecc-jsbn": { @@ -1197,7 +1203,7 @@ "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", "optional": true, "requires": { - "jsbn": "0.1.1" + "jsbn": "~0.1.0" } }, "ee-first": { @@ -1215,7 +1221,7 @@ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.0.tgz", "integrity": "sha1-epDYM+/abPpurA9JSduw+tOmMgY=", "requires": { - "once": "1.4.0" + "once": "^1.4.0" } }, "entities": { @@ -1228,8 +1234,8 @@ "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.30.tgz", "integrity": "sha1-cUGhaDZpfbq/qq7uQUlc4p9SyTk=", "requires": { - "es6-iterator": "2.0.1", - "es6-symbol": "3.1.1" + "es6-iterator": "2", + "es6-symbol": "~3.1" } }, "es6-iterator": { @@ -1237,9 +1243,9 @@ "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.1.tgz", "integrity": "sha1-jjGcnwRTv1ddN0lAplWSDlnKVRI=", "requires": { - "d": "1.0.0", - "es5-ext": "0.10.30", - "es6-symbol": "3.1.1" + "d": "1", + "es5-ext": "^0.10.14", + "es6-symbol": "^3.1" } }, "es6-promise": { @@ -1252,8 +1258,8 @@ "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", "requires": { - "d": "1.0.0", - "es5-ext": "0.10.30" + "d": "1", + "es5-ext": "~0.10.14" } }, "es6-weak-map": { @@ -1261,10 +1267,10 @@ "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", "requires": { - "d": "1.0.0", - "es5-ext": "0.10.30", - "es6-iterator": "2.0.1", - "es6-symbol": "3.1.1" + "d": "1", + "es5-ext": "^0.10.14", + "es6-iterator": "^2.0.1", + "es6-symbol": "^3.1.1" } }, "escape-html": { @@ -1298,8 +1304,8 @@ "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", "requires": { - "d": "1.0.0", - "es5-ext": "0.10.30" + "d": "1", + "es5-ext": "~0.10.14" } }, "events": { @@ -1312,36 +1318,36 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.16.1.tgz", "integrity": "sha512-STB7LZ4N0L+81FJHGla2oboUHTk4PaN1RsOkoRh9OSeEKylvF5hwKYVX1xCLFaCT7MD0BNG/gX2WFMLqY6EMBw==", "requires": { - "accepts": "1.3.4", + "accepts": "~1.3.4", "array-flatten": "1.1.1", "body-parser": "1.18.2", "content-disposition": "0.5.2", - "content-type": "1.0.4", + "content-type": "~1.0.4", "cookie": "0.3.1", "cookie-signature": "1.0.6", "debug": "2.6.9", - "depd": "1.1.1", - "encodeurl": "1.0.1", - "escape-html": "1.0.3", - "etag": "1.8.1", + "depd": "~1.1.1", + "encodeurl": "~1.0.1", + "escape-html": "~1.0.3", + "etag": "~1.8.1", "finalhandler": "1.1.0", "fresh": "0.5.2", "merge-descriptors": "1.0.1", - "methods": "1.1.2", - "on-finished": "2.3.0", - "parseurl": "1.3.2", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", "path-to-regexp": "0.1.7", - "proxy-addr": "2.0.2", + "proxy-addr": "~2.0.2", "qs": "6.5.1", - "range-parser": "1.2.0", + "range-parser": "~1.2.0", "safe-buffer": "5.1.1", "send": "0.16.1", "serve-static": "1.13.1", "setprototypeof": "1.1.0", - "statuses": "1.3.1", - "type-is": "1.6.15", + "statuses": "~1.3.1", + "type-is": "~1.6.15", "utils-merge": "1.0.1", - "vary": "1.1.2" + "vary": "~1.1.2" }, "dependencies": { "setprototypeof": { @@ -1360,10 +1366,10 @@ "cookie-signature": "1.0.6", "crc": "3.4.4", "debug": "2.6.9", - "depd": "1.1.1", - "on-headers": "1.0.1", - "parseurl": "1.3.2", - "uid-safe": "2.1.5", + "depd": "~1.1.1", + "on-headers": "~1.0.1", + "parseurl": "~1.3.2", + "uid-safe": "~2.1.5", "utils-merge": "1.0.1" }, "dependencies": { @@ -1395,12 +1401,12 @@ "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", "requires": { "debug": "2.6.9", - "encodeurl": "1.0.1", - "escape-html": "1.0.3", - "on-finished": "2.3.0", - "parseurl": "1.3.2", - "statuses": "1.3.1", - "unpipe": "1.0.0" + "encodeurl": "~1.0.1", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.3.1", + "unpipe": "~1.0.0" } }, "flat": { @@ -1408,7 +1414,7 @@ "resolved": "https://registry.npmjs.org/flat/-/flat-4.0.0.tgz", "integrity": "sha512-ji/WMv2jdsE+LaznpkIF9Haax0sdpTBozrz/Dtg4qSRMfbs8oVg4ypJunIRYPiMLvH/ed6OflXbnbTIKJhtgeg==", "requires": { - "is-buffer": "1.1.5" + "is-buffer": "~1.1.5" } }, "forever-agent": { @@ -1421,9 +1427,9 @@ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.5", - "mime-types": "2.1.17" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.5", + "mime-types": "^2.1.12" } }, "format-util": { @@ -1452,8 +1458,8 @@ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-2.1.2.tgz", "integrity": "sha1-BGxwFjzvmq1GsOSn+kZ/si1x3jU=", "requires": { - "graceful-fs": "4.1.11", - "jsonfile": "2.4.0" + "graceful-fs": "^4.1.2", + "jsonfile": "^2.1.0" } }, "fs.realpath": { @@ -1472,7 +1478,7 @@ "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", "requires": { - "assert-plus": "1.0.0" + "assert-plus": "^1.0.0" } }, "git-rev-sync": { @@ -1490,12 +1496,12 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" } }, "graceful-fs": { @@ -1514,7 +1520,7 @@ "resolved": "https://registry.npmjs.org/graphql/-/graphql-0.10.5.tgz", "integrity": "sha512-Q7cx22DiLhwHsEfUnUip1Ww/Vfx7FS0w6+iHItNuN61+XpegHSa3k5U0+6M5BcpavQImBwFiy0z3uYwY7cXMLQ==", "requires": { - "iterall": "1.1.3" + "iterall": "^1.1.0" } }, "graphql-server-express": { @@ -1522,7 +1528,7 @@ "resolved": "https://registry.npmjs.org/graphql-server-express/-/graphql-server-express-1.1.2.tgz", "integrity": "sha1-boowamYWrHLHhQ76n9qcuimDYzU=", "requires": { - "apollo-server-express": "1.1.2" + "apollo-server-express": "^1.1.2" } }, "graphql-subscriptions": { @@ -1530,15 +1536,15 @@ "resolved": "https://registry.npmjs.org/graphql-subscriptions/-/graphql-subscriptions-0.4.4.tgz", "integrity": "sha512-hqfUsZv39qmK4SEoKMnTO05U4EVvIeAD4ai5ztE9gCl4hEdeaF2Q5gvF80ONQQAnkys4odzxWYd2tBLS/cWl8g==", "requires": { - "@types/graphql": "0.9.4", - "es6-promise": "4.1.1", - "iterall": "1.1.3" + "@types/graphql": "^0.9.1", + "es6-promise": "^4.0.5", + "iterall": "^1.1.1" }, "dependencies": { "es6-promise": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.1.1.tgz", - "integrity": "sha512-OaU1hHjgJf+b0NzsxCg7NdIYERD6Hy/PEmFLTjw+b65scuisG3Kt4QoTvJ66BBkPZ581gr0kpoVzKnxniM8nng==" + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz", + "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==" } } }, @@ -1547,20 +1553,21 @@ "resolved": "https://registry.npmjs.org/graphql-tools/-/graphql-tools-1.2.3.tgz", "integrity": "sha512-3inNK3rmk32G4hGWbqBuVNxusF+Mcuckg+3aD4hHaMxO0LrSgteWoTD8pTD9GUnmoSRG4AbYHZ0jibGD5MTlrQ==", "requires": { - "@types/graphql": "0.9.4", - "deprecated-decorator": "0.1.6", - "uuid": "3.1.0" + "@types/graphql": "^0.9.0", + "deprecated-decorator": "^0.1.6", + "uuid": "^3.0.1" } }, "graphql-typewriter": { "version": "git://github.com/illegalprime/graphql-typewriter.git#6c9e4c7490256cc1fcac7af32f18d471c0b375ca", + "from": "git://github.com/illegalprime/graphql-typewriter.git#hacks", "requires": { - "commander": "2.11.0", - "glob": "7.1.2", - "graphql": "0.10.5", - "graphql-subscriptions": "0.4.4", - "graphql-tools": "1.2.3", - "m-io": "0.5.0" + "commander": "^2.11.0", + "glob": "^7.1.2", + "graphql": "^0.10.5", + "graphql-subscriptions": "^0.4.4", + "graphql-tools": "^1.1.0", + "m-io": "^0.5.0" } }, "growl": { @@ -1574,10 +1581,10 @@ "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.10.tgz", "integrity": "sha1-PTDHGLCaPZbyPqTMH0A8TTup/08=", "requires": { - "async": "1.5.2", - "optimist": "0.6.1", - "source-map": "0.4.4", - "uglify-js": "2.8.29" + "async": "^1.4.0", + "optimist": "^0.6.1", + "source-map": "^0.4.4", + "uglify-js": "^2.6" }, "dependencies": { "async": { @@ -1597,8 +1604,8 @@ "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", "requires": { - "ajv": "5.2.3", - "har-schema": "2.0.0" + "ajv": "^5.1.0", + "har-schema": "^2.0.0" } }, "has-ansi": { @@ -1607,7 +1614,7 @@ "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", "dev": true, "requires": { - "ansi-regex": "2.1.1" + "ansi-regex": "^2.0.0" } }, "has-flag": { @@ -1620,10 +1627,10 @@ "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", "requires": { - "boom": "4.3.1", - "cryptiles": "3.1.2", - "hoek": "4.2.1", - "sntp": "2.1.0" + "boom": "4.x.x", + "cryptiles": "3.x.x", + "hoek": "4.x.x", + "sntp": "2.x.x" } }, "he": { @@ -1647,12 +1654,12 @@ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz", "integrity": "sha1-G9+HrMoPP55T+k/M6w9LTLsAszg=", "requires": { - "domelementtype": "1.3.0", - "domhandler": "2.4.1", - "domutils": "1.5.1", - "entities": "1.1.1", - "inherits": "2.0.3", - "readable-stream": "2.3.3" + "domelementtype": "^1.3.0", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^2.0.2" } }, "http-errors": { @@ -1663,7 +1670,7 @@ "depd": "1.1.1", "inherits": "2.0.3", "setprototypeof": "1.0.3", - "statuses": "1.3.1" + "statuses": ">= 1.3.1 < 2" } }, "http-signature": { @@ -1671,9 +1678,9 @@ "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", "requires": { - "assert-plus": "1.0.0", - "jsprim": "1.4.1", - "sshpk": "1.13.1" + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" } }, "iconv-lite": { @@ -1691,8 +1698,8 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" + "once": "^1.3.0", + "wrappy": "1" } }, "inherits": { @@ -1756,8 +1763,8 @@ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", "requires": { - "argparse": "1.0.9", - "esprima": "4.0.0" + "argparse": "^1.0.7", + "esprima": "^4.0.0" } }, "jsbn": { @@ -1776,12 +1783,12 @@ "resolved": "https://registry.npmjs.org/json-schema-ref-parser/-/json-schema-ref-parser-3.3.1.tgz", "integrity": "sha512-stQTMhec2R/p2L9dH4XXRlpNCP0mY8QrLd/9Kl+8SHJQmwHtE1nDfXH4wbsSM+GkJMl8t92yZbI0OIol432CIQ==", "requires": { - "call-me-maybe": "1.0.1", - "debug": "3.1.0", - "es6-promise": "4.1.1", - "js-yaml": "3.10.0", - "ono": "4.0.2", - "z-schema": "3.18.4" + "call-me-maybe": "^1.0.1", + "debug": "^3.0.0", + "es6-promise": "^4.1.1", + "js-yaml": "^3.9.1", + "ono": "^4.0.2", + "z-schema": "^3.18.2" }, "dependencies": { "debug": { @@ -1804,12 +1811,12 @@ "resolved": "https://registry.npmjs.org/json-schema-to-typescript/-/json-schema-to-typescript-4.6.4.tgz", "integrity": "sha512-XXa5Qt5yoRW9M/88b1+57LZ1Z76h5qXIjRQ0GxaRAoHE5uSzwFR/+plzv+SSJs6gsJedXiyuVuyeqPU8mkDsew==", "requires": { - "cli-color": "1.2.0", - "json-schema-ref-parser": "3.3.1", - "json-stringify-safe": "5.0.1", - "lodash": "4.17.4", - "minimist": "1.2.0", - "mz": "2.7.0", + "cli-color": "^1.2.0", + "json-schema-ref-parser": "^3.3.1", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.4", + "minimist": "^1.2.0", + "mz": "^2.7.0", "stdin": "0.0.1" }, "dependencies": { @@ -1830,7 +1837,7 @@ "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", "requires": { - "jsonify": "0.0.0" + "jsonify": "~0.0.0" } }, "json-stringify-safe": { @@ -1843,16 +1850,16 @@ "resolved": "https://registry.npmjs.org/json2csv/-/json2csv-3.11.2.tgz", "integrity": "sha1-aoFm2jQ3UatOjDH4Q0F7rUTMgz8=", "requires": { - "cli-table": "0.3.1", - "commander": "2.11.0", - "debug": "3.1.0", - "flat": "4.0.0", - "lodash.clonedeep": "4.5.0", - "lodash.flatten": "4.4.0", - "lodash.get": "4.4.2", - "lodash.set": "4.3.2", - "lodash.uniq": "4.5.0", - "path-is-absolute": "1.0.1" + "cli-table": "^0.3.1", + "commander": "^2.8.1", + "debug": "^3.0.0", + "flat": "^4.0.0", + "lodash.clonedeep": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.get": "^4.4.0", + "lodash.set": "^4.3.0", + "lodash.uniq": "^4.5.0", + "path-is-absolute": "^1.0.0" }, "dependencies": { "debug": { @@ -1876,7 +1883,7 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", "requires": { - "graceful-fs": "4.1.11" + "graceful-fs": "^4.1.6" } }, "jsonify": { @@ -1905,7 +1912,7 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "requires": { - "is-buffer": "1.1.5" + "is-buffer": "^1.1.5" } }, "lazy-cache": { @@ -1919,7 +1926,7 @@ "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", "requires": { - "readable-stream": "2.3.3" + "readable-stream": "^2.0.5" } }, "lodash": { @@ -1933,8 +1940,8 @@ "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", "dev": true, "requires": { - "lodash._basecopy": "3.0.1", - "lodash.keys": "3.1.2" + "lodash._basecopy": "^3.0.0", + "lodash.keys": "^3.0.0" } }, "lodash._basecopy": { @@ -1972,9 +1979,9 @@ "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=", "dev": true, "requires": { - "lodash._baseassign": "3.2.0", - "lodash._basecreate": "3.0.3", - "lodash._isiterateecall": "3.0.9" + "lodash._baseassign": "^3.0.0", + "lodash._basecreate": "^3.0.0", + "lodash._isiterateecall": "^3.0.0" } }, "lodash.flatten": { @@ -2010,9 +2017,9 @@ "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", "dev": true, "requires": { - "lodash._getnative": "3.9.1", - "lodash.isarguments": "3.1.0", - "lodash.isarray": "3.0.4" + "lodash._getnative": "^3.0.0", + "lodash.isarguments": "^3.0.0", + "lodash.isarray": "^3.0.0" } }, "lodash.set": { @@ -2035,7 +2042,7 @@ "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", "integrity": "sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=", "requires": { - "es5-ext": "0.10.30" + "es5-ext": "~0.10.2" } }, "m-io": { @@ -2043,8 +2050,8 @@ "resolved": "https://registry.npmjs.org/m-io/-/m-io-0.5.0.tgz", "integrity": "sha1-chuybdjUN13Oy1bY3AFm64vp0AM=", "requires": { - "fs-extra": "2.1.2", - "q": "1.5.0" + "fs-extra": "^2.0.0", + "q": "^1.4.1" } }, "marked": { @@ -2062,14 +2069,14 @@ "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.11.tgz", "integrity": "sha1-vemBdmPJ5A/bKk6hw2cpYIeujI8=", "requires": { - "d": "1.0.0", - "es5-ext": "0.10.30", - "es6-weak-map": "2.0.2", - "event-emitter": "0.3.5", - "is-promise": "2.1.0", - "lru-queue": "0.1.0", - "next-tick": "1.0.0", - "timers-ext": "0.1.2" + "d": "1", + "es5-ext": "^0.10.30", + "es6-weak-map": "^2.0.2", + "event-emitter": "^0.3.5", + "is-promise": "^2.1", + "lru-queue": "0.1", + "next-tick": "1", + "timers-ext": "^0.1.2" } }, "merge-descriptors": { @@ -2097,7 +2104,7 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", "requires": { - "mime-db": "1.30.0" + "mime-db": "~1.30.0" } }, "minimatch": { @@ -2105,7 +2112,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "requires": { - "brace-expansion": "1.1.8" + "brace-expansion": "^1.1.7" } }, "minimist": { @@ -2154,7 +2161,7 @@ "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", "dev": true, "requires": { - "graceful-readlink": "1.0.1" + "graceful-readlink": ">= 1.0.0" } }, "debug": { @@ -2172,12 +2179,12 @@ "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=", "dev": true, "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.2", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" } }, "has-flag": { @@ -2192,7 +2199,7 @@ "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", "dev": true, "requires": { - "has-flag": "1.0.0" + "has-flag": "^1.0.0" } } } @@ -2207,7 +2214,7 @@ "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.13.tgz", "integrity": "sha1-mc5cfYJyYusPH3AgRBd/YHRde5A=", "requires": { - "moment": "2.21.0" + "moment": ">= 2.9.0" } }, "mongodb": { @@ -2225,13 +2232,13 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.7.tgz", "integrity": "sha1-BwV6y+JGeyIELTb5jFrVBwVOlbE=", "requires": { - "buffer-shims": "1.0.0", - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "string_decoder": "1.0.3", - "util-deprecate": "1.0.2" + "buffer-shims": "~1.0.0", + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~1.0.0", + "util-deprecate": "~1.0.1" } } } @@ -2241,8 +2248,8 @@ "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.15.tgz", "integrity": "sha1-hB9TuH//9MdFgYnDXIroJ+EWl2Q=", "requires": { - "bson": "1.0.4", - "require_optional": "1.0.1" + "bson": "~1.0.4", + "require_optional": "~1.0.0" } }, "mongoose": { @@ -2251,7 +2258,7 @@ "integrity": "sha1-ul3POxM4yEmC+XdMEMoEkxAs2DA=", "requires": { "async": "2.1.4", - "bson": "1.0.4", + "bson": "~1.0.4", "hooks-fixed": "2.0.0", "kareem": "1.5.0", "mongodb": "2.2.31", @@ -2269,7 +2276,7 @@ "resolved": "https://registry.npmjs.org/async/-/async-2.1.4.tgz", "integrity": "sha1-LSFgx3iAMuTdbL4lAvH5osj2zeQ=", "requires": { - "lodash": "4.17.4" + "lodash": "^4.14.0" } } } @@ -2279,11 +2286,11 @@ "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.0.tgz", "integrity": "sha1-0B+mxlhZt2/PMbPLU6OCGjEdgFE=", "requires": { - "basic-auth": "2.0.0", + "basic-auth": "~2.0.0", "debug": "2.6.9", - "depd": "1.1.1", - "on-finished": "2.3.0", - "on-headers": "1.0.1" + "depd": "~1.1.1", + "on-finished": "~2.3.0", + "on-headers": "~1.0.1" } }, "mpath": { @@ -2301,9 +2308,9 @@ "resolved": "https://registry.npmjs.org/mquery/-/mquery-2.3.2.tgz", "integrity": "sha512-KXWMypZSvhCuqRtza+HMQZdYw7PfFBjBTFvP31NNAq0OX0/NTIgpcDpkWQ2uTxk6vGQtwQ2elhwhs+ZvCA8OaA==", "requires": { - "bluebird": "3.5.0", - "debug": "2.6.9", - "regexp-clone": "0.0.1", + "bluebird": "^3.5.0", + "debug": "^2.6.9", + "regexp-clone": "^0.0.1", "sliced": "0.0.5" }, "dependencies": { @@ -2324,14 +2331,14 @@ "resolved": "https://registry.npmjs.org/multer/-/multer-1.3.0.tgz", "integrity": "sha1-CSsmcPaEb6SRSWXvyM+Uwg/sbNI=", "requires": { - "append-field": "0.1.0", - "busboy": "0.2.14", - "concat-stream": "1.6.0", - "mkdirp": "0.5.1", - "object-assign": "3.0.0", - "on-finished": "2.3.0", - "type-is": "1.6.15", - "xtend": "4.0.1" + "append-field": "^0.1.0", + "busboy": "^0.2.11", + "concat-stream": "^1.5.0", + "mkdirp": "^0.5.1", + "object-assign": "^3.0.0", + "on-finished": "^2.3.0", + "type-is": "^1.6.4", + "xtend": "^4.0.0" }, "dependencies": { "object-assign": { @@ -2351,9 +2358,9 @@ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", "requires": { - "any-promise": "1.3.0", - "object-assign": "4.1.1", - "thenify-all": "1.6.0" + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" } }, "negotiator": { @@ -2371,7 +2378,7 @@ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", "requires": { - "remove-trailing-separator": "1.1.0" + "remove-trailing-separator": "^1.0.1" } }, "nth-check": { @@ -2379,7 +2386,7 @@ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz", "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=", "requires": { - "boolbase": "1.0.0" + "boolbase": "~1.0.0" } }, "oauth": { @@ -2415,7 +2422,7 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "requires": { - "wrappy": "1.0.2" + "wrappy": "1" } }, "ono": { @@ -2423,7 +2430,7 @@ "resolved": "https://registry.npmjs.org/ono/-/ono-4.0.2.tgz", "integrity": "sha512-EFXJFoeF+KkZW4lwmcPMKHp2ZU7o6CM+ccX2nPbEJKiJIdyqbIcS1v6pmNgeNJ6x4/vEYn0/8oz66qXSPnnmSQ==", "requires": { - "format-util": "1.0.3" + "format-util": "^1.0.3" } }, "optimist": { @@ -2431,8 +2438,8 @@ "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", "requires": { - "minimist": "0.0.10", - "wordwrap": "0.0.3" + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" } }, "os-tmpdir": { @@ -2445,7 +2452,7 @@ "resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.2.tgz", "integrity": "sha1-Be/1fw70V3+xRKefi5qWemzERRA=", "requires": { - "@types/node": "6.0.88" + "@types/node": "^6.0.46" }, "dependencies": { "@types/node": { @@ -2465,7 +2472,7 @@ "resolved": "https://registry.npmjs.org/passport/-/passport-0.3.2.tgz", "integrity": "sha1-ndAJ+RXo/glbASSgG4+C2gdRAQI=", "requires": { - "passport-strategy": "1.0.0", + "passport-strategy": "1.x.x", "pause": "0.0.1" } }, @@ -2474,7 +2481,7 @@ "resolved": "https://registry.npmjs.org/passport-facebook/-/passport-facebook-2.1.1.tgz", "integrity": "sha1-w50LUq5NWRYyRaTiGnubYyEwMxE=", "requires": { - "passport-oauth2": "1.4.0" + "passport-oauth2": "1.x.x" } }, "passport-github2": { @@ -2482,7 +2489,7 @@ "resolved": "https://registry.npmjs.org/passport-github2/-/passport-github2-0.1.11.tgz", "integrity": "sha1-yStW88OKROdmqsfp58E4TF6TyZk=", "requires": { - "passport-oauth2": "1.4.0" + "passport-oauth2": "1.x.x" } }, "passport-google-oauth20": { @@ -2490,7 +2497,7 @@ "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-1.0.0.tgz", "integrity": "sha1-O5YOih1w0dvnlGFcgnxoxAOSpdA=", "requires": { - "passport-oauth2": "1.4.0" + "passport-oauth2": "1.x.x" } }, "passport-local": { @@ -2498,7 +2505,7 @@ "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", "integrity": "sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=", "requires": { - "passport-strategy": "1.0.0" + "passport-strategy": "1.x.x" } }, "passport-oauth2": { @@ -2506,10 +2513,10 @@ "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.4.0.tgz", "integrity": "sha1-9i+BWDy+EmCb585vFguTlaJ7hq0=", "requires": { - "oauth": "0.9.15", - "passport-strategy": "1.0.0", - "uid2": "0.0.3", - "utils-merge": "1.0.1" + "oauth": "0.9.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" } }, "passport-strategy": { @@ -2558,7 +2565,7 @@ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.2.tgz", "integrity": "sha1-ZXFQT0e7mI7IGAJT+F3X4UlSvew=", "requires": { - "forwarded": "0.1.2", + "forwarded": "~0.1.2", "ipaddr.js": "1.5.2" } }, @@ -2568,9 +2575,9 @@ "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" }, "q": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.0.tgz", - "integrity": "sha1-3QG6ydBtMObyGa7LglPunr3DCPE=" + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" }, "qs": { "version": "6.5.1", @@ -2608,13 +2615,13 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "safe-buffer": "5.1.1", - "string_decoder": "1.0.3", - "util-deprecate": "1.0.2" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.0.3", + "util-deprecate": "~1.0.1" } }, "rechoir": { @@ -2622,7 +2629,7 @@ "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", "requires": { - "resolve": "1.4.0" + "resolve": "^1.1.6" } }, "regexp-clone": { @@ -2645,28 +2652,28 @@ "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", "requires": { - "aws-sign2": "0.7.0", - "aws4": "1.6.0", - "caseless": "0.12.0", - "combined-stream": "1.0.5", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.3.1", - "har-validator": "5.0.3", - "hawk": "6.0.2", - "http-signature": "1.2.0", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.17", - "oauth-sign": "0.8.2", - "performance-now": "2.1.0", - "qs": "6.5.1", - "safe-buffer": "5.1.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.4", - "tunnel-agent": "0.6.0", - "uuid": "3.1.0" + "aws-sign2": "~0.7.0", + "aws4": "^1.6.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.1", + "forever-agent": "~0.6.1", + "form-data": "~2.3.1", + "har-validator": "~5.0.3", + "hawk": "~6.0.2", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.17", + "oauth-sign": "~0.8.2", + "performance-now": "^2.1.0", + "qs": "~6.5.1", + "safe-buffer": "^5.1.1", + "stringstream": "~0.0.5", + "tough-cookie": "~2.3.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.1.0" } }, "require_optional": { @@ -2674,8 +2681,8 @@ "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", "requires": { - "resolve-from": "2.0.0", - "semver": "5.4.1" + "resolve-from": "^2.0.0", + "semver": "^5.1.0" } }, "resolve": { @@ -2683,7 +2690,7 @@ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.4.0.tgz", "integrity": "sha512-aW7sVKPufyHqOmyyLzg/J+8606v5nevBgaliIlV7nUpVMsDnoBGV/cbSLNjZAg9q0Cfd/+easKVKQ8vOu8fn1Q==", "requires": { - "path-parse": "1.0.5" + "path-parse": "^1.0.5" } }, "resolve-from": { @@ -2697,7 +2704,7 @@ "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", "optional": true, "requires": { - "align-text": "0.1.4" + "align-text": "^0.1.1" } }, "safe-buffer": { @@ -2721,18 +2728,18 @@ "integrity": "sha512-ElCLJdJIKPk6ux/Hocwhk7NFHpI3pVm/IZOYWqUmoxcgeyM+MpxHHKhb8QmlJDX1pU6WrgaHBkVNm73Sv7uc2A==", "requires": { "debug": "2.6.9", - "depd": "1.1.1", - "destroy": "1.0.4", - "encodeurl": "1.0.1", - "escape-html": "1.0.3", - "etag": "1.8.1", + "depd": "~1.1.1", + "destroy": "~1.0.4", + "encodeurl": "~1.0.1", + "escape-html": "~1.0.3", + "etag": "~1.8.1", "fresh": "0.5.2", - "http-errors": "1.6.2", + "http-errors": "~1.6.2", "mime": "1.4.1", "ms": "2.0.0", - "on-finished": "2.3.0", - "range-parser": "1.2.0", - "statuses": "1.3.1" + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.3.1" } }, "serve-static": { @@ -2740,9 +2747,9 @@ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.1.tgz", "integrity": "sha512-hSMUZrsPa/I09VYFJwa627JJkNs0NrfL1Uzuup+GqHfToR2KcsXFymXSV90hoyw3M+msjFuQly+YzIH/q0MGlQ==", "requires": { - "encodeurl": "1.0.1", - "escape-html": "1.0.3", - "parseurl": "1.3.2", + "encodeurl": "~1.0.1", + "escape-html": "~1.0.3", + "parseurl": "~1.3.2", "send": "0.16.1" } }, @@ -2756,9 +2763,9 @@ "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.7.tgz", "integrity": "sha1-svXHfvlxSPS09uImguELuoZnz/E=", "requires": { - "glob": "7.1.2", - "interpret": "1.0.4", - "rechoir": "0.6.2" + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" } }, "sliced": { @@ -2771,7 +2778,7 @@ "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", "requires": { - "hoek": "4.2.1" + "hoek": "4.x.x" } }, "source-map": { @@ -2779,7 +2786,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", "requires": { - "amdefine": "1.0.1" + "amdefine": ">=0.0.4" } }, "sprintf-js": { @@ -2792,14 +2799,14 @@ "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "tweetnacl": "~0.14.0" } }, "statuses": { @@ -2822,7 +2829,7 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", "requires": { - "safe-buffer": "5.1.1" + "safe-buffer": "~5.1.0" } }, "stringstream": { @@ -2836,7 +2843,7 @@ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { - "ansi-regex": "2.1.1" + "ansi-regex": "^2.0.0" } }, "striptags": { @@ -2850,16 +2857,16 @@ "integrity": "sha512-eCWciyl+6YrBQmMBS54GTPsdDLhVV27URTDpvMFKXZDfzXgtU42KAUpo2GFkldWA/1tGnWbn5Xo1vO5RygVtew==", "dev": true, "requires": { - "component-emitter": "1.2.1", - "cookiejar": "2.1.1", - "debug": "3.1.0", - "extend": "3.0.1", - "form-data": "2.3.1", - "formidable": "1.1.1", - "methods": "1.1.2", - "mime": "2.0.3", - "qs": "6.5.1", - "readable-stream": "2.3.3" + "component-emitter": "^1.2.0", + "cookiejar": "^2.1.0", + "debug": "^3.1.0", + "extend": "^3.0.0", + "form-data": "^2.3.1", + "formidable": "^1.1.1", + "methods": "^1.1.1", + "mime": "^2.0.3", + "qs": "^6.5.1", + "readable-stream": "^2.0.5" }, "dependencies": { "debug": { @@ -2885,8 +2892,8 @@ "integrity": "sha1-jUu2j9GDDuBwM7HFpamkAhyWUpY=", "dev": true, "requires": { - "methods": "1.1.2", - "superagent": "3.6.2" + "methods": "~1.1.2", + "superagent": "^3.0.0" } }, "supports-color": { @@ -2894,7 +2901,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==", "requires": { - "has-flag": "2.0.0" + "has-flag": "^2.0.0" } }, "sweetalert2": { @@ -2907,10 +2914,10 @@ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.5.4.tgz", "integrity": "sha1-NlSc8E7RrumyowwBQyUiONr5QBY=", "requires": { - "bl": "1.2.1", - "end-of-stream": "1.4.0", - "readable-stream": "2.3.3", - "xtend": "4.0.1" + "bl": "^1.0.0", + "end-of-stream": "^1.0.0", + "readable-stream": "^2.0.0", + "xtend": "^4.0.0" } }, "thenify": { @@ -2918,7 +2925,7 @@ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz", "integrity": "sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=", "requires": { - "any-promise": "1.3.0" + "any-promise": "^1.0.0" } }, "thenify-all": { @@ -2926,7 +2933,7 @@ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", "requires": { - "thenify": "3.3.0" + "thenify": ">= 3.1.0 < 4" } }, "timers-ext": { @@ -2934,8 +2941,8 @@ "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.2.tgz", "integrity": "sha1-YcxHp2wavTGV8UUn+XjViulMUgQ=", "requires": { - "es5-ext": "0.10.30", - "next-tick": "1.0.0" + "es5-ext": "~0.10.14", + "next-tick": "1" } }, "tmp": { @@ -2943,7 +2950,7 @@ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", "requires": { - "os-tmpdir": "1.0.2" + "os-tmpdir": "~1.0.2" } }, "tough-cookie": { @@ -2951,7 +2958,7 @@ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", "requires": { - "punycode": "1.4.1" + "punycode": "^1.4.1" }, "dependencies": { "punycode": { @@ -2962,34 +2969,71 @@ } }, "tslib": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.7.1.tgz", - "integrity": "sha1-vIAEFkaRkjp5/oN4u+s9ogF1OOw=", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.2.tgz", + "integrity": "sha512-AVP5Xol3WivEr7hnssHDsaM+lVrVXWUvd1cfXTRkTj80b//6g2wIFEH6hZG0muGZRnHGrfttpdzRk3YlBkWjKw==", "dev": true }, "tslint": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.7.0.tgz", - "integrity": "sha1-wl4NDJL6EgHCvDDoROCOaCtPNVI=", + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.10.0.tgz", + "integrity": "sha1-EeJrzLiK+gLdDZlWyuPUVAtfVMM=", "dev": true, "requires": { - "babel-code-frame": "6.26.0", - "colors": "1.1.2", - "commander": "2.11.0", - "diff": "3.2.0", - "glob": "7.1.2", - "minimatch": "3.0.4", - "resolve": "1.4.0", - "semver": "5.4.1", - "tslib": "1.7.1", - "tsutils": "2.11.2" + "babel-code-frame": "^6.22.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^3.2.0", + "glob": "^7.1.1", + "js-yaml": "^3.7.0", + "minimatch": "^3.0.4", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.8.0", + "tsutils": "^2.12.1" }, "dependencies": { - "colors": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", - "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } } } }, @@ -3000,12 +3044,12 @@ "dev": true }, "tsutils": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.11.2.tgz", - "integrity": "sha1-YBNgHjb6FP+VhBPlQdQn+4xqw0E=", + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.27.1.tgz", + "integrity": "sha512-AE/7uzp32MmaHvNNFES85hhUDHFdFZp6OAiZcd6y4ZKKIg6orJTm8keYWBhIhrJQH3a4LzNKat7ZPXZt5aTf6w==", "dev": true, "requires": { - "tslib": "1.7.1" + "tslib": "^1.8.1" } }, "tunnel-agent": { @@ -3013,7 +3057,7 @@ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", "requires": { - "safe-buffer": "5.1.1" + "safe-buffer": "^5.0.1" } }, "tweetnacl": { @@ -3034,7 +3078,7 @@ "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", "requires": { "media-typer": "0.3.0", - "mime-types": "2.1.17" + "mime-types": "~2.1.15" } }, "typedarray": { @@ -3043,9 +3087,9 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, "typescript": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.5.3.tgz", - "integrity": "sha512-ptLSQs2S4QuS6/OD1eAKG+S5G8QQtrU5RT32JULdZQtM1L3WTi34Wsu48Yndzi8xsObRAB9RPt/KhA9wlpEF6w==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.1.tgz", + "integrity": "sha512-h6pM2f/GDchCFlldnriOhs1QHuwbnmj6/v7499eMHqPeW4V2G0elua2eIc2nu8v2NdHV0Gm+tzX83Hr6nUFjQA==", "dev": true }, "uglify-js": { @@ -3054,9 +3098,9 @@ "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", "optional": true, "requires": { - "source-map": "0.5.7", - "uglify-to-browserify": "1.0.2", - "yargs": "3.10.0" + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.10.0" }, "dependencies": { "source-map": { @@ -3078,7 +3122,7 @@ "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", "requires": { - "random-bytes": "1.0.0" + "random-bytes": "~1.0.0" } }, "uid2": { @@ -3130,9 +3174,9 @@ "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", "requires": { - "assert-plus": "1.0.0", + "assert-plus": "^1.0.0", "core-util-is": "1.0.2", - "extsprintf": "1.3.0" + "extsprintf": "^1.2.0" } }, "walkdir": { @@ -3166,8 +3210,8 @@ "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.17.tgz", "integrity": "sha1-F76T6q4/O3eTWceVtBlwWogX6Gg=", "requires": { - "sax": "1.2.1", - "xmlbuilder": "4.2.1" + "sax": ">=0.6.0", + "xmlbuilder": "^4.1.0" } }, "xmlbuilder": { @@ -3175,7 +3219,7 @@ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-4.2.1.tgz", "integrity": "sha1-qlijBBoGb5DqoWwvU4n/GfP0YaU=", "requires": { - "lodash": "4.17.4" + "lodash": "^4.0.0" } }, "xtend": { @@ -3189,9 +3233,9 @@ "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", "optional": true, "requires": { - "camelcase": "1.2.1", - "cliui": "2.1.0", - "decamelize": "1.2.0", + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", "window-size": "0.1.0" } }, @@ -3200,10 +3244,10 @@ "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-3.18.4.tgz", "integrity": "sha512-DUOKC/IhbkdLKKiV89gw9DUauTV8U/8yJl1sjf6MtDmzevLKOF2duNJ495S3MFVjqZarr+qNGCPbkg4mu4PpLw==", "requires": { - "commander": "2.11.0", - "lodash.get": "4.4.2", - "lodash.isequal": "4.5.0", - "validator": "8.2.0" + "commander": "^2.7.1", + "lodash.get": "^4.0.0", + "lodash.isequal": "^4.0.0", + "validator": "^8.0.0" } }, "zip-stream": { @@ -3211,10 +3255,10 @@ "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-1.2.0.tgz", "integrity": "sha1-qLxF9MG0lpnGuQGYuqyqzbzUugQ=", "requires": { - "archiver-utils": "1.3.0", - "compress-commons": "1.2.0", - "lodash": "4.17.4", - "readable-stream": "2.3.3" + "archiver-utils": "^1.3.0", + "compress-commons": "^1.2.0", + "lodash": "^4.8.0", + "readable-stream": "^2.0.0" } } } diff --git a/package.json b/package.json index b52ec99e..c209061d 100644 --- a/package.json +++ b/package.json @@ -100,8 +100,8 @@ "chai": "^4.0.0", "mocha": "^3.4.2", "supertest": "^3.0.0", - "tslint": "^5.4.3", + "tslint": "^5.10.0", "tslint-language-service": "^0.9.6", - "typescript": "^2.5.2" + "typescript": "^2.9.1" } } diff --git a/server/branch.ts b/server/branch.ts index 6ffd47da..fedb0763 100644 --- a/server/branch.ts +++ b/server/branch.ts @@ -248,7 +248,7 @@ export class BranchConfig { public static async loadBranchFromDB(name: string, location: string = config.questionsLocation): Promise { let branchConfig = await QuestionBranchConfig.findOne({ name }); if (!branchConfig) { - return await new NoopBranch(name, location).loadFromSchema(); + return new NoopBranch(name, location).loadFromSchema(); } let instance: QuestionBranch; diff --git a/server/common.ts b/server/common.ts index 3fd56658..8525f5e7 100644 --- a/server/common.ts +++ b/server/common.ts @@ -1,3 +1,5 @@ +// Needed so that common.ts <-> schema.ts cyclical dependencies don't cause problems +/* tslint:disable:no-duplicate-imports */ import * as fs from "fs"; import * as crypto from "crypto"; import * as path from "path"; @@ -464,7 +466,7 @@ export async function renderEmailHTML(markdown: string, user: IUser): Promise { let html: string; diff --git a/server/middleware.ts b/server/middleware.ts index 3fd55f2f..17df9af3 100644 --- a/server/middleware.ts +++ b/server/middleware.ts @@ -4,9 +4,8 @@ import * as path from "path"; import * as os from "os"; import { config, isBranchOpen } from "./common"; -import { BranchConfig, ApplicationBranch } from "./branch"; -import { User, IUser } from "./schema"; -import * as Branches from "./branch"; +import { BranchConfig, ApplicationBranch, ConfirmationBranch } from "./branch"; +import { User, IUser, DataLog, HackGTMetrics } from "./schema"; // // Express middleware @@ -151,16 +150,16 @@ export async function onlyAllowAnonymousBranch(request: express.Request, respons export async function canUserModify(request: express.Request, response: express.Response, next: express.NextFunction) { let user = await User.findOne({uuid: request.params.uuid}) as IUser; let branchName = request.params.branch as string; - let questionBranch = (await Branches.BranchConfig.loadAllBranches()).find(branch => branch.name.toLowerCase() === branchName.toLowerCase()); + let questionBranch = (await BranchConfig.loadAllBranches()).find(branch => branch.name.toLowerCase() === branchName.toLowerCase()); - if (!(await isBranchOpen(request.params.branch, user, questionBranch instanceof Branches.ApplicationBranch ? ApplicationType.Application : ApplicationType.Confirmation))) { + if (!(await isBranchOpen(request.params.branch, user, questionBranch instanceof ApplicationBranch ? ApplicationType.Application : ApplicationType.Confirmation))) { response.status(400).json({ "error": "Branch is closed" }); return; } - if (questionBranch instanceof Branches.ApplicationBranch) { + if (questionBranch instanceof ApplicationBranch) { // Don't allow user to modify application if we assigned them a confirmation branch if (user.confirmationBranch) { response.status(400).json({ @@ -174,7 +173,7 @@ export async function canUserModify(request: express.Request, response: express. }); return; } - } else if (questionBranch instanceof Branches.ConfirmationBranch) { + } else if (questionBranch instanceof ConfirmationBranch) { if (!user.confirmationBranch) { response.status(400).json({ "error": "You can't confirm for that branch" @@ -261,7 +260,6 @@ export function branchRedirector(requestType: ApplicationType): (request: expres }; } -import { DataLog, HackGTMetrics } from "./schema"; export function trackEvent(action: string, request: express.Request, user?: string, data?: object) { let thisEvent: DataLog = { action, diff --git a/server/routes/api/graphql.ts b/server/routes/api/graphql.ts index 040f2d30..ec0cf8c3 100644 --- a/server/routes/api/graphql.ts +++ b/server/routes/api/graphql.ts @@ -31,7 +31,7 @@ const resolvers: IResolver = { user: async (prev, args, request) => { const id = args.id || (request.user as IUser).uuid; const user = await User.findOne({uuid: id}); - return user ? await userRecordToGraphql(user) : undefined; + return user ? userRecordToGraphql(user) : undefined; }, users: async (prev, args) => { const lastIdQuery = args.pagination_token ? { @@ -46,7 +46,7 @@ const resolvers: IResolver = { }) .limit(args.n); - return await Promise.all(allUsers.map(userRecordToGraphql)); + return Promise.all(allUsers.map(userRecordToGraphql)); }, search_user: searchUser, search_user_simple: async (prev, args) => { @@ -83,7 +83,7 @@ const resolvers: IResolver = { return (await findQuestions(prev, { names: [args.name] }))[0]; }, questions: async (prev, args) => { - return await findQuestions(prev, args); + return findQuestions(prev, args); } } }; diff --git a/tslint.json b/tslint.json index 7d22b21a..95a783b0 100644 --- a/tslint.json +++ b/tslint.json @@ -61,7 +61,6 @@ "allow-undefined-check" ], "use-isnan": true, - "typeof-compare": true, "deprecation": true, "eofline": true, "linebreak-style": [ @@ -159,6 +158,19 @@ "allow-declarations", "allow-named-functions" ], - "max-classes-per-file": [false] + "max-classes-per-file": [false], + "no-duplicate-imports": true, + "no-this-assignment": [ + true, + { + "allow-destructuring": true + } + ], + "no-return-await": true, + "no-duplicate-switch-case": true, + "ban-comma-operator": true, + "no-dynamic-delete": true, + "prefer-readonly": true, + "prefer-while": true } } From 41bf1f54a9944f542a2f61e40f45c2e45ad1873e Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Mon, 4 Jun 2018 18:07:41 -0400 Subject: [PATCH 049/194] Implement and use method for getting canonical names from URL slugs Fixes lowercase branch names crashing the server when navigating to the apply or confirm forms --- server/branch.ts | 13 +++++++++++++ server/common.ts | 7 ++++--- server/middleware.ts | 39 +++++++++++++++++++++++---------------- 3 files changed, 40 insertions(+), 19 deletions(-) diff --git a/server/branch.ts b/server/branch.ts index fedb0763..491fec4f 100644 --- a/server/branch.ts +++ b/server/branch.ts @@ -225,6 +225,19 @@ export class BranchConfig { let questionBranches: QuestionBranches = JSON.parse(await readFileAsync(config.questionsLocation)); return questionBranches.map(branch => branch.name); } + public static async getCanonicalName(rawName: string): Promise { + const branchNames = await BranchConfig.getNames(); + try { + rawName = decodeURIComponent(rawName); + } + catch { + // DecodeURIComponent() will fail if branch name is already decoded and is supposed to have a % escape sequence in it + } + rawName = rawName.toLowerCase(); + return branchNames.find(name => { + return rawName === name.toLowerCase(); + }) || null; + } public static async loadAllBranches(type: keyof QuestionBranchTypes | "All" = "All", location: string = config.questionsLocation): Promise { let names = await this.getNames(); let branches: QuestionBranch[] = []; diff --git a/server/common.ts b/server/common.ts index 8525f5e7..bea12b5d 100644 --- a/server/common.ts +++ b/server/common.ts @@ -509,11 +509,12 @@ BranchConfig.verifyConfig().then(good => { import {ApplicationType} from "./middleware"; import * as moment from "moment-timezone"; -export async function isBranchOpen(branchName: string, user: IUser, requestType: ApplicationType) { - let branch = (await BranchConfig.loadAllBranches()).find(b => b.name.toLowerCase() === branchName.toLowerCase()) as (ApplicationBranch | ConfirmationBranch); - if (!branch) { +export async function isBranchOpen(rawBranchName: string, user: IUser, requestType: ApplicationType): Promise { + const branchName = await BranchConfig.getCanonicalName(rawBranchName); + if (!branchName) { return false; } + let branch = await BranchConfig.loadBranchFromDB(branchName) as (ApplicationBranch | ConfirmationBranch); let openDate = branch.open; let closeDate = branch.close; diff --git a/server/middleware.ts b/server/middleware.ts index 17df9af3..40b9ce9a 100644 --- a/server/middleware.ts +++ b/server/middleware.ts @@ -130,29 +130,30 @@ export enum ApplicationType { } export async function onlyAllowAnonymousBranch(request: express.Request, response: express.Response, next: express.NextFunction) { - let branchName = request.params.branch as string; - let questionBranches = (await BranchConfig.loadAllBranches()).filter(br => { - return br.name.toLowerCase() === branchName.toLowerCase(); - }); - if (questionBranches.length !== 1) { + const branchName = await BranchConfig.getCanonicalName(request.params.branch); + if (!branchName) { response.redirect("/"); return; } - - let branch = questionBranches[0] as ApplicationBranch; + const branch = await BranchConfig.loadBranchFromDB(branchName) as ApplicationBranch; if (!branch.allowAnonymous) { response.redirect("/"); return; } - next(); } export async function canUserModify(request: express.Request, response: express.Response, next: express.NextFunction) { - let user = await User.findOne({uuid: request.params.uuid}) as IUser; - let branchName = request.params.branch as string; - let questionBranch = (await BranchConfig.loadAllBranches()).find(branch => branch.name.toLowerCase() === branchName.toLowerCase()); + const user = await User.findOne({uuid: request.params.uuid}) as IUser; + const branchName = await BranchConfig.getCanonicalName(request.params.branch); + if (!branchName) { + response.status(400).json({ + "error": "Invalid application branch name" + }); + return; + } + let questionBranch = await BranchConfig.loadBranchFromDB(branchName); - if (!(await isBranchOpen(request.params.branch, user, questionBranch instanceof ApplicationBranch ? ApplicationType.Application : ApplicationType.Confirmation))) { + if (!(await isBranchOpen(branchName, user, questionBranch instanceof ApplicationBranch ? ApplicationType.Application : ApplicationType.Confirmation))) { response.status(400).json({ "error": "Branch is closed" }); @@ -209,8 +210,14 @@ export function branchRedirector(requestType: ApplicationType): (request: expres } if (request.params.branch) { - let branchName = (request.params.branch as string).toLowerCase(); - let branch = (await BranchConfig.loadBranchFromDB(request.params.branch as string)); + // Branch name from URL can be different from config in casing + let branchName = await BranchConfig.getCanonicalName(request.params.branch); + if (!branchName) { + // Invalid branch name + response.redirect("/"); + return; + } + let branch = await BranchConfig.loadBranchFromDB(branchName); if ((branch.type === "Application" && requestType !== ApplicationType.Application) || (branch.type === "Confirmation" && requestType !== ApplicationType.Confirmation)) { response.redirect("/"); return; @@ -229,7 +236,7 @@ export function branchRedirector(requestType: ApplicationType): (request: expres } } if (requestType === ApplicationType.Confirmation) { - if (request.params.branch !== user.confirmationBranch ) { + if (!user.accepted || branchName.toLowerCase() !== (user.confirmationBranch || "").toLowerCase()) { response.redirect("/"); return; } @@ -243,7 +250,7 @@ export function branchRedirector(requestType: ApplicationType): (request: expres } if (targetBranch) { - const uriBranch = encodeURIComponent(targetBranch); + const uriBranch = encodeURIComponent(targetBranch.toLowerCase()); const redirPath = requestType === ApplicationType.Application ? "apply" : "confirm"; response.redirect(`/${redirPath}/${uriBranch}`); return; From c472c7b8ed872e6b473a92b4c2f6fb5edfaf456a Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Tue, 5 Jun 2018 11:05:52 -0400 Subject: [PATCH 050/194] Use existence of confirmation branch for /confirm redirect --- server/middleware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/middleware.ts b/server/middleware.ts index 40b9ce9a..1cae679c 100644 --- a/server/middleware.ts +++ b/server/middleware.ts @@ -236,7 +236,7 @@ export function branchRedirector(requestType: ApplicationType): (request: expres } } if (requestType === ApplicationType.Confirmation) { - if (!user.accepted || branchName.toLowerCase() !== (user.confirmationBranch || "").toLowerCase()) { + if (!user.confirmationBranch || branchName.toLowerCase() !== user.confirmationBranch.toLowerCase()) { response.redirect("/"); return; } From f6a98962555b3c9ac54460d7d61c30655cd94951 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Tue, 5 Jun 2018 13:06:22 -0400 Subject: [PATCH 051/194] Fix user.confirmationDeadline checks failing because {} == true --- server/branch.ts | 2 +- server/common.ts | 5 ++++- server/routes/templates.ts | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/server/branch.ts b/server/branch.ts index 491fec4f..fb80c7da 100644 --- a/server/branch.ts +++ b/server/branch.ts @@ -307,7 +307,7 @@ export async function getOpenConfirmationBranches(user: IUser): Promise confirmTimes[name]); From 69e0fc4b2436674843548d6d9c07266c9360e67f Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Tue, 5 Jun 2018 15:08:38 -0400 Subject: [PATCH 052/194] Improve accepted language and related statistics in admin panel --- client/admin.html | 14 +++++++------- server/routes/templates.ts | 7 ++++--- server/schema.ts | 7 ++++--- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/client/admin.html b/client/admin.html index 8070ac6b..c9bad251 100644 --- a/client/admin.html +++ b/client/admin.html @@ -55,16 +55,16 @@

Statistics

{{numberFormat applicationStatistics.appliedUsers}} - Admitted users - {{numberFormat applicationStatistics.admittedUsers}} + Accepted users + {{numberFormat applicationStatistics.acceptedUsers}} Confirmed users - {{numberFormat applicationStatistics.attendingUsers}} + {{numberFormat applicationStatistics.confirmedUsers}} - Non-confirmed users - {{numberFormat applicationStatistics.declinedUsers}} + Non-confirmed accepted users + {{numberFormat applicationStatistics.nonConfirmedUsers}} Applications by branch {{#each applicationStatistics.applicationBranches}} @@ -77,7 +77,7 @@

Statistics

{{#each applicationStatistics.confirmationBranches}} {{this.name}} - {{numberFormat this.count}} + {{numberFormat this.confirmed}} of {{numberFormat this.count}} {{/each}} @@ -278,7 +278,7 @@

{{this.name}}

- +
diff --git a/server/routes/templates.ts b/server/routes/templates.ts index 51315a03..59778c5c 100644 --- a/server/routes/templates.ts +++ b/server/routes/templates.ts @@ -551,9 +551,9 @@ templateRoutes.route("/admin").get(authenticateWithRedirect, async (request, res applicationStatistics: { totalUsers: await User.find().count(), appliedUsers: await User.find({ "applied": true }).count(), - admittedUsers: await User.find({ "accepted": true }).count(), - attendingUsers: await User.find({ "confirmed": true }).count(), - declinedUsers: await User.find({ "accepted": true, "confirmed": false }).count(), + acceptedUsers: await User.find({ "accepted": true }).count(), + confirmedUsers: await User.find({ "accepted": true, "confirmed": true }).count(), + nonConfirmedUsers: await User.find({ "accepted": true, "confirmed": false }).count(), applicationBranches: await Promise.all(applicationBranches.map(async branch => { return { "name": branch.name, @@ -563,6 +563,7 @@ templateRoutes.route("/admin").get(authenticateWithRedirect, async (request, res confirmationBranches: await Promise.all(confirmationBranches.map(async branch => { return { "name": branch.name, + "confirmed": await User.find({ "confirmed": true, "confirmationBranch": branch.name }).count(), "count": await User.find({ "confirmationBranch": branch.name }).count() }; })) diff --git a/server/schema.ts b/server/schema.ts index 6f99bbc0..c9f9af11 100644 --- a/server/schema.ts +++ b/server/schema.ts @@ -337,15 +337,16 @@ export interface IAdminTemplate extends ICommonTemplate { applicationStatistics: { totalUsers: number; appliedUsers: number; - admittedUsers: number; - attendingUsers: number; - declinedUsers: number; + acceptedUsers: number; + confirmedUsers: number; + nonConfirmedUsers: number; applicationBranches: { name: string; count: number; }[]; confirmationBranches: { name: string; + confirmed: number; count: number; }[]; }; From 3ec3578eaccb2f635a4c8f159a49e74fc8d687d1 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Tue, 5 Jun 2018 15:23:41 -0400 Subject: [PATCH 053/194] Simplify some redundant function signatures --- server/middleware.ts | 2 +- server/routes/api/user.ts | 4 ++-- server/routes/templates.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/server/middleware.ts b/server/middleware.ts index 1cae679c..af463193 100644 --- a/server/middleware.ts +++ b/server/middleware.ts @@ -195,7 +195,7 @@ export async function canUserModify(request: express.Request, response: express. next(); } export function branchRedirector(requestType: ApplicationType): (request: express.Request, response: express.Response, next: express.NextFunction) => Promise { - return async (request: express.Request, response: express.Response, next: express.NextFunction) => { + return async (request, response, next) => { // TODO: fix branch names so they have a machine ID and human label let user = request.user as IUser; diff --git a/server/routes/api/user.ts b/server/routes/api/user.ts index 8bf2fd92..0c7124a0 100644 --- a/server/routes/api/user.ts +++ b/server/routes/api/user.ts @@ -67,8 +67,8 @@ userRoutes.route("/confirmation/:branch").post( canUserModify, deleteApplicationBranchHandler ); -function postApplicationBranchHandler(anonymous: boolean): express.Handler { - return async (request: express.Request, response: express.Response): Promise => { +function postApplicationBranchHandler(anonymous: boolean): (request: express.Request, response: express.Response) => Promise { + return async (request, response) => { let user: IUserMongoose; if (anonymous) { let email = request.body["anonymous-registration-email"] as string; diff --git a/server/routes/templates.ts b/server/routes/templates.ts index 59778c5c..a3539756 100644 --- a/server/routes/templates.ts +++ b/server/routes/templates.ts @@ -344,7 +344,7 @@ templateRoutes.route("/confirm").get( ); function applicationHandler(requestType: ApplicationType): (request: express.Request, response: express.Response) => Promise { - return async (request: express.Request, response: express.Response) => { + return async (request, response) => { let user = request.user as IUser; // TODO: integrate this logic with `middleware.branchRedirector` and `middleware.timeLimited` @@ -406,7 +406,7 @@ templateRoutes.route("/confirm/:branch").get( ); function applicationBranchHandler(requestType: ApplicationType, anonymous: boolean): (request: express.Request, response: express.Response) => Promise { - return async (request: express.Request, response: express.Response) => { + return async (request, response) => { let user: IUser; if (anonymous) { user = new User({ From 41f8138a1b594027e170ebced43586ac06f89d32 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Tue, 5 Jun 2018 16:45:29 -0400 Subject: [PATCH 054/194] Replace broken global variable hacks with data attributes Fixes missing space in formatted application submission success message and removes "We look forward to seeing you" because the message is also shown for rejected applicants. --- client/application.html | 14 +++++--------- client/confirmation.html | 5 +---- client/js/application.ts | 14 +++++++------- 3 files changed, 13 insertions(+), 20 deletions(-) diff --git a/client/application.html b/client/application.html index 70567a6f..6c3df225 100644 --- a/client/application.html +++ b/client/application.html @@ -15,7 +15,7 @@ - + {{#> sidebar}}

Apply: {{branch}}

@@ -24,11 +24,11 @@

Apply: {{branch}}

Running event.preventDefault() in the submit button's click handler also disables validation for some reason --> {{#unless unauthenticated}} -
+ {{else}} - - - + + + {{/unless}} {{#each questionData}} @@ -97,9 +97,5 @@

Apply: {{branch}}

{{/sidebar}} - diff --git a/client/confirmation.html b/client/confirmation.html index b28b7c6d..69d5e570 100644 --- a/client/confirmation.html +++ b/client/confirmation.html @@ -15,7 +15,7 @@ - + {{#> sidebar}}

RSVP: {{branch}}

@@ -85,8 +85,5 @@

RSVP: {{branch}}

{{/sidebar}} - diff --git a/client/js/application.ts b/client/js/application.ts index ebb650f3..eb9cb330 100644 --- a/client/js/application.ts +++ b/client/js/application.ts @@ -2,10 +2,9 @@ enum FormType { Application, Confirmation } -declare let formTypeString: keyof typeof FormType; -let formType = FormType[formTypeString]; - -let unauthenticated: (boolean | null) = null; +const formTypeString = document.body.dataset.formType as keyof typeof FormType; +const formType = FormType[formTypeString]; +const unauthenticated = document.body.dataset.unauthenticated === "true"; let form = document.querySelector("form") as HTMLFormElement | null; let submitButton = document.querySelector("form input[type=submit]") as HTMLInputElement; @@ -21,9 +20,10 @@ submitButton.addEventListener("click", e => { method: "POST", body: new FormData(form) }).then(checkStatus).then(parseJSON).then(async () => { - let successMessage: string = formType === FormType.Application ? - "Your application has been saved." + (!unauthenticated ? "Feel free to come back here and edit it at any time." : "") : - "Your RSVP has been saved. Feel free to come back here and edit it at any time. We look forward to seeing you!"; + let successMessage: string = formType === FormType.Application ? "Your application has been saved." : "Your RSVP has been saved."; + if (!unauthenticated) { + successMessage += " Feel free to come back here and edit it at any time."; + } await sweetAlert("Awesome!", successMessage, "success"); From 8bbaf5c391338bd410d72b56089c5474dfbbff9e Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Tue, 5 Jun 2018 17:27:11 -0400 Subject: [PATCH 055/194] Improvements to anonymous form submission - Scroll to top of page on successful submission - Properly slugify anonymous submission links in the admin panel - Set a title on branch names in the admin panel that might be truncated due to limited column width --- client/admin.html | 4 ++-- client/js/application.ts | 1 + client/js/team.ts | 6 +++--- server/routes/api/user.ts | 1 + server/routes/templates.ts | 1 - 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/client/admin.html b/client/admin.html index c9bad251..59b39179 100644 --- a/client/admin.html +++ b/client/admin.html @@ -228,7 +228,7 @@

Settings

{{#each settings.branches.application}}
-

{{this.name}}

+

{{this.name}}

+ + + +

+ Don't have an account? +
+ Connect with an external service or sign up. +

+
+

Sign Up

@@ -59,17 +40,25 @@

Sign Up

+

+ Already have a local account? Log in. +

- +
From 02420a78990c12e8d92a7b639f11c13d4120c43c Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Fri, 15 Jun 2018 16:45:17 -0400 Subject: [PATCH 066/194] Show/hide login methods depending on setting value --- client/css/login.css | 6 ++- client/login.html | 94 ++++++++++++++++++++++---------------- server/routes/templates.ts | 15 ++++-- server/schema.ts | 1 + 4 files changed, 73 insertions(+), 43 deletions(-) diff --git a/client/css/login.css b/client/css/login.css index 4294089b..d42d483e 100644 --- a/client/css/login.css +++ b/client/css/login.css @@ -18,6 +18,10 @@ a.facebook { color: white; background-color: #3b5998; } +a.gatech { + color: black; + background-color: #eeb211; +} #error, #success { text-align: center; margin-top: 1em; @@ -42,7 +46,7 @@ main { flex-direction: column-reverse; align-items: center; } -main > * { +main > div { box-sizing: border-box; width: 100%; display: none; diff --git a/client/login.html b/client/login.html index a5b246ba..1cc51a48 100644 --- a/client/login.html +++ b/client/login.html @@ -18,45 +18,61 @@

{{siteTitle}}

{{error}}
{{success}}
-
-

Log In

- Forgot your password? -
- - - -
-

- Don't have an account? -
- Connect with an external service or sign up. -

-
-
-

Sign Up

-
- - - - -
-

- Already have a local account? Log in. -

-
-
- - - Connect with GitHub - - - - Connect with Google - - + {{#ifIn "local" loginMethods}} +
+

Log In

+ Forgot your password? +
+ + + +
+

+ Don't have an account? +
+ Connect with an external service or sign up. +

+
+
+

Sign Up

+
+ + + + +
+

+ Already have a local account? Log in. +

+
+ {{/ifIn}} +
+ {{#ifIn "local" loginMethods}}{{else}} +

Log In

+ {{/ifIn}} + {{#ifIn "gatech" loginMethods}} + + Connect with Georgia Tech + + {{/ifIn}} + {{#ifIn "github" loginMethods}} + + + Connect with GitHub + + {{/ifIn}} + {{#ifIn "google" loginMethods}} + + + Connect with Google + + {{/ifIn}} + {{#ifIn "facebook" loginMethods}} + + {{/ifIn}}
diff --git a/server/routes/templates.ts b/server/routes/templates.ts index edee0323..e112eb7c 100644 --- a/server/routes/templates.ts +++ b/server/routes/templates.ts @@ -80,7 +80,8 @@ templateRoutes.use(async (request, response, next) => { }); // tslint:disable-next-line:no-any -Handlebars.registerHelper("ifCond", function(v1: any, v2: any, options: any) { +// tslint:disable:no-invalid-this +Handlebars.registerHelper("ifCond", (v1: any, v2: any, options: any) => { if (v1 === v2) { // tslint:disable-next-line:no-invalid-this return options.fn(this); @@ -88,6 +89,13 @@ Handlebars.registerHelper("ifCond", function(v1: any, v2: any, options: any) { // tslint:disable-next-line:no-invalid-this return options.inverse(this); }); +Handlebars.registerHelper("ifIn", (elem: T, list: T[], options: any) => { + if (list.includes(elem)) { + return options.fn(this); + } + return options.inverse(this); +}); +// tslint:enable:no-invalid-this Handlebars.registerHelper("required", (isRequired: boolean) => { // Adds the "required" form attribute if the element requests to be required return isRequired ? "required" : ""; @@ -260,11 +268,12 @@ templateRoutes.route("/").get(authenticateWithRedirect, async (request, response response.send(indexTemplate(templateData)); }); -templateRoutes.route("/login").get((request, response) => { +templateRoutes.route("/login").get(async (request, response) => { let templateData: ILoginTemplate = { siteTitle: config.eventName, error: request.flash("error"), - success: request.flash("success") + success: request.flash("success"), + loginMethods: await getSetting("loginMethods") }; response.send(loginTemplate(templateData)); }); diff --git a/server/schema.ts b/server/schema.ts index 6a4593e9..0a8f5321 100644 --- a/server/schema.ts +++ b/server/schema.ts @@ -311,6 +311,7 @@ export interface ILoginTemplate { siteTitle: string; error?: string; success?: string; + loginMethods?: string[]; } export interface IRegisterBranchChoiceTemplate extends ICommonTemplate { branches: string[]; From 6bb746722b3d3e423b09898055f786509aed9999 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Fri, 15 Jun 2018 17:52:16 -0400 Subject: [PATCH 067/194] Add admin setting for changing available login methods --- client/admin.html | 16 ++++++++++++++++ client/css/admin.css | 7 +++++-- client/js/admin.ts | 16 ++++++++++++++++ server/routes/api/settings.ts | 36 +++++++++++++++++++++++++++++++++++ server/routes/strategies.ts | 7 +++++++ server/routes/templates.ts | 18 +++++++++++++++--- server/schema.ts | 5 +++++ 7 files changed, 100 insertions(+), 5 deletions(-) diff --git a/client/admin.html b/client/admin.html index fe401117..67ee2eec 100644 --- a/client/admin.html +++ b/client/admin.html @@ -346,6 +346,22 @@
List of variables:
+
+
+

Change login methods

+
+
+
+ {{#each settings.loginMethodsInfo}} +
+
{{this.name}}
+ +
+ {{/each}} +

config.json options

diff --git a/client/css/admin.css b/client/css/admin.css index 0d93589d..a2ac36ea 100644 --- a/client/css/admin.css +++ b/client/css/admin.css @@ -137,10 +137,10 @@ canvas { } -.branch-role:nth-of-type(3n + 1) { +.branch-role:nth-of-type(3n + 1), .auth-method:nth-of-type(3n + 1) { margin-left: 0; } -.branch-role > h4 { +.branch-role > h4, .auth-method > h5 { height: 40px; white-space: nowrap; overflow: hidden; @@ -148,6 +148,9 @@ canvas { margin-top: 2rem; margin-bottom: 1rem; } +.auth-method > select { + width: 90%; +} .row-flex { display: flex; flex-wrap: wrap; diff --git a/client/js/admin.ts b/client/js/admin.ts index 9334b632..5195da8b 100644 --- a/client/js/admin.ts +++ b/client/js/admin.ts @@ -658,6 +658,17 @@ function settingsUpdate(e: MouseEvent) { let adminEmailData = new FormData(); adminEmailData.append("adminString", (document.getElementById("admin-emails") as HTMLInputElement).value); adminEmailData.append("addAdmins", (document.getElementById("add-admins") as HTMLInputElement).checked ? "true" : "false"); + + let loginMethodsData = new FormData(); + let loginMethods = document.querySelectorAll("div.auth-method") as NodeListOf; + let enabledMethods: string[] = []; + for (let i = 0; i < loginMethods.length; i++) { + if (loginMethods[i].querySelector("select")!.value === "enabled") { + enabledMethods.push(loginMethods[i].dataset.rawName!); + } + } + loginMethodsData.append("enabledMethods", JSON.stringify(enabledMethods)); + let branchRoleData = new FormData(); let branchRoles = document.querySelectorAll("div.branch-role") as NodeListOf; for (let i = 0; i < branchRoles.length; i++) { @@ -727,6 +738,11 @@ function settingsUpdate(e: MouseEvent) { ...defaultOptions, body: adminEmailData }); + }).then(checkStatus).then(parseJSON).then(() => { + return fetch("/api/settings/login_methods", { + ...defaultOptions, + body: loginMethodsData + }); }).then(checkStatus).then(parseJSON).then(() => { return fetch("/api/settings/branch_roles", { ...defaultOptions, diff --git a/server/routes/api/settings.ts b/server/routes/api/settings.ts index 01ed1844..8caf3ffe 100644 --- a/server/routes/api/settings.ts +++ b/server/routes/api/settings.ts @@ -108,6 +108,42 @@ settingsRoutes.route("/admin_emails") }); }); +settingsRoutes.route("/login_methods") + .get(async (request, response) => { + let methods = await getSetting("loginMethods"); + response.json({ + methods + }); + }) + .put(isAdmin, uploadHandler.any(), async (request, response) => { + let { enabledMethods } = request.body; + if (!enabledMethods) { + response.status(400).json({ + "error": "Missing value for enabled methods" + }); + return; + } + try { + let methods = JSON.parse(enabledMethods); + if (!Array.isArray(methods)) { + response.status(400).json({ + "error": "Invalid value for enabled methods" + }); + return; + } + await updateSetting("loginMethods", methods); + response.json({ + "success": true + }); + } + catch (err) { + console.error(err); + response.status(500).json({ + "error": "An error occurred while changing available login methods" + }); + } + }); + settingsRoutes.route("/branch_roles") .get(isAdmin, async (request, response) => { response.json({ diff --git a/server/routes/strategies.ts b/server/routes/strategies.ts index db90a629..f9dc937c 100644 --- a/server/routes/strategies.ts +++ b/server/routes/strategies.ts @@ -515,6 +515,13 @@ export const strategies = { "google": Google, "facebook": Facebook }; +export const prettyNames: Record = { + "local": "Local", + "gatech": "Georgia Tech CAS", + "github": "GitHub", + "google": "Google", + "facebook": "Facebook" +}; // Authentication helpers function getExternalPort(request: Request): number { diff --git a/server/routes/templates.ts b/server/routes/templates.ts index e112eb7c..9f98b148 100644 --- a/server/routes/templates.ts +++ b/server/routes/templates.ts @@ -22,6 +22,7 @@ import { IFormItem } from "../schema"; import * as Branches from "../branch"; +import { strategies, prettyNames } from "../routes/strategies"; export let templateRoutes = express.Router(); @@ -540,7 +541,17 @@ templateRoutes.route("/admin").get(authenticateWithRedirect, async (request, res let teamsEnabled = await getSetting("teamsEnabled"); let qrEnabled = await getSetting("qrEnabled"); - let adminEmails = await User.find({admin: true}).select('email'); + type StrategyNames = keyof typeof strategies; + let enabledMethods = await getSetting("loginMethods"); + let loginMethodsInfo = Object.keys(strategies).map((name: StrategyNames) => { + return { + name: prettyNames[name], + raw: name, + enabled: enabledMethods.includes(name) + }; + }); + + let adminEmails = await User.find({ admin: true }).select("email"); let noopBranches = (await Branches.BranchConfig.loadAllBranches("Noop")) as Branches.NoopBranch[]; let applicationBranches = (await Branches.BranchConfig.loadAllBranches("Application")) as Branches.ApplicationBranch[]; @@ -581,7 +592,6 @@ templateRoutes.route("/admin").get(authenticateWithRedirect, async (request, res teamsEnabled, teamsEnabledChecked: teamsEnabled ? "checked" : "", qrEnabled, - adminEmails, qrEnabledChecked: qrEnabled ? "checked" : "", branches: { noop: noopBranches.map(branch => { @@ -606,7 +616,9 @@ templateRoutes.route("/admin").get(authenticateWithRedirect, async (request, res isAcceptance: branch.isAcceptance }; }) - } + }, + loginMethodsInfo, + adminEmails }, config: { admins: config.admins.join(", "), diff --git a/server/schema.ts b/server/schema.ts index 0a8f5321..6f98a4df 100644 --- a/server/schema.ts +++ b/server/schema.ts @@ -369,6 +369,11 @@ export interface IAdminTemplate extends ICommonTemplate { close: string; }[]; }; + loginMethodsInfo: { + name: string; + raw: string; + enabled: boolean; + }[]; adminEmails: IUserMongoose[]; }; config: { From 41309ff164196893936312fdbf537f13f42872a8 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Fri, 15 Jun 2018 18:07:28 -0400 Subject: [PATCH 068/194] Reload authentication on login settings change so a restart isn't needed --- server/routes/api/settings.ts | 1 + server/routes/auth.ts | 38 +++++++++++++++++++++-------------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/server/routes/api/settings.ts b/server/routes/api/settings.ts index 8caf3ffe..3f3c8277 100644 --- a/server/routes/api/settings.ts +++ b/server/routes/api/settings.ts @@ -132,6 +132,7 @@ settingsRoutes.route("/login_methods") return; } await updateSetting("loginMethods", methods); + await (await import("../auth")).reloadAuthentication(); response.json({ "success": true }); diff --git a/server/routes/auth.ts b/server/routes/auth.ts index 83cbaebf..892bee5b 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -46,10 +46,16 @@ passport.deserializeUser((id, done) => { }); }); -export let authRoutes = express.Router(); +let router = express.Router(); +export let authRoutes: express.RequestHandler = (request, response, next) => { + // Allows for dynamic dispatch when authentication gets reloaded + router(request, response, next); +}; -let authenticationMethods: RegistrationStrategy[] = []; -getSetting<(keyof typeof strategies)[]>("loginMethods").then(methods => { +export async function reloadAuthentication() { + router = express.Router(); + let authenticationMethods: RegistrationStrategy[] = []; + let methods = await getSetting<(keyof typeof strategies)[]>("loginMethods"); console.info(`Using authentication methods: ${methods.join(", ")}`); for (let methodName of methods) { if (!strategies[methodName]) { @@ -58,21 +64,23 @@ getSetting<(keyof typeof strategies)[]>("loginMethods").then(methods => { } let method = new strategies[methodName](); authenticationMethods.push(method); - method.use(authRoutes); + method.use(router); } -}).catch(err => { + + // Need to be redefined on every instance of a new router + router.get("/validatehost/:nonce", (request, response) => { + let nonce: string = request.params.nonce || ""; + response.send(crypto.createHmac("sha256", config.secrets.session).update(nonce).digest().toString("hex")); + }); + + router.all("/logout", (request, response) => { + request.logout(); + response.redirect("/login"); + }); +} +reloadAuthentication().catch(err => { throw err; }); app.use(passport.initialize()); app.use(passport.session()); - -authRoutes.get("/validatehost/:nonce", (request, response) => { - let nonce: string = request.params.nonce || ""; - response.send(crypto.createHmac("sha256", config.secrets.session).update(nonce).digest().toString("hex")); -}); - -authRoutes.all("/logout", (request, response) => { - request.logout(); - response.redirect("/login"); -}); From e8ff555910dbf009064f1954ee614035833d8727 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Mon, 18 Jun 2018 13:04:27 -0400 Subject: [PATCH 069/194] Add a confirm new account page after first login for editing name/email --- client/css/login.css | 8 +++ client/postlogin.html | 42 ++++++++++++++ server/routes/api/graphql.ts | 15 ++--- server/routes/auth.ts | 70 ++++++++++++++++++++++- server/routes/strategies.ts | 107 ++++++++++++++++++----------------- server/routes/templates.ts | 35 +++++++++++- server/schema.ts | 30 +++------- 7 files changed, 218 insertions(+), 89 deletions(-) create mode 100644 client/postlogin.html diff --git a/client/css/login.css b/client/css/login.css index d42d483e..0bcdd71b 100644 --- a/client/css/login.css +++ b/client/css/login.css @@ -76,3 +76,11 @@ main > .active { padding-left: 10px; } } + +.unit { + display: flex; + align-items: center; +} +.unit > *:first-child { + width: 125px; +} diff --git a/client/postlogin.html b/client/postlogin.html new file mode 100644 index 00000000..eecd7dc7 --- /dev/null +++ b/client/postlogin.html @@ -0,0 +1,42 @@ + + + + New Account - {{siteTitle}} + + + + + + + + +
+

{{siteTitle}}

+ +
{{error}}
+
{{success}}
+
+
+

New Account

+

Please confirm a few things for us.

+

If you already have an account, please log in with the same authentication method you used originally.

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+ + diff --git a/server/routes/api/graphql.ts b/server/routes/api/graphql.ts index 7c2b2a57..0b07823f 100644 --- a/server/routes/api/graphql.ts +++ b/server/routes/api/graphql.ts @@ -10,6 +10,7 @@ import { User, IUser, Team, IFormItem, QuestionBranchConfig } from "../../schema import { Branches, Tags, AllTags, BranchConfig, ApplicationBranch, ConfirmationBranch, NoopBranch } from "../../branch"; import { schema as types } from "./api.graphql.types"; import { formatSize } from "../../common"; +import { prettyNames as strategyNames } from "../strategies"; const typeDefs = fs.readFileSync(path.resolve(__dirname, "../../../api.graphql"), "utf8"); @@ -291,18 +292,12 @@ async function userRecordToGraphql(user: IUser): Promise> { } : undefined; let loginMethods: string[] = []; - if (user.github && user.github.id) { - loginMethods.push("GitHub"); - } - if (user.google && user.google.id) { - loginMethods.push("Google"); - } - if (user.facebook && user.facebook.id) { - loginMethods.push("Facebook"); - } - if (user.local && user.local.hash) { + if (user.local && user.local!.hash) { loginMethods.push("Local"); } + for (let service of Object.keys(user.services) as (keyof typeof user.services)[]) { + loginMethods.push(strategyNames[service]); + } let team = user.teamId ? await Team.findById(user.teamId) : null; diff --git a/server/routes/auth.ts b/server/routes/auth.ts index 892bee5b..45fb50ab 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -1,4 +1,5 @@ import * as crypto from "crypto"; +import * as path from "path"; import * as express from "express"; import * as session from "express-session"; import * as connectMongo from "connect-mongo"; @@ -9,10 +10,11 @@ import { config, mongoose, COOKIE_OPTIONS, getSetting } from "../common"; import { - IUser, User + IUser, User, IUserMongoose } from "../schema"; +import { postParser } from "../middleware"; import { - RegistrationStrategy, strategies + RegistrationStrategy, strategies, validateAndCacheHostName, sendVerificationEmail } from "./strategies"; // Passport authentication @@ -67,7 +69,54 @@ export async function reloadAuthentication() { method.use(router); } - // Need to be redefined on every instance of a new router + // These routes need to be redefined on every instance of a new router + router.post("/confirm", validateAndCacheHostName, postParser, async (request, response) => { + let user = request.user as IUserMongoose; + let name = request.body.name as string; + if (!name || !name.trim()) { + request.flash("error", "Invalid name"); + response.redirect("/login/confirm"); + return; + } + user.name = name.trim(); + + let email = request.body.email as string | undefined; + if (email && email !== user.email) { + if (!email.trim()) { + request.flash("error", "Invalid email"); + response.redirect("/login/confirm"); + return; + } + user.admin = false; + user.verifiedEmail = false; + user.email = email; + if (config.admins.includes(email)) { + user.admin = true; + console.info(`Adding new admin: ${email}`); + } + } + user.accountConfirmed = true; + + try { + await user.save(); + if (email) { + await sendVerificationEmail(request, user); + } + if (!user.verifiedEmail) { + request.logout(); + request.flash("success", "Account created successfully. Please verify your email before logging in."); + response.redirect("/login"); + return; + } + response.redirect("/"); + } + catch (err) { + console.error(err); + request.flash("error", "An error occurred while creating your account"); + response.redirect("/login/confirm"); + } + }); + router.get("/validatehost/:nonce", (request, response) => { let nonce: string = request.params.nonce || ""; response.send(crypto.createHmac("sha256", config.secrets.session).update(nonce).digest().toString("hex")); @@ -84,3 +133,18 @@ reloadAuthentication().catch(err => { app.use(passport.initialize()); app.use(passport.session()); + +app.use((request, response, next) => { + // Only block requests for GET requests to non-auth pages + if (path.extname(request.url) !== "" || request.method !== "GET" || request.originalUrl.match(/^\/auth/)) { + next(); + return; + } + let user = request.user as IUser; + if (user && !user.accountConfirmed && request.originalUrl !== "/login/confirm") { + response.redirect("/login/confirm"); + } + else { + next(); + } +}); diff --git a/server/routes/strategies.ts b/server/routes/strategies.ts index f9dc937c..975c52ca 100644 --- a/server/routes/strategies.ts +++ b/server/routes/strategies.ts @@ -9,7 +9,7 @@ import * as uuid from "uuid/v4"; import { config, pbkdf2Async, renderEmailHTML, renderEmailText, sendMailAsync } from "../common"; import { postParser, trackEvent } from "../middleware"; -import { IConfig, IUserMongoose, User } from "../schema"; +import { IConfig, IUser, IUserMongoose, User } from "../schema"; import { Request, Response, NextFunction, Router } from "express"; import {Strategy as LocalStrategy} from "passport-local"; @@ -69,15 +69,13 @@ export interface RegistrationStrategy { abstract class OAuthStrategy implements RegistrationStrategy { public readonly passportStrategy: Strategy; - public static get defaultUserProperties() { + public static get defaultUserProperties(): Partial { return { "uuid": uuid(), "verifiedEmail": false, + "accountConfirmed": false, - "local": {}, - "github": {}, - "google": {}, - "facebook": {}, + "services": {}, "applied": false, "accepted": false, @@ -117,10 +115,7 @@ abstract class OAuthStrategy implements RegistrationStrategy { done(null, false, { message: "Your GitHub profile does not have any public email addresses. Please make an email address public before logging in with GitHub." }); return; } - else if (!profile.displayName || !profile.displayName.trim()) { - done(null, false, { message: "Your profile does not have a publicly visible name. Please set a name on your account before logging in." }); - return; - } + let user = await User.findOne({"email": email}); let isAdmin = false; if (config.admins.includes(email)) { @@ -133,16 +128,20 @@ abstract class OAuthStrategy implements RegistrationStrategy { user = new User({ ...OAuthStrategy.defaultUserProperties, email, - name: profile.displayName, + name: profile.displayName ? profile.displayName.trim() : "", verifiedEmail: true, admin: isAdmin }); - user[serviceName]!.id = profile.id; - if (serviceName === "github" && profile.username && profile.profileUrl) { - user[serviceName]!.username = profile.username; - user[serviceName]!.profileUrl = profile.profileUrl; - } + if (!user.services) { + user.services = {}; + } + user.services[serviceName] = { + id: profile.id, + username: profile.username, + profileUrl: profile.profileUrl + }; try { + user.markModified("services"); await user.save(); trackEvent("created account (auth)", request, email); } @@ -154,22 +153,24 @@ abstract class OAuthStrategy implements RegistrationStrategy { done(null, user); } else { - if (!user[serviceName] || !user[serviceName]!.id) { - user[serviceName] = { - id: profile.id + if (!user.services) { + user.services = {}; + } + if (!user.services[serviceName]) { + user.services[serviceName] = { + id: profile.id, + username: profile.username, + profileUrl: profile.profileUrl }; } if (!user.verifiedEmail) { // We trust our OAuth provider to have verified the user's email for us user.verifiedEmail = true; } - if (serviceName === "github" && (!user.github || !user.github.username || !user.github.profileUrl) && (profile.username && profile.profileUrl)) { - user.github!.username = profile.username; - user.github!.profileUrl = profile.profileUrl; - } if (!user.admin && isAdmin) { user.admin = true; } + user.markModified("services"); await user.save(); done(null, user); } @@ -305,8 +306,7 @@ export class Local implements RegistrationStrategy { verifiedEmail: false, local: { "hash": hash.toString("hex"), - "salt": salt.toString("hex"), - "verificationCode": crypto.randomBytes(32).toString("hex") + "salt": salt.toString("hex") } }); try { @@ -317,34 +317,11 @@ export class Local implements RegistrationStrategy { done(err); return; } - // Send verification email (hostname validated by previous middleware) - let link = createLink(request, `/auth/verify/${user.local!.verificationCode}`); - let markdown = -`Hi {{name}}, - -Thanks for signing up for ${config.eventName}! To verify your email, please [click here](${link}). - -Sincerely, - -The ${config.eventName} Team.`; - try { - await sendMailAsync({ - from: config.email.from, - to: email, - subject: `[${config.eventName}] - Verify your email`, - html: await renderEmailHTML(markdown, user), - text: await renderEmailText(markdown, user) - }); - } - catch (err) { - done(err); - return; - } done(null, user); } else { // Log the user in - let hash = await pbkdf2Async(password, Buffer.from(user.local.salt, "hex"), PBKDF2_ROUNDS); + let hash = await pbkdf2Async(password, Buffer.from(user.local.salt || "", "hex"), PBKDF2_ROUNDS); if (hash.toString("hex") === user.local.hash) { if (user.verifiedEmail) { done(null, user); @@ -364,9 +341,7 @@ The ${config.eventName} Team.`; authRoutes.post("/signup", validateAndCacheHostName, postParser, passport.authenticate("local", { failureRedirect: "/login", failureFlash: true }), (request, response) => { // User is logged in automatically by Passport but we want them to verify their email first - request.logout(); - request.flash("success", "Account created successfully. Please verify your email before logging in."); - response.redirect("/login"); + response.redirect("/login/confirm"); }); authRoutes.post("/login", postParser, passport.authenticate("local", { failureRedirect: "/login", failureFlash: true, successRedirect: "/" })); @@ -547,7 +522,7 @@ function getExternalPort(request: Request): number { } let validatedHostNames: string[] = []; -function validateAndCacheHostName(request: Request, response: Response, next: NextFunction) { +export function validateAndCacheHostName(request: Request, response: Response, next: NextFunction) { // Basically checks to see if the server behind the hostname has the same session key by HMACing a random nonce if (validatedHostNames.find(hostname => hostname === request.hostname)) { next(); @@ -597,3 +572,29 @@ function createLink(request: Request, link: string): string { return `http${request.secure ? "s" : ""}://${request.hostname}:${getExternalPort(request)}/${link}`; } } + +export async function sendVerificationEmail(request: Request, user: IUserMongoose) { + // Send verification email (hostname validated by previous middleware) + if (!user.local) { + user.local = {}; + } + user.local.verificationCode = crypto.randomBytes(32).toString("hex"); + await user.save(); + + let link = createLink(request, `/auth/verify/${user.local.verificationCode}`); + let markdown = +`Hi {{name}}, + +Thanks for signing up for ${config.eventName}! To verify your email, please [click here](${link}). + +Sincerely, + +The ${config.eventName} Team.`; + await sendMailAsync({ + from: config.email.from, + to: user.email, + subject: `[${config.eventName}] - Verify your email`, + html: await renderEmailHTML(markdown, user), + text: await renderEmailText(markdown, user) + }); +} diff --git a/server/routes/templates.ts b/server/routes/templates.ts index 9f98b148..0f192e08 100644 --- a/server/routes/templates.ts +++ b/server/routes/templates.ts @@ -30,6 +30,7 @@ export let templateRoutes = express.Router(); let [ indexTemplate, loginTemplate, + postLoginTemplate, forgotPasswordTemplate, resetPasswordTemplate, preregisterTemplate, @@ -42,6 +43,7 @@ let [ ] = [ "index.html", "login.html", + "postlogin.html", "forgotpassword.html", "resetpassword.html", "preapplication.html", @@ -278,6 +280,35 @@ templateRoutes.route("/login").get(async (request, response) => { }; response.send(loginTemplate(templateData)); }); +templateRoutes.route("/login/confirm").get((request, response) => { + let user = request.user as IUser; + if (!user) { + response.redirect("/login"); + return; + } + if (user.accountConfirmed) { + response.redirect("/"); + } + + let loginMethods: string[] = []; + if (user.local && user.local!.hash) { + loginMethods.push("Local"); + } + for (let service of Object.keys(user.services || {}) as (keyof typeof user.services)[]) { + loginMethods.push(prettyNames[service]); + } + + response.send(postLoginTemplate({ + siteTitle: config.eventName, + error: request.flash("error"), + success: request.flash("success"), + + name: user.name || "", + email: user.email || "", + verifiedEmail: user.verifiedEmail || false, + loginMethods: loginMethods.join(", ") + })); +}); templateRoutes.route("/login/forgot").get((request, response) => { let templateData: ILoginTemplate = { siteTitle: config.eventName, @@ -286,14 +317,14 @@ templateRoutes.route("/login/forgot").get((request, response) => { }; response.send(forgotPasswordTemplate(templateData)); }); -templateRoutes.get("/auth/forgot/:code", async (request, response) => { +templateRoutes.route("/auth/forgot/:code").get(async (request, response) => { let user = await User.findOne({ "local.resetCode": request.params.code }); if (!user) { request.flash("error", "Invalid password reset code"); response.redirect("/login"); return; } - else if (!user.local!.resetRequested || Date.now() - user.local!.resetRequestedTime.valueOf() > 1000 * 60 * 60) { + else if (!user.local!.resetRequested || Date.now() - user.local!.resetRequestedTime!.valueOf() > 1000 * 60 * 60) { request.flash("error", "Your password reset link has expired. Please request a new one."); user.local!.resetCode = ""; user.local!.resetRequested = false; diff --git a/server/schema.ts b/server/schema.ts index 6f98a4df..8c3e2211 100644 --- a/server/schema.ts +++ b/server/schema.ts @@ -89,6 +89,7 @@ export interface IUser { email: string; name: string; verifiedEmail: boolean; + accountConfirmed: boolean; local?: { hash: string; @@ -98,16 +99,12 @@ export interface IUser { resetCode: string; resetRequestedTime: Date; }; - github?: { - id: string; - username: string; - profileUrl: string; - }; - google?: { - id: string; - }; - facebook?: { - id: string; + services: { + [Service in Exclude]?: { + id: string; + username?: string; + profileUrl?: string; + }; }; applied: boolean; @@ -155,6 +152,7 @@ export const User = mongoose.model("User", new mongoose.Schema({ index: true }, verifiedEmail: Boolean, + accountConfirmed: Boolean, local: { hash: String, @@ -164,17 +162,7 @@ export const User = mongoose.model("User", new mongoose.Schema({ resetCode: String, resetRequestedTime: Date }, - github: { - id: String, - username: String, - profileUrl: String - }, - google: { - id: String - }, - facebook: { - id: String - }, + services: mongoose.Schema.Types.Mixed, teamId: { type: mongoose.Schema.Types.ObjectId From f55f39b3695e7e0d866e2829c6b6d257f0ed8d58 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Mon, 18 Jun 2018 16:47:25 -0400 Subject: [PATCH 070/194] Better error for existing email address when confirming account --- server/routes/auth.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/routes/auth.ts b/server/routes/auth.ts index 45fb50ab..8f66267d 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -87,6 +87,11 @@ export async function reloadAuthentication() { response.redirect("/login/confirm"); return; } + if (await User.count({ email }) > 0) { + request.flash("error", "That email address is already in use. You may already have an account from another login service."); + response.redirect("/login/confirm"); + return; + } user.admin = false; user.verifiedEmail = false; user.email = email; From 423e23997b993a96ae0f20de7f1b0e04ac0b38d1 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Mon, 18 Jun 2018 16:55:59 -0400 Subject: [PATCH 071/194] Implement CAS login as a hybrid of OAuth and local --- package-lock.json | 5 ++- package.json | 2 +- server/middleware.ts | 8 ++--- server/routes/strategies.ts | 71 +++++++++++++++++++++++++++++++++---- server/schema.ts | 4 +-- 5 files changed, 73 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 94fb2c6c..c66c5772 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2852,9 +2852,8 @@ } }, "passport-cas2": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/passport-cas2/-/passport-cas2-0.0.9.tgz", - "integrity": "sha1-kWRu0gvrYFyGPOZE0VuILQyE6mU=", + "version": "github:petschekr/passport-cas#3d026507a1c7949d25ce720886a127acb024744b", + "from": "github:petschekr/passport-cas", "requires": { "cas": "git://github.com/joshchan/node-cas.git#344a8bfba9d054e2e378adaf95b720c898ae48a2", "passport-strategy": "^1.0.0" diff --git a/package.json b/package.json index 3cd15c28..325a5042 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "morgan": "^1.8.2", "multer": "^1.3.0", "passport": "^0.3.2", - "passport-cas2": "0.0.9", + "passport-cas2": "petschekr/passport-cas", "passport-facebook": "^2.1.1", "passport-github2": "^0.1.10", "passport-google-oauth20": "^1.0.0", diff --git a/server/middleware.ts b/server/middleware.ts index af463193..06312cf8 100644 --- a/server/middleware.ts +++ b/server/middleware.ts @@ -60,7 +60,7 @@ export function isUserOrAdmin(request: express.Request, response: express.Respon "error": "You must log in to access this endpoint" }); } - else if (user.uuid !== request.params.uuid && !user.admin) { + else if ((user.uuid !== request.params.uuid && !user.admin) || !user.verifiedEmail || !user.accountConfirmed) { response.status(403).json({ "error": "You are not permitted to access this endpoint" }); @@ -91,7 +91,7 @@ export function isAdmin(request: express.Request, response: express.Response, ne "error": "You must log in to access this endpoint" }); } - else if (!user.admin) { + else if (!user.admin || !user.verifiedEmail || !user.accountConfirmed) { response.status(403).json({ "error": "You are not permitted to access this endpoint" }); @@ -104,7 +104,7 @@ export function isAdmin(request: express.Request, response: express.Response, ne // For API endpoints export function authenticateWithReject(request: express.Request, response: express.Response, next: express.NextFunction) { response.setHeader("Cache-Control", "no-cache"); - if (!request.isAuthenticated()) { + if (!request.isAuthenticated() || !request.user || !request.user.verifiedEmail || !request.user.accountConfirmed) { response.status(401).json({ "error": "You must log in to access this endpoint" }); @@ -117,7 +117,7 @@ export function authenticateWithReject(request: express.Request, response: expre // For directly user facing endpoints export function authenticateWithRedirect(request: express.Request, response: express.Response, next: express.NextFunction) { response.setHeader("Cache-Control", "private"); - if (!request.isAuthenticated()) { + if (!request.isAuthenticated() || !request.user || !request.user.verifiedEmail || !request.user.accountConfirmed) { response.redirect("/login"); } else { diff --git a/server/routes/strategies.ts b/server/routes/strategies.ts index 975c52ca..156af24f 100644 --- a/server/routes/strategies.ts +++ b/server/routes/strategies.ts @@ -101,7 +101,7 @@ abstract class OAuthStrategy implements RegistrationStrategy { profileFields, passReqToCallback: true }; - this.passportStrategy = new strategy(options, this.passportCallback); + this.passportStrategy = new strategy(options, this.passportCallback.bind(this)); } protected async passportCallback(request: Request, accessToken: string, refreshToken: string, profile: Profile, done: PassportDone) { @@ -232,15 +232,72 @@ export class Facebook extends OAuthStrategy { abstract class CASStrategy implements RegistrationStrategy { public readonly passportStrategy: Strategy; - constructor(public readonly name: IConfig.CASServices, url: string) { + constructor(public readonly name: IConfig.CASServices, url: string, private readonly emailDomain: string) { this.passportStrategy = new CASStrategyProvider({ casURL: url, passReqToCallback: true - }, this.passportCallback); + }, this.passportCallback.bind(this)); } private async passportCallback(request: Request, username: string, profile: Profile, done: PassportDone) { - done(null, false, {message: "Not supported"}); + let user = await User.findOne({[`services.${this.name}.id`]: username}); + let email = `${username}@${this.emailDomain}`; + let isAdmin = false; + + if (config.admins.includes(email)) { + isAdmin = true; + if (!user || !user.admin) { + console.info(`Adding new admin: ${email}`); + } + } + if (!user) { + user = new User({ + ...OAuthStrategy.defaultUserProperties, + email, + name: "", + verifiedEmail: false, + admin: isAdmin + }); + if (!user.services) { + user.services = {}; + } + user.services[this.name] = { + id: username, + username + }; + try { + user.markModified("services"); + await user.save(); + trackEvent("created account (auth)", request, email); + } + catch (err) { + done(err); + return; + } + + done(null, user); + } + else { + if (!user.services) { + user.services = {}; + } + if (!user.services[this.name]) { + user.services[this.name] = { + id: username, + username + }; + } + if (!user.admin && isAdmin && user.email === email && user.verifiedEmail) { + user.admin = true; + } + user.markModified("services"); + await user.save(); + if (!user.verifiedEmail && user.accountConfirmed) { + done(null, false, { "message": "You must verify your email before you can sign in" }); + return; + } + done(null, user); + } } public use(authRoutes: Router) { @@ -259,7 +316,7 @@ abstract class CASStrategy implements RegistrationStrategy { export class GeorgiaTechCAS extends CASStrategy { constructor() { // Registration must be hosted on a *.hack.gt domain for this to work - super("gatech", "https://login.gatech.edu/cas"); + super("gatech", "https://login.gatech.edu/cas", "gatech.edu"); } } @@ -273,13 +330,13 @@ export class Local implements RegistrationStrategy { passwordField: "password", passReqToCallback: true }; - this.passportStrategy = new LocalStrategy(options, this.passportCallback); + this.passportStrategy = new LocalStrategy(options, this.passportCallback.bind(this)); } protected async passportCallback(request: Request, email: string, password: string, done: PassportDone) { let user = await User.findOne({ email }); if (user && request.path.match(/\/signup$/i)) { - done(null, false, { "message": "That email address is already in use" }); + done(null, false, { "message": "That email address is already in use. You may already have an account from another login service." }); } else if (user && !user.local!.hash) { done(null, false, { "message": "Please log back in with an external provider" }); diff --git a/server/schema.ts b/server/schema.ts index 8c3e2211..48953bf2 100644 --- a/server/schema.ts +++ b/server/schema.ts @@ -91,14 +91,14 @@ export interface IUser { verifiedEmail: boolean; accountConfirmed: boolean; - local?: { + local?: Partial<{ hash: string; salt: string; verificationCode: string; resetRequested: boolean; resetCode: string; resetRequestedTime: Date; - }; + }>; services: { [Service in Exclude]?: { id: string; From f51c4d06b2546135dcf596d6f769d63d299a2442 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Mon, 18 Jun 2018 17:27:29 -0400 Subject: [PATCH 072/194] Fix mobile styles for new account page --- client/css/login.css | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/client/css/login.css b/client/css/login.css index 0bcdd71b..6cba4cd3 100644 --- a/client/css/login.css +++ b/client/css/login.css @@ -60,6 +60,15 @@ main > .active { flex-direction: column; } +.unit { + display: flex; + align-items: center; + flex-direction: column; +} +.unit > *:first-child { + width: 125px; +} + /* Desktop styles */ @media only screen and (min-width: 700px) { main { @@ -75,12 +84,7 @@ main > .active { main > .active:last-of-type { padding-left: 10px; } -} - -.unit { - display: flex; - align-items: center; -} -.unit > *:first-child { - width: 125px; + .unit { + flex-direction: row; + } } From bcf488e83b52ab8fe733b7418f2b32de571174ee Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Tue, 19 Jun 2018 14:20:16 -0400 Subject: [PATCH 073/194] Add ability to add other login methods to newly created accounts --- client/css/login.css | 10 +++++++- client/login.html | 24 +------------------ client/partials/login-methods.html | 23 ++++++++++++++++++ client/postlogin.html | 38 +++++++++++++++++------------- server/routes/strategies.ts | 34 +++++++++++++++++++++++--- server/routes/templates.ts | 26 +++++++++++++------- 6 files changed, 104 insertions(+), 51 deletions(-) create mode 100644 client/partials/login-methods.html diff --git a/client/css/login.css b/client/css/login.css index 6cba4cd3..602d5f0a 100644 --- a/client/css/login.css +++ b/client/css/login.css @@ -68,6 +68,14 @@ main > .active { .unit > *:first-child { width: 125px; } +#additional-logins { + display: flex; + flex-direction: column; + flex-wrap: wrap; + justify-content: center; + margin-bottom: 10px; + font-size: 85%; +} /* Desktop styles */ @media only screen and (min-width: 700px) { @@ -84,7 +92,7 @@ main > .active { main > .active:last-of-type { padding-left: 10px; } - .unit { + .unit, #additional-logins { flex-direction: row; } } diff --git a/client/login.html b/client/login.html index 1cc51a48..232d75dd 100644 --- a/client/login.html +++ b/client/login.html @@ -50,29 +50,7 @@

Sign Up

{{#ifIn "local" loginMethods}}{{else}}

Log In

{{/ifIn}} - {{#ifIn "gatech" loginMethods}} - - Connect with Georgia Tech - - {{/ifIn}} - {{#ifIn "github" loginMethods}} - - - Connect with GitHub - - {{/ifIn}} - {{#ifIn "google" loginMethods}} - - - Connect with Google - - {{/ifIn}} - {{#ifIn "facebook" loginMethods}} - - {{/ifIn}} + {{> login-methods}}
diff --git a/client/partials/login-methods.html b/client/partials/login-methods.html new file mode 100644 index 00000000..34e993f1 --- /dev/null +++ b/client/partials/login-methods.html @@ -0,0 +1,23 @@ +{{#ifIn "gatech" loginMethods}} + + Connect with Georgia Tech + +{{/ifIn}} +{{#ifIn "github" loginMethods}} + + + Connect with GitHub + +{{/ifIn}} +{{#ifIn "google" loginMethods}} + + + Connect with Google + +{{/ifIn}} +{{#ifIn "facebook" loginMethods}} + +{{/ifIn}} diff --git a/client/postlogin.html b/client/postlogin.html index eecd7dc7..b4685376 100644 --- a/client/postlogin.html +++ b/client/postlogin.html @@ -8,6 +8,7 @@ +
@@ -17,22 +18,27 @@

{{siteTitle}}

{{success}}
-

New Account

-

Please confirm a few things for us.

-

If you already have an account, please log in with the same authentication method you used originally.

-
-
- - -
-
- - -
-
- - -
+

New Account

+ {{#if canAddLogins}} +

Connect your account with another provider or an existing account:

+
+ {{> login-methods}} +
+ {{/if}} +

Please confirm a few things for us:

+ +
+ + +
+
+ + +
+
+ + +
diff --git a/server/routes/strategies.ts b/server/routes/strategies.ts index 156af24f..c8ce10c4 100644 --- a/server/routes/strategies.ts +++ b/server/routes/strategies.ts @@ -116,7 +116,8 @@ abstract class OAuthStrategy implements RegistrationStrategy { return; } - let user = await User.findOne({"email": email}); + let user = await User.findOne({ email }); + let loggedInUser = request.user as IUserMongoose | undefined; let isAdmin = false; if (config.admins.includes(email)) { isAdmin = true; @@ -124,7 +125,7 @@ abstract class OAuthStrategy implements RegistrationStrategy { console.info(`Adding new admin: ${email}`); } } - if (!user) { + if (!user && !loggedInUser) { user = new User({ ...OAuthStrategy.defaultUserProperties, email, @@ -153,6 +154,19 @@ abstract class OAuthStrategy implements RegistrationStrategy { done(null, user); } else { + if (user && loggedInUser) { + // Remove extra account represented by loggedInUser and merge into user + User.remove({ "uuid": loggedInUser.uuid }); + } + else if (!user && loggedInUser) { + // Attach service info to logged in user instead of non-existant user pulled via email address + user = loggedInUser; + } + if (!user) { + done(null, false, { "message": "Shouldn't happen: no user defined" }); + return; + } + if (!user.services) { user.services = {}; } @@ -240,6 +254,7 @@ abstract class CASStrategy implements RegistrationStrategy { } private async passportCallback(request: Request, username: string, profile: Profile, done: PassportDone) { + let loggedInUser = request.user as IUserMongoose | undefined; let user = await User.findOne({[`services.${this.name}.id`]: username}); let email = `${username}@${this.emailDomain}`; let isAdmin = false; @@ -250,7 +265,7 @@ abstract class CASStrategy implements RegistrationStrategy { console.info(`Adding new admin: ${email}`); } } - if (!user) { + if (!user && !loggedInUser) { user = new User({ ...OAuthStrategy.defaultUserProperties, email, @@ -278,6 +293,19 @@ abstract class CASStrategy implements RegistrationStrategy { done(null, user); } else { + if (user && loggedInUser) { + // Remove extra account represented by loggedInUser and merge into user + User.remove({ "uuid": loggedInUser.uuid }); + } + else if (!user && loggedInUser) { + // Attach service info to logged in user instead of non-existant user pulled via email address + user = loggedInUser; + } + if (!user) { + done(null, false, { "message": "Shouldn't happen: no user defined" }); + return; + } + if (!user.services) { user.services = {}; } diff --git a/server/routes/templates.ts b/server/routes/templates.ts index 0f192e08..91d064b5 100644 --- a/server/routes/templates.ts +++ b/server/routes/templates.ts @@ -19,7 +19,8 @@ import { ITeamMongoose, Team, IIndexTemplate, ILoginTemplate, IAdminTemplate, ITeamTemplate, IRegisterBranchChoiceTemplate, IRegisterTemplate, StatisticEntry, - IFormItem + IFormItem, + IConfig } from "../schema"; import * as Branches from "../branch"; import { strategies, prettyNames } from "../routes/strategies"; @@ -130,7 +131,12 @@ Handlebars.registerHelper("toJSONString", (stat: StatisticEntry): string => { Handlebars.registerHelper("removeSpaces", (input: string): string => { return input.replace(/ /g, "-"); }); -Handlebars.registerPartial("sidebar", fs.readFileSync(path.resolve(STATIC_ROOT, "partials", "sidebar.html"), "utf8")); +Handlebars.registerHelper("join", (arr: T[]): string => { + return arr.join(", "); +}); +for (let name of ["sidebar", "login-methods"]) { + Handlebars.registerPartial(name, fs.readFileSync(path.resolve(STATIC_ROOT, "partials", `${name}.html`), "utf8")); +} templateRoutes.route("/dashboard").get((request, response) => response.redirect("/")); templateRoutes.route("/").get(authenticateWithRedirect, async (request, response) => { @@ -280,7 +286,7 @@ templateRoutes.route("/login").get(async (request, response) => { }; response.send(loginTemplate(templateData)); }); -templateRoutes.route("/login/confirm").get((request, response) => { +templateRoutes.route("/login/confirm").get(async (request, response) => { let user = request.user as IUser; if (!user) { response.redirect("/login"); @@ -290,13 +296,15 @@ templateRoutes.route("/login/confirm").get((request, response) => { response.redirect("/"); } - let loginMethods: string[] = []; + let usedLoginMethods: string[] = []; if (user.local && user.local!.hash) { - loginMethods.push("Local"); + usedLoginMethods.push("Local"); } - for (let service of Object.keys(user.services || {}) as (keyof typeof user.services)[]) { - loginMethods.push(prettyNames[service]); + let services = Object.keys(user.services || {}) as (keyof typeof user.services)[]; + for (let service of services) { + usedLoginMethods.push(prettyNames[service]); } + let loginMethods = (await getSetting("loginMethods")).filter(method => method !== "local" && !services.includes(method)); response.send(postLoginTemplate({ siteTitle: config.eventName, @@ -306,7 +314,9 @@ templateRoutes.route("/login/confirm").get((request, response) => { name: user.name || "", email: user.email || "", verifiedEmail: user.verifiedEmail || false, - loginMethods: loginMethods.join(", ") + usedLoginMethods, + loginMethods, + canAddLogins: loginMethods.length !== 0 })); }); templateRoutes.route("/login/forgot").get((request, response) => { From 0b2b61943a6d525205f97dd1107256ecb2c420b0 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Tue, 19 Jun 2018 15:32:27 -0400 Subject: [PATCH 074/194] Improve account merging and replacement --- client/postlogin.html | 2 +- server/routes/strategies.ts | 43 +++++++++++++++++++++++++++++++------ 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/client/postlogin.html b/client/postlogin.html index b4685376..3585c18e 100644 --- a/client/postlogin.html +++ b/client/postlogin.html @@ -18,7 +18,7 @@

{{siteTitle}}

{{success}}
-

New Account

+

New Login

{{#if canAddLogins}}

Connect your account with another provider or an existing account:

diff --git a/server/routes/strategies.ts b/server/routes/strategies.ts index c8ce10c4..4decad9e 100644 --- a/server/routes/strategies.ts +++ b/server/routes/strategies.ts @@ -116,7 +116,10 @@ abstract class OAuthStrategy implements RegistrationStrategy { return; } - let user = await User.findOne({ email }); + let user = await User.findOne({[`services.${this.name}.id`]: profile.id}); + if (!user) { + user = await User.findOne({ email }); + } let loggedInUser = request.user as IUserMongoose | undefined; let isAdmin = false; if (config.admins.includes(email)) { @@ -154,9 +157,21 @@ abstract class OAuthStrategy implements RegistrationStrategy { done(null, user); } else { - if (user && loggedInUser) { + if (user && loggedInUser && user.uuid !== loggedInUser.uuid) { // Remove extra account represented by loggedInUser and merge into user - User.remove({ "uuid": loggedInUser.uuid }); + user.services = { + ...loggedInUser.services, + // Don't overwrite any existing services + ...user.services + }; + if (loggedInUser.local && loggedInUser.local.hash && (!user.local || !user.local.hash)) { + user.local = { + ...loggedInUser.local + }; + } + await User.findOneAndRemove({ "uuid": loggedInUser.uuid }); + // So that the user has an indication of the linking + user.accountConfirmed = false; } else if (!user && loggedInUser) { // Attach service info to logged in user instead of non-existant user pulled via email address @@ -176,8 +191,10 @@ abstract class OAuthStrategy implements RegistrationStrategy { username: profile.username, profileUrl: profile.profileUrl }; + // So that the user has an indication of the linking + user.accountConfirmed = false; } - if (!user.verifiedEmail) { + if (!user.verifiedEmail && user.email === email) { // We trust our OAuth provider to have verified the user's email for us user.verifiedEmail = true; } @@ -293,9 +310,21 @@ abstract class CASStrategy implements RegistrationStrategy { done(null, user); } else { - if (user && loggedInUser) { + if (user && loggedInUser && user.uuid !== loggedInUser.uuid) { // Remove extra account represented by loggedInUser and merge into user - User.remove({ "uuid": loggedInUser.uuid }); + user.services = { + ...loggedInUser.services, + // Don't overwrite any existing services + ...user.services + }; + if (loggedInUser.local && loggedInUser.local.hash && (!user.local || !user.local.hash)) { + user.local = { + ...loggedInUser.local + }; + } + await User.findOneAndRemove({ "uuid": loggedInUser.uuid }); + // So that the user has an indication of the linking + user.accountConfirmed = false; } else if (!user && loggedInUser) { // Attach service info to logged in user instead of non-existant user pulled via email address @@ -470,7 +499,7 @@ export class Local implements RegistrationStrategy { response.redirect("/login"); return; } - if (!user.local) { + if (!user.local || !user.local.hash) { request.flash("error", "The account with the email that you submitted has no password set. Please log in with an external service like GitHub, Google, or Facebook instead."); response.redirect("/login"); return; From 41dba6ba7817936003a5c8e6c6e03f0343ed113f Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Thu, 21 Jun 2018 23:27:06 -0400 Subject: [PATCH 075/194] Upgrade Mongoose to version 5 --- package-lock.json | 299 +++++++++++++--------------------------------- package.json | 9 +- server/common.ts | 9 +- 3 files changed, 94 insertions(+), 223 deletions(-) diff --git a/package-lock.json b/package-lock.json index c66c5772..f8817bd1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,9 +60,9 @@ } }, "@types/bson": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@types/bson/-/bson-1.0.4.tgz", - "integrity": "sha512-/nysVvxwup1WniGHIM31UZXM+6727h4FAa2tZpFSQBooBcl2Bh1N9oQmVVg8QYnjchN/DOGi7UvVN0jpzWL6sw==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/bson/-/bson-1.0.9.tgz", + "integrity": "sha512-IO2bGcW3ApLptLPOQ0HY3RLY40psH5aG5/DAU9HBEJ21vqiNE0cYZM52P8iWw0Dzk5qiKLReEUsCtn6V6qVMNg==", "dev": true, "requires": { "@types/node": "*" @@ -206,28 +206,29 @@ "dev": true }, "@types/moment-timezone": { - "version": "0.2.35", - "resolved": "https://registry.npmjs.org/@types/moment-timezone/-/moment-timezone-0.2.35.tgz", - "integrity": "sha512-HG4pUK/fTrGY3FerMlINxK74MxdAxkCRYrp5AM+oJ2jLcK0jWUi64ZV15JKwDR4TYLIxrT3y9SVnEWcLPbC/YA==", + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/@types/moment-timezone/-/moment-timezone-0.5.6.tgz", + "integrity": "sha512-rMZjLmXs9sly1UbwxckyAEvQkrwrGqR24nFAjFrndRJBBnUooCCD0LPmdRcf9haHXFnckI9E3ko0oC6LEDk7dw==", "dev": true, "requires": { "moment": ">=2.14.0" } }, "@types/mongodb": { - "version": "2.2.12", - "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-2.2.12.tgz", - "integrity": "sha512-nJLPAQro3U0nwhXnglmH+4DMUBVDa4tsPnwEFtzyv4x+eT0UynlrPm2rGR8UPDw0VcTF3UGI5UEqyjcnq/ukgA==", + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.0.21.tgz", + "integrity": "sha512-O+jw6Pi3n0LKsVkO86NL+oM6GadFaLnEtioM0bRwD/LK/7ULL9+F30G7n6rW5HcNJZ0wKalP1ERz3ezIseMH2Q==", "dev": true, "requires": { "@types/bson": "*", + "@types/events": "*", "@types/node": "*" } }, "@types/mongoose": { - "version": "4.7.36", - "resolved": "https://registry.npmjs.org/@types/mongoose/-/mongoose-4.7.36.tgz", - "integrity": "sha512-aa2DNfTCoMkTMA39JAfSJATFc/oBiSN84FG8fTTHbWjdP+hXyARLVsvN7xYCjq7Pvb5tahlfvF3TjT1PZI1cIg==", + "version": "5.0.18", + "resolved": "https://registry.npmjs.org/@types/mongoose/-/mongoose-5.0.18.tgz", + "integrity": "sha512-NTZYnHKJcIuVU7sqojZVG84SoitGEBHC6iDAT/hgGIzDHZ+mwO+snjUnglkHdS9+jJ2YssCJbSZp80i86fCBvQ==", "dev": true, "requires": { "@types/events": "*", @@ -761,9 +762,9 @@ } }, "browser-stdout": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", - "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, "bson": { @@ -1827,12 +1828,6 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" }, - "graceful-readlink": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", - "dev": true - }, "graphql": { "version": "0.10.5", "resolved": "https://registry.npmjs.org/graphql/-/graphql-0.10.5.tgz", @@ -1898,9 +1893,9 @@ } }, "growl": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", - "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", "dev": true }, "handlebars": { @@ -1979,11 +1974,6 @@ "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==" }, - "hooks-fixed": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hooks-fixed/-/hooks-fixed-2.0.2.tgz", - "integrity": "sha512-YurCM4gQSetcrhwEtpQHhQ4M7Zo7poNGqY4kQGeBS6eZtOcT3tnNs01ThFa0jYBByAiYt1MjMjP/YApG0EnAvQ==" - }, "htmlparser2": { "version": "3.9.2", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz", @@ -2209,12 +2199,6 @@ } } }, - "json3": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", - "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", - "dev": true - }, "jsonfile": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", @@ -2235,9 +2219,9 @@ } }, "kareem": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/kareem/-/kareem-1.5.0.tgz", - "integrity": "sha1-4+QQHZ3P3imXadr0tNtk2JXRdEg=" + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.2.1.tgz", + "integrity": "sha512-xpDFy8OxkFM+vK6pXy6JmH92ibeEFUuDWzas5M9L7MzVmHW3jzwAHxodCPV/BYkf4A31bVDLyonrMfp9RXb/oA==" }, "kind-of": { "version": "3.2.2", @@ -2262,60 +2246,15 @@ } }, "lodash": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", - "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" - }, - "lodash._baseassign": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", - "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", - "dev": true, - "requires": { - "lodash._basecopy": "^3.0.0", - "lodash.keys": "^3.0.0" - } - }, - "lodash._basecopy": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", - "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", - "dev": true - }, - "lodash._basecreate": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz", - "integrity": "sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=", - "dev": true - }, - "lodash._getnative": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", - "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", - "dev": true - }, - "lodash._isiterateecall": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", - "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", - "dev": true + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" }, "lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" }, - "lodash.create": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz", - "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=", - "dev": true, - "requires": { - "lodash._baseassign": "^3.0.0", - "lodash._basecreate": "^3.0.0", - "lodash._isiterateecall": "^3.0.0" - } - }, "lodash.flatten": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", @@ -2326,34 +2265,11 @@ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" }, - "lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", - "dev": true - }, - "lodash.isarray": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", - "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", - "dev": true - }, "lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" }, - "lodash.keys": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", - "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", - "dev": true, - "requires": { - "lodash._getnative": "^3.0.0", - "lodash.isarguments": "^3.0.0", - "lodash.isarray": "^3.0.0" - } - }, "lodash.set": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", @@ -2468,71 +2384,44 @@ } }, "mocha": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.5.3.tgz", - "integrity": "sha512-/6na001MJWEtYxHOV1WLfsmR4YIynkUEhBwzsb+fk2qmQ3iqsi258l/Q2MWHJMImAcNpZ8DEdYAK72NHoIQ9Eg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", "dev": true, "requires": { - "browser-stdout": "1.3.0", - "commander": "2.9.0", - "debug": "2.6.8", - "diff": "3.2.0", + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", "escape-string-regexp": "1.0.5", - "glob": "7.1.1", - "growl": "1.9.2", + "glob": "7.1.2", + "growl": "1.10.5", "he": "1.1.1", - "json3": "3.3.2", - "lodash.create": "3.1.1", + "minimatch": "3.0.4", "mkdirp": "0.5.1", - "supports-color": "3.1.2" + "supports-color": "5.4.0" }, "dependencies": { "commander": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", - "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", - "dev": true, - "requires": { - "graceful-readlink": ">= 1.0.0" - } + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true }, "debug": { - "version": "2.6.8", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", - "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", "dev": true, "requires": { "ms": "2.0.0" } }, - "glob": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", - "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.2", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", "dev": true - }, - "supports-color": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz", - "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", - "dev": true, - "requires": { - "has-flag": "^1.0.0" - } } } }, @@ -2594,68 +2483,60 @@ } }, "mongoose": { - "version": "4.13.14", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-4.13.14.tgz", - "integrity": "sha512-20zynb1fvCO37AP+0iTPGDbt4dJJkzM9fNK5BwKf5n+gFU5YYdXpnhxs9Kf8C+Fe0xY8vpUKV8wA7VGWcmDaFw==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.1.6.tgz", + "integrity": "sha512-p8p/3Z2kfXViqawN1TV+cZ8XbHz6SsllkytKTog+CDWfCNObyGraHQlUuRv/9aYPNKiZfq6WWITgLpJLZW/o/A==", "requires": { - "async": "2.6.0", - "bson": "~1.0.4", - "hooks-fixed": "2.0.2", - "kareem": "1.5.0", + "async": "2.6.1", + "bson": "~1.0.5", + "kareem": "2.2.1", "lodash.get": "4.4.2", - "mongodb": "2.2.34", - "mpath": "0.3.0", - "mpromise": "0.5.5", - "mquery": "2.3.3", + "mongodb": "3.0.10", + "mongoose-legacy-pluralize": "1.0.2", + "mpath": "0.4.1", + "mquery": "3.0.0", "ms": "2.0.0", - "muri": "1.3.0", "regexp-clone": "0.0.1", "sliced": "1.0.1" }, "dependencies": { "async": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", - "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", + "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", "requires": { - "lodash": "^4.14.0" + "lodash": "^4.17.10" } }, + "bson": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.9.tgz", + "integrity": "sha512-IQX9/h7WdMBIW/q/++tGd+emQr0XMdeZ6icnT/74Xk9fnabWn+gZgpE+9V+gujL3hhJOoNrnDVY7tWdzc7NUTg==" + }, "mongodb": { - "version": "2.2.34", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.34.tgz", - "integrity": "sha1-o09Zu+thdUrsQy3nLD/iFSakTBo=", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.0.10.tgz", + "integrity": "sha512-jy9s4FgcM4rl8sHNETYHGeWcuRh9AlwQCUuMiTj041t/HD02HwyFgmm2VZdd9/mA9YNHaUJLqj0tzBx2QFivtg==", "requires": { - "es6-promise": "3.2.1", - "mongodb-core": "2.1.18", - "readable-stream": "2.2.7" + "mongodb-core": "3.0.9" } }, "mongodb-core": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.18.tgz", - "integrity": "sha1-TEYTm986HwMt7ZHbSfOO7AFlkFA=", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-3.0.9.tgz", + "integrity": "sha512-buOWjdLLBlEqjHDeHYSXqXx173wHMVp7bafhdHxSjxWdB9V6Ri4myTqxjYZwL/eGFZxvd8oRQSuhwuIDbaaB+g==", "requires": { "bson": "~1.0.4", - "require_optional": "~1.0.0" - } - }, - "readable-stream": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.7.tgz", - "integrity": "sha1-BwV6y+JGeyIELTb5jFrVBwVOlbE=", - "requires": { - "buffer-shims": "~1.0.0", - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "~1.0.0", - "process-nextick-args": "~1.0.6", - "string_decoder": "~1.0.0", - "util-deprecate": "~1.0.1" + "require_optional": "^1.0.1" } } } }, + "mongoose-legacy-pluralize": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz", + "integrity": "sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ==" + }, "morgan": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.0.tgz", @@ -2669,19 +2550,14 @@ } }, "mpath": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.3.0.tgz", - "integrity": "sha1-elj3iem1/TyUUgY0FXlg8mvV70Q=" - }, - "mpromise": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mpromise/-/mpromise-0.5.5.tgz", - "integrity": "sha1-9bJCWddjrMIlewoMjG2Gb9UXMuY=" + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.4.1.tgz", + "integrity": "sha512-NNY/MpBkALb9jJmjpBlIi6GRoLveLUM0pJzgbp9vY9F7IQEb/HREC/nxrixechcQwd1NevOhJnWWV8QQQRE+OA==" }, "mquery": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-2.3.3.tgz", - "integrity": "sha512-NC8L14kn+qxJbbJ1gbcEMDxF0sC3sv+1cbRReXXwVvowcwY1y9KoVZFq0ebwARibsadu8lx8nWGvm3V0Pf0ZWQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-3.0.0.tgz", + "integrity": "sha512-WL1Lk8v4l8VFSSwN3yCzY9TXw+fKVYKn6f+w86TRzOLSE8k1yTgGaLBPUByJQi8VcLbOdnUneFV/y3Kv874pnQ==", "requires": { "bluebird": "3.5.0", "debug": "2.6.9", @@ -2723,11 +2599,6 @@ } } }, - "muri": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/muri/-/muri-1.3.0.tgz", - "integrity": "sha512-FiaFwKl864onHFFUV/a2szAl7X0fxVlSKNdhTf+BM8i8goEgYut8u5P9MqQqIYwvaMxjzVESsoEm/2kfkFH1rg==" - }, "mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", diff --git a/package.json b/package.json index 325a5042..1509195c 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "marked": "^0.3.19", "moment": "^2.22.2", "moment-timezone": "^0.5.17", - "mongoose": "^4.13.14", + "mongoose": "^5.1.6", "morgan": "^1.8.2", "multer": "^1.3.0", "passport": "^0.3.2", @@ -85,8 +85,9 @@ "@types/handlebars": "^4.0.38", "@types/marked": "0.0.28", "@types/mocha": "^2.2.48", - "@types/moment-timezone": "^0.2.34", - "@types/mongoose": "^4.7.36", + "@types/moment-timezone": "^0.5.6", + "@types/mongodb": "^3.0.21", + "@types/mongoose": "^5.0.18", "@types/morgan": "^1.7.35", "@types/multer": "^1.3.6", "@types/node": "^8.10.19", @@ -99,7 +100,7 @@ "@types/tmp": "0.0.33", "@types/whatwg-fetch": "0.0.33", "chai": "^4.0.0", - "mocha": "^3.4.2", + "mocha": "^5.2.0", "supertest": "^3.1.0", "tslint": "^5.10.0", "tslint-language-service": "^0.9.9", diff --git a/server/common.ts b/server/common.ts index a047d0ab..30352762 100644 --- a/server/common.ts +++ b/server/common.ts @@ -280,11 +280,10 @@ export function formatSize(size: number, binary: boolean = true): string { // Database connection // import * as mongoose from "mongoose"; -(mongoose as any).Promise = global.Promise; -mongoose.connect(config.server.mongoURL, { - useMongoClient: true -} as mongoose.ConnectionOptions); -export {mongoose}; +mongoose.connect(config.server.mongoURL).catch(err => { + throw err; +}); +export { mongoose }; import { Setting } from "./schema"; From cce3e761d85d549f5d57b0a5921553bedd4d8e46 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Thu, 21 Jun 2018 23:36:22 -0400 Subject: [PATCH 076/194] Store login service email which can now be different from account email --- server/routes/strategies.ts | 4 ++++ server/schema.ts | 2 ++ 2 files changed, 6 insertions(+) diff --git a/server/routes/strategies.ts b/server/routes/strategies.ts index 4decad9e..b82ae968 100644 --- a/server/routes/strategies.ts +++ b/server/routes/strategies.ts @@ -141,6 +141,7 @@ abstract class OAuthStrategy implements RegistrationStrategy { } user.services[serviceName] = { id: profile.id, + email, username: profile.username, profileUrl: profile.profileUrl }; @@ -188,6 +189,7 @@ abstract class OAuthStrategy implements RegistrationStrategy { if (!user.services[serviceName]) { user.services[serviceName] = { id: profile.id, + email, username: profile.username, profileUrl: profile.profileUrl }; @@ -295,6 +297,7 @@ abstract class CASStrategy implements RegistrationStrategy { } user.services[this.name] = { id: username, + email, username }; try { @@ -341,6 +344,7 @@ abstract class CASStrategy implements RegistrationStrategy { if (!user.services[this.name]) { user.services[this.name] = { id: username, + email, username }; } diff --git a/server/schema.ts b/server/schema.ts index 48953bf2..32c36d82 100644 --- a/server/schema.ts +++ b/server/schema.ts @@ -102,6 +102,8 @@ export interface IUser { services: { [Service in Exclude]?: { id: string; + // OAuth account email can be different than registration account email + email: string; username?: string; profileUrl?: string; }; From e834726a0268969a00d4c9921c2e10d20aeff427 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Thu, 21 Jun 2018 23:44:00 -0400 Subject: [PATCH 077/194] Update config.example.json to reflect recent configuration changes --- server/config/config.example.json | 41 ++++++++++++++++--------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/server/config/config.example.json b/server/config/config.example.json index 0181a5e0..57d9d5f2 100644 --- a/server/config/config.example.json +++ b/server/config/config.example.json @@ -1,24 +1,25 @@ { "secrets": { - "session": "", - "github": { - "id": "", - "secret": "" - }, - "google": { - "id": "", - "secret": "" - }, - "facebook": { - "id": "", - "secret": "" + "session": "", + "oauth": { + "github": { + "id": "", + "secret": "" + }, + "google": { + "id": "", + "secret": "" + }, + "facebook": { + "id": "", + "secret": "" + } } }, "email": { - "from": "", - "host": "", - "username": "", - "password": "", + "from": "HackGT Team ", + "host": "smtp.sendgrid.net", + "key": "", "port": 465 }, "server": { @@ -26,16 +27,16 @@ "port": 3000, "cookieMaxAge": 15552000000, "cookieSecureOnly": true, - "passwordResetExpiration": 3600000 + "passwordResetExpiration": 3600000, + "mongoURL": "mongodb://localhost/registration" }, "admins": ["example@example.com"], "eventName": "My Hackathon", "storageEngine": { - "name": "disk", + "name": "disk | s3", "options": { "uploadDirectory": "uploads" } }, - "maxTeamSize": 4, - "enableQRCode": false + "maxTeamSize": 4 } From 321a7c06499fdb85c63483cdbb14b5e0b20b06cc Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Fri, 22 Jun 2018 00:42:51 -0400 Subject: [PATCH 078/194] Fix broken templating within some condition blocks due to "this" binding --- server/routes/templates.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/server/routes/templates.ts b/server/routes/templates.ts index 91d064b5..a0e2169f 100644 --- a/server/routes/templates.ts +++ b/server/routes/templates.ts @@ -85,15 +85,13 @@ templateRoutes.use(async (request, response, next) => { // tslint:disable-next-line:no-any // tslint:disable:no-invalid-this -Handlebars.registerHelper("ifCond", (v1: any, v2: any, options: any) => { +Handlebars.registerHelper("ifCond", function(v1: any, v2: any, options: any) { if (v1 === v2) { - // tslint:disable-next-line:no-invalid-this return options.fn(this); } - // tslint:disable-next-line:no-invalid-this return options.inverse(this); }); -Handlebars.registerHelper("ifIn", (elem: T, list: T[], options: any) => { +Handlebars.registerHelper("ifIn", function(elem: T, list: T[], options: any) { if (list.includes(elem)) { return options.fn(this); } From e7b5ccc6363f328124721e473a7ef4c86c0b7954 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Fri, 22 Jun 2018 01:13:51 -0400 Subject: [PATCH 079/194] Fix Object.keys() call on undefined object if user has no login services --- server/routes/api/graphql.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routes/api/graphql.ts b/server/routes/api/graphql.ts index 0b07823f..56de01fc 100644 --- a/server/routes/api/graphql.ts +++ b/server/routes/api/graphql.ts @@ -295,7 +295,7 @@ async function userRecordToGraphql(user: IUser): Promise> { if (user.local && user.local!.hash) { loginMethods.push("Local"); } - for (let service of Object.keys(user.services) as (keyof typeof user.services)[]) { + for (let service of Object.keys(user.services || {}) as (keyof typeof user.services)[]) { loginMethods.push(strategyNames[service]); } From 51a460a7ddd75cc89b09cf4f2b650228d00d5073 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Fri, 22 Jun 2018 00:48:55 -0400 Subject: [PATCH 080/194] Add static timeline and associated styles --- client/css/index.css | 57 ++++++++++++++++++++++++++++++++++- client/css/theme.css | 71 -------------------------------------------- client/index.html | 27 +++++++++++++++++ 3 files changed, 83 insertions(+), 72 deletions(-) delete mode 100644 client/css/theme.css diff --git a/client/css/index.css b/client/css/index.css index 03713117..7b30b75d 100644 --- a/client/css/index.css +++ b/client/css/index.css @@ -37,4 +37,59 @@ p { #qrCode > svg { width: 50%; height: auto; -} \ No newline at end of file +} + +/* Timeline */ +ul.timeline { + list-style-type: none; + display: flex; + align-items: center; + justify-content: center; + padding: 0; +} +ul.timeline > li { + position: relative; +} +ul.timeline > li > time { + margin-bottom: 20px; + padding: 0 40px; + font-weight: 100; + display: flex; + flex-direction: column; + align-items: center; +} +ul.timeline > li > div::before { + display: block; + content: ""; + width: 25px; + height: 25px; + background-color: white; + border-radius: 25px; + border: 1px solid #ddd; + position: absolute; + top: 31px; +} +ul.timeline > li.complete > div::before { + background-color: #66DC71; + border: none; +} +ul.timeline > li > div { + padding: 0 40px; + display: flex; + justify-content: center; + border-top: 2px solid #D6DCE0; + text-align: center; +} +ul.timeline > li.complete > div { + border-top: 2px solid #66DC71; +} +ul.timeline > li > div > h4 { + display: block; + font-size: 1.7rem; + font-weight: 600; +} +@media screen and (max-width: 768px) { + ul.timeline { + display: none; + } +} diff --git a/client/css/theme.css b/client/css/theme.css deleted file mode 100644 index b7977204..00000000 --- a/client/css/theme.css +++ /dev/null @@ -1,71 +0,0 @@ -@import url("https://fonts.googleapis.com/css?family=Droid+Sans+Mono|VT323"); - -/* Colors */ -html, body, #sidebar-nav { - background-color: #081517; - color: #c4fbf5; -} -#sidebar a, #sidebar, .editor-toolbar > a { - color: #c4fbf5; - border: none; - text-decoration: initial; -} -.editor-toolbar > a { - color: #c4fbf5 !important; -} -.editor-toolbar a.active, .editor-toolbar a:hover { - border-color: #081517; - background-color: rgba(196, 251, 245, 0.2); - outline: none; -} -#sidebar-nav > span.divider, .main, table thead { - border-color: #c4fbf5; -} -a { - color: #c4fbf5; - text-decoration: none; - border-bottom: 1px dotted; - cursor: pointer; -} -a:hover { - border-bottom-style: solid; -} -#email-rendered { - background-color: #081517; - border: 1px solid #c4fbf5; -} - -/* Buttons */ -.btn, [type=submit], button, .accepted-btn-false { - background-color: #c4fbf5; - /* border: 2px solid #03565c; */ - color: #081517; -} - -/* Fonts */ -body, swal2-content, form > p, fieldset > div > label { - font-family: "Droid Sans Mono"; -} -fieldset > div > label { - font-size: 90%; -} -h1, h2, h3, h4, h5, h6, code, #sidebar a, #sidebar, label { - font-family: "VT323"; -} -code { - color: #081517; -} - -/* Form elements */ -input:not([type=submit]):not([type=radio]):not([type=checkbox]):not([type=reset]), select, fieldset, textarea { - background-color: #081517; - color: white; - border-color: #c4fbf5; - font-family: "Droid Sans Mono"; -} -input:not([type=submit]):not([type=radio]):not([type=checkbox]):not([type=reset]):focus, textarea:focus, textarea[type=text]:focus { - border-color: white; -} -input:disabled, textarea:disabled { - background: repeating-linear-gradient(45deg, #081517, #081517 10px, #192729 10px, #192729 20px ) -} diff --git a/client/index.html b/client/index.html index e00dea96..f87eac40 100644 --- a/client/index.html +++ b/client/index.html @@ -22,6 +22,33 @@

Dashboard

+
    +
  • + +
    +

    Account created

    +
    +
  • +
  • + +
    +

    Application Submitted

    +
    +
  • +
  • + +
    +

    Application Decision

    +
    +
  • +
  • + +
    +

    Confirmation Submitted

    +
    +
  • +
+

Your status:

From 91d7eb5abdc44892052e204dd36ac43ab20b713d Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Sat, 23 Jun 2018 11:48:11 -0400 Subject: [PATCH 081/194] Add team formation --- client/css/index.css | 1 + client/index.html | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/client/css/index.css b/client/css/index.css index 7b30b75d..c5fc321a 100644 --- a/client/css/index.css +++ b/client/css/index.css @@ -74,6 +74,7 @@ ul.timeline > li.complete > div::before { border: none; } ul.timeline > li > div { + height: 85px; padding: 0 40px; display: flex; justify-content: center; diff --git a/client/index.html b/client/index.html index f87eac40..f17be412 100644 --- a/client/index.html +++ b/client/index.html @@ -24,12 +24,6 @@

Dashboard

  • - -
    -

    Account created

    -
    -
  • -
  • Application Submitted

    @@ -47,6 +41,12 @@

    Application Decision

    Confirmation Submitted

  • +
  • + +
    +

    Team Formation (optional)

    +
    +
From f7480c828ee2685894cffc85c49eb21a9585bca2 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Sat, 23 Jun 2018 20:35:10 -0400 Subject: [PATCH 082/194] Add / tweak success, rejection, and warning styles --- client/css/index.css | 30 +++++++++++++++++++++++++----- client/index.html | 12 ++++++------ 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/client/css/index.css b/client/css/index.css index c5fc321a..c521b2f1 100644 --- a/client/css/index.css +++ b/client/css/index.css @@ -50,7 +50,7 @@ ul.timeline { ul.timeline > li { position: relative; } -ul.timeline > li > time { +ul.timeline > li > span { margin-bottom: 20px; padding: 0 40px; font-weight: 100; @@ -69,10 +69,6 @@ ul.timeline > li > div::before { position: absolute; top: 31px; } -ul.timeline > li.complete > div::before { - background-color: #66DC71; - border: none; -} ul.timeline > li > div { height: 85px; padding: 0 40px; @@ -84,6 +80,30 @@ ul.timeline > li > div { ul.timeline > li.complete > div { border-top: 2px solid #66DC71; } +ul.timeline > li.complete > div::before { + content: "✔"; + background-color: #66DC71; + color: white; + border: none; +} +ul.timeline > li.warning > div { + border-top: 2px solid #FFDC00; +} +ul.timeline > li.warning > div::before { + content: "!"; + background-color: #FFDC00; + color: black; + border: none; +} +ul.timeline > li.rejected > div { + border-top: 2px solid #FF4136; +} +ul.timeline > li.rejected > div::before { + content: "✕"; + background-color: #FF4136; + color: white; + border: none; +} ul.timeline > li > div > h4 { display: block; font-size: 1.7rem; diff --git a/client/index.html b/client/index.html index f17be412..9dfe3534 100644 --- a/client/index.html +++ b/client/index.html @@ -24,25 +24,25 @@

Dashboard

  • - + Not completed
    -

    Application Submitted

    +

    Application Submission

  • - + Pending

    Application Decision

  • - + Not completed
    -

    Confirmation Submitted

    +

    Confirmation Submission

  • - + Not completed

    Team Formation (optional)

    From 4fa1bced1a66d414213ec5ca64955ee0316a30e5 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Sat, 23 Jun 2018 21:26:04 -0400 Subject: [PATCH 083/194] Generate timeline classes to display in index template --- client/css/index.css | 12 +++++++- client/index.html | 12 +++----- server/routes/templates.ts | 58 ++++++++++++++++++++++++++++++++------ server/schema.ts | 7 +++++ 4 files changed, 72 insertions(+), 17 deletions(-) diff --git a/client/css/index.css b/client/css/index.css index c521b2f1..4e80c280 100644 --- a/client/css/index.css +++ b/client/css/index.css @@ -50,7 +50,8 @@ ul.timeline { ul.timeline > li { position: relative; } -ul.timeline > li > span { +ul.timeline > li::before { + content: attr(data-pending); margin-bottom: 20px; padding: 0 40px; font-weight: 100; @@ -77,6 +78,9 @@ ul.timeline > li > div { border-top: 2px solid #D6DCE0; text-align: center; } +ul.timeline > li.complete::before { + content: attr(data-complete); +} ul.timeline > li.complete > div { border-top: 2px solid #66DC71; } @@ -86,6 +90,9 @@ ul.timeline > li.complete > div::before { color: white; border: none; } +ul.timeline > li.warning::before { + content: attr(data-warning); +} ul.timeline > li.warning > div { border-top: 2px solid #FFDC00; } @@ -94,6 +101,9 @@ ul.timeline > li.warning > div::before { background-color: #FFDC00; color: black; border: none; +} +ul.timeline > li.rejected::before { + content: attr(data-rejected); } ul.timeline > li.rejected > div { border-top: 2px solid #FF4136; diff --git a/client/index.html b/client/index.html index 9dfe3534..83e5bedf 100644 --- a/client/index.html +++ b/client/index.html @@ -23,26 +23,22 @@

    Dashboard

      -
    • - Not completed +
    • Application Submission

    • -
    • - Pending +
    • Application Decision

    • -
    • - Not completed +
    • Confirmation Submission

    • -
    • - Not completed +
    • Team Formation (optional)

      diff --git a/server/routes/templates.ts b/server/routes/templates.ts index 8a029320..5081ba46 100644 --- a/server/routes/templates.ts +++ b/server/routes/templates.ts @@ -129,7 +129,8 @@ templateRoutes.route("/").get(authenticateWithRedirect, async (request, response if (user.applicationBranch) { applyBranches = [(await Branches.BranchConfig.loadBranchFromDB(user.applicationBranch))] as Branches.ApplicationBranch[]; - } else { + } + else { applyBranches = (await Branches.BranchConfig.loadAllBranches("Application") as Branches.ApplicationBranch[]); } @@ -196,22 +197,28 @@ templateRoutes.route("/").get(authenticateWithRedirect, async (request, response // Block of logic to dermine status: if (!user.applied) { status = "Incomplete"; - } else if (user.applied && !user.confirmationBranch) { + } + else if (user.applied && !user.confirmationBranch) { status = "Pending Decision"; - } else if (user.applied && user.confirmationBranch) { + } + else if (user.applied && user.confirmationBranch) { // After confirmation - they either confirmed in time, did not, or branch did not require confirmation if (user.confirmed) { if (user.accepted) { status = "Attending - " + user.confirmationBranch; - } else { + } + else { // For confirmation branches that do not accept such as Rejected/Waitlist status = user.confirmationBranch; } - } else if (moment().isAfter(confirmTimesArr[0].close)) { + } + else if (moment().isAfter(confirmTimesArr[0].close)) { status = "Confirmation Incomplete - " + user.confirmationBranch; - } else if (moment().isBefore(confirmTimesArr[0].open)) { + } + else if (moment().isBefore(confirmTimesArr[0].open)) { status = "Confirmation Opens Soon - " + user.confirmationBranch; - } else { + } + else { status = "Please Confirm - " + user.confirmationBranch; } } @@ -223,9 +230,15 @@ templateRoutes.route("/").get(authenticateWithRedirect, async (request, response let templateData: IIndexTemplate = { siteTitle: config.eventName, + user, + timeline: { + application: "", + decision: "", + confirmation: "", + teamFormation: "" + }, status, autoConfirm, - user, settings: { teamsEnabled: await getSetting("teamsEnabled"), qrEnabled: await getSetting("qrEnabled") @@ -257,6 +270,35 @@ templateRoutes.route("/").get(authenticateWithRedirect, async (request, response }; }) }; + + // Timeline configuration + if (user.applied) { + templateData.timeline.application = "complete"; + } + else if (templateData.applicationStatus.beforeOpen) { + templateData.timeline.application = "warning"; + } + else if (templateData.applicationStatus.afterClose) { + templateData.timeline.application = "rejected"; + } + if (user.applied && user.confirmationBranch) { + templateData.timeline.decision = user.accepted ? "complete" : "rejected"; + } + if (user.confirmationBranch) { + if (user.confirmed) { + templateData.timeline.confirmation = "complete"; + } + else if (templateData.confirmationStatus.beforeOpen) { + templateData.timeline.confirmation = "warning"; + } + else if (templateData.confirmationStatus.afterClose) { + templateData.timeline.confirmation = "rejected"; + } + } + if (user.teamId) { + templateData.timeline.teamFormation = "complete"; + } + response.send(indexTemplate(templateData)); }); diff --git a/server/schema.ts b/server/schema.ts index 2ade5298..ff3d66e6 100644 --- a/server/schema.ts +++ b/server/schema.ts @@ -276,7 +276,14 @@ export interface ICommonTemplate { qrEnabled: boolean; }; } +type TimelineClass = "" | "complete" | "warning" | "rejected"; export interface IIndexTemplate extends ICommonTemplate { + timeline: { + application: TimelineClass; + decision: TimelineClass; + confirmation: TimelineClass; + teamFormation: TimelineClass; + }; status: string; applicationOpen: string; applicationClose: string; From 254c73d23533c409191c6a25cc95af65009878bf Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Sat, 23 Jun 2018 21:38:02 -0400 Subject: [PATCH 084/194] Tweak mobile styles instead of hiding completely --- client/css/index.css | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/css/index.css b/client/css/index.css index 4e80c280..54537bff 100644 --- a/client/css/index.css +++ b/client/css/index.css @@ -121,6 +121,10 @@ ul.timeline > li > div > h4 { } @media screen and (max-width: 768px) { ul.timeline { - display: none; + flex-direction: column; + } + ul.timeline > li { + width: 100%; + margin-bottom: -10px; } } From b6c12b269593b12b9301de77281ab58eb3369b39 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Fri, 29 Jun 2018 12:50:52 -0400 Subject: [PATCH 085/194] Tweak sending verification email condition --- server/routes/auth.ts | 2 +- server/routes/strategies.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/server/routes/auth.ts b/server/routes/auth.ts index 8f66267d..66ec7da0 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -104,7 +104,7 @@ export async function reloadAuthentication() { try { await user.save(); - if (email) { + if (!user.verifiedEmail && (!user.local || !user.local.verificationCode)) { await sendVerificationEmail(request, user); } if (!user.verifiedEmail) { diff --git a/server/routes/strategies.ts b/server/routes/strategies.ts index b82ae968..cd60933e 100644 --- a/server/routes/strategies.ts +++ b/server/routes/strategies.ts @@ -471,6 +471,9 @@ export class Local implements RegistrationStrategy { } else { user.verifiedEmail = true; + if (user.local) { + user.local.verificationCode = undefined; + } // Possibly promote to admin status if (config.admins.indexOf(user.email) !== -1) { user.admin = true; From 4571f170448b3dbd9a189a3f4ba60fb8e46a70a9 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Fri, 29 Jun 2018 12:57:36 -0400 Subject: [PATCH 086/194] Bump version to 2.1.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1320bfc9..e7b5b9dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "2.0.0", + "version": "2.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index d0dc1b2f..f2a5d14e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "2.0.0", + "version": "2.1.0", "description": "Powerful and extensible registration system for hackathons and other large events", "main": "server/app.js", "scripts": { From ad7fed18de6acbabea472319405102191d506e3d Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Fri, 29 Jun 2018 12:58:15 -0400 Subject: [PATCH 087/194] Bump version to 2.2.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index f8817bd1..a0bfd2d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "2.0.0", + "version": "2.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 1509195c..943be3d2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "2.0.0", + "version": "2.2.0", "description": "Powerful and extensible registration system for hackathons and other large events", "main": "server/app.js", "scripts": { From c8be49e75a1e1345f80dfde69e89ae29682f418d Mon Sep 17 00:00:00 2001 From: Ehsan Asdar Date: Thu, 5 Jul 2018 23:29:30 -0700 Subject: [PATCH 088/194] Switch admin default emails to hack.gt domain (#222) --- deployment.yaml | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/deployment.yaml b/deployment.yaml index c864cd59..a572a9e1 100644 --- a/deployment.yaml +++ b/deployment.yaml @@ -25,12 +25,7 @@ env: EMAIL_FROM: "HackGT Team " PRODUCTION: true ADMIN_EMAILS: - - petschekr@gmail.com - - a@andrewdai.co - - faizan.virani.44@gmail.com - - lesurasani@gmail.com - - arshiya.singh16@gmail.com - - ajoshi97@gatech.edu - - ehsanmasdar@gmail.com - - mjkaufer@gmail.com + - ehsan@hack.gt + - ryan@hack.gt + - julian@hack.gt From 077e3ce211ee6df1505340f8b8ea4793b582bad4 Mon Sep 17 00:00:00 2001 From: Ehsan Asdar Date: Tue, 17 Jul 2018 23:12:59 -0700 Subject: [PATCH 089/194] Add an explict configurable default timezone to prevent issues with moment (#224) * Add an explict configurable default timezone to prevent issues with moment.tz.guess() Default is America/New York (EST) * Bump version to 2.2.1 --- package-lock.json | 2 +- package.json | 2 +- server/common.ts | 6 +++++- server/routes/templates.ts | 2 +- server/schema.ts | 1 + 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index a0bfd2d5..a4a1f19d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "2.2.0", + "version": "2.2.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 943be3d2..8e0afb2e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "2.2.0", + "version": "2.2.1", "description": "Powerful and extensible registration system for hackathons and other large events", "main": "server/app.js", "scripts": { diff --git a/server/common.ts b/server/common.ts index 30352762..5e21c212 100644 --- a/server/common.ts +++ b/server/common.ts @@ -43,7 +43,8 @@ class Config implements IConfig.Main { cookieMaxAge: 1000 * 60 * 60 * 24 * 30 * 6, // 6 months cookieSecureOnly: false, mongoURL: "mongodb://localhost/", - passwordResetExpiration: 1000 * 60 * 60 // 1 hour + passwordResetExpiration: 1000 * 60 * 60, // 1 hour + defaultTimezone: "America/New_York" }; public admins: string[] = []; public eventName: string = "Untitled Event"; @@ -200,6 +201,9 @@ class Config implements IConfig.Main { if (process.env.MONGO_URL) { this.server.mongoURL = process.env.MONGO_URL!; } + if (process.env.DEFAULT_TIMEZONE) { + this.server.defaultTimezone = process.env.DEFAULT_TIMEZONE; + } if (process.env.PASSWORD_RESET_EXPIRATION) { let expirationTime = parseInt(process.env.PASSWORD_RESET_EXPIRATION!, 10); if (!isNaN(expirationTime) && expirationTime > 0) { diff --git a/server/routes/templates.ts b/server/routes/templates.ts index 59142cc9..d3493059 100644 --- a/server/routes/templates.ts +++ b/server/routes/templates.ts @@ -192,7 +192,7 @@ templateRoutes.route("/").get(authenticateWithRedirect, async (request, response function formatMoment(date: moment.Moment | null): string { const FORMAT = "dddd, MMMM Do YYYY [at] h:mm a z"; if (date) { - return date.tz(moment.tz.guess()).format(FORMAT); + return date.tz(config.server.defaultTimezone).format(FORMAT); } return "(No branches configured)"; } diff --git a/server/schema.ts b/server/schema.ts index 7e969ae9..a20f235b 100644 --- a/server/schema.ts +++ b/server/schema.ts @@ -33,6 +33,7 @@ export namespace IConfig { cookieSecureOnly: boolean; mongoURL: string; passwordResetExpiration: number; + defaultTimezone: string; } export interface Style { theme: string; From 7ec4f76c825b63fafca47b3ed5bf3d738b65a12d Mon Sep 17 00:00:00 2001 From: Ehsan Asdar Date: Thu, 19 Jul 2018 21:22:36 -0700 Subject: [PATCH 090/194] Move ip and user to fields (#227) * Move ip and user to fields InfluxDB recommends high cardinality data go in fields, and we may want to run queries on this data that require it is stored in a field. * Version Bump --- package-lock.json | 2 +- package.json | 2 +- server/middleware.ts | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index a4a1f19d..9b491455 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "2.2.1", + "version": "2.2.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 8e0afb2e..6cb6e527 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "2.2.1", + "version": "2.2.2", "description": "Powerful and extensible registration system for hackathons and other large events", "main": "server/app.js", "scripts": { diff --git a/server/middleware.ts b/server/middleware.ts index 06312cf8..9e74e041 100644 --- a/server/middleware.ts +++ b/server/middleware.ts @@ -280,15 +280,14 @@ export function trackEvent(action: string, request: express.Request, user?: stri let tags = { action, url: request.path, - ip: request.ip, - user, ...data }; let metricsEvent: HackGTMetrics = { hackgtmetricsversion: 1, serviceName: `registration-${config.eventName.replace(/[^a-zA-Z0-9]/g, "")}-${action}`, values: { - value: 1 + user, + ip: request.ip }, tags }; From 2e11c01cfb373dc9db6cdccad4c8454ad4488724 Mon Sep 17 00:00:00 2001 From: Ehsan Asdar Date: Wed, 25 Jul 2018 00:13:54 -0700 Subject: [PATCH 091/194] Require user logged in for POSTing confirm endpoint (#228) * Require user logged in for POSTing confirm endpoint * Don't use middleware to validate user * Version bump --- server/routes/auth.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/routes/auth.ts b/server/routes/auth.ts index 66ec7da0..bc5a782a 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -78,6 +78,11 @@ export async function reloadAuthentication() { response.redirect("/login/confirm"); return; } + if (!request.isAuthenticated() || !user) { + request.flash("error", "Must be logged in"); + response.redirect("/login"); + return; + } user.name = name.trim(); let email = request.body.email as string | undefined; From 607aa8a411686be86763b33319d8a7a5c21a2adb Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Wed, 25 Jul 2018 14:51:57 -0400 Subject: [PATCH 092/194] Fix Mongoose deprecation warnings --- server/common.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/common.ts b/server/common.ts index 5e21c212..c52c2f11 100644 --- a/server/common.ts +++ b/server/common.ts @@ -284,7 +284,8 @@ export function formatSize(size: number, binary: boolean = true): string { // Database connection // import * as mongoose from "mongoose"; -mongoose.connect(config.server.mongoURL).catch(err => { +(mongoose as any).Promise = global.Promise; +mongoose.connect(config.server.mongoURL, { useMongoClient: true }).catch(err => { throw err; }); export { mongoose }; From 824e429f9f073aec8b4043f243e3579ba6336dad Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Wed, 25 Jul 2018 16:00:52 -0400 Subject: [PATCH 093/194] Display interservice API key in admin panel --- client/admin.html | 4 ++++ server/routes/templates.ts | 3 ++- server/schema.ts | 5 +++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/client/admin.html b/client/admin.html index 67ee2eec..b416650f 100644 --- a/client/admin.html +++ b/client/admin.html @@ -399,6 +399,10 @@

      config.json options

      +
      +
      + +
diff --git a/server/routes/templates.ts b/server/routes/templates.ts index d3493059..3f5c6e5e 100644 --- a/server/routes/templates.ts +++ b/server/routes/templates.ts @@ -699,7 +699,8 @@ templateRoutes.route("/admin").get(authenticateWithRedirect, async (request, res }) }, loginMethodsInfo, - adminEmails + adminEmails, + apiKey: config.secrets.adminKey }, config: { admins: config.admins.join(", "), diff --git a/server/schema.ts b/server/schema.ts index a20f235b..aa091c69 100644 --- a/server/schema.ts +++ b/server/schema.ts @@ -193,8 +193,8 @@ export const User = mongoose.model("User", new mongoose.Schema({ admin: Boolean }).index({ - email: 'text', - name: 'text' + email: "text", + name: "text" })); export interface ISetting { @@ -373,6 +373,7 @@ export interface IAdminTemplate extends ICommonTemplate { enabled: boolean; }[]; adminEmails: IUserMongoose[]; + apiKey: string; }; config: { admins: string; From 1cb527ba2195aecc8f607e0aa140aa346e070679 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Wed, 25 Jul 2018 14:51:57 -0400 Subject: [PATCH 094/194] Revert "Fix Mongoose deprecation warnings" This reverts commit 607aa8a411686be86763b33319d8a7a5c21a2adb. Was using Mongoose 4.x.x by accident. 5.x.x doesn't need these "fixes" --- server/common.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/common.ts b/server/common.ts index c52c2f11..5e21c212 100644 --- a/server/common.ts +++ b/server/common.ts @@ -284,8 +284,7 @@ export function formatSize(size: number, binary: boolean = true): string { // Database connection // import * as mongoose from "mongoose"; -(mongoose as any).Promise = global.Promise; -mongoose.connect(config.server.mongoURL, { useMongoClient: true }).catch(err => { +mongoose.connect(config.server.mongoURL).catch(err => { throw err; }); export { mongoose }; From 80235e549d72e21bb0b346da16a9e8e10c519cea Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Mon, 6 Aug 2018 22:38:14 -0400 Subject: [PATCH 095/194] Allow for searching by UUID --- server/routes/api/graphql.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/routes/api/graphql.ts b/server/routes/api/graphql.ts index 56de01fc..cf748b9e 100644 --- a/server/routes/api/graphql.ts +++ b/server/routes/api/graphql.ts @@ -111,6 +111,9 @@ async function searchUser(prev: any, args: { email: { $regex: queryRegExp } + }, + { + uuid: args.search } ]; const total = await User.find(userFilterToMongo(args.filter)) From f4f5e97dfc6124bd60f0f4bf7e1415929b4b1d52 Mon Sep 17 00:00:00 2001 From: Ehsan Asdar Date: Tue, 21 Aug 2018 21:38:57 -0500 Subject: [PATCH 096/194] Add ability to template in reimbursement amount for sending pre-confirm (#229) * Add ability to template in reimbursement amount for sending pre-confirm to a participant Only templates if the property is set on a user * Don't use space * Fix the regex :/ * Bump version --- client/admin.html | 1 + package-lock.json | 2 +- package.json | 2 +- server/common.ts | 5 +++++ server/schema.ts | 2 ++ 5 files changed, 10 insertions(+), 2 deletions(-) diff --git a/client/admin.html b/client/admin.html index b416650f..766f8a3a 100644 --- a/client/admin.html +++ b/client/admin.html @@ -343,6 +343,7 @@
List of variables:
  • \{{confirmationBranch}}: The question branch name that the user RSVPed to.
  • \{{application.question-name}}: Prints the user's response to the application question with the specified name from questions.json. Note that the question name is different from the question label. See the GitHub project for details. Will print N/A if not yet answered.
  • \{{confirmation.question-name}}: Prints the user's response to the confirmation question with the specified name from questions.json.
  • +
  • \{{reimbursementAmount}}: A string representing how much a user should be reimbursed for.
  • diff --git a/package-lock.json b/package-lock.json index 9b491455..cd36d882 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "2.2.2", + "version": "2.2.3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 6cb6e527..fe481bae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "2.2.2", + "version": "2.2.3", "description": "Powerful and extensible registration system for hackathons and other large events", "main": "server/app.js", "scripts": { diff --git a/server/common.ts b/server/common.ts index 5e21c212..75fbbed1 100644 --- a/server/common.ts +++ b/server/common.ts @@ -458,6 +458,11 @@ export async function renderEmailHTML(markdown: string, user: IUser): Promise("User", new mongoose.Schema({ preConfirmEmailSent: Boolean, confirmed: Boolean, applicationBranch: String, + reimbursementAmount: String, applicationData: [mongoose.Schema.Types.Mixed], applicationStartTime: Date, applicationSubmitTime: Date, From efc40136a5f88d24ed8d90ff2fb51bc2fe4725ba Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Wed, 22 Aug 2018 00:07:38 -0400 Subject: [PATCH 097/194] Escape branch names before interpolating into URL --- client/js/admin.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/js/admin.ts b/client/js/admin.ts index 5195da8b..047d14a4 100644 --- a/client/js/admin.ts +++ b/client/js/admin.ts @@ -571,7 +571,7 @@ markdownEditor.codemirror.on("change", async () => { content.append("content", markdownEditor.value()); let { html, text }: { html: string; text: string } = ( - await fetch(`/api/settings/email_content/${emailTypeSelect.value}/rendered`, { + await fetch(`/api/settings/email_content/${encodeURIComponent(emailTypeSelect.value)}/rendered`, { credentials: "same-origin", method: "POST", body: content @@ -601,7 +601,7 @@ async function emailTypeChange(): Promise { // Load editor content via AJAX try { - let emailSettings: { subject: string; content: string } = await fetch(`/api/settings/email_content/${emailTypeSelect.value}`, { credentials: "same-origin" }).then(checkStatus).then(parseJSON); + let emailSettings: { subject: string; content: string } = await fetch(`/api/settings/email_content/${encodeURIComponent(emailTypeSelect.value)}`, { credentials: "same-origin" }).then(checkStatus).then(parseJSON); emailSubject.value = emailSettings.subject; markdownEditor.value(emailSettings.content); } @@ -750,7 +750,7 @@ function settingsUpdate(e: MouseEvent) { }); }).then(checkStatus).then(parseJSON).then(() => { if (emailTypeSelect.value) { - return fetch(`/api/settings/email_content/${emailTypeSelect.value}`, { + return fetch(`/api/settings/email_content/${encodeURIComponent(emailTypeSelect.value)}`, { ...defaultOptions, body: emailContentData }).then(checkStatus).then(parseJSON); From d1440b0ef70d8b4e8e1b0453f7f2dfb419829860 Mon Sep 17 00:00:00 2001 From: Ehsan Asdar Date: Tue, 21 Aug 2018 23:17:42 -0500 Subject: [PATCH 098/194] Add new JSON api (#230) --- server/routes/api/user.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/server/routes/api/user.ts b/server/routes/api/user.ts index e3e5caa3..b8143a9a 100644 --- a/server/routes/api/user.ts +++ b/server/routes/api/user.ts @@ -3,6 +3,7 @@ import * as express from "express"; import * as json2csv from "json2csv"; import * as archiver from "archiver"; import * as uuid from "uuid/v4"; +import * as bodyParser from "body-parser"; import { STORAGE_ENGINE, @@ -325,6 +326,32 @@ userRoutes.route("/status").post(isAdmin, uploadHandler.any(), async (request, r } }); +userRoutes.route("/status_api").post(isAdmin, bodyParser.json(), async (request, response): Promise => { + let user = await User.findOne({uuid: request.params.uuid}); + let status = request.body.status as string; + + if (!user) { + response.status(400).json({ + "error": `No such user with id ${request.params.uuid}` + }); + return; + } + + try { + await updateUserStatus(user, status); + await user.save(); + response.status(200).json({ + "success": true + }); + } + catch (err) { + console.error(err); + response.status(500).json({ + "error": "An error occurred while changing user status" + }); + } +}); + async function updateUserStatus(user: IUserMongoose, status: string): Promise { if (status === "no-decision") { // Clear all confirmation data From 05504f8b4992e95e0b2f293df9c2249c51ea8115 Mon Sep 17 00:00:00 2001 From: Ehsan Asdar Date: Sun, 26 Aug 2018 22:43:51 -0400 Subject: [PATCH 099/194] Revert "Add new JSON api (#230)" (#232) This reverts commit d1440b0ef70d8b4e8e1b0453f7f2dfb419829860. --- server/routes/api/user.ts | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/server/routes/api/user.ts b/server/routes/api/user.ts index b8143a9a..e3e5caa3 100644 --- a/server/routes/api/user.ts +++ b/server/routes/api/user.ts @@ -3,7 +3,6 @@ import * as express from "express"; import * as json2csv from "json2csv"; import * as archiver from "archiver"; import * as uuid from "uuid/v4"; -import * as bodyParser from "body-parser"; import { STORAGE_ENGINE, @@ -326,32 +325,6 @@ userRoutes.route("/status").post(isAdmin, uploadHandler.any(), async (request, r } }); -userRoutes.route("/status_api").post(isAdmin, bodyParser.json(), async (request, response): Promise => { - let user = await User.findOne({uuid: request.params.uuid}); - let status = request.body.status as string; - - if (!user) { - response.status(400).json({ - "error": `No such user with id ${request.params.uuid}` - }); - return; - } - - try { - await updateUserStatus(user, status); - await user.save(); - response.status(200).json({ - "success": true - }); - } - catch (err) { - console.error(err); - response.status(500).json({ - "error": "An error occurred while changing user status" - }); - } -}); - async function updateUserStatus(user: IUserMongoose, status: string): Promise { if (status === "no-decision") { // Clear all confirmation data From 4841e1f046141c57d377503936a739ef8a55de2d Mon Sep 17 00:00:00 2001 From: Ehsan Asdar Date: Tue, 28 Aug 2018 01:03:45 -0400 Subject: [PATCH 100/194] Add support for sending ad hoc batch emails (#234) * Add support for sending adhoc batch emails We can slice by different groups: All users with an account Users that haven't submitted an application Per Application Branch: Users that have submitted that application branch Per Confirmation Branch: Users that have not submitted their confirmation Users that have submitted their confirmation Closes #193, based on #195 by @mjkaufer. * Updates * Show branch type in dropdown * HTML tweaks * Bump version * Use batch email to speed up acceptance emails --- client/admin.html | 39 ++++++++++++ client/js/admin.ts | 113 +++++++++++++++++++++++++++++++++- package-lock.json | 2 +- package.json | 2 +- server/common.ts | 22 +++---- server/routes/api/settings.ts | 50 ++++++++++++++- server/routes/api/user.ts | 16 ++--- 7 files changed, 222 insertions(+), 22 deletions(-) diff --git a/client/admin.html b/client/admin.html index 766f8a3a..e30a8ad8 100644 --- a/client/admin.html +++ b/client/admin.html @@ -35,6 +35,7 @@

    Admin Panel

    Statistics Users Applicants + Emails Settings @@ -412,6 +413,44 @@

    config.json options

    +
    +

    Batch Emails

    + + + + + +
    Rendered HTML and text:
    +
    + +
    List of variables:
    +
      +
    • \{{eventName}}: The name of the event, configured by the eventName key-value pair in the config.json and displayed at the top of the page.
    • +
    • \{{email}}: The user's email as reported by them or a 3rd party OAuth provider (i.e. Google, GitHub, Facebook).
    • +
    • \{{name}}: The user's name as reported by them or a 3rd party OAuth provider (i.e. Google, GitHub, Facebook).
    • +
    • \{{teamName}}: The user's team name if teams are enabled and the user has joined a team. Otherwise, will output Teams not enabled or No team created or joined respectively.
    • +
    • \{{applicationBranch}}: The question branch name that the user applied / was accepted to.
    • +
    • \{{confirmationBranch}}: The question branch name that the user RSVPed to.
    • +
    • \{{application.question-name}}: Prints the user's response to the application question with the specified name from questions.json. Note that the question name is different from the question label. See the GitHub project for details. Will print N/A if not yet answered.
    • +
    • \{{confirmation.question-name}}: Prints the user's response to the confirmation question with the specified name from questions.json.
    • +
    • \{{reimbursementAmount}}: A string representing how much a user should be reimbursed for.
    • +
    +
    + +
    +
    {{/sidebar}} diff --git a/client/js/admin.ts b/client/js/admin.ts index 047d14a4..0185de81 100644 --- a/client/js/admin.ts +++ b/client/js/admin.ts @@ -25,7 +25,61 @@ class State { this.sectionElement.style.display = "block"; } } -const states: State[] = ["statistics", "users", "applicants", "settings"].map(id => new State(id)); +const states: State[] = ["statistics", "users", "applicants", "settings", "emails"].map(id => new State(id)); + +function generateFilter(branchFilter: HTMLInputElement, statusFilter: HTMLInputElement) { + let filter: any = {}; + if (branchFilter.value !== "*" && branchFilter.value !== "na") { + let [, type, branchName] = branchFilter.value.match(/^(application|confirmation)-(.*)$/)!; + if (type === "application") { + filter.applicationBranch = branchName; + } + else if (type === "confirmation") { + filter.confirmationBranch = branchName; + } + switch (statusFilter.value) { + case "no-submission": + if (type === "confirmation") { + filter.confirmed = false; + } + break; + case "submitted": + if (type === "confirmation") { + filter.confirmed = true; + } else { + filter.applied = true; + } + break; + } + } else if (branchFilter.value === "na") { + filter.applied = false; + } + return filter; +} +const batchEmailBranchFilterSelect = document.getElementById("email-branch-filter") as HTMLSelectElement; +const batchEmailStatusFilterSelect = document.getElementById("email-status-filter") as HTMLSelectElement; +async function batchEmailTypeChange(): Promise { + if (batchEmailBranchFilterSelect.value === "*" || batchEmailBranchFilterSelect.value === "na") { + batchEmailStatusFilterSelect.style.display = "none"; + } else { + for (let i = 0; i < batchEmailBranchFilterSelect.options.length; i++) { + batchEmailStatusFilterSelect.options.remove(0); + } + batchEmailStatusFilterSelect.style.display = "block"; + let [, type ] = batchEmailBranchFilterSelect.value.match(/^(application|confirmation)-(.*)$/)!; + // Only confirmation branches have no-submission option since confirmation is manually assigned + if (type === "confirmation") { + let noSubmission = new Option("Have not submitted (Confirmation)", "no-submission"); + batchEmailStatusFilterSelect.add(noSubmission); + } + let submitted = new Option(`Submitted (${type.charAt(0).toUpperCase() + type.slice(1)})`, "submitted"); + batchEmailStatusFilterSelect.add(submitted); + } +} +batchEmailBranchFilterSelect.addEventListener("change", batchEmailTypeChange); +batchEmailTypeChange().catch(err => { + console.error(err); +}); class UserEntries { private static readonly NODE_COUNT = 20; @@ -833,3 +887,60 @@ for (let i = 0; i < data.length; i++) { } }); } + +let emailBranchFilter = document.getElementById("email-branch-filter") as HTMLInputElement; +let emailStatusFilter = document.getElementById("email-status-filter") as HTMLInputElement; +let sendEmailButton = document.getElementById("sendEmail") as HTMLButtonElement; +let batchEmailSubject = document.getElementById("batch-email-subject") as HTMLInputElement; +let batchEmailEditor = new SimpleMDE({ element: document.getElementById("batch-email-content")! }); +let batchEmailRenderedArea: HTMLElement | ShadowRoot = document.getElementById("batch-email-rendered") as HTMLElement; +if (document.head.attachShadow) { + // Browser supports Shadow DOM + batchEmailRenderedArea = batchEmailRenderedArea.attachShadow({ mode: "open" }); +} +batchEmailEditor.codemirror.on("change", async () => { + try { + let content = new FormData(); + content.append("content", batchEmailEditor.value()); + let { html, text }: { html: string; text: string } = ( + await fetch(`/api/settings/email_content/batch_email/rendered`, { + credentials: "same-origin", + method: "POST", + body: content + }).then(checkStatus).then(parseJSON) + ); + batchEmailRenderedArea.innerHTML = html; + let hr = document.createElement("hr"); + hr.style.border = "1px solid #737373"; + batchEmailRenderedArea.appendChild(hr); + let textContainer = document.createElement("pre"); + textContainer.textContent = text; + batchEmailRenderedArea.appendChild(textContainer); + } + catch { + batchEmailRenderedArea.textContent = "Couldn't retrieve email content"; + } +}); +sendEmailButton.addEventListener("click", () => { + let subject = batchEmailSubject.value; + let markdownContent = batchEmailEditor.value(); + if (subject === "") { + return sweetAlert("Oh no!", "You need an email subject", "error"); + } else if (markdownContent === "") { + return sweetAlert("Oh no!", "Your email body is empty.", "error"); + } + let filter = generateFilter(emailBranchFilter, emailStatusFilter); + let content = new FormData(); + content.append("filter", JSON.stringify(filter)); + content.append("subject", subject); + content.append("markdownContent", markdownContent); + sendEmailButton.disabled = true; + return fetch(`/api/settings/send_batch_email`, { + credentials: "same-origin", + method: "POST", + body: content + }).then(checkStatus).then(parseJSON).then((result: {success: boolean; count: number} ) => { + sendEmailButton.disabled = false; + sweetAlert("Success!", `Successfully sent ${result.count} email(s)!`, "success"); + }); +}); diff --git a/package-lock.json b/package-lock.json index cd36d882..0fb7f728 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "2.2.3", + "version": "2.3.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index fe481bae..4020dda7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "2.2.3", + "version": "2.3.0", "description": "Powerful and extensible registration system for hackathons and other large events", "main": "server/app.js", "scripts": { diff --git a/server/common.ts b/server/common.ts index 75fbbed1..7fccbea3 100644 --- a/server/common.ts +++ b/server/common.ts @@ -389,18 +389,22 @@ export const defaultEmailSubjects = { preConfirm: `[${config.eventName}] - Application Update`, attend: `[${config.eventName}] - Thank you for RSVPing!` }; -interface IMailObject { +export interface IMailObject { to: string; from: string; subject: string; html: string; text: string; } -export async function sendMailAsync(mail: IMailObject): Promise { - await sendgrid.send(mail); +// Union types don't work well with overloaded method resolution in Typescript so we split into two methods +export async function sendMailAsync(mail: IMailObject) { + return sendgrid.send(mail); } -export function sanitize(input: string): string { - if (typeof input !== "string") { +export async function sendBatchMailAsync(mail: IMailObject[]) { + return sendgrid.send(mail); +} +export function sanitize(input?: string): string { + if (!input || typeof input !== "string") { return ""; } return input.replace(/&/g, "&").replace(//g, ">"); @@ -458,16 +462,12 @@ export async function renderEmailHTML(markdown: string, user: IUser): Promise { let question = user.applicationData.find(data => data.name === name); return formatFormItem(question); diff --git a/server/routes/api/settings.ts b/server/routes/api/settings.ts index 3f3c8277..3ae4f63c 100644 --- a/server/routes/api/settings.ts +++ b/server/routes/api/settings.ts @@ -1,7 +1,7 @@ import * as express from "express"; import { - getSetting, updateSetting, renderEmailHTML, renderEmailText, defaultEmailSubjects + getSetting, updateSetting, renderEmailHTML, renderEmailText, defaultEmailSubjects, sendBatchMailAsync, config, IMailObject } from "../../common"; import { isAdmin, uploadHandler @@ -279,3 +279,51 @@ settingsRoutes.route("/email_content/:type/rendered") }); } }); + +settingsRoutes.route("/send_batch_email") + .post(isAdmin, uploadHandler.any(), async (request, response) => { + let filter = JSON.parse(request.body.filter); + let subject = request.body.subject; + let markdownContent = request.body.markdownContent; + if (typeof filter !== "object") { + return response.status(400).json({ + "error": `Your query '${filter}' is not a valid MongoDB query` + }); + } else if (subject === "" || subject === undefined) { + return response.status(400).json({ + "error": "Can't have an empty subject!" + }); + } else if (markdownContent === "" || markdownContent === undefined) { + return response.status(400).json({ + "error": "Can't have an empty email body!" + }); + } + + let users = await User.find(filter); + let emails: IMailObject[] = []; + for (let user of users) { + let html: string = await renderEmailHTML(markdownContent, user); + let text: string = await renderEmailText(html, user, true); + + emails.push({ + from: config.email.from, + to: user.email, + subject, + html, + text + }); + } + try { + await sendBatchMailAsync(emails); + } catch (e) { + console.error(e); + return response.status(500).json({ + "error": "Error sending email!" + }); + } + console.log(`Sent ${emails.length} batch emails requested by ${(request.user as IUser).email}`); + return response.json({ + "success": true, + "count": emails.length + }); + }); diff --git a/server/routes/api/user.ts b/server/routes/api/user.ts index e3e5caa3..b744a82e 100644 --- a/server/routes/api/user.ts +++ b/server/routes/api/user.ts @@ -7,7 +7,7 @@ import * as uuid from "uuid/v4"; import { STORAGE_ENGINE, formatSize, - config, getSetting, renderEmailHTML, renderEmailText, sendMailAsync, defaultEmailSubjects + config, getSetting, renderEmailHTML, renderEmailText, sendMailAsync, defaultEmailSubjects, IMailObject, sendBatchMailAsync } from "../../common"; import { MAX_FILE_SIZE, postParser, uploadHandler, @@ -379,6 +379,7 @@ async function updateUserStatus(user: IUserMongoose, status: string): Promise => { try { let users = await User.find({ "confirmationBranch": {$exists: true}, "preConfirmEmailSent": { $ne: true } }); + let emails: IMailObject[] = []; for (let user of users) { // Email the applicant about their acceptance let emailSubject: string | null; @@ -397,24 +398,25 @@ userRoutes.route("/send_acceptances").post(isAdmin, async (request, response): P emailMarkdown = ""; } - let emailHTML = await renderEmailHTML(emailMarkdown, user); - let emailText = await renderEmailText(emailHTML, user, true); + let html = await renderEmailHTML(emailMarkdown, user); + let text = await renderEmailText(html, user, true); - await sendMailAsync({ + emails.push({ from: config.email.from, to: user.email, subject: emailSubject || defaultEmailSubjects.preConfirm, - html: emailHTML, - text: emailText + html, + text }); user.preConfirmEmailSent = true; await user.save(); } + await sendBatchMailAsync(emails); response.json({ "success": true, - "count": users.length + "count": emails.length }); } catch (err) { From 5d1a029f426523943f6fab553f1bbf92ae30e695 Mon Sep 17 00:00:00 2001 From: Kexin Zhang Date: Wed, 29 Aug 2018 14:56:18 -0400 Subject: [PATCH 101/194] Add ability to query for multiple users by list of IDs (#235) * add optional ids field to graphql users query * bump version --- api.graphql | 2 +- package-lock.json | 2 +- package.json | 2 +- server/routes/api/graphql.ts | 6 ++++++ 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/api.graphql b/api.graphql index ab31beab..e582f152 100644 --- a/api.graphql +++ b/api.graphql @@ -11,7 +11,7 @@ type Query { # All the users in the database, useful for polling for new user information. # This is paginated, n is the number of results, and last_id is the last ID # seen from the latest page retrieved, if you want the first page leave this out. - users(pagination_token: ID, n: Int!, filter: UserFilter): [User!]! + users(pagination_token: ID, n: Int!, filter: UserFilter, ids: [String]): [User!]! # Search for users by name and email search_user(search: String!, use_regex: Boolean = false, offset: Int!, n: Int!, filter: UserFilter): SearchResult! # Simplified search_user that can be forwarded correctly from checkin2 diff --git a/package-lock.json b/package-lock.json index 0fb7f728..97c1b36f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "2.3.0", + "version": "2.3.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 4020dda7..b19b0e5b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "2.3.0", + "version": "2.3.1", "description": "Powerful and extensible registration system for hackathons and other large events", "main": "server/app.js", "scripts": { diff --git a/server/routes/api/graphql.ts b/server/routes/api/graphql.ts index cf748b9e..952ddc1c 100644 --- a/server/routes/api/graphql.ts +++ b/server/routes/api/graphql.ts @@ -40,9 +40,15 @@ const resolvers: IResolver = { $gt: args.pagination_token } } : {}; + const uuidQuery = args.ids ? { + uuid: { + $in: args.ids + } + } : {}; const allUsers = await User .find({ ...lastIdQuery, + ...uuidQuery, ...userFilterToMongo(args.filter) }) .limit(args.n); From 1a0a88b73c636b84bbc03ad0f40cf153d14d238e Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Wed, 5 Sep 2018 00:03:26 -0400 Subject: [PATCH 102/194] Fix timeline on medium sized screens --- client/css/index.css | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/client/css/index.css b/client/css/index.css index 54537bff..cdaa3cd5 100644 --- a/client/css/index.css +++ b/client/css/index.css @@ -46,6 +46,7 @@ ul.timeline { align-items: center; justify-content: center; padding: 0; + overflow-x: hidden; } ul.timeline > li { position: relative; @@ -53,11 +54,12 @@ ul.timeline > li { ul.timeline > li::before { content: attr(data-pending); margin-bottom: 20px; - padding: 0 40px; + padding: 0 15px; font-weight: 100; display: flex; flex-direction: column; align-items: center; + white-space: nowrap; } ul.timeline > li > div::before { display: block; @@ -71,8 +73,8 @@ ul.timeline > li > div::before { top: 31px; } ul.timeline > li > div { - height: 85px; - padding: 0 40px; + height: 95px; + padding: 0 10px; display: flex; justify-content: center; border-top: 2px solid #D6DCE0; @@ -122,6 +124,7 @@ ul.timeline > li > div > h4 { @media screen and (max-width: 768px) { ul.timeline { flex-direction: column; + overflow-x: visible; } ul.timeline > li { width: 100%; From ca61481061276420b2f9ddf56bcf004f5e0c7d5e Mon Sep 17 00:00:00 2001 From: Ehsan Asdar Date: Thu, 6 Sep 2018 22:01:52 -0400 Subject: [PATCH 103/194] Add admin fyi for batch emails (#236) * Add admin fyi for batch emails * Remove extra newline * Fix tslint * Bump version --- package-lock.json | 2 +- package.json | 2 +- server/routes/api/settings.ts | 18 +++++++++++++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 97c1b36f..46287f37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "2.3.1", + "version": "2.3.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index b19b0e5b..388ff05a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "2.3.1", + "version": "2.3.2", "description": "Powerful and extensible registration system for hackathons and other large events", "main": "server/app.js", "scripts": { diff --git a/server/routes/api/settings.ts b/server/routes/api/settings.ts index 3ae4f63c..439b1db6 100644 --- a/server/routes/api/settings.ts +++ b/server/routes/api/settings.ts @@ -283,7 +283,7 @@ settingsRoutes.route("/email_content/:type/rendered") settingsRoutes.route("/send_batch_email") .post(isAdmin, uploadHandler.any(), async (request, response) => { let filter = JSON.parse(request.body.filter); - let subject = request.body.subject; + let subject = request.body.subject as string; let markdownContent = request.body.markdownContent; if (typeof filter !== "object") { return response.status(400).json({ @@ -313,6 +313,22 @@ settingsRoutes.route("/send_batch_email") text }); } + + let admins = await User.find({ admin: true }); + for (let user of admins) { + let html: string = await renderEmailHTML(markdownContent, user); + let text: string = await renderEmailText(html, user, true); + subject = `[Admin FYI] ${subject}`; + text = `${filter.toString()}\n${text}`; + text = `${filter.toString()}
    ${text}`; + emails.push({ + from: config.email.from, + to: user.email, + subject, + html, + text + }); + } try { await sendBatchMailAsync(emails); } catch (e) { From 45a80c4000c4f536b5f444c65de39f55e0f7bb6f Mon Sep 17 00:00:00 2001 From: Ehsan Asdar Date: Thu, 6 Sep 2018 22:17:24 -0400 Subject: [PATCH 104/194] Bug fix for admin FYI (#237) --- server/routes/api/settings.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/routes/api/settings.ts b/server/routes/api/settings.ts index 439b1db6..6cab8cd5 100644 --- a/server/routes/api/settings.ts +++ b/server/routes/api/settings.ts @@ -315,12 +315,12 @@ settingsRoutes.route("/send_batch_email") } let admins = await User.find({ admin: true }); + subject = `[Admin FYI] ${subject}`; for (let user of admins) { let html: string = await renderEmailHTML(markdownContent, user); let text: string = await renderEmailText(html, user, true); - subject = `[Admin FYI] ${subject}`; - text = `${filter.toString()}\n${text}`; - text = `${filter.toString()}
    ${text}`; + text = `${JSON.stringify(filter)}\n${text}`; + html = `${JSON.stringify(filter)}
    ${text}`; emails.push({ from: config.email.from, to: user.email, From a704923da479dfe3e32e628368716d3bb1635bda Mon Sep 17 00:00:00 2001 From: Ehsan Asdar Date: Tue, 11 Sep 2018 18:53:27 -0400 Subject: [PATCH 105/194] Switch over registration from Travis CI -> Cloud Build (#238) * This switches over registration from Travis CI -> Cloud Build Piloting in registration, if this works well we can move it to other repos. * Remove .travis.yml * Update to latest template --- .travis.d/Gemfile | 5 - .travis.d/build.sh | 219 ------------------------------------- .travis.d/pr_autodeploy.rb | 135 ----------------------- .travis.yml | 18 --- cloudbuild.yaml | 26 +++++ 5 files changed, 26 insertions(+), 377 deletions(-) delete mode 100644 .travis.d/Gemfile delete mode 100755 .travis.d/build.sh delete mode 100644 .travis.d/pr_autodeploy.rb delete mode 100644 .travis.yml create mode 100644 cloudbuild.yaml diff --git a/.travis.d/Gemfile b/.travis.d/Gemfile deleted file mode 100644 index 5a1ec47e..00000000 --- a/.travis.d/Gemfile +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true -source 'https://rubygems.org' - -gem 'git', '~> 1.3' -gem 'octokit', '~> 4.0' diff --git a/.travis.d/build.sh b/.travis.d/build.sh deleted file mode 100755 index f9f03802..00000000 --- a/.travis.d/build.sh +++ /dev/null @@ -1,219 +0,0 @@ -#!/usr/bin/env bash -# HACKGPROJECT VERSION: a4089e26eea6f51517a0e00330e1296bb05cfef4 -set -euo pipefail -PROJECT_TYPE="deployment" -ORG_NAME_CASE_PRESERVE="HackGT" -ORG_NAME=$(echo "${ORG_NAME_CASE_PRESERVE}" | tr '[:upper:]' '[:lower:]') -SOURCE_DIR=$(readlink -f "${BASH_SOURCE[0]}") -SOURCE_DIR=$(dirname "$SOURCE_DIR") -cd "${SOURCE_DIR}/.." -set -x - -if ! hash docker &>/dev/null; then - echo 'Cannot find the docker binary!' >&2 - exit 64 -fi - -docker= -if docker ps &>/dev/null; then - docker=docker -else - docker='sudo docker' -fi - -remote=$(git remote -v | grep -Pio "${ORG_NAME}"'/[a-zA-Z0-9-_\.]*' | head -1) -image_name=$(basename "${remote%.*}") -image_name=$(echo "$image_name" | tr '[:upper:]' '[:lower:]') - -build_project_source() { - if [[ -f Dockerfile.build ]]; then - local build_image_name - build_image_name="$(basename "$(pwd)")-build" - $docker build -f Dockerfile.build --rm -t "$build_image_name" . - $docker run -w '/src' -v "$(pwd):/src" "$build_image_name" - sudo chown -R "$(id -u):$(id -g)" . - fi -} - -test_project_source() { - if [[ -f Dockerfile.test ]]; then - local test_image_name - test_image_name="$(basename "$(pwd)")-test" - $docker build -f Dockerfile.test --rm -t "$test_image_name" . - $docker run -w '/src' -v "$(pwd):/src" "$test_image_name" - sudo chown -R "$(id -u):$(id -g)" . - fi -} - -build_project_container() { - $docker build -f Dockerfile --rm -t "$image_name" . -} - -git_branch() { - if [[ ${TRAVIS_PULL_REQUEST_BRANCH} ]]; then - echo "${TRAVIS_PULL_REQUEST_BRANCH}" - else - echo "${TRAVIS_BRANCH:-$(git rev-parse --abbrev-ref HEAD)}" - fi -} - -git_branch_id() { - git_branch | sed 's/[^0-9a-zA-Z_.-]/-/g' -} - -publish_project_container() { - local git_rev - local branch - git_rev=$(git rev-parse HEAD) - branch=$(git_branch_id) - local latest_tag_name="latest" - local push_image_name="${DOCKER_ID_USER}/${image_name}" - if [[ $branch != master ]]; then - latest_tag_name="latest-${branch}" - fi - docker login -u="${DOCKER_ID_USER}" -p="${DOCKER_PASSWORD}" - docker tag "$image_name" "$push_image_name":"$git_rev" - docker push "$push_image_name" - docker tag "$push_image_name":"$git_rev" "$push_image_name":"$latest_tag_name" - docker push "$push_image_name" -} - -trigger_biodomes_build() { - body='{ - "request": { - "branch":"master" - } }' - - curl -s -X POST \ - -H "Content-Type: application/json" \ - -H "Accept: application/json" \ - -H "Travis-API-Version: 3" \ - -H "Authorization: token ${TRAVIS_TOKEN}" \ - -d "$body" \ - https://api.travis-ci.org/repo/${ORG_NAME_CASE_PRESERVE}%2Fbiodomes/requests -} - -commit_to_branch() { - local branch - local git_rev - branch="${1:-gh-pages}" - git_rev=$(git rev-parse --short HEAD) - git config user.name 'HackGBot' - git config user.email 'thehackgt@gmail.com' - git remote remove origin - git remote add origin \ - "https://${GH_TOKEN}@github.com/${ORG_NAME}/${image_name}.git" - git fetch origin - git reset "origin/$branch" || git checkout -b "$branch" - git add -A . - git status - git commit -m "Automatic Travis deploy of ${git_rev}." - git push -q origin "HEAD:${branch}" -} - -set_cloudflare_dns() { - local type="$1" - local name="$2" - local content="$3" - local proxied="$4" - local type_downcase - local name_downcase - local content_downcase - local dns_records - type_downcase=$(echo "${type}" | tr '[:upper:]' '[:lower:]') - name_downcase=$(echo "${name}" | tr '[:upper:]' '[:lower:]') - content_downcase=$(echo "${content}" | tr '[:upper:]' '[:lower:]') - - # get all the dns records - dns_records=$(curl -X GET \ - -H "X-Auth-Email: ${CLOUDFLARE_EMAIL}" \ - -H "X-Auth-Key: ${CLOUDFLARE_AUTH}" \ - -H "Content-Type: application/json" \ - "https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE}/dns_records" \ - | tr '[:upper:]' '[:lower:]') - - # Check if we already set it - local jq_exists - jq_exists=$(cat <<-END - .result[] - | select(.type == "${type_downcase}") - | select(.name == "${name_downcase}") - | select(.content == "${content_downcase}") -END - ) - if [[ -n $(echo "${dns_records}" | jq "${jq_exists}") ]]; then - echo "Record already set, not setting again." - return - fi - - # Check if there's a different one already set - local duplicate_exists - duplicate_exists=$(echo "${dns_records}" \ - | jq '.result[] | select(.name == '"${name_downcase}"')') - if [[ -n $duplicate_exists ]]; then - echo "Record with the same host exists, will not overwrite!" - exit 64 - fi - - # Set IT! - local dns_record - dns_record=$(cat <<-END - { - "type": "${type}", - "name": "${name}", - "content": "${content}", - "proxied": $proxied - } -END - ) - local dns_success - dns_success=$(curl -X POST \ - --data "$dns_record" \ - -H "X-Auth-Email: ${CLOUDFLARE_EMAIL}" \ - -H "X-Auth-Key: ${CLOUDFLARE_AUTH}" \ - -H "Content-Type: application/json" \ - "https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE}/dns_records") - - if [[ $dns_success != true ]]; then - echo 'DNS Setting on cloudflare failed!!' - echo 'CloudFlare output:' - echo "$dns_success" - exit 64 - fi - echo DNS set! You\'ll have to wait a bit to see the changes! -} - - -deployment_project() { - build_project_container - - if [[ ${TRAVIS_PULL_REQUEST:-} = false ]]; then - publish_project_container - trigger_biodomes_build - fi -} - -static_project() { - if [[ ${TRAVIS_BRANCH:-} = master && ${TRAVIS_PULL_REQUEST:-} = false ]]; then - commit_to_branch 'gh-pages' - set_cloudflare_dns CNAME "$(cat CNAME)" "${ORG_NAME}.github.io" true - fi -} - - -# Build & Test the project, if needed. -build_project_source -test_project_source - -case "$PROJECT_TYPE" in - deployment) - deployment_project - ;; - static) - static_project - ;; - *) - echo "Unknown project type!" - exit 1 -esac - diff --git a/.travis.d/pr_autodeploy.rb b/.travis.d/pr_autodeploy.rb deleted file mode 100644 index 736ee987..00000000 --- a/.travis.d/pr_autodeploy.rb +++ /dev/null @@ -1,135 +0,0 @@ -# frozen_string_literal: true -require 'git' -require 'octokit' - -TR_PR_BRANCH = ENV['TRAVIS_PULL_REQUEST_BRANCH'] -GH_TOKEN = ENV['GH_TOKEN'] -ORG_NAME = ENV['ORG_NAME'] || 'HackGT' -GIT_NAME = ENV['GIT_NAME'] || 'HackGBot' -GIT_EMAIL = ENV['GIT_EMAIL'] || 'thehackgt@gmail.com' - -def commit_to_biodomes - path = Dir.mktmpdir - git = Git.clone("https://github.com/#{ORG_NAME}/biodomes.git", - 'biodomes', - path: path, - depth: 1) - # Configure git - git.config('user.name', GIT_NAME) - git.config('user.email', GIT_EMAIL) - # Change origin - git.remote('origin').remove - git.add_remote('origin', - "https://#{GH_TOKEN}@github.com/#{ORG_NAME}/biodomes.git", - fetch: false) - - # do whatever work you want - message = nil - Dir.chdir(File.join(path, 'biodomes')) { || message = yield } - - # commit & push - git.add(all: true) - begin - git.commit(message) - rescue - puts 'Nothing to commit, skipping...' - return - end - git.push -end - -def git_branch - return TR_PR_BRANCH unless TR_PR_BRANCH.nil? - return ENV['TRAVIS_BRANCH'] unless ENV['TRAVIS_BRANCH'].nil? - `git rev-parse --abbrev-ref HEAD`.strip -end - -def git_remote - remotes = `git remote -v`.match %r{#{ORG_NAME}/(.*?)\.git }i - remotes[1] -end - -def git_branch_id(branch) - branch.gsub(/[^0-9a-zA-Z_-]/, '-') -end - -def pr_id(branch) - "#{git_remote}-#{git_branch_id branch}" -end - -def create_biodome_file(branch) - remote = git_remote - data = <<~EOF - git: - remote: "https://github.com/#{ORG_NAME}/#{remote}.git" - branch: "#{branch}" - - secrets-source: git-#{ORG_NAME}-#{remote}-secrets - deployment: - replicas: 1 - strategy: - type: Recreate - EOF - ["pr/#{pr_id branch}.yaml", data.downcase] -end - -def create_message(branch) - <<~EOF - Hey y'all! A deployment of this PR can be found here: - https://#{pr_id branch}.pr.hack.gt - EOF -end - -def pr_digest(github, slug) - github.pulls(slug) - .select { |p| p.state == 'open' } - .map do |p| - { - branch: p.head.ref, - number: p.number - } - end - .uniq -end - -def main - github = Octokit::Client.new(access_token: GH_TOKEN) - - remote = git_remote - slug = "#{ORG_NAME}/#{remote}" - - digest = pr_digest(github, slug) - open_branches = digest.map { |pr| pr[:branch] } - files = open_branches.map { |branch| create_biodome_file(branch) } - - # commit all the right files to biodomes - commit_to_biodomes do - FileUtils.rm_rf(Dir["./pr/#{remote}-*"]) - files.each do |(path, data)| - FileUtils.mkdir_p(File.dirname(path)) - File.write(path, data) - end - - puts `git status` - puts `git diff` - "Automatic #{remote} PR deploys of #{open_branches.join(', ')}." - end - - # Check if this is part of a PR build - current_branch = git_branch - current_pr = digest.find { |pr| pr[:branch] == current_branch } - return if current_pr.nil? - - # Check if a message has already been written - message = create_message current_branch - comment_written = - github - .issue_comments(slug, current_pr[:number]) - .find { |comment| comment.body.gsub(/\s+/, '') == message.gsub(/\s+/, '') } - return unless comment_written.nil? - - # Write a message - github.add_comment(slug, current_pr[:number], message) -end - -main diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 751ae1f0..00000000 --- a/.travis.yml +++ /dev/null @@ -1,18 +0,0 @@ -dist: trusty -language: ruby -sudo: required -services: -- docker -script: | - .travis.d/build.sh && - cd .travis.d && bundle install --path vendor/bundle && - bundle exec ruby pr_autodeploy.rb -env: - global: - - HACKGPROJECT_REV="a4089e26eea6f51517a0e00330e1296bb05cfef4" - - DOCKER_ID_USER="hackgt" -notifications: - slack: - secure: pjf6fz9v2uA9HZwSrdnZLQjnmwW3uiVHP6aviKzqmmAMslm9Zx+gUH9S4f3S3pK099neKXvJeoVZXLHUeXAxsoZVsPPvAEIO8U4FzVx1ZiU1FCjaXXM56n3Iw1UmPXx52FcG2Yh+KDgcdLTYoBNidWaC80dqEIi+HbZ0XJQg1RLEKm+c0TLpP6JfLQsfNlS768jBnyOoo0/qyCQzc/w/Xbmm+Zb2fxxC50AncjNDqK1okYXxMKMlHmWhNAx3J+3dB89OdV6vWLYibCCdOc/eQV73qtgpF02tt1TeYtIeapFegdPlzg5hHwQ347OujnzKcdbUsCgEVK+AzhBvdu1DP+UPPD2TFyptD+snCjdDVytbPIv+OgXjC9x4h8mrA7Q6N+OZMd7bVOV6jiHmcabYd47hrr3Peg8D56LcQ+YzkdWIbzEjoqFT1NJ+wIK5cv+a9QEVW1e0DK5EsRTkC5N/cZdyZvoFp3ob0js8ak8r6GnWLcN20rz9VsVICrbonZ60puFCjw9r1PGpP6GwccJMarhzIxQ+OxAsiQb00pNE+VMhM/6MGiwNxGRtDHmkC6eNv/AZDt7jYtrXnPnXyPJAUa0NDydZTVXJpdICW703U5BfX3fZTqQs7iD9jEDlJ4lL4up7F7Uq2B4q9gJ6dr9LLVkZb6w+kh5eHJWjvTBPtNY= - on_success: change - on_failure: always diff --git a/cloudbuild.yaml b/cloudbuild.yaml new file mode 100644 index 00000000..b382fc4c --- /dev/null +++ b/cloudbuild.yaml @@ -0,0 +1,26 @@ +steps: +- name: 'gcr.io/cloud-builders/docker' + args: ['build', '-t', 'hackgt/$REPO_NAME:$COMMIT_SHA', '-t', 'hackgt/$REPO_NAME:latest', '.'] + timeout: 120s +- name: 'gcr.io/cloud-builders/docker' + entrypoint: 'bash' + args: ['-c', 'docker login --username=hackgt --password=$$DOCKER_PASSWORD'] + secretEnv: ['DOCKER_PASSWORD'] +- name: 'gcr.io/cloud-builders/docker' + args: ['push', 'hackgt/$REPO_NAME:$COMMIT_SHA'] +- name: 'gcr.io/cloud-builders/docker' + args: ['push', 'hackgt/$REPO_NAME:latest'] +- name: 'hackgt/builder-trigger-beehive-build' + secretEnv: ['TRAVIS_TOKEN'] +- name: 'hackgt/builder-pr-autodeploy' + secretEnv: ['GH_TOKEN'] + env: + - 'REPO_NAME=$REPO_NAME' + - 'BRANCH_NAME=$BRANCH_NAME' +timeout: 180s +secrets: +- kmsKeyName: projects/hackgt-cluster/locations/global/keyRings/hackgt-build/cryptoKeys/hackgt-build-key + secretEnv: + DOCKER_PASSWORD: CiQATk4Dt7S6ki9JbJvw22WkKHsdyQhEOflZeK/1IJz/rJ18LmASNQD6KHO+IyjkNS/GHaDxJuLmbFXCr1kU5xzVVHaePe1CZeI1P1J/vyO3y1lXvSJpyID0hoXm + TRAVIS_TOKEN: CiQATk4Dt3x0+R2zKUvt090ul3dDhcZUnYsGoQfOzSjKquHVwiUSPwD6KHO+mVuFK9L8KYnFzg20D24Ih3FPujmuEiNPugBU0qTlTSQFcEMQrv93l8sLPlW8E8k0cg3v0mZqI721Rg== + GH_TOKEN: CiQATk4Dt+aCWrSvmOjyeFMx1cE6IdyMcp/9QICxGVymKcwNRnUSUQD6KHO+AF+uv67L1thZYzN4WY9U/UYDy69RR8KUEKNwfiUW2vxtvLOjMawfj4NHEnIthVC6Z9aWqb3In6l+/+mcKh5ZgVFKrWY8Whh+LEZGGA== \ No newline at end of file From b248d4f997498c080e46f1cc4f20c76f4cdc29bc Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Sat, 15 Sep 2018 13:02:54 -0400 Subject: [PATCH 106/194] Fix headers already sent error when account is already confirmed --- server/routes/templates.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/routes/templates.ts b/server/routes/templates.ts index 3f5c6e5e..38bf9f1c 100644 --- a/server/routes/templates.ts +++ b/server/routes/templates.ts @@ -334,6 +334,7 @@ templateRoutes.route("/login/confirm").get(async (request, response) => { } if (user.accountConfirmed) { response.redirect("/"); + return; } let usedLoginMethods: string[] = []; From 9f4cb95f7d11f279ad52d598ea162bed33428e02 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Sat, 15 Sep 2018 13:47:49 -0400 Subject: [PATCH 107/194] Trim emails to prevent duplicate key errors from emails that differ by whitespace --- server/routes/strategies.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/routes/strategies.ts b/server/routes/strategies.ts index cd60933e..d0061900 100644 --- a/server/routes/strategies.ts +++ b/server/routes/strategies.ts @@ -109,7 +109,7 @@ abstract class OAuthStrategy implements RegistrationStrategy { let email: string = ""; if (profile.emails && profile.emails.length > 0) { - email = profile.emails[0].value; + email = profile.emails[0].value.trim(); } else if (!profile.emails || profile.emails.length === 0) { done(null, false, { message: "Your GitHub profile does not have any public email addresses. Please make an email address public before logging in with GitHub." }); @@ -274,6 +274,7 @@ abstract class CASStrategy implements RegistrationStrategy { private async passportCallback(request: Request, username: string, profile: Profile, done: PassportDone) { let loggedInUser = request.user as IUserMongoose | undefined; + username = username.trim(); let user = await User.findOne({[`services.${this.name}.id`]: username}); let email = `${username}@${this.emailDomain}`; let isAdmin = false; @@ -395,6 +396,7 @@ export class Local implements RegistrationStrategy { } protected async passportCallback(request: Request, email: string, password: string, done: PassportDone) { + email = email.trim(); let user = await User.findOne({ email }); if (user && request.path.match(/\/signup$/i)) { done(null, false, { "message": "That email address is already in use. You may already have an account from another login service." }); From 5afb01532116fe33ebe634d7c2c814685eb2731f Mon Sep 17 00:00:00 2001 From: Ehsan Asdar Date: Thu, 27 Sep 2018 19:28:49 -0400 Subject: [PATCH 108/194] Only send batch email to verified accounts (#240) --- server/routes/api/settings.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/routes/api/settings.ts b/server/routes/api/settings.ts index 6cab8cd5..5878f96f 100644 --- a/server/routes/api/settings.ts +++ b/server/routes/api/settings.ts @@ -283,6 +283,8 @@ settingsRoutes.route("/email_content/:type/rendered") settingsRoutes.route("/send_batch_email") .post(isAdmin, uploadHandler.any(), async (request, response) => { let filter = JSON.parse(request.body.filter); + filter.verifiedEmail = true; + filter.accountConfirmed = true; let subject = request.body.subject as string; let markdownContent = request.body.markdownContent; if (typeof filter !== "object") { From 1a618554178343c5a8d74062e08af4eff87ee692 Mon Sep 17 00:00:00 2001 From: Ehsan Asdar Date: Thu, 27 Sep 2018 20:34:19 -0400 Subject: [PATCH 109/194] Fix admin emails (#242) --- server/routes/api/settings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routes/api/settings.ts b/server/routes/api/settings.ts index 5878f96f..400e8c3c 100644 --- a/server/routes/api/settings.ts +++ b/server/routes/api/settings.ts @@ -321,8 +321,8 @@ settingsRoutes.route("/send_batch_email") for (let user of admins) { let html: string = await renderEmailHTML(markdownContent, user); let text: string = await renderEmailText(html, user, true); + html = `${JSON.stringify(filter)}
    ${html}`; text = `${JSON.stringify(filter)}\n${text}`; - html = `${JSON.stringify(filter)}
    ${text}`; emails.push({ from: config.email.from, to: user.email, From 86e5f831204760c8081f81770e866de148bfd61e Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Fri, 28 Sep 2018 16:44:29 -0400 Subject: [PATCH 110/194] Fix GT CAS capitalization bug --- server/routes/strategies.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/routes/strategies.ts b/server/routes/strategies.ts index d0061900..e94bbc30 100644 --- a/server/routes/strategies.ts +++ b/server/routes/strategies.ts @@ -273,8 +273,9 @@ abstract class CASStrategy implements RegistrationStrategy { } private async passportCallback(request: Request, username: string, profile: Profile, done: PassportDone) { + // GT login will pass long invalid usernames of different capitalizations + username = username.toLowerCase().trim(); let loggedInUser = request.user as IUserMongoose | undefined; - username = username.trim(); let user = await User.findOne({[`services.${this.name}.id`]: username}); let email = `${username}@${this.emailDomain}`; let isAdmin = false; From 44c981aa9bb4e609a9a342da012c699934ca10cb Mon Sep 17 00:00:00 2001 From: Ehsan Asdar Date: Sat, 6 Oct 2018 21:29:12 -0400 Subject: [PATCH 111/194] Tweak wording in client (#243) * Tweak wording in two places * Bump version --- client/admin.html | 2 +- client/index.html | 2 +- package-lock.json | 2 +- package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/admin.html b/client/admin.html index e30a8ad8..1ec4a6ee 100644 --- a/client/admin.html +++ b/client/admin.html @@ -137,7 +137,7 @@

    Users

    Applicants

    - +
    @@ -446,6 +448,9 @@
    List of variables:
  • \{{application.question-name}}: Prints the user's response to the application question with the specified name from questions.json. Note that the question name is different from the question label. See the GitHub project for details. Will print N/A if not yet answered.
  • \{{confirmation.question-name}}: Prints the user's response to the confirmation question with the specified name from questions.json.
  • \{{reimbursementAmount}}: A string representing how much a user should be reimbursed for.
  • +
  • \{{qrURI}}: A data URI + with the user's check-in QR code containing their UUID.
  • +
  • \{{qrCode}}: Image with the user's check-in QR code containing their UUID.
  • diff --git a/package-lock.json b/package-lock.json index 81deb3f2..3149d6b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "2.3.3", + "version": "2.3.4", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -309,6 +309,15 @@ "@types/passport": "*" } }, + "@types/qr-image": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/qr-image/-/qr-image-3.2.1.tgz", + "integrity": "sha512-x8Jv9YUw0wPXviOwX1/QPcLWFv2M9dYAQwYKWs4pqzCcBMWrgjGlrPSpvc9T2YtKitJa/+BkGQnAUgphqdeoeA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/range-parser": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.2.tgz", @@ -369,6 +378,7 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.3.tgz", "integrity": "sha512-5fRLCYhLtDb3hMWqQyH10qtF+Ud2JnNCXTCZ+9ktNdCcgslcuXkDTkFcJNk++MT29yDntDnlF1+jD+uVGumsbw==", + "dev": true, "requires": { "@types/node": "*" } @@ -946,12 +956,12 @@ }, "lodash": { "version": "3.10.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "resolved": "http://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" }, "readable-stream": { "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", "requires": { "core-util-is": "~1.0.0", @@ -1864,9 +1874,9 @@ }, "dependencies": { "es6-promise": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz", - "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==" + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.5.tgz", + "integrity": "sha512-n6wvpdE43VFtJq+lUDYDBFUwV8TZbuGXLV4D6wKafg13ldznKsyEvatubnmUe31zcvelSzOHF+XbaT+Bl9ObDg==" } } }, @@ -2833,6 +2843,11 @@ "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" }, + "qr-image": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/qr-image/-/qr-image-3.2.0.tgz", + "integrity": "sha1-n6gpW+rlDEoUnPn5CaHbRkqGcug=" + }, "qs": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", diff --git a/package.json b/package.json index 32622566..62271a42 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "2.3.3", + "version": "2.3.4", "description": "Powerful and extensible registration system for hackathons and other large events", "main": "server/app.js", "scripts": { @@ -27,7 +27,6 @@ "private": true, "dependencies": { "@sendgrid/mail": "^6.2.1", - "@types/uuid": "^3.4.3", "ajv": "^5.5.2", "archiver": "^1.3.0", "aws-sdk": "^2.255.1", @@ -62,6 +61,7 @@ "passport-github2": "^0.1.10", "passport-google-oauth20": "^1.0.0", "passport-local": "^1.0.0", + "qr-image": "^3.2.0", "serve-static": "^1.13.2", "striptags": "^3.1.1", "sweetalert2": "^6.11.5", @@ -95,9 +95,11 @@ "@types/passport-facebook": "^2.1.8", "@types/passport-github2": "^1.1.3", "@types/passport-local": "^1.0.33", + "@types/qr-image": "^3.2.1", "@types/serve-static": "^1.13.2", "@types/supertest": "^2.0.4", "@types/tmp": "0.0.33", + "@types/uuid": "^3.4.3", "@types/whatwg-fetch": "0.0.33", "chai": "^4.0.0", "mocha": "^5.2.0", diff --git a/server/common.ts b/server/common.ts index 7fccbea3..ac5fe9c1 100644 --- a/server/common.ts +++ b/server/common.ts @@ -4,6 +4,7 @@ import * as fs from "fs"; import * as crypto from "crypto"; import * as path from "path"; import * as tmp from "tmp"; +import * as qr from "qr-image"; import "passport"; // @@ -460,9 +461,24 @@ export async function renderEmailHTML(markdown: string, user: IUser): Promise(resolve => { + let qrStream = qr.image(`user:${user.uuid}`); + let buffer: any[] = []; + qrStream.on("data", chunk => { + buffer.push(chunk); + }); + qrStream.on("end", () => { + resolve(Buffer.concat(buffer)); + }); + }); + let qrURI = `data:image/png;base64,${qrBuffer.toString("base64")}`; + let qrMarkdown = `![${user.uuid}](${qrURI})`; + // Interpolate and sanitize variables markdown = markdown.replace(/{{eventName}}/g, sanitize(config.eventName)); markdown = markdown.replace(/{{reimbursementAmount}}/g, sanitize(user.reimbursementAmount)); + markdown = markdown.replace(/{{qrURI}}/g, qrURI); + markdown = markdown.replace(/{{qrCode}}/g, qrMarkdown); markdown = markdown.replace(/{{email}}/g, sanitize(user.email)); markdown = markdown.replace(/{{name}}/g, sanitize(user.name)); markdown = markdown.replace(/{{teamName}}/g, sanitize(teamName)); diff --git a/server/tsconfig.json b/server/tsconfig.json index 349e1b3a..c8ed7565 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -9,7 +9,7 @@ "lib": [ "esnext.asynciterable", "es2017", - "es2015.promise", + "es2015.promise" ], "sourceMap": true, "alwaysStrict": true, From 7e1afdea89fa24b80c1debce4c94979b1b3ce55b Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Tue, 16 Oct 2018 02:08:48 -0400 Subject: [PATCH 114/194] Support non authed linking (#247) * Simplify completed auth redirection code and support non authed linking * Allow redirect to any subpath of registration * Bump version --- package-lock.json | 2 +- package.json | 2 +- server/middleware.ts | 3 +++ server/routes/strategies.ts | 18 +++++++++--------- server/routes/templates.ts | 4 ++++ 5 files changed, 18 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3149d6b1..97e34307 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "2.3.4", + "version": "2.3.5", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 62271a42..8c246f5f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "2.3.4", + "version": "2.3.5", "description": "Powerful and extensible registration system for hackathons and other large events", "main": "server/app.js", "scripts": { diff --git a/server/middleware.ts b/server/middleware.ts index 9e74e041..6e54ed75 100644 --- a/server/middleware.ts +++ b/server/middleware.ts @@ -118,6 +118,9 @@ export function authenticateWithReject(request: express.Request, response: expre export function authenticateWithRedirect(request: express.Request, response: express.Response, next: express.NextFunction) { response.setHeader("Cache-Control", "private"); if (!request.isAuthenticated() || !request.user || !request.user.verifiedEmail || !request.user.accountConfirmed) { + if (request.session) { + request.session.returnTo = request.originalUrl; + } response.redirect("/login"); } else { diff --git a/server/routes/strategies.ts b/server/routes/strategies.ts index e94bbc30..481b6f21 100644 --- a/server/routes/strategies.ts +++ b/server/routes/strategies.ts @@ -226,11 +226,13 @@ abstract class OAuthStrategy implements RegistrationStrategy { passport.authenticate( this.name, - { failureRedirect: "/login", failureFlash: true, callbackURL } as AuthenticateOptions + { + failureRedirect: "/login", + successReturnToOrRedirect: "/", + failureFlash: true, + callbackURL + } as AuthenticateOptions )(request, response, next); - }, (request, response) => { - // Successful authentication, redirect home - response.redirect("/"); }); } } @@ -368,11 +370,9 @@ abstract class CASStrategy implements RegistrationStrategy { authRoutes.get(`/${this.name}`, passport.authenticate(this.name, { failureRedirect: "/login", + successReturnToOrRedirect: "/", failureFlash: true - }), (request, response) => { - // Successful authentication, redirect home - response.redirect("/"); - }); + })); } } @@ -465,7 +465,7 @@ export class Local implements RegistrationStrategy { response.redirect("/login/confirm"); }); - authRoutes.post("/login", postParser, passport.authenticate("local", { failureRedirect: "/login", failureFlash: true, successRedirect: "/" })); + authRoutes.post("/login", postParser, passport.authenticate("local", { failureRedirect: "/login", failureFlash: true, successReturnToOrRedirect: "/" })); authRoutes.get("/verify/:code", async (request, response) => { let user = await User.findOne({ "local.verificationCode": request.params.code }); diff --git a/server/routes/templates.ts b/server/routes/templates.ts index 38bf9f1c..ad627beb 100644 --- a/server/routes/templates.ts +++ b/server/routes/templates.ts @@ -318,6 +318,10 @@ templateRoutes.route("/").get(authenticateWithRedirect, async (request, response }); templateRoutes.route("/login").get(async (request, response) => { + // Allow redirect to any subpath of registration + if (request.session && request.query.r && request.query.r.startsWith('/')) { + request.session.returnTo = request.query.r; + } let templateData: ILoginTemplate = { siteTitle: config.eventName, error: request.flash("error"), From f9722eba48fa2f44a5f5116a330189198d84a7fa Mon Sep 17 00:00:00 2001 From: Ehsan Asdar Date: Wed, 17 Oct 2018 10:35:38 -0400 Subject: [PATCH 115/194] Add admin status to the user info endpoint (#248) --- server/routes/api/user.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/routes/api/user.ts b/server/routes/api/user.ts index c5ea7852..1167d71f 100644 --- a/server/routes/api/user.ts +++ b/server/routes/api/user.ts @@ -683,6 +683,7 @@ userRoutes.get('/', isUserOrAdmin, async (request, response) => { response.json({ uuid: user.uuid, name: user.name, - email: user.email + email: user.email, + admin: user.admin || false }); }); From f8d4bb60ce724fac195d70d2860e1c80e699494c Mon Sep 17 00:00:00 2001 From: Ehsan Asdar Date: Thu, 18 Oct 2018 18:25:25 -0400 Subject: [PATCH 116/194] Collect name in account --- client/application.html | 2 ++ server/routes/api/user.ts | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/client/application.html b/client/application.html index 6c3df225..a51d872b 100644 --- a/client/application.html +++ b/client/application.html @@ -29,6 +29,8 @@

    Apply: {{branch}}

    + + {{/unless}} {{#each questionData}} diff --git a/server/routes/api/user.ts b/server/routes/api/user.ts index 1167d71f..09c56ab5 100644 --- a/server/routes/api/user.ts +++ b/server/routes/api/user.ts @@ -72,6 +72,7 @@ function postApplicationBranchHandler(anonymous: boolean): (request: express.Req let user: IUserMongoose; if (anonymous) { let email = request.body["anonymous-registration-email"] as string; + let name = request.body["anonymous-registration-name"] as string; if (await User.findOne({email})) { response.status(400).json({ "error": `User with email "${email}" already exists` @@ -80,7 +81,7 @@ function postApplicationBranchHandler(anonymous: boolean): (request: express.Req } user = new User({ uuid: uuid(), - name: "Anonymous User", + name, email }) as IUserMongoose; } else { From a5b98378759d07df7f9db6fe990db82ef4f20ed0 Mon Sep 17 00:00:00 2001 From: Ehsan Asdar Date: Sat, 20 Oct 2018 00:35:24 -0400 Subject: [PATCH 117/194] Unrestrict endpoint --- server/routes/api/user.ts | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/server/routes/api/user.ts b/server/routes/api/user.ts index 09c56ab5..a3264d99 100644 --- a/server/routes/api/user.ts +++ b/server/routes/api/user.ts @@ -679,12 +679,18 @@ userRoutes.route("/team/rename/:newTeamName").post(isUserOrAdmin, async (request }); }); -userRoutes.get('/', isUserOrAdmin, async (request, response) => { - let user = await User.findOne({uuid: request.user!.uuid}) as IUserMongoose; - response.json({ - uuid: user.uuid, - name: user.name, - email: user.email, - admin: user.admin || false - }); +userRoutes.get('/', async (request, response) => { + if (request.user) { + let user = await User.findOne({uuid: request.user!.uuid}) as IUserMongoose; + response.json({ + uuid: user.uuid, + name: user.name, + email: user.email, + admin: user.admin || false + }); + } else { + response.json({ + error: 1 + }); + } }); From 34af0ebebd3cda77bda663ff5f92df1994c3addf Mon Sep 17 00:00:00 2001 From: Ehsan Asdar Date: Sat, 20 Oct 2018 20:03:15 -0400 Subject: [PATCH 118/194] Gracefully handle stats errors --- server/routes/templates.ts | 44 +++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/server/routes/templates.ts b/server/routes/templates.ts index ad627beb..6cf6bb93 100644 --- a/server/routes/templates.ts +++ b/server/routes/templates.ts @@ -805,31 +805,35 @@ templateRoutes.route("/admin").get(authenticateWithRedirect, async (request, res }).map(async statistic => { let questions = (await Branches.BranchConfig.loadBranchFromDB(statistic.branch)).questions; let question = questions.find(q => q.label === statistic.questionName)!; + try { + statistic.responses = statistic.responses.sort((a, b) => { + let aIndex: number = question.options.indexOf(a.response); + let bIndex: number = question.options.indexOf(b.response); - statistic.responses = statistic.responses.sort((a, b) => { - let aIndex: number = question.options.indexOf(a.response); - let bIndex: number = question.options.indexOf(b.response); - - if (aIndex !== -1 && bIndex === -1) { - return -1; - } - if (aIndex === -1 && bIndex !== -1) { - return 1; - } - if (aIndex === -1 && bIndex === -1) { - if (a.response.trim() === "") { - return 1; - } - if (a.response.toLowerCase() < b.response.toLowerCase()) { + if (aIndex !== -1 && bIndex === -1) { return -1; } - if (a.response.toLowerCase() > b.response.toLowerCase()) { + if (aIndex === -1 && bIndex !== -1) { return 1; } - return 0; - } - return aIndex - bIndex; - }); + if (aIndex === -1 && bIndex === -1) { + if (a.response.trim() === "") { + return 1; + } + if (a.response.toLowerCase() < b.response.toLowerCase()) { + return -1; + } + if (a.response.toLowerCase() > b.response.toLowerCase()) { + return 1; + } + return 0; + } + return aIndex - bIndex; + }); + } catch(e) { + console.trace(e); + return {}; + } return statistic; })); From 843af35852fc08c70020cd0ea25573430e93b209 Mon Sep 17 00:00:00 2001 From: Ehsan Asdar Date: Sat, 20 Oct 2018 20:11:35 -0400 Subject: [PATCH 119/194] Another fix --- server/routes/templates.ts | 46 ++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/server/routes/templates.ts b/server/routes/templates.ts index 6cf6bb93..19efe1c7 100644 --- a/server/routes/templates.ts +++ b/server/routes/templates.ts @@ -805,35 +805,33 @@ templateRoutes.route("/admin").get(authenticateWithRedirect, async (request, res }).map(async statistic => { let questions = (await Branches.BranchConfig.loadBranchFromDB(statistic.branch)).questions; let question = questions.find(q => q.label === statistic.questionName)!; - try { - statistic.responses = statistic.responses.sort((a, b) => { - let aIndex: number = question.options.indexOf(a.response); - let bIndex: number = question.options.indexOf(b.response); - if (aIndex !== -1 && bIndex === -1) { + statistic.responses = statistic.responses.sort((a, b) => { + let aIndex: number = question.options.indexOf(a.response); + let bIndex: number = question.options.indexOf(b.response); + if (!a || !b || !a.response || !b.response) { + return 0; + } + if (aIndex !== -1 && bIndex === -1) { + return -1; + } + if (aIndex === -1 && bIndex !== -1) { + return 1; + } + if (aIndex === -1 && bIndex === -1) { + if (a.response.trim() === "") { + return 1; + } + if (a.response.toLowerCase() < b.response.toLowerCase()) { return -1; } - if (aIndex === -1 && bIndex !== -1) { + if (a.response.toLowerCase() > b.response.toLowerCase()) { return 1; } - if (aIndex === -1 && bIndex === -1) { - if (a.response.trim() === "") { - return 1; - } - if (a.response.toLowerCase() < b.response.toLowerCase()) { - return -1; - } - if (a.response.toLowerCase() > b.response.toLowerCase()) { - return 1; - } - return 0; - } - return aIndex - bIndex; - }); - } catch(e) { - console.trace(e); - return {}; - } + return 0; + } + return aIndex - bIndex; + }); return statistic; })); From dec1a6e89ddb039bef8759e9f30f4f8d441fa4d4 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Mon, 4 Feb 2019 02:08:50 -0500 Subject: [PATCH 120/194] Fix grammar, styles, markup on team page --- client/css/index.css | 9 +---- client/css/main.css | 12 +++++-- client/team.html | 80 +++++++++++++++++++++++--------------------- 3 files changed, 53 insertions(+), 48 deletions(-) diff --git a/client/css/index.css b/client/css/index.css index cdaa3cd5..f3be9fcc 100644 --- a/client/css/index.css +++ b/client/css/index.css @@ -1,13 +1,6 @@ h1 { margin-bottom: 35px; } -.main { - padding: 40px; - margin: 8px 0; - border: 1px solid #ccc; - border-radius: 4px; - box-sizing: border-box; -} .row:first-of-type h2, .row:first-of-type h4 { margin: 0; } @@ -106,7 +99,7 @@ ul.timeline > li.warning > div::before { } ul.timeline > li.rejected::before { content: attr(data-rejected); -} +} ul.timeline > li.rejected > div { border-top: 2px solid #FF4136; } diff --git a/client/css/main.css b/client/css/main.css index fb05a00b..119c09b3 100644 --- a/client/css/main.css +++ b/client/css/main.css @@ -64,13 +64,13 @@ main { display: flex; justify-content: center; align-items: center; - + cursor: pointer; user-select: none; -webkit-transition: all 60ms ease-in-out; transition: all 60ms ease-in-out; - + text-align: center; white-space: nowrap; text-decoration: none !important; @@ -100,3 +100,11 @@ main { .btn, button { font-size: 15px; } + +section.main { + padding: 40px; + margin: 8px 0; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; +} diff --git a/client/team.html b/client/team.html index 6f5542ee..88caad70 100644 --- a/client/team.html +++ b/client/team.html @@ -11,7 +11,11 @@ @@ -23,56 +27,56 @@ {{#> sidebar}}

    Team Management

    - {{#if user.teamId}} -

    Team {{team.teamName}}

    -
    +
    + {{#if user.teamId}} +

    Team {{team.teamName}}

      -
    • {{teamLeaderAsUser.name}} ({{teamLeaderAsUser.email}}) — Team Leader
    • +
    • {{teamLeaderAsUser.name}} ({{teamLeaderAsUser.email}}) — Team Leader
    • {{#each membersAsUsers}}
    • {{name}} ({{email}})
    • {{/each}}
    -
    - {{#each membersAsUsers}} - {{else}} -

    There are no members in this team other than you!

    - {{/each}} -

    Tell your friends to enter {{team.teamName}} at their join team menu

    - - {{#if isCurrentUserTeamLeader}} - - - {{/if}} + {{#each membersAsUsers}} + {{else}} +

    There are no members in this team other than you!

    + {{/each}} +

    Tell your friends to enter {{team.teamName}} at their join team menu.

    -
    {{#if isCurrentUserTeamLeader}} - + + {{/if}} - -
    - {{else}} -

    Let's get you in a team

    -
    -
    -

    Join A Team

    -

    Ask your friend for their team name

    - - -
    - -
    +
    + {{#if isCurrentUserTeamLeader}} + + {{/if}} +
    -
    -

    Create A Team

    - - -
    - + {{else}} +

    Let's get you in a team!

    + +
    +
    +

    Join a team

    +

    Ask your friend for their team name.

    + + +
    + +
    +
    +
    +

    Create a team

    + + +
    + +
    -
    - {{/if}} + {{/if}} +
    {{/sidebar}} From fe7ce5ed5ac110b17f6fcf4c224b58e9dd4a3820 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Mon, 4 Feb 2019 15:08:46 -0500 Subject: [PATCH 121/194] Get better chart color from elements on page --- client/js/admin.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/js/admin.ts b/client/js/admin.ts index 0185de81..fae98ffe 100644 --- a/client/js/admin.ts +++ b/client/js/admin.ts @@ -837,8 +837,7 @@ declare let data: { declare const Chart: any; // Get the text color and use that for graphs -const pageHeader = document.querySelector("#sidebar > h1") as HTMLHeadingElement; -const color = window.getComputedStyle(pageHeader).getPropertyValue("color"); +const color = window.getComputedStyle(document.querySelector("thead")!).getPropertyValue("border-color"); for (let i = 0; i < data.length; i++) { let context = document.getElementById(`chart-${i}`) as HTMLCanvasElement | null; From cf98bb23f14821ccb0b8dd1f3bb758da52fbe337 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Mon, 4 Feb 2019 15:47:59 -0500 Subject: [PATCH 122/194] Update code editor to supported fork --- client/admin.html | 4 +- client/js/admin.ts | 10 +- package-lock.json | 340 ++++++++++++++++++++++++++++++--------------- package.json | 4 +- 4 files changed, 234 insertions(+), 124 deletions(-) diff --git a/client/admin.html b/client/admin.html index 83af9fbc..23e35dcb 100644 --- a/client/admin.html +++ b/client/admin.html @@ -7,15 +7,15 @@ - + + - - + - + - - - -
    -

    {{siteTitle}}

    - -
    {{error}}
    -
    {{success}}
    -
    - {{#ifIn "local" loginMethods}} - {{#if localOnly}} -
    - {{else}} -
    - {{/if}} -

    Log In

    - Forgot your password? - - - - - -

    - Don't have an account? -
    - Connect with an external service or sign up. -

    -
    - {{#if localOnly}} -
    - {{else}} -
    - {{/if}} -

    Sign Up

    -
    - - - - -
    -

    - Already have a local account? Log in. -

    -
    - {{/ifIn}} - {{#unless localOnly}} -
    - {{#ifIn "local" loginMethods}}{{else}} -

    Log In

    - {{/ifIn}} - {{> login-methods}} -
    - {{/unless}} -
    -
    - - diff --git a/client/postlogin.html b/client/postlogin.html deleted file mode 100644 index 3585c18e..00000000 --- a/client/postlogin.html +++ /dev/null @@ -1,48 +0,0 @@ - - - - New Account - {{siteTitle}} - - - - - - - - - -
    -

    {{siteTitle}}

    - -
    {{error}}
    -
    {{success}}
    -
    -
    -

    New Login

    - {{#if canAddLogins}} -

    Connect your account with another provider or an existing account:

    -
    - {{> login-methods}} -
    - {{/if}} -

    Please confirm a few things for us:

    -
    -
    - - -
    -
    - - -
    -
    - - -
    - -
    -
    -
    -
    - - diff --git a/client/resetpassword.html b/client/resetpassword.html deleted file mode 100644 index a2729a81..00000000 --- a/client/resetpassword.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - Reset Password - {{siteTitle}} - - - - - - - - -
    -

    {{siteTitle}}

    - -
    {{error}}
    -
    {{success}}
    -
    -
    -

    Reset your password

    -
    - - - -
    -
    -
    -
    - - diff --git a/package-lock.json b/package-lock.json index b0b2ab6d..ee1044f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,9 +60,9 @@ } }, "@types/bson": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/bson/-/bson-1.0.9.tgz", - "integrity": "sha512-IO2bGcW3ApLptLPOQ0HY3RLY40psH5aG5/DAU9HBEJ21vqiNE0cYZM52P8iWw0Dzk5qiKLReEUsCtn6V6qVMNg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.0.tgz", + "integrity": "sha512-pq/rqJwJWkbS10crsG5bgnrisL8pML79KlMKQMoQwLUjlPAkrUHMvHJ3oGwE7WHR61Lv/nadMwXVAD2b+fpD8Q==", "dev": true, "requires": { "@types/node": "*" @@ -230,23 +230,21 @@ } }, "@types/mongodb": { - "version": "3.0.21", - "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.0.21.tgz", - "integrity": "sha512-O+jw6Pi3n0LKsVkO86NL+oM6GadFaLnEtioM0bRwD/LK/7ULL9+F30G7n6rW5HcNJZ0wKalP1ERz3ezIseMH2Q==", + "version": "3.1.22", + "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.1.22.tgz", + "integrity": "sha512-hvNR0txBlJJAy1eZOeIDshW4dnQaC694COou4eHHaMdIcteCfoCQATD7sYNlXxNxfTc1iIbHUi7A8CAhQe08uA==", "dev": true, "requires": { "@types/bson": "*", - "@types/events": "*", "@types/node": "*" } }, "@types/mongoose": { - "version": "5.0.18", - "resolved": "https://registry.npmjs.org/@types/mongoose/-/mongoose-5.0.18.tgz", - "integrity": "sha512-NTZYnHKJcIuVU7sqojZVG84SoitGEBHC6iDAT/hgGIzDHZ+mwO+snjUnglkHdS9+jJ2YssCJbSZp80i86fCBvQ==", + "version": "5.3.23", + "resolved": "https://registry.npmjs.org/@types/mongoose/-/mongoose-5.3.23.tgz", + "integrity": "sha512-UZJOkFe/ShSt3iYFBiadwwCu2Y8qm/RZyAoCQI2uf88wr3NfDBpbqqoIyrchBy1y2XtvAAyktEPzvvR7up6/TQ==", "dev": true, "requires": { - "@types/events": "*", "@types/mongodb": "*", "@types/node": "*" } @@ -270,57 +268,36 @@ } }, "@types/node": { - "version": "8.10.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.19.tgz", - "integrity": "sha512-+PU57o6DtOSx0/algmxgCwWrmCiomwC/K+LPfXonT0tQMbNTjHEqVzwL9dFEhFoPmLFIiSWjRorLH6Z0hJMT+Q==" - }, - "@types/passport": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/@types/passport/-/passport-0.4.5.tgz", - "integrity": "sha512-Ow5akVXwEZlOPCWGbEGy0GX4ocdwKz7JJH1K+BMd/BSOxmJTo2obH2AKbsgcncQvw5z7AGopdIu1Ap/j9sMRnQ==", - "dev": true, - "requires": { - "@types/express": "*" - } - }, - "@types/passport-facebook": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@types/passport-facebook/-/passport-facebook-2.1.8.tgz", - "integrity": "sha512-5FGF6zNN0ZELetEdIDjVjfHSJfXSehNWeRLv9/8JD6Des4Z9A7sthhyXVRQUXeUxv0SmQ/i+IHZjR8R/G61wIg==", - "dev": true, - "requires": { - "@types/express": "*", - "@types/passport": "*" - } - }, - "@types/passport-github2": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@types/passport-github2/-/passport-github2-1.1.3.tgz", - "integrity": "sha512-mv1ynABtAH9eGrCEncBA7i+/ztoFOAp83xT2VklilK0sG9Hp24Hyaa2S17tJKT548iA35j1uycoanSAMPpyqFg==", + "version": "11.11.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.3.tgz", + "integrity": "sha512-wp6IOGu1lxsfnrD+5mX6qwSwWuqsdkKKxTN4aQc4wByHAKZJf9/D4KXPQ1POUjEbnCP5LMggB0OEFNY9OTsMqg==" + }, + "@types/oauth": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.1.tgz", + "integrity": "sha512-a1iY62/a3yhZ7qH7cNUsxoI3U/0Fe9+RnuFrpTKr+0WVOzbKlSLojShCKe20aOD1Sppv+i8Zlq0pLDuTJnwS4A==", "dev": true, "requires": { - "@types/express": "*", - "@types/passport": "*" + "@types/node": "*" } }, - "@types/passport-local": { - "version": "1.0.33", - "resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.33.tgz", - "integrity": "sha512-+rn6ZIxje0jZ2+DAiWFI8vGG7ZFKB0hXx2cUdMmudSWsigSq6ES7Emso46r4HJk0qCgrZVfI8sJiM7HIYf4SbA==", + "@types/passport": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.0.tgz", + "integrity": "sha512-R2FXqM+AgsMIym0PuKj08Ybx+GR6d2rU3b1/8OcHolJ+4ga2pRPX105wboV6hq1AJvMo2frQzYKdqXS5+4cyMw==", "dev": true, "requires": { - "@types/express": "*", - "@types/passport": "*", - "@types/passport-strategy": "*" + "@types/express": "*" } }, - "@types/passport-strategy": { - "version": "0.2.33", - "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.33.tgz", - "integrity": "sha512-tmj//XbNqCWmD+PJ/KnxAouircAmMGLN9IHBO3utH5DXuHHHYN4ZG53DRrQBjlZMiS/1b5IP38U2ay1GfbcQrQ==", + "@types/passport-oauth2": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.4.8.tgz", + "integrity": "sha512-tlX16wyFE5YJR2pHpZ308dgB1MV9/Ra2wfQh71eWk+/umPoD1Rca2D4N5M27W7nZm1wqUNGTk1I864nHvEgiFA==", "dev": true, "requires": { "@types/express": "*", + "@types/oauth": "*", "@types/passport": "*" } }, @@ -667,6 +644,11 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" }, + "base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==" + }, "basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -866,113 +848,6 @@ "stack-trace": "~0.0.7" } }, - "cas": { - "version": "git://github.com/joshchan/node-cas.git#344a8bfba9d054e2e378adaf95b720c898ae48a2", - "from": "git://github.com/joshchan/node-cas.git", - "requires": { - "cheerio": "0.19.0" - }, - "dependencies": { - "cheerio": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.19.0.tgz", - "integrity": "sha1-dy5wFfLuKZZQltcepBdbdas1SSU=", - "requires": { - "css-select": "~1.0.0", - "dom-serializer": "~0.1.0", - "entities": "~1.1.1", - "htmlparser2": "~3.8.1", - "lodash": "^3.2.0" - } - }, - "css-select": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.0.0.tgz", - "integrity": "sha1-sRIcpRhI3SZOIkTQWM7iVN7rRLA=", - "requires": { - "boolbase": "~1.0.0", - "css-what": "1.0", - "domutils": "1.4", - "nth-check": "~1.0.0" - } - }, - "css-what": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-1.0.0.tgz", - "integrity": "sha1-18wt9FGAZm+Z0rFEYmOUaeAPc2w=" - }, - "domhandler": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", - "integrity": "sha1-LeWaCCLVAn+r/28DLCsloqir5zg=", - "requires": { - "domelementtype": "1" - } - }, - "domutils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.4.3.tgz", - "integrity": "sha1-CGVRN5bGswYDGFDhdVFrr4C3Km8=", - "requires": { - "domelementtype": "1" - } - }, - "htmlparser2": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", - "integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=", - "requires": { - "domelementtype": "1", - "domhandler": "2.3", - "domutils": "1.5", - "entities": "1.0", - "readable-stream": "1.1" - }, - "dependencies": { - "domutils": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", - "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", - "requires": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "entities": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", - "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=" - } - } - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "lodash": { - "version": "3.10.1", - "resolved": "http://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", - "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - } - } - }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -2414,9 +2289,9 @@ } }, "mongoose": { - "version": "5.4.17", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.4.17.tgz", - "integrity": "sha512-9aAg8M0YUmGHQRDYvsKJ02wx/Qaof1Jn2iDH21ZtWGAZpQt9uVLNEOdcBuzi+lPJwGbLYh2dphdKX0sZ+dXAJQ==", + "version": "5.4.19", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.4.19.tgz", + "integrity": "sha512-paRU3nbCrPIUVw1GAlxo11uIIqrYORctUx1kcLj7i2NhkxPQuy5OK2/FYj8+tglsaixycmONSyop2HQp1IUQSA==", "requires": { "async": "2.6.1", "bson": "~1.1.0", @@ -2441,9 +2316,9 @@ } }, "bson": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.0.tgz", - "integrity": "sha512-9Aeai9TacfNtWXOYarkFJRW2CWo+dRon+fuLZYJmvLV3+MiUp0bEI6IAZfXEIg7/Pl/7IWlLaDnhzTsD81etQA==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.1.tgz", + "integrity": "sha512-jCGVYLoYMHDkOsbwJZBCqwMHyH4c+wzgI9hG7Z6SZJRXWr+x58pdIbm2i9a/jFGCkRJqRUr8eoI7lDWa0hTkxg==" }, "mongodb": { "version": "3.1.13", @@ -2670,59 +2545,20 @@ "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" }, "passport": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/passport/-/passport-0.3.2.tgz", - "integrity": "sha1-ndAJ+RXo/glbASSgG4+C2gdRAQI=", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.0.tgz", + "integrity": "sha1-xQlWkTR71a07XhgCOMORTRbwWBE=", "requires": { "passport-strategy": "1.x.x", "pause": "0.0.1" } }, - "passport-cas2": { - "version": "github:petschekr/passport-cas#3d026507a1c7949d25ce720886a127acb024744b", - "from": "github:petschekr/passport-cas", - "requires": { - "cas": "git://github.com/joshchan/node-cas.git", - "passport-strategy": "^1.0.0" - } - }, - "passport-facebook": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/passport-facebook/-/passport-facebook-2.1.1.tgz", - "integrity": "sha1-w50LUq5NWRYyRaTiGnubYyEwMxE=", - "requires": { - "passport-oauth2": "1.x.x" - } - }, - "passport-github2": { - "version": "0.1.11", - "resolved": "https://registry.npmjs.org/passport-github2/-/passport-github2-0.1.11.tgz", - "integrity": "sha1-yStW88OKROdmqsfp58E4TF6TyZk=", - "requires": { - "passport-oauth2": "1.x.x" - } - }, - "passport-google-oauth20": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-1.0.0.tgz", - "integrity": "sha1-O5YOih1w0dvnlGFcgnxoxAOSpdA=", - "requires": { - "passport-oauth2": "1.x.x" - } - }, - "passport-local": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", - "integrity": "sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=", - "requires": { - "passport-strategy": "1.x.x" - } - }, "passport-oauth2": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.4.0.tgz", - "integrity": "sha1-9i+BWDy+EmCb585vFguTlaJ7hq0=", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.5.0.tgz", + "integrity": "sha512-kqBt6vR/5VlCK8iCx1/KpY42kQ+NEHZwsSyt4Y6STiNjU+wWICG1i8ucc1FapXDGO15C5O5VZz7+7vRzrDPXXQ==", "requires": { + "base64url": "3.x.x", "oauth": "0.9.x", "passport-strategy": "1.x.x", "uid2": "0.0.x", diff --git a/package.json b/package.json index 8969c271..5eeb45fe 100644 --- a/package.json +++ b/package.json @@ -53,15 +53,11 @@ "marked": "^0.3.19", "moment": "^2.22.2", "moment-timezone": "^0.5.17", - "mongoose": "^5.4.17", + "mongoose": "^5.4.19", "morgan": "^1.9.1", "multer": "^1.3.0", - "passport": "^0.3.2", - "passport-cas2": "petschekr/passport-cas", - "passport-facebook": "^2.1.1", - "passport-github2": "^0.1.10", - "passport-google-oauth20": "^1.0.0", - "passport-local": "^1.0.0", + "passport": "^0.4.0", + "passport-oauth2": "^1.5.0", "qr-image": "^3.2.0", "serve-static": "^1.13.2", "striptags": "^3.1.1", @@ -88,15 +84,13 @@ "@types/marked": "0.0.28", "@types/mocha": "^2.2.48", "@types/moment-timezone": "^0.5.6", - "@types/mongodb": "^3.0.21", - "@types/mongoose": "^5.0.18", + "@types/mongodb": "^3.1.22", + "@types/mongoose": "^5.3.23", "@types/morgan": "^1.7.35", "@types/multer": "^1.3.6", - "@types/node": "^8.10.19", - "@types/passport": "^0.4.5", - "@types/passport-facebook": "^2.1.8", - "@types/passport-github2": "^1.1.3", - "@types/passport-local": "^1.0.33", + "@types/node": "^11.11.3", + "@types/passport": "^1.0.0", + "@types/passport-oauth2": "^1.4.8", "@types/qr-image": "^3.2.1", "@types/serve-static": "^1.13.2", "@types/supertest": "^2.0.4", diff --git a/server/common.ts b/server/common.ts index 46982edc..854a39ba 100644 --- a/server/common.ts +++ b/server/common.ts @@ -16,19 +16,10 @@ class Config implements IConfig.Main { public secrets: IConfig.Secrets = { adminKey: crypto.randomBytes(32).toString("hex"), session: crypto.randomBytes(32).toString("hex"), - oauth: { - github: { - id: "", - secret: "" - }, - google: { - id: "", - secret: "" - }, - facebook: { - id: "", - secret: "" - } + groundTruth: { + url: "", + id: "", + secret: "" } }; public email: IConfig.Email = { @@ -102,9 +93,6 @@ class Config implements IConfig.Main { this.server[key] = config.server[key]; } } - if (config.admins) { - this.admins = config.admins; - } if (config.eventName) { this.eventName = config.eventName; } @@ -131,42 +119,33 @@ class Config implements IConfig.Main { protected loadFromEnv(): void { // Secrets if (process.env.ADMIN_KEY_SECRET) { - this.secrets.adminKey = process.env.ADMIN_KEY_SECRET!; + this.secrets.adminKey = process.env.ADMIN_KEY_SECRET; } else { console.warn("Setting random admin key! Cannot use the service-to-service APIs."); } if (process.env.SESSION_SECRET) { - this.secrets.session = process.env.SESSION_SECRET!; + this.secrets.session = process.env.SESSION_SECRET; this.sessionSecretSet = true; } - if (process.env.GITHUB_CLIENT_ID) { - this.secrets.oauth.github.id = process.env.GITHUB_CLIENT_ID!; - } - if (process.env.GITHUB_CLIENT_SECRET) { - this.secrets.oauth.github.secret = process.env.GITHUB_CLIENT_SECRET!; - } - if (process.env.GOOGLE_CLIENT_ID) { - this.secrets.oauth.google.id = process.env.GOOGLE_CLIENT_ID!; - } - if (process.env.GOOGLE_CLIENT_SECRET) { - this.secrets.oauth.google.secret = process.env.GOOGLE_CLIENT_SECRET!; + if (process.env.GROUND_TRUTH_URL) { + this.secrets.groundTruth.url = process.env.GROUND_TRUTH_URL; } - if (process.env.FACEBOOK_CLIENT_ID) { - this.secrets.oauth.facebook.id = process.env.FACEBOOK_CLIENT_ID!; + if (process.env.GROUND_TRUTH_ID) { + this.secrets.groundTruth.id = process.env.GROUND_TRUTH_ID; } - if (process.env.FACEBOOK_CLIENT_SECRET) { - this.secrets.oauth.facebook.secret = process.env.FACEBOOK_CLIENT_SECRET!; + if (process.env.GROUND_TRUTH_SECRET) { + this.secrets.groundTruth.secret = process.env.GROUND_TRUTH_SECRET; } // Email if (process.env.EMAIL_FROM) { - this.email.from = process.env.EMAIL_FROM!; + this.email.from = process.env.EMAIL_FROM; } if (process.env.EMAIL_KEY) { - this.email.key = process.env.EMAIL_KEY!; + this.email.key = process.env.EMAIL_KEY; } // Server - if (process.env.PRODUCTION && process.env.PRODUCTION!.toLowerCase() === "true") { + if (process.env.PRODUCTION && process.env.PRODUCTION.toLowerCase() === "true") { this.server.isProduction = true; } if (process.env.PORT) { @@ -176,37 +155,37 @@ class Config implements IConfig.Main { } } if (process.env.VERSION_HASH) { - this.server.versionHash = process.env.VERSION_HASH!; + this.server.versionHash = process.env.VERSION_HASH; } if (process.env.SOURCE_REV) { - this.server.versionHash = process.env.SOURCE_REV!; + this.server.versionHash = process.env.SOURCE_REV; } if (process.env.SOURCE_VERSION) { - this.server.versionHash = process.env.SOURCE_VERSION!; + this.server.versionHash = process.env.SOURCE_VERSION; } if (process.env.WORKFLOW_RELEASE_CREATED_AT) { - this.server.workflowReleaseCreatedAt = process.env.WORKFLOW_RELEASE_CREATED_AT!; + this.server.workflowReleaseCreatedAt = process.env.WORKFLOW_RELEASE_CREATED_AT; } if (process.env.WORKFLOW_RELEASE_SUMMARY) { - this.server.workflowReleaseSummary = process.env.WORKFLOW_RELEASE_SUMMARY!; + this.server.workflowReleaseSummary = process.env.WORKFLOW_RELEASE_SUMMARY; } if (process.env.COOKIE_MAX_AGE) { - let maxAge = parseInt(process.env.COOKIE_MAX_AGE!, 10); + let maxAge = parseInt(process.env.COOKIE_MAX_AGE, 10); if (!isNaN(maxAge) && maxAge > 0) { this.server.cookieMaxAge = maxAge; } } - if (process.env.COOKIE_SECURE_ONLY && process.env.COOKIE_SECURE_ONLY!.toLowerCase() === "true") { + if (process.env.COOKIE_SECURE_ONLY && process.env.COOKIE_SECURE_ONLY.toLowerCase() === "true") { this.server.cookieSecureOnly = true; } if (process.env.MONGO_URL) { - this.server.mongoURL = process.env.MONGO_URL!; + this.server.mongoURL = process.env.MONGO_URL; } if (process.env.DEFAULT_TIMEZONE) { this.server.defaultTimezone = process.env.DEFAULT_TIMEZONE; } if (process.env.PASSWORD_RESET_EXPIRATION) { - let expirationTime = parseInt(process.env.PASSWORD_RESET_EXPIRATION!, 10); + let expirationTime = parseInt(process.env.PASSWORD_RESET_EXPIRATION, 10); if (!isNaN(expirationTime) && expirationTime > 0) { this.server.passwordResetExpiration = expirationTime; } @@ -217,27 +196,27 @@ class Config implements IConfig.Main { } // Event name if (process.env.EVENT_NAME) { - this.eventName = process.env.EVENT_NAME!; + this.eventName = process.env.EVENT_NAME; } // Questions if (process.env.QUESTIONS_FILE) { - this.questionsLocation = process.env.QUESTIONS_FILE!; + this.questionsLocation = process.env.QUESTIONS_FILE; } // Style if (process.env.THEME_FILE) { - this.style.theme = process.env.THEME_FILE!; + this.style.theme = process.env.THEME_FILE; } if (process.env.FAVICON_FILE) { - this.style.favicon = process.env.FAVICON_FILE!; + this.style.favicon = process.env.FAVICON_FILE; } else if (process.env.FAVICON_FILE_BASE64) { - this.style.favicon = unbase64File(process.env.FAVICON_FILE_BASE64!); + this.style.favicon = unbase64File(process.env.FAVICON_FILE_BASE64); } // Storage engine if (process.env.STORAGE_ENGINE) { - this.storageEngine.name = process.env.STORAGE_ENGINE!; + this.storageEngine.name = process.env.STORAGE_ENGINE; if (process.env.STORAGE_ENGINE_OPTIONS) { - this.storageEngine.options = JSON.parse(process.env.STORAGE_ENGINE_OPTIONS!); + this.storageEngine.options = JSON.parse(process.env.STORAGE_ENGINE_OPTIONS); } else { console.warn("Custom storage engine defined but no storage engine options passed"); @@ -248,7 +227,7 @@ class Config implements IConfig.Main { } // Team size if (process.env.MAX_TEAM_SIZE) { - this.maxTeamSize = parseInt(process.env.MAX_TEAM_SIZE!, 10); + this.maxTeamSize = parseInt(process.env.MAX_TEAM_SIZE, 10); } } } @@ -285,7 +264,7 @@ export function formatSize(size: number, binary: boolean = true): string { // Database connection // import * as mongoose from "mongoose"; -mongoose.connect(config.server.mongoURL).catch(err => { +mongoose.connect(config.server.mongoURL, { useNewUrlParser: true }).catch(err => { throw err; }); export { mongoose }; @@ -299,8 +278,7 @@ async function setDefaultSettings() { const DEFAULTS: any = { "teamsEnabled": true, - "qrEnabled": true, - "loginMethods": ["local"] + "qrEnabled": true }; for (let setting in DEFAULTS) { @@ -490,11 +468,11 @@ export async function renderEmailHTML(markdown: string, user: IUser): Promise { - let question = user.applicationData.find(data => data.name === name); + let question = (user.applicationData || []).find(data => data.name === name); return formatFormItem(question); }); markdown = markdown.replace(/{{confirmation\.([a-zA-Z0-9\- ]+)}}/g, (match, name: string) => { - let question = user.confirmationData.find(data => data.name === name); + let question = (user.confirmationData || []).find(data => data.name === name); return formatFormItem(question); }); diff --git a/server/config/config.example.json b/server/config/config.example.json index 57d9d5f2..118c8283 100644 --- a/server/config/config.example.json +++ b/server/config/config.example.json @@ -1,19 +1,10 @@ { "secrets": { "session": "", - "oauth": { - "github": { - "id": "", - "secret": "" - }, - "google": { - "id": "", - "secret": "" - }, - "facebook": { - "id": "", - "secret": "" - } + "groundTruth": { + "url": "", + "id": "", + "secret": "" } }, "email": { @@ -30,7 +21,6 @@ "passwordResetExpiration": 3600000, "mongoURL": "mongodb://localhost/registration" }, - "admins": ["example@example.com"], "eventName": "My Hackathon", "storageEngine": { "name": "disk | s3", diff --git a/server/middleware.ts b/server/middleware.ts index 6e54ed75..64f364ac 100644 --- a/server/middleware.ts +++ b/server/middleware.ts @@ -5,7 +5,7 @@ import * as os from "os"; import { config, isBranchOpen } from "./common"; import { BranchConfig, ApplicationBranch, ConfirmationBranch } from "./branch"; -import { User, IUser, DataLog, HackGTMetrics } from "./schema"; +import { IUser, User, DataLog, HackGTMetrics } from "./schema"; // // Express middleware @@ -60,7 +60,7 @@ export function isUserOrAdmin(request: express.Request, response: express.Respon "error": "You must log in to access this endpoint" }); } - else if ((user.uuid !== request.params.uuid && !user.admin) || !user.verifiedEmail || !user.accountConfirmed) { + else if (user.uuid !== request.params.uuid && !user.admin) { response.status(403).json({ "error": "You are not permitted to access this endpoint" }); @@ -91,7 +91,7 @@ export function isAdmin(request: express.Request, response: express.Response, ne "error": "You must log in to access this endpoint" }); } - else if (!user.admin || !user.verifiedEmail || !user.accountConfirmed) { + else if (!user.admin) { response.status(403).json({ "error": "You are not permitted to access this endpoint" }); @@ -104,7 +104,7 @@ export function isAdmin(request: express.Request, response: express.Response, ne // For API endpoints export function authenticateWithReject(request: express.Request, response: express.Response, next: express.NextFunction) { response.setHeader("Cache-Control", "no-cache"); - if (!request.isAuthenticated() || !request.user || !request.user.verifiedEmail || !request.user.accountConfirmed) { + if (!request.isAuthenticated() || !request.user) { response.status(401).json({ "error": "You must log in to access this endpoint" }); @@ -117,7 +117,7 @@ export function authenticateWithReject(request: express.Request, response: expre // For directly user facing endpoints export function authenticateWithRedirect(request: express.Request, response: express.Response, next: express.NextFunction) { response.setHeader("Cache-Control", "private"); - if (!request.isAuthenticated() || !request.user || !request.user.verifiedEmail || !request.user.accountConfirmed) { + if (!request.isAuthenticated() || !request.user) { if (request.session) { request.session.returnTo = request.originalUrl; } @@ -171,7 +171,7 @@ export async function canUserModify(request: express.Request, response: express. }); return; } - if (user.applied && branchName.toLowerCase() !== user.applicationBranch.toLowerCase()) { + if (user.applied && branchName.toLowerCase() !== user.applicationBranch!.toLowerCase()) { response.status(400).json({ "error": "You can only edit the application branch that you originally submitted" }); @@ -233,8 +233,8 @@ export function branchRedirector(requestType: ApplicationType): (request: expres if (requestType === ApplicationType.Application) { // Redirect directly to branch if there is an existing application or confirmation - if (user.applied && branchName.toLowerCase() !== user.applicationBranch.toLowerCase()) { - response.redirect(`/apply/${encodeURIComponent(user.applicationBranch.toLowerCase())}`); + if (user.applied && branchName.toLowerCase() !== user.applicationBranch!.toLowerCase()) { + response.redirect(`/apply/${encodeURIComponent(user.applicationBranch!.toLowerCase())}`); return; } } diff --git a/server/routes/api/graphql.ts b/server/routes/api/graphql.ts index 952ddc1c..4633289c 100644 --- a/server/routes/api/graphql.ts +++ b/server/routes/api/graphql.ts @@ -10,7 +10,6 @@ import { User, IUser, Team, IFormItem, QuestionBranchConfig } from "../../schema import { Branches, Tags, AllTags, BranchConfig, ApplicationBranch, ConfirmationBranch, NoopBranch } from "../../branch"; import { schema as types } from "./api.graphql.types"; import { formatSize } from "../../common"; -import { prettyNames as strategyNames } from "../strategies"; const typeDefs = fs.readFileSync(path.resolve(__dirname, "../../../api.graphql"), "utf8"); @@ -161,13 +160,13 @@ async function findQuestions( let items: types.FormItem[] = []; if (user.applied) { - items = items.concat(await Promise.all(user.applicationData + items = items.concat(await Promise.all(user.applicationData! .reduce(questionFilter, []) - .map(item => recordToFormItem(item, user.applicationBranch)) + .map(item => recordToFormItem(item, user.applicationBranch!)) )); } if (user.confirmed) { - items = items.concat(await Promise.all(user.confirmationData + items = items.concat(await Promise.all(user.confirmationData! .reduce(questionFilter, []) .map(item => recordToFormItem(item, user.confirmationBranch!)) )); @@ -283,8 +282,8 @@ async function recordToFormItem(item: IFormItem, branchName: string): Promise> { const application: types.Branch | undefined = user.applied ? { - type: user.applicationBranch, - data: await Promise.all(user.applicationData.map(item => recordToFormItem(item, user.applicationBranch))), + type: user.applicationBranch!, + data: await Promise.all(user.applicationData!.map(item => recordToFormItem(item, user.applicationBranch!))), start_time: user.applicationStartTime && user.applicationStartTime.toDateString(), submit_time: user.applicationSubmitTime && @@ -293,21 +292,13 @@ async function userRecordToGraphql(user: IUser): Promise> { const confirmation: types.Branch | undefined = user.confirmed ? { type: user.confirmationBranch!, - data: await Promise.all(user.confirmationData.map(item => recordToFormItem(item, user.confirmationBranch!))), + data: await Promise.all(user.confirmationData!.map(item => recordToFormItem(item, user.confirmationBranch!))), start_time: user.confirmationStartTime && user.confirmationStartTime.toDateString(), submit_time: user.confirmationSubmitTime && user.confirmationSubmitTime.toDateString() } : undefined; - let loginMethods: string[] = []; - if (user.local && user.local!.hash) { - loginMethods.push("Local"); - } - for (let service of Object.keys(user.services || {}) as (keyof typeof user.services)[]) { - loginMethods.push(strategyNames[service]); - } - let team = user.teamId ? await Team.findById(user.teamId) : null; return { @@ -315,9 +306,7 @@ async function userRecordToGraphql(user: IUser): Promise> { name: user.name || "", email: user.email, - email_verified: !!user.verifiedEmail, admin: !!user.admin, - login_methods: loginMethods, applied: !!user.applied, accepted: !!user.accepted, diff --git a/server/routes/api/settings.ts b/server/routes/api/settings.ts index 400e8c3c..75c7fbbd 100644 --- a/server/routes/api/settings.ts +++ b/server/routes/api/settings.ts @@ -108,43 +108,6 @@ settingsRoutes.route("/admin_emails") }); }); -settingsRoutes.route("/login_methods") - .get(async (request, response) => { - let methods = await getSetting("loginMethods"); - response.json({ - methods - }); - }) - .put(isAdmin, uploadHandler.any(), async (request, response) => { - let { enabledMethods } = request.body; - if (!enabledMethods) { - response.status(400).json({ - "error": "Missing value for enabled methods" - }); - return; - } - try { - let methods = JSON.parse(enabledMethods); - if (!Array.isArray(methods)) { - response.status(400).json({ - "error": "Invalid value for enabled methods" - }); - return; - } - await updateSetting("loginMethods", methods); - await (await import("../auth")).reloadAuthentication(); - response.json({ - "success": true - }); - } - catch (err) { - console.error(err); - response.status(500).json({ - "error": "An error occurred while changing available login methods" - }); - } - }); - settingsRoutes.route("/branch_roles") .get(isAdmin, async (request, response) => { response.json({ @@ -283,8 +246,6 @@ settingsRoutes.route("/email_content/:type/rendered") settingsRoutes.route("/send_batch_email") .post(isAdmin, uploadHandler.any(), async (request, response) => { let filter = JSON.parse(request.body.filter); - filter.verifiedEmail = true; - filter.accountConfirmed = true; let subject = request.body.subject as string; let markdownContent = request.body.markdownContent; if (typeof filter !== "object") { diff --git a/server/routes/api/user.ts b/server/routes/api/user.ts index 78706367..ea100657 100644 --- a/server/routes/api/user.ts +++ b/server/routes/api/user.ts @@ -15,11 +15,13 @@ import { trackEvent, canUserModify } from "../../middleware"; import { + Model, createNew, IFormItem, - IUserMongoose, User, - ITeamMongoose, Team + IUser, User, + Team } from "../../schema"; import * as Branches from "../../branch"; +import { GroundTruthStrategy } from "../strategies"; export let userRoutes = express.Router({ "mergeParams": true }); export let registrationRoutes = express.Router({ "mergeParams": true }); @@ -69,23 +71,31 @@ userRoutes.route("/confirmation/:branch").post( ); function postApplicationBranchHandler(anonymous: boolean): (request: express.Request, response: express.Response) => Promise { return async (request, response) => { - let user: IUserMongoose; + let user: Model; if (anonymous) { let email = request.body["anonymous-registration-email"] as string; let name = request.body["anonymous-registration-name"] as string; - if (await User.findOne({email})) { + if (await User.findOne({ email })) { response.status(400).json({ "error": `User with email "${email}" already exists` }); return; } - user = new User({ + user = createNew(User, { + ...GroundTruthStrategy.defaultUserProperties, uuid: uuid(), name, email - }) as IUserMongoose; + }); } else { - user = await User.findOne({uuid: request.params.uuid}) as IUserMongoose; + let existingUser = await User.findOne({ uuid: request.params.uuid }); + if (!existingUser) { + response.status(400).json({ + "error": "Invalid user id" + }); + return; + } + user = existingUser; } let branchName = await Branches.BranchConfig.getCanonicalName(request.params.branch); @@ -104,12 +114,15 @@ function postApplicationBranchHandler(anonymous: boolean): (request: express.Req return null; } let files = request.files as Express.Multer.File[]; - let preexistingFile: boolean = question.type === "file" && user.applicationData && user.applicationData.some(entry => entry.name === question.name && !!entry.value); + let preexistingFile: boolean = + question.type === "file" + && user.applicationData != undefined + && user.applicationData.some(entry => entry.name === question.name && !!entry.value); if (question.required && !request.body[question.name] && !files.find(file => file.fieldname === question.name)) { // Required field not filled in if (preexistingFile) { - let previousValue = user.applicationData.find(entry => entry.name === question.name && !!entry.value)!.value as Express.Multer.File; + let previousValue = user.applicationData!.find(entry => entry.name === question.name && !!entry.value)!.value as Express.Multer.File; unchangedFiles.push(previousValue.filename); return { "name": question.name, @@ -267,7 +280,13 @@ function postApplicationBranchHandler(anonymous: boolean): (request: express.Req async function deleteApplicationBranchHandler(request: express.Request, response: express.Response) { let requestType: ApplicationType = request.url.match(/\/application\//) ? ApplicationType.Application : ApplicationType.Confirmation; - let user = await User.findOne({uuid: request.params.uuid}) as IUserMongoose; + let user = await User.findOne({ uuid: request.params.uuid }); + if (!user) { + response.status(400).json({ + "error": "Invalid user id" + }); + return; + } if (requestType === ApplicationType.Application) { user.applied = false; user.accepted = false; @@ -327,7 +346,7 @@ userRoutes.route("/status").post(isAdmin, uploadHandler.any(), async (request, r } }); -async function updateUserStatus(user: IUserMongoose, status: string): Promise { +async function updateUserStatus(user: Model, status: string): Promise { if (status === user.confirmationBranch) { throw new Error(`User status is already ${status}!`); } else if (status === "no-decision") { @@ -442,14 +461,14 @@ userRoutes.route("/export").get(isAdmin, async (request, response): Promise { // TODO: Replace with more robust schema-agnostic version - let nameFormItem = user.applicationData.find(item => item.name === "name"); + let nameFormItem = (user.applicationData || []).find(item => item.name === "name"); return { "name": nameFormItem && typeof nameFormItem.value === "string" ? nameFormItem.value : user.name, "email": user.email @@ -467,12 +486,12 @@ userRoutes.route("/export").get(isAdmin, async (request, response): Promise { +async function removeUserFromAllTeams(user: IUser): Promise { if (!user.teamId) { return true; } - let currentUserTeam = await Team.findById(user.teamId) as ITeamMongoose; + let currentUserTeam = await Team.findById(user.teamId); if (!currentUserTeam) { return false; } @@ -518,13 +537,18 @@ userRoutes.route("/team/create/:teamName").post(isUserOrAdmin, async (request, r Else if the user's in a team, take them out of it Else make them a new team */ - let user = await User.findOne({uuid: request.params.uuid}) as IUserMongoose; + let user = await User.findOne({ uuid: request.params.uuid }); + if (!user) { + response.status(400).json({ + "error": "Invalid user id" + }); + return; + } let decodedTeamName = decodeURI(request.params.teamName); - let existingTeam = await Team.findOne({ teamName: decodedTeamName }) as ITeamMongoose; + let existingTeam = await Team.findOne({ teamName: decodedTeamName }); if (existingTeam) { - // Someone else has a team with this name response.status(400).json({ "error": `Someone else has a team called ${decodedTeamName}. Please pick a different name.` @@ -534,18 +558,20 @@ userRoutes.route("/team/create/:teamName").post(isUserOrAdmin, async (request, r // If the user is in a team, remove them from their current team unless they're the team leader if (user.teamId) { - let currentUserTeam = await Team.findById(user.teamId) as ITeamMongoose; + let currentUserTeam = await Team.findById(user.teamId); - if (currentUserTeam.teamLeader === user._id) { - // The user is in the team they made already - // Ideally this will never happen if we do some validation client side - response.status(400).json({ - "error": "You're already the leader of this team" - }); - return; - } + if (currentUserTeam) { + if (currentUserTeam.teamLeader === user._id) { + // The user is in the team they made already + // Ideally this will never happen if we do some validation client side + response.status(400).json({ + "error": "You're already the leader of this team" + }); + return; + } - await removeUserFromAllTeams(user); + await removeUserFromAllTeams(user); + } } let query = { @@ -562,9 +588,11 @@ userRoutes.route("/team/create/:teamName").post(isUserOrAdmin, async (request, r setDefaultsOnInsert: true }; - let team = await Team.findOneAndUpdate(query, {}, options) as ITeamMongoose; + let team = await Team.findOneAndUpdate(query, {}, options); - user.teamId = team._id; + if (team) { + user.teamId = team._id; + } await user.save(); response.json({ @@ -573,7 +601,14 @@ userRoutes.route("/team/create/:teamName").post(isUserOrAdmin, async (request, r }); userRoutes.route("/team/join/:teamName").post(isUserOrAdmin, async (request, response): Promise => { - let user = await User.findOne({uuid: request.params.uuid}) as IUserMongoose; + let user = await User.findOne({ uuid: request.params.uuid }); + if (!user) { + response.status(400).json({ + "error": "Invalid user id" + }); + return; + } + let decodedTeamName = decodeURI(request.params.teamName); if (user.teamId) { @@ -589,7 +624,7 @@ userRoutes.route("/team/join/:teamName").post(isUserOrAdmin, async (request, res return; } - let teamToJoin = await Team.findOne({ teamName: decodedTeamName }) as ITeamMongoose; + let teamToJoin = await Team.findOne({ teamName: decodedTeamName }); if (!teamToJoin) { // If the team they tried to join isn't real... @@ -626,7 +661,13 @@ userRoutes.route("/team/join/:teamName").post(isUserOrAdmin, async (request, res }); userRoutes.route("/team/leave").post(isUserOrAdmin, async (request, response): Promise => { - let user = await User.findOne({uuid: request.params.uuid}) as IUserMongoose; + let user = await User.findOne({ uuid: request.params.uuid }); + if (!user) { + response.status(400).json({ + "error": "Invalid user id" + }); + return; + } await removeUserFromAllTeams(user); response.status(200).json({ @@ -635,8 +676,13 @@ userRoutes.route("/team/leave").post(isUserOrAdmin, async (request, response): P }); userRoutes.route("/team/rename/:newTeamName").post(isUserOrAdmin, async (request, response): Promise => { - let user = await User.findOne({uuid: request.params.uuid}) as IUserMongoose; - + let user = await User.findOne({ uuid: request.params.uuid }); + if (!user) { + response.status(400).json({ + "error": "Invalid user id" + }); + return; + } if (!user.teamId) { response.status(400).json({ "error": "You're not in a team" @@ -644,8 +690,7 @@ userRoutes.route("/team/rename/:newTeamName").post(isUserOrAdmin, async (request return; } - let currentUserTeam = await Team.findById(user.teamId) as ITeamMongoose; - + let currentUserTeam = await Team.findById(user.teamId); if (!currentUserTeam) { // User tried to change their team name even though they don't have a team response.status(400).json({ @@ -682,18 +727,26 @@ userRoutes.route("/team/rename/:newTeamName").post(isUserOrAdmin, async (request }); }); -userRoutes.get('/', async (request, response) => { +userRoutes.get("/", async (request, response) => { if (request.user) { - let user = await User.findOne({uuid: request.user!.uuid}) as IUserMongoose; + let user = await User.findOne({ uuid: request.user.uuid }); + if (!user) { + response.status(400).json({ + "error": "Invalid user id" + }); + return; + } + response.json({ uuid: user.uuid, name: user.name, email: user.email, admin: user.admin || false }); - } else { + } + else { response.json({ - error: 1 + "error": "Not logged in" }); } }); diff --git a/server/routes/auth.ts b/server/routes/auth.ts index bc5a782a..9ff6f31a 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -1,5 +1,4 @@ import * as crypto from "crypto"; -import * as path from "path"; import * as express from "express"; import * as session from "express-session"; import * as connectMongo from "connect-mongo"; @@ -7,18 +6,17 @@ const MongoStore = connectMongo(session); import * as passport from "passport"; import { - config, mongoose, COOKIE_OPTIONS, getSetting + config, mongoose, COOKIE_OPTIONS } from "../common"; import { - IUser, User, IUserMongoose + IUser, User } from "../schema"; -import { postParser } from "../middleware"; import { - RegistrationStrategy, strategies, validateAndCacheHostName, sendVerificationEmail + AuthenticateOptions, GroundTruthStrategy, createLink, validateAndCacheHostName } from "./strategies"; // Passport authentication -import {app} from "../app"; +import { app } from "../app"; if (!config.server.isProduction) { console.warn("OAuth callback(s) running in development mode"); @@ -40,121 +38,56 @@ app.use(session({ saveUninitialized: false })); passport.serializeUser((user, done) => { - done(null, user._id.toString()); + done(null, user.uuid); }); passport.deserializeUser((id, done) => { - User.findById(id, (err, user) => { + User.findOne({ uuid: id }, (err, user) => { done(err, user!); }); }); -let router = express.Router(); -export let authRoutes: express.RequestHandler = (request, response, next) => { - // Allows for dynamic dispatch when authentication gets reloaded - router(request, response, next); -}; +export let authRoutes = express.Router(); -export async function reloadAuthentication() { - router = express.Router(); - let authenticationMethods: RegistrationStrategy[] = []; - let methods = await getSetting<(keyof typeof strategies)[]>("loginMethods"); - console.info(`Using authentication methods: ${methods.join(", ")}`); - for (let methodName of methods) { - if (!strategies[methodName]) { - console.error(`Authentication method "${methodName}" is not available. Did you add it to the exported list of strategies?`); - continue; - } - let method = new strategies[methodName](); - authenticationMethods.push(method); - method.use(router); - } +authRoutes.get("/validatehost/:nonce", (request, response) => { + let nonce: string = request.params.nonce || ""; + response.send(crypto.createHmac("sha256", config.secrets.session).update(nonce).digest().toString("hex")); +}); + +authRoutes.all("/logout", (request, response) => { + request.logout(); + response.redirect("/login"); +}); + +app.use(passport.initialize()); +app.use(passport.session()); + +const groundTruthStrategy = new GroundTruthStrategy(config.secrets.groundTruth.url); + +passport.use(groundTruthStrategy); - // These routes need to be redefined on every instance of a new router - router.post("/confirm", validateAndCacheHostName, postParser, async (request, response) => { - let user = request.user as IUserMongoose; - let name = request.body.name as string; - if (!name || !name.trim()) { - request.flash("error", "Invalid name"); - response.redirect("/login/confirm"); +authRoutes.get("/login", validateAndCacheHostName, (request, response, next) => { + let callbackURL = createLink(request, "auth/login/callback"); + passport.authenticate("oauth2", { callbackURL } as AuthenticateOptions)(request, response, next); +}); +authRoutes.get("/login/callback", validateAndCacheHostName, (request, response, next) => { + let callbackURL = createLink(request, "auth/login/callback"); + passport.authenticate("oauth2", { callbackURL } as AuthenticateOptions, (err: Error | null, user?: IUser) { + if (err) { + console.error(err); + next(err); return; } - if (!request.isAuthenticated() || !user) { - request.flash("error", "Must be logged in"); + if (!user) { response.redirect("/login"); return; } - user.name = name.trim(); - - let email = request.body.email as string | undefined; - if (email && email !== user.email) { - if (!email.trim()) { - request.flash("error", "Invalid email"); - response.redirect("/login/confirm"); - return; - } - if (await User.count({ email }) > 0) { - request.flash("error", "That email address is already in use. You may already have an account from another login service."); - response.redirect("/login/confirm"); - return; - } - user.admin = false; - user.verifiedEmail = false; - user.email = email; - if (config.admins.includes(email)) { - user.admin = true; - console.info(`Adding new admin: ${email}`); - } - } - user.accountConfirmed = true; - - try { - await user.save(); - if (!user.verifiedEmail && (!user.local || !user.local.verificationCode)) { - await sendVerificationEmail(request, user); - } - if (!user.verifiedEmail) { - request.logout(); - request.flash("success", "Account created successfully. Please verify your email before logging in."); - response.redirect("/login"); + request.login(user, err2 => { + if (err) { + console.error(err2); + next(err); return; } response.redirect("/"); - } - catch (err) { - console.error(err); - request.flash("error", "An error occurred while creating your account"); - response.redirect("/login/confirm"); - } - }); - - router.get("/validatehost/:nonce", (request, response) => { - let nonce: string = request.params.nonce || ""; - response.send(crypto.createHmac("sha256", config.secrets.session).update(nonce).digest().toString("hex")); - }); - - router.all("/logout", (request, response) => { - request.logout(); - response.redirect("/login"); - }); -} -reloadAuthentication().catch(err => { - throw err; -}); - -app.use(passport.initialize()); -app.use(passport.session()); - -app.use((request, response, next) => { - // Only block requests for GET requests to non-auth pages - if (path.extname(request.url) !== "" || request.method !== "GET" || request.originalUrl.match(/^\/auth/)) { - next(); - return; - } - let user = request.user as IUser; - if (user && !user.accountConfirmed && request.originalUrl !== "/login/confirm") { - response.redirect("/login/confirm"); - } - else { - next(); - } + }); + })(request, response, next); }); diff --git a/server/routes/strategies.ts b/server/routes/strategies.ts index 481b6f21..be9a5469 100644 --- a/server/routes/strategies.ts +++ b/server/routes/strategies.ts @@ -1,627 +1,103 @@ -// tslint:disable:interface-name import * as crypto from "crypto"; import * as http from "http"; import * as https from "https"; -import * as path from "path"; +import { URL } from "url"; import * as passport from "passport"; -import * as moment from "moment-timezone"; -import * as uuid from "uuid/v4"; +import { Strategy as OAuthStrategy } from "passport-oauth2"; -import { config, pbkdf2Async, renderEmailHTML, renderEmailText, sendMailAsync } from "../common"; -import { postParser, trackEvent } from "../middleware"; -import { IConfig, IUser, IUserMongoose, User } from "../schema"; -import { Request, Response, NextFunction, Router } from "express"; +import { config } from "../common"; +// TODO import { trackEvent } from "../middleware"; +import { createNew, IUser, User } from "../schema"; +import { Request, Response, NextFunction } from "express"; -import {Strategy as LocalStrategy} from "passport-local"; -import {Strategy as GitHubStrategy} from "passport-github2"; -import {Strategy as FacebookStrategy} from "passport-facebook"; -// No type definitions available yet for these modules -// tslint:disable:no-var-requires -const GoogleStrategy: StrategyConstructor = require("passport-google-oauth20").Strategy; -const CASStrategyProvider: StrategyConstructor = require("passport-cas2").Strategy; - -type Strategy = passport.Strategy & { - logout?(request: Request, response: Response, returnURL: string): void; -}; -type PassportDone = (err: Error | null, user?: IUserMongoose | false, errMessage?: { message: string }) => void; -type Profile = passport.Profile & { - profileUrl?: string; - _json: any; -}; -interface StrategyOptions { +type PassportDone = (err: Error | null, user?: IUser | false, errMessage?: { message: string }) => void; +type PassportProfileDone = (err: Error | null, profile?: IProfile) => void; +interface IStrategyOptions { passReqToCallback: true; // Forced to true for our usecase } -interface OAuthStrategyOptions extends StrategyOptions { +interface IOAuthStrategyOptions extends IStrategyOptions { + authorizationURL: string; + tokenURL: string; clientID: string; clientSecret: string; - profileFields?: string[]; } -interface CASStrategyOptions extends StrategyOptions { - casURL: string; - pgtURL?: string; - sessionKey?: string; - propertyMap?: object; - sslCA?: any[]; -} -interface LocalStrategyOptions extends StrategyOptions { - usernameField: string; - passwordField: string; -} -interface StrategyConstructor { - // OAuth constructor - new(options: OAuthStrategyOptions, cb: (request: Request, accessToken: string, refreshToken: string, profile: Profile, done: PassportDone) => Promise): Strategy; - // CAS constructor - new(options: CASStrategyOptions, cb: (request: Request, username: string, profile: Profile, done: PassportDone) => Promise): Strategy; +interface IProfile { + uuid: string; + name: string; + email: string; } + // Because the passport typedefs don't include this for some reason // Defined: https://github.com/jaredhanson/passport-oauth2/blob/9ddff909a992c3428781b7b2957ce1a97a924367/lib/strategy.js#L135 -type AuthenticateOptions = passport.AuthenticateOptions & { +export type AuthenticateOptions = passport.AuthenticateOptions & { callbackURL: string; }; -export const PBKDF2_ROUNDS: number = 300000; +export class GroundTruthStrategy extends OAuthStrategy { + public readonly url: string; -export interface RegistrationStrategy { - readonly name: string; - readonly passportStrategy: Strategy; - use(authRoutes: Router, scope?: string[]): void; -} -abstract class OAuthStrategy implements RegistrationStrategy { - public readonly passportStrategy: Strategy; - - public static get defaultUserProperties(): Partial { + public static get defaultUserProperties() { return { - "uuid": uuid(), - "verifiedEmail": false, - "accountConfirmed": false, - - "services": {}, + "admin": false, "applied": false, "accepted": false, "confirmed": false, "preConfirmEmailSent": false, - "applicationData": [], "applicationStartTime": undefined, - "applicationSubmitTime": undefined, - - "admin": false + "applicationSubmitTime": undefined }; } - constructor(public readonly name: IConfig.OAuthServices, strategy: StrategyConstructor, profileFields?: string[]) { - - const secrets = config.secrets.oauth[name]; + constructor(url: string) { + const secrets = config.secrets.groundTruth; if (!secrets || !secrets.id || !secrets.secret) { - throw new Error(`Client ID or secret not configured in config.json or environment variables for strategy "${this.name}"`); + throw new Error(`Client ID or secret not configured in config.json or environment variables for Ground Truth`); } - let options: OAuthStrategyOptions = { + let options: IOAuthStrategyOptions = { + authorizationURL: new URL("/oauth/authorize", url).toString(), + tokenURL: new URL("/oauth/token", url).toString(), clientID: secrets.id, clientSecret: secrets.secret, - profileFields, passReqToCallback: true }; - this.passportStrategy = new strategy(options, this.passportCallback.bind(this)); + super(options, GroundTruthStrategy.passportCallback); + this.url = url; } - protected async passportCallback(request: Request, accessToken: string, refreshToken: string, profile: Profile, done: PassportDone) { - let serviceName = this.name as IConfig.OAuthServices; - - let email: string = ""; - if (profile.emails && profile.emails.length > 0) { - email = profile.emails[0].value.trim(); - } - else if (!profile.emails || profile.emails.length === 0) { - done(null, false, { message: "Your GitHub profile does not have any public email addresses. Please make an email address public before logging in with GitHub." }); - return; - } - - let user = await User.findOne({[`services.${this.name}.id`]: profile.id}); - if (!user) { - user = await User.findOne({ email }); - } - let loggedInUser = request.user as IUserMongoose | undefined; - let isAdmin = false; - if (config.admins.includes(email)) { - isAdmin = true; - if (!user || !user.admin) { - console.info(`Adding new admin: ${email}`); - } - } - if (!user && !loggedInUser) { - user = new User({ - ...OAuthStrategy.defaultUserProperties, - email, - name: profile.displayName ? profile.displayName.trim() : "", - verifiedEmail: true, - admin: isAdmin - }); - if (!user.services) { - user.services = {}; - } - user.services[serviceName] = { - id: profile.id, - email, - username: profile.username, - profileUrl: profile.profileUrl - }; - try { - user.markModified("services"); - await user.save(); - trackEvent("created account (auth)", request, email); - } - catch (err) { + public userProfile(accessToken: string, done: PassportProfileDone) { + (this._oauth2 as any)._request("GET", new URL("/api/user", this.url).toString(), null, null, accessToken, (err: Error | null, data: string) => { + if (err) { done(err); return; } - - done(null, user); - } - else { - if (user && loggedInUser && user.uuid !== loggedInUser.uuid) { - // Remove extra account represented by loggedInUser and merge into user - user.services = { - ...loggedInUser.services, - // Don't overwrite any existing services - ...user.services - }; - if (loggedInUser.local && loggedInUser.local.hash && (!user.local || !user.local.hash)) { - user.local = { - ...loggedInUser.local - }; - } - await User.findOneAndRemove({ "uuid": loggedInUser.uuid }); - // So that the user has an indication of the linking - user.accountConfirmed = false; - } - else if (!user && loggedInUser) { - // Attach service info to logged in user instead of non-existant user pulled via email address - user = loggedInUser; - } - if (!user) { - done(null, false, { "message": "Shouldn't happen: no user defined" }); - return; - } - - if (!user.services) { - user.services = {}; - } - if (!user.services[serviceName]) { - user.services[serviceName] = { - id: profile.id, - email, - username: profile.username, - profileUrl: profile.profileUrl - }; - // So that the user has an indication of the linking - user.accountConfirmed = false; - } - if (!user.verifiedEmail && user.email === email) { - // We trust our OAuth provider to have verified the user's email for us - user.verifiedEmail = true; - } - if (!user.admin && isAdmin) { - user.admin = true; - } - user.markModified("services"); - await user.save(); - done(null, user); - } - } - - public use(authRoutes: Router, scope: string[]) { - passport.use(this.passportStrategy); - - const callbackHref = `auth/${this.name}/callback`; - authRoutes.get(`/${this.name}`, validateAndCacheHostName, (request, response, next) => { - let callbackURL = `${request.protocol}://${request.hostname}:${getExternalPort(request)}/${callbackHref}`; - - passport.authenticate( - this.name, - { scope, callbackURL } as AuthenticateOptions - )(request, response, next); - }); - authRoutes.get(`/${this.name}/callback`, validateAndCacheHostName, (request, response, next) => { - let callbackURL = `${request.protocol}://${request.hostname}:${getExternalPort(request)}/${callbackHref}`; - - passport.authenticate( - this.name, - { - failureRedirect: "/login", - successReturnToOrRedirect: "/", - failureFlash: true, - callbackURL - } as AuthenticateOptions - )(request, response, next); - }); - } -} - -export class GitHub extends OAuthStrategy { - constructor() { - super("github", GitHubStrategy as any); - } - public use(authRoutes: Router) { - super.use(authRoutes, ["user:email"]); - } -} - -export class Google extends OAuthStrategy { - constructor() { - super("google", GoogleStrategy); - } - public use(authRoutes: Router) { - super.use(authRoutes, ["email", "profile"]); - } -} - -export class Facebook extends OAuthStrategy { - constructor() { - super("facebook", FacebookStrategy as any, ["id", "displayName", "email"]); - } - public use(authRoutes: Router) { - super.use(authRoutes, ["email"]); - } -} - -abstract class CASStrategy implements RegistrationStrategy { - public readonly passportStrategy: Strategy; - - constructor(public readonly name: IConfig.CASServices, url: string, private readonly emailDomain: string) { - this.passportStrategy = new CASStrategyProvider({ - casURL: url, - passReqToCallback: true - }, this.passportCallback.bind(this)); - } - - private async passportCallback(request: Request, username: string, profile: Profile, done: PassportDone) { - // GT login will pass long invalid usernames of different capitalizations - username = username.toLowerCase().trim(); - let loggedInUser = request.user as IUserMongoose | undefined; - let user = await User.findOne({[`services.${this.name}.id`]: username}); - let email = `${username}@${this.emailDomain}`; - let isAdmin = false; - - if (config.admins.includes(email)) { - isAdmin = true; - if (!user || !user.admin) { - console.info(`Adding new admin: ${email}`); - } - } - if (!user && !loggedInUser) { - user = new User({ - ...OAuthStrategy.defaultUserProperties, - email, - name: "", - verifiedEmail: false, - admin: isAdmin - }); - if (!user.services) { - user.services = {}; - } - user.services[this.name] = { - id: username, - email, - username - }; try { - user.markModified("services"); - await user.save(); - trackEvent("created account (auth)", request, email); + let profile: IProfile = JSON.parse(data); + done(null, profile); } catch (err) { - done(err); - return; - } - - done(null, user); - } - else { - if (user && loggedInUser && user.uuid !== loggedInUser.uuid) { - // Remove extra account represented by loggedInUser and merge into user - user.services = { - ...loggedInUser.services, - // Don't overwrite any existing services - ...user.services - }; - if (loggedInUser.local && loggedInUser.local.hash && (!user.local || !user.local.hash)) { - user.local = { - ...loggedInUser.local - }; - } - await User.findOneAndRemove({ "uuid": loggedInUser.uuid }); - // So that the user has an indication of the linking - user.accountConfirmed = false; - } - else if (!user && loggedInUser) { - // Attach service info to logged in user instead of non-existant user pulled via email address - user = loggedInUser; - } - if (!user) { - done(null, false, { "message": "Shouldn't happen: no user defined" }); - return; - } - - if (!user.services) { - user.services = {}; - } - if (!user.services[this.name]) { - user.services[this.name] = { - id: username, - email, - username - }; - } - if (!user.admin && isAdmin && user.email === email && user.verifiedEmail) { - user.admin = true; - } - user.markModified("services"); - await user.save(); - if (!user.verifiedEmail && user.accountConfirmed) { - done(null, false, { "message": "You must verify your email before you can sign in" }); - return; + return done(err); } - done(null, user); - } - } - - public use(authRoutes: Router) { - passport.use(this.name, this.passportStrategy); - - authRoutes.get(`/${this.name}`, passport.authenticate(this.name, { - failureRedirect: "/login", - successReturnToOrRedirect: "/", - failureFlash: true - })); - } -} - -export class GeorgiaTechCAS extends CASStrategy { - constructor() { - // Registration must be hosted on a *.hack.gt domain for this to work - super("gatech", "https://login.gatech.edu/cas", "gatech.edu"); - } -} - -export class Local implements RegistrationStrategy { - public readonly name = "local"; - public readonly passportStrategy: Strategy; - - constructor() { - let options: LocalStrategyOptions = { - usernameField: "email", - passwordField: "password", - passReqToCallback: true - }; - this.passportStrategy = new LocalStrategy(options, this.passportCallback.bind(this)); + }); } - protected async passportCallback(request: Request, email: string, password: string, done: PassportDone) { - email = email.trim(); - let user = await User.findOne({ email }); - if (user && request.path.match(/\/signup$/i)) { - done(null, false, { "message": "That email address is already in use. You may already have an account from another login service." }); - } - else if (user && !user.local!.hash) { - done(null, false, { "message": "Please log back in with an external provider" }); - } - else if (!user || !user.local) { - // User hasn't signed up yet - if (!request.path.match(/\/signup$/i)) { - // Only create the user when targeting /signup - done(null, false, { "message": "Incorrect email or password" }); - return; - } - let name: string = request.body.name || ""; - name = name.trim(); - if (!name || !email || !password) { - done(null, false, { "message": "Missing email, name, or password" }); - return; - } - let salt = crypto.randomBytes(32); - let hash = await pbkdf2Async(password, salt, PBKDF2_ROUNDS); - user = new User({ - ...OAuthStrategy.defaultUserProperties, - email, - name: request.body.name, - verifiedEmail: false, - local: { - "hash": hash.toString("hex"), - "salt": salt.toString("hex") - } + protected static async passportCallback(request: Request, accessToken: string, refreshToken: string, profile: IProfile, done: PassportDone) { + let user = await User.findOne({ uuid: profile.uuid }); + if (!user) { + user = createNew(User, { + ...GroundTruthStrategy.defaultUserProperties, + ...profile }); - try { - await user.save(); - trackEvent("created account", request, email); - } - catch (err) { - done(err); - return; - } - done(null, user); + await user.save(); } - else { - // Log the user in - let hash = await pbkdf2Async(password, Buffer.from(user.local.salt || "", "hex"), PBKDF2_ROUNDS); - if (hash.toString("hex") === user.local.hash) { - if (user.verifiedEmail) { - done(null, user); - } - else { - done(null, false, { "message": "You must verify your email before you can sign in" }); - } - } - else { - done(null, false, { "message": "Incorrect email or password" }); - } + if (config.admins.includes(profile.email) && !user.admin) { + user.admin = true; + await user.save(); } - } - - public use(authRoutes: Router) { - passport.use(this.passportStrategy); - - authRoutes.post("/signup", validateAndCacheHostName, postParser, passport.authenticate("local", { failureRedirect: "/login", failureFlash: true }), (request, response) => { - // User is logged in automatically by Passport but we want them to verify their email first - response.redirect("/login/confirm"); - }); - - authRoutes.post("/login", postParser, passport.authenticate("local", { failureRedirect: "/login", failureFlash: true, successReturnToOrRedirect: "/" })); - - authRoutes.get("/verify/:code", async (request, response) => { - let user = await User.findOne({ "local.verificationCode": request.params.code }); - if (!user) { - request.flash("error", "Invalid email verification code"); - } - else { - user.verifiedEmail = true; - if (user.local) { - user.local.verificationCode = undefined; - } - // Possibly promote to admin status - if (config.admins.indexOf(user.email) !== -1) { - user.admin = true; - console.info(`Adding new admin: ${user.email}`); - } - await user.save(); - request.flash("success", "Thanks for verifying your email. You can now log in."); - trackEvent("verified email", request, user.email); - } - response.redirect("/login"); - }); - - authRoutes.post("/forgot", validateAndCacheHostName, postParser, async (request, response) => { - let email: string | undefined = request.body.email; - if (!email || !email.toString().trim()) { - request.flash("error", "Invalid email"); - response.redirect("/login/forgot"); - return; - } - email = email.toString().trim(); - - let user = await User.findOne({ email }); - if (!user) { - request.flash("error", "No account matching the email that you submitted was found"); - response.redirect("/login/forgot"); - return; - } - if (!user.verifiedEmail) { - request.flash("error", "Please verify your email first"); - response.redirect("/login"); - return; - } - if (!user.local || !user.local.hash) { - request.flash("error", "The account with the email that you submitted has no password set. Please log in with an external service like GitHub, Google, or Facebook instead."); - response.redirect("/login"); - return; - } - - user.local.resetRequested = true; - user.local.resetRequestedTime = new Date(); - user.local.resetCode = crypto.randomBytes(32).toString("hex"); - - // Send reset email (hostname validated by previous middleware) - let link = createLink(request, `/auth/forgot/${user.local.resetCode}`); - let markdown = -`Hi {{name}}, - -You (or someone who knows your email address) recently asked to reset the password for this account: {{email}}. - -You can update your password by [clicking here](${link}). - -If you don't use this link within ${moment.duration(config.server.passwordResetExpiration, "milliseconds").humanize()}, it will expire and you will have to [request a new one](${createLink(request, "/login/forgot")}). - -If you didn't request a password reset, you can safely disregard this email and no changes will be made to your account. - -Sincerely, - -The ${config.eventName} Team.`; - try { - await user.save(); - await sendMailAsync({ - from: config.email.from, - to: email, - subject: `[${config.eventName}] - Password reset request`, - html: await renderEmailHTML(markdown, user), - text: await renderEmailText(markdown, user) - }); - request.flash("success", "Please check your email for a link to reset your password. If it doesn't appear within a few minutes, check your spam folder."); - response.redirect("/login/forgot"); - } - catch (err) { - console.error(err); - request.flash("error", "An error occurred while sending you a password reset email"); - response.redirect("/login/forgot"); - } - }); - - authRoutes.post("/forgot/:code", validateAndCacheHostName, postParser, async (request, response) => { - let user = await User.findOne({ "local.resetCode": request.params.code }); - if (!user) { - request.flash("error", "Invalid password reset code"); - response.redirect("/login"); - return; - } - - let expirationDuration = moment.duration(config.server.passwordResetExpiration, "milliseconds"); - if (!user.local!.resetRequested || moment().isAfter(moment(user.local!.resetRequestedTime).add(expirationDuration))) { - request.flash("error", "Your password reset link has expired. Please request a new one."); - user.local!.resetCode = ""; - user.local!.resetRequested = false; - await user.save(); - response.redirect("/login"); - return; - } - - let password1: string | undefined = request.body.password1; - let password2: string | undefined = request.body.password2; - if (!password1 || !password2) { - request.flash("error", "Missing new password or confirm password"); - response.redirect(path.join("/auth", request.url)); - return; - } - if (password1 !== password2) { - request.flash("error", "Passwords must match"); - response.redirect(path.join("/auth", request.url)); - return; - } - - let salt = crypto.randomBytes(32); - let hash = await pbkdf2Async(password1, salt, PBKDF2_ROUNDS); - - try { - user.local!.salt = salt.toString("hex"); - user.local!.hash = hash.toString("hex"); - user.local!.resetCode = ""; - user.local!.resetRequested = false; - await user.save(); - - request.flash("success", "Password reset successfully. You can now log in."); - response.redirect("/login"); - } - catch (err) { - console.error(err); - request.flash("error", "An error occurred while saving your new password"); - response.redirect(path.join("/auth", request.url)); - } - }); + done(null, user); } } -export const strategies = { - "local": Local, - "gatech": GeorgiaTechCAS, - "github": GitHub, - "google": Google, - "facebook": Facebook -}; -export const prettyNames: Record = { - "local": "Local", - "gatech": "Georgia Tech CAS", - "github": "GitHub", - "google": "Google", - "facebook": "Facebook" -}; - // Authentication helpers function getExternalPort(request: Request): number { function defaultPort(): number { @@ -685,7 +161,7 @@ export function validateAndCacheHostName(request: Request, response: Response, n } } -function createLink(request: Request, link: string): string { +export function createLink(request: Request, link: string): string { if (link[0] === "/") { link = link.substring(1); } @@ -696,29 +172,3 @@ function createLink(request: Request, link: string): string { return `http${request.secure ? "s" : ""}://${request.hostname}:${getExternalPort(request)}/${link}`; } } - -export async function sendVerificationEmail(request: Request, user: IUserMongoose) { - // Send verification email (hostname validated by previous middleware) - if (!user.local) { - user.local = {}; - } - user.local.verificationCode = crypto.randomBytes(32).toString("hex"); - await user.save(); - - let link = createLink(request, `/auth/verify/${user.local.verificationCode}`); - let markdown = -`Hi {{name}}, - -Thanks for signing up for ${config.eventName}! To verify your email, please [click here](${link}). - -Sincerely, - -The ${config.eventName} Team.`; - await sendMailAsync({ - from: config.email.from, - to: user.email, - subject: `[${config.eventName}] - Verify your email`, - html: await renderEmailHTML(markdown, user), - text: await renderEmailText(markdown, user) - }); -} diff --git a/server/routes/templates.ts b/server/routes/templates.ts index 0caab1b5..8994ada4 100644 --- a/server/routes/templates.ts +++ b/server/routes/templates.ts @@ -15,25 +15,20 @@ import { onlyAllowAnonymousBranch, branchRedirector, ApplicationType } from "../middleware"; import { - IUser, IUserMongoose, User, - ITeamMongoose, Team, - IIndexTemplate, ILoginTemplate, IAdminTemplate, ITeamTemplate, + Model, + IUser, User, + ITeam, Team, + IIndexTemplate, IAdminTemplate, ITeamTemplate, IRegisterBranchChoiceTemplate, IRegisterTemplate, StatisticEntry, - IFormItem, - IConfig + IFormItem } from "../schema"; import * as Branches from "../branch"; -import { strategies, prettyNames } from "../routes/strategies"; export let templateRoutes = express.Router(); // Load and compile Handlebars templates let [ indexTemplate, - loginTemplate, - postLoginTemplate, - forgotPasswordTemplate, - resetPasswordTemplate, preregisterTemplate, preconfirmTemplate, registerTemplate, @@ -43,10 +38,6 @@ let [ teamTemplate ] = [ "index.html", - "login.html", - "postlogin.html", - "forgotpassword.html", - "resetpassword.html", "preapplication.html", "preconfirmation.html", "application.html", @@ -322,96 +313,26 @@ templateRoutes.route("/login").get(async (request, response) => { if (request.session && request.query.r && request.query.r.startsWith('/')) { request.session.returnTo = request.query.r; } - let loginMethods = await getSetting("loginMethods"); - let templateData: ILoginTemplate = { - siteTitle: config.eventName, - error: request.flash("error"), - success: request.flash("success"), - loginMethods, - localOnly: loginMethods && loginMethods.length === 1 && loginMethods[0] === "local" - }; - response.send(loginTemplate(templateData)); -}); -templateRoutes.route("/login/confirm").get(async (request, response) => { - let user = request.user as IUser; - if (!user) { - response.redirect("/login"); - return; - } - if (user.accountConfirmed) { - response.redirect("/"); - return; - } - - let usedLoginMethods: string[] = []; - if (user.local && user.local!.hash) { - usedLoginMethods.push("Local"); - } - let services = Object.keys(user.services || {}) as (keyof typeof user.services)[]; - for (let service of services) { - usedLoginMethods.push(prettyNames[service]); - } - let loginMethods = (await getSetting("loginMethods")).filter(method => method !== "local" && !services.includes(method)); - - response.send(postLoginTemplate({ - siteTitle: config.eventName, - error: request.flash("error"), - success: request.flash("success"), - - name: user.name || "", - email: user.email || "", - verifiedEmail: user.verifiedEmail || false, - usedLoginMethods, - loginMethods, - canAddLogins: loginMethods.length !== 0 - })); -}); -templateRoutes.route("/login/forgot").get((request, response) => { - let templateData: ILoginTemplate = { - siteTitle: config.eventName, - error: request.flash("error"), - success: request.flash("success") - }; - response.send(forgotPasswordTemplate(templateData)); -}); -templateRoutes.route("/auth/forgot/:code").get(async (request, response) => { - let user = await User.findOne({ "local.resetCode": request.params.code }); - if (!user) { - request.flash("error", "Invalid password reset code"); - response.redirect("/login"); - return; - } - else if (!user.local!.resetRequested || Date.now() - user.local!.resetRequestedTime!.valueOf() > 1000 * 60 * 60) { - request.flash("error", "Your password reset link has expired. Please request a new one."); - user.local!.resetCode = ""; - user.local!.resetRequested = false; - await user.save(); - response.redirect("/login"); - return; - } - let templateData: ILoginTemplate = { - siteTitle: config.eventName, - error: request.flash("error"), - success: request.flash("success") - }; - response.send(resetPasswordTemplate(templateData)); + response.redirect("/auth/login"); }); templateRoutes.route("/team").get(authenticateWithRedirect, async (request, response) => { - let team: ITeamMongoose | null = null; - let membersAsUsers: IUserMongoose[] | null = null; - let teamLeaderAsUser: IUserMongoose | null = null; + let team: ITeam | null = null; + let membersAsUsers: IUser[] | null = null; + let teamLeaderAsUser: IUser | null = null; let isCurrentUserTeamLeader = false; if (request.user && request.user.teamId) { - team = await Team.findById(request.user.teamId) as ITeamMongoose; - membersAsUsers = await User.find({ - _id: { - $in: team.members - } - }); - teamLeaderAsUser = await User.findById(team.teamLeader) as IUserMongoose; - isCurrentUserTeamLeader = teamLeaderAsUser._id.toString() === request.user._id.toString(); + team = await Team.findById(request.user.teamId); + if (team) { + membersAsUsers = await User.find({ + _id: { + $in: team.members + } + }); + teamLeaderAsUser = await User.findById(team.teamLeader); + isCurrentUserTeamLeader = teamLeaderAsUser != null && teamLeaderAsUser._id.toString() === request.user._id.toString(); + } } let templateData: ITeamTemplate = { @@ -450,7 +371,7 @@ function applicationHandler(requestType: ApplicationType): (request: express.Req // NOTE: this assumes the user is still able to apply as this type at this point if (requestType === ApplicationType.Application) { if (user.applied) { - questionBranches = [user.applicationBranch.toLowerCase()]; + questionBranches = [user.applicationBranch!.toLowerCase()]; } else { const branches = await Branches.BranchConfig.getOpenBranches("Application"); @@ -523,7 +444,7 @@ function applicationBranchHandler(requestType: ApplicationType, anonymous: boole let questionData = await Promise.all(questionBranch.questions.map(async question => { let savedValue: IFormItem | undefined; if (user) { - savedValue = user[requestType === ApplicationType.Application ? "applicationData" : "confirmationData"].find(item => item.name === question.name); + savedValue = (user[requestType === ApplicationType.Application ? "applicationData" : "confirmationData"] || []).find(item => item.name === question.name); } if (question.type === "checkbox" || question.type === "radio" || question.type === "select") { @@ -589,7 +510,7 @@ function applicationBranchHandler(requestType: ApplicationType, anonymous: boole } if (!anonymous) { - let thisUser = await User.findById(user._id) as IUserMongoose; + let thisUser = await User.findById(user._id) as Model; // TODO this is a bug - dates are wrong if (requestType === ApplicationType.Application && !thisUser.applicationStartTime) { thisUser.applicationStartTime = new Date(); @@ -630,16 +551,6 @@ templateRoutes.route("/admin").get(authenticateWithRedirect, async (request, res let teamsEnabled = await getSetting("teamsEnabled"); let qrEnabled = await getSetting("qrEnabled"); - type StrategyNames = keyof typeof strategies; - let enabledMethods = await getSetting("loginMethods"); - let loginMethodsInfo = Object.keys(strategies).map((name: StrategyNames) => { - return { - name: prettyNames[name], - raw: name, - enabled: enabledMethods.includes(name) - }; - }); - let adminEmails = await User.find({ admin: true }).select("email"); let noopBranches = (await Branches.BranchConfig.loadAllBranches("Noop")) as Branches.NoopBranch[]; @@ -649,7 +560,7 @@ templateRoutes.route("/admin").get(authenticateWithRedirect, async (request, res let teamIDNameMap: { [id: string]: string; } = {}; - (await Team.find()).forEach((team: ITeamMongoose) => { + (await Team.find()).forEach(team => { teamIDNameMap[team._id.toString()] = team.teamName; }); @@ -706,7 +617,6 @@ templateRoutes.route("/admin").get(authenticateWithRedirect, async (request, res }; }) }, - loginMethodsInfo, adminEmails, apiKey: config.secrets.adminKey }, @@ -730,11 +640,11 @@ templateRoutes.route("/admin").get(authenticateWithRedirect, async (request, res // Generate general statistics (await User.find({ "applied": true })).forEach(async statisticUser => { - let appliedBranch = applicationBranchMap[statisticUser.applicationBranch]; + let appliedBranch = applicationBranchMap[statisticUser.applicationBranch!]; if (!appliedBranch) { return; } - statisticUser.applicationData.forEach(question => { + statisticUser.applicationData!.forEach(question => { if (question.value === null) { return; } @@ -758,7 +668,7 @@ templateRoutes.route("/admin").get(authenticateWithRedirect, async (request, res statisticEntry = { questionName, questionLabel: removeTags(rawQuestion.label), - branch: statisticUser.applicationBranch, + branch: statisticUser.applicationBranch!, responses: [] }; templateData.generalStatistics.push(statisticEntry); diff --git a/server/schema.ts b/server/schema.ts index b7d92f1f..db032a48 100644 --- a/server/schema.ts +++ b/server/schema.ts @@ -12,11 +12,10 @@ export namespace IConfig { export interface Secrets { adminKey: string; session: string; - oauth: { - [Service in OAuthServices]: { - id: string; - secret: string; - } + groundTruth: { + url: string; + id: string; + secret: string; }; } export interface Email { @@ -45,7 +44,6 @@ export namespace IConfig { email: Email; server: Server; style: Style; - admins: string[]; eventName: string; questionsLocation: string; storageEngine: { @@ -63,60 +61,47 @@ export interface IFormItem { "value": string | string[] | Express.Multer.File | null; } -export interface ITeam { +// For stricter type checking of new object creation +type Omit = Pick>; +interface RootDocument { _id: mongoose.Types.ObjectId; +} +export function createNew(model: mongoose.Model, doc: Omit) { + return new model(doc); +} +export type Model = T & mongoose.Document; + +export interface ITeam extends RootDocument { teamLeader: mongoose.Types.ObjectId; members: mongoose.Types.ObjectId[]; teamName: string; } -export type ITeamMongoose = ITeam & mongoose.Document; - -export const Team = mongoose.model("Team", new mongoose.Schema({ +export const Team = mongoose.model>("Team", new mongoose.Schema({ teamLeader: { type: mongoose.Schema.Types.ObjectId }, members: [{ type: mongoose.Schema.Types.ObjectId }], - teamName: { - type: mongoose.Schema.Types.String - } + teamName: String })); -export interface IUser { - _id: mongoose.Types.ObjectId; +export interface IUser extends RootDocument { uuid: string; email: string; name: string; - verifiedEmail: boolean; - accountConfirmed: boolean; - local?: Partial<{ - hash: string; - salt: string; - verificationCode: string; - resetRequested: boolean; - resetCode: string; - resetRequestedTime: Date; - }>; - services: { - [Service in Exclude]?: { - id: string; - // OAuth account email can be different than registration account email - email: string; - username?: string; - profileUrl?: string; - }; - }; + teamId?: mongoose.Types.ObjectId; + admin: boolean; applied: boolean; accepted: boolean; preConfirmEmailSent: boolean; confirmed: boolean; - applicationBranch: string; + applicationBranch?: string; reimbursementAmount?: string; - applicationData: IFormItem[]; + applicationData?: IFormItem[]; applicationStartTime?: Date; applicationSubmitTime?: Date; @@ -127,18 +112,14 @@ export interface IUser { }; confirmationBranch?: string; - confirmationData: IFormItem[]; + confirmationData?: IFormItem[]; confirmationStartTime?: Date; confirmationSubmitTime?: Date; - admin?: boolean; - - teamId?: mongoose.Types.ObjectId; } -export type IUserMongoose = IUser & mongoose.Document; // This is basically a type definition that exists at runtime and is derived manually from the IUser definition above -export const User = mongoose.model("User", new mongoose.Schema({ +export const User = mongoose.model>("User", new mongoose.Schema({ uuid: { type: String, required: true, @@ -155,23 +136,13 @@ export const User = mongoose.model("User", new mongoose.Schema({ type: String, index: true }, - verifiedEmail: Boolean, - accountConfirmed: Boolean, - - local: { - hash: String, - salt: String, - verificationCode: String, - resetRequested: Boolean, - resetCode: String, - resetRequestedTime: Date - }, - services: mongoose.Schema.Types.Mixed, teamId: { type: mongoose.Schema.Types.ObjectId }, + admin: Boolean, + applied: Boolean, accepted: Boolean, preConfirmEmailSent: Boolean, @@ -191,22 +162,18 @@ export const User = mongoose.model("User", new mongoose.Schema({ confirmationBranch: String, confirmationData: [mongoose.Schema.Types.Mixed], confirmationStartTime: Date, - confirmationSubmitTime: Date, - - admin: Boolean + confirmationSubmitTime: Date }).index({ email: "text", name: "text" })); -export interface ISetting { - _id: mongoose.Types.ObjectId; +export interface ISetting extends RootDocument { name: string; value: any; } -export type ISettingMongoose = ISetting & mongoose.Document; -export const Setting = mongoose.model("Setting", new mongoose.Schema({ +export const Setting = mongoose.model>("Setting", new mongoose.Schema({ name: { type: String, required: true, @@ -228,16 +195,14 @@ export interface QuestionBranchSettings { isAcceptance?: boolean; // Used by confirmation branch autoConfirm?: boolean; // Used by confirmation branch } -export interface IQuestionBranchConfig { - _id: mongoose.Types.ObjectId; +export interface IQuestionBranchConfig extends RootDocument { name: string; type: QuestionBranchType; settings: QuestionBranchSettings; location: string; } -export type IQuestionBranchConfigMongoose = IQuestionBranchConfig & mongoose.Document; -export const QuestionBranchConfig = mongoose.model("QuestionBranchConfig", new mongoose.Schema({ +export const QuestionBranchConfig = mongoose.model>("QuestionBranchConfig", new mongoose.Schema({ name: { type: String, required: true, @@ -302,18 +267,11 @@ export interface IIndexTemplate extends ICommonTemplate { }[]; } export interface ITeamTemplate extends ICommonTemplate { - team?: ITeamMongoose | null; - membersAsUsers?: IUserMongoose[] | null; - teamLeaderAsUser?: IUserMongoose | null; + team?: ITeam | null; + membersAsUsers?: IUser[] | null; + teamLeaderAsUser?: IUser | null; isCurrentUserTeamLeader: boolean; } -export interface ILoginTemplate { - siteTitle: string; - error?: string; - success?: string; - loginMethods?: string[]; - localOnly?: boolean; -} export interface IRegisterBranchChoiceTemplate extends ICommonTemplate { branches: string[]; } @@ -371,12 +329,7 @@ export interface IAdminTemplate extends ICommonTemplate { close: string; }[]; }; - loginMethodsInfo: { - name: string; - raw: string; - enabled: boolean; - }[]; - adminEmails: IUserMongoose[]; + adminEmails: IUser[]; apiKey: string; }; config: { From 82ac003460ee6ae6cb5b70b110b8ffac3cfbf7d7 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Tue, 19 Mar 2019 23:00:56 -0600 Subject: [PATCH 134/194] Update README to reflect auth changes --- README.md | 60 ++++++++++--------------------------------- server/common.ts | 5 +--- server/middleware.ts | 2 +- server/routes/auth.ts | 2 +- 4 files changed, 16 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 868eeeaa..ae624653 100644 --- a/README.md +++ b/README.md @@ -9,15 +9,15 @@ ## Features -- Seamless OAuth and local signup logins with automatic email verification - - Get users up and running quickly with GitHub, Google, and Facebook OAuth logins right out of the box ([MyMLH](https://my.mlh.io) login planned) - - Full support for local logins as well if users choose +- Seamless OAuth and local signup logins with automatic email verification via [HackGT Ground Truth](https://github.com/HackGT/ground-truth) + - Get users up and running quickly with GitHub, Google, and Facebook OAuth or Georgia Tech CAS logins right out of the box + - Full support for local logins as well if users prefer - Users can easily register (and confirm their attendance if accepted) and choose which "branch" they want to complete (e.g. partipant, mentor, volunteer) all from a single location - Users can create or join a team any time before or after completing registration. Admins can configure the maximum team size (defaults to 4). - For admins, the admin panel contains options for managing all aspects of registration including: - Statistics about the user of sign ups, registrations, acceptances, and confirmations - Graphs displaying aggregated registration data - - List of all users in a table including name, email, email verified status, admin status, and application status, and log in method + - List of all users in a table including name, email, admin status, and application status, and log in method - List of all applicants with application responses and accept / unaccept button sortable by application branch and accepted status - Acceptance emails are sent out only when a send acceptance emails button is clicked allowing for decisions to be reviewed before being finalized - Setting application and confirmation open and close times as well as what question branches from `questions.json` are for applications, confirmations, or hidden @@ -60,61 +60,27 @@ A [Dockerfile](Dockerfile) is provided for convenience. Configuration should normally be done by editing the `server/config/config.json` file. Environment variables take precedence over `config.json` and should be used when those options need to be overridden or `config.json` can't be used for some reason (e.g. certain deployment scenarios). -### OAuth IDs and secrets - -Can be obtained from: -- [GitHub](https://github.com/settings/developers) - - Register a new application - - Application name, URL, and description are up to you - - Callback URL should be in the format: `https://YOUR_DOMAIN/auth/github/callback` - - Local testing callback URL should be `http://localhost:3000/auth/github/callback` - - GitHub only lets you register one callback URL per application so you might want to make a testing application and a separate application for production usage. -- [Google API Console](https://console.developers.google.com/apis/credentials) - - Create an application - - Go to the credentials tab in the left panel - - Click Create credentials > OAuth client ID - - Set web application as the application type - - Give it a name (won't be shown publically, e.g. `HackGT Registration server testing`) - - Leave Authorized JavaScript origins blank - - List all testing / production callback URLs in Authorized redirect URIs - - Should be in the format: `https://YOUR_DOMAIN/auth/google/callback` - - For local testing: `http://localhost:3000/auth/google/callback` - - It is recommended that you create two OAuth applications with different IDs and secrets for testing and production usage. -- [Facebook](https://developers.facebook.com/) - - Create an application - - Add the Facebook Login product from the left panel - - Enable Client OAuth Login, Web OAuth Login, and Embedded Browser OAuth Login - - List all testing / production callback URLs in Valid OAuth redirect URLs - - Should be in the format: `https://YOUR_DOMAIN/auth/facebook/callback` - - For local testing: `http://localhost:3000/auth/facebook/callback` - - Optionally, repeat the process for separate testing and production applications - Environment Variable | Description ---------------------|------------ -PRODUCTION | Set to `true` to set OAuth callbacks to production URLs (default: `false`) +PRODUCTION | Set to `true` to enable reverse proxy trusting (default: `false`) PORT | The port the check in system should run on (default: `3000`) -MONGO_URL | The URL to the MongoDB server (default: `mongodb://localhost/`) -UNIQUE_APP_ID | The MongoDB database name to store data in (default: `registration`) +MONGO_URL | The URL to the MongoDB server (default: `mongodb://localhost/registration`) VERSION_HASH | The Git short hash used to identify the current commit (default: parsed automatically from the `.git` folder, if it exists) -*SOURCE_VERSION* | Same as `VERSION_HASH` but overrides it if present. Used by Deis. -*WORKFLOW_RELEASE_CREATED_AT* | Provided by Deis (default: `null`) -*WORKFLOW_RELEASE_SUMMARY* | Provided by Deis (default: `null`) +ADMIN_KEY_SECRET | A API key used to authenticate as admin an access the GraphQL api (default: random key that changes every server restart) COOKIE_MAX_AGE | The `maxAge` of cookies set in milliseconds (default: 6 months) **NOTE: this is different from the session TTL** COOKIE_SECURE_ONLY | Whether session cookies should sent exclusively over secure connections (default: `false`) PASSWORD_RESET_EXPIRATION | The time that password reset links sent via email should be valid for in milliseconds (default: 1 hour) SESSION_SECRET | The secret used to sign and validate session cookies (default: random 32 bytes regenerated on every start up) -GITHUB_CLIENT_ID | OAuth client ID for GitHub *required* -GITHUB_CLIENT_SECRET | OAuth client secret for GitHub *required* -GOOGLE_CLIENT_ID | OAuth client ID for Google *required* -GOOGLE_CLIENT_SECRET | OAuth client secret for Google *required* -FACEBOOK_CLIENT_ID | OAuth client ID for Facebook *required* -FACEBOOK_CLIENT_SECRET | OAuth client secret for Facebook *required* +GROUND_TRUTH_URL | Base URL of [Ground Truth](https://github.com/HackGT/ground-truth) instance (e.g. `https://login.hack.gt`) *required* +GROUND_TRUTH_ID | OAuth client ID from Ground Truth *required* +GROUND_TRUTH_SECRET | OAuth client secret from Ground Truth *required* EMAIL_FROM | The `From` header for sent emails (default: `HackGT Team `) EMAIL_KEY | The SendGrid API key for sending emails (default: *none*) ADMIN_EMAILS | A JSON array of the emails of the users that you want promoted to admin status when they create their account (default: none) EVENT_NAME | The current event's name which affects rendered templates and sent emails (default: `Untitled Event`) STORAGE_ENGINE | The name of the storage engine that handles file uploads as defined in [storage.ts](server/storage.ts) (default: `disk`) STORAGE_ENGINE_OPTIONS | JSON-encoded object containing options to be passed to the storage engine. Must at least contain a value for the `uploadDirectory` key. For the default `disk` storage engine, this directory is relative to the app's root, can be absolute, and will be created if it doesn't exist. (default: `{ "uploadDirectory": "uploads" }`) +DEFAULT_TIMEZONE | Timezone used for dates and times (default: `America/New_York`) MAX_TEAM_SIZE | The maximum number of users allowed per team (default: `4`) QUESTIONS_FILE | Specify a path for the `questions.json` file. THEME_FILE | Specify a path for the `theme.css` file, which will be loaded last at every page. @@ -124,7 +90,7 @@ FAVICON_FILE_BASE64 | Same as `FAVICON_FILE_BASE64` but the file is base64 encod ## Contributing -If you happen to find a bug or have a feature you'd like to see implemented, please [file an issue](https://github.com/HackGT/registration/issues). +If you happen to find a bug or have a feature you'd like to see implemented, please [file an issue](https://github.com/HackGT/registration/issues). If you have some time and want to help us out with development, thank you! You can get started by taking a look at the open issues, particularly the ones marked [help wanted](https://github.com/HackGT/registration/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) or [help wanted - beginner](https://github.com/HackGT/registration/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted+-+beginner%22). Feel free to ask questions to clarify things, determine the best way to implement a new feature or bug fix, or anything else! @@ -140,4 +106,4 @@ If you have some time and want to help us out with development, thank you! You c ## License -Copyright © 2018 HackGT. Released under the MIT license. See [LICENSE](LICENSE) for more information. +Copyright © 2019 HackGT. Released under the MIT license. See [LICENSE](LICENSE) for more information. diff --git a/server/common.ts b/server/common.ts index 854a39ba..c6711c5d 100644 --- a/server/common.ts +++ b/server/common.ts @@ -34,7 +34,7 @@ class Config implements IConfig.Main { workflowReleaseSummary: null, cookieMaxAge: 1000 * 60 * 60 * 24 * 30 * 6, // 6 months cookieSecureOnly: false, - mongoURL: "mongodb://localhost/", + mongoURL: "mongodb://localhost/registration", passwordResetExpiration: 1000 * 60 * 60, // 1 hour defaultTimezone: "America/New_York" }; @@ -121,9 +121,6 @@ class Config implements IConfig.Main { if (process.env.ADMIN_KEY_SECRET) { this.secrets.adminKey = process.env.ADMIN_KEY_SECRET; } - else { - console.warn("Setting random admin key! Cannot use the service-to-service APIs."); - } if (process.env.SESSION_SECRET) { this.secrets.session = process.env.SESSION_SECRET; this.sessionSecretSet = true; diff --git a/server/middleware.ts b/server/middleware.ts index 64f364ac..332d91b5 100644 --- a/server/middleware.ts +++ b/server/middleware.ts @@ -76,7 +76,7 @@ export function isAdmin(request: express.Request, response: express.Response, ne const auth = request.headers.authorization; if (auth && typeof auth === "string" && auth.indexOf(" ") > -1) { - const key = new Buffer(auth.split(" ")[1], "base64").toString(); + const key = Buffer.from(auth.split(" ")[1], "base64").toString(); if (key === config.secrets.adminKey) { next(); } diff --git a/server/routes/auth.ts b/server/routes/auth.ts index 9ff6f31a..6e450dc0 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -71,7 +71,7 @@ authRoutes.get("/login", validateAndCacheHostName, (request, response, next) => }); authRoutes.get("/login/callback", validateAndCacheHostName, (request, response, next) => { let callbackURL = createLink(request, "auth/login/callback"); - passport.authenticate("oauth2", { callbackURL } as AuthenticateOptions, (err: Error | null, user?: IUser) { + passport.authenticate("oauth2", { callbackURL } as AuthenticateOptions, (err: Error | null, user?: IUser) => { if (err) { console.error(err); next(err); From 2a508bd0747c71960936e56d5bf9d807d5b2408a Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Tue, 19 Mar 2019 23:34:14 -0600 Subject: [PATCH 135/194] Update Node version --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 0ac90087..a85a7d2e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:10-alpine +FROM node:11-alpine MAINTAINER Ryan Petschek # Deis wants bash From 0f955715258292b2fd380e04a4e238b321cf9ff1 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Tue, 19 Mar 2019 23:34:58 -0600 Subject: [PATCH 136/194] Bump version to 3.0.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index ee1044f7..72471f16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "2.3.8", + "version": "3.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 5eeb45fe..88c1e952 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "2.3.8", + "version": "3.0.0", "description": "Powerful and extensible registration system for hackathons and other large events", "main": "server/app.js", "scripts": { From a3326ca9032d0caa7a70f26c0e1cf6f975077417 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Wed, 20 Mar 2019 22:12:10 -0600 Subject: [PATCH 137/194] Implement intermediate login page used for login errors and logouts --- client/css/login.css | 98 -------------------------------------- client/login.html | 42 ++++++++++++++++ server/routes/auth.ts | 33 ++++++------- server/routes/templates.ts | 95 +++++++++++++++++++++++------------- server/schema.ts | 7 +++ 5 files changed, 125 insertions(+), 150 deletions(-) delete mode 100644 client/css/login.css create mode 100644 client/login.html diff --git a/client/css/login.css b/client/css/login.css deleted file mode 100644 index 602d5f0a..00000000 --- a/client/css/login.css +++ /dev/null @@ -1,98 +0,0 @@ -h1 { - margin-bottom: 64px; - text-align: center; -} -a > i { - margin-right: 10px; - transform: scale(1.5); -} -a.github { - color: white; - background-color: #444444; -} -a.google { - color: white; - background-color: #4285F4; -} -a.facebook { - color: white; - background-color: #3b5998; -} -a.gatech { - color: black; - background-color: #eeb211; -} -#error, #success { - text-align: center; - margin-top: 1em; - font-size: 2rem; - margin-bottom: 1em; - color: white; - padding: 10px; - border-radius: 5px; -} -#error { - background-color: rgba(255, 65, 54, 0.85); -} -#success { - background-color: #3D9970; -} -#error:empty, #success:empty { - display: none; -} - -main { - display: flex; - flex-direction: column-reverse; - align-items: center; -} -main > div { - box-sizing: border-box; - width: 100%; - display: none; -} -main > .active { - display: block; -} - -.simple-methods { - display: flex !important; - flex-direction: column; -} - -.unit { - display: flex; - align-items: center; - flex-direction: column; -} -.unit > *:first-child { - width: 125px; -} -#additional-logins { - display: flex; - flex-direction: column; - flex-wrap: wrap; - justify-content: center; - margin-bottom: 10px; - font-size: 85%; -} - -/* Desktop styles */ -@media only screen and (min-width: 700px) { - main { - flex-direction: row; - } - main > * { - width: 50%; - } - main > .active.border { - border-right: 1px dashed #333030; - padding-right: 15px; - } - main > .active:last-of-type { - padding-left: 10px; - } - .unit, #additional-logins { - flex-direction: row; - } -} diff --git a/client/login.html b/client/login.html new file mode 100644 index 00000000..64718a58 --- /dev/null +++ b/client/login.html @@ -0,0 +1,42 @@ + + + + Login - {{siteTitle}} + + + + + + + + +
    +

    {{siteTitle}}

    + + {{#if isLogOut}} +

    Logout successful

    +
    Log in + {{else}} +

    Login Error

    +
    {{error}}
    + Try again + {{/if}} +
    + + diff --git a/server/routes/auth.ts b/server/routes/auth.ts index 6e450dc0..29e05dfd 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -55,6 +55,9 @@ authRoutes.get("/validatehost/:nonce", (request, response) => { authRoutes.all("/logout", (request, response) => { request.logout(); + if (request.session) { + request.session.loginAction = "render"; + } response.redirect("/login"); }); @@ -71,23 +74,15 @@ authRoutes.get("/login", validateAndCacheHostName, (request, response, next) => }); authRoutes.get("/login/callback", validateAndCacheHostName, (request, response, next) => { let callbackURL = createLink(request, "auth/login/callback"); - passport.authenticate("oauth2", { callbackURL } as AuthenticateOptions, (err: Error | null, user?: IUser) => { - if (err) { - console.error(err); - next(err); - return; - } - if (!user) { - response.redirect("/login"); - return; - } - request.login(user, err2 => { - if (err) { - console.error(err2); - next(err); - return; - } - response.redirect("/"); - }); - })(request, response, next); + + if (request.query.error === "access_denied") { + request.flash("error", "Authentication request was denied"); + response.redirect("/login"); + return; + } + passport.authenticate("oauth2", { + failureRedirect: "/login", + successReturnToOrRedirect: "/", + callbackURL + } as AuthenticateOptions)(request, response, next); }); diff --git a/server/routes/templates.ts b/server/routes/templates.ts index 8994ada4..a5746317 100644 --- a/server/routes/templates.ts +++ b/server/routes/templates.ts @@ -1,5 +1,6 @@ import * as fs from "fs"; import * as path from "path"; +import { URL } from "url"; import * as express from "express"; import * as Handlebars from "handlebars"; import * as moment from "moment-timezone"; @@ -20,35 +21,42 @@ import { ITeam, Team, IIndexTemplate, IAdminTemplate, ITeamTemplate, IRegisterBranchChoiceTemplate, IRegisterTemplate, StatisticEntry, - IFormItem + IFormItem, + ILoginTemplate } from "../schema"; import * as Branches from "../branch"; export let templateRoutes = express.Router(); -// Load and compile Handlebars templates -let [ - indexTemplate, - preregisterTemplate, - preconfirmTemplate, - registerTemplate, - confirmTemplate, - adminTemplate, - unsupportedTemplate, - teamTemplate -] = [ - "index.html", - "preapplication.html", - "preconfirmation.html", - "application.html", - "confirmation.html", - "admin.html", - "unsupported.html", - "team.html" -].map(file => { - let data = fs.readFileSync(path.resolve(STATIC_ROOT, file), "utf8"); - return Handlebars.compile(data); -}); +export class Template { + private template: Handlebars.TemplateDelegate | null = null; + + constructor(private file: string) { + this.loadTemplate(); + } + + private loadTemplate(): void { + let data = fs.readFileSync(path.resolve(STATIC_ROOT, this.file), "utf8"); + this.template = Handlebars.compile(data); + } + + public render(input: T): string { + if (!config.server.isProduction) { + this.loadTemplate(); + } + return this.template!(input); + } +} + +const IndexTemplate = new Template("index.html"); +const PreRegisterTemplate = new Template("preapplication.html"); +const PreConfirmTemplate = new Template("preconfirmation.html"); +const RegisterTemplate = new Template("application.html"); +const ConfirmTemplate = new Template("confirmation.html"); +const AdminTemplate = new Template("admin.html"); +const UnsupportedTemplate = new Template<{ siteTitle: string }>("unsupported.html"); +const TeamTemplate = new Template("team.html"); +const LoginTemplate = new Template("login.html"); // Block IE templateRoutes.use(async (request, response, next) => { @@ -67,7 +75,7 @@ templateRoutes.use(async (request, response, next) => { let templateData = { siteTitle: config.eventName }; - response.send(unsupportedTemplate(templateData)); + response.send(UnsupportedTemplate.render(templateData)); } else { next(); @@ -305,7 +313,7 @@ templateRoutes.route("/").get(authenticateWithRedirect, async (request, response templateData.timeline.teamFormation = "complete"; } - response.send(indexTemplate(templateData)); + response.send(IndexTemplate.render(templateData)); }); templateRoutes.route("/login").get(async (request, response) => { @@ -313,7 +321,28 @@ templateRoutes.route("/login").get(async (request, response) => { if (request.session && request.query.r && request.query.r.startsWith('/')) { request.session.returnTo = request.query.r; } - response.redirect("/auth/login"); + + let errorMessage = request.flash("error") as string[]; + if (request.session && request.session.loginAction === "render") { + request.session.loginAction = "redirect"; + let templateData = { + siteTitle: config.eventName, + isLogOut: true, + groundTruthLogOut: new URL("/logout", config.secrets.groundTruth.url) + }; + response.send(LoginTemplate.render(templateData)); + } + else if (errorMessage.length > 0) { + let templateData = { + siteTitle: config.eventName, + error: errorMessage.join(" "), + isLogOut: false + }; + response.send(LoginTemplate.render(templateData)); + } + else { + response.redirect("/auth/login"); + } }); templateRoutes.route("/team").get(authenticateWithRedirect, async (request, response) => { @@ -347,7 +376,7 @@ templateRoutes.route("/team").get(authenticateWithRedirect, async (request, resp qrEnabled: await getSetting("qrEnabled") } }; - response.send(teamTemplate(templateData)); + response.send(TeamTemplate.render(templateData)); }); templateRoutes.route("/apply").get( @@ -398,10 +427,10 @@ function applicationHandler(requestType: ApplicationType): (request: express.Req }; if (requestType === ApplicationType.Application) { - response.send(preregisterTemplate(templateData)); + response.send(PreRegisterTemplate.render(templateData)); } else { - response.send(preconfirmTemplate(templateData)); + response.send(PreConfirmTemplate.render(templateData)); } }; } @@ -535,9 +564,9 @@ function applicationBranchHandler(requestType: ApplicationType, anonymous: boole }; if (requestType === ApplicationType.Application) { - response.send(registerTemplate(templateData)); + response.send(RegisterTemplate.render(templateData)); } else if (requestType === ApplicationType.Confirmation) { - response.send(confirmTemplate(templateData)); + response.send(ConfirmTemplate.render(templateData)); } }; } @@ -754,5 +783,5 @@ templateRoutes.route("/admin").get(authenticateWithRedirect, async (request, res return question; }); - response.send(adminTemplate(templateData)); + response.send(AdminTemplate.render(templateData)); }); diff --git a/server/schema.ts b/server/schema.ts index db032a48..200322f2 100644 --- a/server/schema.ts +++ b/server/schema.ts @@ -342,6 +342,13 @@ export interface IAdminTemplate extends ICommonTemplate { }; } +export interface ILoginTemplate { + siteTitle: string; + isLogOut: boolean; + error?: string; + groundTruthLogOut?: string; +} + export interface DataLog { action: string; url: string; From 3300b3e660b95903341b48057084504405dce16c Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Wed, 20 Mar 2019 23:07:39 -0600 Subject: [PATCH 138/194] Log users out of Ground Truth when logging out --- package.json | 2 +- server/routes/api/user.ts | 3 ++- server/routes/auth.ts | 17 ++++++++++++++++- server/routes/strategies.ts | 12 +++++++++--- server/routes/templates.ts | 4 ++-- server/schema.ts | 2 ++ 6 files changed, 32 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 88c1e952..85c44635 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,6 @@ "supertest": "^3.1.0", "tslint": "^5.10.0", "tslint-language-service": "^0.9.9", - "typescript": "^2.9.1" + "typescript": "^3.3.0" } } diff --git a/server/routes/api/user.ts b/server/routes/api/user.ts index ea100657..ebf0aa41 100644 --- a/server/routes/api/user.ts +++ b/server/routes/api/user.ts @@ -85,7 +85,8 @@ function postApplicationBranchHandler(anonymous: boolean): (request: express.Req ...GroundTruthStrategy.defaultUserProperties, uuid: uuid(), name, - email + email, + token: null }); } else { let existingUser = await User.findOne({ uuid: request.params.uuid }); diff --git a/server/routes/auth.ts b/server/routes/auth.ts index 29e05dfd..cc08b118 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -1,3 +1,6 @@ +import * as http from "http"; +import * as https from "https"; +import { URL } from "url"; import * as crypto from "crypto"; import * as express from "express"; import * as session from "express-session"; @@ -54,7 +57,19 @@ authRoutes.get("/validatehost/:nonce", (request, response) => { }); authRoutes.all("/logout", (request, response) => { - request.logout(); + let user = request.user as IUser | undefined; + if (user) { + let groundTruthURL = new URL(config.secrets.groundTruth.url); + let requester = groundTruthURL.protocol === "http:" ? http : https; + requester.request(new URL("/api/user/logout", config.secrets.groundTruth.url), { + method: "POST", + headers: { + "Authorization": `Bearer ${user.token}` + } + }).end(); + + request.logout(); + } if (request.session) { request.session.loginAction = "render"; } diff --git a/server/routes/strategies.ts b/server/routes/strategies.ts index be9a5469..0cf8b1e1 100644 --- a/server/routes/strategies.ts +++ b/server/routes/strategies.ts @@ -25,6 +25,7 @@ interface IProfile { uuid: string; name: string; email: string; + token: string; } // Because the passport typedefs don't include this for some reason @@ -72,7 +73,10 @@ export class GroundTruthStrategy extends OAuthStrategy { return; } try { - let profile: IProfile = JSON.parse(data); + let profile: IProfile = { + ...JSON.parse(data), + token: accessToken + }; done(null, profile); } catch (err) { @@ -88,12 +92,14 @@ export class GroundTruthStrategy extends OAuthStrategy { ...GroundTruthStrategy.defaultUserProperties, ...profile }); - await user.save(); + } + else { + user.token = accessToken; } if (config.admins.includes(profile.email) && !user.admin) { user.admin = true; - await user.save(); } + await user.save(); done(null, user); } } diff --git a/server/routes/templates.ts b/server/routes/templates.ts index a5746317..05e476fd 100644 --- a/server/routes/templates.ts +++ b/server/routes/templates.ts @@ -31,7 +31,7 @@ export let templateRoutes = express.Router(); export class Template { private template: Handlebars.TemplateDelegate | null = null; - constructor(private file: string) { + constructor(private readonly file: string) { this.loadTemplate(); } @@ -328,7 +328,7 @@ templateRoutes.route("/login").get(async (request, response) => { let templateData = { siteTitle: config.eventName, isLogOut: true, - groundTruthLogOut: new URL("/logout", config.secrets.groundTruth.url) + groundTruthLogOut: new URL("/logout", config.secrets.groundTruth.url).toString() }; response.send(LoginTemplate.render(templateData)); } diff --git a/server/schema.ts b/server/schema.ts index 200322f2..953b2a2a 100644 --- a/server/schema.ts +++ b/server/schema.ts @@ -91,6 +91,7 @@ export interface IUser extends RootDocument { uuid: string; email: string; name: string; + token: string | null; teamId?: mongoose.Types.ObjectId; admin: boolean; @@ -136,6 +137,7 @@ export const User = mongoose.model>("User", new mongoose.Schema({ type: String, index: true }, + token: String, teamId: { type: mongoose.Schema.Types.ObjectId From add5a4c96a446730fdc6ad1a348e99a512e29937 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Thu, 21 Mar 2019 22:05:15 -0600 Subject: [PATCH 139/194] Support adding admins by email domain --- client/admin.html | 8 +++- client/tsconfig.json | 2 +- package-lock.json | 82 +++++++++++-------------------------- package.json | 4 +- server/common.ts | 20 +++++++-- server/routes/strategies.ts | 7 +++- server/routes/templates.ts | 4 +- server/schema.ts | 4 ++ server/storage.ts | 4 +- server/tsconfig.json | 2 +- 10 files changed, 65 insertions(+), 72 deletions(-) diff --git a/client/admin.html b/client/admin.html index 4bf81132..4c8a7ca7 100644 --- a/client/admin.html +++ b/client/admin.html @@ -359,14 +359,18 @@

    config.json options

    -
    +
    -
    +
    +
    + + +
    diff --git a/client/tsconfig.json b/client/tsconfig.json index 0cc10965..4cf04ce6 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -18,7 +18,7 @@ "noUnusedLocals": true, "plugins": [ { - "name": "tslint-language-service", + "name": "tslint-language-service-ts3", "alwaysShowRuleFailuresAsWarnings": false, "ignoreDefinitionFiles": true, "configFile": "../tslint.json" diff --git a/package-lock.json b/package-lock.json index 72471f16..373158f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1209,9 +1209,9 @@ } }, "diff": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz", - "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", "dev": true }, "dom-serializer": { @@ -3130,15 +3130,15 @@ } }, "tslib": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.2.tgz", - "integrity": "sha512-AVP5Xol3WivEr7hnssHDsaM+lVrVXWUvd1cfXTRkTj80b//6g2wIFEH6hZG0muGZRnHGrfttpdzRk3YlBkWjKw==", + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", "dev": true }, "tslint": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.10.0.tgz", - "integrity": "sha1-EeJrzLiK+gLdDZlWyuPUVAtfVMM=", + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.14.0.tgz", + "integrity": "sha512-IUla/ieHVnB8Le7LdQFRGlVJid2T/gaJe5VkjzRVSRR6pA2ODYrnfR1hmxi+5+au9l50jBwpbBL34txgv4NnTQ==", "dev": true, "requires": { "babel-code-frame": "^6.22.0", @@ -3149,68 +3149,34 @@ "glob": "^7.1.1", "js-yaml": "^3.7.0", "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", "resolve": "^1.3.2", "semver": "^5.3.0", "tslib": "^1.8.0", - "tsutils": "^2.12.1" + "tsutils": "^2.29.0" }, "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, "commander": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", + "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", "dev": true - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } } } }, - "tslint-language-service": { - "version": "0.9.9", - "resolved": "https://registry.npmjs.org/tslint-language-service/-/tslint-language-service-0.9.9.tgz", - "integrity": "sha1-9UbcOEg5eeb7PPpZWErYUls61No=", + "tslint-language-service-ts3": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tslint-language-service-ts3/-/tslint-language-service-ts3-1.0.0.tgz", + "integrity": "sha512-SE1QymT9i0bpKmDEiba+abgp8SUuxayM1sWZsrR9ffouiV2CtkB4GdGC/eFxp4rCPXuUXsdsVDxQBHNXSuah7A==", "dev": true, "requires": { "mock-require": "^2.0.2" } }, "tsutils": { - "version": "2.27.1", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.27.1.tgz", - "integrity": "sha512-AE/7uzp32MmaHvNNFES85hhUDHFdFZp6OAiZcd6y4ZKKIg6orJTm8keYWBhIhrJQH3a4LzNKat7ZPXZt5aTf6w==", + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", "dev": true, "requires": { "tslib": "^1.8.1" @@ -3250,9 +3216,9 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, "typescript": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.1.tgz", - "integrity": "sha512-h6pM2f/GDchCFlldnriOhs1QHuwbnmj6/v7499eMHqPeW4V2G0elua2eIc2nu8v2NdHV0Gm+tzX83Hr6nUFjQA==", + "version": "3.3.4000", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.3.4000.tgz", + "integrity": "sha512-jjOcCZvpkl2+z7JFn0yBOoLQyLoIkNZAs/fYJkUG6VKy6zLPHJGfQJYFHzibB6GJaF/8QrcECtlQ5cpvRHSMEA==", "dev": true }, "typo-js": { diff --git a/package.json b/package.json index 85c44635..52b52e14 100644 --- a/package.json +++ b/package.json @@ -100,8 +100,8 @@ "chai": "^4.0.0", "mocha": "^5.2.0", "supertest": "^3.1.0", - "tslint": "^5.10.0", - "tslint-language-service": "^0.9.9", + "tslint": "^5.14.0", + "tslint-language-service-ts3": "^1.0.0", "typescript": "^3.3.0" } } diff --git a/server/common.ts b/server/common.ts index c6711c5d..329bc43f 100644 --- a/server/common.ts +++ b/server/common.ts @@ -38,7 +38,10 @@ class Config implements IConfig.Main { passwordResetExpiration: 1000 * 60 * 60, // 1 hour defaultTimezone: "America/New_York" }; - public admins: string[] = []; + public admins = { + domains: ["hack.gt"], + emails: [] as string[] + }; public eventName: string = "Untitled Event"; public storageEngine = { "name": "disk", @@ -66,7 +69,7 @@ class Config implements IConfig.Main { } protected loadFromJSON(fileName: string): void { // tslint:disable-next-line:no-shadowed-variable - let config: IConfig.Main | null = null; + let config: Partial | null = null; try { config = JSON.parse(fs.readFileSync(path.resolve(__dirname, "./config", fileName), "utf8")); } @@ -93,6 +96,14 @@ class Config implements IConfig.Main { this.server[key] = config.server[key]; } } + if (config.admins) { + if (config.admins.domains) { + this.admins.domains = config.admins.domains; + } + if (config.admins.emails) { + this.admins.emails = config.admins.emails; + } + } if (config.eventName) { this.eventName = config.eventName; } @@ -189,7 +200,10 @@ class Config implements IConfig.Main { } // Admins if (process.env.ADMIN_EMAILS) { - this.admins = JSON.parse(process.env.ADMIN_EMAILS!); + this.admins.emails = JSON.parse(process.env.ADMIN_EMAILS!); + } + if (process.env.ADMIN_DOMAINS) { + this.admins.domains = JSON.parse(process.env.ADMIN_DOMAINS); } // Event name if (process.env.EVENT_NAME) { diff --git a/server/routes/strategies.ts b/server/routes/strategies.ts index 0cf8b1e1..2dcf9f1a 100644 --- a/server/routes/strategies.ts +++ b/server/routes/strategies.ts @@ -96,7 +96,12 @@ export class GroundTruthStrategy extends OAuthStrategy { else { user.token = accessToken; } - if (config.admins.includes(profile.email) && !user.admin) { + + let domain = user.email.split("@").pop(); + if (domain && config.admins.domains.includes(domain)) { + user.admin = true; + } + if (config.admins.emails.includes(profile.email)) { user.admin = true; } await user.save(); diff --git a/server/routes/templates.ts b/server/routes/templates.ts index 05e476fd..dd12c657 100644 --- a/server/routes/templates.ts +++ b/server/routes/templates.ts @@ -593,6 +593,8 @@ templateRoutes.route("/admin").get(authenticateWithRedirect, async (request, res teamIDNameMap[team._id.toString()] = team.teamName; }); + let preconfiguredAdmins = config.admins.emails.concat(config.admins.domains.map(domain => `*@${domain}`)); + let templateData: IAdminTemplate = { siteTitle: config.eventName, user, @@ -650,7 +652,7 @@ templateRoutes.route("/admin").get(authenticateWithRedirect, async (request, res apiKey: config.secrets.adminKey }, config: { - admins: config.admins.join(", "), + admins: preconfiguredAdmins.join(", "), eventName: config.eventName, storageEngine: config.storageEngine.name, uploadDirectoryRaw: config.storageEngine.options.uploadDirectory, diff --git a/server/schema.ts b/server/schema.ts index 953b2a2a..d85b0883 100644 --- a/server/schema.ts +++ b/server/schema.ts @@ -44,6 +44,10 @@ export namespace IConfig { email: Email; server: Server; style: Style; + admins: { + domains: string[]; + emails: string[]; + }; eventName: string; questionsLocation: string; storageEngine: { diff --git a/server/storage.ts b/server/storage.ts index 558eb438..e0ce9b3e 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -121,9 +121,7 @@ class S3StorageEngine implements IStorageEngine { } interface IStorageEngines { - [name: string]: { - new(options: ICommonOptions): IStorageEngine; - }; + [name: string]: new(options: ICommonOptions) => IStorageEngine; } export const storageEngines: IStorageEngines = { "disk": DiskStorageEngine, diff --git a/server/tsconfig.json b/server/tsconfig.json index c8ed7565..6b4faded 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -19,7 +19,7 @@ "noUnusedLocals": true, "plugins": [ { - "name": "tslint-language-service", + "name": "tslint-language-service-ts3", "alwaysShowRuleFailuresAsWarnings": false, "ignoreDefinitionFiles": true, "configFile": "../tslint.json" From 3838ff2763a0b17488960483be45fa01e6299ff5 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Thu, 28 Mar 2019 00:34:58 -0400 Subject: [PATCH 140/194] Remove unused password reset expiration setting --- server/common.ts | 7 ------- server/config/config.example.json | 1 - server/schema.ts | 1 - 3 files changed, 9 deletions(-) diff --git a/server/common.ts b/server/common.ts index 329bc43f..0727131d 100644 --- a/server/common.ts +++ b/server/common.ts @@ -35,7 +35,6 @@ class Config implements IConfig.Main { cookieMaxAge: 1000 * 60 * 60 * 24 * 30 * 6, // 6 months cookieSecureOnly: false, mongoURL: "mongodb://localhost/registration", - passwordResetExpiration: 1000 * 60 * 60, // 1 hour defaultTimezone: "America/New_York" }; public admins = { @@ -192,12 +191,6 @@ class Config implements IConfig.Main { if (process.env.DEFAULT_TIMEZONE) { this.server.defaultTimezone = process.env.DEFAULT_TIMEZONE; } - if (process.env.PASSWORD_RESET_EXPIRATION) { - let expirationTime = parseInt(process.env.PASSWORD_RESET_EXPIRATION, 10); - if (!isNaN(expirationTime) && expirationTime > 0) { - this.server.passwordResetExpiration = expirationTime; - } - } // Admins if (process.env.ADMIN_EMAILS) { this.admins.emails = JSON.parse(process.env.ADMIN_EMAILS!); diff --git a/server/config/config.example.json b/server/config/config.example.json index 118c8283..c76a30c5 100644 --- a/server/config/config.example.json +++ b/server/config/config.example.json @@ -18,7 +18,6 @@ "port": 3000, "cookieMaxAge": 15552000000, "cookieSecureOnly": true, - "passwordResetExpiration": 3600000, "mongoURL": "mongodb://localhost/registration" }, "eventName": "My Hackathon", diff --git a/server/schema.ts b/server/schema.ts index d85b0883..09b3b8d9 100644 --- a/server/schema.ts +++ b/server/schema.ts @@ -31,7 +31,6 @@ export namespace IConfig { cookieMaxAge: number; cookieSecureOnly: boolean; mongoURL: string; - passwordResetExpiration: number; defaultTimezone: string; } export interface Style { From edcb965bbde7f2745812fc5bdad3e867b74604e4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Fri, 7 Jun 2019 01:58:07 +0000 Subject: [PATCH 141/194] Bump handlebars from 4.1.0 to 4.1.2 Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.1.0 to 4.1.2. - [Release notes](https://github.com/wycats/handlebars.js/releases) - [Changelog](https://github.com/wycats/handlebars.js/blob/master/release-notes.md) - [Commits](https://github.com/wycats/handlebars.js/compare/v4.1.0...v4.1.2) Signed-off-by: dependabot[bot] --- package-lock.json | 37 +++++++++++++++++++++---------------- package.json | 2 +- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index b0b2ab6d..b61c8355 100644 --- a/package-lock.json +++ b/package-lock.json @@ -952,12 +952,12 @@ }, "lodash": { "version": "3.10.1", - "resolved": "http://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" }, "readable-stream": { "version": "1.1.14", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", "requires": { "core-util-is": "~1.0.0", @@ -1849,9 +1849,9 @@ }, "dependencies": { "es6-promise": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.5.tgz", - "integrity": "sha512-n6wvpdE43VFtJq+lUDYDBFUwV8TZbuGXLV4D6wKafg13ldznKsyEvatubnmUe31zcvelSzOHF+XbaT+Bl9ObDg==" + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" } } }, @@ -1884,11 +1884,11 @@ "dev": true }, "handlebars": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.0.tgz", - "integrity": "sha512-l2jRuU1NAWK6AW5qqcTATWQJvNPEwkM7NEKSiv/gqOsoSQbVoWyqVEY5GS+XPQ88zLNmqASRpzfdm8d79hJS+w==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", + "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", "requires": { - "async": "^2.5.0", + "neo-async": "^2.6.0", "optimist": "^0.6.1", "source-map": "^0.6.1", "uglify-js": "^3.1.4" @@ -2575,6 +2575,11 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" }, + "neo-async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==" + }, "next-tick": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", @@ -3425,19 +3430,19 @@ "integrity": "sha1-VNjrx5SfGngQkItgAsaEFSbJnVo=" }, "uglify-js": { - "version": "3.4.9", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz", - "integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", + "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==", "optional": true, "requires": { - "commander": "~2.17.1", + "commander": "~2.20.0", "source-map": "~0.6.1" }, "dependencies": { "commander": { - "version": "2.17.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", - "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", "optional": true } } diff --git a/package.json b/package.json index 8969c271..3616631c 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "graphql-server-express": "^1.3.6", "graphql-tools": "^1.2.2", "graphql-typewriter": "git://github.com/illegalprime/graphql-typewriter.git#hacks", - "handlebars": "^4.1.0", + "handlebars": "^4.1.2", "json-schema-to-typescript": "^4.6.5", "json2csv": "^3.11.5", "marked": "^0.3.19", From 519a89652715734cf0a44cde02789250a5ad9d3f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Thu, 13 Jun 2019 19:40:12 +0000 Subject: [PATCH 142/194] Bump diff from 3.2.0 to 3.5.0 Bumps [diff](https://github.com/kpdecker/jsdiff) from 3.2.0 to 3.5.0. - [Release notes](https://github.com/kpdecker/jsdiff/releases) - [Changelog](https://github.com/kpdecker/jsdiff/blob/master/release-notes.md) - [Commits](https://github.com/kpdecker/jsdiff/compare/v3.2.0...v3.5.0) Signed-off-by: dependabot[bot] --- package-lock.json | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index b61c8355..86088f5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1334,9 +1334,9 @@ } }, "diff": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz", - "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", "dev": true }, "dom-serializer": { @@ -2347,12 +2347,6 @@ "requires": { "ms": "2.0.0" } - }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", - "dev": true } } }, From e7ed93d0bdc91044c2629c14c223474153802149 Mon Sep 17 00:00:00 2001 From: Ehsan Asdar Date: Sun, 16 Jun 2019 11:33:49 -0700 Subject: [PATCH 143/194] Update secrets required --- deployment.yaml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/deployment.yaml b/deployment.yaml index a572a9e1..24837267 100644 --- a/deployment.yaml +++ b/deployment.yaml @@ -11,12 +11,9 @@ secrets: - ADMIN_KEY_SECRET - SESSION_SECRET - EMAIL_KEY - - GOOGLE_CLIENT_ID - - GOOGLE_CLIENT_SECRET - - GITHUB_CLIENT_ID - - GITHUB_CLIENT_SECRET - - FACEBOOK_CLIENT_ID - - FACEBOOK_CLIENT_SECRET + - GROUND_TRUTH_URL + - GROUND_TRUTH_ID + - GROUND_TRUTH_SECRET - STORAGE_ENGINE_OPTIONS env: From 4d7af31590a557507e44269e385785c12128f6b3 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Sun, 23 Jun 2019 15:46:38 -0700 Subject: [PATCH 144/194] Bump now out-of-date Node version --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index a85a7d2e..1b1e53b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:11-alpine +FROM node:12-alpine MAINTAINER Ryan Petschek # Deis wants bash From 1c971a8953d1f61be247789d35816d2787a4b3d5 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Sun, 23 Jun 2019 15:50:52 -0700 Subject: [PATCH 145/194] Fix inconsistencies and typos in README --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ae624653..96dbfb02 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ PRODUCTION | Set to `true` to enable reverse proxy trusting (default: `false`) PORT | The port the check in system should run on (default: `3000`) MONGO_URL | The URL to the MongoDB server (default: `mongodb://localhost/registration`) VERSION_HASH | The Git short hash used to identify the current commit (default: parsed automatically from the `.git` folder, if it exists) -ADMIN_KEY_SECRET | A API key used to authenticate as admin an access the GraphQL api (default: random key that changes every server restart) +ADMIN_KEY_SECRET | An API key used to authenticate as admin an access the GraphQL api (default: random key that changes every server restart) COOKIE_MAX_AGE | The `maxAge` of cookies set in milliseconds (default: 6 months) **NOTE: this is different from the session TTL** COOKIE_SECURE_ONLY | Whether session cookies should sent exclusively over secure connections (default: `false`) PASSWORD_RESET_EXPIRATION | The time that password reset links sent via email should be valid for in milliseconds (default: 1 hour) @@ -75,14 +75,14 @@ GROUND_TRUTH_URL | Base URL of [Ground Truth](https://github.com/HackGT/ground-t GROUND_TRUTH_ID | OAuth client ID from Ground Truth *required* GROUND_TRUTH_SECRET | OAuth client secret from Ground Truth *required* EMAIL_FROM | The `From` header for sent emails (default: `HackGT Team `) -EMAIL_KEY | The SendGrid API key for sending emails (default: *none*) +EMAIL_KEY | The SendGrid API key for sending emails (default: *none*) *required* ADMIN_EMAILS | A JSON array of the emails of the users that you want promoted to admin status when they create their account (default: none) EVENT_NAME | The current event's name which affects rendered templates and sent emails (default: `Untitled Event`) STORAGE_ENGINE | The name of the storage engine that handles file uploads as defined in [storage.ts](server/storage.ts) (default: `disk`) STORAGE_ENGINE_OPTIONS | JSON-encoded object containing options to be passed to the storage engine. Must at least contain a value for the `uploadDirectory` key. For the default `disk` storage engine, this directory is relative to the app's root, can be absolute, and will be created if it doesn't exist. (default: `{ "uploadDirectory": "uploads" }`) DEFAULT_TIMEZONE | Timezone used for dates and times (default: `America/New_York`) MAX_TEAM_SIZE | The maximum number of users allowed per team (default: `4`) -QUESTIONS_FILE | Specify a path for the `questions.json` file. +QUESTIONS_FILE | Specify a path for the `questions.json` file. (default: ./server/config/questions.json) THEME_FILE | Specify a path for the `theme.css` file, which will be loaded last at every page. FAVICON_FILE | Path to the favicon file (default is no favicon). FAVICON_FILE_BASE64 | Same as `FAVICON_FILE_BASE64` but the file is base64 encoded. @@ -92,7 +92,7 @@ FAVICON_FILE_BASE64 | Same as `FAVICON_FILE_BASE64` but the file is base64 encod If you happen to find a bug or have a feature you'd like to see implemented, please [file an issue](https://github.com/HackGT/registration/issues). -If you have some time and want to help us out with development, thank you! You can get started by taking a look at the open issues, particularly the ones marked [help wanted](https://github.com/HackGT/registration/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) or [help wanted - beginner](https://github.com/HackGT/registration/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted+-+beginner%22). Feel free to ask questions to clarify things, determine the best way to implement a new feature or bug fix, or anything else! +If you have some time and want to help us out with development, thank you! You can get started by taking a look at the open issues, particularly the ones marked [help wanted](https://github.com/HackGT/registration/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) or [good first issue](https://github.com/HackGT/registration/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). Feel free to ask questions to clarify things, determine the best way to implement a new feature or bug fix, or anything else! ### Tips From 7badd486bf9612698ab20027282105cf105ae408 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Sun, 23 Jun 2019 15:53:17 -0700 Subject: [PATCH 146/194] Move login styles to separate file --- client/css/login.css | 15 +++++++++++++++ client/login.html | 18 +----------------- 2 files changed, 16 insertions(+), 17 deletions(-) create mode 100644 client/css/login.css diff --git a/client/css/login.css b/client/css/login.css new file mode 100644 index 00000000..8058b0ee --- /dev/null +++ b/client/css/login.css @@ -0,0 +1,15 @@ +#error { + text-align: center; + margin-top: 1em; + font-size: 2rem; + margin-bottom: 1em; + color: white; + padding: 10px; + border-radius: 5px; + background-color: rgba(255, 65, 54, 0.85); +} +.btn { + display: block; + width: 100px; + margin: 0 auto; +} diff --git a/client/login.html b/client/login.html index 64718a58..9c0719ce 100644 --- a/client/login.html +++ b/client/login.html @@ -6,23 +6,7 @@ - + From 58b215c653589cafef718d90196a7a701b2ead51 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Sun, 23 Jun 2019 15:58:21 -0700 Subject: [PATCH 147/194] Don't make @hack.gt emails hardcoded admin promotion default --- server/common.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/common.ts b/server/common.ts index 0727131d..0baad9d6 100644 --- a/server/common.ts +++ b/server/common.ts @@ -38,7 +38,7 @@ class Config implements IConfig.Main { defaultTimezone: "America/New_York" }; public admins = { - domains: ["hack.gt"], + domains: [] as string[], emails: [] as string[] }; public eventName: string = "Untitled Event"; From f7fdd981100a5a18e7c9bbbfcc26f5b57d4fa52f Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Sun, 23 Jun 2019 16:03:04 -0700 Subject: [PATCH 148/194] Add example usage of admins promotion configuration --- server/config/config.example.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/config/config.example.json b/server/config/config.example.json index c76a30c5..8b24fc2a 100644 --- a/server/config/config.example.json +++ b/server/config/config.example.json @@ -7,6 +7,10 @@ "secret": "" } }, + "admins": { + "domains": ["gatech.edu", "hack.gt"], + "emails": ["george.p@burdell.com", "buzz@gatech.edu"] + }, "email": { "from": "HackGT Team ", "host": "smtp.sendgrid.net", From 71ad39ae38b2bc17e28a67e00aabe3b0dfbc6c4d Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Sun, 23 Jun 2019 19:12:11 -0400 Subject: [PATCH 149/194] Minor style fix --- server/routes/api/graphql.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/server/routes/api/graphql.ts b/server/routes/api/graphql.ts index 4633289c..0fa41472 100644 --- a/server/routes/api/graphql.ts +++ b/server/routes/api/graphql.ts @@ -160,15 +160,17 @@ async function findQuestions( let items: types.FormItem[] = []; if (user.applied) { - items = items.concat(await Promise.all(user.applicationData! - .reduce(questionFilter, []) - .map(item => recordToFormItem(item, user.applicationBranch!)) + items = items.concat(await Promise.all( + user.applicationData! + .reduce(questionFilter, []) + .map(item => recordToFormItem(item, user.applicationBranch!)) )); } if (user.confirmed) { - items = items.concat(await Promise.all(user.confirmationData! - .reduce(questionFilter, []) - .map(item => recordToFormItem(item, user.confirmationBranch!)) + items = items.concat(await Promise.all( + user.confirmationData! + .reduce(questionFilter, []) + .map(item => recordToFormItem(item, user.confirmationBranch!)) )); } return items; From 9ffbbcc78db408d0b22850dfadc6389ef67c962d Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Sun, 7 Jul 2019 22:32:35 -0400 Subject: [PATCH 150/194] Implement client side max / min character counts and labels --- client/partials/form.html | 20 +++++++++++++++++--- server/config/questions.schema.json | 14 +++++++++++++- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/client/partials/form.html b/client/partials/form.html index 567dfacb..f95cbfae 100644 --- a/client/partials/form.html +++ b/client/partials/form.html @@ -33,9 +33,23 @@
    {{/ifCond}} {{else}} - + {{#ifCond this.type "textarea"}} - + {{else}} {{#ifCond this.type "file"}} {{#if this.value}} @@ -45,7 +59,7 @@ {{/if}} {{else}} - + {{/ifCond}} {{/ifCond}} {{/if}} diff --git a/server/config/questions.schema.json b/server/config/questions.schema.json index c5f746fb..da22fe56 100644 --- a/server/config/questions.schema.json +++ b/server/config/questions.schema.json @@ -77,6 +77,18 @@ }, "required": { "type": "boolean" + }, + "maxWordCount": { + "type": "number" + }, + "minWordCount": { + "type": "number" + }, + "maxCharacterCount": { + "type": "number" + }, + "minCharacterCount": { + "type": "number" } }, "required": [ @@ -127,4 +139,4 @@ } } } -} \ No newline at end of file +} From 517b4d0303a4c5b64d3a939b8249782f3978c870 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Sun, 7 Jul 2019 23:28:20 -0400 Subject: [PATCH 151/194] Implement server-side checking for min / max character counts --- server/routes/api/user.ts | 114 +++++++++++++++++++++++--------------- 1 file changed, 70 insertions(+), 44 deletions(-) diff --git a/server/routes/api/user.ts b/server/routes/api/user.ts index ebf0aa41..ca422108 100644 --- a/server/routes/api/user.ts +++ b/server/routes/api/user.ts @@ -38,37 +38,45 @@ let postApplicationBranchErrorHandler: express.ErrorRequestHandler = (err, reque }; // We don't use canUserModify here, instead check for admin -registrationRoutes.route("/:branch").post( - isAdmin, - postParser, - uploadHandler.any(), - postApplicationBranchErrorHandler, - postApplicationBranchHandler(true) -); - -userRoutes.route("/application/:branch").post( - isUserOrAdmin, - canUserModify, - postParser, - uploadHandler.any(), - postApplicationBranchErrorHandler, - postApplicationBranchHandler(false) -).delete( - isUserOrAdmin, - canUserModify, - deleteApplicationBranchHandler); -userRoutes.route("/confirmation/:branch").post( - isUserOrAdmin, - canUserModify, - postParser, - uploadHandler.any(), - postApplicationBranchErrorHandler, - postApplicationBranchHandler(false) -).delete( - isUserOrAdmin, - canUserModify, - deleteApplicationBranchHandler -); +registrationRoutes.route("/:branch") + .post( + isAdmin, + postParser, + uploadHandler.any(), + postApplicationBranchErrorHandler, + postApplicationBranchHandler(true) + ); + +userRoutes.route("/application/:branch") + .post( + isUserOrAdmin, + canUserModify, + postParser, + uploadHandler.any(), + postApplicationBranchErrorHandler, + postApplicationBranchHandler(false) + ) + .delete( + isUserOrAdmin, + canUserModify, + deleteApplicationBranchHandler + ); + +userRoutes.route("/confirmation/:branch") + .post( + isUserOrAdmin, + canUserModify, + postParser, + uploadHandler.any(), + postApplicationBranchErrorHandler, + postApplicationBranchHandler(false) + ) + .delete( + isUserOrAdmin, + canUserModify, + deleteApplicationBranchHandler + ); + function postApplicationBranchHandler(anonymous: boolean): (request: express.Request, response: express.Response) => Promise { return async (request, response) => { let user: Model; @@ -111,6 +119,21 @@ function postApplicationBranchHandler(anonymous: boolean): (request: express.Req let unchangedFiles: string[] = []; let errored: boolean = false; // Used because .map() can't be broken out of let rawData: (IFormItem | null)[] = questionBranch.questions.map(question => { + function reportError(message: string): null { + errored = true; + response.status(400).json({ + "error": message + }); + return null; + } + function getQuestion(defaultValue?: T): T { + let value = request.body[question.name] as T; + if (defaultValue) { + return value || defaultValue; + } + return value; + } + if (errored) { return null; } @@ -120,7 +143,7 @@ function postApplicationBranchHandler(anonymous: boolean): (request: express.Req && user.applicationData != undefined && user.applicationData.some(entry => entry.name === question.name && !!entry.value); - if (question.required && !request.body[question.name] && !files.find(file => file.fieldname === question.name)) { + if (question.required && !getQuestion() && !files.find(file => file.fieldname === question.name)) { // Required field not filled in if (preexistingFile) { let previousValue = user.applicationData!.find(entry => entry.name === question.name && !!entry.value)!.value as Express.Multer.File; @@ -132,31 +155,34 @@ function postApplicationBranchHandler(anonymous: boolean): (request: express.Req }; } else { - errored = true; - response.status(400).json({ - "error": `"${question.label}" is a required field` - }); - return null; + return reportError(`"${question.label}" is a required field`); } } - if ((question.type === "select" || question.type === "radio") && Array.isArray(request.body[question.name]) && question.hasOther) { + if (question.minCharacterCount && getQuestion("").length < question.minCharacterCount) { + return reportError(`Your response to "${question.label}" must have at least ${question.minCharacterCount} characters`); + } + if (question.maxCharacterCount && getQuestion("").length > question.maxCharacterCount) { + return reportError(`Your response to "${question.label}" cannot exceed ${question.maxCharacterCount} characters`); + } + + if ((question.type === "select" || question.type === "radio") && Array.isArray(getQuestion(question.name)) && question.hasOther) { // "Other" option selected - request.body[question.name] = request.body[question.name].pop(); + request.body[question.name] = getQuestion().pop(); } else if (question.type === "checkbox" && question.hasOther) { - if (!request.body[question.name]) { + if (!getQuestion(question.name)) { request.body[question.name] = []; } - if (!Array.isArray(request.body[question.name])) { - request.body[question.name] = [request.body[question.name]]; + if (!Array.isArray(getQuestion(question.name))) { + request.body[question.name] = [getQuestion()]; } // Filter out "other" option - request.body[question.name] = (request.body[question.name] as string[]).filter(value => value !== "Other"); + request.body[question.name] = getQuestion().filter(value => value !== "Other"); } return { "name": question.name, "type": question.type, - "value": request.body[question.name] || files.find(file => file.fieldname === question.name) + "value": getQuestion(files.find(file => file.fieldname === question.name)) }; }); if (errored) { From 480c68f46897a008f6f8d112bbc97f921cbedc16 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Sun, 7 Jul 2019 23:28:58 -0400 Subject: [PATCH 152/194] Fix compile error caused by TS thinking different types are being assigned during config load --- server/common.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/common.ts b/server/common.ts index 0baad9d6..0b3aab89 100644 --- a/server/common.ts +++ b/server/common.ts @@ -82,7 +82,7 @@ class Config implements IConfig.Main { } if (config.secrets) { for (let key of Object.keys(config.secrets) as (keyof IConfig.Secrets)[]) { - this.secrets[key] = config.secrets[key]; + (this.secrets as any)[key] = config.secrets[key]; } } if (config.email) { @@ -92,7 +92,7 @@ class Config implements IConfig.Main { } if (config.server) { for (let key of Object.keys(config.server) as (keyof IConfig.Server)[]) { - this.server[key] = config.server[key]; + (this.server as any)[key] = config.server[key]; } } if (config.admins) { From dcd42d67cd5f56731ed6d2be27ebb65c285446a3 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Mon, 8 Jul 2019 00:00:45 -0400 Subject: [PATCH 153/194] Implement server side checking for word counts --- server/routes/api/user.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/routes/api/user.ts b/server/routes/api/user.ts index ca422108..8f4a027c 100644 --- a/server/routes/api/user.ts +++ b/server/routes/api/user.ts @@ -164,6 +164,14 @@ function postApplicationBranchHandler(anonymous: boolean): (request: express.Req if (question.maxCharacterCount && getQuestion("").length > question.maxCharacterCount) { return reportError(`Your response to "${question.label}" cannot exceed ${question.maxCharacterCount} characters`); } + let wordCount = getQuestion("").split(/\s+/).length; + let wordCountPlural = wordCount === 1 ? "" : "s"; + if (question.minWordCount && wordCount < question.minWordCount) { + return reportError(`Your response to "${question.label}" must contain at least ${question.minWordCount} words (currently has ${wordCount} word${wordCountPlural})`); + } + if (question.maxWordCount && wordCount > question.maxWordCount) { + return reportError(`Your response to "${question.label}" cannot exceed ${question.maxWordCount} words (currently has ${wordCount} word${wordCountPlural})`); + } if ((question.type === "select" || question.type === "radio") && Array.isArray(getQuestion(question.name)) && question.hasOther) { // "Other" option selected From 3731abc9899b46302eaa5d52f2eadafc9e58faba Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Mon, 8 Jul 2019 00:29:42 -0400 Subject: [PATCH 154/194] Add live word count to application --- client/js/application.ts | 18 ++++++++++++++++++ client/partials/form.html | 13 +++++++------ server/routes/api/user.ts | 5 ++++- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/client/js/application.ts b/client/js/application.ts index 23fe5ae5..673fe2ec 100644 --- a/client/js/application.ts +++ b/client/js/application.ts @@ -126,3 +126,21 @@ for (let i = 0; i < inputsWithOther.length; i++) { } }); } + +let wordCountInputs = document.querySelectorAll("[data-max-word-count], [data-min-word-count]") as NodeListOf; +for (let i = 0; i < wordCountInputs.length; i++) { + wordCountInputs[i].addEventListener("input", e => { + let target = e.target as HTMLInputElement | HTMLTextAreaElement; + let correspondingLabel = document.querySelector(`label[for="${target.id}"] > .current-count`); + if (!correspondingLabel) return; + + let { maxWordCount, minWordCount } = target.dataset; + if (!maxWordCount && !minWordCount) return; + + let wordCount = target.value.trim().split(/\s+/).length; + if (target.value.trim().length === 0) { + wordCount = 0; + } + correspondingLabel.textContent = `(${wordCount.toLocaleString()} word${wordCount === 1 ? "" : "s"})`; + }); +} diff --git a/client/partials/form.html b/client/partials/form.html index f95cbfae..207e600f 100644 --- a/client/partials/form.html +++ b/client/partials/form.html @@ -36,20 +36,21 @@ {{#ifCond this.type "textarea"}} - + {{else}} {{#ifCond this.type "file"}} {{#if this.value}} @@ -59,7 +60,7 @@ {{/if}} {{else}} - + {{/ifCond}} {{/ifCond}} {{/if}} diff --git a/server/routes/api/user.ts b/server/routes/api/user.ts index 8f4a027c..13e3074e 100644 --- a/server/routes/api/user.ts +++ b/server/routes/api/user.ts @@ -164,7 +164,10 @@ function postApplicationBranchHandler(anonymous: boolean): (request: express.Req if (question.maxCharacterCount && getQuestion("").length > question.maxCharacterCount) { return reportError(`Your response to "${question.label}" cannot exceed ${question.maxCharacterCount} characters`); } - let wordCount = getQuestion("").split(/\s+/).length; + let wordCount = getQuestion("").trim().split(/\s+/).length; + if (getQuestion("").trim().length === 0) { + wordCount = 0; + } let wordCountPlural = wordCount === 1 ? "" : "s"; if (question.minWordCount && wordCount < question.minWordCount) { return reportError(`Your response to "${question.label}" must contain at least ${question.minWordCount} words (currently has ${wordCount} word${wordCountPlural})`); From 598dea6a412b3d70355d7c251c7a6fae55874bbb Mon Sep 17 00:00:00 2001 From: Ehsan Asdar Date: Sun, 7 Jul 2019 21:51:53 -0700 Subject: [PATCH 155/194] Add email templating to provide rich HTML emails (#271) * Add email templating to provide rich HTML emails * Fix quotes --- Dockerfile | 1 + package-lock.json | 1248 +++++++++++++++++++++++- package.json | 3 + server/common.ts | 74 +- server/emails/email-template/html.pug | 177 ++++ server/emails/email-template/style.css | 531 ++++++++++ server/emails/email-template/text.pug | 1 + server/routes/api/settings.ts | 6 +- server/routes/api/user.ts | 4 +- server/schema.ts | 4 + 10 files changed, 2015 insertions(+), 34 deletions(-) create mode 100644 server/emails/email-template/html.pug create mode 100644 server/emails/email-template/style.css create mode 100644 server/emails/email-template/text.pug diff --git a/Dockerfile b/Dockerfile index 1b1e53b5..0b62e6b4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,6 +14,7 @@ RUN npm run build # Set Timezone to EST RUN apk add tzdata ENV TZ="/usr/share/zoneinfo/America/New_York" +ENV NODE_ENV="production" # Deis wants EXPOSE and CMD EXPOSE 3000 diff --git a/package-lock.json b/package-lock.json index 9791d63a..989965b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,43 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@babel/runtime": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.5.1.tgz", + "integrity": "sha512-g+hmPKs16iewFSmW57NkH9xpPkuYD1RV3UE2BCkXx9j+nhhRb9hsiSxPmEa67j35IecTQdn4iyMtHMbt5VoREg==", + "requires": { + "regenerator-runtime": "^0.13.2" + } + }, + "@ladjs/i18n": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@ladjs/i18n/-/i18n-1.1.0.tgz", + "integrity": "sha512-Kynr5osjApDCyiik35MMNZC1lgjgrk7fbV6P1qHXKQ67sR/U85Ddnv1NNPc/2s08PQVjvIBNY96UACb0CivrWg==", + "requires": { + "auto-bind": "^2.0.0", + "boolean": "^0.2.0", + "boom": "7.3.0", + "country-language": "^0.1.7", + "i18n": "^0.8.3", + "i18n-locales": "^0.0.2", + "lodash": "^4.17.11", + "moment": "^2.23.0", + "qs": "^6.6.0", + "underscore.string": "^3.3.5" + }, + "dependencies": { + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + } + } + }, "@sendgrid/client": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-6.2.1.tgz", @@ -31,6 +68,11 @@ "@sendgrid/helpers": "^6.2.1" } }, + "@sindresorhus/is": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.17.1.tgz", + "integrity": "sha512-kg/maAZD2Z2AHDFp7cY/ACokjUL0e7MaupTtGXkSW2SV4DJQEHdslFUioP0SMccotjwqTdI0b4XH/qZh6CN+kQ==" + }, "@types/archiver": { "version": "0.15.37", "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-0.15.37.tgz", @@ -49,6 +91,19 @@ "aws-sdk": "*" } }, + "@types/babel-types": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/@types/babel-types/-/babel-types-7.0.7.tgz", + "integrity": "sha512-dBtBbrc+qTHy1WdfHYjBwRln4+LWqASWakLHsWHR2NWHIFkv4W3O070IGoGLEBrJBvct3r0L1BUPuvURi7kYUQ==" + }, + "@types/babylon": { + "version": "6.16.5", + "resolved": "https://registry.npmjs.org/@types/babylon/-/babylon-6.16.5.tgz", + "integrity": "sha512-xH2e58elpj1X4ynnKp9qSnWlsRTIs6n3tgLGNfwAGHwePw0mulHQllV34n0T25uYSu1k0hRKkWXF890B1yS47w==", + "requires": { + "@types/babel-types": "*" + } + }, "@types/body-parser": { "version": "1.16.0", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.16.0.tgz", @@ -202,6 +257,12 @@ "integrity": "sha512-oMzU0D7jDp+H2go/i0XqBHfr+HEhYD/e1TvkhHi3yrhQm/7JFR8FJMdvoH76X8G1FBpgc6Pwi+QslCJBeJ1N9g==", "dev": true }, + "@types/html-to-text": { + "version": "1.4.31", + "resolved": "https://registry.npmjs.org/@types/html-to-text/-/html-to-text-1.4.31.tgz", + "integrity": "sha512-9vTFw6vYZNnjPOep9WRXs7cw0vg04pAZgcX9bqx70q1BNT7y9sOJovpbiNIcSNyHF/6LscLvGhtb5Og1T0UEvA==", + "dev": true + }, "@types/marked": { "version": "0.0.28", "resolved": "https://registry.npmjs.org/@types/marked/-/marked-0.0.28.tgz", @@ -301,6 +362,11 @@ "@types/passport": "*" } }, + "@types/prop-types": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.1.tgz", + "integrity": "sha512-CFzn9idOEpHrgdw8JsoTkaDDyRWk1jrzIV8djzcgpq0y9tG4B4lFT+Nxh52DVpDXV+n4+NPNv7M1Dj5uMp6XFg==" + }, "@types/qr-image": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@types/qr-image/-/qr-image-3.2.1.tgz", @@ -316,6 +382,15 @@ "integrity": "sha512-HtKGu+qG1NPvYe1z7ezLsyIaXYyi8SoAVqWDZgDQ8dLrsZvSzUNCwZyfX33uhWxL/SU0ZDQZ3nwZ0nimt507Kw==", "dev": true }, + "@types/react": { + "version": "16.8.23", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.8.23.tgz", + "integrity": "sha512-abkEOIeljniUN9qB5onp++g0EY38h7atnDHxwKUFz1r3VH1+yG1OKi2sNPTyObL40goBmfKFpdii2lEzwLX1cA==", + "requires": { + "@types/prop-types": "*", + "csstype": "^2.2.0" + } + }, "@types/request": { "version": "2.47.0", "resolved": "https://registry.npmjs.org/@types/request/-/request-2.47.0.tgz", @@ -399,6 +474,11 @@ "integrity": "sha512-dM5YQytWb1EunntizWnsAsADJxbXhHQyPoRxXlfEMPULcnbgzB02qZ8KI/K5yFItulzoidxWbX4OO/w4FN92Sg==", "dev": true }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, "accepts": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", @@ -423,6 +503,26 @@ } } }, + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=" + }, + "acorn-globals": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-3.1.0.tgz", + "integrity": "sha1-/YJw9x+7SZawBPqIDuXUZXOnMb8=", + "requires": { + "acorn": "^4.0.4" + }, + "dependencies": { + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" + } + } + }, "ajv": { "version": "5.5.2", "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", @@ -434,6 +534,51 @@ "json-schema-traverse": "^0.3.0" } }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "requires": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + } + }, + "ambi": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ambi/-/ambi-2.5.0.tgz", + "integrity": "sha1-fI43K+SIkRV+fOoBy2+RQ9H3QiA=", + "requires": { + "editions": "^1.1.1", + "typechecker": "^4.3.0" + }, + "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + }, + "typechecker": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/typechecker/-/typechecker-4.7.0.tgz", + "integrity": "sha512-4LHc1KMNJ6NDGO+dSM/yNfZQRtp8NN7psYrPHUblD62Dvkwsp3VShsbM78kOgpcmMkRTgvwdKOTjctS+uMllgQ==", + "requires": { + "editions": "^2.1.0" + }, + "dependencies": { + "editions": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/editions/-/editions-2.1.3.tgz", + "integrity": "sha512-xDZyVm0A4nLgMNWVVLJvcwMjI80ShiH/27RyLiCnW1L273TcJIA25C4pwJ33AWV01OX6UriP35Xu+lH4S7HWQw==", + "requires": { + "errlop": "^1.1.1", + "semver": "^5.6.0" + } + } + } + } + } + }, "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", @@ -539,6 +684,11 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + }, "asn1": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", @@ -568,6 +718,14 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, + "auto-bind": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-2.1.0.tgz", + "integrity": "sha512-qZuFvkes1eh9lB2mg8/HG18C+5GIO51r+RrCSst/lh+i5B1CtVlkhTE488M805Nr3dKl0sM/pIFKSKUIlg3zUg==", + "requires": { + "@types/react": "^16.8.12" + } + }, "aws-sdk": { "version": "2.255.1", "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.255.1.tgz", @@ -634,6 +792,38 @@ } } }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + } + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -741,6 +931,19 @@ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" }, + "boolean": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-0.2.0.tgz", + "integrity": "sha512-mDcM3ChboDuhv4glLXEH1us7jMiWXRSs3R13Okoo+kkFOlLIjvF1y88507wTfDf9zsuv0YffSDFUwX95VAT/mg==" + }, + "boom": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-7.3.0.tgz", + "integrity": "sha512-Swpoyi2t5+GhOEGw8rEsKvTxFLIDiiKoUc2gsoV6Lyr43LHBIzch3k2MvYUs8RTROrIkVJ3Al0TkaOGjnb+B6A==", + "requires": { + "hoek": "6.x.x" + } + }, "bowser": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/bowser/-/bowser-1.9.3.tgz", @@ -848,11 +1051,25 @@ "stack-trace": "~0.0.7" } }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" + }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "requires": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + } + }, "chai": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", @@ -877,6 +1094,14 @@ "supports-color": "^5.3.0" } }, + "character-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", + "integrity": "sha1-x84o821LzZdE5f/CxfzeHHMmH8A=", + "requires": { + "is-regex": "^1.0.3" + } + }, "check-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", @@ -896,6 +1121,14 @@ "parse5": "^3.0.1" } }, + "clean-css": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz", + "integrity": "sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==", + "requires": { + "source-map": "~0.6.0" + } + }, "cli-color": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-1.2.0.tgz", @@ -917,6 +1150,23 @@ "colors": "1.0.3" } }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "requires": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" + } + } + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -1041,6 +1291,25 @@ "mongodb": "^2.0.36" } }, + "consolidate": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.15.1.tgz", + "integrity": "sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==", + "requires": { + "bluebird": "^3.1.1" + } + }, + "constantinople": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.1.2.tgz", + "integrity": "sha512-yePcBqEFhLOqSBtwYOGGS1exHo/s1xjekXiinh4itpNQGCu4KA1euPh1fg07N2wMITZXQkBz75Ntdt1ctGZouw==", + "requires": { + "@types/babel-types": "^7.0.0", + "@types/babylon": "^6.16.2", + "babel-types": "^6.26.0", + "babylon": "^6.18.0" + } + }, "content-disposition": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", @@ -1093,6 +1362,15 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "country-language": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/country-language/-/country-language-0.1.7.tgz", + "integrity": "sha1-eHD0uhJduaYHHxlze9nvk0OuNds=", + "requires": { + "underscore": "~1.7.0", + "underscore.deep": "~0.5.1" + } + }, "crc": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/crc/-/crc-3.5.0.tgz", @@ -1107,6 +1385,30 @@ "readable-stream": "^2.0.0" } }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + } + } + }, + "csextends": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/csextends/-/csextends-1.2.0.tgz", + "integrity": "sha512-S/8k1bDTJIwuGgQYmsRoE+8P+ohV32WhQ0l4zqrc0XDdxOhjQQD7/wTZwCzoZX53jSX3V/qwjT+OkPTxWQcmjg==" + }, "css-select": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", @@ -1123,6 +1425,11 @@ "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.0.tgz", "integrity": "sha1-lGfQMsOM+u+58teVASUwYvh/ob0=" }, + "csstype": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.5.tgz", + "integrity": "sha512-JsTaiksRsel5n7XwqPAfB0l3TFKdpjW/kgAELf9vrb5adGA7UCPLajKK5s3nFrcFm3Rkyp/Qkgl73ENc1UY3cA==" + }, "d": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", @@ -1139,6 +1446,15 @@ "assert-plus": "^1.0.0" } }, + "datauri": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/datauri/-/datauri-2.0.0.tgz", + "integrity": "sha512-zS2HSf9pI5XPlNZgIqJg/wCJpecgU/HA6E/uv2EfaWnW1EiTGLfy/EexTIsC9c99yoCOTXlqeeWk4FkCSuO3/g==", + "requires": { + "image-size": "^0.7.3", + "mimer": "^1.0.0" + } + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -1147,6 +1463,11 @@ "ms": "2.0.0" } }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, "deep-eql": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", @@ -1156,6 +1477,11 @@ "type-detect": "^4.0.0" } }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1214,6 +1540,11 @@ "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", "dev": true }, + "doctypes": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", + "integrity": "sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=" + }, "dom-serializer": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", @@ -1252,6 +1583,14 @@ "domelementtype": "1" } }, + "eachr": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/eachr/-/eachr-2.0.4.tgz", + "integrity": "sha1-Rm98qhBwj2EFCeMsgHqv5X/BIr8=", + "requires": { + "typechecker": "^2.0.8" + } + }, "easymde": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/easymde/-/easymde-2.5.1.tgz", @@ -1277,11 +1616,50 @@ "jsbn": "~0.1.0" } }, + "editions": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/editions/-/editions-1.3.4.tgz", + "integrity": "sha512-gzao+mxnYDzIysXKMQi/+M1mjy/rjestjg6OPoYTtI+3Izp23oiGZitsl9lPDPiTGXbcSIk1iJWhliSaglxnUg==" + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, + "email-templates": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/email-templates/-/email-templates-6.0.0.tgz", + "integrity": "sha512-NzneEyM+J/DpMY7hK4Ii1HBmiX/BTQyAf8OEZh1yU+O9uYMgnJr+JvpAxLkqRxeWeA0dT2IV5K+6UcF/jMJk7Q==", + "requires": { + "@ladjs/i18n": "^1.1.0", + "@sindresorhus/is": "^0.17.1", + "auto-bind": "^2.1.0", + "consolidate": "^0.15.1", + "debug": "^4.1.1", + "get-paths": "^0.0.4", + "html-to-text": "^5.1.1", + "juice": "^5.2.0", + "lodash": "^4.17.11", + "nodemailer": "^6.2.1", + "pify": "^4.0.1", + "preview-email": "^1.0.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -1300,6 +1678,30 @@ "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=" }, + "errlop": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/errlop/-/errlop-1.1.1.tgz", + "integrity": "sha512-WX7QjiPHhsny7/PQvrhS5VMizXXKoKCS3udaBp8gjlARdbn+XmK300eKBAAN0hGyRaTCtRpOaxK+xFVPUJ3zkw==", + "requires": { + "editions": "^2.1.2" + }, + "dependencies": { + "editions": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/editions/-/editions-2.1.3.tgz", + "integrity": "sha512-xDZyVm0A4nLgMNWVVLJvcwMjI80ShiH/27RyLiCnW1L273TcJIA25C4pwJ33AWV01OX6UriP35Xu+lH4S7HWQw==", + "requires": { + "errlop": "^1.1.1", + "semver": "^5.6.0" + } + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + } + } + }, "es5-ext": { "version": "0.10.45", "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.45.tgz", @@ -1363,8 +1765,7 @@ "esutils": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" }, "etag": { "version": "1.8.1", @@ -1551,6 +1952,36 @@ "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", "dev": true }, + "extendr": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/extendr/-/extendr-2.1.0.tgz", + "integrity": "sha1-MBqgu+pWX00tyPVw8qImEahSe1Y=", + "requires": { + "typechecker": "~2.0.1" + }, + "dependencies": { + "typechecker": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/typechecker/-/typechecker-2.0.8.tgz", + "integrity": "sha1-6D2oS7ZMWEzLNFg4V2xAsDN9uC4=" + } + } + }, + "extract-opts": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/extract-opts/-/extract-opts-2.2.0.tgz", + "integrity": "sha1-H6KOunNSxttID4hc63GkaBC+bX0=", + "requires": { + "typechecker": "~2.0.1" + }, + "dependencies": { + "typechecker": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/typechecker/-/typechecker-2.0.8.tgz", + "integrity": "sha1-6D2oS7ZMWEzLNFg4V2xAsDN9uC4=" + } + } + }, "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", @@ -1646,12 +2077,46 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, "get-func-name": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", "dev": true }, + "get-paths": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/get-paths/-/get-paths-0.0.4.tgz", + "integrity": "sha512-+AxlfMGN7FuJr2zhT6aErH08HMKkRwynTTHtWCenIWkIZgx2OlkZKgt7SM4+rh8Dfi32lo6HcvqeTLxph3kjQw==", + "requires": { + "bluebird": "^3.5.1", + "fs-extra": "^4.0.2" + }, + "dependencies": { + "fs-extra": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", + "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "^4.1.6" + } + } + } + }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -1774,11 +2239,18 @@ "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, "has-ansi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, "requires": { "ansi-regex": "^2.0.0" } @@ -1794,6 +2266,70 @@ "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", "dev": true }, + "hoek": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-6.1.3.tgz", + "integrity": "sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ==" + }, + "html-to-text": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-5.1.1.tgz", + "integrity": "sha512-Bci6bD/JIfZSvG4s0gW/9mMKwBRoe/1RWLxUME/d6WUSZCdY7T60bssf/jFf7EYXRyqU4P5xdClVqiYU0/ypdA==", + "requires": { + "he": "^1.2.0", + "htmlparser2": "^3.10.1", + "lodash": "^4.17.11", + "minimist": "^1.2.0" + }, + "dependencies": { + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" + }, + "htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "requires": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "readable-stream": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", + "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "string_decoder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", + "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "htmlparser2": { "version": "3.9.2", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz", @@ -1835,6 +2371,24 @@ "sshpk": "^1.7.0" } }, + "i18n": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/i18n/-/i18n-0.8.3.tgz", + "integrity": "sha1-LYzxwkciYCwgQdAbpq5eqlE4jw4=", + "requires": { + "debug": "*", + "make-plural": "^3.0.3", + "math-interval-parser": "^1.1.0", + "messageformat": "^0.3.1", + "mustache": "*", + "sprintf-js": ">=1.0.3" + } + }, + "i18n-locales": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/i18n-locales/-/i18n-locales-0.0.2.tgz", + "integrity": "sha512-WCaJVIfU10v0/ZNy+mG7fCUQb1o2PsM7tNf1dUg0uU9OxtygDkWRqLT9Q/X30V2XsUb6XUEPbSsdUiORfDPVQA==" + }, "iconv-lite": { "version": "0.4.23", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", @@ -1848,6 +2402,25 @@ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=" }, + "ignorefs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ignorefs/-/ignorefs-1.2.0.tgz", + "integrity": "sha1-2ln7hYl25KXkNwLM0fKC/byeV1Y=", + "requires": { + "editions": "^1.3.3", + "ignorepatterns": "^1.1.0" + } + }, + "ignorepatterns": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ignorepatterns/-/ignorepatterns-1.1.0.tgz", + "integrity": "sha1-rI9DbyI5td+2bV8NOpBKh6xnzF4=" + }, + "image-size": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.7.4.tgz", + "integrity": "sha512-GqPgxs+VkOr12aWwjSkyRzf5atzObWpFtiRuDgxCl2I/SDpZOKZFRD3iIAeAN6/usmn8SeLWRt7a8JRYK0Whbw==" + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -1877,21 +2450,55 @@ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, + "is-expression": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-3.0.0.tgz", + "integrity": "sha1-Oayqa+f9HzRx3ELHQW5hwkMXrJ8=", + "requires": { + "acorn": "~4.0.2", + "object-assign": "^4.0.1" + }, + "dependencies": { + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" + } + } + }, "is-promise": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "requires": { + "has": "^1.0.1" + } + }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=" + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -1907,6 +2514,11 @@ "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" }, + "js-stringify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", + "integrity": "sha1-Fzb939lyTyijaCrcYjCufk6Weds=" + }, "js-tokens": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", @@ -2020,7 +2632,7 @@ }, "jsonfile": { "version": "2.4.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "resolved": "http://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", "requires": { "graceful-fs": "^4.1.6" @@ -2037,11 +2649,77 @@ "verror": "1.10.0" } }, + "jstransformer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", + "integrity": "sha1-7Yvwkh4vPx7U1cGkT2hwntJHIsM=", + "requires": { + "is-promise": "^2.0.0", + "promise": "^7.0.1" + } + }, + "juice": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/juice/-/juice-5.2.0.tgz", + "integrity": "sha512-0l6GZmT3efexyaaay3SchKT5kG311N59TEFP5lfvEy0nz9SNqjx311plJ3b4jze7arsmDsiHQLh/xnAuk0HFTQ==", + "requires": { + "cheerio": "^0.22.0", + "commander": "^2.15.1", + "cross-spawn": "^6.0.5", + "deep-extend": "^0.6.0", + "mensch": "^0.3.3", + "slick": "^1.12.2", + "web-resource-inliner": "^4.3.1" + }, + "dependencies": { + "cheerio": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz", + "integrity": "sha1-qbqoYKP5tZWmuBsahocxIe06Jp4=", + "requires": { + "css-select": "~1.2.0", + "dom-serializer": "~0.1.0", + "entities": "~1.1.1", + "htmlparser2": "^3.9.1", + "lodash.assignin": "^4.0.9", + "lodash.bind": "^4.1.4", + "lodash.defaults": "^4.0.1", + "lodash.filter": "^4.4.0", + "lodash.flatten": "^4.2.0", + "lodash.foreach": "^4.3.0", + "lodash.map": "^4.4.0", + "lodash.merge": "^4.4.0", + "lodash.pick": "^4.2.1", + "lodash.reduce": "^4.4.0", + "lodash.reject": "^4.4.0", + "lodash.some": "^4.4.0" + } + }, + "commander": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==" + } + } + }, "kareem": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.0.tgz", "integrity": "sha512-6hHxsp9e6zQU8nXsP+02HGWXwTkOEw6IROhF2ZA28cYbUk4eJ6QbtZvdqZOdD9YPKghG3apk5eOCvs+tLl3lRg==" }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=" + }, "lazystream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", @@ -2055,16 +2733,41 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" }, + "lodash.assignin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz", + "integrity": "sha1-uo31+4QesKPoBEIysOJjqNxqKKI=" + }, + "lodash.bind": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz", + "integrity": "sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU=" + }, "lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" }, + "lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" + }, + "lodash.filter": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", + "integrity": "sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4=" + }, "lodash.flatten": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" }, + "lodash.foreach": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", + "integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=" + }, "lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -2075,16 +2778,56 @@ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" }, + "lodash.map": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", + "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=" + }, + "lodash.merge": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.1.tgz", + "integrity": "sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ==" + }, + "lodash.pick": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", + "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=" + }, + "lodash.reduce": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", + "integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs=" + }, + "lodash.reject": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz", + "integrity": "sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU=" + }, "lodash.set": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=" }, + "lodash.some": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", + "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=" + }, + "lodash.unescape": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz", + "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=" + }, "lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" + }, "lru-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", @@ -2102,11 +2845,35 @@ "q": "^1.4.1" } }, + "make-plural": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/make-plural/-/make-plural-3.0.6.tgz", + "integrity": "sha1-IDOgO6wpC487uRJY9lud9+iwHKc=", + "requires": { + "minimist": "^1.2.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "optional": true + } + } + }, "marked": { "version": "0.3.19", "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.19.tgz", "integrity": "sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg==" }, + "math-interval-parser": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-interval-parser/-/math-interval-parser-1.1.0.tgz", + "integrity": "sha1-2+2lsGsySZc8bfYXD94jhvCv2JM=", + "requires": { + "xregexp": "^2.0.0" + } + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -2133,11 +2900,47 @@ "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", "optional": true }, + "mensch": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/mensch/-/mensch-0.3.3.tgz", + "integrity": "sha1-4gD/TdgjcX+OBWOzLj9UgfyiYrI=" + }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" }, + "messageformat": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/messageformat/-/messageformat-0.3.1.tgz", + "integrity": "sha1-5Y//gkXps5cXmeW0PbWLPpQX9aI=", + "requires": { + "async": "~1.5.2", + "glob": "~6.0.4", + "make-plural": "~3.0.3", + "nopt": "~3.0.6", + "watchr": "~2.4.13" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" + }, + "glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -2161,6 +2964,11 @@ "mime-db": "~1.30.0" } }, + "mimer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mimer/-/mimer-1.0.0.tgz", + "integrity": "sha512-4ZJvCzfcwsBgPbkKXUzGoVZMWjv8IDIygkGzVc7uUYhgnK0t2LmGxxjdgH1i+pn0/KQfB5F/VKUJlfyTSOFQjg==" + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -2429,6 +3237,11 @@ } } }, + "mustache": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-3.0.1.tgz", + "integrity": "sha512-jFI/4UVRsRYdUbuDTKT7KzfOp7FiD5WzYmmwNwXyUVypC0xjoTL78Fqc0jHUPIvvGD+6DQSPHIt1NE7D1ArsqA==" + }, "mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -2454,6 +3267,24 @@ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" + }, + "nodemailer": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.2.1.tgz", + "integrity": "sha512-TagB7iuIi9uyNgHExo8lUDq3VK5/B0BpbkcjIgNvxbtVrjNqq0DwAOTuzALPVkK76kMhTSzIgHqg8X1uklVs6g==" + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "requires": { + "abbrev": "1" + } + }, "normalize-path": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", @@ -2509,6 +3340,14 @@ "format-util": "^1.0.3" } }, + "open": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-6.4.0.tgz", + "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==", + "requires": { + "is-wsl": "^1.1.0" + } + }, "optimist": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", @@ -2574,6 +3413,11 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + }, "path-parse": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", @@ -2600,11 +3444,64 @@ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + }, + "preview-email": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/preview-email/-/preview-email-1.0.1.tgz", + "integrity": "sha512-GmkAoyraQg8Ypq6mFVePdFL1upN6X2PQn3tD7UJP/aUs8HamtRekFsMunu3TaextEKycixE+Aol4hsHtqoG1pg==", + "requires": { + "@babel/runtime": "^7.4.5", + "debug": "^4.1.1", + "moment": "^2.24.0", + "nodemailer": "^6.2.1", + "open": "^6.3.0", + "pify": "^4.0.1", + "pug": "^2.0.4", + "uuid": "^3.3.2" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + } + } + }, "process-nextick-args": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" }, + "promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "requires": { + "asap": "~2.0.3" + } + }, "proxy-addr": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz", @@ -2619,6 +3516,137 @@ "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==" }, + "pug": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pug/-/pug-2.0.4.tgz", + "integrity": "sha512-XhoaDlvi6NIzL49nu094R2NA6P37ijtgMDuWE+ofekDChvfKnzFal60bhSdiy8y2PBO6fmz3oMEIcfpBVRUdvw==", + "requires": { + "pug-code-gen": "^2.0.2", + "pug-filters": "^3.1.1", + "pug-lexer": "^4.1.0", + "pug-linker": "^3.0.6", + "pug-load": "^2.0.12", + "pug-parser": "^5.0.1", + "pug-runtime": "^2.0.5", + "pug-strip-comments": "^1.0.4" + } + }, + "pug-attrs": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-2.0.4.tgz", + "integrity": "sha512-TaZ4Z2TWUPDJcV3wjU3RtUXMrd3kM4Wzjbe3EWnSsZPsJ3LDI0F3yCnf2/W7PPFF+edUFQ0HgDL1IoxSz5K8EQ==", + "requires": { + "constantinople": "^3.0.1", + "js-stringify": "^1.0.1", + "pug-runtime": "^2.0.5" + } + }, + "pug-code-gen": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-2.0.2.tgz", + "integrity": "sha512-kROFWv/AHx/9CRgoGJeRSm+4mLWchbgpRzTEn8XCiwwOy6Vh0gAClS8Vh5TEJ9DBjaP8wCjS3J6HKsEsYdvaCw==", + "requires": { + "constantinople": "^3.1.2", + "doctypes": "^1.1.0", + "js-stringify": "^1.0.1", + "pug-attrs": "^2.0.4", + "pug-error": "^1.3.3", + "pug-runtime": "^2.0.5", + "void-elements": "^2.0.1", + "with": "^5.0.0" + } + }, + "pug-error": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-1.3.3.tgz", + "integrity": "sha512-qE3YhESP2mRAWMFJgKdtT5D7ckThRScXRwkfo+Erqga7dyJdY3ZquspprMCj/9sJ2ijm5hXFWQE/A3l4poMWiQ==" + }, + "pug-filters": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-3.1.1.tgz", + "integrity": "sha512-lFfjNyGEyVWC4BwX0WyvkoWLapI5xHSM3xZJFUhx4JM4XyyRdO8Aucc6pCygnqV2uSgJFaJWW3Ft1wCWSoQkQg==", + "requires": { + "clean-css": "^4.1.11", + "constantinople": "^3.0.1", + "jstransformer": "1.0.0", + "pug-error": "^1.3.3", + "pug-walk": "^1.1.8", + "resolve": "^1.1.6", + "uglify-js": "^2.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "requires": { + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.10.0" + } + } + } + }, + "pug-lexer": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-4.1.0.tgz", + "integrity": "sha512-i55yzEBtjm0mlplW4LoANq7k3S8gDdfC6+LThGEvsK4FuobcKfDAwt6V4jKPH9RtiE3a2Akfg5UpafZ1OksaPA==", + "requires": { + "character-parser": "^2.1.1", + "is-expression": "^3.0.0", + "pug-error": "^1.3.3" + } + }, + "pug-linker": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-3.0.6.tgz", + "integrity": "sha512-bagfuHttfQOpANGy1Y6NJ+0mNb7dD2MswFG2ZKj22s8g0wVsojpRlqveEQHmgXXcfROB2RT6oqbPYr9EN2ZWzg==", + "requires": { + "pug-error": "^1.3.3", + "pug-walk": "^1.1.8" + } + }, + "pug-load": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-2.0.12.tgz", + "integrity": "sha512-UqpgGpyyXRYgJs/X60sE6SIf8UBsmcHYKNaOccyVLEuT6OPBIMo6xMPhoJnqtB3Q3BbO4Z3Bjz5qDsUWh4rXsg==", + "requires": { + "object-assign": "^4.1.0", + "pug-walk": "^1.1.8" + } + }, + "pug-parser": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-5.0.1.tgz", + "integrity": "sha512-nGHqK+w07p5/PsPIyzkTQfzlYfuqoiGjaoqHv1LjOv2ZLXmGX1O+4Vcvps+P4LhxZ3drYSljjq4b+Naid126wA==", + "requires": { + "pug-error": "^1.3.3", + "token-stream": "0.0.1" + } + }, + "pug-runtime": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-2.0.5.tgz", + "integrity": "sha512-P+rXKn9un4fQY77wtpcuFyvFaBww7/91f3jHa154qU26qFAnOe6SW1CbIDcxiG5lLK9HazYrMCCuDvNgDQNptw==" + }, + "pug-strip-comments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-1.0.4.tgz", + "integrity": "sha512-i5j/9CS4yFhSxHp5iKPHwigaig/VV9g+FgReLJWWHEHbvKsbqL0oP/K5ubuLco6Wu3Kan5p7u7qk8A4oLLh6vw==", + "requires": { + "pug-error": "^1.3.3" + } + }, + "pug-walk": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-1.1.8.tgz", + "integrity": "sha512-GMu3M5nUL3fju4/egXwZO0XLi6fW/K3T3VTgFQ14GxNi8btlxgT5qZL//JwZFm/2Fa64J/PNS8AZeys3wiMkVA==" + }, "punycode": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", @@ -2687,6 +3715,11 @@ "resolve": "^1.1.6" } }, + "regenerator-runtime": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz", + "integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA==" + }, "regexp-clone": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-0.0.1.tgz", @@ -2697,6 +3730,11 @@ "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, "request": { "version": "2.88.0", "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", @@ -2853,11 +3891,27 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "requires": { + "align-text": "^0.1.1" + } + }, "safe-buffer": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" }, + "safefs": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/safefs/-/safefs-3.2.2.tgz", + "integrity": "sha1-gXDBRE1wOOCMrqBaN0+uL6NJ4Vw=", + "requires": { + "graceful-fs": "*" + } + }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -2877,6 +3931,16 @@ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" }, + "scandirectory": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/scandirectory/-/scandirectory-2.5.0.tgz", + "integrity": "sha1-bOA/VKCQtmjjy+2/IO354xBZPnI=", + "requires": { + "ignorefs": "^1.0.0", + "safefs": "^3.1.2", + "taskgroup": "^4.0.5" + } + }, "semver": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", @@ -2930,6 +3994,19 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + }, "shelljs": { "version": "0.7.7", "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.7.tgz", @@ -2945,6 +4022,11 @@ "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" }, + "slick": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz", + "integrity": "sha1-vQSN23TefRymkV+qSldXCzVQwtc=" + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -3022,7 +4104,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3095,6 +4176,15 @@ "xtend": "^4.0.0" } }, + "taskgroup": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/taskgroup/-/taskgroup-4.3.1.tgz", + "integrity": "sha1-feGT/r12gnPEV3MElwJNUSwnkVo=", + "requires": { + "ambi": "^2.2.0", + "csextends": "^1.0.3" + } + }, "thenify": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz", @@ -3128,6 +4218,16 @@ "os-tmpdir": "~1.0.2" } }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" + }, + "token-stream": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-0.0.1.tgz", + "integrity": "sha1-zu78cXp2xDFvEm0LnbqlXX598Bo=" + }, "tslib": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", @@ -3209,6 +4309,11 @@ "mime-types": "~2.1.15" } }, + "typechecker": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/typechecker/-/typechecker-2.1.0.tgz", + "integrity": "sha1-0cIJOlT/ihn1jP+HfuqlTyJC04M=" + }, "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -3243,6 +4348,12 @@ } } }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "optional": true + }, "uid-safe": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", @@ -3256,6 +4367,30 @@ "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", "integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=" }, + "underscore": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", + "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=" + }, + "underscore.deep": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/underscore.deep/-/underscore.deep-0.5.1.tgz", + "integrity": "sha1-ByZx9I1oc1w0Ij/P72PmnlJ2zCs=" + }, + "underscore.string": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.5.tgz", + "integrity": "sha512-g+dpmgn+XBneLmXXo+sGlW5xQEt4ErkS3mgeN2GFbremYeMBSJKr9Wf2KJplQVaiPY/f7FN6atosWYNm9ovrYg==", + "requires": { + "sprintf-js": "^1.0.3", + "util-deprecate": "^1.0.2" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -3300,6 +4435,11 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" }, + "valid-data-url": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-0.1.6.tgz", + "integrity": "sha512-FXg2qXMzfAhZc0y2HzELNfUeiOjPr+52hU1DNBWiJJ2luXD+dD1R9NA48Ug5aj0ibbxroeGDc/RJv6ThiGgkDw==" + }, "validator": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/validator/-/validator-10.3.0.tgz", @@ -3320,16 +4460,98 @@ "extsprintf": "^1.2.0" } }, + "void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" + }, "walkdir": { "version": "0.0.11", "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.0.11.tgz", "integrity": "sha1-oW0CXrkxvQO1LzCMrtD0D86+lTI=" }, + "watchr": { + "version": "2.4.13", + "resolved": "https://registry.npmjs.org/watchr/-/watchr-2.4.13.tgz", + "integrity": "sha1-10hHu01vkPYf4sdPn2hmKqDgdgE=", + "requires": { + "eachr": "^2.0.2", + "extendr": "^2.1.0", + "extract-opts": "^2.2.0", + "ignorefs": "^1.0.0", + "safefs": "^3.1.2", + "scandirectory": "^2.5.0", + "taskgroup": "^4.2.0", + "typechecker": "^2.0.8" + } + }, + "web-resource-inliner": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-4.3.2.tgz", + "integrity": "sha512-eVnNqwG20sbAgqv2JONwyr57UNZFJP4oauioeUjpCMY83AM11956eIhxlCGGXfSMi7bRBjR9Vao05bXFzslh7w==", + "requires": { + "async": "^2.1.2", + "chalk": "^1.1.3", + "datauri": "^2.0.0", + "htmlparser2": "^3.9.2", + "lodash.unescape": "^4.0.1", + "request": "^2.78.0", + "safer-buffer": "^2.1.2", + "valid-data-url": "^0.1.4", + "xtend": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, "whatwg-fetch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz", "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==" }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" + }, + "with": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/with/-/with-5.1.1.tgz", + "integrity": "sha1-+k2qktrzLE6pTtRTyB8EaGtXXf4=", + "requires": { + "acorn": "^3.1.0", + "acorn-globals": "^3.0.0" + } + }, "wordwrap": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", @@ -3357,11 +4579,27 @@ "lodash": "^4.0.0" } }, + "xregexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", + "integrity": "sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM=" + }, "xtend": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "requires": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + }, "z-schema": { "version": "3.22.0", "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-3.22.0.tgz", diff --git a/package.json b/package.json index 12caf0f2..78d78029 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "cookie-parser": "^1.4.3", "cookie-signature": "^1.1.0", "easymde": "^2.5.1", + "email-templates": "^6.0.0", "express": "^4.16.3", "express-session": "^1.15.1", "git-rev-sync": "^1.12.0", @@ -48,6 +49,7 @@ "graphql-tools": "^1.2.2", "graphql-typewriter": "git://github.com/illegalprime/graphql-typewriter.git#hacks", "handlebars": "^4.1.2", + "html-to-text": "^5.1.1", "json-schema-to-typescript": "^4.6.5", "json2csv": "^3.11.5", "marked": "^0.3.19", @@ -81,6 +83,7 @@ "@types/express": "^4.16.0", "@types/express-session": "0.0.32", "@types/handlebars": "^4.0.38", + "@types/html-to-text": "^1.4.31", "@types/marked": "0.0.28", "@types/mocha": "^2.2.48", "@types/moment-timezone": "^0.5.6", diff --git a/server/common.ts b/server/common.ts index 0baad9d6..a24db8bb 100644 --- a/server/common.ts +++ b/server/common.ts @@ -24,7 +24,11 @@ class Config implements IConfig.Main { }; public email: IConfig.Email = { from: "HackGT Team ", - key: "" + key: "", + headerImage: "", + twitterHandle: "TheHackGT", + facebookHandle: "thehackgt", + contactAddress: "hello@hack.gt" }; public server: IConfig.Server = { isProduction: false, @@ -151,6 +155,18 @@ class Config implements IConfig.Main { if (process.env.EMAIL_KEY) { this.email.key = process.env.EMAIL_KEY; } + if (process.env.EMAIL_HEADER_IMAGE) { + this.email.headerImage = process.env.EMAIL_HEADER_IMAGE; + } + if (process.env.EMAIL_TWITTER_HANDLE) { + this.email.twitterHandle = process.env.EMAIL_TWITTER_HANDLE; + } + if (process.env.EMAIL_FACEBOOK_HANDLE) { + this.email.facebookHandle = process.env.EMAIL_FACEBOOK_HANDLE; + } + if (process.env.EMAIL_CONTACT_ADDRESS) { + this.email.contactAddress = process.env.EMAIL_CONTACT_ADDRESS; + } // Server if (process.env.PRODUCTION && process.env.PRODUCTION.toLowerCase() === "true") { this.server.isProduction = true; @@ -366,6 +382,21 @@ import * as marked from "marked"; // tslint:disable-next-line:no-var-requires const striptags = require("striptags"); import { IUser, Team, IFormItem } from "./schema"; +import * as htmlToText from "html-to-text"; +// tslint:disable-next-line:no-var-requires +const Email = require("email-templates"); +const email = new Email({ + views: { + root: path.resolve("server/emails/") + }, + juice: true, + juiceResources: { + preserveImportant: true, + webResources: { + relativeTo: path.join(__dirname, "emails", "email-template") + } + } +}); export const defaultEmailSubjects = { apply: `[${config.eventName}] - Thank you for applying!`, @@ -414,7 +445,7 @@ export async function renderMarkdown(markdown: string, options?: MarkedOptions, }); }); } -export async function renderEmailHTML(markdown: string, user: IUser): Promise { +async function templateMarkdown(markdown: string, user: IUser): Promise { let teamName: string; if (await getSetting("teamsEnabled")) { teamName = "No team created or joined"; @@ -479,30 +510,25 @@ export async function renderEmailHTML(markdown: string, user: IUser): Promise data.name === name); return formatFormItem(question); }); - - return renderMarkdown(markdown); + return markdown; } -export async function renderEmailText(markdown: string, user: IUser, markdownRendered: boolean = false): Promise { - let html: string; - if (!markdownRendered) { - html = await renderEmailHTML(markdown, user); - } - else { - html = markdown; - } - // Remove ").replace(/"); - - // Append href of links to their text - const cheerio = await import("cheerio"); - let $ = cheerio.load(html, { decodeEntities: false }); - $("a").each((i, el) => { - let element = $(el); - element.text(`${element.text()} (${element.attr("href")})`); +export async function renderEmailHTML(markdown: string, user: IUser): Promise { + markdown = await templateMarkdown(markdown, user); + + let renderedMarkdown = await renderMarkdown(markdown); + return email.render("email-template/html", { + emailHeaderImage: config.email.headerImage, + twitterHandle: config.email.twitterHandle, + facebookHandle: config.email.facebookHandle, + emailAddress: config.email.contactAddress, + hackathonName: config.eventName, + body: renderedMarkdown }); - html = $.html(); - - return removeTags(html); +} +export async function renderEmailText(markdown: string, user: IUser): Promise { + let templatedMarkdown = await templateMarkdown(markdown, user); + let renderedHtml = await renderMarkdown(templatedMarkdown); + return htmlToText.fromString(renderedHtml); } // Verify and load questions diff --git a/server/emails/email-template/html.pug b/server/emails/email-template/html.pug new file mode 100644 index 00000000..1eb9dc8d --- /dev/null +++ b/server/emails/email-template/html.pug @@ -0,0 +1,177 @@ +doctype html +html(xmlns='http://www.w3.org/1999/xhtml' xmlns:v='urn:schemas-microsoft-com:vml' xmlns:o='urn:schemas-microsoft-com:office:office') + head + // NAME: 1 COLUMN + // + [if gte mso 15]> + + + + 96 + + + + + + + +
    + + + + + + + +
    + +
    + + + + + + + + + + + +
    + + + + + +
    + +
    + ${html}`; text = `${JSON.stringify(filter)}\n${text}`; emails.push({ diff --git a/server/routes/api/user.ts b/server/routes/api/user.ts index ebf0aa41..6ed4102f 100644 --- a/server/routes/api/user.ts +++ b/server/routes/api/user.ts @@ -205,7 +205,7 @@ function postApplicationBranchHandler(anonymous: boolean): (request: express.Req } let emailHTML = await renderEmailHTML(emailMarkdown, user); - let emailText = await renderEmailText(emailHTML, user, true); + let emailText = await renderEmailText(emailMarkdown, user); if (questionBranch instanceof Branches.ApplicationBranch) { if (!user.applied) { @@ -423,7 +423,7 @@ userRoutes.route("/send_acceptances").post(isAdmin, async (request, response): P } let html = await renderEmailHTML(emailMarkdown, user); - let text = await renderEmailText(html, user, true); + let text = await renderEmailText(emailMarkdown, user); emails.push({ from: config.email.from, diff --git a/server/schema.ts b/server/schema.ts index 09b3b8d9..9cb945d4 100644 --- a/server/schema.ts +++ b/server/schema.ts @@ -21,6 +21,10 @@ export namespace IConfig { export interface Email { from: string; key: string; + headerImage: string; + twitterHandle: string; + facebookHandle: string; + contactAddress: string; } export interface Server { isProduction: boolean; From 0885651af9865b36389c31805c0e1dadb6743b20 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Mon, 8 Jul 2019 00:54:50 -0400 Subject: [PATCH 156/194] Tidy up some syntax --- client/js/application.ts | 10 +++++----- server/routes/api/user.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/js/application.ts b/client/js/application.ts index 673fe2ec..3babfe26 100644 --- a/client/js/application.ts +++ b/client/js/application.ts @@ -130,12 +130,12 @@ for (let i = 0; i < inputsWithOther.length; i++) { let wordCountInputs = document.querySelectorAll("[data-max-word-count], [data-min-word-count]") as NodeListOf; for (let i = 0; i < wordCountInputs.length; i++) { wordCountInputs[i].addEventListener("input", e => { - let target = e.target as HTMLInputElement | HTMLTextAreaElement; - let correspondingLabel = document.querySelector(`label[for="${target.id}"] > .current-count`); - if (!correspondingLabel) return; + const target = e.target as HTMLInputElement | HTMLTextAreaElement; + const correspondingLabel = document.querySelector(`label[for="${target.id}"] > .current-count`); + if (!correspondingLabel) { return; } - let { maxWordCount, minWordCount } = target.dataset; - if (!maxWordCount && !minWordCount) return; + const { maxWordCount, minWordCount } = target.dataset; + if (!maxWordCount && !minWordCount) { return; } let wordCount = target.value.trim().split(/\s+/).length; if (target.value.trim().length === 0) { diff --git a/server/routes/api/user.ts b/server/routes/api/user.ts index 13e3074e..2e89c997 100644 --- a/server/routes/api/user.ts +++ b/server/routes/api/user.ts @@ -168,7 +168,7 @@ function postApplicationBranchHandler(anonymous: boolean): (request: express.Req if (getQuestion("").trim().length === 0) { wordCount = 0; } - let wordCountPlural = wordCount === 1 ? "" : "s"; + const wordCountPlural = wordCount === 1 ? "" : "s"; if (question.minWordCount && wordCount < question.minWordCount) { return reportError(`Your response to "${question.label}" must contain at least ${question.minWordCount} words (currently has ${wordCount} word${wordCountPlural})`); } From b6a37c2669fe128b3a8ab7c4480d2c8b5c33ca50 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Mon, 8 Jul 2019 01:21:34 -0400 Subject: [PATCH 157/194] Fix truthy bug --- server/routes/api/user.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routes/api/user.ts b/server/routes/api/user.ts index 2e89c997..a1ebcfef 100644 --- a/server/routes/api/user.ts +++ b/server/routes/api/user.ts @@ -128,7 +128,7 @@ function postApplicationBranchHandler(anonymous: boolean): (request: express.Req } function getQuestion(defaultValue?: T): T { let value = request.body[question.name] as T; - if (defaultValue) { + if (defaultValue !== undefined) { return value || defaultValue; } return value; From d6e47252a9219e8f80f2b22a0dfc358378c49e77 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Mon, 8 Jul 2019 01:26:44 -0400 Subject: [PATCH 158/194] Bump version to 3.1.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 12caf0f2..c243cfbe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "3.0.0", + "version": "3.1.1", "description": "Powerful and extensible registration system for hackathons and other large events", "main": "server/app.js", "scripts": { From 7b197036ecedff481bab64b82c31454b8b180df4 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Mon, 8 Jul 2019 01:39:31 -0400 Subject: [PATCH 159/194] Only check for word / char counts on string responses --- server/routes/api/user.ts | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/server/routes/api/user.ts b/server/routes/api/user.ts index a1ebcfef..f23e1f12 100644 --- a/server/routes/api/user.ts +++ b/server/routes/api/user.ts @@ -158,22 +158,25 @@ function postApplicationBranchHandler(anonymous: boolean): (request: express.Req return reportError(`"${question.label}" is a required field`); } } - if (question.minCharacterCount && getQuestion("").length < question.minCharacterCount) { - return reportError(`Your response to "${question.label}" must have at least ${question.minCharacterCount} characters`); - } - if (question.maxCharacterCount && getQuestion("").length > question.maxCharacterCount) { - return reportError(`Your response to "${question.label}" cannot exceed ${question.maxCharacterCount} characters`); - } - let wordCount = getQuestion("").trim().split(/\s+/).length; - if (getQuestion("").trim().length === 0) { - wordCount = 0; - } - const wordCountPlural = wordCount === 1 ? "" : "s"; - if (question.minWordCount && wordCount < question.minWordCount) { - return reportError(`Your response to "${question.label}" must contain at least ${question.minWordCount} words (currently has ${wordCount} word${wordCountPlural})`); - } - if (question.maxWordCount && wordCount > question.maxWordCount) { - return reportError(`Your response to "${question.label}" cannot exceed ${question.maxWordCount} words (currently has ${wordCount} word${wordCountPlural})`); + if (question.type !== "file" && (question.minCharacterCount || question.maxCharacterCount || question.minWordCount || question.maxWordCount)) { + let questionValue = getQuestion(""); + if (question.minCharacterCount && questionValue.length < question.minCharacterCount) { + return reportError(`Your response to "${question.label}" must have at least ${question.minCharacterCount} characters`); + } + if (question.maxCharacterCount && questionValue.length > question.maxCharacterCount) { + return reportError(`Your response to "${question.label}" cannot exceed ${question.maxCharacterCount} characters`); + } + let wordCount = questionValue.trim().split(/\s+/).length; + if (questionValue.trim().length === 0) { + wordCount = 0; + } + const wordCountPlural = wordCount === 1 ? "" : "s"; + if (question.minWordCount && wordCount < question.minWordCount) { + return reportError(`Your response to "${question.label}" must contain at least ${question.minWordCount} words (currently has ${wordCount} word${wordCountPlural})`); + } + if (question.maxWordCount && wordCount > question.maxWordCount) { + return reportError(`Your response to "${question.label}" cannot exceed ${question.maxWordCount} words (currently has ${wordCount} word${wordCountPlural})`); + } } if ((question.type === "select" || question.type === "radio") && Array.isArray(getQuestion(question.name)) && question.hasOther) { From 79c1ac07c1ef4c5e2ee0807a5321a0dc6a26bea2 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Mon, 8 Jul 2019 22:46:37 -0400 Subject: [PATCH 160/194] Debounce email change events to only hit the server for rendering every 500ms --- client/js/admin.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/client/js/admin.ts b/client/js/admin.ts index 384c99c9..b159fdba 100644 --- a/client/js/admin.ts +++ b/client/js/admin.ts @@ -602,7 +602,18 @@ const markdownEditor = new EasyMDE({ element: document.getElementById("email-con let contentChanged = false; let lastSelected = emailTypeSelect.value; -markdownEditor.codemirror.on("change", async () => { +const debounceTimeout = 500; // Milliseconds to wait before content is rendered to avoid hitting the server for every keystroke +function debounce(func: () => void): () => void { + let timer: number | null = null; + return () => { + if (timer) { + clearTimeout(timer); + } + timer = setTimeout(func, debounceTimeout); + }; +} + +markdownEditor.codemirror.on("change", debounce(async () => { contentChanged = true; try { let content = new FormData(); @@ -626,7 +637,7 @@ markdownEditor.codemirror.on("change", async () => { catch { emailRenderedArea.textContent = "Couldn't retrieve email content"; } -}); +})); async function emailTypeChange(): Promise { if (contentChanged) { From 8f417a7cb06747d08e0f51ed8a2fa85c5b031521 Mon Sep 17 00:00:00 2001 From: Ryan Petschek Date: Tue, 9 Jul 2019 00:18:55 -0400 Subject: [PATCH 161/194] Implement admin panel editor for interstitial content and generic markdown editor class --- client/admin.html | 22 ++++ client/css/admin.css | 2 +- client/js/admin.ts | 191 +++++++++++++++++++++++++--------- server/common.ts | 9 +- server/routes/api/settings.ts | 46 +++++++- 5 files changed, 218 insertions(+), 52 deletions(-) diff --git a/client/admin.html b/client/admin.html index 4c8a7ca7..80bbce4c 100644 --- a/client/admin.html +++ b/client/admin.html @@ -343,6 +343,28 @@
    List of variables:
    +
    +
    +

    Configure interstitial pages

    +

    These are pages that will be shown to users after they click on an application branch and before they are shown the form itself. It will not be shown to users who are editing an application that they already submitted.

    +

    Interstitial page content is written in Markdown. Valid HTML is also valid Markdown and can be useful for more complex layouts or styles. The same variables as email templating are supported (see above).

    + + +
    Rendered HTML:
    +
    +
    +

    config.json options

    diff --git a/client/css/admin.css b/client/css/admin.css index 574f9d3e..2b4f9d57 100644 --- a/client/css/admin.css +++ b/client/css/admin.css @@ -62,7 +62,7 @@ table thead { text-align: center; } -#email-rendered { +#email-rendered, #interstitial-rendered { padding: 10px; border-radius: 3px; background-color: #eee; diff --git a/client/js/admin.ts b/client/js/admin.ts index b159fdba..83e05dd2 100644 --- a/client/js/admin.ts +++ b/client/js/admin.ts @@ -587,23 +587,25 @@ sendAcceptancesButton.addEventListener("click", async e => { }); // -// Email content +// Markdown content // declare const EasyMDE: typeof import("easymde"); const emailTypeSelect = document.getElementById("email-type") as HTMLSelectElement; const emailSubject = document.getElementById("email-subject") as HTMLInputElement; + +const interstitialTypeSelect = document.getElementById("interstitial-type") as HTMLSelectElement; + let emailRenderedArea: HTMLElement | ShadowRoot = document.getElementById("email-rendered") as HTMLElement; +let interstitialRenderedArea: HTMLElement | ShadowRoot = document.getElementById("interstitial-rendered") as HTMLElement; if (document.head.attachShadow) { // Browser supports Shadow DOM emailRenderedArea = emailRenderedArea.attachShadow({ mode: "open" }); + interstitialRenderedArea = interstitialRenderedArea.attachShadow({ mode: "open" }); } -const markdownEditor = new EasyMDE({ element: document.getElementById("email-content")! }); -let contentChanged = false; -let lastSelected = emailTypeSelect.value; const debounceTimeout = 500; // Milliseconds to wait before content is rendered to avoid hitting the server for every keystroke -function debounce(func: () => void): () => void { +function debounce(func: (...args: unknown[]) => void): (...args: unknown[]) => void { let timer: number | null = null; return () => { if (timer) { @@ -613,57 +615,138 @@ function debounce(func: () => void): () => void { }; } -markdownEditor.codemirror.on("change", debounce(async () => { - contentChanged = true; - try { - let content = new FormData(); - content.append("content", markdownEditor.value()); +abstract class Editor { + private readonly editor: EasyMDE; + protected contentChanged: boolean = false; + protected lastSelected: string; - let { html, text }: { html: string; text: string } = ( - await fetch(`/api/settings/email_content/${encodeURIComponent(emailTypeSelect.value)}/rendered`, { - credentials: "same-origin", - method: "POST", - body: content - }).then(checkStatus).then(parseJSON) - ); - emailRenderedArea.innerHTML = html; - let hr = document.createElement("hr"); - hr.style.border = "1px solid #737373"; - emailRenderedArea.appendChild(hr); - let textContainer = document.createElement("pre"); - textContainer.textContent = text; - emailRenderedArea.appendChild(textContainer); + constructor(editorElementID: string, protected typeSelector: HTMLSelectElement) { + let element = document.getElementById(editorElementID); + if (!element) { + throw new Error(`Cannot create Markdown editor from non-existent