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 diff --git a/client/admin.html b/client/admin.html index 4b814941..4d06b710 100644 --- a/client/admin.html +++ b/client/admin.html @@ -243,13 +243,28 @@

{{this.name}}

+ +
+
+ + +
+
+ + +
+
+ + +
+
{{#each ../settings.branches.confirmation}} -
- - -
+
+ + +
{{/each}}
@@ -259,6 +274,12 @@

{{this.name}}

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

Edit email content

+ {{/unless}} + {{#each questionData}} {{#if this.textContent}} {{{this.textContent}}} @@ -76,14 +83,23 @@

Apply: {{branch}}

{{/each}} {{{endText}}}
+ {{#unless unauthenticated}} {{#if user.applied}} {{else}} {{/if}} + {{else}} + + {{/unless}} +
{{/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/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/js/admin.ts b/client/js/admin.ts index ee80b8f7..a61d965f 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; @@ -642,6 +645,61 @@ 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)); @@ -680,11 +738,14 @@ function settingsUpdate(e: MouseEvent) { let branchName = branchRoles[i].dataset.name!; let branchRole = branchRoles[i].querySelector("select")!.value; let branchData: { - role: string; - open?: Date; - close?: Date; - usesRollingDeadline?: boolean; - confirmationBranches?: string[]; + role: string; + open?: Date; + close?: Date; + usesRollingDeadline?: boolean; + confirmationBranches?: string[]; + noConfirmation?: boolean; + autoAccept?: boolean; + allowAnonymous?: boolean; } = {role: branchRole}; // TODO this should probably be typed (not just strings) if (branchRole !== "Noop") { @@ -702,6 +763,18 @@ 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 { + 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; + branchData.noConfirmation = false; + } } if (branchRole === "Confirmation") { let usesRollingDeadlineCheckbox = (branchRoles[i].querySelectorAll("input.usesRollingDeadline") as NodeListOf); diff --git a/client/js/application.ts b/client/js/application.ts index eea62066..799460c3 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")!.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 f25e7e30..0683ae7c 100644 --- a/client/partials/sidebar.html +++ b/client/partials/sidebar.html @@ -1,31 +1,40 @@
{{> @partial-block }} 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": { 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/branch.ts b/server/branch.ts index c08fa322..073c0264 100644 --- a/server/branch.ts +++ b/server/branch.ts @@ -155,7 +155,7 @@ export class NoopBranch { } } -abstract class TimedBranch extends NoopBranch { +export abstract class TimedBranch extends NoopBranch { public open: Date; public close: Date; @@ -177,16 +177,27 @@ 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; public confirmationBranches: 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 || []; } protected serializeSettings(): QuestionBranchSettings { return { ...super.serializeSettings(), + allowAnonymous: this.allowAnonymous, + autoAccept: this.autoAccept, + noConfirmation: this.noConfirmation, confirmationBranches: this.confirmationBranches }; } diff --git a/server/middleware.ts b/server/middleware.ts index df3e6823..01338f7f 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 @@ -130,22 +131,142 @@ 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 => { + return br.name.toLowerCase() === branchName.toLowerCase(); + }); + 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) { + 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.Confirmation) { + if (!user.accepted || !user.applied) { + response.redirect("/apply"); + return; + } + if (user.attending && !user.confirmationBranch) { + 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 (request.params.branch) { + 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()) { + 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 (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"; + response.redirect(`/${redirPath}/${uriBranch}`); + 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; - 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/settings.ts b/server/routes/api/settings.ts index 66c239f4..4e3a4fc3 100644 --- a/server/routes/api/settings.ts +++ b/server/routes/api/settings.ts @@ -150,13 +150,16 @@ settingsRoutes.route("/branch_roles") } // Set open/close times (if not noop) - if (branch instanceof Branches.ApplicationBranch || branch instanceof Branches.ConfirmationBranch) { + if (branch instanceof Branches.TimedBranch) { branch.open = branchData.open ? new Date(branchData.open) : new Date(); branch.close = branchData.close ? new Date(branchData.close) : new Date(); } // 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; } // Set rolling deadline flag (if confirmation branch) if (branch instanceof Branches.ConfirmationBranch) { diff --git a/server/routes/api/user.ts b/server/routes/api/user.ts index 9811c849..6e26da77 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,242 +36,300 @@ 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; - } - - 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; + }); + 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; - } -}; + let user = request.user as IUserMongoose; -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); + 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; + } + } -async function postApplicationBranchHandler(request: express.Request, response: express.Response): Promise { - let requestType: ApplicationType = request.url.match(/\/application\//) ? ApplicationType.Application : ApplicationType.Confirmation; + if (moment().isBetween(openDate, closeDate) || request.user.isAdmin) { + next(); + } + else { + response.status(400).json({ + "error": `${requestType === ApplicationType.Application ? "Applications" : "Confirmations"} are currently closed` + }); + return; + } + }; +} - 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()) { - response.status(400).json({ - "error": "You can only edit the confirmation branch that you originally submitted" - }); - return; - } +registrationRoutes.route("/:branch").post( + isAdmin, + applicationTimeRestriction(ApplicationType.Application), + postParser, + uploadHandler.any(), + postApplicationBranchErrorHandler, + postApplicationBranchHandler(true) +); + +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 { + 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 + }) as IUserMongoose; + } else { + user = await User.findOne({uuid: request.params.uuid}) as IUserMongoose; + } - // 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; - 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; + // 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 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 - }; + + 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 { - errored = true; + } else if (questionBranch instanceof Branches.ConfirmationBranch) { + if (user.attending && branchName.toLowerCase() !== user.confirmationBranch.toLowerCase()) { response.status(400).json({ - "error": `'${question.label}' is a required field` + "error": "You can only edit the confirmation branch that you originally submitted" }); - return null; + return; } + } else { + response.status(400).json({ + "error": "Invalid application branch" + }); + return; } - 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] = []; - } - if (!Array.isArray(request.body[question.name])) { - request.body[question.name] = [request.body[question.name]]; + + 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; } - // 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); + 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 { - return Promise.resolve(); + errored = true; + response.status(400).json({ + "error": `'${question.label}' is a required field` + }); + return null; } - }) - ); - // 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; + 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] = []; + } + if (!Array.isArray(request.body[question.name])) { + request.body[question.name] = [request.body[question.name]]; + } + // 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) + }; }); - // 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 { - 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 (requestType === ApplicationType.Application) { - 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); - } - else if (requestType === ApplicationType.Confirmation) { - if (!user.attending) { - await sendMailAsync({ - from: config.email.from, - to: user.email, - subject: emailSubject || defaultEmailSubjects.attend, - html: emailHTML, - text: emailText - }); + catch { + emailSubject = null; + } + 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; 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"); @@ -333,6 +393,18 @@ 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; + } + + // 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); diff --git a/server/routes/templates.ts b/server/routes/templates.ts index f22ecee8..372fb566 100644 --- a/server/routes/templates.ts +++ b/server/routes/templates.ts @@ -4,20 +4,22 @@ 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, config, getSetting, renderMarkdown } from "../common"; import { - authenticateWithRedirect, - timeLimited, ApplicationType + authenticateWithRedirect, isAdmin, + 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"; @@ -120,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("/")); @@ -127,8 +130,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 +212,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 +219,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: { @@ -311,30 +316,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) { @@ -366,13 +364,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, @@ -392,135 +383,142 @@ function applicationHandler(requestType: ApplicationType): (request: express.Req }; } -templateRoutes.route("/apply/:branch").get(authenticateWithRedirect, timeLimited, applicationBranchHandler); -templateRoutes.route("/confirm/:branch").get(authenticateWithRedirect, timeLimited, applicationBranchHandler); - -async function applicationBranchHandler(request: express.Request, response: express.Response) { - let requestType: ApplicationType = request.url.match(/^\/apply/) ? ApplicationType.Application : ApplicationType.Confirmation; +templateRoutes.route("/register/:branch").get( + isAdmin, + onlyAllowAnonymousBranch, + timeLimited, + applicationBranchHandler(ApplicationType.Application, true) +); - let user = request.user as IUser; +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) +); - // 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; - } +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 (anonymous) { + user = new User({ + uuid: uuid(), + email: "" + }); + } else { + 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; - } + let branchName = request.params.branch as string; - // 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())!; - let questionBranches = await Branches.BranchConfig.loadAllBranches(); + // 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); + } - 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); - 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 (!anonymous) { + 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: anonymous, + 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) => { @@ -583,6 +581,9 @@ templateRoutes.route("/admin").get(authenticateWithRedirect, async (request, res name: branch.name, open: branch.open.toISOString(), close: branch.close.toISOString(), + allowAnonymous: branch.allowAnonymous, + autoAccept: branch.autoAccept, + noConfirmation: branch.noConfirmation, confirmationBranches: branch.confirmationBranches }; }), diff --git a/server/schema.ts b/server/schema.ts index 65f74cc4..e1619641 100644 --- a/server/schema.ts +++ b/server/schema.ts @@ -231,6 +231,9 @@ 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 usesRollingDeadline?: boolean; // Used by confirmation branch } @@ -253,6 +256,9 @@ export const QuestionBranchConfig = mongoose.model