From dd32f5b369eb344d3385bcd32db20b53c0b44b5c Mon Sep 17 00:00:00 2001 From: remlabm Date: Sat, 2 Aug 2014 12:15:11 -0700 Subject: [PATCH 1/3] Initial Commit --- .gitignore | 31 ++++++++++++ GulpFile.js | 32 +++++++++++++ config/default.yaml | 4 ++ config/runtime.json | 1 + index.js | 51 ++++++++++++++++++++ package.json | 41 ++++++++++++++++ src/controllers/auth.js | 59 +++++++++++++++++++++++ src/controllers/user-notes.js | 64 +++++++++++++++++++++++++ src/lib/restify-oauth2.js | 88 +++++++++++++++++++++++++++++++++++ src/models/access-token.js | 25 ++++++++++ src/models/user-note.js | 17 +++++++ src/models/user.js | 43 +++++++++++++++++ src/server.js | 68 +++++++++++++++++++++++++++ test/1.setup.coffee | 63 +++++++++++++++++++++++++ test/2.auth.spec.coffee | 43 +++++++++++++++++ test/3.user-notes.spec.coffee | 53 +++++++++++++++++++++ test/lib/user.coffee | 53 +++++++++++++++++++++ test/lib/users.coffee | 53 +++++++++++++++++++++ test/schemas/index.coffee | 35 ++++++++++++++ 19 files changed, 824 insertions(+) create mode 100644 .gitignore create mode 100644 GulpFile.js create mode 100644 config/default.yaml create mode 100644 config/runtime.json create mode 100644 index.js create mode 100644 package.json create mode 100644 src/controllers/auth.js create mode 100644 src/controllers/user-notes.js create mode 100644 src/lib/restify-oauth2.js create mode 100644 src/models/access-token.js create mode 100644 src/models/user-note.js create mode 100644 src/models/user.js create mode 100644 src/server.js create mode 100644 test/1.setup.coffee create mode 100644 test/2.auth.spec.coffee create mode 100644 test/3.user-notes.spec.coffee create mode 100644 test/lib/user.coffee create mode 100644 test/lib/users.coffee create mode 100644 test/schemas/index.coffee diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..36c3310 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# Created by .gitignore support plugin (hsz.mobi) + +### Node template +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# Commenting this out is preferred by some people, see +# https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git +node_modules + +# Users Environment Variables +.lock-wscript diff --git a/GulpFile.js b/GulpFile.js new file mode 100644 index 0000000..2cb2eca --- /dev/null +++ b/GulpFile.js @@ -0,0 +1,32 @@ +var gulp = require('gulp') + , mocha = require('gulp-spawn-mocha') + , join = require('path').join + , nodemon = require('gulp-nodemon') + ; + +var paths = { + scripts: ['src/**/*.js', 'test/**/*.coffee'] +}; + +gulp.task('mocha.test', function () { + return gulp.src(['test/*.coffee'], {read: false}).pipe(mocha({ + bin: 'node_modules/mocha/bin/mocha', + R: 'spec', + compilers: 'coffee:coffee-script', + c: true + })).on('error', console.warn.bind(console)); +}); + +gulp.task('watch', function () { + gulp.watch( paths.scripts , ['mocha.test']); +}); + +gulp.task('serve', function(){ + nodemon({ script: 'index.js', ext: 'js', ignore: ['ignored.js'] }) + .on('restart', function () { + console.log('restarted!') + }) +}); + +gulp.task('default', ['watch', 'mocha.test', 'serve']); + diff --git a/config/default.yaml b/config/default.yaml new file mode 100644 index 0000000..be89930 --- /dev/null +++ b/config/default.yaml @@ -0,0 +1,4 @@ + +Security: + saltWorkFactor: 10 + tokenTTL: 604800 # 7 days diff --git a/config/runtime.json b/config/runtime.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/config/runtime.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..71aced4 --- /dev/null +++ b/index.js @@ -0,0 +1,51 @@ +var CONFIG = require('config') + , mongoose = require( 'mongoose' ) + , restify = require('restify'); + +// Database +// ---------------------------------------------------------------------------- +if( !mongoose.connection.readyState ){ + mongoose.connect( 'mongodb://localhost/meelocal-api-dev', function( err ){ + if( err ){ + console.log('Unable to connect to Mongo!', err ); + process.exit(1); + } + }); +} + +// Logs +// ---------------------------------------------------------------------------- +var Logger = require( 'bunyan' ); +global.log = new Logger( { + name : 'cpb-api', + streams: [ + { + level : 'info', +// path : 'logs/api-debug.log' // log ERROR and above to a file + stream: process.stdout // log INFO and above to stdout + }, + { + level: 'error', + path : 'logs/api-error.log' // log ERROR and above to a file + }, + { + level: 'debug', + path : 'logs/debug.log' // log ERROR and above to a file + } + ] +}); + +// Server +// ---------------------------------------------------------------------------- +global.server = require('./src/server'); + +server.on('after', restify.auditLogger({ + log: Logger.createLogger({ + name: 'audit', + stream: process.stdout + }) +})); + +server.listen( 8000, function() { + console.log( '%s listening at %s', server.name, server.url ); +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..89dca01 --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "cpb-api", + "description": "", + "version": "0.0.1", + "main": "index.js", + "dependencies": { + "async": "^0.8.0", + "bcrypt": "^0.7.8", + "bunyan": "0.22.1", + "config": "^0.4.35", + "js-yaml": "^3.0.2", + "lodash": "~2.4.1", + "mongoose": "~3.8", + "mongoose-timestamp": "^0.2.1", + "moment": "2.5.1", + "restify": "~2.8" + }, + "devDependencies": { + "chai": "1.9.0", + "chai-json-schema": "^1.1.0", + "coffee-script": "~1.6.3", + "gulp": "^3.6.2", + "gulp-coffee": "^1.4.3", + "gulp-mocha": "^0.4.1", + "gulp-nodemon": "^1.0.3", + "gulp-spawn": "^0.3.0", + "gulp-spawn-mocha": "^0.1.4", + "gulp-util": "^2.2.14", + "microtime": "^0.5.1", + "mocha": "~1.17.1", + "sinon": "^1.9.1", + "sinon-chai": "^2.5.0" + }, + "scripts": { + "start": "node ./index.js", + "test": "mocha -R spec --compilers coffee:coffee-script test/*.coffee" + }, + "repository": { + "type": "git" + } +} diff --git a/src/controllers/auth.js b/src/controllers/auth.js new file mode 100644 index 0000000..4577b08 --- /dev/null +++ b/src/controllers/auth.js @@ -0,0 +1,59 @@ +var mongoose = require('mongoose') + , restify = require('restify') + , _ = require('lodash') + , C = require('config').OAuth + , User = mongoose.model('User') + , AccessToken = mongoose.model('AccessToken') + ; + +// ### Register +exports.register = function (req, res, next) { + + var user = new User( req.params ); + user.validate( function( err ){ + if( err ){ next( new restify.PreconditionFailedError );} + + user.save( function(err, user) { + next.ifError(err); + + AccessToken.create({ _userId: user._id }, function (err, token) { + next.ifError(err); + res.send(token); + }); + + }); + }); +}; + +// ### Login +exports.login = function (req, res, next) { + + User.findOne( { email: req.params.login }, function( err, user ){ + next.ifError( err ); + if(_.isEmpty( user )){ return next( new restify.InvalidCredentialsError()); } + + user.verifyPassword( req.params.password, function(err, isMatch ){ + next.ifError( err ); + + if( !isMatch ){ + return next( new restify.InvalidCredentialsError()); + } + + AccessToken.create({ _userId: user._id }, function( err, token ){ + next.ifError( err ); + res.send( token ); + }); + + }) + }); +}; + +// ## Logout User and Delete Token +exports.logout = function( req, res, next ){ + AccessToken.findByIdAndRemove( req.userToken._id , function( err ) { + next.ifError( err ); + res.send( 204 ); + return next(); + }); +}; + diff --git a/src/controllers/user-notes.js b/src/controllers/user-notes.js new file mode 100644 index 0000000..06b7425 --- /dev/null +++ b/src/controllers/user-notes.js @@ -0,0 +1,64 @@ +var mongoose = require( 'mongoose' ) + , _ = require('lodash') + , CONFIG = require('config') + , async = require('async') + , UserNote = mongoose.model( 'UserNote' ) + ; + +// ### Get Note by ID +exports.findOne = function( req, res, next ) { + UserNote.findById( req.params.id ) + .exec( function( err, note ) { + next.ifError( err ); + res.send( note ); + return next(); + }); +}; + +// ### Add Note +exports.insert = function( req, res, next ) { + var note = new UserNote( req.body ); + note.set('userId', req.userId); + note.save( function( err, note ) { + next.ifError( err ); + res.send( 201, note ); + return next(); + }); +}; + +// ### Update Note +exports.update = function( req, res, next ) { + UserNote.findByIdAndUpdate( req.params.id, { $set: req.body }, function( err, note ){ + next.ifError( err ); + res.send( 202, note ); + return next(); + }); +}; + +// ### Delete Place +exports.remove = function( req, res, next ) { + UserNote.findByIdAndRemove( req.params.id , function( err ) { + next.ifError( err ); + res.send( 204 ); + return next(); + } ); +}; + +// ### Query Notes +exports.findAll = function( req, res, next ) { + UserNote + .aggregate() + .match( { userId: res.userId } ) + .sort({ createdAt: -1 }) + .project("_id") + .limit( 100 ) + .exec( function( err, noteIds ){ + next.ifError( err ); + UserNote.find( { _id : { $in : _.pluck( noteIds, '_id') } }) + .sort({ createdAt: -1 }) + .exec( function( err, notes ){ + res.send( notes ); + return next(); + }); + }); +}; \ No newline at end of file diff --git a/src/lib/restify-oauth2.js b/src/lib/restify-oauth2.js new file mode 100644 index 0000000..f3b7a10 --- /dev/null +++ b/src/lib/restify-oauth2.js @@ -0,0 +1,88 @@ + +var restify = require('restify') + , mongoose = require('mongoose') + , _ = require('lodash') + ; + +/** + * Returns a plugin that will parse the client's Authorization header. + * + * Subsequent handlers will see `req.authorization`, which looks like: + * + * { + * scheme: , + * token: , + * userId: $userId + * } + * + * `req.userId` will also be set, and defaults to 'null'. + * + * @return {Function} restify handler. + * @throws {TypeError} on bad input + */ +function authorizationParser( options ) { + options = options || {}; + + var AccessToken = mongoose.model('AccessToken') + , User = mongoose.model('User'); + + function parseAuthorization(req, res, next) { + req.authorization = {}; + req.userId = null; + + if (!req.headers.authorization) + return (next()); + + var pieces = req.headers.authorization.split(' ', 2); + if (!pieces || pieces.length !== 2) { + var e = new restify.InvalidHeaderError('BearerAuth content is invalid.'); + return (next(e)); + } + + req.authorization.scheme = pieces[0]; + req.authorization.token = pieces[1]; + + AccessToken.verify( pieces[1], function( err, token ){ + if( err ){ + var e = new restify.UnauthorizedError('Invalid Token!'); + return (next(e)); + } + + req.userId = token._userId; + req.userToken = token; + + User.findOne({ _id: req.userId }, function( err, user ){ + if( err ){ + var e = new restify.UnauthorizedError('Invalid Token!'); + return (next(e)); + } + + // attach profile to the request + req.user = user; + req.userId = user._id.toString(); + return (next()); + }); + + }); + } + + return (parseAuthorization); +} + +function mustBeAuthorized( options ){ + options = options || {}; + + function isAuthorized( req, res, next ){ + if(_.isNull( req.userId ) ){ + var e = new restify.UnauthorizedError('Must be authenticated!'); + return (next(e)); + } + return (next()); + } + + return ( isAuthorized ); +} + + +module.exports.authorizationParser = authorizationParser; +module.exports.mustBeAuthorized = mustBeAuthorized; diff --git a/src/models/access-token.js b/src/models/access-token.js new file mode 100644 index 0000000..9ef3b6a --- /dev/null +++ b/src/models/access-token.js @@ -0,0 +1,25 @@ +var mongoose = require( 'mongoose') + , CONFIG = require('config').Security + , crypto = require('crypto') + , moment = require('moment') + ; + +function generateToken(){ + return crypto.randomBytes(16).toString('hex') +} + +var AccessTokenSchema = new mongoose.Schema({ + token : { type: String, default: generateToken, unique: true } + , _userId : { type: mongoose.Schema.ObjectId } + , expires : { type: Date, default: moment().add('s', CONFIG.tokenTTL).utc().format(), index: { expires: CONFIG.tokenTTL }} +}); + +AccessTokenSchema.static('verify', function( token, done ){ + this.findOne( { token: token }, function( err, user ){ + if( err ){ return done( err ); } + if( !user ){ return done( null, false ); } + return done( null, user ) + }); +}); + +module.exports = mongoose.model('AccessToken', AccessTokenSchema) diff --git a/src/models/user-note.js b/src/models/user-note.js new file mode 100644 index 0000000..448f2db --- /dev/null +++ b/src/models/user-note.js @@ -0,0 +1,17 @@ +var mongoose = require( 'mongoose') + , timestamps = require('mongoose-timestamp') + ; + +var UserNoteSchema = new mongoose.Schema( { + userId: { type: mongoose.Schema.Types.ObjectId, required: true }, + title : { type: String, required: true }, + description : { type: String, required: true } +}); + +// Indexes +UserNoteSchema.index({ "title" : 1 }); + +// Plugins +UserNoteSchema.plugin(timestamps); + +module.exports = mongoose.model( 'UserNote', UserNoteSchema ); \ No newline at end of file diff --git a/src/models/user.js b/src/models/user.js new file mode 100644 index 0000000..f0edeea --- /dev/null +++ b/src/models/user.js @@ -0,0 +1,43 @@ +var mongoose = require('mongoose') + , timestamps = require('mongoose-timestamp') + , bcrypt = require('bcrypt') + , C = require('config') + ; + +var UserSchema = new mongoose.Schema({ + email : { type: String, required: true, unique: true } + , password : { type: String, required: true } +}); + +// Check password update +UserSchema.pre('save', function(next){ + var user = this; + + // not changing password + if(!user.isModified('password')) return next(); + bcrypt.genSalt( C.Security.saltWorkFactor , function(err, salt ){ + if( err ) return next( err ); + + bcrypt.hash( user.password, salt, function( err, hash ){ + if( err ) return next( err ); + + user.password = hash; + next(); + }) + }) + +}); + +// Verify password +UserSchema.method('verifyPassword', function( candidatePassword, cb ){ + bcrypt.compare( candidatePassword, this.password, function( err, isMatch ){ + if(err) return cb( err ); + + cb(null, isMatch ); + }) +}); + +// Plugins +UserSchema.plugin(timestamps); + +module.exports = mongoose.model('User', UserSchema) diff --git a/src/server.js b/src/server.js new file mode 100644 index 0000000..5029bb5 --- /dev/null +++ b/src/server.js @@ -0,0 +1,68 @@ +var CONFIG = require('config') + , os = require('os') + , restify = require('restify') + , restifyOAuth2 = require('./lib/restify-oauth2') + ; + +require('./models/user'); +require('./models/access-token'); +require('./models/user-note'); + +// HTTP Server +// ---------------------------------------------------------------------------- +var server = restify.createServer( { + name: 'cpb-api', + version: '0.1.0' + /* https cert/key */ +} ); + +server.use( [ + restify.queryParser() + , restify.bodyParser( { + maxBodySize: 0, + uploadDir: os.tmpdir() + }) + , restifyOAuth2.authorizationParser() + , restify.CORS({ + origins: ['*'], + headers: ['Authorization'], + credentials: true + }) + ] +); + +var auth = require('./controllers/auth') + , userNotes = require('./controllers/user-notes') + ; + +// Routes +// ---------------------------------------------------------------------------- + +// Echo +server.get('/echo', function( req, res, next ){ + res.send( req.params ); + next(); +}); + +// User Auth +server.post('/api/register', auth.register); +server.post('/api/login', auth.login); +server.get('/api/logout', auth.logout ); + +// ** Everything below here will require Auth Token ** +server.use( restifyOAuth2.mustBeAuthorized() ); + +// Echo +server.get('/echo-auth', function( req, res, next ){ + res.send( req.params ); + next(); +}); + +// Notes CRUD +server.get( '/api/user/notes', userNotes.findAll ); +server.post( '/api/user/notes', userNotes.insert ); +server.get( '/api/user/notes/:id', userNotes.findOne ); +server.put( '/api/user/notes/:id', userNotes.update ); +server.del( '/api/user/notes/:id', userNotes.remove ); + +module.exports = server; diff --git a/test/1.setup.coffee b/test/1.setup.coffee new file mode 100644 index 0000000..dec906d --- /dev/null +++ b/test/1.setup.coffee @@ -0,0 +1,63 @@ +global.restify = require 'restify' +global.mongoose = require 'mongoose' +mongoose.connect 'mongodb://localhost/cpb-api-test' +Logger = require 'bunyan' + +global.chai = require 'chai' +global.assert = chai.assert + +require './schemas' ## Load schemas + +global.log = new Logger { + name: 'meelocal-api-test' + streams:[ + { + level: 'info' + path: 'logs/info-tests.log' + } + { + level: 'debug' + path: 'logs/debug-tests.log' + } + ] +} + +global.server = require './../src/server' +global.client = restify.createJsonClient { + url: 'http://localhost:8081' +} +global.httpClient = restify.createHttpClient { + url: 'http://localhost:8081' +} + +server.on 'after', restify.auditLogger { + log: Logger.createLogger { + name: 'audit' + streams: [ + path: 'logs/audit-tests.log' + ] + } +} + +global.qs = require 'querystring' +global._ = require 'lodash' +global.async = require 'async' + +describe 'Setting Up', -> + + it 'should clear the TEST database', (done)-> + mongoose.connection.on 'open', ()-> + mongoose.connection.db.dropDatabase done + + it 'should start the SERVER', (done)-> + server.listen 8081, '127.0.0.1', done() + + it 'should have respond to ECHO', (done)-> + q = _.random 0, 10 + client.get "/echo?q=#{q}", (err, req, res, obj)-> + assert.equal obj.q, q + done() + + after (done) -> + + done() \ No newline at end of file diff --git a/test/2.auth.spec.coffee b/test/2.auth.spec.coffee new file mode 100644 index 0000000..e7af9eb --- /dev/null +++ b/test/2.auth.spec.coffee @@ -0,0 +1,43 @@ +User = require './lib/user' + +describe 'Authentication', -> + + describe 'Register, Login and Fetch my Profile', -> + + before (done)-> + @user = new User( + email: 'test@test.com' + password: 'test123' + ) + done() + + it 'should register a new user and return a token', (done) -> + @user.register ( err, obj )-> + assert.isNull err + assert.jsonSchema(obj, chai.tv4.getSchema 'auth' ) + done() + + it 'should login as user and return new a token', (done) -> + @user.login ( err, obj )-> + assert.isNull err + assert.jsonSchema(obj, chai.tv4.getSchema 'auth' ) + done() + + it 'should forbid access without a token', (done) -> + client.get '/echo-auth', (err, req, res, obj) -> + assert.equal err.statusCode, 401 + assert.equal err.name, 'UnauthorizedError' + done() + + it 'should allow access with a Bearer token', (done) -> + @user.client.get "/echo-auth", (err, req, res, profile) -> + assert.ifError err + assert.equal res.statusCode, 200 + done() + + describe 'Failed Registration Scenarios', -> + it 'empty data set, should return 412', (done) -> + client.post '/api/register', {}, ( err, req, res )-> + assert.equal err.statusCode, 412 + done() + diff --git a/test/3.user-notes.spec.coffee b/test/3.user-notes.spec.coffee new file mode 100644 index 0000000..b54b601 --- /dev/null +++ b/test/3.user-notes.spec.coffee @@ -0,0 +1,53 @@ +User = require './lib/user' + +describe 'User Note Tests', -> + + describe 'CRUD Tests', -> + noteId = null + noteObject = null + + sampleNote = + title : 'Sample Note' + description: 'Description - Creating' + + before (done) -> + @user = new User( + email: 'test1@test.com' + password: 'test123' + ) + @user.register done + + it 'should create a new note and return the note', ( done ) -> + @user.client.post "/api/user/notes", sampleNote, ( err, req, res, note ) -> + assert.isNull err + assert.equal res.statusCode, 201 + assert.jsonSchema(note, chai.tv4.getSchema 'userNote' ) + noteId = note._id + delete note._id + delete note.__v + noteObject = note + done() + + it 'should return our new note', ( done ) -> + @user.client.get "/api/user/notes/#{noteId}", ( err, req, res, note ) -> + assert.isNull err + assert.jsonSchema note, chai.tv4.getSchema 'userNote' + done() + + it 'should update our note', ( done ) -> + putNote = + description: 'Description - Updating' + + @user.client.put "/api/user/notes/#{noteId}", _.merge( noteObject, putNote ), ( err, req, res, note ) -> + assert.isNull err + assert.equal res.statusCode, 202 + assert.jsonSchema note, chai.tv4.getSchema 'userNote' + assert.equal note.description, putNote.description + done() + + it 'should delete our note', ( done ) -> + @user.client.del "/api/user/notes/#{noteId}", ( err, req, res ) => + assert.isNull err + assert.equal res.statusCode, 204 + done() + diff --git a/test/lib/user.coffee b/test/lib/user.coffee new file mode 100644 index 0000000..730977a --- /dev/null +++ b/test/lib/user.coffee @@ -0,0 +1,53 @@ +async = require 'async' +_ = require 'lodash' +restify = require 'restify' +qs = require 'querystring' + +module.exports = class User + + defaults: + client: + url: 'http://localhost:8081' + + constructor: ( @userData, options )-> + @setOptions options + @client = restify.createJsonClient @options.client + + setOptions: (options) -> + @options = _.merge @defaults, options + this + + login: ( done )-> + @client.post '/api/login', { login: @userData.email, password: @userData.password }, ( err, req, res, auth)=> + if( err ) + done( err ) + @auth = auth + @setToken auth.token + done( null, auth ) + + register: ( done )-> + @client.post '/api/register', @userData, ( err, req, res, auth )=> + if( err ) + done( err ) + @auth = auth + @setToken auth.token + done( null, auth ) + + setHeaders: ( headers )-> + @client.headers = _.merge( @client.headers, headers ) + + setToken: ( token )-> + @setHeaders { Authorization: "Bearer #{token}" } + + @setup: ( userData, options, done )-> + user = new @( userData, options) + + user.register (err)-> + if err + done err + + user.getProfile ()-> + if err + done err + + done null, user diff --git a/test/lib/users.coffee b/test/lib/users.coffee new file mode 100644 index 0000000..8d3b5d1 --- /dev/null +++ b/test/lib/users.coffee @@ -0,0 +1,53 @@ +User = require './user' + +module.exports = class Users + + defaults: + setupTasks: + register: true + + users: [] + + constructor: ( @users, @options )-> +# @setUsers users +# @setOptions options + + setUsers: (users) -> + @users = _.merge @users, users + this + + setOptions: (options) -> + @options = _.merge @defaults, options + this + + create: ( user, cb )-> + User.setup user, null, ( err, user)=> + if err + cb err + + @users.push user + cb null, user + + getAll: ()-> + return @users + + get: ( name )-> + return _(@users).chain().where( { name: name }).first().value() + + getNames: ()-> + return _.pluck( @users, 'name') + + @setup: ( usersData, done )-> + if !_.isArray usersData + usersData = [ usersData ] + + async.map usersData, ( userData, cb )-> + + User.setup userData, null, cb + + , ( err, usersObj )=> + if err + done err + + users = new @(usersObj) + done null, users diff --git a/test/schemas/index.coffee b/test/schemas/index.coffee new file mode 100644 index 0000000..a092d28 --- /dev/null +++ b/test/schemas/index.coffee @@ -0,0 +1,35 @@ +chai = require 'chai' +chai.use( require 'chai-json-schema' ) + +authSchema = + id: 'auth' + type: 'object' + required: [ 'token', 'expires'] + properties: + token: + type: 'string' + expires: + type: 'string' + +userNoteSchema = + id: 'userNote' + type: 'object' + required: [ '_id', 'title', 'description', 'createdAt'] + properties: + _id: + type: 'string' + title: + type: 'string' + description: + type: 'string' + +userNotesSchema = + id: 'userNotes' + type: 'array' + items: + $ref: 'userNote' + +# Register Schemas +chai.tv4.addSchema userNotesSchema +chai.tv4.addSchema userNoteSchema +chai.tv4.addSchema authSchema \ No newline at end of file From 646ddc85e3d8c7bb63be8c1ed547a5620eb7be17 Mon Sep 17 00:00:00 2001 From: remlabm Date: Sat, 2 Aug 2014 12:18:11 -0700 Subject: [PATCH 2/3] updated mongo connection --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 71aced4..d467238 100644 --- a/index.js +++ b/index.js @@ -5,7 +5,7 @@ var CONFIG = require('config') // Database // ---------------------------------------------------------------------------- if( !mongoose.connection.readyState ){ - mongoose.connect( 'mongodb://localhost/meelocal-api-dev', function( err ){ + mongoose.connect( 'mongodb://localhost/cpb-api-dev', function( err ){ if( err ){ console.log('Unable to connect to Mongo!', err ); process.exit(1); From 1307a3340089d10cbea9d655c014d98429d751d7 Mon Sep 17 00:00:00 2001 From: remlabm Date: Sat, 2 Aug 2014 12:38:37 -0700 Subject: [PATCH 3/3] Update README.md --- docs/README.md | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/docs/README.md b/docs/README.md index cbbeb28..70d2cf9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,6 +4,27 @@ Place any related documentation in this folder. -## Notes +## Completed Tasks +- User can register / login / logout +- User can CRUD notes +- Functional tests + +## Enhancments +- Added Bearer Token support ( oAuth2 ) +- Added CORS support ( required by API's ) +- Added Logs and Audit reports + +## Incomplete Tasks +- API Documentation, would normally use auto-gen documents plugin. Routes defined in `src/server.js` + +## Requirements +NodeJS >= 10.* +NPM Packages - `npm install -g gulp mocha chai` +MongoDb + +## Run Tests +`$ gulp test mocha.test` + +## Start Local Server +`$ gulp serve` -Use this area to communicate any thought processes, ideas, or challenges you encountered. If you had more time, what would you enhance or do differently? \ No newline at end of file