diff --git a/README.md b/README.md deleted file mode 100644 index 5e09c5c..0000000 --- a/README.md +++ /dev/null @@ -1,28 +0,0 @@ -Mongo Backed REST API -========================== -To complete this assignment: - * fork this repository (the sub module for this specific assignment) - * clone down your fork - * place all of your work in a folder that is your full name, use `_`s instead of spaces - * push back up to your fork - * create a pull request back to the original repo - * submit a link to the PR in canvas - -Assignment Description --------------------------- -Create a single resource rest API with Express that's backed by Mongo. - -I'm leaving this pretty open to interpretation. I want you to write this from scratch, don't just copy and paste code from class or previous projects. - -Add a feature of Mongoose that we didn't use class, such as data validation. - -Also, implement a non CRUD resource (meaning that it doesn't use the full GET/POST/PUT/PATCH/DELETE interface). - - - -Rubric - - * Use of Express: 3pts - * Use of Mongo: 3pts - * Tests: 2pts - * Project Organization: 2pts diff --git a/walter_nicholas/app/css/base.css b/walter_nicholas/app/css/base.css new file mode 100644 index 0000000..d72c046 --- /dev/null +++ b/walter_nicholas/app/css/base.css @@ -0,0 +1,25 @@ +body { + background: lightgrey; +} + +td { + border: 1px solid black; + width: 200px; + height: 40px; + font-size: 1.3em; + background: white; + text-align: center; +} + +.deleteTasksButton, .updateButton { + width: 200px; + height: 40px; + font-size: 1.2em; +} + +.deleteTasksButton { + color: red; +} + +#taskTable { +} \ No newline at end of file diff --git a/walter_nicholas/app/index.html b/walter_nicholas/app/index.html new file mode 100644 index 0000000..961af6a --- /dev/null +++ b/walter_nicholas/app/index.html @@ -0,0 +1,87 @@ + + + + + + Task Finder + + +

Task Finder

+
+ +
+
+
+
+ +

+ +
+
+ +
+
+ Logged in as {{username}}.
+
+ +
+
+ + +
+ + +
+
+ +
+

New Task:

+ + + + + + + + +

+ + +
+ +

+ + + + + + + + +
Description Location Priority Completed Delete Tasks
{{task.description}} {{task.location}} {{task.priority}}
+ +
+ + + + \ No newline at end of file diff --git a/walter_nicholas/app/js/entry.js b/walter_nicholas/app/js/entry.js new file mode 100644 index 0000000..cd2e689 --- /dev/null +++ b/walter_nicholas/app/js/entry.js @@ -0,0 +1,6 @@ +require('angular/angular'); +var angular = window.angular; + +var taskApp = angular.module('taskApp', []); +// require('./controllers/controllers')(taskApp); +require('./tasks/tasks')(taskApp); diff --git a/walter_nicholas/app/js/tasks/controllers/tasks_controller.js b/walter_nicholas/app/js/tasks/controllers/tasks_controller.js new file mode 100644 index 0000000..b3d9d8d --- /dev/null +++ b/walter_nicholas/app/js/tasks/controllers/tasks_controller.js @@ -0,0 +1,148 @@ +module.exports = function(app) { + app.controller('tasksController', ['$scope', '$http', function($scope, $http) { + $scope.tasks = []; + var defaults = {location: 'work', priority: 1}; + $scope.newTask = Object.create(defaults); + $scope.token = false; + $scope.username = ''; + + $scope.signUp = function() { + $scope.username = $('input[id="newUsername"]').val(); + var password = $('input[id="newPassword"]').val(); + var repeated = $('input[id="repeated"]').val(); + if(!($scope.username != '' && password != '' && repeated != '')) alert('Fill out all fields'); + else if(repeated != password) alert('password != repeated'); + else { + var newUser = {"username": $scope.username, "password": password}; + + var successCb = function(res) { + if(res.data.token) $scope.token = res.data.token; + }; + var errorCb = function(err) { + console.log(err.data.msg); + alert(err.data.msg); + }; + + var req = { + method: 'POST', + url:'/api/signup', + data: newUser + }; + + $http(req).then(successCb, errorCb); + } + }; + + // $scope.signIn = function() { + // $scope.username = $('input[id="username"]').val(); + // var password = $('input[id="password"]').val(); + // if(!($scope.username != '' && password != '')) alert('Fill out all fields.'); + // else { + + // var successCb = function(res) { + // if(res.data.token) $scope.token = res.data.token; + // }; + + // var errorCb = function(err) { + // console.log(err.data) + // }; + + // var user = $scope.username; + // var req = { + // method: 'GET', + // url: '/api/signin' + // headers: {"Authorization": "Basic " + btoa(user + ":" + password)} + // }; + + // $http(req).then(successCb, errorCb); + + // var newUser = {"username": $scope.username, "password": password}; + // $http.post('/api/signup', newUser) + // .then(function(res) { + // if(res.data.token) $scope.token = res.data.token; + // }, function(err) { + // console.log(err.data) + // }); + // } + // }; + + + $scope.showTasksByPriority = function() { + var priority = $('select[id="priority"]').val(); + + $http.get('/api/tasks/priority/' + priority) + .then(function(res) { + $scope.tasks = res.data; + }, function(err) { + console.log(err.data); + }); + }; + + $scope.showTasksByCompletion = function() { + var status = $('select[id="completionStatus"]').val(); + + $http.get('/api/tasks/completed/' + status) + .then(function(res) { + $scope.tasks = res.data; + }, function(err) { + console.log(err.data); + }); + }; + + $scope.createTask = function(task) { + $http.post('/api/tasks', task) + .then(function(res) { + $scope.tasks.push(res.data); + $scope.newTask = Object.create(defaults); + }, function(err) { + console.log(err.data) + }); + }; + + $scope.retrieveTasks = function() { + $http.get('/api/tasks') + .then(function(res) { + $scope.tasks = res.data; + }, function(err) { + console.log(err.data); + }); + }; + + $scope.updateTask = function(task) { + if(task.completed) task.completed = false; + else task.completed = true; + $http.put('/api/tasks/' + task._id, task) + .then(function(res) { + console.log('task completion status updated.'); + }, function(err) { + console.log(err.data); + }); + }; + + $scope.deleteTask = function(task) { + if(!($scope.token)) alert('Must be logged in to delete tasks.'); + else { + $scope.tasks.splice($scope.tasks.indexOf(task), 1); + + var successCb = function(res) { + console.log('task deleted.') + }; + + var errorCb = function(err) { + console.log(err.data.msg); + $scope.retrieveTasks(); + }; + + var req = { + method: 'POST', + url:'/api/tasks/delete/' + task._id, + data: {token: $scope.token} + }; + + $http(req).then(successCb, errorCb); + + } + }; + + }]); +}; \ No newline at end of file diff --git a/walter_nicholas/app/js/tasks/tasks.js b/walter_nicholas/app/js/tasks/tasks.js new file mode 100644 index 0000000..bc6c1a0 --- /dev/null +++ b/walter_nicholas/app/js/tasks/tasks.js @@ -0,0 +1,3 @@ +module.exports = function(app) { + require('./controllers/tasks_controller')(app); +}; diff --git a/walter_nicholas/app/scss/_base.scss b/walter_nicholas/app/scss/_base.scss new file mode 100644 index 0000000..74e2bbb --- /dev/null +++ b/walter_nicholas/app/scss/_base.scss @@ -0,0 +1,43 @@ +$a-variable-height: 40px; + +@mixin center { + text-align: center; + margin-left: auto; + margin-right: auto; +} + +@mixin container($bg, $radius, $height, $width) { + text-align: center; + background: $bg; + border: 1px solid black; + border-radius: $radius; + height: $height; + width: $width; + margin: 12px; +} + +@mixin border($radius) { + border: 1px solid black; + border-radius: $radius; +} + +body { + background: cornsilk; +} + +h1 { + font-size: 3em; +} + +h2 { + font-size: 2em; +} + +td { + @include border(3px); + width: 200px; + height: $a-variable-height; + font-size: 1.3em; + background: white; + text-align: center; +} diff --git a/walter_nicholas/app/scss/_layout.scss b/walter_nicholas/app/scss/_layout.scss new file mode 100644 index 0000000..054cd11 --- /dev/null +++ b/walter_nicholas/app/scss/_layout.scss @@ -0,0 +1,54 @@ +#taskTable { + @include center; +} + +.title { + @include container(lime, 3px, 70px, 270px); + @include center; +} + +.formContainer { + @include container(lime, 3px, 170px, 540px); + padding: 10px; + text-align: center; + @include center; +} + +.sortButtonsContainer { + @include container(yellow, 3px, 125px, 430px); + @include center; + + + button { + margin: 3px; + padding: 4px; + font-size: 1.1em; + } + select { + font-size: 1.1em; + padding: 4px; + } +} + +.deleteTasksButton, .updateButton { + width: 200px; + height: $a-variable-height; + font-size: 1.2em; +} + +.deleteTasksButton { + color: darkred; +} + +.login { + @include container(salmon, 10px, 200px, 250px); + position: absolute; + right: 6px; + top: 6px; + padding: 3px; + + input { + margin: 3px; + } +} + diff --git a/walter_nicholas/app/scss/_state.scss b/walter_nicholas/app/scss/_state.scss new file mode 100644 index 0000000..5e83670 --- /dev/null +++ b/walter_nicholas/app/scss/_state.scss @@ -0,0 +1,5 @@ +.loggedIn { + height: $a-variable-height; + font-size: 1.4em; + text-align: center; +} \ No newline at end of file diff --git a/walter_nicholas/app/scss/application.scss b/walter_nicholas/app/scss/application.scss new file mode 100644 index 0000000..ac111de --- /dev/null +++ b/walter_nicholas/app/scss/application.scss @@ -0,0 +1,3 @@ +@import "base"; +@import "layout"; +@import "state"; \ No newline at end of file diff --git a/walter_nicholas/gulpfile.js b/walter_nicholas/gulpfile.js new file mode 100644 index 0000000..1147cb5 --- /dev/null +++ b/walter_nicholas/gulpfile.js @@ -0,0 +1,36 @@ +var gulp = require('gulp'); +var webpack = require('webpack-stream'); +var sass = require('gulp-sass'); +var minifyCSS = require('gulp-minify-css'); +var sourcemaps = require('gulp-sourcemaps'); + +gulp.task('static:dev', function() { + gulp.src('app/**/*.html') + .pipe(gulp.dest('build/')); +}); + +gulp.task('cssFiles:dev', function() { + gulp.src('app/scss/**/*.scss') + .pipe(sourcemaps.init()) + .pipe(sass()) + .pipe(minifyCSS()) + .pipe(sourcemaps.write()) + .pipe(gulp.dest('build/css/')); +}); + +gulp.task('css:watch', function () { + gulp.watch('app/css/**/*.css', ['css:dev']); +}); + +gulp.task('webpack:dev', function() { + return gulp.src('app/js/entry.js') + .pipe(webpack({ + output: { + filename: 'bundle.js' + } + })) + .pipe(gulp.dest('build/')); +}); + +gulp.task('build:dev', ['webpack:dev', 'static:dev', 'cssFiles:dev']); +gulp.task('default', ['build:dev']); diff --git a/walter_nicholas/lib/basic_http_auth.js b/walter_nicholas/lib/basic_http_auth.js new file mode 100644 index 0000000..92987fe --- /dev/null +++ b/walter_nicholas/lib/basic_http_auth.js @@ -0,0 +1,18 @@ +module.exports = function(req, res, next) { + try { + var authString = req.headers.authorization; + var basicString = authString.split(' ')[1]; + var basicBuffer = new Buffer(basicString, 'base64'); + var authArray = basicBuffer.toString().split(':'); + req.auth = { + username: authArray[0], + password: authArray[1] + }; + // debugger; + next(); + } catch(e) { + // debugger; + console.log(e); + return res.status(401).json({msg: 'nope cat 4 u'}); + } +}; diff --git a/walter_nicholas/lib/eat_auth.js b/walter_nicholas/lib/eat_auth.js new file mode 100644 index 0000000..efe00c8 --- /dev/null +++ b/walter_nicholas/lib/eat_auth.js @@ -0,0 +1,34 @@ +var eat = require('eat'); +var User = require(__dirname + '/../models/user'); + +module.exports = exports = function(req, res, next) { + var token = req.headers.token || (req.body)? req.body.token : ''; + + if (!token) { + console.log('no token'); + return res.status(401).json({msg: 'no token'}); + } + + eat.decode(token, process.env.APP_SECRET, function(err, decoded) { + if (err) { + console.log(err); + return res.status(401).json({msg: 'eat cat say nope (1)'}); + } + + User.findOne({_id: decoded.id}, function(err, user) { + if (err) { + console.log(err); + return res.status(401).json({msg: 'eat cat say nope (2)'}); + } + + if (!user) { + console.log(err); + return res.status(401).json({msg: 'eat cat say nope (3)'}); + + } + + req.user = user; + next(); + }); + }); +}; diff --git a/walter_nicholas/lib/handle_error.js b/walter_nicholas/lib/handle_error.js new file mode 100644 index 0000000..fb84c51 --- /dev/null +++ b/walter_nicholas/lib/handle_error.js @@ -0,0 +1,4 @@ +module.exports = function(err, res) { + console.log(err); + res.status(500).json({msg: 'server error.'}); +}; \ No newline at end of file diff --git a/walter_nicholas/models/task.js b/walter_nicholas/models/task.js index fc5a3f1..3f56952 100644 --- a/walter_nicholas/models/task.js +++ b/walter_nicholas/models/task.js @@ -4,8 +4,7 @@ var taskSchema = new mongoose.Schema({ description: String, location: {type: String, default: 'work'}, deadline: Date, - priority: {type: Number, min: 0, max: 5}, - requiresCollab: {type: Boolean, default: false}, + priority: {type: Number, min: 1, max: 5, default: 1}, completed: {type: Boolean, default: false} }); diff --git a/walter_nicholas/models/user.js b/walter_nicholas/models/user.js new file mode 100644 index 0000000..b0fd324 --- /dev/null +++ b/walter_nicholas/models/user.js @@ -0,0 +1,29 @@ +var mongoose = require('mongoose'); +var bcrypt = require('bcrypt'); +var eat = require('eat'); + +var userSchema = new mongoose.Schema({ + username: String, + auth: { + basic: { + username: String, + password: String + } + } +}); + +userSchema.methods.hashPassword = function(password) { + var hash = this.auth.basic.password = bcrypt.hashSync(password, 8); + return hash; +}; + +userSchema.methods.checkPassword = function(password) { + return bcrypt.compareSync(password, this.auth.basic.password); +}; + +userSchema.methods.generateToken = function(cb) { + var id = this._id; + eat.encode({id: id}, process.env.APP_SECRET, cb); +}; + +module.exports = mongoose.model('User', userSchema); \ No newline at end of file diff --git a/walter_nicholas/package.json b/walter_nicholas/package.json index 4ca581f..894f7ac 100644 --- a/walter_nicholas/package.json +++ b/walter_nicholas/package.json @@ -14,8 +14,24 @@ "mongoose": "^4.2.5" }, "devDependencies": { + "angular": "^1.4.8", + "angular-mocks": "^1.4.8", + "bcrypt": "^0.8.5", + "body-parser": "^1.14.1", "chai": "^3.4.1", "chai-http": "^1.0.0", - "mocha": "^2.3.3" + "eat": "^0.1.1", + "express": "^4.13.3", + "gulp": "^3.9.0", + "gulp-minify-css": "^1.2.2", + "gulp-sass": "^2.1.0", + "gulp-sourcemaps": "^1.6.0", + "jasmine-core": "^2.3.4", + "karma": "^0.13.15", + "karma-jasmine": "^0.3.6", + "karma-phantomjs-launcher": "^0.2.1", + "mocha": "^2.3.3", + "phantomjs": "^1.9.19", + "webpack-stream": "^2.3.0" } } diff --git a/walter_nicholas/routes/task_routes.js b/walter_nicholas/routes/task_routes.js new file mode 100644 index 0000000..b6ee87b --- /dev/null +++ b/walter_nicholas/routes/task_routes.js @@ -0,0 +1,76 @@ +var express = require('express'); +var bodyParser = require('body-parser').json(); +var handleError = require(__dirname + '/../lib/handle_error'); +var Task = require(__dirname + '/../models/task'); +var taskRouter = module.exports = exports = express.Router(); +var eatAuth = require(__dirname + '/../lib/eat_auth'); + +taskRouter.get('/tasks', function (req, res) { + Task.find({}, function (err, data) { + if (err) return handleError(err, res); + + res.json(data); + }); +}); + +taskRouter.get('/tasks/priority/:num', function (req, res) { + Task.find({priority : { $lt: (parseInt(req.params.num) + 1)}}, function(err, data) { //finds all tasks with priority less than or equal to route specified by :num + if (err) return handleError(err, res); + + res.json(data); + }); +}); + +taskRouter.get('/tasks/completed/:status', function (req, res) { + Task.find({completed : req.params.status}, function(err, data) { + if (err) return handleError(err, res); + + res.json(data); + }); +}); + +taskRouter.get('/tasks/location/:loc', function (req, res) { + Task.find({location : req.params.loc}, function(err, data) { + if (err) return handleError(err, res); + + res.json(data); + }); +}); + +taskRouter.post('/tasks', bodyParser, function (req, res) { + var newTask = new Task(req.body); + newTask.save(function(err, data) { + if (err) return handleError(err, res); + + res.json(data); + }); +}); + +taskRouter.put('/tasks/:id', bodyParser, function (req, res) { + var taskData = req.body; + delete taskData._id; + Task.update({_id: req.params.id}, taskData, function (err) { + if (err) return handleError(err, res); + + res.json({msg: 'Task modified.'}); + }); +}); + +//had to use post instead of delete because angular's $http delete cannot send an object with the request, +//and we need to send an object with token to authenticate +taskRouter.post('/tasks/delete/:id', bodyParser, eatAuth, function (req, res) { + Task.remove({_id: req.params.id}, function (err) { + if (err) return handleError(err, res); + + res.json({msg: 'eat auth success!'}); + }); +}); + +// taskRouter.delete('/tasks/delete/:id', bodyParser, eatAuth, function (req, res) { +// Task.remove({_id: req.params.id}, function (err) { +// if (err) return handleError(err, res); + +// res.json({msg: 'eat auth success!'}); +// }); +// }); + diff --git a/walter_nicholas/routes/tasks_routes.js b/walter_nicholas/routes/tasks_routes.js deleted file mode 100644 index 33a3ecf..0000000 --- a/walter_nicholas/routes/tasks_routes.js +++ /dev/null @@ -1,70 +0,0 @@ -var express = require('express'); -var bodyParser = require('body-parser'); - -var Task = require(__dirname + '/../models/task'); -var tasksRouter = express.Router(); -module.exports = exports = tasksRouter; - -tasksRouter.get('/tasks', function(req, res) { - Task.find({}, function(err, data) { - if (err) return function(err, res) { - res.status(500).json({msg: 'Server error.'}); - }; - - res.json(data); - }); -}); - -tasksRouter.get('/tasks/priority/:num', function(req, res) { - Task.find({priority : { $lt: (parseInt(req.params.num) + 1)}}, function(err, data) { //finds all tasks with priority less than or equal to route specified by :num - if (err) return function(err, res) { - res.status(500).json({msg: 'Server error.'}); - }; - - res.json(data); - }); -}); - -tasksRouter.get('/tasks/location/:loc', function(req, res) { - Task.find({location : req.params.loc}, function(err, data) { - if (err) return function(err, res) { - res.status(500).json({msg: 'Server error.'}); - }; - - res.json(data); - }); -}); - -tasksRouter.post('/tasks', bodyParser.json(), function(req, res) { - var newTask = new Task(req.body); - newTask.save(function(err, data) { - if (err) return function(err, res) { - res.status(500).json({msg: 'Server error.'}); - }; - - res.json(data); - }); -}); - -tasksRouter.put('/tasks/:id', bodyParser.json(), function(req, res) { - var taskData = req.body; - delete taskData._id; - Task.update({_id: req.params.id}, taskData, function(err) { - if (err) return function(err, res) { - res.status(500).json({msg: 'Server error.'}); - }; - - res.json({msg: 'Task modified.'}); - }); -}); - -tasksRouter.delete('/tasks/:id', function(req, res) { - Task.remove({_id: req.params.id}, function(err) { - if (err) return function(err, res) { - res.status(500).json({msg: 'Server error.'}); - }; - - res.json({msg: 'Task deleted.'}); - }); -}); - diff --git a/walter_nicholas/routes/user_routes.js b/walter_nicholas/routes/user_routes.js new file mode 100644 index 0000000..3177866 --- /dev/null +++ b/walter_nicholas/routes/user_routes.js @@ -0,0 +1,69 @@ +var express = require('express'); +var bodyParser = require('body-parser').json(); +var handleError = require(__dirname + '/../lib/handle_error'); +var basicHttp = require(__dirname + '/../lib/basic_http_auth') +var User = require(__dirname + '/../models/user'); + +var userRouter = module.exports = exports = express.Router(); + +userRouter.post('/signup', bodyParser, function (req, res) { + + User.findOne({'username': req.body.username}, function (err, user) { + if(err) { + console.log('error finding user'); + return res.status(401).json({msg: 'signup cat say some sort of error'}); + } + + if(!user) { //only creates new user after it determines there is no user with that name already in db + var user = new User(); + user.auth.basic.username = req.body.username; + user.username = req.body.username; + user.hashPassword(req.body.password); + + user.save(function(err, data) { + if(err) return handleError(err, res); + + user.generateToken(function(err, token) { + if(err) return handleError(err, res); + + res.json({token: token}); + }); + }); + } else { + console.log('user already exists'); + return res.status(401).json({msg: 'signup cat says user by that name already exists.'}); + } + }); +}); + +userRouter.get('/signin', basicHttp, function (req, res) { + if(!(req.auth.username && req.auth.password)) { + console.log('no username/password provided'); + return res.status(401).json({msg: 'cat say gib username and password rigt now'}); + } + + User.findOne({'auth.basic.username': req.auth.username}, function (err, user) { + if(err) { + console.log('error finding user'); + return res.status(401).json({msg: 'cat say some sort of error finding user'}); + } + + if(!user) { + console.log('no user found'); + return res.status(401).json({msg: 'cat say no user found by that name'}); + } + + if(!user.checkPassword(req.auth.password)) { + console.log('wrong password'); + return res.status(401).json({msg: 'cat say wrong password'}); + } + + user.generateToken(function (err, token) { + if(err) return handleError(err, res); + + res.json({token: token}); + }); + }); +}); + + diff --git a/walter_nicholas/server.js b/walter_nicholas/server.js index f0a16f2..ed6272f 100644 --- a/walter_nicholas/server.js +++ b/walter_nicholas/server.js @@ -1,10 +1,20 @@ var mongoose = require('mongoose'); +var fs = require('fs'); +var express = require('express'); var app = require('express')(); -var tasksRouter = require(__dirname + '/routes/tasks_routes'); +var userRouter = require(__dirname + '/routes/user_routes'); +var taskRouter = require(__dirname + '/routes/task_routes'); +process.env.APP_SECRET = process.env.APP_SECRET || 'changethislaterforsomereason'; mongoose.connect(process.env.MONGOLAB_URI || 'mongodb://localhost/tasks_db_dev'); -app.use('/api', tasksRouter); +app.use('/api', userRouter); +app.use('/api', taskRouter); + +app.use(express.static('build')); +app.get('/', function (req, res) { + res.send((fs.readFileSync(__dirname + '/build/index.html')).toString()); +}); app.listen(3000, function() { console.log('server up');