diff --git a/.travis.d/Gemfile b/.travis.d/Gemfile deleted file mode 100644 index 5a1ec47e..00000000 --- a/.travis.d/Gemfile +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true -source 'https://rubygems.org' - -gem 'git', '~> 1.3' -gem 'octokit', '~> 4.0' diff --git a/.travis.d/build.sh b/.travis.d/build.sh deleted file mode 100755 index f9f03802..00000000 --- a/.travis.d/build.sh +++ /dev/null @@ -1,219 +0,0 @@ -#!/usr/bin/env bash -# HACKGPROJECT VERSION: a4089e26eea6f51517a0e00330e1296bb05cfef4 -set -euo pipefail -PROJECT_TYPE="deployment" -ORG_NAME_CASE_PRESERVE="HackGT" -ORG_NAME=$(echo "${ORG_NAME_CASE_PRESERVE}" | tr '[:upper:]' '[:lower:]') -SOURCE_DIR=$(readlink -f "${BASH_SOURCE[0]}") -SOURCE_DIR=$(dirname "$SOURCE_DIR") -cd "${SOURCE_DIR}/.." -set -x - -if ! hash docker &>/dev/null; then - echo 'Cannot find the docker binary!' >&2 - exit 64 -fi - -docker= -if docker ps &>/dev/null; then - docker=docker -else - docker='sudo docker' -fi - -remote=$(git remote -v | grep -Pio "${ORG_NAME}"'/[a-zA-Z0-9-_\.]*' | head -1) -image_name=$(basename "${remote%.*}") -image_name=$(echo "$image_name" | tr '[:upper:]' '[:lower:]') - -build_project_source() { - if [[ -f Dockerfile.build ]]; then - local build_image_name - build_image_name="$(basename "$(pwd)")-build" - $docker build -f Dockerfile.build --rm -t "$build_image_name" . - $docker run -w '/src' -v "$(pwd):/src" "$build_image_name" - sudo chown -R "$(id -u):$(id -g)" . - fi -} - -test_project_source() { - if [[ -f Dockerfile.test ]]; then - local test_image_name - test_image_name="$(basename "$(pwd)")-test" - $docker build -f Dockerfile.test --rm -t "$test_image_name" . - $docker run -w '/src' -v "$(pwd):/src" "$test_image_name" - sudo chown -R "$(id -u):$(id -g)" . - fi -} - -build_project_container() { - $docker build -f Dockerfile --rm -t "$image_name" . -} - -git_branch() { - if [[ ${TRAVIS_PULL_REQUEST_BRANCH} ]]; then - echo "${TRAVIS_PULL_REQUEST_BRANCH}" - else - echo "${TRAVIS_BRANCH:-$(git rev-parse --abbrev-ref HEAD)}" - fi -} - -git_branch_id() { - git_branch | sed 's/[^0-9a-zA-Z_.-]/-/g' -} - -publish_project_container() { - local git_rev - local branch - git_rev=$(git rev-parse HEAD) - branch=$(git_branch_id) - local latest_tag_name="latest" - local push_image_name="${DOCKER_ID_USER}/${image_name}" - if [[ $branch != master ]]; then - latest_tag_name="latest-${branch}" - fi - docker login -u="${DOCKER_ID_USER}" -p="${DOCKER_PASSWORD}" - docker tag "$image_name" "$push_image_name":"$git_rev" - docker push "$push_image_name" - docker tag "$push_image_name":"$git_rev" "$push_image_name":"$latest_tag_name" - docker push "$push_image_name" -} - -trigger_biodomes_build() { - body='{ - "request": { - "branch":"master" - } }' - - curl -s -X POST \ - -H "Content-Type: application/json" \ - -H "Accept: application/json" \ - -H "Travis-API-Version: 3" \ - -H "Authorization: token ${TRAVIS_TOKEN}" \ - -d "$body" \ - https://api.travis-ci.org/repo/${ORG_NAME_CASE_PRESERVE}%2Fbiodomes/requests -} - -commit_to_branch() { - local branch - local git_rev - branch="${1:-gh-pages}" - git_rev=$(git rev-parse --short HEAD) - git config user.name 'HackGBot' - git config user.email 'thehackgt@gmail.com' - git remote remove origin - git remote add origin \ - "https://${GH_TOKEN}@github.com/${ORG_NAME}/${image_name}.git" - git fetch origin - git reset "origin/$branch" || git checkout -b "$branch" - git add -A . - git status - git commit -m "Automatic Travis deploy of ${git_rev}." - git push -q origin "HEAD:${branch}" -} - -set_cloudflare_dns() { - local type="$1" - local name="$2" - local content="$3" - local proxied="$4" - local type_downcase - local name_downcase - local content_downcase - local dns_records - type_downcase=$(echo "${type}" | tr '[:upper:]' '[:lower:]') - name_downcase=$(echo "${name}" | tr '[:upper:]' '[:lower:]') - content_downcase=$(echo "${content}" | tr '[:upper:]' '[:lower:]') - - # get all the dns records - dns_records=$(curl -X GET \ - -H "X-Auth-Email: ${CLOUDFLARE_EMAIL}" \ - -H "X-Auth-Key: ${CLOUDFLARE_AUTH}" \ - -H "Content-Type: application/json" \ - "https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE}/dns_records" \ - | tr '[:upper:]' '[:lower:]') - - # Check if we already set it - local jq_exists - jq_exists=$(cat <<-END - .result[] - | select(.type == "${type_downcase}") - | select(.name == "${name_downcase}") - | select(.content == "${content_downcase}") -END - ) - if [[ -n $(echo "${dns_records}" | jq "${jq_exists}") ]]; then - echo "Record already set, not setting again." - return - fi - - # Check if there's a different one already set - local duplicate_exists - duplicate_exists=$(echo "${dns_records}" \ - | jq '.result[] | select(.name == '"${name_downcase}"')') - if [[ -n $duplicate_exists ]]; then - echo "Record with the same host exists, will not overwrite!" - exit 64 - fi - - # Set IT! - local dns_record - dns_record=$(cat <<-END - { - "type": "${type}", - "name": "${name}", - "content": "${content}", - "proxied": $proxied - } -END - ) - local dns_success - dns_success=$(curl -X POST \ - --data "$dns_record" \ - -H "X-Auth-Email: ${CLOUDFLARE_EMAIL}" \ - -H "X-Auth-Key: ${CLOUDFLARE_AUTH}" \ - -H "Content-Type: application/json" \ - "https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE}/dns_records") - - if [[ $dns_success != true ]]; then - echo 'DNS Setting on cloudflare failed!!' - echo 'CloudFlare output:' - echo "$dns_success" - exit 64 - fi - echo DNS set! You\'ll have to wait a bit to see the changes! -} - - -deployment_project() { - build_project_container - - if [[ ${TRAVIS_PULL_REQUEST:-} = false ]]; then - publish_project_container - trigger_biodomes_build - fi -} - -static_project() { - if [[ ${TRAVIS_BRANCH:-} = master && ${TRAVIS_PULL_REQUEST:-} = false ]]; then - commit_to_branch 'gh-pages' - set_cloudflare_dns CNAME "$(cat CNAME)" "${ORG_NAME}.github.io" true - fi -} - - -# Build & Test the project, if needed. -build_project_source -test_project_source - -case "$PROJECT_TYPE" in - deployment) - deployment_project - ;; - static) - static_project - ;; - *) - echo "Unknown project type!" - exit 1 -esac - diff --git a/.travis.d/pr_autodeploy.rb b/.travis.d/pr_autodeploy.rb deleted file mode 100644 index 736ee987..00000000 --- a/.travis.d/pr_autodeploy.rb +++ /dev/null @@ -1,135 +0,0 @@ -# frozen_string_literal: true -require 'git' -require 'octokit' - -TR_PR_BRANCH = ENV['TRAVIS_PULL_REQUEST_BRANCH'] -GH_TOKEN = ENV['GH_TOKEN'] -ORG_NAME = ENV['ORG_NAME'] || 'HackGT' -GIT_NAME = ENV['GIT_NAME'] || 'HackGBot' -GIT_EMAIL = ENV['GIT_EMAIL'] || 'thehackgt@gmail.com' - -def commit_to_biodomes - path = Dir.mktmpdir - git = Git.clone("https://github.com/#{ORG_NAME}/biodomes.git", - 'biodomes', - path: path, - depth: 1) - # Configure git - git.config('user.name', GIT_NAME) - git.config('user.email', GIT_EMAIL) - # Change origin - git.remote('origin').remove - git.add_remote('origin', - "https://#{GH_TOKEN}@github.com/#{ORG_NAME}/biodomes.git", - fetch: false) - - # do whatever work you want - message = nil - Dir.chdir(File.join(path, 'biodomes')) { || message = yield } - - # commit & push - git.add(all: true) - begin - git.commit(message) - rescue - puts 'Nothing to commit, skipping...' - return - end - git.push -end - -def git_branch - return TR_PR_BRANCH unless TR_PR_BRANCH.nil? - return ENV['TRAVIS_BRANCH'] unless ENV['TRAVIS_BRANCH'].nil? - `git rev-parse --abbrev-ref HEAD`.strip -end - -def git_remote - remotes = `git remote -v`.match %r{#{ORG_NAME}/(.*?)\.git }i - remotes[1] -end - -def git_branch_id(branch) - branch.gsub(/[^0-9a-zA-Z_-]/, '-') -end - -def pr_id(branch) - "#{git_remote}-#{git_branch_id branch}" -end - -def create_biodome_file(branch) - remote = git_remote - data = <<~EOF - git: - remote: "https://github.com/#{ORG_NAME}/#{remote}.git" - branch: "#{branch}" - - secrets-source: git-#{ORG_NAME}-#{remote}-secrets - deployment: - replicas: 1 - strategy: - type: Recreate - EOF - ["pr/#{pr_id branch}.yaml", data.downcase] -end - -def create_message(branch) - <<~EOF - Hey y'all! A deployment of this PR can be found here: - https://#{pr_id branch}.pr.hack.gt - EOF -end - -def pr_digest(github, slug) - github.pulls(slug) - .select { |p| p.state == 'open' } - .map do |p| - { - branch: p.head.ref, - number: p.number - } - end - .uniq -end - -def main - github = Octokit::Client.new(access_token: GH_TOKEN) - - remote = git_remote - slug = "#{ORG_NAME}/#{remote}" - - digest = pr_digest(github, slug) - open_branches = digest.map { |pr| pr[:branch] } - files = open_branches.map { |branch| create_biodome_file(branch) } - - # commit all the right files to biodomes - commit_to_biodomes do - FileUtils.rm_rf(Dir["./pr/#{remote}-*"]) - files.each do |(path, data)| - FileUtils.mkdir_p(File.dirname(path)) - File.write(path, data) - end - - puts `git status` - puts `git diff` - "Automatic #{remote} PR deploys of #{open_branches.join(', ')}." - end - - # Check if this is part of a PR build - current_branch = git_branch - current_pr = digest.find { |pr| pr[:branch] == current_branch } - return if current_pr.nil? - - # Check if a message has already been written - message = create_message current_branch - comment_written = - github - .issue_comments(slug, current_pr[:number]) - .find { |comment| comment.body.gsub(/\s+/, '') == message.gsub(/\s+/, '') } - return unless comment_written.nil? - - # Write a message - github.add_comment(slug, current_pr[:number], message) -end - -main diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 751ae1f0..00000000 --- a/.travis.yml +++ /dev/null @@ -1,18 +0,0 @@ -dist: trusty -language: ruby -sudo: required -services: -- docker -script: | - .travis.d/build.sh && - cd .travis.d && bundle install --path vendor/bundle && - bundle exec ruby pr_autodeploy.rb -env: - global: - - HACKGPROJECT_REV="a4089e26eea6f51517a0e00330e1296bb05cfef4" - - DOCKER_ID_USER="hackgt" -notifications: - slack: - secure: pjf6fz9v2uA9HZwSrdnZLQjnmwW3uiVHP6aviKzqmmAMslm9Zx+gUH9S4f3S3pK099neKXvJeoVZXLHUeXAxsoZVsPPvAEIO8U4FzVx1ZiU1FCjaXXM56n3Iw1UmPXx52FcG2Yh+KDgcdLTYoBNidWaC80dqEIi+HbZ0XJQg1RLEKm+c0TLpP6JfLQsfNlS768jBnyOoo0/qyCQzc/w/Xbmm+Zb2fxxC50AncjNDqK1okYXxMKMlHmWhNAx3J+3dB89OdV6vWLYibCCdOc/eQV73qtgpF02tt1TeYtIeapFegdPlzg5hHwQ347OujnzKcdbUsCgEVK+AzhBvdu1DP+UPPD2TFyptD+snCjdDVytbPIv+OgXjC9x4h8mrA7Q6N+OZMd7bVOV6jiHmcabYd47hrr3Peg8D56LcQ+YzkdWIbzEjoqFT1NJ+wIK5cv+a9QEVW1e0DK5EsRTkC5N/cZdyZvoFp3ob0js8ak8r6GnWLcN20rz9VsVICrbonZ60puFCjw9r1PGpP6GwccJMarhzIxQ+OxAsiQb00pNE+VMhM/6MGiwNxGRtDHmkC6eNv/AZDt7jYtrXnPnXyPJAUa0NDydZTVXJpdICW703U5BfX3fZTqQs7iD9jEDlJ4lL4up7F7Uq2B4q9gJ6dr9LLVkZb6w+kh5eHJWjvTBPtNY= - on_success: change - on_failure: always diff --git a/Dockerfile b/Dockerfile index 7099a8cf..0b62e6b4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,9 @@ -FROM node:8-alpine +FROM node:12-alpine MAINTAINER Ryan Petschek # Deis wants bash RUN apk update && apk add bash RUN apk add git -# Install latest npm version (in case Node.js hasn't updated with the newest version yet) -# npm install -g npm@latest doesn't work -> see https://github.com/npm/npm/issues/15611#issuecomment-289133810 for this hack -RUN npm install npm@"~5.4.0" && rm -rf /usr/local/lib/node_modules && mv node_modules /usr/local/lib # Bundle app source WORKDIR /usr/src/registration @@ -17,6 +14,7 @@ RUN npm run build # Set Timezone to EST RUN apk add tzdata ENV TZ="/usr/share/zoneinfo/America/New_York" +ENV NODE_ENV="production" # Deis wants EXPOSE and CMD EXPOSE 3000 diff --git a/LICENSE b/LICENSE index 3df23cf0..2a71b63b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017 HackGT +Copyright (c) 2018 HackGT Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index dcd486b5..0f344ee7 100644 --- a/README.md +++ b/README.md @@ -9,15 +9,15 @@ ## Features -- Seamless OAuth and local signup logins with automatic email verification - - Get users up and running quickly with GitHub, Google, and Facebook OAuth logins right out of the box ([MyMLH](https://my.mlh.io) login planned) - - Full support for local logins as well if users choose +- Seamless OAuth and local signup logins with automatic email verification via [HackGT Ground Truth](https://github.com/HackGT/ground-truth) + - Get users up and running quickly with GitHub, Google, and Facebook OAuth or Georgia Tech CAS logins right out of the box + - Full support for local logins as well if users prefer - Users can easily register (and confirm their attendance if accepted) and choose which "branch" they want to complete (e.g. partipant, mentor, volunteer) all from a single location - Users can create or join a team any time before or after completing registration. Admins can configure the maximum team size (defaults to 4). - For admins, the admin panel contains options for managing all aspects of registration including: - Statistics about the user of sign ups, registrations, acceptances, and confirmations - Graphs displaying aggregated registration data - - List of all users in a table including name, email, email verified status, admin status, and application status, and log in method + - List of all users in a table including name, email, admin status, and application status, and log in method - List of all applicants with application responses and accept / unaccept button sortable by application branch and accepted status - Acceptance emails are sent out only when a send acceptance emails button is clicked allowing for decisions to be reviewed before being finalized - Setting application and confirmation open and close times as well as what question branches from `questions.json` are for applications, confirmations, or hidden @@ -58,78 +58,47 @@ 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). - -### OAuth IDs and secrets - -Can be obtained from: -- [GitHub](https://github.com/settings/developers) - - Register a new application - - Application name, URL, and description are up to you - - Callback URL should be in the format: `https://YOUR_DOMAIN/auth/github/callback` - - Local testing callback URL should be `http://localhost:3000/auth/github/callback` - - GitHub only lets you register one callback URL per application so you might want to make a testing application and a separate application for production usage. -- [Google API Console](https://console.developers.google.com/apis/credentials) - - Create an application - - Go to the credentials tab in the left panel - - Click Create credentials > OAuth client ID - - Set web application as the application type - - Give it a name (won't be shown publically, e.g. `HackGT Registration server testing`) - - Leave Authorized JavaScript origins blank - - List all testing / production callback URLs in Authorized redirect URIs - - Should be in the format: `https://YOUR_DOMAIN/auth/google/callback` - - For local testing: `http://localhost:3000/auth/google/callback` - - It is recommended that you create two OAuth applications with different IDs and secrets for testing and production usage. -- [Facebook](https://developers.facebook.com/) - - Create an application - - Add the Facebook Login product from the left panel - - Enable Client OAuth Login, Web OAuth Login, and Embedded Browser OAuth Login - - List all testing / production callback URLs in Valid OAuth redirect URLs - - Should be in the format: `https://YOUR_DOMAIN/auth/facebook/callback` - - For local testing: `http://localhost:3000/auth/facebook/callback` - - Optionally, repeat the process for separate testing and production applications +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). Environment Variable | Description ---------------------|------------ -PRODUCTION | Set to `true` to set OAuth callbacks to production URLs (default: `false`) +PRODUCTION | Set to `true` to enable reverse proxy trusting (default: `false`) PORT | The port the check in system should run on (default: `3000`) -MONGO_URL | The URL to the MongoDB server (default: `mongodb://localhost/`) -UNIQUE_APP_ID | The MongoDB database name to store data in (default: `registration`) +MONGO_URL | The URL to the MongoDB server (default: `mongodb://localhost/registration`) VERSION_HASH | The Git short hash used to identify the current commit (default: parsed automatically from the `.git` folder, if it exists) -*SOURCE_VERSION* | Same as `VERSION_HASH` but overrides it if present. Used by Deis. -*WORKFLOW_RELEASE_CREATED_AT* | Provided by Deis (default: `null`) -*WORKFLOW_RELEASE_SUMMARY* | Provided by Deis (default: `null`) +ADMIN_KEY_SECRET | An API key used to authenticate as admin an access the GraphQL api (default: random key that changes every server restart) COOKIE_MAX_AGE | The `maxAge` of cookies set in milliseconds (default: 6 months) **NOTE: this is different from the session TTL** COOKIE_SECURE_ONLY | Whether session cookies should sent exclusively over secure connections (default: `false`) PASSWORD_RESET_EXPIRATION | The time that password reset links sent via email should be valid for in milliseconds (default: 1 hour) SESSION_SECRET | The secret used to sign and validate session cookies (default: random 32 bytes regenerated on every start up) -GITHUB_CLIENT_ID | OAuth client ID for GitHub *required* -GITHUB_CLIENT_SECRET | OAuth client secret for GitHub *required* -GOOGLE_CLIENT_ID | OAuth client ID for Google *required* -GOOGLE_CLIENT_SECRET | OAuth client secret for Google *required* -FACEBOOK_CLIENT_ID | OAuth client ID for Facebook *required* -FACEBOOK_CLIENT_SECRET | OAuth client secret for Facebook *required* +GROUND_TRUTH_URL | Base URL of [Ground Truth](https://github.com/HackGT/ground-truth) instance (e.g. `https://login.hack.gt`) *required* +GROUND_TRUTH_ID | OAuth client ID from Ground Truth *required* +GROUND_TRUTH_SECRET | OAuth client secret from Ground Truth *required* EMAIL_FROM | The `From` header for sent emails (default: `HackGT Team `) -EMAIL_HOST | The SMTP email server's hostname (default: *none*) -EMAIL_PORT | The SMTP email server's port (default: `465`) -EMAIL_USERNAME | The username for the SMTP email server (default: *none*) -EMAIL_PASSWORD | The password for the SMTP email server (default: *none*) +EMAIL_KEY | The SendGrid API key for sending emails (default: *none*) *required* ADMIN_EMAILS | A JSON array of the emails of the users that you want promoted to admin status when they create their account (default: none) EVENT_NAME | The current event's name which affects rendered templates and sent emails (default: `Untitled Event`) STORAGE_ENGINE | The name of the storage engine that handles file uploads as defined in [storage.ts](server/storage.ts) (default: `disk`) STORAGE_ENGINE_OPTIONS | JSON-encoded object containing options to be passed to the storage engine. Must at least contain a value for the `uploadDirectory` key. For the default `disk` storage engine, this directory is relative to the app's root, can be absolute, and will be created if it doesn't exist. (default: `{ "uploadDirectory": "uploads" }`) +DEFAULT_TIMEZONE | Timezone used for dates and times (default: `America/New_York`) MAX_TEAM_SIZE | The maximum number of users allowed per team (default: `4`) -QUESTIONS_FILE | Specify a path for the `questions.json` file. +QUESTIONS_FILE | Specify a path for the `questions.json` file. (default: ./server/config/questions.json) THEME_FILE | Specify a path for the `theme.css` file, which will be loaded last at every page. FAVICON_FILE | Path to the favicon file (default is no favicon). FAVICON_FILE_BASE64 | Same as `FAVICON_FILE_BASE64` but the file is base64 encoded. +HELPSCOUT_INTEGRATION_ENABLED | Whether to enable the backend API endpoint that can provide a dynamic app for Help Scout (default: false) +HELPSCOUT_INTEGRATION_SECRET_KEY | A random, 40-character long string of letters, numbers, and symbols. Must also be entered in Help Scout; see the Setting Up Help Scout integration section for more information. (required if Help Scout integration is enabled) +HELPSCOUT_BEACON_ENABLED | Whether to show the Help Scout Beacon on certain frontend pages (default: false) +HELPSCOUT_BEACON_ID | Unique ID for the Beacon provided by Help Scout (required if Beacon functionality is enabled) +HELPSCOUT_BEACON_SUPPORT_HISTORY_SECRET_KEY | Secret key provided by Help Scout for the Beacon Support History option (required if Beacon functionality is enabled) + ## Contributing -If you happen to find a bug or have a feature you'd like to see implemented, please [file an issue](https://github.com/HackGT/registration/issues). +If you happen to find a bug or have a feature you'd like to see implemented, please [file an issue](https://github.com/HackGT/registration/issues). -If you have some time and want to help us out with development, thank you! You can get started by taking a look at the open issues, particularly the ones marked [help wanted](https://github.com/HackGT/registration/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) or [help wanted - beginner](https://github.com/HackGT/registration/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted+-+beginner%22). Feel free to ask questions to clarify things, determine the best way to implement a new feature or bug fix, or anything else! +If you have some time and want to help us out with development, thank you! You can get started by taking a look at the open issues, particularly the ones marked [help wanted](https://github.com/HackGT/registration/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) or [good first issue](https://github.com/HackGT/registration/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). Feel free to ask questions to clarify things, determine the best way to implement a new feature or bug fix, or anything else! ### Tips @@ -143,4 +112,53 @@ If you have some time and want to help us out with development, thank you! You c ## License -Copyright © 2017 HackGT. Released under the MIT license. See [LICENSE](LICENSE) for more information. +Copyright © 2020 HackGT. Released under the MIT license. See [LICENSE](LICENSE) for more information. + +## Help Scout + +Registration includes two features to enable deep integration with Help Scout, a service desk solution, allowing us to +provide better support to users requesting support. + +- The **Help Scout Integration** allows registration to serve a Dynamic App for Help Scout to show additional information +about a user's application to a specific event, based on the email address they sent the email from. +- The **Help Scout Beacon** adds a small widget to user-facing pages requiring authentication making it super easy to +send an email asking for help, engage in live chat support (if enabled), or view knowledge base articles (if enabled). + +### Help Scout Integration +(Note: The Help Scout dynamic app integration will not work if registration is being hosted from `localhost`. In other +words, you can't test local changes to the Help Scout integration functionality in registration and view them in Help Scout.) + +To setup Help Scout integration: + +1. Configure the 2 required environment variables for Help Scout integration, starting with `HELPSCOUT_INTEGRATION` +2. Create a new custom app in Help Scout +3. For **Content Type**, choose **Dynamic Content** +4. For **Callback Url**, enter `/api/helpscout/userInfo` +5. For **Secret Key**, enter the exact same value as set in the `HELPSCOUT_INTEGRATION_SECRET_KEY` environment variable +6. Once you get the integration working, turn off **Debug Mode** in Help Scout. +7. Pick the mailboxes you want the registration integration to appear in. + +Note: If the Integration Secret Key is missing or empty but Help Scout Integration is enabled, +the feature will be disabled automatically. + +The Help Scout Integration can optionally show selected answers to questions from a user's application or confirmation +forms. You can indicate which questions to show in Help Scout using by adding `showInHelpScout: true` to any question +in your `questions.json` file (see example in the `question.json` included in this repo). + +### Help Scout Beacon +To setup the Help Scout Beacon: + +1. Make sure the `HELPSCOUT_BEACON_ENABLED` environment variable is set to `true`. +2. In Help Scout, go to Manage > Beacons and find or create a new Beacon. +3. First, find the Beacon ID. Go to the Installation tab of the Beacon settings and look for the part of the code (near the bottom) +that looks like `window.Beacon('init', '')`. +4. Set the `HELPSCOUT_BEACON_ID` environment variable to the Beacon ID you just found. +5. Now go to the Contact settings page for your Beacon. +6. In the Contact Form setting, turn on the **Support history security** setting. +7. A **secure key** will appear. Set the `HELPSCOUT_BEACON_SUPPORT_HISTORY_SECRET_KEY` to this value. +8. That's all the required configuration on registration's end. However, make sure you check out all of the available customization +options for Beacons available in Help Scout. You can change the settings remotely through Help Scout without changing +any code or configuration in registration! + +Note: if either of the required Beacon settings is missing or empty, the Beacon functionality +will be automatically disabled. diff --git a/api.graphql b/api.graphql index cbd29767..caee18d7 100644 --- a/api.graphql +++ b/api.graphql @@ -11,9 +11,11 @@ type Query { # All the users in the database, useful for polling for new user information. # This is paginated, n is the number of results, and last_id is the last ID # seen from the latest page retrieved, if you want the first page leave this out. - users(pagination_token: ID, n: Int!, filter: UserFilter): [User!]! - # Search through a user's name and email through regex - search_user(search: String!, use_regex: Boolean = false, offset: Int!, n: Int!, filter: UserFilter): [User!]! + users(pagination_token: ID, n: Int!, filter: UserFilter, ids: [String]): [User!]! + # Search for users by name and email + search_user(search: String!, use_regex: Boolean = false, offset: Int!, n: Int!, filter: UserFilter): SearchResult! + # Simplified search_user that can be forwarded correctly from checkin2 + search_user_simple(search: String!, use_regex: Boolean = false, offset: Int!, n: Int!, filter: UserFilter): [User!]! # All possible application question branches application_branches: [String!]! # All possible confirmation question branches @@ -24,6 +26,18 @@ type Query { question_names(branch: String): [String!] } +# A search result containing the results and associated data +type SearchResult { + # The offset passed to the search + offset: Int! + # The number of users returned by this query (limited by n) + count: Int! + # The total number of users that match this query (but not necessarily returned) + total: Int! + # The array of matching users + users: [User!]! +} + # Registration info about the user type User { # User ID, valid across the entire system @@ -33,8 +47,9 @@ type User { name: String! # User's email email: String! - # If the user's email is a verified email - email_verified: Boolean! + + # If the user has admin privileges + admin: Boolean! # If the user has applied to the event applied: Boolean! @@ -42,8 +57,10 @@ type User { accepted: Boolean! # If the user has been accepted and notified of his or her acceptance accepted_and_notified: Boolean! - # If the user has indicated that he or she is attending - attending: Boolean! + # If the user has submitted a confirmation + confirmed: Boolean! + # A users assigned confirmation branch + confirmationBranch: String # A users application phase answers # null if user has not filled out this phase @@ -76,7 +93,7 @@ input UserFilter { # If the user has been accepted to the event accepted: Boolean # If the user has indicated that he or she is attending - attending: Boolean + confirmed: Boolean # The type of application a user filled out (e.g. Mentor, Participant) application_branch: String # The type of confirmation a user filled out (e.g. Needs Reimbursement) @@ -100,18 +117,22 @@ type Branch { type Team { # ID of the Team id: ID! + # Name of the Team + name: String! } # Entries to various forms (application, confirmation, etc.) type FormItem { - # Name of the question / form item + # Name (basically the ID) of the question / form item name: String! + # Label of form item + label: String! # Type of form item (textbox, checkbox, phone no.) type: String! # Value (if just one string) value: String # Values (if many selections are applicable, like checkbox) - values: [String!] + values: [String] # File if type contains a file file: File } @@ -128,5 +149,6 @@ type File { path: String! # The size of the file in bytes size: Int! + # The formatted size of the file in human-readable units + size_formatted: String! } - diff --git a/client/admin.html b/client/admin.html index a49b3801..80bbce4c 100644 --- a/client/admin.html +++ b/client/admin.html @@ -7,17 +7,16 @@ - + - - + + - - + + + - + {{#> sidebar}}

Apply: {{branch}}

@@ -24,65 +25,28 @@

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}}
- {{#each questionData}} - {{#if this.textContent}} - {{{this.textContent}}} - {{/if}} - {{#if this.multi}} - {{#ifCond this.type "select"}} - - - {{#if this.hasOther}} - - {{/if}} - {{else}} - -
- {{#each this.options}} -
- - -
- {{/each}} - {{#if this.hasOther}} -
- -
- {{/if}} -
- {{/ifCond}} - {{else}} - - {{#ifCond this.type "textarea"}} - - {{else}} - {{#ifCond this.type "file"}} - {{#if this.value}} - -

Previously uploaded: {{this.value}}

- {{else}} - - {{/if}} - {{else}} - - {{/ifCond}} - {{/ifCond}} - {{/if}} -
- {{/each}} - {{{endText}}} + {{else}} + + + + + + {{/unless}} + + {{> form}}
+ {{#unless unauthenticated}} {{#if user.applied}} {{else}} {{/if}} + {{else}} + + {{/unless}}
{{/sidebar}} diff --git a/client/confirmation.html b/client/confirmation.html index 9d6b55d2..8b6822d6 100644 --- a/client/confirmation.html +++ b/client/confirmation.html @@ -10,13 +10,12 @@ - - + {{#> sidebar}}

RSVP: {{branch}}

@@ -25,59 +24,9 @@

RSVP: {{branch}}

Running event.preventDefault() in the submit button's click handler also disables validation for some reason -->
- {{#each questionData}} - {{#if this.textContent}} - {{{this.textContent}}} - {{/if}} - {{#if this.multi}} - {{#ifCond this.type "select"}} - - - {{#if this.hasOther}} - - {{/if}} - {{else}} - -
- {{#each this.options}} -
- - -
- {{/each}} - {{#if this.hasOther}} -
- -
- {{/if}} -
- {{/ifCond}} - {{else}} - - {{#ifCond this.type "textarea"}} - - {{else}} - {{#ifCond this.type "file"}} - {{#if this.value}} - -

Previously uploaded: {{this.value}}

- {{else}} - - {{/if}} - {{else}} - - {{/ifCond}} - {{/ifCond}} - {{/if}} -
- {{/each}} - {{{endText}}} + {{> form}}
- {{#if user.attending}} + {{#if user.confirmed}} {{else}} diff --git a/client/css/admin.css b/client/css/admin.css index e0387075..2b4f9d57 100644 --- a/client/css/admin.css +++ b/client/css/admin.css @@ -21,6 +21,16 @@ table td:last-child { width: 50px; text-align: center; } +#applicants > .search > * { + padding: 10px; +} +#applicants > .search > label { + min-width: 60px; + text-align: right; +} +#applicants > .search > button { + min-width: 100px; +} #applicants table td:last-child { width: 140px; } @@ -36,12 +46,6 @@ table td:last-child { td.email > i.fa { display: none; } -td.email.verified > i.fa-check { - display: inline; -} -td.email.notverified > i.fa-exclamation-triangle { - display: inline; -} td.email.admin > i.fa-asterisk { display: inline; } @@ -58,7 +62,7 @@ table thead { text-align: center; } -#email-rendered { +#email-rendered, #interstitial-rendered { padding: 10px; border-radius: 3px; background-color: #eee; @@ -127,10 +131,10 @@ canvas { } -.branch-role:nth-of-type(3n + 1) { +.branch-role:nth-of-type(3n + 1), .auth-method:nth-of-type(3n + 1) { margin-left: 0; } -.branch-role > h4 { +.branch-role > h4, .auth-method > h5 { height: 40px; white-space: nowrap; overflow: hidden; @@ -138,6 +142,9 @@ canvas { margin-top: 2rem; margin-bottom: 1rem; } +.auth-method > select { + width: 90%; +} .row-flex { display: flex; flex-wrap: wrap; diff --git a/client/css/index.css b/client/css/index.css index 03713117..f3be9fcc 100644 --- a/client/css/index.css +++ b/client/css/index.css @@ -1,13 +1,6 @@ h1 { margin-bottom: 35px; } -.main { - padding: 40px; - margin: 8px 0; - border: 1px solid #ccc; - border-radius: 4px; - box-sizing: border-box; -} .row:first-of-type h2, .row:first-of-type h4 { margin: 0; } @@ -37,4 +30,97 @@ p { #qrCode > svg { width: 50%; height: auto; -} \ No newline at end of file +} + +/* Timeline */ +ul.timeline { + list-style-type: none; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + overflow-x: hidden; +} +ul.timeline > li { + position: relative; +} +ul.timeline > li::before { + content: attr(data-pending); + margin-bottom: 20px; + padding: 0 15px; + font-weight: 100; + display: flex; + flex-direction: column; + align-items: center; + white-space: nowrap; +} +ul.timeline > li > div::before { + display: block; + content: ""; + width: 25px; + height: 25px; + background-color: white; + border-radius: 25px; + border: 1px solid #ddd; + position: absolute; + top: 31px; +} +ul.timeline > li > div { + height: 95px; + padding: 0 10px; + display: flex; + justify-content: center; + border-top: 2px solid #D6DCE0; + text-align: center; +} +ul.timeline > li.complete::before { + content: attr(data-complete); +} +ul.timeline > li.complete > div { + border-top: 2px solid #66DC71; +} +ul.timeline > li.complete > div::before { + content: "✔"; + background-color: #66DC71; + color: white; + border: none; +} +ul.timeline > li.warning::before { + content: attr(data-warning); +} +ul.timeline > li.warning > div { + border-top: 2px solid #FFDC00; +} +ul.timeline > li.warning > div::before { + content: "!"; + background-color: #FFDC00; + color: black; + border: none; +} +ul.timeline > li.rejected::before { + content: attr(data-rejected); +} +ul.timeline > li.rejected > div { + border-top: 2px solid #FF4136; +} +ul.timeline > li.rejected > div::before { + content: "✕"; + background-color: #FF4136; + color: white; + border: none; +} +ul.timeline > li > div > h4 { + display: block; + font-size: 1.7rem; + font-weight: 600; +} +@media screen and (max-width: 768px) { + ul.timeline { + flex-direction: column; + overflow-x: visible; + } + ul.timeline > li { + width: 100%; + margin-bottom: -10px; + } +} diff --git a/client/css/login.css b/client/css/login.css index 0ff1dcf4..8058b0ee 100644 --- a/client/css/login.css +++ b/client/css/login.css @@ -1,38 +1,15 @@ -h1 { - margin-bottom: 64px; - text-align: center; -} -a > i { - margin-right: 10px; - transform: scale(1.5); -} -a.github { - color: white; - background-color: #444444; -} -a.google { - color: white; - background-color: #4285F4; -} -a.facebook { - color: white; - background-color: #3b5998; -} -#error, #success { - text-align: center; - margin-top: 1em; - font-size: 2rem; - margin-bottom: -1em; - color: white; - padding: 10px; - border-radius: 5px; -} -#error { - background-color: rgba(255, 65, 54, 0.85); -} -#success { - background-color: #3D9970; -} -#error:empty, #success:empty { - display: none; -} +#error { + text-align: center; + margin-top: 1em; + font-size: 2rem; + margin-bottom: 1em; + color: white; + padding: 10px; + border-radius: 5px; + background-color: rgba(255, 65, 54, 0.85); +} +.btn { + display: block; + width: 100px; + margin: 0 auto; +} diff --git a/client/css/main.css b/client/css/main.css index fb05a00b..119c09b3 100644 --- a/client/css/main.css +++ b/client/css/main.css @@ -64,13 +64,13 @@ main { display: flex; justify-content: center; align-items: center; - + cursor: pointer; user-select: none; -webkit-transition: all 60ms ease-in-out; transition: all 60ms ease-in-out; - + text-align: center; white-space: nowrap; text-decoration: none !important; @@ -100,3 +100,11 @@ main { .btn, button { font-size: 15px; } + +section.main { + padding: 40px; + margin: 8px 0; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; +} diff --git a/client/css/theme.css b/client/css/theme.css deleted file mode 100644 index b7977204..00000000 --- a/client/css/theme.css +++ /dev/null @@ -1,71 +0,0 @@ -@import url("https://fonts.googleapis.com/css?family=Droid+Sans+Mono|VT323"); - -/* Colors */ -html, body, #sidebar-nav { - background-color: #081517; - color: #c4fbf5; -} -#sidebar a, #sidebar, .editor-toolbar > a { - color: #c4fbf5; - border: none; - text-decoration: initial; -} -.editor-toolbar > a { - color: #c4fbf5 !important; -} -.editor-toolbar a.active, .editor-toolbar a:hover { - border-color: #081517; - background-color: rgba(196, 251, 245, 0.2); - outline: none; -} -#sidebar-nav > span.divider, .main, table thead { - border-color: #c4fbf5; -} -a { - color: #c4fbf5; - text-decoration: none; - border-bottom: 1px dotted; - cursor: pointer; -} -a:hover { - border-bottom-style: solid; -} -#email-rendered { - background-color: #081517; - border: 1px solid #c4fbf5; -} - -/* Buttons */ -.btn, [type=submit], button, .accepted-btn-false { - background-color: #c4fbf5; - /* border: 2px solid #03565c; */ - color: #081517; -} - -/* Fonts */ -body, swal2-content, form > p, fieldset > div > label { - font-family: "Droid Sans Mono"; -} -fieldset > div > label { - font-size: 90%; -} -h1, h2, h3, h4, h5, h6, code, #sidebar a, #sidebar, label { - font-family: "VT323"; -} -code { - color: #081517; -} - -/* Form elements */ -input:not([type=submit]):not([type=radio]):not([type=checkbox]):not([type=reset]), select, fieldset, textarea { - background-color: #081517; - color: white; - border-color: #c4fbf5; - font-family: "Droid Sans Mono"; -} -input:not([type=submit]):not([type=radio]):not([type=checkbox]):not([type=reset]):focus, textarea:focus, textarea[type=text]:focus { - border-color: white; -} -input:disabled, textarea:disabled { - background: repeating-linear-gradient(45deg, #081517, #081517 10px, #192729 10px, #192729 20px ) -} diff --git a/client/forgotpassword.html b/client/forgotpassword.html deleted file mode 100644 index d9d40136..00000000 --- a/client/forgotpassword.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - Reset Password - {{siteTitle}} - - - - - - - - -
-

{{siteTitle}}

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

Reset your password

-

Enter your email address and we'll send you a link to reset your password.

- - - - -
-
-
- - diff --git a/client/helpscout/email_not_found.html b/client/helpscout/email_not_found.html new file mode 100644 index 00000000..2f09b8e3 --- /dev/null +++ b/client/helpscout/email_not_found.html @@ -0,0 +1,4 @@ +

+ {{email}} +

+

There are no users in registration with this email

diff --git a/client/helpscout/main.html b/client/helpscout/main.html new file mode 100644 index 00000000..b75ee6b3 --- /dev/null +++ b/client/helpscout/main.html @@ -0,0 +1,103 @@ +{{#*inline "listBlock"}} +
  • + {{title}} + {{> @partial-block}} + +
  • +{{/inline}} + +{{#*inline "badge"}} +{{> @partial-block}} +{{/inline}} + +{{#*inline "applicationQuestionListBlock"}} +
  • + App: {{title}} + {{> @partial-block}} + +
  • +{{/inline}} + +{{#*inline "confirmationQuestionListBlock"}} +
  • + Confirmation: {{title}} + {{> @partial-block}} + +
  • +{{/inline}} + +

    + {{email}} +

    + +

    {{applicationBranch}}

    + +
      + {{#if applied}} + {{#> listBlock title="Applied?"}} + To {{applicationBranch}} on {{applicationSubmitTime}} + {{/listBlock}} + {{else}} + {{#> listBlock title="Applied?"}} + No application + {{/listBlock}} + {{/if}} + + {{#if confirmationBranch}} + {{#> listBlock title="Accepted?"}} + {{#if accepted}} + {{#> badge type="success"}} + Accepted + {{/badge}} ({{confirmationBranch}}) + {{else}} + {{#> badge type="error"}} + Rejected + {{/badge}} ({{confirmationBranch}}) + {{/if}} + {{/listBlock}} + + {{#> listBlock title="Confirmed?"}} + {{#if confirmed}} + {{#if accepted}} + {{#> badge type="success"}} + Confirmed + {{/badge}}{{#if confirmationSubmitTime}} on {{confirmationSubmitTime}}{{/if}} + {{else}} + {{#> badge type="pending"}} + Confirmed + {{/badge}} + {{/if}} + {{else}} + No + {{/if}} + {{/listBlock}} + {{else}} + {{#if applied}} + {{#> listBlock title="Accepted?"}} + {{#> badge type="pending"}} + No decision + {{/badge}} + {{/listBlock}} + {{/if}} + {{/if}} + {{#each applicationQuestionsToShow}} + {{#> applicationQuestionListBlock title=this.name}} + {{#ifCond this.type "file"}} + {{this.prettyValue}} + {{else}} + {{this.prettyValue}} + {{/ifCond}} + {{/applicationQuestionListBlock}} + {{/each}} + + {{#each confirmationQuestionsToShow}} + {{#> confirmationQuestionListBlock title=this.name}} + {{this.prettyValue}} + {{/confirmationQuestionListBlock}} + {{/each}} + + {{#> listBlock title="Ground Truth UUID"}} + {{uuid}} + {{/listBlock}} +
    + diff --git a/client/index.html b/client/index.html index 44a44f97..6da788b6 100644 --- a/client/index.html +++ b/client/index.html @@ -10,10 +10,11 @@ - {{#if user.attending}} + {{#if user.confirmed}} {{#if settings.qrEnabled}} + {{/if}} {{/if}} @@ -22,46 +23,64 @@

    Dashboard

    +
      +
    • +
      +

      Application Submission

      +
      +
    • +
    • +
      +

      Application Decision

      +
      +
    • +
    • +
      +

      Confirmation Submission

      +
      +
    • +
    • +
      +

      Team Formation (optional)

      +
      +
    • +
    +

    Your status:

    - {{#if user.attending}} -

    Attending

    - {{else if user.accepted}} -

    Accepted

    - {{else if user.applied}} -

    Submitted

    - {{else}} -

    Incomplete

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

    Welcome back, {{user.name}}!

    - {{#if user.attending}} -

    You're all set!

    + {{#if user.confirmed}}

    Application type: {{user.applicationBranch}}

    Confirmation type: {{user.confirmationBranch}}

    - {{#if confirmationStatus.areOpen}} -

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

    - Edit your confirmation - {{else}} -

    RSVPing closed on {{confirmationClose}}.

    - {{/if}} -

    We look forward to seeing you!

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

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

    + Edit your confirmation + {{else}} +

    RSVPing closed on {{confirmationClose}}.

    + {{/if}} + {{/unless}} {{#if settings.qrEnabled}} -

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

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

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

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

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

    +

    Please confirm your application decision.

    Application type: {{user.applicationBranch}}

    +

    Confirmation type: {{user.confirmationBranch}}

    {{#ifCond allConfirmationTimes.length 1}} -

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

    +

    You must confirm your decision by {{confirmationClose}}.

    {{else}}

    If you do not RSVP before the deadline for your confirmation type, you will not be able to attend {{siteTitle}}!

      @@ -72,7 +91,6 @@

      Incomplete

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

      You've been accepted!

      Application type: {{user.applicationBranch}}

      {{#if confirmationStatus.beforeOpen}} {{#ifCond allConfirmationTimes.length 1}} diff --git a/client/interstitial.html b/client/interstitial.html new file mode 100644 index 00000000..f2667e2e --- /dev/null +++ b/client/interstitial.html @@ -0,0 +1,27 @@ + + + + {{siteTitle}} + + + + + + + + + + {{#> sidebar}} + {{{html}}} +
      +
      + + +
      + {{/sidebar}} + + diff --git a/client/js/admin.ts b/client/js/admin.ts index 24290c94..3a7d0f9f 100644 --- a/client/js/admin.ts +++ b/client/js/admin.ts @@ -1,6 +1,6 @@ class State { public id: string; - private sectionElement: HTMLElement; + private readonly sectionElement: HTMLElement; public static hideAll() { // tslint:disable-next-line:no-use-before-declare @@ -25,7 +25,61 @@ class State { this.sectionElement.style.display = "block"; } } -const states: State[] = ["statistics", "users", "applicants", "batch-accept", "settings"].map(id => new State(id)); +const states: State[] = ["statistics", "users", "applicants", "settings", "emails"].map(id => new State(id)); + +function generateFilter(branchFilter: HTMLInputElement, statusFilter: HTMLInputElement) { + let filter: any = {}; + if (branchFilter.value !== "*" && branchFilter.value !== "na") { + let [, type, branchName] = branchFilter.value.match(/^(application|confirmation)-(.*)$/)!; + if (type === "application") { + filter.applicationBranch = branchName; + } + else if (type === "confirmation") { + filter.confirmationBranch = branchName; + } + switch (statusFilter.value) { + case "no-submission": + if (type === "confirmation") { + filter.confirmed = false; + } + break; + case "submitted": + if (type === "confirmation") { + filter.confirmed = true; + } else { + filter.applied = true; + } + break; + } + } else if (branchFilter.value === "na") { + filter.applied = false; + } + return filter; +} +const batchEmailBranchFilterSelect = document.getElementById("email-branch-filter") as HTMLSelectElement; +const batchEmailStatusFilterSelect = document.getElementById("email-status-filter") as HTMLSelectElement; +async function batchEmailTypeChange(): Promise { + if (batchEmailBranchFilterSelect.value === "*" || batchEmailBranchFilterSelect.value === "na") { + batchEmailStatusFilterSelect.style.display = "none"; + } else { + for (let i = 0; i < batchEmailBranchFilterSelect.options.length; i++) { + batchEmailStatusFilterSelect.options.remove(0); + } + batchEmailStatusFilterSelect.style.display = "block"; + let [, type ] = batchEmailBranchFilterSelect.value.match(/^(application|confirmation)-(.*)$/)!; + // Only confirmation branches have no-submission option since confirmation is manually assigned + if (type === "confirmation") { + let noSubmission = new Option("Have not submitted (Confirmation)", "no-submission"); + batchEmailStatusFilterSelect.add(noSubmission); + } + let submitted = new Option(`Submitted (${type.charAt(0).toUpperCase() + type.slice(1)})`, "submitted"); + batchEmailStatusFilterSelect.add(submitted); + } +} +batchEmailBranchFilterSelect.addEventListener("change", batchEmailTypeChange); +batchEmailTypeChange().catch(err => { + console.error(err); +}); class UserEntries { private static readonly NODE_COUNT = 20; @@ -47,68 +101,109 @@ class UserEntries { const status = document.getElementById("users-entries-status") as HTMLParagraphElement; status.textContent = "Loading..."; - let query: { [index: string]: any } = { + let query = ` + query($offset: Int!, $count: Int!) { + search_user(search: "", offset: $offset, n: $count) { + offset, + count, + total, + users { + id, + name, + email, + admin, + + application { + type + }, + confirmation { + type + }, + applied, + accepted, + confirmed + } + } + }`; + let variables = { offset: this.offset, count: this.NODE_COUNT }; - let params = Object.keys(query) - .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(query[key])}`) - .join("&") - .replace(/%20/g, "+"); - fetch(`/api/admin/users?${params}`, { + + fetch("/graphql", { credentials: "same-origin", - method: "GET" + method: "POST", + headers: new Headers({ + "Content-Type": "application/json" + }), + body: JSON.stringify({ + query, + variables + }) }).then(checkStatus).then(parseJSON).then((response: { - offset: number; - count: number; - total: number; - data: any[]; + data: { + search_user: { + offset: number; + count: number; + total: number; + users: any[]; + }; + }; }) => { + let res = response.data.search_user; + for (let i = 0; i < this.NODE_COUNT; i++) { let node = this.nodes[i]; - let user = response.data[i]; + let user = res.users[i]; if (user) { node.style.display = "table-row"; node.querySelector("td.name")!.textContent = user.name; node.querySelector("td.email > span")!.textContent = user.email; - node.querySelector("td.email")!.classList.remove("verified", "notverified", "admin"); - if (user.verifiedEmail) { - node.querySelector("td.email")!.classList.add("verified"); - } - else { - node.querySelector("td.email")!.classList.add("notverified"); - } + node.querySelector("td.email")!.classList.remove("admin"); if (user.admin) { node.querySelector("td.email")!.classList.add("admin"); } - node.querySelector("td.status")!.textContent = user.status; - node.querySelector("td.login-method")!.textContent = user.loginMethods; + + let userStatus = "Signed up"; + if (user.applied) { + userStatus = `Applied (${user.application.type})`; + } + if (user.applied && user.accepted) { + userStatus = `Accepted (${user.application.type})`; + } + if (user.applied && user.accepted && user.confirmed) { + userStatus = `Accepted (${user.application.type}) / Confirmed`; + } + if (user.applied && user.accepted && user.confirmed && user.confirmation) { + userStatus = `Accepted (${user.application.type}) / Confirmed (${user.confirmation.type})`; + } + node.querySelector("td.status")!.textContent = userStatus; } else { node.style.display = "none"; } } - if (response.offset <= 0) { + if (res.offset <= 0) { this.previousButton.disabled = true; } else { this.previousButton.disabled = false; } - let upperBound = response.offset + response.count; - if (upperBound >= response.total) { - upperBound = response.total; + let upperBound = res.offset + res.count; + if (upperBound >= res.total) { + upperBound = res.total; this.nextButton.disabled = true; } else { this.nextButton.disabled = false; } - let lowerBound = response.offset + 1; - if (response.data.length <= 0) { + let lowerBound = res.offset + 1; + if (res.users.length <= 0) { lowerBound = 0; } - status.textContent = `${lowerBound} – ${upperBound} of ${response.total.toLocaleString()}`; + status.textContent = `${lowerBound} – ${upperBound} of ${res.total.toLocaleString()}`; }).catch(async err => { console.error(err); await sweetAlert("Oh no!", err.message, "error"); @@ -141,14 +236,16 @@ class UserEntries { } class ApplicantEntries { - private static readonly NODE_COUNT = 100; + private static readonly NODE_COUNT = 10; private static generalNodes: HTMLTableRowElement[] = []; private static detailsNodes: HTMLTableRowElement[] = []; private static offset: number = 0; + private static readonly searchButton = document.getElementById("applicant-search-execute") as HTMLButtonElement; private static readonly previousButton = document.getElementById("applicants-entries-previous") as HTMLButtonElement; private static readonly nextButton = document.getElementById("applicants-entries-next") as HTMLButtonElement; private static readonly branchFilter = document.getElementById("branch-filter") as HTMLInputElement; - private static readonly statusFilter = document.getElementById("status-filter") as HTMLInputElement; + private static readonly searchBox = document.getElementById("applicant-search") as HTMLInputElement; + private static readonly searchRegex = document.getElementById("applicant-search-regex") as HTMLInputElement; private static filter: any = {}; private static instantiate() { @@ -182,12 +279,17 @@ class ApplicantEntries { } private static updateFilter() { this.filter = {}; + if (this.branchFilter.value !== "*") { - this.filter.branch = this.branchFilter.value; - } - if (this.statusFilter.value !== "*") { - this.filter.status = this.statusFilter.value; + let [, type, branchName] = this.branchFilter.value.match(/^(application|confirmation)-(.*)$/)!; + if (type === "application") { + this.filter.application_branch = branchName; + } + else if (type === "confirmation") { + this.filter.confirmation_branch = branchName; + } } + this.offset = 0; this.load(); } @@ -195,79 +297,184 @@ class ApplicantEntries { const status = document.getElementById("applicants-entries-status") as HTMLParagraphElement; status.textContent = "Loading..."; - let query: { [index: string]: any } = { + let query = ` + query($search: String!, $useRegex: Boolean!, $offset: Int!, $count: Int!, $filter: UserFilter!) { + search_user(search: $search, use_regex: $useRegex, offset: $offset, n: $count, filter: $filter) { + offset, + count, + total, + users { + id, + name, + email, + admin, + team { + id, + name + }, + accepted, + confirmed, + confirmationBranch, + application { + type, + data { + name, + label, + value, + values, + file { + mimetype, + size_formatted, + path + } + } + }, + confirmation { + type, + data { + name, + label, + value, + values, + file { + mimetype, + size_formatted, + path + } + } + } + } + } + }`; + let variables = { + search: this.searchBox.value, + useRegex: this.searchRegex.checked, offset: this.offset, count: this.NODE_COUNT, - applied: true, - ...this.filter + filter: { + applied: true, + ...this.filter + } }; - let params = Object.keys(query) - .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(query[key])}`) - .join("&") - .replace(/%20/g, "+"); - fetch(`/api/admin/users?${params}`, { + + fetch("/graphql", { credentials: "same-origin", - method: "GET" + method: "POST", + headers: new Headers({ + "Content-Type": "application/json" + }), + body: JSON.stringify({ + query, + variables + }) }).then(checkStatus).then(parseJSON).then((response: { - offset: number; - count: number; - total: number; - data: any[]; + data: { + search_user: { + offset: number; + count: number; + total: number; + users: any[]; + }; + }; }) => { + let res = response.data.search_user; + + interface IFormItem { + name: string; + label: string; + type: string; + value?: string; + values?: string[]; + file?: { + original_name: string; + encoding: string; + mimetype: string; + path: string; + size: number; + size_formatted: string; + }; + } + function addApplicationData(dataSection: HTMLDivElement, applicationData: IFormItem[]): void { + for (let answer of applicationData as IFormItem[]) { + let row = document.createElement("p"); + let label = document.createElement("b"); + label.innerHTML = answer.label; + row.appendChild(label); + let value = ""; + if (answer.value) { + value = answer.value; + } + else if (answer.values) { + value = answer.values.join(", "); + } + else if (answer.file) { + value = `[${answer.file.mimetype} | ${answer.file.size_formatted}]: ${answer.file.path}`; + } + row.appendChild(document.createTextNode(` → ${value}`)); + if (answer.file) { + row.appendChild(document.createTextNode(" (")); + let link = document.createElement("a"); + link.setAttribute("href", `/${answer.file.path}`); + link.textContent = "Download"; + row.appendChild(link); + row.appendChild(document.createTextNode(")")); + } + dataSection.appendChild(row); + } + } + function addApplicationDataHeader(dataSection: HTMLDivElement, content: string): void { + let header = document.createElement("h4"); + header.textContent = content; + dataSection.appendChild(header); + } + for (let i = 0; i < this.NODE_COUNT; i++) { let generalNode = this.generalNodes[i]; let detailsNode = this.detailsNodes[i]; - let user = response.data[i]; + let user = res.users[i]; if (user) { generalNode.style.display = "table-row"; detailsNode.style.display = "table-row"; - generalNode.dataset.id = user.uuid; + generalNode.dataset.id = user.id; generalNode.querySelector("td.name")!.textContent = user.name; generalNode.querySelector("td.team")!.textContent = ""; - if (user.teamName) { + if (user.team) { let teamContainer = document.createElement("b"); - teamContainer.textContent = user.teamName; + teamContainer.textContent = user.team.name; generalNode.querySelector("td.team")!.appendChild(teamContainer); } else { generalNode.querySelector("td.team")!.textContent = "No Team"; } generalNode.querySelector("td.email > span")!.textContent = user.email; - generalNode.querySelector("td.email")!.classList.remove("verified", "notverified", "admin"); - if (user.verifiedEmail) { - generalNode.querySelector("td.email")!.classList.add("verified"); - } - else { - generalNode.querySelector("td.email")!.classList.add("notverified"); - } + generalNode.querySelector("td.email")!.classList.remove("admin"); if (user.admin) { generalNode.querySelector("td.email")!.classList.add("admin"); } - generalNode.querySelector("td.branch")!.textContent = user.applicationBranch; + generalNode.querySelector("td.branch")!.textContent = user.application.type; let statusSelect = generalNode.querySelector("select.status") as HTMLSelectElement; - statusSelect.value = user.accepted ? "accepted" : "no-decision"; + statusSelect.value = user.confirmationBranch ? user.confirmationBranch : "no-decision"; let dataSection = detailsNode.querySelector("div.applicantData") as HTMLDivElement; while (dataSection.hasChildNodes()) { dataSection.removeChild(dataSection.lastChild!); } - for (let answer of user.applicationDataFormatted as { label: string; value: string; filename?: string }[]) { - let row = document.createElement("p"); - let label = document.createElement("b"); - label.innerHTML = answer.label; - row.appendChild(label); - row.appendChild(document.createTextNode(` → ${answer.value}`)); - if (answer.filename) { - row.appendChild(document.createTextNode(" (")); - let link = document.createElement("a"); - link.setAttribute("href", `/uploads/${answer.filename}`); - link.textContent = "Download"; - row.appendChild(link); - row.appendChild(document.createTextNode(")")); - } - dataSection.appendChild(row); + + if (user.application) { + addApplicationDataHeader(dataSection, `Application data (${user.application.type})`); + addApplicationData(dataSection, user.application.data); + } + else { + addApplicationDataHeader(dataSection, "No application data"); + } + if (user.confirmation) { + addApplicationDataHeader(dataSection, `Confirmation data (${user.confirmation.type})`); + addApplicationData(dataSection, user.confirmation.data); + } + else { + addApplicationDataHeader(dataSection, "No confirmation data"); } } else { @@ -276,25 +483,25 @@ class ApplicantEntries { } } - if (response.offset <= 0) { + if (res.offset <= 0) { this.previousButton.disabled = true; } else { this.previousButton.disabled = false; } - let upperBound = response.offset + response.count; - if (upperBound >= response.total) { - upperBound = response.total; + let upperBound = res.offset + res.count; + if (upperBound >= res.total) { + upperBound = res.total; this.nextButton.disabled = true; } else { this.nextButton.disabled = false; } - let lowerBound = response.offset + 1; - if (response.data.length <= 0) { + let lowerBound = res.offset + 1; + if (res.users.length <= 0) { lowerBound = 0; } - status.textContent = `${lowerBound} – ${upperBound} of ${response.total.toLocaleString()}`; + status.textContent = `${lowerBound} – ${upperBound} of ${res.total.toLocaleString()}`; }).catch(async err => { console.error(err); await sweetAlert("Oh no!", err.message, "error"); @@ -303,9 +510,18 @@ class ApplicantEntries { public static setup() { this.generalNodes = []; + this.detailsNodes = []; this.instantiate(); this.offset = 0; - this.load(); + this.updateFilter(); + this.searchButton.addEventListener("click", () => { + this.updateFilter(); + }); + this.searchBox.addEventListener("keydown", event => { + if (event.key === "Enter") { + this.updateFilter(); + } + }); this.previousButton.addEventListener("click", () => { this.previous(); }); @@ -315,15 +531,14 @@ class ApplicantEntries { this.branchFilter.addEventListener("change", e => { this.updateFilter(); }); - this.statusFilter.addEventListener("change", e => { - this.updateFilter(); - }); } public static next() { + document.querySelector("#applicants > table")!.scrollIntoView(); this.offset += this.NODE_COUNT; this.load(); } public static previous() { + document.querySelector("#applicants > table")!.scrollIntoView(); this.offset -= this.NODE_COUNT; if (this.offset < 0) { this.offset = 0; @@ -372,70 +587,167 @@ sendAcceptancesButton.addEventListener("click", async e => { }); // -// Email content +// Markdown content // -declare let SimpleMDE: any; +declare const EasyMDE: typeof import("easymde"); const emailTypeSelect = document.getElementById("email-type") as HTMLSelectElement; +const emailSubject = document.getElementById("email-subject") as HTMLInputElement; + +const interstitialTypeSelect = document.getElementById("interstitial-type") as HTMLSelectElement; + let emailRenderedArea: HTMLElement | ShadowRoot = document.getElementById("email-rendered") as HTMLElement; +let interstitialRenderedArea: HTMLElement | ShadowRoot = document.getElementById("interstitial-rendered") as HTMLElement; +// @ts-ignore if (document.head.attachShadow) { // Browser supports Shadow DOM emailRenderedArea = emailRenderedArea.attachShadow({ mode: "open" }); + interstitialRenderedArea = interstitialRenderedArea.attachShadow({ mode: "open" }); } -const markdownEditor = new SimpleMDE({ element: document.getElementById("email-content")! }); -let contentChanged = false; -let lastSelected = emailTypeSelect.value; -markdownEditor.codemirror.on("change", async () => { - contentChanged = true; - try { - let content = new FormData(); - content.append("content", markdownEditor.value()); +const debounceTimeout = 500; // Milliseconds to wait before content is rendered to avoid hitting the server for every keystroke +function debounce(func: (...args: unknown[]) => void): (...args: unknown[]) => void { + let timer: any = null; + return () => { + if (timer) { + clearTimeout(timer); + } + timer = setTimeout(func, debounceTimeout); + }; +} - let { html, text }: { html: string; text: string } = ( - await fetch(`/api/settings/email_content/${emailTypeSelect.value}/rendered`, { - credentials: "same-origin", - method: "POST", - body: content - }).then(checkStatus).then(parseJSON) - ); - emailRenderedArea.innerHTML = html; - let hr = document.createElement("hr"); - hr.style.border = "1px solid #737373"; - emailRenderedArea.appendChild(hr); - let textContainer = document.createElement("pre"); - textContainer.textContent = text; - emailRenderedArea.appendChild(textContainer); +abstract class Editor { + private readonly editor: EasyMDE; + protected contentChanged: boolean = false; + protected lastSelected: string; + + constructor(editorElementID: string, protected typeSelector: HTMLSelectElement) { + let element = document.getElementById(editorElementID); + if (!element) { + throw new Error(`Cannot create Markdown editor from non-existent + {{else}} + {{#ifCond this.type "file"}} + {{#if this.value}} + +

      Previously uploaded: {{this.value}}

      + {{else}} + + {{/if}} + {{else}} + + {{/ifCond}} + {{/ifCond}} + {{/if}} +
      +{{/each}} +{{{endText}}} diff --git a/client/partials/login-methods.html b/client/partials/login-methods.html new file mode 100644 index 00000000..34e993f1 --- /dev/null +++ b/client/partials/login-methods.html @@ -0,0 +1,23 @@ +{{#ifIn "gatech" loginMethods}} + + Connect with Georgia Tech + +{{/ifIn}} +{{#ifIn "github" loginMethods}} + + + Connect with GitHub + +{{/ifIn}} +{{#ifIn "google" loginMethods}} + + + Connect with Google + +{{/ifIn}} +{{#ifIn "facebook" loginMethods}} + +{{/ifIn}} diff --git a/client/partials/sidebar.html b/client/partials/sidebar.html index f25e7e30..ac4a8aac 100644 --- a/client/partials/sidebar.html +++ b/client/partials/sidebar.html @@ -1,33 +1,47 @@
      {{> @partial-block }}
      +{{#unless unauthenticated}} +{{#if helpscout.beaconEnabled}} + +{{/if}} +{{/unless}} diff --git a/client/resetpassword.html b/client/resetpassword.html deleted file mode 100644 index a2729a81..00000000 --- a/client/resetpassword.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - Reset Password - {{siteTitle}} - - - - - - - - -
      -

      {{siteTitle}}

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

      Reset your password

      -
      - - - -
      -
      -
      -
      - - diff --git a/client/team.html b/client/team.html index ff8e8fab..4babd32d 100644 --- a/client/team.html +++ b/client/team.html @@ -11,11 +11,17 @@ - @@ -24,56 +30,58 @@ {{#> sidebar}}

      Team Management

      - {{#if user.teamId}} -

      Team {{team.teamName}}

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

      Team {{team.teamName}}

      +

      Teams can have up to {{settings.maxTeamSize}} members.

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

      There are no members in this team other than you!

      - {{/each}} -

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

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

      There are no members in this team other than you!

      + {{/each}} +

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

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

      Let's get you in a team

      -
      -
      -

      Join A Team

      -

      Ask your friend for their team name

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

      Create A Team

      - - -
      - + {{else}} +

      Let's get you in a team!

      +

      Teams can have up to {{settings.maxTeamSize}} members. If you don't currently have a team, you can form one at the event!

      + +
      +
      +

      Join a team

      +

      Ask your friend for their team name.

      + + +
      + +
      +
      +
      +

      Create a team

      + + +
      + +
      -
      - {{/if}} + {{/if}} +
    {{/sidebar}} diff --git a/client/tsconfig.json b/client/tsconfig.json index 0cc10965..4cf04ce6 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -18,7 +18,7 @@ "noUnusedLocals": true, "plugins": [ { - "name": "tslint-language-service", + "name": "tslint-language-service-ts3", "alwaysShowRuleFailuresAsWarnings": false, "ignoreDefinitionFiles": true, "configFile": "../tslint.json" diff --git a/cloudbuild.yaml b/cloudbuild.yaml new file mode 100644 index 00000000..4fdd8276 --- /dev/null +++ b/cloudbuild.yaml @@ -0,0 +1,28 @@ +steps: +- name: 'gcr.io/cloud-builders/docker' + args: ['build', '-t', 'hackgt/$REPO_NAME:$COMMIT_SHA', '-t', 'hackgt/$REPO_NAME:latest', '.'] +- name: 'gcr.io/cloud-builders/docker' + entrypoint: 'bash' + args: ['-c', 'docker login --username=hackgt --password=$$DOCKER_PASSWORD'] + secretEnv: ['DOCKER_PASSWORD'] +- name: 'gcr.io/cloud-builders/docker' + args: ['push', 'hackgt/$REPO_NAME:$COMMIT_SHA'] +- name: 'gcr.io/cloud-builders/docker' + args: ['push', 'hackgt/$REPO_NAME:latest'] +- name: 'hackgt/builder-trigger-beehive-build' + secretEnv: ['TRAVIS_TOKEN'] + env: + - 'REPO_NAME=$REPO_NAME' +- name: 'hackgt/builder-pr-autodeploy' + secretEnv: ['GH_TOKEN'] + env: + - 'REPO_NAME=$REPO_NAME' + - 'BRANCH_NAME=$BRANCH_NAME' +timeout: 300s +secrets: +- kmsKeyName: projects/hackgt-cluster/locations/global/keyRings/hackgt-build/cryptoKeys/hackgt-build-key + secretEnv: + DOCKER_PASSWORD: CiQATk4Dt7S6ki9JbJvw22WkKHsdyQhEOflZeK/1IJz/rJ18LmASNQD6KHO+IyjkNS/GHaDxJuLmbFXCr1kU5xzVVHaePe1CZeI1P1J/vyO3y1lXvSJpyID0hoXm + TRAVIS_TOKEN: CiQATk4Dt3x0+R2zKUvt090ul3dDhcZUnYsGoQfOzSjKquHVwiUSPwD6KHO+mVuFK9L8KYnFzg20D24Ih3FPujmuEiNPugBU0qTlTSQFcEMQrv93l8sLPlW8E8k0cg3v0mZqI721Rg== + GH_TOKEN: CiQATk4Dt+aCWrSvmOjyeFMx1cE6IdyMcp/9QICxGVymKcwNRnUSUQD6KHO+AF+uv67L1thZYzN4WY9U/UYDy69RR8KUEKNwfiUW2vxtvLOjMawfj4NHEnIthVC6Z9aWqb3In6l+/+mcKh5ZgVFKrWY8Whh+LEZGGA== + diff --git a/deployment.yaml b/deployment.yaml index df171364..a850b8ac 100644 --- a/deployment.yaml +++ b/deployment.yaml @@ -10,30 +10,21 @@ health: secrets: - ADMIN_KEY_SECRET - SESSION_SECRET - - EMAIL_USERNAME - - EMAIL_PASSWORD - - GOOGLE_CLIENT_ID - - GOOGLE_CLIENT_SECRET - - GITHUB_CLIENT_ID - - GITHUB_CLIENT_SECRET - - FACEBOOK_CLIENT_ID - - FACEBOOK_CLIENT_SECRET + - EMAIL_KEY + - GROUND_TRUTH_URL + - GROUND_TRUTH_ID + - GROUND_TRUTH_SECRET - STORAGE_ENGINE_OPTIONS + - HELPSCOUT_INTEGRATION_SECRET_KEY + - HELPSCOUT_BEACON_SUPPORT_HISTORY_SECRET_KEY env: - STORAGE_ENGINE: s3 + STORAGE_ENGINE: gcs COOKIE_SECURE_ONLY: true EMAIL_FROM: "HackGT Team " - EMAIL_HOST: smtp.sendgrid.net - EMAIL_PORT: 465 PRODUCTION: true ADMIN_EMAILS: - - petschekr@gmail.com - - a@andrewdai.co - - faizan.virani.44@gmail.com - - lesurasani@gmail.com - - arshiya.singh16@gmail.com - - ajoshi97@gatech.edu - - ehsanmasdar@gmail.com - - mjkaufer@gmail.com + - ehsan@hack.gt + - ryan@hack.gt + - julian@hack.gt diff --git a/package-lock.json b/package-lock.json index 6fd3a8ce..561c8540 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,224 @@ { "name": "registration", - "version": "1.11.7", + "version": "3.4.0", "lockfileVersion": 1, "requires": true, "dependencies": { + "@babel/runtime": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.5.1.tgz", + "integrity": "sha512-g+hmPKs16iewFSmW57NkH9xpPkuYD1RV3UE2BCkXx9j+nhhRb9hsiSxPmEa67j35IecTQdn4iyMtHMbt5VoREg==", + "requires": { + "regenerator-runtime": "^0.13.2" + } + }, + "@google-cloud/common": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-2.3.0.tgz", + "integrity": "sha512-nmIyi3q/FL2j6ZJ61xK/863DoJEZayI2/W/iCgwrCYUYsem277XO45MBTAimjgiKBCA0c9InmQyfT48h/IK4jg==", + "requires": { + "@google-cloud/projectify": "^1.0.0", + "@google-cloud/promisify": "^1.0.0", + "arrify": "^2.0.0", + "duplexify": "^3.6.0", + "ent": "^2.2.0", + "extend": "^3.0.2", + "google-auth-library": "^5.5.0", + "retry-request": "^4.0.0", + "teeny-request": "^6.0.0" + }, + "dependencies": { + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + } + } + }, + "@google-cloud/paginator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-2.0.3.tgz", + "integrity": "sha512-kp/pkb2p/p0d8/SKUu4mOq8+HGwF8NPzHWkj+VKrIPQPyMRw8deZtrO/OcSiy9C/7bpfU5Txah5ltUNfPkgEXg==", + "requires": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "dependencies": { + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + } + } + }, + "@google-cloud/projectify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-1.0.4.tgz", + "integrity": "sha512-ZdzQUN02eRsmTKfBj9FDL0KNDIFNjBn/d6tHQmA/+FImH5DO6ZV8E7FzxMgAUiVAUq41RFAkb25p1oHOZ8psfg==" + }, + "@google-cloud/promisify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-1.0.4.tgz", + "integrity": "sha512-VccZDcOql77obTnFh0TbNED/6ZbbmHDf8UMNnzO1d5g9V0Htfm4k5cllY8P1tJsRKC3zWYGRLaViiupcgVjBoQ==" + }, + "@google-cloud/storage": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-4.3.0.tgz", + "integrity": "sha512-ph0jsUsZ9FPtN40V5eIkKPLUmxnTpxqBDkWxStW/kbQZgoNVGW9vJcbsYSyE4ath7jQIpM4OHu6aqmPFX1OnGw==", + "requires": { + "@google-cloud/common": "^2.1.1", + "@google-cloud/paginator": "^2.0.0", + "@google-cloud/promisify": "^1.0.0", + "arrify": "^2.0.0", + "compressible": "^2.0.12", + "concat-stream": "^2.0.0", + "date-and-time": "^0.12.0", + "duplexify": "^3.5.0", + "extend": "^3.0.2", + "gaxios": "^2.0.1", + "gcs-resumable-upload": "^2.2.4", + "hash-stream-validation": "^0.2.2", + "mime": "^2.2.0", + "mime-types": "^2.0.8", + "onetime": "^5.1.0", + "p-limit": "^2.2.0", + "pumpify": "^2.0.0", + "readable-stream": "^3.4.0", + "snakeize": "^0.1.0", + "stream-events": "^1.0.1", + "through2": "^3.0.0", + "xdg-basedir": "^4.0.0" + }, + "dependencies": { + "concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "mime": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", + "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==" + }, + "readable-stream": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.5.0.tgz", + "integrity": "sha512-gSz026xs2LfxBPudDuI41V1lka8cxg64E66SGe78zJlsUofOg/yqwezdIcdfwik6B4h8LFmWPA9ef9X3FiNFLA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + } + } + }, + "@handlebars/allow-prototype-access": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@handlebars/allow-prototype-access/-/allow-prototype-access-1.0.3.tgz", + "integrity": "sha512-UjIJnXYirTtSuRAC/3Sq+2XCRd3+JBW5+wQVu7cRCXisvEj/u/913QKH3D5+YVyJkz6cAyD70sQkaL5zvXql5Q==" + }, + "@ladjs/i18n": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@ladjs/i18n/-/i18n-1.1.0.tgz", + "integrity": "sha512-Kynr5osjApDCyiik35MMNZC1lgjgrk7fbV6P1qHXKQ67sR/U85Ddnv1NNPc/2s08PQVjvIBNY96UACb0CivrWg==", + "requires": { + "auto-bind": "^2.0.0", + "boolean": "^0.2.0", + "boom": "7.3.0", + "country-language": "^0.1.7", + "i18n": "^0.8.3", + "i18n-locales": "^0.0.2", + "lodash": "^4.17.11", + "moment": "^2.23.0", + "qs": "^6.6.0", + "underscore.string": "^3.3.5" + }, + "dependencies": { + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + } + } + }, + "@sendgrid/client": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-6.2.1.tgz", + "integrity": "sha512-FLqoh2UqmFs5R/92xzF1jYMLGU89rTgLK6XX+VA02YcfQW8rGjbMrj7zsSCQ7SLkeiWekmUU2+naeIO9L4dqxA==", + "requires": { + "@sendgrid/helpers": "^6.2.1", + "@types/request": "^2.0.3", + "request": "^2.81.0" + } + }, + "@sendgrid/helpers": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-6.2.1.tgz", + "integrity": "sha512-WnQ4TV51Xln/X70lk6J1/3tfRHW3K4zagz19vlJrtQUtX1wwghOj926OqcMm5nOiBHEh+la3cvdzHENb09FsIA==", + "requires": { + "chalk": "^2.0.1" + } + }, + "@sendgrid/mail": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-6.2.1.tgz", + "integrity": "sha512-gTd8gMp4JVLGZhXb/DkyrjByTfIR1OvtpPpQLwO11Vz72x3JdPl4tJTtWA/svAFfN5UXnZtAomAvjJCdcd4lzw==", + "requires": { + "@sendgrid/client": "^6.2.1", + "@sendgrid/helpers": "^6.2.1" + } + }, + "@sindresorhus/is": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.17.1.tgz", + "integrity": "sha512-kg/maAZD2Z2AHDFp7cY/ACokjUL0e7MaupTtGXkSW2SV4DJQEHdslFUioP0SMccotjwqTdI0b4XH/qZh6CN+kQ==" + }, + "@types/agenda": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/agenda/-/agenda-2.0.5.tgz", + "integrity": "sha512-2loWrgsW675/bappNGZwLeuXSPaF7b3ZxXsqFz6zmA1sY31Y8De5XZM5377EQf7VgLPRXVN6jx/V9jAYk9953A==", + "dev": true, + "requires": { + "@types/mongodb": "*", + "@types/node": "*" + } + }, "@types/archiver": { "version": "0.15.37", "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-0.15.37.tgz", "integrity": "sha1-5cgLrWyaiY/AlK6UWy3DITx47A4=", "dev": true, "requires": { - "@types/node": "8.0.32" + "@types/node": "*" } }, "@types/aws-sdk": { @@ -19,7 +227,20 @@ "integrity": "sha1-g1iLPRTr3KHUzl4CM4dXdWjOgvM=", "dev": true, "requires": { - "aws-sdk": "2.126.0" + "aws-sdk": "*" + } + }, + "@types/babel-types": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/@types/babel-types/-/babel-types-7.0.7.tgz", + "integrity": "sha512-dBtBbrc+qTHy1WdfHYjBwRln4+LWqASWakLHsWHR2NWHIFkv4W3O070IGoGLEBrJBvct3r0L1BUPuvURi7kYUQ==" + }, + "@types/babylon": { + "version": "6.16.5", + "resolved": "https://registry.npmjs.org/@types/babylon/-/babylon-6.16.5.tgz", + "integrity": "sha512-xH2e58elpj1X4ynnKp9qSnWlsRTIs6n3tgLGNfwAGHwePw0mulHQllV34n0T25uYSu1k0hRKkWXF890B1yS47w==", + "requires": { + "@types/babel-types": "*" } }, "@types/body-parser": { @@ -28,36 +249,38 @@ "integrity": "sha1-mOndlmSn+oFP7CxQyVu/ntrPXwU=", "dev": true, "requires": { - "@types/express": "4.0.37", - "@types/node": "8.0.32" + "@types/express": "*", + "@types/node": "*" } }, "@types/bson": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@types/bson/-/bson-1.0.4.tgz", - "integrity": "sha512-/nysVvxwup1WniGHIM31UZXM+6727h4FAa2tZpFSQBooBcl2Bh1N9oQmVVg8QYnjchN/DOGi7UvVN0jpzWL6sw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.0.tgz", + "integrity": "sha512-pq/rqJwJWkbS10crsG5bgnrisL8pML79KlMKQMoQwLUjlPAkrUHMvHJ3oGwE7WHR61Lv/nadMwXVAD2b+fpD8Q==", "dev": true, "requires": { - "@types/node": "8.0.32" + "@types/node": "*" } }, + "@types/caseless": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", + "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==" + }, "@types/chai": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-3.5.2.tgz", "integrity": "sha1-wRzSgX06QBt7oPWkIPNcVhObHB4=", "dev": true }, - "@types/chalk": { - "version": "0.4.31", - "resolved": "https://registry.npmjs.org/@types/chalk/-/chalk-0.4.31.tgz", - "integrity": "sha1-ox10JBprHtu5c8822XooloNKUfk=", - "dev": true - }, - "@types/cheerio": { - "version": "0.22.2", - "resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.2.tgz", - "integrity": "sha512-7YJQbllem83umRzZ+ry6RZFAbBWNoHvbiW6tE5VEAJIoX6F22hrKrBw3ibRvHm2zPnOhZGrZJusAbp1zZKcLzQ==", - "dev": true + "@types/codemirror": { + "version": "0.0.71", + "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.71.tgz", + "integrity": "sha512-b2oEEnno1LIGKMR7uBEsr40al1UijF1HEpRn0+Yf1xOLl24iQgB7DBpZVMM7y54G5wCNoclDrRO65E6KHPNO2w==", + "dev": true, + "requires": { + "@types/tern": "*" + } }, "@types/compression": { "version": "0.0.33", @@ -65,7 +288,7 @@ "integrity": "sha1-ldxzOiM5qoRjgdfxN3eS0lU9wn0=", "dev": true, "requires": { - "@types/express": "4.0.37" + "@types/express": "*" } }, "@types/connect-flash": { @@ -74,7 +297,7 @@ "integrity": "sha1-hrjj2tEwfi0QXYNopxO/kPjLwFo=", "dev": true, "requires": { - "@types/express": "4.0.37" + "@types/express": "*" } }, "@types/connect-mongo": { @@ -83,10 +306,10 @@ "integrity": "sha1-k5ynCFE0gsfT8yo4xjfUnSrH0JA=", "dev": true, "requires": { - "@types/express": "4.0.37", - "@types/express-session": "0.0.32", - "@types/mongodb": "2.2.12", - "@types/mongoose": "4.7.23" + "@types/express": "*", + "@types/express-session": "*", + "@types/mongodb": "*", + "@types/mongoose": "*" } }, "@types/cookie-parser": { @@ -95,32 +318,47 @@ "integrity": "sha512-iJY6B3ZGufLiDf2OCAgiAAQuj1sMKC/wz/7XCEjZ+/MDuultfFJuSwrBKcLSmJ5iYApLzCCYBYJZs0Ws8GPmwA==", "dev": true, "requires": { - "@types/express": "4.0.37" + "@types/express": "*" } }, "@types/cookie-signature": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/cookie-signature/-/cookie-signature-1.0.0.tgz", - "integrity": "sha1-NHhg7+hV9W+Gxi1TGtw0e34K9XE=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/cookie-signature/-/cookie-signature-1.0.1.tgz", + "integrity": "sha512-9KMD0CrZb9x8NBzCmApIiMXNmicNPZIWQO9y6lM27IrPjhOGV4Nc3HBvEeoHd0FkRNK3es2mQ1TFlEV1qclRhA==", + "dev": true + }, + "@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, + "@types/events": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-1.2.0.tgz", + "integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==", "dev": true }, "@types/express": { - "version": "4.0.37", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.0.37.tgz", - "integrity": "sha512-tIULTLzQpFFs5/PKnFIAFOsXQxss76glppbVKR3/jddPK26SBsD5HF5grn5G2jOGtpRWSBvYmDYoduVv+3wOXg==", + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.16.0.tgz", + "integrity": "sha512-TtPEYumsmSTtTetAPXlJVf3kEqb6wZK0bZojpJQrnD/djV4q1oB6QQ8aKvKqwNPACoe02GNiy5zDzcYivR5Z2w==", "dev": true, "requires": { - "@types/express-serve-static-core": "4.0.53", - "@types/serve-static": "1.7.32" + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" } }, "@types/express-serve-static-core": { - "version": "4.0.53", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.0.53.tgz", - "integrity": "sha512-zaGeOpEYp5G2EhjaUFdVwysDrfEYc6Q6iPhd3Kl4ip30x0tvVv7SuJvY3yzCUSuFlzAG8N5KsyY6BJg93/cn+Q==", + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.16.0.tgz", + "integrity": "sha512-lTeoCu5NxJU4OD9moCgm0ESZzweAx0YqsAcab6OB0EB3+As1OaHtKnaGJvcngQxYsi9UNv0abn4/DRavrRxt4w==", "dev": true, "requires": { - "@types/node": "8.0.32" + "@types/events": "*", + "@types/node": "*", + "@types/range-parser": "*" } }, "@types/express-session": { @@ -129,8 +367,8 @@ "integrity": "sha1-gvnmoCjrYSWkEtuV8OYal9GUXuA=", "dev": true, "requires": { - "@types/express": "4.0.37", - "@types/node": "8.0.32" + "@types/express": "*", + "@types/node": "*" } }, "@types/graphql": { @@ -139,15 +377,21 @@ "integrity": "sha512-ob2dps4itT/Le5DbxjssBXtBnloDIRUbkgtAvaB42mJ8pVIWMRuURD9WjnhaEGZ4Ql/EryXMQWeU8Y0EU73QLw==" }, "@types/handlebars": { - "version": "4.0.36", - "resolved": "https://registry.npmjs.org/@types/handlebars/-/handlebars-4.0.36.tgz", - "integrity": "sha512-LjNiTX7TY7wtuC6y3QwC93hKMuqYhgV9A1uXBKNvZtVC8ZvyWAjZkJ5BvT0K7RKqORRYRLMrqCxpw5RgS+MdrQ==", + "version": "4.0.38", + "resolved": "https://registry.npmjs.org/@types/handlebars/-/handlebars-4.0.38.tgz", + "integrity": "sha512-oMzU0D7jDp+H2go/i0XqBHfr+HEhYD/e1TvkhHi3yrhQm/7JFR8FJMdvoH76X8G1FBpgc6Pwi+QslCJBeJ1N9g==", + "dev": true + }, + "@types/html-to-text": { + "version": "1.4.31", + "resolved": "https://registry.npmjs.org/@types/html-to-text/-/html-to-text-1.4.31.tgz", + "integrity": "sha512-9vTFw6vYZNnjPOep9WRXs7cw0vg04pAZgcX9bqx70q1BNT7y9sOJovpbiNIcSNyHF/6LscLvGhtb5Og1T0UEvA==", "dev": true }, "@types/marked": { - "version": "0.0.28", - "resolved": "https://registry.npmjs.org/@types/marked/-/marked-0.0.28.tgz", - "integrity": "sha1-RLp1Tp+lFDJYPo6zCnxN0km1L6o=", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-1.1.0.tgz", + "integrity": "sha512-j8XXj6/l9kFvCwMyVqozznqpd/nk80krrW+QiIJN60Uu9gX5Pvn4/qPJ2YngQrR3QREPwmrE1f9/EWKVTFzoEw==", "dev": true }, "@types/mime": { @@ -157,160 +401,187 @@ "dev": true }, "@types/mocha": { - "version": "2.2.43", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-2.2.43.tgz", - "integrity": "sha512-xNlAmH+lRJdUMXClMTI9Y0pRqIojdxfm7DHsIxoB2iTzu3fnPmSMEN8SsSx0cdwV36d02PWCWaDUoZPDSln+xw==", + "version": "2.2.48", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-2.2.48.tgz", + "integrity": "sha512-nlK/iyETgafGli8Zh9zJVCTicvU3iajSkRwOh3Hhiva598CMqNJ4NcVCGMTGKpGpTYj/9R8RLzS9NAykSSCqGw==", "dev": true }, "@types/moment-timezone": { - "version": "0.2.35", - "resolved": "https://registry.npmjs.org/@types/moment-timezone/-/moment-timezone-0.2.35.tgz", - "integrity": "sha512-HG4pUK/fTrGY3FerMlINxK74MxdAxkCRYrp5AM+oJ2jLcK0jWUi64ZV15JKwDR4TYLIxrT3y9SVnEWcLPbC/YA==", + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/@types/moment-timezone/-/moment-timezone-0.5.6.tgz", + "integrity": "sha512-rMZjLmXs9sly1UbwxckyAEvQkrwrGqR24nFAjFrndRJBBnUooCCD0LPmdRcf9haHXFnckI9E3ko0oC6LEDk7dw==", "dev": true, "requires": { - "moment": "2.18.1" + "moment": ">=2.14.0" } }, "@types/mongodb": { - "version": "2.2.12", - "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-2.2.12.tgz", - "integrity": "sha512-nJLPAQro3U0nwhXnglmH+4DMUBVDa4tsPnwEFtzyv4x+eT0UynlrPm2rGR8UPDw0VcTF3UGI5UEqyjcnq/ukgA==", + "version": "3.1.22", + "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.1.22.tgz", + "integrity": "sha512-hvNR0txBlJJAy1eZOeIDshW4dnQaC694COou4eHHaMdIcteCfoCQATD7sYNlXxNxfTc1iIbHUi7A8CAhQe08uA==", "dev": true, "requires": { - "@types/bson": "1.0.4", - "@types/node": "8.0.32" + "@types/bson": "*", + "@types/node": "*" } }, "@types/mongoose": { - "version": "4.7.23", - "resolved": "https://registry.npmjs.org/@types/mongoose/-/mongoose-4.7.23.tgz", - "integrity": "sha512-13UfisRIfRCP/+FV85e99vofsc7JB0K6KeevN4Bq6UjyXHwFGRDnHvc9iAB591fQI+MRQyNvrFHJHrNou16Dww==", + "version": "5.3.23", + "resolved": "https://registry.npmjs.org/@types/mongoose/-/mongoose-5.3.23.tgz", + "integrity": "sha512-UZJOkFe/ShSt3iYFBiadwwCu2Y8qm/RZyAoCQI2uf88wr3NfDBpbqqoIyrchBy1y2XtvAAyktEPzvvR7up6/TQ==", "dev": true, "requires": { - "@types/mongodb": "2.2.12", - "@types/node": "8.0.32" + "@types/mongodb": "*", + "@types/node": "*" } }, "@types/morgan": { - "version": "1.7.33", - "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.7.33.tgz", - "integrity": "sha512-HBsWVjFJWDbH79Aug/Pyxsc5KWyZs2vkxn7qbo+9a7w2jVur6egGsyJeacDW5Pb5cO+fUl+X5kZaDct8asYp1w==", + "version": "1.7.35", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.7.35.tgz", + "integrity": "sha512-E9qFi0seOkdlQnCTPv54brNfGWeFdRaEhI5tSue4pdx/V+xfxvMETsxXhOEcj1cYL+0n/jcTEmj/jD2gjzCwMg==", "dev": true, "requires": { - "@types/express": "4.0.37" + "@types/express": "*" } }, "@types/multer": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.3.3.tgz", - "integrity": "sha512-a4vEHvs9MEyvHwb2/rw3Cg0MaPrp1jbEEWrhBL9xf457AYS79bOajcfkYCEICH+1NExZkqxxtZKydKIX5UTbmg==", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.3.6.tgz", + "integrity": "sha512-MAFQ/UqkLtnweDfSr1je71ed0L1XIL/lL/cfpjE+eJ9gDekObXCVHYCSZXxSfTTfN8BdrP77O6YzQ1Yi4lglGg==", "dev": true, "requires": { - "@types/express": "4.0.37" + "@types/express": "*" } }, "@types/node": { - "version": "8.0.32", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.32.tgz", - "integrity": "sha512-n1zzgeQehndikZc/8N4rGSZc99cO6Tb3OInKzvWYniJsT/jet3m57buaBFa5cMeVNFosN4PKZ2LM1y16CFD7Rg==", - "dev": true - }, - "@types/nodemailer": { - "version": "1.3.33", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-1.3.33.tgz", - "integrity": "sha512-PONEJf/LwNcqgU/GpMIAquSBFdq+kCdpYI9TdoeGcTfLCsXzWunKzv4bUQs8zfKGz97CLymgoL0fMLYpOu+/1A==", + "version": "11.11.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.3.tgz", + "integrity": "sha512-wp6IOGu1lxsfnrD+5mX6qwSwWuqsdkKKxTN4aQc4wByHAKZJf9/D4KXPQ1POUjEbnCP5LMggB0OEFNY9OTsMqg==" + }, + "@types/oauth": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.1.tgz", + "integrity": "sha512-a1iY62/a3yhZ7qH7cNUsxoI3U/0Fe9+RnuFrpTKr+0WVOzbKlSLojShCKe20aOD1Sppv+i8Zlq0pLDuTJnwS4A==", "dev": true, "requires": { - "@types/node": "8.0.32", - "@types/nodemailer-direct-transport": "1.0.30", - "@types/nodemailer-smtp-transport": "2.7.3" + "@types/node": "*" } }, - "@types/nodemailer-direct-transport": { - "version": "1.0.30", - "resolved": "https://registry.npmjs.org/@types/nodemailer-direct-transport/-/nodemailer-direct-transport-1.0.30.tgz", - "integrity": "sha512-gH49BNkXM8EZb/UgI4hUwWwTW3izRx5L+0VyohKkbVijvfUIhn7RALSpBjCUyXzEj0XZSNmQMFVc97Lj0z8UIw==", + "@types/passport": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.0.tgz", + "integrity": "sha512-R2FXqM+AgsMIym0PuKj08Ybx+GR6d2rU3b1/8OcHolJ+4ga2pRPX105wboV6hq1AJvMo2frQzYKdqXS5+4cyMw==", "dev": true, "requires": { - "@types/nodemailer": "1.3.33" + "@types/express": "*" } }, - "@types/nodemailer-smtp-transport": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/@types/nodemailer-smtp-transport/-/nodemailer-smtp-transport-2.7.3.tgz", - "integrity": "sha512-HxKPBErWelYVIWiKkUl06IaG4ojEMDtH6cAlojKgjsqwF8UQun4QeahYCWLCkA8/vKOX0G6VV1Vu2Z4x4ovqLQ==", + "@types/passport-oauth2": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.4.8.tgz", + "integrity": "sha512-tlX16wyFE5YJR2pHpZ308dgB1MV9/Ra2wfQh71eWk+/umPoD1Rca2D4N5M27W7nZm1wqUNGTk1I864nHvEgiFA==", "dev": true, "requires": { - "@types/node": "8.0.32", - "@types/nodemailer": "1.3.33" + "@types/express": "*", + "@types/oauth": "*", + "@types/passport": "*" } }, - "@types/passport": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@types/passport/-/passport-0.3.4.tgz", - "integrity": "sha512-P+eK/+A7KX2Ngtbr4wPO2dV4HxvHpT9cf76iFXnkQ/BuPGqkWPYBL7gElDlcO/XksMCmmwZRv3rUmiBRNkpmjA==", - "dev": true, - "requires": { - "@types/express": "4.0.37" - } + "@types/prop-types": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.1.tgz", + "integrity": "sha512-CFzn9idOEpHrgdw8JsoTkaDDyRWk1jrzIV8djzcgpq0y9tG4B4lFT+Nxh52DVpDXV+n4+NPNv7M1Dj5uMp6XFg==" }, - "@types/passport-facebook": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@types/passport-facebook/-/passport-facebook-2.1.6.tgz", - "integrity": "sha512-T3toYvfb9Qy0KcQYwp/fngNvG45VVdvpl4JzjPCN4/RjCCpQ2u4WWL3OrkxKtRWS7iWgeYyY6HcvZspHwSA/xg==", + "@types/qr-image": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/qr-image/-/qr-image-3.2.1.tgz", + "integrity": "sha512-x8Jv9YUw0wPXviOwX1/QPcLWFv2M9dYAQwYKWs4pqzCcBMWrgjGlrPSpvc9T2YtKitJa/+BkGQnAUgphqdeoeA==", "dev": true, "requires": { - "@types/express": "4.0.37", - "@types/passport": "0.3.4" + "@types/node": "*" } }, - "@types/passport-local": { - "version": "1.0.31", - "resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.31.tgz", - "integrity": "sha512-edWrCkVkgrUUbTohZoXeo1hxbguNwpbx1VasicxqLGAO6N/3P8d1rKhlx8KhkVLWFH+gL5PIzejdYQSt1I30/g==", - "dev": true, + "@types/range-parser": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.2.tgz", + "integrity": "sha512-HtKGu+qG1NPvYe1z7ezLsyIaXYyi8SoAVqWDZgDQ8dLrsZvSzUNCwZyfX33uhWxL/SU0ZDQZ3nwZ0nimt507Kw==", + "dev": true + }, + "@types/react": { + "version": "16.8.23", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.8.23.tgz", + "integrity": "sha512-abkEOIeljniUN9qB5onp++g0EY38h7atnDHxwKUFz1r3VH1+yG1OKi2sNPTyObL40goBmfKFpdii2lEzwLX1cA==", "requires": { - "@types/express": "4.0.37", - "@types/passport": "0.3.4", - "@types/passport-strategy": "0.2.31" + "@types/prop-types": "*", + "csstype": "^2.2.0" } }, - "@types/passport-strategy": { - "version": "0.2.31", - "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.31.tgz", - "integrity": "sha512-YCxa7Xowz0dQ3AOIE7UMGBgMpNWHtxtsv+vmRH7rc6lo3zcDHiKjh12lFyiGpbwyu1digyP70XtuDYtI0jbobQ==", - "dev": true, + "@types/request": { + "version": "2.48.2", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.2.tgz", + "integrity": "sha512-gP+PSFXAXMrd5PcD7SqHeUjdGshAI8vKQ3+AvpQr3ht9iQea+59LOKvKITcQI+Lg+1EIkDP6AFSBUJPWG8GDyA==", "requires": { - "@types/express": "4.0.37", - "@types/passport": "0.3.4" + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + }, + "dependencies": { + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "form-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.0.tgz", + "integrity": "sha512-WXieX3G/8side6VIqx44ablyULoGruSde5PNTxoUyo5CeyAMX6nVWUd0rgist/EuX655cjhUhTo1Fo3tRYqbcA==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + } } }, "@types/serve-static": { - "version": "1.7.32", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.7.32.tgz", - "integrity": "sha512-WpI0g7M1FiOmJ/a97Qrjafq2I938tjAZ3hZr9O7sXyA6oUhH3bqUNZIt7r1KZg8TQAKxcvxt6JjQ5XuLfIBFvg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q==", "dev": true, "requires": { - "@types/express-serve-static-core": "4.0.53", - "@types/mime": "2.0.0" + "@types/express-serve-static-core": "*", + "@types/mime": "*" } }, "@types/superagent": { - "version": "3.5.6", - "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-3.5.6.tgz", - "integrity": "sha512-yGiVkRbB1qtIkRCpEJIxlHazBoILmu33xbbu4IiwxTJjwDi/EudiPYAD7QwWe035jkE40yQgTVXZsAePFtleww==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-3.8.0.tgz", + "integrity": "sha512-NDR8ieJZfgDjoIE5cQ3bDAwx9bkHoYo33JxolNK7/6RWevHWVkYIniwX0uZG1yWqEbv81bXBdX0v3eAmQ0Hsbw==", "dev": true, "requires": { - "@types/node": "8.0.32" + "@types/node": "*" } }, "@types/supertest": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.3.tgz", - "integrity": "sha512-QxgjuDhQEq4tPJXTOnGoN4P/BWwvftvkaCZhXMp1C1Otk9kGXb7l/wA7kwz3MwtEMnovdYikS+ZtvaQ4RbhF4g==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.4.tgz", + "integrity": "sha512-0TvOJ+6XVMSImgqc2ClNllfVffCxHQhFbsbwOGzGTjdFydoaG052LPqnP8SnmSlnokOcQiPPcbz+Yi30LxWPyA==", + "dev": true, + "requires": { + "@types/superagent": "*" + } + }, + "@types/tern": { + "version": "0.22.1", + "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.22.1.tgz", + "integrity": "sha512-CRzPRkg8hYLwunsj61r+rqPJQbiCIEQqlMMY/0k7krgIsoSaFgGg1ZH2f9qaR1YpenaMl6PnlTtUkCbNH/uo+A==", "dev": true, "requires": { - "@types/superagent": "3.5.6" + "@types/estree": "*" } }, "@types/tmp": { @@ -319,13 +590,27 @@ "integrity": "sha1-EHPEvIJHVK49EM+riKsCN7qWTk0=", "dev": true }, + "@types/tough-cookie": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.5.tgz", + "integrity": "sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg==" + }, + "@types/uuid": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.3.tgz", + "integrity": "sha512-5fRLCYhLtDb3hMWqQyH10qtF+Ud2JnNCXTCZ+9ktNdCcgslcuXkDTkFcJNk++MT29yDntDnlF1+jD+uVGumsbw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/whatwg-fetch": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/@types/whatwg-fetch/-/whatwg-fetch-0.0.33.tgz", "integrity": "sha1-GcDShjyMsjgPIaHHNrecv3iVuxM=", "dev": true, "requires": { - "@types/whatwg-streams": "0.0.4" + "@types/whatwg-streams": "*" } }, "@types/whatwg-streams": { @@ -334,24 +619,130 @@ "integrity": "sha512-dM5YQytWb1EunntizWnsAsADJxbXhHQyPoRxXlfEMPULcnbgzB02qZ8KI/K5yFItulzoidxWbX4OO/w4FN92Sg==", "dev": true }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, "accepts": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", - "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=", + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", "requires": { - "mime-types": "2.1.17", + "mime-types": "~2.1.18", "negotiator": "0.6.1" + }, + "dependencies": { + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "requires": { + "mime-db": "~1.33.0" + } + } + } + }, + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=" + }, + "acorn-globals": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-3.1.0.tgz", + "integrity": "sha1-/YJw9x+7SZawBPqIDuXUZXOnMb8=", + "requires": { + "acorn": "^4.0.4" + }, + "dependencies": { + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" + } + } + }, + "agenda": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/agenda/-/agenda-2.0.2.tgz", + "integrity": "sha512-iP3aFNMeGKivtSDrXuzs29w88FB47WKIRkLhnkdh4Jj4Pe2VxpM2zOnNcAQEW1qxiC0lP1wgOI1f6OfPACOPkA==", + "requires": { + "cron": "~1.3.0", + "date.js": "~0.3.2", + "debug": "~3.1.0", + "human-interval": "~0.1.3", + "moment-timezone": "~0.5.0", + "mongodb": "~3.1" + }, + "dependencies": { + "bson": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.1.tgz", + "integrity": "sha512-jCGVYLoYMHDkOsbwJZBCqwMHyH4c+wzgI9hG7Z6SZJRXWr+x58pdIbm2i9a/jFGCkRJqRUr8eoI7lDWa0hTkxg==" + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "mongodb": { + "version": "3.1.13", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.1.13.tgz", + "integrity": "sha512-sz2dhvBZQWf3LRNDhbd30KHVzdjZx9IKC0L+kSZ/gzYquCF5zPOgGqRz6sSCqYZtKP2ekB4nfLxhGtzGHnIKxA==", + "requires": { + "mongodb-core": "3.1.11", + "safe-buffer": "^5.1.2" + } + }, + "mongodb-core": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-3.1.11.tgz", + "integrity": "sha512-rD2US2s5qk/ckbiiGFHeu+yKYDXdJ1G87F6CG3YdaZpzdOm5zpoAZd/EKbPmFO6cQZ+XVXBXBJ660sSI0gc6qg==", + "requires": { + "bson": "^1.1.0", + "require_optional": "^1.0.1", + "safe-buffer": "^5.1.2", + "saslprep": "^1.0.0" + } + }, + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + } } }, + "agent-base": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", + "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==" + }, "ajv": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.2.3.tgz", - "integrity": "sha1-wG9Zh3jETGsWGrr+NGa4GtGBTtI=", + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", "requires": { - "co": "4.6.0", - "fast-deep-equal": "1.0.0", - "json-schema-traverse": "0.3.1", - "json-stable-stringify": "1.0.1" + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" } }, "align-text": { @@ -359,15 +750,45 @@ "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", "requires": { - "kind-of": "3.2.2", - "longest": "1.0.1", - "repeat-string": "1.6.1" + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" } }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" + "ambi": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ambi/-/ambi-2.5.0.tgz", + "integrity": "sha1-fI43K+SIkRV+fOoBy2+RQ9H3QiA=", + "requires": { + "editions": "^1.1.1", + "typechecker": "^4.3.0" + }, + "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + }, + "typechecker": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/typechecker/-/typechecker-4.7.0.tgz", + "integrity": "sha512-4LHc1KMNJ6NDGO+dSM/yNfZQRtp8NN7psYrPHUblD62Dvkwsp3VShsbM78kOgpcmMkRTgvwdKOTjctS+uMllgQ==", + "requires": { + "editions": "^2.1.0" + }, + "dependencies": { + "editions": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/editions/-/editions-2.1.3.tgz", + "integrity": "sha512-xDZyVm0A4nLgMNWVVLJvcwMjI80ShiH/27RyLiCnW1L273TcJIA25C4pwJ33AWV01OX6UriP35Xu+lH4S7HWQw==", + "requires": { + "errlop": "^1.1.1", + "semver": "^5.6.0" + } + } + } + } + } }, "ansi-regex": { "version": "2.1.1", @@ -375,11 +796,11 @@ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "requires": { - "color-convert": "1.9.0" + "color-convert": "^1.9.0" } }, "any-promise": { @@ -387,34 +808,44 @@ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" }, + "apollo-cache-control": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/apollo-cache-control/-/apollo-cache-control-0.1.1.tgz", + "integrity": "sha512-XJQs167e9u+e5ybSi51nGYr70NPBbswdvTEHtbtXbwkZ+n9t0SLPvUcoqceayOSwjK1XYOdU/EKPawNdb3rLQA==", + "requires": { + "graphql-extensions": "^0.0.x" + } + }, "apollo-server-core": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-1.1.0.tgz", - "integrity": "sha1-dMO/Q5ThTq56tgsdmZo8W4qpTpo=", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-1.3.6.tgz", + "integrity": "sha1-CGNiQ8LeVvqMJn1o3WAssfvTI+M=", "requires": { - "apollo-tracing": "0.0.7" + "apollo-cache-control": "^0.1.0", + "apollo-tracing": "^0.1.0", + "graphql-extensions": "^0.0.x" } }, "apollo-server-express": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-1.1.2.tgz", - "integrity": "sha1-aTPHf+XfuafzDdOTI5rZlTphPNk=", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-1.3.6.tgz", + "integrity": "sha1-ISCwUCGofe9E+v2EbooOKjKFLbc=", "requires": { - "apollo-server-core": "1.1.0", - "apollo-server-module-graphiql": "1.1.2" + "apollo-server-core": "^1.3.6", + "apollo-server-module-graphiql": "^1.3.4" } }, "apollo-server-module-graphiql": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/apollo-server-module-graphiql/-/apollo-server-module-graphiql-1.1.2.tgz", - "integrity": "sha1-SaFUz4DphKywgr0AlhdbVh4b+8w=" + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/apollo-server-module-graphiql/-/apollo-server-module-graphiql-1.3.4.tgz", + "integrity": "sha1-UDmbfFG3Jn0MhBUp9Rc+X8cwTeQ=" }, "apollo-tracing": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/apollo-tracing/-/apollo-tracing-0.0.7.tgz", - "integrity": "sha512-jvjNmOOb3M2QiBEuz9Vjp6HiXtZuDwRvHxqBZQ+TE0UoODRnJoQu5LF1uvPI2ooOHiPC1ce4SAKNNIU9y02EeA==", + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/apollo-tracing/-/apollo-tracing-0.1.4.tgz", + "integrity": "sha512-Uv+1nh5AsNmC3m130i2u3IqbS+nrxyVV3KYimH5QKsdPjxxIQB3JAT+jJmpeDxBel8gDVstNmCh82QSLxLSIdQ==", "requires": { - "graphql-tools": "1.2.3" + "graphql-extensions": "~0.0.9" } }, "append-field": { @@ -427,15 +858,15 @@ "resolved": "https://registry.npmjs.org/archiver/-/archiver-1.3.0.tgz", "integrity": "sha1-TyGU1tj5nfP1MeaIHxTxXVX6ryI=", "requires": { - "archiver-utils": "1.3.0", - "async": "2.5.0", - "buffer-crc32": "0.2.13", - "glob": "7.1.2", - "lodash": "4.17.4", - "readable-stream": "2.3.3", - "tar-stream": "1.5.4", - "walkdir": "0.0.11", - "zip-stream": "1.2.0" + "archiver-utils": "^1.3.0", + "async": "^2.0.0", + "buffer-crc32": "^0.2.1", + "glob": "^7.0.0", + "lodash": "^4.8.0", + "readable-stream": "^2.0.0", + "tar-stream": "^1.5.0", + "walkdir": "^0.0.11", + "zip-stream": "^1.1.0" } }, "archiver-utils": { @@ -443,12 +874,12 @@ "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-1.3.0.tgz", "integrity": "sha1-5QtMCccL89aA4y/xt5lOn52JUXQ=", "requires": { - "glob": "7.1.2", - "graceful-fs": "4.1.11", - "lazystream": "1.0.0", - "lodash": "4.17.4", - "normalize-path": "2.1.1", - "readable-stream": "2.3.3" + "glob": "^7.0.0", + "graceful-fs": "^4.1.0", + "lazystream": "^1.0.0", + "lodash": "^4.8.0", + "normalize-path": "^2.0.0", + "readable-stream": "^2.0.0" } }, "argparse": { @@ -456,7 +887,7 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", "requires": { - "sprintf-js": "1.0.3" + "sprintf-js": "~1.0.2" } }, "array-flatten": { @@ -464,6 +895,29 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, + "arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==" + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, "assertion-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", @@ -475,41 +929,64 @@ "resolved": "https://registry.npmjs.org/async/-/async-2.5.0.tgz", "integrity": "sha512-e+lJAJeNWuPCNyxZKOBdaJGyLGHugXVQtrAwtuAe2vhxTYxFTKE73p8JuTmdH0qdQZtDvI4dhJwjZc5zsfIsYw==", "requires": { - "lodash": "4.17.4" + "lodash": "^4.14.0" } }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "auto-bind": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-2.1.0.tgz", + "integrity": "sha512-qZuFvkes1eh9lB2mg8/HG18C+5GIO51r+RrCSst/lh+i5B1CtVlkhTE488M805Nr3dKl0sM/pIFKSKUIlg3zUg==", + "requires": { + "@types/react": "^16.8.12" + } }, "aws-sdk": { - "version": "2.126.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.126.0.tgz", - "integrity": "sha1-9355QVO2iwME9Sa2z/ThDMhJsnQ=", + "version": "2.255.1", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.255.1.tgz", + "integrity": "sha512-E9aXfpFJUlVHZoQiRvkGULPUPkXCdtwKvLBO04MmxC8AcNXwSQ3lX/OI+944ehWNJA0nNrsMPOo/T6hBdS+3TQ==", "requires": { "buffer": "4.9.1", - "crypto-browserify": "1.0.9", "events": "1.1.1", + "ieee754": "1.1.8", "jmespath": "0.15.0", "querystring": "0.2.0", "sax": "1.2.1", "url": "0.10.3", - "uuid": "3.0.1", - "xml2js": "0.4.17", - "xmlbuilder": "4.2.1" + "uuid": "3.1.0", + "xml2js": "0.4.17" + }, + "dependencies": { + "uuid": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" + } } }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + }, "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", "dev": true, "requires": { - "chalk": "1.1.3", - "esutils": "2.0.2", - "js-tokens": "3.0.2" + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" }, "dependencies": { "ansi-styles": { @@ -524,11 +1001,11 @@ "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" } }, "supports-color": { @@ -539,77 +1016,211 @@ } } }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + } + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "base64-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz", - "integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==" + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" + }, + "base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==" }, "basic-auth": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.0.tgz", - "integrity": "sha1-AV2z81PgLlY3d1X5YnQuiYHnu7o=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "requires": { + "safe-buffer": "5.1.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", "requires": { - "safe-buffer": "5.1.1" + "tweetnacl": "^0.14.3" } }, + "bignumber.js": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz", + "integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ==" + }, "bl": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.1.tgz", - "integrity": "sha1-ysMo977kVzDUBLaSID/LWQ4XLV4=", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", "requires": { - "readable-stream": "2.3.3" + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } } }, "bluebird": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz", - "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw=" + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" }, "body-parser": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", - "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", + "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", "requires": { "bytes": "3.0.0", - "content-type": "1.0.4", + "content-type": "~1.0.4", "debug": "2.6.9", - "depd": "1.1.1", - "http-errors": "1.6.2", - "iconv-lite": "0.4.19", - "on-finished": "2.3.0", - "qs": "6.5.1", - "raw-body": "2.3.2", - "type-is": "1.6.15" - } - }, - "boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" - }, - "bowser": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-1.7.3.tgz", - "integrity": "sha1-UEvbQxGMqNucu63yj9YPJlr5bk8=" + "depd": "~1.1.2", + "http-errors": "~1.6.3", + "iconv-lite": "0.4.23", + "on-finished": "~2.3.0", + "qs": "6.5.2", + "raw-body": "2.3.3", + "type-is": "~1.6.16" + }, + "dependencies": { + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "requires": { + "mime-db": "~1.33.0" + } + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "type-is": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", + "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.18" + } + } + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" + }, + "boolean": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-0.2.0.tgz", + "integrity": "sha512-mDcM3ChboDuhv4glLXEH1us7jMiWXRSs3R13Okoo+kkFOlLIjvF1y88507wTfDf9zsuv0YffSDFUwX95VAT/mg==" + }, + "boom": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-7.3.0.tgz", + "integrity": "sha512-Swpoyi2t5+GhOEGw8rEsKvTxFLIDiiKoUc2gsoV6Lyr43LHBIzch3k2MvYUs8RTROrIkVJ3Al0TkaOGjnb+B6A==", + "requires": { + "hoek": "6.x.x" + } + }, + "bowser": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-1.9.3.tgz", + "integrity": "sha512-/gp96UlcFw5DbV2KQPCqTqi0Mb9gZRyDAHiDsGEH+4B/KOQjeoE5lM1PxlVX8DQDvfEfitmC1rW2Oy8fk/XBDg==" }, "brace-expansion": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", "requires": { - "balanced-match": "1.0.0", + "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "browser-stdout": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", - "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, "bson": { @@ -622,9 +1233,9 @@ "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", "requires": { - "base64-js": "1.2.1", - "ieee754": "1.1.8", - "isarray": "1.0.0" + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" } }, "buffer-crc32": { @@ -632,18 +1243,34 @@ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, + "buffer-from": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.0.tgz", + "integrity": "sha512-c5mRlguI/Pe2dSZmpER62rSCu0ryKmWddzRYsuXc50U2/g8jMOulc31VZMa4mYx31U5xsmSOpDCgH88Vl9cDGQ==" + }, "buffer-shims": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=" }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, "busboy": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", "requires": { "dicer": "0.2.5", - "readable-stream": "1.1.14" + "readable-stream": "1.1.x" }, "dependencies": { "isarray": { @@ -656,10 +1283,10 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", "isarray": "0.0.1", - "string_decoder": "0.10.31" + "string_decoder": "~0.10.x" } }, "string_decoder": { @@ -679,20 +1306,32 @@ "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=" }, + "caller-id": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/caller-id/-/caller-id-0.1.0.tgz", + "integrity": "sha1-Wb2sCJPRLDhxQIJ5Ix+XRYNk8Hs=", + "dev": true, + "requires": { + "stack-trace": "~0.0.7" + } + }, "camelcase": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", - "optional": true + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, "center-align": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", - "optional": true, "requires": { - "align-text": "0.1.4", - "lazy-cache": "1.0.4" + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" } }, "chai": { @@ -701,22 +1340,30 @@ "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", "dev": true, "requires": { - "assertion-error": "1.0.2", - "check-error": "1.0.2", - "deep-eql": "3.0.1", - "get-func-name": "2.0.0", - "pathval": "1.1.0", - "type-detect": "4.0.3" + "assertion-error": "^1.0.1", + "check-error": "^1.0.1", + "deep-eql": "^3.0.0", + "get-func-name": "^2.0.0", + "pathval": "^1.0.0", + "type-detect": "^4.0.0" } }, "chalk": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.1.0.tgz", - "integrity": "sha512-LUHGS/dge4ujbXMJrnihYMcL4AoOweGnw9Tp3kQuqy1Kx5c1qKjqvMJZ6nVJPMWJtKCTN72ZogH3oeSO9g9rXQ==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "4.4.0" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "character-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", + "integrity": "sha1-x84o821LzZdE5f/CxfzeHHMmH8A=", + "requires": { + "is-regex": "^1.0.3" } }, "check-error": { @@ -725,17 +1372,12 @@ "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", "dev": true }, - "cheerio": { - "version": "1.0.0-rc.2", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.2.tgz", - "integrity": "sha1-S59TqBsn5NXawxwP/Qz6A8xoMNs=", + "clean-css": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz", + "integrity": "sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==", "requires": { - "css-select": "1.2.0", - "dom-serializer": "0.1.0", - "entities": "1.1.1", - "htmlparser2": "3.9.2", - "lodash": "4.17.4", - "parse5": "3.0.2" + "source-map": "~0.6.0" } }, "cli-color": { @@ -743,12 +1385,12 @@ "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-1.2.0.tgz", "integrity": "sha1-OlrnT9drYmevZm5p4q+70B3vNNE=", "requires": { - "ansi-regex": "2.1.1", - "d": "1.0.0", - "es5-ext": "0.10.30", - "es6-iterator": "2.0.1", - "memoizee": "0.4.11", - "timers-ext": "0.1.2" + "ansi-regex": "^2.1.1", + "d": "1", + "es5-ext": "^0.10.12", + "es6-iterator": "2", + "memoizee": "^0.4.3", + "timers-ext": "0.1" } }, "cli-table": { @@ -763,18 +1405,16 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", - "optional": true, "requires": { - "center-align": "0.1.3", - "right-align": "0.1.3", + "center-align": "^0.1.1", + "right-align": "^0.1.1", "wordwrap": "0.0.2" }, "dependencies": { "wordwrap": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", - "optional": true + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" } } }, @@ -783,12 +1423,25 @@ "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" }, + "codemirror": { + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.43.0.tgz", + "integrity": "sha512-mljwQWUaWIf85I7QwTBryF2ASaIvmYAL4s5UCanCJFfKeXOKhrqdHWdHiZWAMNT+hjLTCnVx2S/SYTORIgxsgA==" + }, + "codemirror-spell-checker": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/codemirror-spell-checker/-/codemirror-spell-checker-1.1.2.tgz", + "integrity": "sha1-HGYPkIlIPMtRE7m6nKGcP0mTNx4=", + "requires": { + "typo-js": "*" + } + }, "color-convert": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.0.tgz", "integrity": "sha1-Gsz5fdc5uYO/mU1W/sj5WFNkG3o=", "requires": { - "color-name": "1.1.3" + "color-name": "^1.1.1" } }, "color-name": { @@ -807,7 +1460,7 @@ "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", "dev": true, "requires": { - "delayed-stream": "1.0.0" + "delayed-stream": "~1.0.0" } }, "commander": { @@ -826,32 +1479,39 @@ "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-1.2.0.tgz", "integrity": "sha1-WFhwku8g03y1i68AARLJJ4/3O58=", "requires": { - "buffer-crc32": "0.2.13", - "crc32-stream": "2.0.0", - "normalize-path": "2.1.1", - "readable-stream": "2.3.3" + "buffer-crc32": "^0.2.1", + "crc32-stream": "^2.0.0", + "normalize-path": "^2.0.0", + "readable-stream": "^2.0.0" } }, "compressible": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.11.tgz", - "integrity": "sha1-FnGKdd4oPtjmBAQWJaIGRYZ5fYo=", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.14.tgz", + "integrity": "sha1-MmxfUH+7BV9UEWeCuWmoG2einac=", "requires": { - "mime-db": "1.30.0" + "mime-db": ">= 1.34.0 < 2" + }, + "dependencies": { + "mime-db": { + "version": "1.34.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.34.0.tgz", + "integrity": "sha1-RS0Oz/XDA0am3B5kseruDTcZ/5o=" + } } }, "compression": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.1.tgz", - "integrity": "sha1-7/JgPvwuIs+G810uuTWJ+YdTc9s=", + "version": "1.7.2", + "resolved": "http://registry.npmjs.org/compression/-/compression-1.7.2.tgz", + "integrity": "sha1-qv+81qr4VLROuygDU9WtFlH1mmk=", "requires": { - "accepts": "1.3.4", + "accepts": "~1.3.4", "bytes": "3.0.0", - "compressible": "2.0.11", + "compressible": "~2.0.13", "debug": "2.6.9", - "on-headers": "1.0.1", + "on-headers": "~1.0.1", "safe-buffer": "5.1.1", - "vary": "1.1.2" + "vary": "~1.1.2" } }, "concat-map": { @@ -864,9 +1524,22 @@ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", "requires": { - "inherits": "2.0.3", - "readable-stream": "2.3.3", - "typedarray": "0.0.6" + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "configstore": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.0.tgz", + "integrity": "sha512-eE/hvMs7qw7DlcB5JPRnthmrITuHMmACUJAp89v6PT6iOqzoLS7HRWhBtuHMlhNHo2AhUSA/3Dh1bKNJHcublQ==", + "requires": { + "dot-prop": "^5.1.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" } }, "connect-flash": { @@ -875,12 +1548,30 @@ "integrity": "sha1-2GMPJtlaf4UfmVax6MxnMvO2qjA=" }, "connect-mongo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/connect-mongo/-/connect-mongo-1.3.2.tgz", - "integrity": "sha1-fL9Y3/8mdg5eAOAX0KhbS8kLnTc=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/connect-mongo/-/connect-mongo-2.0.1.tgz", + "integrity": "sha512-ghBvVq0SA0SkTFsbAB8HdF1+uoHdFJICSlrTklNloMKXuRpX9IuVBnG0DlKnXBZSQI0Joyaq22cazsrV9+5g2A==", + "requires": { + "mongodb": "^2.0.36" + } + }, + "consolidate": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.15.1.tgz", + "integrity": "sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==", + "requires": { + "bluebird": "^3.1.1" + } + }, + "constantinople": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.1.2.tgz", + "integrity": "sha512-yePcBqEFhLOqSBtwYOGGS1exHo/s1xjekXiinh4itpNQGCu4KA1euPh1fg07N2wMITZXQkBz75Ntdt1ctGZouw==", "requires": { - "bluebird": "3.5.0", - "mongodb": "2.2.31" + "@types/babel-types": "^7.0.0", + "@types/babylon": "^6.16.2", + "babel-types": "^6.26.0", + "babylon": "^6.18.0" } }, "content-disposition": { @@ -905,24 +1596,45 @@ "requires": { "cookie": "0.3.1", "cookie-signature": "1.0.6" + }, + "dependencies": { + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + } } }, "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.1.0.tgz", + "integrity": "sha512-Alvs19Vgq07eunykd3Xy2jF0/qSNv2u7KDbAek9H5liV1UMijbqFs5cycZvv5dVsvseT/U4H8/7/w8Koh35C4A==" }, "cookiejar": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.1.tgz", - "integrity": "sha1-Qa1XsbVVlR7BcUEqgZQrHoIA00o=", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", "dev": true }, + "core-js": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", + "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==" + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "country-language": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/country-language/-/country-language-0.1.7.tgz", + "integrity": "sha1-eHD0uhJduaYHHxlze9nvk0OuNds=", + "requires": { + "underscore": "~1.7.0", + "underscore.deep": "~0.5.1" + } + }, "crc": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/crc/-/crc-3.5.0.tgz", @@ -933,24 +1645,56 @@ "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-2.0.0.tgz", "integrity": "sha1-483TtN8xaN10494/u8t7KX/pCPQ=", "requires": { - "crc": "3.5.0", - "readable-stream": "2.3.3" + "crc": "^3.4.4", + "readable-stream": "^2.0.0" } }, - "crypto-browserify": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-1.0.9.tgz", - "integrity": "sha1-zFRJaF37hesRyYKKzHy4erW7/MA=" + "cron": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cron/-/cron-1.3.1.tgz", + "integrity": "sha512-iZlSOfm2IQUUMMko4Nj0+B8Sk5S/wWwJF++X+L4TfAfd3Y0i8s5beqm4nhwFyorkOOnmy8czymLyh+MeYKLxzg==", + "requires": { + "moment-timezone": "^0.5.x" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + } + } + }, + "crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==" + }, + "csextends": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/csextends/-/csextends-1.2.0.tgz", + "integrity": "sha512-S/8k1bDTJIwuGgQYmsRoE+8P+ohV32WhQ0l4zqrc0XDdxOhjQQD7/wTZwCzoZX53jSX3V/qwjT+OkPTxWQcmjg==" }, "css-select": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", "requires": { - "boolbase": "1.0.0", - "css-what": "2.1.0", + "boolbase": "~1.0.0", + "css-what": "2.1", "domutils": "1.5.1", - "nth-check": "1.0.1" + "nth-check": "~1.0.1" } }, "css-what": { @@ -958,12 +1702,57 @@ "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.0.tgz", "integrity": "sha1-lGfQMsOM+u+58teVASUwYvh/ob0=" }, + "csstype": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.5.tgz", + "integrity": "sha512-JsTaiksRsel5n7XwqPAfB0l3TFKdpjW/kgAELf9vrb5adGA7UCPLajKK5s3nFrcFm3Rkyp/Qkgl73ENc1UY3cA==" + }, "d": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", "requires": { - "es5-ext": "0.10.30" + "es5-ext": "^0.10.9" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "datauri": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/datauri/-/datauri-2.0.0.tgz", + "integrity": "sha512-zS2HSf9pI5XPlNZgIqJg/wCJpecgU/HA6E/uv2EfaWnW1EiTGLfy/EexTIsC9c99yoCOTXlqeeWk4FkCSuO3/g==", + "requires": { + "image-size": "^0.7.3", + "mimer": "^1.0.0" + } + }, + "date-and-time": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-0.12.0.tgz", + "integrity": "sha512-n2RJIAp93AucgF/U/Rz5WRS2Hjg5Z+QxscaaMCi6pVZT1JpJKRH+C08vyH/lRR1kxNXnPxgo3lWfd+jCb/UcuQ==" + }, + "date.js": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/date.js/-/date.js-0.3.3.tgz", + "integrity": "sha512-HgigOS3h3k6HnW011nAb43c5xx5rBXk8P2v/WIT9Zv4koIaVXiH2BURguI78VVp+5Qc076T7OR378JViCnZtBw==", + "requires": { + "debug": "~3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } } }, "debug": { @@ -977,8 +1766,7 @@ "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "optional": true + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, "deep-eql": { "version": "3.0.1", @@ -986,14 +1774,18 @@ "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", "dev": true, "requires": { - "type-detect": "4.0.3" + "type-detect": "^4.0.0" } }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, "depd": { "version": "1.1.1", @@ -1015,7 +1807,7 @@ "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", "requires": { - "readable-stream": "1.1.14", + "readable-stream": "1.1.x", "streamsearch": "0.1.2" }, "dependencies": { @@ -1029,10 +1821,10 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", "isarray": "0.0.1", - "string_decoder": "0.10.31" + "string_decoder": "~0.10.x" } }, "string_decoder": { @@ -1043,18 +1835,23 @@ } }, "diff": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz", - "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", "dev": true }, + "doctypes": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", + "integrity": "sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=" + }, "dom-serializer": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", "requires": { - "domelementtype": "1.1.3", - "entities": "1.1.1" + "domelementtype": "~1.1.1", + "entities": "~1.1.1" }, "dependencies": { "domelementtype": { @@ -1074,7 +1871,7 @@ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.1.tgz", "integrity": "sha1-iS5HAAqZvlW783dP/qBWHYh5wlk=", "requires": { - "domelementtype": "1.3.0" + "domelementtype": "1" } }, "domutils": { @@ -1082,50 +1879,180 @@ "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", "requires": { - "dom-serializer": "0.1.0", - "domelementtype": "1.3.0" + "dom-serializer": "0", + "domelementtype": "1" } }, + "dot-prop": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", + "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", + "requires": { + "is-obj": "^2.0.0" + } + }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "eachr": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/eachr/-/eachr-2.0.4.tgz", + "integrity": "sha1-Rm98qhBwj2EFCeMsgHqv5X/BIr8=", + "requires": { + "typechecker": "^2.0.8" + } + }, + "easymde": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/easymde/-/easymde-2.5.1.tgz", + "integrity": "sha512-8ESZCuareVshPdYcjoY5VnyUHMM/skTQDfmGzl23D+hcgww64DBIVQZWuu9htMGoZZGDVvCU/pYSqa3nZB7bmA==", + "requires": { + "codemirror": "^5.41.0", + "codemirror-spell-checker": "1.1.2", + "marked": "^0.5.1" + }, + "dependencies": { + "marked": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.5.2.tgz", + "integrity": "sha512-fdZvBa7/vSQIZCi4uuwo2N3q+7jJURpMVCcbaX0S1Mg65WZ5ilXvC67MviJAsdjqqgD+CEq4RKo5AYGgINkVAA==" + } + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "editions": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/editions/-/editions-1.3.4.tgz", + "integrity": "sha512-gzao+mxnYDzIysXKMQi/+M1mjy/rjestjg6OPoYTtI+3Izp23oiGZitsl9lPDPiTGXbcSIk1iJWhliSaglxnUg==" + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, + "email-templates": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/email-templates/-/email-templates-6.0.0.tgz", + "integrity": "sha512-NzneEyM+J/DpMY7hK4Ii1HBmiX/BTQyAf8OEZh1yU+O9uYMgnJr+JvpAxLkqRxeWeA0dT2IV5K+6UcF/jMJk7Q==", + "requires": { + "@ladjs/i18n": "^1.1.0", + "@sindresorhus/is": "^0.17.1", + "auto-bind": "^2.1.0", + "consolidate": "^0.15.1", + "debug": "^4.1.1", + "get-paths": "^0.0.4", + "html-to-text": "^5.1.1", + "juice": "^5.2.0", + "lodash": "^4.17.11", + "nodemailer": "^6.2.1", + "pify": "^4.0.1", + "preview-email": "^1.0.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "encodeurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", - "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, "end-of-stream": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.0.tgz", "integrity": "sha1-epDYM+/abPpurA9JSduw+tOmMgY=", "requires": { - "once": "1.4.0" + "once": "^1.4.0" } }, + "ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=" + }, "entities": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=" }, + "errlop": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/errlop/-/errlop-1.1.1.tgz", + "integrity": "sha512-WX7QjiPHhsny7/PQvrhS5VMizXXKoKCS3udaBp8gjlARdbn+XmK300eKBAAN0hGyRaTCtRpOaxK+xFVPUJ3zkw==", + "requires": { + "editions": "^2.1.2" + }, + "dependencies": { + "editions": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/editions/-/editions-2.1.3.tgz", + "integrity": "sha512-xDZyVm0A4nLgMNWVVLJvcwMjI80ShiH/27RyLiCnW1L273TcJIA25C4pwJ33AWV01OX6UriP35Xu+lH4S7HWQw==", + "requires": { + "errlop": "^1.1.1", + "semver": "^5.6.0" + } + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + } + } + }, "es5-ext": { - "version": "0.10.30", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.30.tgz", - "integrity": "sha1-cUGhaDZpfbq/qq7uQUlc4p9SyTk=", + "version": "0.10.45", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.45.tgz", + "integrity": "sha512-FkfM6Vxxfmztilbxxz5UKSD4ICMf5tSpRFtDNtkAhOxZ0EKtX6qwmXNyH/sFyIbX2P/nU5AMiA9jilWsUGJzCQ==", "requires": { - "es6-iterator": "2.0.1", - "es6-symbol": "3.1.1" + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.1", + "next-tick": "1" } }, "es6-iterator": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.1.tgz", - "integrity": "sha1-jjGcnwRTv1ddN0lAplWSDlnKVRI=", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", "requires": { - "d": "1.0.0", - "es5-ext": "0.10.30", - "es6-symbol": "3.1.1" + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" } }, "es6-promise": { @@ -1138,8 +2065,8 @@ "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", "requires": { - "d": "1.0.0", - "es5-ext": "0.10.30" + "d": "1", + "es5-ext": "~0.10.14" } }, "es6-weak-map": { @@ -1147,10 +2074,10 @@ "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", "requires": { - "d": "1.0.0", - "es5-ext": "0.10.30", - "es6-iterator": "2.0.1", - "es6-symbol": "3.1.1" + "d": "1", + "es5-ext": "^0.10.14", + "es6-iterator": "^2.0.1", + "es6-symbol": "^3.1.1" } }, "escape-html": { @@ -1171,8 +2098,7 @@ "esutils": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" }, "etag": { "version": "1.8.1", @@ -1184,56 +2110,149 @@ "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", "requires": { - "d": "1.0.0", - "es5-ext": "0.10.30" + "d": "1", + "es5-ext": "~0.10.14" } }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, "events": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" }, "express": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.16.1.tgz", - "integrity": "sha512-STB7LZ4N0L+81FJHGla2oboUHTk4PaN1RsOkoRh9OSeEKylvF5hwKYVX1xCLFaCT7MD0BNG/gX2WFMLqY6EMBw==", + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz", + "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=", "requires": { - "accepts": "1.3.4", + "accepts": "~1.3.5", "array-flatten": "1.1.1", "body-parser": "1.18.2", "content-disposition": "0.5.2", - "content-type": "1.0.4", + "content-type": "~1.0.4", "cookie": "0.3.1", "cookie-signature": "1.0.6", "debug": "2.6.9", - "depd": "1.1.1", - "encodeurl": "1.0.1", - "escape-html": "1.0.3", - "etag": "1.8.1", - "finalhandler": "1.1.0", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.1.1", "fresh": "0.5.2", "merge-descriptors": "1.0.1", - "methods": "1.1.2", - "on-finished": "2.3.0", - "parseurl": "1.3.2", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", "path-to-regexp": "0.1.7", - "proxy-addr": "2.0.2", + "proxy-addr": "~2.0.3", "qs": "6.5.1", - "range-parser": "1.2.0", + "range-parser": "~1.2.0", "safe-buffer": "5.1.1", - "send": "0.16.1", - "serve-static": "1.13.1", + "send": "0.16.2", + "serve-static": "1.13.2", "setprototypeof": "1.1.0", - "statuses": "1.3.1", - "type-is": "1.6.15", + "statuses": "~1.4.0", + "type-is": "~1.6.16", "utils-merge": "1.0.1", - "vary": "1.1.2" + "vary": "~1.1.2" }, "dependencies": { - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + "body-parser": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", + "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", + "requires": { + "bytes": "3.0.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.1", + "http-errors": "~1.6.2", + "iconv-lite": "0.4.19", + "on-finished": "~2.3.0", + "qs": "6.5.1", + "raw-body": "2.3.2", + "type-is": "~1.6.15" + } + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" + }, + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "requires": { + "mime-db": "~1.33.0" + } + }, + "raw-body": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", + "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.2", + "iconv-lite": "0.4.19", + "unpipe": "1.0.0" + }, + "dependencies": { + "depd": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", + "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" + }, + "http-errors": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", + "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "requires": { + "depd": "1.1.1", + "inherits": "2.0.3", + "setprototypeof": "1.0.3", + "statuses": ">= 1.3.1 < 2" + } + }, + "setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" + } + } + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + }, + "type-is": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", + "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.18" + } } } }, @@ -1246,13 +2265,18 @@ "cookie-signature": "1.0.6", "crc": "3.4.4", "debug": "2.6.9", - "depd": "1.1.1", - "on-headers": "1.0.1", - "parseurl": "1.3.2", - "uid-safe": "2.1.5", + "depd": "~1.1.1", + "on-headers": "~1.0.1", + "parseurl": "~1.3.2", + "uid-safe": "~2.1.5", "utils-merge": "1.0.1" }, "dependencies": { + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, "crc": { "version": "3.4.4", "resolved": "https://registry.npmjs.org/crc/-/crc-3.4.4.tgz", @@ -1266,23 +2290,75 @@ "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", "dev": true }, + "extendr": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/extendr/-/extendr-2.1.0.tgz", + "integrity": "sha1-MBqgu+pWX00tyPVw8qImEahSe1Y=", + "requires": { + "typechecker": "~2.0.1" + }, + "dependencies": { + "typechecker": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/typechecker/-/typechecker-2.0.8.tgz", + "integrity": "sha1-6D2oS7ZMWEzLNFg4V2xAsDN9uC4=" + } + } + }, + "extract-opts": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/extract-opts/-/extract-opts-2.2.0.tgz", + "integrity": "sha1-H6KOunNSxttID4hc63GkaBC+bX0=", + "requires": { + "typechecker": "~2.0.1" + }, + "dependencies": { + "typechecker": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/typechecker/-/typechecker-2.0.8.tgz", + "integrity": "sha1-6D2oS7ZMWEzLNFg4V2xAsDN9uC4=" + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "fast-text-encoding": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", - "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=" + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.0.tgz", + "integrity": "sha512-R9bHCvweUxxwkDwhjav5vxpFvdPGlVngtqmx4pIZfSUhM/Q4NiIUHB456BAf+Q1Nwu3HEZYONtu+Rya+af4jiQ==" }, "finalhandler": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", - "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", "requires": { "debug": "2.6.9", - "encodeurl": "1.0.1", - "escape-html": "1.0.3", - "on-finished": "2.3.0", - "parseurl": "1.3.2", - "statuses": "1.3.1", - "unpipe": "1.0.0" + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.4.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + } } }, "flat": { @@ -1290,18 +2366,23 @@ "resolved": "https://registry.npmjs.org/flat/-/flat-4.0.0.tgz", "integrity": "sha512-ji/WMv2jdsE+LaznpkIF9Haax0sdpTBozrz/Dtg4qSRMfbs8oVg4ypJunIRYPiMLvH/ed6OflXbnbTIKJhtgeg==", "requires": { - "is-buffer": "1.1.5" + "is-buffer": "~1.1.5" } }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, "form-data": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", "dev": true, "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.5", - "mime-types": "2.1.17" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.5", + "mime-types": "^2.1.12" } }, "format-util": { @@ -1310,9 +2391,9 @@ "integrity": "sha1-Ay3KShFiYqEsQ/TD7IVmQWxbLZU=" }, "formidable": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.1.1.tgz", - "integrity": "sha1-lriIb3w8NQi5Mta9cMTTqI818ak=", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", + "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==", "dev": true }, "forwarded": { @@ -1330,8 +2411,8 @@ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-2.1.2.tgz", "integrity": "sha1-BGxwFjzvmq1GsOSn+kZ/si1x3jU=", "requires": { - "graceful-fs": "4.1.11", - "jsonfile": "2.4.0" + "graceful-fs": "^4.1.2", + "jsonfile": "^2.1.0" } }, "fs.realpath": { @@ -1339,60 +2420,169 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, - "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", - "dev": true + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, - "git-rev-sync": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/git-rev-sync/-/git-rev-sync-1.9.1.tgz", - "integrity": "sha1-oMLj3TkqvPa3aWLif8dfsyI0Sc4=", + "gaxios": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-2.3.0.tgz", + "integrity": "sha512-VgC4JKJQAAAGK5rFZbPcS5mXsdIYVMIUJOxMjSOkYdfhB74R0L6y8PFQDdS0r1ObG6hdP11e71EjHh3xbI+6fQ==", + "requires": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^4.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.3.0" + }, + "dependencies": { + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + } + } + }, + "gcp-metadata": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-3.3.1.tgz", + "integrity": "sha512-RrASg1HaVAxoB9Q/8sYfJ++v9PMiiqIgOrOxZeagMgS4osZtICT1lKBx2uvzYgwetxj8i6K99Z0iuKMg7WraTg==", "requires": { - "escape-string-regexp": "1.0.5", - "graceful-fs": "4.1.11", - "shelljs": "0.7.7" + "gaxios": "^2.1.0", + "json-bigint": "^0.3.0" } }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "gcs-resumable-upload": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-2.3.2.tgz", + "integrity": "sha512-OPS0iAmPCV+r7PziOIhyxmQOzsazFCy76yYDOS/Z80O/7cuny1KMfqDQa2T0jLaL8EreTU7EMZG5pUuqBKgzHA==", "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" + "abort-controller": "^3.0.0", + "configstore": "^5.0.0", + "gaxios": "^2.0.0", + "google-auth-library": "^5.0.0", + "pumpify": "^2.0.0", + "stream-events": "^1.0.4" } }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" - }, - "graceful-readlink": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", "dev": true }, + "get-paths": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/get-paths/-/get-paths-0.0.4.tgz", + "integrity": "sha512-+AxlfMGN7FuJr2zhT6aErH08HMKkRwynTTHtWCenIWkIZgx2OlkZKgt7SM4+rh8Dfi32lo6HcvqeTLxph3kjQw==", + "requires": { + "bluebird": "^3.5.1", + "fs-extra": "^4.0.2" + }, + "dependencies": { + "fs-extra": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", + "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "^4.1.6" + } + } + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "git-rev-sync": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/git-rev-sync/-/git-rev-sync-1.12.0.tgz", + "integrity": "sha1-RGhAbH5sO6TPRYeZnhrbKNnRr1U=", + "requires": { + "escape-string-regexp": "1.0.5", + "graceful-fs": "4.1.11", + "shelljs": "0.7.7" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "google-auth-library": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-5.9.2.tgz", + "integrity": "sha512-rBE1YTOZ3/Hu6Mojkr+UUmbdc/F28hyMGYEGxjyfVA9ZFmq12oqS3AeftX4h9XpdVIcxPooSo8hECYGT6B9XqQ==", + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "fast-text-encoding": "^1.0.0", + "gaxios": "^2.1.0", + "gcp-metadata": "^3.3.0", + "gtoken": "^4.1.0", + "jws": "^4.0.0", + "lru-cache": "^5.0.0" + } + }, + "google-p12-pem": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-2.0.4.tgz", + "integrity": "sha512-S4blHBQWZRnEW44OcR7TL9WR+QCqByRvhNDZ/uuQfpxywfupikf/miba8js1jZi6ZOGv5slgSuoshCWh6EMDzg==", + "requires": { + "node-forge": "^0.9.0" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + }, "graphql": { "version": "0.10.5", "resolved": "https://registry.npmjs.org/graphql/-/graphql-0.10.5.tgz", "integrity": "sha512-Q7cx22DiLhwHsEfUnUip1Ww/Vfx7FS0w6+iHItNuN61+XpegHSa3k5U0+6M5BcpavQImBwFiy0z3uYwY7cXMLQ==", "requires": { - "iterall": "1.1.3" + "iterall": "^1.1.0" + } + }, + "graphql-extensions": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/graphql-extensions/-/graphql-extensions-0.0.10.tgz", + "integrity": "sha512-TnQueqUDCYzOSrpQb3q1ngDSP2otJSF+9yNLrQGPzkMsvnQ+v6e2d5tl+B35D4y+XpmvVnAn4T3ZK28mkILveA==", + "requires": { + "core-js": "^2.5.3", + "source-map-support": "^0.5.1" } }, "graphql-server-express": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/graphql-server-express/-/graphql-server-express-1.1.2.tgz", - "integrity": "sha1-boowamYWrHLHhQ76n9qcuimDYzU=", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/graphql-server-express/-/graphql-server-express-1.3.6.tgz", + "integrity": "sha1-Y0aVTE3qBnAi3ZMtgUVygDpA1Fs=", "requires": { - "apollo-server-express": "1.1.2" + "apollo-server-express": "^1.3.6" } }, "graphql-subscriptions": { @@ -1400,15 +2590,15 @@ "resolved": "https://registry.npmjs.org/graphql-subscriptions/-/graphql-subscriptions-0.4.4.tgz", "integrity": "sha512-hqfUsZv39qmK4SEoKMnTO05U4EVvIeAD4ai5ztE9gCl4hEdeaF2Q5gvF80ONQQAnkys4odzxWYd2tBLS/cWl8g==", "requires": { - "@types/graphql": "0.9.4", - "es6-promise": "4.1.1", - "iterall": "1.1.3" + "@types/graphql": "^0.9.1", + "es6-promise": "^4.0.5", + "iterall": "^1.1.1" }, "dependencies": { "es6-promise": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.1.1.tgz", - "integrity": "sha512-OaU1hHjgJf+b0NzsxCg7NdIYERD6Hy/PEmFLTjw+b65scuisG3Kt4QoTvJ66BBkPZ581gr0kpoVzKnxniM8nng==" + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" } } }, @@ -1417,59 +2607,162 @@ "resolved": "https://registry.npmjs.org/graphql-tools/-/graphql-tools-1.2.3.tgz", "integrity": "sha512-3inNK3rmk32G4hGWbqBuVNxusF+Mcuckg+3aD4hHaMxO0LrSgteWoTD8pTD9GUnmoSRG4AbYHZ0jibGD5MTlrQ==", "requires": { - "@types/graphql": "0.9.4", - "deprecated-decorator": "0.1.6", - "uuid": "3.0.1" + "@types/graphql": "^0.9.0", + "deprecated-decorator": "^0.1.6", + "uuid": "^3.0.1" } }, "graphql-typewriter": { "version": "git://github.com/illegalprime/graphql-typewriter.git#6c9e4c7490256cc1fcac7af32f18d471c0b375ca", + "from": "git://github.com/illegalprime/graphql-typewriter.git#hacks", "requires": { - "commander": "2.11.0", - "glob": "7.1.2", - "graphql": "0.10.5", - "graphql-subscriptions": "0.4.4", - "graphql-tools": "1.2.3", - "m-io": "0.5.0" + "commander": "^2.11.0", + "glob": "^7.1.2", + "graphql": "^0.10.5", + "graphql-subscriptions": "^0.4.4", + "graphql-tools": "^1.1.0", + "m-io": "^0.5.0" } }, "growl": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", - "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", "dev": true }, + "gtoken": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-4.1.4.tgz", + "integrity": "sha512-VxirzD0SWoFUo5p8RDP8Jt2AGyOmyYcT/pOUgDKJCK+iSw0TMqwrVfY37RXTNmoKwrzmDHSk0GMT9FsgVmnVSA==", + "requires": { + "gaxios": "^2.1.0", + "google-p12-pem": "^2.0.0", + "jws": "^4.0.0", + "mime": "^2.2.0" + }, + "dependencies": { + "mime": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", + "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==" + } + } + }, "handlebars": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.10.tgz", - "integrity": "sha1-PTDHGLCaPZbyPqTMH0A8TTup/08=", + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz", + "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==", + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", "requires": { - "async": "1.5.2", - "optimist": "0.6.1", - "source-map": "0.4.4", - "uglify-js": "2.8.29" + "ajv": "^6.5.5", + "har-schema": "^2.0.0" }, "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" + "ajv": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" } } }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, "has-ansi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, "requires": { - "ansi-regex": "2.1.1" + "ansi-regex": "^2.0.0" } }, "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "hash-stream-validation": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.2.tgz", + "integrity": "sha512-cMlva5CxWZOrlS/cY0C+9qAzesn5srhFA8IT1VPiHc9bWWBLkJfEUIZr7MWoi89oOOGmpg8ymchaOjiArsGu5A==", + "requires": { + "through2": "^2.0.0" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } + } }, "he": { "version": "1.1.1", @@ -1477,52 +2770,234 @@ "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", "dev": true }, - "hooks-fixed": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hooks-fixed/-/hooks-fixed-2.0.0.tgz", - "integrity": "sha1-oB2JTVKsf2WZu7H2PfycQR33DLo=" + "hoek": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-6.1.3.tgz", + "integrity": "sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ==" + }, + "html-to-text": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-5.1.1.tgz", + "integrity": "sha512-Bci6bD/JIfZSvG4s0gW/9mMKwBRoe/1RWLxUME/d6WUSZCdY7T60bssf/jFf7EYXRyqU4P5xdClVqiYU0/ypdA==", + "requires": { + "he": "^1.2.0", + "htmlparser2": "^3.10.1", + "lodash": "^4.17.11", + "minimist": "^1.2.0" + }, + "dependencies": { + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" + }, + "htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "requires": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "readable-stream": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", + "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "string_decoder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", + "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } }, "htmlparser2": { "version": "3.9.2", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz", "integrity": "sha1-G9+HrMoPP55T+k/M6w9LTLsAszg=", "requires": { - "domelementtype": "1.3.0", - "domhandler": "2.4.1", - "domutils": "1.5.1", - "entities": "1.1.1", - "inherits": "2.0.3", - "readable-stream": "2.3.3" + "domelementtype": "^1.3.0", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^2.0.2" } }, "http-errors": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", - "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", "requires": { - "depd": "1.1.1", + "depd": "~1.1.2", "inherits": "2.0.3", - "setprototypeof": "1.0.3", - "statuses": "1.3.1" + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "dependencies": { + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + } } }, + "http-proxy-agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.0.tgz", + "integrity": "sha512-GX0FA6+IcDf4Oxc/FBWgYj4zKgo/DnZrksaG9jyuQLExs6xlX+uI5lcA8ymM3JaZTRrF/4s2UX19wJolyo7OBA==", + "requires": { + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "agent-base": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.0.tgz", + "integrity": "sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw==", + "requires": { + "debug": "4" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-proxy-agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", + "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", + "requires": { + "agent-base": "5", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "human-interval": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/human-interval/-/human-interval-0.1.6.tgz", + "integrity": "sha1-AFeXNFR2TDq8vrKu1hL8lkTmhIg=" + }, + "i18n": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/i18n/-/i18n-0.8.3.tgz", + "integrity": "sha1-LYzxwkciYCwgQdAbpq5eqlE4jw4=", + "requires": { + "debug": "*", + "make-plural": "^3.0.3", + "math-interval-parser": "^1.1.0", + "messageformat": "^0.3.1", + "mustache": "*", + "sprintf-js": ">=1.0.3" + } + }, + "i18n-locales": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/i18n-locales/-/i18n-locales-0.0.2.tgz", + "integrity": "sha512-WCaJVIfU10v0/ZNy+mG7fCUQb1o2PsM7tNf1dUg0uU9OxtygDkWRqLT9Q/X30V2XsUb6XUEPbSsdUiORfDPVQA==" + }, "iconv-lite": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", - "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } }, "ieee754": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=" }, + "ignorefs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ignorefs/-/ignorefs-1.2.0.tgz", + "integrity": "sha1-2ln7hYl25KXkNwLM0fKC/byeV1Y=", + "requires": { + "editions": "^1.3.3", + "ignorepatterns": "^1.1.0" + } + }, + "ignorepatterns": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ignorepatterns/-/ignorepatterns-1.1.0.tgz", + "integrity": "sha1-rI9DbyI5td+2bV8NOpBKh6xnzF4=" + }, + "image-size": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.7.4.tgz", + "integrity": "sha512-GqPgxs+VkOr12aWwjSkyRzf5atzObWpFtiRuDgxCl2I/SDpZOKZFRD3iIAeAN6/usmn8SeLWRt7a8JRYK0Whbw==" + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" + "once": "^1.3.0", + "wrappy": "1" } }, "inherits": { @@ -1531,30 +3006,84 @@ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "interpret": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.0.4.tgz", - "integrity": "sha1-ggzdWIuGj/sZGoCVBtbJyPISsbA=" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", + "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=" }, "ipaddr.js": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.5.2.tgz", - "integrity": "sha1-1LUFvemUaYfM8PxY2QEP+WB+P6A=" + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz", + "integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs=" }, "is-buffer": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", - "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=" + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-expression": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-3.0.0.tgz", + "integrity": "sha1-Oayqa+f9HzRx3ELHQW5hwkMXrJ8=", + "requires": { + "acorn": "~4.0.2", + "object-assign": "^4.0.1" + }, + "dependencies": { + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" + } + } + }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" }, "is-promise": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "requires": { + "has": "^1.0.1" + } + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=" + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, "iterall": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.1.3.tgz", @@ -1565,6 +3094,11 @@ "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" }, + "js-stringify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", + "integrity": "sha1-Fzb939lyTyijaCrcYjCufk6Weds=" + }, "js-tokens": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", @@ -1576,21 +3110,39 @@ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", "requires": { - "argparse": "1.0.9", - "esprima": "4.0.0" + "argparse": "^1.0.7", + "esprima": "^4.0.0" } }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "json-bigint": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-0.3.0.tgz", + "integrity": "sha1-DM2RLEuCcNBfBW+9E4FLU9OCWx4=", + "requires": { + "bignumber.js": "^7.0.0" + } + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, "json-schema-ref-parser": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/json-schema-ref-parser/-/json-schema-ref-parser-3.3.1.tgz", "integrity": "sha512-stQTMhec2R/p2L9dH4XXRlpNCP0mY8QrLd/9Kl+8SHJQmwHtE1nDfXH4wbsSM+GkJMl8t92yZbI0OIol432CIQ==", "requires": { - "call-me-maybe": "1.0.1", - "debug": "3.1.0", - "es6-promise": "4.1.1", - "js-yaml": "3.10.0", - "ono": "4.0.2", - "z-schema": "3.18.4" + "call-me-maybe": "^1.0.1", + "debug": "^3.0.0", + "es6-promise": "^4.1.1", + "js-yaml": "^3.9.1", + "ono": "^4.0.2", + "z-schema": "^3.18.2" }, "dependencies": { "debug": { @@ -1602,23 +3154,23 @@ } }, "es6-promise": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.1.1.tgz", - "integrity": "sha512-OaU1hHjgJf+b0NzsxCg7NdIYERD6Hy/PEmFLTjw+b65scuisG3Kt4QoTvJ66BBkPZ581gr0kpoVzKnxniM8nng==" + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz", + "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==" } } }, "json-schema-to-typescript": { - "version": "4.6.4", - "resolved": "https://registry.npmjs.org/json-schema-to-typescript/-/json-schema-to-typescript-4.6.4.tgz", - "integrity": "sha512-XXa5Qt5yoRW9M/88b1+57LZ1Z76h5qXIjRQ0GxaRAoHE5uSzwFR/+plzv+SSJs6gsJedXiyuVuyeqPU8mkDsew==", - "requires": { - "cli-color": "1.2.0", - "json-schema-ref-parser": "3.3.1", - "json-stringify-safe": "5.0.1", - "lodash": "4.17.4", - "minimist": "1.2.0", - "mz": "2.7.0", + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/json-schema-to-typescript/-/json-schema-to-typescript-4.6.5.tgz", + "integrity": "sha512-fg6c/J9oWMnFUH3wAzyw78UXFZwswid+EPQdiRK42Zskm/GLemryXP4Vb4QAxBg/nhPrcCX2e2ckZH6JQJitmA==", + "requires": { + "cli-color": "^1.2.0", + "json-schema-ref-parser": "^3.3.1", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.4", + "minimist": "^1.2.0", + "mz": "^2.7.0", "stdin": "0.0.1" }, "dependencies": { @@ -1634,34 +3186,26 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" }, - "json-stable-stringify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", - "requires": { - "jsonify": "0.0.0" - } - }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, "json2csv": { - "version": "3.11.2", - "resolved": "https://registry.npmjs.org/json2csv/-/json2csv-3.11.2.tgz", - "integrity": "sha1-aoFm2jQ3UatOjDH4Q0F7rUTMgz8=", - "requires": { - "cli-table": "0.3.1", - "commander": "2.11.0", - "debug": "3.1.0", - "flat": "4.0.0", - "lodash.clonedeep": "4.5.0", - "lodash.flatten": "4.4.0", - "lodash.get": "4.4.2", - "lodash.set": "4.3.2", - "lodash.uniq": "4.5.0", - "path-is-absolute": "1.0.1" + "version": "3.11.5", + "resolved": "https://registry.npmjs.org/json2csv/-/json2csv-3.11.5.tgz", + "integrity": "sha512-ORsw84BuRKMLxfI+HFZuvxRDnsJps53D5fIGr6tLn4ZY+ymcG8XU00E+JJ2wfAiHx5w2QRNmOLE8xHiGAeSfuQ==", + "requires": { + "cli-table": "^0.3.1", + "commander": "^2.8.1", + "debug": "^3.1.0", + "flat": "^4.0.0", + "lodash.clonedeep": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.get": "^4.4.0", + "lodash.set": "^4.3.0", + "lodash.uniq": "^4.5.0", + "path-is-absolute": "^1.0.0" }, "dependencies": { "debug": { @@ -1674,150 +3218,213 @@ } } }, - "json3": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", - "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", - "dev": true - }, "jsonfile": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", "requires": { - "graceful-fs": "4.1.11" + "graceful-fs": "^4.1.6" + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "jstransformer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", + "integrity": "sha1-7Yvwkh4vPx7U1cGkT2hwntJHIsM=", + "requires": { + "is-promise": "^2.0.0", + "promise": "^7.0.1" + } + }, + "juice": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/juice/-/juice-5.2.0.tgz", + "integrity": "sha512-0l6GZmT3efexyaaay3SchKT5kG311N59TEFP5lfvEy0nz9SNqjx311plJ3b4jze7arsmDsiHQLh/xnAuk0HFTQ==", + "requires": { + "cheerio": "^0.22.0", + "commander": "^2.15.1", + "cross-spawn": "^6.0.5", + "deep-extend": "^0.6.0", + "mensch": "^0.3.3", + "slick": "^1.12.2", + "web-resource-inliner": "^4.3.1" + }, + "dependencies": { + "cheerio": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz", + "integrity": "sha1-qbqoYKP5tZWmuBsahocxIe06Jp4=", + "requires": { + "css-select": "~1.2.0", + "dom-serializer": "~0.1.0", + "entities": "~1.1.1", + "htmlparser2": "^3.9.1", + "lodash.assignin": "^4.0.9", + "lodash.bind": "^4.1.4", + "lodash.defaults": "^4.0.1", + "lodash.filter": "^4.4.0", + "lodash.flatten": "^4.2.0", + "lodash.foreach": "^4.3.0", + "lodash.map": "^4.4.0", + "lodash.merge": "^4.4.0", + "lodash.pick": "^4.2.1", + "lodash.reduce": "^4.4.0", + "lodash.reject": "^4.4.0", + "lodash.some": "^4.4.0" + } + }, + "commander": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==" + } + } + }, + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" } }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } }, "kareem": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/kareem/-/kareem-1.5.0.tgz", - "integrity": "sha1-4+QQHZ3P3imXadr0tNtk2JXRdEg=" + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.0.tgz", + "integrity": "sha512-6hHxsp9e6zQU8nXsP+02HGWXwTkOEw6IROhF2ZA28cYbUk4eJ6QbtZvdqZOdD9YPKghG3apk5eOCvs+tLl3lRg==" }, "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "requires": { - "is-buffer": "1.1.5" + "is-buffer": "^1.1.5" } }, "lazy-cache": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", - "optional": true + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=" }, "lazystream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", "requires": { - "readable-stream": "2.3.3" + "readable-stream": "^2.0.5" } }, "lodash": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", - "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" - }, - "lodash._baseassign": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", - "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", - "dev": true, - "requires": { - "lodash._basecopy": "3.0.1", - "lodash.keys": "3.1.2" - } + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" }, - "lodash._basecopy": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", - "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", - "dev": true - }, - "lodash._basecreate": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz", - "integrity": "sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=", - "dev": true + "lodash.assignin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz", + "integrity": "sha1-uo31+4QesKPoBEIysOJjqNxqKKI=" }, - "lodash._getnative": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", - "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", - "dev": true - }, - "lodash._isiterateecall": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", - "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", - "dev": true + "lodash.bind": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz", + "integrity": "sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU=" }, "lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" }, - "lodash.create": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz", - "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=", - "dev": true, - "requires": { - "lodash._baseassign": "3.2.0", - "lodash._basecreate": "3.0.3", - "lodash._isiterateecall": "3.0.9" - } + "lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" + }, + "lodash.filter": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", + "integrity": "sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4=" }, "lodash.flatten": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" }, + "lodash.foreach": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", + "integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=" + }, "lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" }, - "lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", - "dev": true - }, - "lodash.isarray": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", - "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", - "dev": true - }, "lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" }, - "lodash.keys": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", - "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", - "dev": true, - "requires": { - "lodash._getnative": "3.9.1", - "lodash.isarguments": "3.1.0", - "lodash.isarray": "3.0.4" - } + "lodash.map": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", + "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=" + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "lodash.pick": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", + "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=" + }, + "lodash.reduce": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", + "integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs=" + }, + "lodash.reject": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz", + "integrity": "sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU=" }, "lodash.set": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=" }, + "lodash.some": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", + "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=" + }, + "lodash.unescape": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz", + "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=" + }, "lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -1828,12 +3435,20 @@ "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "requires": { + "yallist": "^3.0.2" + } + }, "lru-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", "integrity": "sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=", "requires": { - "es5-ext": "0.10.30" + "es5-ext": "~0.10.2" } }, "m-io": { @@ -1841,14 +3456,53 @@ "resolved": "https://registry.npmjs.org/m-io/-/m-io-0.5.0.tgz", "integrity": "sha1-chuybdjUN13Oy1bY3AFm64vp0AM=", "requires": { - "fs-extra": "2.1.2", - "q": "1.5.0" + "fs-extra": "^2.0.0", + "q": "^1.4.1" + } + }, + "make-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", + "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "make-plural": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/make-plural/-/make-plural-3.0.6.tgz", + "integrity": "sha1-IDOgO6wpC487uRJY9lud9+iwHKc=", + "requires": { + "minimist": "^1.2.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "optional": true + } } }, "marked": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.6.tgz", - "integrity": "sha1-ssbGGPzOzk74bE/Gy4p8v1rtqNc=" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/marked/-/marked-1.1.1.tgz", + "integrity": "sha512-mJzT8D2yPxoPh7h0UXkB+dBj4FykPJ2OIfxAWeIHrvoHDkFxukV/29QxoFQoPM6RLEwhIFdJpmKBlqVM3s2ZIw==" + }, + "math-interval-parser": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-interval-parser/-/math-interval-parser-1.1.0.tgz", + "integrity": "sha1-2+2lsGsySZc8bfYXD94jhvCv2JM=", + "requires": { + "xregexp": "^2.0.0" + } }, "media-typer": { "version": "0.3.0", @@ -1856,25 +3510,67 @@ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, "memoizee": { - "version": "0.4.11", - "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.11.tgz", - "integrity": "sha1-vemBdmPJ5A/bKk6hw2cpYIeujI8=", + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.12.tgz", + "integrity": "sha512-sprBu6nwxBWBvBOh5v2jcsGqiGLlL2xr2dLub3vR8dnE8YB17omwtm/0NSHl8jjNbcsJd5GMWJAnTSVe/O0Wfg==", "requires": { - "d": "1.0.0", - "es5-ext": "0.10.30", - "es6-weak-map": "2.0.2", - "event-emitter": "0.3.5", - "is-promise": "2.1.0", - "lru-queue": "0.1.0", - "next-tick": "1.0.0", - "timers-ext": "0.1.2" + "d": "1", + "es5-ext": "^0.10.30", + "es6-weak-map": "^2.0.2", + "event-emitter": "^0.3.5", + "is-promise": "^2.1", + "lru-queue": "0.1", + "next-tick": "1", + "timers-ext": "^0.1.2" } }, + "memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "optional": true + }, + "mensch": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/mensch/-/mensch-0.3.3.tgz", + "integrity": "sha1-4gD/TdgjcX+OBWOzLj9UgfyiYrI=" + }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" }, + "messageformat": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/messageformat/-/messageformat-0.3.1.tgz", + "integrity": "sha1-5Y//gkXps5cXmeW0PbWLPpQX9aI=", + "requires": { + "async": "~1.5.2", + "glob": "~6.0.4", + "make-plural": "~3.0.3", + "nopt": "~3.0.6", + "watchr": "~2.4.13" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" + }, + "glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -1895,21 +3591,31 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", "requires": { - "mime-db": "1.30.0" + "mime-db": "~1.30.0" } }, + "mimer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mimer/-/mimer-1.0.0.tgz", + "integrity": "sha512-4ZJvCzfcwsBgPbkKXUzGoVZMWjv8IDIygkGzVc7uUYhgnK0t2LmGxxjdgH1i+pn0/KQfB5F/VKUJlfyTSOFQjg==" + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "requires": { - "brace-expansion": "1.1.8" + "brace-expansion": "^1.1.7" } }, "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "mkdirp": { "version": "0.5.1", @@ -1927,85 +3633,61 @@ } }, "mocha": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.5.3.tgz", - "integrity": "sha512-/6na001MJWEtYxHOV1WLfsmR4YIynkUEhBwzsb+fk2qmQ3iqsi258l/Q2MWHJMImAcNpZ8DEdYAK72NHoIQ9Eg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", "dev": true, "requires": { - "browser-stdout": "1.3.0", - "commander": "2.9.0", - "debug": "2.6.8", - "diff": "3.2.0", + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", "escape-string-regexp": "1.0.5", - "glob": "7.1.1", - "growl": "1.9.2", + "glob": "7.1.2", + "growl": "1.10.5", "he": "1.1.1", - "json3": "3.3.2", - "lodash.create": "3.1.1", + "minimatch": "3.0.4", "mkdirp": "0.5.1", - "supports-color": "3.1.2" + "supports-color": "5.4.0" }, "dependencies": { "commander": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", - "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", - "dev": true, - "requires": { - "graceful-readlink": "1.0.1" - } + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true }, "debug": { - "version": "2.6.8", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", - "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", "dev": true, "requires": { "ms": "2.0.0" } - }, - "glob": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", - "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=", - "dev": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", - "dev": true - }, - "supports-color": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz", - "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", - "dev": true, - "requires": { - "has-flag": "1.0.0" - } } } }, + "mock-require": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mock-require/-/mock-require-2.0.2.tgz", + "integrity": "sha1-HqpxqtIwE3c9En3H6Ro/u0g31g0=", + "dev": true, + "requires": { + "caller-id": "^0.1.0" + } + }, "moment": { - "version": "2.18.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.18.1.tgz", - "integrity": "sha1-w2GT3Tzhwu7SrbfIAtu8d6gbHA8=" + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz", + "integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=" }, "moment-timezone": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.13.tgz", - "integrity": "sha1-mc5cfYJyYusPH3AgRBd/YHRde5A=", + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.17.tgz", + "integrity": "sha512-Y/JpVEWIOA9Gho4vO15MTnW1FCmHi3ypprrkUaxsZ1TKg3uqC8q/qMBjTddkHoiwwZN3qvZSr4zJP7x9V3LpXA==", "requires": { - "moment": "2.18.1" + "moment": ">= 2.9.0" } }, "mongodb": { @@ -2023,13 +3705,13 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.7.tgz", "integrity": "sha1-BwV6y+JGeyIELTb5jFrVBwVOlbE=", "requires": { - "buffer-shims": "1.0.0", - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "string_decoder": "1.0.3", - "util-deprecate": "1.0.2" + "buffer-shims": "~1.0.0", + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~1.0.0", + "util-deprecate": "~1.0.1" } } } @@ -2039,76 +3721,127 @@ "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.15.tgz", "integrity": "sha1-hB9TuH//9MdFgYnDXIroJ+EWl2Q=", "requires": { - "bson": "1.0.4", - "require_optional": "1.0.1" + "bson": "~1.0.4", + "require_optional": "~1.0.0" } }, "mongoose": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-4.12.0.tgz", - "integrity": "sha1-ul3POxM4yEmC+XdMEMoEkxAs2DA=", - "requires": { - "async": "2.1.4", - "bson": "1.0.4", - "hooks-fixed": "2.0.0", - "kareem": "1.5.0", - "mongodb": "2.2.31", - "mpath": "0.3.0", - "mpromise": "0.5.5", - "mquery": "2.3.2", - "ms": "2.0.0", - "muri": "1.2.2", + "version": "5.4.19", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.4.19.tgz", + "integrity": "sha512-paRU3nbCrPIUVw1GAlxo11uIIqrYORctUx1kcLj7i2NhkxPQuy5OK2/FYj8+tglsaixycmONSyop2HQp1IUQSA==", + "requires": { + "async": "2.6.1", + "bson": "~1.1.0", + "kareem": "2.3.0", + "mongodb": "3.1.13", + "mongodb-core": "3.1.11", + "mongoose-legacy-pluralize": "1.0.2", + "mpath": "0.5.1", + "mquery": "3.2.0", + "ms": "2.1.1", "regexp-clone": "0.0.1", + "safe-buffer": "5.1.2", "sliced": "1.0.1" }, "dependencies": { "async": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.1.4.tgz", - "integrity": "sha1-LSFgx3iAMuTdbL4lAvH5osj2zeQ=", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", + "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", + "requires": { + "lodash": "^4.17.10" + } + }, + "bson": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.1.tgz", + "integrity": "sha512-jCGVYLoYMHDkOsbwJZBCqwMHyH4c+wzgI9hG7Z6SZJRXWr+x58pdIbm2i9a/jFGCkRJqRUr8eoI7lDWa0hTkxg==" + }, + "mongodb": { + "version": "3.1.13", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.1.13.tgz", + "integrity": "sha512-sz2dhvBZQWf3LRNDhbd30KHVzdjZx9IKC0L+kSZ/gzYquCF5zPOgGqRz6sSCqYZtKP2ekB4nfLxhGtzGHnIKxA==", + "requires": { + "mongodb-core": "3.1.11", + "safe-buffer": "^5.1.2" + } + }, + "mongodb-core": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-3.1.11.tgz", + "integrity": "sha512-rD2US2s5qk/ckbiiGFHeu+yKYDXdJ1G87F6CG3YdaZpzdOm5zpoAZd/EKbPmFO6cQZ+XVXBXBJ660sSI0gc6qg==", "requires": { - "lodash": "4.17.4" + "bson": "^1.1.0", + "require_optional": "^1.0.1", + "safe-buffer": "^5.1.2", + "saslprep": "^1.0.0" } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" } } }, + "mongoose-legacy-pluralize": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz", + "integrity": "sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ==" + }, "morgan": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.0.tgz", - "integrity": "sha1-0B+mxlhZt2/PMbPLU6OCGjEdgFE=", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.1.tgz", + "integrity": "sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA==", "requires": { - "basic-auth": "2.0.0", + "basic-auth": "~2.0.0", "debug": "2.6.9", - "depd": "1.1.1", - "on-finished": "2.3.0", - "on-headers": "1.0.1" + "depd": "~1.1.2", + "on-finished": "~2.3.0", + "on-headers": "~1.0.1" + }, + "dependencies": { + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + } } }, "mpath": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.3.0.tgz", - "integrity": "sha1-elj3iem1/TyUUgY0FXlg8mvV70Q=" - }, - "mpromise": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mpromise/-/mpromise-0.5.5.tgz", - "integrity": "sha1-9bJCWddjrMIlewoMjG2Gb9UXMuY=" + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.5.1.tgz", + "integrity": "sha512-H8OVQ+QEz82sch4wbODFOz+3YQ61FYz/z3eJ5pIdbMEaUzDqA268Wd+Vt4Paw9TJfvDgVKaayC0gBzMIw2jhsg==" }, "mquery": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-2.3.2.tgz", - "integrity": "sha512-KXWMypZSvhCuqRtza+HMQZdYw7PfFBjBTFvP31NNAq0OX0/NTIgpcDpkWQ2uTxk6vGQtwQ2elhwhs+ZvCA8OaA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-3.2.0.tgz", + "integrity": "sha512-qPJcdK/yqcbQiKoemAt62Y0BAc0fTEKo1IThodBD+O5meQRJT/2HSe5QpBNwaa4CjskoGrYWsEyjkqgiE0qjhg==", "requires": { - "bluebird": "3.5.0", - "debug": "2.6.9", + "bluebird": "3.5.1", + "debug": "3.1.0", "regexp-clone": "0.0.1", - "sliced": "0.0.5" + "safe-buffer": "5.1.2", + "sliced": "1.0.1" }, "dependencies": { - "sliced": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/sliced/-/sliced-0.0.5.tgz", - "integrity": "sha1-XtwETKTrb3gW1Qui/GPiXY/kcH8=" + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" } } }, @@ -2122,14 +3855,14 @@ "resolved": "https://registry.npmjs.org/multer/-/multer-1.3.0.tgz", "integrity": "sha1-CSsmcPaEb6SRSWXvyM+Uwg/sbNI=", "requires": { - "append-field": "0.1.0", - "busboy": "0.2.14", - "concat-stream": "1.6.0", - "mkdirp": "0.5.1", - "object-assign": "3.0.0", - "on-finished": "2.3.0", - "type-is": "1.6.15", - "xtend": "4.0.1" + "append-field": "^0.1.0", + "busboy": "^0.2.11", + "concat-stream": "^1.5.0", + "mkdirp": "^0.5.1", + "object-assign": "^3.0.0", + "on-finished": "^2.3.0", + "type-is": "^1.6.4", + "xtend": "^4.0.0" }, "dependencies": { "object-assign": { @@ -2139,19 +3872,19 @@ } } }, - "muri": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/muri/-/muri-1.2.2.tgz", - "integrity": "sha1-YxmBMmUNsIoEzHnM0A3Tia/SYxw=" + "mustache": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-3.0.1.tgz", + "integrity": "sha512-jFI/4UVRsRYdUbuDTKT7KzfOp7FiD5WzYmmwNwXyUVypC0xjoTL78Fqc0jHUPIvvGD+6DQSPHIt1NE7D1ArsqA==" }, "mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", "requires": { - "any-promise": "1.3.0", - "object-assign": "4.1.1", - "thenify-all": "1.6.0" + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" } }, "negotiator": { @@ -2159,22 +3892,50 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, "next-tick": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" + }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, + "node-forge": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.1.tgz", + "integrity": "sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ==" + }, "nodemailer": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-3.1.8.tgz", - "integrity": "sha1-/r+sy0vSc2eEc6MJxstLSi88SOM=" + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.2.1.tgz", + "integrity": "sha512-TagB7iuIi9uyNgHExo8lUDq3VK5/B0BpbkcjIgNvxbtVrjNqq0DwAOTuzALPVkK76kMhTSzIgHqg8X1uklVs6g==" + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "requires": { + "abbrev": "1" + } }, "normalize-path": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", "requires": { - "remove-trailing-separator": "1.1.0" + "remove-trailing-separator": "^1.0.1" } }, "nth-check": { @@ -2182,7 +3943,7 @@ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz", "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=", "requires": { - "boolbase": "1.0.0" + "boolbase": "~1.0.0" } }, "oauth": { @@ -2190,6 +3951,11 @@ "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", "integrity": "sha1-vR/vr2hslrdUda7VGWQS/2DPucE=" }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2213,24 +3979,31 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "requires": { - "wrappy": "1.0.2" + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "requires": { + "mimic-fn": "^2.1.0" } }, "ono": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/ono/-/ono-4.0.2.tgz", - "integrity": "sha512-EFXJFoeF+KkZW4lwmcPMKHp2ZU7o6CM+ccX2nPbEJKiJIdyqbIcS1v6pmNgeNJ6x4/vEYn0/8oz66qXSPnnmSQ==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/ono/-/ono-4.0.5.tgz", + "integrity": "sha512-ZVNuV9kJbr/2tWs83I2snrYo+WIS0DISF/xUfX9p9b6GyDD6F5N9PzHjW+p/dep6IGwSYylf1HCub5I/nM0R5Q==", "requires": { - "format-util": "1.0.3" + "format-util": "^1.0.3" } }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "open": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-6.4.0.tgz", + "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==", "requires": { - "minimist": "0.0.10", - "wordwrap": "0.0.3" + "is-wsl": "^1.1.0" } }, "os-tmpdir": { @@ -2238,76 +4011,43 @@ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, - "parse5": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.2.tgz", - "integrity": "sha1-Be/1fw70V3+xRKefi5qWemzERRA=", + "p-limit": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", + "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", "requires": { - "@types/node": "6.0.88" - }, - "dependencies": { - "@types/node": { - "version": "6.0.88", - "resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.88.tgz", - "integrity": "sha512-bYDPZTX0/s1aihdjLuAgogUAT5M+TpoWChEMea2p0yOcfn5bu3k6cJb9cp6nw268XeSNIGGr+4+/8V5K6BGzLQ==" - } + "p-try": "^2.0.0" } }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, "parseurl": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" }, "passport": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/passport/-/passport-0.3.2.tgz", - "integrity": "sha1-ndAJ+RXo/glbASSgG4+C2gdRAQI=", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.0.tgz", + "integrity": "sha1-xQlWkTR71a07XhgCOMORTRbwWBE=", "requires": { - "passport-strategy": "1.0.0", + "passport-strategy": "1.x.x", "pause": "0.0.1" } }, - "passport-facebook": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/passport-facebook/-/passport-facebook-2.1.1.tgz", - "integrity": "sha1-w50LUq5NWRYyRaTiGnubYyEwMxE=", - "requires": { - "passport-oauth2": "1.4.0" - } - }, - "passport-github2": { - "version": "0.1.11", - "resolved": "https://registry.npmjs.org/passport-github2/-/passport-github2-0.1.11.tgz", - "integrity": "sha1-yStW88OKROdmqsfp58E4TF6TyZk=", - "requires": { - "passport-oauth2": "1.4.0" - } - }, - "passport-google-oauth20": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-1.0.0.tgz", - "integrity": "sha1-O5YOih1w0dvnlGFcgnxoxAOSpdA=", - "requires": { - "passport-oauth2": "1.4.0" - } - }, - "passport-local": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", - "integrity": "sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=", - "requires": { - "passport-strategy": "1.0.0" - } - }, "passport-oauth2": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.4.0.tgz", - "integrity": "sha1-9i+BWDy+EmCb585vFguTlaJ7hq0=", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.5.0.tgz", + "integrity": "sha512-kqBt6vR/5VlCK8iCx1/KpY42kQ+NEHZwsSyt4Y6STiNjU+wWICG1i8ucc1FapXDGO15C5O5VZz7+7vRzrDPXXQ==", "requires": { - "oauth": "0.9.15", - "passport-strategy": "1.0.0", - "uid2": "0.0.3", - "utils-merge": "1.0.1" + "base64url": "3.x.x", + "oauth": "0.9.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" } }, "passport-strategy": { @@ -2320,6 +4060,11 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + }, "path-parse": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", @@ -2341,18 +4086,275 @@ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + }, + "preview-email": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/preview-email/-/preview-email-1.0.1.tgz", + "integrity": "sha512-GmkAoyraQg8Ypq6mFVePdFL1upN6X2PQn3tD7UJP/aUs8HamtRekFsMunu3TaextEKycixE+Aol4hsHtqoG1pg==", + "requires": { + "@babel/runtime": "^7.4.5", + "debug": "^4.1.1", + "moment": "^2.24.0", + "nodemailer": "^6.2.1", + "open": "^6.3.0", + "pify": "^4.0.1", + "pug": "^2.0.4", + "uuid": "^3.3.2" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + } + } + }, "process-nextick-args": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" }, + "promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "requires": { + "asap": "~2.0.3" + } + }, "proxy-addr": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.2.tgz", - "integrity": "sha1-ZXFQT0e7mI7IGAJT+F3X4UlSvew=", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz", + "integrity": "sha512-jQTChiCJteusULxjBp8+jftSQE5Obdl3k4cnmLA6WXtK6XFuWRnvVL7aCiBqaLPM8c4ph0S4tKna8XvmIwEnXQ==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.6.0" + } + }, + "psl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.2.0.tgz", + "integrity": "sha512-GEn74ZffufCmkDDLNcl3uuyF/aSD6exEyh1v/ZSdAomB82t6G9hzJVRx0jBmLDW+VfZqks3aScmMw9DszwUalA==" + }, + "pug": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pug/-/pug-2.0.4.tgz", + "integrity": "sha512-XhoaDlvi6NIzL49nu094R2NA6P37ijtgMDuWE+ofekDChvfKnzFal60bhSdiy8y2PBO6fmz3oMEIcfpBVRUdvw==", + "requires": { + "pug-code-gen": "^2.0.2", + "pug-filters": "^3.1.1", + "pug-lexer": "^4.1.0", + "pug-linker": "^3.0.6", + "pug-load": "^2.0.12", + "pug-parser": "^5.0.1", + "pug-runtime": "^2.0.5", + "pug-strip-comments": "^1.0.4" + } + }, + "pug-attrs": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-2.0.4.tgz", + "integrity": "sha512-TaZ4Z2TWUPDJcV3wjU3RtUXMrd3kM4Wzjbe3EWnSsZPsJ3LDI0F3yCnf2/W7PPFF+edUFQ0HgDL1IoxSz5K8EQ==", + "requires": { + "constantinople": "^3.0.1", + "js-stringify": "^1.0.1", + "pug-runtime": "^2.0.5" + } + }, + "pug-code-gen": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-2.0.3.tgz", + "integrity": "sha512-r9sezXdDuZJfW9J91TN/2LFbiqDhmltTFmGpHTsGdrNGp3p4SxAjjXEfnuK2e4ywYsRIVP0NeLbSAMHUcaX1EA==", + "requires": { + "constantinople": "^3.1.2", + "doctypes": "^1.1.0", + "js-stringify": "^1.0.1", + "pug-attrs": "^2.0.4", + "pug-error": "^1.3.3", + "pug-runtime": "^2.0.5", + "void-elements": "^2.0.1", + "with": "^5.0.0" + } + }, + "pug-error": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-1.3.3.tgz", + "integrity": "sha512-qE3YhESP2mRAWMFJgKdtT5D7ckThRScXRwkfo+Erqga7dyJdY3ZquspprMCj/9sJ2ijm5hXFWQE/A3l4poMWiQ==" + }, + "pug-filters": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-3.1.1.tgz", + "integrity": "sha512-lFfjNyGEyVWC4BwX0WyvkoWLapI5xHSM3xZJFUhx4JM4XyyRdO8Aucc6pCygnqV2uSgJFaJWW3Ft1wCWSoQkQg==", + "requires": { + "clean-css": "^4.1.11", + "constantinople": "^3.0.1", + "jstransformer": "1.0.0", + "pug-error": "^1.3.3", + "pug-walk": "^1.1.8", + "resolve": "^1.1.6", + "uglify-js": "^2.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "requires": { + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.10.0" + } + } + } + }, + "pug-lexer": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-4.1.0.tgz", + "integrity": "sha512-i55yzEBtjm0mlplW4LoANq7k3S8gDdfC6+LThGEvsK4FuobcKfDAwt6V4jKPH9RtiE3a2Akfg5UpafZ1OksaPA==", + "requires": { + "character-parser": "^2.1.1", + "is-expression": "^3.0.0", + "pug-error": "^1.3.3" + } + }, + "pug-linker": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-3.0.6.tgz", + "integrity": "sha512-bagfuHttfQOpANGy1Y6NJ+0mNb7dD2MswFG2ZKj22s8g0wVsojpRlqveEQHmgXXcfROB2RT6oqbPYr9EN2ZWzg==", + "requires": { + "pug-error": "^1.3.3", + "pug-walk": "^1.1.8" + } + }, + "pug-load": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-2.0.12.tgz", + "integrity": "sha512-UqpgGpyyXRYgJs/X60sE6SIf8UBsmcHYKNaOccyVLEuT6OPBIMo6xMPhoJnqtB3Q3BbO4Z3Bjz5qDsUWh4rXsg==", + "requires": { + "object-assign": "^4.1.0", + "pug-walk": "^1.1.8" + } + }, + "pug-parser": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-5.0.1.tgz", + "integrity": "sha512-nGHqK+w07p5/PsPIyzkTQfzlYfuqoiGjaoqHv1LjOv2ZLXmGX1O+4Vcvps+P4LhxZ3drYSljjq4b+Naid126wA==", + "requires": { + "pug-error": "^1.3.3", + "token-stream": "0.0.1" + } + }, + "pug-runtime": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-2.0.5.tgz", + "integrity": "sha512-P+rXKn9un4fQY77wtpcuFyvFaBww7/91f3jHa154qU26qFAnOe6SW1CbIDcxiG5lLK9HazYrMCCuDvNgDQNptw==" + }, + "pug-strip-comments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-1.0.4.tgz", + "integrity": "sha512-i5j/9CS4yFhSxHp5iKPHwigaig/VV9g+FgReLJWWHEHbvKsbqL0oP/K5ubuLco6Wu3Kan5p7u7qk8A4oLLh6vw==", + "requires": { + "pug-error": "^1.3.3" + } + }, + "pug-walk": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-1.1.8.tgz", + "integrity": "sha512-GMu3M5nUL3fju4/egXwZO0XLi6fW/K3T3VTgFQ14GxNi8btlxgT5qZL//JwZFm/2Fa64J/PNS8AZeys3wiMkVA==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", + "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", "requires": { - "forwarded": "0.1.2", - "ipaddr.js": "1.5.2" + "duplexify": "^4.1.1", + "inherits": "^2.0.3", + "pump": "^3.0.0" + }, + "dependencies": { + "duplexify": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", + "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "readable-stream": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.5.0.tgz", + "integrity": "sha512-gSz026xs2LfxBPudDuI41V1lka8cxg64E66SGe78zJlsUofOg/yqwezdIcdfwik6B4h8LFmWPA9ef9X3FiNFLA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + } } }, "punycode": { @@ -2361,9 +4363,14 @@ "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" }, "q": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.0.tgz", - "integrity": "sha1-3QG6ydBtMObyGa7LglPunr3DCPE=" + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" + }, + "qr-image": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/qr-image/-/qr-image-3.2.0.tgz", + "integrity": "sha1-n6gpW+rlDEoUnPn5CaHbRkqGcug=" }, "qs": { "version": "6.5.1", @@ -2386,13 +4393,13 @@ "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" }, "raw-body": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", - "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", + "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", "requires": { "bytes": "3.0.0", - "http-errors": "1.6.2", - "iconv-lite": "0.4.19", + "http-errors": "1.6.3", + "iconv-lite": "0.4.23", "unpipe": "1.0.0" } }, @@ -2401,13 +4408,13 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "safe-buffer": "5.1.1", - "string_decoder": "1.0.3", - "util-deprecate": "1.0.2" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.0.3", + "util-deprecate": "~1.0.1" } }, "rechoir": { @@ -2415,9 +4422,14 @@ "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", "requires": { - "resolve": "1.4.0" + "resolve": "^1.1.6" } }, + "regenerator-runtime": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz", + "integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA==" + }, "regexp-clone": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-0.0.1.tgz", @@ -2433,13 +4445,93 @@ "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "requires": { + "mime-db": "1.40.0" + } + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + } + } + }, "require_optional": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", "requires": { - "resolve-from": "2.0.0", - "semver": "5.4.1" + "resolve-from": "^2.0.0", + "semver": "^5.1.0" } }, "resolve": { @@ -2447,7 +4539,7 @@ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.4.0.tgz", "integrity": "sha512-aW7sVKPufyHqOmyyLzg/J+8606v5nevBgaliIlV7nUpVMsDnoBGV/cbSLNjZAg9q0Cfd/+easKVKQ8vOu8fn1Q==", "requires": { - "path-parse": "1.0.5" + "path-parse": "^1.0.5" } }, "resolve-from": { @@ -2455,13 +4547,36 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" }, + "retry-request": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.1.1.tgz", + "integrity": "sha512-BINDzVtLI2BDukjWmjAIRZ0oglnCAkpP2vQjM3jdLhmT62h0xnQgciPwBRDAvHqpkPT2Wo1XuUyLyn6nbGrZQQ==", + "requires": { + "debug": "^4.1.1", + "through2": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "right-align": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", - "optional": true, "requires": { - "align-text": "0.1.4" + "align-text": "^0.1.1" } }, "safe-buffer": { @@ -2469,73 +4584,160 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" }, + "safefs": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/safefs/-/safefs-3.2.2.tgz", + "integrity": "sha1-gXDBRE1wOOCMrqBaN0+uL6NJ4Vw=", + "requires": { + "graceful-fs": "*" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "saslprep": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.2.tgz", + "integrity": "sha512-4cDsYuAjXssUSjxHKRe4DTZC0agDwsCqcMqtJAQPzC74nJ7LfAJflAtC1Zed5hMzEQKj82d3tuzqdGNRsLJ4Gw==", + "optional": true, + "requires": { + "sparse-bitfield": "^3.0.3" + } + }, "sax": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" }, + "scandirectory": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/scandirectory/-/scandirectory-2.5.0.tgz", + "integrity": "sha1-bOA/VKCQtmjjy+2/IO354xBZPnI=", + "requires": { + "ignorefs": "^1.0.0", + "safefs": "^3.1.2", + "taskgroup": "^4.0.5" + } + }, "semver": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" }, "send": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.1.tgz", - "integrity": "sha512-ElCLJdJIKPk6ux/Hocwhk7NFHpI3pVm/IZOYWqUmoxcgeyM+MpxHHKhb8QmlJDX1pU6WrgaHBkVNm73Sv7uc2A==", + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", "requires": { "debug": "2.6.9", - "depd": "1.1.1", - "destroy": "1.0.4", - "encodeurl": "1.0.1", - "escape-html": "1.0.3", - "etag": "1.8.1", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", "fresh": "0.5.2", - "http-errors": "1.6.2", + "http-errors": "~1.6.2", "mime": "1.4.1", "ms": "2.0.0", - "on-finished": "2.3.0", - "range-parser": "1.2.0", - "statuses": "1.3.1" + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.4.0" + }, + "dependencies": { + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + } } }, "serve-static": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.1.tgz", - "integrity": "sha512-hSMUZrsPa/I09VYFJwa627JJkNs0NrfL1Uzuup+GqHfToR2KcsXFymXSV90hoyw3M+msjFuQly+YzIH/q0MGlQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", "requires": { - "encodeurl": "1.0.1", - "escape-html": "1.0.3", - "parseurl": "1.3.2", - "send": "0.16.1" + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.2", + "send": "0.16.2" } }, "setprototypeof": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", - "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" }, "shelljs": { "version": "0.7.7", "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.7.tgz", "integrity": "sha1-svXHfvlxSPS09uImguELuoZnz/E=", "requires": { - "glob": "7.1.2", - "interpret": "1.0.4", - "rechoir": "0.6.2" + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" } }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, "sliced": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" }, + "slick": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz", + "integrity": "sha1-vQSN23TefRymkV+qSldXCzVQwtc=" + }, + "snakeize": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/snakeize/-/snakeize-0.1.0.tgz", + "integrity": "sha1-EMCI2LWOsHazIpu1oE4jLOEmQi0=" + }, "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-support": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.6.tgz", + "integrity": "sha512-N4KXEz7jcKqPf2b2vZF11lQIz9W5ZMuUcIOGj243lduidkf2fjkVKJS9vNxVWn3u/uxX38AcE8U9nnH9FPcq+g==", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=", + "optional": true, "requires": { - "amdefine": "1.0.1" + "memory-pager": "^1.0.2" } }, "sprintf-js": { @@ -2543,16 +4745,51 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", + "dev": true + }, "statuses": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", - "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, "stdin": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/stdin/-/stdin-0.0.1.tgz", "integrity": "sha1-0wQZgarsPf28d6GzjWNy449ftx4=" }, + "stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "requires": { + "stubs": "^3.0.0" + } + }, + "stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" + }, "streamsearch": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", @@ -2563,39 +4800,43 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", "requires": { - "safe-buffer": "5.1.1" + "safe-buffer": "~5.1.0" } }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, "requires": { - "ansi-regex": "2.1.1" + "ansi-regex": "^2.0.0" } }, "striptags": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/striptags/-/striptags-3.1.0.tgz", - "integrity": "sha1-dj5TQzjZz1QvAEpLHrCZ4y0pXkQ=" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/striptags/-/striptags-3.1.1.tgz", + "integrity": "sha1-yMPn/db7S7OjKjt1LltePjgJPr0=" + }, + "stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=" }, "superagent": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.6.2.tgz", - "integrity": "sha512-eCWciyl+6YrBQmMBS54GTPsdDLhVV27URTDpvMFKXZDfzXgtU42KAUpo2GFkldWA/1tGnWbn5Xo1vO5RygVtew==", + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.2.tgz", + "integrity": "sha512-gVH4QfYHcY3P0f/BZzavLreHW3T1v7hG9B+hpMQotGQqurOvhv87GcMCd6LWySmBuf+BDR44TQd0aISjVHLeNQ==", "dev": true, "requires": { - "component-emitter": "1.2.1", - "cookiejar": "2.1.1", - "debug": "3.1.0", - "extend": "3.0.1", - "form-data": "2.3.1", - "formidable": "1.1.1", - "methods": "1.1.2", - "mime": "2.0.3", - "qs": "6.5.1", - "readable-stream": "2.3.3" + "component-emitter": "^1.2.0", + "cookiejar": "^2.1.0", + "debug": "^3.1.0", + "extend": "^3.0.0", + "form-data": "^2.3.1", + "formidable": "^1.1.1", + "methods": "^1.1.1", + "mime": "^1.4.1", + "qs": "^6.5.1", + "readable-stream": "^2.0.5" }, "dependencies": { "debug": { @@ -2606,47 +4847,69 @@ "requires": { "ms": "2.0.0" } - }, - "mime": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.0.3.tgz", - "integrity": "sha512-TrpAd/vX3xaLPDgVRm6JkZwLR0KHfukMdU2wTEbqMDdCnY6Yo3mE+mjs9YE6oMNw2QRfXVeBEYpmpO94BIqiug==", - "dev": true } } }, "supertest": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-3.0.0.tgz", - "integrity": "sha1-jUu2j9GDDuBwM7HFpamkAhyWUpY=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-3.1.0.tgz", + "integrity": "sha512-O44AMnmJqx294uJQjfUmEyYOg7d9mylNFsMw/Wkz4evKd1njyPrtCN+U6ZIC7sKtfEVQhfTqFFijlXx8KP/Czw==", "dev": true, "requires": { - "methods": "1.1.2", - "superagent": "3.6.2" + "methods": "~1.1.2", + "superagent": "3.8.2" } }, "supports-color": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", - "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", "requires": { - "has-flag": "2.0.0" + "has-flag": "^3.0.0" } }, "sweetalert2": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-6.10.1.tgz", - "integrity": "sha512-GNZi9AUpcIz5xdjK17WjlYazjs4sI3PugCAP2T5iZiIOa0oBYIycV6v8YV60UBpatEPHHwl5eA05A6+imtN+4g==" + "version": "6.11.5", + "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-6.11.5.tgz", + "integrity": "sha512-8Otu1SlWGS/u3e31cOg+uqrwyoQbByEScKp7UupmCfwEZE9St3coO1e6CXv83YzZtzdDgowdK1hBPKPW7SRp4Q==" }, "tar-stream": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.5.4.tgz", "integrity": "sha1-NlSc8E7RrumyowwBQyUiONr5QBY=", "requires": { - "bl": "1.2.1", - "end-of-stream": "1.4.0", - "readable-stream": "2.3.3", - "xtend": "4.0.1" + "bl": "^1.0.0", + "end-of-stream": "^1.0.0", + "readable-stream": "^2.0.0", + "xtend": "^4.0.0" + } + }, + "taskgroup": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/taskgroup/-/taskgroup-4.3.1.tgz", + "integrity": "sha1-feGT/r12gnPEV3MElwJNUSwnkVo=", + "requires": { + "ambi": "^2.2.0", + "csextends": "^1.0.3" + } + }, + "teeny-request": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-6.0.1.tgz", + "integrity": "sha512-TAK0c9a00ELOqLrZ49cFxvPVogMUFaWY8dUsQc/0CuQPGF+BOxOQzXfE413BAk2kLomwNplvdtMpeaeGWmoc2g==", + "requires": { + "http-proxy-agent": "^4.0.0", + "https-proxy-agent": "^4.0.0", + "node-fetch": "^2.2.0", + "stream-events": "^1.0.5", + "uuid": "^3.3.2" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + } } }, "thenify": { @@ -2654,7 +4917,7 @@ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz", "integrity": "sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=", "requires": { - "any-promise": "1.3.0" + "any-promise": "^1.0.0" } }, "thenify-all": { @@ -2662,16 +4925,24 @@ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", "requires": { - "thenify": "3.3.0" + "thenify": ">= 3.1.0 < 4" + } + }, + "through2": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz", + "integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==", + "requires": { + "readable-stream": "2 || 3" } }, "timers-ext": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.2.tgz", - "integrity": "sha1-YcxHp2wavTGV8UUn+XjViulMUgQ=", + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.5.tgz", + "integrity": "sha512-tsEStd7kmACHENhsUPaxb8Jf8/+GZZxyNFQbZD07HQOyooOa6At1rQqjffgvg7n+dxscQa9cjjMdWhJtsP2sxg==", "requires": { - "es5-ext": "0.10.30", - "next-tick": "1.0.0" + "es5-ext": "~0.10.14", + "next-tick": "1" } }, "tmp": { @@ -2679,56 +4950,101 @@ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", "requires": { - "os-tmpdir": "1.0.2" + "os-tmpdir": "~1.0.2" + } + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" + }, + "token-stream": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-0.0.1.tgz", + "integrity": "sha1-zu78cXp2xDFvEm0LnbqlXX598Bo=" + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + } } }, "tslib": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.7.1.tgz", - "integrity": "sha1-vIAEFkaRkjp5/oN4u+s9ogF1OOw=", + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", "dev": true }, "tslint": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.7.0.tgz", - "integrity": "sha1-wl4NDJL6EgHCvDDoROCOaCtPNVI=", + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.14.0.tgz", + "integrity": "sha512-IUla/ieHVnB8Le7LdQFRGlVJid2T/gaJe5VkjzRVSRR6pA2ODYrnfR1hmxi+5+au9l50jBwpbBL34txgv4NnTQ==", "dev": true, "requires": { - "babel-code-frame": "6.26.0", - "colors": "1.1.2", - "commander": "2.11.0", - "diff": "3.2.0", - "glob": "7.1.2", - "minimatch": "3.0.4", - "resolve": "1.4.0", - "semver": "5.4.1", - "tslib": "1.7.1", - "tsutils": "2.11.2" + "babel-code-frame": "^6.22.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^3.2.0", + "glob": "^7.1.1", + "js-yaml": "^3.7.0", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.8.0", + "tsutils": "^2.29.0" }, "dependencies": { - "colors": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", - "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "commander": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", + "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", "dev": true } } }, - "tslint-language-service": { - "version": "0.9.6", - "resolved": "https://registry.npmjs.org/tslint-language-service/-/tslint-language-service-0.9.6.tgz", - "integrity": "sha1-iCuwcEz4OXszLdwK9xaMfyLyeeo=", - "dev": true + "tslint-language-service-ts3": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tslint-language-service-ts3/-/tslint-language-service-ts3-1.0.0.tgz", + "integrity": "sha512-SE1QymT9i0bpKmDEiba+abgp8SUuxayM1sWZsrR9ffouiV2CtkB4GdGC/eFxp4rCPXuUXsdsVDxQBHNXSuah7A==", + "dev": true, + "requires": { + "mock-require": "^2.0.2" + } }, "tsutils": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.11.2.tgz", - "integrity": "sha1-YBNgHjb6FP+VhBPlQdQn+4xqw0E=", + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", "dev": true, "requires": { - "tslib": "1.7.1" + "tslib": "^1.8.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" } }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, "type-detect": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.3.tgz", @@ -2741,38 +5057,43 @@ "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", "requires": { "media-typer": "0.3.0", - "mime-types": "2.1.17" + "mime-types": "~2.1.15" } }, + "typechecker": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/typechecker/-/typechecker-2.1.0.tgz", + "integrity": "sha1-0cIJOlT/ihn1jP+HfuqlTyJC04M=" + }, "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "requires": { + "is-typedarray": "^1.0.0" + } + }, "typescript": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.5.3.tgz", - "integrity": "sha512-ptLSQs2S4QuS6/OD1eAKG+S5G8QQtrU5RT32JULdZQtM1L3WTi34Wsu48Yndzi8xsObRAB9RPt/KhA9wlpEF6w==", + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", + "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==", "dev": true }, + "typo-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.0.3.tgz", + "integrity": "sha1-VNjrx5SfGngQkItgAsaEFSbJnVo=" + }, "uglify-js": { - "version": "2.8.29", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", - "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", - "optional": true, - "requires": { - "source-map": "0.5.7", - "uglify-to-browserify": "1.0.2", - "yargs": "3.10.0" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "optional": true - } - } + "version": "3.10.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.10.4.tgz", + "integrity": "sha512-kBFT3U4Dcj4/pJ52vfjCSfyLyvG9VYYuGYPmrPvAxRw/i7xHiT4VvCev+uiEMcEEiu6UNB6KgWmGtSUYIWScbw==", + "optional": true }, "uglify-to-browserify": { "version": "1.0.2", @@ -2785,7 +5106,7 @@ "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", "requires": { - "random-bytes": "1.0.0" + "random-bytes": "~1.0.0" } }, "uid2": { @@ -2793,11 +5114,58 @@ "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", "integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=" }, + "underscore": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", + "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=" + }, + "underscore.deep": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/underscore.deep/-/underscore.deep-0.5.1.tgz", + "integrity": "sha1-ByZx9I1oc1w0Ij/P72PmnlJ2zCs=" + }, + "underscore.string": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.5.tgz", + "integrity": "sha512-g+dpmgn+XBneLmXXo+sGlW5xQEt4ErkS3mgeN2GFbremYeMBSJKr9Wf2KJplQVaiPY/f7FN6atosWYNm9ovrYg==", + "requires": { + "sprintf-js": "^1.0.3", + "util-deprecate": "^1.0.2" + } + }, + "unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "requires": { + "crypto-random-string": "^2.0.0" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + } + } + }, "url": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", @@ -2818,53 +5186,160 @@ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, "uuid": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz", - "integrity": "sha1-ZUS7ot/ajBzxfmKaOjBeK7H+5sE=" + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" + }, + "valid-data-url": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-0.1.6.tgz", + "integrity": "sha512-FXg2qXMzfAhZc0y2HzELNfUeiOjPr+52hU1DNBWiJJ2luXD+dD1R9NA48Ug5aj0ibbxroeGDc/RJv6ThiGgkDw==" }, "validator": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-8.2.0.tgz", - "integrity": "sha512-Yw5wW34fSv5spzTXNkokD6S6/Oq92d8q/t14TqsS3fAiA1RYnxSFSIZ+CY3n6PGGRCq5HhJTSepQvFUS2QUDxA==" + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-10.3.0.tgz", + "integrity": "sha512-bn7dcJcdkpSjcujYlf8lrY9VL660h5njEkFzQzQOFMQgJ3Id1C4+MkazHKgHE45NoGsyQYEPmo4dCIbDQ7eTdw==" }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" + }, "walkdir": { "version": "0.0.11", "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.0.11.tgz", "integrity": "sha1-oW0CXrkxvQO1LzCMrtD0D86+lTI=" }, + "watchr": { + "version": "2.4.13", + "resolved": "https://registry.npmjs.org/watchr/-/watchr-2.4.13.tgz", + "integrity": "sha1-10hHu01vkPYf4sdPn2hmKqDgdgE=", + "requires": { + "eachr": "^2.0.2", + "extendr": "^2.1.0", + "extract-opts": "^2.2.0", + "ignorefs": "^1.0.0", + "safefs": "^3.1.2", + "scandirectory": "^2.5.0", + "taskgroup": "^4.2.0", + "typechecker": "^2.0.8" + } + }, + "web-resource-inliner": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-4.3.2.tgz", + "integrity": "sha512-eVnNqwG20sbAgqv2JONwyr57UNZFJP4oauioeUjpCMY83AM11956eIhxlCGGXfSMi7bRBjR9Vao05bXFzslh7w==", + "requires": { + "async": "^2.1.2", + "chalk": "^1.1.3", + "datauri": "^2.0.0", + "htmlparser2": "^3.9.2", + "lodash.unescape": "^4.0.1", + "request": "^2.78.0", + "safer-buffer": "^2.1.2", + "valid-data-url": "^0.1.4", + "xtend": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, "whatwg-fetch": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz", - "integrity": "sha1-nITsLc9oGH/wC8ZOEnS0QhduHIQ=" + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz", + "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==" + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } }, "window-size": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", - "optional": true + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" + }, + "with": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/with/-/with-5.1.1.tgz", + "integrity": "sha1-+k2qktrzLE6pTtRTyB8EaGtXXf4=", + "requires": { + "acorn": "^3.1.0", + "acorn-globals": "^3.0.0" + } }, "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, + "write-file-atomic": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.1.tgz", + "integrity": "sha512-JPStrIyyVJ6oCSz/691fAjFtefZ6q+fP6tm+OS4Qw6o+TGQxNp1ziY2PgS+X/m0V8OWhZiO/m4xSj+Pr4RrZvw==", + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==" + }, "xml2js": { "version": "0.4.17", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.17.tgz", "integrity": "sha1-F76T6q4/O3eTWceVtBlwWogX6Gg=", "requires": { - "sax": "1.2.1", - "xmlbuilder": "4.2.1" + "sax": ">=0.6.0", + "xmlbuilder": "^4.1.0" } }, "xmlbuilder": { @@ -2872,35 +5347,44 @@ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-4.2.1.tgz", "integrity": "sha1-qlijBBoGb5DqoWwvU4n/GfP0YaU=", "requires": { - "lodash": "4.17.4" + "lodash": "^4.0.0" } }, + "xregexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", + "integrity": "sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM=" + }, "xtend": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, "yargs": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", - "optional": true, "requires": { - "camelcase": "1.2.1", - "cliui": "2.1.0", - "decamelize": "1.2.0", + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", "window-size": "0.1.0" } }, "z-schema": { - "version": "3.18.4", - "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-3.18.4.tgz", - "integrity": "sha512-DUOKC/IhbkdLKKiV89gw9DUauTV8U/8yJl1sjf6MtDmzevLKOF2duNJ495S3MFVjqZarr+qNGCPbkg4mu4PpLw==", + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-3.22.0.tgz", + "integrity": "sha512-Oq82unxX2PTcJ031gFGcksDHE5PNBs5CbcQ1tbre0Sl4Mu5habZTVmEAkuZS4cK//VgIdNg9UG9PMgMlN6KmiA==", "requires": { - "commander": "2.11.0", - "lodash.get": "4.4.2", - "lodash.isequal": "4.5.0", - "validator": "8.2.0" + "commander": "^2.7.1", + "lodash.get": "^4.0.0", + "lodash.isequal": "^4.0.0", + "validator": "^10.0.0" } }, "zip-stream": { @@ -2908,10 +5392,10 @@ "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-1.2.0.tgz", "integrity": "sha1-qLxF9MG0lpnGuQGYuqyqzbzUugQ=", "requires": { - "archiver-utils": "1.3.0", - "compress-commons": "1.2.0", - "lodash": "4.17.4", - "readable-stream": "2.3.3" + "archiver-utils": "^1.3.0", + "compress-commons": "^1.2.0", + "lodash": "^4.8.0", + "readable-stream": "^2.0.0" } } } diff --git a/package.json b/package.json index b93581e7..a4e5ce9d 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,16 @@ { "name": "registration", - "version": "1.11.7", - "description": "TBD", + "version": "3.4.0", + "description": "Powerful and extensible registration system for hackathons and other large events", "main": "server/app.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "bash ./build.sh", "quickbuild": "./node_modules/typescript/bin/tsc -p server/ && ./node_modules/typescript/bin/tsc -p client/", "start": "node server/app.js", - "build-start": "npm run build && npm start" + "build-start": "npm run build && npm start", + "qb-start": "npm run quickbuild && npm start", + "debug": "node --nolazy --inspect=9230 server/app.js" }, "author": { "name": "Ryan Petschek", @@ -26,83 +28,89 @@ "homepage": "https://github.com/HackGT/registration#readme", "private": true, "dependencies": { - "@types/uuid": "^3.4.2", - "ajv": "^5.1.6", + "@google-cloud/storage": "^4.3.0", + "@handlebars/allow-prototype-access": "^1.0.3", + "@sendgrid/mail": "^6.2.1", + "agenda": "^2.0.2", + "ajv": "^5.5.2", "archiver": "^1.3.0", - "aws-sdk": "^2.83.0", - "body-parser": "^1.17.1", - "bowser": "^1.7.3", - "chalk": "^2.1.0", - "cheerio": "^1.0.0-rc.2", - "compression": "^1.6.2", + "aws-sdk": "^2.255.1", + "body-parser": "^1.18.3", + "bowser": "^1.9.3", + "chalk": "^2.4.1", + "compression": "^1.7.2", "connect-flash": "^0.1.1", - "connect-mongo": "^1.3.2", + "connect-mongo": "^2.0.1", "cookie-parser": "^1.4.3", - "cookie-signature": "^1.0.6", - "express": "^4.15.2", + "cookie-signature": "^1.1.0", + "easymde": "^2.5.1", + "email-templates": "^6.0.0", + "express": "^4.16.3", "express-session": "^1.15.1", - "git-rev-sync": "^1.8.0", + "git-rev-sync": "^1.12.0", "graphql": "^0.10.5", - "graphql-server-express": "^1.1.0", + "graphql-server-express": "^1.3.6", "graphql-tools": "^1.2.2", "graphql-typewriter": "git://github.com/illegalprime/graphql-typewriter.git#hacks", - "handlebars": "^4.0.6", - "json-schema-to-typescript": "^4.4.0", - "json2csv": "^3.7.3", - "marked": "^0.3.6", - "moment": "^2.18.1", - "moment-timezone": "^0.5.13", - "mongoose": "^4.10.3", - "morgan": "^1.8.2", + "handlebars": "^4.7.6", + "html-to-text": "^5.1.1", + "json-schema-to-typescript": "^4.6.5", + "json2csv": "^3.11.5", + "marked": "^1.1.1", + "moment": "^2.22.2", + "moment-timezone": "^0.5.17", + "mongoose": "^5.4.19", + "morgan": "^1.9.1", "multer": "^1.3.0", - "nodemailer": "^3.1.7", - "passport": "^0.3.2", - "passport-facebook": "^2.1.1", - "passport-github2": "^0.1.10", - "passport-google-oauth20": "^1.0.0", - "passport-local": "^1.0.0", - "serve-static": "^1.12.1", - "striptags": "^3.0.1", - "sweetalert2": "^6.4.4", + "passport": "^0.4.0", + "passport-oauth2": "^1.5.0", + "qr-image": "^3.2.0", + "request": "^2.88.0", + "serve-static": "^1.13.2", + "striptags": "^3.1.1", + "sweetalert2": "^6.11.5", "tmp": "0.0.33", - "uuid": "^3.1.0", - "whatwg-fetch": "^2.0.3" + "uuid": "^3.2.1", + "whatwg-fetch": "^2.0.4" }, "devDependencies": { + "@types/agenda": "^2.0.5", "@types/archiver": "^0.15.37", "@types/aws-sdk": "^2.7.0", "@types/body-parser": "1.16.0", "@types/chai": "^3.4.35", - "@types/chalk": "^0.4.31", - "@types/cheerio": "^0.22.2", + "@types/codemirror": "0.0.71", "@types/compression": "0.0.33", "@types/connect-flash": "0.0.31", "@types/connect-mongo": "0.0.32", "@types/cookie-parser": "^1.4.0", - "@types/cookie-signature": "^1.0.0", - "@types/express": "^4.0.35", + "@types/cookie-signature": "^1.0.1", + "@types/express": "^4.16.0", "@types/express-session": "0.0.32", - "@types/handlebars": "^4.0.32", - "@types/marked": "0.0.28", - "@types/mocha": "^2.2.40", - "@types/moment-timezone": "^0.2.34", - "@types/mongoose": "^4.7.18", - "@types/morgan": "^1.7.32", - "@types/multer": "^1.3.2", - "@types/node": "^8.0.8", - "@types/nodemailer": "^1.3.32", - "@types/passport": "^0.3.3", - "@types/passport-facebook": "^2.1.3", - "@types/passport-local": "^1.0.29", - "@types/serve-static": "^1.7.31", - "@types/supertest": "^2.0.0", + "@types/handlebars": "^4.0.38", + "@types/html-to-text": "^1.4.31", + "@types/marked": "1.1.0", + "@types/mocha": "^2.2.48", + "@types/moment-timezone": "^0.5.6", + "@types/mongodb": "^3.1.22", + "@types/mongoose": "^5.3.23", + "@types/morgan": "^1.7.35", + "@types/multer": "^1.3.6", + "@types/node": "^11.11.3", + "@types/passport": "^1.0.0", + "@types/passport-oauth2": "^1.4.8", + "@types/qr-image": "^3.2.1", + "@types/request": "^2.48.2", + "@types/serve-static": "^1.13.2", + "@types/supertest": "^2.0.4", "@types/tmp": "0.0.33", + "@types/uuid": "^3.4.3", "@types/whatwg-fetch": "0.0.33", "chai": "^4.0.0", - "mocha": "^3.4.2", - "supertest": "^3.0.0", - "tslint": "^5.4.3", - "tslint-language-service": "^0.9.6", - "typescript": "^2.5.2" + "mocha": "^5.2.0", + "supertest": "^3.1.0", + "tslint": "^5.14.0", + "tslint-language-service-ts3": "^1.0.0", + "typescript": "^3.9.7" } } diff --git a/quick-build.sh b/quick-build.sh new file mode 100644 index 00000000..dc25396a --- /dev/null +++ b/quick-build.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +SOURCE_DIR=$(readlink -f "${BASH_SOURCE[0]}") +cd "$(dirname "$SOURCE_DIR")" || exit +set -xeuo pipefail + +node node_modules/json-schema-to-typescript/dist/src/cli.js \ + server/config/questions.schema.json \ + > server/config/questions.schema.d.ts + +./node_modules/.bin/graphql-typewriter -i ./api.graphql +mv ./api.graphql.types.ts ./server/routes/api/ + +./node_modules/typescript/bin/tsc -p server/ +./node_modules/typescript/bin/tsc -p client/ diff --git a/server/app.ts b/server/app.ts index f6a51b16..1505bef2 100644 --- a/server/app.ts +++ b/server/app.ts @@ -41,16 +41,16 @@ morgan.token("sessionid", (request, response) => { morgan.format("hackgt", (tokens, request, response) => { let statusColorizer: (input: string) => string = input => input; // Default passthrough function if (response.statusCode >= 500) { - statusColorizer = chalk.red; + statusColorizer = chalk.default.red; } - if (response.statusCode >= 400) { - statusColorizer = chalk.yellow; + else if (response.statusCode >= 400) { + statusColorizer = chalk.default.yellow; } - if (response.statusCode >= 300) { - statusColorizer = chalk.cyan; + else if (response.statusCode >= 300) { + statusColorizer = chalk.default.cyan; } - if (response.statusCode >= 200) { - statusColorizer = chalk.green; + else if (response.statusCode >= 200) { + statusColorizer = chalk.default.green; } return [ @@ -100,12 +100,13 @@ 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); -import {adminRoutes} from "./routes/api/admin"; -apiRouter.use("/admin", adminRoutes); +apiRouter.use("/registration", registrationRoutes); import {settingsRoutes} from "./routes/api/settings"; apiRouter.use("/settings", settingsRoutes); +import {helpScoutRoutes} from "./routes/api/helpscout"; +apiRouter.use("/helpscout", helpScoutRoutes); app.use("/api", apiRouter); diff --git a/server/branch.ts b/server/branch.ts index c08fa322..fec60198 100644 --- a/server/branch.ts +++ b/server/branch.ts @@ -1,3 +1,4 @@ +/* tslint:disable:no-use-before-declare */ import * as fs from "fs"; import * as path from "path"; import * as ajv from "ajv"; @@ -27,12 +28,12 @@ Branches.forEach(branch => { interface Labels { [questionName: string]: string; } -interface QuestionBranchTypes { - "Application": ApplicationBranch; - "Confirmation": ConfirmationBranch; - "Noop": NoopBranch; -} -type QuestionBranch = QuestionBranchTypes[keyof QuestionBranchTypes]; + +type BranchTypeAsString = + T extends ApplicationBranch ? "Application" : + T extends ConfirmationBranch ? "Confirmation" : + "Noop"; +type BranchTypes = NoopBranch | ApplicationBranch | ConfirmationBranch; const SCHEMA_FILE: string = "./config/questions.schema.json"; @@ -41,9 +42,11 @@ interface IQuestionBranchCache { } let QuestionBranchCache: IQuestionBranchCache = {}; +// Super class for all other branches +type QuestionBranch = NoopBranch; export class NoopBranch { public readonly name: string; - public readonly type: keyof QuestionBranchTypes = "Noop"; + public readonly type: BranchTypeAsString = "Noop"; public textBlocks: TextBlocks | undefined; public questions: Questions; @@ -123,11 +126,9 @@ export class NoopBranch { return this; } - public async convertTo(type: keyof QuestionBranchTypes): Promise { - // TODO typecast and return if types match - do not need to save to DB + public async convertTo(type: BranchTypeAsString): Promise { await this.save(); await QuestionBranchConfig.update({ "name": this.name }, { "$set": { "type": type } }); - // tslint:disable-next-line:no-use-before-declare return await BranchConfig.loadBranchFromDB(this.name) as T; } @@ -155,7 +156,7 @@ export class NoopBranch { } } -abstract class TimedBranch extends NoopBranch { +export abstract class TimedBranch extends NoopBranch { public open: Date; public close: Date; @@ -175,36 +176,45 @@ abstract class TimedBranch extends NoopBranch { } export class ApplicationBranch extends TimedBranch { - public readonly type: keyof QuestionBranchTypes = "Application"; + public readonly type: BranchTypeAsString = "Application"; - public confirmationBranches: string[]; + public allowAnonymous: boolean; + public autoAccept: string; protected async loadSettings(): Promise { await super.loadSettings(); let branchConfig = await QuestionBranchConfig.findOne({ "name": this.name }); - this.confirmationBranches = branchConfig && branchConfig.settings && branchConfig.settings.confirmationBranches || []; + this.allowAnonymous = branchConfig && branchConfig.settings && branchConfig.settings.allowAnonymous || false; + this.autoAccept = branchConfig && branchConfig.settings && branchConfig.settings.autoAccept || "disabled"; } protected serializeSettings(): QuestionBranchSettings { return { ...super.serializeSettings(), - confirmationBranches: this.confirmationBranches + allowAnonymous: this.allowAnonymous, + autoAccept: this.autoAccept }; } } export class ConfirmationBranch extends TimedBranch { - public readonly type: keyof QuestionBranchTypes = "Confirmation"; + public readonly type: BranchTypeAsString = "Confirmation"; public usesRollingDeadline: boolean; + public isAcceptance: boolean; + public autoConfirm: boolean; protected async loadSettings(): Promise { await super.loadSettings(); let branchConfig = await QuestionBranchConfig.findOne({ "name": this.name }); this.usesRollingDeadline = branchConfig && branchConfig.settings && branchConfig.settings.usesRollingDeadline || false; + this.isAcceptance = branchConfig && branchConfig.settings && branchConfig.settings.isAcceptance || false; + this.autoConfirm = branchConfig && branchConfig.settings && branchConfig.settings.autoConfirm || false; } protected serializeSettings(): QuestionBranchSettings { return { ...super.serializeSettings(), + isAcceptance: this.isAcceptance, + autoConfirm: this.autoConfirm, usesRollingDeadline: this.usesRollingDeadline }; } @@ -215,7 +225,20 @@ export class BranchConfig { let questionBranches: QuestionBranches = JSON.parse(await readFileAsync(config.questionsLocation)); return questionBranches.map(branch => branch.name); } - public static async loadAllBranches(type: keyof QuestionBranchTypes | "All" = "All", location: string = config.questionsLocation): Promise { + public static async getCanonicalName(rawName: string): Promise { + const branchNames = await BranchConfig.getNames(); + try { + rawName = decodeURIComponent(rawName); + } + catch { + // DecodeURIComponent() will fail if branch name is already decoded and is supposed to have a % escape sequence in it + } + rawName = rawName.toLowerCase(); + return branchNames.find(name => { + return rawName === name.toLowerCase(); + }) || null; + } + public static async loadAllBranches(type: BranchTypeAsString | "All" = "All", location: string = config.questionsLocation): Promise { let names = await this.getNames(); let branches: QuestionBranch[] = []; for (let name of names) { @@ -238,7 +261,7 @@ export class BranchConfig { public static async loadBranchFromDB(name: string, location: string = config.questionsLocation): Promise { let branchConfig = await QuestionBranchConfig.findOne({ name }); if (!branchConfig) { - return await new NoopBranch(name, location).loadFromSchema(); + return new NoopBranch(name, location).loadFromSchema(); } let instance: QuestionBranch; @@ -256,7 +279,7 @@ export class BranchConfig { return instance; } - public static async getOpenBranches(type: keyof QuestionBranchTypes): Promise { + public static async getOpenBranches(type: BranchTypeAsString): Promise { let branches: TimedBranch[]; switch (type) { case "Application": @@ -282,10 +305,11 @@ export async function getOpenConfirmationBranches(user: IUser): Promise { - map[data.name] = data; - return map; - }, {} as DeadlineMap); + + let deadlines = {} as DeadlineMap; + if (user.confirmationDeadline && user.confirmationDeadline.name) { + deadlines[user.confirmationDeadline.name] = user.confirmationDeadline; + } let branches = await (BranchConfig.loadAllBranches("Confirmation")) as ConfirmationBranch[]; diff --git a/server/common.ts b/server/common.ts index 08871ab7..cba5f100 100644 --- a/server/common.ts +++ b/server/common.ts @@ -1,7 +1,10 @@ +// Needed so that common.ts <-> schema.ts cyclical dependencies don't cause problems +/* tslint:disable:no-duplicate-imports */ import * as fs from "fs"; import * as crypto from "crypto"; import * as path from "path"; import * as tmp from "tmp"; +import * as qr from "qr-image"; import "passport"; // @@ -13,25 +16,19 @@ class Config implements IConfig.Main { public secrets: IConfig.Secrets = { adminKey: crypto.randomBytes(32).toString("hex"), session: crypto.randomBytes(32).toString("hex"), - github: { - id: "", - secret: "" - }, - google: { - id: "", - secret: "" - }, - facebook: { + groundTruth: { + url: "", id: "", secret: "" } }; public email: IConfig.Email = { from: "HackGT Team ", - host: "", - username: "", - password: "", - port: 465 + key: "", + headerImage: "", + twitterHandle: "TheHackGT", + facebookHandle: "thehackgt", + contactAddress: "hello@hack.gt" }; public server: IConfig.Server = { isProduction: false, @@ -41,10 +38,14 @@ class Config implements IConfig.Main { workflowReleaseSummary: null, cookieMaxAge: 1000 * 60 * 60 * 24 * 30 * 6, // 6 months cookieSecureOnly: false, - mongoURL: "mongodb://localhost/", - passwordResetExpiration: 1000 * 60 * 60 // 1 hour + mongoURL: "mongodb://localhost/registration", + defaultTimezone: "America/New_York", + rootURL: "http://localhost:3000" + }; + public admins = { + domains: [] as string[], + emails: [] as string[] }; - public admins: string[] = []; public eventName: string = "Untitled Event"; public storageEngine = { "name": "disk", @@ -61,6 +62,18 @@ class Config implements IConfig.Main { favicon: path.resolve(__dirname, "../client/favicon.ico") }; + public helpscout: IConfig.HelpScout = { + integration: { + enabled: false, + secretKey: "" + }, + beacon: { + enabled: false, + beaconId: "", + supportHistorySecretKey: "" + } + }; + public questionsLocation: string = path.resolve(__dirname, "./config/questions.json"); constructor(fileName: string = "config.json") { @@ -72,7 +85,7 @@ class Config implements IConfig.Main { } protected loadFromJSON(fileName: string): void { // tslint:disable-next-line:no-shadowed-variable - let config: IConfig.Main | null = null; + let config: Partial | null = null; try { config = JSON.parse(fs.readFileSync(path.resolve(__dirname, "./config", fileName), "utf8")); } @@ -86,7 +99,7 @@ class Config implements IConfig.Main { } if (config.secrets) { for (let key of Object.keys(config.secrets) as (keyof IConfig.Secrets)[]) { - this.secrets[key] = config.secrets[key]; + (this.secrets as any)[key] = config.secrets[key]; } } if (config.email) { @@ -96,11 +109,16 @@ class Config implements IConfig.Main { } if (config.server) { for (let key of Object.keys(config.server) as (keyof IConfig.Server)[]) { - this.server[key] = config.server[key]; + (this.server as any)[key] = config.server[key]; } } if (config.admins) { - this.admins = config.admins; + if (config.admins.domains) { + this.admins.domains = config.admins.domains; + } + if (config.admins.emails) { + this.admins.emails = config.admins.emails; + } } if (config.eventName) { this.eventName = config.eventName; @@ -124,58 +142,68 @@ class Config implements IConfig.Main { if (config.style) { this.style = config.style; } + if (config.helpscout) { + if (config.helpscout.integration.enabled) { + this.helpscout.integration.enabled = config.helpscout.integration.enabled; + this.helpscout.integration.secretKey = config.helpscout.integration.secretKey; + if (!config.helpscout.integration.secretKey) { + console.warn("Disabling Help Scout integration because it is set to be enabled but " + + "integration secret key is empty"); + this.helpscout.integration.enabled = false; + } + } + + if (config.helpscout.beacon.enabled) { + this.helpscout.beacon.enabled = config.helpscout.beacon.enabled; + this.helpscout.beacon.beaconId = config.helpscout.beacon.beaconId; + this.helpscout.beacon.supportHistorySecretKey = config.helpscout.beacon.supportHistorySecretKey; + if (!config.helpscout.beacon.beaconId || !config.helpscout.beacon.supportHistorySecretKey) { + console.log("Disabling Help Scout Beacon because it is set to be enabled but Beacon ID or " + + "support history secret key is missing"); + this.helpscout.beacon.enabled = false; + } + } + } } protected loadFromEnv(): void { // Secrets if (process.env.ADMIN_KEY_SECRET) { - this.secrets.adminKey = process.env.ADMIN_KEY_SECRET!; - } - else { - console.warn("Setting random admin key! Cannot use the service-to-service APIs."); + this.secrets.adminKey = process.env.ADMIN_KEY_SECRET; } if (process.env.SESSION_SECRET) { - this.secrets.session = process.env.SESSION_SECRET!; + this.secrets.session = process.env.SESSION_SECRET; this.sessionSecretSet = true; } - if (process.env.GITHUB_CLIENT_ID) { - this.secrets.github.id = process.env.GITHUB_CLIENT_ID!; - } - if (process.env.GITHUB_CLIENT_SECRET) { - this.secrets.github.secret = process.env.GITHUB_CLIENT_SECRET!; - } - if (process.env.GOOGLE_CLIENT_ID) { - this.secrets.google.id = process.env.GOOGLE_CLIENT_ID!; + if (process.env.GROUND_TRUTH_URL) { + this.secrets.groundTruth.url = process.env.GROUND_TRUTH_URL; } - if (process.env.GOOGLE_CLIENT_SECRET) { - this.secrets.google.secret = process.env.GOOGLE_CLIENT_SECRET!; + if (process.env.GROUND_TRUTH_ID) { + this.secrets.groundTruth.id = process.env.GROUND_TRUTH_ID; } - if (process.env.FACEBOOK_CLIENT_ID) { - this.secrets.facebook.id = process.env.FACEBOOK_CLIENT_ID!; - } - if (process.env.FACEBOOK_CLIENT_SECRET) { - this.secrets.facebook.secret = process.env.FACEBOOK_CLIENT_SECRET!; + if (process.env.GROUND_TRUTH_SECRET) { + this.secrets.groundTruth.secret = process.env.GROUND_TRUTH_SECRET; } // Email if (process.env.EMAIL_FROM) { - this.email.from = process.env.EMAIL_FROM!; + this.email.from = process.env.EMAIL_FROM; } - if (process.env.EMAIL_HOST) { - this.email.host = process.env.EMAIL_HOST!; + if (process.env.EMAIL_KEY) { + this.email.key = process.env.EMAIL_KEY; } - if (process.env.EMAIL_USERNAME) { - this.email.username = process.env.EMAIL_USERNAME!; + if (process.env.EMAIL_HEADER_IMAGE) { + this.email.headerImage = process.env.EMAIL_HEADER_IMAGE; } - if (process.env.EMAIL_PASSWORD) { - this.email.password = process.env.EMAIL_PASSWORD!; + if (process.env.EMAIL_TWITTER_HANDLE) { + this.email.twitterHandle = process.env.EMAIL_TWITTER_HANDLE; } - if (process.env.EMAIL_PORT) { - let port = parseInt(process.env.EMAIL_PORT!, 10); - if (!isNaN(port) && port > 0) { - this.email.port = port; - } + if (process.env.EMAIL_FACEBOOK_HANDLE) { + this.email.facebookHandle = process.env.EMAIL_FACEBOOK_HANDLE; + } + if (process.env.EMAIL_CONTACT_ADDRESS) { + this.email.contactAddress = process.env.EMAIL_CONTACT_ADDRESS; } // Server - if (process.env.PRODUCTION && process.env.PRODUCTION!.toLowerCase() === "true") { + if (process.env.PRODUCTION && process.env.PRODUCTION.toLowerCase() === "true") { this.server.isProduction = true; } if (process.env.PORT) { @@ -185,65 +213,65 @@ class Config implements IConfig.Main { } } if (process.env.VERSION_HASH) { - this.server.versionHash = process.env.VERSION_HASH!; + this.server.versionHash = process.env.VERSION_HASH; } if (process.env.SOURCE_REV) { - this.server.versionHash = process.env.SOURCE_REV!; + this.server.versionHash = process.env.SOURCE_REV; } if (process.env.SOURCE_VERSION) { - this.server.versionHash = process.env.SOURCE_VERSION!; + this.server.versionHash = process.env.SOURCE_VERSION; } if (process.env.WORKFLOW_RELEASE_CREATED_AT) { - this.server.workflowReleaseCreatedAt = process.env.WORKFLOW_RELEASE_CREATED_AT!; + this.server.workflowReleaseCreatedAt = process.env.WORKFLOW_RELEASE_CREATED_AT; } if (process.env.WORKFLOW_RELEASE_SUMMARY) { - this.server.workflowReleaseSummary = process.env.WORKFLOW_RELEASE_SUMMARY!; + this.server.workflowReleaseSummary = process.env.WORKFLOW_RELEASE_SUMMARY; } if (process.env.COOKIE_MAX_AGE) { - let maxAge = parseInt(process.env.COOKIE_MAX_AGE!, 10); + let maxAge = parseInt(process.env.COOKIE_MAX_AGE, 10); if (!isNaN(maxAge) && maxAge > 0) { this.server.cookieMaxAge = maxAge; } } - if (process.env.COOKIE_SECURE_ONLY && process.env.COOKIE_SECURE_ONLY!.toLowerCase() === "true") { + if (process.env.COOKIE_SECURE_ONLY && process.env.COOKIE_SECURE_ONLY.toLowerCase() === "true") { this.server.cookieSecureOnly = true; } if (process.env.MONGO_URL) { - this.server.mongoURL = process.env.MONGO_URL!; + this.server.mongoURL = process.env.MONGO_URL; } - if (process.env.PASSWORD_RESET_EXPIRATION) { - let expirationTime = parseInt(process.env.PASSWORD_RESET_EXPIRATION!, 10); - if (!isNaN(expirationTime) && expirationTime > 0) { - this.server.passwordResetExpiration = expirationTime; - } + if (process.env.DEFAULT_TIMEZONE) { + this.server.defaultTimezone = process.env.DEFAULT_TIMEZONE; } // Admins if (process.env.ADMIN_EMAILS) { - this.admins = JSON.parse(process.env.ADMIN_EMAILS!); + this.admins.emails = JSON.parse(process.env.ADMIN_EMAILS!); + } + if (process.env.ADMIN_DOMAINS) { + this.admins.domains = JSON.parse(process.env.ADMIN_DOMAINS); } // Event name if (process.env.EVENT_NAME) { - this.eventName = process.env.EVENT_NAME!; + this.eventName = process.env.EVENT_NAME; } // Questions if (process.env.QUESTIONS_FILE) { - this.questionsLocation = process.env.QUESTIONS_FILE!; + this.questionsLocation = process.env.QUESTIONS_FILE; } // Style if (process.env.THEME_FILE) { - this.style.theme = process.env.THEME_FILE!; + this.style.theme = process.env.THEME_FILE; } if (process.env.FAVICON_FILE) { - this.style.favicon = process.env.FAVICON_FILE!; + this.style.favicon = process.env.FAVICON_FILE; } else if (process.env.FAVICON_FILE_BASE64) { - this.style.favicon = unbase64File(process.env.FAVICON_FILE_BASE64!); + this.style.favicon = unbase64File(process.env.FAVICON_FILE_BASE64); } // Storage engine if (process.env.STORAGE_ENGINE) { - this.storageEngine.name = process.env.STORAGE_ENGINE!; + this.storageEngine.name = process.env.STORAGE_ENGINE; if (process.env.STORAGE_ENGINE_OPTIONS) { - this.storageEngine.options = JSON.parse(process.env.STORAGE_ENGINE_OPTIONS!); + this.storageEngine.options = JSON.parse(process.env.STORAGE_ENGINE_OPTIONS); } else { console.warn("Custom storage engine defined but no storage engine options passed"); @@ -254,7 +282,37 @@ class Config implements IConfig.Main { } // Team size if (process.env.MAX_TEAM_SIZE) { - this.maxTeamSize = parseInt(process.env.MAX_TEAM_SIZE!, 10); + this.maxTeamSize = parseInt(process.env.MAX_TEAM_SIZE, 10); + } + + if (process.env.HELPSCOUT_INTEGRATION_ENABLED && process.env.HELPSCOUT_INTEGRATION_ENABLED.toLowerCase() === "true") { + this.helpscout.integration.enabled = true; + if (process.env.HELPSCOUT_INTEGRATION_SECRET_KEY) { + this.helpscout.integration.secretKey = process.env.HELPSCOUT_INTEGRATION_SECRET_KEY; + } else { + console.warn("Disabling Help Scout integration because it is set to be enabled but " + + "integration secret key is empty"); + this.helpscout.integration.enabled = false; + } + } + + if (process.env.HELPSCOUT_BEACON_ENABLED && process.env.HELPSCOUT_BEACON_ENABLED.toLowerCase() === "true") { + this.helpscout.beacon.enabled = true; + if (process.env.HELPSCOUT_BEACON_ID) { + this.helpscout.beacon.beaconId = process.env.HELPSCOUT_BEACON_ID; + } + if (process.env.HELPSCOUT_BEACON_SUPPORT_HISTORY_SECRET_KEY) { + this.helpscout.beacon.supportHistorySecretKey = process.env.HELPSCOUT_BEACON_SUPPORT_HISTORY_SECRET_KEY; + } + if (!process.env.HELPSCOUT_BEACON_ID || !process.env.HELPSCOUT_BEACON_SUPPORT_HISTORY_SECRET_KEY) { + console.log("Disabling Help Scout Beacon because of missing Beacon ID or support history " + + "secret key"); + this.helpscout.beacon.enabled = false; + } + } + + if (process.env.ROOT_URL) { + this.server.rootURL = process.env.ROOT_URL; } } } @@ -291,15 +349,14 @@ export function formatSize(size: number, binary: boolean = true): string { // Database connection // import * as mongoose from "mongoose"; -(mongoose as any).Promise = global.Promise; -mongoose.connect(config.server.mongoURL, { - useMongoClient: true -} as mongoose.ConnectionOptions); -export {mongoose}; +mongoose.connect(config.server.mongoURL, { useNewUrlParser: true }).catch(err => { + throw err; +}); +export { mongoose }; import { Setting } from "./schema"; -export async function setDefaultSettings() { +async function setDefaultSettings() { async function doesNotExist(key: string): Promise { return await Setting.find({ "name": key }).count() === 0; } @@ -384,38 +441,51 @@ export function unbase64File(filename: string): string { // // Email // -import * as nodemailer from "nodemailer"; +import * as sendgrid from "@sendgrid/mail"; +sendgrid.setApiKey(config.email.key); import * as marked from "marked"; // tslint:disable-next-line:no-var-requires const striptags = require("striptags"); import { IUser, Team, IFormItem } from "./schema"; - -export let emailTransporter = nodemailer.createTransport({ - host: config.email.host, - port: config.email.port, - secure: true, - auth: { - user: config.email.username, - pass: config.email.password +import * as htmlToText from "html-to-text"; +// tslint:disable-next-line:no-var-requires +const Email = require("email-templates"); +const email = new Email({ + views: { + root: path.resolve("server/emails/") + }, + juice: true, + juiceResources: { + preserveImportant: true, + webResources: { + relativeTo: path.join(__dirname, "emails", "email-template") + } } }); -export async function sendMailAsync(mail: nodemailer.SendMailOptions): Promise { - return new Promise((resolve, reject) => { - emailTransporter.sendMail(mail, (err, info) => { - if (err) { - reject(err); - return; - } - resolve(info); - }); - }); + +export const defaultEmailSubjects = { + apply: `[${config.eventName}] - Thank you for applying!`, + preConfirm: `[${config.eventName}] - Application Update`, + attend: `[${config.eventName}] - Thank you for RSVPing!` +}; +export interface IMailObject { + to: string; + from: string; + subject: string; + html: string; + text: string; } -export function sanitize(input: string): string { - if (typeof input !== "string") { +export function sanitize(input?: string): string { + if (!input || typeof input !== "string") { return ""; } return input.replace(/&/g, "&").replace(//g, ">"); } +export function removeTags(input: string): string { + let text = striptags(input); + text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); + return text; +} let renderer = new marked.Renderer(); let singleLineRenderer = new marked.Renderer(); @@ -433,7 +503,7 @@ export async function renderMarkdown(markdown: string, options?: MarkedOptions, }); }); } -export async function renderEmailHTML(markdown: string, user: IUser): Promise { +async function templateMarkdown(markdown: string, user: IUser): Promise { let teamName: string; if (await getSetting("teamsEnabled")) { teamName = "No team created or joined"; @@ -467,51 +537,63 @@ export async function renderEmailHTML(markdown: string, user: IUser): Promise(resolve => { + let qrStream = qr.image(`user:${user.uuid}`); + let buffer: any[] = []; + qrStream.on("data", chunk => { + buffer.push(chunk); + }); + qrStream.on("end", () => { + resolve(Buffer.concat(buffer)); + }); + }); + let qrURI = `data:image/png;base64,${qrBuffer.toString("base64")}`; + let qrMarkdown = `![${user.uuid}](${qrURI})`; + // Interpolate and sanitize variables markdown = markdown.replace(/{{eventName}}/g, sanitize(config.eventName)); + markdown = markdown.replace(/{{reimbursementAmount}}/g, sanitize(user.reimbursementAmount)); + markdown = markdown.replace(/{{qrURI}}/g, qrURI); + markdown = markdown.replace(/{{qrCode}}/g, qrMarkdown); markdown = markdown.replace(/{{email}}/g, sanitize(user.email)); markdown = markdown.replace(/{{name}}/g, sanitize(user.name)); markdown = markdown.replace(/{{teamName}}/g, sanitize(teamName)); markdown = markdown.replace(/{{applicationBranch}}/g, sanitize(user.applicationBranch)); markdown = markdown.replace(/{{confirmationBranch}}/g, sanitize(user.confirmationBranch)); markdown = markdown.replace(/{{application\.([a-zA-Z0-9\- ]+)}}/g, (match, name: string) => { - let question = user.applicationData.find(data => data.name === name); + let question = (user.applicationData || []).find(data => data.name === name); return formatFormItem(question); }); markdown = markdown.replace(/{{confirmation\.([a-zA-Z0-9\- ]+)}}/g, (match, name: string) => { - let question = user.confirmationData.find(data => data.name === name); + let question = (user.confirmationData || []).find(data => data.name === name); return formatFormItem(question); }); - - return await renderMarkdown(markdown); + return markdown; } -export async function renderEmailText(markdown: string, user: IUser, markdownRendered: boolean = false): Promise { - let html: string; - if (!markdownRendered) { - html = await renderEmailHTML(markdown, user); - } - else { - html = markdown; - } - // Remove ").replace(/"); - - // Append href of links to their text - const cheerio = await import("cheerio"); - let $ = cheerio.load(html, { decodeEntities: false }); - $("a").each((i, el) => { - let element = $(el); - element.text(`${element.text()} (${element.attr("href")})`); +export async function renderEmailHTML(markdown: string, user: IUser): Promise { + let templatedMarkdown = await templateMarkdown(markdown, user); + let renderedMarkdown = await renderMarkdown(templatedMarkdown); + return email.render("email-template/html", { + emailHeaderImage: config.email.headerImage, + twitterHandle: config.email.twitterHandle, + facebookHandle: config.email.facebookHandle, + emailAddress: config.email.contactAddress, + hackathonName: config.eventName, + body: renderedMarkdown }); - html = $.html(); - - let text: string = striptags(html); - // Reverse sanitization - return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); +} +export async function renderEmailText(markdown: string, user: IUser): Promise { + let templatedMarkdown = await templateMarkdown(markdown, user); + let renderedHtml = await renderMarkdown(templatedMarkdown); + return htmlToText.fromString(renderedHtml); +} +export async function renderPageHTML(markdown: string, user: IUser): Promise { + let templatedMarkdown = await templateMarkdown(markdown, user); + return renderMarkdown(templatedMarkdown); } // Verify and load questions -import { BranchConfig } from "./branch"; +import { BranchConfig, ApplicationBranch, ConfirmationBranch } from "./branch"; BranchConfig.verifyConfig().then(good => { if (good) { console.log(`Question branches loaded from ${config.questionsLocation} to DB successfully`); @@ -522,3 +604,70 @@ BranchConfig.verifyConfig().then(good => { }).catch(err => { throw err; }); + +import {ApplicationType} from "./middleware"; +import * as moment from "moment-timezone"; + +export async function isBranchOpen(rawBranchName: string, user: IUser, requestType: ApplicationType): Promise { + const branchName = await BranchConfig.getCanonicalName(rawBranchName); + if (!branchName) { + return false; + } + let branch = await BranchConfig.loadBranchFromDB(branchName) as (ApplicationBranch | ConfirmationBranch); + + let openDate = branch.open; + let closeDate = branch.close; + if (requestType === ApplicationType.Confirmation + && user.confirmationDeadline + && user.confirmationDeadline.name + && user.confirmationDeadline.name.toLowerCase() === branchName.toLowerCase()) { + openDate = user.confirmationDeadline.open; + closeDate = user.confirmationDeadline.close; + } + + if (branch instanceof ConfirmationBranch && branch.autoConfirm) { + return false; + } + + if (moment().isBetween(openDate, closeDate)) { + return true; + } + + return false; +} + +// +// Agenda Async Queue +// +import * as Agenda from "agenda"; +export const agenda = new Agenda({db: {address: config.server.mongoURL}}); +import { User } from "./schema"; +import {MarkedOptions} from "marked"; +agenda.define("send_templated_email", async (job, done) => { + try { + let user = await User.findOne({uuid: job.attrs.data.id}); + if (user) { + let emailHTML = await renderEmailHTML(job.attrs.data.markdown, user); + let emailText = await renderEmailText(job.attrs.data.markdown, user); + let emailDetails = { + from: config.email.from, + to: user.email, + subject: job.attrs.data.subject, + html: emailHTML, + text: emailText + }; + await sendgrid.send(emailDetails); + await done(); + } else { + await done(new Error("No such user")); + } + } + catch(err) { + console.error(err); + await done(err); + } +}); + +agenda.start().catch((err) => { + console.error("Unable to start agenda worker: ", err); +}); diff --git a/server/config/config.example.json b/server/config/config.example.json index 0181a5e0..177fcc22 100644 --- a/server/config/config.example.json +++ b/server/config/config.example.json @@ -1,24 +1,20 @@ { "secrets": { - "session": "", - "github": { - "id": "", - "secret": "" - }, - "google": { - "id": "", - "secret": "" - }, - "facebook": { + "session": "", + "groundTruth": { + "url": "", "id": "", "secret": "" } }, + "admins": { + "domains": ["gatech.edu.invalid", "hack.gt.invalid"], + "emails": ["george.p@burdell.com.invalid", "buzz@gatech.edu.invalid"] + }, "email": { - "from": "", - "host": "", - "username": "", - "password": "", + "from": "HackGT Team ", + "host": "smtp.sendgrid.net", + "key": "", "port": 465 }, "server": { @@ -26,16 +22,27 @@ "port": 3000, "cookieMaxAge": 15552000000, "cookieSecureOnly": true, - "passwordResetExpiration": 3600000 + "mongoURL": "mongodb://localhost/registration", + "rootURL": "http://localhost:3000" + }, - "admins": ["example@example.com"], "eventName": "My Hackathon", "storageEngine": { - "name": "disk", + "name": "disk | s3 | gcs", "options": { "uploadDirectory": "uploads" } }, "maxTeamSize": 4, - "enableQRCode": false + "helpscout": { + "integration": { + "enabled": false, + "secretKey": "" + }, + "beacon": { + "enabled": false, + "beaconId": " + + + + 96 + + + + + + + +
    + + + + + + + +
    + +
    + + + + + + + + + + + +
    + + + + + +
    + +
    + -1) { - const key = new Buffer(auth.split(" ")[1], "base64").toString(); + const key = Buffer.from(auth.split(" ")[1], "base64").toString(); if (key === config.secrets.adminKey) { next(); } @@ -104,7 +106,7 @@ export function isAdmin(request: express.Request, response: express.Response, ne // For API endpoints export function authenticateWithReject(request: express.Request, response: express.Response, next: express.NextFunction) { response.setHeader("Cache-Control", "no-cache"); - if (!request.isAuthenticated()) { + if (!request.isAuthenticated() || !request.user) { response.status(401).json({ "error": "You must log in to access this endpoint" }); @@ -117,7 +119,10 @@ export function authenticateWithReject(request: express.Request, response: expre // For directly user facing endpoints export function authenticateWithRedirect(request: express.Request, response: express.Response, next: express.NextFunction) { response.setHeader("Cache-Control", "private"); - if (!request.isAuthenticated()) { + if (!request.isAuthenticated() || !request.user) { + if (request.session) { + request.session.returnTo = request.originalUrl; + } response.redirect("/login"); } else { @@ -125,80 +130,148 @@ export function authenticateWithRedirect(request: express.Request, response: exp } } -import * as Handlebars from "handlebars"; -import { ICommonTemplate } from "./schema"; export enum ApplicationType { Application, Confirmation } -export async function 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 openBranches: (ApplicationBranch | ConfirmationBranch)[]; - if (requestType === ApplicationType.Application) { - openBranches = await BranchConfig.getOpenBranches("Application"); - if (user.applied) { - openBranches = openBranches.filter((b => b.name === user.applicationBranch)); - } +export async function onlyAllowAnonymousBranch(request: express.Request, response: express.Response, next: express.NextFunction) { + const branchName = await BranchConfig.getCanonicalName(request.params.branch); + if (!branchName) { + response.redirect("/"); + return; } - else { - openBranches = await getOpenConfirmationBranches(request.user as IUser); - if (user.attending) { - openBranches = openBranches.filter((b => b.name === user.confirmationBranch)); - } + const branch = await BranchConfig.loadBranchFromDB(branchName) as ApplicationBranch; + if (!branch.allowAnonymous) { + response.redirect("/"); + return; + } + next(); +} +export async function canUserModify(request: express.Request, response: express.Response, next: express.NextFunction) { + const user = await User.findOne({uuid: request.params.uuid}) as IUser; + const branchName = await BranchConfig.getCanonicalName(request.params.branch); + if (!branchName) { + response.status(400).json({ + "error": "Invalid application branch name" + }); + return; } + let questionBranch = await BranchConfig.loadBranchFromDB(branchName); - if (openBranches.length > 0) { - next(); + if (!(await isBranchOpen(branchName, user, questionBranch instanceof ApplicationBranch ? ApplicationType.Application : ApplicationType.Confirmation))) { + response.status(400).json({ + "error": "Branch is closed" + }); return; } - // TODO reimplement open and close times - /* - const TIME_FORMAT = "dddd, MMMM Do YYYY [at] h:mm a z"; - */ - interface IClosedTemplate extends ICommonTemplate { - type: string; - /* - open: { - time: string; - verb: string; - }; - close: { - time: string; - verb: string; - }; - */ - contactEmail: string; - } - let template = Handlebars.compile(await readFileAsync(path.resolve(STATIC_ROOT, "closed.html"))); - let emailParsed = config.email.from.match(/<(.*?)>/); - let templateData: IClosedTemplate = { - siteTitle: config.eventName, - user: request.user, - settings: { - teamsEnabled: await getSetting("teamsEnabled"), - qrEnabled: await getSetting("qrEnabled") - }, + if (questionBranch instanceof ApplicationBranch) { + // Don't allow user to modify application if we assigned them a confirmation branch + if (user.confirmationBranch) { + response.status(400).json({ + "error": "You can no longer edit this application" + }); + return; + } + if (user.applied && branchName.toLowerCase() !== user.applicationBranch!.toLowerCase()) { + response.status(400).json({ + "error": "You can only edit the application branch that you originally submitted" + }); + return; + } + } else if (questionBranch instanceof ConfirmationBranch) { + if (!user.confirmationBranch) { + response.status(400).json({ + "error": "You can't confirm for that branch" + }); + } else if (user.confirmationBranch && branchName.toLowerCase() !== user.confirmationBranch.toLowerCase()) { + response.status(400).json({ + "error": "You can only submit the confirmation branch you were assigned" + }); + return; + } + } else { + response.status(400).json({ + "error": "Invalid application branch" + }); + return; + } - type: requestType === ApplicationType.Application ? "Application" : "Confirmation", - /* - open: { - time: openDate.tz(moment.tz.guess()).format(TIME_FORMAT), - verb: moment().isBefore(openDate) ? "will open" : "opened" - }, - close: { - time: closeDate.tz(moment.tz.guess()).format(TIME_FORMAT), - verb: moment().isBefore(closeDate) ? "will close" : "closed" - }, - */ - contactEmail: emailParsed ? emailParsed[1] : config.email.from + next(); +} +export function branchRedirector(requestType: ApplicationType): (request: express.Request, response: express.Response, next: express.NextFunction) => Promise { + return async (request, response, next) => { + // TODO: fix branch names so they have a machine ID and human label + let user = request.user as IUser; + + if (requestType === ApplicationType.Application && user.confirmationBranch) { + response.redirect("/"); + return; + } + + if (requestType === ApplicationType.Confirmation && !user.confirmationBranch) { + response.redirect("/"); + return; + } + + if (request.params.branch) { + // Branch name from URL can be different from config in casing + let branchName = await BranchConfig.getCanonicalName(request.params.branch); + if (!branchName) { + // Invalid branch name + response.redirect("/"); + return; + } + let branch = await BranchConfig.loadBranchFromDB(branchName); + if ((branch.type === "Application" && requestType !== ApplicationType.Application) || (branch.type === "Confirmation" && requestType !== ApplicationType.Confirmation)) { + response.redirect("/"); + return; + } + + if (!(await isBranchOpen(branchName, user, requestType))) { + response.redirect("/"); + return; + } + + 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; + } + } + if (requestType === ApplicationType.Confirmation) { + if (!user.confirmationBranch || branchName.toLowerCase() !== user.confirmationBranch.toLowerCase()) { + response.redirect("/"); + return; + } + } + } else { + let targetBranch: string | undefined; + if (requestType === ApplicationType.Application && user.applied && user.applicationBranch) { + targetBranch = user.applicationBranch; + } else if (requestType === ApplicationType.Confirmation) { + targetBranch = user.confirmationBranch; + } + + if (targetBranch) { + const uriBranch = encodeURIComponent(targetBranch.toLowerCase()); + const redirPath = requestType === ApplicationType.Application ? "apply" : "confirm"; + response.redirect(`/${redirPath}/${uriBranch}`); + return; + } + + // If there are no open application branches, redirect to main page. + if ((await BranchConfig.getOpenBranches("Application")).length === 0) { + response.redirect("/"); + return; + } + } + + next(); }; - response.send(template(templateData)); } -import { DataLog, HackGTMetrics } from "./schema"; export function trackEvent(action: string, request: express.Request, user?: string, data?: object) { let thisEvent: DataLog = { action, @@ -212,17 +285,68 @@ export function trackEvent(action: string, request: express.Request, user?: stri let tags = { action, url: request.path, - ip: request.ip, - user, ...data }; let metricsEvent: HackGTMetrics = { hackgtmetricsversion: 1, serviceName: `registration-${config.eventName.replace(/[^a-zA-Z0-9]/g, "")}-${action}`, values: { - value: 1 + user, + ip: request.ip }, tags }; console.log(JSON.stringify(metricsEvent)); } + +export function isHelpScoutIntegrationEnabled(request: express.Request, response: express.Response, next: express.NextFunction) { + const enabled = config.helpscout.integration.enabled; + if (!enabled) { + console.error("Error while responding to Help Scout API request: Help Scout integration functionality is currently disabled"); + response.status(404).json({ + error: "Not found" + }); + return; + } + + next(); +} + +// Inspired by https://github.com/suryagh/tsscmp/blob/master/lib/index.js#L11 +function bufferEqual(a: Buffer, b: Buffer) { + if (a.length !== b.length) { + return false; + } + + return crypto.timingSafeEqual(a, b); +} + +export function validateHelpScoutSignature(request: RequestWithRawBody, response: express.Response, next: express.NextFunction) { + const secret = config.helpscout.integration.secretKey; + const hsSignature = request.header('X-HelpScout-Signature')?.trim(); + + if (!request.rawBody) { + console.warn("Rejecting request for Help Scout integration: missing rawBody attribute on request " + + "(must be added by middleware)"); + } else if (hsSignature) { + const computedHash = crypto.createHmac('sha1', secret) + .update(request.rawBody) + .digest('base64'); + + // Prevents timing attacks with HMAC hashes https://stackoverflow.com/a/51489494 + if (bufferEqual(Buffer.from(hsSignature), Buffer.from(computedHash))) { + next(); + return; + } else { + console.warn("Rejecting request for Help Scout integration due to incorrect signature (check that " + + "the secret key is correct in local config and in Helpscout app config)"); + } + } else { + console.warn("Rejecting request for Help Scout integration: missing required X-HelpScout-Signature " + + "header"); + } + + response.status(400).json({ + "error": "Unable to process request" + }); +} diff --git a/server/routes/api/admin.ts b/server/routes/api/admin.ts deleted file mode 100644 index 3f25ff24..00000000 --- a/server/routes/api/admin.ts +++ /dev/null @@ -1,115 +0,0 @@ -import * as express from "express"; - -import { - formatSize -} from "../../common"; -import { - isAdmin -} from "../../middleware"; -import { - User, Team -} from "../../schema"; -import * as Branches from "../../branch"; - -export let adminRoutes = express.Router(); - -adminRoutes.route("/users").get(isAdmin, async (request, response) => { - let offset = parseInt(request.query.offset, 10); - if (isNaN(offset) || offset < 0) { - offset = 0; - } - let count = parseInt(request.query.count, 10); - if (isNaN(count) || count <= 0) { - count = 10; - } - let filter: any = {}; - if (request.query.applied === "true") { - filter.applied = true; - } - if (request.query.branch) { - // tslint:disable-next-line:no-string-literal - filter["$or"] = [{ "applicationBranch": request.query.branch }, { "confirmationBranch": request.query.branch }]; - } - if (request.query.status === "no-decision") { - filter.applied = true; - filter.accepted = false; - } - if (request.query.status === "accepted") { - filter.applied = true; - filter.accepted = true; - } - let branches = await Branches.BranchConfig.loadAllBranches(); - - let teamIDNameMap: { - [id: string]: string; - } = {}; - (await Team.find()).forEach(team => { - teamIDNameMap[team._id.toString()] = team.teamName; - }); - - let total = await User.count(filter); - let results = (await User.find(filter).collation({ "locale": "en" }).sort({ name: "asc" }).skip(offset).limit(count).exec()).map(user => { - let loginMethods: string[] = []; - if (user.githubData && user.githubData.id) { - loginMethods.push("GitHub"); - } - if (user.googleData && user.googleData.id) { - loginMethods.push("Google"); - } - if (user.facebookData && user.facebookData.id) { - loginMethods.push("Facebook"); - } - if (user.localData && user.localData.hash) { - loginMethods.push("Local"); - } - let status: string = "Signed up"; - if (user.applied) { - status = `Applied (${user.applicationBranch})`; - } - if (user.accepted) { - status = `Accepted (${user.applicationBranch})`; - } - if (user.attending) { - status = `Accepted (${user.applicationBranch}) / Attending (${user.confirmationBranch})`; - } - let questionsFromBranch = branches.find(branch => branch.name === user.applicationBranch); - let applicationDataFormatted: {"label": string; "value": string}[] = []; - if (questionsFromBranch) { - applicationDataFormatted = user.applicationData.map(question => { - let rawQuestion = questionsFromBranch!.questions.find(q => q.name === question.name); - // If there isn't schema information for this question return the raw name as the label - let label: string = rawQuestion ? rawQuestion.label : question.name; - - if (typeof question.value === "string") { - return { label, value: question.value }; - } - else if (Array.isArray(question.value)) { - return { label, value: question.value.join(", ") }; - } - else if (question.value === null) { - return { label, value: "N/A" }; - } - else { - // Multer file - let file = question.value; - let formattedSize = formatSize(file.size); - return { label, value: `[${file.mimetype} | ${formattedSize}]: ${file.path}`, filename: file.filename }; - } - }); - } - - return { - ...user.toObject(), - "status": status, - "loginMethods": loginMethods.join(", "), - "applicationDataFormatted": applicationDataFormatted, - "teamName": user.teamId ? teamIDNameMap[user.teamId.toString()] : null - }; - }); - response.json({ - offset, - count, - total, - data: results - }); -}); diff --git a/server/routes/api/graphql.ts b/server/routes/api/graphql.ts index 8553d8cc..0fa41472 100644 --- a/server/routes/api/graphql.ts +++ b/server/routes/api/graphql.ts @@ -6,9 +6,10 @@ import * as express from "express"; import { graphqlExpress, graphiqlExpress } from "graphql-server-express"; import { makeExecutableSchema } from "graphql-tools"; import { isAdmin, authenticateWithRedirect } from "../../middleware"; -import { User, IUser, IFormItem, QuestionBranchConfig } from "../../schema"; -import { Branches, Tags, AllTags } from "../../branch"; +import { User, IUser, Team, IFormItem, QuestionBranchConfig } from "../../schema"; +import { Branches, Tags, AllTags, BranchConfig, ApplicationBranch, ConfirmationBranch, NoopBranch } from "../../branch"; import { schema as types } from "./api.graphql.types"; +import { formatSize } from "../../common"; const typeDefs = fs.readFileSync(path.resolve(__dirname, "../../../api.graphql"), "utf8"); @@ -38,41 +39,24 @@ const resolvers: IResolver = { $gt: args.pagination_token } } : {}; + const uuidQuery = args.ids ? { + uuid: { + $in: args.ids + } + } : {}; const allUsers = await User .find({ ...lastIdQuery, + ...uuidQuery, ...userFilterToMongo(args.filter) }) .limit(args.n); - return allUsers.map(userRecordToGraphql); + return Promise.all(allUsers.map(userRecordToGraphql)); }, - search_user: async (prev, args) => { - let escapedQuery: string = args.search; - if (!args.use_regex) { - escapedQuery = escapedQuery.trim().replace(/[|\\{()[^$+*?.-]/g, "\\$&"); - } - const queryRegExp = new RegExp(escapedQuery, "i"); - - const results = await User - .find(userFilterToMongo(args.filter)) - .or([ - { - name: { - $regex: queryRegExp - } - }, - { - email: { - $regex: queryRegExp - } - } - ]) - .skip(args.offset) - .limit(args.n) - .exec(); - - return results.map(userRecordToGraphql); + search_user: searchUser, + search_user_simple: async (prev, args) => { + return (await searchUser(prev, args)).users; }, question_branches: () => { return Branches; @@ -105,28 +89,91 @@ const resolvers: IResolver = { return (await findQuestions(prev, { names: [args.name] }))[0]; }, questions: async (prev, args) => { - return await findQuestions(prev, args); + return findQuestions(prev, args); } } }; +async function searchUser(prev: any, args: { + search: string; + use_regex: boolean; + offset: number; + n: number; + filter: types.UserFilter; +}) { + let escapedQuery: string = args.search; + if (!args.use_regex) { + escapedQuery = escapedQuery.trim().replace(/[|\\{()[^$+*?.-]/g, "\\$&"); + } + const queryRegExp = new RegExp(escapedQuery, "i"); + const query = [ + { + name: { + $regex: queryRegExp + } + }, + { + email: { + $regex: queryRegExp + } + }, + { + uuid: args.search + } + ]; + const total = await User.find(userFilterToMongo(args.filter)) + .or(query) + .count(); + const results = await User + .find(userFilterToMongo(args.filter)) + .or(query) + .collation({ "locale": "en" }).sort({ name: "asc" }) + .skip(args.offset) + .limit(args.n) + .exec(); + + return { + offset: args.offset, + count: results.length, + total, + users: await Promise.all(results.map(userRecordToGraphql)) + }; +} + async function findQuestions( target: types.User, args: { names: string[] } ): Promise[]> { const user = await User.findOne({uuid: target.id}); - if (!user) return []; + if (!user) { + return []; + } const names = new Set(args.names); - return user.confirmationData.concat(user.applicationData) - .reduce((results, question) => { - if (names.has(question.name)) { - results.push(question); - } - return results; - }, [] as IFormItem[]) - .map(recordToFormItem); + function questionFilter(results: IFormItem[], question: IFormItem): IFormItem[] { + if (names.has(question.name)) { + results.push(question); + } + return results; + } + + let items: types.FormItem[] = []; + if (user.applied) { + items = items.concat(await Promise.all( + user.applicationData! + .reduce(questionFilter, []) + .map(item => recordToFormItem(item, user.applicationBranch!)) + )); + } + if (user.confirmed) { + items = items.concat(await Promise.all( + user.confirmationData! + .reduce(questionFilter, []) + .map(item => recordToFormItem(item, user.confirmationBranch!)) + )); + } + return items; } export const schema = makeExecutableSchema({ @@ -170,25 +217,40 @@ function userFilterToMongo(filter: types.UserFilter | undefined) { return {}; } const query: { [name: string]: any } = {}; - const setIf = (key: string, val: any) => val ? query[key] = val : undefined; + + function setIf(key: string, val: any): void { + if (val !== null && val !== undefined) { + query[key] = val; + } + } setIf("applied", filter.applied); setIf("accepted", filter.accepted); - setIf("attending", filter.attending); + setIf("confirmed", filter.confirmed); setIf("applicationBranch", filter.application_branch); setIf("confirmationBranch", filter.confirmation_branch); return query; } -function recordToFormItem(item: IFormItem): types.FormItem { +let cachedBranches: { + [name: string]: NoopBranch | ApplicationBranch | ConfirmationBranch; +} = {}; +async function recordToFormItem(item: IFormItem, branchName: string): Promise> { + if (!cachedBranches[branchName]) { + cachedBranches[branchName] = await BranchConfig.loadBranchFromDB(branchName); + } + let label: string = cachedBranches[branchName].questionLabels[item.name] || item.name; + if (!item.value) { return { name: item.name, + label, type: item.type }; } else if (typeof item.value === "string") { return { name: item.name, + label, type: item.type, value: item.value }; @@ -196,6 +258,7 @@ function recordToFormItem(item: IFormItem): types.FormItem { else if (item.value instanceof Array) { return { name: item.name, + label, type: item.type, values: item.value }; @@ -205,48 +268,53 @@ function recordToFormItem(item: IFormItem): types.FormItem { const file = item.value as Express.Multer.File; return { name: item.name, + label, type: item.type, file: { original_name: file.originalname, encoding: file.encoding, mimetype: file.mimetype, path: file.path, - size: file.size + size: file.size, + size_formatted: formatSize(file.size) } }; } } -function userRecordToGraphql(user: IUser): types.User { +async function userRecordToGraphql(user: IUser): Promise> { const application: types.Branch | undefined = user.applied ? { - type: user.applicationBranch, - data: user.applicationData.map(recordToFormItem), + type: user.applicationBranch!, + data: await Promise.all(user.applicationData!.map(item => recordToFormItem(item, user.applicationBranch!))), start_time: user.applicationStartTime && user.applicationStartTime.toDateString(), submit_time: user.applicationSubmitTime && user.applicationSubmitTime.toDateString() } : undefined; - const confirmation: types.Branch | undefined = user.attending ? { - type: user.confirmationBranch, - data: user.confirmationData.map(recordToFormItem), + const confirmation: types.Branch | undefined = user.confirmed ? { + type: user.confirmationBranch!, + data: await Promise.all(user.confirmationData!.map(item => recordToFormItem(item, user.confirmationBranch!))), start_time: user.confirmationStartTime && user.confirmationStartTime.toDateString(), submit_time: user.confirmationSubmitTime && user.confirmationSubmitTime.toDateString() } : undefined; + let team = user.teamId ? await Team.findById(user.teamId) : null; + return { id: user.uuid, - name: user.name, + name: user.name || "", email: user.email, - email_verified: !!user.verifiedEmail, + admin: !!user.admin, applied: !!user.applied, accepted: !!user.accepted, - accepted_and_notified: !!user.acceptedEmailSent, - attending: !!user.attending, + accepted_and_notified: !!user.preConfirmEmailSent, + confirmed: !!user.confirmed, + confirmationBranch: user.confirmationBranch, application, confirmation, @@ -254,7 +322,8 @@ function userRecordToGraphql(user: IUser): types.User { // Will be filled in child resolver. questions: [], team: user.teamId && { - id: user.teamId.toHexString() + id: user.teamId.toHexString(), + name: team ? team.teamName : "(Missing team)" }, pagination_token: user._id.toHexString() diff --git a/server/routes/api/helpscout.ts b/server/routes/api/helpscout.ts new file mode 100644 index 00000000..5a2bd3e4 --- /dev/null +++ b/server/routes/api/helpscout.ts @@ -0,0 +1,122 @@ +import * as express from "express"; +import {isHelpScoutIntegrationEnabled, validateHelpScoutSignature} from "../../middleware"; +import { + IFormItem, + IHelpScoutEmailNotFoundTemplate, + IHelpScoutFormItem, + IHelpScoutMainTemplate, + IUser, + User +} from "../../schema"; +import bodyParser = require("body-parser"); +import * as moment from "moment-timezone"; +import {Template} from "../templates"; +import * as Handlebars from "handlebars"; +import * as Branches from "../../branch"; +import {config} from "../../common"; + +export const helpScoutRoutes = express.Router({"mergeParams": true}); + +export type RequestWithRawBody = express.Request & { rawBody: string }; +helpScoutRoutes.use(isHelpScoutIntegrationEnabled); +helpScoutRoutes.use(bodyParser.json({ + verify: (req: RequestWithRawBody, res, buffer, encoding) => { + if (buffer && buffer.length) { + req.rawBody = buffer.toString(encoding || 'utf-8'); + } + } +})); +helpScoutRoutes.use(validateHelpScoutSignature); +helpScoutRoutes.route("/userInfo").post(helpScoutUserInfoHandler); + +async function findUserByEmail(email: string) { + return User.findOne({ + email + }); +} + +function safe(text: string) { + return Handlebars.Utils.escapeExpression(text); +} + +const EmailNotFoundTemplate = new Template("helpscout/email_not_found.html"); +const MainHelpScoutTemplate = new Template("helpscout/main.html"); + +async function getFormAnswers(userData: IFormItem[], branch: string): Promise { + let branchName = await Branches.BranchConfig.getCanonicalName(branch); + let questionBranch = branchName ? await Branches.BranchConfig.loadBranchFromDB(branchName) : null; + if (questionBranch) { + const hsQuestionNames = questionBranch?.questions + .filter(question => question.showInHelpScout) + .map(question => question.name); + + return userData + .filter(question => hsQuestionNames.includes(question.name)) + .map((question: IFormItem): IHelpScoutFormItem => { + let name = question.name.replace(/-/g, " "); + name = `${name.charAt(0).toUpperCase()}${name.slice(1)}`; + + let prettyValue: string = ""; + + if (!question.value) { + prettyValue = "No response"; + } else if (question.type === "file") { + const file = question.value as Express.Multer.File; + prettyValue = file.path; + } else if (question.value instanceof Array) { + prettyValue = question.value.join(", "); + } else { + prettyValue = question.value as string; + } + + return { + ...question, + prettyValue, + name + }; + }); + } + + return []; +} + +async function helpScoutUserInfoHandler(request: express.Request, response: express.Response) { + const email = safe(request.body.customer.email); + const user: IUser | null = await findUserByEmail(email); + + if (!user) { + response.status(200).json({ + html: EmailNotFoundTemplate.render({ email }) + }); + } else { + const helpScoutInput: IHelpScoutMainTemplate = { + name: user.name, + email: user.email, + uuid: user.uuid, + rootURL: config.server.rootURL, + applicationSubmitTime: user.applicationSubmitTime ? moment(user.applicationSubmitTime) + .format("DD-MMM-YYYY h:mm a") : undefined, + applicationQuestionsToShow: [], + confirmationQuestionsToShow: [], + applied: user.applied, + accepted: user.accepted, + confirmed: user.confirmed, + applicationBranch: user.applicationBranch, + confirmationBranch: user.confirmationBranch, + confirmationSubmitTime: user.confirmationSubmitTime ? moment(user.confirmationSubmitTime) + .format("DD-MMM-YYYY h:mm a") : undefined + }; + + if (user.applicationBranch && user.applicationData) { + helpScoutInput.applicationQuestionsToShow = await getFormAnswers(user.applicationData, user.applicationBranch); + } + + if (user.confirmationBranch && user.confirmationData) { + helpScoutInput.confirmationQuestionsToShow = await getFormAnswers(user.confirmationData, user.confirmationBranch); + } + + response.status(200).json({ + "html": MainHelpScoutTemplate.render(helpScoutInput) + }); + } +} diff --git a/server/routes/api/settings.ts b/server/routes/api/settings.ts index f0d2a111..998eac59 100644 --- a/server/routes/api/settings.ts +++ b/server/routes/api/settings.ts @@ -1,17 +1,13 @@ import * as express from "express"; import { - getSetting, updateSetting, setDefaultSettings, renderEmailHTML, renderEmailText + getSetting, updateSetting, renderEmailHTML, renderEmailText, renderPageHTML, defaultEmailSubjects, agenda } from "../../common"; import { isAdmin, uploadHandler } from "../../middleware"; import * as Branches from "../../branch"; -import {User} from "../../schema"; - -setDefaultSettings().catch(err => { - throw err; -}); +import { IUser, User } from "../../schema"; export let settingsRoutes = express.Router(); @@ -129,38 +125,40 @@ settingsRoutes.route("/branch_roles") } try { - // TODO use promises/async for (let branchName of Object.keys(request.body)) { let branchData = JSON.parse(request.body[branchName]); - let branch = await Branches.BranchConfig.loadBranchFromDB(branchName); // TODO type checker determines this to be always a NoopBranch - ensure that branch.type will be the real type everytime + let branch = await Branches.BranchConfig.loadBranchFromDB(branchName); // This to be always a NoopBranch because it is the super class - ensure that branch.type will be the real type everytime // Convert the branch type (if not match) if (branch.type !== branchData.role) { switch (branchData.role) { case "Application": - branch = await branch.convertTo("Application") as Branches.ApplicationBranch; + branch = await branch.convertTo("Application"); break; case "Confirmation": - branch = await branch.convertTo("Confirmation") as Branches.ConfirmationBranch; + branch = await branch.convertTo("Confirmation"); break; default: - branch = await branch.convertTo("Noop") as Branches.NoopBranch; + branch = await branch.convertTo("Noop"); break; } } // 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 || "disabled"; } // Set rolling deadline flag (if confirmation branch) if (branch instanceof Branches.ConfirmationBranch) { branch.usesRollingDeadline = branchData.usesRollingDeadline || false; + branch.isAcceptance = branchData.isAcceptance || false; + branch.autoConfirm = branchData.autoConfirm || false; } await branch.save(); @@ -180,19 +178,41 @@ settingsRoutes.route("/branch_roles") settingsRoutes.route("/email_content/:type") .get(isAdmin, async (request, response) => { let content: string; + let subject: string; try { content = await getSetting(`${request.params.type}-email`, false); } - catch (err) { + catch { // Content not set yet content = ""; } + try { + subject = await getSetting(`${request.params.type}-email-subject`, false); + } + catch { + // Subject not set yet + let type: string = request.params.type; + if (type.match(/-apply$/)) { + subject = defaultEmailSubjects.apply; + } + else if (type.match(/-pre-confirm$/)) { + subject = defaultEmailSubjects.preConfirm; + } + else if (type.match(/-attend$/)) { + subject = defaultEmailSubjects.attend; + } + else { + subject = ""; + } + } - response.json({ content }); + response.json({ subject, content }); }) .put(isAdmin, uploadHandler.any(), async (request, response) => { - let content = request.body.content as string; + let subject: string = request.body.subject; + let content: string = request.body.content; try { + await updateSetting(`${request.params.type}-email-subject`, subject); await updateSetting(`${request.params.type}-email`, content); response.json({ "success": true @@ -210,8 +230,8 @@ settingsRoutes.route("/email_content/:type/rendered") .post(isAdmin, uploadHandler.any(), async (request, response) => { try { let markdown: string = request.body.content; - let html: string = await renderEmailHTML(markdown, request.user); - let text: string = await renderEmailText(html, request.user, true); + let html: string = await renderEmailHTML(markdown, request.user as IUser); + let text: string = await renderEmailText(markdown, request.user as IUser); response.json({ html, text }); } @@ -222,3 +242,91 @@ settingsRoutes.route("/email_content/:type/rendered") }); } }); + +settingsRoutes.route("/interstitial/:type") + .get(isAdmin, async (request, response) => { + let content: string; + try { + content = await getSetting(`${request.params.type}-interstitial`, false); + } + catch { + // Content not set yet + content = ""; + } + response.json({ content }); + }) + .put(isAdmin, uploadHandler.any(), async (request, response) => { + let content: string = request.body.content; + try { + await updateSetting(`${request.params.type}-interstitial`, content); + response.json({ + "success": true + }); + } + catch (err) { + console.error(err); + response.status(500).json({ + "error": "An error occurred while setting interstitial content" + }); + } + }); + +settingsRoutes.route("/interstitial/:type/rendered") + .post(isAdmin, uploadHandler.any(), async (request, response) => { + try { + let markdown: string = request.body.content; + let html = await renderPageHTML(markdown, request.user as IUser); + + response.json({ html }); + } + catch (err) { + console.error(err); + response.status(500).json({ + "error": "An error occurred while rendering the interstitial content" + }); + } + }); + +settingsRoutes.route("/send_batch_email") + .post(isAdmin, uploadHandler.any(), async (request, response) => { + let filter = JSON.parse(request.body.filter); + let subject = request.body.subject as string; + let markdownContent = request.body.markdownContent; + if (typeof filter !== "object") { + return response.status(400).json({ + "error": `Your query '${filter}' is not a valid MongoDB query` + }); + } else if (subject === "" || subject === undefined) { + return response.status(400).json({ + "error": "Can't have an empty subject!" + }); + } else if (markdownContent === "" || markdownContent === undefined) { + return response.status(400).json({ + "error": "Can't have an empty email body!" + }); + } + + let users = await User.find(filter); + for (let user of users) { + await agenda.now("send_templated_email", { + id: user.uuid, + subject, + markdown: markdownContent + }); + } + + let admins = await User.find({ admin: true }); + subject = `[Admin FYI] ${subject}`; + for (let user of admins) { + await agenda.now("send_templated_email", { + id: user.uuid, + subject: subject + JSON.stringify(filter), + markdown: markdownContent + }); + } + console.log(`Sent ${users.length} batch emails requested by ${(request.user as IUser).email}`); + return response.json({ + "success": true, + "count": users.length + }); + }); diff --git a/server/routes/api/user.ts b/server/routes/api/user.ts index 02e4ad7a..56033551 100644 --- a/server/routes/api/user.ts +++ b/server/routes/api/user.ts @@ -2,26 +2,29 @@ import * as path from "path"; import * as express from "express"; import * as json2csv from "json2csv"; import * as archiver from "archiver"; -import * as moment from "moment-timezone"; +import * as uuid from "uuid/v4"; import { STORAGE_ENGINE, formatSize, - config, getSetting, renderEmailHTML, renderEmailText, sendMailAsync + config, getSetting, defaultEmailSubjects, agenda } from "../../common"; import { MAX_FILE_SIZE, postParser, uploadHandler, isAdmin, isUserOrAdmin, ApplicationType, - trackEvent + trackEvent, canUserModify } from "../../middleware"; import { + Model, createNew, IFormItem, - IUserMongoose, User, - ITeamMongoose, Team + IUser, User, + Team } from "../../schema"; import * as Branches from "../../branch"; +import { GroundTruthStrategy } from "../strategies"; export let userRoutes = express.Router({ "mergeParams": true }); +export let registrationRoutes = express.Router({ "mergeParams": true }); let postApplicationBranchErrorHandler: express.ErrorRequestHandler = (err, request, response, next) => { if (err.code === "LIMIT_FILE_SIZE") { @@ -34,223 +37,295 @@ 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({ - "error": "Invalid application branch" - }); - return; - } - - let user = request.user as IUserMongoose; - - let openDate = branch.open; - let closeDate = branch.close; - if (requestType === ApplicationType.Confirmation && user.confirmationDeadlines) { - let times = user.confirmationDeadlines.find((d) => d.name === branch.name); - if (times) { - openDate = times.open; - closeDate = times.close; - } - } - - if (moment().isBetween(openDate, closeDate) || request.user.isAdmin) { - next(); - } - else { - response.status(408).json({ - "error": `${requestType === ApplicationType.Application ? "Applications" : "Confirmations"} are currently closed` - }); - return; - } -}; +// We don't use canUserModify here, instead check for admin +registrationRoutes.route("/:branch") + .post( + isAdmin, + postParser, + uploadHandler.any(), + postApplicationBranchErrorHandler, + postApplicationBranchHandler(true) + ); userRoutes.route("/application/:branch") - .post(isUserOrAdmin, 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); + .post( + isUserOrAdmin, + canUserModify, + postParser, + uploadHandler.any(), + postApplicationBranchErrorHandler, + postApplicationBranchHandler(false) + ) + .delete( + isUserOrAdmin, + canUserModify, + 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()) { - response.status(400).json({ - "error": "You can only edit the confirmation branch that you originally submitted" - }); - 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) { - response.status(400).json({ - "error": "Invalid application branch" - }); - return; - } +userRoutes.route("/confirmation/:branch") + .post( + isUserOrAdmin, + canUserModify, + postParser, + uploadHandler.any(), + postApplicationBranchErrorHandler, + postApplicationBranchHandler(false) + ) + .delete( + isUserOrAdmin, + canUserModify, + deleteApplicationBranchHandler + ); + +function postApplicationBranchHandler(anonymous: boolean): (request: express.Request, response: express.Response) => Promise { + return async (request, response) => { + let user: Model; + if (anonymous) { + let email = request.body["anonymous-registration-email"] as string; + let name = request.body["anonymous-registration-name"] as string; + if (await User.findOne({ email })) { + response.status(400).json({ + "error": `User with email "${email}" already exists` + }); + return; + } + user = createNew(User, { + ...GroundTruthStrategy.defaultUserProperties, + uuid: uuid(), + name, + email, + token: null + }); + } else { + let existingUser = await User.findOne({ uuid: request.params.uuid }); + if (!existingUser) { + response.status(400).json({ + "error": "Invalid user id" + }); + return; + } + user = existingUser; + } - 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 branchName = await Branches.BranchConfig.getCanonicalName(request.params.branch); + let questionBranch = branchName ? await Branches.BranchConfig.loadBranchFromDB(branchName) : null; + if (!questionBranch) { + response.status(400).json({ + "error": "Invalid 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 - }; - } - else { + + let unchangedFiles: string[] = []; + let errored: boolean = false; // Used because .map() can't be broken out of + let rawData: (IFormItem | null)[] = questionBranch.questions.map(question => { + function reportError(message: string): null { errored = true; response.status(400).json({ - "error": `'${question.label}' is a required field` + "error": message }); 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] = []; + function getQuestion(defaultValue?: T): T { + let value = request.body[question.name] as T; + if (defaultValue !== undefined) { + return value || defaultValue; + } + return value; } - if (!Array.isArray(request.body[question.name])) { - request.body[question.name] = [request.body[question.name]]; + + 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 != undefined + && user.applicationData.some(entry => entry.name === question.name && !!entry.value); + + if (question.required && !getQuestion() && !files.find(file => file.fieldname === question.name)) { + // Required field not filled in + if (preexistingFile) { + let previousValue = user.applicationData!.find(entry => entry.name === question.name && !!entry.value)!.value as Express.Multer.File; + unchangedFiles.push(previousValue.filename); + return { + "name": question.name, + "type": "file", + "value": previousValue + }; } else { - return Promise.resolve(); + return reportError(`"${question.label}" is a required field`); + } + } + if (question.type !== "file" && (question.minCharacterCount || question.maxCharacterCount || question.minWordCount || question.maxWordCount)) { + let questionValue = getQuestion(""); + if (question.minCharacterCount && questionValue.length < question.minCharacterCount) { + return reportError(`Your response to "${question.label}" must have at least ${question.minCharacterCount} characters`); + } + if (question.maxCharacterCount && questionValue.length > question.maxCharacterCount) { + return reportError(`Your response to "${question.label}" cannot exceed ${question.maxCharacterCount} characters`); + } + let wordCount = questionValue.trim().split(/\s+/).length; + if (questionValue.trim().length === 0) { + wordCount = 0; + } + const wordCountPlural = wordCount === 1 ? "" : "s"; + if (question.minWordCount && wordCount < question.minWordCount) { + return reportError(`Your response to "${question.label}" must contain at least ${question.minWordCount} words (currently has ${wordCount} word${wordCountPlural})`); } - }) - ); - // 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); + if (question.maxWordCount && wordCount > question.maxWordCount) { + return reportError(`Your response to "${question.label}" cannot exceed ${question.maxWordCount} words (currently has ${wordCount} word${wordCountPlural})`); + } + } + + if ((question.type === "select" || question.type === "radio") && Array.isArray(getQuestion(question.name)) && question.hasOther) { + // "Other" option selected + request.body[question.name] = getQuestion().pop(); } - return item; + else if (question.type === "checkbox" && question.hasOther) { + if (!getQuestion(question.name)) { + request.body[question.name] = []; + } + if (!Array.isArray(getQuestion(question.name))) { + request.body[question.name] = [getQuestion()]; + } + // Filter out "other" option + request.body[question.name] = getQuestion().filter(value => value !== "Other"); + } + return { + "name": question.name, + "type": question.type, + "value": getQuestion(files.find(file => file.fieldname === question.name)) + }; }); - // Email the applicant to confirm - let emailMarkdown: string; - try { - let type = requestType === ApplicationType.Application ? "apply" : "attend"; - emailMarkdown = await getSetting(`${questionBranch.name}-${type}-email`, false); - } - catch (err) { - // 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: `[${config.eventName}] - Thank you for applying!`, - html: emailHTML, - text: emailText - }); + 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); + } + catch { + emailSubject = null; + } + let emailMarkdown: string; + try { + emailMarkdown = await getSetting(`${questionBranch.name}-${type}-email`, false); } - 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} = {}; - for (let ele of data) { - if (ele && ele.name && ele.value) { - tags[ele.name.toString()] = ele.value.toString(); + catch { + // Content not set yet + emailMarkdown = ""; + } + + if (questionBranch instanceof Branches.ApplicationBranch) { + if (!user.applied) { + await agenda.now("send_templated_email", { + id: user.uuid, + subject: emailSubject || defaultEmailSubjects.apply, + markdown: emailMarkdown + }); + } + 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 && questionBranch.autoAccept !== "disabled") { + await updateUserStatus(user, questionBranch.autoAccept); + } + + } else if (questionBranch instanceof Branches.ConfirmationBranch) { + if (!user.confirmed) { + await agenda.now("send_templated_email", { + id: user.uuid, + subject: emailSubject || defaultEmailSubjects.attend, + markdown: emailMarkdown + }); + } + user.confirmed = 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 application", request, user.email, tags); + + await user.save(); + response.status(200).json({ + "uuid": user.uuid, + "success": true + }); } - else if (requestType === ApplicationType.Confirmation) { - if (!user.attending) { - await sendMailAsync({ - from: config.email.from, - to: user.email, - subject: `[${config.eventName}] - Thank you for RSVPing!`, - html: emailHTML, - text: emailText - }); - } - user.attending = true; - user.confirmationBranch = questionBranch.name; - user.confirmationData = data; - user.markModified("confirmationData"); - user.confirmationSubmitTime = new Date(); + 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; + let user = await User.findOne({ uuid: request.params.uuid }); + if (!user) { + response.status(400).json({ + "error": "Invalid user id" + }); + return; + } if (requestType === ApplicationType.Application) { user.applied = false; + user.accepted = false; + user.confirmed = false; user.applicationBranch = ""; user.applicationData = []; user.markModified("applicationData"); @@ -258,8 +333,7 @@ async function deleteApplicationBranchHandler(request: express.Request, response user.applicationStartTime = undefined; } else if (requestType === ApplicationType.Confirmation) { - user.attending = false; - user.confirmationBranch = ""; + user.confirmed = false; user.confirmationData = []; user.markModified("confirmationData"); user.confirmationSubmitTime = undefined; @@ -282,7 +356,7 @@ async function deleteApplicationBranchHandler(request: express.Request, response userRoutes.route("/status").post(isAdmin, uploadHandler.any(), async (request, response): Promise => { let user = await User.findOne({uuid: request.params.uuid}); - let status = request.body.status as ("accepted" | "no-decision"); + let status = request.body.status as string; if (!user) { response.status(400).json({ @@ -291,9 +365,8 @@ userRoutes.route("/status").post(isAdmin, uploadHandler.any(), async (request, r return; } - await updateUserStatus(user, status); - try { + await updateUserStatus(user, status); await user.save(); response.status(200).json({ "success": true @@ -302,50 +375,92 @@ userRoutes.route("/status").post(isAdmin, uploadHandler.any(), async (request, r catch (err) { console.error(err); response.status(500).json({ - "error": "An error occurred while accepting or rejecting the user" + // This endpoint is admin-only so directly communicating error messages is fine + "error": `Error occurred while changing user status: ${err.message}` }); } }); -async function updateUserStatus(user: IUserMongoose, status: ("accepted" | "no-decision")): Promise { - if (status === "no-decision") { +async function updateUserStatus(user: Model, status: string): Promise { + if (status === user.confirmationBranch) { + throw new Error(`User status is already ${status}!`); + } else if (status === "no-decision") { + // Clear all confirmation data + user.confirmationBranch = undefined; + user.confirmationData = []; + user.confirmationDeadline = undefined; + user.confirmationStartTime = undefined; + user.confirmationSubmitTime = undefined; + user.confirmed = false; user.accepted = false; - user.confirmationDeadlines = []; - } else if (status === "accepted") { - user.accepted = true; - let applicationBranch = (await Branches.BranchConfig.loadBranchFromDB(user.applicationBranch)) as Branches.ApplicationBranch; - user.confirmationDeadlines = ((await Branches.BranchConfig.loadAllBranches("Confirmation")) as Branches.ConfirmationBranch[]) - .filter(c => c.usesRollingDeadline) - .filter(c => applicationBranch.confirmationBranches.indexOf(c.name) > -1); + user.preConfirmEmailSent = false; + } else { + let confirmationBranch = await Branches.BranchConfig.loadBranchFromDB(status) as Branches.ConfirmationBranch; + + if (confirmationBranch) { + // Clear all confirmation data + user.confirmationData = []; + user.confirmationDeadline = undefined; + user.confirmationStartTime = undefined; + user.confirmationSubmitTime = undefined; + user.confirmed = false; + user.accepted = false; + user.preConfirmEmailSent = false; + + // Update branch + user.confirmationBranch = status; + + // Handle rolling deadline + if (confirmationBranch.usesRollingDeadline) { + user.confirmationDeadline = { + name: confirmationBranch.name, + open: confirmationBranch.open, + close: confirmationBranch.close + }; + } + + // Handle accptance + if (confirmationBranch.isAcceptance) { + user.accepted = true; + } + + // Handle no-op confirmation + if (confirmationBranch.autoConfirm) { + user.confirmed = true; + } + } else { + throw new Error("Confirmation branch not valid!"); + } } } userRoutes.route("/send_acceptances").post(isAdmin, async (request, response): Promise => { try { - let users = await User.find({ "accepted": true, "acceptedEmailSent": { $ne: true } }); + let users = await User.find({ "confirmationBranch": {$exists: true}, "preConfirmEmailSent": { $ne: true } }); for (let user of users) { // Email the applicant about their acceptance + let emailSubject: string | null; + try { + emailSubject = await getSetting(`${user.confirmationBranch}-pre-confirm-email-subject`, false); + } + catch { + emailSubject = null; + } let emailMarkdown: string; try { - emailMarkdown = await getSetting(`${user.applicationBranch}-accept-email`, false); + emailMarkdown = await getSetting(`${user.confirmationBranch}-pre-confirm-email`, false); } - catch (err) { + catch { // Content not set yet emailMarkdown = ""; } - let emailHTML = await renderEmailHTML(emailMarkdown, user); - let emailText = await renderEmailText(emailHTML, user, true); - - await sendMailAsync({ - from: config.email.from, - to: user.email, - subject: `[${config.eventName}] - You've been accepted!`, - html: emailHTML, - text: emailText + user.preConfirmEmailSent = true; + await agenda.now("send_templated_email", { + id: user.uuid, + subject: emailSubject || defaultEmailSubjects.preConfirm, + markdown: emailMarkdown }); - - user.acceptedEmailSent = true; await user.save(); } @@ -362,44 +477,6 @@ userRoutes.route("/send_acceptances").post(isAdmin, async (request, response): P } }); -userRoutes.route("/batch_accept").post(isAdmin, postParser, uploadHandler.any(), async (request, response): Promise => { - try { - let userIds = request.body.userIds.split(','); - let acceptedCount = 0; - let notAccepted = []; - for (let userId of userIds) { - let user; - try { - user = await User.findById(userId); - } catch { - notAccepted.push(userId); - } - if (user && user.applied && !user.accepted) { - try { - await updateUserStatus(user, "accepted"); // Assume that this succeeds - await user.save(); - acceptedCount += 1; - } catch { - notAccepted.push(userId); - } - } else { - notAccepted.push(userId); - } - } - response.json({ - "success": true, - "count": acceptedCount, - "notAccepted": notAccepted - }); - } - catch (err) { - console.error(err); - response.status(500).json({ - "error": "An error occurred while batch accepting applicants" - }); - } -}); - userRoutes.route("/export").get(isAdmin, async (request, response): Promise => { try { let archive = archiver("zip", { @@ -411,14 +488,14 @@ userRoutes.route("/export").get(isAdmin, async (request, response): Promise { // TODO: Replace with more robust schema-agnostic version - let nameFormItem = user.applicationData.find(item => item.name === "name"); + let nameFormItem = (user.applicationData || []).find(item => item.name === "name"); return { "name": nameFormItem && typeof nameFormItem.value === "string" ? nameFormItem.value : user.name, "email": user.email @@ -436,12 +513,12 @@ userRoutes.route("/export").get(isAdmin, async (request, response): Promise { +async function removeUserFromAllTeams(user: IUser): Promise { if (!user.teamId) { return true; } - let currentUserTeam = await Team.findById(user.teamId) as ITeamMongoose; + let currentUserTeam = await Team.findById(user.teamId); if (!currentUserTeam) { return false; } @@ -487,13 +564,18 @@ userRoutes.route("/team/create/:teamName").post(isUserOrAdmin, async (request, r Else if the user's in a team, take them out of it Else make them a new team */ - let user = await User.findOne({uuid: request.params.uuid}) as IUserMongoose; + let user = await User.findOne({ uuid: request.params.uuid }); + if (!user) { + response.status(400).json({ + "error": "Invalid user id" + }); + return; + } let decodedTeamName = decodeURI(request.params.teamName); - let existingTeam = await Team.findOne({ teamName: decodedTeamName }) as ITeamMongoose; + let existingTeam = await Team.findOne({ teamName: decodedTeamName }); if (existingTeam) { - // Someone else has a team with this name response.status(400).json({ "error": `Someone else has a team called ${decodedTeamName}. Please pick a different name.` @@ -503,22 +585,24 @@ userRoutes.route("/team/create/:teamName").post(isUserOrAdmin, async (request, r // If the user is in a team, remove them from their current team unless they're the team leader if (user.teamId) { - let currentUserTeam = await Team.findById(user.teamId) as ITeamMongoose; + let currentUserTeam = await Team.findById(user.teamId); - if (currentUserTeam.teamLeader === user._id) { - // The user is in the team they made already - // Ideally this will never happen if we do some validation client side - response.status(400).json({ - "error": "You're already the leader of this team" - }); - return; - } + if (currentUserTeam) { + if (currentUserTeam.teamLeader === user._id) { + // The user is in the team they made already + // Ideally this will never happen if we do some validation client side + response.status(400).json({ + "error": "You're already the leader of this team" + }); + return; + } - await removeUserFromAllTeams(user); + await removeUserFromAllTeams(user); + } } let query = { - teamLeader: request.user._id, + teamLeader: request.user && request.user._id, members: { $in: [user._id] }, @@ -531,9 +615,11 @@ userRoutes.route("/team/create/:teamName").post(isUserOrAdmin, async (request, r setDefaultsOnInsert: true }; - let team = await Team.findOneAndUpdate(query, {}, options) as ITeamMongoose; + let team = await Team.findOneAndUpdate(query, {}, options); - user.teamId = team._id; + if (team) { + user.teamId = team._id; + } await user.save(); response.json({ @@ -542,7 +628,14 @@ userRoutes.route("/team/create/:teamName").post(isUserOrAdmin, async (request, r }); userRoutes.route("/team/join/:teamName").post(isUserOrAdmin, async (request, response): Promise => { - let user = await User.findOne({uuid: request.params.uuid}) as IUserMongoose; + let user = await User.findOne({ uuid: request.params.uuid }); + if (!user) { + response.status(400).json({ + "error": "Invalid user id" + }); + return; + } + let decodedTeamName = decodeURI(request.params.teamName); if (user.teamId) { @@ -558,7 +651,7 @@ userRoutes.route("/team/join/:teamName").post(isUserOrAdmin, async (request, res return; } - let teamToJoin = await Team.findOne({ teamName: decodedTeamName }) as ITeamMongoose; + let teamToJoin = await Team.findOne({ teamName: decodedTeamName }); if (!teamToJoin) { // If the team they tried to join isn't real... @@ -595,7 +688,13 @@ userRoutes.route("/team/join/:teamName").post(isUserOrAdmin, async (request, res }); userRoutes.route("/team/leave").post(isUserOrAdmin, async (request, response): Promise => { - let user = await User.findOne({uuid: request.params.uuid}) as IUserMongoose; + let user = await User.findOne({ uuid: request.params.uuid }); + if (!user) { + response.status(400).json({ + "error": "Invalid user id" + }); + return; + } await removeUserFromAllTeams(user); response.status(200).json({ @@ -604,8 +703,13 @@ userRoutes.route("/team/leave").post(isUserOrAdmin, async (request, response): P }); userRoutes.route("/team/rename/:newTeamName").post(isUserOrAdmin, async (request, response): Promise => { - let user = await User.findOne({uuid: request.params.uuid}) as IUserMongoose; - + let user = await User.findOne({ uuid: request.params.uuid }); + if (!user) { + response.status(400).json({ + "error": "Invalid user id" + }); + return; + } if (!user.teamId) { response.status(400).json({ "error": "You're not in a team" @@ -613,8 +717,7 @@ userRoutes.route("/team/rename/:newTeamName").post(isUserOrAdmin, async (request return; } - let currentUserTeam = await Team.findById(user.teamId) as ITeamMongoose; - + let currentUserTeam = await Team.findById(user.teamId); if (!currentUserTeam) { // User tried to change their team name even though they don't have a team response.status(400).json({ @@ -650,3 +753,27 @@ userRoutes.route("/team/rename/:newTeamName").post(isUserOrAdmin, async (request "success": true }); }); + +userRoutes.get("/", async (request, response) => { + if (request.user) { + let user = await User.findOne({ uuid: request.user.uuid }); + if (!user) { + response.status(400).json({ + "error": "Invalid user id" + }); + return; + } + + response.json({ + uuid: user.uuid, + name: user.name, + email: user.email, + admin: user.admin || false + }); + } + else { + response.json({ + "error": "Not logged in" + }); + } +}); diff --git a/server/routes/auth.ts b/server/routes/auth.ts index 3e89867b..d50dac79 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -1,52 +1,23 @@ -import * as http from "http"; -import * as https from "https"; +import { URL } from "url"; import * as crypto from "crypto"; -import * as path from "path"; -import * as moment from "moment-timezone"; import * as express from "express"; import * as session from "express-session"; import * as connectMongo from "connect-mongo"; -import * as uuid from "uuid/v4"; const MongoStore = connectMongo(session); import * as passport from "passport"; import { - config, mongoose, COOKIE_OPTIONS, pbkdf2Async, renderEmailHTML, renderEmailText, sendMailAsync + config, mongoose, COOKIE_OPTIONS } from "../common"; import { - postParser, trackEvent -} from "../middleware"; -import { - IUser, IUserMongoose, User + IUser, User } from "../schema"; +import { + AuthenticateOptions, GroundTruthStrategy, createLink, validateAndCacheHostName +} from "./strategies"; // Passport authentication -import {app} from "../app"; - -// tslint:disable:no-var-requires -// No type definitions available yet for these module (or for Google) -const GitHubStrategy = require("passport-github2").Strategy; -const GoogleStrategy = require("passport-google-oauth20").Strategy; -import {Strategy as FacebookStrategy} from "passport-facebook"; -import {Strategy as LocalStrategy} from "passport-local"; - -// GitHub -if (!config.secrets.github.id || !config.secrets.github.secret) { - throw new Error("GitHub client ID or secret not configured in config.json or environment variables"); -} -const GITHUB_CALLBACK_HREF: string = "auth/github/callback"; - -// Google -if (!config.secrets.google.id || !config.secrets.google.secret) { - throw new Error("Google client ID or secret not configured in config.json or environment variables"); -} -const GOOGLE_CALLBACK_HREF: string = "auth/google/callback"; - -// Facebook -if (!config.secrets.facebook.id || !config.secrets.facebook.secret) { - throw new Error("Facebook client ID or secret not configured in config.json or environment variables"); -} -const FACEBOOK_CALLBACK_HREF: string = "auth/facebook/callback"; +import { app } from "../app"; if (!config.server.isProduction) { console.warn("OAuth callback(s) running in development mode"); @@ -68,478 +39,64 @@ app.use(session({ saveUninitialized: false })); passport.serializeUser((user, done) => { - done(null, user._id.toString()); + done(null, user.uuid); }); passport.deserializeUser((id, done) => { - User.findById(id, (err, user) => { + User.findOne({ uuid: id }, (err, user) => { done(err, user!); }); }); -// tslint:disable-next-line:interface-over-type-literal -type OAuthStrategyOptions = { - clientID: string; - clientSecret: string; - profileFields?: string[]; - passReqToCallback: boolean; -}; -type Profile = passport.Profile & { - profileUrl?: string; - _json: any; -}; -function useLoginStrategy(strategy: any, dataFieldName: "githubData" | "googleData" | "facebookData", options: OAuthStrategyOptions) { - passport.use(new strategy(options, async (request: express.Request, accessToken: string, refreshToken: string, profile: Profile, done: (err: Error | null, user?: IUserMongoose | false, errMessage?: object) => void) => { - let email: string = ""; - if (profile.emails && profile.emails.length > 0) { - email = profile.emails[0].value; - } - else if (!profile.emails || profile.emails.length === 0) { - done(null, false, { message: "Your GitHub profile does not have any public email addresses. Please make an email address public before logging in with GitHub." }); - return; - } - else if (!profile.displayName || !profile.displayName.trim()) { - done(null, false, { message: "Your profile does not have a publically visible name. Please set a name on your account before logging in." }); - return; - } - let user = await User.findOne({"email": email}); - let isAdmin = false; - if (config.admins.indexOf(email) !== -1) { - isAdmin = true; - if (!user || !user.admin) { - console.info(`Adding new admin: ${email}`); - } - } - if (!user) { - user = new User({ - "uuid": uuid(), - "email": email, - "name": profile.displayName, - "verifiedEmail": true, - - "localData": {}, - "githubData": {}, - "googleData": {}, - "facebookData": {}, - - "applied": false, - "accepted": false, - "attending": false, - "applicationData": [], - "applicationStartTime": undefined, - "applicationSubmitTime": undefined, - - "admin": isAdmin - }); - user[dataFieldName]!.id = profile.id; - if (dataFieldName === "githubData" && profile.username && profile.profileUrl) { - user[dataFieldName]!.username = profile.username; - user[dataFieldName]!.profileUrl = profile.profileUrl; - } - try { - await user.save(); - trackEvent("created account (auth)", request, email); - } - catch (err) { - done(err); - return; - } - - done(null, user); - } - else { - if (!user[dataFieldName] || !user[dataFieldName]!.id) { - user[dataFieldName] = { - id: profile.id - }; - } - if (!user.verifiedEmail) { - // We trust our OAuth provider to have verified the user's email for us - user.verifiedEmail = true; - } - if (dataFieldName === "githubData" && (!user.githubData || !user.githubData.username || !user.githubData.profileUrl) && (profile.username && profile.profileUrl)) { - user.githubData!.username = profile.username; - user.githubData!.profileUrl = profile.profileUrl; - } - if (!user.admin && isAdmin) { - user.admin = true; - } - await user.save(); - done(null, user); - } - })); -} - -useLoginStrategy(GitHubStrategy, "githubData", { - clientID: config.secrets.github.id, - clientSecret: config.secrets.github.secret, - passReqToCallback: true -}); -useLoginStrategy(GoogleStrategy, "googleData", { - clientID: config.secrets.google.id, - clientSecret: config.secrets.google.secret, - passReqToCallback: true -}); -useLoginStrategy(FacebookStrategy, "facebookData", { - clientID: config.secrets.facebook.id, - clientSecret: config.secrets.facebook.secret, - profileFields: ["id", "displayName", "email"], - passReqToCallback: true -}); - -const PBKDF2_ROUNDS: number = 300000; -passport.use(new LocalStrategy({ - usernameField: "email", - passwordField: "password", - passReqToCallback: true -}, async (request, email, password, done) => { - let user = await User.findOne({ "email": email }); - if (user && request.path.match(/\/signup$/i)) { - done(null, false, { "message": "That email address is already in use" }); - } - else if (user && !user.localData!.hash && (user.githubData!.id || user.googleData!.id || user.facebookData!.id)) { - done(null, false, { "message": "Please log back in with GitHub, Google, or Facebook" }); - } - else if (!user || !user.localData) { - // User hasn't signed up yet - if (!request.path.match(/\/signup$/i)) { - // Only create the user when targeting /signup - done(null, false, { "message": "Incorrect email or password" }); - return; - } - let name: string = request.body.name || ""; - name = name.trim(); - if (!name || !email || !password) { - done(null, false, { "message": "Missing email, name, or password" }); - return; - } - let isAdmin = false; // Only set this after they have verified their email - let salt = crypto.randomBytes(32); - let hash = await pbkdf2Async(password, salt, PBKDF2_ROUNDS); - user = new User({ - "uuid": uuid(), - "email": email, - "name": request.body.name, - "verifiedEmail": false, - - "localData": { - "hash": hash.toString("hex"), - "salt": salt.toString("hex"), - "verificationCode": crypto.randomBytes(32).toString("hex") - }, - "githubData": {}, - "googleData": {}, - "facebookData": {}, - - "applied": false, - "accepted": false, - "acceptedEmailSent": false, - "attending": false, - "applicationData": [], - "applicationStartTime": undefined, - "applicationSubmitTime": undefined, - - "admin": isAdmin - }); - try { - await user.save(); - trackEvent("created account", request, email); - } - catch (err) { - done(err); - return; - } - // Send verification email (hostname validated by previous middleware) - let link = createLink(request, `/auth/verify/${user.localData!.verificationCode}`); - let markdown = -`Hi {{name}}, - -Thanks for signing up for ${config.eventName}! To verify your email, please [click here](${link}). - -Sincerely, - -The ${config.eventName} Team.`; - try { - await sendMailAsync({ - from: config.email.from, - to: email, - subject: `[${config.eventName}] - Verify your email`, - html: await renderEmailHTML(markdown, user), - text: await renderEmailText(markdown, user) - }); - } - catch (err) { - done(err); - return; - } - done(null, user); - } - else { - // Log the user in - let hash = await pbkdf2Async(password, Buffer.from(user.localData.salt, "hex"), PBKDF2_ROUNDS); - if (hash.toString("hex") === user.localData.hash) { - if (user.verifiedEmail) { - done(null, user); - } - else { - done(null, false, { "message": "You must verify your email before you can sign in" }); - } - } - else { - done(null, false, { "message": "Incorrect email or password" }); - } - } -})); - -app.use(passport.initialize()); -app.use(passport.session()); - export let authRoutes = express.Router(); -function getExternalPort(request: express.Request): number { - function defaultPort(): number { - // Default ports for HTTP and HTTPS - return request.protocol === "http" ? 80 : 443; - } - - let host = request.headers.host; - if (!host || Array.isArray(host)) { - return defaultPort(); - } - - // IPv6 literal support - let offset = host[0] === "[" ? host.indexOf("]") + 1 : 0; - let index = host.indexOf(":", offset); - if (index !== -1) { - return parseInt(host.substring(index + 1), 10); - } - else { - return defaultPort(); - } -} -let validatedHostNames: string[] = []; -function validateAndCacheHostName(request: express.Request, response: express.Response, next: express.NextFunction) { - // Basically checks to see if the server behind the hostname has the same session key by HMACing a random nonce - if (validatedHostNames.find(hostname => hostname === request.hostname)) { - next(); - return; - } - - let nonce = crypto.randomBytes(64).toString("hex"); - function callback(message: http.IncomingMessage) { - if (message.statusCode !== 200) { - console.error(`Got non-OK status code when validating hostname: ${request.hostname}`); - message.resume(); - return; - } - message.setEncoding("utf8"); - let data = ""; - message.on("data", (chunk) => data += chunk); - message.on("end", () => { - let localHMAC = crypto.createHmac("sha256", config.secrets.session).update(nonce).digest().toString("hex"); - if (localHMAC === data) { - validatedHostNames.push(request.hostname); - next(); - } - else { - console.error(`Got invalid HMAC when validating hostname: ${request.hostname}`); - } - }); - } - function onError(err: Error) { - console.error(`Error when validating hostname: ${request.hostname}`, err); - } - if (request.protocol === "http") { - http.get(`http://${request.hostname}:${getExternalPort(request)}/auth/validatehost/${nonce}`, callback).on("error", onError); - } - else { - https.get(`https://${request.hostname}:${getExternalPort(request)}/auth/validatehost/${nonce}`, callback).on("error", onError); - } -} authRoutes.get("/validatehost/:nonce", (request, response) => { let nonce: string = request.params.nonce || ""; response.send(crypto.createHmac("sha256", config.secrets.session).update(nonce).digest().toString("hex")); }); -function createLink(request: express.Request, link: string): string { - if (link[0] === "/") { - link = link.substring(1); - } - if ((request.secure && getExternalPort(request) === 443) || (!request.secure && getExternalPort(request) === 80)) { - return `http${request.secure ? "s" : ""}://${request.hostname}/${link}`; - } - else { - return `http${request.secure ? "s" : ""}://${request.hostname}:${getExternalPort(request)}/${link}`; - } -} - -function addAuthenticationRoute(serviceName: "github" | "google" | "facebook", scope: string[], callbackHref: string) { - authRoutes.get(`/${serviceName}`, validateAndCacheHostName, (request, response, next) => { - let callbackURL = `${request.protocol}://${request.hostname}:${getExternalPort(request)}/${callbackHref}`; - passport.authenticate( - serviceName, - { scope, callbackURL } as passport.AuthenticateOptions - )(request, response, next); - }); - authRoutes.get(`/${serviceName}/callback`, validateAndCacheHostName, (request, response, next) => { - let callbackURL = `${request.protocol}://${request.hostname}:${getExternalPort(request)}/${callbackHref}`; - passport.authenticate( - serviceName, - { failureRedirect: "/login", failureFlash: true, callbackURL } as passport.AuthenticateOptions - )(request, response, next); - }, (request, response) => { - // Successful authentication, redirect home - response.redirect("/"); - }); -} - -addAuthenticationRoute("github", ["user:email"], GITHUB_CALLBACK_HREF); -addAuthenticationRoute("google", ["email", "profile"], GOOGLE_CALLBACK_HREF); -addAuthenticationRoute("facebook", ["email"], FACEBOOK_CALLBACK_HREF); - -authRoutes.post("/signup", validateAndCacheHostName, postParser, passport.authenticate("local", { failureRedirect: "/login", failureFlash: true }), (request, response) => { - // User is logged in automatically by Passport but we want them to verify their email first - request.logout(); - request.flash("success", "Account created successfully. Please verify your email before logging in."); - response.redirect("/login"); -}); - -authRoutes.post("/login", postParser, passport.authenticate("local", { failureRedirect: "/login", failureFlash: true, successRedirect: "/" })); - -authRoutes.get("/verify/:code", async (request, response) => { - let user = await User.findOne({ "localData.verificationCode": request.params.code }); - if (!user) { - request.flash("error", "Invalid email verification code"); - } - else { - user.verifiedEmail = true; - // Possibly promote to admin status - if (config.admins.indexOf(user.email) !== -1) { - user.admin = true; - console.info(`Adding new admin: ${user.email}`); +authRoutes.all("/logout", async (request, response) => { + let user = request.user as IUser | undefined; + if (user) { + let groundTruthURL = new URL(config.secrets.groundTruth.url); + // Invalidates token and logs user out of Ground Truth too + try { + await GroundTruthStrategy.apiRequest("POST", new URL("/api/user/logout", groundTruthURL).toString(), user.token || ""); + } + catch (err) { + // User might already be logged out of Ground Truth + // Log them out of registration and continue + console.error(err); } - await user.save(); - request.flash("success", "Thanks for verifying your email. You can now log in."); - trackEvent("verified email", request, user.email); + request.logout(); + } + if (request.session) { + request.session.loginAction = "render"; } response.redirect("/login"); }); -authRoutes.post("/forgot", validateAndCacheHostName, postParser, async (request, response) => { - let email: string | undefined = request.body.email; - if (!email || !email.toString().trim()) { - request.flash("error", "Invalid email"); - response.redirect("/login/forgot"); - return; - } - email = email.toString().trim(); - - let user = await User.findOne({ email }); - if (!user) { - request.flash("error", "No account matching the email that you submitted was found"); - response.redirect("/login/forgot"); - return; - } - if (!user.verifiedEmail) { - request.flash("error", "Please verify your email first"); - response.redirect("/login"); - return; - } - if (!user.localData) { - request.flash("error", "The account with the email that you submitted has no password set. Please log in with GitHub, Google, or Facebook instead."); - response.redirect("/login"); - return; - } - - user.localData.resetRequested = true; - user.localData.resetRequestedTime = new Date(); - user.localData.resetCode = crypto.randomBytes(32).toString("hex"); - - // Send reset email (hostname validated by previous middleware) - let link = createLink(request, `/auth/forgot/${user.localData.resetCode}`); - let markdown = -`Hi {{name}}, - -You recently asked to reset the password for this account: {{email}}. - -You can update your password by [clicking here](${link}). +app.use(passport.initialize()); +app.use(passport.session()); -If you don't use this link within ${moment.duration(config.server.passwordResetExpiration, "milliseconds").humanize()}, it will expire and you will have to [request a new one](${createLink(request, "/login/forgot")}). +const groundTruthStrategy = new GroundTruthStrategy(config.secrets.groundTruth.url); -Sincerely, +passport.use(groundTruthStrategy); -The ${config.eventName} Team.`; - try { - await user.save(); - await sendMailAsync({ - from: config.email.from, - to: email, - subject: `[${config.eventName}] - Please reset your password`, - html: await renderEmailHTML(markdown, user), - text: await renderEmailText(markdown, user) - }); - request.flash("success", "Please check your email for a link to reset your password. If it doesn't appear within a few minutes, check your spam folder."); - response.redirect("/login/forgot"); - } - catch (err) { - console.error(err); - request.flash("error", "An error occurred while sending you a password reset email"); - response.redirect("/login/forgot"); - } +authRoutes.get("/login", validateAndCacheHostName, (request, response, next) => { + let callbackURL = createLink(request, "auth/login/callback"); + passport.authenticate("oauth2", { callbackURL } as AuthenticateOptions)(request, response, next); }); -authRoutes.post("/forgot/:code", validateAndCacheHostName, postParser, async (request, response) => { - let user = await User.findOne({ "localData.resetCode": request.params.code }); - if (!user) { - request.flash("error", "Invalid password reset code"); - response.redirect("/login"); - return; - } +authRoutes.get("/login/callback", validateAndCacheHostName, (request, response, next) => { + let callbackURL = createLink(request, "auth/login/callback"); - let expirationDuration = moment.duration(config.server.passwordResetExpiration, "milliseconds"); - // TSLint is matching to the wrong type definition when checking for deprecation - // tslint:disable-next-line:deprecation - if (!user.localData!.resetRequested || moment().isAfter(moment(user.localData!.resetRequestedTime).add(expirationDuration))) { - request.flash("error", "Your password reset link has expired. Please request a new one."); - user.localData!.resetCode = ""; - user.localData!.resetRequested = false; - await user.save(); + if (request.query.error === "access_denied") { + request.flash("error", "Authentication request was denied"); response.redirect("/login"); return; } - - let password1: string | undefined = request.body.password1; - let password2: string | undefined = request.body.password2; - if (!password1 || !password2) { - request.flash("error", "Missing new password or confirm password"); - response.redirect(path.join("/auth", request.url)); - return; - } - if (password1 !== password2) { - request.flash("error", "Passwords must match"); - response.redirect(path.join("/auth", request.url)); - return; - } - - let salt = crypto.randomBytes(32); - let hash = await pbkdf2Async(password1, salt, PBKDF2_ROUNDS); - - try { - user.localData!.salt = salt.toString("hex"); - user.localData!.hash = hash.toString("hex"); - user.localData!.resetCode = ""; - user.localData!.resetRequested = false; - await user.save(); - - request.flash("success", "Password reset successfully. You can now log in."); - response.redirect("/login"); - } - catch (err) { - console.error(err); - request.flash("error", "An error occurred while saving your new password"); - response.redirect(path.join("/auth", request.url)); - } -}); - -authRoutes.all("/logout", (request, response) => { - request.logout(); - response.redirect("/login"); + passport.authenticate("oauth2", { + failureRedirect: "/login", + successReturnToOrRedirect: "/", + callbackURL + } as AuthenticateOptions)(request, response, next); }); diff --git a/server/routes/strategies.ts b/server/routes/strategies.ts new file mode 100644 index 00000000..cc2b5040 --- /dev/null +++ b/server/routes/strategies.ts @@ -0,0 +1,210 @@ +import * as crypto from "crypto"; +import * as http from "http"; +import * as https from "https"; +import { URL } from "url"; +import * as requester from "request"; +import * as passport from "passport"; +import { Strategy as OAuthStrategy } from "passport-oauth2"; + +import { config } from "../common"; +// TODO import { trackEvent } from "../middleware"; +import { createNew, IUser, User } from "../schema"; +import { Request, Response, NextFunction } from "express"; + +type PassportDone = (err: Error | null, user?: IUser | false, errMessage?: { message: string }) => void; +type PassportProfileDone = (err: Error | null, profile?: IProfile) => void; +interface IStrategyOptions { + passReqToCallback: true; // Forced to true for our usecase +} +interface IOAuthStrategyOptions extends IStrategyOptions { + authorizationURL: string; + tokenURL: string; + clientID: string; + clientSecret: string; +} +interface IProfile { + uuid: string; + name: string; + email: string; + token: string; +} + +// Because the passport typedefs don't include this for some reason +// Defined: https://github.com/jaredhanson/passport-oauth2/blob/9ddff909a992c3428781b7b2957ce1a97a924367/lib/strategy.js#L135 +export type AuthenticateOptions = passport.AuthenticateOptions & { + callbackURL: string; +}; + +export class GroundTruthStrategy extends OAuthStrategy { + public readonly url: string; + + public static async apiRequest(method: "GET" | "POST", url: string, token: string): Promise { + return new Promise((resolve, reject) => { + requester(url, { + method, + auth: { + sendImmediately: true, + bearer: token + } + }, (error, response, body) => { + if (error) { + reject(error); + return; + } + try { + resolve(JSON.parse(body)); + } + catch { + reject(new Error(`Invalid JSON: ${body}`)); + } + }); + }); + } + + public static get defaultUserProperties() { + return { + "admin": false, + + "applied": false, + "accepted": false, + "confirmed": false, + "preConfirmEmailSent": false, + "applicationStartTime": undefined, + "applicationSubmitTime": undefined + }; + } + + constructor(url: string) { + const secrets = config.secrets.groundTruth; + if (!secrets || !secrets.id || !secrets.secret) { + throw new Error(`Client ID or secret not configured in config.json or environment variables for Ground Truth`); + } + let options: IOAuthStrategyOptions = { + authorizationURL: new URL("/oauth/authorize", url).toString(), + tokenURL: new URL("/oauth/token", url).toString(), + clientID: secrets.id, + clientSecret: secrets.secret, + passReqToCallback: true + }; + super(options, GroundTruthStrategy.passportCallback); + this.url = url; + } + + public userProfile(accessToken: string, done: PassportProfileDone) { + GroundTruthStrategy + .apiRequest("GET", new URL("/api/user", this.url).toString(), accessToken) + .then(data => { + try { + let profile: IProfile = { + ...data, + token: accessToken + }; + done(null, profile); + } + catch (err) { + return done(err); + } + }) + .catch(err => { + done(err); + }); + } + + protected static async passportCallback(request: Request, accessToken: string, refreshToken: string, profile: IProfile, done: PassportDone) { + let user = await User.findOne({ uuid: profile.uuid }); + if (!user) { + user = createNew(User, { + ...GroundTruthStrategy.defaultUserProperties, + ...profile + }); + } + else { + user.token = accessToken; + } + + let domain = user.email.split("@").pop(); + if (domain && config.admins.domains.includes(domain)) { + user.admin = true; + } + if (config.admins.emails.includes(profile.email)) { + user.admin = true; + } + await user.save(); + done(null, user); + } +} + +// Authentication helpers +function getExternalPort(request: Request): number { + function defaultPort(): number { + // Default ports for HTTP and HTTPS + return request.protocol === "http" ? 80 : 443; + } + + let host = request.headers.host; + if (!host || Array.isArray(host)) { + return defaultPort(); + } + + // IPv6 literal support + let offset = host[0] === "[" ? host.indexOf("]") + 1 : 0; + let index = host.indexOf(":", offset); + if (index !== -1) { + return parseInt(host.substring(index + 1), 10); + } + else { + return defaultPort(); + } +} + +let validatedHostNames: string[] = []; +export function validateAndCacheHostName(request: Request, response: Response, next: NextFunction) { + // Basically checks to see if the server behind the hostname has the same session key by HMACing a random nonce + if (validatedHostNames.find(hostname => hostname === request.hostname)) { + next(); + return; + } + + let nonce = crypto.randomBytes(64).toString("hex"); + function callback(message: http.IncomingMessage) { + if (message.statusCode !== 200) { + console.error(`Got non-OK status code when validating hostname: ${request.hostname}`); + message.resume(); + return; + } + message.setEncoding("utf8"); + let data = ""; + message.on("data", (chunk) => data += chunk); + message.on("end", () => { + let localHMAC = crypto.createHmac("sha256", config.secrets.session).update(nonce).digest().toString("hex"); + if (localHMAC === data) { + validatedHostNames.push(request.hostname); + next(); + } + else { + console.error(`Got invalid HMAC when validating hostname: ${request.hostname}`); + } + }); + } + function onError(err: Error) { + console.error(`Error when validating hostname: ${request.hostname}`, err); + } + if (request.protocol === "http") { + http.get(`http://${request.hostname}:${getExternalPort(request)}/auth/validatehost/${nonce}`, callback).on("error", onError); + } + else { + https.get(`https://${request.hostname}:${getExternalPort(request)}/auth/validatehost/${nonce}`, callback).on("error", onError); + } +} + +export function createLink(request: Request, link: string): string { + if (link[0] === "/") { + link = link.substring(1); + } + if ((request.secure && getExternalPort(request) === 443) || (!request.secure && getExternalPort(request) === 80)) { + return `http${request.secure ? "s" : ""}://${request.hostname}/${link}`; + } + else { + return `http${request.secure ? "s" : ""}://${request.hostname}:${getExternalPort(request)}/${link}`; + } +} diff --git a/server/routes/templates.ts b/server/routes/templates.ts index 8daa1b5b..d7bd405d 100644 --- a/server/routes/templates.ts +++ b/server/routes/templates.ts @@ -1,57 +1,68 @@ import * as fs from "fs"; import * as path from "path"; +import { URL } from "url"; import * as express from "express"; -import * as Handlebars from "handlebars"; +import * as HandlebarsImport 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 + config, getSetting, renderMarkdown, removeTags, renderPageHTML } from "../common"; import { - authenticateWithRedirect, - timeLimited, ApplicationType + authenticateWithRedirect, isAdmin, + onlyAllowAnonymousBranch, branchRedirector, ApplicationType, + postParser } from "../middleware"; import { - IUser, IUserMongoose, User, - ITeamMongoose, Team, - IIndexTemplate, ILoginTemplate, IAdminTemplate, ITeamTemplate, - IRegisterBranchChoiceTemplate, IRegisterTemplate, StatisticEntry + Model, + IUser, User, + ITeam, Team, + ILoginTemplate, IIndexTemplate, IAdminTemplate, ITeamTemplate, + IRegisterBranchChoiceTemplate, IInterstitialTemplate, IRegisterTemplate, StatisticEntry, + IFormItem } from "../schema"; import * as Branches from "../branch"; +import * as crypto from "crypto"; + +import {allowInsecurePrototypeAccess} from "@handlebars/allow-prototype-access"; + +const Handlebars = allowInsecurePrototypeAccess(HandlebarsImport); export let templateRoutes = express.Router(); -// Load and compile Handlebars templates -let [ - indexTemplate, - loginTemplate, - forgotPasswordTemplate, - resetPasswordTemplate, - preregisterTemplate, - preconfirmTemplate, - registerTemplate, - confirmTemplate, - adminTemplate, - unsupportedTemplate, - teamTemplate -] = [ - "index.html", - "login.html", - "forgotpassword.html", - "resetpassword.html", - "preapplication.html", - "preconfirmation.html", - "application.html", - "confirmation.html", - "admin.html", - "unsupported.html", - "team.html" -].map(file => { - let data = fs.readFileSync(path.resolve(STATIC_ROOT, file), "utf8"); - return Handlebars.compile(data); -}); +export class Template { + private template: Handlebars.TemplateDelegate | null = null; + + constructor(private readonly file: string) { + this.loadTemplate(); + } + + private loadTemplate(): void { + let data = fs.readFileSync(path.resolve(STATIC_ROOT, this.file), "utf8"); + this.template = Handlebars.compile(data); + } + + public render(input: T): string { + if (!config.server.isProduction) { + this.loadTemplate(); + } + return this.template!(input); + } +} + +const IndexTemplate = new Template("index.html"); +const PreRegisterTemplate = new Template("preapplication.html"); +const PreConfirmTemplate = new Template("preconfirmation.html"); +const InterstitialTemplate = new Template("interstitial.html"); +const RegisterTemplate = new Template("application.html"); +const ConfirmTemplate = new Template("confirmation.html"); +const AdminTemplate = new Template("admin.html"); +const UnsupportedTemplate = new Template<{ siteTitle: string }>("unsupported.html"); +const TeamTemplate = new Template("team.html"); +const LoginTemplate = new Template("login.html"); // Block IE templateRoutes.use(async (request, response, next) => { @@ -70,7 +81,7 @@ templateRoutes.use(async (request, response, next) => { let templateData = { siteTitle: config.eventName }; - response.send(unsupportedTemplate(templateData)); + response.send(UnsupportedTemplate.render(templateData)); } else { next(); @@ -78,14 +89,20 @@ templateRoutes.use(async (request, response, next) => { }); // tslint:disable-next-line:no-any +// tslint:disable:no-invalid-this Handlebars.registerHelper("ifCond", function(v1: any, v2: any, options: any) { if (v1 === v2) { - // tslint:disable-next-line:no-invalid-this return options.fn(this); } - // tslint:disable-next-line:no-invalid-this return options.inverse(this); }); +Handlebars.registerHelper("ifIn", function(elem: T, list: T[], options: any) { + if (list.includes(elem)) { + return options.fn(this); + } + return options.inverse(this); +}); +// tslint:enable:no-invalid-this Handlebars.registerHelper("required", (isRequired: boolean) => { // Adds the "required" form attribute if the element requests to be required return isRequired ? "required" : ""; @@ -94,9 +111,6 @@ Handlebars.registerHelper("checked", (selected: boolean[], index: number) => { // Adds the "checked" form attribute if the element was checked previously return selected[index] ? "checked" : ""; }); -Handlebars.registerHelper("branchChecked", (selectedBranches: string[], confirmationBranch: string) => { - return (selectedBranches.indexOf(confirmationBranch) !== -1) ? "checked" : ""; -}); Handlebars.registerHelper("selected", (selected: boolean[], index: number) => { // Adds the "selected" form attribute if the element was selected previously return selected[index] ? "selected" : ""; @@ -120,32 +134,30 @@ Handlebars.registerHelper("toJSONString", (stat: StatisticEntry): string => { Handlebars.registerHelper("removeSpaces", (input: string): string => { return input.replace(/ /g, "-"); }); -Handlebars.registerPartial("sidebar", fs.readFileSync(path.resolve(STATIC_ROOT, "partials", "sidebar.html"), "utf8")); +Handlebars.registerHelper("join", (arr: T[]): string => { + return arr.join(", "); +}); +for (let name of ["sidebar", "login-methods", "form"]) { + Handlebars.registerPartial(name, fs.readFileSync(path.resolve(STATIC_ROOT, "partials", `${name}.html`), "utf8")); +} templateRoutes.route("/dashboard").get((request, response) => response.redirect("/")); templateRoutes.route("/").get(authenticateWithRedirect, async (request, response) => { let user = request.user as IUser; let applyBranches: Branches.ApplicationBranch[]; + if (user.applicationBranch) { applyBranches = [(await Branches.BranchConfig.loadBranchFromDB(user.applicationBranch))] as Branches.ApplicationBranch[]; - } else { - applyBranches = (await Branches.BranchConfig.loadAllBranches("Application") as Branches.ApplicationBranch[]); } - let confirmBranches: Branches.ConfirmationBranch[]; - if (user.confirmationBranch) { - confirmBranches = [(await Branches.BranchConfig.loadBranchFromDB(user.confirmationBranch))] as Branches.ConfirmationBranch[]; - } else { - confirmBranches = (await Branches.BranchConfig.loadAllBranches("Confirmation")) as Branches.ConfirmationBranch[]; + else { + applyBranches = (await Branches.BranchConfig.loadAllBranches("Application") as Branches.ApplicationBranch[]); } - // Filter out branches user does not have access to based on apply branch - if (user.applicationBranch) { - let appliedBranch = applyBranches[0]; - confirmBranches = confirmBranches.filter((branch) => { - // TODO, verify template looks reasonable - return appliedBranch.confirmationBranches && appliedBranch.confirmationBranches.indexOf(branch.name) > -1; - }); + let confirmBranches: Branches.ConfirmationBranch[] = []; + + if (user.confirmationBranch) { + confirmBranches.push(await Branches.BranchConfig.loadBranchFromDB(user.confirmationBranch) as Branches.ConfirmationBranch); } interface IBranchOpenClose { @@ -161,8 +173,9 @@ templateRoutes.route("/").get(authenticateWithRedirect, async (request, response map[branch.name] = branch; return map; }, {} as IDeadlineMap); - for (let branchTimes of user.confirmationDeadlines) { - confirmTimes[branchTimes.name] = branchTimes; + + if (user.confirmationDeadline && user.confirmationDeadline.name) { + confirmTimes[user.confirmationDeadline.name] = user.confirmationDeadline; } let confirmTimesArr: IBranchOpenClose[] = Object.keys(confirmTimes).map(name => confirmTimes[name]); @@ -182,9 +195,9 @@ templateRoutes.route("/").get(authenticateWithRedirect, async (request, response } function formatMoment(date: moment.Moment | null): string { - const FORMAT = "dddd, MMMM Do YYYY [at] h:mm a z"; + const FORMAT = "dddd, MMMM Do, YYYY, [at] h:mm a z"; if (date) { - return date.tz(moment.tz.guess()).format(FORMAT); + return date.tz(config.server.defaultTimezone).format(FORMAT); } return "(No branches configured)"; } @@ -199,15 +212,66 @@ templateRoutes.route("/").get(authenticateWithRedirect, async (request, response close: closeString }; } + let status = ""; + + // Block of logic to dermine status: + if (!user.applied) { + status = "Incomplete"; + } + else if (user.applied && !user.confirmationBranch) { + status = "Pending Decision"; + } + else if (user.applied && user.confirmationBranch) { + // After confirmation - they either confirmed in time, did not, or branch did not require confirmation + if (user.confirmed) { + if (user.accepted) { + status = "Attending - " + user.confirmationBranch; + } + else { + // For confirmation branches that do not accept such as Rejected/Waitlist + status = user.confirmationBranch; + } + } + else if (moment().isAfter(confirmTimesArr[0].close)) { + status = "Confirmation Incomplete - " + user.confirmationBranch; + } + else if (moment().isBefore(confirmTimesArr[0].open)) { + status = "Confirmation Opens Soon - " + user.confirmationBranch; + } + else { + status = "Please Confirm - " + user.confirmationBranch; + } + } + + let autoConfirm = false; + if (user.confirmationBranch) { + autoConfirm = confirmBranches[0].autoConfirm; + } + + const helpscout = { + beaconEnabled: config.helpscout.beacon.enabled, + beaconId: config.helpscout.beacon.beaconId, + signature: crypto.createHmac('sha256', config.helpscout.beacon.supportHistorySecretKey) + .update(user.email) + .digest('hex') + }; let templateData: IIndexTemplate = { siteTitle: config.eventName, user, + helpscout, + timeline: { + application: "", + decision: "", + confirmation: "", + teamFormation: "" + }, + status, + autoConfirm, settings: { teamsEnabled: await getSetting("teamsEnabled"), qrEnabled: await getSetting("qrEnabled") }, - applicationOpen: formatMoment(applicationOpenDate), applicationClose: formatMoment(applicationCloseDate), applicationStatus: { @@ -235,111 +299,133 @@ templateRoutes.route("/").get(authenticateWithRedirect, async (request, response }; }) }; - response.send(indexTemplate(templateData)); -}); -templateRoutes.route("/login").get((request, response) => { - let templateData: ILoginTemplate = { - siteTitle: config.eventName, - error: request.flash("error"), - success: request.flash("success") - }; - response.send(loginTemplate(templateData)); -}); -templateRoutes.route("/login/forgot").get((request, response) => { - let templateData: ILoginTemplate = { - siteTitle: config.eventName, - error: request.flash("error"), - success: request.flash("success") - }; - response.send(forgotPasswordTemplate(templateData)); + // Timeline configuration + if (user.applied) { + templateData.timeline.application = "complete"; + } + else if (templateData.applicationStatus.beforeOpen) { + templateData.timeline.application = "warning"; + } + else if (templateData.applicationStatus.afterClose) { + templateData.timeline.application = "rejected"; + } + if (user.applied && user.confirmationBranch) { + templateData.timeline.decision = user.accepted ? "complete" : "rejected"; + } + if (user.confirmationBranch) { + if (user.confirmed) { + templateData.timeline.confirmation = "complete"; + } + else if (templateData.confirmationStatus.beforeOpen) { + templateData.timeline.confirmation = "warning"; + } + else if (templateData.confirmationStatus.afterClose) { + templateData.timeline.confirmation = "rejected"; + } + } + if (user.teamId) { + templateData.timeline.teamFormation = "complete"; + } + + response.send(IndexTemplate.render(templateData)); }); -templateRoutes.get("/auth/forgot/:code", async (request, response) => { - let user = await User.findOne({ "localData.resetCode": request.params.code }); - if (!user) { - request.flash("error", "Invalid password reset code"); - response.redirect("/login"); - return; + +templateRoutes.route("/login").get(async (request, response) => { + // Allow redirect to any subpath of registration + if (request.session && request.query.r && request.query.r.startsWith('/')) { + request.session.returnTo = request.query.r; } - else if (!user.localData!.resetRequested || Date.now() - user.localData!.resetRequestedTime.valueOf() > 1000 * 60 * 60) { - request.flash("error", "Your password reset link has expired. Please request a new one."); - user.localData!.resetCode = ""; - user.localData!.resetRequested = false; - await user.save(); - response.redirect("/login"); - return; + + let errorMessage = request.flash("error") as string[]; + if (request.session && request.session.loginAction === "render") { + request.session.loginAction = "redirect"; + let templateData = { + siteTitle: config.eventName, + isLogOut: true, + groundTruthLogOut: new URL("/logout", config.secrets.groundTruth.url).toString() + }; + response.send(LoginTemplate.render(templateData)); + } + else if (errorMessage.length > 0) { + let templateData = { + siteTitle: config.eventName, + error: errorMessage.join(" "), + isLogOut: false + }; + response.send(LoginTemplate.render(templateData)); + } + else { + response.redirect("/auth/login"); } - let templateData: ILoginTemplate = { - siteTitle: config.eventName, - error: request.flash("error"), - success: request.flash("success") - }; - response.send(resetPasswordTemplate(templateData)); }); templateRoutes.route("/team").get(authenticateWithRedirect, async (request, response) => { - let team: ITeamMongoose | null = null; - let membersAsUsers: IUserMongoose[] | null = null; - let teamLeaderAsUser: IUserMongoose | null = null; + let team: ITeam | null = null; + let membersAsUsers: IUser[] | null = null; + let teamLeaderAsUser: IUser | null = null; let isCurrentUserTeamLeader = false; - if (request.user.teamId) { - team = await Team.findById(request.user.teamId) as ITeamMongoose; - membersAsUsers = await User.find({ - _id: { - $in: team.members - } - }); - teamLeaderAsUser = await User.findById(team.teamLeader) as IUserMongoose; - isCurrentUserTeamLeader = teamLeaderAsUser._id.toString() === request.user._id.toString(); + if (request.user && request.user.teamId) { + team = await Team.findById(request.user.teamId); + if (team) { + membersAsUsers = await User.find({ + _id: { + $in: team.members + } + }); + teamLeaderAsUser = await User.findById(team.teamLeader); + isCurrentUserTeamLeader = teamLeaderAsUser != null && teamLeaderAsUser._id.toString() === request.user._id.toString(); + } } + const helpscout = { + beaconEnabled: config.helpscout.beacon.enabled, + beaconId: config.helpscout.beacon.beaconId, + signature: crypto.createHmac('sha256', config.helpscout.beacon.supportHistorySecretKey) + .update(request.user.email) + .digest('hex') + }; + let templateData: ITeamTemplate = { siteTitle: config.eventName, - user: request.user, + user: request.user as IUser, + helpscout, team, membersAsUsers, teamLeaderAsUser, isCurrentUserTeamLeader, settings: { teamsEnabled: await getSetting("teamsEnabled"), - qrEnabled: await getSetting("qrEnabled") + qrEnabled: await getSetting("qrEnabled"), + maxTeamSize: config.maxTeamSize } }; - response.send(teamTemplate(templateData)); + response.send(TeamTemplate.render(templateData)); }); templateRoutes.route("/apply").get( authenticateWithRedirect, - timeLimited, + branchRedirector(ApplicationType.Application), applicationHandler(ApplicationType.Application) ); templateRoutes.route("/confirm").get( authenticateWithRedirect, - timeLimited, + branchRedirector(ApplicationType.Confirmation), 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 + return async (request, response) => { 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) { if (user.applied) { - questionBranches = [user.applicationBranch.toLowerCase()]; + questionBranches = [user.applicationBranch!.toLowerCase()]; } else { const branches = await Branches.BranchConfig.getOpenBranches("Application"); @@ -348,34 +434,25 @@ function applicationHandler(requestType: ApplicationType): (request: express.Req } // Additionally selectively allow confirmation branches based on what the user applied as else if (requestType === ApplicationType.Confirmation) { - if (user.attending) { + if (user.confirmationBranch) { questionBranches = [user.confirmationBranch.toLowerCase()]; - } - else { - const branches = await Branches.getOpenConfirmationBranches(user); - questionBranches = branches.map(branch => branch.name.toLowerCase()); - - let appliedBranch = (await Branches.BranchConfig.loadBranchFromDB(user.applicationBranch)) as Branches.ApplicationBranch; - if (appliedBranch) { - questionBranches = questionBranches.filter(branch => { - return !!appliedBranch.confirmationBranches.find(confirm => { - return confirm.toLowerCase() === branch; - }); - }); - } + } else { + response.redirect("/"); } } - // 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; - } + const helpscout = { + beaconEnabled: config.helpscout.beacon.enabled, + beaconId: config.helpscout.beacon.beaconId, + signature: crypto.createHmac('sha256', config.helpscout.beacon.supportHistorySecretKey) + .update(user.email) + .digest('hex') + }; + let templateData: IRegisterBranchChoiceTemplate = { siteTitle: config.eventName, user, + helpscout, settings: { teamsEnabled: await getSetting("teamsEnabled"), qrEnabled: await getSetting("qrEnabled") @@ -384,143 +461,215 @@ function applicationHandler(requestType: ApplicationType): (request: express.Req }; if (requestType === ApplicationType.Application) { - response.send(preregisterTemplate(templateData)); + response.send(PreRegisterTemplate.render(templateData)); } else { - response.send(preconfirmTemplate(templateData)); + response.send(PreConfirmTemplate.render(templateData)); } }; } -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; - - let user = request.user as IUser; +templateRoutes.route("/register/:branch").get( + isAdmin, + onlyAllowAnonymousBranch, + applicationBranchHandler(ApplicationType.Application, true) +); - // Redirect to application screen if confirmation was requested and user has not applied/been accepted - if (requestType === ApplicationType.Confirmation && (!user.accepted || !user.applied)) { +templateRoutes.route("/apply/:branch") + .get( + authenticateWithRedirect, + branchRedirector(ApplicationType.Application), + applicationBranchHandler(ApplicationType.Application, false) + ) + .post(postParser, interstitialPostHandler); +templateRoutes.route("/confirm/:branch") + .get( + authenticateWithRedirect, + branchRedirector(ApplicationType.Confirmation), + applicationBranchHandler(ApplicationType.Confirmation, false) + ) + .post(postParser, interstitialPostHandler); + +function interstitialPostHandler(request: express.Request, response: express.Response) { + if (request.body["interstitial-action"] === "Back") { 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()) { - 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; + if (request.session) { + request.session.interstitialShown = true; } + response.redirect(request.url); // Redirect to a GET of this page +} +function applicationBranchHandler(requestType: ApplicationType, anonymous: boolean): (request: express.Request, response: express.Response) => Promise { + return async (request, response) => { + let user: IUser; + + const helpscout = { + beaconEnabled: config.helpscout.beacon.enabled, + beaconId: config.helpscout.beacon.beaconId, + signature: "" + }; + + if (anonymous) { + user = new User({ + uuid: uuid(), + email: "" + }); + } else { + user = request.user as IUser; + helpscout.signature = crypto.createHmac('sha256', config.helpscout.beacon.supportHistorySecretKey) + .update(user.email) + .digest('hex'); + } - // 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"); + let branchName = request.params.branch as string; + let questionBranches = await Branches.BranchConfig.loadAllBranches(); + let questionBranch = questionBranches.find(branch => branch.name.toLowerCase() === branchName.toLowerCase())!; + + let interstitialMarkdown: string = ""; + try { + interstitialMarkdown = await getSetting(`${questionBranch.name}-interstitial`, false); + } + // tslint:disable-next-line:no-empty + catch {} // Setting retrieval will throw if the setting is unset + + // Show user interstitial for this branch if: + // 1. The interstitial content is not blank + // 2. User has not already been shown interstitial for this branch (and clicked Continue) + // 3. User is applying to this branch for the first time (i.e. is not editing their existing application) + let interstitialShown = false; + if (request.session && request.session.interstitialShown) { + interstitialShown = true; + } + let isUserEditing = false; + if (requestType === ApplicationType.Application && user.applicationBranch) { + isUserEditing = true; + } + if (requestType === ApplicationType.Confirmation && user.confirmationBranch) { + isUserEditing = true; + } + if (interstitialMarkdown.trim() && !interstitialShown && !isUserEditing) { + response.send(InterstitialTemplate.render({ + siteTitle: config.eventName, + user, + helpscout, + settings: { + teamsEnabled: await getSetting("teamsEnabled"), + qrEnabled: await getSetting("qrEnabled") + }, + html: await renderPageHTML(interstitialMarkdown, user) + })); return; } - } + if (request.session) { + request.session.interstitialShown = false; + } - 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; + } } } } + question["hasResponse"] = savedValue && savedValue.value; // Used to determine whether "Please select" is selected in dropdown lists } - } - 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 : ""; + 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 : ""; + 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 + if (!anonymous) { + let thisUser = await User.findById(user._id) as Model; + // TODO this is a bug - dates are wrong + if (requestType === ApplicationType.Application && !thisUser.applicationStartTime) { + thisUser.applicationStartTime = new Date(); + } + else if (requestType === ApplicationType.Confirmation && !thisUser.confirmationStartTime) { + thisUser.confirmationStartTime = new Date(); + } + await thisUser.save(); - 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"); - } + helpscout.signature = crypto.createHmac('sha256', config.helpscout.beacon.supportHistorySecretKey) + .update(user.email) + .digest('hex'); + } - 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, + helpscout, + user: request.user as IUser, + 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.render(templateData)); + } else if (requestType === ApplicationType.Confirmation) { + response.send(ConfirmTemplate.render(templateData)); + } }; - - response.send(requestType === ApplicationType.Application ? registerTemplate(templateData) : confirmTemplate(templateData)); } templateRoutes.route("/admin").get(authenticateWithRedirect, async (request, response) => { @@ -532,7 +681,7 @@ templateRoutes.route("/admin").get(authenticateWithRedirect, async (request, res let teamsEnabled = await getSetting("teamsEnabled"); let qrEnabled = await getSetting("qrEnabled"); - let adminEmails = await User.find({admin: true}).select('email'); + let adminEmails = await User.find({ admin: true }).select("email"); let noopBranches = (await Branches.BranchConfig.loadAllBranches("Noop")) as Branches.NoopBranch[]; let applicationBranches = (await Branches.BranchConfig.loadAllBranches("Application")) as Branches.ApplicationBranch[]; @@ -541,20 +690,24 @@ templateRoutes.route("/admin").get(authenticateWithRedirect, async (request, res let teamIDNameMap: { [id: string]: string; } = {}; - (await Team.find()).forEach((team: ITeamMongoose) => { + (await Team.find()).forEach(team => { teamIDNameMap[team._id.toString()] = team.teamName; }); + let preconfiguredAdmins = config.admins.emails.concat(config.admins.domains.map(domain => `*@${domain}`)); + let templateData: IAdminTemplate = { siteTitle: config.eventName, user, - branchNames: await Branches.BranchConfig.getNames(), + helpscout: { + beaconEnabled: false // No need to show Help Scout Beacon on admin screens + }, applicationStatistics: { totalUsers: await User.find().count(), appliedUsers: await User.find({ "applied": true }).count(), - admittedUsers: await User.find({ "accepted": true }).count(), - attendingUsers: await User.find({ "attending": true }).count(), - declinedUsers: await User.find({ "accepted": true, "attending": false }).count(), + acceptedUsers: await User.find({ "accepted": true }).count(), + confirmedUsers: await User.find({ "accepted": true, "confirmed": true }).count(), + nonConfirmedUsers: await User.find({ "accepted": true, "confirmed": false }).count(), applicationBranches: await Promise.all(applicationBranches.map(async branch => { return { "name": branch.name, @@ -564,6 +717,7 @@ templateRoutes.route("/admin").get(authenticateWithRedirect, async (request, res confirmationBranches: await Promise.all(confirmationBranches.map(async branch => { return { "name": branch.name, + "confirmed": await User.find({ "confirmed": true, "confirmationBranch": branch.name }).count(), "count": await User.find({ "confirmationBranch": branch.name }).count() }; })) @@ -573,7 +727,6 @@ templateRoutes.route("/admin").get(authenticateWithRedirect, async (request, res teamsEnabled, teamsEnabledChecked: teamsEnabled ? "checked" : "", qrEnabled, - adminEmails, qrEnabledChecked: qrEnabled ? "checked" : "", branches: { noop: noopBranches.map(branch => { @@ -584,7 +737,8 @@ 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 }; }), confirmation: confirmationBranches.map((branch: Branches.ConfirmationBranch) => { @@ -593,13 +747,16 @@ templateRoutes.route("/admin").get(authenticateWithRedirect, async (request, res open: branch.open.toISOString(), close: branch.close.toISOString(), usesRollingDeadline: branch.usesRollingDeadline, - usesRollingDeadlineChecked: branch.usesRollingDeadline ? "checked" : "" + autoConfirm: branch.autoConfirm, + isAcceptance: branch.isAcceptance }; }) - } + }, + adminEmails, + apiKey: config.secrets.adminKey }, config: { - admins: config.admins.join(", "), + admins: preconfiguredAdmins.join(", "), eventName: config.eventName, storageEngine: config.storageEngine.name, uploadDirectoryRaw: config.storageEngine.options.uploadDirectory, @@ -618,11 +775,11 @@ templateRoutes.route("/admin").get(authenticateWithRedirect, async (request, res // Generate general statistics (await User.find({ "applied": true })).forEach(async statisticUser => { - let appliedBranch = applicationBranchMap[statisticUser.applicationBranch]; + let appliedBranch = applicationBranchMap[statisticUser.applicationBranch!]; if (!appliedBranch) { return; } - statisticUser.applicationData.forEach(question => { + statisticUser.applicationData!.forEach(question => { if (question.value === null) { return; } @@ -639,91 +796,98 @@ templateRoutes.route("/admin").get(authenticateWithRedirect, async (request, res if (!rawQuestion) { continue; } - let rawQuestionLabel = rawQuestion.label; - let statisticEntry: StatisticEntry | undefined = templateData.generalStatistics.find(entry => entry.questionName === rawQuestionLabel && entry.branch === appliedBranch.name); + let questionName = rawQuestion.name; + let statisticEntry: StatisticEntry | undefined = templateData.generalStatistics.find(entry => entry.questionName === questionName && entry.branch === appliedBranch.name); if (!statisticEntry) { statisticEntry = { - "questionName": rawQuestionLabel, - "branch": statisticUser.applicationBranch, - "responses": [] + questionName, + questionLabel: removeTags(rawQuestion.label), + branch: statisticUser.applicationBranch!, + responses: [] }; templateData.generalStatistics.push(statisticEntry); } + checkboxValue = removeTags(checkboxValue); let responsesIndex = statisticEntry.responses.findIndex(resp => resp.response === checkboxValue); if (responsesIndex !== -1) { statisticEntry.responses[responsesIndex].count++; } else { statisticEntry.responses.push({ - "response": checkboxValue, - "count": 1 + response: checkboxValue, + count: 1 }); } } } - /*else if (question.type === "date") { - // Categorize by date - let years = moment().diff(moment(question.value as string), "years", true); - - let rawQuestion = rawQuestions.find(branch => branch.name === user.applicationBranch)!.questions.find(q => q.name === question.name); - let title = `${user.applicationBranch} → ${rawQuestion ? rawQuestion.label : question.name} (average)`; - let index = templateData.generalStatistics.findIndex(stat => stat.title === title); - if (index !== -1) { - templateData.generalStatistics[index].value += years; - templateData.generalStatistics[index].count = 1; - } - else { - templateData.generalStatistics.push({ - "title": title, - "value": years, - "count": 1 - }); - } - }*/ }); }); // Order general statistics as they appear in questions.json - templateData.generalStatistics = await Promise.all(templateData.generalStatistics.sort((a, b) => { - if (a.branch.toLowerCase() < b.branch.toLowerCase()) { - return -1; + templateData.generalStatistics = templateData.generalStatistics.sort((a, b) => { + if (a.branch !== b.branch) { + // Sort the branches into order + let branchIndexA = Branches.Branches.indexOf(a.branch); + let branchIndexB = Branches.Branches.indexOf(b.branch); + // Sort unknown branches at the end (shouldn't usually happen) + if (branchIndexA === -1) branchIndexA = Infinity; + if (branchIndexB === -1) branchIndexB = Infinity; + + return branchIndexA - branchIndexB; + } + else { + if (!Branches.Tags[a.branch] || !Branches.Tags[b.branch]) { + // If the user applied to a branch that doesn't exist anymore + return 0; + } + // Sort the questions into order + let questionIndexA = Branches.Tags[a.branch].indexOf(a.questionName); + let questionIndexB = Branches.Tags[b.branch].indexOf(b.questionName); + // Sort unknown questions at the end (shouldn't usually happen) + if (questionIndexA === -1) questionIndexA = Infinity; + if (questionIndexB === -1) questionIndexB = Infinity; + + return questionIndexA - questionIndexB; } - if (a.branch.toLowerCase() > b.branch.toLowerCase()) { - return 1; + }).map(question => { + // Sort question responses into order + let branchIndex = Branches.Branches.indexOf(question.branch); + if (branchIndex === -1) { + // Branch not found; return unchanged + return question; } - return 0; - }).map(async statistic => { - let questions = (await Branches.BranchConfig.loadBranchFromDB(statistic.branch)).questions; - let question = questions.find(q => q.label === statistic.questionName)!; + let branch = Branches.QuestionsConfig[branchIndex]; - statistic.responses = statistic.responses.sort((a, b) => { - let aIndex: number = question.options.indexOf(a.response); - let bIndex: number = question.options.indexOf(b.response); + let branchQuestion = branch.questions.find(q => q.name === question.questionName); + if (!branchQuestion) { + // Question not found; return unchanged + return question; + } - if (aIndex !== -1 && bIndex === -1) { - return -1; - } - if (aIndex === -1 && bIndex !== -1) { - return 1; - } - if (aIndex === -1 && bIndex === -1) { - if (a.response.trim() === "") { - return 1; + if (branchQuestion.type === "checkbox" || branchQuestion.type === "radio" || branchQuestion.type === "select") { + let options = branchQuestion.options; + question.responses = question.responses.sort((a, b) => { + let optionIndexA = options.indexOf(a.response); + let optionIndexB = options.indexOf(b.response); + // Sort unknown options at the end (happens for "other" responses) + if (optionIndexA === -1) optionIndexA = Infinity; + if (optionIndexB === -1) optionIndexB = Infinity; + + // If both are unknown, sort alphabetically + if (optionIndexA === Infinity && optionIndexB === Infinity) { + let responseA = a.response.toLowerCase(); + let responseB = b.response.toLowerCase(); + if (responseA < responseB) return -1; + if (responseA > responseB) return 1; + return 0; } - if (a.response.toLowerCase() < b.response.toLowerCase()) { - return -1; - } - if (a.response.toLowerCase() > b.response.toLowerCase()) { - return 1; - } - return 0; - } - return aIndex - bIndex; - }); + return optionIndexA - optionIndexB; + }); + } - return statistic; - })); + return question; + }); - response.send(adminTemplate(templateData)); + response.send(AdminTemplate.render(templateData)); }); diff --git a/server/routes/uploads.ts b/server/routes/uploads.ts index d7735296..cda80db7 100644 --- a/server/routes/uploads.ts +++ b/server/routes/uploads.ts @@ -5,10 +5,11 @@ import { isAdmin } from "../middleware"; export let uploadsRoutes = express.Router(); -uploadsRoutes.route("/:file").get(isAdmin, (request, response) => { - response.attachment(request.params.file); +uploadsRoutes.route("/:file").get(isAdmin, async (request, response) => { try { - STORAGE_ENGINE.readFile(request.params.file).pipe(response); + let stream = await STORAGE_ENGINE.readFile(request.params.file); + response.attachment(request.params.file); + stream.pipe(response); } catch { response.status(404).send("The requested file could not be found"); diff --git a/server/schema.ts b/server/schema.ts index ae99b543..f366bc73 100644 --- a/server/schema.ts +++ b/server/schema.ts @@ -6,28 +6,25 @@ import {Questions} from "./config/questions.schema"; // Secrets JSON file schema export namespace IConfig { + export type OAuthServices = "github" | "google" | "facebook"; + export type CASServices = "gatech"; + export type Services = "local" | OAuthServices | CASServices; export interface Secrets { adminKey: string; session: string; - github: { - id: string; - secret: string; - }; - google: { - id: string; - secret: string; - }; - facebook: { + groundTruth: { + url: string; id: string; secret: string; }; } export interface Email { from: string; - host: string; - username: string; - password: string; - port: number; + key: string; + headerImage: string; + twitterHandle: string; + facebookHandle: string; + contactAddress: string; } export interface Server { isProduction: boolean; @@ -38,19 +35,39 @@ export namespace IConfig { cookieMaxAge: number; cookieSecureOnly: boolean; mongoURL: string; - passwordResetExpiration: number; + defaultTimezone: string; + rootURL: string; } export interface Style { theme: string; favicon: string; } + export interface HelpScout { + integration: HelpScoutIntegrationOptions; + beacon: HelpScoutBeaconOptions; + } + + export interface HelpScoutIntegrationOptions { + enabled: boolean; + secretKey: string; + } + + export interface HelpScoutBeaconOptions { + enabled: boolean; + beaconId: string; + supportHistorySecretKey: string; + } + export interface Main { secrets: Secrets; email: Email; server: Server; style: Style; - admins: string[]; + admins: { + domains: string[]; + emails: string[]; + }; eventName: string; questionsLocation: string; storageEngine: { @@ -58,6 +75,7 @@ export namespace IConfig { options: any; }; maxTeamSize: number; + helpscout: HelpScout; } } @@ -68,82 +86,72 @@ export interface IFormItem { "value": string | string[] | Express.Multer.File | null; } -export interface ITeam { +// For stricter type checking of new object creation +type Omit = Pick>; +interface RootDocument { _id: mongoose.Types.ObjectId; +} +export function createNew(model: mongoose.Model, doc: Omit) { + return new model(doc); +} +export type Model = T & mongoose.Document; + +export interface ITeam extends RootDocument { teamLeader: mongoose.Types.ObjectId; members: mongoose.Types.ObjectId[]; teamName: string; } -export type ITeamMongoose = ITeam & mongoose.Document; - -export const Team = mongoose.model("Team", new mongoose.Schema({ +export const Team = mongoose.model>("Team", new mongoose.Schema({ teamLeader: { type: mongoose.Schema.Types.ObjectId }, members: [{ type: mongoose.Schema.Types.ObjectId }], - teamName: { - type: mongoose.Schema.Types.String - } + teamName: String })); -export interface IUser { - _id: mongoose.Types.ObjectId; +export interface IUser extends RootDocument { + uuid: string; email: string; name: string; - verifiedEmail: boolean; - - localData?: { - hash: string; - salt: string; - verificationCode: string; - resetRequested: boolean; - resetCode: string; - resetRequestedTime: Date; - }; - githubData?: { - id: string; - username: string; - profileUrl: string; - }; - googleData?: { - id: string; - }; - facebookData?: { - id: string; - }; + token: string | null; + + teamId?: mongoose.Types.ObjectId; + admin: boolean; applied: boolean; accepted: boolean; - acceptedEmailSent: boolean; - attending: boolean; - applicationBranch: string; - applicationData: IFormItem[]; + preConfirmEmailSent: boolean; + confirmed: boolean; + applicationBranch?: string; + reimbursementAmount?: string; + applicationData?: IFormItem[]; applicationStartTime?: Date; applicationSubmitTime?: Date; - confirmationDeadlines: { + confirmationDeadline?: { name: string; open: Date; close: Date; - }[]; + }; - confirmationBranch: string; - confirmationData: IFormItem[]; + confirmationBranch?: string; + confirmationData?: IFormItem[]; confirmationStartTime?: Date; confirmationSubmitTime?: Date; - admin?: boolean; - uuid: string; - - teamId?: mongoose.Types.ObjectId; } -export type IUserMongoose = IUser & mongoose.Document; // This is basically a type definition that exists at runtime and is derived manually from the IUser definition above -export const User = mongoose.model("User", new mongoose.Schema({ +export const User = mongoose.model>("User", new mongoose.Schema({ + uuid: { + type: String, + required: true, + index: true, + unique: true + }, email: { type: String, required: true, @@ -154,72 +162,45 @@ export const User = mongoose.model("User", new mongoose.Schema({ type: String, index: true }, - verifiedEmail: Boolean, - - localData: { - hash: String, - salt: String, - verificationCode: String, - resetRequested: Boolean, - resetCode: String, - resetRequestedTime: Date - }, - githubData: { - id: String, - username: String, - profileUrl: String - }, - googleData: { - id: String - }, - facebookData: { - id: String - }, + token: String, teamId: { type: mongoose.Schema.Types.ObjectId }, + admin: Boolean, + applied: Boolean, accepted: Boolean, - acceptedEmailSent: Boolean, - attending: Boolean, + preConfirmEmailSent: Boolean, + confirmed: Boolean, applicationBranch: String, + reimbursementAmount: String, applicationData: [mongoose.Schema.Types.Mixed], applicationStartTime: Date, applicationSubmitTime: Date, - confirmationDeadlines: [{ + confirmationDeadline: { name: String, open: Date, close: Date - }], + }, confirmationBranch: String, confirmationData: [mongoose.Schema.Types.Mixed], confirmationStartTime: Date, - confirmationSubmitTime: Date, - - admin: Boolean, - uuid: { - type: String, - required: true, - index: true, - unique: true - } + confirmationSubmitTime: Date }).index({ - email: 'text', - name: 'text' + email: "text", + name: "text" })); -export interface ISetting { - _id: mongoose.Types.ObjectId; +export interface ISetting extends RootDocument { name: string; value: any; } -export type ISettingMongoose = ISetting & mongoose.Document; -export const Setting = mongoose.model("Setting", new mongoose.Schema({ +export const Setting = mongoose.model>("Setting", new mongoose.Schema({ name: { type: String, required: true, @@ -234,19 +215,21 @@ 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?: string; // Used by application branch confirmationBranches?: string[]; // Used by application branch usesRollingDeadline?: boolean; // Used by confirmation branch + isAcceptance?: boolean; // Used by confirmation branch + autoConfirm?: boolean; // Used by confirmation branch } -export interface IQuestionBranchConfig { - _id: mongoose.Types.ObjectId; +export interface IQuestionBranchConfig extends RootDocument { name: string; type: QuestionBranchType; settings: QuestionBranchSettings; location: string; } -export type IQuestionBranchConfigMongoose = IQuestionBranchConfig & mongoose.Document; -export const QuestionBranchConfig = mongoose.model("QuestionBranchConfig", new mongoose.Schema({ +export const QuestionBranchConfig = mongoose.model>("QuestionBranchConfig", new mongoose.Schema({ name: { type: String, required: true, @@ -256,8 +239,12 @@ export const QuestionBranchConfig = mongoose.model; - readFile(name: string): Readable; + readFile(name: string): Promise; } interface ICommonOptions { uploadDirectory: string; @@ -56,7 +57,7 @@ class DiskStorageEngine implements IStorageEngine { readStream.pipe(writeStream); }); } - public readFile(name: string): Readable { + public async readFile(name: string): Promise { return fs.createReadStream(path.join(this.options.uploadDirectory, name)); } } @@ -101,7 +102,7 @@ class S3StorageEngine implements IStorageEngine { }).catch(reject); }); } - public readFile(name: string): Readable { + public async readFile(name: string): Promise { AWS.config.update({ region: this.options.region, credentials: new AWS.Credentials({ @@ -110,23 +111,56 @@ class S3StorageEngine implements IStorageEngine { }) }); let s3 = new AWS.S3(); - let stream = s3.getObject({ + const object = { Bucket: this.options.bucket, Key: name - }).createReadStream(); - stream.on("error", err => { - throw err; + }; + // Will throw if the object does not exist + await s3.headObject(object).promise(); + return s3.getObject(object).createReadStream(); + } +} + +interface IGCSOptions extends ICommonOptions { + bucket: string; + clientEmail: string; + privateKey: string; +} + +class GCSStorageEngine implements IStorageEngine { + public readonly uploadRoot: string; + private readonly options: IGCSOptions; + private readonly storage: Storage; + + constructor(options: IGCSOptions) { + // Values copied via spread operator instead of being passed by reference + this.options = { + ...options + }; + this.uploadRoot = this.options.uploadDirectory; + this.storage = new Storage({ + credentials: { + client_email: this.options.clientEmail, + private_key: this.options.privateKey + } }); - return stream; + } + + public async saveFile(currentPath: string, name: string): Promise { + await this.storage.bucket(this.options.bucket).upload(currentPath, { + destination: name + }); + } + public async readFile(name: string): Promise { + return this.storage.bucket(this.options.bucket).file(name).createReadStream(); } } interface IStorageEngines { - [name: string]: { - new(options: ICommonOptions): IStorageEngine; - }; + [name: string]: new(options: ICommonOptions) => IStorageEngine; } export const storageEngines: IStorageEngines = { "disk": DiskStorageEngine, - "s3": S3StorageEngine + "s3": S3StorageEngine, + "gcs": GCSStorageEngine }; diff --git a/server/tsconfig.json b/server/tsconfig.json index 06584f8f..6b4faded 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -8,9 +8,8 @@ "target": "es6", "lib": [ "esnext.asynciterable", - "es6", - "es2015.promise", - "dom" + "es2017", + "es2015.promise" ], "sourceMap": true, "alwaysStrict": true, @@ -20,7 +19,7 @@ "noUnusedLocals": true, "plugins": [ { - "name": "tslint-language-service", + "name": "tslint-language-service-ts3", "alwaysShowRuleFailuresAsWarnings": false, "ignoreDefinitionFiles": true, "configFile": "../tslint.json" diff --git a/tslint.json b/tslint.json index 7d22b21a..95a783b0 100644 --- a/tslint.json +++ b/tslint.json @@ -61,7 +61,6 @@ "allow-undefined-check" ], "use-isnan": true, - "typeof-compare": true, "deprecation": true, "eofline": true, "linebreak-style": [ @@ -159,6 +158,19 @@ "allow-declarations", "allow-named-functions" ], - "max-classes-per-file": [false] + "max-classes-per-file": [false], + "no-duplicate-imports": true, + "no-this-assignment": [ + true, + { + "allow-destructuring": true + } + ], + "no-return-await": true, + "no-duplicate-switch-case": true, + "ban-comma-operator": true, + "no-dynamic-delete": true, + "prefer-readonly": true, + "prefer-while": true } }