├── .eslintrc ├── .gitignore ├── .nodemonignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SETUP.md ├── Vagrantfile ├── api.js ├── api ├── cache.js ├── config.js ├── database.js ├── files.js ├── index.js ├── logging.js └── v1 │ ├── controllers │ ├── AnnouncementController.js │ ├── AuthController.js │ ├── CheckInController.js │ ├── EcosystemController.js │ ├── EventController.js │ ├── HealthController.js │ ├── MailController.js │ ├── PermissionController.js │ ├── ProjectController.js │ ├── RSVPController.js │ ├── RecruiterInterestController.js │ ├── RegistrationController.js │ ├── StatsController.js │ ├── TrackingController.js │ ├── UploadController.js │ ├── UserController.js │ └── index.js │ ├── errors │ ├── ApiError.js │ ├── Constants.js │ ├── EntityNotSupportedError.js │ ├── EntityTooLargeError.js │ ├── ExistsError.js │ ├── ExternalProviderError.js │ ├── InvalidHeaderError.js │ ├── InvalidParameterError.js │ ├── InvalidTrackingStateError.js │ ├── MissingHeaderError.js │ ├── MissingParameterError.js │ ├── NotFoundError.js │ ├── RedisError.js │ ├── TokenExpirationError.js │ ├── UnauthorizedError.js │ ├── UnprocessableRequestError.js │ └── index.js │ ├── index.js │ ├── middleware │ ├── auth.js │ ├── errors.js │ ├── index.js │ ├── permission.js │ ├── ratelimiting.js │ ├── request.js │ ├── response.js │ └── upload.js │ ├── models │ ├── Announcement.js │ ├── Attendee.js │ ├── AttendeeExtraInfo.js │ ├── AttendeeLongForm.js │ ├── AttendeeOSContributor.js │ ├── AttendeeRSVP.js │ ├── AttendeeRequestedCollaborator.js │ ├── CheckIn.js │ ├── Ecosystem.js │ ├── Event.js │ ├── EventFavorite.js │ ├── EventLocation.js │ ├── Location.js │ ├── MailingList.js │ ├── MailingListUser.js │ ├── Mentor.js │ ├── MentorProjectIdea.js │ ├── Model.js │ ├── NetworkCredential.js │ ├── Project.js │ ├── ProjectMentor.js │ ├── RecruiterInterest.js │ ├── Token.js │ ├── TrackingEvent.js │ ├── Upload.js │ ├── User.js │ ├── UserRole.js │ └── index.js │ ├── requests │ ├── AccreditedUserCreationRequest.js │ ├── AnnouncementRequest.js │ ├── AttendeeDecisionRequest.js │ ├── AttendeeRequest.js │ ├── BasicAuthRequest.js │ ├── CheckInRequest.js │ ├── EcosystemCreationRequest.js │ ├── EventCreationRequest.js │ ├── EventDeletionRequest.js │ ├── EventFavoriteRequest.js │ ├── EventUpdateRequest.js │ ├── LocationCreationRequest.js │ ├── MentorRequest.js │ ├── ProjectMentorRequest.js │ ├── ProjectRequest.js │ ├── RSVPRequest.js │ ├── RecruiterInterestRequest.js │ ├── Request.js │ ├── ResetPasswordRequest.js │ ├── ResetTokenRequest.js │ ├── SendListRequest.js │ ├── UniversalTrackingRequest.js │ ├── UpdateRecruiterInterestRequest.js │ ├── UploadRequest.js │ ├── UserContactInfoRequest.js │ └── index.js │ ├── services │ ├── AnnouncementService.js │ ├── AuthService.js │ ├── CheckInService.js │ ├── EcosystemService.js │ ├── EventService.js │ ├── MailService.js │ ├── PermissionService.js │ ├── ProjectService.js │ ├── RSVPService.js │ ├── RecruiterInterestService.js │ ├── RegistrationService.js │ ├── StatsService.js │ ├── StorageService.js │ ├── TokenService.js │ ├── TrackingService.js │ ├── UserService.js │ └── index.js │ └── utils │ ├── cache.js │ ├── crypto.js │ ├── database.js │ ├── errors.js │ ├── index.js │ ├── logs.js │ ├── mail.js │ ├── roles.js │ ├── scopes.js │ ├── storage.js │ ├── time.js │ └── validators.js ├── config ├── development.json.template ├── production.json.template └── test.json.template ├── ctx ├── ctx.js └── package.json ├── database ├── README.md ├── flyway.sh ├── migration.sh ├── migration │ ├── V1_0__hackillinois-2017.sql │ ├── V20160614_1845__createUsersTable.sql │ ├── V20160712_1603__createMailingListTables.sql │ ├── V20160724_1239__createPasswordTokenTable.sql │ ├── V20160727_2020__createUploadsTable.sql │ ├── V20161004_0917__createMentors.sql │ ├── V20161105_1750__createProjects.sql │ ├── V20161105_2059__createProjectMentors.sql │ ├── V20161220_1152__createEcosystemModel.sql │ ├── V20161226_0927__createAttendees.sql │ ├── V20170212_1950__addAnnouncements.sql │ ├── V20170216_1324__createCheckInTable.sql │ ├── V20170216_1919__addEventTracking.sql │ ├── V20170216_1920__addNetworkCredentials.sql │ ├── V20170222_0021__addEventAPI.sql │ ├── V20180131_2100__removeRSVPType.sql │ ├── V20180207_1335__changeRSVPUnique.sql │ ├── V20180218_2014__addEventFavorites.sql │ ├── V20180222_1758__createRecruiterInterests.sql │ └── V20180223_0035__addRecruiterUniqueIndex.sql └── revert │ ├── V20160614_1845__createUsersTable.revert.sql │ ├── V20160712_1603__createMailingListTables.revert.sql │ ├── V20160724_1239__createPasswordTokenTable.revert.sql │ ├── V20160727_2020__createUploadsTable.revert.sql │ ├── V20161004_0917__createMentors.revert.sql │ ├── V20161105_1750__createProjects.revert.sql │ ├── V20161105_2059__createProjectMentors.revert.sql │ ├── V20161108_0927__createAttendees.revert.sql │ ├── V20161220_1152__createEcosystemModel.revert.sql │ ├── V20170212_1950__addAnnouncements.revert.sql │ ├── V20170216_1324__createCheckInTable.revert.sql │ ├── V20170216_1919__addEventTracking.revert.sql │ ├── V20170216_1920__addNetworkCredentials.revert.sql │ ├── V20170222_0021_addEventAPI.revert.sql │ ├── V20180131_2100__removeRSVPType.revert.sql │ ├── V20180131_2152__createRecruiterInterestsModel.revert.sql │ ├── V20180218_2014__addEventFavorites.revert.sql │ └── V20180223_0035__addRecruiterUniqueIndex.revert.sql ├── docs ├── Announcements.md ├── Applicant-Decision-Utils.md ├── Attendee-Statistics-Documentation.md ├── Auth-Documentation.md ├── Check-In-Documentation.md ├── Ecosystems-Documentation.md ├── Errors-Documentation.md ├── Events-Documentation.md ├── Health-Check-Documentation.md ├── Home.md ├── Permission-Documentation.md ├── Project-Documentation.md ├── RSVP-Documentation.md ├── RecruiterInterest-Documentation.md ├── Registration-Documentation-(Attendee).md ├── Registration-Documentation-(Mentor).md ├── Universal-Tracking-Documenation.md ├── Upload-Documentation.md └── User-Documentation.md ├── package.json ├── scripts ├── bridge.py ├── deploy │ └── deps.py ├── dev.sh ├── install │ ├── deps.sh │ └── flyway.sh ├── prod.sh └── test.sh └── test ├── auth.js ├── checkin.js ├── ecosystem.js ├── event.js ├── permission.js ├── registration.js ├── rsvp.js ├── storage.js ├── test.js ├── token.js ├── tracking.js └── user.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true, 6 | "mocha": true 7 | }, 8 | "plugins": ["node"], 9 | "extends": [ 10 | "eslint:recommended", 11 | "plugin:node/recommended" 12 | ], 13 | "parserOptions": { 14 | "sourceType": "module", 15 | "ecmaVersion": 2016 16 | }, 17 | "rules": { 18 | "strict": ["error", "global"], 19 | "indent": [ 20 | "error", 21 | 2 22 | ], 23 | "linebreak-style": [ 24 | "error", 25 | "unix" 26 | ], 27 | "quotes": [ 28 | "error", 29 | "single", 30 | { 31 | "avoidEscape": true 32 | } 33 | ], 34 | "semi": [ 35 | "error", 36 | "always" 37 | ], 38 | "node/exports-style": [ 39 | "error", 40 | "module.exports" 41 | ], 42 | "node/no-unpublished-require": ["error", { 43 | "allowModules": ["chai", "sinon", "mock-knex", "chai-as-promised", "mockery", "fakeredis", "express-brute", "express-brute-redis"], 44 | "tryExtensions": [".js", ".json", ".node"] 45 | }], 46 | "prefer-const": "error", 47 | "no-var": "error", 48 | "prefer-arrow-callback": "error", 49 | "no-template-curly-in-string": "error", 50 | "array-callback-return": "error", 51 | "default-case": "error", 52 | "curly": "error", 53 | "guard-for-in": "error", 54 | "no-else-return": "error", 55 | "no-empty-function": "error", 56 | "no-extra-bind": "error", 57 | "no-labels": "error", 58 | "no-lone-blocks": "error", 59 | "no-loop-func": "error", 60 | "no-multi-spaces": "error", 61 | "no-new": "error", 62 | "no-new-func": "error", 63 | "no-new-wrappers": "error", 64 | "no-return-assign": ["error", "except-parens"], 65 | "no-return-await": "error", 66 | "no-script-url": "error", 67 | "no-self-compare": "error", 68 | "no-throw-literal": "error", 69 | "no-unmodified-loop-condition": "error", 70 | "no-unused-expressions": "error", 71 | "no-useless-call": "error", 72 | "no-useless-concat": "error", 73 | "no-useless-escape": "error", 74 | "no-useless-return": "error", 75 | "no-void": "error", 76 | "yoda": ["error", "never"], 77 | "no-catch-shadow": "error", 78 | "array-bracket-spacing": ["error", "never", { 79 | "singleValue": true, 80 | "objectsInArrays": true, 81 | "arraysInArrays": true 82 | }], 83 | "block-spacing": "error", 84 | "brace-style": "error", 85 | "comma-dangle": ["error", "never"], 86 | "comma-spacing": "error", 87 | "comma-style": "error", 88 | "computed-property-spacing": "error", 89 | "func-call-spacing": "error", 90 | "key-spacing": "error", 91 | "arrow-body-style": "error", 92 | "arrow-parens": "error", 93 | "arrow-spacing": "error" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | npm-debug.log* 4 | 5 | pids 6 | *.pid 7 | *.seed 8 | dump.rdb 9 | 10 | node_modules 11 | .npm 12 | 13 | .idea 14 | .DS_Store 15 | temp 16 | 17 | /.idea 18 | 19 | config/*.json 20 | .env 21 | 22 | .vagrant 23 | .eslintcache 24 | -------------------------------------------------------------------------------- /.nodemonignore: -------------------------------------------------------------------------------- 1 | database/** 2 | scripts/** 3 | node_modules/** 4 | temp/** 5 | 6 | .gitignore 7 | .nodemonignore 8 | .env 9 | .DS_STORE 10 | package.json 11 | CONTRIBUTING.md 12 | README.md 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6.11.1" 4 | env: 5 | - CXX=g++-4.8 6 | addons: 7 | apt: 8 | sources: 9 | - ubuntu-toolchain-r-test 10 | packages: 11 | - g++-4.8 12 | - python3-pip 13 | cache: 14 | directories: 15 | - node_modules 16 | before_script: 17 | - npm install 18 | - cp ./config/test.json.template ./config/test.json 19 | after_success: 20 | - - rm ./config/test.json 21 | before_deploy: 22 | - ./scripts/install/flyway.sh 23 | - ./scripts/install/deps.sh 24 | - cp ./config/production.json.template ./config/production.json 25 | - python3 ./scripts/deploy/deps.py 26 | - npm run prod-migrations 27 | - git add -f ./config/production.json 28 | deploy: 29 | provider: elasticbeanstalk 30 | skip_cleanup: false 31 | access_key_id: $HACKILLINOIS_ACCESS_KEY_ID 32 | secret_access_key: $HACKILLINOIS_SECRET_ACCESS_KEY 33 | region: "us-east-1" 34 | app: "hackillinois-api" 35 | env: "hackillinois-api" 36 | bucket_name: $HACKILLINOIS_BUCKET_NAME 37 | skip_cleanup: true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | University of Illinois/NCSA Open Source License 2 | 3 | Copyright (c) 2017 HackIllinois 4 | All rights reserved. 5 | 6 | Developed by: HackIllinois Systems 7 | HackIllinois 8 | https://hackillinois.org 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy of 11 | this software and associated documentation files (the "Software"), to deal with 12 | the Software without restriction, including without limitation the rights to 13 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 14 | the Software, and to permit persons to whom the Software is furnished to do so, 15 | subject to the following conditions: 16 | 17 | * Redistributions of source code must retain the above copyright notice, 18 | this list of conditions and the following disclaimers. 19 | 20 | * Redistributions in binary form must reproduce the above copyright notice, 21 | this list of conditions and the following disclaimers in the documentation 22 | and/or other materials provided with the distribution. 23 | 24 | * Neither the names of the HackIllinois Systems team, HackIllinois, nor the 25 | names of its contributors may be used to endorse or promote products 26 | derived from this Software without specific prior written permission. 27 | 28 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 29 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 30 | FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE 31 | CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 32 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 33 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH 34 | THE SOFTWARE. 35 | -------------------------------------------------------------------------------- /SETUP.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | ## Overview 4 | 5 | We use Node.js + Express in the application layer. MySQL is used as our primary datastore in the persistence layer and Redis is used as our primary cache. 6 | 7 | You do not have to set up any of these components yourself! 8 | 9 | ## Environment 10 | 11 | A development environment is available as a Vagrant box. To use it, you must have Vagrant, Virtualbox, and rsync installed. If you are in a \*nix-like environment you probably have rsync installed already. Windows 10+ users should use the Windows Linux Subsystem. Then, from the project directory, run `vagrant up`. 12 | 13 | By default, the box forwards ports 8080 and 3306 for the application and database, respectively. If you would like to change these defaults, set `VAGRANT_APP_PORT` and/or `VAGRANT_DB_PORT` as environment variables prior to running `vagrant up` (e.g. `VAGRANT_DB_PORT=3305 vagrant up`). 14 | 15 | ## Configuration 16 | 17 | Go to the `config` directory on your local (host) machine and copy `dev.config.template` to `dev.config`. You'll need to do this to let the API know the configuration details for the environment you're working in (the defaults in that file match up with what's in the Vagrant box, so there's no need to change anything). 18 | 19 | ## Changes 20 | 21 | To avoid build issues, we use rsync instead of Virtualbox's shared folders (Vagrant's default). In order to make sure syncing occurs properly, we make `npm run bridge` available. This sets up auto-syncing from your machine to the virtual guest and connects you to the guest via ssh. 22 | 23 | Once connected to the machine via the bridge, you can start the development server with `npm run dev` (inside the virtual guest). The dev server auto-reloads when it receives changes from rsync, which means that you can edit the source from your host machine and expect changes to be deployed immediately (unless there are errors). 24 | 25 | Any changes to migrations or dependencies will require stopping the server, running any necessary migrations or package updates, and then restarting the development server via `npm run dev`. 26 | 27 | Finally, note that rsync is a host-to-guest sync, so any source changes made directly on the Vagrant box **will be overwritten** when you make changes on your local (host) machine! Treat the guest machine as a deployment target. 28 | 29 | ## Other Information 30 | Be sure to read the [database README](/database/README.md) if you plan to work on issues that have the `database` tag. It has important information that you'll want to be familiar with. 31 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | Vagrant.configure("2") do |config| 2 | config.vm.box = "ubuntu/xenial64" 3 | 4 | config.vm.synced_folder ".", "/vagrant", disabled: true 5 | config.vm.synced_folder ".", "/hackillinois/api", create: "true", type: "rsync", 6 | rsync__exclude: [".git", "node_modules"] 7 | 8 | config.vm.provider :virtualbox do |vb| 9 | vb.name = "hackillinois-api" 10 | end 11 | 12 | config.vm.provision "shell", privileged: true, inline: <<-DEPENDENCIES 13 | onerror(){ echo "Command failed. Stopping execution..."; exit 1; } 14 | trap onerror ERR 15 | cd /tmp 16 | 17 | echo "Updating package lists (this may take a while)" 18 | export DEBIAN_FRONTEND=noninteractive 19 | apt-get update 20 | 21 | echo "Installing system packages (this may take a while)" 22 | apt-get -y -q install python2.7 make g++ mysql-server-5.7 redis-server 23 | ln /usr/bin/python2.7 /usr/bin/python 24 | 25 | echo "Installing Node.js" 26 | curl -sL https://deb.nodesource.com/setup_6.x | bash - \ 27 | && apt-get -y -q install nodejs \ 28 | && npm config set python python2.7 \ 29 | && npm install -g node-gyp nodemon 30 | 31 | echo "Installing Flyway" 32 | wget https://repo1.maven.org/maven2/org/flywaydb/flyway-commandline/4.0.3/flyway-commandline-4.0.3-linux-x64.tar.gz -nv &>/dev/null \ 33 | && tar -xzf flyway-commandline-4.0.3-linux-x64.tar.gz \ 34 | && mv flyway-4.0.3 /opt/flyway-4.0.3 \ 35 | && ln -s /opt/flyway-4.0.3/jre/bin/java /usr/local/bin/java \ 36 | && ln -s /opt/flyway-4.0.3/flyway /usr/local/bin/flyway \ 37 | && chmod +x /opt/flyway-4.0.3/flyway \ 38 | && rm -rf /tmp/flyway-* \ 39 | 40 | echo "Configuring MySQL" 41 | mysql_ssl_rsa_setup --uid=mysql &>/dev/null 42 | service mysql restart 43 | mysql -u root -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'pass123'; FLUSH PRIVILEGES;" 44 | mysql -u root -p"pass123" -e "CREATE DATABASE hackillinois" 45 | 46 | DEPENDENCIES 47 | 48 | config.vm.provision "shell", inline: <<-SETUP 49 | onerror(){ echo "Command failed. Stopping execution..."; exit 1; } 50 | trap onerror ERR 51 | cd /hackillinois/api 52 | 53 | echo "Installing API" 54 | npm install 55 | 56 | echo "Running migrations" 57 | export DB_USERNAME=root 58 | export DB_PASSWORD=pass123 59 | export DB_HOSTNAME=127.0.0.1 60 | export DB_PORT=3306 61 | export DB_NAME=hackillinois 62 | npm run dev-migrations 63 | 64 | echo "Finishing Setup" 65 | echo "cd /hackillinois/api" >> /home/ubuntu/.bashrc 66 | 67 | SETUP 68 | 69 | config.vm.network "forwarded_port", guest: 8080, host: ENV['VAGRANT_APP_PORT'] || 8080 70 | config.vm.network "forwarded_port", guest: 3306, host: ENV['VAGRANT_DB_PORT'] || 3306 71 | end 72 | -------------------------------------------------------------------------------- /api.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const requid = require('cuid'); 3 | const helmet = require('helmet'); 4 | 5 | const ctx = require('ctx'); 6 | const config = ctx.config(); 7 | const logger = ctx.logger(); 8 | 9 | // the dirname is local to every module, so we expose the app root's cwd 10 | // here (before initializing the api) 11 | config.cwd = process.__dirname; 12 | 13 | const instance = express(); 14 | instance.use(helmet()); 15 | instance.use((req, res, next) => { 16 | req.id = requid(); 17 | next(); 18 | }); 19 | 20 | const api = require('./api/'); 21 | instance.use('/v1', api.v1); 22 | 23 | instance.listen(config.port, () => { 24 | logger.info('initialized api (http://localhost:%d)', config.port); 25 | }); 26 | -------------------------------------------------------------------------------- /api/cache.js: -------------------------------------------------------------------------------- 1 | const config = require('./config'); 2 | const logger = require('./logging'); 3 | 4 | const _Promise = require('bluebird'); 5 | const redis = _Promise.promisifyAll(require('redis')); 6 | 7 | const _REDIS_CONFIG = { 8 | host: config.redis.host, 9 | port: config.redis.port 10 | }; 11 | 12 | function CacheManager () { 13 | logger.info('connecting to cache'); 14 | 15 | this._cache = redis.createClient(_REDIS_CONFIG); 16 | this._cache.on('error', (err) => { 17 | logger.error(err); 18 | }); 19 | } 20 | 21 | CacheManager.prototype.constructor = CacheManager; 22 | 23 | CacheManager.prototype.instance = function() { 24 | return this._cache; 25 | }; 26 | 27 | module.exports = new CacheManager(); 28 | -------------------------------------------------------------------------------- /api/config.js: -------------------------------------------------------------------------------- 1 | /* jshint esversion: 6 */ 2 | /* eslint-disable no-process-exit, no-console */ 3 | const _ = require('lodash'); 4 | 5 | function handleEnvironmentIncomplete () { 6 | console.error('fatal: environment incomplete. shutting down...'); 7 | process.exit(); 8 | } 9 | 10 | function handleEnvironmentLoad () { 11 | const environment = process.env.NODE_ENV; 12 | const path = '../config/' + environment; 13 | 14 | try { 15 | require.resolve(path); 16 | } catch (e) { 17 | console.error('error: no configuration found for environment \'%s\'', environment); 18 | handleEnvironmentIncomplete(); 19 | } 20 | 21 | return require(path); 22 | } 23 | 24 | function handleEnvironmentOverrides (config, overrides) { 25 | _.forEach(overrides, (configKey, envKey) => { 26 | if (!_.isUndefined(process.env[envKey])) { 27 | let overrideValue = process.env[envKey]; 28 | if (envKey === 'AWS') { 29 | overrideValue = !!overrideValue; 30 | } 31 | if (envKey === 'APP_PORT' || envKey === 'DB_PORT' || envKey === 'REDIS_PORT') { 32 | overrideValue = parseInt(overrideValue); 33 | } 34 | 35 | _.set(config, configKey, overrideValue); 36 | } 37 | }); 38 | } 39 | 40 | function handleEnvironmentRequireds (config, requireds) { 41 | let incomplete = false; 42 | requireds.forEach((requiredKey) => { 43 | if (_.isNil(_.get(config, requiredKey))) { 44 | incomplete = true; 45 | console.error('error: configuration key %s was null or undefined. it should set or overriden', requiredKey); 46 | } 47 | }); 48 | 49 | if (incomplete) { 50 | handleEnvironmentIncomplete(); 51 | } 52 | } 53 | 54 | function handleAWSOverrides (config) { 55 | const sharedAWSCreds = new (require('aws-sdk').SharedIniFileCredentials)(); 56 | config.aws.defaults.credentials = (sharedAWSCreds.accessKeyId) ? sharedAWSCreds : undefined; 57 | 58 | if (config.aws.enabled && _.isUndefined(config.aws.defaults.credentials)) { 59 | console.error('error: unable to retrieve AWS credentials, but AWS access was enabled'); 60 | } 61 | } 62 | 63 | const config = handleEnvironmentLoad(); 64 | 65 | const overrides = {}; 66 | overrides['AWS'] = 'aws.enabled'; 67 | overrides['SECRET'] = 'auth.secret'; 68 | overrides['APP_PORT'] = 'port'; 69 | overrides['SUPERUSER_EMAIL'] = 'superuser.email'; 70 | overrides['SUPERUSER_PASSWORD'] = 'superuser.password'; 71 | overrides['MAIL_KEY'] = 'mail.key'; 72 | overrides['GITHUB_CLIENT_ID'] = 'auth.github.id'; 73 | overrides['GITHUB_CLIENT_SECRET'] = 'auth.github.secret'; 74 | overrides['GITHUB_MOBILE_REDIRECT'] = 'auth.github.mobileRedirect'; 75 | overrides['DB_NAME'] = 'database.primary.name'; 76 | overrides['DB_USERNAME'] = 'database.primary.user'; 77 | overrides['DB_PASSWORD'] = 'database.primary.password'; 78 | overrides['DB_HOSTNAME'] = 'database.primary.host'; 79 | overrides['DB_PORT'] = 'database.primary.port'; 80 | overrides['REDIS_HOST'] = 'redis.host'; 81 | overrides['REDIS_PORT'] = 'redis.port'; 82 | overrides['RATELIMIT_COUNT'] = 'limit.count'; 83 | overrides['RATELIMIT_WINDOW'] = 'limit.window'; 84 | handleEnvironmentOverrides(config, overrides); 85 | handleAWSOverrides(config); 86 | 87 | const requireds = new Set(_.values(overrides)); 88 | if (!config.isProduction) { 89 | requireds.delete(overrides['MAIL_KEY']); 90 | requireds.delete(overrides['GITHUB_CLIENT_ID']); 91 | requireds.delete(overrides['GITHUB_CLIENT_SECRET']); 92 | requireds.delete(overrides['GITHUB_MOBILE_REDIRECT']); 93 | requireds.delete(overrides['RATELIMIT_COUNT']); 94 | requireds.delete(overrides['RATELIMIT_WINDOW']); 95 | } 96 | handleEnvironmentRequireds(config, requireds); 97 | 98 | module.exports = config; 99 | -------------------------------------------------------------------------------- /api/database.js: -------------------------------------------------------------------------------- 1 | const config = require('./config'); 2 | const logger = require('./logging'); 3 | 4 | const milliseconds = require('ms'); 5 | 6 | const KNEX_CONFIG = { 7 | client: 'mysql', 8 | connection: { 9 | host: config.database.primary.host, 10 | port: config.database.primary.port, 11 | user: config.database.primary.user, 12 | password: config.database.primary.password, 13 | database: config.database.primary.name 14 | }, 15 | pool: { 16 | min: config.database.primary.pool.min, 17 | max: config.database.primary.pool.max, 18 | idleTimeout: milliseconds(config.database.primary.pool.idleTimeout) 19 | } 20 | }; 21 | 22 | function DatabaseManager() { 23 | logger.info('connecting to database'); 24 | this._knex = require('knex')(KNEX_CONFIG); 25 | 26 | this._bookshelf = require('bookshelf')(this._knex); 27 | this._bookshelf.plugin('pagination'); 28 | } 29 | 30 | DatabaseManager.prototype.constructor = DatabaseManager; 31 | 32 | DatabaseManager.prototype.instance = function() { 33 | return this._bookshelf; 34 | }; 35 | 36 | DatabaseManager.prototype.connection = function () { 37 | return this._knex; 38 | }; 39 | 40 | module.exports = new DatabaseManager(); 41 | -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | const ctx = require('ctx'); 2 | const config = ctx.config(); 3 | const logger = ctx.logger(); 4 | const v1 = require('./v1'); 5 | 6 | logger.info('starting superuser setup check in the background'); 7 | 8 | // the following uses the V1 API to ensure that a superuser is present on the 9 | // currently-running instance. we might want to consider a way to do this without 10 | // using version-specific functionality 11 | const User = require('./v1/models/User'); 12 | const utils = require('./v1/utils/'); 13 | 14 | User.findByEmail(config.superuser.email) 15 | .then((result) => { 16 | if (!result) { 17 | return User.create(config.superuser.email, config.superuser.password, utils.roles.SUPERUSER); 18 | } 19 | 20 | return null; 21 | }); 22 | 23 | module.exports = { 24 | v1: v1 25 | }; 26 | -------------------------------------------------------------------------------- /api/logging.js: -------------------------------------------------------------------------------- 1 | const winston = require('winston'); 2 | 3 | const config = require('./config'); 4 | 5 | const transports = []; 6 | const consoleTransport = new winston.transports.Console({ 7 | colorize: !config.isProduction, 8 | prettyPrint: !config.isProduction, 9 | stringify: config.isProduction, 10 | json: config.isProduction, 11 | level: 'debug' 12 | }); 13 | 14 | // add other transports as needed 15 | transports.push(consoleTransport); 16 | 17 | const logger = new winston.Logger({ 18 | transports: transports 19 | }); 20 | 21 | module.exports = logger; 22 | -------------------------------------------------------------------------------- /api/v1/controllers/AnnouncementController.js: -------------------------------------------------------------------------------- 1 | const bodyParser = require('body-parser'); 2 | const router = require('express').Router(); 3 | 4 | const AnnouncementRequest = require('../requests/AnnouncementRequest'); 5 | const AnnouncementService = require('../services/AnnouncementService'); 6 | const middleware = require('../middleware'); 7 | const roles = require('../utils/roles'); 8 | 9 | function getAllAnnouncements(req, res, next) { 10 | const before = (Date.parse(req.query.before)) ? new Date(req.query.before) : null; 11 | const after = (Date.parse(req.query.after)) ? new Date(req.query.after) : null; 12 | const limit = Number.parseInt(req.query.limit) || null; 13 | 14 | return AnnouncementService.getAllAnnouncements(before, after, limit) 15 | .then((result) => { 16 | res.body = result.toJSON(); 17 | return next(); 18 | }) 19 | .catch((err) => next(err)); 20 | } 21 | 22 | function createAnnouncement(req, res, next) { 23 | return AnnouncementService.createAnnouncement(req.body) 24 | .then((result) => { 25 | res.body = result.toJSON(); 26 | return next(); 27 | }) 28 | .catch((err) => next(err)); 29 | } 30 | 31 | function updateAnnouncement(req, res, next) { 32 | return AnnouncementService.findById(req.params.id) 33 | .then((announcement) => AnnouncementService.updateAnnouncement(announcement, req.body)) 34 | .then((result) => { 35 | res.body = result.toJSON(); 36 | return next(); 37 | }) 38 | .catch((err) => next(err)); 39 | } 40 | 41 | function deleteAnnouncement(req, res, next) { 42 | return AnnouncementService.findById(req.params.id) 43 | .then((announcement) => AnnouncementService.deleteAnnouncement(announcement)) 44 | .then(() => next()) 45 | .catch((err) => next(err)); 46 | } 47 | 48 | router.use(bodyParser.json()); 49 | router.use(middleware.auth); 50 | 51 | router.post('/', middleware.request(AnnouncementRequest), middleware.permission(roles.ADMIN), createAnnouncement); 52 | router.get('/all', getAllAnnouncements); 53 | router.put('/:id(\\d+)', middleware.request(AnnouncementRequest), middleware.permission(roles.ADMIN), updateAnnouncement); 54 | router.delete('/:id(\\d+)', middleware.permission(roles.ADMIN), deleteAnnouncement); 55 | 56 | router.use(middleware.response); 57 | router.use(middleware.errors); 58 | 59 | module.exports.router = router; 60 | -------------------------------------------------------------------------------- /api/v1/controllers/CheckInController.js: -------------------------------------------------------------------------------- 1 | const bodyParser = require('body-parser'); 2 | const _ = require('lodash'); 3 | 4 | const services = require('../services'); 5 | const middleware = require('../middleware'); 6 | const requests = require('../requests'); 7 | const roles = require('../utils/roles'); 8 | const config = require('ctx').config(); 9 | 10 | const router = require('express').Router(); 11 | 12 | function updateCheckInByUserId(req, res, next) { 13 | req.body.userId = req.params.id; 14 | delete req.body.credentialsRequested; 15 | 16 | services.CheckInService 17 | .updateCheckIn(req.body) 18 | .then((response) => { 19 | response.checkin = response.checkin.toJSON(); 20 | if (!_.isNil(response.credentials)) { 21 | response.credentials = response.credentials.toJSON(); 22 | } 23 | res.body = response; 24 | return next(); 25 | }) 26 | .catch((error) => next(error)); 27 | } 28 | 29 | function fetchCheckInByUserId(req, res, next) { 30 | services.CheckInService 31 | .findCheckInByUserId(req.params.id) 32 | .then((response) => { 33 | response.checkin = response.checkin.toJSON(); 34 | if (!_.isNil(response.credentials)) { 35 | response.credentials = response.credentials.toJSON(); 36 | } 37 | res.body = response; 38 | return next(); 39 | }) 40 | .catch((error) => next(error)); 41 | } 42 | 43 | function fetchCheckInByUser(req, res, next) { 44 | services.CheckInService 45 | .findCheckInByUserId(req.user.id) 46 | .then((response) => { 47 | response.checkin = response.checkin.toJSON(); 48 | if (!_.isNil(response.credentials)) { 49 | response.credentials = response.credentials.toJSON(); 50 | } 51 | res.body = response; 52 | return next(); 53 | }) 54 | .catch((error) => next(error)); 55 | } 56 | 57 | function createCheckIn(req, res, next) { 58 | req.body.userId = req.params.id; 59 | services.CheckInService 60 | .createCheckIn(req.body) 61 | .tap((model) => services.UserService.findUserById(model.checkin.get('userId')) 62 | .then((user) => { 63 | const substitutions = { 64 | isDevelopment: config.isDevelopment 65 | }; 66 | services.MailService.send(user.get('email'), config.mail.templates.slackInvite, substitutions); 67 | return null; 68 | } 69 | )) 70 | .then((response) => { 71 | response.checkin = response.checkin.toJSON(); 72 | if (!_.isNil(response.credentials)) { 73 | response.credentials = response.credentials.toJSON(); 74 | } 75 | res.body = response; 76 | return next(); 77 | }) 78 | .catch((error) => next(error)); 79 | } 80 | 81 | 82 | router.use(bodyParser.json()); 83 | router.use(middleware.auth); 84 | 85 | router.post('/user/:id(\\d+)', middleware.request(requests.CheckInRequest), 86 | middleware.permission(roles.HOSTS), createCheckIn); 87 | router.put('/user/:id(\\d+)', middleware.request(requests.CheckInRequest), 88 | middleware.permission(roles.HOSTS), updateCheckInByUserId); 89 | router.get('/user/:id(\\d+)', middleware.permission(roles.HOSTS), fetchCheckInByUserId); 90 | router.get('/', fetchCheckInByUser); 91 | 92 | router.use(middleware.response); 93 | router.use(middleware.errors); 94 | 95 | 96 | module.exports.router = router; 97 | -------------------------------------------------------------------------------- /api/v1/controllers/EcosystemController.js: -------------------------------------------------------------------------------- 1 | const bodyParser = require('body-parser'); 2 | const middleware = require('../middleware'); 3 | const router = require('express').Router(); 4 | 5 | const requests = require('../requests'); 6 | const roles = require('../utils/roles'); 7 | 8 | const EcosystemService = require('../services/EcosystemService'); 9 | 10 | function createEcosystem(req, res, next) { 11 | EcosystemService 12 | .createEcosystem(req.body.name) 13 | .then((newEcosystem) => { 14 | res.body = newEcosystem.toJSON(); 15 | 16 | return next(); 17 | }) 18 | .catch((error) => next(error)); 19 | } 20 | 21 | function getAllEcosystems(req, res, next) { 22 | EcosystemService 23 | .getAllEcosystems() 24 | .then((results) => { 25 | res.body = results.toJSON(); 26 | 27 | return next(); 28 | }) 29 | .catch((error) => next(error)); 30 | } 31 | 32 | function deleteEcosystem(req, res, next) { 33 | EcosystemService 34 | .deleteEcosystem(req.body.name) 35 | .then(() => { 36 | res.body = {}; 37 | 38 | return next(); 39 | }) 40 | .catch((error) => next(error)); 41 | } 42 | 43 | 44 | router.use(bodyParser.json()); 45 | router.use(middleware.auth); 46 | 47 | router.get('/all', middleware.permission(roles.ORGANIZERS), getAllEcosystems); 48 | router.post('/', middleware.request(requests.EcosystemCreationRequest), middleware.permission(roles.ORGANIZERS), createEcosystem); 49 | router.delete('/', middleware.permission(roles.ORGANIZERS), deleteEcosystem); 50 | 51 | router.use(middleware.response); 52 | router.use(middleware.errors); 53 | 54 | module.exports.router = router; 55 | -------------------------------------------------------------------------------- /api/v1/controllers/HealthController.js: -------------------------------------------------------------------------------- 1 | const middleware = require('../middleware'); 2 | 3 | const router = require('express').Router(); 4 | 5 | function healthCheck(req, res, next) { 6 | return next(); 7 | } 8 | 9 | router.get('', healthCheck); 10 | router.use(middleware.response); 11 | router.use(middleware.errors); 12 | 13 | module.exports.router = router; 14 | -------------------------------------------------------------------------------- /api/v1/controllers/MailController.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const bodyParser = require('body-parser'); 3 | const _Promise = require('bluebird'); 4 | 5 | const services = require('../services'); 6 | const middleware = require('../middleware'); 7 | const requests = require('../requests'); 8 | const roles = require('../utils/roles'); 9 | const config = require('ctx').config(); 10 | 11 | const router = require('express').Router(); 12 | 13 | const ACCEPTANCE_LISTS = ['wave1', 'wave2', 'wave3', 'wave4', 'wave5']; 14 | 15 | function sendMailinglist(req, res, next) { 16 | const listName = req.body.listName; 17 | const mailList = config.mail.lists[listName]; 18 | const template = req.body.template; 19 | 20 | services.MailService.checkIfSent(mailList) 21 | .then(() => services.MailService.sendToList(mailList, template)) 22 | .then(() => { 23 | if (_.includes(ACCEPTANCE_LISTS, listName)) { 24 | return services.MailService.markAsSent(mailList); 25 | } 26 | return _Promise.resolve(true); 27 | }) 28 | .then(() => { 29 | res.body = {}; 30 | res.body.sent = true; 31 | 32 | return next(); 33 | }) 34 | .catch((error) => next(error)); 35 | } 36 | 37 | 38 | router.use(bodyParser.json()); 39 | router.use(middleware.auth); 40 | 41 | router.put('/send', middleware.request(requests.SendListRequest), middleware.permission(roles.ORGANIZERS), sendMailinglist); 42 | 43 | router.use(middleware.response); 44 | router.use(middleware.errors); 45 | 46 | module.exports.router = router; 47 | -------------------------------------------------------------------------------- /api/v1/controllers/PermissionController.js: -------------------------------------------------------------------------------- 1 | const bodyParser = require('body-parser'); 2 | const middleware = require('../middleware'); 3 | const router = require('express').Router(); 4 | 5 | const roles = require('../utils/roles'); 6 | 7 | const PermissionService = require('../services/PermissionService'); 8 | 9 | function isOrganizer(req, res, next) { 10 | PermissionService.isOrganizer(req.user) 11 | .then((isOrganizer) => { 12 | res.body = {}; 13 | res.body.allowed = isOrganizer; 14 | 15 | return next(); 16 | }) 17 | .catch((error) => next(error)); 18 | } 19 | 20 | function isHost(req, res, next) { 21 | PermissionService.isHost(req.user) 22 | .then((isHost) => { 23 | res.body = {}; 24 | res.body.allowed = isHost; 25 | 26 | return next(); 27 | }) 28 | .catch((error) => next(error)); 29 | } 30 | 31 | router.use(bodyParser.json()); 32 | router.use(middleware.auth); 33 | 34 | router.get('/host', middleware.permission(roles.ALL), isHost); 35 | router.get('/organizer', middleware.permission(roles.ALL), isOrganizer); 36 | 37 | router.use(middleware.response); 38 | router.use(middleware.errors); 39 | 40 | module.exports.router = router; 41 | -------------------------------------------------------------------------------- /api/v1/controllers/RecruiterInterestController.js: -------------------------------------------------------------------------------- 1 | const bodyParser = require('body-parser'); 2 | 3 | const services = require('../services'); 4 | 5 | const middleware = require('../middleware'); 6 | const requests = require('../requests'); 7 | const roles = require('../utils/roles'); 8 | 9 | const router = require('express').Router(); 10 | 11 | function createInterest(req, res, next) { 12 | services.RegistrationService 13 | .findAttendeeById(req.body.attendeeUserId, false) 14 | .then((attendee) => services.RecruiterInterestService 15 | .createInterest(req.user.get('id'), attendee.get('id'), req.body.comments, req.body.favorite) 16 | .then((result) => { 17 | res.body = result.toJSON(); 18 | return next(); 19 | })) 20 | .catch((error) => next(error)); 21 | } 22 | 23 | function getRecruitersInterests(req, res, next) { 24 | services.RecruiterInterestService 25 | .findByRecruiterId(req.user.get('id')) 26 | .then((result) => { 27 | res.body = result.toJSON(); 28 | return next(); 29 | }) 30 | .catch((error) => next(error)); 31 | } 32 | 33 | function updateRecruiterInterest(req, res, next) { 34 | services.RecruiterInterestService 35 | .updateInterest(req.params.id, req.body.comments, req.body.favorite) 36 | .then((result) => { 37 | res.body = result.toJSON(); 38 | return next(); 39 | }) 40 | .catch((error) => next(error)); 41 | } 42 | 43 | router.use(bodyParser.json()); 44 | router.use(middleware.auth); 45 | 46 | router.get('/interest/all', middleware.permission(roles.PROFESSIONALS), getRecruitersInterests); 47 | router.post('/interest', middleware.request(requests.RecruiterInterestRequest), middleware.permission(roles.PROFESSIONALS), createInterest); 48 | router.put('/interest/:id(\\d+)', middleware.request(requests.UpdateRecruiterInterestRequest), middleware.permission(roles.PROFESSIONALS), updateRecruiterInterest); 49 | 50 | router.use(middleware.response); 51 | router.use(middleware.errors); 52 | 53 | module.exports.router = router; 54 | -------------------------------------------------------------------------------- /api/v1/controllers/StatsController.js: -------------------------------------------------------------------------------- 1 | const bodyParser = require('body-parser'); 2 | const middleware = require('../middleware'); 3 | const router = require('express').Router(); 4 | 5 | const roles = require('../utils/roles'); 6 | 7 | const StatsService = require('../services/StatsService'); 8 | function getAllStats(req, res, next) { 9 | StatsService.fetchAllStats() 10 | .then((stats) => { 11 | res.body = stats; 12 | 13 | return next(); 14 | }) 15 | .catch((error) => next(error)); 16 | } 17 | 18 | function getRegStats(req, res, next) { 19 | StatsService.fetchRegistrationStats() 20 | .then((stats) => { 21 | res.body = stats; 22 | 23 | return next(); 24 | }) 25 | .catch((error) => next(error)); 26 | } 27 | 28 | function getRSVPStats(req, res, next) { 29 | StatsService.fetchRSVPStats() 30 | .then((stats) => { 31 | res.body = stats; 32 | 33 | return next(); 34 | }) 35 | .catch((error) => next(error)); 36 | } 37 | 38 | function getLiveEventStats(req, res, next) { 39 | StatsService.fetchLiveEventStats() 40 | .then((stats) => { 41 | res.body = stats; 42 | 43 | return next(); 44 | }) 45 | .catch((error) => next(error)); 46 | } 47 | 48 | 49 | router.use(bodyParser.json()); 50 | router.use(middleware.auth); 51 | 52 | router.get('/all', middleware.permission(roles.ORGANIZERS), getAllStats); 53 | router.get('/registration', middleware.permission(roles.ORGANIZERS), getRegStats); 54 | router.get('/rsvp', middleware.permission(roles.ORGANIZERS), getRSVPStats); 55 | router.get('/live', middleware.permission(roles.ORGANIZERS), getLiveEventStats); 56 | 57 | router.use(middleware.response); 58 | router.use(middleware.errors); 59 | 60 | module.exports.router = router; 61 | -------------------------------------------------------------------------------- /api/v1/controllers/TrackingController.js: -------------------------------------------------------------------------------- 1 | const bodyParser = require('body-parser'); 2 | 3 | const services = require('../services'); 4 | const middleware = require('../middleware'); 5 | const requests = require('../requests'); 6 | const roles = require('../utils/roles'); 7 | 8 | const router = require('express').Router(); 9 | 10 | function createTrackedEvent(req, res, next) { 11 | services.TrackingService 12 | .createTrackingEvent(req.body) 13 | .then((result) => { 14 | res.body = result.toJSON(); 15 | 16 | return next(); 17 | }) 18 | .catch((error) => next(error)); 19 | } 20 | 21 | function addTrackedEventParticipant(req, res, next) { 22 | services.TrackingService 23 | .addEventParticipant(req.params.participantId) 24 | .then(() => next()) 25 | .catch((error) => next(error)); 26 | } 27 | 28 | router.use(bodyParser.json()); 29 | router.use(middleware.auth); 30 | 31 | router.post('/', middleware.request(requests.UniversalTrackingRequest), 32 | middleware.permission(roles.ADMIN), createTrackedEvent); 33 | router.get('/:participantId', middleware.permission(roles.HOSTS), addTrackedEventParticipant); 34 | 35 | router.use(middleware.response); 36 | router.use(middleware.errors); 37 | 38 | module.exports.router = router; 39 | -------------------------------------------------------------------------------- /api/v1/controllers/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | AnnouncementController: require('./AnnouncementController.js'), 3 | AuthController: require('./AuthController.js'), 4 | EcosystemController: require('./EcosystemController.js'), 5 | EventController: require('./EventController.js'), 6 | UploadController: require('./UploadController.js'), 7 | UserController: require('./UserController.js'), 8 | RecruiterInterestController: require('./RecruiterInterestController.js'), 9 | RegistrationController: require('./RegistrationController.js'), 10 | PermissionController: require('./PermissionController.js'), 11 | ProjectController: require('./ProjectController.js'), 12 | HealthController: require('./HealthController.js'), 13 | CheckInController: require('./CheckInController.js'), 14 | RSVPController: require('./RSVPController.js'), 15 | StatsController: require('./StatsController.js'), 16 | TrackingController: require('./TrackingController.js'), 17 | MailController: require('./MailController.js') 18 | }; 19 | -------------------------------------------------------------------------------- /api/v1/errors/ApiError.js: -------------------------------------------------------------------------------- 1 | const ERROR_TYPE = 'ApiError'; 2 | const ERROR_TITLE = 'API Error'; 3 | const STATUS_CODE = 500; 4 | 5 | const DEFAULT_MESSAGE = 'An error occurred. If this persists, please contact us'; 6 | 7 | function ApiError(message, source) { 8 | Error.call(this, message); 9 | 10 | this.type = ERROR_TYPE; 11 | this.status = STATUS_CODE; 12 | this.title = ERROR_TITLE; 13 | this.message = (message) ? message : DEFAULT_MESSAGE; 14 | this.source = (source) ? source : null; 15 | 16 | this.isApiError = true; 17 | } 18 | 19 | ApiError.prototype = Object.create(Error.prototype); 20 | ApiError.prototype.toJSON = function() { 21 | return { 22 | type: this.type, 23 | status: this.status, 24 | title: this.title, 25 | message: this.message, 26 | source: this.source 27 | }; 28 | }; 29 | 30 | module.exports = ApiError; 31 | -------------------------------------------------------------------------------- /api/v1/errors/Constants.js: -------------------------------------------------------------------------------- 1 | const DUP_ENTRY = 'ER_DUP_ENTRY'; 2 | 3 | module.exports.DupEntry = DUP_ENTRY; 4 | -------------------------------------------------------------------------------- /api/v1/errors/EntityNotSupportedError.js: -------------------------------------------------------------------------------- 1 | const UnprocessableRequestError = require('./UnprocessableRequestError.js'); 2 | 3 | const ERROR_TYPE = 'EntityNotSupportedError'; 4 | const ERROR_TITLE = 'Entity Not Supported'; 5 | const ERROR_STATUS = 415; 6 | 7 | const DEFAULT_MESSAGE = 'The provided entity cannot be handled by this resource'; 8 | 9 | function EntityNotSupportedError(message, source) { 10 | UnprocessableRequestError.call(this, message, source); 11 | 12 | this.type = ERROR_TYPE; 13 | this.title = ERROR_TITLE; 14 | this.status = ERROR_STATUS; 15 | this.message = (message) ? message : DEFAULT_MESSAGE; 16 | this.source = (source) ? source : null; 17 | } 18 | 19 | EntityNotSupportedError.prototype = Object.create(UnprocessableRequestError.prototype); 20 | EntityNotSupportedError.prototype.constructor = EntityNotSupportedError; 21 | 22 | module.exports = EntityNotSupportedError; 23 | -------------------------------------------------------------------------------- /api/v1/errors/EntityTooLargeError.js: -------------------------------------------------------------------------------- 1 | const UnprocessableRequestError = require('./UnprocessableRequestError.js'); 2 | 3 | const ERROR_TYPE = 'EntityTooLargeError'; 4 | const ERROR_TITLE = 'Entity Too Large'; 5 | const ERROR_STATUS = 413; 6 | 7 | const DEFAULT_MESSAGE = 'The provided entity was too large'; 8 | 9 | function EntityTooLargeError(message, source) { 10 | UnprocessableRequestError.call(this, message, source); 11 | 12 | this.type = ERROR_TYPE; 13 | this.title = ERROR_TITLE; 14 | this.status = ERROR_STATUS; 15 | this.message = (message) ? message : DEFAULT_MESSAGE; 16 | this.source = (source) ? source : null; 17 | } 18 | 19 | EntityTooLargeError.prototype = Object.create(UnprocessableRequestError.prototype); 20 | EntityTooLargeError.prototype.constructor = EntityTooLargeError; 21 | 22 | module.exports = EntityTooLargeError; 23 | -------------------------------------------------------------------------------- /api/v1/errors/ExistsError.js: -------------------------------------------------------------------------------- 1 | const UnprocessableRequestError = require('./UnprocessableRequestError'); 2 | 3 | const ERROR_TYPE = 'ExistsError'; 4 | const ERROR_TITLE = 'Already Exists'; 5 | 6 | const DEFAULT_MESSAGE = 'The resource described already exists'; 7 | 8 | function ExistsError(message, source) { 9 | UnprocessableRequestError.call(this, message, source); 10 | 11 | this.type = ERROR_TYPE; 12 | this.title = ERROR_TITLE; 13 | this.message = (message) ? message : DEFAULT_MESSAGE; 14 | } 15 | 16 | ExistsError.prototype = Object.create(UnprocessableRequestError.prototype); 17 | ExistsError.prototype.constructor = ExistsError; 18 | 19 | module.exports = ExistsError; 20 | -------------------------------------------------------------------------------- /api/v1/errors/ExternalProviderError.js: -------------------------------------------------------------------------------- 1 | const ApiError = require('./ApiError'); 2 | 3 | const ERROR_TYPE = 'ExternalProviderError'; 4 | const ERROR_TITLE = 'External Provider Error'; 5 | const STATUS_CODE = 500; 6 | 7 | function ExternalProviderError(message, source) { 8 | ApiError.call(this, message, source); 9 | 10 | this.type = ERROR_TYPE; 11 | this.status = STATUS_CODE; 12 | this.title = ERROR_TITLE; 13 | 14 | this.isApiError = false; 15 | } 16 | 17 | ExternalProviderError.prototype = Object.create(ApiError.prototype); 18 | ExternalProviderError.prototype.constructor = ExternalProviderError; 19 | 20 | module.exports = ExternalProviderError; 21 | -------------------------------------------------------------------------------- /api/v1/errors/InvalidHeaderError.js: -------------------------------------------------------------------------------- 1 | const UnprocessableRequestError = require('./UnprocessableRequestError'); 2 | 3 | const ERROR_TYPE = 'InvalidHeaderError'; 4 | const ERROR_TITLE = 'Invalid Header'; 5 | 6 | const DEFAULT_MESSAGE = 'One or more headers present in the request were invalid'; 7 | 8 | function InvalidHeaderError(message, source) { 9 | UnprocessableRequestError.call(this, message, source); 10 | 11 | this.type = ERROR_TYPE; 12 | this.title = ERROR_TITLE; 13 | this.message = (message) ? message : DEFAULT_MESSAGE; 14 | this.source = (source) ? source : undefined; 15 | } 16 | 17 | InvalidHeaderError.prototype = Object.create(UnprocessableRequestError.prototype); 18 | InvalidHeaderError.prototype.constructor = InvalidHeaderError; 19 | 20 | module.exports = InvalidHeaderError; 21 | -------------------------------------------------------------------------------- /api/v1/errors/InvalidParameterError.js: -------------------------------------------------------------------------------- 1 | const UnprocessableRequestError = require('./UnprocessableRequestError.js'); 2 | 3 | const ERROR_TYPE = 'InvalidParameterError'; 4 | const ERROR_TITLE = 'Invalid Parameter'; 5 | 6 | const DEFAULT_MESSAGE = 'One or more parameters present in the request were invalid'; 7 | 8 | function InvalidParameterError(message, source) { 9 | UnprocessableRequestError.call(this, message, source); 10 | 11 | this.type = ERROR_TYPE; 12 | this.title = ERROR_TITLE; 13 | this.message = (message) ? message : DEFAULT_MESSAGE; 14 | this.source = (source) ? source : null; 15 | } 16 | 17 | InvalidParameterError.prototype = Object.create(UnprocessableRequestError.prototype); 18 | InvalidParameterError.prototype.constructor = InvalidParameterError; 19 | 20 | module.exports = InvalidParameterError; 21 | -------------------------------------------------------------------------------- /api/v1/errors/InvalidTrackingStateError.js: -------------------------------------------------------------------------------- 1 | const UnprocessableRequestError = require('./UnprocessableRequestError.js'); 2 | 3 | const ERROR_TYPE = 'InvalidTrackingStateError'; 4 | const ERROR_TITLE = 'Invalid Tracking State'; 5 | const DEFAULT_MESSAGE = 'An error occurred with the current state of the event tracking system'; 6 | 7 | function TrackingInProgressError(message, source) { 8 | UnprocessableRequestError.call(this, message, source); 9 | 10 | this.type = ERROR_TYPE; 11 | this.title = ERROR_TITLE; 12 | this.message = (message) ? message : DEFAULT_MESSAGE; 13 | this.source = (source) ? source : null; 14 | } 15 | 16 | TrackingInProgressError.prototype = Object.create(UnprocessableRequestError.prototype); 17 | TrackingInProgressError.prototype.constructor = TrackingInProgressError; 18 | 19 | module.exports = TrackingInProgressError; 20 | -------------------------------------------------------------------------------- /api/v1/errors/MissingHeaderError.js: -------------------------------------------------------------------------------- 1 | const UnprocessableRequestError = require('./UnprocessableRequestError'); 2 | 3 | const ERROR_TYPE = 'MissingHeaderError'; 4 | const ERROR_TITLE = 'Missing Header'; 5 | 6 | const DEFAULT_MESSAGE = 'One or more headers were missing from the request'; 7 | 8 | function MissingHeaderError(message, source) { 9 | UnprocessableRequestError.call(this, message, source); 10 | 11 | this.type = ERROR_TYPE; 12 | this.title = ERROR_TITLE; 13 | this.message = (message) ? message : DEFAULT_MESSAGE; 14 | this.source = (source) ? source : null; 15 | } 16 | 17 | MissingHeaderError.prototype = Object.create(UnprocessableRequestError.prototype); 18 | MissingHeaderError.prototype.constructor = MissingHeaderError; 19 | 20 | module.exports = MissingHeaderError; 21 | -------------------------------------------------------------------------------- /api/v1/errors/MissingParameterError.js: -------------------------------------------------------------------------------- 1 | const UnprocessableRequestError = require('./UnprocessableRequestError'); 2 | 3 | const ERROR_TYPE = 'MissingParameterError'; 4 | const ERROR_TITLE = 'Missing Parameter'; 5 | 6 | const DEFAULT_MESSAGE = 'One or more parameters were missing from the request'; 7 | 8 | function MissingParameterError(message, source) { 9 | UnprocessableRequestError.call(this, message, source); 10 | 11 | this.type = ERROR_TYPE; 12 | this.title = ERROR_TITLE; 13 | this.message = (message) ? message : DEFAULT_MESSAGE; 14 | this.source = (source) ? source : null; 15 | } 16 | 17 | MissingParameterError.prototype = Object.create(UnprocessableRequestError.prototype); 18 | MissingParameterError.prototype.constructor = MissingParameterError; 19 | 20 | module.exports = MissingParameterError; 21 | -------------------------------------------------------------------------------- /api/v1/errors/NotFoundError.js: -------------------------------------------------------------------------------- 1 | const ApiError = require('./ApiError'); 2 | 3 | const ERROR_TYPE = 'NotFoundError'; 4 | const ERROR_TITLE = 'Not Found'; 5 | const STATUS_CODE = 404; 6 | 7 | const DEFAULT_MESSAGE = 'The requested resource could not be found'; 8 | 9 | function NotFoundError(message, source) { 10 | ApiError.call(this, message, source); 11 | 12 | this.type = ERROR_TYPE; 13 | this.status = STATUS_CODE; 14 | this.title = ERROR_TITLE; 15 | this.message = (message) ? message : DEFAULT_MESSAGE; 16 | this.source = (source) ? source : null; 17 | } 18 | 19 | NotFoundError.prototype = Object.create(ApiError.prototype); 20 | NotFoundError.prototype.constructor = NotFoundError; 21 | 22 | module.exports = NotFoundError; 23 | -------------------------------------------------------------------------------- /api/v1/errors/RedisError.js: -------------------------------------------------------------------------------- 1 | const ApiError = require('./ApiError'); 2 | 3 | const ERROR_TYPE = 'RedisError'; 4 | const ERROR_TITLE = 'Redis Error'; 5 | 6 | const DEFAULT_MESSAGE = 'The key provided does not exist in the Redis DB'; 7 | 8 | function RedisError(message, source) { 9 | ApiError.call(this, message, source); 10 | 11 | this.type = ERROR_TYPE; 12 | this.title = ERROR_TITLE; 13 | this.message = (message) ? message : DEFAULT_MESSAGE; 14 | } 15 | 16 | RedisError.prototype = Object.create(ApiError.prototype); 17 | RedisError.prototype.constructor = RedisError; 18 | 19 | module.exports = RedisError; 20 | -------------------------------------------------------------------------------- /api/v1/errors/TokenExpirationError.js: -------------------------------------------------------------------------------- 1 | const ApiError = require('./ApiError'); 2 | 3 | const ERROR_TYPE = 'TokenExpirationError'; 4 | const ERROR_TITLE = 'Token Expired'; 5 | 6 | const DEFAULT_MESSAGE = 'The provided token has expired'; 7 | const STATUS_CODE = 401; 8 | 9 | function TokenExpirationError(message, source) { 10 | ApiError.call(this, message, source); 11 | 12 | this.type = ERROR_TYPE; 13 | this.status = STATUS_CODE; 14 | this.title = ERROR_TITLE; 15 | this.message = (message) ? message : DEFAULT_MESSAGE; 16 | this.source = (source) ? source : null; 17 | } 18 | 19 | TokenExpirationError.prototype = Object.create(ApiError.prototype); 20 | TokenExpirationError.prototype.constructor = TokenExpirationError; 21 | 22 | module.exports = TokenExpirationError; 23 | -------------------------------------------------------------------------------- /api/v1/errors/UnauthorizedError.js: -------------------------------------------------------------------------------- 1 | const ApiError = require('./ApiError'); 2 | 3 | const ERROR_TYPE = 'UnauthorizedError'; 4 | const ERROR_TITLE = 'Unauthorized'; 5 | 6 | const DEFAULT_MESSAGE = 'The requested resource cannot be accessed with the ' + 7 | 'provided credentials'; 8 | const STATUS_CODE = 401; 9 | 10 | function UnauthorizedError(message, source) { 11 | ApiError.call(this, message, source); 12 | 13 | this.type = ERROR_TYPE; 14 | this.status = STATUS_CODE; 15 | this.title = ERROR_TITLE; 16 | this.message = (message) ? message : DEFAULT_MESSAGE; 17 | this.source = (source) ? source : null; 18 | } 19 | 20 | UnauthorizedError.prototype = Object.create(ApiError.prototype); 21 | UnauthorizedError.prototype.constructor = UnauthorizedError; 22 | 23 | module.exports = UnauthorizedError; 24 | -------------------------------------------------------------------------------- /api/v1/errors/UnprocessableRequestError.js: -------------------------------------------------------------------------------- 1 | const ApiError = require('./ApiError'); 2 | 3 | const ERROR_TYPE = 'UnprocessableRequestError'; 4 | const ERROR_TITLE = 'Unprocessable Request'; 5 | const STATUS_CODE = 400; 6 | 7 | const DEFAULT_MESSAGE = 'The server received a request that could not be processed'; 8 | 9 | function UnprocessableRequestError(message, source) { 10 | ApiError.call(this, message, source); 11 | 12 | this.type = ERROR_TYPE; 13 | this.status = STATUS_CODE; 14 | this.title = ERROR_TITLE; 15 | this.message = (message) ? message : DEFAULT_MESSAGE; 16 | this.source = (source) ? source : null; 17 | } 18 | 19 | UnprocessableRequestError.prototype = Object.create(ApiError.prototype); 20 | UnprocessableRequestError.prototype.constructor = UnprocessableRequestError; 21 | 22 | module.exports = UnprocessableRequestError; 23 | -------------------------------------------------------------------------------- /api/v1/errors/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ApiError: require('./ApiError.js'), 3 | Constants: require('./Constants'), 4 | EntityNotSupportedError: require('./EntityNotSupportedError.js'), 5 | EntityTooLargeError: require('./EntityTooLargeError.js'), 6 | ExistsError: require('./ExistsError.js'), 7 | ExternalProviderError: require('./ExternalProviderError.js'), 8 | InvalidHeaderError: require('./InvalidHeaderError.js'), 9 | InvalidParameterError: require('./InvalidParameterError.js'), 10 | MissingHeaderError: require('./MissingHeaderError.js'), 11 | MissingParameterError: require('./MissingParameterError.js'), 12 | RedisError: require('./RedisError.js'), 13 | NotFoundError: require('./NotFoundError.js'), 14 | TokenExpirationError: require('./TokenExpirationError.js'), 15 | InvalidTrackingStateError: require('./InvalidTrackingStateError'), 16 | UnauthorizedError: require('./UnauthorizedError.js'), 17 | UnprocessableRequestError: require('./UnprocessableRequestError.js') 18 | }; 19 | -------------------------------------------------------------------------------- /api/v1/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const v1 = express.Router(); 3 | 4 | const controllers = require('./controllers'); 5 | const utils = require('./utils'); 6 | const middleware = require('./middleware'); 7 | 8 | // acknowledge receipt of request 9 | v1.use((req, res, next) => { 10 | utils.logs.logRequestReceipt(req); 11 | next(); 12 | }); 13 | 14 | v1.use(middleware.ratelimiting); 15 | 16 | // set up CORS to allow for usage from different origins 17 | // we may remove this in the future 18 | v1.all('*', (req, res, next) => { 19 | res.header('Access-Control-Allow-Origin', '*'); 20 | res.header('Access-Control-Allow-Methods', 'POST, GET, PUT, OPTIONS'); 21 | res.header('Access-Control-Allow-Headers', 'Authorization, Content-Type, Content-Length'); 22 | next(); 23 | }); 24 | 25 | v1.use('/auth', controllers.AuthController.router); 26 | v1.use('/user', controllers.UserController.router); 27 | v1.use('/upload', controllers.UploadController.router); 28 | v1.use('/registration', controllers.RegistrationController.router); 29 | v1.use('/permission', controllers.PermissionController.router); 30 | v1.use('/project', controllers.ProjectController.router); 31 | v1.use('/ecosystem', controllers.EcosystemController.router); 32 | v1.use('/health', controllers.HealthController.router); 33 | v1.use('/checkin', controllers.CheckInController.router); 34 | v1.use('/rsvp', controllers.RSVPController.router); 35 | v1.use('/announcement', controllers.AnnouncementController.router); 36 | v1.use('/stats', controllers.StatsController.router); 37 | v1.use('/tracking', controllers.TrackingController.router); 38 | v1.use('/mail', controllers.MailController.router); 39 | v1.use('/event', controllers.EventController.router); 40 | v1.use('/recruiter', controllers.RecruiterInterestController.router); 41 | 42 | // logs resolved requests (the request once processed by various middleware) and outgoing responses 43 | v1.use((req, res, next) => { 44 | utils.logs.logRequest(req); 45 | utils.logs.logResponse(req, res); 46 | next(); 47 | }); 48 | 49 | module.exports = v1; 50 | -------------------------------------------------------------------------------- /api/v1/middleware/auth.js: -------------------------------------------------------------------------------- 1 | const AuthService = require('../services/AuthService'); 2 | const ctx = require('ctx'); 3 | const User = require('../models/User'); 4 | const config = ctx.config(); 5 | const errors = require('../errors'); 6 | const roles = require('../utils/roles'); 7 | const _ = require('lodash'); 8 | const logger = ctx.logger(); 9 | 10 | module.exports = (req, res, next) => { 11 | let auth = req.get(config.auth.headers.all); 12 | if(auth) { 13 | auth = _.split(auth, ' ', 2); 14 | } else { 15 | return next(); 16 | } 17 | 18 | if(auth[0] == config.auth.types.bearer) { 19 | return AuthService.getGitHubAccountDetails(auth[1]) 20 | .then((handle) => { 21 | req.auth = true; 22 | return User.findByGitHubHandle(handle); 23 | }) 24 | .then((user) => { 25 | req.user = user; 26 | return next(); 27 | }) 28 | .catch(errors.UnprocessableRequestError, (error) => { 29 | const message = 'The provided token was invalid (' + error.message + ')'; 30 | const source = config.auth.headers.all + ': ' + config.auth.types.bearer; 31 | 32 | return next(new errors.InvalidHeaderError(message, source)); 33 | }); 34 | } else if(auth[0] == config.auth.types.basic) { 35 | return AuthService.verify(auth[1]) 36 | .then((decoded) => { 37 | // specifies that request supplied a valid auth token 38 | // (but not necessarily that the associated user data has been retrieved) 39 | req.auth = true; 40 | return User.findById(decoded.sub); 41 | }) 42 | .then((user) => { 43 | const adminUserOverride = req.get(config.auth.headers.impersonation); 44 | 45 | if (!_.isUndefined(adminUserOverride) && user.hasRole(roles.SUPERUSER)) { 46 | return User.findById(adminUserOverride) 47 | .then((impersonated) => { 48 | if (_.isNull(impersonated) || impersonated.hasRole(roles.SUPERUSER)) { 49 | const message = 'The provided userId was invalid (' + adminUserOverride + ')'; 50 | const source = config.auth.headers.impersonation; 51 | 52 | return next(new errors.InvalidHeaderError(message, source)); 53 | } 54 | 55 | logger.debug('Impersonation: %d %d at %s with %s %s', 56 | user.get('id'), 57 | impersonated.get('id'), 58 | new Date(), 59 | req.method, 60 | req.originalUrl 61 | ); 62 | 63 | req.user = impersonated; 64 | req.originUser = user.get('id'); 65 | return next(); 66 | }); 67 | } 68 | 69 | req.user = user; 70 | return next(); 71 | }) 72 | .catch(errors.UnprocessableRequestError, (error) => { 73 | const message = 'The provided token was invalid (' + error.message + ')'; 74 | const source = config.auth.headers.all + ': ' + config.auth.types.basic; 75 | 76 | return next(new errors.InvalidHeaderError(message, source)); 77 | }); 78 | } 79 | 80 | return next(); 81 | }; 82 | -------------------------------------------------------------------------------- /api/v1/middleware/errors.js: -------------------------------------------------------------------------------- 1 | /* jshint esversion: 6 */ 2 | 3 | const errors = require('../errors'); 4 | const logUtils = require('../utils/logs'); 5 | 6 | module.exports = (err, req, res, next) => { 7 | if (err instanceof Error && err.status === 413) { 8 | // caught a body-parser entity length error 9 | err = new errors.EntityTooLargeError(); 10 | } else if (err instanceof SyntaxError && err.status === 400) { 11 | // caught a body-parser formatting error 12 | // https://github.com/expressjs/body-parser/issues/122 13 | err = new errors.UnprocessableRequestError(); 14 | } 15 | 16 | if (!(err instanceof Error)) { 17 | logUtils.logError(req, err, null, logUtils.errorTypes.UNKNOWN); 18 | err = new errors.ApiError(); 19 | } else if (!err.isApiError) { 20 | logUtils.logError(req, err.stack, null, logUtils.errorTypes.UNCAUGHT); 21 | err = new errors.ApiError(); 22 | } else { 23 | logUtils.logError(req, err.message, err.status, logUtils.errorTypes.CLIENT); 24 | } 25 | 26 | if (res.headersSent) { 27 | return next(); 28 | } 29 | 30 | const response = { 31 | meta: null, 32 | error: err.toJSON() 33 | }; 34 | 35 | return res.status(err.status) 36 | .json(response); 37 | }; 38 | -------------------------------------------------------------------------------- /api/v1/middleware/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | auth: require('./auth.js'), 3 | errors: require('./errors.js'), 4 | permission: require('./permission.js'), 5 | response: require('./response.js'), 6 | request: require('./request.js'), 7 | upload: require('./upload.js'), 8 | ratelimiting: require('./ratelimiting.js') 9 | }; 10 | -------------------------------------------------------------------------------- /api/v1/middleware/permission.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | const errors = require('../errors'); 4 | 5 | module.exports = (allowed, isOwner) => { 6 | if (!_.isArray(allowed)) { 7 | allowed = [ allowed ]; 8 | } 9 | 10 | return (req, res, next) => { 11 | if (!req.auth) { 12 | // there is no auth information, so the requester cannot be allowed 13 | return next(new errors.UnauthorizedError()); 14 | } else if (req.user.hasRoles(allowed)) { 15 | return next(); 16 | } else if (isOwner) { 17 | // the endpoint defined an ownership method 18 | const result = isOwner(req); 19 | 20 | if (typeof result.then === 'function') { 21 | // the ownership method is async, so resolve its promise 22 | result.then((truth) => { 23 | if (truth) { 24 | return next(); 25 | 26 | } 27 | return next(new errors.UnauthorizedError()); 28 | 29 | }) 30 | .catch((error) => next(error)); 31 | } else if (result) { 32 | // the ownership method is synchronous (and succeeded) 33 | return next(); 34 | } else { 35 | // the ownership method is synchronous (but failed) 36 | return next(new errors.UnauthorizedError()); 37 | } 38 | } else { 39 | return next(new errors.UnauthorizedError()); 40 | } 41 | }; 42 | }; 43 | -------------------------------------------------------------------------------- /api/v1/middleware/ratelimiting.js: -------------------------------------------------------------------------------- 1 | const ctx = require('ctx'); 2 | const cache = ctx.cache().instance(); 3 | const config = ctx.config(); 4 | 5 | const BruteLimiter = require('express-brute'); 6 | 7 | const RedisStore = require('express-brute-redis'); 8 | const store = new RedisStore({ 9 | client: cache 10 | }); 11 | 12 | const ratelimiter = new BruteLimiter(store, { 13 | freeRetries: config.limit.count, 14 | attachResetToRequest: false, 15 | refreshTimeoutOnRequest: false, 16 | minWait: config.limit.window * 1000 + 1, // Set delay equal to just over the lifetime, * 1000 to convert to milliseconds 17 | maxWait: config.limit.window * 1000 + 1, // Set delay equal to just over the lifetime, * 1000 to convert to milliseconds 18 | lifetime: config.limit.window 19 | }); 20 | 21 | module.exports = ratelimiter.prevent; 22 | -------------------------------------------------------------------------------- /api/v1/middleware/request.js: -------------------------------------------------------------------------------- 1 | const CheckitError = require('checkit').Error; 2 | 3 | const errorUtils = require('../utils/errors'); 4 | 5 | module.exports = function(Request) { 6 | return (req, res, next) => { 7 | if (!Request) { 8 | return next(); 9 | } 10 | 11 | const request = new Request(req.headers, req.body); 12 | return request.validate() 13 | .then(() => { 14 | req.body = request.body(); 15 | 16 | return next(); 17 | }) 18 | .catch(CheckitError, errorUtils.handleValidationError) 19 | .catch((error) => next(error)); 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /api/v1/middleware/response.js: -------------------------------------------------------------------------------- 1 | module.exports = (req, res, next) => { 2 | if (res.headersSent) { 3 | return next(); 4 | } 5 | 6 | const response = { 7 | meta: (res.meta) ? res.meta : null, 8 | data: (res.body) ? res.body : {} 9 | }; 10 | 11 | res.json(response); 12 | return next(); 13 | }; 14 | -------------------------------------------------------------------------------- /api/v1/middleware/upload.js: -------------------------------------------------------------------------------- 1 | /*jshint esversion: 6 */ 2 | const _ = require('lodash'); 3 | const EntityNotSupportedError = require('../errors/EntityNotSupportedError'); 4 | 5 | const CONTENT_TYPE_MISMATCH = 'The uploaded content type is not allowed'; 6 | 7 | module.exports = (req, res, next) => { 8 | // when the content type does not match, the body parser just leaves the 9 | // request body empty (as opposed to throwing an error) 10 | if (_.isEmpty(req.body)) { 11 | return next(new EntityNotSupportedError(CONTENT_TYPE_MISMATCH)); 12 | } 13 | 14 | return next(); 15 | }; 16 | -------------------------------------------------------------------------------- /api/v1/models/Announcement.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | const Model = require('./Model'); 4 | const Announcement = Model.extend({ 5 | tableName: 'announcements', 6 | idAttribute: 'id', 7 | hasTimestamps: [ 'created' ], 8 | validations: { 9 | 'title': ['required', 'string', 'maxLength:255'], 10 | 'description': ['required', 'string', 'maxLength:1000'] 11 | } 12 | }); 13 | 14 | /** 15 | * Creates a query builder handler for findAll 16 | * @param {Date} before see #findAll 17 | * @param {Date} after see #findAll 18 | * @param {Number} limit see #findAll 19 | * @return {Function} a bookshelf query-builder handler 20 | */ 21 | function _buildFindAllQuery(before, after, limit) { 22 | return (qb) => { 23 | if (!_.isNil(before)) { 24 | qb.where('created', '<', before); 25 | } 26 | if (!_.isNil(after)) { 27 | if (!_.isNil(before)) { 28 | qb.andWhere('created', '>', after); 29 | } else { 30 | qb.where('created', '>', after); 31 | } 32 | } 33 | if (!_.isNil(limit)) { 34 | qb.limit(limit); 35 | } 36 | }; 37 | } 38 | 39 | /** 40 | * Finds all announcements before/after specific dates 41 | * @param {Date} before the latest date to look for announcements (optional) 42 | * @param {Date} after the earliest date to look for announcements (optional) 43 | * @param {Number} limit the maximum number of results to return (optional) 44 | * @return {Promise} a promise resolving to the resulting collection 45 | */ 46 | Announcement.findAll = (before, after, limit) => Announcement.query(_buildFindAllQuery(before, after, limit)) 47 | .orderBy('created', 'DESC') 48 | .fetchAll(); 49 | 50 | module.exports = Announcement; 51 | -------------------------------------------------------------------------------- /api/v1/models/AttendeeExtraInfo.js: -------------------------------------------------------------------------------- 1 | const Model = require('./Model'); 2 | const AttendeeExtraInfo = Model.extend({ 3 | tableName: 'attendee_extra_infos', 4 | idAttribute: 'id', 5 | validations: { 6 | attendeeId: ['required', 'integer'], 7 | info: ['required', 'string', 'maxLength:255'] 8 | } 9 | }); 10 | 11 | /** 12 | * Finds an attendee's extra information by its relational attendee's id 13 | * @param {Number|String} id the ID of the attendee with the appropriate type 14 | * @return {Promise} a Promise resolving to the resulting AttendeeExtraInfo or null 15 | */ 16 | AttendeeExtraInfo.findByAttendeeId = (attendeeId) => AttendeeExtraInfo.where({ 17 | attendee_id: attendeeId 18 | }) 19 | .fetch(); 20 | 21 | /** 22 | * Finds an attendee's extra information by its ID 23 | * @param {Number|String} id the ID of the model with the appropriate type 24 | * @return {Promise} a Promise resolving to the resulting model or null 25 | */ 26 | AttendeeExtraInfo.findById = (id) => AttendeeExtraInfo.where({ 27 | id: id 28 | }) 29 | .fetch(); 30 | 31 | module.exports = AttendeeExtraInfo; 32 | -------------------------------------------------------------------------------- /api/v1/models/AttendeeLongForm.js: -------------------------------------------------------------------------------- 1 | const Model = require('./Model'); 2 | const AttendeeLongForm = Model.extend({ 3 | tableName: 'attendee_long_form', 4 | idAttribute: 'id', 5 | validations: { 6 | attendeeId: ['required', 'integer'], 7 | info: ['required', 'string', 'maxLength:16383'] 8 | } 9 | }); 10 | 11 | /** 12 | * Finds an attendee's extra information by its relational attendee's id 13 | * @param {Number|String} id the ID of the attendee with the appropriate type 14 | * @return {Promise} a Promise resolving to the resulting AttendeeLongForm or null 15 | */ 16 | AttendeeLongForm.findByAttendeeId = (attendeeId) => AttendeeLongForm.where({ 17 | attendee_id: attendeeId 18 | }) 19 | .fetch(); 20 | 21 | /** 22 | * Finds an attendee's extra information by its ID 23 | * @param {Number|String} id the ID of the model with the appropriate type 24 | * @return {Promise} a Promise resolving to the resulting model or null 25 | */ 26 | AttendeeLongForm.findById = (id) => AttendeeLongForm.where({ 27 | id: id 28 | }) 29 | .fetch(); 30 | 31 | module.exports = AttendeeLongForm; 32 | -------------------------------------------------------------------------------- /api/v1/models/AttendeeOSContributor.js: -------------------------------------------------------------------------------- 1 | const Model = require('./Model'); 2 | const AttendeeOSContributor = Model.extend({ 3 | tableName: 'attendee_os_contributor', 4 | idAttribute: 'id', 5 | validations: { 6 | attendeeId: ['required', 'integer'], 7 | osContributor: ['required', 'string', 'maxLength:255'] 8 | } 9 | }); 10 | 11 | AttendeeOSContributor.findByAttendeeId = (attendeeId) => AttendeeOSContributor.where({ 12 | attendee_id: attendeeId 13 | }) 14 | .fetch(); 15 | 16 | AttendeeOSContributor.findById = (id) => AttendeeOSContributor.where({ 17 | id: id 18 | }) 19 | .fetch(); 20 | 21 | module.exports = AttendeeOSContributor; 22 | -------------------------------------------------------------------------------- /api/v1/models/AttendeeRSVP.js: -------------------------------------------------------------------------------- 1 | const CheckIt = require('checkit'); 2 | 3 | const Model = require('./Model'); 4 | const AttendeeRSVP = Model.extend({ 5 | tableName: 'attendee_rsvps', 6 | idAttribute: 'id', 7 | validations: { 8 | attendeeId: ['required', 'integer'], 9 | isAttending: ['required', 'boolean'] 10 | } 11 | }); 12 | 13 | AttendeeRSVP.findByAttendeeId = function(attendeeId) { 14 | return AttendeeRSVP.where({ 15 | attendee_id: attendeeId 16 | }) 17 | .fetch(); 18 | }; 19 | 20 | AttendeeRSVP.prototype.validate = function() { 21 | const checkit = CheckIt(this.validations); 22 | return checkit.run(this.attributes); 23 | }; 24 | 25 | module.exports = AttendeeRSVP; 26 | -------------------------------------------------------------------------------- /api/v1/models/AttendeeRequestedCollaborator.js: -------------------------------------------------------------------------------- 1 | const Model = require('./Model'); 2 | const AttendeeRequestedCollaborator = Model.extend({ 3 | tableName: 'attendee_requested_collaborators', 4 | idAttribute: 'id', 5 | validations: { 6 | attendeeId: ['required', 'integer'], 7 | collaborator: ['required', 'string', 'maxLength:255'] 8 | } 9 | }); 10 | 11 | 12 | /** 13 | * Finds an attendee's requested collaborator by its relational attendee's id 14 | * @param {Number|String} id the ID of the attendee with the appropriate type 15 | * @return {Promise} a Promise resolving to the resulting AttendeeRequestedCollaborator or null 16 | */ 17 | AttendeeRequestedCollaborator.findByAttendeeId = function(attendeeId) { 18 | return AttendeeRequestedCollaborator.where({ 19 | attendee_id: attendeeId 20 | }) 21 | .fetch(); 22 | }; 23 | 24 | /** 25 | * Finds an attendee's requested collaborator by its ID 26 | * @param {Number|String} id the ID of the model with the appropriate type 27 | * @return {Promise} a Promise resolving to the resulting model or null 28 | */ 29 | AttendeeRequestedCollaborator.findById = function(id) { 30 | return AttendeeRequestedCollaborator.where({ 31 | id: id 32 | }) 33 | .fetch(); 34 | }; 35 | 36 | module.exports = AttendeeRequestedCollaborator; 37 | -------------------------------------------------------------------------------- /api/v1/models/CheckIn.js: -------------------------------------------------------------------------------- 1 | const Model = require('./Model'); 2 | const validators = require('../utils/validators'); 3 | 4 | const LOCATIONS = ['NONE', 'ECEB', 'SIEBEL', 'DCL']; 5 | const CheckIn = Model.extend({ 6 | tableName: 'checkins', 7 | idAttribute: 'id', 8 | validations: { 9 | userId: ['required', 'integer'], 10 | location: ['required', 'string', validators.in(LOCATIONS)], 11 | swag: ['required', 'boolean'] 12 | }, 13 | parse: function(attrs) { 14 | attrs = Model.prototype.parse(attrs); 15 | if (Number.isInteger(attrs.swag)) { 16 | attrs.swag = !!attrs.swag; 17 | } 18 | return attrs; 19 | } 20 | }); 21 | 22 | CheckIn.findByUserId = function(id) { 23 | return CheckIn.where({ 24 | user_id: id 25 | }) 26 | .fetch(); 27 | }; 28 | 29 | 30 | module.exports = CheckIn; 31 | -------------------------------------------------------------------------------- /api/v1/models/Ecosystem.js: -------------------------------------------------------------------------------- 1 | const Model = require('./Model'); 2 | 3 | const Ecosystem = Model.extend({ 4 | tableName: 'ecosystems', 5 | idAttribute: 'id', 6 | validations: { 7 | name: ['required', 'string', 'maxLength:100'] 8 | } 9 | }); 10 | 11 | Ecosystem.findByName = function(name) { 12 | name = name.toLowerCase(); 13 | return Ecosystem.where({ 14 | name: name 15 | }) 16 | .fetch(); 17 | }; 18 | 19 | module.exports = Ecosystem; 20 | -------------------------------------------------------------------------------- /api/v1/models/Event.js: -------------------------------------------------------------------------------- 1 | const Model = require('./Model'); 2 | const validators = require('../utils/validators'); 3 | 4 | const TAGS = ['PRE_EVENT', 'POST_EVENT']; 5 | const EventLocation = require('./EventLocation'); 6 | const Event = Model.extend({ 7 | tableName: 'events', 8 | idAttribute: 'id', 9 | validations: { 10 | name: ['required', 'string', 'maxLength:255'], 11 | description: ['required', 'string', 'maxLength:2047'], 12 | startTime: ['required', validators.date], 13 | endTime: ['required', validators.date], 14 | tag: ['required', 'string', validators.in(TAGS)] 15 | }, 16 | locations: function() { 17 | return this.hasMany(EventLocation); 18 | } 19 | }); 20 | 21 | Event.findById = function(id) { 22 | return Event.where({ 23 | id: id 24 | }) 25 | .fetch({ 26 | withRelated: [ 'locations' ] 27 | }); 28 | }; 29 | 30 | Event.findByName = function(name) { 31 | return Event.where({ 32 | name: name 33 | }) 34 | .fetch({ 35 | withRelated: [ 'locations' ] 36 | }); 37 | }; 38 | 39 | module.exports = Event; 40 | -------------------------------------------------------------------------------- /api/v1/models/EventFavorite.js: -------------------------------------------------------------------------------- 1 | const Model = require('./Model'); 2 | 3 | const EventFavorite = Model.extend({ 4 | tableName: 'event_favorites', 5 | idAttribute: 'id', 6 | validations: { 7 | userId: ['required', 'integer'], 8 | eventId: ['required', 'integer'] 9 | } 10 | }); 11 | 12 | EventFavorite.findById = function(id) { 13 | return EventFavorite.where({ 14 | id: id 15 | }) 16 | .fetch(); 17 | }; 18 | 19 | EventFavorite.findByUserId = function(user_id) { 20 | return EventFavorite.where({ 21 | user_id: user_id 22 | }) 23 | .fetchAll(); 24 | }; 25 | 26 | EventFavorite.findByUserFavoriteEvent = function(user_id, event_id) { 27 | return EventFavorite.where({ 28 | user_id: user_id, 29 | event_id: event_id 30 | }) 31 | .fetch(); 32 | }; 33 | 34 | module.exports = EventFavorite; 35 | -------------------------------------------------------------------------------- /api/v1/models/EventLocation.js: -------------------------------------------------------------------------------- 1 | const Model = require('./Model'); 2 | const EventLocation = Model.extend({ 3 | tableName: 'event_locations', 4 | idAttribute: 'id', 5 | validations: { 6 | eventId: ['required', 'integer'], 7 | locationId: ['required', 'integer'] 8 | } 9 | }); 10 | 11 | EventLocation.eventId = function(eventId) { 12 | return EventLocation.where({ 13 | event_id: eventId 14 | }) 15 | .fetch(); 16 | }; 17 | 18 | module.exports = EventLocation; 19 | -------------------------------------------------------------------------------- /api/v1/models/Location.js: -------------------------------------------------------------------------------- 1 | const Model = require('./Model'); 2 | 3 | const Location = Model.extend({ 4 | tableName: 'locations', 5 | idAttribute: 'id', 6 | validations: { 7 | name: ['required', 'string', 'maxLength:255'], 8 | latitude: ['required', 'number'], 9 | longitude: ['required', 'number'] 10 | } 11 | }); 12 | 13 | module.exports = Location; 14 | -------------------------------------------------------------------------------- /api/v1/models/MailingList.js: -------------------------------------------------------------------------------- 1 | const _Promise = require('bluebird'); 2 | 3 | const Model = require('./Model'); 4 | const User = require('./User'); 5 | const MailingListUser = require('./MailingListUser'); 6 | 7 | const MailingList = Model.extend({ 8 | tableName: 'mailing_lists', 9 | idAttribute: 'id', 10 | validations: { 11 | sent: [ 'boolean' ] 12 | } 13 | }); 14 | 15 | /** 16 | * Finds a list by its name 17 | * @param {String} name a list's name 18 | * @return {Promise} the desired mailing list, or null 19 | */ 20 | MailingList.findByName = function(name) { 21 | return this.collection() 22 | .query({ 23 | where: { 24 | name: name 25 | } 26 | }) 27 | .fetchOne(); 28 | }; 29 | 30 | /** 31 | * Adds a user to this list, if it is not already present 32 | * @param {User} user the user to add 33 | * @returns {Promise} an promise with the save result 34 | */ 35 | MailingList.prototype.addUser = function(user) { 36 | const mailingListUser = MailingListUser.forge({ 37 | user_id: user.id, 38 | mailing_list_id: this.attributes.id 39 | }); 40 | return MailingListUser 41 | .transaction((t) => mailingListUser 42 | .fetch({ 43 | transacting: t 44 | }) 45 | .then((result) => { 46 | if (result) { 47 | return _Promise.resolve(result); 48 | } 49 | return mailingListUser.save(null, { 50 | transacting: t 51 | }); 52 | })); 53 | }; 54 | 55 | /** 56 | * Removes a user from this list, if it is present 57 | * @param {User} user the user to remove from the list 58 | * @return {Promise} a promise with the deleted result 59 | */ 60 | MailingList.prototype.removeUser = function(user) { 61 | return MailingListUser 62 | .where({ 63 | user_id: user.id, 64 | mailing_list_id: this.attributes.id 65 | }) 66 | .destroy(); 67 | }; 68 | 69 | /** 70 | * Determines the members of this list 71 | * @return {Promise} the Users that are on this list 72 | */ 73 | MailingList.prototype.members = function() { 74 | return this.belongsToMany(User) 75 | .through(MailingListUser) 76 | .fetch(); 77 | }; 78 | 79 | module.exports = MailingList; 80 | -------------------------------------------------------------------------------- /api/v1/models/MailingListUser.js: -------------------------------------------------------------------------------- 1 | const Model = require('./Model'); 2 | const MailingListUser = Model.extend({ 3 | tableName: 'mailing_lists_users', 4 | idAttribute: 'id' 5 | }); 6 | 7 | module.exports = MailingListUser; 8 | -------------------------------------------------------------------------------- /api/v1/models/Mentor.js: -------------------------------------------------------------------------------- 1 | const validators = require('../utils/validators'); 2 | 3 | const TSHIRT_SIZES = ['S', 'M', 'L', 'XL']; 4 | const STATUSES = ['ACCEPTED', 'WAITLISTED', 'REJECTED', 'PENDING']; 5 | 6 | const Model = require('./Model'); 7 | const MentorProjectIdea = require('./MentorProjectIdea'); 8 | const Mentor = Model.extend({ 9 | tableName: 'mentors', 10 | idAttribute: 'id', 11 | validations: { 12 | firstName: ['required', 'string', 'maxLength:255'], 13 | lastName: ['required', 'string', 'maxLength:255'], 14 | shirtSize: ['required', 'string', validators.in(TSHIRT_SIZES)], 15 | status: ['string', validators.in(STATUSES)], 16 | github: ['string', 'maxLength:50'], 17 | location: ['required', 'string', 'maxLength:255'], 18 | summary: ['required', 'string', 'maxLength:255'], 19 | occupation: ['required', 'string', 'maxLength:255'], 20 | userId: ['required', 'integer'] 21 | }, 22 | ideas: function() { 23 | return this.hasMany(MentorProjectIdea); 24 | } 25 | }); 26 | 27 | 28 | /** 29 | * Finds a mentor by its relational user's id, joining in its related project ideas 30 | * @param {Number|String} id the ID of the user with the appropriate type 31 | * @return {Promise} a Promise resolving to the resulting mentor or null 32 | */ 33 | Mentor.findByUserId = function(userId) { 34 | return Mentor.where({ 35 | user_id: userId 36 | }) 37 | .fetch({ 38 | withRelated: [ 'ideas' ] 39 | }); 40 | }; 41 | 42 | /** 43 | * Finds a mentor by its ID, joining in its related project ideas 44 | * @param {Number|String} id the ID of the model with the appropriate type 45 | * @return {Promise} a Promise resolving to the resulting model or null 46 | */ 47 | Mentor.findById = function(id) { 48 | return Mentor.where({ 49 | id: id 50 | }) 51 | .fetch({ 52 | withRelated: [ 'ideas' ] 53 | }); 54 | }; 55 | 56 | module.exports = Mentor; 57 | -------------------------------------------------------------------------------- /api/v1/models/MentorProjectIdea.js: -------------------------------------------------------------------------------- 1 | const Model = require('./Model'); 2 | const MentorProjectIdea = Model.extend({ 3 | tableName: 'mentor_project_ideas', 4 | idAttribute: 'id', 5 | validations: { 6 | link: ['required', 'url', 'maxLength:255'], 7 | contributions: ['required', 'string', 'maxLength:255'], 8 | ideas: ['required', 'string', 'maxLength:255'], 9 | mentorId: ['required', 'integer'] 10 | } 11 | }); 12 | 13 | module.exports = MentorProjectIdea; 14 | -------------------------------------------------------------------------------- /api/v1/models/Model.js: -------------------------------------------------------------------------------- 1 | const checkit = require('checkit'); 2 | 3 | const database = require('../../database'); 4 | const databaseUtils = require('../utils/database'); 5 | const bookshelf = database.instance(); 6 | 7 | /** 8 | * Produces datastore transaction 9 | * @param {Function} callback method to start transaction 10 | * @return {Promise} the result of the callback 11 | */ 12 | function _transaction(callback) { 13 | return bookshelf.transaction(callback); 14 | } 15 | 16 | /** 17 | * Fetches a model by its ID 18 | * @param {Number|String} id the ID of the model with the appropriate type 19 | * @return {Promise} a Promise resolving to the resulting model or null 20 | */ 21 | function _findById(id) { 22 | const _model = new this(); 23 | 24 | const queryParams = {}; 25 | queryParams[_model.idAttribute] = id; 26 | return _model.query({ 27 | where: queryParams 28 | }) 29 | .fetch(); 30 | } 31 | 32 | const Model = bookshelf.Model.extend({ 33 | // the default model has no validations, but more can be 34 | // added as desired 35 | validations: {} 36 | }, { 37 | transaction: _transaction, 38 | findById: _findById 39 | }); 40 | 41 | /** 42 | * Initializes the model by setting up all event handlers 43 | */ 44 | Model.prototype.initialize = function() { 45 | this.on('saving', this.validate); 46 | }; 47 | 48 | /** 49 | * Ensures keys being inserted into the datastore have the correct format 50 | * @param {Object} attrs the attributes to transform 51 | * @return {Object} the transformed attributes (underscored) 52 | */ 53 | Model.prototype.format = function(attrs) { 54 | return databaseUtils.format(attrs); 55 | }; 56 | 57 | /** 58 | * Ensures keys being retrieved from the datastore have the correct format 59 | * @param {Object} attrs the attributes to transform 60 | * @return {Object} the transformed attributes (camel-cased) 61 | */ 62 | Model.prototype.parse = function(attrs) { 63 | return databaseUtils.parse(attrs); 64 | }; 65 | 66 | /** 67 | * Validates the attributes of this model based on the assigned validations 68 | * @return {Promise} resolving to the validity of the attributes, as decided by 69 | * the Checkit library 70 | */ 71 | Model.prototype.validate = function() { 72 | return checkit(this.validations) 73 | .run(this.attributes); 74 | }; 75 | 76 | module.exports = Model; 77 | -------------------------------------------------------------------------------- /api/v1/models/NetworkCredential.js: -------------------------------------------------------------------------------- 1 | const Model = require('./Model'); 2 | const NetworkCredential = Model.extend({ 3 | tableName: 'network_credentials', 4 | idAttribute: 'id', 5 | validations: { 6 | userId: [ 'integer' ], 7 | account: ['required', 'string', 'maxLength:25'], 8 | password: ['required', 'string', 'maxLength:25'], 9 | assigned: ['required', 'boolean'] 10 | } 11 | }); 12 | 13 | 14 | /** 15 | * Finds a network credential by its relational user's id 16 | * @param {Number|String} userId the ID of the attendee's relational user 17 | * @return {Promise} a Promise resolving to the resulting NetworkCredential or null 18 | */ 19 | NetworkCredential.findByUserId = function(userId) { 20 | return NetworkCredential.where({ 21 | user_id: userId 22 | }) 23 | .fetch(); 24 | }; 25 | /** 26 | * Finds a network credential by its ID 27 | * @param {Number|String} id the ID of the model with the appropriate type 28 | * @return {Promise} a Promise resolving to the resulting model or null 29 | */ 30 | NetworkCredential.findById = function(id) { 31 | return NetworkCredential.where({ 32 | id: id 33 | }) 34 | .fetch(); 35 | }; 36 | 37 | /** 38 | * Finds an unassigned network credential 39 | * @param {Number|String} id the ID of the model with the appropriate type 40 | * @return {Promise} a Promise resolving to the resulting model or null 41 | */ 42 | NetworkCredential.findUnassigned = function() { 43 | return NetworkCredential.where({ 44 | assigned: 0 45 | }) 46 | .fetch(); 47 | }; 48 | 49 | module.exports = NetworkCredential; 50 | -------------------------------------------------------------------------------- /api/v1/models/Project.js: -------------------------------------------------------------------------------- 1 | const Model = require('./Model'); 2 | 3 | const Project = Model.extend({ 4 | tableName: 'projects', 5 | idAttribute: 'id', 6 | validations: { 7 | name: ['required', 'string', 'maxLength:100'], 8 | description: ['required', 'string', 'maxLength:255'], 9 | repo: ['required', 'string', 'maxLength:255'], 10 | isPublished: [ 'boolean' ] 11 | } 12 | }); 13 | 14 | Project.findByName = function(name) { 15 | name = name.toLowerCase(); 16 | return Project.where({ 17 | name: name 18 | }) 19 | .fetch(); 20 | }; 21 | 22 | /** 23 | * Finds an project by its ID 24 | * @param {Number|String} id the ID of the model with the appropriate type 25 | * @return {Promise} a Promise resolving to the resulting model or null 26 | */ 27 | Project.findById = function(id) { 28 | return Project.where({ 29 | id: id 30 | }) 31 | .fetch(); 32 | }; 33 | 34 | module.exports = Project; 35 | -------------------------------------------------------------------------------- /api/v1/models/ProjectMentor.js: -------------------------------------------------------------------------------- 1 | const Model = require('./Model'); 2 | const Project = require('./Project'); 3 | const Mentor = require('./Mentor'); 4 | 5 | const ProjectMentor = Model.extend({ 6 | tableName: 'project_mentors', 7 | idAttribute: 'id', 8 | project: function() { 9 | return this.belongsTo(Project, 'project_id'); 10 | }, 11 | mentor: function() { 12 | return this.belongsTo(Mentor, 'mentor_id'); 13 | } 14 | }); 15 | 16 | ProjectMentor.findByProjectAndMentorId = function(project_id, mentor_id) { 17 | return ProjectMentor.where({ 18 | project_id: project_id, 19 | mentor_id: mentor_id 20 | }) 21 | .fetch(); 22 | }; 23 | 24 | module.exports = ProjectMentor; 25 | -------------------------------------------------------------------------------- /api/v1/models/RecruiterInterest.js: -------------------------------------------------------------------------------- 1 | const Model = require('./Model'); 2 | 3 | const RecruiterInterest = Model.extend({ 4 | tableName: 'recruiter_interests', 5 | hasTimestamps: ['created', 'updated'], 6 | idAttribute: 'app_id', 7 | validations: { 8 | recruiterId: ['required', 'integer'], 9 | attendeeId: ['required', 'integer'], 10 | comments: [ 'string' ], 11 | favorite: [ 'boolean' ] 12 | } 13 | }); 14 | 15 | RecruiterInterest.findById = (id) => RecruiterInterest.where({ 16 | app_id: id 17 | }) 18 | .fetch(); 19 | 20 | RecruiterInterest.findByRecruiterId = (id) => RecruiterInterest.where({ 21 | recruiter_id: id 22 | }) 23 | .fetchAll(); 24 | 25 | RecruiterInterest.findByAttendeeId = (id) => RecruiterInterest.where({ 26 | attendee_id: id 27 | }) 28 | .fetchAll(); 29 | 30 | RecruiterInterest.updateInterest = (appId, comments, favorite) => RecruiterInterest 31 | .where({app_id: appId}) 32 | .fetch() 33 | .then((result) => { 34 | result.set({ comments: comments, favorite: favorite }); 35 | return result.save({ app_id: appId }, { method: 'update'}); 36 | }); 37 | 38 | module.exports = RecruiterInterest; 39 | -------------------------------------------------------------------------------- /api/v1/models/Token.js: -------------------------------------------------------------------------------- 1 | const Model = require('./Model'); 2 | const User = require('./User'); 3 | 4 | const Token = Model.extend({ 5 | tableName: 'tokens', 6 | idAttribute: 'id', 7 | hasTimestamps: [ 'created' ], 8 | user: function() { 9 | return this.belongsTo(User, 'user_id'); 10 | }, 11 | validations: { 12 | value: ['required', 'string'] 13 | } 14 | }); 15 | 16 | /** 17 | * Finds a token given the Token value 18 | * @param {String} value The Token's value 19 | * @return {Promise} resolving to the associated Token Model 20 | */ 21 | Token.findByValue = function(value) { 22 | return this.collection() 23 | .query({ 24 | where: { 25 | value: value 26 | } 27 | }) 28 | .fetchOne({ 29 | withRelated: [ 'user' ] 30 | }); 31 | }; 32 | 33 | module.exports = Token; 34 | -------------------------------------------------------------------------------- /api/v1/models/TrackingEvent.js: -------------------------------------------------------------------------------- 1 | const Model = require('./Model'); 2 | 3 | const TrackingEvent = Model.extend({ 4 | tableName: 'tracking_events', 5 | idAttribute: 'id', 6 | hasTimestamps: [ 'created' ], 7 | validations: { 8 | name: ['required', 'string', 'maxLength:255'], 9 | duration: ['required', 'naturalNonZero'] 10 | } 11 | }); 12 | 13 | TrackingEvent.findByName = function(searchName) { 14 | return TrackingEvent.where({ 15 | name: searchName 16 | }) 17 | .fetch(); 18 | }; 19 | 20 | module.exports = TrackingEvent; 21 | -------------------------------------------------------------------------------- /api/v1/models/Upload.js: -------------------------------------------------------------------------------- 1 | const Model = require('./Model'); 2 | const Upload = Model.extend({ 3 | tableName: 'uploads', 4 | idAttribute: 'id', 5 | hasTimestamps: ['created', 'updated'] 6 | }); 7 | 8 | /** 9 | * Finds all uploads belonging to a given user 10 | * @param {User} owner the owner of the uploads 11 | * @param {String} bucket (optional) the bucket in which to search 12 | * @return {Promise>} all of the uploads belonging to the owner 13 | */ 14 | Upload.findByOwner = function(owner, bucket) { 15 | const queryParams = { 16 | where: {} 17 | }; 18 | 19 | queryParams.where.owner_id = owner.get('id'); 20 | if (bucket) { 21 | queryParams.where.bucket = bucket; 22 | } 23 | 24 | return Upload.collection() 25 | .query(queryParams) 26 | .fetch(); 27 | }; 28 | 29 | /** 30 | * Finds the upload with the associated key in the specified bucket 31 | * @param {String} key the key associated with the upload 32 | * @param {String} bucket the bucket in which to look 33 | * @return {Promise} the requested upload, if it exists 34 | */ 35 | Upload.findByKey = function(key, bucket) { 36 | return Upload.collection() 37 | .query({ 38 | where: { 39 | key: key, 40 | bucket: bucket 41 | } 42 | }) 43 | .fetchOne(); 44 | }; 45 | 46 | module.exports = Upload; 47 | -------------------------------------------------------------------------------- /api/v1/models/UserRole.js: -------------------------------------------------------------------------------- 1 | const _Promise = require('bluebird'); 2 | const _ = require('lodash'); 3 | 4 | const roles = require('../utils/roles'); 5 | 6 | const Model = require('./Model'); 7 | const UserRole = Model.extend({ 8 | tableName: 'user_roles', 9 | idAttribute: 'id', 10 | validations: { 11 | role: ['required', 'string', roles.verifyRole] 12 | } 13 | }); 14 | /** 15 | * Saves a forged user role using the passed transaction 16 | */ 17 | function _addRole(userRole, active, t) { 18 | return userRole 19 | .fetch({ 20 | transacting: t 21 | }) 22 | .then((result) => { 23 | if (result) { 24 | return _Promise.resolve(result); 25 | } 26 | userRole.set({ 27 | active: (_.isUndefined(active) || active) 28 | }); 29 | return userRole.save(null, { 30 | transacting: t 31 | }); 32 | }); 33 | } 34 | 35 | /** 36 | * Adds a role to the specified user. If the role already exists, it is returned 37 | * unmodified 38 | * @param {User} user the target user 39 | * @param {String} role the string representation of the role from utils.roles 40 | * @param {Boolean} active whether or not the role should be activated (defaults to true) 41 | * @param {Transaction} t pending transaction (optional) 42 | * @returns {Promise} the result of the addititon 43 | */ 44 | UserRole.addRole = function(user, role, active, t) { 45 | const userRole = UserRole.forge({ 46 | user_id: user.id, 47 | role: role 48 | }); 49 | if (t) { 50 | return _addRole(userRole, active, t); 51 | } 52 | return UserRole.transaction((t) => _addRole(userRole, active, t)); 53 | }; 54 | 55 | /** 56 | * Sets the activation state of the role. If the userRole.active and active 57 | * fields are the same, the role is returned unmodified 58 | * @param {UserRole} userRole the role to modify 59 | * @param {Boolean} active whether or not the role should be active 60 | * @param {Transaction} t optional pending transaction 61 | * @returns {Promise} the updated role 62 | */ 63 | UserRole.setActive = function(userRole, active, t) { 64 | if (userRole.get('active') == active) { 65 | return _Promise.resolve(userRole); 66 | } 67 | return userRole.set({ 68 | active: active 69 | }) 70 | .save(null, { 71 | transacting: t 72 | }); 73 | }; 74 | 75 | UserRole.prototype.serialize = function() { 76 | return _.omit(this.attributes, ['id', 'userId']); 77 | }; 78 | 79 | module.exports = UserRole; 80 | -------------------------------------------------------------------------------- /api/v1/models/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Attendee: require('./Attendee'), 3 | AttendeeLongForm: require('./AttendeeLongForm'), 4 | AttendeeRequestedCollaborator: require('./AttendeeRequestedCollaborator'), 5 | AttendeeRSVP: require('./AttendeeRSVP'), 6 | CheckIn: require('./CheckIn'), 7 | Project: require('./Project'), 8 | Ecosystem: require('./Ecosystem'), 9 | Event: require('./Event'), 10 | EventLocation: require('./EventLocation'), 11 | Location: require('./Location'), 12 | MailingList: require('./MailingList'), 13 | MailingListUser: require('./MailingListUser'), 14 | User: require('./User'), 15 | UserRole: require('./UserRole'), 16 | RecruiterInterest: require('./RecruiterInterest'), 17 | Token: require('./Token'), 18 | Mentor: require('./Mentor'), 19 | MentorProjectIdea: require('./MentorProjectIdea'), 20 | UniversalTrackingItem: require('./TrackingEvent'), 21 | NetworkCredential: require('./NetworkCredential') 22 | }; 23 | -------------------------------------------------------------------------------- /api/v1/requests/AccreditedUserCreationRequest.js: -------------------------------------------------------------------------------- 1 | const roles = require('../utils/roles'); 2 | const Request = require('./Request'); 3 | 4 | const bodyRequired = ['email', 'role']; 5 | const bodyValidations = { 6 | 'email': [ 'email' ], 7 | 'role': ['string', roles.verifyRole] 8 | }; 9 | 10 | function AccreditedUserCreationRequest(headers, body) { 11 | Request.call(this, headers, body); 12 | 13 | this.bodyRequired = bodyRequired; 14 | this.bodyValidations = bodyValidations; 15 | } 16 | 17 | AccreditedUserCreationRequest.prototype = Object.create(Request.prototype); 18 | AccreditedUserCreationRequest.prototype.constructor = AccreditedUserCreationRequest; 19 | 20 | module.exports = AccreditedUserCreationRequest; 21 | -------------------------------------------------------------------------------- /api/v1/requests/AnnouncementRequest.js: -------------------------------------------------------------------------------- 1 | const Request = require('./Request'); 2 | 3 | const bodyRequired = ['title', 'description']; 4 | const bodyValidations = { 5 | 'title': ['required', 'string', 'maxLength:255'], 6 | 'description': ['required', 'string', 'maxLength:1000'] 7 | }; 8 | 9 | function AnnouncementRequest(headers, body) { 10 | Request.call(this, headers, body); 11 | 12 | this.bodyRequired = bodyRequired; 13 | this.bodyValidations = bodyValidations; 14 | } 15 | 16 | AnnouncementRequest.prototype = Object.create(Request.prototype); 17 | AnnouncementRequest.prototype.constructor = AnnouncementRequest; 18 | 19 | module.exports = AnnouncementRequest; 20 | -------------------------------------------------------------------------------- /api/v1/requests/AttendeeDecisionRequest.js: -------------------------------------------------------------------------------- 1 | const Request = require('./Request'); 2 | const Attendee = require('../models/Attendee'); 3 | 4 | const bodyRequired = ['priority', 'wave', 'status']; 5 | const attendee = new Attendee(); 6 | const bodyValidations = { 7 | 'priority': ['required', 'integer', 'max:10'], 8 | 'wave': attendee.validations.wave.concat([ 'required' ]), 9 | 'status': attendee.validations.status.concat([ 'required' ]) 10 | }; 11 | 12 | function AttendeeDecisionRequest(headers, body) { 13 | Request.call(this, headers, body); 14 | 15 | this.bodyRequired = bodyRequired; 16 | this.bodyValidations = bodyValidations; 17 | } 18 | 19 | AttendeeDecisionRequest.prototype = Object.create(Request.prototype); 20 | AttendeeDecisionRequest.prototype.constructor = AttendeeDecisionRequest; 21 | 22 | module.exports = AttendeeDecisionRequest; 23 | -------------------------------------------------------------------------------- /api/v1/requests/AttendeeRequest.js: -------------------------------------------------------------------------------- 1 | const Request = require('./Request'); 2 | const Attendee = require('../models/Attendee'); 3 | const validators = require('../utils/validators'); 4 | 5 | const longFormValidations = { 6 | info: ['string', 'maxLength:16383'] 7 | }; 8 | 9 | const requestedCollaboratorValidations = { 10 | collaborator: ['required', 'string', 'maxLength:255'] 11 | }; 12 | 13 | const extraInfoValidations = { 14 | info: ['string', 'maxLength:255'] 15 | }; 16 | 17 | const osContributorValidations = { 18 | osContributor: ['string', 'maxLength:255'] 19 | }; 20 | 21 | const bodyRequired = ['attendee.firstName', 'attendee.lastName', 'attendee.shirtSize', 'attendee.diet', 'attendee.age', 'attendee.graduationYear', 'attendee.transportation', 'attendee.school', 'attendee.major', 'attendee.gender', 'attendee.professionalInterest', 'attendee.github', 'attendee.linkedin', 'attendee.isNovice', 'attendee.isPrivate', 'osContributors']; 22 | const bodyAllowed = ['attendee.interests', 'attendee.hasLightningInterest', 'attendee.phoneNumber', 'longForm', 'extraInfo', 'collaborators']; 23 | const attendee = new Attendee(); 24 | const bodyValidations = { 25 | 'attendee': ['required', 'plainObject'], 26 | 'attendee.firstName': attendee.validations.firstName, 27 | 'attendee.lastName': attendee.validations.lastName, 28 | 'attendee.shirtSize': attendee.validations.shirtSize, 29 | 'attendee.diet': attendee.validations.diet, 30 | 'attendee.age': attendee.validations.age, 31 | 'attendee.graduationYear': attendee.validations.graduationYear, 32 | 'attendee.transportation': attendee.validations.transportation, 33 | 'attendee.school': attendee.validations.school, 34 | 'attendee.major': attendee.validations.major, 35 | 'attendee.gender': attendee.validations.gender, 36 | 'attendee.professionalInterest': attendee.validations.professionalInterest, 37 | 'attendee.github': attendee.validations.github, 38 | 'attendee.linkedin': attendee.validations.linkedin, 39 | 'attendee.interests': attendee.validations.interests, 40 | 'attendee.isNovice': attendee.validations.isNovice, 41 | 'attendee.isPrivate': attendee.validations.isPrivate, 42 | 'attendee.hasLightningInterest': [ 'boolean' ], 43 | 'attendee.phoneNumber': attendee.validations.phoneNumber, 44 | 'longForm': ['array', 'maxLength:1', validators.array(validators.nested(longFormValidations, 'longForm'), 'longForm')], 45 | 'collaborators': ['array', 'maxLength:8', validators.array(validators.nested(requestedCollaboratorValidations, 'collaborators'))], 46 | 'extraInfo': ['array', 'maxLength:2', validators.array(validators.nested(extraInfoValidations, 'extraInfo'), 'extraInfo')], 47 | 'oscontributors': ['array', 'maxLength:8', validators.array(validators.nested(osContributorValidations, 'osContributors'), 'osContributors')] 48 | }; 49 | 50 | function AttendeeRequest(headers, body) { 51 | Request.call(this, headers, body); 52 | 53 | this.bodyRequired = bodyRequired; 54 | this.bodyAllowed = bodyAllowed; 55 | this.bodyValidations = bodyValidations; 56 | } 57 | 58 | AttendeeRequest._longFormValidations = longFormValidations; 59 | AttendeeRequest._requestedCollaboratorValidations = requestedCollaboratorValidations; 60 | AttendeeRequest._extraInfoValidations = extraInfoValidations; 61 | AttendeeRequest._osContributorValidations = osContributorValidations; 62 | 63 | 64 | AttendeeRequest.prototype = Object.create(Request.prototype); 65 | AttendeeRequest.prototype.constructor = AttendeeRequest; 66 | 67 | module.exports = AttendeeRequest; 68 | -------------------------------------------------------------------------------- /api/v1/requests/BasicAuthRequest.js: -------------------------------------------------------------------------------- 1 | const Request = require('./Request'); 2 | 3 | const bodyRequired = ['email', 'password']; 4 | const bodyValidations = { 5 | 'email': ['required', 'email'], 6 | 'password': ['required', 'string', 'minLength:8', 'maxLength:50'] 7 | }; 8 | 9 | function BasicAuthRequest(headers, body) { 10 | Request.call(this, headers, body); 11 | 12 | this.bodyRequired = bodyRequired; 13 | this.bodyValidations = bodyValidations; 14 | } 15 | 16 | BasicAuthRequest.prototype = Object.create(Request.prototype); 17 | BasicAuthRequest.prototype.constructor = BasicAuthRequest; 18 | 19 | module.exports = BasicAuthRequest; 20 | -------------------------------------------------------------------------------- /api/v1/requests/CheckInRequest.js: -------------------------------------------------------------------------------- 1 | const Request = require('./Request'); 2 | const CheckIn = require('../models/CheckIn'); 3 | 4 | const bodyRequired = ['location', 'swag', 'credentialsRequested']; 5 | const checkin = new CheckIn(); 6 | const bodyValidations = { 7 | location: checkin.validations.location, 8 | credentialsRequested: ['required', 'boolean'], 9 | swag: checkin.validations.swag 10 | }; 11 | 12 | function CheckInRequest(headers, body) { 13 | Request.call(this, headers, body); 14 | 15 | this.bodyRequired = bodyRequired; 16 | this.bodyValidations = bodyValidations; 17 | } 18 | 19 | CheckInRequest.prototype = Object.create(Request.prototype); 20 | CheckInRequest.prototype.constructor = CheckInRequest; 21 | 22 | module.exports = CheckInRequest; 23 | -------------------------------------------------------------------------------- /api/v1/requests/EcosystemCreationRequest.js: -------------------------------------------------------------------------------- 1 | const Request = require('./Request'); 2 | 3 | const bodyRequired = [ 'name' ]; 4 | const bodyValidations = { 5 | 'name': ['string', 'required', 'maxLength:100'] 6 | }; 7 | 8 | function EcosystemCreationRequest(headers, body) { 9 | Request.call(this, headers, body); 10 | 11 | this.bodyRequired = bodyRequired; 12 | this.bodyValidations = bodyValidations; 13 | } 14 | 15 | EcosystemCreationRequest.prototype = Object.create(Request.prototype); 16 | EcosystemCreationRequest.prototype.constructor = EcosystemCreationRequest; 17 | 18 | module.exports = EcosystemCreationRequest; 19 | -------------------------------------------------------------------------------- /api/v1/requests/EventCreationRequest.js: -------------------------------------------------------------------------------- 1 | const Request = require('./Request'); 2 | const Event = require('../models/Event'); 3 | const validators = require('../utils/validators'); 4 | 5 | const eventLocationValidations = { 6 | locationId: ['required', 'integer'] 7 | }; 8 | 9 | const bodyRequired = [ 'event' ]; 10 | const bodyAllowed = [ 'eventLocations' ]; 11 | const event = new Event(); 12 | const bodyValidations = { 13 | 'event': ['required', 'plainObject'], 14 | 'event.name': ['required', 'string', 'maxLength:255'], 15 | 'event.description': ['required', 'string', 'maxLength:2047'], 16 | 'event.startTime': ['required', validators.date], 17 | 'event.endTime': ['required', validators.date], 18 | 'event.tag': event.validations.tag, 19 | 'eventLocations': ['array', validators.array(validators.nested(eventLocationValidations, 'eventLocations'))] 20 | }; 21 | 22 | function EventCreationRequest(headers, body) { 23 | Request.call(this, headers, body); 24 | 25 | this.bodyRequired = bodyRequired; 26 | this.bodyAllowed = bodyAllowed; 27 | this.bodyValidations = bodyValidations; 28 | } 29 | 30 | EventCreationRequest._eventLocationValidations = eventLocationValidations; 31 | 32 | EventCreationRequest.prototype = Object.create(Request.prototype); 33 | EventCreationRequest.prototype.constructor = EventCreationRequest; 34 | 35 | module.exports = EventCreationRequest; 36 | -------------------------------------------------------------------------------- /api/v1/requests/EventDeletionRequest.js: -------------------------------------------------------------------------------- 1 | const Request = require('./Request'); 2 | 3 | const bodyRequired = [ 'eventId' ]; 4 | const bodyValidations = { 5 | 'eventId': ['required', 'natural'] 6 | }; 7 | 8 | function EventDeletionRequest(headers, body) { 9 | Request.call(this, headers, body); 10 | 11 | this.bodyRequired = bodyRequired; 12 | this.bodyValidations = bodyValidations; 13 | } 14 | 15 | EventDeletionRequest.prototype = Object.create(Request.prototype); 16 | EventDeletionRequest.prototype.constructor = EventDeletionRequest; 17 | 18 | module.exports = EventDeletionRequest; 19 | -------------------------------------------------------------------------------- /api/v1/requests/EventFavoriteRequest.js: -------------------------------------------------------------------------------- 1 | const Request = require('./Request'); 2 | 3 | const bodyRequired = [ 'eventId' ]; 4 | const bodyValidations = { 5 | 'eventId': ['required', 'integer'] 6 | }; 7 | 8 | function EventFavoriteRequest(headers, body) { 9 | Request.call(this, headers, body); 10 | 11 | this.bodyRequired = bodyRequired; 12 | this.bodyValidations = bodyValidations; 13 | } 14 | 15 | EventFavoriteRequest.prototype = Object.create(Request.prototype); 16 | EventFavoriteRequest.prototype.constructor = EventFavoriteRequest; 17 | 18 | module.exports = EventFavoriteRequest; 19 | -------------------------------------------------------------------------------- /api/v1/requests/EventUpdateRequest.js: -------------------------------------------------------------------------------- 1 | const Request = require('./Request'); 2 | const Event = require('../models/Event'); 3 | const validators = require('../utils/validators'); 4 | 5 | const eventLocationValidations = { 6 | locationId: ['required', 'integer'] 7 | }; 8 | 9 | const bodyRequired = [ 'event' ]; 10 | const bodyAllowed = [ 'eventLocations' ]; 11 | const event = new Event(); 12 | const bodyValidations = { 13 | 'event': ['required', 'plainObject'], 14 | 'event.id': ['required', 'natural'], 15 | 'event.name': ['required', 'string', 'maxLength:255'], 16 | 'event.description': ['required', 'string', 'maxLength:2047'], 17 | 'event.startTime': ['required', validators.date], 18 | 'event.endTime': ['required', validators.date], 19 | 'event.tag': event.validations.tag, 20 | 'eventLocations': ['array', validators.array(validators.nested(eventLocationValidations, 'eventLocations'))] 21 | }; 22 | 23 | function EventUpdateRequest(headers, body) { 24 | Request.call(this, headers, body); 25 | 26 | this.bodyRequired = bodyRequired; 27 | this.bodyAllowed = bodyAllowed; 28 | this.bodyValidations = bodyValidations; 29 | } 30 | 31 | EventUpdateRequest._eventLocationValidations = eventLocationValidations; 32 | 33 | EventUpdateRequest.prototype = Object.create(Request.prototype); 34 | EventUpdateRequest.prototype.constructor = EventUpdateRequest; 35 | 36 | module.exports = EventUpdateRequest; 37 | -------------------------------------------------------------------------------- /api/v1/requests/LocationCreationRequest.js: -------------------------------------------------------------------------------- 1 | const Request = require('./Request'); 2 | 3 | const bodyRequired = ['name', 'latitude', 'longitude']; 4 | const bodyValidations = { 5 | 'name': ['required', 'string', 'maxLength:255'], 6 | 'latitude': ['required', 'number'], 7 | 'longitude': ['required', 'number'] 8 | }; 9 | 10 | function LocationCreationRequest(headers, body) { 11 | Request.call(this, headers, body); 12 | 13 | this.bodyRequired = bodyRequired; 14 | this.bodyValidations = bodyValidations; 15 | } 16 | 17 | LocationCreationRequest.prototype = Object.create(Request.prototype); 18 | LocationCreationRequest.prototype.constructor = LocationCreationRequest; 19 | 20 | module.exports = LocationCreationRequest; 21 | -------------------------------------------------------------------------------- /api/v1/requests/MentorRequest.js: -------------------------------------------------------------------------------- 1 | const Request = require('./Request'); 2 | const Mentor = require('../models/Mentor'); 3 | const validators = require('../utils/validators'); 4 | 5 | const mentor = new Mentor(); 6 | const mentorValidations = mentor.validations; 7 | delete mentorValidations['userId']; 8 | 9 | const ideaValidations = { 10 | link: ['required', 'url', 'maxLength:255'], 11 | contributions: ['required', 'string', 'maxLength:255'], 12 | ideas: ['required', 'string', 'maxLength:255'] 13 | }; 14 | const bodyRequired = ['mentor', 'ideas']; 15 | const bodyValidations = { 16 | 'mentor': ['required', 'plainObject', validators.nested(mentor.validations, 'mentor')], 17 | 'ideas': ['required', 'array', 'minLength:1', 'maxLength:5', validators.array(validators.nested(ideaValidations, 'ideas'))] 18 | }; 19 | 20 | function MentorCreationRequest(headers, body) { 21 | Request.call(this, headers, body); 22 | 23 | this.bodyRequired = bodyRequired; 24 | this.bodyValidations = bodyValidations; 25 | } 26 | 27 | MentorCreationRequest._mentorValidations = mentorValidations; 28 | MentorCreationRequest._ideaValidations = ideaValidations; 29 | 30 | MentorCreationRequest.prototype = Object.create(Request.prototype); 31 | MentorCreationRequest.prototype.constructor = MentorCreationRequest; 32 | 33 | module.exports = MentorCreationRequest; 34 | -------------------------------------------------------------------------------- /api/v1/requests/ProjectMentorRequest.js: -------------------------------------------------------------------------------- 1 | const Request = require('./Request'); 2 | 3 | const bodyRequired = ['project_id', 'mentor_id']; 4 | const bodyValidations = { 5 | 'project_id': ['integer', 'required'], 6 | 'mentor_id': ['integer', 'required'] 7 | }; 8 | 9 | function ProjectMentorRequest(headers, body) { 10 | Request.call(this, headers, body); 11 | 12 | this.bodyRequired = bodyRequired; 13 | this.bodyValidations = bodyValidations; 14 | } 15 | 16 | ProjectMentorRequest.prototype = Object.create(Request.prototype); 17 | ProjectMentorRequest.prototype.constructor = ProjectMentorRequest; 18 | 19 | module.exports = ProjectMentorRequest; 20 | -------------------------------------------------------------------------------- /api/v1/requests/ProjectRequest.js: -------------------------------------------------------------------------------- 1 | const Request = require('./Request'); 2 | 3 | const bodyRequired = ['name', 'description', 'repo', 'isPublished']; 4 | const bodyValidations = { 5 | 'name': ['string', 'required'], 6 | 'description': ['string', 'required'], 7 | 'repo': ['required', 'string', 'maxLength:255'], 8 | 'isPublished': [ 'boolean' ] 9 | }; 10 | 11 | function ProjectRequest(headers, body) { 12 | Request.call(this, headers, body); 13 | 14 | this.bodyRequired = bodyRequired; 15 | this.bodyValidations = bodyValidations; 16 | } 17 | 18 | ProjectRequest.prototype = Object.create(Request.prototype); 19 | ProjectRequest.prototype.constructor = ProjectRequest; 20 | 21 | module.exports = ProjectRequest; 22 | -------------------------------------------------------------------------------- /api/v1/requests/RSVPRequest.js: -------------------------------------------------------------------------------- 1 | const Request = require('./Request'); 2 | 3 | const bodyRequired = [ 'isAttending' ]; 4 | const bodyValidations = { 5 | 'isAttending': ['required', 'boolean'] 6 | }; 7 | 8 | function RSVPRequest(headers, body) { 9 | Request.call(this, headers, body); 10 | 11 | this.bodyRequired = bodyRequired; 12 | this.bodyValidations = bodyValidations; 13 | } 14 | 15 | RSVPRequest.prototype = Object.create(Request.prototype); 16 | RSVPRequest.prototype.constructor = RSVPRequest; 17 | 18 | module.exports = RSVPRequest; 19 | -------------------------------------------------------------------------------- /api/v1/requests/RecruiterInterestRequest.js: -------------------------------------------------------------------------------- 1 | const Request = require('./Request'); 2 | 3 | const bodyRequired = [ 'attendeeUserId' ]; 4 | const bodyAllowed = ['comments', 'favorite']; 5 | const bodyValidations = { 6 | 'attendeeUserId': ['required', 'integer'], 7 | 'comments': ['string', 'maxLength:255'], 8 | 'favorite': [ 'boolean' ] 9 | }; 10 | 11 | function RecruiterInterestRequest(headers, body) { 12 | Request.call(this, headers, body); 13 | 14 | this.bodyRequired = bodyRequired; 15 | this.bodyValidations = bodyValidations; 16 | this.bodyAllowed = bodyAllowed; 17 | } 18 | 19 | RecruiterInterestRequest.prototype = Object.create(Request.prototype); 20 | RecruiterInterestRequest.prototype.constructor = RecruiterInterestRequest; 21 | 22 | module.exports = RecruiterInterestRequest; 23 | -------------------------------------------------------------------------------- /api/v1/requests/ResetPasswordRequest.js: -------------------------------------------------------------------------------- 1 | const Request = require('./Request'); 2 | 3 | const bodyRequired = ['token', 'password']; 4 | const bodyValidations = { 5 | 'token': [ 'string' ], 6 | 'password': ['string', 'minLength:8', 'maxLength:50'] 7 | }; 8 | 9 | function ResetPasswordRequest(headers, body) { 10 | Request.call(this, headers, body); 11 | 12 | this.bodyRequired = bodyRequired; 13 | this.bodyValidations = bodyValidations; 14 | } 15 | 16 | ResetPasswordRequest.prototype = Object.create(Request.prototype); 17 | ResetPasswordRequest.prototype.constructor = ResetPasswordRequest; 18 | 19 | module.exports = ResetPasswordRequest; 20 | -------------------------------------------------------------------------------- /api/v1/requests/ResetTokenRequest.js: -------------------------------------------------------------------------------- 1 | const Request = require('./Request'); 2 | 3 | const bodyRequired = [ 'email' ]; 4 | const bodyValidations = { 5 | 'email': [ 'email' ] 6 | }; 7 | 8 | function ResetTokenRequest(headers, body) { 9 | Request.call(this, headers, body); 10 | 11 | this.bodyRequired = bodyRequired; 12 | this.bodyValidations = bodyValidations; 13 | } 14 | 15 | ResetTokenRequest.prototype = Object.create(Request.prototype); 16 | ResetTokenRequest.prototype.constructor = ResetTokenRequest; 17 | 18 | module.exports = ResetTokenRequest; 19 | -------------------------------------------------------------------------------- /api/v1/requests/SendListRequest.js: -------------------------------------------------------------------------------- 1 | const Request = require('./Request'); 2 | const mailUtils = require('../utils/mail'); 3 | 4 | const bodyRequired = ['listName', 'template']; 5 | const bodyValidations = { 6 | 'listName': ['required', 'string', mailUtils.checkValidMailName], 7 | 'template': ['required', 'string', mailUtils.checkValidTemplateName] 8 | }; 9 | 10 | function SendListRequest(headers, body) { 11 | Request.call(this, headers, body); 12 | 13 | this.bodyRequired = bodyRequired; 14 | this.bodyValidations = bodyValidations; 15 | } 16 | 17 | SendListRequest.prototype = Object.create(Request.prototype); 18 | SendListRequest.prototype.constructor = SendListRequest; 19 | 20 | module.exports = SendListRequest; 21 | -------------------------------------------------------------------------------- /api/v1/requests/UniversalTrackingRequest.js: -------------------------------------------------------------------------------- 1 | const Request = require('./Request'); 2 | 3 | const bodyRequired = ['name', 'duration']; 4 | const bodyValidations = { 5 | 'name': ['required', 'string', 'maxLength:255'], 6 | 'duration': ['required', 'naturalNonZero'] 7 | }; 8 | 9 | function UniversalTrackingRequest(headers, body) { 10 | Request.call(this, headers, body); 11 | 12 | this.bodyRequired = bodyRequired; 13 | this.bodyValidations = bodyValidations; 14 | } 15 | 16 | UniversalTrackingRequest.prototype = Object.create(Request.prototype); 17 | UniversalTrackingRequest.prototype.constructor = UniversalTrackingRequest; 18 | 19 | module.exports = UniversalTrackingRequest; 20 | -------------------------------------------------------------------------------- /api/v1/requests/UpdateRecruiterInterestRequest.js: -------------------------------------------------------------------------------- 1 | const Request = require('./Request'); 2 | 3 | const bodyRequired = []; 4 | const bodyAllowed = ['comments', 'favorite']; 5 | const bodyValidations = { 6 | 'comments': ['string', 'maxLength:255'], 7 | 'favorite': [ 'boolean' ] 8 | }; 9 | 10 | function UpdateRecruiterInterestRequest(headers, body) { 11 | Request.call(this, headers, body); 12 | 13 | this.bodyRequired = bodyRequired; 14 | this.bodyValidations = bodyValidations; 15 | this.bodyAllowed = bodyAllowed; 16 | } 17 | 18 | UpdateRecruiterInterestRequest.prototype = Object.create(Request.prototype); 19 | UpdateRecruiterInterestRequest.prototype.constructor = UpdateRecruiterInterestRequest; 20 | 21 | module.exports = UpdateRecruiterInterestRequest; 22 | -------------------------------------------------------------------------------- /api/v1/requests/UploadRequest.js: -------------------------------------------------------------------------------- 1 | const Request = require('./Request'); 2 | 3 | const headerRequired = ['Content-Length', 'Content-Type']; 4 | 5 | function UploadRequest(headers, body) { 6 | Request.call(this, headers, body); 7 | 8 | this.headerRequired = headerRequired; 9 | } 10 | 11 | UploadRequest.prototype = Object.create(Request.prototype); 12 | UploadRequest.prototype.constructor = UploadRequest; 13 | 14 | module.exports = UploadRequest; 15 | -------------------------------------------------------------------------------- /api/v1/requests/UserContactInfoRequest.js: -------------------------------------------------------------------------------- 1 | const Request = require('./Request'); 2 | 3 | const bodyRequired = [ 'newEmail' ]; 4 | const bodyValidations = { 5 | 'newEmail': [ 'email' ] 6 | }; 7 | 8 | function UserContactInfoRequest(headers, body) { 9 | Request.call(this, headers, body); 10 | 11 | this.bodyRequired = bodyRequired; 12 | this.bodyValidations = bodyValidations; 13 | } 14 | 15 | UserContactInfoRequest.prototype = Object.create(Request.prototype); 16 | UserContactInfoRequest.prototype.constructor = UserContactInfoRequest; 17 | 18 | module.exports = UserContactInfoRequest; 19 | -------------------------------------------------------------------------------- /api/v1/requests/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | AnnouncementRequest: require('./AnnouncementRequest'), 3 | AttendeeRequest: require('./AttendeeRequest'), 4 | AttendeeDecisionRequest: require('./AttendeeDecisionRequest'), 5 | AccreditedUserCreationRequest: require('./AccreditedUserCreationRequest'), 6 | BasicAuthRequest: require('./BasicAuthRequest'), 7 | EcosystemCreationRequest: require('./EcosystemCreationRequest'), 8 | EventCreationRequest: require('./EventCreationRequest'), 9 | EventUpdateRequest: require('./EventUpdateRequest'), 10 | EventDeletionRequest: require('./EventDeletionRequest'), 11 | LocationCreationRequest: require('./LocationCreationRequest'), 12 | MentorRequest: require('./MentorRequest'), 13 | UpdateRecruiterInterestRequest: require('./UpdateRecruiterInterestRequest'), 14 | RecruiterInterestRequest: require('./RecruiterInterestRequest'), 15 | ProjectRequest: require('./ProjectRequest'), 16 | ProjectMentorRequest: require('./ProjectMentorRequest'), 17 | ResetTokenRequest: require('./ResetTokenRequest'), 18 | ResetPasswordRequest: require('./ResetPasswordRequest'), 19 | SendListRequest: require('./SendListRequest'), 20 | UploadRequest: require('./UploadRequest'), 21 | CheckInRequest: require('./CheckInRequest'), 22 | RSVPRequest: require('./RSVPRequest'), 23 | UniversalTrackingRequest: require('./UniversalTrackingRequest'), 24 | UserContactInfoRequest: require('./UserContactInfoRequest') 25 | }; 26 | -------------------------------------------------------------------------------- /api/v1/services/AnnouncementService.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | const InvalidParameterError = require('../errors/InvalidParameterError'); 4 | const Announcement = require('../models/Announcement'); 5 | 6 | /** 7 | * Retrieves all announcements within the given bounds in order descending from creation 8 | * @param {Date} before the date at which to start querying (optional) 9 | * @param {Date} after the date at which to stop querying (optional) 10 | * @param {Number} limit the maximum number of announcements to return (optional) 11 | * @return {Promise} a promise resolving to a collection of Announcements 12 | */ 13 | module.exports.getAllAnnouncements = (before, after, limit) => Announcement.findAll(before, after, limit); 14 | 15 | /** 16 | * Creates a new announcement 17 | * @param {Object} parameters the parameters for creation 18 | * @return {Promise} a promise resolving to a new Announcement 19 | */ 20 | module.exports.createAnnouncement = (parameters) => Announcement.forge(parameters).save(); 21 | 22 | /** 23 | * Finds an announcement by its id 24 | * @param {Number} id the id of the Announcement to be queried 25 | * @return {Promise} a promise resolving to the desired Announcement 26 | * @throws {InvalidParameterError} when no Announcement can be found 27 | */ 28 | module.exports.findById = (id) => Announcement.findById(id).then((result) => { 29 | if (_.isNil(result)) { 30 | const message = 'An Announcement with the given id does not exist'; 31 | const source = 'id'; 32 | throw new InvalidParameterError(message, source); 33 | } 34 | 35 | return result; 36 | }); 37 | 38 | /** 39 | * Updates an announcement 40 | * @param {Announcement} announcement an existing announcement 41 | * @param {Object} parameters the key-value pairs with which to update the announcement 42 | * @return {Promise} a promise resolving to the updated announcement 43 | */ 44 | module.exports.updateAnnouncement = (announcement, parameters) => { 45 | announcement.set(parameters); 46 | return announcement.save(); 47 | }; 48 | 49 | /** 50 | * Deletes an announcement 51 | * @param {Announcement} announcement an existing announcement 52 | * @return {Promise<>} an empty-resolving promise 53 | */ 54 | module.exports.deleteAnnouncement = (announcement) => announcement.destroy(); 55 | -------------------------------------------------------------------------------- /api/v1/services/EcosystemService.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | const utils = require('../utils'); 4 | const errors = require('../errors'); 5 | const Ecosystem = require('../models/Ecosystem'); 6 | 7 | 8 | module.exports.getAllEcosystems = () => Ecosystem.fetchAll(); 9 | 10 | module.exports.createEcosystem = (name) => { 11 | const ecosystem = Ecosystem.forge({ 12 | name: name.toLowerCase() 13 | }); 14 | 15 | return ecosystem.save() 16 | .catch( 17 | utils.errors.DuplicateEntryError, 18 | utils.errors.handleDuplicateEntryError('An ecosystem with the given name already exists', 'name') 19 | ); 20 | }; 21 | 22 | module.exports.deleteEcosystem = (name) => Ecosystem 23 | .findByName(name) 24 | .then((result) => { 25 | if (_.isNull(result)) { 26 | const message = 'An ecosystem with the given name does not exist'; 27 | const source = 'name'; 28 | throw new errors.InvalidParameterError(message, source); 29 | } 30 | 31 | return result.destroy(); 32 | }); 33 | -------------------------------------------------------------------------------- /api/v1/services/PermissionService.js: -------------------------------------------------------------------------------- 1 | const _Promise = require('bluebird'); 2 | 3 | const errors = require('../errors'); 4 | const roles = require('../utils/roles'); 5 | 6 | /** 7 | * Determines whether the provided creator can create a user 8 | * with the provided user role 9 | * @param {User} creator the intended creator with related role information 10 | * @param {String} userRole the role of the desired user 11 | * @return {Promise} resolving whether or not creation should be allowed 12 | * @throws {UnauthorizedError} when the provided role is not authorized (rejected promise) 13 | */ 14 | module.exports.canCreateUser = (creator, userRole) => { 15 | if (creator.hasRole(roles.SUPERUSER)) { 16 | // the superuser can create anyone 17 | return _Promise.resolve(true); 18 | } 19 | if (roles.isIn(roles.COMMON, userRole) && creator.hasRoles(roles.ORGANIZERS)) { 20 | // the organizers must be able to create any of the 21 | // common roles 22 | return _Promise.resolve(true); 23 | } 24 | 25 | const message = 'The requested user cannot be created with the provided credentials'; 26 | return _Promise.reject(new errors.UnauthorizedError(message)); 27 | }; 28 | 29 | 30 | /** 31 | * Checks to see if a requestor valid permissions to create a new project 32 | * @param {User} user creating the new project 33 | * @return {Promise} resolving to true if the user is an organizer 34 | * @throws InvalidParameterError when a user does not have correct permissions 35 | */ 36 | module.exports.canCreateProject = (creator) => { 37 | if (creator.hasRole(roles.SUPERUSER) || creator.hasRole(roles.ORGANIZERS)) { 38 | return _Promise.resolve(true); 39 | } 40 | 41 | const message = 'A project cannot be created with the provided credentials'; 42 | return _Promise.reject(new errors.UnauthorizedError(message)); 43 | }; 44 | 45 | 46 | /** 47 | * Checks to see if a requester is an organizer (or superuser) 48 | * @param {User} user to check roles for 49 | * @return {Promise} resolving to true if the user is an organizer, false otherwise 50 | */ 51 | module.exports.isOrganizer = (user) => { 52 | if (user.hasRole(roles.SUPERUSER)) { 53 | // the superuser is allowed 54 | return _Promise.resolve(true); 55 | } 56 | if (user.hasRoles(roles.ORGANIZERS)) { 57 | // the user is an organizer 58 | return _Promise.resolve(true); 59 | } 60 | // return false 61 | return _Promise.resolve(false); 62 | 63 | }; 64 | 65 | /** 66 | * Checks to see if a requester is a host 67 | * @param {User} user to check roles for 68 | * @return {Promise} resolving to true if the user is a host, false otherwise 69 | */ 70 | module.exports.isHost = (user) => { 71 | if (user.hasRole(roles.SUPERUSER)) { 72 | return _Promise.resolve(true); 73 | } 74 | if (user.hasRoles(roles.HOSTS)) { 75 | return _Promise.resolve(true); 76 | } 77 | 78 | return _Promise.resolve(false); 79 | }; 80 | -------------------------------------------------------------------------------- /api/v1/services/RSVPService.js: -------------------------------------------------------------------------------- 1 | const CheckitError = require('checkit').Error; 2 | const _Promise = require('bluebird'); 3 | const _ = require('lodash'); 4 | 5 | const RSVP = require('../models/AttendeeRSVP'); 6 | const UserRole = require('../models/UserRole'); 7 | const errors = require('../errors'); 8 | const utils = require('../utils'); 9 | /** 10 | * Gets an rsvp by its id 11 | * @param {integer} id the id of the RSVP to find 12 | * @returns {Promise} the resolved rsvp 13 | */ 14 | module.exports.getRSVPById = (id) => RSVP.findById(id); 15 | 16 | /** 17 | * Creates an RSVP and sets the users attendee role to active 18 | * @param {Attendee} attendee the associated attendee for the rsvp 19 | * @param {User} user the associated user for the rsvp 20 | * @param {Object} attributes the rsvp data 21 | * @returns {Promise} the resolved rsvp 22 | * @throws {InvalidParameterError} thrown when an attendee already has an rsvp 23 | */ 24 | module.exports.createRSVP = (attendee, user, attributes) => { 25 | attributes.attendeeId = attendee.get('id'); 26 | const rsvp = RSVP.forge(attributes); 27 | 28 | return rsvp 29 | .validate() 30 | .catch(CheckitError, utils.errors.handleValidationError) 31 | .then(() => RSVP.transaction((t) => rsvp.save(null, { 32 | transacting: t 33 | }) 34 | .tap(() => { 35 | const userRole = user.getRole(utils.roles.ATTENDEE); 36 | return UserRole.setActive(userRole, true, t); 37 | }))) 38 | .catch( 39 | utils.errors.DuplicateEntryError, 40 | utils.errors.handleDuplicateEntryError('An RSVP already exists for the given attendee', 'attendeeId') 41 | ); 42 | }; 43 | 44 | /** 45 | * Finds an RSVP by its associated attendee 46 | * @param {Attendee} attendee the associated attendee for the rsvp 47 | * @returns {Promise} the resolved rsvp for the attendee 48 | * @throws {NotFoundError} when the attendee has no RSVP 49 | */ 50 | module.exports.findRSVPByAttendee = (attendee) => RSVP 51 | .findByAttendeeId(attendee.get('id')) 52 | .then((result) => { 53 | if (_.isNull(result)) { 54 | const message = 'An RSVP cannot be found for the given attendee'; 55 | const source = 'attendeeId'; 56 | throw new errors.NotFoundError(message, source); 57 | } 58 | 59 | return _Promise.resolve(result); 60 | }); 61 | 62 | /** 63 | * Updates a given RSVP 64 | * @param {RSVP} rsvp the RSVP to update 65 | * @param {Object} attributes the new RSVP data to set 66 | * @returns {Promise} the resolved RSVP 67 | */ 68 | module.exports.updateRSVP = (user, rsvp, attributes) => { 69 | rsvp.set(attributes); 70 | 71 | return rsvp 72 | .validate() 73 | .catch(CheckitError, utils.errors.handleValidationError) 74 | .then(() => { 75 | const userRole = user.getRole(utils.roles.ATTENDEE); 76 | UserRole.setActive(userRole, rsvp.get('isAttending')); 77 | 78 | return rsvp.save(); 79 | }); 80 | }; 81 | -------------------------------------------------------------------------------- /api/v1/services/RecruiterInterestService.js: -------------------------------------------------------------------------------- 1 | const _Promise = require('bluebird'); 2 | const _ = require('lodash'); 3 | 4 | const RecruiterInterest = require('../models/RecruiterInterest'); 5 | const errors = require('../errors'); 6 | const utils = require('../utils'); 7 | 8 | module.exports.findByRecruiterId = (recruiterId) => RecruiterInterest 9 | .findByRecruiterId(recruiterId) 10 | .then((result) => { 11 | if (_.isNull(result)) { 12 | const message = 'A recruiter with the given ID cannot be found'; 13 | const source = 'id'; 14 | throw new errors.NotFoundError(message, source); 15 | } 16 | return _Promise.resolve(result); 17 | }); 18 | 19 | module.exports.findById = (id) => RecruiterInterest 20 | .findById(id) 21 | .then((result) => { 22 | if (_.isNull(result)) { 23 | const message = 'An interest with the given ID cannot be found'; 24 | const source = 'id'; 25 | throw new errors.NotFoundError(message, source); 26 | } 27 | return _Promise.resolve(result); 28 | }); 29 | 30 | module.exports.findByAttendeeId = (attendeeId) => RecruiterInterest 31 | .findByAttendeeId(attendeeId) 32 | .then((result) => { 33 | if (_.isNull(result)) { 34 | const message = 'An attendee with the given ID cannot be found'; 35 | const source = 'id'; 36 | throw new errors.NotFoundError(message, source); 37 | } 38 | return _Promise.resolve(result); 39 | }); 40 | 41 | module.exports.createInterest = (recruiterId, attendeeId, comments, favorite) => { 42 | if (_.isUndefined(comments)) { 43 | comments = ''; 44 | } 45 | if (_.isUndefined(favorite)) { 46 | favorite = 0; 47 | } 48 | 49 | const interest = RecruiterInterest.forge({ 50 | recruiterId: recruiterId, 51 | attendeeId: attendeeId, 52 | comments: comments, 53 | favorite: favorite 54 | }); 55 | 56 | return interest 57 | .validate() 58 | .catch(utils.errors.handleValidationError) 59 | .then(() => interest.save()) 60 | .catch( 61 | utils.errors.DuplicateEntryError, 62 | utils.errors.handleDuplicateEntryError('An interest with the given attendee and recruiter exists', 'attendeeUserId')); 63 | 64 | }; 65 | 66 | module.exports.updateInterest = (id, comments, favorite) => RecruiterInterest 67 | .updateInterest(id, comments, favorite) 68 | .then((result) => { 69 | if (_.isNull(result)) { 70 | const message = 'An application with the given ID cannot be found'; 71 | const source = 'id'; 72 | throw new errors.NotFoundError(message, source); 73 | } 74 | return _Promise.resolve(result); 75 | }); 76 | -------------------------------------------------------------------------------- /api/v1/services/TokenService.js: -------------------------------------------------------------------------------- 1 | /* jshint esversion: 6 */ 2 | 3 | const _Promise = require('bluebird'); 4 | 5 | const ctx = require('ctx'); 6 | const Token = require('../models/Token'); 7 | const config = ctx.config(); 8 | const errors = require('../errors'); 9 | const utils = require('../utils'); 10 | 11 | const TOKEN_NOT_FOUND_ERROR = 'The supplied token does not exist'; 12 | const TOKEN_SCOPE_INVALID_ERROR = 'An invalid or non-existent scope was supplied'; 13 | 14 | /** 15 | * Finds a token given the Token value 16 | * @param {String} value The Token's value 17 | * @param {String} scope The Scope the token is for 18 | * @return {Promise} resolving to the associated Token Model 19 | * @throws {NotFoundError} when the requested token cannot be found 20 | * @throws {TokenExpirationError} when the request token has expired 21 | * @throws {TypeError} when the scope was not found 22 | */ 23 | module.exports.findTokenByValue = (value, scope) => { 24 | if (!(scope in config.token.expiration)) { 25 | return _Promise.reject(new TypeError(TOKEN_SCOPE_INVALID_ERROR)); 26 | } 27 | 28 | return Token 29 | .findByValue(value) 30 | .then((result) => { 31 | if (!result) { 32 | throw new errors.NotFoundError(TOKEN_NOT_FOUND_ERROR); 33 | } 34 | 35 | const expiration = utils.time.toMilliseconds(config.token.expiration[scope]); 36 | const tokenExpiration = Date.parse(result.get('created')) + expiration; 37 | if (tokenExpiration < Date.now()) { 38 | result.destroy(); 39 | throw new errors.TokenExpirationError(); 40 | } 41 | 42 | return _Promise.resolve(result); 43 | }); 44 | }; 45 | 46 | /** 47 | * Generates a token and deletes all existing tokens 48 | * with the same scope. 49 | * @param {User} user The user object to create a reset token for. 50 | * @param {String} scope The scope to create the token for. 51 | * @return {Promise} Returns a Promise that resolves to 52 | * true on a successful token creation. 53 | */ 54 | module.exports.generateToken = (user, scope) => { 55 | const tokenVal = utils.crypto.generateResetToken(); 56 | const userId = user.get('id'); 57 | 58 | return Token 59 | .where({ 60 | user_id: userId, 61 | type: scope 62 | }) 63 | .fetchAll() 64 | .then((tokens) => tokens.invokeThen('destroy') 65 | .then(() => { 66 | const token = Token.forge({ 67 | type: scope, 68 | value: tokenVal, 69 | user_id: userId 70 | }); 71 | return token.save() 72 | .then(() => tokenVal); 73 | })); 74 | }; 75 | -------------------------------------------------------------------------------- /api/v1/services/TrackingService.js: -------------------------------------------------------------------------------- 1 | const CheckitError = require('checkit').Error; 2 | const _ = require('lodash'); 3 | const _Promise = require('bluebird'); 4 | 5 | const ctx = require('ctx'); 6 | const cache = ctx.cache().instance(); 7 | const TrackingItem = require('../models/TrackingEvent'); 8 | const errors = require('../errors'); 9 | const utils = require('../utils'); 10 | 11 | const TRACKING_NAMESPACE = 'utracking_'; 12 | const TRACKED_EVENT = 'trackedEvent'; 13 | 14 | /** 15 | * Allows an Admin to post a new tracking event if one is not being trucked 16 | * @param {Object} attributes the attributes of the event to be tracked 17 | * @return {Promise} resolving to the event model 18 | * @throws {InvalidParameterError} when the provided event is already a tracked event 19 | * @throws {InvalidTrackingStateError} when an active event is already occurring 20 | */ 21 | module.exports.createTrackingEvent = (attributes) => { 22 | const trackingItem = TrackingItem.forge(attributes); 23 | 24 | return trackingItem 25 | .validate() 26 | .catch(CheckitError, utils.errors.handleValidationError) 27 | .then(() => cache.getAsync(TRACKED_EVENT)) 28 | .then((result) => { 29 | if (!_.isNil(result)) { 30 | return cache.ttlAsync(TRACKED_EVENT) 31 | .then((ttl) => { 32 | const message = 'An event is currently being tracked. The current event, ' + result + 33 | ', ends in: ' + utils.time.secondsToHHMMSS(ttl); 34 | const source = trackingItem.get('name'); 35 | return _Promise.reject(new errors.InvalidTrackingStateError(message, source)); 36 | }); 37 | } 38 | 39 | return trackingItem.save(); 40 | }) 41 | .tap(() => cache.multi() 42 | .set(TRACKED_EVENT, trackingItem.get('name')) 43 | .expire(TRACKED_EVENT, trackingItem.get('duration')) 44 | .execAsync()) 45 | .catch( 46 | utils.errors.DuplicateEntryError, 47 | utils.errors.handleDuplicateEntryError('This event is already being tracked', 'name') 48 | ); 49 | }; 50 | 51 | /** 52 | * Allows a Host to determine if an attendee has already participated in a tracked event 53 | * @param {Object} participantId the id of the user to track 54 | * @throws {InvalidTrackingStateError} when there is no event being currently tracked 55 | * @throws {InvalidParameterError} when an attendee has already participated in an event 56 | */ 57 | module.exports.addEventParticipant = (participantId) => { 58 | let currentEvent; 59 | return cache.getAsync(TRACKED_EVENT) 60 | .then((result) => { 61 | if (_.isNil(result)) { 62 | const message = 'No event is currently being tracked'; 63 | const source = 'EventTracking'; 64 | throw new errors.InvalidTrackingStateError(message, source); 65 | } 66 | 67 | currentEvent = result; 68 | 69 | return cache.getAsync(TRACKING_NAMESPACE + participantId); 70 | }) 71 | .then((result) => { 72 | if (!_.isNil(result)) { 73 | const message = 'This attendee has already participated in ' + currentEvent + '!'; 74 | const source = participantId; 75 | throw new errors.InvalidParameterError(message, source); 76 | } 77 | 78 | return cache.ttlAsync(TRACKED_EVENT); 79 | }) 80 | .then((ttl) => cache.multi() 81 | .set(TRACKING_NAMESPACE + participantId, true) 82 | .expire(TRACKING_NAMESPACE + participantId, ttl) 83 | .execAsync()) 84 | .then(() => TrackingItem.query() 85 | .where('name', currentEvent) 86 | .increment('count', 1)); 87 | }; 88 | -------------------------------------------------------------------------------- /api/v1/services/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | AuthService: require('./AuthService'), 3 | EcosystemService: require('./EcosystemService'), 4 | EventService: require('./EventService'), 5 | MailService: require('./MailService'), 6 | PermissionService: require('./PermissionService'), 7 | ProjectService: require('./ProjectService'), 8 | RecruiterInterestService: require('./RecruiterInterestService'), 9 | RegistrationService: require('./RegistrationService'), 10 | StatsService: require('./StatsService'), 11 | StorageService: require('./StorageService'), 12 | UserService: require('./UserService'), 13 | TokenService: require('./TokenService'), 14 | CheckInService: require('./CheckInService'), 15 | RSVPService: require('./RSVPService'), 16 | TrackingService: require('./TrackingService') 17 | }; 18 | -------------------------------------------------------------------------------- /api/v1/utils/cache.js: -------------------------------------------------------------------------------- 1 | const ctx = require('ctx'); 2 | const client = ctx.cache().instance(); 3 | const errors = require('../errors'); 4 | 5 | module.exports.hasKey = (key) => client.existsAsync(key); 6 | 7 | module.exports.expireKey = (key, duration) => client.existsAsync(key) 8 | .then((reply) => { 9 | if (reply != 1) { 10 | throw new errors.RedisError(); 11 | } 12 | }) 13 | .then(() => client.expireAsync(key, duration)) 14 | .then((reply) => reply); 15 | 16 | module.exports.getString = (key) => client.existsAsync(key) 17 | .then((reply) => { 18 | if (reply != 1) { 19 | throw new errors.RedisError(); 20 | } 21 | return null; 22 | }) 23 | .then(() => client.getAsync(key)) 24 | .then((res) => res); 25 | 26 | module.exports.storeString = (key, value) => client.setAsync(key, value) 27 | .then((reply) => reply); 28 | -------------------------------------------------------------------------------- /api/v1/utils/crypto.js: -------------------------------------------------------------------------------- 1 | /* jshint esversion: 6 */ 2 | 3 | const uuid4 = require('uuid/v4'); 4 | 5 | module.exports.generatePassword = () => uuid4(); 6 | module.exports.generateResetToken = () => uuid4(); 7 | -------------------------------------------------------------------------------- /api/v1/utils/database.js: -------------------------------------------------------------------------------- 1 | /* jshint esversion: 6 */ 2 | 3 | const inflection = require('inflection'); 4 | const _ = require('lodash'); 5 | 6 | module.exports.format = (target) => { 7 | if (_.isObject(target)) { 8 | return _.mapKeys(target, (v, k) => inflection.underscore(k, true)); 9 | } 10 | if (_.isArray(target)) { 11 | return _.map(target, (v) => _.isString(v) ? inflection.underscore(v, true) : v); 12 | } 13 | return _.isString(target) ? inflection.underscore(target, true) : target; 14 | }; 15 | 16 | module.exports.parse = (target) => { 17 | if (_.isObject(target)) { 18 | return _.mapKeys(target, (v, k) => inflection.camelize(k, true)); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /api/v1/utils/errors.js: -------------------------------------------------------------------------------- 1 | const InvalidParameterError = require('../errors/InvalidParameterError'); 2 | const ErrorConstants = require('../errors/Constants'); 3 | 4 | /** 5 | * Re-throws a Checkit validation error as an Invalid parameter error 6 | * @param {Checkit.Error} error the error to re-throw 7 | * @throws {InvalidParameterError} the re-thrown error 8 | */ 9 | module.exports.handleValidationError = (error) => { 10 | const errorKey = error.keys()[0]; 11 | let specificError = error.errors[errorKey]; 12 | 13 | const errorDetail = specificError.message; 14 | let errorSource; 15 | while (specificError.key) { 16 | // find the most-complete error source in the error stack 17 | errorSource = specificError.key; 18 | specificError = (specificError.errors) ? specificError.errors[0] : undefined; 19 | } 20 | 21 | throw new InvalidParameterError(errorDetail, errorSource); 22 | }; 23 | 24 | module.exports.DuplicateEntryError = (error) => error.code === ErrorConstants.DupEntry; 25 | 26 | module.exports.handleDuplicateEntryError = (message, source) => () => { 27 | throw new InvalidParameterError(message, source); 28 | }; 29 | -------------------------------------------------------------------------------- /api/v1/utils/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | crypto: require('./crypto.js'), 3 | cache: require('./cache.js'), 4 | database: require('./database.js'), 5 | errors: require('./errors.js'), 6 | logs: require('./logs.js'), 7 | mail: require('./mail.js'), 8 | roles: require('./roles.js'), 9 | scopes: require('./scopes.js'), 10 | storage: require('./storage.js'), 11 | time: require('./time.js'), 12 | validators: require('./validators.js') 13 | }; 14 | -------------------------------------------------------------------------------- /api/v1/utils/logs.js: -------------------------------------------------------------------------------- 1 | /* jshint esversion: 6 */ 2 | const _ = require('lodash'); 3 | const ctx = require('ctx'); 4 | const config = ctx.config(); 5 | const logger = require('../../logging'); 6 | 7 | const ERROR_TYPES = { 8 | UNCAUGHT: 'UNCAUGHT', 9 | CLIENT: 'CLIENT', 10 | UNKNOWN: 'UNKNOWN' 11 | }; 12 | 13 | // although we won't make use of this now, it might be useful in 14 | // the future (e.g. logging the body in development but not in production) 15 | function _filterBody(body, blacklist) { 16 | for (const key in body) { // eslint-disable-line guard-for-in 17 | const value = body[key]; 18 | if (_.isPlainObject(value)) { 19 | body[key] = _filterBody(value, blacklist); 20 | } 21 | } 22 | 23 | return _.omit(body, blacklist); 24 | } 25 | 26 | function _acknowledgeRequest(req) { 27 | return { 28 | id: req.id, 29 | method: req.method, 30 | url: req.originalUrl, 31 | ip: req.ip 32 | }; 33 | } 34 | 35 | function _makeRequestMetadata(req) { 36 | return { 37 | id: req.id, 38 | user: req.user ? req.user.get('id') : null, 39 | adminOverRide: req.originUser ? req.originUser : null, 40 | query: req.query, 41 | params: req.params, 42 | body: _filterBody(req.body, config.logs.request.blacklist) 43 | }; 44 | } 45 | 46 | function _makeResponseMetadata(req, res) { 47 | return { 48 | id: req.id, 49 | status: res.statusCode, 50 | body: res.body 51 | }; 52 | } 53 | 54 | module.exports.errorTypes = ERROR_TYPES; 55 | 56 | module.exports.logRequestReceipt = (req) => { 57 | logger.debug('received request', _acknowledgeRequest(req)); 58 | }; 59 | 60 | module.exports.logRequest = (req) => { 61 | logger.debug('qualified request', _makeRequestMetadata(req)); 62 | }; 63 | 64 | module.exports.logResponse = (req, res) => { 65 | logger.debug('sent response', _makeResponseMetadata(req, res)); 66 | }; 67 | 68 | module.exports.logError = (req, error, status, cause) => { 69 | const metadata = _makeRequestMetadata(req); 70 | metadata.cause = cause; 71 | metadata.error = error; 72 | metadata.status = status || 500; 73 | 74 | const level = (cause !== ERROR_TYPES.CLIENT) ? 'error' : 'debug'; 75 | logger.log(level, 'an error was thrown', metadata); 76 | }; 77 | -------------------------------------------------------------------------------- /api/v1/utils/mail.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const ctx = require('ctx'); 3 | const config = ctx.config(); 4 | 5 | module.exports.checkValidMailName = (listName) => !_.isUndefined(config.mail.lists[listName]); 6 | module.exports.checkValidTemplateName = (templateName) => !_.isUndefined(config.mail.templates[templateName]); 7 | -------------------------------------------------------------------------------- /api/v1/utils/roles.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | const ALL_ROLES = ['ADMIN', 'STAFF', 'SPONSOR', 'MENTOR', 'VOLUNTEER', 'ATTENDEE']; 4 | 5 | _.forEach(ALL_ROLES, (role) => { 6 | module.exports[role] = role; 7 | }); 8 | 9 | module.exports.NONE = []; 10 | module.exports.ALL = ALL_ROLES; 11 | module.exports.SUPERUSER = ALL_ROLES[0]; 12 | module.exports.ORGANIZERS = ['ADMIN', 'STAFF']; 13 | module.exports.PROFESSIONALS = ['SPONSOR', 'MENTOR']; 14 | module.exports.NON_PROFESSIONALS = ['ADMIN', 'STAFF', 'VOLUNTEER', 'ATTENDEE']; 15 | module.exports.HOSTS = ['ADMIN', 'STAFF', 'VOLUNTEER']; 16 | module.exports.COMMON = ['SPONSOR', 'MENTOR', 'VOLUNTEER', 'ATTENDEE']; 17 | 18 | /** 19 | * Determines whether or not a given role is in a certain role group 20 | * @param {Array} group a group of roles 21 | * @param {String} role a role to verify 22 | * @return {Boolean} whether or not role is in the given group 23 | */ 24 | module.exports.isIn = (group, role) => _.includes(group, role); 25 | 26 | /** 27 | * Ensures that the provided role is in ALL_ROLES 28 | * @param {String} role the value to check 29 | * @return {Boolean} true when the role is valid 30 | * @throws TypeError when the role is invalid 31 | */ 32 | module.exports.verifyRole = (role) => { 33 | if (!module.exports.isIn(ALL_ROLES, role)) { 34 | throw new TypeError(role + ' is not a valid role'); 35 | } 36 | 37 | return true; 38 | }; 39 | -------------------------------------------------------------------------------- /api/v1/utils/scopes.js: -------------------------------------------------------------------------------- 1 | const ALL_SCOPES = ['AUTH', 'OTHER']; 2 | 3 | module.exports.AUTH = 'AUTH'; 4 | module.exports.OTHER = 'OTHER'; 5 | module.exports.ALL = ALL_SCOPES; 6 | -------------------------------------------------------------------------------- /api/v1/utils/storage.js: -------------------------------------------------------------------------------- 1 | const ctx = require('ctx'); 2 | const config = ctx.config(); 3 | 4 | module.exports.buckets = {}; 5 | 6 | Object.keys(config.storage.buckets) 7 | .forEach((key) => { 8 | module.exports.buckets[key] = config.storage.buckets[key] + config.storage.bucketExtension; 9 | }); 10 | -------------------------------------------------------------------------------- /api/v1/utils/time.js: -------------------------------------------------------------------------------- 1 | /* jshint esversion: 6 */ 2 | 3 | const MILLISECONDS_PER_SECOND = 1000; 4 | 5 | const milliseconds = require('ms'); 6 | 7 | module.exports.unix = () => Math.floor(Date.now() / MILLISECONDS_PER_SECOND); 8 | 9 | module.exports.toMilliseconds = (description) => milliseconds(description); 10 | 11 | module.exports.secondsToHHMMSS = (numSeconds) => { 12 | numSeconds = Number(numSeconds); 13 | const h = Math.floor(numSeconds / 3600); 14 | const m = Math.floor(numSeconds % 3600 / 60); 15 | const s = Math.floor(numSeconds % 3600 % 60); 16 | return ((h > 0 ? h + ':' + (m < 10 ? '0' : '') : '') + m + ':' + (s < 10 ? '0' : '') + s); 17 | }; 18 | 19 | module.exports.convertISOTimeToMySQLTime = (isotime) => new Date(isotime).toISOString().substring(0, 19).replace('T', ' '); 20 | -------------------------------------------------------------------------------- /api/v1/utils/validators.js: -------------------------------------------------------------------------------- 1 | const checkit = require('checkit'); 2 | const _ = require('lodash'); 3 | const _Promise = require('bluebird'); 4 | 5 | module.exports.nested = (validations, parentName) => function(value) { 6 | return checkit(validations) 7 | .run(value) 8 | .catch(checkit.Error, (error) => { 9 | const specificError = error.errors[error.keys()[0]]; 10 | specificError.key = parentName + '.' + specificError.key; 11 | 12 | throw specificError; 13 | }); 14 | }; 15 | 16 | module.exports.array = (validator) => function(value) { 17 | return _Promise.all( 18 | _.map(value, validator)) 19 | .then(() => true); 20 | }; 21 | 22 | module.exports.in = (array, errorMessage) => (value) => { 23 | if (!_.includes(array, value)) { 24 | errorMessage = (errorMessage) ? errorMessage : 'is not a valid option '; 25 | throw new TypeError(value + ' ' + errorMessage); 26 | } 27 | 28 | return true; 29 | }; 30 | 31 | module.exports.upTo = (condition, count, errorMessage) => (value) => { 32 | if (_.filter(value, condition).length > count) { 33 | errorMessage = (errorMessage) ? errorMessage : 'Too many values given'; 34 | throw new TypeError(errorMessage); 35 | } 36 | 37 | return true; 38 | }; 39 | 40 | module.exports.date = (date) => !!Date.parse(date); 41 | -------------------------------------------------------------------------------- /config/development.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "isDevelopment": true, 3 | "port": 8080, 4 | "auth": { 5 | "expiration": "7d", 6 | "secret": "VERY_SPECIAL_KEY", 7 | "github": { 8 | "id": null, 9 | "secret": null, 10 | "useragent": "HackIllinois", 11 | "mobileRedirect": "https://hackillinois.org/auth/mobile" 12 | }, 13 | "headers": { 14 | "all": "Authorization", 15 | "impersonation": "Admin-User-Override" 16 | }, 17 | "types": { 18 | "basic": "Basic", 19 | "bearer": "Bearer" 20 | } 21 | }, 22 | "aws": { 23 | "enabled": 0, 24 | "defaults": { 25 | "credentials": null, 26 | "region": "us-east-1", 27 | "sslEnabled": true 28 | } 29 | }, 30 | "database": { 31 | "primary": { 32 | "host": "127.0.0.1", 33 | "name": "hackillinois", 34 | "password": "pass123", 35 | "port": "3306", 36 | "user": "root", 37 | "pool": { 38 | "min": 0, 39 | "max": 7500, 40 | "idleTimeout": "5s" 41 | } 42 | } 43 | }, 44 | "mail": { 45 | "key": null, 46 | "sinkhole": ".sink.sparkpostmail.com", 47 | "whitelistedDomains": [ "@hackillinois.org" ], 48 | "whitelistedLists": [ "test" ], 49 | "lists": { 50 | "test": { "name": "test", "id": "test" }, 51 | "idlers": { "name": "idlers", "id": "idlers-2017" }, 52 | "applicants": { "name": "applicants", "id": "applicants-2017" }, 53 | "accepted": { "name": "accepted", "id": "accepted-2017" }, 54 | "lightningTalks": { "name": "lightning_talks", "id": "lightning-talks-2017" }, 55 | "waitlisted": { "name": "waitlisted", "id": "waitlisted-2017" }, 56 | "attendees": { "name": "attendees", "id": "attendees-2017" }, 57 | "admins": { "name": "admins", "id": "admins-2017" }, 58 | "staff": { "name": "staff", "id": "staff-2017" }, 59 | "sponsors": { "name": "sponsors", "id": "sponsors-2017" }, 60 | "mentors": { "name": "mentors", "id": "mentors-2017" }, 61 | "volunteers": { "name": "volunteers", "id": "volunteers-2017" }, 62 | "wave1": { "name": "wave_1", "id": "wave-1-2017" }, 63 | "wave2": { "name": "wave_2", "id": "wave-2-2017" }, 64 | "wave3": { "name": "wave_3", "id": "wave-3-2017" }, 65 | "wave4": { "name": "wave_4", "id": "wave-4-2017" }, 66 | "wave5": { "name": "wave_5", "id": "wave-5-2017" }, 67 | "rejected": { "name": "rejected", "id": "rejected-2017"} 68 | }, 69 | "templates": { 70 | "acceptance": "acceptance", 71 | "passwordReset": "password_reset", 72 | "registrationConfirmation": "registration_confirmation", 73 | "registrationUpdate": "registration_update", 74 | "rsvpConfirmation": "rsvp_confirmation", 75 | "rsvpUpdate": "rsvp_update", 76 | "slackInvite": "slack_invite", 77 | "test": "test" 78 | } 79 | }, 80 | "logs": { 81 | "streamPrefix": "instances", 82 | "groupName": "api-dev", 83 | "request": { 84 | "blacklist": [ "password" ] 85 | } 86 | }, 87 | "redis": { 88 | "host": "127.0.0.1", 89 | "port": 6379 90 | }, 91 | "storage": { 92 | "bucketExtension": "-development", 93 | "buckets": { 94 | "resumes": "hackillinois-resumes" 95 | } 96 | }, 97 | "superuser": { 98 | "email": "admin@example.com", 99 | "password": "ABCD1234!" 100 | }, 101 | "token": { 102 | "expiration": { 103 | "DEFAULT": "7d", 104 | "AUTH": "7d" 105 | } 106 | }, 107 | "limit": { 108 | "count": 50, 109 | "window": 60 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /config/production.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "isDevelopment": false, 3 | "port": 8080, 4 | "auth": { 5 | "expiration": "7d", 6 | "secret": "VERY_SPECIAL_KEY", 7 | "github": { 8 | "id": null, 9 | "secret": null, 10 | "useragent": "HackIllinois", 11 | "mobileRedirect": "https://hackillinois.org/auth/mobile" 12 | }, 13 | "headers": { 14 | "all": "Authorization", 15 | "impersonation": "Admin-User-Override" 16 | }, 17 | "types": { 18 | "basic": "Basic", 19 | "bearer": "Bearer" 20 | } 21 | }, 22 | "aws": { 23 | "enabled": 0, 24 | "defaults": { 25 | "credentials": null, 26 | "region": "us-east-1", 27 | "sslEnabled": true 28 | } 29 | }, 30 | "database": { 31 | "primary": { 32 | "host": "127.0.0.1", 33 | "name": "hackillinois", 34 | "password": "pass123", 35 | "port": "3306", 36 | "user": "root", 37 | "pool": { 38 | "min": 0, 39 | "max": 7500, 40 | "idleTimeout": "5s" 41 | } 42 | } 43 | }, 44 | "mail": { 45 | "key": null, 46 | "sinkhole": ".sink.sparkpostmail.com", 47 | "whitelistedDomains": [ "@hackillinois.org" ], 48 | "whitelistedLists": [ "test" ], 49 | "lists": { 50 | "test": { "name": "test", "id": "test" }, 51 | "idlers": { "name": "idlers", "id": "idlers-2018" }, 52 | "applicants": { "name": "applicants", "id": "applicants-2018" }, 53 | "accepted": { "name": "accepted", "id": "accepted-2018" }, 54 | "lightningTalks": { "name": "lightning_talks", "id": "lightning-talks-2018" }, 55 | "waitlisted": { "name": "waitlisted", "id": "waitlisted-2018" }, 56 | "attendees": { "name": "attendees", "id": "attendees-2018" }, 57 | "admins": { "name": "admins", "id": "admins-2018" }, 58 | "staff": { "name": "staff", "id": "staff-2018" }, 59 | "sponsors": { "name": "sponsors", "id": "sponsors-2018" }, 60 | "mentors": { "name": "mentors", "id": "mentors-2018" }, 61 | "volunteers": { "name": "volunteers", "id": "volunteers-2018" }, 62 | "wave1": { "name": "wave_1", "id": "wave-1-2018" }, 63 | "wave2": { "name": "wave_2", "id": "wave-2-2018" }, 64 | "wave3": { "name": "wave_3", "id": "wave-3-2018" }, 65 | "wave4": { "name": "wave_4", "id": "wave-4-2018" }, 66 | "wave5": { "name": "wave_5", "id": "wave-5-2018" }, 67 | "rejected": { "name": "rejected", "id": "rejected-2018"} 68 | }, 69 | "templates": { 70 | "acceptance": "acceptance", 71 | "passwordReset": "password_reset", 72 | "registrationConfirmation": "registration_confirmation", 73 | "registrationUpdate": "registration_update", 74 | "rsvpConfirmation": "rsvp_confirmation", 75 | "rsvpUpdate": "rsvp_update", 76 | "slackInvite": "slack_invite", 77 | "test": "test" 78 | } 79 | }, 80 | "logs": { 81 | "streamPrefix": "instances", 82 | "groupName": "api", 83 | "request": { 84 | "blacklist": [ "password" ] 85 | } 86 | }, 87 | "redis": { 88 | "host": "127.0.0.1", 89 | "port": 6379 90 | }, 91 | "storage": { 92 | "bucketExtension": "-2018", 93 | "buckets": { 94 | "resumes": "hackillinois-resumes" 95 | } 96 | }, 97 | "superuser": { 98 | "email": "admin@example.com", 99 | "password": "ABCD1234!" 100 | }, 101 | "token": { 102 | "expiration": { 103 | "DEFAULT": "7d", 104 | "AUTH": "7d" 105 | } 106 | }, 107 | "limit": { 108 | "count": 50, 109 | "window": 60 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /config/test.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "isTest": true, 3 | "port": 8080, 4 | "auth": { 5 | "expiration": "7d", 6 | "secret": "VERY_SPECIAL_KEY", 7 | "github": { 8 | "id": null, 9 | "secret": null, 10 | "useragent": "HackIllinois" 11 | }, 12 | "headers": { 13 | "all": "Authorization", 14 | "impersonation": "Admin-User-Override" 15 | }, 16 | "types": { 17 | "basic": "Basic", 18 | "bearer": "Bearer" 19 | } 20 | }, 21 | "aws": { 22 | "enabled": 0, 23 | "defaults": { 24 | "credentials": null, 25 | "region": "us-east-1", 26 | "sslEnabled": true 27 | } 28 | }, 29 | "database": { 30 | "primary": { 31 | "host": "127.0.0.1", 32 | "name": "hackillinois", 33 | "password": "pass123", 34 | "port": "3306", 35 | "user": "root", 36 | "pool": { 37 | "min": 0, 38 | "max": 7500, 39 | "idleTimeout": "5s" 40 | } 41 | } 42 | }, 43 | "mail": { 44 | "key": null, 45 | "sinkhole": ".sink.sparkpostmail.com", 46 | "whitelistedDomains": [ "@hackillinois.org" ], 47 | "whitelistedLists": [ "test" ], 48 | "lists": { 49 | "test": { "name": "test", "id": "test" }, 50 | "idlers": { "name": "idlers", "id": "idlers-2017" }, 51 | "applicants": { "name": "applicants", "id": "applicants-2017" }, 52 | "accepted": { "name": "accepted", "id": "accepted-2017" }, 53 | "lightningTalks": { "name": "lightning_talks", "id": "lightning-talks-2017" }, 54 | "waitlisted": { "name": "waitlisted", "id": "waitlisted-2017" }, 55 | "attendees": { "name": "attendees", "id": "attendees-2017" }, 56 | "admins": { "name": "admins", "id": "admins-2017" }, 57 | "staff": { "name": "staff", "id": "staff-2017" }, 58 | "sponsors": { "name": "sponsors", "id": "sponsors-2017" }, 59 | "mentors": { "name": "mentors", "id": "mentors-2017" }, 60 | "volunteers": { "name": "volunteers", "id": "volunteers-2017" }, 61 | "wave1": { "name": "wave_1", "id": "wave-1-2017" }, 62 | "wave2": { "name": "wave_2", "id": "wave-2-2017" }, 63 | "wave3": { "name": "wave_3", "id": "wave-3-2017" }, 64 | "wave4": { "name": "wave_4", "id": "wave-4-2017" }, 65 | "wave5": { "name": "wave_5", "id": "wave-5-2017" }, 66 | "rejected": { "name": "rejected", "id": "rejected-2017"} 67 | }, 68 | "templates": { 69 | "acceptance": "acceptance", 70 | "passwordReset": "password_reset", 71 | "registrationConfirmation": "registration_confirmation", 72 | "registrationUpdate": "registration_update", 73 | "rsvpConfirmation": "rsvp_confirmation", 74 | "rsvpUpdate": "rsvp_update", 75 | "slackInvite": "slack_invite", 76 | "test": "test" 77 | } 78 | }, 79 | "logs": { 80 | "streamPrefix": "instances", 81 | "groupName": "api-dev", 82 | "request": { 83 | "blacklist": [ "password" ] 84 | } 85 | }, 86 | "redis": { 87 | "host": "127.0.0.1", 88 | "port": 6379 89 | }, 90 | "storage": { 91 | "bucketExtension": "-development", 92 | "buckets": { 93 | "resumes": "hackillinois-resumes" 94 | } 95 | }, 96 | "superuser": { 97 | "email": "admin@example.com", 98 | "password": "ABCD1234!" 99 | }, 100 | "token": { 101 | "expiration": { 102 | "DEFAULT": "7d", 103 | "AUTH": "7d" 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /ctx/ctx.js: -------------------------------------------------------------------------------- 1 | function Ctx() { 2 | //relative to hackillinois/api/node_modules/ctx 3 | this._config = require('../../api/config'); 4 | this._logger = require('../../api/logging'); 5 | this._database = require('../../api/database'); 6 | this._cache = require('../../api/cache'); 7 | } 8 | 9 | Ctx.prototype.constructor = Ctx; 10 | 11 | Ctx.prototype.config = function() { 12 | return this._config; 13 | }; 14 | 15 | Ctx.prototype.logger = function() { 16 | return this._logger; 17 | }; 18 | 19 | Ctx.prototype.database = function() { 20 | return this._database; 21 | }; 22 | 23 | Ctx.prototype.cache = function() { 24 | return this._cache; 25 | }; 26 | 27 | module.exports = new Ctx(); 28 | -------------------------------------------------------------------------------- /ctx/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ctx", 3 | "version": "0.0.1", 4 | "description": "context module", 5 | "main": "ctx.js" 6 | } 7 | -------------------------------------------------------------------------------- /database/README.md: -------------------------------------------------------------------------------- 1 | # HackIllinois Database 2 | 3 | ### Migrations 4 | We'll be using FlywayDB to keep our database schema in sync (you should have this command line utility from when you set up your database initially). The `migration` folder will keep track of all migrations, with each migration following this naming convention: 5 | > [YYYYmmDD]\_[HHMM]\__[description].sql 6 | 7 | A migration that creates a new table called `foo` on 6/15/2016 at 12:00 PM might look like this: 8 | 9 | > 20161506\_1200__createFooModel.sql 10 | 11 | Since this is painful to type for every migration, use the shell script called `migration.sh` to generate this file name. To generate the above migration file name, run: 12 | 13 | > migration.sh createFooModel 14 | 15 | ### Reverts 16 | Although FlywayDB does not have the ability to revert migrations for us, having a quick way to revert a previous migration may be useful in production hotfixes. The naming convention is exactly the same as that which is above, but should be placed in the `revert` folder, and have a `.revert.sql` ending: 17 | 18 | > [YYYYmmDD]\_[HHMM]\__[description].revert.sql 19 | 20 | You can typically just use the same output from the `migration.sh` script to name your revert, taking care to add the `.revert.sql` filename ending. 21 | 22 | ### Table and Column Naming 23 | Let's have tables always represent the data that they hold per row, as a plural entity. Columns should be singular entities. Additionally, all letters should be lowercase. For example, always make a table like `foos`, not like `foo`, `Foo`, or `Foos`. 24 | 25 | In any case, make sure to exchange places where spaces would normally be inserted with underscores. 26 | 27 | ### Cascading ON UPDATE/DELETE 28 | As a reference, please read the marked answer to [this StackOverflow response](http://stackoverflow.com/questions/6720050/foreign-key-varraints-when-to-use-on-update-and-on-delete). 29 | 30 | Overall, it's saying that you usually (but not always) want to cascade ON UPDATE. However, for ON DELETE operations, you need to think carefully. 31 | 32 | For instance, if the foreign key is from a user to his/her organization, cascading on delete would be very bad -- deleting the organization would delete 33 | the user as well! 34 | 35 | If you're not sure which to pick, make your best guess and leave a justification with a TODO for your code reviewer(s) to check out. 36 | 37 | ### Deploying Schema Changes 38 | 39 | In development, execute `npm run dev-migrations` from the root of the project directory. Equivalently, before deploying 40 | to production, execute `npm run prod-migrations`. 41 | 42 | If you see any errors, such as an inability to access the database, make sure you have set up the schema correctly and that you have set any necessary MySQL environment variables listed in the configuration section above. 43 | -------------------------------------------------------------------------------- /database/flyway.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if [ "$#" -ne 2 ]; then 6 | echo "Usage: $0 " 7 | echo "Type 'flyway' for a list of available commands" 8 | echo "Target is any available environment (usually 'production' or 'development')" 9 | exit 2 10 | fi 11 | 12 | cd "$(dirname ${BASH_SOURCE[0]})" 13 | if [ -f "../config/$2.json" ]; then 14 | configuration=`echo -e $(cat ../config/$2.json)` 15 | FLYWAY_USERNAME=`python -c "import json; print(json.loads('$configuration')['database']['primary']['user'])"` 16 | FLYWAY_PASSWORD=`python -c "import json; print(json.loads('$configuration')['database']['primary']['password'])"` 17 | FLYWAY_HOSTNAME=`python -c "import json; print(json.loads('$configuration')['database']['primary']['host'])"` 18 | FLYWAY_PORT=`python -c "import json; print(json.loads('$configuration')['database']['primary']['port'])"` 19 | FLYWAY_NAME=`python -c "import json; print(json.loads('$configuration')['database']['primary']['name'])"` 20 | else 21 | echo "no configuration found for target '$2'. defaulting to environment" 22 | fi 23 | 24 | FLYWAY_USERNAME=${DB_USERNAME:-$FLYWAY_USERNAME} 25 | FLYWAY_PASSWORD=${DB_PASSWORD:-$FLYWAY_PASSWORD} 26 | FLYWAY_HOSTNAME=${DB_HOSTNAME:-$FLYWAY_HOSTNAME} 27 | FLYWAY_PORT=${DB_PORT:-$FLYWAY_PORT} 28 | FLYWAY_NAME=${DB_NAME:-$FLYWAY_NAME} 29 | 30 | flyway -user=$FLYWAY_USERNAME -password=$FLYWAY_PASSWORD -url=jdbc:mysql://$FLYWAY_HOSTNAME:$FLYWAY_PORT/$FLYWAY_NAME -locations=filesystem:"$PWD/migration" -baselineOnMigrate=true -sqlMigrationSuffix=.sql $1 31 | -------------------------------------------------------------------------------- /database/migration.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | now=$(date +"%Y%m%d_%H%M") 4 | echo "V"$now"__"$1".sql" 5 | -------------------------------------------------------------------------------- /database/migration/V1_0__hackillinois-2017.sql: -------------------------------------------------------------------------------- 1 | ##### 2 | # FlywayDB baseline migration 3 | ##### 4 | -------------------------------------------------------------------------------- /database/migration/V20160614_1845__createUsersTable.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `users` ( 2 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, 3 | `github_handle` VARCHAR(50), 4 | `email` VARCHAR(255) NOT NULL, 5 | `password` CHAR(60), 6 | `created` DATETIME NULL DEFAULT CURRENT_TIMESTAMP, 7 | `updated` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 8 | PRIMARY KEY (`id`), 9 | UNIQUE INDEX `email_UNIQUE` (`email` ASC), 10 | UNIQUE INDEX `github_handle_UNIQUE` (`github_handle` ASC) 11 | ); 12 | 13 | CREATE TABLE `user_roles` ( 14 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, 15 | `user_id` INT UNSIGNED NOT NULL, 16 | `role` ENUM('ADMIN', 'STAFF', 'MENTOR', 'SPONSOR', 'ATTENDEE', 'VOLUNTEER') NOT NULL, 17 | `active` TINYINT(1) UNSIGNED NULL DEFAULT 0, 18 | PRIMARY KEY (`id`), 19 | INDEX `fk_users_user_roles_id_idx` (`user_id` ASC), 20 | CONSTRAINT `fk_users_user_roles_id` 21 | FOREIGN KEY (`user_id`) 22 | REFERENCES `users` (`id`) 23 | ON DELETE NO ACTION 24 | ON UPDATE NO ACTION); 25 | -------------------------------------------------------------------------------- /database/migration/V20160712_1603__createMailingListTables.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `mailing_lists` ( 2 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, 3 | `name` VARCHAR(255) NOT NULL, 4 | `sent` TINYINT(1) NULL DEFAULT 0, 5 | PRIMARY KEY (`id`), 6 | UNIQUE INDEX `mailing_lists_name_UNIQUE` (`name` ASC)); 7 | 8 | CREATE TABLE `mailing_lists_users` ( 9 | `ID` INT UNSIGNED NOT NULL AUTO_INCREMENT, 10 | `mailing_list_id` INT UNSIGNED NOT NULL, 11 | `user_id` INT UNSIGNED NOT NULL, 12 | PRIMARY KEY (`ID`), 13 | INDEX `fk_mailing_lists_users_user_id_idx` (`user_id` ASC), 14 | INDEX `fk_mailing_lists_users_mailing_list_id_idx` (`mailing_list_id` ASC), 15 | CONSTRAINT `fk_mailing_lists_users_user_id` 16 | FOREIGN KEY (`user_id`) 17 | REFERENCES `users` (`id`) 18 | ON DELETE NO ACTION 19 | ON UPDATE NO ACTION, 20 | CONSTRAINT `fk_mailing_lists_users_mailing_list_id` 21 | FOREIGN KEY (`mailing_list_id`) 22 | REFERENCES `mailing_lists` (`id`) 23 | ON DELETE NO ACTION 24 | ON UPDATE NO ACTION); 25 | 26 | INSERT INTO `mailing_lists` (`name`) VALUES ('idlers'); 27 | INSERT INTO `mailing_lists` (`name`) VALUES ('applicants'); 28 | INSERT INTO `mailing_lists` (`name`) VALUES ('accepted'); 29 | INSERT INTO `mailing_lists` (`name`) VALUES ('waitlisted'); 30 | INSERT INTO `mailing_lists` (`name`) VALUES ('attendees'); 31 | INSERT INTO `mailing_lists` (`name`) VALUES ('admins'); 32 | INSERT INTO `mailing_lists` (`name`) VALUES ('staff'); 33 | INSERT INTO `mailing_lists` (`name`) VALUES ('sponsors'); 34 | INSERT INTO `mailing_lists` (`name`) VALUES ('mentors'); 35 | INSERT INTO `mailing_lists` (`name`) VALUES ('volunteers'); 36 | INSERT INTO `mailing_lists` (`name`) VALUES ('test'); 37 | -------------------------------------------------------------------------------- /database/migration/V20160724_1239__createPasswordTokenTable.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `tokens` ( 2 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, 3 | `user_id` INT UNSIGNED NOT NULL, 4 | `type` ENUM('AUTH', 'OTHER') NOT NULL, 5 | `value` VARCHAR(255) NOT NULL, 6 | `created` DATETIME NULL DEFAULT CURRENT_TIMESTAMP, 7 | PRIMARY KEY (`id`), 8 | INDEX `fk_token_users_user_id_idx` (`user_id` ASC), 9 | CONSTRAINT `fk_token_users_user_id` 10 | FOREIGN KEY (`user_id`) 11 | REFERENCES `users` (`id`) 12 | ON DELETE NO ACTION 13 | ON UPDATE NO ACTION 14 | ); -------------------------------------------------------------------------------- /database/migration/V20160727_2020__createUploadsTable.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `uploads` ( 2 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, 3 | `owner_id` INT UNSIGNED NOT NULL, 4 | `key` CHAR(36) NOT NULL, 5 | `bucket` VARCHAR(255) NOT NULL, 6 | `created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | `updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 8 | PRIMARY KEY (`id`), 9 | INDEX `fk_uploads_owner_id_idx` (`owner_id` ASC), 10 | CONSTRAINT `fk_uploads_owner_id` 11 | FOREIGN KEY (`owner_id`) 12 | REFERENCES `users` (`id`) 13 | ON DELETE NO ACTION 14 | ON UPDATE NO ACTION); 15 | -------------------------------------------------------------------------------- /database/migration/V20161004_0917__createMentors.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `mentors` ( 2 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, 3 | `user_id` INT UNSIGNED NOT NULL, 4 | `first_name` VARCHAR(255) NOT NULL, 5 | `last_name` VARCHAR(255) NOT NULL, 6 | `shirt_size` ENUM('S', 'M', 'L', 'XL') NOT NULL, 7 | `github` VARCHAR(50) NULL, 8 | `location` VARCHAR(255) NOT NULL, 9 | `summary` VARCHAR(255) NOT NULL, 10 | `occupation` VARCHAR(100) NOT NULL, 11 | `status` ENUM('ACCEPTED', 'WAITLISTED', 'REJECTED', 'PENDING') NOT NULL DEFAULT 'PENDING', 12 | PRIMARY KEY (`id`), 13 | INDEX `fk_mentors_user_id_idx` (`user_id` ASC), 14 | CONSTRAINT `fk_mentors_user_id` 15 | FOREIGN KEY (`user_id`) 16 | REFERENCES `users` (`id`) 17 | ON DELETE NO ACTION 18 | ON UPDATE NO ACTION 19 | ); 20 | 21 | CREATE TABLE `mentor_project_ideas` ( 22 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, 23 | `mentor_id` INT UNSIGNED NOT NULL, 24 | `link` VARCHAR(255) NOT NULL, 25 | `contributions` VARCHAR(255) NOT NULL, 26 | `ideas` VARCHAR(255) NOT NULL, 27 | PRIMARY KEY (`id`), 28 | INDEX `fk_mentor_project_ideas_mentor_id_idx` (`mentor_id` ASC), 29 | CONSTRAINT `fk_mentor_project_ideas_mentor_id` 30 | FOREIGN KEY (`mentor_id`) 31 | REFERENCES `mentors` (`id`) 32 | ON DELETE NO ACTION 33 | ON UPDATE NO ACTION 34 | ); 35 | -------------------------------------------------------------------------------- /database/migration/V20161105_1750__createProjects.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `projects` ( 2 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, 3 | `name` VARCHAR(100) NOT NULL, 4 | `description` VARCHAR(200) NOT NULL, 5 | `repo` VARCHAR(255) NULL, 6 | `is_published` TINYINT(1) NOT NULL DEFAULT 0, 7 | PRIMARY KEY (`id`) 8 | ); -------------------------------------------------------------------------------- /database/migration/V20161105_2059__createProjectMentors.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `project_mentors` ( 2 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, 3 | `project_id` INT UNSIGNED NOT NULL, 4 | `mentor_id` INT UNSIGNED NOT NULL, 5 | PRIMARY KEY (`id`), 6 | INDEX `fk_project_mentors_projects_id_idx` (`project_id` ASC), 7 | INDEX `fk_project_mentors_mentors_id_idx` (`mentor_id` ASC), 8 | CONSTRAINT `project_id` 9 | FOREIGN KEY (`mentor_id`) 10 | REFERENCES `projects` (`id`) 11 | ON DELETE NO ACTION 12 | ON UPDATE NO ACTION, 13 | CONSTRAINT `mentor_id` 14 | FOREIGN KEY (`mentor_id`) 15 | REFERENCES `mentors` (`id`) 16 | ON DELETE NO ACTION 17 | ON UPDATE NO ACTION 18 | ); 19 | -------------------------------------------------------------------------------- /database/migration/V20161220_1152__createEcosystemModel.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `ecosystems` ( 2 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, 3 | `name` VARCHAR(100) NOT NULL, 4 | PRIMARY KEY (`id`), 5 | UNIQUE KEY `name_UNIQUE` (`name`) 6 | ); 7 | -------------------------------------------------------------------------------- /database/migration/V20170212_1950__addAnnouncements.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `announcements` ( 2 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, 3 | `title` VARCHAR(255) NOT NULL, 4 | `description` VARCHAR(1000) NOT NULL, 5 | `created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 6 | PRIMARY KEY (`id`)); 7 | -------------------------------------------------------------------------------- /database/migration/V20170216_1324__createCheckInTable.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `checkins` ( 2 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, 3 | `user_id` INT UNSIGNED NOT NULL, 4 | `time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP, 5 | `location` ENUM('SIEBEL', 'ECEB', 'DCL') NOT NULL, 6 | `swag` TINYINT(1) NOT NULL DEFAULT 0, 7 | PRIMARY KEY (`id`), 8 | UNIQUE INDEX `user_id_UNIQUE` (`user_id` ASC), 9 | CONSTRAINT `fk_checkins_user_id` 10 | FOREIGN KEY (`user_id`) 11 | REFERENCES `users` (`id`) 12 | ON DELETE NO ACTION 13 | ON UPDATE NO ACTION 14 | ); 15 | -------------------------------------------------------------------------------- /database/migration/V20170216_1919__addEventTracking.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `tracking_events` ( 2 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, 3 | `name` VARCHAR(255) NOT NULL, 4 | `created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 5 | `duration` INTEGER UNSIGNED NOT NULL, 6 | `count` SMALLINT UNSIGNED NOT NULL DEFAULT 0, 7 | PRIMARY KEY (`id`), 8 | UNIQUE INDEX `name_UNIQUE` (`name` ASC) 9 | ); -------------------------------------------------------------------------------- /database/migration/V20170216_1920__addNetworkCredentials.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `network_credentials` ( 2 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, 3 | `user_id` INT UNSIGNED NULL, 4 | `account` VARCHAR(25) NOT NULL, 5 | `password` VARCHAR(25) NOT NULL, 6 | `assigned` TINYINT(1) NOT NULL DEFAULT 0, 7 | PRIMARY KEY (`id`), 8 | INDEX `fk_network_credentials_user_id_idx` (`user_id` ASC), 9 | CONSTRAINT `fk_network_credentials_user_id` 10 | FOREIGN KEY (`user_id`) 11 | REFERENCES `users` (`id`) 12 | ON DELETE NO ACTION 13 | ON UPDATE NO ACTION 14 | ); 15 | -------------------------------------------------------------------------------- /database/migration/V20170222_0021__addEventAPI.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `locations` ( 2 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, 3 | `name` VARCHAR(255) NOT NULL, 4 | `longitude` DOUBLE NOT NULL, 5 | `latitude` DOUBLE NOT NULL, 6 | UNIQUE INDEX `name_UNIQUE` (`name` ASC), 7 | PRIMARY KEY (`id`)); 8 | 9 | 10 | CREATE TABLE `events` ( 11 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, 12 | `name` VARCHAR(255) NOT NULL, 13 | `description` VARCHAR(2047) NOT NULL, 14 | `start_time` DATETIME NOT NULL, 15 | `end_time` DATETIME NOT NULL, 16 | `tag` ENUM('PRE_EVENT', 'POST_EVENT') NOT NULL, 17 | UNIQUE INDEX `unique_EventIndex` (`start_time` ASC, `end_time` ASC, `name` ASC, `description` ASC), 18 | PRIMARY KEY (`id`) 19 | ); 20 | 21 | CREATE TABLE `event_locations` ( 22 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, 23 | `event_id` INT UNSIGNED NOT NULL, 24 | `location_id` INT UNSIGNED NOT NULL, 25 | PRIMARY KEY (`id`), 26 | INDEX `fk_event_locations_event_id_idx` (`event_id` ASC), 27 | INDEX `fk_event_locations_location_id_idx` (`location_id` ASC), 28 | CONSTRAINT `fk_event_locations_event_id_idx` 29 | FOREIGN KEY (`event_id`) 30 | REFERENCES `events` (`id`) 31 | ON DELETE NO ACTION 32 | ON UPDATE NO ACTION, 33 | CONSTRAINT `fk_event_locations_location_id_idx` 34 | FOREIGN KEY (`location_id`) 35 | REFERENCES `locations` (`id`) 36 | ON DELETE NO ACTION 37 | ON UPDATE NO ACTION 38 | ); 39 | -------------------------------------------------------------------------------- /database/migration/V20180131_2100__removeRSVPType.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `attendee_rsvps` DROP COLUMN `type`; 2 | ALTER TABLE `attendee_rsvps` ADD CONSTRAINT `rsvp_UNIQUE` UNIQUE (`attendee_id`, `is_attending`); 3 | -------------------------------------------------------------------------------- /database/migration/V20180207_1335__changeRSVPUnique.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `attendee_rsvps` DROP INDEX `rsvp_UNIQUE`; 2 | ALTER TABLE `attendee_rsvps` ADD CONSTRAINT `rsvp_UNIQUE` UNIQUE (`attendee_id`); 3 | -------------------------------------------------------------------------------- /database/migration/V20180218_2014__addEventFavorites.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `event_favorites` ( 2 | `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, 3 | `user_id` INT UNSIGNED NOT NULL, 4 | `event_id` INT UNSIGNED NOT NULL, 5 | PRIMARY KEY (`id`), 6 | INDEX `fk_event_favorites_user_id_idx` (`user_id` ASC), 7 | INDEX `fk_event_favorites_event_id_idx` (`event_id` ASC), 8 | CONSTRAINT `event_favorite_UNIQUE` UNIQUE (`user_id`, `event_id`), 9 | CONSTRAINT `fk_event_favorites_user_id` 10 | FOREIGN KEY (`user_id`) 11 | REFERENCES `users` (`id`) 12 | ON DELETE NO ACTION 13 | ON UPDATE NO ACTION, 14 | CONSTRAINT `fk_event_favorites_event_id` 15 | FOREIGN KEY (`event_id`) 16 | REFERENCES `events` (`id`) 17 | ON DELETE NO ACTION 18 | ON UPDATE NO ACTION 19 | ); 20 | -------------------------------------------------------------------------------- /database/migration/V20180222_1758__createRecruiterInterests.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `recruiter_interests` ( 2 | `app_id` INT UNSIGNED NOT NULL AUTO_INCREMENT, 3 | `recruiter_id` INT UNSIGNED NOT NULL, 4 | `attendee_id` INT UNSIGNED NOT NULL, 5 | `comments` VARCHAR(255) NOT NULL, 6 | `favorite` TINYINT(1) UNSIGNED NOT NULL, 7 | `created` DATETIME NULL DEFAULT CURRENT_TIMESTAMP, 8 | `updated` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 9 | PRIMARY KEY (`app_id`), 10 | CONSTRAINT `fk_applications_recruiter_id` 11 | FOREIGN KEY (`recruiter_id`) 12 | REFERENCES `users` (`id`) 13 | ON DELETE NO ACTION 14 | ON UPDATE NO ACTION, 15 | CONSTRAINT `fk_applications_attendee_id` 16 | FOREIGN KEY (`attendee_id`) 17 | REFERENCES `attendees` (`id`) 18 | ON DELETE NO ACTION 19 | ON UPDATE NO ACTION 20 | ); 21 | -------------------------------------------------------------------------------- /database/migration/V20180223_0035__addRecruiterUniqueIndex.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `recruiter_interests` ADD UNIQUE INDEX(`recruiter_id`, `attendee_id`); 2 | -------------------------------------------------------------------------------- /database/revert/V20160614_1845__createUsersTable.revert.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE `user_roles`; 2 | DROP TABLE `users`; 3 | -------------------------------------------------------------------------------- /database/revert/V20160712_1603__createMailingListTables.revert.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE `mailing_lists_users`; 2 | DROP TABLE `mailing_lists`; 3 | -------------------------------------------------------------------------------- /database/revert/V20160724_1239__createPasswordTokenTable.revert.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE `tokens`; 2 | -------------------------------------------------------------------------------- /database/revert/V20160727_2020__createUploadsTable.revert.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE `uploads`; 2 | -------------------------------------------------------------------------------- /database/revert/V20161004_0917__createMentors.revert.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE `mentor_project_ideas`; 2 | DROP TABLE `mentors`; 3 | -------------------------------------------------------------------------------- /database/revert/V20161105_1750__createProjects.revert.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE `projects`; 2 | -------------------------------------------------------------------------------- /database/revert/V20161105_2059__createProjectMentors.revert.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE `project_mentors`; 2 | -------------------------------------------------------------------------------- /database/revert/V20161108_0927__createAttendees.revert.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE `attendee_rsvps`; 2 | DROP TABLE `attendee_extra_infos`; 3 | DROP TABLE `attendee_long_form`; 4 | DROP TABLE `attendee_os_contributor`; 5 | DROP TABLE `attendee_requested_collaborators`; 6 | 7 | DELETE FROM `mailing_lists` WHERE `name`='wave_1'; 8 | DELETE FROM `mailing_lists` WHERE `name`='wave_2'; 9 | DELETE FROM `mailing_lists` WHERE `name`='wave_3'; 10 | DELETE FROM `mailing_lists` WHERE `name`='wave_4'; 11 | DELETE FROM `mailing_lists` WHERE `name`='wave_5'; 12 | DELETE FROM `mailing_lists` WHERE `name`='rejected'; 13 | DELETE FROM `mailing_lists` WHERE `name`='lightning_talks'; 14 | 15 | DROP TABLE `attendees`; 16 | -------------------------------------------------------------------------------- /database/revert/V20161220_1152__createEcosystemModel.revert.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE `ecosystems`; 2 | -------------------------------------------------------------------------------- /database/revert/V20170212_1950__addAnnouncements.revert.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE `announcements`; 2 | -------------------------------------------------------------------------------- /database/revert/V20170216_1324__createCheckInTable.revert.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE `checkins`; 2 | -------------------------------------------------------------------------------- /database/revert/V20170216_1919__addEventTracking.revert.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE `tracking_events`; -------------------------------------------------------------------------------- /database/revert/V20170216_1920__addNetworkCredentials.revert.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE `network_credentials`; 2 | -------------------------------------------------------------------------------- /database/revert/V20170222_0021_addEventAPI.revert.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE `locations`; 2 | DROP TABLE `events`; 3 | DROP TAbLE `event_locations`; -------------------------------------------------------------------------------- /database/revert/V20180131_2100__removeRSVPType.revert.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `attendee_rsvps` ADD COLUMN `type` enum('CREATE','CONTRIBUTE'); 2 | -------------------------------------------------------------------------------- /database/revert/V20180131_2152__createRecruiterInterestsModel.revert.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE `recruiter_interests`; 2 | -------------------------------------------------------------------------------- /database/revert/V20180218_2014__addEventFavorites.revert.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE `event_favorites`; 2 | -------------------------------------------------------------------------------- /database/revert/V20180223_0035__addRecruiterUniqueIndex.revert.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `recruiter_interests` DROP UNIQUE INDEX(`recruiter_id`, `attendee_id`); 2 | -------------------------------------------------------------------------------- /docs/Attendee-Statistics-Documentation.md: -------------------------------------------------------------------------------- 1 | ### Purpose 2 | 3 | Provides functionality for statistics. Registration and RSVP stats are cached for 10 minutes before being requeried, live event stats are cached for two minutes. 4 | 5 | --- 6 | 7 | **GET /v1/stats/all**
8 | Fetch all stats 9 | 10 | Headers
11 | 12 | | Header | Description | Required | 13 | | ------------- | --------------------- | --------- | 14 | | `Authorization` | a valid authentication token | Yes | 15 | 16 | URL Parameters
17 | None 18 | 19 | Request Parameters
20 | None 21 | 22 | Response 23 | ``` 24 | 25 | ``` 26 | 27 | Errors:
28 | 29 | | Error | Source | Cause | 30 | | ------------ | ------ | ------ | 31 | | InvalidHeaderError | `Authorization` | the authentication token was invalid or absent | 32 | 33 | --- 34 | 35 | **GET /v1/stats/registration**
36 | Fetch all attendee stats from registration 37 | 38 | Headers
39 | 40 | | Header | Description | Required | 41 | | ------------- | --------------------- | --------- | 42 | | `Authorization` | a valid authentication token | Yes | 43 | 44 | URL Parameters
45 | None 46 | 47 | Request Parameters
48 | None 49 | 50 | Response 51 | ``` 52 | 53 | ``` 54 | 55 | Errors:
56 | 57 | | Error | Source | Cause | 58 | | ------------ | ------ | ------ | 59 | | InvalidHeaderError | `Authorization` | the authentication token was invalid or absent | 60 | 61 | --- 62 | 63 | **GET /v1/stats/rsvp**
64 | Fetch all stats from attendees who have been accepted and RSVP'ed yes 65 | 66 | Headers
67 | 68 | | Header | Description | Required | 69 | | ------------- | --------------------- | --------- | 70 | | `Authorization` | a valid authentication token | Yes | 71 | 72 | URL Parameters
73 | None 74 | 75 | Request Parameters
76 | None 77 | 78 | Response 79 | ``` 80 | 81 | ``` 82 | 83 | Errors:
84 | 85 | | Error | Source | Cause | 86 | | ------------ | ------ | ------ | 87 | | InvalidHeaderError | `Authorization` | the authentication token was invalid or absent | 88 | 89 | --- 90 | 91 | **GET /v1/stats/live**
92 | Fetch all ecosystems stats for checkedin attendedees, and tracked event stats 93 | 94 | Headers
95 | 96 | | Header | Description | Required | 97 | | ------------- | --------------------- | --------- | 98 | | `Authorization` | a valid authentication token | Yes | 99 | 100 | URL Parameters
101 | None 102 | 103 | Request Parameters
104 | None 105 | 106 | Response 107 | ``` 108 | 109 | ``` 110 | 111 | Errors:
112 | 113 | | Error | Source | Cause | 114 | | ------------ | ------ | ------ | 115 | | InvalidHeaderError | `Authorization` | the authentication token was invalid or absent | 116 | 117 | --- -------------------------------------------------------------------------------- /docs/Ecosystems-Documentation.md: -------------------------------------------------------------------------------- 1 | ### Purpose 2 | 3 | Provides functionality for creating, modifying, and accessing ecosystems. 4 | 5 | --- 6 | 7 | **POST /v1/ecosystem**
8 | Creates a new ecosystem 9 | 10 | Headers
11 | 12 | | Header | Description | Required | 13 | | ------------- | --------------------- | --------- | 14 | | `Authorization` | a valid authentication token for ORGANIZERS | Yes | 15 | 16 | URL Parameters
17 | None 18 | 19 | Request Parameters
20 | 21 | | Parameter | Description | Required | 22 | | ---------------- | --------------------- | --------- | 23 | | `name` | a valid, unused name of length up to 100 characters | Yes | 24 | 25 | Request 26 | ``` 27 | { 28 | "name": "Javascript" 29 | } 30 | ``` 31 | 32 | Response 33 | ``` 34 | { 35 | "meta": null, 36 | "data": { 37 | "name": "Javascript", 38 | "id": 1 39 | } 40 | } 41 | ``` 42 | 43 | Errors:
44 | 45 | | Error | Source | Cause | 46 | | ------------ | ------ | ------ | 47 | | InvalidParameterError | `name` | an ecosystem with the requested name already exists | 48 | | UnauthorizedError | user | the requesting user is not an organizer | 49 | 50 | --- 51 | 52 | **GET /v1/ecosystem/all**
53 | Retrieves all ecosystems in the database 54 | 55 | Headers
56 | 57 | | Header | Description | Required | 58 | | ------------- | --------------------- | --------- | 59 | | `Authorization` | a valid authentication token for ORGANIZERS | Yes | 60 | 61 | URL Parameters
62 | None 63 | 64 | Request Parameters
65 | 66 | | Parameter | Description | Required | 67 | | ---------------- | --------------------- | --------- | 68 | | None 69 | 70 | 71 | Response 72 | ``` 73 | { 74 | "meta": null, 75 | "data": [ 76 | { 77 | "id": 2, 78 | "name": "Android" 79 | }, 80 | { 81 | "id": 5, 82 | "name": "Is" 83 | }, 84 | { 85 | "id": 6, 86 | "name": "Better" 87 | }, 88 | { 89 | "id": 7, 90 | "name": "Than" 91 | }, 92 | { 93 | "id": 8, 94 | "name": "iOS" 95 | } 96 | ] 97 | } 98 | ``` 99 | 100 | Errors:
101 | 102 | | Error | Source | Cause | 103 | | ------------ | ------ | ------ | 104 | | UnauthorizedError | user | the requesting user is not an organizer | 105 | 106 | --- 107 | 108 | **DELETE /v1/ecosystem**
109 | Deletes an existing ecosystem 110 | 111 | Headers
112 | 113 | | Header | Description | Required | 114 | | ------------- | --------------------- | --------- | 115 | | `Authorization` | a valid authentication token for ORGANIZERS | Yes | 116 | 117 | URL Parameters
118 | None 119 | 120 | Request Parameters
121 | 122 | | Parameter | Description | Required | 123 | | ---------------- | --------------------- | --------- | 124 | | `name` | a valid, unused name of length up to 100 characters | Yes | 125 | 126 | Request 127 | ``` 128 | { 129 | "name": "Javascript" 130 | } 131 | ``` 132 | 133 | Response 134 | ``` 135 | { 136 | "meta": null, 137 | "data": {} 138 | } 139 | ``` 140 | 141 | Errors:
142 | 143 | | Error | Source | Cause | 144 | | ------------ | ------ | ------ | 145 | | InvalidParameterError | `name` | an ecosystem with the requested name already exists | 146 | | UnauthorizedError | user | the requesting user is not an organizer | 147 | -------------------------------------------------------------------------------- /docs/Errors-Documentation.md: -------------------------------------------------------------------------------- 1 | ### Purpose 2 | 3 | Provides detailed information about an issue that occurred while processing a request. 4 | 5 | --- 6 | 7 | **ApiError**
8 | Returned when an unknown error occurs and the API cannot resolve the problem. 9 | 10 | Example 11 | ``` 12 | { 13 | "meta": null, 14 | "error": { 15 | "type": "ApiError", 16 | "status": 500, 17 | "title": "API Error", 18 | "message": "An error occurred. If this persists, please contact us", 19 | "source": null 20 | } 21 | } 22 | ``` 23 | 24 | --- 25 | 26 | **UnprocessableRequestError**
27 | Returned when a specific aspect of the request is not applicable. 28 | 29 | Example 30 | ``` 31 | { 32 | "meta": null, 33 | "error": { 34 | "type": "UnprocessableRequestError", 35 | "status": 400, 36 | "title": "Unprocessable Request", 37 | "message": "The server received a request that could not be processed", 38 | "source": null 39 | } 40 | } 41 | ``` 42 | 43 | --- 44 | 45 | **MissingParameterError**
46 | Returned when a specific parameter of the request is missing. 47 | 48 | Example 49 | ``` 50 | { 51 | "meta": null, 52 | "error": { 53 | "type": "MissingParameterError", 54 | "status": 400, 55 | "title": "Missing Parameter", 56 | "message": "One or more parameters were missing from the request", 57 | "source": id 58 | } 59 | } 60 | ``` 61 | 62 | --- 63 | 64 | **InvalidParameterError**
65 | Returned when a specific parameter of the request is invalid or not applicable. 66 | 67 | Example 68 | ``` 69 | { 70 | "meta": null, 71 | "error": { 72 | "type": "InvalidParameterError", 73 | "status": 400, 74 | "title": "Invalid Parameter", 75 | "message": "One or more parameters present in the request were invalid", 76 | "source": id 77 | } 78 | } 79 | ``` 80 | 81 | --- 82 | 83 | **InvalidHeaderError**
84 | Returned when a specific parameter in the header of a request is invalid or not applicable. 85 | 86 | Example 87 | ``` 88 | { 89 | "meta": null, 90 | "error": { 91 | "type": "InvalidHeaderError", 92 | "status": 400, 93 | "title": "Invalid Header", 94 | "message": "One or more headers present in the request were invalid", 95 | "source": Authorization 96 | } 97 | } 98 | ``` 99 | 100 | --- 101 | 102 | **NotFoundError**
103 | Returned when a requeste resource cannot be found. 104 | 105 | Example 106 | ``` 107 | { 108 | "meta": null, 109 | "error": { 110 | "type": "NotFoundError", 111 | "status": 404, 112 | "title": "Not Found", 113 | "message": "The requested resource could not be found", 114 | "source": id 115 | } 116 | } 117 | ``` 118 | 119 | --- 120 | 121 | **UnauthorizedError**
122 | Returned when the requester is not allowed to access a specific resource. 123 | 124 | Example 125 | ``` 126 | { 127 | "meta": null, 128 | "error": { 129 | "type": "UnauthorizedError", 130 | "status": 401, 131 | "title": "Unauthorized", 132 | "message": "The requested resource cannot be accessed with the provided credentials", 133 | "source": null 134 | } 135 | } 136 | ``` 137 | 138 | --- 139 | -------------------------------------------------------------------------------- /docs/Health-Check-Documentation.md: -------------------------------------------------------------------------------- 1 | ### Purpose 2 | 3 | Provides functionality for checking which instances of the api are healthy 4 | 5 | --- 6 | 7 | **GET /v1/health**
8 | Check to see if the api instance is healthy 9 | 10 | Headers
11 | None 12 | 13 | URL Parameters
14 | None 15 | 16 | Request Parameters
17 | None 18 | 19 | Response 20 | ``` 21 | 200 OK 22 | ``` -------------------------------------------------------------------------------- /docs/Home.md: -------------------------------------------------------------------------------- 1 | The documentation for this API is provided here. All documentation currently refers to the `v1` version of the API. 2 | 3 | Articles are grouped by the resources they describe. Most articles describe the endpoints associated with a specific resource, providing sample requests and responses, as well as required headers or required parameters. The errors that may be returned by an endpoint are listed when applicable. 4 | 5 | Some articles may describe a resource that is not associated with a single entity. For example, the Errors Documentation only provides an overview of the different types of errors returned by the API, but not the specific endpoints that may return each of those errors. 6 | 7 | -------------------------------------------------------------------------------- /docs/Permission-Documentation.md: -------------------------------------------------------------------------------- 1 | ### Purpose 2 | 3 | Provides functionality for a client to check permissions of a user. 4 | 5 | --- 6 | 7 | **GET /v1/permission/organizer**
8 | Checks if user has organizer permissions. 9 | 10 | Headers
11 | 12 | | Header | Description | Required | 13 | | ------------- | --------------------- | --------- | 14 | | `Authorization` | a valid authentication token | Yes | 15 | 16 | URL Parameters
17 | None 18 | 19 | Request Parameters
20 | None 21 | 22 | Response 23 | ``` 24 | { 25 | "meta": null, 26 | "data": { 27 | "allowed": true 28 | } 29 | } 30 | ``` 31 | 32 | Errors:
33 | 34 | | Error | Source | Cause | 35 | | ------------ | ------ | ------ | 36 | | InvalidHeaderError | `Authorization` | the authentication token was invalid or absent | -------------------------------------------------------------------------------- /docs/RSVP-Documentation.md: -------------------------------------------------------------------------------- 1 | **POST /v1/rsvp/attendee**
2 | Creates a new rsvp. 3 | 4 | Headers
5 | 6 | | Header | Description | Required | 7 | | ------------- | --------------------- | --------- | 8 | | `Authorization` | a valid authentication token | Yes | 9 | 10 | URL Parameters
11 | None 12 | 13 | Request Parameters
14 | 15 | | Parameter | Description | Required | 16 | | ---------------- | --------------------- | --------- | 17 | | `isAttending` | boolean | Yes | 18 | | `type` | type of their attendance: ['CREATE', 'CONTRIBUTE']| If isAttending is true | 19 | 20 | Request 21 | ``` 22 | { 23 | "isAttending": true 24 | "type": "CREATE" 25 | } 26 | ``` 27 | 28 | Response 29 | ``` 30 | { 31 | "meta": null, 32 | "data": { 33 | "id": 1, 34 | "attendeeId": 1, 35 | "isAttending": true, 36 | "type": "CREATE" 37 | } 38 | } 39 | ``` 40 | 41 | Errors:
42 | 43 | | Error | Source | Cause | 44 | | ------------ | ------ | ------ | 45 | | InvalidParameterError | `Attendee` | an RSVP for the requested attendee already exists | 46 | | InvalidHeaderError | `Authorization` | the authentication token was invalid or absent | 47 | --- 48 | 49 | **GET /v1/rsvp/attendee**
50 | Allows requester to retrieve their rsvp. Requires requester to have already rsvp'ed. 51 | 52 | Headers
53 | 54 | | Header | Description | Required | 55 | | ------------- | --------------------- | --------- | 56 | | `Authorization` | a valid authentication token | Yes | 57 | 58 | URL Parameters
59 | None 60 | 61 | Response 62 | ``` 63 | { 64 | "meta": null, 65 | "data": { 66 | "id": 1, 67 | "attendeeId": 1, 68 | "isAttending": true, 69 | "type": "CREATE" 70 | } 71 | } 72 | ``` 73 | 74 | Errors:
75 | 76 | | Error | Source | Cause | 77 | | ------------ | ------ | ------ | 78 | | InvalidHeaderError | `Authorization` | the authentication token was invalid or absent | 79 | | NotFoundError | N/A | a rsvp doesn't exist for the attendee| 80 | 81 | --- 82 | 83 | **GET /v1/rsvp/attendee/{:id}**
84 | Retrieves information about a rsvp. Requires requester to have either the `ADMIN` 85 | or `STAFF` permission. Identical to `GET /v1/rsvp/attendee`. 86 | 87 | --- 88 | 89 | **PUT /v1/rsvp/attendee**
90 | Updates an attendee rsvp. 91 | 92 | Headers
93 | 94 | | Header | Description | Required | 95 | | ------------- | --------------------- | --------- | 96 | | `Authorization` | a valid authentication token | Yes | 97 | 98 | URL Parameters
99 | None 100 | 101 | Request Parameters
102 | 103 | | Parameter | Description | Required | 104 | | ---------------- | --------------------- | --------- | 105 | | `isAttending` | boolean | Yes | 106 | | `type` | type of their attendance: ['CREATE', 'CONTRIBUTE']| If isAttending is true | 107 | 108 | Request 109 | ``` 110 | { 111 | "isAttending": false 112 | } 113 | ``` 114 | 115 | Response 116 | ``` 117 | { 118 | "meta": null, 119 | "data": { 120 | "id": 1, 121 | "attendeeId": 1, 122 | "isAttending": false 123 | } 124 | } 125 | ``` 126 | 127 | Errors:
128 | 129 | | Error | Source | Cause | 130 | | ------------ | ------ | ------ | 131 | | NotFoundError | `attendeeId` | a rsvp was not found for the attendee | 132 | | InvalidHeaderError | `Authorization` | the authentication token was invalid or absent | 133 | 134 | --- -------------------------------------------------------------------------------- /docs/RecruiterInterest-Documentation.md: -------------------------------------------------------------------------------- 1 | ### Purpose 2 | 3 | --- 4 | 5 | **POST /v1/recruiter/interest**
6 | Creates a new recruiter interest 7 | 8 | Headers
9 | 10 | | Header | Description | Required | 11 | | ------------- | --------------------- | --------- | 12 | | `Authorization` | a valid authentication token for SPONSOR | Yes | 13 | 14 | URL Parameters
15 | None 16 | 17 | Request Parameters
18 | 19 | | Parameter | Description | Required | 20 | | ---------------- | --------------------- | --------- | 21 | | `attendeeUserId` | The attendee's user id | Yes | 22 | | `comments` | a string of length up to 255 characters | No | 23 | | `favorite` | a boolean | No | 24 | 25 | Request 26 | ``` 27 | { 28 | "attendeeUserId": 2, 29 | "comments": "Meh", 30 | "favorite": 1 31 | } 32 | ``` 33 | 34 | Response 35 | ``` 36 | { 37 | "meta": null, 38 | "data": { 39 | "appId": "1", 40 | "recruiterId": 3, 41 | "attendeeId": 2, 42 | "comments": "Meh", 43 | "favorite": 1, 44 | } 45 | } 46 | ``` 47 | 48 | Errors:
49 | 50 | | Error | Source | Cause | 51 | | ------------ | ------ | ------ | 52 | | UnauthorizedError | user | the requesting user is not an organizer | 53 | 54 | --- 55 | 56 | **GET /v1/recruiter/interest/all**
57 | Retrieve a project by ID 58 | 59 | Headers
60 | 61 | | Header | Description | Required | 62 | | ------------- | --------------------- | --------- | 63 | | `Authorization` | a valid authentication token for ALL | Yes | 64 | 65 | URL Parameters
66 | None 67 | 68 | Response 69 | ``` 70 | { 71 | "meta": null, 72 | "data": [ 73 | { 74 | "appId": 2, 75 | "recruiterId": 3, 76 | "attendeeId": 2, 77 | "comments": "nice", 78 | "favorite": 0, 79 | "created": "2018-02-22T04:51:55.000Z", 80 | "updated": "2018-02-22T22:21:47.000Z" 81 | } 82 | ] 83 | } 84 | ``` 85 | 86 | Errors:
87 | 88 | | Error | Source | Cause | 89 | | ------------ | ------ | ------ | 90 | | UnauthorizedError | user | the requesting user is not an organizer | 91 | 92 | --- 93 | 94 | **PUT /v1/recruiter/interest/{:id}**
95 | Updates an existing project with new attributes 96 | 97 | Headers
98 | 99 | | Header | Description | Required | 100 | | ------------- | --------------------- | --------- | 101 | | `Authorization` | a valid authentication token for ORGANIZERS | Yes | 102 | 103 | URL Parameters
104 | None 105 | 106 | Request Parameters
107 | 108 | | Parameter | Description | Required | 109 | | ---------------- | --------------------- | --------- | 110 | | `id` | the id of the project to update | Yes | 111 | | `comments` | a string of length up to 255 characters | No | 112 | | `favorite` | a boolean | No | 113 | 114 | Request 115 | ``` 116 | { 117 | "comments": "Meh", 118 | "favorite": 1 119 | } 120 | ``` 121 | 122 | Response 123 | ``` 124 | { 125 | "meta": null, 126 | "data": { 127 | "appId": 2, 128 | "recruiterId": 3, 129 | "attendeeId": 2, 130 | "comments": "nice", 131 | "favorite": 0, 132 | "created": "2018-02-22T04:51:55.000Z", 133 | "updated": "2018-02-22T22:39:08.341Z" 134 | } 135 | } 136 | ``` 137 | 138 | Errors:
139 | 140 | | Error | Source | Cause | 141 | | ------------ | ------ | ------ | 142 | | UnauthorizedError | user | the requesting user is not an organizer | 143 | -------------------------------------------------------------------------------- /docs/Universal-Tracking-Documenation.md: -------------------------------------------------------------------------------- 1 | ### Purpose 2 | 3 | Implements a distributed system to allow volunteers to scan attendee qr codes and figure out whether they have already partaken in a Universally Tracked Event. 4 | 5 | 6 | **POST /v1/tracking**
7 | Creates a new tracked event 8 | 9 | Headers
10 | 11 | | Header | Description | Required | 12 | | ------------- | --------------------- | --------- | 13 | | `Authorization` | a valid authentication token for Organizer| Yes | 14 | 15 | URL Parameters
16 | None 17 | 18 | Request Parameters
19 | 20 | | Parameter | Description | Required | 21 | | ---------------- | --------------------- | --------- | 22 | | `name` | the name of the event (255 chars max) | Yes | 23 | | `duration` | how long the event takes in seconds | Yes | 24 | 25 | Request 26 | ``` 27 | { 28 | "name": "Monday Dinner", 29 | "duration": "3600" 30 | } 31 | ``` 32 | 33 | Response 34 | ``` 35 | { 36 | "id": 1, 37 | "name": "Monday Dinner", 38 | "duration": "3600", 39 | "created": "2017-02-13T03:53:49.000Z", 40 | "count": 0 41 | } 42 | ``` 43 | 44 | Errors:
45 | 46 | | Error | Source | Cause | 47 | | ------------ | ------ | ------ | 48 | | | N/A | the given authorization token was invalid | 49 | | InvalidTrackingStateError | `name` | this event is already being tracked | 50 | | InvalidTrackingStateError | `eventTracking` | an event is currently being tracked | 51 | 52 | --- 53 | 54 | **GET /v1/tracking/:id
55 | Allows a volunteer to register an attendee as a participant in an event 56 | 57 | Headers
58 | 59 | | Header | Description | Required | 60 | | ------------- | --------------------- | --------- | 61 | | `Authorization` | a valid authentication token for a Volunteer| Yes | 62 | 63 | URL Parameters
64 | 65 | | Parameter | Description | Required | 66 | | ---------------- | --------------------- | --------- | 67 | | `id` | The id of the user to check | Yes| 68 | 69 | Request Parameters
70 | None 71 | 72 | Response 73 | ``` 74 | 200 OK 75 | ``` 76 | 77 | Errors:
78 | 79 | | Error | Source | Cause | 80 | | ------------ | ------ | ------ | 81 | | | N/A | the given authorization token was invalid | 82 | | InvalidTrackingStateError | eventTracking | no event is currently being tracked | 83 | | InvalidParameterError| id | this user has already partaken in the currently tracked event | 84 | 85 | 86 | --- 87 | -------------------------------------------------------------------------------- /docs/Upload-Documentation.md: -------------------------------------------------------------------------------- 1 | ### Purpose 2 | 3 | Provides functionality for uploading and retrieving blobs. 4 | 5 | --- 6 | 7 | **POST /v1/upload/resume**
8 | Accepts a byte-stream to be associated as the uploading user's resume. The stream must meet the following criteria: 9 | * Under 2MB in length 10 | * Matches MIME type `application/pdf` 11 | 12 | Headers
13 | 14 | | Header | Description | Required | 15 | | ------------- | --------------------- | --------- | 16 | | `Authorization` | a valid authentication token | Yes | 17 | | `Content-Length` | the size of the blob in bytes | Yes | 18 | | `Content-Type` | the MIME type of the blob | Yes | 19 | 20 | URL Parameters
21 | None 22 | 23 | Request Parameters
24 | None 25 | 26 | Request
27 | The body may include only the raw byte-stream of the upload 28 | 29 | Response 30 | ``` 31 | { 32 | "meta": null, 33 | "data": { 34 | "id": 1, 35 | "ownerId": 3, 36 | "key": "5f695bdd-cc2d-4833-9b28-22a76072c00a", 37 | "bucket": "example-bucket", 38 | "created": "2016-08-12T04:00:00.000Z", 39 | "updated": "2016-08-12T04:00:00.000Z" 40 | } 41 | } 42 | ``` 43 | 44 | Errors:
45 | 46 | | Error | Source | Cause | 47 | | ------------ | ------ | ------ | 48 | | UnauthorizedError | N/A | the requester does not have permission to upload a file | 49 | | UnsupportedMediaType | N/A | the byte-stream's file type is not permitted | 50 | | EntityTooLargeError | N/A | the byte-stream's length is over the specified limit | 51 | 52 | --- 53 | 54 | **GET /v1/upload/resume/:id**
55 | Retrieves a resume from storage. 56 | 57 | Headers
58 | 59 | | Header | Description | Required | 60 | | ------------- | --------------------- | --------- | 61 | | `Authorization` | a valid authentication token | Yes | 62 | 63 | URL Parameters
64 | 65 | | Parameter | Description | Required | 66 | | -----| --------------------- | --------- | 67 | | `id` | the id of the upload to replace | Yes | 68 | 69 | Response 70 | The body will include only the byte-stream of the requested resume 71 | 72 | Errors:
73 | 74 | | Error | Source | Cause | 75 | | ------------ | ------ | ------ | 76 | | InvalidHeaderError | `Authorization` | the authentication token was absent or invalid | 77 | | NotFoundError | id | the provided upload ID does not exist | 78 | 79 | --- 80 | 81 | **PUT /v1/upload/resume/:id**
82 | Updates the resume currently associated with the uploading user. The usage of this endpoint is very similar to the POST resume endpoint above; see its content in addition to the content provided here. 83 | 84 | URL Parameters
85 | 86 | | Parameter | Description | Required | 87 | | -----| --------------------- | --------- | 88 | | `id` | the id of the upload to replace | Yes | 89 | 90 | | Error | Source | Cause | 91 | | ------------ | ------ | ------ | 92 | | NotFoundError | id | the provided upload ID does not exist | 93 | 94 | --- 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hackillinois-api", 3 | "version": "0.0.1", 4 | "private": true, 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/HackIllinois/api.git" 8 | }, 9 | "dependencies": { 10 | "aws-sdk": "^2.5.0", 11 | "bcrypt": "^0.8.7", 12 | "bluebird": "^3.4.1", 13 | "body-parser": "^1.15.1", 14 | "bookshelf": "^0.9.5", 15 | "bytes": "^2.4.0", 16 | "checkit": "^0.6.0", 17 | "cuid": "^1.3.8", 18 | "debug": "^2.2.0", 19 | "express": "^4.13.4", 20 | "express-brute": "^1.0.1", 21 | "express-brute-redis": "0.0.1", 22 | "helmet": "^3.6.1", 23 | "inflection": "^1.10.0", 24 | "jsonwebtoken": "^7.0.1", 25 | "jssha": "^2.1.0", 26 | "knex": "^0.11.5", 27 | "lodash": "^4.13.1", 28 | "mkdirp": "^0.5.1", 29 | "ms": "^0.7.1", 30 | "mysql": "^2.11.1", 31 | "redis": "^2.6.2", 32 | "request-promise": "^4.2.1", 33 | "sparkpost": "^1.3.5", 34 | "uuid": "^3.1.0", 35 | "winston": "^2.2.0", 36 | "ctx": "file:./ctx" 37 | }, 38 | "devDependencies": { 39 | "chai": "^3.5.0", 40 | "chai-as-promised": "^6.0.0", 41 | "chai-http": "^3.0.0", 42 | "eslint": "^3.19.0", 43 | "eslint-plugin-node": "^4.2.2", 44 | "fakeredis": "^2.0.0", 45 | "mocha": "^3.2.0", 46 | "mock-knex": "^0.3.7", 47 | "mockery": "^2.0.0", 48 | "sinon": "^1.17.2" 49 | }, 50 | "scripts": { 51 | "prod": "./scripts/prod.sh", 52 | "dev": "./scripts/dev.sh", 53 | "test": "./scripts/test.sh", 54 | "bridge": "python ./scripts/bridge.py", 55 | "dev-migrations": "./database/flyway.sh migrate development", 56 | "prod-migrations": "./database/flyway.sh migrate production" 57 | }, 58 | "engines": { 59 | "node": ">=6.11.1" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /scripts/bridge.py: -------------------------------------------------------------------------------- 1 | import os 2 | import signal 3 | from subprocess import Popen, STDOUT 4 | 5 | # see https://stackoverflow.com/a/11270665/996249 6 | # and https://stackoverflow.com/a/4791612/996249 7 | try: 8 | from subprocess import DEVNULL 9 | except ImportError: 10 | import os 11 | DEVNULL = open(os.devnull, 'wb') 12 | 13 | rsync = Popen(['vagrant', 'rsync-auto'], stdout=DEVNULL, stderr=STDOUT, preexec_fn=os.setsid) 14 | 15 | ssh = Popen(['vagrant', 'ssh']) 16 | ssh.communicate() 17 | 18 | os.killpg(os.getpgid(rsync.pid), signal.SIGTERM) 19 | -------------------------------------------------------------------------------- /scripts/deploy/deps.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sys 4 | 5 | def load_configuration(): 6 | configuration_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', '..', 'config', 'production.json') 7 | if not os.path.isfile(configuration_path): 8 | print("error: no production configuration found") 9 | sys.exit(1) 10 | 11 | configuration = None 12 | with open(configuration_path, 'r') as f: 13 | configuration = json.loads(f.read()) 14 | if configuration is None or configuration['mail'] is None: 15 | print("error: configuration is not complete") 16 | sys.exit(1) 17 | 18 | return configuration 19 | 20 | def index_sp_result(result): 21 | return dict(map(lambda enum: (enum[1]['id'], enum[0]), enumerate(result))) 22 | 23 | def verify_templates(sp, configuration): 24 | existing_templates = sp.templates.list() 25 | existing_templates_index = index_sp_result(existing_templates) 26 | 27 | desired_templates = configuration['mail']['templates'] 28 | invalid_templates = [] 29 | for template_id in desired_templates.values(): 30 | template_idx = existing_templates_index.get(template_id) 31 | if template_idx == None: 32 | invalid_templates.append(template_id) 33 | continue 34 | 35 | template = existing_templates[template_idx] 36 | if not template['published']: 37 | invalid_templates.append(template_id) 38 | 39 | return invalid_templates 40 | 41 | def create_lists(sp, configuration): 42 | existing_lists = sp.recipient_lists.list() 43 | existing_lists_index = index_sp_result(existing_lists) 44 | 45 | desired_lists = configuration['mail']['lists'] 46 | created_lists = [] 47 | for desired_list in desired_lists.values(): 48 | if existing_lists_index.get(desired_list['id']) is not None: 49 | continue 50 | 51 | default_recipient = { "address": { "email": "default@example.com" } } 52 | response = sp.recipient_lists.create(id=desired_list['id'], name=desired_list['name'], recipients=[default_recipient]) 53 | 54 | created_lists.append(desired_list['id']) 55 | 56 | return created_lists 57 | 58 | 59 | from sparkpost import SparkPost 60 | sp = SparkPost() 61 | 62 | configuration = load_configuration() 63 | 64 | invalid_templates = verify_templates(sp, configuration) 65 | if len(invalid_templates) > 0: 66 | print("error: the following templates have not been created or published") 67 | print("\t%s" % '\n\t'.join(invalid_templates)) 68 | sys.exit(1) 69 | else: 70 | print("info: all templates were verified") 71 | 72 | created_lists = create_lists(sp, configuration) 73 | if len(created_lists) > 0: 74 | num_created = len(created_lists) 75 | plural = 's' if num_created > 1 else '' 76 | print("info: created %d new list%s (%s)" % (num_created, plural, ', '.join(created_lists))) 77 | else: 78 | print("info: no new lists created") 79 | -------------------------------------------------------------------------------- /scripts/dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export NODE_ENV=development 3 | nodemon --delay 125ms api.js 4 | -------------------------------------------------------------------------------- /scripts/install/deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | pip3 install --user sparkpost 4 | -------------------------------------------------------------------------------- /scripts/install/flyway.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | curl -o /tmp/flyway.tar.gz -s -S https://repo1.maven.org/maven2/org/flywaydb/flyway-commandline/4.0.3/flyway-commandline-4.0.3-linux-x64.tar.gz 5 | 6 | mkdir -p /opt/flyway 7 | tar -xf /tmp/flyway.tar.gz -C /opt/flyway --strip-components=1 8 | chmod 755 /opt/flyway/flyway 9 | ln -s /opt/flyway/flyway ~/bin/flyway -------------------------------------------------------------------------------- /scripts/prod.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export NODE_ENV=production 3 | node api.js 4 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export NODE_ENV=test 3 | ./node_modules/eslint/bin/eslint.js --cache --fix api.js api test \ 4 | && ./node_modules/mocha/bin/mocha test/test.js --reporter dot --slow 500 --check-leaks --full-trace 5 | -------------------------------------------------------------------------------- /test/auth.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | 3 | const errors = require('../api/v1/errors'); 4 | const utils = require('../api/v1/utils'); 5 | const User = require('../api/v1/models/User.js'); 6 | const AuthService = require('../api/v1/services/AuthService.js'); 7 | 8 | const jwt = require('jsonwebtoken'); 9 | 10 | const expect = chai.expect; 11 | 12 | describe('AuthService', () => { 13 | 14 | describe('issueForUser', () => { 15 | let testUser; 16 | 17 | before((done) => { 18 | testUser = User.forge({ 19 | id: 1, 20 | email: 'new@example.com' 21 | }); 22 | testUser.related('roles') 23 | .add({ 24 | role: utils.roles.ATTENDEE 25 | }); 26 | 27 | done(); 28 | }); 29 | it('issues a token for a valid user', (done) => { 30 | const token = AuthService.issueForUser(testUser); 31 | token.then((data) => { 32 | const decoded = jwt.decode(data, { 33 | complete: true 34 | }); 35 | 36 | expect(decoded.payload.email) 37 | .to.equal('new@example.com'); 38 | expect(decoded.payload.roles[0].role) 39 | .to.equal('ATTENDEE'); 40 | expect(decoded.payload.sub) 41 | .to.equal('1'); 42 | 43 | done(); 44 | }); 45 | }); 46 | it('refuses a token for a blank user', (done) => { 47 | try { 48 | AuthService.issueForUser(new User()); 49 | } catch (e) { 50 | expect(e) 51 | .to.be.instanceof(TypeError); 52 | done(); 53 | } 54 | }); 55 | }); 56 | 57 | describe('verify', () => { 58 | let testUser; 59 | before((done) => { 60 | testUser = User.forge({ 61 | id: 1, 62 | email: 'new@example.com' 63 | }); 64 | testUser.related('roles') 65 | .add({ 66 | role: utils.roles.ATTENDEE 67 | }); 68 | done(); 69 | }); 70 | it('verifies a valid auth token', (done) => { 71 | AuthService.issueForUser(testUser) 72 | .then((token) => { 73 | const verification = AuthService.verify(token); 74 | expect(verification) 75 | .to.eventually.have.deep.property('email', 'new@example.com') 76 | .then(() => { 77 | expect(verification) 78 | .to.eventually.have.deep.property('sub', '1') 79 | .and.notify(done); 80 | }); 81 | }); 82 | }); 83 | it('refuses a fake auth token', (done) => { 84 | const token = 'FAKE TOKEN'; 85 | const verification = AuthService.verify(token); 86 | expect(verification) 87 | .to.eventually.be.rejectedWith(errors.UnprocessableRequestError) 88 | .and.notify(done); 89 | }); 90 | }); 91 | 92 | }); 93 | -------------------------------------------------------------------------------- /test/ecosystem.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | 3 | const EcosystemService = require('../api/v1/services/EcosystemService.js'); 4 | 5 | const expect = chai.expect; 6 | const tracker = require('mock-knex').getTracker(); 7 | 8 | 9 | function _patchInsertUpdate () { 10 | // adds a query handler to the mock database connection so that 11 | // it returns a result (i.e. on save for these tests) 12 | tracker.on('query', (query) => { 13 | query.response([]); 14 | }); 15 | } 16 | 17 | describe('EcosystemService', () => { 18 | 19 | describe('createEcosystem', () => { 20 | let name; 21 | before((done) => { 22 | name = 'testEcosystem'; 23 | done(); 24 | }); 25 | beforeEach((done) => { 26 | tracker.install(); 27 | done(); 28 | }); 29 | it('creates a new ecosystem', (done) => { 30 | _patchInsertUpdate(); 31 | const ecosystem = EcosystemService.createEcosystem(name); 32 | expect(ecosystem).to.eventually.have.deep.property('attributes.name', name.toLowerCase()).and.notify(done); 33 | }); 34 | afterEach((done) => { 35 | tracker.uninstall(); 36 | done(); 37 | }); 38 | }); 39 | 40 | describe('getAllEcosystems', () => { 41 | before((done) => { 42 | done(); 43 | }); 44 | beforeEach((done) => { 45 | tracker.install(); 46 | done(); 47 | }); 48 | it('retrieves all current ecosystems', (done) => { 49 | tracker.on('query', (query) => { 50 | query.response([ { name: 'testEcosystem' } ]); 51 | }); 52 | 53 | const ecosystems = EcosystemService.getAllEcosystems(); 54 | expect(ecosystems).to.eventually.not.be.empty.and.notify(done); 55 | }); 56 | afterEach((done) => { 57 | tracker.uninstall(); 58 | done(); 59 | }); 60 | }); 61 | 62 | }); 63 | -------------------------------------------------------------------------------- /test/permission.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | 3 | const errors = require('../api/v1/errors'); 4 | const utils = require('../api/v1/utils'); 5 | const PermissionService = require('../api/v1/services/PermissionService.js'); 6 | const User = require('../api/v1/models/User.js'); 7 | 8 | const expect = chai.expect; 9 | 10 | const test_allow = function(creatorRole, createdRole, success, done) { 11 | const testUser = User.forge({ 12 | id: 1, 13 | email: 'new@example.com' 14 | }); 15 | testUser.setPassword('password123') 16 | .then(() => { 17 | testUser.related('roles') 18 | .add({ 19 | role: creatorRole, 20 | active: 1 21 | }); 22 | const allow = PermissionService.canCreateUser(testUser, createdRole); 23 | if (success) { 24 | expect(allow) 25 | .to.eventually.equal(true) 26 | .and.notify(done); 27 | } else { 28 | expect(allow) 29 | .to.eventually.be.rejectedWith(errors.UnauthorizedError) 30 | .and.notify(done); 31 | } 32 | }); 33 | }; 34 | 35 | describe('PermissionService', () => { 36 | describe('canCreateUser', () => { 37 | it('allows creation by SUPERUSER', (done) => { 38 | test_allow(utils.roles.SUPERUSER, '', true, done); 39 | }); 40 | it('allows creation of COMMON by ORGANIZER', (done) => { 41 | test_allow('ADMIN', 'MENTOR', true, done); 42 | }); 43 | it('denies creation of COMMON by non-ORGANIZER', (done) => { 44 | test_allow('MENTOR', 'MENTOR', false, done); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/storage.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const sinon = require('sinon'); 3 | 4 | const StorageService = require('../api/v1/services/StorageService.js'); 5 | const Upload = require('../api/v1/models/Upload.js'); 6 | const User = require('../api/v1/models/User.js'); 7 | 8 | const assert = chai.assert; 9 | const tracker = require('mock-knex').getTracker(); 10 | 11 | function _patchInsertUpdate () { 12 | // adds a query handler to the mock database connection so that 13 | // it returns a result (i.e. on save for these tests) 14 | tracker.on('query', (query) => { 15 | query.response([]); 16 | }); 17 | } 18 | 19 | describe('StorageService', () => { 20 | 21 | describe('createUpload', () => { 22 | let _forge; 23 | let _save; 24 | let testUser; 25 | 26 | before((done) => { 27 | testUser = User.forge({ id: 1, email: 'new@example.com' }); 28 | _forge = sinon.spy(Upload, 'forge'); 29 | _save = sinon.spy(Upload.prototype, 'save'); 30 | done(); 31 | }); 32 | beforeEach((done) => { 33 | tracker.install(); 34 | done(); 35 | }); 36 | 37 | it('creates a new upload with generated key', (done) => { 38 | const params = {}; 39 | params.bucket = 'target_bucket'; 40 | 41 | _patchInsertUpdate(); 42 | const upload = StorageService.createUpload(testUser, params); 43 | 44 | // define the expected parameters 45 | params.ownerId = 1; 46 | params.key = sinon.match.string; 47 | 48 | upload.then(() => { 49 | assert(_forge.withArgs(params).calledOnce, 'forge not called with right parameters'); 50 | assert(_save.calledOnce, 'save not called'); 51 | return done(); 52 | }).catch((err) => done(err)); 53 | }); 54 | it('creates a new upload with defined key', (done) => { 55 | const params = {}; 56 | params.bucket = 'target_bucket'; 57 | params.key = 'key'; 58 | params.ownerId = 1; 59 | 60 | _patchInsertUpdate(); 61 | const upload = StorageService.createUpload(testUser, params); 62 | 63 | upload.then(() => { 64 | assert(_forge.withArgs(params).called, 'forge not called with right parameters'); 65 | assert(_save.called, 'save not called'); 66 | return done(); 67 | }).catch((e) => done(e)); 68 | }); 69 | 70 | afterEach((done) => { 71 | tracker.uninstall(); 72 | done(); 73 | }); 74 | after((done) => { 75 | _forge.restore(); 76 | _save.restore(); 77 | done(); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | chai.use(require('chai-as-promised')); 3 | 4 | const mockery = require('mockery'); 5 | const mockknex = require('mock-knex'); 6 | const bookshelf = require('bookshelf'); 7 | function bookshelfMock (knex) { 8 | mockknex.mock(knex); 9 | return bookshelf(knex); 10 | } 11 | 12 | mockery.registerMock('bookshelf', bookshelfMock); 13 | mockery.registerMock('redis', require('fakeredis')); 14 | 15 | mockery.enable({ warnOnUnregistered: false }); 16 | 17 | require('./user.js'); 18 | require('./registration.js'); 19 | require('./auth.js'); 20 | require('./ecosystem.js'); 21 | require('./permission.js'); 22 | require('./token.js'); 23 | require('./storage.js'); 24 | require('./checkin.js'); 25 | require('./event.js'); 26 | require('./tracking.js'); 27 | require('./rsvp.js'); 28 | 29 | mockery.disable(); 30 | --------------------------------------------------------------------------------