Merge pull request #124 from KevinMidboe/feature/plex-authentication
Feature plex authentication
This commit is contained in:
@@ -11,6 +11,11 @@
|
|||||||
"plex": {
|
"plex": {
|
||||||
"ip": ""
|
"ip": ""
|
||||||
},
|
},
|
||||||
|
"tautulli": {
|
||||||
|
"apiKey": "",
|
||||||
|
"ip": "",
|
||||||
|
"port": ""
|
||||||
|
},
|
||||||
"raven": {
|
"raven": {
|
||||||
"DSN": ""
|
"DSN": ""
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"body-parser": "~1.18.2",
|
"body-parser": "~1.18.2",
|
||||||
"cross-env": "~5.1.4",
|
"cross-env": "~5.1.4",
|
||||||
"express": "~4.16.0",
|
"express": "~4.16.0",
|
||||||
|
"form-data": "^2.5.1",
|
||||||
"jsonwebtoken": "^8.2.0",
|
"jsonwebtoken": "^8.2.0",
|
||||||
"km-moviedb": "^0.2.12",
|
"km-moviedb": "^0.2.12",
|
||||||
"node-cache": "^4.1.1",
|
"node-cache": "^4.1.1",
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
CREATE TABLE IF NOT EXISTS user (
|
CREATE TABLE IF NOT EXISTS user (
|
||||||
user_name varchar(127) UNIQUE,
|
user_name varchar(127) UNIQUE,
|
||||||
password varchar(127),
|
password varchar(127),
|
||||||
email varchar(127) UNIQUE,
|
|
||||||
admin boolean DEFAULT 0,
|
admin boolean DEFAULT 0,
|
||||||
|
email varchar(127) UNIQUE,
|
||||||
primary key (user_name)
|
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 (
|
CREATE TABLE IF NOT EXISTS cache (
|
||||||
key varchar(255),
|
key varchar(255),
|
||||||
value blob,
|
value blob,
|
||||||
@@ -28,12 +36,13 @@ CREATE TABLE IF NOT EXISTS requests(
|
|||||||
year NUMBER,
|
year NUMBER,
|
||||||
poster_path TEXT DEFAULT NULL,
|
poster_path TEXT DEFAULT NULL,
|
||||||
background_path TEXT DEFAULT NULL,
|
background_path TEXT DEFAULT NULL,
|
||||||
requested_by TEXT,
|
requested_by varchar(127) DEFAULT NULL,
|
||||||
ip TEXT,
|
ip TEXT,
|
||||||
date DATE DEFAULT CURRENT_TIMESTAMP,
|
date DATE DEFAULT CURRENT_TIMESTAMP,
|
||||||
status CHAR(25) DEFAULT 'requested' NOT NULL,
|
status CHAR(25) DEFAULT 'requested' NOT NULL,
|
||||||
user_agent CHAR(255) DEFAULT 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(
|
CREATE TABLE IF NOT EXISTS request(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
DROP TABLE IF EXISTS user;
|
DROP TABLE IF EXISTS user;
|
||||||
|
DROP TABLE IF EXISTS settings;
|
||||||
DROP TABLE IF EXISTS search_history;
|
DROP TABLE IF EXISTS search_history;
|
||||||
DROP TABLE IF EXISTS requests;
|
DROP TABLE IF EXISTS requests;
|
||||||
DROP TABLE IF EXISTS request;
|
DROP TABLE IF EXISTS request;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ class SqliteDatabase {
|
|||||||
constructor(host) {
|
constructor(host) {
|
||||||
this.host = host;
|
this.host = host;
|
||||||
this.connection = new sqlite3.Database(this.host);
|
this.connection = new sqlite3.Database(this.host);
|
||||||
|
this.execute('pragma foreign_keys = on;');
|
||||||
this.schemaDirectory = path.join(__dirname, 'schemas');
|
this.schemaDirectory = path.join(__dirname, 'schemas');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -86,18 +86,18 @@ class RequestRepository {
|
|||||||
* @param {tmdb} tmdb class of movie|show to add
|
* @param {tmdb} tmdb class of movie|show to add
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
requestFromTmdb(tmdb, ip, user_agent, user) {
|
requestFromTmdb(tmdb, ip, user_agent, username) {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
.then(() => this.database.get(this.queries.read, [tmdb.id, tmdb.type]))
|
.then(() => this.database.get(this.queries.read, [tmdb.id, tmdb.type]))
|
||||||
.then(row => assert.equal(row, undefined, 'Id has already been requested'))
|
.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) => {
|
.catch((error) => {
|
||||||
if (error.name === 'AssertionError' || error.message.endsWith('been requested')) {
|
if (error.name === 'AssertionError' || error.message.endsWith('been requested')) {
|
||||||
throw new Error('This id is already requested', error.message);
|
throw new Error('This id is already requested', error.message);
|
||||||
}
|
}
|
||||||
console.log('Error @ request.addTmdb:', error);
|
console.log('Error @ request.addTmdb:', error);
|
||||||
throw new Error('Could not add request');
|
throw new Error('Could not add request');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -28,17 +28,23 @@ class SearchHistory {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new search entry in the database.
|
* 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
|
* @param {String} searchQuery the query the user searched for
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
create(user, searchQuery) {
|
create(username, searchQuery) {
|
||||||
return Promise.resolve()
|
return this.database.run(this.queries.create, [searchQuery, username])
|
||||||
.then(() => this.database.run(this.queries.create, [searchQuery, user]))
|
.catch(error => {
|
||||||
.catch((error) => {
|
|
||||||
if (error.message.includes('FOREIGN')) {
|
if (error.message.includes('FOREIGN')) {
|
||||||
throw new Error('Could not create search history.');
|
throw new Error('Could not create search history.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
status: 500,
|
||||||
|
message: 'An unexpected error occured',
|
||||||
|
source: 'database'
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
58
seasoned_api/src/tautulli/tautulli.js
Normal file
58
seasoned_api/src/tautulli/tautulli.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
@@ -2,64 +2,218 @@ const assert = require('assert');
|
|||||||
const establishedDatabase = require('src/database/database');
|
const establishedDatabase = require('src/database/database');
|
||||||
|
|
||||||
class UserRepository {
|
class UserRepository {
|
||||||
constructor(database) {
|
constructor(database) {
|
||||||
this.database = database || establishedDatabase;
|
this.database = database || establishedDatabase;
|
||||||
this.queries = {
|
this.queries = {
|
||||||
read: 'select * from user where lower(user_name) = lower(?)',
|
read: 'select * from user where lower(user_name) = lower(?)',
|
||||||
create: 'insert into user (user_name) values (?)',
|
create: 'insert into user (user_name) values (?)',
|
||||||
change: 'update user set password = ? where user_name = ?',
|
change: 'update user set password = ? where user_name = ?',
|
||||||
retrieveHash: 'select * from user where user_name = ?',
|
retrieveHash: 'select * from user where user_name = ?',
|
||||||
getAdminStateByUser: 'select admin from user where user_name = ?'
|
getAdminStateByUser: 'select admin from user 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.
|
* Create a user in a database.
|
||||||
* @param {User} user the user you want to create
|
* @param {User} user the user you want to create
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
create(user) {
|
create(user) {
|
||||||
return Promise.resolve()
|
return this.database.get(this.queries.read, user.username)
|
||||||
.then(() => this.database.get(this.queries.read, user.username))
|
.then(() => this.database.run(this.queries.create, user.username))
|
||||||
.then(() => this.database.run(this.queries.create, user.username))
|
.catch((error) => {
|
||||||
.catch((error) => {
|
if (error.name === 'AssertionError' || error.message.endsWith('user_name')) {
|
||||||
if (error.name === 'AssertionError' || error.message.endsWith('user_name')) {
|
throw new Error('That username is already registered');
|
||||||
throw new Error('That username is already registered');
|
}
|
||||||
}
|
throw Error(error)
|
||||||
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.'); });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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]));
|
|
||||||
}
|
|
||||||
|
|
||||||
checkAdmin(user) {
|
|
||||||
return this.database.get(this.queries.getAdminStateByUser, user.username).then((row) => {
|
|
||||||
return row.admin;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {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, 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 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;
|
module.exports = UserRepository;
|
||||||
|
|||||||
@@ -4,9 +4,13 @@ const bodyParser = require('body-parser');
|
|||||||
const tokenToUser = require('./middleware/tokenToUser');
|
const tokenToUser = require('./middleware/tokenToUser');
|
||||||
const mustBeAuthenticated = require('./middleware/mustBeAuthenticated');
|
const mustBeAuthenticated = require('./middleware/mustBeAuthenticated');
|
||||||
const mustBeAdmin = require('./middleware/mustBeAdmin');
|
const mustBeAdmin = require('./middleware/mustBeAdmin');
|
||||||
|
const mustHaveAccountLinkedToPlex = require('./middleware/mustHaveAccountLinkedToPlex');
|
||||||
const configuration = require('src/config/configuration').getInstance();
|
const configuration = require('src/config/configuration').getInstance();
|
||||||
|
|
||||||
const listController = require('./controllers/list/listController');
|
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.
|
// TODO: Have our raven router check if there is a value, if not don't enable raven.
|
||||||
Raven.config(configuration.get('raven', 'DSN')).install();
|
Raven.config(configuration.get('raven', 'DSN')).install();
|
||||||
@@ -53,8 +57,17 @@ app.use(function onError(err, req, res, next) {
|
|||||||
*/
|
*/
|
||||||
router.post('/v1/user', require('./controllers/user/register.js'));
|
router.post('/v1/user', require('./controllers/user/register.js'));
|
||||||
router.post('/v1/user/login', require('./controllers/user/login.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/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.get('/v1/user/requests', mustBeAuthenticated, require('./controllers/user/requests.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);
|
||||||
|
router.get('/v1/user/plays_by_day', mustHaveAccountLinkedToPlex, tautulli.getPlaysByDaysController);
|
||||||
|
router.get('/v1/user/plays_by_dayofweek', mustHaveAccountLinkedToPlex, tautulli.getPlaysByDayOfWeekController);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Seasoned
|
* Seasoned
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ function multiSearchController(req, res) {
|
|||||||
const { query, page } = req.query;
|
const { query, page } = req.query;
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
searchHistory.create(user, query)
|
searchHistory.create(user.username, query)
|
||||||
}
|
}
|
||||||
|
|
||||||
return tmdb.multiSearch(query, page)
|
return tmdb.multiSearch(query, page)
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
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 link(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))
|
||||||
|
}
|
||||||
|
|
||||||
|
function unlink(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
|
||||||
|
};
|
||||||
@@ -8,6 +8,9 @@ const secret = configuration.get('authentication', 'secret');
|
|||||||
const userSecurity = new UserSecurity();
|
const userSecurity = new UserSecurity();
|
||||||
const userRepository = new UserRepository();
|
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.
|
* Controller: Log in a user provided correct credentials.
|
||||||
* @param {Request} req http request variable
|
* @param {Request} req http request variable
|
||||||
|
|||||||
42
seasoned_api/src/webserver/controllers/user/settings.js
Normal file
42
seasoned_api/src/webserver/controllers/user/settings.js
Normal file
@@ -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
|
||||||
|
}
|
||||||
105
seasoned_api/src/webserver/controllers/user/viewHistory.js
Normal file
105
seasoned_api/src/webserver/controllers/user/viewHistory.js
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
const configuration = require('src/config/configuration').getInstance();
|
||||||
|
const Tautulli = require('src/tautulli/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;
|
||||||
|
|
||||||
|
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 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, y_axis } = req.query;
|
||||||
|
|
||||||
|
if (days === undefined) {
|
||||||
|
return res.status(422).send({
|
||||||
|
success: false,
|
||||||
|
message: "Missing parameter: days (number)"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
getPlaysByDayOfWeekController,
|
||||||
|
userViewHistoryController
|
||||||
|
};
|
||||||
@@ -6,7 +6,7 @@ const mustBeAdmin = (req, res, next) => {
|
|||||||
if (req.loggedInUser === undefined) {
|
if (req.loggedInUser === undefined) {
|
||||||
return res.status(401).send({
|
return res.status(401).send({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'You must be logged in.',
|
message: 'You must be logged in.',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
database.get(`SELECT admin FROM user WHERE user_name IS ?`, req.loggedInUser.username)
|
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) {
|
if (isAdmin.admin == 0) {
|
||||||
return res.status(401).send({
|
return res.status(401).send({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'You must be logged in as a admin.'
|
message: 'You must be logged in as a admin.'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ const mustBeAuthenticated = (req, res, next) => {
|
|||||||
if (req.loggedInUser === undefined) {
|
if (req.loggedInUser === undefined) {
|
||||||
return res.status(401).send({
|
return res.status(401).send({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'You must be logged in.',
|
message: 'You must be logged in.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return next();
|
return next();
|
||||||
|
|||||||
@@ -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 have your account linked to a plex account.',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
database.get(`SELECT plex_userid FROM settings 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;
|
||||||
@@ -2777,7 +2777,7 @@ forever-agent@~0.6.1:
|
|||||||
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
|
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
|
||||||
integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
|
integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
|
||||||
|
|
||||||
form-data@^2.3.1:
|
form-data@^2.3.1, form-data@^2.5.1:
|
||||||
version "2.5.1"
|
version "2.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4"
|
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4"
|
||||||
integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==
|
integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==
|
||||||
|
|||||||
Reference in New Issue
Block a user