diff --git a/seasoned_api/src/user/userRepository.js b/seasoned_api/src/user/userRepository.js index c9f9f32..216387d 100644 --- a/seasoned_api/src/user/userRepository.js +++ b/seasoned_api/src/user/userRepository.js @@ -10,78 +10,210 @@ class UserRepository { change: 'update user set password = ? where user_name = ?', retrieveHash: 'select * from user where user_name = ?', getAdminStateByUser: 'select admin from user where user_name = ?', - link: 'update user set plex_userid = ? where user_name = ?' + link: 'update settings set plex_userid = ? where user_name = ?', + unlink: 'update settings set plex_userid = null where user_name = ?', + createSettings: 'insert into settings (user_name) values (?)', + updateSettings: 'update settings set user_name = ?, dark_mode = ?, emoji = ?', + getSettings: 'select * from settings where user_name = ?' }; -} + } -/** -* Create a user in a database. -* @param {User} user the user you want to create -* @returns {Promise} -*/ -create(user) { - return this.database.get(this.queries.read, user.username) - .then(() => this.database.run(this.queries.create, user.username)) - .catch((error) => { - if (error.name === 'AssertionError' || error.message.endsWith('user_name')) { - throw new Error('That username is already registered'); - } - throw Error(error) - }); -} + /** + * Create a user in a database. + * @param {User} user the user you want to create + * @returns {Promise} + */ + create(user) { + return this.database.get(this.queries.read, user.username) + .then(() => this.database.run(this.queries.create, user.username)) + .catch((error) => { + if (error.name === 'AssertionError' || error.message.endsWith('user_name')) { + throw new Error('That username is already registered'); + } + throw Error(error) + }); + } -/** -* 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; - }) - .catch(err => { console.log(error); throw new Error('Unable to find your user.'); }) -} - -/** -* 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]) -} - -/** -* Link plex userid with seasoned user -* @param {User} user the user you want to lunk plex userid with -* @param {Number} plexUserID plex unique id -* @returns {Promsie} -*/ -linkPlexUserId(username, plexUserID) { - return new Promise((resolve, reject) => { - this.database.run(this.queries.link, [plexUserID, username]) - .then(row => resolve(row)) - .catch(error => { - // TODO log this unknown db error - console.log('db error', error) - - reject({ - status: 500, - message: 'An unexpected error occured while linking plex and seasoned accounts', - source: 'seasoned database' - }) + /** + * 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; }) - }) -} + .catch(err => { console.log(error); throw new Error('Unable to find your user.'); }) + } -checkAdmin(user) { - return this.database.get(this.queries.getAdminStateByUser, user.username).then((row) => { - return row.admin; - }); + /** + * 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]) + } + + /** + * Link plex userid with seasoned user + * @param {String} username the user you want to lunk plex userid with + * @param {Number} plexUserID plex unique id + * @returns {Promsie} + */ + linkPlexUserId(username, plexUserID) { + return new Promise((resolve, reject) => { + this.database.run(this.queries.link, [plexUserID, username]) + .then(row => resolve(row)) + .catch(error => { + // TODO log this unknown db error + console.log('db error', error) + + reject({ + status: 500, + message: 'An unexpected error occured while linking plex and seasoned accounts', + source: 'seasoned database' + }) + }) + }) + } + + /** + * Unlink plex userid with seasoned user + * @param {User} user the user you want to lunk plex userid with + * @returns {Promsie} + */ + unlinkPlexUserId(username) { + return new Promise((resolve, reject) => { + this.database.run(this.queries.unlink, plexUserID) + .then(row => resolve(row)) + .catch(error => { + // TODO log this unknown db error + console.log('db error', error) + + reject({ + status: 500, + message: 'An unexpected error occured while unlinking plex and seasoned accounts', + source: 'seasoned database' + }) + }) + }) + } + + /** + * Check if the user has boolean flag set for admin in database + * @param {User} user object + * @returns {Promsie} + */ + checkAdmin(user) { + return this.database.get(this.queries.getAdminStateByUser, user.username) + .then((row) => row.admin); + } + + /** + * Get settings for user matching string username + * @param {String} username + * @returns {Promsie} + */ + getSettings(username) { + return new Promise((resolve, reject) => { + this.database.get(this.queries.getSettings, username) + .then(async (row) => { + if (row == null) { + console.log(`settings do not exist for user: ${username}. Creating settings entry.`) + + const userExistsWithUsername = await this.database.get('select * from user where user_name is ?', username) + if (userExistsWithUsername !== undefined) { + try { + resolve(this.dbCreateSettings(username)) + } catch (error) { + reject(error) + } + } else { + reject({ + status: 404, + message: 'User not found, no settings to get' + }) + } + } + + resolve(row) + }) + .catch(error => { + console.error('Unexpected error occured while fetching settings for your account. Error:', error) + reject({ + status: 500, + message: 'An unexpected error occured while fetching settings for your account', + source: 'seasoned database' + }) + }) + }) + } + + /** + * Update settings values for user matching string username + * @param {String} username + * @param {String} dark_mode + * @param {String} emoji + * @returns {Promsie} + */ + updateSettings(username, dark_mode=undefined, emoji=undefined) { + const settings = this.getSettings(username) + dark_mode = dark_mode !== undefined ? dark_mode : settings.dark_mode + emoji = emoji !== undefined ? emoji : settings.emoji + + return this.dbUpdateSettings(username, dark_mode, emoji) + .catch(error => { + if (error.status && error.message) { + return error + } + + return { + status: 500, + message: 'An unexpected error occured while updating settings for your account' + } + }) + } + + /** + * Helper function for creating settings in the database + * @param {String} username + * @returns {Promsie} + */ + dbCreateSettings(username) { + return this.database.run(this.queries.createSettings, username) + .then(() => this.database.get(this.queries.getSettings, username)) + .catch(error => rejectUnexpectedDatabaseError('Unexpected error occured while creating settings', 503, error)) + } + + /** + * Helper function for updating settings in the database + * @param {String} username + * @returns {Promsie} + */ + dbUpdateSettings(username, dark_mode, emoji) { + return new Promise((resolve, reject) => + this.database.run(this.queries.updateSettings, [username, dark_mode, emoji]) + .then(row => resolve(row))) } } + +const rejectUnexpectedDatabaseError = (message, status, error, reject=null) => { + console.error(error) + const body = { + status, + message, + source: 'seasoned database' + } + + if (reject == null) { + return new Promise((resolve, reject) => reject(body)) + } + reject(body) +} + module.exports = UserRepository; diff --git a/seasoned_api/src/webserver/app.js b/seasoned_api/src/webserver/app.js index a7d74b3..185fd58 100644 --- a/seasoned_api/src/webserver/app.js +++ b/seasoned_api/src/webserver/app.js @@ -9,6 +9,8 @@ const configuration = require('src/config/configuration').getInstance(); const listController = require('./controllers/list/listController'); const tautulli = require('./controllers/user/viewHistory.js'); +const SettingsController = require('./controllers/user/settings'); +const AuthenticatePlexAccountController = require('./controllers/user/AuthenticatePlexAccount'); // TODO: Have our raven router check if there is a value, if not don't enable raven. Raven.config(configuration.get('raven', 'DSN')).install(); @@ -55,9 +57,12 @@ app.use(function onError(err, req, res, next) { */ router.post('/v1/user', require('./controllers/user/register.js')); router.post('/v1/user/login', require('./controllers/user/login.js')); +router.get('/v1/user/settings', mustBeAuthenticated, SettingsController.getSettingsController); +router.put('/v1/user/settings', mustBeAuthenticated, SettingsController.updateSettingsController); router.get('/v1/user/search_history', mustBeAuthenticated, require('./controllers/user/searchHistory.js')); router.get('/v1/user/requests', mustBeAuthenticated, require('./controllers/user/requests.js')); -router.post('/v1/user/authenticate', mustBeAuthenticated, require('./controllers/user/authenticatePlexAccount.js')); +router.post('/v1/user/link_plex', mustBeAuthenticated, AuthenticatePlexAccountController.link); +router.post('/v1/user/unlink_plex', mustBeAuthenticated, AuthenticatePlexAccountController.unlink); router.get('/v1/user/view_history', mustHaveAccountLinkedToPlex, tautulli.userViewHistoryController); router.get('/v1/user/watch_time', mustHaveAccountLinkedToPlex, tautulli.watchTimeStatsController); diff --git a/seasoned_api/src/webserver/controllers/user/authenticatePlexAccount.js b/seasoned_api/src/webserver/controllers/user/authenticatePlexAccount.js index 891212f..f21a6f3 100644 --- a/seasoned_api/src/webserver/controllers/user/authenticatePlexAccount.js +++ b/seasoned_api/src/webserver/controllers/user/authenticatePlexAccount.js @@ -57,7 +57,7 @@ function plexAuthenticate(username, password) { .then(resp => handleResponse(resp)) } -function authenticatePlexAccountController(req, res) { +function link(req, res) { const user = req.loggedInUser; const { username, password } = req.body; @@ -70,4 +70,18 @@ function authenticatePlexAccountController(req, res) { .catch(error => handleError(error, res)) } -module.exports = authenticatePlexAccountController; +function link(req, res) { + const user = req.loggedInUser; + + return userRepository.unlinkPlexUserId(user.username) + .then(response => res.send({ + success: true, + message: "Successfully unlinked plex account from seasoned request." + })) + .catch(error => handleError(error, res)) +} + +module.exports = { + link, + unlink +}; diff --git a/seasoned_api/src/webserver/controllers/user/settings.js b/seasoned_api/src/webserver/controllers/user/settings.js new file mode 100644 index 0000000..6774146 --- /dev/null +++ b/seasoned_api/src/webserver/controllers/user/settings.js @@ -0,0 +1,42 @@ +const UserRepository = require('src/user/userRepository'); +const userRepository = new UserRepository(); +/** + * Controller: Retrieves settings of a logged in user + * @param {Request} req http request variable + * @param {Response} res + * @returns {Callback} + */ +const getSettingsController = (req, res) => { + const user = req.loggedInUser; + const username = user === undefined ? undefined : user.username; + + userRepository.getSettings(username) + .then(settings => { + res.send({ success: true, settings }); + }) + .catch(error => { + res.status(404).send({ success: false, message: error.message }); + }); +} + + +const updateSettingsController = (req, res) => { + const user = req.loggedInUser; + const username = user === undefined ? undefined : user.username; + + const idempotencyKey = req.headers('Idempotency-Key'); // TODO implement better transactions + const { dark_mode, emoji } = req.body; + + userRepository.updateSettings(username, dark_mode, emoji) + .then(settings => { + res.send({ success: true, settings }); + }) + .catch(error => { + res.status(404).send({ success: false, message: error.message }); + }); +} + +module.exports = { + getSettingsController, + updateSettingsController +} \ No newline at end of file diff --git a/seasoned_api/src/webserver/middleware/mustHaveAccountLinkedToPlex.js b/seasoned_api/src/webserver/middleware/mustHaveAccountLinkedToPlex.js index de1ae69..67f6944 100644 --- a/seasoned_api/src/webserver/middleware/mustHaveAccountLinkedToPlex.js +++ b/seasoned_api/src/webserver/middleware/mustHaveAccountLinkedToPlex.js @@ -7,10 +7,10 @@ const mustHaveAccountLinkedToPlex = (req, res, next) => { if (loggedInUser === undefined) { return res.status(401).send({ success: false, - message: 'You must be logged in.', + message: 'You must have your account linked to a plex account.', }); } else { - database.get(`SELECT plex_userid FROM user WHERE user_name IS ?`, loggedInUser.username) + database.get(`SELECT plex_userid FROM settings WHERE user_name IS ?`, loggedInUser.username) .then(row => { const plex_userid = row.plex_userid;