From b15a0b427b31ee82d5e6f5012304b156c1b80645 Mon Sep 17 00:00:00 2001 From: Nikko Pisciotti Date: Mon, 6 Mar 2017 16:18:18 -0800 Subject: [PATCH 01/15] almost all tests done --- .eslintrc | 22 ++++++++ .gitignore | 120 ++++++++++++++++++++++++++++++++++++++++ gulpfile.js | 21 +++++++ lib/auth-middleware.js | 34 ++++++++++++ lib/error-middleware.js | 29 ++++++++++ model/user.js | 75 +++++++++++++++++++++++++ package.json | 43 ++++++++++++++ route/auth-route.js | 35 ++++++++++++ server.js | 28 ++++++++++ test/auth-route-test.js | 44 +++++++++++++++ 10 files changed, 451 insertions(+) create mode 100644 .eslintrc create mode 100644 .gitignore create mode 100644 gulpfile.js create mode 100644 lib/auth-middleware.js create mode 100644 lib/error-middleware.js create mode 100644 model/user.js create mode 100644 package.json create mode 100644 route/auth-route.js create mode 100644 server.js create mode 100644 test/auth-route-test.js diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..7f50cb6 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,22 @@ +{ + "rules": { + "no-console": "off", + "indent": [ "error", 2 ], + "quotes": [ "error", "single" ], + "semi": ["error", "always"], + "linebreak-style": [ "error", "unix" ], + "no-unused-vars": [2, {"vars": "local", "args": "after-used"}] + }, + "env": { + "es6": true, + "node": true, + "mocha": true, + "jasmine": true + }, + "ecmaFeatures": { + "modules": true, + "experimentalObjectRestSpread": true, + "impliedStrict": true + }, + "extends": "eslint:recommended" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..916de44 --- /dev/null +++ b/.gitignore @@ -0,0 +1,120 @@ + +# Created by https://www.gitignore.io/api/node,osx,windows + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +#Test-Data +/data + +#Node +/node_modules + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + + +### OSX ### +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.gitignore.io/api/node,osx,windows diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..1704e61 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,21 @@ +'use strict'; + +const gulp = require('gulp'); +const eslint = require('gulp-eslint'); +const mocha = require('gulp-mocha'); + +gulp.task('test', function() { + gulp.src('./test/*-test.js', { read: false}) + .pipe(mocha({ reporter: 'nyan' })); +}); + +gulp.task('lint', function() { + return gulp.src(['**/*.js', '!node_modules']) + .pipe(eslint()) + .pipe(eslint.format()) + .pipe(eslint.failAfterError()); +}); + +gulp.task('dev', ['test', 'lint']); + +gulp.task('default', ['dev']); diff --git a/lib/auth-middleware.js b/lib/auth-middleware.js new file mode 100644 index 0000000..2dff1fb --- /dev/null +++ b/lib/auth-middleware.js @@ -0,0 +1,34 @@ +'use strict'; + +const createError = require('http-errors'); +const debug = require('debug')('cfgram:basic-auth-middleware'); + +module.exports = function(req, res, next) { + debug('basic auth'); + + var authHeader = req.headers.authorization; + if (!authHeader) { + return next(createError(401, 'authorization header required')); + } + + var base64str = authHeader.split('Basic ')[1]; + if (!base64str) { + return next(createError(401, 'username and password required')); + } + + var utf8string = new Buffer(base64str, 'base64').toString(); + var authArray = utf8string.split(':'); + + req.auth = { + username: authArray[0], + password: authArray[1] + }; + + if (!req.auth.username) { + return next(createError(401, 'username required')); + } + if (!req.auth.password) { + return next(createError(401, 'password required')); + } + next(); +}; diff --git a/lib/error-middleware.js b/lib/error-middleware.js new file mode 100644 index 0000000..2810390 --- /dev/null +++ b/lib/error-middleware.js @@ -0,0 +1,29 @@ +'use strict'; + +const createError = require('http-errors'); +const debug = require('debug')('cfgram:error-middleware'); + + +module.exports = function(err, req, res, next) { + debug('error middleware'); + + console.error('msg:', err.message); + console.error('name:', err.name); + + if(err.status) { + res.status(err.status).send(err.name); + next(); + return; + } + + if(err.name === 'ValidationError') { + err = createError(400, err.message); + res.status(err.status).send(err.name); + next(); + return; + } + + err = createError(500, err.message); + res.status(err.status).send(err.name); + next(); +}; diff --git a/model/user.js b/model/user.js new file mode 100644 index 0000000..25d4ffe --- /dev/null +++ b/model/user.js @@ -0,0 +1,75 @@ +'use strict'; + +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const jwt = require('jsonwebtoken'); +const mongoose = require('mongoose'); +const debug = require('debug')('cfgram:user'); +const createError = require('http-errors'); + +const Schema = mongoose.Schema; + + +const userSchema = Schema ({ + username: { type: String, required: true, unique: true}, + email: { type: String, required: true, unique: true}, + password: { type: String, required: true}, + findHash: { type: String, unique: true} +}); + +userSchema.methods.generatePasswordHash = function(password) { + debug('generatePasswordHash'); + + return new Promise((resolve, reject) => { + bcrypt.hash(password, 10, (err, hash) => { + if(err) return reject(err); + this.password = hash; + resolve(this); + }); + }); +}; + +userSchema.methods.comparePasswordHash = function(password) { + debug('comparePasswordHash'); + + return new Promise((resolve, reject) => { + bcrypt.compare(password, this.password, (err, valid) => { + if(err) return reject(err); + if(!valid) return reject(createError(401, 'wrong password')); + resolve(this); + }); + }); +}; + +userSchema.methods.generateFindHash = function() { + debug('generateFindHash'); + + return new Promise((resolve, reject) => { + let tries = 0; + + _generateFindHash.call(this); + + function _generateFindHash() { + this.findHash = crypto.randomBytes(32).toString('hex'); + this.save() + .then( () => resolve(this.findHash)) + .catch( err => { + if(tries > 3) return reject(err); + tries++; + _generateFindHash.call(this); + }); + } + }); +}; + +userSchema.methods.generateToken = function() { + debug('generateToken'); + + return new Promise((resolve, reject) => { + this.generateFindHash() + .then( findHash => resolve(jwt.sign({ token: findHash}, process.env.APP_SECRET))) + .catch( err => reject(err)); + }); +}; + +module.exports = mongoose.model('user', userSchema); diff --git a/package.json b/package.json new file mode 100644 index 0000000..91d7f38 --- /dev/null +++ b/package.json @@ -0,0 +1,43 @@ +{ + "name": "16-basic_auth", + "version": "1.0.0", + "description": "![cf](https://i.imgur.com/7v5ASc8.png) Lab 16 - Basic Auth ======", + "main": "gulpfile.js", + "directories": { + "test": "test" + }, + "scripts": { + "test": "DEBUG='cfgram*' mocha", + "start": "DEBUG='cfgram*' node server.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/npisciotti1/16-basic_auth.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/npisciotti1/16-basic_auth/issues" + }, + "homepage": "https://github.com/npisciotti1/16-basic_auth#readme", + "dependencies": { + "bcrypt": "^1.0.2", + "bluebird": "^3.5.0", + "body-parser": "^1.17.1", + "cors": "^2.8.1", + "debug": "^2.6.1", + "dotenv": "^4.0.0", + "express": "^4.15.2", + "http-errors": "^1.6.1", + "jsonwebtoken": "^7.3.0", + "mongoose": "^4.8.6", + "morgan": "^1.8.1" + }, + "devDependencies": { + "chai": "^3.5.0", + "expect": "^1.20.2", + "mocha": "^3.2.0", + "superagent": "^3.5.0" + } +} diff --git a/route/auth-route.js b/route/auth-route.js new file mode 100644 index 0000000..b9a8fad --- /dev/null +++ b/route/auth-route.js @@ -0,0 +1,35 @@ +'use strict'; + +const Router = require('express').Router; +const debug = require('debug')('cfgram:auth-route'); +const jsonParser = require('body-parser'); + +const basicAuth = require('../lib/auth-middleware.js'); +const User = require('../model/user.js'); + +const authRouter = module.exports = Router(); + +authRouter.post('/api/signup', jsonParser, function(req, res, next) { + debug('POST: /api/signup'); + console.log('we got here'); + + let password = req.body.password; + delete req.body.password; + + let user = new User(req.body); + user.generatePasswordHash(password) + .then( user => user.save()) + .then( user => user.generateToken()) + .then( token => res.send(token)) + .catch(next); +}); + +authRouter.get('/api/signin', basicAuth, function(req, res, next) { + debug('GET: /api/signin'); + + User.findOne({ username: req.auth.username }) + .then( user => user.comparePasswordHash(req.auth.password)) + .then( user => user.generateToken()) + .then( token => res.send(token)) + .catch( err => next(err)); +}); diff --git a/server.js b/server.js new file mode 100644 index 0000000..1d9cfa3 --- /dev/null +++ b/server.js @@ -0,0 +1,28 @@ +'use strict'; + +const express = require('express'); +const debug = require('debug')('cfgram:server'); +const Promise = require('bluebird'); +const cors = require('cors'); +const morgan = require('morgan'); +const mongoose = require('mongoose'); +const dotenv = require('dotenv'); + +const errors = require('./lib/error-middleware.js'); +const basicAuth = require('./lib/auth-middleware.js'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +dotenv.load(); +mongoose.connect(process.env.MONGODB_URI); + +app.use(cors()); +app.use(morgan('dev')); + +app.use(basicAuth); +app.use(errors); + +app.listen(PORT, () => { + debug(`Port is up yo: ${PORT}`); +}); diff --git a/test/auth-route-test.js b/test/auth-route-test.js new file mode 100644 index 0000000..9b27193 --- /dev/null +++ b/test/auth-route-test.js @@ -0,0 +1,44 @@ +'use strict'; + +const expect = require('chai').expect; +const request = require('superagent'); +const Promise = require('bluebird'); +const mongoose = require('mongoose'); +const User = require('../model/user.js'); + +mongoose.Promise = Promise; + +require('../server.js'); + +const url = `http://localhost:${process.env.PORT}`; + +const exampleUser = { + username: 'nikko', + password: 'soopersekret', + email: 'nikko@soopersekret.com' +}; + + +describe('Auth Routes', function() { + describe('POST /api/signup', function() { + describe('with a valid body', function() { + after( done => { + User.remove({}) + .then( () => done()) + .catch(err => done(err)); + }); + + it('should return a token', done => { + request.post(`${url}/api/signup`) + .send(exampleUser) + .end((err, res) => { + if(err) return done(err); + console.log('response token:', res.body); + expect(res.status).to.equal(200); + expect(res.body).to.be.a('string'); + done(); + }); + }); + }); + }); +}); From 771b9aa2ce01ace6a96a13ef2c520ced3b41b587 Mon Sep 17 00:00:00 2001 From: Nikko Pisciotti Date: Mon, 6 Mar 2017 22:31:11 -0800 Subject: [PATCH 02/15] yeehaw the tests pass --- route/auth-route.js | 7 +++--- server.js | 4 +-- test/auth-route-test.js | 54 +++++++++++++++++++++++++++++++++++++++-- 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/route/auth-route.js b/route/auth-route.js index b9a8fad..de568bc 100644 --- a/route/auth-route.js +++ b/route/auth-route.js @@ -2,7 +2,8 @@ const Router = require('express').Router; const debug = require('debug')('cfgram:auth-route'); -const jsonParser = require('body-parser'); +const jsonParser = require('body-parser').json(); +const createError = require('http-errors'); const basicAuth = require('../lib/auth-middleware.js'); const User = require('../model/user.js'); @@ -21,7 +22,7 @@ authRouter.post('/api/signup', jsonParser, function(req, res, next) { .then( user => user.save()) .then( user => user.generateToken()) .then( token => res.send(token)) - .catch(next); + .catch( () => next(createError(400, 'bad request'))); }); authRouter.get('/api/signin', basicAuth, function(req, res, next) { @@ -31,5 +32,5 @@ authRouter.get('/api/signin', basicAuth, function(req, res, next) { .then( user => user.comparePasswordHash(req.auth.password)) .then( user => user.generateToken()) .then( token => res.send(token)) - .catch( err => next(err)); + .catch( () => next(createError(400, 'bad request'))); }); diff --git a/server.js b/server.js index 1d9cfa3..effd04b 100644 --- a/server.js +++ b/server.js @@ -9,7 +9,7 @@ const mongoose = require('mongoose'); const dotenv = require('dotenv'); const errors = require('./lib/error-middleware.js'); -const basicAuth = require('./lib/auth-middleware.js'); +const authRouter = require('./route/auth-route.js'); const app = express(); const PORT = process.env.PORT || 3000; @@ -20,7 +20,7 @@ mongoose.connect(process.env.MONGODB_URI); app.use(cors()); app.use(morgan('dev')); -app.use(basicAuth); +app.use(authRouter); app.use(errors); app.listen(PORT, () => { diff --git a/test/auth-route-test.js b/test/auth-route-test.js index 9b27193..a4a9499 100644 --- a/test/auth-route-test.js +++ b/test/auth-route-test.js @@ -33,12 +33,62 @@ describe('Auth Routes', function() { .send(exampleUser) .end((err, res) => { if(err) return done(err); - console.log('response token:', res.body); + console.log('response token:', res.text); expect(res.status).to.equal(200); - expect(res.body).to.be.a('string'); + expect(res.text).to.be.a('string'); done(); }); }); }); + describe('with an invalid body', function() { + it('should return a 400 error', done => { + request.post(`${url}/api/signup`) + .send('invalid body') + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + }); + }); + + describe('GET: /api/signin', function() { + describe('With a valid body', () => { + before( done => { + let user = new User(exampleUser); + user.generatePasswordHash(exampleUser.password) + .then( user => user.save()) + .then( user => { + this.tempUser = user; + done(); + }) + .catch(done); + }); + after( done => { + User.remove({}) + .then( () => done()) + .catch(done); + }); + it('should return a token', done => { + request.get(`${url}/api/signin`) + .auth('nikko', 'soopersekret') + .end((err, res) => { + expect(res.status).to.equal(200); + expect(res.text).to.be.a('string'); + done(); + }); + }); + + describe('with an invalid body', function() { + it('should return a 401 error', done => { + request.get(`${url}/api/signin`) + .auth('just-nikko') + .end((err, res) => { + expect(res.status).to.equal(401); + done(); + }); + }); + }); + }); }); }); From f9805ee57b03ea88daf392d01bbefed67fb14699 Mon Sep 17 00:00:00 2001 From: Nikko Pisciotti Date: Tue, 7 Mar 2017 13:25:42 -0800 Subject: [PATCH 03/15] added bearer-auth-middleware.js --- lib/bearer-auth-middleware.js | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 lib/bearer-auth-middleware.js diff --git a/lib/bearer-auth-middleware.js b/lib/bearer-auth-middleware.js new file mode 100644 index 0000000..2bcf10e --- /dev/null +++ b/lib/bearer-auth-middleware.js @@ -0,0 +1,29 @@ +'use strict'; + +const jwt = require('jsonwebtoken'); +const createError = require('http-errors'); +const debug = require('debug')('cfgram:bearer-auth-middleware'); + +const User = require('../model/user.js'); + +module.exports = function(req, res, next) { + debug('bearer auth middleware'); + + var authHeader = req.headers.authorization; + if(!authHeader) return next(createError(401, 'authorization headers required')); + + var token = authHeader.split('Bearer ')[1]; + if(!token) return next(createError(401, 'token required')); + + jwt.verify(token, process.env.APP_SECRET, (err, decoded) => { + if(err) return next(err); + console.log('im the decoded object', decoded); + + User.findOne({ findHash: decoded.token}) + .then( user => { + req.user = user; + }) + .catch(err => next(err)); + }); + next(); +}; From 35db7f3222f19870d81fc2e9b6774c4237b0db27 Mon Sep 17 00:00:00 2001 From: Nikko Pisciotti Date: Tue, 7 Mar 2017 14:01:55 -0800 Subject: [PATCH 04/15] added gallery-route.js --- route/gallery-route.js | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 route/gallery-route.js diff --git a/route/gallery-route.js b/route/gallery-route.js new file mode 100644 index 0000000..3cb3a30 --- /dev/null +++ b/route/gallery-route.js @@ -0,0 +1,36 @@ +'use strict'; + +const createError = require('http-errors'); +const jsonParser = require('body-parser').json(); +const Router = require('express').Router; +const debug = require('debug')('cfgram:gallery-route'); + +const Gallery = require('../model/gallery.js'); +const bearerAuth = require('../lib/bearer-auth-middleware.js'); + +const galleryRouter = module.exports = Router(); + +galleryRouter.post('/api/gallery', bearerAuth, jsonParser, function(req, res, next) { + debug('POST /api/gallery'); + + req.body.userID = req.user._id; + new Gallery(req.body).save() + .then( gallery => res.json(gallery)) + .catch(next); +}); + +galleryRouter.get('/api/gallery:id', bearerAuth, function(req, res, next) { + debug('GET /api/gallery:id'); + + Gallery.findOne({ findHash: req.params.id }) + .then( gallery => res.json(gallery)) + .catch(next); +}); + +galleryRouter.delete('/api/gallery/:id', bearerAuth, function(req, res, next) { + debug('DELETE /api/gallery/:id'); + + Gallery.findByIdAndRemove(req.params.id) + .then( () => res.status(204).send('no content')) + .catch(next); +}); From 7435646202ae640801b6ecdc595c84d37f02372f Mon Sep 17 00:00:00 2001 From: Nikko Pisciotti Date: Tue, 7 Mar 2017 14:21:33 -0800 Subject: [PATCH 05/15] added gallery-route-test.js, unfinished --- test/gallery-route-test.js | 61 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 test/gallery-route-test.js diff --git a/test/gallery-route-test.js b/test/gallery-route-test.js new file mode 100644 index 0000000..a4536cc --- /dev/null +++ b/test/gallery-route-test.js @@ -0,0 +1,61 @@ +'use strict'; + +const expect = require('chai').expect; +const request = require('superagent'); +const mongoose = require('mongoose'); +const Promise = require('bluebird'); + +const User = require('../model/user.js'); +const Gallery = require('../model/gallery.js'); + +const url = `http://localhost:${process.env.PORT}`; + +const exampleUser = { + username: 'example user', + email: 'example@user.com', + password: 'user password' +}; + +const exampleGallery = { + name: 'example gallery', + description: 'my example gallery' +}; + +describe('Gallery Routes', function () { + describe('POST: /api/gallery', function() { + describe('with a valid body', () => { + afterEach( done => { + Promise.all([ + User.remove({}), + Gallery.remove({}) + ]) + .then( () => done()) + .catch(done); + }); + before( done => { + new User(exampleUser) + .generatePasswordHash(exampleUser.password) + .then( user => user.save()) + .then( user => { + this.tempUser = user; + user.generateToken(); + }) + .then( token => { + this.tempToken = token; + done(); + }) + .catch(done); + }); + }); + it('should return a gallery', done => { + request.post('/api/gallery') + .send(exampleGallery) + .set({ + Authorization: `Bearer ${this.tempToken}` + }) + .end((err, res) => { + expect(res.) + }) + }) + }); +}); From fd201d8300e165708e9fe3ed27452e793f933709 Mon Sep 17 00:00:00 2001 From: Nikko Pisciotti Date: Tue, 7 Mar 2017 16:05:32 -0800 Subject: [PATCH 06/15] post tests all working --- test/auth-route-test.js | 1 - test/gallery-route-test.js | 76 +++++++++++++++++++++++++++----------- 2 files changed, 54 insertions(+), 23 deletions(-) diff --git a/test/auth-route-test.js b/test/auth-route-test.js index a4a9499..2c2f443 100644 --- a/test/auth-route-test.js +++ b/test/auth-route-test.js @@ -33,7 +33,6 @@ describe('Auth Routes', function() { .send(exampleUser) .end((err, res) => { if(err) return done(err); - console.log('response token:', res.text); expect(res.status).to.equal(200); expect(res.text).to.be.a('string'); done(); diff --git a/test/gallery-route-test.js b/test/gallery-route-test.js index a4536cc..4e85d6f 100644 --- a/test/gallery-route-test.js +++ b/test/gallery-route-test.js @@ -10,6 +10,8 @@ const Gallery = require('../model/gallery.js'); const url = `http://localhost:${process.env.PORT}`; +mongoose.Promise = Promise; + const exampleUser = { username: 'example user', email: 'example@user.com', @@ -22,23 +24,23 @@ const exampleGallery = { }; describe('Gallery Routes', function () { - describe('POST: /api/gallery', function() { + afterEach( done => { + Promise.all([ + User.remove({}), + Gallery.remove({}) + ]) + .then( () => done()) + .catch(done); + }); + describe('POST: /api/gallery', () => { describe('with a valid body', () => { - afterEach( done => { - Promise.all([ - User.remove({}), - Gallery.remove({}) - ]) - .then( () => done()) - .catch(done); - }); before( done => { - new User(exampleUser) - .generatePasswordHash(exampleUser.password) + let user = new User(exampleUser); + user.generatePasswordHash(exampleUser.password) .then( user => user.save()) .then( user => { this.tempUser = user; - user.generateToken(); + return user.generateToken(); }) .then( token => { this.tempToken = token; @@ -46,16 +48,46 @@ describe('Gallery Routes', function () { }) .catch(done); }); + it('should return a gallery', done => { + request.post(`${url}/api/gallery`) + .send(exampleGallery) + .set({ + Authorization: `Bearer ${this.tempToken}` + }) + .end((err, res) => { + expect(res.body.name).to.equal(exampleGallery.name); + expect(res.body.description).to.equal(exampleGallery.description); + expect(res.body.userID).to.equal(this.tempUser._id.toString()); + expect(res.status).to.equal(200); + done(); + }); + }); + }); + describe('with an invalid token', () => { + it('should return a 401 error', done => { + request.post(`${url}/api/gallery`) + .send(exampleGallery) + .set({ + Authorization: '' + }) + .end((err, res) => { + expect(res.status).to.equal(401); + done(); + }); + }); + }); + describe('with an invalid body', () => { + it('should return a 400 error', done => { + request.post(`${url}/api/gallery`) + .send() + .set({ + Authorization: `Bearer ${this.tempToken}` + }) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); }); - it('should return a gallery', done => { - request.post('/api/gallery') - .send(exampleGallery) - .set({ - Authorization: `Bearer ${this.tempToken}` - }) - .end((err, res) => { - expect(res.) - }) - }) }); }); From 69543644e58ee47aa1de14fcf0f342a50d469ac2 Mon Sep 17 00:00:00 2001 From: Nikko Pisciotti Date: Tue, 7 Mar 2017 17:27:36 -0800 Subject: [PATCH 07/15] first GET test in gallery-route-test is passing --- test/auth-route-test.js | 2 +- test/gallery-route-test.js | 48 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/test/auth-route-test.js b/test/auth-route-test.js index 2c2f443..74c8652 100644 --- a/test/auth-route-test.js +++ b/test/auth-route-test.js @@ -5,7 +5,7 @@ const request = require('superagent'); const Promise = require('bluebird'); const mongoose = require('mongoose'); const User = require('../model/user.js'); - + mongoose.Promise = Promise; require('../server.js'); diff --git a/test/gallery-route-test.js b/test/gallery-route-test.js index 4e85d6f..90919ac 100644 --- a/test/gallery-route-test.js +++ b/test/gallery-route-test.js @@ -8,6 +8,8 @@ const Promise = require('bluebird'); const User = require('../model/user.js'); const Gallery = require('../model/gallery.js'); +require('../server.js'); + const url = `http://localhost:${process.env.PORT}`; mongoose.Promise = Promise; @@ -90,4 +92,50 @@ describe('Gallery Routes', function () { }); }); }); + describe('GET /api/gallery/:id', function() { + describe('with a valid body', () => { + before( done => { + new User(exampleUser) + .generatePasswordHash(exampleUser.password) + .then( user => user.save()) + .then( user => { + this.tempUser = user; + return user.generateToken(); + }) + .then( token => { + this.tempToken = token; + done(); + }) + .catch(done); + }); + before( done => { + exampleGallery.userID = this.tempUser._id.toString(); + new Gallery(exampleGallery).save() + .then( gallery => { + this.tempGallery = gallery; + done(); + }) + .catch(done); + }); + + after( done => { + delete exampleGallery.userID; + done(); + }); + + it('should return a gallery', done => { + request.get(`${url}/api/gallery/${this.tempGallery._id}`) + .set({ + Authorization: `Bearer ${this.tempToken}` + }) + .end((err, res) => { + if(err) return done(err); + // console.log('this.tempGallery:', this.tempGallery); + // console.log('this.tempToken:', this.tempToken); + expect(res.status).to.equal(200); + done(); + }); + }); + }); + }); }); From 4a2034d66c8f8b25522a751625a1e1bc8807e6b0 Mon Sep 17 00:00:00 2001 From: Nikko Pisciotti Date: Tue, 7 Mar 2017 17:29:50 -0800 Subject: [PATCH 08/15] working on second GET test --- lib/bearer-auth-middleware.js | 5 ++--- model/gallery.js | 13 +++++++++++++ route/auth-route.js | 1 - route/gallery-route.js | 13 ++++++++----- server.js | 2 ++ test/gallery-route-test.js | 6 ++++-- 6 files changed, 29 insertions(+), 11 deletions(-) create mode 100644 model/gallery.js diff --git a/lib/bearer-auth-middleware.js b/lib/bearer-auth-middleware.js index 2bcf10e..20d79e5 100644 --- a/lib/bearer-auth-middleware.js +++ b/lib/bearer-auth-middleware.js @@ -17,13 +17,12 @@ module.exports = function(req, res, next) { jwt.verify(token, process.env.APP_SECRET, (err, decoded) => { if(err) return next(err); - console.log('im the decoded object', decoded); - User.findOne({ findHash: decoded.token}) + User.findOne({ findHash: decoded.token }) .then( user => { req.user = user; + next(); }) .catch(err => next(err)); }); - next(); }; diff --git a/model/gallery.js b/model/gallery.js new file mode 100644 index 0000000..f3bde39 --- /dev/null +++ b/model/gallery.js @@ -0,0 +1,13 @@ +'use strict'; + +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const gallerySchema = Schema({ + name: { type: String, required: true}, + description: { type: String, required: true}, + created: { type: Date, required: true, default: Date.now}, + userID: { type: Schema.Types.ObjectId, required: true } +}); + +module.exports = mongoose.model('gallery', gallerySchema); diff --git a/route/auth-route.js b/route/auth-route.js index de568bc..d8f7523 100644 --- a/route/auth-route.js +++ b/route/auth-route.js @@ -12,7 +12,6 @@ const authRouter = module.exports = Router(); authRouter.post('/api/signup', jsonParser, function(req, res, next) { debug('POST: /api/signup'); - console.log('we got here'); let password = req.body.password; delete req.body.password; diff --git a/route/gallery-route.js b/route/gallery-route.js index 3cb3a30..7251d8a 100644 --- a/route/gallery-route.js +++ b/route/gallery-route.js @@ -13,18 +13,21 @@ const galleryRouter = module.exports = Router(); galleryRouter.post('/api/gallery', bearerAuth, jsonParser, function(req, res, next) { debug('POST /api/gallery'); + if(!req.user) return next(createError(400, 'bad request')); + req.body.userID = req.user._id; new Gallery(req.body).save() .then( gallery => res.json(gallery)) - .catch(next); + .catch( () => next(createError(400, 'bad request'))); }); -galleryRouter.get('/api/gallery:id', bearerAuth, function(req, res, next) { - debug('GET /api/gallery:id'); +galleryRouter.get('/api/gallery/:id', bearerAuth, function(req, res, next) { + debug('GET /api/gallery/:id'); + console.log('we got here, heres req.params.id', req.params.id); Gallery.findOne({ findHash: req.params.id }) .then( gallery => res.json(gallery)) - .catch(next); + .catch( () => next(createError(400, 'bad request'))); }); galleryRouter.delete('/api/gallery/:id', bearerAuth, function(req, res, next) { @@ -32,5 +35,5 @@ galleryRouter.delete('/api/gallery/:id', bearerAuth, function(req, res, next) { Gallery.findByIdAndRemove(req.params.id) .then( () => res.status(204).send('no content')) - .catch(next); + .catch( () => next(createError(400, 'bad request'))); }); diff --git a/server.js b/server.js index effd04b..c56b014 100644 --- a/server.js +++ b/server.js @@ -10,6 +10,7 @@ const dotenv = require('dotenv'); const errors = require('./lib/error-middleware.js'); const authRouter = require('./route/auth-route.js'); +const galleryRouter = require('./route/gallery-route.js'); const app = express(); const PORT = process.env.PORT || 3000; @@ -21,6 +22,7 @@ app.use(cors()); app.use(morgan('dev')); app.use(authRouter); +app.use(galleryRouter); app.use(errors); app.listen(PORT, () => { diff --git a/test/gallery-route-test.js b/test/gallery-route-test.js index 90919ac..4b42a72 100644 --- a/test/gallery-route-test.js +++ b/test/gallery-route-test.js @@ -130,9 +130,11 @@ describe('Gallery Routes', function () { }) .end((err, res) => { if(err) return done(err); - // console.log('this.tempGallery:', this.tempGallery); - // console.log('this.tempToken:', this.tempToken); + let date = new Date(res.body.created).toString(); expect(res.status).to.equal(200); + expect(res.body.name).to.equal(exampleGallery.name); + expect(res.body.description).to.equal(exampleGallery.description); + expect(date).to.not.equal('invalid date'); done(); }); }); From 2b7f57b2b631d97b9377734222431240681260f3 Mon Sep 17 00:00:00 2001 From: Nikko Pisciotti Date: Tue, 7 Mar 2017 17:43:51 -0800 Subject: [PATCH 09/15] GET route gallery-tests working --- route/gallery-route.js | 7 +++---- test/gallery-route-test.js | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/route/gallery-route.js b/route/gallery-route.js index 7251d8a..1d50c4a 100644 --- a/route/gallery-route.js +++ b/route/gallery-route.js @@ -23,11 +23,10 @@ galleryRouter.post('/api/gallery', bearerAuth, jsonParser, function(req, res, ne galleryRouter.get('/api/gallery/:id', bearerAuth, function(req, res, next) { debug('GET /api/gallery/:id'); - console.log('we got here, heres req.params.id', req.params.id); - Gallery.findOne({ findHash: req.params.id }) + Gallery.findById(req.params.id) .then( gallery => res.json(gallery)) - .catch( () => next(createError(400, 'bad request'))); + .catch( () => next(createError(404, 'not found'))); }); galleryRouter.delete('/api/gallery/:id', bearerAuth, function(req, res, next) { @@ -35,5 +34,5 @@ galleryRouter.delete('/api/gallery/:id', bearerAuth, function(req, res, next) { Gallery.findByIdAndRemove(req.params.id) .then( () => res.status(204).send('no content')) - .catch( () => next(createError(400, 'bad request'))); + .catch( () => next(createError(404, 'not found'))); }); diff --git a/test/gallery-route-test.js b/test/gallery-route-test.js index 4b42a72..ffbd0d9 100644 --- a/test/gallery-route-test.js +++ b/test/gallery-route-test.js @@ -139,5 +139,30 @@ describe('Gallery Routes', function () { }); }); }); + describe('with an invalid token', () => { + it('should return a 401', done => { + request.get(`${url}/api/gallery/${this.tempGallery._id}`) + .set({ + Authorization: '' + }) + .end((err, res) => { + expect(res.status).to.equal(401); + done(); + }); + }); + }); + describe('with an invalid id', () => { + let invalidId = '123-invalid-id'; + it('should return a 404', done => { + request.get(`${url}/api/gallery/${invalidId}`) + .set({ + Authorization: `Bearer ${this.tempToken}` + }) + .end((err, res) => { + expect(res.status).to.equal(404); + done(); + }); + }); + }); }); }); From 57bed52301f10a92cedae01b841facf300a2bf31 Mon Sep 17 00:00:00 2001 From: Nikko Pisciotti Date: Tue, 7 Mar 2017 22:39:05 -0800 Subject: [PATCH 10/15] almost got all my tests passing --- lib/bearer-auth-middleware.js | 2 ++ route/gallery-route.js | 9 ++++++ test/gallery-route-test.js | 60 +++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+) diff --git a/lib/bearer-auth-middleware.js b/lib/bearer-auth-middleware.js index 20d79e5..013d703 100644 --- a/lib/bearer-auth-middleware.js +++ b/lib/bearer-auth-middleware.js @@ -9,6 +9,7 @@ const User = require('../model/user.js'); module.exports = function(req, res, next) { debug('bearer auth middleware'); + var authHeader = req.headers.authorization; if(!authHeader) return next(createError(401, 'authorization headers required')); @@ -20,6 +21,7 @@ module.exports = function(req, res, next) { User.findOne({ findHash: decoded.token }) .then( user => { + console.log('we got here, req.user', req.user); req.user = user; next(); }) diff --git a/route/gallery-route.js b/route/gallery-route.js index 1d50c4a..b0cd694 100644 --- a/route/gallery-route.js +++ b/route/gallery-route.js @@ -29,6 +29,15 @@ galleryRouter.get('/api/gallery/:id', bearerAuth, function(req, res, next) { .catch( () => next(createError(404, 'not found'))); }); +galleryRouter.put('/api/gallery/:id', bearerAuth, jsonParser, function(req, res, next) { + debug('PUT /api/gallery/:id'); + + + Gallery.findByIdAndUpdate(req.params.id, req.body, {new: true}) + .then( gallery => res.json(gallery)) + .catch( () => next(createError(404, 'not found'))); +}); + galleryRouter.delete('/api/gallery/:id', bearerAuth, function(req, res, next) { debug('DELETE /api/gallery/:id'); diff --git a/test/gallery-route-test.js b/test/gallery-route-test.js index ffbd0d9..b7fdb0a 100644 --- a/test/gallery-route-test.js +++ b/test/gallery-route-test.js @@ -165,4 +165,64 @@ describe('Gallery Routes', function () { }); }); }); + + describe('PUT /api/gallery/:id', function() { + describe('with a valid token and body', () => { + before( done => { + new User(exampleUser) + .generatePasswordHash(exampleUser.password) + .then( user => user.save()) + .then( user => { + this.tempUser = user; + return user.generateToken(); + }) + .then( token => { + this.tempToken = token; + done(); + }) + .catch(done); + }); + before( done => { + exampleGallery.userID = this.tempUser._id; + new Gallery(exampleGallery).save() + .then( gallery => { + this.tempGallery = gallery; + done(); + }) + .catch(done); + }); + after( () => { + delete exampleGallery.userID; + }); + it('should return an updated gallery', done => { + let updatedGallery = { name: 'updated gallery', description: 'dis the new shit'}; + request.put(`${url}/api/gallery/${this.tempGallery._id}`) + .send(updatedGallery) + .set({ + Authorization: `Bearer ${this.tempToken}` + }) + .end((err, res) => { + if(err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.name).to.equal('updated gallery'); + expect(res.body.description).to.equal('dis the new shit'); + done(); + }); + }); + }); + describe('with an invalid token', () => { + let updatedGallery = { name: 'updated gallery', description: 'dis the new shit'}; + it('should return a 401 error', done => { + request.put(`${url}/api/gallery/${this.tempGallery._id}`) + .send(updatedGallery) + .set({ + Authorization: '' + }) + .send((err, res) => { + expect(res.status).to.equal(401); + done(); + }); + }); + }); + }); }); From dbe6002d3523cb6ba0c2baa069a362b37bbb99cb Mon Sep 17 00:00:00 2001 From: Nikko Pisciotti Date: Wed, 8 Mar 2017 13:34:37 -0800 Subject: [PATCH 11/15] added new dependencies and npm modules --- model/pic.js | 0 package.json | 5 ++++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 model/pic.js diff --git a/model/pic.js b/model/pic.js new file mode 100644 index 0000000..e69de29 diff --git a/package.json b/package.json index 91d7f38..c53e4c3 100644 --- a/package.json +++ b/package.json @@ -22,17 +22,20 @@ }, "homepage": "https://github.com/npisciotti1/16-basic_auth#readme", "dependencies": { + "aws-sdk": "^2.24.0", "bcrypt": "^1.0.2", "bluebird": "^3.5.0", "body-parser": "^1.17.1", "cors": "^2.8.1", "debug": "^2.6.1", + "del": "^2.2.2", "dotenv": "^4.0.0", "express": "^4.15.2", "http-errors": "^1.6.1", "jsonwebtoken": "^7.3.0", "mongoose": "^4.8.6", - "morgan": "^1.8.1" + "morgan": "^1.8.1", + "multer": "^1.3.0" }, "devDependencies": { "chai": "^3.5.0", From 550eb6bfc3fae872839b2d550f51a19e750550fe Mon Sep 17 00:00:00 2001 From: Nikko Pisciotti Date: Wed, 8 Mar 2017 13:42:39 -0800 Subject: [PATCH 12/15] added pic schema --- model/pic.js | 16 ++++++++++++++++ route/pic-route.js | 0 2 files changed, 16 insertions(+) create mode 100644 route/pic-route.js diff --git a/model/pic.js b/model/pic.js index e69de29..7cd6d4d 100644 --- a/model/pic.js +++ b/model/pic.js @@ -0,0 +1,16 @@ +'use strict'; + +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const picSchema = Schema({ + name: { type: String, required: true }, + description: { type: String, required: true }, + created: { type: Date, default: Date.now }, + userID: { type: Schema.Types.ObjectId, required: true }, + galleryID: { type: Schema.Types.ObjectId, required: true }, + imageURI: { type: String, required: true, unique: true }, + objectKey: { type: String, required: true, unique: true} +}); + +module.exports = mongoose.model('pic', picSchema); diff --git a/route/pic-route.js b/route/pic-route.js new file mode 100644 index 0000000..e69de29 From a70e272997d67b630d60338dff35f952373a0eff Mon Sep 17 00:00:00 2001 From: Nikko Pisciotti Date: Wed, 8 Mar 2017 14:05:22 -0800 Subject: [PATCH 13/15] committing to update --- route/pic-route.js | 50 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/route/pic-route.js b/route/pic-route.js index e69de29..190ee20 100644 --- a/route/pic-route.js +++ b/route/pic-route.js @@ -0,0 +1,50 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs'); +const multer = require('multer'); +const del = require('del'); +const AWS = require('aws-sdk'); +const Router = require('express').Router; +// const jsonParser = require('body-parser').json(); +const createError = require('http-errors'); +const debug = require('debug')('cfgram:pic-route'); + +const bearerAuth = require('../lib/bearer-auth-middleware.js'); +const Gallery = require('../model/gallery.js'); +const Pic = require('../model/pic.js'); + +AWS.config.setPromisesDependency(require('bluebird')); + +const s3 = new AWS.S3(); +const dataDir = `${__dirname}/../data`; +const upload = multer({ dest: dataDir }); + +const picRouter = module.exports = Router(); + +function s3UploadProm(params) { + debug('s3UploadProm'); + + return new Promise((reject, resolve) => { + s3.upload(params, (err, data) => { + resolve(data); + }); + }); +}; + +picRouter.post('/api/gallery/:galleryID/pic', bearerAuth, upload.single('image'), function(req, res, next) { + debug('POST /api/gallery/:galleryID/pic'); + + if(!req.file) return next(createError(400, 'bad request')); + + if(!req.file.path) return next(createError(500, 'file not saved')); + + let ext = path.extname(req.file.originalname); + + let params = { + ACL: 'public-read', + Bucket: process.env.AWS_BUCKET, + Key: `${req.file.filename}${ext}`, + Body: fs.createReadStream(req.file.path) + } +}) From d9dd75cc1f557031d61a771a92be03b1499a2aac Mon Sep 17 00:00:00 2001 From: Nikko Pisciotti Date: Wed, 8 Mar 2017 14:47:20 -0800 Subject: [PATCH 14/15] added server-toggle.js --- route/pic-route.js | 23 ++++++++++++++++++++--- server.js | 6 +++++- test/lib/server-toggle.js | 30 ++++++++++++++++++++++++++++++ test/pic-route-test.js | 1 + 4 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 test/lib/server-toggle.js create mode 100644 test/pic-route-test.js diff --git a/route/pic-route.js b/route/pic-route.js index 190ee20..20bd2f8 100644 --- a/route/pic-route.js +++ b/route/pic-route.js @@ -30,7 +30,7 @@ function s3UploadProm(params) { resolve(data); }); }); -}; +} picRouter.post('/api/gallery/:galleryID/pic', bearerAuth, upload.single('image'), function(req, res, next) { debug('POST /api/gallery/:galleryID/pic'); @@ -46,5 +46,22 @@ picRouter.post('/api/gallery/:galleryID/pic', bearerAuth, upload.single('image') Bucket: process.env.AWS_BUCKET, Key: `${req.file.filename}${ext}`, Body: fs.createReadStream(req.file.path) - } -}) + }; + + Gallery.findById(req.params.galleryID) + .then( () => s3UploadProm(params)) + .then( data => { + del([`${dataDir}/*`]); + let picData = { + name: req.body.name, + description: req.body.description, + userID: req.user._id, + galleryID: req.params.galleryID, + objectKey: data.Key, + imageURI: data.Location + }; + return new Pic(picData).save(); + }) + .then( pic => res.json(pic)) + .catch( err => next(err)); +}); diff --git a/server.js b/server.js index c56b014..b8c1023 100644 --- a/server.js +++ b/server.js @@ -11,6 +11,7 @@ const dotenv = require('dotenv'); const errors = require('./lib/error-middleware.js'); const authRouter = require('./route/auth-route.js'); const galleryRouter = require('./route/gallery-route.js'); +const picRouter = require('./route/pic-route.js'); const app = express(); const PORT = process.env.PORT || 3000; @@ -23,8 +24,11 @@ app.use(morgan('dev')); app.use(authRouter); app.use(galleryRouter); +app.use(picRouter); app.use(errors); -app.listen(PORT, () => { +const server = module.exports = app.listen(PORT, () => { debug(`Port is up yo: ${PORT}`); }); + +server.isRunning = true; diff --git a/test/lib/server-toggle.js b/test/lib/server-toggle.js new file mode 100644 index 0000000..02477e1 --- /dev/null +++ b/test/lib/server-toggle.js @@ -0,0 +1,30 @@ +'use strict'; + +const debug = require('debug')('cfgram:server-toggle'); + +module.exports = exports = {}; + +exports.serverOn = function(server, done) { + if(!server.isRunning) { + server.listen(process.env.PORT, () => { + debug('server turnt'); + done(); + }); + server.isRunning = true; + return; + } + done(); +}; + +exports.serverOff = function(server, done) { + if(server.isRuning) { + server.close( err => { + if(err) return done(err); + debug('server unturnt'); + done(); + }); + server.isRunning = false; + done(); + } + done(); +}; diff --git a/test/pic-route-test.js b/test/pic-route-test.js new file mode 100644 index 0000000..ad9a93a --- /dev/null +++ b/test/pic-route-test.js @@ -0,0 +1 @@ +'use strict'; From 9fa8a90e163c4131735495040fca5a4b4ad8ff11 Mon Sep 17 00:00:00 2001 From: Nikko Pisciotti Date: Wed, 8 Mar 2017 21:37:16 -0800 Subject: [PATCH 15/15] added test scaffold --- test/pic-route-test.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/pic-route-test.js b/test/pic-route-test.js index ad9a93a..4043376 100644 --- a/test/pic-route-test.js +++ b/test/pic-route-test.js @@ -1 +1,35 @@ 'use strict'; + +const expect = require('chai').expect; +const request = require('superagent'); +const debug = require('debug')('cfgram:pic-router-test'); + +const Pic = require('../model/pic.js'); +const User = require('../model/user.js'); +const Gallery = require('../model/gallery.js'); + +const serverToggle = require('./lib/server-toggle.js'); +const server = require('../server.js'); + +const url = `http://localhost:${process.env.PORT}`; + +const exampleUser = { + username: 'example user', + password: 'badpass', + email: 'example@user.com' +}; + +const exampleGallery = { + name: 'test gallery', + description: 'my test gallery' +}; + +const examplePic = { + name: 'example pic', + description: 'my example pic', + image: `${__dirname}/data/tester.jpg` +}; + +describe('Pic Routes', function() { + +});