diff --git a/.gitignore b/.gitignore index 361442a..d6f05ef 100644 --- a/.gitignore +++ b/.gitignore @@ -5,10 +5,12 @@ npm-debug.log* pids *.pid *.seed +dump.rdb node_modules .npm +.idea .DS_Store temp diff --git a/README.md b/README.md index f9db599..8fe4045 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/HackIllinois/api-2017?utm_source=badge&utm_medium=badge) + # HackIllinois API (2017) The back-end services supporting HackIllinois 2017 are stored here. Looking to diff --git a/api/database.js b/api/database.js index 3bac88e..b592ebf 100644 --- a/api/database.js +++ b/api/database.js @@ -25,6 +25,7 @@ function DatabaseManager() { this._knex = Knex(KNEX_CONFIG); this._bookshelf = Bookshelf(this._knex); + this._bookshelf.plugin('pagination') } DatabaseManager.prototype.constructor = DatabaseManager; diff --git a/api/v1/controllers/AuthController.js b/api/v1/controllers/AuthController.js index 2b4d335..fc1330f 100644 --- a/api/v1/controllers/AuthController.js +++ b/api/v1/controllers/AuthController.js @@ -4,6 +4,7 @@ var _Promise = require('bluebird'); var config = require('../../config'); var errors = require('../errors'); var middleware = require('../middleware'); +var requests = require('../requests'); var utils = require('../utils'); var AuthService = require('../services/AuthService'); @@ -101,11 +102,10 @@ function passwordReset(req, res, next) { } router.use(bodyParser.json()); router.use(middleware.auth); -router.use(middleware.request); -router.post('', createToken); +router.post('', middleware.request(requests.BasicAuthRequest), createToken); router.get('/refresh', refreshToken); -router.post('/reset', passwordReset); +router.post('/reset', middleware.request(requests.ResetPasswordRequest), passwordReset); router.use(middleware.response); router.use(middleware.errors); diff --git a/api/v1/controllers/HealthController.js b/api/v1/controllers/HealthController.js new file mode 100644 index 0000000..288e7a8 --- /dev/null +++ b/api/v1/controllers/HealthController.js @@ -0,0 +1,14 @@ +var middleware = require('../middleware'); + +var router = require('express').Router(); + +function healthCheck(req, res, next) { + next(); + return null; +} + +router.get('', healthCheck); +router.use(middleware.response); +router.use(middleware.errors); + +module.exports.router = router; diff --git a/api/v1/controllers/ProjectController.js b/api/v1/controllers/ProjectController.js new file mode 100644 index 0000000..23e5fd2 --- /dev/null +++ b/api/v1/controllers/ProjectController.js @@ -0,0 +1,171 @@ +var _ = require('lodash'); +var bodyParser = require('body-parser'); +var middleware = require('../middleware'); +var router = require('express').Router(); +var _Promise = require('bluebird'); + +var errors = require('../errors'); +var config = require('../../config'); +var requests = require('../requests'); +var roles = require('../utils/roles'); + +var ProjectService = require('../services/ProjectService'); +var PermissionService = require('../services/PermissionService'); + + +function _validGetAllRequest(page, count, published){ + if(_.isNaN(page)){ + var message = "Invalid page parameter"; + var source = "page"; + return _Promise.reject(new errors.InvalidParameterError(message, source)); + } + if(_.isNaN(count)){ + var message = "Invalid count parameter"; + var source = "count"; + return _Promise.reject(new errors.InvalidParameterError(message, source)); + } + if(_.isNaN(published) || (published != 0 && published != 1)){ + var message = "Invalid published parameter"; + var source = "published"; + return _Promise.reject(new errors.InvalidParameterError(message, source)); + } + return _Promise.resolve(true); +} + +function createProject (req, res, next) { + attributes = req.body; + + PermissionService + .canCreateProject(req.user) + .then(function (isAuthed) { + return ProjectService.createProject(attributes); + }) + .then(function (newProject) { + res.body = newProject.toJSON(); + + next(); + return null; + }) + .catch(function (error){ + next(error); + return null; + }); +} + +function getProject (req, res, next) { + var id = req.params.id; + + ProjectService + .findProjectById(id) + .then(function (project) { + res.body = project.toJSON(); + + next(); + return null; + }) + .catch(function (error) { + next(error); + return null; + }); +} + +function getAllProjects (req, res, next) { + _.defaults(req.params, {'page': 1}); + _.defaults(req.query, {'count': 10, 'published': 1}); + var page = parseInt(req.params.page); + var count = parseInt(req.query.count); + var published = parseInt(req.query.published); + + _validGetAllRequest(page, count, published) + .then(function () { + return ProjectService.getAllProjects(page, count, published); + }) + .then(function (results) { + res.body = {}; + res.body.projects = results; + + next(); + return null; + }) + .catch(function (error) { + next(error); + return null; + }); +} + +function updateProject (req, res, next) { + var id = req.params.id; + var attributes = req.body; + + ProjectService + .findProjectById(id) + .then(function (project) { + return ProjectService.updateProject(project, attributes); + }) + .then(function (project) { + res.body = project.toJSON(); + + next(); + return null; + }) + .catch(function (error) { + next(error); + return null; + }); +} + +function addProjectMentor (req, res, next) { + var project_id = req.body.project_id; + var mentor_id = req.body.mentor_id; + + ProjectService + .addProjectMentor(project_id, mentor_id) + .then(function (projectMentor) { + res.body = projectMentor.toJSON(); + + next(); + return null; + }) + .catch( function (error) { + next(error); + return null; + }); +} + +function deleteProjectMentor (req, res, next) { + var project_id = req.body.project_id; + var mentor_id = req.body.mentor_id; + + ProjectService + .deleteProjectMentor(project_id, mentor_id) + .then(function () { + res.body = {} + + next(); + return null; + }) + .catch( function (error) { + next(error); + return null; + }); +} + +router.use(bodyParser.json()); +router.use(middleware.auth); + +router.post('/mentor', middleware.request(requests.ProjectMentorRequest), + middleware.permission(roles.ORGANIZERS), addProjectMentor); +router.delete('/mentor', middleware.request(requests.ProjectMentorRequest), + middleware.permission(roles.ORGANIZERS), deleteProjectMentor); +router.post('/', middleware.request(requests.ProjectRequest), + middleware.permission(roles.ORGANIZERS), createProject); +router.get('/:id', middleware.permission(roles.ALL), getProject); +router.put('/:id', middleware.request(requests.ProjectRequest), + middleware.permission(roles.ORGANIZERS), updateProject); +router.get('/all/:page', middleware.permission(roles.ALL), getAllProjects); + +router.use(middleware.response); +router.use(middleware.errors); + +module.exports.router = router; + diff --git a/api/v1/controllers/RegistrationController.js b/api/v1/controllers/RegistrationController.js new file mode 100644 index 0000000..953b6cb --- /dev/null +++ b/api/v1/controllers/RegistrationController.js @@ -0,0 +1,119 @@ +var bodyParser = require('body-parser'); + +var services = require('../services'); +var middleware = require('../middleware'); +var requests = require('../requests'); +var roles = require('../utils/roles'); + +var router = require('express').Router(); + +function _isAuthenticated (req) { + return req.auth && (req.user !== undefined); +} + +function createMentor(req, res, next) { + delete req.body.status; + + services.RegistrationService.createMentor(req.user, req.body) + .then(function (mentor) { + res.body = mentor.toJSON(); + + next(); + return null; + }) + .catch(function (error) { + next(error); + return null; + }); +} + +function fetchMentorByUser(req, res, next) { + services.RegistrationService + .findMentorByUser(req.user) + .then(function(mentor){ + res.body = mentor.toJSON(); + + next(); + return null; + }) + .catch(function (error) { + next(error); + return null; + }); +} + +function fetchMentorById(req, res, next) { + services.RegistrationService.findMentorById(req.params.id) + .then(function(mentor){ + res.body = mentor.toJSON(); + + next(); + return null; + }) + .catch(function (error) { + next(error); + return null; + }); +} + +function updateMentorByUser(req, res, next) { + if (!req.user.hasRoles(roles.ORGANIZERS)) { + delete req.body.status; + } + + services.RegistrationService + .findMentorByUser(req.user) + .then(function (mentor) { + return services.RegistrationService.updateMentor(mentor, req.body); + }) + .then(function(mentor){ + res.body = mentor.toJSON(); + + next(); + return null; + }) + .catch(function (error) { + next(error); + return null; + }); +} + +function updateMentorById(req, res, next) { + if (!req.user.hasRoles(roles.ORGANIZERS)) { + delete req.body.status; + } + + services.RegistrationService + .findMentorById(req.params.id) + .then (function (mentor) { + return services.RegistrationService.updateMentor(mentor, req.body); + }) + .then(function (mentor) { + res.body = mentor.toJSON(); + + next(); + return null; + }) + .catch(function (error) { + next(error); + return null; + }); +} + +router.use(bodyParser.json()); +router.use(middleware.auth); +router.use(middleware.request); + +router.post('/mentor', middleware.request(requests.MentorRequest), + middleware.permission(roles.NONE, _isAuthenticated), createMentor); +router.get('/mentor', middleware.permission(roles.MENTOR), fetchMentorByUser); +router.get('/mentor/:id', middleware.permission(roles.ORGANIZERS), fetchMentorById); +router.put('/mentor', middleware.request(requests.MentorRequest), + middleware.permission(roles.MENTOR), updateMentorByUser); +router.put('/mentor/:id', middleware.request(requests.MentorRequest), + middleware.permission(roles.ORGANIZERS), updateMentorById); + +router.use(middleware.response); +router.use(middleware.errors); + +module.exports.router = router; diff --git a/api/v1/controllers/UploadController.js b/api/v1/controllers/UploadController.js index da71493..5a5f75a 100644 --- a/api/v1/controllers/UploadController.js +++ b/api/v1/controllers/UploadController.js @@ -15,6 +15,7 @@ var services = require('../services'); var utils = require('../utils'); var Upload = require('../models/Upload'); +var UploadRequest = require('../requests/UploadRequest'); var User = require('../models/User'); const UPLOAD_ALREADY_PRESENT = "An upload has already been associated with this user"; @@ -114,10 +115,11 @@ function getUpload (req, res, next) { // note that the request middleware is added after the body-parser, else there will be no body var resumeRouter = ExpressRouter(); resumeRouter.use(bodyParser.raw({ limit: RESUME_UPLOAD_LIMIT, type: RESUME_UPLOAD_TYPE })); -resumeRouter.use(middleware.request); -resumeRouter.post('/', middleware.upload, middleware.permission(utils.roles.NON_PROFESSIONALS), createResumeUpload); -resumeRouter.put('/:id', middleware.upload, _findUpload, middleware.permission(utils.roles.NONE, _isOwner), replaceResumeUpload); +resumeRouter.post('/', middleware.request(UploadRequest), middleware.upload, + middleware.permission(utils.roles.NON_PROFESSIONALS), createResumeUpload); +resumeRouter.put('/:id', middleware.request(UploadRequest), middleware.upload, + _findUpload, middleware.permission(utils.roles.NONE, _isOwner), replaceResumeUpload); resumeRouter.get('/:id', _findUpload, middleware.permission(utils.roles.ORGANIZERS, _isOwner), getUpload); // set up the primary router with just the auth middleware since the sub-routers diff --git a/api/v1/controllers/UserController.js b/api/v1/controllers/UserController.js index 888cbad..5fb4a18 100644 --- a/api/v1/controllers/UserController.js +++ b/api/v1/controllers/UserController.js @@ -5,6 +5,7 @@ var services = require('../services'); var config = require('../../config'); var middleware = require('../middleware'); +var requests = require('../requests'); var scopes = require('../utils/scopes'); var mail = require('../utils/mail'); var roles = require('../utils/roles'); @@ -21,9 +22,9 @@ function isRequester(req) { return req.user.get('id') == req.params.id; } -function createAttendee (req, res, next) { +function createUser (req, res, next) { services.UserService - .createUser(req.body.email, req.body.password, roles.ATTENDEE) + .createUser(req.body.email, req.body.password) .then(function (user) { return services.AuthService.issueForUser(user); }) @@ -101,16 +102,16 @@ function requestPasswordReset (req, res, next) { router.use(bodyParser.json()); router.use(middleware.auth); -router.use(middleware.request); -router.post('/attendee', createAttendee); -router.post('/accredited', middleware.permission(roles.ORGANIZERS), createAccreditedUser); -router.post('/reset', requestPasswordReset); +router.post('/', middleware.request(requests.BasicAuthRequest), createUser); +router.post('/accredited', middleware.request(requests.AccreditedUserCreationRequest), + middleware.permission(roles.ORGANIZERS), createAccreditedUser); +router.post('/reset', middleware.request(requests.ResetTokenRequest), requestPasswordReset); router.get('/:id', middleware.permission(roles.ORGANIZERS, isRequester), getUser); router.use(middleware.response); router.use(middleware.errors); -module.exports.createAttendee = createAttendee; +module.exports.createUser = createUser; module.exports.createAccreditedUser = createAccreditedUser; module.exports.router = router; diff --git a/api/v1/controllers/index.js b/api/v1/controllers/index.js index 73adcd1..8c8137c 100644 --- a/api/v1/controllers/index.js +++ b/api/v1/controllers/index.js @@ -1,5 +1,8 @@ module.exports = { AuthController: require('./AuthController.js'), UploadController: require('./UploadController.js'), - UserController: require('./UserController.js') + UserController: require('./UserController.js'), + RegistrationController: require('./RegistrationController.js'), + ProjectController: require('./ProjectController.js'), + HealthController: require('./HealthController.js') }; diff --git a/api/v1/endpoints.js b/api/v1/endpoints.js deleted file mode 100644 index 96fa10e..0000000 --- a/api/v1/endpoints.js +++ /dev/null @@ -1,27 +0,0 @@ -var requests = require('./requests'); - -var endpoints = {}; - -endpoints['/v1/user/attendee'] = { - POST: requests.BasicAuthRequest -}; -endpoints['/v1/user/accredited'] = { - POST: requests.AccreditedUserCreationRequest -}; -endpoints['/v1/user/reset'] = { - POST: requests.ResetTokenRequest -}; -endpoints['/v1/auth/reset'] = { - POST: requests.ResetPasswordRequest -}; -endpoints['/v1/auth'] = { - POST: requests.BasicAuthRequest -}; -endpoints['/v1/upload/resume'] = { - POST: requests.UploadRequest -}; -endpoints['/v1/upload/resume/:id'] = { - PUT: requests.UploadRequest -}; - -module.exports = endpoints; diff --git a/api/v1/index.js b/api/v1/index.js index fbd7bc7..a51a182 100644 --- a/api/v1/index.js +++ b/api/v1/index.js @@ -15,5 +15,8 @@ var controllers = require('./controllers'); v1.use('/auth', controllers.AuthController.router); v1.use('/user', controllers.UserController.router); v1.use('/upload', controllers.UploadController.router); +v1.use('/registration', controllers.RegistrationController.router); +v1.use('/project', controllers.ProjectController.router); +v1.use('/health', controllers.HealthController.router); module.exports = v1; diff --git a/api/v1/middleware/index.js b/api/v1/middleware/index.js index b58dec7..06d3ad7 100644 --- a/api/v1/middleware/index.js +++ b/api/v1/middleware/index.js @@ -2,7 +2,7 @@ module.exports = { auth: require('./auth.js'), errors: require('./errors.js'), permission: require('./permission.js'), - request: require('./request.js'), response: require('./response.js'), + request: require('./request.js'), upload: require('./upload.js') }; diff --git a/api/v1/middleware/request.js b/api/v1/middleware/request.js index a220b7f..3521069 100644 --- a/api/v1/middleware/request.js +++ b/api/v1/middleware/request.js @@ -1,34 +1,26 @@ var CheckitError = require('checkit').Error; -var endpoints = require('../endpoints'); var errors = require('../errors'); var errorUtils = require('../utils/errors'); -module.exports = function(req, res, next) { - // we need to find whether or not this endpoint's method has a validating - // request object mapped to it - var pathRequests = endpoints[req.originalUrl.replace(/\/+$/, "")]; - var MethodRequest = (pathRequests) ? pathRequests[req.method] : undefined; +module.exports = function (Request) { + return function (req, res, next) { + if (!Request) { + return next(); + } - // not all methods (or endpoints) define such an object - if (!MethodRequest) { - return next(); - } - - // the request we find is an object type, so we instantiate it - // and then handle any validation errors before continuing - var request = new MethodRequest(req.headers, req.body); - request.validate() - .then(function (validated) { - req.body = request.body(); - - next(); - return null; - }) - .catch(CheckitError, errorUtils.handleValidationError) - .catch(function (error) { - next(error); - return null; - }); + var request = new Request(req.headers, req.body); + request.validate() + .then(function (validated) { + req.body = request.body(); + next(); + return null; + }) + .catch(CheckitError, errorUtils.handleValidationError) + .catch(function (error) { + next(error); + return null; + }); + }; }; diff --git a/api/v1/models/Mentor.js b/api/v1/models/Mentor.js new file mode 100644 index 0000000..4db2189 --- /dev/null +++ b/api/v1/models/Mentor.js @@ -0,0 +1,44 @@ +var _ = require('lodash'); +var registration = require('../utils/registration'); + +var Model = require('./Model'); +var MentorProjectIdea = require('./MentorProjectIdea'); +var Mentor = Model.extend({ + tableName: 'mentors', + idAttribute: 'id', + validations: { + firstName: ['required', 'string', 'maxLength:255'], + lastName: ['required', 'string', 'maxLength:255'], + shirtSize: ['required', 'string', registration.verifyTshirtSize], + status: ['string', registration.verifyStatus], + github: ['string', 'maxLength:50'], + location: ['required', 'string', 'maxLength:255'], + summary: ['required', 'string', 'maxLength:255'], + occupation: ['required', 'string', 'maxLength:255'], + userId: ['required', 'integer'] + }, + ideas: function () { + return this.hasMany(MentorProjectIdea); + } +}); + + +/** +* Finds a mentor by its relational user's id, joining in its related project ideas +* @param {Number|String} id the ID of the user with the appropriate type +* @return {Promise} a Promise resolving to the resulting mentor or null +*/ +Mentor.findByUserId = function (userId) { + return Mentor.where({ user_id: userId }).fetch({ withRelated: ['ideas'] }); +}; + +/** +* Finds a mentor by its ID, joining in its related project ideas +* @param {Number|String} id the ID of the model with the appropriate type +* @return {Promise} a Promise resolving to the resulting model or null +*/ +Mentor.findById = function (id) { + return Mentor.where({ id: id }).fetch({ withRelated: ['ideas'] }); +}; + +module.exports = Mentor; diff --git a/api/v1/models/MentorProjectIdea.js b/api/v1/models/MentorProjectIdea.js new file mode 100644 index 0000000..c24de7b --- /dev/null +++ b/api/v1/models/MentorProjectIdea.js @@ -0,0 +1,13 @@ +var Model = require('./Model'); +var MentorProjectIdea = Model.extend({ + tableName: 'mentor_project_ideas', + idAttribute: 'id', + validations: { + link: ['required', 'url', 'maxLength:255'], + contributions: ['required', 'string', 'maxLength:255'], + ideas: ['required', 'string', 'maxLength:255'], + mentorId: ['required', 'integer'] + } +}); + +module.exports = MentorProjectIdea; diff --git a/api/v1/models/Project.js b/api/v1/models/Project.js new file mode 100644 index 0000000..5da7746 --- /dev/null +++ b/api/v1/models/Project.js @@ -0,0 +1,23 @@ +var Model = require('./Model'); + +var Project = Model.extend({ + tableName: 'projects', + idAttribute: 'id', + validations: { + name: ['required', 'string', 'maxLength:100'], + description: ['required', 'string', 'maxLength:255'], + repo: ['required', 'string', 'maxLength:255'], + isPublished: ['boolean'] + } +}); + +Project.findByName = function (name) { + name = name.toLowerCase(); + return Project.where({ name:name }).fetch(); +} + +Project.findById = function (id) { + return Project.where({ id:id }).fetch(); +} + +module.exports = Project; \ No newline at end of file diff --git a/api/v1/models/ProjectMentor.js b/api/v1/models/ProjectMentor.js new file mode 100644 index 0000000..4549f85 --- /dev/null +++ b/api/v1/models/ProjectMentor.js @@ -0,0 +1,23 @@ +var _Promise = require('bluebird'); +var _ = require('lodash'); + +var Model = require('./Model'); +var Project = require('./Project'); +var Mentor = require('./Mentor'); + +var ProjectMentor = Model.extend({ + tableName: 'project_mentors', + idAttribute: 'id', + project : function () { + return this.belongsTo(Project, 'project_id'); + }, + mentor: function () { + return this.belongsTo(Mentor, 'mentor_id'); + } +}); + +ProjectMentor.findByProjectAndMentorId = function (project_id, mentor_id) { + return ProjectMentor.where({ project_id: project_id, mentor_id: mentor_id }).fetch(); +} + +module.exports = ProjectMentor; \ No newline at end of file diff --git a/api/v1/models/User.js b/api/v1/models/User.js index 36c17dc..21bb95b 100644 --- a/api/v1/models/User.js +++ b/api/v1/models/User.js @@ -46,13 +46,21 @@ User.findByEmail = function (email) { * set to active if user creation succeeds. Validation is performed on-save only * @param {String} email the user's email * @param {String} password the user's raw password - * @param {String} role the string representation of a role from utils.roles - * @return {Promise} the User object with the related roles joined-in + * @param {String} role the string representation of a role from utils.roles (optional) + * @return {Promise} the User object with the related roles joined-in (if any) */ User.create = function (email, password, role) { var user = User.forge({ email: email }); var userRole = UserRole.forge({ role: role, active: true }); + if(!role){ + // No roles were provided, so create the User + return user.setPassword(password) + .then(function(result){ + return result.save(); + }); + } + return User .transaction(function (t) { return user.setPassword(password) @@ -151,12 +159,7 @@ User.prototype.hasPassword = function (password) { * @return {Object} the serialized form of this User */ User.prototype.serialize = function () { - var serialized = _.omit(this.attributes, ['password']); - - var roles = this.related('roles'); - serialized.roles = (_.isUndefined(roles)) ? null : roles.serialize(); - - return serialized; + return _.omit(this.attributes, ['password']); }; module.exports = User; diff --git a/api/v1/models/UserRole.js b/api/v1/models/UserRole.js index 300799c..f499488 100644 --- a/api/v1/models/UserRole.js +++ b/api/v1/models/UserRole.js @@ -11,6 +11,20 @@ var UserRole = Model.extend({ role: ['required', 'string', roles.verifyRole] } }); +/** + * Saves a forged user role using the passed transaction + */ +function _addRole (userRole, active, t) { + return userRole + .fetch({ transacting: t }) + .then(function (result) { + if (result) { + return _Promise.resolve(result); + } + userRole.set({ active: (_.isUndefined(active) || active) }); + return userRole.save(null, { transacting: t }); + }); +} /** * Adds a role to the specified user. If the role already exists, it is returned @@ -18,22 +32,17 @@ var UserRole = Model.extend({ * @param {User} user the target user * @param {String} role the string representation of the role from utils.roles * @param {Boolean} active whether or not the role should be activated (defaults to true) + * @param {Transaction} t pending transaction (optional) * @returns {Promise} the result of the addititon */ -UserRole.addRole = function (user, role, active) { +UserRole.addRole = function (user, role, active, t) { var userRole = UserRole.forge({ user_id: user.id, role: role }); - return UserRole - .transaction(function (t) { - return userRole - .fetch({ transacting: t }) - .then(function (result) { - if (result) { - return _Promise.resolve(result); - } - userRole.set({ active: (_.isUndefined(active) || active) }); - return userRole.save(null, { transacting: t }); - }); - }); + if (t) { + return _addRole(userRole, active, t); + } + return UserRole.transaction(function(t){ + return _addRole(userRole, active, t); + }); }; /** diff --git a/api/v1/models/index.js b/api/v1/models/index.js index 1e33d32..ad436ed 100644 --- a/api/v1/models/index.js +++ b/api/v1/models/index.js @@ -3,5 +3,7 @@ module.exports = { MailingListUser: require('./MailingListUser'), User: require('./User'), UserRole: require('./UserRole'), - Token: require('./Token') + Token: require('./Token'), + Mentor: require('./Mentor'), + MentorProjectIdea: require('./MentorProjectIdea') }; diff --git a/api/v1/requests/MentorRequest.js b/api/v1/requests/MentorRequest.js new file mode 100644 index 0000000..b1b10d4 --- /dev/null +++ b/api/v1/requests/MentorRequest.js @@ -0,0 +1,39 @@ +var Request = require('./Request'); +var validators = require('../utils/validators'); +var registration = require('../utils/registration'); + +var mentorValidations = { + firstName: ['required', 'string', 'maxLength:255'], + lastName: ['required', 'string', 'maxLength:255'], + shirtSize: ['required', 'string', registration.verifyTshirtSize], + status: ['string', registration.verifyStatus], + github: ['required', 'string', 'maxLength:50'], + location: ['required', 'string', 'maxLength:255'], + summary: ['required', 'string', 'maxLength:255'], + occupation: ['required', 'string', 'maxLength:255'], +}; +var ideaValidations = { + link: ['required', 'url', 'maxLength:255'], + contributions: ['required', 'string', 'maxLength:255'], + ideas: ['required', 'string', 'maxLength:255'] +}; +var bodyRequired = ['mentor', 'ideas']; +var bodyValidations = { + 'mentor': ['required', 'plainObject', validators.nested(mentorValidations, 'mentor')], + 'ideas': ['required', 'array', 'minLength:1', 'maxLength:5', validators.array(validators.nested(ideaValidations, 'ideas'))] +}; + +function MentorCreationRequest(headers, body) { + Request.call(this, headers, body); + + this.bodyRequired = bodyRequired; + this.bodyValidations = bodyValidations; +} + +MentorCreationRequest._mentorValidations = mentorValidations; +MentorCreationRequest._ideaValidations = ideaValidations; + +MentorCreationRequest.prototype = Object.create(Request.prototype); +MentorCreationRequest.prototype.constructor = MentorCreationRequest; + +module.exports = MentorCreationRequest; diff --git a/api/v1/requests/ProjectMentorRequest.js b/api/v1/requests/ProjectMentorRequest.js new file mode 100644 index 0000000..493869a --- /dev/null +++ b/api/v1/requests/ProjectMentorRequest.js @@ -0,0 +1,19 @@ +var Request = require('./Request'); + +var bodyRequired = ['project_id', 'mentor_id']; +var bodyValidations = { + 'project_id': ['integer', 'required'], + 'mentor_id': ['integer', 'required'], +}; + +function ProjectMentorRequest(headers, body) { + Request.call(this, headers, body); + + this.bodyRequired = bodyRequired; + this.bodyValidations = bodyValidations; +} + +ProjectMentorRequest.prototype = Object.create(Request.prototype); +ProjectMentorRequest.prototype.constructor = ProjectMentorRequest; + +module.exports = ProjectMentorRequest; \ No newline at end of file diff --git a/api/v1/requests/ProjectRequest.js b/api/v1/requests/ProjectRequest.js new file mode 100644 index 0000000..f151b4b --- /dev/null +++ b/api/v1/requests/ProjectRequest.js @@ -0,0 +1,22 @@ +var roles = require('../utils/roles'); +var Request = require('./Request'); + +var bodyRequired = ['name', 'description', 'repo', 'isPublished']; +var bodyValidations = { + 'name': ['string', 'required'], + 'description': ['string', 'required'], + 'repo': ['required', 'string', 'maxLength:255'], + 'isPublished': ['boolean'] +}; + +function ProjectRequest(headers, body) { + Request.call(this, headers, body); + + this.bodyRequired = bodyRequired; + this.bodyValidations = bodyValidations; +} + +ProjectRequest.prototype = Object.create(Request.prototype); +ProjectRequest.prototype.constructor = ProjectRequest; + +module.exports = ProjectRequest; \ No newline at end of file diff --git a/api/v1/requests/index.js b/api/v1/requests/index.js index 4c2c586..3e46bed 100644 --- a/api/v1/requests/index.js +++ b/api/v1/requests/index.js @@ -1,6 +1,9 @@ module.exports = { AccreditedUserCreationRequest: require('./AccreditedUserCreationRequest'), BasicAuthRequest: require('./BasicAuthRequest'), + MentorRequest: require('./MentorRequest'), + ProjectRequest: require('./ProjectRequest'), + ProjectMentorRequest: require('./ProjectMentorRequest'), ResetTokenRequest: require('./ResetTokenRequest'), ResetPasswordRequest: require('./ResetPasswordRequest'), UploadRequest: require('./UploadRequest') diff --git a/api/v1/services/PermissionService.js b/api/v1/services/PermissionService.js index 58a96cb..0ae6504 100644 --- a/api/v1/services/PermissionService.js +++ b/api/v1/services/PermissionService.js @@ -25,3 +25,19 @@ module.exports.canCreateUser = function (creator, userRole) { var message = "The requested user cannot be created with the provided credentials"; return _Promise.reject(new errors.UnauthorizedError(message)); }; + + +/** + * Checks to see if a requestor valid permissions to create a new project + * @param {User} user creating the new project + * @return {Promise} resolving to true if the user is an organizer + * @throws InvalidParameterError when a user does not have correct permissions + */ +module.exports.canCreateProject = function (creator) { + if(creator.hasRole(roles.SUPERUSER) || creator.hasRole(roles.ORGANIZERS)){ + return _Promise.resolve(true); + } + + var message = "A project cannot be created with the provided credentials"; + return _Promise.reject(new errors.UnauthorizedError(message)); +} \ No newline at end of file diff --git a/api/v1/services/ProjectService.js b/api/v1/services/ProjectService.js new file mode 100644 index 0000000..52feb55 --- /dev/null +++ b/api/v1/services/ProjectService.js @@ -0,0 +1,188 @@ +var Checkit = require('checkit'); +var _Promise = require('bluebird'); +var _ = require('lodash'); + +var Mentor = require('../models/Mentor'); +var Project = require('../models/Project'); +var ProjectMentor = require('../models/ProjectMentor'); + +var errors = require('../errors'); +var utils = require('../utils'); +var roles = require('../utils/roles'); + + +/** + * Creates a project with the specificed attributes + * @param {Object} Contains name, description, repo, and isPublished + * @return {Promise} resolving to the newly-created project + * @throws InvalidParameterError when a project exists with the specified name + */ +module.exports.createProject = function (attributes) { + if(_.isNull(attributes.isPublished) || _.isUndefined(attributes.isPublished)){ + attributes.isPublished = false; + } + + var project = Project.forge(attributes); + return project + .validate() + .catch(Checkit.Error, utils.errors.handleValidationError) + .then(function (validated) { + return Project.findByName(attributes.name); + }) + .then(function (result){ + if (!_.isNull(result)) { + var message = "A project with the given name already exists"; + var source = "name"; + throw new errors.InvalidParameterError(message, source); + } + + return project.save() + }); +} + +/** + * Returns a project with the specified project id + * @param {int} ID of the project + * @return {Promise} resolving to the project + * @throws InvalidParameterError when a project doesn't exist with the specified ID + */ +module.exports.findProjectById = function (id) { + return Project + .findById(id) + .then(function (result) { + if(_.isNull(result)){ + var message = "A project with the given ID cannot be found"; + var source = "id"; + throw new errors.NotFoundError(message, source); + } + + return _Promise.resolve(result); + }); +} + +/** + * Update a key value pair in a project + * @param {Project} Project that will be updated + * @param {Object} JSON representing new project mentor key value pairs + * @return {Promise} resolving to the updated project + * @throws InvalidParameterError when the key is not valid + */ +module.exports.updateProject = function (project, attributes) { + project.set(attributes); + + return project + .validate() + .catch(Checkit.Error, utils.errors.handleValidationError) + .then(function (validated) { + return project.save(); + }); +} + +/** + * Helper function for determining valid project/mentor ids + * @param {Int} ID of the project assigned to the mentor + * @param {Int} ID of the mentor assigned to the project + * @return {Promise} resolving to whether or not the ids are valid + * @throws InvalidParameterError when a project or mentor doesn't exist with the specified ID + */ +_isProjectMentorValid = function (project_id, mentor_id) { + return Project + .findById(project_id) + .then(function (result) { + if(_.isNull(result)) { + var message = "The project id is invalid"; + var source = "project_id"; + throw new errors.InvalidParameterError(message, source); + } + return Mentor.findById(mentor_id); + }) + .then(function (mentor) { + if(_.isNull(mentor)) { + var message = "The mentor id is invalid"; + var source = "mentor_id"; + throw new errors.InvalidParameterError(message, source); + } + return _Promise.resolve(false); + }); +} + +/** + * Helper function for deleting project-mentor relationships + * @param {Int} ID of the project assigned to the mentor + * @param {Int} ID of the mentor assigned to the project + * @return {Promise} resolving to null + * @throws InvalidParameterError when a project or mentor doesn't exist with the specified ID + */ +_deleteProjectMentor = function (project_id, mentor_id) { + return ProjectMentor + .findByProjectAndMentorId(project_id, mentor_id) + .then(function(oldProjectMentor) { + if(_.isNull(oldProjectMentor)) { + var message = "A project-mentor relationship with the given IDs cannot be found"; + var source = "project_id/mentor_id"; + throw new errors.NotFoundError(message, source); + } + return oldProjectMentor.destroy(); + }); +} + + +/** + * Add a new project-mentor relationship + * @param {Int} ID of the project assigned to the mentor + * @param {Int} ID of the mentor assigned to the project + * @return {Promise} resolving to the new relationship + * @throws InvalidParameterError when a project or mentor doesn't exist with the specified ID + */ +module.exports.addProjectMentor = function (project_id, mentor_id) { + var projectMentor = ProjectMentor.forge({ project_id: project_id, mentor_id: mentor_id }); + + return _isProjectMentorValid(project_id, mentor_id) + .then(function (isValid) { + return ProjectMentor.findByProjectAndMentorId(project_id, mentor_id); + }) + .then(function (result) { + if (!_.isNull(result)) { + //The project mentor relationship already exists + return _Promise.resolve(result); + } + return projectMentor.save() + }); +} + +/** + * Deletes a project-mentor relationship + * @param {Int} ID of the project in question + * @param {Int} ID of the mentor in question + * @return {Promise} resolving to the deleted relationship + * @throws InvalidParameterError when a project or mentor doesn't exist with the specified ID + */ +module.exports.deleteProjectMentor = function (project_id, mentor_id) { + return _deleteProjectMentor(project_id, mentor_id); +} + + +/** + * Returns a list of all projects + * @param {Int} Page number + * @param {Int} Number of items on the page + * @param {Int} Boolean in int form representing published/unpublished + * @return {Promise} resolving to an array of project objects + */ +module.exports.getAllProjects = function (page, count, isPublished) { + return Project + .query(function (qb){ + qb.groupBy('projects.id'); + qb.where('is_published', '=', isPublished); + }) + .orderBy('-name') + .fetchPage({ + pageSize: count, + page: page + }) + .then(function (results) { + var projects = _.map(results.models, 'attributes'); + return projects; + }); +} + diff --git a/api/v1/services/RegistrationService.js b/api/v1/services/RegistrationService.js new file mode 100644 index 0000000..0b4c877 --- /dev/null +++ b/api/v1/services/RegistrationService.js @@ -0,0 +1,193 @@ +/* jshint esversion: 6 */ + +var CheckitError = require('checkit').Error; +var _Promise = require('bluebird'); +var _ = require('lodash'); + +var Mentor = require('../models/Mentor'); +var MentorProjectIdea = require('../models/MentorProjectIdea'); +var UserRole = require('../models/UserRole'); +var errors = require('../errors'); +var utils = require('../utils'); + +/** + * Persists a mentor and its ideas + * @param {Mentor} mentor a mentor object to be created/updated + * @param {Array} ideas an array of raw mentor attributes + * @param {Transaction} t a pending transaction + * @return {Promise} the mentor with related ideas + */ +function _saveMentorAndIdeas(mentor, ideas, t) { + return mentor + .save(null, { transacting: t }) + .then(function (mentor) { + return _Promise.map(ideas, function (idea) { + return mentor.related('ideas').create(idea, { transacting: t }); + }).return(mentor); + }); +} + +/** + * Determines which ideas are new and which are + * existing ones that need to be updated + * @param {Mentor} mentor the Mentor with whom the ideas are associated + * @param {Array} mentorIdeas the list of MentorProjectIdea objects/attributes + * @return {Array} containing the new ideas, updated ideas, and ids of updated ideas + */ +function _extractMentorIdeas(mentor, mentorIdeas) { + var newIdeas = []; + var updatedIdeas = []; + var updatedIdeaIds = []; + + _.forEach(mentorIdeas, function (idea) { + var MESSAGE, SOURCE; + if (!_.has(idea, 'id')) { + newIdeas.push(idea); + } else if (_.isUndefined(mentor.related('ideas').get(idea.id))) { + MESSAGE = "A MentorProjectIdea with the given ID does not exist"; + SOURCE = "idea.id"; + throw new errors.NotFoundError(MESSAGE, SOURCE); + } else if (mentor.related('ideas').get(idea.id).get('mentorId') !== mentor.get('id')){ + MESSAGE = "A MentorProjectIdea that does not belong to this mentor cannot be updated here"; + throw new errors.UnauthorizedError(MESSAGE); + } else { + // TODO remove this once Request validator can marshal recursively + idea.mentorId = mentor.get('id'); + + updatedIdeas.push(mentor.related('ideas').get(idea.id).set(idea)); + updatedIdeaIds.push(idea.id); + } + }); + + return _Promise.all([newIdeas, updatedIdeas, updatedIdeaIds]); +} + +/** + * Removes unwanted ideas and updates desired ideas + * @param {Mentor} mentor the Mentor with whom the ideas are associated + * @param {Array} updatedIdeas a list of related MentorProjectIdeas with new attributes + * @param {Array} updatedIdeaIds a list of the ids contained in the updatedIdeas + * @param {Transaction} t a pending transaction + * @return {Promise<>} a promise indicating all changes have been added to the transaction + */ +function _adjustMentorIdeas(mentor, updatedIdeas, updatedIdeaIds, t) { + return mentor.related('ideas') + .query().transacting(t) + .whereNotIn('id', updatedIdeaIds) + .delete() + .catch(Mentor.NoRowsDeletedError, function () { return null; }) + .then(function () { + mentor.related('ideas').reset(); + + return _Promise.map(updatedIdeas, function (idea) { + mentor.related('ideas').add(idea); + return idea.save(null, { transacting: t, require: false }); + }); + }); +} + +/** +* Registers a mentor and their project ideas for the given user +* @param {Object} user the user for which a mentor will be registered +* @param {Object} attributes a JSON object holding the mentor attributes +* @return {Promise} the mentor with related ideas +* @throws {InvalidParameterError} when a mentor exists for the specified user +*/ +var createMentor = function (user, attributes) { + var mentorAttributes = attributes.mentor; + var mentorIdeas = attributes.ideas; + + mentorAttributes.userId = user.get('id'); + var mentor = Mentor.forge(mentorAttributes); + + return mentor + .validate() + .catch(CheckitError, utils.errors.handleValidationError) + .then(function (validated) { + if (user.hasRole(utils.roles.MENTOR, false)) { + var message = "The given user has already registered as a mentor"; + var source = "userId"; + throw new errors.InvalidParameterError(message, source); + } + + return Mentor.transaction(function (t) { + return UserRole + .addRole(user, utils.roles.MENTOR, false, t) + .then(function (result) { + return _saveMentorAndIdeas(mentor, mentorIdeas); + }); + }); + }); +}; + +/** +* Finds a mentor by querying on a user's ID +* @param {User} user the user expected to be associated with a mentor +* @return {Promise} resolving to the associated Mentor model +* @throws {NotFoundError} when the requested mentor cannot be found +*/ +var findMentorByUser = function (user) { + return Mentor + .findByUserId(user.get('id')) + .tap(function (result) { + if (_.isNull(result)) { + var message = "A mentor with the given user ID cannot be found"; + var source = "userId"; + throw new errors.NotFoundError(message, source); + } + }); +}; + +/** +* Finds a mentor by querying for the given ID +* @param {Number} id the ID to query +* @return {Promise} resolving to the associated Mentor model +* @throws {NotFoundError} when the requested mentor cannot be found +*/ +var findMentorById = function (id) { + return Mentor + .findById(id) + .tap(function (result) { + if (_.isNull(result)) { + var message = "A mentor with the given ID cannot be found"; + var source = "id"; + throw new errors.NotFoundError(message, source); + } + }); +}; + +/** +* Updates a mentor and their project ideas by relational user +* @param {Mentor} mentor the mentor to be updated +* @param {Object} attributes a JSON object holding the mentor registration attributes +* @return {Promise} resolving to an object in the same format as attributes, holding the saved models +* @throws {InvalidParameterError} when a mentor doesn't exist for the specified user +*/ +var updateMentor = function (mentor, attributes) { + var mentorAttributes = attributes.mentor; + var mentorIdeas = attributes.ideas; + + mentor.set(mentorAttributes); + + return mentor + .validate() + .catch(CheckitError, utils.errors.handleValidationError) + .then(function (){ + return _extractMentorIdeas(mentor, mentorIdeas); + }) + .spread(function (newIdeas, updatedIdeas, updatedIdeaIds){ + return Mentor.transaction(function (t) { + return _adjustMentorIdeas(mentor, updatedIdeas, updatedIdeaIds, t) + .then(function () { + return _saveMentorAndIdeas(mentor, newIdeas, t); + }); + }); + }); +}; + +module.exports = { + createMentor: createMentor, + findMentorByUser: findMentorByUser, + findMentorById: findMentorById, + updateMentor: updateMentor +}; diff --git a/api/v1/services/index.js b/api/v1/services/index.js index 1fb983c..1c63f9d 100644 --- a/api/v1/services/index.js +++ b/api/v1/services/index.js @@ -2,7 +2,9 @@ module.exports = { AuthService: require('./AuthService'), MailService: require('./MailService'), PermissionService: require('./PermissionService'), + ProjectService: require('./ProjectService'), StorageService: require('./StorageService'), UserService: require('./UserService'), - TokenService: require('./TokenService') + TokenService: require('./TokenService'), + RegistrationService: require('./RegistrationService') }; diff --git a/api/v1/utils/errors.js b/api/v1/utils/errors.js index 284b9c6..7079ea9 100644 --- a/api/v1/utils/errors.js +++ b/api/v1/utils/errors.js @@ -6,8 +6,16 @@ var InvalidParameterError = require('../errors/InvalidParameterError'); * @throws {InvalidParameterError} the re-thrown error */ module.exports.handleValidationError = function (error) { - var errorSource = error.keys()[0]; - var errorDetail = error.errors[errorSource].message; + var errorKey = error.keys()[0]; + var specificError = error.errors[errorKey]; + + var errorDetail = specificError.message; + var errorSource; + while (specificError.key) { + // find the most-complete error source in the error stack + errorSource = specificError.key; + specificError = (specificError.errors) ? specificError.errors[0] : undefined; + } throw new InvalidParameterError(errorDetail, errorSource); }; diff --git a/api/v1/utils/index.js b/api/v1/utils/index.js index 9732dee..ac053c3 100644 --- a/api/v1/utils/index.js +++ b/api/v1/utils/index.js @@ -5,5 +5,7 @@ module.exports = { roles: require('./roles.js'), scopes: require('./scopes.js'), storage: require('./storage.js'), - time: require('./time.js') + time: require('./time.js'), + registration: require('./registration.js'), + validators: require('./validators.js') }; diff --git a/api/v1/utils/registration.js b/api/v1/utils/registration.js new file mode 100644 index 0000000..47df36d --- /dev/null +++ b/api/v1/utils/registration.js @@ -0,0 +1,35 @@ +var _ = require('lodash'); + +var TSHIRT_SIZES = ['S', 'M', 'L', 'XL']; +var STATUSES = ['ACCEPTED', 'WAITLISTED', 'REJECTED', 'PENDING']; + + +/** + * Ensures that the provided tshirt-size is in the list + * of valid size options + * @param {String} size the value to check + * @return {Boolean} true when the size is valid + * @throws TypeError when the size is invalid + */ +module.exports.verifyTshirtSize = function (size) { + if (!_.includes(TSHIRT_SIZES, size)) { + throw new TypeError(size + " is not a valid size"); + } + + return true; +}; + +/** + * Ensures that the provided status is in the list + * of valid status options + * @param {String} size the value to check + * @return {Boolean} true when the status is valid + * @throws TypeError when the status is invalid + */ +module.exports.verifyStatus = function (status) { + if (!_.includes(STATUSES, status)) { + throw new TypeError(status + " is not a valid status"); + } + + return true; +}; diff --git a/api/v1/utils/validators.js b/api/v1/utils/validators.js new file mode 100644 index 0000000..e9d974d --- /dev/null +++ b/api/v1/utils/validators.js @@ -0,0 +1,24 @@ +var checkit = require('checkit'); +var _ = require('lodash'); +var _Promise = require('bluebird'); + +module.exports.nested = function(validations, parentName){ + return function(value){ + return checkit(validations).run(value) + .catch(checkit.Error, function (error) { + var specificError = error.errors[error.keys()[0]]; + specificError.key = parentName + '.' + specificError.key; + + throw specificError; + }); + }; +}; + +module.exports.array = function(validator, parentName){ + return function(value){ + return _Promise.all(_.map(value, validator)) + .then(function(value){ + return true; + }); + }; +}; diff --git a/database/migration/V20161004_0917__createMentors.sql b/database/migration/V20161004_0917__createMentors.sql new file mode 100644 index 0000000..76226f4 --- /dev/null +++ b/database/migration/V20161004_0917__createMentors.sql @@ -0,0 +1,34 @@ +CREATE TABLE `mentors` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `user_id` INT UNSIGNED NOT NULL, + `first_name` VARCHAR(255) NOT NULL, + `last_name` VARCHAR(255) NOT NULL, + `shirt_size` ENUM('S', 'M', 'L', 'XL') NOT NULL, + `github` VARCHAR(50) NULL, + `location` VARCHAR(255) NOT NULL, + `summary` VARCHAR(255) NOT NULL, + `occupation` VARCHAR(100) NOT NULL, + `status` ENUM('ACCEPTED', 'WAITLISTED', 'REJECTED', 'PENDING') NOT NULL DEFAULT 'PENDING', + PRIMARY KEY (`id`), + INDEX `fk_mentors_user_id_idx` (`user_id` ASC), + CONSTRAINT `fk_mentors_user_id` + FOREIGN KEY (`user_id`) + REFERENCES `users` (`id`) + ON DELETE NO ACTION + ON UPDATE NO ACTION +); + +CREATE TABLE `mentor_project_ideas` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `mentor_id` INT UNSIGNED NOT NULL, + `link` VARCHAR(255) NOT NULL, + `contributions` VARCHAR(255) NOT NULL, + `ideas` VARCHAR(255) NOT NULL, + PRIMARY KEY (`id`), + INDEX `fk_mentor_project_ideas_mentor_id_idx` (`mentor_id` ASC), + CONSTRAINT `fk_mentor_project_ideas_mentor_id` + FOREIGN KEY (`mentor_id`) + REFERENCES `mentors` (`id`) + ON DELETE NO ACTION + ON UPDATE NO ACTION +); diff --git a/database/migration/V20161105_1750__createProjects.sql b/database/migration/V20161105_1750__createProjects.sql new file mode 100644 index 0000000..cb8b633 --- /dev/null +++ b/database/migration/V20161105_1750__createProjects.sql @@ -0,0 +1,8 @@ +CREATE TABLE `projects` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `name` VARCHAR(100) NOT NULL, + `description` VARCHAR(200) NOT NULL, + `repo` VARCHAR(255) NULL, + `is_published` TINYINT(1) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`) +); \ No newline at end of file diff --git a/database/migration/V20161105_2059__createProjectMentors.sql b/database/migration/V20161105_2059__createProjectMentors.sql new file mode 100644 index 0000000..80e713d --- /dev/null +++ b/database/migration/V20161105_2059__createProjectMentors.sql @@ -0,0 +1,18 @@ +CREATE TABLE `project_mentors` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `project_id` INT UNSIGNED NOT NULL, + `mentor_id` INT UNSIGNED NOT NULL, + PRIMARY KEY (`id`), + INDEX `fk_project_mentors_projects_id_idx` (`project_id` ASC), + INDEX `fk_project_mentors_mentors_id_idx` (`mentor_id` ASC), + CONSTRAINT `project_id` + FOREIGN KEY (`mentor_id`) + REFERENCES `projects` (`id`) + ON DELETE NO ACTION + ON UPDATE NO ACTION, + CONSTRAINT `mentor_id` + FOREIGN KEY (`mentor_id`) + REFERENCES `mentors` (`id`) + ON DELETE NO ACTION + ON UPDATE NO ACTION +); diff --git a/database/revert/V20161004_0917__createMentors.revert.sql b/database/revert/V20161004_0917__createMentors.revert.sql new file mode 100644 index 0000000..810db6c --- /dev/null +++ b/database/revert/V20161004_0917__createMentors.revert.sql @@ -0,0 +1,2 @@ +DROP TABLE `mentor_project_ideas`; +DROP TABLE `mentors`;