From 05b001de2e7bd4a19af17036a4153e289fb12ef7 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Mon, 4 Nov 2019 00:43:42 +0100 Subject: [PATCH 01/19] Created endpoint for authenticating with plex and linking your plex user to the logged in seasoned user. User database table gets a new plex_userid column. This will be used to fetch tautulli stats for your account. --- seasoned_api/src/database/schemas/setup.sql | 3 +- seasoned_api/src/user/userRepository.js | 131 ++++++++++-------- seasoned_api/src/webserver/app.js | 1 + .../user/authenticatePlexAccount.js | 73 ++++++++++ 4 files changed, 153 insertions(+), 55 deletions(-) create mode 100644 seasoned_api/src/webserver/controllers/user/authenticatePlexAccount.js diff --git a/seasoned_api/src/database/schemas/setup.sql b/seasoned_api/src/database/schemas/setup.sql index 91bfc3e..a55e4e6 100644 --- a/seasoned_api/src/database/schemas/setup.sql +++ b/seasoned_api/src/database/schemas/setup.sql @@ -1,8 +1,9 @@ CREATE TABLE IF NOT EXISTS user ( user_name varchar(127) UNIQUE, password varchar(127), - email varchar(127) UNIQUE, admin boolean DEFAULT 0, + email varchar(127) UNIQUE, + plex_userid varchar(127) DEFAULT NULL, primary key (user_name) ); diff --git a/seasoned_api/src/user/userRepository.js b/seasoned_api/src/user/userRepository.js index c2cf6b2..76da283 100644 --- a/seasoned_api/src/user/userRepository.js +++ b/seasoned_api/src/user/userRepository.js @@ -2,64 +2,87 @@ 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) values (?)', - change: 'update user set password = ? where user_name = ?', - retrieveHash: 'select * from user where user_name = ?', - getAdminStateByUser: 'select admin from user where user_name = ?' - }; - } + constructor(database) { + this.database = database || establishedDatabase; + this.queries = { + read: 'select * from user where lower(user_name) = lower(?)', + create: 'insert into user (user_name) values (?)', + 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 = ?' + }; +} - /** - * 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(() => 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 Promise.resolve() - .then(() => 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.'); }); - } +/** +* Retrieve a password from a database. +* @param {User} user the user you want to retrieve the password +* @returns {Promise} +*/ +retrieveHash(user) { + console.log('retrieving hash for user', 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 Promise.resolve(this.database.run(this.queries.change, [password, user.username])); - } +/** +* 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]) +} - checkAdmin(user) { - return this.database.get(this.queries.getAdminStateByUser, user.username).then((row) => { - return row.admin; - }); - } +/** +* 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' + }) + }) + }) +} + +checkAdmin(user) { + return this.database.get(this.queries.getAdminStateByUser, user.username).then((row) => { + return row.admin; + }); + } } module.exports = UserRepository; diff --git a/seasoned_api/src/webserver/app.js b/seasoned_api/src/webserver/app.js index b1b625e..0b0a57b 100644 --- a/seasoned_api/src/webserver/app.js +++ b/seasoned_api/src/webserver/app.js @@ -55,6 +55,7 @@ router.post('/v1/user', require('./controllers/user/register.js')); router.post('/v1/user/login', require('./controllers/user/login.js')); router.get('/v1/user/history', mustBeAuthenticated, require('./controllers/user/history.js')); router.get('/v1/user/requests', mustBeAuthenticated, require('./controllers/user/requests.js')); +router.post('/v1/user/authenticate', mustBeAuthenticated, require('./controllers/user/authenticatePlexAccount.js')); /** * Seasoned diff --git a/seasoned_api/src/webserver/controllers/user/authenticatePlexAccount.js b/seasoned_api/src/webserver/controllers/user/authenticatePlexAccount.js new file mode 100644 index 0000000..891212f --- /dev/null +++ b/seasoned_api/src/webserver/controllers/user/authenticatePlexAccount.js @@ -0,0 +1,73 @@ +const UserRepository = require('src/user/userRepository'); +const userRepository = new UserRepository(); + const fetch = require('node-fetch'); + const FormData = require('form-data'); + +function handleError(error, res) { + let { status, message, source } = error; + + if (status && message) { + if (status === 401) { + message = 'Unauthorized. Please check plex credentials.', + source = 'plex' + } + + res.status(status).send({ success: false, message, source }) + } else { + console.log('caught authenticate plex account controller error', error) + res.status(500).send({ + message: 'An unexpected error occured while authenticating your account with plex', + source + }) + } +} + +function handleResponse(response) { + if (!response.ok) { + throw { + success: false, + status: response.status, + message: response.statusText + } + } + + return response.json() +} + +function plexAuthenticate(username, password) { + const url = 'https://plex.tv/api/v2/users/signin' + + const form = new FormData() + form.append('login', username) + form.append('password', password) + form.append('rememberMe', 'false') + + const headers = { + 'Accept': 'application/json, text/plain, */*', + 'Content-Type': form.getHeaders()['content-type'], + 'X-Plex-Client-Identifier': 'seasonedRequest' + } + const options = { + method: 'POST', + headers, + body: form + } + + return fetch(url, options) + .then(resp => handleResponse(resp)) +} + +function authenticatePlexAccountController(req, res) { + const user = req.loggedInUser; + const { username, password } = req.body; + + return plexAuthenticate(username, password) + .then(plexUser => userRepository.linkPlexUserId(user.username, plexUser.id)) + .then(response => res.send({ + success: true, + message: "Successfully authenticated and linked plex account with seasoned request." + })) + .catch(error => handleError(error, res)) +} + +module.exports = authenticatePlexAccountController; From 9e2a0101c9d868533e66b0be4aeb9e63fb17e512 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Mon, 4 Nov 2019 00:44:57 +0100 Subject: [PATCH 02/19] Added new formdata pacakge --- seasoned_api/package.json | 1 + seasoned_api/yarn.lock | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/seasoned_api/package.json b/seasoned_api/package.json index 05daee0..7cf8324 100644 --- a/seasoned_api/package.json +++ b/seasoned_api/package.json @@ -22,6 +22,7 @@ "body-parser": "~1.18.2", "cross-env": "~5.1.4", "express": "~4.16.0", + "form-data": "^2.5.1", "jsonwebtoken": "^8.0.1", "km-moviedb": "^0.2.12", "node-cache": "^4.1.1", diff --git a/seasoned_api/yarn.lock b/seasoned_api/yarn.lock index 58e6b54..a022954 100644 --- a/seasoned_api/yarn.lock +++ b/seasoned_api/yarn.lock @@ -2777,7 +2777,7 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= -form-data@^2.3.1: +form-data@^2.3.1, form-data@^2.5.1: version "2.5.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== From 5d3a5dc8a49f6839e6851af9c1c556597a3e0ffc Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Mon, 4 Nov 2019 18:46:42 +0100 Subject: [PATCH 03/19] Removed unused console log --- seasoned_api/src/user/userRepository.js | 1 - 1 file changed, 1 deletion(-) diff --git a/seasoned_api/src/user/userRepository.js b/seasoned_api/src/user/userRepository.js index 76da283..c9f9f32 100644 --- a/seasoned_api/src/user/userRepository.js +++ b/seasoned_api/src/user/userRepository.js @@ -36,7 +36,6 @@ create(user) { * @returns {Promise} */ retrieveHash(user) { - console.log('retrieving hash for user', user) return this.database.get(this.queries.retrieveHash, user.username) .then(row => { assert(row, 'The user does not exist.'); From acc26a2f09cd40ac045587178c73a28ffabd1f51 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Mon, 4 Nov 2019 20:32:41 +0100 Subject: [PATCH 04/19] Renamed user history to search_history and fixed an issue where search history received the entire user object and not just the username --- seasoned_api/src/searchHistory/searchHistory.js | 16 +++++++++++----- seasoned_api/src/webserver/app.js | 2 +- .../webserver/controllers/search/multiSearch.js | 2 +- .../user/{history.js => search_history.js} | 0 4 files changed, 13 insertions(+), 7 deletions(-) rename seasoned_api/src/webserver/controllers/user/{history.js => search_history.js} (100%) diff --git a/seasoned_api/src/searchHistory/searchHistory.js b/seasoned_api/src/searchHistory/searchHistory.js index 4ebb899..c2d5a67 100644 --- a/seasoned_api/src/searchHistory/searchHistory.js +++ b/seasoned_api/src/searchHistory/searchHistory.js @@ -28,17 +28,23 @@ class SearchHistory { /** * Creates a new search entry in the database. - * @param {User} user a new user + * @param {String} username logged in user doing the search * @param {String} searchQuery the query the user searched for * @returns {Promise} */ - create(user, searchQuery) { - return Promise.resolve() - .then(() => this.database.run(this.queries.create, [searchQuery, user])) - .catch((error) => { + create(username, searchQuery) { + return this.database.run(this.queries.create, [searchQuery, username]) + .catch(error => { if (error.message.includes('FOREIGN')) { throw new Error('Could not create search history.'); } + + throw { + success: false, + status: 500, + message: 'An unexpected error occured', + source: 'database' + } }); } } diff --git a/seasoned_api/src/webserver/app.js b/seasoned_api/src/webserver/app.js index 0b0a57b..9f5ff24 100644 --- a/seasoned_api/src/webserver/app.js +++ b/seasoned_api/src/webserver/app.js @@ -53,7 +53,7 @@ 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/history', mustBeAuthenticated, require('./controllers/user/history.js')); +router.get('/v1/user/search_history', mustBeAuthenticated, require('./controllers/user/search_history.js')); router.get('/v1/user/requests', mustBeAuthenticated, require('./controllers/user/requests.js')); router.post('/v1/user/authenticate', mustBeAuthenticated, require('./controllers/user/authenticatePlexAccount.js')); diff --git a/seasoned_api/src/webserver/controllers/search/multiSearch.js b/seasoned_api/src/webserver/controllers/search/multiSearch.js index 0f9b05f..f0a3810 100644 --- a/seasoned_api/src/webserver/controllers/search/multiSearch.js +++ b/seasoned_api/src/webserver/controllers/search/multiSearch.js @@ -24,7 +24,7 @@ function multiSearchController(req, res) { const { query, page } = req.query; if (user) { - searchHistory.create(user, query) + searchHistory.create(user.username, query) } return tmdb.multiSearch(query, page) diff --git a/seasoned_api/src/webserver/controllers/user/history.js b/seasoned_api/src/webserver/controllers/user/search_history.js similarity index 100% rename from seasoned_api/src/webserver/controllers/user/history.js rename to seasoned_api/src/webserver/controllers/user/search_history.js From 601fc1d0de625d2d64d56a8f7c592a30e5c307f7 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Mon, 4 Nov 2019 22:57:59 +0100 Subject: [PATCH 05/19] Renamed search_history controller to searchHistory --- seasoned_api/src/webserver/app.js | 2 +- .../controllers/user/{search_history.js => searchHistory.js} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename seasoned_api/src/webserver/controllers/user/{search_history.js => searchHistory.js} (100%) diff --git a/seasoned_api/src/webserver/app.js b/seasoned_api/src/webserver/app.js index 9f5ff24..a6219cc 100644 --- a/seasoned_api/src/webserver/app.js +++ b/seasoned_api/src/webserver/app.js @@ -53,7 +53,7 @@ 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/search_history', mustBeAuthenticated, require('./controllers/user/search_history.js')); +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')); diff --git a/seasoned_api/src/webserver/controllers/user/search_history.js b/seasoned_api/src/webserver/controllers/user/searchHistory.js similarity index 100% rename from seasoned_api/src/webserver/controllers/user/search_history.js rename to seasoned_api/src/webserver/controllers/user/searchHistory.js From 977d05c6f28e15f298d8e05c73461f147d0f3f36 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Mon, 4 Nov 2019 22:58:42 +0100 Subject: [PATCH 06/19] Refactor. Responses should return error string in object key message not error. --- seasoned_api/src/webserver/middleware/mustBeAdmin.js | 4 ++-- seasoned_api/src/webserver/middleware/mustBeAuthenticated.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/seasoned_api/src/webserver/middleware/mustBeAdmin.js b/seasoned_api/src/webserver/middleware/mustBeAdmin.js index 042bd40..3fe9c50 100644 --- a/seasoned_api/src/webserver/middleware/mustBeAdmin.js +++ b/seasoned_api/src/webserver/middleware/mustBeAdmin.js @@ -6,7 +6,7 @@ const mustBeAdmin = (req, res, next) => { if (req.loggedInUser === undefined) { return res.status(401).send({ success: false, - error: 'You must be logged in.', + message: 'You must be logged in.', }); } else { database.get(`SELECT admin FROM user WHERE user_name IS ?`, req.loggedInUser.username) @@ -15,7 +15,7 @@ const mustBeAdmin = (req, res, next) => { if (isAdmin.admin == 0) { return res.status(401).send({ success: false, - error: 'You must be logged in as a admin.' + message: 'You must be logged in as a admin.' }) } }) diff --git a/seasoned_api/src/webserver/middleware/mustBeAuthenticated.js b/seasoned_api/src/webserver/middleware/mustBeAuthenticated.js index 17a8973..e1153ce 100644 --- a/seasoned_api/src/webserver/middleware/mustBeAuthenticated.js +++ b/seasoned_api/src/webserver/middleware/mustBeAuthenticated.js @@ -2,7 +2,7 @@ const mustBeAuthenticated = (req, res, next) => { if (req.loggedInUser === undefined) { return res.status(401).send({ success: false, - error: 'You must be logged in.', + message: 'You must be logged in.', }); } return next(); From 639f0ec17aa815cf90241682340f4115bd0ff42e Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Mon, 4 Nov 2019 23:04:33 +0100 Subject: [PATCH 07/19] Created middleware to check if the user has a plex user linked to seasoned account. If not respond with 403 meaning you did have a authorization key, but this is forbidden; explaining in the response message that no plex account user id was found for this user and to please authenticate their plex account at authenticate endpoint. --- seasoned_api/src/webserver/app.js | 1 + .../middleware/mustHaveAccountLinkedToPlex.js | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 seasoned_api/src/webserver/middleware/mustHaveAccountLinkedToPlex.js diff --git a/seasoned_api/src/webserver/app.js b/seasoned_api/src/webserver/app.js index a6219cc..10597f7 100644 --- a/seasoned_api/src/webserver/app.js +++ b/seasoned_api/src/webserver/app.js @@ -4,6 +4,7 @@ const bodyParser = require('body-parser'); const tokenToUser = require('./middleware/tokenToUser'); const mustBeAuthenticated = require('./middleware/mustBeAuthenticated'); const mustBeAdmin = require('./middleware/mustBeAdmin'); +const mustHaveAccountLinkedToPlex = require('./middleware/mustHaveAccountLinkedToPlex'); const configuration = require('src/config/configuration').getInstance(); const listController = require('./controllers/list/listController'); diff --git a/seasoned_api/src/webserver/middleware/mustHaveAccountLinkedToPlex.js b/seasoned_api/src/webserver/middleware/mustHaveAccountLinkedToPlex.js new file mode 100644 index 0000000..de1ae69 --- /dev/null +++ b/seasoned_api/src/webserver/middleware/mustHaveAccountLinkedToPlex.js @@ -0,0 +1,31 @@ +const establishedDatabase = require('src/database/database'); + +const mustHaveAccountLinkedToPlex = (req, res, next) => { + let database = establishedDatabase; + const loggedInUser = req.loggedInUser; + + if (loggedInUser === undefined) { + return res.status(401).send({ + success: false, + message: 'You must be logged in.', + }); + } else { + database.get(`SELECT plex_userid FROM user WHERE user_name IS ?`, loggedInUser.username) + .then(row => { + const plex_userid = row.plex_userid; + + if (plex_userid === null || plex_userid === undefined) { + return res.status(403).send({ + success: false, + message: 'No plex account user id found for your user. Please authenticate your plex account at /user/authenticate.' + }) + } else { + req.loggedInUser.plex_userid = plex_userid; + return next(); + } + }) + } + +}; + +module.exports = mustHaveAccountLinkedToPlex; From 265049798642380e4e31ff0eddc22a331152496d Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Mon, 4 Nov 2019 23:07:25 +0100 Subject: [PATCH 08/19] Tautulli will serve all the tautulli api calls. Mostly this will be used by user requests for getting stats related to the logged in user. WIP but currently watch time stats, plays by number of days, and view history has been implemented. TODO: - Error handling if tautulli request fails. - Filter the responses and/or limit them from tautulli - Handle parsing variable parameters from request --- seasoned_api/src/tautulli/tautulli.js | 48 +++++++++++ seasoned_api/src/webserver/app.js | 5 ++ .../webserver/controllers/user/viewHistory.js | 80 +++++++++++++++++++ 3 files changed, 133 insertions(+) create mode 100644 seasoned_api/src/tautulli/tautulli.js create mode 100644 seasoned_api/src/webserver/controllers/user/viewHistory.js diff --git a/seasoned_api/src/tautulli/tautulli.js b/seasoned_api/src/tautulli/tautulli.js new file mode 100644 index 0000000..311dfca --- /dev/null +++ b/seasoned_api/src/tautulli/tautulli.js @@ -0,0 +1,48 @@ +const fetch = require('node-fetch'); + +class Tautulli { + constructor(apiKey, ip, port) { + this.apiKey = apiKey; + this.ip = ip; + this.port = port; + } + + buildUrlWithCmdAndUserid(cmd, user_id) { + const url = new URL('api/v2', `http://${this.ip}:${this.port}`) + url.searchParams.append('apikey', this.apiKey) + url.searchParams.append('cmd', cmd) + url.searchParams.append('user_id', user_id) + + return url + } + + getPlaysByDays(plex_userid, days) { + const url = this.buildUrlWithCmdAndUserid('get_plays_by_date', plex_userid) + url.searchParams.append('time_range', days) + + return fetch(url.href) + .then(resp => resp.json()) + } + + watchTimeStats(plex_userid) { + const url = this.buildUrlWithCmdAndUserid('get_user_watch_time_stats', plex_userid) + url.searchParams.append('grouping', 0) + + return fetch(url.href) + .then(resp => resp.json()) +} + + viewHistory(plex_userid) { + const url = this.buildUrlWithCmdAndUserid('get_history', plex_userid) + + url.searchParams.append('start', 0) + url.searchParams.append('length', 50) + + console.log('fetching url', url.href) + + return fetch(url.href) + .then(resp => resp.json()) + } +} + +module.exports = Tautulli; diff --git a/seasoned_api/src/webserver/app.js b/seasoned_api/src/webserver/app.js index 10597f7..de38044 100644 --- a/seasoned_api/src/webserver/app.js +++ b/seasoned_api/src/webserver/app.js @@ -8,6 +8,7 @@ const mustHaveAccountLinkedToPlex = require('./middleware/mustHaveAccountLinkedT const configuration = require('src/config/configuration').getInstance(); const listController = require('./controllers/list/listController'); +const tautulli = require('./controllers/user/viewHistory.js'); // TODO: Have our raven router check if there is a value, if not don't enable raven. Raven.config(configuration.get('raven', 'DSN')).install(); @@ -58,6 +59,10 @@ router.get('/v1/user/search_history', mustBeAuthenticated, require('./controller router.get('/v1/user/requests', mustBeAuthenticated, require('./controllers/user/requests.js')); router.post('/v1/user/authenticate', mustBeAuthenticated, require('./controllers/user/authenticatePlexAccount.js')); +router.get('/v1/user/view_history', mustHaveAccountLinkedToPlex, tautulli.userViewHistoryController); +router.get('/v1/user/watch_time', mustHaveAccountLinkedToPlex, tautulli.watchTimeStatsController); +router.get('/v1/user/plays_by_day', mustHaveAccountLinkedToPlex, tautulli.getPlaysByDaysController); + /** * Seasoned */ diff --git a/seasoned_api/src/webserver/controllers/user/viewHistory.js b/seasoned_api/src/webserver/controllers/user/viewHistory.js new file mode 100644 index 0000000..f129cbe --- /dev/null +++ b/seasoned_api/src/webserver/controllers/user/viewHistory.js @@ -0,0 +1,80 @@ +const configuration = require('src/config/configuration').getInstance(); +const Tautulli = require('src/tautulli/tautulli'); +const tautulli = new Tautulli('', '', ); + +function handleError(error, res) { + const { status, message } = error; + + if (status && message) { + res.status(status).send({ success: false, message }) + } else { + console.log('caught view history controller error', error) + res.status(500).send({ message: 'An unexpected error occured while fetching view history'}) + } +} + +function watchTimeStatsController(req, res) { + const user = req.loggedInUser; + + tautulli.watchTimeStats(user.plex_userid) + .then(data => { + console.log('data', data, JSON.stringify(data.response.data)) + + return res.send({ + success: true, + data: data.response.data, + message: 'watch time successfully fetched from tautulli' + }) + }) +} + +function getPlaysByDaysController(req, res) { + const user = req.loggedInUser; + const days = req.query.days || undefined; + + if (days === undefined) { + return res.status(422).send({ + success: false, + message: "Missing parameter: days (number)" + }) + } + + tautulli.getPlaysByDays(user.plex_userid, days) + .then(data => res.send({ + success: true, + data: data.response.data + })) +} + + +function userViewHistoryController(req, res) { + const user = req.loggedInUser; + + console.log('user', user) + + + // TODO here we should check if we can init tau + // and then return 501 Not implemented + + tautulli.viewHistory(user.plex_userid) + .then(data => { + console.log('data', data, JSON.stringify(data.response.data.data)) + + + return res.send({ + success: true, + data: data.response.data.data, + message: 'view history successfully fetched from tautulli' + }) + }) + .catch(error => handleError(error)) + + + // const username = user.username; +} + +module.exports = { + watchTimeStatsController, + getPlaysByDaysController, + userViewHistoryController +}; From 510c01454994c1ceb27ef6802a0457e74209817a Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Mon, 11 Nov 2019 17:59:16 +0100 Subject: [PATCH 09/19] Added endpoint for getting plays grouped by days of week. --- seasoned_api/src/tautulli/tautulli.js | 12 ++++++++- seasoned_api/src/webserver/app.js | 1 + .../webserver/controllers/user/viewHistory.js | 26 +++++++++++++++++-- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/seasoned_api/src/tautulli/tautulli.js b/seasoned_api/src/tautulli/tautulli.js index 311dfca..42ebafc 100644 --- a/seasoned_api/src/tautulli/tautulli.js +++ b/seasoned_api/src/tautulli/tautulli.js @@ -16,9 +16,19 @@ class Tautulli { return url } - getPlaysByDays(plex_userid, days) { + getPlaysByDayOfWeek(plex_userid, days, y_axis) { + const url = this.buildUrlWithCmdAndUserid('get_plays_by_dayofweek', plex_userid) + url.searchParams.append('time_range', days) + url.searchParams.append('y_axis', y_axis) + + return fetch(url.href) + .then(resp => resp.json()) + } + + getPlaysByDays(plex_userid, days, y_axis) { const url = this.buildUrlWithCmdAndUserid('get_plays_by_date', plex_userid) url.searchParams.append('time_range', days) + url.searchParams.append('y_axis', y_axis) return fetch(url.href) .then(resp => resp.json()) diff --git a/seasoned_api/src/webserver/app.js b/seasoned_api/src/webserver/app.js index de38044..a7d74b3 100644 --- a/seasoned_api/src/webserver/app.js +++ b/seasoned_api/src/webserver/app.js @@ -62,6 +62,7 @@ router.post('/v1/user/authenticate', mustBeAuthenticated, require('./controllers router.get('/v1/user/view_history', mustHaveAccountLinkedToPlex, tautulli.userViewHistoryController); router.get('/v1/user/watch_time', mustHaveAccountLinkedToPlex, tautulli.watchTimeStatsController); router.get('/v1/user/plays_by_day', mustHaveAccountLinkedToPlex, tautulli.getPlaysByDaysController); +router.get('/v1/user/plays_by_dayofweek', mustHaveAccountLinkedToPlex, tautulli.getPlaysByDayOfWeekController); /** * Seasoned diff --git a/seasoned_api/src/webserver/controllers/user/viewHistory.js b/seasoned_api/src/webserver/controllers/user/viewHistory.js index f129cbe..52f08f8 100644 --- a/seasoned_api/src/webserver/controllers/user/viewHistory.js +++ b/seasoned_api/src/webserver/controllers/user/viewHistory.js @@ -28,9 +28,22 @@ function watchTimeStatsController(req, res) { }) } +function getPlaysByDayOfWeekController(req, res) { + const user = req.loggedInUser; + const { days, y_axis } = req.query; + + tautulli.getPlaysByDayOfWeek(user.plex_userid, days, y_axis) + .then(data => res.send({ + success: true, + data: data.response.data, + message: 'play by day of week successfully fetched from tautulli' + }) + ) +} + function getPlaysByDaysController(req, res) { const user = req.loggedInUser; - const days = req.query.days || undefined; + const { days, y_axis } = req.query; if (days === undefined) { return res.status(422).send({ @@ -39,7 +52,15 @@ function getPlaysByDaysController(req, res) { }) } - tautulli.getPlaysByDays(user.plex_userid, days) + const allowedYAxisDataType = ['plays', 'duration']; + if (!allowedYAxisDataType.includes(y_axis)) { + return res.status(422).send({ + success: false, + message: `Y axis parameter must be one of values: [${ allowedYAxisDataType }]` + }) + } + + tautulli.getPlaysByDays(user.plex_userid, days, y_axis) .then(data => res.send({ success: true, data: data.response.data @@ -76,5 +97,6 @@ function userViewHistoryController(req, res) { module.exports = { watchTimeStatsController, getPlaysByDaysController, + getPlaysByDayOfWeekController, userViewHistoryController }; From a5248f0631b04829882f378b72efb07318c1b90f Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Mon, 25 Nov 2019 23:38:52 +0100 Subject: [PATCH 10/19] TODO comment for login --- seasoned_api/src/webserver/controllers/user/login.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/seasoned_api/src/webserver/controllers/user/login.js b/seasoned_api/src/webserver/controllers/user/login.js index ada2bcc..b0f091c 100644 --- a/seasoned_api/src/webserver/controllers/user/login.js +++ b/seasoned_api/src/webserver/controllers/user/login.js @@ -8,6 +8,9 @@ const secret = configuration.get('authentication', 'secret'); const userSecurity = new UserSecurity(); const userRepository = new UserRepository(); +// TODO look to move some of the token generation out of the reach of the final "catch-all" +// catch including the, maybe sensitive, error message. + /** * Controller: Log in a user provided correct credentials. * @param {Request} req http request variable From c1b96e17cac13dd464e60352131874c704d90419 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Fri, 20 Dec 2019 21:45:31 +0100 Subject: [PATCH 11/19] Moved database row plex_userid from user to a new table settings. Currently includes plex_userid, emoji and darkmode with user_name as a foreign key to user.user_name. --- seasoned_api/src/database/schemas/setup.sql | 9 ++++++++- seasoned_api/src/database/schemas/teardown.sql | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/seasoned_api/src/database/schemas/setup.sql b/seasoned_api/src/database/schemas/setup.sql index a55e4e6..0d653d4 100644 --- a/seasoned_api/src/database/schemas/setup.sql +++ b/seasoned_api/src/database/schemas/setup.sql @@ -3,10 +3,17 @@ CREATE TABLE IF NOT EXISTS user ( password varchar(127), admin boolean DEFAULT 0, email varchar(127) UNIQUE, - plex_userid varchar(127) DEFAULT NULL, primary key (user_name) ); +CREATE TABLE IF NOT EXISTS settings ( + user_name varchar(127) UNIQUE, + dark_mode boolean DEFAULT 0, + plex_userid varchar(127) DEFAULT NULL, + emoji varchar(16) DEFAULT NULL, + foreign key(user_name) REFERENCES user(user_name) ON DELETE CASCADE +); + CREATE TABLE IF NOT EXISTS cache ( key varchar(255), value blob, diff --git a/seasoned_api/src/database/schemas/teardown.sql b/seasoned_api/src/database/schemas/teardown.sql index cf7e6e3..31f54b2 100644 --- a/seasoned_api/src/database/schemas/teardown.sql +++ b/seasoned_api/src/database/schemas/teardown.sql @@ -1,4 +1,5 @@ DROP TABLE IF EXISTS user; +DROP TABLE IF EXISTS settings; DROP TABLE IF EXISTS search_history; DROP TABLE IF EXISTS requests; DROP TABLE IF EXISTS request; From 9022853502fe7086b0c1a1d89b71c926f728830c Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Sun, 22 Dec 2019 12:43:12 +0100 Subject: [PATCH 12/19] Sql schema now has requested_by as a foreign key for user_name and on delete set null clause. --- seasoned_api/src/database/schemas/setup.sql | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/seasoned_api/src/database/schemas/setup.sql b/seasoned_api/src/database/schemas/setup.sql index 0d653d4..015d356 100644 --- a/seasoned_api/src/database/schemas/setup.sql +++ b/seasoned_api/src/database/schemas/setup.sql @@ -36,12 +36,13 @@ CREATE TABLE IF NOT EXISTS requests( year NUMBER, poster_path TEXT DEFAULT NULL, background_path TEXT DEFAULT NULL, - requested_by TEXT, + requested_by varchar(127) DEFAULT NULL, ip TEXT, date DATE DEFAULT CURRENT_TIMESTAMP, status CHAR(25) DEFAULT 'requested' NOT NULL, user_agent CHAR(255) DEFAULT NULL, - type CHAR(50) DEFAULT 'movie' + type CHAR(50) DEFAULT 'movie', + foreign key(requested_by) REFERENCES user(user_name) ON DELETE SET NULL ); CREATE TABLE IF NOT EXISTS request( From fedacf498e0f361acc2522724f59c10de7f40684 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Sun, 22 Dec 2019 12:44:45 +0100 Subject: [PATCH 13/19] Changed input parameter from name from user to username, cause we just get the username string. --- seasoned_api/src/request/request.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/seasoned_api/src/request/request.js b/seasoned_api/src/request/request.js index 380c12a..888fb88 100644 --- a/seasoned_api/src/request/request.js +++ b/seasoned_api/src/request/request.js @@ -86,11 +86,11 @@ class RequestRepository { * @param {tmdb} tmdb class of movie|show to add * @returns {Promise} */ - requestFromTmdb(tmdb, ip, user_agent, user) { + requestFromTmdb(tmdb, ip, user_agent, username) { return Promise.resolve() .then(() => this.database.get(this.queries.read, [tmdb.id, tmdb.type])) .then(row => assert.equal(row, undefined, 'Id has already been requested')) - .then(() => this.database.run(this.queries.add, [tmdb.id, tmdb.title, tmdb.year, tmdb.poster, tmdb.backdrop, user, ip, user_agent, tmdb.type])) + .then(() => this.database.run(this.queries.add, [tmdb.id, tmdb.title, tmdb.year, tmdb.poster, tmdb.backdrop, username, ip, user_agent, tmdb.type])) .catch((error) => { if (error.name === 'AssertionError' || error.message.endsWith('been requested')) { throw new Error('This id is already requested', error.message); From 720fb69648a9db22a3c7214237a04243ea25ba98 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Sun, 22 Dec 2019 12:45:01 +0100 Subject: [PATCH 14/19] Indentation --- seasoned_api/src/request/request.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/seasoned_api/src/request/request.js b/seasoned_api/src/request/request.js index 888fb88..43fe112 100644 --- a/seasoned_api/src/request/request.js +++ b/seasoned_api/src/request/request.js @@ -88,16 +88,16 @@ class RequestRepository { */ requestFromTmdb(tmdb, ip, user_agent, username) { return Promise.resolve() - .then(() => this.database.get(this.queries.read, [tmdb.id, tmdb.type])) - .then(row => assert.equal(row, undefined, 'Id has already been requested')) - .then(() => this.database.run(this.queries.add, [tmdb.id, tmdb.title, tmdb.year, tmdb.poster, tmdb.backdrop, username, ip, user_agent, tmdb.type])) - .catch((error) => { - if (error.name === 'AssertionError' || error.message.endsWith('been requested')) { - throw new Error('This id is already requested', error.message); - } - console.log('Error @ request.addTmdb:', error); - throw new Error('Could not add request'); - }); + .then(() => this.database.get(this.queries.read, [tmdb.id, tmdb.type])) + .then(row => assert.equal(row, undefined, 'Id has already been requested')) + .then(() => this.database.run(this.queries.add, [tmdb.id, tmdb.title, tmdb.year, tmdb.poster, tmdb.backdrop, username, ip, user_agent, tmdb.type])) + .catch((error) => { + if (error.name === 'AssertionError' || error.message.endsWith('been requested')) { + throw new Error('This id is already requested', error.message); + } + console.log('Error @ request.addTmdb:', error); + throw new Error('Could not add request'); + }); } /** From ddb7e7379d2ee903e43189329f5722f2014e609d Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Sun, 22 Dec 2019 13:14:12 +0100 Subject: [PATCH 15/19] Every instance of sqlite database should have foreign_keys constraints on --- seasoned_api/src/database/sqliteDatabase.js | 1 + 1 file changed, 1 insertion(+) diff --git a/seasoned_api/src/database/sqliteDatabase.js b/seasoned_api/src/database/sqliteDatabase.js index aee129a..fc5459e 100644 --- a/seasoned_api/src/database/sqliteDatabase.js +++ b/seasoned_api/src/database/sqliteDatabase.js @@ -6,6 +6,7 @@ class SqliteDatabase { constructor(host) { this.host = host; this.connection = new sqlite3.Database(this.host); + this.execute('pragma foreign_keys = on;'); this.schemaDirectory = path.join(__dirname, 'schemas'); } From f8847c62f2f31a1b7319b841326855279a6882a1 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Sun, 22 Dec 2019 13:17:20 +0100 Subject: [PATCH 16/19] UserRepository handles updating db settings better. Moved the plex_userid to settings to expanded with functions for updating and fetching settings, each with its own helper function towards the database. Since we had a linkPlexUserId function and we dont want plex_userid to be updated from the updatesettings function we moved unlinking to a separate endpoint and class function. Also a new controller and endpoints for getting and updating settings. --- seasoned_api/src/user/userRepository.js | 264 +++++++++++++----- seasoned_api/src/webserver/app.js | 7 +- .../user/authenticatePlexAccount.js | 18 +- .../webserver/controllers/user/settings.js | 42 +++ .../middleware/mustHaveAccountLinkedToPlex.js | 4 +- 5 files changed, 264 insertions(+), 71 deletions(-) create mode 100644 seasoned_api/src/webserver/controllers/user/settings.js 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; From 8eacde9ccc873e945c2e7748f42f6044b201c1b9 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Sun, 22 Dec 2019 13:36:13 +0100 Subject: [PATCH 17/19] Moved tautulli config settings to be fetched from our configuration (either env variable or conf/development.json. --- seasoned_api/conf/development.json.example | 5 +++++ seasoned_api/src/webserver/controllers/user/viewHistory.js | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/seasoned_api/conf/development.json.example b/seasoned_api/conf/development.json.example index 6f95f9a..cb421e7 100644 --- a/seasoned_api/conf/development.json.example +++ b/seasoned_api/conf/development.json.example @@ -11,6 +11,11 @@ "plex": { "ip": "" }, + "tautulli": { + "apiKey": "", + "ip": "", + "port": "" + }, "raven": { "DSN": "" }, diff --git a/seasoned_api/src/webserver/controllers/user/viewHistory.js b/seasoned_api/src/webserver/controllers/user/viewHistory.js index 52f08f8..1b7b4a4 100644 --- a/seasoned_api/src/webserver/controllers/user/viewHistory.js +++ b/seasoned_api/src/webserver/controllers/user/viewHistory.js @@ -1,6 +1,9 @@ const configuration = require('src/config/configuration').getInstance(); const Tautulli = require('src/tautulli/tautulli'); -const tautulli = new Tautulli('', '', ); +const apiKey = configuration.get('tautulli', 'apiKey'); +const ip = configuration.get('tautulli', 'ip'); +const port = configuration.get('tautulli', 'port'); +const tautulli = new Tautulli(apiKey, ip, port); function handleError(error, res) { const { status, message } = error; From 291bdf089cc53d8f4cebe32e2ec5714020c67b0c Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Sun, 22 Dec 2019 13:42:13 +0100 Subject: [PATCH 18/19] Forget to rename copied link function to unlink --- .../src/webserver/controllers/user/authenticatePlexAccount.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seasoned_api/src/webserver/controllers/user/authenticatePlexAccount.js b/seasoned_api/src/webserver/controllers/user/authenticatePlexAccount.js index f21a6f3..6e17b59 100644 --- a/seasoned_api/src/webserver/controllers/user/authenticatePlexAccount.js +++ b/seasoned_api/src/webserver/controllers/user/authenticatePlexAccount.js @@ -70,7 +70,7 @@ function link(req, res) { .catch(error => handleError(error, res)) } -function link(req, res) { +function unlink(req, res) { const user = req.loggedInUser; return userRepository.unlinkPlexUserId(user.username) From 5923cbf0512d34f857dd9d92195ba5ce8f0f148a Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Sun, 22 Dec 2019 13:49:16 +0100 Subject: [PATCH 19/19] Incorrect query parameter. Changed from (the not defined) plex_userid to username. --- seasoned_api/src/user/userRepository.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seasoned_api/src/user/userRepository.js b/seasoned_api/src/user/userRepository.js index 216387d..8c0221c 100644 --- a/seasoned_api/src/user/userRepository.js +++ b/seasoned_api/src/user/userRepository.js @@ -88,7 +88,7 @@ class UserRepository { */ unlinkPlexUserId(username) { return new Promise((resolve, reject) => { - this.database.run(this.queries.unlink, plexUserID) + this.database.run(this.queries.unlink, username) .then(row => resolve(row)) .catch(error => { // TODO log this unknown db error