├── .gitignore ├── README.md ├── _config.yml ├── app.js ├── bin └── www ├── config ├── dbconfig.json └── passport.js ├── hello-api.postman_collection.json ├── models ├── car.js ├── index.js └── user.js ├── modules ├── account-manager.js ├── data-manager.js └── encrypt.js ├── package.json └── routes ├── auth.js └── cars.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # node-waf configuration 27 | .lock-wscript 28 | 29 | # Compiled binary addons (http://nodejs.org/api/addons.html) 30 | build/Release 31 | 32 | # Dependency directories 33 | node_modules 34 | jspm_packages 35 | 36 | # Optional npm cache directory 37 | .npm 38 | 39 | # Optional eslint cache 40 | .eslintcache 41 | 42 | # Optional REPL history 43 | .node_repl_history 44 | 45 | # Output of 'npm pack' 46 | *.tgz 47 | 48 | # Yarn Integrity file 49 | .yarn-integrity 50 | 51 | 52 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hello-api 2 | A simple REST api built on nodejs with a mysql database, redis session store, and passport authentication 3 | 4 | ## Install the essentials 5 | 6 | install nodejs [here](https://nodejs.org/en/download/package-manager/) if you already have it, cool, here's a cookie 🍪 7 | 8 | install mysql [here](https://dev.mysql.com/downloads/) 9 | 10 | # Getting started 11 | Clone the repo `git clone https://github.com/thenoblesavage/hello-api.git` 12 | 13 | install the dependencies `npm install` 14 | then run `npm start` to start the server 15 | 16 | # Sequelize 17 | 18 | This framework makes it really easy for you to write queries and get data directly from the database... yea so no more SQL queries... 19 | you can check out the docs [here](http://docs.sequelizejs.com/en/latest/docs/getting-started/#your-first-query) 20 | 21 | ## Adding stuff?? 22 | Out of the box, this code will let you login, signup, and that's all, but you can add more features later 23 | To add more functionality to the app, this is what you should do 24 | 25 | 1) Make the model 26 | 2) Make the route file and handle each request and response 27 | 3) Make the queries in the modules/data-manager.js file to get the data 28 | 29 | For example, maybe you wanted a way to create, delete, update, and get a list of cars. Here's what you'd have to do... 30 | 31 | ### Make the model 32 | 33 | Start by making a cars.js file and adding it to /models/ folder. 34 | That model is used to map data to the database so it's important you get this right, if you need help modelling the objects, check out [this](http://docs.sequelizejs.com/en/v3/docs/models-definition/) page for more info. The model object you make is going to be used to make queries later on. You can also do joins and subqueries within the model file 35 | 36 | in models/car.js 37 | ```javascript 38 | "use strict"; 39 | 40 | module.exports = function (sequelize, DataTypes) { 41 | var Car = sequelize.define('Car', { 42 | id: { 43 | type: DataTypes.INTEGER, 44 | primaryKey: true, 45 | autoIncrement: true 46 | }, 47 | make: { 48 | type: DataTypes.STRING 49 | }, 50 | model: { 51 | type: DataTypes.STRING 52 | }, 53 | color: { 54 | type: DataTypes.STRING 55 | }, 56 | licensePlate: { 57 | type: DataTypes.STRING 58 | }, 59 | //id of the user that owns the car 60 | ownerId: { 61 | defaultValue: null, 62 | type: DataTypes.INTEGER 63 | } 64 | }, { 65 | classMethods: { 66 | associate: function (models) { 67 | 68 | } 69 | }, 70 | // disable the modification of table names; By default, sequelize will automatically 71 | // transform all passed model names (first parameter of define) into plural. 72 | // if you don't want that, set the following 73 | //freezeTableName: true, 74 | 75 | // define the table's name 76 | tableName: 'Car' 77 | }, { 78 | dialect: 'mysql' 79 | }); 80 | 81 | return Car; 82 | }; 83 | ``` 84 | 85 | 86 | ### Make the route file and handle each request and response 87 | - Then, make a corresponding file, cars.js in the /routes/ folder 88 | This is where you handle network requests (it's also called the middlware), so if a user makes a GET request at the route /cars/ it should return a json array of car objects. 89 | Conversly, if they make a POST at the route /cars/ a new car object will be created in the data base with the information in the body of the request. 90 | 91 | in routes/cars.js 92 | ```javascript 93 | var express = require('express'); 94 | var router = express.Router(); 95 | var DM = require('../modules/data-manager'); 96 | 97 | /** 98 | * GET a list of cars that the current user has created 99 | * */ 100 | router.get('/', function (req, res, next) { 101 | var user = req.user; 102 | DM.getAllCars(user.id, function (cars) { 103 | res.status(200).send({status: 200, cars: cars}); 104 | }); 105 | }); 106 | 107 | /** 108 | * POST create a new car 109 | * 110 | * The request body should look like this... 111 | * 112 | * { 113 | * "make": "Ferrari", 114 | * "model": "F12", 115 | * "color": "Red", 116 | * "licensePlate": "FastAF" 117 | * } 118 | * 119 | * */ 120 | router.post('/', function (req, res) { 121 | var user = req.user; 122 | 123 | var post = req.body; 124 | var newData = { 125 | make: post.make, 126 | model: post.model, 127 | color: post.color, 128 | licensePlate: post.licensePlate, 129 | ownerId: user.id 130 | }; 131 | 132 | DM.createCar(user.id, newData, function (car, err) { 133 | if (car) { 134 | res.status(200).send({status: 200, message: "created!", car: car}); 135 | } else { 136 | res.status(400).send({status: 400, message: "Error creating car", err: err}); 137 | } 138 | }) 139 | }); 140 | 141 | /** 142 | * GET a car by id 143 | * anyone can access this 144 | * */ 145 | router.get('/:carId', function (req, res, next) { 146 | var carId = req.params.carId; 147 | 148 | if (!carId) { 149 | return res.status(400).send({status: 400, message: "No car id specified"}); 150 | } 151 | 152 | DM.getCar(carId, function (car) { 153 | res.status(200).send({status: 200, car: car}); 154 | }); 155 | }); 156 | 157 | /** 158 | * PUT update the information about the car 159 | * 160 | * in the request body include something like the following 161 | * 162 | * { 163 | * "make": "Ferrari", 164 | * "model": "FF", 165 | * "color": "White", 166 | * "licensePlate": "NiceAF" 167 | * } 168 | * 169 | * */ 170 | router.put('/:carId', function (req, res) { 171 | var user = req.user; 172 | var carId = req.params.carId; 173 | 174 | if (!carId) { 175 | return res.status(400).send({status: 400, message: "No car id specified"}); 176 | } 177 | 178 | //the post made by the user 179 | var post = req.body; 180 | var newData = {id: carId}; 181 | if (post.make) newData.make = post.make; 182 | if (post.model) newData.model = post.model; 183 | if (post.color) newData.color = post.color; 184 | if (post.licensePlate) newData.licensePlate = post.licensePlate; 185 | 186 | DM.updateCarInfo(user.id, newData, function (car, err) { 187 | console.log(err); 188 | if (car) { 189 | res.status(200).send({status: 200, message: "Updated!", car: car}); 190 | } else { 191 | res.status(400).send({status: 400, message: err}); 192 | } 193 | }) 194 | }); 195 | 196 | /** 197 | * DELETE delete a car 198 | * */ 199 | router.delete('/:carId', function (req, res) { 200 | var user = req.user; 201 | var carId = req.params.carId; 202 | 203 | if (!carId) { 204 | return res.status(400).send({status: 400, message: "No car id specified"}); 205 | } 206 | 207 | DM.deleteCar(user.id, carId, function (deletedCar) { 208 | if (deletedCar) { 209 | res.status(200).send({status: 200, message: "Deleted car"}); 210 | } else { 211 | res.status(400).send({status: 400, message: "Can't delete car"}); 212 | } 213 | }) 214 | }); 215 | 216 | 217 | module.exports = router; 218 | ``` 219 | 220 | Import the route to the app.js file so it can process requests 221 | ```javascript 222 | ... 223 | 224 | //import route files here 225 | var auth = require('./routes/auth')(passport); 226 | var cars = require('./routes/cars'); 227 | //set the routes 228 | app.use('/auth', auth); //routes that are exposed 229 | app.use('/cars', isLoggedIn, cars); 230 | 231 | ... 232 | ``` 233 | 234 | ### Make the queries in the modules/data-manager.js file to get the data 235 | 236 | make queries and update the methods in the data-manager file. That's a singular place where all queries to the database are made so that this way it's easier to debug. 237 | 238 | ```javascript 239 | var models = require('../models'); 240 | 241 | /** 242 | * gets the list of cars associated with the user 243 | * */ 244 | exports.getAllCars = function (userId, callback) { 245 | models.Car.findAll({ 246 | where: { 247 | ownerId: userId 248 | } 249 | }).then(function (cars) { 250 | callback(cars); 251 | }); 252 | }; 253 | 254 | /** 255 | * creates a new car and puts it in the database 256 | * */ 257 | exports.createCar = function (userId, newData, callback) { 258 | var car = {ownerId: userId}; 259 | if (newData.make) car.make = newData.make; 260 | if (newData.model) car.model = newData.model; 261 | if (newData.color) car.color = newData.color; 262 | if (newData.licensePlate) car.licensePlate = newData.licensePlate; 263 | 264 | models.Car.create(car).then(function (car) { 265 | callback(car); 266 | }); 267 | }; 268 | 269 | /** 270 | * gets a car by id 271 | * */ 272 | exports.getCar = function (carId, callback) { 273 | models.Car.find({ 274 | where: { 275 | id: carId 276 | } 277 | }).then(function (car) { 278 | callback(car); 279 | }); 280 | }; 281 | 282 | /** 283 | * updates the car info by id 284 | * */ 285 | exports.updateCarInfo = function (userId, newData, callback) { 286 | models.Car.find({ 287 | where: { 288 | id: newData.id 289 | } 290 | }).then(function (car) { 291 | if (car) { 292 | if(car.ownerId != userId){ 293 | return callback(null, "You don't own that car"); 294 | } 295 | if (newData.make) car.make = newData.make; 296 | if (newData.model) car.model = newData.model; 297 | if (newData.color) car.color = newData.color; 298 | if (newData.licensePlate) car.licensePlate = newData.licensePlate; 299 | car.save().then((saved) => { 300 | callback(saved); 301 | }) 302 | } else { 303 | callback(null, "Car not found"); 304 | } 305 | }); 306 | }; 307 | 308 | /** 309 | * deletes a car object 310 | * */ 311 | exports.deleteCar = function (userId, carId, callback) { 312 | models.Car.find({ 313 | where: { 314 | id: carId 315 | } 316 | }).then(function (car) { 317 | if (car) { 318 | //check if the user is the owner of the car otherwise they can't delete it 319 | if(car.ownerId != userId){ 320 | return callback(null, "You don't own that car"); 321 | } 322 | car.destroy().then(callback); 323 | } else { 324 | callback(null, "Car not found"); 325 | } 326 | }); 327 | }; 328 | 329 | /** 330 | * gets the list of all users 331 | * */ 332 | exports.getAllUsers = function (callback) { 333 | models.User.findAll().then(function (users) { 334 | if (users) { 335 | callback(users); 336 | } else { 337 | callback(null); 338 | } 339 | }); 340 | }; 341 | 342 | ``` 343 | 344 | 345 | ## Testing 346 | There is a really great doc about the best practices for building a REST api, you can check it out [here](http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api#restful) 347 | if you want to test the api, you can use [Postman](https://www.getpostman.com/) to make requests to the endpoints 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var favicon = require('serve-favicon'); 4 | var logger = require('morgan'); 5 | var cookieParser = require('cookie-parser'); 6 | var cookieSession = require('cookie-session'); 7 | var bodyParser = require('body-parser'); 8 | var redis = require('redis'); 9 | var session = require('express-session'); 10 | var passport = require('passport'); 11 | require('./config/passport')(passport); 12 | 13 | var app = express(); 14 | 15 | var rds = redis.createClient({port: 6379, host: 'localhost', db: 1}); 16 | app.use(bodyParser.json()); 17 | app.use(cookieParser('random-key')); 18 | app.use(cookieSession({secret: "random-key"})); 19 | var sessionMiddleware = session({ 20 | store: rds, // XXX redis server config 21 | secret: "random-key", 22 | resave: true, 23 | saveUninitialized: false 24 | }); 25 | 26 | app.use(sessionMiddleware); 27 | app.use(passport.initialize()); 28 | app.use(passport.session()); 29 | 30 | function isLoggedIn(req, res, next) { 31 | //console.log(req) 32 | if (req.isAuthenticated()) { 33 | return next(); 34 | } 35 | return res.status(401).send({status: 401, message: "You must be logged in."}) 36 | } 37 | 38 | // uncomment after placing your favicon in /public 39 | //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); 40 | app.use(logger('dev')); 41 | app.use(bodyParser.json()); 42 | app.use(bodyParser.urlencoded({extended: false})); 43 | app.use(cookieParser()); 44 | 45 | 46 | app.use(function (req, res, next) { 47 | res.header("Access-Control-Allow-Credentials", "true"); 48 | res.header("Access-Control-Allow-Origin", "http://localhost:3000"); 49 | res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE'); 50 | res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Authorization, Accept"); 51 | res.header('Cache-Control', 'no-store, no-cache'); 52 | 53 | // intercept OPTIONS method 54 | if ('OPTIONS' == req.method) { 55 | res.send(200); 56 | } 57 | else { 58 | next(); 59 | } 60 | }); 61 | 62 | //import route files here 63 | var auth = require('./routes/auth')(passport); 64 | var cars = require('./routes/cars'); 65 | //set the routes 66 | app.use('/auth', auth); //routes that are exposed 67 | app.use('/cars', isLoggedIn, cars); 68 | 69 | // catch 404 and forward to error handler 70 | app.use(function (req, res, next) { 71 | res.status(404).send({status: 404, message: "Route not found!"}); 72 | //next(err); 73 | }); 74 | 75 | // error handler 76 | app.use(function (err, req, res, next) { 77 | // set locals, only providing error in development 78 | res.locals.message = err.message; 79 | res.locals.error = req.app.get('env') === 'development' ? err : {}; 80 | 81 | // render the error page 82 | if(err == "invalid session, please login again") { 83 | req.logOut() 84 | } 85 | res.status(500).send({status: 500, message: "Server Error!", err: err }); 86 | console.log(err); 87 | }); 88 | 89 | module.exports = app; 90 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('hello-api:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * linked sequelize to the database 26 | * this part goes to the models directory and makes the connections between everything in mysql and the js code 27 | * */ 28 | require('../models').sequelize.sync({ 29 | //force: true 30 | }).then(function () { 31 | /** 32 | * Listen on provided port, on all network interfaces. 33 | */ 34 | server.listen(port, function () { 35 | console.log('Express server listening on port ' + server.address().port); 36 | debug('Express server listening on port ' + server.address().port); 37 | }); 38 | server.on('error', onError); 39 | server.on('listening', onListening); 40 | }); 41 | console.log('Connecting to DB please wait...'); 42 | 43 | /** 44 | * Listen on provided port, on all network interfaces. 45 | */ 46 | 47 | server.listen(port); 48 | server.on('error', onError); 49 | server.on('listening', onListening); 50 | 51 | /** 52 | * Normalize a port into a number, string, or false. 53 | */ 54 | 55 | function normalizePort(val) { 56 | var port = parseInt(val, 10); 57 | 58 | if (isNaN(port)) { 59 | // named pipe 60 | return val; 61 | } 62 | 63 | if (port >= 0) { 64 | // port number 65 | return port; 66 | } 67 | 68 | return false; 69 | } 70 | 71 | /** 72 | * Event listener for HTTP server "error" event. 73 | */ 74 | 75 | function onError(error) { 76 | if (error.syscall !== 'listen') { 77 | throw error; 78 | } 79 | 80 | var bind = typeof port === 'string' 81 | ? 'Pipe ' + port 82 | : 'Port ' + port; 83 | 84 | // handle specific listen errors with friendly messages 85 | switch (error.code) { 86 | case 'EACCES': 87 | console.error(bind + ' requires elevated privileges'); 88 | process.exit(1); 89 | break; 90 | case 'EADDRINUSE': 91 | console.error(bind + ' is already in use'); 92 | process.exit(1); 93 | break; 94 | default: 95 | throw error; 96 | } 97 | } 98 | 99 | /** 100 | * Event listener for HTTP server "listening" event. 101 | */ 102 | 103 | function onListening() { 104 | var addr = server.address(); 105 | var bind = typeof addr === 'string' 106 | ? 'pipe ' + addr 107 | : 'port ' + addr.port; 108 | debug('Listening on ' + bind); 109 | } 110 | -------------------------------------------------------------------------------- /config/dbconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "development": { 3 | "username": "root", 4 | "password": "password", 5 | "database": "hello-api-database", 6 | "host": "127.0.0.1", 7 | "dialect": "mysql" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /config/passport.js: -------------------------------------------------------------------------------- 1 | var models = require('../models'); 2 | //var bCrypt = require('bcrypt-nodejs'); 3 | var AM = require('../modules/account-manager'); 4 | var passport = require('passport'); 5 | var LocalStrategy = require('passport-local').Strategy; 6 | 7 | module.exports = function (passport) { 8 | 9 | // Passport needs to be able to serialize and deserialize users to support persistent login sessions 10 | passport.serializeUser(function (user, done) { 11 | console.log('serializing user:', user.email); 12 | return done(null, user); 13 | }); 14 | 15 | passport.deserializeUser(function (user, done) { 16 | console.log("deserializing user " + user.email); 17 | try { 18 | models.User.find({ 19 | where: {id: user.id}, 20 | attributes: ['id', 'first', 'last', 'email'] 21 | }).then(function (user) { 22 | if (!user) { 23 | return done("invalid session, please login again", null); 24 | } 25 | return done(null, user); 26 | }).error((err) => { 27 | return done("User does not exist", null); 28 | }); 29 | } catch (err) { 30 | return done("Couldn't deserialize user... invalid session" + err, null); 31 | } 32 | }); 33 | 34 | passport.use('login', new LocalStrategy({ 35 | passReqToCallback: true 36 | }, function (req, username, password, done) { 37 | 38 | if (!username || !password) { 39 | return done(null, false, 'Missing Fields'); 40 | } 41 | 42 | AM.manualLogin(username, password, (err, user) => { 43 | if (err) { 44 | return done(null, false, 'Invalid username or password.'); 45 | } else { 46 | return done(null, user, 'Signed in succesfully!'); 47 | } 48 | }); 49 | })); 50 | 51 | passport.use('signup', new LocalStrategy({ 52 | passReqToCallback: true 53 | }, function (req, username, password, done) { 54 | // Check if all the required fields are gotten 55 | username = req.body.username; 56 | password = req.body.password; 57 | var reqEmail = req.body.email; 58 | var firstName = req.body.firstName; 59 | var lastName = req.body.lastName; 60 | 61 | if (!username || !password || !reqEmail || !lastName || !firstName) { 62 | return done(null, false, 'Missing Fields'); 63 | } 64 | 65 | AM.addNewAccount({ 66 | username: username, 67 | password: password, 68 | first: firstName, 69 | last: lastName, 70 | email: reqEmail 71 | }, (err, user) => { 72 | if (err) { 73 | return done(null, false, err); 74 | } else { 75 | return done(null, user, 'Signup successful'); 76 | } 77 | }); 78 | 79 | })); 80 | 81 | }; 82 | -------------------------------------------------------------------------------- /hello-api.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "variables": [], 3 | "info": { 4 | "name": "hello-api", 5 | "_postman_id": "ff22906c-b245-48f0-c028-b0a0cf37e899", 6 | "description": "testing endpoints for hello-api", 7 | "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" 8 | }, 9 | "item": [ 10 | { 11 | "name": "Signup user", 12 | "request": { 13 | "url": "http://localhost:3000/auth/signup", 14 | "method": "POST", 15 | "header": [ 16 | { 17 | "key": "Content-Type", 18 | "value": "application/x-www-form-urlencoded", 19 | "description": "" 20 | } 21 | ], 22 | "body": { 23 | "mode": "urlencoded", 24 | "urlencoded": [ 25 | { 26 | "key": "username", 27 | "value": "ankit", 28 | "type": "text", 29 | "enabled": true 30 | }, 31 | { 32 | "key": "password", 33 | "value": "password", 34 | "type": "text", 35 | "enabled": true 36 | }, 37 | { 38 | "key": "firstName", 39 | "value": "Ankit", 40 | "type": "text", 41 | "enabled": true 42 | }, 43 | { 44 | "key": "lastName", 45 | "value": "Sheth", 46 | "type": "text", 47 | "enabled": true 48 | }, 49 | { 50 | "key": "email", 51 | "value": "ankit.s1213@gmail.com", 52 | "type": "text", 53 | "enabled": true 54 | } 55 | ] 56 | }, 57 | "description": "Route to signup the user" 58 | }, 59 | "response": [] 60 | }, 61 | { 62 | "name": "Login", 63 | "request": { 64 | "url": "http://localhost:3000/auth/login", 65 | "method": "POST", 66 | "header": [ 67 | { 68 | "key": "Content-Type", 69 | "value": "application/x-www-form-urlencoded", 70 | "description": "" 71 | } 72 | ], 73 | "body": { 74 | "mode": "urlencoded", 75 | "urlencoded": [ 76 | { 77 | "key": "username", 78 | "value": "ankit", 79 | "type": "text", 80 | "enabled": true 81 | }, 82 | { 83 | "key": "password", 84 | "value": "password", 85 | "type": "text", 86 | "enabled": true 87 | } 88 | ] 89 | }, 90 | "description": "route to login the user, this will return a cookie in the header of the response" 91 | }, 92 | "response": [] 93 | }, 94 | { 95 | "name": "Check login", 96 | "request": { 97 | "url": "http://localhost:3000/auth/", 98 | "method": "GET", 99 | "header": [], 100 | "body": {}, 101 | "description": "Route to check to see if the user is logged in" 102 | }, 103 | "response": [] 104 | }, 105 | { 106 | "name": "Logout", 107 | "request": { 108 | "url": "http://localhost:3000/auth/logout", 109 | "method": "GET", 110 | "header": [], 111 | "body": {}, 112 | "description": "Route to logout the user from the app" 113 | }, 114 | "response": [] 115 | }, 116 | { 117 | "name": "Get cars", 118 | "request": { 119 | "url": "http://localhost:3000/cars", 120 | "method": "GET", 121 | "header": [], 122 | "body": {}, 123 | "description": "route to get a list of cars that the user has saved" 124 | }, 125 | "response": [] 126 | }, 127 | { 128 | "name": "create car", 129 | "request": { 130 | "url": "http://localhost:3000/cars", 131 | "method": "POST", 132 | "header": [ 133 | { 134 | "key": "Content-Type", 135 | "value": "application/x-www-form-urlencoded", 136 | "description": "" 137 | } 138 | ], 139 | "body": { 140 | "mode": "urlencoded", 141 | "urlencoded": [ 142 | { 143 | "key": "make", 144 | "value": "Ferrari", 145 | "type": "text", 146 | "enabled": true 147 | }, 148 | { 149 | "key": "model", 150 | "value": "458", 151 | "type": "text", 152 | "enabled": true 153 | }, 154 | { 155 | "key": "licensePlate", 156 | "value": "DOPEAF", 157 | "type": "text", 158 | "enabled": true 159 | }, 160 | { 161 | "key": "color", 162 | "value": "Ferrari Red", 163 | "type": "text", 164 | "enabled": true 165 | } 166 | ] 167 | }, 168 | "description": "gets a list of cars associated with the user" 169 | }, 170 | "response": [] 171 | }, 172 | { 173 | "name": "update car", 174 | "request": { 175 | "url": "http://localhost:3000/cars/2", 176 | "method": "PUT", 177 | "header": [ 178 | { 179 | "key": "Content-Type", 180 | "value": "application/x-www-form-urlencoded", 181 | "description": "" 182 | } 183 | ], 184 | "body": { 185 | "mode": "urlencoded", 186 | "urlencoded": [ 187 | { 188 | "key": "make", 189 | "value": "Ferrari", 190 | "type": "text", 191 | "enabled": true 192 | }, 193 | { 194 | "key": "model", 195 | "value": "FF", 196 | "type": "text", 197 | "enabled": true 198 | }, 199 | { 200 | "key": "licensePlate", 201 | "value": "NiceAF", 202 | "type": "text", 203 | "enabled": true 204 | }, 205 | { 206 | "key": "color", 207 | "value": "White", 208 | "type": "text", 209 | "enabled": true 210 | } 211 | ] 212 | }, 213 | "description": "Updates the car information" 214 | }, 215 | "response": [] 216 | }, 217 | { 218 | "name": "get specific car", 219 | "request": { 220 | "url": "http://localhost:3000/cars/2", 221 | "method": "GET", 222 | "header": [], 223 | "body": {}, 224 | "description": "Gets a car object at an id" 225 | }, 226 | "response": [] 227 | }, 228 | { 229 | "name": "delete car", 230 | "request": { 231 | "url": "http://localhost:3000/cars/2", 232 | "method": "DELETE", 233 | "header": [], 234 | "body": {}, 235 | "description": "Deletes a car object" 236 | }, 237 | "response": [] 238 | } 239 | ] 240 | } -------------------------------------------------------------------------------- /models/car.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by deep on 4/5/17. 3 | */ 4 | "use strict"; 5 | 6 | module.exports = function (sequelize, DataTypes) { 7 | var Car = sequelize.define('Car', { 8 | id: { 9 | type: DataTypes.INTEGER, 10 | primaryKey: true, 11 | autoIncrement: true 12 | }, 13 | make: { 14 | type: DataTypes.STRING 15 | }, 16 | model: { 17 | type: DataTypes.STRING 18 | }, 19 | color: { 20 | type: DataTypes.STRING 21 | }, 22 | licensePlate: { 23 | type: DataTypes.STRING 24 | }, 25 | //id of the user that owns the car 26 | ownerId: { 27 | defaultValue: null, 28 | type: DataTypes.INTEGER 29 | } 30 | }, { 31 | classMethods: { 32 | associate: function (models) { 33 | 34 | } 35 | }, 36 | // disable the modification of table names; By default, sequelize will automatically 37 | // transform all passed model names (first parameter of define) into plural. 38 | // if you don't want that, set the following 39 | //freezeTableName: true, 40 | 41 | // define the table's name 42 | tableName: 'Car' 43 | }, { 44 | dialect: 'mysql' 45 | }); 46 | 47 | return Car; 48 | }; -------------------------------------------------------------------------------- /models/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fs = require("fs"); 4 | var path = require("path"); 5 | var Sequelize = require("sequelize"); 6 | var env = process.env.NODE_ENV || "development"; 7 | var config = require(path.join(__dirname, '..', 'config', 'dbconfig.json'))[env]; 8 | 9 | if (process.env.DATABASE_URL) { 10 | var sequelize = new Sequelize(process.env.DATABASE_URL); 11 | } else { 12 | var sequelize = new Sequelize(config.database, config.username, config.password, config); 13 | } 14 | var db = {}; 15 | 16 | fs 17 | .readdirSync(__dirname) 18 | .filter(function (file) { 19 | return (file.indexOf(".") !== 0) && (file !== "index.js"); 20 | }) 21 | .forEach(function (file) { 22 | var model = sequelize.import(path.join(__dirname, file)); 23 | db[model.name] = model; 24 | }); 25 | 26 | Object.keys(db).forEach(function (modelName) { 27 | if ("associate" in db[modelName]) { 28 | db[modelName].associate(db); 29 | } 30 | }); 31 | 32 | db.sequelize = sequelize; 33 | db.Sequelize = Sequelize; 34 | 35 | module.exports = db; 36 | -------------------------------------------------------------------------------- /models/user.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by deep on 10/10/16. 3 | * data model class for a user 4 | */ 5 | "use strict"; 6 | 7 | module.exports = function (sequelize, DataTypes) { 8 | var User = sequelize.define('User', { 9 | id: { 10 | type: DataTypes.INTEGER, 11 | primaryKey: true, 12 | autoIncrement: true 13 | }, 14 | first: { 15 | type: DataTypes.STRING, 16 | allowNull: false 17 | }, 18 | last: { 19 | type: DataTypes.STRING, 20 | allowNull: false 21 | }, 22 | email: { 23 | type: DataTypes.STRING, 24 | allowNull: false 25 | }, 26 | username: { 27 | type: DataTypes.STRING, 28 | allowNull: false 29 | }, 30 | password: { 31 | type: DataTypes.STRING, 32 | allowNull: false 33 | }, 34 | createdAt: { 35 | type: DataTypes.DATE, 36 | defaultValue: sequelize.NOW 37 | }, 38 | updatedAt: { 39 | type: DataTypes.DATE, 40 | defaultValue: sequelize.NOW 41 | } 42 | }, { 43 | classMethods: { 44 | associate: function (models) { 45 | User.hasMany(models.Car, { 46 | foreignKey: 'ownerId' 47 | //automatically makes a join on the ownerId in this model with 48 | //id in the user table 49 | }); 50 | } 51 | }, 52 | // add the timestamp attributes (updatedAt, createdAt) 53 | timestamps: true, 54 | 55 | // disable the modification of table names; By default, sequelize will automatically 56 | // transform all passed model names (first parameter of define) into plural. 57 | // if you don't want that, set the following 58 | //freezeTableName: true, 59 | 60 | // define the table's name 61 | tableName: 'User' 62 | }, { 63 | dialect: 'mysql' 64 | }); 65 | 66 | return User; 67 | }; 68 | -------------------------------------------------------------------------------- /modules/account-manager.js: -------------------------------------------------------------------------------- 1 | var encrypt = require('./encrypt'); 2 | var models = require('../models'); 3 | 4 | 'use strict'; 5 | 6 | /* login validation methods */ 7 | 8 | exports.autoLogin = function (username, pass, callback) { 9 | console.log('DEBUG: account-manager: auto login'); 10 | //debug('account-manager: auto login'); 11 | models.User.find({where: {username: username}}).then(function (user) { 12 | if (user) { 13 | user.password == pass ? callback(user) : callback(null); 14 | } else { 15 | return callback(null); 16 | } 17 | }); 18 | }; 19 | 20 | exports.manualLogin = function (username, pass, callback) { 21 | console.log('DEBUG: account-manager: manual login'); 22 | models.User.find({where: {username: username}}).then(function (user) { 23 | if (user == null) { 24 | return callback('user-not-found'); 25 | } else { 26 | encrypt.validatePassword(pass, user.password, function (err, res) { 27 | if (res) { 28 | user.password = undefined; 29 | return callback(null, user); 30 | } else { 31 | return callback('invalid-password'); 32 | } 33 | }); 34 | } 35 | }); 36 | }; 37 | 38 | exports.addNewAccount = function (newData, callback) { 39 | //find a user that has an email or username that is equal to the attempted signup 40 | models.User.find({ 41 | where: { 42 | $or: [ 43 | {username: newData.username}, 44 | {email: newData.email} 45 | ] 46 | } 47 | } 48 | ).then(function (user) { 49 | if (user) { 50 | if (user.username.toLocaleLowerCase() == newData.username.toLocaleLowerCase()) { 51 | return callback("That username is already in use"); 52 | } else if (user.email.toLocaleLowerCase() == newData.email.toLocaleLowerCase()) { 53 | return callback("That email is already in use"); 54 | } 55 | 56 | } else { 57 | encrypt.saltAndHash(newData.password, function (hash) { 58 | newData.password = hash; 59 | 60 | models.User.create({ 61 | username: newData.username, 62 | password: newData.password, 63 | first: newData.first, 64 | last: newData.last, 65 | email: newData.email 66 | }).then((user) => { 67 | return callback(null, user); 68 | }); 69 | 70 | }); 71 | } 72 | }); 73 | }; 74 | 75 | exports.getProfile = function (id, callback) { 76 | models.User.find({ 77 | where: {id: id}, 78 | attributes: ['id', 'first', 'last', 'email', 'createdAt', 'updatedAt'] 79 | }).then(function (user) { 80 | if (!user) { 81 | return callback("Couldn't get user", null); 82 | } 83 | return callback(null, user); 84 | }).error((err) => { 85 | return callback("User does not exist", null); 86 | }); 87 | }; 88 | 89 | exports.updateAccount = function (newData, callback) { 90 | models.User.find({ 91 | where: { 92 | id: newData.id 93 | } 94 | }).then(function (user) { 95 | if (newData.first) user.first = newData.first; 96 | if (newData.last) user.last = newData.last; 97 | 98 | if (newData.email) { 99 | models.User.find({where: {email: newData.email}}).then(function (userWithEmail) { 100 | if (userWithEmail) { 101 | //there's already a user with that email... send an error 102 | return callback('email-taken', null); 103 | } else { 104 | //there isn't a user with that email, update the email 105 | user.email = newData.email; 106 | user.save().then(function (updatedUser) { 107 | if (updatedUser) { 108 | return callback(null, updatedUser) 109 | } else { 110 | return callback('error-updating-user', null); 111 | } 112 | }); 113 | } 114 | }); 115 | } else { 116 | user.save().then(function (updatedUser) { 117 | if (updatedUser) { 118 | return callback(null, updatedUser) 119 | } else { 120 | return callback('error-updating-user', null); 121 | } 122 | }); 123 | } 124 | 125 | }); 126 | }; 127 | 128 | exports.updatePassword = function (email, newPass, callback) { 129 | models.User.find({where: {email: email}}).then(function (user) { 130 | 131 | encrypt.saltAndHash(newPass, function (pass) { 132 | user.update({password: pass}).then(callback) 133 | }); 134 | 135 | }); 136 | }; 137 | 138 | /* account lookup methods */ 139 | 140 | exports.deleteAccount = function (id, callback) { 141 | 142 | }; 143 | 144 | exports.getAccountByEmail = function (email, callback) { 145 | 146 | }; 147 | 148 | exports.validateResetLink = function (email, passHash, callback) { 149 | models.User.find({where: {email: email, password: passHash}}).then(function (user) { 150 | return callback(user ? 'ok' : null); 151 | }); 152 | }; 153 | 154 | exports.getAllRecords = function (callback) { 155 | models.User.findAll().then(function (users) { 156 | return callback(null, users) 157 | }); 158 | }; 159 | 160 | exports.delAllRecords = function (callback) { 161 | //accounts.remove({}, callback); // reset accounts collection for testing // 162 | return callback('Nahh I\'m not doing that go fuck yourself...'); 163 | }; 164 | -------------------------------------------------------------------------------- /modules/data-manager.js: -------------------------------------------------------------------------------- 1 | var models = require('../models'); 2 | 3 | /** 4 | * gets the list of cars associated with the user 5 | * */ 6 | exports.getAllCars = function (userId, callback) { 7 | models.Car.findAll({ 8 | where: { 9 | ownerId: userId 10 | } 11 | }).then(function (cars) { 12 | callback(cars); 13 | }); 14 | }; 15 | 16 | /** 17 | * creates a new car and puts it in the database 18 | * */ 19 | exports.createCar = function (userId, newData, callback) { 20 | var car = {ownerId: userId}; 21 | if (newData.make) car.make = newData.make; 22 | if (newData.model) car.model = newData.model; 23 | if (newData.color) car.color = newData.color; 24 | if (newData.licensePlate) car.licensePlate = newData.licensePlate; 25 | 26 | models.Car.create(car).then(function (car) { 27 | callback(car); 28 | }); 29 | }; 30 | 31 | /** 32 | * gets a car by id 33 | * */ 34 | exports.getCar = function (carId, callback) { 35 | models.Car.find({ 36 | where: { 37 | id: carId 38 | } 39 | }).then(function (car) { 40 | callback(car); 41 | }); 42 | }; 43 | 44 | /** 45 | * updates the car info by id 46 | * */ 47 | exports.updateCarInfo = function (userId, newData, callback) { 48 | models.Car.find({ 49 | where: { 50 | id: newData.id 51 | } 52 | }).then(function (car) { 53 | if (car) { 54 | if(car.ownerId != userId){ 55 | return callback(null, "You don't own that car"); 56 | } 57 | if (newData.make) car.make = newData.make; 58 | if (newData.model) car.model = newData.model; 59 | if (newData.color) car.color = newData.color; 60 | if (newData.licensePlate) car.licensePlate = newData.licensePlate; 61 | car.save().then((saved) => { 62 | callback(saved); 63 | }) 64 | } else { 65 | callback(null, "Car not found"); 66 | } 67 | }); 68 | }; 69 | 70 | /** 71 | * deletes a car object 72 | * */ 73 | exports.deleteCar = function (userId, carId, callback) { 74 | models.Car.find({ 75 | where: { 76 | id: carId 77 | } 78 | }).then(function (car) { 79 | if (car) { 80 | //check if the user is the owner of the car otherwise they can't delete it 81 | if(car.ownerId != userId){ 82 | return callback(null, "You don't own that car"); 83 | } 84 | car.destroy().then(callback); 85 | } else { 86 | callback(null, "Car not found"); 87 | } 88 | }); 89 | }; 90 | 91 | /** 92 | * gets the list of all users 93 | * */ 94 | exports.getAllUsers = function (callback) { 95 | models.User.findAll().then(function (users) { 96 | if (users) { 97 | callback(users); 98 | } else { 99 | callback(null); 100 | } 101 | }); 102 | }; 103 | -------------------------------------------------------------------------------- /modules/encrypt.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by deep on 12/6/16. 3 | */ 4 | var crypto = require('crypto'); 5 | 6 | /* encryption & validation methods */ 7 | exports.generateSalt = function () { 8 | var set = '0123456789abcdefghijklmnopqurstuvwxyzABCDEFGHIJKLMNOPQURSTUVWXYZ'; 9 | var salt = ''; 10 | for (var i = 0; i < 10; i++) { 11 | var p = Math.floor(Math.random() * set.length); 12 | salt += set[p]; 13 | } 14 | return salt; 15 | }; 16 | 17 | exports.md5 = function (str) { 18 | return crypto.createHash('md5').update(str).digest('hex'); 19 | }; 20 | 21 | exports.saltAndHash = function (pass, callback) { 22 | var salt = this.generateSalt(); 23 | callback(salt + this.md5(pass + salt)); 24 | }; 25 | 26 | exports.validatePassword = function (plainPass, hashedPass, callback) { 27 | var salt = hashedPass.substr(0, 10); 28 | var validHash = salt + this.md5(plainPass + salt); 29 | callback(null, hashedPass === validHash); 30 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-api", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "nodemon ./bin/www" 7 | }, 8 | "dependencies": { 9 | "body-parser": "~1.17.1", 10 | "cookie-parser": "~1.4.3", 11 | "cookie-session": "^2.0.0-beta.1", 12 | "debug": "~2.6.3", 13 | "express": "~4.15.2", 14 | "express-session": "^1.15.2", 15 | "morgan": "~1.8.1", 16 | "mysql": "^2.13.0", 17 | "passport": "^0.3.2", 18 | "passport-local": "^1.0.0", 19 | "redis": "^2.7.1", 20 | "sequelize": "^3.30.4", 21 | "serve-favicon": "~2.4.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /routes/auth.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var models = require('../models'); 4 | 5 | module.exports = function (passport) { 6 | 7 | /** 8 | * Use this route to check if the user is logged in 9 | * */ 10 | router.get('/', function (req, res, next) { 11 | var user = req.user; 12 | 13 | if (user) { 14 | return res.send({status: 200, message: "You're logged in", user: user}); 15 | } else { 16 | return res.send({status: 401, message: "You're not logged in"}); 17 | } 18 | }); 19 | 20 | /** 21 | * request that handles the login of the user 22 | * 23 | * in the post body include the following 24 | * username: {user's username} 25 | * password: {user's password} 26 | * */ 27 | router.post('/login', function (req, res, next) { 28 | passport.authenticate('login', function (err, user, info) { 29 | if (err) 30 | return next(err); 31 | if (!user) 32 | return res.status(202).send({status: 202, message: info.message ? info.message : info}); 33 | 34 | req.logIn(user, function (err) { 35 | if (err) { 36 | return next(err); 37 | } 38 | return res.status(200).send({status: 200, message: info, user: user}); 39 | }); 40 | })(req, res, next); 41 | }); 42 | 43 | /** 44 | * request that handles the signup of the user 45 | * 46 | * in the post body include the following 47 | * 48 | * firstName: {user's first name} 49 | * lastName: {user's last name} 50 | * email: {user's email} 51 | * username: {user's username} 52 | * password: {user's password} 53 | * */ 54 | router.post('/signup', function (req, res, next) { 55 | console.log("signup"); 56 | passport.authenticate('signup', function (err, user, info) { 57 | if (err) { 58 | return next(err); 59 | } 60 | if (!user) { 61 | return res.status(202).send({status: 202, message: info.message ? info.message : info}); 62 | } 63 | 64 | req.logIn(user, function (err) { 65 | if (err) { 66 | return next(err); 67 | } 68 | user.password = undefined; 69 | return res.status(200).send({status: 200, message: info, user: user}); 70 | 71 | }); 72 | })(req, res, next); 73 | }); 74 | 75 | //user log out 76 | router.get('/logout', function (req, res) { 77 | if (req.isAuthenticated()) { 78 | req.logout(); 79 | return res.status(200).send({status: 200, message: "User logged out successfully"}); 80 | } else { 81 | return res.status(401).send({status: 401, message: "Not logged in"}); 82 | } 83 | }); 84 | return router; 85 | }; 86 | -------------------------------------------------------------------------------- /routes/cars.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var DM = require('../modules/data-manager'); 4 | 5 | /** 6 | * GET a list of cars that the current user has created 7 | * */ 8 | router.get('/', function (req, res, next) { 9 | var user = req.user; 10 | DM.getAllCars(user.id, function (cars) { 11 | res.status(200).send({status: 200, cars: cars}); 12 | }); 13 | }); 14 | 15 | /** 16 | * POST create a new car 17 | * 18 | * The request body should look like this... 19 | * 20 | * { 21 | * "make": "Ferrari", 22 | * "model": "F12", 23 | * "color": "Red", 24 | * "licensePlate": "FastAF" 25 | * } 26 | * 27 | * */ 28 | router.post('/', function (req, res) { 29 | var user = req.user; 30 | 31 | var post = req.body; 32 | var newData = { 33 | make: post.make, 34 | model: post.model, 35 | color: post.color, 36 | licensePlate: post.licensePlate, 37 | ownerId: user.id 38 | }; 39 | 40 | DM.createCar(user.id, newData, function (car, err) { 41 | if (car) { 42 | res.status(200).send({status: 200, message: "created!", car: car}); 43 | } else { 44 | res.status(400).send({status: 400, message: "Error creating car", err: err}); 45 | } 46 | }) 47 | }); 48 | 49 | /** 50 | * GET a car by id 51 | * anyone can access this 52 | * */ 53 | router.get('/:carId', function (req, res, next) { 54 | var carId = req.params.carId; 55 | 56 | if (!carId) { 57 | return res.status(400).send({status: 400, message: "No car id specified"}); 58 | } 59 | 60 | DM.getCar(carId, function (car) { 61 | res.status(200).send({status: 200, car: car}); 62 | }); 63 | }); 64 | 65 | /** 66 | * PUT update the information about the car 67 | * 68 | * in the request body include something like the following 69 | * 70 | * { 71 | * "make": "Ferrari", 72 | * "model": "FF", 73 | * "color": "White", 74 | * "licensePlate": "NiceAF" 75 | * } 76 | * 77 | * */ 78 | router.put('/:carId', function (req, res) { 79 | var user = req.user; 80 | var carId = req.params.carId; 81 | 82 | if (!carId) { 83 | return res.status(400).send({status: 400, message: "No car id specified"}); 84 | } 85 | 86 | //the post made by the user 87 | var post = req.body; 88 | var newData = {id: carId}; 89 | if (post.make) newData.make = post.make; 90 | if (post.model) newData.model = post.model; 91 | if (post.color) newData.color = post.color; 92 | if (post.licensePlate) newData.licensePlate = post.licensePlate; 93 | 94 | DM.updateCarInfo(user.id, newData, function (car, err) { 95 | console.log(err); 96 | if (car) { 97 | res.status(200).send({status: 200, message: "Updated!", car: car}); 98 | } else { 99 | res.status(400).send({status: 400, message: err}); 100 | } 101 | }) 102 | }); 103 | 104 | /** 105 | * DELETE delete a car 106 | * */ 107 | router.delete('/:carId', function (req, res) { 108 | var user = req.user; 109 | var carId = req.params.carId; 110 | 111 | if (!carId) { 112 | return res.status(400).send({status: 400, message: "No car id specified"}); 113 | } 114 | 115 | DM.deleteCar(user.id, carId, function (deletedCar) { 116 | if (deletedCar) { 117 | res.status(200).send({status: 200, message: "Deleted car"}); 118 | } else { 119 | res.status(400).send({status: 400, message: "Can't delete car"}); 120 | } 121 | }) 122 | }); 123 | 124 | 125 | module.exports = router; --------------------------------------------------------------------------------