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"
|
||||
},
|
||||
"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",
|
||||
|
||||
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 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()
|
||||
@@ -9,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']
|
||||
@@ -28,11 +33,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 +57,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 +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/:mediaId', require('./controllers/tmdb/readMedia.js'));
|
||||
|
||||
/**
|
||||
* git
|
||||
*/
|
||||
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