├── scripts
├── prod.sh
├── install
│ ├── deps.sh
│ └── flyway.sh
├── dev.sh
├── test.sh
├── bridge.py
└── deploy
│ └── deps.py
├── database
├── revert
│ ├── V20160727_2020__createUploadsTable.revert.sql
│ ├── V20161105_1750__createProjects.revert.sql
│ ├── V20170216_1324__createCheckInTable.revert.sql
│ ├── V20170216_1919__addEventTracking.revert.sql
│ ├── V20160724_1239__createPasswordTokenTable.revert.sql
│ ├── V20161220_1152__createEcosystemModel.revert.sql
│ ├── V20170212_1950__addAnnouncements.revert.sql
│ ├── V20161105_2059__createProjectMentors.revert.sql
│ ├── V20180218_2014__addEventFavorites.revert.sql
│ ├── V20170216_1920__addNetworkCredentials.revert.sql
│ ├── V20160614_1845__createUsersTable.revert.sql
│ ├── V20180131_2152__createRecruiterInterestsModel.revert.sql
│ ├── V20161004_0917__createMentors.revert.sql
│ ├── V20160712_1603__createMailingListTables.revert.sql
│ ├── V20170222_0021_addEventAPI.revert.sql
│ ├── V20180131_2100__removeRSVPType.revert.sql
│ ├── V20180223_0035__addRecruiterUniqueIndex.revert.sql
│ └── V20161108_0927__createAttendees.revert.sql
├── migration.sh
├── migration
│ ├── V1_0__hackillinois-2017.sql
│ ├── V20180223_0035__addRecruiterUniqueIndex.sql
│ ├── V20180207_1335__changeRSVPUnique.sql
│ ├── V20180131_2100__removeRSVPType.sql
│ ├── V20161220_1152__createEcosystemModel.sql
│ ├── V20170212_1950__addAnnouncements.sql
│ ├── V20161105_1750__createProjects.sql
│ ├── V20170216_1919__addEventTracking.sql
│ ├── V20160724_1239__createPasswordTokenTable.sql
│ ├── V20170216_1920__addNetworkCredentials.sql
│ ├── V20170216_1324__createCheckInTable.sql
│ ├── V20160727_2020__createUploadsTable.sql
│ ├── V20161105_2059__createProjectMentors.sql
│ ├── V20180218_2014__addEventFavorites.sql
│ ├── V20180222_1758__createRecruiterInterests.sql
│ ├── V20160614_1845__createUsersTable.sql
│ ├── V20161004_0917__createMentors.sql
│ ├── V20170222_0021__addEventAPI.sql
│ └── V20160712_1603__createMailingListTables.sql
├── flyway.sh
└── README.md
├── api
├── v1
│ ├── errors
│ │ ├── Constants.js
│ │ ├── RedisError.js
│ │ ├── ExternalProviderError.js
│ │ ├── ExistsError.js
│ │ ├── NotFoundError.js
│ │ ├── TokenExpirationError.js
│ │ ├── MissingHeaderError.js
│ │ ├── InvalidHeaderError.js
│ │ ├── UnauthorizedError.js
│ │ ├── MissingParameterError.js
│ │ ├── InvalidParameterError.js
│ │ ├── UnprocessableRequestError.js
│ │ ├── EntityTooLargeError.js
│ │ ├── InvalidTrackingStateError.js
│ │ ├── ApiError.js
│ │ ├── EntityNotSupportedError.js
│ │ └── index.js
│ ├── utils
│ │ ├── scopes.js
│ │ ├── crypto.js
│ │ ├── storage.js
│ │ ├── mail.js
│ │ ├── index.js
│ │ ├── database.js
│ │ ├── time.js
│ │ ├── cache.js
│ │ ├── errors.js
│ │ ├── validators.js
│ │ ├── roles.js
│ │ └── logs.js
│ ├── models
│ │ ├── MailingListUser.js
│ │ ├── Location.js
│ │ ├── Ecosystem.js
│ │ ├── MentorProjectIdea.js
│ │ ├── EventLocation.js
│ │ ├── TrackingEvent.js
│ │ ├── AttendeeOSContributor.js
│ │ ├── ProjectMentor.js
│ │ ├── AttendeeRSVP.js
│ │ ├── CheckIn.js
│ │ ├── Token.js
│ │ ├── EventFavorite.js
│ │ ├── Project.js
│ │ ├── index.js
│ │ ├── Event.js
│ │ ├── AttendeeLongForm.js
│ │ ├── AttendeeExtraInfo.js
│ │ ├── RecruiterInterest.js
│ │ ├── AttendeeRequestedCollaborator.js
│ │ ├── Upload.js
│ │ ├── NetworkCredential.js
│ │ ├── Announcement.js
│ │ ├── Mentor.js
│ │ ├── MailingList.js
│ │ ├── Model.js
│ │ └── UserRole.js
│ ├── middleware
│ │ ├── response.js
│ │ ├── index.js
│ │ ├── upload.js
│ │ ├── request.js
│ │ ├── ratelimiting.js
│ │ ├── errors.js
│ │ ├── permission.js
│ │ └── auth.js
│ ├── controllers
│ │ ├── HealthController.js
│ │ ├── index.js
│ │ ├── PermissionController.js
│ │ ├── TrackingController.js
│ │ ├── MailController.js
│ │ ├── EcosystemController.js
│ │ ├── StatsController.js
│ │ ├── RecruiterInterestController.js
│ │ ├── AnnouncementController.js
│ │ └── CheckInController.js
│ ├── requests
│ │ ├── UploadRequest.js
│ │ ├── RSVPRequest.js
│ │ ├── ResetTokenRequest.js
│ │ ├── EventDeletionRequest.js
│ │ ├── EventFavoriteRequest.js
│ │ ├── UserContactInfoRequest.js
│ │ ├── EcosystemCreationRequest.js
│ │ ├── BasicAuthRequest.js
│ │ ├── ResetPasswordRequest.js
│ │ ├── ProjectMentorRequest.js
│ │ ├── AnnouncementRequest.js
│ │ ├── UniversalTrackingRequest.js
│ │ ├── AccreditedUserCreationRequest.js
│ │ ├── ProjectRequest.js
│ │ ├── LocationCreationRequest.js
│ │ ├── SendListRequest.js
│ │ ├── CheckInRequest.js
│ │ ├── UpdateRecruiterInterestRequest.js
│ │ ├── RecruiterInterestRequest.js
│ │ ├── AttendeeDecisionRequest.js
│ │ ├── MentorRequest.js
│ │ ├── EventCreationRequest.js
│ │ ├── EventUpdateRequest.js
│ │ ├── index.js
│ │ └── AttendeeRequest.js
│ ├── services
│ │ ├── index.js
│ │ ├── EcosystemService.js
│ │ ├── AnnouncementService.js
│ │ ├── RecruiterInterestService.js
│ │ ├── TokenService.js
│ │ ├── PermissionService.js
│ │ ├── RSVPService.js
│ │ └── TrackingService.js
│ └── index.js
├── logging.js
├── cache.js
├── index.js
├── database.js
└── config.js
├── ctx
├── package.json
└── ctx.js
├── .nodemonignore
├── .gitignore
├── docs
├── Health-Check-Documentation.md
├── Home.md
├── Permission-Documentation.md
├── Universal-Tracking-Documenation.md
├── Attendee-Statistics-Documentation.md
├── Upload-Documentation.md
├── Errors-Documentation.md
├── RSVP-Documentation.md
├── Ecosystems-Documentation.md
└── RecruiterInterest-Documentation.md
├── api.js
├── test
├── test.js
├── permission.js
├── ecosystem.js
├── storage.js
└── auth.js
├── .travis.yml
├── package.json
├── LICENSE
├── SETUP.md
├── Vagrantfile
├── .eslintrc
└── config
├── test.json.template
├── production.json.template
└── development.json.template
/scripts/prod.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | export NODE_ENV=production
3 | node api.js
4 |
--------------------------------------------------------------------------------
/database/revert/V20160727_2020__createUploadsTable.revert.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE `uploads`;
2 |
--------------------------------------------------------------------------------
/database/revert/V20161105_1750__createProjects.revert.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE `projects`;
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`;
--------------------------------------------------------------------------------
/scripts/install/deps.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 | pip3 install --user sparkpost
4 |
--------------------------------------------------------------------------------
/database/revert/V20160724_1239__createPasswordTokenTable.revert.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE `tokens`;
2 |
--------------------------------------------------------------------------------
/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/V20161105_2059__createProjectMentors.revert.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE `project_mentors`;
2 |
--------------------------------------------------------------------------------
/database/revert/V20180218_2014__addEventFavorites.revert.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE `event_favorites`;
2 |
--------------------------------------------------------------------------------
/scripts/dev.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | export NODE_ENV=development
3 | nodemon --delay 125ms api.js
4 |
--------------------------------------------------------------------------------
/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/revert/V20170216_1920__addNetworkCredentials.revert.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE `network_credentials`;
2 |
--------------------------------------------------------------------------------
/api/v1/errors/Constants.js:
--------------------------------------------------------------------------------
1 | const DUP_ENTRY = 'ER_DUP_ENTRY';
2 |
3 | module.exports.DupEntry = DUP_ENTRY;
4 |
--------------------------------------------------------------------------------
/database/revert/V20160614_1845__createUsersTable.revert.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE `user_roles`;
2 | DROP TABLE `users`;
3 |
--------------------------------------------------------------------------------
/database/revert/V20180131_2152__createRecruiterInterestsModel.revert.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE `recruiter_interests`;
2 |
--------------------------------------------------------------------------------
/database/revert/V20161004_0917__createMentors.revert.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE `mentor_project_ideas`;
2 | DROP TABLE `mentors`;
3 |
--------------------------------------------------------------------------------
/ctx/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ctx",
3 | "version": "0.0.1",
4 | "description": "context module",
5 | "main": "ctx.js"
6 | }
7 |
--------------------------------------------------------------------------------
/database/revert/V20160712_1603__createMailingListTables.revert.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE `mailing_lists_users`;
2 | DROP TABLE `mailing_lists`;
3 |
--------------------------------------------------------------------------------
/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/migration/V20180223_0035__addRecruiterUniqueIndex.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `recruiter_interests` ADD UNIQUE INDEX(`recruiter_id`, `attendee_id`);
2 |
--------------------------------------------------------------------------------
/database/revert/V20180223_0035__addRecruiterUniqueIndex.revert.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `recruiter_interests` DROP UNIQUE INDEX(`recruiter_id`, `attendee_id`);
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | );
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 | );
--------------------------------------------------------------------------------
/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 | ```
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 | );
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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.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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | ---
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------