Merge pull request #36 from KevinMidboe/feature/authentication
Feature/authentication
This commit is contained in:
@@ -5,9 +5,11 @@
|
|||||||
"start": "cross-env SEASONED_CONFIG=conf/development.json NODE_PATH=. node src/webserver/server.js"
|
"start": "cross-env SEASONED_CONFIG=conf/development.json NODE_PATH=. node src/webserver/server.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bcrypt-nodejs": "^0.0.3",
|
||||||
"body-parser": "~1.0.1",
|
"body-parser": "~1.0.1",
|
||||||
"cross-env": "^3.1.3",
|
"cross-env": "^3.1.3",
|
||||||
"express": "~4.0.0",
|
"express": "~4.0.0",
|
||||||
|
"jsonwebtoken": "^8.0.1",
|
||||||
"mongoose": "^3.6.13",
|
"mongoose": "^3.6.13",
|
||||||
"moviedb": "^0.2.10",
|
"moviedb": "^0.2.10",
|
||||||
"node-cache": "^4.1.1",
|
"node-cache": "^4.1.1",
|
||||||
|
|||||||
39
seasoned_api/src/searchHistory/searchHistory.js
Normal file
39
seasoned_api/src/searchHistory/searchHistory.js
Normal file
@@ -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;
|
||||||
38
seasoned_api/src/user/token.js
Normal file
38
seasoned_api/src/user/token.js
Normal file
@@ -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;
|
||||||
8
seasoned_api/src/user/user.js
Normal file
8
seasoned_api/src/user/user.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
class User {
|
||||||
|
constructor(username, email) {
|
||||||
|
this.username = username;
|
||||||
|
this.email = email;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = User;
|
||||||
59
seasoned_api/src/user/userRepository.js
Normal file
59
seasoned_api/src/user/userRepository.js
Normal file
@@ -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;
|
||||||
76
seasoned_api/src/user/userSecurity.js
Normal file
76
seasoned_api/src/user/userSecurity.js
Normal file
@@ -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;
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
|
|
||||||
var express = require('express'); // call express
|
var express= require('express'); // call express
|
||||||
var app = express(); // define our app using express
|
var app = express(); // define our app using express
|
||||||
var bodyParser = require('body-parser');
|
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
|
// this will let us get the data from a POST
|
||||||
// configure app to use bodyParser()
|
// configure app to use bodyParser()
|
||||||
@@ -9,6 +11,9 @@ app.use(bodyParser.json());
|
|||||||
app.use(bodyParser.urlencoded({ extended: true }));
|
app.use(bodyParser.urlencoded({ extended: true }));
|
||||||
|
|
||||||
|
|
||||||
|
/* Decode the Authorization header if provided */
|
||||||
|
app.use(tokenToUser);
|
||||||
|
|
||||||
var port = 31459; // set our port
|
var port = 31459; // set our port
|
||||||
var router = express.Router();
|
var router = express.Router();
|
||||||
var allowedOrigins = ['https://kevinmidboe.com', 'http://localhost:8080']
|
var allowedOrigins = ['https://kevinmidboe.com', 'http://localhost:8080']
|
||||||
@@ -28,11 +33,23 @@ router.get('/', function(req, res) {
|
|||||||
res.json({ message: 'hooray! welcome to this api!' });
|
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/all', require('./controllers/seasoned/readStrays.js'));
|
||||||
router.get('/v1/seasoned/:strayId', require('./controllers/seasoned/strayById.js'));
|
router.get('/v1/seasoned/:strayId', require('./controllers/seasoned/strayById.js'));
|
||||||
router.post('/v1/seasoned/verify/:strayId', require('./controllers/seasoned/verifyStray.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/search', require('./controllers/plex/searchMedia.js'));
|
||||||
router.get('/v1/plex/playing', require('./controllers/plex/plexPlaying.js'));
|
router.get('/v1/plex/playing', require('./controllers/plex/plexPlaying.js'));
|
||||||
router.get('/v1/plex/request', require('./controllers/plex/searchRequest.js'));
|
router.get('/v1/plex/request', require('./controllers/plex/searchRequest.js'));
|
||||||
@@ -40,6 +57,9 @@ router.get('/v1/plex/request/:mediaId', require('./controllers/plex/readRequest.
|
|||||||
router.post('/v1/plex/request/:mediaId', require('./controllers/plex/submitRequest.js'));
|
router.post('/v1/plex/request/:mediaId', require('./controllers/plex/submitRequest.js'));
|
||||||
router.get('/v1/plex/hook', require('./controllers/plex/hookDump.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/search', require('./controllers/tmdb/searchMedia.js'));
|
||||||
router.get('/v1/tmdb/discover', require('./controllers/tmdb/discoverMedia.js'));
|
router.get('/v1/tmdb/discover', require('./controllers/tmdb/discoverMedia.js'));
|
||||||
router.get('/v1/tmdb/popular', require('./controllers/tmdb/popularMedia.js'));
|
router.get('/v1/tmdb/popular', require('./controllers/tmdb/popularMedia.js'));
|
||||||
@@ -49,6 +69,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/similar/:mediaId', require('./controllers/tmdb/searchSimilar.js'));
|
||||||
router.get('/v1/tmdb/:mediaId', require('./controllers/tmdb/readMedia.js'));
|
router.get('/v1/tmdb/:mediaId', require('./controllers/tmdb/readMedia.js'));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* git
|
||||||
|
*/
|
||||||
router.post('/v1/git/dump', require('./controllers/git/dumpHook.js'));
|
router.post('/v1/git/dump', require('./controllers/git/dumpHook.js'));
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
22
seasoned_api/src/webserver/controllers/user/history.js
Normal file
22
seasoned_api/src/webserver/controllers/user/history.js
Normal file
@@ -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;
|
||||||
28
seasoned_api/src/webserver/controllers/user/login.js
Normal file
28
seasoned_api/src/webserver/controllers/user/login.js
Normal file
@@ -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;
|
||||||
24
seasoned_api/src/webserver/controllers/user/register.js
Normal file
24
seasoned_api/src/webserver/controllers/user/register.js
Normal file
@@ -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;
|
||||||
11
seasoned_api/src/webserver/middleware/mustBeAuthenticated.js
Normal file
11
seasoned_api/src/webserver/middleware/mustBeAuthenticated.js
Normal file
@@ -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;
|
||||||
22
seasoned_api/src/webserver/middleware/tokenToUser.js
Normal file
22
seasoned_api/src/webserver/middleware/tokenToUser.js
Normal file
@@ -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;
|
||||||
Reference in New Issue
Block a user