From bb6dd1ad59d1a6869ca5c6ab1fe84c42fa94e07e Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Wed, 27 Sep 2017 14:02:59 +0200 Subject: [PATCH 1/7] Added middleware for authentiaction, endpoints for a user and import for token handling. --- seasoned_api/src/webserver/app.js | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/seasoned_api/src/webserver/app.js b/seasoned_api/src/webserver/app.js index 9bd47fe..270ef71 100644 --- a/seasoned_api/src/webserver/app.js +++ b/seasoned_api/src/webserver/app.js @@ -1,7 +1,9 @@ -var express = require('express'); // call express -var app = express(); // define our app using express +var express= require('express'); // call express +var app = express(); // define our app using express var bodyParser = require('body-parser'); +const tokenToUser = require('./middleware/tokenToUser'); +const mustBeAuthenticated = require('./middleware/mustBeAuthenticated'); // this will let us get the data from a POST // configure app to use bodyParser() @@ -28,11 +30,23 @@ router.get('/', function(req, res) { res.json({ message: 'hooray! welcome to this api!' }); }); +/** + * User + */ +app.post('/api/v1/user', require('./controllers/user/register.js')); +app.post('/api/v1/user/login', require('./controllers/user/login.js')); +app.get('/api/v1/user/history', mustBeAuthenticated, require('./controllers/user/history.js')); +/** + * Seasoned + */ router.get('/v1/seasoned/all', require('./controllers/seasoned/readStrays.js')); router.get('/v1/seasoned/:strayId', require('./controllers/seasoned/strayById.js')); router.post('/v1/seasoned/verify/:strayId', require('./controllers/seasoned/verifyStray.js')); +/** + * Plex + */ router.get('/v1/plex/search', require('./controllers/plex/searchMedia.js')); router.get('/v1/plex/playing', require('./controllers/plex/plexPlaying.js')); router.get('/v1/plex/request', require('./controllers/plex/searchRequest.js')); @@ -40,6 +54,9 @@ router.get('/v1/plex/request/:mediaId', require('./controllers/plex/readRequest. router.post('/v1/plex/request/:mediaId', require('./controllers/plex/submitRequest.js')); router.get('/v1/plex/hook', require('./controllers/plex/hookDump.js')); +/** + * TMDB + */ router.get('/v1/tmdb/search', require('./controllers/tmdb/searchMedia.js')); router.get('/v1/tmdb/discover', require('./controllers/tmdb/discoverMedia.js')); router.get('/v1/tmdb/popular', require('./controllers/tmdb/popularMedia.js')); @@ -49,6 +66,9 @@ router.get('/v1/tmdb/upcoming', require('./controllers/tmdb/getUpcoming.js')); router.get('/v1/tmdb/similar/:mediaId', require('./controllers/tmdb/searchSimilar.js')); router.get('/v1/tmdb/:mediaId', require('./controllers/tmdb/readMedia.js')); +/** + * git + */ router.post('/v1/git/dump', require('./controllers/git/dumpHook.js')); From f4aee549be68ba92cfe8b92a1639e55bedfad985 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Wed, 27 Sep 2017 16:12:20 +0200 Subject: [PATCH 2/7] Added bcrypt and jsonwebtoken to package list --- seasoned_api/package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/seasoned_api/package.json b/seasoned_api/package.json index 94cd8bd..74a5699 100644 --- a/seasoned_api/package.json +++ b/seasoned_api/package.json @@ -5,9 +5,11 @@ "start": "cross-env SEASONED_CONFIG=conf/development.json NODE_PATH=. node src/webserver/server.js" }, "dependencies": { + "bcrypt-nodejs": "^0.0.3", "body-parser": "~1.0.1", "cross-env": "^3.1.3", "express": "~4.0.0", + "jsonwebtoken": "^8.0.1", "mongoose": "^3.6.13", "moviedb": "^0.2.10", "node-cache": "^4.1.1", From 72654fd46536a6f4beddba268f5f4bfe147fe9be Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Wed, 27 Sep 2017 16:13:39 +0200 Subject: [PATCH 3/7] Added searchHistory for adding a logged in users history trace. (More like a test function of the page) --- .../src/searchHistory/searchHistory.js | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 seasoned_api/src/searchHistory/searchHistory.js diff --git a/seasoned_api/src/searchHistory/searchHistory.js b/seasoned_api/src/searchHistory/searchHistory.js new file mode 100644 index 0000000..fbe2f35 --- /dev/null +++ b/seasoned_api/src/searchHistory/searchHistory.js @@ -0,0 +1,39 @@ +const establishedDatabase = require('src/database/database'); + +class SearchHistory { + + constructor(database) { + this.database = database || establishedDatabase; + this.queries = { + 'create': 'insert into search_history (search_query, user_name) values (?, ?)', + 'read': 'select search_query from search_history where user_name = ? order by id desc', + }; + } + + /** + * Retrive a search queries for a user from the database. + * @param {User} user existing user + * @returns {Promise} + */ + read(user) { + return this.database.all(this.queries.read, user.username) + .then(rows => rows.map(row => row.search_query)); + } + + /** + * Creates a new search entry in the database. + * @param {User} user a new user + * @param {String} searchQuery the query the user searched for + * @returns {Promise} + */ + create(user, searchQuery) { + return this.database.run(this.queries.create, [searchQuery, user.username]).catch((error) => { + if (error.message.includes('FOREIGN')) { + throw new Error('Could not create search history.'); + } + }); + } + +} + +module.exports = SearchHistory; From 6ec6c7f9cbaa5d9862ec7ce5ae229a1ca1456305 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Wed, 27 Sep 2017 16:15:28 +0200 Subject: [PATCH 4/7] Added token, user, userRepository and userSecurity in user folder. This handles creating new users, loging in and creating a user specific token then returning it when logging in. --- seasoned_api/src/user/token.js | 38 +++++++++++++ seasoned_api/src/user/user.js | 8 +++ seasoned_api/src/user/userRepository.js | 59 +++++++++++++++++++ seasoned_api/src/user/userSecurity.js | 76 +++++++++++++++++++++++++ 4 files changed, 181 insertions(+) create mode 100644 seasoned_api/src/user/token.js create mode 100644 seasoned_api/src/user/user.js create mode 100644 seasoned_api/src/user/userRepository.js create mode 100644 seasoned_api/src/user/userSecurity.js diff --git a/seasoned_api/src/user/token.js b/seasoned_api/src/user/token.js new file mode 100644 index 0000000..bce5c84 --- /dev/null +++ b/seasoned_api/src/user/token.js @@ -0,0 +1,38 @@ +const User = require('src/user/user'); +const jwt = require('jsonwebtoken'); + +class Token { + + constructor(user) { + this.user = user; + } + + /** + * Generate a new token. + * @param {String} secret a cipher of the token + * @returns {String} + */ + toString(secret) { + return jwt.sign({ username: this.user.username }, secret); + } + + /** + * Decode a token. + * @param {Token} jwtToken an encrypted token + * @param {String} secret a cipher of the token + * @returns {Token} + */ + static fromString(jwtToken, secret) { + let username = null; + + try { + username = jwt.verify(jwtToken, secret).username; + } catch (error) { + throw new Error('The token is invalid.'); + } + const user = new User(username); + return new Token(user); + } +} + +module.exports = Token; diff --git a/seasoned_api/src/user/user.js b/seasoned_api/src/user/user.js new file mode 100644 index 0000000..a4bb44c --- /dev/null +++ b/seasoned_api/src/user/user.js @@ -0,0 +1,8 @@ +class User { + constructor(username, email) { + this.username = username; + this.email = email; + } +} + +module.exports = User; \ No newline at end of file diff --git a/seasoned_api/src/user/userRepository.js b/seasoned_api/src/user/userRepository.js new file mode 100644 index 0000000..b81ef30 --- /dev/null +++ b/seasoned_api/src/user/userRepository.js @@ -0,0 +1,59 @@ +const assert = require('assert'); +const establishedDatabase = require('src/database/database'); + +class UserRepository { + + constructor(database) { + this.database = database || establishedDatabase; + this.queries = { + read: 'select * from user where lower(user_name) = lower(?)', + create: 'insert into user (user_name, email) values(?, ?)', + change: 'update user set password = ? where user_name = ?', + retrieveHash: 'select * from user where user_name = ?', + }; + } + + /** + * Create a user in a database. + * @param {User} user the user you want to create + * @returns {Promise} + */ + create(user) { + return Promise.resolve() + .then(() => this.database.get(this.queries.read, user.username)) + .then(row => assert.equal(row, undefined)) + .then(() => this.database.run(this.queries.create, [user.username, user.email])) + .catch((error) => { + if (error.message.endsWith('email')) { + throw new Error('That email is already taken'); + } else if (error.name === 'AssertionError' || error.message.endsWith('user_name')) { + throw new Error('That username is already taken'); + } + }); + } + + /** + * Retrieve a password from a database. + * @param {User} user the user you want to retrieve the password + * @returns {Promise} + */ + retrieveHash(user) { + return this.database.get(this.queries.retrieveHash, user.username).then((row) => { + assert(row, 'The user does not exist.'); + return row.password; + }); + } + + /** + * Change a user's password in a database. + * @param {User} user the user you want to create + * @param {String} password the new password you want to change + * @returns {Promise} + */ + changePassword(user, password) { + return this.database.run(this.queries.change, [password, user.username]); + } + +} + +module.exports = UserRepository; diff --git a/seasoned_api/src/user/userSecurity.js b/seasoned_api/src/user/userSecurity.js new file mode 100644 index 0000000..185b1bd --- /dev/null +++ b/seasoned_api/src/user/userSecurity.js @@ -0,0 +1,76 @@ +const bcrypt = require('bcrypt-nodejs'); +const UserRepository = require('src/user/userRepository'); + +class UserSecurity { + + constructor(database) { + this.userRepository = new UserRepository(database); + } + + /** + * Create a new user in PlanFlix. + * @param {User} user the new user you want to create + * @param {String} clearPassword a password of the user + * @returns {Promise} + */ + createNewUser(user, clearPassword) { + if (user.username.trim() === '') { + throw new Error('The username is empty.'); + } else if (user.email.trim() === '') { + throw new Error('The email is empty.'); + } else if (clearPassword.trim() === '') { + throw new Error('The password is empty.'); + } else { + return Promise.resolve() + .then(() => this.userRepository.create(user)) + .then(() => UserSecurity.hashPassword(clearPassword)) + .then(hash => this.userRepository.changePassword(user, hash)); + } + } + + /** + * Login into PlanFlix. + * @param {User} user the user you want to login + * @param {String} clearPassword the user's password + * @returns {Promise} + */ + login(user, clearPassword) { + return Promise.resolve() + .then(() => this.userRepository.retrieveHash(user)) + .then(hash => UserSecurity.compareHashes(hash, clearPassword)) + .catch(() => { throw new Error('Wrong username or password.'); }); + } + + /** + * Compare between a password and a hash password from database. + * @param {String} hash the hash password from database + * @param {String} clearPassword the user's password + * @returns {Promise} + */ + static compareHashes(hash, clearPassword) { + return new Promise((resolve, reject) => { + bcrypt.compare(clearPassword, hash, (error, matches) => { + if (matches === true) { + resolve(); + } else { + reject(); + } + }); + }); + } + + /** + * Hashes a password. + * @param {String} clearPassword the user's password + * @returns {Promise} + */ + static hashPassword(clearPassword) { + return new Promise((resolve) => { + bcrypt.hash(clearPassword, null, null, (error, hash) => { + resolve(hash); + }); + }); + } +} + +module.exports = UserSecurity; From 698d2d60726bf3df022d88eef1b48fae6f5be297 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Wed, 27 Sep 2017 16:19:02 +0200 Subject: [PATCH 5/7] Created endpoints for user tasks like login, register and a test endpoint history. A test for checking that session token works as expected. --- .../src/webserver/controllers/user/history.js | 22 +++++++++++++++ .../src/webserver/controllers/user/login.js | 28 +++++++++++++++++++ .../webserver/controllers/user/register.js | 24 ++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 seasoned_api/src/webserver/controllers/user/history.js create mode 100644 seasoned_api/src/webserver/controllers/user/login.js create mode 100644 seasoned_api/src/webserver/controllers/user/register.js diff --git a/seasoned_api/src/webserver/controllers/user/history.js b/seasoned_api/src/webserver/controllers/user/history.js new file mode 100644 index 0000000..e619d19 --- /dev/null +++ b/seasoned_api/src/webserver/controllers/user/history.js @@ -0,0 +1,22 @@ +const SearchHistory = require('src/searchHistory/searchHistory'); +const searchHistory = new SearchHistory(); + +/** + * Controller: Retrieves search history of a logged in user + * @param {Request} req http request variable + * @param {Response} res + * @returns {Callback} + */ +function historyController(req, res) { + const user = req.loggedInUser; + + searchHistory.read(user) + .then((searchQueries) => { + res.send({ success: true, searchQueries }); + }) + .catch((error) => { + res.status(401).send({ success: false, error: error.message }); + }); +} + +module.exports = historyController; diff --git a/seasoned_api/src/webserver/controllers/user/login.js b/seasoned_api/src/webserver/controllers/user/login.js new file mode 100644 index 0000000..4fe7755 --- /dev/null +++ b/seasoned_api/src/webserver/controllers/user/login.js @@ -0,0 +1,28 @@ +const User = require('src/user/user'); +const Token = require('src/user/token'); +const UserSecurity = require('src/user/userSecurity'); +const configuration = require('src/config/configuration').getInstance(); +const secret = configuration.get('authentication', 'secret'); +const userSecurity = new UserSecurity(); + +/** + * Controller: Log in a user provided correct credentials. + * @param {Request} req http request variable + * @param {Response} res + * @returns {Callback} + */ +function loginController(req, res) { + const user = new User(req.body.username); + const password = req.body.password; + + userSecurity.login(user, password) + .then(() => { + const token = new Token(user).toString(secret); + res.send({ success: true, token }); + }) + .catch((error) => { + res.status(401).send({ success: false, error: error.message }); + }); +} + +module.exports = loginController; diff --git a/seasoned_api/src/webserver/controllers/user/register.js b/seasoned_api/src/webserver/controllers/user/register.js new file mode 100644 index 0000000..b7e081a --- /dev/null +++ b/seasoned_api/src/webserver/controllers/user/register.js @@ -0,0 +1,24 @@ +const User = require('src/user/user'); +const UserSecurity = require('src/user/userSecurity'); +const userSecurity = new UserSecurity(); + +/** + * Controller: Register a new user + * @param {Request} req http request variable + * @param {Response} res + * @returns {Callback} + */ +function registerController(req, res) { + const user = new User(req.body.username, req.body.email); + const password = req.body.password; + + userSecurity.createNewUser(user, password) + .then(() => { + res.send({ success: true, message: 'Welcome to Seasoned!' }); + }) + .catch((error) => { + res.status(401).send({ success: false, error: error.message }); + }); +} + +module.exports = registerController; From a3de70e2dad769197bcc7cb36e47d0797ea59e9b Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Wed, 27 Sep 2017 16:25:54 +0200 Subject: [PATCH 6/7] Created a middleware for requests that checks for a token in the Authentication field in the header and verifies that the token is valid for a user. --- .../middleware/mustBeAuthenticated.js | 11 ++++++++++ .../src/webserver/middleware/tokenToUser.js | 22 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 seasoned_api/src/webserver/middleware/mustBeAuthenticated.js create mode 100644 seasoned_api/src/webserver/middleware/tokenToUser.js diff --git a/seasoned_api/src/webserver/middleware/mustBeAuthenticated.js b/seasoned_api/src/webserver/middleware/mustBeAuthenticated.js new file mode 100644 index 0000000..7613179 --- /dev/null +++ b/seasoned_api/src/webserver/middleware/mustBeAuthenticated.js @@ -0,0 +1,11 @@ +const mustBeAuthenticated = (req, res, next) => { + + if (req.loggedInUser === undefined) { + return res.status(401).send({ + success: false, + error: 'You must be logged in.', + }); } + return next(); +}; + +module.exports = mustBeAuthenticated; diff --git a/seasoned_api/src/webserver/middleware/tokenToUser.js b/seasoned_api/src/webserver/middleware/tokenToUser.js new file mode 100644 index 0000000..08e8f2a --- /dev/null +++ b/seasoned_api/src/webserver/middleware/tokenToUser.js @@ -0,0 +1,22 @@ +/* eslint-disable no-param-reassign */ +const configuration = require('src/config/configuration').getInstance(); +const secret = configuration.get('authentication', 'secret'); +const Token = require('src/user/token'); + +// Token example: +// curl -i -H "Authorization:[token]" localhost:31459/api/v1/user/history + +const tokenToUser = (req, res, next) => { + const rawToken = req.headers.authorization; + if (rawToken) { + try { + const token = Token.fromString(rawToken, secret); + req.loggedInUser = token.user; + } catch (error) { + req.loggedInUser = undefined; + } + } + next(); +}; + +module.exports = tokenToUser; From bab4af08d990939132ae935625343d835454460f Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Wed, 27 Sep 2017 16:27:05 +0200 Subject: [PATCH 7/7] Added the use of tokenToUser so that it is verified that a valid token is in the request header when doing actions that need login verification. --- seasoned_api/src/webserver/app.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/seasoned_api/src/webserver/app.js b/seasoned_api/src/webserver/app.js index 270ef71..b09f078 100644 --- a/seasoned_api/src/webserver/app.js +++ b/seasoned_api/src/webserver/app.js @@ -11,6 +11,9 @@ app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); +/* Decode the Authorization header if provided */ +app.use(tokenToUser); + var port = 31459; // set our port var router = express.Router(); var allowedOrigins = ['https://kevinmidboe.com', 'http://localhost:8080']