diff --git a/client/components/auth/Login.jsx b/client/components/auth/Login.jsx index 7f70e0f..cf8eb4b 100644 --- a/client/components/auth/Login.jsx +++ b/client/components/auth/Login.jsx @@ -30,7 +30,7 @@ class Login extends Component { }; this.handleLogin = this.handleLogin.bind(this); this.handleChange = this.handleChange.bind(this); - this.responseGoogle = this.responseGoogle.bind(this); + this.handleGoogleLogin = this.handleGoogleLogin.bind(this); } /** @@ -39,7 +39,7 @@ class Login extends Component { * @memberof Login * @returns {Undefined} redirects to dashboard */ - responseGoogle(response) { + handleGoogleLogin(response) { const googleProfile = response.profileObj; this.props.login(googleProfile); } @@ -66,7 +66,7 @@ class Login extends Component { handleChange(event) { event.preventDefault(); const formField = event.target.name; - const user = Object.assign({}, this.state); + const user = { ...this.state }; if (event.target.value.trim()) { user[formField] = event.target.value.trim(); } @@ -136,8 +136,8 @@ class Login extends Component {
diff --git a/package.json b/package.json index d5af79f..a1145f2 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "job": "babel-node server/cronjob", "cron": "node dist/server/cronjob", "serve": "babel-node server/bin/www.js", - "start": "cross-env NODE_ENV=production node dist/server/bin/www.js", + "start": "node dist/server/bin/www.js", "start:debug": "node --inspect dist/server/bin/www.js", "start:client": "webpack-dev-server --hot --config ./webpack.dev.config.js", "start:dev": "nodemon server/bin/www --exec babel-node", @@ -17,6 +17,7 @@ "pretest": "sequelize db:migrate:undo:all --env test && sequelize db:migrate --env test && cross-env NODE_ENV=test npm run seed:tables", "test": "cross-env NODE_ENV=test npm run seed:join && npm run test:travis", "migrate:dev": "sequelize db:migrate:undo:all --env development && sequelize db:migrate --env development", + "migrate": "sequelize db:migrate:undo:all --env production && sequelize db:migrate --env production", "coverage": "NODE_ENV=test nyc report --reporter=text-lcov | coveralls", "seed:tables": "cross-env NODE_ENV=test babel-node server/seeders/user.js && babel-node server/seeders/bookCategory.js && babel-node server/seeders/book.js", "seed:join": "cross-env NODE_ENV=test babel-node server/seeders/borrowedBook.js", diff --git a/server/config/firebase.js b/server/config/firebase.js new file mode 100644 index 0000000..9a0e90a --- /dev/null +++ b/server/config/firebase.js @@ -0,0 +1,10 @@ +// Initialize Firebase // firebase.initializeApp(config); +// TODO: MOVE THESE TO ENVIRONMENT VARIABLES +export default { + apiKey: 'AIzaSyCUQ8B5UajnVa04E6hFUETBzKWD_4XlnVw', + authDomain: 'hellobooks-180211.firebaseapp.com', + databaseURL: 'https://hellobooks-180211.firebaseio.com', + projectId: 'hellobooks-180211', + storageBucket: 'hellobooks-180211.appspot.com', + messagingSenderId: '701806023399' +}; diff --git a/server/controllers/books.js b/server/controllers/bookController.js similarity index 91% rename from server/controllers/books.js rename to server/controllers/bookController.js index 3819dfe..93c3c74 100644 --- a/server/controllers/books.js +++ b/server/controllers/bookController.js @@ -1,30 +1,7 @@ import { Book, BorrowedBook, BookCategory, Notification } from '../models'; -/** - * Fetch all books that match a catagory from database - * @private - * @param {object} req - express http request object - * @param {object} res - express http response object - * @return {object} - express http response object - */ -const filterBooksByCategory = (req, res) => { - const categoryId = req.query.category; - Book.findAll({ where: { categoryId } }) - .then((books) => { - const message = books.length ? '' : - 'No books match the requested category'; - return res.status(200).send({ - books, - message, - }); - }) - .catch(error => res.status(500).send({ - error - })); -}; - -export default { +const bookController = { /** * Add new book category to library. * @public @@ -130,7 +107,7 @@ export default { */ getAllBooks(req, res) { if (req.query.category) { - return filterBooksByCategory(req, res); + return bookController.filterBooksByCategory(req, res); } Book.findAll() .then((books) => { @@ -147,6 +124,29 @@ export default { .catch(error => res.status(500).send({ error })); }, + /** + * Fetch all books that match a catagory from database + * @private + * @param {object} req - express http request object + * @param {object} res - express http response object + * @return {object} - express http response object + */ + filterBooksByCategory(req, res) { + const categoryId = req.query.category; + Book.findAll({ where: { categoryId } }) + .then((books) => { + const message = books.length ? '' : + 'No books match the requested category'; + return res.status(200).send({ + books, + message, + }); + }) + .catch(error => res.status(500).send({ + error + })); + }, + /** * Edit a book's metadata. * @public @@ -326,3 +326,5 @@ export default { })); } }; + +export default bookController; diff --git a/server/controllers/userController.js b/server/controllers/userController.js new file mode 100644 index 0000000..32ecc4f --- /dev/null +++ b/server/controllers/userController.js @@ -0,0 +1,249 @@ +import bcrypt from 'bcrypt'; +import dotenv from 'dotenv'; + +import { User, Book } from '../models'; +import { getJWT } from '../helpers/helpers'; +import { transporter, mailOptions } from '../config/mail'; + +dotenv.config(); + + +const userController = { + /** + * Create new user account. + * It sends a an object containing a success boolean + * and a json web token or error + * @public + * @method + * @param {object} req - express http request object + * @param {object} res - express http response object + * @return {Object} - returns an http response object + */ + + createUser(req, res) { + const username = req.body.username; + const email = req.body.email; + return User.find({ + where: { $or: [{ username }, { email }] } + }).then((existingUser) => { + if (existingUser && existingUser.username === username) { + return res.status(409).json({ + message: 'username is taken', + }); + } + if (existingUser && existingUser.email === email) { + return res.status(409).json({ + message: 'email is associated with an account', + }); + } + User.create(req.body) + .then((user) => { + const { + id, + isAdmin, + membershipType, + } = user; + const jwtOptions = { id, email, username, isAdmin, membershipType }; + const token = getJWT(jwtOptions); + const { firstName, lastName } = user; + return res.status(201).json({ + token, + id, + firstName, + lastName, + isAdmin, + message: `Welcome ${firstName}. This is your dashboard`, + }); + }) + .catch(error => res.status(400).send({ + error + })); + }) + .catch(error => res.status(500).send({ + error + })); + }, + + /** + * Edit user Information + * @public + * @method + * @param {object} req - express http request object + * @param {object} res - express http response object + * @return {Object} - returns an http response object + */ + updateUserInfo(req, res) { + const updateData = req.body; + updateData.passwordResetToken = null; + return User.findById(req.user.id) + .then((user) => { + user.update(updateData, { returning: true, plain: true }) + .then(() => { + const { + id, + email, + username, + isAdmin, + membershipType, + } = user; + const jwtOptions = { id, email, username, isAdmin, membershipType }; + const token = getJWT(jwtOptions); + const { firstName, lastName } = user; + return res.status(200).json({ + token, + id, + firstName, + lastName, + isAdmin, + message: 'Your information was successfully updated', + }); + }, (error) => { + res.status(500).send({ + error, + }); + }); + }) + .catch(error => res.status(500).send({ + error, + })); + }, + + /** + * Get user data on sign in. + * It sends a an object containing a success boolean + * and a json web token or error + * @public + * @method + * @param {object} req - express http request object + * @param {object} res - express http response object + * @return {Object} - returns an http response object + */ + + getUser(req, res) { + const username = req.body.username; + const password = req.body.password; + return User.findOne({ where: { username } }).then((user) => { + if (!user) { + if (req.body.authId) { + return userController.createUser(req, res); + } + return res.status(403).send({ + message: 'user does not exist', + }); + } + bcrypt.compare(password, user.password).then((result) => { + if (!result) { + return res.status(403).send({ + message: 'wrong username and password combination', + }); + } + const { + id, + email, + isAdmin, + membershipType, + } = user; + const jwtOptions = { id, email, username, isAdmin, membershipType }; + const token = getJWT(jwtOptions); + const { firstName, lastName } = user; + return res.status(200).json({ + token, + id, + firstName, + lastName, + isAdmin, + message: `Welcome back ${firstName}`, + }); + }).catch(error => res.status(500).send({ + error, + })); + }).catch(error => res.status(400).send({ + error + })); + }, + + /** + * Get list of books borrowed by specific user + * It sends a an object containing a success boolean + * and a data key, an array of borrowed books or an error + * Response can be filtered by returned status + * @public + * @method + * @param {object} req - express http request object + * @param {object} res - express http response object + * @return {Object} - returns an http rresponse object + */ + getBorrowedBooks(req, res) { + const id = req.params.id; + User.findOne({ + where: { id }, + include: [{ model: Book }] + }).then((user) => { + let books; + if (req.query && req.query.returned === 'false') { + books = user.Books.filter( + book => book.BorrowedBook.returned === false + ); + } else if (req.query && req.query.returned === 'true') { + books = user.Books.filter( + book => book.BorrowedBook.returned === true + ); + } else { + books = user.Books; + } + return res.status(200).send({ + books + }); + }) + .catch(error => res.status(500).send({ + message: 'An error occured while fetching borrowing history', + error, + })); + }, + + passwordResetMail(req, res) { + return User.findOne({ + where: { email: req.body.email }, + attributes: ['id', 'email'], + plain: true, + }) + .then((user) => { + if (!user) { + return res.status(404).send({ + message: 'Email does not match any account in our records', + }); + } + const BASE_URL = process.env.NODE_ENV === 'development' ? + 'http://localhost:8080' : + 'https://segunolalive-hellobooks.com'; + const token = getJWT({ id: user.id }, '1h'); + user.passwordResetToken = token; + user.save(); + const to = user.email; + const bcc = null; + const subject = 'no-reply: Password reset link'; + const html = `

Use this link to reset your password.

+ ${BASE_URL}/reset-password?token=${token}} +

This link is valid only for an hour

`; + transporter.sendMail(mailOptions(to, bcc, subject, html), + (err) => { + if (err) { + return res.status(500).send({ + message: 'An error occured while sending you a link. Try again', + }); + } + return res.status(200).send({ + message: 'An password reset link has been sent to your email', + }); + }); + }) + .catch(() => ( + res.status(500).send({ + message: 'An error occured while sending you a link. Try again', + }) + )); + } +}; + + +export default userController; diff --git a/server/cronjob.js b/server/cronjob.js index 34ba078..6bb9a50 100644 --- a/server/cronjob.js +++ b/server/cronjob.js @@ -1,9 +1,8 @@ -import sequelize from 'sequelize'; import { BorrowedBook, User } from './models'; import { borrowingDuration } from './helpers/borrowingLimits'; import { transporter, mailOptions } from './config/mail'; -const Op = sequelize.Op; + const timeLimit = borrowingDuration * 1000 * 60 * 60 * 24; const defaulters = () => ( @@ -11,7 +10,7 @@ const defaulters = () => ( where: { returned: false, updatedAt: { - [Op.lt]: new Date(new Date() - timeLimit) + $lt: new Date(new Date() - timeLimit) } }, attributes: ['userId'] @@ -20,7 +19,7 @@ const defaulters = () => ( const ids = borrowedBooks.map(book => book.userId); return User.findAll({ where: { - id: { [Op.in]: ids } + id: { $in: ids } }, attributes: ['email'] }) diff --git a/server/routes/index.js b/server/routes/index.js index f844bfd..901dd84 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -1,8 +1,8 @@ import express from 'express'; -import userController from '../controllers/users'; -import bookController from '../controllers/books'; +import userController from '../controllers/userController'; +import bookController from '../controllers/bookController'; import transactionController from '../controllers/transactionController'; import authenticate from '../middleware/authenticate';