Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Anonymous/walk-in registration #205

Merged
merged 21 commits into from
Mar 10, 2018
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c3df883
Remove request url matching and use types instead
bunsenmcdubbs Mar 5, 2018
d68a23f
Implement auto-accept for after application submitted
bunsenmcdubbs Mar 5, 2018
1667838
Implement no-confirmation workflow after applicant is accepted
bunsenmcdubbs Mar 5, 2018
f9b0461
Start refactor of branch (apply/confirm) redirect middleware
bunsenmcdubbs Mar 6, 2018
ed73b2c
Hide and display proper options for users depending on new branch opt…
bunsenmcdubbs Mar 6, 2018
720cc2d
Clear user `accepted` and `attending` values when deleting application
bunsenmcdubbs Mar 6, 2018
1fdbd60
Add auto-accept and skip-confirmation options in admin panel
bunsenmcdubbs Mar 6, 2018
5bdd02f
Add "allow anonymous" option to admin panel (and db)
bunsenmcdubbs Mar 6, 2018
d134f70
Refactor branch redirection middleware
bunsenmcdubbs Mar 6, 2018
b676ed5
Use `addEventListener` when setting click handlers
bunsenmcdubbs Mar 7, 2018
92107a0
Implement anonymous registration
bunsenmcdubbs Mar 7, 2018
d13d6db
Display registration link in admin panel for public branches
bunsenmcdubbs Mar 7, 2018
3963457
Clarify location of config file
bunsenmcdubbs Mar 7, 2018
6462123
Encode URLs in admin screen
bunsenmcdubbs Mar 8, 2018
a0669b1
Protect walkin/anonymous registration by enforcing admin user
bunsenmcdubbs Mar 9, 2018
737bd47
Uncheck allow-anonymous open when skip confirmation is disabled
bunsenmcdubbs Mar 9, 2018
224668c
Fix typos
bunsenmcdubbs Mar 9, 2018
04dc661
Prevent anonymous registration with existing email
bunsenmcdubbs Mar 9, 2018
2ae1708
Small fixes from PR feedback
bunsenmcdubbs Mar 9, 2018
9fdce67
Small type cast fix
petschekr Mar 10, 2018
06699a2
Bump version to 1.13.0
petschekr Mar 10, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
35 changes: 29 additions & 6 deletions client/admin.html
Original file line number Diff line number Diff line change
Expand Up @@ -243,13 +243,28 @@ <h4>{{this.name}}</h4>
<option value="Application" selected>Application</option>
<option value="Confirmation">Confirmation</option>
</select>
<label>Options</label>
<fieldset class="applicationBranchOptions">
<div>
<input type="checkbox" id="{{removeSpaces this.name}}-allow-anonymous" class="allowAnonymous" data-branch-name="{{this.name}}" {{#if this.allowAnonymous}}checked{{/if}} />
<label for="{{removeSpaces this.name}}-allow-anonymous">Allow anonymous submissions</label>
</div>
<div>
<input type="checkbox" id="{{removeSpaces this.name}}-auto-accept" class="autoAccept" {{#if this.autoAccept}}checked{{/if}} />
<label for="{{removeSpaces this.name}}-auto-accept">Auto-accept applicants</label>
</div>
<div>
<input type="checkbox" id="{{removeSpaces this.name}}-no-confirmation" class="noConfirmation" data-branch-name="{{this.name}}" {{#if this.noConfirmation}}checked{{/if}} />
<label for="{{removeSpaces this.name}}-no-confirmation">Skip Confirmation</label>
</div>
</fieldset>
<label>Available confirmation branches</label>
<fieldset class="availableConfirmationBranches">
{{#each ../settings.branches.confirmation}}
<div>
<input type="checkbox" id="{{removeSpaces ../this.name}}-to-{{removeSpaces this.name}}" data-confirmation="{{this.name}}" {{branchChecked ../this.confirmationBranches this.name}} />
<label for="{{removeSpaces ../this.name}}-to-{{removeSpaces this.name}}">{{this.name}}</label>
</div>
<div>
<input type="checkbox" id="{{removeSpaces ../this.name}}-to-{{removeSpaces this.name}}" data-confirmation="{{this.name}}" data-branch-name="{{../this.name}}"{{branchChecked ../this.confirmationBranches this.name}} />
<label for="{{removeSpaces ../this.name}}-to-{{removeSpaces this.name}}">{{this.name}}</label>
</div>
{{/each}}
</fieldset>
<label>Open/Close Times</label>
Expand All @@ -259,6 +274,12 @@ <h4>{{this.name}}</h4>
<label for="{{removeSpaces this.name}}-close">Close</label>
<input type="datetime-local" id="{{removeSpaces this.name}}-close" class="closeTime" data-raw-value="{{this.close}}"/>
</fieldset>
<div class="public-link" {{#unless this.allowAnonymous}}hidden{{/unless}}>
<label>Public Link</label>
<fieldset>
<pre><a href="/register/{{encodeURI this.name}}">/register/{{encodeURI this.name}}</a></pre>
</fieldset>
</div>
</div>
{{/each}}
{{#each settings.branches.confirmation}}
Expand Down Expand Up @@ -298,8 +319,10 @@ <h4>Edit email content</h4>
<select id="email-type">
<optgroup label="Application branches">
{{#each settings.branches.application}}
<option value="{{this.name}}-apply">{{this.name}} Post-Apply Email</option>
<option value="{{this.name}}-accept">{{this.name}} Accepted Email</option>
<option value="{{this.name}}-apply">{{this.name}} Post-Apply Email</option>
{{#unless this.autoAccept}}
<option value="{{this.name}}-accept">{{this.name}} Accepted Email</option>
{{/unless}}
{{/each}}
</optgroup>
<optgroup label="Confirmation branches">
Expand Down
16 changes: 16 additions & 0 deletions client/application.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,14 @@ <h1 class="center">Apply: {{branch}}</h1>
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}}
<form method="post" data-action="/api/user/{{user.uuid}}/application/{{slug branch}}">
{{else}}
<form method="post" data-action="/api/registration/{{slug branch}}">
<label for="anonymous-registration-email" class="required">Email</label>
<input type="email" name="anonymous-registration-email" id="anonymous-registration-email" required/>
{{/unless}}

{{#each questionData}}
{{#if this.textContent}}
{{{this.textContent}}}
Expand Down Expand Up @@ -76,14 +83,23 @@ <h1 class="center">Apply: {{branch}}</h1>
{{/each}}
{{{endText}}}
<div class="center">
{{#unless unauthenticated}}
{{#if user.applied}}
<input type="submit" value="Update" />
<button id="delete" style="background-color: #FF4136;">Delete</button>
{{else}}
<input type="submit" value="Submit" />
{{/if}}
{{else}}
<input type="submit" value="Submit" />
{{/unless}}

</div>
</form>
{{/sidebar}}
<script type="text/javascript">
formTypeString = "Application";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WTF is this a global variable?? Remove the type attribute from the <script> tag. If you need a global variable, do window.myVariable = x instead so that it will work in strict mode.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adding it to window won't work because typescript doesn't recognize additional properties on window

unauthenticated = {{unauthenticated}}
</script>
</body>
</html>
3 changes: 3 additions & 0 deletions client/confirmation.html
Original file line number Diff line number Diff line change
Expand Up @@ -85,5 +85,8 @@ <h1 class="center">RSVP: {{branch}}</h1>
</div>
</form>
{{/sidebar}}
<script type="text/javascript">
formTypeString = "Confirmation";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Globals again

</script>
</body>
</html>
2 changes: 2 additions & 0 deletions client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,15 @@ <h4>Incomplete</h4>
{{#if user.attending}}
<p>You're all set!</p>
<p>Application type: <b>{{user.applicationBranch}}</b></p>
{{#unless skipConfirmation}}
<p>Confirmation type: <b>{{user.confirmationBranch}}</b></p>
{{#if confirmationStatus.areOpen}}
<p>Feel free to edit your RSVP at any time. However, once RSVPing closes on {{confirmationClose}}, you will not be able to edit it anymore.</p>
<a class="btn" href="/confirm">Edit your confirmation</a>
{{else}}
<p>RSVPing closed on {{confirmationClose}}.</p>
{{/if}}
{{/unless}}
<p>We look forward to seeing you!</p>

{{#if settings.qrEnabled}}
Expand Down
82 changes: 77 additions & 5 deletions client/js/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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"]`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Types will work better here if it's let checkboxes: NodeListOf<HTMLInputElement> = x. Then you don't need the ugly cast on line 652.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typescript won't allow me to cast NodeListOf<Element> to NodeListOf<HTMLInputElement>

for (let input of Array.from(checkboxes)) {
(input as HTMLInputElement).checked = false;
}
}
let skipConfirmationToggles = document.querySelectorAll(".branch-role input[type=\"checkbox\"].noConfirmation");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing with types

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing with how its not possible.

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\"]");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better typing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing with how its not possible.

for (let input of Array.from(availableConfirmationBranchCheckboxes)) {
let checkbox = input as HTMLInputElement;
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");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better typing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing with how its not possible.

for (let input of Array.from(allowAnonymousCheckboxes)) {
let checkbox = input as HTMLInputElement;
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));
Expand Down Expand Up @@ -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") {
Expand All @@ -702,6 +763,17 @@ 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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you make fieldset.applicationBranchOptions input[type="checkbox"] a variable or something somewhere? Seems like it's getting repeated a lot. Or consider running a querySelector() on that element up front and querySelector() on the returned element for better performance and readability

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;
}
}
if (branchRole === "Confirmation") {
let usesRollingDeadlineCheckbox = (branchRoles[i].querySelectorAll("input.usesRollingDeadline") as NodeListOf<HTMLInputElement>);
Expand Down
14 changes: 11 additions & 3 deletions client/js/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ enum FormType {
Application,
Confirmation
}
let formType = window.location.pathname.match(/^\/apply/) ? FormType.Application : FormType.Confirmation;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whyyyyyyyyy globals. There has to be a better way

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed this crime. I literally removed this global.

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;
Expand All @@ -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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cast not needed here. Typescript can tell that searching for "form" will always return a form element

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is that the type returned is HTMLFormElement | null and I want to force it to be not null.

} else {
window.location.assign("/");
}
}).catch(async (err: Error) => {
await sweetAlert("Oh no!", err.message, "error");
submitButton.disabled = false;
Expand Down
21 changes: 15 additions & 6 deletions client/partials/sidebar.html
Original file line number Diff line number Diff line change
@@ -1,31 +1,40 @@
<div class="container">
<aside id="sidebar">
<h1>{{siteTitle}}</h1>
{{#unless unauthenticated}}
<nav id="sidebar-nav">
<a href="/">Dashboard</a>
<span class="divider"></span>

{{#if user.accepted}}
<a href="/confirm">Confirmation</a>
<span class="divider"></span>
{{#if user.attending}}
{{#if user.confirmation}}
<a href="/confirm">Confirmation</a>
{{else}}
<a href="/apply">Application</a>
{{/if}}
{{else}}
<a href="/confirm">Confirmation</a>
{{/if}}
{{else}}
<a href="/apply">Application</a>
<span class="divider"></span>
{{/if}}

<span class="divider"></span>

{{#if settings.teamsEnabled}}
<a href="/team">Team</a>
<span class="divider"></span>
{{/if}}

{{#if user.admin}}
<a href="/admin">Admin</a>
<span class="divider"></span>
{{/if}}

<a href="/auth/logout">Log out</a>
</nav>
<p>{{user.name}} ({{user.email}})</p>
{{/unless}}
</aside>
<main class="col-9">
{{> @partial-block }}
Expand Down
3 changes: 2 additions & 1 deletion server/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
13 changes: 12 additions & 1 deletion server/branch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ export class NoopBranch {
}
}

abstract class TimedBranch extends NoopBranch {
export abstract class TimedBranch extends NoopBranch {
public open: Date;
public close: Date;

Expand All @@ -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<void> {
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
};
}
Expand Down