diff --git a/README.md b/README.md
index 21f5dd1..413c413 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,5 @@
# Final Project Starter
A starter repository for the Final Project.
+
+Hi, my name is Steven Fiero and this is the repo for my final project for the ACA Advanced class.
diff --git a/client/package.json b/client/package.json
index a7340d9..f78a4c3 100644
--- a/client/package.json
+++ b/client/package.json
@@ -18,5 +18,6 @@
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
- }
+ },
+ "proxy": "http://localhost:3001"
}
diff --git a/client/src/App.js b/client/src/App.js
index d03f4a8..c178e0a 100644
--- a/client/src/App.js
+++ b/client/src/App.js
@@ -1,13 +1,119 @@
import React, { Component } from 'react';
-import { BrowserRouter, Route } from 'react-router-dom';
+import { BrowserRouter, Route, Switch } from 'react-router-dom';
+import axios from 'axios';
import './App.css';
import SignUpSignIn from './SignUpSignIn';
import TopNavbar from './TopNavbar';
import Secret from './Secret';
-import axios from 'axios';
+
class App extends Component {
+ constructor() {
+ super();
+
+ this.state = {
+ signUpSignInError: '',
+ authenticated: localStorage.getItem('token')
+ };
+ }
+
+
+ handleSignUp(credentials) {
+ // Handler is responsible for taking the credentials, verifying the information
+ // and submitting the request to the API to signup a new user.
+ const { username, password, confirmPassword } = credentials;
+ if (!username.trim() || !password.trim() || password.trim() !== confirmPassword.trim()) {
+ this.setState({
+ signUpSignInError: 'Must Provide All Fields'
+ });
+ } else {
+ axios.post('/api/signup', credentials)
+ .then(resp => {
+ const { token } = resp.data;
+ localStorage.setItem('token', token);
+
+ this.setState({
+ signUpSignInError: '',
+ authenticated: token
+ });
+ });
+ }
+ }
+
+
+ handleSignIn(credentials) {
+ // Handler is responsible for taking the credentials, verifying the information
+ // and submitting the request to the API to signin an existing user.
+ const { username, password } = credentials;
+ if (!username.trim() || !password.trim()) {
+ this.setState({
+ signUpSignInError: 'Must Provide All Fields'
+ });
+ } else {
+ axios.post('/api/signin', credentials)
+ .then(resp => {
+ const { token } = resp.data;
+ localStorage.setItem('token', token);
+ this.setState({
+ signUpSignInError: '',
+ authenticated: token
+ });
+ });
+ }
+ }
+
+
+ handleSignOut() {
+ localStorage.removeItem('token');
+ this.setState({
+ authenticated: false
+ });
+ }
+
+
+ renderSignUpSignIn() {
+ return ;
+ }
+
+
+ renderApp() {
+ // Returning routing logic to be rendered
+ // When user successfully signsup / signsin, the token is updated in state,
+ // which causes a re-render and then the renderApp method is called,
+ // which allows the user access to the application.
+ return (
+
+
+ I am protected! } />
+
+ NOT FOUND! } />
+
+
+ );
+ }
+
+
+ // Looking into state for authenticated value to be set, if so render App.
+ // If state does not have authenticated value, render SignUpSignIn.
+ render() {
+ return (
+
+
+
+ {this.state.authenticated ? this.renderApp() : this.renderSignUpSignIn()}
+
+
+ );
+ }
}
+
export default App;
diff --git a/client/src/Secret.js b/client/src/Secret.js
index dc30585..f370cda 100644
--- a/client/src/Secret.js
+++ b/client/src/Secret.js
@@ -1,6 +1,7 @@
import React, { Component } from 'react';
import axios from 'axios';
+
class Secret extends Component {
constructor() {
super();
@@ -10,21 +11,24 @@ class Secret extends Component {
};
}
+
componentDidMount() {
axios.get('/api/secret', {
+ // Passing token via authorization header
headers: {
authorization: localStorage.getItem('token')
}
})
.then(resp => {
this.setState({
- ...this.state,
message: resp.data
});
})
+ /* eslint no-console: 0 */
.catch(err => console.log(err));
}
+
render() {
return (
{this.state.message}
@@ -32,4 +36,5 @@ class Secret extends Component {
}
}
+
export default Secret;
diff --git a/client/src/SignIn.js b/client/src/SignIn.js
new file mode 100644
index 0000000..88782a7
--- /dev/null
+++ b/client/src/SignIn.js
@@ -0,0 +1,75 @@
+import React, { Component, PropTypes } from 'react';
+import { FormGroup, ControlLabel, FormControl, Button } from 'react-bootstrap';
+
+
+class SignIn extends Component {
+ constructor() {
+ super();
+
+ this.state = {
+ username: '',
+ password: '',
+ };
+ }
+
+
+ handleSubmit(event) {
+ // handleSubmit method is invoked when the form is submitted
+ event.preventDefault();
+
+ this.props.onSignIn({
+ username: this.state.username,
+ password: this.state.password
+ });
+ }
+
+
+ handleChange(event) {
+ const { name, value } = event.target;
+
+ this.setState({
+ [name]: value
+ });
+ }
+
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+
+SignIn.propTypes = {
+ onSignIn: PropTypes.func.isRequired
+};
+
+
+export default SignIn;
diff --git a/client/src/SignUp.js b/client/src/SignUp.js
index 7b533c2..658fde2 100644
--- a/client/src/SignUp.js
+++ b/client/src/SignUp.js
@@ -1,6 +1,7 @@
import React, { Component, PropTypes } from 'react';
import { FormGroup, ControlLabel, FormControl, Button } from 'react-bootstrap';
+
class SignUp extends Component {
constructor() {
super();
@@ -8,11 +9,13 @@ class SignUp extends Component {
this.state = {
username: '',
password: '',
- confirmPassword: '',
- }
+ confirmPassword: ''
+ };
}
+
handleSubmit(event) {
+ // handleSubmit method is invoked when the form is submitted
event.preventDefault();
this.props.onSignUp({
@@ -22,15 +25,16 @@ class SignUp extends Component {
});
}
+
handleChange(event) {
const { name, value } = event.target;
- this.setState(prev => ({
- ...prev,
+ this.setState({
[name]: value
- }));
+ });
}
+
render() {
return (
);
}
}
+
SignUp.propTypes = {
onSignUp: PropTypes.func.isRequired
};
+
export default SignUp;
diff --git a/client/src/SignUpSignIn.js b/client/src/SignUpSignIn.js
index 8bbfc4f..5541fd8 100644
--- a/client/src/SignUpSignIn.js
+++ b/client/src/SignUpSignIn.js
@@ -1,6 +1,8 @@
import React, { Component, PropTypes } from 'react';
import { Tabs, Tab, Row, Col, Alert } from 'react-bootstrap';
import SignUp from './SignUp';
+import SignIn from './SignIn';
+
class SignUpSignIn extends Component {
@@ -22,18 +24,21 @@ class SignUpSignIn extends Component {
- Sign In
+
- )
+ );
}
}
+
SignUpSignIn.propTypes = {
error: PropTypes.string,
- onSignUp: PropTypes.func.isRequired
+ onSignUp: PropTypes.func.isRequired,
+ onSignIn: PropTypes.func.isRequired
};
+
export default SignUpSignIn;
diff --git a/client/src/TopNavbar.js b/client/src/TopNavbar.js
index e5d36b9..6d5f027 100644
--- a/client/src/TopNavbar.js
+++ b/client/src/TopNavbar.js
@@ -1,6 +1,7 @@
import React, { PropTypes } from 'react';
import { Navbar, Nav, NavItem } from 'react-bootstrap';
-import { Link } from 'react-router';
+import { Link } from 'react-router-dom';
+
const TopNavbar = (props) => {
return (
@@ -20,16 +21,17 @@ const TopNavbar = (props) => {
Secret
-
- : null
+ : null
}
);
-}
+};
+
TopNavbar.propTypes = {
onSignOut: PropTypes.func.isRequired,
- showNavItems: PropTypes.bool.isRequired
+ showNavItems: PropTypes.string.isRequired
};
+
export default TopNavbar;
diff --git a/src/controllers/ListsController.js b/src/controllers/ListsController.js
new file mode 100644
index 0000000..244a9a1
--- /dev/null
+++ b/src/controllers/ListsController.js
@@ -0,0 +1,81 @@
+import ListModel from '../models/ListModel';
+
+
+const ListController = {
+
+
+ // Declare a (GET = list) route.
+ list(req, res, next) {
+ ListModel.find({ _id: req.params.list_id, userId: req.user._id })
+ .exec()
+ .then(list => {
+ return res.json(list);
+ })
+ .catch(err => next(err));
+ },
+
+
+ // Declare a (GET/:id = show) route
+ show(req, res, next) {
+ ListModel.find({ _id: req.params.list_id, userId: req.user._id })
+ .exec()
+ .then(list => {
+ return res.json(list);
+ })
+ .catch(err => next(err));
+ },
+
+
+ // Declare a (POST = create) route.
+ create(req, res, next ) {
+ // In create method, assigning the title based on what is received
+ // from the request body, and user_id from the req.user.
+ const list = new List({ title: req.body.title, userId: req.user._id });
+ list
+ .save()
+ .then(newList => res.json(newList))
+ .catch(err => next(err));
+ },
+
+
+ // Declare a (PUT = update) route.
+ update(req, res, next) {
+ // In update method, finding the List based on the _id and user_id.
+ // This means a user will not be able to update a list they do not own.
+ ListModel.find({ _id: req.params.id, userId: req.user._id })
+ .exec()
+ .then(list => {
+ if (!list) {
+ return next('No List Found');
+ }
+
+ list.title = req.params.title;
+ return list.save();
+ })
+ .then(list => {
+ return req.json(list);
+ })
+ .catch(err => next(err));
+ },
+
+
+ // Declare a DELETE (remove) route.
+ remove(req, res, next) {
+ const itemId = req.params.item_id;
+
+ ListModel.find({ _id: req.params.list_id, userId: req.user._id })
+ .exec()
+ .then(list => {
+ list.items.id(itemId).remove();
+
+ return list.save();
+ })
+ .then(list => {
+ return req.json(list.items.id(itemId));
+ })
+ .catch(err => next(err));
+ }
+};
+
+
+export default ListController;
diff --git a/src/controllers/ListsItemsController.js b/src/controllers/ListsItemsController.js
new file mode 100644
index 0000000..7d7c1b0
--- /dev/null
+++ b/src/controllers/ListsItemsController.js
@@ -0,0 +1,101 @@
+import ListModel from '../models/ListModel';
+
+
+const ListsItemsController = {
+
+
+ // Declare a (GET = list) route.
+ list(req, res, next) {
+ ListModel.find({ _id: req.params.list_id, userId: req.user._id })
+ .exec()
+ .then(list => {
+ return res.json(list);
+ })
+ .catch(err => next(err));
+ },
+
+
+ // Declare a (GET/:id = show) route.
+ show(req, res, next) {
+ ListModel.find({ _id: req.params.list_id, userId: req.user._id })
+ .exec()
+ .then(list => {
+ return res.json(list);
+ })
+ .catch(err => next(err));
+ },
+
+
+ // Declare a (POST = create) route.
+ create(req, res, next) {
+ // Find the list by it's id, and ensure that the current user owns the list
+ ListModel.find({ _id: req.params.list_id, userId: req.user._id })
+ .then(list => {
+ // Create a new object that represents an item
+ const item = {
+ text: req.body.text,
+ completed: false
+ };
+
+ // Add the item to the lists items array
+ list.items.push(item);
+
+ // Save the list
+ return list.save();
+ })
+ .then(list => {
+ // Grab the newly created item, which is the last item in the array
+ const newItem = list.items[list.items.length - 1];
+
+ // Return that item
+ return res.json(newItem);
+ })
+ .catch(err => next(err));
+ },
+
+
+ // Declare a (PUT = update) route.
+ update(req, res, next) {
+ const itemId = req.params.item_id;
+
+ // Find the list by it's id, and ensure that the current user owns the list
+ ListModel.find({ _id: req.params.list_id, userId: req.user._id })
+ .exec()
+ .then(list => {
+ // Find the item by it's _id
+ const item = list.items.id(itemId);
+
+ // Update the item if new attributes are sent, or use the current attributes
+ item.text = req.body.text || item.text;
+ item.completed = req.body.completed || item.completed;
+
+ return list.save();
+ })
+ .then(list => {
+ // Return the updated item
+ return req.json(list.items.id(itemId));
+ })
+ .catch(err => next(err));
+ },
+
+
+ // Declare a (DELETE = remove) route.
+ remove(req, res, next) {
+ const itemId = req.params.item_id;
+
+ ListModel.find({ _id: req.params.list_id, userId: req.user._id })
+ .exec()
+ .then(list => {
+ list.items.id(itemId).remove();
+
+ return list.save();
+ })
+ .then(list => {
+ return req.json(list.items.id(itemId));
+ })
+ .catch(err => next(err));
+ }
+};
+
+
+export default ListsItemsController;
diff --git a/src/index.js b/src/index.js
index 96dee02..2fff3f5 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,24 +1,47 @@
// dotenv allows us to declare environment variables in a .env file, \
// find out more here https://github.com/motdotla/dotenv
-require("dotenv").config();
-const express = require("express");
-const bodyParser = require("body-parser");
-const mongoose = require("mongoose");
-const passport = require("passport");
+require('dotenv').config();
+const express = require('express');
+const bodyParser = require('body-parser');
+const mongoose = require('mongoose');
+const passport = require('passport');
+import ListRoutes from './routes/ListRoutes';
+import ListItemRoutes from './routes/ListItemRoutes';
+
+// Require our custom strategies
+require('./services/passport');
+
+
+/* eslint no-console: 0 */
mongoose.Promise = global.Promise;
mongoose
- .connect("mongodb://localhost/todo-list-app")
- .then(() => console.log("[mongoose] Connected to MongoDB"))
- .catch(() => console.log("[mongoose] Error connecting to MongoDB"));
+ .connect('mongodb://localhost/advanced-final-project-starter')
+ .then(() => console.log('[mongoose] Connected to MongoDB'))
+ .catch(() => console.log('[mongoose] Error connecting to MongoDB'));
const app = express();
-const authenticationRoutes = require("./routes/AuthenticationRoutes");
+const authenticationRoutes = require('./routes/AuthenticationRoutes');
app.use(bodyParser.json());
-// app.use(authenticationRoutes);
+app.use(authenticationRoutes);
+
+const authStrategy = passport.authenticate('authStrategy', { session: false });
+
+app.use(authStrategy, ListRoutes);
+app.use(authStrategy, ListItemRoutes);
+// Applied middleware to routes, and all ListRoutes will be protected by authStrategy.
+// This means a user will not have access unless they are logged into the application.
+
+/* eslint no-unused-vars: 0 */
+app.get('/api/secret', authStrategy, function (req, res, next) {
+ res.send(`The current user is ${req.user.username}`);
+});
+
+
const port = process.env.PORT || 3001;
+
app.listen(port, () => {
console.log(`Listening on port:${port}`);
});
diff --git a/src/models/ListModel.js b/src/models/ListModel.js
new file mode 100644
index 0000000..79454fc
--- /dev/null
+++ b/src/models/ListModel.js
@@ -0,0 +1,33 @@
+import mongoose, { Schema } from 'mongoose';
+
+
+const listSchema = new Schema({
+ title: {
+ type: String,
+ required: true
+ },
+
+ userId: {
+ type: Schema.Types.ObjectId,
+ required: true
+ },
+
+ // Items do not have their own ItemModel.js file, but will instead exist
+ // inside of a List document in the Lists collection.
+ // Declare a new property items, and set its value to an array.
+ // Mongoose will store an array of items.
+ items: [{
+ text: {
+ type: String,
+ required: true
+ },
+
+ completed: {
+ type: Boolean,
+ required: true
+ }
+ }]
+});
+
+
+export default mongoose.model('List', listSchema);
diff --git a/src/models/UserModel.js b/src/models/UserModel.js
index 47c5e39..48107ef 100644
--- a/src/models/UserModel.js
+++ b/src/models/UserModel.js
@@ -1,6 +1,6 @@
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
-const bcrypt = require('bcrypt');
+
const userSchema = new Schema({
username: {
diff --git a/src/routes/AuthenticationRoutes.js b/src/routes/AuthenticationRoutes.js
index 611413a..3479dd9 100644
--- a/src/routes/AuthenticationRoutes.js
+++ b/src/routes/AuthenticationRoutes.js
@@ -3,3 +3,65 @@ const router = express.Router();
const jwt = require('jwt-simple');
const User = require('../models/UserModel');
const bcrypt = require('bcrypt');
+const passport = require('passport');
+
+
+// Require our custom strategies
+require('../services/passport');
+
+
+// Authentication
+const signinStrategy = passport.authenticate('signinStrategy', { session: false });
+
+
+// Helper method to create token for a user
+function tokenForUser(user) {
+ const timestamp = new Date().getTime();
+ return jwt.encode({ userId: user.id, iat: timestamp }, process.env.SECRET);
+}
+
+
+/* eslint no-unused-vars: 0 */
+router.post('/api/signin', signinStrategy, function (req, res, next) {
+ res.json({ token: tokenForUser(req.user)});
+});
+
+
+router.post('/api/signup', function (req, res, next) {
+ // Grab the username and password from the request body
+ const { username, password } = req.body;
+
+ // If no username or password was supplied return an error
+ if (!username || !password) {
+ return res.status(422)
+ .json({ error: 'You must provide a username and password' });
+ }
+
+ // Look for a user with the current user name
+ User.findOne({ username }).exec()
+ .then((existingUser) => {
+ // If the user exists, return an error on sign up
+ if (existingUser) {
+ return res.status(422).json({ error: 'Username is in use' });
+ }
+
+ // If the user does not exist, create the user
+ // Use bcrypt to hash their password (Never save plain text passwords!)
+ bcrypt.hash(password, 10, function (err, hashedPassword) {
+ if (err) {
+ return next(err);
+ }
+
+ // Create a new user with the supplied username and the hashed password
+ const user = new User({ username, password: hashedPassword });
+
+ // Save and return the user
+ user.save()
+ .then(newUser => res.json({ token: tokenForUser(newUser) }));
+ });
+ })
+ .catch(err => next(err));
+});
+
+
+module.exports = router;
diff --git a/src/routes/ListItemRoutes.js b/src/routes/ListItemRoutes.js
new file mode 100644
index 0000000..3da61e4
--- /dev/null
+++ b/src/routes/ListItemRoutes.js
@@ -0,0 +1,24 @@
+import express from 'express';
+import ListsItemsController from '../controllers/ListsItemsController';
+const router = express.Router();
+
+
+// Declare GET routes.
+router.get('/lists', ListsItemsController.list);
+
+router.get('/lists/:list_id/items', ListsItemsController.show);
+
+
+// Declare a POST (create) route.
+router.post('/lists/:list_id/items', ListsItemsController.create);
+
+
+// Declare a PUT (update) route.
+router.put('/lists/:list_id/items/:item_id', ListsItemsController.update);
+
+
+// Declare a DELETE (remove) route.
+router.delete('/lists/:list_id/items/:item_id', ListsItemsController.remove);
+
+
+export default router;
diff --git a/src/routes/ListRoutes.js b/src/routes/ListRoutes.js
new file mode 100644
index 0000000..74e7c64
--- /dev/null
+++ b/src/routes/ListRoutes.js
@@ -0,0 +1,24 @@
+import express from 'express';
+import ListController from '../controllers/ListsController';
+const router = express.Router();
+
+
+// Declare GET routes.
+router.get('/lists', ListController.list);
+
+router.get('/lists/:list_id', ListController.show);
+
+
+// Declare a POST (create) route.
+router.post('/lists', ListController.create);
+
+
+// Declare a PUT (update) route.
+router.put('/lists/:id', ListController.update);
+
+
+// Declare a DELETE (remove) route.
+router.delete('/lists/:id', ListController.remove);
+
+
+export default router;
diff --git a/src/services/passport.js b/src/services/passport.js
index 5680076..f55105c 100644
--- a/src/services/passport.js
+++ b/src/services/passport.js
@@ -3,3 +3,64 @@ const passport = require('passport');
const User = require('../models/UserModel');
const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt');
const LocalStrategy = require('passport-local');
+
+
+const signinStrategy = new LocalStrategy(function (username, password, done) {
+ User.findOne({ username }).exec()
+ .then(user => {
+ // If there is no user found, call done with a `null` argument, signifying
+ // no error and `false` signifying that the signin failed
+ if (!user) {
+ return done(null, false);
+ }
+
+ bcrypt.compare(password, user.password, function (err, isMatch) {
+ // If there is an error, call done with the error
+ if (err) {
+ return done(err, false);
+ }
+
+ // If the passwords do not match, call done with a `null` argument, signifying
+ // no error and `false` signifying that the signin failed
+ if (!isMatch) {
+ return done(null, false);
+ }
+
+ // If there are no errors and the passwords match,
+ // call done with a `null` argument, signifying no error
+ // and with the now signed in user
+ return done(null, user);
+ });
+ })
+ .catch(err => done(err, false));
+});
+
+
+// Setup options for JwtStrategy
+const jwtOptions = {
+ // Get the secret from the environment
+ secretOrKey: process.env.SECRET,
+ // Tell the strategy where to find the token in the request
+ jwtFromRequest: ExtractJwt.fromHeader('authorization')
+};
+
+
+// Create JWT strategy
+// This will take the token and decode it to
+// extract the information we have stored in it
+const authStrategy = new JwtStrategy(jwtOptions, function (payload, done) {
+ User.findById(payload.userId, function (err, user) {
+ if (err) { return done(err, false); }
+
+ if (user) {
+ done(null, user);
+ } else {
+ done(null, false);
+ }
+ });
+});
+
+
+// Tell passport to use this strategy
+passport.use('authStrategy', authStrategy);
+passport.use('signinStrategy', signinStrategy);