From c3df883be7b476b2ee1d0a65f4f89ac53c53d7d6 Mon Sep 17 00:00:00 2001 From: Andrew Dai Date: Mon, 5 Mar 2018 08:48:50 -0500 Subject: [PATCH 01/21] 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 02/21] 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 03/21] 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 04/21] 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 05/21] 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 06/21] 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 07/21] 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 09/21] 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 10/21] 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 11/21] 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 12/21] 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 13/21] 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 14/21] 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 15/21] 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 16/21] 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 17/21] 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 18/21] 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 19/21] 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 20/21] 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 21/21] 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": {