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;