This commit is contained in:
2022-03-06 10:27:09 +01:00
parent 4f7a22fff1
commit 5b6a2c2651
6 changed files with 295 additions and 236 deletions

View File

@@ -11,15 +11,15 @@ class Cache {
return new Promise((resolve, reject) => {
client.get(key, (error, reply) => {
if (reply == null) {
return reject()
return reject();
}
resolve(JSON.parse(reply))
})
})
resolve(JSON.parse(reply));
});
});
}
/**
/**
* Insert cache entry with key and value.
* @param {String} key of the cache entry
* @param {String} value of the cache entry
@@ -27,22 +27,25 @@ class Cache {
* @returns {Object}
*/
set(key, value, timeToLive = 10800) {
if (value == null || key == null)
return null
if (value == null || key == null) return null;
const json = JSON.stringify(value);
client.set(key, json, (error, reply) => {
if (reply == 'OK') {
if (reply == "OK") {
// successfully set value with key, now set TTL for key
client.expire(key, timeToLive, (e) => {
if (e)
console.error('Unexpected error while setting expiration for key:', key, '. Error:', error)
})
client.expire(key, timeToLive, e => {
if (e)
console.error(
"Unexpected error while setting expiration for key:",
key,
". Error:",
error
);
});
}
})
});
return value
return value;
}
}

View File

@@ -1,4 +1,4 @@
const fetch = require('node-fetch');
const fetch = require("node-fetch");
class Tautulli {
constructor(apiKey, ip, port) {
@@ -8,50 +8,66 @@ class Tautulli {
}
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)
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
return url;
}
logTautulliError(error) {
console.error("error fetching from tautulli");
throw error;
}
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)
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())
.catch(error => this.logTautulliError(error));
}
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)
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())
.catch(error => this.logTautulliError(error));
}
watchTimeStats(plex_userid) {
const url = this.buildUrlWithCmdAndUserid('get_user_watch_time_stats', plex_userid)
url.searchParams.append('grouping', 0)
const url = this.buildUrlWithCmdAndUserid(
"get_user_watch_time_stats",
plex_userid
);
url.searchParams.append("grouping", 0);
return fetch(url.href)
.then(resp => resp.json())
}
.catch(error => this.logTautulliError(error));
}
viewHistory(plex_userid) {
const url = this.buildUrlWithCmdAndUserid('get_history', plex_userid)
url.searchParams.append('start', 0)
url.searchParams.append('length', 50)
const url = this.buildUrlWithCmdAndUserid("get_history", plex_userid);
console.log('fetching url', url.href)
url.searchParams.append("start", 0);
url.searchParams.append("length", 50);
console.log("fetching url", url.href);
return fetch(url.href)
.then(resp => resp.json())
.catch(error => this.logTautulliError(error));
}
}

View File

@@ -1,219 +1,256 @@
const assert = require('assert');
const establishedDatabase = require('src/database/database');
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 = ?',
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 = ?'
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 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 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)
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');
.catch(error => {
if (
error.name === "AssertionError" ||
error.message.endsWith("user_name")
) {
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}
*/
* 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)
return this.database
.get(this.queries.retrieveHash, user.username)
.then(row => {
assert(row, 'The user does not exist.');
assert(row, "The user does not exist.");
return row.password;
})
.catch(err => { console.log(error); throw new Error('Unable to find your user.'); })
.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}
*/
* 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])
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}
*/
* 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])
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)
console.error("db error", error);
reject({
status: 500,
message: 'An unexpected error occured while linking plex and seasoned accounts',
source: 'seasoned database'
})
})
})
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}
*/
* 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)
this.database
.run(this.queries.unlink, username)
.then(row => resolve(row))
.catch(error => {
// TODO log this unknown db error
console.log('db error', error)
console.log("db error", error);
reject({
status: 500,
message: 'An unexpected error occured while unlinking plex and seasoned accounts',
source: 'seasoned database'
})
})
})
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}
*/
/**
* 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);
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}
*/
/**
* 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) => {
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.`)
console.debug(
`settings do not exist for user: ${username}. Creating settings entry.`
);
const userExistsWithUsername = await this.database.get('select * from user where user_name is ?', username)
const userExistsWithUsername = await this.database.get(
"select * from user where user_name is ?",
username
);
if (userExistsWithUsername !== undefined) {
try {
resolve(this.dbCreateSettings(username))
resolve(this.dbCreateSettings(username));
} catch (error) {
reject(error)
reject(error);
}
} else {
reject({
status: 404,
message: 'User not found, no settings to get'
})
message: "User not found, no settings to get"
});
}
}
resolve(row)
resolve(row);
})
.catch(error => {
console.error('Unexpected error occured while fetching settings for your account. Error:', 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'
})
})
})
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
/**
* 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 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'
}
})
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}
*/
/**
* Helper function for creating settings in the database
* @param {String} username
* @returns {Promsie}
*/
dbCreateSettings(username) {
return this.database.run(this.queries.createSettings, 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))
.catch(error =>
rejectUnexpectedDatabaseError(
"Unexpected error occured while creating settings",
503,
error
)
);
}
/**
* Helper function for updating settings in the database
* @param {String} username
* @returns {Promsie}
*/
/**
* 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)))
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 rejectUnexpectedDatabaseError = (
message,
status,
error,
reject = null
) => {
console.error(error);
const body = {
status,
message,
source: 'seasoned database'
}
source: "seasoned database"
};
if (reject == null) {
return new Promise((resolve, reject) => reject(body))
return new Promise((resolve, reject) => reject(body));
}
reject(body)
}
reject(body);
};
module.exports = UserRepository;

View File

@@ -1,10 +1,10 @@
const bcrypt = require('bcrypt');
const UserRepository = require('src/user/userRepository');
const bcrypt = require("bcrypt");
const UserRepository = require("src/user/userRepository");
class UserSecurity {
constructor(database) {
this.userRepository = new UserRepository(database);
}
this.userRepository = new UserRepository(database);
}
/**
* Create a new user in PlanFlix.
@@ -13,15 +13,15 @@ class UserSecurity {
* @returns {Promise}
*/
createNewUser(user, clearPassword) {
if (user.username.trim() === '') {
throw new Error('The username is empty.');
} else if (clearPassword.trim() === '') {
throw new Error('The password is empty.');
if (user.username.trim() === "") {
throw new Error("The username is empty.");
} else if (clearPassword.trim() === "") {
throw new Error("The password is empty.");
} else {
return Promise.resolve()
.then(() => this.userRepository.create(user))
.then(() => UserSecurity.hashPassword(clearPassword))
.then(hash => this.userRepository.changePassword(user, hash))
.then(hash => this.userRepository.changePassword(user, hash));
}
}
@@ -35,21 +35,22 @@ class UserSecurity {
return Promise.resolve()
.then(() => this.userRepository.retrieveHash(user))
.then(hash => UserSecurity.compareHashes(hash, clearPassword))
.catch(() => { throw new Error('Incorrect username or password.'); });
.catch(() => {
throw new Error("Incorrect username or password.");
});
}
/**
* Compare between a password and a hash password from database.
* @param {String} hash the hash password from database
* @param {String} clearPassword the user's password
* @returns {Promise}
*/
/**
* Compare between a password and a hash password from database.
* @param {String} hash the hash password from database
* @param {String} clearPassword the user's password
* @returns {Promise}
*/
static compareHashes(hash, clearPassword) {
return new Promise((resolve, reject) => {
bcrypt.compare(clearPassword, hash, (error, match) => {
if (match)
resolve()
reject()
if (match) resolve(true);
reject(false);
});
});
}
@@ -60,7 +61,7 @@ class UserSecurity {
* @returns {Promise}
*/
static hashPassword(clearPassword) {
return new Promise((resolve) => {
return new Promise(resolve => {
const saltRounds = 10;
bcrypt.hash(clearPassword, saltRounds, (error, hash) => {
resolve(hash);

View File

@@ -1,47 +1,52 @@
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 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 })
return 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'})
console.log("caught view history controller error", error);
return 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)
return 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'
})
message: "watch time successfully fetched from tautulli"
});
})
.catch(error => handleError(error, 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'
})
return 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"
})
)
.catch(error => handleError(error, res));
}
function getPlaysByDaysController(req, res) {
@@ -52,49 +57,46 @@ function getPlaysByDaysController(req, res) {
return res.status(422).send({
success: false,
message: "Missing parameter: days (number)"
})
});
}
const allowedYAxisDataType = ['plays', 'duration'];
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 }]`
})
message: `Y axis parameter must be one of values: [${allowedYAxisDataType}]`
});
}
tautulli.getPlaysByDays(user.plex_userid, days, y_axis)
.then(data => res.send({
return tautulli
.getPlaysByDays(user.plex_userid, days, y_axis)
.then(data =>
res.send({
success: true,
data: data.response.data
}))
})
)
.catch(error => handleError(error, res));
}
function userViewHistoryController(req, res) {
const user = req.loggedInUser;
const user = req.loggedInUser;
console.log('user', user)
// TODO here we should check if we can init tau
// and then return 501 Not implemented
return tautulli
.viewHistory(user.plex_userid)
.then(data => {
return res.send({
success: true,
data: data.response.data.data,
message: "view history successfully fetched from tautulli"
});
})
.catch(error => handleError(error, res));
// 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;
// const username = user.username;
}
module.exports = {

View File

@@ -1,11 +1,11 @@
const mustBeAuthenticated = (req, res, next) => {
if (req.loggedInUser === undefined) {
return res.status(401).send({
success: false,
message: 'You must be logged in.',
});
}
return next();
if (req.loggedInUser === undefined) {
return res.status(401).send({
success: false,
message: "You must be logged in."
});
}
return next();
};
module.exports = mustBeAuthenticated;